agents: bail script support, pid file simplification, cleanup

- Bail command moved from hardcoded closure to external script
  specified in agent JSON header ("bail": "bail-no-competing.sh")
- Runner executes script between steps with pid file path as $1,
  cwd = state dir. Non-zero exit stops the pipeline.
- PID files simplified to just the phase name (no JSON) for easy
  bash inspection (cat pid-*)
- scan_pid_files helper deduplicates pid scanning logic
- Timeout check uses file mtime instead of embedded timestamp
- PID file cleaned up on bail/error (not just success)
- output() tool validates key names (rejects pid-*, /, ..)
- Agent log files append instead of truncate
- Fixed orphaned derive and doc comment on AgentStep/AgentDef
- Phase written after bail check passes, not before

Co-Authored-By: Kent Overstreet <kent.overstreet@linux.dev>
This commit is contained in:
ProofOfConcept 2026-03-26 15:20:29 -04:00
parent e20aeeeabe
commit 52703b4637
5 changed files with 135 additions and 85 deletions

View file

@ -24,8 +24,6 @@ use serde::Deserialize;
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,
@ -34,6 +32,7 @@ pub struct AgentStep {
pub phase: String,
}
/// Agent definition: config (from JSON header) + prompt steps.
pub struct AgentDef {
pub agent: String,
pub query: String,
@ -47,6 +46,9 @@ pub struct AgentDef {
pub chunk_size: Option<usize>,
pub chunk_overlap: Option<usize>,
pub temperature: Option<f32>,
/// Bail check command — run between steps with pid file path as $1,
/// cwd = state dir. Non-zero exit = stop the pipeline.
pub bail: Option<String>,
}
/// The JSON header portion (first line of the file).
@ -73,6 +75,10 @@ struct AgentHeader {
/// LLM temperature override
#[serde(default)]
temperature: Option<f32>,
/// Bail check command — run between steps with pid file path as $1,
/// cwd = state dir. Non-zero exit = stop the pipeline.
#[serde(default)]
bail: Option<String>,
}
fn default_model() -> String { "sonnet".into() }
@ -143,10 +149,11 @@ fn parse_agent_file(content: &str) -> Option<AgentDef> {
chunk_size: header.chunk_size,
chunk_overlap: header.chunk_overlap,
temperature: header.temperature,
bail: header.bail,
})
}
fn agents_dir() -> PathBuf {
pub fn agents_dir() -> PathBuf {
let repo = PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("src/subconscious/agents");
if repo.is_dir() { return repo; }
crate::store::memory_dir().join("agents")