merge: pod orchestration guidance
This commit is contained in:
commit
b7cbf05305
|
|
@ -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();
|
||||||
|
|
|
||||||
|
|
@ -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 }}");
|
||||||
|
|
|
||||||
10
resources/prompts/common/pod-orchestration.md
Normal file
10
resources/prompts/common/pod-orchestration.md
Normal 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.
|
||||||
|
|
@ -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.
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue
Block a user