Skip to content

feat: エージェントとして Grok (xAI) に対応する - #124

Merged
takecchi merged 5 commits into
mainfrom
codiva/https-github-com-takecchi-codiva-issues-7
Aug 12, 2026
Merged

feat: エージェントとして Grok (xAI) に対応する#124
takecchi merged 5 commits into
mainfrom
codiva/https-github-com-takecchi-codiva-issues-7

Conversation

@takecchi

@takecchi takecchi commented Aug 12, 2026

Copy link
Copy Markdown
Owner

closes #107

grok CLI(xAI の Grok Build 1.0.0)を 3 つ目の provider として追加します。一覧・詳細の /agent で選べ、/model で Grok 自身のモデル一覧から選択でき、セッション一覧には実際に動いているモデル名が出ます。許可・質問ダイアログも効きます(Codex では諦めていた機能)。

なぜ ACP(JSON-RPC over stdio)なのか

grok の非対話モードは 2 つあります。

モード 許可・質問
grok -p … --output-format streaming-json 1 ターン 1 プロセス、NDJSON を一方向に吐く 上げられない
grok agent stdio JSON-RPC 2.0(ACP)の双方向ストリーム session/request_permission / _x.ai/ask_user_question が飛んでくる

Codex に倣うなら前者ですが、それだと Codex と同じく permissions: false に落ちます。後者ならエージェント側からの要求を codiva のダイアログにそのまま繋げるので、ACP を選びました。

構成(Claude / Codex と同じ 3 点セット + 型と I/O)

ファイル 役割
core/grok-events.ts ACP メッセージの型と受理ガード。通知が session/update_x.ai/session_notification2 本のレールで来るのをここで吸収
core/grok-parse.ts 通知 → AgentEvent本文の区切りが通知に無いので、ここだけは状態(溜めた本文・思考)を持ち、ツール呼び出しとターン終了で確定させる
core/grok-errors.ts retry_state.error_type(CLI 自身の分類)を優先し、api だけ文言判定へ落とす(401 を素の failed に格下げしないため)
core/grok-adapter.ts 1 セッション 1 プロセス + 要求 id の対応表。許可・質問・中断・resume・モデル切替
core/grok-models.ts / utils/grok.ts カタログは initialize_meta.modelState から(grok models は人間向けテキストのみ)
  • createJsonlSplitter は provider 非依存なので core/jsonl.ts へ切り出して Codex と共用(振る舞いは不変)。
  • モデルカタログの受け渡しは modelsByAgent の表に一般化し、ビュー側の agent === 'codex' ? … を廃止しました(4 つ目の provider は 1 行で足せます)。
  • 新しい設定項目はありません。agent: "grok" を受理し、自動選択の順に grok を足しただけです。

実測で分かった要点(詳細は docs/TECH_NOTES.md)

  • session/cancel通知id を付けると -32601 Method not found になる
  • エージェントからの要求には必ず答える(放置するとターンが永久に終わらない)ので、知らない要求にも -32601 を返す
  • 継続は session/load ではなく session/resume(load は過去の会話を全部流し直す)
  • systemPrompt は session/new_meta.rulessystemPromptOverride は Grok 自身の system prompt を潰す)
  • 解決済みモデルは provider が自分で報告するので、Codex のような rollout 探索は不要
  • grok models は未認証でも終了コード 0 なので、ログイン判定には使えない(~/.grok/auth.json / XAI_API_KEY の有無で見る)
  • authenticate を勝手に呼ぶと応答が返らないままブラウザ OAuth を待ち続けるので、ログインは必ずユーザー操作(/login = grok login --device-auth)から

capability

permissions interrupt setModel resume modelCatalog usage cost transcript
Claude
Codex
Grok

テスト

  • フィクスチャは実バイナリから採取しました(src/core/__fixtures__/grok-*.jsonl)。実 grok 1.0.0 を、モデル側だけローカルのモック Responses API に差し替えて走らせた実出力です(ACP のフレーミング・ツール実行・許可要求・質問要求はすべて本物)。採取手順は docs/TECH_NOTES.md に残してあります。
  • 新規 174 テスト(events / parse / errors / models / adapter / utils)。全体で 2905 passed。
  • 実バイナリ + 実アダプタコードの E2E も通しました: 「思考 → 許可ダイアログ → ツール実行 → 本文ストリーム → 完了」まで到達し、セッション状態は completed、モデル欄は grok-4.5、resume id も取れています。

