@@ -0,0 +1,129 @@
|
||||
//! Tool-calling loop driving the model against a [`Sandbox`].
|
||||
//!
|
||||
//! The loop sends the conversation and the available tools to OpenRouter,
|
||||
//! executes any requested tool call inside the sandbox and feeds the results
|
||||
//! back, until the model produces a final message or the iteration budget is
|
||||
//! exhausted.
|
||||
|
||||
use anyhow::Context;
|
||||
use openrouter_rs::{
|
||||
Message,
|
||||
types::{Role, Tool, ToolCall},
|
||||
};
|
||||
use serde_json::Value;
|
||||
use tracing::{debug, warn};
|
||||
|
||||
use crate::{
|
||||
open_router::OpenRouterClient,
|
||||
sandbox::{Sandbox, tools},
|
||||
};
|
||||
|
||||
/// Final output of an agent run.
|
||||
pub struct AgentResult {
|
||||
pub message: String,
|
||||
pub cost: Option<f64>,
|
||||
pub iterations: usize,
|
||||
}
|
||||
|
||||
/// Runs the tool-calling loop until the model answers or `max_iterations` is
|
||||
/// reached.
|
||||
///
|
||||
/// `tool_definitions` is the set of tools the model may call; it is selected by
|
||||
/// the caller based on the webhook action (see [`tools::for_webhook`]).
|
||||
pub async fn run(
|
||||
open_router: &OpenRouterClient,
|
||||
sandbox: &Sandbox,
|
||||
tool_definitions: Vec<Tool>,
|
||||
system_prompt: &str,
|
||||
user_prompt: &str,
|
||||
max_iterations: usize,
|
||||
) -> anyhow::Result<AgentResult> {
|
||||
let mut messages = vec![
|
||||
Message::new(Role::System, system_prompt),
|
||||
Message::new(Role::User, user_prompt),
|
||||
];
|
||||
|
||||
let mut total_cost = 0.0_f64;
|
||||
let mut has_cost = false;
|
||||
|
||||
for iteration in 1..=max_iterations {
|
||||
let response = open_router
|
||||
.chat_with_tools(messages.clone(), tool_definitions.clone())
|
||||
.await?;
|
||||
|
||||
if let Some(cost) = response.cost {
|
||||
total_cost += cost;
|
||||
has_cost = true;
|
||||
}
|
||||
|
||||
if response.tool_calls.is_empty() {
|
||||
return Ok(AgentResult {
|
||||
message: response.message.unwrap_or_default(),
|
||||
cost: has_cost.then_some(total_cost),
|
||||
iterations: iteration,
|
||||
});
|
||||
}
|
||||
|
||||
messages.push(Message::assistant_with_tool_calls(
|
||||
response.message.unwrap_or_default(),
|
||||
response.tool_calls.clone(),
|
||||
));
|
||||
|
||||
for call in &response.tool_calls {
|
||||
debug!(tool = call.name(), "Executing tool call");
|
||||
let content = execute(sandbox, call).await;
|
||||
messages.push(Message::tool_response(call.id(), content));
|
||||
}
|
||||
}
|
||||
|
||||
warn!(max_iterations, "Agent reached the iteration limit");
|
||||
anyhow::bail!("agent exceeded the maximum number of iterations ({max_iterations})")
|
||||
}
|
||||
|
||||
async fn execute(sandbox: &Sandbox, call: &ToolCall) -> String {
|
||||
let args = match parse_args(call) {
|
||||
Ok(args) => args,
|
||||
Err(err) => return format!("error: {err}"),
|
||||
};
|
||||
|
||||
match tools::dispatch(sandbox, call.name(), &args).await {
|
||||
Ok(output) => output,
|
||||
Err(err) => format!("error: {err}"),
|
||||
}
|
||||
}
|
||||
|
||||
fn parse_args(call: &ToolCall) -> anyhow::Result<Value> {
|
||||
let raw = call.arguments_json().trim();
|
||||
if raw.is_empty() {
|
||||
return Ok(Value::Object(serde_json::Map::new()));
|
||||
}
|
||||
|
||||
serde_json::from_str(raw)
|
||||
.with_context(|| format!("invalid arguments for tool `{}`", call.name()))
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn parse_args_accepts_empty_arguments() {
|
||||
let call = ToolCall::new("id", "ls", "");
|
||||
assert_eq!(
|
||||
parse_args(&call).unwrap(),
|
||||
Value::Object(serde_json::Map::new())
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parse_args_parses_json_object() {
|
||||
let call = ToolCall::new("id", "ls", r#"{"path":"src"}"#);
|
||||
assert_eq!(parse_args(&call).unwrap()["path"], "src");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parse_args_rejects_invalid_json() {
|
||||
let call = ToolCall::new("id", "ls", "not json");
|
||||
assert!(parse_args(&call).is_err());
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user