1pub mod transport;
7
8use crate::event::AgentEvent;
9use crate::event::EventEnvelope;
10use crate::lifecycle::run_primitive::RuntimeTurnMetadata;
11use crate::session::{PendingSystemContextAppend, SystemContextStageError};
12use crate::time_compat::SystemTime;
13#[cfg(target_arch = "wasm32")]
14use crate::tokio;
15use crate::types::{
16 ContentInput, HandlingMode, Message, RenderMetadata, RunResult, SessionId, ToolDef, Usage,
17};
18use crate::{
19 AgentToolDispatcher, BudgetLimits, HookRunOverrides, OutputSchema, PeerMeta, Provider, Session,
20 SessionLlmIdentity, ToolCategoryOverride,
21};
22use crate::{EventStream, StreamError};
23use async_trait::async_trait;
24use serde::{Deserialize, Serialize};
25use std::collections::BTreeMap;
26use std::collections::BTreeSet;
27use std::sync::Arc;
28use tokio::sync::mpsc;
29
30#[derive(Debug, Clone, Copy, PartialEq, Eq)]
32pub enum InitialTurnPolicy {
33 RunImmediately,
35 Defer,
40}
41
42#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)]
44#[serde(rename_all = "snake_case")]
45pub enum DeferredPromptPolicy {
46 #[default]
48 Discard,
49 Stage,
51}
52
53#[derive(Debug, thiserror::Error)]
55pub enum SessionError {
56 #[error("session not found: {id}")]
58 NotFound { id: SessionId },
59
60 #[error("session is busy: {id}")]
62 Busy { id: SessionId },
63
64 #[error("session persistence is disabled")]
66 PersistenceDisabled,
67
68 #[error("session compaction is disabled")]
70 CompactionDisabled,
71
72 #[error("no turn running on session: {id}")]
74 NotRunning { id: SessionId },
75
76 #[error("store error: {0}")]
78 Store(#[source] Box<dyn std::error::Error + Send + Sync>),
79
80 #[error("agent error: {0}")]
82 Agent(#[from] crate::error::AgentError),
83
84 #[error("{message}")]
86 FailedWithData {
87 message: String,
88 data: serde_json::Value,
89 },
90
91 #[error("unsupported: {0}")]
93 Unsupported(String),
94}
95
96impl SessionError {
97 pub fn code(&self) -> &'static str {
99 match self {
100 Self::NotFound { .. } => "SESSION_NOT_FOUND",
101 Self::Busy { .. } => "SESSION_BUSY",
102 Self::PersistenceDisabled => "SESSION_PERSISTENCE_DISABLED",
103 Self::CompactionDisabled => "SESSION_COMPACTION_DISABLED",
104 Self::NotRunning { .. } => "SESSION_NOT_RUNNING",
105 Self::Store(_) => "SESSION_STORE_ERROR",
106 Self::Unsupported(_) => "SESSION_UNSUPPORTED",
107 Self::Agent(_) => "AGENT_ERROR",
108 Self::FailedWithData { .. } => "SESSION_ERROR",
109 }
110 }
111
112 pub fn structured_data(&self) -> Option<serde_json::Value> {
113 match self {
114 Self::FailedWithData { data, .. } => Some(data.clone()),
115 _ => None,
116 }
117 }
118}
119
120#[derive(Debug, thiserror::Error)]
122pub enum SessionControlError {
123 #[error(transparent)]
125 Session(#[from] SessionError),
126
127 #[error("invalid system-context request: {message}")]
129 InvalidRequest { message: String },
130
131 #[error(
133 "system-context idempotency conflict on session {id}: key '{key}' already maps to different content"
134 )]
135 Conflict { id: SessionId, key: String },
136}
137
138impl SessionControlError {
139 pub fn code(&self) -> &'static str {
141 match self {
142 Self::Session(err) => err.code(),
143 Self::InvalidRequest { .. } => "INVALID_PARAMS",
144 Self::Conflict { .. } => "SESSION_SYSTEM_CONTEXT_CONFLICT",
145 }
146 }
147}
148
149impl SystemContextStageError {
150 pub fn into_control_error(self, id: &SessionId) -> SessionControlError {
152 match self {
153 Self::InvalidRequest(message) => SessionControlError::InvalidRequest { message },
154 Self::Conflict { key, .. } => SessionControlError::Conflict {
155 id: id.clone(),
156 key,
157 },
158 }
159 }
160}
161
162#[derive(Debug)]
164pub struct CreateSessionRequest {
165 pub model: String,
167 pub prompt: ContentInput,
169 pub render_metadata: Option<RenderMetadata>,
171 pub system_prompt: Option<String>,
173 pub max_tokens: Option<u32>,
175 pub event_tx: Option<mpsc::Sender<EventEnvelope<AgentEvent>>>,
177 pub skill_references: Option<Vec<crate::skills::SkillKey>>,
179 pub initial_turn: InitialTurnPolicy,
181 pub deferred_prompt_policy: DeferredPromptPolicy,
183 pub build: Option<SessionBuildOptions>,
185 pub labels: Option<BTreeMap<String, String>>,
187}
188
189impl CreateSessionRequest {
190 #[must_use]
193 pub fn surface_metadata(&self) -> crate::SurfaceMetadata {
194 crate::SurfaceMetadata::from_optional_parts(
195 self.labels.clone(),
196 self.build
197 .as_ref()
198 .and_then(|build| build.app_context.clone()),
199 )
200 }
201}
202
203#[derive(Clone)]
205pub struct SessionBuildOptions {
206 pub provider: Option<Provider>,
207 pub self_hosted_server_id: Option<String>,
208 pub output_schema: Option<OutputSchema>,
209 pub structured_output_retries: u32,
210 pub hooks_override: HookRunOverrides,
211 pub comms_name: Option<String>,
212 pub peer_meta: Option<PeerMeta>,
213 pub resume_session: Option<Session>,
214 pub budget_limits: Option<BudgetLimits>,
215 pub provider_params: Option<serde_json::Value>,
216 pub external_tools: Option<Arc<dyn AgentToolDispatcher>>,
217 pub recoverable_tool_defs: Option<Vec<crate::ToolDef>>,
220 pub blob_store_override: Option<Arc<dyn crate::BlobStore>>,
223 pub llm_client_override: Option<Arc<dyn std::any::Any + Send + Sync>>,
227 pub override_builtins: ToolCategoryOverride,
230 pub override_shell: ToolCategoryOverride,
231 pub override_memory: ToolCategoryOverride,
232 pub override_schedule: ToolCategoryOverride,
234 pub override_mob: ToolCategoryOverride,
235 pub schedule_tools: Option<Arc<dyn AgentToolDispatcher>>,
240 pub preload_skills: Option<Vec<crate::skills::SkillKey>>,
241 pub realm_id: Option<String>,
242 pub instance_id: Option<String>,
243 pub backend: Option<String>,
244 pub config_generation: Option<u64>,
245 pub auth_binding: Option<crate::AuthBindingRef>,
248 pub keep_alive: bool,
251 pub checkpointer: Option<std::sync::Arc<dyn crate::checkpoint::SessionCheckpointer>>,
253 pub silent_comms_intents: Vec<String>,
256 pub max_inline_peer_notifications: Option<i32>,
264 pub app_context: Option<serde_json::Value>,
271 pub additional_instructions: Option<Vec<String>>,
274 pub shell_env: Option<std::collections::HashMap<String, String>>,
278 pub call_timeout_override: crate::CallTimeoutOverride,
284 pub resume_override_mask: ResumeOverrideMask,
290 pub mob_tools: Option<Arc<dyn MobToolsFactory>>,
298 pub runtime_build_mode: crate::runtime_epoch::RuntimeBuildMode,
306 pub initial_turn_metadata: Option<RuntimeTurnMetadata>,
311 pub mob_tool_authority_context: Option<MobToolAuthorityContext>,
318}
319
320#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
326pub struct OpaquePrincipalToken(String);
327
328impl OpaquePrincipalToken {
329 pub fn new(token: impl Into<String>) -> Self {
330 Self(token.into())
331 }
332
333 pub fn generated() -> Self {
334 Self(uuid::Uuid::new_v4().to_string())
335 }
336
337 pub fn as_str(&self) -> &str {
338 &self.0
339 }
340}
341
342impl std::fmt::Display for OpaquePrincipalToken {
343 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
344 f.write_str(self.as_str())
345 }
346}
347
348#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, Default)]
353pub struct MobToolCallerProvenance {
354 #[serde(default, skip_serializing_if = "Option::is_none")]
355 caller_session_id: Option<crate::SessionId>,
356 #[serde(default, skip_serializing_if = "Option::is_none")]
357 caller_mob_id: Option<String>,
358 #[serde(default, skip_serializing_if = "Option::is_none")]
359 caller_member_id: Option<String>,
360}
361
362impl MobToolCallerProvenance {
363 pub fn new() -> Self {
364 Self::default()
365 }
366
367 pub fn with_session_id(mut self, session_id: crate::SessionId) -> Self {
368 self.caller_session_id = Some(session_id);
369 self
370 }
371
372 pub fn with_mob_id(mut self, mob_id: impl Into<String>) -> Self {
373 self.caller_mob_id = Some(mob_id.into());
374 self
375 }
376
377 pub fn with_member_id(mut self, member_id: impl Into<String>) -> Self {
378 self.caller_member_id = Some(member_id.into());
379 self
380 }
381
382 pub fn caller_session_id(&self) -> Option<&crate::SessionId> {
383 self.caller_session_id.as_ref()
384 }
385
386 pub fn caller_mob_id(&self) -> Option<&str> {
387 self.caller_mob_id.as_deref()
388 }
389
390 pub fn caller_member_id(&self) -> Option<&str> {
391 self.caller_member_id.as_deref()
392 }
393}
394
395#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
401pub struct MobToolAuthorityContext {
402 principal_token: OpaquePrincipalToken,
403 can_create_mobs: bool,
404 #[serde(default)]
405 can_mutate_profiles: bool,
406 #[serde(default, skip_serializing_if = "BTreeSet::is_empty")]
407 managed_mob_scope: BTreeSet<String>,
408 #[serde(default, skip_serializing_if = "Option::is_none")]
409 caller_provenance: Option<MobToolCallerProvenance>,
410 #[serde(default, skip_serializing_if = "Option::is_none")]
411 audit_invocation_id: Option<String>,
412}
413
414impl MobToolAuthorityContext {
415 pub fn new(principal_token: OpaquePrincipalToken, can_create_mobs: bool) -> Self {
416 Self {
417 principal_token,
418 can_create_mobs,
419 can_mutate_profiles: can_create_mobs,
420 managed_mob_scope: BTreeSet::new(),
421 caller_provenance: None,
422 audit_invocation_id: None,
423 }
424 }
425
426 pub fn create_only_generated() -> Self {
427 Self::new(OpaquePrincipalToken::generated(), true)
428 }
429
430 pub fn principal_token(&self) -> &OpaquePrincipalToken {
431 &self.principal_token
432 }
433
434 pub fn can_create_mobs(&self) -> bool {
435 self.can_create_mobs
436 }
437
438 pub fn can_mutate_profiles(&self) -> bool {
439 self.can_mutate_profiles
440 }
441
442 pub fn with_profile_mutation(mut self, allowed: bool) -> Self {
443 self.can_mutate_profiles = allowed;
444 self
445 }
446
447 pub fn managed_mob_scope(&self) -> &BTreeSet<String> {
448 &self.managed_mob_scope
449 }
450
451 pub fn caller_provenance(&self) -> Option<&MobToolCallerProvenance> {
452 self.caller_provenance.as_ref()
453 }
454
455 pub fn audit_invocation_id(&self) -> Option<&str> {
456 self.audit_invocation_id.as_deref()
457 }
458
459 pub fn can_manage_mob(&self, mob_id: &str) -> bool {
460 self.managed_mob_scope.contains(mob_id)
461 }
462
463 pub fn grant_manage_mob(mut self, mob_id: impl Into<String>) -> Self {
464 self.managed_mob_scope.insert(mob_id.into());
465 self
466 }
467
468 pub fn grant_manage_mob_in_place(&mut self, mob_id: String) {
473 self.managed_mob_scope.insert(mob_id);
474 }
475
476 pub fn with_managed_mob_scope<I, S>(mut self, mob_ids: I) -> Self
477 where
478 I: IntoIterator<Item = S>,
479 S: Into<String>,
480 {
481 self.managed_mob_scope = mob_ids.into_iter().map(Into::into).collect();
482 self
483 }
484
485 pub fn with_caller_provenance(mut self, caller_provenance: MobToolCallerProvenance) -> Self {
486 self.caller_provenance = Some(caller_provenance);
487 self
488 }
489
490 pub fn with_audit_invocation_id(mut self, audit_invocation_id: impl Into<String>) -> Self {
491 self.audit_invocation_id = Some(audit_invocation_id.into());
492 self
493 }
494}
495
496pub fn generated_create_only_mob_operator_authority(
502 enable_mob: ToolCategoryOverride,
503) -> Option<MobToolAuthorityContext> {
504 matches!(enable_mob, ToolCategoryOverride::Enable)
505 .then(MobToolAuthorityContext::create_only_generated)
506}
507
508pub fn resolve_mob_operator_access(
514 enable_mob: ToolCategoryOverride,
515 persisted_authority_context: Option<MobToolAuthorityContext>,
516) -> (ToolCategoryOverride, Option<MobToolAuthorityContext>) {
517 if matches!(enable_mob, ToolCategoryOverride::Disable) {
518 return (ToolCategoryOverride::Disable, None);
519 }
520
521 let authority_context = persisted_authority_context
522 .or_else(|| generated_create_only_mob_operator_authority(enable_mob));
523 let override_mob = if authority_context.is_some() {
524 ToolCategoryOverride::Enable
525 } else {
526 enable_mob
527 };
528
529 (override_mob, authority_context)
530}
531
532pub trait VisibleToolSnapshotProvider: Send + Sync {
537 fn snapshot_visible_tools(&self) -> Vec<Arc<ToolDef>>;
539}
540
541pub enum MobToolSnapshotContext {
547 ParentOwned(Arc<dyn VisibleToolSnapshotProvider>),
549 Standalone,
551}
552
553pub struct MobToolsBuildArgs {
555 pub session_id: crate::SessionId,
557 pub model: String,
559 pub authority_context: Option<MobToolAuthorityContext>,
565 pub effective_authority: Option<Arc<std::sync::RwLock<MobToolAuthorityContext>>>,
572 pub comms_name: Option<String>,
574 pub comms_runtime: Option<Arc<dyn crate::agent::CommsRuntime>>,
576 pub snapshot_context: MobToolSnapshotContext,
578}
579
580#[cfg_attr(target_arch = "wasm32", async_trait(?Send))]
586#[cfg_attr(not(target_arch = "wasm32"), async_trait)]
587pub trait MobToolsFactory: Send + Sync {
588 async fn build_mob_tools(
590 &self,
591 args: MobToolsBuildArgs,
592 ) -> Result<Arc<dyn AgentToolDispatcher>, Box<dyn std::error::Error + Send + Sync>>;
593}
594
595#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
599pub struct ResumeOverrideMask {
600 pub model: bool,
601 pub provider: bool,
602 pub max_tokens: bool,
603 pub structured_output_retries: bool,
604 pub provider_params: bool,
605 pub auth_binding: bool,
606 pub override_builtins: bool,
607 pub override_shell: bool,
608 pub override_memory: bool,
609 pub override_mob: bool,
610 pub preload_skills: bool,
611 pub keep_alive: bool,
612 pub comms_name: bool,
613 pub peer_meta: bool,
614}
615
616impl SessionBuildOptions {
617 pub fn apply_persisted_mob_operator_access(
623 &mut self,
624 enable_mob: ToolCategoryOverride,
625 persisted_authority_context: Option<MobToolAuthorityContext>,
626 ) {
627 let (override_mob, authority_context) =
628 resolve_mob_operator_access(enable_mob, persisted_authority_context);
629 self.override_mob = override_mob;
630 self.mob_tool_authority_context = authority_context;
631 }
632
633 pub fn apply_generated_create_only_mob_operator_access(
640 &mut self,
641 enable_mob: ToolCategoryOverride,
642 ) {
643 self.apply_persisted_mob_operator_access(enable_mob, None);
644 }
645}
646
647impl Default for SessionBuildOptions {
648 fn default() -> Self {
649 Self {
650 provider: None,
651 self_hosted_server_id: None,
652 output_schema: None,
653 structured_output_retries: 2,
654 hooks_override: HookRunOverrides::default(),
655 comms_name: None,
656 peer_meta: None,
657 resume_session: None,
658 budget_limits: None,
661 provider_params: None,
662 external_tools: None,
663 recoverable_tool_defs: None,
664 blob_store_override: None,
665 llm_client_override: None,
666 override_builtins: ToolCategoryOverride::Inherit,
667 override_shell: ToolCategoryOverride::Inherit,
668 override_memory: ToolCategoryOverride::Inherit,
669 override_schedule: ToolCategoryOverride::Inherit,
670 override_mob: ToolCategoryOverride::Inherit,
671 schedule_tools: None,
672 preload_skills: None,
673 realm_id: None,
674 instance_id: None,
675 backend: None,
676 config_generation: None,
677 auth_binding: None,
678 keep_alive: false,
679 checkpointer: None,
680 silent_comms_intents: Vec::new(),
681 max_inline_peer_notifications: None,
682 app_context: None,
683 additional_instructions: None,
684 shell_env: None,
685 call_timeout_override: crate::CallTimeoutOverride::Inherit,
686 resume_override_mask: ResumeOverrideMask::default(),
687 mob_tools: None,
688 runtime_build_mode: crate::runtime_epoch::RuntimeBuildMode::StandaloneEphemeral,
689 initial_turn_metadata: None,
690 mob_tool_authority_context: None,
691 }
692 }
693}
694
695impl std::fmt::Debug for SessionBuildOptions {
696 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
697 f.debug_struct("SessionBuildOptions")
698 .field("provider", &self.provider)
699 .field("output_schema", &self.output_schema.is_some())
700 .field("structured_output_retries", &self.structured_output_retries)
701 .field("hooks_override", &self.hooks_override)
702 .field("comms_name", &self.comms_name)
703 .field("peer_meta", &self.peer_meta)
704 .field("resume_session", &self.resume_session.is_some())
705 .field("budget_limits", &self.budget_limits)
706 .field("provider_params", &self.provider_params.is_some())
707 .field("external_tools", &self.external_tools.is_some())
708 .field("recoverable_tool_defs", &self.recoverable_tool_defs)
709 .field("blob_store_override", &self.blob_store_override.is_some())
710 .field("llm_client_override", &self.llm_client_override.is_some())
711 .field("override_builtins", &self.override_builtins)
712 .field("override_shell", &self.override_shell)
713 .field("override_memory", &self.override_memory)
714 .field("override_schedule", &self.override_schedule)
715 .field("override_mob", &self.override_mob)
716 .field("schedule_tools", &self.schedule_tools.is_some())
717 .field("preload_skills", &self.preload_skills)
718 .field("realm_id", &self.realm_id)
719 .field("instance_id", &self.instance_id)
720 .field("backend", &self.backend)
721 .field("config_generation", &self.config_generation)
722 .field("keep_alive", &self.keep_alive)
723 .field("checkpointer", &self.checkpointer.is_some())
724 .field("silent_comms_intents", &self.silent_comms_intents)
725 .field(
726 "max_inline_peer_notifications",
727 &self.max_inline_peer_notifications,
728 )
729 .field("app_context", &self.app_context.is_some())
730 .field("additional_instructions", &self.additional_instructions)
731 .field("call_timeout_override", &self.call_timeout_override)
732 .field("resume_override_mask", &self.resume_override_mask)
733 .field("mob_tools", &self.mob_tools.is_some())
734 .field("runtime_build_mode", &self.runtime_build_mode)
735 .field(
736 "initial_turn_metadata",
737 &self.initial_turn_metadata.is_some(),
738 )
739 .field(
740 "mob_tool_authority_context",
741 &self.mob_tool_authority_context.is_some(),
742 )
743 .field("runtime_build_mode", &self.runtime_build_mode)
744 .finish()
745 }
746}
747
748#[derive(Debug)]
750pub struct StartTurnRequest {
751 pub prompt: ContentInput,
753 pub system_prompt: Option<String>,
758 pub render_metadata: Option<RenderMetadata>,
760 pub handling_mode: HandlingMode,
767 pub event_tx: Option<mpsc::Sender<EventEnvelope<AgentEvent>>>,
769 pub skill_references: Option<Vec<crate::skills::SkillKey>>,
771 pub flow_tool_overlay: Option<TurnToolOverlay>,
773 pub pre_turn_context_appends: Vec<PendingSystemContextAppend>,
776 pub turn_metadata: Option<RuntimeTurnMetadata>,
782}
783
784#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
786pub struct AppendSystemContextRequest {
787 pub text: String,
788 #[serde(default, skip_serializing_if = "Option::is_none")]
789 pub source: Option<String>,
790 #[serde(default, skip_serializing_if = "Option::is_none")]
791 pub idempotency_key: Option<String>,
792}
793
794#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
796pub struct AppendSystemContextResult {
797 pub status: AppendSystemContextStatus,
798}
799
800#[derive(Debug, Clone, Serialize, Deserialize)]
802pub struct StageToolResultsRequest {
803 pub results: Vec<crate::ToolResult>,
804}
805
806#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
808pub struct StageToolResultsResult {
809 pub accepted_result_count: usize,
810}
811
812#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
814#[serde(rename_all = "snake_case")]
815pub enum AppendSystemContextStatus {
816 Applied,
817 Staged,
818 Duplicate,
819}
820
821#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
823#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
824pub struct TurnToolOverlay {
825 #[serde(default)]
827 pub allowed_tools: Option<Vec<String>>,
828 #[serde(default)]
830 pub blocked_tools: Option<Vec<String>>,
831}
832
833#[derive(Debug, Default)]
835pub struct SessionQuery {
836 pub limit: Option<usize>,
838 pub offset: Option<usize>,
840 pub labels: Option<BTreeMap<String, String>>,
842}
843
844#[derive(Debug, Clone, Serialize, Deserialize)]
848pub struct SessionSummary {
849 pub session_id: SessionId,
850 pub created_at: SystemTime,
851 pub updated_at: SystemTime,
852 pub message_count: usize,
853 pub total_tokens: u64,
854 pub is_active: bool,
855 #[serde(default, skip_serializing_if = "BTreeMap::is_empty")]
856 pub labels: BTreeMap<String, String>,
857}
858
859#[derive(Debug, Clone, Serialize, Deserialize)]
861pub struct SessionInfo {
862 pub session_id: SessionId,
863 pub created_at: SystemTime,
864 pub updated_at: SystemTime,
865 pub message_count: usize,
866 pub is_active: bool,
867 pub model: String,
868 pub provider: Provider,
869 pub last_assistant_text: Option<String>,
870 #[serde(default, skip_serializing_if = "BTreeMap::is_empty")]
871 pub labels: BTreeMap<String, String>,
872}
873
874#[derive(Debug, Clone, Serialize, Deserialize)]
876pub struct SessionUsage {
877 pub total_tokens: u64,
878 pub usage: Usage,
879}
880
881#[derive(Debug, Clone, Serialize, Deserialize)]
884pub struct SessionView {
885 pub state: SessionInfo,
886 pub billing: SessionUsage,
887}
888
889impl SessionView {
890 pub fn session_id(&self) -> &SessionId {
892 &self.state.session_id
893 }
894}
895
896#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq, Eq)]
898pub struct SessionHistoryQuery {
899 pub offset: usize,
901 #[serde(default, skip_serializing_if = "Option::is_none")]
903 pub limit: Option<usize>,
904}
905
906#[derive(Debug, Clone, Serialize, Deserialize)]
908pub struct SessionHistoryPage {
909 pub session_id: SessionId,
910 pub message_count: usize,
911 pub offset: usize,
912 #[serde(default, skip_serializing_if = "Option::is_none")]
913 pub limit: Option<usize>,
914 pub has_more: bool,
915 pub messages: Vec<Message>,
916}
917
918impl SessionHistoryPage {
919 pub fn from_messages(
921 session_id: SessionId,
922 messages: &[Message],
923 query: SessionHistoryQuery,
924 ) -> Self {
925 let message_count = messages.len();
926 let start = query.offset.min(message_count);
927 let end = match query.limit {
928 Some(limit) => start.saturating_add(limit).min(message_count),
929 None => message_count,
930 };
931 Self {
932 session_id,
933 message_count,
934 offset: start,
935 limit: query.limit,
936 has_more: end < message_count,
937 messages: messages[start..end].to_vec(),
938 }
939 }
940}
941
942#[cfg_attr(target_arch = "wasm32", async_trait(?Send))]
947#[cfg_attr(not(target_arch = "wasm32"), async_trait)]
948pub trait SessionService: Send + Sync {
949 async fn create_session(&self, req: CreateSessionRequest) -> Result<RunResult, SessionError>;
951
952 async fn start_turn(
954 &self,
955 id: &SessionId,
956 req: StartTurnRequest,
957 ) -> Result<RunResult, SessionError>;
958
959 async fn interrupt(&self, id: &SessionId) -> Result<(), SessionError>;
963
964 async fn cancel_after_boundary(&self, _id: &SessionId) -> Result<(), SessionError> {
968 Err(SessionError::Unsupported(
969 "cancel_after_boundary".to_string(),
970 ))
971 }
972
973 async fn set_session_client(
980 &self,
981 _id: &SessionId,
982 _client: std::sync::Arc<dyn crate::AgentLlmClient>,
983 ) -> Result<(), SessionError> {
984 Err(SessionError::Unsupported("set_session_client".to_string()))
985 }
986
987 async fn hot_swap_session_llm_identity(
994 &self,
995 _id: &SessionId,
996 _client: std::sync::Arc<dyn crate::AgentLlmClient>,
997 _identity: SessionLlmIdentity,
998 _request_policy: crate::SessionLlmRequestPolicy,
999 ) -> Result<(), SessionError> {
1000 Err(SessionError::Unsupported(
1001 "hot_swap_session_llm_identity".to_string(),
1002 ))
1003 }
1004
1005 async fn set_session_tool_visibility_state(
1010 &self,
1011 _id: &SessionId,
1012 _state: Option<crate::SessionToolVisibilityState>,
1013 ) -> Result<(), SessionError> {
1014 Err(SessionError::Unsupported(
1015 "set_session_tool_visibility_state".to_string(),
1016 ))
1017 }
1018
1019 async fn update_session_keep_alive(
1025 &self,
1026 _id: &SessionId,
1027 _keep_alive: bool,
1028 ) -> Result<(), SessionError> {
1029 Err(SessionError::Unsupported(
1030 "update_session_keep_alive".to_string(),
1031 ))
1032 }
1033
1034 async fn update_session_mob_authority_context(
1040 &self,
1041 _id: &SessionId,
1042 _authority_context: Option<MobToolAuthorityContext>,
1043 ) -> Result<(), SessionError> {
1044 Err(SessionError::Unsupported(
1045 "update_session_mob_authority_context".to_string(),
1046 ))
1047 }
1048
1049 async fn has_live_session(&self, _id: &SessionId) -> Result<bool, SessionError> {
1055 Err(SessionError::Unsupported("has_live_session".to_string()))
1056 }
1057
1058 async fn set_session_tool_filter(
1064 &self,
1065 _id: &SessionId,
1066 _filter: crate::ToolFilter,
1067 ) -> Result<(), SessionError> {
1068 Err(SessionError::Unsupported(
1069 "set_session_tool_filter".to_string(),
1070 ))
1071 }
1072
1073 async fn read(&self, id: &SessionId) -> Result<SessionView, SessionError>;
1075
1076 async fn list(&self, query: SessionQuery) -> Result<Vec<SessionSummary>, SessionError>;
1078
1079 async fn archive(&self, id: &SessionId) -> Result<(), SessionError>;
1081
1082 async fn subscribe_session_events(&self, id: &SessionId) -> Result<EventStream, StreamError> {
1086 Err(StreamError::NotFound(format!("session {id}")))
1087 }
1088}
1089
1090#[cfg_attr(target_arch = "wasm32", async_trait(?Send))]
1096#[cfg_attr(not(target_arch = "wasm32"), async_trait)]
1097pub trait SessionServiceCommsExt: SessionService {
1098 async fn comms_runtime(
1100 &self,
1101 _session_id: &SessionId,
1102 ) -> Option<Arc<dyn crate::agent::CommsRuntime>> {
1103 None
1104 }
1105
1106 async fn event_injector(
1108 &self,
1109 session_id: &SessionId,
1110 ) -> Option<Arc<dyn crate::EventInjector>> {
1111 self.comms_runtime(session_id)
1112 .await
1113 .and_then(|runtime| runtime.event_injector())
1114 }
1115
1116 #[doc(hidden)]
1118 async fn interaction_event_injector(
1119 &self,
1120 session_id: &SessionId,
1121 ) -> Option<Arc<dyn crate::event_injector::SubscribableInjector>> {
1122 self.comms_runtime(session_id)
1123 .await
1124 .and_then(|runtime| runtime.interaction_event_injector())
1125 }
1126}
1127
1128#[cfg_attr(target_arch = "wasm32", async_trait(?Send))]
1133#[cfg_attr(not(target_arch = "wasm32"), async_trait)]
1134pub trait SessionServiceControlExt: SessionService {
1135 async fn append_system_context(
1141 &self,
1142 id: &SessionId,
1143 req: AppendSystemContextRequest,
1144 ) -> Result<AppendSystemContextResult, SessionControlError>;
1145
1146 async fn stage_tool_results(
1152 &self,
1153 id: &SessionId,
1154 req: StageToolResultsRequest,
1155 ) -> Result<StageToolResultsResult, SessionError> {
1156 let _ = (id, req);
1157 Err(SessionError::Unsupported("stage_tool_results".to_string()))
1158 }
1159}
1160
1161#[cfg_attr(target_arch = "wasm32", async_trait(?Send))]
1166#[cfg_attr(not(target_arch = "wasm32"), async_trait)]
1167pub trait SessionServiceHistoryExt: SessionService {
1168 async fn read_history(
1173 &self,
1174 id: &SessionId,
1175 query: SessionHistoryQuery,
1176 ) -> Result<SessionHistoryPage, SessionError>;
1177}
1178
1179impl dyn SessionService {
1181 pub fn into_arc(self: Box<Self>) -> Arc<dyn SessionService> {
1183 Arc::from(self)
1184 }
1185}
1186
1187#[cfg(test)]
1188#[allow(
1189 clippy::unimplemented,
1190 clippy::unwrap_used,
1191 clippy::expect_used,
1192 clippy::panic
1193)]
1194mod tests {
1195 use super::*;
1196
1197 struct UnsupportedSessionService;
1198
1199 #[cfg_attr(target_arch = "wasm32", async_trait(?Send))]
1200 #[cfg_attr(not(target_arch = "wasm32"), async_trait)]
1201 impl SessionService for UnsupportedSessionService {
1202 async fn create_session(
1203 &self,
1204 _req: CreateSessionRequest,
1205 ) -> Result<RunResult, SessionError> {
1206 unimplemented!()
1207 }
1208
1209 async fn start_turn(
1210 &self,
1211 _id: &SessionId,
1212 _req: StartTurnRequest,
1213 ) -> Result<RunResult, SessionError> {
1214 unimplemented!()
1215 }
1216
1217 async fn interrupt(&self, _id: &SessionId) -> Result<(), SessionError> {
1218 unimplemented!()
1219 }
1220
1221 async fn read(&self, _id: &SessionId) -> Result<SessionView, SessionError> {
1222 unimplemented!()
1223 }
1224
1225 async fn list(&self, _query: SessionQuery) -> Result<Vec<SessionSummary>, SessionError> {
1226 unimplemented!()
1227 }
1228
1229 async fn archive(&self, _id: &SessionId) -> Result<(), SessionError> {
1230 unimplemented!()
1231 }
1232 }
1233
1234 #[tokio::test]
1235 async fn has_live_session_defaults_to_unsupported() {
1236 let service = UnsupportedSessionService;
1237 let err = service
1238 .has_live_session(&SessionId::new())
1239 .await
1240 .expect_err("default implementation should fail loudly");
1241 assert!(matches!(err, SessionError::Unsupported(name) if name == "has_live_session"));
1242 }
1243
1244 #[test]
1245 fn grant_manage_mob_in_place_adds_mob_id() {
1246 let mut ctx = MobToolAuthorityContext::create_only_generated();
1247 ctx.grant_manage_mob_in_place("mob-1".into());
1248 assert!(ctx.managed_mob_scope.contains("mob-1"));
1249 }
1250
1251 #[test]
1252 fn grant_manage_mob_in_place_is_idempotent() {
1253 let mut ctx = MobToolAuthorityContext::create_only_generated();
1254 ctx.grant_manage_mob_in_place("mob-1".into());
1255 ctx.grant_manage_mob_in_place("mob-1".into());
1256 assert_eq!(ctx.managed_mob_scope.len(), 1);
1257 }
1258
1259 #[test]
1260 fn grant_manage_mob_in_place_accumulates() {
1261 let mut ctx = MobToolAuthorityContext::create_only_generated();
1262 ctx.grant_manage_mob_in_place("mob-1".into());
1263 ctx.grant_manage_mob_in_place("mob-2".into());
1264 assert!(ctx.managed_mob_scope.contains("mob-1"));
1265 assert!(ctx.managed_mob_scope.contains("mob-2"));
1266 assert_eq!(ctx.managed_mob_scope.len(), 2);
1267 }
1268
1269 struct MockSnapshotProvider {
1270 tools: Vec<Arc<ToolDef>>,
1271 }
1272
1273 impl VisibleToolSnapshotProvider for MockSnapshotProvider {
1274 fn snapshot_visible_tools(&self) -> Vec<Arc<ToolDef>> {
1275 self.tools.clone()
1276 }
1277 }
1278
1279 #[test]
1280 fn mob_tool_snapshot_context_standalone() {
1281 let ctx = MobToolSnapshotContext::Standalone;
1282 assert!(matches!(ctx, MobToolSnapshotContext::Standalone));
1283 }
1284
1285 #[test]
1286 fn mob_tool_snapshot_context_parent_owned_returns_tools() {
1287 let tools = vec![Arc::new(ToolDef {
1288 name: "test_tool".into(),
1289 description: "a test".to_string(),
1290 input_schema: serde_json::json!({"type": "object"}),
1291 provenance: None,
1292 })];
1293 let provider = Arc::new(MockSnapshotProvider { tools });
1294 let ctx = MobToolSnapshotContext::ParentOwned(provider);
1295 match ctx {
1296 MobToolSnapshotContext::ParentOwned(p) => {
1297 let snapshot = p.snapshot_visible_tools();
1298 assert_eq!(snapshot.len(), 1);
1299 assert_eq!(snapshot[0].name, "test_tool");
1300 }
1301 MobToolSnapshotContext::Standalone => panic!("expected ParentOwned"),
1302 }
1303 }
1304}