Implement standalone AutoAgent::run() for poc-hook agents

Creates an Agent from global config (API credentials, system prompt,
identity), overrides tools with the agent's tool set, and runs through
the standard Backend → run_with_backend → Agent::turn() path.

This enables poc-hook spawned agents (surface-observe, journal, etc.)
to work with the completions API instead of the deleted chat API.

Also added Default derive to CliArgs for config loading.

Co-Authored-By: Proof of Concept <poc@bcachefs.org>
Signed-off-by: Kent Overstreet <kent.overstreet@linux.dev>
This commit is contained in:
Kent Overstreet 2026-04-08 21:42:31 -04:00
parent bf1fa62d14
commit 850008ece7
3 changed files with 56 additions and 6 deletions

View file

@ -110,9 +110,39 @@ impl AutoAgent {
pub async fn run(
&mut self,
_bail_fn: Option<&(dyn Fn(usize) -> Result<(), String> + Sync)>,
bail_fn: Option<&(dyn Fn(usize) -> Result<(), String> + Sync)>,
) -> Result<String, String> {
Err("standalone agent run not yet migrated to completions API".to_string())
let config = crate::config::get();
let base_url = config.api_base_url.as_deref().unwrap_or("");
let api_key = config.api_key.as_deref().unwrap_or("");
let model = config.api_model.as_deref().unwrap_or("");
if base_url.is_empty() || model.is_empty() {
return Err("API not configured (no base_url or model)".to_string());
}
let client = super::api::ApiClient::new(base_url, api_key, model);
// Load system prompt + identity from config
let cli = crate::user::CliArgs::default();
let (app, _) = crate::config::load_app(&cli)
.map_err(|e| format!("config: {}", e))?;
let (system_prompt, personality) = crate::config::reload_for_model(
&app, &app.prompts.other,
).map_err(|e| format!("config: {}", e))?;
let agent = Agent::new(
client, system_prompt, personality,
app, String::new(),
None,
super::tools::ActiveTools::new(),
).await;
{
let mut st = agent.state.lock().await;
st.provenance = format!("standalone:{}", self.name);
st.tools = self.tools.clone();
}
let mut backend = Backend(agent);
self.run_with_backend(&mut backend, bail_fn).await
}
/// Run forked using a shared agent Arc. The UI can lock the same
@ -254,15 +284,35 @@ pub fn run_one_agent(
defs::run_agent(store, &def, effective_count, &Default::default())?
};
// Filter tools based on agent def
// Filter tools based on agent def, add filesystem output tool
let all_tools = super::tools::memory_and_journal_tools();
let effective_tools: Vec<super::tools::Tool> = if def.tools.is_empty() {
let mut effective_tools: Vec<super::tools::Tool> = if def.tools.is_empty() {
all_tools.to_vec()
} else {
all_tools.into_iter()
.filter(|t| def.tools.iter().any(|w| w == &t.name))
.collect()
};
effective_tools.push(super::tools::Tool {
name: "output",
description: "Produce a named output value for passing between steps.",
parameters_json: r#"{"type":"object","properties":{"key":{"type":"string","description":"Output name"},"value":{"type":"string","description":"Output value"}},"required":["key","value"]}"#,
handler: std::sync::Arc::new(|_agent, v| Box::pin(async move {
let key = v["key"].as_str()
.ok_or_else(|| anyhow::anyhow!("output requires 'key'"))?;
if key.starts_with("pid-") || key.contains('/') || key.contains("..") {
anyhow::bail!("invalid output key: {}", key);
}
let value = v["value"].as_str()
.ok_or_else(|| anyhow::anyhow!("output requires 'value'"))?;
let dir = std::env::var("POC_AGENT_OUTPUT_DIR")
.map_err(|_| anyhow::anyhow!("no output directory set"))?;
let path = std::path::Path::new(&dir).join(key);
std::fs::write(&path, value)
.map_err(|e| anyhow::anyhow!("writing output {}: {}", path.display(), e))?;
Ok(format!("{}: {}", key, value))
})),
});
let n_steps = agent_batch.steps.len();
// Guard: reject oversized first prompt