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 model_override: Option<String>,
720    #[serde(default, skip_serializing_if = "Option::is_none")]
721    pub auth_binding: Option<WireAuthBindingRef>,
722}
723
724/// Response payload for `mob/spawn`.
725#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
726#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
727pub struct MobSpawnResult {
728    pub mob_id: String,
729    pub agent_identity: String,
730    pub member_ref: WireMemberRef,
731}
732
733/// Per-member request payload inside `mob/spawn_many`.
734#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
735#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
736#[serde(deny_unknown_fields)]
737pub struct MobSpawnSpecParams {
738    pub profile: String,
739    pub agent_identity: String,
740    #[serde(default, skip_serializing_if = "Option::is_none")]
741    pub initial_message: Option<WireContentInput>,
742    #[serde(default, skip_serializing_if = "Option::is_none")]
743    pub runtime_mode: Option<WireMobRuntimeMode>,
744    #[serde(default, skip_serializing_if = "Option::is_none")]
745    pub backend: Option<WireMobBackendKind>,
746    #[serde(default, skip_serializing_if = "Option::is_none")]
747    pub labels: Option<BTreeMap<String, String>>,
748    #[serde(default, skip_serializing_if = "Option::is_none")]
749    pub context: Option<Value>,
750    #[serde(default, skip_serializing_if = "Option::is_none")]
751    pub additional_instructions: Option<Vec<String>>,
752    #[serde(default, skip_serializing_if = "Option::is_none")]
753    pub model_override: Option<String>,
754    #[serde(default, skip_serializing_if = "Option::is_none")]
755    pub auth_binding: Option<WireAuthBindingRef>,
756}
757
758/// Request payload for `mob/spawn_many`.
759#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
760#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
761#[serde(deny_unknown_fields)]
762pub struct MobSpawnManyParams {
763    pub mob_id: String,
764    pub specs: Vec<MobSpawnSpecParams>,
765}
766
767/// Typed status for one `mob/spawn_many` row.
768#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
769#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
770#[serde(rename_all = "snake_case")]
771pub enum MobSpawnManyResultStatus {
772    Spawned,
773    Failed,
774}
775
776/// Successful per-member `mob/spawn_many` result payload.
777#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
778#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
779#[serde(deny_unknown_fields)]
780pub struct MobSpawnManySpawnedResult {
781    pub agent_identity: String,
782    pub member_ref: WireMemberRef,
783}
784
785/// Typed failure cause for one failed `mob/spawn_many` member row.
786#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
787#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
788#[serde(rename_all = "snake_case")]
789pub enum MobSpawnManyFailureCause {
790    ProfileNotFound,
791    MemberNotFound,
792    MemberAlreadyExists,
793    NotExternallyAddressable,
794    InvalidTransition,
795    WiringError,
796    BridgeCommandRejected,
797    MemberRestoreFailed,
798    KickoffWaitTimedOut,
799    ReadyWaitTimedOut,
800    DefinitionError,
801    FlowNotFound,
802    FlowFailed,
803    RunNotFound,
804    RunCanceled,
805    FlowTurnTimedOut,
806    FrameDepthLimitExceeded,
807    FrameAtomicPersistenceUnavailable,
808    SpecRevisionConflict,
809    SchemaValidation,
810    InsufficientTargets,
811    TopologyViolation,
812    BridgeDeliveryRejected,
813    SupervisorEscalation,
814    UnsupportedForMode,
815    MissingMemberCapability,
816    ResetBarrier,
817    StorageError,
818    SessionError,
819    CommsError,
820    CallbackPending,
821    StaleFenceToken,
822    StaleEventCursor,
823    WorkNotFound,
824    Internal,
825}
826
827/// Failed per-member `mob/spawn_many` result payload.
828#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
829#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
830#[serde(deny_unknown_fields)]
831pub struct MobSpawnManyFailedResult {
832    pub cause: MobSpawnManyFailureCause,
833    pub message: String,
834}
835
836/// Typed payload for one `mob/spawn_many` row.
837#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
838#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
839#[serde(untagged)]
840pub enum MobSpawnManyResultPayload {
841    Spawned(MobSpawnManySpawnedResult),
842    Failed(MobSpawnManyFailedResult),
843}
844
845/// One typed result entry in a `mob/spawn_many` response.
846#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
847#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
848#[serde(try_from = "MobSpawnManyResultEntryRaw")]
849pub struct MobSpawnManyResultEntry {
850    pub status: MobSpawnManyResultStatus,
851    pub result: MobSpawnManyResultPayload,
852}
853
854#[derive(Debug, Deserialize)]
855#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
856#[serde(deny_unknown_fields)]
857struct MobSpawnManyResultEntryRaw {
858    status: MobSpawnManyResultStatus,
859    result: MobSpawnManyResultPayload,
860}
861
862impl TryFrom<MobSpawnManyResultEntryRaw> for MobSpawnManyResultEntry {
863    type Error = String;
864
865    fn try_from(raw: MobSpawnManyResultEntryRaw) -> Result<Self, Self::Error> {
866        let entry = Self {
867            status: raw.status,
868            result: raw.result,
869        };
870        entry.validate().map_err(str::to_owned)?;
871        Ok(entry)
872    }
873}
874
875impl MobSpawnManyResultEntry {
876    pub fn spawned(agent_identity: impl Into<String>, member_ref: WireMemberRef) -> Self {
877        Self {
878            status: MobSpawnManyResultStatus::Spawned,
879            result: MobSpawnManyResultPayload::Spawned(MobSpawnManySpawnedResult {
880                agent_identity: agent_identity.into(),
881                member_ref,
882            }),
883        }
884    }
885
886    pub fn failed(cause: MobSpawnManyFailureCause, message: impl Into<String>) -> Self {
887        Self {
888            status: MobSpawnManyResultStatus::Failed,
889            result: MobSpawnManyResultPayload::Failed(MobSpawnManyFailedResult {
890                cause,
891                message: message.into(),
892            }),
893        }
894    }
895
896    pub fn validate(&self) -> Result<(), &'static str> {
897        match (&self.status, &self.result) {
898            (MobSpawnManyResultStatus::Spawned, MobSpawnManyResultPayload::Spawned(_))
899            | (MobSpawnManyResultStatus::Failed, MobSpawnManyResultPayload::Failed(_)) => Ok(()),
900            (MobSpawnManyResultStatus::Spawned, MobSpawnManyResultPayload::Failed(_)) => {
901                Err("mob spawn_many result status spawned requires spawned result")
902            }
903            (MobSpawnManyResultStatus::Failed, MobSpawnManyResultPayload::Spawned(_)) => {
904                Err("mob spawn_many result status failed requires failed result")
905            }
906        }
907    }
908}
909
910/// Response payload for `mob/spawn_many`.
911#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
912#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
913pub struct MobSpawnManyResult {
914    pub results: Vec<MobSpawnManyResultEntry>,
915}
916
917/// Response payload for `mob/retire`.
918#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
919#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
920pub struct MobRetireResult {
921    pub retired: bool,
922}
923
924/// Request payload for `mob/respawn`.
925#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
926#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
927#[serde(deny_unknown_fields)]
928pub struct MobRespawnParams {
929    pub mob_id: String,
930    pub agent_identity: String,
931    #[serde(default, skip_serializing_if = "Option::is_none")]
932    pub initial_message: Option<WireContentInput>,
933}
934
935/// Identity-native respawn receipt returned inside `MobRespawnResult`.
936#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
937#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
938pub struct MobRespawnReceipt {
939    pub identity: String,
940    pub member_ref: WireMemberRef,
941}
942
943/// Outcome of a `mob/respawn` call. Mirrors the success vs
944/// `MobRespawnError::TopologyRestoreFailed` distinction as a closed type so SDK
945/// consumers branch on a typed variant instead of re-deriving meaning from a
946/// free-form status string.
947#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
948#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
949#[serde(rename_all = "snake_case")]
950pub enum WireMobRespawnOutcome {
951    Completed,
952    TopologyRestoreFailed,
953}
954
955/// Response payload for `mob/respawn`.
956#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
957#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
958pub struct MobRespawnResult {
959    pub status: WireMobRespawnOutcome,
960    pub receipt: MobRespawnReceipt,
961    #[serde(default, skip_serializing_if = "Vec::is_empty")]
962    pub failed_peer_ids: Vec<String>,
963}
964
965/// Response payload for `mob/members`.
966#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
967#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
968pub struct MobMembersResult {
969    pub mob_id: String,
970    pub members: Vec<MobMemberListEntryWire>,
971}
972
973/// Request payload for `mob/events`.
974#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
975#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
976#[serde(deny_unknown_fields)]
977pub struct MobEventsParams {
978    pub mob_id: String,
979    #[serde(default)]
980    pub after_cursor: u64,
981    #[serde(default = "default_mob_events_limit")]
982    pub limit: usize,
983    #[serde(default)]
984    pub strict: bool,
985}
986
987const fn default_mob_events_limit() -> usize {
988    100
989}
990
991/// Response payload for `mob/events`.
992#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
993#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
994pub struct MobEventsResult {
995    pub events: Vec<Value>,
996}
997
998/// Typed external peer identity for public mob wiring surfaces.
999#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
1000#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
1001#[serde(tag = "kind", rename_all = "snake_case", deny_unknown_fields)]
1002pub enum WireTrustedPeerIdentity {
1003    /// Recoverable Ed25519 public key string in `ed25519:<base64>` form.
1004    Ed25519PublicKey { public_key: String },
1005}
1006
1007/// Resolved external peer identity atoms used after the wire boundary.
1008#[derive(Debug, Clone, Copy, PartialEq, Eq)]
1009pub struct ResolvedWireTrustedPeerIdentity {
1010    pub peer_id: meerkat_core::comms::PeerId,
1011    pub pubkey: [u8; 32],
1012}
1013
1014/// Failure modes for resolving a typed external peer identity.
1015#[derive(Debug, Clone, thiserror::Error, PartialEq, Eq)]
1016pub enum WireTrustedPeerIdentityError {
1017    #[error("external peer identity public_key must start with 'ed25519:'")]
1018    MissingEd25519Prefix,
1019    #[error("external peer identity public_key is not valid base64: {0}")]
1020    InvalidBase64(String),
1021    #[error("external peer identity public_key must decode to 32 bytes, got {actual}")]
1022    InvalidLength { actual: usize },
1023    #[error("external peer identity public_key must be non-zero")]
1024    ZeroPublicKey,
1025}
1026
1027impl WireTrustedPeerIdentity {
1028    pub fn resolve(&self) -> Result<ResolvedWireTrustedPeerIdentity, WireTrustedPeerIdentityError> {
1029        match self {
1030            Self::Ed25519PublicKey { public_key } => {
1031                let pubkey = parse_ed25519_public_key(public_key)?;
1032                if pubkey == [0u8; 32] {
1033                    return Err(WireTrustedPeerIdentityError::ZeroPublicKey);
1034                }
1035                Ok(ResolvedWireTrustedPeerIdentity {
1036                    peer_id: meerkat_core::comms::PeerId::from_ed25519_pubkey(&pubkey),
1037                    pubkey,
1038                })
1039            }
1040        }
1041    }
1042}
1043
1044fn parse_ed25519_public_key(raw: &str) -> Result<[u8; 32], WireTrustedPeerIdentityError> {
1045    const PREFIX: &str = "ed25519:";
1046    let encoded = raw
1047        .strip_prefix(PREFIX)
1048        .ok_or(WireTrustedPeerIdentityError::MissingEd25519Prefix)?;
1049    let bytes = BASE64
1050        .decode(encoded)
1051        .map_err(|err| WireTrustedPeerIdentityError::InvalidBase64(err.to_string()))?;
1052    let actual = bytes.len();
1053    let pubkey: [u8; 32] = bytes
1054        .try_into()
1055        .map_err(|_| WireTrustedPeerIdentityError::InvalidLength { actual })?;
1056    Ok(pubkey)
1057}
1058
1059/// Minimal trusted peer spec for public mob wiring surfaces.
1060///
1061/// `identity` is required and resolves to the Ed25519 signing public key
1062/// plus the canonical comms `PeerId` derived from that key. MCP callers do
1063/// not provide raw peer IDs, and missing key material fails at the boundary.
1064#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
1065#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
1066#[serde(deny_unknown_fields)]
1067pub struct WireTrustedPeerSpec {
1068    pub name: String,
1069    pub address: String,
1070    pub identity: WireTrustedPeerIdentity,
1071}
1072
1073/// Target for a mob wire/unwire call.
1074#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
1075#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
1076#[serde(rename_all = "snake_case")]
1077pub enum MobPeerTarget {
1078    Local(String),
1079    External(WireTrustedPeerSpec),
1080}
1081
1082/// Request payload for `mob/wire`.
1083#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
1084#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
1085#[serde(deny_unknown_fields)]
1086pub struct MobWireParams {
1087    pub mob_id: String,
1088    pub member: String,
1089    pub peer: MobPeerTarget,
1090}
1091
1092/// Response payload for `mob/wire`.
1093#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
1094#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
1095pub struct MobWireResult {
1096    pub wired: bool,
1097}
1098
1099/// One local-member edge in `mob/wire_members_batch`.
1100#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
1101#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
1102#[serde(deny_unknown_fields)]
1103pub struct MobWireMembersBatchEdge {
1104    pub a: String,
1105    pub b: String,
1106}
1107
1108/// Request payload for `mob/wire_members_batch`.
1109#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
1110#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
1111#[serde(deny_unknown_fields)]
1112pub struct MobWireMembersBatchParams {
1113    pub mob_id: String,
1114    pub edges: Vec<MobWireMembersBatchEdge>,
1115}
1116
1117/// Response payload for `mob/wire_members_batch`.
1118#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
1119#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
1120pub struct MobWireMembersBatchResult {
1121    pub requested: usize,
1122    pub wired: Vec<MobWireMembersBatchEdge>,
1123    pub already_wired: Vec<MobWireMembersBatchEdge>,
1124}
1125
1126/// Request payload for `mob/unwire`.
1127#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
1128#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
1129#[serde(deny_unknown_fields)]
1130pub struct MobUnwireParams {
1131    pub mob_id: String,
1132    pub member: String,
1133    pub peer: MobPeerTarget,
1134}
1135
1136/// Response payload for `mob/unwire`.
1137#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
1138#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
1139pub struct MobUnwireResult {
1140    pub unwired: bool,
1141}
1142
1143/// Request payload for host-side mob member delivery.
1144#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
1145#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
1146#[serde(deny_unknown_fields)]
1147pub struct MobMemberSendParams {
1148    pub mob_id: String,
1149    pub agent_identity: String,
1150    pub content: WireContentInput,
1151    #[serde(default)]
1152    pub handling_mode: WireHandlingMode,
1153    #[serde(default, skip_serializing_if = "Option::is_none")]
1154    pub render_metadata: Option<WireRenderMetadata>,
1155}
1156
1157/// Response payload for host-side mob member delivery.
1158#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
1159#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
1160pub struct WireAgentRuntimeId {
1161    pub identity: String,
1162    pub generation: u64,
1163}
1164
1165/// Response payload for host-side mob member delivery.
1166#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
1167#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
1168pub struct MobMemberSendResult {
1169    pub mob_id: String,
1170    /// Identity-native member identity (0.6).
1171    pub agent_identity: String,
1172    /// Server-resolved opaque handle for subsequent member-targeted calls.
1173    /// App code routes through `member_ref`; the binding-era
1174    /// `{identity, generation}` pair carried by `WireAgentRuntimeId` is
1175    /// retired from app-facing responses per dogma #10.
1176    pub member_ref: WireMemberRef,
1177    pub handling_mode: WireHandlingMode,
1178}
1179
1180/// Request payload for `mob/ingress_interaction`.
1181///
1182/// This is the ergonomic "ensure an ingress member, then deliver user input"
1183/// path. It composes the existing declarative roster and member-send
1184/// semantics without introducing a separate thread/project runtime.
1185#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
1186#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
1187#[serde(deny_unknown_fields)]
1188pub struct MobIngressInteractionParams {
1189    pub mob_id: String,
1190    pub spec: MobMemberSpecWire,
1191    pub content: WireContentInput,
1192    #[serde(default)]
1193    pub handling_mode: WireHandlingMode,
1194    #[serde(default, skip_serializing_if = "Option::is_none")]
1195    pub render_metadata: Option<WireRenderMetadata>,
1196}
1197
1198/// Response payload for `mob/ingress_interaction`.
1199#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
1200#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
1201pub struct MobIngressInteractionResult {
1202    pub mob_id: String,
1203    pub agent_identity: String,
1204    pub member_ref: WireMemberRef,
1205    pub ensure_outcome: MobEnsureMemberOutcomeWire,
1206    pub delivery: MobMemberSendResult,
1207    /// Cursor observed immediately before the ensure/send composition.
1208    pub events_after_cursor: u64,
1209    /// Cursor observed after delivery was accepted.
1210    pub latest_event_cursor: u64,
1211}
1212
1213/// Public handling mode for mob member delivery.
1214#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq, Default)]
1215#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
1216#[serde(rename_all = "snake_case")]
1217pub enum WireHandlingMode {
1218    #[default]
1219    Queue,
1220    Steer,
1221}
1222
1223impl From<WireHandlingMode> for HandlingMode {
1224    fn from(mode: WireHandlingMode) -> Self {
1225        match mode {
1226            WireHandlingMode::Queue => HandlingMode::Queue,
1227            WireHandlingMode::Steer => HandlingMode::Steer,
1228        }
1229    }
1230}
1231
1232impl From<HandlingMode> for WireHandlingMode {
1233    fn from(mode: HandlingMode) -> Self {
1234        match mode {
1235            HandlingMode::Queue => WireHandlingMode::Queue,
1236            HandlingMode::Steer => WireHandlingMode::Steer,
1237        }
1238    }
1239}
1240
1241/// Public render class contract for mob member delivery.
1242#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
1243#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
1244#[serde(rename_all = "snake_case")]
1245pub enum WireRenderClass {
1246    UserPrompt,
1247    PeerMessage,
1248    PeerRequest,
1249    PeerResponse,
1250    ExternalEvent,
1251    FlowStep,
1252    Continuation,
1253    SystemNotice,
1254    ToolScopeNotice,
1255    OpsProgress,
1256}
1257
1258impl From<WireRenderClass> for RenderClass {
1259    fn from(class: WireRenderClass) -> Self {
1260        match class {
1261            WireRenderClass::UserPrompt => RenderClass::UserPrompt,
1262            WireRenderClass::PeerMessage => RenderClass::PeerMessage,
1263            WireRenderClass::PeerRequest => RenderClass::PeerRequest,
1264            WireRenderClass::PeerResponse => RenderClass::PeerResponse,
1265            WireRenderClass::ExternalEvent => RenderClass::ExternalEvent,
1266            WireRenderClass::FlowStep => RenderClass::FlowStep,
1267            WireRenderClass::Continuation => RenderClass::Continuation,
1268            WireRenderClass::SystemNotice => RenderClass::SystemNotice,
1269            WireRenderClass::ToolScopeNotice => RenderClass::ToolScopeNotice,
1270            WireRenderClass::OpsProgress => RenderClass::OpsProgress,
1271        }
1272    }
1273}
1274
1275impl From<RenderClass> for WireRenderClass {
1276    fn from(class: RenderClass) -> Self {
1277        match class {
1278            RenderClass::UserPrompt => WireRenderClass::UserPrompt,
1279            RenderClass::PeerMessage => WireRenderClass::PeerMessage,
1280            RenderClass::PeerRequest => WireRenderClass::PeerRequest,
1281            RenderClass::PeerResponse => WireRenderClass::PeerResponse,
1282            RenderClass::ExternalEvent => WireRenderClass::ExternalEvent,
1283            RenderClass::FlowStep => WireRenderClass::FlowStep,
1284            RenderClass::Continuation => WireRenderClass::Continuation,
1285            RenderClass::SystemNotice => WireRenderClass::SystemNotice,
1286            RenderClass::ToolScopeNotice => WireRenderClass::ToolScopeNotice,
1287            RenderClass::OpsProgress => WireRenderClass::OpsProgress,
1288        }
1289    }
1290}
1291
1292/// Public render salience contract for mob member delivery.
1293#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
1294#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
1295#[serde(rename_all = "snake_case")]
1296pub enum WireRenderSalience {
1297    Background,
1298    Normal,
1299    Important,
1300    Urgent,
1301}
1302
1303impl From<WireRenderSalience> for RenderSalience {
1304    fn from(salience: WireRenderSalience) -> Self {
1305        match salience {
1306            WireRenderSalience::Background => RenderSalience::Background,
1307            WireRenderSalience::Normal => RenderSalience::Normal,
1308            WireRenderSalience::Important => RenderSalience::Important,
1309            WireRenderSalience::Urgent => RenderSalience::Urgent,
1310        }
1311    }
1312}
1313
1314impl From<RenderSalience> for WireRenderSalience {
1315    fn from(salience: RenderSalience) -> Self {
1316        match salience {
1317            RenderSalience::Background => WireRenderSalience::Background,
1318            RenderSalience::Normal => WireRenderSalience::Normal,
1319            RenderSalience::Important => WireRenderSalience::Important,
1320            RenderSalience::Urgent => WireRenderSalience::Urgent,
1321        }
1322    }
1323}
1324
1325/// Public render metadata contract for mob member delivery.
1326#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
1327#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
1328pub struct WireRenderMetadata {
1329    pub class: WireRenderClass,
1330    #[serde(default, skip_serializing_if = "Option::is_none")]
1331    pub salience: Option<WireRenderSalience>,
1332}
1333
1334impl From<WireRenderMetadata> for RenderMetadata {
1335    fn from(metadata: WireRenderMetadata) -> Self {
1336        Self {
1337            class: metadata.class.into(),
1338            salience: metadata
1339                .salience
1340                .unwrap_or(WireRenderSalience::Normal)
1341                .into(),
1342        }
1343    }
1344}
1345
1346impl From<RenderMetadata> for WireRenderMetadata {
1347    fn from(metadata: RenderMetadata) -> Self {
1348        Self {
1349            class: metadata.class.into(),
1350            salience: Some(metadata.salience.into()),
1351        }
1352    }
1353}
1354
1355// ---------------------------------------------------------------------------
1356// Declarative roster API (`mob/ensure_member`, `mob/reconcile`,
1357// `mob/list_members_matching`). These methods compose over spawn / retire /
1358// list_members; they introduce no new lifecycle.
1359// ---------------------------------------------------------------------------
1360
1361/// Per-member spec for `mob/ensure_member` and the `desired` entries of
1362/// `mob/reconcile`.
1363///
1364/// Mirrors the essential, codegen-friendly fields of
1365/// [`meerkat_mob::SpawnMemberSpec`]. Complex sub-types (tool access policy,
1366/// budget split, inherited tool filter, override profile) are not on this
1367/// wire surface — callers that need that parity should use the non-declarative
1368/// `mob/spawn` method.
1369#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
1370#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
1371pub struct MobMemberSpecWire {
1372    /// Profile name (role) in the mob definition.
1373    pub profile: String,
1374    /// Stable member identity within the mob.
1375    pub agent_identity: String,
1376    #[serde(default, skip_serializing_if = "Option::is_none")]
1377    pub initial_message: Option<WireContentInput>,
1378    #[serde(default, skip_serializing_if = "Option::is_none")]
1379    pub runtime_mode: Option<WireMobRuntimeMode>,
1380    #[serde(default, skip_serializing_if = "Option::is_none")]
1381    pub backend: Option<WireMobBackendKind>,
1382    #[serde(default, skip_serializing_if = "Option::is_none")]
1383    pub binding: Option<WireRuntimeBinding>,
1384    #[serde(default, skip_serializing_if = "Option::is_none")]
1385    pub context: Option<Value>,
1386    #[serde(default, skip_serializing_if = "Option::is_none")]
1387    pub labels: Option<BTreeMap<String, String>>,
1388    #[serde(default, skip_serializing_if = "Option::is_none")]
1389    pub additional_instructions: Option<Vec<String>>,
1390    #[serde(default, skip_serializing_if = "Option::is_none")]
1391    pub auto_wire_parent: Option<bool>,
1392}
1393
1394impl MobMemberSpecWire {
1395    /// Compose the existing member `labels` and opaque `context` fields into
1396    /// the shared surface metadata contract without changing the JSON shape.
1397    #[must_use]
1398    pub fn surface_metadata(&self) -> SurfaceMetadata {
1399        SurfaceMetadata::from_optional_parts(self.labels.clone(), self.context.clone())
1400    }
1401
1402    /// Validate caller-supplied metadata for public member create surfaces.
1403    pub fn validate_public_surface_metadata(&self) -> Result<(), SurfaceMetadataError> {
1404        self.surface_metadata().validate_public()
1405    }
1406}
1407
1408/// Request payload for `mob/ensure_member`.
1409#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
1410#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
1411#[serde(deny_unknown_fields)]
1412pub struct MobEnsureMemberParams {
1413    pub mob_id: String,
1414    pub spec: MobMemberSpecWire,
1415}
1416
1417/// Server-resolved opaque handle for a mob member.
1418///
1419/// Encodes `{mob_id, agent_identity}` as a single base64url-encoded token
1420/// that callers treat as opaque. The server resolves the current
1421/// `AgentRuntimeId` and fence token against the live mob roster on every
1422/// dispatch — clients never reason about `generation` or `fence_token`
1423/// directly.
1424///
1425/// Use [`WireMemberRef::encode`] to produce a token and
1426/// [`WireMemberRef::decode`] inside an RPC handler to recover the
1427/// `(mob_id, agent_identity)` pair before resolving against the runtime.
1428#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, Hash)]
1429#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
1430#[serde(transparent)]
1431pub struct WireMemberRef(String);
1432
1433impl WireMemberRef {
1434    /// Construct a handle from its components. The `mob_id` and
1435    /// `agent_identity` together form the resolution key the server uses to
1436    /// look up the member's current incarnation.
1437    #[must_use]
1438    pub fn encode(mob_id: &str, agent_identity: &str) -> Self {
1439        // Single-letter keys keep the encoded payload short so the token
1440        // remains compact in URLs and JSON payloads.
1441        // `Value::to_string` on a two-field object is infallible.
1442        let payload = serde_json::json!({ "m": mob_id, "a": agent_identity });
1443        Self(base64_url_encode(payload.to_string().as_bytes()))
1444    }
1445
1446    /// Borrow the raw token string for transport.
1447    #[must_use]
1448    pub fn as_str(&self) -> &str {
1449        &self.0
1450    }
1451
1452    /// Construct a handle from a raw token string without validation. Used
1453    /// when forwarding an opaque token received from the wire.
1454    #[must_use]
1455    pub fn from_token(token: impl Into<String>) -> Self {
1456        Self(token.into())
1457    }
1458
1459    /// Decode the handle into `(mob_id, agent_identity)`. Returns `Err` when
1460    /// the token is malformed.
1461    pub fn decode(&self) -> Result<(String, String), WireMemberRefError> {
1462        let bytes = base64_url_decode(&self.0).map_err(|_| WireMemberRefError::Malformed)?;
1463        let value: Value =
1464            serde_json::from_slice(&bytes).map_err(|_| WireMemberRefError::Malformed)?;
1465        let mob_id = value
1466            .get("m")
1467            .and_then(Value::as_str)
1468            .ok_or(WireMemberRefError::Malformed)?;
1469        let agent_identity = value
1470            .get("a")
1471            .and_then(Value::as_str)
1472            .ok_or(WireMemberRefError::Malformed)?;
1473        Ok((mob_id.to_string(), agent_identity.to_string()))
1474    }
1475}
1476
1477/// Failure modes for [`WireMemberRef::decode`].
1478#[derive(Debug, thiserror::Error)]
1479pub enum WireMemberRefError {
1480    /// Token is not valid base64url or its decoded payload is not the
1481    /// expected `{m, a}` shape.
1482    #[error("malformed member ref token")]
1483    Malformed,
1484}
1485
1486fn base64_url_encode(bytes: &[u8]) -> String {
1487    use base64::Engine as _;
1488    base64::engine::general_purpose::URL_SAFE_NO_PAD.encode(bytes)
1489}
1490
1491fn base64_url_decode(input: &str) -> Result<Vec<u8>, base64::DecodeError> {
1492    use base64::Engine as _;
1493    base64::engine::general_purpose::URL_SAFE_NO_PAD.decode(input)
1494}
1495
1496/// Identity-native payload for `EnsureMemberOutcome::Spawned`.
1497#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
1498#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
1499pub struct MobSpawnReceiptWire {
1500    pub agent_identity: String,
1501    /// Server-resolved opaque handle for subsequent member-targeted calls
1502    /// (work submission, cancellation, lifecycle). Replaces the binding-era
1503    /// `generation` / `fence_token` pair on app-facing surfaces.
1504    pub member_ref: WireMemberRef,
1505}
1506
1507/// Execution status mirroring `meerkat_mob::runtime::MobMemberStatus`.
1508#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
1509#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
1510#[serde(rename_all = "snake_case")]
1511pub enum WireMobMemberStatus {
1512    Active,
1513    Retiring,
1514    Broken,
1515    Completed,
1516    Unknown,
1517}
1518
1519/// Public roster entry returned by `mob/ensure_member`'s `Existed` outcome
1520/// (and other surfaces that want a typed snapshot of a single member). Mirrors
1521/// the public-facing fields of `meerkat_mob::runtime::MobMemberListEntry`
1522/// without leaking bridge-internal fields.
1523#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
1524#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
1525pub struct MobMemberListEntryWire {
1526    pub agent_identity: String,
1527    pub member_ref: WireMemberRef,
1528    pub role: String,
1529    pub runtime_mode: WireMobRuntimeMode,
1530    #[serde(default, skip_serializing_if = "Vec::is_empty")]
1531    pub wired_to: Vec<String>,
1532    #[serde(default, skip_serializing_if = "BTreeMap::is_empty")]
1533    pub labels: BTreeMap<String, String>,
1534    pub status: WireMobMemberStatus,
1535    #[serde(default, skip_serializing_if = "Option::is_none")]
1536    pub error: Option<String>,
1537    pub is_final: bool,
1538}
1539
1540/// Outcome of a `mob/ensure_member` call.
1541///
1542/// `Existed` returns the typed [`MobMemberListEntryWire`] roster snapshot so
1543/// public consumers do not need out-of-band knowledge of the Rust domain
1544/// `MobMemberListEntry` shape.
1545#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
1546#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
1547pub enum MobEnsureMemberOutcomeWire {
1548    #[serde(rename = "spawned")]
1549    Spawned(MobSpawnReceiptWire),
1550    #[serde(rename = "existed")]
1551    Existed(MobMemberListEntryWire),
1552}
1553
1554/// Response payload for `mob/ensure_member`.
1555#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
1556#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
1557pub struct MobEnsureMemberResult {
1558    pub outcome: MobEnsureMemberOutcomeWire,
1559}
1560
1561/// Options controlling a `mob/reconcile` pass.
1562#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq, Eq)]
1563#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
1564#[serde(deny_unknown_fields)]
1565pub struct MobReconcileOptionsWire {
1566    /// When `true`, members on the roster whose identity is not in the
1567    /// `desired` set are retired.
1568    #[serde(default)]
1569    pub retire_stale: bool,
1570}
1571
1572/// Closed wire stage for a per-identity `mob/reconcile` failure.
1573#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
1574#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
1575#[serde(rename_all = "snake_case")]
1576pub enum WireMobReconcileStage {
1577    Spawn,
1578    Retire,
1579}
1580
1581/// Request payload for `mob/reconcile`.
1582#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
1583#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
1584#[serde(deny_unknown_fields)]
1585pub struct MobReconcileParams {
1586    pub mob_id: String,
1587    #[serde(default)]
1588    pub desired: Vec<MobMemberSpecWire>,
1589    #[serde(default)]
1590    pub options: MobReconcileOptionsWire,
1591}
1592
1593/// Typed mob error projection for wire surfaces. Carries the closed failure
1594/// class alongside the human-readable message so consumers branch on the typed
1595/// `code` rather than parsing the free-form `message`. Reuses
1596/// [`MobSpawnManyFailureCause`] as the canonical closed mob-error vocabulary.
1597#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
1598#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
1599pub struct WireMobError {
1600    pub code: MobSpawnManyFailureCause,
1601    pub message: String,
1602}
1603
1604/// Per-identity failure in a `mob/reconcile` pass.
1605#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
1606#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
1607pub struct MobReconcileFailureWire {
1608    pub agent_identity: String,
1609    pub stage: WireMobReconcileStage,
1610    /// Typed mob error: closed failure `code` plus human-readable `message`.
1611    pub error: WireMobError,
1612}
1613
1614/// Summary produced by a `mob/reconcile` pass.
1615#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq)]
1616#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
1617pub struct MobReconcileReportWire {
1618    #[serde(default, skip_serializing_if = "Vec::is_empty")]
1619    pub desired: Vec<String>,
1620    #[serde(default, skip_serializing_if = "Vec::is_empty")]
1621    pub retained: Vec<String>,
1622    #[serde(default, skip_serializing_if = "Vec::is_empty")]
1623    pub spawned: Vec<MobSpawnReceiptWire>,
1624    #[serde(default, skip_serializing_if = "Vec::is_empty")]
1625    pub retired: Vec<String>,
1626    #[serde(default, skip_serializing_if = "Vec::is_empty")]
1627    pub failures: Vec<MobReconcileFailureWire>,
1628}
1629
1630/// Response payload for `mob/reconcile`.
1631#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
1632#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
1633pub struct MobReconcileResult {
1634    pub report: MobReconcileReportWire,
1635}
1636
1637/// Typed lifecycle action for `mob/lifecycle`. Replaces the prior
1638/// `action: String` discriminator with an exhaustive enum so callers and
1639/// handlers reason about lifecycle transitions through the type system
1640/// rather than string folklore.
1641#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
1642#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
1643#[serde(rename_all = "snake_case")]
1644pub enum WireMobLifecycleAction {
1645    Stop,
1646    Resume,
1647    Complete,
1648    Reset,
1649    Destroy,
1650}
1651
1652/// Typed wire/unwire action for the `mob_wire` agent tool. Replaces the prior
1653/// `action: String` discriminator with an exhaustive enum so the agent-tool
1654/// surface reasons about the wire/unwire distinction through the type system
1655/// rather than string folklore (mirrors [`WireMobLifecycleAction`]).
1656#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
1657#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
1658#[serde(rename_all = "snake_case")]
1659pub enum WireMobWireAction {
1660    Wire,
1661    Unwire,
1662}
1663
1664/// Request payload for `mob/lifecycle`.
1665#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
1666#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
1667#[serde(deny_unknown_fields)]
1668pub struct MobLifecycleParams {
1669    pub mob_id: String,
1670    pub action: WireMobLifecycleAction,
1671}
1672
1673/// Response payload for `mob/lifecycle`.
1674#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
1675#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
1676pub struct MobLifecycleResult {
1677    pub mob_id: String,
1678    pub action: WireMobLifecycleAction,
1679    pub ok: bool,
1680    #[serde(default, skip_serializing_if = "Option::is_none")]
1681    pub destroy_report: Option<Value>,
1682}
1683
1684/// Request payload for `mob/append_system_context`.
1685#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
1686#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
1687#[serde(deny_unknown_fields)]
1688pub struct MobAppendSystemContextParams {
1689    pub mob_id: String,
1690    pub agent_identity: String,
1691    pub text: String,
1692    #[serde(default, skip_serializing_if = "Option::is_none")]
1693    pub source: Option<String>,
1694    #[serde(default, skip_serializing_if = "Option::is_none")]
1695    pub idempotency_key: Option<String>,
1696}
1697
1698/// Outcome of a `mob/append_system_context` call on the wire. Mirrors
1699/// `meerkat_core::AppendSystemContextStatus` so consumers reason about the
1700/// applied/staged/duplicate distinction through a closed type rather than a
1701/// free-form status string.
1702#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
1703#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
1704#[serde(rename_all = "snake_case")]
1705pub enum WireAppendSystemContextStatus {
1706    Applied,
1707    Staged,
1708    Duplicate,
1709}
1710
1711impl From<meerkat_core::AppendSystemContextStatus> for WireAppendSystemContextStatus {
1712    fn from(status: meerkat_core::AppendSystemContextStatus) -> Self {
1713        match status {
1714            meerkat_core::AppendSystemContextStatus::Applied => Self::Applied,
1715            meerkat_core::AppendSystemContextStatus::Staged => Self::Staged,
1716            meerkat_core::AppendSystemContextStatus::Duplicate => Self::Duplicate,
1717        }
1718    }
1719}
1720
1721/// Response payload for `mob/append_system_context`.
1722#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
1723#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
1724pub struct MobAppendSystemContextResult {
1725    pub mob_id: String,
1726    pub agent_identity: String,
1727    pub status: WireAppendSystemContextStatus,
1728}
1729
1730/// Response payload for `mob/flows`.
1731#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
1732#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
1733pub struct MobFlowsResult {
1734    pub mob_id: String,
1735    pub flows: Vec<String>,
1736}
1737
1738/// Request payload for `mob/flow_run`.
1739#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
1740#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
1741#[serde(deny_unknown_fields)]
1742pub struct MobFlowRunParams {
1743    pub mob_id: String,
1744    pub flow_id: String,
1745    #[serde(default)]
1746    pub params: Value,
1747}
1748
1749/// Request payload for `mob/run`.
1750///
1751/// Starts the pack's callable flow. `flow_id` defaults to `main`; `prompt` is
1752/// sugar for `params.prompt` when the caller does not provide that key.
1753#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
1754#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
1755#[serde(deny_unknown_fields)]
1756pub struct MobRunParams {
1757    pub mob_id: String,
1758    #[serde(default, skip_serializing_if = "Option::is_none")]
1759    pub flow_id: Option<String>,
1760    #[serde(default, skip_serializing_if = "Option::is_none")]
1761    pub prompt: Option<String>,
1762    #[serde(default)]
1763    pub params: Value,
1764}
1765
1766/// Response payload for `mob/flow_run`.
1767#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
1768#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
1769pub struct MobFlowRunResult {
1770    pub run_id: String,
1771}
1772
1773/// Request payload for `mob/flow_status`.
1774#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
1775#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
1776#[serde(deny_unknown_fields)]
1777pub struct MobFlowStatusParams {
1778    pub mob_id: String,
1779    pub run_id: String,
1780}
1781
1782/// Lifecycle status of a flow run on the wire. Mirrors
1783/// `meerkat_mob::MobRunStatus` so consumers branch on a closed type rather than
1784/// re-deriving meaning from a free-form status string.
1785#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
1786#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
1787#[serde(rename_all = "snake_case")]
1788pub enum WireMobRunStatus {
1789    Pending,
1790    Running,
1791    Completed,
1792    Failed,
1793    Canceled,
1794}
1795
1796/// Typed public projection of a single flow run for `mob/flow_status`.
1797///
1798/// The canonical identity and lifecycle fields (`run_id`, `mob_id`, `flow_id`,
1799/// `status`) are typed; the remaining kernel-owned step/loop projection rides
1800/// along as the `kernel` map. Producers project a domain `MobRun` into this
1801/// shape so consumers never re-derive run identity or lifecycle from a free
1802/// `serde_json::Value`.
1803#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
1804#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
1805pub struct WireMobRun {
1806    pub run_id: String,
1807    pub mob_id: String,
1808    pub flow_id: String,
1809    pub status: WireMobRunStatus,
1810    /// Remaining kernel-owned run projection (step ledger, frame/loop outputs,
1811    /// flow state) after the typed identity/lifecycle fields are lifted out.
1812    #[serde(flatten)]
1813    pub kernel: serde_json::Map<String, Value>,
1814}
1815
1816/// Response payload for `mob/flow_status`.
1817///
1818/// `run` is `None` when the requested run id has no persisted run.
1819#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
1820#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
1821pub struct MobFlowStatusResult {
1822    #[serde(default, skip_serializing_if = "Option::is_none")]
1823    pub run: Option<WireMobRun>,
1824}
1825
1826/// Request payload for `mob/run_result`.
1827#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
1828#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
1829#[serde(deny_unknown_fields)]
1830pub struct MobRunResultParams {
1831    pub mob_id: String,
1832    pub run_id: String,
1833}
1834
1835/// Typed output envelope for a completed or in-flight mob flow run.
1836#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
1837#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
1838#[serde(deny_unknown_fields)]
1839pub struct WireMobRunResultEnvelope {
1840    pub run_id: String,
1841    pub mob_id: String,
1842    pub flow_id: String,
1843    pub status: WireMobRunStatus,
1844    #[serde(default, skip_serializing_if = "Option::is_none")]
1845    pub result: Option<Value>,
1846    #[serde(default, skip_serializing_if = "BTreeMap::is_empty")]
1847    pub outputs: BTreeMap<String, Value>,
1848}
1849
1850/// Response payload for `mob/run_result`.
1851///
1852/// `run` is `None` when the requested run id has no persisted run.
1853#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
1854#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
1855pub struct MobRunResult {
1856    #[serde(default, skip_serializing_if = "Option::is_none")]
1857    pub run: Option<WireMobRunResultEnvelope>,
1858}
1859
1860/// Request payload for `mob/flow_cancel`.
1861#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
1862#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
1863#[serde(deny_unknown_fields)]
1864pub struct MobFlowCancelParams {
1865    pub mob_id: String,
1866    pub run_id: String,
1867}
1868
1869/// Response payload for `mob/flow_cancel`.
1870#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
1871#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
1872pub struct MobFlowCancelResult {
1873    pub canceled: bool,
1874}
1875
1876/// Request payload for `mob/spawn_helper`.
1877#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
1878#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
1879#[serde(deny_unknown_fields)]
1880pub struct MobSpawnHelperParams {
1881    pub mob_id: String,
1882    pub prompt: String,
1883    #[serde(default, skip_serializing_if = "Option::is_none")]
1884    pub agent_identity: Option<String>,
1885    #[serde(default, skip_serializing_if = "Option::is_none")]
1886    pub role_name: Option<String>,
1887    #[serde(default, skip_serializing_if = "Option::is_none")]
1888    pub model_override: Option<String>,
1889    #[serde(default, skip_serializing_if = "Option::is_none")]
1890    pub auth_binding: Option<WireAuthBindingRef>,
1891    #[serde(default, skip_serializing_if = "Option::is_none")]
1892    pub runtime_mode: Option<WireMobRuntimeMode>,
1893    #[serde(default, skip_serializing_if = "Option::is_none")]
1894    pub backend: Option<WireMobBackendKind>,
1895}
1896
1897/// Request payload for `mob/fork_helper`.
1898#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
1899#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
1900#[serde(deny_unknown_fields)]
1901pub struct MobForkHelperParams {
1902    pub mob_id: String,
1903    pub source_member_id: String,
1904    pub prompt: String,
1905    #[serde(default, skip_serializing_if = "Option::is_none")]
1906    pub agent_identity: Option<String>,
1907    #[serde(default, skip_serializing_if = "Option::is_none")]
1908    pub role_name: Option<String>,
1909    #[serde(default, skip_serializing_if = "Option::is_none")]
1910    pub model_override: Option<String>,
1911    #[serde(default, skip_serializing_if = "Option::is_none")]
1912    pub auth_binding: Option<WireAuthBindingRef>,
1913    #[serde(default, skip_serializing_if = "Option::is_none")]
1914    pub fork_context: Option<Value>,
1915    #[serde(default, skip_serializing_if = "Option::is_none")]
1916    pub runtime_mode: Option<WireMobRuntimeMode>,
1917    #[serde(default, skip_serializing_if = "Option::is_none")]
1918    pub backend: Option<WireMobBackendKind>,
1919}
1920
1921/// Response payload for `mob/spawn_helper` and `mob/fork_helper`.
1922#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
1923#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
1924pub struct MobHelperResult {
1925    #[serde(default, skip_serializing_if = "Option::is_none")]
1926    pub output: Option<String>,
1927    pub tokens_used: u64,
1928    pub agent_identity: String,
1929    pub member_ref: WireMemberRef,
1930}
1931
1932/// Response payload for `mob/force_cancel`.
1933#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
1934#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
1935pub struct MobForceCancelResult {
1936    pub cancelled: bool,
1937}
1938
1939/// Request payload for `mob/turn_start`.
1940///
1941/// `provider_params` and `auth_binding` carry the canonical Inherit/Set/Clear
1942/// tri-state via [`WireTurnMetadataOverride`]; unknown fields (including the
1943/// retired `clear_*` split wire form) fail closed at the serde boundary via
1944/// `deny_unknown_fields`, which also keeps the emitted JSON Schema's
1945/// `additionalProperties: false` aligned with the deserializer.
1946#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
1947#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
1948#[serde(deny_unknown_fields)]
1949pub struct MobTurnStartParams {
1950    pub mob_id: String,
1951    pub agent_identity: String,
1952    pub prompt: WireContentInput,
1953    #[serde(default, skip_serializing_if = "Option::is_none")]
1954    pub skill_refs: Option<Vec<meerkat_core::skills::SkillRef>>,
1955    #[serde(default, skip_serializing_if = "Option::is_none")]
1956    pub turn_tool_overlay: Option<meerkat_core::service::PublicTurnToolOverlay>,
1957    #[serde(default, skip_serializing_if = "Option::is_none")]
1958    pub additional_instructions: Option<Vec<String>>,
1959    #[serde(default, skip_serializing_if = "Option::is_none")]
1960    pub keep_alive: Option<bool>,
1961    #[serde(default, skip_serializing_if = "Option::is_none")]
1962    pub model: Option<String>,
1963    #[serde(default, skip_serializing_if = "Option::is_none")]
1964    pub provider: Option<String>,
1965    #[serde(default, skip_serializing_if = "Option::is_none")]
1966    pub max_tokens: Option<u32>,
1967    #[serde(default, skip_serializing_if = "Option::is_none")]
1968    pub system_prompt: Option<String>,
1969    #[serde(default, skip_serializing_if = "Option::is_none")]
1970    pub output_schema: Option<Value>,
1971    #[serde(default, skip_serializing_if = "Option::is_none")]
1972    pub structured_output_retries: Option<u32>,
1973    #[serde(default, skip_serializing_if = "Option::is_none")]
1974    pub provider_params:
1975        Option<WireTurnMetadataOverride<crate::wire::runtime::WireProviderParamsOverride>>,
1976    #[serde(default, skip_serializing_if = "Option::is_none")]
1977    pub auth_binding: Option<WireTurnMetadataOverride<WireAuthBindingRef>>,
1978    /// Host-attached injected context for this turn. Each entry materializes
1979    /// as a separate typed injected-context transcript message immediately
1980    /// before the turn's user message, in order. `mob/turn_start` already
1981    /// rejects autonomous members, so this always rides a turn-driven turn.
1982    #[serde(default, skip_serializing_if = "Option::is_none")]
1983    pub injected_context: Option<Vec<WireContentInput>>,
1984}
1985
1986/// One currently wired peer that is known to be unreachable.
1987#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
1988#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
1989pub struct WireUnreachablePeer {
1990    pub peer: String,
1991    #[serde(default, skip_serializing_if = "Option::is_none")]
1992    pub reason: Option<String>,
1993}
1994
1995/// Live connectivity summary for a member's currently wired peers. Mirrors
1996/// `meerkat_mob::MobPeerConnectivitySnapshot`.
1997#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
1998#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
1999pub struct WirePeerConnectivitySnapshot {
2000    pub reachable_peer_count: usize,
2001    pub unknown_peer_count: usize,
2002    #[serde(default, skip_serializing_if = "Vec::is_empty")]
2003    pub unreachable_peers: Vec<WireUnreachablePeer>,
2004}
2005
2006/// Tri-state peer-connectivity projection for `mob/member_status`.
2007///
2008/// Distinguishes "connectivity is not applicable to this member" (no bridge
2009/// session backs the member) from "the live probe timed out" (the answer is
2010/// transiently unknown) from a resolved connectivity snapshot. The legacy
2011/// `Option<MobPeerConnectivitySnapshot>` projection collapsed both the
2012/// not-applicable and timed-out cases into `None`, laundering a transient
2013/// probe fault into the same shape as a structurally-absent binding.
2014#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
2015#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
2016#[serde(tag = "status", rename_all = "snake_case")]
2017pub enum WirePeerConnectivity {
2018    /// The member has no bridge session, so live peer connectivity is not a
2019    /// resolvable fact for it.
2020    NotApplicable,
2021    /// A live connectivity probe was attempted but did not resolve in time.
2022    ProbeTimedOut,
2023    /// A resolved connectivity snapshot.
2024    Known {
2025        snapshot: WirePeerConnectivitySnapshot,
2026    },
2027}
2028
2029/// Response payload for `mob/member_status`.
2030#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
2031#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
2032pub struct MobMemberStatusResult {
2033    pub status: WireMobMemberStatus,
2034    /// Server-resolved opaque handle for subsequent member-targeted calls.
2035    pub member_ref: WireMemberRef,
2036    #[serde(default, skip_serializing_if = "Option::is_none")]
2037    pub output_preview: Option<String>,
2038    #[serde(default, skip_serializing_if = "Option::is_none")]
2039    pub error: Option<String>,
2040    pub tokens_used: u64,
2041    pub is_final: bool,
2042    #[serde(default, skip_serializing_if = "Option::is_none")]
2043    pub current_session_id: Option<String>,
2044    #[serde(default, skip_serializing_if = "Option::is_none")]
2045    pub peer_connectivity: Option<WirePeerConnectivity>,
2046    #[serde(default, skip_serializing_if = "Option::is_none")]
2047    pub kickoff: Option<Value>,
2048    #[serde(default, skip_serializing_if = "Option::is_none")]
2049    pub external_member: Option<Value>,
2050    #[serde(default, skip_serializing_if = "Option::is_none")]
2051    pub resolved_capabilities: Option<crate::wire::WireResolvedModelCapabilities>,
2052    #[serde(default, skip_serializing_if = "Option::is_none")]
2053    pub progress: Option<WireMemberProgressSnapshot>,
2054}
2055
2056#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
2057#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
2058#[serde(rename_all = "snake_case")]
2059pub enum WireMemberRunState {
2060    Idle,
2061    RunOpen,
2062    Unknown,
2063}
2064
2065#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
2066#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
2067#[serde(rename_all = "snake_case")]
2068pub enum WireMemberHealthClass {
2069    Healthy,
2070    Degraded,
2071    Wedged,
2072    Unknown,
2073}
2074
2075#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
2076#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
2077#[serde(rename_all = "snake_case")]
2078pub enum WireMemberProgressEvent {
2079    ExecutionAdvanced,
2080    BecameIdle,
2081    Unchanged,
2082}
2083
2084#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
2085#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
2086pub struct WireMemberProgressSnapshot {
2087    pub run_state: WireMemberRunState,
2088    pub in_flight_work: u64,
2089    pub last_progress_at_ms: u64,
2090    pub last_progress_event: WireMemberProgressEvent,
2091    pub health: WireMemberHealthClass,
2092}
2093
2094/// Response payload for `mob/snapshot`.
2095#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
2096#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
2097pub struct MobSnapshotResult {
2098    pub mob_id: String,
2099    pub status: WireMobLifecycleStatus,
2100    pub members: Vec<MobMemberListEntryWire>,
2101}
2102
2103#[cfg(test)]
2104mod member_status_capability_tests {
2105    use super::*;
2106
2107    #[test]
2108    fn member_status_result_round_trips_resolved_capabilities() -> Result<(), serde_json::Error> {
2109        let capabilities = crate::wire::WireResolvedModelCapabilities {
2110            vision: true,
2111            image_input: true,
2112            image_tool_results: false,
2113            inline_video: false,
2114            realtime: true,
2115            web_search: true,
2116            image_generation: true,
2117        };
2118        let result = MobMemberStatusResult {
2119            status: WireMobMemberStatus::Active,
2120            member_ref: WireMemberRef::encode("mob-1", "worker-1"),
2121            output_preview: None,
2122            error: None,
2123            tokens_used: 0,
2124            is_final: false,
2125            current_session_id: Some("session-1".to_string()),
2126            peer_connectivity: Some(WirePeerConnectivity::Known {
2127                snapshot: WirePeerConnectivitySnapshot {
2128                    reachable_peer_count: 1,
2129                    unknown_peer_count: 0,
2130                    unreachable_peers: Vec::new(),
2131                },
2132            }),
2133            kickoff: None,
2134            external_member: None,
2135            resolved_capabilities: Some(capabilities.clone()),
2136            progress: None,
2137        };
2138
2139        let json = serde_json::to_string(&result)?;
2140        assert!(json.contains("\"resolved_capabilities\""));
2141        let parsed: MobMemberStatusResult = serde_json::from_str(&json)?;
2142        assert_eq!(parsed.resolved_capabilities, Some(capabilities));
2143        Ok(())
2144    }
2145}
2146
2147/// Response payload for `mob/destroy`.
2148#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
2149#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
2150pub struct MobDestroyResult {
2151    pub mob_id: String,
2152    pub ok: bool,
2153    pub destroy_report: Value,
2154}
2155
2156/// Response payload for `mob/rotate_supervisor`.
2157#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
2158#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
2159pub struct MobRotateSupervisorResult {
2160    pub mob_id: String,
2161    pub ok: bool,
2162    pub report: SupervisorRotationReportWire,
2163}
2164
2165/// Confirmed supervisor rotation report returned by `mob/rotate_supervisor`.
2166#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
2167#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
2168pub struct SupervisorRotationReportWire {
2169    pub previous_epoch: u64,
2170    pub current_epoch: u64,
2171    pub public_peer_id: String,
2172}
2173
2174/// Discriminator kind for the supervisor-rotation-incomplete error details.
2175#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
2176#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
2177#[serde(rename_all = "snake_case")]
2178pub enum SupervisorRotationIncompleteKind {
2179    SupervisorRotationIncomplete,
2180}
2181
2182/// Which authority a supervisor-rotation retry validates against.
2183#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
2184#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
2185#[serde(rename_all = "snake_case")]
2186pub enum SupervisorRotationRetryAuthority {
2187    PendingRotation,
2188    PreRotation,
2189}
2190
2191/// Durability scope of a supervisor-rotation retry.
2192#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
2193#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
2194#[serde(rename_all = "snake_case")]
2195pub enum SupervisorRotationRetryScope {
2196    Durable,
2197    PreRotation,
2198}
2199
2200/// Typed details of `MobError::SupervisorRotationIncomplete` on the wire.
2201#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
2202#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
2203#[serde(rename_all = "snake_case")]
2204pub struct SupervisorRotationIncompleteDetailsWire {
2205    pub kind: SupervisorRotationIncompleteKind,
2206    pub previous_epoch: u64,
2207    pub attempted_epoch: u64,
2208    pub attempted_public_peer_id: String,
2209    pub rotated_peer_count: usize,
2210    pub rollback_succeeded: bool,
2211    pub pending_authority_recorded: bool,
2212    #[serde(default, skip_serializing_if = "Option::is_none")]
2213    pub rollback_error: Option<String>,
2214    pub retry_authority: SupervisorRotationRetryAuthority,
2215    pub retry_scope: SupervisorRotationRetryScope,
2216}
2217
2218/// JSON-RPC `error.data` payload for an incomplete supervisor rotation.
2219#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
2220#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
2221#[serde(rename_all = "snake_case")]
2222pub struct SupervisorRotationIncompleteDataWire {
2223    pub code: String,
2224    pub message: String,
2225    pub details: SupervisorRotationIncompleteDetailsWire,
2226}
2227
2228/// Shared request payload for mob readiness waits.
2229#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
2230#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
2231#[serde(deny_unknown_fields)]
2232pub struct MobWaitParams {
2233    pub mob_id: String,
2234    #[serde(default, skip_serializing_if = "Option::is_none")]
2235    pub member_ids: Option<Vec<String>>,
2236    #[serde(default, skip_serializing_if = "Option::is_none")]
2237    pub timeout_ms: Option<u64>,
2238}
2239
2240/// Response payload for `mob/wait_kickoff` and `mob/wait_ready`.
2241#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
2242#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
2243pub struct MobWaitMembersResult {
2244    pub members: Vec<Value>,
2245}
2246
2247/// Response payload for `mob/cancel_work`.
2248#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
2249#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
2250pub struct MobCancelWorkResult {
2251    pub mob_id: String,
2252    pub ok: bool,
2253}
2254
2255/// Response payload for `mob/cancel_all_work`.
2256#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
2257#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
2258pub struct MobCancelAllWorkResult {
2259    pub mob_id: String,
2260    pub ok: bool,
2261}
2262
2263/// Request payload for `mob/profile/create`.
2264#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
2265#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
2266#[serde(deny_unknown_fields)]
2267pub struct MobProfileCreateParams {
2268    pub name: String,
2269    pub profile: MobProfileInput,
2270}
2271
2272/// Request payload for `mob/profile/get`.
2273#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
2274#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
2275#[serde(deny_unknown_fields)]
2276pub struct MobProfileNameParams {
2277    pub name: String,
2278}
2279
2280/// Request payload for `mob/profile/update`.
2281#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
2282#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
2283#[serde(deny_unknown_fields)]
2284pub struct MobProfileUpdateParams {
2285    pub name: String,
2286    pub profile: MobProfileInput,
2287    pub expected_revision: u64,
2288}
2289
2290/// Request payload for `mob/profile/delete`.
2291#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
2292#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
2293#[serde(deny_unknown_fields)]
2294pub struct MobProfileDeleteParams {
2295    pub name: String,
2296    pub expected_revision: u64,
2297}
2298
2299/// Stored realm profile projection returned by `mob/profile/*`.
2300#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
2301#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
2302pub struct MobProfileLookupResult {
2303    #[serde(default)]
2304    pub not_found: bool,
2305    pub name: String,
2306    #[serde(default, skip_serializing_if = "Option::is_none")]
2307    pub profile: Option<WireMobProfile>,
2308    #[serde(default, skip_serializing_if = "Option::is_none")]
2309    pub revision: Option<u64>,
2310    #[serde(default, skip_serializing_if = "Option::is_none")]
2311    pub created_at: Option<String>,
2312    #[serde(default, skip_serializing_if = "Option::is_none")]
2313    pub updated_at: Option<String>,
2314}
2315
2316/// Response payload for `mob/profile/list`.
2317#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
2318#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
2319pub struct MobProfileListResult {
2320    pub profiles: Vec<MobProfileLookupResult>,
2321}
2322
2323/// Response payload for `mob/profile/delete`.
2324#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
2325#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
2326pub struct MobProfileDeleteResult {
2327    pub name: String,
2328    pub deleted_revision: u64,
2329}
2330
2331/// Request payload for `mob/stream_open`.
2332#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
2333#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
2334#[serde(deny_unknown_fields)]
2335pub struct MobStreamOpenParams {
2336    pub mob_id: String,
2337    #[serde(default, skip_serializing_if = "Option::is_none")]
2338    pub agent_identity: Option<String>,
2339}
2340
2341/// Response payload for `mob/stream_open`.
2342#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
2343#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
2344pub struct MobStreamOpenResult {
2345    pub stream_id: String,
2346    pub opened: bool,
2347}
2348
2349/// Request payload for `mob/stream_close`.
2350#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
2351#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
2352#[serde(deny_unknown_fields)]
2353pub struct MobStreamCloseParams {
2354    pub stream_id: String,
2355}
2356
2357/// Response payload for `mob/stream_close`.
2358#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
2359#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
2360pub struct MobStreamCloseResult {
2361    pub stream_id: String,
2362    pub closed: bool,
2363    pub already_closed: bool,
2364}
2365
2366/// Origin for `MobSubmitWorkParams`. Replaces the prior free-form
2367/// `origin: Option<String>` shape.
2368#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq, Default)]
2369#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
2370#[serde(rename_all = "snake_case")]
2371pub enum WireWorkOrigin {
2372    #[default]
2373    External,
2374    Internal,
2375}
2376
2377/// Request payload for `mob/submit_work`.
2378///
2379/// Identifies the member through the opaque [`WireMemberRef`] handle the
2380/// server resolves against the live roster — callers do not pass
2381/// `generation` or `fence_token`.
2382#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
2383#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
2384#[serde(deny_unknown_fields)]
2385pub struct MobSubmitWorkParams {
2386    pub member_ref: WireMemberRef,
2387    /// Optional caller-supplied work reference. When absent the server
2388    /// generates a fresh UUID.
2389    #[serde(default, skip_serializing_if = "Option::is_none")]
2390    pub work_ref: Option<String>,
2391    pub content: WireContentInput,
2392    #[serde(default)]
2393    pub origin: WireWorkOrigin,
2394    /// Host-attached injected context delivered alongside the work content.
2395    /// Each entry materializes on the member as a separate typed
2396    /// injected-context transcript message immediately before the work
2397    /// content, in order. Deliverable to queue-mode turn-driven members;
2398    /// autonomous inbox delivery rejects it with a typed error.
2399    #[serde(default, skip_serializing_if = "Option::is_none")]
2400    pub injected_context: Option<Vec<WireContentInput>>,
2401    /// Durable kickoff objective correlation to stamp onto this delegated turn.
2402    #[serde(default, skip_serializing_if = "Option::is_none")]
2403    pub objective_id: Option<String>,
2404}
2405
2406/// Response payload for `mob/submit_work`.
2407#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
2408#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
2409pub struct MobSubmitWorkResult {
2410    pub mob_id: String,
2411    pub work_ref: String,
2412    pub member_ref: WireMemberRef,
2413    #[serde(default, skip_serializing_if = "Option::is_none")]
2414    pub objective_id: Option<String>,
2415}
2416
2417/// Explicitly concludes one machine-owned kickoff objective.
2418#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
2419#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
2420#[serde(deny_unknown_fields)]
2421pub struct MobConcludeObjectiveParams {
2422    pub member_ref: WireMemberRef,
2423    pub objective_id: String,
2424    pub outcome: String,
2425}
2426
2427#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
2428#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
2429pub struct MobConcludeObjectiveResult {
2430    pub member_ref: WireMemberRef,
2431    pub objective_id: String,
2432    pub concluded: bool,
2433}
2434
2435/// Request payload for `mob/cancel_work`.
2436#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
2437#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
2438#[serde(deny_unknown_fields)]
2439pub struct MobCancelWorkParams {
2440    pub mob_id: String,
2441    pub work_ref: String,
2442}
2443
2444/// Request payload for `mob/cancel_all_work`.
2445#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
2446#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
2447#[serde(deny_unknown_fields)]
2448pub struct MobCancelAllWorkParams {
2449    pub member_ref: WireMemberRef,
2450}
2451
2452/// Filter for `mob/list_members_matching`. Non-empty / `Some` fields are
2453/// combined conjunctively; an empty filter matches every member.
2454#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq, Eq)]
2455#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
2456#[serde(deny_unknown_fields)]
2457pub struct MobMemberFilterWire {
2458    /// Required exact matches on member labels.
2459    #[serde(default, skip_serializing_if = "BTreeMap::is_empty")]
2460    pub labels: BTreeMap<String, String>,
2461    /// Required profile name (role).
2462    #[serde(default, skip_serializing_if = "Option::is_none")]
2463    pub role: Option<String>,
2464    /// Required canonical machine-projected member status.
2465    #[serde(default, skip_serializing_if = "Option::is_none")]
2466    pub status: Option<WireMobMemberStatus>,
2467}
2468
2469/// Request payload for `mob/list_members_matching`.
2470#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
2471#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
2472#[serde(deny_unknown_fields)]
2473pub struct MobListMembersMatchingParams {
2474    pub mob_id: String,
2475    #[serde(default)]
2476    pub filter: MobMemberFilterWire,
2477}
2478
2479/// Response payload for `mob/list_members_matching`. Each member is the raw
2480/// roster entry JSON.
2481#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq)]
2482#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
2483pub struct MobListMembersMatchingResult {
2484    #[serde(default)]
2485    pub members: Vec<Value>,
2486}
2487
2488#[cfg(test)]
2489#[allow(clippy::expect_used, clippy::panic)]
2490mod tests {
2491    use super::*;
2492
2493    #[test]
2494    fn mob_helper_params_carry_structural_auth_binding() {
2495        let parsed: MobSpawnHelperParams = serde_json::from_value(serde_json::json!({
2496            "mob_id": "mob-1",
2497            "prompt": "help",
2498            "agent_identity": "helper",
2499            "auth_binding": {
2500                "realm": "dev",
2501                "binding": "default_anthropic",
2502                "profile": "console"
2503            }
2504        }))
2505        .expect("spawn helper params parse");
2506        let auth_binding = parsed.auth_binding.expect("auth_binding should parse");
2507        assert_eq!(auth_binding.realm.as_str(), "dev");
2508        assert_eq!(auth_binding.binding.as_str(), "default_anthropic");
2509        assert_eq!(
2510            auth_binding
2511                .profile
2512                .as_ref()
2513                .map(|profile| profile.as_str()),
2514            Some("console")
2515        );
2516
2517        let parsed: MobForkHelperParams = serde_json::from_value(serde_json::json!({
2518            "mob_id": "mob-1",
2519            "source_member_id": "source",
2520            "prompt": "help",
2521            "agent_identity": "helper",
2522            "auth_binding": {
2523                "realm": "dev",
2524                "binding": "default_anthropic"
2525            }
2526        }))
2527        .expect("fork helper params parse");
2528        let auth_binding = parsed.auth_binding.expect("auth_binding should parse");
2529        assert_eq!(auth_binding.realm.as_str(), "dev");
2530        assert_eq!(auth_binding.binding.as_str(), "default_anthropic");
2531        assert!(auth_binding.profile.is_none());
2532    }
2533
2534    #[test]
2535    fn wire_mob_profile_parses_provider_fields_fail_closed() {
2536        // Minimal legacy payload (no new fields) still parses.
2537        let legacy: WireMobProfile =
2538            serde_json::from_str(r#"{"model":"claude-opus-4-8"}"#).expect("legacy profile parses");
2539        assert_eq!(legacy.provider, None);
2540        assert!(legacy.resume_overrides.is_empty());
2541
2542        // Typed provider + resume override vocabulary parse into closed enums.
2543        let full: WireMobProfile = serde_json::from_str(
2544            r#"{
2545                "model": "claude-internal-preview",
2546                "provider": "anthropic",
2547                "image_generation_provider": "gemini",
2548                "auto_compact_threshold": 60000,
2549                "resume_overrides": ["model", "provider"]
2550            }"#,
2551        )
2552        .expect("typed profile parses");
2553        assert_eq!(full.provider, Some(meerkat_core::Provider::Anthropic));
2554        assert_eq!(
2555            full.image_generation_provider,
2556            Some(meerkat_core::Provider::Gemini)
2557        );
2558        assert_eq!(
2559            full.resume_overrides,
2560            vec![
2561                WireMobResumeOverrideField::Model,
2562                WireMobResumeOverrideField::Provider
2563            ]
2564        );
2565
2566        // Fail-closed: unknown provider names and zero thresholds reject.
2567        assert!(
2568            serde_json::from_str::<WireMobProfile>(r#"{"model":"m","provider":"not-a-provider"}"#)
2569                .is_err(),
2570            "unknown provider names must fail closed at the wire boundary"
2571        );
2572        assert!(
2573            serde_json::from_str::<WireMobProfile>(r#"{"model":"m","auto_compact_threshold":0}"#)
2574                .is_err(),
2575            "zero auto_compact_threshold must fail closed at the wire boundary"
2576        );
2577        assert!(
2578            serde_json::from_str::<WireMobProfile>(
2579                r#"{"model":"m","resume_overrides":["everything"]}"#
2580            )
2581            .is_err(),
2582            "resume_overrides vocabulary is closed"
2583        );
2584    }
2585
2586    #[test]
2587    fn mob_definition_input_parses_custom_models() {
2588        let input: MobDefinitionInput = serde_json::from_str(
2589            r#"{
2590                "id": "m",
2591                "profiles": {"worker": {"model": "claude-internal-preview"}},
2592                "models": {
2593                    "claude-internal-preview": {
2594                        "provider": "anthropic",
2595                        "context_window": 500000,
2596                        "vision": true
2597                    }
2598                },
2599                "image_generation_provider": "openai"
2600            }"#,
2601        )
2602        .expect("definition with custom models parses");
2603        let model = input
2604            .models
2605            .get("claude-internal-preview")
2606            .expect("custom model present");
2607        assert_eq!(model.provider, meerkat_core::Provider::Anthropic);
2608        assert_eq!(model.context_window, Some(500_000));
2609        assert_eq!(model.vision, Some(true));
2610        assert_eq!(
2611            input.image_generation_provider,
2612            Some(meerkat_core::Provider::OpenAI)
2613        );
2614    }
2615
2616    #[test]
2617    fn wire_member_ref_round_trips_through_encode_decode() {
2618        let token = WireMemberRef::encode("mob-42", "worker-1");
2619        let (mob_id, agent_identity) = token.decode().expect("decode round-trips");
2620        assert_eq!(mob_id, "mob-42");
2621        assert_eq!(agent_identity, "worker-1");
2622    }
2623
2624    #[test]
2625    fn wire_member_ref_rejects_malformed_token() {
2626        let err = WireMemberRef::from_token("not-a-token-payload")
2627            .decode()
2628            .expect_err("malformed tokens must fail to decode");
2629        assert!(matches!(err, WireMemberRefError::Malformed));
2630    }
2631
2632    #[test]
2633    fn mob_member_spec_exposes_shared_surface_metadata() {
2634        let spec = MobMemberSpecWire {
2635            profile: "worker".into(),
2636            agent_identity: "w1".into(),
2637            initial_message: None,
2638            runtime_mode: None,
2639            backend: None,
2640            binding: None,
2641            context: Some(serde_json::json!({"client_ref": "member-card"})),
2642            labels: Some(BTreeMap::from([("client.member_id".into(), "w1".into())])),
2643            additional_instructions: None,
2644            auto_wire_parent: None,
2645        };
2646
2647        let metadata = spec.surface_metadata();
2648        assert_eq!(
2649            metadata.labels.get("client.member_id").map(String::as_str),
2650            Some("w1")
2651        );
2652        assert_eq!(
2653            metadata.app_context,
2654            Some(serde_json::json!({"client_ref": "member-card"}))
2655        );
2656    }
2657
2658    #[test]
2659    fn mob_member_spec_surface_metadata_rejects_reserved_keys() {
2660        let spec = MobMemberSpecWire {
2661            profile: "worker".into(),
2662            agent_identity: "w1".into(),
2663            initial_message: None,
2664            runtime_mode: None,
2665            backend: None,
2666            binding: None,
2667            context: None,
2668            labels: Some(BTreeMap::from([("mob_id".into(), "spoof".into())])),
2669            additional_instructions: None,
2670            auto_wire_parent: None,
2671        };
2672
2673        assert!(spec.validate_public_surface_metadata().is_err());
2674    }
2675
2676    #[test]
2677    fn mob_reconcile_failure_stage_is_typed_wire_enum() {
2678        let failure = MobReconcileFailureWire {
2679            agent_identity: "worker-1".into(),
2680            stage: WireMobReconcileStage::Spawn,
2681            error: WireMobError {
2682                code: MobSpawnManyFailureCause::ProfileNotFound,
2683                message: "spawn failed".into(),
2684            },
2685        };
2686
2687        let json = serde_json::to_value(&failure).expect("serialize failure");
2688        assert_eq!(json["stage"], "spawn");
2689        assert_eq!(json["error"]["code"], "profile_not_found");
2690        assert_eq!(json["error"]["message"], "spawn failed");
2691
2692        let round_trip: MobReconcileFailureWire =
2693            serde_json::from_value(json).expect("deserialize failure");
2694        assert_eq!(round_trip.stage, WireMobReconcileStage::Spawn);
2695        assert_eq!(
2696            round_trip.error.code,
2697            MobSpawnManyFailureCause::ProfileNotFound
2698        );
2699
2700        let err = serde_json::from_value::<MobReconcileFailureWire>(serde_json::json!({
2701            "agent_identity": "worker-1",
2702            "stage": "restart",
2703            "error": { "code": "profile_not_found", "message": "bad stage" }
2704        }))
2705        .expect_err("unknown reconcile stage must be rejected");
2706        assert!(err.to_string().contains("unknown variant"));
2707    }
2708
2709    #[test]
2710    fn mob_lifecycle_params_reject_unknown_action_string() {
2711        let err = serde_json::from_value::<MobLifecycleParams>(serde_json::json!({
2712            "mob_id": "mob-1",
2713            "action": "explode"
2714        }))
2715        .expect_err("unknown lifecycle actions must fail at the typed wire boundary");
2716
2717        assert!(
2718            err.to_string().contains("unknown variant"),
2719            "unexpected error: {err}"
2720        );
2721    }
2722
2723    #[test]
2724    fn mob_lifecycle_result_round_trips_typed_action() {
2725        let result = MobLifecycleResult {
2726            mob_id: "mob-1".into(),
2727            action: WireMobLifecycleAction::Complete,
2728            ok: true,
2729            destroy_report: None,
2730        };
2731
2732        let json = serde_json::to_value(&result).expect("serialize lifecycle result");
2733        assert_eq!(json["action"], "complete");
2734
2735        let round_trip: MobLifecycleResult =
2736            serde_json::from_value(json).expect("deserialize lifecycle result");
2737        assert_eq!(round_trip.action, WireMobLifecycleAction::Complete);
2738    }
2739
2740    #[test]
2741    fn mob_wire_members_batch_contract_is_local_edge_native() {
2742        let params: MobWireMembersBatchParams = serde_json::from_value(serde_json::json!({
2743            "mob_id": "mob-1",
2744            "edges": [
2745                { "a": "lead", "b": "worker-b" },
2746                { "a": "worker-a", "b": "lead" }
2747            ]
2748        }))
2749        .expect("batch wire params deserialize");
2750
2751        assert_eq!(params.mob_id, "mob-1");
2752        assert_eq!(params.edges.len(), 2);
2753        assert_eq!(params.edges[0].a, "lead");
2754        assert_eq!(params.edges[0].b, "worker-b");
2755
2756        let result = MobWireMembersBatchResult {
2757            requested: 2,
2758            wired: vec![MobWireMembersBatchEdge {
2759                a: "lead".into(),
2760                b: "worker-a".into(),
2761            }],
2762            already_wired: vec![MobWireMembersBatchEdge {
2763                a: "lead".into(),
2764                b: "worker-b".into(),
2765            }],
2766        };
2767        let json = serde_json::to_value(&result).expect("serialize batch wire result");
2768        assert_eq!(json["requested"], 2);
2769        assert_eq!(json["wired"][0]["a"], "lead");
2770        assert_eq!(json["already_wired"][0]["b"], "worker-b");
2771
2772        let err = serde_json::from_value::<MobWireMembersBatchParams>(serde_json::json!({
2773            "mob_id": "mob-1",
2774            "edges": [{ "member": "lead", "peer": "worker-a" }]
2775        }))
2776        .expect_err("mixed local/external mob/wire shape must not deserialize");
2777        let message = err.to_string();
2778        assert!(
2779            message.contains("unknown field `member`") || message.contains("missing field `a`"),
2780            "unexpected error: {message}"
2781        );
2782    }
2783
2784    #[test]
2785    fn mob_spawn_many_result_entry_uses_typed_status_result_envelope() {
2786        let member_ref = WireMemberRef::encode("mob-1", "worker-1");
2787        let entry = MobSpawnManyResultEntry::spawned("worker-1", member_ref.clone());
2788
2789        let json = serde_json::to_value(&entry).expect("serialize typed spawn_many row");
2790        assert_eq!(json["status"], "spawned");
2791        assert_eq!(json["result"]["agent_identity"], "worker-1");
2792        assert_eq!(json["result"]["member_ref"], member_ref.as_str());
2793        assert!(json.get("ok").is_none());
2794        assert!(json.get("error").is_none());
2795
2796        let round_trip: MobSpawnManyResultEntry =
2797            serde_json::from_value(json).expect("deserialize typed spawn_many row");
2798        assert_eq!(round_trip, entry);
2799
2800        let failed = MobSpawnManyResultEntry::failed(
2801            MobSpawnManyFailureCause::ProfileNotFound,
2802            "profile missing",
2803        );
2804        let json = serde_json::to_value(&failed).expect("serialize typed failed spawn_many row");
2805        assert_eq!(json["status"], "failed");
2806        assert_eq!(json["result"]["cause"], "profile_not_found");
2807        assert_eq!(json["result"]["message"], "profile missing");
2808        assert!(json.get("ok").is_none());
2809        assert!(json.get("error").is_none());
2810
2811        let round_trip: MobSpawnManyResultEntry =
2812            serde_json::from_value(json).expect("deserialize typed failed spawn_many row");
2813        assert_eq!(round_trip, failed);
2814    }
2815
2816    #[test]
2817    fn mob_spawn_many_result_entry_rejects_legacy_or_malformed_envelopes() {
2818        let legacy = serde_json::json!({
2819            "ok": true,
2820            "agent_identity": "worker-1",
2821            "member_ref": WireMemberRef::encode("mob-1", "worker-1"),
2822        });
2823        let err = serde_json::from_value::<MobSpawnManyResultEntry>(legacy)
2824            .expect_err("legacy ok carrier must not deserialize");
2825        assert!(
2826            err.to_string().contains("missing field `status`")
2827                || err.to_string().contains("unknown field"),
2828            "unexpected error: {err}"
2829        );
2830
2831        let missing_result = serde_json::json!({
2832            "status": "spawned"
2833        });
2834        let err = serde_json::from_value::<MobSpawnManyResultEntry>(missing_result)
2835            .expect_err("missing typed result must fail closed");
2836        assert!(
2837            err.to_string().contains("missing field `result`"),
2838            "unexpected error: {err}"
2839        );
2840
2841        let unknown_status = serde_json::json!({
2842            "status": "ok",
2843            "result": {
2844                "agent_identity": "worker-1",
2845                "member_ref": WireMemberRef::encode("mob-1", "worker-1"),
2846            }
2847        });
2848        let err = serde_json::from_value::<MobSpawnManyResultEntry>(unknown_status)
2849            .expect_err("unknown typed status must fail closed");
2850        assert!(
2851            err.to_string().contains("unknown variant"),
2852            "unexpected error: {err}"
2853        );
2854
2855        let mismatched = serde_json::json!({
2856            "status": "spawned",
2857            "result": {
2858                "cause": "profile_not_found",
2859                "message": "profile missing"
2860            }
2861        });
2862        let err = serde_json::from_value::<MobSpawnManyResultEntry>(mismatched)
2863            .expect_err("status/result mismatch must fail closed");
2864        assert!(
2865            err.to_string()
2866                .contains("status spawned requires spawned result"),
2867            "unexpected error: {err}"
2868        );
2869
2870        let message_only_failure = serde_json::json!({
2871            "status": "failed",
2872            "result": {
2873                "message": "profile missing"
2874            }
2875        });
2876        let err = serde_json::from_value::<MobSpawnManyResultEntry>(message_only_failure)
2877            .expect_err("string-only failure result must fail closed");
2878        assert!(
2879            err.to_string().contains("data did not match any variant")
2880                || err.to_string().contains("missing field `cause`"),
2881            "unexpected error: {err}"
2882        );
2883
2884        let unknown_failure_cause = serde_json::json!({
2885            "status": "failed",
2886            "result": {
2887                "cause": "future_failure",
2888                "message": "future failure"
2889            }
2890        });
2891        let err = serde_json::from_value::<MobSpawnManyResultEntry>(unknown_failure_cause)
2892            .expect_err("unknown failure cause must fail closed");
2893        assert!(
2894            err.to_string().contains("data did not match any variant")
2895                || err.to_string().contains("unknown variant"),
2896            "unexpected error: {err}"
2897        );
2898    }
2899
2900    #[test]
2901    fn mob_wire_params_reject_legacy_local_target_shape() {
2902        let err = serde_json::from_value::<MobWireParams>(serde_json::json!({
2903            "mob_id": "mob-1",
2904            "local": "member-a",
2905            "target": { "local": "member-b" }
2906        }))
2907        .expect_err("legacy local/target shape must be rejected");
2908
2909        let msg = err.to_string();
2910        assert!(
2911            msg.contains("unknown field `local`") || msg.contains("missing field `member`"),
2912            "unexpected error: {msg}"
2913        );
2914    }
2915
2916    #[test]
2917    fn mob_wire_params_accept_canonical_external_peer_identity() {
2918        let params = serde_json::from_value::<MobWireParams>(serde_json::json!({
2919            "mob_id": "mob-1",
2920            "member": "member-a",
2921            "peer": {
2922                "external": {
2923                    "name": "external-worker",
2924                    "address": "inproc://external-worker",
2925                    "identity": {
2926                        "kind": "ed25519_public_key",
2927                        "public_key": "ed25519:BwcHBwcHBwcHBwcHBwcHBwcHBwcHBwcHBwcHBwcHBwc="
2928                    }
2929                }
2930            }
2931        }))
2932        .expect("canonical external peer identity should deserialize");
2933
2934        let MobPeerTarget::External(spec) = params.peer else {
2935            panic!("expected external peer target");
2936        };
2937        assert_eq!(spec.name, "external-worker");
2938    }
2939
2940    #[test]
2941    fn mob_wire_params_reject_raw_external_peer_id_shape() {
2942        let err = serde_json::from_value::<MobWireParams>(serde_json::json!({
2943            "mob_id": "mob-1",
2944            "member": "member-a",
2945            "peer": {
2946                "external": {
2947                    "name": "external-worker",
2948                    "peer_id": meerkat_core::comms::PeerId::from_ed25519_pubkey(&[7u8; 32]).to_string(),
2949                    "address": "inproc://external-worker",
2950                    "pubkey": vec![7u8; 32]
2951                }
2952            }
2953        }))
2954        .expect_err("raw peer_id/pubkey external peer shape must be rejected");
2955
2956        let msg = err.to_string();
2957        assert!(
2958            msg.contains("peer_id") || msg.contains("identity"),
2959            "unexpected error: {msg}"
2960        );
2961    }
2962
2963    #[test]
2964    fn mob_wire_params_reject_missing_external_peer_pubkey_material() {
2965        let err = serde_json::from_value::<MobWireParams>(serde_json::json!({
2966            "mob_id": "mob-1",
2967            "member": "member-a",
2968            "peer": {
2969                "external": {
2970                    "name": "external-worker",
2971                    "address": "inproc://external-worker",
2972                    "identity": {
2973                        "kind": "ed25519_public_key"
2974                    }
2975                }
2976            }
2977        }))
2978        .expect_err("missing external peer pubkey material must fail closed");
2979
2980        let msg = err.to_string();
2981        assert!(
2982            msg.contains("public_key") || msg.contains("identity"),
2983            "unexpected error: {msg}"
2984        );
2985    }
2986
2987    #[test]
2988    fn runtime_binding_accepts_canonical_external_peer_identity() {
2989        let binding = serde_json::from_value::<WireRuntimeBinding>(serde_json::json!({
2990            "kind": "external",
2991            "address": "inproc://external-worker",
2992            "identity": {
2993                "kind": "ed25519_public_key",
2994                "public_key": "ed25519:BwcHBwcHBwcHBwcHBwcHBwcHBwcHBwcHBwcHBwcHBwc="
2995            }
2996        }))
2997        .expect("canonical external runtime binding identity should deserialize");
2998
2999        let WireRuntimeBinding::External {
3000            identity, address, ..
3001        } = binding
3002        else {
3003            panic!("expected external runtime binding");
3004        };
3005        assert_eq!(address, "inproc://external-worker");
3006        assert_eq!(
3007            identity.resolve().expect("identity resolves").pubkey,
3008            [7u8; 32]
3009        );
3010    }
3011
3012    #[test]
3013    fn runtime_binding_rejects_raw_external_peer_id_shape() {
3014        let err = serde_json::from_value::<WireRuntimeBinding>(serde_json::json!({
3015            "kind": "external",
3016            "peer_id": meerkat_core::comms::PeerId::from_ed25519_pubkey(&[7u8; 32]).to_string(),
3017            "address": "inproc://external-worker",
3018            "pubkey": vec![7u8; 32]
3019        }))
3020        .expect_err("raw peer_id/pubkey external runtime binding shape must be rejected");
3021
3022        let msg = err.to_string();
3023        assert!(
3024            msg.contains("peer_id") || msg.contains("identity"),
3025            "unexpected error: {msg}"
3026        );
3027    }
3028
3029    #[test]
3030    fn runtime_binding_rejects_missing_external_peer_pubkey_material() {
3031        let err = serde_json::from_value::<WireRuntimeBinding>(serde_json::json!({
3032            "kind": "external",
3033            "address": "inproc://external-worker",
3034            "identity": {
3035                "kind": "ed25519_public_key"
3036            }
3037        }))
3038        .expect_err("missing external runtime binding pubkey material must fail closed");
3039
3040        let msg = err.to_string();
3041        assert!(
3042            msg.contains("public_key") || msg.contains("identity"),
3043            "unexpected error: {msg}"
3044        );
3045    }
3046
3047    #[test]
3048    fn mob_turn_start_params_capture_turn_override_fields() {
3049        let params = serde_json::from_value::<MobTurnStartParams>(serde_json::json!({
3050            "mob_id": "mob-1",
3051            "agent_identity": "worker",
3052            "prompt": "continue",
3053            "output_schema": { "type": "object" },
3054            "structured_output_retries": 2
3055        }))
3056        .expect("turn_start should accept explicit turn override fields");
3057
3058        assert_eq!(params.mob_id, "mob-1");
3059        assert_eq!(params.agent_identity, "worker");
3060        assert_eq!(params.prompt, WireContentInput::Text("continue".into()));
3061        assert_eq!(
3062            params.output_schema,
3063            Some(serde_json::json!({ "type": "object" }))
3064        );
3065        assert_eq!(params.structured_output_retries, Some(2));
3066
3067        let err = serde_json::from_value::<MobTurnStartParams>(serde_json::json!({
3068            "mob_id": "mob-1",
3069            "agent_identity": "worker",
3070            "prompt": "continue",
3071            "unknown_override": true
3072        }))
3073        .expect_err("turn_start must reject unknown override fields");
3074        assert!(
3075            err.to_string().contains("unknown field"),
3076            "unexpected error: {err}"
3077        );
3078    }
3079
3080    #[test]
3081    fn mob_create_params_reject_reserved_runtime_lifecycle_fields() {
3082        let err = serde_json::from_value::<MobCreateParams>(serde_json::json!({
3083            "definition": {
3084                "id": "mob-1",
3085                "owner_runtime_binding": "runtime:worker:0",
3086                "profiles": {
3087                    "worker": { "model": "claude-sonnet-4-6" }
3088                }
3089            }
3090        }))
3091        .expect_err("reserved runtime lifecycle fields must be rejected");
3092
3093        assert!(
3094            err.to_string()
3095                .contains("unknown field `owner_runtime_binding`"),
3096            "unexpected error: {err}"
3097        );
3098    }
3099
3100    #[test]
3101    fn mob_create_params_reject_reserved_runtime_bridge_owner_field() {
3102        let err = serde_json::from_value::<MobCreateParams>(serde_json::json!({
3103            "definition": {
3104                "id": "mob-1",
3105                "owner_transport_binding": "transport:worker:0",
3106                "profiles": {
3107                    "worker": { "model": "claude-sonnet-4-6" }
3108                }
3109            }
3110        }))
3111        .expect_err("reserved runtime bridge owner field must be rejected");
3112
3113        assert!(
3114            err.to_string()
3115                .contains("unknown field `owner_transport_binding`"),
3116            "unexpected error: {err}"
3117        );
3118    }
3119
3120    #[test]
3121    fn mob_create_params_reject_internal_profile_tool_bundles() {
3122        let err = serde_json::from_value::<MobCreateParams>(serde_json::json!({
3123            "definition": {
3124                "id": "mob-1",
3125                "profiles": {
3126                    "worker": {
3127                        "model": "claude-sonnet-4-6",
3128                        "tools": {
3129                            "rust_bundles": ["internal-only"]
3130                        }
3131                    }
3132                }
3133            }
3134        }))
3135        .expect_err("internal rust tool bundles must be rejected");
3136
3137        // With untagged MobProfileBindingInput, the error message is about
3138        // no variant matching rather than the specific unknown field.
3139        assert!(
3140            err.to_string().contains("did not match any variant")
3141                || err.to_string().contains("unknown field `rust_bundles`"),
3142            "unexpected error: {err}"
3143        );
3144    }
3145
3146    #[test]
3147    fn mob_create_params_accept_typed_nested_flow_definition() {
3148        let params = serde_json::from_value::<MobCreateParams>(serde_json::json!({
3149            "definition": {
3150                "id": "mob-1",
3151                "profiles": {
3152                    "worker": { "model": "claude-sonnet-4-6" }
3153                },
3154                "flows": {
3155                    "review": {
3156                        "description": "review flow",
3157                        "steps": {
3158                            "draft": {
3159                                "role": "worker",
3160                                "message": "draft it"
3161                            }
3162                        }
3163                    }
3164                }
3165            }
3166        }))
3167        .expect("typed nested flow definition should parse");
3168
3169        assert_eq!(
3170            params.definition.flows["review"].steps["draft"].role,
3171            "worker"
3172        );
3173    }
3174}