use std::collections::HashSet;
use std::sync::Arc;
use crate::event::{Event, EventSink};
use crate::message::{Content, Message, Role};
use crate::output::{ModelOutput, Step, StopReason};
use crate::provider::{ModelProvider, ProviderError};
use crate::request::{ChatParams, ModelRequest};
use crate::session::Session;
use crate::tool::{ToolContext, ToolResolver, ToolResult};
#[derive(Debug, PartialEq, Eq, thiserror::Error)]
pub enum AgentError {
#[error("agent built without a model provider")]
MissingProvider,
#[error(transparent)]
Provider(#[from] ProviderError),
#[error("commit before tool execution failed: {0}")]
Commit(String),
#[error(
"stopped after {0} steps without finishing; send a follow-up \
to keep going, or raise the step limit"
)]
MaxIterations(u32),
}
struct ForwardingSink<'a> {
inner: &'a mut dyn EventSink,
announced: HashSet<String>,
}
impl EventSink for ForwardingSink<'_> {
fn emit(&mut self, event: Event<'_>) {
if let Event::ToolCallStart { id, .. } = event {
self.announced.insert(id.to_owned());
}
self.inner.emit(event);
}
}
pub type CommitHook = Box<dyn Fn(&Session) -> Result<(), String> + Send + Sync>;
pub struct Agent {
provider: Arc<dyn ModelProvider>,
resolver: Option<Arc<dyn ToolResolver>>,
system: Option<String>,
params: ChatParams,
commit: Option<CommitHook>,
steps: u32,
}
impl Agent {
pub fn builder() -> AgentBuilder {
AgentBuilder::default()
}
pub async fn step(
&mut self,
session: &mut Session,
sink: &mut dyn EventSink,
ctx: &ToolContext,
) -> Result<Step, AgentError> {
let iteration = self.steps;
self.steps += 1;
let mut events = ForwardingSink {
inner: sink,
announced: HashSet::new(),
};
events.emit(Event::StepStart { iteration });
let req = ModelRequest {
system: self.system.clone(),
messages: session.history.clone(),
tools: self
.resolver
.as_ref()
.map(|r| r.specs())
.unwrap_or_default(),
params: self.params.clone(),
};
let out = self.provider.complete(req, &mut events).await?;
let call_indices: Vec<usize> = out
.message
.content
.iter()
.enumerate()
.filter_map(|(i, c)| matches!(c, Content::ToolCall(_)).then_some(i))
.collect();
session.history.push(out.message.clone());
if out.stop_reason == StopReason::ToolUse {
if let Some(commit) = &self.commit {
commit(session).map_err(AgentError::Commit)?;
}
for i in call_indices {
let Some(Content::ToolCall(call)) = out.message.content.get(i) else {
continue;
};
if !events.announced.contains(&call.id) {
events.emit(Event::ToolCallStart {
id: &call.id,
name: &call.name,
});
}
let id = call.id.clone();
let mut progress = |chunk: &str| {
events.emit(Event::ToolOutputDelta { id: &id, chunk });
};
let result = match &self.resolver {
Some(resolver) => {
resolver
.resolve_streaming(call.clone(), ctx, &mut progress)
.await
}
None => ToolResult::failure(&call.id, "no tool resolver is configured"),
};
events.emit(Event::ToolResult {
id: &result.id,
content: &result.content,
is_error: result.is_error,
code: result.code,
});
session.history.push(Message {
role: Role::Tool,
content: vec![Content::ToolResult(result)],
});
}
}
events.emit(Event::StepEnd {
usage: out.usage,
stop_reason: &out.stop_reason,
});
Ok(Step { output: out })
}
pub fn system(&self) -> &str {
self.system.as_deref().unwrap_or_default()
}
pub fn set_commit_hook(&mut self, hook: Option<CommitHook>) {
self.commit = hook;
}
}
#[derive(Default)]
pub struct AgentBuilder {
provider: Option<Arc<dyn ModelProvider>>,
resolver: Option<Arc<dyn ToolResolver>>,
system: Option<String>,
params: Option<ChatParams>,
commit: Option<CommitHook>,
}
impl AgentBuilder {
pub fn provider(mut self, provider: Arc<dyn ModelProvider>) -> Self {
self.provider = Some(provider);
self
}
pub fn resolver(mut self, resolver: Arc<dyn ToolResolver>) -> Self {
self.resolver = Some(resolver);
self
}
pub fn system(mut self, system: impl Into<String>) -> Self {
self.system = Some(system.into());
self
}
pub fn params(mut self, params: ChatParams) -> Self {
self.params = Some(params);
self
}
pub fn commit_hook(mut self, hook: CommitHook) -> Self {
self.commit = Some(hook);
self
}
pub fn build(self) -> Result<Agent, AgentError> {
Ok(Agent {
provider: self.provider.ok_or(AgentError::MissingProvider)?,
resolver: self.resolver,
system: self.system,
params: self.params.unwrap_or_default(),
commit: self.commit,
steps: 0,
})
}
}
pub async fn run_turn(
agent: &mut Agent,
session: &mut Session,
input: Message,
sink: &mut dyn EventSink,
ctx: &ToolContext,
max_iterations: u32,
) -> Result<ModelOutput, AgentError> {
session.history.push(input);
for _ in 0..max_iterations {
let step = agent.step(session, sink, ctx).await?;
if step.output.stop_reason != StopReason::ToolUse {
return Ok(step.output);
}
}
Err(AgentError::MaxIterations(max_iterations))
}