1use std::borrow::Cow;
2use std::path::PathBuf;
3use std::time::{Duration, Instant};
4
5use serde_json::{json, Value};
6
7use crate::context::AppContext;
8use crate::protocol::{RawRequest, Response};
9
10pub type DispatchFn<'a> = dyn Fn(RawRequest, &AppContext) -> Response + 'a;
11pub type FinalizeFn<'a> = dyn Fn(&mut Response) + 'a;
12
13#[derive(Debug)]
16pub struct PhaseTrace {
17 frame_decoded: Instant,
18 executor_submitted: Option<Instant>,
19 job_admitted: Option<Instant>,
20 translate_done: Option<Instant>,
21 execute_done: Option<Instant>,
22 format_done: Option<Instant>,
23 finalize_done: Option<Instant>,
24}
25
26#[derive(Debug, Clone, Copy)]
27pub struct ToolCallEgressTiming {
28 pub enqueued: Instant,
29 pub dequeued: Instant,
30 pub write_started: Instant,
31 pub write_finished: Instant,
32 pub frame_bytes: usize,
33 pub queue_depth: usize,
34 pub writer_active_at_enqueue: bool,
35 pub writer_queue_was_full: bool,
36 pub reserve_timeouts: u32,
37}
38
39#[derive(Debug, Clone, Copy)]
40pub struct ToolCallPhaseDurations {
41 pub queue: Duration,
42 pub translate: Duration,
43 pub execute: Duration,
44 pub format: Duration,
45 pub finalize: Duration,
46 pub egress_enqueue: Duration,
47 pub egress_queue: Duration,
48 pub egress_prepare: Duration,
49 pub egress_write: Duration,
50 pub egress: Duration,
51 pub frame_bytes: usize,
52 pub writer_queue_depth: usize,
53 pub writer_active_at_enqueue: bool,
54 pub writer_queue_was_full: bool,
55 pub writer_reserve_timeouts: u32,
56 pub total: Duration,
57}
58
59impl PhaseTrace {
60 pub fn new(frame_decoded: Instant) -> Self {
61 Self {
62 frame_decoded,
63 executor_submitted: None,
64 job_admitted: None,
65 translate_done: None,
66 execute_done: None,
67 format_done: None,
68 finalize_done: None,
69 }
70 }
71
72 pub fn mark_executor_submitted(&mut self) {
73 self.executor_submitted = Some(Instant::now());
74 }
75
76 pub fn mark_job_admitted(&mut self) {
77 self.job_admitted = Some(Instant::now());
78 }
79
80 fn mark_translate_done(&mut self) {
81 self.translate_done = Some(Instant::now());
82 }
83
84 fn mark_execute_done(&mut self) {
85 self.execute_done = Some(Instant::now());
86 }
87
88 fn mark_format_done(&mut self) {
89 self.format_done = Some(Instant::now());
90 }
91
92 fn mark_finalize_done(&mut self) {
93 self.finalize_done = Some(Instant::now());
94 }
95
96 pub fn finish(self, egress: ToolCallEgressTiming) -> Option<ToolCallPhaseDurations> {
97 let executor_submitted = self.executor_submitted?;
98 let job_admitted = self.job_admitted?;
99 let translate_done = self.translate_done?;
100 let execute_done = self.execute_done?;
101 let format_done = self.format_done?;
102 let finalize_done = self.finalize_done?;
103 Some(ToolCallPhaseDurations {
104 queue: job_admitted.duration_since(executor_submitted),
105 translate: translate_done.duration_since(job_admitted),
106 execute: execute_done.duration_since(translate_done),
107 format: format_done.duration_since(execute_done),
108 finalize: finalize_done.duration_since(format_done),
109 egress_enqueue: egress.enqueued.duration_since(finalize_done),
110 egress_queue: egress.dequeued.duration_since(egress.enqueued),
111 egress_prepare: egress.write_started.duration_since(egress.dequeued),
112 egress_write: egress.write_finished.duration_since(egress.write_started),
113 egress: egress.write_finished.duration_since(finalize_done),
114 frame_bytes: egress.frame_bytes,
115 writer_queue_depth: egress.queue_depth,
116 writer_active_at_enqueue: egress.writer_active_at_enqueue,
117 writer_queue_was_full: egress.writer_queue_was_full,
118 writer_reserve_timeouts: egress.reserve_timeouts,
119 total: egress.write_finished.duration_since(self.frame_decoded),
120 })
121 }
122}
123
124#[derive(Debug)]
129pub struct ToolCallResult {
130 pub text: String,
131 pub response: crate::protocol::Response,
132}
133
134#[derive(Debug)]
137pub enum ToolCallOutcome {
138 Unary(ToolCallResult),
139}
140
141#[derive(Debug, Clone)]
144pub struct ToolCallContext {
145 pub project_root: PathBuf,
146 pub session_id: Option<String>,
147 pub request_id: String,
148 pub diagnostics_on_edit: bool,
149 pub preview: bool,
150}
151
152pub fn run_tool_call(
153 bare_name: &str,
154 args: &Value,
155 format_context: &crate::subc_format::FormatContext,
156 ctx: &ToolCallContext,
157 app_ctx: &AppContext,
158 dispatch: &DispatchFn<'_>,
159 finalizer: Option<&FinalizeFn<'_>>,
160 mut phase_trace: Option<&mut PhaseTrace>,
161) -> ToolCallOutcome {
162 let sanitized_args = strip_agent_preview_arg(args);
163 let translate_context = crate::subc_translate::TranslateContext {
164 diagnostics_on_edit: ctx.diagnostics_on_edit,
165 preview: ctx.preview,
166 };
167 let (command, translated_args) = match crate::subc_translate::subc_translate_with_context(
168 bare_name,
169 &sanitized_args,
170 ctx.project_root.as_path(),
171 translate_context,
172 ) {
173 Ok(translated) => (translated.command, translated.args),
174 Err(err) if err.code != "unsupported_tool" => {
178 if let Some(trace) = phase_trace.as_mut() {
179 trace.mark_translate_done();
180 trace.mark_execute_done();
181 }
182 let response = Response::error(ctx.request_id.clone(), err.code, err.message);
183 let result = tool_call_result_from_response(bare_name, format_context, response);
184 if let Some(trace) = phase_trace.as_mut() {
185 trace.mark_format_done();
186 trace.mark_finalize_done();
187 }
188 return ToolCallOutcome::Unary(result);
189 }
190 Err(_) => {
191 let map = sanitized_args.as_object().cloned().unwrap_or_default();
192 (bare_name.to_string(), map)
193 }
194 };
195
196 let mut map = translated_args;
197 if ctx.preview {
198 map.insert("preview".to_string(), json!(true));
199 }
200 map.insert("id".to_string(), json!(ctx.request_id.clone()));
201 map.insert("command".to_string(), json!(command));
202 map.insert("session_id".to_string(), json!(ctx.session_id.clone()));
203
204 let raw_req = match serde_json::from_value::<RawRequest>(Value::Object(map)) {
205 Ok(req) => req,
206 Err(error) => {
207 if let Some(trace) = phase_trace.as_mut() {
208 trace.mark_translate_done();
209 trace.mark_execute_done();
210 }
211 let response = Response::error(
212 ctx.request_id.clone(),
213 "invalid_request",
214 format!("failed to build request from tool call: {error}"),
215 );
216 let result = tool_call_result_from_response(bare_name, format_context, response);
217 if let Some(trace) = phase_trace.as_mut() {
218 trace.mark_format_done();
219 trace.mark_finalize_done();
220 }
221 return ToolCallOutcome::Unary(result);
222 }
223 };
224 if let Some(trace) = phase_trace.as_mut() {
225 trace.mark_translate_done();
226 }
227
228 let mut response = dispatch(raw_req, app_ctx);
229 if let Some(trace) = phase_trace.as_mut() {
230 trace.mark_execute_done();
231 }
232 let text =
233 crate::subc_format::format_response_with_context(bare_name, &response, format_context);
234 if let Some(trace) = phase_trace.as_mut() {
235 trace.mark_format_done();
236 }
237 if let Some(finalizer) = finalizer {
238 finalizer(&mut response);
239 }
240 if let Some(trace) = phase_trace.as_mut() {
241 trace.mark_finalize_done();
242 }
243
244 ToolCallOutcome::Unary(ToolCallResult { text, response })
245}
246
247pub(crate) fn strip_agent_preview_arg(args: &Value) -> Cow<'_, Value> {
248 let Some(map) = args.as_object() else {
249 return Cow::Borrowed(args);
250 };
251 if !map.contains_key("preview") {
252 return Cow::Borrowed(args);
253 }
254
255 let mut sanitized = map.clone();
256 sanitized.remove("preview");
257 Cow::Owned(Value::Object(sanitized))
258}
259
260pub(crate) fn strip_agent_preview_arg_owned(mut args: Value) -> Value {
261 if let Some(map) = args.as_object_mut() {
262 map.remove("preview");
263 }
264 args
265}
266
267fn tool_call_result_from_response(
268 bare_name: &str,
269 format_context: &crate::subc_format::FormatContext,
270 response: Response,
271) -> ToolCallResult {
272 let text =
273 crate::subc_format::format_response_with_context(bare_name, &response, format_context);
274 ToolCallResult { text, response }
275}
276
277#[cfg(test)]
278mod tests {
279 use super::*;
280
281 #[test]
282 fn phase_trace_reports_execution_and_writer_egress_subphases() {
283 let t0 = Instant::now();
284 let trace = PhaseTrace {
285 frame_decoded: t0,
286 executor_submitted: Some(t0 + Duration::from_millis(1)),
287 job_admitted: Some(t0 + Duration::from_millis(3)),
288 translate_done: Some(t0 + Duration::from_millis(6)),
289 execute_done: Some(t0 + Duration::from_millis(10)),
290 format_done: Some(t0 + Duration::from_millis(15)),
291 finalize_done: Some(t0 + Duration::from_millis(21)),
292 };
293
294 let phases = trace
295 .finish(ToolCallEgressTiming {
296 enqueued: t0 + Duration::from_millis(28),
297 dequeued: t0 + Duration::from_millis(35),
298 write_started: t0 + Duration::from_millis(37),
299 write_finished: t0 + Duration::from_millis(48),
300 frame_bytes: 262_144,
301 queue_depth: 17,
302 writer_active_at_enqueue: true,
303 writer_queue_was_full: true,
304 reserve_timeouts: 2,
305 })
306 .unwrap();
307
308 assert_eq!(phases.queue, Duration::from_millis(2));
309 assert_eq!(phases.translate, Duration::from_millis(3));
310 assert_eq!(phases.execute, Duration::from_millis(4));
311 assert_eq!(phases.format, Duration::from_millis(5));
312 assert_eq!(phases.finalize, Duration::from_millis(6));
313 assert_eq!(phases.egress_enqueue, Duration::from_millis(7));
314 assert_eq!(phases.egress_queue, Duration::from_millis(7));
315 assert_eq!(phases.egress_prepare, Duration::from_millis(2));
316 assert_eq!(phases.egress_write, Duration::from_millis(11));
317 assert_eq!(phases.egress, Duration::from_millis(27));
318 assert_eq!(phases.frame_bytes, 262_144);
319 assert_eq!(phases.writer_queue_depth, 17);
320 assert!(phases.writer_active_at_enqueue);
321 assert!(phases.writer_queue_was_full);
322 assert_eq!(phases.writer_reserve_timeouts, 2);
323 assert_eq!(phases.total, Duration::from_millis(48));
324 }
325}