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    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/// Errors produced while converting a runtime event to the stable wire shape.
98#[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/// Stable, versioned event representation shared by every SDK.
116///
117/// `event_type` is intentionally an open string. Deserializers therefore keep
118/// future event types and their complete payload instead of collapsing them to
119/// an `unknown` sentinel.
120#[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    /// Construct an envelope for a known or future event type.
164    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    /// Attach optional protocol metadata such as run or correlation context.
174    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
210/// Convert a persisted run event into the same v1 envelope used by live SDK
211/// streams, attaching replay position and correlation metadata.
212pub 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/// SDK-facing projection of an envelope.
228///
229/// The canonical fields (`version`, `event_type`, `payload`, `metadata`) are
230/// lossless. Remaining fields retain the pre-v1 SDK conveniences and are
231/// derived centrally so Node and Python cannot disagree about them.
232#[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    /// Legacy SDK `data` view. Events whose payload is not completely
241    /// represented by convenience fields keep the full payload here.
242    /// Unknown future event types always retain their payload.
243    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}