Skip to main content

a3s_code_core/
event_protocol.rs

1//! Versioned wire protocol for [`AgentEvent`].
2//!
3//! `AgentEvent` is the runtime enum. [`EventEnvelopeV1`] is its stable,
4//! language-neutral representation. SDKs consume the envelope instead of
5//! maintaining their own event-name matches, so a new runtime event cannot be
6//! silently projected as `unknown`.
7
8use crate::agent::AgentEvent;
9use serde::{de::Error as _, Deserialize, Deserializer, Serialize};
10use serde_json::Value;
11use thiserror::Error;
12
13/// Wire version carried by [`EventEnvelopeV1`].
14pub const EVENT_ENVELOPE_V1_VERSION: u16 = 1;
15
16macro_rules! define_agent_event_types_v1 {
17    ($( $variant:ident => $constant:ident = $wire_name:literal ),+ $(,)?) => {
18        /// Canonical event type names for event envelope version 1.
19        ///
20        /// SDK constants and parity checks are generated from this catalog.
21        #[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        /// Complete, ordered set of event type names supported by envelope v1.
31        pub const AGENT_EVENT_TYPES_V1: &[&str] = &[
32            $(AgentEventTypeV1::$constant),+
33        ];
34
35        impl AgentEvent {
36            /// Return the canonical version-1 wire type for this event.
37            ///
38            /// This match is deliberately exhaustive. Adding an `AgentEvent`
39            /// variant without assigning it a stable wire name is a compile
40            /// error rather than an SDK event named `unknown`.
41            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/// Errors produced while converting a runtime event to the stable wire shape.
99#[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/// Stable, versioned event representation shared by every SDK.
117///
118/// `event_type` is intentionally an open string. Deserializers therefore keep
119/// future event types and their complete payload instead of collapsing them to
120/// an `unknown` sentinel.
121#[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    /// Construct an envelope for a known or future event type.
165    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    /// Attach optional protocol metadata such as run or correlation context.
175    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
211/// Convert a persisted run event into the same v1 envelope used by live SDK
212/// streams, attaching replay position and correlation metadata.
213pub 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/// SDK-facing projection of an envelope.
229///
230/// The canonical fields (`version`, `event_type`, `payload`, `metadata`) are
231/// lossless. Remaining fields retain the pre-v1 SDK conveniences and are
232/// derived centrally so Node and Python cannot disagree about them.
233#[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    /// Legacy SDK `data` view. Events whose payload is not completely
242    /// represented by convenience fields keep the full payload here.
243    /// Unknown future event types always retain their payload.
244    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}