yoi/docs/pod-factory.md

17 KiB
Raw Blame History

Pod Factory: カスケード設定とプロンプト資産

PodFactory は、複数の層に分かれた manifest.toml とプログラマティック overlay をマージして、検証済みの PodManifestPromptLoader を生成する ビルダー。これにより 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.rspub const として集約 されており、serde #[default = "..."] 経路(PodManifest の直接 deserializeTryFrom<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 = "." がワークスペース全体を指すように
overlayinline 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:

[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.mdresources/{providers,models}/builtin.toml を参照。

プロジェクト層(最小)

<project>/.insomnia/manifest.toml:

[[scope.allow]]
target = "/abs/path/to/project"
permission = "write"

[[scope.deny]]
target = "/abs/path/to/project/secrets"
permission = "read"

[compaction]
compact_threshold = 80000

全オプション例

[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"

[web]
enabled = true

[web.search]
provider = "brave"
api_key_env = "BRAVE_SEARCH_API_KEY"
timeout_secs = 15

[web.fetch]
timeout_secs = 20
redirect_limit = 5
max_response_bytes = 2097152
max_output_bytes = 65536

[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 応答で検出する。

[web] 設定

WebSearch / WebFetch は通常の built-in function tool として登録されるが、manifest で明示的に有効化されるまでネットワークアクセスしない。無効または未設定の場合、tool call は「設定されていない」旨の明示的なエラーを返す。

[web]
enabled = true

[web.search]
provider = "brave"
api_key_env = "BRAVE_SEARCH_API_KEY" # API key は env 参照に置き、manifest に raw secret を書かない
timeout_secs = 15

[web.fetch]
timeout_secs = 20
redirect_limit = 5
max_response_bytes = 2097152
max_output_bytes = 65536

WebSearch の最初の provider は Brave Search APIhttps://api.search.brave.com/res/v1/web/search)で、入力は query と任意の limit / offset。Brave の制約に合わせて query は 400 文字 / 50 words まで、limit は 1-20、offset は 0-9 に制限される。timeout_secs を省略した場合は安全な既定値が使われ、provider response は固定上限内で読み込まれる。

WebFetch は http/https URL のみを fetch し、timeout・redirect・response/output byte limit を適用する。localhost / private / link-local などの host/IP は fetch 前と各 redirect で拒否される。テストや明示的に信頼した環境では [web] allow_private_addresses = true または [web.fetch] allow_private_addresses = true を指定できる。

[permissions] 設定

[permissions] が無い場合、ツール permission 層は無効で従来通り実行する。[permissions] を書く場合は default_action = "allow" | "deny" | "ask" が必須で、[[permissions.rule]] は宣言順に最初に一致した rule が採用される。一致しなければ default_action を使う。

[permissions]
default_action = "allow"

[[permissions.rule]]
tool = "Bash"
pattern = "rm *"
action = "deny"

tool は実行時に登録されているツール名(Bash, Read, Write, Edit, Glob, Grep, WebSearch, WebFetch 等)に対して大小文字を無視して照合する。pattern は built-in tool では主に command / file_path / path / pattern / query / url 引数に対する * / ? ワイルドカードとして評価される。

allow は通常実行、deny はその tool call を実行せず is_error = true の synthetic tool result を履歴へ追加してターンを継続する。ask は型として受け付けるが、承認 protocol は未実装のため現在は headless に待機せず fail-closedsynthetic 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/defaultresources/prompts/default.md
  • 省略時のデフォルト値は $insomnia/defaultdefaults::DEFAULT_INSTRUCTION
  • 指定した prefix の dir に該当ファイルが無ければ hard errorfallthrough しない)

ビルトインプロンプト

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

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 のプロンプトローダで動く。