use crate::{AgentLimits, ChatMessage, LocalStateEffect, ToolCall, ToolDefinition, ToolExecutor};
use serde_json::Value;
pub(super) fn invalid_reason(call: &ToolCall, definitions: &[ToolDefinition]) -> Option<String> {
if !call.name.is_empty() && definitions.iter().any(|tool| tool.name == call.name) {
if call.arguments.is_object() {
return None;
}
return Some("arguments must be a JSON object".into());
}
let available = definitions
.iter()
.map(|tool| tool.name.as_str())
.collect::<Vec<_>>()
.join(", ");
Some(format!(
"unknown tool '{}'; available tools: {available}",
call.name
))
}
pub(super) fn external_side_effect_gated(definition: &ToolDefinition) -> bool {
definition.effect.external_side_effect && !definition.effect.requires_approval
}
pub(super) fn candidate_denied(definition: &ToolDefinition, limits: &AgentLimits) -> bool {
definition.effect.local_state == LocalStateEffect::WriteCandidate
&& !limits.permit_candidate_writes
}
pub(super) fn auto_runnable(definition: &ToolDefinition, limits: &AgentLimits) -> bool {
!definition.effect.requires_approval
&& !external_side_effect_gated(definition)
&& !candidate_denied(definition, limits)
}
pub(super) async fn execute(
tools: &dyn ToolExecutor,
name: &str,
arguments: Value,
read_only: bool,
) -> (Value, &'static str) {
match tools.execute(name, arguments).await {
Ok(value) => (
value,
if read_only {
"read-only database tool completed"
} else {
"local-state write completed"
},
),
Err(error) => (
serde_json::json!({"error": error.to_string()}),
if read_only {
"read-only database tool failed"
} else {
"local-state write failed"
},
),
}
}
pub(super) async fn execute_batch(
tools: &dyn ToolExecutor,
calls: &[ToolCall],
definitions: &[ToolDefinition],
) -> Vec<(Value, &'static str)> {
use futures_util::{StreamExt, stream};
let pending = calls.iter().map(|call| {
let read_only = definitions
.iter()
.find(|definition| definition.name == call.name)
.is_some_and(|definition| definition.read_only);
let name = call.name.clone();
let arguments = call.arguments.clone();
async move { execute(tools, &name, arguments, read_only).await }
});
stream::iter(pending)
.buffered(MAX_CONCURRENT_TOOL_CALLS)
.collect()
.await
}
const MAX_CONCURRENT_TOOL_CALLS: usize = 4;
pub(super) fn tool_message(id: String, result: Value, byte_budget: usize) -> (ChatMessage, bool) {
let cap = byte_budget.min(MAX_TOOL_MESSAGE_BYTES);
let (content, truncated) = bounded_json(&result, cap);
(
ChatMessage {
role: "tool".into(),
content,
tool_calls: Vec::new(),
tool_call_id: Some(id),
},
truncated,
)
}
const MAX_TOOL_MESSAGE_BYTES: usize = 65_536;
fn bounded_json(value: &Value, cap: usize) -> (String, bool) {
let text = serde_json::to_string(value)
.unwrap_or_else(|_| "{\"error\":\"tool result unavailable\"}".into());
if text.len() <= cap {
(text, false)
} else {
let marker = "…[truncated: tool result exceeded the conversation byte budget]";
let head = cap.saturating_sub(marker.len());
let mut truncated = String::from(&text[..floor_boundary(&text, head)]);
truncated.push_str(marker);
(truncated, true)
}
}
pub(super) fn floor_boundary(text: &str, mut idx: usize) -> usize {
if idx >= text.len() {
idx = text.len();
}
while idx > 0 && !text.is_char_boundary(idx) {
idx -= 1;
}
idx
}