Skip to content

feat: サブエージェントの実行状況を追えるようにする - #138

Merged
takecchi merged 5 commits into
mainfrom
codiva/task-5
Sep 8, 2026
Merged

feat: サブエージェントの実行状況を追えるようにする#138
takecchi merged 5 commits into
mainfrom
codiva/task-5

Conversation

@takecchi

@takecchi takecchi commented Sep 8, 2026

Copy link
Copy Markdown
Owner

何を

エージェントがサブエージェント(Claude Code の Task ツール)を起こしたとき、いま codiva からは何も見えません。実行状況を追えて、そのサブエージェント専用のログまで潜れるようにします。

⏺ サブエージェント: general-purpose · 実行中 · Writing report.txt · Write · 12s
  • 詳細ビューのログ下段に 1 行で出ます(種別・状態・今やっていること・直近に使ったツール・経過時間)。「今やっていること」はサブエージェント側の進捗に合わせて書き換わります。
  • その行をクリックすると専用のログ画面へ入ります(Esc で戻る)。マウス無効環境では /subagents
  • 複数走っているときは「他 N 件」に畳み、クリック(または /subagents)でピッカーから選べます。
  • 内部のツール実行が親のログに混ざらなくなります。専用ログ側にまとまり、親には「何を任せたか」の 1 行だけが残ります。
  • 終わったあとも記録は残るので結果を読み返せます(1 セッション 8 本まで・transient)。

SDK は以前からこれらを流していて、codiva が task_id の集合(完了ゲート)以外を全部捨てていました。ついでに直るもの:

  • 裸の Agent だったログ行が Agent "Create report.txt file" (general-purpose) になります(summarizeToolUsetranscript.ts と共用なので復元ログの体裁も同時に改善)。
  • task_notification.status を読むようにしたので、サブエージェントの成否が区別されます(従来はどちらも同じ settle)。

どう作ったか

