# システムプロンプトテンプレート Pod のシステムプロンプトは minijinja テンプレート。first turn 直前に1回だけ render され、以降のターンおよび compact 後も同じ文字列を使い続ける。 ## マニフェスト記法 既存の `[worker].system_prompt` をそのままテンプレート文字列として解釈する。新フィールドは無い。 ```toml [worker] system_prompt = """ You are operating in {{ cwd }}. Today is {{ date }}. {{ scope.summary }} Available tools: {{ tools | join(", ") }} {% if files.agents_md is defined -%} {{ files.agents_md }} {%- endif %} """ ``` 構文は minijinja (Jinja2 互換) のサブセット。未定義変数の参照は `UndefinedBehavior::Strict` により render エラーになる。`{{` のリテラル出力は `{{ '{{' }}` で逃がす。 ## 組み込み変数 | キー | 型 | 内容 | |---|---|---| | `date` | string | `YYYY-MM-DD` (UTC) | | `time` | string | `HH:MM:SS` (UTC) | | `datetime` | string | RFC3339 (UTC, 秒精度) | | `cwd` | string | Pod の絶対 pwd | | `scope.readable` | list\ | allow ルールの全パス (Read 以上) | | `scope.writable` | list\ | allow ルールのうち Write のパス | | `scope.summary` | string | "Readable:\n - ...\nWritable:\n - ..." 形式の整形済み文字列 | | `tools` | list\ | 登録済みツール名の sort 済み一覧 | | `files` | map\ | 外部ファイル。AGENTS.md 等の供給先として予約。空の場合もあるので `is defined` でガードする | `files` は常に存在する (本体が空 Map のことはあっても `files` 自体が未定義にはならない) が、個別キー (`files.agents_md` 等) は供給元次第で未定義になり得る。 ## 評価モデル テンプレートの実体化 (render) は遅延評価。プロジェクト内の他の遅延初期化パターン (tool factory / hook builder) と同じ形に揃えている。 ### タイミング 1. `Pod::from_manifest` 時点: テンプレート文字列を `SystemPromptTemplate::parse` で **構文検査のみ** 行い、`Pod.system_prompt_template: Option` に保持する。この時点で Worker 側の system_prompt は `None`、segment log の head もまだ作られない。 2. `Pod::run` / `Pod::resume` 初回呼び出し冒頭 (`ensure_system_prompt_materialized`): 1. `worker.tool_server_handle().flush_pending()` で pending な tool factory を materialize して tool 名を確定させる 2. 現在時刻・cwd・scope・tool 名を集めて `SystemPromptContext` を作る 3. `template.render(ctx)` で文字列化して `worker.set_system_prompt(rendered)` を呼ぶ 4. `Pod.system_prompt_template = None` (`Option::take()` で構造的に1回性を保証) 3. その直後の `ensure_segment_head` が現在 segment の entry count を見て初回なら `SegmentStart` を append し、materialize 後の system_prompt を segment log に焼き付ける。 ### 1回性の保証 - `Pod.system_prompt_template` は `Option` で、materialize 時に `take()` する。2 ターン目以降はフィールドが `None` なので `ensure_system_prompt_materialized` は早期 return し、再 render は発生しない。 - compact は Worker の system_prompt フィールドを触らない (pod.rs の `compact` は `w.get_system_prompt()` を読み取って新 segment に引き継ぐだけ)。そのため compact 前後で同じ文字列が流れ続ける。 - 統合テスト `compact_preserves_system_prompt` が実装で直接検証している。 ### 責務分離 テンプレート機構は **Pod 層** に閉じる。`llm-worker` はテンプレートの存在を知らず、`Worker::set_system_prompt(String)` で render 済みの文字列を受け取るだけ。llm-worker 側に入った唯一の変更は `ToolServerHandle::flush_pending` を `pub` に昇格させたこと (tool 名を先取りするため)。 ## エラー処理 - **構文エラー**: `Pod::from_manifest` の parse 段階で検出 → `PodError::InvalidSystemPromptTemplate { source: SystemPromptError::Parse }` で Pod の生成自体が失敗する。 - **render エラー** (未定義変数など): first turn 直前の `ensure_system_prompt_materialized` で検出 → `PodError::SystemPromptRender { source: SystemPromptError::Render }` で初回 `Pod::run` が失敗する。 - フォールバックはしない。どちらも fail-fast で Pod 起動を止める。 ## 供給元の拡張 `SystemPromptContext.files: BTreeMap` は本チケットの範囲では常に空だが、key 空間として予約してある。AGENTS.md 取り込み (別チケット) では `ensure_system_prompt_materialized` 内で `files` を埋めるだけで拡張できる。テンプレート側・エンジン側の変更は不要。 ## 関連ファイル - `crates/pod/src/system_prompt.rs` — `SystemPromptTemplate` / `SystemPromptContext` / `SystemPromptError` - `crates/pod/src/pod.rs` — `Pod.system_prompt_template` フィールド、`ensure_system_prompt_materialized`、`ensure_segment_head` の初回 append ロジック - `crates/manifest/src/scope.rs` — `Scope::summary` / `readable_paths` / `writable_paths` - `crates/session-store/src/segment_log.rs` — `LogEntry::SegmentStart` / segment replay entries - `crates/llm-worker/src/tool_server.rs` — `ToolServerHandle::flush_pending` (pub)