refactor: エージェント抽象を導入して Codex / Grok の差し込み口を作る(Phase A) - #97
Merged
Conversation
`unavailable` を検証する 2 件だけ `runUpdateCommand` の直後に assert していて、 他の同種テストにある `await flush()` が抜けていた。ダイアログは「確認中…」を 一度描いてから結果に差し替わるため、負荷の高い並列実行では確認中のまま assert して落ちる。兄弟テストと同じ待ち方に揃える。
Codex / Grok など Claude 以外のコーディングエージェントに対応できる形にする
Phase A。挙動は変えず、差し込み口だけを作る。
## 何をしたか
SDK メッセージを直接 SessionState へ畳んでいた `sdk-parse.ts` を 2 段に割った:
provider のメッセージ ─[アダプタの parse]→ AgentEvent[] ─[applyAgentEvent]→ SessionState
- `core/agent-events.ts` … provider 非依存のイベント語彙と**共通の畳み込み**。
ログの上限・進捗・サブエージェントの完了ゲート・PR 検出・コスト集計はここ。
- `core/agent-ports.ts` … `AgentAdapter` / `AgentCapabilities` / `AgentRun` /
`PermissionDecision`。新しいエージェントはこれを実装するだけで済む。
- `core/claude-parse.ts` / `claude-errors.ts` / `claude-adapter.ts` …
Claude 固有の知識(メッセージの形・CLI の文言・`query()` の署名)の置き場所。
境界は `SessionHandle` / `AgentAdapter` に引いた。`QueryFn` ではない —
あれは Claude の control-request モデルそのもので、共通 IF にすると全 provider が
それを模倣する羽目になる。
## 副次的に直ったこと
- **core の中立モジュールから SDK の import が消えた**。SDK を import するのは
`claude-*.ts`(アダプタ)だけになり、`status-reducer.ts` / `config.ts` は
自前の型・定数で閉じた。
- `PermissionResult`(SDK 型)が core に漏れていたのを自前の `PermissionDecision`
に置き換えた(`PermissionRequest` は元から自前だったので、ようやく対になった)。
- `Session.consume` が `rate_limit_event` を直接読んでいた規約違反
(「形の知識は 1 か所」)を解消した。
- 失敗の分類(認証切れ / レート制限 / 通信断)が reducer から
`AgentAdapter.classifyError` へ移り、`aborted` イベントは `cause` を運ぶように
なった。文言の正規表現は provider ごとの知識なので状態機械には置かない。
## セッション途中のエージェント切替に備えた状態
worktree(成果物)は provider 非依存なので、Claude で進めた作業を途中から Codex に
引き継げる。ただしモデル側の文脈は provider をまたげない(各 CLI が自分の
トランスクリプトを持つ)ため、切替は「今のターンを畳む → 同じ worktree で別
provider の新セッションを開く」になる。
- `SessionState.agentSessions` に **provider ごとの resume id** を持たせ、
永続化もした。Claude → Codex → Claude と戻ると続きから再開できる。
- `LogEntry.agent` でどこからが別エージェントの発言かを刻む。切替を使っていない
セッションでは付かないので、既存の表示は一切変わらない。
- `Session.setAgent()` / `getAgent()` と `CodivaEvent` の `agent_switched` を追加。
`/agent` コマンドと引き継ぎプロンプトの生成は Phase B。
## 検証
lint / typecheck / test(2277 件)/ build すべて通過。1,100 行超の実データテストは
入口(旧 `applySdkMessage` → `applyClaudeMessage`)を変えずに残してあり、
分割のリグレッション網になっている。
Phase A(Codex / Grok の差し込み口づくり)の実装に合わせてドキュメントを更新する。 - `docs/ARCHITECTURE.md` … 「エージェント抽象」節を新設。境界を `SessionHandle` / `AgentAdapter` に引いた理由(`QueryFn` を共通 IF にすると全 provider に Claude の control-request モデルの模倣を強いる)、中立モジュールが SDK を import しない という線引き、畳み込みを共通化して写像だけをアダプタに置いた構造、そして セッション途中の切替で**何が引き継がれて何が引き継がれないか**を書いた。 - `CLAUDE.md` … 「コードの地図」に「別のエージェントに対応させる」の行を追加。 不変条件 3 を「SDK の形を知るのは sdk-parse.ts だけ」から「エージェント固有の 知識はアダプタに閉じる」へ書き直した。 - `.claude/rules/sdk-integration.md` … 2 段構成(アダプタの parse → 共通の fold)と、 中立モジュールは SDK を import しないルールを明記。 - `.claude/rules/session-domain.md` … `agent_switched` / `aborted.cause` を `CodivaEvent` の表に追加し、永続化の節に `agent` / `agentSessions` を足した。 - `.claude/rules/architecture.md` … DI seam の節に `core/agent-ports.ts` を追加。 - `docs/PRD.md` / `docs/TASKS.md` … 「他エージェント対応 … 未実装」を Phase A 完了に 更新し、Phase B(ACP アダプタ + Codex)/ C(Grok)/ D(capability による UI 縮退・ `/agent`・引き継ぎプロンプト)を未着手として残した。 README.md は変更なし(ユーザー可視の挙動は変わっていない)。
`AgentCapabilities` は Phase A で「表」と `getAgent()` までが入っただけで、 それを見て UI を縮退させる配線は Phase D。読んだ人が「もう縮退する」と 誤解しないよう明記した。併せて不変条件 2(状態遷移は `reduce` と `applyAgentEvent` の 2 本)と 4(i18n の例外にエージェント名を追加)を補足。
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
何のための PR か
codiva は Claude Code 専用でしたが、将来 Codex / Grok に対応できる形にするための土台を入れます。
挙動は変えず、差し込み口だけを作る Phase A です(アダプタ本体の実装は Phase B 以降)。
さらに「Claude で実装を進めていたが途中から Codex に切り替える」というユースケースを想定した
状態モデルまで入れてあります。
構造の変更
SDKMessageを直接SessionStateへ畳み込んでいたsdk-parse.tsを 2 段に割りました。core/agent-ports.tsAgentAdapter/AgentRun/AgentCapabilities/PermissionDecision= DI 境界(leaf)core/agent-events.tsAgentEventの語彙 + 全 provider 共通の畳み込みapplyAgentEventcore/claude-parse.tssdk-parse.ts。SDKMessage→AgentEvent[]の写像に専念core/claude-errors.tsAgentStopCauseの分類core/claude-adapter.tsquery()の組み立てとcanUseTool⇄requestPermissionの写像境界を
SessionHandle/AgentAdapterに引いた理由(QueryFnではない)SessionManagerから上(UI・永続化・PR 自動化・worktree・通知)は既にSessionHandle越しにしかセッションを触っておらず、もともとエージェント非依存でした。その下にアダプタを足すだけで済みます。
query()の署名(AsyncIterable<SDKUserMessage>+Options+canUseTool+control request)を共通 IF にすると、全 provider に Claude の制御モデルの模倣を強いることになります。
副次的に直ったこと
@anthropic-ai/claude-agent-sdkを import するのはclaude-*.ts(アダプタ 3 点)だけになりました。PermissionResult(SDK 型)が core に漏れていたのを自前のPermissionDecisionに置換(
PermissionRequestは元から自前だったので、ようやく対になりました)。Session.consumeがrate_limit_eventを直接読んでいた既存の規約違反(「形の知識は 1 か所」)を解消。AgentAdapter.classifyErrorへ。abortedイベントはcauseを運びます。文言の見分け方は provider ごとの知識なので状態機械には置きません。セッション途中のエージェント切替
worktree(=実際の成果物)は provider 非依存なので引き継げますが、モデル側の文脈は provider を
またげません(各 CLI が自分のトランスクリプトを持つため)。したがって切替は
「今のターンを畳む → 同じ worktree で別 provider の新セッションを開く」形になります。
SessionState.agentSessionsに provider ごとの resume id を持たせ、永続化しました。Claude → Codex → Claude と戻ると Claude 側は続きから再開します(新規会話になりません)。
LogEntry.agentでどこからが別エージェントの発言かを刻みます。切替を使っていないセッションには付かないので、既存の表示・ログの形は一切変わりません。
Session.setAgent()/getAgent()、CodivaEventのagent_switchedを追加。'claude'にフォールバックするので、既存のstate.jsonはそのまま読めます。/agentコマンドと引き継ぎプロンプトの生成は Phase B です。挙動が変わらないことの担保
claude-parse.spec.ts+__fixtures__/*.jsonl)は、分割前と同じ入口(旧
applySdkMessage→applyClaudeMessage)を叩き続けます = 分割のリグレッション網。status-reducer.spec.tsの 4 件だけで、これは分類がアダプタへ移ったことの反映です(文言 → 分類のテストは
claude-errors.spec.tsのclassifyClaudeErrorに移設・拡充)。agent-events.spec.tsを新規追加(19 件)。切替時の resume id の退避・復元、完了ゲート、rollupガード、PR 検出、通知の畳み込みをカバー。テスト
npm run lint… 211 ファイル通過npm run typecheck… 通過npm test… 2,277 件通過 / カバレッジ statements 95.07%・branches 90.42%(閾値 80 / 75)npm run build… 通過(dist/index.js+dist/main-<hash>.jsの 2 ファイル構成も維持)リファクタリングであり配線は統合テストで検証済みですが、体感確認はお願いします)
ついでに直したもの
tests/update.test.tsxの 2 件が既存の flake でした(unavailableを検証する 2 つだけawait flush()が抜けており、ダイアログが「確認中…」のまま assert していた)。兄弟テストと同じ待ち方に揃えています。
ドキュメント
docs/ARCHITECTURE.mdに「エージェント抽象」節を新設し、上記の設計判断を理由つきで記録しました。CLAUDE.mdの不変条件 2・3・4、.claude/rules/{sdk-integration,session-domain,architecture}.md、docs/PRD.md/docs/TASKS.md(Phase A ✅ / B〜D 未着手)も更新済みです。README.md はユーザー可視の挙動が変わらないため無変更です。
次の Phase(この PR には含みません)
session/request_permissionがcanUseToolに、session/cancelがinterruptに、session/loadがresumeにほぼ 1:1 で対応しますgrok agent stdio)/agentコマンド・引き継ぎプロンプトの生成