merge: pod orchestration guidance

This commit is contained in:
Keisuke Hirata 2026-06-01 10:24:33 +09:00
commit b7cbf05305
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
/// workflow advertises `model_invokation: true`.
ResidentWorkflowsSection,
/// Trailing Pod orchestration guidance, appended when registered tools
/// include Pod-management capabilities.
PodOrchestrationGuidanceSection,
/// LLM-facing description for the SpawnPod tool, including discovered
/// profile selectors.
SpawnPodToolDescription,
@ -111,6 +114,7 @@ impl PodPrompt {
Self::ResidentMemorySummarySection => "resident_memory_summary_section",
Self::ResidentKnowledgeSection => "resident_knowledge_section",
Self::ResidentWorkflowsSection => "resident_workflows_section",
Self::PodOrchestrationGuidanceSection => "pod_orchestration_guidance_section",
Self::SpawnPodToolDescription => "spawn_pod_tool_description",
}
}
@ -130,6 +134,7 @@ impl PodPrompt {
PodPrompt::ResidentMemorySummarySection,
PodPrompt::ResidentKnowledgeSection,
PodPrompt::ResidentWorkflowsSection,
PodPrompt::PodOrchestrationGuidanceSection,
PodPrompt::SpawnPodToolDescription,
];
@ -145,6 +150,7 @@ impl PodPrompt {
"resident_memory_summary_section",
"resident_knowledge_section",
"resident_workflows_section",
"pod_orchestration_guidance_section",
"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`.
pub fn spawn_pod_tool_description(
&self,
@ -715,6 +726,19 @@ compact_system = "PREFIX\n{% include \"$insomnia/internal/compact_system\" %}"
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]
fn spawn_pod_tool_description_renders_profile_block() {
let cat = PromptCatalog::builtins_only().unwrap();

View File

@ -7,10 +7,11 @@
//! eagerly syntax-checks it at Pod construction. The final system
//! prompt is materialised exactly once just before the first LLM turn:
//! the rendered body is appended with a fixed trailing section carrying
//! the Pod's `Scope` summary and (if present) the project's `AGENTS.md`
//! contents plus resident memory sections, and the whole string is handed
//! to the Worker via `set_system_prompt`. Subsequent turns and compactions
//! reuse that materialised string verbatim.
//! the Pod's `Scope` summary, (if present) the project's `AGENTS.md`
//! contents, resident memory sections, and conditional Pod-orchestration
//! guidance, then the whole string is handed to the Worker via
//! `set_system_prompt`. Subsequent turns and compactions reuse that
//! materialised string verbatim.
use std::collections::BTreeMap;
use std::path::Path;
@ -216,6 +217,12 @@ struct ToolCapabilities {
memory_write: bool,
memory_edit: 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 {
@ -229,6 +236,12 @@ impl ToolCapabilities {
"MemoryWrite" => capabilities.memory_write = true,
"MemoryEdit" => capabilities.memory_edit = 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
}
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 {
let mut map: BTreeMap<&'static str, Value> = BTreeMap::new();
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_delete", Value::from(self.memory_delete));
map.insert("memory_mutation", Value::from(self.memory_mutation()));
map.insert("pod_management", Value::from(self.pod_management()));
Value::from(map)
}
}
@ -329,6 +352,12 @@ fn append_trailing_section(
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
// regardless of how individual templates chose to end.
while out.ends_with('\n') || out.ends_with(' ') {
@ -496,6 +525,20 @@ mod tests {
.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
/// tests, so every `ctx()` can hand out a `&'static PromptCatalog`
/// reference without forcing test bodies to create one per call.
@ -586,6 +629,46 @@ mod tests {
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]
fn instruction_prefix_addressing_user() {
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 }}\
"""
pod_orchestration_guidance_section = "{% include \"$insomnia/common/pod-orchestration\" %}"
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.