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

@ -1 +1 @@
{"sessionId":"463c6050-b49f-4509-9d4b-4596af79a90e","pid":11339,"acquiredAt":1775649730868} {"sessionId":"463c6050-b49f-4509-9d4b-4596af79a90e","pid":61703,"acquiredAt":1775698574304}

View file

@ -110,9 +110,39 @@ impl AutoAgent {
pub async fn run( pub async fn run(
&mut self, &mut self,
_bail_fn: Option<&(dyn Fn(usize) -> Result<(), String> + Sync)>, bail_fn: Option<&(dyn Fn(usize) -> Result<(), String> + Sync)>,
) -> Result<String, String> { ) -> 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 /// 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())? 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 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() all_tools.to_vec()
} else { } else {
all_tools.into_iter() all_tools.into_iter()
.filter(|t| def.tools.iter().any(|w| w == &t.name)) .filter(|t| def.tools.iter().any(|w| w == &t.name))
.collect() .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(); let n_steps = agent_batch.steps.len();
// Guard: reject oversized first prompt // Guard: reject oversized first prompt

View file

@ -449,7 +449,7 @@ async fn run(
use clap::{Parser, Subcommand}; use clap::{Parser, Subcommand};
use std::path::PathBuf; use std::path::PathBuf;
#[derive(Parser, Debug)] #[derive(Parser, Debug, Default)]
#[command(name = "consciousness", about = "Substrate-independent AI agent")] #[command(name = "consciousness", about = "Substrate-independent AI agent")]
pub struct CliArgs { pub struct CliArgs {
/// Select active backend ("anthropic" or "openrouter") /// Select active backend ("anthropic" or "openrouter")