Skip to main content

mobius_gateway/wire/
records.rs

1use std::borrow::Cow;
2use std::sync::Arc;
3
4use mobius::backend::model::provider::MediaModelPreset;
5pub use mobius::backend::model::provider::{
6    ModelPreset as ProviderModel, ReasoningPreset as ReasoningChoice,
7};
8
9use super::*;
10
11/// Optional gateway broadcasts a client may suppress on its connection.
12/// Approvals, session events, errors, and request responses cannot be suppressed.
13#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
14#[serde(rename_all = "snake_case")]
15pub enum GatewayNotification {
16    /// Unsolicited conversation catalog and activity updates.
17    Sessions,
18    /// Unsolicited Bot catalog updates.
19    Bots,
20}
21
22/// The computer view this connection can open.
23#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
24#[serde(rename_all = "snake_case")]
25pub enum ComputerView {
26    /// No interactive computer view is available.
27    Unavailable,
28    /// The local app renders the gateway's assigned browser page.
29    EmbeddedBrowser,
30    /// The gateway serves its desktop through an authenticated stream.
31    RemoteDesktop,
32}
33
34/// Gateway-wide frontend-safe state sent after authentication.
35#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
36pub struct ReadyPayload {
37    /// Release version of the connected gateway process.
38    pub gateway_version: String,
39    /// The machine name.
40    pub machine_name: String,
41    /// The interactive computer view available to this connection.
42    pub computer_view: ComputerView,
43    /// The bots.
44    pub bots: Vec<BotRecord>,
45    /// The sessions.
46    pub sessions: Vec<SessionRecord>,
47    /// The background approvals.
48    pub background_approvals: Vec<BackgroundApproval>,
49    /// The providers.
50    pub providers: Vec<ProviderStatus>,
51    /// The provider instances.
52    pub provider_instances: Vec<ProviderInstance>,
53    /// The bot defaults.
54    pub bot_defaults: Option<VersionedAgentConfig>,
55    /// The models.
56    pub models: Vec<ModelChoice>,
57    /// The model providers.
58    pub model_providers: BTreeMap<String, String>,
59    /// Selectable image model routes, keyed into `model_providers` like `models`.
60    #[serde(default)]
61    pub image_models: Vec<ModelChoice>,
62    /// Selectable voice routes; each route's variant is its voice.
63    #[serde(default)]
64    pub voice_models: Vec<ModelChoice>,
65    /// The middleware features.
66    pub middleware_features: Vec<MiddlewareFeature>,
67    /// The extensions.
68    pub extensions: Vec<ExtensionRecord>,
69    /// The contributions.
70    pub contributions: Vec<FrontendContribution>,
71    /// The max active sessions.
72    pub max_active_sessions: usize,
73    /// The session file limits.
74    pub session_file_limits: SessionFileLimits,
75    /// Content revision of each cacheable section.
76    pub revisions: BTreeMap<ReadySection, String>,
77    /// Sections sent empty because the client holds them at these revisions or skips them.
78    pub omitted: BTreeSet<ReadySection>,
79}
80
81impl ReadyPayload {
82    /// Exchanges one cacheable section with `other`.
83    pub fn swap_section(&mut self, other: &mut Self, section: ReadySection) {
84        use std::mem::swap;
85        match section {
86            ReadySection::Config => {
87                swap(&mut self.providers, &mut other.providers);
88                swap(&mut self.provider_instances, &mut other.provider_instances);
89                swap(&mut self.bot_defaults, &mut other.bot_defaults);
90                swap(&mut self.models, &mut other.models);
91                swap(&mut self.model_providers, &mut other.model_providers);
92                swap(&mut self.image_models, &mut other.image_models);
93                swap(&mut self.voice_models, &mut other.voice_models);
94                swap(
95                    &mut self.middleware_features,
96                    &mut other.middleware_features,
97                );
98                swap(&mut self.extensions, &mut other.extensions);
99                swap(&mut self.contributions, &mut other.contributions);
100            }
101            ReadySection::Bots => swap(&mut self.bots, &mut other.bots),
102            ReadySection::Sessions => swap(&mut self.sessions, &mut other.sessions),
103        }
104    }
105
106    /// Moves the sections the gateway omitted back in from the catalog the client holds.
107    pub fn restore_omitted(&mut self, held: &mut Self) {
108        for section in std::mem::take(&mut self.omitted) {
109            self.swap_section(held, section);
110        }
111    }
112
113    /// Replaces this held catalog with a newer Ready, keeping the sections it omitted.
114    pub fn update(&mut self, mut next: Self) {
115        next.restore_omitted(self);
116        *self = next;
117    }
118
119    /// Content revision of one section, comparable across connections to one gateway build.
120    #[must_use]
121    pub fn revision(&self, section: ReadySection) -> String {
122        match section {
123            ReadySection::Config => content_revision(&(
124                &self.providers,
125                &self.provider_instances,
126                &self.bot_defaults,
127                &self.models,
128                &self.model_providers,
129                &self.image_models,
130                &self.voice_models,
131                &self.middleware_features,
132                &self.extensions,
133                &self.contributions,
134            )),
135            ReadySection::Bots => content_revision(&self.bots),
136            ReadySection::Sessions => content_revision(&self.sessions),
137        }
138    }
139
140    /// The same gateway identity with no catalog content.
141    pub(crate) fn blank(&self) -> Self {
142        Self {
143            gateway_version: String::new(),
144            machine_name: String::new(),
145            computer_view: self.computer_view,
146            bots: Vec::new(),
147            sessions: Vec::new(),
148            background_approvals: Vec::new(),
149            providers: Vec::new(),
150            provider_instances: Vec::new(),
151            bot_defaults: None,
152            models: Vec::new(),
153            model_providers: BTreeMap::new(),
154            image_models: Vec::new(),
155            voice_models: Vec::new(),
156            middleware_features: Vec::new(),
157            extensions: Vec::new(),
158            contributions: Vec::new(),
159            max_active_sessions: self.max_active_sessions,
160            session_file_limits: self.session_file_limits,
161            revisions: BTreeMap::new(),
162            omitted: BTreeSet::new(),
163        }
164    }
165}
166
167/// A stable digest of one catalog value's wire form.
168#[must_use]
169pub fn content_revision(value: &impl Serialize) -> String {
170    use sha2::Digest as _;
171    let mut hasher = sha2::Sha256::new();
172    // Serializing into a digest cannot fail for catalog records.
173    let _ = serde_json::to_writer(&mut hasher, value);
174    hasher.finalize()[..16]
175        .iter()
176        .map(|byte| format!("{byte:02x}"))
177        .collect()
178}
179
180/// A cacheable part of the Ready catalog.
181#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
182#[serde(rename_all = "snake_case")]
183pub enum ReadySection {
184    /// Providers, models, Bot defaults, middleware, extensions and contributions.
185    Config,
186    /// The Bot catalog.
187    Bots,
188    /// The visible session catalog.
189    Sessions,
190}
191
192/// Every section a Ready payload can omit.
193pub const READY_SECTIONS: [ReadySection; 3] = [
194    ReadySection::Config,
195    ReadySection::Bots,
196    ReadySection::Sessions,
197];
198
199/// What a client already holds of the Ready catalog, sent with `authenticate`.
200#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
201pub struct CatalogHint {
202    /// Sections the client holds, by the revision the gateway reported.
203    #[serde(default, skip_serializing_if = "BTreeMap::is_empty")]
204    pub known: BTreeMap<ReadySection, String>,
205    /// Sections this connection never needs, such as on a file-transfer connection.
206    #[serde(default, skip_serializing_if = "BTreeSet::is_empty")]
207    pub skip: BTreeSet<ReadySection>,
208}
209
210impl CatalogHint {
211    /// A hint that names no section.
212    #[must_use]
213    pub fn is_empty(&self) -> bool {
214        self.known.is_empty() && self.skip.is_empty()
215    }
216}
217
218/// One position of the session catalog in a [`ServerMessage::SessionsChanged`].
219#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
220#[serde(untagged)]
221pub enum SessionSlot {
222    /// A session unchanged since the catalog this connection last received, by ID.
223    Unchanged(String),
224    /// A new or changed session.
225    Changed(Box<SessionRecord>),
226}
227
228/// Rebuilds the catalog `sessions` held from a [`ServerMessage::SessionsChanged`].
229pub fn apply_session_changes(sessions: &mut Vec<SessionRecord>, changes: Vec<SessionSlot>) {
230    let mut held: Vec<_> = std::mem::take(sessions).into_iter().map(Some).collect();
231    *sessions = changes
232        .into_iter()
233        .filter_map(|slot| match slot {
234            SessionSlot::Changed(session) => Some(*session),
235            SessionSlot::Unchanged(id) => held
236                .iter_mut()
237                .find(|held| held.as_ref().is_some_and(|held| held.session_id == id))
238                .and_then(Option::take),
239        })
240        .collect();
241}
242
243/// `+added −removed` lines of a Git diff, as Git's `--numstat` counts them.
244#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Serialize, Deserialize)]
245pub struct DiffTotals {
246    /// Added lines.
247    pub additions: u64,
248    /// Removed lines.
249    pub deletions: u64,
250}
251
252/// One hidden Bot conversation currently waiting for a human execution decision.
253#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
254pub struct BackgroundApproval {
255    /// The session identifier.
256    pub session_id: String,
257    /// The bot identifier.
258    pub bot_id: String,
259    /// The turn identifier.
260    pub turn_id: String,
261    /// The request identifier.
262    pub request_id: String,
263}
264
265/// Frontend-safe state for one opened session.
266#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
267pub struct SessionReadyPayload {
268    /// The active turn identifiers.
269    pub active_turn_ids: Vec<String>,
270    /// The pending approvals.
271    pub pending_approvals: Vec<mobius::protocol::ExecApprovalRequestEvent>,
272    /// The latest sequence.
273    pub latest_sequence: u64,
274    /// The next before sequence.
275    pub next_before_sequence: Option<u64>,
276    /// The workspace.
277    pub workspace: Option<WorkspaceInfo>,
278    /// The attached folders.
279    pub attached_folders: Vec<PathBuf>,
280    /// The git.
281    pub git: Option<GitStatus>,
282    /// The session.
283    pub session: SessionConfiguredEvent,
284    /// The contributions.
285    pub contributions: Vec<FrontendContribution>,
286    /// The widgets.
287    pub widgets: Vec<SessionWidget>,
288    /// The tool count.
289    pub tool_count: usize,
290    /// The compaction count.
291    pub compaction_count: u64,
292    /// The context limit tokens.
293    pub context_limit_tokens: Option<i64>,
294    /// The active message delivery.
295    pub active_message_delivery: mobius::protocol::ActiveMessageDelivery,
296    /// The run stats.
297    pub run_stats: RunStats,
298}
299
300/// One currently mounted capability widget and its owning namespace.
301#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
302pub struct SessionWidget {
303    /// The capability.
304    pub capability: String,
305    /// The item.
306    pub item: FrontendWidget,
307}
308
309/// One visible session with gateway-owned catalog presentation metadata.
310#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
311pub struct SessionRecord {
312    /// The session identifier.
313    pub session_id: String,
314    /// The session context.
315    pub session_context: mobius::protocol::SessionContext,
316    /// The parent session identifier.
317    pub parent_session_id: Option<String>,
318    /// The parent sequence.
319    pub parent_sequence: Option<u64>,
320    /// The sequence.
321    pub sequence: u64,
322    /// The first user message.
323    pub first_user_message: Option<String>,
324    /// The execution stats.
325    pub execution_stats: mobius::backend::checkpoint::ExecutionStats,
326    /// The title.
327    pub title: Option<String>,
328    /// The pinned.
329    pub pinned: bool,
330    /// The activity.
331    pub activity: SessionActivity,
332    /// The created at.
333    pub created_at: i64,
334    /// The updated at.
335    pub updated_at: i64,
336}
337
338/// Gateway-observed lifecycle state for one session.
339#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
340#[serde(deny_unknown_fields)]
341pub struct SessionActivity {
342    /// Pending capability attention items, independent of turn activity.
343    pub attention: u32,
344    /// The state.
345    pub state: SessionActivityState,
346    /// The turn identifier.
347    pub turn_id: Option<String>,
348    /// The approval request identifier.
349    pub approval_request_id: Option<String>,
350    /// The started at.
351    pub started_at: Option<i64>,
352    /// The last outcome.
353    pub last_outcome: Option<mobius::backend::checkpoint::ExecutionOutcome>,
354    /// The message.
355    pub message: Option<String>,
356}
357
358impl Default for SessionActivity {
359    fn default() -> Self {
360        Self {
361            attention: 0,
362            state: SessionActivityState::Idle,
363            turn_id: None,
364            approval_request_id: None,
365            started_at: None,
366            last_outcome: None,
367            message: None,
368        }
369    }
370}
371
372/// Current work state advertised in the session catalog.
373#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
374#[serde(rename_all = "snake_case")]
375pub enum SessionActivityState {
376    /// Selects the idle case.
377    Idle,
378    /// Selects the running case.
379    Running,
380    /// Selects the awaiting approval case.
381    AwaitingApproval,
382}
383
384/// Canonical workspace identity and path for one chat.
385#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
386pub struct WorkspaceInfo {
387    /// The identifier.
388    pub id: String,
389    /// The path.
390    pub path: PathBuf,
391}
392
393/// Local branch state for a Git-backed workspace.
394#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
395pub struct GitStatus {
396    /// The current branch.
397    pub current_branch: String,
398    /// The branches.
399    pub branches: Vec<String>,
400}
401
402/// Public metadata for one SSH identity found on the gateway host.
403#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
404#[serde(deny_unknown_fields)]
405pub struct SshIdentityRecord {
406    /// The label.
407    pub label: String,
408    /// The algorithm.
409    pub algorithm: String,
410    /// The fingerprint.
411    pub fingerprint: String,
412}
413
414/// One explicit Git patch selection.
415#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
416#[serde(rename_all = "snake_case")]
417pub enum GitDiffScope {
418    /// Selects the staged case.
419    Staged,
420    /// Selects the unstaged case.
421    Unstaged,
422    /// Selects the committed case.
423    Committed,
424}
425
426/// Which openable files to include in a workspace catalog.
427#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
428#[serde(rename_all = "snake_case")]
429pub enum WorkspaceFileScope {
430    /// Selects the modified case.
431    Modified,
432    /// Selects the all case.
433    All,
434}
435
436/// One regular file confined to the selected chat workspace.
437#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
438pub struct WorkspaceFileRecord {
439    /// The path.
440    pub path: String,
441    /// The size.
442    pub size: u64,
443}
444
445/// One bounded folder listing from the gateway host.
446#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
447pub struct DirectoryListing {
448    /// The path.
449    pub path: PathBuf,
450    /// The parent.
451    pub parent: Option<PathBuf>,
452    /// The entries.
453    pub entries: Vec<DirectoryEntry>,
454}
455
456/// A selectable child folder on the gateway host.
457#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
458pub struct DirectoryEntry {
459    /// The name.
460    pub name: String,
461    /// The path.
462    pub path: PathBuf,
463    /// The is directory.
464    pub is_directory: bool,
465}
466
467/// A frontend-safe agent composition guarded by an optimistic revision.
468#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
469pub struct VersionedAgentConfig {
470    /// The revision.
471    pub revision: u64,
472    /// The config.
473    pub config: AgentComposition,
474}
475
476/// Runtime settings an authenticated client may read and replace atomically.
477#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
478#[serde(deny_unknown_fields)]
479pub struct AgentComposition {
480    /// The provider.
481    pub provider: ProviderConfig,
482    /// The realtime voice.
483    pub realtime_voice: Option<String>,
484    /// The middleware.
485    pub middleware: MiddlewareConfig,
486    /// The extensions.
487    pub extensions: BTreeSet<String>,
488    /// The system prompt.
489    pub system_prompt: String,
490    /// The max model steps.
491    pub max_model_steps: u64,
492}
493
494/// Package format of one gateway-managed extension.
495#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
496#[serde(rename_all = "snake_case")]
497pub enum ExtensionKind {
498    /// Selects the skill case.
499    Skill,
500    /// Selects the plugin case.
501    Plugin,
502}
503
504/// One executable plugin hook shown before digest-bound trust is granted.
505#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
506#[serde(deny_unknown_fields)]
507pub struct ExtensionHookRecord {
508    /// The event.
509    pub event: String,
510    /// The matcher.
511    pub matcher: Option<String>,
512    /// The command.
513    pub command: String,
514    /// The timeout seconds.
515    pub timeout_seconds: u64,
516}
517
518/// Frontend-safe metadata for one installed extension snapshot.
519#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
520#[serde(deny_unknown_fields)]
521pub struct ExtensionRecord {
522    /// The identifier.
523    pub id: String,
524    /// The capability.
525    pub capability: String,
526    /// The kind.
527    pub kind: ExtensionKind,
528    /// The name.
529    pub name: String,
530    /// The description.
531    pub description: String,
532    /// The version.
533    pub version: Option<String>,
534    /// The source.
535    pub source: String,
536    /// The reference.
537    pub reference: Option<String>,
538    /// The subdirectory.
539    pub subdirectory: Option<String>,
540    /// The resolved revision.
541    pub resolved_revision: String,
542    /// The digest.
543    pub digest: String,
544    /// The skills.
545    pub skills: Vec<String>,
546    /// The hooks.
547    pub hooks: Vec<ExtensionHookRecord>,
548    /// The hooks trusted.
549    pub hooks_trusted: bool,
550}
551
552/// Provider and model settings. Credentials are resolved only on the gateway host.
553#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
554#[serde(deny_unknown_fields)]
555pub struct ProviderConfig {
556    /// Stable identity of one configured setup of `provider`. A gateway may hold
557    /// several instances of the same provider with separate credentials.
558    pub instance: String,
559    /// The provider.
560    pub provider: String,
561    /// The model.
562    #[serde(default)]
563    pub model: String,
564    #[serde(default, skip_serializing_if = "Option::is_none")]
565    /// The base URL.
566    pub base_url: Option<String>,
567    /// The endpoint auth.
568    pub endpoint_auth: ProviderEndpointAuth,
569    #[serde(default, skip_serializing_if = "Option::is_none")]
570    /// The reasoning effort.
571    pub reasoning_effort: Option<String>,
572    #[serde(default, skip_serializing_if = "Option::is_none")]
573    /// Optional native Responses processing tier.
574    pub service_tier: Option<String>,
575    /// Provider-level tool discovery override; omission uses the provider default.
576    #[serde(default, skip_serializing_if = "Option::is_none")]
577    pub tool_discovery: Option<ToolDiscoveryMode>,
578    /// The web search.
579    pub web_search: HostedWebSearch,
580}
581
582/// Authentication applied when calling one configured provider endpoint.
583#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
584#[serde(rename_all = "snake_case")]
585pub enum ProviderEndpointAuth {
586    /// Selects the provider default case.
587    ProviderDefault,
588    /// Selects the credentialless case.
589    Credentialless,
590}
591
592/// Credential availability exposed without returning credential material.
593#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
594pub struct ProviderStatus {
595    /// The provider.
596    pub provider: String,
597    /// The label.
598    pub label: String,
599    /// The symbol.
600    pub symbol: FrontendSymbol,
601    /// The description.
602    pub description: String,
603    /// The model identifiers configurable.
604    pub model_ids_configurable: bool,
605    /// The auth.
606    pub auth: ProviderAuthKind,
607    /// The default base URL.
608    pub default_base_url: Option<String>,
609    /// Always true: custom roots keep every native capability. Kept for older clients.
610    pub native_custom_endpoints: bool,
611    /// The default API key env.
612    pub default_api_key_env: Option<String>,
613    /// The models.
614    pub models: Vec<ProviderModel>,
615    /// Image models the provider advertises; empty when setups list their own.
616    #[serde(default)]
617    pub image_models: Cow<'static, [MediaModelPreset]>,
618    /// Whether setups list their own image model identifiers.
619    #[serde(default)]
620    pub image_model_ids_configurable: bool,
621    /// Realtime voice models the provider advertises, default first.
622    #[serde(default)]
623    pub voice_models: Cow<'static, [MediaModelPreset]>,
624    /// The web search.
625    pub web_search: Vec<FrontendSettingOption>,
626    /// The tool discovery.
627    pub tool_discovery: ToolDiscoveryMode,
628    /// Discovery modes implemented by the provider adapter.
629    pub supported_tool_discovery: Vec<ToolDiscoveryMode>,
630    /// The custom endpoint tool discovery.
631    pub custom_endpoint_tool_discovery: Option<ToolDiscoveryMode>,
632    /// The realtime voices.
633    pub realtime_voices: Vec<String>,
634}
635
636/// User-chosen accent for distinguishing provider instances in model selectors.
637#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Serialize, Deserialize)]
638#[serde(rename_all = "snake_case")]
639pub enum ProviderTint {
640    #[default]
641    /// Selects the blue case.
642    Blue,
643    /// Selects the teal case.
644    Teal,
645    /// Selects the green case.
646    Green,
647    /// Selects the yellow case.
648    Yellow,
649    /// Selects the orange case.
650    Orange,
651    /// Selects the red case.
652    Red,
653    /// Selects the purple case.
654    Purple,
655    /// Selects the white case.
656    White,
657}
658
659/// The silhouette of a Bot's face.
660#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
661#[serde(rename_all = "snake_case")]
662pub enum BotShape {
663    /// Selects the circle case.
664    Circle,
665    /// Selects the squircle case.
666    Squircle,
667    /// Selects the triangle case.
668    Triangle,
669    /// Selects the diamond case.
670    Diamond,
671    /// Selects the hexagon case.
672    Hexagon,
673    /// Selects the star case.
674    Star,
675    /// Selects the flower case.
676    Flower,
677}
678
679impl BotShape {
680    /// Every shape, in the order pickers show them.
681    pub const ALL: [Self; 7] = [
682        Self::Circle,
683        Self::Squircle,
684        Self::Triangle,
685        Self::Diamond,
686        Self::Hexagon,
687        Self::Star,
688        Self::Flower,
689    ];
690}
691
692/// One durable setup of a provider. Several may share one `provider`.
693#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
694pub struct ProviderInstance {
695    /// The label.
696    pub label: String,
697    /// The tint.
698    pub tint: ProviderTint,
699    /// The configured.
700    pub configured: bool,
701    #[serde(default, skip_serializing_if = "Option::is_none")]
702    /// The credential hint.
703    pub credential_hint: Option<String>,
704    /// The selection.
705    pub selection: ProviderConfig,
706    /// Configured model overrides; built-in catalogs remain in [`ProviderStatus::models`].
707    pub models: Vec<ProviderModel>,
708    /// Image model identifiers listed by this setup.
709    #[serde(default, skip_serializing_if = "Vec::is_empty")]
710    pub image_model_ids: Vec<String>,
711}
712
713/// Frontend type attached to one authenticated connection.
714#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
715#[serde(rename_all = "snake_case")]
716pub enum ClientKind {
717    /// Selects the CLI case.
718    Cli,
719    /// Selects the macos case.
720    Macos,
721    /// Selects the ios case.
722    Ios,
723    /// Selects the ipados case.
724    Ipados,
725    /// Selects the gateway dashboard case.
726    GatewayDashboard,
727}
728
729/// One paired client and its current connection state.
730#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
731pub struct ClientStatus {
732    /// The client identifier.
733    pub client_id: String,
734    /// The label.
735    pub label: String,
736    /// The kinds.
737    pub kinds: Vec<ClientKind>,
738    /// The connections.
739    pub connections: usize,
740}
741
742impl ProviderStatus {
743    #[must_use]
744    /// Returns the configurable base URL.
745    pub fn configurable_base_url(&self) -> bool {
746        self.default_base_url.is_some()
747    }
748
749    #[must_use]
750    /// Returns the default model.
751    pub fn default_model(&self) -> Option<&ProviderModel> {
752        self.models.first()
753    }
754}
755
756/// Model edits; optional metadata is accepted only from the gateway operator.
757#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
758#[serde(deny_unknown_fields)]
759pub struct ConfiguredModel {
760    /// Provider model identifier.
761    pub id: String,
762    /// Omit to retain choices; an empty list explicitly clears them.
763    #[serde(default, skip_serializing_if = "Option::is_none")]
764    pub reasoning_efforts: Option<Vec<String>>,
765    /// Required member of a nonempty reasoning list; absent when the list is empty.
766    #[serde(default, skip_serializing_if = "Option::is_none")]
767    pub default_reasoning: Option<String>,
768    /// Operator-only display label; omission preserves the stored value.
769    #[serde(default, skip_serializing_if = "Option::is_none")]
770    pub label: Option<String>,
771    /// Operator-only description; omission preserves the stored value.
772    #[serde(default, skip_serializing_if = "Option::is_none")]
773    pub description: Option<String>,
774    /// Operator-only context window in tokens; omission preserves the stored value.
775    #[serde(default, skip_serializing_if = "Option::is_none")]
776    pub context_window: Option<i64>,
777}
778
779/// Frontend-safe provider authentication mechanism.
780#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
781#[serde(rename_all = "snake_case")]
782pub enum ProviderAuthKind {
783    /// Selects the API key case.
784    ApiKey,
785    /// Selects the device code case.
786    DeviceCode,
787}
788
789/// Enabled optional middleware IDs and their schema-backed settings.
790#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
791#[serde(deny_unknown_fields)]
792pub struct MiddlewareConfig {
793    pub(crate) enabled: BTreeSet<String>,
794    /// The settings.
795    pub settings: BTreeMap<String, BTreeMap<String, FrontendSettingValue>>,
796}
797
798impl MiddlewareConfig {
799    /// Returns the selected policy that excludes an optional capability.
800    #[must_use]
801    pub fn disabled_by<'a>(
802        &self,
803        features: &'a [mobius::protocol::MiddlewareFeature],
804        id: &str,
805        selected_model: Option<&mobius::protocol::ModelChoice>,
806    ) -> Option<&'a str> {
807        if let Some(capability) = features
808            .iter()
809            .find(|feature| feature.id == id)
810            .and_then(|feature| feature.required_model_capability)
811            && selected_model.is_some_and(|model| !model.supports(capability))
812        {
813            return Some(match capability {
814                mobius::protocol::ModelCapability::ImageGeneration => {
815                    "a model without image generation"
816                }
817                mobius::protocol::ModelCapability::RealtimeVoice => {
818                    "a model without realtime voice"
819                }
820            });
821        }
822        features
823            .iter()
824            .filter(|feature| feature.required || self.enabled(&feature.id))
825            .find_map(|feature| {
826                feature.settings.iter().find_map(|setting| {
827                    let mobius::protocol::FrontendSettingKind::Select { options, .. } =
828                        &setting.kind
829                    else {
830                        return None;
831                    };
832                    let Some(FrontendSettingValue::String(value)) =
833                        self.setting(&feature.id, &setting.id)
834                    else {
835                        return None;
836                    };
837                    options
838                        .iter()
839                        .find(|option| {
840                            option.value == *value
841                                && option.disables.iter().any(|disabled| disabled == id)
842                        })
843                        .map(|option| option.label.as_str())
844                })
845            })
846    }
847
848    /// Applies exclusions advertised by the currently selected policies.
849    pub fn reconcile(
850        &mut self,
851        features: &[mobius::protocol::MiddlewareFeature],
852        selected_model: Option<&mobius::protocol::ModelChoice>,
853    ) {
854        let excluded = self
855            .enabled
856            .iter()
857            .filter(|id| self.disabled_by(features, id, selected_model).is_some())
858            // Evaluate all exclusions against the unchanged enabled set before removing any selected IDs.
859            .cloned()
860            .collect::<Vec<_>>();
861        for id in excluded {
862            self.enabled.remove(&id);
863        }
864    }
865
866    /// Returns whether one advertised optional middleware is enabled.
867    #[must_use]
868    pub fn enabled(&self, id: &str) -> bool {
869        self.enabled.contains(id)
870    }
871
872    /// Updates one advertised optional middleware before gateway validation.
873    pub fn set_enabled(&mut self, id: impl Into<String>, enabled: bool) {
874        let id = id.into();
875        if enabled {
876            self.enabled.insert(id);
877        } else {
878            self.enabled.remove(&id);
879        }
880    }
881
882    /// Returns one advertised middleware setting.
883    #[must_use]
884    pub fn setting(&self, middleware: &str, setting: &str) -> Option<&FrontendSettingValue> {
885        self.settings.get(middleware)?.get(setting)
886    }
887
888    /// Sets or clears one advertised middleware setting before gateway validation.
889    pub fn set_setting(
890        &mut self,
891        middleware: impl Into<String>,
892        setting: impl Into<String>,
893        value: Option<FrontendSettingValue>,
894    ) {
895        let middleware = middleware.into();
896        let setting = setting.into();
897        if let Some(value) = value {
898            self.settings
899                .entry(middleware)
900                .or_default()
901                .insert(setting, value);
902        } else if let Some(settings) = self.settings.get_mut(&middleware) {
903            settings.remove(&setting);
904            if settings.is_empty() {
905                self.settings.remove(&middleware);
906            }
907        }
908    }
909
910    pub(crate) fn entries(&self) -> impl Iterator<Item = &str> {
911        self.enabled.iter().map(String::as_str)
912    }
913}
914
915/// Capability-rendered preview whose inner events remain provider-neutral.
916#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
917pub struct RenderedPreview {
918    /// The semantic preview symbol.
919    pub symbol: Option<FrontendSymbol>,
920    /// Accumulated completed duration in milliseconds.
921    pub duration_ms: Option<u64>,
922    /// Start of the current activity, when still running.
923    pub started_at_ms: Option<i64>,
924    /// The identifier.
925    pub id: String,
926    /// The title.
927    pub title: String,
928    /// The subtitle.
929    pub subtitle: String,
930    /// The page identifier.
931    pub page_id: String,
932    /// The update.
933    pub update: FrontendPreviewUpdate,
934    /// The events.
935    pub events: Vec<RenderedEvent>,
936    /// The next.
937    pub next: Option<Op>,
938}
939
940/// One preview event and its capability-rendered blocks.
941#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
942pub struct RenderedEvent {
943    /// The submission identifier.
944    pub submission_id: Option<Arc<str>>,
945    /// The recorded at milliseconds.
946    pub recorded_at_ms: i64,
947    /// The event.
948    pub event: EventMsg,
949    /// The blocks.
950    pub blocks: Vec<RenderedBlock>,
951}
952
953/// One timestamped semantic event and its deterministic presentation.
954#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
955pub struct RecordedEvent {
956    /// The sequence.
957    pub sequence: u64,
958    /// The recorded at milliseconds.
959    pub recorded_at_ms: i64,
960    /// The event.
961    pub event: Event,
962    /// The stream metrics.
963    pub stream_metrics: Vec<StreamMetrics>,
964    /// The blocks.
965    pub blocks: Vec<RenderedBlock>,
966    /// The preview.
967    pub preview: Option<RenderedPreview>,
968}
969
970/// Gateway-owned profile and aggregate usage information.
971#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
972pub struct ProfileSnapshot {
973    /// The user name.
974    pub user_name: Option<String>,
975    /// The daily usage.
976    pub daily_usage: Vec<DailyUsage>,
977    /// The provider usage.
978    pub provider_usage: Vec<ProviderUsage>,
979    /// The run stats.
980    pub run_stats: RunStats,
981    /// The recent run groups.
982    pub recent_run_groups: Vec<SessionRunGroup>,
983}
984
985/// One configured provider's remote subscription usage.
986#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
987pub struct ProviderUsage {
988    /// The provider.
989    pub provider: String,
990    /// The limits.
991    pub limits: Option<Vec<UsageLimit>>,
992    /// The error.
993    pub error: Option<String>,
994}
995
996/// Recent executions grouped under their nearest visible session.
997#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
998pub struct SessionRunGroup {
999    /// The session identifier.
1000    pub session_id: String,
1001    /// The title.
1002    pub title: String,
1003    /// The runs.
1004    pub runs: Vec<RunSummary>,
1005}
1006
1007/// Completed execution totals plus the active run, when one exists.
1008#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
1009pub struct RunStats {
1010    #[serde(flatten)]
1011    /// The completed.
1012    pub completed: mobius::backend::checkpoint::ExecutionStats,
1013    /// The active.
1014    pub active: Option<RunSummary>,
1015}
1016
1017/// Frontend-safe summary of one completed or active user turn.
1018#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
1019pub struct RunSummary {
1020    /// The session identifier.
1021    pub session_id: String,
1022    /// The submission identifier.
1023    pub submission_id: String,
1024    /// The turn identifier.
1025    pub turn_id: String,
1026    /// The started at milliseconds.
1027    pub started_at_ms: i64,
1028    /// The finished at milliseconds.
1029    pub finished_at_ms: Option<i64>,
1030    /// The elapsed milliseconds.
1031    pub elapsed_ms: u64,
1032    /// The outcome.
1033    pub outcome: Option<mobius::backend::checkpoint::ExecutionOutcome>,
1034    /// The model calls.
1035    pub model_calls: u64,
1036    /// The tool calls.
1037    pub tool_calls: u64,
1038    /// The failed tool calls.
1039    pub failed_tool_calls: u64,
1040    /// The usage.
1041    pub usage: TokenUsage,
1042}
1043
1044/// Usage accrued during one Unix day.
1045#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
1046pub struct DailyUsage {
1047    /// The unix day.
1048    pub unix_day: u64,
1049    /// The provider.
1050    pub provider: String,
1051    /// The usage.
1052    pub usage: TokenUsage,
1053}
1054
1055/// One durable Bot profile.
1056#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
1057pub struct BotRecord {
1058    /// Canonical project-free conversation, opened through the ordinary session API.
1059    pub conversation_session_id: String,
1060    /// The identifier.
1061    pub id: String,
1062    /// The handle.
1063    pub handle: String,
1064    /// The name.
1065    pub name: String,
1066    /// The description.
1067    pub description: String,
1068    /// The tint.
1069    pub tint: ProviderTint,
1070    /// The face's silhouette.
1071    pub shape: BotShape,
1072    /// The config.
1073    pub config: VersionedAgentConfig,
1074    /// The accepts file attachments.
1075    pub accepts_file_attachments: bool,
1076    /// The routine interaction policy.
1077    pub routine_interaction_policy: RoutineInteractionPolicy,
1078}
1079
1080/// Whether unattended routine work can stop for a human execution decision.
1081#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
1082#[serde(rename_all = "snake_case")]
1083pub enum RoutineInteractionPolicy {
1084    /// Selects the unattended case.
1085    Unattended,
1086    /// Selects the may pause for approval case.
1087    MayPauseForApproval,
1088}
1089
1090/// One Bot-owned routine.
1091#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
1092pub struct Routine {
1093    /// The identifier.
1094    pub id: String,
1095    /// The bot identifier.
1096    pub bot_id: String,
1097    /// The workspace.
1098    pub workspace: PathBuf,
1099    /// The instructions.
1100    pub instructions: String,
1101    /// User-authored event and timer bindings.
1102    pub bindings: Vec<RoutineBinding>,
1103    /// The enabled.
1104    pub enabled: bool,
1105    /// The finished.
1106    pub finished: bool,
1107    /// The next run at.
1108    pub next_run_at: Option<i64>,
1109}
1110
1111/// A user-selected scheduling rule.
1112#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
1113#[serde(deny_unknown_fields)]
1114pub struct RoutineSchedule {
1115    /// The kind.
1116    pub kind: RoutineScheduleKind,
1117    /// The at.
1118    pub at: Option<i64>,
1119    /// The every seconds.
1120    pub every_seconds: Option<u64>,
1121    /// The expression.
1122    pub expression: Option<String>,
1123    /// The time zone.
1124    pub time_zone: Option<String>,
1125}
1126
1127/// The supported scheduling rule families.
1128#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
1129#[serde(rename_all = "snake_case")]
1130pub enum RoutineScheduleKind {
1131    /// Selects the once case.
1132    Once,
1133    /// Selects the interval case.
1134    Interval,
1135    /// Selects the cron case.
1136    Cron,
1137}
1138
1139/// A read-only page of a Bot routine transcript.
1140#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
1141pub struct RoutineRunPreview {
1142    /// The routine.
1143    pub routine: Routine,
1144    /// The run.
1145    pub run: RoutineRun,
1146    /// The records.
1147    pub records: Vec<RecordedEvent>,
1148    /// The next before sequence.
1149    pub next_before_sequence: Option<u64>,
1150}
1151
1152/// One completed or active invocation of a Bot routine.
1153#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
1154pub struct RoutineRun {
1155    /// The identifier.
1156    pub id: String,
1157    /// The routine identifier.
1158    pub routine_id: String,
1159    /// The bot identifier.
1160    pub bot_id: String,
1161    /// The started at.
1162    pub started_at: i64,
1163    /// The finished at.
1164    pub finished_at: Option<i64>,
1165    /// The status.
1166    pub status: RoutineRunStatus,
1167    /// The session identifier.
1168    pub session_id: Option<String>,
1169    /// The message.
1170    pub message: Option<String>,
1171}
1172
1173/// Durable state of one routine invocation.
1174#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
1175#[serde(rename_all = "snake_case")]
1176pub enum RoutineRunStatus {
1177    /// Selects the running case.
1178    Running,
1179    /// Selects the succeeded case.
1180    Succeeded,
1181    /// Selects the failed case.
1182    Failed,
1183    /// Selects the skipped case.
1184    Skipped,
1185    /// The user stopped the invocation.
1186    Cancelled,
1187}
1188
1189/// Editable routine content and its user-authorized bindings.
1190#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
1191#[serde(deny_unknown_fields)]
1192pub struct RoutineDefinition {
1193    /// Explicit execution workspace.
1194    pub workspace: PathBuf,
1195    /// Saved task instructions.
1196    pub instructions: String,
1197    /// Event and timer triggers with exact actions.
1198    pub bindings: Vec<RoutineBinding>,
1199}
1200
1201/// One user-authored trigger on its containing routine.
1202#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
1203#[serde(deny_unknown_fields)]
1204pub struct HookBinding<A> {
1205    /// Stable binding identity within the routine.
1206    pub id: String,
1207    /// Exact event or timer trigger.
1208    pub on: HookSelector,
1209    /// Action on this routine.
1210    pub action: A,
1211}
1212
1213/// A trigger whose action addresses its containing routine.
1214pub type RoutineBinding = HookBinding<RoutineAction>;
1215
1216/// Typed actions shared by reporting and control subscriptions.
1217#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
1218#[serde(tag = "type", rename_all = "snake_case", deny_unknown_fields)]
1219pub enum BotAction {
1220    /// Deliver the saved reporting mandate to the Bot conversation.
1221    Report {
1222        /// User-authored mandate.
1223        instruction: String,
1224    },
1225    /// Invoke the ordinary routine command handler.
1226    Routine {
1227        /// Addressed command.
1228        command: RoutineCommand,
1229    },
1230    /// Submit an ordinary message or interrupt to an owned session.
1231    Session {
1232        /// Owned destination.
1233        session_id: String,
1234        /// Existing core operation.
1235        op: Box<mobius::protocol::Op>,
1236    },
1237}
1238
1239/// Finite commands shared by UI, tools, timers and event bindings.
1240#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
1241#[serde(tag = "type", rename_all = "snake_case", deny_unknown_fields)]
1242pub enum RoutineAction {
1243    /// Start one invocation.
1244    Start,
1245    /// Stop only the identified invocation.
1246    Stop {
1247        /// Invocation identity.
1248        run_id: String,
1249    },
1250    /// Disable future starts without stopping an active run.
1251    Pause,
1252    /// Enable future starts from the current time.
1253    Resume,
1254    /// Delete this routine through the ordinary cleanup path.
1255    Delete,
1256    /// Replace the editable definition.
1257    Update {
1258        /// Validated replacement.
1259        definition: RoutineDefinition,
1260    },
1261}
1262
1263/// A command addressed to one routine.
1264#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
1265#[serde(deny_unknown_fields)]
1266pub struct RoutineCommand {
1267    /// Target routine identity.
1268    pub routine_id: String,
1269    /// Typed action.
1270    pub action: RoutineAction,
1271}
1272
1273/// Gateway-established origin of a committed hook event.
1274#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
1275#[serde(tag = "type", rename_all = "snake_case", deny_unknown_fields)]
1276pub enum HookSource {
1277    /// A Bot command.
1278    Bot {
1279        /// Owner identity.
1280        bot_id: String,
1281    },
1282    /// An owned routine.
1283    Routine {
1284        /// Routine identity.
1285        routine_id: String,
1286    },
1287    /// An owned session.
1288    Session {
1289        /// Session identity.
1290        session_id: String,
1291    },
1292    /// A timer on an owned routine.
1293    Schedule {
1294        /// Routine identity.
1295        routine_id: String,
1296        /// Binding identity.
1297        binding_id: String,
1298    },
1299    /// A gateway lifecycle operation.
1300    Gateway,
1301    /// An authenticated client.
1302    Client {
1303        /// Paired client identity.
1304        client_id: String,
1305    },
1306}
1307
1308/// Exact trigger shared by routine bindings and reporting consumers.
1309#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
1310#[serde(tag = "type", rename_all = "snake_case", deny_unknown_fields)]
1311pub enum HookSelector {
1312    /// The existing schedule model, owned by this binding.
1313    Schedule {
1314        /// Timer rule.
1315        schedule: RoutineSchedule,
1316        /// Optional cutoff.
1317        ends_at: Option<i64>,
1318    },
1319    /// An exact source and typed boundary, with applicable outcome filters.
1320    Event {
1321        /// Source identity.
1322        source: HookSource,
1323        /// Boundary.
1324        kind: HookKind,
1325        /// Optional terminal routine outcome.
1326        routine_outcome: Option<RoutineRunStatus>,
1327        /// Optional terminal session outcome.
1328        session_outcome: Option<mobius::backend::checkpoint::ExecutionOutcome>,
1329        /// Optional custom event name.
1330        custom_name: Option<String>,
1331    },
1332}
1333
1334/// Finite lifecycle boundaries; source payloads cannot introduce commands.
1335#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
1336#[serde(rename_all = "snake_case")]
1337pub enum HookKind {
1338    /// Routine registered.
1339    RoutineCreated,
1340    /// Routine definition replaced.
1341    RoutineUpdated,
1342    /// Routine disabled.
1343    RoutinePaused,
1344    /// Routine enabled.
1345    RoutineResumed,
1346    /// Routine removed.
1347    RoutineDeleted,
1348    /// Invocation reserved.
1349    RunStarted,
1350    /// Invocation completed, failed or was cancelled.
1351    RunFinished,
1352    /// Invocation skipped before starting.
1353    RunSkipped,
1354    /// A binding's timer became due.
1355    ScheduleDue,
1356    /// Session created.
1357    SessionCreated,
1358    /// A turn began.
1359    SessionTurnStarted,
1360    /// A turn reached a durable outcome.
1361    SessionTurnFinished,
1362    /// A turn awaits a human decision.
1363    SessionApproval,
1364    /// A capability requests a new user decision.
1365    SessionAttention,
1366    /// Session deleted.
1367    SessionDeleted,
1368    /// Session owner changed.
1369    SessionOwnerChanged,
1370    /// Client authenticated.
1371    ClientConnected,
1372    /// Client disconnected.
1373    ClientDisconnected,
1374    /// A Bot emitted a named event.
1375    CustomReceived,
1376}
1377
1378/// One durable gateway event used by all hook consumers.
1379#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
1380#[serde(deny_unknown_fields)]
1381pub struct HookEvent {
1382    /// Stable event identity.
1383    pub id: String,
1384    /// Authenticated or internally established source.
1385    pub source: HookSource,
1386    /// Immediate causing event, if any.
1387    pub cause_id: Option<String>,
1388    /// Bounded prior event chain used to prevent feedback.
1389    pub ancestry: Vec<String>,
1390    /// Owning Bot.
1391    pub bot_id: String,
1392    /// Unix timestamp in seconds.
1393    pub occurred_at: i64,
1394    /// Small typed lifecycle fact.
1395    pub data: HookData,
1396}
1397
1398/// Typed event facts, without executable source-provided instructions.
1399#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
1400#[serde(tag = "type", rename_all = "snake_case", deny_unknown_fields)]
1401pub enum HookData {
1402    /// Routine registered.
1403    RoutineCreated {
1404        /// Routine identity.
1405        routine_id: String,
1406    },
1407    /// Routine definition changed.
1408    RoutineUpdated {
1409        /// Routine identity.
1410        routine_id: String,
1411    },
1412    /// Routine disabled.
1413    RoutinePaused {
1414        /// Routine identity.
1415        routine_id: String,
1416    },
1417    /// Routine enabled.
1418    RoutineResumed {
1419        /// Routine identity.
1420        routine_id: String,
1421    },
1422    /// Routine removed.
1423    RoutineDeleted {
1424        /// Routine identity.
1425        routine_id: String,
1426    },
1427    /// Invocation reserved.
1428    RunStarted {
1429        /// Routine identity.
1430        routine_id: String,
1431        /// Invocation identity.
1432        run_id: String,
1433        /// Execution transcript.
1434        session_id: Option<String>,
1435    },
1436    /// Invocation completed, failed or was cancelled.
1437    RunFinished {
1438        /// Routine identity.
1439        routine_id: String,
1440        /// Invocation identity.
1441        run_id: String,
1442        /// Terminal outcome.
1443        status: RoutineRunStatus,
1444        /// Execution transcript.
1445        session_id: Option<String>,
1446        /// Failure or cancellation reason.
1447        reason: Option<String>,
1448    },
1449    /// An overlapping or unavailable invocation was skipped.
1450    RunSkipped {
1451        /// Routine identity.
1452        routine_id: String,
1453        /// Skipped invocation identity.
1454        run_id: String,
1455        /// Saved reason.
1456        reason: String,
1457    },
1458    /// A binding's timer became due.
1459    ScheduleDue {
1460        /// Binding identity.
1461        binding_id: String,
1462    },
1463    /// A session was created.
1464    SessionCreated {
1465        /// Session identity.
1466        session_id: String,
1467    },
1468    /// A session turn began.
1469    SessionTurnStarted {
1470        /// Session identity.
1471        session_id: String,
1472        /// Turn identity.
1473        turn_id: String,
1474    },
1475    /// A session turn reached its committed outcome.
1476    SessionTurnFinished {
1477        /// Session identity.
1478        session_id: String,
1479        /// Turn identity.
1480        turn_id: String,
1481        /// Core execution outcome.
1482        outcome: mobius::backend::checkpoint::ExecutionOutcome,
1483    },
1484    /// A session awaits an execution decision.
1485    SessionApproval {
1486        /// Session identity.
1487        session_id: String,
1488        /// Turn identity.
1489        turn_id: String,
1490        /// Approval identity.
1491        request_id: String,
1492    },
1493    /// A capability requests a user decision.
1494    SessionAttention {
1495        /// Session identity.
1496        session_id: String,
1497        /// Owning capability.
1498        capability: String,
1499        /// Stable item identity.
1500        item_id: String,
1501        /// Bounded user-facing question or decision.
1502        text: String,
1503    },
1504    /// Session removed.
1505    SessionDeleted {
1506        /// Session identity.
1507        session_id: String,
1508    },
1509    /// Session transferred to another Bot.
1510    SessionOwnerChanged {
1511        /// Session identity.
1512        session_id: String,
1513        /// Previous Bot identity.
1514        previous_bot_id: String,
1515    },
1516    /// Paired client authenticated.
1517    ClientConnected {
1518        /// Paired client identity.
1519        client_id: String,
1520    },
1521    /// Paired client disconnected.
1522    ClientDisconnected {
1523        /// Paired client identity.
1524        client_id: String,
1525    },
1526    /// A Bot emitted a named JSON event.
1527    CustomReceived {
1528        /// User-selected event name.
1529        name: String,
1530        /// Bounded untrusted JSON evidence.
1531        data: serde_json::Value,
1532    },
1533}
1534
1535impl HookData {
1536    /// The selector boundary for this fact.
1537    #[must_use]
1538    pub fn kind(&self) -> HookKind {
1539        match self {
1540            Self::RoutineCreated { .. } => HookKind::RoutineCreated,
1541            Self::RoutineUpdated { .. } => HookKind::RoutineUpdated,
1542            Self::RoutinePaused { .. } => HookKind::RoutinePaused,
1543            Self::RoutineResumed { .. } => HookKind::RoutineResumed,
1544            Self::RoutineDeleted { .. } => HookKind::RoutineDeleted,
1545            Self::RunStarted { .. } => HookKind::RunStarted,
1546            Self::RunFinished { .. } => HookKind::RunFinished,
1547            Self::RunSkipped { .. } => HookKind::RunSkipped,
1548            Self::ScheduleDue { .. } => HookKind::ScheduleDue,
1549            Self::SessionCreated { .. } => HookKind::SessionCreated,
1550            Self::SessionTurnStarted { .. } => HookKind::SessionTurnStarted,
1551            Self::SessionTurnFinished { .. } => HookKind::SessionTurnFinished,
1552            Self::SessionApproval { .. } => HookKind::SessionApproval,
1553            Self::SessionAttention { .. } => HookKind::SessionAttention,
1554            Self::SessionDeleted { .. } => HookKind::SessionDeleted,
1555            Self::SessionOwnerChanged { .. } => HookKind::SessionOwnerChanged,
1556            Self::ClientConnected { .. } => HookKind::ClientConnected,
1557            Self::ClientDisconnected { .. } => HookKind::ClientDisconnected,
1558            Self::CustomReceived { .. } => HookKind::CustomReceived,
1559        }
1560    }
1561}
1562
1563/// User-authored reporting consumer of the same typed hook stream.
1564#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
1565#[serde(deny_unknown_fields)]
1566pub struct BotSubscription {
1567    /// Owning Bot.
1568    pub bot_id: String,
1569    /// Exact trigger and its saved action.
1570    pub binding: HookBinding<BotAction>,
1571    /// Whether future matching facts should report.
1572    pub enabled: bool,
1573}