Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 10 additions & 4 deletions backend/app/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -353,14 +353,20 @@ def workspaces(path: str | None = None):
return safely(lambda: store.workspaces(path))


class BuildRunRequest(BaseModel):
output_locale: str = Field(default="", max_length=35, pattern=r"^[A-Za-z]{2,3}(?:-[A-Za-z0-9]{2,8})*$")


@app.post("/api/builds/{build_id}/runs")
def invoke_build(build_id: str):
return safely(lambda: store.invoke_remote_build(build_id))
def invoke_build(build_id: str, values: BuildRunRequest | None = None):
return safely(
lambda: store.invoke_remote_build(build_id, output_locale=values.output_locale if values else None)
)


@app.post("/api/builds/{build_id}/tests")
def test_build(build_id: str):
return safely(lambda: store.test_build(build_id))
def test_build(build_id: str, values: BuildRunRequest | None = None):
return safely(lambda: store.test_build(build_id, output_locale=values.output_locale if values else None))


@app.get("/api/build-tests/{session_id}")
Expand Down
1 change: 1 addition & 0 deletions backend/app/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,7 @@ class Run(BaseModel):
build_name: str | None = None
repository: str | None = None
supervisor_profile_name: str | None = None
output_locale: str | None = None
prompt_source: str | None = None
prompt_snapshot: str | None = None
execution_mode: Literal["run", "test"] = "run"
Expand Down
39 changes: 30 additions & 9 deletions backend/app/store.py
Original file line number Diff line number Diff line change
Expand Up @@ -2864,7 +2864,7 @@ def _write_prompt_template(
temporary.replace(CONFIG / "prompt-templates.yaml")
return template

def _assembled_prompt(self, build: dict[str, Any]) -> tuple[str, str]:
def _assembled_prompt(self, build: dict[str, Any], output_locale: str | None = None) -> tuple[str, str]:
"""Resolve the global manager contract and the build's evaluation policy."""
template_id = build.get("manager_template_id", "manager-default-v1")
template = next((item for item in self.prompt_templates() if item.get("id") == template_id), None)
Expand Down Expand Up @@ -2910,15 +2910,27 @@ def _assembled_prompt(self, build: dict[str, Any]) -> tuple[str, str]:
)
if part
)
resolved_output_locale = (
str(output_locale or self.application_settings()["manager_output_locale"]).strip() or "en"
)
output_language_name = {
"en": "English",
"ja": "Japanese",
"ko": "Korean",
}.get(resolved_output_locale.lower().split("-", 1)[0], resolved_output_locale)
assembled = "\n\n".join(
part
for part in (
operational.replace(MANAGER_PROMPT_SLOT, manager_policy).replace(
MANAGER_OUTPUT_LANGUAGE_SLOT,
"# Output language\n"
"Write all human-readable string values in the configured application language "
f"({self.application_settings()['manager_output_locale']}). "
"Keep JSON keys, field names, and required enum values exactly as specified.",
"Write every human-readable JSON string value in "
f"{output_language_name} ({resolved_output_locale}). "
"This applies even when the persona, rendered UI, or source evidence uses another language; "
"do not switch to the persona's language for behavior_trace, summaries, improvements, or issues. "
"If another instruction asks you to preserve an issue title, evidence, or reproduction, "
"preserve its facts and severity rather than its source-language wording. "
"Keep JSON keys, field names, required enum values, and verbatim UI strings quoted as evidence exactly as specified.",
),
f"# Evaluation context\nRepository: {build.get('repository', '')}\n{legacy_context}",
case_text,
Expand Down Expand Up @@ -4030,6 +4042,7 @@ def create_run(
supervisor_profile_name: str | None = None,
prompt_source: str | None = None,
prompt_snapshot: str | None = None,
output_locale: str | None = None,
loop_limit: int = 1,
timezone: str = "UTC",
schedule_enabled: bool = False,
Expand Down Expand Up @@ -4069,6 +4082,7 @@ def create_run(
build_name=build_name,
repository=repository,
supervisor_profile_name=supervisor_profile_name,
output_locale=output_locale,
prompt_source=prompt_source,
prompt_snapshot=prompt_snapshot,
execution_mode=execution_mode,
Expand Down Expand Up @@ -4695,7 +4709,7 @@ def supervisor_result(result: object) -> object:
supervisor_prompt = run.prompt_snapshot or ""
if run.build_id:
try:
_, supervisor_prompt = self._assembled_prompt(self.build(run.build_id))
_, supervisor_prompt = self._assembled_prompt(self.build(run.build_id), run.output_locale)
except ValueError:
# The original immutable run snapshot remains a safe fallback
# if an operator has made the prompt temporarily unreadable.
Expand Down Expand Up @@ -5257,14 +5271,19 @@ def emergency_stop(self) -> list[Run]:
stopped.append(self.cancel(run.id))
return stopped

def invoke_remote_build(self, build_id: str, execution_mode: str = "run") -> Run:
def invoke_remote_build(
self, build_id: str, execution_mode: str = "run", output_locale: str | None = None
) -> Run:
build = self.build(build_id)
executor = build.get("executor", {})
if not build.get("enabled"):
raise ValueError("This build is not enabled.")
if execution_mode not in {"run", "test"}:
raise ValueError("execution_mode must be run or test")
prompt_source, prompt_snapshot = self._assembled_prompt(build)
resolved_output_locale = (
str(output_locale or self.application_settings()["manager_output_locale"]).strip() or "en"
)
prompt_source, prompt_snapshot = self._assembled_prompt(build, resolved_output_locale)
if executor.get("type") != "remote-http":
return self.create_run(
build["runner_id"],
Expand All @@ -5273,6 +5292,7 @@ def invoke_remote_build(self, build_id: str, execution_mode: str = "run") -> Run
build_id=build["id"],
build_name=build["name"],
supervisor_profile_name=build.get("model_profile_name"),
output_locale=resolved_output_locale,
prompt_source=prompt_source,
prompt_snapshot=prompt_snapshot,
loop_limit=1 if execution_mode == "test" else int(build.get("run_limit", 1)),
Expand Down Expand Up @@ -5314,6 +5334,7 @@ def invoke_remote_build(self, build_id: str, execution_mode: str = "run") -> Run
build_id=build["id"],
build_name=build["name"],
supervisor_profile_name=build.get("model_profile_name"),
output_locale=resolved_output_locale,
execution_mode=execution_mode,
execution_type="invoke",
status="queued",
Expand All @@ -5330,11 +5351,11 @@ def invoke_remote_build(self, build_id: str, execution_mode: str = "run") -> Run
threading.Thread(target=self._execute_remote, args=(run.id, executor), daemon=True).start()
return run

def test_build(self, build_id: str) -> Run:
def test_build(self, build_id: str, output_locale: str | None = None) -> Run:
build = self.build(build_id)
if not build.get("enabled"):
raise ValueError("This build is not enabled.")
return self.invoke_remote_build(build_id, "test")
return self.invoke_remote_build(build_id, "test", output_locale=output_locale)

def _execute_remote(self, run_id: str, executor: dict[str, Any]) -> None:
run = self._load(run_id)
Expand Down
20 changes: 19 additions & 1 deletion backend/tests/test_api.py
Original file line number Diff line number Diff line change
Expand Up @@ -1525,5 +1525,23 @@ def test_manager_output_language_is_injected_into_the_assembled_prompt(tmp_path,
encoding="utf-8",
)
_, prompt = store._assembled_prompt({"manager_template_id": "manager-default-v1", "repository": "test"})
assert "configured application language (ja)" in prompt
assert "Japanese (ja)" in prompt
assert store_module.MANAGER_OUTPUT_LANGUAGE_SLOT not in prompt


def test_run_output_language_overrides_the_shared_application_setting(tmp_path, monkeypatch):
monkeypatch.setattr(store_module, "SETTINGS", tmp_path / "settings.json")
monkeypatch.setattr(store_module, "CONFIG", tmp_path / "config")
store = store_module.ConsoleStore()
store.save_application_settings({"manager_output_locale": "en"})
(tmp_path / "config").mkdir(exist_ok=True)
(tmp_path / "config" / "prompt-templates.yaml").write_text(
"- id: manager-default-v1\n name: Default\n version: 1\n content: Assess evidence.\n",
encoding="utf-8",
)
_, prompt = store._assembled_prompt(
{"manager_template_id": "manager-default-v1", "repository": "test"}, "ko"
)
assert "Korean (ko)" in prompt
assert "do not switch to the persona's language" in prompt
assert "facts and severity rather than its source-language wording" in prompt
2 changes: 1 addition & 1 deletion frontend/src/domain/models.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ export type BehaviorTrace = { persona_goal?:string; current_action?:string; next
export type SupervisorEvaluation = { score:number; approval:'approved'|'rejected'|'pending'; behavior_summary?:string; behavior_trace?:BehaviorTrace; summary:string }
export type SupervisorResult = { evaluation?:SupervisorEvaluation; improvements:Record<string,unknown>[]; reported_issues:Record<string,unknown>[] }
export type SupervisorRecord = { iteration:number; candidate_id?:string|null; status:'pending'|'completed'|'not_configured'|'invalid_response'|'failed'; prompt?:string; response?:SupervisorResult; error?:string; recorded_at?:string }
export type Run = { id:string; workflow_id:string; workflow_name:string;runner_version?:number;runner_source_sha256?:string; build_id?:string; build_name?:string; execution_mode?:'run'|'test'; execution_type?:'pipeline'|'invoke'; loop_limit?:number;start_iteration?:number;retry_of_run_id?:string;retry_mode?:'restart'|'resume';iteration_strategy?:'linear'|'score_select';candidates_per_iteration?:number;iteration_candidates?:{id:string;iteration:number;score?:number;status:string;selected?:boolean}[]; status:string; created_at?:string; updated_at?:string; finished_at?:string; current_phase?:string; pid?:number; last_pid?:number; telemetry_trace_id?:string; prompt_source?:string; prompt_snapshot?:string; supervisor_status?:'pending'|'completed'|'not_configured'|'invalid_response'|'failed'; supervisor_response?:SupervisorResult; supervisor_error?:string; supervisor_results?:SupervisorRecord[]; workflow_graph?:WorkflowGraphDefinition|null; step_results?:RunStepResult[]; proposed_improvements?:number; approved_improvements?:number; reported_issues?:number; approval_score?:number }
export type Run = { id:string; workflow_id:string; workflow_name:string;runner_version?:number;runner_source_sha256?:string; build_id?:string; build_name?:string; execution_mode?:'run'|'test'; execution_type?:'pipeline'|'invoke'; loop_limit?:number;start_iteration?:number;retry_of_run_id?:string;retry_mode?:'restart'|'resume';iteration_strategy?:'linear'|'score_select';candidates_per_iteration?:number;iteration_candidates?:{id:string;iteration:number;score?:number;status:string;selected?:boolean}[]; status:string; created_at?:string; updated_at?:string; finished_at?:string; current_phase?:string; pid?:number; last_pid?:number; telemetry_trace_id?:string; output_locale?:string; prompt_source?:string; prompt_snapshot?:string; supervisor_status?:'pending'|'completed'|'not_configured'|'failed'; supervisor_response?:SupervisorResult; supervisor_error?:string; supervisor_results?:SupervisorRecord[]; workflow_graph?:WorkflowGraphDefinition|null; step_results?:RunStepResult[]; proposed_improvements?:number; approved_improvements?:number; reported_issues?:number; approval_score?:number }
export type TelemetrySpan = { name:string; traceId:string; spanId:string; parentSpanId?:string|null; startTime?:number; endTime?:number; attributes?:Record<string,unknown>; status?:string; events?:{name:string;attributes?:Record<string,unknown>}[] }
export type RunTelemetry = { trace_id?:string; spans:TelemetrySpan[] }
export type PromptRevision = { iteration?:number; phase?:string; path:string; status:'initial'|'applied'|'blocked'|'rolled_back'|'unchanged'; reason?:string|null; recorded_at?:string; version_id?:string|null; run_id?:string|null; before?:string|null; after?:string|null; before_sha256?:string|null; after_sha256?:string|null }
Expand Down
5 changes: 0 additions & 5 deletions frontend/src/features/settings/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -75,11 +75,6 @@ export function SettingsPage({
.then((values) => {
setPrompt(values.manager_prompt_template);
setChatProfile(values.chat_model_profile_name);
if (values.manager_output_locale !== locale) {
return api<ApplicationSettings>("/api/application-settings", "PUT", {
manager_output_locale: locale,
});
}
})
.catch(() => pushToast("Unable to load operational prompt."));
}, [locale, pushToast]);
Expand Down
4 changes: 2 additions & 2 deletions frontend/src/main.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -173,14 +173,14 @@ export default function App() {
});
};
const invoke = (id: string) =>
api(`/api/builds/${id}/runs`, "POST")
api(`/api/builds/${id}/runs`, "POST", { output_locale: locale })
.then(() => {
room.setNotice(ui.evaluationStarted, "success");
room.refresh();
})
.catch((e) => room.setNotice(e.message));
const testBuild = (id: string) =>
api<Run>(`/api/builds/${id}/tests`, "POST")
api<Run>(`/api/builds/${id}/tests`, "POST", { output_locale: locale })
.then((run) => {
room.setNotice(ui.evaluationTestStarted, "success");
return run;
Expand Down
Loading