1pub mod transport;
7
8use crate::event::AgentEvent;
9use crate::event::EventEnvelope;
10use crate::lifecycle::run_primitive::{ConversationAppend, 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
30pub use crate::session::{TranscriptEditError, TranscriptReplacement};
31
32#[derive(Debug, Clone, Copy, PartialEq, Eq)]
34pub enum InitialTurnPolicy {
35 RunImmediately,
37 Defer,
42}
43
44#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)]
46#[serde(rename_all = "snake_case")]
47pub enum DeferredPromptPolicy {
48 #[default]
50 Discard,
51 Stage,
53}
54
55#[derive(Debug, thiserror::Error)]
57pub enum SessionError {
58 #[error("session not found: {id}")]
60 NotFound { id: SessionId },
61
62 #[error("session is busy: {id}")]
64 Busy { id: SessionId },
65
66 #[error("session persistence is disabled")]
68 PersistenceDisabled,
69
70 #[error("session compaction is disabled")]
72 CompactionDisabled,
73
74 #[error("no turn running on session: {id}")]
76 NotRunning { id: SessionId },
77
78 #[error("store error: {0}")]
80 Store(#[source] Box<dyn std::error::Error + Send + Sync>),
81
82 #[error("agent error: {0}")]
84 Agent(#[from] crate::error::AgentError),
85
86 #[error("{message}")]
88 FailedWithData {
89 message: String,
90 data: serde_json::Value,
91 },
92
93 #[error("unsupported: {0}")]
95 Unsupported(String),
96}
97
98impl SessionError {
99 pub fn code(&self) -> &'static str {
101 match self {
102 Self::NotFound { .. } => "SESSION_NOT_FOUND",
103 Self::Busy { .. } => "SESSION_BUSY",
104 Self::PersistenceDisabled => "SESSION_PERSISTENCE_DISABLED",
105 Self::CompactionDisabled => "SESSION_COMPACTION_DISABLED",
106 Self::NotRunning { .. } => "SESSION_NOT_RUNNING",
107 Self::Store(_) => "SESSION_STORE_ERROR",
108 Self::Unsupported(_) => "SESSION_UNSUPPORTED",
109 Self::Agent(_) => "AGENT_ERROR",
110 Self::FailedWithData { .. } => "SESSION_ERROR",
111 }
112 }
113
114 pub fn structured_data(&self) -> Option<serde_json::Value> {
115 match self {
116 Self::FailedWithData { data, .. } => Some(data.clone()),
117 _ => None,
118 }
119 }
120}
121
122#[derive(Debug, thiserror::Error)]
124pub enum SessionControlError {
125 #[error(transparent)]
127 Session(#[from] SessionError),
128
129 #[error("invalid system-context request: {message}")]
131 InvalidRequest { message: String },
132
133 #[error(
135 "system-context idempotency conflict on session {id}: key '{key}' already maps to different content"
136 )]
137 Conflict { id: SessionId, key: String },
138}
139
140impl SessionControlError {
141 pub fn code(&self) -> &'static str {
143 match self {
144 Self::Session(err) => err.code(),
145 Self::InvalidRequest { .. } => "INVALID_PARAMS",
146 Self::Conflict { .. } => "SESSION_SYSTEM_CONTEXT_CONFLICT",
147 }
148 }
149}
150
151impl SystemContextStageError {
152 pub fn into_control_error(self, id: &SessionId) -> SessionControlError {
154 match self {
155 Self::InvalidRequest(message) => SessionControlError::InvalidRequest { message },
156 Self::Conflict { key, .. } => SessionControlError::Conflict {
157 id: id.clone(),
158 key,
159 },
160 }
161 }
162}
163
164#[derive(Debug)]
166pub struct CreateSessionRequest {
167 pub model: String,
169 pub prompt: ContentInput,
171 pub render_metadata: Option<RenderMetadata>,
173 pub system_prompt: Option<String>,
175 pub max_tokens: Option<u32>,
177 pub event_tx: Option<mpsc::Sender<EventEnvelope<AgentEvent>>>,
179 pub skill_references: Option<Vec<crate::skills::SkillKey>>,
181 pub initial_turn: InitialTurnPolicy,
183 pub deferred_prompt_policy: DeferredPromptPolicy,
185 pub build: Option<SessionBuildOptions>,
187 pub labels: Option<BTreeMap<String, String>>,
189}
190
191impl CreateSessionRequest {
192 #[must_use]
195 pub fn surface_metadata(&self) -> crate::SurfaceMetadata {
196 crate::SurfaceMetadata::from_optional_parts(
197 self.labels.clone(),
198 self.build
199 .as_ref()
200 .and_then(|build| build.app_context.clone()),
201 )
202 }
203}
204
205#[derive(Clone)]
207pub struct SessionBuildOptions {
208 pub provider: Option<Provider>,
209 pub self_hosted_server_id: Option<String>,
210 pub output_schema: Option<OutputSchema>,
211 pub structured_output_retries: u32,
212 pub hooks_override: HookRunOverrides,
213 pub comms_name: Option<String>,
214 pub peer_meta: Option<PeerMeta>,
215 pub resume_session: Option<Session>,
216 pub budget_limits: Option<BudgetLimits>,
217 pub provider_params: Option<serde_json::Value>,
218 pub external_tools: Option<Arc<dyn AgentToolDispatcher>>,
219 pub recoverable_tool_defs: Option<Vec<crate::ToolDef>>,
222 pub blob_store_override: Option<Arc<dyn crate::BlobStore>>,
225 pub llm_client_override: Option<Arc<dyn std::any::Any + Send + Sync>>,
229 pub agent_llm_client_decorator: Option<crate::AgentLlmClientDecorator>,
234 pub override_builtins: ToolCategoryOverride,
237 pub override_shell: ToolCategoryOverride,
238 pub override_memory: ToolCategoryOverride,
239 pub override_schedule: ToolCategoryOverride,
241 pub override_workgraph: ToolCategoryOverride,
243 pub override_mob: ToolCategoryOverride,
244 pub override_image_generation: ToolCategoryOverride,
249 pub override_web_search: ToolCategoryOverride,
254 pub schedule_tools: Option<Arc<dyn AgentToolDispatcher>>,
259 pub workgraph_tools: Option<Arc<dyn AgentToolDispatcher>>,
261 pub preload_skills: Option<Vec<crate::skills::SkillKey>>,
262 pub realm_id: Option<String>,
263 pub instance_id: Option<String>,
264 pub backend: Option<String>,
265 pub config_generation: Option<u64>,
266 pub auth_binding: Option<crate::AuthBindingRef>,
269 pub keep_alive: bool,
272 pub checkpointer: Option<std::sync::Arc<dyn crate::checkpoint::SessionCheckpointer>>,
274 pub silent_comms_intents: Vec<String>,
277 pub max_inline_peer_notifications: Option<i32>,
285 pub app_context: Option<serde_json::Value>,
292 pub additional_instructions: Option<Vec<String>>,
295 pub initial_metadata_entries: BTreeMap<String, serde_json::Value>,
300 pub shell_env: Option<std::collections::HashMap<String, String>>,
304 pub call_timeout_override: crate::CallTimeoutOverride,
310 pub resume_override_mask: ResumeOverrideMask,
316 pub mob_tools: Option<Arc<dyn MobToolsFactory>>,
324 pub runtime_build_mode: crate::runtime_epoch::RuntimeBuildMode,
332 pub initial_turn_metadata: Option<RuntimeTurnMetadata>,
337 pub mob_tool_authority_context: Option<MobToolAuthorityContext>,
344}
345
346#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
352pub struct OpaquePrincipalToken(String);
353
354impl OpaquePrincipalToken {
355 pub fn new(token: impl Into<String>) -> Self {
356 Self(token.into())
357 }
358
359 pub fn generated() -> Self {
360 Self(uuid::Uuid::new_v4().to_string())
361 }
362
363 pub fn as_str(&self) -> &str {
364 &self.0
365 }
366}
367
368impl std::fmt::Display for OpaquePrincipalToken {
369 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
370 f.write_str(self.as_str())
371 }
372}
373
374#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, Default)]
379pub struct MobToolCallerProvenance {
380 #[serde(default, skip_serializing_if = "Option::is_none")]
381 caller_session_id: Option<crate::SessionId>,
382 #[serde(default, skip_serializing_if = "Option::is_none")]
383 caller_mob_id: Option<String>,
384 #[serde(default, skip_serializing_if = "Option::is_none")]
385 caller_member_id: Option<String>,
386}
387
388impl MobToolCallerProvenance {
389 pub fn new() -> Self {
390 Self::default()
391 }
392
393 pub fn with_session_id(mut self, session_id: crate::SessionId) -> Self {
394 self.caller_session_id = Some(session_id);
395 self
396 }
397
398 pub fn with_mob_id(mut self, mob_id: impl Into<String>) -> Self {
399 self.caller_mob_id = Some(mob_id.into());
400 self
401 }
402
403 pub fn with_member_id(mut self, member_id: impl Into<String>) -> Self {
404 self.caller_member_id = Some(member_id.into());
405 self
406 }
407
408 pub fn caller_session_id(&self) -> Option<&crate::SessionId> {
409 self.caller_session_id.as_ref()
410 }
411
412 pub fn caller_mob_id(&self) -> Option<&str> {
413 self.caller_mob_id.as_deref()
414 }
415
416 pub fn caller_member_id(&self) -> Option<&str> {
417 self.caller_member_id.as_deref()
418 }
419}
420
421#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
427pub struct MobToolAuthorityContext {
428 principal_token: OpaquePrincipalToken,
429 can_create_mobs: bool,
430 #[serde(default)]
431 can_mutate_profiles: bool,
432 #[serde(default, skip_serializing_if = "BTreeSet::is_empty")]
433 managed_mob_scope: BTreeSet<String>,
434 #[serde(default, skip_serializing_if = "BTreeMap::is_empty")]
435 spawn_profile_scope: BTreeMap<String, BTreeSet<String>>,
436 #[serde(default, skip_serializing_if = "Option::is_none")]
437 caller_provenance: Option<MobToolCallerProvenance>,
438 #[serde(default, skip_serializing_if = "Option::is_none")]
439 audit_invocation_id: Option<String>,
440}
441
442impl MobToolAuthorityContext {
443 pub fn new(principal_token: OpaquePrincipalToken, can_create_mobs: bool) -> Self {
444 Self {
445 principal_token,
446 can_create_mobs,
447 can_mutate_profiles: can_create_mobs,
448 managed_mob_scope: BTreeSet::new(),
449 spawn_profile_scope: BTreeMap::new(),
450 caller_provenance: None,
451 audit_invocation_id: None,
452 }
453 }
454
455 pub fn create_only_generated() -> Self {
456 Self::new(OpaquePrincipalToken::generated(), true)
457 }
458
459 pub fn principal_token(&self) -> &OpaquePrincipalToken {
460 &self.principal_token
461 }
462
463 pub fn can_create_mobs(&self) -> bool {
464 self.can_create_mobs
465 }
466
467 pub fn can_mutate_profiles(&self) -> bool {
468 self.can_mutate_profiles
469 }
470
471 pub fn with_profile_mutation(mut self, allowed: bool) -> Self {
472 self.can_mutate_profiles = allowed;
473 self
474 }
475
476 pub fn managed_mob_scope(&self) -> &BTreeSet<String> {
477 &self.managed_mob_scope
478 }
479
480 pub fn caller_provenance(&self) -> Option<&MobToolCallerProvenance> {
481 self.caller_provenance.as_ref()
482 }
483
484 pub fn audit_invocation_id(&self) -> Option<&str> {
485 self.audit_invocation_id.as_deref()
486 }
487
488 pub fn can_manage_mob(&self, mob_id: &str) -> bool {
489 self.managed_mob_scope.contains(mob_id)
490 }
491
492 pub fn can_spawn_profile_in_mob(&self, mob_id: &str, profile: &str) -> bool {
493 self.can_manage_mob(mob_id)
494 || self
495 .spawn_profile_scope
496 .get(mob_id)
497 .is_some_and(|profiles| profiles.contains(profile))
498 }
499
500 pub fn can_spawn_any_profile_in_mob(&self, mob_id: &str) -> bool {
501 self.can_manage_mob(mob_id)
502 || self
503 .spawn_profile_scope
504 .get(mob_id)
505 .is_some_and(|profiles| !profiles.is_empty())
506 }
507
508 pub fn grant_manage_mob(mut self, mob_id: impl Into<String>) -> Self {
509 self.managed_mob_scope.insert(mob_id.into());
510 self
511 }
512
513 pub fn grant_spawn_profile_in_mob(
514 mut self,
515 mob_id: impl Into<String>,
516 profile: impl Into<String>,
517 ) -> Self {
518 self.spawn_profile_scope
519 .entry(mob_id.into())
520 .or_default()
521 .insert(profile.into());
522 self
523 }
524
525 pub fn grant_spawn_profiles_in_mob<I, S>(
526 mut self,
527 mob_id: impl Into<String>,
528 profiles: I,
529 ) -> Self
530 where
531 I: IntoIterator<Item = S>,
532 S: Into<String>,
533 {
534 self.spawn_profile_scope
535 .entry(mob_id.into())
536 .or_default()
537 .extend(profiles.into_iter().map(Into::into));
538 self
539 }
540
541 pub fn grant_manage_mob_in_place(&mut self, mob_id: String) {
546 self.managed_mob_scope.insert(mob_id);
547 }
548
549 pub fn with_managed_mob_scope<I, S>(mut self, mob_ids: I) -> Self
550 where
551 I: IntoIterator<Item = S>,
552 S: Into<String>,
553 {
554 self.managed_mob_scope = mob_ids.into_iter().map(Into::into).collect();
555 self
556 }
557
558 pub fn with_caller_provenance(mut self, caller_provenance: MobToolCallerProvenance) -> Self {
559 self.caller_provenance = Some(caller_provenance);
560 self
561 }
562
563 pub fn with_audit_invocation_id(mut self, audit_invocation_id: impl Into<String>) -> Self {
564 self.audit_invocation_id = Some(audit_invocation_id.into());
565 self
566 }
567}
568
569pub fn generated_create_only_mob_operator_authority(
575 enable_mob: ToolCategoryOverride,
576) -> Option<MobToolAuthorityContext> {
577 matches!(enable_mob, ToolCategoryOverride::Enable)
578 .then(MobToolAuthorityContext::create_only_generated)
579}
580
581pub fn resolve_mob_operator_access(
587 enable_mob: ToolCategoryOverride,
588 persisted_authority_context: Option<MobToolAuthorityContext>,
589) -> (ToolCategoryOverride, Option<MobToolAuthorityContext>) {
590 if matches!(enable_mob, ToolCategoryOverride::Disable) {
591 return (ToolCategoryOverride::Disable, None);
592 }
593
594 let authority_context = persisted_authority_context
595 .or_else(|| generated_create_only_mob_operator_authority(enable_mob));
596 let override_mob = if authority_context.is_some() {
597 ToolCategoryOverride::Enable
598 } else {
599 enable_mob
600 };
601
602 (override_mob, authority_context)
603}
604
605pub trait VisibleToolSnapshotProvider: Send + Sync {
610 fn snapshot_visible_tools(&self) -> Vec<Arc<ToolDef>>;
612}
613
614pub enum MobToolSnapshotContext {
620 ParentOwned(Arc<dyn VisibleToolSnapshotProvider>),
622 Standalone,
624}
625
626pub struct MobToolsBuildArgs {
628 pub session_id: crate::SessionId,
630 pub model: String,
632 pub authority_context: Option<MobToolAuthorityContext>,
638 pub effective_authority: Option<Arc<std::sync::RwLock<MobToolAuthorityContext>>>,
645 pub comms_name: Option<String>,
647 pub comms_runtime: Option<Arc<dyn crate::agent::CommsRuntime>>,
649 pub snapshot_context: MobToolSnapshotContext,
651}
652
653#[cfg_attr(target_arch = "wasm32", async_trait(?Send))]
659#[cfg_attr(not(target_arch = "wasm32"), async_trait)]
660pub trait MobToolsFactory: Send + Sync {
661 async fn build_mob_tools(
663 &self,
664 args: MobToolsBuildArgs,
665 ) -> Result<Arc<dyn AgentToolDispatcher>, Box<dyn std::error::Error + Send + Sync>>;
666}
667
668#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
672pub struct ResumeOverrideMask {
673 pub model: bool,
674 pub provider: bool,
675 pub max_tokens: bool,
676 pub structured_output_retries: bool,
677 pub provider_params: bool,
678 pub auth_binding: bool,
679 pub override_builtins: bool,
680 pub override_shell: bool,
681 pub override_memory: bool,
682 pub override_schedule: bool,
683 pub override_workgraph: bool,
684 pub override_mob: bool,
685 pub override_image_generation: bool,
686 pub override_web_search: bool,
687 pub preload_skills: bool,
688 pub keep_alive: bool,
689 pub comms_name: bool,
690 pub peer_meta: bool,
691}
692
693impl SessionBuildOptions {
694 pub fn apply_persisted_mob_operator_access(
700 &mut self,
701 enable_mob: ToolCategoryOverride,
702 persisted_authority_context: Option<MobToolAuthorityContext>,
703 ) {
704 let (override_mob, authority_context) =
705 resolve_mob_operator_access(enable_mob, persisted_authority_context);
706 self.override_mob = override_mob;
707 self.mob_tool_authority_context = authority_context;
708 }
709
710 pub fn apply_generated_create_only_mob_operator_access(
717 &mut self,
718 enable_mob: ToolCategoryOverride,
719 ) {
720 self.apply_persisted_mob_operator_access(enable_mob, None);
721 }
722}
723
724impl Default for SessionBuildOptions {
725 fn default() -> Self {
726 Self {
727 provider: None,
728 self_hosted_server_id: None,
729 output_schema: None,
730 structured_output_retries: 2,
731 hooks_override: HookRunOverrides::default(),
732 comms_name: None,
733 peer_meta: None,
734 resume_session: None,
735 budget_limits: None,
738 provider_params: None,
739 external_tools: None,
740 recoverable_tool_defs: None,
741 blob_store_override: None,
742 llm_client_override: None,
743 agent_llm_client_decorator: None,
744 override_builtins: ToolCategoryOverride::Inherit,
745 override_shell: ToolCategoryOverride::Inherit,
746 override_memory: ToolCategoryOverride::Inherit,
747 override_schedule: ToolCategoryOverride::Inherit,
748 override_workgraph: ToolCategoryOverride::Inherit,
749 override_mob: ToolCategoryOverride::Inherit,
750 override_image_generation: ToolCategoryOverride::Inherit,
751 override_web_search: ToolCategoryOverride::Inherit,
752 schedule_tools: None,
753 workgraph_tools: None,
754 preload_skills: None,
755 realm_id: None,
756 instance_id: None,
757 backend: None,
758 config_generation: None,
759 auth_binding: None,
760 keep_alive: false,
761 checkpointer: None,
762 silent_comms_intents: Vec::new(),
763 max_inline_peer_notifications: None,
764 app_context: None,
765 additional_instructions: None,
766 initial_metadata_entries: BTreeMap::new(),
767 shell_env: None,
768 call_timeout_override: crate::CallTimeoutOverride::Inherit,
769 resume_override_mask: ResumeOverrideMask::default(),
770 mob_tools: None,
771 runtime_build_mode: crate::runtime_epoch::RuntimeBuildMode::StandaloneEphemeral,
772 initial_turn_metadata: None,
773 mob_tool_authority_context: None,
774 }
775 }
776}
777
778impl std::fmt::Debug for SessionBuildOptions {
779 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
780 f.debug_struct("SessionBuildOptions")
781 .field("provider", &self.provider)
782 .field("output_schema", &self.output_schema.is_some())
783 .field("structured_output_retries", &self.structured_output_retries)
784 .field("hooks_override", &self.hooks_override)
785 .field("comms_name", &self.comms_name)
786 .field("peer_meta", &self.peer_meta)
787 .field("resume_session", &self.resume_session.is_some())
788 .field("budget_limits", &self.budget_limits)
789 .field("provider_params", &self.provider_params.is_some())
790 .field("external_tools", &self.external_tools.is_some())
791 .field("recoverable_tool_defs", &self.recoverable_tool_defs)
792 .field("blob_store_override", &self.blob_store_override.is_some())
793 .field("llm_client_override", &self.llm_client_override.is_some())
794 .field(
795 "agent_llm_client_decorator",
796 &self.agent_llm_client_decorator.is_some(),
797 )
798 .field("override_builtins", &self.override_builtins)
799 .field("override_shell", &self.override_shell)
800 .field("override_memory", &self.override_memory)
801 .field("override_schedule", &self.override_schedule)
802 .field("override_workgraph", &self.override_workgraph)
803 .field("override_mob", &self.override_mob)
804 .field("schedule_tools", &self.schedule_tools.is_some())
805 .field("workgraph_tools", &self.workgraph_tools.is_some())
806 .field("preload_skills", &self.preload_skills)
807 .field("realm_id", &self.realm_id)
808 .field("instance_id", &self.instance_id)
809 .field("backend", &self.backend)
810 .field("config_generation", &self.config_generation)
811 .field("keep_alive", &self.keep_alive)
812 .field("checkpointer", &self.checkpointer.is_some())
813 .field("silent_comms_intents", &self.silent_comms_intents)
814 .field(
815 "max_inline_peer_notifications",
816 &self.max_inline_peer_notifications,
817 )
818 .field("app_context", &self.app_context.is_some())
819 .field("additional_instructions", &self.additional_instructions)
820 .field("initial_metadata_entries", &self.initial_metadata_entries)
821 .field("call_timeout_override", &self.call_timeout_override)
822 .field("resume_override_mask", &self.resume_override_mask)
823 .field("mob_tools", &self.mob_tools.is_some())
824 .field("runtime_build_mode", &self.runtime_build_mode)
825 .field(
826 "initial_turn_metadata",
827 &self.initial_turn_metadata.is_some(),
828 )
829 .field(
830 "mob_tool_authority_context",
831 &self.mob_tool_authority_context.is_some(),
832 )
833 .field("runtime_build_mode", &self.runtime_build_mode)
834 .finish()
835 }
836}
837
838#[derive(Debug)]
844pub struct StartTurnRuntimeSemantics {
845 pub render_metadata: Option<RenderMetadata>,
847 pub handling_mode: HandlingMode,
854 pub skill_references: Option<Vec<crate::skills::SkillKey>>,
856 pub flow_tool_overlay: Option<TurnToolOverlay>,
858 pub pre_turn_context_appends: Vec<PendingSystemContextAppend>,
861 pub typed_turn_appends: Vec<ConversationAppend>,
867 pub turn_metadata: Option<RuntimeTurnMetadata>,
873}
874
875impl Default for StartTurnRuntimeSemantics {
876 fn default() -> Self {
877 Self {
878 render_metadata: None,
879 handling_mode: HandlingMode::Queue,
880 skill_references: None,
881 flow_tool_overlay: None,
882 pre_turn_context_appends: Vec::new(),
883 typed_turn_appends: Vec::new(),
884 turn_metadata: None,
885 }
886 }
887}
888
889impl StartTurnRuntimeSemantics {
890 #[must_use]
891 pub fn new(
892 render_metadata: Option<RenderMetadata>,
893 handling_mode: HandlingMode,
894 skill_references: Option<Vec<crate::skills::SkillKey>>,
895 flow_tool_overlay: Option<TurnToolOverlay>,
896 pre_turn_context_appends: Vec<PendingSystemContextAppend>,
897 turn_metadata: Option<RuntimeTurnMetadata>,
898 ) -> Self {
899 Self {
900 render_metadata,
901 handling_mode,
902 skill_references,
903 flow_tool_overlay,
904 pre_turn_context_appends,
905 typed_turn_appends: Vec::new(),
906 turn_metadata,
907 }
908 }
909
910 #[must_use]
911 pub fn runtime_metadata(turn_metadata: RuntimeTurnMetadata) -> Self {
912 Self {
913 turn_metadata: Some(turn_metadata),
914 ..Self::default()
915 }
916 }
917
918 #[must_use]
919 pub fn with_typed_turn_appends(mut self, typed_turn_appends: Vec<ConversationAppend>) -> Self {
920 self.typed_turn_appends = typed_turn_appends;
921 self
922 }
923}
924
925#[derive(Debug)]
927pub struct StartTurnRequest {
928 pub prompt: ContentInput,
930 pub system_prompt: Option<String>,
935 pub event_tx: Option<mpsc::Sender<EventEnvelope<AgentEvent>>>,
937 pub runtime: StartTurnRuntimeSemantics,
939}
940
941#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
943pub struct AppendSystemContextRequest {
944 pub text: String,
945 #[serde(default, skip_serializing_if = "Option::is_none")]
946 pub source: Option<String>,
947 #[serde(default, skip_serializing_if = "Option::is_none")]
948 pub idempotency_key: Option<String>,
949}
950
951#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
953pub struct AppendSystemContextResult {
954 pub status: AppendSystemContextStatus,
955}
956
957#[derive(Debug, Clone, Serialize, Deserialize)]
959pub struct StageToolResultsRequest {
960 pub results: Vec<crate::ToolResult>,
961}
962
963#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
965pub struct StageToolResultsResult {
966 pub accepted_result_count: usize,
967}
968
969#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
971#[serde(rename_all = "snake_case")]
972pub enum AppendSystemContextStatus {
973 Applied,
974 Staged,
975 Duplicate,
976}
977
978#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
980#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
981pub struct TurnToolOverlay {
982 #[serde(default)]
984 pub allowed_tools: Option<Vec<String>>,
985 #[serde(default)]
987 pub blocked_tools: Option<Vec<String>>,
988}
989
990#[derive(Debug, Default)]
992pub struct SessionQuery {
993 pub limit: Option<usize>,
995 pub offset: Option<usize>,
997 pub labels: Option<BTreeMap<String, String>>,
999}
1000
1001#[derive(Debug, Clone, Serialize, Deserialize)]
1005pub struct SessionSummary {
1006 pub session_id: SessionId,
1007 pub created_at: SystemTime,
1008 pub updated_at: SystemTime,
1009 pub message_count: usize,
1010 pub total_tokens: u64,
1011 pub is_active: bool,
1012 #[serde(default, skip_serializing_if = "BTreeMap::is_empty")]
1013 pub labels: BTreeMap<String, String>,
1014}
1015
1016#[derive(Debug, Clone, Serialize, Deserialize)]
1018pub struct SessionInfo {
1019 pub session_id: SessionId,
1020 pub created_at: SystemTime,
1021 pub updated_at: SystemTime,
1022 pub message_count: usize,
1023 pub is_active: bool,
1024 pub model: String,
1025 pub provider: Provider,
1026 pub last_assistant_text: Option<String>,
1027 #[serde(default, skip_serializing_if = "BTreeMap::is_empty")]
1028 pub labels: BTreeMap<String, String>,
1029}
1030
1031#[derive(Debug, Clone, Serialize, Deserialize)]
1033pub struct SessionUsage {
1034 pub total_tokens: u64,
1035 pub usage: Usage,
1036}
1037
1038#[derive(Debug, Clone, Serialize, Deserialize)]
1041pub struct SessionView {
1042 pub state: SessionInfo,
1043 pub billing: SessionUsage,
1044}
1045
1046impl SessionView {
1047 pub fn session_id(&self) -> &SessionId {
1049 &self.state.session_id
1050 }
1051}
1052
1053#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq, Eq)]
1055pub struct SessionHistoryQuery {
1056 pub offset: usize,
1058 #[serde(default, skip_serializing_if = "Option::is_none")]
1060 pub limit: Option<usize>,
1061}
1062
1063#[derive(Debug, Clone, Serialize, Deserialize)]
1065pub struct SessionHistoryPage {
1066 pub session_id: SessionId,
1067 pub message_count: usize,
1068 pub offset: usize,
1069 #[serde(default, skip_serializing_if = "Option::is_none")]
1070 pub limit: Option<usize>,
1071 pub has_more: bool,
1072 pub messages: Vec<Message>,
1073}
1074
1075impl SessionHistoryPage {
1076 pub fn from_messages(
1078 session_id: SessionId,
1079 messages: &[Message],
1080 query: SessionHistoryQuery,
1081 ) -> Self {
1082 let message_count = messages.len();
1083 let start = query.offset.min(message_count);
1084 let end = match query.limit {
1085 Some(limit) => start.saturating_add(limit).min(message_count),
1086 None => message_count,
1087 };
1088 Self {
1089 session_id,
1090 message_count,
1091 offset: start,
1092 limit: query.limit,
1093 has_more: end < message_count,
1094 messages: messages[start..end].to_vec(),
1095 }
1096 }
1097}
1098
1099#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Serialize, Deserialize)]
1102#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
1103#[serde(rename_all = "snake_case")]
1104pub enum TranscriptEditRunningBehavior {
1105 #[default]
1107 Reject,
1108}
1109
1110#[derive(Debug, Clone, Serialize, Deserialize)]
1112#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
1113pub struct SessionForkAtRequest {
1114 pub message_index: usize,
1115 #[serde(default)]
1116 pub running_behavior: TranscriptEditRunningBehavior,
1117}
1118
1119#[derive(Debug, Clone, Serialize, Deserialize)]
1121#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
1122pub struct SessionForkReplaceRequest {
1123 pub message_index: usize,
1124 #[cfg_attr(feature = "schema", schemars(with = "serde_json::Value"))]
1125 pub replacement: TranscriptReplacement,
1126 #[serde(default)]
1127 pub running_behavior: TranscriptEditRunningBehavior,
1128}
1129
1130#[derive(Debug, Clone, Serialize, Deserialize)]
1132#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
1133pub struct SessionForkResult {
1134 #[cfg_attr(feature = "schema", schemars(with = "String"))]
1135 pub source_session_id: SessionId,
1136 #[cfg_attr(feature = "schema", schemars(with = "String"))]
1137 pub session_id: SessionId,
1138 pub message_count: usize,
1139 #[serde(default, skip_serializing_if = "Option::is_none")]
1140 pub session_ref: Option<String>,
1141}
1142
1143impl TranscriptEditError {
1144 pub fn into_session_error(self) -> SessionError {
1147 SessionError::Agent(crate::error::AgentError::ConfigError(self.to_string()))
1148 }
1149}
1150
1151#[cfg_attr(target_arch = "wasm32", async_trait(?Send))]
1156#[cfg_attr(not(target_arch = "wasm32"), async_trait)]
1157pub trait SessionService: Send + Sync {
1158 async fn create_session(&self, req: CreateSessionRequest) -> Result<RunResult, SessionError>;
1160
1161 async fn start_turn(
1163 &self,
1164 id: &SessionId,
1165 req: StartTurnRequest,
1166 ) -> Result<RunResult, SessionError>;
1167
1168 async fn interrupt(&self, id: &SessionId) -> Result<(), SessionError>;
1172
1173 async fn cancel_after_boundary(&self, _id: &SessionId) -> Result<(), SessionError> {
1177 Err(SessionError::Unsupported(
1178 "cancel_after_boundary".to_string(),
1179 ))
1180 }
1181
1182 async fn set_session_client(
1189 &self,
1190 _id: &SessionId,
1191 _client: std::sync::Arc<dyn crate::AgentLlmClient>,
1192 ) -> Result<(), SessionError> {
1193 Err(SessionError::Unsupported("set_session_client".to_string()))
1194 }
1195
1196 async fn hot_swap_session_llm_identity(
1203 &self,
1204 _id: &SessionId,
1205 _client: std::sync::Arc<dyn crate::AgentLlmClient>,
1206 _identity: SessionLlmIdentity,
1207 _request_policy: crate::SessionLlmRequestPolicy,
1208 ) -> Result<(), SessionError> {
1209 Err(SessionError::Unsupported(
1210 "hot_swap_session_llm_identity".to_string(),
1211 ))
1212 }
1213
1214 async fn set_session_tool_visibility_state(
1219 &self,
1220 _id: &SessionId,
1221 _state: Option<crate::SessionToolVisibilityState>,
1222 ) -> Result<(), SessionError> {
1223 Err(SessionError::Unsupported(
1224 "set_session_tool_visibility_state".to_string(),
1225 ))
1226 }
1227
1228 async fn update_session_keep_alive(
1234 &self,
1235 _id: &SessionId,
1236 _keep_alive: bool,
1237 ) -> Result<(), SessionError> {
1238 Err(SessionError::Unsupported(
1239 "update_session_keep_alive".to_string(),
1240 ))
1241 }
1242
1243 async fn update_session_mob_authority_context(
1249 &self,
1250 _id: &SessionId,
1251 _authority_context: Option<MobToolAuthorityContext>,
1252 ) -> Result<(), SessionError> {
1253 Err(SessionError::Unsupported(
1254 "update_session_mob_authority_context".to_string(),
1255 ))
1256 }
1257
1258 async fn has_live_session(&self, _id: &SessionId) -> Result<bool, SessionError> {
1264 Err(SessionError::Unsupported("has_live_session".to_string()))
1265 }
1266
1267 async fn set_session_tool_filter(
1273 &self,
1274 _id: &SessionId,
1275 _filter: crate::ToolFilter,
1276 ) -> Result<(), SessionError> {
1277 Err(SessionError::Unsupported(
1278 "set_session_tool_filter".to_string(),
1279 ))
1280 }
1281
1282 async fn read(&self, id: &SessionId) -> Result<SessionView, SessionError>;
1284
1285 async fn list(&self, query: SessionQuery) -> Result<Vec<SessionSummary>, SessionError>;
1287
1288 async fn archive(&self, id: &SessionId) -> Result<(), SessionError>;
1290
1291 async fn subscribe_session_events(&self, id: &SessionId) -> Result<EventStream, StreamError> {
1295 Err(StreamError::NotFound(format!("session {id}")))
1296 }
1297}
1298
1299#[cfg_attr(target_arch = "wasm32", async_trait(?Send))]
1305#[cfg_attr(not(target_arch = "wasm32"), async_trait)]
1306pub trait SessionServiceCommsExt: SessionService {
1307 async fn comms_runtime(
1309 &self,
1310 _session_id: &SessionId,
1311 ) -> Option<Arc<dyn crate::agent::CommsRuntime>> {
1312 None
1313 }
1314
1315 async fn event_injector(
1317 &self,
1318 session_id: &SessionId,
1319 ) -> Option<Arc<dyn crate::EventInjector>> {
1320 self.comms_runtime(session_id)
1321 .await
1322 .and_then(|runtime| runtime.event_injector())
1323 }
1324
1325 #[doc(hidden)]
1327 async fn interaction_event_injector(
1328 &self,
1329 session_id: &SessionId,
1330 ) -> Option<Arc<dyn crate::event_injector::SubscribableInjector>> {
1331 self.comms_runtime(session_id)
1332 .await
1333 .and_then(|runtime| runtime.interaction_event_injector())
1334 }
1335}
1336
1337#[cfg_attr(target_arch = "wasm32", async_trait(?Send))]
1342#[cfg_attr(not(target_arch = "wasm32"), async_trait)]
1343pub trait SessionServiceControlExt: SessionService {
1344 async fn append_system_context(
1350 &self,
1351 id: &SessionId,
1352 req: AppendSystemContextRequest,
1353 ) -> Result<AppendSystemContextResult, SessionControlError>;
1354
1355 async fn stage_tool_results(
1361 &self,
1362 id: &SessionId,
1363 req: StageToolResultsRequest,
1364 ) -> Result<StageToolResultsResult, SessionError> {
1365 let _ = (id, req);
1366 Err(SessionError::Unsupported("stage_tool_results".to_string()))
1367 }
1368}
1369
1370#[cfg_attr(target_arch = "wasm32", async_trait(?Send))]
1375#[cfg_attr(not(target_arch = "wasm32"), async_trait)]
1376pub trait SessionServiceHistoryExt: SessionService {
1377 async fn read_history(
1382 &self,
1383 id: &SessionId,
1384 query: SessionHistoryQuery,
1385 ) -> Result<SessionHistoryPage, SessionError>;
1386}
1387
1388#[cfg_attr(target_arch = "wasm32", async_trait(?Send))]
1393#[cfg_attr(not(target_arch = "wasm32"), async_trait)]
1394pub trait SessionServiceTranscriptEditExt: SessionService {
1395 async fn fork_session_at(
1397 &self,
1398 id: &SessionId,
1399 req: SessionForkAtRequest,
1400 ) -> Result<SessionForkResult, SessionError> {
1401 let _ = (id, req);
1402 Err(SessionError::Unsupported("fork_session_at".to_string()))
1403 }
1404
1405 async fn fork_session_replace(
1407 &self,
1408 id: &SessionId,
1409 req: SessionForkReplaceRequest,
1410 ) -> Result<SessionForkResult, SessionError> {
1411 let _ = (id, req);
1412 Err(SessionError::Unsupported(
1413 "fork_session_replace".to_string(),
1414 ))
1415 }
1416}
1417
1418impl dyn SessionService {
1420 pub fn into_arc(self: Box<Self>) -> Arc<dyn SessionService> {
1422 Arc::from(self)
1423 }
1424}
1425
1426#[cfg(test)]
1427#[allow(
1428 clippy::unimplemented,
1429 clippy::unwrap_used,
1430 clippy::expect_used,
1431 clippy::panic
1432)]
1433mod tests {
1434 use super::*;
1435
1436 struct UnsupportedSessionService;
1437
1438 #[cfg_attr(target_arch = "wasm32", async_trait(?Send))]
1439 #[cfg_attr(not(target_arch = "wasm32"), async_trait)]
1440 impl SessionService for UnsupportedSessionService {
1441 async fn create_session(
1442 &self,
1443 _req: CreateSessionRequest,
1444 ) -> Result<RunResult, SessionError> {
1445 unimplemented!()
1446 }
1447
1448 async fn start_turn(
1449 &self,
1450 _id: &SessionId,
1451 _req: StartTurnRequest,
1452 ) -> Result<RunResult, SessionError> {
1453 unimplemented!()
1454 }
1455
1456 async fn interrupt(&self, _id: &SessionId) -> Result<(), SessionError> {
1457 unimplemented!()
1458 }
1459
1460 async fn read(&self, _id: &SessionId) -> Result<SessionView, SessionError> {
1461 unimplemented!()
1462 }
1463
1464 async fn list(&self, _query: SessionQuery) -> Result<Vec<SessionSummary>, SessionError> {
1465 unimplemented!()
1466 }
1467
1468 async fn archive(&self, _id: &SessionId) -> Result<(), SessionError> {
1469 unimplemented!()
1470 }
1471 }
1472
1473 #[tokio::test]
1474 async fn has_live_session_defaults_to_unsupported() {
1475 let service = UnsupportedSessionService;
1476 let err = service
1477 .has_live_session(&SessionId::new())
1478 .await
1479 .expect_err("default implementation should fail loudly");
1480 assert!(matches!(err, SessionError::Unsupported(name) if name == "has_live_session"));
1481 }
1482
1483 #[test]
1484 fn grant_manage_mob_in_place_adds_mob_id() {
1485 let mut ctx = MobToolAuthorityContext::create_only_generated();
1486 ctx.grant_manage_mob_in_place("mob-1".into());
1487 assert!(ctx.managed_mob_scope.contains("mob-1"));
1488 }
1489
1490 #[test]
1491 fn grant_manage_mob_in_place_is_idempotent() {
1492 let mut ctx = MobToolAuthorityContext::create_only_generated();
1493 ctx.grant_manage_mob_in_place("mob-1".into());
1494 ctx.grant_manage_mob_in_place("mob-1".into());
1495 assert_eq!(ctx.managed_mob_scope.len(), 1);
1496 }
1497
1498 #[test]
1499 fn grant_manage_mob_in_place_accumulates() {
1500 let mut ctx = MobToolAuthorityContext::create_only_generated();
1501 ctx.grant_manage_mob_in_place("mob-1".into());
1502 ctx.grant_manage_mob_in_place("mob-2".into());
1503 assert!(ctx.managed_mob_scope.contains("mob-1"));
1504 assert!(ctx.managed_mob_scope.contains("mob-2"));
1505 assert_eq!(ctx.managed_mob_scope.len(), 2);
1506 }
1507
1508 #[test]
1509 fn spawn_profile_scope_allows_only_granted_profile_without_manage_scope() {
1510 let ctx = MobToolAuthorityContext::create_only_generated()
1511 .grant_spawn_profile_in_mob("mob-1", "investigator");
1512
1513 assert!(ctx.can_spawn_any_profile_in_mob("mob-1"));
1514 assert!(ctx.can_spawn_profile_in_mob("mob-1", "investigator"));
1515 assert!(!ctx.can_spawn_profile_in_mob("mob-1", "writer"));
1516 assert!(!ctx.can_manage_mob("mob-1"));
1517 }
1518
1519 struct MockSnapshotProvider {
1520 tools: Vec<Arc<ToolDef>>,
1521 }
1522
1523 impl VisibleToolSnapshotProvider for MockSnapshotProvider {
1524 fn snapshot_visible_tools(&self) -> Vec<Arc<ToolDef>> {
1525 self.tools.clone()
1526 }
1527 }
1528
1529 #[test]
1530 fn mob_tool_snapshot_context_standalone() {
1531 let ctx = MobToolSnapshotContext::Standalone;
1532 assert!(matches!(ctx, MobToolSnapshotContext::Standalone));
1533 }
1534
1535 #[test]
1536 fn mob_tool_snapshot_context_parent_owned_returns_tools() {
1537 let tools = vec![Arc::new(ToolDef {
1538 name: "test_tool".into(),
1539 description: "a test".to_string(),
1540 input_schema: serde_json::json!({"type": "object"}),
1541 provenance: None,
1542 })];
1543 let provider = Arc::new(MockSnapshotProvider { tools });
1544 let ctx = MobToolSnapshotContext::ParentOwned(provider);
1545 match ctx {
1546 MobToolSnapshotContext::ParentOwned(p) => {
1547 let snapshot = p.snapshot_visible_tools();
1548 assert_eq!(snapshot.len(), 1);
1549 assert_eq!(snapshot[0].name, "test_tool");
1550 }
1551 MobToolSnapshotContext::Standalone => panic!("expected ParentOwned"),
1552 }
1553 }
1554}