Skip to main content

rightkit_agent_engine_protocol/event/protocol_event/
methods.rs

1use super::{ProtocolEvent, ProtocolEventMeta};
2use crate::message::TranscriptMessage;
3
4macro_rules! match_event_meta {
5    ($event:expr, |$session_id:ident, $seq:ident, $created_at:ident| $body:expr) => {
6        match $event {
7            ProtocolEvent::SessionStarted {
8                session_id: $session_id,
9                seq: $seq,
10                created_at: $created_at,
11                ..
12            }
13            | ProtocolEvent::SessionTitleUpdated {
14                session_id: $session_id,
15                seq: $seq,
16                created_at: $created_at,
17                ..
18            }
19            | ProtocolEvent::AssistantDelta {
20                session_id: $session_id,
21                seq: $seq,
22                created_at: $created_at,
23                ..
24            }
25            | ProtocolEvent::AssistantMessageCompleted {
26                session_id: $session_id,
27                seq: $seq,
28                created_at: $created_at,
29                ..
30            }
31            | ProtocolEvent::ToolCallStarted {
32                session_id: $session_id,
33                seq: $seq,
34                created_at: $created_at,
35                ..
36            }
37            | ProtocolEvent::ToolApprovalRequested {
38                session_id: $session_id,
39                seq: $seq,
40                created_at: $created_at,
41                ..
42            }
43            | ProtocolEvent::ToolApprovalResolved {
44                session_id: $session_id,
45                seq: $seq,
46                created_at: $created_at,
47                ..
48            }
49            | ProtocolEvent::ToolResultAdded {
50                session_id: $session_id,
51                seq: $seq,
52                created_at: $created_at,
53                ..
54            }
55            | ProtocolEvent::SessionTurnFinished {
56                session_id: $session_id,
57                seq: $seq,
58                created_at: $created_at,
59                ..
60            }
61            | ProtocolEvent::StreamResyncRequired {
62                session_id: $session_id,
63                seq: $seq,
64                created_at: $created_at,
65                ..
66            }
67            | ProtocolEvent::Overloaded {
68                session_id: $session_id,
69                seq: $seq,
70                created_at: $created_at,
71                ..
72            }
73            | ProtocolEvent::PersistenceFailed {
74                session_id: $session_id,
75                seq: $seq,
76                created_at: $created_at,
77                ..
78            }
79            | ProtocolEvent::ProviderRetrying {
80                session_id: $session_id,
81                seq: $seq,
82                created_at: $created_at,
83                ..
84            }
85            | ProtocolEvent::SessionCompactionStarted {
86                session_id: $session_id,
87                seq: $seq,
88                created_at: $created_at,
89                ..
90            }
91            | ProtocolEvent::VerificationFinished {
92                session_id: $session_id,
93                seq: $seq,
94                created_at: $created_at,
95                ..
96            }
97            | ProtocolEvent::TaskGraphUpdated {
98                session_id: $session_id,
99                seq: $seq,
100                created_at: $created_at,
101                ..
102            }
103            | ProtocolEvent::TaskStatusUpdated {
104                session_id: $session_id,
105                seq: $seq,
106                created_at: $created_at,
107                ..
108            }
109            | ProtocolEvent::TaskArtifactUpdated {
110                session_id: $session_id,
111                seq: $seq,
112                created_at: $created_at,
113                ..
114            }
115            | ProtocolEvent::ArtifactLineageUpdated {
116                session_id: $session_id,
117                seq: $seq,
118                created_at: $created_at,
119                ..
120            }
121            | ProtocolEvent::RunInterrupted {
122                session_id: $session_id,
123                seq: $seq,
124                created_at: $created_at,
125                ..
126            }
127            | ProtocolEvent::RunFailed {
128                session_id: $session_id,
129                seq: $seq,
130                created_at: $created_at,
131                ..
132            }
133            | ProtocolEvent::SessionCompacted {
134                session_id: $session_id,
135                seq: $seq,
136                created_at: $created_at,
137                ..
138            }
139            | ProtocolEvent::SessionContextFolded {
140                session_id: $session_id,
141                seq: $seq,
142                created_at: $created_at,
143                ..
144            }
145            | ProtocolEvent::UsageUpdated {
146                session_id: $session_id,
147                seq: $seq,
148                created_at: $created_at,
149                ..
150            }
151            | ProtocolEvent::TurnUsage {
152                session_id: $session_id,
153                seq: $seq,
154                created_at: $created_at,
155                ..
156            }
157            | ProtocolEvent::PromptCacheBreak {
158                session_id: $session_id,
159                seq: $seq,
160                created_at: $created_at,
161                ..
162            }
163            | ProtocolEvent::ToolOutputDelta {
164                session_id: $session_id,
165                seq: $seq,
166                created_at: $created_at,
167                ..
168            }
169            | ProtocolEvent::BackgroundShellExited {
170                session_id: $session_id,
171                seq: $seq,
172                created_at: $created_at,
173                ..
174            }
175            | ProtocolEvent::BudgetExceeded {
176                session_id: $session_id,
177                seq: $seq,
178                created_at: $created_at,
179                ..
180            }
181            | ProtocolEvent::ProviderRouteUpdated {
182                session_id: $session_id,
183                seq: $seq,
184                created_at: $created_at,
185                ..
186            }
187            | ProtocolEvent::AccountRateLimited {
188                session_id: $session_id,
189                seq: $seq,
190                created_at: $created_at,
191                ..
192            }
193            | ProtocolEvent::AccountUsageReported {
194                session_id: $session_id,
195                seq: $seq,
196                created_at: $created_at,
197                ..
198            }
199            | ProtocolEvent::DispatchDecided {
200                session_id: $session_id,
201                seq: $seq,
202                created_at: $created_at,
203                ..
204            }
205            | ProtocolEvent::OrchestrationObservationRecorded {
206                session_id: $session_id,
207                seq: $seq,
208                created_at: $created_at,
209                ..
210            }
211            | ProtocolEvent::AgentQueryIssued {
212                session_id: $session_id,
213                seq: $seq,
214                created_at: $created_at,
215                ..
216            }
217            | ProtocolEvent::AgentQueryResponded {
218                session_id: $session_id,
219                seq: $seq,
220                created_at: $created_at,
221                ..
222            }
223            | ProtocolEvent::HumanSteeringInjected {
224                session_id: $session_id,
225                seq: $seq,
226                created_at: $created_at,
227                ..
228            }
229            | ProtocolEvent::SubAgentSpawned {
230                session_id: $session_id,
231                seq: $seq,
232                created_at: $created_at,
233                ..
234            }
235            | ProtocolEvent::SubAgentCompleted {
236                session_id: $session_id,
237                seq: $seq,
238                created_at: $created_at,
239                ..
240            }
241            | ProtocolEvent::SubAgentCancelled {
242                session_id: $session_id,
243                seq: $seq,
244                created_at: $created_at,
245                ..
246            }
247            | ProtocolEvent::UserMessageSubmitted {
248                session_id: $session_id,
249                seq: $seq,
250                created_at: $created_at,
251                ..
252            }
253            | ProtocolEvent::UserQuestionAsked {
254                session_id: $session_id,
255                seq: $seq,
256                created_at: $created_at,
257                ..
258            }
259            | ProtocolEvent::McpElicitationRequested {
260                session_id: $session_id,
261                seq: $seq,
262                created_at: $created_at,
263                ..
264            }
265            | ProtocolEvent::CredentialRequested {
266                session_id: $session_id,
267                seq: $seq,
268                created_at: $created_at,
269                ..
270            }
271            | ProtocolEvent::CredentialResolved {
272                session_id: $session_id,
273                seq: $seq,
274                created_at: $created_at,
275                ..
276            }
277            | ProtocolEvent::UserQuestionAnswered {
278                session_id: $session_id,
279                seq: $seq,
280                created_at: $created_at,
281                ..
282            }
283            | ProtocolEvent::PlanPresented {
284                session_id: $session_id,
285                seq: $seq,
286                created_at: $created_at,
287                ..
288            }
289            | ProtocolEvent::AgentRosterUpdated {
290                session_id: $session_id,
291                seq: $seq,
292                created_at: $created_at,
293                ..
294            }
295            | ProtocolEvent::CheckRunCompleted {
296                session_id: $session_id,
297                seq: $seq,
298                created_at: $created_at,
299                ..
300            }
301            | ProtocolEvent::GoalProgressUpdated {
302                session_id: $session_id,
303                seq: $seq,
304                created_at: $created_at,
305                ..
306            }
307            | ProtocolEvent::SelfReviewProposalReceived {
308                session_id: $session_id,
309                seq: $seq,
310                created_at: $created_at,
311                ..
312            }
313            | ProtocolEvent::ToolApprovalReviewed {
314                session_id: $session_id,
315                seq: $seq,
316                created_at: $created_at,
317                ..
318            }
319            | ProtocolEvent::ModelRouteDecision {
320                session_id: $session_id,
321                seq: $seq,
322                created_at: $created_at,
323                ..
324            }
325            | ProtocolEvent::RunGovernanceRejected {
326                session_id: $session_id,
327                seq: $seq,
328                created_at: $created_at,
329                ..
330            } => $body,
331        }
332    };
333}
334
335/// Exhaustive set_seq matcher: any new ProtocolEvent variant forces a
336/// compile-time decision here. Used by the storage event-row sequence
337/// unifier to align the row sequence with the payload sequence at the
338/// boundary, and by the conductor's pre-publish canonicalization (where the
339/// row sequence may be unknown but the caller supplies one).
340fn assign_seq(event: &mut ProtocolEvent, new_seq: i64) {
341    match event {
342        ProtocolEvent::SessionStarted { seq, .. }
343        | ProtocolEvent::SessionTitleUpdated { seq, .. }
344        | ProtocolEvent::AssistantDelta { seq, .. }
345        | ProtocolEvent::AssistantMessageCompleted { seq, .. }
346        | ProtocolEvent::ToolCallStarted { seq, .. }
347        | ProtocolEvent::ToolApprovalRequested { seq, .. }
348        | ProtocolEvent::ToolApprovalResolved { seq, .. }
349        | ProtocolEvent::ToolResultAdded { seq, .. }
350        | ProtocolEvent::SessionTurnFinished { seq, .. }
351        | ProtocolEvent::StreamResyncRequired { seq, .. }
352        | ProtocolEvent::Overloaded { seq, .. }
353        | ProtocolEvent::PersistenceFailed { seq, .. }
354        | ProtocolEvent::ProviderRetrying { seq, .. }
355        | ProtocolEvent::SessionCompactionStarted { seq, .. }
356        | ProtocolEvent::VerificationFinished { seq, .. }
357        | ProtocolEvent::TaskGraphUpdated { seq, .. }
358        | ProtocolEvent::TaskStatusUpdated { seq, .. }
359        | ProtocolEvent::TaskArtifactUpdated { seq, .. }
360        | ProtocolEvent::ArtifactLineageUpdated { seq, .. }
361        | ProtocolEvent::RunInterrupted { seq, .. }
362        | ProtocolEvent::RunFailed { seq, .. }
363        | ProtocolEvent::SessionCompacted { seq, .. }
364        | ProtocolEvent::SessionContextFolded { seq, .. }
365        | ProtocolEvent::UsageUpdated { seq, .. }
366        | ProtocolEvent::TurnUsage { seq, .. }
367        | ProtocolEvent::PromptCacheBreak { seq, .. }
368        | ProtocolEvent::ToolOutputDelta { seq, .. }
369        | ProtocolEvent::BackgroundShellExited { seq, .. }
370        | ProtocolEvent::BudgetExceeded { seq, .. }
371        | ProtocolEvent::ProviderRouteUpdated { seq, .. }
372        | ProtocolEvent::AccountRateLimited { seq, .. }
373        | ProtocolEvent::AccountUsageReported { seq, .. }
374        | ProtocolEvent::DispatchDecided { seq, .. }
375        | ProtocolEvent::OrchestrationObservationRecorded { seq, .. }
376        | ProtocolEvent::AgentQueryIssued { seq, .. }
377        | ProtocolEvent::AgentQueryResponded { seq, .. }
378        | ProtocolEvent::HumanSteeringInjected { seq, .. }
379        | ProtocolEvent::SubAgentSpawned { seq, .. }
380        | ProtocolEvent::SubAgentCompleted { seq, .. }
381        | ProtocolEvent::SubAgentCancelled { seq, .. }
382        | ProtocolEvent::UserMessageSubmitted { seq, .. }
383        | ProtocolEvent::UserQuestionAsked { seq, .. }
384        | ProtocolEvent::McpElicitationRequested { seq, .. }
385        | ProtocolEvent::CredentialRequested { seq, .. }
386        | ProtocolEvent::CredentialResolved { seq, .. }
387        | ProtocolEvent::UserQuestionAnswered { seq, .. }
388        | ProtocolEvent::PlanPresented { seq, .. }
389        | ProtocolEvent::AgentRosterUpdated { seq, .. }
390        | ProtocolEvent::CheckRunCompleted { seq, .. }
391        | ProtocolEvent::GoalProgressUpdated { seq, .. }
392        | ProtocolEvent::SelfReviewProposalReceived { seq, .. }
393        | ProtocolEvent::ModelRouteDecision { seq, .. }
394        | ProtocolEvent::ToolApprovalReviewed { seq, .. }
395        | ProtocolEvent::RunGovernanceRejected { seq, .. } => *seq = new_seq,
396    }
397}
398
399/// Id-bearing message helper — exhaustive over every variant that carries a
400/// `TranscriptMessage`. The compiler refuses to add a new variant without
401/// deciding the identifier semantics here.
402fn rewrite_message_id<F: FnOnce(&mut TranscriptMessage)>(event: &mut ProtocolEvent, rewrite: F) {
403    match event {
404        ProtocolEvent::AssistantMessageCompleted { message, .. } => rewrite(message),
405        ProtocolEvent::ToolResultAdded { result, .. } => rewrite(result),
406        ProtocolEvent::SessionContextFolded { fold, .. } => rewrite(fold),
407        _ => {}
408    }
409}
410
411impl ProtocolEvent {
412    pub fn meta(&self) -> ProtocolEventMeta {
413        match_event_meta!(self, |session_id, seq, created_at| ProtocolEventMeta {
414            session_id: session_id.clone(),
415            seq: *seq,
416            created_at: created_at.clone(),
417        })
418    }
419
420    pub fn session_id(&self) -> &str {
421        match_event_meta!(self, |session_id, _seq, _created_at| session_id.as_str())
422    }
423
424    pub fn seq(&self) -> i64 {
425        match_event_meta!(self, |_session_id, seq, _created_at| *seq)
426    }
427
428    pub fn created_at(&self) -> &str {
429        match_event_meta!(self, |_session_id, _seq, created_at| created_at.as_str())
430    }
431
432    /// Exhaustive set_seq. Used by `decode_event_row` and by `Conduit::publish`
433    /// when the caller has determined the canonical sequence for this event.
434    pub fn set_seq(&mut self, new_seq: i64) {
435        assign_seq(self, new_seq);
436    }
437
438    /// Canonicalize every identity-bearing field against `authoritative_seq`.
439    /// `seq` is rewritten for every variant; `TranscriptMessage::id` on each
440    /// nested message is rewritten too so producer-chosen constant IDs (`final`,
441    /// `forced-stop`) cannot survive a row-replay.
442    pub fn canonicalize_identity(&mut self, authoritative_seq: i64) {
443        assign_seq(self, authoritative_seq);
444        let outer = authoritative_seq;
445        rewrite_message_id(self, |message| {
446            message.set_id(derive_message_id(message, outer));
447        });
448    }
449}
450
451/// Choose the canonical id for a `TranscriptMessage` based on its variant.
452fn derive_message_id(message: &TranscriptMessage, row_seq: i64) -> String {
453    use TranscriptMessage::*;
454    match message {
455        System { .. } => crate::ids::message_id(row_seq),
456        User { .. } => crate::ids::message_id(row_seq),
457        Assistant { .. } => crate::ids::assistant_message_id(row_seq),
458        Tool { tool_call_id, .. } => crate::ids::tool_result_id(tool_call_id, row_seq),
459        Compact { .. } => crate::ids::message_id(row_seq),
460        Fold { .. } => crate::ids::message_id(row_seq),
461    }
462}
463
464#[cfg(test)]
465mod tests {
466    use super::*;
467    use crate::message::TranscriptMessage;
468    use crate::tool::ToolCall;
469
470    fn assistant_with_id(id: &str) -> TranscriptMessage {
471        TranscriptMessage::Assistant {
472            id: id.into(),
473            created_at: "t".into(),
474            content: "hi".into(),
475            reasoning_content: None,
476            tool_calls: vec![],
477            pinned: false,
478        }
479    }
480
481    fn tool_message(id: &str) -> TranscriptMessage {
482        TranscriptMessage::Tool {
483            id: id.into(),
484            created_at: "t".into(),
485            tool_call_id: "tc1".into(),
486            summary: "ok".into(),
487            output: None,
488            trust: None,
489            is_error: false,
490            pinned: false,
491            process_id: None,
492        }
493    }
494
495    #[test]
496    fn set_seq_rewrites_every_variant() {
497        let mut ev = ProtocolEvent::AssistantDelta {
498            session_id: "s".into(),
499            seq: 0,
500            created_at: "t".into(),
501            text: "x".into(),
502            is_reasoning: false,
503        };
504        ev.set_seq(42);
505        assert_eq!(ev.seq(), 42);
506
507        let mut ev = ProtocolEvent::ModelRouteDecision {
508            session_id: "s".into(),
509            seq: 0,
510            created_at: "t".into(),
511            decision: crate::event::ModelRouteDecisionPayload {
512                agent_role: "default".into(),
513                model_id: "test-model".into(),
514                tier: "cheap".into(),
515                reason: "unit-test".into(),
516                signals_digest: "0".into(),
517                price_snapshot: crate::event::PriceSnapshot {
518                    prompt_per_m: 0.0,
519                    completion_per_m: 0.0,
520                    source: "test".into(),
521                    fetched_at: "t".into(),
522                    hash: "h".into(),
523                },
524                est_cost_usd: 0.0,
525                schema_version: 1,
526            },
527        };
528        ev.set_seq(7);
529        assert_eq!(ev.seq(), 7);
530    }
531
532    #[test]
533    fn canonicalize_identity_assigns_seq_and_message_id() {
534        let mut ev = ProtocolEvent::AssistantMessageCompleted {
535            session_id: "s".into(),
536            seq: 0,
537            created_at: "t".into(),
538            message: assistant_with_id("final"),
539        };
540        ev.canonicalize_identity(11);
541        assert_eq!(ev.seq(), 11);
542        match &ev {
543            ProtocolEvent::AssistantMessageCompleted { message, .. } => {
544                assert_eq!(message.id(), "assistant:11");
545            }
546            _ => panic!("variant"),
547        }
548    }
549
550    #[test]
551    fn canonicalize_tool_result_uses_tool_call_id() {
552        let mut ev = ProtocolEvent::ToolResultAdded {
553            session_id: "s".into(),
554            seq: 0,
555            created_at: "t".into(),
556            result: tool_message("tr"),
557        };
558        ev.canonicalize_identity(22);
559        match &ev {
560            ProtocolEvent::ToolResultAdded { result, .. } => {
561                assert_eq!(result.id(), "tool:tc1:22");
562            }
563            _ => panic!("variant"),
564        }
565    }
566
567    #[test]
568    fn canonicalize_session_started_keeps_variants_without_a_message() {
569        let mut ev = ProtocolEvent::SessionStarted {
570            session_id: "s".into(),
571            seq: 0,
572            created_at: "t".into(),
573            title: None,
574        };
575        ev.canonicalize_identity(3);
576        assert_eq!(ev.seq(), 3);
577        assert_eq!(ev.session_id(), "s");
578    }
579
580    #[test]
581    fn set_seq_is_exhaustive_compile_check() {
582        // Adding a ProtocolEvent variant must also add an arm to
583        // `assign_seq`/`set_seq`. The exhaustive match below lists every
584        // existing variant; if a new variant is added this test fails to
585        // compile — the developer is forced to choose identity semantics
586        // there too. Any variant not explicitly handled falls into a
587        // deliberate compile error.
588        fn all_arms(e: &mut ProtocolEvent, mutate: impl Fn(&mut i64)) {
589            match e {
590                ProtocolEvent::SessionStarted { seq, .. }
591                | ProtocolEvent::SessionTitleUpdated { seq, .. }
592                | ProtocolEvent::AssistantDelta { seq, .. }
593                | ProtocolEvent::AssistantMessageCompleted { seq, .. }
594                | ProtocolEvent::ToolCallStarted { seq, .. }
595                | ProtocolEvent::ToolApprovalRequested { seq, .. }
596                | ProtocolEvent::ToolApprovalResolved { seq, .. }
597                | ProtocolEvent::ToolResultAdded { seq, .. }
598                | ProtocolEvent::SessionTurnFinished { seq, .. }
599                | ProtocolEvent::StreamResyncRequired { seq, .. }
600                | ProtocolEvent::Overloaded { seq, .. }
601                | ProtocolEvent::PersistenceFailed { seq, .. }
602                | ProtocolEvent::ProviderRetrying { seq, .. }
603                | ProtocolEvent::SessionCompactionStarted { seq, .. }
604                | ProtocolEvent::VerificationFinished { seq, .. }
605                | ProtocolEvent::TaskGraphUpdated { seq, .. }
606                | ProtocolEvent::TaskStatusUpdated { seq, .. }
607                | ProtocolEvent::TaskArtifactUpdated { seq, .. }
608                | ProtocolEvent::ArtifactLineageUpdated { seq, .. }
609                | ProtocolEvent::RunInterrupted { seq, .. }
610                | ProtocolEvent::RunFailed { seq, .. }
611                | ProtocolEvent::SessionCompacted { seq, .. }
612                | ProtocolEvent::SessionContextFolded { seq, .. }
613                | ProtocolEvent::UsageUpdated { seq, .. }
614                | ProtocolEvent::TurnUsage { seq, .. }
615                | ProtocolEvent::PromptCacheBreak { seq, .. }
616                | ProtocolEvent::ToolOutputDelta { seq, .. }
617                | ProtocolEvent::BackgroundShellExited { seq, .. }
618                | ProtocolEvent::BudgetExceeded { seq, .. }
619                | ProtocolEvent::ProviderRouteUpdated { seq, .. }
620                | ProtocolEvent::AccountRateLimited { seq, .. }
621                | ProtocolEvent::AccountUsageReported { seq, .. }
622                | ProtocolEvent::DispatchDecided { seq, .. }
623                | ProtocolEvent::OrchestrationObservationRecorded { seq, .. }
624                | ProtocolEvent::AgentQueryIssued { seq, .. }
625                | ProtocolEvent::AgentQueryResponded { seq, .. }
626                | ProtocolEvent::HumanSteeringInjected { seq, .. }
627                | ProtocolEvent::SubAgentSpawned { seq, .. }
628                | ProtocolEvent::SubAgentCompleted { seq, .. }
629                | ProtocolEvent::SubAgentCancelled { seq, .. }
630                | ProtocolEvent::UserMessageSubmitted { seq, .. }
631                | ProtocolEvent::UserQuestionAsked { seq, .. }
632                | ProtocolEvent::McpElicitationRequested { seq, .. }
633                | ProtocolEvent::CredentialRequested { seq, .. }
634                | ProtocolEvent::CredentialResolved { seq, .. }
635                | ProtocolEvent::UserQuestionAnswered { seq, .. }
636                | ProtocolEvent::PlanPresented { seq, .. }
637                | ProtocolEvent::AgentRosterUpdated { seq, .. }
638                | ProtocolEvent::CheckRunCompleted { seq, .. }
639                | ProtocolEvent::GoalProgressUpdated { seq, .. }
640                | ProtocolEvent::SelfReviewProposalReceived { seq, .. }
641                | ProtocolEvent::ToolApprovalReviewed { seq, .. }
642                | ProtocolEvent::RunGovernanceRejected { seq, .. } => mutate(seq),
643                // ModelRouteDecision nests a payload — exhaustive coverage
644                // requires the inner seq; in practice, set_seq delegates to
645                // assign_seq which already mirrors this exact arm list.
646                ProtocolEvent::ModelRouteDecision { seq, .. } => mutate(seq),
647            }
648        }
649        let mut ev = ProtocolEvent::ToolCallStarted {
650            session_id: "s".into(),
651            seq: 0,
652            created_at: "t".into(),
653            call: ToolCall {
654                id: "tc1".into(),
655                name: "read_file".into(),
656                arguments_json: "{}".into(),
657            },
658        };
659        all_arms(&mut ev, |s| *s = 9);
660        assert_eq!(ev.seq(), 9);
661    }
662}