deerworkflow
§ · Nested detail page

Codex / Claude Agent 适配器 深度解析

从 deer-workflow 源码逐段确认该模块的契约、执行路径、事件语义和失败边界。

§ 00 · IMPORT CHAIN

导入链与契约

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 和库调用共享。

src/src/agents/types.ts · L33-83TypeScript
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>;
§ 01 · CORE IMPL

核心实现,逐段拆

▸入口与状态

第二层证据是 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 都纳入可解释的运行轨迹。

src/src/agents/codex-agent.ts · L117-203TypeScript
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 子进程出现异常时,源码会抛出具名错误或保留原始诊断,而不是吞掉上下文。

src/src/agents/claude-agent.ts · L114-189TypeScript
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  }
§ 02 · KEY FLOW

关键流程

▸端到端调用路径

input → contract check → context/event binding → user handler → end/error event → typed result

最后看 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 示例更有价值,因为它回答了失败时系统还剩下哪些可观察信号。

§ 03 · EDGE CASES

边界 / 失败模式

  • 源码把错误路径显式化,但调用方仍要决定 null、throw 和 retry 的业务语义。
  • CLI adapter 依赖宿主机安装对应命令,测试中通过 stub 覆盖,生产中还需要版本治理。
  • 事件流是同步 emitter,监听器自身不能做阻塞重活。
§ 04 · RELATED

关联模块

Generated · 2026-07-28Source · deerwork-ai/deer-workflow↑ 回到索引