IT BUILDS: Full AST migration compiles — zero errors

All callers migrated from old context types to AstNode/ContextState.
Killed: Message, Role (api), ConversationEntry, ContextEntry,
ContextSection, working_stack, api/parsing.rs, api/types.rs,
api/openai.rs, context_old.rs.

Oneshot standalone path stubbed (needs completions API rewrite).
12 warnings remaining (dead code cleanup).

Co-Authored-By: Proof of Concept <poc@bcachefs.org>
This commit is contained in:
Kent Overstreet 2026-04-08 15:29:52 -04:00
parent d0d876e067
commit e587431f9a
5 changed files with 99 additions and 224 deletions

View file

@ -577,7 +577,6 @@ impl Agent {
self.push_node(AstNode::tool_result(&output));
}
pub fn conversation_from(&self, from: usize) -> &[AstNode] {
let conv = self.context.conversation();
if from < conv.len() { &conv[from..] } else { &[] }
@ -698,7 +697,4 @@ impl Agent {
pub fn client_clone(&self) -> ApiClient {
self.client.clone()
}
}

View file

@ -14,7 +14,8 @@ use std::fs;
use std::path::PathBuf;
use std::sync::OnceLock;
use super::api::{ApiClient, Message, Usage};
use super::api::{ApiClient, Usage};
use super::context::{AstNode, Role};
use super::tools::{self as agent_tools};
use super::Agent;
@ -61,43 +62,13 @@ pub struct AutoAgent {
pub turn: usize,
}
/// Per-run conversation backend — created fresh by run() or run_forked().
enum Backend {
Standalone { client: ApiClient, messages: Vec<Message> },
Forked(std::sync::Arc<tokio::sync::Mutex<Agent>>),
}
/// Per-run conversation backend — wraps a forked agent.
struct Backend(std::sync::Arc<tokio::sync::Mutex<Agent>>);
impl Backend {
async fn client(&self) -> ApiClient {
match self {
Backend::Standalone { client, .. } => client.clone(),
Backend::Forked(agent) => agent.lock().await.client_clone(),
}
async fn push_node(&mut self, node: AstNode) {
self.0.lock().await.push_node(node);
}
async fn messages(&self) -> Vec<Message> {
match self {
Backend::Standalone { messages, .. } => messages.clone(),
Backend::Forked(agent) => agent.lock().await.assemble_api_messages(),
}
}
async fn push_message(&mut self, msg: Message) {
match self {
Backend::Standalone { messages, .. } => messages.push(msg),
Backend::Forked(agent) => agent.lock().await.push_message(msg),
}
}
async fn push_raw(&mut self, msg: Message) {
match self {
Backend::Standalone { messages, .. } => messages.push(msg),
Backend::Forked(agent) => {
agent.lock().await.push_message(msg);
}
}
}
}
/// Resolve {{placeholder}} templates in subconscious agent prompts.
@ -166,17 +137,12 @@ impl AutoAgent {
}
}
/// Run standalone — creates a fresh message list from the global
/// API client. Used by oneshot CLI agents.
/// Run standalone — TODO: needs rewrite to use completions API
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> {
let client = get_client()?.clone();
let mut backend = Backend::Standalone {
client, messages: Vec::new(),
};
self.run_with_backend(&mut backend, bail_fn).await
Err("standalone agent run not yet migrated to completions API".to_string())
}
/// Run forked using a shared agent Arc. The UI can lock the same
@ -192,7 +158,7 @@ impl AutoAgent {
phase: s.phase.clone(),
}).collect();
let orig_steps = std::mem::replace(&mut self.steps, resolved_steps);
let mut backend = Backend::Forked(agent.clone());
let mut backend = Backend(agent.clone());
let result = self.run_with_backend(&mut backend, None).await;
self.steps = orig_steps;
result
@ -211,43 +177,27 @@ impl AutoAgent {
let mut next_step = 0;
if next_step < self.steps.len() {
backend.push_message(
Message::user(&self.steps[next_step].prompt)).await;
backend.push_node(
AstNode::user_msg(&self.steps[next_step].prompt)).await;
next_step += 1;
}
let reasoning = crate::config::get().api_reasoning.clone();
let max_turns = 50 * self.steps.len().max(1);
for _ in 0..max_turns {
self.turn += 1;
let messages = backend.messages().await;
let client = backend.client().await;
dbglog!("[auto] {} turn {} ({} messages)",
self.name, self.turn, messages.len());
let result = Agent::turn(backend.0.clone()).await
.map_err(|e| format!("{}: {}", self.name, e))?;
let (msg, usage_opt) = Self::api_call_with_retry(
&self.name, &client, &self.tools, &messages,
&reasoning, self.sampling, self.priority).await?;
if let Some(u) = &usage_opt {
dbglog!("[auto] {} tokens: {} prompt + {} completion",
self.name, u.prompt_tokens, u.completion_tokens);
}
let has_content = msg.content.is_some();
let has_tools = msg.tool_calls.as_ref().is_some_and(|tc| !tc.is_empty());
if has_tools {
self.dispatch_tools(backend, &msg).await;
if result.had_tool_calls {
continue;
}
let text = msg.content_text().to_string();
if text.is_empty() && !has_content {
let text = result.text;
if text.is_empty() {
dbglog!("[auto] {} empty response, retrying", self.name);
backend.push_message(Message::user(
backend.push_node(AstNode::user_msg(
"[system] Your previous response was empty. \
Please respond with text or use a tool."
)).await;
@ -257,15 +207,13 @@ impl AutoAgent {
dbglog!("[auto] {} response: {}",
self.name, &text[..text.len().min(200)]);
backend.push_message(Message::assistant(&text)).await;
if next_step < self.steps.len() {
if let Some(ref check) = bail_fn {
check(next_step)?;
}
self.current_phase = self.steps[next_step].phase.clone();
backend.push_message(
Message::user(&self.steps[next_step].prompt)).await;
backend.push_node(
AstNode::user_msg(&self.steps[next_step].prompt)).await;
next_step += 1;
dbglog!("[auto] {} step {}/{}",
self.name, next_step, self.steps.len());
@ -278,102 +226,6 @@ impl AutoAgent {
Err(format!("{}: exceeded {} tool turns", self.name, max_turns))
}
async fn api_call_with_retry(
name: &str,
client: &ApiClient,
tools: &[agent_tools::Tool],
messages: &[Message],
reasoning: &str,
sampling: super::api::SamplingParams,
priority: i32,
) -> Result<(Message, Option<Usage>), String> {
let mut last_err = None;
for attempt in 0..5 {
match client.chat_completion_stream_temp(
messages, tools, reasoning, sampling, Some(priority),
).await {
Ok((msg, usage)) => {
if let Some(ref e) = last_err {
dbglog!("[auto] {} succeeded after retry (previous: {})",
name, e);
}
return Ok((msg, usage));
}
Err(e) => {
let err_str = e.to_string();
let is_transient = err_str.contains("IncompleteMessage")
|| err_str.contains("connection closed")
|| err_str.contains("connection reset")
|| err_str.contains("timed out")
|| err_str.contains("Connection refused");
if is_transient && attempt < 4 {
dbglog!("[auto] {} transient error (attempt {}): {}, retrying",
name, attempt + 1, err_str);
tokio::time::sleep(std::time::Duration::from_secs(2 << attempt)).await;
last_err = Some(e);
continue;
}
let msg_bytes: usize = messages.iter()
.map(|m| m.content_text().len()).sum();
return Err(format!(
"{}: API error (~{}KB, {} messages, {} attempts): {}",
name, msg_bytes / 1024,
messages.len(), attempt + 1, e));
}
}
}
Err(format!("{}: all retry attempts exhausted", name))
}
async fn dispatch_tools(&mut self, backend: &mut Backend, msg: &Message) {
let mut sanitized = msg.clone();
if let Some(ref mut calls) = sanitized.tool_calls {
for call in calls {
if serde_json::from_str::<serde_json::Value>(&call.function.arguments).is_err() {
dbglog!("[auto] {} sanitizing malformed args for {}: {}",
self.name, call.function.name, &call.function.arguments);
call.function.arguments = "{}".to_string();
}
}
}
backend.push_raw(sanitized).await;
for call in msg.tool_calls.as_ref().unwrap() {
dbglog!("[auto] {} tool: {}({})",
self.name, call.function.name, &call.function.arguments);
let args: serde_json::Value = match serde_json::from_str(&call.function.arguments) {
Ok(v) => v,
Err(_) => {
backend.push_raw(Message::tool_result(
&call.id,
"Error: your tool call had malformed JSON arguments. \
Please retry with valid JSON.",
)).await;
continue;
}
};
// Intercept output() — store in-memory instead of filesystem
let output = if call.function.name == "output" {
let key = args["key"].as_str().unwrap_or("");
let value = args["value"].as_str().unwrap_or("");
if !key.is_empty() {
self.outputs.insert(key.to_string(), value.to_string());
}
format!("{}: {}", key, value)
} else {
let agent = match &*backend {
Backend::Forked(a) => Some(a.clone()),
_ => None,
};
agent_tools::dispatch_with_agent(&call.function.name, &args, agent).await
};
dbglog!("[auto] {} result: {} chars", self.name, output.len());
backend.push_raw(Message::tool_result(&call.id, &output)).await;
}
}
}
// ---------------------------------------------------------------------------