1use crate::Provider;
13use crate::peer_meta::PeerMeta;
14use crate::service::{AppendSystemContextRequest, MobToolAuthorityContext};
15use crate::time_compat::SystemTime;
16use crate::tool_scope::ToolFilter;
17use crate::types::{ContentInput, Message, SessionId, ToolDef, ToolProvenance, ToolResult, Usage};
18use serde::{Deserialize, Deserializer, Serialize, Serializer};
19use std::collections::{BTreeMap, BTreeSet, HashMap};
20use std::sync::Arc;
21
22pub const SESSION_VERSION: u32 = 1;
24
25#[derive(Debug, Clone)]
29pub struct Session {
30 version: u32,
32 id: SessionId,
34 pub(crate) messages: Arc<Vec<Message>>,
36 created_at: SystemTime,
38 updated_at: SystemTime,
40 metadata: serde_json::Map<String, serde_json::Value>,
42 usage: Usage,
44}
45
46#[derive(Serialize, Deserialize)]
48#[serde(rename_all = "snake_case")]
49struct SessionSerde {
50 #[serde(default = "default_version")]
51 version: u32,
52 id: SessionId,
53 messages: Vec<Message>,
54 created_at: SystemTime,
55 updated_at: SystemTime,
56 #[serde(default)]
57 metadata: serde_json::Map<String, serde_json::Value>,
58 #[serde(default)]
59 usage: Usage,
60}
61
62impl Serialize for Session {
63 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
64 where
65 S: Serializer,
66 {
67 let serde_repr = SessionSerde {
68 version: self.version,
69 id: self.id.clone(),
70 messages: (*self.messages).clone(),
71 created_at: self.created_at,
72 updated_at: self.updated_at,
73 metadata: self.metadata.clone(),
74 usage: self.usage.clone(),
75 };
76 serde_repr.serialize(serializer)
77 }
78}
79
80impl<'de> Deserialize<'de> for Session {
81 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
82 where
83 D: Deserializer<'de>,
84 {
85 let serde_repr = SessionSerde::deserialize(deserializer)?;
86 Ok(Session {
87 version: serde_repr.version,
88 id: serde_repr.id,
89 messages: Arc::new(serde_repr.messages),
90 created_at: serde_repr.created_at,
91 updated_at: serde_repr.updated_at,
92 metadata: serde_repr.metadata,
93 usage: serde_repr.usage,
94 })
95 }
96}
97
98fn default_version() -> u32 {
99 SESSION_VERSION
100}
101
102pub const SESSION_SYSTEM_CONTEXT_STATE_KEY: &str = "session_system_context_state";
104
105pub const SESSION_DEFERRED_TURN_STATE_KEY: &str = "session_deferred_turn_state";
107
108pub const SESSION_BUILD_STATE_KEY: &str = "session_build_state";
110
111pub const SESSION_TOOL_VISIBILITY_STATE_KEY: &str = "session_tool_visibility_state_v1";
113
114pub const SYSTEM_CONTEXT_SEPARATOR: &str = "\n\n---\n\n";
116
117#[derive(Debug, Clone, Serialize, Deserialize, Default, PartialEq, Eq)]
119#[serde(rename_all = "snake_case")]
120pub struct SessionSystemContextState {
121 #[serde(default, skip_serializing_if = "Vec::is_empty")]
122 pub pending: Vec<PendingSystemContextAppend>,
123 #[serde(default, skip_serializing_if = "std::collections::BTreeMap::is_empty")]
124 pub seen: std::collections::BTreeMap<String, SeenSystemContextKey>,
125}
126
127#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
129#[serde(rename_all = "snake_case")]
130pub struct PendingSystemContextAppend {
131 pub text: String,
132 #[serde(default, skip_serializing_if = "Option::is_none")]
133 pub source: Option<String>,
134 #[serde(default, skip_serializing_if = "Option::is_none")]
135 pub idempotency_key: Option<String>,
136 pub accepted_at: SystemTime,
137}
138
139#[derive(Debug, Clone, Serialize, Deserialize, Default, PartialEq)]
141#[serde(rename_all = "snake_case")]
142pub struct SessionDeferredTurnState {
143 #[serde(default, skip_serializing_if = "DeferredFirstTurnPhase::is_inactive")]
144 pub first_turn_phase: DeferredFirstTurnPhase,
145 #[serde(default, skip_serializing_if = "Option::is_none")]
146 pub pending_initial_prompt: Option<PendingDeferredPrompt>,
147 #[serde(default, skip_serializing_if = "Vec::is_empty")]
148 pub pending_tool_results: Vec<PendingToolResultsMessage>,
149}
150
151#[derive(Debug, Clone, Copy, Serialize, Deserialize, Default, PartialEq, Eq)]
153#[serde(rename_all = "snake_case")]
154pub enum DeferredFirstTurnPhase {
155 #[default]
157 Inactive,
158 Pending,
160 Consumed,
162}
163
164impl DeferredFirstTurnPhase {
165 pub fn is_inactive(&self) -> bool {
166 matches!(self, Self::Inactive)
167 }
168}
169
170fn is_default_hook_run_overrides(value: &crate::HookRunOverrides) -> bool {
171 value == &crate::HookRunOverrides::default()
172}
173
174fn is_default_call_timeout_override(value: &crate::CallTimeoutOverride) -> bool {
175 value == &crate::CallTimeoutOverride::default()
176}
177
178fn is_tool_filter_all(value: &ToolFilter) -> bool {
179 matches!(value, ToolFilter::All)
180}
181
182fn is_zero(value: &u64) -> bool {
183 *value == 0
184}
185
186#[derive(Debug, Clone, Serialize, Deserialize, Default, PartialEq, Eq)]
188#[serde(rename_all = "snake_case")]
189pub struct ToolVisibilityWitness {
190 #[serde(default, skip_serializing_if = "Option::is_none")]
191 pub stable_owner_key: Option<String>,
192 #[serde(default, skip_serializing_if = "Option::is_none")]
193 pub last_seen_provenance: Option<ToolProvenance>,
194}
195
196#[derive(Debug, Clone, Serialize, Deserialize, Default, PartialEq, Eq)]
198#[serde(rename_all = "snake_case")]
199pub struct SessionToolVisibilityState {
200 #[serde(default, skip_serializing_if = "is_tool_filter_all")]
201 pub inherited_base_filter: ToolFilter,
202 #[serde(default, skip_serializing_if = "is_tool_filter_all")]
203 pub active_filter: ToolFilter,
204 #[serde(default, skip_serializing_if = "is_tool_filter_all")]
205 pub staged_filter: ToolFilter,
206 #[serde(default, skip_serializing_if = "BTreeSet::is_empty")]
207 pub active_requested_deferred_names: BTreeSet<String>,
208 #[serde(default, skip_serializing_if = "BTreeSet::is_empty")]
209 pub staged_requested_deferred_names: BTreeSet<String>,
210 #[serde(default, skip_serializing_if = "is_zero")]
211 pub active_revision: u64,
212 #[serde(default, skip_serializing_if = "is_zero")]
213 pub staged_revision: u64,
214 #[serde(default, skip_serializing_if = "BTreeMap::is_empty")]
215 pub requested_witnesses: BTreeMap<String, ToolVisibilityWitness>,
216 #[serde(default, skip_serializing_if = "BTreeMap::is_empty")]
217 pub filter_witnesses: BTreeMap<String, ToolVisibilityWitness>,
218}
219
220#[derive(Debug, Clone, Serialize, Deserialize, Default)]
223#[serde(rename_all = "snake_case")]
224pub struct SessionBuildState {
225 #[serde(default, skip_serializing_if = "Option::is_none")]
226 pub system_prompt: Option<String>,
227 #[serde(default, skip_serializing_if = "Option::is_none")]
228 pub output_schema: Option<crate::OutputSchema>,
229 #[serde(default, skip_serializing_if = "is_default_hook_run_overrides")]
230 pub hooks_override: crate::HookRunOverrides,
231 #[serde(default, skip_serializing_if = "Option::is_none")]
232 pub budget_limits: Option<crate::BudgetLimits>,
233 #[serde(default, skip_serializing_if = "Vec::is_empty")]
234 pub recoverable_tool_defs: Vec<ToolDef>,
235 #[serde(default, skip_serializing_if = "Vec::is_empty")]
236 pub silent_comms_intents: Vec<String>,
237 #[serde(default, skip_serializing_if = "Option::is_none")]
238 pub max_inline_peer_notifications: Option<i32>,
239 #[serde(default, skip_serializing_if = "Option::is_none")]
240 pub app_context: Option<serde_json::Value>,
241 #[serde(default, skip_serializing_if = "Option::is_none")]
242 pub additional_instructions: Option<Vec<String>>,
243 #[serde(default, skip_serializing_if = "Option::is_none")]
244 pub shell_env: Option<HashMap<String, String>>,
245 #[serde(default, skip_serializing_if = "Option::is_none")]
246 pub mob_tool_authority_context: Option<MobToolAuthorityContext>,
247 #[serde(default, skip_serializing_if = "is_default_call_timeout_override")]
248 pub call_timeout_override: crate::CallTimeoutOverride,
249}
250
251#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
253#[serde(rename_all = "snake_case")]
254pub struct PendingDeferredPrompt {
255 pub prompt: ContentInput,
256 pub accepted_at: SystemTime,
257}
258
259#[derive(Debug, Clone, Serialize, Deserialize)]
261#[serde(rename_all = "snake_case")]
262pub struct PendingToolResultsMessage {
263 pub results: Vec<ToolResult>,
264 pub accepted_at: SystemTime,
265}
266
267impl PartialEq for PendingToolResultsMessage {
268 fn eq(&self, other: &Self) -> bool {
269 self.accepted_at == other.accepted_at
270 && serde_json::to_value(&self.results).ok() == serde_json::to_value(&other.results).ok()
271 }
272}
273
274#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
276#[serde(rename_all = "snake_case")]
277pub struct SeenSystemContextKey {
278 pub text: String,
279 #[serde(default, skip_serializing_if = "Option::is_none")]
280 pub source: Option<String>,
281 pub state: SeenSystemContextState,
282}
283
284#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
286#[serde(rename_all = "snake_case")]
287pub enum SeenSystemContextState {
288 Pending,
289 Applied,
290}
291
292impl SessionSystemContextState {
293 pub fn stage_append(
295 &mut self,
296 req: &AppendSystemContextRequest,
297 accepted_at: SystemTime,
298 ) -> Result<crate::service::AppendSystemContextStatus, SystemContextStageError> {
299 let text = req.text.trim();
300 if text.is_empty() {
301 return Err(SystemContextStageError::InvalidRequest(
302 "system context text must not be empty".to_string(),
303 ));
304 }
305
306 if let Some(key) = req.idempotency_key.as_ref() {
307 match self.seen.get(key) {
308 Some(existing)
309 if existing.text == text
310 && existing.source.as_deref() == req.source.as_deref() =>
311 {
312 return Ok(crate::service::AppendSystemContextStatus::Duplicate);
313 }
314 Some(existing) => {
315 return Err(SystemContextStageError::Conflict {
316 key: key.clone(),
317 existing_text: existing.text.clone(),
318 existing_source: existing.source.clone(),
319 });
320 }
321 None => {}
322 }
323 }
324
325 let append = PendingSystemContextAppend {
326 text: text.to_string(),
327 source: req.source.clone(),
328 idempotency_key: req.idempotency_key.clone(),
329 accepted_at,
330 };
331 if let Some(key) = req.idempotency_key.as_ref() {
332 self.seen.insert(
333 key.clone(),
334 SeenSystemContextKey {
335 text: append.text.clone(),
336 source: append.source.clone(),
337 state: SeenSystemContextState::Pending,
338 },
339 );
340 }
341 self.pending.push(append);
342 Ok(crate::service::AppendSystemContextStatus::Staged)
343 }
344
345 pub fn mark_pending_applied(&mut self) {
347 for pending in &self.pending {
348 if let Some(key) = pending.idempotency_key.as_ref()
349 && let Some(seen) = self.seen.get_mut(key)
350 {
351 seen.state = SeenSystemContextState::Applied;
352 }
353 }
354 self.pending.clear();
355 }
356}
357
358impl SessionDeferredTurnState {
359 pub fn mark_initial_turn_pending(&mut self) {
361 self.first_turn_phase = DeferredFirstTurnPhase::Pending;
362 }
363
364 pub fn mark_initial_turn_started(&mut self) -> bool {
368 let was_pending = matches!(self.first_turn_phase, DeferredFirstTurnPhase::Pending);
369 if was_pending {
370 self.first_turn_phase = DeferredFirstTurnPhase::Consumed;
371 }
372 was_pending
373 }
374
375 pub fn restore_initial_turn_pending(&mut self) {
377 self.first_turn_phase = DeferredFirstTurnPhase::Pending;
378 }
379
380 pub fn allows_initial_turn_overrides(&self) -> bool {
382 matches!(self.first_turn_phase, DeferredFirstTurnPhase::Pending)
383 }
384
385 pub fn stage_initial_prompt(&mut self, prompt: ContentInput, accepted_at: SystemTime) {
387 if !prompt.has_images() && prompt.text_content().trim().is_empty() {
388 self.pending_initial_prompt = None;
389 return;
390 }
391
392 self.pending_initial_prompt = Some(PendingDeferredPrompt {
393 prompt,
394 accepted_at,
395 });
396 }
397
398 pub fn stage_tool_results(
400 &mut self,
401 results: Vec<ToolResult>,
402 accepted_at: SystemTime,
403 ) -> usize {
404 if results.is_empty() {
405 return 0;
406 }
407
408 let accepted = results.len();
409 self.pending_tool_results.push(PendingToolResultsMessage {
410 results,
411 accepted_at,
412 });
413 accepted
414 }
415
416 pub fn take_initial_prompt(&mut self) -> Option<ContentInput> {
418 self.pending_initial_prompt
419 .take()
420 .map(|pending| pending.prompt)
421 }
422
423 pub fn take_tool_results(&mut self) -> Vec<PendingToolResultsMessage> {
425 std::mem::take(&mut self.pending_tool_results)
426 }
427
428 pub fn has_pending_tool_results(&self) -> bool {
430 !self.pending_tool_results.is_empty()
431 }
432}
433
434#[derive(Debug, Clone, PartialEq, Eq)]
436pub enum SystemContextStageError {
437 InvalidRequest(String),
438 Conflict {
439 key: String,
440 existing_text: String,
441 existing_source: Option<String>,
442 },
443}
444
445fn render_system_context_block(append: &PendingSystemContextAppend) -> String {
446 let mut rendered = String::from("[Runtime System Context]");
447 if let Some(source) = &append.source {
448 rendered.push_str("\nsource: ");
449 rendered.push_str(source);
450 }
451 rendered.push_str("\n\n");
452 rendered.push_str(&append.text);
453 rendered
454}
455
456impl Session {
457 pub fn new() -> Self {
459 let now = SystemTime::now();
460 Self {
461 version: SESSION_VERSION,
462 id: SessionId::new(),
463 messages: Arc::new(Vec::new()),
464 created_at: now,
465 updated_at: now,
466 metadata: serde_json::Map::new(),
467 usage: Usage::default(),
468 }
469 }
470
471 pub fn with_id(id: SessionId) -> Self {
473 let mut session = Self::new();
474 session.id = id;
475 session
476 }
477
478 pub fn id(&self) -> &SessionId {
480 &self.id
481 }
482
483 pub fn version(&self) -> u32 {
485 self.version
486 }
487
488 pub fn messages(&self) -> &[Message] {
490 &self.messages
491 }
492
493 pub fn messages_mut(&mut self) -> &mut Vec<Message> {
495 Arc::make_mut(&mut self.messages)
496 }
497
498 pub fn created_at(&self) -> SystemTime {
500 self.created_at
501 }
502
503 pub fn updated_at(&self) -> SystemTime {
505 self.updated_at
506 }
507
508 pub fn push(&mut self, message: Message) {
512 Arc::make_mut(&mut self.messages).push(message);
513 self.updated_at = SystemTime::now();
514 }
515
516 pub fn push_batch(&mut self, messages: Vec<Message>) {
520 if messages.is_empty() {
521 return;
522 }
523 let inner = Arc::make_mut(&mut self.messages);
524 inner.extend(messages);
525 self.updated_at = SystemTime::now();
526 }
527
528 pub fn touch(&mut self) {
532 self.updated_at = SystemTime::now();
533 }
534
535 pub fn has_pending_boundary(&self) -> bool {
541 self.messages
542 .last()
543 .is_some_and(|m| matches!(m, Message::User(_) | Message::ToolResults { .. }))
544 }
545
546 pub fn last_n(&self, n: usize) -> &[Message] {
548 let start = self.messages.len().saturating_sub(n);
549 &self.messages[start..]
550 }
551
552 pub fn total_tokens(&self) -> u64 {
554 self.usage.total_tokens()
555 }
556
557 pub fn total_usage(&self) -> Usage {
559 self.usage.clone()
560 }
561
562 pub fn record_usage(&mut self, turn_usage: Usage) {
564 self.usage.add(&turn_usage);
565 self.updated_at = SystemTime::now();
566 }
567
568 pub fn set_system_prompt(&mut self, prompt: String) {
570 use crate::types::SystemMessage;
571
572 let inner = Arc::make_mut(&mut self.messages);
573 if let Some(Message::System(_)) = inner.first() {
575 inner[0] = Message::System(SystemMessage { content: prompt });
576 } else {
577 inner.insert(0, Message::System(SystemMessage { content: prompt }));
578 }
579 self.updated_at = SystemTime::now();
580 }
581
582 pub fn append_system_context_blocks(&mut self, appends: &[PendingSystemContextAppend]) {
584 if appends.is_empty() {
585 return;
586 }
587
588 let rendered = appends
589 .iter()
590 .map(render_system_context_block)
591 .collect::<Vec<_>>()
592 .join(SYSTEM_CONTEXT_SEPARATOR);
593
594 let next = match self.messages.first() {
595 Some(Message::System(sys)) if !sys.content.is_empty() => {
596 format!("{}{}{}", sys.content, SYSTEM_CONTEXT_SEPARATOR, rendered)
597 }
598 _ => rendered,
599 };
600 self.set_system_prompt(next);
601 }
602
603 pub fn last_assistant_text(&self) -> Option<String> {
605 self.messages.iter().rev().find_map(|m| match m {
606 Message::BlockAssistant(a) => {
607 let mut buf = String::new();
608 for block in &a.blocks {
609 if let crate::types::AssistantBlock::Text { text, .. } = block {
610 buf.push_str(text);
611 }
612 }
613 if buf.is_empty() { None } else { Some(buf) }
614 }
615 Message::Assistant(a) if !a.content.is_empty() => Some(a.content.clone()),
616 _ => None,
617 })
618 }
619
620 pub fn tool_call_count(&self) -> usize {
622 self.messages
623 .iter()
624 .filter_map(|m| match m {
625 Message::BlockAssistant(a) => Some(
626 a.blocks
627 .iter()
628 .filter(|b| matches!(b, crate::types::AssistantBlock::ToolUse { .. }))
629 .count(),
630 ),
631 Message::Assistant(a) => Some(a.tool_calls.len()),
632 _ => None,
633 })
634 .sum()
635 }
636
637 pub fn metadata(&self) -> &serde_json::Map<String, serde_json::Value> {
639 &self.metadata
640 }
641
642 pub fn set_metadata(&mut self, key: &str, value: serde_json::Value) {
644 self.metadata.insert(key.to_string(), value);
645 self.updated_at = SystemTime::now();
646 }
647
648 pub fn remove_metadata(&mut self, key: &str) {
650 self.metadata.remove(key);
651 self.updated_at = SystemTime::now();
652 }
653
654 pub fn set_session_metadata(
656 &mut self,
657 metadata: SessionMetadata,
658 ) -> Result<(), serde_json::Error> {
659 let value = serde_json::to_value(metadata)?;
660 self.set_metadata(SESSION_METADATA_KEY, value);
661 Ok(())
662 }
663
664 pub fn session_metadata(&self) -> Option<SessionMetadata> {
666 self.metadata
667 .get(SESSION_METADATA_KEY)
668 .and_then(|value| serde_json::from_value(value.clone()).ok())
669 }
670
671 pub fn set_system_context_state(
673 &mut self,
674 state: SessionSystemContextState,
675 ) -> Result<(), serde_json::Error> {
676 let value = serde_json::to_value(state)?;
677 self.set_metadata(SESSION_SYSTEM_CONTEXT_STATE_KEY, value);
678 Ok(())
679 }
680
681 pub fn system_context_state(&self) -> Option<SessionSystemContextState> {
683 self.metadata
684 .get(SESSION_SYSTEM_CONTEXT_STATE_KEY)
685 .and_then(|value| serde_json::from_value(value.clone()).ok())
686 }
687
688 pub fn set_deferred_turn_state(
690 &mut self,
691 state: SessionDeferredTurnState,
692 ) -> Result<(), serde_json::Error> {
693 let value = serde_json::to_value(state)?;
694 self.set_metadata(SESSION_DEFERRED_TURN_STATE_KEY, value);
695 Ok(())
696 }
697
698 pub fn deferred_turn_state(&self) -> Option<SessionDeferredTurnState> {
700 self.metadata
701 .get(SESSION_DEFERRED_TURN_STATE_KEY)
702 .and_then(|value| serde_json::from_value(value.clone()).ok())
703 }
704
705 pub fn set_build_state(&mut self, state: SessionBuildState) -> Result<(), serde_json::Error> {
707 let value = serde_json::to_value(state)?;
708 self.set_metadata(SESSION_BUILD_STATE_KEY, value);
709 Ok(())
710 }
711
712 pub fn build_state(&self) -> Option<SessionBuildState> {
714 self.metadata
715 .get(SESSION_BUILD_STATE_KEY)
716 .and_then(|value| serde_json::from_value(value.clone()).ok())
717 }
718
719 pub fn set_tool_visibility_state(
721 &mut self,
722 state: SessionToolVisibilityState,
723 ) -> Result<(), serde_json::Error> {
724 let value = serde_json::to_value(state)?;
725 self.set_metadata(SESSION_TOOL_VISIBILITY_STATE_KEY, value);
726 Ok(())
727 }
728
729 pub fn tool_visibility_state(&self) -> Option<SessionToolVisibilityState> {
731 self.metadata
732 .get(SESSION_TOOL_VISIBILITY_STATE_KEY)
733 .and_then(|value| serde_json::from_value(value.clone()).ok())
734 }
735
736 pub fn set_mob_tool_authority_context(
738 &mut self,
739 authority_context: Option<MobToolAuthorityContext>,
740 ) -> Result<(), serde_json::Error> {
741 let mut build_state = self.build_state().unwrap_or_default();
742 build_state.mob_tool_authority_context = authority_context;
743 self.set_build_state(build_state)
744 }
745
746 pub fn mob_tool_authority_context(&self) -> Option<MobToolAuthorityContext> {
748 self.build_state()
749 .and_then(|state| state.mob_tool_authority_context)
750 }
751
752 pub fn fork_at(&self, index: usize) -> Self {
757 let now = SystemTime::now();
758 let truncated = self.messages[..index.min(self.messages.len())].to_vec();
759 Self {
760 version: SESSION_VERSION,
761 id: SessionId::new(),
762 messages: Arc::new(truncated),
763 created_at: now,
764 updated_at: now,
765 metadata: self.metadata.clone(),
766 usage: self.usage.clone(),
767 }
768 }
769
770 pub fn fork(&self) -> Self {
775 let now = SystemTime::now();
776 Self {
777 version: SESSION_VERSION,
778 id: SessionId::new(),
779 messages: Arc::clone(&self.messages),
780 created_at: now,
781 updated_at: now,
782 metadata: self.metadata.clone(),
783 usage: self.usage.clone(),
784 }
785 }
786}
787
788impl Default for Session {
789 fn default() -> Self {
790 Self::new()
791 }
792}
793
794#[derive(Debug, Clone, Serialize, Deserialize)]
796#[serde(rename_all = "snake_case")]
797pub struct SessionMeta {
798 pub id: SessionId,
799 pub created_at: SystemTime,
800 pub updated_at: SystemTime,
801 pub message_count: usize,
802 pub total_tokens: u64,
803 #[serde(default)]
804 pub metadata: serde_json::Map<String, serde_json::Value>,
805}
806
807#[derive(Debug, Clone, Serialize, Deserialize)]
809#[serde(rename_all = "snake_case")]
810pub struct SessionMetadata {
811 pub model: String,
812 pub max_tokens: u32,
813 #[serde(default = "default_structured_output_retries")]
814 pub structured_output_retries: u32,
815 pub provider: Provider,
816 #[serde(default, skip_serializing_if = "Option::is_none")]
817 pub self_hosted_server_id: Option<String>,
818 #[serde(default, skip_serializing_if = "Option::is_none")]
819 pub provider_params: Option<serde_json::Value>,
820 pub tooling: SessionTooling,
821 #[serde(default)]
822 pub keep_alive: bool,
823 pub comms_name: Option<String>,
824 #[serde(default, skip_serializing_if = "Option::is_none")]
826 pub peer_meta: Option<PeerMeta>,
827 #[serde(default, skip_serializing_if = "Option::is_none")]
829 pub realm_id: Option<String>,
830 #[serde(default, skip_serializing_if = "Option::is_none")]
832 pub instance_id: Option<String>,
833 #[serde(default, skip_serializing_if = "Option::is_none")]
835 pub backend: Option<String>,
836 #[serde(default, skip_serializing_if = "Option::is_none")]
838 pub config_generation: Option<u64>,
839}
840
841fn default_structured_output_retries() -> u32 {
842 2
843}
844
845#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
847#[serde(rename_all = "snake_case")]
848pub struct SessionLlmIdentity {
849 pub model: String,
850 pub provider: Provider,
851 #[serde(default, skip_serializing_if = "Option::is_none")]
852 pub self_hosted_server_id: Option<String>,
853 #[serde(default, skip_serializing_if = "Option::is_none")]
854 pub provider_params: Option<serde_json::Value>,
855}
856
857impl SessionMetadata {
858 pub fn llm_identity(&self) -> SessionLlmIdentity {
860 SessionLlmIdentity {
861 model: self.model.clone(),
862 provider: self.provider,
863 self_hosted_server_id: self.self_hosted_server_id.clone(),
864 provider_params: self.provider_params.clone(),
865 }
866 }
867
868 pub fn apply_llm_identity(&mut self, identity: &SessionLlmIdentity) {
870 self.model = identity.model.clone();
871 self.provider = identity.provider;
872 self.self_hosted_server_id = identity.self_hosted_server_id.clone();
873 self.provider_params = identity.provider_params.clone();
874 }
875}
876
877pub const SESSION_METADATA_KEY: &str = "session_metadata";
879
880#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Serialize, Deserialize)]
888#[serde(rename_all = "snake_case")]
889pub enum ToolCategoryOverride {
890 #[default]
892 Inherit,
893 Enable,
895 Disable,
897}
898
899impl ToolCategoryOverride {
900 #[must_use]
906 pub fn resolve(self, runtime_default: bool) -> bool {
907 match self {
908 Self::Enable => true,
909 Self::Disable => false,
910 Self::Inherit => runtime_default,
911 }
912 }
913
914 #[must_use]
920 pub fn to_override(self) -> Option<bool> {
921 match self {
922 Self::Enable => Some(true),
923 Self::Disable => Some(false),
924 Self::Inherit => None,
925 }
926 }
927
928 #[must_use]
936 pub fn from_effective(enabled: bool) -> Self {
937 if enabled { Self::Enable } else { Self::Disable }
938 }
939
940 #[must_use]
950 pub fn from_override(value: Option<bool>) -> Self {
951 match value {
952 Some(true) => Self::Enable,
953 Some(false) => Self::Disable,
954 None => Self::Inherit,
955 }
956 }
957}
958
959fn deserialize_tool_category_compat<'de, D>(
967 deserializer: D,
968) -> Result<ToolCategoryOverride, D::Error>
969where
970 D: serde::Deserializer<'de>,
971{
972 use serde::de;
973
974 struct ToolCategoryVisitor;
975
976 impl de::Visitor<'_> for ToolCategoryVisitor {
977 type Value = ToolCategoryOverride;
978
979 fn expecting(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
980 formatter.write_str("a boolean or one of \"inherit\", \"enable\", \"disable\"")
981 }
982
983 fn visit_bool<E: de::Error>(self, v: bool) -> Result<Self::Value, E> {
984 Ok(if v {
985 ToolCategoryOverride::Enable
986 } else {
987 ToolCategoryOverride::Inherit
988 })
989 }
990
991 fn visit_str<E: de::Error>(self, v: &str) -> Result<Self::Value, E> {
992 match v {
993 "inherit" => Ok(ToolCategoryOverride::Inherit),
994 "enable" => Ok(ToolCategoryOverride::Enable),
995 "disable" => Ok(ToolCategoryOverride::Disable),
996 _ => Err(de::Error::unknown_variant(
997 v,
998 &["inherit", "enable", "disable"],
999 )),
1000 }
1001 }
1002 }
1003
1004 deserializer.deserialize_any(ToolCategoryVisitor)
1005}
1006
1007#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, Default)]
1014#[serde(rename_all = "snake_case")]
1015pub struct SessionTooling {
1016 #[serde(default, deserialize_with = "deserialize_tool_category_compat")]
1017 pub builtins: ToolCategoryOverride,
1018 #[serde(default, deserialize_with = "deserialize_tool_category_compat")]
1019 pub shell: ToolCategoryOverride,
1020 #[serde(default, deserialize_with = "deserialize_tool_category_compat")]
1021 pub comms: ToolCategoryOverride,
1022 #[serde(default, deserialize_with = "deserialize_tool_category_compat")]
1024 pub mob: ToolCategoryOverride,
1025 #[serde(default, deserialize_with = "deserialize_tool_category_compat")]
1027 pub memory: ToolCategoryOverride,
1028 #[serde(default, skip_serializing_if = "Option::is_none")]
1030 pub active_skills: Option<Vec<crate::skills::SkillId>>,
1031}
1032
1033impl From<&Session> for SessionMeta {
1034 fn from(session: &Session) -> Self {
1035 Self {
1036 id: session.id.clone(),
1037 created_at: session.created_at,
1038 updated_at: session.updated_at,
1039 message_count: session.messages.len(),
1040 total_tokens: session.total_tokens(),
1041 metadata: session.metadata.clone(),
1042 }
1043 }
1044}
1045
1046#[cfg(test)]
1047#[allow(clippy::unwrap_used, clippy::expect_used)]
1048mod tests {
1049 use super::*;
1050 use crate::types::{
1051 AssistantMessage, BlockAssistantMessage, StopReason, SystemMessage, UserMessage,
1052 };
1053 use std::sync::Arc;
1054
1055 #[test]
1056 fn test_session_new() {
1057 let session = Session::new();
1058 assert_eq!(session.version(), SESSION_VERSION);
1059 assert!(session.messages().is_empty());
1060 assert!(session.created_at() <= session.updated_at());
1061 }
1062
1063 #[test]
1066 fn test_fork_shares_arc_no_clone() {
1067 let mut session = Session::new();
1068 for i in 0..100 {
1069 session.push(Message::User(UserMessage::text(format!("Message {i}"))));
1070 }
1071
1072 let forked = session.fork();
1074
1075 assert!(Arc::ptr_eq(&session.messages, &forked.messages));
1077 assert_eq!(forked.messages().len(), 100);
1078 }
1079
1080 #[test]
1081 fn test_fork_at_shares_arc_prefix() {
1082 let mut session = Session::new();
1083 for i in 0..100 {
1084 session.push(Message::User(UserMessage::text(format!("Message {i}"))));
1085 }
1086
1087 let forked = session.fork_at(50);
1089 assert_eq!(forked.messages().len(), 50);
1090
1091 assert_eq!(session.messages().len(), 100);
1093 }
1094
1095 #[test]
1096 fn test_push_cow_behavior() {
1097 let mut session = Session::new();
1098 session.push(Message::User(UserMessage::text("First".to_string())));
1099
1100 let forked = session.fork();
1102 assert!(Arc::ptr_eq(&session.messages, &forked.messages));
1103
1104 session.push(Message::User(UserMessage::text("Second".to_string())));
1106
1107 assert!(!Arc::ptr_eq(&session.messages, &forked.messages));
1109 assert_eq!(session.messages().len(), 2);
1110 assert_eq!(forked.messages().len(), 1);
1111 }
1112
1113 #[test]
1116 fn test_push_batch_single_timestamp() {
1117 let mut session = Session::new();
1118 let initial_updated = session.updated_at();
1119
1120 session.push_batch(vec![
1122 Message::User(UserMessage::text("First".to_string())),
1123 Message::User(UserMessage::text("Second".to_string())),
1124 Message::User(UserMessage::text("Third".to_string())),
1125 ]);
1126
1127 assert_eq!(session.messages().len(), 3);
1128 assert!(session.updated_at() >= initial_updated);
1130 }
1131
1132 #[test]
1133 fn test_touch_updates_timestamp() {
1134 let mut session = Session::new();
1135 let initial = session.updated_at();
1136
1137 std::thread::sleep(std::time::Duration::from_millis(10));
1138
1139 session.touch();
1141
1142 assert!(session.updated_at() > initial);
1143 }
1144
1145 #[test]
1146 fn test_session_push() {
1147 let mut session = Session::new();
1148 let initial_updated = session.updated_at();
1149
1150 std::thread::sleep(std::time::Duration::from_millis(10));
1152
1153 session.push(Message::User(UserMessage::text("Hello".to_string())));
1154
1155 assert_eq!(session.messages().len(), 1);
1156 assert!(session.updated_at() > initial_updated);
1157 }
1158
1159 #[test]
1160 fn test_session_fork() {
1161 let mut session = Session::new();
1162 session.push(Message::System(SystemMessage {
1163 content: "System prompt".to_string(),
1164 }));
1165 session.push(Message::User(UserMessage::text("Hello".to_string())));
1166 session.push(Message::Assistant(AssistantMessage {
1167 content: "Hi!".to_string(),
1168 tool_calls: vec![],
1169 stop_reason: StopReason::EndTurn,
1170 usage: Usage::default(),
1171 }));
1172
1173 let forked = session.fork_at(2);
1175 assert_eq!(forked.messages().len(), 2);
1176 assert_ne!(forked.id(), session.id());
1177
1178 let full_fork = session.fork();
1180 assert_eq!(full_fork.messages().len(), 3);
1181 }
1182
1183 #[test]
1184 fn test_session_metadata() {
1185 let mut session = Session::new();
1186 session.set_metadata("key", serde_json::json!("value"));
1187
1188 assert_eq!(session.metadata().get("key").unwrap(), "value");
1189 }
1190
1191 #[test]
1192 fn test_session_mob_tool_authority_context_roundtrip() {
1193 let mut session = Session::new();
1194 let authority = MobToolAuthorityContext::new(
1195 crate::service::OpaquePrincipalToken::new("opaque-principal"),
1196 false,
1197 )
1198 .with_managed_mob_scope(["mob-a"])
1199 .with_audit_invocation_id("audit-1");
1200
1201 session
1202 .set_mob_tool_authority_context(Some(authority.clone()))
1203 .expect("authority should serialize");
1204 assert_eq!(session.mob_tool_authority_context(), Some(authority));
1205
1206 session
1207 .set_mob_tool_authority_context(None)
1208 .expect("authority should clear");
1209 assert!(session.mob_tool_authority_context().is_none());
1210 }
1211
1212 #[test]
1213 fn test_session_tool_visibility_state_roundtrip() {
1214 let mut session = Session::new();
1215 let state = SessionToolVisibilityState {
1216 inherited_base_filter: ToolFilter::Allow(["visible".to_string()].into_iter().collect()),
1217 active_filter: ToolFilter::Allow(
1218 ["visible".to_string(), "missing".to_string()]
1219 .into_iter()
1220 .collect(),
1221 ),
1222 staged_filter: ToolFilter::Allow(
1223 ["visible".to_string(), "missing".to_string()]
1224 .into_iter()
1225 .collect(),
1226 ),
1227 active_revision: 1,
1228 staged_revision: 2,
1229 ..Default::default()
1230 };
1231
1232 session
1233 .set_tool_visibility_state(state.clone())
1234 .expect("tool visibility state should serialize");
1235 assert_eq!(session.tool_visibility_state(), Some(state));
1236 }
1237
1238 #[test]
1239 fn test_session_serialization() {
1240 let mut session = Session::new();
1241 session.push(Message::User(UserMessage::text("Test".to_string())));
1242
1243 let json = serde_json::to_string(&session).unwrap();
1244 let parsed: Session = serde_json::from_str(&json).unwrap();
1245
1246 assert_eq!(parsed.id(), session.id());
1247 assert_eq!(parsed.messages().len(), 1);
1248 assert_eq!(parsed.version(), SESSION_VERSION);
1249 }
1250
1251 #[test]
1252 fn test_session_meta_from_session() {
1253 let mut session = Session::new();
1254 session.push(Message::User(UserMessage::text("Hello".to_string())));
1255 session.push(Message::Assistant(AssistantMessage {
1256 content: "Hi!".to_string(),
1257 tool_calls: vec![],
1258 stop_reason: StopReason::EndTurn,
1259 usage: Usage {
1260 input_tokens: 10,
1261 output_tokens: 5,
1262 cache_creation_tokens: None,
1263 cache_read_tokens: None,
1264 },
1265 }));
1266 session.record_usage(Usage {
1267 input_tokens: 10,
1268 output_tokens: 5,
1269 cache_creation_tokens: None,
1270 cache_read_tokens: None,
1271 });
1272
1273 let meta = SessionMeta::from(&session);
1274 assert_eq!(meta.id, *session.id());
1275 assert_eq!(meta.message_count, 2);
1276 assert_eq!(meta.total_tokens, 15);
1277 }
1278
1279 #[test]
1280 fn has_pending_boundary_empty_session() {
1281 let session = Session::new();
1282 assert!(!session.has_pending_boundary());
1283 }
1284
1285 #[test]
1286 fn has_pending_boundary_after_user_message() {
1287 let mut session = Session::new();
1288 session.push(Message::User(UserMessage::text("hello")));
1289 assert!(session.has_pending_boundary());
1290 }
1291
1292 #[test]
1293 fn has_pending_boundary_after_assistant_message() {
1294 let mut session = Session::new();
1295 session.push(Message::User(UserMessage::text("hello")));
1296 session.push(Message::BlockAssistant(BlockAssistantMessage {
1297 blocks: vec![],
1298 stop_reason: StopReason::EndTurn,
1299 }));
1300 assert!(!session.has_pending_boundary());
1301 }
1302
1303 #[test]
1304 fn has_pending_boundary_after_tool_results() {
1305 let mut session = Session::new();
1306 session.push(Message::User(UserMessage::text("hello")));
1307 session.push(Message::ToolResults { results: vec![] });
1308 assert!(session.has_pending_boundary());
1309 }
1310
1311 #[test]
1312 fn has_pending_boundary_after_system() {
1313 let mut session = Session::new();
1314 session.push(Message::System(SystemMessage {
1315 content: "system".into(),
1316 }));
1317 assert!(!session.has_pending_boundary());
1318 }
1319}