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")]
2078 pub activity: Option<crate::wire::JobExecutionActivity>,
2079 #[serde(default, skip_serializing_if = "Option::is_none")]
2081 pub detached_jobs: Option<crate::wire::DetachedJobsActivitySummary>,
2082 #[serde(default, skip_serializing_if = "Option::is_none")]
2088 pub placement: Option<WireHostRef>,
2089 #[serde(default, skip_serializing_if = "Option::is_none")]
2091 pub control_reachability: Option<WireReachability>,
2092 #[serde(default, skip_serializing_if = "Option::is_none")]
2094 pub comms_reachability: Option<WireReachability>,
2095 #[serde(default, skip_serializing_if = "Option::is_none")]
2098 pub last_seen_ms: Option<u64>,
2099 #[serde(default, skip_serializing_if = "Option::is_none")]
2100 pub freshness_reason: Option<String>,
2101 #[serde(default, skip_serializing_if = "Option::is_none")]
2103 pub lifecycle_capabilities: Option<WireMemberLifecycleCapabilities>,
2104 #[serde(default, skip_serializing_if = "Option::is_none")]
2107 pub non_portable_disabled: Option<Vec<super::portable_spec::WireNonPortableResourceKind>>,
2108}
2109
2110#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
2111#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
2112#[serde(rename_all = "snake_case")]
2113pub enum WireMemberRunState {
2114 Idle,
2115 RunOpen,
2116 Unknown,
2117}
2118
2119#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
2120#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
2121#[serde(rename_all = "snake_case")]
2122pub enum WireMemberHealthClass {
2123 Healthy,
2124 Degraded,
2125 Wedged,
2126 Unknown,
2127}
2128
2129#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
2130#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
2131#[serde(rename_all = "snake_case")]
2132pub enum WireMemberProgressEvent {
2133 ExecutionAdvanced,
2134 BecameIdle,
2135 Unchanged,
2136}
2137
2138#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
2139#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
2140pub struct WireMemberProgressSnapshot {
2141 pub run_state: WireMemberRunState,
2142 pub in_flight_work: u64,
2143 pub last_progress_at_ms: u64,
2144 pub last_progress_event: WireMemberProgressEvent,
2145 pub health: WireMemberHealthClass,
2146}
2147
2148#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
2150#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
2151pub struct MobSnapshotResult {
2152 pub mob_id: String,
2153 pub status: WireMobLifecycleStatus,
2154 pub members: Vec<MobMemberListEntryWire>,
2155}
2156
2157#[cfg(test)]
2158mod member_status_capability_tests {
2159 use super::*;
2160
2161 #[test]
2162 fn member_status_result_round_trips_resolved_capabilities() -> Result<(), serde_json::Error> {
2163 let capabilities = crate::wire::WireResolvedModelCapabilities {
2164 vision: true,
2165 image_input: true,
2166 image_tool_results: false,
2167 inline_video: false,
2168 realtime: true,
2169 web_search: true,
2170 image_generation: true,
2171 };
2172 let result = MobMemberStatusResult {
2173 status: WireMobMemberStatus::Active,
2174 member_ref: WireMemberRef::encode("mob-1", "worker-1"),
2175 output_preview: None,
2176 error: None,
2177 tokens_used: 0,
2178 is_final: false,
2179 current_session_id: Some("session-1".to_string()),
2180 peer_connectivity: Some(WirePeerConnectivity::Known {
2181 snapshot: WirePeerConnectivitySnapshot {
2182 reachable_peer_count: 1,
2183 unknown_peer_count: 0,
2184 unreachable_peers: Vec::new(),
2185 },
2186 }),
2187 kickoff: None,
2188 external_member: None,
2189 resolved_capabilities: Some(capabilities.clone()),
2190 progress: None,
2191 activity: None,
2192 detached_jobs: None,
2193 placement: None,
2194 control_reachability: None,
2195 comms_reachability: None,
2196 last_seen_ms: None,
2197 freshness_reason: None,
2198 lifecycle_capabilities: None,
2199 non_portable_disabled: None,
2200 };
2201
2202 let json = serde_json::to_string(&result)?;
2203 assert!(json.contains("\"resolved_capabilities\""));
2204 let parsed: MobMemberStatusResult = serde_json::from_str(&json)?;
2205 assert_eq!(parsed.resolved_capabilities, Some(capabilities));
2206 Ok(())
2207 }
2208}
2209
2210#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
2212#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
2213pub struct MobDestroyResult {
2214 pub mob_id: String,
2215 pub ok: bool,
2216 pub destroy_report: Value,
2217}
2218
2219#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
2221#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
2222pub struct MobRotateSupervisorResult {
2223 pub mob_id: String,
2224 pub ok: bool,
2225 pub report: SupervisorRotationReportWire,
2226}
2227
2228#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
2230#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
2231pub struct SupervisorRotationReportWire {
2232 pub previous_epoch: u64,
2233 pub current_epoch: u64,
2234 pub public_peer_id: String,
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 SupervisorRotationIncompleteKind {
2242 SupervisorRotationIncomplete,
2243}
2244
2245#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
2247#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
2248#[serde(rename_all = "snake_case")]
2249pub enum SupervisorRotationRetryAuthority {
2250 PendingRotation,
2251 PreRotation,
2252}
2253
2254#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
2256#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
2257#[serde(rename_all = "snake_case")]
2258pub enum SupervisorRotationRetryScope {
2259 Durable,
2260 PreRotation,
2261}
2262
2263#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
2265#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
2266#[serde(rename_all = "snake_case")]
2267pub struct SupervisorRotationIncompleteDetailsWire {
2268 pub kind: SupervisorRotationIncompleteKind,
2269 pub previous_epoch: u64,
2270 pub attempted_epoch: u64,
2271 pub attempted_public_peer_id: String,
2272 pub rotated_peer_count: usize,
2273 pub rollback_succeeded: bool,
2274 pub pending_authority_recorded: bool,
2275 #[serde(default, skip_serializing_if = "Option::is_none")]
2276 pub rollback_error: Option<String>,
2277 pub retry_authority: SupervisorRotationRetryAuthority,
2278 pub retry_scope: SupervisorRotationRetryScope,
2279}
2280
2281#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
2283#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
2284#[serde(rename_all = "snake_case")]
2285pub struct SupervisorRotationIncompleteDataWire {
2286 pub code: String,
2287 pub message: String,
2288 pub details: SupervisorRotationIncompleteDetailsWire,
2289}
2290
2291#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
2293#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
2294#[serde(deny_unknown_fields)]
2295pub struct MobWaitParams {
2296 pub mob_id: String,
2297 #[serde(default, skip_serializing_if = "Option::is_none")]
2298 pub member_ids: Option<Vec<String>>,
2299 #[serde(default, skip_serializing_if = "Option::is_none")]
2300 pub timeout_ms: Option<u64>,
2301}
2302
2303#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
2305#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
2306pub struct MobWaitMembersResult {
2307 pub members: Vec<Value>,
2308}
2309
2310#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
2312#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
2313pub struct MobCancelWorkResult {
2314 pub mob_id: String,
2315 pub ok: bool,
2316}
2317
2318#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
2320#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
2321pub struct MobCancelAllWorkResult {
2322 pub mob_id: String,
2323 pub ok: bool,
2324}
2325
2326#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
2328#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
2329#[serde(deny_unknown_fields)]
2330pub struct MobProfileCreateParams {
2331 pub name: String,
2332 pub profile: MobProfileInput,
2333}
2334
2335#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
2337#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
2338#[serde(deny_unknown_fields)]
2339pub struct MobProfileNameParams {
2340 pub name: String,
2341}
2342
2343#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
2345#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
2346#[serde(deny_unknown_fields)]
2347pub struct MobProfileUpdateParams {
2348 pub name: String,
2349 pub profile: MobProfileInput,
2350 pub expected_revision: u64,
2351}
2352
2353#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
2355#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
2356#[serde(deny_unknown_fields)]
2357pub struct MobProfileDeleteParams {
2358 pub name: String,
2359 pub expected_revision: u64,
2360}
2361
2362#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
2364#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
2365pub struct MobProfileLookupResult {
2366 #[serde(default)]
2367 pub not_found: bool,
2368 pub name: String,
2369 #[serde(default, skip_serializing_if = "Option::is_none")]
2370 pub profile: Option<WireMobProfile>,
2371 #[serde(default, skip_serializing_if = "Option::is_none")]
2372 pub revision: Option<u64>,
2373 #[serde(default, skip_serializing_if = "Option::is_none")]
2374 pub created_at: Option<String>,
2375 #[serde(default, skip_serializing_if = "Option::is_none")]
2376 pub updated_at: Option<String>,
2377}
2378
2379#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
2381#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
2382pub struct MobProfileListResult {
2383 pub profiles: Vec<MobProfileLookupResult>,
2384}
2385
2386#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
2388#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
2389pub struct MobProfileDeleteResult {
2390 pub name: String,
2391 pub deleted_revision: u64,
2392}
2393
2394#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
2396#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
2397#[serde(deny_unknown_fields)]
2398pub struct MobStreamOpenParams {
2399 pub mob_id: String,
2400 #[serde(default, skip_serializing_if = "Option::is_none")]
2401 pub agent_identity: Option<String>,
2402}
2403
2404#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
2406#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
2407pub struct MobStreamOpenResult {
2408 pub stream_id: String,
2409 pub opened: bool,
2410}
2411
2412#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
2414#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
2415#[serde(deny_unknown_fields)]
2416pub struct MobStreamCloseParams {
2417 pub stream_id: String,
2418}
2419
2420#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
2422#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
2423pub struct MobStreamCloseResult {
2424 pub stream_id: String,
2425 pub closed: bool,
2426 pub already_closed: bool,
2427}
2428
2429#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq, Default)]
2432#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
2433#[serde(rename_all = "snake_case")]
2434pub enum WireWorkOrigin {
2435 #[default]
2436 External,
2437 Internal,
2438}
2439
2440#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
2446#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
2447#[serde(deny_unknown_fields)]
2448pub struct MobSubmitWorkParams {
2449 pub member_ref: WireMemberRef,
2450 #[serde(default, skip_serializing_if = "Option::is_none")]
2453 pub work_ref: Option<String>,
2454 pub content: WireContentInput,
2455 #[serde(default)]
2456 pub origin: WireWorkOrigin,
2457 #[serde(default, skip_serializing_if = "Option::is_none")]
2463 pub injected_context: Option<Vec<WireContentInput>>,
2464 #[serde(default, skip_serializing_if = "Option::is_none")]
2466 pub objective_id: Option<String>,
2467}
2468
2469#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
2471#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
2472pub struct MobSubmitWorkResult {
2473 pub mob_id: String,
2474 pub work_ref: String,
2475 pub member_ref: WireMemberRef,
2476 #[serde(default, skip_serializing_if = "Option::is_none")]
2477 pub objective_id: Option<String>,
2478}
2479
2480#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
2482#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
2483#[serde(deny_unknown_fields)]
2484pub struct MobConcludeObjectiveParams {
2485 pub member_ref: WireMemberRef,
2486 pub objective_id: String,
2487 pub outcome: String,
2488}
2489
2490#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
2491#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
2492pub struct MobConcludeObjectiveResult {
2493 pub member_ref: WireMemberRef,
2494 pub objective_id: String,
2495 pub concluded: bool,
2496}
2497
2498#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
2500#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
2501#[serde(deny_unknown_fields)]
2502pub struct MobCancelWorkParams {
2503 pub mob_id: String,
2504 pub work_ref: String,
2505}
2506
2507#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
2509#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
2510#[serde(deny_unknown_fields)]
2511pub struct MobCancelAllWorkParams {
2512 pub member_ref: WireMemberRef,
2513}
2514
2515#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq, Eq)]
2518#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
2519#[serde(deny_unknown_fields)]
2520pub struct MobMemberFilterWire {
2521 #[serde(default, skip_serializing_if = "BTreeMap::is_empty")]
2523 pub labels: BTreeMap<String, String>,
2524 #[serde(default, skip_serializing_if = "Option::is_none")]
2526 pub role: Option<String>,
2527 #[serde(default, skip_serializing_if = "Option::is_none")]
2529 pub status: Option<WireMobMemberStatus>,
2530}
2531
2532#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
2534#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
2535#[serde(deny_unknown_fields)]
2536pub struct MobListMembersMatchingParams {
2537 pub mob_id: String,
2538 #[serde(default)]
2539 pub filter: MobMemberFilterWire,
2540}
2541
2542#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq)]
2545#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
2546pub struct MobListMembersMatchingResult {
2547 #[serde(default)]
2548 pub members: Vec<Value>,
2549}
2550
2551#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq, Hash)]
2560#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
2561#[serde(rename_all = "snake_case")]
2562pub enum WireControlScope {
2563 List,
2564 ReadHistory,
2565 SubscribeEvents,
2566 SendCommand,
2567 Cancel,
2568 Retire,
2569 WireTopology,
2570 Live,
2571 AdminHost,
2572 AdminGrants,
2573}
2574
2575#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
2579#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
2580#[serde(rename_all = "snake_case")]
2581pub enum WireReachability {
2582 Reachable,
2583 Stale,
2584 Unreachable,
2585 Unknown,
2586}
2587
2588#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, Hash)]
2590#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
2591#[serde(transparent)]
2592pub struct WireHostRef(pub String);
2593
2594#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
2596#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
2597#[serde(deny_unknown_fields)]
2598pub struct WireMemberLifecycleCapabilities {
2599 pub transcript_edits: bool,
2600 pub revisions: bool,
2601 pub resume_after_restart: bool,
2602}
2603
2604#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
2606#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
2607#[serde(rename_all = "snake_case")]
2608pub enum WireHostBindPhase {
2609 Requested,
2610 Bound,
2611}
2612
2613#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
2622#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
2623#[serde(deny_unknown_fields)]
2624pub struct WireHostCapabilityFlags {
2625 pub protocol_min: u64,
2626 pub protocol_max: u64,
2627 pub engine_version: String,
2628 pub durable_sessions: bool,
2629 pub autonomous_members: bool,
2630 pub hard_cancel_member: bool,
2631 #[serde(default)]
2632 pub tracked_input_cancel: bool,
2633 pub memory_store: bool,
2634 pub mcp: bool,
2635 #[serde(default, skip_serializing_if = "std::collections::BTreeSet::is_empty")]
2636 pub resolvable_providers: std::collections::BTreeSet<String>,
2637 pub approval_forwarding: bool,
2638 #[serde(default, skip_serializing_if = "Option::is_none")]
2639 pub live_endpoint: Option<String>,
2640}
2641
2642#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
2654#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
2655pub struct MobHostStatus {
2656 pub host_id: WireHostRef,
2657 #[serde(default, skip_serializing_if = "Option::is_none")]
2658 pub endpoint: Option<String>,
2659 pub bind_phase: WireHostBindPhase,
2660 #[serde(default, skip_serializing_if = "Option::is_none")]
2661 pub authority_epoch: Option<u64>,
2662 #[serde(default, skip_serializing_if = "Option::is_none")]
2663 pub capabilities: Option<WireHostCapabilityFlags>,
2664 #[serde(default, skip_serializing_if = "Option::is_none")]
2665 pub control_reachability: Option<WireReachability>,
2666 #[serde(default, skip_serializing_if = "Option::is_none")]
2668 pub last_seen_ms: Option<u64>,
2669 #[serde(default, skip_serializing_if = "Option::is_none")]
2670 pub freshness_reason: Option<String>,
2671 pub materialized_member_count: u64,
2672}
2673
2674#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
2676#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
2677pub struct MobHostsResult {
2678 pub hosts: Vec<MobHostStatus>,
2679}
2680
2681#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
2683#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
2684pub struct WireRouteInstallObligation {
2685 pub edge_a: String,
2686 pub edge_b: String,
2687 pub host: WireHostRef,
2688}
2689
2690#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
2692#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
2693pub struct MobRouteInstallsResult {
2694 pub outstanding: Vec<WireRouteInstallObligation>,
2695 pub complete: bool,
2696}
2697
2698#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
2702#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
2703#[serde(rename_all = "snake_case")]
2704pub enum WireProjectionProvenance {
2705 HostClaimed,
2706 ControllingHostVerified,
2707}
2708
2709#[derive(Debug, Clone, Serialize, Deserialize)]
2718#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
2719#[serde(transparent)]
2720pub struct WireHistoryRow(pub super::session::WireSessionMessage);
2721
2722impl PartialEq for WireHistoryRow {
2723 fn eq(&self, other: &Self) -> bool {
2724 match (
2725 serde_json::to_value(&self.0),
2726 serde_json::to_value(&other.0),
2727 ) {
2728 (Ok(a), Ok(b)) => a == b,
2729 _ => false,
2733 }
2734 }
2735}
2736
2737impl Eq for WireHistoryRow {}
2738
2739#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
2743#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
2744#[serde(deny_unknown_fields)]
2745pub struct WireMemberHistoryPageBody {
2746 pub from_index: u64,
2747 pub messages: Vec<WireHistoryRow>,
2748 pub message_count: u64,
2751 #[serde(default, skip_serializing_if = "Option::is_none")]
2752 pub next_index: Option<u64>,
2753 pub complete: bool,
2754}
2755
2756impl WireMemberHistoryPageBody {
2757 pub fn try_from_history_page(
2762 page: &meerkat_core::service::SessionHistoryPage,
2763 ) -> Result<Self, super::error::WireConversionError> {
2764 let invalid =
2765 |reason: String| super::error::WireConversionError::MemberHistoryPage { debug: reason };
2766 let message_count = u64::try_from(page.message_count).map_err(|_| {
2767 invalid(format!(
2768 "message_count {} exceeds the u64 wire domain",
2769 page.message_count
2770 ))
2771 })?;
2772 let from_index = u64::try_from(page.offset).map_err(|_| {
2773 invalid(format!(
2774 "offset {} exceeds the u64 wire domain",
2775 page.offset
2776 ))
2777 })?;
2778 let served = u64::try_from(page.messages.len()).map_err(|_| {
2779 invalid(format!(
2780 "served row count {} exceeds the u64 wire domain",
2781 page.messages.len()
2782 ))
2783 })?;
2784 let next_index = if page.has_more {
2785 if served == 0 {
2786 return Err(invalid(format!(
2787 "page at offset {from_index} claims more rows but serves none"
2788 )));
2789 }
2790 Some(from_index.checked_add(served).ok_or_else(|| {
2791 invalid(format!(
2792 "offset {from_index} plus served row count {served} exhausts the u64 cursor domain"
2793 ))
2794 })?)
2795 } else {
2796 None
2797 };
2798 Ok(Self {
2799 from_index,
2800 messages: page
2801 .messages
2802 .iter()
2803 .map(|message| {
2804 WireHistoryRow(super::session::WireSessionMessage::from(message.clone()))
2805 })
2806 .collect(),
2807 message_count,
2808 next_index,
2809 complete: !page.has_more,
2810 })
2811 }
2812}
2813
2814#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
2816#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
2817#[serde(deny_unknown_fields)]
2818pub struct MobMemberHistoryParams {
2819 pub mob_id: String,
2820 pub agent_identity: String,
2821 #[serde(default, skip_serializing_if = "Option::is_none")]
2822 pub from_index: Option<u64>,
2823 #[serde(default, skip_serializing_if = "Option::is_none")]
2824 pub limit: Option<u32>,
2825}
2826
2827#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
2831#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
2832pub struct MobMemberHistoryResult {
2833 pub page: WireMemberHistoryPageBody,
2834 pub generation: u64,
2835 #[serde(default, skip_serializing_if = "Option::is_none")]
2836 pub placement: Option<WireHostRef>,
2837 pub provenance: WireProjectionProvenance,
2838}
2839
2840#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
2842#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
2843#[serde(deny_unknown_fields)]
2844pub struct MobBindHostParams {
2845 pub mob_id: String,
2846 pub descriptor: super::supervisor_bridge::WireHostBindingDescriptor,
2847}
2848
2849#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
2851#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
2852pub struct MobBindHostResult {
2853 pub host_id: WireHostRef,
2854 pub capabilities: WireHostCapabilityFlags,
2855 pub authority_epoch: u64,
2856}
2857
2858#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
2860#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
2861#[serde(deny_unknown_fields)]
2862pub struct MobRevokeHostParams {
2863 pub mob_id: String,
2864 pub host_id: WireHostRef,
2865}
2866
2867#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
2869#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
2870pub struct MobRevokeHostResult {
2871 pub host_id: WireHostRef,
2872 pub released_members: Vec<String>,
2875}
2876
2877#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
2879#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
2880pub struct WireGrantRecord {
2881 pub principal: String,
2882 pub scopes: Vec<WireControlScope>,
2883 #[serde(default, skip_serializing_if = "Option::is_none")]
2884 pub expires_at_ms: Option<u64>,
2885}
2886
2887#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
2889#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
2890#[serde(deny_unknown_fields)]
2891pub struct MobGrantScopesParams {
2892 pub mob_id: String,
2893 pub principal: String,
2894 pub scopes: Vec<WireControlScope>,
2895 #[serde(default, skip_serializing_if = "Option::is_none")]
2896 pub expires_at_ms: Option<u64>,
2897}
2898
2899#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
2901#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
2902pub struct MobGrantScopesResult {
2903 pub record: WireGrantRecord,
2904}
2905
2906#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
2909#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
2910#[serde(deny_unknown_fields)]
2911pub struct MobRevokeScopesParams {
2912 pub mob_id: String,
2913 pub principal: String,
2914 #[serde(default, skip_serializing_if = "Option::is_none")]
2915 pub scopes: Option<Vec<WireControlScope>>,
2916}
2917
2918#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
2920#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
2921pub struct MobRevokeScopesResult {
2922 pub removed: bool,
2923}
2924
2925#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
2927#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
2928pub struct MobGrantsResult {
2929 pub grants: Vec<WireGrantRecord>,
2930}
2931
2932#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
2939#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
2940#[serde(deny_unknown_fields)]
2941pub struct WireScopeDeniedDetail {
2942 pub required: WireControlScope,
2943 pub presented: Vec<WireControlScope>,
2944}
2945
2946#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
2949#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
2950#[serde(deny_unknown_fields)]
2951pub struct MobMemberLiveOpenParams {
2952 pub mob_id: String,
2953 pub agent_identity: String,
2954 #[serde(default, skip_serializing_if = "Option::is_none")]
2955 pub turning_mode: Option<super::realtime::RealtimeTurningMode>,
2956 #[serde(default, skip_serializing_if = "Option::is_none")]
2957 pub transport: Option<super::live::LiveOpenTransport>,
2958}
2959
2960#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
2966#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
2967#[serde(deny_unknown_fields)]
2968pub struct MobMemberLiveChannelParams {
2969 pub mob_id: String,
2970 pub agent_identity: String,
2971 pub channel_id: String,
2972}
2973
2974#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
2981#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
2982#[serde(deny_unknown_fields)]
2983pub struct MobMemberLiveStatusParams {
2984 pub mob_id: String,
2985 pub agent_identity: String,
2986 #[serde(default, skip_serializing_if = "Option::is_none")]
2987 pub channel_id: Option<String>,
2988}
2989
2990#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
2994#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
2995#[serde(deny_unknown_fields)]
2996pub struct MobHardCancelParams {
2997 pub mob_id: String,
2998 pub agent_identity: String,
2999 pub reason: String,
3000}
3001
3002#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
3006#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
3007pub struct MobHardCancelResult {
3008 pub cancelled: bool,
3009}
3010
3011#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
3013#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
3014#[serde(deny_unknown_fields)]
3015pub struct MobMemberLiveControlParams {
3016 pub mob_id: String,
3017 pub agent_identity: String,
3018 pub channel_id: String,
3019 pub verb: super::supervisor_bridge::BridgeLiveControlVerb,
3020}
3021
3022#[cfg(test)]
3023#[allow(clippy::expect_used, clippy::panic)]
3024mod tests {
3025 use super::*;
3026
3027 #[test]
3028 fn mob_helper_params_carry_structural_auth_binding() {
3029 let parsed: MobSpawnHelperParams = serde_json::from_value(serde_json::json!({
3030 "mob_id": "mob-1",
3031 "prompt": "help",
3032 "agent_identity": "helper",
3033 "auth_binding": {
3034 "realm": "dev",
3035 "binding": "default_anthropic",
3036 "profile": "console"
3037 }
3038 }))
3039 .expect("spawn helper params parse");
3040 let auth_binding = parsed.auth_binding.expect("auth_binding should parse");
3041 assert_eq!(auth_binding.realm.as_str(), "dev");
3042 assert_eq!(auth_binding.binding.as_str(), "default_anthropic");
3043 assert_eq!(
3044 auth_binding
3045 .profile
3046 .as_ref()
3047 .map(|profile| profile.as_str()),
3048 Some("console")
3049 );
3050
3051 let parsed: MobForkHelperParams = serde_json::from_value(serde_json::json!({
3052 "mob_id": "mob-1",
3053 "source_member_id": "source",
3054 "prompt": "help",
3055 "agent_identity": "helper",
3056 "auth_binding": {
3057 "realm": "dev",
3058 "binding": "default_anthropic"
3059 }
3060 }))
3061 .expect("fork helper params parse");
3062 let auth_binding = parsed.auth_binding.expect("auth_binding should parse");
3063 assert_eq!(auth_binding.realm.as_str(), "dev");
3064 assert_eq!(auth_binding.binding.as_str(), "default_anthropic");
3065 assert!(auth_binding.profile.is_none());
3066 }
3067
3068 #[test]
3069 fn wire_mob_profile_parses_provider_fields_fail_closed() {
3070 let legacy: WireMobProfile =
3072 serde_json::from_str(r#"{"model":"claude-opus-4-8"}"#).expect("legacy profile parses");
3073 assert_eq!(legacy.provider, None);
3074 assert!(legacy.resume_overrides.is_empty());
3075
3076 let full: WireMobProfile = serde_json::from_str(
3078 r#"{
3079 "model": "claude-internal-preview",
3080 "provider": "anthropic",
3081 "image_generation_provider": "gemini",
3082 "auto_compact_threshold": 60000,
3083 "resume_overrides": ["model", "provider"]
3084 }"#,
3085 )
3086 .expect("typed profile parses");
3087 assert_eq!(full.provider, Some(meerkat_core::Provider::Anthropic));
3088 assert_eq!(
3089 full.image_generation_provider,
3090 Some(meerkat_core::Provider::Gemini)
3091 );
3092 assert_eq!(
3093 full.resume_overrides,
3094 vec![
3095 WireMobResumeOverrideField::Model,
3096 WireMobResumeOverrideField::Provider
3097 ]
3098 );
3099
3100 assert!(
3102 serde_json::from_str::<WireMobProfile>(r#"{"model":"m","provider":"not-a-provider"}"#)
3103 .is_err(),
3104 "unknown provider names must fail closed at the wire boundary"
3105 );
3106 assert!(
3107 serde_json::from_str::<WireMobProfile>(r#"{"model":"m","auto_compact_threshold":0}"#)
3108 .is_err(),
3109 "zero auto_compact_threshold must fail closed at the wire boundary"
3110 );
3111 assert!(
3112 serde_json::from_str::<WireMobProfile>(
3113 r#"{"model":"m","resume_overrides":["everything"]}"#
3114 )
3115 .is_err(),
3116 "resume_overrides vocabulary is closed"
3117 );
3118 }
3119
3120 #[test]
3121 fn mob_definition_input_parses_custom_models() {
3122 let input: MobDefinitionInput = serde_json::from_str(
3123 r#"{
3124 "id": "m",
3125 "profiles": {"worker": {"model": "claude-internal-preview"}},
3126 "models": {
3127 "claude-internal-preview": {
3128 "provider": "anthropic",
3129 "context_window": 500000,
3130 "vision": true
3131 }
3132 },
3133 "image_generation_provider": "openai"
3134 }"#,
3135 )
3136 .expect("definition with custom models parses");
3137 let model = input
3138 .models
3139 .get("claude-internal-preview")
3140 .expect("custom model present");
3141 assert_eq!(model.provider, meerkat_core::Provider::Anthropic);
3142 assert_eq!(model.context_window, Some(500_000));
3143 assert_eq!(model.vision, Some(true));
3144 assert_eq!(
3145 input.image_generation_provider,
3146 Some(meerkat_core::Provider::OpenAI)
3147 );
3148 }
3149
3150 #[test]
3151 fn wire_member_ref_round_trips_through_encode_decode() {
3152 let token = WireMemberRef::encode("mob-42", "worker-1");
3153 let (mob_id, agent_identity) = token.decode().expect("decode round-trips");
3154 assert_eq!(mob_id, "mob-42");
3155 assert_eq!(agent_identity, "worker-1");
3156 }
3157
3158 #[test]
3159 fn wire_member_ref_rejects_malformed_token() {
3160 let err = WireMemberRef::from_token("not-a-token-payload")
3161 .decode()
3162 .expect_err("malformed tokens must fail to decode");
3163 assert!(matches!(err, WireMemberRefError::Malformed));
3164 }
3165
3166 #[test]
3167 fn mob_spawn_many_spec_placement_is_optional_and_round_trips() {
3168 let placed: MobSpawnSpecParams = serde_json::from_value(serde_json::json!({
3169 "profile": "worker",
3170 "agent_identity": "w1",
3171 "placement": "host-b-peer"
3172 }))
3173 .expect("placed spawn-many spec parses");
3174 assert_eq!(
3175 placed.placement.as_ref().map(|host| host.0.as_str()),
3176 Some("host-b-peer")
3177 );
3178 assert_eq!(
3179 serde_json::to_value(&placed).expect("placed spawn-many spec serializes")["placement"],
3180 "host-b-peer"
3181 );
3182
3183 let local: MobSpawnSpecParams = serde_json::from_value(serde_json::json!({
3184 "profile": "worker",
3185 "agent_identity": "w2"
3186 }))
3187 .expect("local spawn-many spec parses");
3188 assert!(local.placement.is_none());
3189 assert!(
3190 serde_json::to_value(&local)
3191 .expect("local spawn-many spec serializes")
3192 .get("placement")
3193 .is_none(),
3194 "absent placement must remain omitted for source and wire compatibility"
3195 );
3196 }
3197
3198 #[test]
3199 fn mob_member_spec_placement_is_optional_and_round_trips() {
3200 let placed: MobMemberSpecWire = serde_json::from_value(serde_json::json!({
3201 "profile": "worker",
3202 "agent_identity": "w1",
3203 "placement": "host-b-peer"
3204 }))
3205 .expect("placed declarative member spec parses");
3206 assert_eq!(
3207 placed.placement.as_ref().map(|host| host.0.as_str()),
3208 Some("host-b-peer")
3209 );
3210
3211 let local: MobMemberSpecWire = serde_json::from_value(serde_json::json!({
3212 "profile": "worker",
3213 "agent_identity": "w2"
3214 }))
3215 .expect("local declarative member spec parses");
3216 assert!(local.placement.is_none());
3217 }
3218
3219 #[test]
3220 fn mob_member_spec_exposes_shared_surface_metadata() {
3221 let spec = MobMemberSpecWire {
3222 profile: "worker".into(),
3223 agent_identity: "w1".into(),
3224 initial_message: None,
3225 runtime_mode: None,
3226 backend: None,
3227 placement: None,
3228 binding: None,
3229 context: Some(serde_json::json!({"client_ref": "member-card"})),
3230 labels: Some(BTreeMap::from([("client.member_id".into(), "w1".into())])),
3231 additional_instructions: None,
3232 auto_wire_parent: None,
3233 };
3234
3235 let metadata = spec.surface_metadata();
3236 assert_eq!(
3237 metadata.labels.get("client.member_id").map(String::as_str),
3238 Some("w1")
3239 );
3240 assert_eq!(
3241 metadata.app_context,
3242 Some(serde_json::json!({"client_ref": "member-card"}))
3243 );
3244 }
3245
3246 #[test]
3247 fn mob_member_spec_surface_metadata_rejects_reserved_keys() {
3248 let spec = MobMemberSpecWire {
3249 profile: "worker".into(),
3250 agent_identity: "w1".into(),
3251 initial_message: None,
3252 runtime_mode: None,
3253 backend: None,
3254 placement: None,
3255 binding: None,
3256 context: None,
3257 labels: Some(BTreeMap::from([("mob_id".into(), "spoof".into())])),
3258 additional_instructions: None,
3259 auto_wire_parent: None,
3260 };
3261
3262 assert!(spec.validate_public_surface_metadata().is_err());
3263 }
3264
3265 #[test]
3266 fn mob_reconcile_failure_stage_is_typed_wire_enum() {
3267 let failure = MobReconcileFailureWire {
3268 agent_identity: "worker-1".into(),
3269 stage: WireMobReconcileStage::Spawn,
3270 error: WireMobError {
3271 code: MobSpawnManyFailureCause::ProfileNotFound,
3272 message: "spawn failed".into(),
3273 },
3274 };
3275
3276 let json = serde_json::to_value(&failure).expect("serialize failure");
3277 assert_eq!(json["stage"], "spawn");
3278 assert_eq!(json["error"]["code"], "profile_not_found");
3279 assert_eq!(json["error"]["message"], "spawn failed");
3280
3281 let round_trip: MobReconcileFailureWire =
3282 serde_json::from_value(json).expect("deserialize failure");
3283 assert_eq!(round_trip.stage, WireMobReconcileStage::Spawn);
3284 assert_eq!(
3285 round_trip.error.code,
3286 MobSpawnManyFailureCause::ProfileNotFound
3287 );
3288
3289 let err = serde_json::from_value::<MobReconcileFailureWire>(serde_json::json!({
3290 "agent_identity": "worker-1",
3291 "stage": "restart",
3292 "error": { "code": "profile_not_found", "message": "bad stage" }
3293 }))
3294 .expect_err("unknown reconcile stage must be rejected");
3295 assert!(err.to_string().contains("unknown variant"));
3296 }
3297
3298 #[test]
3299 fn mob_lifecycle_params_reject_unknown_action_string() {
3300 let err = serde_json::from_value::<MobLifecycleParams>(serde_json::json!({
3301 "mob_id": "mob-1",
3302 "action": "explode"
3303 }))
3304 .expect_err("unknown lifecycle actions must fail at the typed wire boundary");
3305
3306 assert!(
3307 err.to_string().contains("unknown variant"),
3308 "unexpected error: {err}"
3309 );
3310 }
3311
3312 #[test]
3313 fn mob_lifecycle_result_round_trips_typed_action() {
3314 let result = MobLifecycleResult {
3315 mob_id: "mob-1".into(),
3316 action: WireMobLifecycleAction::Complete,
3317 ok: true,
3318 destroy_report: None,
3319 };
3320
3321 let json = serde_json::to_value(&result).expect("serialize lifecycle result");
3322 assert_eq!(json["action"], "complete");
3323
3324 let round_trip: MobLifecycleResult =
3325 serde_json::from_value(json).expect("deserialize lifecycle result");
3326 assert_eq!(round_trip.action, WireMobLifecycleAction::Complete);
3327 }
3328
3329 #[test]
3330 fn mob_wire_members_batch_contract_is_local_edge_native() {
3331 let params: MobWireMembersBatchParams = serde_json::from_value(serde_json::json!({
3332 "mob_id": "mob-1",
3333 "edges": [
3334 { "a": "lead", "b": "worker-b" },
3335 { "a": "worker-a", "b": "lead" }
3336 ]
3337 }))
3338 .expect("batch wire params deserialize");
3339
3340 assert_eq!(params.mob_id, "mob-1");
3341 assert_eq!(params.edges.len(), 2);
3342 assert_eq!(params.edges[0].a, "lead");
3343 assert_eq!(params.edges[0].b, "worker-b");
3344
3345 let result = MobWireMembersBatchResult {
3346 requested: 2,
3347 wired: vec![MobWireMembersBatchEdge {
3348 a: "lead".into(),
3349 b: "worker-a".into(),
3350 }],
3351 already_wired: vec![MobWireMembersBatchEdge {
3352 a: "lead".into(),
3353 b: "worker-b".into(),
3354 }],
3355 };
3356 let json = serde_json::to_value(&result).expect("serialize batch wire result");
3357 assert_eq!(json["requested"], 2);
3358 assert_eq!(json["wired"][0]["a"], "lead");
3359 assert_eq!(json["already_wired"][0]["b"], "worker-b");
3360
3361 let err = serde_json::from_value::<MobWireMembersBatchParams>(serde_json::json!({
3362 "mob_id": "mob-1",
3363 "edges": [{ "member": "lead", "peer": "worker-a" }]
3364 }))
3365 .expect_err("mixed local/external mob/wire shape must not deserialize");
3366 let message = err.to_string();
3367 assert!(
3368 message.contains("unknown field `member`") || message.contains("missing field `a`"),
3369 "unexpected error: {message}"
3370 );
3371 }
3372
3373 #[test]
3374 fn mob_spawn_many_result_entry_uses_typed_status_result_envelope() {
3375 let member_ref = WireMemberRef::encode("mob-1", "worker-1");
3376 let entry = MobSpawnManyResultEntry::spawned("worker-1", member_ref.clone());
3377
3378 let json = serde_json::to_value(&entry).expect("serialize typed spawn_many row");
3379 assert_eq!(json["status"], "spawned");
3380 assert_eq!(json["result"]["agent_identity"], "worker-1");
3381 assert_eq!(json["result"]["member_ref"], member_ref.as_str());
3382 assert!(json.get("ok").is_none());
3383 assert!(json.get("error").is_none());
3384
3385 let round_trip: MobSpawnManyResultEntry =
3386 serde_json::from_value(json).expect("deserialize typed spawn_many row");
3387 assert_eq!(round_trip, entry);
3388
3389 let failed = MobSpawnManyResultEntry::failed(
3390 MobSpawnManyFailureCause::ProfileNotFound,
3391 "profile missing",
3392 );
3393 let json = serde_json::to_value(&failed).expect("serialize typed failed spawn_many row");
3394 assert_eq!(json["status"], "failed");
3395 assert_eq!(json["result"]["cause"], "profile_not_found");
3396 assert_eq!(json["result"]["message"], "profile missing");
3397 assert!(json.get("ok").is_none());
3398 assert!(json.get("error").is_none());
3399
3400 let round_trip: MobSpawnManyResultEntry =
3401 serde_json::from_value(json).expect("deserialize typed failed spawn_many row");
3402 assert_eq!(round_trip, failed);
3403 }
3404
3405 #[test]
3406 fn mob_spawn_many_result_entry_rejects_legacy_or_malformed_envelopes() {
3407 let legacy = serde_json::json!({
3408 "ok": true,
3409 "agent_identity": "worker-1",
3410 "member_ref": WireMemberRef::encode("mob-1", "worker-1"),
3411 });
3412 let err = serde_json::from_value::<MobSpawnManyResultEntry>(legacy)
3413 .expect_err("legacy ok carrier must not deserialize");
3414 assert!(
3415 err.to_string().contains("missing field `status`")
3416 || err.to_string().contains("unknown field"),
3417 "unexpected error: {err}"
3418 );
3419
3420 let missing_result = serde_json::json!({
3421 "status": "spawned"
3422 });
3423 let err = serde_json::from_value::<MobSpawnManyResultEntry>(missing_result)
3424 .expect_err("missing typed result must fail closed");
3425 assert!(
3426 err.to_string().contains("missing field `result`"),
3427 "unexpected error: {err}"
3428 );
3429
3430 let unknown_status = serde_json::json!({
3431 "status": "ok",
3432 "result": {
3433 "agent_identity": "worker-1",
3434 "member_ref": WireMemberRef::encode("mob-1", "worker-1"),
3435 }
3436 });
3437 let err = serde_json::from_value::<MobSpawnManyResultEntry>(unknown_status)
3438 .expect_err("unknown typed status must fail closed");
3439 assert!(
3440 err.to_string().contains("unknown variant"),
3441 "unexpected error: {err}"
3442 );
3443
3444 let mismatched = serde_json::json!({
3445 "status": "spawned",
3446 "result": {
3447 "cause": "profile_not_found",
3448 "message": "profile missing"
3449 }
3450 });
3451 let err = serde_json::from_value::<MobSpawnManyResultEntry>(mismatched)
3452 .expect_err("status/result mismatch must fail closed");
3453 assert!(
3454 err.to_string()
3455 .contains("status spawned requires spawned result"),
3456 "unexpected error: {err}"
3457 );
3458
3459 let message_only_failure = serde_json::json!({
3460 "status": "failed",
3461 "result": {
3462 "message": "profile missing"
3463 }
3464 });
3465 let err = serde_json::from_value::<MobSpawnManyResultEntry>(message_only_failure)
3466 .expect_err("string-only failure result must fail closed");
3467 assert!(
3468 err.to_string().contains("data did not match any variant")
3469 || err.to_string().contains("missing field `cause`"),
3470 "unexpected error: {err}"
3471 );
3472
3473 let unknown_failure_cause = serde_json::json!({
3474 "status": "failed",
3475 "result": {
3476 "cause": "future_failure",
3477 "message": "future failure"
3478 }
3479 });
3480 let err = serde_json::from_value::<MobSpawnManyResultEntry>(unknown_failure_cause)
3481 .expect_err("unknown failure cause must fail closed");
3482 assert!(
3483 err.to_string().contains("data did not match any variant")
3484 || err.to_string().contains("unknown variant"),
3485 "unexpected error: {err}"
3486 );
3487 }
3488
3489 #[test]
3490 fn mob_wire_params_reject_legacy_local_target_shape() {
3491 let err = serde_json::from_value::<MobWireParams>(serde_json::json!({
3492 "mob_id": "mob-1",
3493 "local": "member-a",
3494 "target": { "local": "member-b" }
3495 }))
3496 .expect_err("legacy local/target shape must be rejected");
3497
3498 let msg = err.to_string();
3499 assert!(
3500 msg.contains("unknown field `local`") || msg.contains("missing field `member`"),
3501 "unexpected error: {msg}"
3502 );
3503 }
3504
3505 #[test]
3506 fn mob_wire_params_accept_canonical_external_peer_identity() {
3507 let params = serde_json::from_value::<MobWireParams>(serde_json::json!({
3508 "mob_id": "mob-1",
3509 "member": "member-a",
3510 "peer": {
3511 "external": {
3512 "name": "external-worker",
3513 "address": "inproc://external-worker",
3514 "identity": {
3515 "kind": "ed25519_public_key",
3516 "public_key": "ed25519:BwcHBwcHBwcHBwcHBwcHBwcHBwcHBwcHBwcHBwcHBwc="
3517 }
3518 }
3519 }
3520 }))
3521 .expect("canonical external peer identity should deserialize");
3522
3523 let MobPeerTarget::External(spec) = params.peer else {
3524 panic!("expected external peer target");
3525 };
3526 assert_eq!(spec.name, "external-worker");
3527 }
3528
3529 #[test]
3530 fn mob_wire_params_reject_raw_external_peer_id_shape() {
3531 let err = serde_json::from_value::<MobWireParams>(serde_json::json!({
3532 "mob_id": "mob-1",
3533 "member": "member-a",
3534 "peer": {
3535 "external": {
3536 "name": "external-worker",
3537 "peer_id": meerkat_core::comms::PeerId::from_ed25519_pubkey(&[7u8; 32]).to_string(),
3538 "address": "inproc://external-worker",
3539 "pubkey": vec![7u8; 32]
3540 }
3541 }
3542 }))
3543 .expect_err("raw peer_id/pubkey external peer shape must be rejected");
3544
3545 let msg = err.to_string();
3546 assert!(
3547 msg.contains("peer_id") || msg.contains("identity"),
3548 "unexpected error: {msg}"
3549 );
3550 }
3551
3552 #[test]
3553 fn mob_wire_params_reject_missing_external_peer_pubkey_material() {
3554 let err = serde_json::from_value::<MobWireParams>(serde_json::json!({
3555 "mob_id": "mob-1",
3556 "member": "member-a",
3557 "peer": {
3558 "external": {
3559 "name": "external-worker",
3560 "address": "inproc://external-worker",
3561 "identity": {
3562 "kind": "ed25519_public_key"
3563 }
3564 }
3565 }
3566 }))
3567 .expect_err("missing external peer pubkey material must fail closed");
3568
3569 let msg = err.to_string();
3570 assert!(
3571 msg.contains("public_key") || msg.contains("identity"),
3572 "unexpected error: {msg}"
3573 );
3574 }
3575
3576 #[test]
3577 fn runtime_binding_accepts_canonical_external_peer_identity() {
3578 let binding = serde_json::from_value::<WireRuntimeBinding>(serde_json::json!({
3579 "kind": "external",
3580 "address": "inproc://external-worker",
3581 "identity": {
3582 "kind": "ed25519_public_key",
3583 "public_key": "ed25519:BwcHBwcHBwcHBwcHBwcHBwcHBwcHBwcHBwcHBwcHBwc="
3584 }
3585 }))
3586 .expect("canonical external runtime binding identity should deserialize");
3587
3588 let WireRuntimeBinding::External {
3589 identity, address, ..
3590 } = binding
3591 else {
3592 panic!("expected external runtime binding");
3593 };
3594 assert_eq!(address, "inproc://external-worker");
3595 assert_eq!(
3596 identity.resolve().expect("identity resolves").pubkey,
3597 [7u8; 32]
3598 );
3599 }
3600
3601 #[test]
3602 fn runtime_binding_rejects_raw_external_peer_id_shape() {
3603 let err = serde_json::from_value::<WireRuntimeBinding>(serde_json::json!({
3604 "kind": "external",
3605 "peer_id": meerkat_core::comms::PeerId::from_ed25519_pubkey(&[7u8; 32]).to_string(),
3606 "address": "inproc://external-worker",
3607 "pubkey": vec![7u8; 32]
3608 }))
3609 .expect_err("raw peer_id/pubkey external runtime binding shape must be rejected");
3610
3611 let msg = err.to_string();
3612 assert!(
3613 msg.contains("peer_id") || msg.contains("identity"),
3614 "unexpected error: {msg}"
3615 );
3616 }
3617
3618 #[test]
3619 fn runtime_binding_rejects_missing_external_peer_pubkey_material() {
3620 let err = serde_json::from_value::<WireRuntimeBinding>(serde_json::json!({
3621 "kind": "external",
3622 "address": "inproc://external-worker",
3623 "identity": {
3624 "kind": "ed25519_public_key"
3625 }
3626 }))
3627 .expect_err("missing external runtime binding pubkey material must fail closed");
3628
3629 let msg = err.to_string();
3630 assert!(
3631 msg.contains("public_key") || msg.contains("identity"),
3632 "unexpected error: {msg}"
3633 );
3634 }
3635
3636 #[test]
3637 fn mob_turn_start_params_capture_turn_override_fields() {
3638 let params = serde_json::from_value::<MobTurnStartParams>(serde_json::json!({
3639 "mob_id": "mob-1",
3640 "agent_identity": "worker",
3641 "prompt": "continue",
3642 "output_schema": { "type": "object" },
3643 "structured_output_retries": 2
3644 }))
3645 .expect("turn_start should accept explicit turn override fields");
3646
3647 assert_eq!(params.mob_id, "mob-1");
3648 assert_eq!(params.agent_identity, "worker");
3649 assert_eq!(params.prompt, WireContentInput::Text("continue".into()));
3650 assert_eq!(
3651 params.output_schema,
3652 Some(serde_json::json!({ "type": "object" }))
3653 );
3654 assert_eq!(params.structured_output_retries, Some(2));
3655
3656 let err = serde_json::from_value::<MobTurnStartParams>(serde_json::json!({
3657 "mob_id": "mob-1",
3658 "agent_identity": "worker",
3659 "prompt": "continue",
3660 "unknown_override": true
3661 }))
3662 .expect_err("turn_start must reject unknown override fields");
3663 assert!(
3664 err.to_string().contains("unknown field"),
3665 "unexpected error: {err}"
3666 );
3667 }
3668
3669 #[test]
3670 fn mob_create_params_reject_reserved_runtime_lifecycle_fields() {
3671 let err = serde_json::from_value::<MobCreateParams>(serde_json::json!({
3672 "definition": {
3673 "id": "mob-1",
3674 "owner_runtime_binding": "runtime:worker:0",
3675 "profiles": {
3676 "worker": { "model": "claude-sonnet-4-6" }
3677 }
3678 }
3679 }))
3680 .expect_err("reserved runtime lifecycle fields must be rejected");
3681
3682 assert!(
3683 err.to_string()
3684 .contains("unknown field `owner_runtime_binding`"),
3685 "unexpected error: {err}"
3686 );
3687 }
3688
3689 #[test]
3690 fn mob_create_params_reject_reserved_runtime_bridge_owner_field() {
3691 let err = serde_json::from_value::<MobCreateParams>(serde_json::json!({
3692 "definition": {
3693 "id": "mob-1",
3694 "owner_transport_binding": "transport:worker:0",
3695 "profiles": {
3696 "worker": { "model": "claude-sonnet-4-6" }
3697 }
3698 }
3699 }))
3700 .expect_err("reserved runtime bridge owner field must be rejected");
3701
3702 assert!(
3703 err.to_string()
3704 .contains("unknown field `owner_transport_binding`"),
3705 "unexpected error: {err}"
3706 );
3707 }
3708
3709 #[test]
3710 fn mob_create_params_reject_internal_profile_tool_bundles() {
3711 let err = serde_json::from_value::<MobCreateParams>(serde_json::json!({
3712 "definition": {
3713 "id": "mob-1",
3714 "profiles": {
3715 "worker": {
3716 "model": "claude-sonnet-4-6",
3717 "tools": {
3718 "rust_bundles": ["internal-only"]
3719 }
3720 }
3721 }
3722 }
3723 }))
3724 .expect_err("internal rust tool bundles must be rejected");
3725
3726 assert!(
3729 err.to_string().contains("did not match any variant")
3730 || err.to_string().contains("unknown field `rust_bundles`"),
3731 "unexpected error: {err}"
3732 );
3733 }
3734
3735 #[test]
3736 fn mob_create_params_accept_typed_nested_flow_definition() {
3737 let params = serde_json::from_value::<MobCreateParams>(serde_json::json!({
3738 "definition": {
3739 "id": "mob-1",
3740 "profiles": {
3741 "worker": { "model": "claude-sonnet-4-6" }
3742 },
3743 "flows": {
3744 "review": {
3745 "description": "review flow",
3746 "steps": {
3747 "draft": {
3748 "role": "worker",
3749 "message": "draft it"
3750 }
3751 }
3752 }
3753 }
3754 }
3755 }))
3756 .expect("typed nested flow definition should parse");
3757
3758 assert_eq!(
3759 params.definition.flows["review"].steps["draft"].role,
3760 "worker"
3761 );
3762 }
3763
3764 #[test]
3769 fn mob_spawn_params_reject_deleted_budget_split_policy() {
3770 let err = serde_json::from_value::<MobSpawnParams>(serde_json::json!({
3771 "mob_id": "mob-1",
3772 "profile": "worker",
3773 "agent_identity": "worker-1",
3774 "budget_split_policy": { "type": "equal" }
3775 }))
3776 .expect_err("deleted budget_split_policy must fail closed at the wire boundary");
3777 assert!(
3778 err.to_string()
3779 .contains("unknown field `budget_split_policy`"),
3780 "unexpected error: {err}"
3781 );
3782 }
3783
3784 #[test]
3788 fn mob_spawn_params_placement_round_trips_and_defaults_absent() {
3789 let params = serde_json::from_value::<MobSpawnParams>(serde_json::json!({
3790 "mob_id": "mob-1",
3791 "profile": "worker",
3792 "agent_identity": "worker-1"
3793 }))
3794 .expect("placement-less params must decode");
3795 assert_eq!(params.placement, None);
3796 let encoded = serde_json::to_value(¶ms).expect("serialize params");
3797 assert!(
3798 encoded.get("placement").is_none(),
3799 "absent placement must not serialize: {encoded}"
3800 );
3801
3802 let params = serde_json::from_value::<MobSpawnParams>(serde_json::json!({
3803 "mob_id": "mob-1",
3804 "profile": "worker",
3805 "agent_identity": "worker-1",
3806 "placement": "host-peer-b"
3807 }))
3808 .expect("placed params must decode");
3809 assert_eq!(params.placement.as_deref(), Some("host-peer-b"));
3810 let encoded = serde_json::to_value(¶ms).expect("serialize params");
3811 assert_eq!(encoded["placement"], serde_json::json!("host-peer-b"));
3812 }
3813
3814 fn minimal_member_status() -> MobMemberStatusResult {
3815 MobMemberStatusResult {
3816 status: WireMobMemberStatus::Active,
3817 member_ref: WireMemberRef::encode("mob-1", "worker-1"),
3818 output_preview: None,
3819 error: None,
3820 tokens_used: 0,
3821 is_final: false,
3822 current_session_id: None,
3823 peer_connectivity: None,
3824 kickoff: None,
3825 external_member: None,
3826 resolved_capabilities: None,
3827 progress: None,
3828 activity: None,
3829 detached_jobs: None,
3830 placement: None,
3831 control_reachability: None,
3832 comms_reachability: None,
3833 last_seen_ms: None,
3834 freshness_reason: None,
3835 lifecycle_capabilities: None,
3836 non_portable_disabled: None,
3837 }
3838 }
3839
3840 #[test]
3843 fn member_status_multi_host_fields_skip_when_absent_and_decode_legacy() {
3844 let value = serde_json::to_value(minimal_member_status()).expect("serialize member status");
3845 for absent in [
3846 "placement",
3847 "control_reachability",
3848 "comms_reachability",
3849 "last_seen_ms",
3850 "freshness_reason",
3851 "lifecycle_capabilities",
3852 "non_portable_disabled",
3853 ] {
3854 assert!(
3855 value.get(absent).is_none(),
3856 "absent {absent} must be omitted from the wire form: {value}"
3857 );
3858 }
3859
3860 let legacy = serde_json::json!({
3862 "status": "active",
3863 "member_ref": WireMemberRef::encode("mob-1", "worker-1"),
3864 "tokens_used": 3,
3865 "is_final": false,
3866 });
3867 let decoded: MobMemberStatusResult =
3868 serde_json::from_value(legacy).expect("legacy member status decodes");
3869 assert!(decoded.placement.is_none());
3870 assert!(decoded.lifecycle_capabilities.is_none());
3871 }
3872
3873 #[test]
3876 fn member_status_carries_placement_only_at_typed_keys() {
3877 let mut status = minimal_member_status();
3878 status.placement = Some(WireHostRef("host-b-peer".to_string()));
3879 status.control_reachability = Some(WireReachability::Stale);
3880 status.comms_reachability = Some(WireReachability::Reachable);
3881 status.last_seen_ms = Some(1_234);
3882 status.freshness_reason = Some("pump idle".to_string());
3883 status.lifecycle_capabilities = Some(WireMemberLifecycleCapabilities {
3884 transcript_edits: false,
3885 revisions: false,
3886 resume_after_restart: true,
3887 });
3888 status.non_portable_disabled = Some(vec![
3889 super::super::portable_spec::WireNonPortableResourceKind::WorkgraphTools,
3890 ]);
3891 status.external_member = Some(serde_json::json!({"endpoint": "tcp://10.0.0.2:7101"}));
3892
3893 let value = serde_json::to_value(&status).expect("serialize member status");
3894 assert_eq!(value["placement"], serde_json::json!("host-b-peer"));
3895 assert_eq!(value["control_reachability"], serde_json::json!("stale"));
3896 assert_eq!(
3897 value["non_portable_disabled"],
3898 serde_json::json!(["workgraph_tools"])
3899 );
3900 assert!(
3901 value["external_member"].get("placement").is_none(),
3902 "placement must not ride the opaque external_member value (SD-5)"
3903 );
3904
3905 let decoded: MobMemberStatusResult =
3906 serde_json::from_value(value).expect("decode member status");
3907 assert_eq!(decoded.placement, status.placement);
3908 assert_eq!(decoded.control_reachability, status.control_reachability);
3909 }
3910
3911 #[test]
3912 fn control_scope_and_reachability_round_trip_snake_case() {
3913 let scopes: &[(WireControlScope, &str)] = &[
3914 (WireControlScope::List, "list"),
3915 (WireControlScope::ReadHistory, "read_history"),
3916 (WireControlScope::SubscribeEvents, "subscribe_events"),
3917 (WireControlScope::SendCommand, "send_command"),
3918 (WireControlScope::Cancel, "cancel"),
3919 (WireControlScope::Retire, "retire"),
3920 (WireControlScope::WireTopology, "wire_topology"),
3921 (WireControlScope::Live, "live"),
3922 (WireControlScope::AdminHost, "admin_host"),
3923 (WireControlScope::AdminGrants, "admin_grants"),
3924 ];
3925 for (scope, expected) in scopes {
3926 let value = serde_json::to_value(scope).expect("serialize scope");
3927 assert_eq!(value, serde_json::json!(expected));
3928 let decoded: WireControlScope = serde_json::from_value(value).expect("decode scope");
3929 assert_eq!(decoded, *scope);
3930 }
3931 assert!(
3932 serde_json::from_value::<WireControlScope>(serde_json::json!("admin")).is_err(),
3933 "unknown scopes must fail decode (closed vocabulary)"
3934 );
3935
3936 let classes: &[(WireReachability, &str)] = &[
3937 (WireReachability::Reachable, "reachable"),
3938 (WireReachability::Stale, "stale"),
3939 (WireReachability::Unreachable, "unreachable"),
3940 (WireReachability::Unknown, "unknown"),
3941 ];
3942 for (class, expected) in classes {
3943 let value = serde_json::to_value(class).expect("serialize reachability");
3944 assert_eq!(value, serde_json::json!(expected));
3945 let decoded: WireReachability =
3946 serde_json::from_value(value).expect("decode reachability");
3947 assert_eq!(decoded, *class);
3948 }
3949 }
3950
3951 #[test]
3952 fn scope_denied_detail_round_trips_snake_case_and_denies_unknown_fields() {
3953 let detail = WireScopeDeniedDetail {
3954 required: WireControlScope::AdminGrants,
3955 presented: vec![WireControlScope::List, WireControlScope::SendCommand],
3956 };
3957 let value = serde_json::to_value(&detail).expect("serialize detail");
3958 assert_eq!(
3959 value,
3960 serde_json::json!({
3961 "required": "admin_grants",
3962 "presented": ["list", "send_command"],
3963 })
3964 );
3965 let decoded: WireScopeDeniedDetail = serde_json::from_value(value).expect("decode detail");
3966 assert_eq!(decoded, detail);
3967
3968 assert!(
3969 serde_json::from_value::<WireScopeDeniedDetail>(serde_json::json!({
3970 "required": "admin_grants",
3971 "presented": [],
3972 "reason": "extra",
3973 }))
3974 .is_err(),
3975 "unknown fields must be rejected (deny_unknown_fields)"
3976 );
3977 }
3978
3979 #[test]
3980 fn host_status_and_grants_round_trip() {
3981 let host = MobHostStatus {
3982 host_id: WireHostRef("host-b-peer".to_string()),
3983 endpoint: Some("tcp://10.0.0.2:7100".to_string()),
3984 bind_phase: WireHostBindPhase::Bound,
3985 authority_epoch: Some(4),
3986 capabilities: Some(WireHostCapabilityFlags {
3987 protocol_min: 2,
3988 protocol_max: 4,
3989 engine_version: "0.7.22".to_string(),
3990 durable_sessions: true,
3991 autonomous_members: true,
3992 hard_cancel_member: false,
3993 tracked_input_cancel: false,
3994 memory_store: false,
3995 mcp: true,
3996 resolvable_providers: std::collections::BTreeSet::from(["anthropic".to_string()]),
3997 approval_forwarding: false,
3998 live_endpoint: None,
3999 }),
4000 control_reachability: Some(WireReachability::Reachable),
4001 last_seen_ms: Some(250),
4002 freshness_reason: None,
4003 materialized_member_count: 2,
4004 };
4005 let result = MobHostsResult { hosts: vec![host] };
4006 let value = serde_json::to_value(&result).expect("serialize hosts");
4007 assert_eq!(value["hosts"][0]["bind_phase"], serde_json::json!("bound"));
4008 let decoded: MobHostsResult = serde_json::from_value(value).expect("decode hosts");
4009 assert_eq!(decoded, result);
4010
4011 let requested = MobHostStatus {
4014 host_id: WireHostRef("host-c-peer".to_string()),
4015 endpoint: None,
4016 bind_phase: WireHostBindPhase::Requested,
4017 authority_epoch: None,
4018 capabilities: None,
4019 control_reachability: None,
4020 last_seen_ms: None,
4021 freshness_reason: None,
4022 materialized_member_count: 0,
4023 };
4024 let value = serde_json::to_value(&requested).expect("serialize requested host");
4025 assert_eq!(value["bind_phase"], serde_json::json!("requested"));
4026 assert!(value.get("endpoint").is_none());
4027 assert!(value.get("authority_epoch").is_none());
4028 assert!(value.get("capabilities").is_none());
4029 let decoded: MobHostStatus = serde_json::from_value(value).expect("decode requested host");
4030 assert_eq!(decoded, requested);
4031
4032 let record = WireGrantRecord {
4033 principal: "console:luka".to_string(),
4034 scopes: vec![WireControlScope::List, WireControlScope::Live],
4035 expires_at_ms: None,
4036 };
4037 let value = serde_json::to_value(&record).expect("serialize grant");
4038 assert!(
4039 value.get("expires_at_ms").is_none(),
4040 "absent expiry must be omitted"
4041 );
4042 let decoded: WireGrantRecord = serde_json::from_value(value).expect("decode grant");
4043 assert_eq!(decoded, record);
4044 }
4045
4046 #[test]
4047 fn member_history_result_round_trips_with_provenance() {
4048 let result = MobMemberHistoryResult {
4049 page: WireMemberHistoryPageBody {
4050 from_index: 5,
4051 messages: Vec::new(),
4052 message_count: 12,
4053 next_index: Some(10),
4054 complete: false,
4055 },
4056 generation: 2,
4057 placement: Some(WireHostRef("host-b-peer".to_string())),
4058 provenance: WireProjectionProvenance::HostClaimed,
4059 };
4060 let value = serde_json::to_value(&result).expect("serialize history result");
4061 assert_eq!(value["provenance"], serde_json::json!("host_claimed"));
4062 assert_eq!(value["page"]["message_count"], serde_json::json!(12));
4063 let decoded: MobMemberHistoryResult =
4064 serde_json::from_value(value.clone()).expect("decode history result");
4065 let reencoded = serde_json::to_value(&decoded).expect("reserialize history result");
4066 assert_eq!(value, reencoded);
4067 }
4068
4069 #[test]
4070 fn member_history_projection_rejects_non_advancing_page() {
4071 let page = meerkat_core::service::SessionHistoryPage {
4072 session_id: meerkat_core::SessionId::new(),
4073 message_count: 1,
4074 offset: 0,
4075 limit: Some(1),
4076 has_more: true,
4077 messages: Vec::new(),
4078 };
4079 let error = WireMemberHistoryPageBody::try_from_history_page(&page)
4080 .expect_err("a page that claims more rows must advance its cursor");
4081 assert!(matches!(
4082 error,
4083 crate::wire::error::WireConversionError::MemberHistoryPage { debug }
4084 if debug.contains("serves none")
4085 ));
4086 }
4087
4088 #[cfg(target_pointer_width = "64")]
4089 #[test]
4090 fn member_history_projection_rejects_exhausted_cursor() {
4091 let page = meerkat_core::service::SessionHistoryPage {
4092 session_id: meerkat_core::SessionId::new(),
4093 message_count: usize::MAX,
4094 offset: usize::MAX,
4095 limit: Some(2),
4096 has_more: true,
4097 messages: vec![
4098 meerkat_core::types::Message::User(meerkat_core::types::UserMessage::text("first")),
4099 meerkat_core::types::Message::User(meerkat_core::types::UserMessage::text(
4100 "second",
4101 )),
4102 ],
4103 };
4104 let error = WireMemberHistoryPageBody::try_from_history_page(&page)
4105 .expect_err("MAX has no representable member-history successor");
4106 assert!(matches!(
4107 error,
4108 crate::wire::error::WireConversionError::MemberHistoryPage { debug }
4109 if debug.contains("exhausts the u64 cursor domain")
4110 ));
4111 }
4112
4113 #[test]
4118 fn hard_cancel_params_round_trip_and_reject_unknown_fields() {
4119 let params = MobHardCancelParams {
4120 mob_id: "mob-1".to_string(),
4121 agent_identity: "worker".to_string(),
4122 reason: "operator interrupt".to_string(),
4123 };
4124 let value = serde_json::to_value(¶ms).expect("serialize hard-cancel params");
4125 let decoded: MobHardCancelParams =
4126 serde_json::from_value(value).expect("decode hard-cancel params");
4127 assert_eq!(decoded, params);
4128
4129 serde_json::from_value::<MobHardCancelParams>(serde_json::json!({
4130 "mob_id": "mob-1",
4131 "agent_identity": "worker",
4132 "reason": "x",
4133 "force": true,
4134 }))
4135 .expect_err("unknown field must be rejected");
4136
4137 serde_json::from_value::<MobHardCancelParams>(serde_json::json!({
4140 "mob_id": "mob-1",
4141 "agent_identity": "worker",
4142 }))
4143 .expect_err("missing reason must be rejected");
4144
4145 let result = MobHardCancelResult { cancelled: true };
4146 let value = serde_json::to_value(&result).expect("serialize hard-cancel result");
4147 assert_eq!(value, serde_json::json!({ "cancelled": true }));
4148 }
4149
4150 #[test]
4151 fn member_live_status_params_keep_the_discovery_read() {
4152 let discovery: MobMemberLiveStatusParams = serde_json::from_value(serde_json::json!({
4155 "mob_id": "mob-1",
4156 "agent_identity": "worker",
4157 }))
4158 .expect("discovery status params parse");
4159 assert_eq!(discovery.channel_id, None);
4160 let value = serde_json::to_value(&discovery).expect("serialize discovery params");
4161 assert!(
4162 value.get("channel_id").is_none(),
4163 "absent channel_id must be omitted"
4164 );
4165
4166 let named: MobMemberLiveStatusParams = serde_json::from_value(serde_json::json!({
4167 "mob_id": "mob-1",
4168 "agent_identity": "worker",
4169 "channel_id": "chan-7",
4170 }))
4171 .expect("named status params parse");
4172 assert_eq!(named.channel_id.as_deref(), Some("chan-7"));
4173
4174 serde_json::from_value::<MobMemberLiveStatusParams>(serde_json::json!({
4175 "mob_id": "mob-1",
4176 "agent_identity": "worker",
4177 "chan": "chan-7",
4178 }))
4179 .expect_err("unknown field must be rejected");
4180
4181 serde_json::from_value::<MobMemberLiveChannelParams>(serde_json::json!({
4183 "mob_id": "mob-1",
4184 "agent_identity": "worker",
4185 }))
4186 .expect_err("close without channel_id must be rejected");
4187 }
4188}