Skip to main content

meerkat_contracts/wire/
mob.rs

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