prompt: add pod orchestration guidance

This commit is contained in:
Keisuke Hirata 2026-06-01 10:19:49 +09:00
parent 5af9a5b1b6
commit 6437580600
No known key found for this signature in database
4 changed files with 123 additions and 4 deletions

View File

@ -92,6 +92,9 @@ pub enum PodPrompt {
/// knowledge when Workflow resident injection is enabled and at least one /// knowledge when Workflow resident injection is enabled and at least one
/// workflow advertises `model_invokation: true`. /// workflow advertises `model_invokation: true`.
ResidentWorkflowsSection, ResidentWorkflowsSection,
/// Trailing Pod orchestration guidance, appended when registered tools
/// include Pod-management capabilities.
PodOrchestrationGuidanceSection,
/// LLM-facing description for the SpawnPod tool, including discovered /// LLM-facing description for the SpawnPod tool, including discovered
/// profile selectors. /// profile selectors.
SpawnPodToolDescription, SpawnPodToolDescription,
@ -111,6 +114,7 @@ impl PodPrompt {
Self::ResidentMemorySummarySection => "resident_memory_summary_section", Self::ResidentMemorySummarySection => "resident_memory_summary_section",
Self::ResidentKnowledgeSection => "resident_knowledge_section", Self::ResidentKnowledgeSection => "resident_knowledge_section",
Self::ResidentWorkflowsSection => "resident_workflows_section", Self::ResidentWorkflowsSection => "resident_workflows_section",
Self::PodOrchestrationGuidanceSection => "pod_orchestration_guidance_section",
Self::SpawnPodToolDescription => "spawn_pod_tool_description", Self::SpawnPodToolDescription => "spawn_pod_tool_description",
} }
} }
@ -130,6 +134,7 @@ impl PodPrompt {
PodPrompt::ResidentMemorySummarySection, PodPrompt::ResidentMemorySummarySection,
PodPrompt::ResidentKnowledgeSection, PodPrompt::ResidentKnowledgeSection,
PodPrompt::ResidentWorkflowsSection, PodPrompt::ResidentWorkflowsSection,
PodPrompt::PodOrchestrationGuidanceSection,
PodPrompt::SpawnPodToolDescription, PodPrompt::SpawnPodToolDescription,
]; ];
@ -145,6 +150,7 @@ impl PodPrompt {
"resident_memory_summary_section", "resident_memory_summary_section",
"resident_knowledge_section", "resident_knowledge_section",
"resident_workflows_section", "resident_workflows_section",
"pod_orchestration_guidance_section",
"spawn_pod_tool_description", "spawn_pod_tool_description",
]; ];
} }
@ -402,6 +408,11 @@ impl PromptCatalog {
) )
} }
/// Render `PodPrompt::PodOrchestrationGuidanceSection` (no inputs).
pub fn pod_orchestration_guidance_section(&self) -> Result<String, CatalogError> {
self.render(PodPrompt::PodOrchestrationGuidanceSection, Value::UNDEFINED)
}
/// Render `PodPrompt::SpawnPodToolDescription`. /// Render `PodPrompt::SpawnPodToolDescription`.
pub fn spawn_pod_tool_description( pub fn spawn_pod_tool_description(
&self, &self,
@ -715,6 +726,19 @@ compact_system = "PREFIX\n{% include \"$insomnia/internal/compact_system\" %}"
assert!(rendered.contains("write_summary")); assert!(rendered.contains("write_summary"));
} }
#[test]
fn pod_orchestration_guidance_section_renders_resource_body() {
let cat = PromptCatalog::builtins_only().unwrap();
let rendered = cat.pod_orchestration_guidance_section().unwrap();
assert!(rendered.contains("## Pod orchestration"));
assert!(rendered.contains("spawned Pod notifications are background signals"));
assert!(rendered.contains("does not need to keep a turn open"));
assert!(rendered.contains("Do not use `sleep` or polling loops"));
assert!(rendered.contains("worktree status, diff, and test results"));
assert!(rendered.contains("not scheduler or auto-maintain authorization"));
assert!(rendered.contains("bypass user/workflow authorization"));
}
#[test] #[test]
fn spawn_pod_tool_description_renders_profile_block() { fn spawn_pod_tool_description_renders_profile_block() {
let cat = PromptCatalog::builtins_only().unwrap(); let cat = PromptCatalog::builtins_only().unwrap();

View File

@ -7,10 +7,11 @@
//! eagerly syntax-checks it at Pod construction. The final system //! eagerly syntax-checks it at Pod construction. The final system
//! prompt is materialised exactly once just before the first LLM turn: //! prompt is materialised exactly once just before the first LLM turn:
//! the rendered body is appended with a fixed trailing section carrying //! the rendered body is appended with a fixed trailing section carrying
//! the Pod's `Scope` summary and (if present) the project's `AGENTS.md` //! the Pod's `Scope` summary, (if present) the project's `AGENTS.md`
//! contents plus resident memory sections, and the whole string is handed //! contents, resident memory sections, and conditional Pod-orchestration
//! to the Worker via `set_system_prompt`. Subsequent turns and compactions //! guidance, then the whole string is handed to the Worker via
//! reuse that materialised string verbatim. //! `set_system_prompt`. Subsequent turns and compactions reuse that
//! materialised string verbatim.
use std::collections::BTreeMap; use std::collections::BTreeMap;
use std::path::Path; use std::path::Path;
@ -216,6 +217,12 @@ struct ToolCapabilities {
memory_write: bool, memory_write: bool,
memory_edit: bool, memory_edit: bool,
memory_delete: bool, memory_delete: bool,
pod_spawn: bool,
pod_send: bool,
pod_read_output: bool,
pod_stop: bool,
pod_list: bool,
pod_restore: bool,
} }
impl ToolCapabilities { impl ToolCapabilities {
@ -229,6 +236,12 @@ impl ToolCapabilities {
"MemoryWrite" => capabilities.memory_write = true, "MemoryWrite" => capabilities.memory_write = true,
"MemoryEdit" => capabilities.memory_edit = true, "MemoryEdit" => capabilities.memory_edit = true,
"MemoryDelete" => capabilities.memory_delete = true, "MemoryDelete" => capabilities.memory_delete = true,
"SpawnPod" => capabilities.pod_spawn = true,
"SendToPod" => capabilities.pod_send = true,
"ReadPodOutput" => capabilities.pod_read_output = true,
"StopPod" => capabilities.pod_stop = true,
"ListPods" => capabilities.pod_list = true,
"RestorePod" => capabilities.pod_restore = true,
_ => {} _ => {}
} }
} }
@ -251,6 +264,15 @@ impl ToolCapabilities {
self.memory_write || self.memory_edit || self.memory_delete self.memory_write || self.memory_edit || self.memory_delete
} }
fn pod_management(self) -> bool {
self.pod_spawn
|| self.pod_send
|| self.pod_read_output
|| self.pod_stop
|| self.pod_list
|| self.pod_restore
}
fn to_minijinja_value(self) -> Value { fn to_minijinja_value(self) -> Value {
let mut map: BTreeMap<&'static str, Value> = BTreeMap::new(); let mut map: BTreeMap<&'static str, Value> = BTreeMap::new();
map.insert("memory_any", Value::from(self.memory_any())); map.insert("memory_any", Value::from(self.memory_any()));
@ -262,6 +284,7 @@ impl ToolCapabilities {
map.insert("memory_edit", Value::from(self.memory_edit)); map.insert("memory_edit", Value::from(self.memory_edit));
map.insert("memory_delete", Value::from(self.memory_delete)); map.insert("memory_delete", Value::from(self.memory_delete));
map.insert("memory_mutation", Value::from(self.memory_mutation())); map.insert("memory_mutation", Value::from(self.memory_mutation()));
map.insert("pod_management", Value::from(self.pod_management()));
Value::from(map) Value::from(map)
} }
} }
@ -329,6 +352,12 @@ fn append_trailing_section(
out.push('\n'); out.push('\n');
} }
} }
if tool_capabilities.pod_management() {
out.push('\n');
let section = prompts.pod_orchestration_guidance_section()?;
out.push_str(section.trim_end_matches(&['\n', ' '][..]));
out.push('\n');
}
// Canonicalise the tail so the emitted prompt has a single form // Canonicalise the tail so the emitted prompt has a single form
// regardless of how individual templates chose to end. // regardless of how individual templates chose to end.
while out.ends_with('\n') || out.ends_with(' ') { while out.ends_with('\n') || out.ends_with(' ') {
@ -496,6 +525,20 @@ mod tests {
.collect() .collect()
} }
fn pod_management_tool_names() -> Vec<String> {
[
"SpawnPod",
"SendToPod",
"ReadPodOutput",
"StopPod",
"ListPods",
"RestorePod",
]
.into_iter()
.map(String::from)
.collect()
}
/// Lazily-initialised builtin catalog shared across system-prompt /// Lazily-initialised builtin catalog shared across system-prompt
/// tests, so every `ctx()` can hand out a `&'static PromptCatalog` /// tests, so every `ctx()` can hand out a `&'static PromptCatalog`
/// reference without forcing test bodies to create one per call. /// reference without forcing test bodies to create one per call.
@ -586,6 +629,46 @@ mod tests {
assert!(!rendered.contains("MemoryDelete")); assert!(!rendered.contains("MemoryDelete"));
} }
#[test]
fn pod_orchestration_guidance_is_included_for_pod_management_tools() {
let loader = PromptLoader::builtins_only();
let tmpl = SystemPromptTemplate::parse("$insomnia/default", loader).unwrap();
let dir = TempDir::new().unwrap();
let scope = build_scope(dir.path());
let rendered = tmpl
.render(&ctx(dir.path(), &scope, pod_management_tool_names(), None))
.unwrap();
assert!(rendered.contains("## Pod orchestration"));
assert!(rendered.contains("spawned Pod notifications are background signals"));
assert!(rendered.contains("does not need to keep a turn open"));
assert!(rendered.contains("Do not use `sleep` or polling loops"));
assert!(rendered.contains("worktree status, diff, and test results"));
assert!(rendered.contains("not scheduler or auto-maintain authorization"));
assert!(rendered.contains("bypass user/workflow authorization"));
}
#[test]
fn pod_orchestration_guidance_is_omitted_without_pod_management_tools() {
let loader = PromptLoader::builtins_only();
let tmpl = SystemPromptTemplate::parse("$insomnia/default", loader).unwrap();
let dir = TempDir::new().unwrap();
let scope = build_scope(dir.path());
let rendered = tmpl
.render(&ctx(
dir.path(),
&scope,
vec!["Read".into(), "Edit".into(), "MemoryRead".into()],
None,
))
.unwrap();
assert!(!rendered.contains("## Pod orchestration"));
assert!(!rendered.contains("spawned Pod notifications are background signals"));
assert!(!rendered.contains("does not need to keep a turn open"));
assert!(!rendered.contains("Do not use `sleep` or polling loops"));
}
#[test] #[test]
fn instruction_prefix_addressing_user() { fn instruction_prefix_addressing_user() {
let (_tmp, loader) = user_loader_with("greet.md", "HELLO from {{ cwd }}"); let (_tmp, loader) = user_loader_with("greet.md", "HELLO from {{ cwd }}");

View File

@ -0,0 +1,10 @@
---
## Pod orchestration
When Pod-management tools are available, spawned Pod notifications are background signals for the parent to handle at a natural stopping point. Do not ignore routine follow-up, but do not interrupt the current user request unnecessarily.
The parent does not need to keep a turn open or call tools solely to wait for a notification. Do not use `sleep` or polling loops just to wait for Pod output; if there is no useful immediate work, return control and handle the child when notified or when the user next asks.
Before treating delegated work as complete, read the child output and inspect concrete evidence such as worktree status, diff, and test results. Notifications are hints, not proof of completion.
This guidance is not scheduler or auto-maintain authorization. Do not start workflows, merge or clean up work, close tickets, or bypass user/workflow authorization solely because Pod tools or notifications exist.

View File

@ -66,6 +66,8 @@ The following workflows are advertised resident. When a user request matches one
{{ entries }}\ {{ entries }}\
""" """
pod_orchestration_guidance_section = "{% include \"$insomnia/common/pod-orchestration\" %}"
spawn_pod_tool_description = """\ spawn_pod_tool_description = """\
Spawn a new Pod process to work on a delegated task. The spawner's write scope is reduced by the scope passed here; the spawned Pod receives its own socket and starts running `task` immediately. The spawned Pod outlives the spawner's current turn and can be contacted again through its socket path. Spawn a new Pod process to work on a delegated task. The spawner's write scope is reduced by the scope passed here; the spawned Pod receives its own socket and starts running `task` immediately. The spawned Pod outlives the spawner's current turn and can be contacted again through its socket path.