test plan

  • npm run lint(この worktree の node_modules に biome のネイティブバイナリが無いため、同バージョンを別の場所へ展開して実行)
  • npm run typecheck
  • npm test(2905 passed / カバレッジ閾値も維持)
  • npm run build
  • モックバックエンド + 実 grok 1.0.0 での E2E(許可ダイアログ・ツール実行・完了・モデル表示)
  • 実アカウント(SuperGrok / X Premium+ もしくは XAI_API_KEY)での手動確認 — 認証が要るためエージェントセッションでは実行できません。/agent で Grok を選び、1 ターン流して「モデル名が一覧に出る」「許可ダイアログが出る」「Ctrl+C で中断できる」「再開できる」あたりをご確認ください

行区切り JSON は Codex の `codex exec --json` だけのものではない(次に足す Grok の
ACP も JSON-RPC を 1 行 1 通で流す)。framing 自体は provider 固有の知識を持たない
純粋な関数なので、codex-events.ts から中立の core/jsonl.ts へ移す。振る舞いは不変。
closes #107

`grok` CLI(Grok Build 1.0.0)を 3 つ目の provider として追加する。一覧・詳細の
`/agent` で選べ、`/model` で Grok 自身のモデル一覧から選択でき、セッション一覧には
実際に動いているモデル名が出る。

**ACP(JSON-RPC over stdio)を選んだ理由**: `grok` の非対話モードは 2 つあり、
headless(`-p --output-format streaming-json`)は一方向で許可・質問を上げられない。
`grok agent stdio` は双方向なので、`session/request_permission` と
`_x.ai/ask_user_question` をそのまま codiva の許可・質問ダイアログへ繋げる
(`permissions: true`。Codex では諦めていた機能)。

構成は Claude / Codex と同じ 3 点セット + 型と I/O:
- `core/grok-events.ts` … ACP メッセージの型と受理ガード(通知が `session/update` と
  `_x.ai/session_notification` の 2 本のレールで来ることを吸収)
- `core/grok-parse.ts` … 通知 → `AgentEvent`。本文の区切りが通知に無いので、
  ここだけは状態(溜めた本文・思考)を持ち、ツール呼び出しとターン終了で確定させる
- `core/grok-errors.ts` … `retry_state.error_type`(CLI 自身の分類)を優先し、
  `api` だけは文言判定へ落とす(401 を素の failed に格下げしないため)
- `core/grok-adapter.ts` … 1 セッション 1 プロセス + 要求 id の対応表
- `core/grok-models.ts` / `utils/grok.ts` … カタログは `initialize` の
  `_meta.modelState` から取る(`grok models` は人間向けテキストのみ)

実測で分かった要点(詳細は docs/TECH_NOTES.md):
- `session/cancel` は**通知**。id を付けると `-32601` になる
- エージェントからの要求は**必ず答える**(放置するとターンが永久に止まる)ので、
  知らない要求にも `-32601` を返す
- 継続は `session/load` ではなく `session/resume`(load は会話を全部流し直す)
- systemPrompt は `session/new` の `_meta.rules`(`systemPromptOverride` は Grok 自身の
  system prompt を潰すので使わない)
- 解決済みモデルは provider が自分で報告するので、Codex のような rollout 探索は不要

テストは実 `grok` 1.0.0 の通信を採取したフィクスチャ(`__fixtures__/grok-*.jsonl`)で
駆動する。モデルカタログの受け渡しは `modelsByAgent` の表に一般化し、ビュー側の
provider ごとの分岐をやめた。
- README: 対応エージェント・動作要件・`/agent` の手順・`agent` 設定値・Grok
  セッションの制約(許可と質問は届く / コストと使用状況は出ない / ログ復元なし)
- TECH_NOTES: `grok agent stdio`(ACP)の実測。メソッド表・2 本の通知レール・
  要求への応答形・error_type による分類・モデルの取得元・モックバックエンドを
  使ったフィクスチャ採取の手順
- ARCHITECTURE: core/ のファイル一覧、parse→fold 図、capability 表、
  「1 セッション 1 プロセス + 対応表」にした理由
