修复AI游戏创作长耗时与运行收口
自主构建只执行正式清单任务图并移除重复静态委派 预览基础设施失败立即收口并绑定当前版本验证凭证 客户端退出前检查Runner持久任务状态并阻止忙碌关闭 修复上下文检查点恢复边界并补齐恢复测试 为全部Agent配置默认推理强度与实际重试状态展示 补齐配置、协作、预览、Runner及全量回归测试 同步技术方案、开发流程与长期决策记录
This commit is contained in:
@@ -11,7 +11,7 @@
|
||||
"autoCompactTokenLimit": 64000,
|
||||
"toolOutputTokenLimit": 12000,
|
||||
"requestTimeoutMs": 180000,
|
||||
"maxRetries": 0,
|
||||
"maxRetries": 2,
|
||||
"retryBackoffMs": 500
|
||||
},
|
||||
"agentLlm": {},
|
||||
|
||||
@@ -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(
|
||||
|
||||
+5
@@ -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、读取、搜索、状态查询或空验证。"
|
||||
));
|
||||
|
||||
@@ -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;
|
||||
|
||||
+125
@@ -511,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
|
||||
@@ -1264,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,
|
||||
|
||||
+90
@@ -520,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 =
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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]
|
||||
|
||||
@@ -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(
|
||||
|
||||
+69
-74
@@ -1428,6 +1428,19 @@ async fn supervisor_collaboration_partial_initial_wave_repairs_with_collaboratio
|
||||
"总控首轮只读逃逸修复测试",
|
||||
)
|
||||
.expect("project init");
|
||||
write_supervisor_collaboration_policy_at(
|
||||
&root,
|
||||
SupervisorCollaborationPolicy {
|
||||
required_initial_wave: SupervisorInitialCollaborationWave::Static,
|
||||
min_static_delegates: 2,
|
||||
required_static_agent_ids: vec![
|
||||
"code-prototype".to_string(),
|
||||
"quality-review".to_string(),
|
||||
],
|
||||
..SupervisorCollaborationPolicy::default()
|
||||
},
|
||||
)
|
||||
.expect("write explicit collaboration repair policy");
|
||||
let (sender, receiver) = mpsc::channel();
|
||||
let delegate_function =
|
||||
native_runtime_function_name("agent.delegate").expect("delegate function");
|
||||
@@ -1614,7 +1627,8 @@ async fn supervisor_collaboration_partial_initial_wave_repairs_with_collaboratio
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn supervisor_autonomous_game_build_without_project_policy_requires_builder_and_reviewer() {
|
||||
fn supervisor_autonomous_game_build_without_project_policy_uses_manifest_as_the_only_initial_wave()
|
||||
{
|
||||
let root = unique_project_path();
|
||||
let config_dir = unique_project_path();
|
||||
fs::create_dir_all(&config_dir).expect("create isolated runtime config dir");
|
||||
@@ -1657,13 +1671,10 @@ fn supervisor_autonomous_game_build_without_project_policy_requires_builder_and_
|
||||
assert_eq!(resolution.project_policy_status, "absent");
|
||||
assert_eq!(
|
||||
resolution.policy.required_initial_wave,
|
||||
SupervisorInitialCollaborationWave::Static
|
||||
);
|
||||
assert_eq!(resolution.policy.min_static_delegates, 2);
|
||||
assert_eq!(
|
||||
resolution.policy.required_static_agent_ids,
|
||||
vec!["code-prototype".to_string(), "quality-review".to_string()]
|
||||
SupervisorInitialCollaborationWave::Auto
|
||||
);
|
||||
assert_eq!(resolution.policy.min_static_delegates, 0);
|
||||
assert!(resolution.policy.required_static_agent_ids.is_empty());
|
||||
assert!(!root
|
||||
.join(SUPERVISOR_COLLABORATION_POLICY_RELATIVE_PATH)
|
||||
.exists());
|
||||
@@ -1673,8 +1684,7 @@ fn supervisor_autonomous_game_build_without_project_policy_requires_builder_and_
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn supervisor_autonomous_game_build_with_editor_api_key_requires_visual_agents_in_dependency_order()
|
||||
{
|
||||
fn supervisor_autonomous_game_build_with_editor_api_key_keeps_visual_agents_in_manifest_order() {
|
||||
let root = unique_project_path();
|
||||
let config_dir = unique_project_path();
|
||||
fs::create_dir_all(&config_dir).expect("create isolated runtime config dir");
|
||||
@@ -1717,18 +1727,10 @@ fn supervisor_autonomous_game_build_with_editor_api_key_requires_visual_agents_i
|
||||
assert_eq!(resolution.project_policy_status, "absent");
|
||||
assert_eq!(
|
||||
resolution.policy.required_initial_wave,
|
||||
SupervisorInitialCollaborationWave::Static
|
||||
);
|
||||
assert_eq!(resolution.policy.min_static_delegates, 3);
|
||||
assert_eq!(
|
||||
resolution
|
||||
.policy
|
||||
.required_static_agent_ids
|
||||
.iter()
|
||||
.map(String::as_str)
|
||||
.collect::<BTreeSet<_>>(),
|
||||
BTreeSet::from(["code-prototype", "quality-review", "art-director"])
|
||||
SupervisorInitialCollaborationWave::Auto
|
||||
);
|
||||
assert_eq!(resolution.policy.min_static_delegates, 0);
|
||||
assert!(resolution.policy.required_static_agent_ids.is_empty());
|
||||
assert!(!root
|
||||
.join(SUPERVISOR_COLLABORATION_POLICY_RELATIVE_PATH)
|
||||
.exists());
|
||||
@@ -1750,16 +1752,11 @@ fn supervisor_autonomous_game_build_with_editor_api_key_requires_visual_agents_i
|
||||
design_run_id,
|
||||
)
|
||||
.expect("resolve autonomous collaboration policy after art spec delivery");
|
||||
assert_eq!(design_resolution.policy.min_static_delegates, 3);
|
||||
assert_eq!(
|
||||
design_resolution
|
||||
.policy
|
||||
.required_static_agent_ids
|
||||
.iter()
|
||||
.map(String::as_str)
|
||||
.collect::<BTreeSet<_>>(),
|
||||
BTreeSet::from(["code-prototype", "quality-review", "design-foundation"])
|
||||
);
|
||||
assert_eq!(design_resolution.policy.min_static_delegates, 0);
|
||||
assert!(design_resolution
|
||||
.policy
|
||||
.required_static_agent_ids
|
||||
.is_empty());
|
||||
|
||||
register_canvas_visual_asset_fixture(&root, "assets/ui-prototype.png", "ui-prototype");
|
||||
let art_run_id = "supervisor-autonomous-art-after-ui-run";
|
||||
@@ -1778,16 +1775,8 @@ fn supervisor_autonomous_game_build_with_editor_api_key_requires_visual_agents_i
|
||||
art_run_id,
|
||||
)
|
||||
.expect("resolve autonomous collaboration policy after UI delivery");
|
||||
assert_eq!(art_resolution.policy.min_static_delegates, 3);
|
||||
assert_eq!(
|
||||
art_resolution
|
||||
.policy
|
||||
.required_static_agent_ids
|
||||
.iter()
|
||||
.map(String::as_str)
|
||||
.collect::<BTreeSet<_>>(),
|
||||
BTreeSet::from(["code-prototype", "quality-review", "art-asset-plan"])
|
||||
);
|
||||
assert_eq!(art_resolution.policy.min_static_delegates, 0);
|
||||
assert!(art_resolution.policy.required_static_agent_ids.is_empty());
|
||||
|
||||
register_canvas_visual_asset_fixture(&root, "assets/art-spritesheet.png", "art-spritesheet");
|
||||
let complete_run_id = "supervisor-autonomous-after-all-visual-assets-run";
|
||||
@@ -1806,18 +1795,18 @@ fn supervisor_autonomous_game_build_with_editor_api_key_requires_visual_agents_i
|
||||
complete_run_id,
|
||||
)
|
||||
.expect("resolve autonomous collaboration policy after all visual deliveries");
|
||||
assert_eq!(complete_resolution.policy.min_static_delegates, 2);
|
||||
assert_eq!(
|
||||
complete_resolution.policy.required_static_agent_ids,
|
||||
vec!["code-prototype".to_string(), "quality-review".to_string()]
|
||||
);
|
||||
assert_eq!(complete_resolution.policy.min_static_delegates, 0);
|
||||
assert!(complete_resolution
|
||||
.policy
|
||||
.required_static_agent_ids
|
||||
.is_empty());
|
||||
|
||||
fs::remove_dir_all(root).ok();
|
||||
fs::remove_dir_all(config_dir).ok();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn supervisor_autonomous_game_build_augments_existing_project_policy_with_required_art_director() {
|
||||
fn supervisor_autonomous_game_build_preserves_explicit_project_policy_without_hidden_agents() {
|
||||
let root = unique_project_path();
|
||||
let config_dir = unique_project_path();
|
||||
fs::create_dir_all(&config_dir).expect("create isolated runtime config dir");
|
||||
@@ -1839,8 +1828,16 @@ fn supervisor_autonomous_game_build_augments_existing_project_policy_with_requir
|
||||
"自主构建已有策略美术协作测试",
|
||||
)
|
||||
.expect("project init");
|
||||
write_supervisor_collaboration_policy_at(&root, SupervisorCollaborationPolicy::default())
|
||||
.expect("write default project collaboration policy");
|
||||
write_supervisor_collaboration_policy_at(
|
||||
&root,
|
||||
SupervisorCollaborationPolicy {
|
||||
required_initial_wave: SupervisorInitialCollaborationWave::Static,
|
||||
min_static_delegates: 1,
|
||||
required_static_agent_ids: vec!["code-prototype".to_string()],
|
||||
..SupervisorCollaborationPolicy::default()
|
||||
},
|
||||
)
|
||||
.expect("write explicit project collaboration policy");
|
||||
let run_id = "supervisor-autonomous-existing-policy-art-run";
|
||||
bind_game_creator_agent_runtime_run_profile_at(
|
||||
&root,
|
||||
@@ -1857,18 +1854,17 @@ fn supervisor_autonomous_game_build_augments_existing_project_policy_with_requir
|
||||
GAME_CREATOR_PROJECT_SUPERVISOR_AGENT_ID,
|
||||
run_id,
|
||||
)
|
||||
.expect("resolve augmented project collaboration policy");
|
||||
.expect("resolve explicit project collaboration policy");
|
||||
assert_eq!(resolution.source, "project-policy-unbound");
|
||||
assert_eq!(resolution.project_policy_status, "current");
|
||||
assert_eq!(
|
||||
resolution.policy.required_initial_wave,
|
||||
SupervisorInitialCollaborationWave::Static
|
||||
);
|
||||
assert_eq!(resolution.policy.min_static_delegates, 1);
|
||||
assert_eq!(
|
||||
resolution
|
||||
.policy
|
||||
.required_static_agent_ids
|
||||
.iter()
|
||||
.map(String::as_str)
|
||||
.collect::<BTreeSet<_>>(),
|
||||
BTreeSet::from(["art-director"])
|
||||
resolution.policy.required_static_agent_ids,
|
||||
vec!["code-prototype".to_string()]
|
||||
);
|
||||
|
||||
fs::remove_dir_all(root).ok();
|
||||
@@ -1876,8 +1872,7 @@ fn supervisor_autonomous_game_build_augments_existing_project_policy_with_requir
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn supervisor_autonomous_game_build_with_editor_api_key_and_all_visual_assets_skips_visual_delegate(
|
||||
) {
|
||||
fn supervisor_autonomous_game_build_visual_asset_state_does_not_add_hidden_delegates() {
|
||||
let root = unique_project_path();
|
||||
let config_dir = unique_project_path();
|
||||
fs::create_dir_all(&config_dir).expect("create isolated runtime config dir");
|
||||
@@ -1923,13 +1918,10 @@ fn supervisor_autonomous_game_build_with_editor_api_key_and_all_visual_assets_sk
|
||||
assert_eq!(resolution.project_policy_status, "absent");
|
||||
assert_eq!(
|
||||
resolution.policy.required_initial_wave,
|
||||
SupervisorInitialCollaborationWave::Static
|
||||
);
|
||||
assert_eq!(resolution.policy.min_static_delegates, 2);
|
||||
assert_eq!(
|
||||
resolution.policy.required_static_agent_ids,
|
||||
vec!["code-prototype".to_string(), "quality-review".to_string()]
|
||||
SupervisorInitialCollaborationWave::Auto
|
||||
);
|
||||
assert_eq!(resolution.policy.min_static_delegates, 0);
|
||||
assert!(resolution.policy.required_static_agent_ids.is_empty());
|
||||
assert!(!root
|
||||
.join(SUPERVISOR_COLLABORATION_POLICY_RELATIVE_PATH)
|
||||
.exists());
|
||||
@@ -1952,19 +1944,22 @@ fn supervisor_autonomous_game_build_with_editor_api_key_and_all_visual_assets_sk
|
||||
corrupt_run_id,
|
||||
)
|
||||
.expect("resolve autonomous collaboration policy with corrupt art asset");
|
||||
assert_eq!(corrupt_resolution.policy.min_static_delegates, 3);
|
||||
assert_eq!(
|
||||
corrupt_resolution.policy.required_initial_wave,
|
||||
SupervisorInitialCollaborationWave::Auto
|
||||
);
|
||||
assert_eq!(corrupt_resolution.policy.min_static_delegates, 0);
|
||||
assert!(corrupt_resolution
|
||||
.policy
|
||||
.required_static_agent_ids
|
||||
.iter()
|
||||
.any(|agent_id| agent_id == "art-asset-plan"));
|
||||
.is_empty());
|
||||
|
||||
fs::remove_dir_all(root).ok();
|
||||
fs::remove_dir_all(config_dir).ok();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn supervisor_autonomous_legacy_visual_assets_do_not_skip_the_visual_delegate() {
|
||||
fn supervisor_autonomous_legacy_visual_assets_do_not_add_hidden_delegate() {
|
||||
let root = unique_project_path();
|
||||
let config_dir = unique_project_path();
|
||||
fs::create_dir_all(&config_dir).expect("create isolated runtime config dir");
|
||||
@@ -2012,12 +2007,12 @@ fn supervisor_autonomous_legacy_visual_assets_do_not_skip_the_visual_delegate()
|
||||
run_id,
|
||||
)
|
||||
.expect("resolve legacy visual collaboration policy");
|
||||
assert_eq!(resolution.policy.min_static_delegates, 3);
|
||||
assert!(resolution
|
||||
.policy
|
||||
.required_static_agent_ids
|
||||
.iter()
|
||||
.any(|agent_id| agent_id == "art-director"));
|
||||
assert_eq!(
|
||||
resolution.policy.required_initial_wave,
|
||||
SupervisorInitialCollaborationWave::Auto
|
||||
);
|
||||
assert_eq!(resolution.policy.min_static_delegates, 0);
|
||||
assert!(resolution.policy.required_static_agent_ids.is_empty());
|
||||
|
||||
fs::remove_dir_all(root).ok();
|
||||
fs::remove_dir_all(config_dir).ok();
|
||||
|
||||
@@ -92,6 +92,7 @@ fn config_file_overrides_defaults_without_env() {
|
||||
assert_eq!(generator_llm.base_url, "https://generator.example.test/v1");
|
||||
assert_eq!(generator_llm.model, "generator-model");
|
||||
assert_eq!(generator_llm.api_kind, "openai_chat");
|
||||
assert_eq!(generator_llm.reasoning_effort, "high");
|
||||
assert!(generator_llm.web_search_enabled);
|
||||
assert_eq!(config.editor_api.base_url, "http://127.0.0.1:8099");
|
||||
assert_eq!(config.editor_api.api_key, "editor-key");
|
||||
@@ -129,6 +130,199 @@ fn legacy_llm_config_deserialization_supplies_context_budget_defaults() {
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn canonical_agent_reasoning_effort_defaults_are_exhaustive_and_auditable() {
|
||||
let expected = BTreeMap::from([
|
||||
("project-supervisor", "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"),
|
||||
]);
|
||||
let rust_defaults = GAME_CREATOR_LLM_AGENT_REASONING_EFFORT_DEFAULTS
|
||||
.iter()
|
||||
.copied()
|
||||
.collect::<BTreeMap<_, _>>();
|
||||
assert_eq!(rust_defaults, expected);
|
||||
assert_eq!(
|
||||
GAME_CREATOR_LLM_AGENT_REASONING_EFFORT_DEFAULTS.len(),
|
||||
rust_defaults.len(),
|
||||
"规范 Agent 默认映射不能包含重复 ID"
|
||||
);
|
||||
|
||||
let status_agent_ids = game_creator_llm_agent_status_definitions()
|
||||
.into_iter()
|
||||
.map(|definition| definition.agent_id)
|
||||
.collect::<std::collections::BTreeSet<_>>();
|
||||
assert_eq!(
|
||||
status_agent_ids,
|
||||
expected
|
||||
.keys()
|
||||
.map(|agent_id| (*agent_id).to_string())
|
||||
.collect::<std::collections::BTreeSet<_>>(),
|
||||
"新增规范 Agent 时必须先显式选择 reasoning effort,不能静默继承全局"
|
||||
);
|
||||
for (agent_id, effort) in &expected {
|
||||
assert_eq!(
|
||||
game_creator_llm_agent_default_reasoning_effort(agent_id),
|
||||
Some(*effort)
|
||||
);
|
||||
parse_game_creator_llm_reasoning_effort(effort).expect("canonical reasoning effort");
|
||||
}
|
||||
|
||||
let template =
|
||||
serde_json::from_str::<GameCreatorAppConfigFile>(DEFAULT_GAME_CREATOR_APP_CONFIG_JSON)
|
||||
.expect("parse bundled runtime config template");
|
||||
assert_eq!(
|
||||
template.llm.as_ref().and_then(|llm| llm.max_retries),
|
||||
Some(DEFAULT_GAME_CREATOR_LLM_MAX_RETRIES)
|
||||
);
|
||||
assert!(
|
||||
template.agent_llm.unwrap_or_default().is_empty(),
|
||||
"bundled template must not persist canonical defaults as explicit overrides"
|
||||
);
|
||||
|
||||
let ui_source = include_str!("../../../src/features/runtime-config/RuntimeConfigDialog.tsx");
|
||||
let ui_mapping = ui_source
|
||||
.split("const runtimeAgentReasoningEffortDefaults = {")
|
||||
.nth(1)
|
||||
.and_then(|source| source.split("} as const satisfies").next())
|
||||
.expect("frontend Agent reasoning effort contract")
|
||||
.lines()
|
||||
.filter_map(|line| {
|
||||
let line = line.trim().trim_end_matches(',');
|
||||
let (agent_id, effort) = line.split_once(": ")?;
|
||||
Some((
|
||||
agent_id.trim_matches(&['\'', '"'][..]).to_string(),
|
||||
effort.trim_matches(&['\'', '"'][..]).to_string(),
|
||||
))
|
||||
})
|
||||
.collect::<BTreeMap<_, _>>();
|
||||
assert_eq!(
|
||||
ui_mapping,
|
||||
expected
|
||||
.iter()
|
||||
.map(|(agent_id, effort)| ((*agent_id).to_string(), (*effort).to_string()))
|
||||
.collect::<BTreeMap<_, _>>()
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn canonical_reasoning_only_patch_does_not_activate_role_llm_override() {
|
||||
let mut config = GameCreatorAppConfig::default();
|
||||
for (agent_id, effort) in GAME_CREATOR_LLM_AGENT_REASONING_EFFORT_DEFAULTS {
|
||||
config.agent_llm.insert(
|
||||
agent_id.to_string(),
|
||||
GameCreatorLlmConfigFile {
|
||||
reasoning_effort: Some(effort.to_string()),
|
||||
..GameCreatorLlmConfigFile::default()
|
||||
},
|
||||
);
|
||||
assert!(
|
||||
!has_game_creator_agent_llm_override(&config, agent_id),
|
||||
"canonical reasoning-only default must not activate {agent_id} role LLM"
|
||||
);
|
||||
}
|
||||
|
||||
config.agent_llm.insert(
|
||||
"design-director".to_string(),
|
||||
GameCreatorLlmConfigFile {
|
||||
reasoning_effort: Some("high".to_string()),
|
||||
..GameCreatorLlmConfigFile::default()
|
||||
},
|
||||
);
|
||||
assert!(has_game_creator_agent_llm_override(
|
||||
&config,
|
||||
"design-director"
|
||||
));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn empty_legacy_agent_llm_uses_agent_defaults_and_explicit_patch_wins() {
|
||||
let root = unique_project_path();
|
||||
fs::create_dir_all(&root).expect("runtime config dir");
|
||||
fs::write(
|
||||
root.join(GAME_CREATOR_CONFIG_FILE_NAME),
|
||||
r#"{
|
||||
"llm": {
|
||||
"apiKey": "global-key",
|
||||
"baseUrl": "https://global.example.test/v1",
|
||||
"model": "global-model",
|
||||
"reasoningEffort": "default"
|
||||
},
|
||||
"agentLlm": {}
|
||||
}
|
||||
"#,
|
||||
)
|
||||
.expect("write legacy empty Agent config");
|
||||
let _guard = use_test_runtime_config_dir(root.clone());
|
||||
|
||||
let config = load_game_creator_app_config().expect("load legacy empty Agent config");
|
||||
assert!(config.agent_llm.is_empty());
|
||||
for (agent_id, effort) in GAME_CREATOR_LLM_AGENT_REASONING_EFFORT_DEFAULTS {
|
||||
assert_eq!(
|
||||
resolve_game_creator_llm_config_for_agent(&config, agent_id).reasoning_effort,
|
||||
effort
|
||||
);
|
||||
}
|
||||
assert_eq!(
|
||||
resolve_game_creator_llm_config_for_agent(&config, "non-canonical-agent").reasoning_effort,
|
||||
"default"
|
||||
);
|
||||
|
||||
let status = check_game_creator_llm_config_from_config();
|
||||
assert!(status.configured, "{:?}", status.error);
|
||||
for (agent_id, effort) in GAME_CREATOR_LLM_AGENT_REASONING_EFFORT_DEFAULTS {
|
||||
let agent = status
|
||||
.agents
|
||||
.iter()
|
||||
.find(|agent| agent.agent_id == agent_id)
|
||||
.expect("canonical Agent status");
|
||||
assert_eq!(agent.reasoning_effort, effort, "{agent_id}");
|
||||
}
|
||||
|
||||
let mut overridden = config;
|
||||
overridden.agent_llm.insert(
|
||||
GAME_CREATOR_LEGACY_CHAT_AGENT_CONFIG_ID.to_string(),
|
||||
GameCreatorLlmConfigFile {
|
||||
reasoning_effort: Some("medium".to_string()),
|
||||
..GameCreatorLlmConfigFile::default()
|
||||
},
|
||||
);
|
||||
for (agent_id, _) in GAME_CREATOR_LLM_AGENT_REASONING_EFFORT_DEFAULTS {
|
||||
overridden.agent_llm.insert(
|
||||
agent_id.to_string(),
|
||||
GameCreatorLlmConfigFile {
|
||||
reasoning_effort: Some("default".to_string()),
|
||||
..GameCreatorLlmConfigFile::default()
|
||||
},
|
||||
);
|
||||
assert_eq!(
|
||||
resolve_game_creator_llm_config_for_agent(&overridden, agent_id).reasoning_effort,
|
||||
"default",
|
||||
"显式 agentLlm.{agent_id} patch 必须覆盖规范默认值"
|
||||
);
|
||||
}
|
||||
|
||||
fs::remove_dir_all(root).ok();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn runtime_config_dir_supplies_app_config_file() {
|
||||
let root = unique_project_path();
|
||||
@@ -576,6 +770,19 @@ fn llm_config_check_reports_per_agent_status_without_leaking_keys() {
|
||||
assert!(art.configured);
|
||||
assert_eq!(art.label, "美术组 / Asset");
|
||||
assert_eq!(art.model.as_deref(), Some("art-model"));
|
||||
assert_eq!(art.reasoning_effort, "high");
|
||||
let orchestrator = status
|
||||
.agents
|
||||
.iter()
|
||||
.find(|agent| agent.agent_id == "orchestrator")
|
||||
.expect("orchestrator status");
|
||||
assert_eq!(orchestrator.reasoning_effort, "medium");
|
||||
let preview = status
|
||||
.agents
|
||||
.iter()
|
||||
.find(|agent| agent.agent_id == "preview-readiness")
|
||||
.expect("preview status");
|
||||
assert_eq!(preview.reasoning_effort, "low");
|
||||
let serialized = serde_json::to_string(&status).expect("status json");
|
||||
assert!(!serialized.contains("planner-secret-key"));
|
||||
assert!(!serialized.contains("generator-secret-key"));
|
||||
@@ -639,6 +846,9 @@ fn llm_status_cli_lines_include_agent_errors_without_leaking_keys() {
|
||||
context_window_tokens: 128_000,
|
||||
auto_compact_token_limit: 64_000,
|
||||
tool_output_token_limit: 12_000,
|
||||
request_timeout_ms: 180_000,
|
||||
max_retries: 2,
|
||||
retry_backoff_ms: 500,
|
||||
error: Some("Generator:缺少 API Key".to_string()),
|
||||
agents: vec![GameCreatorAgentLlmConfigStatus {
|
||||
agent_id: "generator".to_string(),
|
||||
@@ -654,6 +864,9 @@ fn llm_status_cli_lines_include_agent_errors_without_leaking_keys() {
|
||||
context_window_tokens: 96_000,
|
||||
auto_compact_token_limit: 48_000,
|
||||
tool_output_token_limit: 8_000,
|
||||
request_timeout_ms: 90_000,
|
||||
max_retries: 1,
|
||||
retry_backoff_ms: 250,
|
||||
error: Some("LLM 未配置:请在 agentLlm.generator.apiKey 中设置 API Key".to_string()),
|
||||
}],
|
||||
};
|
||||
@@ -666,6 +879,8 @@ fn llm_status_cli_lines_include_agent_errors_without_leaking_keys() {
|
||||
assert!(lines.contains("llm.agent.generator.webSearchEnabled=false"));
|
||||
assert!(lines.contains("llm.reasoningEffort=high"));
|
||||
assert!(lines.contains("llm.agent.generator.reasoningEffort=medium"));
|
||||
assert!(lines.contains("llm.maxRetries=2"));
|
||||
assert!(lines.contains("llm.agent.generator.maxRetries=1"));
|
||||
assert!(lines.contains("llm.error=Generator:缺少 API Key"));
|
||||
assert!(!lines.contains("sk-"));
|
||||
assert!(!lines.contains("secret"));
|
||||
|
||||
@@ -1017,7 +1017,7 @@ async fn background_agent_runtime_can_search_patch_and_read_in_sequence() {
|
||||
assert!(plan_request.contains("startLine"));
|
||||
assert!(plan_request.contains("expectedReplacements"));
|
||||
assert!(plan_request.contains("\"max_output_tokens\":4000"));
|
||||
assert!(plan_request.contains("\"reasoning\":{\"effort\":\"high\"}"));
|
||||
assert!(plan_request.contains("\"reasoning\":{\"effort\":\"medium\"}"));
|
||||
let verification_request = receiver
|
||||
.recv_timeout(Duration::from_secs(2))
|
||||
.expect("verification llm request");
|
||||
|
||||
@@ -26,6 +26,30 @@ import {
|
||||
type RuntimeMcpStructuredDraft,
|
||||
} from '../../app/types';
|
||||
|
||||
const runtimeAgentReasoningEffortDefaults = {
|
||||
'project-supervisor': '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',
|
||||
} as const satisfies Record<string, GameCreatorLlmReasoningEffort>;
|
||||
|
||||
const defaultRuntimeConfigDraft: GameCreatorAppConfig = {
|
||||
llm: {
|
||||
apiKey: '',
|
||||
@@ -1068,6 +1092,10 @@ export function RuntimeConfigDialog({
|
||||
</label>
|
||||
{runtimeAgentLlmRows.map((agent) => {
|
||||
const agentLlm = runtimeConfigDraft.agentLlm?.[agent.id] ?? {};
|
||||
const defaultReasoningEffort =
|
||||
runtimeAgentReasoningEffortDefaults[
|
||||
agent.id as keyof typeof runtimeAgentReasoningEffortDefaults
|
||||
];
|
||||
return (
|
||||
<Fragment key={agent.id}>
|
||||
<label>
|
||||
@@ -1173,7 +1201,9 @@ export function RuntimeConfigDialog({
|
||||
)
|
||||
}
|
||||
>
|
||||
<option value="">继承</option>
|
||||
<option value="">
|
||||
Agent 默认({defaultReasoningEffort})
|
||||
</option>
|
||||
{gameCreatorLlmReasoningEfforts.map((effort) => (
|
||||
<option key={effort} value={effort}>
|
||||
{effort}
|
||||
|
||||
@@ -64,6 +64,7 @@
|
||||
- 影响范围:`ImageCanvasAssetLibraryModel.ts` 的素材与图层搜索值、图片画布素材 / 图层侧栏搜索和对应前端架构文档;不改变持久化、详情展示、删除、移动或画布保存行为。
|
||||
- 验证方式:模型单测覆盖内部模型和 Provider 不命中、正常模型继续命中且原对象元数据不变;侧栏交互测试覆盖素材与图层两类入口。运行对应 Vitest、`npm run typecheck`、`npm run lint:eslint`、`npm run check:encoding` 和 `git diff --check`。
|
||||
- 关联文档:`docs/technical/【前端架构】图片画布编辑器MVP接入方案-2026-06-11.md`。
|
||||
|
||||
---
|
||||
|
||||
## 2026-07-31 每日免费泥点基础额度纳入后台钱包配置
|
||||
@@ -85,6 +86,7 @@
|
||||
- 影响范围:`scripts/deploy/production-stdb-publish.sh`、`scripts/database-backup-to-oss.mjs`、生产运维门禁和本文档。
|
||||
- 验证方式:`npm run check:database-backup`、`npm run check:production-ops`、`npm run check:encoding`、`git diff --check`;dev 现场还必须确认 transient unit 不在 Jenkins session scope,旧 deferred 归档逐份变为 OSS 已验真对象后被删除,备份锁清空,核心服务与公开接口健康。
|
||||
- 关联文档:`docs/【开发运维】本地开发验证与生产运维-2026-05-15.md`。
|
||||
|
||||
---
|
||||
|
||||
## 2026-07-31 画布 Agent 图片结果单击直接定位
|
||||
@@ -161,6 +163,7 @@
|
||||
- 验证方式:覆盖八类 factory 与 dyn validation / pricing / display / job / formatter / media projection 的 api-server 定向测试,运行 `cargo test -p api-server --manifest-path server-rs/Cargo.toml editor_agent`、`cargo check -p api-server --manifest-path server-rs/Cargo.toml`、`npm run check:rustfmt`、`npm run check:encoding` 和 `git diff --check`。
|
||||
|
||||
---
|
||||
|
||||
## 2026-07-28 AI 游戏创作资源画布布局使用本地双模式 CAS sidecar
|
||||
|
||||
- 背景:项目开发工作台当前只在 React 会话内保存同分类资源的一维拖拽顺序,项目切换或客户端重启后重建默认排列;工作台 PRD 虽已给出二维位置字段,但缺少落盘路径、坐标系、Tauri API、CAS、异常与安全边界,仍不足以直接编码。
|
||||
@@ -5876,3 +5879,10 @@
|
||||
- 美术资源门禁:External Editor API 有效时,`code-prototype` 必须通过 `asset.list` 核对 Canvas 登记的 `assets/art-spritesheet.png`,并在 `game/index.html` 真实引用该文件;manifest、文件或 HTML 引用任一缺失均拒绝完成。确定性 Provider fixture 也必须带该引用,不能用占位内容绕过门禁。
|
||||
- 验证:`agentRuntimeModel.test.ts` 10 项通过;新增 Rust source allowlist、game-chat parent completion 与 Canvas spritesheet reference 合同测试通过;`cargo fmt --check`、`npm run check:encoding`、`git diff --check` 通过。两项既有 Windows `os error 32` 文件锁竞态仍单独记录,未归因于本次改动。
|
||||
- 关联文档:`docs/technical/【技术方案】AI游戏创作智能体App实施计划-2026-06-24.md`、`docs/project-memory/shared-memory/development-workflow.md`。
|
||||
|
||||
## 2026-07-31 autonomous 单一任务图、预览 fail-fast 与逐 Agent 推理默认
|
||||
|
||||
- 决策:`autonomous-game-build` 只允许固定 manifest DAG 作为缺省首轮专业执行链;通用 Project Supervisor collaboration 仍服务 standard profile 和显式项目 policy,但不得再在 autonomous 缺省路径复制 code、quality 或视觉职责。
|
||||
- 决策:预览的业务失败与浏览器基础设施失败分流。基础设施失败按稳定 kind 持久化并立即失败结束当前 run;preview readiness/playtest 的 manifest 完成分别绑定当前 revision smoke 和根合同 browser receipt,read-only 文本交付不能绕过。
|
||||
- 决策:game-chat 的 client-owned Runner 在活动任务期间禁止关闭客户端;关闭前复用既有 durable idle 真相源,避免另建 UI busy 状态。用户明确暂停/取消并达到 idle 后再退出,不能靠重启后自动重放未知 Provider 结果。
|
||||
- 决策:规范 Agent reasoning 默认由角色职责分层,显式 per-Agent patch 优先;配置状态对外展示实际 timing/retry,避免全局文件、per-Agent resolver 与历史 run snapshot 混淆。
|
||||
|
||||
@@ -639,3 +639,10 @@ npm run ai-game-creator-shell:agent-runtime:supervisor-swarm-tool-plan-handoff-r
|
||||
- checkpoint 只允许在目标 tool-plan handoff 原子落盘并逐字段回读一致后、同一实际 requestId lifecycle `completed` 前 ACK;收到 ACK 后才可用 pidfd `SIGKILL` 强杀 suite 自有 Runner。新 boot 恢复前不得出现由该计划产生的 action、pending、delivery、claim 或其它副作用。
|
||||
- 单轮验收必须证明同一 requestId 唯一闭合且没有替代 identity,proxy `networkReplayCount=0`,protocol/repair audit 幂等,handoff plan fingerprint 与恢复后的 durable pending/action batch 对应;终局 retry/tool-plan handoff/provider handoff/finalization/confirmation sidecar、重复 lifecycle/audit/action/message、capability/Runner/AppData 临时资源和正文/API Key/Provider URL/项目及正式配置绝对路径泄漏全部为 `0`。失败轮不得与后续轮拼接。
|
||||
- 当前该 suite 的实现、E2E self-test、Tauri/Rust 串行全量 `1054 passed / 4 ignored / 0 failed`、Linux `cargo check --tests` 与 `x86_64-pc-windows-gnu cargo check --tests` 已通过。2026-07-20 的真实外部 Provider 单轮已到达 checkpoint,并证明旧/新 Runner boot 切换、同一请求恢复、`networkReplayCount=0`、恢复前零 action/pending/delivery 与生命周期唯一闭合;但该轮随后因专业 Agent 连续连接失败而以 FAIL 结束,另一独立轮首批工具数不满足 fixture 也以 FAIL 结束,因此仍没有该 suite 的外部 PASS,且不得拼接两轮证据。Provider 成功到 handoff 原子落盘回读前的 unknown-result 仍未关闭,手动 context-compaction 也不在覆盖内;确定性 mock、命令注册成功或其它 suite PASS 都不能替代单轮完整真实验收。
|
||||
|
||||
## AI 游戏创作长耗时与退出恢复验证
|
||||
|
||||
- 修改 autonomous 调度时,必须证明缺省 policy 不产生 manifest 之外的首波专业委派,Editor Key、已有/损坏视觉资产均不能改变该事实;显式项目 policy 只验证“原样尊重”,不能由 Runtime 隐式扩充。
|
||||
- 修改预览完成门时,分别覆盖 child `preview-readiness` 当前 revision smoke、child `preview-playtest` 到根 Supervisor 合同的 browser receipt,以及 WebSocket 启动前退出的稳定基础设施分类。基础设施错误必须在一次浏览器调用后让 run 失败,不能只做到后续调用快速失败而继续消耗 Provider 轮次。
|
||||
- 修改 game-chat Runner 生命周期时,至少覆盖 busy durable sidecar 拒绝 client-exit、拒绝后 `draining=false` 可继续执行、清空后 idle shutdown、重复 shutdown 幂等;Windows target check 继续保留,不能以删除 Job Object 或放任后台继续来规避 reconciliation。
|
||||
- LLM 配置回归必须穷举全部规范 Agent,校验无遗漏/重复、显式 patch 覆盖默认,并锁定 GUI 展示映射和 Rust resolver 一致;模板与 GUI 初始草稿不得把规范默认持久化成 `agentLlm` 显式覆盖。`--llm-status` 要输出逐 Agent 实际 reasoning/timing/retry 值。运行日志与当前配置冲突时,先区分 durable run snapshot 和后来修改的文件,不能按当前文件反推历史请求。
|
||||
|
||||
@@ -801,3 +801,11 @@ game-project/
|
||||
- 2026-07-27 补充 tool-plan 成功响应交接的内容边界:Provider 的自然语言计划叙述,以及结构化 arguments 中 `body / code / content / css / html / newText / oldText / patch / script / text` 等源码内容字段,只检查真实密钥 token 形状、凭据头标记和不安全控制字符;仅仅提及 `.env` 或 `game-creator.config` 不能阻断已经计费的安全响应。结构化输入中的敏感 JSON key、非内容字段中的配置痕迹或绝对路径、真实 token、容量、thinking、身份、顺序和账本完整性门禁仍失败关闭。成功 handoff 失败进入 reconciliation 时,Runtime 额外只持久化受控 `failureKind`、脱敏错误 SHA-256 和字符数,不保存 Provider 正文、function arguments、密钥或绝对路径。定向回归覆盖叙述/源码字段放行、`.env.local` 路径和真实 token 拒绝、全部 tool-plan handoff 回归及诊断零正文。
|
||||
- 自然语言 interaction 的 `resume` 只代表“继续当前未完成 Runtime”。宿主在进入 interaction 前已确认当前 Session 没有可 steer、pending 或 running 的 Runtime 时,模型返回的自然语言 `resume` 必须规范化为 `execute`,基于会话历史新建 run;显式 `/resume` 仍只执行恢复扫描且无任务时不新建,避免“那就继续修复”被反复吞成空恢复。
|
||||
- tool-plan 成功响应落账前,对内置 Runtime 原生函数与 legacy wrapper 的合法、无重复 key JSON arguments 按工具 schema 的精确位置做项目路径 canonicalization:`file.*.path`、`project.patchset.changes[*].path`、`project.git_commit.paths[*]`、`command.*.cwd`、`image.inspect.paths[*]` 与 `canvas.asset_generate.outputPath` 若是当前项目根目录内的完整绝对路径,转换为 `/` 分隔的项目相对路径后再校验、持久化并执行;源码/叙述字段、任务产物描述、动态 MCP arguments 和项目外绝对路径不得改写,后两者继续由绝对路径门禁失败关闭。项目根只允许搜索/列举范围与命令 cwd 规范化为 `.`,不能成为文件目标。当前进程与重启恢复都必须从同一份规范化 handoff 重放,禁止分别执行原响应和持久响应。
|
||||
|
||||
## 2026-07-31 长耗时与恢复收口
|
||||
|
||||
- `autonomous-game-build` 的固定 manifest DAG 是唯一首轮专业执行链。缺省 collaboration policy 不再额外强制 `code-prototype / quality-review / art-*` 静态首波;显式项目 policy 仍原样生效,但 Runtime 不再按 Editor Key 或已有图片偷偷追加 Agent。Supervisor prompt 同步禁止在 manifest 前复制同职责委派。
|
||||
- `preview-readiness` 只有在自己的 child run 持有当前 project revision 的 `game.static_smoke=passed` 凭证后才能完成;`preview-playtest` 作为根 Supervisor 的直接 manifest child,必须解析并写入根完成合同的当前 revision browser receipt,报告、desktop/mobile 截图及摘要复核通过后才能投影 manifest completed。
|
||||
- 浏览器未发现、临时环境不可建、启动超时或在 WebSocket URL 解析前退出统一分类为 `preview-infrastructure-unavailable`。首个持久 observation 后收束当前 action batch并失败结束 child/root run,禁止继续用 Provider 逐轮规划同一 revision 的重复启动;普通页面/玩法验收失败仍保留为业务失败,不混入基础设施分类。
|
||||
- game-chat release 在 `CloseRequested / ExitRequested` 前复用 Runner durable idle probe;只要存在 process session、pending/finalization/provider/tool-plan handoff 或非终态 Agent queue/phase,就阻止关闭并提示先完成、暂停或取消。不可撤销的最终 `Exit` 不再作为唯一保护点,Windows Job Object 的 child-owned 安全边界保持不变。
|
||||
- 规范 Agent 默认推理档覆盖全部 21 个角色:核心规划、生成、设计/美术/代码原型和质量角色使用 `high`,协调与结构化交付使用 `medium`,确定性预览 gate、音频总监和发布策略使用 `low`;显式 `agentLlm.<id>.reasoningEffort` 始终最高优先。规范默认由 Runtime resolver 解析,模板与 GUI 初始草稿保持 `agentLlm` 为空,避免默认值被误判成角色独立 LLM 路由;GUI 必须显示每个角色的实际默认档。全局与逐 Agent status/CLI 必须同时显示实际解析后的 reasoning、request timeout、max retries 和 retry backoff,区分运行快照与后来配置。
|
||||
|
||||
Reference in New Issue
Block a user