1use std::collections::{BTreeMap, BTreeSet};
2
3use tea_protocol::{
4 ApprovalDecision, ApprovalId, BranchId, CanonicalMessage, MessageId, RecordEnvelope, RecordId,
5 SessionRecord, SessionSequence, ToolCallId,
6};
7
8use crate::error::SessionReplayError;
9use crate::state::{
10 MaterializedSessionState, RunRecoveryState, ToolExecutionState, TurnCheckpoint,
11 commit_tool_result, declared_tool_calls, finish_tool, interrupt_tool, message_id,
12 new_pending_approval, new_state, new_tool_call, resolve_approval, set_approval, set_compaction,
13 set_model, set_policy, set_profile, set_reasoning_effort, start_tool,
14};
15
16#[derive(Debug, Clone, PartialEq)]
17struct DeclaredToolCall {
18 tool_name: String,
19 arguments: serde_json::Value,
20}
21
22#[derive(Debug, Clone, PartialEq)]
23struct BranchProjection {
24 configuration: crate::SessionConfiguration,
25 messages: Vec<CanonicalMessage>,
26 pending_approvals: BTreeMap<ApprovalId, crate::PendingApproval>,
27 tool_calls: BTreeMap<ToolCallId, crate::ToolCallState>,
28 run_recovery: BTreeMap<tea_protocol::RunId, RunRecoveryState>,
29 latest_checkpoint: Option<TurnCheckpoint>,
30 latest_compaction: Option<crate::SessionCompaction>,
31}
32
33impl BranchProjection {
34 fn capture(state: &MaterializedSessionState) -> Self {
35 Self {
36 configuration: state.configuration.clone(),
37 messages: state.messages.clone(),
38 pending_approvals: state.pending_approvals.clone(),
39 tool_calls: state.tool_calls.clone(),
40 run_recovery: state.run_recovery.clone(),
41 latest_checkpoint: state.latest_checkpoint.clone(),
42 latest_compaction: state.latest_compaction.clone(),
43 }
44 }
45
46 fn install(&self, state: &mut MaterializedSessionState) {
47 state.configuration.clone_from(&self.configuration);
48 state.messages.clone_from(&self.messages);
49 state.pending_approvals.clone_from(&self.pending_approvals);
50 state.tool_calls.clone_from(&self.tool_calls);
51 state.run_recovery.clone_from(&self.run_recovery);
52 state.latest_checkpoint.clone_from(&self.latest_checkpoint);
53 state.latest_compaction.clone_from(&self.latest_compaction);
54 }
55}
56
57#[derive(Debug, Clone, PartialEq)]
58struct BranchSnapshot {
59 projection: BranchProjection,
60 history: BTreeSet<RecordId>,
61 declared_tool_calls: BTreeSet<ToolCallId>,
62}
63
64impl BranchSnapshot {
65 fn safe_to_fork(&self) -> bool {
66 self.projection.pending_approvals.is_empty()
67 && self.declared_tool_calls.iter().all(|tool_call_id| {
68 self.projection
69 .tool_calls
70 .get(tool_call_id)
71 .is_some_and(|tool| {
72 matches!(tool.execution(), ToolExecutionState::Finished { .. })
73 && tool.result_message_id().is_some()
74 })
75 })
76 }
77}
78
79#[derive(Debug, Default)]
80struct HistoricalSnapshots {
81 required: BTreeSet<RecordId>,
82 captured: BTreeMap<RecordId, BranchSnapshot>,
83}
84
85impl HistoricalSnapshots {
86 fn for_prefix(records: &[RecordEnvelope], target: RecordId) -> Option<(Self, usize)> {
87 let target_index = records
88 .iter()
89 .position(|record| record.record_id() == target)?;
90 let mut required = BTreeSet::from([target]);
91 for record in &records[..=target_index] {
92 if let SessionRecord::BranchCreated { from_record_id, .. } = record.record() {
93 required.insert(*from_record_id);
94 }
95 }
96 Some((
97 Self {
98 required,
99 captured: BTreeMap::new(),
100 },
101 target_index,
102 ))
103 }
104
105 fn capture(&mut self, record_id: RecordId, snapshot: &BranchSnapshot) {
106 if self.required.contains(&record_id) {
107 self.captured.insert(record_id, snapshot.clone());
108 }
109 }
110
111 fn get(&self, record_id: RecordId) -> Option<&BranchSnapshot> {
112 self.captured.get(&record_id)
113 }
114}
115
116#[derive(Debug, Clone, Default)]
118pub struct SessionReducer {
119 state: Option<MaterializedSessionState>,
120 records: Vec<RecordEnvelope>,
121 record_ids: BTreeSet<RecordId>,
122 message_ids: BTreeSet<MessageId>,
123 declared_tool_calls: BTreeMap<ToolCallId, DeclaredToolCall>,
124 requested_tool_call_ids: BTreeSet<ToolCallId>,
125 approval_ids: BTreeSet<ApprovalId>,
126 branch_heads: BTreeMap<BranchId, BranchSnapshot>,
127}
128
129impl SessionReducer {
130 #[must_use]
132 pub const fn new() -> Self {
133 Self {
134 state: None,
135 records: Vec::new(),
136 record_ids: BTreeSet::new(),
137 message_ids: BTreeSet::new(),
138 declared_tool_calls: BTreeMap::new(),
139 requested_tool_call_ids: BTreeSet::new(),
140 approval_ids: BTreeSet::new(),
141 branch_heads: BTreeMap::new(),
142 }
143 }
144
145 pub fn replay(
152 records: impl IntoIterator<Item = RecordEnvelope>,
153 ) -> Result<MaterializedSessionState, SessionReplayError> {
154 let reducer = Self::replay_reducer(records)?;
155 reducer.state.ok_or(SessionReplayError::EmptyLog)
156 }
157
158 pub fn replay_reducer(
164 records: impl IntoIterator<Item = RecordEnvelope>,
165 ) -> Result<Self, SessionReplayError> {
166 let mut reducer = Self::new();
167 for record in records {
168 reducer.apply(&record)?;
169 }
170 if reducer.state.is_none() {
171 return Err(SessionReplayError::EmptyLog);
172 }
173 Ok(reducer)
174 }
175
176 pub fn apply(&mut self, envelope: &RecordEnvelope) -> Result<(), SessionReplayError> {
183 if let Err(error) = self.apply_inner(envelope) {
184 self.restore_durable_state();
185 return Err(error);
186 }
187 self.records.push(envelope.clone());
188 Ok(())
189 }
190
191 #[must_use]
193 pub const fn state(&self) -> Option<&MaterializedSessionState> {
194 self.state.as_ref()
195 }
196
197 fn restore_durable_state(&mut self) {
198 let records = std::mem::take(&mut self.records);
199 let mut restored = Self::new();
200 for record in records {
201 restored
202 .apply_inner(&record)
203 .expect("previously accepted records remain replayable");
204 restored.records.push(record);
205 }
206 *self = restored;
207 }
208
209 fn apply_inner(&mut self, envelope: &RecordEnvelope) -> Result<(), SessionReplayError> {
210 self.apply_inner_with_history(envelope, None)
211 }
212
213 fn apply_inner_with_history(
214 &mut self,
215 envelope: &RecordEnvelope,
216 historical: Option<&mut HistoricalSnapshots>,
217 ) -> Result<(), SessionReplayError> {
218 if self.record_ids.contains(&envelope.record_id()) {
219 return Err(SessionReplayError::DuplicateRecord {
220 record_id: envelope.record_id(),
221 });
222 }
223 if self.state.is_none() {
224 return self.apply_creation(envelope, historical);
225 }
226
227 self.validate_envelope(envelope)?;
228 if matches!(envelope.record(), SessionRecord::SessionCreated { .. }) {
229 return Err(SessionReplayError::InvalidCreation);
230 }
231 self.validate_branch_scope(envelope)?;
232 match envelope.record() {
233 SessionRecord::BranchCreated {
234 source_branch_id,
235 branch_id,
236 from_record_id,
237 } => {
238 self.apply_branch_created(
239 envelope,
240 *source_branch_id,
241 *branch_id,
242 *from_record_id,
243 historical,
244 )?;
245 }
246 SessionRecord::ActiveBranchChanged { branch_id } => {
247 self.apply_active_branch_changed(envelope, *branch_id, historical)?;
248 }
249 _ => {
250 self.apply_record(envelope)?;
251 self.capture_active_branch(envelope.record_id(), historical)?;
252 }
253 }
254
255 let state = self
256 .state
257 .as_mut()
258 .ok_or(SessionReplayError::InvalidTransition {
259 transition: "missing_materialized_state",
260 })?;
261 state.tail_sequence = envelope.sequence();
262 state.tail_record_id = envelope.record_id();
263 self.record_ids.insert(envelope.record_id());
264 Ok(())
265 }
266
267 fn apply_creation(
268 &mut self,
269 envelope: &RecordEnvelope,
270 historical: Option<&mut HistoricalSnapshots>,
271 ) -> Result<(), SessionReplayError> {
272 if envelope.sequence() != SessionSequence::new(0) {
273 return Err(SessionReplayError::SequenceMismatch {
274 expected: SessionSequence::new(0),
275 actual: envelope.sequence(),
276 });
277 }
278 let SessionRecord::SessionCreated {
279 profile_id,
280 metadata,
281 } = envelope.record()
282 else {
283 return Err(SessionReplayError::InvalidCreation);
284 };
285 self.state = Some(new_state(
286 envelope.record_id(),
287 envelope.session_id(),
288 envelope.sequence(),
289 profile_id.clone(),
290 metadata.clone(),
291 envelope.branch_id(),
292 ));
293 if let Some(branch_id) = envelope.branch_id() {
294 let state = self
295 .state
296 .as_ref()
297 .ok_or(SessionReplayError::InvalidCreation)?;
298 let snapshot = BranchSnapshot {
299 projection: BranchProjection::capture(state),
300 history: BTreeSet::from([envelope.record_id()]),
301 declared_tool_calls: BTreeSet::new(),
302 };
303 if let Some(historical) = historical {
304 historical.capture(envelope.record_id(), &snapshot);
305 }
306 self.branch_heads.insert(branch_id, snapshot);
307 }
308 self.record_ids.insert(envelope.record_id());
309 Ok(())
310 }
311
312 fn validate_envelope(&self, envelope: &RecordEnvelope) -> Result<(), SessionReplayError> {
313 let state = self
314 .state
315 .as_ref()
316 .ok_or(SessionReplayError::InvalidTransition {
317 transition: "missing_materialized_state",
318 })?;
319 if envelope.session_id() != state.session_id {
320 return Err(SessionReplayError::SessionMismatch {
321 expected: state.session_id,
322 actual: envelope.session_id(),
323 });
324 }
325 let expected = state
326 .tail_sequence
327 .checked_next()
328 .ok_or(SessionReplayError::SequenceOverflow)?;
329 if envelope.sequence() != expected {
330 return Err(SessionReplayError::SequenceMismatch {
331 expected,
332 actual: envelope.sequence(),
333 });
334 }
335 Ok(())
336 }
337
338 fn validate_branch_scope(&self, envelope: &RecordEnvelope) -> Result<(), SessionReplayError> {
339 let state = self
340 .state
341 .as_ref()
342 .ok_or(SessionReplayError::InvalidTransition {
343 transition: "missing_materialized_state",
344 })?;
345 match envelope.record() {
346 SessionRecord::BranchCreated { .. } | SessionRecord::ActiveBranchChanged { .. } => {
347 Ok(())
348 }
349 _ if envelope.branch_id() == state.active_branch_id => Ok(()),
350 _ => Err(SessionReplayError::InvalidReference {
351 reference: "inactive_branch",
352 }),
353 }
354 }
355
356 fn capture_active_branch(
357 &mut self,
358 record_id: RecordId,
359 historical: Option<&mut HistoricalSnapshots>,
360 ) -> Result<(), SessionReplayError> {
361 let state = self
362 .state
363 .as_mut()
364 .ok_or(SessionReplayError::InvalidTransition {
365 transition: "missing_materialized_state",
366 })?;
367 let Some(branch_id) = state.active_branch_id else {
368 return Ok(());
369 };
370 let head =
371 self.branch_heads
372 .get_mut(&branch_id)
373 .ok_or(SessionReplayError::InvalidReference {
374 reference: "active_branch",
375 })?;
376 head.history.insert(record_id);
377 head.declared_tool_calls = state
378 .messages
379 .iter()
380 .flat_map(declared_tool_calls)
381 .map(|(tool_call_id, _, _)| tool_call_id)
382 .collect();
383 head.projection = BranchProjection::capture(state);
384 state
385 .branches
386 .get_mut(&branch_id)
387 .ok_or(SessionReplayError::InvalidReference {
388 reference: "active_branch_summary",
389 })?
390 .set_leaf(record_id);
391 if let Some(historical) = historical {
392 historical.capture(record_id, head);
393 }
394 Ok(())
395 }
396
397 fn apply_branch_created(
398 &mut self,
399 envelope: &RecordEnvelope,
400 source_branch_id: BranchId,
401 branch_id: BranchId,
402 from_record_id: RecordId,
403 historical: Option<&mut HistoricalSnapshots>,
404 ) -> Result<(), SessionReplayError> {
405 if self.branch_heads.contains_key(&branch_id) {
406 return Err(SessionReplayError::DuplicateEntity { entity: "branch" });
407 }
408 let source = self.branch_heads.get(&source_branch_id).ok_or(
409 SessionReplayError::InvalidReference {
410 reference: "source_branch",
411 },
412 )?;
413 if !source.history.contains(&from_record_id) {
414 return Err(SessionReplayError::InvalidReference {
415 reference: "fork_point_outside_source_branch",
416 });
417 }
418 let source_snapshot = match historical.as_deref() {
419 Some(historical) => historical.get(from_record_id).cloned(),
420 None => self.replay_branch_snapshot(source_branch_id, from_record_id)?,
421 }
422 .ok_or(SessionReplayError::InvalidReference {
423 reference: "fork_record_snapshot",
424 })?;
425 if !source_snapshot.safe_to_fork() {
426 return Err(SessionReplayError::InvalidTransition {
427 transition: "unsafe_fork_point",
428 });
429 }
430 let mut new_snapshot = source_snapshot;
431 new_snapshot.history.insert(envelope.record_id());
432 if let Some(historical) = historical {
433 historical.capture(envelope.record_id(), &new_snapshot);
434 }
435 self.branch_heads.insert(branch_id, new_snapshot);
436 self.state
437 .as_mut()
438 .ok_or(SessionReplayError::InvalidTransition {
439 transition: "missing_materialized_state",
440 })?
441 .branches
442 .insert(
443 branch_id,
444 crate::BranchSummary::new(
445 branch_id,
446 Some(source_branch_id),
447 from_record_id,
448 envelope.record_id(),
449 ),
450 );
451 Ok(())
452 }
453
454 fn apply_active_branch_changed(
455 &mut self,
456 envelope: &RecordEnvelope,
457 branch_id: BranchId,
458 historical: Option<&mut HistoricalSnapshots>,
459 ) -> Result<(), SessionReplayError> {
460 let head =
461 self.branch_heads
462 .get_mut(&branch_id)
463 .ok_or(SessionReplayError::InvalidReference {
464 reference: "active_branch_change",
465 })?;
466 let state = self
467 .state
468 .as_mut()
469 .ok_or(SessionReplayError::InvalidTransition {
470 transition: "missing_materialized_state",
471 })?;
472 head.projection.install(state);
473 head.history.insert(envelope.record_id());
474 state.active_branch_id = Some(branch_id);
475 state
476 .branches
477 .get_mut(&branch_id)
478 .ok_or(SessionReplayError::InvalidReference {
479 reference: "active_branch_summary",
480 })?
481 .set_leaf(envelope.record_id());
482 if let Some(historical) = historical {
483 historical.capture(envelope.record_id(), head);
484 }
485 Ok(())
486 }
487
488 fn replay_branch_snapshot(
489 &self,
490 source_branch_id: BranchId,
491 from_record_id: RecordId,
492 ) -> Result<Option<BranchSnapshot>, SessionReplayError> {
493 let Some((mut historical, target_index)) =
494 HistoricalSnapshots::for_prefix(&self.records, from_record_id)
495 else {
496 return Ok(None);
497 };
498 let mut replay = Self::new();
499 for record in &self.records[..=target_index] {
500 replay.apply_inner_with_history(record, Some(&mut historical))?;
501 replay.records.push(record.clone());
502 }
503 Ok(historical
504 .get(from_record_id)
505 .filter(|snapshot| {
506 replay
507 .branch_heads
508 .get(&source_branch_id)
509 .is_some_and(|source| source.history.contains(&from_record_id))
510 && snapshot.history.contains(&from_record_id)
511 })
512 .cloned())
513 }
514
515 #[allow(clippy::too_many_lines)]
516 fn apply_record(&mut self, envelope: &RecordEnvelope) -> Result<(), SessionReplayError> {
517 let state = self
518 .state
519 .as_mut()
520 .ok_or(SessionReplayError::InvalidTransition {
521 transition: "missing_materialized_state",
522 })?;
523 match envelope.record() {
524 SessionRecord::SessionCreated { .. } => Err(SessionReplayError::InvalidCreation),
525 SessionRecord::MessageCommitted { message } => {
526 let id = message_id(message);
527 if !self.message_ids.insert(id) {
528 return Err(SessionReplayError::DuplicateEntity { entity: "message" });
529 }
530 for (tool_call_id, tool_name, arguments) in declared_tool_calls(message) {
531 if self
532 .declared_tool_calls
533 .insert(
534 tool_call_id,
535 DeclaredToolCall {
536 tool_name: tool_name.to_owned(),
537 arguments: arguments.clone(),
538 },
539 )
540 .is_some()
541 || state.tool_calls.contains_key(&tool_call_id)
542 {
543 return Err(SessionReplayError::DuplicateEntity {
544 entity: "tool_call",
545 });
546 }
547 }
548 if let CanonicalMessage::ToolResult {
549 tool_call_id,
550 tool_name,
551 content,
552 is_error,
553 error,
554 ..
555 } = message
556 {
557 let tool = state.tool_calls.get_mut(tool_call_id).ok_or(
558 SessionReplayError::InvalidReference {
559 reference: "tool_result_tool_call",
560 },
561 )?;
562 if tool.result_message_id().is_some() {
563 return Err(SessionReplayError::InvalidTransition {
564 transition: "duplicate_tool_result_message",
565 });
566 }
567 match tool.execution() {
568 ToolExecutionState::Finished {
569 is_error: terminal_error,
570 content: terminal_content,
571 error: terminal_failure,
572 ..
573 } if terminal_error == is_error
574 && terminal_content == content
575 && terminal_failure == error
576 && tool.tool_name() == tool_name => {}
577 ToolExecutionState::NotStarted
578 | ToolExecutionState::Started { .. }
579 | ToolExecutionState::Interrupted { .. }
580 | ToolExecutionState::Finished { .. } => {
581 return Err(SessionReplayError::InvalidTransition {
582 transition: "tool_result_before_matching_terminal",
583 });
584 }
585 }
586 commit_tool_result(tool, id);
587 }
588 state.messages.push(message.clone());
589 Ok(())
590 }
591 SessionRecord::ConfigurationChanged {
592 model,
593 profile_id,
594 reasoning_effort,
595 } => {
596 if let Some(model) = model {
597 set_model(&mut state.configuration, model.clone());
598 }
599 if let Some(profile_id) = profile_id {
600 set_profile(&mut state.configuration, profile_id.clone());
601 }
602 if let Some(reasoning_effort) = reasoning_effort {
603 set_reasoning_effort(&mut state.configuration, *reasoning_effort);
604 }
605 Ok(())
606 }
607 SessionRecord::ToolCallRequested {
608 tool_call_id,
609 tool_name,
610 arguments,
611 } => {
612 let visible_on_branch = state.active_branch_id.is_none_or(|branch_id| {
613 self.branch_heads
614 .get(&branch_id)
615 .is_some_and(|head| head.declared_tool_calls.contains(tool_call_id))
616 });
617 let declared = self
618 .declared_tool_calls
619 .get(tool_call_id)
620 .filter(|_| visible_on_branch)
621 .ok_or(SessionReplayError::InvalidReference {
622 reference: "undeclared_tool_call",
623 })?;
624 if declared.tool_name != *tool_name || declared.arguments != *arguments {
625 return Err(SessionReplayError::InvalidReference {
626 reference: "tool_call_declaration_mismatch",
627 });
628 }
629 if !self.requested_tool_call_ids.insert(*tool_call_id)
630 || state.tool_calls.contains_key(tool_call_id)
631 {
632 return Err(SessionReplayError::DuplicateEntity {
633 entity: "tool_call_request",
634 });
635 }
636 state.tool_calls.insert(
637 *tool_call_id,
638 new_tool_call(*tool_call_id, tool_name.clone(), arguments.clone()),
639 );
640 Ok(())
641 }
642 SessionRecord::PolicyDecisionRecorded {
643 tool_call_id,
644 decision,
645 } => {
646 let tool = state.tool_calls.get_mut(tool_call_id).ok_or(
647 SessionReplayError::InvalidReference {
648 reference: "policy_tool_call",
649 },
650 )?;
651 if tool.policy_decision().is_some() {
652 return Err(SessionReplayError::InvalidTransition {
653 transition: "duplicate_policy_decision",
654 });
655 }
656 set_policy(tool, *decision);
657 Ok(())
658 }
659 SessionRecord::ApprovalRequested {
660 approval_id,
661 tool_call_id,
662 expires_at,
663 } => {
664 if !self.approval_ids.insert(*approval_id) {
665 return Err(SessionReplayError::DuplicateEntity { entity: "approval" });
666 }
667 let tool = state.tool_calls.get_mut(tool_call_id).ok_or(
668 SessionReplayError::InvalidReference {
669 reference: "approval_tool_call",
670 },
671 )?;
672 if tool.policy_decision() != Some(tea_protocol::PolicyDecision::RequireApproval)
673 || tool.approval_id().is_some()
674 || *expires_at <= envelope.timestamp()
675 {
676 return Err(SessionReplayError::InvalidTransition {
677 transition: "approval_request",
678 });
679 }
680 set_approval(tool, *approval_id);
681 state.pending_approvals.insert(
682 *approval_id,
683 new_pending_approval(
684 *approval_id,
685 *tool_call_id,
686 *expires_at,
687 envelope.timestamp(),
688 ),
689 );
690 Ok(())
691 }
692 SessionRecord::ApprovalResolved {
693 approval_id,
694 decision,
695 } => {
696 let pending = state.pending_approvals.remove(approval_id).ok_or(
697 SessionReplayError::InvalidReference {
698 reference: "pending_approval",
699 },
700 )?;
701 if envelope.timestamp() < pending.requested_at()
702 || envelope.timestamp() >= pending.expires_at()
703 {
704 return Err(SessionReplayError::InvalidTransition {
705 transition: "expired_approval_resolution",
706 });
707 }
708 let tool = state.tool_calls.get_mut(&pending.tool_call_id()).ok_or(
709 SessionReplayError::InvalidReference {
710 reference: "approval_resolution_tool_call",
711 },
712 )?;
713 resolve_approval(tool, *decision);
714 Ok(())
715 }
716 SessionRecord::ToolExecutionStarted {
717 tool_call_id,
718 execution_target,
719 idempotency,
720 } => {
721 let tool = state.tool_calls.get_mut(tool_call_id).ok_or(
722 SessionReplayError::InvalidReference {
723 reference: "execution_tool_call",
724 },
725 )?;
726 let authorized = match tool.policy_decision() {
727 Some(tea_protocol::PolicyDecision::Allow) => true,
728 Some(tea_protocol::PolicyDecision::RequireApproval) => matches!(
729 tool.approval_decision(),
730 Some(ApprovalDecision::AllowOnce | ApprovalDecision::AllowSession)
731 ),
732 Some(tea_protocol::PolicyDecision::Deny) | None => false,
733 };
734 if !authorized || !matches!(tool.execution(), ToolExecutionState::NotStarted) {
735 return Err(SessionReplayError::InvalidTransition {
736 transition: "tool_execution_start",
737 });
738 }
739 start_tool(tool, *execution_target, *idempotency);
740 Ok(())
741 }
742 SessionRecord::ToolExecutionFinished {
743 tool_call_id,
744 is_error,
745 content,
746 error,
747 presentation,
748 } => {
749 let tool = state.tool_calls.get_mut(tool_call_id).ok_or(
750 SessionReplayError::InvalidReference {
751 reference: "tool_terminal_tool_call",
752 },
753 )?;
754 let denied_without_execution =
755 matches!(tool.execution(), ToolExecutionState::NotStarted)
756 && *is_error
757 && (tool.policy_decision() == Some(tea_protocol::PolicyDecision::Deny)
758 || tool.approval_decision() == Some(ApprovalDecision::Deny));
759 if !matches!(tool.execution(), ToolExecutionState::Started { .. })
760 && !denied_without_execution
761 {
762 return Err(SessionReplayError::InvalidTransition {
763 transition: "tool_execution_finish",
764 });
765 }
766 finish_tool(
767 tool,
768 *is_error,
769 content.clone(),
770 error.clone(),
771 presentation.clone(),
772 );
773 Ok(())
774 }
775 SessionRecord::ToolExecutionInterrupted {
776 tool_call_id,
777 reason,
778 } => {
779 let tool = state.tool_calls.get_mut(tool_call_id).ok_or(
780 SessionReplayError::InvalidReference {
781 reference: "tool_interruption_tool_call",
782 },
783 )?;
784 if !matches!(tool.execution(), ToolExecutionState::Started { .. }) {
785 return Err(SessionReplayError::InvalidTransition {
786 transition: "tool_execution_interruption",
787 });
788 }
789 interrupt_tool(tool, reason.clone());
790 Ok(())
791 }
792 SessionRecord::RunInterrupted {
793 run_id,
794 turn_id,
795 reason,
796 } => {
797 if state.run_recovery.contains_key(run_id) {
798 return Err(SessionReplayError::InvalidTransition {
799 transition: "duplicate_run_terminal",
800 });
801 }
802 state.run_recovery.insert(
803 *run_id,
804 RunRecoveryState::Interrupted {
805 turn_id: *turn_id,
806 reason: reason.clone(),
807 },
808 );
809 Ok(())
810 }
811 SessionRecord::RunCancelled { run_id } => {
812 if state
813 .run_recovery
814 .insert(*run_id, RunRecoveryState::Cancelled)
815 .is_some()
816 {
817 return Err(SessionReplayError::InvalidTransition {
818 transition: "duplicate_run_terminal",
819 });
820 }
821 Ok(())
822 }
823 SessionRecord::BranchCreated { .. } | SessionRecord::ActiveBranchChanged { .. } => {
824 Err(SessionReplayError::InvalidTransition {
825 transition: "branching_not_applied",
826 })
827 }
828 SessionRecord::SessionCompacted {
829 summary,
830 compacted_through_record_id,
831 } => {
832 if !matches!(summary, CanonicalMessage::Assistant { .. }) {
833 return Err(SessionReplayError::InvalidTransition {
834 transition: "compaction_summary_role",
835 });
836 }
837 let source_is_active = state.active_branch_id.map_or_else(
838 || self.record_ids.contains(compacted_through_record_id),
839 |branch_id| {
840 self.branch_heads
841 .get(&branch_id)
842 .is_some_and(|head| head.history.contains(compacted_through_record_id))
843 },
844 );
845 if !source_is_active {
846 return Err(SessionReplayError::InvalidReference {
847 reference: "compaction_source",
848 });
849 }
850 let id = message_id(summary);
851 if !self.message_ids.insert(id) {
852 return Err(SessionReplayError::DuplicateEntity { entity: "message" });
853 }
854 set_compaction(state, summary.clone(), *compacted_through_record_id);
855 Ok(())
856 }
857 SessionRecord::TurnCheckpointed {
858 run_id,
859 turn_id,
860 next_action,
861 } => {
862 state.latest_checkpoint = Some(TurnCheckpoint::new(
863 *run_id,
864 *turn_id,
865 envelope.record_id(),
866 envelope.sequence(),
867 *next_action,
868 ));
869 Ok(())
870 }
871 }
872 }
873}