1use crate::ChatHistory;
4use crate::history::HistoryEntryType;
5use crate::ui::{self, AgentId, SubagentId};
6use anyhow::Result;
7use llimorse::line_format::ToolCall;
8use llimorse::{Agent, StreamingChunk};
9use llimorse_tools::{GateableToolParams, ToolGate};
10use std::result;
11use std::sync::atomic::{AtomicUsize, Ordering};
12use std::sync::{Arc, Mutex};
13use tokio::sync::{mpsc, oneshot};
14
15#[derive(Debug, Clone)]
23pub struct UserToolGate {
24 notifications: mpsc::UnboundedSender<ui::Notification>,
26}
27
28impl UserToolGate {
29 pub fn new(notifications: &ui::NotificationChannel) -> Self {
31 Self {
32 notifications: notifications.sender(),
33 }
34 }
35}
36
37impl ToolGate for UserToolGate {
38 async fn permitted(&self, params: &dyn GateableToolParams) -> result::Result<(), String> {
40 let (approval, wait) = oneshot::channel();
41 self.notifications
42 .send(ui::Notification::RequestPermission {
43 prompt: params.to_string(),
44 approval,
45 })
46 .map_err(|_| "UI notifications channel is closed".to_string())?;
47
48 wait.await
49 .map_err(|_| "permission request cancelled (UI closed)".to_string())?
50 }
51}
52
53#[derive(Debug)]
58pub struct SubagentNotifier {
59 notifications: mpsc::UnboundedSender<ui::Notification>,
61
62 subagent_id_counter: AtomicUsize,
64}
65
66#[derive(Debug)]
68pub struct SubagentConnector {
69 id: SubagentId,
71
72 chat_history: Arc<Mutex<ChatHistory>>,
74
75 notifications: mpsc::UnboundedSender<ui::Notification>,
77}
78
79impl SubagentNotifier {
80 pub fn new(notifications: &ui::NotificationChannel) -> Self {
85 SubagentNotifier {
86 notifications: notifications.sender(),
87 subagent_id_counter: 0.into(),
88 }
89 }
90}
91
92impl llimorse_tools::subagent::SubagentNotifier for SubagentNotifier {
93 type Error = anyhow::Error;
94 type Subagent = SubagentConnector;
95
96 async fn created(&self, agent: &Agent, prompt: &str) -> Result<SubagentConnector> {
97 let subagent = SubagentConnector::new(
98 SubagentId::new(self.subagent_id_counter.fetch_add(1, Ordering::Relaxed)),
99 self.notifications.clone(),
100 );
101
102 subagent
103 .chat_history
104 .lock()
105 .unwrap()
106 .push_lines(prompt, HistoryEntryType::User, true);
107
108 let _ = self.notifications.send(ui::Notification::SubagentCreated {
109 subagent_id: subagent.id,
110 prompt: prompt.to_string(),
111 client_info: agent.client_info().clone(),
112 client_state: agent.client_state_arc(),
113 chat_history: Arc::clone(&subagent.chat_history),
114 });
115
116 Ok(subagent)
117 }
118}
119
120impl SubagentConnector {
121 fn new(id: SubagentId, notifications: mpsc::UnboundedSender<ui::Notification>) -> Self {
126 SubagentConnector {
127 id,
128 chat_history: Default::default(),
129 notifications,
130 }
131 }
132
133 fn notify_update(&self) {
135 let _ = self.notifications.send(ui::Notification::UpdateAgent {
136 agent_id: AgentId::Subagent(self.id),
137 });
138 }
139}
140
141impl Drop for SubagentConnector {
142 fn drop(&mut self) {
143 let _ = self.notifications.send(ui::Notification::SubagentDropped {
144 subagent_id: self.id,
145 });
146 }
147}
148
149impl llimorse_tools::subagent::SubagentConnector for SubagentConnector {
150 type Error = anyhow::Error;
151
152 async fn push_chunk(&self, chunk: StreamingChunk) {
153 let (string, kind) = match chunk {
154 StreamingChunk::Content(c) => (c, HistoryEntryType::Content),
155 StreamingChunk::Reasoning(r) => (r, HistoryEntryType::Reasoning),
156 };
157 self.chat_history
158 .lock()
159 .unwrap()
160 .push_lines(&string, kind, false);
161 self.notify_update();
162 }
163
164 fn tool_call(&self, agent: &Agent, call: &ToolCall) -> Result<()> {
165 self.chat_history.lock().unwrap().push_lines(
166 &format!("[{}] {}\n", call.id, agent.display_call(&call.call)),
167 HistoryEntryType::ToolCall,
168 true,
169 );
170 self.notify_update();
171 Ok(())
172 }
173
174 fn tool_result(&self, agent: &Agent, call: &ToolCall, result: &Result<String>) -> Result<()> {
175 let name = call.call.name();
176 let id = &call.id;
177 match result {
178 Ok(result) => self.chat_history.lock().unwrap().push_lines(
179 &format!(
180 "=[{name}/{id}]=> {}\n",
181 agent.display_call_result(&call.call, result)
182 ),
183 HistoryEntryType::ToolResultOk,
184 true,
185 ),
186 Err(err) => self.chat_history.lock().unwrap().push_lines(
187 &format!("=[{name}/{id}]=> {err}\n"),
188 HistoryEntryType::ToolResultErr,
189 true,
190 ),
191 }
192
193 self.notify_update();
194 Ok(())
195 }
196}
197
198#[cfg(test)]
199mod tests {
200 use super::*;
201 use crate::ui;
202
203 #[derive(Debug)]
205 struct TestParams;
206
207 impl std::fmt::Display for TestParams {
208 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
209 write!(f, "echo hello")
210 }
211 }
212
213 #[tokio::test]
214 async fn approval_round_trip() {
215 let notifications = ui::NotificationChannel::new();
216 let gate = UserToolGate::new(¬ifications);
217 let mut receiver = notifications.into_receiver();
218 let params = TestParams;
219
220 let (prompt, decision) = futures::join!(
221 async {
222 let ui::Notification::RequestPermission { prompt, approval } =
223 receiver.recv().await.expect("channel closed")
224 else {
225 panic!("not a permission request");
226 };
227 approval.send(Ok(())).unwrap();
228 prompt
229 },
230 async { gate.permitted(¶ms).await },
231 );
232
233 assert_eq!(prompt, "echo hello");
234 assert!(decision.is_ok());
235 }
236
237 #[tokio::test]
238 async fn dropped_ui_cancels_the_request() {
239 let notifications = ui::NotificationChannel::new();
240 let gate = UserToolGate::new(¬ifications);
241 let params = TestParams;
242
243 drop(notifications.into_receiver());
245
246 let decision = gate.permitted(¶ms).await;
247 assert!(decision.is_err());
248 }
249}