1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
//! Opt-in raw model-turn capture.
//!
//! [`DebugCapture`] receives owned request and response payloads for a host
//! that wants them on disk or in a debugger. It is a separate seam from
//! [`crate::observe::Observer`]: observations stay payload-free, and production
//! hosts leave [`crate::execute::RunConfig::debug`] unset so they pay
//! nothing for this path.
use Value;
/// An opt-in sink for raw model-turn payloads.
///
/// Implementations own any synchronization they need. The runtime never consults
/// a capture for a decision; dropping every event cannot change the result.
///
/// # Sensitivity
/// A [`DebugEvent`] carries the verbatim request and response bodies, including
/// the full prompt, model output, tool arguments and results, and any store
/// contents that reached the turn. It is raw, unredacted capture: a host that
/// persists it owns treating it as sensitive. The bearer credential is never
/// part of a body (it rides an HTTP header the client never captures).
///
/// # Ordering and delivery
/// Both events for a turn are delivered only after the model round trip
/// succeeds. [`on_event`](Self::on_event) is then called synchronously from the
/// task driving the run, in turn order, with the [`DebugEvent::Request`]
/// delivered before its matching [`DebugEvent::Response`]. A turn whose round
/// trip does not complete - a transport error, cancellation, or a response the
/// client rejects - emits neither event, so a capture records only completed
/// turns and never a lone request.
///
/// Implementations must return promptly (copy into a queue rather than blocking
/// on I/O) and must not panic; a panic unwinds the run.
///
/// # Examples
/// A nonblocking capture copies each event into an in-memory queue and handles
/// events forward-compatibly. [`DebugEvent`] and its variants are
/// `#[non_exhaustive]`, so a wildcard arm is required:
///
/// ```
/// use std::sync::Mutex;
/// use promptforge_core::debug::{DebugCapture, DebugEvent};
///
/// #[derive(Default)]
/// struct QueueCapture {
/// turns: Mutex<Vec<(String, u32)>>,
/// }
///
/// impl DebugCapture for QueueCapture {
/// fn on_event(&self, _execution: &str, section: &str, turn_index: u32, event: DebugEvent) {
/// // Copy into an in-memory queue; never block on I/O on this path.
/// let kind = match event {
/// DebugEvent::Request { .. } => "request",
/// DebugEvent::Response { .. } => "response",
/// _ => "other",
/// };
/// // Handle poisoning explicitly: this callback must never panic
/// // (a panic unwinds the run), so a poisoned lock is skipped.
/// if let Ok(mut turns) = self.turns.lock() {
/// turns.push((format!("{section}:{kind}"), turn_index));
/// }
/// }
/// }
///
/// let capture = QueueCapture::default();
/// capture.on_event("run", "Say hi", 1, DebugEvent::request(serde_json::Value::Null));
/// capture.on_event("run", "Say hi", 1, DebugEvent::response(serde_json::Value::Null, None, None));
/// let turns = capture.turns.lock().map_err(|_| "capture mutex poisoned")?;
/// assert_eq!(turns.as_slice(), &[("Say hi:request".to_owned(), 1), ("Say hi:response".to_owned(), 1)]);
/// # Ok::<(), Box<dyn std::error::Error>>(())
/// ```
/// One owned capture payload for a model turn.
///
/// The `serde_json::Value` bodies are the intentional raw-capture wire contract:
/// a debug sink wants exactly what crossed the wire, not a re-typed view.