From dd3903efde1db2dc7c0e2ea10b9dd2390d31d02b Mon Sep 17 00:00:00 2001 From: Hare Date: Mon, 4 May 2026 21:16:31 +0900 Subject: [PATCH] =?UTF-8?q?docs(tickets):=20Reasoning=E3=81=AE=E3=82=B3?= =?UTF-8?q?=E3=83=B3=E3=83=86=E3=82=AD=E3=82=B9=E3=83=88=E7=AE=A1=E7=90=86?= =?UTF-8?q?=E3=81=A8Prune=E3=81=AE=E8=AA=BF=E6=95=B4=E3=83=81=E3=82=B1?= =?UTF-8?q?=E3=83=83=E3=83=88=E8=BF=BD=E5=8A=A0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- TODO.md | 3 + docs/ref/model-reasoning-context.md | 217 +++++++++++++++++++++++++++ tickets/persistence-semantics.md | 91 +++++++++++ tickets/prune-token-budget.md | 56 +++++++ tickets/reasoning-history-persist.md | 34 +++++ 5 files changed, 401 insertions(+) create mode 100644 docs/ref/model-reasoning-context.md create mode 100644 tickets/persistence-semantics.md create mode 100644 tickets/prune-token-budget.md create mode 100644 tickets/reasoning-history-persist.md diff --git a/TODO.md b/TODO.md index 3357d62d..81678070 100644 --- a/TODO.md +++ b/TODO.md @@ -7,8 +7,10 @@ - Pod: 任意ターンからの Fork(複数ターン巻き戻しを汎用化) → [tickets/pod-session-fork.md](tickets/pod-session-fork.md) - Pod: 子→親の TurnEnded/Errored callback を親由来ターンのみに絞る → [tickets/pod-parent-turn-callback.md](tickets/pod-parent-turn-callback.md) - Pod: セッションログをバックエンドにした Pod 単位の永続化 → [tickets/pod-persistent-state.md](tickets/pod-persistent-state.md) +- 永続化層のセマンティック整理 → [tickets/persistence-semantics.md](tickets/persistence-semantics.md) - llm-worker のエラー耐性 - ストリーム途中失敗時の継続 → [tickets/llm-worker-stream-continuation.md](tickets/llm-worker-stream-continuation.md) +- llm-worker: reasoning ブロックを history に永続化 (Anthropic signature 保持・ツール使用ループでの round-trip) → [tickets/reasoning-history-persist.md](tickets/reasoning-history-persist.md) - ネイティブ GUI クライアント MVP → [tickets/native-gui-mvp.md](tickets/native-gui-mvp.md) - TUI 拡充 - Run 中の入力キューイング → [tickets/tui-input-queue.md](tickets/tui-input-queue.md) @@ -18,6 +20,7 @@ - セッションコンテキスト長 / ウィンドウ占有率の常時表示 → [tickets/tui-context-usage-indicator.md](tickets/tui-context-usage-indicator.md) - Compaction 進行中のライブ表示 → [tickets/tui-compact-progress.md](tickets/tui-compact-progress.md) - Manifest: Tool Output / File Upload 上限の分離とデフォルト緩和 → [tickets/manifest-output-upload-limits.md](tickets/manifest-output-upload-limits.md) +- Prune: 保護境界を turn 数から末尾 token budget に置き換え → [tickets/prune-token-budget.md](tickets/prune-token-budget.md) - メモリ機構 - 使用頻度メトリクス + Knowledge 化候補レポート → [tickets/memory-usage-metrics.md](tickets/memory-usage-metrics.md) - セッション内 Task ツールの注意機構(無アクティビティで `` ナッジ) → [tickets/session-todo-reminder.md](tickets/session-todo-reminder.md) diff --git a/docs/ref/model-reasoning-context.md b/docs/ref/model-reasoning-context.md new file mode 100644 index 00000000..1f1d4a31 --- /dev/null +++ b/docs/ref/model-reasoning-context.md @@ -0,0 +1,217 @@ +# LLMのReasoningコンテキスト管理仕様 比較レポート + +**対象**: Claude (Anthropic) / ChatGPT (OpenAI) / Ollama +**作成日**: 2026年5月4日 +**目的**: 各LLMプロバイダがReasoning(思考トレース)をマルチターン会話でどのように扱うかを整理し、実装時の注意点をまとめる。 + +--- + +## 1. はじめに + +Reasoning対応モデルは、最終応答の前に「思考プロセス」を生成する。この思考はユーザーに見せる/見せない、次ターンに残す/残す、ツール使用中に保持する/しない、といった扱いがプロバイダごとに大きく異なる。本レポートでは Claude / ChatGPT / Ollama の3プラットフォームについて、コンテキスト管理の仕様を比較する。 + +主要な論点は以下の3つ: + +1. **思考の表現形式** — どのフィールド/ブロックに格納されるか +2. **マルチターン保持** — 次のユーザーターンに進んだとき思考は残るか +3. **ツール使用との関係** — ツール呼び出しの前後で思考をどう扱うか + +--- + +## 2. Claude (Anthropic) + +### 2.1 表現形式 + +Claudeは API レスポンスに専用の `thinking` ブロックを返す。コンテンツブロック配列の中に `type: "thinking"` の要素として並び、最終応答は `type: "text"` ブロックとして別に格納される。 + +`thinking` ブロックには `signature` という暗号化フィールドが付与され、改ざん検知や正当性確認に使われる。これは Claude 4 系列で長くなり、API 利用者は値を解釈・パースしてはならない仕様。 + +### 2.2 マルチターン保持の挙動 + +モデル世代によってデフォルトが分かれる: + +- **Opus 4.5+ / Sonnet 4.6+**: 過去ターンの thinking ブロックがコンテキストに**保持される**(入力トークンとしてカウント) +- **それ以前の Opus/Sonnet および全 Haiku**: 過去ターンの thinking ブロックは**剥離される**(コンテキストに加算されない) + +これは Anthropic 側の方針転換で、新世代では「推論の連続性」を優先する設計に変わった。実効コンテキストウィンドウの計算式は次のようになる: + +``` +context_window = (current input tokens − previous thinking tokens) + (thinking tokens + encrypted thinking tokens + text output tokens) +``` + +### 2.3 ツール使用との関係 + +ツール使用時は仕様が厳密になる。`tool_use` → `tool_result` → 続きのアシスタント応答という流れが**同一論理ターン**として扱われ、この間 thinking ブロックは**必ず保持して API に渡し戻す**必要がある。これはモデルの推論連続性を維持するため。 + +ただし境界条件として、tool_result ではない通常の user メッセージが入った時点で、それまでの thinking ブロックは無視・剥離される。つまり「新しいユーザー発話」がリセットポイントになる。 + +### 2.4 制御API + +`clear_thinking_20251015` という context-editing 戦略により、開発者側で保持ポリシーを上書きできる: + +```python +context_management={ + "edits": [{ + "type": "clear_thinking_20251015", + "keep": {"type": "thinking_turns", "value": 2} + }] +} +``` + +`keep.value` で「直近Nターン分の thinking だけ残す」といった粒度で指定可能。新世代モデルで thinking 保持がデフォルトになったため、コンテキスト圧迫を避けたい場合に使う。 + +--- + +## 3. ChatGPT (OpenAI) + +### 3.1 表現形式 + +OpenAIの設計思想は Claude/Ollama と大きく異なり、**生のreasoningトークンはユーザーに見せない**。安全性上の理由から、reasoning は要約された形式(reasoning summaries)でのみ可視化される。 + +API レスポンスには `ResponseReasoningItem` という型のオブジェクトが含まれ、これは ID とオプションの暗号化コンテンツを持つが、本文は黒箱として扱われる。raw reasoning を `reasoning summary` 以外の方法で抽出しようとする試みは利用規約違反となる可能性がある。 + +### 3.2 マルチターン保持の挙動 + +ここが最も特殊。**Chat Completions API と Responses API で挙動が違う**。 + +#### Chat Completions API (旧来) + +reasoning トークンは各ターンの後に破棄される。次ターンには引き継がれない(input/output tokens のみが次ステップに送られる)。これは o1 系列の初期設計。 + +#### Responses API (推奨) + +ステートフル管理が可能。reasoning items を次のリクエストに引き継ぐには2方式ある: + +1. `previous_response_id` パラメータで過去のレスポンスを参照 +2. `response.output` の全アイテムを次の `input` に手動で渡す + +ステートレス利用(`store=false`、ZDR組織)の場合は `include=["reasoning.encrypted_content"]` を指定すれば暗号化された推論コンテンツを受け取り、次リクエストに渡すことで推論を引き継げる。 + +#### モデル世代差 + +- **o1 / o3-mini / o1-mini / o1-preview**: フォローアップリクエストで reasoning items は常に無視される(input に含めても) +- **o3 / o4-mini 以降**: function call に隣接する一部の reasoning items はコンテキストに含まれ、ツール使用の連続性を改善する + +ZDR (Zero Data Retention) を有効にした組織でも、暗号化機構により reasoning items を OpenAI 側に保存せずに API リクエスト間で再利用できるようになっている。 + +### 3.3 ツール使用との関係 + +o3 / o4-mini 以降では、function call をチェーン・オブ・ソート内で直接呼べるようになっており、reasoning tokens がリクエスト・ツール呼び出しを跨いで保持される。これにより複数ステップのエージェント的タスクでインテリジェンスが向上し、コスト/レイテンシも削減される(推論キャッシュの再利用)。 + +### 3.4 制御API + +`reasoning.effort` パラメータで思考量を `low` / `medium` / `high` で指定可能(o1-mini は非対応)。`max_output_tokens` で reasoning + 最終出力の合計トークンを制限できる。 + +--- + +## 4. Ollama + +### 4.1 表現形式 + +Ollamaはローカル実行プラットフォームで、モデルごとに思考タグの規約が異なる。レスポンスでは `message.thinking`(chat エンドポイント)または `thinking`(generate エンドポイント)に推論トレース、`message.content` / `response` に最終回答が分離されて格納される。 + +内部的にはモデル固有のパーサが動く: + +- **Qwen3Parser** — Qwen 系の thinking タグとツール呼び出しタグを処理 +- **DeepSeek3Parser** — DeepSeek の推論出力に最適化 +- **Harmony** — GPT-OSS モデル用、low/medium/high の段階的 thinking レベルをサポート + +ハードコードされたパーサがないモデルでは `thinking.InferTags` がプロンプトテンプレートをスキャンし、`...` などのデリミタを推測する。 + +### 4.2 マルチターン保持の挙動 + +**Ollama自体はthinking履歴を管理しない**。これは設計上の重要な特徴で、メッセージ配列を組み立てるクライアント側の責任になる。 + +モデル側のテンプレート設計レベルで「履歴に思考を残すな」と明示しているケースが多い。例えば Gemma 4 系は公式ドキュメントで明示的に「マルチターン会話では過去のモデル出力には最終応答のみを含めるべきで、過去のモデルターンの思考は次のユーザーターンの前に追加してはいけない」と指示している。DeepSeek-R1 や Qwen3 も同様の前提で訓練されている。 + +したがって標準的な実装パターンは「**次ターン送信時に thinking フィールドを落とし、content だけ履歴に積む**」となる。Ollama が thinking を別フィールドに分離しているのは、ユーザーが履歴構築時に簡単に捨てられるようにする意図もある。 + +### 4.3 ツール使用との関係 + +ツール呼び出しループで思考を保持したい場合、`clear_thinking=false` を上流APIで設定することで `` ブロックを会話コンテキストに保持できる。OllamaはThinkValueが nil または true のときこれを自動処理する。 + +実用上は Open WebUI などのクライアント実装が参考になる。Open WebUI は同一ターン内のツール呼び出しでは reasoning コンテンツを保持し、`...` タグでシリアライズして次のAPIコール時の assistant メッセージの content フィールドに含める方式を採っている。 + +### 4.4 制御API + +主要な制御は以下: + +- `think` パラメータ — boolean(true/false)、ただし GPT-OSS は `"low"` / `"medium"` / `"high"` の文字列のみ受け付ける +- CLI: `--think` / `--think=false` / `--hidethinking`(思考はするが表示しない) +- サーバー起動時: `ollama serve --reasoning-parser deepseek_r1` でパーサを明示 + +サポート判定は `supportsThinking` 関数が `config.json` を見て、glm4moe / deepseek / qwen3 などの既知アーキテクチャか確認する仕組み。 + +--- + +## 5. 比較表 + +### 5.1 仕様サマリー + +| 観点 | Claude | ChatGPT (OpenAI) | Ollama | +|---|---|---|---| +| **思考の可視性** | 完全可視(生のthinking) | 要約のみ可視(生は黒箱) | 完全可視(モデル次第でタグ形式) | +| **思考の格納先** | 専用ブロック (`type: "thinking"`) | `ResponseReasoningItem` | `message.thinking` フィールド | +| **暗号化/署名** | signature付き | encrypted_content(オプション) | なし | +| **デフォルトの履歴保持** | Opus 4.5+/Sonnet 4.6+: 保持
それ以前: 剥離 | Chat Completions: 破棄
Responses: 引き継ぎ可 | クライアント責任
(モデル指示は剥離が主流) | +| **ツール使用中の保持** | 必須(同一ターン内) | o3/o4-mini で関連部分自動保持 | `clear_thinking=false` で制御 | +| **管理層** | API側がマネージド | API側がマネージド | クライアント側で実装 | +| **思考量制御** | `clear_thinking_20251015`戦略 | `reasoning.effort` (low/medium/high) | `think`パラメータ (bool / level) | + +### 5.2 設計思想の違い + +| プロバイダ | 思想 | +|---|---| +| **Claude** | 「マネージドな推論ブロック」として抽象化。署名付きで改ざん耐性を持たせ、API側で保持/剥離を判断 | +| **ChatGPT** | 「思考は黒箱、要約だけ見せる」。安全性とIP保護優先で、推論の引き継ぎはAPI仕様(Responses)でハンドル | +| **Ollama** | 「プロンプトに混ぜるタグの開閉とパース」という素朴な仕組み。クライアントの自由度が高いが責任も大きい | + +--- + +## 6. 実装上の注意点 + +### 6.1 共通の落とし穴 + +- **モデル世代差を見落とす** — Claude も OpenAI も、世代によってデフォルトが大きく異なる。バージョン固定や挙動確認は必須 +- **ツール使用ループでの推論喪失** — Claude/Ollama 共に、ツール使用中に thinking を落とすと推論連続性が壊れる。同一ターン内は必ず保持する +- **トークンコスト** — Claude 新世代では thinking が入力トークンとして加算される。長い対話では `clear_thinking_20251015` でのトリミングが必要 + +### 6.2 プロバイダ別の推奨パターン + +**Claude を使うとき** +- 4.5+/4.6+ ではデフォルトで thinking が残るため、長期会話ではコンテキスト圧迫に注意 +- 旧世代との互換コードを書く場合は、両方のデフォルト挙動を意識する +- ツール使用時は受け取った thinking ブロックを**完全な形で**渡し戻す + +**ChatGPT を使うとき** +- 新規実装は **Responses API** を選ぶ(Chat Completions は推論引き継ぎが弱い) +- ZDR組織でも `reasoning.encrypted_content` で推論を引き継げる +- raw reasoning の抽出を試みない(規約違反の可能性) + +**Ollama を使うとき** +- クライアント側で thinking フィールドの扱いを明示的に決める +- モデル毎のテンプレート規約を確認する(Gemma 4 のように「履歴に残すな」指示があるモデルもある) +- サーバー起動時に `--reasoning-parser` を適切に設定する + +--- + +## 7. まとめ + +3プラットフォームのReasoning管理は、それぞれ異なる優先事項を反映している: + +- **Claude** は「推論の透明性と連続性」を最重視し、API側でリッチなマネジメントを提供 +- **ChatGPT** は「安全性とIP保護」を最重視し、生の推論をユーザーから隠蔽しつつ機能性は Responses API で担保 +- **Ollama** は「ローカル実行と柔軟性」を最重視し、クライアントに最大限の制御権を委ねる + +実装者にとっては、「次のユーザーターンに進んだ時点で何が剥がれるか」を**プロバイダ × モデル世代 × API選択**の3軸で把握しておくことが重要。特にツール使用を伴うエージェント構築では、各プラットフォームの推論保持機構を正しく使わないと、推論連続性の喪失によりインテリジェンスが大幅に低下する。 + +--- + +## 参考資料 + +- Anthropic: [Building with extended thinking](https://docs.claude.com/en/docs/build-with-claude/extended-thinking) +- Anthropic: [Context editing](https://platform.claude.com/docs/en/build-with-claude/context-editing) +- OpenAI: [Reasoning models guide](https://developers.openai.com/api/docs/guides/reasoning) +- OpenAI Cookbook: [Better performance from reasoning models using the Responses API](https://cookbook.openai.com/examples/responses_api/reasoning_items) +- Ollama: [Thinking capability docs](https://docs.ollama.com/capabilities/thinking) +- Ollama Blog: [Thinking](https://ollama.com/blog/thinking) diff --git a/tickets/persistence-semantics.md b/tickets/persistence-semantics.md new file mode 100644 index 00000000..c340cf94 --- /dev/null +++ b/tickets/persistence-semantics.md @@ -0,0 +1,91 @@ +# 永続化層のセマンティック整理 + +## 背景 + +現在の永続化は `SessionId` 単位の append-only JSONL log を中心に構成されている。これは実装上は扱いやすい一方で、今後 Pod 単位永続化、compaction、fork、DB backend 追加などを進めるにあたり、以下の概念が混ざり始めている。 + +- ユーザー視点の「同じ会話 / 作業の継続単位」 +- Pod 視点の「現在 active な会話状態」 +- append-only log の物理的 / 復元上の単位 +- compaction によって生成される新しい履歴系列 +- fork の起点となる履歴中の境界 +- runtime dir に置かれる一時状態と、data dir / DB に置く永続正本 + +特に、現在は compaction によって新しい `SessionId` が発行される。これは append-only log の低レベル単位としては自然だが、ユーザー視点では「同じ会話が継続している」とも見えるため、`Session` という名称・粒度が今後の設計上あいまいになり得る。 + +このチケットでは、実装変更に入る前に、永続化層のドメイン概念・名称・責務境界を整理する。 + +## 目的 + +- 永続化層で扱う概念を、ユーザー視点 / Pod 視点 / storage 視点に分けて定義する。 +- `SessionId` が今後も適切な中心概念か、あるいは別概念に分解すべきかを判断する。 +- compaction / fork / resume / Pod state / spawned child registry が、どの粒度のデータに属するかを決める。 +- 将来 DB backend を追加しても歪みにくいデータ構造を設計する。 +- 既存の session-store JSONL 実装から段階的に移行できる命名・API 境界を決める。 + +## 検討事項 + +- 会話継続単位と append-only log 単位を分けるべきか。 + - 例: user-visible conversation/thread と、internal log segment の分離。 +- compaction の扱い。 + - compaction 後の履歴を新しい低レベル log として扱うのは自然か。 + - その場合、ユーザー視点では同じ会話の継続としてどう表現するか。 +- fork の扱い。 + - fork は新しい会話単位を作るのか、同一会話内の branch とするのか。 + - fork 起点を entry hash / turn boundary / checkpoint など、どの抽象度で表すか。 +- Pod state の責務。 + - Pod 名から active な会話 / log を復元するために何を持つべきか。 + - Pod が過去に辿った session / log の順序付き履歴をどこに持つべきか。 +- runtime state と persistent state の境界。 + - `history.json` / `status.json` / `spawned_pods.json` を永続正本として扱わない方針の確認。 +- DB backend を想定した場合のテーブル / relation 相当の構造。 + - append-only entry log + - lineage / origin + - active pointer + - Pod / child Pod registry + - index / listing / GC の余地 +- 既存 API / CLI 名称の移行方針。 + - `--session` の扱い + - debug 用 ID とユーザー向け ID の分離 + +## 一案: Thread / Segment / Checkpoint に分ける案 + +これは現時点の決定ではなく、検討材料の一案として置く。 + +- `Pod`: agent 実行主体 / process identity。 +- `Thread`: ユーザー視点の会話・作業継続単位。compaction しても同じ Thread と見なす。 +- `Segment`: append-only log の物理的 / 復元上の単位。現在の `SessionId` に近い。compaction / fork で新しい Segment が生まれる。 +- `Entry`: Segment 内の 1 永続化イベント。 +- `Checkpoint`: fork / rollback / UI 選択などの起点を表す抽象境界。内部的には Segment + EntryHash を指してもよいが、表層 API では entry pointer を直接露出しすぎない。 + +この案では: + +- compaction = same Thread, new Segment +- fork = new Thread または branch, new Segment +- resume = same Thread の active Segment に append +- Pod state = active Thread / spawned children / 必要な runtime 復元メタデータを保持 +- lineage = Segment origin または Checkpoint reference として保持 + +この案を採用するかは本チケット内で改めて比較・判断する。 + +## 完了条件 + +- 永続化層の主要概念と名称が文書化されている。 +- compaction / fork / resume / Pod state のデータ粒度が決まっている。 +- 現在の `SessionId` / session-store API をどう扱うか、維持・alias・rename・段階移行の方針が決まっている。 +- DB backend を追加する場合の概念モデルが、最低限テーブル / relation 相当で説明できる。 +- `tickets/pod-persistent-state.md` や fork 関連チケットに反映すべき前提が整理されている。 + +## 範囲外 + +- このチケット単体での大規模 rename 実装。 +- DB backend の実装。 +- UI の履歴表示 / branch 表示の詳細 UX。 +- GC / retention policy の実装。 + +## 関連 + +- `tickets/pod-persistent-state.md` +- `tickets/pod-session-fork.md` +- `crates/session-store/` +- `crates/pod/src/pod.rs` diff --git a/tickets/prune-token-budget.md b/tickets/prune-token-budget.md new file mode 100644 index 00000000..ab28bd47 --- /dev/null +++ b/tickets/prune-token-budget.md @@ -0,0 +1,56 @@ +# Prune: 保護境界を token budget 化 + +## 背景 + +現状の Prune は `prune_protected_turns`(デフォルト 3)で user message 起点の turn 数を数え、直近 N turn 分の `Item::ToolResult.content` を保護する(`crates/llm-worker/src/prune.rs:107-164`、`crates/manifest/src/defaults.rs:13-15`)。 + +この境界定義は対話頻度に依存して挙動が極端に変わる: + +- 短い対話中心のセッション: 4 turn 目以降は意図通り定常的に刈れる。 +- 単発の長タスク(agentic loop で 1 user message から数十〜数百 LLM call が走るケース): history 全体が 1 turn 扱いになり、`turn_starts.len() <= protected_turns` で候補抽出すら行われず、`SkippedNoCandidates` で恒常的に発火しない。コンテキスト窓が一方的に膨れて compaction に押し付ける形になる。 + +そもそも prune は「古い tool_result の content を切り詰めて token を回収する」機構であり、保護量も token で測るのが意味論的に一貫している。compaction 側は既に `compact_retained_tokens: 8000` という末尾 token budget で保護しており、二つの機構の保護軸を揃えると設定の理解が単純になる。 + +LLM call 境界(assistant 出力の単位)を history から後追い検出する必要は無い。Prune が走るのは LLM call 直前のみで、その時点で Worker は usage 履歴を持っており、末尾からの累計トークンで境界を引ける。 + +## 方針 + +`prune_protected_turns` を撤廃し、`prune_protected_tokens: u64` に置き換える。 + +- 候補抽出: history 末尾から item ごとの推定トークンを累計し、累計が `protected_tokens` を超える位置までを保護領域とする。それより前の `Item::ToolResult { content: Some(_), .. }` が prune 候補。 +- usage 測定が無い時点(最初の LLM call 前 / compact 直後)は推定が `NoData` を返すため、保護領域の決定もできない。この場合は候補抽出を諦めて `SkippedNoCandidates` 相当で抜ける(既存 NoData ハンドリングの踏襲)。 +- `prune_min_savings` 判定はそのまま残す。二段の判定(候補があるか / savings が閾値を超えるか)は維持する。 +- tool_call と tool_result のペアは `call_id` で対応が取られているため、保護境界が途中を切っても projection は content を `None` にするだけで summary 構造は壊れない。境界の精度よりも token 量の正確さを優先してよい。 + +## 要件 + +- Manifest: `compaction.prune_protected_turns` を撤廃し `compaction.prune_protected_tokens: u64` を追加する。後方互換 shim は入れない。 +- デフォルト: `PRUNE_PROTECTED_TOKENS = 8000`(`COMPACT_RETAINED_TOKENS` と揃える)。 +- `PruneConfig` も同様に `protected_turns` → `protected_tokens` に rename。 +- 候補抽出ロジック (`prune.rs`) は token 累計ベースに切り替える。usage 推定の取得経路は既存 `SavingsEstimator` と同じ「Worker に callback を install する」パターンで足す。Worker / prune.rs 自体は usage source を知らないままに保つ。 +- メトリクス: `prune.fire` / `prune.skip` の既存 dimension のうち `border_turn` は意味を失うので、保護境界を表す新 dimension(保護領域の先頭 item index または保護領域の累計トークン)に差し替える。`candidate_count` と `value=estimated_savings` は維持する。 +- 既存テスト (`prune.rs` 末尾のユニットテスト群) は token budget ベースに書き直す。 + +## 完了条件 + +- 単発の長タスクで重い ToolResult が積もるシナリオで、4 番目以降の LLM call から `prune.fire` が観測される(重い ToolResult ほど早く刈られる挙動)。 +- 短い対話セッションでも、末尾 8000 token に収まらなくなった古い ToolResult の content が従来通り刈られる。 +- `prune_protected_turns` を旧フィールド名で書いた manifest は明示的にエラーになる(後方互換無し)。 + +## 範囲外 + +- `Item` に `request_seq` 等の LLM call 境界情報を埋め込む案。今回は履歴構造を変更しない。 +- compaction 側 (`compact_retained_tokens`) のロジック変更。 +- `prune_min_savings` の値・判定ロジックの変更。 +- token 推定アルゴリズム自体の改善(`UsageHistory` ベースの既存推定をそのまま使う)。 + +## 影響範囲 + +- `crates/manifest/src/defaults.rs`: `PRUNE_PROTECTED_TURNS` 削除、`PRUNE_PROTECTED_TOKENS` 追加。 +- `crates/manifest/src/{config,lib}.rs`: `CompactionConfig` の field rename とカスケード解決の差し替え。 +- `crates/llm-worker/src/prune.rs`: `PruneConfig`、`prunable_indices` / `evaluate_candidates` の引数とロジック、ユニットテスト。 +- `crates/llm-worker/src/worker.rs`: prune 評価呼び出し箇所、必要なら新しい token-estimator callback の install 経路。 +- `crates/pod/src/compact/prune.rs`: `PruneConfig` 組み立てと、新しい token 推定 callback の注入。 +- `crates/pod/src/compact/token_counter.rs`: 末尾累計トークン算出のヘルパー(`savings_for_prune_impl` の隣に追加 or 共通化)。 +- `crates/pod/tests/session_metrics_test.rs`: `prune.fire` / `prune.skip` の dimension 期待値。 +- `docs/compaction.md`: 設定セクションの記述を更新。 diff --git a/tickets/reasoning-history-persist.md b/tickets/reasoning-history-persist.md new file mode 100644 index 00000000..479e0344 --- /dev/null +++ b/tickets/reasoning-history-persist.md @@ -0,0 +1,34 @@ +# reasoning ブロックを history に永続化する + +## 背景 + +`docs/ref/model-reasoning-context.md` でまとめた通り、近年の Reasoning 対応モデルは「assistant 応答に含まれた thinking / reasoning ブロックを次のリクエストに戻す」ことを前提に設計が進んでいる。特に: + +- Anthropic: ツール使用ループ (`tool_use` → `tool_result` → 続きの assistant) では同一論理ターン内で thinking ブロックを **必ず** 返送する必要がある。新世代 (Opus 4.5+/Sonnet 4.6+) ではマルチターンでも保持がデフォルト。thinking ブロックには `signature` が付与され、改ざん検知に使われる +- OpenAI Responses API: reasoning items を `previous_response_id` または `output` 再送で引き継ぐ設計。o3/o4-mini 以降は function call 隣接の reasoning items が連続性に効く +- Ollama: client 側で thinking フィールドの再送有無を制御する責務 + +現状の `llm-worker` は streaming イベントとして thinking デルタを観測でき、`Worker::on_thinking_block` callback まで到達する一方で、**`worker.history` 上の `Item` には一切 commit されず、ターン境界で蒸発する**。`worker.rs:976` で `text_block_collector` のみが `take_collected()` され、`build_assistant_items` (`worker.rs:591-610`) は `assistant_message` と `tool_call` しか生成しない。 + +加えて、Anthropic 応答中の `SignatureDelta` は `anthropic/events.rs:256` で明示的に捨てられている。`Item::Reasoning` (`types.rs:84-102`) にも `signature` フィールドがない。このまま新世代 Claude に thinking を送り返そうとすると signature 不整合で 400 を返される可能性が高い。 + +CLAUDE.md の「LLM コンテキスト加工原則」に照らしても、thinking を history に commit せず context にだけ載せる解決は禁止側に該当する。最初から history 上の Item として扱うのが正解。 + +## 要件 + +- llm-worker のストリーミング層で受信した thinking / reasoning ブロックが、ターン終了時に `Item::Reasoning` として `worker.history` に append され、`history.json` に永続化される +- Anthropic の `signature` が round-trip で保持される (受信→Item に格納→次リクエストの assistant message として再送)。`Item::Reasoning` に必要なフィールドを追加する +- ツール使用ループ内 (同一論理ターン: assistant → tool_use → tool_result → 次の assistant) で、直前 assistant ターンの reasoning が次のリクエストに含まれる +- 通常のマルチターン (新しいユーザー入力をまたぐ) でも reasoning が引き継がれる。世代別 keep/strip のデフォルト分岐は本チケットでは扱わず、保持側の挙動を実装する +- 既存の `Worker::on_thinking_block` callback の発火タイミング・ペイロードは互換維持 +- OpenAI Responses scheme で既に部分対応している `encrypted_content` / `summary` の経路は活用しつつ、history への commit ルートを統一する +- resume (`history.json` から再開) 時にも reasoning が再現できる (これは history に乗ってさえいれば自動) + +## スコープ外 (フォローアップ候補) + +以下は本チケット完了後に別途検討する。本チケットでは触らない: + +- モデル世代別の keep/strip デフォルト分岐 (Opus 4.5+/Sonnet 4.6+ vs それ以前、OpenAI Responses vs Chat Completions) +- Anthropic `clear_thinking_20251015` context-editing 戦略の実装 +- `prune.rs` の reasoning aware 化 (古い reasoning の選択的剥離) +- Ollama scheme の `think` パラメータ対応 (そもそも Ollama scheme 自体が未実装)