1mod reduce;
9mod runtime;
10
11pub(crate) use reduce::approval_state_from_record;
12use reduce::*;
13
14pub use runtime::{EMPTY_RUNTIME_GUIDANCE, RuntimePhase, RuntimeState};
15
16use std::{
17 collections::{BTreeMap, HashMap, HashSet, VecDeque},
18 time::{Duration, Instant},
19};
20
21use cookie_agent_protocol::{
22 AgentId, ApprovalCapability, ApprovalConstraints, ApprovalEvaluation, ApprovalFinalOutcome,
23 ApprovalId, ApprovalRecord, ApprovalRequest, ApprovalStatus, ApprovalTrigger,
24 AssistantToolCallRef, AttemptId, EventPayload, EventSubscriptionMessage, GoalId,
25 GoalReminderIdentity, GoalState, GoalStatus, ModelErrorSummary, OperationFingerprint,
26 PersistedModelTurn, PreparedApprovalResource, PreparedCapabilityLifetime, ProducerDeliveryMode,
27 ProducerIdempotencyKey, ProducerMessageId, ProducerOwner, ReplayDecision, ReplayDisposition,
28 ResolvedModelRef, RunId, SafeCode, SessionId, SessionTitleChange, Sha256Digest, StoredEvent,
29 ToolAttachment, ToolCallId, ToolTerminationOutcome, Usage, VariantId,
30};
31use serde::Serialize;
32
33use crate::{client::ClientDelivery, markdown::MarkdownDocument};
34
35#[derive(Clone, Debug, Eq, PartialEq)]
38pub enum ToolStatus {
39 Running,
40 Completed,
41 Failed,
42 Cancelled,
43 Interrupted,
44}
45
46#[derive(Clone, Debug)]
51pub struct ToolCallState {
52 pub id: ToolCallId,
53 pub owner: AssistantToolCallRef,
54 pub presentation: cookie_agent_protocol::ToolCallPresentation,
55 pub arguments: String,
57 pub status: ToolStatus,
58 pub detail: String,
59 pub has_output_chunks: bool,
60}
61
62impl ToolCallState {
63 pub fn compact_title(&self) -> String {
68 match &self.presentation.primary_argument {
69 Some(argument) => format!("{} {argument}", self.presentation.title),
70 None => self.presentation.title.to_string(),
71 }
72 }
73}
74
75#[derive(Clone, Debug)]
78pub struct ApprovalState {
79 pub session_id: SessionId,
80 pub approval_id: ApprovalId,
81 pub request_revision: u64,
82 pub operation_fingerprint: OperationFingerprint,
83 pub trigger: ApprovalTrigger,
84 pub normalized_arguments_digest: Sha256Digest,
85 pub execution_context_digest: Sha256Digest,
86 pub capability_lifetime: PreparedCapabilityLifetime,
87 pub capabilities: Vec<ApprovalCapability>,
88 pub resources: Vec<PreparedApprovalResource>,
89 pub evaluations: Vec<ApprovalEvaluation>,
90 pub constraints: ApprovalConstraints,
91 pub escalated: bool,
92}
93
94impl ApprovalState {
95 pub(crate) fn is_visible_user_escalation(&self) -> bool {
98 self.escalated
99 && self
100 .constraints
101 .expires_at
102 .is_none_or(|expires_at| expires_at > jiff::Timestamp::now())
103 }
104}
105
106#[derive(Clone, Copy, Debug, Eq, Ord, PartialEq, PartialOrd)]
109pub enum EventLevel {
110 Debug,
111 Info,
112 Warning,
113 Error,
114}
115
116impl EventLevel {
117 pub fn badge(self) -> &'static str {
118 match self {
119 Self::Debug => "[D]",
120 Self::Info => "[I]",
121 Self::Warning => "[W]",
122 Self::Error => "[E]",
123 }
124 }
125
126 pub fn name(self) -> &'static str {
127 match self {
128 Self::Debug => "debug",
129 Self::Info => "info",
130 Self::Warning => "warning",
131 Self::Error => "error",
132 }
133 }
134}
135
136#[derive(Clone, Debug, Eq, PartialEq)]
139pub struct FrozenAssistantAttribution {
140 pub agent: AgentId,
141 pub resolved_model: ResolvedModelRef,
142}
143
144impl FrozenAssistantAttribution {
145 pub fn header(&self) -> String {
147 format!(
148 "{} • {}[{}]",
149 self.agent,
150 self.resolved_model.selection.model,
151 self.variant_label()
152 )
153 }
154
155 pub fn variant_label(&self) -> String {
158 self.resolved_model
159 .selection
160 .variant
161 .as_ref()
162 .map_or_else(|| "base".to_owned(), |variant| variant.to_string())
163 }
164}
165
166#[derive(Clone, Debug)]
171pub struct PendingInput {
172 pub text: String,
173 pub admission_seq: u64,
174 pub admitted_at: jiff::Timestamp,
176}
177
178#[derive(Clone, Copy, Debug, Eq, PartialEq)]
179pub enum ProducerMessageStatus {
180 Pending,
181 Admitted,
182 Claimed,
183 Consumed,
184 Discarded,
185}
186
187#[derive(Clone, Debug)]
189pub enum TranscriptItem {
190 User {
191 id: u64,
192 version: u64,
193 text: String,
194 seq: u64,
197 },
198 Assistant {
199 id: u64,
200 version: u64,
201 attribution: FrozenAssistantAttribution,
202 committed_turn_seq: Option<u64>,
203 children: Vec<AssistantChild>,
204 },
205 Event {
209 id: u64,
210 version: u64,
211 level: EventLevel,
212 text: String,
213 },
214 Compaction {
216 id: u64,
217 version: u64,
218 seq: u64,
219 commit: cookie_agent_protocol::ContextCheckpointCommit,
220 },
221 PluginMessage {
223 id: u64,
224 version: u64,
225 seq: u64,
226 role: cookie_agent_protocol::ExtensionMessageRole,
227 input: String,
228 },
229 AgentMd {
232 id: u64,
233 version: u64,
234 seq: u64,
235 entries: Vec<cookie_agent_protocol::AgentMdEntry>,
236 },
237 SkillLoaded {
239 id: u64,
240 version: u64,
241 seq: u64,
242 name: String,
243 source_path: String,
244 args: String,
245 rendered_body: String,
246 },
247 Goal {
248 id: u64,
249 seq: u64,
250 activation: bool,
253 goal: GoalState,
254 },
255 ProducerMessage {
256 id: u64,
257 seq: u64,
260 accepted_at: jiff::Timestamp,
263 message_id: ProducerMessageId,
264 producer_owner: ProducerOwner,
265 mode: ProducerDeliveryMode,
266 body: String,
267 summary: Option<String>,
269 reminder: Option<GoalReminderIdentity>,
270 status: ProducerMessageStatus,
271 },
272}
273
274#[derive(Clone, Debug)]
277pub enum AssistantChild {
278 Text {
279 id: u64,
281 version: u64,
282 markdown: MarkdownDocument,
283 },
284 Thinking {
285 id: u64,
287 version: u64,
288 text: String,
289 },
290 Tool {
291 call_id: ToolCallId,
292 },
293 Attribution {
295 resolved_model: ResolvedModelRef,
296 },
297 CommittedTool {
302 turn_seq: u64,
303 content_index: u32,
304 name: SafeCode,
307 },
308 MediaFile {
310 turn_seq: u64,
311 content_index: u32,
312 file: cookie_agent_protocol::PersistedFilePart,
313 },
314}
315
316impl AssistantChild {
317 pub fn id(&self) -> u64 {
318 match self {
319 Self::Text { id, .. } | Self::Thinking { id, .. } => *id,
320 Self::Tool { .. }
321 | Self::Attribution { .. }
322 | Self::CommittedTool { .. }
323 | Self::MediaFile { .. } => 0,
324 }
325 }
326
327 pub fn version(&self) -> u64 {
328 match self {
329 Self::Text { version, .. } | Self::Thinking { version, .. } => *version,
330 Self::Tool { .. }
331 | Self::Attribution { .. }
332 | Self::CommittedTool { .. }
333 | Self::MediaFile { .. } => 0,
334 }
335 }
336}
337
338#[derive(Clone, Copy, Debug, Eq, PartialEq)]
339pub(crate) enum AssistantPartKind {
340 Text,
341 Thinking,
342}
343
344#[derive(Clone, Copy, Debug)]
345pub(crate) struct OpenAssistantProjection {
346 item_id: u64,
347 part_id: u64,
348 kind: AssistantPartKind,
349 opened_at: jiff::Timestamp,
353}
354
355impl TranscriptItem {
356 pub fn id(&self) -> u64 {
357 match self {
358 Self::User { id, .. }
359 | Self::Assistant { id, .. }
360 | Self::Event { id, .. }
361 | Self::Compaction { id, .. }
362 | Self::PluginMessage { id, .. }
363 | Self::AgentMd { id, .. }
364 | Self::SkillLoaded { id, .. }
365 | Self::Goal { id, .. }
366 | Self::ProducerMessage { id, .. } => *id,
367 }
368 }
369
370 pub fn version(&self) -> u64 {
371 match self {
372 Self::User { version, .. }
373 | Self::Assistant { version, .. }
374 | Self::Event { version, .. }
375 | Self::Compaction { version, .. }
376 | Self::PluginMessage { version, .. }
377 | Self::AgentMd { version, .. }
378 | Self::SkillLoaded { version, .. } => *version,
379 Self::Goal { .. } => 0,
380 Self::ProducerMessage { status, .. } => match status {
381 ProducerMessageStatus::Pending => 0,
382 ProducerMessageStatus::Admitted => 1,
383 ProducerMessageStatus::Claimed => 2,
384 ProducerMessageStatus::Consumed => 3,
385 ProducerMessageStatus::Discarded => 4,
386 },
387 }
388 }
389
390 #[cfg(test)]
391 pub fn user(text: impl Into<String>) -> Self {
392 Self::User {
393 id: 1,
394 version: 0,
395 text: text.into(),
396 seq: 2,
399 }
400 }
401
402 #[cfg(test)]
403 pub fn internal(text: impl Into<String>) -> Self {
404 Self::Event {
405 id: 1,
406 version: 0,
407 level: EventLevel::Info,
408 text: text.into(),
409 }
410 }
411}
412
413#[derive(Clone, Debug)]
415pub(crate) struct AttemptProjection {
416 item_id: u64,
417 run_id: Option<RunId>,
418 committed_prefix: usize,
421 attribution_marker: Option<usize>,
422 split_segments: Vec<(u64, usize)>,
428}
429
430#[derive(Clone, Debug)]
432pub(crate) struct RunAssistantProjection {
433 pub(crate) run_id: RunId,
434 pub(crate) item_id: u64,
435 pub(crate) committed_prefix: usize,
436 pub(crate) current_model: ResolvedModelRef,
437 pub(crate) split_pending: bool,
442}
443
444#[derive(Clone, Copy, Debug, Default)]
450pub(crate) struct AssistantTurnMetrics {
451 pub(crate) timed_output_tokens: u64,
452 pub(crate) generation: Duration,
453 pub(crate) timed_turns: u32,
454 pub(crate) context_tokens: Option<u64>,
455 pub(crate) estimated_cost_pico_usd: Option<u64>,
456 cost_unpriced: bool,
457}
458
459impl AssistantTurnMetrics {
460 fn record_cost(&mut self, cost: Option<u64>) {
461 if self.cost_unpriced {
462 return;
463 }
464 let Some(cost) = cost else {
465 self.estimated_cost_pico_usd = None;
466 self.cost_unpriced = true;
467 return;
468 };
469 self.estimated_cost_pico_usd = self.estimated_cost_pico_usd.unwrap_or(0).checked_add(cost);
470 if self.estimated_cost_pico_usd.is_none() {
471 self.cost_unpriced = true;
472 }
473 }
474}
475
476#[derive(Clone, Copy, Debug)]
479pub(crate) struct PendingToolRow {
480 turn_seq: u64,
481 content_index: u32,
482 call_id: ToolCallId,
483}
484
485#[derive(Clone, Debug)]
486pub(crate) struct IndexedToolCall {
487 name: SafeCode,
488 arguments: String,
489}
490
491#[derive(Clone, Debug, Eq, Hash, PartialEq)]
495pub(crate) enum ReplayContextTransition {
496 Adapter {
497 found: SafeCode,
498 expected: SafeCode,
499 },
500 ModelSelection {
501 found: cookie_agent_protocol::ModelSelection,
502 expected: cookie_agent_protocol::ModelSelection,
503 },
504 Variant {
505 found: Option<VariantId>,
506 expected: Option<VariantId>,
507 },
508 InvalidPayload {
513 reason: String,
514 },
515}
516
517#[derive(Clone, Debug)]
518pub(crate) struct ProducerMessageProjection {
519 transcript_index: usize,
520 producer_owner: ProducerOwner,
521 reminder: Option<GoalReminderIdentity>,
522 accepted_seq: u64,
523 admission: Option<(RunId, u64)>,
524 claims: HashSet<u64>,
525 status: ProducerMessageStatus,
526 discarded_seq: Option<u64>,
527 consumed_run: Option<RunId>,
528 consumption_recorded: bool,
529}
530
531#[derive(Clone, Debug)]
532pub(crate) struct ProducerClaimProjection {
533 pub(crate) run_id: RunId,
534 pub(crate) message_ids: Vec<ProducerMessageId>,
535}
536
537#[derive(Clone, Debug, Default)]
539pub struct SessionState {
540 pub version: u64,
542 pub generation: u64,
543 pub last_seq: u64,
544 pub(crate) created_at: Option<jiff::Timestamp>,
546 pub(crate) last_agent_activity: Option<jiff::Timestamp>,
548 pub active_run: Option<RunId>,
549 pub cwd_identity: Option<cookie_agent_protocol::CwdIdentity>,
550 pub context_tokens: Option<u64>,
554 pub estimated_cost_usd: Option<f64>,
558 pub run_agent: Option<AgentId>,
560 pub creation_agent: Option<Box<cookie_agent_protocol::AgentSnapshot>>,
564 pub run_snapshot: Option<Box<cookie_agent_protocol::AgentSnapshot>>,
566 pub run_selected_suffix: Option<Vec<cookie_agent_protocol::FrozenModelBinding>>,
571 pub model_selection: cookie_agent_protocol::SessionModelState,
572 pub goal: Option<GoalState>,
573 pub transcript: Vec<TranscriptItem>,
574 pub(crate) item_times: HashMap<u64, jiff::Timestamp>,
579 pub pending_inputs: VecDeque<PendingInput>,
584 pub(crate) initial_input_submitted: HashSet<RunId>,
587 pub voided_inputs: Vec<String>,
591 pub(crate) next_transcript_id: u64,
592 pub(crate) open_assistant: Option<OpenAssistantProjection>,
593 pub thinking_durations: HashMap<(u64, u64), Duration>,
596 pub(crate) event_timestamps: BTreeMap<u64, jiff::Timestamp>,
599 pub(crate) assistant_metrics: HashMap<u64, AssistantTurnMetrics>,
602 pub(crate) open_run_assistant: Option<RunAssistantProjection>,
603 pub(crate) interrupted_assistant_items: HashSet<u64>,
606 pub(crate) attempts: HashMap<AttemptId, AttemptProjection>,
607 pub(crate) pending_attempt: Option<AttemptId>,
609 pub tools: HashMap<ToolCallId, ToolCallState>,
610 pub(crate) pending_tool_rows: Vec<PendingToolRow>,
613 pub(crate) turn_tool_index: HashMap<(u64, String), IndexedToolCall>,
616 pub(crate) turn_items: HashMap<u64, u64>,
618 pub(crate) replay_context_warnings: HashSet<(RunId, Sha256Digest, ReplayContextTransition)>,
622 pub(crate) model_turn_warnings: HashSet<(RunId, String)>,
626 pub(crate) goal_revisions: HashMap<GoalId, u64>,
627 pub(crate) producer_messages: HashMap<ProducerMessageId, ProducerMessageProjection>,
628 pub(crate) producer_dedup: HashMap<(ProducerOwner, ProducerIdempotencyKey), ProducerMessageId>,
629 pub(crate) producer_claims: HashMap<u64, ProducerClaimProjection>,
630 pub(crate) terminal_runs: HashSet<RunId>,
631 pub approvals: Vec<ApprovalState>,
632 pub(crate) agent_md_latest_run: Option<Vec<cookie_agent_protocol::AgentMdEntry>>,
637 pub(crate) agent_md_previous_run: Option<Vec<cookie_agent_protocol::AgentMdEntry>>,
638}
639
640impl SessionState {
641 pub fn is_open_assistant_part(&self, item_id: u64, part_id: u64) -> bool {
642 self.open_assistant
643 .is_some_and(|open| open.item_id == item_id && open.part_id == part_id)
644 }
645
646 pub fn is_open_thinking(&self, item_id: u64, part_id: u64) -> bool {
647 self.open_assistant.is_some_and(|open| {
648 open.item_id == item_id
649 && open.part_id == part_id
650 && open.kind == AssistantPartKind::Thinking
651 })
652 }
653
654 pub fn has_open_thinking(&self) -> bool {
656 self.open_assistant
657 .is_some_and(|open| open.kind == AssistantPartKind::Thinking)
658 }
659
660 pub fn has_running_tool(&self) -> bool {
662 self.tools
663 .values()
664 .any(|tool| tool.status == ToolStatus::Running)
665 }
666
667 pub fn has_pending_producers(&self) -> bool {
669 self.transcript.iter().any(|item| {
670 matches!(
671 item,
672 TranscriptItem::ProducerMessage {
673 status: ProducerMessageStatus::Pending | ProducerMessageStatus::Admitted,
674 ..
675 }
676 )
677 })
678 }
679
680 pub fn thinking_duration(&self, item_id: u64, part_id: u64) -> Option<Duration> {
682 self.thinking_durations.get(&(item_id, part_id)).copied()
683 }
684
685 pub(crate) fn item_time(&self, item_id: u64) -> Option<jiff::Timestamp> {
687 self.item_times.get(&item_id).copied()
688 }
689
690 pub(crate) fn mark_event_split_pending(&mut self) {
695 if (self.open_assistant.is_some() || self.pending_attempt.is_some())
696 && let Some(projection) = self.open_run_assistant.as_mut()
697 {
698 projection.split_pending = true;
699 }
700 }
701
702 pub(crate) fn split_run_at_compaction(&mut self) -> Option<usize> {
714 let item_id = self
715 .open_run_assistant
716 .as_ref()
717 .map(|projection| projection.item_id)?;
718 let index = self
719 .transcript
720 .iter()
721 .position(|item| item.id() == item_id)?;
722 if matches!(
723 &self.transcript[index],
724 TranscriptItem::Assistant {
725 children,
726 committed_turn_seq,
727 ..
728 } if children.is_empty() && committed_turn_seq.is_none()
729 ) {
730 let projection = self
734 .open_run_assistant
735 .as_mut()
736 .expect("located run projection");
737 projection.split_pending = false;
738 projection.committed_prefix = 0;
739 return Some(index);
740 }
741 self.open_run_assistant
742 .as_mut()
743 .expect("located run projection")
744 .split_pending = true;
745 None
746 }
747}
748
749#[derive(Clone, Debug, Default)]
751pub struct StateStore {
752 pub sessions: HashMap<SessionId, SessionState>,
753 physical_events: HashMap<SessionId, Vec<StoredEvent>>,
754 abandoned_output: HashMap<ToolCallId, SessionId>,
755 tool_sessions: HashMap<ToolCallId, SessionId>,
756 quarantined_sessions: HashSet<SessionId>,
757 replays: HashMap<SessionId, ReplayProgress>,
758}
759
760#[derive(Clone, Debug)]
761struct ReplayProgress {
762 generation: u64,
763 final_seq: u64,
764 scratch: SessionState,
765 physical_events: Vec<StoredEvent>,
766 deadline: Instant,
767}
768
769#[derive(Clone, Copy, Debug, Eq, PartialEq)]
771pub enum DeliveryOutcome {
772 Applied,
773 Gap { session_id: SessionId, cursor: u64 },
774 ReplayFailed { session_id: SessionId },
775}
776
777const REPLAY_END_TIMEOUT: Duration = Duration::from_secs(5);
778
779impl StateStore {
780 pub fn apply_delivery(&mut self, delivery: ClientDelivery) -> DeliveryOutcome {
787 match delivery {
788 ClientDelivery::Live {
789 message,
790 generation,
791 } => match *message {
792 EventSubscriptionMessage::Event { event } => {
793 let session_id = event.session_id;
794 if self.apply_event_for_generation(*event, generation) {
795 DeliveryOutcome::Applied
796 } else {
797 let cursor = self
798 .sessions
799 .get(&session_id)
800 .map_or(0, |state| state.last_seq);
801 DeliveryOutcome::Gap { session_id, cursor }
802 }
803 }
804 EventSubscriptionMessage::Gap {
805 session_id,
806 last_delivered_seq,
807 } => DeliveryOutcome::Gap {
808 session_id,
809 cursor: last_delivered_seq,
810 },
811 },
812 ClientDelivery::ReplayStart {
813 session_id,
814 generation,
815 final_seq,
816 rebuild,
817 } => {
818 let mut scratch = if rebuild {
819 SessionState {
820 generation,
821 ..SessionState::default()
822 }
823 } else if let Some(state) = self.sessions.get(&session_id)
824 && state.generation == generation
825 {
826 state.clone()
827 } else {
828 self.quarantined_sessions.insert(session_id);
829 return DeliveryOutcome::ReplayFailed { session_id };
830 };
831 close_open_assistant(&mut scratch, jiff::Timestamp::now());
832 self.replays.insert(
833 session_id,
834 ReplayProgress {
835 generation,
836 final_seq,
837 scratch,
838 physical_events: if rebuild {
839 Vec::new()
840 } else {
841 self.physical_events
842 .get(&session_id)
843 .cloned()
844 .unwrap_or_default()
845 },
846 deadline: Instant::now() + REPLAY_END_TIMEOUT,
847 },
848 );
849 DeliveryOutcome::Applied
850 }
851 ClientDelivery::ReplayEvent {
852 session_id,
853 generation,
854 final_seq,
855 event,
856 } => {
857 let event = *event;
858 let started_call = match &event.payload {
859 EventPayload::ToolCallStarted { start } => Some(start.tool_call_id),
860 _ => None,
861 };
862 if let Some(call_id) = started_call {
863 self.tool_sessions.insert(call_id, session_id);
864 }
865 let valid = self.replays.get_mut(&session_id).is_some_and(|replay| {
866 if replay.generation != generation || replay.final_seq != final_seq {
867 return false;
868 }
869 if event.seq <= replay.scratch.last_seq {
870 return true;
871 }
872 replay.physical_events.push(event.clone());
873 if matches!(event.payload, EventPayload::SessionReverted { .. }) {
874 replay.scratch =
875 reduce_session_events(session_id, generation, &replay.physical_events);
876 } else {
877 replay.scratch.last_seq = event.seq;
878 reduce_event(
879 &mut replay.scratch,
880 event.session_id,
881 event.run_id,
882 event.seq,
883 event.timestamp,
884 event.payload,
885 );
886 }
887 replay.scratch.version = replay.scratch.version.wrapping_add(1);
888 true
889 });
890 if !valid {
891 self.abandon_replay(session_id);
892 DeliveryOutcome::ReplayFailed { session_id }
893 } else {
894 DeliveryOutcome::Applied
895 }
896 }
897 ClientDelivery::ReplayEnd {
898 session_id,
899 generation,
900 final_seq,
901 } => {
902 let replay = self.replays.remove(&session_id);
903 let valid = replay.as_ref().is_some_and(|replay| {
904 replay.generation == generation
905 && replay.final_seq == final_seq
906 && replay.scratch.last_seq == final_seq
907 });
908 match replay {
909 Some(mut replay) if valid => {
910 self.quarantined_sessions.remove(&session_id);
911 self.abandoned_output
912 .retain(|_, output_session| *output_session != session_id);
913 for call_id in replay.scratch.tools.keys() {
914 self.abandoned_output.remove(call_id);
915 }
916 replay.scratch.version = self
917 .sessions
918 .get(&session_id)
919 .map(|previous| {
920 previous.version.max(replay.scratch.version).wrapping_add(1)
921 })
922 .unwrap_or(replay.scratch.version);
923 self.physical_events
924 .insert(session_id, replay.physical_events);
925 self.sessions.insert(session_id, replay.scratch);
926 DeliveryOutcome::Applied
927 }
928 Some(replay) => {
929 self.quarantine_replay_output(session_id, &replay);
930 DeliveryOutcome::ReplayFailed { session_id }
931 }
932 None => DeliveryOutcome::ReplayFailed { session_id },
933 }
934 }
935 ClientDelivery::OutputSnapshot(snapshot) => {
938 if let Some(session_id) = self.quarantined_output(snapshot.snapshot.call_id) {
939 return DeliveryOutcome::ReplayFailed { session_id };
940 }
941 DeliveryOutcome::Applied
942 }
943 ClientDelivery::OutputDelta(delta) => {
944 if let Some(session_id) = self.quarantined_output(delta.call_id) {
945 return DeliveryOutcome::ReplayFailed { session_id };
946 }
947 DeliveryOutcome::Applied
948 }
949 ClientDelivery::OutputGap(gap) => {
950 if let Some(session_id) = self.quarantined_output(gap.call_id) {
951 return DeliveryOutcome::ReplayFailed { session_id };
952 }
953 DeliveryOutcome::Applied
954 }
955 ClientDelivery::RecoveryFailed { .. } => DeliveryOutcome::Applied,
956 ClientDelivery::Disconnected { error } => {
957 let timestamp = jiff::Timestamp::now();
960 for state in self.sessions.values_mut() {
961 push_event(
962 state,
963 EventLevel::Error,
964 format!("connection failed: {error}"),
965 timestamp,
966 );
967 }
968 DeliveryOutcome::Applied
969 }
970 ClientDelivery::PluginEvent(_) => DeliveryOutcome::Applied,
971 ClientDelivery::RuntimeChanged(_) => DeliveryOutcome::Applied,
972 }
973 }
974
975 pub fn abandon_timed_out_replays(&mut self) -> Vec<SessionId> {
978 let now = Instant::now();
979 let expired = self
980 .replays
981 .iter()
982 .filter_map(|(session_id, replay)| (replay.deadline <= now).then_some(*session_id))
983 .collect::<Vec<_>>();
984 for session_id in &expired {
985 self.abandon_replay(*session_id);
986 }
987 expired
988 }
989
990 pub fn abandon_replays(&mut self) -> Vec<SessionId> {
992 let sessions = self.replays.keys().copied().collect::<Vec<_>>();
993 for session_id in &sessions {
994 self.abandon_replay(*session_id);
995 }
996 sessions
997 }
998
999 fn quarantined_output(&self, call_id: ToolCallId) -> Option<SessionId> {
1000 if self
1001 .replays
1002 .values()
1003 .any(|replay| replay.scratch.tools.contains_key(&call_id))
1004 {
1005 return None;
1006 }
1007 self.tool_sessions
1008 .get(&call_id)
1009 .copied()
1010 .filter(|session_id| {
1011 self.quarantined_sessions.contains(session_id)
1012 || self.replays.contains_key(session_id)
1013 })
1014 .or_else(|| self.abandoned_output.get(&call_id).copied())
1015 }
1016
1017 fn abandon_replay(&mut self, session_id: SessionId) {
1018 self.quarantined_sessions.insert(session_id);
1019 if let Some(replay) = self.replays.remove(&session_id) {
1020 self.quarantine_replay_output(session_id, &replay);
1021 }
1022 }
1023
1024 fn quarantine_replay_output(&mut self, session_id: SessionId, replay: &ReplayProgress) {
1025 for call_id in replay.scratch.tools.keys() {
1026 self.abandoned_output.insert(*call_id, session_id);
1027 }
1028 }
1029
1030 pub fn take_voided_inputs(&mut self, session_id: SessionId) -> Vec<String> {
1033 self.sessions
1034 .get_mut(&session_id)
1035 .map(|state| std::mem::take(&mut state.voided_inputs))
1036 .unwrap_or_default()
1037 }
1038
1039 pub fn park_voided_input(&mut self, session_id: SessionId, text: String) {
1042 if let Some(state) = self.sessions.get_mut(&session_id) {
1043 state.voided_inputs.push(text);
1044 }
1045 }
1046
1047 pub fn apply_event(&mut self, event: StoredEvent) -> bool {
1049 self.apply_event_for_generation(event, 0)
1050 }
1051
1052 pub fn apply_event_for_generation(&mut self, event: StoredEvent, generation: u64) -> bool {
1053 let started_call = match &event.payload {
1054 EventPayload::ToolCallStarted { start } => Some(start.tool_call_id),
1055 _ => None,
1056 };
1057 if let Some(call_id) = started_call {
1058 self.tool_sessions.insert(call_id, event.session_id);
1059 }
1060 if self.quarantined_sessions.contains(&event.session_id) {
1061 return false;
1062 }
1063 let state = self.sessions.entry(event.session_id).or_default();
1064 if state.generation != generation {
1065 return false;
1066 }
1067 if event.seq <= state.last_seq {
1068 return true;
1069 }
1070 self.physical_events
1071 .entry(event.session_id)
1072 .or_default()
1073 .push(event.clone());
1074 if matches!(event.payload, EventPayload::SessionReverted { .. }) {
1075 let previous_version = state.version;
1076 *state = reduce_session_events(
1077 event.session_id,
1078 generation,
1079 self.physical_events
1080 .get(&event.session_id)
1081 .expect("physical event was inserted"),
1082 );
1083 state.version = previous_version.max(state.version).wrapping_add(1);
1084 } else {
1085 state.last_seq = event.seq;
1086 reduce_event(
1087 state,
1088 event.session_id,
1089 event.run_id,
1090 event.seq,
1091 event.timestamp,
1092 event.payload,
1093 );
1094 state.version = state.version.wrapping_add(1);
1095 }
1096 true
1097 }
1098
1099 pub fn apply_subscription(&mut self, message: EventSubscriptionMessage) -> Option<u64> {
1102 self.apply_subscription_for_generation(message, 0)
1103 }
1104
1105 pub fn apply_subscription_for_generation(
1106 &mut self,
1107 message: EventSubscriptionMessage,
1108 generation: u64,
1109 ) -> Option<u64> {
1110 match message {
1111 EventSubscriptionMessage::Event { event } => {
1112 let cursor = self
1113 .sessions
1114 .get(&event.session_id)
1115 .map_or(0, |state| state.last_seq);
1116 self.apply_event_for_generation(*event, generation)
1117 .then_some(())
1118 .map_or(Some(cursor), |_| None)
1119 }
1120 EventSubscriptionMessage::Gap {
1121 last_delivered_seq, ..
1122 } => Some(last_delivered_seq),
1123 }
1124 }
1125
1126 pub fn reset_session(&mut self, session_id: SessionId, generation: u64) -> bool {
1132 if self.quarantined_sessions.contains(&session_id) || self.replays.contains_key(&session_id)
1133 {
1134 return false;
1135 }
1136 self.sessions.insert(
1137 session_id,
1138 SessionState {
1139 generation,
1140 ..SessionState::default()
1141 },
1142 );
1143 self.physical_events.remove(&session_id);
1144 true
1145 }
1146
1147 pub fn rebuild_session(
1150 &mut self,
1151 session_id: SessionId,
1152 generation: u64,
1153 events: Vec<StoredEvent>,
1154 ) -> bool {
1155 for (expected, event) in (1..).zip(&events) {
1156 if event.session_id != session_id || event.seq != expected {
1157 return false;
1158 }
1159 }
1160 if self.quarantined_sessions.contains(&session_id) || self.replays.contains_key(&session_id)
1161 {
1162 return false;
1163 }
1164 let state = reduce_session_events(session_id, generation, &events);
1165 self.physical_events.insert(session_id, events);
1166 self.sessions.insert(session_id, state);
1167 true
1168 }
1169}
1170
1171#[cfg(test)]
1172mod tests;