- CLAUDE.md / rules: 地図と不変条件に Grok の 3 点セットと core/jsonl.ts を追加。
  「要求には必ず答える」「要求と通知を取り違えない」を規約に足す
- TASKS: Phase C を完了にして実績メモを残す
@takecchi
takecchi marked this pull request as ready for review August 12, 2026 08:37
コードレビューで見つかった、セッションが嘘の状態に落ちる 4 件:

- **中断が完了に化ける**。`Ctrl+C` と `end_turn` が競ると `turn_completed` を出して
  しまい、`interrupted` が `completed` に化けて auto-PR まで走っていた。終端イベントの
  手前で `interrupted` / abort を見るようにする(Codex 側と同じ守り)。
- **セッション確立前の `Ctrl+C` が無視される**。`session/new` の最中は `sessionId` が
  無いので `session/cancel` を送れず、UI は「中断した」と言うのにエージェントは走り
  切っていた。確立直後に送り直す予約を持つ。
- **プロセスが黙って死ぬと `failed`(再開不可)になる**。合成文言(`grok agent exited`)
  では分類が効かないため。`GrokProcess.result()` の stderr と終了コードから理由を
  組み立て、それでも読み取れないものは `connection`(再開可能)へ倒す。
- **`retry_state.error_type` が分類に使われていなかった**(docs は使うと書いていた)。
  CLI 自身の分類を覚えて `grokStopCause` に渡す。文言では読めないレート制限が
  `failed` にならなくなる。

ほか、未処理 rejection で TUI を落としうる箇所を塞ぐ:
`.finally()` の後ろへ catch を移す、許可・質問の応答が例外で飛んだら `cancelled` で
必ず閉じる(放置するとターンが永久に終わらない)、`drive()` の失敗を**キューを
閉じる前に**積む(閉じたあとの push は黙って捨てられていた)。`errorText` は
`message` と `data` の両方を残す(`Internal error` だけでは分類も説明もできない)。
@takecchi takecchi changed the title codiva/https github com takecchi codiva issues 7 feat: エージェントとして Grok (xAI) に対応する Aug 12, 2026

@takecchi takecchi left a comment

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

1件、修正が必要な問題があります。

[High] session/new 中の Ctrl+C は、cancelを送ったあとに中断対象のpromptを新規開始してしまいます。