役割 場所
記録のリスト操作(純粋) core/subagents.ts(新規)の startSubagent / progressSubagent / settleSubagent / sealSubagents
下段 1 行の文言・当たり判定・ピッカーの選択肢(純粋) core/subagent-row.ts(新規)
内部ログ行の振り分け core/agent-events.tsrouteLogEntry
SDK メッセージの解釈 core/claude-parse.tstask_progress ハンドラ新設・task_notification の成否・parent_tool_use_id
ログの機械(両画面で共通 ui/log-pane.tsx(新規)の useLogPane + <LogPane>
専用ログ画面 / ピッカー ui/subagent-detail.tsx / ui/subagent-picker.tsx(新規)

完了ゲートには触っていません(意図的な二重管理)

実行状況と専用ログは SessionState.subagents という別フィールドで持ちます。ゲート(activeTaskIds)と統合しなかった理由が 3 つあります。

  1. 寿命が違う。ゲートは clearTurnState で必ず捨てるもの(残すと次のターンの完了を飲み込んで永久 running)。記録はターンをまたいで残すのが機能要件です。
  2. 記録は有界で決着済みも保持する(8 本)。ここから live set を導出すると、追い出しが生きたタスクをゲートから消して早すぎる完了か、status の分類ミスで永久 running のどちらかを新しい扉から作り込みます。ゲートは「素の文字列集合 + レベル信号の REPLACE による自己修復」という一番壊れにくい形のままにしました。
  3. レベル信号は表示に使えないbackground_tasks_changed が運ぶのは live background tasks で、実採取した前景の Task では一度も出ていません。これで表示を REPLACE すると走っているものが「終わった」と表示される嘘になります。

番人として **「表示用メタあり / なしでゲートの遷移(activeTaskIdsdeferredResultstatus)が完全一致する」**テーブル駆動 spec を置いています。同じ理由で task_progress はゲートを触らず、決着の分類を外しても task_settledstatus が何であれ必ず出します。

行は増やしていません(一度失敗しました)

最初はサブエージェント行を独立した 1 行にして DETAIL_CHROME_ROWS を 9 にしました。ところがそこから引き算する dialogMaxRows のせいで、24 行の端末で質問ダイアログの選択肢が窓に 1 件も入らなくなりました(既存テスト「低い端末でも Ask がログを潰さず…」が検出)。結局あれは操作ヒント行と排他だったので、同じ 1 行に畳んであります。

優先順位は 認証 → 再開 → サブエージェント → Ctrl+C → 空行Ctrl+C より優先してよいのは、あれがフォーカス横断の chord で案内が無くても効くのに対し「何が走っているか」はこの行でしか分からないからです。逆に認証・再開は行動を促す案内なので譲ります(そのときサブエージェントは走っていません)。

ログの機械を 1 実装に統一しました

会話ログを描く画面が 2 つになるので、可視域の実測・スクロール・範囲選択・URL クリック・端の自動スクロール・幅変更での選択破棄を useLogPane へ切り出しました(振る舞い不変の単独コミット。既存 3021 件のテストを 1 つも書き換えずに緑)。ここは「簡略コピー」が黙って壊れる場所(flexShrink={0} 忘れ → 行の虫食い、幅変更のリセット忘れ → 触っていない行がコピーされる)なので、片方の画面だけで再発しないようにしています。

useInput はフックに持たせず(1 画面 1 useInput は view のまま)、handleMouse が boolean を返して押下の裁定順を view に残す契約です(useComposer.handleMouse と同じ)。

#137 のマージで、ツール実行の畳み込みもここへ取り込みました。 機械(press で保留 → drag で取消 → release で開閉、アンカー固定)は pane が持ち、state(grouping / openRuns)は view のまま。おかげで専用ログ画面でも Ctrl+O / クリックの開閉が同じに動きます

その他

  • ログの予算は LogLimits として引数化し、サブエージェント 1 本は 200 件 / 12k 文字。全 8 本ぶんでも親(400k)の半分に収まることを spec で数値固定しています(素朴に配列を N 本増やすと予算が N 倍になるため)。
  • 内部ログ行の振り分けは畳み込み側。parse は parent_tool_use_id を載せるだけで状態を持たず、帰属先が引けなければ親ログへ落とすので未知の provider・未知の id でも行を捨てません。
  • 経過時間は provider が報告した usage.duration_ms を優先し、走っている間だけ useClock(ms, enabled) で毎秒描き直します(詳細ビューに無条件の 1 秒タイマーを持ち込まない)。
  • AgentCapabilities.subagents は足しましたが、表示の可否はこれで決めません/agent で切り替えたセッションでも、そのセッションが実際に走らせたサブエージェントの履歴は残るべきなので state.subagents の空判定で決めます。

テスト

  • src/core/subagents.spec.ts(新規)… start の冪等・進捗が記録を作らない・最初の決着が勝つ・上限は「決着済み最古」から落ちる・appendSubagentLog の未知 ref は undefined(呼び側が親ログへ落とす契約)。
  • src/core/agent-events.spec.tsゲート不変条件の表(メタあり/なしで完全一致)+ ログ振り分けの表(subagentRef が undefined / 既知 / 未知 × 記録あり/なし → どのセルでも行が消えない)+ seq の重複なし + 終端遷移で記録が残り印だけ封じられる。
  • src/core/claude-parse.spec.ts実データ__fixtures__/session-subagent.jsonl)で task_progress が効いている(description が "Writing report.txt" に・lastTool: 'Write')/ 決着の成否・要約・出力先・usage / 内部ツール実行が専用ログへ分離され親には混ざらない / status 5 値の表(どれでもゲートは解ける)。
  • src/core/subagent-row.spec.ts(新規)… 代表が揺れない・ラベルを必ず表示幅に切る(返す width が実測値と一致)・当たり判定は行末より右を拾わない・ピッカーの逆算が choiceRowHeights と整合。
  • src/core/log-buffer.spec.ts / src/ui/log-pane.spec.tsx(新規)… 予算の差し替え / 描画行数が高さを超えない・状態行は idle でも scrollback でも 1 行(高さ不変の番人)。
  • tests/subagent.test.tsx(新規)… 実 SDK メッセージを流して 0 件 / 1 件 / 3 件でフレームの行数が変わらない・クリックで専用画面へ(press だけ・drag あり・右ボタン・行末より右では遷移しない)・ピッカーの ↑↓ / Enter / Esc・/subagents の 3 分岐。

