docs(tickets): セッション内 Task ツールを本体と注意機構に分割

This commit is contained in:
Keisuke Hirata 2026-05-03 19:03:48 +09:00
parent 0c10150b02
commit 28fe1dae1c
No known key found for this signature in database
3 changed files with 136 additions and 43 deletions

View File

@ -15,6 +15,8 @@
- Manifest: Tool Output / File Upload 上限の分離とデフォルト緩和 → [tickets/manifest-output-upload-limits.md](tickets/manifest-output-upload-limits.md)
- メモリ機構
- 使用頻度メトリクス + Knowledge 化候補レポート → [tickets/memory-usage-metrics.md](tickets/memory-usage-metrics.md)
- セッション内 TODO ツール(注意機構付き) → [tickets/session-todo.md](tickets/session-todo.md)
- セッション内 Task ツール
- ツール本体TaskCreate / TaskUpdate / TaskList / TaskGet + 永続化 / compact 跨ぎ) → [tickets/session-todo.md](tickets/session-todo.md)
- 注意機構(無アクティビティで `<system-reminder>` ナッジ) → [tickets/session-todo-reminder.md](tickets/session-todo-reminder.md)
- ワークスペースのメモリーをLintするヘッドレスCLI
- system-reminder 注入機構の汎用化2件目の利用者が出た時に検討。タグ形式と「履歴を汚さない」原則は session-todo で先行確立)

View File

@ -0,0 +1,60 @@
# セッション内 Task ツールの注意機構
## 背景
`tickets/session-todo.md` で導入する Task ツール群があっても、LLM はそれを使わずに作業を続け得る。ツールを呼ばないまま会話が長引くと、
- 開始した作業の `inprogress` がずっと放置されたままになる
- 「やったつもり」になって `completed` への更新を忘れる
- そもそも TaskStore の存在を忘れて、構造化を諦めて自由記述に回帰する
OpenCode の todo は専用の注意機構を持たない(汎用 reminder 経由)。一方 Claude Code は `task_reminder` を「N リクエスト無アクティビティで初めて発火するナッジ型」として実装しており、毎リクエスト押し戻しはしない(`/home/hare/.local/share/claude/versions/2.x` の `du_` / `cu_` 関数、閾値 `Z_8 = { TURNS_SINCE_WRITE: 10, TURNS_BETWEEN_REMINDERS: 10 }`)。
Insomnia でも同方針を採り、active Task が残っているのに `TaskCreate` / `TaskUpdate` が一定リクエスト呼ばれていない場合に限り、`<system-reminder>` で揮発的に思い出させる。「やったつもり」抑止と、トークン浪費・LLM の自律性侵害のバランスを取るため、毎リクエスト押し戻しはしない。
## 前提
- `tickets/session-todo.md` の TaskStore と `TaskCreate` / `TaskUpdate` / `TaskList` / `TaskGet` ツールが利用可能であること
- `pre_llm_request` 相当のフックを Pod が持つこと無ければ本ticket で導入)
## 方針
- **`pre_llm_request` Interceptor として実装**。直近の user message に `<system-reminder>` ブロックを揮発的に append するだけ。履歴・ログには載せない
- **system-reminder 注入の汎用化はやらない**。利用者が Task 1機構しかない段階で抽象を立てないCLAUDE.md「概念の追加は不在が問題になってから」。ただし「タグ形式は `<system-reminder>...</system-reminder>` で揃える」「履歴は汚さない」の2点は本実装で確立し、将来の追加機構が同じ規約に乗れるようにする
- **発火はナッジ型**。N リクエスト無アクティビティで初めて発火し、cooldown も持つ
## 要件
### Interceptor
- `pre_llm_request``Vec<Item>` を受け取り、以下の AND を満たした場合のみ発動
- active Task`pending` または `inprogress`が1件以上存在する
- 直近 N リクエスト (暫定 N=8) `TaskCreate` / `TaskUpdate` のいずれも呼ばれていない
- 前回 reminder 注入から M リクエスト (暫定 M=8) 以上経過
- ここで言う「リクエスト」は **LLM への1回の推論呼び出し (= assistant 応答1回)** の単位で数える。ユーザー発火単位ではない。1ユーザー発火内で tool ループが回れば、tool_result を受けて発火する次のリクエストもそれぞれ1としてカウントする
- カウント対象はメインスレッドの assistant 応答に限る。サブエージェント / sidechain の assistant 応答は除外する
- カウンタは Pod 側の session-lifetime 状態として保持する(`requests_since_last_task_management` / `requests_since_last_reminder`。resume 時は履歴の逆走査で再計算するか 0 リセットで再開する。どちらでも「初回ナッジが最大 N リクエスト遅れる」だけで挙動として致命ではない
- 発動時、直近の user message の contentまたは content[最終 text part])の末尾に `<system-reminder>` ブロックを append し、現在の active Task リストを `taskid` / `status` / `subject` を含む簡潔な形式で列挙する。`description` は長大化を避けるため省略してよい
- 履歴 (`Worker` の保持する `Vec<Item>`) は変更しない。リクエスト送信時の Vec のみ加工する
- active Task が空の場合は何も差し込まない(忘却防止が目的なので、思い出させる対象が無いなら不要)
## 完了条件
- 直近 N リクエスト連続で `TaskCreate` / `TaskUpdate` が呼ばれず、かつ active Task が残っている場合に限り、`pre_llm_request` で `<system-reminder>` が直近 user message に append される
- `TaskCreate` / `TaskUpdate` のいずれかが呼ばれるとカウンタがリセットされ、再び N リクエスト経過するまでは reminder が出ない
- reminder が一度出たあとは、cooldown M リクエストが経過するまで再注入されない
- active Task が0件の場合は reminder が出ない
- system-reminder の注入は揮発的で、`get_history` / セッションログには現れない
- 単体テストで Interceptor の発火条件リクエスト回数閾値、active 0件、cooldown、サブエージェント除外がカバーされる
## 範囲外
- inprogress 滞留検出 / 多重 inprogress 検出など、状態異常ベースの追加トリガ(必要になれば別チケットで追加)
- system-reminder 注入機構の汎用化(`TODO.md` に立項済み、別途検討)
- `TaskCreate` / `TaskUpdate` の戻り値に active Task 全件を埋め込む強化(必要に応じて Tool ticket 側で対応)
## 参照
- 設計指針: `CLAUDE.md`(最小の構造化 / 概念の追加は不在が問題になってから)
- 前提: `tickets/session-todo.md`Tool 群と TaskStore
- 参考実装: Claude Code の `task_reminder``du_` / `cu_` 関数、閾値 `Z_8 = { TURNS_SINCE_WRITE: 10, TURNS_BETWEEN_REMINDERS: 10 }`

