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