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