agents: phase tracking, pid files, pipelining, unified cycle
- AgentStep with phase labels (=== PROMPT phase:name ===)
- PID files in state dir (pid-{PID} with JSON phase/timestamp)
- Built-in bail check: between steps, bail if other pid files exist
- surface_observe_cycle replaces surface_agent_cycle + journal_agent_cycle
- Reads surface output from state dir instead of parsing stdout
- Pipelining: starts new agent if running one is past surface phase
- link_set upserts (creates link if missing)
- Better error message for context window overflow
Co-Authored-By: Kent Overstreet <kent.overstreet@linux.dev>
This commit is contained in:
parent
11289667f5
commit
e20aeeeabe
8 changed files with 256 additions and 178 deletions
|
|
@ -26,12 +26,20 @@ use std::path::PathBuf;
|
|||
|
||||
/// Agent definition: config (from JSON header) + prompt (raw markdown body).
|
||||
#[derive(Clone, Debug)]
|
||||
/// A single step in a multi-step agent.
|
||||
pub struct AgentStep {
|
||||
pub prompt: String,
|
||||
/// Phase label for PID file tracking (e.g. "surface", "observe").
|
||||
/// Parsed from `=== PROMPT phase:name ===` or auto-generated as "step-N".
|
||||
pub phase: String,
|
||||
}
|
||||
|
||||
pub struct AgentDef {
|
||||
pub agent: String,
|
||||
pub query: String,
|
||||
/// Prompt steps — single-step agents have one entry, multi-step have several.
|
||||
/// Steps — single-step agents have one entry, multi-step have several.
|
||||
/// Steps are separated by `=== PROMPT ===` in the .agent file.
|
||||
pub prompts: Vec<String>,
|
||||
pub steps: Vec<AgentStep>,
|
||||
pub model: String,
|
||||
pub schedule: String,
|
||||
pub tools: Vec<String>,
|
||||
|
|
@ -70,28 +78,64 @@ struct AgentHeader {
|
|||
fn default_model() -> String { "sonnet".into() }
|
||||
|
||||
/// Parse an agent file: first line is JSON config, rest is the prompt(s).
|
||||
/// Multiple prompts are separated by `=== PROMPT ===` lines.
|
||||
/// Multiple prompts are separated by `=== PROMPT [phase:name] ===` lines.
|
||||
fn parse_agent_file(content: &str) -> Option<AgentDef> {
|
||||
let (first_line, rest) = content.split_once('\n')?;
|
||||
let header: AgentHeader = serde_json::from_str(first_line.trim()).ok()?;
|
||||
// Skip optional blank line between header and prompt body
|
||||
let body = rest.strip_prefix('\n').unwrap_or(rest);
|
||||
|
||||
// Split on === PROMPT === delimiter for multi-step agents
|
||||
let prompts: Vec<String> = body
|
||||
.split("\n=== PROMPT ===\n")
|
||||
.map(|s| s.trim().to_string())
|
||||
.filter(|s| !s.is_empty())
|
||||
.collect();
|
||||
// Split on === PROMPT ... === lines, capturing the delimiter content
|
||||
let mut steps: Vec<AgentStep> = Vec::new();
|
||||
let mut current_prompt = String::new();
|
||||
let mut current_phase: Option<String> = None;
|
||||
let mut step_num = 0;
|
||||
|
||||
if prompts.is_empty() {
|
||||
for line in body.lines() {
|
||||
if line.starts_with("=== PROMPT") && line.ends_with("===") {
|
||||
// Save previous step if any
|
||||
let trimmed = current_prompt.trim().to_string();
|
||||
if !trimmed.is_empty() {
|
||||
steps.push(AgentStep {
|
||||
prompt: trimmed,
|
||||
phase: current_phase.take()
|
||||
.unwrap_or_else(|| format!("step-{}", step_num)),
|
||||
});
|
||||
step_num += 1;
|
||||
}
|
||||
|
||||
// Parse delimiter: === PROMPT [phase:name] ===
|
||||
let inner = line.strip_prefix("=== PROMPT").unwrap()
|
||||
.strip_suffix("===").unwrap().trim();
|
||||
current_phase = inner.strip_prefix("phase:")
|
||||
.map(|s| s.trim().to_string());
|
||||
current_prompt.clear();
|
||||
} else {
|
||||
if !current_prompt.is_empty() {
|
||||
current_prompt.push('\n');
|
||||
}
|
||||
current_prompt.push_str(line);
|
||||
}
|
||||
}
|
||||
|
||||
// Save final step
|
||||
let trimmed = current_prompt.trim().to_string();
|
||||
if !trimmed.is_empty() {
|
||||
steps.push(AgentStep {
|
||||
prompt: trimmed,
|
||||
phase: current_phase.take()
|
||||
.unwrap_or_else(|| format!("step-{}", step_num)),
|
||||
});
|
||||
}
|
||||
|
||||
if steps.is_empty() {
|
||||
return None;
|
||||
}
|
||||
|
||||
Some(AgentDef {
|
||||
agent: header.agent,
|
||||
query: header.query,
|
||||
prompts,
|
||||
steps,
|
||||
model: header.model,
|
||||
schedule: header.schedule,
|
||||
tools: header.tools,
|
||||
|
|
@ -744,18 +788,21 @@ pub fn run_agent(
|
|||
vec![]
|
||||
};
|
||||
|
||||
// Resolve placeholders for all prompts. The conversation context
|
||||
// Resolve placeholders for all steps. The conversation context
|
||||
// carries forward between steps naturally via the LLM's message history.
|
||||
let mut all_keys = keys;
|
||||
let mut prompts = Vec::new();
|
||||
for prompt_template in &def.prompts {
|
||||
let template = prompt_template.replace("{agent_name}", &def.agent);
|
||||
let mut resolved_steps = Vec::new();
|
||||
for step in &def.steps {
|
||||
let template = step.prompt.replace("{agent_name}", &def.agent);
|
||||
let (prompt, extra_keys) = resolve_placeholders(&template, store, &graph, &all_keys, count);
|
||||
all_keys.extend(extra_keys);
|
||||
prompts.push(prompt);
|
||||
resolved_steps.push(super::prompts::ResolvedStep {
|
||||
prompt,
|
||||
phase: step.phase.clone(),
|
||||
});
|
||||
}
|
||||
|
||||
Ok(super::prompts::AgentBatch { prompts, node_keys: all_keys })
|
||||
Ok(super::prompts::AgentBatch { steps: resolved_steps, node_keys: all_keys })
|
||||
}
|
||||
|
||||
/// Convert a list of keys to ReplayItems with priority and graph metrics.
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue