1use std::collections::BTreeMap;
2
3use serde_json::Value;
4use tea_protocol::{
5 ApprovalDecision, ApprovalId, BranchId, CanonicalMessage, ContentBlock, ExecutionTarget,
6 MessageId, ModelRef, PolicyDecision, ProfileId, ProtocolMetadata, ProtocolTimestamp,
7 ReasoningEffort, RecordId, RunId, SessionId, SessionSequence, ToolCallId, ToolFailure,
8 ToolIdempotency, ToolPresentation, TurnId,
9};
10
11#[derive(Debug, Clone, PartialEq, Eq)]
13pub struct SessionConfiguration {
14 model: Option<ModelRef>,
15 profile_id: ProfileId,
16 reasoning_effort: Option<ReasoningEffort>,
17}
18
19impl SessionConfiguration {
20 #[must_use]
22 pub const fn model_ref(&self) -> Option<&ModelRef> {
23 self.model.as_ref()
24 }
25
26 #[must_use]
28 pub const fn model_id(&self) -> Option<&tea_protocol::ModelId> {
29 match &self.model {
30 Some(model) => Some(model.model_id()),
31 None => None,
32 }
33 }
34
35 #[must_use]
37 pub const fn provider_id(&self) -> Option<&tea_protocol::ProviderId> {
38 match &self.model {
39 Some(model) => Some(model.provider_id()),
40 None => None,
41 }
42 }
43
44 #[must_use]
46 pub const fn profile_id(&self) -> &ProfileId {
47 &self.profile_id
48 }
49
50 #[must_use]
52 pub const fn reasoning_effort(&self) -> Option<ReasoningEffort> {
53 self.reasoning_effort
54 }
55}
56
57#[derive(Debug, Clone, PartialEq, Eq)]
59pub struct PendingApproval {
60 approval_id: ApprovalId,
61 tool_call_id: ToolCallId,
62 expires_at: ProtocolTimestamp,
63 requested_at: ProtocolTimestamp,
64}
65
66impl PendingApproval {
67 #[must_use]
69 pub const fn approval_id(&self) -> ApprovalId {
70 self.approval_id
71 }
72
73 #[must_use]
75 pub const fn tool_call_id(&self) -> ToolCallId {
76 self.tool_call_id
77 }
78
79 #[must_use]
81 pub const fn expires_at(&self) -> ProtocolTimestamp {
82 self.expires_at
83 }
84
85 #[must_use]
87 pub const fn requested_at(&self) -> ProtocolTimestamp {
88 self.requested_at
89 }
90}
91
92#[derive(Debug, Clone, PartialEq)]
94pub enum ToolExecutionState {
95 NotStarted,
97 Started {
99 execution_target: ExecutionTarget,
101 idempotency: ToolIdempotency,
103 },
104 Finished {
106 is_error: bool,
108 content: Vec<ContentBlock>,
110 error: Option<ToolFailure>,
112 presentation: Option<ToolPresentation>,
114 },
115 Interrupted {
117 reason: String,
119 execution_target: ExecutionTarget,
121 idempotency: ToolIdempotency,
123 },
124}
125
126#[derive(Debug, Clone, PartialEq)]
128pub struct ToolCallState {
129 tool_call_id: ToolCallId,
130 tool_name: String,
131 arguments: Value,
132 policy_decision: Option<PolicyDecision>,
133 approval_id: Option<ApprovalId>,
134 approval_decision: Option<ApprovalDecision>,
135 execution: ToolExecutionState,
136 result_message_id: Option<MessageId>,
137}
138
139impl ToolCallState {
140 #[must_use]
142 pub const fn tool_call_id(&self) -> ToolCallId {
143 self.tool_call_id
144 }
145
146 #[must_use]
148 pub fn tool_name(&self) -> &str {
149 &self.tool_name
150 }
151
152 #[must_use]
154 pub const fn arguments(&self) -> &Value {
155 &self.arguments
156 }
157
158 #[must_use]
160 pub const fn policy_decision(&self) -> Option<PolicyDecision> {
161 self.policy_decision
162 }
163
164 #[must_use]
166 pub const fn approval_id(&self) -> Option<ApprovalId> {
167 self.approval_id
168 }
169
170 #[must_use]
172 pub const fn approval_decision(&self) -> Option<ApprovalDecision> {
173 self.approval_decision
174 }
175
176 #[must_use]
178 pub const fn execution(&self) -> &ToolExecutionState {
179 &self.execution
180 }
181
182 #[must_use]
184 pub const fn result_message_id(&self) -> Option<MessageId> {
185 self.result_message_id
186 }
187}
188
189#[derive(Debug, Clone, PartialEq, Eq)]
191pub enum RunRecoveryState {
192 Interrupted {
194 turn_id: TurnId,
196 reason: String,
198 },
199 Cancelled,
201}
202
203#[derive(Debug, Clone, PartialEq, Eq)]
205pub struct TurnCheckpoint {
206 run_id: RunId,
207 turn_id: TurnId,
208 record_id: RecordId,
209 sequence: SessionSequence,
210 next_action: tea_protocol::NextTurnAction,
211}
212
213impl TurnCheckpoint {
214 pub(crate) const fn new(
215 run_id: RunId,
216 turn_id: TurnId,
217 record_id: RecordId,
218 sequence: SessionSequence,
219 next_action: tea_protocol::NextTurnAction,
220 ) -> Self {
221 Self {
222 run_id,
223 turn_id,
224 record_id,
225 sequence,
226 next_action,
227 }
228 }
229
230 #[must_use]
232 pub const fn run_id(&self) -> RunId {
233 self.run_id
234 }
235
236 #[must_use]
238 pub const fn turn_id(&self) -> TurnId {
239 self.turn_id
240 }
241
242 #[must_use]
244 pub const fn record_id(&self) -> RecordId {
245 self.record_id
246 }
247
248 #[must_use]
250 pub const fn sequence(&self) -> SessionSequence {
251 self.sequence
252 }
253
254 #[must_use]
256 pub const fn next_action(&self) -> tea_protocol::NextTurnAction {
257 self.next_action
258 }
259}
260
261#[derive(Debug, Clone, PartialEq)]
263pub struct SessionCompaction {
264 summary: CanonicalMessage,
265 compacted_through_record_id: RecordId,
266}
267
268impl SessionCompaction {
269 #[must_use]
271 pub const fn summary(&self) -> &CanonicalMessage {
272 &self.summary
273 }
274
275 #[must_use]
277 pub const fn compacted_through_record_id(&self) -> RecordId {
278 self.compacted_through_record_id
279 }
280}
281
282#[derive(Debug, Clone, PartialEq, Eq)]
284#[allow(clippy::struct_field_names)] pub struct BranchSummary {
286 branch_id: BranchId,
287 source_branch_id: Option<BranchId>,
288 from_record_id: RecordId,
289 leaf_record_id: RecordId,
290}
291
292impl BranchSummary {
293 pub(crate) const fn new(
294 branch_id: BranchId,
295 source_branch_id: Option<BranchId>,
296 from_record_id: RecordId,
297 leaf_record_id: RecordId,
298 ) -> Self {
299 Self {
300 branch_id,
301 source_branch_id,
302 from_record_id,
303 leaf_record_id,
304 }
305 }
306
307 pub(crate) fn set_leaf(&mut self, record_id: RecordId) {
308 self.leaf_record_id = record_id;
309 }
310
311 #[must_use]
313 pub const fn branch_id(&self) -> BranchId {
314 self.branch_id
315 }
316
317 #[must_use]
319 pub const fn source_branch_id(&self) -> Option<BranchId> {
320 self.source_branch_id
321 }
322
323 #[must_use]
325 pub const fn from_record_id(&self) -> RecordId {
326 self.from_record_id
327 }
328
329 #[must_use]
331 pub const fn leaf_record_id(&self) -> RecordId {
332 self.leaf_record_id
333 }
334}
335
336#[derive(Debug, Clone, PartialEq)]
338pub struct MaterializedSessionState {
339 pub(crate) session_id: SessionId,
340 pub(crate) tail_sequence: SessionSequence,
341 pub(crate) tail_record_id: RecordId,
342 pub(crate) metadata: ProtocolMetadata,
343 pub(crate) configuration: SessionConfiguration,
344 pub(crate) messages: Vec<CanonicalMessage>,
345 pub(crate) pending_approvals: BTreeMap<ApprovalId, PendingApproval>,
346 pub(crate) tool_calls: BTreeMap<ToolCallId, ToolCallState>,
347 pub(crate) active_branch_id: Option<BranchId>,
348 pub(crate) branches: BTreeMap<BranchId, BranchSummary>,
349 pub(crate) run_recovery: BTreeMap<RunId, RunRecoveryState>,
350 pub(crate) latest_checkpoint: Option<TurnCheckpoint>,
351 pub(crate) latest_compaction: Option<SessionCompaction>,
352}
353
354impl MaterializedSessionState {
355 #[must_use]
357 pub const fn session_id(&self) -> SessionId {
358 self.session_id
359 }
360
361 #[must_use]
363 pub const fn tail_sequence(&self) -> SessionSequence {
364 self.tail_sequence
365 }
366
367 #[must_use]
369 pub const fn tail_record_id(&self) -> RecordId {
370 self.tail_record_id
371 }
372
373 #[must_use]
375 pub const fn metadata(&self) -> &ProtocolMetadata {
376 &self.metadata
377 }
378
379 #[must_use]
381 pub const fn configuration(&self) -> &SessionConfiguration {
382 &self.configuration
383 }
384
385 #[must_use]
387 pub fn messages(&self) -> &[CanonicalMessage] {
388 &self.messages
389 }
390
391 #[must_use]
393 pub const fn pending_approvals(&self) -> &BTreeMap<ApprovalId, PendingApproval> {
394 &self.pending_approvals
395 }
396
397 #[must_use]
399 pub const fn tool_calls(&self) -> &BTreeMap<ToolCallId, ToolCallState> {
400 &self.tool_calls
401 }
402
403 #[must_use]
405 pub const fn active_branch_id(&self) -> Option<BranchId> {
406 self.active_branch_id
407 }
408
409 #[must_use]
411 pub const fn branches(&self) -> &BTreeMap<BranchId, BranchSummary> {
412 &self.branches
413 }
414
415 #[must_use]
417 pub const fn run_recovery(&self) -> &BTreeMap<RunId, RunRecoveryState> {
418 &self.run_recovery
419 }
420
421 #[must_use]
423 pub const fn latest_checkpoint(&self) -> Option<&TurnCheckpoint> {
424 self.latest_checkpoint.as_ref()
425 }
426
427 #[must_use]
429 pub const fn latest_compaction(&self) -> Option<&SessionCompaction> {
430 self.latest_compaction.as_ref()
431 }
432}
433
434pub(crate) fn message_id(message: &CanonicalMessage) -> MessageId {
435 match message {
436 CanonicalMessage::User { id, .. }
437 | CanonicalMessage::Assistant { id, .. }
438 | CanonicalMessage::ToolResult { id, .. } => *id,
439 }
440}
441
442pub(crate) fn declared_tool_calls(
443 message: &CanonicalMessage,
444) -> impl Iterator<Item = (ToolCallId, &str, &Value)> {
445 let content = match message {
446 CanonicalMessage::Assistant { content, .. } => Some(content.as_slice()),
447 CanonicalMessage::User { .. } | CanonicalMessage::ToolResult { .. } => None,
448 };
449 content
450 .into_iter()
451 .flatten()
452 .filter_map(|block| match block {
453 ContentBlock::ToolCall {
454 tool_call_id,
455 tool_name,
456 arguments,
457 ..
458 } => Some((*tool_call_id, tool_name.as_str(), arguments)),
459 ContentBlock::Text { .. }
460 | ContentBlock::Thinking { .. }
461 | ContentBlock::Image { .. }
462 | ContentBlock::HostedTool { .. }
463 | ContentBlock::Citation { .. } => None,
464 })
465}
466
467pub(crate) fn new_state(
468 record_id: RecordId,
469 session_id: SessionId,
470 sequence: SessionSequence,
471 profile_id: ProfileId,
472 metadata: ProtocolMetadata,
473 root_branch_id: Option<BranchId>,
474) -> MaterializedSessionState {
475 let branches = root_branch_id.map_or_else(BTreeMap::new, |branch_id| {
476 BTreeMap::from([(
477 branch_id,
478 BranchSummary {
479 branch_id,
480 source_branch_id: None,
481 from_record_id: record_id,
482 leaf_record_id: record_id,
483 },
484 )])
485 });
486 MaterializedSessionState {
487 session_id,
488 tail_sequence: sequence,
489 tail_record_id: record_id,
490 metadata,
491 configuration: SessionConfiguration {
492 model: None,
493 profile_id,
494 reasoning_effort: None,
495 },
496 messages: Vec::new(),
497 pending_approvals: BTreeMap::new(),
498 tool_calls: BTreeMap::new(),
499 active_branch_id: root_branch_id,
500 branches,
501 run_recovery: BTreeMap::new(),
502 latest_checkpoint: None,
503 latest_compaction: None,
504 }
505}
506
507pub(crate) fn new_pending_approval(
508 approval_id: ApprovalId,
509 tool_call_id: ToolCallId,
510 expires_at: ProtocolTimestamp,
511 requested_at: ProtocolTimestamp,
512) -> PendingApproval {
513 PendingApproval {
514 approval_id,
515 tool_call_id,
516 expires_at,
517 requested_at,
518 }
519}
520
521pub(crate) fn new_tool_call(
522 tool_call_id: ToolCallId,
523 tool_name: String,
524 arguments: Value,
525) -> ToolCallState {
526 ToolCallState {
527 tool_call_id,
528 tool_name,
529 arguments,
530 policy_decision: None,
531 approval_id: None,
532 approval_decision: None,
533 execution: ToolExecutionState::NotStarted,
534 result_message_id: None,
535 }
536}
537
538pub(crate) fn set_model(configuration: &mut SessionConfiguration, model: ModelRef) {
539 configuration.model = Some(model);
540}
541
542pub(crate) fn set_profile(configuration: &mut SessionConfiguration, profile_id: ProfileId) {
543 configuration.profile_id = profile_id;
544}
545
546pub(crate) fn set_reasoning_effort(
547 configuration: &mut SessionConfiguration,
548 reasoning_effort: ReasoningEffort,
549) {
550 configuration.reasoning_effort = Some(reasoning_effort);
551}
552
553pub(crate) fn set_policy(tool: &mut ToolCallState, decision: PolicyDecision) {
554 tool.policy_decision = Some(decision);
555}
556
557pub(crate) fn set_approval(tool: &mut ToolCallState, approval_id: ApprovalId) {
558 tool.approval_id = Some(approval_id);
559}
560
561pub(crate) fn resolve_approval(tool: &mut ToolCallState, decision: ApprovalDecision) {
562 tool.approval_decision = Some(decision);
563}
564
565pub(crate) fn start_tool(
566 tool: &mut ToolCallState,
567 execution_target: ExecutionTarget,
568 idempotency: ToolIdempotency,
569) {
570 tool.execution = ToolExecutionState::Started {
571 execution_target,
572 idempotency,
573 };
574}
575
576pub(crate) fn finish_tool(
577 tool: &mut ToolCallState,
578 is_error: bool,
579 content: Vec<ContentBlock>,
580 error: Option<ToolFailure>,
581 presentation: Option<ToolPresentation>,
582) {
583 tool.execution = ToolExecutionState::Finished {
584 is_error,
585 content,
586 error,
587 presentation,
588 };
589}
590
591pub(crate) fn set_compaction(
592 state: &mut MaterializedSessionState,
593 summary: CanonicalMessage,
594 compacted_through_record_id: RecordId,
595) {
596 state.messages.clear();
600 state.messages.push(summary.clone());
601 state.latest_compaction = Some(SessionCompaction {
602 summary,
603 compacted_through_record_id,
604 });
605}
606
607pub(crate) fn commit_tool_result(tool: &mut ToolCallState, message_id: MessageId) {
608 tool.result_message_id = Some(message_id);
609}
610
611pub(crate) fn interrupt_tool(tool: &mut ToolCallState, reason: String) {
612 if let ToolExecutionState::Started {
613 execution_target,
614 idempotency,
615 } = tool.execution
616 {
617 tool.execution = ToolExecutionState::Interrupted {
618 reason,
619 execution_target,
620 idempotency,
621 };
622 }
623}