View File

@ -1,74 +1,105 @@
# セッション内 TODO ツール
# セッション内 Task ツール
## 背景
長めのタスクを LLM に進めさせる際、Claude Code / OpenCode が備える「セッション内 TODO リスト」相当の機構が無いため、エージェントが自分の作業計画を構造化された形で保持・更新できない。Reasoning や text 出力の中で擬似的に TODO を書くことはできるが、
長めのタスクを LLM に進めさせる際、Claude Code / OpenCode が備える「セッション内 TODO / Task リスト」相当の機構が無いため、エージェントが自分の作業計画を構造化された形で保持・更新できない。Reasoning や text 出力の中で擬似的に TODO を書くことはできるが、
- ターンを跨いだとき直近の TODO 状態が context から押し出される
- compact を跨ぐと完全に消える
- ツール結果ではないため、状態の上書き・部分更新の規約が決まらず、意図と乖離した「やったつもり」を引き起こす
- ツール結果ではないため、状態の作成・更新・削除の規約が決まらず、意図と乖離した「やったつもり」を起こしやすい
- 安定した識別子が無いため、後から特定 entry を更新できない
この用途のために、セッション内に正規化された TODO リストを保持し、ターンごとに LLM へ最新状態を再提示注意機構し、compact を跨いで保存される専用ツールを導入する。
この用途のために、セッション内に正規化された Task リストを保持し、compact を跨いで保存される専用ツールを導入する。
「LLM がツールを使わなくなった場合のサボり防止」を目的とした注意機構(`<system-reminder>` の自動注入)は本チケットの範囲外。`tickets/session-todo-reminder.md` で別途扱う。
## 方針
- **保存先は `tools` 層の session-lifetime 状態**。`Tracker` と同じ生存スコープで `Pod` が所有。`Arc<Mutex<Vec<TodoItem>>>` ベースの `TodoStore` を tool に注入する
- **永続化は専用レーンを持たない**。`tool_call.arguments` がセッションログに既に乗っているため、resume 時には履歴 replay の中で最後の `todo_write` 引数を `TodoStore` に再適用すれば状態が復元される
- **注意機構は `pre_llm_request` Interceptor**。直近の user message に `<system-reminder>` ブロックを揮発的に append するだけ。履歴・ログには載せない
- **system-reminder 注入の汎用化はやらない**。利用者が TODO 1個しかない段階で抽象を立てないCLAUDE.md「概念の追加は不在が問題になってから」。ただし「タグ形式は `<system-reminder>...</system-reminder>` で揃える」「履歴は汚さない」の2点は本実装で確立し、将来の追加機構が同じ規約に乗れるようにする
- **保存先は `tools` 層の session-lifetime 状態**。`Tracker` と同じ生存スコープで `Pod` が所有。`Arc<Mutex<TaskStore>>` ベースの store を tool に注入する
- **Task は差分操作**。全置換ではなく `TaskCreate` / `TaskUpdate` によって TaskStore を逐次更新する
- **永続化は専用レーンを持たない**。`tool_call.arguments` がセッションログに既に乗っているため、resume 時には履歴 replay の中で `TaskCreate` / `TaskUpdate` を順に再適用すれば状態が復元される
## Task データモデル
各 Task entry は以下の4フィールドを持つ。
- `taskid`: store が自動採番する整数 ID。`TaskCreate` ごとに単調増加する
- `status`: `pending | inprogress | completed | deleted`
- `subject`: 1行程度の短い件名
- `description`: 詳細説明
状態遷移の通常ルートは `pending -> inprogress -> completed` とする。完了ではなく一覧から取り下げたい場合は、物理削除ではなく `TaskUpdate``status = deleted` を設定する。
## 要件
### `todo_write` ツール
### `TaskCreate` ツール
- 入力は TODO リスト全体(全置換)。差分更新は受けない
- 各エントリは `id` / `content` / `status (pending | in_progress | completed)` の 3 フィールド
- `id` は LLM 側が一貫して採番できる文字列。同 id があれば置換、なければ新規。順序は配列順を信頼
- 戻り値は更新後のスナップショットを summary に含める(次ターンで再確認可能)
- 読み出し専用ツール(`todo_read`)は作らない。注意機構と tool result snapshot で代替
- 入力は `subject` / `description` のみ
- TaskStore の末尾に新規 entry を push する
- `taskid` は TaskStore が自動で割り当てる。LLM は指定できない
- 初期 `status``pending`
- 戻り値は作成された Task と、更新後の TaskStore スナップショット概要を summary に含める
### `TaskList` ツール
- 入力なし
- 現在の TaskStore スナップショットを返す
- `taskid` / `status` / `subject` / `description` を含める
- `completed` / `deleted` も含め、store に残っている Task をすべて列挙する
### `TaskGet` ツール
- 入力は `taskid`
- 指定された Task を1件返す
- 存在しない `taskid` はエラーにする
### `TaskUpdate` ツール
- 入力は `taskid` と、更新する `status` / `subject` / `description` のいずれか
- 少なくとも1フィールドは更新指定が必要
- `taskid` は変更できない
- `status``pending | inprogress | completed | deleted` のいずれか
- 存在しない `taskid` はエラーにする
- 削除は `status = deleted` への更新として表現し、entry は store から取り除かない
- 戻り値は更新された Task と、更新後の TaskStore スナップショット概要を summary に含める
### Resume 時の復元
- `Pod::resume` の履歴 replay 中に `todo_write``tool_call.arguments` を観測したら、`TodoStore` を引数値で上書き
- `Pod::resume` / restore の履歴 replay 中に `TaskCreate` / `TaskUpdate``tool_call.arguments` を観測したら、TaskStore に順に再適用する
- `TaskCreate` は replay 時も自動採番を行う。ログ上の call 順序が保持されるため、通常の replay では元の `taskid` が再現される
- `TaskUpdate` は replay 済みの TaskStore に対して適用する
- 専用 LogEntry / Persistence 型は追加しない(`Tracker` と同じ方針)
- `tool_call.arguments` のフォーマットが `todo_write` の引数 schema と乖離した場合(旧バージョンのログ)は、その call を無視してよい
- `tool_call.arguments` のフォーマットが現在の schema と乖離した場合(旧バージョンのログ)は、その call を無視してよい
### Compact 跨ぎ
- compact 起動時、Pod は現在の `TodoStore` スナップショットを compact worker context に渡す
- compact worker は summary を書く際、未完了 TODO を summary 文に取り込める情報源として参照する(強制ではない)
- compact 後の新セッション開始時、Pod は **`mark_read_required` と同じ system message 注入レーン**に「未完了 TODO スナップショット」を 1 メッセージとして注入する
- 新セッションは空の `TodoStore` で始まる。次に LLM が `todo_write` を呼び出した時点で再構築されるsystem message に書かれたスナップショットがその拠り所)
- compact worker に TODO 編集権限は与えない(消去・縮約はしない)
### 注意機構Interceptor
- `pre_llm_request``Vec<Item>` を受け取り、未完了 TODO`pending` または `in_progress`)が 1 件でも存在する場合に発動
- 直近の user message の contentまたは content[最終 text part])の末尾に `<system-reminder>` ブロックを append
- ブロック内には現在の TODO リストを、status を含む簡潔な形式で列挙
- 履歴 (`Worker` の保持する `Vec<Item>`) は変更しない。リクエスト送信時の Vec のみ加工
- TODO が空の場合は何も差し込まない
- compact 起動時、Pod は現在の TaskStore **全スナップショット**を compact worker context に渡す
- compact worker は summary を書く際、TaskStore を参照できる。未完了 Task を summary 文に取り込める情報源として参照する(強制ではない)
- compact worker に Task 編集権限は与えない(作成・更新・削除はしない)
- compact 後の新セッションでも TaskStore は空にせず、compact 前の TaskStore **全件**を維持する。`completed` / `deleted` も含めて保持する
- compact 後の新セッション開始時、Pod は **`mark_read_required` と同じ system message 注入レーン**に TaskStore 全スナップショットを1メッセージとして注入する
- compact 完了後、Pod は新セッションの継続前に必ず `TaskList` を発火させ、LLM が compact 後の TaskStore 状態を tool result として再確認できるようにする
## 完了条件
- `todo_write` ツールが builtin tool として登録され、Pod で利用できる
- LLM が `todo_write` を呼ぶと TodoStore が更新され、その後の `pre_llm_request` で system-reminder として LLM に再提示される
- セッションを resume すると、最後の `todo_write` の状態から再開される
- compact を跨いでも、未完了 TODO が新セッション冒頭の system message として残る
- system-reminder の注入は揮発的で、`get_history` / セッションログには現れない
- 単体テストで `todo_write` の更新挙動 / replay 復元 / Interceptor の差し込みがカバーされる
- `TaskCreate` / `TaskList` / `TaskGet` / `TaskUpdate` が builtin tool として登録され、Pod で利用できる
- LLM が `TaskCreate` を呼ぶと TaskStore に pending Task が末尾追加され、自動採番された `taskid` が返る
- LLM が `TaskUpdate` を呼ぶと既存 Task の `status` / `subject` / `description` が更新される
- `TaskList` / `TaskGet` で TaskStore の状態を確認できる
- セッションを resume すると、履歴中の `TaskCreate` / `TaskUpdate` replay により TaskStore が復元される
- compact を跨いでも、TaskStore 全件が維持され、新セッション冒頭の system message と `TaskList` 発火により再提示される
- 単体テストで TaskCreate / TaskUpdate / TaskList / TaskGet の挙動、replay 復元がカバーされる
## 範囲外
- 差分更新 APIadd / remove / patch。全置換のみで十分
- TODO 階層・優先度・タグ
- TUI / GUI での TODO 状態の可視化(ツール呼び出しのイベントは既に流れているので、クライアント側で表示するかは別軸)
- system-reminder 注入機構の汎用化(`TODO.md` に立項済み、別途検討)
- TODO の永続化を専用 LogEntry に分離する設計(現方針は tool_call replay で復元、追加レーン不要)
- 複数 Pod 間で TODO を共有する仕組み
- 注意機構(`<system-reminder>` の自動注入による「サボり防止」)→ `tickets/session-todo-reminder.md`
- Task の階層・優先度・タグ
- TUI / GUI での Task 状態の可視化(ツール呼び出しのイベントは既に流れているので、クライアント側で表示するかは別軸)
- Task の永続化を専用 LogEntry に分離する設計(現方針は tool_call replay と compact 時 snapshot 維持で復元、追加レーン不要)
- 複数 Pod 間で Task を共有する仕組み
## 参照
- 設計指針: `CLAUDE.md`(最小の構造化 / 概念の追加は不在が問題になってから)
- 参考実装: Claude Code の TodoWrite、OpenCode の todo tool
- 関連: `crates/tools/src/tracker.rs`session-lifetime 状態の前例)、`crates/pod/src/compact/worker.rs`auto-injection レーン)
- 後続: `tickets/session-todo-reminder.md`(注意機構)