From 79de326ddca6d2ead3b4326823c1a2449e6ee4de Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E7=8E=8B=E5=BE=B7=E5=AE=87?= Date: Wed, 2 Sep 2026 18:08:39 +0800 Subject: [PATCH] =?UTF-8?q?=E9=99=90=E5=88=B6Direct=E5=8E=86=E5=8F=B2?= =?UTF-8?q?=E5=9B=9E=E6=94=BE=E5=88=B0=E6=96=B0=E7=BA=BF=E7=A8=8B\n?= =?UTF-8?q?=E4=BF=9D=E7=95=99=E8=BF=9B=E7=A8=8B=E5=86=85thread=E7=9A=84?= =?UTF-8?q?=E6=AD=A3=E5=B8=B8=E8=BF=9E=E7=BB=AD=E5=AF=B9=E8=AF=9D\n?= =?UTF-8?q?=E8=A1=A5=E5=85=85=E4=B8=AD=E6=96=AD=E6=81=A2=E5=A4=8D=E4=B8=8E?= =?UTF-8?q?=E7=BA=BF=E7=A8=8B=E9=87=8D=E5=BB=BA=E6=96=87=E6=A1=A3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../src-tauri/src/agent/codex_app_server.rs | 41 +++++++++++++------ .../src-tauri/src/agent/direct_runtime.rs | 16 ++------ .../src/agent/runtime_driver/entrypoints.rs | 4 ++ .../shared-memory/decision-log.md | 2 +- ...案】AI游戏创作智能体App实施计划-2026-06-24.md | 2 +- 5 files changed, 39 insertions(+), 26 deletions(-) diff --git a/apps/ai-game-creator-shell/src-tauri/src/agent/codex_app_server.rs b/apps/ai-game-creator-shell/src-tauri/src/agent/codex_app_server.rs index 1a45580fe..6e1e985b0 100644 --- a/apps/ai-game-creator-shell/src-tauri/src/agent/codex_app_server.rs +++ b/apps/ai-game-creator-shell/src-tauri/src/agent/codex_app_server.rs @@ -1417,6 +1417,11 @@ fn trust_isolated_game_creator_codex_workspace( } impl CodexAppServerConnection { + async fn has_thread_for(&self, snapshot: &AgentRuntimeProviderRequestSnapshot) -> bool { + let key = CodexNodeThreadKey::from(snapshot); + self.inner.threads.lock().await.contains_key(&key) + } + async fn acquire( snapshot: &AgentRuntimeProviderRequestSnapshot, llm: &GameCreatorLlmConfig, @@ -3020,6 +3025,7 @@ pub(crate) async fn direct_game_creator_codex_chat_at( user_prompt, None, None, + None, ) .await } @@ -3034,6 +3040,7 @@ pub(crate) async fn direct_game_creator_codex_chat_at_with_observer( root, system_prompt, user_prompt, + None, Some(observer), None, ) @@ -3090,6 +3097,7 @@ pub(crate) async fn direct_game_creator_codex_chat_at_with_optional_observer( root: &std::path::Path, system_prompt: String, user_prompt: String, + client_turn_id: Option<&str>, observer: Option<&mut (dyn FnMut(DirectCodexTurnObservation) + Send)>, audit: Option<&mut DirectCodexTurnAudit>, ) -> Result { @@ -3129,11 +3137,6 @@ pub(crate) async fn direct_game_creator_codex_chat_at_with_optional_observer( }; let api_kind = parse_game_creator_llm_api_kind(&config.llm.api_kind).map_err(|error| error.to_string())?; - let request = LlmRunRequest::single_turn(system_prompt, user_prompt) - .with_api_kind(api_kind) - .with_model(config.llm.model.clone()) - .with_request_timeout_ms(config.llm.request_timeout_ms) - .with_max_output_tokens(16_000); let connection = CodexAppServerConnection::acquire_at_workspace( &snapshot, &config.llm, @@ -3142,6 +3145,20 @@ pub(crate) async fn direct_game_creator_codex_chat_at_with_optional_observer( ) .await .map_err(|error| error.to_string())?; + let user_prompt = if connection.has_thread_for(&snapshot).await { + user_prompt + } else { + build_direct_codex_history_prompt( + &codex_root, + client_turn_id.unwrap_or("__none__"), + &user_prompt, + )? + }; + let request = LlmRunRequest::single_turn(system_prompt, user_prompt) + .with_api_kind(api_kind) + .with_model(config.llm.model.clone()) + .with_request_timeout_ms(config.llm.request_timeout_ms) + .with_max_output_tokens(16_000); connection .run_turn_with_direct_observer(&snapshot, &config.llm, request, None, observer, audit) .await @@ -3257,7 +3274,11 @@ mod tests { init_local_game_project_at(root.path(), "history-project", "history").expect("init"); for (role, content, message_id) in [ ("user", "hello", "direct-codex:old:user"), - ("assistant", "partial\nunexpected interrupt happened here", "partial-id"), + ( + "assistant", + "partial\nunexpected interrupt happened here", + "partial-id", + ), ("tool", "file-read result", "tool-id"), ("user", "stored raw request", "direct-codex:new-turn:user"), ] { @@ -3274,12 +3295,8 @@ mod tests { ) .expect("append history"); } - let prompt = build_direct_codex_history_prompt( - root.path(), - "new-turn", - "new request", - ) - .expect("build prompt"); + let prompt = build_direct_codex_history_prompt(root.path(), "new-turn", "new request") + .expect("build prompt"); assert_eq!( prompt, "user: hello\nassistant: partial\nunexpected interrupt happened here\ntool: file-read result\nuser: new request" diff --git a/apps/ai-game-creator-shell/src-tauri/src/agent/direct_runtime.rs b/apps/ai-game-creator-shell/src-tauri/src/agent/direct_runtime.rs index 69d959195..f27a8dce1 100644 --- a/apps/ai-game-creator-shell/src-tauri/src/agent/direct_runtime.rs +++ b/apps/ai-game-creator-shell/src-tauri/src/agent/direct_runtime.rs @@ -3835,6 +3835,7 @@ async fn run_direct_game_creator_turn_inner( DirectCodexTurnFailure::new(DirectCodexFailureStage::CodeGeneration, error) })?; let reply = if let Some(emitter) = turn_emitter { + let client_turn_id = emitter.turn_id().to_string(); let emitter = emitter.clone(); let mut has_streamed = false; let mut latest_accumulated_text = None; @@ -3856,6 +3857,7 @@ async fn run_direct_game_creator_turn_inner( root, system_prompt, prompt.to_string(), + Some(&client_turn_id), Some(&mut observer), audit, ) @@ -3866,6 +3868,7 @@ async fn run_direct_game_creator_turn_inner( system_prompt, prompt.to_string(), None, + None, audit, ) .await @@ -4220,20 +4223,9 @@ pub(crate) async fn chat_with_game_creator_direct_codex( return Err(error); } }; - let replay_prompt = match crate::agent::codex_app_server::build_direct_codex_history_prompt( - root, - &turn_id, - &user_prompt, - ) { - Ok(prompt) => prompt, - Err(error) => { - audit.finish(false); - return Err(error); - } - }; let reply = match run_direct_game_creator_turn_at_with_creation_type_and_emitter( root, - &replay_prompt, + &user_prompt, creation_type.as_deref(), Some(&turn_emitter), Some(&mut audit), diff --git a/apps/ai-game-creator-shell/src-tauri/src/agent/runtime_driver/entrypoints.rs b/apps/ai-game-creator-shell/src-tauri/src/agent/runtime_driver/entrypoints.rs index bae479e11..e8c207db6 100644 --- a/apps/ai-game-creator-shell/src-tauri/src/agent/runtime_driver/entrypoints.rs +++ b/apps/ai-game-creator-shell/src-tauri/src/agent/runtime_driver/entrypoints.rs @@ -93,6 +93,10 @@ impl DirectGameCreatorTurnUpdateEmitter { }, ); } + + pub(crate) fn turn_id(&self) -> &str { + &self.turn_id + } } pub(crate) fn start_game_creator_manifest_invalidation_event_sink( diff --git a/docs/project-memory/shared-memory/decision-log.md b/docs/project-memory/shared-memory/decision-log.md index 63f4a5cff..a1e20d8cd 100644 --- a/docs/project-memory/shared-memory/decision-log.md +++ b/docs/project-memory/shared-memory/decision-log.md @@ -7568,7 +7568,7 @@ CI 上 `background_agent_runtime_recovers_stale_running_before_pending_task` 在 - 配置:有无 AGC LLM Key 都只支持 `openai_responses`;非空 Key 映射后 base URL 与逐 Agent model/effort 生效,Key 只经专用环境变量;空 Key 只桥接用户 Codex `auth.json`,移除继承环境 Key。`stream=true` 复用现有 durable final-reply delta;`webSearchEnabled=true`、`openai_chat / anthropic` 必须显式切换 `provider`,不得静默忽略已有 LLM 配置。 - 安全与恢复:app-server 使用隔离临时 `CODEX_HOME` 与 OS HOME,只桥接认证,不加载用户 MCP/config/skills/hooks;启动前关闭 web/multi-agent/shell/browser/plugin/image 等原生能力,固定 read-only、network off、never approval,AGC 是唯一 ToolHost。取消覆盖 turn-start 回包前窗口并只发送单 turn interrupt;已开始 turn 的连接/终态未知直接标记 reconciliation,明确 failed/interrupted 不自动重试。模式与 durable 指纹取同一配置快照。 - 资源与退出:pool 按实际凭据快照/base URL/API kind/CLI 版本和节点 run 身份隔离;空 AppData Key 只允许桥接一次性读取的有界 `auth.json` 快照,并用同一字节快照生成池指纹,宿主 `CODEX_API_KEY` 对 app-server 与一次性 CLI 都必须移除。节点进程与 thread 均有上限并只淘汰 inactive LRU;stdout NDJSON 与 stderr 无换行记录均有硬上限,stderr 原文不得写入错误或日志,只记录固定分类、总字节数、SHA-256 与可取得的退出状态。Runner 正常、强制、watchdog 退出显式关池;Linux child 绑定 parent-death signal,避免 Runner 被强杀后遗留带凭据孤儿进程。 -- DirectProject 会话恢复补充:Codex thread 继续使用 `ephemeral=true`,不保存或恢复 Codex 原生 thread。项目对话 `.agent/conversations/project.jsonl` 是唯一聊天事实源;Direct 新消息由 AGC 读取全部 `user`、`assistant`、`tool` 行,按原顺序渲染为简单的 `user:` / `assistant:` / `tool:` 文本后,再追加本次新 user 请求发送给新 thread。app-server 意外中断时,已收到的 partial 文本作为普通 `assistant` 消息追加,并在末尾写入 `unexpected interrupt happened here`;断开处理与下一次发送均可重复追加,但使用普通消息 `messageId` 幂等。项目打开不再根据“只有 user 没有 assistant”自动重发旧请求;Direct 不新增 retry 入口。Runtime Agent 的恢复合同保持独立,不消费项目 Direct 对话历史。 +- DirectProject 会话恢复补充:Codex thread 继续使用 `ephemeral=true`,不保存或恢复 Codex 原生 thread。项目对话 `.agent/conversations/project.jsonl` 是唯一聊天事实源;仅当 app-server 连接没有可用的项目 thread(通常是进程重启或 thread 被淘汰)时,AGC 才读取全部 `user`、`assistant`、`tool` 行,按原顺序渲染为简单的 `user:` / `assistant:` / `tool:` 文本后,再追加本次新 user 请求发送给新 thread;已有 thread 的普通消息仍只发送新 user。app-server 意外中断时,已收到的 partial 文本作为普通 `assistant` 消息追加,并在末尾写入 `unexpected interrupt happened here`;断开处理与下一次发送均可重复追加,但使用普通消息 `messageId` 幂等。项目打开不再根据“只有 user 没有 assistant”自动重发旧请求;Direct 不新增 retry 入口。Runtime Agent 的恢复合同保持独立,不消费项目 Direct 对话历史。 - 兼容迁移:已有 AppData 未写 `agentMode` 时,仅当全局及逐 Agent 都是 `openai_responses` 才迁入 app-server;任何 `openai_chat / anthropic` 路由保持 `provider`,防止项目自动恢复先于用户改配置而批量失败。新安装仍默认 app-server;已确认兼容 Responses 的旧端点可由用户显式切换且继续使用原 model/base URL/API Key。 - 关联:`docs/technical/【技术方案】AI游戏创作Agent Runtime V1.1-2026-07-12.md` V1.52。 diff --git a/docs/technical/【技术方案】AI游戏创作智能体App实施计划-2026-06-24.md b/docs/technical/【技术方案】AI游戏创作智能体App实施计划-2026-06-24.md index fd5ff54f3..e19db9ecc 100644 --- a/docs/technical/【技术方案】AI游戏创作智能体App实施计划-2026-06-24.md +++ b/docs/technical/【技术方案】AI游戏创作智能体App实施计划-2026-06-24.md @@ -212,7 +212,7 @@ Supervisor 认领该回执后,由父 run 自己为每个原 delivery 逐一创 - 模式升级:`agentMode` 扩为 `codex_app_server / codex_cli / provider`,新默认为 `codex_app_server`;V1.51 的一次性 `codex exec` 保留为显式兼容模式,HTTP Provider 保留为非 Responses 配置及故障回退模式。 - 进程与节点:External Runner 按“有效 Agent LLM 凭据/Responses 路由 + `projectId/agentId/sessionId/runId`”隔离长期 `codex app-server --stdio`,即每个权威节点 run 直接持有自己的 Codex CLI 子进程与 ephemeral thread,每次完整权威请求映射 turn。同一节点 turn 串行,节点之间进程级隔离;单节点连接失败不得使其它节点同时失去终态。Codex thread 不写 durable recovery;节点完成、重启、retry、handoff 和 finalization 仍只认 AGC 账本。 -- DirectProject replay:Codex thread 仍保持 `ephemeral=true`。`.agent/conversations/project.jsonl` 是聊天唯一事实源;Direct 新消息由 AGC 读取全部 `user`、`assistant`、`tool` 记录,按原顺序渲染为简单 `user:` / `assistant:` / `tool:` 行,再追加本次新 user request,发送给新建 thread。app-server 意外中断时,已收到的 partial 文本按普通 `assistant` 消息追加,并在文本末尾追加 `unexpected interrupt happened here`;断开处理和下一次发送都可尝试写入,依赖普通 `messageId` 幂等。项目打开只读取历史,不因 user-only 记录自动重发;Direct 不提供 retry 入口。Runtime Agent 继续使用独立的 runtime/context 恢复链路,不读取 DirectProject 对话作为原生 thread history。 +- DirectProject replay:Codex thread 仍保持 `ephemeral=true`。`.agent/conversations/project.jsonl` 是聊天唯一事实源;仅当 app-server 连接没有可用的项目 thread(通常是进程重启或 thread 被淘汰)时,AGC 才读取全部 `user`、`assistant`、`tool` 记录,按原顺序渲染为简单 `user:` / `assistant:` / `tool:` 行,再追加本次新 user request,发送给新建 thread;已有 thread 的普通消息仍只发送新 user。app-server 意外中断时,已收到的 partial 文本按普通 `assistant` 消息追加,并在文本末尾追加 `unexpected interrupt happened here`;断开处理和下一次发送都可尝试写入,依赖普通 `messageId` 幂等。项目打开只读取历史,不因 user-only 记录自动重发;Direct 不提供 retry 入口。Runtime Agent 继续使用独立的 runtime/context 恢复链路,不读取 DirectProject 对话作为原生 thread history。 - LLM 配置:`apiKind` 始终只接受 `openai_responses`;非空 Key 转换为 app-server model provider,base URL 生效,Key 仅走专用环境变量;空 Key 只桥接用户 Codex `auth.json`,不继承环境 `CODEX_API_KEY`。设置面板在 app-server 模式继续显示并保存 model、effort、stream、全局/逐 Agent Key 与路由配置;`openai_chat / anthropic` 明确提示切 `provider`,不得悄悄忽略。`stream=true` 接入 app-server 文本 delta;`webSearchEnabled=true` 只允许 DirectProject 经客户端审核的 `agc_web_search` 使用,不得启用 Codex 原生 webSearch 或任意网络。 - 安全与取消:临时 cwd、隔离 `CODEX_HOME` 与 OS HOME、read-only、network off、never approval,并在启动前关闭 web/multi-agent/shell/browser/plugin/image 等原生能力;取消从 turn-start pending 阶段就跟踪且只 interrupt 当前 turn。已发送 turn 后连接断开或终态丢失进入 reconciliation,只关闭当前节点进程且不重放同一 request slot;明确 failed/interrupted 不按 transport 重试。 - remote-control 认证边界:没有 ChatGPT `auth.json` 的 API Key / provider-proxy app-server 在启动时设置 Codex 内部环境变量 `CODEX_INTERNAL_APP_SERVER_REMOTE_CONTROL_DISABLED=1`,让 remote-control 以 `desired_state=Disabled` 启动,避免上游进入 1Hz 认证重试;不再依赖需要 ChatGPT 登录态的 `remoteControl/disable` RPC。只有实际桥接 ChatGPT 登录态的 AuthBridge 保持 remote-control 可用。API Key 子进程同时使用 `RUST_LOG=warn` 收敛剩余预期噪音,不伪造 `auth.json` 或静默继续。