fix(daemon): scope active tools to turn
This commit is contained in:
@@ -1,6 +1,6 @@
|
||||
//! Fold persisted ACP notifications into client-neutral transcript and state.
|
||||
|
||||
use std::collections::HashMap;
|
||||
use std::collections::{HashMap, HashSet};
|
||||
|
||||
use maestro_acp::schema::{ContentChunk, SessionNotification, SessionUpdate, ToolCall};
|
||||
use maestro_protocol::{
|
||||
@@ -36,6 +36,7 @@ pub struct Projection {
|
||||
pending_tool_updates: HashMap<String, Vec<maestro_acp::schema::ToolCallUpdateFields>>,
|
||||
state: ReducedState,
|
||||
turn_active: bool,
|
||||
turn_tool_calls: HashSet<String>,
|
||||
}
|
||||
|
||||
impl Projection {
|
||||
@@ -110,11 +111,15 @@ impl Projection {
|
||||
self.turn_active = true;
|
||||
self.state.turn.responding = true;
|
||||
self.state.turn.thinking = true;
|
||||
self.turn_tool_calls.clear();
|
||||
self.state.turn.active_tool_calls.clear();
|
||||
}
|
||||
TurnLifecycle::Finish | TurnLifecycle::Cancel | TurnLifecycle::Failure => {
|
||||
self.turn_active = false;
|
||||
self.state.turn.responding = false;
|
||||
self.state.turn.thinking = false;
|
||||
self.turn_tool_calls.clear();
|
||||
self.state.turn.active_tool_calls.clear();
|
||||
}
|
||||
}
|
||||
ProjectionUpdate {
|
||||
@@ -159,7 +164,7 @@ impl Projection {
|
||||
TranscriptRowKind::ToolCall
|
||||
};
|
||||
self.tools.insert(tool_id, canonical.clone());
|
||||
self.sync_active_tools();
|
||||
self.track_tool_call(&canonical);
|
||||
Some(self.replace(
|
||||
id,
|
||||
if tool_has_diff(&canonical) {
|
||||
@@ -203,7 +208,7 @@ impl Projection {
|
||||
row.data = to_value(tool);
|
||||
row.clone()
|
||||
};
|
||||
self.sync_active_tools();
|
||||
self.sync_active_tool(&tool_id);
|
||||
Some(TranscriptDelta::Patch { row })
|
||||
}
|
||||
|
||||
@@ -238,20 +243,33 @@ impl Projection {
|
||||
}
|
||||
}
|
||||
|
||||
fn sync_active_tools(&mut self) {
|
||||
self.state.turn.active_tool_calls = self
|
||||
.tools
|
||||
.iter()
|
||||
.filter(|(_, tool)| {
|
||||
matches!(
|
||||
tool.status,
|
||||
maestro_acp::schema::ToolCallStatus::Pending
|
||||
| maestro_acp::schema::ToolCallStatus::InProgress
|
||||
)
|
||||
})
|
||||
.map(|(id, _)| id.clone())
|
||||
.collect();
|
||||
self.state.turn.active_tool_calls.sort();
|
||||
fn track_tool_call(&mut self, tool: &ToolCall) {
|
||||
if self.turn_active {
|
||||
self.turn_tool_calls.insert(tool.tool_call_id.to_string());
|
||||
self.sync_active_tool(&tool.tool_call_id.to_string());
|
||||
}
|
||||
}
|
||||
|
||||
fn sync_active_tool(&mut self, tool_id: &str) {
|
||||
if !self.turn_active || !self.turn_tool_calls.contains(tool_id) {
|
||||
return;
|
||||
}
|
||||
let active = self.tools.get(tool_id).is_some_and(|tool| {
|
||||
matches!(
|
||||
tool.status,
|
||||
maestro_acp::schema::ToolCallStatus::Pending
|
||||
| maestro_acp::schema::ToolCallStatus::InProgress
|
||||
)
|
||||
});
|
||||
let active_tool_calls = &mut self.state.turn.active_tool_calls;
|
||||
if active {
|
||||
if !active_tool_calls.iter().any(|id| id == tool_id) {
|
||||
active_tool_calls.push(tool_id.to_owned());
|
||||
active_tool_calls.sort();
|
||||
}
|
||||
} else {
|
||||
active_tool_calls.retain(|id| id != tool_id);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -546,4 +564,45 @@ mod tests {
|
||||
assert!(!projection.state().turn.thinking);
|
||||
}
|
||||
}
|
||||
|
||||
fn terminal_turn_drops_unfinished_tools(
|
||||
terminal: TurnLifecycle,
|
||||
unfinished_id: &str,
|
||||
next_id: &str,
|
||||
) {
|
||||
let mut projection = Projection::default();
|
||||
projection.turn_lifecycle(TurnLifecycle::Begin);
|
||||
projection.apply(
|
||||
1,
|
||||
&event(serde_json::json!({
|
||||
"sessionUpdate":"tool_call", "toolCallId":unfinished_id,
|
||||
"title":"Unfinished", "status":"in_progress"
|
||||
})),
|
||||
);
|
||||
assert_eq!(projection.state().turn.active_tool_calls, [unfinished_id]);
|
||||
|
||||
projection.turn_lifecycle(terminal);
|
||||
assert!(projection.state().turn.active_tool_calls.is_empty());
|
||||
assert_eq!(projection.snapshot(1).rows.len(), 1);
|
||||
|
||||
projection.turn_lifecycle(TurnLifecycle::Begin);
|
||||
projection.apply(
|
||||
2,
|
||||
&event(serde_json::json!({
|
||||
"sessionUpdate":"tool_call", "toolCallId":next_id,
|
||||
"title":"Next", "status":"pending"
|
||||
})),
|
||||
);
|
||||
assert_eq!(projection.state().turn.active_tool_calls, [next_id]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn cancelled_turn_does_not_reactivate_unfinished_tools() {
|
||||
terminal_turn_drops_unfinished_tools(TurnLifecycle::Cancel, "cancelled-1", "next-1");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn failed_turn_does_not_reactivate_unfinished_tools() {
|
||||
terminal_turn_drops_unfinished_tools(TurnLifecycle::Failure, "failed-1", "next-1");
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user