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