forked from kent/consciousness
agents: multi-step agent support
Split agent prompts on === PROMPT === delimiter. Each step runs as
a new user message in the same LLM conversation, so context carries
forward naturally between steps. Single-step agents are unchanged.
- AgentDef.prompt -> AgentDef.prompts: Vec<String>
- AgentBatch.prompt -> AgentBatch.prompts: Vec<String>
- API layer injects next prompt after each text response
- {{conversation:N}} parameterized byte budget for conversation context
Co-Authored-By: Kent Overstreet <kent.overstreet@linux.dev>
This commit is contained in:
parent
baf208281d
commit
77d1d39f3f
6 changed files with 166 additions and 65 deletions
|
|
@ -26,11 +26,14 @@ fn get_client() -> Result<&'static ApiClient, String> {
|
|||
}))
|
||||
}
|
||||
|
||||
/// Run an agent prompt through the direct API with tool support.
|
||||
/// Returns the final text response after all tool calls are resolved.
|
||||
/// Run agent prompts through the direct API with tool support.
|
||||
/// For multi-step agents, each prompt is injected as a new user message
|
||||
/// after the previous step's tool loop completes. The conversation
|
||||
/// context carries forward naturally between steps.
|
||||
/// Returns the final text response after all steps complete.
|
||||
pub async fn call_api_with_tools(
|
||||
agent: &str,
|
||||
prompt: &str,
|
||||
prompts: &[String],
|
||||
temperature: Option<f32>,
|
||||
log: &dyn Fn(&str),
|
||||
) -> Result<String, String> {
|
||||
|
|
@ -39,15 +42,18 @@ pub async fn call_api_with_tools(
|
|||
// Set up a UI channel — we drain reasoning tokens into the log
|
||||
let (ui_tx, mut ui_rx) = crate::agent::ui_channel::channel();
|
||||
|
||||
// Build tool definitions — memory tools for graph operations
|
||||
// Build tool definitions — memory and journal tools for graph operations
|
||||
let all_defs = tools::definitions();
|
||||
let tool_defs: Vec<ToolDef> = all_defs.into_iter()
|
||||
.filter(|d| d.function.name.starts_with("memory_"))
|
||||
.filter(|d| d.function.name.starts_with("memory_")
|
||||
|| d.function.name.starts_with("journal_")
|
||||
|| d.function.name == "output")
|
||||
.collect();
|
||||
let tracker = ProcessTracker::new();
|
||||
|
||||
// Start with the prompt as a user message
|
||||
let mut messages = vec![Message::user(prompt)];
|
||||
// Start with the first prompt as a user message
|
||||
let mut messages = vec![Message::user(&prompts[0])];
|
||||
let mut next_prompt_idx = 1; // index of next prompt to inject
|
||||
let reasoning = crate::config::get().api_reasoning.clone();
|
||||
|
||||
let max_turns = 50;
|
||||
|
|
@ -65,8 +71,15 @@ pub async fn call_api_with_tools(
|
|||
let msg_bytes: usize = messages.iter()
|
||||
.map(|m| m.content_text().len())
|
||||
.sum();
|
||||
format!("API error on turn {} (~{}KB payload, {} messages): {}",
|
||||
turn, msg_bytes / 1024, messages.len(), e)
|
||||
let err_str = e.to_string();
|
||||
let hint = if err_str.contains("IncompleteMessage") || err_str.contains("connection closed") {
|
||||
format!(" — likely exceeded model context window (~{}KB ≈ {}K tokens)",
|
||||
msg_bytes / 1024, msg_bytes / 4096)
|
||||
} else {
|
||||
String::new()
|
||||
};
|
||||
format!("API error on turn {} (~{}KB payload, {} messages): {}{}",
|
||||
turn, msg_bytes / 1024, messages.len(), e, hint)
|
||||
})?;
|
||||
|
||||
if let Some(u) = &usage {
|
||||
|
|
@ -125,7 +138,9 @@ pub async fn call_api_with_tools(
|
|||
}
|
||||
};
|
||||
|
||||
let output = if call.function.name.starts_with("memory_") {
|
||||
let output = if call.function.name.starts_with("memory_")
|
||||
|| call.function.name.starts_with("journal_")
|
||||
|| call.function.name == "output" {
|
||||
let prov = format!("agent:{}", agent);
|
||||
match crate::agent::tools::memory::dispatch(
|
||||
&call.function.name, &args, Some(&prov),
|
||||
|
|
@ -151,7 +166,7 @@ pub async fn call_api_with_tools(
|
|||
continue;
|
||||
}
|
||||
|
||||
// Text-only response — we're done
|
||||
// Text-only response — step complete
|
||||
let text = msg.content_text().to_string();
|
||||
if text.is_empty() && !has_content {
|
||||
log("empty response, retrying");
|
||||
|
|
@ -162,6 +177,17 @@ pub async fn call_api_with_tools(
|
|||
}
|
||||
|
||||
log(&format!("\n=== RESPONSE ===\n\n{}", text));
|
||||
|
||||
// If there are more prompts, inject the next one and continue
|
||||
if next_prompt_idx < prompts.len() {
|
||||
messages.push(Message::assistant(&text));
|
||||
let next = &prompts[next_prompt_idx];
|
||||
next_prompt_idx += 1;
|
||||
log(&format!("\n=== STEP {}/{} ===\n", next_prompt_idx, prompts.len()));
|
||||
messages.push(Message::user(next));
|
||||
continue;
|
||||
}
|
||||
|
||||
return Ok(text);
|
||||
}
|
||||
|
||||
|
|
@ -172,7 +198,7 @@ pub async fn call_api_with_tools(
|
|||
/// with its own tokio runtime. Safe to call from any context.
|
||||
pub fn call_api_with_tools_sync(
|
||||
agent: &str,
|
||||
prompt: &str,
|
||||
prompts: &[String],
|
||||
temperature: Option<f32>,
|
||||
log: &(dyn Fn(&str) + Sync),
|
||||
) -> Result<String, String> {
|
||||
|
|
@ -185,7 +211,7 @@ pub fn call_api_with_tools_sync(
|
|||
let prov = format!("agent:{}", agent);
|
||||
rt.block_on(
|
||||
crate::store::TASK_PROVENANCE.scope(prov,
|
||||
call_api_with_tools(agent, prompt, temperature, log))
|
||||
call_api_with_tools(agent, prompts, temperature, log))
|
||||
)
|
||||
}).join().unwrap()
|
||||
})
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue