use super::model_call::ToolCallBase;
use super::*;
use crate::harness::tool::ToolExecutionContext;
enum ResolvedToolCall<State: Send + Sync> {
Tool(Arc<dyn Tool<State>>),
ErrorMessage(String),
}
enum ToolSlot {
Execute,
Immediate { call_id: String, message: String },
}
struct PreparedToolCall {
call_id: CallId,
tool_name: String,
captured_input: Option<Value>,
started_at_ms: u64,
}
impl<State: Send + Sync, Ctx: Send + Sync> AgentHarness<State, Ctx> {
pub(super) async fn execute_tools(
&self,
state: &State,
ctx: &mut RunContext<Ctx>,
run: &mut AgentRun,
status: &mut HarnessRunStatus,
messages: &mut Vec<Message>,
tool_calls: Vec<ToolCall>,
) -> Result<()> {
if tool_calls.len() > 1 && self.middleware.tool_middleware_len() == 0 {
self.execute_tools_concurrently(state, ctx, run, status, messages, tool_calls)
.await
} else {
self.execute_tools_serially(state, ctx, run, status, messages, tool_calls)
.await
}
}
async fn admit_tool_call(
&self,
state: &State,
ctx: &mut RunContext<Ctx>,
status: &mut HarnessRunStatus,
call: &mut ToolCall,
) -> Result<ResolvedToolCall<State>> {
if ctx.cancellation.is_cancelled() {
return Err(TinyAgentsError::Cancelled);
}
if ctx.check_deadline().is_err() {
ctx.emit(AgentEvent::LimitReached {
kind: LimitKind::WallClock,
});
return Err(TinyAgentsError::Timeout(format!(
"run `{}` exceeded its wall-clock deadline",
ctx.run_id()
)));
}
if let Err(err) = ctx.record_tool_call() {
ctx.emit(AgentEvent::LimitReached {
kind: LimitKind::ToolCalls,
});
return Err(TinyAgentsError::LimitExceeded(err.to_string()));
}
self.middleware.run_before_tool(ctx, state, call).await?;
if let Some(detail) = call.invalid.clone() {
let call_id = CallId::new(call.id.clone());
let record = ctx.emit(AgentEvent::InvalidToolArgs {
call_id,
tool_name: call.name.clone(),
arguments: call.arguments.clone(),
error: detail.clone(),
recovery: "tool_error".to_string(),
});
status.set_last_event(record.id);
return Ok(ResolvedToolCall::ErrorMessage(detail));
}
let tool = match self.tools.get(&call.name) {
Some(tool) => tool,
None => {
let requested = call.name.clone();
let arguments = call.arguments.clone();
let call_id = CallId::new(call.id.clone());
let rewrite_target = match &self.policy.unknown_tool {
UnknownToolPolicy::Rewrite { tool_name } => {
self.tools.get(tool_name).map(|t| (tool_name.clone(), t))
}
_ => None,
};
if let Some((tool_name, tool)) = rewrite_target {
call.name = tool_name.clone();
let record = ctx.emit(AgentEvent::UnknownToolCall {
call_id,
requested_name: requested,
arguments,
recovery: format!("rewrite:{tool_name}"),
});
status.set_last_event(record.id);
tool
} else if matches!(self.policy.unknown_tool, UnknownToolPolicy::Fail) {
return Err(TinyAgentsError::ToolNotFound(requested));
} else {
let valid = self.tools.names().join(", ");
let args_repr = serde_json::to_string(&arguments)
.unwrap_or_else(|_| "<unserializable>".to_string());
let message = format!(
"unknown tool `{requested}` (arguments: {args_repr}); \
valid tools: [{valid}]"
);
let record = ctx.emit(AgentEvent::UnknownToolCall {
call_id,
requested_name: requested.clone(),
arguments,
recovery: "tool_error".to_string(),
});
status.set_last_event(record.id);
return Ok(ResolvedToolCall::ErrorMessage(message));
}
}
};
if let Err(err) = tool.schema().validate_call(call) {
if matches!(self.policy.invalid_args, InvalidArgsPolicy::Fail) {
return Err(err);
}
let call_id = CallId::new(call.id.clone());
let detail = err.to_string();
let schema_repr = serde_json::to_string(&tool.schema().parameters)
.unwrap_or_else(|_| "<unserializable>".to_string());
let message = format!(
"invalid arguments for tool `{}`: {detail}; expected schema: {schema_repr}",
call.name
);
let record = ctx.emit(AgentEvent::InvalidToolArgs {
call_id,
tool_name: call.name.clone(),
arguments: call.arguments.clone(),
error: detail,
recovery: "tool_error".to_string(),
});
status.set_last_event(record.id);
return Ok(ResolvedToolCall::ErrorMessage(message));
}
Ok(ResolvedToolCall::Tool(tool))
}
fn start_tool_call(
&self,
ctx: &RunContext<Ctx>,
status: &mut HarnessRunStatus,
call: &ToolCall,
) -> PreparedToolCall {
let call_id = CallId::new(call.id.clone());
let tool_name = call.name.clone();
status.active_tool_calls.push(call_id.clone());
let started_at_ms = crate::harness::ids::now_ms();
let record = ctx.emit(AgentEvent::ToolStarted {
call_id: call_id.clone(),
tool_name: tool_name.clone(),
});
status.set_last_event(record.id);
let captured_input = self.policy.capture.tool_io.then(|| call.arguments.clone());
PreparedToolCall {
call_id,
tool_name,
captured_input,
started_at_ms,
}
}
#[allow(clippy::too_many_arguments)]
async fn finish_tool_call(
&self,
state: &State,
ctx: &mut RunContext<Ctx>,
run: &mut AgentRun,
status: &mut HarnessRunStatus,
messages: &mut Vec<Message>,
prepared: PreparedToolCall,
mut result: crate::harness::tool::ToolResult,
) -> Result<()> {
self.middleware
.run_after_tool(ctx, state, &mut result)
.await?;
run.tool_calls += 1;
status.tool_calls = run.tool_calls;
status.active_tool_calls.retain(|c| c != &prepared.call_id);
let captured_output = self
.policy
.capture
.tool_io
.then(|| Value::String(result.content.clone()));
let duration_ms = crate::harness::ids::now_ms().saturating_sub(prepared.started_at_ms);
let output_bytes = result.content.len() as u64;
let error = result.error.clone();
let record = ctx.emit(AgentEvent::ToolCompleted {
call_id: prepared.call_id,
tool_name: prepared.tool_name,
started_at_ms: Some(prepared.started_at_ms),
input: prepared.captured_input,
output: captured_output,
duration_ms: Some(duration_ms),
output_bytes: Some(output_bytes),
error,
});
status.set_last_event(record.id);
messages.push(Message::tool(
result.call_id.clone(),
result.content.clone(),
));
Ok(())
}
async fn execute_tools_serially(
&self,
state: &State,
ctx: &mut RunContext<Ctx>,
run: &mut AgentRun,
status: &mut HarnessRunStatus,
messages: &mut Vec<Message>,
tool_calls: Vec<ToolCall>,
) -> Result<()> {
for mut call in tool_calls {
let tool = match self.admit_tool_call(state, ctx, status, &mut call).await? {
ResolvedToolCall::Tool(tool) => tool,
ResolvedToolCall::ErrorMessage(message) => {
run.tool_calls += 1;
status.tool_calls = run.tool_calls;
messages.push(Message::tool(call.id.clone(), message));
continue;
}
};
let prepared = self.start_tool_call(ctx, status, &call);
let base = ToolCallBase { tool };
let remaining = self.call_budget(ctx);
let run_id = ctx.run_id().as_str().to_string();
let fut = self.middleware.run_wrapped_tool(ctx, state, call, &base);
let result = Self::with_call_budget(remaining, &run_id, "tool call", fut)
.await?
.into_result();
self.finish_tool_call(state, ctx, run, status, messages, prepared, result)
.await?;
}
Ok(())
}
async fn execute_tools_concurrently(
&self,
state: &State,
ctx: &mut RunContext<Ctx>,
run: &mut AgentRun,
status: &mut HarnessRunStatus,
messages: &mut Vec<Message>,
tool_calls: Vec<ToolCall>,
) -> Result<()> {
let mut slots: Vec<ToolSlot> = Vec::with_capacity(tool_calls.len());
let mut prepared: Vec<PreparedToolCall> = Vec::new();
let mut futures: Vec<_> = Vec::new();
for mut call in tool_calls {
let tool = match self.admit_tool_call(state, ctx, status, &mut call).await? {
ResolvedToolCall::Tool(tool) => tool,
ResolvedToolCall::ErrorMessage(message) => {
run.tool_calls += 1;
status.tool_calls = run.tool_calls;
slots.push(ToolSlot::Immediate {
call_id: call.id.clone(),
message,
});
continue;
}
};
prepared.push(self.start_tool_call(ctx, status, &call));
slots.push(ToolSlot::Execute);
let remaining = self.call_budget(ctx);
let run_id = ctx.run_id().as_str().to_string();
let exec_ctx = ToolExecutionContext::from_run_context(ctx);
futures.push(async move {
let fut = tool.call_with_context(state, call, exec_ctx);
Self::with_call_budget(remaining, &run_id, "tool call", fut).await
});
}
let results = futures::future::join_all(futures).await;
let mut executed = prepared.into_iter().zip(results);
for slot in slots {
match slot {
ToolSlot::Immediate { call_id, message } => {
messages.push(Message::tool(call_id, message));
}
ToolSlot::Execute => {
let (prepared, result) = executed
.next()
.expect("every Execute slot has a prepared/result pair");
let result = result?;
self.finish_tool_call(state, ctx, run, status, messages, prepared, result)
.await?;
}
}
}
Ok(())
}
}