Write a partial report instead of erroring out when research hits max turns #8

Merged
schaefera merged 6 commits from worktree-deep-research-max-turns-report into master 2026-08-18 11:03:03 +00:00
2 changed files with 102 additions and 5 deletions
Showing only changes of commit e446046744 - Show all commits

View file

@ -17,3 +17,6 @@ serde = { workspace = true }
tokio = { workspace = true }
tracing = { workspace = true }
tracing-subscriber = { workspace = true }
[dev-dependencies]
serde_json = "1"

View file

@ -121,11 +121,10 @@ fn partial_findings_from_history(chat_history: &[Message]) -> String {
})
.collect();
if sections.is_empty() {
"The researcher exhausted its turn budget before gathering any usable evidence."
.to_string()
} else {
sections.join("\n\n")
match sections.is_empty() {
true => "The researcher exhausted its turn budget before gathering any usable evidence."
.to_string(),
false => sections.join("\n"),
}
}
@ -313,3 +312,98 @@ async fn write_report(
None => report,
})
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn truncate_leaves_short_text_untouched() {
assert_eq!(truncate("hello", 10), "hello");
}
#[test]
fn truncate_leaves_exact_length_text_untouched() {
assert_eq!(truncate("hello", 5), "hello");
}
#[test]
fn truncate_cuts_long_text_and_marks_it() {
assert_eq!(truncate("hello world", 5), "hello [...truncated]");
}
#[test]
fn assistant_text_extracts_text_blocks() {
let message = Message::assistant("found it");
assert_eq!(assistant_text(&message), vec!["found it".to_string()]);
}
#[test]
fn assistant_text_ignores_tool_calls() {
let message = Message::Assistant {
id: None,
content: rig::OneOrMany::one(AssistantContent::tool_call(
"call-1",
"search_web",
serde_json::json!({ "query": "test" }),
)),
};
assert!(assistant_text(&message).is_empty());
}
#[test]
fn assistant_text_ignores_non_assistant_messages() {
assert!(assistant_text(&Message::user("hi")).is_empty());
assert!(assistant_text(&Message::system("be careful")).is_empty());
}
#[test]
fn tool_result_text_extracts_and_truncates() {
let short = Message::tool_result("call-1", "short result");
assert_eq!(tool_result_text(&short, 100), vec!["short result".to_string()]);
let long = Message::tool_result("call-2", "0123456789");
assert_eq!(tool_result_text(&long, 5), vec!["01234 [...truncated]".to_string()]);
}
#[test]
fn tool_result_text_ignores_non_tool_result_content() {
assert!(tool_result_text(&Message::user("plain text, no tool result"), 100).is_empty());
assert!(tool_result_text(&Message::assistant("also ignored"), 100).is_empty());
}
#[test]
fn partial_findings_from_history_falls_back_when_nothing_usable() {
let history = vec![
Message::system("preamble"),
Message::Assistant {
id: None,
content: rig::OneOrMany::one(AssistantContent::tool_call(
"call-1",
"search_web",
serde_json::json!({ "query": "test" }),
)),
},
];
assert_eq!(
partial_findings_from_history(&history),
"The researcher exhausted its turn budget before gathering any usable evidence."
);
}
#[test]
fn partial_findings_from_history_collects_assistant_text_and_tool_results_in_order() {
let history = vec![
Message::assistant("Checking sources..."),
Message::tool_result("call-1", "Result A [1]"),
Message::assistant("Cross-checking..."),
Message::tool_result("call-2", "Result B [2]"),
];
assert_eq!(
partial_findings_from_history(&history),
"Checking sources...\nResult A [1]\nCross-checking...\nResult B [2]"
);
}
}