From bd85694ee9923580f8f4d4fef3e60ad2d616e068 Mon Sep 17 00:00:00 2001 From: kdletters Date: Thu, 10 Sep 2026 12:45:54 +0800 Subject: [PATCH] =?UTF-8?q?=E6=8E=A5=E5=85=A5=20AGC=20=E7=8B=AC=E7=AB=8B?= =?UTF-8?q?=20Agent=20=E8=AF=95=E9=AA=8C=E6=A8=A1=E5=BC=8F?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 新增 agent_runtime 配置并通过 stdio App Server 启动独立 Agent 接通后台、交互和 DirectProject 文本回合,保留旧 Codex 回退 对未接入的 function tool 明确失败,避免静默丢失工具调用 补充 AGC 配置回归与实施边界文档 --- .../src-tauri/src/agent.rs | 2 + .../src/agent/agent_runtime_client.rs | 241 ++++++++++++++++++ .../src-tauri/src/agent/codex_app_server.rs | 10 + .../src-tauri/src/agent/direct_runtime.rs | 20 +- .../src-tauri/src/agent/interaction.rs | 1 + .../runtime_actions/provider_final_reply.rs | 6 + .../agent/runtime_protocol/provider_retry.rs | 6 + .../src-tauri/src/config.rs | 16 +- .../src-tauri/src/main.rs | 1 + .../src-tauri/src/tests/configuration.rs | 5 + apps/ai-game-creator-shell/src/app/types.ts | 1 + .../runtime-config/RuntimeConfigDialog.tsx | 2 +- .../shared-memory/decision-log.md | 11 + ...施计划】AGC接入独立Agent内核-2026-09-09.md | 21 +- 14 files changed, 330 insertions(+), 13 deletions(-) create mode 100644 apps/ai-game-creator-shell/src-tauri/src/agent/agent_runtime_client.rs diff --git a/apps/ai-game-creator-shell/src-tauri/src/agent.rs b/apps/ai-game-creator-shell/src-tauri/src/agent.rs index bc2bedb87..da9a3c982 100644 --- a/apps/ai-game-creator-shell/src-tauri/src/agent.rs +++ b/apps/ai-game-creator-shell/src-tauri/src/agent.rs @@ -12,6 +12,7 @@ use std::sync::atomic::{AtomicBool, AtomicU64, Ordering}; mod codex_app_server; mod codex_cli; mod codex_provider_proxy; +mod agent_runtime_client; mod direct_codex_attachments; mod direct_codex_audit; mod direct_runtime; @@ -41,6 +42,7 @@ pub(crate) use direct_codex_audit::*; pub(crate) use direct_runtime::*; pub(crate) use direct_tool_bridge::*; pub(crate) use direct_tools_mcp::*; +pub(crate) use agent_runtime_client::request_game_creator_agent_runtime; pub(crate) use generation::*; pub(crate) use interaction::*; pub(crate) use prompt::*; diff --git a/apps/ai-game-creator-shell/src-tauri/src/agent/agent_runtime_client.rs b/apps/ai-game-creator-shell/src-tauri/src/agent/agent_runtime_client.rs new file mode 100644 index 000000000..2867619d3 --- /dev/null +++ b/apps/ai-game-creator-shell/src-tauri/src/agent/agent_runtime_client.rs @@ -0,0 +1,241 @@ +//! AGC 到独立 Agent Runtime 的最小外部客户端。 +//! +//! 该模块只替换“推理回合”的进程连接:项目工具、AGC Runtime 和完成门仍由 +//! AGC 掌控。协议是 agent 自有 JSON-RPC JSONL,不是 Codex App Server。 + +use super::*; +use serde_json::json; +use std::io::{BufRead, BufReader, Write}; +use std::process::{Command, Stdio}; +use std::time::Duration; + +const AGENT_RUNTIME_PROGRAM_ENV: &str = "GENARRATIVE_AGENT_RUNTIME_PROGRAM"; +const AGENT_RUNTIME_MAX_LINE_BYTES: usize = 4 * 1024 * 1024; +const AGENT_RUNTIME_TIMEOUT: Duration = Duration::from_secs(120); + +struct ChildGuard(Option); + +impl Drop for ChildGuard { + fn drop(&mut self) { + if let Some(mut child) = self.0.take() { + let _ = child.kill(); + let _ = child.wait(); + } + } +} + +pub(crate) async fn request_game_creator_agent_runtime( + llm: &GameCreatorLlmConfig, + request: platform_llm::LlmRunRequest, +) -> Result { + if !request.function_tools.is_empty() { + return Err(platform_llm::LlmError::InvalidRequest( + "agent_runtime 当前仅支持无工具文本回合;工具协议尚未接入".into(), + )); + } + let llm = llm.clone(); + tokio::task::spawn_blocking(move || request_game_creator_agent_runtime_blocking(&llm, request)) + .await + .map_err(|_| platform_llm::LlmError::Transport("独立 Agent worker 线程异常退出".into()))? +} + +fn request_game_creator_agent_runtime_blocking( + llm: &GameCreatorLlmConfig, + request: platform_llm::LlmRunRequest, +) -> Result { + let program = std::env::var_os(AGENT_RUNTIME_PROGRAM_ENV).unwrap_or_else(|| "agent".into()); + let temp = tempfile::Builder::new() + .prefix("genarrative-agent-runtime-") + .tempdir() + .map_err(|error| { + platform_llm::LlmError::Transport(format!("创建 Agent 临时目录失败:{error}")) + })?; + let db = temp.path().join("agent.db"); + let config = temp.path().join("agent.toml"); + std::fs::write(&config, "provider = \"openai\"\n").map_err(|error| { + platform_llm::LlmError::Transport(format!("创建 Agent 配置失败:{error}")) + })?; + let home = temp.path().join("home"); + std::fs::create_dir_all(&home).map_err(|error| { + platform_llm::LlmError::Transport(format!("创建 Agent HOME 失败:{error}")) + })?; + let mut child = ChildGuard(Some( + Command::new(program) + .args(["app-server", "--stdio"]) + .current_dir(temp.path()) + .env_clear() + .env("PATH", std::env::var_os("PATH").unwrap_or_default()) + .env("HOME", &home) + .env("TMPDIR", temp.path()) + .env("AGENT_CONFIG", &config) + .env("AGENT_DB", &db) + .env("AGENT_PROVIDER", "openai") + .env("AGENT_MODEL", &llm.model) + .env("OPENAI_BASE_URL", &llm.base_url) + .env("OPENAI_API_KEY", &llm.api_key) + .stdin(Stdio::piped()) + .stdout(Stdio::piped()) + .stderr(Stdio::null()) + .spawn() + .map_err(|error| { + platform_llm::LlmError::Transport(format!("启动独立 Agent 失败:{error}")) + })?, + )); + let mut stdin = child + .0 + .as_mut() + .and_then(|child| child.stdin.take()) + .ok_or_else(|| platform_llm::LlmError::Transport("独立 Agent stdin 不可用".into()))?; + let stdout = child + .0 + .as_mut() + .and_then(|child| child.stdout.take()) + .ok_or_else(|| platform_llm::LlmError::Transport("独立 Agent stdout 不可用".into()))?; + let mut reader = BufReader::new(stdout); + write_rpc( + &mut stdin, + json!({"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":1}}), + )?; + read_response(&mut reader, 1)?; + let task = request + .messages + .iter() + .map(|message| message.content.as_str()) + .collect::>() + .join("\n\n"); + if task.trim().is_empty() { + return Err(platform_llm::LlmError::InvalidRequest( + "独立 Agent 请求缺少文本消息".into(), + )); + } + write_rpc( + &mut stdin, + json!({"jsonrpc":"2.0","id":2,"method":"run/start","params":{"task":task,"stream":false}}), + )?; + let accepted = read_response(&mut reader, 2)?; + let run_id = accepted + .get("result") + .and_then(|value| value.get("runId")) + .and_then(serde_json::Value::as_str) + .ok_or_else(|| platform_llm::LlmError::Deserialize("独立 Agent 未返回 runId".into()))? + .to_owned(); + let (line_sender, line_receiver) = std::sync::mpsc::sync_channel(8); + std::thread::spawn(move || { + while let Ok(line) = read_line_bounded(&mut reader) { + if line_sender.send(line).is_err() { + break; + } + } + }); + let deadline = std::time::Instant::now() + AGENT_RUNTIME_TIMEOUT; + loop { + let remaining = deadline.saturating_duration_since(std::time::Instant::now()); + if remaining.is_zero() { + return Err(platform_llm::LlmError::Timeout { attempts: 1 }); + } + let line = line_receiver + .recv_timeout(remaining) + .map_err(|error| match error { + std::sync::mpsc::RecvTimeoutError::Timeout => { + platform_llm::LlmError::Timeout { attempts: 1 } + } + std::sync::mpsc::RecvTimeoutError::Disconnected => { + platform_llm::LlmError::Transport("独立 Agent 连接提前关闭".into()) + } + })?; + let value: serde_json::Value = serde_json::from_slice(&line) + .map_err(|_| platform_llm::LlmError::Deserialize("独立 Agent 返回无效 JSON".into()))?; + if value.get("method").and_then(serde_json::Value::as_str) == Some("run/completed") + && value + .get("params") + .and_then(|params| params.get("runId")) + .and_then(serde_json::Value::as_str) + == Some(run_id.as_str()) + { + let output = value + .get("params") + .and_then(|params| params.get("result")) + .and_then(|result| result.get("output")) + .ok_or_else(|| { + platform_llm::LlmError::Deserialize("独立 Agent 完成结果缺少 output".into()) + })?; + let text = output + .get("text") + .and_then(serde_json::Value::as_str) + .unwrap_or_default() + .to_owned(); + let _ = write_rpc( + &mut stdin, + json!({"jsonrpc":"2.0","id":3,"method":"shutdown","params":{}}), + ); + if let Some(mut child) = child.0.take() { + let _ = child.wait(); + } + return Ok(platform_llm::LlmRunResponse { + provider: platform_llm::LlmProvider::OpenAiCompatible, + model: llm.model.clone(), + text, + finish_reason: Some("completed".into()), + response_id: Some(run_id), + usage: None, + tool_calls: Vec::new(), + }); + } + if value.get("method").and_then(serde_json::Value::as_str) == Some("run/failed") { + return Err(platform_llm::LlmError::Transport( + "独立 Agent 执行失败".into(), + )); + } + } +} + +fn write_rpc( + stdin: &mut impl Write, + value: serde_json::Value, +) -> Result<(), platform_llm::LlmError> { + serde_json::to_writer(&mut *stdin, &value) + .map_err(|_| platform_llm::LlmError::Transport("写入独立 Agent 请求失败".into()))?; + stdin + .write_all(b"\n") + .and_then(|_| stdin.flush()) + .map_err(|_| platform_llm::LlmError::Transport("刷新独立 Agent 请求失败".into())) +} + +fn read_line_bounded(reader: &mut impl BufRead) -> Result, platform_llm::LlmError> { + let mut line = Vec::new(); + reader + .read_until(b'\n', &mut line) + .map_err(|_| platform_llm::LlmError::Transport("读取独立 Agent 响应失败".into()))?; + if line.is_empty() { + return Err(platform_llm::LlmError::Transport( + "独立 Agent 连接提前关闭".into(), + )); + } + if line.len() > AGENT_RUNTIME_MAX_LINE_BYTES { + return Err(platform_llm::LlmError::Deserialize( + "独立 Agent 响应超过大小限制".into(), + )); + } + Ok(line) +} + +fn read_response( + reader: &mut impl BufRead, + id: u64, +) -> Result { + let line = read_line_bounded(reader)?; + let value: serde_json::Value = serde_json::from_slice(&line) + .map_err(|_| platform_llm::LlmError::Deserialize("独立 Agent 返回无效 JSON".into()))?; + if value.get("id").and_then(serde_json::Value::as_u64) != Some(id) { + return Err(platform_llm::LlmError::Transport( + "独立 Agent 响应 ID 不匹配".into(), + )); + } + if value.get("error").is_some() { + return Err(platform_llm::LlmError::Upstream { + status_code: 500, + message: "独立 Agent 请求失败".into(), + }); + } + Ok(value) +} 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 6d3aa6237..f2f46dd6b 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 @@ -2953,6 +2953,16 @@ pub(crate) async fn direct_game_creator_home_codex_chat( user_prompt: String, ) -> Result { let config = load_game_creator_app_config()?; + if config.agent_mode == GAME_CREATOR_AGENT_MODE_AGENT_RUNTIME { + let request = platform_llm::LlmRunRequest::single_turn(system_prompt, user_prompt) + .with_api_kind(parse_game_creator_llm_api_kind(&config.llm.api_kind)?) + .with_model(config.llm.model.clone()) + .with_request_timeout_ms(config.llm.request_timeout_ms); + return request_game_creator_agent_runtime(&config.llm, request) + .await + .map(|response| response.text) + .map_err(|error| error.to_string()); + } game_creator_codex_app_server_validate_llm_config(&config.llm) .map_err(|error| error.to_string())?; let snapshot = AgentRuntimeProviderRequestSnapshot { 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 798bac52e..7be4f77f5 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 @@ -3832,7 +3832,25 @@ async fn run_direct_game_creator_turn_inner( .map_err(|error| { DirectCodexTurnFailure::new(DirectCodexFailureStage::CodeGeneration, error) })?; - let reply = if let Some(emitter) = turn_emitter { + let config = load_game_creator_app_config().map_err(|error| { + DirectCodexTurnFailure::new(DirectCodexFailureStage::CodeGeneration, error) + })?; + let reply = if config.agent_mode == GAME_CREATOR_AGENT_MODE_AGENT_RUNTIME { + let request = platform_llm::LlmRunRequest::single_turn(system_prompt.clone(), prompt.to_string()) + .with_api_kind(parse_game_creator_llm_api_kind(&config.llm.api_kind).map_err(|error| { + DirectCodexTurnFailure::new(DirectCodexFailureStage::CodeGeneration, error) + })?) + .with_model(config.llm.model.clone()) + .with_request_timeout_ms(config.llm.request_timeout_ms); + let reply = request_game_creator_agent_runtime(&config.llm, request) + .await + .map_err(|error| DirectCodexTurnFailure::new(DirectCodexFailureStage::CodeGeneration, error.to_string()))? + .text; + if let Some(emitter) = turn_emitter { + emitter.emit("finalizing", Some("response-finalization"), Some(reply.clone())); + } + Ok(reply) + } else if let Some(emitter) = turn_emitter { let emitter = emitter.clone(); let mut has_streamed = false; let mut latest_accumulated_text = None; diff --git a/apps/ai-game-creator-shell/src-tauri/src/agent/interaction.rs b/apps/ai-game-creator-shell/src-tauri/src/agent/interaction.rs index db6594659..e3f142a63 100644 --- a/apps/ai-game-creator-shell/src-tauri/src/agent/interaction.rs +++ b/apps/ai-game-creator-shell/src-tauri/src/agent/interaction.rs @@ -232,6 +232,7 @@ pub(crate) async fn decide_game_creator_agent_runtime_steer_at( request_game_creator_agent_codex_app_server(&decision_snapshot, &llm, request).await } GAME_CREATOR_AGENT_MODE_CODEX_CLI => request_game_creator_agent_codex_cli(request).await, + GAME_CREATOR_AGENT_MODE_AGENT_RUNTIME => request_game_creator_agent_runtime(&llm, request).await, GAME_CREATOR_AGENT_MODE_PROVIDER => { build_game_creator_agent_runtime_llm_client(&llm, &config_path)? .run(request) diff --git a/apps/ai-game-creator-shell/src-tauri/src/agent/runtime_actions/provider_final_reply.rs b/apps/ai-game-creator-shell/src-tauri/src/agent/runtime_actions/provider_final_reply.rs index b61b5c807..aef57ae9e 100644 --- a/apps/ai-game-creator-shell/src-tauri/src/agent/runtime_actions/provider_final_reply.rs +++ b/apps/ai-game-creator-shell/src-tauri/src/agent/runtime_actions/provider_final_reply.rs @@ -259,6 +259,12 @@ pub(in crate::agent) async fn request_game_creator_agent_background_final_reply_ response_observations, ); } + if agent_mode == GAME_CREATOR_AGENT_MODE_AGENT_RUNTIME { + return normalize_game_creator_agent_background_final_reply_response( + request_game_creator_agent_runtime(&llm_for_request, attempt_request).await?, + response_observations, + ); + } let client = build_game_creator_agent_runtime_llm_client( &llm_for_request, &config_path_for_request, diff --git a/apps/ai-game-creator-shell/src-tauri/src/agent/runtime_protocol/provider_retry.rs b/apps/ai-game-creator-shell/src-tauri/src/agent/runtime_protocol/provider_retry.rs index 27b7a3164..c918714e3 100644 --- a/apps/ai-game-creator-shell/src-tauri/src/agent/runtime_protocol/provider_retry.rs +++ b/apps/ai-game-creator-shell/src-tauri/src/agent/runtime_protocol/provider_retry.rs @@ -1706,6 +1706,9 @@ pub(in crate::agent) async fn request_game_creator_agent_runtime_llm_with_persis GAME_CREATOR_AGENT_MODE_CODEX_CLI => { request_game_creator_agent_codex_cli(request).await } + GAME_CREATOR_AGENT_MODE_AGENT_RUNTIME => { + request_game_creator_agent_runtime(&llm_for_request, request).await + } GAME_CREATOR_AGENT_MODE_PROVIDER => { let client = build_game_creator_agent_runtime_llm_client( &llm_for_request, @@ -1782,6 +1785,9 @@ pub(in crate::agent) async fn request_game_creator_agent_runtime_llm_with_transi GAME_CREATOR_AGENT_MODE_CODEX_CLI => { request_game_creator_agent_codex_cli(request.clone()).await } + GAME_CREATOR_AGENT_MODE_AGENT_RUNTIME => { + request_game_creator_agent_runtime(llm, request.clone()).await + } GAME_CREATOR_AGENT_MODE_PROVIDER => { let client = client.expect("provider mode constructs an HTTP client"); request_game_creator_agent_runtime_provider_llm(&client, llm, request.clone()) diff --git a/apps/ai-game-creator-shell/src-tauri/src/config.rs b/apps/ai-game-creator-shell/src-tauri/src/config.rs index 514345097..1eb8f867d 100644 --- a/apps/ai-game-creator-shell/src-tauri/src/config.rs +++ b/apps/ai-game-creator-shell/src-tauri/src/config.rs @@ -317,6 +317,9 @@ pub(crate) fn check_game_creator_llm_config_from_config() -> GameCreatorLlmConfi }; } }; + if app_config.agent_mode == GAME_CREATOR_AGENT_MODE_AGENT_RUNTIME { + return check_game_creator_agent_runtime_config(&app_config); + } if app_config.agent_mode != GAME_CREATOR_AGENT_MODE_PROVIDER { return check_game_creator_codex_config(&app_config); } @@ -376,6 +379,16 @@ pub(crate) fn check_game_creator_llm_config_from_config() -> GameCreatorLlmConfi status } +fn check_game_creator_agent_runtime_config( + app_config: &GameCreatorAppConfig, +) -> GameCreatorLlmConfigStatus { + let mut status = check_game_creator_llm_config_values(&app_config.llm, "llm"); + status.agent_mode = GAME_CREATOR_AGENT_MODE_AGENT_RUNTIME.to_string(); + // The standalone Agent process receives the already configured endpoint/key + // through its private child environment; no Codex binary is required. + status +} + fn check_game_creator_codex_config( app_config: &GameCreatorAppConfig, ) -> GameCreatorLlmConfigStatus { @@ -3750,9 +3763,10 @@ pub(crate) fn normalize_game_creator_agent_mode(value: &str) -> Result Ok(GAME_CREATOR_AGENT_MODE_CODEX_CLI.to_string()), + GAME_CREATOR_AGENT_MODE_AGENT_RUNTIME => Ok(GAME_CREATOR_AGENT_MODE_AGENT_RUNTIME.to_string()), GAME_CREATOR_AGENT_MODE_PROVIDER => Ok(GAME_CREATOR_AGENT_MODE_PROVIDER.to_string()), value => Err(format!( - "配置项 agentMode 无效:{value},请使用 codex_app_server、codex_cli 或 provider" + "配置项 agentMode 无效:{value},请使用 agent_runtime、codex_app_server、codex_cli 或 provider" )), } } diff --git a/apps/ai-game-creator-shell/src-tauri/src/main.rs b/apps/ai-game-creator-shell/src-tauri/src/main.rs index d7657c796..2be327079 100644 --- a/apps/ai-game-creator-shell/src-tauri/src/main.rs +++ b/apps/ai-game-creator-shell/src-tauri/src/main.rs @@ -1290,6 +1290,7 @@ const GAME_CREATOR_LOCAL_CONFIG_FILE_NAME: &str = "game-creator.config.local.jso const GAME_CREATOR_AGENT_MODE_CODEX_APP_SERVER: &str = "codex_app_server"; const GAME_CREATOR_AGENT_MODE_CODEX_CLI: &str = "codex_cli"; const GAME_CREATOR_AGENT_MODE_PROVIDER: &str = "provider"; +const GAME_CREATOR_AGENT_MODE_AGENT_RUNTIME: &str = "agent_runtime"; const DEFAULT_GAME_CREATOR_LLM_BASE_URL: &str = "https://dev.genarrative.world/gpt/v1"; const DEFAULT_GAME_CREATOR_LLM_MODEL: &str = "gpt-5.6-sol"; const DEFAULT_GAME_CREATOR_LLM_API_KIND: &str = "openai_responses"; diff --git a/apps/ai-game-creator-shell/src-tauri/src/tests/configuration.rs b/apps/ai-game-creator-shell/src-tauri/src/tests/configuration.rs index 9d5b849e6..ec2028397 100644 --- a/apps/ai-game-creator-shell/src-tauri/src/tests/configuration.rs +++ b/apps/ai-game-creator-shell/src-tauri/src/tests/configuration.rs @@ -160,6 +160,11 @@ fn agent_mode_defaults_to_codex_app_server_and_preserves_explicit_modes() { normalize_game_creator_agent_mode(GAME_CREATOR_AGENT_MODE_CODEX_CLI).expect("cli mode"), GAME_CREATOR_AGENT_MODE_CODEX_CLI ); + assert_eq!( + normalize_game_creator_agent_mode(GAME_CREATOR_AGENT_MODE_AGENT_RUNTIME) + .expect("standalone runtime mode"), + GAME_CREATOR_AGENT_MODE_AGENT_RUNTIME + ); assert!(normalize_game_creator_agent_mode("unknown") .expect_err("unknown mode") .contains("agentMode")); diff --git a/apps/ai-game-creator-shell/src/app/types.ts b/apps/ai-game-creator-shell/src/app/types.ts index aaf922b5d..561ce973b 100644 --- a/apps/ai-game-creator-shell/src/app/types.ts +++ b/apps/ai-game-creator-shell/src/app/types.ts @@ -656,6 +656,7 @@ export type GameCreatorLlmApiKind = | 'openai_chat' | 'anthropic'; export type GameCreatorAgentMode = + | 'agent_runtime' | 'codex_app_server' | 'codex_cli' | 'provider'; diff --git a/apps/ai-game-creator-shell/src/features/runtime-config/RuntimeConfigDialog.tsx b/apps/ai-game-creator-shell/src/features/runtime-config/RuntimeConfigDialog.tsx index 386c6d2cf..0404ac9c1 100644 --- a/apps/ai-game-creator-shell/src/features/runtime-config/RuntimeConfigDialog.tsx +++ b/apps/ai-game-creator-shell/src/features/runtime-config/RuntimeConfigDialog.tsx @@ -547,7 +547,7 @@ export function RuntimeConfigDialog({ const config = normalizeRuntimeConfigDraft( { ...runtimeConfigDraft, - agentMode: 'codex_app_server', + agentMode: runtimeConfigDraft.agentMode, editorApi: allowAdvancedExternalEditorConfig ? runtimeConfigDraft.editorApi : defaultRuntimeConfigDraft.editorApi, diff --git a/docs/project-memory/shared-memory/decision-log.md b/docs/project-memory/shared-memory/decision-log.md index 4a5a822b8..036753d66 100644 --- a/docs/project-memory/shared-memory/decision-log.md +++ b/docs/project-memory/shared-memory/decision-log.md @@ -9018,6 +9018,17 @@ CI 上 `background_agent_runtime_recovers_stale_running_before_pending_task` 在 - 验证:CLI App Server 黑盒子进程测试 4/4、Host 80/80 与实时流 2/2 通过;workspace lock/check、Clippy、fmt、编码和 diff 门禁通过。 +## 2026-09-10 AGC 接入独立 Agent 试验模式 + +- 决策:AGC 新增可选 `agent_runtime` 模式,通过 `GENARRATIVE_AGENT_RUNTIME_PROGRAM` + 启动独立 `agent app-server --stdio`;不把独立 workspace 作为 AGC Rust 依赖,也不把 + Codex App Server 作为中间协议。旧 `codex_app_server`/`codex_cli` 模式保留为回退路径。 +- 当前范围:已接通后台 Provider、交互回合和 DirectProject/首页文本回合;消息与模型配置 + 经 JSONL 传给独立 Agent,完成结果映射回现有 LLM 响应。独立 Agent 自己拥有 Kernel、 + Engine、Provider 和 Runtime,AGC 暂不接管其工具/审批/持久化。 +- 未完成:AGC 工具动作的跨进程协议、流式事件回传、取消/审批映射和默认模式切换;本阶段 + 不修改 Codex 版本或删除现有接线。 + ## 2026-09-09 AGC 改用独立 Agent 内核的替换边界 - 决策:后续目标是用独立 `rust/` workspace 的 Kernel/Engine 替换 AGC 的 Codex 执行循环, diff --git a/docs/technical/【实施计划】AGC接入独立Agent内核-2026-09-09.md b/docs/technical/【实施计划】AGC接入独立Agent内核-2026-09-09.md index 37e8e01de..6ddf39d0c 100644 --- a/docs/technical/【实施计划】AGC接入独立Agent内核-2026-09-09.md +++ b/docs/technical/【实施计划】AGC接入独立Agent内核-2026-09-09.md @@ -5,19 +5,20 @@ 让 AGC 的 Codex 模式改由独立 `rust/` workspace 的 Agent Kernel/Engine 执行, 同时保留 AGC 宿主对项目、工具权限、审批、Runtime 持久化、预览和完成门的控制。 -## 第一阶段:兼容桥(当前) +## 第一阶段:独立进程试验(已完成) -- 在 AGC 与独立 workspace 之间建立明确的 `AgentTurnAdapter` 边界,不让两套 - `agent-runtime-core` 类型直接互相泄漏。 -- 输入:AGC 的消息历史、模型配置、工具定义、当前 Agent 身份和取消信号。 -- 输出:文本增量、结构化 tool call、工具结果、终态错误和可审计事件。 -- AGC 继续执行工具动作并写入自己的 Runtime;新 Engine 不创建第二套项目状态机。 -- 同步 Engine 与 AGC Tokio 的连接通过独立受控线程/有界 channel,禁止在同一 Tokio - worker 内直接 `block_on`。 +- `agent-cli app-server --stdio` 已提供独立 JSON-RPC JSONL 服务;AGC 通过 + `agent_runtime_client.rs` 以外部子进程连接,不共享 Rust 类型、SQLite 或 Codex。 +- `agent_runtime` 模式已接入后台 Provider 回合、交互回合和 DirectProject/首页直聊入口; + 文本消息会转换为新 Agent 的一次 `run/start`,结果再映射回现有 `LlmRunResponse`。 +- `GENARRATIVE_AGENT_RUNTIME_PROGRAM` 可指定 Agent 可执行文件,默认从 PATH 查找 `agent`; + 子进程使用独立临时目录、HOME、DB 和环境,旧 Codex 模式仍可回退。 +- 当前只替换无 function tool 的文本推理;带工具的 AGC 回合会明确失败,不伪装迁移 + AGC 工具调用、审批、项目 Runtime 或预览完成门。这些仍留在 AGC,后续按协议逐项接入。 -## 第二阶段:实验模式 +## 第二阶段:工具协议实验(下一步) -- 新增 `agent_runtime` 模式,仅作为可回滚实验开关,默认仍使用现有 Codex 模式。 +- 保持 `agent_runtime` 为可回滚实验开关,默认仍使用现有 Codex 模式。 - 先覆盖文本回复、单工具调用、工具结果回传、拒绝/取消/超时和事件顺序。 - 使用现有 AGC fixture 对比旧 Codex 模式和新 Engine:消息、工具调用参数、Runtime 事件、错误分类和终态必须一致。