From 6437580600c6becc90a2d69b63974cbe8bb5e0e8 Mon Sep 17 00:00:00 2001 From: Hare Date: Mon, 1 Jun 2026 10:19:49 +0900 Subject: [PATCH] prompt: add pod orchestration guidance --- crates/pod/src/prompt/catalog.rs | 24 +++++ crates/pod/src/prompt/system.rs | 91 ++++++++++++++++++- resources/prompts/common/pod-orchestration.md | 10 ++ resources/prompts/internal.toml | 2 + 4 files changed, 123 insertions(+), 4 deletions(-) create mode 100644 resources/prompts/common/pod-orchestration.md diff --git a/crates/pod/src/prompt/catalog.rs b/crates/pod/src/prompt/catalog.rs index a12c8a84..083de98a 100644 --- a/crates/pod/src/prompt/catalog.rs +++ b/crates/pod/src/prompt/catalog.rs @@ -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 { + 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(); diff --git a/crates/pod/src/prompt/system.rs b/crates/pod/src/prompt/system.rs index 336ddcd1..81f3f5b9 100644 --- a/crates/pod/src/prompt/system.rs +++ b/crates/pod/src/prompt/system.rs @@ -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 { + [ + "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 }}"); diff --git a/resources/prompts/common/pod-orchestration.md b/resources/prompts/common/pod-orchestration.md new file mode 100644 index 00000000..06b8bdd1 --- /dev/null +++ b/resources/prompts/common/pod-orchestration.md @@ -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. diff --git a/resources/prompts/internal.toml b/resources/prompts/internal.toml index caffc1c3..ae42c6ac 100644 --- a/resources/prompts/internal.toml +++ b/resources/prompts/internal.toml @@ -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.