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