fix: Don't terminate agent loops early for null tool output
This commit is contained in:
+30
-12
@@ -144,7 +144,6 @@ pub async fn eval_tool_calls(
|
|||||||
if calls.is_empty() {
|
if calls.is_empty() {
|
||||||
bail!("The request was aborted because an infinite loop of function calls was detected.")
|
bail!("The request was aborted because an infinite loop of function calls was detected.")
|
||||||
}
|
}
|
||||||
let mut is_all_null = true;
|
|
||||||
for call in calls {
|
for call in calls {
|
||||||
if let Some(msg) = ctx.tool_scope.tool_tracker.check_loop(&call.clone()) {
|
if let Some(msg) = ctx.tool_scope.tool_tracker.check_loop(&call.clone()) {
|
||||||
let dup_msg = format!("{{\"tool_call_loop_alert\":{}}}", msg.trim());
|
let dup_msg = format!("{{\"tool_call_loop_alert\":{}}}", msg.trim());
|
||||||
@@ -154,19 +153,10 @@ pub async fn eval_tool_calls(
|
|||||||
);
|
);
|
||||||
let val = json!(dup_msg);
|
let val = json!(dup_msg);
|
||||||
output.push(ToolResult::new(call, val));
|
output.push(ToolResult::new(call, val));
|
||||||
is_all_null = false;
|
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
let mut result = call.eval(ctx).await?;
|
let result = call.eval(ctx).await?;
|
||||||
if result.is_null() {
|
output.push(ToolResult::new(call, normalize_tool_result(result)));
|
||||||
result = json!("DONE");
|
|
||||||
} else {
|
|
||||||
is_all_null = false;
|
|
||||||
}
|
|
||||||
output.push(ToolResult::new(call, result));
|
|
||||||
}
|
|
||||||
if is_all_null {
|
|
||||||
output = vec![];
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if !output.is_empty() {
|
if !output.is_empty() {
|
||||||
@@ -196,6 +186,19 @@ pub async fn eval_tool_calls(
|
|||||||
Ok(output)
|
Ok(output)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Tools that succeed silently (e.g. `mkdir -p` via execute_command) evaluate to
|
||||||
|
/// `Null`. Substitute a concrete `"DONE"` marker so every call produces a
|
||||||
|
/// `ToolResult`: agentic loops (graph llm nodes, spawned agents, the REPL) treat
|
||||||
|
/// an empty `tool_results` as "the LLM concluded", so dropping silent results
|
||||||
|
/// would prematurely terminate a turn that called only silent tools.
|
||||||
|
fn normalize_tool_result(result: Value) -> Value {
|
||||||
|
if result.is_null() {
|
||||||
|
json!("DONE")
|
||||||
|
} else {
|
||||||
|
result
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone, Deserialize, Serialize)]
|
#[derive(Debug, Clone, Deserialize, Serialize)]
|
||||||
pub struct ToolResult {
|
pub struct ToolResult {
|
||||||
pub call: ToolCall,
|
pub call: ToolCall,
|
||||||
@@ -1527,6 +1530,21 @@ mod tests {
|
|||||||
ToolCall::new(name.to_string(), args, Some("id1".to_string()))
|
ToolCall::new(name.to_string(), args, Some("id1".to_string()))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn normalize_tool_result_substitutes_done_for_null() {
|
||||||
|
assert_eq!(normalize_tool_result(Value::Null), json!("DONE"));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn normalize_tool_result_preserves_non_null_values() {
|
||||||
|
assert_eq!(
|
||||||
|
normalize_tool_result(json!({"output": "hi"})),
|
||||||
|
json!({"output": "hi"})
|
||||||
|
);
|
||||||
|
assert_eq!(normalize_tool_result(json!("")), json!(""));
|
||||||
|
assert_eq!(normalize_tool_result(json!(false)), json!(false));
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn toolcall_new_sets_fields() {
|
fn toolcall_new_sets_fields() {
|
||||||
let tc = ToolCall::new("my_tool".into(), json!({"x": 1}), Some("call-1".into()));
|
let tc = ToolCall::new("my_tool".into(), json!({"x": 1}), Some("call-1".into()));
|
||||||
|
|||||||
Reference in New Issue
Block a user