Skip to main content

meerkat_contracts/wire/
mob.rs

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