短版说明 · 面向后续 agent

Claude Adapter
能力与原理

让后续 agent 快速理解 Eyrie 如何把统一的 agent session/run/event 接口,接到本机 claude CLI:能做什么、靠什么机制工作、改哪里最安全。

01

一句话

Claude adapter 是 daemon 内部的 AgentProvider 实现:它把 Eyrie 统一的 agent session/run/event 接口,翻译成本机 claude CLI 的 stream-json + stdio 控制协议。

范围:本文描述 Claude adapter 子系统本身。更细的代码索引见当前产品 worktree 中的 docs/claude-code-provider.md

上层只看到统一接口

AgentProvider · AgentRunner · AgentProviderEvent

adapter 内部处理Claude 协议

命令行参数 · stdout stream-json · permission hook · control_request / control_response

状态落点runner 不写库

runner 把事件交给 SessionSink,由 sink 持久化后再广播

02

已具备的能力

能力当前含义
Provider 注册内置 provider id 是 builtin-claude-code,kind 是 claude-code,transport 是 stdio
可用性探测claude --version 做有界探测;Windows 默认 claude.cmd,其他平台默认 claude
Turn 执行把用户文本/图片输入转成 Claude stream-json user message,写入 stdin。
Resume已有 providerSessionId 时用 --resume <id> 启动 Claude;可用 resumeAtMessageId 建立首个 resumed turn 的边界。
输出翻译把 Claude stdout 翻成 message.deltatool.startedusage.updatedrun.completed 等统一事件。
ApprovalClaude can_use_tool 普通工具请求会变成 approval.requested,用户选择后写回 control_response
AskUserQuestionClaude AskUserQuestion 会变成 Eyrie 的 input.requested,用户回答后写回 Claude。
Slash control支持 compactclearreviewsecurity-reviewinitpr-commentscontextcost
Interruptrunner 可以通过 Claude control message 请求中断,必要时进入进程树 teardown。
Token usageClaude usage payload 会映射成 usage.updated
持久化事件所有 provider event 经 SessionSinkevent-codec.ts 落库,再通过事件流发给订阅者。

能力边界:Claude provider 对外报告 resumeinterrupttoken-usageapproval报告 loadstructured-diffforkrollbackAgentService.startTurn(...)runControl(...)respondToApproval(...) 是内部 service/runner 能力;当前公开 tRPC 面主要是 agent.eventsproviders.listModelsinputRequests.respond

03

工作原理

主路径可以压缩成两条线。

IN用户输入进 Claude

AgentService.startTurn(...)
RunnerManager
DbAgentRegistry
ClaudeProvider.createRunner(...)
ClaudeRunner.startTurn(...)
ClaudeProcess.writeLine(...)
claude stdin

OUTClaude 输出回 Eyrie

claude stdout
ClaudeRunner.readStdoutLoop(...)
control-protocol.ts 或 translator.ts
AgentProviderEvent
SessionSink.emit(...)
event-codec.ts
DrizzleAgentRepository.appendEvent(...)
EventBroadcaster.publish(...)
agent.events

关键原则

01 · 协议隔离

Claude 原生 JSON 只在 apps/daemon/src/agent/providers/claude/ 内部出现。service、repository、UI 不理解 Claude 协议。

02 · 先落库再广播

SessionSink 先调用 repository 持久化,成功后才 publish。订阅者断线重连时可以靠 sessionSeq 补事件。

03 · runner 是 live state owner

进程、stdout loop、pending approvals、pending input requests、resume cursor 都由 ClaudeRunner 管。

04 · 状态副作用集中

run.completed 如何改变 run/session/request 状态,不写在 adapter 里,而在 event-codec.ts 和 repository projection 里。

04

启动与配置

normalizeClaudeProviderConfig(...) 先校验配置,再允许启动进程。它会拒绝未知字段,也会拒绝覆盖 adapter 自己必须控制的 flag。

adapter 固定加的 Claude 参数

-p
--input-format stream-json
--output-format stream-json
--verbose
--include-partial-messages
--permission-prompt-tool=stdio

可配置项(走 typed config,不要塞进 raw extraArgs)

modeleffortagent permissionModesettingsPathmcpConfigPath allowedToolsdisallowedToolsappendSystemPromptenv
05

