diff --git a/.gitignore b/.gitignore
index 5529773cd..88a842855 100644
--- a/.gitignore
+++ b/.gitignore
@@ -56,6 +56,7 @@ temp*build*/
/plugins/agc-unity-editor/dotnet/native-build/
/apps/ai-game-creator-shell/logs/
/apps/ai-game-creator-shell/src-tauri/resources/node-runtime/
+/apps/ai-game-creator-shell/src-tauri/resources/claude-agent/
/apps/ai-game-creator-shell/src-tauri/resources/node-runtime-staging-*/
/apps/ai-game-creator-shell/.llm-drafts/
/apps/ai-game-creator-shell/game-creator.config.local.json
diff --git a/apps/admin-web/src/api/adminApiTypes.ts b/apps/admin-web/src/api/adminApiTypes.ts
index 01e99ed10..429ce7c5a 100644
--- a/apps/admin-web/src/api/adminApiTypes.ts
+++ b/apps/admin-web/src/api/adminApiTypes.ts
@@ -1075,11 +1075,14 @@ export interface AdminRechargeRefundActionResponse {
export interface AdminWalletRestrictionResponse {
wallet: AdminProfileWalletPayload;
}
+export type AdminAgcAgentMode = 'codex' | 'cc';
+
export interface AdminAgcModel {
id: string;
alias: string;
modelId: string;
enabled: boolean;
+ agentMode: AdminAgcAgentMode;
}
export interface AdminAgcModelCatalog {
diff --git a/apps/admin-web/src/pages/AdminAgcModelsPage.test.tsx b/apps/admin-web/src/pages/AdminAgcModelsPage.test.tsx
index bb88b9de6..77b9262bb 100644
--- a/apps/admin-web/src/pages/AdminAgcModelsPage.test.tsx
+++ b/apps/admin-web/src/pages/AdminAgcModelsPage.test.tsx
@@ -23,14 +23,23 @@ vi.mock('../components/useAdminWriteConfirm', () => ({
confirmDialog: null,
}),
}));
-afterEach(cleanup);
+afterEach(() => {
+ cleanup();
+ vi.clearAllMocks();
+});
test('edits alias and upstream model without changing the stable identifier or revision', async () => {
const catalog = {
revision: 3,
defaultModelId: 'quality',
models: [
- { id: 'quality', alias: '高质量', modelId: 'gpt-6-astra', enabled: true },
+ {
+ id: 'quality',
+ alias: '高质量',
+ modelId: 'gpt-6-astra',
+ enabled: true,
+ agentMode: 'codex' as const,
+ },
],
};
vi.mocked(getAgcModelCatalog).mockResolvedValue(catalog);
@@ -54,3 +63,44 @@ test('edits alias and upstream model without changing the stable identifier or r
expect(screen.getAllByText('已保存').length).toBeGreaterThanOrEqual(2);
});
});
+
+test('new models default to Codex and save the selected Agent mode', async () => {
+ const catalog = {
+ revision: 3,
+ defaultModelId: 'quality',
+ models: [
+ {
+ id: 'quality',
+ alias: '高质量',
+ modelId: 'gpt-6-astra',
+ enabled: true,
+ agentMode: 'codex' as const,
+ },
+ ],
+ };
+ vi.mocked(getAgcModelCatalog).mockResolvedValue(catalog);
+ vi.mocked(saveAgcModelCatalog).mockImplementation(async (_, input) => ({
+ ...input,
+ revision: 4,
+ }));
+ render();
+ await screen.findByDisplayValue('gpt-6-astra');
+
+ fireEvent.click(screen.getByRole('button', { name: '添加模型' }));
+ const agentMode = screen.getByLabelText('模型 2 Agent 模式');
+ expect((agentMode as HTMLSelectElement).value).toBe('codex');
+ fireEvent.change(agentMode, { target: { value: 'cc' } });
+ fireEvent.click(screen.getByRole('button', { name: '保存' }));
+
+ await waitFor(() => {
+ const calls = vi.mocked(saveAgcModelCatalog).mock.calls;
+ const payload = calls[calls.length - 1]?.[1];
+ expect(payload?.models).toHaveLength(2);
+ expect(payload?.models[1]).toMatchObject({
+ alias: '',
+ modelId: '',
+ enabled: true,
+ agentMode: 'cc',
+ });
+ });
+});
diff --git a/apps/admin-web/src/pages/AdminAgcModelsPage.tsx b/apps/admin-web/src/pages/AdminAgcModelsPage.tsx
index 0697eec0a..c64efc67d 100644
--- a/apps/admin-web/src/pages/AdminAgcModelsPage.tsx
+++ b/apps/admin-web/src/pages/AdminAgcModelsPage.tsx
@@ -116,6 +116,7 @@ export function AdminAgcModelsPage({
columns={[
{ key: 'alias', label: '别名' },
{ key: 'modelId', label: '实际模型名' },
+ { key: 'agentMode', label: 'Agent 模式' },
{ key: 'enabled', label: '启用' },
{ key: 'default', label: '默认' },
{ key: 'actions', label: '操作' },
@@ -155,6 +156,7 @@ export function AdminAgcModelsPage({
alias: '',
modelId: '',
enabled: true,
+ agentMode: 'codex',
},
],
},
@@ -211,6 +213,21 @@ export function AdminAgcModelsPage({
}
/>
+
+
+ |
+ new Promise((resolve, reject) => {
+ process.stdout.write(`${JSON.stringify(value)}\n`, (error) =>
+ error ? reject(error) : resolve(),
+ );
+ }),
+ )
+ .catch(() => undefined);
+ return outputChain;
+}
+
+function safeEnv(input) {
+ const env = { ...process.env };
+ if (input && typeof input === 'object') {
+ for (const [key, value] of Object.entries(input)) {
+ if (typeof value === 'string') env[key] = value;
+ }
+ }
+ env.CLAUDE_AGENT_SDK_CLIENT_APP ??= 'genarrative-agc-agent-sidecar/0.1';
+ return env;
+}
+
+function buildOptions(request, controller) {
+ const options = {
+ abortController: controller,
+ cwd: request.cwd,
+ model: request.model,
+ permissionMode: 'dontAsk',
+ settingSources: [],
+ tools: [],
+ disallowedTools: ['Bash', 'Edit', 'Write', 'NotebookEdit'],
+ verbatimPrompts: true,
+ persistSession: true,
+ includePartialMessages: true,
+ env: safeEnv(request.env),
+ };
+ if (request.systemPrompt) {
+ options.systemPrompt = {
+ type: 'custom',
+ prompt: request.systemPrompt,
+ snapshot: true,
+ };
+ }
+ if (request.resume) options.resume = request.resume;
+ if (request.sessionId) options.sessionId = request.sessionId;
+ if (request.mcpServers && typeof request.mcpServers === 'object') {
+ options.mcpServers = request.mcpServers;
+ }
+ if (request.outputSchema) {
+ options.outputFormat = {
+ type: 'json_schema',
+ schema: request.outputSchema,
+ };
+ }
+ if (Number.isInteger(request.maxTurns) && request.maxTurns > 0) {
+ options.maxTurns = request.maxTurns;
+ }
+ return options;
+}
+
+async function runTurn(request) {
+ const id = String(request.id ?? '');
+ if (!id) return write({ type: 'error', message: 'sidecar turn 缺少 id' });
+ if (active.has(id))
+ return write({ id, type: 'error', message: 'sidecar turn 已存在' });
+ const controller = new AbortController();
+ active.set(id, { controller });
+ try {
+ const sessionKey =
+ typeof request.sessionKey === 'string' ? request.sessionKey : '';
+ const resume =
+ request.resume || (sessionKey ? sessions.get(sessionKey) : undefined);
+ const queryRequest = { ...request, resume };
+ const stream = query({
+ prompt: String(request.prompt ?? ''),
+ options: buildOptions(queryRequest, controller),
+ });
+ for await (const message of stream) {
+ if (
+ message?.type === 'system' &&
+ message?.subtype === 'init' &&
+ message.session_id
+ ) {
+ if (sessionKey) sessions.set(sessionKey, message.session_id);
+ }
+ if (message?.session_id && sessionKey)
+ sessions.set(sessionKey, message.session_id);
+ await write({ id, type: 'event', event: message });
+ if (message?.type === 'result') {
+ await write({
+ id,
+ type: 'done',
+ sessionId:
+ message.session_id ??
+ (sessionKey ? sessions.get(sessionKey) : undefined),
+ result: message,
+ });
+ }
+ }
+ } catch (error) {
+ await write({
+ id,
+ type: 'error',
+ message: error instanceof Error ? error.message : String(error),
+ });
+ } finally {
+ active.delete(id);
+ }
+}
+
+async function cancelTurn(request) {
+ const id = String(request.id ?? '');
+ const task = active.get(id);
+ if (!task) return write({ id, type: 'cancelled', cancelled: false });
+ task.controller.abort(new Error('AGC cancelled Claude Agent SDK query'));
+ return write({ id, type: 'cancelled', cancelled: true });
+}
+
+const input = readline.createInterface({
+ input: process.stdin,
+ crlfDelay: Infinity,
+});
+for await (const line of input) {
+ if (!line.trim()) continue;
+ let request;
+ try {
+ request = JSON.parse(line);
+ } catch {
+ await write({ type: 'error', message: 'sidecar 输入不是有效 JSON' });
+ continue;
+ }
+ if (request.type === 'cancel') {
+ await cancelTurn(request);
+ } else if (request.type === 'turn') {
+ void runTurn(request);
+ } else if (request.type === 'shutdown') {
+ for (const task of active.values()) task.controller.abort();
+ break;
+ } else {
+ await write({
+ id: request.id,
+ type: 'error',
+ message: 'sidecar 不支持该请求类型',
+ });
+ }
+}
diff --git a/apps/ai-game-creator-shell/package.json b/apps/ai-game-creator-shell/package.json
index ce79c75a1..83dbe4b56 100644
--- a/apps/ai-game-creator-shell/package.json
+++ b/apps/ai-game-creator-shell/package.json
@@ -27,6 +27,7 @@
"typecheck": "tsc -p tsconfig.json --noEmit && npm run skill-pack:check && node scripts/check-config.mjs"
},
"dependencies": {
+ "@anthropic-ai/claude-agent-sdk": "0.3.285",
"@cubone/react-file-manager": "^1.35.0",
"@genarrative/image-canvas-core": "0.1.0",
"@genarrative/image-canvas-react": "0.1.0",
diff --git a/apps/ai-game-creator-shell/scripts/check-config.mjs b/apps/ai-game-creator-shell/scripts/check-config.mjs
index 680e864f2..a2c349e77 100644
--- a/apps/ai-game-creator-shell/scripts/check-config.mjs
+++ b/apps/ai-game-creator-shell/scripts/check-config.mjs
@@ -1358,6 +1358,7 @@ for (const channel of ['release', 'beta-2']) {
const expectedBundledDesignAgentResources = {
'design-agent': 'design-agent',
+ 'resources/claude-agent': 'claude-agent',
...Object.fromEntries(
[
'codex-patch-parser',
diff --git a/apps/ai-game-creator-shell/src-tauri/build.rs b/apps/ai-game-creator-shell/src-tauri/build.rs
index 892ec4600..9238cb34c 100644
--- a/apps/ai-game-creator-shell/src-tauri/build.rs
+++ b/apps/ai-game-creator-shell/src-tauri/build.rs
@@ -32,6 +32,83 @@ fn sha256_file(path: &std::path::Path) -> Result {
Ok(format!("{:x}", hasher.finalize()))
}
+fn claude_agent_platform(target: &str) -> Option<&'static str> {
+ match target {
+ "x86_64-pc-windows-msvc" => Some("win32-x64"),
+ "aarch64-pc-windows-msvc" => Some("win32-arm64"),
+ "x86_64-apple-darwin" => Some("darwin-x64"),
+ "aarch64-apple-darwin" => Some("darwin-arm64"),
+ "x86_64-unknown-linux-gnu" => Some("linux-x64"),
+ "aarch64-unknown-linux-gnu" => Some("linux-arm64"),
+ "x86_64-unknown-linux-musl" => Some("linux-x64-musl"),
+ "aarch64-unknown-linux-musl" => Some("linux-arm64-musl"),
+ _ => None,
+ }
+}
+
+fn stage_bundled_claude_agent(manifest_dir: &std::path::Path) {
+ let target = env::var("TARGET").expect("Cargo TARGET");
+ let Some(platform) = claude_agent_platform(&target) else {
+ println!("cargo:warning=Claude Agent SDK sidecar 暂不支持目标:{target}");
+ return;
+ };
+ let app_root = manifest_dir.parent().expect("AGC Tauri manifest parent");
+ let repo_root = app_root
+ .parent()
+ .and_then(|apps_dir| apps_dir.parent())
+ .expect("AGC 应用必须位于仓库 apps 目录下");
+ let roots = [app_root, repo_root];
+ let sdk_source = roots
+ .iter()
+ .map(|root| root.join("node_modules/@anthropic-ai/claude-agent-sdk"))
+ .find(|path| path.join("sdk.mjs").is_file())
+ .unwrap_or_else(|| panic!("Claude Agent SDK 缺失;请先执行 npm ci"));
+ let binary_package = format!("claude-agent-sdk-{platform}");
+ let binary_source = roots
+ .iter()
+ .map(|root| {
+ root.join("node_modules/@anthropic-ai")
+ .join(&binary_package)
+ })
+ .find(|path| path.is_dir())
+ .unwrap_or_else(|| panic!("Claude Agent SDK 原生运行时缺失:{binary_package}"));
+ let entry_source = app_root.join("agent-sidecar/src/index.mjs");
+ assert!(entry_source.is_file(), "Claude Agent SDK sidecar 入口缺失");
+ let target_root = manifest_dir.join("resources/claude-agent");
+ if target_root.exists() {
+ fs::remove_dir_all(&target_root).expect("清理 Claude Agent SDK staging 失败");
+ }
+ fs::create_dir_all(target_root.join("node_modules/@anthropic-ai"))
+ .expect("创建 Claude Agent SDK staging 目录失败");
+ stage_plugin_file(&entry_source, &target_root.join("index.mjs"));
+ copy_plugin_tree(
+ &sdk_source,
+ &target_root.join("node_modules/@anthropic-ai/claude-agent-sdk"),
+ );
+ copy_plugin_tree(
+ &binary_source,
+ &target_root
+ .join("node_modules/@anthropic-ai")
+ .join(&binary_package),
+ );
+ let binary_name = if target.contains("windows") {
+ "claude.exe"
+ } else {
+ "claude"
+ };
+ let staged_binary = target_root
+ .join("node_modules/@anthropic-ai")
+ .join(&binary_package)
+ .join(binary_name);
+ if let Ok(metadata) = fs::metadata(binary_source.join(binary_name)) {
+ fs::set_permissions(&staged_binary, metadata.permissions())
+ .expect("保留 Claude Agent SDK 原生运行时权限失败");
+ }
+ println!("cargo:rerun-if-changed={}", entry_source.display());
+ println!("cargo:rerun-if-changed={}", sdk_source.display());
+ println!("cargo:rerun-if-changed={}", binary_source.display());
+}
+
fn stage_bundled_codex_cli(manifest_dir: &std::path::Path) {
let target = env::var("TARGET").expect("Cargo TARGET");
println!("cargo:rustc-env=AGC_BUILD_TARGET={target}");
@@ -262,6 +339,7 @@ fn main() {
env::var_os("CARGO_MANIFEST_DIR").expect("CARGO_MANIFEST_DIR must be available"),
);
let manifest_path = manifest_dir.join("prompts/runtime/manifest.json");
+ stage_bundled_claude_agent(&manifest_dir);
stage_bundled_codex_cli(&manifest_dir);
prepare_unity_editor_helper(&manifest_dir);
prepare_godot_editor_extension(&manifest_dir);
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 347ec3139..6940945d5 100644
--- a/apps/ai-game-creator-shell/src-tauri/src/agent.rs
+++ b/apps/ai-game-creator-shell/src-tauri/src/agent.rs
@@ -9,6 +9,7 @@ use std::collections::BTreeSet;
use std::io::{Seek, SeekFrom};
use std::sync::atomic::{AtomicBool, AtomicU64, Ordering};
+mod claude_code_cli;
mod codex_app_server;
mod codex_cli;
mod codex_provider_proxy;
@@ -48,6 +49,12 @@ mod runtime_protocol;
mod runtime_state;
mod runtime_tools;
mod skill_pack;
+use claude_code_cli::*;
+pub(crate) use claude_code_cli::{
+ cancel_direct_claude_code_turn_at, direct_game_creator_claude_code_chat_at,
+ direct_game_creator_claude_code_home_chat, game_creator_claude_code_cli_route_error,
+ game_creator_claude_code_cli_version_identity,
+};
use codex_app_server::*;
pub(crate) use codex_app_server::{
cancel_direct_codex_turn_at, direct_game_creator_codex_chat_at,
diff --git a/apps/ai-game-creator-shell/src-tauri/src/agent/claude_code_cli.rs b/apps/ai-game-creator-shell/src-tauri/src/agent/claude_code_cli.rs
new file mode 100644
index 000000000..a6ad51e1f
--- /dev/null
+++ b/apps/ai-game-creator-shell/src-tauri/src/agent/claude_code_cli.rs
@@ -0,0 +1,906 @@
+//! Claude Agent SDK sidecar execution adapter for the AGC Agent Runtime.
+//!
+//! This adapter intentionally keeps Claude Code behind the existing LLM/Direct
+//! boundaries. Codex app-server remains a separate executor; the two wire
+//! protocols are never mixed.
+
+use super::*;
+use std::path::{Path, PathBuf};
+use std::process::Stdio;
+use std::sync::atomic::{AtomicBool, Ordering};
+use std::sync::{Arc, Mutex, OnceLock};
+use std::time::Duration;
+use tokio::io::{AsyncBufReadExt, AsyncReadExt, AsyncWriteExt, BufReader};
+
+const CLAUDE_AGENT_SIDECAR_ENV: &str = "GENARRATIVE_CLAUDE_AGENT_SIDECAR";
+const CLAUDE_NODE_ENV: &str = "GENARRATIVE_CLAUDE_NODE";
+const CLAUDE_CODE_OUTPUT_MAX_BYTES: usize = 8 * 1024 * 1024;
+const CLAUDE_CODE_PROMPT_MAX_BYTES: usize = 4 * 1024 * 1024;
+const CLAUDE_CODE_MODEL_MAX_CHARS: usize = 200;
+
+struct ActiveClaudeCodeTurn {
+ client_turn_id: String,
+ pid: u32,
+ alive: Arc,
+}
+
+static ACTIVE_CLAUDE_CODE_TURNS: OnceLock<
+ Mutex>,
+> = OnceLock::new();
+static CLAUDE_DIRECT_SESSIONS: OnceLock>> =
+ OnceLock::new();
+
+fn claude_direct_sessions() -> &'static Mutex> {
+ CLAUDE_DIRECT_SESSIONS.get_or_init(|| Mutex::new(std::collections::HashMap::new()))
+}
+
+fn active_claude_code_turns(
+) -> &'static Mutex> {
+ ACTIVE_CLAUDE_CODE_TURNS.get_or_init(|| Mutex::new(std::collections::HashMap::new()))
+}
+
+fn claude_project_key(root: &Path) -> PathBuf {
+ std::fs::canonicalize(root).unwrap_or_else(|_| root.to_path_buf())
+}
+
+fn kill_claude_code_process_tree(pid: u32) {
+ #[cfg(unix)]
+ unsafe {
+ libc::kill(-(pid as i32), libc::SIGKILL);
+ }
+ #[cfg(windows)]
+ {
+ let _ = std::process::Command::new("taskkill")
+ .args(["/PID", &pid.to_string(), "/T", "/F"])
+ .stdin(Stdio::null())
+ .stdout(Stdio::null())
+ .stderr(Stdio::null())
+ .status();
+ }
+}
+
+struct ActiveClaudeCodeTurnGuard {
+ key: PathBuf,
+ pid: u32,
+ alive: Arc,
+}
+
+impl Drop for ActiveClaudeCodeTurnGuard {
+ fn drop(&mut self) {
+ self.alive.store(false, Ordering::Release);
+ if let Ok(mut turns) = active_claude_code_turns().lock() {
+ if turns
+ .get(&self.key)
+ .is_some_and(|active| active.pid == self.pid)
+ {
+ turns.remove(&self.key);
+ }
+ }
+ }
+}
+
+struct ExternalMcpStopGuard;
+
+impl Drop for ExternalMcpStopGuard {
+ fn drop(&mut self) {
+ stop_external_mcp_loopback();
+ }
+}
+
+fn resolve_claude_agent_sidecar_script() -> Result {
+ if let Some(value) = std::env::var_os(CLAUDE_AGENT_SIDECAR_ENV) {
+ let path = PathBuf::from(value);
+ if path.is_file() {
+ return Ok(path);
+ }
+ return Err("Claude Agent SDK sidecar 路径不可用".to_string());
+ }
+ let manifest = Path::new(env!("CARGO_MANIFEST_DIR"));
+ let mut candidates = vec![manifest
+ .parent()
+ .expect("AGC Tauri manifest parent")
+ .join("agent-sidecar/src/index.mjs")];
+ if let Ok(executable) = std::env::current_exe() {
+ let exe_dir = executable.parent().unwrap_or_else(|| Path::new("."));
+ candidates.push(exe_dir.join("claude-agent/index.mjs"));
+ candidates.push(exe_dir.join("resources/claude-agent/index.mjs"));
+ #[cfg(target_os = "macos")]
+ if exe_dir.file_name().and_then(|value| value.to_str()) == Some("MacOS") {
+ if let Some(contents) = exe_dir.parent() {
+ candidates.push(contents.join("Resources/claude-agent/index.mjs"));
+ }
+ }
+ }
+ candidates
+ .into_iter()
+ .find(|path| path.is_file())
+ .ok_or_else(|| "Claude Agent SDK sidecar 未随 AGC 资源提供".to_string())
+}
+
+fn resolve_claude_node(root: &Path) -> PathBuf {
+ if let Some(value) = std::env::var_os(CLAUDE_NODE_ENV) {
+ return PathBuf::from(value);
+ }
+ crate::environment_check::resolve_node_runtime(root)
+ .map(|runtime| runtime.node)
+ .unwrap_or_else(|_| PathBuf::from("node"))
+}
+
+fn configure_sidecar_command(
+ command: &mut tokio::process::Command,
+ root: &Path,
+ llm: Option<&GameCreatorLlmConfig>,
+ home: &Path,
+) -> Result<(), String> {
+ let script = resolve_claude_agent_sidecar_script()?;
+ command
+ .arg(script)
+ .current_dir(root)
+ .stdin(Stdio::piped())
+ .stdout(Stdio::piped())
+ .stderr(Stdio::piped())
+ .kill_on_drop(true);
+ configure_claude_code_environment(command, llm, home);
+ configure_claude_code_process(command);
+ Ok(())
+}
+
+#[derive(Debug)]
+struct SidecarTurnResult {
+ events: Vec,
+ result: serde_json::Value,
+}
+
+async fn run_sidecar_turn(
+ root: &Path,
+ llm: Option<&GameCreatorLlmConfig>,
+ home: &Path,
+ request: serde_json::Value,
+ timeout_ms: u64,
+ active_turn: Option<&str>,
+) -> Result {
+ let node = resolve_claude_node(root);
+ let mut command = tokio::process::Command::new(node);
+ configure_sidecar_command(&mut command, root, llm, home)?;
+ let mut child = command
+ .spawn()
+ .map_err(|error| format!("Claude Agent SDK sidecar 启动失败:{error}"))?;
+ let pid = child
+ .id()
+ .ok_or_else(|| "Claude Agent SDK sidecar 进程缺少 PID".to_string())?;
+ let active_key = active_turn.map(|_| claude_project_key(root));
+ let active_alive = Arc::new(AtomicBool::new(true));
+ if let (Some(key), Some(client_turn_id)) = (active_key.clone(), active_turn) {
+ let mut turns = active_claude_code_turns()
+ .lock()
+ .map_err(|_| "Claude Code Direct 回合中断表不可用".to_string())?;
+ turns.insert(
+ key,
+ ActiveClaudeCodeTurn {
+ client_turn_id: client_turn_id.to_string(),
+ pid,
+ alive: Arc::clone(&active_alive),
+ },
+ );
+ }
+ let _active_guard = active_key.map(|key| ActiveClaudeCodeTurnGuard {
+ key,
+ pid,
+ alive: Arc::clone(&active_alive),
+ });
+ let mut stdin = child
+ .stdin
+ .take()
+ .ok_or_else(|| "Claude Agent SDK sidecar stdin 未建立".to_string())?;
+ stdin
+ .write_all(
+ format!(
+ "{}\n",
+ serde_json::to_string(&request).map_err(|error| error.to_string())?
+ )
+ .as_bytes(),
+ )
+ .await
+ .map_err(|error| format!("写入 Claude Agent SDK sidecar 请求失败:{error}"))?;
+ drop(stdin);
+ let stdout = child
+ .stdout
+ .take()
+ .ok_or_else(|| "Claude Agent SDK sidecar stdout 未建立".to_string())?;
+ let stderr = child
+ .stderr
+ .take()
+ .ok_or_else(|| "Claude Agent SDK sidecar stderr 未建立".to_string())?;
+ let stderr_task = tokio::spawn(read_capped(stderr, CLAUDE_CODE_OUTPUT_MAX_BYTES));
+ let result = tokio::time::timeout(Duration::from_millis(timeout_ms.max(1)), async {
+ let mut lines = BufReader::new(stdout).lines();
+ let mut events = Vec::new();
+ let mut terminal = None;
+ while let Some(line) = lines
+ .next_line()
+ .await
+ .map_err(|error| format!("读取 Claude Agent SDK sidecar 输出失败:{error}"))?
+ {
+ if line.trim().is_empty() {
+ continue;
+ }
+ let value: serde_json::Value = serde_json::from_str(&line)
+ .map_err(|_| "Claude Agent SDK sidecar 输出不是有效 JSON".to_string())?;
+ match value.get("type").and_then(serde_json::Value::as_str) {
+ Some("event") => {
+ if let Some(event) = value.get("event") {
+ events.push(event.clone());
+ }
+ }
+ Some("done") => {
+ terminal = value.get("result").cloned();
+ break;
+ }
+ Some("error") => {
+ return Err(value
+ .get("message")
+ .and_then(serde_json::Value::as_str)
+ .unwrap_or("Claude Agent SDK sidecar 执行失败")
+ .to_string());
+ }
+ _ => {}
+ }
+ }
+ let status = child
+ .wait()
+ .await
+ .map_err(|error| format!("等待 Claude Agent SDK sidecar 结束失败:{error}"))?;
+ let result = terminal.ok_or_else(|| "Claude Agent SDK sidecar 缺少终态".to_string())?;
+ if !status.success() {
+ return Err("Claude Agent SDK sidecar 非零退出".to_string());
+ }
+ Ok(SidecarTurnResult { events, result })
+ })
+ .await
+ .map_err(|_| "Claude Agent SDK sidecar 回合超时".to_string())??;
+ let _ = stderr_task.await;
+ Ok(result)
+}
+
+pub(crate) fn cancel_direct_claude_code_turn_at(
+ root: &Path,
+ client_turn_id: Option<&str>,
+) -> Result |