tests/helpers.tsdrivenManager / asMsg を集約しました(app.test.tsx のローカル定義を移動)。

ローカルで lint / typecheck / test124 files・3213 件#137 のぶんも含む)/ build の 4 点を通しています。

スコープ外(follow-up は docs/TASKS.md

  • transcript からの復元core/transcript.tsisSidechain 行を捨てており、CLI の jsonl に parent_tool_use_id / task id が載るかを実測できていません(フィクスチャにサブエージェントのターンが含まれていない)。想定で書けない箇所なので、sdk-spike で確かめてから別途。載っていなければ task_notification.output_file を読むほうが筋が良いです。復元後のセッションでは 0 件になります。
  • stream_text の帰属(サブエージェントのストリーミングが parent_tool_use_id を運ぶか未実測)/ 一覧へのサブエージェント数の表示 / 詳細ビューの表示状態をビュー切替で保つ(detailStateRef)。

手動確認のお願い

非対話セッションなので実際の TUI 操作は確認できていません。node <codiva>/dist/index.js で以下をご確認いただけると助かります(サブエージェントを起こすには「3 つのサブエージェントを並列で調査に使って」のような指示が手軽です)。

  • 下段の 1 行に種別・状態・進捗が出て、進捗に合わせて書き換わる
  • その行のクリックで専用ログ画面へ入れて、Esc で詳細へ戻る(一覧まで飛ばない)
  • 複数件で「他 N 件」に畳まれ、クリック → ピッカー → Enter で開ける
  • 専用ログ画面でも Ctrl+O / クリックの畳み込み・範囲選択・URL クリックが効く(feat: 会話ログのツール実行を畳み、ユーザーの発言に地の色を付ける #137 との組み合わせ)
  • 24 行程度の低い端末で、質問ダイアログの選択肢が窓に入る(行を増やさなかったことの確認)
  • 終わったサブエージェントの記録を後から開いて結果が読める

セッション詳細のログ機械(可視域の実測・スクロール・範囲選択・URL クリック・端の
自動スクロール)を ui/log-pane.tsx へ移す。**振る舞いは変えない**(既存テスト 3021 件は
1 つも書き換えずに緑)。サブエージェント専用のログ画面と同じ実装を共用するための前段で、
「触っていない行がコピーされる」「行が虫食いで落ちる」類の不具合が片方の画面だけで
再発しないようにする。

- 状態行(常時 1 行)は LogPane の中に含める。あれは「ログの高さを状態で変えない」ための
  予約行なので、ログ本体と離すと片方の画面で条件付きに戻されうる
- handleMouse は boolean を返し、押下の裁定順(コンポーザが先)は view に残す
  (useComposer.handleMouse と同じ契約)
- useInput は持たない(1 画面 1 useInput は view のまま)
- 既定の行 prefix はモジュール定数にする。logLines のメモ化の依存に入るので、
  描画ごとに新しい関数を渡すと全エントリが毎フレーム再展開される
SDK は `system/task_progress`(説明の動的更新・last_tool_name・usage)や
`task_notification` の status / summary / output_file を流しているのに、codiva は
task_id の集合(完了ゲート)以外を全部捨てていた。サブエージェント内部のツール実行も
`parent_tool_use_id` 付きで親ストリームに来るので、親の作業と無標識で混ざっていた。

`SessionState.subagents`(transient)に実行状況と**専用ログ**を持たせる。

**完了ゲート(activeTaskIds)には手を入れない。** ゲートは「素の id 集合 +
tasks_changed の REPLACE による自己修復」という一番壊れにくい形で、そこへ上限付き・
決着済みも保持する表示用の構造体を挟むと (a) 追い出しが生きたタスクをゲートから消す
=早すぎる完了、(b) status の分類ミスでゲートが永久に埋まる= running の張り付き、を
作り込む。寿命も違う(ゲートはターン終端で必ず捨てる / 記録はターンをまたいで残すのが
機能要件)。意図的な二重管理で、番人は agent-events.spec.ts の「表示用メタあり/なしで
activeTaskIds・deferredResult・status の遷移が完全一致する」表。

- ログ行の振り分けは fold 側(parse は parent_tool_use_id を subagentRef として載せる
  だけ)。**帰属先が引けなければ親ログへ落とす** = 未知の provider・未知の id でも
  行を捨てない(従来どおりの見た目に degrade するだけ)
- task_progress はゲートを触らない(進捗はエッジでもレベル信号でもないので、これを
  起点に積むと「進捗だけ来て決着が来ない」新しい wedge の経路になる)
- tasks_changed も表示を触らない(あれは live *background* タスクの集合で、実採取の
  前景 Task では一度も出ない。表示を REPLACE すると走っているものが終わって見える)
- 決着の成否は task_notification.status / task_updated.patch.status から。**未知の値でも
  必ず決着イベントを出す**(ゲートを解くのが最優先。分類は stopped へ丸める)
- ログ予算は LogLimits として引数化し、サブエージェント 1 本は 200 件 / 12k 文字。
  全 8 本ぶんでも親(400k)の半分に収まることを spec で数値固定
- ターン終端と CLI 再起動では記録を残して「走っている印」だけ封じる(そのプロセスの
  タスクは二度と決着を報告しないので、封じないとスピナーが永久に回る)
- summarizeToolUse に Agent / Task を追加 → 裸の `Agent` が
  `Agent "Create report.txt file" (general-purpose)` になる(transcript.ts と共用なので
  復元ログの体裁も同時に改善)
- AgentCapabilities.subagents を追加(claude のみ true)。ただし**表示の可否は
  capability で決めない** — /agent で切り替えたセッションでも、そのセッションが実際に
  走らせたサブエージェントの履歴は残るべきなので state.subagents の空判定で決める

実データ(__fixtures__/session-subagent.jsonl)の番人を claude-parse.spec.ts に追加。
セッション詳細のログ下段にサブエージェントの実行状況が 1 行で出て、クリック(または
`/subagents`)でそのサブエージェント専用のログ画面へ入れるようにする。内部のツール実行は
親のログに混ざらず、そちらへ振り分けられる(core 側は前のコミット)。

**行は増やしていない。** 最初は独立した 1 行にして DETAIL_CHROME_ROWS を 9 にしたが、
そこから引き算する dialogMaxRows のせいで 24 行の端末では質問ダイアログの選択肢が窓に
1 件も入らなくなった(tests/app.test.tsx の「低い端末でも Ask がログを潰さず…」が検出)。
結局あれは操作ヒント行と排他だったので同じ 1 行に畳んである。優先順位は
認証 → 再開 → サブエージェント → Ctrl+C → 空行。Ctrl+C より優先してよいのは、あれが
フォーカス横断の chord で案内が無くても効くのに対し「何が走っているか」はここでしか
分からないから。逆に認証・再開は行動を促す案内なので譲る(そのときサブエージェントは
走っていない)。

- クリックは **press では開かず release で開く**(drag が来たら取り消す)・**左ボタン限定**・
  **ラベルの表示幅の内側だけ**(行末より右の余白は何も無いので当たりにしない)・
  許可/質問待ちの間は遷移しない(アンマウントするとダイアログの内部 state が捨てられる)
- 複数件はピッカー(`ui/subagent-picker.tsx`)。**自分の useInput を持たない**
  presentational で、キーは詳細ビューの単一ハンドラが処理する。表示ウィンドウは親が
  1 度だけ組んで描画と当たり判定の両方へ渡す(別々に計算すると押した行とズレる)
- 専用画面(`ui/subagent-detail.tsx`)は **入力欄を持たない**(サブエージェントに指示は
  送れないので、あるように見せない)。出口は Esc 一本で、Ctrl+C は親セッションのターンを
  中断する。ログの機械は Phase 1 の `useLogPane` を共用するので範囲選択・URL クリック・
  端の自動スクロールが詳細ビューと完全に同じ
- 経過時間は provider が報告した所要時間を優先し、走っている間だけ `useClock(ms, enabled)`
  で毎秒描き直す(詳細ビューに無条件の 1 秒タイマーを持ち込まない)
- `/subagents` を追加(マウス無効環境でのキーボード経路)。別名は付けない — `agents` は
  `/agent` の前方一致に混ざり、`tasks` は TODO リストと紛らわしい

`tests/helpers.ts` に `drivenManager` / `asMsg` を集約(app.test.tsx のローカル定義を移動)。
- ARCHITECTURE.md: 完了ゲート(activeTaskIds)と表示(subagents)を**意図的に二重管理**
  している理由 3 点(寿命が違う / 有界な表示から live set を導出すると早すぎる完了か
  永久 running を作る / レベル信号は前景 Task では一度も出ないので表示に使えない)と、
  ログ行の振り分けを畳み込み側に置いた理由
- rules/sdk-integration.md: 崩してはいけない 4 点(subagents から live set を導出しない /
  tasks_changed は表示を触らない / task_progress はゲートを触らない / 決着の分類を
  外してもゲートは必ず解ける)と、capability で表示可否を決めない契約
- rules/ink-components.md: 常時 1 行の枠を増やす前に既存の 1 行と排他かを疑う
  (DETAIL_CHROME_ROWS を 1 増やして低い端末のダイアログを壊した実例)/ ログの機械は
  useLogPane に統一 / 読み取り専用画面には入力欄を置かない / 画面遷移は release で
  左ボタン・ラベル幅の内側だけ
- TECH_NOTES.md: task_started / task_progress / task_notification が実際に運ぶフィールドの
  表と、前景 Task では background_tasks_changed が出ないという実測
- CLAUDE.md: コードの地図に subagents.ts / subagent-row.ts / log-pane.tsx などを追加
- README.ja.md + README.md(対で): 「サブエージェントの実行状況を追う(/subagents)」節
- TASKS.md: follow-up(transcript 復元・stream_event の帰属・一覧への表示・detailStateRef)
main の `/tools`(会話ログのツール実行を畳む・#137)と、このブランチのサブエージェント
表示が同じ場所(session-detail.tsx のログ機械・コマンドレジストリ・ログ行の型)を触って
いたので、**両方を残す**形で解決した。

- `src/ui/session-detail.tsx`: main 版をベースに、このブランチの変更(ログ機械の
  `useLogPane` への委譲・サブエージェント行・ピッカー・`/subagents`)を載せ直した。
  11 箇所の衝突マーカーを手で繕うより、main の畳み込み配線を壊さない確実な経路を選んだ
- `src/ui/log-pane.tsx`: **ツール実行の畳み込みを共通実装へ取り込んだ**(`collapse` /
  `onToggleGroup` / `beforeToggle`)。畳み込みの機械(press で保留 → drag で取消 →
  release で開閉、アンカーの固定)は pane が持ち、state(`grouping` / `openRuns`)は
  view が持つ。おかげでサブエージェント専用ログでも `Ctrl+O` / クリックの開閉が同じに動く
- `src/core/agent-events.ts`: `routeLogEntry`(サブエージェントへの振り分け)に main の
  `LogEntry.tool`(畳み込みの内訳)を載せる。`AgentToolKind` は main で `types.ts` へ
  移ったので、そこから引く
- `src/core/commands.ts`: `/tools` と `/subagents` の両方を登録(どちらも詳細ビュー専用)
- `src/app.tsx`: `collapseTools` と `onOpenSubagent` の両方を `SessionDetail` へ渡す
- README(ja/en)・CLAUDE.md: 両方の節を残し、コードの地図では畳み込みの当たり判定が
  `ui/log-pane.tsx` に移ったことを反映

lint / typecheck / test(124 files・3213 tests)/ build を通してある。
@takecchi
takecchi marked this pull request as ready for review September 8, 2026 02:20
@takecchi takecchi changed the title codiva/task 5 feat: サブエージェントの実行状況を追えるようにする Sep 8, 2026
@takecchi
takecchi merged commit 7ea0916 into main Sep 8, 2026
1 check passed
@takecchi
takecchi deleted the codiva/task-5 branch September 8, 2026 02:35
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.

1 participant