Turn 和 Resume

fresh session 的特殊点是:Eyrie 一开始还不知道 Claude native session id。runner 会先缓存普通业务事件,等 stdout 里捕获 providerSessionId 后,先发 run.started,再 flush 之前缓存的事件。

resumed session 已经有 providerSessionId,所以可以先发 run.started。如果 resume handle 带 resumeAtMessageId,第一条 resumed user message 会把它作为 parent_tool_use_id 发给 Claude;之后这个边界会被清掉。

这个设计保证

  • run 一开始就能记录 provider-native session。
  • 之后可以用 --resume <providerSessionId> 继续同一 Claude 会话。
  • 事件顺序对 UI 和 replay 都稳定。
06

Approval 与 AskUserQuestion

Claude 的 permission prompt 从 stdout 进来,形状是 control_request.can_use_tool

普通工具Bash / Edit / Read / ...

can_use_tool(Bash/Edit/Read/...)
approval.requested
user chooses allow / always / deny
runner writes control_response
approval.resolved

AskUserQuestion结构化提问

can_use_tool(AskUserQuestion)
input.requested
inputRequests.respond
service validates & redacts persisted answers
runner writes unredacted answer in control_response
input.resolved

注意两点:持久化和广播只保存 sensitive answer 的脱敏摘要;发回 Claude 的答案必须是未脱敏版本,否则 Claude 无法继续执行。

07

Control Command

Claude slash commands 在 commands.ts 声明。ClaudeRunner.runControl(...) 把 Eyrie command 转成类似 /compact 的 user message,写进同一个 Claude stdin,再收集 stdout 直到 terminal result。

  • 成功的 clear 除了 control.completed,还会发 context.cleared
  • 未知 command 不写给 Claude,只产生 control.completed(status: unsupported)
08

状态机要点

一次 session 只能有一个 active run

repo.beginTurn(...) 在事务里创建 run 并标记 session active。ClaudeRunner 也会拒绝并发 startTurn(...)

request resolution 必须经过 resolving

// approval
pendingresolving approved|denied|send_failed
pendingcancelled
// input request
pendingresolving accepted|declined|cancelled|send_failed
pending/resolvingcancelled

resolving 的意义是:用户已经提交响应,但 service 还在尝试把响应送到 provider。只有写回 provider 成功,数据库才进入 accepted/approved 这类终态。

resolution event 归属原 run

用户可能在后续 turn 已经开始后才回答旧 approval/input request。approval.resolvedinput.resolved 必须挂到请求所属的原 run,而不是当前 sink 的 live run。

lifecycle cancel 和 send failure 不同

daemon restart、runner crash、interrupt 这类生命周期清理可以直接 UPDATE pending rows,因为可能已经没有 live sink。用户响应发送失败则应该记录成 send_failed,因为这是一次活跃响应路径的失败。

09

读代码顺序

先读共享边界

  1. apps/daemon/src/agent/types.ts
  2. apps/daemon/src/agent/service.ts
  3. apps/daemon/src/agent/runner-manager.ts
  4. apps/daemon/src/agent/session-sink.ts
  5. apps/daemon/src/agent/event-codec.ts

再读 Claude adapter

  1. apps/daemon/src/agent/providers/claude/module.ts
  2. apps/daemon/src/agent/providers/claude/provider.ts
  3. apps/daemon/src/agent/providers/claude/config.ts
  4. apps/daemon/src/agent/providers/claude/process.ts
  5. apps/daemon/src/agent/providers/claude/input.ts
  6. apps/daemon/src/agent/providers/claude/translator.ts
  7. apps/daemon/src/agent/providers/claude/control-protocol.ts
  8. apps/daemon/src/agent/providers/claude/commands.ts
  9. apps/daemon/src/agent/providers/claude/runner.ts

runner.ts 最后读。它是组合点,不是最好的第一入口。

10

改动入口

想改什么从哪里开始
provider metadata / capabilityconfig.tsmodule.tsprovider.ts
Claude 启动参数config.tsprocess.tsspawn.ts
文本/图片输入input.tsservice.ts、API schema
stdout 普通消息翻译translator.ts
approval / AskUserQuestioncontrol-protocol.tsrunner.tsservice.tsevent-codec.ts
slash commandcommands.tsrunner.ts
run/session/request 状态副作用event-codec.tsdrizzle-repository.ts
公开 tRPC APIpackages/api/src/schemas.tspackages/api/src/services.tspackages/api/src/trpc.tsapps/daemon/src/trpc/services.ts
11

