pub mod transport;
use crate::event::AgentEvent;
use crate::event::EventEnvelope;
use crate::session::SystemContextStageError;
use crate::time_compat::SystemTime;
#[cfg(target_arch = "wasm32")]
use crate::tokio;
use crate::types::{
ContentInput, HandlingMode, Message, RenderMetadata, 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;
use tokio::sync::mpsc;
#[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("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",
}
}
}
#[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 render_metadata: Option<RenderMetadata>,
pub system_prompt: Option<String>,
pub max_tokens: Option<u32>,
pub event_tx: Option<mpsc::Sender<EventEnvelope<AgentEvent>>>,
pub skill_references: Option<Vec<crate::skills::SkillKey>>,
pub initial_turn: InitialTurnPolicy,
pub deferred_prompt_policy: DeferredPromptPolicy,
pub build: Option<SessionBuildOptions>,
pub labels: Option<BTreeMap<String, String>>,
}
#[derive(Clone)]
pub struct SessionBuildOptions {
pub provider: Option<Provider>,
pub self_hosted_server_id: Option<String>,
pub output_schema: Option<OutputSchema>,
pub structured_output_retries: 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<serde_json::Value>,
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 override_builtins: ToolCategoryOverride,
pub override_shell: ToolCategoryOverride,
pub override_memory: ToolCategoryOverride,
pub override_schedule: ToolCategoryOverride,
pub override_mob: ToolCategoryOverride,
pub schedule_tools: Option<Arc<dyn AgentToolDispatcher>>,
pub preload_skills: Option<Vec<crate::skills::SkillId>>,
pub realm_id: Option<String>,
pub instance_id: Option<String>,
pub backend: Option<String>,
pub config_generation: Option<u64>,
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 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 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 {
principal_token: OpaquePrincipalToken,
can_create_mobs: bool,
#[serde(default, skip_serializing_if = "BTreeSet::is_empty")]
managed_mob_scope: 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>,
}
impl MobToolAuthorityContext {
pub fn new(principal_token: OpaquePrincipalToken, can_create_mobs: bool) -> Self {
Self {
principal_token,
can_create_mobs,
managed_mob_scope: BTreeSet::new(),
caller_provenance: None,
audit_invocation_id: None,
}
}
pub fn create_only_generated() -> Self {
Self::new(OpaquePrincipalToken::generated(), true)
}
pub fn principal_token(&self) -> &OpaquePrincipalToken {
&self.principal_token
}
pub fn can_create_mobs(&self) -> bool {
self.can_create_mobs
}
pub fn managed_mob_scope(&self) -> &BTreeSet<String> {
&self.managed_mob_scope
}
pub fn caller_provenance(&self) -> Option<&MobToolCallerProvenance> {
self.caller_provenance.as_ref()
}
pub fn audit_invocation_id(&self) -> Option<&str> {
self.audit_invocation_id.as_deref()
}
pub fn can_manage_mob(&self, mob_id: &str) -> bool {
self.managed_mob_scope.contains(mob_id)
}
pub fn grant_manage_mob(mut self, mob_id: impl Into<String>) -> Self {
self.managed_mob_scope.insert(mob_id.into());
self
}
pub fn grant_manage_mob_in_place(&mut self, mob_id: String) {
self.managed_mob_scope.insert(mob_id);
}
pub fn with_managed_mob_scope<I, S>(mut self, mob_ids: I) -> Self
where
I: IntoIterator<Item = S>,
S: Into<String>,
{
self.managed_mob_scope = mob_ids.into_iter().map(Into::into).collect();
self
}
pub fn with_caller_provenance(mut self, caller_provenance: MobToolCallerProvenance) -> Self {
self.caller_provenance = Some(caller_provenance);
self
}
pub fn with_audit_invocation_id(mut self, audit_invocation_id: impl Into<String>) -> Self {
self.audit_invocation_id = Some(audit_invocation_id.into());
self
}
}
pub fn generated_create_only_mob_operator_authority(
enable_mob: ToolCategoryOverride,
) -> Option<MobToolAuthorityContext> {
matches!(enable_mob, ToolCategoryOverride::Enable)
.then(MobToolAuthorityContext::create_only_generated)
}
pub fn resolve_mob_operator_access(
enable_mob: ToolCategoryOverride,
persisted_authority_context: Option<MobToolAuthorityContext>,
) -> (ToolCategoryOverride, Option<MobToolAuthorityContext>) {
if matches!(enable_mob, ToolCategoryOverride::Disable) {
return (ToolCategoryOverride::Disable, None);
}
let authority_context = persisted_authority_context
.or_else(|| generated_create_only_mob_operator_authority(enable_mob));
let override_mob = if authority_context.is_some() {
ToolCategoryOverride::Enable
} else {
enable_mob
};
(override_mob, authority_context)
}
pub trait VisibleToolSnapshotProvider: Send + Sync {
fn snapshot_visible_tools(&self) -> Vec<Arc<ToolDef>>;
}
pub enum MobToolSnapshotContext {
ParentOwned(Arc<dyn VisibleToolSnapshotProvider>),
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 override_builtins: bool,
pub override_shell: bool,
pub override_memory: bool,
pub override_mob: 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>,
) {
let (override_mob, authority_context) =
resolve_mob_operator_access(enable_mob, persisted_authority_context);
self.override_mob = override_mob;
self.mob_tool_authority_context = authority_context;
}
pub fn apply_generated_create_only_mob_operator_access(
&mut self,
enable_mob: ToolCategoryOverride,
) {
self.apply_persisted_mob_operator_access(enable_mob, None);
}
}
impl Default for SessionBuildOptions {
fn default() -> Self {
Self {
provider: None,
self_hosted_server_id: None,
output_schema: None,
structured_output_retries: 2,
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,
override_builtins: ToolCategoryOverride::Inherit,
override_shell: ToolCategoryOverride::Inherit,
override_memory: ToolCategoryOverride::Inherit,
override_schedule: ToolCategoryOverride::Inherit,
override_mob: ToolCategoryOverride::Inherit,
schedule_tools: None,
preload_skills: None,
realm_id: None,
instance_id: None,
backend: None,
config_generation: None,
keep_alive: false,
checkpointer: None,
silent_comms_intents: Vec::new(),
max_inline_peer_notifications: None,
app_context: None,
additional_instructions: 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,
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("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("override_builtins", &self.override_builtins)
.field("override_shell", &self.override_shell)
.field("override_memory", &self.override_memory)
.field("override_schedule", &self.override_schedule)
.field("override_mob", &self.override_mob)
.field("schedule_tools", &self.schedule_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("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(
"mob_tool_authority_context",
&self.mob_tool_authority_context.is_some(),
)
.field("runtime_build_mode", &self.runtime_build_mode)
.finish()
}
}
#[derive(Debug)]
pub struct StartTurnRequest {
pub prompt: ContentInput,
pub system_prompt: Option<String>,
pub render_metadata: Option<RenderMetadata>,
pub handling_mode: HandlingMode,
pub event_tx: Option<mpsc::Sender<EventEnvelope<AgentEvent>>>,
pub skill_references: Option<Vec<crate::skills::SkillKey>>,
pub flow_tool_overlay: Option<TurnToolOverlay>,
pub additional_instructions: Option<Vec<String>>,
pub execution_kind: Option<crate::lifecycle::run_primitive::RuntimeExecutionKind>,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub struct AppendSystemContextRequest {
pub text: String,
#[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>,
}
#[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)]
#[serde(rename_all = "snake_case")]
pub enum AppendSystemContextStatus {
Applied,
Staged,
Duplicate,
}
#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
pub struct TurnToolOverlay {
#[serde(default)]
pub allowed_tools: Option<Vec<String>>,
#[serde(default)]
pub blocked_tools: Option<Vec<String>>,
}
#[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(),
}
}
}
#[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 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,
) -> Result<(), SessionError> {
Err(SessionError::Unsupported(
"hot_swap_session_llm_identity".to_string(),
))
}
async fn update_session_keep_alive(
&self,
_id: &SessionId,
_keep_alive: bool,
) -> Result<(), SessionError> {
Err(SessionError::Unsupported(
"update_session_keep_alive".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}")))
}
}
#[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>;
}
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 grant_manage_mob_in_place_adds_mob_id() {
let mut ctx = MobToolAuthorityContext::create_only_generated();
ctx.grant_manage_mob_in_place("mob-1".into());
assert!(ctx.managed_mob_scope.contains("mob-1"));
}
#[test]
fn grant_manage_mob_in_place_is_idempotent() {
let mut ctx = MobToolAuthorityContext::create_only_generated();
ctx.grant_manage_mob_in_place("mob-1".into());
ctx.grant_manage_mob_in_place("mob-1".into());
assert_eq!(ctx.managed_mob_scope.len(), 1);
}
#[test]
fn grant_manage_mob_in_place_accumulates() {
let mut ctx = MobToolAuthorityContext::create_only_generated();
ctx.grant_manage_mob_in_place("mob-1".into());
ctx.grant_manage_mob_in_place("mob-2".into());
assert!(ctx.managed_mob_scope.contains("mob-1"));
assert!(ctx.managed_mob_scope.contains("mob-2"));
assert_eq!(ctx.managed_mob_scope.len(), 2);
}
struct MockSnapshotProvider {
tools: Vec<Arc<ToolDef>>,
}
impl VisibleToolSnapshotProvider for MockSnapshotProvider {
fn snapshot_visible_tools(&self) -> Vec<Arc<ToolDef>> {
self.tools.clone()
}
}
#[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 = vec![Arc::new(ToolDef {
name: "test_tool".to_string(),
description: "a test".to_string(),
input_schema: serde_json::json!({"type": "object"}),
provenance: None,
})];
let provider = Arc::new(MockSnapshotProvider { tools });
let ctx = MobToolSnapshotContext::ParentOwned(provider);
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"),
}
}
}