use std::collections::HashSet;
use super::events::SubagentEvent;
#[derive(Debug, Default)]
pub(crate) struct TurnClosure {
current: String,
previous: String,
seen_calls: HashSet<String>,
}
impl TurnClosure {
pub(crate) fn observe(&mut self, event: &SubagentEvent) {
match event {
SubagentEvent::Message { text } => self.current.push_str(text),
SubagentEvent::ToolCall { id, .. }
if self.seen_calls.insert(id.clone()) && !self.current.is_empty() =>
{
self.previous = std::mem::take(&mut self.current);
}
_ => {}
}
}
pub(crate) fn output(&self) -> String {
if self.current.is_empty() {
self.previous.clone()
} else {
self.current.clone()
}
}
}
#[cfg(test)]
mod tests {
use super::*;
fn message(text: &str) -> SubagentEvent {
SubagentEvent::Message {
text: text.to_string(),
}
}
fn tool_call(id: &str) -> SubagentEvent {
SubagentEvent::ToolCall {
id: id.to_string(),
title: "call".to_string(),
kind: crate::subagent::events::ToolKind::Read,
status: crate::subagent::events::ToolCallStatus::InProgress,
raw_input: None,
}
}
#[test]
fn the_message_after_the_last_tool_call_is_the_output() {
let mut closure = TurnClosure::default();
closure.observe(&message("narration one "));
closure.observe(&tool_call("a"));
closure.observe(&message("narration two "));
closure.observe(&tool_call("b"));
closure.observe(&message("final report"));
assert_eq!(closure.output(), "final report");
}
#[test]
fn a_turn_ending_right_after_a_tool_call_falls_back() {
let mut closure = TurnClosure::default();
closure.observe(&message("the last real message"));
closure.observe(&tool_call("a"));
assert_eq!(closure.output(), "the last real message");
}
#[test]
fn back_to_back_tool_calls_keep_the_fallback() {
let mut closure = TurnClosure::default();
closure.observe(&message("before"));
closure.observe(&tool_call("a"));
closure.observe(&tool_call("b"));
assert_eq!(closure.output(), "before");
}
#[test]
fn a_reannounced_call_does_not_split_the_message() {
let mut closure = TurnClosure::default();
closure.observe(&tool_call("a"));
closure.observe(&message("part one "));
closure.observe(&tool_call("a"));
closure.observe(&message("part two"));
assert_eq!(closure.output(), "part one part two");
}
#[test]
fn a_reannounced_call_after_the_final_text_does_not_reset() {
let mut closure = TurnClosure::default();
closure.observe(&tool_call("a"));
closure.observe(&message("the report"));
closure.observe(&tool_call("a"));
assert_eq!(closure.output(), "the report");
}
#[test]
fn display_only_events_never_enter_the_output() {
let mut closure = TurnClosure::default();
closure.observe(&SubagentEvent::Thought {
text: "thinking".to_string(),
});
closure.observe(&SubagentEvent::Plan {
entries: Vec::new(),
});
closure.observe(&SubagentEvent::Usage {
context_window: 1,
tokens_in_context: 1,
});
assert_eq!(closure.output(), "");
}
#[test]
fn a_cancelled_turn_with_no_text_is_empty() {
let mut closure = TurnClosure::default();
closure.observe(&tool_call("a"));
assert_eq!(closure.output(), "");
}
}