真实行为测试覆盖

读完“改动入口”后,可以把测试当成回归边界来读:它们不是按文件名凑覆盖,而是锁住调用者能观察到的结果。这里的“结果”包括 CLI 参数、stdin/stdout 协议、事件、数据库状态投影、tRPC 返回值和错误码。

配置和能力claude-provider.test.ts

覆盖 provider id、transport、capability、session config 字段、非法配置拒绝、availability probe,以及 fresh/resume command 的参数形状。

进程启动和退出claude-process.test.ts

覆盖 Windows claude.cmd 启动、批处理参数安全、环境变量合并、stdout/stderr 读取、早期 spawn 失败诊断,以及终止时的清理顺序。

Turn 和 Resumerunner-session / runner-turn

覆盖启动参数如何进入 live runner、resume 边界如何只用于第一轮、session id 到达前如何缓冲事件、超时/早退如何变成 failed completion、并发 turn 如何被拒绝、文本和图片输入如何进入 stdin。

人工介入和控制命令claude-runner-control.test.ts

覆盖 approval、AskUserQuestion、ExitPlanMode、slash command、context clear 投影、重复输出去重、interrupt 后的 pending waiter 清理,以及过期 provider response 不应影响当前 run。

真实行为主要测试锚点读者应该看懂什么
公开 provider 表面只暴露已经接通的能力,启动配置不能绕过 adapter 控制的协议参数。 tests/claude-provider.test.ts 改 capability、session config 或命令参数时,测试会检查外部调用者看到的 metadata 和实际生成的 Claude command 是否一致。
本机 claude 进程按平台安全启动,并且 stdout、stderr、exit、kill 的行为都有确定结果。 tests/claude-process.test.ts process.ts 时,不只要能 spawn,还要保住 Windows batch 参数、错误诊断、输出流消费和进程树清理。
一次 turn 从用户输入进入 runner,经过 stream-json 写入,再由 stdout 事件推动 run.started、message delta 和 run.completed tests/claude-runner-session.test.ts
tests/claude-runner-turn.test.ts
读这些测试可以看到普通 turn、resume turn、初始化超时、早期退出、写入失败、图片路径校验和并发拒绝这些主路径边界。
approval 和 AskUserQuestion 的用户响应先占用请求,再送到 provider,最后通过事件落到终态。 tests/claude-runner-control.test.ts
tests/agent-service-methods.test.ts
测试守住 pending → resolving → terminal,也守住“响应事件属于原 run,而不是后来变成 current 的 run”。敏感答案会给 provider 使用,但存储和广播只保留脱敏结果。
control command 走 live Claude stream,成功、失败、interrupt、unknown command 和 stale response 都有可观察结果。 tests/claude-runner-control.test.ts commands.ts 或 control protocol 时,要确认 slash command 输出、context-cleared projection、控制 run 终态和 pending 请求清理仍然成立。
事件编码把 provider event 变成数据库行、外键前置行和状态投影,并且 replay 时以数据库行字段为准。 tests/agent-event-codec.test.ts event-codec.ts 时,要先看事件 payload、projection、FK prerequisite 和敏感字段落点,避免破坏落库后的 session/run/request 状态。
公开 tRPC 输入响应接口会收窄 accept、decline、cancel,并把 missing、跨 session、非 pending 请求映射成稳定错误。 tests/input-requests-trpc.test.ts 改 API schema 或 service 边界时,测试会检查客户端看到的成功返回和错误码,而不是只检查内部函数能不能跑。
12

最小心智模型

记住这四句话就够用:

  1. Claude adapter 是协议翻译器,不是业务状态中心。
  2. ClaudeRunner 管 live process;SessionSink 管事件落库和广播。
  3. approval/input request 的用户响应先 pending → resolving,送达 provider 后才进入终态。
  4. 改数据库副作用先看 event-codec.ts,改 Claude 协议先看 process.ts / translator.ts / control-protocol.ts,最后再进 runner.ts