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    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/// Errors produced while converting a runtime event to the stable wire shape.
104#[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/// Stable, versioned event representation shared by every SDK.
122///
123/// `event_type` is intentionally an open string. Deserializers therefore keep
124/// future event types and their complete payload instead of collapsing them to
125/// an `unknown` sentinel.
126#[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    /// Construct an envelope for a known or future event type.
170    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    /// Attach optional protocol metadata such as run or correlation context.
180    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
216/// Convert a persisted run event into the same v1 envelope used by live SDK
217/// streams, attaching replay position and correlation metadata.
218pub 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/// SDK-facing projection of an envelope.
234///
235/// The canonical fields (`version`, `event_type`, `payload`, `metadata`) are
236/// lossless. Remaining fields retain the pre-v1 SDK conveniences and are
237/// derived centrally so Node and Python cannot disagree about them.
238#[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    /// Legacy SDK `data` view. Events whose payload is not completely
247    /// represented by convenience fields keep the full payload here.
248    /// Unknown future event types always retain their payload.
249    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}