From 850008ece78f9d87d397351ff8431934a0198f5f Mon Sep 17 00:00:00 2001 From: Kent Overstreet Date: Wed, 8 Apr 2026 21:42:31 -0400 Subject: [PATCH] Implement standalone AutoAgent::run() for poc-hook agents MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 Signed-off-by: Kent Overstreet --- .claude/scheduled_tasks.lock | 2 +- src/agent/oneshot.rs | 58 +++++++++++++++++++++++++++++++++--- src/user/mod.rs | 2 +- 3 files changed, 56 insertions(+), 6 deletions(-) diff --git a/.claude/scheduled_tasks.lock b/.claude/scheduled_tasks.lock index a7b36c5..b3c14b6 100644 --- a/.claude/scheduled_tasks.lock +++ b/.claude/scheduled_tasks.lock @@ -1 +1 @@ -{"sessionId":"463c6050-b49f-4509-9d4b-4596af79a90e","pid":11339,"acquiredAt":1775649730868} \ No newline at end of file +{"sessionId":"463c6050-b49f-4509-9d4b-4596af79a90e","pid":61703,"acquiredAt":1775698574304} \ No newline at end of file diff --git a/src/agent/oneshot.rs b/src/agent/oneshot.rs index eded885..b228cb8 100644 --- a/src/agent/oneshot.rs +++ b/src/agent/oneshot.rs @@ -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 { - 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 = if def.tools.is_empty() { + let mut effective_tools: Vec = 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 diff --git a/src/user/mod.rs b/src/user/mod.rs index 30a7822..7ebc818 100644 --- a/src/user/mod.rs +++ b/src/user/mod.rs @@ -449,7 +449,7 @@ async fn run( use clap::{Parser, Subcommand}; use std::path::PathBuf; -#[derive(Parser, Debug)] +#[derive(Parser, Debug, Default)] #[command(name = "consciousness", about = "Substrate-independent AI agent")] pub struct CliArgs { /// Select active backend ("anthropic" or "openrouter")