use crate::message::{
self, AssistantContent, CallId, ToolCall, ToolName, ToolResultContent, UserContent,
};
use crate::tool::{ToolResult, Touches};
pub fn tool_result_output(call: CallId, name: ToolName, result: &ToolResult) -> UserContent {
UserContent::ToolResult(message::ToolResult {
call,
name,
content: result.output().clone().into_content(),
is_error: !result.is_success(),
})
}
pub fn tool_result_message(call: CallId, name: ToolName, message: String) -> UserContent {
UserContent::ToolResult(message::ToolResult {
call,
name,
content: vec![ToolResultContent::text(message)],
is_error: true,
})
}
pub fn invalid_arguments_feedback(tool: &str, raw: &str) -> String {
format!(
"The arguments for tool `{tool}` are not a JSON object: {}. \
Call the tool again with a JSON object as its arguments.",
echoed(raw)
)
}
pub fn output_limit_feedback(tool: &str, raw: &str) -> String {
format!(
"Not run: your reply reached the output limit before `{tool}`'s arguments ended. Send \
less in one call: split the work into smaller calls, such as writing part of a file \
and adding the rest with edits. The arguments began: {}",
echoed(raw)
)
}
pub const ECHOED_CHARS: usize = 200;
fn echoed(raw: &str) -> String {
match raw.char_indices().nth(ECHOED_CHARS) {
Some((end, _)) => format!(
"{}… ({} more characters)",
raw.get(..end).unwrap_or_default(),
raw.len().saturating_sub(end)
),
None => raw.to_owned(),
}
}
pub const TOOL_NOT_EXECUTED_DUE_TO_INVALID_PEER: &str =
"Tool not executed because another tool call in the same assistant turn was invalid.";
pub fn invalid_call_feedback(
content: &[AssistantContent],
invalid: &CallId,
feedback: &str,
) -> Vec<UserContent> {
content
.iter()
.filter_map(|part| match part {
AssistantContent::ToolCall(call) if &call.id == invalid => Some(tool_result_message(
call.id.clone(),
call.function.name.clone(),
feedback.to_owned(),
)),
AssistantContent::ToolCall(call) => Some(not_executed(call)),
_ => None,
})
.collect()
}
pub fn not_executed(call: &ToolCall) -> UserContent {
tool_result_message(
call.id.clone(),
call.function.name.clone(),
TOOL_NOT_EXECUTED_DUE_TO_INVALID_PEER.to_owned(),
)
}
pub fn arguments_refusal(parameters: &serde_json::Value, call: &ToolCall) -> Option<String> {
let name = call.function.name.as_str();
if let Some(raw) = &call.function.invalid_arguments {
return Some(invalid_arguments_feedback(name, raw));
}
let allows_more = parameters
.get("additionalProperties")
.is_some_and(|more| more != &serde_json::Value::Bool(false))
|| parameters.get("patternProperties").is_some();
let declared = parameters
.get("properties")
.and_then(serde_json::Value::as_object)
.filter(|_| !allows_more);
fn quoted<'a>(names: impl Iterator<Item = &'a String>) -> String {
names
.map(|arg| format!("`{arg}`"))
.collect::<Vec<_>>()
.join(", ")
}
let unknown = quoted(
call.function
.arguments
.keys()
.filter(|arg| declared.is_some_and(|declared| !declared.contains_key(arg.as_str()))),
);
if let Some(declared) = declared.filter(|_| !unknown.is_empty()) {
let mut known = quoted(declared.keys());
if known.is_empty() {
known = "none".to_owned();
}
return Some(format!(
"`{name}` has no argument {unknown}. Its arguments are: {known}. Call it again with \
only those."
));
}
const SHOWN: usize = 5;
let arguments = serde_json::Value::Object(call.function.arguments.clone());
let problems = crate::tool::validate::validate(parameters, &arguments).err()?;
let mut named: Vec<String> = problems
.iter()
.take(SHOWN)
.map(ToString::to_string)
.collect();
if let Some(more) = problems.len().checked_sub(SHOWN).filter(|more| *more > 0) {
named.push(format!("and {more} more"));
}
Some(format!(
"`{name}`'s arguments do not fit its schema: {}. Call it again with arguments that fit.",
named.join("; ")
))
}
pub fn not_finished(why: &str, started: bool, footprint: &Touches) -> String {
let why = why.trim().trim_end_matches('.');
match (started, footprint.reads_only()) {
(false, _) => format!("Not run: {why}."),
(true, true) => format!("Stopped: {why}. It only read; run it again if you still need it."),
(true, false) => {
format!("Stopped: {why}. It may have partly run; check before running it again.")
}
}
}
#[cfg(test)]
mod tests;