Skip to main content

meerkat_core/service/
mod.rs

1//! SessionService trait — canonical lifecycle abstraction.
2//!
3//! All surfaces (CLI, REST, MCP Server, JSON-RPC) route through `SessionService`.
4//! Implementations may be ephemeral (in-memory only) or persistent (backed by a store).
5
6pub mod transport;
7
8use crate::event::AgentEvent;
9use crate::event::EventEnvelope;
10use crate::session::SystemContextStageError;
11use crate::time_compat::SystemTime;
12#[cfg(target_arch = "wasm32")]
13use crate::tokio;
14use crate::types::{
15    ContentInput, HandlingMode, Message, RenderMetadata, RunResult, SessionId, ToolDef, Usage,
16};
17use crate::{
18    AgentToolDispatcher, BudgetLimits, HookRunOverrides, OutputSchema, PeerMeta, Provider, Session,
19    SessionLlmIdentity, ToolCategoryOverride,
20};
21use crate::{EventStream, StreamError};
22use async_trait::async_trait;
23use serde::{Deserialize, Serialize};
24use std::collections::BTreeMap;
25use std::collections::BTreeSet;
26use std::sync::Arc;
27use tokio::sync::mpsc;
28
29/// Controls whether `create_session()` should execute an initial turn.
30#[derive(Debug, Clone, Copy, PartialEq, Eq)]
31pub enum InitialTurnPolicy {
32    /// Run the initial turn immediately as part of session creation.
33    RunImmediately,
34    /// Register the session and return without running an initial turn.
35    ///
36    /// `CreateSessionRequest::deferred_prompt_policy` determines whether the
37    /// create-time prompt is discarded or staged for the first later turn.
38    Defer,
39}
40
41/// How a deferred create request treats its create-time prompt.
42#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)]
43#[serde(rename_all = "snake_case")]
44pub enum DeferredPromptPolicy {
45    /// Register the session only; the caller will supply the first runtime input separately.
46    #[default]
47    Discard,
48    /// Persist the create-time prompt and merge it into the first later turn.
49    Stage,
50}
51
52/// Errors returned by `SessionService` methods.
53#[derive(Debug, thiserror::Error)]
54pub enum SessionError {
55    /// The requested session does not exist.
56    #[error("session not found: {id}")]
57    NotFound { id: SessionId },
58
59    /// A turn is already in progress on this session.
60    #[error("session is busy: {id}")]
61    Busy { id: SessionId },
62
63    /// The operation requires persistence but the `session-store` feature is disabled.
64    #[error("session persistence is disabled")]
65    PersistenceDisabled,
66
67    /// The operation requires compaction but the `session-compaction` feature is disabled.
68    #[error("session compaction is disabled")]
69    CompactionDisabled,
70
71    /// No turn is currently running on this session.
72    #[error("no turn running on session: {id}")]
73    NotRunning { id: SessionId },
74
75    /// A session store operation failed.
76    #[error("store error: {0}")]
77    Store(#[source] Box<dyn std::error::Error + Send + Sync>),
78
79    /// An agent-level error occurred during execution.
80    #[error("agent error: {0}")]
81    Agent(#[from] crate::error::AgentError),
82
83    /// The requested operation is not supported by this session service.
84    #[error("unsupported: {0}")]
85    Unsupported(String),
86}
87
88impl SessionError {
89    /// Return a stable error code string for wire formats.
90    pub fn code(&self) -> &'static str {
91        match self {
92            Self::NotFound { .. } => "SESSION_NOT_FOUND",
93            Self::Busy { .. } => "SESSION_BUSY",
94            Self::PersistenceDisabled => "SESSION_PERSISTENCE_DISABLED",
95            Self::CompactionDisabled => "SESSION_COMPACTION_DISABLED",
96            Self::NotRunning { .. } => "SESSION_NOT_RUNNING",
97            Self::Store(_) => "SESSION_STORE_ERROR",
98            Self::Unsupported(_) => "SESSION_UNSUPPORTED",
99            Self::Agent(_) => "AGENT_ERROR",
100        }
101    }
102}
103
104/// Errors returned by session control-plane mutation methods.
105#[derive(Debug, thiserror::Error)]
106pub enum SessionControlError {
107    /// A lifecycle/session-store error occurred while handling the control request.
108    #[error(transparent)]
109    Session(#[from] SessionError),
110
111    /// The control request was malformed.
112    #[error("invalid system-context request: {message}")]
113    InvalidRequest { message: String },
114
115    /// The idempotency key was replayed with different request content.
116    #[error(
117        "system-context idempotency conflict on session {id}: key '{key}' already maps to different content"
118    )]
119    Conflict { id: SessionId, key: String },
120}
121
122impl SessionControlError {
123    /// Return a stable error code string for wire formats.
124    pub fn code(&self) -> &'static str {
125        match self {
126            Self::Session(err) => err.code(),
127            Self::InvalidRequest { .. } => "INVALID_PARAMS",
128            Self::Conflict { .. } => "SESSION_SYSTEM_CONTEXT_CONFLICT",
129        }
130    }
131}
132
133impl SystemContextStageError {
134    /// Convert a stage-time state conflict into a surface-level control error.
135    pub fn into_control_error(self, id: &SessionId) -> SessionControlError {
136        match self {
137            Self::InvalidRequest(message) => SessionControlError::InvalidRequest { message },
138            Self::Conflict { key, .. } => SessionControlError::Conflict {
139                id: id.clone(),
140                key,
141            },
142        }
143    }
144}
145
146/// Request to create a new session and run the first turn.
147#[derive(Debug)]
148pub struct CreateSessionRequest {
149    /// Model name (e.g. "claude-opus-4-6").
150    pub model: String,
151    /// Initial user prompt (text or multimodal).
152    pub prompt: ContentInput,
153    /// Optional normalized rendering metadata for the initial prompt.
154    pub render_metadata: Option<RenderMetadata>,
155    /// Optional system prompt override.
156    pub system_prompt: Option<String>,
157    /// Max tokens per LLM turn.
158    pub max_tokens: Option<u32>,
159    /// Channel for streaming events during the turn.
160    pub event_tx: Option<mpsc::Sender<EventEnvelope<AgentEvent>>>,
161    /// Canonical SkillKeys to resolve and inject for the first turn.
162    pub skill_references: Option<Vec<crate::skills::SkillKey>>,
163    /// Initial turn behavior for this session creation call.
164    pub initial_turn: InitialTurnPolicy,
165    /// How to treat `prompt` when `initial_turn == Defer`.
166    pub deferred_prompt_policy: DeferredPromptPolicy,
167    /// Optional extended build options for factory-backed builders.
168    pub build: Option<SessionBuildOptions>,
169    /// Optional key-value labels attached at session creation.
170    pub labels: Option<BTreeMap<String, String>>,
171}
172
173/// Optional build-time options used by factory-backed session builders.
174#[derive(Clone)]
175pub struct SessionBuildOptions {
176    pub provider: Option<Provider>,
177    pub self_hosted_server_id: Option<String>,
178    pub output_schema: Option<OutputSchema>,
179    pub structured_output_retries: u32,
180    pub hooks_override: HookRunOverrides,
181    pub comms_name: Option<String>,
182    pub peer_meta: Option<PeerMeta>,
183    pub resume_session: Option<Session>,
184    pub budget_limits: Option<BudgetLimits>,
185    pub provider_params: Option<serde_json::Value>,
186    pub external_tools: Option<Arc<dyn AgentToolDispatcher>>,
187    /// Serializable tool definitions used to reconstruct recoverable
188    /// surface-owned dispatchers during session resume/rebuild.
189    pub recoverable_tool_defs: Option<Vec<crate::ToolDef>>,
190    /// Blob store used to externalize durable image content and hydrate refs
191    /// back to bytes at execution seams.
192    pub blob_store_override: Option<Arc<dyn crate::BlobStore>>,
193    /// Opaque transport for an optional per-request LLM override.
194    ///
195    /// Factory builders may downcast this to their concrete client trait.
196    pub llm_client_override: Option<Arc<dyn std::any::Any + Send + Sync>>,
197    // NOTE: ops_lifecycle_override was removed in Phase 3.
198    // Use runtime_build_mode instead.
199    pub override_builtins: ToolCategoryOverride,
200    pub override_shell: ToolCategoryOverride,
201    pub override_memory: ToolCategoryOverride,
202    /// Per-build override for the factory-level scheduler capability.
203    pub override_schedule: ToolCategoryOverride,
204    pub override_mob: ToolCategoryOverride,
205    /// Agent-facing scheduler tools supplied by the embedding surface.
206    ///
207    /// Scheduler remains surface-owned. This dispatcher only controls
208    /// tool visibility/composition for the built agent.
209    pub schedule_tools: Option<Arc<dyn AgentToolDispatcher>>,
210    pub preload_skills: Option<Vec<crate::skills::SkillId>>,
211    pub realm_id: Option<String>,
212    pub instance_id: Option<String>,
213    pub backend: Option<String>,
214    pub config_generation: Option<u64>,
215    /// Whether this session runs as a keep-alive (long-running, interrupt-to-stop)
216    /// agent. Surfaces use this to decide blocking vs fire-and-return semantics.
217    pub keep_alive: bool,
218    /// Optional session checkpointer for keep-alive persistence.
219    pub checkpointer: Option<std::sync::Arc<dyn crate::checkpoint::SessionCheckpointer>>,
220    /// Comms intents that should be silently injected into the session
221    /// without triggering an LLM turn.
222    pub silent_comms_intents: Vec<String>,
223    /// Maximum peer-count threshold for inline peer lifecycle context injection.
224    ///
225    /// - `None`: use runtime default
226    /// - `0`: never inline peer lifecycle notifications
227    /// - `-1`: always inline peer lifecycle notifications
228    /// - `>0`: inline only when post-drain peer count is <= threshold
229    /// - `<-1`: invalid
230    pub max_inline_peer_notifications: Option<i32>,
231    /// Opaque application context passed through to custom `SessionAgentBuilder`
232    /// implementations. Not consumed by the standard build pipeline.
233    ///
234    /// Uses `Value` rather than `Box<RawValue>` because `SessionBuildOptions`
235    /// must be `Clone` and `Box<RawValue>` does not implement `Clone`.
236    /// Same tradeoff as `provider_params`.
237    pub app_context: Option<serde_json::Value>,
238    /// Additional instruction sections appended to the system prompt after skill
239    /// assembly, before tool instructions. Order preserved.
240    pub additional_instructions: Option<Vec<String>>,
241    /// Environment variables injected into shell tool subprocesses for this agent.
242    /// Set by the application's `SessionAgentBuilder` — never by the LLM.
243    /// Values are not included in the agent's context window.
244    pub shell_env: Option<std::collections::HashMap<String, String>>,
245    /// Explicit call-timeout override at the build seam.
246    ///
247    /// - `Inherit` (default): defer to config override, then profile default
248    /// - `Disabled`: explicitly disable call timeout regardless of profile
249    /// - `Value(d)`: explicitly set call timeout to `d`
250    pub call_timeout_override: crate::CallTimeoutOverride,
251    /// Typed explicit-override intent for resumed-session merges.
252    ///
253    /// Surfaces set bits only for fields they can prove were explicitly
254    /// supplied by the caller. Resumed metadata then fills only the
255    /// non-explicit fields.
256    pub resume_override_mask: ResumeOverrideMask,
257    /// Late-binding mob tool factory, called inside `build_agent()` with
258    /// session-scoped args to produce the mob tool dispatcher.
259    ///
260    /// Surfaces that enable mob tools pass an `Arc<dyn MobToolsFactory>` here.
261    /// The factory calls [`MobToolsFactory::build_mob_tools`] during agent
262    /// construction with the session ID, ops lifecycle registry, and optional
263    /// comms runtime — then composes the result into the tool gateway.
264    pub mob_tools: Option<Arc<dyn MobToolsFactory>>,
265    /// Runtime build mode — determines how the factory resolves the ops lifecycle
266    /// registry and completion feed.
267    ///
268    /// - `SessionOwned(bindings)`: runtime-backed build with epoch-owned
269    ///   bindings. Factory validates `bindings.session_id == session.id()`.
270    /// - `StandaloneEphemeral`: factory creates local-only ephemeral bindings.
271    ///   Suitable for WASM, tests, embedded, and standalone surfaces.
272    pub runtime_build_mode: crate::runtime_epoch::RuntimeBuildMode,
273    /// Runtime-injected mob operator authority context.
274    ///
275    /// This is the only source of mob operator tool authority. Tool visibility
276    /// may depend on this context being present, but dispatch-time
277    /// authorization must still re-check the typed create/scope fields on
278    /// every operator call.
279    pub mob_tool_authority_context: Option<MobToolAuthorityContext>,
280}
281
282/// Opaque principal token carried through mob tool authority and provenance.
283///
284/// `meerkat-mob` may store or compare this token as an opaque blob, but it
285/// must not decode token structure, branch on token contents, or expand scope
286/// from it.
287#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
288pub struct OpaquePrincipalToken(String);
289
290impl OpaquePrincipalToken {
291    pub fn new(token: impl Into<String>) -> Self {
292        Self(token.into())
293    }
294
295    pub fn generated() -> Self {
296        Self(uuid::Uuid::new_v4().to_string())
297    }
298
299    pub fn as_str(&self) -> &str {
300        &self.0
301    }
302}
303
304impl std::fmt::Display for OpaquePrincipalToken {
305    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
306        f.write_str(self.as_str())
307    }
308}
309
310/// Runtime-supplied caller provenance carried alongside mob tool authority.
311///
312/// This is informational/projection-only data. It is not a second authority
313/// source and must never be used for policy expansion inside `meerkat-mob`.
314#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, Default)]
315pub struct MobToolCallerProvenance {
316    #[serde(default, skip_serializing_if = "Option::is_none")]
317    caller_session_id: Option<crate::SessionId>,
318    #[serde(default, skip_serializing_if = "Option::is_none")]
319    caller_mob_id: Option<String>,
320    #[serde(default, skip_serializing_if = "Option::is_none")]
321    caller_member_id: Option<String>,
322}
323
324impl MobToolCallerProvenance {
325    pub fn new() -> Self {
326        Self::default()
327    }
328
329    pub fn with_session_id(mut self, session_id: crate::SessionId) -> Self {
330        self.caller_session_id = Some(session_id);
331        self
332    }
333
334    pub fn with_mob_id(mut self, mob_id: impl Into<String>) -> Self {
335        self.caller_mob_id = Some(mob_id.into());
336        self
337    }
338
339    pub fn with_member_id(mut self, member_id: impl Into<String>) -> Self {
340        self.caller_member_id = Some(member_id.into());
341        self
342    }
343
344    pub fn caller_session_id(&self) -> Option<&crate::SessionId> {
345        self.caller_session_id.as_ref()
346    }
347
348    pub fn caller_mob_id(&self) -> Option<&str> {
349        self.caller_mob_id.as_deref()
350    }
351
352    pub fn caller_member_id(&self) -> Option<&str> {
353        self.caller_member_id.as_deref()
354    }
355}
356
357/// Typed mob operator authority injected by the host/runtime.
358///
359/// This is capability-oriented only. It is not an identity or ownership
360/// model, and it must never be inferred from mob membership, session shape,
361/// `owner_session_id`, or profile flags.
362#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
363pub struct MobToolAuthorityContext {
364    principal_token: OpaquePrincipalToken,
365    can_create_mobs: bool,
366    #[serde(default, skip_serializing_if = "BTreeSet::is_empty")]
367    managed_mob_scope: BTreeSet<String>,
368    #[serde(default, skip_serializing_if = "Option::is_none")]
369    caller_provenance: Option<MobToolCallerProvenance>,
370    #[serde(default, skip_serializing_if = "Option::is_none")]
371    audit_invocation_id: Option<String>,
372}
373
374impl MobToolAuthorityContext {
375    pub fn new(principal_token: OpaquePrincipalToken, can_create_mobs: bool) -> Self {
376        Self {
377            principal_token,
378            can_create_mobs,
379            managed_mob_scope: BTreeSet::new(),
380            caller_provenance: None,
381            audit_invocation_id: None,
382        }
383    }
384
385    pub fn create_only_generated() -> Self {
386        Self::new(OpaquePrincipalToken::generated(), true)
387    }
388
389    pub fn principal_token(&self) -> &OpaquePrincipalToken {
390        &self.principal_token
391    }
392
393    pub fn can_create_mobs(&self) -> bool {
394        self.can_create_mobs
395    }
396
397    pub fn managed_mob_scope(&self) -> &BTreeSet<String> {
398        &self.managed_mob_scope
399    }
400
401    pub fn caller_provenance(&self) -> Option<&MobToolCallerProvenance> {
402        self.caller_provenance.as_ref()
403    }
404
405    pub fn audit_invocation_id(&self) -> Option<&str> {
406        self.audit_invocation_id.as_deref()
407    }
408
409    pub fn can_manage_mob(&self, mob_id: &str) -> bool {
410        self.managed_mob_scope.contains(mob_id)
411    }
412
413    pub fn grant_manage_mob(mut self, mob_id: impl Into<String>) -> Self {
414        self.managed_mob_scope.insert(mob_id.into());
415        self
416    }
417
418    /// Grant management scope for a mob in-place (mutable borrow).
419    ///
420    /// Used by the turn executor when applying `SessionEffect::GrantManageMob`
421    /// effects from tool dispatch.
422    pub fn grant_manage_mob_in_place(&mut self, mob_id: String) {
423        self.managed_mob_scope.insert(mob_id);
424    }
425
426    pub fn with_managed_mob_scope<I, S>(mut self, mob_ids: I) -> Self
427    where
428        I: IntoIterator<Item = S>,
429        S: Into<String>,
430    {
431        self.managed_mob_scope = mob_ids.into_iter().map(Into::into).collect();
432        self
433    }
434
435    pub fn with_caller_provenance(mut self, caller_provenance: MobToolCallerProvenance) -> Self {
436        self.caller_provenance = Some(caller_provenance);
437        self
438    }
439
440    pub fn with_audit_invocation_id(mut self, audit_invocation_id: impl Into<String>) -> Self {
441        self.audit_invocation_id = Some(audit_invocation_id.into());
442        self
443    }
444}
445
446/// Shared host/runtime policy for explicit mob-operator enablement.
447///
448/// When a host/runtime build seam explicitly enables mob operator tools for a
449/// session, the default authority shape is create-only. Existing-mob scope
450/// must still be injected separately and explicitly.
451pub fn generated_create_only_mob_operator_authority(
452    enable_mob: ToolCategoryOverride,
453) -> Option<MobToolAuthorityContext> {
454    matches!(enable_mob, ToolCategoryOverride::Enable)
455        .then(MobToolAuthorityContext::create_only_generated)
456}
457
458/// Shared build-seam rule for mob operator access rehydration.
459///
460/// Explicit disable clears authority. Otherwise, persisted typed authority
461/// wins; if none exists, explicit mob enablement falls back to generated
462/// create-only authority.
463pub fn resolve_mob_operator_access(
464    enable_mob: ToolCategoryOverride,
465    persisted_authority_context: Option<MobToolAuthorityContext>,
466) -> (ToolCategoryOverride, Option<MobToolAuthorityContext>) {
467    if matches!(enable_mob, ToolCategoryOverride::Disable) {
468        return (ToolCategoryOverride::Disable, None);
469    }
470
471    let authority_context = persisted_authority_context
472        .or_else(|| generated_create_only_mob_operator_authority(enable_mob));
473    let override_mob = if authority_context.is_some() {
474        ToolCategoryOverride::Enable
475    } else {
476        enable_mob
477    };
478
479    (override_mob, authority_context)
480}
481
482/// Provider of a snapshot of currently visible tools.
483///
484/// Implemented by the agent's `ToolScope` holder to capture tool visibility
485/// at spawn time for inheritance by mob children.
486pub trait VisibleToolSnapshotProvider: Send + Sync {
487    /// Returns the tool definitions currently visible to the parent agent.
488    fn snapshot_visible_tools(&self) -> Vec<Arc<ToolDef>>;
489}
490
491/// Context for capturing a parent agent's tool scope snapshot.
492///
493/// `ParentOwned` carries a provider that can snapshot the parent's visible
494/// tools at child spawn time. `Standalone` means no parent scope is available
495/// (e.g. top-level agents, tests).
496pub enum MobToolSnapshotContext {
497    /// Parent agent owns a tool scope; snapshot available on demand.
498    ParentOwned(Arc<dyn VisibleToolSnapshotProvider>),
499    /// No parent scope available.
500    Standalone,
501}
502
503/// Session-scoped arguments passed to [`MobToolsFactory::build_mob_tools`].
504pub struct MobToolsBuildArgs {
505    /// Session ID of the agent being built.
506    pub session_id: crate::SessionId,
507    /// Model name of the owning agent — inherited by implicit mob helpers.
508    pub model: String,
509    /// Runtime-injected mob operator authority context.
510    ///
511    /// Tool visibility may depend on this context being present, but operator
512    /// dispatch must still re-check the typed create/scope fields on every
513    /// call.
514    pub authority_context: Option<MobToolAuthorityContext>,
515    /// Shared effective mob authority handle owned by the agent.
516    ///
517    /// Mob tools read from this handle for authorization checks. The agent
518    /// (turn owner) is the sole writer — it updates this handle via
519    /// `apply_session_effects` after merging tool-produced `SessionEffect`s.
520    /// If `None`, mob tools fall back to `authority_context` as a static snapshot.
521    pub effective_authority: Option<Arc<std::sync::RwLock<MobToolAuthorityContext>>>,
522    /// Comms name of the owning agent (for building TrustedPeerSpec).
523    pub comms_name: Option<String>,
524    /// Optional comms runtime for auto-wiring spawned members.
525    pub comms_runtime: Option<Arc<dyn crate::agent::CommsRuntime>>,
526    /// Context for capturing a snapshot of the parent agent's visible tools.
527    pub snapshot_context: MobToolSnapshotContext,
528}
529
530/// Factory trait for late-binding mob tool construction.
531///
532/// Implementations capture surface-specific state (e.g. `MobMcpState`) and
533/// receive session-scoped arguments from `build_agent()` at construction time.
534/// This avoids a cyclic dependency between the facade crate and `meerkat-mob-mcp`.
535#[cfg_attr(target_arch = "wasm32", async_trait(?Send))]
536#[cfg_attr(not(target_arch = "wasm32"), async_trait)]
537pub trait MobToolsFactory: Send + Sync {
538    /// Build a mob tool dispatcher for the given session.
539    async fn build_mob_tools(
540        &self,
541        args: MobToolsBuildArgs,
542    ) -> Result<Arc<dyn AgentToolDispatcher>, Box<dyn std::error::Error + Send + Sync>>;
543}
544
545/// Typed explicit-override intent for resumed-session metadata merges.
546///
547/// This avoids trying to recover caller intent from flattened build config.
548#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
549pub struct ResumeOverrideMask {
550    pub model: bool,
551    pub provider: bool,
552    pub max_tokens: bool,
553    pub structured_output_retries: bool,
554    pub provider_params: bool,
555    pub override_builtins: bool,
556    pub override_shell: bool,
557    pub override_memory: bool,
558    pub override_mob: bool,
559    pub preload_skills: bool,
560    pub keep_alive: bool,
561    pub comms_name: bool,
562    pub peer_meta: bool,
563}
564
565impl SessionBuildOptions {
566    /// Apply the shared rehydration rule for mob operator access.
567    ///
568    /// This preserves exact persisted authority when available and otherwise
569    /// falls back to generated create-only authority for explicit mob
570    /// enablement.
571    pub fn apply_persisted_mob_operator_access(
572        &mut self,
573        enable_mob: ToolCategoryOverride,
574        persisted_authority_context: Option<MobToolAuthorityContext>,
575    ) {
576        let (override_mob, authority_context) =
577            resolve_mob_operator_access(enable_mob, persisted_authority_context);
578        self.override_mob = override_mob;
579        self.mob_tool_authority_context = authority_context;
580    }
581
582    /// Apply the shared host/runtime default for explicit mob operator
583    /// enablement.
584    ///
585    /// This keeps `override_mob` and the generated create-only authority
586    /// context aligned at the composition seam. Existing-mob scope must be
587    /// injected explicitly elsewhere; this helper never infers it.
588    pub fn apply_generated_create_only_mob_operator_access(
589        &mut self,
590        enable_mob: ToolCategoryOverride,
591    ) {
592        self.apply_persisted_mob_operator_access(enable_mob, None);
593    }
594}
595
596impl Default for SessionBuildOptions {
597    fn default() -> Self {
598        Self {
599            provider: None,
600            self_hosted_server_id: None,
601            output_schema: None,
602            structured_output_retries: 2,
603            hooks_override: HookRunOverrides::default(),
604            comms_name: None,
605            peer_meta: None,
606            resume_session: None,
607            budget_limits: None,
608            provider_params: None,
609            external_tools: None,
610            recoverable_tool_defs: None,
611            blob_store_override: None,
612            llm_client_override: None,
613            override_builtins: ToolCategoryOverride::Inherit,
614            override_shell: ToolCategoryOverride::Inherit,
615            override_memory: ToolCategoryOverride::Inherit,
616            override_schedule: ToolCategoryOverride::Inherit,
617            override_mob: ToolCategoryOverride::Inherit,
618            schedule_tools: None,
619            preload_skills: None,
620            realm_id: None,
621            instance_id: None,
622            backend: None,
623            config_generation: None,
624            keep_alive: false,
625            checkpointer: None,
626            silent_comms_intents: Vec::new(),
627            max_inline_peer_notifications: None,
628            app_context: None,
629            additional_instructions: None,
630            shell_env: None,
631            call_timeout_override: crate::CallTimeoutOverride::Inherit,
632            resume_override_mask: ResumeOverrideMask::default(),
633            mob_tools: None,
634            runtime_build_mode: crate::runtime_epoch::RuntimeBuildMode::StandaloneEphemeral,
635            mob_tool_authority_context: None,
636        }
637    }
638}
639
640impl std::fmt::Debug for SessionBuildOptions {
641    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
642        f.debug_struct("SessionBuildOptions")
643            .field("provider", &self.provider)
644            .field("output_schema", &self.output_schema.is_some())
645            .field("structured_output_retries", &self.structured_output_retries)
646            .field("hooks_override", &self.hooks_override)
647            .field("comms_name", &self.comms_name)
648            .field("peer_meta", &self.peer_meta)
649            .field("resume_session", &self.resume_session.is_some())
650            .field("budget_limits", &self.budget_limits)
651            .field("provider_params", &self.provider_params.is_some())
652            .field("external_tools", &self.external_tools.is_some())
653            .field("recoverable_tool_defs", &self.recoverable_tool_defs)
654            .field("blob_store_override", &self.blob_store_override.is_some())
655            .field("llm_client_override", &self.llm_client_override.is_some())
656            .field("override_builtins", &self.override_builtins)
657            .field("override_shell", &self.override_shell)
658            .field("override_memory", &self.override_memory)
659            .field("override_schedule", &self.override_schedule)
660            .field("override_mob", &self.override_mob)
661            .field("schedule_tools", &self.schedule_tools.is_some())
662            .field("preload_skills", &self.preload_skills)
663            .field("realm_id", &self.realm_id)
664            .field("instance_id", &self.instance_id)
665            .field("backend", &self.backend)
666            .field("config_generation", &self.config_generation)
667            .field("keep_alive", &self.keep_alive)
668            .field("checkpointer", &self.checkpointer.is_some())
669            .field("silent_comms_intents", &self.silent_comms_intents)
670            .field(
671                "max_inline_peer_notifications",
672                &self.max_inline_peer_notifications,
673            )
674            .field("app_context", &self.app_context.is_some())
675            .field("additional_instructions", &self.additional_instructions)
676            .field("call_timeout_override", &self.call_timeout_override)
677            .field("resume_override_mask", &self.resume_override_mask)
678            .field("mob_tools", &self.mob_tools.is_some())
679            .field("runtime_build_mode", &self.runtime_build_mode)
680            .field(
681                "mob_tool_authority_context",
682                &self.mob_tool_authority_context.is_some(),
683            )
684            .field("runtime_build_mode", &self.runtime_build_mode)
685            .finish()
686    }
687}
688
689/// Request to start a new turn on an existing session.
690#[derive(Debug)]
691pub struct StartTurnRequest {
692    /// User prompt for this turn (text or multimodal).
693    pub prompt: ContentInput,
694    /// Optional system prompt override for a deferred session's first turn.
695    ///
696    /// This is only supported before the session has any conversation history.
697    /// Materialized sessions with existing messages must reject it.
698    pub system_prompt: Option<String>,
699    /// Optional normalized rendering metadata for this turn prompt.
700    pub render_metadata: Option<RenderMetadata>,
701    /// Handling mode for this turn's ordinary content-bearing work.
702    ///
703    /// This is a **runtime-owned semantic**: the runtime routes Queue/Steer
704    /// before calling the executor. The session service passes this through
705    /// to the `SessionAgent` but does not act on it. Non-Queue handling
706    /// only works correctly on runtime-backed surfaces.
707    pub handling_mode: HandlingMode,
708    /// Channel for streaming events during the turn.
709    pub event_tx: Option<mpsc::Sender<EventEnvelope<AgentEvent>>>,
710    /// Canonical SkillKeys to resolve and inject for this turn.
711    pub skill_references: Option<Vec<crate::skills::SkillKey>>,
712    /// Optional per-turn flow tool overlay (ephemeral, non-persistent).
713    pub flow_tool_overlay: Option<TurnToolOverlay>,
714    /// Optional additional instructions prepended as `[SYSTEM NOTICE: ...]` to the user prompt.
715    ///
716    /// Unlike `SessionBuildOptions.additional_instructions` (which are appended to the
717    /// system prompt as extra sections at session creation), turn-level instructions
718    /// are prepended to the user message as `[SYSTEM NOTICE: {instruction}]` blocks.
719    /// This distinction means create-time instructions persist across turns (system prompt)
720    /// while turn-level instructions are per-turn only (conversation history).
721    pub additional_instructions: Option<Vec<String>>,
722    /// Typed execution intent from the runtime layer.
723    ///
724    /// `Some(ContentTurn)` forces `run_turn`, `Some(ResumePending)` forces
725    /// `run_pending`. `None` preserves the existing `has_prompt` heuristic
726    /// for non-runtime substrate-direct paths.
727    pub execution_kind: Option<crate::lifecycle::run_primitive::RuntimeExecutionKind>,
728}
729
730/// Request to append runtime system context to an existing session.
731#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
732pub struct AppendSystemContextRequest {
733    pub text: String,
734    #[serde(default, skip_serializing_if = "Option::is_none")]
735    pub source: Option<String>,
736    #[serde(default, skip_serializing_if = "Option::is_none")]
737    pub idempotency_key: Option<String>,
738}
739
740/// Result of appending runtime system context to a session.
741#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
742pub struct AppendSystemContextResult {
743    pub status: AppendSystemContextStatus,
744}
745
746/// Request to stage callback tool results for the next turn.
747#[derive(Debug, Clone, Serialize, Deserialize)]
748pub struct StageToolResultsRequest {
749    pub results: Vec<crate::ToolResult>,
750}
751
752/// Result of staging callback tool results for the next turn.
753#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
754pub struct StageToolResultsResult {
755    pub accepted_result_count: usize,
756}
757
758/// Outcome of an append-system-context request.
759#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
760#[serde(rename_all = "snake_case")]
761pub enum AppendSystemContextStatus {
762    Applied,
763    Staged,
764    Duplicate,
765}
766
767/// Ephemeral per-turn tool overlay for flow-dispatched turns.
768#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
769pub struct TurnToolOverlay {
770    /// Optional allow-list for this turn.
771    #[serde(default)]
772    pub allowed_tools: Option<Vec<String>>,
773    /// Optional deny-list for this turn.
774    #[serde(default)]
775    pub blocked_tools: Option<Vec<String>>,
776}
777
778/// Query parameters for listing sessions.
779#[derive(Debug, Default)]
780pub struct SessionQuery {
781    /// Maximum number of results.
782    pub limit: Option<usize>,
783    /// Offset for pagination.
784    pub offset: Option<usize>,
785    /// Filters sessions where all specified k/v pairs match.
786    pub labels: Option<BTreeMap<String, String>>,
787}
788
789/// Summary of a session (for list results).
790///
791/// Kept lightweight — no billing data. Use `read()` for full details.
792#[derive(Debug, Clone, Serialize, Deserialize)]
793pub struct SessionSummary {
794    pub session_id: SessionId,
795    pub created_at: SystemTime,
796    pub updated_at: SystemTime,
797    pub message_count: usize,
798    pub total_tokens: u64,
799    pub is_active: bool,
800    #[serde(default, skip_serializing_if = "BTreeMap::is_empty")]
801    pub labels: BTreeMap<String, String>,
802}
803
804/// Detailed view of a session's state and history metadata.
805#[derive(Debug, Clone, Serialize, Deserialize)]
806pub struct SessionInfo {
807    pub session_id: SessionId,
808    pub created_at: SystemTime,
809    pub updated_at: SystemTime,
810    pub message_count: usize,
811    pub is_active: bool,
812    pub model: String,
813    pub provider: Provider,
814    pub last_assistant_text: Option<String>,
815    #[serde(default, skip_serializing_if = "BTreeMap::is_empty")]
816    pub labels: BTreeMap<String, String>,
817}
818
819/// Billing/usage data for a session, returned separately from state.
820#[derive(Debug, Clone, Serialize, Deserialize)]
821pub struct SessionUsage {
822    pub total_tokens: u64,
823    pub usage: Usage,
824}
825
826/// Combined session view (state + usage). Convenience wrapper used by
827/// `SessionService::read()` to avoid requiring two calls.
828#[derive(Debug, Clone, Serialize, Deserialize)]
829pub struct SessionView {
830    pub state: SessionInfo,
831    pub billing: SessionUsage,
832}
833
834impl SessionView {
835    /// Convenience: session ID from the state.
836    pub fn session_id(&self) -> &SessionId {
837        &self.state.session_id
838    }
839}
840
841/// Query parameters for reading session history.
842#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq, Eq)]
843pub struct SessionHistoryQuery {
844    /// Number of messages to skip from the start of the transcript.
845    pub offset: usize,
846    /// Maximum number of messages to return.
847    #[serde(default, skip_serializing_if = "Option::is_none")]
848    pub limit: Option<usize>,
849}
850
851/// Paginated transcript page for a session.
852#[derive(Debug, Clone, Serialize, Deserialize)]
853pub struct SessionHistoryPage {
854    pub session_id: SessionId,
855    pub message_count: usize,
856    pub offset: usize,
857    #[serde(default, skip_serializing_if = "Option::is_none")]
858    pub limit: Option<usize>,
859    pub has_more: bool,
860    pub messages: Vec<Message>,
861}
862
863impl SessionHistoryPage {
864    /// Build a transcript page from the full ordered message list.
865    pub fn from_messages(
866        session_id: SessionId,
867        messages: &[Message],
868        query: SessionHistoryQuery,
869    ) -> Self {
870        let message_count = messages.len();
871        let start = query.offset.min(message_count);
872        let end = match query.limit {
873            Some(limit) => start.saturating_add(limit).min(message_count),
874            None => message_count,
875        };
876        Self {
877            session_id,
878            message_count,
879            offset: start,
880            limit: query.limit,
881            has_more: end < message_count,
882            messages: messages[start..end].to_vec(),
883        }
884    }
885}
886
887/// Canonical session lifecycle abstraction.
888///
889/// All surfaces delegate to this trait. Implementations control persistence,
890/// compaction, and event logging behavior.
891#[cfg_attr(target_arch = "wasm32", async_trait(?Send))]
892#[cfg_attr(not(target_arch = "wasm32"), async_trait)]
893pub trait SessionService: Send + Sync {
894    /// Create a new session and run the first turn.
895    async fn create_session(&self, req: CreateSessionRequest) -> Result<RunResult, SessionError>;
896
897    /// Start a new turn on an existing session.
898    async fn start_turn(
899        &self,
900        id: &SessionId,
901        req: StartTurnRequest,
902    ) -> Result<RunResult, SessionError>;
903
904    /// Cancel an in-flight turn.
905    ///
906    /// Returns `NotRunning` if no turn is active.
907    async fn interrupt(&self, id: &SessionId) -> Result<(), SessionError>;
908
909    /// Replace the LLM client on a live session.
910    ///
911    /// Enables mid-session model/provider hot-swap without rebuilding the
912    /// agent. The new client takes effect on the next turn. Returns
913    /// `Unsupported` by default; session services that support live agents
914    /// override this.
915    async fn set_session_client(
916        &self,
917        _id: &SessionId,
918        _client: std::sync::Arc<dyn crate::AgentLlmClient>,
919    ) -> Result<(), SessionError> {
920        Err(SessionError::Unsupported("set_session_client".to_string()))
921    }
922
923    /// Atomically replace the live session client and the session's durable
924    /// LLM identity.
925    ///
926    /// This is the canonical seam for materialized-session hot-swap semantics.
927    /// Implementations should apply both updates together so future turns and
928    /// resume/recovery see the same model/provider/provider_params identity.
929    async fn hot_swap_session_llm_identity(
930        &self,
931        _id: &SessionId,
932        _client: std::sync::Arc<dyn crate::AgentLlmClient>,
933        _identity: SessionLlmIdentity,
934    ) -> Result<(), SessionError> {
935        Err(SessionError::Unsupported(
936            "hot_swap_session_llm_identity".to_string(),
937        ))
938    }
939
940    /// Update the `keep_alive` flag on a live session's durable metadata.
941    ///
942    /// Called by the runtime when an explicit override changes the session's
943    /// keep-alive intent so that subsequent inheriting calls observe the
944    /// updated value. Returns `Unsupported` by default.
945    async fn update_session_keep_alive(
946        &self,
947        _id: &SessionId,
948        _keep_alive: bool,
949    ) -> Result<(), SessionError> {
950        Err(SessionError::Unsupported(
951            "update_session_keep_alive".to_string(),
952        ))
953    }
954
955    /// Update the session's canonical mob operator authority context.
956    ///
957    /// This is the only supported seam for widening or narrowing exact mob
958    /// management scope after session creation so recovery and live runtime
959    /// state stay aligned.
960    async fn update_session_mob_authority_context(
961        &self,
962        _id: &SessionId,
963        _authority_context: Option<MobToolAuthorityContext>,
964    ) -> Result<(), SessionError> {
965        Err(SessionError::Unsupported(
966            "update_session_mob_authority_context".to_string(),
967        ))
968    }
969
970    /// Whether a live in-memory session bridge currently exists for `id`.
971    ///
972    /// This is intentionally distinct from `list()` / `SessionSummary`:
973    /// persisted-only summaries must not count as live, and idle live sessions
974    /// must still count as live even when no turn is running.
975    async fn has_live_session(&self, _id: &SessionId) -> Result<bool, SessionError> {
976        Err(SessionError::Unsupported("has_live_session".to_string()))
977    }
978
979    /// Stage an external tool visibility filter on a live session.
980    ///
981    /// Used to dynamically hide/show tools (e.g., `view_image`) after a
982    /// model hot-swap changes capability support. Returns `Unsupported`
983    /// by default.
984    async fn set_session_tool_filter(
985        &self,
986        _id: &SessionId,
987        _filter: crate::ToolFilter,
988    ) -> Result<(), SessionError> {
989        Err(SessionError::Unsupported(
990            "set_session_tool_filter".to_string(),
991        ))
992    }
993
994    /// Read the current state of a session.
995    async fn read(&self, id: &SessionId) -> Result<SessionView, SessionError>;
996
997    /// List sessions matching the query.
998    async fn list(&self, query: SessionQuery) -> Result<Vec<SessionSummary>, SessionError>;
999
1000    /// Archive (remove) a session.
1001    async fn archive(&self, id: &SessionId) -> Result<(), SessionError>;
1002
1003    /// Subscribe to session-wide events regardless of triggering interaction.
1004    ///
1005    /// Services that do not support this capability return `StreamError::NotFound`.
1006    async fn subscribe_session_events(&self, id: &SessionId) -> Result<EventStream, StreamError> {
1007        Err(StreamError::NotFound(format!("session {id}")))
1008    }
1009}
1010
1011/// Optional comms/control-plane extension for `SessionService`.
1012///
1013/// Base lifecycle operations stay on `SessionService`; advanced surfaces
1014/// (RPC/REST/mob orchestration) can use this trait when they need direct
1015/// access to comms runtime and injector handles.
1016#[cfg_attr(target_arch = "wasm32", async_trait(?Send))]
1017#[cfg_attr(not(target_arch = "wasm32"), async_trait)]
1018pub trait SessionServiceCommsExt: SessionService {
1019    /// Get the comms runtime for a session, if available.
1020    async fn comms_runtime(
1021        &self,
1022        _session_id: &SessionId,
1023    ) -> Option<Arc<dyn crate::agent::CommsRuntime>> {
1024        None
1025    }
1026
1027    /// Get the event injector for a session, if available.
1028    async fn event_injector(
1029        &self,
1030        session_id: &SessionId,
1031    ) -> Option<Arc<dyn crate::EventInjector>> {
1032        self.comms_runtime(session_id)
1033            .await
1034            .and_then(|runtime| runtime.event_injector())
1035    }
1036
1037    /// Internal runtime seam for interaction-scoped injection.
1038    #[doc(hidden)]
1039    async fn interaction_event_injector(
1040        &self,
1041        session_id: &SessionId,
1042    ) -> Option<Arc<dyn crate::event_injector::SubscribableInjector>> {
1043        self.comms_runtime(session_id)
1044            .await
1045            .and_then(|runtime| runtime.interaction_event_injector())
1046    }
1047}
1048
1049/// Optional control-plane extension for `SessionService`.
1050///
1051/// Keeps the base lifecycle contract minimal while exposing first-class
1052/// session mutation operations shared across external surfaces.
1053#[cfg_attr(target_arch = "wasm32", async_trait(?Send))]
1054#[cfg_attr(not(target_arch = "wasm32"), async_trait)]
1055pub trait SessionServiceControlExt: SessionService {
1056    /// Append runtime system context to a session.
1057    ///
1058    /// The request is idempotent per `(session_id, idempotency_key)`. When a
1059    /// turn is active, implementations may stage the append for application at
1060    /// the next LLM boundary rather than mutating in-flight request state.
1061    async fn append_system_context(
1062        &self,
1063        id: &SessionId,
1064        req: AppendSystemContextRequest,
1065    ) -> Result<AppendSystemContextResult, SessionControlError>;
1066
1067    /// Stage callback tool results for application on the next turn seam.
1068    ///
1069    /// Implementations must persist the staged results durably before a live
1070    /// session can observe them so a failed call never leaves hidden pending
1071    /// transcript mutations behind.
1072    async fn stage_tool_results(
1073        &self,
1074        id: &SessionId,
1075        req: StageToolResultsRequest,
1076    ) -> Result<StageToolResultsResult, SessionError> {
1077        let _ = (id, req);
1078        Err(SessionError::Unsupported("stage_tool_results".to_string()))
1079    }
1080}
1081
1082/// Optional history-read extension for `SessionService`.
1083///
1084/// Keeps the base lifecycle contract lightweight while allowing surfaces to
1085/// fetch full transcript contents when they explicitly opt in.
1086#[cfg_attr(target_arch = "wasm32", async_trait(?Send))]
1087#[cfg_attr(not(target_arch = "wasm32"), async_trait)]
1088pub trait SessionServiceHistoryExt: SessionService {
1089    /// Read the committed transcript for a session.
1090    ///
1091    /// Implementations may return `PersistenceDisabled` if they cannot provide
1092    /// authoritative history for the requested lifecycle state.
1093    async fn read_history(
1094        &self,
1095        id: &SessionId,
1096        query: SessionHistoryQuery,
1097    ) -> Result<SessionHistoryPage, SessionError>;
1098}
1099
1100/// Extension trait for `Arc<dyn SessionService>` to allow calling methods directly.
1101impl dyn SessionService {
1102    /// Wrap self in an Arc.
1103    pub fn into_arc(self: Box<Self>) -> Arc<dyn SessionService> {
1104        Arc::from(self)
1105    }
1106}
1107
1108#[cfg(test)]
1109#[allow(
1110    clippy::unimplemented,
1111    clippy::unwrap_used,
1112    clippy::expect_used,
1113    clippy::panic
1114)]
1115mod tests {
1116    use super::*;
1117
1118    struct UnsupportedSessionService;
1119
1120    #[cfg_attr(target_arch = "wasm32", async_trait(?Send))]
1121    #[cfg_attr(not(target_arch = "wasm32"), async_trait)]
1122    impl SessionService for UnsupportedSessionService {
1123        async fn create_session(
1124            &self,
1125            _req: CreateSessionRequest,
1126        ) -> Result<RunResult, SessionError> {
1127            unimplemented!()
1128        }
1129
1130        async fn start_turn(
1131            &self,
1132            _id: &SessionId,
1133            _req: StartTurnRequest,
1134        ) -> Result<RunResult, SessionError> {
1135            unimplemented!()
1136        }
1137
1138        async fn interrupt(&self, _id: &SessionId) -> Result<(), SessionError> {
1139            unimplemented!()
1140        }
1141
1142        async fn read(&self, _id: &SessionId) -> Result<SessionView, SessionError> {
1143            unimplemented!()
1144        }
1145
1146        async fn list(&self, _query: SessionQuery) -> Result<Vec<SessionSummary>, SessionError> {
1147            unimplemented!()
1148        }
1149
1150        async fn archive(&self, _id: &SessionId) -> Result<(), SessionError> {
1151            unimplemented!()
1152        }
1153    }
1154
1155    #[tokio::test]
1156    async fn has_live_session_defaults_to_unsupported() {
1157        let service = UnsupportedSessionService;
1158        let err = service
1159            .has_live_session(&SessionId::new())
1160            .await
1161            .expect_err("default implementation should fail loudly");
1162        assert!(matches!(err, SessionError::Unsupported(name) if name == "has_live_session"));
1163    }
1164
1165    #[test]
1166    fn grant_manage_mob_in_place_adds_mob_id() {
1167        let mut ctx = MobToolAuthorityContext::create_only_generated();
1168        ctx.grant_manage_mob_in_place("mob-1".into());
1169        assert!(ctx.managed_mob_scope.contains("mob-1"));
1170    }
1171
1172    #[test]
1173    fn grant_manage_mob_in_place_is_idempotent() {
1174        let mut ctx = MobToolAuthorityContext::create_only_generated();
1175        ctx.grant_manage_mob_in_place("mob-1".into());
1176        ctx.grant_manage_mob_in_place("mob-1".into());
1177        assert_eq!(ctx.managed_mob_scope.len(), 1);
1178    }
1179
1180    #[test]
1181    fn grant_manage_mob_in_place_accumulates() {
1182        let mut ctx = MobToolAuthorityContext::create_only_generated();
1183        ctx.grant_manage_mob_in_place("mob-1".into());
1184        ctx.grant_manage_mob_in_place("mob-2".into());
1185        assert!(ctx.managed_mob_scope.contains("mob-1"));
1186        assert!(ctx.managed_mob_scope.contains("mob-2"));
1187        assert_eq!(ctx.managed_mob_scope.len(), 2);
1188    }
1189
1190    struct MockSnapshotProvider {
1191        tools: Vec<Arc<ToolDef>>,
1192    }
1193
1194    impl VisibleToolSnapshotProvider for MockSnapshotProvider {
1195        fn snapshot_visible_tools(&self) -> Vec<Arc<ToolDef>> {
1196            self.tools.clone()
1197        }
1198    }
1199
1200    #[test]
1201    fn mob_tool_snapshot_context_standalone() {
1202        let ctx = MobToolSnapshotContext::Standalone;
1203        assert!(matches!(ctx, MobToolSnapshotContext::Standalone));
1204    }
1205
1206    #[test]
1207    fn mob_tool_snapshot_context_parent_owned_returns_tools() {
1208        let tools = vec![Arc::new(ToolDef {
1209            name: "test_tool".to_string(),
1210            description: "a test".to_string(),
1211            input_schema: serde_json::json!({"type": "object"}),
1212            provenance: None,
1213        })];
1214        let provider = Arc::new(MockSnapshotProvider { tools });
1215        let ctx = MobToolSnapshotContext::ParentOwned(provider);
1216        match ctx {
1217            MobToolSnapshotContext::ParentOwned(p) => {
1218                let snapshot = p.snapshot_visible_tools();
1219                assert_eq!(snapshot.len(), 1);
1220                assert_eq!(snapshot[0].name, "test_tool");
1221            }
1222            MobToolSnapshotContext::Standalone => panic!("expected ParentOwned"),
1223        }
1224    }
1225}