Codex / Claude Agent 适配器 深度解析
从 deer-workflow 源码逐段确认该模块的契约、执行路径、事件语义和失败边界。
导入链与契约
Codex / Claude Agent 适配器 的第一层证据来自 src/agents/types.ts:33、src/agents/types.ts:59、src/agents/agent.ts:9、src/agents/codex-agent.ts:117 与 src/agents/claude-agent.ts:114:这些行把入口、上下文、错误或配置集中在一个小范围内,说明 deer-workflow 倾向把复杂度留在明确契约里,而不是散到调用者脚本。这里的设计价值是让 Agent 自动化可以被测试、被日志系统消费,也能被 CLI 和库调用共享。
33export interface AgentOptions { 34 /** Working directory presented to the Agent. */ 35 cwd?: string; 36 37 /** Runtime-specific model identifier. */ 38 model?: string; 39 40 /** JSON Schema that constrains and types the final response. */ 41 schema?: JsonSchema; 42 43 /** Sandbox policy applied to tools used by the Agent. */ 44 sandbox?: AgentSandbox; 45 46 /** Extra directories the Agent may write outside {@link AgentOptions.cwd}. */ 47 additionalWritableDirectories?: string[]; 48 49 /** Environment variables merged into the Agent process environment. */ 50 env?: Record<string, string | undefined>; 51 52 /** Signal used to cancel the complete Agent run. */ 53 signal?: AbortSignal; 54} 55 56/** 57 * Vendor-neutral contract for a complete tool-using Agent Loop. 58 */ 59export interface Agent { 60 /** 61 * Runs an Agent until it produces a final response. 62 * 63 * @typeParam TOutput - Expected final response type. Use an object type when 64 * {@link AgentOptions.schema} is provided; otherwise it defaults to text. 65 * @param prompt - Task instructions for the Agent. 66 * @param options - Runtime, output, and cancellation options. 67 * @returns The final text or parsed schema-backed response. 68 */ 69 run<TOutput = string>( 70 prompt: string, 71 options?: AgentOptions, 72 ): Promise<TOutput>; 73} 74 75/** 76 * Callable form of {@link Agent.run}. 77 * 78 * @typeParam TOutput - Expected final response type. 79 */ 80export type AgentFunction = <TOutput = string>( 81 prompt: string, 82 options?: AgentOptions, 83) => Promise<TOutput>;
核心实现,逐段拆
▸入口与状态
第二层证据是 src/agents/codex-agent.ts:128、src/agents/codex-agent.ts:137、src/agents/codex-agent.ts:176、src/agents/claude-agent.ts:127 与 src/agents/claude-agent.ts:157。这些引用展示了失败路径、事件路径或参数路径如何闭环:不是只在 happy path 发一个结果,而是把输入解析、阶段切换、stdout/stderr、schema 或 duration 都纳入可解释的运行轨迹。
117 async run<TOutput = string>( 118 prompt: string, 119 options: AgentOptions = {}, 120 ): Promise<TOutput> { 121 if (prompt.trim().length === 0) { 122 throw new TypeError("CodexAgent requires a non-empty prompt."); 123 } 124 125 options.signal?.throwIfAborted(); 126 this.#assertCommandAvailable(); 127 128 const temporaryDirectory = await mkdtemp( 129 join(tmpdir(), "deer-workflow-codex-"), 130 ); 131 const outputPath = join(temporaryDirectory, "last-message.txt"); 132 const schemaPath = join(temporaryDirectory, "output-schema.json"); 133 134 try { 135 const command = this.#buildCommand(options, outputPath, schemaPath); 136 137 if (options.schema) { 138 await writeFile( 139 schemaPath, 140 `${JSON.stringify(options.schema, null, 2)}\n`, 141 "utf8", 142 ); 143 } 144 145 const cwd = resolve(options.cwd ?? this.#config.cwd ?? process.cwd()); 146 const subprocess = Bun.spawn(command, { 147 cwd, 148 env: mergeEnvironment(process.env, this.#config.env, options.env), 149 stdin: "pipe", 150 stdout: "pipe", 151 stderr: "pipe", 152 }); 153 154 const abort = (): void => { 155 subprocess.kill(); 156 }; 157 options.signal?.addEventListener("abort", abort, { once: true }); 158 159 subprocess.stdin.write(prompt); 160 subprocess.stdin.end(); 161 162 try { 163 const [stdout, stderr, exitCode] = await Promise.all([ 164 new Response(subprocess.stdout).text(), 165 new Response(subprocess.stderr).text(), 166 subprocess.exited, 167 ]); 168 169 if (options.signal?.aborted) { 170 throw ( 171 options.signal.reason ?? 172 new DOMException("The Agent run was aborted.", "AbortError") 173 ); 174 } 175 176 if (exitCode !== 0) { 177 throw new CodexAgentError( 178 `Codex CLI exited with status ${exitCode}.`, 179 { exitCode, stdout, stderr }, 180 ); 181 } 182 183 const lastMessage = await readFile(outputPath, "utf8"); 184 185 if (!options.schema) { 186 return lastMessage.trimEnd() as TOutput; 187 } 188 189 try { 190 return JSON.parse(lastMessage) as TOutput; 191 } catch (cause) { 192 throw new CodexAgentError( 193 `Codex CLI returned invalid JSON for the requested schema: ${formatCause(cause)}`, 194 { exitCode, stdout, stderr }, 195 ); 196 } 197 } finally { 198 options.signal?.removeEventListener("abort", abort); 199 } 200 } finally { 201 await rm(temporaryDirectory, { recursive: true, force: true }); 202 } 203 }
▸失败与边界
第三层证据来自 src/agents/codex-agent.ts:211、src/agents/codex-agent.ts:244、src/agents/claude-agent.ts:211、src/agents/claude-agent.ts:244 与 tests/agents/codex-agent.test.ts:65。这些行说明这个模块的边界主要靠类型和测试守住:当 workflow 嵌套、metadata、JSON input、CLI 命令或 Agent 子进程出现异常时,源码会抛出具名错误或保留原始诊断,而不是吞掉上下文。
114 async run<TOutput = string>( 115 prompt: string, 116 options: AgentOptions = {}, 117 ): Promise<TOutput> { 118 if (prompt.trim().length === 0) { 119 throw new TypeError("ClaudeAgent requires a non-empty prompt."); 120 } 121 122 options.signal?.throwIfAborted(); 123 this.#assertCommandAvailable(); 124 125 const command = this.#buildCommand(options); 126 const cwd = resolve(options.cwd ?? this.#config.cwd ?? process.cwd()); 127 const subprocess = Bun.spawn(command, { 128 cwd, 129 env: mergeEnvironment(process.env, this.#config.env, options.env), 130 stdin: "pipe", 131 stdout: "pipe", 132 stderr: "pipe", 133 }); 134 135 const abort = (): void => { 136 subprocess.kill(); 137 }; 138 options.signal?.addEventListener("abort", abort, { once: true }); 139 140 subprocess.stdin.write(prompt); 141 subprocess.stdin.end(); 142 143 try { 144 const [stdout, stderr, exitCode] = await Promise.all([ 145 new Response(subprocess.stdout).text(), 146 new Response(subprocess.stderr).text(), 147 subprocess.exited, 148 ]); 149 150 if (options.signal?.aborted) { 151 throw ( 152 options.signal.reason ?? 153 new DOMException("The Agent run was aborted.", "AbortError") 154 ); 155 } 156 157 const message = this.#parseResultMessage(stdout, { 158 exitCode, 159 stdout, 160 stderr, 161 }); 162 163 if (exitCode !== 0 || message.is_error) { 164 throw new ClaudeAgentError( 165 `Claude Code CLI reported a failure: ${message.result ?? `exit status ${exitCode}`}`, 166 { exitCode, stdout, stderr }, 167 ); 168 } 169 170 if (!options.schema) { 171 return (message.result ?? "").trimEnd() as TOutput; 172 } 173 174 if (message.structured_output !== undefined) { 175 return message.structured_output as TOutput; 176 } 177 178 try { 179 return JSON.parse(message.result ?? "") as TOutput; 180 } catch (cause) { 181 throw new ClaudeAgentError( 182 `Claude Code CLI returned invalid JSON for the requested schema: ${formatCause(cause)}`, 183 { exitCode, stdout, stderr }, 184 ); 185 } 186 } finally { 187 options.signal?.removeEventListener("abort", abort); 188 } 189 }
关键流程
▸端到端调用路径
最后看 tests/agents/codex-agent.test.ts:86、tests/agents/codex-agent.test.ts:99、src/agents/codex-agent.ts:201、src/agents/claude-agent.ts:178 与 src/agents/types.ts:207。这组证据把实现和测试连起来:源码定义行为,测试断言事件顺序、错误闭合和返回值结构。对真实 coding-agent 平台来说,这比 README 示例更有价值,因为它回答了失败时系统还剩下哪些可观察信号。
边界 / 失败模式
- 源码把错误路径显式化,但调用方仍要决定 null、throw 和 retry 的业务语义。
- CLI adapter 依赖宿主机安装对应命令,测试中通过 stub 覆盖,生产中还需要版本治理。
- 事件流是同步 emitter,监听器自身不能做阻塞重活。