From a87be4cbc2ea056b058d258adb8d5542cfca1081 Mon Sep 17 00:00:00 2001 From: Hare Date: Sun, 3 May 2026 19:03:48 +0900 Subject: [PATCH] =?UTF-8?q?docs(tickets):=20=E3=82=BB=E3=83=83=E3=82=B7?= =?UTF-8?q?=E3=83=A7=E3=83=B3=E5=86=85=20Task=20=E3=83=84=E3=83=BC?= =?UTF-8?q?=E3=83=AB=E3=82=92=E6=9C=AC=E4=BD=93=E3=81=A8=E6=B3=A8=E6=84=8F?= =?UTF-8?q?=E6=A9=9F=E6=A7=8B=E3=81=AB=E5=88=86=E5=89=B2?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- TODO.md | 4 +- tickets/session-todo-reminder.md | 60 ++++++++++++++++ tickets/session-todo.md | 115 ++++++++++++++++++++----------- 3 files changed, 136 insertions(+), 43 deletions(-) create mode 100644 tickets/session-todo-reminder.md diff --git a/TODO.md b/TODO.md index 13b0d465..e7e49967 100644 --- a/TODO.md +++ b/TODO.md @@ -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) + - 注意機構(無アクティビティで `` ナッジ) → [tickets/session-todo-reminder.md](tickets/session-todo-reminder.md) - ワークスペースのメモリーをLintするヘッドレスCLI - system-reminder 注入機構の汎用化(2件目の利用者が出た時に検討。タグ形式と「履歴を汚さない」原則は session-todo で先行確立) diff --git a/tickets/session-todo-reminder.md b/tickets/session-todo-reminder.md new file mode 100644 index 00000000..091d0f0f --- /dev/null +++ b/tickets/session-todo-reminder.md @@ -0,0 +1,60 @@ +# セッション内 Task ツールの注意機構 + +## 背景 + +`tickets/session-todo.md` で導入する Task ツール群があっても、LLM はそれを使わずに作業を続け得る。ツールを呼ばないまま会話が長引くと、 + +- 開始した作業の `inprogress` がずっと放置されたままになる +- 「やったつもり」になって `completed` への更新を忘れる +- そもそも TaskStore の存在を忘れて、構造化を諦めて自由記述に回帰する + +OpenCode の todo は専用の注意機構を持たない(汎用 reminder 経由)。一方 Claude Code は `task_reminder` を「N リクエスト無アクティビティで初めて発火するナッジ型」として実装しており、毎リクエスト押し戻しはしない(`` の local reminder helper functions、閾値 `a local reminder threshold`)。 + +Insomnia でも同方針を採り、active Task が残っているのに `TaskCreate` / `TaskUpdate` が一定リクエスト呼ばれていない場合に限り、`` で揮発的に思い出させる。「やったつもり」抑止と、トークン浪費・LLM の自律性侵害のバランスを取るため、毎リクエスト押し戻しはしない。 + +## 前提 + +- `tickets/session-todo.md` の TaskStore と `TaskCreate` / `TaskUpdate` / `TaskList` / `TaskGet` ツールが利用可能であること +- `pre_llm_request` 相当のフックを Pod が持つこと(無ければ本ticket で導入) + +## 方針 + +- **`pre_llm_request` Interceptor として実装**。直近の user message に `` ブロックを揮発的に append するだけ。履歴・ログには載せない +- **system-reminder 注入の汎用化はやらない**。利用者が Task 1機構しかない段階で抽象を立てない(CLAUDE.md「概念の追加は不在が問題になってから」)。ただし「タグ形式は `...` で揃える」「履歴は汚さない」の2点は本実装で確立し、将来の追加機構が同じ規約に乗れるようにする +- **発火はナッジ型**。N リクエスト無アクティビティで初めて発火し、cooldown も持つ + +## 要件 + +### Interceptor + +- `pre_llm_request` で `Vec` を受け取り、以下の 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])の末尾に `` ブロックを append し、現在の active Task リストを `taskid` / `status` / `subject` を含む簡潔な形式で列挙する。`description` は長大化を避けるため省略してよい +- 履歴 (`Worker` の保持する `Vec`) は変更しない。リクエスト送信時の Vec のみ加工する +- active Task が空の場合は何も差し込まない(忘却防止が目的なので、思い出させる対象が無いなら不要) + +## 完了条件 + +- 直近 N リクエスト連続で `TaskCreate` / `TaskUpdate` が呼ばれず、かつ active Task が残っている場合に限り、`pre_llm_request` で `` が直近 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`(local reminder helper functions、閾値 `a local reminder threshold`) diff --git a/tickets/session-todo.md b/tickets/session-todo.md index 81994966..3dc27291 100644 --- a/tickets/session-todo.md +++ b/tickets/session-todo.md @@ -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 がツールを使わなくなった場合のサボり防止」を目的とした注意機構(`` の自動注入)は本チケットの範囲外。`tickets/session-todo-reminder.md` で別途扱う。 ## 方針 -- **保存先は `tools` 層の session-lifetime 状態**。`Tracker` と同じ生存スコープで `Pod` が所有。`Arc>>` ベースの `TodoStore` を tool に注入する -- **永続化は専用レーンを持たない**。`tool_call.arguments` がセッションログに既に乗っているため、resume 時には履歴 replay の中で最後の `todo_write` 引数を `TodoStore` に再適用すれば状態が復元される -- **注意機構は `pre_llm_request` Interceptor**。直近の user message に `` ブロックを揮発的に append するだけ。履歴・ログには載せない -- **system-reminder 注入の汎用化はやらない**。利用者が TODO 1個しかない段階で抽象を立てない(CLAUDE.md「概念の追加は不在が問題になってから」)。ただし「タグ形式は `...` で揃える」「履歴は汚さない」の2点は本実装で確立し、将来の追加機構が同じ規約に乗れるようにする +- **保存先は `tools` 層の session-lifetime 状態**。`Tracker` と同じ生存スコープで `Pod` が所有。`Arc>` ベースの 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` を受け取り、未完了 TODO(`pending` または `in_progress`)が 1 件でも存在する場合に発動 -- 直近の user message の content(または content[最終 text part])の末尾に `` ブロックを append -- ブロック内には現在の TODO リストを、status を含む簡潔な形式で列挙 -- 履歴 (`Worker` の保持する `Vec`) は変更しない。リクエスト送信時の 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 復元がカバーされる ## 範囲外 -- 差分更新 API(add / remove / patch)。全置換のみで十分 -- TODO 階層・優先度・タグ -- TUI / GUI での TODO 状態の可視化(ツール呼び出しのイベントは既に流れているので、クライアント側で表示するかは別軸) -- system-reminder 注入機構の汎用化(`TODO.md` に立項済み、別途検討) -- TODO の永続化を専用 LogEntry に分離する設計(現方針は tool_call replay で復元、追加レーン不要) -- 複数 Pod 間で TODO を共有する仕組み +- 注意機構(`` の自動注入による「サボり防止」)→ `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`(注意機構)