Merge remote-tracking branch 'web/master' into feat/pixel_art2
Project CI / Repository checks (pull_request) Failing after 51s
Project CI / Backend tests (pull_request) Successful in 4m9s
Project CI / Frontend tests (pull_request) Failing after 22s
Project CI / Native shell tests (pull_request) Successful in 12m34s

# Conflicts:
#	docs/project-memory/shared-memory/decision-log.md
This commit is contained in:
2026-07-31 11:37:57 +00:00
61 changed files with 2878 additions and 551 deletions
@@ -11,7 +11,7 @@
"autoCompactTokenLimit": 64000,
"toolOutputTokenLimit": 12000,
"requestTimeoutMs": 180000,
"maxRetries": 0,
"maxRetries": 2,
"retryBackoffMs": 500
},
"agentLlm": {},
@@ -120,7 +120,7 @@ export function deterministicLaneDefenseInitialHtml() {
*{box-sizing:border-box}body{margin:0;min-height:100vh;background:#f4f8ee;color:#18351f;font:16px system-ui,sans-serif}main{width:min(960px,100%);margin:auto;padding:18px}h1{margin:0 0 4px;font-size:clamp(28px,7vw,46px)}p{margin:4px 0 14px}.toolbar,.plants{display:flex;flex-wrap:wrap;gap:8px;margin:10px 0}button{min-height:44px;border:1px solid #315d35;background:#fff;color:#18351f;padding:9px 14px;font:inherit;font-weight:700;cursor:pointer}button:hover{background:#e6f3dc}.board{display:grid;gap:10px;background:#d9edc8;border:2px solid #315d35;padding:10px}.lane{display:grid;grid-template-columns:repeat(5,1fr);gap:6px}.cell{min-height:54px;background:#eef8e7}.status{font-weight:700;min-height:24px}#game{display:none;width:100%;height:auto;aspect-ratio:20/9;background:#18351f;border:2px solid #315d35}@media(max-width:520px){main{padding:12px}button{flex:1 1 44%}.cell{min-height:44px}}
</style>
</head>
<body><main>
<body><main><img src="assets/art-spritesheet.png" alt="Garden defenders" width="96" height="96">
<h1>灵露花园</h1><p>GENARRATIVE_REAL_E2E_VISIBLE</p><p>Goal: defend the garden and win every wave.</p>
<div class="toolbar"><button data-playtest-id="start">Start Game</button><button data-playtest-id="speed-up">Speed Up</button><button data-playtest-id="next-level">Next Level</button><button data-playtest-id="restart">Restart</button></div>
<div class="plants"><button data-playtest-id="defender-option">露华花</button><button id="thorn">棘刺芽</button></div>
@@ -367,10 +367,26 @@ pub(crate) fn has_game_creator_agent_llm_override(
config: &GameCreatorAppConfig,
agent_id: &str,
) -> bool {
config
.agent_llm
.get(agent_id)
.is_some_and(|patch| !is_empty_game_creator_llm_patch(patch))
config.agent_llm.get(agent_id).is_some_and(|patch| {
if is_empty_game_creator_llm_patch(patch) {
return false;
}
let only_canonical_reasoning_default = patch.api_key.is_none()
&& patch.base_url.is_none()
&& patch.model.is_none()
&& patch.api_kind.is_none()
&& patch.stream.is_none()
&& patch.web_search_enabled.is_none()
&& patch.context_window_tokens.is_none()
&& patch.auto_compact_token_limit.is_none()
&& patch.tool_output_token_limit.is_none()
&& patch.request_timeout_ms.is_none()
&& patch.max_retries.is_none()
&& patch.retry_backoff_ms.is_none()
&& patch.reasoning_effort.as_deref()
== game_creator_llm_agent_default_reasoning_effort(agent_id);
!only_canonical_reasoning_default
})
}
pub(crate) async fn request_agent_role_brief_with_config(
@@ -560,7 +560,32 @@ pub(in crate::agent) fn autonomous_manifest_dag_in_progress_at(
root: &Path,
) -> Result<bool, String> {
let manifest = read_manifest_for_project(root)?;
let seed_task_ids = new_game_creation_app_seed_tasks()
let source = read_game_creator_agent_runtime_at(root, GAME_CREATOR_PROJECT_SUPERVISOR_AGENT_ID)
.ok()
.filter(|runtime| {
runtime.state.run_profile == AGENT_RUNTIME_RUN_PROFILE_AUTONOMOUS_GAME_BUILD
&& agent_runtime_supervisor_source_is_trusted(&runtime.state.source)
})
.map(|runtime| runtime.state.source)
.or_else(|| {
let path = game_creator_agent_runtime_task_path(
root,
GAME_CREATOR_PROJECT_SUPERVISOR_AGENT_ID,
);
read_all_game_creator_agent_runtime_tasks(&path)
.ok()
.map(latest_game_creator_agent_runtime_tasks)
.and_then(|records| {
records.into_iter().rev().find_map(|record| {
(record.run_profile == AGENT_RUNTIME_RUN_PROFILE_AUTONOMOUS_GAME_BUILD
&& record.parent_run_id.is_none()
&& agent_runtime_supervisor_source_is_trusted(&record.source))
.then_some(record.source)
})
})
})
.unwrap_or_else(|| AGENT_RUNTIME_SUPERVISOR_GUI_SOURCE.to_string());
let seed_task_ids = autonomous_manifest_seed_tasks_for_source(&source)
.into_iter()
.map(|task| task.id)
.collect::<BTreeSet<_>>();
@@ -126,6 +126,11 @@ pub(in crate::agent) fn build_game_creator_agent_background_tool_plan_request(
system_prompt.push_str(
"\n\n当前 Run Profile 为 autonomous-game-build。不得调用 user.input_request,也不得为了等待确认而中断;对不改变核心目标的缺失细节,直接采用可逆、保守且可试玩的默认值。只使用当前 autoTools 推进项目内实现、委派和验证,不得请求 project.git_commit、command.exec、command.start、command.stdin、command.terminate 或其他仍需确认的动作。Project Supervisor 必须持续编排到最小可玩闭环通过 Runtime 完成门禁;专业 Agent 必须完成自己的合同并把结果交回父 Run。",
);
if agent_id == GAME_CREATOR_PROJECT_SUPERVISOR_AGENT_ID {
system_prompt.push_str(
"\n\nautonomous-game-build 的正式 manifest 任务图是唯一首轮专业执行链。不得在 manifest 之前另行创建 code-prototype、quality-review、art-director、design-foundation 或 art-asset-plan 的首批 agent.delegate;这些角色会由 Runtime 按 manifest 依赖顺序调度。没有待认领的显式返工合同时也不得额外委派。请直接推进/观察 manifest,Runtime 会在你尝试收束时调度 ready task,并在任务图完成前阻止最终交付。",
);
}
system_prompt.push_str(&format!(
"\n\n自主构建专业 Agent 在首次项目修改前最多允许 {AGENT_RUNTIME_AUTONOMOUS_PRE_MUTATION_LOOP_LIMIT} 轮 planning 探索。达到上限后,本响应必须直接调用 file.write、file.patch、file.delete、project.patchset、project.restore、canvas.asset_generate 等实际项目修改工具;若当前专业合同确实只要求只读验收,则必须调用 respond_to_user 交付结论。不得继续只调用 update_agent_plan、读取、搜索、状态查询或空验证。"
));
@@ -100,6 +100,16 @@ pub(crate) const AGENT_RUNTIME_ISOLATED_CHILD_SOURCE: &str = "agent-isolated-chi
pub(crate) const AGENT_RUNTIME_ISOLATED_JOIN_SOURCE: &str = "agent-isolated-join";
pub(crate) const AGENT_RUNTIME_SUPERVISOR_GUI_SOURCE: &str = "project-supervisor-gui";
pub(crate) const AGENT_RUNTIME_SUPERVISOR_CLI_SOURCE: &str = "project-supervisor-cli";
pub(crate) const AGENT_RUNTIME_SUPERVISOR_GAME_CHAT_SOURCE: &str = "project-supervisor-game-chat";
pub(crate) fn agent_runtime_supervisor_source_is_trusted(source: &str) -> bool {
matches!(
source.trim(),
AGENT_RUNTIME_SUPERVISOR_GUI_SOURCE
| AGENT_RUNTIME_SUPERVISOR_CLI_SOURCE
| AGENT_RUNTIME_SUPERVISOR_GAME_CHAT_SOURCE
)
}
pub(super) const AGENT_RUNTIME_RUN_PROFILE_BINDING_SCHEMA_VERSION: &str =
"game-creator-run-profile-binding.v1";
pub(super) const AGENT_RUNTIME_AUTONOMOUS_COMPLETION_CONTRACT_SCHEMA_VERSION: &str =
@@ -761,10 +761,7 @@ pub(crate) fn resolve_game_creator_agent_runtime_retry_configuration_at(
if binding.parent_run_id.is_some()
|| binding.root_agent_id != GAME_CREATOR_PROJECT_SUPERVISOR_AGENT_ID
|| binding.root_run_id != task.run_id
|| !matches!(
binding.source.as_str(),
AGENT_RUNTIME_SUPERVISOR_GUI_SOURCE | AGENT_RUNTIME_SUPERVISOR_CLI_SOURCE
)
|| !agent_runtime_supervisor_source_is_trusted(&binding.source)
{
return Err("自主构建 Agent Runtime 重试绑定不是可信 Supervisor 根 Run".to_string());
}
@@ -1460,6 +1460,7 @@ pub(in crate::agent) async fn run_game_creator_agent_background_task_pass_with_c
let mut steered_during_actions = false;
let mut provider_batch_superseded = false;
let mut preview_infrastructure_blocker = None;
let mut parallel_batch_consumed_until = 0_usize;
for (action_index, action) in plan
.actions
@@ -2502,6 +2503,11 @@ pub(in crate::agent) async fn run_game_creator_agent_background_task_pass_with_c
if stop_game_creator_agent_runtime_if_cancel_requested(&root, &mut runtime) {
return AgentBackgroundTaskOutcome::Finished;
}
if let Some(failure_kind) = agent_runtime_preview_infrastructure_blocker(&observation) {
preview_infrastructure_blocker = Some(failure_kind);
provider_batch_superseded = true;
break;
}
if resumed_provider_batch.is_some() && observation.status != "ok" {
provider_batch_superseded = true;
break;
@@ -2672,6 +2678,17 @@ pub(in crate::agent) async fn run_game_creator_agent_background_task_pass_with_c
return AgentBackgroundTaskOutcome::NeedsReconciliation;
}
}
if let Some(failure_kind) = preview_infrastructure_blocker {
return fail_game_creator_agent_background_context_at(
&root,
&agent_id,
&session_id,
runtime,
&format!(
"preview-infrastructure-unavailable: 浏览器验证基础设施不可用({failure_kind}),已停止当前 run,避免在同一 revision 重复请求 Provider 和启动浏览器"
),
);
}
if checkpoint == AgentRuntimeContextCheckpoint::Stalled {
context_stalled = true;
break 'agent_loop;
@@ -119,10 +119,7 @@ pub(crate) fn start_game_creator_supervisor_background_task_for_session_at(
source: &str,
run_profile: &str,
) -> Result<AgentRuntimeResult, String> {
if !matches!(
source,
AGENT_RUNTIME_SUPERVISOR_GUI_SOURCE | AGENT_RUNTIME_SUPERVISOR_CLI_SOURCE
) {
if !agent_runtime_supervisor_source_is_trusted(source) {
return Err("Project Supervisor 提交 source 不受信任".to_string());
}
normalize_agent_runtime_run_profile(Some(run_profile))?;
@@ -641,10 +638,7 @@ fn current_autonomous_game_build_root_task_at(
&& record.run_profile == AGENT_RUNTIME_RUN_PROFILE_AUTONOMOUS_GAME_BUILD
&& record.parent_agent_id.is_none()
&& record.parent_run_id.is_none()
&& matches!(
record.source.as_str(),
AGENT_RUNTIME_SUPERVISOR_GUI_SOURCE | AGENT_RUNTIME_SUPERVISOR_CLI_SOURCE
)
&& agent_runtime_supervisor_source_is_trusted(&record.source)
&& seen_run_ids.insert(record.run_id.clone())
{
root_run_ids.push(record.run_id.clone());
@@ -658,8 +652,25 @@ fn current_autonomous_game_build_root_task_at(
.find(|record| record.run_id == *current_run_id))
}
fn autonomous_manifest_ready_task_ids(tasks: &[GameCreationAppTaskState]) -> Vec<String> {
new_game_creation_app_seed_tasks()
pub(in crate::agent) fn autonomous_manifest_seed_tasks_for_source(
source: &str,
) -> Vec<GameCreationAppTaskState> {
let seed_tasks = new_game_creation_app_seed_tasks();
if source.trim() == AGENT_RUNTIME_SUPERVISOR_GAME_CHAT_SOURCE {
seed_tasks
.into_iter()
.take_while(|task| task.id != "publish-strategy")
.collect()
} else {
seed_tasks
}
}
fn autonomous_manifest_ready_task_ids(
tasks: &[GameCreationAppTaskState],
source: &str,
) -> Vec<String> {
autonomous_manifest_seed_tasks_for_source(source)
.into_iter()
.filter_map(|seed_task| {
let task = tasks.iter().find(|task| task.id == seed_task.id)?;
@@ -893,9 +904,17 @@ pub(crate) fn schedule_autonomous_game_build_ready_tasks_at(
.count();
let available = 3usize.saturating_sub(active_count);
let seed_task_order = new_game_creation_app_seed_tasks();
let allowed_seed_task_ids =
autonomous_manifest_seed_tasks_for_source(&parent_binding.source)
.into_iter()
.map(|task| task.id)
.collect::<BTreeSet<_>>();
let mut candidates = Vec::new();
for seed_task in &seed_task_order {
if !allowed_seed_task_ids.contains(&seed_task.id) {
continue;
}
let Some(task) = manifest.tasks.iter().find(|task| task.id == seed_task.id) else {
continue;
};
@@ -917,7 +936,7 @@ pub(crate) fn schedule_autonomous_game_build_ready_tasks_at(
candidates.push((task.clone(), false));
}
}
for task_id in autonomous_manifest_ready_task_ids(&manifest.tasks)
for task_id in autonomous_manifest_ready_task_ids(&manifest.tasks, &parent_binding.source)
.into_iter()
.take(limit.min(available))
{
@@ -1183,9 +1202,11 @@ pub(in crate::agent) fn project_autonomous_manifest_ready_task_terminal_at_locke
}),
)?;
let completed = status == GameCreationAppTaskStatus::Completed;
let game_chat_single_round =
root_parent_binding.source == AGENT_RUNTIME_SUPERVISOR_GAME_CHAT_SOURCE;
let root = root.to_path_buf();
tauri::async_runtime::spawn(async move {
if completed {
if completed && !game_chat_single_round {
if let Err(error) = schedule_autonomous_game_build_ready_tasks_at(
&root,
&parent_agent_id,
@@ -1242,9 +1263,17 @@ pub(in crate::agent) fn render_autonomous_manifest_ready_task_background_prompt(
} else {
""
};
return format!(
let code_visual_asset_requirement = if task.id == "code-prototype"
&& editor_api_key_is_configured()
{
" External Editor API 已配置时,必须先调用 asset.list 确认 assets/art-spritesheet.png 已登记且 source.kind=canvas、资源有效;game/index.html 必须实际通过 HTML、CSS background 或 Canvas drawImage 引用 assets/art-spritesheet.png 作为游戏 UI 素材,不得只用 emoji、色块、CSS 绘图或占位文本冒充。"
} else {
""
};
let owner_prompt = format!(
"{base}\n\n这是 autonomous-game-build 的正式 owner 写入任务。必须实际生成并写入非空正式产物:{paths};JSON 文件必须是可解析 JSONcode-prototype 的 game/index.html 不能沿用初始化占位。{publish_package_requirement}任务声明中的视觉图片继续按现有 visual gate 生成、登记并验收。完成修改后按当前 run 的验证门完成验证并直接交付结论;不要调用 task.updateRuntime 会在子 Run 终态后幂等投影 manifest。"
);
return format!("{owner_prompt}{code_visual_asset_requirement}");
}
format!(
"{base}\n\n这是 autonomous-game-build 的只读协调任务,不要修改项目文件,也不要为了 manifest 内部回执路径写入 memory/、game/、assets/ 或 exports/。只读取当前项目事实,完成方向协调、审查或验收并直接交付结论;不要调用 task.updateRuntime 会在子 Run 终态后幂等投影 manifest。"
@@ -6,6 +6,7 @@ pub(crate) fn autonomous_game_build_root_run_active_at(root: &Path) -> bool {
if read_game_creator_agent_runtime_at(root, GAME_CREATOR_PROJECT_SUPERVISOR_AGENT_ID).is_ok_and(
|runtime| {
runtime.state.run_profile == AGENT_RUNTIME_RUN_PROFILE_AUTONOMOUS_GAME_BUILD
&& agent_runtime_supervisor_source_is_trusted(&runtime.state.source)
&& !matches!(
runtime.state.phase.as_str(),
"completed" | "failed" | "cancelled" | "budget-exhausted"
@@ -22,10 +23,7 @@ pub(crate) fn autonomous_game_build_root_run_active_at(root: &Path) -> bool {
tasks.into_iter().any(|task| {
task.run_profile == AGENT_RUNTIME_RUN_PROFILE_AUTONOMOUS_GAME_BUILD
&& task.parent_run_id.is_none()
&& matches!(
task.source.as_str(),
AGENT_RUNTIME_SUPERVISOR_GUI_SOURCE | AGENT_RUNTIME_SUPERVISOR_CLI_SOURCE
)
&& agent_runtime_supervisor_source_is_trusted(&task.source)
&& matches!(
task.status.as_str(),
"pending"
@@ -383,6 +381,42 @@ fn autonomous_manifest_owner_artifact_gaps_at(
Ok(gaps)
}
fn autonomous_code_prototype_art_asset_reference_gap_at(
root: &Path,
task_id: &str,
) -> Result<Option<String>, String> {
if task_id != "code-prototype" || !editor_api_key_is_configured() {
return Ok(None);
}
let manifest = read_manifest_for_project(root)?;
if let Err(error) = validate_manifest_required_visual_asset(root, &manifest, "art-asset-plan") {
return Ok(Some(format!(
"assets/art-spritesheet.png(canvas-registration-invalid:{})",
sanitize_agent_runtime_text(&error, 240)
)));
}
let Some((_, html)) = read_autonomous_evidence_file_at(
root,
AGENT_RUNTIME_GAME_INDEX_PATH,
"autonomous code-prototype 游戏入口",
AGENT_RUNTIME_AUTONOMOUS_GAME_INDEX_MAX_BYTES,
)?
else {
return Ok(Some(
"game/index.html(missing-art-spritesheet-reference)".to_string(),
));
};
if !html
.windows(b"assets/art-spritesheet.png".len())
.any(|window| window == b"assets/art-spritesheet.png")
{
return Ok(Some(
"game/index.html(missing-art-spritesheet-reference)".to_string(),
));
}
Ok(None)
}
pub(in crate::agent) fn game_creation_app_task_status_label(
status: &GameCreationAppTaskStatus,
) -> String {
@@ -397,7 +431,13 @@ fn autonomous_manifest_parent_completion_gaps_at(
contract: &AgentRuntimeAutonomousCompletionContract,
) -> Result<(Vec<String>, Vec<AutonomousManifestArtifactGap>), String> {
let manifest = read_manifest_for_project(root)?;
let seed_tasks = new_game_creation_app_seed_tasks();
let binding = read_game_creator_agent_runtime_run_profile_binding(
root,
&contract.agent_id,
&contract.run_id,
)?
.ok_or_else(|| "自主构建根 Supervisor Run 缺少 Run Profile 绑定".to_string())?;
let seed_tasks = crate::agent::autonomous_manifest_seed_tasks_for_source(&binding.source);
let mut missing_tasks = Vec::new();
let mut missing_paths = Vec::new();
for seed_task in &seed_tasks {
@@ -416,6 +456,13 @@ fn autonomous_manifest_parent_completion_gaps_at(
contract.baseline_index_sha256.as_deref(),
&contract.baseline_artifacts,
)?);
if seed_task.id == "code-prototype" {
if let Some(gap) =
autonomous_code_prototype_art_asset_reference_gap_at(root, &seed_task.id)?
{
missing_paths.push(AutonomousManifestArtifactGap::new(gap));
}
}
}
missing_paths.sort_by(|left, right| left.summary.cmp(&right.summary));
missing_paths.dedup_by(|left, right| left.summary == right.summary);
@@ -464,6 +511,102 @@ fn autonomous_manifest_ready_task_completion_blocker_at_locked(
),
));
}
if state.agent_id == "preview-readiness" {
let revision = match read_game_creator_agent_runtime_project_revision(root) {
Ok(revision) => revision,
Err(error) => {
return Some(autonomous_completion_blocker(
"preview-readiness 无法读取当前项目 revision",
error,
));
}
};
let gate = match read_game_creator_agent_runtime_verification_gate(
root,
&state.agent_id,
&state.run_id,
) {
Ok(gate) => gate,
Err(error) => {
return Some(autonomous_completion_blocker(
"preview-readiness 静态验证凭证不可用",
error,
));
}
};
if gate.last_verification_tool.as_deref() != Some("game.static_smoke")
|| gate.last_verification_status.as_deref()
!= Some(AGENT_RUNTIME_VERIFICATION_STATUS_PASSED)
|| gate.verified_revision != Some(revision.revision)
{
return Some(autonomous_completion_blocker(
"preview-readiness 尚未通过当前 revision 的 game.static_smoke",
format!(
"currentRevision={}, verifiedRevision={}",
revision.revision,
gate.verified_revision
.map(|value| value.to_string())
.unwrap_or_else(|| "none".to_string())
),
));
}
}
if state.agent_id == "preview-playtest" {
let contract = match autonomous_playtest_completion_contract_for_state_at(root, state) {
Ok(Some(contract)) => contract,
Ok(None) => {
return Some(autonomous_completion_blocker(
"preview-playtest 缺少自主试玩完成合同",
"当前 child run 无法绑定父 Supervisor 的试玩场景与 revision。",
));
}
Err(error) => {
return Some(autonomous_completion_blocker(
"preview-playtest 自主试玩完成合同不可用",
error,
));
}
};
let revision = match read_game_creator_agent_runtime_project_revision(root) {
Ok(revision) => revision,
Err(error) => {
return Some(autonomous_completion_blocker(
"preview-playtest 无法读取当前项目 revision",
error,
));
}
};
let receipt = match read_autonomous_playtest_receipt(root, &contract) {
Ok(Some(receipt)) => receipt,
Ok(None) => {
return Some(autonomous_completion_blocker(
"preview-playtest 尚未形成成功浏览器试玩回执",
"必须由 preview.validate 在当前 revision 生成 passed report 与桌面、移动截图。",
));
}
Err(error) => {
return Some(autonomous_completion_blocker(
"preview-playtest 浏览器试玩回执不可用",
error,
));
}
};
if receipt.revision != revision.revision {
return Some(autonomous_completion_blocker(
"preview-playtest 浏览器试玩回执不属于当前 revision",
format!(
"receiptRevision={}, currentRevision={}",
receipt.revision, revision.revision
),
));
}
if let Err(error) = verify_autonomous_playtest_evidence_files_at(root, &receipt) {
return Some(autonomous_completion_blocker(
"preview-playtest 浏览器试玩证据复核未通过",
error,
));
}
}
let parent_contract = match read_autonomous_completion_contract(
root,
binding
@@ -500,6 +643,24 @@ fn autonomous_manifest_ready_task_completion_blocker_at_locked(
));
}
};
match autonomous_code_prototype_art_asset_reference_gap_at(root, &state.agent_id) {
Ok(Some(gap)) => {
return Some(autonomous_completion_blocker(
"code-prototype 必须实际使用平台生成的美术资源",
format!(
"task={} missingPaths={},请先通过 asset.list 核对 Canvas 来源并在 game/index.html 中引用 assets/art-spritesheet.png",
state.agent_id, gap
),
));
}
Ok(None) => {}
Err(error) => {
return Some(autonomous_completion_blocker(
"code-prototype 美术资源引用无法安全核对",
error,
));
}
}
if gaps.is_empty() {
return None;
}
@@ -758,6 +919,7 @@ pub(in crate::agent) fn validate_autonomous_completion_contract(
|| binding.parent_run_id.is_some()
|| binding.root_agent_id != contract.agent_id
|| binding.root_run_id != contract.run_id
|| !agent_runtime_supervisor_source_is_trusted(&binding.source)
{
return Err("自主构建完成合同与根 Supervisor Run 不匹配".to_string());
}
@@ -797,10 +959,7 @@ pub(in crate::agent) fn ensure_autonomous_completion_contract_for_task_at(
return Ok(());
}
if task.agent_id != GAME_CREATOR_PROJECT_SUPERVISOR_AGENT_ID
|| !matches!(
task.source.as_str(),
AGENT_RUNTIME_SUPERVISOR_GUI_SOURCE | AGENT_RUNTIME_SUPERVISOR_CLI_SOURCE
)
|| !agent_runtime_supervisor_source_is_trusted(&task.source)
{
return Err("自主构建完成合同只允许可信根 Supervisor Run".to_string());
}
@@ -1201,6 +1360,35 @@ pub(in crate::agent) fn autonomous_completion_contract_for_state_at(
Ok(Some(contract))
}
pub(in crate::agent) fn autonomous_playtest_completion_contract_for_state_at(
root: &Path,
state: &AgentRuntimeState,
) -> Result<Option<AgentRuntimeAutonomousCompletionContract>, String> {
if let Some(contract) = autonomous_completion_contract_for_state_at(root, state)? {
return Ok(Some(contract));
}
if state.run_profile != AGENT_RUNTIME_RUN_PROFILE_AUTONOMOUS_GAME_BUILD {
return Ok(None);
}
let binding =
read_game_creator_agent_runtime_run_profile_binding(root, &state.agent_id, &state.run_id)?
.ok_or_else(|| "自主试玩 child Runtime 缺少 Run Profile 绑定".to_string())?;
if binding.parent_agent_id.as_deref() != Some(binding.root_agent_id.as_str())
|| binding.parent_run_id.as_deref() != Some(binding.root_run_id.as_str())
{
return Err("自主试玩只接受根 Supervisor 的直接 manifest child".to_string());
}
let contract =
read_autonomous_completion_contract(root, &binding.root_agent_id, &binding.root_run_id)?
.ok_or_else(|| "自主试玩 child Runtime 缺少根 Supervisor 完成合同".to_string())?;
if binding.parent_binding_fingerprint.as_deref()
!= Some(contract.run_profile_binding_fingerprint.as_str())
{
return Err("自主试玩 child Runtime 与根 Supervisor 完成合同绑定不匹配".to_string());
}
Ok(Some(contract))
}
pub(in crate::agent) fn autonomous_game_build_completion_blocker_at_locked(
root: &Path,
state: &AgentRuntimeState,
@@ -12,6 +12,65 @@ fn autonomous_fixture(
autonomous_fixture_with_setup(task, run_id, |_| {})
}
fn autonomous_fixture_with_source(
task: &str,
run_id: &str,
source: &str,
) -> (
tempfile::TempDir,
PathBuf,
AgentRuntimeState,
AgentRuntimeAutonomousCompletionContract,
) {
let temporary = tempfile::tempdir().expect("create autonomous fixture root");
let root = temporary.path().join("project");
init_local_game_project_at(&root, "autonomous-project", task).expect("init project");
let session_id = resolve_agent_conversation_session_id_at(
&root,
GAME_CREATOR_PROJECT_SUPERVISOR_AGENT_ID,
None,
true,
)
.expect("resolve supervisor session");
let record = append_unique_game_creator_agent_runtime_pending_task(
&root,
GAME_CREATOR_PROJECT_SUPERVISOR_AGENT_ID,
&session_id,
task,
run_id,
source,
Some(AGENT_RUNTIME_RUN_PROFILE_AUTONOMOUS_GAME_BUILD),
None,
)
.expect("queue autonomous task");
let state = agent_runtime_state_from_task_record(&record);
let contract = read_autonomous_completion_contract(
&root,
GAME_CREATOR_PROJECT_SUPERVISOR_AGENT_ID,
&record.run_id,
)
.expect("read completion contract")
.expect("completion contract exists");
prepare_completed_autonomous_manifest_fixture(&root);
(temporary, root, state, contract)
}
#[test]
fn autonomous_supervisor_source_allowlist_includes_game_chat_only() {
assert!(agent_runtime_supervisor_source_is_trusted(
AGENT_RUNTIME_SUPERVISOR_GUI_SOURCE
));
assert!(agent_runtime_supervisor_source_is_trusted(
AGENT_RUNTIME_SUPERVISOR_CLI_SOURCE
));
assert!(agent_runtime_supervisor_source_is_trusted(
AGENT_RUNTIME_SUPERVISOR_GAME_CHAT_SOURCE
));
assert!(!agent_runtime_supervisor_source_is_trusted(
"project-supervisor-forged"
));
}
fn autonomous_fixture_with_setup(
task: &str,
run_id: &str,
@@ -461,6 +520,96 @@ fn autonomous_preview_manifest_roles_keep_their_fixed_read_only_core() {
assert!(publish_package.contains("所有 Markdown checklist 必须使用 [x] 或 [X]"));
}
#[test]
fn autonomous_preview_manifest_tasks_require_current_revision_receipts_before_completion() {
let (_temporary, root, parent_state, _contract) =
autonomous_fixture("做一个完整小游戏", "autonomous-preview-task-receipt-parent");
for (task_id, expected_summary) in [
("preview-readiness", "尚未通过当前 revision"),
("preview-playtest", "preview-playtest"),
] {
update_manifest_task_status_at(&root, task_id, GameCreationAppTaskStatus::Running)
.expect("mark preview manifest task running");
let child = queue_autonomous_manifest_child_fixture(&root, &parent_state, task_id);
let blocker = autonomous_game_build_completion_blocker_at_locked(
&root,
&agent_runtime_state_from_task_record(&child),
)
.expect("preview task without current receipt must be blocked");
assert!(blocker.summary.contains(expected_summary));
update_manifest_task_status_at(&root, task_id, GameCreationAppTaskStatus::Pending)
.expect("reset preview manifest task");
}
}
#[test]
fn autonomous_preview_manifest_tasks_accept_bound_current_revision_receipts() {
let (_temporary, root, parent_state, _contract) = autonomous_fixture(
"做一个完整小游戏",
"autonomous-preview-readiness-receipt-parent",
);
update_manifest_task_status_at(
&root,
"preview-readiness",
GameCreationAppTaskStatus::Running,
)
.expect("mark preview readiness running");
let readiness_child =
queue_autonomous_manifest_child_fixture(&root, &parent_state, "preview-readiness");
let readiness_state = agent_runtime_state_from_task_record(&readiness_child);
advance_game_index_revision(
&root,
&parent_state,
"<!doctype html><title>静态检查通过</title><canvas></canvas>",
);
mark_verification_passed(&root, &readiness_state, "game.static_smoke");
assert!(autonomous_game_build_completion_blocker_at_locked(&root, &readiness_state).is_none());
let (_temporary, root, parent_state, contract) = autonomous_fixture(
"做一个完整小游戏",
"autonomous-preview-playtest-receipt-parent",
);
update_manifest_task_status_at(
&root,
"preview-playtest",
GameCreationAppTaskStatus::Running,
)
.expect("mark preview playtest running");
let playtest_child =
queue_autonomous_manifest_child_fixture(&root, &parent_state, "preview-playtest");
let playtest_state = agent_runtime_state_from_task_record(&playtest_child);
let revision = advance_game_index_revision(
&root,
&parent_state,
"<!doctype html><title>浏览器试玩通过</title><canvas></canvas>",
);
let result = browser_result_fixture(
&root,
&parent_state,
revision,
BrowserPlaytestScenario::GenericV1,
);
let action = AgentRuntimeToolAction {
tool: "preview.validate".to_string(),
reason: Some("验证当前 revision 的真实可玩闭环".to_string()),
input: serde_json::json!({}),
};
let action_fingerprint =
agent_runtime_tool_action_fingerprint(&action, &playtest_state.current_task);
let action_id =
agent_runtime_tool_action_id(&playtest_state.run_id, 1, 0, 1, &action_fingerprint);
write_autonomous_playtest_receipt_at(
&root,
&contract,
&action_id,
&action_fingerprint,
revision,
&result,
)
.expect("persist child-bound autonomous playtest receipt");
assert!(autonomous_game_build_completion_blocker_at_locked(&root, &playtest_state).is_none());
}
#[test]
fn autonomous_completion_rejects_formal_artifact_unchanged_from_run_baseline() {
let baseline_bytes =
@@ -843,6 +992,80 @@ fn autonomous_parent_completion_lists_missing_seed_tasks_and_formal_artifacts()
assert!(detail.contains("missingPaths=game/balance.json"));
}
#[test]
fn game_chat_parent_completion_stops_after_preview_playtest_without_publish_tasks() {
let _config_guard = crate::tests::write_test_local_config("{}".to_string());
let (_temporary, root, state, contract) = autonomous_fixture_with_source(
"创建一轮植物塔防游戏",
"game-chat-single-round-parent",
AGENT_RUNTIME_SUPERVISOR_GAME_CHAT_SOURCE,
);
update_manifest_task_status_at(
&root,
"publish-strategy",
GameCreationAppTaskStatus::Pending,
)
.expect("leave publish strategy pending");
update_manifest_task_status_at(&root, "publish-package", GameCreationAppTaskStatus::Pending)
.expect("leave publish package pending");
let revision = advance_game_index_revision(
&root,
&state,
"<!doctype html><html><body><img src=\"assets/art-spritesheet.png\"><canvas></canvas></body></html>",
);
mark_verification_passed(&root, &state, "game.static_smoke");
let result = browser_result_fixture(&root, &state, revision, contract.playtest_scenario);
let action = AgentRuntimeToolAction {
tool: "preview.validate".to_string(),
reason: Some("game-chat single round preview".to_string()),
input: serde_json::json!({}),
};
let action_fingerprint = agent_runtime_tool_action_fingerprint(&action, &state.current_task);
let action_id = agent_runtime_tool_action_id(&state.run_id, 1, 0, 1, &action_fingerprint);
write_autonomous_playtest_receipt_at(
&root,
&contract,
&action_id,
&action_fingerprint,
revision,
&result,
)
.expect("persist game-chat playtest receipt");
assert!(autonomous_game_build_completion_blocker_at_locked(&root, &state).is_none());
}
#[test]
fn code_prototype_requires_registered_canvas_spritesheet_reference_when_editor_is_configured() {
let _config_guard = crate::tests::write_test_local_config(
r#"{"editorApi":{"apiKey":"game-chat-art-gate-key"}}"#.to_string(),
);
let (_temporary, root, parent_state, _contract) =
autonomous_fixture("创建一轮植物塔防游戏", "game-chat-code-art-gate-parent");
update_manifest_task_status_at(&root, "code-prototype", GameCreationAppTaskStatus::Running)
.expect("mark code prototype running");
let code_record =
queue_autonomous_manifest_child_fixture(&root, &parent_state, "code-prototype");
let code_state = agent_runtime_state_from_task_record(&code_record);
advance_game_index_revision(
&root,
&code_state,
"<!doctype html><html><body><canvas></canvas></body></html>",
);
let blocker = autonomous_game_build_completion_blocker_at_locked(&root, &code_state)
.expect("missing spritesheet reference must block code prototype");
assert!(blocker
.detail
.as_deref()
.is_some_and(|detail| detail.contains("assets/art-spritesheet.png")));
advance_game_index_revision(
&root,
&code_state,
"<!doctype html><html><body><img src=\"assets/art-spritesheet.png\"><canvas></canvas></body></html>",
);
assert!(autonomous_game_build_completion_blocker_at_locked(&root, &code_state).is_none());
}
#[test]
fn autonomous_ready_child_missing_or_invalid_owner_artifact_is_blocked() {
let (_temporary, root, parent_state, _contract) =
@@ -523,8 +523,14 @@ pub(in crate::agent) fn read_game_creator_agent_runtime_context_bundle_with_supe
{
return Err("Agent Runtime context bundle 的停滞标记只能出现在上下文窗口边界".to_string());
}
let loop_limit = u32::try_from(AGENT_RUNTIME_BACKGROUND_LOOP_LIMIT).unwrap_or(1);
let completed_loop_remainder = bundle.next_loop_index % loop_limit;
let max_completed_loops =
bundle.next_loop_index % u32::try_from(AGENT_RUNTIME_BACKGROUND_LOOP_LIMIT).unwrap_or(1);
if bundle.next_loop_index > 0 && completed_loop_remainder == 0 && !bundle.context_stalled {
loop_limit.saturating_sub(1)
} else {
completed_loop_remainder
};
if bundle.window_completed_loops > max_completed_loops {
return Err(format!(
"Agent Runtime context bundle 窗口轮次无效:windowCompletedLoops={} max={max_completed_loops}",
@@ -809,3 +815,65 @@ pub(in crate::agent) fn persist_game_creator_agent_runtime_pause_boundary_contex
context_tracker,
)
}
#[cfg(test)]
mod tests {
use super::*;
use std::time::{SystemTime, UNIX_EPOCH};
#[test]
fn agent_runtime_context_bundle_restores_pre_checkpoint_window_boundary() {
let unique = SystemTime::now()
.duration_since(UNIX_EPOCH)
.expect("system clock should be after epoch")
.as_nanos();
let root = std::env::temp_dir().join(format!(
"genarrative-context-window-boundary-{}-{unique}",
std::process::id()
));
init_local_game_project_at(&root, "project-1", "上下文窗口边界恢复项目")
.expect("project init");
let mut runtime = start_game_creator_agent_runtime_task_at(
&root,
"design-director",
"验证窗口边界恢复",
"design-context-window-boundary-run",
"agent-background-task",
"窗口边界恢复测试",
vec!["恢复 checkpoint 前的窗口状态".to_string()],
)
.expect("start window boundary runtime state");
let mut tracker = AgentRuntimeContextWindowTracker::default();
for next_loop_index in 1..AGENT_RUNTIME_BACKGROUND_LOOP_LIMIT {
assert_eq!(
tracker.complete_loop(next_loop_index),
AgentRuntimeContextCheckpoint::Continue
);
}
assert_eq!(tracker.completed_loops, 5);
let bundle = build_game_creator_agent_runtime_context_bundle(
&root,
&runtime,
&runtime.current_task,
&AgentRuntimeToolPlan::default(),
&[],
AGENT_RUNTIME_BACKGROUND_LOOP_LIMIT,
&tracker,
)
.expect("build pre-checkpoint boundary bundle");
assert_eq!(bundle.next_loop_index, 6);
assert_eq!(bundle.context_window, 2);
assert_eq!(bundle.window_completed_loops, 5);
write_game_creator_agent_runtime_context_bundle(&root, &bundle)
.expect("write pre-checkpoint boundary bundle");
runtime.loop_iteration = 6;
let loaded = read_game_creator_agent_runtime_context_bundle(&root, &runtime)
.expect("pre-checkpoint boundary bundle must remain recoverable")
.expect("pre-checkpoint boundary bundle exists");
assert_eq!(loaded.window_completed_loops, 5);
fs::remove_dir_all(root).ok();
}
}
@@ -111,10 +111,7 @@ pub(in crate::agent) fn validate_agent_runtime_run_profile_binding_record(
}
if binding.profile == AGENT_RUNTIME_RUN_PROFILE_AUTONOMOUS_GAME_BUILD
&& (binding.agent_id != GAME_CREATOR_PROJECT_SUPERVISOR_AGENT_ID
|| !matches!(
binding.source.as_str(),
AGENT_RUNTIME_SUPERVISOR_GUI_SOURCE | AGENT_RUNTIME_SUPERVISOR_CLI_SOURCE
))
|| !agent_runtime_supervisor_source_is_trusted(&binding.source))
{
return Err("自主构建 Run Profile 只允许可信 Supervisor 入口绑定".to_string());
}
@@ -1,5 +1,64 @@
use super::*;
const AGENT_RUNTIME_PREVIEW_INFRASTRUCTURE_ERROR_KIND: &str = "preview-infrastructure-unavailable";
fn agent_runtime_preview_infrastructure_failure_kind(error: &str) -> Option<&'static str> {
if error.contains("before websocket URL could be resolved")
|| error.contains("启动浏览器失败")
|| error.contains("启动浏览器超时")
{
Some("browser-launch-failed")
} else if error.contains("未发现可用的 Google Chrome") {
Some("browser-not-found")
} else if error.contains("创建浏览器临时目录失败")
|| error.contains("创建浏览器临时 Profile 失败")
|| error.contains("构建浏览器配置失败")
{
Some("browser-environment-invalid")
} else {
None
}
}
fn agent_runtime_preview_infrastructure_observation(
root: &Path,
revision: u64,
failure_kind: &str,
error: &str,
) -> AgentRuntimeToolObservation {
let detail = serde_json::to_string(&serde_json::json!({
"errorKind": AGENT_RUNTIME_PREVIEW_INFRASTRUCTURE_ERROR_KIND,
"failureKind": failure_kind,
"revision": revision,
"diagnostic": redact_agent_runtime_project_paths(root, error, 500),
}))
.ok();
AgentRuntimeToolObservation {
tool: "preview.validate".to_string(),
status: "blocked".to_string(),
summary: "浏览器验证基础设施不可用,当前任务已停止,未重复重试".to_string(),
detail,
}
}
pub(in crate::agent) fn agent_runtime_preview_infrastructure_blocker(
observation: &AgentRuntimeToolObservation,
) -> Option<String> {
if observation.tool != "preview.validate" || observation.status != "blocked" {
return None;
}
let detail = serde_json::from_str::<serde_json::Value>(observation.detail.as_deref()?).ok()?;
(detail.get("errorKind").and_then(serde_json::Value::as_str)
== Some(AGENT_RUNTIME_PREVIEW_INFRASTRUCTURE_ERROR_KIND))
.then(|| {
detail
.get("failureKind")
.and_then(serde_json::Value::as_str)
.unwrap_or("browser-infrastructure")
.to_string()
})
}
pub(in crate::agent) fn observe_agent_runtime_preview_start(
root: &Path,
agent_id: &str,
@@ -115,17 +174,18 @@ pub(in crate::agent) async fn observe_agent_runtime_preview_validate(
};
}
};
let completion_contract = match autonomous_completion_contract_for_state_at(root, &runtime) {
Ok(contract) => contract,
Err(error) => {
return AgentRuntimeToolObservation {
tool: "preview.validate".to_string(),
status: "failed".to_string(),
summary: "自主构建完成合同不可用,未执行浏览器试玩".to_string(),
detail: Some(redact_agent_runtime_project_paths(root, &error, 500)),
};
}
};
let completion_contract =
match autonomous_playtest_completion_contract_for_state_at(root, &runtime) {
Ok(contract) => contract,
Err(error) => {
return AgentRuntimeToolObservation {
tool: "preview.validate".to_string(),
status: "failed".to_string(),
summary: "自主构建完成合同不可用,未执行浏览器试玩".to_string(),
detail: Some(redact_agent_runtime_project_paths(root, &error, 500)),
};
}
};
if let (Some(contract), Some(requested)) = (
completion_contract.as_ref(),
input.playtest_scenario.as_ref(),
@@ -219,6 +279,14 @@ pub(in crate::agent) async fn observe_agent_runtime_preview_validate(
let result = match validation {
Ok(result) => result,
Err(error) => {
if let Some(failure_kind) = agent_runtime_preview_infrastructure_failure_kind(&error) {
return agent_runtime_preview_infrastructure_observation(
root,
revision_before.revision,
failure_kind,
&error,
);
}
return AgentRuntimeToolObservation {
tool: "preview.validate".to_string(),
status: "failed".to_string(),
@@ -399,3 +467,38 @@ pub(in crate::agent) async fn observe_agent_runtime_preview_validate(
detail,
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn browser_websocket_launch_exit_is_classified_as_infrastructure_failure() {
let error = "启动浏览器失败:Browser process exited with status ExitStatus(0) before websocket URL could be resolved, stderr=\"\"";
assert_eq!(
agent_runtime_preview_infrastructure_failure_kind(error),
Some("browser-launch-failed")
);
let observation = agent_runtime_preview_infrastructure_observation(
Path::new("/project"),
19,
"browser-launch-failed",
error,
);
assert_eq!(observation.status, "blocked");
assert_eq!(
agent_runtime_preview_infrastructure_blocker(&observation).as_deref(),
Some("browser-launch-failed")
);
}
#[test]
fn gameplay_validation_failure_is_not_an_infrastructure_failure() {
assert_eq!(
agent_runtime_preview_infrastructure_failure_kind(
"浏览器验证未通过,请根据诊断修复后重试"
),
None
);
}
}
@@ -309,6 +309,9 @@ pub(crate) fn game_creator_llm_status_lines(status: &GameCreatorLlmConfigStatus)
"llm.toolOutputTokenLimit={}",
status.tool_output_token_limit
),
format!("llm.requestTimeoutMs={}", status.request_timeout_ms),
format!("llm.maxRetries={}", status.max_retries),
format!("llm.retryBackoffMs={}", status.retry_backoff_ms),
];
for agent in &status.agents {
lines.push(format!(
@@ -357,6 +360,18 @@ pub(crate) fn game_creator_llm_status_lines(status: &GameCreatorLlmConfigStatus)
"llm.agent.{}.toolOutputTokenLimit={}",
agent.agent_id, agent.tool_output_token_limit
));
lines.push(format!(
"llm.agent.{}.requestTimeoutMs={}",
agent.agent_id, agent.request_timeout_ms
));
lines.push(format!(
"llm.agent.{}.maxRetries={}",
agent.agent_id, agent.max_retries
));
lines.push(format!(
"llm.agent.{}.retryBackoffMs={}",
agent.agent_id, agent.retry_backoff_ms
));
if let Some(error) = agent.error.as_deref() {
lines.push(format!("llm.agent.{}.error={error}", agent.agent_id));
}
@@ -22,8 +22,6 @@ const SUPERVISOR_COLLABORATION_POLICY_BINDING_LEGACY_CURRENT: &str =
const SUPERVISOR_COLLABORATION_MAX_STATIC_DELEGATES: usize = 3;
const SUPERVISOR_COLLABORATION_MAX_ISOLATED_CHILDREN: usize = 3;
const SUPERVISOR_COLLABORATION_MAX_ISOLATED_GROUPS_BEFORE_CLAIM: usize = 16;
const AUTONOMOUS_GAME_BUILD_BASE_REQUIRED_STATIC_AGENT_IDS: [&str; 2] =
["code-prototype", "quality-review"];
#[derive(Clone, Copy, Debug, Default, Deserialize, Eq, PartialEq, Serialize)]
#[serde(rename_all = "kebab-case")]
@@ -67,93 +65,10 @@ impl Default for SupervisorCollaborationPolicy {
}
}
fn autonomous_game_build_has_canonical_art_asset(root: &Path) -> bool {
read_existing_manifest_for_project(root)
.ok()
.is_some_and(|manifest| {
manifest_has_required_visual_asset(root, &manifest, "art-asset-plan")
})
}
fn autonomous_game_build_has_canonical_art_spec(root: &Path) -> bool {
read_existing_manifest_for_project(root)
.ok()
.is_some_and(|manifest| manifest_has_required_visual_asset(root, &manifest, "art-director"))
}
fn autonomous_game_build_has_canonical_ui_prototype(root: &Path) -> bool {
read_existing_manifest_for_project(root)
.ok()
.is_some_and(|manifest| {
manifest_has_required_visual_asset(root, &manifest, "design-foundation")
})
}
fn autonomous_game_build_supervisor_collaboration_policy(
root: &Path,
_root: &Path,
) -> SupervisorCollaborationPolicy {
let mut required_static_agent_ids = AUTONOMOUS_GAME_BUILD_BASE_REQUIRED_STATIC_AGENT_IDS
.into_iter()
.map(str::to_string)
.collect::<Vec<_>>();
if editor_api_key_is_configured() {
if !autonomous_game_build_has_canonical_art_spec(root) {
required_static_agent_ids.push("art-director".to_string());
} else if !autonomous_game_build_has_canonical_ui_prototype(root) {
required_static_agent_ids.push("design-foundation".to_string());
} else if !autonomous_game_build_has_canonical_art_asset(root) {
required_static_agent_ids.push("art-asset-plan".to_string());
}
}
SupervisorCollaborationPolicy {
required_initial_wave: SupervisorInitialCollaborationWave::Static,
min_static_delegates: required_static_agent_ids.len(),
required_static_agent_ids,
..SupervisorCollaborationPolicy::default()
}
}
fn apply_autonomous_game_build_required_static_agents(
root: &Path,
mut policy: SupervisorCollaborationPolicy,
) -> Result<SupervisorCollaborationPolicy, String> {
if editor_api_key_is_configured() {
if !autonomous_game_build_has_canonical_art_spec(root)
&& !policy
.required_static_agent_ids
.iter()
.any(|existing| existing == "art-director")
{
policy
.required_static_agent_ids
.push("art-director".to_string());
} else if autonomous_game_build_has_canonical_art_spec(root)
&& !autonomous_game_build_has_canonical_ui_prototype(root)
&& !policy
.required_static_agent_ids
.iter()
.any(|existing| existing == "design-foundation")
{
policy
.required_static_agent_ids
.push("design-foundation".to_string());
} else if autonomous_game_build_has_canonical_art_spec(root)
&& autonomous_game_build_has_canonical_ui_prototype(root)
&& !autonomous_game_build_has_canonical_art_asset(root)
&& !policy
.required_static_agent_ids
.iter()
.any(|existing| existing == "art-asset-plan")
{
policy
.required_static_agent_ids
.push("art-asset-plan".to_string());
}
}
policy.min_static_delegates = policy
.min_static_delegates
.max(policy.required_static_agent_ids.len());
normalize_supervisor_collaboration_policy(policy)
SupervisorCollaborationPolicy::default()
}
#[derive(Clone, Debug)]
@@ -176,10 +91,7 @@ fn read_supervisor_collaboration_unbound_policy_for_run_at(
let policy_path = root.join(SUPERVISOR_COLLABORATION_POLICY_RELATIVE_PATH);
match fs::symlink_metadata(&policy_path) {
Ok(_) => {
let mut policy = read_supervisor_collaboration_policy_at(root)?;
if autonomous_supervisor {
policy = apply_autonomous_game_build_required_static_agents(root, policy)?;
}
let policy = read_supervisor_collaboration_policy_at(root)?;
return Ok(SupervisorCollaborationUnboundPolicy {
policy,
source: "project-policy-unbound",
@@ -489,6 +489,7 @@ pub(crate) fn start_game_creator_supervisor_runtime_task(
task: String,
run_id: String,
run_profile: Option<String>,
source: Option<String>,
) -> Result<AgentRuntimeResult, String> {
let root = Path::new(project_path.trim());
enforce_project_permission_policy(root, "conversation.read")?;
@@ -499,12 +500,20 @@ pub(crate) fn start_game_creator_supervisor_runtime_task(
.map(str::trim)
.filter(|value| !value.is_empty())
.unwrap_or(AGENT_RUNTIME_RUN_PROFILE_STANDARD);
let source = source
.as_deref()
.map(str::trim)
.filter(|value| !value.is_empty())
.unwrap_or(AGENT_RUNTIME_SUPERVISOR_GUI_SOURCE);
if !agent_runtime_supervisor_source_is_trusted(source) {
return Err("Project Supervisor 提交 source 不受信任".to_string());
}
start_game_creator_supervisor_background_task_for_session_at(
root,
session_id.as_deref(),
task.trim(),
run_id.trim(),
AGENT_RUNTIME_SUPERVISOR_GUI_SOURCE,
source,
run_profile,
)
}
@@ -663,11 +672,30 @@ pub(crate) fn steer_game_creator_agent_runtime_task(
steer_id: String,
instruction: String,
run_profile: Option<String>,
source: Option<String>,
) -> Result<AgentRuntimeSteerResult, String> {
let root = Path::new(project_path.trim());
enforce_project_permission_policy(root, "conversation.read")?;
enforce_project_permission_policy(root, "conversation.write")?;
enforce_project_permission_policy(root, "agent.run_status")?;
if let Some(source) = source
.as_deref()
.map(str::trim)
.filter(|value| !value.is_empty())
{
if !agent_runtime_supervisor_source_is_trusted(source) {
return Err("Project Supervisor steer source 不受信任".to_string());
}
let task = read_latest_game_creator_agent_runtime_task_by_run_id(
root,
agent_id.trim(),
run_id.trim(),
)?
.ok_or_else(|| "Agent Runtime steer 的 Run 不存在".to_string())?;
if task.source != source {
return Err("Agent Runtime steer source 与当前 Run 不一致".to_string());
}
}
let mut result = steer_game_creator_agent_runtime_task_for_profile_at(
root,
agent_id.trim(),
@@ -1,5 +1,37 @@
use super::*;
pub(crate) const GAME_CREATOR_LLM_AGENT_REASONING_EFFORT_DEFAULTS: [(&str, &str); 21] = [
(GAME_CREATOR_PROJECT_SUPERVISOR_AGENT_ID, "high"),
("planner", "high"),
("orchestrator", "medium"),
("generator", "high"),
("evaluator", "high"),
("design-director", "medium"),
("design-foundation", "high"),
("balance-director", "medium"),
("balance-seed", "medium"),
("art-director", "high"),
("art-asset-plan", "high"),
("art-polish", "medium"),
("audio-director", "low"),
("audio-asset-plan", "medium"),
("code-director", "medium"),
("code-prototype", "high"),
("quality-review", "high"),
("preview-readiness", "low"),
("preview-playtest", "low"),
("publish-strategy", "low"),
("publish-package", "medium"),
];
pub(crate) fn game_creator_llm_agent_default_reasoning_effort(
agent_id: &str,
) -> Option<&'static str> {
GAME_CREATOR_LLM_AGENT_REASONING_EFFORT_DEFAULTS
.iter()
.find_map(|(candidate, effort)| (*candidate == agent_id).then_some(*effort))
}
pub(crate) fn build_game_creator_llm_client_from_llm_config(
llm: &GameCreatorLlmConfig,
config_path: &str,
@@ -137,6 +169,9 @@ pub(crate) fn check_game_creator_llm_config_from_config() -> GameCreatorLlmConfi
context_window_tokens: DEFAULT_GAME_CREATOR_LLM_CONTEXT_WINDOW_TOKENS,
auto_compact_token_limit: DEFAULT_GAME_CREATOR_LLM_AUTO_COMPACT_TOKEN_LIMIT,
tool_output_token_limit: DEFAULT_GAME_CREATOR_LLM_TOOL_OUTPUT_TOKEN_LIMIT,
request_timeout_ms: GAME_CREATOR_LLM_REQUEST_TIMEOUT_MS,
max_retries: DEFAULT_GAME_CREATOR_LLM_MAX_RETRIES,
retry_backoff_ms: DEFAULT_RETRY_BACKOFF_MS,
error: Some(error),
agents: Vec::new(),
}
@@ -248,6 +283,9 @@ pub(crate) fn check_game_creator_llm_config_values(
context_window_tokens: config.context_window_tokens,
auto_compact_token_limit: config.auto_compact_token_limit,
tool_output_token_limit: config.tool_output_token_limit,
request_timeout_ms: config.request_timeout_ms,
max_retries: config.max_retries,
retry_backoff_ms: config.retry_backoff_ms,
error,
agents: Vec::new(),
}
@@ -287,6 +325,9 @@ pub(crate) fn check_game_creator_agent_llm_config_values(
context_window_tokens: config.context_window_tokens,
auto_compact_token_limit: config.auto_compact_token_limit,
tool_output_token_limit: config.tool_output_token_limit,
request_timeout_ms: config.request_timeout_ms,
max_retries: config.max_retries,
retry_backoff_ms: config.retry_backoff_ms,
error: status.error,
}
}
@@ -1367,6 +1408,9 @@ pub(crate) fn resolve_game_creator_llm_config_for_agent(
agent_id: &str,
) -> GameCreatorLlmConfig {
let mut llm = config.llm.clone();
if let Some(reasoning_effort) = game_creator_llm_agent_default_reasoning_effort(agent_id) {
llm.reasoning_effort = reasoning_effort.to_string();
}
if agent_id == GAME_CREATOR_PROJECT_SUPERVISOR_AGENT_ID {
if let Some(patch) = config
.agent_llm
@@ -647,6 +647,9 @@ struct GameCreatorLlmConfigStatus {
context_window_tokens: u64,
auto_compact_token_limit: u64,
tool_output_token_limit: u64,
request_timeout_ms: u64,
max_retries: u32,
retry_backoff_ms: u64,
error: Option<String>,
agents: Vec<GameCreatorAgentLlmConfigStatus>,
}
@@ -667,6 +670,9 @@ struct GameCreatorAgentLlmConfigStatus {
context_window_tokens: u64,
auto_compact_token_limit: u64,
tool_output_token_limit: u64,
request_timeout_ms: u64,
max_retries: u32,
retry_backoff_ms: u64,
error: Option<String>,
}
@@ -1969,6 +1975,52 @@ fn handle_game_creator_gui_run_event(event: &tauri::RunEvent) {
}
}
#[derive(Debug, Eq, PartialEq)]
enum GameChatReleaseClientExitOutcome {
Shutdown,
Busy,
Failed(String),
}
fn resolve_game_chat_release_client_exit<F>(shutdown: F) -> GameChatReleaseClientExitOutcome
where
F: FnOnce() -> Result<bool, String>,
{
match shutdown() {
Ok(true) => GameChatReleaseClientExitOutcome::Shutdown,
Ok(false) => GameChatReleaseClientExitOutcome::Busy,
Err(error) => GameChatReleaseClientExitOutcome::Failed(error),
}
}
fn show_game_chat_release_client_exit_blocked(app: &tauri::AppHandle) {
app.dialog()
.message("当前仍有游戏创作任务或 Provider 请求在运行。为避免结果丢失,已阻止关闭;请先等待任务完成,或在任务页暂停/取消后再退出。")
.title("游戏创作任务仍在运行")
.show(|_| {});
}
#[cfg(test)]
mod game_chat_release_client_exit_tests {
use super::*;
#[test]
fn client_exit_resolution_distinguishes_shutdown_busy_and_failure() {
assert_eq!(
resolve_game_chat_release_client_exit(|| Ok(true)),
GameChatReleaseClientExitOutcome::Shutdown
);
assert_eq!(
resolve_game_chat_release_client_exit(|| Ok(false)),
GameChatReleaseClientExitOutcome::Busy
);
assert_eq!(
resolve_game_chat_release_client_exit(|| Err("runner unavailable".to_string())),
GameChatReleaseClientExitOutcome::Failed("runner unavailable".to_string())
);
}
}
fn main() {
let mut args = std::env::args().skip(1).collect::<Vec<_>>();
#[cfg(target_os = "linux")]
@@ -2351,17 +2403,28 @@ fn main() {
let _ = append_bounded_diagnostic_line(path, "startup.run.begin");
}
let shutdown_log = startup_log.clone();
app.run(move |_, event| {
app.run(move |app_handle, event| {
let game_chat_release = cfg!(all(not(debug_assertions), feature = "game-chat-release"));
if game_chat_release && should_shutdown_runner_on_tauri_event(true, &event) {
let game_chat_exit_requested = game_chat_release
&& matches!(
&event,
tauri::RunEvent::WindowEvent {
event: tauri::WindowEvent::CloseRequested { .. },
..
} | tauri::RunEvent::ExitRequested { .. }
);
if game_chat_exit_requested {
if let Some(path) = shutdown_log.as_deref() {
let _ = append_bounded_diagnostic_line(
path,
"startup.runner.shutdown-for-client-exit.begin",
);
}
match shutdown_external_agent_runner_for_client_exit() {
Ok(()) => {
let outcome = resolve_game_chat_release_client_exit(
shutdown_external_agent_runner_for_client_exit,
);
match &outcome {
GameChatReleaseClientExitOutcome::Shutdown => {
if let Some(path) = shutdown_log.as_deref() {
let _ = append_bounded_diagnostic_line(
path,
@@ -2369,7 +2432,15 @@ fn main() {
);
}
}
Err(error) => {
GameChatReleaseClientExitOutcome::Busy => {
if let Some(path) = shutdown_log.as_deref() {
let _ = append_bounded_diagnostic_line(
path,
"startup.runner.shutdown-for-client-exit.busy",
);
}
}
GameChatReleaseClientExitOutcome::Failed(error) => {
if let Some(path) = shutdown_log.as_deref() {
let details = sanitize_diagnostic_message(&error, path.parent());
let _ = append_bounded_diagnostic_line(
@@ -2382,6 +2453,17 @@ fn main() {
eprintln!("game-chat 客户端退出协议关闭 Agent Runner 失败:{error}")
}
}
if outcome != GameChatReleaseClientExitOutcome::Shutdown {
match &event {
tauri::RunEvent::WindowEvent {
event: tauri::WindowEvent::CloseRequested { api, .. },
..
} => api.prevent_close(),
tauri::RunEvent::ExitRequested { api, .. } => api.prevent_exit(),
_ => {}
}
show_game_chat_release_client_exit_blocked(app_handle);
}
} else if !game_chat_release {
handle_game_creator_gui_run_event(&event);
}
@@ -931,11 +931,11 @@ pub(crate) fn attach_external_agent_runner_gui_owner() -> Result<(), String> {
pub(super) fn shutdown_external_agent_runner_for_client_exit_at(
config_dir: &Path,
) -> Result<(), String> {
) -> Result<bool, String> {
let Some((endpoint_path, endpoint)) =
read_external_agent_runner_endpoint_for_shutdown(config_dir)?
else {
return Ok(());
return Ok(true);
};
let request_id = random_identifier(b"genarrative-agent-runner-client-exit-request-id")?;
let result = match send_external_agent_runner_request_with_protocol_and_id(
@@ -949,7 +949,7 @@ pub(super) fn shutdown_external_agent_runner_for_client_exit_at(
Err(error) => {
return match read_external_agent_runner_endpoint(&endpoint_path) {
Ok(current) if current.boot_id == endpoint.boot_id => Err(error),
_ => Ok(()),
_ => Ok(true),
};
}
};
@@ -957,25 +957,32 @@ pub(super) fn shutdown_external_agent_runner_for_client_exit_at(
.get("accepted")
.and_then(Value::as_bool)
.ok_or_else(|| "Agent Runner shutdown_for_client_exit 响应缺少 accepted".to_string())?;
let busy = result
.get("busy")
.and_then(Value::as_bool)
.ok_or_else(|| "Agent Runner shutdown_for_client_exit 响应缺少 busy".to_string())?;
let will_shutdown = result
.get("willShutdown")
.and_then(Value::as_bool)
.ok_or_else(|| "Agent Runner shutdown_for_client_exit 响应缺少 willShutdown".to_string())?;
if !accepted || !will_shutdown {
return Err("Agent Runner 拒绝按客户端退出协议关闭".to_string());
match (accepted, busy, will_shutdown) {
(false, true, false) => return Ok(false),
(true, false, true) => {}
_ => return Err("Agent Runner shutdown_for_client_exit 响应状态不一致".to_string()),
}
wait_for_external_agent_runner_boot_exit(
&endpoint_path,
&endpoint,
AGENT_RUNNER_CLIENT_EXIT_TIMEOUT,
"Agent Runner 未在客户端退出期限内停止",
)
)?;
Ok(true)
}
pub(crate) fn shutdown_external_agent_runner_for_client_exit() -> Result<(), String> {
pub(crate) fn shutdown_external_agent_runner_for_client_exit() -> Result<bool, String> {
let _configure = lock_unpoisoned(external_agent_runner_configure_lock());
let Some(config_dir) = external_agent_runner_config_dir() else {
return Ok(());
return Ok(true);
};
shutdown_external_agent_runner_for_client_exit_at(&config_dir)
}
@@ -618,12 +618,53 @@ pub(super) fn dispatch_external_agent_runner_runtime_request(
)
}
"runner.shutdown_for_client_exit" if cfg!(any(test, feature = "game-chat-release")) => {
state.draining.store(true, Ordering::Release);
state.shutdown_requested.store(true, Ordering::Release);
ExternalAgentRunnerResponse::success(
&request.request_id,
json!({ "accepted": true, "willShutdown": true }),
)
if state.shutdown_requested.load(Ordering::Acquire) {
ExternalAgentRunnerResponse::success(
&request.request_id,
json!({ "accepted": true, "busy": false, "willShutdown": true }),
)
} else if state
.draining
.compare_exchange(false, true, Ordering::AcqRel, Ordering::Acquire)
.is_err()
{
ExternalAgentRunnerResponse::failure(
&request.request_id,
"runner-draining",
"Agent Runner 已在排空",
)
} else if state.active_connections.load(Ordering::Acquire) > 1 {
state.draining.store(false, Ordering::Release);
ExternalAgentRunnerResponse::success(
&request.request_id,
json!({ "accepted": false, "busy": true, "willShutdown": false }),
)
} else {
match external_agent_runner_known_roots_are_idle(state) {
Ok(false) => {
state.draining.store(false, Ordering::Release);
ExternalAgentRunnerResponse::success(
&request.request_id,
json!({ "accepted": false, "busy": true, "willShutdown": false }),
)
}
Ok(true) => {
state.shutdown_requested.store(true, Ordering::Release);
ExternalAgentRunnerResponse::success(
&request.request_id,
json!({ "accepted": true, "busy": false, "willShutdown": true }),
)
}
Err(error) => {
state.draining.store(false, Ordering::Release);
ExternalAgentRunnerResponse::failure(
&request.request_id,
"runtime-state-unreadable",
redact_runner_secret(&error, &token),
)
}
}
}
}
"runner.shutdown_if_idle" | "shutdown_if_idle" => {
if request.params.root.is_some() {
@@ -150,6 +150,91 @@ fn legacy_runner_force_migration_requires_authenticated_exact_ping_identity() {
server.join().expect("join mismatched identity fixture");
}
#[test]
fn client_exit_client_returns_busy_without_waiting_and_accepts_idle_shutdown() {
let directory = unique_test_directory();
let config_dir = private_runner_test_config_dir(&directory);
let endpoint_path = external_agent_runner_endpoint_path(&config_dir);
let token = "client-exit-response-token-client-exit-response-token";
let busy_listener = TcpListener::bind(SocketAddrV4::new(Ipv4Addr::LOCALHOST, 0))
.expect("bind busy client-exit fixture");
let busy_endpoint = test_endpoint(
token,
"client-exit-busy-response-boot",
busy_listener
.local_addr()
.expect("busy fixture address")
.port(),
);
write_external_agent_runner_endpoint_atomic(&endpoint_path, &busy_endpoint)
.expect("write busy client-exit endpoint");
let busy_server = std::thread::spawn(move || {
let (mut stream, _) = busy_listener.accept().expect("accept busy client exit");
let payload = read_external_agent_runner_frame(&mut stream).expect("read busy client exit");
let request = serde_json::from_slice::<ExternalAgentRunnerRequest>(&payload)
.expect("parse busy client exit");
assert_eq!(request.method, "runner.shutdown_for_client_exit");
let response = ExternalAgentRunnerResponse::success(
&request.request_id,
json!({ "accepted": false, "busy": true, "willShutdown": false }),
);
write_external_agent_runner_frame(
&mut stream,
&serde_json::to_vec(&response).expect("serialize busy client-exit response"),
)
.expect("write busy client-exit response");
});
let started = Instant::now();
assert!(
!shutdown_external_agent_runner_for_client_exit_at(&config_dir)
.expect("busy client exit remains a successful refusal")
);
assert!(
started.elapsed() < Duration::from_secs(2),
"busy client exit must not wait for Runner boot shutdown"
);
busy_server.join().expect("join busy client-exit fixture");
let idle_listener = TcpListener::bind(SocketAddrV4::new(Ipv4Addr::LOCALHOST, 0))
.expect("bind idle client-exit fixture");
let idle_endpoint = test_endpoint(
token,
"client-exit-idle-response-boot",
idle_listener
.local_addr()
.expect("idle fixture address")
.port(),
);
write_external_agent_runner_endpoint_atomic(&endpoint_path, &idle_endpoint)
.expect("write idle client-exit endpoint");
let idle_endpoint_path = endpoint_path.clone();
let idle_server = std::thread::spawn(move || {
let (mut stream, _) = idle_listener.accept().expect("accept idle client exit");
let payload = read_external_agent_runner_frame(&mut stream).expect("read idle client exit");
let request = serde_json::from_slice::<ExternalAgentRunnerRequest>(&payload)
.expect("parse idle client exit");
assert_eq!(request.method, "runner.shutdown_for_client_exit");
let response = ExternalAgentRunnerResponse::success(
&request.request_id,
json!({ "accepted": true, "busy": false, "willShutdown": true }),
);
write_external_agent_runner_frame(
&mut stream,
&serde_json::to_vec(&response).expect("serialize idle client-exit response"),
)
.expect("write idle client-exit response");
fs::remove_file(idle_endpoint_path).expect("remove idle endpoint after shutdown response");
});
assert!(
shutdown_external_agent_runner_for_client_exit_at(&config_dir)
.expect("idle client exit must complete Runner shutdown")
);
idle_server.join().expect("join idle client-exit fixture");
}
#[test]
fn endpoint_shape_accepts_legacy_missing_fingerprint_but_rejects_malformed_values() {
let endpoint = test_endpoint(
@@ -1248,7 +1333,7 @@ fn forced_shutdown_is_accepted_even_when_runtime_is_busy() {
}
#[test]
fn shutdown_for_client_exit_preserves_busy_durable_state_and_is_idempotent() {
fn shutdown_for_client_exit_rejects_busy_then_closes_idle_runner_idempotently() {
let directory = unique_test_directory();
let root = directory.0.join("project");
let pending = root.join(".agent/runtime/pending-actions/code-prototype/run-client-exit.json");
@@ -1309,10 +1394,50 @@ fn shutdown_for_client_exit_preserves_busy_durable_state_and_is_idempotent() {
assert!(!state.shutdown_requested.load(Ordering::Acquire));
assert!(!state.draining.load(Ordering::Acquire));
let busy_response = handle_external_agent_runner_request(
ExternalAgentRunnerRequest {
protocol_version: EXTERNAL_AGENT_RUNNER_PROTOCOL_VERSION,
request_id: "shutdown-client-exit-busy-1".to_string(),
token: token.to_string(),
method: "runner.shutdown_for_client_exit".to_string(),
params: ExternalAgentRunnerRequestParams::default(),
},
&state,
);
assert!(busy_response.ok);
assert_eq!(
busy_response
.result
.as_ref()
.and_then(|value| value["accepted"].as_bool()),
Some(false)
);
assert_eq!(
busy_response
.result
.as_ref()
.and_then(|value| value["busy"].as_bool()),
Some(true)
);
assert_eq!(
busy_response
.result
.as_ref()
.and_then(|value| value["willShutdown"].as_bool()),
Some(false)
);
assert!(!state.shutdown_requested.load(Ordering::Acquire));
assert!(!state.draining.load(Ordering::Acquire));
assert_eq!(
fs::read(&pending).expect("read pending action"),
durable_bytes
);
fs::remove_file(&pending).expect("clear pending action before idle client exit");
let shutdown_response = handle_external_agent_runner_request(
ExternalAgentRunnerRequest {
protocol_version: EXTERNAL_AGENT_RUNNER_PROTOCOL_VERSION,
request_id: "shutdown-client-exit-force-1".to_string(),
request_id: "shutdown-client-exit-idle-1".to_string(),
token: token.to_string(),
method: "runner.shutdown_for_client_exit".to_string(),
params: ExternalAgentRunnerRequestParams::default(),
@@ -1327,6 +1452,13 @@ fn shutdown_for_client_exit_preserves_busy_durable_state_and_is_idempotent() {
.and_then(|value| value["accepted"].as_bool()),
Some(true)
);
assert_eq!(
shutdown_response
.result
.as_ref()
.and_then(|value| value["busy"].as_bool()),
Some(false)
);
assert_eq!(
shutdown_response
.result
@@ -1336,35 +1468,6 @@ fn shutdown_for_client_exit_preserves_busy_durable_state_and_is_idempotent() {
);
assert!(state.shutdown_requested.load(Ordering::Acquire));
assert!(state.draining.load(Ordering::Acquire));
assert_eq!(
fs::read(&pending).expect("read pending action"),
durable_bytes
);
let write_response = handle_external_agent_runner_request(
ExternalAgentRunnerRequest {
protocol_version: EXTERNAL_AGENT_RUNNER_PROTOCOL_VERSION,
request_id: "shutdown-client-exit-write-after-drain".to_string(),
token: token.to_string(),
method: "runtime.continue_action".to_string(),
params: ExternalAgentRunnerRequestParams {
root: Some(root.to_string_lossy().into_owned()),
agent: Some("code-prototype".to_string()),
run_id: Some("run-client-exit".to_string()),
action_id: Some("action-client-exit".to_string()),
..ExternalAgentRunnerRequestParams::default()
},
},
&state,
);
assert!(!write_response.ok);
assert_eq!(
write_response
.error
.as_ref()
.map(|error| error.code.as_str()),
Some("runner-draining")
);
let repeated_response = handle_external_agent_runner_request(
ExternalAgentRunnerRequest {
@@ -1384,6 +1487,13 @@ fn shutdown_for_client_exit_preserves_busy_durable_state_and_is_idempotent() {
.and_then(|value| value["accepted"].as_bool()),
Some(true)
);
assert_eq!(
repeated_response
.result
.as_ref()
.and_then(|value| value["busy"].as_bool()),
Some(false)
);
assert_eq!(
repeated_response
.result
@@ -1391,10 +1501,7 @@ fn shutdown_for_client_exit_preserves_busy_durable_state_and_is_idempotent() {
.and_then(|value| value["willShutdown"].as_bool()),
Some(true)
);
assert_eq!(
fs::read(&pending).expect("reread pending action"),
durable_bytes
);
assert!(!pending.exists());
}
#[test]
@@ -356,6 +356,7 @@ fn steer_and_wait_for_swarm_turn<W: Write>(
steer_id.clone(),
message.to_string(),
Some(run_profile.to_string()),
None,
)?;
writeln!(
output,
@@ -355,6 +355,7 @@ pub(super) fn wait_for_swarm_turn<W: Write>(
steer_id.clone(),
message,
Some(run_profile.to_string()),
None,
)?;
writeln!(
output,
@@ -281,7 +281,7 @@ fn project_supervisor_llm_config_prefers_specific_patch_and_falls_back_to_legacy
assert_eq!(fallback.api_key, "legacy-chat-key");
assert_eq!(fallback.base_url, "https://legacy-chat.example.test/v1");
assert_eq!(fallback.model, "legacy-chat-model");
assert_eq!(fallback.reasoning_effort, "medium");
assert_eq!(fallback.reasoning_effort, "high");
assert!(fallback.stream);
config.agent_llm.insert(

Some files were not shown because too many files have changed in this diff Show More