Skip to main content

acp_utils/conversation/
tool_calls.rs

1use crate::notifications::{SubAgentEvent, SubAgentProgressParams};
2use agent_client_protocol::schema::{MaybeUndefined, v2 as acp};
3use schemars::JsonSchema;
4use serde::Serialize;
5
6/// Per-sub-agent state: tracks its tool calls in arrival order.
7#[derive(Debug, Clone, PartialEq, Serialize, JsonSchema)]
8#[serde(rename_all = "camelCase")]
9pub struct SubAgentState {
10    pub task_id: String,
11    pub agent_name: String,
12    pub done: bool,
13    pub tool_calls: Vec<ToolCall>,
14}
15
16/// A tool call as the merge of every update the agent sent for it.
17#[derive(Debug, Clone, PartialEq, Serialize, JsonSchema)]
18#[serde(rename_all = "camelCase")]
19pub struct ToolCall {
20    pub status: ToolStatus,
21    #[serde(skip_serializing_if = "Option::is_none")]
22    pub error: Option<String>,
23    pub sub_agents: Vec<SubAgentState>,
24    #[serde(rename = "toolCall")]
25    protocol: Box<acp::ToolCallUpdate>,
26}
27
28#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, JsonSchema)]
29#[serde(rename_all = "snake_case")]
30pub enum ToolStatus {
31    Running,
32    Success,
33    Cancelled,
34    Failed,
35}
36
37impl ToolCall {
38    pub fn id(&self) -> &str {
39        &self.protocol.tool_call_id.0
40    }
41
42    pub fn title(&self) -> &str {
43        self.protocol.title.value().map_or("", String::as_str)
44    }
45
46    pub fn raw_input(&self) -> String {
47        self.protocol.raw_input.value().map_or_else(String::new, raw_input_text)
48    }
49
50    pub fn display_value(&self) -> Option<&str> {
51        self.meta_str("display_value")
52    }
53
54    pub fn content(&self) -> &[acp::ToolCallContent] {
55        self.protocol.content.value().map_or(&[], Vec::as_slice)
56    }
57
58    pub fn diffs(&self) -> impl Iterator<Item = &acp::Diff> {
59        self.content().iter().filter_map(|content| match content {
60            acp::ToolCallContent::Diff(diff) => Some(diff),
61            _ => None,
62        })
63    }
64
65    pub fn bash_command(&self) -> Option<&str> {
66        if self.kind() != ToolKind::Bash {
67            return None;
68        }
69        self.protocol.raw_input.value()?.get("command")?.as_str()
70    }
71
72    pub(super) fn from_update(update: &acp::ToolCallUpdate) -> Self {
73        let mut tool = Self {
74            status: ToolStatus::Running,
75            error: None,
76            sub_agents: Vec::new(),
77            protocol: Box::new(update.clone()),
78        };
79        tool.refresh_status();
80        tool
81    }
82
83    pub(super) fn apply_update(&mut self, update: &acp::ToolCallUpdate) {
84        self.protocol.apply_update(update.clone());
85        self.refresh_status();
86    }
87
88    pub(super) fn append_content(&mut self, content: acp::ToolCallContent) {
89        match &mut self.protocol.content {
90            MaybeUndefined::Value(items) => items.push(content),
91            value => *value = MaybeUndefined::Value(vec![content]),
92        }
93    }
94
95    pub(super) fn apply_sub_agent_progress(&mut self, notification: &SubAgentProgressParams) {
96        apply_sub_agent_progress(&mut self.sub_agents, notification);
97    }
98
99    pub(super) fn finalize(&mut self, status: ToolStatus, error: Option<&str>) {
100        if self.status == ToolStatus::Running {
101            self.status = status;
102            self.error = error.map(str::to_owned);
103        }
104        for agent in &mut self.sub_agents {
105            agent.done = true;
106            for call in &mut agent.tool_calls {
107                call.finalize(status, None);
108            }
109        }
110    }
111
112    pub(super) fn is_running(&self) -> bool {
113        self.status == ToolStatus::Running
114            || self.sub_agents.iter().any(|agent| !agent.done || agent.tool_calls.iter().any(ToolCall::is_running))
115    }
116
117    pub(super) fn rendering_final(&self) -> bool {
118        !self.is_running() && (self.kind() != ToolKind::SpawnSubagent || !self.sub_agents.is_empty())
119    }
120
121    fn kind(&self) -> ToolKind {
122        tool_kind(self.protocol.name.value().map_or_else(|| self.title(), String::as_str))
123    }
124
125    fn refresh_status(&mut self) {
126        self.status = match self.protocol.status.value() {
127            Some(acp::ToolCallStatus::Completed) => ToolStatus::Success,
128            Some(acp::ToolCallStatus::Failed) => ToolStatus::Failed,
129            Some(acp::ToolCallStatus::Cancelled) => ToolStatus::Cancelled,
130            _ => ToolStatus::Running,
131        };
132        self.error = None;
133    }
134
135    fn meta_str(&self, key: &str) -> Option<&str> {
136        self.protocol.meta.value().and_then(|meta| meta.get(key)).and_then(serde_json::Value::as_str)
137    }
138}
139
140fn apply_sub_agent_progress(states: &mut Vec<SubAgentState>, notification: &SubAgentProgressParams) {
141    let index = states.iter().position(|agent| agent.task_id == notification.task_id).unwrap_or_else(|| {
142        states.push(SubAgentState {
143            task_id: notification.task_id.clone(),
144            agent_name: notification.agent_name.clone(),
145            done: false,
146            tool_calls: Vec::new(),
147        });
148        states.len() - 1
149    });
150    let agent = &mut states[index];
151
152    match &notification.event {
153        SubAgentEvent::Started => {}
154        SubAgentEvent::ToolCallUpdate(update) => {
155            match agent.tool_calls.iter_mut().find(|call| call.protocol.tool_call_id == update.tool_call_id) {
156                Some(call) => call.apply_update(update),
157                None => agent.tool_calls.push(ToolCall::from_update(update)),
158            }
159        }
160        SubAgentEvent::Done => agent.done = true,
161    }
162}
163
164fn raw_input_text(raw_input: &serde_json::Value) -> String {
165    raw_input.as_str().map_or_else(|| raw_input.to_string(), str::to_string)
166}
167
168#[derive(Debug, Clone, Copy, PartialEq, Eq)]
169enum ToolKind {
170    Bash,
171    SpawnSubagent,
172    Other,
173}
174
175fn tool_kind(tool_name: &str) -> ToolKind {
176    let name = tool_name.rsplit("__").next().unwrap_or(tool_name);
177    if name.eq_ignore_ascii_case("bash") {
178        ToolKind::Bash
179    } else if name.eq_ignore_ascii_case("spawn_subagent") {
180        ToolKind::SpawnSubagent
181    } else {
182        ToolKind::Other
183    }
184}