rig_core/tool/
feedback.rs1use crate::message::{
13 self, AssistantContent, CallId, ToolCall, ToolName, ToolResultContent, UserContent,
14};
15use crate::tool::{ToolResult, Touches};
16
17pub fn tool_result_output(call: CallId, name: ToolName, result: &ToolResult) -> UserContent {
20 UserContent::ToolResult(message::ToolResult {
21 call,
22 name,
23 content: result.output().clone().into_content(),
24 is_error: !result.is_success(),
25 })
26}
27
28pub fn tool_result_message(call: CallId, name: ToolName, message: String) -> UserContent {
32 UserContent::ToolResult(message::ToolResult {
33 call,
34 name,
35 content: vec![ToolResultContent::text(message)],
36 is_error: true,
37 })
38}
39
40pub fn invalid_arguments_feedback(tool: &str, raw: &str) -> String {
44 format!(
45 "The arguments for tool `{tool}` are not a JSON object: {}. \
46 Call the tool again with a JSON object as its arguments.",
47 echoed(raw)
48 )
49}
50
51pub fn output_limit_feedback(tool: &str, raw: &str) -> String {
58 format!(
59 "Not run: your reply reached the output limit before `{tool}`'s arguments ended. Send \
60 less in one call: split the work into smaller calls, such as writing part of a file \
61 and adding the rest with edits. The arguments began: {}",
62 echoed(raw)
63 )
64}
65
66pub const ECHOED_CHARS: usize = 200;
68
69fn echoed(raw: &str) -> String {
71 match raw.char_indices().nth(ECHOED_CHARS) {
72 Some((end, _)) => format!(
73 "{}… ({} more characters)",
74 raw.get(..end).unwrap_or_default(),
75 raw.len().saturating_sub(end)
76 ),
77 None => raw.to_owned(),
78 }
79}
80
81pub const TOOL_NOT_EXECUTED_DUE_TO_INVALID_PEER: &str =
84 "Tool not executed because another tool call in the same assistant turn was invalid.";
85
86pub fn invalid_call_feedback(
90 content: &[AssistantContent],
91 invalid: &CallId,
92 feedback: &str,
93) -> Vec<UserContent> {
94 content
95 .iter()
96 .filter_map(|part| match part {
97 AssistantContent::ToolCall(call) if &call.id == invalid => Some(tool_result_message(
98 call.id.clone(),
99 call.function.name.clone(),
100 feedback.to_owned(),
101 )),
102 AssistantContent::ToolCall(call) => Some(not_executed(call)),
103 _ => None,
104 })
105 .collect()
106}
107
108pub fn not_executed(call: &ToolCall) -> UserContent {
111 tool_result_message(
112 call.id.clone(),
113 call.function.name.clone(),
114 TOOL_NOT_EXECUTED_DUE_TO_INVALID_PEER.to_owned(),
115 )
116}
117
118pub fn arguments_refusal(parameters: &serde_json::Value, call: &ToolCall) -> Option<String> {
132 let name = call.function.name.as_str();
133 if let Some(raw) = &call.function.invalid_arguments {
134 return Some(invalid_arguments_feedback(name, raw));
135 }
136 let allows_more = parameters
137 .get("additionalProperties")
138 .is_some_and(|more| more != &serde_json::Value::Bool(false))
139 || parameters.get("patternProperties").is_some();
140 let declared = parameters
141 .get("properties")
142 .and_then(serde_json::Value::as_object)
143 .filter(|_| !allows_more);
144 fn quoted<'a>(names: impl Iterator<Item = &'a String>) -> String {
145 names
146 .map(|arg| format!("`{arg}`"))
147 .collect::<Vec<_>>()
148 .join(", ")
149 }
150 let unknown = quoted(
151 call.function
152 .arguments
153 .keys()
154 .filter(|arg| declared.is_some_and(|declared| !declared.contains_key(arg.as_str()))),
155 );
156 if let Some(declared) = declared.filter(|_| !unknown.is_empty()) {
157 let mut known = quoted(declared.keys());
158 if known.is_empty() {
159 known = "none".to_owned();
160 }
161 return Some(format!(
162 "`{name}` has no argument {unknown}. Its arguments are: {known}. Call it again with \
163 only those."
164 ));
165 }
166 const SHOWN: usize = 5;
167 let arguments = serde_json::Value::Object(call.function.arguments.clone());
168 let problems = crate::tool::validate::validate(parameters, &arguments).err()?;
169 let mut named: Vec<String> = problems
170 .iter()
171 .take(SHOWN)
172 .map(ToString::to_string)
173 .collect();
174 if let Some(more) = problems.len().checked_sub(SHOWN).filter(|more| *more > 0) {
175 named.push(format!("and {more} more"));
176 }
177 Some(format!(
178 "`{name}`'s arguments do not fit its schema: {}. Call it again with arguments that fit.",
179 named.join("; ")
180 ))
181}
182
183pub fn not_finished(why: &str, started: bool, footprint: &Touches) -> String {
202 let why = why.trim().trim_end_matches('.');
203 match (started, footprint.reads_only()) {
204 (false, _) => format!("Not run: {why}."),
205 (true, true) => format!("Stopped: {why}. It only read; run it again if you still need it."),
206 (true, false) => {
207 format!("Stopped: {why}. It may have partly run; check before running it again.")
208 }
209 }
210}
211
212#[cfg(test)]
213mod tests;