补齐智能体消息活锁回归与验收记录
新增重复 agent.message 的有界活锁回归并验证持久化幂等 收紧 isolated all-join 与 PTY 测试的异步竞态断言 同步自主 Swarm 真实验收结果、工作流和长期踩坑记录
This commit is contained in:
@@ -3623,7 +3623,13 @@ setInterval(() => {}, 1000);
|
||||
"{:?}",
|
||||
transcript.output
|
||||
);
|
||||
assert!(transcript_lines.contains(&"BRIDGE_ENV:"));
|
||||
assert!(
|
||||
transcript_lines
|
||||
.iter()
|
||||
.any(|line| line.ends_with("BRIDGE_ENV:")),
|
||||
"{:?}",
|
||||
transcript.output
|
||||
);
|
||||
let record_json =
|
||||
fs::read_to_string(root.join(process_session_record_relative_path(&record.process_id)))
|
||||
.expect("read process record json");
|
||||
|
||||
@@ -10622,6 +10622,259 @@ async fn background_agent_runtime_marks_unconverged_loop_budget_exhausted() {
|
||||
fs::remove_dir_all(root).ok();
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn background_agent_runtime_bounds_duplicate_agent_message_livelock() {
|
||||
let root = unique_project_path();
|
||||
init_local_game_project_at(&root, "project-1", "定向消息停滞项目").expect("project init");
|
||||
write_project_permission_policy_at(
|
||||
&root,
|
||||
ProjectPermissionPolicy {
|
||||
denied_commands: Vec::new(),
|
||||
confirm_commands: Vec::new(),
|
||||
agent_policies: BTreeMap::new(),
|
||||
},
|
||||
)
|
||||
.expect("allow runtime collaboration writes");
|
||||
|
||||
let (sender, receiver) = mpsc::channel();
|
||||
let responses = (1..=AGENT_RUNTIME_BACKGROUND_LOOP_LIMIT)
|
||||
.map(|iteration| {
|
||||
serde_json::json!({
|
||||
"thinkingSummary": format!("第 {iteration} 轮重复发送同一条进展消息"),
|
||||
"planUpdate": {
|
||||
"explanation": "等待自身专业交付完成",
|
||||
"steps": [{
|
||||
"step": "完成专业交付并返回最终回执",
|
||||
"status": "in_progress"
|
||||
}]
|
||||
},
|
||||
"plan": [],
|
||||
"actions": [{
|
||||
"tool": "agent.message",
|
||||
"reason": "重复同一消息以验证 durable no-op 不伪造进展",
|
||||
"input": {
|
||||
"agentId": "project-supervisor",
|
||||
"content": "专业交付仍在处理中,请等待。"
|
||||
}
|
||||
}],
|
||||
"response": ""
|
||||
})
|
||||
.to_string()
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
let base_url = spawn_mock_llm_server_responses_with_capture(responses, Some(sender));
|
||||
let _config_guard = write_test_local_config(format!(
|
||||
r#"{{
|
||||
"agentLlm": {{
|
||||
"design-director": {{
|
||||
"apiKey": "design-key",
|
||||
"baseUrl": {base_url:?},
|
||||
"model": "design-runtime-model",
|
||||
"apiKind": "openai_responses"
|
||||
}}
|
||||
}}
|
||||
}}"#
|
||||
));
|
||||
|
||||
let run_id = "design-duplicate-message-livelock-run";
|
||||
start_game_creator_agent_background_task_at(
|
||||
&root,
|
||||
"design-director",
|
||||
"验证重复定向消息不会制造无限进展",
|
||||
run_id,
|
||||
)
|
||||
.expect("start background task");
|
||||
for iteration in 1..=AGENT_RUNTIME_BACKGROUND_LOOP_LIMIT {
|
||||
let request = receiver
|
||||
.recv_timeout(Duration::from_secs(2))
|
||||
.expect("planning request");
|
||||
assert!(request.contains(&format!("第 {iteration} 轮")));
|
||||
if iteration == 2 {
|
||||
assert!(request.contains("已给 project-supervisor 留消息"));
|
||||
} else if iteration > 2 {
|
||||
assert!(request.contains("messageAppended=false"));
|
||||
assert!(request.contains("相同定向消息已存在"));
|
||||
}
|
||||
}
|
||||
assert!(
|
||||
receiver.recv_timeout(Duration::from_millis(200)).is_err(),
|
||||
"duplicate no-op must not request a seventh Provider plan"
|
||||
);
|
||||
|
||||
let mut result =
|
||||
read_game_creator_agent_runtime_at(&root, "design-director").expect("read runtime");
|
||||
for _ in 0..50 {
|
||||
if result.state.status == "failed" {
|
||||
break;
|
||||
}
|
||||
std::thread::sleep(Duration::from_millis(20));
|
||||
result =
|
||||
read_game_creator_agent_runtime_at(&root, "design-director").expect("read runtime");
|
||||
}
|
||||
assert_eq!(result.state.status, "failed");
|
||||
assert_eq!(result.state.phase, "budget-exhausted");
|
||||
assert_eq!(result.state.plan_revision, 1);
|
||||
assert_eq!(result.state.plan_steps.len(), 1);
|
||||
assert_eq!(result.state.plan_steps[0].status, "in_progress");
|
||||
assert!(result
|
||||
.state
|
||||
.error
|
||||
.as_deref()
|
||||
.is_some_and(|error| error.contains("loop-budget-exhausted")));
|
||||
assert!(!result
|
||||
.recent_events
|
||||
.iter()
|
||||
.any(|event| event.event_type == "turn.completed"));
|
||||
assert!(!result
|
||||
.recent_events
|
||||
.iter()
|
||||
.any(|event| event.event_type == "context.window_checkpoint"));
|
||||
let message_tool_calls = result
|
||||
.state
|
||||
.recent_tool_calls
|
||||
.iter()
|
||||
.filter(|call| call.tool == "agent.message")
|
||||
.collect::<Vec<_>>();
|
||||
assert_eq!(
|
||||
message_tool_calls.len(),
|
||||
AGENT_RUNTIME_BACKGROUND_LOOP_LIMIT,
|
||||
"all six duplicate attempts must reach the durable action ledger"
|
||||
);
|
||||
assert_eq!(
|
||||
message_tool_calls
|
||||
.iter()
|
||||
.filter_map(|call| call.action_id.as_deref())
|
||||
.collect::<BTreeSet<_>>()
|
||||
.len(),
|
||||
AGENT_RUNTIME_BACKGROUND_LOOP_LIMIT
|
||||
);
|
||||
assert_eq!(
|
||||
message_tool_calls
|
||||
.iter()
|
||||
.filter_map(|call| call.action_fingerprint.as_deref())
|
||||
.collect::<BTreeSet<_>>()
|
||||
.len(),
|
||||
1,
|
||||
"semantic retries must share one action fingerprint"
|
||||
);
|
||||
assert_eq!(
|
||||
message_tool_calls
|
||||
.iter()
|
||||
.filter(|call| call.detail.as_deref() == Some("messageAppended=false"))
|
||||
.count(),
|
||||
AGENT_RUNTIME_BACKGROUND_LOOP_LIMIT - 1
|
||||
);
|
||||
|
||||
let supervisor_conversation = read_local_conversation_at(&root, Some("project-supervisor"))
|
||||
.expect("read supervisor conversation");
|
||||
assert_eq!(
|
||||
supervisor_conversation
|
||||
.messages
|
||||
.iter()
|
||||
.filter(|message| message.role == "tool")
|
||||
.count(),
|
||||
1,
|
||||
"semantic retries must append the target message exactly once"
|
||||
);
|
||||
let agent_db = read_agent_db_records_for_test(&root);
|
||||
assert_eq!(
|
||||
agent_db
|
||||
.iter()
|
||||
.filter(|record| {
|
||||
record.get("recordType").and_then(Value::as_str)
|
||||
== Some("agent.runtime.agent.message")
|
||||
&& record.get("agentId").and_then(Value::as_str) == Some("design-director")
|
||||
&& record.get("runId").and_then(Value::as_str) == Some(run_id)
|
||||
})
|
||||
.count(),
|
||||
1,
|
||||
"semantic retries must write one message audit"
|
||||
);
|
||||
let conversation_message_records = agent_db
|
||||
.iter()
|
||||
.filter(|record| {
|
||||
record.get("recordType").and_then(Value::as_str) == Some("conversation.message")
|
||||
&& record.get("agentId").and_then(Value::as_str) == Some("project-supervisor")
|
||||
&& record.get("role").and_then(Value::as_str) == Some("tool")
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
assert_eq!(conversation_message_records.len(), 1);
|
||||
let executing_records = agent_db
|
||||
.iter()
|
||||
.filter(|record| {
|
||||
record.get("recordType").and_then(Value::as_str)
|
||||
== Some("agent.runtime.tool_action.executing")
|
||||
&& record.get("runId").and_then(Value::as_str) == Some(run_id)
|
||||
&& record.get("tool").and_then(Value::as_str) == Some("agent.message")
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
assert_eq!(executing_records.len(), AGENT_RUNTIME_BACKGROUND_LOOP_LIMIT);
|
||||
assert_eq!(
|
||||
executing_records
|
||||
.iter()
|
||||
.filter_map(|record| record.get("actionId").and_then(Value::as_str))
|
||||
.collect::<BTreeSet<_>>()
|
||||
.len(),
|
||||
AGENT_RUNTIME_BACKGROUND_LOOP_LIMIT
|
||||
);
|
||||
assert_eq!(
|
||||
executing_records
|
||||
.iter()
|
||||
.filter_map(|record| record.get("actionFingerprint").and_then(Value::as_str))
|
||||
.collect::<BTreeSet<_>>()
|
||||
.len(),
|
||||
1
|
||||
);
|
||||
for executing in &executing_records {
|
||||
assert_auto_tool_action_audit_pair(
|
||||
&agent_db,
|
||||
run_id,
|
||||
executing
|
||||
.get("actionId")
|
||||
.and_then(Value::as_str)
|
||||
.expect("action id"),
|
||||
executing
|
||||
.get("actionFingerprint")
|
||||
.and_then(Value::as_str)
|
||||
.expect("action fingerprint"),
|
||||
"agent.message",
|
||||
"ok",
|
||||
);
|
||||
}
|
||||
let receipts = agent_db
|
||||
.iter()
|
||||
.filter(|record| {
|
||||
record.get("recordType").and_then(Value::as_str)
|
||||
== Some(AGENT_RUNTIME_ACTION_RECEIPT_RECORD_TYPE)
|
||||
&& record.get("runId").and_then(Value::as_str) == Some(run_id)
|
||||
&& record.get("tool").and_then(Value::as_str) == Some("agent.message")
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
assert_eq!(receipts.len(), AGENT_RUNTIME_BACKGROUND_LOOP_LIMIT);
|
||||
assert!(receipts.iter().all(|record| {
|
||||
record.get("status").and_then(Value::as_str) == Some("ok")
|
||||
&& record.get("detailUnavailable").and_then(Value::as_bool) == Some(true)
|
||||
&& record.get("safeDetail").is_some_and(Value::is_null)
|
||||
}));
|
||||
assert!(agent_db.iter().any(|record| {
|
||||
record.get("recordType").and_then(Value::as_str)
|
||||
== Some("agent.runtime.background_task.failed")
|
||||
&& record.get("runId").and_then(Value::as_str) == Some(run_id)
|
||||
&& record.get("failureKind").and_then(Value::as_str) == Some("loop-budget-exhausted")
|
||||
}));
|
||||
assert!(!agent_db.iter().any(|record| {
|
||||
record.get("recordType").and_then(Value::as_str)
|
||||
== Some("agent.runtime.background_task.completed")
|
||||
&& record.get("runId").and_then(Value::as_str) == Some(run_id)
|
||||
}));
|
||||
assert!(!agent_db.iter().any(|record| {
|
||||
record.get("recordType").and_then(Value::as_str) == Some("agent.runtime.context.compacted")
|
||||
&& record.get("runId").and_then(Value::as_str) == Some(run_id)
|
||||
}));
|
||||
|
||||
fs::remove_dir_all(root).ok();
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn background_agent_runtime_checkpoints_full_context_across_multiple_windows() {
|
||||
let root = unique_project_path();
|
||||
@@ -13901,7 +14154,14 @@ async fn isolated_agents_with_same_template_run_independently_and_join_once() {
|
||||
write_game_creator_agent_runtime_state(&root, &parent_state)
|
||||
.expect("persist waiting parent state");
|
||||
drop(parent_lock);
|
||||
let joins = reconcile_all_isolated_groups_at(&root).expect("reconcile ready all-join");
|
||||
let mut joins = Vec::new();
|
||||
for _ in 0..100 {
|
||||
joins = reconcile_all_isolated_groups_at(&root).expect("reconcile ready all-join");
|
||||
if !joins.is_empty() {
|
||||
break;
|
||||
}
|
||||
std::thread::sleep(Duration::from_millis(20));
|
||||
}
|
||||
assert_eq!(joins.len(), 1);
|
||||
let join = joins[0].clone();
|
||||
dispatch_isolated_agent_join_at(&root, join.clone()).expect("wake waiting parent run");
|
||||
|
||||
@@ -16,6 +16,16 @@
|
||||
|
||||
---
|
||||
|
||||
## 2026-07-17 AI 游戏创作 V1.30 使用自主 Supervisor 终端门禁和语义消息收敛
|
||||
|
||||
- 背景:V1.28 已证明预置双专业方向下的合同委派、repair、Runner 恢复和唯一回复,V1.29 已证明受控瞬态重试;但二者都没有证明 Supervisor 在用户不提供 Agent ID、数量、并行或 repair 配方时会自主编排,也没有把重复 `agent.message` 的持久幂等与后台 loop 有界收敛串成完整证据。
|
||||
- 自主编排:保留 `project-supervisor` 作为正式用户唯一对话与最终回复 Agent。`supervisor-swarm-autonomous-chat` 必须通过真实发布二进制的 `--swarm-chat` 接收纯业务任务,由 Supervisor 在同一 native planning 批次自主选择至少两个不同规范专业 Agent;真实 child Provider 生命周期必须重叠。弱交付只能由 Supervisor 按 acceptance criteria 做语义裁决,并在同一父 Session/run 创建唯一、完整继承原合同的单层 repair。终局以 durable delivery/claim/receipt/finalization、唯一 Supervisor assistant 和单行脱敏 `turn.report` 为事实源。
|
||||
- 消息收敛:`agent.message` 的语义身份固定为来源 Agent/run、目标 Agent/已解析 Session 与清洗截断后正文 SHA-256。相同语义重放必须复用唯一 conversation message 与 `agent.runtime.agent.message` 审计,冲突失败关闭;不同来源、run、目标、Session 或正文仍是新消息。重复调用返回 `messageAppended=false`,不算上下文窗口的新进展,也不能替代专业 Agent 自身最终回执。持续重复时最多在当前 6 轮停滞窗口结束后进入 `failed / budget-exhausted / loop-budget-exhausted`,原 `in_progress` 计划保持原样,不能写 completed 或成功回复;每次 Runtime action/observation/receipt 仍完整留痕且公共 receipt 不保存正文。
|
||||
- E2E 隔离:自主 suite 的 sentinel AppData 必须创建在正式 AppData 同级,不能嵌套在源目录;正式目录只读,配置副本、source-dir guard、endpoint 身份、CLI 调用计数和自动清理均进入硬门禁。父 Supervisor 在 repair 前执行的 `project.verify` 属于合法宿主验证,harness 只能拒绝其它意外父 pending action,不能把父验证和专业 Agent 修改确认一刀切。
|
||||
- 验证:最终正式 `openai_chat / gpt-5.5` 诊断轮为 PASS:无编排配方任务下完成双专业 Provider 真重叠、2 个初始 delivery、1 个 repair、2 个 Observed claim、pidfd Runner 强杀/boot 恢复、同一父 Session/run、严格宿主验证、唯一正式 assistant 和 3 条内部专业 assistant。51 个 Provider request identity 全部 `started -> completed`,28/28 成功计划和 19/19 格式修复全为 `native_runtime_tools`;重复、残留 sidecar、Provider payload、私有正文、API Key、诱饵、项目/配置路径和报告泄漏均为 0。确定性完整 loop 回归另证明 6 次重复消息 action 全部落账、目标消息/两类消息审计各 1 条、第 6 轮预算失败且第 7 次 Provider 请求、compaction 和 completed 均为 0。
|
||||
- 范围:V1.30 证明自主 static 专业编排与真实终端聊天可组合;同一父 run 的 static delivery + isolated all-join 真实组合,以及 Tauri/WebView 宿主级 Supervisor E2E 仍是独立后续门禁。
|
||||
- 关联文档:`docs/technical/【技术方案】AI游戏创作Agent Runtime V1.1-2026-07-12.md`、`docs/technical/【技术方案】AI游戏创作智能体App实施计划-2026-06-24.md`。
|
||||
|
||||
## 2026-07-17 AI 游戏创作 Swarm 显式重试必须使用受控真实故障门禁
|
||||
|
||||
- 背景:V1.28 `supervisor-swarm` 正式报告的 46 个 Provider request 全部 completed;确定性测试和条件式 E2E validator 虽覆盖 retry 契约,但 `failed=0 / retry=0` 仍可 PASS,不能证明真实 Provider Swarm 进入过显式重试链。
|
||||
|
||||
@@ -104,6 +104,22 @@ npm run ai-game-creator-shell:agent-runtime:supervisor-swarm-transient-retry-rea
|
||||
|
||||
真实 PASS 必须恰好包含 1 个 failed lifecycle、1 条 retry audit 和 1 个 `-transient-1` 后继 identity;forwarding gate 放行前 action、receipt、目标 Agent 子委派、claim、assistant、pending、project revision 与 upstream forwarding 全为 0。放行后仍须完成双专业 Agent 重叠、唯一 repair、Runner 强杀恢复、唯一 Supervisor assistant 和零重复/残留/泄漏。suite 只能读正式 AppData,在其同级目录写入 sentinel 管理的 `0600` 私有副本和 overlay;启动 CLI/Runner 时须把 loopback 合并进大小写两套 no-proxy 环境,防止系统 HTTP 代理绕过本地故障门禁;source-dir guard 必须证明本 suite 前缀未进入源目录,源配置和 endpoint 身份保持不变,报告不得保存 Provider URL、headers、正文、凭据或绝对配置路径。若后续 repair/恢复/终局失败,partial report 仍应保留已经取得的 retry checkpoint。
|
||||
|
||||
### AI 游戏创作自主 Swarm 终端复验
|
||||
|
||||
修改 Supervisor 自主编排、`agent.message`、static delivery/claim/repair、Swarm CLI `turn.report`、Runner 恢复或 autonomous harness 后,先跑确定性收敛门禁,再运行真实终端 suite:
|
||||
|
||||
```bash
|
||||
cargo test --manifest-path apps/ai-game-creator-shell/src-tauri/Cargo.toml background_agent_runtime_bounds_duplicate_agent_message_livelock -- --nocapture
|
||||
cargo test --manifest-path apps/ai-game-creator-shell/src-tauri/Cargo.toml agent_runtime_context_window_ -- --nocapture
|
||||
cargo test --manifest-path apps/ai-game-creator-shell/src-tauri/Cargo.toml project_supervisor_ -- --nocapture
|
||||
cargo test --manifest-path apps/ai-game-creator-shell/src-tauri/Cargo.toml swarm_cli::tests:: -- --nocapture
|
||||
npm run ai-game-creator-shell:agent-runtime:supervisor-swarm-autonomous-chat-real-e2e -- --config-dir <AppData>
|
||||
```
|
||||
|
||||
业务任务和一次性仓库规则不得出现 Agent ID、数量、并行/同轮、工具、repair 次数、run/action/delegation 或 Runner 配方。PASS 必须由真实发布二进制的 `--swarm-chat` 自主形成至少两个不同专业 Agent 的同批委派和真实 Provider 重叠,在 acceptance criteria 不满足时只形成一个继承原合同的 repair;repair 确认边界执行 Runner 强杀后仍保持父 Session/run、delivery、claim、pending action 和 Provider started 身份。父 run 的 `project.verify` 是允许的宿主验证,其它意外父 pending action继续失败关闭。
|
||||
|
||||
终局必须同时得到 `turn.report=settled`、新增 Supervisor assistant 恰好 1、专业 assistant 只在内部 Session、队列/确认/用户输入/reconciliation/sidecar 全 0,以及重复 action/delivery/message/receipt/Provider lifecycle 和正文/凭据/绝对路径泄漏全 0。`agent.message` 完整回归还要证明同语义消息只写一次、后续 no-op 不刷新进展、6 轮后保持未完成计划并诚实 `budget-exhausted`。隔离 AppData 必须位于正式 AppData 同级并自动清理;失败尝试与后续 PASS 不能拼接,`maxRetries=0` 下的真实外部 Provider 失败应单独保留为失败证据。
|
||||
|
||||
### AI 游戏创作 Runtime V1.10 持久进程定向复验
|
||||
|
||||
V1.10 的 PTY 只通过四个 Runner-owned 工具开放;不要把 V1.2 `command.exec` 改成长驻入口。最小工具输入保持结构化:
|
||||
|
||||
@@ -14,6 +14,46 @@
|
||||
- 关联:相关文件、文档、提交或 Issue
|
||||
```
|
||||
|
||||
## 重复成功的 agent.message 不能被当成新的 Runtime 进展
|
||||
|
||||
- 现象:专业 Agent 已把一条定向消息写入目标 Session,却在后续 planning 中反复发送相同正文;目标会话看起来没有重复消息,但 Provider 请求持续增长,run 可能长期不返回自身终态回执。
|
||||
- 原因:conversation 层的 messageId 幂等只能阻止重复落盘。若每个新 Runtime action 的 `status=ok` 都进入上下文进展指纹,相同 durable no-op 会不断刷新 6 轮停滞窗口;只检查目标会话条数无法证明 action loop 已有界收束。
|
||||
- 处理:消息语义键必须包含来源 Agent/run、目标 Agent/已解析 Session 和清洗截断后正文 SHA-256;conversation message、`conversation.message` 和 `agent.runtime.agent.message` 各自 exactly-once。重复调用继续完整记录自己的 action/observation/receipt,但私有 observation 固定返回 `messageAppended=false`,ContextWindowTracker 只忽略这一精确 no-op,不能忽略不同正文的新消息。专业 Agent prompt 同时明确中途消息不能替代自身 final response。
|
||||
- 验证:`background_agent_runtime_bounds_duplicate_agent_message_livelock` 必须真实驱动 6 个相同指纹、不同 actionId 的消息动作,证明 action/observation/receipt 各 6 条,目标消息和两类消息审计各 1 条,后 5 次不算进展,第 6 轮保留 `in_progress` 计划并进入 `budget-exhausted`,没有第 7 次 Provider 请求、context compaction 或 completed。另保留 `agent_runtime_context_window_counts_distinct_agent_message_bodies`,防止把真正不同的新消息误压成 no-op。
|
||||
- 关联:`apps/ai-game-creator-shell/src-tauri/src/agent.rs`、`apps/ai-game-creator-shell/src-tauri/src/project.rs`、`apps/ai-game-creator-shell/src-tauri/src/tests.rs`、`docs/technical/【技术方案】AI游戏创作Agent Runtime V1.1-2026-07-12.md`。
|
||||
|
||||
## Swarm E2E 的隔离 AppData 不能建在正式 AppData 里面
|
||||
|
||||
- 现象:真实 suite 自称使用隔离配置,但一次性 AppData 出现在正式 AppData 子目录;源目录 watcher、配置副本计数和清理归属变得含糊,Runner 还可能把临时 endpoint 或运行态写进正式目录树。
|
||||
- 原因:把 `mkdtemp` 前缀拼在 source config dir 内,只隔离了文件名,没有隔离目录所有权;source-dir guard 无法区分 suite 自己的合法子目录写入与污染,失败清理也可能触碰正式目录边界。
|
||||
- 处理:需要保护正式配置的 suite 一律在 `dirname(realConfigDir)` 下创建 sentinel 管理的 sibling AppData,并要求 realpath 后与源目录同父、互不包含。配置只使用私有副本或受控 hardlink/overlay,启动 CLI/Runner 全部指向 sibling;清理前核对 sentinel、源配置 inode/hash/link count、source-dir 前缀事件、正式 endpoint 身份和正式 CLI 调用计数,随后只删除拥有明确 token 的临时目录。
|
||||
- 验证:真实报告必须同时满足 `isolatedAppDataUsed=true`、`sourceAppDataDirectoryUntouched=true`、`sourceRunnerEndpointUnchanged=true`、`formalConfigCliCallCount=0`、配置副本校验和 `AppDataCleanupPerformed=true`;项目选择 `--keep-project` 时也不能改变 AppData 自动清理。
|
||||
- 关联:`apps/ai-game-creator-shell/scripts/agent-runtime-real-e2e.mjs`、`docs/technical/【技术方案】AI游戏创作Agent Runtime V1.1-2026-07-12.md`。
|
||||
|
||||
## 异步 Runtime 测试不能把 child idle 当成终态结果已发布
|
||||
|
||||
- 现象:isolated child 已显示 idle,单次 all-join reconcile 却偶发返回空列表,完整 Rust suite 里出现低概率失败,单独重跑通常通过。
|
||||
- 原因:child Runtime 释放执行 lane 与持久化终态 result、发布 join readiness 不是同一个原子观测点;测试只等待 idle,会在终态 result 发布前抢先 reconcile。
|
||||
- 处理:产品协议仍以 durable terminal result 和 join readiness 为准。测试在有界时限内重复调用幂等 reconcile,直到取得唯一 join 或超时;不得靠固定长 sleep,也不能因为第一次为空就把协议改成吞掉未完成 child。
|
||||
- 验证:`isolated_agents_with_same_template_run_independently_and_join_once` 最多执行 100 次、每次间隔 20ms 的 reconcile,并继续断言只有一个 all-join 和一次父唤醒。
|
||||
- 关联:`apps/ai-game-creator-shell/src-tauri/src/tests.rs`、`apps/ai-game-creator-shell/src-tauri/src/agent.rs`。
|
||||
|
||||
## PTY 测试不能假设输入回显与后续输出必然分行
|
||||
|
||||
- 现象:PTY 环境隔离用例偶发得到 `你好BRIDGE_ENV:`,而不是独立的 `你好` 与 `BRIDGE_ENV:` 两行;真实私有环境变量并未泄漏,但整行相等断言失败。
|
||||
- 原因:canonical PTY 的输入回显和目标进程后续输出存在合法调度竞争,读取边界不等于逻辑行边界,回显可能与紧随其后的固定标记合并。
|
||||
- 处理:对不含秘密的固定标记按语义边界断言,例如要求某行以标记结尾;敏感值仍必须在完整 transcript 和公共持久面执行严格零命中扫描,不能借此放宽泄漏门禁。
|
||||
- 验证:`process_session_pty_uses_private_environment_and_redacts_public_records` 对 `BRIDGE_ENV:` 使用行尾匹配,并保留真实私有环境变量、stdin 正文与公共记录泄漏扫描。
|
||||
- 关联:`apps/ai-game-creator-shell/src-tauri/src/process_session.rs`、`apps/ai-game-creator-shell/src-tauri/src/command_output.rs`。
|
||||
|
||||
## 自主 Swarm 验收不能把父 project.verify 当成意外确认动作
|
||||
|
||||
- 现象:两个专业 Agent 已完成初始交付,Supervisor 在语义 repair 前合法执行项目宿主验证,但 E2E harness 把所有父 run pending action 一律拒绝,导致真实协作链在业务逻辑正常时提前失败。
|
||||
- 原因:验收器把“repair 前不允许父 Agent 绕过专业工作”错误实现成“父 run 不能出现任何确认动作”,混淆了 Supervisor 自己的 `project.verify` 与会改变专业交付/文件的意外动作。
|
||||
- 处理:确认过滤器必须按 owning run 和 tool 精确判断。repair 前允许当前父 run 的 `project.verify`,仍拒绝其它未列入场景合同的父 pending action;专业 Agent 的修改和验证继续按各自 run、policy 和预期确认集合处理。允许确认不等于通过验收,最终仍由 host oracle、最新 revision verification、delivery/claim/repair 和唯一回复共同裁决。
|
||||
- 验证:自主 suite 必须出现有效 `hostVerificationPassed=true`,同时保持恰好 2 个初始 + 1 个 repair delivery、父计划完成、意外 pending 为 0、Runner 强杀恢复和唯一 Supervisor assistant;若放宽后出现额外父写动作,场景必须失败而不是吞掉。
|
||||
- 关联:`apps/ai-game-creator-shell/scripts/agent-runtime-real-e2e.mjs`、`docs/technical/【技术方案】AI游戏创作Agent Runtime V1.1-2026-07-12.md`。
|
||||
|
||||
## Provider 全成功的真实报告不能证明显式重试可用
|
||||
|
||||
- 现象:真实 Swarm 报告显示全部 Provider lifecycle completed,E2E 的 retry validator 也没有报错,于是文档把“支持瞬态重试”一并写成已真实验收。
|
||||
|
||||
@@ -1138,8 +1138,15 @@ V1.30 新增独立 `supervisor-swarm-autonomous-chat` 真实 Provider suite,
|
||||
- `--swarm-chat --init <project>` 必须由真实发布二进制启动,省略 parentAgentId 后进入 `project-supervisor`;用户任务通过 stdin 发送,所有确认也经同一终端 `approve` 入口完成。允许 Supervisor 先做必要读取,但首个包含专业委派的 native Provider 批次必须自主选择至少两个不同规范专业 Agent,并在同批形成两个初始合同;两个 child 的真实 Provider lifecycle 必须重叠,不能用同一 Agent 的 retry/format repair 或仅凭 delegate action 时间冒充并行。
|
||||
- 弱质量 claim 进入 `Observed` 后,Supervisor 必须在同一父 Session/run 自主创建引用原 delivery 的唯一 repair;目标 Agent、acceptanceCriteria 和 expectedArtifacts 必须完整继承,repair action 必须晚于弱 claim、早于唯一最终回复。repair 待确认边界继续执行 pidfd Runner 强杀与 boot 恢复,任务、delivery、claim、pending action 和 Provider started 身份不得漂移或重放。
|
||||
- 终局必须同时满足:严格 host oracle 判定两项产物语义正确,最后修改后的验证凭证有效;`[turn.report]` 为 v1/settled、父 Agent/Session/run 与 journal 一致、新增 assistant 恰好 1、队列和 reconciliation 计数为 0;正式用户会话只有 1 条 user 和 1 条 Supervisor assistant,专业 assistant 仅留在内部 Session;无 steer、重复 delivery/action/message/receipt/Provider lifecycle、残留 sidecar、Provider payload、私有正文、API Key、诱饵、项目/正式配置路径或报告泄漏,隔离 Runner/AppData/项目全部清理。任何一次带更明确提示的重跑都只能算新的失败后尝试,不能与原 run 拼接成 PASS。
|
||||
- `agent.message` 的语义身份固定绑定来源 Agent/run、目标 Agent/已解析 Session 和清洗截断后正文 SHA-256。同一语义消息重放只能复用唯一 conversation message 与 `agent.runtime.agent.message` 审计,并以 `messageAppended=false` 返回 durable no-op;不同正文、目标、Session、来源 Agent 或来源 run 仍是新消息。该 no-op 不得计入上下文窗口的新进展,也不能替代专业 Agent 自身最终回执。若模型持续重复同一消息,Runtime 最迟在当前完整 6 轮停滞窗口结束时写 `failed / budget-exhausted / loop-budget-exhausted`,保留原 `in_progress` 计划,不写 completed 或伪造成功回复;每个尝试的 action/observation/receipt 仍须完整落账且公共 receipt 不保存消息正文。
|
||||
|
||||
该 suite 完成后只能证明自主 static 专业编排与真实终端聊天可组合;同一父 run 的 static delivery + isolated all-join 组合恢复,以及 Tauri/WebView 宿主级 Supervisor E2E 仍需各自独立门禁。
|
||||
2026-07-17 最终正式 `openai_chat / gpt-5.5` 诊断轮 **PASS**。唯一业务任务未提供 Agent ID、Agent 数量、并行、工具、repair 或 Runner 配方;Supervisor 在 1 个 native planning 批次自主选择 2 个不同专业 Agent,真实 Provider 区间重叠,并在同一父 Session/run 完成 `2` 份初始 delivery、`1` 次语义 repair、`2` 个 Observed claim、严格宿主验证、pidfd Runner 强杀、boot 切换和身份稳定恢复。最终 `[turn.report]` 为 `settled`,正式会话新增 Supervisor assistant 恰好 `1`,内部专业 assistant 为 `3`;父计划 `4/4` completed,pending/running/confirmation/user-input/reconciliation 均为 `0`。
|
||||
|
||||
该轮共形成 110 条 task、197 条 event、330 条 Agent DB 和 9 条会话消息;51 个 Provider request identity 全部唯一闭合为 `51 started / 51 terminal / 51 completed / 0 failed`,28/28 个成功工具计划和 19/19 个格式修复均为 `native_runtime_tools`,wrapper/text fallback 为 `0`。delivery、message、action lifecycle、executing action、receipt、Provider lifecycle 的重复计数均为 `0`,所有 batch/finalization/confirmation/user-input sidecar 为 `0`,Provider payload、私有正文、API Key、诱饵、项目/正式配置绝对路径和报告泄漏均为 `0`。隔离 AppData 自动清理;保留的 disposable 项目经 sentinel/进程核对后手动删除。此前两次独立尝试在 `maxRetries=0` 下各遇到 1 次外部 Provider 终态失败并在恢复边界前停止,均只作失败证据,未与本轮拼接。
|
||||
|
||||
确定性 `background_agent_runtime_bounds_duplicate_agent_message_livelock` 同时证明:6 次同指纹 Runtime action/observation/receipt 全部实际落账,目标 conversation、`conversation.message` 和 `agent.runtime.agent.message` 各仅 1 条,后 5 次为 durable no-op,第 6 轮保留 `in_progress` 计划并进入 `budget-exhausted`,不存在第 7 次 Provider 请求、context compaction 或 completed 投影。
|
||||
|
||||
V1.30 至此只证明自主 static 专业编排与真实终端聊天可组合;同一父 run 的 static delivery + isolated all-join 真实组合恢复,以及 Tauri/WebView 宿主级 Supervisor E2E 仍需各自独立门禁。
|
||||
|
||||
## 验收命令
|
||||
|
||||
|
||||
@@ -54,6 +54,8 @@ V1.11 的受保护仓库控制目录同时包含 `.git / .agent / .agents / .cod
|
||||
|
||||
当前委派协议已把 delivery 与 `Prepared -> Committed -> Observed` claim journal 作为事实源,认领前按 delegationId 排序并取得全部 delivery 锁,`.agent/agent.db` 只作 best-effort 诊断投影。恢复中的 executing 动作只允许 Supervisor `agent.delegate / agent.run_status` 经项目锁、pending 全身份和 policy 重验后补交;parent-wake 使用 singleflight、有界错误分类和稳定 Runner requestId。子终态只有在 parent/child/delivery 完整身份一致后才能 ready 或 suppression;错配不得改写 delivery。最终回复继续由原父 run 的 finalization journal 幂等写入。
|
||||
|
||||
2026-07-17 起,Runtime V1.30 已把真实 `agc:chat / --swarm-chat` 与自主专业编排纳入同一门禁。用户只描述业务交付,不提供 Agent ID、数量、并行、工具、repair 或 Runner 配方;`project-supervisor` 必须在单个 native planning 批次自主选择至少两个不同专业 Agent 并形成真实 Provider 重叠,语义判定不满足时在同一父 Session/run 发起唯一 repair,最终以单条 `[turn.report] game-creator-swarm-turn-report.v1` 和唯一 Supervisor assistant 收束。正式 `openai_chat / gpt-5.5` 最终诊断轮已完成 `51/51` Provider lifecycle、双专业并行、2+1 delivery、2 个 Observed claim、pidfd Runner 强杀/boot 恢复、唯一正式回复和零重复/残留/泄漏,当前门禁状态为 PASS;static delivery + isolated all-join 的真实组合和 Tauri/WebView 宿主级 E2E 仍是独立后续项。
|
||||
|
||||
2026-07-15 起,Runtime V1.1 文档的“V1.17 单 Agent 持久计划”作为后台工具规划进度的新事实源。`submit_agent_tool_plan` 新增 nullable `planUpdate={explanation,steps[{step,status}]}`;步骤只接受 `pending / in_progress / completed`,最多 8 步且至多一个 `in_progress`。结构化计划一旦建立,legacy `plan` 只作旧协议 fallback;终态步骤必须保留,`planRevision` 只在真实变化时单调递增,工具 action 下标不得自动完成结构化步骤,存在未完成步骤时不得写最终回复或 completed。
|
||||
|
||||
V1.17 计划快照随 `game-creator-runtime-context-bundle.v3` 持久化,v2 在通过原身份、revision 和 verification gate 校验后从当前 Runtime state 补齐计划字段继续恢复;计划元数据本身不推进项目 revision、不改变 verification gate,也不触发项目权限确认。开发 UI 和 CLI 有界展示 revision、说明与完整 8 步;正式用户的 Supervisor 只展示完成数、当前步骤、等待对象、下一步和协作数量的紧凑摘要。恢复、same-run steer 和真实 Provider 的完整验收矩阵以 Runtime V1.17 章节为准;2026-07-16 已在当前 v5 context 上完成正式 `openai_chat / gpt-5.5` 的同 run steer + Runner 强杀恢复专项,门禁状态为 PASS。
|
||||
@@ -586,4 +588,6 @@ game-project/
|
||||
- V1.28 对 Provider 瞬态失败采用 Runtime 显式重试:`agentLlm.<agent>.maxRetries / retryBackoffMs` 表示独立物理尝试及其有界指数退避,不得恢复为 `LlmClient` 在单 lifecycle 内隐式重放。每次尝试都重建禁用自动重试的 client,并写独立单次 lifecycle;首次 request slot 不变,第 `N` 次重试稳定使用 `-transient-N` 后缀。只有 `timeout / connectivity / transport` 可重试;工具协议无效仍进入独立 `repair-N` 格式修复,其他错误与重试耗尽按原失败路径收束。重试前必须重新检查 Goal、steer、cancel、task/run 与 orphan 门禁;控制请求可阻止下一次尝试,Runner 强杀后无可信终态的 `started` 仍进入 reconciliation,不能自动补发。重试发生在解析和副作用之前,不创建 action、pending、receipt、delivery、assistant 或 revision;既有 Runner、orphan、finalization 和隐私边界均不放宽,公共审计不得保存请求/响应正文、arguments、凭据或绝对路径。
|
||||
- V1.28 已于 2026-07-17 完成正式 `openai_chat / gpt-5.5` `supervisor-swarm` PASS:同一 native 批次双专业委派、真实 Provider 重叠、2 份初始 delivery、1 次 targeted contract read、1 份唯一 repair、pidfd Runner 强杀/boot 恢复、同一父 Session/run、唯一 Supervisor assistant 和 3 条内部专业 assistant 全部成立。报告包含 46/46 闭合且 completed 的 Provider lifecycle,成功计划 24/24、格式修复 20/20 全为原生工具协议;重复、残留 sidecar、Provider payload、私有正文、API Key、项目/正式配置绝对路径、报告、secret 与 lure 泄漏均为 0。正式 AppData 零 CLI 调用且源 Runner endpoint 未变化;规范复验命令为 `npm run ai-game-creator-shell:agent-runtime:real-e2e -- --config-dir <AppData> --suite supervisor-swarm`。
|
||||
- 2026-07-17 追加 `supervisor-swarm-transient-retry` 受控故障门禁:一次性本地回环代理只让 `design-director` 首个请求在正文转发前断线,并暂停后继请求,直到验收器确认唯一 failed lifecycle、唯一 retry audit、新 `-transient-1` identity,以及 action/receipt/子委派/claim/assistant/pending/revision/upstream forwarding 全为 0。E2E 启动 CLI/Runner 时会把 loopback 合并进 `NO_PROXY / no_proxy`,避免继承的系统 HTTP 代理接触故障门禁请求中的凭据和正文。最终加强版正式 `gpt-5.5` 报告为 46/46 lifecycle 闭合、45 completed/1 failed/1 retry;代理观察到的 10 个目标 Agent 请求与该 Agent lifecycle 数量一致,放行后完整双 Agent、唯一 repair、Runner 强杀恢复、唯一 Supervisor assistant、零重复/残留/泄漏继续 PASS。隔离 AppData 创建在正式目录同级,source-dir guard 与 `sourceAppDataDirectoryUntouched` 证明正式 AppData 未被写入,源配置与 endpoint 身份保持只读,失败 partial report 保留已取得的 retry checkpoint,代理与隔离现场全部清理。该 suite 只证明显式重试和既有协作链可组合,不把预置双 Agent fixture 扩大解释为自主编排;无 Agent ID/并行/repair 配方的自主 suite、真实 `agc:chat`、static+isolated 组合和 Tauri 宿主 E2E 仍待单独验收。
|
||||
- 2026-07-17 V1.30 `supervisor-swarm-autonomous-chat` 最终真实 PASS:唯一业务任务和仓库规则均不包含编排配方;Supervisor 在 1 个 native 批次自主选择两个不同专业 Agent,真实 Provider 重叠后形成 2 个初始 delivery,并基于 acceptance criteria 自主创建 1 个继承原合同的 repair。最终报告包含 110 条 task、197 条 event、330 条 Agent DB、9 条会话消息和 `51 started / 51 terminal / 51 completed / 0 failed` Provider lifecycle;28/28 成功计划与 19/19 格式修复均为原生工具协议,父计划 4/4 completed,Runner pidfd 强杀恢复后身份稳定,`turn.report` settled、正式 assistant 1、内部专业 assistant 3,重复、sidecar、正文、密钥、诱饵、项目/配置路径和报告泄漏均为 0。两次较早的独立尝试在 `maxRetries=0` 下各遇到 1 次外部 Provider 终态失败并中止,未与最终 PASS 拼接。
|
||||
- `agent.message` 使用来源 Agent/run、目标 Agent/Session 和清洗后正文 SHA-256 形成稳定语义身份。同一语义消息只允许写 1 条目标 tool conversation、1 条 `conversation.message` 和 1 条 `agent.runtime.agent.message`;后续 Runtime action 仍完整落账,但返回 `messageAppended=false` 且不算新的 loop 进展。专业 Agent 不得用重复消息替代最终回执;持续重复时最多经过当前 6 轮停滞窗口即以 `loop-budget-exhausted` 失败,保留 `in_progress` 计划且不写 completed。完整后台回归同时断言 6 个 actionId、同一 action fingerprint、6 组 action/observation/receipt、消息持久化唯一、receipt 零正文、第 7 次 Provider 请求为 0。
|
||||
- 开发模式可通过本地项目文件面板执行 `file.list/read/write/delete`,普通用户界面不暴露文件面板。
|
||||
|
||||
Reference in New Issue
Block a user