doubleo7/src/history.rs

293 lines
9.6 KiB
Rust
Raw Normal View History

Split core.rs into one file per concern core.rs had grown into a 612-line grab-bag mixing six unrelated concerns: CLI arg parsing, logging setup, top-level orchestration, the researcher agent phase, chat-history reconstruction utilities, the summarizer agent phase, and the writer agent phase — while review.rs, tools.rs, stream.rs, and progress.rs already correctly isolated their own concerns. This splits core.rs to match that existing pattern instead of being the one file that doesn't follow it: - cli.rs — Cli struct + DEFAULT_TOPIC - observability.rs — initialize_observability - models.rs — RESEARCHER_MODEL / WRITER_MODEL (previously duplicated across call sites, now a single source of truth) - history.rs — pure chat-history parsing/reconstruction helpers (partial_findings_from_history, annotated_transcript_from_history, and their private helpers), plus their unit tests. Also dedupes MAX_TOOL_RESULT_CHARS, which was previously defined twice. - researcher.rs — gather_findings + GatheredFindings (the tool-calling research phase) - summarizer.rs — summarize_partial_history (the max-turns recovery agent) - writer.rs — write_report - research.rs — the top-level research() orchestration loop main.rs now only does argument parsing, logging setup, and the top-level call — no orchestration logic of its own. Unit tests stay co-located with the code they test per Rust convention (not pulled into separate files) rather than under "prefer new files" — that applies to production code organization here. No behavior changes; cargo test/clippy/fmt all clean.
2026-08-18 10:59:03 +00:00
use rig::completion::message::{ToolResultContent, UserContent};
use rig::completion::{AssistantContent, Message};
const MAX_TOOL_RESULT_CHARS: usize = 2000;
/// Best-effort reconstruction of research material from a chat history left
/// behind when the researcher hit its turn budget mid-investigation: whatever
/// prose the model wrote between tool calls, plus the raw text of every tool
/// result (search snippets, fetched pages), truncated so one huge page can't
/// crowd out everything else that was found.
pub(crate) fn partial_findings_from_history(chat_history: &[Message]) -> String {
let sections: Vec<String> = chat_history
.iter()
.flat_map(|message| {
extract_assistant_text(message)
.into_iter()
.chain(tool_result_text(message, MAX_TOOL_RESULT_CHARS))
})
.collect();
match sections.is_empty() {
true => "The researcher exhausted its turn budget before gathering any usable evidence."
.to_string(),
false => sections.join("\n"),
}
}
/// Plain-text blocks from an assistant message, if any.
fn extract_assistant_text(message: &Message) -> Vec<String> {
let Message::Assistant { content, .. } = message else {
return Vec::new();
};
content
.iter()
.filter_map(|item: &AssistantContent| match item {
AssistantContent::Text(text) => Some(text.text().to_string()),
_ => None,
})
.collect()
}
/// Plain-text tool results attached to a user message (that's where rig
/// places them), each truncated to `max_chars` so one huge fetched page
/// can't crowd out everything else that was found.
fn tool_result_text(message: &Message, max_chars: usize) -> Vec<String> {
let Message::User { content } = message else {
return Vec::new();
};
content
.iter()
.filter_map(|item: &UserContent| match item {
UserContent::ToolResult(tool_result) => Some(tool_result),
_ => None,
})
.flat_map(|tool_result| tool_result.content.iter())
.filter_map(
|tool_result_content: &ToolResultContent| match tool_result_content {
ToolResultContent::Text(text) => Some(truncate(text.text(), max_chars)),
_ => None,
},
)
.collect()
}
fn truncate(text: &str, max_chars: usize) -> String {
let mut result = text.to_string();
if result.len() > max_chars {
result.truncate(max_chars);
result.push_str(" ...[truncated]");
}
result
}
/// Chronological transcript of a partial research run, annotated with tool
/// calls (so a fetch's URL or a search's query stays attached to its
/// result) rather than just the bare result text `partial_findings_from_history`
/// collects — the summarizer agent needs that context to attribute facts to
/// the right source.
pub(crate) fn annotated_transcript_from_history(chat_history: &[Message]) -> String {
chat_history
.iter()
.flat_map(transcript_lines)
.collect::<Vec<String>>()
.join("\n")
}
fn transcript_lines(message: &Message) -> Vec<String> {
match message {
Message::Assistant { content, .. } => content
.iter()
.filter_map(|item| match item {
AssistantContent::Text(text) => Some(format!("Note: {}", text.text())),
AssistantContent::ToolCall(call) => Some(format!(
"Called {}({})",
call.function.name, call.function.arguments
)),
_ => None,
})
.collect(),
Message::User { content } => content
.iter()
.filter_map(|item| match item {
UserContent::ToolResult(result) => Some(result),
_ => None,
})
.flat_map(|result| result.content.iter())
.filter_map(|part| match part {
ToolResultContent::Text(text) => Some(format!(
"Result: {}",
truncate(text.text(), MAX_TOOL_RESULT_CHARS)
)),
_ => None,
})
.collect(),
Message::System { .. } => Vec::new(),
}
}
#[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!(
extract_assistant_text(&message),
vec!["found it".to_string()]
);
}
#[test]
fn assistant_text_ignores_tool_calls() {
let message = Message::Assistant {
id: None,
2026-08-19 10:11:47 +00:00
content: vec![AssistantContent::tool_call(
Split core.rs into one file per concern core.rs had grown into a 612-line grab-bag mixing six unrelated concerns: CLI arg parsing, logging setup, top-level orchestration, the researcher agent phase, chat-history reconstruction utilities, the summarizer agent phase, and the writer agent phase — while review.rs, tools.rs, stream.rs, and progress.rs already correctly isolated their own concerns. This splits core.rs to match that existing pattern instead of being the one file that doesn't follow it: - cli.rs — Cli struct + DEFAULT_TOPIC - observability.rs — initialize_observability - models.rs — RESEARCHER_MODEL / WRITER_MODEL (previously duplicated across call sites, now a single source of truth) - history.rs — pure chat-history parsing/reconstruction helpers (partial_findings_from_history, annotated_transcript_from_history, and their private helpers), plus their unit tests. Also dedupes MAX_TOOL_RESULT_CHARS, which was previously defined twice. - researcher.rs — gather_findings + GatheredFindings (the tool-calling research phase) - summarizer.rs — summarize_partial_history (the max-turns recovery agent) - writer.rs — write_report - research.rs — the top-level research() orchestration loop main.rs now only does argument parsing, logging setup, and the top-level call — no orchestration logic of its own. Unit tests stay co-located with the code they test per Rust convention (not pulled into separate files) rather than under "prefer new files" — that applies to production code organization here. No behavior changes; cargo test/clippy/fmt all clean.
2026-08-18 10:59:03 +00:00
"call-1",
"search_web",
2026-08-19 10:11:47 +00:00
serde_json::json!({ "query": "test"}),
)],
Split core.rs into one file per concern core.rs had grown into a 612-line grab-bag mixing six unrelated concerns: CLI arg parsing, logging setup, top-level orchestration, the researcher agent phase, chat-history reconstruction utilities, the summarizer agent phase, and the writer agent phase — while review.rs, tools.rs, stream.rs, and progress.rs already correctly isolated their own concerns. This splits core.rs to match that existing pattern instead of being the one file that doesn't follow it: - cli.rs — Cli struct + DEFAULT_TOPIC - observability.rs — initialize_observability - models.rs — RESEARCHER_MODEL / WRITER_MODEL (previously duplicated across call sites, now a single source of truth) - history.rs — pure chat-history parsing/reconstruction helpers (partial_findings_from_history, annotated_transcript_from_history, and their private helpers), plus their unit tests. Also dedupes MAX_TOOL_RESULT_CHARS, which was previously defined twice. - researcher.rs — gather_findings + GatheredFindings (the tool-calling research phase) - summarizer.rs — summarize_partial_history (the max-turns recovery agent) - writer.rs — write_report - research.rs — the top-level research() orchestration loop main.rs now only does argument parsing, logging setup, and the top-level call — no orchestration logic of its own. Unit tests stay co-located with the code they test per Rust convention (not pulled into separate files) rather than under "prefer new files" — that applies to production code organization here. No behavior changes; cargo test/clippy/fmt all clean.
2026-08-18 10:59:03 +00:00
};
assert!(extract_assistant_text(&message).is_empty());
}
#[test]
fn assistant_text_ignores_non_assistant_messages() {
assert!(extract_assistant_text(&Message::user("hi")).is_empty());
assert!(extract_assistant_text(&Message::system("be careful")).is_empty());
}
#[test]
fn tool_result_text_extracts_and_truncates() {
2026-08-19 10:11:47 +00:00
let short = Message::tool_result("call-1", "call-1-name", "short result");
Split core.rs into one file per concern core.rs had grown into a 612-line grab-bag mixing six unrelated concerns: CLI arg parsing, logging setup, top-level orchestration, the researcher agent phase, chat-history reconstruction utilities, the summarizer agent phase, and the writer agent phase — while review.rs, tools.rs, stream.rs, and progress.rs already correctly isolated their own concerns. This splits core.rs to match that existing pattern instead of being the one file that doesn't follow it: - cli.rs — Cli struct + DEFAULT_TOPIC - observability.rs — initialize_observability - models.rs — RESEARCHER_MODEL / WRITER_MODEL (previously duplicated across call sites, now a single source of truth) - history.rs — pure chat-history parsing/reconstruction helpers (partial_findings_from_history, annotated_transcript_from_history, and their private helpers), plus their unit tests. Also dedupes MAX_TOOL_RESULT_CHARS, which was previously defined twice. - researcher.rs — gather_findings + GatheredFindings (the tool-calling research phase) - summarizer.rs — summarize_partial_history (the max-turns recovery agent) - writer.rs — write_report - research.rs — the top-level research() orchestration loop main.rs now only does argument parsing, logging setup, and the top-level call — no orchestration logic of its own. Unit tests stay co-located with the code they test per Rust convention (not pulled into separate files) rather than under "prefer new files" — that applies to production code organization here. No behavior changes; cargo test/clippy/fmt all clean.
2026-08-18 10:59:03 +00:00
assert_eq!(
tool_result_text(&short, 100),
vec!["short result".to_string()]
);
2026-08-19 10:11:47 +00:00
let long = Message::tool_result("call-2", "call-2-result", "0123456789");
Split core.rs into one file per concern core.rs had grown into a 612-line grab-bag mixing six unrelated concerns: CLI arg parsing, logging setup, top-level orchestration, the researcher agent phase, chat-history reconstruction utilities, the summarizer agent phase, and the writer agent phase — while review.rs, tools.rs, stream.rs, and progress.rs already correctly isolated their own concerns. This splits core.rs to match that existing pattern instead of being the one file that doesn't follow it: - cli.rs — Cli struct + DEFAULT_TOPIC - observability.rs — initialize_observability - models.rs — RESEARCHER_MODEL / WRITER_MODEL (previously duplicated across call sites, now a single source of truth) - history.rs — pure chat-history parsing/reconstruction helpers (partial_findings_from_history, annotated_transcript_from_history, and their private helpers), plus their unit tests. Also dedupes MAX_TOOL_RESULT_CHARS, which was previously defined twice. - researcher.rs — gather_findings + GatheredFindings (the tool-calling research phase) - summarizer.rs — summarize_partial_history (the max-turns recovery agent) - writer.rs — write_report - research.rs — the top-level research() orchestration loop main.rs now only does argument parsing, logging setup, and the top-level call — no orchestration logic of its own. Unit tests stay co-located with the code they test per Rust convention (not pulled into separate files) rather than under "prefer new files" — that applies to production code organization here. No behavior changes; cargo test/clippy/fmt all clean.
2026-08-18 10:59:03 +00:00
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,
2026-08-19 10:11:47 +00:00
content: vec![AssistantContent::tool_call(
Split core.rs into one file per concern core.rs had grown into a 612-line grab-bag mixing six unrelated concerns: CLI arg parsing, logging setup, top-level orchestration, the researcher agent phase, chat-history reconstruction utilities, the summarizer agent phase, and the writer agent phase — while review.rs, tools.rs, stream.rs, and progress.rs already correctly isolated their own concerns. This splits core.rs to match that existing pattern instead of being the one file that doesn't follow it: - cli.rs — Cli struct + DEFAULT_TOPIC - observability.rs — initialize_observability - models.rs — RESEARCHER_MODEL / WRITER_MODEL (previously duplicated across call sites, now a single source of truth) - history.rs — pure chat-history parsing/reconstruction helpers (partial_findings_from_history, annotated_transcript_from_history, and their private helpers), plus their unit tests. Also dedupes MAX_TOOL_RESULT_CHARS, which was previously defined twice. - researcher.rs — gather_findings + GatheredFindings (the tool-calling research phase) - summarizer.rs — summarize_partial_history (the max-turns recovery agent) - writer.rs — write_report - research.rs — the top-level research() orchestration loop main.rs now only does argument parsing, logging setup, and the top-level call — no orchestration logic of its own. Unit tests stay co-located with the code they test per Rust convention (not pulled into separate files) rather than under "prefer new files" — that applies to production code organization here. No behavior changes; cargo test/clippy/fmt all clean.
2026-08-18 10:59:03 +00:00
"call-1",
"search_web",
serde_json::json!({ "query": "test" }),
2026-08-19 10:11:47 +00:00
)],
Split core.rs into one file per concern core.rs had grown into a 612-line grab-bag mixing six unrelated concerns: CLI arg parsing, logging setup, top-level orchestration, the researcher agent phase, chat-history reconstruction utilities, the summarizer agent phase, and the writer agent phase — while review.rs, tools.rs, stream.rs, and progress.rs already correctly isolated their own concerns. This splits core.rs to match that existing pattern instead of being the one file that doesn't follow it: - cli.rs — Cli struct + DEFAULT_TOPIC - observability.rs — initialize_observability - models.rs — RESEARCHER_MODEL / WRITER_MODEL (previously duplicated across call sites, now a single source of truth) - history.rs — pure chat-history parsing/reconstruction helpers (partial_findings_from_history, annotated_transcript_from_history, and their private helpers), plus their unit tests. Also dedupes MAX_TOOL_RESULT_CHARS, which was previously defined twice. - researcher.rs — gather_findings + GatheredFindings (the tool-calling research phase) - summarizer.rs — summarize_partial_history (the max-turns recovery agent) - writer.rs — write_report - research.rs — the top-level research() orchestration loop main.rs now only does argument parsing, logging setup, and the top-level call — no orchestration logic of its own. Unit tests stay co-located with the code they test per Rust convention (not pulled into separate files) rather than under "prefer new files" — that applies to production code organization here. No behavior changes; cargo test/clippy/fmt all clean.
2026-08-18 10:59:03 +00:00
},
];
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..."),
2026-08-19 10:11:47 +00:00
Message::tool_result("call-1", "call-1-name", "Result A [1]"),
Split core.rs into one file per concern core.rs had grown into a 612-line grab-bag mixing six unrelated concerns: CLI arg parsing, logging setup, top-level orchestration, the researcher agent phase, chat-history reconstruction utilities, the summarizer agent phase, and the writer agent phase — while review.rs, tools.rs, stream.rs, and progress.rs already correctly isolated their own concerns. This splits core.rs to match that existing pattern instead of being the one file that doesn't follow it: - cli.rs — Cli struct + DEFAULT_TOPIC - observability.rs — initialize_observability - models.rs — RESEARCHER_MODEL / WRITER_MODEL (previously duplicated across call sites, now a single source of truth) - history.rs — pure chat-history parsing/reconstruction helpers (partial_findings_from_history, annotated_transcript_from_history, and their private helpers), plus their unit tests. Also dedupes MAX_TOOL_RESULT_CHARS, which was previously defined twice. - researcher.rs — gather_findings + GatheredFindings (the tool-calling research phase) - summarizer.rs — summarize_partial_history (the max-turns recovery agent) - writer.rs — write_report - research.rs — the top-level research() orchestration loop main.rs now only does argument parsing, logging setup, and the top-level call — no orchestration logic of its own. Unit tests stay co-located with the code they test per Rust convention (not pulled into separate files) rather than under "prefer new files" — that applies to production code organization here. No behavior changes; cargo test/clippy/fmt all clean.
2026-08-18 10:59:03 +00:00
Message::assistant("Cross-checking..."),
2026-08-19 10:11:47 +00:00
Message::tool_result("call-2", "call-2-name", "Result B [2]"),
Split core.rs into one file per concern core.rs had grown into a 612-line grab-bag mixing six unrelated concerns: CLI arg parsing, logging setup, top-level orchestration, the researcher agent phase, chat-history reconstruction utilities, the summarizer agent phase, and the writer agent phase — while review.rs, tools.rs, stream.rs, and progress.rs already correctly isolated their own concerns. This splits core.rs to match that existing pattern instead of being the one file that doesn't follow it: - cli.rs — Cli struct + DEFAULT_TOPIC - observability.rs — initialize_observability - models.rs — RESEARCHER_MODEL / WRITER_MODEL (previously duplicated across call sites, now a single source of truth) - history.rs — pure chat-history parsing/reconstruction helpers (partial_findings_from_history, annotated_transcript_from_history, and their private helpers), plus their unit tests. Also dedupes MAX_TOOL_RESULT_CHARS, which was previously defined twice. - researcher.rs — gather_findings + GatheredFindings (the tool-calling research phase) - summarizer.rs — summarize_partial_history (the max-turns recovery agent) - writer.rs — write_report - research.rs — the top-level research() orchestration loop main.rs now only does argument parsing, logging setup, and the top-level call — no orchestration logic of its own. Unit tests stay co-located with the code they test per Rust convention (not pulled into separate files) rather than under "prefer new files" — that applies to production code organization here. No behavior changes; cargo test/clippy/fmt all clean.
2026-08-18 10:59:03 +00:00
];
assert_eq!(
partial_findings_from_history(&history),
"Checking sources...\nResult A [1]\nCross-checking...\nResult B [2]"
);
}
#[test]
fn transcript_lines_prefixes_assistant_notes() {
let message = Message::assistant("still checking this claim");
assert_eq!(
transcript_lines(&message),
vec!["Note: still checking this claim".to_string()]
);
}
#[test]
fn transcript_lines_renders_tool_calls_with_their_arguments() {
let message = Message::Assistant {
id: None,
2026-08-19 10:11:47 +00:00
content: vec![AssistantContent::tool_call(
Split core.rs into one file per concern core.rs had grown into a 612-line grab-bag mixing six unrelated concerns: CLI arg parsing, logging setup, top-level orchestration, the researcher agent phase, chat-history reconstruction utilities, the summarizer agent phase, and the writer agent phase — while review.rs, tools.rs, stream.rs, and progress.rs already correctly isolated their own concerns. This splits core.rs to match that existing pattern instead of being the one file that doesn't follow it: - cli.rs — Cli struct + DEFAULT_TOPIC - observability.rs — initialize_observability - models.rs — RESEARCHER_MODEL / WRITER_MODEL (previously duplicated across call sites, now a single source of truth) - history.rs — pure chat-history parsing/reconstruction helpers (partial_findings_from_history, annotated_transcript_from_history, and their private helpers), plus their unit tests. Also dedupes MAX_TOOL_RESULT_CHARS, which was previously defined twice. - researcher.rs — gather_findings + GatheredFindings (the tool-calling research phase) - summarizer.rs — summarize_partial_history (the max-turns recovery agent) - writer.rs — write_report - research.rs — the top-level research() orchestration loop main.rs now only does argument parsing, logging setup, and the top-level call — no orchestration logic of its own. Unit tests stay co-located with the code they test per Rust convention (not pulled into separate files) rather than under "prefer new files" — that applies to production code organization here. No behavior changes; cargo test/clippy/fmt all clean.
2026-08-18 10:59:03 +00:00
"call-1",
"search_web",
serde_json::json!({ "query": "uruguay senior engineers" }),
2026-08-19 10:11:47 +00:00
)],
Split core.rs into one file per concern core.rs had grown into a 612-line grab-bag mixing six unrelated concerns: CLI arg parsing, logging setup, top-level orchestration, the researcher agent phase, chat-history reconstruction utilities, the summarizer agent phase, and the writer agent phase — while review.rs, tools.rs, stream.rs, and progress.rs already correctly isolated their own concerns. This splits core.rs to match that existing pattern instead of being the one file that doesn't follow it: - cli.rs — Cli struct + DEFAULT_TOPIC - observability.rs — initialize_observability - models.rs — RESEARCHER_MODEL / WRITER_MODEL (previously duplicated across call sites, now a single source of truth) - history.rs — pure chat-history parsing/reconstruction helpers (partial_findings_from_history, annotated_transcript_from_history, and their private helpers), plus their unit tests. Also dedupes MAX_TOOL_RESULT_CHARS, which was previously defined twice. - researcher.rs — gather_findings + GatheredFindings (the tool-calling research phase) - summarizer.rs — summarize_partial_history (the max-turns recovery agent) - writer.rs — write_report - research.rs — the top-level research() orchestration loop main.rs now only does argument parsing, logging setup, and the top-level call — no orchestration logic of its own. Unit tests stay co-located with the code they test per Rust convention (not pulled into separate files) rather than under "prefer new files" — that applies to production code organization here. No behavior changes; cargo test/clippy/fmt all clean.
2026-08-18 10:59:03 +00:00
};
assert_eq!(
transcript_lines(&message),
vec!["Called search_web({\"query\":\"uruguay senior engineers\"})".to_string()]
);
}
#[test]
fn transcript_lines_prefixes_and_truncates_tool_results() {
2026-08-19 10:11:47 +00:00
let short = Message::tool_result("call-1", "call-1-name", "found via search [1]");
Split core.rs into one file per concern core.rs had grown into a 612-line grab-bag mixing six unrelated concerns: CLI arg parsing, logging setup, top-level orchestration, the researcher agent phase, chat-history reconstruction utilities, the summarizer agent phase, and the writer agent phase — while review.rs, tools.rs, stream.rs, and progress.rs already correctly isolated their own concerns. This splits core.rs to match that existing pattern instead of being the one file that doesn't follow it: - cli.rs — Cli struct + DEFAULT_TOPIC - observability.rs — initialize_observability - models.rs — RESEARCHER_MODEL / WRITER_MODEL (previously duplicated across call sites, now a single source of truth) - history.rs — pure chat-history parsing/reconstruction helpers (partial_findings_from_history, annotated_transcript_from_history, and their private helpers), plus their unit tests. Also dedupes MAX_TOOL_RESULT_CHARS, which was previously defined twice. - researcher.rs — gather_findings + GatheredFindings (the tool-calling research phase) - summarizer.rs — summarize_partial_history (the max-turns recovery agent) - writer.rs — write_report - research.rs — the top-level research() orchestration loop main.rs now only does argument parsing, logging setup, and the top-level call — no orchestration logic of its own. Unit tests stay co-located with the code they test per Rust convention (not pulled into separate files) rather than under "prefer new files" — that applies to production code organization here. No behavior changes; cargo test/clippy/fmt all clean.
2026-08-18 10:59:03 +00:00
assert_eq!(
transcript_lines(&short),
vec!["Result: found via search [1]".to_string()]
);
}
#[test]
fn transcript_lines_ignores_system_messages() {
assert!(transcript_lines(&Message::system("be thorough")).is_empty());
}
#[test]
fn annotated_transcript_from_history_joins_calls_results_and_notes_in_order() {
let history = vec![
Message::Assistant {
id: None,
2026-08-19 10:11:47 +00:00
content: vec![AssistantContent::tool_call(
Split core.rs into one file per concern core.rs had grown into a 612-line grab-bag mixing six unrelated concerns: CLI arg parsing, logging setup, top-level orchestration, the researcher agent phase, chat-history reconstruction utilities, the summarizer agent phase, and the writer agent phase — while review.rs, tools.rs, stream.rs, and progress.rs already correctly isolated their own concerns. This splits core.rs to match that existing pattern instead of being the one file that doesn't follow it: - cli.rs — Cli struct + DEFAULT_TOPIC - observability.rs — initialize_observability - models.rs — RESEARCHER_MODEL / WRITER_MODEL (previously duplicated across call sites, now a single source of truth) - history.rs — pure chat-history parsing/reconstruction helpers (partial_findings_from_history, annotated_transcript_from_history, and their private helpers), plus their unit tests. Also dedupes MAX_TOOL_RESULT_CHARS, which was previously defined twice. - researcher.rs — gather_findings + GatheredFindings (the tool-calling research phase) - summarizer.rs — summarize_partial_history (the max-turns recovery agent) - writer.rs — write_report - research.rs — the top-level research() orchestration loop main.rs now only does argument parsing, logging setup, and the top-level call — no orchestration logic of its own. Unit tests stay co-located with the code they test per Rust convention (not pulled into separate files) rather than under "prefer new files" — that applies to production code organization here. No behavior changes; cargo test/clippy/fmt all clean.
2026-08-18 10:59:03 +00:00
"call-1",
"search_web",
serde_json::json!({ "query": "test" }),
2026-08-19 10:11:47 +00:00
)],
Split core.rs into one file per concern core.rs had grown into a 612-line grab-bag mixing six unrelated concerns: CLI arg parsing, logging setup, top-level orchestration, the researcher agent phase, chat-history reconstruction utilities, the summarizer agent phase, and the writer agent phase — while review.rs, tools.rs, stream.rs, and progress.rs already correctly isolated their own concerns. This splits core.rs to match that existing pattern instead of being the one file that doesn't follow it: - cli.rs — Cli struct + DEFAULT_TOPIC - observability.rs — initialize_observability - models.rs — RESEARCHER_MODEL / WRITER_MODEL (previously duplicated across call sites, now a single source of truth) - history.rs — pure chat-history parsing/reconstruction helpers (partial_findings_from_history, annotated_transcript_from_history, and their private helpers), plus their unit tests. Also dedupes MAX_TOOL_RESULT_CHARS, which was previously defined twice. - researcher.rs — gather_findings + GatheredFindings (the tool-calling research phase) - summarizer.rs — summarize_partial_history (the max-turns recovery agent) - writer.rs — write_report - research.rs — the top-level research() orchestration loop main.rs now only does argument parsing, logging setup, and the top-level call — no orchestration logic of its own. Unit tests stay co-located with the code they test per Rust convention (not pulled into separate files) rather than under "prefer new files" — that applies to production code organization here. No behavior changes; cargo test/clippy/fmt all clean.
2026-08-18 10:59:03 +00:00
},
2026-08-19 10:11:47 +00:00
Message::tool_result("call-1", "call-1-name", "1. Example\n https://example.com\n snippet"),
Split core.rs into one file per concern core.rs had grown into a 612-line grab-bag mixing six unrelated concerns: CLI arg parsing, logging setup, top-level orchestration, the researcher agent phase, chat-history reconstruction utilities, the summarizer agent phase, and the writer agent phase — while review.rs, tools.rs, stream.rs, and progress.rs already correctly isolated their own concerns. This splits core.rs to match that existing pattern instead of being the one file that doesn't follow it: - cli.rs — Cli struct + DEFAULT_TOPIC - observability.rs — initialize_observability - models.rs — RESEARCHER_MODEL / WRITER_MODEL (previously duplicated across call sites, now a single source of truth) - history.rs — pure chat-history parsing/reconstruction helpers (partial_findings_from_history, annotated_transcript_from_history, and their private helpers), plus their unit tests. Also dedupes MAX_TOOL_RESULT_CHARS, which was previously defined twice. - researcher.rs — gather_findings + GatheredFindings (the tool-calling research phase) - summarizer.rs — summarize_partial_history (the max-turns recovery agent) - writer.rs — write_report - research.rs — the top-level research() orchestration loop main.rs now only does argument parsing, logging setup, and the top-level call — no orchestration logic of its own. Unit tests stay co-located with the code they test per Rust convention (not pulled into separate files) rather than under "prefer new files" — that applies to production code organization here. No behavior changes; cargo test/clippy/fmt all clean.
2026-08-18 10:59:03 +00:00
Message::assistant("that source looks solid"),
];
assert_eq!(
annotated_transcript_from_history(&history),
"Called search_web({\"query\":\"test\"})\n\
Result: 1. Example\n https://example.com\n snippet\n\
Note: that source looks solid"
);
}
#[test]
fn annotated_transcript_from_history_is_empty_with_no_usable_content() {
assert_eq!(
annotated_transcript_from_history(&[Message::system("preamble")]),
""
);
}
}