pod-prompt-catalog完了

This commit is contained in:
Keisuke Hirata 2026-04-22 17:43:42 +09:00
parent c68cd64882
commit 6146b2806f
3 changed files with 0 additions and 192 deletions

View File

@ -6,7 +6,6 @@
- [ ] LLM プロバイダ/モデルカタログ → [tickets/llm-provider-catalog.md](tickets/llm-provider-catalog.md)
- [ ] Pod オーケストレーション
- [ ] 動的 Scope 変更 → [tickets/dynamic-scope.md](tickets/dynamic-scope.md)
- [ ] Pod 内部プロンプトのカタログ化 → [tickets/pod-prompt-catalog.md](tickets/pod-prompt-catalog.md)
- [ ] ネイティブ GUI クライアント MVP → [tickets/native-gui-mvp.md](tickets/native-gui-mvp.md)
- [ ] TUI 拡充
- [ ] フルスクリーン化によるオーバーホール → [tickets/tui-fullscreen-overhaul.md](tickets/tui-fullscreen-overhaul.md)

View File

@ -1,145 +0,0 @@
# Pod 内部プロンプトのカタログ化
## 背景
Pod は Worker を拡張する機構を持つ: コンテキスト圧縮 (Compact)、非同期通知 (Notify)、中断と再開 (Interrupt)、system prompt の trailing section、AGENTS.md 取込時の注記。これらの機構がランタイムで Worker に注入する/Worker 向けに構成するプロンプトは、現状各モジュール内の `const &str` / `format!` に分散している。
| 場所 | 役割 |
|---|---|
| `crates/pod/src/pod.rs:50` `SUMMARY_SYSTEM_PROMPT` | Compact worker の system prompt |
| `crates/pod/src/notification_buffer.rs:73` `format_notification` | `Method::Notify` を Worker history に system_message として注入するラッパー |
| `crates/pod/src/interrupt_and_run.rs:18-20` | 中断時の synthetic tool_result と system_message |
| `crates/pod/src/system_prompt.rs:179-203` `append_trailing_section` | `## Working boundaries` / `## Project instructions (AGENTS.md)` ヘッダとレイアウト |
| `crates/pod/src/agents_md.rs:19` `TRUNCATION_NOTICE` | AGENTS.md が 64KB 超過したときの末尾注記 |
`resources/prompts/default.md` でユーザー向け instruction テンプレートは一元化された一方、Pod が Worker を拡張する側のプロンプトは並行した一元管理を持たない。結果として:
- 全体を俯瞰しづらく、新しい injection 点を足すときに既存との一貫性が取れない
- 多言語化や文体カスタマイズの導線がない
- builtin から差分だけ書き換える軽量な手段がない
tool description は tool 宣言と併置が自然tool 追加 = 1 箇所追加なので本チケット対象外。Pod の Worker 拡張機構は異質なものが「Pod のトーン」として束になって振る舞う性質上、中央で扱う価値がある。
## 要件
### 1. 中央モジュール: `PodPrompt` enum
Pod が Worker に注入する全プロンプトを列挙する enum を 1 つ置く。variant の集合が「存在する injection 点」の master。新しい注入点を増やすときは variant 追加が必要 = コード上で勝手に散らかせない。
Pod 内部の各モジュールは `PodPrompt::CompactSystem.render(&ctx)` のように 1 本の API で引く。直接 `include_str!` / `const &str` / `format!` で prompt 文字列を書かない。
### 2. 翻訳パック形式: `prompts.toml`
全 variant を key-value で持つ TOML。値は minijinja テンプレート文字列builtin/ランタイム問わず一律 minijinja render
```toml
[prompt]
interrupt_system_note = "[The previous turn was interrupted by the user. The user's next request follows.]"
notify_wrapper = """[Notification]
{{ message }}
This is a notification, not a blocking request. ..."""
# 長文は外部ファイルに link
compact_system = "{% include '$insomnia/internal/compact_system.md' %}"
```
変数展開は各 variant ごとに定義された context を render 時に渡す(例: `notify_wrapper``message`、`compact_system` は tool 名リストなど。context の具体形は実装段階で variant ごとに確定する。
### 3. Builtin pack の網羅性をビルドエラーで強制
`resources/prompts/internal.toml``PodPrompt` 全 variant を網羅していないとビルド失敗。enum に variant を足したが builtin pack に key が無い、あるいは逆、はコンパイルが通らない。`build.rs` もしくは proc-macro で検査する(どちらを取るかは実装時判断)。
### 4. 4 段の置換マージ
key 単位で overlay。下層から順に apply、後勝ち:
```
builtin resources/prompts/internal.toml (必須・網羅)
user $XDG_CONFIG_HOME/insomnia/prompts.toml (任意・auto)
workspace <project>/.insomnia/prompts.toml (任意・auto)
manifest pack manifest.pod.prompt_pack で指名 (任意)
```
- 欠落 key: 下層から継承
- ランタイム層 (user/workspace/manifest pack) の unknown key: `tracing::warn!` して無視
- builtin 層の不整合はビルドエラー(前項)
### 5. 値 render は minijinja 統一、include は既存 prefix resolver 流用
全値を minijinja で parse / render。`{% include "$prefix/..." %}` によって外部ファイルを link 可能。resolver は既存 `crates/pod/src/prompt_loader.rs` を流用し、`$insomnia` / `$user` / `$workspace` すべてをどの層の pack からも参照できる。
### 6. manifest 露出
```toml
[pod]
prompt_pack = "$user/packs/japanese.toml"
```
任意フィールド。指定されていれば 4 段目 overlay として適用。auto-discovery (user/workspace) とは独立で共存する。子 Pod を spawn する際、親が子の役割に応じた pack を明示する用途を想定。
## 設計判断
### prompt_pack は prefix 名前空間に載せない
既存 `$insomnia/` / `$user/` / `$workspace/` は**名前空間**で、同じ key で複数の source を指し合うことはない。一方 pack は**レイヤー**で、同じ key を置き換える。この 2 つを同じ軸に混ぜるとユーザーが「どの書き方で上書きされるか」を予測できなくなる。
pack ファイルは**固定パスの auto-discovery** と **manifest による明示指名**の 2 経路のみ。各値内部で `{% include "$prefix/..." %}` を使うのは別軸なので prefix 体系の利点はそのまま享受できる。
### 長文ファイル分離用の独立フィールドを作らない
値が minijinja である以上、長文を別ファイルにしたければ `"{% include '$insomnia/internal/foo.md' %}"` と書けば済む。「TOML 値の文字列」と「ファイル参照」の 2 値型を用意する必要はない。1 種類で統一する。
### ランタイム層の unknown key は warn
pack ファイルを書いた時点と Pod バージョンがずれたとき、hard error にすると古い pack で Pod が起動しなくなる。前方互換のため warn で無視する。builtin 層は build-time に同梱するので不整合はビルドで捕まえられる = error で問題ない。
### tool description は本チケット対象外
tool 追加は tool ごとの 1 箇所の自然な単位で、description は tool の属性として宣言と併置が読みやすい。Pod 内部 injection は「異質なものが一体としてトーンを決める」共通軸なので中央化の価値が別にある。この差を混ぜない。
### auto-discovery と manifest 指定を両立させる
auto-discovery は「ユーザー or プロジェクトの永続設定 (翻訳、文体)」、manifest 指定は「**その Pod の役割**による差し替え」と目的が別。どちらか一方では片方のユースケースが潰れるので両立する。key 単位の merge は共通なので実装コスト差は小さい。
## Scope 外
- tool description の resources 化(併置方針を維持)
- 具体的な翻訳 pack の作成(本チケットは導線のみ)
- pack 編集 GUI / TUI
- user/workspace 以外の auto-discovery パス追加(別 XDG 層、bundle pack 等)
## 依存
- `crates/pod/src/prompt_loader.rs` (`$prefix` resolver を minijinja include から流用)
- `crates/pod/src/system_prompt.rs` (minijinja 使用パターン)
- `crates/manifest`: `pod.prompt_pack: Option<String>` 追加のみで破壊的変更なし
## 影響範囲
- `crates/pod/src/prompts.rs` (新設): `PodPrompt` enum、render API、pack loader、4 段 merge
- `resources/prompts/internal.toml` (新設): builtin pack
- `resources/prompts/internal/*.md` (新設): 長文外出し
- `crates/pod/` 各モジュール: 既存ハードコードを `PodPrompt::...render(&ctx)` 呼び出しに置換
- `crates/pod/build.rs` もしくは proc-macro (新設): enum ⇔ builtin pack 網羅検査
- `crates/manifest/src/config.rs`: `prompt_pack` フィールド追加
## 実装順序
1. `PodPrompt` enum と render API を定義。builtin map (in-memory、`include_str!`) から引くだけの最小実装。variant の render 網羅を単体テストで確認
2. `resources/prompts/internal.toml` に全 key を書き、既存ハードコードをそのまま文字列として移植。各モジュールの呼び出しを `PodPrompt::...render` に置換 (挙動は既存と完全同一)
3. builtin 網羅を build-time 検査に格上げ (enum ⇔ pack の双方向で欠落/余剰を検出)
4. user / workspace の auto-load と 3 段マージ。ランタイム unknown key の warn
5. `manifest.pod.prompt_pack` を追加、4 段目 overlay として load
6. 長文 variant (compact_system など) を `{% include "$insomnia/internal/..." %}` 形式に分離。`$user` / `$workspace` から include で override できることをテスト
各ステップ終了時点でビルド通過・既存テスト合格を維持する。
## Review
- 状態: Approve
- レビュー詳細: [./pod-prompt-catalog.review.md](./pod-prompt-catalog.review.md)
- 日付: 2026-04-22

View File

@ -1,46 +0,0 @@
# Review: Pod 内部プロンプトのカタログ化
## 前提・要件の確認
1. **`PodPrompt` enum (要件1)** — 満たされている。7 variant を列挙し (`crates/pod/src/prompts.rs:60-81`)、各 variant が `key()` とともに declaration-order の `ALL`/`KEYS` 定数に連動する。呼び出し側 (pod/notification_buffer/pod_interceptor/interrupt_and_run/system_prompt/agents_md) はすべてカタログ経由に置換済み。`grep` で `SUMMARY_SYSTEM_PROMPT` / `TRUNCATION_NOTICE` / ハードコード文字列は production コードから一掃されている (テスト文字列のみ残存)。
2. **`internal.toml` builtin pack (要件2)** — 満たされている。`resources/prompts/internal.toml` に 7 key 全てを配置 (`resources/prompts/internal.toml:10-35`)。minijinja テンプレートとして評価され、`compact_system` は `{% include "$insomnia/internal/compact_system" %}` 経由で外部 `.md` を参照。長文分離の設計判断「`include` 一本化」は忠実に実装されている。
3. **ビルド時網羅性検査 (要件3)** — 満たされている。`build.rs` が TOML を parse して `INTERNAL_KEYS` slice を emit、`prompts.rs:122-145` の `const _: ()``PodPrompt::KEYS ↔ INTERNAL_KEYS` を双方向で検査する。ユーザー報告通り、片側除去/余剰で `panic!` メッセージ 2 種が発火することを確認済み。const eval ベースで proc-macro を回避しているのは軽量かつ依存最小で筋が良い。
4. **4 段 overlay merge (要件4)** — 満たされている。`PromptCatalog::load` が builtin → user (`user_pack_file`) → workspace (`workspace_pack_file`) → manifest pack の順で `merge_into` を呼ぶ (`prompts.rs:260-284`)。未知 key は `tracing::warn!` + ignore (`prompts.rs:370-386`)。builtin 層の不整合は build 時 error (要件3 と一体)。テスト `user_pack_overrides_builtin` / `workspace_pack_wins_over_user_pack` / `manifest_pack_wins_over_workspace_pack` / `unknown_key_in_runtime_pack_is_ignored_with_warning` が precedence と warn-ignore を裏打ちしている。
5. **minijinja 統一 + prefix resolver 流用 (要件5)** — 満たされている。`build_catalog` (`prompts.rs:440-482`) が `Environment``path_join_callback`/`loader` に既存 `PromptLoader` を配線し、値内の `{% include "$prefix/..." %}``$insomnia` / `$user` / `$workspace` 全てから引ける。テスト `value_can_pull_long_text_via_include` が builtin の `compact_system` を runtime pack 側から再参照できる挙動を確認している。
6. **`manifest.pod.prompt_pack` (要件6)** — 満たされている。`PodMeta.prompt_pack: Option<String>` を `#[serde(default)]` で追加 (`crates/manifest/src/lib.rs:36-46`)、`PodMetaConfig` のカスケード merge にも反映 (`crates/manifest/src/config.rs:43, 199-206, 340, 415`)。`Pod::from_manifest` / `from_manifest_spawned``PromptCatalog::load(&loader, manifest.pod.prompt_pack.as_deref())` を呼ぶ。`$user/` プレフィックス経由で解決するテスト (`manifest_pack_supports_user_prefix`) 付き。
## アーキテクチャ・スコープ
- **レイヤー境界** — 変更は `crates/pod``crates/manifest` (フィールド追加のみ) に収まり、`llm-worker` は触っていない。Pod 内部プロンプトは Pod 層で扱う、というスコープ方針を守っている。
- **prefix × layer の分離方針** — 設計判断「prefix 名前空間 (resolve where) と layer (merge precedence) は混ぜない」が正しく反映されている。pack 自体は **固定パスの auto-discovery****manifest での明示指名** の 2 経路に限定され、`PromptLoader` の prefix 名前空間 (`$insomnia`/`$user`/`$workspace`) は pack 値内部の `{% include %}` でのみ再利用される。
- **ランタイム vs ビルド時エラーの使い分け** — builtin 不整合は const-eval panic、runtime pack の unknown key は `warn + ignore`。前方互換性の判断通り。
- **tool description に手を入れていない** — scope 外宣言を遵守。`resources/prompts/internal/` ディレクトリは builtin 長文のみ (`compact_system.md` 一件)。
- **cargo add 運用**`[build-dependencies] toml = "1.1.2"` が追加されているが、`cargo add --build` 経由で追加されているかは diff からは直接確認できない。既に `[dependencies]``toml = "1.1.2"` が存在するので workspace の既存バージョンに揃っており、挙動上の懸念は無い (手動編集だったとしても結果は同じ)。
- **影響範囲との一致** — ticket の「影響範囲」リスト (`prompts.rs` 新設 / `internal.toml` / `internal/*.md` / 呼び出し置換 / `build.rs` / `manifest/config.rs`) はすべて diff 上に存在。未対応の項目は無い。
## 指摘事項
### Blocking
- なし。
### Non-blocking / Follow-up
- **[API 表面の二重化]** `PromptCatalog::render(PodPrompt, Value)``compact_system()` 等の typed accessor が同時に `pub` 公開されている (`prompts.rs:289-342`)。ticket は `PodPrompt::CompactSystem.render(&ctx)` を 1 本の API として期待していた。実装の「variant ごとに context 型が固定なので typed accessor が筋」という判断は妥当で、現状の呼び出し元もすべて typed を使っている。`render(PodPrompt, Value)` を `pub(crate)` まで降格するか、typed accessor だけを公開面として残す方が、将来「新しい variant を追加したら typed accessor も実装しないとコンパイル的には気づかない」という弱さを防げる。
- **[`append_trailing_section` の可視性]** `pub fn append_trailing_section` (`system_prompt.rs:188`) は module 内でしか呼ばれていない。`pub(crate)` か private へ落として API 表面を絞るのが望ましい。
- **[factory 側の `.is_file()` と catalog 側の `.is_file()` の二重フィルタ]** `PodFactory::build_prompt_loader` (`factory.rs:202-211`) が既に `.is_file()` で絞った `PathBuf` を渡すのに、`PromptCatalog::load` も `path.is_file()` を再チェックしている (`prompts.rs:267,273`)。冗長で、仕様として「渡されたら読む」なのか「存在チェックは catalog 側の責務」なのかが曖昧。後者に寄せるなら loader 側は無条件に `PathBuf` を渡し、catalog 側だけで分岐させる (既存の挙動を保つ)。前者にするなら loader 側は「pack file が無ければ `None`」という契約を DocComment 化する。ticket にとって致命傷ではない。
- **[`$insomnia/` manifest pack の `.toml` 読み取り経路]** `load_raw_builtin` は拡張子を付けない raw loader だが、これは元々 `$insomnia/` prefix が `.md` 前提で設計されていた `PromptLoader` を「拡張子ごと渡せば読める `.toml` 用 shortcut」として拡張している (`prompt_loader.rs:175-204`)。`$insomnia/` prefix の 2 つの意味 (テンプレートとしての `.md` 参照と、pack ファイルとしての `.toml` 参照) がひとつのローダに同居する形になっている。機能上は問題ないが、`$insomnia/default` (`.md` 付く) と `$insomnia/internal/foo.toml` (拡張子必須) で path の書き方が視覚的に揺れる。ドキュメント or 命名で区別してもよい。
### Nits
- `PromptCatalog::builtins_only()``load(&PromptLoader::builtins_only(), None)` を thin-wrap するだけだが、テスト用に便利なので残しておいて問題なし (多用されている)。
- `CatalogError::UnknownKey``PromptCatalog::render` が未登録テンプレートを引いた場合用だが、現状 `PodPrompt::key()` 経由でしか呼ばれないので到達しづらい。将来外部入力を受ける経路を増やしたときに活きる。
- `single` 関数 (`prompts.rs:345-350`) は `BTreeMap<&'static str, Value>` を毎回組み立てている。typed accessor の呼び出し頻度 (LLM request ごとに高々数回) ではマイクロ最適化する必要はない。
## 判断
**Approve** — 要件 1〜6 はすべて満たされ、設計判断 (prefix × layer 分離 / 単一値型 / ビルド時 vs ランタイム分岐 / tool description 非対象 / auto + manifest 両立) が正確に実装されている。ワークスペーステストは全緑。Non-blocking な API 整理の余地 (render の可視性、loader-catalog 間の is_file 二重チェック) はあるが、ticket を閉じる上の障害ではない。