interrupt()sessionId がまだ無いと cancelPending = true にし、start()session/new の応答後に flushPendingCancel()session/cancel を送ります。しかし start() が戻ると、同じ drive() は無条件で runTurn(text) を呼び、直後に session/prompt を送ります。つまり実際の順序は次のとおりです。

  1. session/new を送信
  2. ユーザーがCtrl+C(UIは interrupted
  3. session/new 応答を受信
  4. session/cancel 通知を送信(この時点ではprompt/ターンはまだ存在しない)
  5. session/prompt を送信して、中断したはずの作業を開始

cancelが未来のpromptへ予約される保証はなく、通常は「現在走っているターン」を止める通知なので、4は空振りし、5の作業がそのままworktreeを変更し得ます。interruptedフラグにより後の完了イベントだけは抑止されるため、UIは中断済みのまま、裏でエージェントだけが動くさらに危険な状態になります。

既存テストはcancel通知が送られたことまでしか見ていません。PR headの同テストへ、数tick後も session/prompt が送られていないことを追加で期待すると失敗し、実際にid 3の session/prompt が記録されることを確認しました。

対処としては、確立前に中断されたターンではセッション確立だけ済ませて元の text を捨て、runTurn()へ進まないのが自然です(またはprompt送信後にcancelし、cancelの対象となるターンが確実に存在する順序にする必要があります)。少なくとも次を固定する回帰テストが必要です。

  • session/new待ち中にinterrupt
  • new応答後も中断対象の session/prompt を送らない
  • 次に新しい指示を送ったときだけ、その新しいtextでpromptが始まる

確認結果:

  • 追加回帰テスト以外のGrok関連185テストは成功
  • npm run typecheck: 成功
  • GitHub CI check: 成功

レビュー指摘。`session/new` の応答待ちで `Ctrl+C` を押すと、cancel を送ったあとに
**その中断したはずの `session/prompt` をそのまま投げていた**。

`session/cancel` が止められるのは「今走っているターン」だけなので、prompt を出す前に
送っても空振りする。しかも完了イベントは `interrupted` フラグで抑止されるため、
UI は「中断済み」のまま**エージェントだけが worktree を書き換え続ける**(誰も
気付けない)という、握り潰しより悪い状態になっていた。

前の修正で入れた「確立後に cancel を送り直す」予約をやめ、`runTurn` の先頭で
中断・abort を見て**そのターンを始めない**(指示ごと捨てる。やり直しはユーザーが
改めて送る)。同じ穴は resume 経路(`sessionId` は既にあるがターンは未開始)にも
あったので、両方を回帰テストで固定する。`interrupt()` も、止める対象が無いときは
空振りの通知を送らないようにした。

1 プロセスが複数ターンを跨ぐ provider では「プロセスを殺せば止まる」(Codex)が
成り立たない、という一般的な教訓なので docs/ARCHITECTURE.md と
.claude/rules/sdk-integration.md にも残す。
@takecchi

Copy link
Copy Markdown
Owner Author

レビューありがとうございます。ご指摘のとおりで、修正しました(2b1aae7)。

何が起きていたか

おっしゃる順序 4→5 が完全に正しく、session/cancel は「今走っているターン」向けの通知なので prompt を出すに送っても空振りし、その直後の session/prompt がそのまま走っていました。しかも完了イベントだけは interrupted フラグで抑止されるので、UI は中断済みのままエージェントだけが worktree を書き換え続けるという、握り潰しより悪い状態でした。前回の「確立後に cancel を送り直す」予約は、対象が存在しない通知を送っていただけで意味がありませんでした。

修正

予約の仕掛け(cancelPending / flushPendingCancel)を撤去し、runTurn の先頭で中断・abort を見てそのターンを始めない(指示ごと捨てる)ようにしました。やり直しはユーザーが改めて送る、という Codex 側(プロセスを殺すので同じ結果になる)と揃った挙動です。interrupt() も、走っているターンが無いときは空振りの通知を送りません。

同じ穴が resume 経路にもありました(sessionId は既にあるのでご指摘の 4 は成立するが、やはり止める対象のターンが無い)。こちらも同時に塞いでいます。

回帰テスト(ご提示の 3 点をそのまま固定)

src/core/grok-adapter.spec.ts に 2 本追加しました。

  • 「セッション確立中の Ctrl+C はそのターンを始めない(裏で走り続けさせない)」
    • session/new 待ちで interrupt → cancel を送らないこと
    • new 応答後に 20 tick 回しても session/prompt が送られていないこと / turn_completed も出ないこと
    • 次に別の指示を送ったときだけ、その新しい text で prompt が始まること
  • 「resume の最中に Ctrl+C を押しても同じ(prompt を出さない)」

「長寿命プロセスの provider では『プロセスを殺せば止まる』が成り立たないので、中断は指示を捨てることで表現する」という教訓は、次の provider でも踏みそうなので docs/ARCHITECTURE.md.claude/rules/sdk-integration.md にも残しました。

確認: lint / typecheck / test(2906 passed)/ build いずれも成功しています。

@takecchi takecchi left a comment

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

再レビューしました。前回指摘した、session/new / session/resume の処理中に Ctrl+C で中断しても、その後 session/prompt が送信されてしまう問題は解消されています。

runTurn() が起動処理後の中断状態を確認してプロンプト送信を打ち切るようになり、実際にプロンプトが送られないことと、その次の入力では正常にターンを開始できることも回帰テストで確認されています。

追加の指摘事項はありません。LGTM です。

確認結果:

  • 全テスト成功: 115 test files / 2,906 tests
  • npm run typecheck 成功
  • npm run build 成功
  • GitHub CI 成功

補足: ローカルの npm run lint は共有 node_modules に macOS ARM64 用 Biome バイナリがなく起動できなかったため、lint のローカル再実行のみ未確認です。

@takecchi
takecchi merged commit ca3b6ec into main Aug 12, 2026
1 check passed
@takecchi
takecchi deleted the codiva/https-github-com-takecchi-codiva-issues-7 branch August 12, 2026 09:39
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

エージェントとして Grok (xAI) への対応を追加

1 participant