1pub mod transport;
7
8use crate::event::AgentEvent;
9use crate::event::EventEnvelope;
10use crate::lifecycle::run_primitive::{ConversationAppend, CoreRenderable, RuntimeTurnMetadata};
11use crate::session::{PendingSystemContextAppend, SystemContextStageError};
12use crate::time_compat::SystemTime;
13#[cfg(target_arch = "wasm32")]
14use crate::tokio;
15use crate::types::{ContentInput, HandlingMode, Message, RunResult, SessionId, ToolDef, Usage};
16use crate::{
17 AgentToolDispatcher, BudgetLimits, HookRunOverrides, OutputSchema, PeerMeta, Provider, Session,
18 SessionLlmIdentity, ToolCategoryOverride,
19};
20use crate::{EventStream, StreamError};
21use async_trait::async_trait;
22use serde::{Deserialize, Serialize};
23use std::collections::BTreeMap;
24use std::collections::BTreeSet;
25use std::sync::{Arc, LazyLock, RwLock};
26use tokio::sync::mpsc;
27
28pub use crate::session::{
29 TranscriptEditError, TranscriptReplacement, TranscriptRewriteCommit, TranscriptRewriteReason,
30 TranscriptRewriteSelection,
31};
32
33#[derive(Debug, Clone, Copy, PartialEq, Eq)]
35pub enum InitialTurnPolicy {
36 RunImmediately,
38 Defer,
43}
44
45#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)]
47#[serde(rename_all = "snake_case")]
48pub enum DeferredPromptPolicy {
49 #[default]
51 Discard,
52 Stage,
54}
55
56#[derive(Debug, thiserror::Error)]
58pub enum SessionError {
59 #[error("session not found: {id}")]
61 NotFound { id: SessionId },
62
63 #[error("session is busy: {id}")]
65 Busy { id: SessionId },
66
67 #[error("session persistence is disabled")]
69 PersistenceDisabled,
70
71 #[error("session compaction is disabled")]
73 CompactionDisabled,
74
75 #[error("no turn running on session: {id}")]
77 NotRunning { id: SessionId },
78
79 #[error("store error: {0}")]
81 Store(#[source] Box<dyn std::error::Error + Send + Sync>),
82
83 #[error("agent error: {0}")]
85 Agent(#[from] crate::error::AgentError),
86
87 #[error("{message}")]
89 FailedWithData {
90 message: String,
91 data: serde_json::Value,
92 },
93
94 #[error("unsupported: {0}")]
96 Unsupported(String),
97}
98
99impl SessionError {
100 pub fn code(&self) -> &'static str {
102 match self {
103 Self::NotFound { .. } => "SESSION_NOT_FOUND",
104 Self::Busy { .. } => "SESSION_BUSY",
105 Self::PersistenceDisabled => "SESSION_PERSISTENCE_DISABLED",
106 Self::CompactionDisabled => "SESSION_COMPACTION_DISABLED",
107 Self::NotRunning { .. } => "SESSION_NOT_RUNNING",
108 Self::Store(_) => "SESSION_STORE_ERROR",
109 Self::Unsupported(_) => "SESSION_UNSUPPORTED",
110 Self::Agent(_) => "AGENT_ERROR",
111 Self::FailedWithData { .. } => "SESSION_ERROR",
112 }
113 }
114
115 pub fn structured_data(&self) -> Option<serde_json::Value> {
116 match self {
117 Self::FailedWithData { data, .. } => Some(data.clone()),
118 _ => None,
119 }
120 }
121}
122
123#[derive(Debug, thiserror::Error)]
125pub enum SessionControlError {
126 #[error(transparent)]
128 Session(#[from] SessionError),
129
130 #[error("invalid system-context request: {message}")]
132 InvalidRequest { message: String },
133
134 #[error(
136 "system-context idempotency conflict on session {id}: key '{key}' already maps to different content"
137 )]
138 Conflict { id: SessionId, key: String },
139}
140
141impl SessionControlError {
142 pub fn code(&self) -> &'static str {
144 match self {
145 Self::Session(err) => err.code(),
146 Self::InvalidRequest { .. } => "INVALID_PARAMS",
147 Self::Conflict { .. } => "SESSION_SYSTEM_CONTEXT_CONFLICT",
148 }
149 }
150}
151
152impl SystemContextStageError {
153 pub fn into_control_error(self, id: &SessionId) -> SessionControlError {
155 match self {
156 Self::InvalidRequest(message) => SessionControlError::InvalidRequest { message },
157 Self::Conflict { key, .. } => SessionControlError::Conflict {
158 id: id.clone(),
159 key,
160 },
161 }
162 }
163}
164
165#[derive(Debug)]
174pub struct CreateSessionRequest {
175 pub model: String,
177 pub prompt: ContentInput,
179 pub injected_context: Vec<ContentInput>,
187 pub system_prompt: crate::config::SystemPromptOverride,
189 pub max_tokens: Option<u32>,
191 pub event_tx: Option<mpsc::Sender<EventEnvelope<AgentEvent>>>,
193 pub initial_turn: InitialTurnPolicy,
195 pub deferred_prompt_policy: DeferredPromptPolicy,
197 pub build: Option<SessionBuildOptions>,
199 pub labels: Option<BTreeMap<String, String>>,
201}
202
203impl CreateSessionRequest {
204 #[must_use]
207 pub fn surface_metadata(&self) -> crate::SurfaceMetadata {
208 crate::SurfaceMetadata::from_optional_parts(
209 self.labels.clone(),
210 self.build
211 .as_ref()
212 .and_then(|build| build.app_context.clone()),
213 )
214 }
215}
216
217#[derive(Clone)]
219pub struct SessionBuildOptions {
220 pub provider: Option<Provider>,
221 pub self_hosted_server_id: Option<String>,
222 pub custom_models: BTreeMap<String, crate::config::CustomModelConfig>,
226 pub image_generation_provider: Option<Provider>,
232 pub auto_compact_threshold_override: Option<std::num::NonZeroU64>,
239 pub output_schema: Option<OutputSchema>,
240 pub structured_output_retries: Option<u32>,
244 pub hooks_override: HookRunOverrides,
245 pub comms_name: Option<String>,
246 pub peer_meta: Option<PeerMeta>,
247 pub resume_session: Option<Session>,
248 pub budget_limits: Option<BudgetLimits>,
249 pub provider_params: Option<crate::lifecycle::run_primitive::ProviderParamsOverride>,
252 pub external_tools: Option<Arc<dyn AgentToolDispatcher>>,
253 pub mcp_servers: Vec<crate::mcp_config::McpServerConfig>,
259 pub recoverable_tool_defs: Option<Vec<crate::ToolDef>>,
262 pub blob_store_override: Option<Arc<dyn crate::BlobStore>>,
265 pub llm_client_override: Option<Arc<dyn std::any::Any + Send + Sync>>,
269 pub agent_llm_client_decorator: Option<crate::AgentLlmClientDecorator>,
274 pub override_builtins: ToolCategoryOverride,
277 pub override_shell: ToolCategoryOverride,
278 pub override_comms: ToolCategoryOverride,
280 pub override_memory: ToolCategoryOverride,
281 pub override_schedule: ToolCategoryOverride,
283 pub override_workgraph: ToolCategoryOverride,
285 pub override_mob: ToolCategoryOverride,
286 pub override_image_generation: ToolCategoryOverride,
291 pub override_web_search: ToolCategoryOverride,
296 pub schedule_tools: Option<Arc<dyn AgentToolDispatcher>>,
301 pub workgraph_tools: Option<Arc<dyn AgentToolDispatcher>>,
303 pub preload_skills: Option<Vec<crate::skills::SkillKey>>,
304 pub realm_id: Option<crate::RealmId>,
305 pub instance_id: Option<String>,
306 pub backend: Option<crate::session_recovery::RecoveryBackendKind>,
313 pub config_generation: Option<u64>,
314 pub auth_binding: Option<crate::AuthBindingRef>,
317 pub mob_member_binding: Option<crate::MobMemberBinding>,
321 pub keep_alive: bool,
324 pub checkpointer: Option<std::sync::Arc<dyn crate::checkpoint::SessionCheckpointer>>,
326 pub silent_comms_intents: Vec<String>,
329 pub max_inline_peer_notifications: Option<i32>,
337 pub app_context: Option<serde_json::Value>,
343 pub additional_instructions: Option<Vec<String>>,
346 pub initial_metadata_entries: BTreeMap<String, serde_json::Value>,
351 pub initial_tool_filter: Option<crate::tool_scope::ToolFilter>,
357 pub tool_access_policy: Option<crate::ops::ToolAccessPolicy>,
364 pub shell_env: Option<std::collections::HashMap<String, String>>,
368 pub call_timeout_override: crate::CallTimeoutOverride,
374 pub resume_override_mask: ResumeOverrideMask,
380 pub mob_tools: Option<Arc<dyn MobToolsFactory>>,
388 pub runtime_build_mode: crate::runtime_epoch::RuntimeBuildMode,
396 pub initial_turn_metadata: Option<RuntimeTurnMetadata>,
401 pub mob_tool_authority_context: Option<MobToolAuthorityContext>,
408}
409
410#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
416pub struct OpaquePrincipalToken(String);
417
418impl OpaquePrincipalToken {
419 pub fn new(token: impl Into<String>) -> Self {
420 Self(token.into())
421 }
422
423 pub fn generated() -> Self {
424 Self(uuid::Uuid::new_v4().to_string())
425 }
426
427 pub fn as_str(&self) -> &str {
428 &self.0
429 }
430}
431
432impl std::fmt::Display for OpaquePrincipalToken {
433 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
434 f.write_str(self.as_str())
435 }
436}
437
438#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, Default)]
443pub struct MobToolCallerProvenance {
444 #[serde(default, skip_serializing_if = "Option::is_none")]
445 caller_session_id: Option<crate::SessionId>,
446 #[serde(default, skip_serializing_if = "Option::is_none")]
447 caller_mob_id: Option<String>,
448 #[serde(default, skip_serializing_if = "Option::is_none")]
449 caller_member_id: Option<String>,
450}
451
452impl MobToolCallerProvenance {
453 pub fn new() -> Self {
454 Self::default()
455 }
456
457 pub fn with_session_id(mut self, session_id: crate::SessionId) -> Self {
458 self.caller_session_id = Some(session_id);
459 self
460 }
461
462 pub fn with_mob_id(mut self, mob_id: impl Into<String>) -> Self {
463 self.caller_mob_id = Some(mob_id.into());
464 self
465 }
466
467 pub fn with_member_id(mut self, member_id: impl Into<String>) -> Self {
468 self.caller_member_id = Some(member_id.into());
469 self
470 }
471
472 pub fn caller_session_id(&self) -> Option<&crate::SessionId> {
473 self.caller_session_id.as_ref()
474 }
475
476 pub fn caller_mob_id(&self) -> Option<&str> {
477 self.caller_mob_id.as_deref()
478 }
479
480 pub fn caller_member_id(&self) -> Option<&str> {
481 self.caller_member_id.as_deref()
482 }
483}
484
485#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
491pub struct MobToolAuthorityContext {
492 #[serde(skip)]
493 generated_authority_seal: MobToolAuthorityContextSeal,
494 principal_token: OpaquePrincipalToken,
495 can_create_mobs: bool,
496 #[serde(default)]
497 can_mutate_profiles: bool,
498 #[serde(default)]
499 can_run_adaptive_packs: bool,
500 #[serde(default, skip_serializing_if = "BTreeSet::is_empty")]
501 managed_mob_scope: BTreeSet<String>,
502 #[serde(default, skip_serializing_if = "BTreeMap::is_empty")]
503 spawn_profile_scope: BTreeMap<String, BTreeSet<String>>,
504 #[serde(default, skip_serializing_if = "Option::is_none")]
505 caller_provenance: Option<MobToolCallerProvenance>,
506 #[serde(default, skip_serializing_if = "Option::is_none")]
507 audit_invocation_id: Option<String>,
508}
509
510#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
511struct MobToolAuthorityContextSeal {
512 generated: bool,
513}
514
515#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
516struct MobToolAuthorityCapabilities {
517 can_create_mobs: bool,
518 can_mutate_profiles: bool,
519 can_run_adaptive_packs: bool,
520}
521
522impl MobToolAuthorityContextSeal {
523 const fn generated() -> Self {
524 Self { generated: true }
525 }
526}
527
528static EMPTY_MOB_AUTHORITY_MANAGED_SCOPE: LazyLock<BTreeSet<String>> = LazyLock::new(BTreeSet::new);
529static EMPTY_MOB_AUTHORITY_SPAWN_SCOPE: LazyLock<BTreeMap<String, BTreeSet<String>>> =
530 LazyLock::new(BTreeMap::new);
531static UNTRUSTED_MOB_AUTHORITY_PRINCIPAL: LazyLock<OpaquePrincipalToken> =
532 LazyLock::new(|| OpaquePrincipalToken::new("untrusted-mob-authority-context"));
533
534impl MobToolAuthorityContext {
535 #[cfg_attr(
536 not(any(test, meerkat_internal_generated_authority_bridge)),
537 allow(dead_code)
538 )]
539 fn from_generated_parts(
540 principal_token: OpaquePrincipalToken,
541 capabilities: MobToolAuthorityCapabilities,
542 managed_mob_scope: BTreeSet<String>,
543 spawn_profile_scope: BTreeMap<String, BTreeSet<String>>,
544 caller_provenance: Option<MobToolCallerProvenance>,
545 audit_invocation_id: Option<String>,
546 ) -> Result<Self, String> {
547 if principal_token.as_str().trim().is_empty() {
548 return Err("generated mob tool authority requires a non-empty principal token".into());
549 }
550 Ok(Self {
551 generated_authority_seal: MobToolAuthorityContextSeal::generated(),
552 principal_token,
553 can_create_mobs: capabilities.can_create_mobs,
554 can_mutate_profiles: capabilities.can_mutate_profiles,
555 can_run_adaptive_packs: capabilities.can_run_adaptive_packs,
556 managed_mob_scope,
557 spawn_profile_scope,
558 caller_provenance,
559 audit_invocation_id,
560 })
561 }
562
563 #[cfg(test)]
564 #[allow(clippy::expect_used, clippy::too_many_arguments)]
565 pub(crate) fn generated_for_test(
566 principal_token: OpaquePrincipalToken,
567 can_create_mobs: bool,
568 can_mutate_profiles: bool,
569 can_run_adaptive_packs: bool,
570 managed_mob_scope: BTreeSet<String>,
571 spawn_profile_scope: BTreeMap<String, BTreeSet<String>>,
572 caller_provenance: Option<MobToolCallerProvenance>,
573 audit_invocation_id: Option<String>,
574 ) -> Self {
575 Self::from_generated_parts(
576 principal_token,
577 MobToolAuthorityCapabilities {
578 can_create_mobs,
579 can_mutate_profiles,
580 can_run_adaptive_packs,
581 },
582 managed_mob_scope,
583 spawn_profile_scope,
584 caller_provenance,
585 audit_invocation_id,
586 )
587 .expect("test mob authority context must be generated-shape valid")
588 }
589
590 pub fn is_generated_authority_context(&self) -> bool {
596 self.generated_authority_seal.generated
597 }
598
599 pub fn principal_token(&self) -> &OpaquePrincipalToken {
600 if self.is_generated_authority_context() {
601 &self.principal_token
602 } else {
603 &UNTRUSTED_MOB_AUTHORITY_PRINCIPAL
604 }
605 }
606
607 pub fn can_create_mobs(&self) -> bool {
608 self.is_generated_authority_context() && self.can_create_mobs
609 }
610
611 pub fn can_mutate_profiles(&self) -> bool {
612 self.is_generated_authority_context() && self.can_mutate_profiles
613 }
614
615 pub fn can_run_adaptive_packs(&self) -> bool {
616 self.is_generated_authority_context() && self.can_run_adaptive_packs
617 }
618
619 pub fn managed_mob_scope(&self) -> &BTreeSet<String> {
620 if self.is_generated_authority_context() {
621 &self.managed_mob_scope
622 } else {
623 &EMPTY_MOB_AUTHORITY_MANAGED_SCOPE
624 }
625 }
626
627 pub fn spawn_profile_scope(&self) -> &BTreeMap<String, BTreeSet<String>> {
628 if self.is_generated_authority_context() {
629 &self.spawn_profile_scope
630 } else {
631 &EMPTY_MOB_AUTHORITY_SPAWN_SCOPE
632 }
633 }
634
635 pub fn caller_provenance(&self) -> Option<&MobToolCallerProvenance> {
636 self.is_generated_authority_context()
637 .then_some(self.caller_provenance.as_ref())
638 .flatten()
639 }
640
641 pub fn audit_invocation_id(&self) -> Option<&str> {
642 self.is_generated_authority_context()
643 .then_some(self.audit_invocation_id.as_deref())
644 .flatten()
645 }
646
647 pub fn can_manage_mob(&self, mob_id: &str) -> bool {
648 self.managed_mob_scope().contains(mob_id)
649 }
650
651 pub fn spawn_profile_scope_present(&self, mob_id: &str) -> bool {
658 self.spawn_profile_scope()
659 .get(mob_id)
660 .is_some_and(|profiles| !profiles.is_empty())
661 }
662
663 pub fn spawn_profile_scope_contains(&self, mob_id: &str, profile: &str) -> bool {
673 self.spawn_profile_scope()
674 .get(mob_id)
675 .is_some_and(|profiles| profiles.contains(profile))
676 }
677}
678
679#[cfg(all(meerkat_internal_generated_authority_bridge, not(test)))]
680#[allow(improper_ctypes_definitions, unsafe_code)]
681unsafe extern "Rust" {
682 #[link_name = concat!(
683 "__meerkat_runtime_generated_authority_bridge_token_is_valid_v1_mob_operator_authority_",
684 env!("MEERKAT_GENERATED_AUTHORITY_BRIDGE_SYMBOL_SUFFIX")
685 )]
686 fn runtime_mob_operator_authority_generated_authority_bridge_token_is_valid(
687 token: &(dyn std::any::Any + Send + Sync),
688 ) -> bool;
689}
690
691#[cfg(all(meerkat_internal_generated_authority_bridge, not(test)))]
692#[doc(hidden)]
693#[allow(improper_ctypes_definitions, unsafe_code)]
694#[allow(clippy::too_many_arguments)]
695#[unsafe(export_name = concat!(
696 "__meerkat_core_runtime_generated_mob_tool_authority_context_build_v1_",
697 env!("MEERKAT_GENERATED_AUTHORITY_BRIDGE_SYMBOL_SUFFIX")
698))]
699pub(crate) extern "Rust" fn runtime_generated_mob_tool_authority_context_build(
700 token: &'static (dyn std::any::Any + Send + Sync),
701 principal_token: OpaquePrincipalToken,
702 can_create_mobs: bool,
703 can_mutate_profiles: bool,
704 can_run_adaptive_packs: bool,
705 managed_mob_scope: BTreeSet<String>,
706 spawn_profile_scope: BTreeMap<String, BTreeSet<String>>,
707 caller_provenance: Option<MobToolCallerProvenance>,
708 audit_invocation_id: Option<String>,
709) -> Result<MobToolAuthorityContext, String> {
710 #[allow(unsafe_code)]
711 let valid =
712 unsafe { runtime_mob_operator_authority_generated_authority_bridge_token_is_valid(token) };
713 if !valid {
714 return Err(
715 "mob tool authority context requires the generated runtime bridge token".into(),
716 );
717 }
718 MobToolAuthorityContext::from_generated_parts(
719 principal_token,
720 MobToolAuthorityCapabilities {
721 can_create_mobs,
722 can_mutate_profiles,
723 can_run_adaptive_packs,
724 },
725 managed_mob_scope,
726 spawn_profile_scope,
727 caller_provenance,
728 audit_invocation_id,
729 )
730}
731
732#[derive(Clone)]
739pub struct ParentToolCompositionAuthority {
740 inner: Arc<ParentToolCompositionAuthorityInner>,
741}
742
743struct ParentToolCompositionAuthorityInner {
744 tool_scope: RwLock<Option<crate::ToolScope>>,
745}
746
747impl ParentToolCompositionAuthority {
748 #[cfg_attr(not(meerkat_internal_agent_factory_build), allow(dead_code))]
749 fn new_from_agent_factory_composition() -> Self {
750 Self {
751 inner: Arc::new(ParentToolCompositionAuthorityInner {
752 tool_scope: RwLock::new(None),
753 }),
754 }
755 }
756
757 #[cfg_attr(not(meerkat_internal_agent_factory_build), allow(dead_code))]
758 fn set_tool_scope_from_agent_factory_composition(&self, tool_scope: &crate::ToolScope) {
759 let mut guard = self
760 .inner
761 .tool_scope
762 .write()
763 .unwrap_or_else(std::sync::PoisonError::into_inner);
764 *guard = Some(tool_scope.clone());
765 }
766
767 fn current_tool_scope(&self) -> Option<crate::ToolScope> {
768 self.inner
769 .tool_scope
770 .read()
771 .unwrap_or_else(std::sync::PoisonError::into_inner)
772 .clone()
773 }
774
775 fn map_visible_tools_error(
776 err: crate::tool_scope::ToolScopeApplyError,
777 ) -> crate::tool_scope::ToolScopeStageError {
778 match err {
779 crate::tool_scope::ToolScopeApplyError::LockPoisoned => {
780 crate::tool_scope::ToolScopeStageError::LockPoisoned
781 }
782 crate::tool_scope::ToolScopeApplyError::Owner { message } => {
783 crate::tool_scope::ToolScopeStageError::Owner { message }
784 }
785 crate::tool_scope::ToolScopeApplyError::InjectedFailure => {
786 crate::tool_scope::ToolScopeStageError::Owner {
787 message: err.to_string(),
788 }
789 }
790 }
791 }
792
793 fn visible_tool_snapshot_result(
794 &self,
795 ) -> Result<Vec<Arc<ToolDef>>, crate::tool_scope::ToolScopeStageError> {
796 let tool_scope = self.current_tool_scope().ok_or_else(|| {
797 crate::tool_scope::ToolScopeStageError::Owner {
798 message: "parent tool scope is unavailable".to_string(),
799 }
800 })?;
801 tool_scope
802 .visible_tools_result()
803 .map(|tools| tools.iter().cloned().collect())
804 .map_err(Self::map_visible_tools_error)
805 }
806
807 pub fn snapshot_visible_tools(&self) -> Vec<Arc<ToolDef>> {
809 self.visible_tool_snapshot_result().unwrap_or_default()
810 }
811
812 pub fn authorize_inherited_tool_visibility(
815 &self,
816 filter: crate::tool_scope::ToolFilter,
817 ) -> Result<crate::InheritedToolVisibilityAuthority, crate::tool_scope::ToolScopeStageError>
818 {
819 let tools = self.visible_tool_snapshot_result()?;
820 let tool_defs = tools
821 .iter()
822 .map(|tool| tool.as_ref().clone())
823 .collect::<Vec<_>>();
824 let witnesses = crate::tool_scope::filter_witnesses_for_tool_defs(&tool_defs, &filter);
825 crate::tool_scope::validate_witnessed_filter_authority(&filter, &witnesses)?;
826 Ok(
827 crate::InheritedToolVisibilityAuthority::from_generated_composition_authority(
828 filter, witnesses,
829 ),
830 )
831 }
832
833 pub fn authorize_inherited_tool_visibility_with_overlays(
836 &self,
837 allow_overlay: Option<&std::collections::HashSet<String>>,
838 deny_overlay: Option<&std::collections::HashSet<String>>,
839 ) -> Result<crate::InheritedToolVisibilityAuthority, crate::tool_scope::ToolScopeStageError>
840 {
841 let tools = self.visible_tool_snapshot_result()?;
842 let mut names = tools
843 .iter()
844 .map(|tool| tool.name.as_str().to_string())
845 .collect::<std::collections::HashSet<_>>();
846 if let Some(allow) = allow_overlay {
847 names = names.intersection(allow).cloned().collect();
848 }
849 if let Some(deny) = deny_overlay {
850 for name in deny {
851 names.remove(name);
852 }
853 }
854 let filter = crate::tool_scope::ToolFilter::Allow(names.into_iter().collect());
855 let tool_defs = tools
856 .iter()
857 .map(|tool| tool.as_ref().clone())
858 .collect::<Vec<_>>();
859 let witnesses = crate::tool_scope::filter_witnesses_for_tool_defs(&tool_defs, &filter);
860 crate::tool_scope::validate_witnessed_filter_authority(&filter, &witnesses)?;
861 Ok(
862 crate::InheritedToolVisibilityAuthority::from_generated_composition_authority(
863 filter, witnesses,
864 ),
865 )
866 }
867}
868
869#[cfg(all(meerkat_internal_agent_factory_build, not(test)))]
870#[allow(improper_ctypes_definitions, unsafe_code)]
871unsafe extern "Rust" {
872 #[link_name = concat!(
873 "__meerkat_agent_factory_policy_bridge_token_is_valid_v1_",
874 env!("MEERKAT_AGENT_FACTORY_POLICY_BRIDGE_SYMBOL_SUFFIX")
875 )]
876 fn agent_factory_parent_tool_composition_bridge_token_is_valid(
877 factory_bridge_token: &(dyn std::any::Any + Send + Sync),
878 ) -> bool;
879}
880
881#[cfg(all(meerkat_internal_agent_factory_build, not(test)))]
882fn validate_agent_factory_parent_tool_composition_bridge_token(
883 token: &(dyn std::any::Any + Send + Sync),
884) -> Result<(), String> {
885 #[allow(unsafe_code)]
886 let is_valid = unsafe { agent_factory_parent_tool_composition_bridge_token_is_valid(token) };
887 if is_valid {
888 Ok(())
889 } else {
890 Err("parent tool composition authority requires the AgentFactory bridge token".into())
891 }
892}
893
894#[cfg(all(meerkat_internal_agent_factory_build, not(test)))]
895#[doc(hidden)]
896#[allow(improper_ctypes_definitions, unsafe_code)]
897#[unsafe(export_name = concat!(
898 "__meerkat_agent_factory_parent_tool_composition_authority_new_v1_",
899 env!("MEERKAT_AGENT_FACTORY_POLICY_BRIDGE_SYMBOL_SUFFIX")
900))]
901pub(crate) extern "Rust" fn agent_factory_parent_tool_composition_authority_new(
902 token: &'static (dyn std::any::Any + Send + Sync),
903) -> Result<ParentToolCompositionAuthority, String> {
904 validate_agent_factory_parent_tool_composition_bridge_token(token)?;
905 Ok(ParentToolCompositionAuthority::new_from_agent_factory_composition())
906}
907
908#[cfg(all(meerkat_internal_agent_factory_build, not(test)))]
909#[doc(hidden)]
910#[allow(improper_ctypes_definitions, unsafe_code)]
911#[unsafe(export_name = concat!(
912 "__meerkat_agent_factory_parent_tool_composition_authority_set_tool_scope_v1_",
913 env!("MEERKAT_AGENT_FACTORY_POLICY_BRIDGE_SYMBOL_SUFFIX")
914))]
915pub(crate) extern "Rust" fn agent_factory_parent_tool_composition_authority_set_tool_scope(
916 token: &'static (dyn std::any::Any + Send + Sync),
917 authority: &ParentToolCompositionAuthority,
918 tool_scope: &crate::ToolScope,
919) -> Result<(), String> {
920 validate_agent_factory_parent_tool_composition_bridge_token(token)?;
921 authority.set_tool_scope_from_agent_factory_composition(tool_scope);
922 Ok(())
923}
924
925#[derive(Clone)]
931pub enum MobToolSnapshotContext {
932 ParentOwned(ParentToolCompositionAuthority),
934 Standalone,
936}
937
938pub struct MobToolsBuildArgs {
940 pub session_id: crate::SessionId,
942 pub model: String,
944 pub authority_context: Option<MobToolAuthorityContext>,
950 pub effective_authority: Option<Arc<std::sync::RwLock<MobToolAuthorityContext>>>,
958 pub comms_name: Option<String>,
960 pub comms_runtime: Option<Arc<dyn crate::agent::CommsRuntime>>,
962 pub snapshot_context: MobToolSnapshotContext,
964}
965
966#[cfg_attr(target_arch = "wasm32", async_trait(?Send))]
972#[cfg_attr(not(target_arch = "wasm32"), async_trait)]
973pub trait MobToolsFactory: Send + Sync {
974 async fn build_mob_tools(
976 &self,
977 args: MobToolsBuildArgs,
978 ) -> Result<Arc<dyn AgentToolDispatcher>, Box<dyn std::error::Error + Send + Sync>>;
979}
980
981#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
985pub struct ResumeOverrideMask {
986 pub model: bool,
987 pub provider: bool,
988 pub max_tokens: bool,
989 pub structured_output_retries: bool,
990 pub provider_params: bool,
991 pub auth_binding: bool,
992 pub override_builtins: bool,
993 pub override_shell: bool,
994 pub override_comms: bool,
995 pub override_memory: bool,
996 pub override_schedule: bool,
997 pub override_workgraph: bool,
998 pub override_mob: bool,
999 pub override_image_generation: bool,
1000 pub override_web_search: bool,
1001 pub preload_skills: bool,
1002 pub keep_alive: bool,
1003 pub comms_name: bool,
1004 pub peer_meta: bool,
1005 pub tool_access_policy: bool,
1007}
1008
1009impl SessionBuildOptions {
1010 pub fn apply_persisted_mob_operator_access(
1018 &mut self,
1019 enable_mob: ToolCategoryOverride,
1020 persisted_authority_context: Option<MobToolAuthorityContext>,
1021 ) {
1022 if matches!(enable_mob, ToolCategoryOverride::Disable) {
1023 self.override_mob = ToolCategoryOverride::Disable;
1024 self.mob_tool_authority_context = None;
1025 return;
1026 }
1027
1028 let generated_authority_context = persisted_authority_context
1029 .filter(MobToolAuthorityContext::is_generated_authority_context);
1030 let has_generated_authority = generated_authority_context.is_some();
1031 self.override_mob = if has_generated_authority {
1032 ToolCategoryOverride::Enable
1033 } else {
1034 ToolCategoryOverride::Inherit
1035 };
1036 self.mob_tool_authority_context = generated_authority_context;
1037 }
1038
1039 pub fn apply_generated_create_only_mob_operator_access(
1045 &mut self,
1046 enable_mob: ToolCategoryOverride,
1047 ) {
1048 self.override_mob = enable_mob;
1049 self.mob_tool_authority_context = None;
1050 }
1051}
1052
1053impl Default for SessionBuildOptions {
1054 fn default() -> Self {
1055 Self {
1056 provider: None,
1057 self_hosted_server_id: None,
1058 custom_models: BTreeMap::new(),
1059 image_generation_provider: None,
1060 auto_compact_threshold_override: None,
1061 output_schema: None,
1062 structured_output_retries: None,
1063 hooks_override: HookRunOverrides::default(),
1064 comms_name: None,
1065 peer_meta: None,
1066 resume_session: None,
1067 budget_limits: None,
1070 provider_params: None,
1071 external_tools: None,
1072 mcp_servers: Vec::new(),
1073 recoverable_tool_defs: None,
1074 blob_store_override: None,
1075 llm_client_override: None,
1076 agent_llm_client_decorator: None,
1077 override_builtins: ToolCategoryOverride::Inherit,
1078 override_shell: ToolCategoryOverride::Inherit,
1079 override_comms: ToolCategoryOverride::Inherit,
1080 override_memory: ToolCategoryOverride::Inherit,
1081 override_schedule: ToolCategoryOverride::Inherit,
1082 override_workgraph: ToolCategoryOverride::Inherit,
1083 override_mob: ToolCategoryOverride::Inherit,
1084 override_image_generation: ToolCategoryOverride::Inherit,
1085 override_web_search: ToolCategoryOverride::Inherit,
1086 schedule_tools: None,
1087 workgraph_tools: None,
1088 preload_skills: None,
1089 realm_id: None,
1090 instance_id: None,
1091 backend: None,
1092 config_generation: None,
1093 auth_binding: None,
1094 mob_member_binding: None,
1095 keep_alive: false,
1096 checkpointer: None,
1097 silent_comms_intents: Vec::new(),
1098 max_inline_peer_notifications: None,
1099 app_context: None,
1100 additional_instructions: None,
1101 initial_metadata_entries: BTreeMap::new(),
1102 initial_tool_filter: None,
1103 tool_access_policy: None,
1104 shell_env: None,
1105 call_timeout_override: crate::CallTimeoutOverride::Inherit,
1106 resume_override_mask: ResumeOverrideMask::default(),
1107 mob_tools: None,
1108 runtime_build_mode: crate::runtime_epoch::RuntimeBuildMode::StandaloneEphemeral,
1109 initial_turn_metadata: None,
1110 mob_tool_authority_context: None,
1111 }
1112 }
1113}
1114
1115impl std::fmt::Debug for SessionBuildOptions {
1116 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1117 f.debug_struct("SessionBuildOptions")
1118 .field("provider", &self.provider)
1119 .field("custom_models", &self.custom_models.keys())
1120 .field("image_generation_provider", &self.image_generation_provider)
1121 .field(
1122 "auto_compact_threshold_override",
1123 &self.auto_compact_threshold_override,
1124 )
1125 .field("output_schema", &self.output_schema.is_some())
1126 .field("structured_output_retries", &self.structured_output_retries)
1127 .field("hooks_override", &self.hooks_override)
1128 .field("comms_name", &self.comms_name)
1129 .field("peer_meta", &self.peer_meta)
1130 .field("resume_session", &self.resume_session.is_some())
1131 .field("budget_limits", &self.budget_limits)
1132 .field("provider_params", &self.provider_params.is_some())
1133 .field("external_tools", &self.external_tools.is_some())
1134 .field("recoverable_tool_defs", &self.recoverable_tool_defs)
1135 .field("blob_store_override", &self.blob_store_override.is_some())
1136 .field("llm_client_override", &self.llm_client_override.is_some())
1137 .field(
1138 "agent_llm_client_decorator",
1139 &self.agent_llm_client_decorator.is_some(),
1140 )
1141 .field("override_builtins", &self.override_builtins)
1142 .field("override_shell", &self.override_shell)
1143 .field("override_comms", &self.override_comms)
1144 .field("override_memory", &self.override_memory)
1145 .field("override_schedule", &self.override_schedule)
1146 .field("override_workgraph", &self.override_workgraph)
1147 .field("override_mob", &self.override_mob)
1148 .field("schedule_tools", &self.schedule_tools.is_some())
1149 .field("workgraph_tools", &self.workgraph_tools.is_some())
1150 .field("preload_skills", &self.preload_skills)
1151 .field("realm_id", &self.realm_id)
1152 .field("instance_id", &self.instance_id)
1153 .field("backend", &self.backend)
1154 .field("config_generation", &self.config_generation)
1155 .field("keep_alive", &self.keep_alive)
1156 .field("checkpointer", &self.checkpointer.is_some())
1157 .field("silent_comms_intents", &self.silent_comms_intents)
1158 .field(
1159 "max_inline_peer_notifications",
1160 &self.max_inline_peer_notifications,
1161 )
1162 .field("app_context", &self.app_context.is_some())
1163 .field("additional_instructions", &self.additional_instructions)
1164 .field("initial_metadata_entries", &self.initial_metadata_entries)
1165 .field("initial_tool_filter", &self.initial_tool_filter.is_some())
1166 .field("tool_access_policy", &self.tool_access_policy)
1167 .field("call_timeout_override", &self.call_timeout_override)
1168 .field("resume_override_mask", &self.resume_override_mask)
1169 .field("mob_tools", &self.mob_tools.is_some())
1170 .field("runtime_build_mode", &self.runtime_build_mode)
1171 .field(
1172 "initial_turn_metadata",
1173 &self.initial_turn_metadata.is_some(),
1174 )
1175 .field(
1176 "mob_tool_authority_context",
1177 &self.mob_tool_authority_context.is_some(),
1178 )
1179 .field("runtime_build_mode", &self.runtime_build_mode)
1180 .finish()
1181 }
1182}
1183
1184#[derive(Debug)]
1190pub struct StartTurnRuntimeSemantics {
1191 pub handling_mode: HandlingMode,
1198 pub turn_tool_overlay: Option<TurnToolOverlay>,
1200 pub pre_turn_context_appends: Vec<PendingSystemContextAppend>,
1203 pub typed_turn_appends: Vec<ConversationAppend>,
1209 pub turn_metadata: Option<RuntimeTurnMetadata>,
1217}
1218
1219impl Default for StartTurnRuntimeSemantics {
1220 fn default() -> Self {
1221 Self {
1222 handling_mode: HandlingMode::Queue,
1223 turn_tool_overlay: None,
1224 pre_turn_context_appends: Vec::new(),
1225 typed_turn_appends: Vec::new(),
1226 turn_metadata: None,
1227 }
1228 }
1229}
1230
1231impl StartTurnRuntimeSemantics {
1232 #[must_use]
1233 pub fn new(
1234 handling_mode: HandlingMode,
1235 turn_tool_overlay: Option<TurnToolOverlay>,
1236 pre_turn_context_appends: Vec<PendingSystemContextAppend>,
1237 turn_metadata: Option<RuntimeTurnMetadata>,
1238 ) -> Self {
1239 Self {
1240 handling_mode,
1241 turn_tool_overlay,
1242 pre_turn_context_appends,
1243 typed_turn_appends: Vec::new(),
1244 turn_metadata,
1245 }
1246 }
1247
1248 #[must_use]
1249 pub fn runtime_metadata(turn_metadata: RuntimeTurnMetadata) -> Self {
1250 Self {
1251 turn_metadata: Some(turn_metadata),
1252 ..Self::default()
1253 }
1254 }
1255
1256 #[must_use]
1257 pub fn with_typed_turn_appends(mut self, typed_turn_appends: Vec<ConversationAppend>) -> Self {
1258 self.typed_turn_appends = typed_turn_appends;
1259 self
1260 }
1261}
1262
1263#[derive(Debug)]
1265pub struct StartTurnRequest {
1266 pub prompt: ContentInput,
1268 pub injected_context: Vec<ContentInput>,
1277 pub system_prompt: Option<String>,
1282 pub event_tx: Option<mpsc::Sender<EventEnvelope<AgentEvent>>>,
1284 pub runtime: StartTurnRuntimeSemantics,
1286}
1287
1288#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
1292pub struct AppendSystemContextRequest {
1293 pub content: CoreRenderable,
1301 #[serde(default, skip_serializing_if = "Option::is_none")]
1302 pub source: Option<String>,
1303 #[serde(default, skip_serializing_if = "Option::is_none")]
1304 pub idempotency_key: Option<String>,
1305 #[serde(
1312 default,
1313 skip_serializing_if = "crate::session::SystemContextSource::is_normal"
1314 )]
1315 pub source_kind: crate::session::SystemContextSource,
1316 #[serde(default, skip_serializing_if = "Option::is_none")]
1321 pub peer_response_terminal: Option<crate::handles::PeerResponseTerminalFact>,
1322}
1323
1324impl AppendSystemContextRequest {
1325 #[must_use]
1331 pub fn from_text(text: impl Into<String>) -> Self {
1332 Self {
1333 content: CoreRenderable::text(text),
1334 source: None,
1335 idempotency_key: None,
1336 source_kind: crate::session::SystemContextSource::Normal,
1337 peer_response_terminal: None,
1338 }
1339 }
1340
1341 #[must_use]
1343 pub fn text(&self) -> String {
1344 self.content.render_text()
1345 }
1346}
1347
1348#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
1350pub struct AppendSystemContextResult {
1351 pub status: AppendSystemContextStatus,
1352}
1353
1354#[derive(Debug, Clone, Serialize, Deserialize)]
1356pub struct StageToolResultsRequest {
1357 pub results: Vec<crate::ToolResult>,
1358}
1359
1360#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
1362pub struct StageToolResultsResult {
1363 pub accepted_result_count: usize,
1364}
1365
1366#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
1368#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
1369#[serde(rename_all = "snake_case")]
1370pub enum AppendSystemContextStatus {
1371 Applied,
1372 Staged,
1373 Duplicate,
1374}
1375
1376#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
1378#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
1379pub struct TurnToolOverlay {
1380 #[serde(default)]
1382 pub allowed_tools: Option<Vec<crate::types::ToolName>>,
1383 #[serde(default)]
1385 pub blocked_tools: Option<Vec<crate::types::ToolName>>,
1386 #[serde(default, skip_serializing_if = "std::collections::BTreeMap::is_empty")]
1388 #[cfg_attr(feature = "schema", schemars(skip))]
1389 pub dispatch_context: std::collections::BTreeMap<String, serde_json::Value>,
1390}
1391
1392impl TurnToolOverlay {
1393 pub fn without_dispatch_context(mut self) -> Self {
1395 self.dispatch_context.clear();
1396 self
1397 }
1398}
1399
1400#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
1402#[serde(deny_unknown_fields)]
1403#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
1404pub struct PublicTurnToolOverlay {
1405 #[serde(default)]
1407 pub allowed_tools: Option<Vec<crate::types::ToolName>>,
1408 #[serde(default)]
1410 pub blocked_tools: Option<Vec<crate::types::ToolName>>,
1411}
1412
1413impl From<PublicTurnToolOverlay> for TurnToolOverlay {
1414 fn from(value: PublicTurnToolOverlay) -> Self {
1415 Self {
1416 allowed_tools: value.allowed_tools,
1417 blocked_tools: value.blocked_tools,
1418 dispatch_context: BTreeMap::new(),
1419 }
1420 }
1421}
1422
1423#[derive(Debug, Default)]
1425pub struct SessionQuery {
1426 pub limit: Option<usize>,
1428 pub offset: Option<usize>,
1430 pub labels: Option<BTreeMap<String, String>>,
1432}
1433
1434#[derive(Debug, Clone, Serialize, Deserialize)]
1438pub struct SessionSummary {
1439 pub session_id: SessionId,
1440 pub created_at: SystemTime,
1441 pub updated_at: SystemTime,
1442 pub message_count: usize,
1443 pub total_tokens: u64,
1444 pub is_active: bool,
1445 #[serde(default, skip_serializing_if = "BTreeMap::is_empty")]
1446 pub labels: BTreeMap<String, String>,
1447}
1448
1449#[derive(Debug, Clone, Serialize, Deserialize)]
1451pub struct SessionInfo {
1452 pub session_id: SessionId,
1453 pub created_at: SystemTime,
1454 pub updated_at: SystemTime,
1455 pub message_count: usize,
1456 pub is_active: bool,
1457 pub model: String,
1458 pub provider: Provider,
1459 pub last_assistant_text: Option<String>,
1460 #[serde(default, skip_serializing_if = "BTreeMap::is_empty")]
1461 pub labels: BTreeMap<String, String>,
1462}
1463
1464#[derive(Debug, Clone, Serialize, Deserialize)]
1466pub struct SessionUsage {
1467 pub total_tokens: u64,
1468 pub usage: Usage,
1469}
1470
1471#[derive(Debug, Clone, Serialize, Deserialize)]
1474pub struct SessionView {
1475 pub state: SessionInfo,
1476 pub billing: SessionUsage,
1477}
1478
1479impl SessionView {
1480 pub fn session_id(&self) -> &SessionId {
1482 &self.state.session_id
1483 }
1484}
1485
1486#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq, Eq)]
1488pub struct SessionHistoryQuery {
1489 pub offset: usize,
1491 #[serde(default, skip_serializing_if = "Option::is_none")]
1493 pub limit: Option<usize>,
1494}
1495
1496#[derive(Debug, Clone, Serialize, Deserialize)]
1498pub struct SessionHistoryPage {
1499 pub session_id: SessionId,
1500 pub message_count: usize,
1501 pub offset: usize,
1502 #[serde(default, skip_serializing_if = "Option::is_none")]
1503 pub limit: Option<usize>,
1504 pub has_more: bool,
1505 pub messages: Vec<Message>,
1506}
1507
1508impl SessionHistoryPage {
1509 pub fn from_messages(
1511 session_id: SessionId,
1512 messages: &[Message],
1513 query: SessionHistoryQuery,
1514 ) -> Self {
1515 let message_count = messages.len();
1516 let start = query.offset.min(message_count);
1517 let end = match query.limit {
1518 Some(limit) => start.saturating_add(limit).min(message_count),
1519 None => message_count,
1520 };
1521 Self {
1522 session_id,
1523 message_count,
1524 offset: start,
1525 limit: query.limit,
1526 has_more: end < message_count,
1527 messages: messages[start..end].to_vec(),
1528 }
1529 }
1530}
1531
1532#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
1534pub struct SessionTranscriptRevisionQuery {
1535 pub revision: String,
1536 pub offset: usize,
1538 #[serde(default, skip_serializing_if = "Option::is_none")]
1540 pub limit: Option<usize>,
1541}
1542
1543#[derive(Debug, Clone, Serialize, Deserialize)]
1545pub struct SessionTranscriptRevisionPage {
1546 pub session_id: SessionId,
1547 pub revision: String,
1548 pub head_revision: String,
1549 pub message_count: usize,
1550 pub offset: usize,
1551 #[serde(default, skip_serializing_if = "Option::is_none")]
1552 pub limit: Option<usize>,
1553 pub has_more: bool,
1554 pub messages: Vec<Message>,
1555}
1556
1557impl SessionTranscriptRevisionPage {
1558 pub fn from_messages(
1560 session_id: SessionId,
1561 revision: String,
1562 head_revision: String,
1563 messages: &[Message],
1564 offset: usize,
1565 limit: Option<usize>,
1566 ) -> Self {
1567 let message_count = messages.len();
1568 let start = offset.min(message_count);
1569 let end = match limit {
1570 Some(limit) => start.saturating_add(limit).min(message_count),
1571 None => message_count,
1572 };
1573 Self {
1574 session_id,
1575 revision,
1576 head_revision,
1577 message_count,
1578 offset: start,
1579 limit,
1580 has_more: end < message_count,
1581 messages: messages[start..end].to_vec(),
1582 }
1583 }
1584}
1585
1586#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq, Eq)]
1588pub struct SessionTranscriptRevisionListQuery {
1589 #[serde(default, skip_serializing_if = "Option::is_none")]
1591 pub limit: Option<usize>,
1592 #[serde(default, skip_serializing_if = "Option::is_none")]
1594 pub offset: Option<usize>,
1595}
1596
1597#[derive(Debug, Clone, Serialize, Deserialize)]
1603pub struct SessionTranscriptRevisionListEntry {
1604 pub revision: String,
1606 pub parent_revision: String,
1608 #[serde(default, skip_serializing_if = "Option::is_none")]
1610 pub actor: Option<String>,
1611 pub reason: String,
1613 pub committed_at: SystemTime,
1615}
1616
1617#[derive(Debug, Clone, Serialize, Deserialize)]
1619pub struct SessionTranscriptRevisionList {
1620 pub entries: Vec<SessionTranscriptRevisionListEntry>,
1622 pub head_revision: String,
1624}
1625
1626#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Serialize, Deserialize)]
1629#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
1630#[serde(rename_all = "snake_case")]
1631pub enum TranscriptEditRunningBehavior {
1632 #[default]
1634 Reject,
1635}
1636
1637#[derive(Debug, Clone, Serialize, Deserialize)]
1639#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
1640pub struct SessionForkAtRequest {
1641 pub message_index: usize,
1642 #[serde(default)]
1643 pub running_behavior: TranscriptEditRunningBehavior,
1644}
1645
1646#[derive(Debug, Clone, Serialize, Deserialize)]
1648#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
1649pub struct SessionForkReplaceRequest {
1650 pub message_index: usize,
1651 #[cfg_attr(feature = "schema", schemars(with = "serde_json::Value"))]
1652 pub replacement: TranscriptReplacement,
1653 #[serde(default)]
1654 pub running_behavior: TranscriptEditRunningBehavior,
1655}
1656
1657#[derive(Debug, Clone, Serialize, Deserialize)]
1659#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
1660pub struct SessionForkResult {
1661 #[cfg_attr(feature = "schema", schemars(with = "String"))]
1662 pub source_session_id: SessionId,
1663 #[cfg_attr(feature = "schema", schemars(with = "String"))]
1664 pub session_id: SessionId,
1665 pub message_count: usize,
1666 #[serde(default, skip_serializing_if = "Option::is_none")]
1667 pub session_ref: Option<String>,
1668}
1669
1670#[derive(Debug, Clone, Serialize, Deserialize)]
1672#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
1673pub struct SessionTranscriptRewriteRequest {
1674 pub selection: TranscriptRewriteSelection,
1675 #[cfg_attr(feature = "schema", schemars(with = "Vec<serde_json::Value>"))]
1676 pub replacement: Vec<Message>,
1677 pub reason: TranscriptRewriteReason,
1678 #[serde(default, skip_serializing_if = "Option::is_none")]
1679 pub actor: Option<String>,
1680 #[serde(default, skip_serializing_if = "Option::is_none")]
1681 pub expected_parent_revision: Option<String>,
1682 #[serde(default)]
1683 pub running_behavior: TranscriptEditRunningBehavior,
1684}
1685
1686#[derive(Debug, Clone, Serialize, Deserialize)]
1688#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
1689pub struct SessionTranscriptRestoreRevisionRequest {
1690 pub revision: String,
1691 pub reason: TranscriptRewriteReason,
1692 #[serde(default, skip_serializing_if = "Option::is_none")]
1693 pub actor: Option<String>,
1694 #[serde(default, skip_serializing_if = "Option::is_none")]
1695 pub expected_parent_revision: Option<String>,
1696 #[serde(default)]
1697 pub running_behavior: TranscriptEditRunningBehavior,
1698}
1699
1700#[derive(Debug, Clone, Serialize, Deserialize)]
1702#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
1703pub struct SessionTranscriptRewriteResult {
1704 #[cfg_attr(feature = "schema", schemars(with = "String"))]
1705 pub session_id: SessionId,
1706 pub parent_revision: String,
1707 pub revision: String,
1708 pub message_count: usize,
1709 pub commit: TranscriptRewriteCommit,
1710}
1711
1712impl TranscriptEditError {
1713 pub fn into_session_error(self) -> SessionError {
1716 SessionError::Agent(crate::error::AgentError::ConfigError(self.to_string()))
1717 }
1718}
1719
1720#[cfg_attr(target_arch = "wasm32", async_trait(?Send))]
1725#[cfg_attr(not(target_arch = "wasm32"), async_trait)]
1726pub trait SessionService: Send + Sync {
1727 async fn create_session(&self, req: CreateSessionRequest) -> Result<RunResult, SessionError>;
1729
1730 async fn start_turn(
1732 &self,
1733 id: &SessionId,
1734 req: StartTurnRequest,
1735 ) -> Result<RunResult, SessionError>;
1736
1737 async fn interrupt(&self, id: &SessionId) -> Result<(), SessionError>;
1741
1742 async fn cancel_after_boundary(&self, _id: &SessionId) -> Result<(), SessionError> {
1746 Err(SessionError::Unsupported(
1747 "cancel_after_boundary".to_string(),
1748 ))
1749 }
1750
1751 async fn set_session_client(
1758 &self,
1759 _id: &SessionId,
1760 _client: std::sync::Arc<dyn crate::AgentLlmClient>,
1761 ) -> Result<(), SessionError> {
1762 Err(SessionError::Unsupported("set_session_client".to_string()))
1763 }
1764
1765 async fn hot_swap_session_llm_identity(
1772 &self,
1773 _id: &SessionId,
1774 _client: std::sync::Arc<dyn crate::AgentLlmClient>,
1775 _identity: SessionLlmIdentity,
1776 _request_policy: crate::SessionLlmRequestPolicy,
1777 ) -> Result<(), SessionError> {
1778 Err(SessionError::Unsupported(
1779 "hot_swap_session_llm_identity".to_string(),
1780 ))
1781 }
1782
1783 async fn set_session_tool_visibility_state(
1788 &self,
1789 _id: &SessionId,
1790 _state: Option<crate::SessionToolVisibilityState>,
1791 ) -> Result<(), SessionError> {
1792 Err(SessionError::Unsupported(
1793 "set_session_tool_visibility_state".to_string(),
1794 ))
1795 }
1796
1797 async fn update_session_mob_authority_context(
1803 &self,
1804 _id: &SessionId,
1805 _authority_context: Option<MobToolAuthorityContext>,
1806 ) -> Result<(), SessionError> {
1807 Err(SessionError::Unsupported(
1808 "update_session_mob_authority_context".to_string(),
1809 ))
1810 }
1811
1812 async fn has_live_session(&self, _id: &SessionId) -> Result<bool, SessionError> {
1818 Err(SessionError::Unsupported("has_live_session".to_string()))
1819 }
1820
1821 async fn set_session_tool_filter(
1827 &self,
1828 _id: &SessionId,
1829 _filter: crate::ToolFilter,
1830 ) -> Result<(), SessionError> {
1831 Err(SessionError::Unsupported(
1832 "set_session_tool_filter".to_string(),
1833 ))
1834 }
1835
1836 async fn read(&self, id: &SessionId) -> Result<SessionView, SessionError>;
1838
1839 async fn list(&self, query: SessionQuery) -> Result<Vec<SessionSummary>, SessionError>;
1841
1842 async fn archive(&self, id: &SessionId) -> Result<(), SessionError>;
1844
1845 async fn subscribe_session_events(&self, id: &SessionId) -> Result<EventStream, StreamError> {
1849 Err(StreamError::NotFound(format!("session {id}")))
1850 }
1851
1852 async fn record_live_terminal_error(
1865 &self,
1866 _id: &SessionId,
1867 _cause: crate::live_adapter::LiveAdapterErrorCode,
1868 ) -> Result<(), SessionError> {
1869 Err(SessionError::Unsupported(
1870 "record_live_terminal_error".to_string(),
1871 ))
1872 }
1873
1874 async fn record_live_output_audio_degraded(
1886 &self,
1887 _id: &SessionId,
1888 _dropped: u64,
1889 ) -> Result<(), SessionError> {
1890 Err(SessionError::Unsupported(
1891 "record_live_output_audio_degraded".to_string(),
1892 ))
1893 }
1894}
1895
1896#[cfg_attr(target_arch = "wasm32", async_trait(?Send))]
1902#[cfg_attr(not(target_arch = "wasm32"), async_trait)]
1903pub trait SessionServiceCommsExt: SessionService {
1904 async fn comms_runtime(
1906 &self,
1907 _session_id: &SessionId,
1908 ) -> Option<Arc<dyn crate::agent::CommsRuntime>> {
1909 None
1910 }
1911
1912 async fn event_injector(
1914 &self,
1915 session_id: &SessionId,
1916 ) -> Option<Arc<dyn crate::EventInjector>> {
1917 self.comms_runtime(session_id)
1918 .await
1919 .and_then(|runtime| runtime.event_injector())
1920 }
1921
1922 #[doc(hidden)]
1924 async fn interaction_event_injector(
1925 &self,
1926 session_id: &SessionId,
1927 ) -> Option<Arc<dyn crate::event_injector::SubscribableInjector>> {
1928 self.comms_runtime(session_id)
1929 .await
1930 .and_then(|runtime| runtime.interaction_event_injector())
1931 }
1932}
1933
1934#[cfg_attr(target_arch = "wasm32", async_trait(?Send))]
1939#[cfg_attr(not(target_arch = "wasm32"), async_trait)]
1940pub trait SessionServiceControlExt: SessionService {
1941 async fn append_system_context(
1947 &self,
1948 id: &SessionId,
1949 req: AppendSystemContextRequest,
1950 ) -> Result<AppendSystemContextResult, SessionControlError>;
1951
1952 async fn stage_tool_results(
1958 &self,
1959 id: &SessionId,
1960 req: StageToolResultsRequest,
1961 ) -> Result<StageToolResultsResult, SessionError> {
1962 let _ = (id, req);
1963 Err(SessionError::Unsupported("stage_tool_results".to_string()))
1964 }
1965}
1966
1967#[cfg_attr(target_arch = "wasm32", async_trait(?Send))]
1972#[cfg_attr(not(target_arch = "wasm32"), async_trait)]
1973pub trait SessionServiceHistoryExt: SessionService {
1974 async fn read_history(
1979 &self,
1980 id: &SessionId,
1981 query: SessionHistoryQuery,
1982 ) -> Result<SessionHistoryPage, SessionError>;
1983
1984 async fn read_transcript_revision(
1986 &self,
1987 id: &SessionId,
1988 query: SessionTranscriptRevisionQuery,
1989 ) -> Result<SessionTranscriptRevisionPage, SessionError> {
1990 let _ = (id, query);
1991 Err(SessionError::Unsupported(
1992 "read_transcript_revision".to_string(),
1993 ))
1994 }
1995
1996 async fn list_transcript_revisions(
1999 &self,
2000 id: &SessionId,
2001 query: SessionTranscriptRevisionListQuery,
2002 ) -> Result<SessionTranscriptRevisionList, SessionError> {
2003 let _ = (id, query);
2004 Err(SessionError::Unsupported(
2005 "list_transcript_revisions".to_string(),
2006 ))
2007 }
2008}
2009
2010#[cfg_attr(target_arch = "wasm32", async_trait(?Send))]
2016#[cfg_attr(not(target_arch = "wasm32"), async_trait)]
2017pub trait SessionServiceTranscriptEditExt: SessionService {
2018 async fn fork_session_at(
2020 &self,
2021 id: &SessionId,
2022 req: SessionForkAtRequest,
2023 ) -> Result<SessionForkResult, SessionError> {
2024 let _ = (id, req);
2025 Err(SessionError::Unsupported("fork_session_at".to_string()))
2026 }
2027
2028 async fn fork_session_replace(
2030 &self,
2031 id: &SessionId,
2032 req: SessionForkReplaceRequest,
2033 ) -> Result<SessionForkResult, SessionError> {
2034 let _ = (id, req);
2035 Err(SessionError::Unsupported(
2036 "fork_session_replace".to_string(),
2037 ))
2038 }
2039
2040 async fn rewrite_session_transcript(
2042 &self,
2043 id: &SessionId,
2044 req: SessionTranscriptRewriteRequest,
2045 ) -> Result<SessionTranscriptRewriteResult, SessionError> {
2046 let _ = (id, req);
2047 Err(SessionError::Unsupported(
2048 "rewrite_session_transcript".to_string(),
2049 ))
2050 }
2051
2052 async fn restore_session_transcript_revision(
2054 &self,
2055 id: &SessionId,
2056 req: SessionTranscriptRestoreRevisionRequest,
2057 ) -> Result<SessionTranscriptRewriteResult, SessionError> {
2058 let _ = (id, req);
2059 Err(SessionError::Unsupported(
2060 "restore_session_transcript_revision".to_string(),
2061 ))
2062 }
2063}
2064
2065impl dyn SessionService {
2067 pub fn into_arc(self: Box<Self>) -> Arc<dyn SessionService> {
2069 Arc::from(self)
2070 }
2071}
2072
2073#[cfg(test)]
2074#[allow(
2075 clippy::unimplemented,
2076 clippy::unwrap_used,
2077 clippy::expect_used,
2078 clippy::panic
2079)]
2080mod tests {
2081 use super::*;
2082
2083 struct UnsupportedSessionService;
2084
2085 #[cfg_attr(target_arch = "wasm32", async_trait(?Send))]
2086 #[cfg_attr(not(target_arch = "wasm32"), async_trait)]
2087 impl SessionService for UnsupportedSessionService {
2088 async fn create_session(
2089 &self,
2090 _req: CreateSessionRequest,
2091 ) -> Result<RunResult, SessionError> {
2092 unimplemented!()
2093 }
2094
2095 async fn start_turn(
2096 &self,
2097 _id: &SessionId,
2098 _req: StartTurnRequest,
2099 ) -> Result<RunResult, SessionError> {
2100 unimplemented!()
2101 }
2102
2103 async fn interrupt(&self, _id: &SessionId) -> Result<(), SessionError> {
2104 unimplemented!()
2105 }
2106
2107 async fn read(&self, _id: &SessionId) -> Result<SessionView, SessionError> {
2108 unimplemented!()
2109 }
2110
2111 async fn list(&self, _query: SessionQuery) -> Result<Vec<SessionSummary>, SessionError> {
2112 unimplemented!()
2113 }
2114
2115 async fn archive(&self, _id: &SessionId) -> Result<(), SessionError> {
2116 unimplemented!()
2117 }
2118 }
2119
2120 #[tokio::test]
2121 async fn has_live_session_defaults_to_unsupported() {
2122 let service = UnsupportedSessionService;
2123 let err = service
2124 .has_live_session(&SessionId::new())
2125 .await
2126 .expect_err("default implementation should fail loudly");
2127 assert!(matches!(err, SessionError::Unsupported(name) if name == "has_live_session"));
2128 }
2129
2130 #[test]
2131 fn spawn_profile_scope_allows_only_granted_profile_without_manage_scope() {
2132 let ctx = MobToolAuthorityContext::generated_for_test(
2133 OpaquePrincipalToken::new("generated-test"),
2134 false,
2135 false,
2136 false,
2137 BTreeSet::new(),
2138 BTreeMap::from([(
2139 "mob-1".to_string(),
2140 BTreeSet::from(["investigator".to_string()]),
2141 )]),
2142 None,
2143 None,
2144 );
2145
2146 assert!(ctx.spawn_profile_scope_present("mob-1"));
2147 assert!(ctx.spawn_profile_scope_contains("mob-1", "investigator"));
2148 assert!(!ctx.spawn_profile_scope_contains("mob-1", "writer"));
2149 assert!(!ctx.can_manage_mob("mob-1"));
2150 }
2151
2152 #[test]
2153 fn deserialized_mob_tool_authority_context_is_projection_only() {
2154 let ctx = MobToolAuthorityContext::generated_for_test(
2155 OpaquePrincipalToken::new("generated-test"),
2156 true,
2157 true,
2158 false,
2159 BTreeSet::from(["mob-1".to_string()]),
2160 BTreeMap::from([(
2161 "mob-1".to_string(),
2162 BTreeSet::from(["investigator".to_string()]),
2163 )]),
2164 Some(MobToolCallerProvenance::default().with_mob_id("mob-1")),
2165 Some("audit-1".to_string()),
2166 );
2167
2168 let restored: MobToolAuthorityContext =
2169 serde_json::from_value(serde_json::to_value(ctx).unwrap()).unwrap();
2170
2171 assert!(!restored.is_generated_authority_context());
2172 assert!(!restored.can_create_mobs());
2173 assert!(!restored.can_mutate_profiles());
2174 assert!(!restored.can_manage_mob("mob-1"));
2175 assert!(!restored.spawn_profile_scope_present("mob-1"));
2176 assert!(restored.caller_provenance().is_none());
2177 assert!(restored.audit_invocation_id().is_none());
2178 }
2179
2180 #[test]
2181 fn persisted_mob_enable_without_generated_seal_does_not_become_override() {
2182 let ctx = MobToolAuthorityContext::generated_for_test(
2183 OpaquePrincipalToken::new("generated-test"),
2184 true,
2185 true,
2186 false,
2187 BTreeSet::from(["mob-1".to_string()]),
2188 BTreeMap::new(),
2189 None,
2190 None,
2191 );
2192 let projected: MobToolAuthorityContext =
2193 serde_json::from_value(serde_json::to_value(ctx).unwrap()).unwrap();
2194
2195 let mut build = SessionBuildOptions::default();
2196 build.apply_persisted_mob_operator_access(ToolCategoryOverride::Enable, Some(projected));
2197
2198 assert_eq!(build.override_mob, ToolCategoryOverride::Inherit);
2199 assert!(build.mob_tool_authority_context.is_none());
2200 }
2201
2202 #[test]
2203 fn explicit_mob_enable_records_create_only_runtime_handoff_intent() {
2204 let mut build = SessionBuildOptions::default();
2205
2206 build.apply_generated_create_only_mob_operator_access(ToolCategoryOverride::Enable);
2207
2208 assert_eq!(build.override_mob, ToolCategoryOverride::Enable);
2209 assert!(build.mob_tool_authority_context.is_none());
2210 }
2211
2212 #[test]
2213 fn mob_tool_snapshot_context_standalone() {
2214 let ctx = MobToolSnapshotContext::Standalone;
2215 assert!(matches!(ctx, MobToolSnapshotContext::Standalone));
2216 }
2217
2218 #[test]
2219 fn mob_tool_snapshot_context_parent_owned_returns_tools() {
2220 let tools = Arc::<[Arc<ToolDef>]>::from(vec![Arc::new(ToolDef {
2221 name: "test_tool".into(),
2222 description: "a test".to_string(),
2223 input_schema: serde_json::json!({"type": "object"}),
2224 provenance: None,
2225 })]);
2226 let tool_scope = crate::ToolScope::new(tools);
2227 let authority = ParentToolCompositionAuthority::new_from_agent_factory_composition();
2228 authority.set_tool_scope_from_agent_factory_composition(&tool_scope);
2229 let ctx = MobToolSnapshotContext::ParentOwned(authority);
2230 match ctx {
2231 MobToolSnapshotContext::ParentOwned(p) => {
2232 let snapshot = p.snapshot_visible_tools();
2233 assert_eq!(snapshot.len(), 1);
2234 assert_eq!(snapshot[0].name, "test_tool");
2235 }
2236 MobToolSnapshotContext::Standalone => panic!("expected ParentOwned"),
2237 }
2238 }
2239}