Skip to main content

rightkit_agent_engine_protocol/event/
protocol_event.rs

1use super::{
2    types::{
3        AgentRosterRow, MiddlewareDecisionEvidence, ModelRouteDecisionPayload,
4        OrchestrationObservation, ProtocolEventMeta, SelfReviewProposalDraft,
5    },
6    UserQuestion, VerificationIssue,
7};
8use crate::message::TranscriptMessage;
9use crate::task::TaskNode;
10use crate::tool::ToolCall;
11use crate::trust::TrustEnvelope;
12use serde::{Deserialize, Serialize};
13use serde_json::Value;
14
15/// A verified, daemon-owned step in a goal plan.
16#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
17#[serde(rename_all = "camelCase")]
18pub struct GoalStepV1 {
19    pub id: String,
20    pub label: String,
21    pub weight: i64,
22    pub state: String,
23    pub verification_state: String,
24}
25
26/// Canonical goal truth shared by SSE and session hydration.
27#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
28#[serde(rename_all = "camelCase")]
29pub struct GoalSnapshotV1 {
30    pub goal_id: String,
31    pub objective: String,
32    pub status: String,
33    pub started_at: String,
34    pub closed_at: Option<String>,
35    pub dismissed_at: Option<String>,
36    pub decomposition_state: String,
37    pub plan_revision: i64,
38    pub completion_percent: Option<i64>,
39    pub now: Option<String>,
40    pub next: Option<String>,
41    pub steps: Vec<GoalStepV1>,
42    pub verification_state: String,
43    pub budget_tokens: i64,
44    pub tokens_used: i64,
45    pub continuation_count: i64,
46    pub no_progress_count: i64,
47}
48
49/// Selection receipt carried inside a compaction-federation decision:
50/// which items the decision covers and a content hash Membrane can use to
51/// detect a stale or substituted selection.
52#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
53#[serde(rename_all = "camelCase")]
54pub struct CompactionFederationSelectionReceiptV1 {
55    pub selected_item_ids: Vec<String>,
56    pub selection_hash: String,
57}
58
59/// CRA-09 joint wire contract (`interface-handoffs.json` row
60/// `compaction-federation-v1`): the one compatible public shape the host app's
61/// compactor-consumer and Membrane's federation-catalog both code against.
62///
63/// Carries identity/fence/scope, the decision, its selection receipt, and a
64/// bounded **server-issued** expiry. The expiry is expressed and checked as
65/// monotonic elapsed milliseconds (`issued_monotonic_elapsed_ms` +
66/// `expires_after_elapsed_ms`), never wall-clock time — a wall clock can be
67/// adjusted (NTP, user change, DST) and would silently defeat the bound.
68/// `restart_epoch` is the process/session monotonic-clock epoch the decision
69/// was issued under; a consumer whose current epoch no longer matches must
70/// treat any pending decision as invalidated by the restart, independent of
71/// whether the elapsed-time expiry would otherwise still be live.
72/// Wire contract-version tag for [`CompactionFederationDecisionV1`], matching
73/// `interface-handoffs.json` row `compaction-federation-v1`. Membrane should
74/// reference this constant rather than the bare string literal.
75pub const COMPACTION_FEDERATION_SCHEMA_V1: &str = "compaction-federation-v1";
76
77fn compaction_federation_schema_v1() -> String {
78    COMPACTION_FEDERATION_SCHEMA_V1.to_string()
79}
80
81#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
82#[serde(rename_all = "camelCase")]
83pub struct CompactionFederationDecisionV1 {
84    /// Wire contract-version tag; always serialized as `$schema`.
85    #[serde(rename = "$schema", default = "compaction_federation_schema_v1")]
86    pub schema: String,
87    /// Stable identity of the decision (idempotency key for one-use commit).
88    pub identity: String,
89    /// Fence token binding this decision to one session/turn boundary.
90    pub fence: String,
91    /// The federation scope this decision governs (e.g. a context/archive id).
92    pub scope: String,
93    /// The decision outcome: `"approved"`, `"deferred"`, or `"denied"`.
94    pub decision: String,
95    pub selection_receipt: CompactionFederationSelectionReceiptV1,
96    /// Monotonic elapsed milliseconds, on the issuing server's own monotonic
97    /// clock, at the moment this decision was issued.
98    pub issued_monotonic_elapsed_ms: u64,
99    /// Bounded server-issued expiry window, in elapsed milliseconds from
100    /// `issued_monotonic_elapsed_ms`. Never a wall-clock timestamp.
101    pub expires_after_elapsed_ms: u64,
102    /// Monotonic-clock epoch (e.g. process/session boot generation) this
103    /// decision was issued under.
104    pub restart_epoch: u64,
105}
106
107impl CompactionFederationDecisionV1 {
108    /// Whether the decision is still live: same monotonic epoch (no restart
109    /// since issuance) and still inside its server-issued expiry window,
110    /// measured strictly against monotonic elapsed time. Must be checked
111    /// before any one-use local commit is honored.
112    pub fn is_live(&self, now_monotonic_elapsed_ms: u64, current_restart_epoch: u64) -> bool {
113        if current_restart_epoch != self.restart_epoch {
114            return false;
115        }
116        match now_monotonic_elapsed_ms.checked_sub(self.issued_monotonic_elapsed_ms) {
117            Some(elapsed) => elapsed <= self.expires_after_elapsed_ms,
118            None => false,
119        }
120    }
121}
122
123#[derive(Debug, Clone, Serialize, Deserialize)]
124#[serde(tag = "$kind")]
125pub enum ProtocolEvent {
126    #[serde(rename = "sessionStarted", rename_all = "camelCase")]
127    SessionStarted {
128        session_id: String,
129        seq: i64,
130        created_at: String,
131        title: Option<String>,
132    },
133
134    #[serde(rename = "sessionTitleUpdated", rename_all = "camelCase")]
135    SessionTitleUpdated {
136        session_id: String,
137        seq: i64,
138        created_at: String,
139        title: String,
140    },
141
142    #[serde(rename = "assistantDelta", rename_all = "camelCase")]
143    AssistantDelta {
144        session_id: String,
145        seq: i64,
146        created_at: String,
147        text: String,
148        is_reasoning: bool,
149    },
150
151    #[serde(rename = "assistantMessageCompleted", rename_all = "camelCase")]
152    AssistantMessageCompleted {
153        session_id: String,
154        seq: i64,
155        created_at: String,
156        message: TranscriptMessage,
157    },
158
159    #[serde(rename = "toolCallStarted", rename_all = "camelCase")]
160    ToolCallStarted {
161        session_id: String,
162        seq: i64,
163        created_at: String,
164        call: ToolCall,
165    },
166
167    #[serde(rename = "toolApprovalRequested", rename_all = "camelCase")]
168    ToolApprovalRequested {
169        session_id: String,
170        seq: i64,
171        created_at: String,
172        call: ToolCall,
173        risk_label: String,
174        proposed_change: Option<String>,
175        policy_source: Option<String>,
176        #[serde(default)]
177        middleware_decisions: Vec<MiddlewareDecisionEvidence>,
178    },
179
180    #[serde(rename = "toolApprovalResolved", rename_all = "camelCase")]
181    ToolApprovalResolved {
182        session_id: String,
183        seq: i64,
184        created_at: String,
185        tool_call_id: String,
186        approved: bool,
187    },
188
189    /// Typed verdict of the approval review stage for one mutating tool call,
190    /// emitted durably before the normal permission/approval gate. `verdict`
191    /// is `allow` (reviewer echoed the exact request identity; the normal
192    /// approval still applies), `block` (reviewer blocked, errored, or answered
193    /// uncorrelated; the call is denied), or `unavailable` (no reviewer is
194    /// configured; nothing is auto-approved and the human gate decides).
195    /// `correlation_id` is the same token the approval and denial records use
196    /// for this tool call.
197    #[serde(rename = "toolApprovalReviewed", rename_all = "camelCase")]
198    ToolApprovalReviewed {
199        session_id: String,
200        seq: i64,
201        created_at: String,
202        tool_call_id: String,
203        tool_name: String,
204        correlation_id: String,
205        verdict: String,
206        reason: String,
207        reviewer_configured: bool,
208    },
209
210    #[serde(rename = "toolResultAdded", rename_all = "camelCase")]
211    ToolResultAdded {
212        session_id: String,
213        seq: i64,
214        created_at: String,
215        result: TranscriptMessage,
216    },
217
218    /// A turn reached a typed terminal state.  Consumers can distinguish a
219    /// completed turn from a transport disconnect without inferring it from
220    /// presentation events.
221    #[serde(rename = "sessionTurnFinished", rename_all = "camelCase")]
222    SessionTurnFinished {
223        session_id: String,
224        seq: i64,
225        created_at: String,
226        turn_id: String,
227        status: String,
228    },
229
230    /// The requested replay cursor predates contiguous conduit coverage.
231    /// Drivers must resync from durable session state before reconnecting.
232    #[serde(rename = "streamResyncRequired", rename_all = "camelCase")]
233    StreamResyncRequired {
234        session_id: String,
235        seq: i64,
236        created_at: String,
237        after_seq: i64,
238        oldest_available_seq: i64,
239        high_water_mark: i64,
240    },
241
242    /// Session persistence latched as failed: later durable facts are refused and
243    /// in-flight tools are cancelled. Live-only by design (never persisted,
244    /// since persistence is what failed); `reason` is the first latch reason.
245    #[serde(rename = "persistenceFailed", rename_all = "camelCase")]
246    PersistenceFailed {
247        session_id: String,
248        seq: i64,
249        created_at: String,
250        reason: String,
251    },
252
253    /// Admission rejected work before it could consume an unbounded queue.
254    #[serde(rename = "overloaded", rename_all = "camelCase")]
255    Overloaded {
256        session_id: String,
257        seq: i64,
258        created_at: String,
259        scope: String,
260        limit: u64,
261        current: u64,
262        #[serde(default)]
263        retry_after_ms: u64,
264    },
265
266    /// A provider call failed transiently and the adapter is backing off
267    /// before retry `attempt` of `max_attempts`.
268    #[serde(rename = "providerRetrying", rename_all = "camelCase")]
269    ProviderRetrying {
270        session_id: String,
271        seq: i64,
272        created_at: String,
273        provider: String,
274        reason: String,
275        attempt: u32,
276        max_attempts: u32,
277        delay_ms: u64,
278    },
279
280    /// Context compaction began; the matching `sessionCompacted` follows on
281    /// success. `trigger` is `automatic`, `manual` or `overflow`.
282    #[serde(rename = "sessionCompactionStarted", rename_all = "camelCase")]
283    SessionCompactionStarted {
284        session_id: String,
285        seq: i64,
286        created_at: String,
287        #[serde(default)]
288        trigger: String,
289        #[serde(default)]
290        command_id: String,
291    },
292
293    #[serde(rename = "verificationFinished", rename_all = "camelCase")]
294    VerificationFinished {
295        session_id: String,
296        seq: i64,
297        created_at: String,
298        summary: String,
299        passed: bool,
300        #[serde(default)]
301        tool: String,
302        #[serde(default)]
303        issue_count: i32,
304        #[serde(default)]
305        issues: Vec<VerificationIssue>,
306    },
307
308    #[serde(rename = "taskGraphUpdated", rename_all = "camelCase")]
309    TaskGraphUpdated {
310        session_id: String,
311        seq: i64,
312        created_at: String,
313        summary: String,
314        #[serde(default)]
315        tasks: Vec<TaskNode>,
316    },
317
318    #[serde(rename = "taskStatusUpdated", rename_all = "camelCase")]
319    TaskStatusUpdated {
320        session_id: String,
321        seq: i64,
322        created_at: String,
323        task_id: String,
324        context_id: String,
325        state: String,
326        task: Value,
327    },
328
329    #[serde(rename = "taskArtifactUpdated", rename_all = "camelCase")]
330    TaskArtifactUpdated {
331        session_id: String,
332        seq: i64,
333        created_at: String,
334        task_id: String,
335        context_id: String,
336        artifact_id: String,
337        artifact: Value,
338    },
339
340    #[serde(rename = "artifactLineageUpdated", rename_all = "camelCase")]
341    ArtifactLineageUpdated {
342        session_id: String,
343        seq: i64,
344        created_at: String,
345        task_id: String,
346        context_id: String,
347        artifact_id: String,
348        lineage: Value,
349    },
350
351    #[serde(rename = "runInterrupted", rename_all = "camelCase")]
352    RunInterrupted {
353        session_id: String,
354        seq: i64,
355        created_at: String,
356        reason: Option<String>,
357    },
358
359    #[serde(rename = "runFailed", rename_all = "camelCase")]
360    RunFailed {
361        session_id: String,
362        seq: i64,
363        created_at: String,
364        error: String,
365        /// Failure class: `network` | `timeout` | `rate_limited` | `auth` |
366        /// `provider_error` | `budget` | `other`
367        #[serde(default)]
368        kind: String,
369        #[serde(default)]
370        hint: String,
371    },
372
373    #[serde(rename = "sessionCompacted", rename_all = "camelCase")]
374    SessionCompacted {
375        session_id: String,
376        seq: i64,
377        created_at: String,
378        summary: String,
379        #[serde(default)]
380        dropped_count: i32,
381        /// Compaction tier that produced this event: `summary` (tier 2,
382        /// model-authored structured session-state summary), `overflow`
383        /// (tier 3, reactive provider-rejection recovery) or `manual`.
384        #[serde(default)]
385        tier: String,
386        /// Estimated model-visible context tokens before compaction.
387        #[serde(default)]
388        tokens_before: u64,
389        /// Estimated model-visible context tokens after compaction.
390        #[serde(default)]
391        tokens_after: u64,
392        /// Session compaction epoch after this compaction (HCTX-004). Each
393        /// successful compaction is an expected prompt-cache break and
394        /// invalidates host-loaded context identities from earlier epochs.
395        #[serde(default)]
396        epoch: u64,
397        #[serde(default)]
398        command_id: String,
399        /// Number of most-recent complete user turns the live pass retained
400        /// verbatim (`recent_turn_message_count` argument; 0 for the
401        /// under-pressure pass). Restart replay uses it to reproduce the same
402        /// retained cut.
403        retained_turns: u32,
404    },
405
406    /// A completed sub-trajectory was replaced by a recoverable fold marker.
407    /// Marker strings are persisted so restart replay applies the same structural
408    /// replacement to the model-facing and visible transcripts.
409    #[serde(rename = "sessionContextFolded", rename_all = "camelCase")]
410    SessionContextFolded {
411        session_id: String,
412        seq: i64,
413        created_at: String,
414        subtask_id: String,
415        start_marker: String,
416        end_marker: String,
417        fold: TranscriptMessage,
418    },
419
420    #[serde(rename = "usageUpdated", rename_all = "camelCase")]
421    UsageUpdated {
422        session_id: String,
423        seq: i64,
424        created_at: String,
425        prompt_tokens: i32,
426        completion_tokens: i32,
427        cost_usd: f64,
428        context_tokens: i32,
429        context_percent: f64,
430        #[serde(default)]
431        cache_read_tokens: i32,
432        #[serde(default)]
433        cache_creation_tokens: i32,
434        /// Whether `cost_usd` came from curated pricing for the active model
435        /// (vs. a conservative estimate for an unpriced model). Consumers that
436        /// surface cost to operators (session rows, `agents --json`) must
437        /// treat `false` as "cost unknown" — report null, not this number.
438        /// Defaults to `true` for older senders that predate this field, to
439        /// preserve their existing (assumed-correct) cost display.
440        #[serde(default = "default_cost_known")]
441        cost_known: bool,
442    },
443
444    /// Per-model-call usage for live prompt-cache / context displays. Emitted
445    /// once per provider response, in addition to the cumulative
446    /// `usageUpdated`. `inputTokens` is the FULL prompt size
447    /// (uncached + cached + cache-write); `cachedInputTokens` is the
448    /// cache-read subset. `reasoningTokens` is a subset of `outputTokens`.
449    /// `contextWindow` is 0 when the model's window is unknown, in which case
450    /// `contextUsedPct` is 0. `contextUsedPct` is 0.0-100.0.
451    #[serde(rename = "turnUsage", rename_all = "camelCase")]
452    TurnUsage {
453        session_id: String,
454        seq: i64,
455        created_at: String,
456        input_tokens: i64,
457        cached_input_tokens: i64,
458        #[serde(default)]
459        cache_creation_tokens: i64,
460        output_tokens: i64,
461        #[serde(default)]
462        reasoning_tokens: i64,
463        #[serde(default)]
464        context_window: i64,
465        #[serde(default)]
466        context_used_tokens: i64,
467        #[serde(default)]
468        context_used_pct: f64,
469        /// This response's prompt-cache hit rate, 0.0-100.0:
470        /// `cachedInputTokens / inputTokens` (`inputTokens` already includes
471        /// cache reads and cache writes). 0 when the response had no input.
472        #[serde(default)]
473        cache_hit_pct: f64,
474        /// Session-aggregate prompt-cache hit rate through this response,
475        /// 0.0-100.0: cache reads / (uncached + cache reads + cache writes).
476        #[serde(default)]
477        session_cache_hit_pct: f64,
478    },
479
480    /// A provider response whose cache read fell materially below what the
481    /// previous request's cached prefix promised (donor
482    /// `promptCacheBreakDetection.ts`). `reason` is the primary attributed
483    /// cause (`system`, `tools`, `model`, `params`, `cacheControl`,
484    /// `messagesEdited`, `compaction`, `ttlExpiry1h`, `ttlExpiry5m`,
485    /// `serverSide`, `unknown`); `detail` lists every observed change.
486    /// `expected` is true for breaks the host app caused deliberately
487    /// (compaction, model switch, explicit tool-set change, idle TTL expiry)
488    /// and false for everything that is silently burning money.
489    /// `estimatedCostLost` is absent when the active model has no curated
490    /// pricing. `turn` is the 1-based provider request number in the session.
491    #[serde(rename = "promptCacheBreak", rename_all = "camelCase")]
492    PromptCacheBreak {
493        session_id: String,
494        seq: i64,
495        created_at: String,
496        reason: String,
497        expected: bool,
498        tokens_lost: i64,
499        #[serde(default, skip_serializing_if = "Option::is_none")]
500        estimated_cost_lost: Option<f64>,
501        turn: i64,
502        #[serde(default)]
503        detail: String,
504        #[serde(default)]
505        previous_cache_read_tokens: i64,
506        #[serde(default)]
507        cache_read_tokens: i64,
508        #[serde(default)]
509        cache_creation_tokens: i64,
510    },
511
512    /// Live output chunk from a running shell/exec tool. Coalesced by the
513    /// emitter (at most one event per stream per ~100ms); `toolResultAdded`
514    /// remains the authoritative final output.
515    #[serde(rename = "toolOutputDelta", rename_all = "camelCase")]
516    ToolOutputDelta {
517        session_id: String,
518        seq: i64,
519        created_at: String,
520        call_id: String,
521        /// `stdout` or `stderr`.
522        stream: String,
523        text: String,
524    },
525
526    /// A background shell reached a terminal state. Emitted once per shell,
527    /// only after the child was reaped, so consumers are notified instead of
528    /// polling `get_shell_output` (which keeps working). Durable: it is the
529    /// settled fact that the process is gone.
530    #[serde(rename = "backgroundShellExited", rename_all = "camelCase")]
531    BackgroundShellExited {
532        session_id: String,
533        seq: i64,
534        created_at: String,
535        shell_id: String,
536        #[serde(default)]
537        command: String,
538        status: super::types::ShellExitStatus,
539        #[serde(default)]
540        output_bytes: u64,
541    },
542
543    #[serde(rename = "budgetExceeded", rename_all = "camelCase")]
544    BudgetExceeded {
545        session_id: String,
546        seq: i64,
547        created_at: String,
548        #[serde(default)]
549        scope: String,
550        current_usd: f64,
551        ceiling_usd: f64,
552        #[serde(default)]
553        hint: String,
554    },
555
556    #[serde(rename = "providerRouteUpdated", rename_all = "camelCase")]
557    ProviderRouteUpdated {
558        session_id: String,
559        seq: i64,
560        created_at: String,
561        #[serde(default)]
562        state: String,
563        #[serde(default)]
564        provider: String,
565        #[serde(default)]
566        model: String,
567        #[serde(default)]
568        failure_kind: String,
569        #[serde(default)]
570        detail: String,
571    },
572
573    /// A vendor account reported rate-limit telemetry. `status` is
574    /// `allowed`, `allowed_warning` or `rejected`; when `rejected`, the UI
575    /// shows "account X capped until `resetsAt`" and offers a manual
576    /// "continue on another account". the host app never switches accounts
577    /// automatically.
578    #[serde(rename = "accountRateLimited", rename_all = "camelCase")]
579    AccountRateLimited {
580        session_id: String,
581        seq: i64,
582        created_at: String,
583        #[serde(default)]
584        provider: String,
585        /// Account display label: the user's label, else email, else the profile id.
586        #[serde(default)]
587        account: String,
588        /// Stable account id (`subscription/<provider>/<label>` or `api_key/<provider>/<slug>`).
589        #[serde(default)]
590        account_id: Option<String>,
591        /// `subscription` or `api_key`.
592        #[serde(default)]
593        account_kind: Option<String>,
594        /// Present only when the vendor CLI reported an email; never a label.
595        #[serde(default)]
596        email: Option<String>,
597        #[serde(default)]
598        organization: Option<String>,
599        #[serde(default)]
600        status: String,
601        #[serde(default)]
602        rate_limit_type: Option<String>,
603        /// Unix seconds at which the window resets.
604        #[serde(default)]
605        resets_at: Option<u64>,
606        #[serde(default)]
607        utilization: Option<f64>,
608        #[serde(default)]
609        overage_status: Option<String>,
610        #[serde(default)]
611        error_code: Option<String>,
612    },
613
614    /// Per-turn cost and token usage reported by a vendor CLI, attributed to
615    /// the account that ran the turn.
616    #[serde(rename = "accountUsageReported", rename_all = "camelCase")]
617    AccountUsageReported {
618        session_id: String,
619        seq: i64,
620        created_at: String,
621        #[serde(default)]
622        provider: String,
623        #[serde(default)]
624        account: String,
625        #[serde(default)]
626        account_id: Option<String>,
627        #[serde(default)]
628        account_kind: Option<String>,
629        #[serde(default)]
630        email: Option<String>,
631        #[serde(default)]
632        total_cost_usd: Option<f64>,
633        #[serde(default)]
634        duration_ms: Option<u64>,
635        #[serde(default)]
636        num_turns: Option<u64>,
637        #[serde(default)]
638        input_tokens: u64,
639        #[serde(default)]
640        output_tokens: u64,
641        #[serde(default)]
642        cache_read_tokens: u64,
643        #[serde(default)]
644        cache_creation_tokens: u64,
645        #[serde(default)]
646        models: Vec<String>,
647    },
648
649    #[serde(rename = "dispatchDecided", rename_all = "camelCase")]
650    DispatchDecided {
651        session_id: String,
652        seq: i64,
653        created_at: String,
654        strategy: String,
655        rationale: String,
656    },
657
658    #[serde(rename = "orchestrationObservationRecorded", rename_all = "camelCase")]
659    OrchestrationObservationRecorded {
660        session_id: String,
661        seq: i64,
662        created_at: String,
663        observation: OrchestrationObservation,
664    },
665
666    #[serde(rename = "agentQueryIssued", rename_all = "camelCase")]
667    AgentQueryIssued {
668        session_id: String,
669        seq: i64,
670        created_at: String,
671        query_id: String,
672        from_role: String,
673        to_role: String,
674        query_type: String,
675        expected_format: String,
676    },
677
678    #[serde(rename = "agentQueryResponded", rename_all = "camelCase")]
679    AgentQueryResponded {
680        session_id: String,
681        seq: i64,
682        created_at: String,
683        query_id: String,
684        from_role: String,
685        to_role: String,
686        confidence: f64,
687    },
688
689    #[serde(rename = "humanSteeringInjected", rename_all = "camelCase")]
690    HumanSteeringInjected {
691        session_id: String,
692        seq: i64,
693        created_at: String,
694        injected_by: String,
695        content_preview: String,
696        /// The actual steering/reprompt text (bounded at the interjection
697        /// byte cap before it is stored). `None` for rows written before this
698        /// field existed; consumers must keep treating the event as an opaque
699        /// steering marker in that case.
700        #[serde(default, skip_serializing_if = "Option::is_none")]
701        text: Option<String>,
702    },
703
704    /// Emitted on the **parent** session when the orchestrator spawns a sub-agent.
705    #[serde(rename = "subAgentSpawned", rename_all = "camelCase")]
706    SubAgentSpawned {
707        session_id: String,
708        seq: i64,
709        created_at: String,
710        sub_session_id: String,
711        role: String,
712        task_title: String,
713    },
714
715    /// Emitted on the parent session when a sub-agent finishes (success or failure).
716    #[serde(rename = "subAgentCompleted", rename_all = "camelCase")]
717    SubAgentCompleted {
718        session_id: String,
719        seq: i64,
720        created_at: String,
721        sub_session_id: String,
722        role: String,
723        success: bool,
724        summary: String,
725        /// The sub-agent's true completion state (parity P-2):
726        /// `"complete"` | `"partial_with_limit"` | `"api_error"`. Defaults
727        /// to `"complete"` for older senders that predate this field, so a
728        /// rolling deploy never fabricates a false partial/error state —
729        /// only new senders that actually detected a cutoff/error set it.
730        #[serde(default = "default_worker_outcome")]
731        outcome: String,
732        /// Set only when `outcome != "complete"`: the limit that fired
733        /// (`"rate_limited"`, `"timeout"`, `"wall_clock"`, `"budget"`) or
734        /// the provider/API error text. Empty for a clean completion.
735        #[serde(default)]
736        outcome_detail: String,
737    },
738
739    /// Emitted on the parent session when a sub-agent is stopped before completing.
740    /// `cancelled_by`: "user" | "budget" | "governance" | "error"
741    #[serde(rename = "subAgentCancelled", rename_all = "camelCase")]
742    SubAgentCancelled {
743        session_id: String,
744        seq: i64,
745        created_at: String,
746        sub_session_id: String,
747        role: String,
748        cancelled_by: String,
749    },
750
751    /// A user/operator turn was submitted to the session. Emitted so the event
752    /// log is the single source of truth for replay (the user side of the
753    /// conversation, not just the assistant/tool side).
754    #[serde(rename = "userMessageSubmitted", rename_all = "camelCase")]
755    UserMessageSubmitted {
756        session_id: String,
757        seq: i64,
758        created_at: String,
759        text: String,
760        #[serde(default, skip_serializing_if = "Option::is_none")]
761        nonce: Option<String>,
762        #[serde(default, skip_serializing_if = "Option::is_none")]
763        trust: Option<TrustEnvelope>,
764        /// Metadata-only references attached to this user turn. Defaulting
765        /// preserves replay compatibility with events written before rich
766        /// references existed.
767        #[serde(default, skip_serializing_if = "Vec::is_empty")]
768        attachments: Vec<crate::ReferenceMeta>,
769    },
770
771    /// The agent is blocking on a structured question to the operator. The UI
772    /// renders the choices and POSTs answers back, keyed by `question_id`.
773    #[serde(rename = "userQuestionAsked", rename_all = "camelCase")]
774    UserQuestionAsked {
775        session_id: String,
776        seq: i64,
777        created_at: String,
778        question_id: String,
779        questions: Vec<UserQuestion>,
780    },
781
782    /// An MCP stdio server issued a bounded elicitation request to the client.
783    /// The operator response is correlated by `request.id` and returned to the
784    /// waiting server request; form values remain opaque JSON data.
785    #[serde(rename = "mcpElicitationRequested", rename_all = "camelCase")]
786    McpElicitationRequested {
787        session_id: String,
788        seq: i64,
789        created_at: String,
790        request: crate::approvals::ElicitationRequestEvent,
791    },
792
793    /// A running tool needs one provider credential before it can continue.
794    /// Carries request identity and provider metadata only; secret input is
795    /// returned through the correlated response channel, never this event.
796    #[serde(rename = "credentialRequested", rename_all = "camelCase")]
797    CredentialRequested {
798        session_id: String,
799        seq: i64,
800        created_at: String,
801        tool_call_id: String,
802        provider: String,
803    },
804
805    #[serde(rename = "credentialResolved", rename_all = "camelCase")]
806    CredentialResolved {
807        session_id: String,
808        seq: i64,
809        created_at: String,
810        tool_call_id: String,
811        outcome: String,
812    },
813
814    /// The operator answered (or the question was otherwise resolved).
815    #[serde(rename = "userQuestionAnswered", rename_all = "camelCase")]
816    UserQuestionAnswered {
817        session_id: String,
818        seq: i64,
819        created_at: String,
820        question_id: String,
821    },
822
823    /// The agent presented a plan via `exit_plan_mode` and is blocking on
824    /// approval. The UI renders the plan and POSTs the decision back keyed by
825    /// `plan_id`; on approval the session leaves Plan mode.
826    #[serde(rename = "planPresented", rename_all = "camelCase")]
827    PlanPresented {
828        session_id: String,
829        seq: i64,
830        created_at: String,
831        plan_id: String,
832        plan: String,
833    },
834
835    /// Per-sub-agent roster snapshot for the session agent rail (F1).
836    #[serde(rename = "agentRosterUpdated", rename_all = "camelCase")]
837    AgentRosterUpdated {
838        session_id: String,
839        seq: i64,
840        created_at: String,
841        agents: Vec<AgentRosterRow>,
842    },
843
844    /// A deterministic pre-gate check finished (F3). `jury_skipped` is true when
845    /// this (failing) check short-circuited the jury for the run.
846    #[serde(rename = "checkRunCompleted", rename_all = "camelCase")]
847    CheckRunCompleted {
848        session_id: String,
849        seq: i64,
850        created_at: String,
851        check_id: String,
852        command: String,
853        exit_code: i32,
854        attempts: u32,
855        duration_ms: u64,
856        passed: bool,
857        jury_skipped: bool,
858    },
859
860    /// A persistent Goal advanced or closed (F2). Emitted on every continuation
861    /// and on close; `status` is the GoalState status string.
862    #[serde(rename = "goalProgressUpdated", rename_all = "camelCase")]
863    GoalProgressUpdated {
864        session_id: String,
865        seq: i64,
866        created_at: String,
867        #[serde(flatten)]
868        snapshot: GoalSnapshotV1,
869    },
870
871    /// A self-review proposal generated by the background self-review fork.
872    #[serde(rename = "selfReviewProposalReceived", rename_all = "camelCase")]
873    SelfReviewProposalReceived {
874        session_id: String,
875        seq: i64,
876        created_at: String,
877        proposal: SelfReviewProposalDraft,
878    },
879
880    /// The capability/cost router (Lane R1a-L3/L4, ADR §D5) picked a model
881    /// for a sub-agent spawn. Emitted at the `router_seam.rs` spawn seam
882    /// alongside the existing `tracing::info!` line — never in place of it.
883    /// `est_cost_usd` is a PRE-CALL estimate (the UI chip's "est." label is
884    /// mandatory, never implied actuals); actual spend still reconciles
885    /// through `UsageUpdated`.
886    #[serde(rename = "modelRouteDecision", rename_all = "camelCase")]
887    ModelRouteDecision {
888        session_id: String,
889        seq: i64,
890        created_at: String,
891        #[serde(flatten)]
892        decision: ModelRouteDecisionPayload,
893    },
894
895    /// Run-governance typed failure. Emitted when the engine rejects a
896    /// tool dispatch on a budget, ownership, or origin invariant (Lane B3/C3).
897    /// The variant's payload is the typed counterexample — both the desktop
898    /// UI and the event log can render the discriminator plus context.
899    #[serde(rename = "runGovernanceRejected", rename_all = "camelCase")]
900    RunGovernanceRejected {
901        session_id: String,
902        seq: i64,
903        created_at: String,
904        kind: super::types::RunFailureKind,
905        tool_call_id: String,
906        #[serde(default)]
907        budget: Option<super::types::RunBudgetExhausted>,
908        #[serde(default)]
909        ownership: Option<super::types::RunOwnershipFailure>,
910        #[serde(default)]
911        origin: Option<super::types::RunOriginFailure>,
912    },
913}
914
915fn default_cost_known() -> bool {
916    true
917}
918
919fn default_worker_outcome() -> String {
920    "complete".to_string()
921}
922
923mod methods;