1pub mod transport;
7
8use crate::event::AgentEvent;
9use crate::event::EventEnvelope;
10use crate::session::SystemContextStageError;
11use crate::time_compat::SystemTime;
12#[cfg(target_arch = "wasm32")]
13use crate::tokio;
14use crate::types::{
15 ContentInput, HandlingMode, Message, RenderMetadata, RunResult, SessionId, ToolDef, Usage,
16};
17use crate::{
18 AgentToolDispatcher, BudgetLimits, HookRunOverrides, OutputSchema, PeerMeta, Provider, Session,
19 SessionLlmIdentity, ToolCategoryOverride,
20};
21use crate::{EventStream, StreamError};
22use async_trait::async_trait;
23use serde::{Deserialize, Serialize};
24use std::collections::BTreeMap;
25use std::collections::BTreeSet;
26use std::sync::Arc;
27use tokio::sync::mpsc;
28
29#[derive(Debug, Clone, Copy, PartialEq, Eq)]
31pub enum InitialTurnPolicy {
32 RunImmediately,
34 Defer,
39}
40
41#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)]
43#[serde(rename_all = "snake_case")]
44pub enum DeferredPromptPolicy {
45 #[default]
47 Discard,
48 Stage,
50}
51
52#[derive(Debug, thiserror::Error)]
54pub enum SessionError {
55 #[error("session not found: {id}")]
57 NotFound { id: SessionId },
58
59 #[error("session is busy: {id}")]
61 Busy { id: SessionId },
62
63 #[error("session persistence is disabled")]
65 PersistenceDisabled,
66
67 #[error("session compaction is disabled")]
69 CompactionDisabled,
70
71 #[error("no turn running on session: {id}")]
73 NotRunning { id: SessionId },
74
75 #[error("store error: {0}")]
77 Store(#[source] Box<dyn std::error::Error + Send + Sync>),
78
79 #[error("agent error: {0}")]
81 Agent(#[from] crate::error::AgentError),
82
83 #[error("unsupported: {0}")]
85 Unsupported(String),
86}
87
88impl SessionError {
89 pub fn code(&self) -> &'static str {
91 match self {
92 Self::NotFound { .. } => "SESSION_NOT_FOUND",
93 Self::Busy { .. } => "SESSION_BUSY",
94 Self::PersistenceDisabled => "SESSION_PERSISTENCE_DISABLED",
95 Self::CompactionDisabled => "SESSION_COMPACTION_DISABLED",
96 Self::NotRunning { .. } => "SESSION_NOT_RUNNING",
97 Self::Store(_) => "SESSION_STORE_ERROR",
98 Self::Unsupported(_) => "SESSION_UNSUPPORTED",
99 Self::Agent(_) => "AGENT_ERROR",
100 }
101 }
102}
103
104#[derive(Debug, thiserror::Error)]
106pub enum SessionControlError {
107 #[error(transparent)]
109 Session(#[from] SessionError),
110
111 #[error("invalid system-context request: {message}")]
113 InvalidRequest { message: String },
114
115 #[error(
117 "system-context idempotency conflict on session {id}: key '{key}' already maps to different content"
118 )]
119 Conflict { id: SessionId, key: String },
120}
121
122impl SessionControlError {
123 pub fn code(&self) -> &'static str {
125 match self {
126 Self::Session(err) => err.code(),
127 Self::InvalidRequest { .. } => "INVALID_PARAMS",
128 Self::Conflict { .. } => "SESSION_SYSTEM_CONTEXT_CONFLICT",
129 }
130 }
131}
132
133impl SystemContextStageError {
134 pub fn into_control_error(self, id: &SessionId) -> SessionControlError {
136 match self {
137 Self::InvalidRequest(message) => SessionControlError::InvalidRequest { message },
138 Self::Conflict { key, .. } => SessionControlError::Conflict {
139 id: id.clone(),
140 key,
141 },
142 }
143 }
144}
145
146#[derive(Debug)]
148pub struct CreateSessionRequest {
149 pub model: String,
151 pub prompt: ContentInput,
153 pub render_metadata: Option<RenderMetadata>,
155 pub system_prompt: Option<String>,
157 pub max_tokens: Option<u32>,
159 pub event_tx: Option<mpsc::Sender<EventEnvelope<AgentEvent>>>,
161 pub skill_references: Option<Vec<crate::skills::SkillKey>>,
163 pub initial_turn: InitialTurnPolicy,
165 pub deferred_prompt_policy: DeferredPromptPolicy,
167 pub build: Option<SessionBuildOptions>,
169 pub labels: Option<BTreeMap<String, String>>,
171}
172
173#[derive(Clone)]
175pub struct SessionBuildOptions {
176 pub provider: Option<Provider>,
177 pub self_hosted_server_id: Option<String>,
178 pub output_schema: Option<OutputSchema>,
179 pub structured_output_retries: u32,
180 pub hooks_override: HookRunOverrides,
181 pub comms_name: Option<String>,
182 pub peer_meta: Option<PeerMeta>,
183 pub resume_session: Option<Session>,
184 pub budget_limits: Option<BudgetLimits>,
185 pub provider_params: Option<serde_json::Value>,
186 pub external_tools: Option<Arc<dyn AgentToolDispatcher>>,
187 pub recoverable_tool_defs: Option<Vec<crate::ToolDef>>,
190 pub blob_store_override: Option<Arc<dyn crate::BlobStore>>,
193 pub llm_client_override: Option<Arc<dyn std::any::Any + Send + Sync>>,
197 pub override_builtins: ToolCategoryOverride,
200 pub override_shell: ToolCategoryOverride,
201 pub override_memory: ToolCategoryOverride,
202 pub override_schedule: ToolCategoryOverride,
204 pub override_mob: ToolCategoryOverride,
205 pub schedule_tools: Option<Arc<dyn AgentToolDispatcher>>,
210 pub preload_skills: Option<Vec<crate::skills::SkillId>>,
211 pub realm_id: Option<String>,
212 pub instance_id: Option<String>,
213 pub backend: Option<String>,
214 pub config_generation: Option<u64>,
215 pub keep_alive: bool,
218 pub checkpointer: Option<std::sync::Arc<dyn crate::checkpoint::SessionCheckpointer>>,
220 pub silent_comms_intents: Vec<String>,
223 pub max_inline_peer_notifications: Option<i32>,
231 pub app_context: Option<serde_json::Value>,
238 pub additional_instructions: Option<Vec<String>>,
241 pub shell_env: Option<std::collections::HashMap<String, String>>,
245 pub call_timeout_override: crate::CallTimeoutOverride,
251 pub resume_override_mask: ResumeOverrideMask,
257 pub mob_tools: Option<Arc<dyn MobToolsFactory>>,
265 pub runtime_build_mode: crate::runtime_epoch::RuntimeBuildMode,
273 pub mob_tool_authority_context: Option<MobToolAuthorityContext>,
280}
281
282#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
288pub struct OpaquePrincipalToken(String);
289
290impl OpaquePrincipalToken {
291 pub fn new(token: impl Into<String>) -> Self {
292 Self(token.into())
293 }
294
295 pub fn generated() -> Self {
296 Self(uuid::Uuid::new_v4().to_string())
297 }
298
299 pub fn as_str(&self) -> &str {
300 &self.0
301 }
302}
303
304impl std::fmt::Display for OpaquePrincipalToken {
305 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
306 f.write_str(self.as_str())
307 }
308}
309
310#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, Default)]
315pub struct MobToolCallerProvenance {
316 #[serde(default, skip_serializing_if = "Option::is_none")]
317 caller_session_id: Option<crate::SessionId>,
318 #[serde(default, skip_serializing_if = "Option::is_none")]
319 caller_mob_id: Option<String>,
320 #[serde(default, skip_serializing_if = "Option::is_none")]
321 caller_member_id: Option<String>,
322}
323
324impl MobToolCallerProvenance {
325 pub fn new() -> Self {
326 Self::default()
327 }
328
329 pub fn with_session_id(mut self, session_id: crate::SessionId) -> Self {
330 self.caller_session_id = Some(session_id);
331 self
332 }
333
334 pub fn with_mob_id(mut self, mob_id: impl Into<String>) -> Self {
335 self.caller_mob_id = Some(mob_id.into());
336 self
337 }
338
339 pub fn with_member_id(mut self, member_id: impl Into<String>) -> Self {
340 self.caller_member_id = Some(member_id.into());
341 self
342 }
343
344 pub fn caller_session_id(&self) -> Option<&crate::SessionId> {
345 self.caller_session_id.as_ref()
346 }
347
348 pub fn caller_mob_id(&self) -> Option<&str> {
349 self.caller_mob_id.as_deref()
350 }
351
352 pub fn caller_member_id(&self) -> Option<&str> {
353 self.caller_member_id.as_deref()
354 }
355}
356
357#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
363pub struct MobToolAuthorityContext {
364 principal_token: OpaquePrincipalToken,
365 can_create_mobs: bool,
366 #[serde(default, skip_serializing_if = "BTreeSet::is_empty")]
367 managed_mob_scope: BTreeSet<String>,
368 #[serde(default, skip_serializing_if = "Option::is_none")]
369 caller_provenance: Option<MobToolCallerProvenance>,
370 #[serde(default, skip_serializing_if = "Option::is_none")]
371 audit_invocation_id: Option<String>,
372}
373
374impl MobToolAuthorityContext {
375 pub fn new(principal_token: OpaquePrincipalToken, can_create_mobs: bool) -> Self {
376 Self {
377 principal_token,
378 can_create_mobs,
379 managed_mob_scope: BTreeSet::new(),
380 caller_provenance: None,
381 audit_invocation_id: None,
382 }
383 }
384
385 pub fn create_only_generated() -> Self {
386 Self::new(OpaquePrincipalToken::generated(), true)
387 }
388
389 pub fn principal_token(&self) -> &OpaquePrincipalToken {
390 &self.principal_token
391 }
392
393 pub fn can_create_mobs(&self) -> bool {
394 self.can_create_mobs
395 }
396
397 pub fn managed_mob_scope(&self) -> &BTreeSet<String> {
398 &self.managed_mob_scope
399 }
400
401 pub fn caller_provenance(&self) -> Option<&MobToolCallerProvenance> {
402 self.caller_provenance.as_ref()
403 }
404
405 pub fn audit_invocation_id(&self) -> Option<&str> {
406 self.audit_invocation_id.as_deref()
407 }
408
409 pub fn can_manage_mob(&self, mob_id: &str) -> bool {
410 self.managed_mob_scope.contains(mob_id)
411 }
412
413 pub fn grant_manage_mob(mut self, mob_id: impl Into<String>) -> Self {
414 self.managed_mob_scope.insert(mob_id.into());
415 self
416 }
417
418 pub fn grant_manage_mob_in_place(&mut self, mob_id: String) {
423 self.managed_mob_scope.insert(mob_id);
424 }
425
426 pub fn with_managed_mob_scope<I, S>(mut self, mob_ids: I) -> Self
427 where
428 I: IntoIterator<Item = S>,
429 S: Into<String>,
430 {
431 self.managed_mob_scope = mob_ids.into_iter().map(Into::into).collect();
432 self
433 }
434
435 pub fn with_caller_provenance(mut self, caller_provenance: MobToolCallerProvenance) -> Self {
436 self.caller_provenance = Some(caller_provenance);
437 self
438 }
439
440 pub fn with_audit_invocation_id(mut self, audit_invocation_id: impl Into<String>) -> Self {
441 self.audit_invocation_id = Some(audit_invocation_id.into());
442 self
443 }
444}
445
446pub fn generated_create_only_mob_operator_authority(
452 enable_mob: ToolCategoryOverride,
453) -> Option<MobToolAuthorityContext> {
454 matches!(enable_mob, ToolCategoryOverride::Enable)
455 .then(MobToolAuthorityContext::create_only_generated)
456}
457
458pub fn resolve_mob_operator_access(
464 enable_mob: ToolCategoryOverride,
465 persisted_authority_context: Option<MobToolAuthorityContext>,
466) -> (ToolCategoryOverride, Option<MobToolAuthorityContext>) {
467 if matches!(enable_mob, ToolCategoryOverride::Disable) {
468 return (ToolCategoryOverride::Disable, None);
469 }
470
471 let authority_context = persisted_authority_context
472 .or_else(|| generated_create_only_mob_operator_authority(enable_mob));
473 let override_mob = if authority_context.is_some() {
474 ToolCategoryOverride::Enable
475 } else {
476 enable_mob
477 };
478
479 (override_mob, authority_context)
480}
481
482pub trait VisibleToolSnapshotProvider: Send + Sync {
487 fn snapshot_visible_tools(&self) -> Vec<Arc<ToolDef>>;
489}
490
491pub enum MobToolSnapshotContext {
497 ParentOwned(Arc<dyn VisibleToolSnapshotProvider>),
499 Standalone,
501}
502
503pub struct MobToolsBuildArgs {
505 pub session_id: crate::SessionId,
507 pub model: String,
509 pub authority_context: Option<MobToolAuthorityContext>,
515 pub effective_authority: Option<Arc<std::sync::RwLock<MobToolAuthorityContext>>>,
522 pub comms_name: Option<String>,
524 pub comms_runtime: Option<Arc<dyn crate::agent::CommsRuntime>>,
526 pub snapshot_context: MobToolSnapshotContext,
528}
529
530#[cfg_attr(target_arch = "wasm32", async_trait(?Send))]
536#[cfg_attr(not(target_arch = "wasm32"), async_trait)]
537pub trait MobToolsFactory: Send + Sync {
538 async fn build_mob_tools(
540 &self,
541 args: MobToolsBuildArgs,
542 ) -> Result<Arc<dyn AgentToolDispatcher>, Box<dyn std::error::Error + Send + Sync>>;
543}
544
545#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
549pub struct ResumeOverrideMask {
550 pub model: bool,
551 pub provider: bool,
552 pub max_tokens: bool,
553 pub structured_output_retries: bool,
554 pub provider_params: bool,
555 pub override_builtins: bool,
556 pub override_shell: bool,
557 pub override_memory: bool,
558 pub override_mob: bool,
559 pub preload_skills: bool,
560 pub keep_alive: bool,
561 pub comms_name: bool,
562 pub peer_meta: bool,
563}
564
565impl SessionBuildOptions {
566 pub fn apply_persisted_mob_operator_access(
572 &mut self,
573 enable_mob: ToolCategoryOverride,
574 persisted_authority_context: Option<MobToolAuthorityContext>,
575 ) {
576 let (override_mob, authority_context) =
577 resolve_mob_operator_access(enable_mob, persisted_authority_context);
578 self.override_mob = override_mob;
579 self.mob_tool_authority_context = authority_context;
580 }
581
582 pub fn apply_generated_create_only_mob_operator_access(
589 &mut self,
590 enable_mob: ToolCategoryOverride,
591 ) {
592 self.apply_persisted_mob_operator_access(enable_mob, None);
593 }
594}
595
596impl Default for SessionBuildOptions {
597 fn default() -> Self {
598 Self {
599 provider: None,
600 self_hosted_server_id: None,
601 output_schema: None,
602 structured_output_retries: 2,
603 hooks_override: HookRunOverrides::default(),
604 comms_name: None,
605 peer_meta: None,
606 resume_session: None,
607 budget_limits: None,
608 provider_params: None,
609 external_tools: None,
610 recoverable_tool_defs: None,
611 blob_store_override: None,
612 llm_client_override: None,
613 override_builtins: ToolCategoryOverride::Inherit,
614 override_shell: ToolCategoryOverride::Inherit,
615 override_memory: ToolCategoryOverride::Inherit,
616 override_schedule: ToolCategoryOverride::Inherit,
617 override_mob: ToolCategoryOverride::Inherit,
618 schedule_tools: None,
619 preload_skills: None,
620 realm_id: None,
621 instance_id: None,
622 backend: None,
623 config_generation: None,
624 keep_alive: false,
625 checkpointer: None,
626 silent_comms_intents: Vec::new(),
627 max_inline_peer_notifications: None,
628 app_context: None,
629 additional_instructions: None,
630 shell_env: None,
631 call_timeout_override: crate::CallTimeoutOverride::Inherit,
632 resume_override_mask: ResumeOverrideMask::default(),
633 mob_tools: None,
634 runtime_build_mode: crate::runtime_epoch::RuntimeBuildMode::StandaloneEphemeral,
635 mob_tool_authority_context: None,
636 }
637 }
638}
639
640impl std::fmt::Debug for SessionBuildOptions {
641 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
642 f.debug_struct("SessionBuildOptions")
643 .field("provider", &self.provider)
644 .field("output_schema", &self.output_schema.is_some())
645 .field("structured_output_retries", &self.structured_output_retries)
646 .field("hooks_override", &self.hooks_override)
647 .field("comms_name", &self.comms_name)
648 .field("peer_meta", &self.peer_meta)
649 .field("resume_session", &self.resume_session.is_some())
650 .field("budget_limits", &self.budget_limits)
651 .field("provider_params", &self.provider_params.is_some())
652 .field("external_tools", &self.external_tools.is_some())
653 .field("recoverable_tool_defs", &self.recoverable_tool_defs)
654 .field("blob_store_override", &self.blob_store_override.is_some())
655 .field("llm_client_override", &self.llm_client_override.is_some())
656 .field("override_builtins", &self.override_builtins)
657 .field("override_shell", &self.override_shell)
658 .field("override_memory", &self.override_memory)
659 .field("override_schedule", &self.override_schedule)
660 .field("override_mob", &self.override_mob)
661 .field("schedule_tools", &self.schedule_tools.is_some())
662 .field("preload_skills", &self.preload_skills)
663 .field("realm_id", &self.realm_id)
664 .field("instance_id", &self.instance_id)
665 .field("backend", &self.backend)
666 .field("config_generation", &self.config_generation)
667 .field("keep_alive", &self.keep_alive)
668 .field("checkpointer", &self.checkpointer.is_some())
669 .field("silent_comms_intents", &self.silent_comms_intents)
670 .field(
671 "max_inline_peer_notifications",
672 &self.max_inline_peer_notifications,
673 )
674 .field("app_context", &self.app_context.is_some())
675 .field("additional_instructions", &self.additional_instructions)
676 .field("call_timeout_override", &self.call_timeout_override)
677 .field("resume_override_mask", &self.resume_override_mask)
678 .field("mob_tools", &self.mob_tools.is_some())
679 .field("runtime_build_mode", &self.runtime_build_mode)
680 .field(
681 "mob_tool_authority_context",
682 &self.mob_tool_authority_context.is_some(),
683 )
684 .field("runtime_build_mode", &self.runtime_build_mode)
685 .finish()
686 }
687}
688
689#[derive(Debug)]
691pub struct StartTurnRequest {
692 pub prompt: ContentInput,
694 pub system_prompt: Option<String>,
699 pub render_metadata: Option<RenderMetadata>,
701 pub handling_mode: HandlingMode,
708 pub event_tx: Option<mpsc::Sender<EventEnvelope<AgentEvent>>>,
710 pub skill_references: Option<Vec<crate::skills::SkillKey>>,
712 pub flow_tool_overlay: Option<TurnToolOverlay>,
714 pub additional_instructions: Option<Vec<String>>,
722 pub execution_kind: Option<crate::lifecycle::run_primitive::RuntimeExecutionKind>,
728}
729
730#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
732pub struct AppendSystemContextRequest {
733 pub text: String,
734 #[serde(default, skip_serializing_if = "Option::is_none")]
735 pub source: Option<String>,
736 #[serde(default, skip_serializing_if = "Option::is_none")]
737 pub idempotency_key: Option<String>,
738}
739
740#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
742pub struct AppendSystemContextResult {
743 pub status: AppendSystemContextStatus,
744}
745
746#[derive(Debug, Clone, Serialize, Deserialize)]
748pub struct StageToolResultsRequest {
749 pub results: Vec<crate::ToolResult>,
750}
751
752#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
754pub struct StageToolResultsResult {
755 pub accepted_result_count: usize,
756}
757
758#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
760#[serde(rename_all = "snake_case")]
761pub enum AppendSystemContextStatus {
762 Applied,
763 Staged,
764 Duplicate,
765}
766
767#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
769pub struct TurnToolOverlay {
770 #[serde(default)]
772 pub allowed_tools: Option<Vec<String>>,
773 #[serde(default)]
775 pub blocked_tools: Option<Vec<String>>,
776}
777
778#[derive(Debug, Default)]
780pub struct SessionQuery {
781 pub limit: Option<usize>,
783 pub offset: Option<usize>,
785 pub labels: Option<BTreeMap<String, String>>,
787}
788
789#[derive(Debug, Clone, Serialize, Deserialize)]
793pub struct SessionSummary {
794 pub session_id: SessionId,
795 pub created_at: SystemTime,
796 pub updated_at: SystemTime,
797 pub message_count: usize,
798 pub total_tokens: u64,
799 pub is_active: bool,
800 #[serde(default, skip_serializing_if = "BTreeMap::is_empty")]
801 pub labels: BTreeMap<String, String>,
802}
803
804#[derive(Debug, Clone, Serialize, Deserialize)]
806pub struct SessionInfo {
807 pub session_id: SessionId,
808 pub created_at: SystemTime,
809 pub updated_at: SystemTime,
810 pub message_count: usize,
811 pub is_active: bool,
812 pub model: String,
813 pub provider: Provider,
814 pub last_assistant_text: Option<String>,
815 #[serde(default, skip_serializing_if = "BTreeMap::is_empty")]
816 pub labels: BTreeMap<String, String>,
817}
818
819#[derive(Debug, Clone, Serialize, Deserialize)]
821pub struct SessionUsage {
822 pub total_tokens: u64,
823 pub usage: Usage,
824}
825
826#[derive(Debug, Clone, Serialize, Deserialize)]
829pub struct SessionView {
830 pub state: SessionInfo,
831 pub billing: SessionUsage,
832}
833
834impl SessionView {
835 pub fn session_id(&self) -> &SessionId {
837 &self.state.session_id
838 }
839}
840
841#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq, Eq)]
843pub struct SessionHistoryQuery {
844 pub offset: usize,
846 #[serde(default, skip_serializing_if = "Option::is_none")]
848 pub limit: Option<usize>,
849}
850
851#[derive(Debug, Clone, Serialize, Deserialize)]
853pub struct SessionHistoryPage {
854 pub session_id: SessionId,
855 pub message_count: usize,
856 pub offset: usize,
857 #[serde(default, skip_serializing_if = "Option::is_none")]
858 pub limit: Option<usize>,
859 pub has_more: bool,
860 pub messages: Vec<Message>,
861}
862
863impl SessionHistoryPage {
864 pub fn from_messages(
866 session_id: SessionId,
867 messages: &[Message],
868 query: SessionHistoryQuery,
869 ) -> Self {
870 let message_count = messages.len();
871 let start = query.offset.min(message_count);
872 let end = match query.limit {
873 Some(limit) => start.saturating_add(limit).min(message_count),
874 None => message_count,
875 };
876 Self {
877 session_id,
878 message_count,
879 offset: start,
880 limit: query.limit,
881 has_more: end < message_count,
882 messages: messages[start..end].to_vec(),
883 }
884 }
885}
886
887#[cfg_attr(target_arch = "wasm32", async_trait(?Send))]
892#[cfg_attr(not(target_arch = "wasm32"), async_trait)]
893pub trait SessionService: Send + Sync {
894 async fn create_session(&self, req: CreateSessionRequest) -> Result<RunResult, SessionError>;
896
897 async fn start_turn(
899 &self,
900 id: &SessionId,
901 req: StartTurnRequest,
902 ) -> Result<RunResult, SessionError>;
903
904 async fn interrupt(&self, id: &SessionId) -> Result<(), SessionError>;
908
909 async fn set_session_client(
916 &self,
917 _id: &SessionId,
918 _client: std::sync::Arc<dyn crate::AgentLlmClient>,
919 ) -> Result<(), SessionError> {
920 Err(SessionError::Unsupported("set_session_client".to_string()))
921 }
922
923 async fn hot_swap_session_llm_identity(
930 &self,
931 _id: &SessionId,
932 _client: std::sync::Arc<dyn crate::AgentLlmClient>,
933 _identity: SessionLlmIdentity,
934 ) -> Result<(), SessionError> {
935 Err(SessionError::Unsupported(
936 "hot_swap_session_llm_identity".to_string(),
937 ))
938 }
939
940 async fn update_session_keep_alive(
946 &self,
947 _id: &SessionId,
948 _keep_alive: bool,
949 ) -> Result<(), SessionError> {
950 Err(SessionError::Unsupported(
951 "update_session_keep_alive".to_string(),
952 ))
953 }
954
955 async fn update_session_mob_authority_context(
961 &self,
962 _id: &SessionId,
963 _authority_context: Option<MobToolAuthorityContext>,
964 ) -> Result<(), SessionError> {
965 Err(SessionError::Unsupported(
966 "update_session_mob_authority_context".to_string(),
967 ))
968 }
969
970 async fn has_live_session(&self, _id: &SessionId) -> Result<bool, SessionError> {
976 Err(SessionError::Unsupported("has_live_session".to_string()))
977 }
978
979 async fn set_session_tool_filter(
985 &self,
986 _id: &SessionId,
987 _filter: crate::ToolFilter,
988 ) -> Result<(), SessionError> {
989 Err(SessionError::Unsupported(
990 "set_session_tool_filter".to_string(),
991 ))
992 }
993
994 async fn read(&self, id: &SessionId) -> Result<SessionView, SessionError>;
996
997 async fn list(&self, query: SessionQuery) -> Result<Vec<SessionSummary>, SessionError>;
999
1000 async fn archive(&self, id: &SessionId) -> Result<(), SessionError>;
1002
1003 async fn subscribe_session_events(&self, id: &SessionId) -> Result<EventStream, StreamError> {
1007 Err(StreamError::NotFound(format!("session {id}")))
1008 }
1009}
1010
1011#[cfg_attr(target_arch = "wasm32", async_trait(?Send))]
1017#[cfg_attr(not(target_arch = "wasm32"), async_trait)]
1018pub trait SessionServiceCommsExt: SessionService {
1019 async fn comms_runtime(
1021 &self,
1022 _session_id: &SessionId,
1023 ) -> Option<Arc<dyn crate::agent::CommsRuntime>> {
1024 None
1025 }
1026
1027 async fn event_injector(
1029 &self,
1030 session_id: &SessionId,
1031 ) -> Option<Arc<dyn crate::EventInjector>> {
1032 self.comms_runtime(session_id)
1033 .await
1034 .and_then(|runtime| runtime.event_injector())
1035 }
1036
1037 #[doc(hidden)]
1039 async fn interaction_event_injector(
1040 &self,
1041 session_id: &SessionId,
1042 ) -> Option<Arc<dyn crate::event_injector::SubscribableInjector>> {
1043 self.comms_runtime(session_id)
1044 .await
1045 .and_then(|runtime| runtime.interaction_event_injector())
1046 }
1047}
1048
1049#[cfg_attr(target_arch = "wasm32", async_trait(?Send))]
1054#[cfg_attr(not(target_arch = "wasm32"), async_trait)]
1055pub trait SessionServiceControlExt: SessionService {
1056 async fn append_system_context(
1062 &self,
1063 id: &SessionId,
1064 req: AppendSystemContextRequest,
1065 ) -> Result<AppendSystemContextResult, SessionControlError>;
1066
1067 async fn stage_tool_results(
1073 &self,
1074 id: &SessionId,
1075 req: StageToolResultsRequest,
1076 ) -> Result<StageToolResultsResult, SessionError> {
1077 let _ = (id, req);
1078 Err(SessionError::Unsupported("stage_tool_results".to_string()))
1079 }
1080}
1081
1082#[cfg_attr(target_arch = "wasm32", async_trait(?Send))]
1087#[cfg_attr(not(target_arch = "wasm32"), async_trait)]
1088pub trait SessionServiceHistoryExt: SessionService {
1089 async fn read_history(
1094 &self,
1095 id: &SessionId,
1096 query: SessionHistoryQuery,
1097 ) -> Result<SessionHistoryPage, SessionError>;
1098}
1099
1100impl dyn SessionService {
1102 pub fn into_arc(self: Box<Self>) -> Arc<dyn SessionService> {
1104 Arc::from(self)
1105 }
1106}
1107
1108#[cfg(test)]
1109#[allow(
1110 clippy::unimplemented,
1111 clippy::unwrap_used,
1112 clippy::expect_used,
1113 clippy::panic
1114)]
1115mod tests {
1116 use super::*;
1117
1118 struct UnsupportedSessionService;
1119
1120 #[cfg_attr(target_arch = "wasm32", async_trait(?Send))]
1121 #[cfg_attr(not(target_arch = "wasm32"), async_trait)]
1122 impl SessionService for UnsupportedSessionService {
1123 async fn create_session(
1124 &self,
1125 _req: CreateSessionRequest,
1126 ) -> Result<RunResult, SessionError> {
1127 unimplemented!()
1128 }
1129
1130 async fn start_turn(
1131 &self,
1132 _id: &SessionId,
1133 _req: StartTurnRequest,
1134 ) -> Result<RunResult, SessionError> {
1135 unimplemented!()
1136 }
1137
1138 async fn interrupt(&self, _id: &SessionId) -> Result<(), SessionError> {
1139 unimplemented!()
1140 }
1141
1142 async fn read(&self, _id: &SessionId) -> Result<SessionView, SessionError> {
1143 unimplemented!()
1144 }
1145
1146 async fn list(&self, _query: SessionQuery) -> Result<Vec<SessionSummary>, SessionError> {
1147 unimplemented!()
1148 }
1149
1150 async fn archive(&self, _id: &SessionId) -> Result<(), SessionError> {
1151 unimplemented!()
1152 }
1153 }
1154
1155 #[tokio::test]
1156 async fn has_live_session_defaults_to_unsupported() {
1157 let service = UnsupportedSessionService;
1158 let err = service
1159 .has_live_session(&SessionId::new())
1160 .await
1161 .expect_err("default implementation should fail loudly");
1162 assert!(matches!(err, SessionError::Unsupported(name) if name == "has_live_session"));
1163 }
1164
1165 #[test]
1166 fn grant_manage_mob_in_place_adds_mob_id() {
1167 let mut ctx = MobToolAuthorityContext::create_only_generated();
1168 ctx.grant_manage_mob_in_place("mob-1".into());
1169 assert!(ctx.managed_mob_scope.contains("mob-1"));
1170 }
1171
1172 #[test]
1173 fn grant_manage_mob_in_place_is_idempotent() {
1174 let mut ctx = MobToolAuthorityContext::create_only_generated();
1175 ctx.grant_manage_mob_in_place("mob-1".into());
1176 ctx.grant_manage_mob_in_place("mob-1".into());
1177 assert_eq!(ctx.managed_mob_scope.len(), 1);
1178 }
1179
1180 #[test]
1181 fn grant_manage_mob_in_place_accumulates() {
1182 let mut ctx = MobToolAuthorityContext::create_only_generated();
1183 ctx.grant_manage_mob_in_place("mob-1".into());
1184 ctx.grant_manage_mob_in_place("mob-2".into());
1185 assert!(ctx.managed_mob_scope.contains("mob-1"));
1186 assert!(ctx.managed_mob_scope.contains("mob-2"));
1187 assert_eq!(ctx.managed_mob_scope.len(), 2);
1188 }
1189
1190 struct MockSnapshotProvider {
1191 tools: Vec<Arc<ToolDef>>,
1192 }
1193
1194 impl VisibleToolSnapshotProvider for MockSnapshotProvider {
1195 fn snapshot_visible_tools(&self) -> Vec<Arc<ToolDef>> {
1196 self.tools.clone()
1197 }
1198 }
1199
1200 #[test]
1201 fn mob_tool_snapshot_context_standalone() {
1202 let ctx = MobToolSnapshotContext::Standalone;
1203 assert!(matches!(ctx, MobToolSnapshotContext::Standalone));
1204 }
1205
1206 #[test]
1207 fn mob_tool_snapshot_context_parent_owned_returns_tools() {
1208 let tools = vec![Arc::new(ToolDef {
1209 name: "test_tool".to_string(),
1210 description: "a test".to_string(),
1211 input_schema: serde_json::json!({"type": "object"}),
1212 provenance: None,
1213 })];
1214 let provider = Arc::new(MockSnapshotProvider { tools });
1215 let ctx = MobToolSnapshotContext::ParentOwned(provider);
1216 match ctx {
1217 MobToolSnapshotContext::ParentOwned(p) => {
1218 let snapshot = p.snapshot_visible_tools();
1219 assert_eq!(snapshot.len(), 1);
1220 assert_eq!(snapshot[0].name, "test_tool");
1221 }
1222 MobToolSnapshotContext::Standalone => panic!("expected ParentOwned"),
1223 }
1224 }
1225}