Skip to main content

meerkat_contracts/wire/
mob.rs

1//! Mob RPC wire contracts.
2
3use super::connection::WireAuthBindingRef;
4use super::runtime::WireTurnMetadataOverride;
5use super::session::WireContentInput;
6use super::supervisor_bridge::BridgeBootstrapToken;
7use base64::{Engine, engine::general_purpose::STANDARD as BASE64};
8use meerkat_core::OutputSchema;
9use meerkat_core::{
10    HandlingMode,
11    types::{RenderClass, RenderMetadata, RenderSalience},
12};
13use serde::{Deserialize, Serialize};
14use serde_json::Value;
15use std::collections::BTreeMap;
16
17use meerkat_core::{SurfaceMetadata, SurfaceMetadataError};
18
19#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq, Default)]
20#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
21#[serde(rename_all = "snake_case")]
22pub enum WireMobBackendKind {
23    #[default]
24    Session,
25    External,
26}
27
28/// Runtime binding for spawn requests.
29///
30/// First step toward identity-first mobs. Carries backend-specific binding
31/// details at spawn time. `External` requires typed process identity; callers
32/// do not supply raw comms peer IDs.
33#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
34#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
35#[serde(tag = "kind", rename_all = "snake_case", deny_unknown_fields)]
36pub enum WireRuntimeBinding {
37    Session,
38    External {
39        address: String,
40        #[serde(default, skip_serializing_if = "Option::is_none")]
41        bootstrap_token: Option<BridgeBootstrapToken>,
42        /// Typed Ed25519 signing identity for the external process. The
43        /// canonical comms `PeerId` is derived from this key after the wire
44        /// boundary, so callers cannot spoof an unrelated raw peer id.
45        identity: WireTrustedPeerIdentity,
46    },
47}
48
49#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq, Default)]
50#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
51#[serde(rename_all = "snake_case")]
52pub enum WireMobRuntimeMode {
53    #[default]
54    AutonomousHost,
55    TurnDriven,
56}
57
58/// How a mob member should be launched by `mob/spawn`.
59#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
60#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
61#[serde(tag = "mode", rename_all = "snake_case")]
62pub enum WireMemberLaunchMode {
63    Fresh,
64    Resume {
65        bridge_session_id: String,
66    },
67    Fork {
68        source_member_id: String,
69        #[serde(default)]
70        fork_context: WireForkContext,
71    },
72}
73
74/// Conversation history scope used when forking a mob member.
75#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq, Eq)]
76#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
77#[serde(tag = "type", rename_all = "snake_case")]
78pub enum WireForkContext {
79    #[default]
80    FullHistory,
81    LastMessages {
82        count: u32,
83    },
84}
85
86/// Budget split policy for a spawned mob member.
87#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq, Eq)]
88#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
89#[serde(tag = "type", content = "value", rename_all = "snake_case")]
90pub enum WireBudgetSplitPolicy {
91    #[default]
92    Equal,
93    Proportional,
94    Remaining,
95    Fixed(u64),
96}
97
98/// Tool access policy for a spawned mob member.
99#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq, Eq)]
100#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
101#[serde(tag = "type", content = "value", rename_all = "snake_case")]
102pub enum WireToolAccessPolicy {
103    #[default]
104    Inherit,
105    AllowList(Vec<String>),
106    DenyList(Vec<String>),
107}
108
109/// Pre-resolved tool filter inherited by a spawned mob member.
110#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq, Eq)]
111#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
112pub enum WireToolFilter {
113    #[default]
114    All,
115    Allow(Vec<String>),
116    Deny(Vec<String>),
117}
118
119/// Tool configuration embedded in a wire mob profile override.
120#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq, Eq)]
121#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
122#[serde(deny_unknown_fields)]
123pub struct WireMobToolConfig {
124    #[serde(default)]
125    pub builtins: bool,
126    #[serde(default)]
127    pub shell: bool,
128    #[serde(default)]
129    pub comms: bool,
130    #[serde(default)]
131    pub memory: bool,
132    #[serde(default)]
133    pub workgraph: bool,
134    #[serde(default)]
135    pub mob: bool,
136    #[serde(default)]
137    pub schedule: bool,
138    #[serde(default)]
139    pub image_generation: bool,
140    #[serde(default)]
141    pub mcp: Vec<String>,
142}
143
144/// Profile fields that win over durable session metadata on resume.
145///
146/// Wire twin of `meerkat_mob::ResumeOverrideField`; closed snake_case
147/// vocabulary, parsed fail-closed at the wire boundary.
148#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
149#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
150#[serde(rename_all = "snake_case")]
151pub enum WireMobResumeOverrideField {
152    Model,
153    Provider,
154    ProviderParams,
155}
156
157/// Profile override for `mob/spawn`.
158#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
159#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
160#[serde(deny_unknown_fields)]
161pub struct WireMobProfile {
162    pub model: String,
163    /// Explicit typed provider for the profile model (closed vocabulary,
164    /// fail-closed at the wire boundary).
165    #[serde(default, skip_serializing_if = "Option::is_none")]
166    pub provider: Option<meerkat_core::Provider>,
167    /// Durable self-hosted server binding for configured self-hosted aliases.
168    #[serde(default, skip_serializing_if = "Option::is_none")]
169    pub self_hosted_server_id: Option<String>,
170    /// Configured default provider for `Auto` image-generation targets.
171    #[serde(default, skip_serializing_if = "Option::is_none")]
172    pub image_generation_provider: Option<meerkat_core::Provider>,
173    /// Per-profile auto-compaction threshold override (tokens, non-zero).
174    #[serde(default, skip_serializing_if = "Option::is_none")]
175    pub auto_compact_threshold: Option<std::num::NonZeroU64>,
176    /// Profile fields that win over durable session metadata on resume.
177    #[serde(default, skip_serializing_if = "Vec::is_empty")]
178    pub resume_overrides: Vec<WireMobResumeOverrideField>,
179    #[serde(default)]
180    pub skills: Vec<String>,
181    #[serde(default)]
182    pub tools: WireMobToolConfig,
183    #[serde(default)]
184    pub peer_description: String,
185    #[serde(default)]
186    pub external_addressable: bool,
187    #[serde(default, skip_serializing_if = "Option::is_none")]
188    pub backend: Option<WireMobBackendKind>,
189    #[serde(default)]
190    pub runtime_mode: WireMobRuntimeMode,
191    #[serde(default, skip_serializing_if = "Option::is_none")]
192    pub max_inline_peer_notifications: Option<i32>,
193    #[serde(default, skip_serializing_if = "Option::is_none")]
194    pub output_schema: Option<Value>,
195    #[serde(default, skip_serializing_if = "Option::is_none")]
196    pub provider_params: Option<crate::wire::runtime::WireProviderParamsOverride>,
197}
198
199#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
200#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
201#[serde(deny_unknown_fields)]
202pub struct MobOrchestratorInput {
203    pub profile: String,
204}
205
206#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
207#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
208#[serde(tag = "source", rename_all = "snake_case")]
209pub enum MobSkillSourceInput {
210    Inline { content: String },
211    Path { path: String },
212}
213
214#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
215#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
216#[serde(deny_unknown_fields)]
217pub struct MobRoleWiringRuleInput {
218    pub a: String,
219    pub b: String,
220}
221
222#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq, Eq)]
223#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
224#[serde(deny_unknown_fields)]
225pub struct MobWiringRulesInput {
226    #[serde(default)]
227    pub auto_wire_orchestrator: bool,
228    #[serde(default, skip_serializing_if = "Vec::is_empty")]
229    pub role_wiring: Vec<MobRoleWiringRuleInput>,
230}
231
232#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq, Eq)]
233#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
234#[serde(deny_unknown_fields)]
235pub struct MobToolConfigInput {
236    #[serde(default)]
237    pub builtins: bool,
238    #[serde(default)]
239    pub shell: bool,
240    #[serde(default)]
241    pub comms: bool,
242    #[serde(default)]
243    pub memory: bool,
244    #[serde(default)]
245    pub workgraph: bool,
246    #[serde(default)]
247    pub mob: bool,
248    #[serde(default)]
249    pub schedule: bool,
250    #[serde(default)]
251    pub image_generation: bool,
252    #[serde(default, skip_serializing_if = "Vec::is_empty")]
253    pub mcp: Vec<String>,
254}
255
256/// Profile binding input: either an inline profile or a realm profile reference.
257///
258/// Not `Eq`: `Inline(MobProfileInput)` transitively carries float provider
259/// params (`temperature`, `top_p`) so `Eq` cannot be derived without
260/// losing fidelity.
261#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
262#[allow(clippy::large_enum_variant)]
263#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
264#[serde(untagged)]
265pub enum MobProfileBindingInput {
266    /// Reference to a realm-scoped profile.
267    RealmRef {
268        /// Name of the realm profile.
269        realm_profile: String,
270    },
271    /// Inline profile definition.
272    Inline(MobProfileInput),
273}
274
275#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
276#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
277#[serde(deny_unknown_fields)]
278pub struct MobProfileInput {
279    pub model: String,
280    /// Explicit typed provider for the profile model (closed vocabulary,
281    /// fail-closed at the wire boundary).
282    #[serde(default, skip_serializing_if = "Option::is_none")]
283    pub provider: Option<meerkat_core::Provider>,
284    /// Durable self-hosted server binding for configured self-hosted aliases.
285    #[serde(default, skip_serializing_if = "Option::is_none")]
286    pub self_hosted_server_id: Option<String>,
287    /// Configured default provider for `Auto` image-generation targets.
288    #[serde(default, skip_serializing_if = "Option::is_none")]
289    pub image_generation_provider: Option<meerkat_core::Provider>,
290    /// Per-profile auto-compaction threshold override (tokens, non-zero).
291    #[serde(default, skip_serializing_if = "Option::is_none")]
292    pub auto_compact_threshold: Option<std::num::NonZeroU64>,
293    /// Profile fields that win over durable session metadata on resume.
294    #[serde(default, skip_serializing_if = "Vec::is_empty")]
295    pub resume_overrides: Vec<WireMobResumeOverrideField>,
296    #[serde(default, skip_serializing_if = "Vec::is_empty")]
297    pub skills: Vec<String>,
298    #[serde(default)]
299    pub tools: MobToolConfigInput,
300    #[serde(default, skip_serializing_if = "String::is_empty")]
301    pub peer_description: String,
302    #[serde(default)]
303    pub external_addressable: bool,
304    #[serde(default, skip_serializing_if = "Option::is_none")]
305    pub backend: Option<WireMobBackendKind>,
306    #[serde(default)]
307    pub runtime_mode: WireMobRuntimeMode,
308    #[serde(default, skip_serializing_if = "Option::is_none")]
309    pub max_inline_peer_notifications: Option<i32>,
310    #[serde(default, skip_serializing_if = "Option::is_none")]
311    pub output_schema: Option<OutputSchema>,
312    /// Non-`Eq` field: `WireProviderParamsOverride` contains float scalars
313    /// (`temperature`, `top_p`) so the struct can't derive `Eq` without
314    /// losing fidelity.
315    #[serde(default, skip_serializing_if = "Option::is_none")]
316    pub provider_params: Option<crate::wire::runtime::WireProviderParamsOverride>,
317}
318
319#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
320#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
321#[serde(deny_unknown_fields)]
322pub struct MobExternalBackendConfigInput {
323    pub address_base: String,
324    #[serde(default, skip_serializing_if = "Option::is_none")]
325    pub supervisor_bridge: Option<MobSupervisorBridgeEndpointConfigInput>,
326}
327
328#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq, Eq)]
329#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
330#[serde(deny_unknown_fields)]
331pub struct MobSupervisorBridgeEndpointConfigInput {
332    #[serde(default, skip_serializing_if = "Option::is_none")]
333    pub bind_address: Option<String>,
334    #[serde(default, skip_serializing_if = "Option::is_none")]
335    pub advertised_address: Option<String>,
336}
337
338#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq, Eq)]
339#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
340#[serde(deny_unknown_fields)]
341pub struct MobBackendConfigInput {
342    #[serde(default)]
343    pub default: WireMobBackendKind,
344    #[serde(default, skip_serializing_if = "Option::is_none")]
345    pub external: Option<MobExternalBackendConfigInput>,
346}
347
348#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq, Default)]
349#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
350#[serde(rename_all = "snake_case")]
351pub enum MobDispatchModeInput {
352    #[default]
353    FanOut,
354    OneToOne,
355    FanIn,
356}
357
358#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq, Eq)]
359#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
360#[serde(tag = "type", rename_all = "snake_case")]
361pub enum MobCollectionPolicyInput {
362    #[default]
363    All,
364    Any,
365    Quorum {
366        n: u8,
367    },
368}
369
370#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq, Default)]
371#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
372#[serde(rename_all = "snake_case")]
373pub enum MobDependencyModeInput {
374    #[default]
375    All,
376    Any,
377}
378
379/// Explicit step output format. Omitting `output_format` on a step is
380/// meaningful — the definition layer resolves a schema-aware default (`json`
381/// when the step declares `expected_schema_ref`, `text` otherwise) — so the
382/// wire shape keeps "omitted" representable instead of baking in a default.
383#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
384#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
385#[serde(rename_all = "snake_case")]
386pub enum MobStepOutputFormatInput {
387    Json,
388    Text,
389}
390
391#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
392#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
393#[serde(tag = "op", rename_all = "snake_case")]
394pub enum MobConditionExprInput {
395    Eq { path: String, value: Value },
396    In { path: String, values: Vec<Value> },
397    Gt { path: String, value: Value },
398    Lt { path: String, value: Value },
399    And { exprs: Vec<MobConditionExprInput> },
400    Or { exprs: Vec<MobConditionExprInput> },
401    Not { expr: Box<MobConditionExprInput> },
402}
403
404#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
405#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
406#[serde(deny_unknown_fields)]
407pub struct MobFrameSpecInput {
408    pub nodes: BTreeMap<String, MobFlowNodeInput>,
409}
410
411#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
412#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
413#[serde(tag = "kind", rename_all = "snake_case")]
414pub enum MobFlowNodeInput {
415    Step(MobFrameStepInput),
416    RepeatUntil(MobRepeatUntilInput),
417}
418
419#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
420#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
421#[serde(deny_unknown_fields)]
422pub struct MobFrameStepInput {
423    pub step_id: String,
424    #[serde(default, skip_serializing_if = "Vec::is_empty")]
425    pub depends_on: Vec<String>,
426    #[serde(default)]
427    pub depends_on_mode: MobDependencyModeInput,
428    #[serde(default, skip_serializing_if = "Option::is_none")]
429    pub branch: Option<String>,
430}
431
432#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
433#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
434#[serde(deny_unknown_fields)]
435pub struct MobRepeatUntilInput {
436    pub loop_id: String,
437    #[serde(default, skip_serializing_if = "Vec::is_empty")]
438    pub depends_on: Vec<String>,
439    #[serde(default)]
440    pub depends_on_mode: MobDependencyModeInput,
441    pub body: MobFrameSpecInput,
442    pub until: MobConditionExprInput,
443    pub max_iterations: u32,
444}
445
446#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
447#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
448#[serde(deny_unknown_fields)]
449pub struct MobFlowStepInput {
450    pub role: String,
451    pub message: WireContentInput,
452    #[serde(default, skip_serializing_if = "Vec::is_empty")]
453    pub depends_on: Vec<String>,
454    #[serde(default)]
455    pub dispatch_mode: MobDispatchModeInput,
456    #[serde(default)]
457    pub collection_policy: MobCollectionPolicyInput,
458    #[serde(default, skip_serializing_if = "Option::is_none")]
459    pub condition: Option<MobConditionExprInput>,
460    #[serde(default, skip_serializing_if = "Option::is_none")]
461    pub timeout_ms: Option<u64>,
462    #[serde(default, skip_serializing_if = "Option::is_none")]
463    pub expected_schema_ref: Option<String>,
464    #[serde(default, skip_serializing_if = "Option::is_none")]
465    pub branch: Option<String>,
466    #[serde(default)]
467    pub depends_on_mode: MobDependencyModeInput,
468    #[serde(default, skip_serializing_if = "Option::is_none")]
469    pub allowed_tools: Option<Vec<String>>,
470    #[serde(default, skip_serializing_if = "Option::is_none")]
471    pub blocked_tools: Option<Vec<String>>,
472    /// Explicit output format; omitted resolves schema-aware at the
473    /// definition layer (`json` with `expected_schema_ref`, `text` without).
474    #[serde(default, skip_serializing_if = "Option::is_none")]
475    pub output_format: Option<MobStepOutputFormatInput>,
476}
477
478#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq, Eq)]
479#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
480#[serde(deny_unknown_fields)]
481pub struct MobFlowSpecInput {
482    #[serde(default, skip_serializing_if = "Option::is_none")]
483    pub description: Option<String>,
484    #[serde(default, skip_serializing_if = "BTreeMap::is_empty")]
485    pub steps: BTreeMap<String, MobFlowStepInput>,
486    #[serde(default, skip_serializing_if = "Option::is_none")]
487    pub root: Option<MobFrameSpecInput>,
488}
489
490#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq, Default)]
491#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
492#[serde(rename_all = "snake_case")]
493pub enum MobPolicyModeInput {
494    #[default]
495    Advisory,
496    Strict,
497}
498
499#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
500#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
501#[serde(deny_unknown_fields)]
502pub struct MobTopologyRuleInput {
503    pub from_role: String,
504    pub to_role: String,
505    pub allowed: bool,
506}
507
508#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
509#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
510#[serde(deny_unknown_fields)]
511pub struct MobTopologySpecInput {
512    pub mode: MobPolicyModeInput,
513    pub rules: Vec<MobTopologyRuleInput>,
514}
515
516#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
517#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
518#[serde(deny_unknown_fields)]
519pub struct MobSupervisorSpecInput {
520    pub role: String,
521    pub escalation_threshold: u32,
522    /// Declared escalation turn timeout in milliseconds. Absent means the
523    /// runtime default applies (mirrors the domain `SupervisorSpec` owner).
524    #[serde(default, skip_serializing_if = "Option::is_none")]
525    pub escalation_turn_timeout_ms: Option<u64>,
526}
527
528#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq, Eq)]
529#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
530#[serde(deny_unknown_fields)]
531pub struct MobLimitsSpecInput {
532    #[serde(default, skip_serializing_if = "Option::is_none")]
533    pub max_flow_duration_ms: Option<u64>,
534    #[serde(default, skip_serializing_if = "Option::is_none")]
535    pub max_step_retries: Option<u32>,
536    #[serde(default, skip_serializing_if = "Option::is_none")]
537    pub max_orphaned_turns: Option<u32>,
538    #[serde(default, skip_serializing_if = "Option::is_none")]
539    pub cancel_grace_timeout_ms: Option<u64>,
540    #[serde(default, skip_serializing_if = "Option::is_none")]
541    pub max_active_nodes: Option<u64>,
542    #[serde(default, skip_serializing_if = "Option::is_none")]
543    pub max_active_frames: Option<u64>,
544    #[serde(default, skip_serializing_if = "Option::is_none")]
545    pub max_frame_depth: Option<u64>,
546}
547
548#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
549#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
550#[serde(tag = "mode", rename_all = "snake_case")]
551pub enum MobSpawnPolicyInput {
552    None,
553    Auto {
554        profile_map: BTreeMap<String, String>,
555    },
556}
557
558#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
559#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
560#[serde(deny_unknown_fields)]
561pub struct MobEventRouterConfigInput {
562    #[serde(default = "default_event_router_buffer_size")]
563    pub buffer_size: usize,
564    #[serde(default, skip_serializing_if = "Option::is_none")]
565    pub include_patterns: Option<Vec<String>>,
566    #[serde(default, skip_serializing_if = "Option::is_none")]
567    pub exclude_patterns: Option<Vec<String>>,
568}
569
570const fn default_event_router_buffer_size() -> usize {
571    256
572}
573
574/// Public mob definition input for `mob/create`.
575///
576/// This mirrors the public creation contract shape. Runtime-owned lifecycle and
577/// bookkeeping fields such as internal owner/runtime bindings,
578/// `session_cleanup_policy`, `is_implicit`, and internal-only profile tool
579/// bundles are intentionally not part of this schema.
580///
581/// Not `Eq`: `profiles` transitively carries float provider params.
582#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
583#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
584#[serde(deny_unknown_fields)]
585pub struct MobDefinitionInput {
586    pub id: String,
587    #[serde(default, skip_serializing_if = "Option::is_none")]
588    pub orchestrator: Option<MobOrchestratorInput>,
589    pub profiles: BTreeMap<String, MobProfileBindingInput>,
590    /// Mob-scoped custom model registry entries (`[models.<id>]`). Reuses the
591    /// typed config owner so one definition feeds provider inference,
592    /// compaction scaling, capability gates, and call timeouts.
593    #[serde(default, skip_serializing_if = "BTreeMap::is_empty")]
594    pub models: BTreeMap<String, meerkat_core::config::CustomModelConfig>,
595    /// Mob-level default provider for `Auto` image-generation targets.
596    #[serde(default, skip_serializing_if = "Option::is_none")]
597    pub image_generation_provider: Option<meerkat_core::Provider>,
598    #[serde(default)]
599    pub wiring: MobWiringRulesInput,
600    #[serde(default, skip_serializing_if = "BTreeMap::is_empty")]
601    pub skills: BTreeMap<String, MobSkillSourceInput>,
602    #[serde(default)]
603    pub backend: MobBackendConfigInput,
604    #[serde(default, skip_serializing_if = "BTreeMap::is_empty")]
605    pub flows: BTreeMap<String, MobFlowSpecInput>,
606    #[serde(default, skip_serializing_if = "Option::is_none")]
607    pub topology: Option<MobTopologySpecInput>,
608    #[serde(default, skip_serializing_if = "Option::is_none")]
609    pub supervisor: Option<MobSupervisorSpecInput>,
610    #[serde(default, skip_serializing_if = "Option::is_none")]
611    pub limits: Option<MobLimitsSpecInput>,
612    #[serde(default, skip_serializing_if = "Option::is_none")]
613    pub spawn_policy: Option<MobSpawnPolicyInput>,
614    #[serde(default, skip_serializing_if = "Option::is_none")]
615    pub event_router: Option<MobEventRouterConfigInput>,
616}
617
618/// Request payload for `mob/create`.
619#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
620#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
621#[serde(deny_unknown_fields)]
622pub struct MobCreateParams {
623    pub definition: MobDefinitionInput,
624}
625
626/// Response payload for `mob/create`.
627#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
628#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
629pub struct MobCreateResult {
630    pub mob_id: String,
631}
632
633/// Shared request payload for mob methods that address a mob by id.
634#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
635#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
636#[serde(deny_unknown_fields)]
637pub struct MobIdParams {
638    pub mob_id: String,
639}
640
641/// Shared request payload for mob methods that address one member by identity.
642#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
643#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
644#[serde(deny_unknown_fields)]
645pub struct MobMemberParams {
646    pub mob_id: String,
647    pub agent_identity: String,
648}
649
650/// Lifecycle status of a mob on the wire. Mirrors
651/// `meerkat_mob::runtime::MobState` so surfaces report mob lifecycle through a
652/// closed type rather than re-deriving meaning from free-form status text.
653///
654/// Variants serialize to their PascalCase names (`"Creating"`, `"Running"`,
655/// ...) to match the canonical `MobState::as_str()` projection that producers
656/// emit on the wire.
657#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
658#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
659pub enum WireMobLifecycleStatus {
660    Creating,
661    Running,
662    Stopped,
663    Completed,
664    Destroyed,
665}
666
667/// One active mob row returned by `mob/list`.
668#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
669#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
670pub struct MobStatusResult {
671    pub mob_id: String,
672    pub status: WireMobLifecycleStatus,
673}
674
675/// Response payload for `mob/list`.
676#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
677#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
678pub struct MobListResult {
679    pub mobs: Vec<MobStatusResult>,
680}
681
682/// Request payload for `mob/spawn`.
683#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
684#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
685#[serde(deny_unknown_fields)]
686pub struct MobSpawnParams {
687    pub mob_id: String,
688    pub profile: String,
689    pub agent_identity: String,
690    #[serde(default, skip_serializing_if = "Option::is_none")]
691    pub initial_message: Option<WireContentInput>,
692    #[serde(default, skip_serializing_if = "Option::is_none")]
693    pub runtime_mode: Option<WireMobRuntimeMode>,
694    #[serde(default, skip_serializing_if = "Option::is_none")]
695    pub backend: Option<WireMobBackendKind>,
696    #[serde(default, skip_serializing_if = "Option::is_none")]
697    pub labels: Option<BTreeMap<String, String>>,
698    #[serde(default, skip_serializing_if = "Option::is_none")]
699    pub context: Option<Value>,
700    #[serde(default, skip_serializing_if = "Option::is_none")]
701    pub additional_instructions: Option<Vec<String>>,
702    #[serde(default, skip_serializing_if = "Option::is_none")]
703    pub binding: Option<WireRuntimeBinding>,
704    #[serde(default, skip_serializing_if = "Option::is_none")]
705    pub shell_env: Option<BTreeMap<String, String>>,
706    #[serde(default, skip_serializing_if = "Option::is_none")]
707    pub auto_wire_parent: Option<bool>,
708    #[serde(default, skip_serializing_if = "Option::is_none")]
709    pub launch_mode: Option<WireMemberLaunchMode>,
710    #[serde(default, skip_serializing_if = "Option::is_none")]
711    pub tool_access_policy: Option<WireToolAccessPolicy>,
712    #[serde(default, skip_serializing_if = "Option::is_none")]
713    pub budget_split_policy: Option<WireBudgetSplitPolicy>,
714    #[serde(default, skip_serializing_if = "Option::is_none")]
715    pub inherited_tool_filter: Option<WireToolFilter>,
716    #[serde(default, skip_serializing_if = "Option::is_none")]
717    pub override_profile: Option<WireMobProfile>,
718    #[serde(default, skip_serializing_if = "Option::is_none")]
719    pub auth_binding: Option<WireAuthBindingRef>,
720}
721
722/// Response payload for `mob/spawn`.
723#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
724#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
725pub struct MobSpawnResult {
726    pub mob_id: String,
727    pub agent_identity: String,
728    pub member_ref: WireMemberRef,
729}
730
731/// Per-member request payload inside `mob/spawn_many`.
732#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
733#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
734#[serde(deny_unknown_fields)]
735pub struct MobSpawnSpecParams {
736    pub profile: String,
737    pub agent_identity: String,
738    #[serde(default, skip_serializing_if = "Option::is_none")]
739    pub initial_message: Option<WireContentInput>,
740    #[serde(default, skip_serializing_if = "Option::is_none")]
741    pub runtime_mode: Option<WireMobRuntimeMode>,
742    #[serde(default, skip_serializing_if = "Option::is_none")]
743    pub backend: Option<WireMobBackendKind>,
744    #[serde(default, skip_serializing_if = "Option::is_none")]
745    pub labels: Option<BTreeMap<String, String>>,
746    #[serde(default, skip_serializing_if = "Option::is_none")]
747    pub context: Option<Value>,
748    #[serde(default, skip_serializing_if = "Option::is_none")]
749    pub additional_instructions: Option<Vec<String>>,
750    #[serde(default, skip_serializing_if = "Option::is_none")]
751    pub auth_binding: Option<WireAuthBindingRef>,
752}
753
754/// Request payload for `mob/spawn_many`.
755#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
756#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
757#[serde(deny_unknown_fields)]
758pub struct MobSpawnManyParams {
759    pub mob_id: String,
760    pub specs: Vec<MobSpawnSpecParams>,
761}
762
763/// Typed status for one `mob/spawn_many` row.
764#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
765#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
766#[serde(rename_all = "snake_case")]
767pub enum MobSpawnManyResultStatus {
768    Spawned,
769    Failed,
770}
771
772/// Successful per-member `mob/spawn_many` result payload.
773#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
774#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
775#[serde(deny_unknown_fields)]
776pub struct MobSpawnManySpawnedResult {
777    pub agent_identity: String,
778    pub member_ref: WireMemberRef,
779}
780
781/// Typed failure cause for one failed `mob/spawn_many` member row.
782#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
783#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
784#[serde(rename_all = "snake_case")]
785pub enum MobSpawnManyFailureCause {
786    ProfileNotFound,
787    MemberNotFound,
788    MemberAlreadyExists,
789    NotExternallyAddressable,
790    InvalidTransition,
791    WiringError,
792    BridgeCommandRejected,
793    MemberRestoreFailed,
794    KickoffWaitTimedOut,
795    ReadyWaitTimedOut,
796    DefinitionError,
797    FlowNotFound,
798    FlowFailed,
799    RunNotFound,
800    RunCanceled,
801    FlowTurnTimedOut,
802    FrameDepthLimitExceeded,
803    FrameAtomicPersistenceUnavailable,
804    SpecRevisionConflict,
805    SchemaValidation,
806    InsufficientTargets,
807    TopologyViolation,
808    BridgeDeliveryRejected,
809    SupervisorEscalation,
810    UnsupportedForMode,
811    MissingMemberCapability,
812    ResetBarrier,
813    StorageError,
814    SessionError,
815    CommsError,
816    CallbackPending,
817    StaleFenceToken,
818    StaleEventCursor,
819    WorkNotFound,
820    Internal,
821}
822
823/// Failed per-member `mob/spawn_many` result payload.
824#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
825#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
826#[serde(deny_unknown_fields)]
827pub struct MobSpawnManyFailedResult {
828    pub cause: MobSpawnManyFailureCause,
829    pub message: String,
830}
831
832/// Typed payload for one `mob/spawn_many` row.
833#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
834#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
835#[serde(untagged)]
836pub enum MobSpawnManyResultPayload {
837    Spawned(MobSpawnManySpawnedResult),
838    Failed(MobSpawnManyFailedResult),
839}
840
841/// One typed result entry in a `mob/spawn_many` response.
842#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
843#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
844#[serde(try_from = "MobSpawnManyResultEntryRaw")]
845pub struct MobSpawnManyResultEntry {
846    pub status: MobSpawnManyResultStatus,
847    pub result: MobSpawnManyResultPayload,
848}
849
850#[derive(Debug, Deserialize)]
851#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
852#[serde(deny_unknown_fields)]
853struct MobSpawnManyResultEntryRaw {
854    status: MobSpawnManyResultStatus,
855    result: MobSpawnManyResultPayload,
856}
857
858impl TryFrom<MobSpawnManyResultEntryRaw> for MobSpawnManyResultEntry {
859    type Error = String;
860
861    fn try_from(raw: MobSpawnManyResultEntryRaw) -> Result<Self, Self::Error> {
862        let entry = Self {
863            status: raw.status,
864            result: raw.result,
865        };
866        entry.validate().map_err(str::to_owned)?;
867        Ok(entry)
868    }
869}
870
871impl MobSpawnManyResultEntry {
872    pub fn spawned(agent_identity: impl Into<String>, member_ref: WireMemberRef) -> Self {
873        Self {
874            status: MobSpawnManyResultStatus::Spawned,
875            result: MobSpawnManyResultPayload::Spawned(MobSpawnManySpawnedResult {
876                agent_identity: agent_identity.into(),
877                member_ref,
878            }),
879        }
880    }
881
882    pub fn failed(cause: MobSpawnManyFailureCause, message: impl Into<String>) -> Self {
883        Self {
884            status: MobSpawnManyResultStatus::Failed,
885            result: MobSpawnManyResultPayload::Failed(MobSpawnManyFailedResult {
886                cause,
887                message: message.into(),
888            }),
889        }
890    }
891
892    pub fn validate(&self) -> Result<(), &'static str> {
893        match (&self.status, &self.result) {
894            (MobSpawnManyResultStatus::Spawned, MobSpawnManyResultPayload::Spawned(_))
895            | (MobSpawnManyResultStatus::Failed, MobSpawnManyResultPayload::Failed(_)) => Ok(()),
896            (MobSpawnManyResultStatus::Spawned, MobSpawnManyResultPayload::Failed(_)) => {
897                Err("mob spawn_many result status spawned requires spawned result")
898            }
899            (MobSpawnManyResultStatus::Failed, MobSpawnManyResultPayload::Spawned(_)) => {
900                Err("mob spawn_many result status failed requires failed result")
901            }
902        }
903    }
904}
905
906/// Response payload for `mob/spawn_many`.
907#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
908#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
909pub struct MobSpawnManyResult {
910    pub results: Vec<MobSpawnManyResultEntry>,
911}
912
913/// Response payload for `mob/retire`.
914#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
915#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
916pub struct MobRetireResult {
917    pub retired: bool,
918}
919
920/// Request payload for `mob/respawn`.
921#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
922#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
923#[serde(deny_unknown_fields)]
924pub struct MobRespawnParams {
925    pub mob_id: String,
926    pub agent_identity: String,
927    #[serde(default, skip_serializing_if = "Option::is_none")]
928    pub initial_message: Option<WireContentInput>,
929}
930
931/// Identity-native respawn receipt returned inside `MobRespawnResult`.
932#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
933#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
934pub struct MobRespawnReceipt {
935    pub identity: String,
936    pub member_ref: WireMemberRef,
937}
938
939/// Outcome of a `mob/respawn` call. Mirrors the success vs
940/// `MobRespawnError::TopologyRestoreFailed` distinction as a closed type so SDK
941/// consumers branch on a typed variant instead of re-deriving meaning from a
942/// free-form status string.
943#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
944#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
945#[serde(rename_all = "snake_case")]
946pub enum WireMobRespawnOutcome {
947    Completed,
948    TopologyRestoreFailed,
949}
950
951/// Response payload for `mob/respawn`.
952#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
953#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
954pub struct MobRespawnResult {
955    pub status: WireMobRespawnOutcome,
956    pub receipt: MobRespawnReceipt,
957    #[serde(default, skip_serializing_if = "Vec::is_empty")]
958    pub failed_peer_ids: Vec<String>,
959}
960
961/// Response payload for `mob/members`.
962#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
963#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
964pub struct MobMembersResult {
965    pub mob_id: String,
966    pub members: Vec<MobMemberListEntryWire>,
967}
968
969/// Request payload for `mob/events`.
970#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
971#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
972#[serde(deny_unknown_fields)]
973pub struct MobEventsParams {
974    pub mob_id: String,
975    #[serde(default)]
976    pub after_cursor: u64,
977    #[serde(default = "default_mob_events_limit")]
978    pub limit: usize,
979    #[serde(default)]
980    pub strict: bool,
981}
982
983const fn default_mob_events_limit() -> usize {
984    100
985}
986
987/// Response payload for `mob/events`.
988#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
989#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
990pub struct MobEventsResult {
991    pub events: Vec<Value>,
992}
993
994/// Typed external peer identity for public mob wiring surfaces.
995#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
996#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
997#[serde(tag = "kind", rename_all = "snake_case", deny_unknown_fields)]
998pub enum WireTrustedPeerIdentity {
999    /// Recoverable Ed25519 public key string in `ed25519:<base64>` form.
1000    Ed25519PublicKey { public_key: String },
1001}
1002
1003/// Resolved external peer identity atoms used after the wire boundary.
1004#[derive(Debug, Clone, Copy, PartialEq, Eq)]
1005pub struct ResolvedWireTrustedPeerIdentity {
1006    pub peer_id: meerkat_core::comms::PeerId,
1007    pub pubkey: [u8; 32],
1008}
1009
1010/// Failure modes for resolving a typed external peer identity.
1011#[derive(Debug, Clone, thiserror::Error, PartialEq, Eq)]
1012pub enum WireTrustedPeerIdentityError {
1013    #[error("external peer identity public_key must start with 'ed25519:'")]
1014    MissingEd25519Prefix,
1015    #[error("external peer identity public_key is not valid base64: {0}")]
1016    InvalidBase64(String),
1017    #[error("external peer identity public_key must decode to 32 bytes, got {actual}")]
1018    InvalidLength { actual: usize },
1019    #[error("external peer identity public_key must be non-zero")]
1020    ZeroPublicKey,
1021}
1022
1023impl WireTrustedPeerIdentity {
1024    pub fn resolve(&self) -> Result<ResolvedWireTrustedPeerIdentity, WireTrustedPeerIdentityError> {
1025        match self {
1026            Self::Ed25519PublicKey { public_key } => {
1027                let pubkey = parse_ed25519_public_key(public_key)?;
1028                if pubkey == [0u8; 32] {
1029                    return Err(WireTrustedPeerIdentityError::ZeroPublicKey);
1030                }
1031                Ok(ResolvedWireTrustedPeerIdentity {
1032                    peer_id: meerkat_core::comms::PeerId::from_ed25519_pubkey(&pubkey),
1033                    pubkey,
1034                })
1035            }
1036        }
1037    }
1038}
1039
1040fn parse_ed25519_public_key(raw: &str) -> Result<[u8; 32], WireTrustedPeerIdentityError> {
1041    const PREFIX: &str = "ed25519:";
1042    let encoded = raw
1043        .strip_prefix(PREFIX)
1044        .ok_or(WireTrustedPeerIdentityError::MissingEd25519Prefix)?;
1045    let bytes = BASE64
1046        .decode(encoded)
1047        .map_err(|err| WireTrustedPeerIdentityError::InvalidBase64(err.to_string()))?;
1048    let actual = bytes.len();
1049    let pubkey: [u8; 32] = bytes
1050        .try_into()
1051        .map_err(|_| WireTrustedPeerIdentityError::InvalidLength { actual })?;
1052    Ok(pubkey)
1053}
1054
1055/// Minimal trusted peer spec for public mob wiring surfaces.
1056///
1057/// `identity` is required and resolves to the Ed25519 signing public key
1058/// plus the canonical comms `PeerId` derived from that key. MCP callers do
1059/// not provide raw peer IDs, and missing key material fails at the boundary.
1060#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
1061#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
1062#[serde(deny_unknown_fields)]
1063pub struct WireTrustedPeerSpec {
1064    pub name: String,
1065    pub address: String,
1066    pub identity: WireTrustedPeerIdentity,
1067}
1068
1069/// Target for a mob wire/unwire call.
1070#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
1071#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
1072#[serde(rename_all = "snake_case")]
1073pub enum MobPeerTarget {
1074    Local(String),
1075    External(WireTrustedPeerSpec),
1076}
1077
1078/// Request payload for `mob/wire`.
1079#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
1080#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
1081#[serde(deny_unknown_fields)]
1082pub struct MobWireParams {
1083    pub mob_id: String,
1084    pub member: String,
1085    pub peer: MobPeerTarget,
1086}
1087
1088/// Response payload for `mob/wire`.
1089#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
1090#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
1091pub struct MobWireResult {
1092    pub wired: bool,
1093}
1094
1095/// One local-member edge in `mob/wire_members_batch`.
1096#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
1097#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
1098#[serde(deny_unknown_fields)]
1099pub struct MobWireMembersBatchEdge {
1100    pub a: String,
1101    pub b: String,
1102}
1103
1104/// Request payload for `mob/wire_members_batch`.
1105#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
1106#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
1107#[serde(deny_unknown_fields)]
1108pub struct MobWireMembersBatchParams {
1109    pub mob_id: String,
1110    pub edges: Vec<MobWireMembersBatchEdge>,
1111}
1112
1113/// Response payload for `mob/wire_members_batch`.
1114#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
1115#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
1116pub struct MobWireMembersBatchResult {
1117    pub requested: usize,
1118    pub wired: Vec<MobWireMembersBatchEdge>,
1119    pub already_wired: Vec<MobWireMembersBatchEdge>,
1120}
1121
1122/// Request payload for `mob/unwire`.
1123#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
1124#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
1125#[serde(deny_unknown_fields)]
1126pub struct MobUnwireParams {
1127    pub mob_id: String,
1128    pub member: String,
1129    pub peer: MobPeerTarget,
1130}
1131
1132/// Response payload for `mob/unwire`.
1133#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
1134#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
1135pub struct MobUnwireResult {
1136    pub unwired: bool,
1137}
1138
1139/// Request payload for host-side mob member delivery.
1140#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
1141#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
1142#[serde(deny_unknown_fields)]
1143pub struct MobMemberSendParams {
1144    pub mob_id: String,
1145    pub agent_identity: String,
1146    pub content: WireContentInput,
1147    #[serde(default)]
1148    pub handling_mode: WireHandlingMode,
1149    #[serde(default, skip_serializing_if = "Option::is_none")]
1150    pub render_metadata: Option<WireRenderMetadata>,
1151}
1152
1153/// Response payload for host-side mob member delivery.
1154#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
1155#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
1156pub struct WireAgentRuntimeId {
1157    pub identity: String,
1158    pub generation: u64,
1159}
1160
1161/// Response payload for host-side mob member delivery.
1162#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
1163#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
1164pub struct MobMemberSendResult {
1165    pub mob_id: String,
1166    /// Identity-native member identity (0.6).
1167    pub agent_identity: String,
1168    /// Server-resolved opaque handle for subsequent member-targeted calls.
1169    /// App code routes through `member_ref`; the binding-era
1170    /// `{identity, generation}` pair carried by `WireAgentRuntimeId` is
1171    /// retired from app-facing responses per dogma #10.
1172    pub member_ref: WireMemberRef,
1173    pub handling_mode: WireHandlingMode,
1174}
1175
1176/// Request payload for `mob/ingress_interaction`.
1177///
1178/// This is the ergonomic "ensure an ingress member, then deliver user input"
1179/// path. It composes the existing declarative roster and member-send
1180/// semantics without introducing a separate thread/project runtime.
1181#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
1182#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
1183#[serde(deny_unknown_fields)]
1184pub struct MobIngressInteractionParams {
1185    pub mob_id: String,
1186    pub spec: MobMemberSpecWire,
1187    pub content: WireContentInput,
1188    #[serde(default)]
1189    pub handling_mode: WireHandlingMode,
1190    #[serde(default, skip_serializing_if = "Option::is_none")]
1191    pub render_metadata: Option<WireRenderMetadata>,
1192}
1193
1194/// Response payload for `mob/ingress_interaction`.
1195#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
1196#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
1197pub struct MobIngressInteractionResult {
1198    pub mob_id: String,
1199    pub agent_identity: String,
1200    pub member_ref: WireMemberRef,
1201    pub ensure_outcome: MobEnsureMemberOutcomeWire,
1202    pub delivery: MobMemberSendResult,
1203    /// Cursor observed immediately before the ensure/send composition.
1204    pub events_after_cursor: u64,
1205    /// Cursor observed after delivery was accepted.
1206    pub latest_event_cursor: u64,
1207}
1208
1209/// Public handling mode for mob member delivery.
1210#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq, Default)]
1211#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
1212#[serde(rename_all = "snake_case")]
1213pub enum WireHandlingMode {
1214    #[default]
1215    Queue,
1216    Steer,
1217}
1218
1219impl From<WireHandlingMode> for HandlingMode {
1220    fn from(mode: WireHandlingMode) -> Self {
1221        match mode {
1222            WireHandlingMode::Queue => HandlingMode::Queue,
1223            WireHandlingMode::Steer => HandlingMode::Steer,
1224        }
1225    }
1226}
1227
1228impl From<HandlingMode> for WireHandlingMode {
1229    fn from(mode: HandlingMode) -> Self {
1230        match mode {
1231            HandlingMode::Queue => WireHandlingMode::Queue,
1232            HandlingMode::Steer => WireHandlingMode::Steer,
1233        }
1234    }
1235}
1236
1237/// Public render class contract for mob member delivery.
1238#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
1239#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
1240#[serde(rename_all = "snake_case")]
1241pub enum WireRenderClass {
1242    UserPrompt,
1243    PeerMessage,
1244    PeerRequest,
1245    PeerResponse,
1246    ExternalEvent,
1247    FlowStep,
1248    Continuation,
1249    SystemNotice,
1250    ToolScopeNotice,
1251    OpsProgress,
1252}
1253
1254impl From<WireRenderClass> for RenderClass {
1255    fn from(class: WireRenderClass) -> Self {
1256        match class {
1257            WireRenderClass::UserPrompt => RenderClass::UserPrompt,
1258            WireRenderClass::PeerMessage => RenderClass::PeerMessage,
1259            WireRenderClass::PeerRequest => RenderClass::PeerRequest,
1260            WireRenderClass::PeerResponse => RenderClass::PeerResponse,
1261            WireRenderClass::ExternalEvent => RenderClass::ExternalEvent,
1262            WireRenderClass::FlowStep => RenderClass::FlowStep,
1263            WireRenderClass::Continuation => RenderClass::Continuation,
1264            WireRenderClass::SystemNotice => RenderClass::SystemNotice,
1265            WireRenderClass::ToolScopeNotice => RenderClass::ToolScopeNotice,
1266            WireRenderClass::OpsProgress => RenderClass::OpsProgress,
1267        }
1268    }
1269}
1270
1271impl From<RenderClass> for WireRenderClass {
1272    fn from(class: RenderClass) -> Self {
1273        match class {
1274            RenderClass::UserPrompt => WireRenderClass::UserPrompt,
1275            RenderClass::PeerMessage => WireRenderClass::PeerMessage,
1276            RenderClass::PeerRequest => WireRenderClass::PeerRequest,
1277            RenderClass::PeerResponse => WireRenderClass::PeerResponse,
1278            RenderClass::ExternalEvent => WireRenderClass::ExternalEvent,
1279            RenderClass::FlowStep => WireRenderClass::FlowStep,
1280            RenderClass::Continuation => WireRenderClass::Continuation,
1281            RenderClass::SystemNotice => WireRenderClass::SystemNotice,
1282            RenderClass::ToolScopeNotice => WireRenderClass::ToolScopeNotice,
1283            RenderClass::OpsProgress => WireRenderClass::OpsProgress,
1284        }
1285    }
1286}
1287
1288/// Public render salience contract for mob member delivery.
1289#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
1290#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
1291#[serde(rename_all = "snake_case")]
1292pub enum WireRenderSalience {
1293    Background,
1294    Normal,
1295    Important,
1296    Urgent,
1297}
1298
1299impl From<WireRenderSalience> for RenderSalience {
1300    fn from(salience: WireRenderSalience) -> Self {
1301        match salience {
1302            WireRenderSalience::Background => RenderSalience::Background,
1303            WireRenderSalience::Normal => RenderSalience::Normal,
1304            WireRenderSalience::Important => RenderSalience::Important,
1305            WireRenderSalience::Urgent => RenderSalience::Urgent,
1306        }
1307    }
1308}
1309
1310impl From<RenderSalience> for WireRenderSalience {
1311    fn from(salience: RenderSalience) -> Self {
1312        match salience {
1313            RenderSalience::Background => WireRenderSalience::Background,
1314            RenderSalience::Normal => WireRenderSalience::Normal,
1315            RenderSalience::Important => WireRenderSalience::Important,
1316            RenderSalience::Urgent => WireRenderSalience::Urgent,
1317        }
1318    }
1319}
1320
1321/// Public render metadata contract for mob member delivery.
1322#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
1323#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
1324pub struct WireRenderMetadata {
1325    pub class: WireRenderClass,
1326    #[serde(default, skip_serializing_if = "Option::is_none")]
1327    pub salience: Option<WireRenderSalience>,
1328}
1329
1330impl From<WireRenderMetadata> for RenderMetadata {
1331    fn from(metadata: WireRenderMetadata) -> Self {
1332        Self {
1333            class: metadata.class.into(),
1334            salience: metadata
1335                .salience
1336                .unwrap_or(WireRenderSalience::Normal)
1337                .into(),
1338        }
1339    }
1340}
1341
1342impl From<RenderMetadata> for WireRenderMetadata {
1343    fn from(metadata: RenderMetadata) -> Self {
1344        Self {
1345            class: metadata.class.into(),
1346            salience: Some(metadata.salience.into()),
1347        }
1348    }
1349}
1350
1351// ---------------------------------------------------------------------------
1352// Declarative roster API (`mob/ensure_member`, `mob/reconcile`,
1353// `mob/list_members_matching`). These methods compose over spawn / retire /
1354// list_members; they introduce no new lifecycle.
1355// ---------------------------------------------------------------------------
1356
1357/// Per-member spec for `mob/ensure_member` and the `desired` entries of
1358/// `mob/reconcile`.
1359///
1360/// Mirrors the essential, codegen-friendly fields of
1361/// [`meerkat_mob::SpawnMemberSpec`]. Complex sub-types (tool access policy,
1362/// budget split, inherited tool filter, override profile) are not on this
1363/// wire surface — callers that need that parity should use the non-declarative
1364/// `mob/spawn` method.
1365#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
1366#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
1367pub struct MobMemberSpecWire {
1368    /// Profile name (role) in the mob definition.
1369    pub profile: String,
1370    /// Stable member identity within the mob.
1371    pub agent_identity: String,
1372    #[serde(default, skip_serializing_if = "Option::is_none")]
1373    pub initial_message: Option<WireContentInput>,
1374    #[serde(default, skip_serializing_if = "Option::is_none")]
1375    pub runtime_mode: Option<WireMobRuntimeMode>,
1376    #[serde(default, skip_serializing_if = "Option::is_none")]
1377    pub backend: Option<WireMobBackendKind>,
1378    #[serde(default, skip_serializing_if = "Option::is_none")]
1379    pub binding: Option<WireRuntimeBinding>,
1380    #[serde(default, skip_serializing_if = "Option::is_none")]
1381    pub context: Option<Value>,
1382    #[serde(default, skip_serializing_if = "Option::is_none")]
1383    pub labels: Option<BTreeMap<String, String>>,
1384    #[serde(default, skip_serializing_if = "Option::is_none")]
1385    pub additional_instructions: Option<Vec<String>>,
1386    #[serde(default, skip_serializing_if = "Option::is_none")]
1387    pub auto_wire_parent: Option<bool>,
1388}
1389
1390impl MobMemberSpecWire {
1391    /// Compose the existing member `labels` and opaque `context` fields into
1392    /// the shared surface metadata contract without changing the JSON shape.
1393    #[must_use]
1394    pub fn surface_metadata(&self) -> SurfaceMetadata {
1395        SurfaceMetadata::from_optional_parts(self.labels.clone(), self.context.clone())
1396    }
1397
1398    /// Validate caller-supplied metadata for public member create surfaces.
1399    pub fn validate_public_surface_metadata(&self) -> Result<(), SurfaceMetadataError> {
1400        self.surface_metadata().validate_public()
1401    }
1402}
1403
1404/// Request payload for `mob/ensure_member`.
1405#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
1406#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
1407#[serde(deny_unknown_fields)]
1408pub struct MobEnsureMemberParams {
1409    pub mob_id: String,
1410    pub spec: MobMemberSpecWire,
1411}
1412
1413/// Server-resolved opaque handle for a mob member.
1414///
1415/// Encodes `{mob_id, agent_identity}` as a single base64url-encoded token
1416/// that callers treat as opaque. The server resolves the current
1417/// `AgentRuntimeId` and fence token against the live mob roster on every
1418/// dispatch — clients never reason about `generation` or `fence_token`
1419/// directly.
1420///
1421/// Use [`WireMemberRef::encode`] to produce a token and
1422/// [`WireMemberRef::decode`] inside an RPC handler to recover the
1423/// `(mob_id, agent_identity)` pair before resolving against the runtime.
1424#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, Hash)]
1425#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
1426#[serde(transparent)]
1427pub struct WireMemberRef(String);
1428
1429impl WireMemberRef {
1430    /// Construct a handle from its components. The `mob_id` and
1431    /// `agent_identity` together form the resolution key the server uses to
1432    /// look up the member's current incarnation.
1433    #[must_use]
1434    pub fn encode(mob_id: &str, agent_identity: &str) -> Self {
1435        // Single-letter keys keep the encoded payload short so the token
1436        // remains compact in URLs and JSON payloads.
1437        // `Value::to_string` on a two-field object is infallible.
1438        let payload = serde_json::json!({ "m": mob_id, "a": agent_identity });
1439        Self(base64_url_encode(payload.to_string().as_bytes()))
1440    }
1441
1442    /// Borrow the raw token string for transport.
1443    #[must_use]
1444    pub fn as_str(&self) -> &str {
1445        &self.0
1446    }
1447
1448    /// Construct a handle from a raw token string without validation. Used
1449    /// when forwarding an opaque token received from the wire.
1450    #[must_use]
1451    pub fn from_token(token: impl Into<String>) -> Self {
1452        Self(token.into())
1453    }
1454
1455    /// Decode the handle into `(mob_id, agent_identity)`. Returns `Err` when
1456    /// the token is malformed.
1457    pub fn decode(&self) -> Result<(String, String), WireMemberRefError> {
1458        let bytes = base64_url_decode(&self.0).map_err(|_| WireMemberRefError::Malformed)?;
1459        let value: Value =
1460            serde_json::from_slice(&bytes).map_err(|_| WireMemberRefError::Malformed)?;
1461        let mob_id = value
1462            .get("m")
1463            .and_then(Value::as_str)
1464            .ok_or(WireMemberRefError::Malformed)?;
1465        let agent_identity = value
1466            .get("a")
1467            .and_then(Value::as_str)
1468            .ok_or(WireMemberRefError::Malformed)?;
1469        Ok((mob_id.to_string(), agent_identity.to_string()))
1470    }
1471}
1472
1473/// Failure modes for [`WireMemberRef::decode`].
1474#[derive(Debug, thiserror::Error)]
1475pub enum WireMemberRefError {
1476    /// Token is not valid base64url or its decoded payload is not the
1477    /// expected `{m, a}` shape.
1478    #[error("malformed member ref token")]
1479    Malformed,
1480}
1481
1482fn base64_url_encode(bytes: &[u8]) -> String {
1483    use base64::Engine as _;
1484    base64::engine::general_purpose::URL_SAFE_NO_PAD.encode(bytes)
1485}
1486
1487fn base64_url_decode(input: &str) -> Result<Vec<u8>, base64::DecodeError> {
1488    use base64::Engine as _;
1489    base64::engine::general_purpose::URL_SAFE_NO_PAD.decode(input)
1490}
1491
1492/// Identity-native payload for `EnsureMemberOutcome::Spawned`.
1493#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
1494#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
1495pub struct MobSpawnReceiptWire {
1496    pub agent_identity: String,
1497    /// Server-resolved opaque handle for subsequent member-targeted calls
1498    /// (work submission, cancellation, lifecycle). Replaces the binding-era
1499    /// `generation` / `fence_token` pair on app-facing surfaces.
1500    pub member_ref: WireMemberRef,
1501}
1502
1503/// Execution status mirroring `meerkat_mob::runtime::MobMemberStatus`.
1504#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
1505#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
1506#[serde(rename_all = "snake_case")]
1507pub enum WireMobMemberStatus {
1508    Active,
1509    Retiring,
1510    Broken,
1511    Completed,
1512    Unknown,
1513}
1514
1515/// Public roster entry returned by `mob/ensure_member`'s `Existed` outcome
1516/// (and other surfaces that want a typed snapshot of a single member). Mirrors
1517/// the public-facing fields of `meerkat_mob::runtime::MobMemberListEntry`
1518/// without leaking bridge-internal fields.
1519#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
1520#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
1521pub struct MobMemberListEntryWire {
1522    pub agent_identity: String,
1523    pub member_ref: WireMemberRef,
1524    pub role: String,
1525    pub runtime_mode: WireMobRuntimeMode,
1526    #[serde(default, skip_serializing_if = "Vec::is_empty")]
1527    pub wired_to: Vec<String>,
1528    #[serde(default, skip_serializing_if = "BTreeMap::is_empty")]
1529    pub labels: BTreeMap<String, String>,
1530    pub status: WireMobMemberStatus,
1531    #[serde(default, skip_serializing_if = "Option::is_none")]
1532    pub error: Option<String>,
1533    pub is_final: bool,
1534}
1535
1536/// Outcome of a `mob/ensure_member` call.
1537///
1538/// `Existed` returns the typed [`MobMemberListEntryWire`] roster snapshot so
1539/// public consumers do not need out-of-band knowledge of the Rust domain
1540/// `MobMemberListEntry` shape.
1541#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
1542#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
1543pub enum MobEnsureMemberOutcomeWire {
1544    #[serde(rename = "spawned")]
1545    Spawned(MobSpawnReceiptWire),
1546    #[serde(rename = "existed")]
1547    Existed(MobMemberListEntryWire),
1548}
1549
1550/// Response payload for `mob/ensure_member`.
1551#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
1552#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
1553pub struct MobEnsureMemberResult {
1554    pub outcome: MobEnsureMemberOutcomeWire,
1555}
1556
1557/// Options controlling a `mob/reconcile` pass.
1558#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq, Eq)]
1559#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
1560#[serde(deny_unknown_fields)]
1561pub struct MobReconcileOptionsWire {
1562    /// When `true`, members on the roster whose identity is not in the
1563    /// `desired` set are retired.
1564    #[serde(default)]
1565    pub retire_stale: bool,
1566}
1567
1568/// Closed wire stage for a per-identity `mob/reconcile` failure.
1569#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
1570#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
1571#[serde(rename_all = "snake_case")]
1572pub enum WireMobReconcileStage {
1573    Spawn,
1574    Retire,
1575}
1576
1577/// Request payload for `mob/reconcile`.
1578#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
1579#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
1580#[serde(deny_unknown_fields)]
1581pub struct MobReconcileParams {
1582    pub mob_id: String,
1583    #[serde(default)]
1584    pub desired: Vec<MobMemberSpecWire>,
1585    #[serde(default)]
1586    pub options: MobReconcileOptionsWire,
1587}
1588
1589/// Typed mob error projection for wire surfaces. Carries the closed failure
1590/// class alongside the human-readable message so consumers branch on the typed
1591/// `code` rather than parsing the free-form `message`. Reuses
1592/// [`MobSpawnManyFailureCause`] as the canonical closed mob-error vocabulary.
1593#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
1594#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
1595pub struct WireMobError {
1596    pub code: MobSpawnManyFailureCause,
1597    pub message: String,
1598}
1599
1600/// Per-identity failure in a `mob/reconcile` pass.
1601#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
1602#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
1603pub struct MobReconcileFailureWire {
1604    pub agent_identity: String,
1605    pub stage: WireMobReconcileStage,
1606    /// Typed mob error: closed failure `code` plus human-readable `message`.
1607    pub error: WireMobError,
1608}
1609
1610/// Summary produced by a `mob/reconcile` pass.
1611#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq)]
1612#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
1613pub struct MobReconcileReportWire {
1614    #[serde(default, skip_serializing_if = "Vec::is_empty")]
1615    pub desired: Vec<String>,
1616    #[serde(default, skip_serializing_if = "Vec::is_empty")]
1617    pub retained: Vec<String>,
1618    #[serde(default, skip_serializing_if = "Vec::is_empty")]
1619    pub spawned: Vec<MobSpawnReceiptWire>,
1620    #[serde(default, skip_serializing_if = "Vec::is_empty")]
1621    pub retired: Vec<String>,
1622    #[serde(default, skip_serializing_if = "Vec::is_empty")]
1623    pub failures: Vec<MobReconcileFailureWire>,
1624}
1625
1626/// Response payload for `mob/reconcile`.
1627#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
1628#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
1629pub struct MobReconcileResult {
1630    pub report: MobReconcileReportWire,
1631}
1632
1633/// Typed lifecycle action for `mob/lifecycle`. Replaces the prior
1634/// `action: String` discriminator with an exhaustive enum so callers and
1635/// handlers reason about lifecycle transitions through the type system
1636/// rather than string folklore.
1637#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
1638#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
1639#[serde(rename_all = "snake_case")]
1640pub enum WireMobLifecycleAction {
1641    Stop,
1642    Resume,
1643    Complete,
1644    Reset,
1645    Destroy,
1646}
1647
1648/// Typed wire/unwire action for the `mob_wire` agent tool. Replaces the prior
1649/// `action: String` discriminator with an exhaustive enum so the agent-tool
1650/// surface reasons about the wire/unwire distinction through the type system
1651/// rather than string folklore (mirrors [`WireMobLifecycleAction`]).
1652#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
1653#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
1654#[serde(rename_all = "snake_case")]
1655pub enum WireMobWireAction {
1656    Wire,
1657    Unwire,
1658}
1659
1660/// Request payload for `mob/lifecycle`.
1661#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
1662#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
1663#[serde(deny_unknown_fields)]
1664pub struct MobLifecycleParams {
1665    pub mob_id: String,
1666    pub action: WireMobLifecycleAction,
1667}
1668
1669/// Response payload for `mob/lifecycle`.
1670#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
1671#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
1672pub struct MobLifecycleResult {
1673    pub mob_id: String,
1674    pub action: WireMobLifecycleAction,
1675    pub ok: bool,
1676    #[serde(default, skip_serializing_if = "Option::is_none")]
1677    pub destroy_report: Option<Value>,
1678}
1679
1680/// Request payload for `mob/append_system_context`.
1681#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
1682#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
1683#[serde(deny_unknown_fields)]
1684pub struct MobAppendSystemContextParams {
1685    pub mob_id: String,
1686    pub agent_identity: String,
1687    pub text: String,
1688    #[serde(default, skip_serializing_if = "Option::is_none")]
1689    pub source: Option<String>,
1690    #[serde(default, skip_serializing_if = "Option::is_none")]
1691    pub idempotency_key: Option<String>,
1692}
1693
1694/// Outcome of a `mob/append_system_context` call on the wire. Mirrors
1695/// `meerkat_core::AppendSystemContextStatus` so consumers reason about the
1696/// applied/staged/duplicate distinction through a closed type rather than a
1697/// free-form status string.
1698#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
1699#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
1700#[serde(rename_all = "snake_case")]
1701pub enum WireAppendSystemContextStatus {
1702    Applied,
1703    Staged,
1704    Duplicate,
1705}
1706
1707impl From<meerkat_core::AppendSystemContextStatus> for WireAppendSystemContextStatus {
1708    fn from(status: meerkat_core::AppendSystemContextStatus) -> Self {
1709        match status {
1710            meerkat_core::AppendSystemContextStatus::Applied => Self::Applied,
1711            meerkat_core::AppendSystemContextStatus::Staged => Self::Staged,
1712            meerkat_core::AppendSystemContextStatus::Duplicate => Self::Duplicate,
1713        }
1714    }
1715}
1716
1717/// Response payload for `mob/append_system_context`.
1718#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
1719#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
1720pub struct MobAppendSystemContextResult {
1721    pub mob_id: String,
1722    pub agent_identity: String,
1723    pub status: WireAppendSystemContextStatus,
1724}
1725
1726/// Response payload for `mob/flows`.
1727#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
1728#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
1729pub struct MobFlowsResult {
1730    pub mob_id: String,
1731    pub flows: Vec<String>,
1732}
1733
1734/// Request payload for `mob/flow_run`.
1735#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
1736#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
1737#[serde(deny_unknown_fields)]
1738pub struct MobFlowRunParams {
1739    pub mob_id: String,
1740    pub flow_id: String,
1741    #[serde(default)]
1742    pub params: Value,
1743}
1744
1745/// Request payload for `mob/run`.
1746///
1747/// Starts the pack's callable flow. `flow_id` defaults to `main`; `prompt` is
1748/// sugar for `params.prompt` when the caller does not provide that key.
1749#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
1750#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
1751#[serde(deny_unknown_fields)]
1752pub struct MobRunParams {
1753    pub mob_id: String,
1754    #[serde(default, skip_serializing_if = "Option::is_none")]
1755    pub flow_id: Option<String>,
1756    #[serde(default, skip_serializing_if = "Option::is_none")]
1757    pub prompt: Option<String>,
1758    #[serde(default)]
1759    pub params: Value,
1760}
1761
1762/// Response payload for `mob/flow_run`.
1763#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
1764#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
1765pub struct MobFlowRunResult {
1766    pub run_id: String,
1767}
1768
1769/// Request payload for `mob/flow_status`.
1770#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
1771#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
1772#[serde(deny_unknown_fields)]
1773pub struct MobFlowStatusParams {
1774    pub mob_id: String,
1775    pub run_id: String,
1776}
1777
1778/// Lifecycle status of a flow run on the wire. Mirrors
1779/// `meerkat_mob::MobRunStatus` so consumers branch on a closed type rather than
1780/// re-deriving meaning from a free-form status string.
1781#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
1782#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
1783#[serde(rename_all = "snake_case")]
1784pub enum WireMobRunStatus {
1785    Pending,
1786    Running,
1787    Completed,
1788    Failed,
1789    Canceled,
1790}
1791
1792/// Typed public projection of a single flow run for `mob/flow_status`.
1793///
1794/// The canonical identity and lifecycle fields (`run_id`, `mob_id`, `flow_id`,
1795/// `status`) are typed; the remaining kernel-owned step/loop projection rides
1796/// along as the `kernel` map. Producers project a domain `MobRun` into this
1797/// shape so consumers never re-derive run identity or lifecycle from a free
1798/// `serde_json::Value`.
1799#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
1800#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
1801pub struct WireMobRun {
1802    pub run_id: String,
1803    pub mob_id: String,
1804    pub flow_id: String,
1805    pub status: WireMobRunStatus,
1806    /// Remaining kernel-owned run projection (step ledger, frame/loop outputs,
1807    /// flow state) after the typed identity/lifecycle fields are lifted out.
1808    #[serde(flatten)]
1809    pub kernel: serde_json::Map<String, Value>,
1810}
1811
1812/// Response payload for `mob/flow_status`.
1813///
1814/// `run` is `None` when the requested run id has no persisted run.
1815#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
1816#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
1817pub struct MobFlowStatusResult {
1818    #[serde(default, skip_serializing_if = "Option::is_none")]
1819    pub run: Option<WireMobRun>,
1820}
1821
1822/// Request payload for `mob/run_result`.
1823#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
1824#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
1825#[serde(deny_unknown_fields)]
1826pub struct MobRunResultParams {
1827    pub mob_id: String,
1828    pub run_id: String,
1829}
1830
1831/// Typed output envelope for a completed or in-flight mob flow run.
1832#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
1833#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
1834#[serde(deny_unknown_fields)]
1835pub struct WireMobRunResultEnvelope {
1836    pub run_id: String,
1837    pub mob_id: String,
1838    pub flow_id: String,
1839    pub status: WireMobRunStatus,
1840    #[serde(default, skip_serializing_if = "Option::is_none")]
1841    pub result: Option<Value>,
1842    #[serde(default, skip_serializing_if = "BTreeMap::is_empty")]
1843    pub outputs: BTreeMap<String, Value>,
1844}
1845
1846/// Response payload for `mob/run_result`.
1847///
1848/// `run` is `None` when the requested run id has no persisted run.
1849#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
1850#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
1851pub struct MobRunResult {
1852    #[serde(default, skip_serializing_if = "Option::is_none")]
1853    pub run: Option<WireMobRunResultEnvelope>,
1854}
1855
1856/// Request payload for `mob/flow_cancel`.
1857#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
1858#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
1859#[serde(deny_unknown_fields)]
1860pub struct MobFlowCancelParams {
1861    pub mob_id: String,
1862    pub run_id: String,
1863}
1864
1865/// Response payload for `mob/flow_cancel`.
1866#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
1867#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
1868pub struct MobFlowCancelResult {
1869    pub canceled: bool,
1870}
1871
1872/// Request payload for `mob/spawn_helper`.
1873#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
1874#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
1875#[serde(deny_unknown_fields)]
1876pub struct MobSpawnHelperParams {
1877    pub mob_id: String,
1878    pub prompt: String,
1879    #[serde(default, skip_serializing_if = "Option::is_none")]
1880    pub agent_identity: Option<String>,
1881    #[serde(default, skip_serializing_if = "Option::is_none")]
1882    pub role_name: Option<String>,
1883    #[serde(default, skip_serializing_if = "Option::is_none")]
1884    pub runtime_mode: Option<WireMobRuntimeMode>,
1885    #[serde(default, skip_serializing_if = "Option::is_none")]
1886    pub backend: Option<WireMobBackendKind>,
1887}
1888
1889/// Request payload for `mob/fork_helper`.
1890#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
1891#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
1892#[serde(deny_unknown_fields)]
1893pub struct MobForkHelperParams {
1894    pub mob_id: String,
1895    pub source_member_id: String,
1896    pub prompt: String,
1897    #[serde(default, skip_serializing_if = "Option::is_none")]
1898    pub agent_identity: Option<String>,
1899    #[serde(default, skip_serializing_if = "Option::is_none")]
1900    pub role_name: Option<String>,
1901    #[serde(default, skip_serializing_if = "Option::is_none")]
1902    pub fork_context: Option<Value>,
1903    #[serde(default, skip_serializing_if = "Option::is_none")]
1904    pub runtime_mode: Option<WireMobRuntimeMode>,
1905    #[serde(default, skip_serializing_if = "Option::is_none")]
1906    pub backend: Option<WireMobBackendKind>,
1907}
1908
1909/// Response payload for `mob/spawn_helper` and `mob/fork_helper`.
1910#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
1911#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
1912pub struct MobHelperResult {
1913    #[serde(default, skip_serializing_if = "Option::is_none")]
1914    pub output: Option<String>,
1915    pub tokens_used: u64,
1916    pub agent_identity: String,
1917    pub member_ref: WireMemberRef,
1918}
1919
1920/// Response payload for `mob/force_cancel`.
1921#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
1922#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
1923pub struct MobForceCancelResult {
1924    pub cancelled: bool,
1925}
1926
1927/// Request payload for `mob/turn_start`.
1928///
1929/// `provider_params` and `auth_binding` carry the canonical Inherit/Set/Clear
1930/// tri-state via [`WireTurnMetadataOverride`]; unknown fields (including the
1931/// retired `clear_*` split wire form) fail closed at the serde boundary via
1932/// `deny_unknown_fields`, which also keeps the emitted JSON Schema's
1933/// `additionalProperties: false` aligned with the deserializer.
1934#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
1935#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
1936#[serde(deny_unknown_fields)]
1937pub struct MobTurnStartParams {
1938    pub mob_id: String,
1939    pub agent_identity: String,
1940    pub prompt: WireContentInput,
1941    #[serde(default, skip_serializing_if = "Option::is_none")]
1942    pub skill_refs: Option<Vec<meerkat_core::skills::SkillRef>>,
1943    #[serde(default, skip_serializing_if = "Option::is_none")]
1944    pub flow_tool_overlay: Option<meerkat_core::service::PublicTurnToolOverlay>,
1945    #[serde(default, skip_serializing_if = "Option::is_none")]
1946    pub additional_instructions: Option<Vec<String>>,
1947    #[serde(default, skip_serializing_if = "Option::is_none")]
1948    pub keep_alive: Option<bool>,
1949    #[serde(default, skip_serializing_if = "Option::is_none")]
1950    pub model: Option<String>,
1951    #[serde(default, skip_serializing_if = "Option::is_none")]
1952    pub provider: Option<String>,
1953    #[serde(default, skip_serializing_if = "Option::is_none")]
1954    pub max_tokens: Option<u32>,
1955    #[serde(default, skip_serializing_if = "Option::is_none")]
1956    pub system_prompt: Option<String>,
1957    #[serde(default, skip_serializing_if = "Option::is_none")]
1958    pub output_schema: Option<Value>,
1959    #[serde(default, skip_serializing_if = "Option::is_none")]
1960    pub structured_output_retries: Option<u32>,
1961    #[serde(default, skip_serializing_if = "Option::is_none")]
1962    pub provider_params:
1963        Option<WireTurnMetadataOverride<crate::wire::runtime::WireProviderParamsOverride>>,
1964    #[serde(default, skip_serializing_if = "Option::is_none")]
1965    pub auth_binding: Option<WireTurnMetadataOverride<WireAuthBindingRef>>,
1966}
1967
1968/// One currently wired peer that is known to be unreachable.
1969#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
1970#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
1971pub struct WireUnreachablePeer {
1972    pub peer: String,
1973    #[serde(default, skip_serializing_if = "Option::is_none")]
1974    pub reason: Option<String>,
1975}
1976
1977/// Live connectivity summary for a member's currently wired peers. Mirrors
1978/// `meerkat_mob::MobPeerConnectivitySnapshot`.
1979#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
1980#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
1981pub struct WirePeerConnectivitySnapshot {
1982    pub reachable_peer_count: usize,
1983    pub unknown_peer_count: usize,
1984    #[serde(default, skip_serializing_if = "Vec::is_empty")]
1985    pub unreachable_peers: Vec<WireUnreachablePeer>,
1986}
1987
1988/// Tri-state peer-connectivity projection for `mob/member_status`.
1989///
1990/// Distinguishes "connectivity is not applicable to this member" (no bridge
1991/// session backs the member) from "the live probe timed out" (the answer is
1992/// transiently unknown) from a resolved connectivity snapshot. The legacy
1993/// `Option<MobPeerConnectivitySnapshot>` projection collapsed both the
1994/// not-applicable and timed-out cases into `None`, laundering a transient
1995/// probe fault into the same shape as a structurally-absent binding.
1996#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
1997#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
1998#[serde(tag = "status", rename_all = "snake_case")]
1999pub enum WirePeerConnectivity {
2000    /// The member has no bridge session, so live peer connectivity is not a
2001    /// resolvable fact for it.
2002    NotApplicable,
2003    /// A live connectivity probe was attempted but did not resolve in time.
2004    ProbeTimedOut,
2005    /// A resolved connectivity snapshot.
2006    Known {
2007        snapshot: WirePeerConnectivitySnapshot,
2008    },
2009}
2010
2011/// Response payload for `mob/member_status`.
2012#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
2013#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
2014pub struct MobMemberStatusResult {
2015    pub status: WireMobMemberStatus,
2016    /// Server-resolved opaque handle for subsequent member-targeted calls.
2017    pub member_ref: WireMemberRef,
2018    #[serde(default, skip_serializing_if = "Option::is_none")]
2019    pub output_preview: Option<String>,
2020    #[serde(default, skip_serializing_if = "Option::is_none")]
2021    pub error: Option<String>,
2022    pub tokens_used: u64,
2023    pub is_final: bool,
2024    #[serde(default, skip_serializing_if = "Option::is_none")]
2025    pub current_session_id: Option<String>,
2026    #[serde(default, skip_serializing_if = "Option::is_none")]
2027    pub peer_connectivity: Option<WirePeerConnectivity>,
2028    #[serde(default, skip_serializing_if = "Option::is_none")]
2029    pub kickoff: Option<Value>,
2030    #[serde(default, skip_serializing_if = "Option::is_none")]
2031    pub external_member: Option<Value>,
2032    #[serde(default, skip_serializing_if = "Option::is_none")]
2033    pub resolved_capabilities: Option<crate::wire::WireResolvedModelCapabilities>,
2034}
2035
2036/// Response payload for `mob/snapshot`.
2037#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
2038#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
2039pub struct MobSnapshotResult {
2040    pub mob_id: String,
2041    pub status: WireMobLifecycleStatus,
2042    pub members: Vec<MobMemberListEntryWire>,
2043}
2044
2045#[cfg(test)]
2046mod member_status_capability_tests {
2047    use super::*;
2048
2049    #[test]
2050    fn member_status_result_round_trips_resolved_capabilities() -> Result<(), serde_json::Error> {
2051        let capabilities = crate::wire::WireResolvedModelCapabilities {
2052            vision: true,
2053            image_input: true,
2054            image_tool_results: false,
2055            inline_video: false,
2056            realtime: true,
2057            web_search: true,
2058            image_generation: true,
2059        };
2060        let result = MobMemberStatusResult {
2061            status: WireMobMemberStatus::Active,
2062            member_ref: WireMemberRef::encode("mob-1", "worker-1"),
2063            output_preview: None,
2064            error: None,
2065            tokens_used: 0,
2066            is_final: false,
2067            current_session_id: Some("session-1".to_string()),
2068            peer_connectivity: Some(WirePeerConnectivity::Known {
2069                snapshot: WirePeerConnectivitySnapshot {
2070                    reachable_peer_count: 1,
2071                    unknown_peer_count: 0,
2072                    unreachable_peers: Vec::new(),
2073                },
2074            }),
2075            kickoff: None,
2076            external_member: None,
2077            resolved_capabilities: Some(capabilities.clone()),
2078        };
2079
2080        let json = serde_json::to_string(&result)?;
2081        assert!(json.contains("\"resolved_capabilities\""));
2082        let parsed: MobMemberStatusResult = serde_json::from_str(&json)?;
2083        assert_eq!(parsed.resolved_capabilities, Some(capabilities));
2084        Ok(())
2085    }
2086}
2087
2088/// Response payload for `mob/destroy`.
2089#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
2090#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
2091pub struct MobDestroyResult {
2092    pub mob_id: String,
2093    pub ok: bool,
2094    pub destroy_report: Value,
2095}
2096
2097/// Response payload for `mob/rotate_supervisor`.
2098#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
2099#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
2100pub struct MobRotateSupervisorResult {
2101    pub mob_id: String,
2102    pub ok: bool,
2103    pub report: SupervisorRotationReportWire,
2104}
2105
2106/// Confirmed supervisor rotation report returned by `mob/rotate_supervisor`.
2107#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
2108#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
2109pub struct SupervisorRotationReportWire {
2110    pub previous_epoch: u64,
2111    pub current_epoch: u64,
2112    pub public_peer_id: String,
2113}
2114
2115/// Discriminator kind for the supervisor-rotation-incomplete error details.
2116#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
2117#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
2118#[serde(rename_all = "snake_case")]
2119pub enum SupervisorRotationIncompleteKind {
2120    SupervisorRotationIncomplete,
2121}
2122
2123/// Which authority a supervisor-rotation retry validates against.
2124#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
2125#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
2126#[serde(rename_all = "snake_case")]
2127pub enum SupervisorRotationRetryAuthority {
2128    PendingRotation,
2129    PreRotation,
2130}
2131
2132/// Durability scope of a supervisor-rotation retry.
2133#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
2134#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
2135#[serde(rename_all = "snake_case")]
2136pub enum SupervisorRotationRetryScope {
2137    Durable,
2138    PreRotation,
2139}
2140
2141/// Typed details of `MobError::SupervisorRotationIncomplete` on the wire.
2142#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
2143#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
2144#[serde(rename_all = "snake_case")]
2145pub struct SupervisorRotationIncompleteDetailsWire {
2146    pub kind: SupervisorRotationIncompleteKind,
2147    pub previous_epoch: u64,
2148    pub attempted_epoch: u64,
2149    pub attempted_public_peer_id: String,
2150    pub rotated_peer_count: usize,
2151    pub rollback_succeeded: bool,
2152    pub pending_authority_recorded: bool,
2153    #[serde(default, skip_serializing_if = "Option::is_none")]
2154    pub rollback_error: Option<String>,
2155    pub retry_authority: SupervisorRotationRetryAuthority,
2156    pub retry_scope: SupervisorRotationRetryScope,
2157}
2158
2159/// JSON-RPC `error.data` payload for an incomplete supervisor rotation.
2160#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
2161#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
2162#[serde(rename_all = "snake_case")]
2163pub struct SupervisorRotationIncompleteDataWire {
2164    pub code: String,
2165    pub message: String,
2166    pub details: SupervisorRotationIncompleteDetailsWire,
2167}
2168
2169/// Shared request payload for mob readiness waits.
2170#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
2171#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
2172#[serde(deny_unknown_fields)]
2173pub struct MobWaitParams {
2174    pub mob_id: String,
2175    #[serde(default, skip_serializing_if = "Option::is_none")]
2176    pub member_ids: Option<Vec<String>>,
2177    #[serde(default, skip_serializing_if = "Option::is_none")]
2178    pub timeout_ms: Option<u64>,
2179}
2180
2181/// Response payload for `mob/wait_kickoff` and `mob/wait_ready`.
2182#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
2183#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
2184pub struct MobWaitMembersResult {
2185    pub members: Vec<Value>,
2186}
2187
2188/// Response payload for `mob/cancel_work`.
2189#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
2190#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
2191pub struct MobCancelWorkResult {
2192    pub mob_id: String,
2193    pub ok: bool,
2194}
2195
2196/// Response payload for `mob/cancel_all_work`.
2197#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
2198#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
2199pub struct MobCancelAllWorkResult {
2200    pub mob_id: String,
2201    pub ok: bool,
2202}
2203
2204/// Request payload for `mob/profile/create`.
2205#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
2206#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
2207#[serde(deny_unknown_fields)]
2208pub struct MobProfileCreateParams {
2209    pub name: String,
2210    pub profile: MobProfileInput,
2211}
2212
2213/// Request payload for `mob/profile/get`.
2214#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
2215#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
2216#[serde(deny_unknown_fields)]
2217pub struct MobProfileNameParams {
2218    pub name: String,
2219}
2220
2221/// Request payload for `mob/profile/update`.
2222#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
2223#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
2224#[serde(deny_unknown_fields)]
2225pub struct MobProfileUpdateParams {
2226    pub name: String,
2227    pub profile: MobProfileInput,
2228    pub expected_revision: u64,
2229}
2230
2231/// Request payload for `mob/profile/delete`.
2232#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
2233#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
2234#[serde(deny_unknown_fields)]
2235pub struct MobProfileDeleteParams {
2236    pub name: String,
2237    pub expected_revision: u64,
2238}
2239
2240/// Stored realm profile projection returned by `mob/profile/*`.
2241#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
2242#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
2243pub struct MobProfileLookupResult {
2244    #[serde(default)]
2245    pub not_found: bool,
2246    pub name: String,
2247    #[serde(default, skip_serializing_if = "Option::is_none")]
2248    pub profile: Option<WireMobProfile>,
2249    #[serde(default, skip_serializing_if = "Option::is_none")]
2250    pub revision: Option<u64>,
2251    #[serde(default, skip_serializing_if = "Option::is_none")]
2252    pub created_at: Option<String>,
2253    #[serde(default, skip_serializing_if = "Option::is_none")]
2254    pub updated_at: Option<String>,
2255}
2256
2257/// Response payload for `mob/profile/list`.
2258#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
2259#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
2260pub struct MobProfileListResult {
2261    pub profiles: Vec<MobProfileLookupResult>,
2262}
2263
2264/// Response payload for `mob/profile/delete`.
2265#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
2266#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
2267pub struct MobProfileDeleteResult {
2268    pub name: String,
2269    pub deleted_revision: u64,
2270}
2271
2272/// Request payload for `mob/stream_open`.
2273#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
2274#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
2275#[serde(deny_unknown_fields)]
2276pub struct MobStreamOpenParams {
2277    pub mob_id: String,
2278    #[serde(default, skip_serializing_if = "Option::is_none")]
2279    pub agent_identity: Option<String>,
2280}
2281
2282/// Response payload for `mob/stream_open`.
2283#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
2284#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
2285pub struct MobStreamOpenResult {
2286    pub stream_id: String,
2287    pub opened: bool,
2288}
2289
2290/// Request payload for `mob/stream_close`.
2291#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
2292#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
2293#[serde(deny_unknown_fields)]
2294pub struct MobStreamCloseParams {
2295    pub stream_id: String,
2296}
2297
2298/// Response payload for `mob/stream_close`.
2299#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
2300#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
2301pub struct MobStreamCloseResult {
2302    pub stream_id: String,
2303    pub closed: bool,
2304    pub already_closed: bool,
2305}
2306
2307/// Origin for `MobSubmitWorkParams`. Replaces the prior free-form
2308/// `origin: Option<String>` shape.
2309#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq, Default)]
2310#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
2311#[serde(rename_all = "snake_case")]
2312pub enum WireWorkOrigin {
2313    #[default]
2314    External,
2315    Internal,
2316}
2317
2318/// Request payload for `mob/submit_work`.
2319///
2320/// Identifies the member through the opaque [`WireMemberRef`] handle the
2321/// server resolves against the live roster — callers do not pass
2322/// `generation` or `fence_token`.
2323#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
2324#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
2325#[serde(deny_unknown_fields)]
2326pub struct MobSubmitWorkParams {
2327    pub member_ref: WireMemberRef,
2328    /// Optional caller-supplied work reference. When absent the server
2329    /// generates a fresh UUID.
2330    #[serde(default, skip_serializing_if = "Option::is_none")]
2331    pub work_ref: Option<String>,
2332    pub content: WireContentInput,
2333    #[serde(default)]
2334    pub origin: WireWorkOrigin,
2335}
2336
2337/// Response payload for `mob/submit_work`.
2338#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
2339#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
2340pub struct MobSubmitWorkResult {
2341    pub mob_id: String,
2342    pub work_ref: String,
2343    pub member_ref: WireMemberRef,
2344}
2345
2346/// Request payload for `mob/cancel_work`.
2347#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
2348#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
2349#[serde(deny_unknown_fields)]
2350pub struct MobCancelWorkParams {
2351    pub mob_id: String,
2352    pub work_ref: String,
2353}
2354
2355/// Request payload for `mob/cancel_all_work`.
2356#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
2357#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
2358#[serde(deny_unknown_fields)]
2359pub struct MobCancelAllWorkParams {
2360    pub member_ref: WireMemberRef,
2361}
2362
2363/// Filter for `mob/list_members_matching`. Non-empty / `Some` fields are
2364/// combined conjunctively; an empty filter matches every member.
2365#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq, Eq)]
2366#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
2367#[serde(deny_unknown_fields)]
2368pub struct MobMemberFilterWire {
2369    /// Required exact matches on member labels.
2370    #[serde(default, skip_serializing_if = "BTreeMap::is_empty")]
2371    pub labels: BTreeMap<String, String>,
2372    /// Required profile name (role).
2373    #[serde(default, skip_serializing_if = "Option::is_none")]
2374    pub role: Option<String>,
2375    /// Required canonical machine-projected member status.
2376    #[serde(default, skip_serializing_if = "Option::is_none")]
2377    pub status: Option<WireMobMemberStatus>,
2378}
2379
2380/// Request payload for `mob/list_members_matching`.
2381#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
2382#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
2383#[serde(deny_unknown_fields)]
2384pub struct MobListMembersMatchingParams {
2385    pub mob_id: String,
2386    #[serde(default)]
2387    pub filter: MobMemberFilterWire,
2388}
2389
2390/// Response payload for `mob/list_members_matching`. Each member is the raw
2391/// roster entry JSON.
2392#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq)]
2393#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
2394pub struct MobListMembersMatchingResult {
2395    #[serde(default)]
2396    pub members: Vec<Value>,
2397}
2398
2399#[cfg(test)]
2400#[allow(clippy::expect_used, clippy::panic)]
2401mod tests {
2402    use super::*;
2403
2404    #[test]
2405    fn wire_mob_profile_parses_provider_fields_fail_closed() {
2406        // Minimal legacy payload (no new fields) still parses.
2407        let legacy: WireMobProfile =
2408            serde_json::from_str(r#"{"model":"claude-opus-4-8"}"#).expect("legacy profile parses");
2409        assert_eq!(legacy.provider, None);
2410        assert!(legacy.resume_overrides.is_empty());
2411
2412        // Typed provider + resume override vocabulary parse into closed enums.
2413        let full: WireMobProfile = serde_json::from_str(
2414            r#"{
2415                "model": "claude-internal-preview",
2416                "provider": "anthropic",
2417                "image_generation_provider": "gemini",
2418                "auto_compact_threshold": 60000,
2419                "resume_overrides": ["model", "provider"]
2420            }"#,
2421        )
2422        .expect("typed profile parses");
2423        assert_eq!(full.provider, Some(meerkat_core::Provider::Anthropic));
2424        assert_eq!(
2425            full.image_generation_provider,
2426            Some(meerkat_core::Provider::Gemini)
2427        );
2428        assert_eq!(
2429            full.resume_overrides,
2430            vec![
2431                WireMobResumeOverrideField::Model,
2432                WireMobResumeOverrideField::Provider
2433            ]
2434        );
2435
2436        // Fail-closed: unknown provider names and zero thresholds reject.
2437        assert!(
2438            serde_json::from_str::<WireMobProfile>(r#"{"model":"m","provider":"not-a-provider"}"#)
2439                .is_err(),
2440            "unknown provider names must fail closed at the wire boundary"
2441        );
2442        assert!(
2443            serde_json::from_str::<WireMobProfile>(r#"{"model":"m","auto_compact_threshold":0}"#)
2444                .is_err(),
2445            "zero auto_compact_threshold must fail closed at the wire boundary"
2446        );
2447        assert!(
2448            serde_json::from_str::<WireMobProfile>(
2449                r#"{"model":"m","resume_overrides":["everything"]}"#
2450            )
2451            .is_err(),
2452            "resume_overrides vocabulary is closed"
2453        );
2454    }
2455
2456    #[test]
2457    fn mob_definition_input_parses_custom_models() {
2458        let input: MobDefinitionInput = serde_json::from_str(
2459            r#"{
2460                "id": "m",
2461                "profiles": {"worker": {"model": "claude-internal-preview"}},
2462                "models": {
2463                    "claude-internal-preview": {
2464                        "provider": "anthropic",
2465                        "context_window": 500000,
2466                        "vision": true
2467                    }
2468                },
2469                "image_generation_provider": "openai"
2470            }"#,
2471        )
2472        .expect("definition with custom models parses");
2473        let model = input
2474            .models
2475            .get("claude-internal-preview")
2476            .expect("custom model present");
2477        assert_eq!(model.provider, meerkat_core::Provider::Anthropic);
2478        assert_eq!(model.context_window, Some(500_000));
2479        assert_eq!(model.vision, Some(true));
2480        assert_eq!(
2481            input.image_generation_provider,
2482            Some(meerkat_core::Provider::OpenAI)
2483        );
2484    }
2485
2486    #[test]
2487    fn wire_member_ref_round_trips_through_encode_decode() {
2488        let token = WireMemberRef::encode("mob-42", "worker-1");
2489        let (mob_id, agent_identity) = token.decode().expect("decode round-trips");
2490        assert_eq!(mob_id, "mob-42");
2491        assert_eq!(agent_identity, "worker-1");
2492    }
2493
2494    #[test]
2495    fn wire_member_ref_rejects_malformed_token() {
2496        let err = WireMemberRef::from_token("not-a-token-payload")
2497            .decode()
2498            .expect_err("malformed tokens must fail to decode");
2499        assert!(matches!(err, WireMemberRefError::Malformed));
2500    }
2501
2502    #[test]
2503    fn mob_member_spec_exposes_shared_surface_metadata() {
2504        let spec = MobMemberSpecWire {
2505            profile: "worker".into(),
2506            agent_identity: "w1".into(),
2507            initial_message: None,
2508            runtime_mode: None,
2509            backend: None,
2510            binding: None,
2511            context: Some(serde_json::json!({"client_ref": "member-card"})),
2512            labels: Some(BTreeMap::from([("client.member_id".into(), "w1".into())])),
2513            additional_instructions: None,
2514            auto_wire_parent: None,
2515        };
2516
2517        let metadata = spec.surface_metadata();
2518        assert_eq!(
2519            metadata.labels.get("client.member_id").map(String::as_str),
2520            Some("w1")
2521        );
2522        assert_eq!(
2523            metadata.app_context,
2524            Some(serde_json::json!({"client_ref": "member-card"}))
2525        );
2526    }
2527
2528    #[test]
2529    fn mob_member_spec_surface_metadata_rejects_reserved_keys() {
2530        let spec = MobMemberSpecWire {
2531            profile: "worker".into(),
2532            agent_identity: "w1".into(),
2533            initial_message: None,
2534            runtime_mode: None,
2535            backend: None,
2536            binding: None,
2537            context: None,
2538            labels: Some(BTreeMap::from([("mob_id".into(), "spoof".into())])),
2539            additional_instructions: None,
2540            auto_wire_parent: None,
2541        };
2542
2543        assert!(spec.validate_public_surface_metadata().is_err());
2544    }
2545
2546    #[test]
2547    fn mob_reconcile_failure_stage_is_typed_wire_enum() {
2548        let failure = MobReconcileFailureWire {
2549            agent_identity: "worker-1".into(),
2550            stage: WireMobReconcileStage::Spawn,
2551            error: WireMobError {
2552                code: MobSpawnManyFailureCause::ProfileNotFound,
2553                message: "spawn failed".into(),
2554            },
2555        };
2556
2557        let json = serde_json::to_value(&failure).expect("serialize failure");
2558        assert_eq!(json["stage"], "spawn");
2559        assert_eq!(json["error"]["code"], "profile_not_found");
2560        assert_eq!(json["error"]["message"], "spawn failed");
2561
2562        let round_trip: MobReconcileFailureWire =
2563            serde_json::from_value(json).expect("deserialize failure");
2564        assert_eq!(round_trip.stage, WireMobReconcileStage::Spawn);
2565        assert_eq!(
2566            round_trip.error.code,
2567            MobSpawnManyFailureCause::ProfileNotFound
2568        );
2569
2570        let err = serde_json::from_value::<MobReconcileFailureWire>(serde_json::json!({
2571            "agent_identity": "worker-1",
2572            "stage": "restart",
2573            "error": { "code": "profile_not_found", "message": "bad stage" }
2574        }))
2575        .expect_err("unknown reconcile stage must be rejected");
2576        assert!(err.to_string().contains("unknown variant"));
2577    }
2578
2579    #[test]
2580    fn mob_lifecycle_params_reject_unknown_action_string() {
2581        let err = serde_json::from_value::<MobLifecycleParams>(serde_json::json!({
2582            "mob_id": "mob-1",
2583            "action": "explode"
2584        }))
2585        .expect_err("unknown lifecycle actions must fail at the typed wire boundary");
2586
2587        assert!(
2588            err.to_string().contains("unknown variant"),
2589            "unexpected error: {err}"
2590        );
2591    }
2592
2593    #[test]
2594    fn mob_lifecycle_result_round_trips_typed_action() {
2595        let result = MobLifecycleResult {
2596            mob_id: "mob-1".into(),
2597            action: WireMobLifecycleAction::Complete,
2598            ok: true,
2599            destroy_report: None,
2600        };
2601
2602        let json = serde_json::to_value(&result).expect("serialize lifecycle result");
2603        assert_eq!(json["action"], "complete");
2604
2605        let round_trip: MobLifecycleResult =
2606            serde_json::from_value(json).expect("deserialize lifecycle result");
2607        assert_eq!(round_trip.action, WireMobLifecycleAction::Complete);
2608    }
2609
2610    #[test]
2611    fn mob_wire_members_batch_contract_is_local_edge_native() {
2612        let params: MobWireMembersBatchParams = serde_json::from_value(serde_json::json!({
2613            "mob_id": "mob-1",
2614            "edges": [
2615                { "a": "lead", "b": "worker-b" },
2616                { "a": "worker-a", "b": "lead" }
2617            ]
2618        }))
2619        .expect("batch wire params deserialize");
2620
2621        assert_eq!(params.mob_id, "mob-1");
2622        assert_eq!(params.edges.len(), 2);
2623        assert_eq!(params.edges[0].a, "lead");
2624        assert_eq!(params.edges[0].b, "worker-b");
2625
2626        let result = MobWireMembersBatchResult {
2627            requested: 2,
2628            wired: vec![MobWireMembersBatchEdge {
2629                a: "lead".into(),
2630                b: "worker-a".into(),
2631            }],
2632            already_wired: vec![MobWireMembersBatchEdge {
2633                a: "lead".into(),
2634                b: "worker-b".into(),
2635            }],
2636        };
2637        let json = serde_json::to_value(&result).expect("serialize batch wire result");
2638        assert_eq!(json["requested"], 2);
2639        assert_eq!(json["wired"][0]["a"], "lead");
2640        assert_eq!(json["already_wired"][0]["b"], "worker-b");
2641
2642        let err = serde_json::from_value::<MobWireMembersBatchParams>(serde_json::json!({
2643            "mob_id": "mob-1",
2644            "edges": [{ "member": "lead", "peer": "worker-a" }]
2645        }))
2646        .expect_err("mixed local/external mob/wire shape must not deserialize");
2647        let message = err.to_string();
2648        assert!(
2649            message.contains("unknown field `member`") || message.contains("missing field `a`"),
2650            "unexpected error: {message}"
2651        );
2652    }
2653
2654    #[test]
2655    fn mob_spawn_many_result_entry_uses_typed_status_result_envelope() {
2656        let member_ref = WireMemberRef::encode("mob-1", "worker-1");
2657        let entry = MobSpawnManyResultEntry::spawned("worker-1", member_ref.clone());
2658
2659        let json = serde_json::to_value(&entry).expect("serialize typed spawn_many row");
2660        assert_eq!(json["status"], "spawned");
2661        assert_eq!(json["result"]["agent_identity"], "worker-1");
2662        assert_eq!(json["result"]["member_ref"], member_ref.as_str());
2663        assert!(json.get("ok").is_none());
2664        assert!(json.get("error").is_none());
2665
2666        let round_trip: MobSpawnManyResultEntry =
2667            serde_json::from_value(json).expect("deserialize typed spawn_many row");
2668        assert_eq!(round_trip, entry);
2669
2670        let failed = MobSpawnManyResultEntry::failed(
2671            MobSpawnManyFailureCause::ProfileNotFound,
2672            "profile missing",
2673        );
2674        let json = serde_json::to_value(&failed).expect("serialize typed failed spawn_many row");
2675        assert_eq!(json["status"], "failed");
2676        assert_eq!(json["result"]["cause"], "profile_not_found");
2677        assert_eq!(json["result"]["message"], "profile missing");
2678        assert!(json.get("ok").is_none());
2679        assert!(json.get("error").is_none());
2680
2681        let round_trip: MobSpawnManyResultEntry =
2682            serde_json::from_value(json).expect("deserialize typed failed spawn_many row");
2683        assert_eq!(round_trip, failed);
2684    }
2685
2686    #[test]
2687    fn mob_spawn_many_result_entry_rejects_legacy_or_malformed_envelopes() {
2688        let legacy = serde_json::json!({
2689            "ok": true,
2690            "agent_identity": "worker-1",
2691            "member_ref": WireMemberRef::encode("mob-1", "worker-1"),
2692        });
2693        let err = serde_json::from_value::<MobSpawnManyResultEntry>(legacy)
2694            .expect_err("legacy ok carrier must not deserialize");
2695        assert!(
2696            err.to_string().contains("missing field `status`")
2697                || err.to_string().contains("unknown field"),
2698            "unexpected error: {err}"
2699        );
2700
2701        let missing_result = serde_json::json!({
2702            "status": "spawned"
2703        });
2704        let err = serde_json::from_value::<MobSpawnManyResultEntry>(missing_result)
2705            .expect_err("missing typed result must fail closed");
2706        assert!(
2707            err.to_string().contains("missing field `result`"),
2708            "unexpected error: {err}"
2709        );
2710
2711        let unknown_status = serde_json::json!({
2712            "status": "ok",
2713            "result": {
2714                "agent_identity": "worker-1",
2715                "member_ref": WireMemberRef::encode("mob-1", "worker-1"),
2716            }
2717        });
2718        let err = serde_json::from_value::<MobSpawnManyResultEntry>(unknown_status)
2719            .expect_err("unknown typed status must fail closed");
2720        assert!(
2721            err.to_string().contains("unknown variant"),
2722            "unexpected error: {err}"
2723        );
2724
2725        let mismatched = serde_json::json!({
2726            "status": "spawned",
2727            "result": {
2728                "cause": "profile_not_found",
2729                "message": "profile missing"
2730            }
2731        });
2732        let err = serde_json::from_value::<MobSpawnManyResultEntry>(mismatched)
2733            .expect_err("status/result mismatch must fail closed");
2734        assert!(
2735            err.to_string()
2736                .contains("status spawned requires spawned result"),
2737            "unexpected error: {err}"
2738        );
2739
2740        let message_only_failure = serde_json::json!({
2741            "status": "failed",
2742            "result": {
2743                "message": "profile missing"
2744            }
2745        });
2746        let err = serde_json::from_value::<MobSpawnManyResultEntry>(message_only_failure)
2747            .expect_err("string-only failure result must fail closed");
2748        assert!(
2749            err.to_string().contains("data did not match any variant")
2750                || err.to_string().contains("missing field `cause`"),
2751            "unexpected error: {err}"
2752        );
2753
2754        let unknown_failure_cause = serde_json::json!({
2755            "status": "failed",
2756            "result": {
2757                "cause": "future_failure",
2758                "message": "future failure"
2759            }
2760        });
2761        let err = serde_json::from_value::<MobSpawnManyResultEntry>(unknown_failure_cause)
2762            .expect_err("unknown failure cause must fail closed");
2763        assert!(
2764            err.to_string().contains("data did not match any variant")
2765                || err.to_string().contains("unknown variant"),
2766            "unexpected error: {err}"
2767        );
2768    }
2769
2770    #[test]
2771    fn mob_wire_params_reject_legacy_local_target_shape() {
2772        let err = serde_json::from_value::<MobWireParams>(serde_json::json!({
2773            "mob_id": "mob-1",
2774            "local": "member-a",
2775            "target": { "local": "member-b" }
2776        }))
2777        .expect_err("legacy local/target shape must be rejected");
2778
2779        let msg = err.to_string();
2780        assert!(
2781            msg.contains("unknown field `local`") || msg.contains("missing field `member`"),
2782            "unexpected error: {msg}"
2783        );
2784    }
2785
2786    #[test]
2787    fn mob_wire_params_accept_canonical_external_peer_identity() {
2788        let params = serde_json::from_value::<MobWireParams>(serde_json::json!({
2789            "mob_id": "mob-1",
2790            "member": "member-a",
2791            "peer": {
2792                "external": {
2793                    "name": "external-worker",
2794                    "address": "inproc://external-worker",
2795                    "identity": {
2796                        "kind": "ed25519_public_key",
2797                        "public_key": "ed25519:BwcHBwcHBwcHBwcHBwcHBwcHBwcHBwcHBwcHBwcHBwc="
2798                    }
2799                }
2800            }
2801        }))
2802        .expect("canonical external peer identity should deserialize");
2803
2804        let MobPeerTarget::External(spec) = params.peer else {
2805            panic!("expected external peer target");
2806        };
2807        assert_eq!(spec.name, "external-worker");
2808    }
2809
2810    #[test]
2811    fn mob_wire_params_reject_raw_external_peer_id_shape() {
2812        let err = serde_json::from_value::<MobWireParams>(serde_json::json!({
2813            "mob_id": "mob-1",
2814            "member": "member-a",
2815            "peer": {
2816                "external": {
2817                    "name": "external-worker",
2818                    "peer_id": meerkat_core::comms::PeerId::from_ed25519_pubkey(&[7u8; 32]).to_string(),
2819                    "address": "inproc://external-worker",
2820                    "pubkey": vec![7u8; 32]
2821                }
2822            }
2823        }))
2824        .expect_err("raw peer_id/pubkey external peer shape must be rejected");
2825
2826        let msg = err.to_string();
2827        assert!(
2828            msg.contains("peer_id") || msg.contains("identity"),
2829            "unexpected error: {msg}"
2830        );
2831    }
2832
2833    #[test]
2834    fn mob_wire_params_reject_missing_external_peer_pubkey_material() {
2835        let err = serde_json::from_value::<MobWireParams>(serde_json::json!({
2836            "mob_id": "mob-1",
2837            "member": "member-a",
2838            "peer": {
2839                "external": {
2840                    "name": "external-worker",
2841                    "address": "inproc://external-worker",
2842                    "identity": {
2843                        "kind": "ed25519_public_key"
2844                    }
2845                }
2846            }
2847        }))
2848        .expect_err("missing external peer pubkey material must fail closed");
2849
2850        let msg = err.to_string();
2851        assert!(
2852            msg.contains("public_key") || msg.contains("identity"),
2853            "unexpected error: {msg}"
2854        );
2855    }
2856
2857    #[test]
2858    fn runtime_binding_accepts_canonical_external_peer_identity() {
2859        let binding = serde_json::from_value::<WireRuntimeBinding>(serde_json::json!({
2860            "kind": "external",
2861            "address": "inproc://external-worker",
2862            "identity": {
2863                "kind": "ed25519_public_key",
2864                "public_key": "ed25519:BwcHBwcHBwcHBwcHBwcHBwcHBwcHBwcHBwcHBwcHBwc="
2865            }
2866        }))
2867        .expect("canonical external runtime binding identity should deserialize");
2868
2869        let WireRuntimeBinding::External {
2870            identity, address, ..
2871        } = binding
2872        else {
2873            panic!("expected external runtime binding");
2874        };
2875        assert_eq!(address, "inproc://external-worker");
2876        assert_eq!(
2877            identity.resolve().expect("identity resolves").pubkey,
2878            [7u8; 32]
2879        );
2880    }
2881
2882    #[test]
2883    fn runtime_binding_rejects_raw_external_peer_id_shape() {
2884        let err = serde_json::from_value::<WireRuntimeBinding>(serde_json::json!({
2885            "kind": "external",
2886            "peer_id": meerkat_core::comms::PeerId::from_ed25519_pubkey(&[7u8; 32]).to_string(),
2887            "address": "inproc://external-worker",
2888            "pubkey": vec![7u8; 32]
2889        }))
2890        .expect_err("raw peer_id/pubkey external runtime binding shape must be rejected");
2891
2892        let msg = err.to_string();
2893        assert!(
2894            msg.contains("peer_id") || msg.contains("identity"),
2895            "unexpected error: {msg}"
2896        );
2897    }
2898
2899    #[test]
2900    fn runtime_binding_rejects_missing_external_peer_pubkey_material() {
2901        let err = serde_json::from_value::<WireRuntimeBinding>(serde_json::json!({
2902            "kind": "external",
2903            "address": "inproc://external-worker",
2904            "identity": {
2905                "kind": "ed25519_public_key"
2906            }
2907        }))
2908        .expect_err("missing external runtime binding pubkey material must fail closed");
2909
2910        let msg = err.to_string();
2911        assert!(
2912            msg.contains("public_key") || msg.contains("identity"),
2913            "unexpected error: {msg}"
2914        );
2915    }
2916
2917    #[test]
2918    fn mob_turn_start_params_capture_turn_override_fields() {
2919        let params = serde_json::from_value::<MobTurnStartParams>(serde_json::json!({
2920            "mob_id": "mob-1",
2921            "agent_identity": "worker",
2922            "prompt": "continue",
2923            "output_schema": { "type": "object" },
2924            "structured_output_retries": 2
2925        }))
2926        .expect("turn_start should accept explicit turn override fields");
2927
2928        assert_eq!(params.mob_id, "mob-1");
2929        assert_eq!(params.agent_identity, "worker");
2930        assert_eq!(params.prompt, WireContentInput::Text("continue".into()));
2931        assert_eq!(
2932            params.output_schema,
2933            Some(serde_json::json!({ "type": "object" }))
2934        );
2935        assert_eq!(params.structured_output_retries, Some(2));
2936
2937        let err = serde_json::from_value::<MobTurnStartParams>(serde_json::json!({
2938            "mob_id": "mob-1",
2939            "agent_identity": "worker",
2940            "prompt": "continue",
2941            "unknown_override": true
2942        }))
2943        .expect_err("turn_start must reject unknown override fields");
2944        assert!(
2945            err.to_string().contains("unknown field"),
2946            "unexpected error: {err}"
2947        );
2948    }
2949
2950    #[test]
2951    fn mob_create_params_reject_reserved_runtime_lifecycle_fields() {
2952        let err = serde_json::from_value::<MobCreateParams>(serde_json::json!({
2953            "definition": {
2954                "id": "mob-1",
2955                "owner_runtime_binding": "runtime:worker:0",
2956                "profiles": {
2957                    "worker": { "model": "claude-sonnet-4-6" }
2958                }
2959            }
2960        }))
2961        .expect_err("reserved runtime lifecycle fields must be rejected");
2962
2963        assert!(
2964            err.to_string()
2965                .contains("unknown field `owner_runtime_binding`"),
2966            "unexpected error: {err}"
2967        );
2968    }
2969
2970    #[test]
2971    fn mob_create_params_reject_reserved_runtime_bridge_owner_field() {
2972        let err = serde_json::from_value::<MobCreateParams>(serde_json::json!({
2973            "definition": {
2974                "id": "mob-1",
2975                "owner_transport_binding": "transport:worker:0",
2976                "profiles": {
2977                    "worker": { "model": "claude-sonnet-4-6" }
2978                }
2979            }
2980        }))
2981        .expect_err("reserved runtime bridge owner field must be rejected");
2982
2983        assert!(
2984            err.to_string()
2985                .contains("unknown field `owner_transport_binding`"),
2986            "unexpected error: {err}"
2987        );
2988    }
2989
2990    #[test]
2991    fn mob_create_params_reject_internal_profile_tool_bundles() {
2992        let err = serde_json::from_value::<MobCreateParams>(serde_json::json!({
2993            "definition": {
2994                "id": "mob-1",
2995                "profiles": {
2996                    "worker": {
2997                        "model": "claude-sonnet-4-6",
2998                        "tools": {
2999                            "rust_bundles": ["internal-only"]
3000                        }
3001                    }
3002                }
3003            }
3004        }))
3005        .expect_err("internal rust tool bundles must be rejected");
3006
3007        // With untagged MobProfileBindingInput, the error message is about
3008        // no variant matching rather than the specific unknown field.
3009        assert!(
3010            err.to_string().contains("did not match any variant")
3011                || err.to_string().contains("unknown field `rust_bundles`"),
3012            "unexpected error: {err}"
3013        );
3014    }
3015
3016    #[test]
3017    fn mob_create_params_accept_typed_nested_flow_definition() {
3018        let params = serde_json::from_value::<MobCreateParams>(serde_json::json!({
3019            "definition": {
3020                "id": "mob-1",
3021                "profiles": {
3022                    "worker": { "model": "claude-sonnet-4-6" }
3023                },
3024                "flows": {
3025                    "review": {
3026                        "description": "review flow",
3027                        "steps": {
3028                            "draft": {
3029                                "role": "worker",
3030                                "message": "draft it"
3031                            }
3032                        }
3033                    }
3034                }
3035            }
3036        }))
3037        .expect("typed nested flow definition should parse");
3038
3039        assert_eq!(
3040            params.definition.flows["review"].steps["draft"].role,
3041            "worker"
3042        );
3043    }
3044}