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