1use std::collections::HashMap;
7use std::fs::OpenOptions;
8use std::io::{BufWriter, Write};
9use std::path::Path;
10use std::time::Instant;
11
12use agent_client_protocol::schema::SuccessorMessage;
13use agent_client_protocol::schema::v1::{
14 MessageMcpNotification, MessageMcpRequest, Notification as RpcNotification,
15 Request as RpcRequest, RequestId, Response as RpcResponse,
16};
17use agent_client_protocol::{
18 DynConnectTo, JsonRpcMessage, RawJsonRpcMessage, RawJsonRpcParams, Role, UntypedMessage,
19};
20use rustc_hash::FxHashMap;
21use serde::{Deserialize, Serialize};
22
23use crate::ComponentIndex;
24use crate::snoop::SnooperComponent;
25
26#[derive(Debug, Clone, Serialize, Deserialize)]
28#[serde(tag = "type", rename_all = "snake_case")]
29#[non_exhaustive]
30pub enum TraceEvent {
31 Request(RequestEvent),
33
34 Response(ResponseEvent),
36
37 Notification(NotificationEvent),
39}
40
41#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
43#[serde(rename_all = "snake_case")]
44#[non_exhaustive]
45pub enum Protocol {
46 Acp,
48 Mcp,
50}
51
52#[derive(Debug, Clone, Serialize, Deserialize)]
54#[non_exhaustive]
55pub struct RequestEvent {
56 pub ts: f64,
58
59 pub protocol: Protocol,
61
62 pub from: String,
64
65 pub to: String,
67
68 pub id: serde_json::Value,
70
71 pub method: String,
73
74 #[serde(skip_serializing_if = "Option::is_none")]
76 pub session: Option<String>,
77
78 pub params: serde_json::Value,
80}
81
82#[derive(Debug, Clone, Serialize, Deserialize)]
84#[non_exhaustive]
85pub struct ResponseEvent {
86 pub ts: f64,
88
89 pub from: String,
91
92 pub to: String,
94
95 pub id: serde_json::Value,
97
98 pub is_error: bool,
100
101 pub payload: serde_json::Value,
103}
104
105#[derive(Debug, Clone, Serialize, Deserialize)]
107#[non_exhaustive]
108pub struct NotificationEvent {
109 pub ts: f64,
111
112 pub protocol: Protocol,
114
115 pub from: String,
117
118 pub to: String,
120
121 pub method: String,
123
124 #[serde(skip_serializing_if = "Option::is_none")]
126 pub session: Option<String>,
127
128 pub params: serde_json::Value,
130}
131
132pub trait WriteEvent: Send + 'static {
134 fn write_event(&mut self, event: &TraceEvent) -> std::io::Result<()>;
136}
137
138pub(crate) struct EventWriter<W> {
140 writer: W,
141}
142
143impl<W: Write> EventWriter<W> {
144 pub fn new(writer: W) -> Self {
145 Self { writer }
146 }
147}
148
149impl<W: Write + Send + 'static> WriteEvent for EventWriter<W> {
150 fn write_event(&mut self, event: &TraceEvent) -> std::io::Result<()> {
151 serde_json::to_writer(&mut self.writer, event).map_err(std::io::Error::other)?;
152 self.writer.write_all(b"\n")?;
153 self.writer.flush()
154 }
155}
156
157impl WriteEvent for futures::channel::mpsc::UnboundedSender<TraceEvent> {
159 fn write_event(&mut self, event: &TraceEvent) -> std::io::Result<()> {
160 self.unbounded_send(event.clone())
161 .map_err(|e| std::io::Error::new(std::io::ErrorKind::BrokenPipe, e))
162 }
163}
164
165pub struct TraceWriter {
167 dest: Box<dyn WriteEvent>,
168 start_time: Instant,
169
170 request_details: FxHashMap<serde_json::Value, RequestDetails>,
173}
174
175impl std::fmt::Debug for TraceWriter {
176 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
177 f.debug_struct("TraceWriter")
178 .field("start_time", &self.start_time)
179 .finish_non_exhaustive()
180 }
181}
182
183struct RequestDetails {
184 #[expect(dead_code)]
185 protocol: Protocol,
186
187 #[expect(dead_code)]
188 method: String,
189
190 request_from: ComponentIndex,
191 request_to: ComponentIndex,
192}
193
194impl TraceWriter {
195 pub fn new<D: WriteEvent>(dest: D) -> Self {
197 Self {
198 dest: Box::new(dest),
199 start_time: Instant::now(),
200 request_details: HashMap::default(),
201 }
202 }
203
204 pub fn from_path(path: impl AsRef<Path>) -> std::io::Result<Self> {
206 let file = OpenOptions::new()
207 .create(true)
208 .write(true)
209 .truncate(true)
210 .open(path.as_ref())?;
211 Ok(Self::new(EventWriter::new(BufWriter::new(file))))
212 }
213
214 fn elapsed(&self) -> f64 {
216 self.start_time.elapsed().as_secs_f64()
217 }
218
219 fn write_event(&mut self, event: &TraceEvent) {
221 drop(self.dest.write_event(event));
223 }
224
225 #[expect(clippy::too_many_arguments)]
227 fn request(
228 &mut self,
229 protocol: Protocol,
230 from: ComponentIndex,
231 to: ComponentIndex,
232 id: serde_json::Value,
233 method: String,
234 session: Option<String>,
235 params: serde_json::Value,
236 ) {
237 self.request_details.insert(
238 id.clone(),
239 RequestDetails {
240 protocol,
241 method: method.clone(),
242 request_from: from,
243 request_to: to,
244 },
245 );
246 self.write_event(&TraceEvent::Request(RequestEvent {
247 ts: self.elapsed(),
248 protocol,
249 from: format!("{from:?}"),
250 to: format!("{to:?}"),
251 id,
252 method,
253 session,
254 params,
255 }));
256 }
257
258 fn response(
260 &mut self,
261 from: ComponentIndex,
262 to: ComponentIndex,
263 id: serde_json::Value,
264 is_error: bool,
265 payload: serde_json::Value,
266 ) {
267 self.write_event(&TraceEvent::Response(ResponseEvent {
268 ts: self.elapsed(),
269 from: format!("{from:?}"),
270 to: format!("{to:?}"),
271 id,
272 is_error,
273 payload,
274 }));
275 }
276
277 fn notification(
279 &mut self,
280 protocol: Protocol,
281 from: ComponentIndex,
282 to: ComponentIndex,
283 method: impl Into<String>,
284 session: Option<String>,
285 params: serde_json::Value,
286 ) {
287 self.write_event(&TraceEvent::Notification(NotificationEvent {
288 ts: self.elapsed(),
289 protocol,
290 from: format!("{from:?}"),
291 to: format!("{to:?}"),
292 method: method.into(),
293 session,
294 params,
295 }));
296 }
297
298 fn trace_message(&mut self, traced_message: TracedMessage) {
300 let TracedMessage {
301 component_index,
302 successor_index,
303 incoming,
304 message,
305 } = traced_message;
306
307 match message {
318 RawJsonRpcMessage::Request(req) => {
319 let MessageInfo {
320 successor,
321 id,
322 protocol,
323 method,
324 params,
325 } = MessageInfo::from_request(req);
326
327 self.trace_request_or_notification(
328 incoming,
329 component_index,
330 successor_index,
331 successor,
332 id,
333 protocol,
334 method,
335 params,
336 );
337 }
338 RawJsonRpcMessage::Notification(notification) => {
339 let MessageInfo {
340 successor,
341 id,
342 protocol,
343 method,
344 params,
345 } = MessageInfo::from_notification(notification);
346
347 self.trace_request_or_notification(
348 incoming,
349 component_index,
350 successor_index,
351 successor,
352 id,
353 protocol,
354 method,
355 params,
356 );
357 }
358 RawJsonRpcMessage::Response(resp) => {
359 let (id, is_error, payload) = match resp {
363 RpcResponse::Result { id, result } => (id, false, result),
364 RpcResponse::Error { id, error } => {
365 (id, true, serde_json::to_value(error).unwrap_or_default())
366 }
367 };
368 let id = id_to_json(&id);
369 if let Some(RequestDetails {
370 protocol: _,
371 method: _,
372 request_from,
373 request_to,
374 }) = self.request_details.remove(&id)
375 {
376 self.response(request_to, request_from, id, is_error, payload);
377 }
378 }
379 }
380 }
381
382 #[expect(clippy::too_many_arguments)]
383 fn trace_request_or_notification(
384 &mut self,
385 incoming: Incoming,
386 component_index: ComponentIndex,
387 successor_index: ComponentIndex,
388 successor: Successor,
389 id: Option<RequestId>,
390 protocol: Protocol,
391 method: String,
392 params: serde_json::Value,
393 ) {
394 let (from, to) = match (successor, incoming, component_index, successor_index) {
395 (Successor(false), Incoming(true), ComponentIndex::Proxy(proxy_index), _) => (
397 ComponentIndex::predecessor_of(proxy_index),
398 ComponentIndex::Proxy(proxy_index),
399 ),
400
401 (Successor(true), Incoming(true), component_index, successor_index) => {
405 (successor_index, component_index)
406 }
407
408 (Successor(true), Incoming(false), component_index, ComponentIndex::Agent) => {
414 (component_index, ComponentIndex::Agent)
415 }
416
417 _ => return,
418 };
419
420 match id {
421 Some(id) => {
422 self.request(protocol, from, to, id_to_json(&id), method, None, params);
423 }
424 None => {
425 self.notification(protocol, from, to, method, None, params);
426 }
427 }
428 }
429
430 pub(crate) fn spawn(
435 mut self: TraceWriter,
436 ) -> (
437 TraceHandle,
438 impl std::future::Future<Output = Result<(), agent_client_protocol::Error>>,
439 ) {
440 use futures::StreamExt;
441
442 let (tx, mut rx) = futures::channel::mpsc::unbounded();
443
444 let future = async move {
445 while let Some(event) = rx.next().await {
446 self.trace_message(event);
447 }
448 Ok(())
449 };
450
451 (TraceHandle { tx }, future)
452 }
453}
454
455#[derive(Clone, Debug)]
459pub(crate) struct TraceHandle {
460 tx: futures::channel::mpsc::UnboundedSender<TracedMessage>,
461}
462
463impl TraceHandle {
464 fn trace_message(
466 &self,
467 component_index: ComponentIndex,
468 successor_index: ComponentIndex,
469 incoming: Incoming,
470 message: &RawJsonRpcMessage,
471 ) -> Result<(), agent_client_protocol::Error> {
472 self.tx
473 .unbounded_send(TracedMessage {
474 component_index,
475 successor_index,
476 incoming,
477 message: message.clone(),
478 })
479 .map_err(agent_client_protocol::util::internal_error)
480 }
481
482 pub fn bridge_component<R: Role>(
497 &self,
498 proxy_index: ComponentIndex,
499 successor_index: ComponentIndex,
500 proxy: impl agent_client_protocol::ConnectTo<R>,
501 ) -> DynConnectTo<R> {
502 DynConnectTo::new(SnooperComponent::new(
503 proxy,
504 {
505 let trace_handle = self.clone();
506 move |msg| {
507 trace_handle.trace_message(proxy_index, successor_index, Incoming(true), msg)
508 }
509 },
510 {
511 let trace_handle = self.clone();
512 move |msg| {
513 trace_handle.trace_message(proxy_index, successor_index, Incoming(false), msg)
514 }
515 },
516 ))
517 }
518}
519
520fn id_to_json(id: &RequestId) -> serde_json::Value {
522 serde_json::to_value(id).expect("RequestId serializes infallibly")
523}
524
525fn params_from_transport(params: Option<RawJsonRpcParams>) -> serde_json::Value {
526 params.map_or(serde_json::Value::Null, RawJsonRpcParams::into_value)
527}
528
529#[derive(Debug)]
532struct TracedMessage {
533 component_index: ComponentIndex,
534 successor_index: ComponentIndex,
535 incoming: Incoming,
536 message: RawJsonRpcMessage,
537}
538
539#[derive(Debug)]
541struct MessageInfo {
542 successor: Successor,
543 id: Option<RequestId>,
544 protocol: Protocol,
545 method: String,
546 params: serde_json::Value,
547}
548
549#[derive(Copy, Clone, Debug)]
550struct Successor(bool);
551
552#[derive(Copy, Clone, Debug)]
553struct Incoming(bool);
554
555impl MessageInfo {
556 fn from_request(req: RpcRequest<RawJsonRpcParams>) -> Self {
564 let untyped =
565 UntypedMessage::parse_message(&req.method, ¶ms_from_transport(req.params))
566 .expect("untyped message is infallible");
567 Self::from_untyped_request(Successor(false), Some(req.id), Protocol::Acp, untyped)
568 }
569
570 fn from_notification(notification: RpcNotification<RawJsonRpcParams>) -> Self {
571 let untyped = UntypedMessage::parse_message(
572 ¬ification.method,
573 ¶ms_from_transport(notification.params),
574 )
575 .expect("untyped message is infallible");
576 Self::from_untyped_notification(Successor(false), Protocol::Acp, untyped)
577 }
578
579 fn from_untyped_request(
580 successor: Successor,
581 id: Option<RequestId>,
582 protocol: Protocol,
583 untyped: UntypedMessage,
584 ) -> Self {
585 if let Ok(m) = SuccessorMessage::parse_message(&untyped.method, &untyped.params) {
586 return Self::from_untyped_request(Successor(true), id, protocol, m.message);
587 }
588
589 if let Ok(m) = MessageMcpRequest::parse_message(&untyped.method, &untyped.params) {
590 let params = m
591 .params
592 .map_or(serde_json::Value::Null, serde_json::Value::Object);
593 return Self::from_untyped_request(
594 successor,
595 id,
596 Protocol::Mcp,
597 UntypedMessage {
598 method: m.method,
599 params,
600 },
601 );
602 }
603
604 Self::new(successor, id, protocol, untyped)
605 }
606
607 fn from_untyped_notification(
608 successor: Successor,
609 protocol: Protocol,
610 untyped: UntypedMessage,
611 ) -> Self {
612 if let Ok(m) = SuccessorMessage::parse_message(&untyped.method, &untyped.params) {
613 return Self::from_untyped_notification(Successor(true), protocol, m.message);
614 }
615
616 if let Ok(m) = MessageMcpNotification::parse_message(&untyped.method, &untyped.params) {
617 let params = m
618 .params
619 .map_or(serde_json::Value::Null, serde_json::Value::Object);
620 return Self::from_untyped_notification(
621 successor,
622 Protocol::Mcp,
623 UntypedMessage {
624 method: m.method,
625 params,
626 },
627 );
628 }
629
630 Self::new(successor, None, protocol, untyped)
631 }
632
633 fn new(
634 successor: Successor,
635 id: Option<RequestId>,
636 protocol: Protocol,
637 untyped: UntypedMessage,
638 ) -> Self {
639 Self {
640 successor,
641 id,
642 protocol,
643 method: untyped.method,
644 params: untyped.params,
645 }
646 }
647}
648
649#[cfg(test)]
650mod tests {
651 use agent_client_protocol::RawJsonRpcMessage;
652 use serde_json::json;
653
654 use super::{MessageInfo, Protocol};
655
656 #[test]
657 fn tolerant_mcp_notification_params_are_traced_as_mcp() {
658 let RawJsonRpcMessage::Notification(notification) = RawJsonRpcMessage::notification(
659 "mcp/message".into(),
660 json!({
661 "connectionId": "connection-1",
662 "method": "notifications/progress",
663 "params": ["invalid named params"]
664 }),
665 )
666 .expect("notification is valid JSON-RPC") else {
667 unreachable!("notification constructor returned a different message kind")
668 };
669
670 let info = MessageInfo::from_notification(notification);
671
672 assert_eq!(info.protocol, Protocol::Mcp);
673 assert_eq!(info.method, "notifications/progress");
674 assert_eq!(info.params, serde_json::Value::Null);
675 }
676}