rightkit_agent_engine_protocol/event/
envelope.rs1use super::ProtocolEvent;
2use crate::TranscriptMessage;
3use serde::{Deserialize, Serialize};
4
5pub const EVENT_ENVELOPE_SCHEMA_V1: u16 = 1;
6
7#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
11#[serde(rename_all = "camelCase")]
12pub struct EventCorrelationV1 {
13 #[serde(default, skip_serializing_if = "Option::is_none")]
14 pub run_id: Option<String>,
15 #[serde(default, skip_serializing_if = "Option::is_none")]
16 pub turn_id: Option<String>,
17 #[serde(default, skip_serializing_if = "Option::is_none")]
18 pub task_id: Option<String>,
19 #[serde(default, skip_serializing_if = "Option::is_none")]
20 pub tool_call_id: Option<String>,
21 #[serde(default, skip_serializing_if = "Option::is_none")]
22 pub attempt_id: Option<String>,
23 #[serde(default, skip_serializing_if = "Option::is_none")]
24 pub causal_parent_event_id: Option<String>,
25}
26
27impl EventCorrelationV1 {
28 pub fn from_event(event: &ProtocolEvent) -> Self {
33 let mut correlation = Self::default();
34 match event {
35 ProtocolEvent::SessionTurnFinished { turn_id, .. } => {
36 correlation.turn_id = Some(turn_id.clone());
37 }
38 ProtocolEvent::ToolCallStarted { call, .. }
39 | ProtocolEvent::ToolApprovalRequested { call, .. } => {
40 correlation.tool_call_id = Some(call.id.clone());
41 }
42 ProtocolEvent::ToolApprovalResolved { tool_call_id, .. }
43 | ProtocolEvent::ToolApprovalReviewed { tool_call_id, .. } => {
44 correlation.tool_call_id = Some(tool_call_id.clone());
45 }
46 ProtocolEvent::CredentialRequested { tool_call_id, .. } => {
47 correlation.tool_call_id = Some(tool_call_id.clone());
48 }
49 ProtocolEvent::CredentialResolved { tool_call_id, .. } => {
50 correlation.tool_call_id = Some(tool_call_id.clone());
51 }
52 ProtocolEvent::ToolResultAdded {
53 result: TranscriptMessage::Tool { tool_call_id, .. },
54 ..
55 } => {
56 correlation.tool_call_id = Some(tool_call_id.clone());
57 }
58 ProtocolEvent::TaskStatusUpdated { task_id, .. }
59 | ProtocolEvent::TaskArtifactUpdated { task_id, .. }
60 | ProtocolEvent::ArtifactLineageUpdated { task_id, .. } => {
61 correlation.task_id = Some(task_id.clone());
62 }
63 ProtocolEvent::SessionContextFolded { subtask_id, .. } => {
64 correlation.task_id = Some(subtask_id.clone());
65 }
66 ProtocolEvent::RunGovernanceRejected {
67 tool_call_id,
68 budget,
69 ownership,
70 origin,
71 ..
72 } => {
73 correlation.tool_call_id = Some(tool_call_id.clone());
74 correlation.run_id = budget
75 .as_ref()
76 .map(|value| value.run_id.clone())
77 .or_else(|| origin.as_ref().map(|value| value.run_id.clone()))
78 .or_else(|| ownership.as_ref().map(|value| value.target_run_id.clone()));
79 }
80 _ => {}
81 }
82 correlation
83 }
84}
85
86#[derive(Debug, Clone, Serialize, Deserialize)]
92#[serde(rename_all = "camelCase")]
93pub struct EventEnvelopeV1 {
94 pub schema_version: u16,
95 pub event_id: String,
96 #[serde(default)]
97 pub correlation: EventCorrelationV1,
98 pub event: ProtocolEvent,
99}
100
101impl EventEnvelopeV1 {
102 pub fn canonical(event: ProtocolEvent) -> Result<Self, String> {
107 let correlation = EventCorrelationV1::from_event(&event);
108 Self::for_durable_event(event, correlation)
109 }
110
111 pub fn for_durable_event(
112 event: ProtocolEvent,
113 correlation: EventCorrelationV1,
114 ) -> Result<Self, String> {
115 if !event.is_durable_fact() {
116 return Err("live projection events do not receive durable event envelopes".into());
117 }
118 let meta = event.meta();
119 if meta.session_id.trim().is_empty() {
120 return Err("durable event envelope requires session_id".into());
121 }
122 if meta.seq < 1 {
123 return Err("durable event envelope requires committed sequence >= 1".into());
124 }
125 Ok(Self {
126 schema_version: EVENT_ENVELOPE_SCHEMA_V1,
127 event_id: stable_event_id(&meta.session_id, meta.seq),
128 correlation,
129 event,
130 })
131 }
132}
133
134pub fn stable_event_id(session_id: &str, seq: i64) -> String {
135 format!("evt-v1:{session_id}:{seq}")
136}