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 RunControlApplied => RUN_CONTROL_APPLIED = "run_control_applied",
98 PersistenceFailed => PERSISTENCE_FAILED = "persistence_failed",
99 BudgetThresholdHit => BUDGET_THRESHOLD_HIT = "budget_threshold_hit",
100 PassivationRequested => PASSIVATION_REQUESTED = "passivation_requested",
101 PeerInvocation => PEER_INVOCATION = "peer_invocation",
102}
103
104#[derive(Debug, Error)]
106pub enum EventProtocolError {
107 #[error("failed to serialize agent event: {0}")]
108 Serialization(#[from] serde_json::Error),
109
110 #[error("serialized AgentEvent must be a JSON object with a string `type` field")]
111 InvalidRuntimeShape,
112
113 #[error(
114 "AgentEvent wire type drifted: canonical type is `{canonical}`, serde emitted `{serialized}`"
115 )]
116 TypeMismatch {
117 canonical: &'static str,
118 serialized: String,
119 },
120}
121
122#[derive(Debug, Clone, PartialEq, Serialize)]
128pub struct EventEnvelopeV1 {
129 pub version: u16,
130 #[serde(rename = "type")]
131 pub event_type: String,
132 pub payload: Value,
133 #[serde(default, skip_serializing_if = "Option::is_none")]
134 pub metadata: Option<Value>,
135}
136
137impl<'de> Deserialize<'de> for EventEnvelopeV1 {
138 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
139 where
140 D: Deserializer<'de>,
141 {
142 #[derive(Deserialize)]
143 struct WireEnvelope {
144 version: u16,
145 #[serde(rename = "type")]
146 event_type: String,
147 payload: Value,
148 #[serde(default)]
149 metadata: Option<Value>,
150 }
151
152 let wire = WireEnvelope::deserialize(deserializer)?;
153 if wire.version != EVENT_ENVELOPE_V1_VERSION {
154 return Err(D::Error::custom(format_args!(
155 "unsupported event envelope version {}; expected {}",
156 wire.version, EVENT_ENVELOPE_V1_VERSION
157 )));
158 }
159
160 Ok(Self {
161 version: wire.version,
162 event_type: wire.event_type,
163 payload: wire.payload,
164 metadata: wire.metadata,
165 })
166 }
167}
168
169impl EventEnvelopeV1 {
170 pub fn new(event_type: impl Into<String>, payload: Value) -> Self {
172 Self {
173 version: EVENT_ENVELOPE_V1_VERSION,
174 event_type: event_type.into(),
175 payload,
176 metadata: None,
177 }
178 }
179
180 pub fn with_metadata(mut self, metadata: Value) -> Self {
182 self.metadata = Some(metadata);
183 self
184 }
185}
186
187impl TryFrom<&AgentEvent> for EventEnvelopeV1 {
188 type Error = EventProtocolError;
189
190 fn try_from(event: &AgentEvent) -> Result<Self, Self::Error> {
191 let canonical = event.event_type_v1();
192 let Value::Object(mut serialized) = serde_json::to_value(event)? else {
193 return Err(EventProtocolError::InvalidRuntimeShape);
194 };
195 let Some(Value::String(serialized_type)) = serialized.remove("type") else {
196 return Err(EventProtocolError::InvalidRuntimeShape);
197 };
198 if serialized_type != canonical {
199 return Err(EventProtocolError::TypeMismatch {
200 canonical,
201 serialized: serialized_type,
202 });
203 }
204
205 Ok(Self::new(canonical, Value::Object(serialized)))
206 }
207}
208
209impl TryFrom<AgentEvent> for EventEnvelopeV1 {
210 type Error = EventProtocolError;
211
212 fn try_from(event: AgentEvent) -> Result<Self, Self::Error> {
213 Self::try_from(&event)
214 }
215}
216
217pub fn run_event_envelope_v1(
220 record: &crate::run::RunEventRecord,
221 run_id: &str,
222 session_id: &str,
223) -> Result<EventEnvelopeV1, EventProtocolError> {
224 Ok(
225 EventEnvelopeV1::try_from(&record.event)?.with_metadata(serde_json::json!({
226 "run_id": run_id,
227 "session_id": session_id,
228 "sequence": record.sequence,
229 "timestamp_ms": record.timestamp_ms,
230 })),
231 )
232}
233
234#[derive(Debug, Clone, PartialEq)]
240pub struct AgentEventProjectionV1 {
241 pub version: u16,
242 pub event_type: String,
243 pub payload: Value,
244 pub metadata: Option<Value>,
245 pub payload_json: String,
246 pub metadata_json: Option<String>,
247 pub data_json: Option<String>,
251 pub text: Option<String>,
252 pub tool_name: Option<String>,
253 pub tool_id: Option<String>,
254 pub tool_output: Option<String>,
255 pub exit_code: Option<i32>,
256 pub turn: Option<usize>,
257 pub prompt: Option<String>,
258 pub error: Option<String>,
259 pub total_tokens: Option<usize>,
260 pub verification_summary_json: Option<String>,
261 pub verification_summary_text: Option<String>,
262 pub error_kind_json: Option<String>,
263}
264
265impl AgentEventProjectionV1 {
266 fn string(payload: &Value, key: &str) -> Option<String> {
267 payload.get(key)?.as_str().map(ToOwned::to_owned)
268 }
269
270 fn usize(payload: &Value, key: &str) -> Option<usize> {
271 usize::try_from(payload.get(key)?.as_u64()?).ok()
272 }
273
274 fn i32(payload: &Value, key: &str) -> Option<i32> {
275 i32::try_from(payload.get(key)?.as_i64()?).ok()
276 }
277}
278
279impl From<EventEnvelopeV1> for AgentEventProjectionV1 {
280 fn from(envelope: EventEnvelopeV1) -> Self {
281 let payload_json = envelope.payload.to_string();
282 let metadata_json = envelope.metadata.as_ref().map(Value::to_string);
283 let data_json = match envelope.event_type.as_str() {
284 AgentEventTypeV1::AGENT_START
285 | AgentEventTypeV1::TURN_START
286 | AgentEventTypeV1::TEXT_DELTA
287 | AgentEventTypeV1::REASONING_DELTA
288 | AgentEventTypeV1::TOOL_START
289 | AgentEventTypeV1::TOOL_INPUT_DELTA
290 | AgentEventTypeV1::TOOL_OUTPUT_DELTA
291 | AgentEventTypeV1::TURN_END
292 | AgentEventTypeV1::AGENT_END
293 | AgentEventTypeV1::ERROR
294 | AgentEventTypeV1::PLANNING_START => None,
295 _ => Some(payload_json.clone()),
296 };
297 let mut projection = Self {
298 version: envelope.version,
299 event_type: envelope.event_type,
300 payload: envelope.payload,
301 metadata: envelope.metadata,
302 payload_json,
303 metadata_json,
304 data_json,
305 text: None,
306 tool_name: None,
307 tool_id: None,
308 tool_output: None,
309 exit_code: None,
310 turn: None,
311 prompt: None,
312 error: None,
313 total_tokens: None,
314 verification_summary_json: None,
315 verification_summary_text: None,
316 error_kind_json: None,
317 };
318
319 match projection.event_type.as_str() {
320 AgentEventTypeV1::AGENT_START | AgentEventTypeV1::PLANNING_START => {
321 projection.prompt = Self::string(&projection.payload, "prompt");
322 }
323 AgentEventTypeV1::TURN_START => {
324 projection.turn = Self::usize(&projection.payload, "turn");
325 }
326 AgentEventTypeV1::TEXT_DELTA | AgentEventTypeV1::REASONING_DELTA => {
327 projection.text = Self::string(&projection.payload, "text");
328 }
329 AgentEventTypeV1::TOOL_START => {
330 projection.tool_id = Self::string(&projection.payload, "id");
331 projection.tool_name = Self::string(&projection.payload, "name");
332 }
333 AgentEventTypeV1::TOOL_INPUT_DELTA => {
334 projection.tool_id = Self::string(&projection.payload, "id");
335 projection.text = Self::string(&projection.payload, "delta");
336 }
337 AgentEventTypeV1::TOOL_REQUEST_BOUND => {
338 projection.tool_id = Self::string(&projection.payload, "tool_id");
339 projection.tool_name = Self::string(&projection.payload, "tool_name");
340 }
341 AgentEventTypeV1::TOOL_EXECUTION_START => {
342 projection.tool_id = Self::string(&projection.payload, "id");
343 projection.tool_name = Self::string(&projection.payload, "name");
344 }
345 AgentEventTypeV1::TOOL_END => {
346 projection.tool_id = Self::string(&projection.payload, "id");
347 projection.tool_name = Self::string(&projection.payload, "name");
348 projection.tool_output = Self::string(&projection.payload, "output");
349 projection.exit_code = Self::i32(&projection.payload, "exit_code");
350 projection.error_kind_json = projection
351 .payload
352 .get("error_kind")
353 .filter(|value| !value.is_null())
354 .map(Value::to_string);
355 }
356 AgentEventTypeV1::TOOL_OUTPUT_DELTA => {
357 projection.tool_id = Self::string(&projection.payload, "id");
358 projection.tool_name = Self::string(&projection.payload, "name");
359 projection.text = Self::string(&projection.payload, "delta");
360 }
361 AgentEventTypeV1::TURN_END => {
362 projection.turn = Self::usize(&projection.payload, "turn");
363 projection.total_tokens = projection
364 .payload
365 .get("usage")
366 .and_then(|usage| Self::usize(usage, "total_tokens"));
367 }
368 AgentEventTypeV1::AGENT_END => {
369 projection.text = Self::string(&projection.payload, "text");
370 projection.total_tokens = projection
371 .payload
372 .get("usage")
373 .and_then(|usage| Self::usize(usage, "total_tokens"));
374 if let Some(summary) = projection.payload.get("verification_summary") {
375 projection.verification_summary_json = Some(summary.to_string());
376 projection.verification_summary_text = serde_json::from_value(summary.clone())
377 .ok()
378 .map(|summary| crate::verification::format_verification_summary(&summary));
379 }
380 }
381 AgentEventTypeV1::ERROR => {
382 projection.error = Self::string(&projection.payload, "message");
383 }
384 AgentEventTypeV1::CONFIRMATION_REQUIRED | AgentEventTypeV1::PERMISSION_DENIED => {
385 projection.tool_id = Self::string(&projection.payload, "tool_id");
386 projection.tool_name = Self::string(&projection.payload, "tool_name");
387 }
388 AgentEventTypeV1::CONFIRMATION_RECEIVED | AgentEventTypeV1::CONFIRMATION_TIMEOUT => {
389 projection.tool_id = Self::string(&projection.payload, "tool_id");
390 }
391 AgentEventTypeV1::SUBAGENT_START => {
392 projection.tool_id = Self::string(&projection.payload, "task_id");
393 projection.tool_name = Self::string(&projection.payload, "agent");
394 projection.text = Self::string(&projection.payload, "session_id");
395 projection.prompt = Self::string(&projection.payload, "description");
396 }
397 AgentEventTypeV1::SUBAGENT_PROGRESS => {
398 projection.tool_id = Self::string(&projection.payload, "task_id");
399 if let (Some(session_id), Some(status)) = (
400 Self::string(&projection.payload, "session_id"),
401 Self::string(&projection.payload, "status"),
402 ) {
403 projection.text = Some(format!("{session_id}: {status}"));
404 }
405 }
406 AgentEventTypeV1::SUBAGENT_END => {
407 projection.tool_id = Self::string(&projection.payload, "task_id");
408 projection.tool_name = Self::string(&projection.payload, "agent");
409 projection.text = Self::string(&projection.payload, "session_id");
410 projection.tool_output = Self::string(&projection.payload, "output");
411 projection.exit_code = projection
412 .payload
413 .get("success")
414 .and_then(Value::as_bool)
415 .map(|success| if success { 0 } else { 1 });
416 }
417 _ => {}
418 }
419
420 projection
421 }
422}
423
424impl TryFrom<&AgentEvent> for AgentEventProjectionV1 {
425 type Error = EventProtocolError;
426
427 fn try_from(event: &AgentEvent) -> Result<Self, Self::Error> {
428 EventEnvelopeV1::try_from(event).map(Self::from)
429 }
430}
431
432impl TryFrom<AgentEvent> for AgentEventProjectionV1 {
433 type Error = EventProtocolError;
434
435 fn try_from(event: AgentEvent) -> Result<Self, Self::Error> {
436 Self::try_from(&event)
437 }
438}