pub mod transport;
use crate::event::AgentEvent;
use crate::event::EventEnvelope;
use crate::lifecycle::run_primitive::{ConversationAppend, CoreRenderable, RuntimeTurnMetadata};
use crate::session::{PendingSystemContextAppend, SystemContextStageError};
use crate::time_compat::SystemTime;
#[cfg(target_arch = "wasm32")]
use crate::tokio;
use crate::types::{ContentInput, HandlingMode, Message, RunResult, SessionId, ToolDef, Usage};
use crate::{
AgentToolDispatcher, BudgetLimits, HookRunOverrides, OutputSchema, PeerMeta, Provider, Session,
SessionLlmIdentity, ToolCategoryOverride,
};
use crate::{EventStream, StreamError};
use async_trait::async_trait;
use serde::{Deserialize, Serialize};
use std::collections::BTreeMap;
use std::collections::BTreeSet;
use std::sync::{Arc, LazyLock, RwLock};
use tokio::sync::mpsc;
pub use crate::session::{
TranscriptEditError, TranscriptReplacement, TranscriptRewriteCommit, TranscriptRewriteReason,
TranscriptRewriteSelection,
};
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum InitialTurnPolicy {
RunImmediately,
Defer,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)]
#[serde(rename_all = "snake_case")]
pub enum DeferredPromptPolicy {
#[default]
Discard,
Stage,
}
#[derive(Debug, thiserror::Error)]
pub enum SessionError {
#[error("session not found: {id}")]
NotFound { id: SessionId },
#[error("session is busy: {id}")]
Busy { id: SessionId },
#[error("session persistence is disabled")]
PersistenceDisabled,
#[error("session compaction is disabled")]
CompactionDisabled,
#[error("no turn running on session: {id}")]
NotRunning { id: SessionId },
#[error("store error: {0}")]
Store(#[source] Box<dyn std::error::Error + Send + Sync>),
#[error("agent error: {0}")]
Agent(#[from] crate::error::AgentError),
#[error("{message}")]
FailedWithData {
message: String,
data: serde_json::Value,
},
#[error("unsupported: {0}")]
Unsupported(String),
}
impl SessionError {
pub fn code(&self) -> &'static str {
match self {
Self::NotFound { .. } => "SESSION_NOT_FOUND",
Self::Busy { .. } => "SESSION_BUSY",
Self::PersistenceDisabled => "SESSION_PERSISTENCE_DISABLED",
Self::CompactionDisabled => "SESSION_COMPACTION_DISABLED",
Self::NotRunning { .. } => "SESSION_NOT_RUNNING",
Self::Store(_) => "SESSION_STORE_ERROR",
Self::Unsupported(_) => "SESSION_UNSUPPORTED",
Self::Agent(_) => "AGENT_ERROR",
Self::FailedWithData { .. } => "SESSION_ERROR",
}
}
pub fn structured_data(&self) -> Option<serde_json::Value> {
match self {
Self::FailedWithData { data, .. } => Some(data.clone()),
_ => None,
}
}
}
#[derive(Debug, thiserror::Error)]
pub enum SessionControlError {
#[error(transparent)]
Session(#[from] SessionError),
#[error("invalid system-context request: {message}")]
InvalidRequest { message: String },
#[error(
"system-context idempotency conflict on session {id}: key '{key}' already maps to different content"
)]
Conflict { id: SessionId, key: String },
}
impl SessionControlError {
pub fn code(&self) -> &'static str {
match self {
Self::Session(err) => err.code(),
Self::InvalidRequest { .. } => "INVALID_PARAMS",
Self::Conflict { .. } => "SESSION_SYSTEM_CONTEXT_CONFLICT",
}
}
}
impl SystemContextStageError {
pub fn into_control_error(self, id: &SessionId) -> SessionControlError {
match self {
Self::InvalidRequest(message) => SessionControlError::InvalidRequest { message },
Self::Conflict { key, .. } => SessionControlError::Conflict {
id: id.clone(),
key,
},
}
}
}
#[derive(Debug)]
pub struct CreateSessionRequest {
pub model: String,
pub prompt: ContentInput,
pub system_prompt: crate::config::SystemPromptOverride,
pub max_tokens: Option<u32>,
pub event_tx: Option<mpsc::Sender<EventEnvelope<AgentEvent>>>,
pub initial_turn: InitialTurnPolicy,
pub deferred_prompt_policy: DeferredPromptPolicy,
pub build: Option<SessionBuildOptions>,
pub labels: Option<BTreeMap<String, String>>,
}
impl CreateSessionRequest {
#[must_use]
pub fn surface_metadata(&self) -> crate::SurfaceMetadata {
crate::SurfaceMetadata::from_optional_parts(
self.labels.clone(),
self.build
.as_ref()
.and_then(|build| build.app_context.clone()),
)
}
}
#[derive(Clone)]
pub struct SessionBuildOptions {
pub provider: Option<Provider>,
pub self_hosted_server_id: Option<String>,
pub custom_models: BTreeMap<String, crate::config::CustomModelConfig>,
pub image_generation_provider: Option<Provider>,
pub auto_compact_threshold_override: Option<std::num::NonZeroU64>,
pub output_schema: Option<OutputSchema>,
pub structured_output_retries: Option<u32>,
pub hooks_override: HookRunOverrides,
pub comms_name: Option<String>,
pub peer_meta: Option<PeerMeta>,
pub resume_session: Option<Session>,
pub budget_limits: Option<BudgetLimits>,
pub provider_params: Option<crate::lifecycle::run_primitive::ProviderParamsOverride>,
pub external_tools: Option<Arc<dyn AgentToolDispatcher>>,
pub recoverable_tool_defs: Option<Vec<crate::ToolDef>>,
pub blob_store_override: Option<Arc<dyn crate::BlobStore>>,
pub llm_client_override: Option<Arc<dyn std::any::Any + Send + Sync>>,
pub agent_llm_client_decorator: Option<crate::AgentLlmClientDecorator>,
pub override_builtins: ToolCategoryOverride,
pub override_shell: ToolCategoryOverride,
pub override_comms: ToolCategoryOverride,
pub override_memory: ToolCategoryOverride,
pub override_schedule: ToolCategoryOverride,
pub override_workgraph: ToolCategoryOverride,
pub override_mob: ToolCategoryOverride,
pub override_image_generation: ToolCategoryOverride,
pub override_web_search: ToolCategoryOverride,
pub schedule_tools: Option<Arc<dyn AgentToolDispatcher>>,
pub workgraph_tools: Option<Arc<dyn AgentToolDispatcher>>,
pub preload_skills: Option<Vec<crate::skills::SkillKey>>,
pub realm_id: Option<crate::RealmId>,
pub instance_id: Option<String>,
pub backend: Option<crate::session_recovery::RecoveryBackendKind>,
pub config_generation: Option<u64>,
pub auth_binding: Option<crate::AuthBindingRef>,
pub mob_member_binding: Option<crate::MobMemberBinding>,
pub keep_alive: bool,
pub checkpointer: Option<std::sync::Arc<dyn crate::checkpoint::SessionCheckpointer>>,
pub silent_comms_intents: Vec<String>,
pub max_inline_peer_notifications: Option<i32>,
pub app_context: Option<serde_json::Value>,
pub additional_instructions: Option<Vec<String>>,
pub initial_metadata_entries: BTreeMap<String, serde_json::Value>,
pub initial_tool_filter: Option<crate::tool_scope::ToolFilter>,
pub shell_env: Option<std::collections::HashMap<String, String>>,
pub call_timeout_override: crate::CallTimeoutOverride,
pub resume_override_mask: ResumeOverrideMask,
pub mob_tools: Option<Arc<dyn MobToolsFactory>>,
pub runtime_build_mode: crate::runtime_epoch::RuntimeBuildMode,
pub initial_turn_metadata: Option<RuntimeTurnMetadata>,
pub mob_tool_authority_context: Option<MobToolAuthorityContext>,
}
#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
pub struct OpaquePrincipalToken(String);
impl OpaquePrincipalToken {
pub fn new(token: impl Into<String>) -> Self {
Self(token.into())
}
pub fn generated() -> Self {
Self(uuid::Uuid::new_v4().to_string())
}
pub fn as_str(&self) -> &str {
&self.0
}
}
impl std::fmt::Display for OpaquePrincipalToken {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.write_str(self.as_str())
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, Default)]
pub struct MobToolCallerProvenance {
#[serde(default, skip_serializing_if = "Option::is_none")]
caller_session_id: Option<crate::SessionId>,
#[serde(default, skip_serializing_if = "Option::is_none")]
caller_mob_id: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
caller_member_id: Option<String>,
}
impl MobToolCallerProvenance {
pub fn new() -> Self {
Self::default()
}
pub fn with_session_id(mut self, session_id: crate::SessionId) -> Self {
self.caller_session_id = Some(session_id);
self
}
pub fn with_mob_id(mut self, mob_id: impl Into<String>) -> Self {
self.caller_mob_id = Some(mob_id.into());
self
}
pub fn with_member_id(mut self, member_id: impl Into<String>) -> Self {
self.caller_member_id = Some(member_id.into());
self
}
pub fn caller_session_id(&self) -> Option<&crate::SessionId> {
self.caller_session_id.as_ref()
}
pub fn caller_mob_id(&self) -> Option<&str> {
self.caller_mob_id.as_deref()
}
pub fn caller_member_id(&self) -> Option<&str> {
self.caller_member_id.as_deref()
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct MobToolAuthorityContext {
#[serde(skip)]
generated_authority_seal: MobToolAuthorityContextSeal,
principal_token: OpaquePrincipalToken,
can_create_mobs: bool,
#[serde(default)]
can_mutate_profiles: bool,
#[serde(default, skip_serializing_if = "BTreeSet::is_empty")]
managed_mob_scope: BTreeSet<String>,
#[serde(default, skip_serializing_if = "BTreeMap::is_empty")]
spawn_profile_scope: BTreeMap<String, BTreeSet<String>>,
#[serde(default, skip_serializing_if = "Option::is_none")]
caller_provenance: Option<MobToolCallerProvenance>,
#[serde(default, skip_serializing_if = "Option::is_none")]
audit_invocation_id: Option<String>,
}
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
struct MobToolAuthorityContextSeal {
generated: bool,
}
impl MobToolAuthorityContextSeal {
const fn generated() -> Self {
Self { generated: true }
}
}
static EMPTY_MOB_AUTHORITY_MANAGED_SCOPE: LazyLock<BTreeSet<String>> = LazyLock::new(BTreeSet::new);
static EMPTY_MOB_AUTHORITY_SPAWN_SCOPE: LazyLock<BTreeMap<String, BTreeSet<String>>> =
LazyLock::new(BTreeMap::new);
static UNTRUSTED_MOB_AUTHORITY_PRINCIPAL: LazyLock<OpaquePrincipalToken> =
LazyLock::new(|| OpaquePrincipalToken::new("untrusted-mob-authority-context"));
impl MobToolAuthorityContext {
#[cfg_attr(
not(any(test, meerkat_internal_generated_authority_bridge)),
allow(dead_code)
)]
fn from_generated_parts(
principal_token: OpaquePrincipalToken,
can_create_mobs: bool,
can_mutate_profiles: bool,
managed_mob_scope: BTreeSet<String>,
spawn_profile_scope: BTreeMap<String, BTreeSet<String>>,
caller_provenance: Option<MobToolCallerProvenance>,
audit_invocation_id: Option<String>,
) -> Result<Self, String> {
if principal_token.as_str().trim().is_empty() {
return Err("generated mob tool authority requires a non-empty principal token".into());
}
Ok(Self {
generated_authority_seal: MobToolAuthorityContextSeal::generated(),
principal_token,
can_create_mobs,
can_mutate_profiles,
managed_mob_scope,
spawn_profile_scope,
caller_provenance,
audit_invocation_id,
})
}
#[cfg(test)]
#[allow(clippy::expect_used, clippy::too_many_arguments)]
pub(crate) fn generated_for_test(
principal_token: OpaquePrincipalToken,
can_create_mobs: bool,
can_mutate_profiles: bool,
managed_mob_scope: BTreeSet<String>,
spawn_profile_scope: BTreeMap<String, BTreeSet<String>>,
caller_provenance: Option<MobToolCallerProvenance>,
audit_invocation_id: Option<String>,
) -> Self {
Self::from_generated_parts(
principal_token,
can_create_mobs,
can_mutate_profiles,
managed_mob_scope,
spawn_profile_scope,
caller_provenance,
audit_invocation_id,
)
.expect("test mob authority context must be generated-shape valid")
}
pub fn is_generated_authority_context(&self) -> bool {
self.generated_authority_seal.generated
}
pub fn principal_token(&self) -> &OpaquePrincipalToken {
if self.is_generated_authority_context() {
&self.principal_token
} else {
&UNTRUSTED_MOB_AUTHORITY_PRINCIPAL
}
}
pub fn can_create_mobs(&self) -> bool {
self.is_generated_authority_context() && self.can_create_mobs
}
pub fn can_mutate_profiles(&self) -> bool {
self.is_generated_authority_context() && self.can_mutate_profiles
}
pub fn managed_mob_scope(&self) -> &BTreeSet<String> {
if self.is_generated_authority_context() {
&self.managed_mob_scope
} else {
&EMPTY_MOB_AUTHORITY_MANAGED_SCOPE
}
}
pub fn spawn_profile_scope(&self) -> &BTreeMap<String, BTreeSet<String>> {
if self.is_generated_authority_context() {
&self.spawn_profile_scope
} else {
&EMPTY_MOB_AUTHORITY_SPAWN_SCOPE
}
}
pub fn caller_provenance(&self) -> Option<&MobToolCallerProvenance> {
self.is_generated_authority_context()
.then_some(self.caller_provenance.as_ref())
.flatten()
}
pub fn audit_invocation_id(&self) -> Option<&str> {
self.is_generated_authority_context()
.then_some(self.audit_invocation_id.as_deref())
.flatten()
}
pub fn can_manage_mob(&self, mob_id: &str) -> bool {
self.managed_mob_scope().contains(mob_id)
}
pub fn spawn_profile_scope_present(&self, mob_id: &str) -> bool {
self.spawn_profile_scope()
.get(mob_id)
.is_some_and(|profiles| !profiles.is_empty())
}
pub fn spawn_profile_scope_contains(&self, mob_id: &str, profile: &str) -> bool {
self.spawn_profile_scope()
.get(mob_id)
.is_some_and(|profiles| profiles.contains(profile))
}
}
#[cfg(all(meerkat_internal_generated_authority_bridge, not(test)))]
#[allow(improper_ctypes_definitions, unsafe_code)]
unsafe extern "Rust" {
#[link_name = concat!(
"__meerkat_runtime_generated_authority_bridge_token_is_valid_v1_mob_operator_authority_",
env!("MEERKAT_GENERATED_AUTHORITY_BRIDGE_SYMBOL_SUFFIX")
)]
fn runtime_mob_operator_authority_generated_authority_bridge_token_is_valid(
token: &(dyn std::any::Any + Send + Sync),
) -> bool;
}
#[cfg(all(meerkat_internal_generated_authority_bridge, not(test)))]
#[doc(hidden)]
#[allow(improper_ctypes_definitions, unsafe_code)]
#[allow(clippy::too_many_arguments)]
#[unsafe(export_name = concat!(
"__meerkat_core_runtime_generated_mob_tool_authority_context_build_v1_",
env!("MEERKAT_GENERATED_AUTHORITY_BRIDGE_SYMBOL_SUFFIX")
))]
pub(crate) extern "Rust" fn runtime_generated_mob_tool_authority_context_build(
token: &'static (dyn std::any::Any + Send + Sync),
principal_token: OpaquePrincipalToken,
can_create_mobs: bool,
can_mutate_profiles: bool,
managed_mob_scope: BTreeSet<String>,
spawn_profile_scope: BTreeMap<String, BTreeSet<String>>,
caller_provenance: Option<MobToolCallerProvenance>,
audit_invocation_id: Option<String>,
) -> Result<MobToolAuthorityContext, String> {
#[allow(unsafe_code)]
let valid =
unsafe { runtime_mob_operator_authority_generated_authority_bridge_token_is_valid(token) };
if !valid {
return Err(
"mob tool authority context requires the generated runtime bridge token".into(),
);
}
MobToolAuthorityContext::from_generated_parts(
principal_token,
can_create_mobs,
can_mutate_profiles,
managed_mob_scope,
spawn_profile_scope,
caller_provenance,
audit_invocation_id,
)
}
#[derive(Clone)]
pub struct ParentToolCompositionAuthority {
inner: Arc<ParentToolCompositionAuthorityInner>,
}
struct ParentToolCompositionAuthorityInner {
tool_scope: RwLock<Option<crate::ToolScope>>,
}
impl ParentToolCompositionAuthority {
#[cfg_attr(not(meerkat_internal_agent_factory_build), allow(dead_code))]
fn new_from_agent_factory_composition() -> Self {
Self {
inner: Arc::new(ParentToolCompositionAuthorityInner {
tool_scope: RwLock::new(None),
}),
}
}
#[cfg_attr(not(meerkat_internal_agent_factory_build), allow(dead_code))]
fn set_tool_scope_from_agent_factory_composition(&self, tool_scope: &crate::ToolScope) {
let mut guard = self
.inner
.tool_scope
.write()
.unwrap_or_else(std::sync::PoisonError::into_inner);
*guard = Some(tool_scope.clone());
}
fn current_tool_scope(&self) -> Option<crate::ToolScope> {
self.inner
.tool_scope
.read()
.unwrap_or_else(std::sync::PoisonError::into_inner)
.clone()
}
fn map_visible_tools_error(
err: crate::tool_scope::ToolScopeApplyError,
) -> crate::tool_scope::ToolScopeStageError {
match err {
crate::tool_scope::ToolScopeApplyError::LockPoisoned => {
crate::tool_scope::ToolScopeStageError::LockPoisoned
}
crate::tool_scope::ToolScopeApplyError::Owner { message } => {
crate::tool_scope::ToolScopeStageError::Owner { message }
}
crate::tool_scope::ToolScopeApplyError::InjectedFailure => {
crate::tool_scope::ToolScopeStageError::Owner {
message: err.to_string(),
}
}
}
}
fn visible_tool_snapshot_result(
&self,
) -> Result<Vec<Arc<ToolDef>>, crate::tool_scope::ToolScopeStageError> {
let tool_scope = self.current_tool_scope().ok_or_else(|| {
crate::tool_scope::ToolScopeStageError::Owner {
message: "parent tool scope is unavailable".to_string(),
}
})?;
tool_scope
.visible_tools_result()
.map(|tools| tools.iter().cloned().collect())
.map_err(Self::map_visible_tools_error)
}
pub fn snapshot_visible_tools(&self) -> Vec<Arc<ToolDef>> {
self.visible_tool_snapshot_result().unwrap_or_default()
}
pub fn authorize_inherited_tool_visibility(
&self,
filter: crate::tool_scope::ToolFilter,
) -> Result<crate::InheritedToolVisibilityAuthority, crate::tool_scope::ToolScopeStageError>
{
let tools = self.visible_tool_snapshot_result()?;
let tool_defs = tools
.iter()
.map(|tool| tool.as_ref().clone())
.collect::<Vec<_>>();
let witnesses = crate::tool_scope::filter_witnesses_for_tool_defs(&tool_defs, &filter);
crate::tool_scope::validate_witnessed_filter_authority(&filter, &witnesses)?;
Ok(
crate::InheritedToolVisibilityAuthority::from_generated_composition_authority(
filter, witnesses,
),
)
}
pub fn authorize_inherited_tool_visibility_with_overlays(
&self,
allow_overlay: Option<&std::collections::HashSet<String>>,
deny_overlay: Option<&std::collections::HashSet<String>>,
) -> Result<crate::InheritedToolVisibilityAuthority, crate::tool_scope::ToolScopeStageError>
{
let tools = self.visible_tool_snapshot_result()?;
let mut names = tools
.iter()
.map(|tool| tool.name.as_str().to_string())
.collect::<std::collections::HashSet<_>>();
if let Some(allow) = allow_overlay {
names = names.intersection(allow).cloned().collect();
}
if let Some(deny) = deny_overlay {
for name in deny {
names.remove(name);
}
}
let filter = crate::tool_scope::ToolFilter::Allow(names.into_iter().collect());
let tool_defs = tools
.iter()
.map(|tool| tool.as_ref().clone())
.collect::<Vec<_>>();
let witnesses = crate::tool_scope::filter_witnesses_for_tool_defs(&tool_defs, &filter);
crate::tool_scope::validate_witnessed_filter_authority(&filter, &witnesses)?;
Ok(
crate::InheritedToolVisibilityAuthority::from_generated_composition_authority(
filter, witnesses,
),
)
}
}
#[cfg(all(meerkat_internal_agent_factory_build, not(test)))]
#[allow(improper_ctypes_definitions, unsafe_code)]
unsafe extern "Rust" {
#[link_name = concat!(
"__meerkat_agent_factory_policy_bridge_token_is_valid_v1_",
env!("MEERKAT_AGENT_FACTORY_POLICY_BRIDGE_SYMBOL_SUFFIX")
)]
fn agent_factory_parent_tool_composition_bridge_token_is_valid(
factory_bridge_token: &(dyn std::any::Any + Send + Sync),
) -> bool;
}
#[cfg(all(meerkat_internal_agent_factory_build, not(test)))]
fn validate_agent_factory_parent_tool_composition_bridge_token(
token: &(dyn std::any::Any + Send + Sync),
) -> Result<(), String> {
#[allow(unsafe_code)]
let is_valid = unsafe { agent_factory_parent_tool_composition_bridge_token_is_valid(token) };
if is_valid {
Ok(())
} else {
Err("parent tool composition authority requires the AgentFactory bridge token".into())
}
}
#[cfg(all(meerkat_internal_agent_factory_build, not(test)))]
#[doc(hidden)]
#[allow(improper_ctypes_definitions, unsafe_code)]
#[unsafe(export_name = concat!(
"__meerkat_agent_factory_parent_tool_composition_authority_new_v1_",
env!("MEERKAT_AGENT_FACTORY_POLICY_BRIDGE_SYMBOL_SUFFIX")
))]
pub(crate) extern "Rust" fn agent_factory_parent_tool_composition_authority_new(
token: &'static (dyn std::any::Any + Send + Sync),
) -> Result<ParentToolCompositionAuthority, String> {
validate_agent_factory_parent_tool_composition_bridge_token(token)?;
Ok(ParentToolCompositionAuthority::new_from_agent_factory_composition())
}
#[cfg(all(meerkat_internal_agent_factory_build, not(test)))]
#[doc(hidden)]
#[allow(improper_ctypes_definitions, unsafe_code)]
#[unsafe(export_name = concat!(
"__meerkat_agent_factory_parent_tool_composition_authority_set_tool_scope_v1_",
env!("MEERKAT_AGENT_FACTORY_POLICY_BRIDGE_SYMBOL_SUFFIX")
))]
pub(crate) extern "Rust" fn agent_factory_parent_tool_composition_authority_set_tool_scope(
token: &'static (dyn std::any::Any + Send + Sync),
authority: &ParentToolCompositionAuthority,
tool_scope: &crate::ToolScope,
) -> Result<(), String> {
validate_agent_factory_parent_tool_composition_bridge_token(token)?;
authority.set_tool_scope_from_agent_factory_composition(tool_scope);
Ok(())
}
#[derive(Clone)]
pub enum MobToolSnapshotContext {
ParentOwned(ParentToolCompositionAuthority),
Standalone,
}
pub struct MobToolsBuildArgs {
pub session_id: crate::SessionId,
pub model: String,
pub authority_context: Option<MobToolAuthorityContext>,
pub effective_authority: Option<Arc<std::sync::RwLock<MobToolAuthorityContext>>>,
pub comms_name: Option<String>,
pub comms_runtime: Option<Arc<dyn crate::agent::CommsRuntime>>,
pub snapshot_context: MobToolSnapshotContext,
}
#[cfg_attr(target_arch = "wasm32", async_trait(?Send))]
#[cfg_attr(not(target_arch = "wasm32"), async_trait)]
pub trait MobToolsFactory: Send + Sync {
async fn build_mob_tools(
&self,
args: MobToolsBuildArgs,
) -> Result<Arc<dyn AgentToolDispatcher>, Box<dyn std::error::Error + Send + Sync>>;
}
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
pub struct ResumeOverrideMask {
pub model: bool,
pub provider: bool,
pub max_tokens: bool,
pub structured_output_retries: bool,
pub provider_params: bool,
pub auth_binding: bool,
pub override_builtins: bool,
pub override_shell: bool,
pub override_comms: bool,
pub override_memory: bool,
pub override_schedule: bool,
pub override_workgraph: bool,
pub override_mob: bool,
pub override_image_generation: bool,
pub override_web_search: bool,
pub preload_skills: bool,
pub keep_alive: bool,
pub comms_name: bool,
pub peer_meta: bool,
}
impl SessionBuildOptions {
pub fn apply_persisted_mob_operator_access(
&mut self,
enable_mob: ToolCategoryOverride,
persisted_authority_context: Option<MobToolAuthorityContext>,
) {
if matches!(enable_mob, ToolCategoryOverride::Disable) {
self.override_mob = ToolCategoryOverride::Disable;
self.mob_tool_authority_context = None;
return;
}
let generated_authority_context = persisted_authority_context
.filter(MobToolAuthorityContext::is_generated_authority_context);
let has_generated_authority = generated_authority_context.is_some();
self.override_mob = if has_generated_authority {
ToolCategoryOverride::Enable
} else {
ToolCategoryOverride::Inherit
};
self.mob_tool_authority_context = generated_authority_context;
}
pub fn apply_generated_create_only_mob_operator_access(
&mut self,
enable_mob: ToolCategoryOverride,
) {
self.override_mob = enable_mob;
self.mob_tool_authority_context = None;
}
}
impl Default for SessionBuildOptions {
fn default() -> Self {
Self {
provider: None,
self_hosted_server_id: None,
custom_models: BTreeMap::new(),
image_generation_provider: None,
auto_compact_threshold_override: None,
output_schema: None,
structured_output_retries: None,
hooks_override: HookRunOverrides::default(),
comms_name: None,
peer_meta: None,
resume_session: None,
budget_limits: None,
provider_params: None,
external_tools: None,
recoverable_tool_defs: None,
blob_store_override: None,
llm_client_override: None,
agent_llm_client_decorator: None,
override_builtins: ToolCategoryOverride::Inherit,
override_shell: ToolCategoryOverride::Inherit,
override_comms: ToolCategoryOverride::Inherit,
override_memory: ToolCategoryOverride::Inherit,
override_schedule: ToolCategoryOverride::Inherit,
override_workgraph: ToolCategoryOverride::Inherit,
override_mob: ToolCategoryOverride::Inherit,
override_image_generation: ToolCategoryOverride::Inherit,
override_web_search: ToolCategoryOverride::Inherit,
schedule_tools: None,
workgraph_tools: None,
preload_skills: None,
realm_id: None,
instance_id: None,
backend: None,
config_generation: None,
auth_binding: None,
mob_member_binding: None,
keep_alive: false,
checkpointer: None,
silent_comms_intents: Vec::new(),
max_inline_peer_notifications: None,
app_context: None,
additional_instructions: None,
initial_metadata_entries: BTreeMap::new(),
initial_tool_filter: None,
shell_env: None,
call_timeout_override: crate::CallTimeoutOverride::Inherit,
resume_override_mask: ResumeOverrideMask::default(),
mob_tools: None,
runtime_build_mode: crate::runtime_epoch::RuntimeBuildMode::StandaloneEphemeral,
initial_turn_metadata: None,
mob_tool_authority_context: None,
}
}
}
impl std::fmt::Debug for SessionBuildOptions {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("SessionBuildOptions")
.field("provider", &self.provider)
.field("custom_models", &self.custom_models.keys())
.field("image_generation_provider", &self.image_generation_provider)
.field(
"auto_compact_threshold_override",
&self.auto_compact_threshold_override,
)
.field("output_schema", &self.output_schema.is_some())
.field("structured_output_retries", &self.structured_output_retries)
.field("hooks_override", &self.hooks_override)
.field("comms_name", &self.comms_name)
.field("peer_meta", &self.peer_meta)
.field("resume_session", &self.resume_session.is_some())
.field("budget_limits", &self.budget_limits)
.field("provider_params", &self.provider_params.is_some())
.field("external_tools", &self.external_tools.is_some())
.field("recoverable_tool_defs", &self.recoverable_tool_defs)
.field("blob_store_override", &self.blob_store_override.is_some())
.field("llm_client_override", &self.llm_client_override.is_some())
.field(
"agent_llm_client_decorator",
&self.agent_llm_client_decorator.is_some(),
)
.field("override_builtins", &self.override_builtins)
.field("override_shell", &self.override_shell)
.field("override_comms", &self.override_comms)
.field("override_memory", &self.override_memory)
.field("override_schedule", &self.override_schedule)
.field("override_workgraph", &self.override_workgraph)
.field("override_mob", &self.override_mob)
.field("schedule_tools", &self.schedule_tools.is_some())
.field("workgraph_tools", &self.workgraph_tools.is_some())
.field("preload_skills", &self.preload_skills)
.field("realm_id", &self.realm_id)
.field("instance_id", &self.instance_id)
.field("backend", &self.backend)
.field("config_generation", &self.config_generation)
.field("keep_alive", &self.keep_alive)
.field("checkpointer", &self.checkpointer.is_some())
.field("silent_comms_intents", &self.silent_comms_intents)
.field(
"max_inline_peer_notifications",
&self.max_inline_peer_notifications,
)
.field("app_context", &self.app_context.is_some())
.field("additional_instructions", &self.additional_instructions)
.field("initial_metadata_entries", &self.initial_metadata_entries)
.field("initial_tool_filter", &self.initial_tool_filter.is_some())
.field("call_timeout_override", &self.call_timeout_override)
.field("resume_override_mask", &self.resume_override_mask)
.field("mob_tools", &self.mob_tools.is_some())
.field("runtime_build_mode", &self.runtime_build_mode)
.field(
"initial_turn_metadata",
&self.initial_turn_metadata.is_some(),
)
.field(
"mob_tool_authority_context",
&self.mob_tool_authority_context.is_some(),
)
.field("runtime_build_mode", &self.runtime_build_mode)
.finish()
}
}
#[derive(Debug)]
pub struct StartTurnRuntimeSemantics {
pub handling_mode: HandlingMode,
pub flow_tool_overlay: Option<TurnToolOverlay>,
pub pre_turn_context_appends: Vec<PendingSystemContextAppend>,
pub typed_turn_appends: Vec<ConversationAppend>,
pub turn_metadata: Option<RuntimeTurnMetadata>,
}
impl Default for StartTurnRuntimeSemantics {
fn default() -> Self {
Self {
handling_mode: HandlingMode::Queue,
flow_tool_overlay: None,
pre_turn_context_appends: Vec::new(),
typed_turn_appends: Vec::new(),
turn_metadata: None,
}
}
}
impl StartTurnRuntimeSemantics {
#[must_use]
pub fn new(
handling_mode: HandlingMode,
flow_tool_overlay: Option<TurnToolOverlay>,
pre_turn_context_appends: Vec<PendingSystemContextAppend>,
turn_metadata: Option<RuntimeTurnMetadata>,
) -> Self {
Self {
handling_mode,
flow_tool_overlay,
pre_turn_context_appends,
typed_turn_appends: Vec::new(),
turn_metadata,
}
}
#[must_use]
pub fn runtime_metadata(turn_metadata: RuntimeTurnMetadata) -> Self {
Self {
turn_metadata: Some(turn_metadata),
..Self::default()
}
}
#[must_use]
pub fn with_typed_turn_appends(mut self, typed_turn_appends: Vec<ConversationAppend>) -> Self {
self.typed_turn_appends = typed_turn_appends;
self
}
}
#[derive(Debug)]
pub struct StartTurnRequest {
pub prompt: ContentInput,
pub system_prompt: Option<String>,
pub event_tx: Option<mpsc::Sender<EventEnvelope<AgentEvent>>>,
pub runtime: StartTurnRuntimeSemantics,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub struct AppendSystemContextRequest {
pub content: CoreRenderable,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub source: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub idempotency_key: Option<String>,
#[serde(
default,
skip_serializing_if = "crate::session::SystemContextSource::is_normal"
)]
pub source_kind: crate::session::SystemContextSource,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub peer_response_terminal: Option<crate::handles::PeerResponseTerminalFact>,
}
impl AppendSystemContextRequest {
#[must_use]
pub fn from_text(text: impl Into<String>) -> Self {
Self {
content: CoreRenderable::text(text),
source: None,
idempotency_key: None,
source_kind: crate::session::SystemContextSource::Normal,
peer_response_terminal: None,
}
}
#[must_use]
pub fn text(&self) -> String {
self.content.render_text()
}
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub struct AppendSystemContextResult {
pub status: AppendSystemContextStatus,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct StageToolResultsRequest {
pub results: Vec<crate::ToolResult>,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub struct StageToolResultsResult {
pub accepted_result_count: usize,
}
#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
#[serde(rename_all = "snake_case")]
pub enum AppendSystemContextStatus {
Applied,
Staged,
Duplicate,
}
#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
pub struct TurnToolOverlay {
#[serde(default)]
pub allowed_tools: Option<Vec<crate::types::ToolName>>,
#[serde(default)]
pub blocked_tools: Option<Vec<crate::types::ToolName>>,
#[serde(default, skip_serializing_if = "std::collections::BTreeMap::is_empty")]
#[cfg_attr(feature = "schema", schemars(skip))]
pub dispatch_context: std::collections::BTreeMap<String, serde_json::Value>,
}
impl TurnToolOverlay {
pub fn without_dispatch_context(mut self) -> Self {
self.dispatch_context.clear();
self
}
}
#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
pub struct PublicTurnToolOverlay {
#[serde(default)]
pub allowed_tools: Option<Vec<crate::types::ToolName>>,
#[serde(default)]
pub blocked_tools: Option<Vec<crate::types::ToolName>>,
}
impl From<PublicTurnToolOverlay> for TurnToolOverlay {
fn from(value: PublicTurnToolOverlay) -> Self {
Self {
allowed_tools: value.allowed_tools,
blocked_tools: value.blocked_tools,
dispatch_context: BTreeMap::new(),
}
}
}
#[derive(Debug, Default)]
pub struct SessionQuery {
pub limit: Option<usize>,
pub offset: Option<usize>,
pub labels: Option<BTreeMap<String, String>>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct SessionSummary {
pub session_id: SessionId,
pub created_at: SystemTime,
pub updated_at: SystemTime,
pub message_count: usize,
pub total_tokens: u64,
pub is_active: bool,
#[serde(default, skip_serializing_if = "BTreeMap::is_empty")]
pub labels: BTreeMap<String, String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct SessionInfo {
pub session_id: SessionId,
pub created_at: SystemTime,
pub updated_at: SystemTime,
pub message_count: usize,
pub is_active: bool,
pub model: String,
pub provider: Provider,
pub last_assistant_text: Option<String>,
#[serde(default, skip_serializing_if = "BTreeMap::is_empty")]
pub labels: BTreeMap<String, String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct SessionUsage {
pub total_tokens: u64,
pub usage: Usage,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct SessionView {
pub state: SessionInfo,
pub billing: SessionUsage,
}
impl SessionView {
pub fn session_id(&self) -> &SessionId {
&self.state.session_id
}
}
#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq, Eq)]
pub struct SessionHistoryQuery {
pub offset: usize,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub limit: Option<usize>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct SessionHistoryPage {
pub session_id: SessionId,
pub message_count: usize,
pub offset: usize,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub limit: Option<usize>,
pub has_more: bool,
pub messages: Vec<Message>,
}
impl SessionHistoryPage {
pub fn from_messages(
session_id: SessionId,
messages: &[Message],
query: SessionHistoryQuery,
) -> Self {
let message_count = messages.len();
let start = query.offset.min(message_count);
let end = match query.limit {
Some(limit) => start.saturating_add(limit).min(message_count),
None => message_count,
};
Self {
session_id,
message_count,
offset: start,
limit: query.limit,
has_more: end < message_count,
messages: messages[start..end].to_vec(),
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub struct SessionTranscriptRevisionQuery {
pub revision: String,
pub offset: usize,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub limit: Option<usize>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct SessionTranscriptRevisionPage {
pub session_id: SessionId,
pub revision: String,
pub head_revision: String,
pub message_count: usize,
pub offset: usize,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub limit: Option<usize>,
pub has_more: bool,
pub messages: Vec<Message>,
}
impl SessionTranscriptRevisionPage {
pub fn from_messages(
session_id: SessionId,
revision: String,
head_revision: String,
messages: &[Message],
offset: usize,
limit: Option<usize>,
) -> Self {
let message_count = messages.len();
let start = offset.min(message_count);
let end = match limit {
Some(limit) => start.saturating_add(limit).min(message_count),
None => message_count,
};
Self {
session_id,
revision,
head_revision,
message_count,
offset: start,
limit,
has_more: end < message_count,
messages: messages[start..end].to_vec(),
}
}
}
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Serialize, Deserialize)]
#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
#[serde(rename_all = "snake_case")]
pub enum TranscriptEditRunningBehavior {
#[default]
Reject,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
pub struct SessionForkAtRequest {
pub message_index: usize,
#[serde(default)]
pub running_behavior: TranscriptEditRunningBehavior,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
pub struct SessionForkReplaceRequest {
pub message_index: usize,
#[cfg_attr(feature = "schema", schemars(with = "serde_json::Value"))]
pub replacement: TranscriptReplacement,
#[serde(default)]
pub running_behavior: TranscriptEditRunningBehavior,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
pub struct SessionForkResult {
#[cfg_attr(feature = "schema", schemars(with = "String"))]
pub source_session_id: SessionId,
#[cfg_attr(feature = "schema", schemars(with = "String"))]
pub session_id: SessionId,
pub message_count: usize,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub session_ref: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
pub struct SessionTranscriptRewriteRequest {
pub selection: TranscriptRewriteSelection,
#[cfg_attr(feature = "schema", schemars(with = "Vec<serde_json::Value>"))]
pub replacement: Vec<Message>,
pub reason: TranscriptRewriteReason,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub actor: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub expected_parent_revision: Option<String>,
#[serde(default)]
pub running_behavior: TranscriptEditRunningBehavior,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
pub struct SessionTranscriptRestoreRevisionRequest {
pub revision: String,
pub reason: TranscriptRewriteReason,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub actor: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub expected_parent_revision: Option<String>,
#[serde(default)]
pub running_behavior: TranscriptEditRunningBehavior,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
pub struct SessionTranscriptRewriteResult {
#[cfg_attr(feature = "schema", schemars(with = "String"))]
pub session_id: SessionId,
pub parent_revision: String,
pub revision: String,
pub message_count: usize,
pub commit: TranscriptRewriteCommit,
}
impl TranscriptEditError {
pub fn into_session_error(self) -> SessionError {
SessionError::Agent(crate::error::AgentError::ConfigError(self.to_string()))
}
}
#[cfg_attr(target_arch = "wasm32", async_trait(?Send))]
#[cfg_attr(not(target_arch = "wasm32"), async_trait)]
pub trait SessionService: Send + Sync {
async fn create_session(&self, req: CreateSessionRequest) -> Result<RunResult, SessionError>;
async fn start_turn(
&self,
id: &SessionId,
req: StartTurnRequest,
) -> Result<RunResult, SessionError>;
async fn interrupt(&self, id: &SessionId) -> Result<(), SessionError>;
async fn cancel_after_boundary(&self, _id: &SessionId) -> Result<(), SessionError> {
Err(SessionError::Unsupported(
"cancel_after_boundary".to_string(),
))
}
async fn set_session_client(
&self,
_id: &SessionId,
_client: std::sync::Arc<dyn crate::AgentLlmClient>,
) -> Result<(), SessionError> {
Err(SessionError::Unsupported("set_session_client".to_string()))
}
async fn hot_swap_session_llm_identity(
&self,
_id: &SessionId,
_client: std::sync::Arc<dyn crate::AgentLlmClient>,
_identity: SessionLlmIdentity,
_request_policy: crate::SessionLlmRequestPolicy,
) -> Result<(), SessionError> {
Err(SessionError::Unsupported(
"hot_swap_session_llm_identity".to_string(),
))
}
async fn set_session_tool_visibility_state(
&self,
_id: &SessionId,
_state: Option<crate::SessionToolVisibilityState>,
) -> Result<(), SessionError> {
Err(SessionError::Unsupported(
"set_session_tool_visibility_state".to_string(),
))
}
async fn update_session_mob_authority_context(
&self,
_id: &SessionId,
_authority_context: Option<MobToolAuthorityContext>,
) -> Result<(), SessionError> {
Err(SessionError::Unsupported(
"update_session_mob_authority_context".to_string(),
))
}
async fn has_live_session(&self, _id: &SessionId) -> Result<bool, SessionError> {
Err(SessionError::Unsupported("has_live_session".to_string()))
}
async fn set_session_tool_filter(
&self,
_id: &SessionId,
_filter: crate::ToolFilter,
) -> Result<(), SessionError> {
Err(SessionError::Unsupported(
"set_session_tool_filter".to_string(),
))
}
async fn read(&self, id: &SessionId) -> Result<SessionView, SessionError>;
async fn list(&self, query: SessionQuery) -> Result<Vec<SessionSummary>, SessionError>;
async fn archive(&self, id: &SessionId) -> Result<(), SessionError>;
async fn subscribe_session_events(&self, id: &SessionId) -> Result<EventStream, StreamError> {
Err(StreamError::NotFound(format!("session {id}")))
}
async fn record_live_terminal_error(
&self,
_id: &SessionId,
_cause: crate::live_adapter::LiveAdapterErrorCode,
) -> Result<(), SessionError> {
Err(SessionError::Unsupported(
"record_live_terminal_error".to_string(),
))
}
async fn record_live_output_audio_degraded(
&self,
_id: &SessionId,
_dropped: u64,
) -> Result<(), SessionError> {
Err(SessionError::Unsupported(
"record_live_output_audio_degraded".to_string(),
))
}
}
#[cfg_attr(target_arch = "wasm32", async_trait(?Send))]
#[cfg_attr(not(target_arch = "wasm32"), async_trait)]
pub trait SessionServiceCommsExt: SessionService {
async fn comms_runtime(
&self,
_session_id: &SessionId,
) -> Option<Arc<dyn crate::agent::CommsRuntime>> {
None
}
async fn event_injector(
&self,
session_id: &SessionId,
) -> Option<Arc<dyn crate::EventInjector>> {
self.comms_runtime(session_id)
.await
.and_then(|runtime| runtime.event_injector())
}
#[doc(hidden)]
async fn interaction_event_injector(
&self,
session_id: &SessionId,
) -> Option<Arc<dyn crate::event_injector::SubscribableInjector>> {
self.comms_runtime(session_id)
.await
.and_then(|runtime| runtime.interaction_event_injector())
}
}
#[cfg_attr(target_arch = "wasm32", async_trait(?Send))]
#[cfg_attr(not(target_arch = "wasm32"), async_trait)]
pub trait SessionServiceControlExt: SessionService {
async fn append_system_context(
&self,
id: &SessionId,
req: AppendSystemContextRequest,
) -> Result<AppendSystemContextResult, SessionControlError>;
async fn stage_tool_results(
&self,
id: &SessionId,
req: StageToolResultsRequest,
) -> Result<StageToolResultsResult, SessionError> {
let _ = (id, req);
Err(SessionError::Unsupported("stage_tool_results".to_string()))
}
}
#[cfg_attr(target_arch = "wasm32", async_trait(?Send))]
#[cfg_attr(not(target_arch = "wasm32"), async_trait)]
pub trait SessionServiceHistoryExt: SessionService {
async fn read_history(
&self,
id: &SessionId,
query: SessionHistoryQuery,
) -> Result<SessionHistoryPage, SessionError>;
async fn read_transcript_revision(
&self,
id: &SessionId,
query: SessionTranscriptRevisionQuery,
) -> Result<SessionTranscriptRevisionPage, SessionError> {
let _ = (id, query);
Err(SessionError::Unsupported(
"read_transcript_revision".to_string(),
))
}
}
#[cfg_attr(target_arch = "wasm32", async_trait(?Send))]
#[cfg_attr(not(target_arch = "wasm32"), async_trait)]
pub trait SessionServiceTranscriptEditExt: SessionService {
async fn fork_session_at(
&self,
id: &SessionId,
req: SessionForkAtRequest,
) -> Result<SessionForkResult, SessionError> {
let _ = (id, req);
Err(SessionError::Unsupported("fork_session_at".to_string()))
}
async fn fork_session_replace(
&self,
id: &SessionId,
req: SessionForkReplaceRequest,
) -> Result<SessionForkResult, SessionError> {
let _ = (id, req);
Err(SessionError::Unsupported(
"fork_session_replace".to_string(),
))
}
async fn rewrite_session_transcript(
&self,
id: &SessionId,
req: SessionTranscriptRewriteRequest,
) -> Result<SessionTranscriptRewriteResult, SessionError> {
let _ = (id, req);
Err(SessionError::Unsupported(
"rewrite_session_transcript".to_string(),
))
}
async fn restore_session_transcript_revision(
&self,
id: &SessionId,
req: SessionTranscriptRestoreRevisionRequest,
) -> Result<SessionTranscriptRewriteResult, SessionError> {
let _ = (id, req);
Err(SessionError::Unsupported(
"restore_session_transcript_revision".to_string(),
))
}
}
impl dyn SessionService {
pub fn into_arc(self: Box<Self>) -> Arc<dyn SessionService> {
Arc::from(self)
}
}
#[cfg(test)]
#[allow(
clippy::unimplemented,
clippy::unwrap_used,
clippy::expect_used,
clippy::panic
)]
mod tests {
use super::*;
struct UnsupportedSessionService;
#[cfg_attr(target_arch = "wasm32", async_trait(?Send))]
#[cfg_attr(not(target_arch = "wasm32"), async_trait)]
impl SessionService for UnsupportedSessionService {
async fn create_session(
&self,
_req: CreateSessionRequest,
) -> Result<RunResult, SessionError> {
unimplemented!()
}
async fn start_turn(
&self,
_id: &SessionId,
_req: StartTurnRequest,
) -> Result<RunResult, SessionError> {
unimplemented!()
}
async fn interrupt(&self, _id: &SessionId) -> Result<(), SessionError> {
unimplemented!()
}
async fn read(&self, _id: &SessionId) -> Result<SessionView, SessionError> {
unimplemented!()
}
async fn list(&self, _query: SessionQuery) -> Result<Vec<SessionSummary>, SessionError> {
unimplemented!()
}
async fn archive(&self, _id: &SessionId) -> Result<(), SessionError> {
unimplemented!()
}
}
#[tokio::test]
async fn has_live_session_defaults_to_unsupported() {
let service = UnsupportedSessionService;
let err = service
.has_live_session(&SessionId::new())
.await
.expect_err("default implementation should fail loudly");
assert!(matches!(err, SessionError::Unsupported(name) if name == "has_live_session"));
}
#[test]
fn spawn_profile_scope_allows_only_granted_profile_without_manage_scope() {
let ctx = MobToolAuthorityContext::generated_for_test(
OpaquePrincipalToken::new("generated-test"),
false,
false,
BTreeSet::new(),
BTreeMap::from([(
"mob-1".to_string(),
BTreeSet::from(["investigator".to_string()]),
)]),
None,
None,
);
assert!(ctx.spawn_profile_scope_present("mob-1"));
assert!(ctx.spawn_profile_scope_contains("mob-1", "investigator"));
assert!(!ctx.spawn_profile_scope_contains("mob-1", "writer"));
assert!(!ctx.can_manage_mob("mob-1"));
}
#[test]
fn deserialized_mob_tool_authority_context_is_projection_only() {
let ctx = MobToolAuthorityContext::generated_for_test(
OpaquePrincipalToken::new("generated-test"),
true,
true,
BTreeSet::from(["mob-1".to_string()]),
BTreeMap::from([(
"mob-1".to_string(),
BTreeSet::from(["investigator".to_string()]),
)]),
Some(MobToolCallerProvenance::default().with_mob_id("mob-1")),
Some("audit-1".to_string()),
);
let restored: MobToolAuthorityContext =
serde_json::from_value(serde_json::to_value(ctx).unwrap()).unwrap();
assert!(!restored.is_generated_authority_context());
assert!(!restored.can_create_mobs());
assert!(!restored.can_mutate_profiles());
assert!(!restored.can_manage_mob("mob-1"));
assert!(!restored.spawn_profile_scope_present("mob-1"));
assert!(restored.caller_provenance().is_none());
assert!(restored.audit_invocation_id().is_none());
}
#[test]
fn persisted_mob_enable_without_generated_seal_does_not_become_override() {
let ctx = MobToolAuthorityContext::generated_for_test(
OpaquePrincipalToken::new("generated-test"),
true,
true,
BTreeSet::from(["mob-1".to_string()]),
BTreeMap::new(),
None,
None,
);
let projected: MobToolAuthorityContext =
serde_json::from_value(serde_json::to_value(ctx).unwrap()).unwrap();
let mut build = SessionBuildOptions::default();
build.apply_persisted_mob_operator_access(ToolCategoryOverride::Enable, Some(projected));
assert_eq!(build.override_mob, ToolCategoryOverride::Inherit);
assert!(build.mob_tool_authority_context.is_none());
}
#[test]
fn explicit_mob_enable_records_create_only_runtime_handoff_intent() {
let mut build = SessionBuildOptions::default();
build.apply_generated_create_only_mob_operator_access(ToolCategoryOverride::Enable);
assert_eq!(build.override_mob, ToolCategoryOverride::Enable);
assert!(build.mob_tool_authority_context.is_none());
}
#[test]
fn mob_tool_snapshot_context_standalone() {
let ctx = MobToolSnapshotContext::Standalone;
assert!(matches!(ctx, MobToolSnapshotContext::Standalone));
}
#[test]
fn mob_tool_snapshot_context_parent_owned_returns_tools() {
let tools = Arc::<[Arc<ToolDef>]>::from(vec![Arc::new(ToolDef {
name: "test_tool".into(),
description: "a test".to_string(),
input_schema: serde_json::json!({"type": "object"}),
provenance: None,
})]);
let tool_scope = crate::ToolScope::new(tools);
let authority = ParentToolCompositionAuthority::new_from_agent_factory_composition();
authority.set_tool_scope_from_agent_factory_composition(&tool_scope);
let ctx = MobToolSnapshotContext::ParentOwned(authority);
match ctx {
MobToolSnapshotContext::ParentOwned(p) => {
let snapshot = p.snapshot_visible_tools();
assert_eq!(snapshot.len(), 1);
assert_eq!(snapshot[0].name, "test_tool");
}
MobToolSnapshotContext::Standalone => panic!("expected ParentOwned"),
}
}
}