1use crate::agent::AgentEvent;
9use serde::{de::Error as _, Deserialize, Deserializer, Serialize};
10use serde_json::Value;
11use thiserror::Error;
12
13pub const EVENT_ENVELOPE_V1_VERSION: u16 = 1;
15
16macro_rules! define_agent_event_types_v1 {
17 ($( $variant:ident => $constant:ident = $wire_name:literal ),+ $(,)?) => {
18 #[derive(Debug, Clone, Copy)]
22 pub struct AgentEventTypeV1;
23
24 impl AgentEventTypeV1 {
25 $(
26 pub const $constant: &'static str = $wire_name;
27 )+
28 }
29
30 pub const AGENT_EVENT_TYPES_V1: &[&str] = &[
32 $(AgentEventTypeV1::$constant),+
33 ];
34
35 impl AgentEvent {
36 pub const fn event_type_v1(&self) -> &'static str {
42 match self {
43 $(Self::$variant { .. } => AgentEventTypeV1::$constant),+
44 }
45 }
46 }
47 };
48}
49
50define_agent_event_types_v1! {
51 Start => AGENT_START = "agent_start",
52 AgentModeChanged => AGENT_MODE_CHANGED = "agent_mode_changed",
53 TurnStart => TURN_START = "turn_start",
54 TextDelta => TEXT_DELTA = "text_delta",
55 ReasoningDelta => REASONING_DELTA = "reasoning_delta",
56 ToolStart => TOOL_START = "tool_start",
57 ToolInputDelta => TOOL_INPUT_DELTA = "tool_input_delta",
58 ToolExecutionStart => TOOL_EXECUTION_START = "tool_execution_start",
59 ToolEnd => TOOL_END = "tool_end",
60 ToolOutputDelta => TOOL_OUTPUT_DELTA = "tool_output_delta",
61 TurnEnd => TURN_END = "turn_end",
62 End => AGENT_END = "agent_end",
63 Error => ERROR = "error",
64 ConfirmationRequired => CONFIRMATION_REQUIRED = "confirmation_required",
65 ConfirmationReceived => CONFIRMATION_RECEIVED = "confirmation_received",
66 ConfirmationTimeout => CONFIRMATION_TIMEOUT = "confirmation_timeout",
67 ExternalTaskPending => EXTERNAL_TASK_PENDING = "external_task_pending",
68 ExternalTaskCompleted => EXTERNAL_TASK_COMPLETED = "external_task_completed",
69 PermissionDenied => PERMISSION_DENIED = "permission_denied",
70 ContextResolving => CONTEXT_RESOLVING = "context_resolving",
71 ContextResolved => CONTEXT_RESOLVED = "context_resolved",
72 CommandDeadLettered => COMMAND_DEAD_LETTERED = "command_dead_lettered",
73 CommandRetry => COMMAND_RETRY = "command_retry",
74 QueueAlert => QUEUE_ALERT = "queue_alert",
75 TaskUpdated => TASK_UPDATED = "task_updated",
76 MemoryStored => MEMORY_STORED = "memory_stored",
77 MemoryRecalled => MEMORY_RECALLED = "memory_recalled",
78 MemoriesSearched => MEMORIES_SEARCHED = "memories_searched",
79 MemoryCleared => MEMORY_CLEARED = "memory_cleared",
80 SubagentStart => SUBAGENT_START = "subagent_start",
81 SubagentProgress => SUBAGENT_PROGRESS = "subagent_progress",
82 SubagentEnd => SUBAGENT_END = "subagent_end",
83 PlanningStart => PLANNING_START = "planning_start",
84 PlanningEnd => PLANNING_END = "planning_end",
85 StepStart => STEP_START = "step_start",
86 StepEnd => STEP_END = "step_end",
87 GoalExtracted => GOAL_EXTRACTED = "goal_extracted",
88 GoalProgress => GOAL_PROGRESS = "goal_progress",
89 GoalAchieved => GOAL_ACHIEVED = "goal_achieved",
90 ContextCompacted => CONTEXT_COMPACTED = "context_compacted",
91 PersistenceFailed => PERSISTENCE_FAILED = "persistence_failed",
92 BudgetThresholdHit => BUDGET_THRESHOLD_HIT = "budget_threshold_hit",
93 PassivationRequested => PASSIVATION_REQUESTED = "passivation_requested",
94 PeerInvocation => PEER_INVOCATION = "peer_invocation",
95}
96
97#[derive(Debug, Error)]
99pub enum EventProtocolError {
100 #[error("failed to serialize agent event: {0}")]
101 Serialization(#[from] serde_json::Error),
102
103 #[error("serialized AgentEvent must be a JSON object with a string `type` field")]
104 InvalidRuntimeShape,
105
106 #[error(
107 "AgentEvent wire type drifted: canonical type is `{canonical}`, serde emitted `{serialized}`"
108 )]
109 TypeMismatch {
110 canonical: &'static str,
111 serialized: String,
112 },
113}
114
115#[derive(Debug, Clone, PartialEq, Serialize)]
121pub struct EventEnvelopeV1 {
122 pub version: u16,
123 #[serde(rename = "type")]
124 pub event_type: String,
125 pub payload: Value,
126 #[serde(default, skip_serializing_if = "Option::is_none")]
127 pub metadata: Option<Value>,
128}
129
130impl<'de> Deserialize<'de> for EventEnvelopeV1 {
131 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
132 where
133 D: Deserializer<'de>,
134 {
135 #[derive(Deserialize)]
136 struct WireEnvelope {
137 version: u16,
138 #[serde(rename = "type")]
139 event_type: String,
140 payload: Value,
141 #[serde(default)]
142 metadata: Option<Value>,
143 }
144
145 let wire = WireEnvelope::deserialize(deserializer)?;
146 if wire.version != EVENT_ENVELOPE_V1_VERSION {
147 return Err(D::Error::custom(format_args!(
148 "unsupported event envelope version {}; expected {}",
149 wire.version, EVENT_ENVELOPE_V1_VERSION
150 )));
151 }
152
153 Ok(Self {
154 version: wire.version,
155 event_type: wire.event_type,
156 payload: wire.payload,
157 metadata: wire.metadata,
158 })
159 }
160}
161
162impl EventEnvelopeV1 {
163 pub fn new(event_type: impl Into<String>, payload: Value) -> Self {
165 Self {
166 version: EVENT_ENVELOPE_V1_VERSION,
167 event_type: event_type.into(),
168 payload,
169 metadata: None,
170 }
171 }
172
173 pub fn with_metadata(mut self, metadata: Value) -> Self {
175 self.metadata = Some(metadata);
176 self
177 }
178}
179
180impl TryFrom<&AgentEvent> for EventEnvelopeV1 {
181 type Error = EventProtocolError;
182
183 fn try_from(event: &AgentEvent) -> Result<Self, Self::Error> {
184 let canonical = event.event_type_v1();
185 let Value::Object(mut serialized) = serde_json::to_value(event)? else {
186 return Err(EventProtocolError::InvalidRuntimeShape);
187 };
188 let Some(Value::String(serialized_type)) = serialized.remove("type") else {
189 return Err(EventProtocolError::InvalidRuntimeShape);
190 };
191 if serialized_type != canonical {
192 return Err(EventProtocolError::TypeMismatch {
193 canonical,
194 serialized: serialized_type,
195 });
196 }
197
198 Ok(Self::new(canonical, Value::Object(serialized)))
199 }
200}
201
202impl TryFrom<AgentEvent> for EventEnvelopeV1 {
203 type Error = EventProtocolError;
204
205 fn try_from(event: AgentEvent) -> Result<Self, Self::Error> {
206 Self::try_from(&event)
207 }
208}
209
210pub fn run_event_envelope_v1(
213 record: &crate::run::RunEventRecord,
214 run_id: &str,
215 session_id: &str,
216) -> Result<EventEnvelopeV1, EventProtocolError> {
217 Ok(
218 EventEnvelopeV1::try_from(&record.event)?.with_metadata(serde_json::json!({
219 "run_id": run_id,
220 "session_id": session_id,
221 "sequence": record.sequence,
222 "timestamp_ms": record.timestamp_ms,
223 })),
224 )
225}
226
227#[derive(Debug, Clone, PartialEq)]
233pub struct AgentEventProjectionV1 {
234 pub version: u16,
235 pub event_type: String,
236 pub payload: Value,
237 pub metadata: Option<Value>,
238 pub payload_json: String,
239 pub metadata_json: Option<String>,
240 pub data_json: Option<String>,
244 pub text: Option<String>,
245 pub tool_name: Option<String>,
246 pub tool_id: Option<String>,
247 pub tool_output: Option<String>,
248 pub exit_code: Option<i32>,
249 pub turn: Option<usize>,
250 pub prompt: Option<String>,
251 pub error: Option<String>,
252 pub total_tokens: Option<usize>,
253 pub verification_summary_json: Option<String>,
254 pub verification_summary_text: Option<String>,
255 pub error_kind_json: Option<String>,
256}
257
258impl AgentEventProjectionV1 {
259 fn string(payload: &Value, key: &str) -> Option<String> {
260 payload.get(key)?.as_str().map(ToOwned::to_owned)
261 }
262
263 fn usize(payload: &Value, key: &str) -> Option<usize> {
264 usize::try_from(payload.get(key)?.as_u64()?).ok()
265 }
266
267 fn i32(payload: &Value, key: &str) -> Option<i32> {
268 i32::try_from(payload.get(key)?.as_i64()?).ok()
269 }
270}
271
272impl From<EventEnvelopeV1> for AgentEventProjectionV1 {
273 fn from(envelope: EventEnvelopeV1) -> Self {
274 let payload_json = envelope.payload.to_string();
275 let metadata_json = envelope.metadata.as_ref().map(Value::to_string);
276 let data_json = match envelope.event_type.as_str() {
277 AgentEventTypeV1::AGENT_START
278 | AgentEventTypeV1::TURN_START
279 | AgentEventTypeV1::TEXT_DELTA
280 | AgentEventTypeV1::REASONING_DELTA
281 | AgentEventTypeV1::TOOL_START
282 | AgentEventTypeV1::TOOL_INPUT_DELTA
283 | AgentEventTypeV1::TOOL_OUTPUT_DELTA
284 | AgentEventTypeV1::TURN_END
285 | AgentEventTypeV1::AGENT_END
286 | AgentEventTypeV1::ERROR
287 | AgentEventTypeV1::PLANNING_START => None,
288 _ => Some(payload_json.clone()),
289 };
290 let mut projection = Self {
291 version: envelope.version,
292 event_type: envelope.event_type,
293 payload: envelope.payload,
294 metadata: envelope.metadata,
295 payload_json,
296 metadata_json,
297 data_json,
298 text: None,
299 tool_name: None,
300 tool_id: None,
301 tool_output: None,
302 exit_code: None,
303 turn: None,
304 prompt: None,
305 error: None,
306 total_tokens: None,
307 verification_summary_json: None,
308 verification_summary_text: None,
309 error_kind_json: None,
310 };
311
312 match projection.event_type.as_str() {
313 AgentEventTypeV1::AGENT_START | AgentEventTypeV1::PLANNING_START => {
314 projection.prompt = Self::string(&projection.payload, "prompt");
315 }
316 AgentEventTypeV1::TURN_START => {
317 projection.turn = Self::usize(&projection.payload, "turn");
318 }
319 AgentEventTypeV1::TEXT_DELTA | AgentEventTypeV1::REASONING_DELTA => {
320 projection.text = Self::string(&projection.payload, "text");
321 }
322 AgentEventTypeV1::TOOL_START => {
323 projection.tool_id = Self::string(&projection.payload, "id");
324 projection.tool_name = Self::string(&projection.payload, "name");
325 }
326 AgentEventTypeV1::TOOL_INPUT_DELTA => {
327 projection.tool_id = Self::string(&projection.payload, "id");
328 projection.text = Self::string(&projection.payload, "delta");
329 }
330 AgentEventTypeV1::TOOL_EXECUTION_START => {
331 projection.tool_id = Self::string(&projection.payload, "id");
332 projection.tool_name = Self::string(&projection.payload, "name");
333 }
334 AgentEventTypeV1::TOOL_END => {
335 projection.tool_id = Self::string(&projection.payload, "id");
336 projection.tool_name = Self::string(&projection.payload, "name");
337 projection.tool_output = Self::string(&projection.payload, "output");
338 projection.exit_code = Self::i32(&projection.payload, "exit_code");
339 projection.error_kind_json = projection
340 .payload
341 .get("error_kind")
342 .filter(|value| !value.is_null())
343 .map(Value::to_string);
344 }
345 AgentEventTypeV1::TOOL_OUTPUT_DELTA => {
346 projection.tool_id = Self::string(&projection.payload, "id");
347 projection.tool_name = Self::string(&projection.payload, "name");
348 projection.text = Self::string(&projection.payload, "delta");
349 }
350 AgentEventTypeV1::TURN_END => {
351 projection.turn = Self::usize(&projection.payload, "turn");
352 projection.total_tokens = projection
353 .payload
354 .get("usage")
355 .and_then(|usage| Self::usize(usage, "total_tokens"));
356 }
357 AgentEventTypeV1::AGENT_END => {
358 projection.text = Self::string(&projection.payload, "text");
359 projection.total_tokens = projection
360 .payload
361 .get("usage")
362 .and_then(|usage| Self::usize(usage, "total_tokens"));
363 if let Some(summary) = projection.payload.get("verification_summary") {
364 projection.verification_summary_json = Some(summary.to_string());
365 projection.verification_summary_text = serde_json::from_value(summary.clone())
366 .ok()
367 .map(|summary| crate::verification::format_verification_summary(&summary));
368 }
369 }
370 AgentEventTypeV1::ERROR => {
371 projection.error = Self::string(&projection.payload, "message");
372 }
373 AgentEventTypeV1::CONFIRMATION_REQUIRED | AgentEventTypeV1::PERMISSION_DENIED => {
374 projection.tool_id = Self::string(&projection.payload, "tool_id");
375 projection.tool_name = Self::string(&projection.payload, "tool_name");
376 }
377 AgentEventTypeV1::CONFIRMATION_RECEIVED | AgentEventTypeV1::CONFIRMATION_TIMEOUT => {
378 projection.tool_id = Self::string(&projection.payload, "tool_id");
379 }
380 AgentEventTypeV1::SUBAGENT_START => {
381 projection.tool_id = Self::string(&projection.payload, "task_id");
382 projection.tool_name = Self::string(&projection.payload, "agent");
383 projection.text = Self::string(&projection.payload, "session_id");
384 projection.prompt = Self::string(&projection.payload, "description");
385 }
386 AgentEventTypeV1::SUBAGENT_PROGRESS => {
387 projection.tool_id = Self::string(&projection.payload, "task_id");
388 if let (Some(session_id), Some(status)) = (
389 Self::string(&projection.payload, "session_id"),
390 Self::string(&projection.payload, "status"),
391 ) {
392 projection.text = Some(format!("{session_id}: {status}"));
393 }
394 }
395 AgentEventTypeV1::SUBAGENT_END => {
396 projection.tool_id = Self::string(&projection.payload, "task_id");
397 projection.tool_name = Self::string(&projection.payload, "agent");
398 projection.text = Self::string(&projection.payload, "session_id");
399 projection.tool_output = Self::string(&projection.payload, "output");
400 projection.exit_code = projection
401 .payload
402 .get("success")
403 .and_then(Value::as_bool)
404 .map(|success| if success { 0 } else { 1 });
405 }
406 _ => {}
407 }
408
409 projection
410 }
411}
412
413impl TryFrom<&AgentEvent> for AgentEventProjectionV1 {
414 type Error = EventProtocolError;
415
416 fn try_from(event: &AgentEvent) -> Result<Self, Self::Error> {
417 EventEnvelopeV1::try_from(event).map(Self::from)
418 }
419}
420
421impl TryFrom<AgentEvent> for AgentEventProjectionV1 {
422 type Error = EventProtocolError;
423
424 fn try_from(event: AgentEvent) -> Result<Self, Self::Error> {
425 Self::try_from(&event)
426 }
427}