use super::{AgentError, AgentEvent, AgentResult, ChatOptions, LlmError, StopReason, Usage};
use crate::message::{Message, Part, Role, ToolCall, ToolResponse};
use crate::prompt::Prompt;
use crate::tool::ToolBox;
use async_stream::try_stream;
use futures_util::{Stream, StreamExt, pin_mut};
use genai::Client;
use genai::chat::{ChatRequest, ChatStreamEvent, JsonSpec, StreamEnd};
use log::{debug, trace};
use schemars::{JsonSchema, schema_for};
use serde::de::DeserializeOwned;
use serde_json::{Value, from_str, json};
use std::sync::Arc;
pub(super) struct ResolvedRun {
pub(super) client: Client,
pub(super) system: String,
pub(super) model: String,
pub(super) toolbox: Option<Arc<dyn ToolBox>>,
pub(super) max_iterations: usize,
pub(super) chat_options: ChatOptions,
pub(super) response_format: Option<JsonSpec>,
}
struct Step {
tool_calls: Vec<ToolCall>,
tool_use_message: Message,
texts: Vec<String>,
usage: Usage,
stop_reason: Option<StopReason>,
reasoning_content: Option<String>,
}
pub(super) fn run_loop<D>(
run: ResolvedRun,
history: Vec<Message>,
prompt: Prompt,
streaming: bool,
) -> impl Stream<Item = Result<AgentEvent<D>, AgentError>> + Send + 'static
where
D: DeserializeOwned + JsonSchema + Send + 'static,
{
try_stream! {
let prompt = prompt.into_message();
let media = prompt.parts.iter().filter(|part| matches!(part, Part::Media(_))).count();
debug!("Agent Question: {} ({media} media)", prompt.text().unwrap_or_default());
let mut new_messages = vec![prompt];
let mut usage = Usage::default();
let mut chat_opts = run.chat_options.to_genai();
let is_answer_string = run.response_format.is_none();
if let Some(format) = run.response_format.clone() {
chat_opts = chat_opts.with_response_format(format);
}
if streaming {
chat_opts = chat_opts
.with_capture_usage(true)
.with_capture_content(true)
.with_capture_tool_calls(true)
.with_capture_reasoning_content(true);
}
let history: Vec<genai::chat::ChatMessage> =
history.into_iter().map(Message::into_genai).collect();
for iteration in 0..run.max_iterations {
debug!("Agent iteration: {iteration}");
let mut chat_req = ChatRequest::from_messages(
history
.iter()
.cloned()
.chain(new_messages.iter().cloned().map(Message::into_genai))
.collect(),
);
if !run.system.is_empty() {
chat_req = chat_req.with_system(run.system.clone());
}
if let Some(toolbox) = &run.toolbox {
let definitions =
toolbox
.tools_definitions()
.map_err(|source| AgentError::Tool {
tool: String::new(),
source,
})?;
chat_req = chat_req.with_tools(definitions.into_iter().map(crate::tool::Tool::into_genai));
}
let step = if streaming {
let mut stream = run
.client
.exec_chat_stream(&run.model, chat_req, Some(&chat_opts))
.await
.map_err(|err| AgentError::Llm(LlmError::from_genai(err)))?
.stream;
let mut end = None;
while let Some(event) = stream.next().await {
match event.map_err(|err| AgentError::Llm(LlmError::from_genai(err)))? {
ChatStreamEvent::Chunk(chunk) => yield AgentEvent::TextDelta(chunk.content),
ChatStreamEvent::ReasoningChunk(chunk) => {
yield AgentEvent::ReasoningDelta(chunk.content)
}
ChatStreamEvent::End(stream_end) => end = Some(stream_end),
_ => {}
}
}
step_from_stream_end(end.unwrap_or_default())
} else {
exec_step(&run, chat_req, &chat_opts).await?
};
usage += step.usage;
if step.stop_reason.as_ref().is_some_and(StopReason::is_max_tokens) {
Err(AgentError::MaxTokens { raw: step.texts.join("") })?;
}
if !step.tool_calls.is_empty() {
new_messages.push(step.tool_use_message);
let mut fatal: Option<AgentError> = None;
for tool_call in step.tool_calls {
if fatal.is_some() {
new_messages.push(Message::tool_response(ToolResponse::new(
&tool_call,
"Tool was not executed because an earlier tool call failed",
)));
continue;
}
yield AgentEvent::ToolCall(tool_call.clone());
let outcome = call_tool(run.toolbox.as_deref(), &tool_call).await;
let ToolOutcome { content, is_error, .. } = outcome;
fatal = outcome.fatal;
new_messages.push(Message::tool_response(ToolResponse::new(&tool_call, content.clone())));
yield AgentEvent::ToolResult {
call_id: tool_call.id,
tool: tool_call.name,
content,
is_error,
};
}
if let Some(err) = fatal {
Err(err)?;
}
continue;
}
if step.texts.is_empty() {
debug!("Agent iteration {iteration}: response has no text and no tool calls");
continue;
}
let raw = step.texts.join("");
debug!("Agent Answer: {raw}");
new_messages.push(Message::assistant(raw.clone()));
let answer = parse_answer::<D>(raw, is_answer_string)?;
yield AgentEvent::Finished(AgentResult {
answer,
new_messages,
usage,
stop_reason: step.stop_reason,
reasoning_content: step.reasoning_content,
iterations: iteration + 1,
});
return;
}
Err(AgentError::MaxIterationsReached(run.max_iterations))?;
}
}
pub(super) async fn finish<D>(
events: impl Stream<Item = Result<AgentEvent<D>, AgentError>>,
) -> Result<AgentResult<D>, AgentError> {
pin_mut!(events);
while let Some(event) = events.next().await {
if let AgentEvent::Finished(result) = event? {
return Ok(result);
}
}
unreachable!("the agent loop ends with a result or an error")
}
async fn exec_step(
run: &ResolvedRun,
chat_req: ChatRequest,
chat_opts: &genai::chat::ChatOptions,
) -> Result<Step, AgentError> {
let resp = run
.client
.exec_chat(&run.model, chat_req, Some(chat_opts))
.await
.map_err(|err| AgentError::Llm(LlmError::from_genai(err)))?;
let tool_calls: Vec<ToolCall> = resp
.tool_calls()
.into_iter()
.cloned()
.map(ToolCall::from_genai)
.collect();
let usage = Usage::from_genai(&resp.usage);
let stop_reason = resp.stop_reason.clone().map(StopReason::from_genai);
let reasoning_content = resp.reasoning_content.clone();
let (tool_use_message, texts) = if tool_calls.is_empty() {
(
Message::new(Role::Assistant, Vec::new()),
resp.content.into_texts(),
)
} else {
let message = resp.into_assistant_message_for_tool_use();
(tool_use_message(message, &tool_calls), Vec::new())
};
Ok(Step {
tool_calls,
tool_use_message,
texts,
usage,
stop_reason,
reasoning_content,
})
}
fn tool_use_message(message: Option<genai::chat::ChatMessage>, tool_calls: &[ToolCall]) -> Message {
match message {
Some(message) => Message::from_genai(message),
None => Message::new(
Role::Assistant,
tool_calls.iter().cloned().map(Part::ToolCall).collect(),
),
}
}
fn step_from_stream_end(end: StreamEnd) -> Step {
let tool_calls: Vec<ToolCall> = end
.captured_tool_calls()
.map(|calls| {
calls
.into_iter()
.cloned()
.map(ToolCall::from_genai)
.collect()
})
.unwrap_or_default();
let usage = end
.captured_usage
.as_ref()
.map(Usage::from_genai)
.unwrap_or_default();
let stop_reason = end.captured_stop_reason.clone().map(StopReason::from_genai);
let reasoning_content = end.captured_reasoning_content.clone();
let (tool_use_message, texts) = if tool_calls.is_empty() {
(
Message::new(Role::Assistant, Vec::new()),
end.into_texts().unwrap_or_default(),
)
} else {
let message = end.into_assistant_message_for_tool_use();
(tool_use_message(message, &tool_calls), Vec::new())
};
Step {
tool_calls,
tool_use_message,
texts,
usage,
stop_reason,
reasoning_content,
}
}
struct ToolOutcome {
content: String,
is_error: bool,
fatal: Option<AgentError>,
}
impl ToolOutcome {
fn recoverable(content: String) -> Self {
Self {
content,
is_error: true,
fatal: None,
}
}
}
async fn call_tool(toolbox: Option<&dyn ToolBox>, tool_call: &ToolCall) -> ToolOutcome {
trace!(
"Tool request: {} with arguments: {}",
tool_call.name, tool_call.arguments
);
let Some(toolbox) = toolbox else {
debug!(
"Model requested tool '{}' but no toolbox was provided",
tool_call.name
);
return ToolOutcome::recoverable(format!(
"Tool '{}' is not available: no tools were provided",
tool_call.name
));
};
match toolbox
.call_tool(tool_call.name.clone(), tool_call.arguments.clone())
.await
{
Ok(result) => {
trace!("Tool result: {result}");
ToolOutcome {
content: result,
is_error: false,
fatal: None,
}
}
Err(err) if err.is_recoverable() => {
trace!("Tool error: {err}");
ToolOutcome::recoverable(err.to_string())
}
Err(source) => {
debug!("Fatal error in tool '{}': {source}", tool_call.name);
ToolOutcome {
content: format!("Tool failed: {source}"),
is_error: true,
fatal: Some(AgentError::Tool {
tool: tool_call.name.clone(),
source,
}),
}
}
}
}
pub(super) fn response_format<D: JsonSchema>() -> Result<JsonSpec, AgentError> {
let mut response_schema =
serde_json::to_value(schema_for!(D)).map_err(|err| AgentError::Schema(err.to_string()))?;
let obj = response_schema.as_object_mut().ok_or_else(|| {
AgentError::Schema("JSON schema of the answer type is not an object".to_string())
})?;
obj.remove("$schema");
obj.remove("title");
Ok(JsonSpec::new("ResponseFormat", json!(obj)))
}
fn parse_answer<D: DeserializeOwned>(raw: String, is_answer_string: bool) -> Result<D, AgentError> {
let resp = if is_answer_string {
Value::String(raw.clone()).to_string()
} else {
raw.clone()
};
from_str(&resp).map_err(|source| AgentError::InvalidAnswer { raw, source })
}