feat(daemon): add Claude Code stdio provider adapter
给 daemon 装上首个内建 agent provider —— Claude Code。把本地
claude CLI 当成长驻的 stream-json stdio 子进程驱动,并将其协议翻译成
Eyrie 归一化的 AgentProviderEvent。本 PR 只实现既有契约,不改契约本身。
01 它落在哪一层
Eyrie agent 层是三层架构。本 PR 只动最底层的 provider 翻译:把 CLI 原生协议译成归一化事件。它不碰 run id、不写库、不发 git diff、不做审批——那些都归 service 层。
sink.emit(event)。02 类图
三个跨模块接口(AgentProvider / AgentRunner / AgentEventSink)+ 进程封装接口 ClaudeProcess + 工厂类型 SpawnClaudeProcess。ClaudeProvider 只造 runner;ClaudeRunner 依赖另外两者,自己只实现 AgentRunner。
03 运行时状态机
runner 用判别式联合表达生命周期,永远无法表示「有活动 turn 却无进程」。这个 PR 的核心修复就在这里:完成 / 干净中断后回 idle 复用同一进程(绿),而非销毁;只有硬杀 / EOF 才回 unstarted 下次 respawn(橙虚线)。
dispose() 进终态;图中只画代表边。04 三条主流程 · 时序
同一套 4 条 lifeline,三种走法。关键差异:Fresh 要缓冲到 session-id 才发 run.started;Resume 已知 id,立即发、直发;Interrupt 分干净退出与 2s 超时硬杀两支。
05 事件缓冲时间线
新 turn 里 到达序 ≠ 发出序。session-id(init 行)到达前的业务事件先进 pendingEvents 缓冲;一旦拿到 id,先发一次 run.started,再按原序 flush 缓冲,之后的行直发。保证 run.started 永远第一、run.completed 永远最后。
delta₁/delta₂ 在 init 之前到达,被缓冲;run.started 经由 init 触发后插到队首,再 flush 缓冲,故连线交叉。06 translator 映射
translateClaudeLine() 是纯函数:一行 stream-json → 0..N 个归一化事件。对 Claude schema 增长保持容忍——抽已知字段、原样留 raw、未知形状忽略(除非命中失败启发式)。
result → run.completed 覆写为 cancelled;translator 本身不感知中断,只产原始形状。07 失败隔离
四类异常各有归宿,但都不让 runner 卡死或泄漏进程。sink 失败是特例:sink 已坏,不能再经它发终态——只内部记录、直接 kill + waitForExit 后回 unstarted。其余三类收敛成一条合成的 run.completed{failed},带末 4 KB stderr 尾巴。
run.started 必先于任何 run.completed——合成失败也经 ensureRunStartedBeforeCompletion 守卫补发。08 落地清单 · 给 reviewer
6 commits · 全在 apps/daemon · 纯增量
feat | process / runner / translator + provider 入口 |
test | fake-process 夹具 + 4 单测 + 1 集成 |
fix | 长/中断后 runner 留 idle 复用,去 per-turn 超时 |
refactor | 统一 run.started 顺序守卫 |
refactor | 共享 stderr limit、清理超时竞态 |
fix | 硬化 teardown:kill 升级、resume 写失败补发 |
5 源码模块 · src/agent/providers/claude/
index.ts | ClaudeProvider · caps 保守(无 approval) |
errors.ts | typed code:configuration/runtime/unsupported |
process.ts | argv 构造 · config 校验 · kill 升级 |
runner.ts | session 级状态机,独占一进程 |
translator.ts | 纯函数 line → events |
测试 · 77 单测 + 2 集成(CI 跳过)
| 套件 | n |
|---|---|
claude-runner | 33 |
claude-translate | 22 |
claude-provider | 14 |
claude-process | 8 |
claude-integration(gated) | 2 |
CLAUDE_CODE_INTEGRATION=1 bunx vitest run \
apps/daemon/tests/claude-integration.test.ts
对现有代码的影响
- 纯增量:11 新文件,0 改动,
+3,829 / −0。 - 运行时惰性:尚未被任何 registry / service 引用,合并不改可观测行为。
- 无新依赖:仅 Node 内建,
bun.lock不变。 - 0 冲突:与 main 的 client/scripts 改动零路径重叠。
adapter 以 --permission-mode bypassPermissions 起进程、不声明 approval 能力,被拉起的 Claude 在 session 的 cwd 下无交互权限提示运行。这对无头 daemon 是有意为之,但它是 review 时最该掂量的一点;交互式审批是已记录的后续项。Unix 下 spawn detached,kill 路径覆盖整个进程组。
09 后续项 · 不在本 PR
- 把
ClaudeProvider接进 registry,补完AgentService编排(createSession / resumeSession / startTurn …)。 - 审批 + 交互式权限模式(plan / review / bypass)——需全程 control protocol,是 approval 的同一块地基。
- provider control commands、attachments、
load式历史回放。 - 在 CLI / desktop 客户端中呈现该 provider。
合并方式:squash-merge,PR 标题成为落地 commit(.github/workflows/pr-title.yml)。本分支自 b1be059 起带 6 个 commit 叠在 main 之上。