357 lines
16 KiB
Markdown
357 lines
16 KiB
Markdown
# Pod Factory: カスケード設定とプロンプト資産
|
||
|
||
`PodFactory` は、複数の層に分かれた `manifest.toml` とプログラマティック
|
||
overlay をマージして、検証済みの `PodManifest` と `PromptLoader` を生成する
|
||
ビルダー。これにより Pod 起動ごとに TOML を手書きする必要がなくなる。
|
||
|
||
---
|
||
|
||
## カスケード層
|
||
|
||
優先順位が低い順(上位ほど下位を上書き):
|
||
|
||
| 優先度 | 層 | 位置 | 典型的な内容 |
|
||
|---|---|---|---|
|
||
| 1 | ビルトインのデフォルト | `manifest::defaults` モジュールの `pub const` 群を `PodManifestConfig::builtin_defaults()` が cascade 層として注入 | `tool_output.default_max_bytes = 64 KiB`, `file_upload.max_bytes = 256 KiB` など |
|
||
| 2 | ユーザー manifest | `<config_dir>/manifest.toml`(解決ルールは `manifest::paths`) | プロバイダ指定、デフォルトモデル、常用ツール設定 |
|
||
| 3 | プロジェクト manifest | 起動ディレクトリから上方向に探索した最初の `<root>/.insomnia/manifest.toml` | scope、compaction、プロジェクト固有の instruction |
|
||
| 4 | プログラマティック overlay | CLI / GUI / 別 Pod からの spawn 等 | `pod.name`、spawn 時の `worker.instruction` のような Pod 固有値 |
|
||
|
||
デフォルト値はすべて `crates/manifest/src/defaults.rs` の `pub const` として集約
|
||
されており、serde `#[default = "..."]` 経路(`PodManifest` の直接 deserialize)
|
||
と `TryFrom<PodManifestConfig>` 経路(cascade 解決)の両方が同じ constants を
|
||
参照する。デフォルトを変更するときは `defaults.rs` の 1 行を書き換えるだけで
|
||
全経路に反映される。
|
||
|
||
どの層も TOML スキーマは `PodManifest` と同じ(全フィールド省略可)。
|
||
|
||
## マージセマンティクス
|
||
|
||
| フィールド種別 | 規則 |
|
||
|---|---|
|
||
| スカラー(`String`, `u32`, `bool` 等) | 上層に値があれば丸ごと置換 |
|
||
| `Option<T>` | 上層が `Some` なら置換、`None` なら据え置き |
|
||
| 配列スカラー(`worker.stop_sequences` 等) | 上層に値があれば配列ごと置換。追記マージはしない |
|
||
| マップ(`tool_output.per_tool` 等) | キー単位でマージ、同一キーは上層優先 |
|
||
| `scope.allow` / `scope.deny` | **union**(各層から全部足す)。上位層は `deny` で下位層の `allow` を必ず削れる |
|
||
| `permissions.rule` | **union**(下位層の rule → 上位層の rule の順に評価)。`permissions.default_action` は上位層があれば上書き |
|
||
|
||
各層をマージした結果(`PodManifestConfig`)を `TryFrom<PodManifestConfig>
|
||
for PodManifest` が必須フィールド検証と絶対パス検証をかけて `PodManifest`
|
||
に変換する。
|
||
|
||
## パス解決
|
||
|
||
manifest 中のパス(`model.auth.file` / `scope.*.target` /
|
||
`compaction.model.auth.file`)は相対記述を許容する。相対パスは
|
||
**各層のベース基準**で層ごとに絶対化され、そのあとで cascade merge に
|
||
かかる。層をまたいだ相対の意味ブレ(user 層の `./keys` が project 層の
|
||
どこを指すのか曖昧)を避けるための設計。
|
||
|
||
| 層 | ベース |
|
||
|---|---|
|
||
| user manifest (`<config_dir>/manifest.toml`) | そのファイルの親ディレクトリ |
|
||
| project manifest (`<project>/.insomnia/manifest.toml`) | **プロジェクトルート**(`.insomnia/` の親)。`target = "."` がワークスペース全体を指すように |
|
||
| overlay(inline TOML・programmatic) | プロセスの `current_dir()` |
|
||
|
||
Pod の作業ディレクトリは manifest に含まれない。プロセス起動時の
|
||
`std::env::current_dir()` がそのまま Pod の pwd となるため、別の作業
|
||
ディレクトリで Pod を走らせたい場合は `cd` してから `insomnia-pod` を起動する
|
||
(または `SpawnPod` が子に対して行っているように、親プロセス側で
|
||
`Command::current_dir` を明示する)。
|
||
|
||
cascade merge 後の `TryFrom<PodManifestConfig>` では `ensure_absolute`
|
||
が不変条件チェックとしてだけ働く。相対パスが残っていれば上流の
|
||
resolve 段を取りこぼしている証拠なので `ResolveError::RelativePath` を
|
||
返す。
|
||
|
||
## 未知フィールドと型エラー
|
||
|
||
- **未知フィールド**: `tracing::warn!` を出して無視。将来バージョンアップで読めない
|
||
旧設定が出るとユーザー体験が悪いため、`#[serde(deny_unknown_fields)]` は使わない。
|
||
- **型ミスマッチ**: `max_tokens = "100"` のような型エラーは hard error として
|
||
resolve 失敗させる。ファイルパスと位置情報をエラーメッセージに含める。
|
||
|
||
---
|
||
|
||
## manifest.toml 例
|
||
|
||
### ユーザー層(最小)
|
||
|
||
`<config_dir>/manifest.toml`:
|
||
|
||
```toml
|
||
[model]
|
||
ref = "anthropic/claude-sonnet-4-6"
|
||
auth = { kind = "api_key", file = "/home/you/.config/insomnia/keys/anthropic" }
|
||
```
|
||
|
||
`ref = "<provider>/<model_id>"` はプロバイダ / モデルカタログを引く短縮形。
|
||
`scheme` / `base_url` / `model_id` は provider 側の宣言から引かれ、`auth` も
|
||
カタログの `auth_hint` を起点に解決する。ここでは env 既定(`INSOMNIA_API_KEY_ANTHROPIC`)
|
||
ではなく file から読みたいので `auth` だけ override している。詳細は
|
||
`crates/provider/README.md` と `resources/{providers,models}/builtin.toml` を参照。
|
||
|
||
### プロジェクト層(最小)
|
||
|
||
`<project>/.insomnia/manifest.toml`:
|
||
|
||
```toml
|
||
[[scope.allow]]
|
||
target = "/abs/path/to/project"
|
||
permission = "write"
|
||
|
||
[[scope.deny]]
|
||
target = "/abs/path/to/project/secrets"
|
||
permission = "read"
|
||
|
||
[compaction]
|
||
compact_threshold = 80000
|
||
```
|
||
|
||
### 全オプション例
|
||
|
||
```toml
|
||
[pod]
|
||
name = "reviewer"
|
||
|
||
# Form A: ref のみ(カタログから scheme / base_url / auth_hint / capability を全部引く)
|
||
# [model]
|
||
# ref = "anthropic/claude-sonnet-4-6"
|
||
#
|
||
# Form B: ref + 部分 override(ここで示している形)
|
||
# カタログ起点に個別フィールドだけ上書き。ref 指定時は scheme / model_id / auth は任意 override。
|
||
#
|
||
# Form C: 完全 inline(カタログ無視。実験用 / カタログに無いモデル)
|
||
# [model]
|
||
# scheme = "anthropic"
|
||
# model_id = "claude-sonnet-4-6"
|
||
# auth = { kind = "api_key", file = "..." }
|
||
[model]
|
||
ref = "anthropic/claude-sonnet-4-6"
|
||
base_url = "https://api.anthropic.com"
|
||
auth = { kind = "api_key", file = "/home/you/.config/insomnia/keys/anthropic" }
|
||
|
||
[worker]
|
||
instruction = "$user/reviewer"
|
||
max_tokens = 4096
|
||
max_turns = 50
|
||
temperature = 0.3
|
||
top_p = 0.9
|
||
top_k = 40
|
||
stop_sequences = ["\n\n", "</stop>"]
|
||
reasoning = "medium" # 文字列 = effort label / 整数 = thinking budget tokens。詳細は docs/reasoning.md
|
||
|
||
[worker.tool_output]
|
||
default_max_bytes = 65536
|
||
|
||
[worker.tool_output.per_tool]
|
||
Read = 131072
|
||
Grep = 8192
|
||
|
||
[worker.file_upload]
|
||
max_bytes = 262144
|
||
|
||
[[scope.allow]]
|
||
target = "/abs/path/to/project"
|
||
permission = "write"
|
||
|
||
[[scope.allow]]
|
||
target = "/abs/path/to/docs"
|
||
permission = "read"
|
||
recursive = false
|
||
|
||
[[scope.deny]]
|
||
target = "/abs/path/to/project/secrets"
|
||
permission = "write"
|
||
|
||
[permissions]
|
||
default_action = "allow" # allow | deny | ask
|
||
|
||
[[permissions.rule]]
|
||
tool = "Bash"
|
||
pattern = "rm *"
|
||
action = "deny"
|
||
|
||
[[permissions.rule]]
|
||
tool = "Write"
|
||
pattern = "*.env"
|
||
action = "deny"
|
||
|
||
[compaction]
|
||
prune_protected_tokens = 8000
|
||
prune_min_savings = 4096
|
||
compact_threshold = 80000
|
||
compact_request_threshold = 90000
|
||
compact_retained_tokens = 8000
|
||
compact_auto_read_budget = 8000
|
||
compact_worker_max_input_tokens = 50000
|
||
compact_worker_max_turns = 20
|
||
|
||
[compaction.model]
|
||
scheme = "gemini"
|
||
model_id = "gemini-2.0-flash"
|
||
auth = { kind = "api_key", file = "/home/you/.config/insomnia/keys/gemini" }
|
||
```
|
||
|
||
---
|
||
|
||
## `[worker]` 設定
|
||
|
||
`[worker]` は Pod 内の `llm_worker::RequestConfig` とターン制御へ渡す設定を持つ。
|
||
Provider ごとの wire 名の違い(OpenAI の `max_completion_tokens` /
|
||
Responses の `max_output_tokens` / Gemini の `generation_config` など)は
|
||
scheme 側が吸収する。
|
||
|
||
| key | 型 | 既定 | 内容 |
|
||
|---|---|---|---|
|
||
| `instruction` | `String` | `$insomnia/default` | システムプロンプト本体として使う prompt asset 参照 |
|
||
| `max_tokens` | `u32` | 未指定 | 1 request の最大出力 token。scheme が provider の該当 wire field に投影。scheme ごとのセマンティクス差は `docs/reasoning.md` |
|
||
| `max_turns` | `NonZeroU32` | 未指定 | 1 run 内で Worker が進められる最大 turn 数 |
|
||
| `temperature` | `f32` | 未指定 | sampling temperature |
|
||
| `top_p` | `f32` | 未指定 | nucleus sampling |
|
||
| `top_k` | `u32` | 未指定 | top-k sampling。未対応 scheme では warning または provider 側挙動に任せる |
|
||
| `stop_sequences` | `Vec<String>` | `[]` | stop sequence。cascade では上層指定が配列ごと置換する |
|
||
| `reasoning` | `String` または `i32` | 未指定 | reasoning / thinking 制御。詳細は `docs/reasoning.md` |
|
||
| `tool_output.default_max_bytes` | `usize` | `65536` | tool result `content` の既定 byte cap |
|
||
| `tool_output.per_tool` | `Map<String, usize>` | `{}` | tool 名ごとの byte cap override |
|
||
| `file_upload.max_bytes` | `usize` | `262144` | submit 時の FileRef (`@<path>`) upload / attachment の byte cap |
|
||
|
||
生成設定は provider 別の値域検証を行わない。型が TOML と合わない場合は manifest
|
||
parse error になるが、provider が受け付けない値や組み合わせは API 応答で検出する。
|
||
|
||
## `[permissions]` 設定
|
||
|
||
`[permissions]` が無い場合、ツール permission 層は無効で従来通り実行する。`[permissions]` を書く場合は `default_action = "allow" | "deny" | "ask"` が必須で、`[[permissions.rule]]` は宣言順に最初に一致した rule が採用される。一致しなければ `default_action` を使う。
|
||
|
||
```toml
|
||
[permissions]
|
||
default_action = "allow"
|
||
|
||
[[permissions.rule]]
|
||
tool = "Bash"
|
||
pattern = "rm *"
|
||
action = "deny"
|
||
```
|
||
|
||
`tool` は実行時に登録されているツール名(`Bash`, `Read`, `Write`, `Edit`, `Glob`, `Grep` 等)に対して大小文字を無視して照合する。`pattern` は built-in tool では主に `command` / `file_path` / `path` / `pattern` 引数に対する `*` / `?` ワイルドカードとして評価される。
|
||
|
||
`allow` は通常実行、`deny` はその tool call を実行せず `is_error = true` の synthetic tool result を履歴へ追加してターンを継続する。`ask` は型として受け付けるが、承認 protocol は未実装のため現在は headless に待機せず fail-closed(synthetic error result)になる。
|
||
|
||
## instruction とプロンプト資産
|
||
|
||
### `worker.instruction` フィールド
|
||
|
||
Pod のシステムプロンプトの**本体**として使うプロンプト資産への参照。
|
||
import-map 形式のプレフィックスで指定する:
|
||
|
||
| プレフィックス | 解決先 |
|
||
|---|---|
|
||
| `$insomnia` | バイナリ同梱の `resources/prompts/`(`include_dir!`) |
|
||
| `$user` | `<config_dir>/prompts/`(`manifest::paths` で解決) |
|
||
| `$workspace` | `<project>/.insomnia/prompts/` |
|
||
|
||
- `.md` 拡張子は省略する(例: `$insomnia/default` → `resources/prompts/default.md`)
|
||
- 省略時のデフォルト値は `$insomnia/default`(`defaults::DEFAULT_INSTRUCTION`)
|
||
- 指定した prefix の dir に該当ファイルが無ければ **hard error**(fallthrough しない)
|
||
|
||
### ビルトインプロンプト
|
||
|
||
`resources/prompts/` 以下に同梱:
|
||
|
||
| 名前 | 用途 |
|
||
|---|---|
|
||
| `default` | デフォルトの instruction 本体。workspace / tool-usage をインクルード |
|
||
| `common/workspace` | cwd・日付の注入 |
|
||
| `common/tool-usage` | ツール使用の共通ガイダンス |
|
||
|
||
### `{% include %}` の相対解決
|
||
|
||
テンプレート内で `{% include "name" %}` のようにプレフィックス無しで書いた場合、
|
||
**include を書いたファイル自身のプレフィックスとディレクトリ**からの相対で解決する:
|
||
|
||
- `$insomnia/default.md` 内の `{% include "common/workspace" %}` → `$insomnia/common/workspace`
|
||
- `$user/custom.md` 内の `{% include "$insomnia/common/tool-usage" %}` → 明示的プレフィックスが優先
|
||
|
||
### システムプロンプトの最終構造
|
||
|
||
`instruction` テンプレートのレンダリング結果に、Rust 側で以下の**固定セクション**を付加する。
|
||
ユーザーテンプレートからは触れない領域:
|
||
|
||
```
|
||
<instruction のレンダ結果>
|
||
|
||
---
|
||
## Working boundaries
|
||
|
||
<scope.summary()>
|
||
|
||
--- ← AGENTS.md が不在なら省略
|
||
## Project instructions (AGENTS.md)
|
||
|
||
<AGENTS.md 本文> ← AGENTS.md が不在なら省略
|
||
```
|
||
|
||
- scope セクションは**必ず**出力される
|
||
- AGENTS.md セクションは不在時に区切り `---` ごと省略
|
||
|
||
---
|
||
|
||
## `insomnia-pod` CLI
|
||
|
||
`insomnia-pod` は通常、builtin default → user manifest → project manifest → overlay の cascade で manifest を解決して起動する。
|
||
|
||
```
|
||
insomnia-pod [--project <path>] [--overlay <toml>] [-s/--store <path>] [--session <uuid>]
|
||
```
|
||
|
||
| フラグ | 説明 |
|
||
|---|---|
|
||
| `--project <path>` | プロジェクト manifest 探索の起点。省略時は cwd から上方向に `.insomnia/manifest.toml` を探索 |
|
||
| `--overlay <toml>` | 最上層の overlay を inline TOML 文字列で渡す(例: `--overlay 'worker.instruction = "$user/foo"'`) |
|
||
| `-s, --store <path>` | セッション永続化ディレクトリ(デフォルト: `<data_dir>/sessions/`、`manifest::paths` で解決) |
|
||
| `--session <uuid>` | 既存 session id から Pod を復元し、同じ jsonl に後続 turn を追記する |
|
||
|
||
user manifest は CLI フラグではなく、以下の規則で解決する。
|
||
|
||
| 入力 | 挙動 |
|
||
|---|---|
|
||
| `INSOMNIA_USER_MANIFEST=<path>` | 指定 path を user manifest として読む。ファイル不在や parse error は起動エラー |
|
||
| `INSOMNIA_USER_MANIFEST=` | 空文字列は未指定扱い |
|
||
| env 未指定 | `manifest::paths::user_manifest_path()` で自動探索し、存在すれば読む |
|
||
|
||
単一ファイルだけで起動したい場合は cascade を使わず、`--manifest` を指定する。
|
||
|
||
```
|
||
insomnia-pod --manifest <path> [-s/--store <path>] [--session <uuid>]
|
||
```
|
||
|
||
`--manifest` は指定 TOML 1 枚だけを `PodManifest::from_toml` で読み、user / project / overlay layer は一切読まない。したがって `--project`、`--overlay`、非空の `INSOMNIA_USER_MANIFEST` とは併用不可。
|
||
|
||
spawn 子 Pod 用の内部フラグとして `--adopt` と `--callback <path>` がある。これらは `SpawnPod` が scope allocation と親 callback socket を引き継がせるために使うもので、通常の手動起動では使わない。
|
||
|
||
Pod の作業ディレクトリは `insomnia-pod` 起動時の cwd が直接使われる。別ディレクトリで
|
||
動かしたい場合は `cd <path> && insomnia-pod ...` のように外側で `cd` してから起動する。
|
||
|
||
引数無しで起動すると、cwd + `manifest::paths` の自動解決だけで動く最小構成になる
|
||
(overlay 無し、プロジェクトに `.insomnia/manifest.toml` があればそれを使う)。
|
||
|
||
---
|
||
|
||
## プログラマティック API
|
||
|
||
```rust
|
||
use pod::{Pod, PodFactory};
|
||
|
||
let (manifest, loader) = PodFactory::new()
|
||
.with_user_manifest_auto()? // manifest::paths から自動読み込み、不在 OK
|
||
.with_project_manifest_auto()? // cwd から上方向に .insomnia/ を探索、不在 OK
|
||
.with_overlay_toml(overlay)? // programmatic な最上層 overlay
|
||
.resolve()?; // -> (PodManifest, PromptLoader)
|
||
|
||
let pod = Pod::from_manifest(manifest, store, loader).await?;
|
||
```
|
||
|
||
`Pod::from_manifest_toml(toml, store)` は単層 manifest を TOML 文字列で直接投げる
|
||
便利関数(テスト・デバッグ向け)。builtins-only のプロンプトローダで動く。
|