1use super::connection::WireAuthBindingRef;
4use super::runtime::WireTurnMetadataOverride;
5use super::session::WireContentInput;
6use super::supervisor_bridge::BridgeBootstrapToken;
7use base64::{Engine, engine::general_purpose::STANDARD as BASE64};
8use meerkat_core::OutputSchema;
9use meerkat_core::{
10 HandlingMode,
11 types::{RenderClass, RenderMetadata, RenderSalience},
12};
13use serde::{Deserialize, Serialize};
14use serde_json::Value;
15use std::collections::BTreeMap;
16
17use meerkat_core::{SurfaceMetadata, SurfaceMetadataError};
18
19#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq, Default)]
20#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
21#[serde(rename_all = "snake_case")]
22pub enum WireMobBackendKind {
23 #[default]
24 Session,
25 External,
26}
27
28#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
34#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
35#[serde(tag = "kind", rename_all = "snake_case", deny_unknown_fields)]
36pub enum WireRuntimeBinding {
37 Session,
38 External {
39 address: String,
40 #[serde(default, skip_serializing_if = "Option::is_none")]
41 bootstrap_token: Option<BridgeBootstrapToken>,
42 identity: WireTrustedPeerIdentity,
46 },
47}
48
49#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq, Default)]
50#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
51#[serde(rename_all = "snake_case")]
52pub enum WireMobRuntimeMode {
53 #[default]
54 AutonomousHost,
55 TurnDriven,
56}
57
58#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
60#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
61#[serde(tag = "mode", rename_all = "snake_case")]
62pub enum WireMemberLaunchMode {
63 Fresh,
64 Resume {
65 bridge_session_id: String,
66 },
67 Fork {
68 source_member_id: String,
69 #[serde(default)]
70 fork_context: WireForkContext,
71 },
72}
73
74#[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#[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 WireToolAccessPolicy {
91 #[default]
92 Inherit,
93 AllowList(Vec<String>),
94 DenyList(Vec<String>),
95}
96
97impl WireToolAccessPolicy {
98 #[must_use]
104 pub fn into_core(self) -> meerkat_core::ops::ToolAccessPolicy {
105 match self {
106 Self::Inherit => meerkat_core::ops::ToolAccessPolicy::Inherit,
107 Self::AllowList(names) => {
108 meerkat_core::ops::ToolAccessPolicy::AllowList(names.into_iter().collect())
109 }
110 Self::DenyList(names) => {
111 meerkat_core::ops::ToolAccessPolicy::DenyList(names.into_iter().collect())
112 }
113 }
114 }
115}
116
117#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq, Eq)]
119#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
120pub enum WireToolFilter {
121 #[default]
122 All,
123 Allow(Vec<String>),
124 Deny(Vec<String>),
125}
126
127#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq, Eq)]
129#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
130#[serde(deny_unknown_fields)]
131pub struct WireMobToolConfig {
132 #[serde(default)]
133 pub builtins: bool,
134 #[serde(default)]
135 pub shell: bool,
136 #[serde(default)]
137 pub comms: bool,
138 #[serde(default)]
139 pub memory: bool,
140 #[serde(default)]
141 pub workgraph: bool,
142 #[serde(default)]
143 pub mob: bool,
144 #[serde(default)]
145 pub schedule: bool,
146 #[serde(default)]
147 pub image_generation: bool,
148 #[serde(default)]
149 pub mcp: Vec<String>,
150}
151
152#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
157#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
158#[serde(rename_all = "snake_case")]
159pub enum WireMobResumeOverrideField {
160 Model,
161 Provider,
162 ProviderParams,
163}
164
165#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
167#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
168#[serde(deny_unknown_fields)]
169pub struct WireMobProfile {
170 pub model: String,
171 #[serde(default, skip_serializing_if = "Option::is_none")]
174 pub provider: Option<meerkat_core::Provider>,
175 #[serde(default, skip_serializing_if = "Option::is_none")]
177 pub self_hosted_server_id: Option<String>,
178 #[serde(default, skip_serializing_if = "Option::is_none")]
180 pub image_generation_provider: Option<meerkat_core::Provider>,
181 #[serde(default, skip_serializing_if = "Option::is_none")]
183 pub auto_compact_threshold: Option<std::num::NonZeroU64>,
184 #[serde(default, skip_serializing_if = "Vec::is_empty")]
186 pub resume_overrides: Vec<WireMobResumeOverrideField>,
187 #[serde(default)]
188 pub skills: Vec<String>,
189 #[serde(default)]
190 pub tools: WireMobToolConfig,
191 #[serde(default)]
192 pub peer_description: String,
193 #[serde(default)]
194 pub external_addressable: bool,
195 #[serde(default, skip_serializing_if = "Option::is_none")]
196 pub backend: Option<WireMobBackendKind>,
197 #[serde(default)]
198 pub runtime_mode: WireMobRuntimeMode,
199 #[serde(default, skip_serializing_if = "Option::is_none")]
200 pub max_inline_peer_notifications: Option<i32>,
201 #[serde(default, skip_serializing_if = "Option::is_none")]
202 pub output_schema: Option<Value>,
203 #[serde(default, skip_serializing_if = "Option::is_none")]
204 pub provider_params: Option<crate::wire::runtime::WireProviderParamsOverride>,
205}
206
207#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
208#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
209#[serde(deny_unknown_fields)]
210pub struct MobOrchestratorInput {
211 pub profile: String,
212}
213
214#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
215#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
216#[serde(tag = "source", rename_all = "snake_case")]
217pub enum MobSkillSourceInput {
218 Inline { content: String },
219 Path { path: String },
220}
221
222#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
223#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
224#[serde(deny_unknown_fields)]
225pub struct MobRoleWiringRuleInput {
226 pub a: String,
227 pub b: String,
228}
229
230#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq, Eq)]
231#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
232#[serde(deny_unknown_fields)]
233pub struct MobWiringRulesInput {
234 #[serde(default)]
235 pub auto_wire_orchestrator: bool,
236 #[serde(default, skip_serializing_if = "Vec::is_empty")]
237 pub role_wiring: Vec<MobRoleWiringRuleInput>,
238}
239
240#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq, Eq)]
241#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
242#[serde(deny_unknown_fields)]
243pub struct MobToolConfigInput {
244 #[serde(default)]
245 pub builtins: bool,
246 #[serde(default)]
247 pub shell: bool,
248 #[serde(default)]
249 pub comms: bool,
250 #[serde(default)]
251 pub memory: bool,
252 #[serde(default)]
253 pub workgraph: bool,
254 #[serde(default)]
255 pub mob: bool,
256 #[serde(default)]
257 pub schedule: bool,
258 #[serde(default)]
259 pub image_generation: bool,
260 #[serde(default, skip_serializing_if = "Vec::is_empty")]
261 pub mcp: Vec<String>,
262}
263
264#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
270#[allow(clippy::large_enum_variant)]
271#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
272#[serde(untagged)]
273pub enum MobProfileBindingInput {
274 RealmRef {
276 realm_profile: String,
278 },
279 Inline(MobProfileInput),
281}
282
283#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
284#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
285#[serde(deny_unknown_fields)]
286pub struct MobProfileInput {
287 pub model: String,
288 #[serde(default, skip_serializing_if = "Option::is_none")]
291 pub provider: Option<meerkat_core::Provider>,
292 #[serde(default, skip_serializing_if = "Option::is_none")]
294 pub self_hosted_server_id: Option<String>,
295 #[serde(default, skip_serializing_if = "Option::is_none")]
297 pub image_generation_provider: Option<meerkat_core::Provider>,
298 #[serde(default, skip_serializing_if = "Option::is_none")]
300 pub auto_compact_threshold: Option<std::num::NonZeroU64>,
301 #[serde(default, skip_serializing_if = "Vec::is_empty")]
303 pub resume_overrides: Vec<WireMobResumeOverrideField>,
304 #[serde(default, skip_serializing_if = "Vec::is_empty")]
305 pub skills: Vec<String>,
306 #[serde(default)]
307 pub tools: MobToolConfigInput,
308 #[serde(default, skip_serializing_if = "String::is_empty")]
309 pub peer_description: String,
310 #[serde(default)]
311 pub external_addressable: bool,
312 #[serde(default, skip_serializing_if = "Option::is_none")]
313 pub backend: Option<WireMobBackendKind>,
314 #[serde(default)]
315 pub runtime_mode: WireMobRuntimeMode,
316 #[serde(default, skip_serializing_if = "Option::is_none")]
317 pub max_inline_peer_notifications: Option<i32>,
318 #[serde(default, skip_serializing_if = "Option::is_none")]
319 pub output_schema: Option<OutputSchema>,
320 #[serde(default, skip_serializing_if = "Option::is_none")]
324 pub provider_params: Option<crate::wire::runtime::WireProviderParamsOverride>,
325}
326
327#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
328#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
329#[serde(deny_unknown_fields)]
330pub struct MobExternalBackendConfigInput {
331 pub address_base: String,
332 #[serde(default, skip_serializing_if = "Option::is_none")]
333 pub supervisor_bridge: Option<MobSupervisorBridgeEndpointConfigInput>,
334}
335
336#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq, Eq)]
337#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
338#[serde(deny_unknown_fields)]
339pub struct MobSupervisorBridgeEndpointConfigInput {
340 #[serde(default, skip_serializing_if = "Option::is_none")]
341 pub bind_address: Option<String>,
342 #[serde(default, skip_serializing_if = "Option::is_none")]
343 pub advertised_address: Option<String>,
344}
345
346#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq, Eq)]
347#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
348#[serde(deny_unknown_fields)]
349pub struct MobBackendConfigInput {
350 #[serde(default)]
351 pub default: WireMobBackendKind,
352 #[serde(default, skip_serializing_if = "Option::is_none")]
353 pub external: Option<MobExternalBackendConfigInput>,
354}
355
356#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq, Default)]
357#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
358#[serde(rename_all = "snake_case")]
359pub enum MobDispatchModeInput {
360 #[default]
361 FanOut,
362 OneToOne,
363 FanIn,
364}
365
366#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq, Eq)]
367#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
368#[serde(tag = "type", rename_all = "snake_case")]
369pub enum MobCollectionPolicyInput {
370 #[default]
371 All,
372 Any,
373 Quorum {
374 n: u8,
375 },
376}
377
378#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq, Default)]
379#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
380#[serde(rename_all = "snake_case")]
381pub enum MobDependencyModeInput {
382 #[default]
383 All,
384 Any,
385}
386
387#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
392#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
393#[serde(rename_all = "snake_case")]
394pub enum MobStepOutputFormatInput {
395 Json,
396 Text,
397}
398
399#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
400#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
401#[serde(tag = "op", rename_all = "snake_case")]
402pub enum MobConditionExprInput {
403 Eq { path: String, value: Value },
404 In { path: String, values: Vec<Value> },
405 Gt { path: String, value: Value },
406 Lt { path: String, value: Value },
407 And { exprs: Vec<MobConditionExprInput> },
408 Or { exprs: Vec<MobConditionExprInput> },
409 Not { expr: Box<MobConditionExprInput> },
410}
411
412#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
413#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
414#[serde(deny_unknown_fields)]
415pub struct MobFrameSpecInput {
416 pub nodes: BTreeMap<String, MobFlowNodeInput>,
417}
418
419#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
420#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
421#[serde(tag = "kind", rename_all = "snake_case")]
422pub enum MobFlowNodeInput {
423 Step(MobFrameStepInput),
424 RepeatUntil(MobRepeatUntilInput),
425}
426
427#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
428#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
429#[serde(deny_unknown_fields)]
430pub struct MobFrameStepInput {
431 pub step_id: String,
432 #[serde(default, skip_serializing_if = "Vec::is_empty")]
433 pub depends_on: Vec<String>,
434 #[serde(default)]
435 pub depends_on_mode: MobDependencyModeInput,
436 #[serde(default, skip_serializing_if = "Option::is_none")]
437 pub branch: Option<String>,
438}
439
440#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
441#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
442#[serde(deny_unknown_fields)]
443pub struct MobRepeatUntilInput {
444 pub loop_id: String,
445 #[serde(default, skip_serializing_if = "Vec::is_empty")]
446 pub depends_on: Vec<String>,
447 #[serde(default)]
448 pub depends_on_mode: MobDependencyModeInput,
449 pub body: MobFrameSpecInput,
450 pub until: MobConditionExprInput,
451 pub max_iterations: u32,
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 MobFlowStepInput {
458 pub role: String,
459 pub message: WireContentInput,
460 #[serde(default, skip_serializing_if = "Vec::is_empty")]
461 pub depends_on: Vec<String>,
462 #[serde(default)]
463 pub dispatch_mode: MobDispatchModeInput,
464 #[serde(default)]
465 pub collection_policy: MobCollectionPolicyInput,
466 #[serde(default, skip_serializing_if = "Option::is_none")]
467 pub condition: Option<MobConditionExprInput>,
468 #[serde(default, skip_serializing_if = "Option::is_none")]
469 pub timeout_ms: Option<u64>,
470 #[serde(default, skip_serializing_if = "Option::is_none")]
471 pub expected_schema_ref: Option<String>,
472 #[serde(default, skip_serializing_if = "Option::is_none")]
473 pub branch: Option<String>,
474 #[serde(default)]
475 pub depends_on_mode: MobDependencyModeInput,
476 #[serde(default, skip_serializing_if = "Option::is_none")]
477 pub allowed_tools: Option<Vec<String>>,
478 #[serde(default, skip_serializing_if = "Option::is_none")]
479 pub blocked_tools: Option<Vec<String>>,
480 #[serde(default, skip_serializing_if = "Option::is_none")]
483 pub output_format: Option<MobStepOutputFormatInput>,
484}
485
486#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq, Eq)]
487#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
488#[serde(deny_unknown_fields)]
489pub struct MobFlowSpecInput {
490 #[serde(default, skip_serializing_if = "Option::is_none")]
491 pub description: Option<String>,
492 #[serde(default, skip_serializing_if = "BTreeMap::is_empty")]
493 pub steps: BTreeMap<String, MobFlowStepInput>,
494 #[serde(default, skip_serializing_if = "Option::is_none")]
495 pub root: Option<MobFrameSpecInput>,
496}
497
498#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq, Default)]
499#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
500#[serde(rename_all = "snake_case")]
501pub enum MobPolicyModeInput {
502 #[default]
503 Advisory,
504 Strict,
505}
506
507#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
508#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
509#[serde(deny_unknown_fields)]
510pub struct MobTopologyRuleInput {
511 pub from_role: String,
512 pub to_role: String,
513 pub allowed: bool,
514}
515
516#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
517#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
518#[serde(deny_unknown_fields)]
519pub struct MobTopologySpecInput {
520 pub mode: MobPolicyModeInput,
521 pub rules: Vec<MobTopologyRuleInput>,
522}
523
524#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
525#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
526#[serde(deny_unknown_fields)]
527pub struct MobSupervisorSpecInput {
528 pub role: String,
529 pub escalation_threshold: u32,
530 #[serde(default, skip_serializing_if = "Option::is_none")]
533 pub escalation_turn_timeout_ms: Option<u64>,
534}
535
536#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq, Eq)]
537#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
538#[serde(deny_unknown_fields)]
539pub struct MobLimitsSpecInput {
540 #[serde(default, skip_serializing_if = "Option::is_none")]
541 pub max_flow_duration_ms: Option<u64>,
542 #[serde(default, skip_serializing_if = "Option::is_none")]
543 pub max_step_retries: Option<u32>,
544 #[serde(default, skip_serializing_if = "Option::is_none")]
545 pub max_orphaned_turns: Option<u32>,
546 #[serde(default, skip_serializing_if = "Option::is_none")]
547 pub cancel_grace_timeout_ms: Option<u64>,
548 #[serde(default, skip_serializing_if = "Option::is_none")]
549 pub max_active_nodes: Option<u64>,
550 #[serde(default, skip_serializing_if = "Option::is_none")]
551 pub max_active_frames: Option<u64>,
552 #[serde(default, skip_serializing_if = "Option::is_none")]
553 pub max_frame_depth: Option<u64>,
554}
555
556#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
557#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
558#[serde(tag = "mode", rename_all = "snake_case")]
559pub enum MobSpawnPolicyInput {
560 None,
561 Auto {
562 profile_map: BTreeMap<String, String>,
563 },
564}
565
566#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
567#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
568#[serde(deny_unknown_fields)]
569pub struct MobEventRouterConfigInput {
570 #[serde(default = "default_event_router_buffer_size")]
571 pub buffer_size: usize,
572 #[serde(default, skip_serializing_if = "Option::is_none")]
573 pub include_patterns: Option<Vec<String>>,
574 #[serde(default, skip_serializing_if = "Option::is_none")]
575 pub exclude_patterns: Option<Vec<String>>,
576}
577
578const fn default_event_router_buffer_size() -> usize {
579 256
580}
581
582#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
591#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
592#[serde(deny_unknown_fields)]
593pub struct MobDefinitionInput {
594 pub id: String,
595 #[serde(default, skip_serializing_if = "Option::is_none")]
596 pub orchestrator: Option<MobOrchestratorInput>,
597 pub profiles: BTreeMap<String, MobProfileBindingInput>,
598 #[serde(default, skip_serializing_if = "BTreeMap::is_empty")]
602 pub models: BTreeMap<String, meerkat_core::config::CustomModelConfig>,
603 #[serde(default, skip_serializing_if = "Option::is_none")]
605 pub image_generation_provider: Option<meerkat_core::Provider>,
606 #[serde(default)]
607 pub wiring: MobWiringRulesInput,
608 #[serde(default, skip_serializing_if = "BTreeMap::is_empty")]
609 pub skills: BTreeMap<String, MobSkillSourceInput>,
610 #[serde(default)]
611 pub backend: MobBackendConfigInput,
612 #[serde(default, skip_serializing_if = "BTreeMap::is_empty")]
613 pub flows: BTreeMap<String, MobFlowSpecInput>,
614 #[serde(default, skip_serializing_if = "Option::is_none")]
615 pub topology: Option<MobTopologySpecInput>,
616 #[serde(default, skip_serializing_if = "Option::is_none")]
617 pub supervisor: Option<MobSupervisorSpecInput>,
618 #[serde(default, skip_serializing_if = "Option::is_none")]
619 pub limits: Option<MobLimitsSpecInput>,
620 #[serde(default, skip_serializing_if = "Option::is_none")]
621 pub spawn_policy: Option<MobSpawnPolicyInput>,
622 #[serde(default, skip_serializing_if = "Option::is_none")]
623 pub event_router: Option<MobEventRouterConfigInput>,
624}
625
626#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
628#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
629#[serde(deny_unknown_fields)]
630pub struct MobCreateParams {
631 pub definition: MobDefinitionInput,
632}
633
634#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
636#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
637pub struct MobCreateResult {
638 pub mob_id: String,
639}
640
641#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
643#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
644#[serde(deny_unknown_fields)]
645pub struct MobIdParams {
646 pub mob_id: String,
647}
648
649#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
651#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
652#[serde(deny_unknown_fields)]
653pub struct MobMemberParams {
654 pub mob_id: String,
655 pub agent_identity: String,
656}
657
658#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
666#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
667pub enum WireMobLifecycleStatus {
668 Creating,
669 Running,
670 Stopped,
671 Completed,
672 Destroyed,
673}
674
675#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
677#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
678pub struct MobStatusResult {
679 pub mob_id: String,
680 pub status: WireMobLifecycleStatus,
681}
682
683#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
685#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
686pub struct MobListResult {
687 pub mobs: Vec<MobStatusResult>,
688}
689
690#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
692#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
693#[serde(deny_unknown_fields)]
694pub struct MobSpawnParams {
695 pub mob_id: String,
696 pub profile: String,
697 pub agent_identity: String,
698 #[serde(default, skip_serializing_if = "Option::is_none")]
699 pub initial_message: Option<WireContentInput>,
700 #[serde(default, skip_serializing_if = "Option::is_none")]
701 pub runtime_mode: Option<WireMobRuntimeMode>,
702 #[serde(default, skip_serializing_if = "Option::is_none")]
703 pub backend: Option<WireMobBackendKind>,
704 #[serde(default, skip_serializing_if = "Option::is_none")]
705 pub labels: Option<BTreeMap<String, String>>,
706 #[serde(default, skip_serializing_if = "Option::is_none")]
707 pub context: Option<Value>,
708 #[serde(default, skip_serializing_if = "Option::is_none")]
709 pub additional_instructions: Option<Vec<String>>,
710 #[serde(default, skip_serializing_if = "Option::is_none")]
711 pub binding: Option<WireRuntimeBinding>,
712 #[serde(default, skip_serializing_if = "Option::is_none")]
713 pub shell_env: Option<BTreeMap<String, String>>,
714 #[serde(default, skip_serializing_if = "Option::is_none")]
715 pub auto_wire_parent: Option<bool>,
716 #[serde(default, skip_serializing_if = "Option::is_none")]
717 pub launch_mode: Option<WireMemberLaunchMode>,
718 #[serde(default, skip_serializing_if = "Option::is_none")]
719 pub tool_access_policy: Option<WireToolAccessPolicy>,
720 #[serde(default, skip_serializing_if = "Option::is_none")]
721 pub inherited_tool_filter: Option<WireToolFilter>,
722 #[serde(default, skip_serializing_if = "Option::is_none")]
723 pub override_profile: Option<WireMobProfile>,
724 #[serde(default, skip_serializing_if = "Option::is_none")]
725 pub model_override: Option<String>,
726 #[serde(default, skip_serializing_if = "Option::is_none")]
727 pub auth_binding: Option<WireAuthBindingRef>,
728 #[serde(default, skip_serializing_if = "Option::is_none")]
734 pub placement: Option<String>,
735}
736
737#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
739#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
740pub struct MobSpawnResult {
741 pub mob_id: String,
742 pub agent_identity: String,
743 pub member_ref: WireMemberRef,
744}
745
746#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
748#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
749#[serde(deny_unknown_fields)]
750pub struct MobSpawnSpecParams {
751 pub profile: String,
752 pub agent_identity: String,
753 #[serde(default, skip_serializing_if = "Option::is_none")]
754 pub initial_message: Option<WireContentInput>,
755 #[serde(default, skip_serializing_if = "Option::is_none")]
756 pub runtime_mode: Option<WireMobRuntimeMode>,
757 #[serde(default, skip_serializing_if = "Option::is_none")]
758 pub backend: Option<WireMobBackendKind>,
759 #[serde(default, skip_serializing_if = "Option::is_none")]
760 pub labels: Option<BTreeMap<String, String>>,
761 #[serde(default, skip_serializing_if = "Option::is_none")]
762 pub context: Option<Value>,
763 #[serde(default, skip_serializing_if = "Option::is_none")]
764 pub additional_instructions: Option<Vec<String>>,
765 #[serde(default, skip_serializing_if = "Option::is_none")]
767 pub placement: Option<WireHostRef>,
768 #[serde(default, skip_serializing_if = "Option::is_none")]
769 pub model_override: Option<String>,
770 #[serde(default, skip_serializing_if = "Option::is_none")]
771 pub auth_binding: Option<WireAuthBindingRef>,
772}
773
774#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
776#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
777#[serde(deny_unknown_fields)]
778pub struct MobSpawnManyParams {
779 pub mob_id: String,
780 pub specs: Vec<MobSpawnSpecParams>,
781}
782
783#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
785#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
786#[serde(rename_all = "snake_case")]
787pub enum MobSpawnManyResultStatus {
788 Spawned,
789 Failed,
790}
791
792#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
794#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
795#[serde(deny_unknown_fields)]
796pub struct MobSpawnManySpawnedResult {
797 pub agent_identity: String,
798 pub member_ref: WireMemberRef,
799}
800
801#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
803#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
804#[serde(rename_all = "snake_case")]
805pub enum MobSpawnManyFailureCause {
806 ProfileNotFound,
807 MemberNotFound,
808 MemberAlreadyExists,
809 NotExternallyAddressable,
810 InvalidTransition,
811 WiringError,
812 BridgeCommandRejected,
813 MemberRestoreFailed,
814 KickoffWaitTimedOut,
815 ReadyWaitTimedOut,
816 DefinitionError,
817 FlowNotFound,
818 FlowFailed,
819 RunNotFound,
820 RunCanceled,
821 FlowTurnTimedOut,
822 FrameDepthLimitExceeded,
823 FrameAtomicPersistenceUnavailable,
824 SpecRevisionConflict,
825 SchemaValidation,
826 InsufficientTargets,
827 TopologyViolation,
828 BridgeDeliveryRejected,
829 SupervisorEscalation,
830 UnsupportedForMode,
831 MissingMemberCapability,
832 ResetBarrier,
833 StorageError,
834 SessionError,
835 CommsError,
836 CallbackPending,
837 StaleFenceToken,
838 StaleEventCursor,
839 WorkNotFound,
840 Internal,
841}
842
843#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
845#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
846#[serde(deny_unknown_fields)]
847pub struct MobSpawnManyFailedResult {
848 pub cause: MobSpawnManyFailureCause,
849 pub message: String,
850}
851
852#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
854#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
855#[serde(untagged)]
856pub enum MobSpawnManyResultPayload {
857 Spawned(MobSpawnManySpawnedResult),
858 Failed(MobSpawnManyFailedResult),
859}
860
861#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
863#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
864#[serde(try_from = "MobSpawnManyResultEntryRaw")]
865pub struct MobSpawnManyResultEntry {
866 pub status: MobSpawnManyResultStatus,
867 pub result: MobSpawnManyResultPayload,
868}
869
870#[derive(Debug, Deserialize)]
871#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
872#[serde(deny_unknown_fields)]
873struct MobSpawnManyResultEntryRaw {
874 status: MobSpawnManyResultStatus,
875 result: MobSpawnManyResultPayload,
876}
877
878impl TryFrom<MobSpawnManyResultEntryRaw> for MobSpawnManyResultEntry {
879 type Error = String;
880
881 fn try_from(raw: MobSpawnManyResultEntryRaw) -> Result<Self, Self::Error> {
882 let entry = Self {
883 status: raw.status,
884 result: raw.result,
885 };
886 entry.validate().map_err(str::to_owned)?;
887 Ok(entry)
888 }
889}
890
891impl MobSpawnManyResultEntry {
892 pub fn spawned(agent_identity: impl Into<String>, member_ref: WireMemberRef) -> Self {
893 Self {
894 status: MobSpawnManyResultStatus::Spawned,
895 result: MobSpawnManyResultPayload::Spawned(MobSpawnManySpawnedResult {
896 agent_identity: agent_identity.into(),
897 member_ref,
898 }),
899 }
900 }
901
902 pub fn failed(cause: MobSpawnManyFailureCause, message: impl Into<String>) -> Self {
903 Self {
904 status: MobSpawnManyResultStatus::Failed,
905 result: MobSpawnManyResultPayload::Failed(MobSpawnManyFailedResult {
906 cause,
907 message: message.into(),
908 }),
909 }
910 }
911
912 pub fn validate(&self) -> Result<(), &'static str> {
913 match (&self.status, &self.result) {
914 (MobSpawnManyResultStatus::Spawned, MobSpawnManyResultPayload::Spawned(_))
915 | (MobSpawnManyResultStatus::Failed, MobSpawnManyResultPayload::Failed(_)) => Ok(()),
916 (MobSpawnManyResultStatus::Spawned, MobSpawnManyResultPayload::Failed(_)) => {
917 Err("mob spawn_many result status spawned requires spawned result")
918 }
919 (MobSpawnManyResultStatus::Failed, MobSpawnManyResultPayload::Spawned(_)) => {
920 Err("mob spawn_many result status failed requires failed result")
921 }
922 }
923 }
924}
925
926#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
928#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
929pub struct MobSpawnManyResult {
930 pub results: Vec<MobSpawnManyResultEntry>,
931}
932
933#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
935#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
936pub struct MobRetireResult {
937 pub retired: bool,
938}
939
940#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
942#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
943#[serde(deny_unknown_fields)]
944pub struct MobRespawnParams {
945 pub mob_id: String,
946 pub agent_identity: String,
947 #[serde(default, skip_serializing_if = "Option::is_none")]
948 pub initial_message: Option<WireContentInput>,
949}
950
951#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
953#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
954pub struct MobRespawnReceipt {
955 pub identity: String,
956 pub member_ref: WireMemberRef,
957}
958
959#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
964#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
965#[serde(rename_all = "snake_case")]
966pub enum WireMobRespawnOutcome {
967 Completed,
968 TopologyRestoreFailed,
969}
970
971#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
973#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
974pub struct MobRespawnResult {
975 pub status: WireMobRespawnOutcome,
976 pub receipt: MobRespawnReceipt,
977 #[serde(default, skip_serializing_if = "Vec::is_empty")]
978 pub failed_peer_ids: Vec<String>,
979}
980
981#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
983#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
984pub struct MobMembersResult {
985 pub mob_id: String,
986 pub members: Vec<MobMemberListEntryWire>,
987}
988
989#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
991#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
992#[serde(deny_unknown_fields)]
993pub struct MobEventsParams {
994 pub mob_id: String,
995 #[serde(default)]
996 pub after_cursor: u64,
997 #[serde(default = "default_mob_events_limit")]
998 pub limit: usize,
999 #[serde(default)]
1000 pub strict: bool,
1001}
1002
1003const fn default_mob_events_limit() -> usize {
1004 100
1005}
1006
1007#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
1009#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
1010pub struct MobEventsResult {
1011 pub events: Vec<Value>,
1012}
1013
1014#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
1016#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
1017#[serde(tag = "kind", rename_all = "snake_case", deny_unknown_fields)]
1018pub enum WireTrustedPeerIdentity {
1019 Ed25519PublicKey { public_key: String },
1021}
1022
1023#[derive(Debug, Clone, Copy, PartialEq, Eq)]
1025pub struct ResolvedWireTrustedPeerIdentity {
1026 pub peer_id: meerkat_core::comms::PeerId,
1027 pub pubkey: [u8; 32],
1028}
1029
1030#[derive(Debug, Clone, thiserror::Error, PartialEq, Eq)]
1032pub enum WireTrustedPeerIdentityError {
1033 #[error("external peer identity public_key must start with 'ed25519:'")]
1034 MissingEd25519Prefix,
1035 #[error("external peer identity public_key is not valid base64: {0}")]
1036 InvalidBase64(String),
1037 #[error("external peer identity public_key must decode to 32 bytes, got {actual}")]
1038 InvalidLength { actual: usize },
1039 #[error("external peer identity public_key must be non-zero")]
1040 ZeroPublicKey,
1041}
1042
1043impl WireTrustedPeerIdentity {
1044 pub fn resolve(&self) -> Result<ResolvedWireTrustedPeerIdentity, WireTrustedPeerIdentityError> {
1045 match self {
1046 Self::Ed25519PublicKey { public_key } => {
1047 let pubkey = parse_ed25519_public_key(public_key)?;
1048 if pubkey == [0u8; 32] {
1049 return Err(WireTrustedPeerIdentityError::ZeroPublicKey);
1050 }
1051 Ok(ResolvedWireTrustedPeerIdentity {
1052 peer_id: meerkat_core::comms::PeerId::from_ed25519_pubkey(&pubkey),
1053 pubkey,
1054 })
1055 }
1056 }
1057 }
1058}
1059
1060fn parse_ed25519_public_key(raw: &str) -> Result<[u8; 32], WireTrustedPeerIdentityError> {
1061 const PREFIX: &str = "ed25519:";
1062 let encoded = raw
1063 .strip_prefix(PREFIX)
1064 .ok_or(WireTrustedPeerIdentityError::MissingEd25519Prefix)?;
1065 let bytes = BASE64
1066 .decode(encoded)
1067 .map_err(|err| WireTrustedPeerIdentityError::InvalidBase64(err.to_string()))?;
1068 let actual = bytes.len();
1069 let pubkey: [u8; 32] = bytes
1070 .try_into()
1071 .map_err(|_| WireTrustedPeerIdentityError::InvalidLength { actual })?;
1072 Ok(pubkey)
1073}
1074
1075#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
1081#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
1082#[serde(deny_unknown_fields)]
1083pub struct WireTrustedPeerSpec {
1084 pub name: String,
1085 pub address: String,
1086 pub identity: WireTrustedPeerIdentity,
1087}
1088
1089#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
1091#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
1092#[serde(rename_all = "snake_case")]
1093pub enum MobPeerTarget {
1094 Local(String),
1095 External(WireTrustedPeerSpec),
1096}
1097
1098#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
1100#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
1101#[serde(deny_unknown_fields)]
1102pub struct MobWireParams {
1103 pub mob_id: String,
1104 pub member: String,
1105 pub peer: MobPeerTarget,
1106}
1107
1108#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
1110#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
1111pub struct MobWireResult {
1112 pub wired: bool,
1113}
1114
1115#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
1117#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
1118#[serde(deny_unknown_fields)]
1119pub struct MobWireMembersBatchEdge {
1120 pub a: String,
1121 pub b: String,
1122}
1123
1124#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
1126#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
1127#[serde(deny_unknown_fields)]
1128pub struct MobWireMembersBatchParams {
1129 pub mob_id: String,
1130 pub edges: Vec<MobWireMembersBatchEdge>,
1131}
1132
1133#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
1135#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
1136pub struct MobWireMembersBatchResult {
1137 pub requested: usize,
1138 pub wired: Vec<MobWireMembersBatchEdge>,
1139 pub already_wired: Vec<MobWireMembersBatchEdge>,
1140}
1141
1142#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
1144#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
1145#[serde(deny_unknown_fields)]
1146pub struct MobUnwireParams {
1147 pub mob_id: String,
1148 pub member: String,
1149 pub peer: MobPeerTarget,
1150}
1151
1152#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
1154#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
1155pub struct MobUnwireResult {
1156 pub unwired: bool,
1157}
1158
1159#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
1161#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
1162#[serde(deny_unknown_fields)]
1163pub struct MobMemberSendParams {
1164 pub mob_id: String,
1165 pub agent_identity: String,
1166 pub content: WireContentInput,
1167 #[serde(default)]
1168 pub handling_mode: WireHandlingMode,
1169 #[serde(default, skip_serializing_if = "Option::is_none")]
1170 pub render_metadata: Option<WireRenderMetadata>,
1171}
1172
1173#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
1175#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
1176pub struct WireAgentRuntimeId {
1177 pub identity: String,
1178 pub generation: u64,
1179}
1180
1181#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
1183#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
1184pub struct MobMemberSendResult {
1185 pub mob_id: String,
1186 pub agent_identity: String,
1188 pub member_ref: WireMemberRef,
1193 pub handling_mode: WireHandlingMode,
1194}
1195
1196#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
1202#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
1203#[serde(deny_unknown_fields)]
1204pub struct MobIngressInteractionParams {
1205 pub mob_id: String,
1206 pub spec: MobMemberSpecWire,
1207 pub content: WireContentInput,
1208 #[serde(default)]
1209 pub handling_mode: WireHandlingMode,
1210 #[serde(default, skip_serializing_if = "Option::is_none")]
1211 pub render_metadata: Option<WireRenderMetadata>,
1212}
1213
1214#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
1216#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
1217pub struct MobIngressInteractionResult {
1218 pub mob_id: String,
1219 pub agent_identity: String,
1220 pub member_ref: WireMemberRef,
1221 pub ensure_outcome: MobEnsureMemberOutcomeWire,
1222 pub delivery: MobMemberSendResult,
1223 pub events_after_cursor: u64,
1225 pub latest_event_cursor: u64,
1227}
1228
1229#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq, Default)]
1231#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
1232#[serde(rename_all = "snake_case")]
1233pub enum WireHandlingMode {
1234 #[default]
1235 Queue,
1236 Steer,
1237}
1238
1239impl From<WireHandlingMode> for HandlingMode {
1240 fn from(mode: WireHandlingMode) -> Self {
1241 match mode {
1242 WireHandlingMode::Queue => HandlingMode::Queue,
1243 WireHandlingMode::Steer => HandlingMode::Steer,
1244 }
1245 }
1246}
1247
1248impl From<HandlingMode> for WireHandlingMode {
1249 fn from(mode: HandlingMode) -> Self {
1250 match mode {
1251 HandlingMode::Queue => WireHandlingMode::Queue,
1252 HandlingMode::Steer => WireHandlingMode::Steer,
1253 }
1254 }
1255}
1256
1257#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
1259#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
1260#[serde(rename_all = "snake_case")]
1261pub enum WireRenderClass {
1262 UserPrompt,
1263 PeerMessage,
1264 PeerRequest,
1265 PeerResponse,
1266 ExternalEvent,
1267 FlowStep,
1268 Continuation,
1269 SystemNotice,
1270 ToolScopeNotice,
1271 OpsProgress,
1272}
1273
1274impl From<WireRenderClass> for RenderClass {
1275 fn from(class: WireRenderClass) -> Self {
1276 match class {
1277 WireRenderClass::UserPrompt => RenderClass::UserPrompt,
1278 WireRenderClass::PeerMessage => RenderClass::PeerMessage,
1279 WireRenderClass::PeerRequest => RenderClass::PeerRequest,
1280 WireRenderClass::PeerResponse => RenderClass::PeerResponse,
1281 WireRenderClass::ExternalEvent => RenderClass::ExternalEvent,
1282 WireRenderClass::FlowStep => RenderClass::FlowStep,
1283 WireRenderClass::Continuation => RenderClass::Continuation,
1284 WireRenderClass::SystemNotice => RenderClass::SystemNotice,
1285 WireRenderClass::ToolScopeNotice => RenderClass::ToolScopeNotice,
1286 WireRenderClass::OpsProgress => RenderClass::OpsProgress,
1287 }
1288 }
1289}
1290
1291impl From<RenderClass> for WireRenderClass {
1292 fn from(class: RenderClass) -> Self {
1293 match class {
1294 RenderClass::UserPrompt => WireRenderClass::UserPrompt,
1295 RenderClass::PeerMessage => WireRenderClass::PeerMessage,
1296 RenderClass::PeerRequest => WireRenderClass::PeerRequest,
1297 RenderClass::PeerResponse => WireRenderClass::PeerResponse,
1298 RenderClass::ExternalEvent => WireRenderClass::ExternalEvent,
1299 RenderClass::FlowStep => WireRenderClass::FlowStep,
1300 RenderClass::Continuation => WireRenderClass::Continuation,
1301 RenderClass::SystemNotice => WireRenderClass::SystemNotice,
1302 RenderClass::ToolScopeNotice => WireRenderClass::ToolScopeNotice,
1303 RenderClass::OpsProgress => WireRenderClass::OpsProgress,
1304 }
1305 }
1306}
1307
1308#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
1310#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
1311#[serde(rename_all = "snake_case")]
1312pub enum WireRenderSalience {
1313 Background,
1314 Normal,
1315 Important,
1316 Urgent,
1317}
1318
1319impl From<WireRenderSalience> for RenderSalience {
1320 fn from(salience: WireRenderSalience) -> Self {
1321 match salience {
1322 WireRenderSalience::Background => RenderSalience::Background,
1323 WireRenderSalience::Normal => RenderSalience::Normal,
1324 WireRenderSalience::Important => RenderSalience::Important,
1325 WireRenderSalience::Urgent => RenderSalience::Urgent,
1326 }
1327 }
1328}
1329
1330impl From<RenderSalience> for WireRenderSalience {
1331 fn from(salience: RenderSalience) -> Self {
1332 match salience {
1333 RenderSalience::Background => WireRenderSalience::Background,
1334 RenderSalience::Normal => WireRenderSalience::Normal,
1335 RenderSalience::Important => WireRenderSalience::Important,
1336 RenderSalience::Urgent => WireRenderSalience::Urgent,
1337 }
1338 }
1339}
1340
1341#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
1343#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
1344pub struct WireRenderMetadata {
1345 pub class: WireRenderClass,
1346 #[serde(default, skip_serializing_if = "Option::is_none")]
1347 pub salience: Option<WireRenderSalience>,
1348}
1349
1350impl From<WireRenderMetadata> for RenderMetadata {
1351 fn from(metadata: WireRenderMetadata) -> Self {
1352 Self {
1353 class: metadata.class.into(),
1354 salience: metadata
1355 .salience
1356 .unwrap_or(WireRenderSalience::Normal)
1357 .into(),
1358 }
1359 }
1360}
1361
1362impl From<RenderMetadata> for WireRenderMetadata {
1363 fn from(metadata: RenderMetadata) -> Self {
1364 Self {
1365 class: metadata.class.into(),
1366 salience: Some(metadata.salience.into()),
1367 }
1368 }
1369}
1370
1371#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
1386#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
1387pub struct MobMemberSpecWire {
1388 pub profile: String,
1390 pub agent_identity: String,
1392 #[serde(default, skip_serializing_if = "Option::is_none")]
1393 pub initial_message: Option<WireContentInput>,
1394 #[serde(default, skip_serializing_if = "Option::is_none")]
1395 pub runtime_mode: Option<WireMobRuntimeMode>,
1396 #[serde(default, skip_serializing_if = "Option::is_none")]
1397 pub backend: Option<WireMobBackendKind>,
1398 #[serde(default, skip_serializing_if = "Option::is_none")]
1400 pub placement: Option<WireHostRef>,
1401 #[serde(default, skip_serializing_if = "Option::is_none")]
1402 pub binding: Option<WireRuntimeBinding>,
1403 #[serde(default, skip_serializing_if = "Option::is_none")]
1404 pub context: Option<Value>,
1405 #[serde(default, skip_serializing_if = "Option::is_none")]
1406 pub labels: Option<BTreeMap<String, String>>,
1407 #[serde(default, skip_serializing_if = "Option::is_none")]
1408 pub additional_instructions: Option<Vec<String>>,
1409 #[serde(default, skip_serializing_if = "Option::is_none")]
1410 pub auto_wire_parent: Option<bool>,
1411}
1412
1413impl MobMemberSpecWire {
1414 #[must_use]
1417 pub fn surface_metadata(&self) -> SurfaceMetadata {
1418 SurfaceMetadata::from_optional_parts(self.labels.clone(), self.context.clone())
1419 }
1420
1421 pub fn validate_public_surface_metadata(&self) -> Result<(), SurfaceMetadataError> {
1423 self.surface_metadata().validate_public()
1424 }
1425}
1426
1427#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
1429#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
1430#[serde(deny_unknown_fields)]
1431pub struct MobEnsureMemberParams {
1432 pub mob_id: String,
1433 pub spec: MobMemberSpecWire,
1434}
1435
1436#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, Hash)]
1448#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
1449#[serde(transparent)]
1450pub struct WireMemberRef(String);
1451
1452impl WireMemberRef {
1453 #[must_use]
1457 pub fn encode(mob_id: &str, agent_identity: &str) -> Self {
1458 let payload = serde_json::json!({ "m": mob_id, "a": agent_identity });
1462 Self(base64_url_encode(payload.to_string().as_bytes()))
1463 }
1464
1465 #[must_use]
1467 pub fn as_str(&self) -> &str {
1468 &self.0
1469 }
1470
1471 #[must_use]
1474 pub fn from_token(token: impl Into<String>) -> Self {
1475 Self(token.into())
1476 }
1477
1478 pub fn decode(&self) -> Result<(String, String), WireMemberRefError> {
1481 let bytes = base64_url_decode(&self.0).map_err(|_| WireMemberRefError::Malformed)?;
1482 let value: Value =
1483 serde_json::from_slice(&bytes).map_err(|_| WireMemberRefError::Malformed)?;
1484 let mob_id = value
1485 .get("m")
1486 .and_then(Value::as_str)
1487 .ok_or(WireMemberRefError::Malformed)?;
1488 let agent_identity = value
1489 .get("a")
1490 .and_then(Value::as_str)
1491 .ok_or(WireMemberRefError::Malformed)?;
1492 Ok((mob_id.to_string(), agent_identity.to_string()))
1493 }
1494}
1495
1496#[derive(Debug, thiserror::Error)]
1498pub enum WireMemberRefError {
1499 #[error("malformed member ref token")]
1502 Malformed,
1503}
1504
1505fn base64_url_encode(bytes: &[u8]) -> String {
1506 use base64::Engine as _;
1507 base64::engine::general_purpose::URL_SAFE_NO_PAD.encode(bytes)
1508}
1509
1510fn base64_url_decode(input: &str) -> Result<Vec<u8>, base64::DecodeError> {
1511 use base64::Engine as _;
1512 base64::engine::general_purpose::URL_SAFE_NO_PAD.decode(input)
1513}
1514
1515#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
1517#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
1518pub struct MobSpawnReceiptWire {
1519 pub agent_identity: String,
1520 pub member_ref: WireMemberRef,
1524}
1525
1526#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
1528#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
1529#[serde(rename_all = "snake_case")]
1530pub enum WireMobMemberStatus {
1531 Active,
1532 Retiring,
1533 Broken,
1534 Completed,
1535 Unknown,
1536}
1537
1538#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
1543#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
1544pub struct MobMemberListEntryWire {
1545 pub agent_identity: String,
1546 pub member_ref: WireMemberRef,
1547 pub role: String,
1548 pub runtime_mode: WireMobRuntimeMode,
1549 #[serde(default, skip_serializing_if = "Vec::is_empty")]
1550 pub wired_to: Vec<String>,
1551 #[serde(default, skip_serializing_if = "BTreeMap::is_empty")]
1552 pub labels: BTreeMap<String, String>,
1553 pub status: WireMobMemberStatus,
1554 #[serde(default, skip_serializing_if = "Option::is_none")]
1555 pub error: Option<String>,
1556 pub is_final: bool,
1557}
1558
1559#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
1565#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
1566pub enum MobEnsureMemberOutcomeWire {
1567 #[serde(rename = "spawned")]
1568 Spawned(MobSpawnReceiptWire),
1569 #[serde(rename = "existed")]
1570 Existed(MobMemberListEntryWire),
1571}
1572
1573#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
1575#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
1576pub struct MobEnsureMemberResult {
1577 pub outcome: MobEnsureMemberOutcomeWire,
1578}
1579
1580#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq, Eq)]
1582#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
1583#[serde(deny_unknown_fields)]
1584pub struct MobReconcileOptionsWire {
1585 #[serde(default)]
1588 pub retire_stale: bool,
1589}
1590
1591#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
1593#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
1594#[serde(rename_all = "snake_case")]
1595pub enum WireMobReconcileStage {
1596 Spawn,
1597 Retire,
1598}
1599
1600#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
1602#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
1603#[serde(deny_unknown_fields)]
1604pub struct MobReconcileParams {
1605 pub mob_id: String,
1606 #[serde(default)]
1607 pub desired: Vec<MobMemberSpecWire>,
1608 #[serde(default)]
1609 pub options: MobReconcileOptionsWire,
1610}
1611
1612#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
1617#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
1618pub struct WireMobError {
1619 pub code: MobSpawnManyFailureCause,
1620 pub message: String,
1621}
1622
1623#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
1625#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
1626pub struct MobReconcileFailureWire {
1627 pub agent_identity: String,
1628 pub stage: WireMobReconcileStage,
1629 pub error: WireMobError,
1631}
1632
1633#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq)]
1635#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
1636pub struct MobReconcileReportWire {
1637 #[serde(default, skip_serializing_if = "Vec::is_empty")]
1638 pub desired: Vec<String>,
1639 #[serde(default, skip_serializing_if = "Vec::is_empty")]
1640 pub retained: Vec<String>,
1641 #[serde(default, skip_serializing_if = "Vec::is_empty")]
1642 pub spawned: Vec<MobSpawnReceiptWire>,
1643 #[serde(default, skip_serializing_if = "Vec::is_empty")]
1644 pub retired: Vec<String>,
1645 #[serde(default, skip_serializing_if = "Vec::is_empty")]
1646 pub failures: Vec<MobReconcileFailureWire>,
1647}
1648
1649#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
1651#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
1652pub struct MobReconcileResult {
1653 pub report: MobReconcileReportWire,
1654}
1655
1656#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
1661#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
1662#[serde(rename_all = "snake_case")]
1663pub enum WireMobLifecycleAction {
1664 Stop,
1665 Resume,
1666 Complete,
1667 Reset,
1668 Destroy,
1669}
1670
1671#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
1676#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
1677#[serde(rename_all = "snake_case")]
1678pub enum WireMobWireAction {
1679 Wire,
1680 Unwire,
1681}
1682
1683#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
1685#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
1686#[serde(deny_unknown_fields)]
1687pub struct MobLifecycleParams {
1688 pub mob_id: String,
1689 pub action: WireMobLifecycleAction,
1690}
1691
1692#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
1694#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
1695pub struct MobLifecycleResult {
1696 pub mob_id: String,
1697 pub action: WireMobLifecycleAction,
1698 pub ok: bool,
1699 #[serde(default, skip_serializing_if = "Option::is_none")]
1700 pub destroy_report: Option<Value>,
1701}
1702
1703#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
1705#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
1706#[serde(deny_unknown_fields)]
1707pub struct MobAppendSystemContextParams {
1708 pub mob_id: String,
1709 pub agent_identity: String,
1710 pub text: String,
1711 #[serde(default, skip_serializing_if = "Option::is_none")]
1712 pub source: Option<String>,
1713 #[serde(default, skip_serializing_if = "Option::is_none")]
1714 pub idempotency_key: Option<String>,
1715}
1716
1717#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
1722#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
1723#[serde(rename_all = "snake_case")]
1724pub enum WireAppendSystemContextStatus {
1725 Applied,
1726 Staged,
1727 Duplicate,
1728}
1729
1730impl From<meerkat_core::AppendSystemContextStatus> for WireAppendSystemContextStatus {
1731 fn from(status: meerkat_core::AppendSystemContextStatus) -> Self {
1732 match status {
1733 meerkat_core::AppendSystemContextStatus::Applied => Self::Applied,
1734 meerkat_core::AppendSystemContextStatus::Staged => Self::Staged,
1735 meerkat_core::AppendSystemContextStatus::Duplicate => Self::Duplicate,
1736 }
1737 }
1738}
1739
1740#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
1742#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
1743pub struct MobAppendSystemContextResult {
1744 pub mob_id: String,
1745 pub agent_identity: String,
1746 pub status: WireAppendSystemContextStatus,
1747}
1748
1749#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
1751#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
1752pub struct MobFlowsResult {
1753 pub mob_id: String,
1754 pub flows: Vec<String>,
1755}
1756
1757#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
1759#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
1760#[serde(deny_unknown_fields)]
1761pub struct MobFlowRunParams {
1762 pub mob_id: String,
1763 pub flow_id: String,
1764 #[serde(default)]
1765 pub params: Value,
1766}
1767
1768#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
1773#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
1774#[serde(deny_unknown_fields)]
1775pub struct MobRunParams {
1776 pub mob_id: String,
1777 #[serde(default, skip_serializing_if = "Option::is_none")]
1778 pub flow_id: Option<String>,
1779 #[serde(default, skip_serializing_if = "Option::is_none")]
1780 pub prompt: Option<String>,
1781 #[serde(default)]
1782 pub params: Value,
1783}
1784
1785#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
1787#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
1788pub struct MobFlowRunResult {
1789 pub run_id: String,
1790}
1791
1792#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
1794#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
1795#[serde(deny_unknown_fields)]
1796pub struct MobFlowStatusParams {
1797 pub mob_id: String,
1798 pub run_id: String,
1799}
1800
1801#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
1805#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
1806#[serde(rename_all = "snake_case")]
1807pub enum WireMobRunStatus {
1808 Pending,
1809 Running,
1810 Completed,
1811 Failed,
1812 Canceled,
1813}
1814
1815#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
1823#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
1824pub struct WireMobRun {
1825 pub run_id: String,
1826 pub mob_id: String,
1827 pub flow_id: String,
1828 pub status: WireMobRunStatus,
1829 #[serde(flatten)]
1832 pub kernel: serde_json::Map<String, Value>,
1833}
1834
1835#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
1839#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
1840pub struct MobFlowStatusResult {
1841 #[serde(default, skip_serializing_if = "Option::is_none")]
1842 pub run: Option<WireMobRun>,
1843}
1844
1845#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
1847#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
1848#[serde(deny_unknown_fields)]
1849pub struct MobRunResultParams {
1850 pub mob_id: String,
1851 pub run_id: String,
1852}
1853
1854#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
1856#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
1857#[serde(deny_unknown_fields)]
1858pub struct WireMobRunResultEnvelope {
1859 pub run_id: String,
1860 pub mob_id: String,
1861 pub flow_id: String,
1862 pub status: WireMobRunStatus,
1863 #[serde(default, skip_serializing_if = "Option::is_none")]
1864 pub result: Option<Value>,
1865 #[serde(default, skip_serializing_if = "BTreeMap::is_empty")]
1866 pub outputs: BTreeMap<String, Value>,
1867}
1868
1869#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
1873#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
1874pub struct MobRunResult {
1875 #[serde(default, skip_serializing_if = "Option::is_none")]
1876 pub run: Option<WireMobRunResultEnvelope>,
1877}
1878
1879#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
1881#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
1882#[serde(deny_unknown_fields)]
1883pub struct MobFlowCancelParams {
1884 pub mob_id: String,
1885 pub run_id: String,
1886}
1887
1888#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
1890#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
1891pub struct MobFlowCancelResult {
1892 pub canceled: bool,
1893}
1894
1895#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
1897#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
1898#[serde(deny_unknown_fields)]
1899pub struct MobSpawnHelperParams {
1900 pub mob_id: String,
1901 pub prompt: String,
1902 #[serde(default, skip_serializing_if = "Option::is_none")]
1903 pub agent_identity: Option<String>,
1904 #[serde(default, skip_serializing_if = "Option::is_none")]
1905 pub role_name: Option<String>,
1906 #[serde(default, skip_serializing_if = "Option::is_none")]
1907 pub model_override: Option<String>,
1908 #[serde(default, skip_serializing_if = "Option::is_none")]
1909 pub auth_binding: Option<WireAuthBindingRef>,
1910 #[serde(default, skip_serializing_if = "Option::is_none")]
1911 pub runtime_mode: Option<WireMobRuntimeMode>,
1912 #[serde(default, skip_serializing_if = "Option::is_none")]
1913 pub backend: Option<WireMobBackendKind>,
1914}
1915
1916#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
1918#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
1919#[serde(deny_unknown_fields)]
1920pub struct MobForkHelperParams {
1921 pub mob_id: String,
1922 pub source_member_id: String,
1923 pub prompt: String,
1924 #[serde(default, skip_serializing_if = "Option::is_none")]
1925 pub agent_identity: Option<String>,
1926 #[serde(default, skip_serializing_if = "Option::is_none")]
1927 pub role_name: Option<String>,
1928 #[serde(default, skip_serializing_if = "Option::is_none")]
1929 pub model_override: Option<String>,
1930 #[serde(default, skip_serializing_if = "Option::is_none")]
1931 pub auth_binding: Option<WireAuthBindingRef>,
1932 #[serde(default, skip_serializing_if = "Option::is_none")]
1933 pub fork_context: Option<Value>,
1934 #[serde(default, skip_serializing_if = "Option::is_none")]
1935 pub runtime_mode: Option<WireMobRuntimeMode>,
1936 #[serde(default, skip_serializing_if = "Option::is_none")]
1937 pub backend: Option<WireMobBackendKind>,
1938}
1939
1940#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
1942#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
1943pub struct MobHelperResult {
1944 #[serde(default, skip_serializing_if = "Option::is_none")]
1945 pub output: Option<String>,
1946 pub tokens_used: u64,
1947 pub agent_identity: String,
1948 pub member_ref: WireMemberRef,
1949}
1950
1951#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
1953#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
1954pub struct MobForceCancelResult {
1955 pub cancelled: bool,
1956}
1957
1958#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
1966#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
1967#[serde(deny_unknown_fields)]
1968pub struct MobTurnStartParams {
1969 pub mob_id: String,
1970 pub agent_identity: String,
1971 pub prompt: WireContentInput,
1972 #[serde(default, skip_serializing_if = "Option::is_none")]
1973 pub skill_refs: Option<Vec<meerkat_core::skills::SkillRef>>,
1974 #[serde(default, skip_serializing_if = "Option::is_none")]
1975 pub turn_tool_overlay: Option<meerkat_core::service::PublicTurnToolOverlay>,
1976 #[serde(default, skip_serializing_if = "Option::is_none")]
1977 pub additional_instructions: Option<Vec<String>>,
1978 #[serde(default, skip_serializing_if = "Option::is_none")]
1979 pub keep_alive: Option<bool>,
1980 #[serde(default, skip_serializing_if = "Option::is_none")]
1981 pub model: Option<String>,
1982 #[serde(default, skip_serializing_if = "Option::is_none")]
1983 pub provider: Option<String>,
1984 #[serde(default, skip_serializing_if = "Option::is_none")]
1986 pub self_hosted_server_id: Option<String>,
1987 #[serde(default, skip_serializing_if = "Option::is_none")]
1988 pub max_tokens: Option<u32>,
1989 #[serde(default, skip_serializing_if = "Option::is_none")]
1990 pub system_prompt: Option<String>,
1991 #[serde(default, skip_serializing_if = "Option::is_none")]
1992 pub output_schema: Option<Value>,
1993 #[serde(default, skip_serializing_if = "Option::is_none")]
1994 pub structured_output_retries: Option<u32>,
1995 #[serde(default, skip_serializing_if = "Option::is_none")]
1996 pub provider_params:
1997 Option<WireTurnMetadataOverride<crate::wire::runtime::WireProviderParamsOverride>>,
1998 #[serde(default, skip_serializing_if = "Option::is_none")]
1999 pub auth_binding: Option<WireTurnMetadataOverride<WireAuthBindingRef>>,
2000 #[serde(default, skip_serializing_if = "Option::is_none")]
2005 pub injected_context: Option<Vec<WireContentInput>>,
2006}
2007
2008#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
2010#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
2011pub struct WireUnreachablePeer {
2012 pub peer: String,
2013 #[serde(default, skip_serializing_if = "Option::is_none")]
2014 pub reason: Option<String>,
2015}
2016
2017#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
2020#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
2021pub struct WirePeerConnectivitySnapshot {
2022 pub reachable_peer_count: usize,
2023 pub unknown_peer_count: usize,
2024 #[serde(default, skip_serializing_if = "Vec::is_empty")]
2025 pub unreachable_peers: Vec<WireUnreachablePeer>,
2026}
2027
2028#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
2037#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
2038#[serde(tag = "status", rename_all = "snake_case")]
2039pub enum WirePeerConnectivity {
2040 NotApplicable,
2043 ProbeTimedOut,
2045 Known {
2047 snapshot: WirePeerConnectivitySnapshot,
2048 },
2049}
2050
2051#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
2053#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
2054pub struct MobMemberStatusResult {
2055 pub status: WireMobMemberStatus,
2056 pub member_ref: WireMemberRef,
2058 #[serde(default, skip_serializing_if = "Option::is_none")]
2059 pub output_preview: Option<String>,
2060 #[serde(default, skip_serializing_if = "Option::is_none")]
2061 pub error: Option<String>,
2062 pub tokens_used: u64,
2063 pub is_final: bool,
2064 #[serde(default, skip_serializing_if = "Option::is_none")]
2065 pub current_session_id: Option<String>,
2066 #[serde(default, skip_serializing_if = "Option::is_none")]
2067 pub peer_connectivity: Option<WirePeerConnectivity>,
2068 #[serde(default, skip_serializing_if = "Option::is_none")]
2069 pub kickoff: Option<Value>,
2070 #[serde(default, skip_serializing_if = "Option::is_none")]
2071 pub external_member: Option<Value>,
2072 #[serde(default, skip_serializing_if = "Option::is_none")]
2073 pub resolved_capabilities: Option<crate::wire::WireResolvedModelCapabilities>,
2074 #[serde(default, skip_serializing_if = "Option::is_none")]
2075 pub progress: Option<WireMemberProgressSnapshot>,
2076 #[serde(default, skip_serializing_if = "Option::is_none")]
2082 pub placement: Option<WireHostRef>,
2083 #[serde(default, skip_serializing_if = "Option::is_none")]
2085 pub control_reachability: Option<WireReachability>,
2086 #[serde(default, skip_serializing_if = "Option::is_none")]
2088 pub comms_reachability: Option<WireReachability>,
2089 #[serde(default, skip_serializing_if = "Option::is_none")]
2092 pub last_seen_ms: Option<u64>,
2093 #[serde(default, skip_serializing_if = "Option::is_none")]
2094 pub freshness_reason: Option<String>,
2095 #[serde(default, skip_serializing_if = "Option::is_none")]
2097 pub lifecycle_capabilities: Option<WireMemberLifecycleCapabilities>,
2098 #[serde(default, skip_serializing_if = "Option::is_none")]
2101 pub non_portable_disabled: Option<Vec<super::portable_spec::WireNonPortableResourceKind>>,
2102}
2103
2104#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
2105#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
2106#[serde(rename_all = "snake_case")]
2107pub enum WireMemberRunState {
2108 Idle,
2109 RunOpen,
2110 Unknown,
2111}
2112
2113#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
2114#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
2115#[serde(rename_all = "snake_case")]
2116pub enum WireMemberHealthClass {
2117 Healthy,
2118 Degraded,
2119 Wedged,
2120 Unknown,
2121}
2122
2123#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
2124#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
2125#[serde(rename_all = "snake_case")]
2126pub enum WireMemberProgressEvent {
2127 ExecutionAdvanced,
2128 BecameIdle,
2129 Unchanged,
2130}
2131
2132#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
2133#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
2134pub struct WireMemberProgressSnapshot {
2135 pub run_state: WireMemberRunState,
2136 pub in_flight_work: u64,
2137 pub last_progress_at_ms: u64,
2138 pub last_progress_event: WireMemberProgressEvent,
2139 pub health: WireMemberHealthClass,
2140}
2141
2142#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
2144#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
2145pub struct MobSnapshotResult {
2146 pub mob_id: String,
2147 pub status: WireMobLifecycleStatus,
2148 pub members: Vec<MobMemberListEntryWire>,
2149}
2150
2151#[cfg(test)]
2152mod member_status_capability_tests {
2153 use super::*;
2154
2155 #[test]
2156 fn member_status_result_round_trips_resolved_capabilities() -> Result<(), serde_json::Error> {
2157 let capabilities = crate::wire::WireResolvedModelCapabilities {
2158 vision: true,
2159 image_input: true,
2160 image_tool_results: false,
2161 inline_video: false,
2162 realtime: true,
2163 web_search: true,
2164 image_generation: true,
2165 };
2166 let result = MobMemberStatusResult {
2167 status: WireMobMemberStatus::Active,
2168 member_ref: WireMemberRef::encode("mob-1", "worker-1"),
2169 output_preview: None,
2170 error: None,
2171 tokens_used: 0,
2172 is_final: false,
2173 current_session_id: Some("session-1".to_string()),
2174 peer_connectivity: Some(WirePeerConnectivity::Known {
2175 snapshot: WirePeerConnectivitySnapshot {
2176 reachable_peer_count: 1,
2177 unknown_peer_count: 0,
2178 unreachable_peers: Vec::new(),
2179 },
2180 }),
2181 kickoff: None,
2182 external_member: None,
2183 resolved_capabilities: Some(capabilities.clone()),
2184 progress: None,
2185 placement: None,
2186 control_reachability: None,
2187 comms_reachability: None,
2188 last_seen_ms: None,
2189 freshness_reason: None,
2190 lifecycle_capabilities: None,
2191 non_portable_disabled: None,
2192 };
2193
2194 let json = serde_json::to_string(&result)?;
2195 assert!(json.contains("\"resolved_capabilities\""));
2196 let parsed: MobMemberStatusResult = serde_json::from_str(&json)?;
2197 assert_eq!(parsed.resolved_capabilities, Some(capabilities));
2198 Ok(())
2199 }
2200}
2201
2202#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
2204#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
2205pub struct MobDestroyResult {
2206 pub mob_id: String,
2207 pub ok: bool,
2208 pub destroy_report: Value,
2209}
2210
2211#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
2213#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
2214pub struct MobRotateSupervisorResult {
2215 pub mob_id: String,
2216 pub ok: bool,
2217 pub report: SupervisorRotationReportWire,
2218}
2219
2220#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
2222#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
2223pub struct SupervisorRotationReportWire {
2224 pub previous_epoch: u64,
2225 pub current_epoch: u64,
2226 pub public_peer_id: String,
2227}
2228
2229#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
2231#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
2232#[serde(rename_all = "snake_case")]
2233pub enum SupervisorRotationIncompleteKind {
2234 SupervisorRotationIncomplete,
2235}
2236
2237#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
2239#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
2240#[serde(rename_all = "snake_case")]
2241pub enum SupervisorRotationRetryAuthority {
2242 PendingRotation,
2243 PreRotation,
2244}
2245
2246#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
2248#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
2249#[serde(rename_all = "snake_case")]
2250pub enum SupervisorRotationRetryScope {
2251 Durable,
2252 PreRotation,
2253}
2254
2255#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
2257#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
2258#[serde(rename_all = "snake_case")]
2259pub struct SupervisorRotationIncompleteDetailsWire {
2260 pub kind: SupervisorRotationIncompleteKind,
2261 pub previous_epoch: u64,
2262 pub attempted_epoch: u64,
2263 pub attempted_public_peer_id: String,
2264 pub rotated_peer_count: usize,
2265 pub rollback_succeeded: bool,
2266 pub pending_authority_recorded: bool,
2267 #[serde(default, skip_serializing_if = "Option::is_none")]
2268 pub rollback_error: Option<String>,
2269 pub retry_authority: SupervisorRotationRetryAuthority,
2270 pub retry_scope: SupervisorRotationRetryScope,
2271}
2272
2273#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
2275#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
2276#[serde(rename_all = "snake_case")]
2277pub struct SupervisorRotationIncompleteDataWire {
2278 pub code: String,
2279 pub message: String,
2280 pub details: SupervisorRotationIncompleteDetailsWire,
2281}
2282
2283#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
2285#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
2286#[serde(deny_unknown_fields)]
2287pub struct MobWaitParams {
2288 pub mob_id: String,
2289 #[serde(default, skip_serializing_if = "Option::is_none")]
2290 pub member_ids: Option<Vec<String>>,
2291 #[serde(default, skip_serializing_if = "Option::is_none")]
2292 pub timeout_ms: Option<u64>,
2293}
2294
2295#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
2297#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
2298pub struct MobWaitMembersResult {
2299 pub members: Vec<Value>,
2300}
2301
2302#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
2304#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
2305pub struct MobCancelWorkResult {
2306 pub mob_id: String,
2307 pub ok: bool,
2308}
2309
2310#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
2312#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
2313pub struct MobCancelAllWorkResult {
2314 pub mob_id: String,
2315 pub ok: bool,
2316}
2317
2318#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
2320#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
2321#[serde(deny_unknown_fields)]
2322pub struct MobProfileCreateParams {
2323 pub name: String,
2324 pub profile: MobProfileInput,
2325}
2326
2327#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
2329#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
2330#[serde(deny_unknown_fields)]
2331pub struct MobProfileNameParams {
2332 pub name: String,
2333}
2334
2335#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
2337#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
2338#[serde(deny_unknown_fields)]
2339pub struct MobProfileUpdateParams {
2340 pub name: String,
2341 pub profile: MobProfileInput,
2342 pub expected_revision: u64,
2343}
2344
2345#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
2347#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
2348#[serde(deny_unknown_fields)]
2349pub struct MobProfileDeleteParams {
2350 pub name: String,
2351 pub expected_revision: u64,
2352}
2353
2354#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
2356#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
2357pub struct MobProfileLookupResult {
2358 #[serde(default)]
2359 pub not_found: bool,
2360 pub name: String,
2361 #[serde(default, skip_serializing_if = "Option::is_none")]
2362 pub profile: Option<WireMobProfile>,
2363 #[serde(default, skip_serializing_if = "Option::is_none")]
2364 pub revision: Option<u64>,
2365 #[serde(default, skip_serializing_if = "Option::is_none")]
2366 pub created_at: Option<String>,
2367 #[serde(default, skip_serializing_if = "Option::is_none")]
2368 pub updated_at: Option<String>,
2369}
2370
2371#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
2373#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
2374pub struct MobProfileListResult {
2375 pub profiles: Vec<MobProfileLookupResult>,
2376}
2377
2378#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
2380#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
2381pub struct MobProfileDeleteResult {
2382 pub name: String,
2383 pub deleted_revision: u64,
2384}
2385
2386#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
2388#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
2389#[serde(deny_unknown_fields)]
2390pub struct MobStreamOpenParams {
2391 pub mob_id: String,
2392 #[serde(default, skip_serializing_if = "Option::is_none")]
2393 pub agent_identity: Option<String>,
2394}
2395
2396#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
2398#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
2399pub struct MobStreamOpenResult {
2400 pub stream_id: String,
2401 pub opened: bool,
2402}
2403
2404#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
2406#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
2407#[serde(deny_unknown_fields)]
2408pub struct MobStreamCloseParams {
2409 pub stream_id: String,
2410}
2411
2412#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
2414#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
2415pub struct MobStreamCloseResult {
2416 pub stream_id: String,
2417 pub closed: bool,
2418 pub already_closed: bool,
2419}
2420
2421#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq, Default)]
2424#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
2425#[serde(rename_all = "snake_case")]
2426pub enum WireWorkOrigin {
2427 #[default]
2428 External,
2429 Internal,
2430}
2431
2432#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
2438#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
2439#[serde(deny_unknown_fields)]
2440pub struct MobSubmitWorkParams {
2441 pub member_ref: WireMemberRef,
2442 #[serde(default, skip_serializing_if = "Option::is_none")]
2445 pub work_ref: Option<String>,
2446 pub content: WireContentInput,
2447 #[serde(default)]
2448 pub origin: WireWorkOrigin,
2449 #[serde(default, skip_serializing_if = "Option::is_none")]
2455 pub injected_context: Option<Vec<WireContentInput>>,
2456 #[serde(default, skip_serializing_if = "Option::is_none")]
2458 pub objective_id: Option<String>,
2459}
2460
2461#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
2463#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
2464pub struct MobSubmitWorkResult {
2465 pub mob_id: String,
2466 pub work_ref: String,
2467 pub member_ref: WireMemberRef,
2468 #[serde(default, skip_serializing_if = "Option::is_none")]
2469 pub objective_id: Option<String>,
2470}
2471
2472#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
2474#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
2475#[serde(deny_unknown_fields)]
2476pub struct MobConcludeObjectiveParams {
2477 pub member_ref: WireMemberRef,
2478 pub objective_id: String,
2479 pub outcome: String,
2480}
2481
2482#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
2483#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
2484pub struct MobConcludeObjectiveResult {
2485 pub member_ref: WireMemberRef,
2486 pub objective_id: String,
2487 pub concluded: bool,
2488}
2489
2490#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
2492#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
2493#[serde(deny_unknown_fields)]
2494pub struct MobCancelWorkParams {
2495 pub mob_id: String,
2496 pub work_ref: String,
2497}
2498
2499#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
2501#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
2502#[serde(deny_unknown_fields)]
2503pub struct MobCancelAllWorkParams {
2504 pub member_ref: WireMemberRef,
2505}
2506
2507#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq, Eq)]
2510#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
2511#[serde(deny_unknown_fields)]
2512pub struct MobMemberFilterWire {
2513 #[serde(default, skip_serializing_if = "BTreeMap::is_empty")]
2515 pub labels: BTreeMap<String, String>,
2516 #[serde(default, skip_serializing_if = "Option::is_none")]
2518 pub role: Option<String>,
2519 #[serde(default, skip_serializing_if = "Option::is_none")]
2521 pub status: Option<WireMobMemberStatus>,
2522}
2523
2524#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
2526#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
2527#[serde(deny_unknown_fields)]
2528pub struct MobListMembersMatchingParams {
2529 pub mob_id: String,
2530 #[serde(default)]
2531 pub filter: MobMemberFilterWire,
2532}
2533
2534#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq)]
2537#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
2538pub struct MobListMembersMatchingResult {
2539 #[serde(default)]
2540 pub members: Vec<Value>,
2541}
2542
2543#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq, Hash)]
2552#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
2553#[serde(rename_all = "snake_case")]
2554pub enum WireControlScope {
2555 List,
2556 ReadHistory,
2557 SubscribeEvents,
2558 SendCommand,
2559 Cancel,
2560 Retire,
2561 WireTopology,
2562 Live,
2563 AdminHost,
2564 AdminGrants,
2565}
2566
2567#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
2571#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
2572#[serde(rename_all = "snake_case")]
2573pub enum WireReachability {
2574 Reachable,
2575 Stale,
2576 Unreachable,
2577 Unknown,
2578}
2579
2580#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, Hash)]
2582#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
2583#[serde(transparent)]
2584pub struct WireHostRef(pub String);
2585
2586#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
2588#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
2589#[serde(deny_unknown_fields)]
2590pub struct WireMemberLifecycleCapabilities {
2591 pub transcript_edits: bool,
2592 pub revisions: bool,
2593 pub resume_after_restart: bool,
2594}
2595
2596#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
2598#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
2599#[serde(rename_all = "snake_case")]
2600pub enum WireHostBindPhase {
2601 Requested,
2602 Bound,
2603}
2604
2605#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
2614#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
2615#[serde(deny_unknown_fields)]
2616pub struct WireHostCapabilityFlags {
2617 pub protocol_min: u64,
2618 pub protocol_max: u64,
2619 pub engine_version: String,
2620 pub durable_sessions: bool,
2621 pub autonomous_members: bool,
2622 pub hard_cancel_member: bool,
2623 #[serde(default)]
2624 pub tracked_input_cancel: bool,
2625 pub memory_store: bool,
2626 pub mcp: bool,
2627 #[serde(default, skip_serializing_if = "std::collections::BTreeSet::is_empty")]
2628 pub resolvable_providers: std::collections::BTreeSet<String>,
2629 pub approval_forwarding: bool,
2630 #[serde(default, skip_serializing_if = "Option::is_none")]
2631 pub live_endpoint: Option<String>,
2632}
2633
2634#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
2646#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
2647pub struct MobHostStatus {
2648 pub host_id: WireHostRef,
2649 #[serde(default, skip_serializing_if = "Option::is_none")]
2650 pub endpoint: Option<String>,
2651 pub bind_phase: WireHostBindPhase,
2652 #[serde(default, skip_serializing_if = "Option::is_none")]
2653 pub authority_epoch: Option<u64>,
2654 #[serde(default, skip_serializing_if = "Option::is_none")]
2655 pub capabilities: Option<WireHostCapabilityFlags>,
2656 #[serde(default, skip_serializing_if = "Option::is_none")]
2657 pub control_reachability: Option<WireReachability>,
2658 #[serde(default, skip_serializing_if = "Option::is_none")]
2660 pub last_seen_ms: Option<u64>,
2661 #[serde(default, skip_serializing_if = "Option::is_none")]
2662 pub freshness_reason: Option<String>,
2663 pub materialized_member_count: u64,
2664}
2665
2666#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
2668#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
2669pub struct MobHostsResult {
2670 pub hosts: Vec<MobHostStatus>,
2671}
2672
2673#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
2675#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
2676pub struct WireRouteInstallObligation {
2677 pub edge_a: String,
2678 pub edge_b: String,
2679 pub host: WireHostRef,
2680}
2681
2682#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
2684#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
2685pub struct MobRouteInstallsResult {
2686 pub outstanding: Vec<WireRouteInstallObligation>,
2687 pub complete: bool,
2688}
2689
2690#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
2694#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
2695#[serde(rename_all = "snake_case")]
2696pub enum WireProjectionProvenance {
2697 HostClaimed,
2698 ControllingHostVerified,
2699}
2700
2701#[derive(Debug, Clone, Serialize, Deserialize)]
2710#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
2711#[serde(transparent)]
2712pub struct WireHistoryRow(pub super::session::WireSessionMessage);
2713
2714impl PartialEq for WireHistoryRow {
2715 fn eq(&self, other: &Self) -> bool {
2716 match (
2717 serde_json::to_value(&self.0),
2718 serde_json::to_value(&other.0),
2719 ) {
2720 (Ok(a), Ok(b)) => a == b,
2721 _ => false,
2725 }
2726 }
2727}
2728
2729impl Eq for WireHistoryRow {}
2730
2731#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
2735#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
2736#[serde(deny_unknown_fields)]
2737pub struct WireMemberHistoryPageBody {
2738 pub from_index: u64,
2739 pub messages: Vec<WireHistoryRow>,
2740 pub message_count: u64,
2743 #[serde(default, skip_serializing_if = "Option::is_none")]
2744 pub next_index: Option<u64>,
2745 pub complete: bool,
2746}
2747
2748impl WireMemberHistoryPageBody {
2749 pub fn try_from_history_page(
2754 page: &meerkat_core::service::SessionHistoryPage,
2755 ) -> Result<Self, super::error::WireConversionError> {
2756 let invalid =
2757 |reason: String| super::error::WireConversionError::MemberHistoryPage { debug: reason };
2758 let message_count = u64::try_from(page.message_count).map_err(|_| {
2759 invalid(format!(
2760 "message_count {} exceeds the u64 wire domain",
2761 page.message_count
2762 ))
2763 })?;
2764 let from_index = u64::try_from(page.offset).map_err(|_| {
2765 invalid(format!(
2766 "offset {} exceeds the u64 wire domain",
2767 page.offset
2768 ))
2769 })?;
2770 let served = u64::try_from(page.messages.len()).map_err(|_| {
2771 invalid(format!(
2772 "served row count {} exceeds the u64 wire domain",
2773 page.messages.len()
2774 ))
2775 })?;
2776 let next_index = if page.has_more {
2777 if served == 0 {
2778 return Err(invalid(format!(
2779 "page at offset {from_index} claims more rows but serves none"
2780 )));
2781 }
2782 Some(from_index.checked_add(served).ok_or_else(|| {
2783 invalid(format!(
2784 "offset {from_index} plus served row count {served} exhausts the u64 cursor domain"
2785 ))
2786 })?)
2787 } else {
2788 None
2789 };
2790 Ok(Self {
2791 from_index,
2792 messages: page
2793 .messages
2794 .iter()
2795 .map(|message| {
2796 WireHistoryRow(super::session::WireSessionMessage::from(message.clone()))
2797 })
2798 .collect(),
2799 message_count,
2800 next_index,
2801 complete: !page.has_more,
2802 })
2803 }
2804}
2805
2806#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
2808#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
2809#[serde(deny_unknown_fields)]
2810pub struct MobMemberHistoryParams {
2811 pub mob_id: String,
2812 pub agent_identity: String,
2813 #[serde(default, skip_serializing_if = "Option::is_none")]
2814 pub from_index: Option<u64>,
2815 #[serde(default, skip_serializing_if = "Option::is_none")]
2816 pub limit: Option<u32>,
2817}
2818
2819#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
2823#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
2824pub struct MobMemberHistoryResult {
2825 pub page: WireMemberHistoryPageBody,
2826 pub generation: u64,
2827 #[serde(default, skip_serializing_if = "Option::is_none")]
2828 pub placement: Option<WireHostRef>,
2829 pub provenance: WireProjectionProvenance,
2830}
2831
2832#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
2834#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
2835#[serde(deny_unknown_fields)]
2836pub struct MobBindHostParams {
2837 pub mob_id: String,
2838 pub descriptor: super::supervisor_bridge::WireHostBindingDescriptor,
2839}
2840
2841#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
2843#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
2844pub struct MobBindHostResult {
2845 pub host_id: WireHostRef,
2846 pub capabilities: WireHostCapabilityFlags,
2847 pub authority_epoch: u64,
2848}
2849
2850#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
2852#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
2853#[serde(deny_unknown_fields)]
2854pub struct MobRevokeHostParams {
2855 pub mob_id: String,
2856 pub host_id: WireHostRef,
2857}
2858
2859#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
2861#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
2862pub struct MobRevokeHostResult {
2863 pub host_id: WireHostRef,
2864 pub released_members: Vec<String>,
2867}
2868
2869#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
2871#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
2872pub struct WireGrantRecord {
2873 pub principal: String,
2874 pub scopes: Vec<WireControlScope>,
2875 #[serde(default, skip_serializing_if = "Option::is_none")]
2876 pub expires_at_ms: Option<u64>,
2877}
2878
2879#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
2881#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
2882#[serde(deny_unknown_fields)]
2883pub struct MobGrantScopesParams {
2884 pub mob_id: String,
2885 pub principal: String,
2886 pub scopes: Vec<WireControlScope>,
2887 #[serde(default, skip_serializing_if = "Option::is_none")]
2888 pub expires_at_ms: Option<u64>,
2889}
2890
2891#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
2893#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
2894pub struct MobGrantScopesResult {
2895 pub record: WireGrantRecord,
2896}
2897
2898#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
2901#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
2902#[serde(deny_unknown_fields)]
2903pub struct MobRevokeScopesParams {
2904 pub mob_id: String,
2905 pub principal: String,
2906 #[serde(default, skip_serializing_if = "Option::is_none")]
2907 pub scopes: Option<Vec<WireControlScope>>,
2908}
2909
2910#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
2912#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
2913pub struct MobRevokeScopesResult {
2914 pub removed: bool,
2915}
2916
2917#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
2919#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
2920pub struct MobGrantsResult {
2921 pub grants: Vec<WireGrantRecord>,
2922}
2923
2924#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
2931#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
2932#[serde(deny_unknown_fields)]
2933pub struct WireScopeDeniedDetail {
2934 pub required: WireControlScope,
2935 pub presented: Vec<WireControlScope>,
2936}
2937
2938#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
2941#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
2942#[serde(deny_unknown_fields)]
2943pub struct MobMemberLiveOpenParams {
2944 pub mob_id: String,
2945 pub agent_identity: String,
2946 #[serde(default, skip_serializing_if = "Option::is_none")]
2947 pub turning_mode: Option<super::realtime::RealtimeTurningMode>,
2948 #[serde(default, skip_serializing_if = "Option::is_none")]
2949 pub transport: Option<super::live::LiveOpenTransport>,
2950}
2951
2952#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
2958#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
2959#[serde(deny_unknown_fields)]
2960pub struct MobMemberLiveChannelParams {
2961 pub mob_id: String,
2962 pub agent_identity: String,
2963 pub channel_id: String,
2964}
2965
2966#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
2973#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
2974#[serde(deny_unknown_fields)]
2975pub struct MobMemberLiveStatusParams {
2976 pub mob_id: String,
2977 pub agent_identity: String,
2978 #[serde(default, skip_serializing_if = "Option::is_none")]
2979 pub channel_id: Option<String>,
2980}
2981
2982#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
2986#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
2987#[serde(deny_unknown_fields)]
2988pub struct MobHardCancelParams {
2989 pub mob_id: String,
2990 pub agent_identity: String,
2991 pub reason: String,
2992}
2993
2994#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
2998#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
2999pub struct MobHardCancelResult {
3000 pub cancelled: bool,
3001}
3002
3003#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
3005#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
3006#[serde(deny_unknown_fields)]
3007pub struct MobMemberLiveControlParams {
3008 pub mob_id: String,
3009 pub agent_identity: String,
3010 pub channel_id: String,
3011 pub verb: super::supervisor_bridge::BridgeLiveControlVerb,
3012}
3013
3014#[cfg(test)]
3015#[allow(clippy::expect_used, clippy::panic)]
3016mod tests {
3017 use super::*;
3018
3019 #[test]
3020 fn mob_helper_params_carry_structural_auth_binding() {
3021 let parsed: MobSpawnHelperParams = serde_json::from_value(serde_json::json!({
3022 "mob_id": "mob-1",
3023 "prompt": "help",
3024 "agent_identity": "helper",
3025 "auth_binding": {
3026 "realm": "dev",
3027 "binding": "default_anthropic",
3028 "profile": "console"
3029 }
3030 }))
3031 .expect("spawn helper params parse");
3032 let auth_binding = parsed.auth_binding.expect("auth_binding should parse");
3033 assert_eq!(auth_binding.realm.as_str(), "dev");
3034 assert_eq!(auth_binding.binding.as_str(), "default_anthropic");
3035 assert_eq!(
3036 auth_binding
3037 .profile
3038 .as_ref()
3039 .map(|profile| profile.as_str()),
3040 Some("console")
3041 );
3042
3043 let parsed: MobForkHelperParams = serde_json::from_value(serde_json::json!({
3044 "mob_id": "mob-1",
3045 "source_member_id": "source",
3046 "prompt": "help",
3047 "agent_identity": "helper",
3048 "auth_binding": {
3049 "realm": "dev",
3050 "binding": "default_anthropic"
3051 }
3052 }))
3053 .expect("fork helper params parse");
3054 let auth_binding = parsed.auth_binding.expect("auth_binding should parse");
3055 assert_eq!(auth_binding.realm.as_str(), "dev");
3056 assert_eq!(auth_binding.binding.as_str(), "default_anthropic");
3057 assert!(auth_binding.profile.is_none());
3058 }
3059
3060 #[test]
3061 fn wire_mob_profile_parses_provider_fields_fail_closed() {
3062 let legacy: WireMobProfile =
3064 serde_json::from_str(r#"{"model":"claude-opus-4-8"}"#).expect("legacy profile parses");
3065 assert_eq!(legacy.provider, None);
3066 assert!(legacy.resume_overrides.is_empty());
3067
3068 let full: WireMobProfile = serde_json::from_str(
3070 r#"{
3071 "model": "claude-internal-preview",
3072 "provider": "anthropic",
3073 "image_generation_provider": "gemini",
3074 "auto_compact_threshold": 60000,
3075 "resume_overrides": ["model", "provider"]
3076 }"#,
3077 )
3078 .expect("typed profile parses");
3079 assert_eq!(full.provider, Some(meerkat_core::Provider::Anthropic));
3080 assert_eq!(
3081 full.image_generation_provider,
3082 Some(meerkat_core::Provider::Gemini)
3083 );
3084 assert_eq!(
3085 full.resume_overrides,
3086 vec![
3087 WireMobResumeOverrideField::Model,
3088 WireMobResumeOverrideField::Provider
3089 ]
3090 );
3091
3092 assert!(
3094 serde_json::from_str::<WireMobProfile>(r#"{"model":"m","provider":"not-a-provider"}"#)
3095 .is_err(),
3096 "unknown provider names must fail closed at the wire boundary"
3097 );
3098 assert!(
3099 serde_json::from_str::<WireMobProfile>(r#"{"model":"m","auto_compact_threshold":0}"#)
3100 .is_err(),
3101 "zero auto_compact_threshold must fail closed at the wire boundary"
3102 );
3103 assert!(
3104 serde_json::from_str::<WireMobProfile>(
3105 r#"{"model":"m","resume_overrides":["everything"]}"#
3106 )
3107 .is_err(),
3108 "resume_overrides vocabulary is closed"
3109 );
3110 }
3111
3112 #[test]
3113 fn mob_definition_input_parses_custom_models() {
3114 let input: MobDefinitionInput = serde_json::from_str(
3115 r#"{
3116 "id": "m",
3117 "profiles": {"worker": {"model": "claude-internal-preview"}},
3118 "models": {
3119 "claude-internal-preview": {
3120 "provider": "anthropic",
3121 "context_window": 500000,
3122 "vision": true
3123 }
3124 },
3125 "image_generation_provider": "openai"
3126 }"#,
3127 )
3128 .expect("definition with custom models parses");
3129 let model = input
3130 .models
3131 .get("claude-internal-preview")
3132 .expect("custom model present");
3133 assert_eq!(model.provider, meerkat_core::Provider::Anthropic);
3134 assert_eq!(model.context_window, Some(500_000));
3135 assert_eq!(model.vision, Some(true));
3136 assert_eq!(
3137 input.image_generation_provider,
3138 Some(meerkat_core::Provider::OpenAI)
3139 );
3140 }
3141
3142 #[test]
3143 fn wire_member_ref_round_trips_through_encode_decode() {
3144 let token = WireMemberRef::encode("mob-42", "worker-1");
3145 let (mob_id, agent_identity) = token.decode().expect("decode round-trips");
3146 assert_eq!(mob_id, "mob-42");
3147 assert_eq!(agent_identity, "worker-1");
3148 }
3149
3150 #[test]
3151 fn wire_member_ref_rejects_malformed_token() {
3152 let err = WireMemberRef::from_token("not-a-token-payload")
3153 .decode()
3154 .expect_err("malformed tokens must fail to decode");
3155 assert!(matches!(err, WireMemberRefError::Malformed));
3156 }
3157
3158 #[test]
3159 fn mob_spawn_many_spec_placement_is_optional_and_round_trips() {
3160 let placed: MobSpawnSpecParams = serde_json::from_value(serde_json::json!({
3161 "profile": "worker",
3162 "agent_identity": "w1",
3163 "placement": "host-b-peer"
3164 }))
3165 .expect("placed spawn-many spec parses");
3166 assert_eq!(
3167 placed.placement.as_ref().map(|host| host.0.as_str()),
3168 Some("host-b-peer")
3169 );
3170 assert_eq!(
3171 serde_json::to_value(&placed).expect("placed spawn-many spec serializes")["placement"],
3172 "host-b-peer"
3173 );
3174
3175 let local: MobSpawnSpecParams = serde_json::from_value(serde_json::json!({
3176 "profile": "worker",
3177 "agent_identity": "w2"
3178 }))
3179 .expect("local spawn-many spec parses");
3180 assert!(local.placement.is_none());
3181 assert!(
3182 serde_json::to_value(&local)
3183 .expect("local spawn-many spec serializes")
3184 .get("placement")
3185 .is_none(),
3186 "absent placement must remain omitted for source and wire compatibility"
3187 );
3188 }
3189
3190 #[test]
3191 fn mob_member_spec_placement_is_optional_and_round_trips() {
3192 let placed: MobMemberSpecWire = serde_json::from_value(serde_json::json!({
3193 "profile": "worker",
3194 "agent_identity": "w1",
3195 "placement": "host-b-peer"
3196 }))
3197 .expect("placed declarative member spec parses");
3198 assert_eq!(
3199 placed.placement.as_ref().map(|host| host.0.as_str()),
3200 Some("host-b-peer")
3201 );
3202
3203 let local: MobMemberSpecWire = serde_json::from_value(serde_json::json!({
3204 "profile": "worker",
3205 "agent_identity": "w2"
3206 }))
3207 .expect("local declarative member spec parses");
3208 assert!(local.placement.is_none());
3209 }
3210
3211 #[test]
3212 fn mob_member_spec_exposes_shared_surface_metadata() {
3213 let spec = MobMemberSpecWire {
3214 profile: "worker".into(),
3215 agent_identity: "w1".into(),
3216 initial_message: None,
3217 runtime_mode: None,
3218 backend: None,
3219 placement: None,
3220 binding: None,
3221 context: Some(serde_json::json!({"client_ref": "member-card"})),
3222 labels: Some(BTreeMap::from([("client.member_id".into(), "w1".into())])),
3223 additional_instructions: None,
3224 auto_wire_parent: None,
3225 };
3226
3227 let metadata = spec.surface_metadata();
3228 assert_eq!(
3229 metadata.labels.get("client.member_id").map(String::as_str),
3230 Some("w1")
3231 );
3232 assert_eq!(
3233 metadata.app_context,
3234 Some(serde_json::json!({"client_ref": "member-card"}))
3235 );
3236 }
3237
3238 #[test]
3239 fn mob_member_spec_surface_metadata_rejects_reserved_keys() {
3240 let spec = MobMemberSpecWire {
3241 profile: "worker".into(),
3242 agent_identity: "w1".into(),
3243 initial_message: None,
3244 runtime_mode: None,
3245 backend: None,
3246 placement: None,
3247 binding: None,
3248 context: None,
3249 labels: Some(BTreeMap::from([("mob_id".into(), "spoof".into())])),
3250 additional_instructions: None,
3251 auto_wire_parent: None,
3252 };
3253
3254 assert!(spec.validate_public_surface_metadata().is_err());
3255 }
3256
3257 #[test]
3258 fn mob_reconcile_failure_stage_is_typed_wire_enum() {
3259 let failure = MobReconcileFailureWire {
3260 agent_identity: "worker-1".into(),
3261 stage: WireMobReconcileStage::Spawn,
3262 error: WireMobError {
3263 code: MobSpawnManyFailureCause::ProfileNotFound,
3264 message: "spawn failed".into(),
3265 },
3266 };
3267
3268 let json = serde_json::to_value(&failure).expect("serialize failure");
3269 assert_eq!(json["stage"], "spawn");
3270 assert_eq!(json["error"]["code"], "profile_not_found");
3271 assert_eq!(json["error"]["message"], "spawn failed");
3272
3273 let round_trip: MobReconcileFailureWire =
3274 serde_json::from_value(json).expect("deserialize failure");
3275 assert_eq!(round_trip.stage, WireMobReconcileStage::Spawn);
3276 assert_eq!(
3277 round_trip.error.code,
3278 MobSpawnManyFailureCause::ProfileNotFound
3279 );
3280
3281 let err = serde_json::from_value::<MobReconcileFailureWire>(serde_json::json!({
3282 "agent_identity": "worker-1",
3283 "stage": "restart",
3284 "error": { "code": "profile_not_found", "message": "bad stage" }
3285 }))
3286 .expect_err("unknown reconcile stage must be rejected");
3287 assert!(err.to_string().contains("unknown variant"));
3288 }
3289
3290 #[test]
3291 fn mob_lifecycle_params_reject_unknown_action_string() {
3292 let err = serde_json::from_value::<MobLifecycleParams>(serde_json::json!({
3293 "mob_id": "mob-1",
3294 "action": "explode"
3295 }))
3296 .expect_err("unknown lifecycle actions must fail at the typed wire boundary");
3297
3298 assert!(
3299 err.to_string().contains("unknown variant"),
3300 "unexpected error: {err}"
3301 );
3302 }
3303
3304 #[test]
3305 fn mob_lifecycle_result_round_trips_typed_action() {
3306 let result = MobLifecycleResult {
3307 mob_id: "mob-1".into(),
3308 action: WireMobLifecycleAction::Complete,
3309 ok: true,
3310 destroy_report: None,
3311 };
3312
3313 let json = serde_json::to_value(&result).expect("serialize lifecycle result");
3314 assert_eq!(json["action"], "complete");
3315
3316 let round_trip: MobLifecycleResult =
3317 serde_json::from_value(json).expect("deserialize lifecycle result");
3318 assert_eq!(round_trip.action, WireMobLifecycleAction::Complete);
3319 }
3320
3321 #[test]
3322 fn mob_wire_members_batch_contract_is_local_edge_native() {
3323 let params: MobWireMembersBatchParams = serde_json::from_value(serde_json::json!({
3324 "mob_id": "mob-1",
3325 "edges": [
3326 { "a": "lead", "b": "worker-b" },
3327 { "a": "worker-a", "b": "lead" }
3328 ]
3329 }))
3330 .expect("batch wire params deserialize");
3331
3332 assert_eq!(params.mob_id, "mob-1");
3333 assert_eq!(params.edges.len(), 2);
3334 assert_eq!(params.edges[0].a, "lead");
3335 assert_eq!(params.edges[0].b, "worker-b");
3336
3337 let result = MobWireMembersBatchResult {
3338 requested: 2,
3339 wired: vec![MobWireMembersBatchEdge {
3340 a: "lead".into(),
3341 b: "worker-a".into(),
3342 }],
3343 already_wired: vec![MobWireMembersBatchEdge {
3344 a: "lead".into(),
3345 b: "worker-b".into(),
3346 }],
3347 };
3348 let json = serde_json::to_value(&result).expect("serialize batch wire result");
3349 assert_eq!(json["requested"], 2);
3350 assert_eq!(json["wired"][0]["a"], "lead");
3351 assert_eq!(json["already_wired"][0]["b"], "worker-b");
3352
3353 let err = serde_json::from_value::<MobWireMembersBatchParams>(serde_json::json!({
3354 "mob_id": "mob-1",
3355 "edges": [{ "member": "lead", "peer": "worker-a" }]
3356 }))
3357 .expect_err("mixed local/external mob/wire shape must not deserialize");
3358 let message = err.to_string();
3359 assert!(
3360 message.contains("unknown field `member`") || message.contains("missing field `a`"),
3361 "unexpected error: {message}"
3362 );
3363 }
3364
3365 #[test]
3366 fn mob_spawn_many_result_entry_uses_typed_status_result_envelope() {
3367 let member_ref = WireMemberRef::encode("mob-1", "worker-1");
3368 let entry = MobSpawnManyResultEntry::spawned("worker-1", member_ref.clone());
3369
3370 let json = serde_json::to_value(&entry).expect("serialize typed spawn_many row");
3371 assert_eq!(json["status"], "spawned");
3372 assert_eq!(json["result"]["agent_identity"], "worker-1");
3373 assert_eq!(json["result"]["member_ref"], member_ref.as_str());
3374 assert!(json.get("ok").is_none());
3375 assert!(json.get("error").is_none());
3376
3377 let round_trip: MobSpawnManyResultEntry =
3378 serde_json::from_value(json).expect("deserialize typed spawn_many row");
3379 assert_eq!(round_trip, entry);
3380
3381 let failed = MobSpawnManyResultEntry::failed(
3382 MobSpawnManyFailureCause::ProfileNotFound,
3383 "profile missing",
3384 );
3385 let json = serde_json::to_value(&failed).expect("serialize typed failed spawn_many row");
3386 assert_eq!(json["status"], "failed");
3387 assert_eq!(json["result"]["cause"], "profile_not_found");
3388 assert_eq!(json["result"]["message"], "profile missing");
3389 assert!(json.get("ok").is_none());
3390 assert!(json.get("error").is_none());
3391
3392 let round_trip: MobSpawnManyResultEntry =
3393 serde_json::from_value(json).expect("deserialize typed failed spawn_many row");
3394 assert_eq!(round_trip, failed);
3395 }
3396
3397 #[test]
3398 fn mob_spawn_many_result_entry_rejects_legacy_or_malformed_envelopes() {
3399 let legacy = serde_json::json!({
3400 "ok": true,
3401 "agent_identity": "worker-1",
3402 "member_ref": WireMemberRef::encode("mob-1", "worker-1"),
3403 });
3404 let err = serde_json::from_value::<MobSpawnManyResultEntry>(legacy)
3405 .expect_err("legacy ok carrier must not deserialize");
3406 assert!(
3407 err.to_string().contains("missing field `status`")
3408 || err.to_string().contains("unknown field"),
3409 "unexpected error: {err}"
3410 );
3411
3412 let missing_result = serde_json::json!({
3413 "status": "spawned"
3414 });
3415 let err = serde_json::from_value::<MobSpawnManyResultEntry>(missing_result)
3416 .expect_err("missing typed result must fail closed");
3417 assert!(
3418 err.to_string().contains("missing field `result`"),
3419 "unexpected error: {err}"
3420 );
3421
3422 let unknown_status = serde_json::json!({
3423 "status": "ok",
3424 "result": {
3425 "agent_identity": "worker-1",
3426 "member_ref": WireMemberRef::encode("mob-1", "worker-1"),
3427 }
3428 });
3429 let err = serde_json::from_value::<MobSpawnManyResultEntry>(unknown_status)
3430 .expect_err("unknown typed status must fail closed");
3431 assert!(
3432 err.to_string().contains("unknown variant"),
3433 "unexpected error: {err}"
3434 );
3435
3436 let mismatched = serde_json::json!({
3437 "status": "spawned",
3438 "result": {
3439 "cause": "profile_not_found",
3440 "message": "profile missing"
3441 }
3442 });
3443 let err = serde_json::from_value::<MobSpawnManyResultEntry>(mismatched)
3444 .expect_err("status/result mismatch must fail closed");
3445 assert!(
3446 err.to_string()
3447 .contains("status spawned requires spawned result"),
3448 "unexpected error: {err}"
3449 );
3450
3451 let message_only_failure = serde_json::json!({
3452 "status": "failed",
3453 "result": {
3454 "message": "profile missing"
3455 }
3456 });
3457 let err = serde_json::from_value::<MobSpawnManyResultEntry>(message_only_failure)
3458 .expect_err("string-only failure result must fail closed");
3459 assert!(
3460 err.to_string().contains("data did not match any variant")
3461 || err.to_string().contains("missing field `cause`"),
3462 "unexpected error: {err}"
3463 );
3464
3465 let unknown_failure_cause = serde_json::json!({
3466 "status": "failed",
3467 "result": {
3468 "cause": "future_failure",
3469 "message": "future failure"
3470 }
3471 });
3472 let err = serde_json::from_value::<MobSpawnManyResultEntry>(unknown_failure_cause)
3473 .expect_err("unknown failure cause must fail closed");
3474 assert!(
3475 err.to_string().contains("data did not match any variant")
3476 || err.to_string().contains("unknown variant"),
3477 "unexpected error: {err}"
3478 );
3479 }
3480
3481 #[test]
3482 fn mob_wire_params_reject_legacy_local_target_shape() {
3483 let err = serde_json::from_value::<MobWireParams>(serde_json::json!({
3484 "mob_id": "mob-1",
3485 "local": "member-a",
3486 "target": { "local": "member-b" }
3487 }))
3488 .expect_err("legacy local/target shape must be rejected");
3489
3490 let msg = err.to_string();
3491 assert!(
3492 msg.contains("unknown field `local`") || msg.contains("missing field `member`"),
3493 "unexpected error: {msg}"
3494 );
3495 }
3496
3497 #[test]
3498 fn mob_wire_params_accept_canonical_external_peer_identity() {
3499 let params = serde_json::from_value::<MobWireParams>(serde_json::json!({
3500 "mob_id": "mob-1",
3501 "member": "member-a",
3502 "peer": {
3503 "external": {
3504 "name": "external-worker",
3505 "address": "inproc://external-worker",
3506 "identity": {
3507 "kind": "ed25519_public_key",
3508 "public_key": "ed25519:BwcHBwcHBwcHBwcHBwcHBwcHBwcHBwcHBwcHBwcHBwc="
3509 }
3510 }
3511 }
3512 }))
3513 .expect("canonical external peer identity should deserialize");
3514
3515 let MobPeerTarget::External(spec) = params.peer else {
3516 panic!("expected external peer target");
3517 };
3518 assert_eq!(spec.name, "external-worker");
3519 }
3520
3521 #[test]
3522 fn mob_wire_params_reject_raw_external_peer_id_shape() {
3523 let err = serde_json::from_value::<MobWireParams>(serde_json::json!({
3524 "mob_id": "mob-1",
3525 "member": "member-a",
3526 "peer": {
3527 "external": {
3528 "name": "external-worker",
3529 "peer_id": meerkat_core::comms::PeerId::from_ed25519_pubkey(&[7u8; 32]).to_string(),
3530 "address": "inproc://external-worker",
3531 "pubkey": vec![7u8; 32]
3532 }
3533 }
3534 }))
3535 .expect_err("raw peer_id/pubkey external peer shape must be rejected");
3536
3537 let msg = err.to_string();
3538 assert!(
3539 msg.contains("peer_id") || msg.contains("identity"),
3540 "unexpected error: {msg}"
3541 );
3542 }
3543
3544 #[test]
3545 fn mob_wire_params_reject_missing_external_peer_pubkey_material() {
3546 let err = serde_json::from_value::<MobWireParams>(serde_json::json!({
3547 "mob_id": "mob-1",
3548 "member": "member-a",
3549 "peer": {
3550 "external": {
3551 "name": "external-worker",
3552 "address": "inproc://external-worker",
3553 "identity": {
3554 "kind": "ed25519_public_key"
3555 }
3556 }
3557 }
3558 }))
3559 .expect_err("missing external peer pubkey material must fail closed");
3560
3561 let msg = err.to_string();
3562 assert!(
3563 msg.contains("public_key") || msg.contains("identity"),
3564 "unexpected error: {msg}"
3565 );
3566 }
3567
3568 #[test]
3569 fn runtime_binding_accepts_canonical_external_peer_identity() {
3570 let binding = serde_json::from_value::<WireRuntimeBinding>(serde_json::json!({
3571 "kind": "external",
3572 "address": "inproc://external-worker",
3573 "identity": {
3574 "kind": "ed25519_public_key",
3575 "public_key": "ed25519:BwcHBwcHBwcHBwcHBwcHBwcHBwcHBwcHBwcHBwcHBwc="
3576 }
3577 }))
3578 .expect("canonical external runtime binding identity should deserialize");
3579
3580 let WireRuntimeBinding::External {
3581 identity, address, ..
3582 } = binding
3583 else {
3584 panic!("expected external runtime binding");
3585 };
3586 assert_eq!(address, "inproc://external-worker");
3587 assert_eq!(
3588 identity.resolve().expect("identity resolves").pubkey,
3589 [7u8; 32]
3590 );
3591 }
3592
3593 #[test]
3594 fn runtime_binding_rejects_raw_external_peer_id_shape() {
3595 let err = serde_json::from_value::<WireRuntimeBinding>(serde_json::json!({
3596 "kind": "external",
3597 "peer_id": meerkat_core::comms::PeerId::from_ed25519_pubkey(&[7u8; 32]).to_string(),
3598 "address": "inproc://external-worker",
3599 "pubkey": vec![7u8; 32]
3600 }))
3601 .expect_err("raw peer_id/pubkey external runtime binding shape must be rejected");
3602
3603 let msg = err.to_string();
3604 assert!(
3605 msg.contains("peer_id") || msg.contains("identity"),
3606 "unexpected error: {msg}"
3607 );
3608 }
3609
3610 #[test]
3611 fn runtime_binding_rejects_missing_external_peer_pubkey_material() {
3612 let err = serde_json::from_value::<WireRuntimeBinding>(serde_json::json!({
3613 "kind": "external",
3614 "address": "inproc://external-worker",
3615 "identity": {
3616 "kind": "ed25519_public_key"
3617 }
3618 }))
3619 .expect_err("missing external runtime binding pubkey material must fail closed");
3620
3621 let msg = err.to_string();
3622 assert!(
3623 msg.contains("public_key") || msg.contains("identity"),
3624 "unexpected error: {msg}"
3625 );
3626 }
3627
3628 #[test]
3629 fn mob_turn_start_params_capture_turn_override_fields() {
3630 let params = serde_json::from_value::<MobTurnStartParams>(serde_json::json!({
3631 "mob_id": "mob-1",
3632 "agent_identity": "worker",
3633 "prompt": "continue",
3634 "output_schema": { "type": "object" },
3635 "structured_output_retries": 2
3636 }))
3637 .expect("turn_start should accept explicit turn override fields");
3638
3639 assert_eq!(params.mob_id, "mob-1");
3640 assert_eq!(params.agent_identity, "worker");
3641 assert_eq!(params.prompt, WireContentInput::Text("continue".into()));
3642 assert_eq!(
3643 params.output_schema,
3644 Some(serde_json::json!({ "type": "object" }))
3645 );
3646 assert_eq!(params.structured_output_retries, Some(2));
3647
3648 let err = serde_json::from_value::<MobTurnStartParams>(serde_json::json!({
3649 "mob_id": "mob-1",
3650 "agent_identity": "worker",
3651 "prompt": "continue",
3652 "unknown_override": true
3653 }))
3654 .expect_err("turn_start must reject unknown override fields");
3655 assert!(
3656 err.to_string().contains("unknown field"),
3657 "unexpected error: {err}"
3658 );
3659 }
3660
3661 #[test]
3662 fn mob_create_params_reject_reserved_runtime_lifecycle_fields() {
3663 let err = serde_json::from_value::<MobCreateParams>(serde_json::json!({
3664 "definition": {
3665 "id": "mob-1",
3666 "owner_runtime_binding": "runtime:worker:0",
3667 "profiles": {
3668 "worker": { "model": "claude-sonnet-4-6" }
3669 }
3670 }
3671 }))
3672 .expect_err("reserved runtime lifecycle fields must be rejected");
3673
3674 assert!(
3675 err.to_string()
3676 .contains("unknown field `owner_runtime_binding`"),
3677 "unexpected error: {err}"
3678 );
3679 }
3680
3681 #[test]
3682 fn mob_create_params_reject_reserved_runtime_bridge_owner_field() {
3683 let err = serde_json::from_value::<MobCreateParams>(serde_json::json!({
3684 "definition": {
3685 "id": "mob-1",
3686 "owner_transport_binding": "transport:worker:0",
3687 "profiles": {
3688 "worker": { "model": "claude-sonnet-4-6" }
3689 }
3690 }
3691 }))
3692 .expect_err("reserved runtime bridge owner field must be rejected");
3693
3694 assert!(
3695 err.to_string()
3696 .contains("unknown field `owner_transport_binding`"),
3697 "unexpected error: {err}"
3698 );
3699 }
3700
3701 #[test]
3702 fn mob_create_params_reject_internal_profile_tool_bundles() {
3703 let err = serde_json::from_value::<MobCreateParams>(serde_json::json!({
3704 "definition": {
3705 "id": "mob-1",
3706 "profiles": {
3707 "worker": {
3708 "model": "claude-sonnet-4-6",
3709 "tools": {
3710 "rust_bundles": ["internal-only"]
3711 }
3712 }
3713 }
3714 }
3715 }))
3716 .expect_err("internal rust tool bundles must be rejected");
3717
3718 assert!(
3721 err.to_string().contains("did not match any variant")
3722 || err.to_string().contains("unknown field `rust_bundles`"),
3723 "unexpected error: {err}"
3724 );
3725 }
3726
3727 #[test]
3728 fn mob_create_params_accept_typed_nested_flow_definition() {
3729 let params = serde_json::from_value::<MobCreateParams>(serde_json::json!({
3730 "definition": {
3731 "id": "mob-1",
3732 "profiles": {
3733 "worker": { "model": "claude-sonnet-4-6" }
3734 },
3735 "flows": {
3736 "review": {
3737 "description": "review flow",
3738 "steps": {
3739 "draft": {
3740 "role": "worker",
3741 "message": "draft it"
3742 }
3743 }
3744 }
3745 }
3746 }
3747 }))
3748 .expect("typed nested flow definition should parse");
3749
3750 assert_eq!(
3751 params.definition.flows["review"].steps["draft"].role,
3752 "worker"
3753 );
3754 }
3755
3756 #[test]
3761 fn mob_spawn_params_reject_deleted_budget_split_policy() {
3762 let err = serde_json::from_value::<MobSpawnParams>(serde_json::json!({
3763 "mob_id": "mob-1",
3764 "profile": "worker",
3765 "agent_identity": "worker-1",
3766 "budget_split_policy": { "type": "equal" }
3767 }))
3768 .expect_err("deleted budget_split_policy must fail closed at the wire boundary");
3769 assert!(
3770 err.to_string()
3771 .contains("unknown field `budget_split_policy`"),
3772 "unexpected error: {err}"
3773 );
3774 }
3775
3776 #[test]
3780 fn mob_spawn_params_placement_round_trips_and_defaults_absent() {
3781 let params = serde_json::from_value::<MobSpawnParams>(serde_json::json!({
3782 "mob_id": "mob-1",
3783 "profile": "worker",
3784 "agent_identity": "worker-1"
3785 }))
3786 .expect("placement-less params must decode");
3787 assert_eq!(params.placement, None);
3788 let encoded = serde_json::to_value(¶ms).expect("serialize params");
3789 assert!(
3790 encoded.get("placement").is_none(),
3791 "absent placement must not serialize: {encoded}"
3792 );
3793
3794 let params = serde_json::from_value::<MobSpawnParams>(serde_json::json!({
3795 "mob_id": "mob-1",
3796 "profile": "worker",
3797 "agent_identity": "worker-1",
3798 "placement": "host-peer-b"
3799 }))
3800 .expect("placed params must decode");
3801 assert_eq!(params.placement.as_deref(), Some("host-peer-b"));
3802 let encoded = serde_json::to_value(¶ms).expect("serialize params");
3803 assert_eq!(encoded["placement"], serde_json::json!("host-peer-b"));
3804 }
3805
3806 fn minimal_member_status() -> MobMemberStatusResult {
3807 MobMemberStatusResult {
3808 status: WireMobMemberStatus::Active,
3809 member_ref: WireMemberRef::encode("mob-1", "worker-1"),
3810 output_preview: None,
3811 error: None,
3812 tokens_used: 0,
3813 is_final: false,
3814 current_session_id: None,
3815 peer_connectivity: None,
3816 kickoff: None,
3817 external_member: None,
3818 resolved_capabilities: None,
3819 progress: None,
3820 placement: None,
3821 control_reachability: None,
3822 comms_reachability: None,
3823 last_seen_ms: None,
3824 freshness_reason: None,
3825 lifecycle_capabilities: None,
3826 non_portable_disabled: None,
3827 }
3828 }
3829
3830 #[test]
3833 fn member_status_multi_host_fields_skip_when_absent_and_decode_legacy() {
3834 let value = serde_json::to_value(minimal_member_status()).expect("serialize member status");
3835 for absent in [
3836 "placement",
3837 "control_reachability",
3838 "comms_reachability",
3839 "last_seen_ms",
3840 "freshness_reason",
3841 "lifecycle_capabilities",
3842 "non_portable_disabled",
3843 ] {
3844 assert!(
3845 value.get(absent).is_none(),
3846 "absent {absent} must be omitted from the wire form: {value}"
3847 );
3848 }
3849
3850 let legacy = serde_json::json!({
3852 "status": "active",
3853 "member_ref": WireMemberRef::encode("mob-1", "worker-1"),
3854 "tokens_used": 3,
3855 "is_final": false,
3856 });
3857 let decoded: MobMemberStatusResult =
3858 serde_json::from_value(legacy).expect("legacy member status decodes");
3859 assert!(decoded.placement.is_none());
3860 assert!(decoded.lifecycle_capabilities.is_none());
3861 }
3862
3863 #[test]
3866 fn member_status_carries_placement_only_at_typed_keys() {
3867 let mut status = minimal_member_status();
3868 status.placement = Some(WireHostRef("host-b-peer".to_string()));
3869 status.control_reachability = Some(WireReachability::Stale);
3870 status.comms_reachability = Some(WireReachability::Reachable);
3871 status.last_seen_ms = Some(1_234);
3872 status.freshness_reason = Some("pump idle".to_string());
3873 status.lifecycle_capabilities = Some(WireMemberLifecycleCapabilities {
3874 transcript_edits: false,
3875 revisions: false,
3876 resume_after_restart: true,
3877 });
3878 status.non_portable_disabled = Some(vec![
3879 super::super::portable_spec::WireNonPortableResourceKind::WorkgraphTools,
3880 ]);
3881 status.external_member = Some(serde_json::json!({"endpoint": "tcp://10.0.0.2:7101"}));
3882
3883 let value = serde_json::to_value(&status).expect("serialize member status");
3884 assert_eq!(value["placement"], serde_json::json!("host-b-peer"));
3885 assert_eq!(value["control_reachability"], serde_json::json!("stale"));
3886 assert_eq!(
3887 value["non_portable_disabled"],
3888 serde_json::json!(["workgraph_tools"])
3889 );
3890 assert!(
3891 value["external_member"].get("placement").is_none(),
3892 "placement must not ride the opaque external_member value (SD-5)"
3893 );
3894
3895 let decoded: MobMemberStatusResult =
3896 serde_json::from_value(value).expect("decode member status");
3897 assert_eq!(decoded.placement, status.placement);
3898 assert_eq!(decoded.control_reachability, status.control_reachability);
3899 }
3900
3901 #[test]
3902 fn control_scope_and_reachability_round_trip_snake_case() {
3903 let scopes: &[(WireControlScope, &str)] = &[
3904 (WireControlScope::List, "list"),
3905 (WireControlScope::ReadHistory, "read_history"),
3906 (WireControlScope::SubscribeEvents, "subscribe_events"),
3907 (WireControlScope::SendCommand, "send_command"),
3908 (WireControlScope::Cancel, "cancel"),
3909 (WireControlScope::Retire, "retire"),
3910 (WireControlScope::WireTopology, "wire_topology"),
3911 (WireControlScope::Live, "live"),
3912 (WireControlScope::AdminHost, "admin_host"),
3913 (WireControlScope::AdminGrants, "admin_grants"),
3914 ];
3915 for (scope, expected) in scopes {
3916 let value = serde_json::to_value(scope).expect("serialize scope");
3917 assert_eq!(value, serde_json::json!(expected));
3918 let decoded: WireControlScope = serde_json::from_value(value).expect("decode scope");
3919 assert_eq!(decoded, *scope);
3920 }
3921 assert!(
3922 serde_json::from_value::<WireControlScope>(serde_json::json!("admin")).is_err(),
3923 "unknown scopes must fail decode (closed vocabulary)"
3924 );
3925
3926 let classes: &[(WireReachability, &str)] = &[
3927 (WireReachability::Reachable, "reachable"),
3928 (WireReachability::Stale, "stale"),
3929 (WireReachability::Unreachable, "unreachable"),
3930 (WireReachability::Unknown, "unknown"),
3931 ];
3932 for (class, expected) in classes {
3933 let value = serde_json::to_value(class).expect("serialize reachability");
3934 assert_eq!(value, serde_json::json!(expected));
3935 let decoded: WireReachability =
3936 serde_json::from_value(value).expect("decode reachability");
3937 assert_eq!(decoded, *class);
3938 }
3939 }
3940
3941 #[test]
3942 fn scope_denied_detail_round_trips_snake_case_and_denies_unknown_fields() {
3943 let detail = WireScopeDeniedDetail {
3944 required: WireControlScope::AdminGrants,
3945 presented: vec![WireControlScope::List, WireControlScope::SendCommand],
3946 };
3947 let value = serde_json::to_value(&detail).expect("serialize detail");
3948 assert_eq!(
3949 value,
3950 serde_json::json!({
3951 "required": "admin_grants",
3952 "presented": ["list", "send_command"],
3953 })
3954 );
3955 let decoded: WireScopeDeniedDetail = serde_json::from_value(value).expect("decode detail");
3956 assert_eq!(decoded, detail);
3957
3958 assert!(
3959 serde_json::from_value::<WireScopeDeniedDetail>(serde_json::json!({
3960 "required": "admin_grants",
3961 "presented": [],
3962 "reason": "extra",
3963 }))
3964 .is_err(),
3965 "unknown fields must be rejected (deny_unknown_fields)"
3966 );
3967 }
3968
3969 #[test]
3970 fn host_status_and_grants_round_trip() {
3971 let host = MobHostStatus {
3972 host_id: WireHostRef("host-b-peer".to_string()),
3973 endpoint: Some("tcp://10.0.0.2:7100".to_string()),
3974 bind_phase: WireHostBindPhase::Bound,
3975 authority_epoch: Some(4),
3976 capabilities: Some(WireHostCapabilityFlags {
3977 protocol_min: 2,
3978 protocol_max: 4,
3979 engine_version: "0.7.22".to_string(),
3980 durable_sessions: true,
3981 autonomous_members: true,
3982 hard_cancel_member: false,
3983 tracked_input_cancel: false,
3984 memory_store: false,
3985 mcp: true,
3986 resolvable_providers: std::collections::BTreeSet::from(["anthropic".to_string()]),
3987 approval_forwarding: false,
3988 live_endpoint: None,
3989 }),
3990 control_reachability: Some(WireReachability::Reachable),
3991 last_seen_ms: Some(250),
3992 freshness_reason: None,
3993 materialized_member_count: 2,
3994 };
3995 let result = MobHostsResult { hosts: vec![host] };
3996 let value = serde_json::to_value(&result).expect("serialize hosts");
3997 assert_eq!(value["hosts"][0]["bind_phase"], serde_json::json!("bound"));
3998 let decoded: MobHostsResult = serde_json::from_value(value).expect("decode hosts");
3999 assert_eq!(decoded, result);
4000
4001 let requested = MobHostStatus {
4004 host_id: WireHostRef("host-c-peer".to_string()),
4005 endpoint: None,
4006 bind_phase: WireHostBindPhase::Requested,
4007 authority_epoch: None,
4008 capabilities: None,
4009 control_reachability: None,
4010 last_seen_ms: None,
4011 freshness_reason: None,
4012 materialized_member_count: 0,
4013 };
4014 let value = serde_json::to_value(&requested).expect("serialize requested host");
4015 assert_eq!(value["bind_phase"], serde_json::json!("requested"));
4016 assert!(value.get("endpoint").is_none());
4017 assert!(value.get("authority_epoch").is_none());
4018 assert!(value.get("capabilities").is_none());
4019 let decoded: MobHostStatus = serde_json::from_value(value).expect("decode requested host");
4020 assert_eq!(decoded, requested);
4021
4022 let record = WireGrantRecord {
4023 principal: "console:luka".to_string(),
4024 scopes: vec![WireControlScope::List, WireControlScope::Live],
4025 expires_at_ms: None,
4026 };
4027 let value = serde_json::to_value(&record).expect("serialize grant");
4028 assert!(
4029 value.get("expires_at_ms").is_none(),
4030 "absent expiry must be omitted"
4031 );
4032 let decoded: WireGrantRecord = serde_json::from_value(value).expect("decode grant");
4033 assert_eq!(decoded, record);
4034 }
4035
4036 #[test]
4037 fn member_history_result_round_trips_with_provenance() {
4038 let result = MobMemberHistoryResult {
4039 page: WireMemberHistoryPageBody {
4040 from_index: 5,
4041 messages: Vec::new(),
4042 message_count: 12,
4043 next_index: Some(10),
4044 complete: false,
4045 },
4046 generation: 2,
4047 placement: Some(WireHostRef("host-b-peer".to_string())),
4048 provenance: WireProjectionProvenance::HostClaimed,
4049 };
4050 let value = serde_json::to_value(&result).expect("serialize history result");
4051 assert_eq!(value["provenance"], serde_json::json!("host_claimed"));
4052 assert_eq!(value["page"]["message_count"], serde_json::json!(12));
4053 let decoded: MobMemberHistoryResult =
4054 serde_json::from_value(value.clone()).expect("decode history result");
4055 let reencoded = serde_json::to_value(&decoded).expect("reserialize history result");
4056 assert_eq!(value, reencoded);
4057 }
4058
4059 #[test]
4060 fn member_history_projection_rejects_non_advancing_page() {
4061 let page = meerkat_core::service::SessionHistoryPage {
4062 session_id: meerkat_core::SessionId::new(),
4063 message_count: 1,
4064 offset: 0,
4065 limit: Some(1),
4066 has_more: true,
4067 messages: Vec::new(),
4068 };
4069 let error = WireMemberHistoryPageBody::try_from_history_page(&page)
4070 .expect_err("a page that claims more rows must advance its cursor");
4071 assert!(matches!(
4072 error,
4073 crate::wire::error::WireConversionError::MemberHistoryPage { debug }
4074 if debug.contains("serves none")
4075 ));
4076 }
4077
4078 #[cfg(target_pointer_width = "64")]
4079 #[test]
4080 fn member_history_projection_rejects_exhausted_cursor() {
4081 let page = meerkat_core::service::SessionHistoryPage {
4082 session_id: meerkat_core::SessionId::new(),
4083 message_count: usize::MAX,
4084 offset: usize::MAX,
4085 limit: Some(2),
4086 has_more: true,
4087 messages: vec![
4088 meerkat_core::types::Message::User(meerkat_core::types::UserMessage::text("first")),
4089 meerkat_core::types::Message::User(meerkat_core::types::UserMessage::text(
4090 "second",
4091 )),
4092 ],
4093 };
4094 let error = WireMemberHistoryPageBody::try_from_history_page(&page)
4095 .expect_err("MAX has no representable member-history successor");
4096 assert!(matches!(
4097 error,
4098 crate::wire::error::WireConversionError::MemberHistoryPage { debug }
4099 if debug.contains("exhausts the u64 cursor domain")
4100 ));
4101 }
4102
4103 #[test]
4108 fn hard_cancel_params_round_trip_and_reject_unknown_fields() {
4109 let params = MobHardCancelParams {
4110 mob_id: "mob-1".to_string(),
4111 agent_identity: "worker".to_string(),
4112 reason: "operator interrupt".to_string(),
4113 };
4114 let value = serde_json::to_value(¶ms).expect("serialize hard-cancel params");
4115 let decoded: MobHardCancelParams =
4116 serde_json::from_value(value).expect("decode hard-cancel params");
4117 assert_eq!(decoded, params);
4118
4119 serde_json::from_value::<MobHardCancelParams>(serde_json::json!({
4120 "mob_id": "mob-1",
4121 "agent_identity": "worker",
4122 "reason": "x",
4123 "force": true,
4124 }))
4125 .expect_err("unknown field must be rejected");
4126
4127 serde_json::from_value::<MobHardCancelParams>(serde_json::json!({
4130 "mob_id": "mob-1",
4131 "agent_identity": "worker",
4132 }))
4133 .expect_err("missing reason must be rejected");
4134
4135 let result = MobHardCancelResult { cancelled: true };
4136 let value = serde_json::to_value(&result).expect("serialize hard-cancel result");
4137 assert_eq!(value, serde_json::json!({ "cancelled": true }));
4138 }
4139
4140 #[test]
4141 fn member_live_status_params_keep_the_discovery_read() {
4142 let discovery: MobMemberLiveStatusParams = serde_json::from_value(serde_json::json!({
4145 "mob_id": "mob-1",
4146 "agent_identity": "worker",
4147 }))
4148 .expect("discovery status params parse");
4149 assert_eq!(discovery.channel_id, None);
4150 let value = serde_json::to_value(&discovery).expect("serialize discovery params");
4151 assert!(
4152 value.get("channel_id").is_none(),
4153 "absent channel_id must be omitted"
4154 );
4155
4156 let named: MobMemberLiveStatusParams = serde_json::from_value(serde_json::json!({
4157 "mob_id": "mob-1",
4158 "agent_identity": "worker",
4159 "channel_id": "chan-7",
4160 }))
4161 .expect("named status params parse");
4162 assert_eq!(named.channel_id.as_deref(), Some("chan-7"));
4163
4164 serde_json::from_value::<MobMemberLiveStatusParams>(serde_json::json!({
4165 "mob_id": "mob-1",
4166 "agent_identity": "worker",
4167 "chan": "chan-7",
4168 }))
4169 .expect_err("unknown field must be rejected");
4170
4171 serde_json::from_value::<MobMemberLiveChannelParams>(serde_json::json!({
4173 "mob_id": "mob-1",
4174 "agent_identity": "worker",
4175 }))
4176 .expect_err("close without channel_id must be rejected");
4177 }
4178}