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 #[serde(default, skip_serializing_if = "Option::is_none")]
851 pub structured_data: Option<Value>,
852}
853
854#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
856#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
857#[serde(untagged)]
858pub enum MobSpawnManyResultPayload {
859 Spawned(MobSpawnManySpawnedResult),
860 Failed(MobSpawnManyFailedResult),
861}
862
863#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
865#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
866#[serde(try_from = "MobSpawnManyResultEntryRaw")]
867pub struct MobSpawnManyResultEntry {
868 pub status: MobSpawnManyResultStatus,
869 pub result: MobSpawnManyResultPayload,
870}
871
872#[derive(Debug, Deserialize)]
873#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
874#[serde(deny_unknown_fields)]
875struct MobSpawnManyResultEntryRaw {
876 status: MobSpawnManyResultStatus,
877 result: MobSpawnManyResultPayload,
878}
879
880impl TryFrom<MobSpawnManyResultEntryRaw> for MobSpawnManyResultEntry {
881 type Error = String;
882
883 fn try_from(raw: MobSpawnManyResultEntryRaw) -> Result<Self, Self::Error> {
884 let entry = Self {
885 status: raw.status,
886 result: raw.result,
887 };
888 entry.validate().map_err(str::to_owned)?;
889 Ok(entry)
890 }
891}
892
893impl MobSpawnManyResultEntry {
894 pub fn spawned(agent_identity: impl Into<String>, member_ref: WireMemberRef) -> Self {
895 Self {
896 status: MobSpawnManyResultStatus::Spawned,
897 result: MobSpawnManyResultPayload::Spawned(MobSpawnManySpawnedResult {
898 agent_identity: agent_identity.into(),
899 member_ref,
900 }),
901 }
902 }
903
904 pub fn failed(cause: MobSpawnManyFailureCause, message: impl Into<String>) -> Self {
905 Self::failed_with_structured_data(cause, message, None)
906 }
907
908 pub fn failed_with_structured_data(
909 cause: MobSpawnManyFailureCause,
910 message: impl Into<String>,
911 structured_data: Option<Value>,
912 ) -> Self {
913 Self {
914 status: MobSpawnManyResultStatus::Failed,
915 result: MobSpawnManyResultPayload::Failed(MobSpawnManyFailedResult {
916 cause,
917 message: message.into(),
918 structured_data,
919 }),
920 }
921 }
922
923 pub fn validate(&self) -> Result<(), &'static str> {
924 match (&self.status, &self.result) {
925 (MobSpawnManyResultStatus::Spawned, MobSpawnManyResultPayload::Spawned(_))
926 | (MobSpawnManyResultStatus::Failed, MobSpawnManyResultPayload::Failed(_)) => Ok(()),
927 (MobSpawnManyResultStatus::Spawned, MobSpawnManyResultPayload::Failed(_)) => {
928 Err("mob spawn_many result status spawned requires spawned result")
929 }
930 (MobSpawnManyResultStatus::Failed, MobSpawnManyResultPayload::Spawned(_)) => {
931 Err("mob spawn_many result status failed requires failed result")
932 }
933 }
934 }
935}
936
937#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
939#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
940pub struct MobSpawnManyResult {
941 pub results: Vec<MobSpawnManyResultEntry>,
942}
943
944#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
946#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
947pub struct MobRetireResult {
948 pub retired: bool,
949}
950
951#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
953#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
954#[serde(deny_unknown_fields)]
955pub struct MobRespawnParams {
956 pub mob_id: String,
957 pub agent_identity: String,
958 #[serde(default, skip_serializing_if = "Option::is_none")]
959 pub initial_message: Option<WireContentInput>,
960}
961
962#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
964#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
965pub struct MobRespawnReceipt {
966 pub identity: String,
967 pub member_ref: WireMemberRef,
968}
969
970#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
975#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
976#[serde(rename_all = "snake_case")]
977pub enum WireMobRespawnOutcome {
978 Completed,
979 TopologyRestoreFailed,
980}
981
982#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
984#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
985pub struct MobRespawnResult {
986 pub status: WireMobRespawnOutcome,
987 pub receipt: MobRespawnReceipt,
988 #[serde(default, skip_serializing_if = "Vec::is_empty")]
989 pub failed_peer_ids: Vec<String>,
990}
991
992#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
994#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
995pub struct MobMembersResult {
996 pub mob_id: String,
997 pub members: Vec<MobMemberListEntryWire>,
998}
999
1000#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
1002#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
1003#[serde(deny_unknown_fields)]
1004pub struct MobEventsParams {
1005 pub mob_id: String,
1006 #[serde(default)]
1007 pub after_cursor: u64,
1008 #[serde(default = "default_mob_events_limit")]
1009 pub limit: usize,
1010 #[serde(default)]
1011 pub strict: bool,
1012}
1013
1014const fn default_mob_events_limit() -> usize {
1015 100
1016}
1017
1018#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
1020#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
1021pub struct MobEventsResult {
1022 pub events: Vec<Value>,
1023}
1024
1025#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
1027#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
1028#[serde(tag = "kind", rename_all = "snake_case", deny_unknown_fields)]
1029pub enum WireTrustedPeerIdentity {
1030 Ed25519PublicKey { public_key: String },
1032}
1033
1034#[derive(Debug, Clone, Copy, PartialEq, Eq)]
1036pub struct ResolvedWireTrustedPeerIdentity {
1037 pub peer_id: meerkat_core::comms::PeerId,
1038 pub pubkey: [u8; 32],
1039}
1040
1041#[derive(Debug, Clone, thiserror::Error, PartialEq, Eq)]
1043pub enum WireTrustedPeerIdentityError {
1044 #[error("external peer identity public_key must start with 'ed25519:'")]
1045 MissingEd25519Prefix,
1046 #[error("external peer identity public_key is not valid base64: {0}")]
1047 InvalidBase64(String),
1048 #[error("external peer identity public_key must decode to 32 bytes, got {actual}")]
1049 InvalidLength { actual: usize },
1050 #[error("external peer identity public_key must be non-zero")]
1051 ZeroPublicKey,
1052}
1053
1054impl WireTrustedPeerIdentity {
1055 pub fn resolve(&self) -> Result<ResolvedWireTrustedPeerIdentity, WireTrustedPeerIdentityError> {
1056 match self {
1057 Self::Ed25519PublicKey { public_key } => {
1058 let pubkey = parse_ed25519_public_key(public_key)?;
1059 if pubkey == [0u8; 32] {
1060 return Err(WireTrustedPeerIdentityError::ZeroPublicKey);
1061 }
1062 Ok(ResolvedWireTrustedPeerIdentity {
1063 peer_id: meerkat_core::comms::PeerId::from_ed25519_pubkey(&pubkey),
1064 pubkey,
1065 })
1066 }
1067 }
1068 }
1069}
1070
1071fn parse_ed25519_public_key(raw: &str) -> Result<[u8; 32], WireTrustedPeerIdentityError> {
1072 const PREFIX: &str = "ed25519:";
1073 let encoded = raw
1074 .strip_prefix(PREFIX)
1075 .ok_or(WireTrustedPeerIdentityError::MissingEd25519Prefix)?;
1076 let bytes = BASE64
1077 .decode(encoded)
1078 .map_err(|err| WireTrustedPeerIdentityError::InvalidBase64(err.to_string()))?;
1079 let actual = bytes.len();
1080 let pubkey: [u8; 32] = bytes
1081 .try_into()
1082 .map_err(|_| WireTrustedPeerIdentityError::InvalidLength { actual })?;
1083 Ok(pubkey)
1084}
1085
1086#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
1092#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
1093#[serde(deny_unknown_fields)]
1094pub struct WireTrustedPeerSpec {
1095 pub name: String,
1096 pub address: String,
1097 pub identity: WireTrustedPeerIdentity,
1098}
1099
1100#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
1102#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
1103#[serde(rename_all = "snake_case")]
1104pub enum MobPeerTarget {
1105 Local(String),
1106 External(WireTrustedPeerSpec),
1107}
1108
1109#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
1111#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
1112#[serde(deny_unknown_fields)]
1113pub struct MobWireParams {
1114 pub mob_id: String,
1115 pub member: String,
1116 pub peer: MobPeerTarget,
1117}
1118
1119#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
1121#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
1122pub struct MobWireResult {
1123 pub wired: bool,
1124}
1125
1126#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
1128#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
1129#[serde(deny_unknown_fields)]
1130pub struct MobWireMembersBatchEdge {
1131 pub a: String,
1132 pub b: String,
1133}
1134
1135#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
1137#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
1138#[serde(deny_unknown_fields)]
1139pub struct MobWireMembersBatchParams {
1140 pub mob_id: String,
1141 pub edges: Vec<MobWireMembersBatchEdge>,
1142}
1143
1144#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
1146#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
1147pub struct MobWireMembersBatchResult {
1148 pub requested: usize,
1149 pub wired: Vec<MobWireMembersBatchEdge>,
1150 pub already_wired: Vec<MobWireMembersBatchEdge>,
1151}
1152
1153#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
1155#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
1156#[serde(deny_unknown_fields)]
1157pub struct MobUnwireParams {
1158 pub mob_id: String,
1159 pub member: String,
1160 pub peer: MobPeerTarget,
1161}
1162
1163#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
1165#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
1166pub struct MobUnwireResult {
1167 pub unwired: bool,
1168}
1169
1170#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
1172#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
1173#[serde(deny_unknown_fields)]
1174pub struct MobMemberSendParams {
1175 pub mob_id: String,
1176 pub agent_identity: String,
1177 pub content: WireContentInput,
1178 #[serde(default)]
1179 pub handling_mode: WireHandlingMode,
1180 #[serde(default, skip_serializing_if = "Option::is_none")]
1181 pub render_metadata: Option<WireRenderMetadata>,
1182}
1183
1184#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
1186#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
1187pub struct WireAgentRuntimeId {
1188 pub identity: String,
1189 pub generation: u64,
1190}
1191
1192#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
1194#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
1195pub struct MobMemberSendResult {
1196 pub mob_id: String,
1197 pub agent_identity: String,
1199 pub member_ref: WireMemberRef,
1204 pub handling_mode: WireHandlingMode,
1205}
1206
1207#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
1213#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
1214#[serde(deny_unknown_fields)]
1215pub struct MobIngressInteractionParams {
1216 pub mob_id: String,
1217 pub spec: MobMemberSpecWire,
1218 pub content: WireContentInput,
1219 #[serde(default)]
1220 pub handling_mode: WireHandlingMode,
1221 #[serde(default, skip_serializing_if = "Option::is_none")]
1222 pub render_metadata: Option<WireRenderMetadata>,
1223}
1224
1225#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
1227#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
1228pub struct MobIngressInteractionResult {
1229 pub mob_id: String,
1230 pub agent_identity: String,
1231 pub member_ref: WireMemberRef,
1232 pub ensure_outcome: MobEnsureMemberOutcomeWire,
1233 pub delivery: MobMemberSendResult,
1234 pub events_after_cursor: u64,
1236 pub latest_event_cursor: u64,
1238}
1239
1240#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq, Default)]
1242#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
1243#[serde(rename_all = "snake_case")]
1244pub enum WireHandlingMode {
1245 #[default]
1246 Queue,
1247 Steer,
1248}
1249
1250impl From<WireHandlingMode> for HandlingMode {
1251 fn from(mode: WireHandlingMode) -> Self {
1252 match mode {
1253 WireHandlingMode::Queue => HandlingMode::Queue,
1254 WireHandlingMode::Steer => HandlingMode::Steer,
1255 }
1256 }
1257}
1258
1259impl From<HandlingMode> for WireHandlingMode {
1260 fn from(mode: HandlingMode) -> Self {
1261 match mode {
1262 HandlingMode::Queue => WireHandlingMode::Queue,
1263 HandlingMode::Steer => WireHandlingMode::Steer,
1264 }
1265 }
1266}
1267
1268#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
1270#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
1271#[serde(rename_all = "snake_case")]
1272pub enum WireRenderClass {
1273 UserPrompt,
1274 PeerMessage,
1275 PeerRequest,
1276 PeerResponse,
1277 ExternalEvent,
1278 FlowStep,
1279 Continuation,
1280 SystemNotice,
1281 ToolScopeNotice,
1282 OpsProgress,
1283}
1284
1285impl From<WireRenderClass> for RenderClass {
1286 fn from(class: WireRenderClass) -> Self {
1287 match class {
1288 WireRenderClass::UserPrompt => RenderClass::UserPrompt,
1289 WireRenderClass::PeerMessage => RenderClass::PeerMessage,
1290 WireRenderClass::PeerRequest => RenderClass::PeerRequest,
1291 WireRenderClass::PeerResponse => RenderClass::PeerResponse,
1292 WireRenderClass::ExternalEvent => RenderClass::ExternalEvent,
1293 WireRenderClass::FlowStep => RenderClass::FlowStep,
1294 WireRenderClass::Continuation => RenderClass::Continuation,
1295 WireRenderClass::SystemNotice => RenderClass::SystemNotice,
1296 WireRenderClass::ToolScopeNotice => RenderClass::ToolScopeNotice,
1297 WireRenderClass::OpsProgress => RenderClass::OpsProgress,
1298 }
1299 }
1300}
1301
1302impl From<RenderClass> for WireRenderClass {
1303 fn from(class: RenderClass) -> Self {
1304 match class {
1305 RenderClass::UserPrompt => WireRenderClass::UserPrompt,
1306 RenderClass::PeerMessage => WireRenderClass::PeerMessage,
1307 RenderClass::PeerRequest => WireRenderClass::PeerRequest,
1308 RenderClass::PeerResponse => WireRenderClass::PeerResponse,
1309 RenderClass::ExternalEvent => WireRenderClass::ExternalEvent,
1310 RenderClass::FlowStep => WireRenderClass::FlowStep,
1311 RenderClass::Continuation => WireRenderClass::Continuation,
1312 RenderClass::SystemNotice => WireRenderClass::SystemNotice,
1313 RenderClass::ToolScopeNotice => WireRenderClass::ToolScopeNotice,
1314 RenderClass::OpsProgress => WireRenderClass::OpsProgress,
1315 }
1316 }
1317}
1318
1319#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
1321#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
1322#[serde(rename_all = "snake_case")]
1323pub enum WireRenderSalience {
1324 Background,
1325 Normal,
1326 Important,
1327 Urgent,
1328}
1329
1330impl From<WireRenderSalience> for RenderSalience {
1331 fn from(salience: WireRenderSalience) -> Self {
1332 match salience {
1333 WireRenderSalience::Background => RenderSalience::Background,
1334 WireRenderSalience::Normal => RenderSalience::Normal,
1335 WireRenderSalience::Important => RenderSalience::Important,
1336 WireRenderSalience::Urgent => RenderSalience::Urgent,
1337 }
1338 }
1339}
1340
1341impl From<RenderSalience> for WireRenderSalience {
1342 fn from(salience: RenderSalience) -> Self {
1343 match salience {
1344 RenderSalience::Background => WireRenderSalience::Background,
1345 RenderSalience::Normal => WireRenderSalience::Normal,
1346 RenderSalience::Important => WireRenderSalience::Important,
1347 RenderSalience::Urgent => WireRenderSalience::Urgent,
1348 }
1349 }
1350}
1351
1352#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
1354#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
1355pub struct WireRenderMetadata {
1356 pub class: WireRenderClass,
1357 #[serde(default, skip_serializing_if = "Option::is_none")]
1358 pub salience: Option<WireRenderSalience>,
1359}
1360
1361impl From<WireRenderMetadata> for RenderMetadata {
1362 fn from(metadata: WireRenderMetadata) -> Self {
1363 Self {
1364 class: metadata.class.into(),
1365 salience: metadata
1366 .salience
1367 .unwrap_or(WireRenderSalience::Normal)
1368 .into(),
1369 }
1370 }
1371}
1372
1373impl From<RenderMetadata> for WireRenderMetadata {
1374 fn from(metadata: RenderMetadata) -> Self {
1375 Self {
1376 class: metadata.class.into(),
1377 salience: Some(metadata.salience.into()),
1378 }
1379 }
1380}
1381
1382#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
1397#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
1398pub struct MobMemberSpecWire {
1399 pub profile: String,
1401 pub agent_identity: String,
1403 #[serde(default, skip_serializing_if = "Option::is_none")]
1404 pub initial_message: Option<WireContentInput>,
1405 #[serde(default, skip_serializing_if = "Option::is_none")]
1406 pub runtime_mode: Option<WireMobRuntimeMode>,
1407 #[serde(default, skip_serializing_if = "Option::is_none")]
1408 pub backend: Option<WireMobBackendKind>,
1409 #[serde(default, skip_serializing_if = "Option::is_none")]
1411 pub placement: Option<WireHostRef>,
1412 #[serde(default, skip_serializing_if = "Option::is_none")]
1413 pub binding: Option<WireRuntimeBinding>,
1414 #[serde(default, skip_serializing_if = "Option::is_none")]
1415 pub context: Option<Value>,
1416 #[serde(default, skip_serializing_if = "Option::is_none")]
1417 pub labels: Option<BTreeMap<String, String>>,
1418 #[serde(default, skip_serializing_if = "Option::is_none")]
1419 pub additional_instructions: Option<Vec<String>>,
1420 #[serde(default, skip_serializing_if = "Option::is_none")]
1421 pub auto_wire_parent: Option<bool>,
1422}
1423
1424impl MobMemberSpecWire {
1425 #[must_use]
1428 pub fn surface_metadata(&self) -> SurfaceMetadata {
1429 SurfaceMetadata::from_optional_parts(self.labels.clone(), self.context.clone())
1430 }
1431
1432 pub fn validate_public_surface_metadata(&self) -> Result<(), SurfaceMetadataError> {
1434 self.surface_metadata().validate_public()
1435 }
1436}
1437
1438#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
1440#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
1441#[serde(deny_unknown_fields)]
1442pub struct MobEnsureMemberParams {
1443 pub mob_id: String,
1444 pub spec: MobMemberSpecWire,
1445}
1446
1447#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, Hash)]
1459#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
1460#[serde(transparent)]
1461pub struct WireMemberRef(String);
1462
1463impl WireMemberRef {
1464 #[must_use]
1468 pub fn encode(mob_id: &str, agent_identity: &str) -> Self {
1469 let payload = serde_json::json!({ "m": mob_id, "a": agent_identity });
1473 Self(base64_url_encode(payload.to_string().as_bytes()))
1474 }
1475
1476 #[must_use]
1478 pub fn as_str(&self) -> &str {
1479 &self.0
1480 }
1481
1482 #[must_use]
1485 pub fn from_token(token: impl Into<String>) -> Self {
1486 Self(token.into())
1487 }
1488
1489 pub fn decode(&self) -> Result<(String, String), WireMemberRefError> {
1492 let bytes = base64_url_decode(&self.0).map_err(|_| WireMemberRefError::Malformed)?;
1493 let value: Value =
1494 serde_json::from_slice(&bytes).map_err(|_| WireMemberRefError::Malformed)?;
1495 let mob_id = value
1496 .get("m")
1497 .and_then(Value::as_str)
1498 .ok_or(WireMemberRefError::Malformed)?;
1499 let agent_identity = value
1500 .get("a")
1501 .and_then(Value::as_str)
1502 .ok_or(WireMemberRefError::Malformed)?;
1503 Ok((mob_id.to_string(), agent_identity.to_string()))
1504 }
1505}
1506
1507#[derive(Debug, thiserror::Error)]
1509pub enum WireMemberRefError {
1510 #[error("malformed member ref token")]
1513 Malformed,
1514}
1515
1516fn base64_url_encode(bytes: &[u8]) -> String {
1517 use base64::Engine as _;
1518 base64::engine::general_purpose::URL_SAFE_NO_PAD.encode(bytes)
1519}
1520
1521fn base64_url_decode(input: &str) -> Result<Vec<u8>, base64::DecodeError> {
1522 use base64::Engine as _;
1523 base64::engine::general_purpose::URL_SAFE_NO_PAD.decode(input)
1524}
1525
1526#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
1528#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
1529pub struct MobSpawnReceiptWire {
1530 pub agent_identity: String,
1531 pub member_ref: WireMemberRef,
1535}
1536
1537#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
1539#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
1540#[serde(rename_all = "snake_case")]
1541pub enum WireMobMemberStatus {
1542 Active,
1543 Retiring,
1544 Broken,
1545 Completed,
1546 Unknown,
1547}
1548
1549#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
1554#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
1555pub struct MobMemberListEntryWire {
1556 pub agent_identity: String,
1557 pub member_ref: WireMemberRef,
1558 pub role: String,
1559 pub runtime_mode: WireMobRuntimeMode,
1560 #[serde(default, skip_serializing_if = "Vec::is_empty")]
1561 pub wired_to: Vec<String>,
1562 #[serde(default, skip_serializing_if = "BTreeMap::is_empty")]
1563 pub labels: BTreeMap<String, String>,
1564 pub status: WireMobMemberStatus,
1565 #[serde(default, skip_serializing_if = "Option::is_none")]
1566 pub error: Option<String>,
1567 pub is_final: bool,
1568}
1569
1570#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
1576#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
1577pub enum MobEnsureMemberOutcomeWire {
1578 #[serde(rename = "spawned")]
1579 Spawned(MobSpawnReceiptWire),
1580 #[serde(rename = "existed")]
1581 Existed(MobMemberListEntryWire),
1582}
1583
1584#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
1586#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
1587pub struct MobEnsureMemberResult {
1588 pub outcome: MobEnsureMemberOutcomeWire,
1589}
1590
1591#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq, Eq)]
1593#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
1594#[serde(deny_unknown_fields)]
1595pub struct MobReconcileOptionsWire {
1596 #[serde(default)]
1599 pub retire_stale: bool,
1600}
1601
1602#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
1604#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
1605#[serde(rename_all = "snake_case")]
1606pub enum WireMobReconcileStage {
1607 Spawn,
1608 Retire,
1609}
1610
1611#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
1613#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
1614#[serde(deny_unknown_fields)]
1615pub struct MobReconcileParams {
1616 pub mob_id: String,
1617 #[serde(default)]
1618 pub desired: Vec<MobMemberSpecWire>,
1619 #[serde(default)]
1620 pub options: MobReconcileOptionsWire,
1621}
1622
1623#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
1628#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
1629pub struct WireMobError {
1630 pub code: MobSpawnManyFailureCause,
1631 pub message: String,
1632}
1633
1634#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
1636#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
1637pub struct MobReconcileFailureWire {
1638 pub agent_identity: String,
1639 pub stage: WireMobReconcileStage,
1640 pub error: WireMobError,
1642}
1643
1644#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq)]
1646#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
1647pub struct MobReconcileReportWire {
1648 #[serde(default, skip_serializing_if = "Vec::is_empty")]
1649 pub desired: Vec<String>,
1650 #[serde(default, skip_serializing_if = "Vec::is_empty")]
1651 pub retained: Vec<String>,
1652 #[serde(default, skip_serializing_if = "Vec::is_empty")]
1653 pub spawned: Vec<MobSpawnReceiptWire>,
1654 #[serde(default, skip_serializing_if = "Vec::is_empty")]
1655 pub retired: Vec<String>,
1656 #[serde(default, skip_serializing_if = "Vec::is_empty")]
1657 pub failures: Vec<MobReconcileFailureWire>,
1658}
1659
1660#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
1662#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
1663pub struct MobReconcileResult {
1664 pub report: MobReconcileReportWire,
1665}
1666
1667#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
1672#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
1673#[serde(rename_all = "snake_case")]
1674pub enum WireMobLifecycleAction {
1675 Stop,
1676 Resume,
1677 Complete,
1678 Reset,
1679 Destroy,
1680}
1681
1682#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
1687#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
1688#[serde(rename_all = "snake_case")]
1689pub enum WireMobWireAction {
1690 Wire,
1691 Unwire,
1692}
1693
1694#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
1696#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
1697#[serde(deny_unknown_fields)]
1698pub struct MobLifecycleParams {
1699 pub mob_id: String,
1700 pub action: WireMobLifecycleAction,
1701}
1702
1703#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
1705#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
1706pub struct MobLifecycleResult {
1707 pub mob_id: String,
1708 pub action: WireMobLifecycleAction,
1709 pub ok: bool,
1710 #[serde(default, skip_serializing_if = "Option::is_none")]
1711 pub destroy_report: Option<Value>,
1712}
1713
1714#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
1719#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
1720#[serde(deny_unknown_fields)]
1721pub struct MobAppendSystemContextParams {
1722 pub mob_id: String,
1723 pub agent_identity: String,
1724 pub text: String,
1725 #[serde(default, skip_serializing_if = "Option::is_none")]
1726 pub source: Option<String>,
1727 #[serde(default, skip_serializing_if = "Option::is_none")]
1728 pub idempotency_key: Option<String>,
1729}
1730
1731#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
1736#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
1737#[serde(rename_all = "snake_case")]
1738pub enum WireAppendSystemContextStatus {
1739 Applied,
1740 Duplicate,
1741}
1742
1743impl From<meerkat_core::AppendSystemContextStatus> for WireAppendSystemContextStatus {
1744 fn from(status: meerkat_core::AppendSystemContextStatus) -> Self {
1745 match status {
1746 meerkat_core::AppendSystemContextStatus::Applied => Self::Applied,
1747 meerkat_core::AppendSystemContextStatus::Duplicate => Self::Duplicate,
1748 }
1749 }
1750}
1751
1752#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
1754#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
1755pub struct MobAppendSystemContextResult {
1756 pub mob_id: String,
1757 pub agent_identity: String,
1758 pub status: WireAppendSystemContextStatus,
1759}
1760
1761#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
1763#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
1764pub struct MobFlowsResult {
1765 pub mob_id: String,
1766 pub flows: Vec<String>,
1767}
1768
1769#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
1771#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
1772#[serde(deny_unknown_fields)]
1773pub struct MobFlowRunParams {
1774 pub mob_id: String,
1775 pub flow_id: String,
1776 #[serde(default)]
1777 pub params: Value,
1778}
1779
1780#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
1785#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
1786#[serde(deny_unknown_fields)]
1787pub struct MobRunParams {
1788 pub mob_id: String,
1789 #[serde(default, skip_serializing_if = "Option::is_none")]
1790 pub flow_id: Option<String>,
1791 #[serde(default, skip_serializing_if = "Option::is_none")]
1792 pub prompt: Option<String>,
1793 #[serde(default)]
1794 pub params: Value,
1795}
1796
1797#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
1799#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
1800pub struct MobFlowRunResult {
1801 pub run_id: String,
1802}
1803
1804#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
1806#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
1807#[serde(deny_unknown_fields)]
1808pub struct MobFlowStatusParams {
1809 pub mob_id: String,
1810 pub run_id: String,
1811}
1812
1813#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
1817#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
1818#[serde(rename_all = "snake_case")]
1819pub enum WireMobRunStatus {
1820 Pending,
1821 Running,
1822 Completed,
1823 Failed,
1824 Canceled,
1825}
1826
1827#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
1835#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
1836pub struct WireMobRun {
1837 pub run_id: String,
1838 pub mob_id: String,
1839 pub flow_id: String,
1840 pub status: WireMobRunStatus,
1841 #[serde(flatten)]
1844 pub kernel: serde_json::Map<String, Value>,
1845}
1846
1847#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
1851#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
1852pub struct MobFlowStatusResult {
1853 #[serde(default, skip_serializing_if = "Option::is_none")]
1854 pub run: Option<WireMobRun>,
1855}
1856
1857#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
1859#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
1860#[serde(deny_unknown_fields)]
1861pub struct MobRunResultParams {
1862 pub mob_id: String,
1863 pub run_id: String,
1864}
1865
1866#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
1868#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
1869#[serde(deny_unknown_fields)]
1870pub struct WireMobRunResultEnvelope {
1871 pub run_id: String,
1872 pub mob_id: String,
1873 pub flow_id: String,
1874 pub status: WireMobRunStatus,
1875 #[serde(default, skip_serializing_if = "Option::is_none")]
1876 pub result: Option<Value>,
1877 #[serde(default, skip_serializing_if = "BTreeMap::is_empty")]
1878 pub outputs: BTreeMap<String, Value>,
1879}
1880
1881#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
1885#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
1886pub struct MobRunResult {
1887 #[serde(default, skip_serializing_if = "Option::is_none")]
1888 pub run: Option<WireMobRunResultEnvelope>,
1889}
1890
1891#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
1893#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
1894#[serde(deny_unknown_fields)]
1895pub struct MobFlowCancelParams {
1896 pub mob_id: String,
1897 pub run_id: String,
1898}
1899
1900#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
1902#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
1903pub struct MobFlowCancelResult {
1904 pub canceled: bool,
1905}
1906
1907#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
1909#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
1910#[serde(deny_unknown_fields)]
1911pub struct MobSpawnHelperParams {
1912 pub mob_id: String,
1913 pub prompt: String,
1914 #[serde(default, skip_serializing_if = "Option::is_none")]
1915 pub agent_identity: Option<String>,
1916 #[serde(default, skip_serializing_if = "Option::is_none")]
1917 pub role_name: Option<String>,
1918 #[serde(default, skip_serializing_if = "Option::is_none")]
1919 pub model_override: Option<String>,
1920 #[serde(default, skip_serializing_if = "Option::is_none")]
1921 pub auth_binding: Option<WireAuthBindingRef>,
1922 #[serde(default, skip_serializing_if = "Option::is_none")]
1923 pub runtime_mode: Option<WireMobRuntimeMode>,
1924 #[serde(default, skip_serializing_if = "Option::is_none")]
1925 pub backend: Option<WireMobBackendKind>,
1926}
1927
1928#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
1930#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
1931#[serde(deny_unknown_fields)]
1932pub struct MobForkHelperParams {
1933 pub mob_id: String,
1934 pub source_member_id: String,
1935 pub prompt: String,
1936 #[serde(default, skip_serializing_if = "Option::is_none")]
1937 pub agent_identity: Option<String>,
1938 #[serde(default, skip_serializing_if = "Option::is_none")]
1939 pub role_name: Option<String>,
1940 #[serde(default, skip_serializing_if = "Option::is_none")]
1941 pub model_override: Option<String>,
1942 #[serde(default, skip_serializing_if = "Option::is_none")]
1943 pub auth_binding: Option<WireAuthBindingRef>,
1944 #[serde(default, skip_serializing_if = "Option::is_none")]
1945 pub fork_context: Option<Value>,
1946 #[serde(default, skip_serializing_if = "Option::is_none")]
1947 pub runtime_mode: Option<WireMobRuntimeMode>,
1948 #[serde(default, skip_serializing_if = "Option::is_none")]
1949 pub backend: Option<WireMobBackendKind>,
1950}
1951
1952#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
1954#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
1955pub struct MobHelperResult {
1956 #[serde(default, skip_serializing_if = "Option::is_none")]
1957 pub output: Option<String>,
1958 pub tokens_used: u64,
1959 pub agent_identity: String,
1960 pub member_ref: WireMemberRef,
1961}
1962
1963#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
1965#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
1966pub struct MobForceCancelResult {
1967 pub cancelled: bool,
1968}
1969
1970#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
1978#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
1979#[serde(deny_unknown_fields)]
1980pub struct MobTurnStartParams {
1981 pub mob_id: String,
1982 pub agent_identity: String,
1983 pub prompt: WireContentInput,
1984 #[serde(default, skip_serializing_if = "Option::is_none")]
1985 pub skill_refs: Option<Vec<meerkat_core::skills::SkillRef>>,
1986 #[serde(default, skip_serializing_if = "Option::is_none")]
1987 pub turn_tool_overlay: Option<meerkat_core::service::PublicTurnToolOverlay>,
1988 #[serde(default, skip_serializing_if = "Option::is_none")]
1989 pub additional_instructions: Option<Vec<String>>,
1990 #[serde(default, skip_serializing_if = "Option::is_none")]
1991 pub keep_alive: Option<bool>,
1992 #[serde(default, skip_serializing_if = "Option::is_none")]
1993 pub model: Option<String>,
1994 #[serde(default, skip_serializing_if = "Option::is_none")]
1995 pub provider: Option<String>,
1996 #[serde(default, skip_serializing_if = "Option::is_none")]
1998 pub self_hosted_server_id: Option<String>,
1999 #[serde(default, skip_serializing_if = "Option::is_none")]
2000 pub max_tokens: Option<u32>,
2001 #[serde(default, skip_serializing_if = "Option::is_none")]
2002 pub system_prompt: Option<String>,
2003 #[serde(default, skip_serializing_if = "Option::is_none")]
2004 pub output_schema: Option<Value>,
2005 #[serde(default, skip_serializing_if = "Option::is_none")]
2006 pub structured_output_retries: Option<u32>,
2007 #[serde(default, skip_serializing_if = "Option::is_none")]
2008 pub provider_params:
2009 Option<WireTurnMetadataOverride<crate::wire::runtime::WireProviderParamsOverride>>,
2010 #[serde(default, skip_serializing_if = "Option::is_none")]
2011 pub auth_binding: Option<WireTurnMetadataOverride<WireAuthBindingRef>>,
2012 #[serde(default, skip_serializing_if = "Option::is_none")]
2017 pub injected_context: Option<Vec<WireContentInput>>,
2018 #[serde(default, skip_serializing_if = "Option::is_none")]
2020 pub transient_turn_context: Option<meerkat_core::lifecycle::run_primitive::TurnRequestContext>,
2021}
2022
2023#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
2025#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
2026pub struct WireUnreachablePeer {
2027 pub peer: String,
2028 #[serde(default, skip_serializing_if = "Option::is_none")]
2029 pub reason: Option<String>,
2030}
2031
2032#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
2035#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
2036pub struct WirePeerConnectivitySnapshot {
2037 pub reachable_peer_count: usize,
2038 pub unknown_peer_count: usize,
2039 #[serde(default, skip_serializing_if = "Vec::is_empty")]
2040 pub unreachable_peers: Vec<WireUnreachablePeer>,
2041}
2042
2043#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
2052#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
2053#[serde(tag = "status", rename_all = "snake_case")]
2054pub enum WirePeerConnectivity {
2055 NotApplicable,
2058 ProbeTimedOut,
2060 Known {
2062 snapshot: WirePeerConnectivitySnapshot,
2063 },
2064}
2065
2066#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
2068#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
2069pub struct MobMemberStatusResult {
2070 pub status: WireMobMemberStatus,
2071 pub member_ref: WireMemberRef,
2073 #[serde(default, skip_serializing_if = "Option::is_none")]
2074 pub output_preview: Option<String>,
2075 #[serde(default, skip_serializing_if = "Option::is_none")]
2076 pub error: Option<String>,
2077 pub tokens_used: u64,
2078 pub is_final: bool,
2079 #[serde(default, skip_serializing_if = "Option::is_none")]
2080 pub current_session_id: Option<String>,
2081 #[serde(default, skip_serializing_if = "Option::is_none")]
2082 pub peer_connectivity: Option<WirePeerConnectivity>,
2083 #[serde(default, skip_serializing_if = "Option::is_none")]
2084 pub kickoff: Option<Value>,
2085 #[serde(default, skip_serializing_if = "Option::is_none")]
2086 pub external_member: Option<Value>,
2087 #[serde(default, skip_serializing_if = "Option::is_none")]
2088 pub resolved_capabilities: Option<crate::wire::WireResolvedModelCapabilities>,
2089 #[serde(default, skip_serializing_if = "Option::is_none")]
2090 pub progress: Option<WireMemberProgressSnapshot>,
2091 #[serde(default, skip_serializing_if = "Option::is_none")]
2093 pub activity: Option<crate::wire::JobExecutionActivity>,
2094 #[serde(default, skip_serializing_if = "Option::is_none")]
2096 pub detached_jobs: Option<crate::wire::DetachedJobsActivitySummary>,
2097 #[serde(default, skip_serializing_if = "Option::is_none")]
2103 pub placement: Option<WireHostRef>,
2104 #[serde(default, skip_serializing_if = "Option::is_none")]
2106 pub control_reachability: Option<WireReachability>,
2107 #[serde(default, skip_serializing_if = "Option::is_none")]
2109 pub comms_reachability: Option<WireReachability>,
2110 #[serde(default, skip_serializing_if = "Option::is_none")]
2113 pub last_seen_ms: Option<u64>,
2114 #[serde(default, skip_serializing_if = "Option::is_none")]
2115 pub freshness_reason: Option<String>,
2116 #[serde(default, skip_serializing_if = "Option::is_none")]
2118 pub lifecycle_capabilities: Option<WireMemberLifecycleCapabilities>,
2119 #[serde(default, skip_serializing_if = "Option::is_none")]
2122 pub non_portable_disabled: Option<Vec<super::portable_spec::WireNonPortableResourceKind>>,
2123}
2124
2125#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
2126#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
2127#[serde(rename_all = "snake_case")]
2128pub enum WireMemberRunState {
2129 Idle,
2130 RunOpen,
2131 Unknown,
2132}
2133
2134#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
2135#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
2136#[serde(rename_all = "snake_case")]
2137pub enum WireMemberHealthClass {
2138 Healthy,
2139 Degraded,
2140 Wedged,
2141 Unknown,
2142}
2143
2144#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
2145#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
2146#[serde(rename_all = "snake_case")]
2147pub enum WireMemberProgressEvent {
2148 ExecutionAdvanced,
2149 BecameIdle,
2150 Unchanged,
2151}
2152
2153#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
2154#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
2155pub struct WireMemberProgressSnapshot {
2156 pub run_state: WireMemberRunState,
2157 pub in_flight_work: u64,
2158 pub last_progress_at_ms: u64,
2159 pub last_progress_event: WireMemberProgressEvent,
2160 pub health: WireMemberHealthClass,
2161}
2162
2163#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
2165#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
2166pub struct MobSnapshotResult {
2167 pub mob_id: String,
2168 pub status: WireMobLifecycleStatus,
2169 pub members: Vec<MobMemberListEntryWire>,
2170}
2171
2172#[cfg(test)]
2173mod member_status_capability_tests {
2174 use super::*;
2175
2176 #[test]
2177 fn member_status_result_round_trips_resolved_capabilities() -> Result<(), serde_json::Error> {
2178 let capabilities = crate::wire::WireResolvedModelCapabilities {
2179 vision: true,
2180 image_input: true,
2181 image_tool_results: false,
2182 inline_video: false,
2183 realtime: true,
2184 web_search: true,
2185 image_generation: true,
2186 };
2187 let result = MobMemberStatusResult {
2188 status: WireMobMemberStatus::Active,
2189 member_ref: WireMemberRef::encode("mob-1", "worker-1"),
2190 output_preview: None,
2191 error: None,
2192 tokens_used: 0,
2193 is_final: false,
2194 current_session_id: Some("session-1".to_string()),
2195 peer_connectivity: Some(WirePeerConnectivity::Known {
2196 snapshot: WirePeerConnectivitySnapshot {
2197 reachable_peer_count: 1,
2198 unknown_peer_count: 0,
2199 unreachable_peers: Vec::new(),
2200 },
2201 }),
2202 kickoff: None,
2203 external_member: None,
2204 resolved_capabilities: Some(capabilities.clone()),
2205 progress: None,
2206 activity: None,
2207 detached_jobs: None,
2208 placement: None,
2209 control_reachability: None,
2210 comms_reachability: None,
2211 last_seen_ms: None,
2212 freshness_reason: None,
2213 lifecycle_capabilities: None,
2214 non_portable_disabled: None,
2215 };
2216
2217 let json = serde_json::to_string(&result)?;
2218 assert!(json.contains("\"resolved_capabilities\""));
2219 let parsed: MobMemberStatusResult = serde_json::from_str(&json)?;
2220 assert_eq!(parsed.resolved_capabilities, Some(capabilities));
2221 Ok(())
2222 }
2223}
2224
2225#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
2227#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
2228pub struct MobDestroyResult {
2229 pub mob_id: String,
2230 pub ok: bool,
2231 pub destroy_report: Value,
2232}
2233
2234#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
2236#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
2237pub struct MobRotateSupervisorResult {
2238 pub mob_id: String,
2239 pub ok: bool,
2240 pub report: SupervisorRotationReportWire,
2241}
2242
2243#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
2245#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
2246pub struct SupervisorRotationReportWire {
2247 pub previous_epoch: u64,
2248 pub current_epoch: u64,
2249 pub public_peer_id: String,
2250}
2251
2252#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
2254#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
2255#[serde(rename_all = "snake_case")]
2256pub enum SupervisorRotationIncompleteKind {
2257 SupervisorRotationIncomplete,
2258}
2259
2260#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
2262#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
2263#[serde(rename_all = "snake_case")]
2264pub enum SupervisorRotationRetryAuthority {
2265 PendingRotation,
2266 PreRotation,
2267}
2268
2269#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
2271#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
2272#[serde(rename_all = "snake_case")]
2273pub enum SupervisorRotationRetryScope {
2274 Durable,
2275 PreRotation,
2276}
2277
2278#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
2280#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
2281#[serde(rename_all = "snake_case")]
2282pub struct SupervisorRotationIncompleteDetailsWire {
2283 pub kind: SupervisorRotationIncompleteKind,
2284 pub previous_epoch: u64,
2285 pub attempted_epoch: u64,
2286 pub attempted_public_peer_id: String,
2287 pub rotated_peer_count: usize,
2288 pub rollback_succeeded: bool,
2289 pub pending_authority_recorded: bool,
2290 #[serde(default, skip_serializing_if = "Option::is_none")]
2291 pub rollback_error: Option<String>,
2292 pub retry_authority: SupervisorRotationRetryAuthority,
2293 pub retry_scope: SupervisorRotationRetryScope,
2294}
2295
2296#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
2298#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
2299#[serde(rename_all = "snake_case")]
2300pub struct SupervisorRotationIncompleteDataWire {
2301 pub code: String,
2302 pub message: String,
2303 pub details: SupervisorRotationIncompleteDetailsWire,
2304}
2305
2306#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
2308#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
2309#[serde(deny_unknown_fields)]
2310pub struct MobWaitParams {
2311 pub mob_id: String,
2312 #[serde(default, skip_serializing_if = "Option::is_none")]
2313 pub member_ids: Option<Vec<String>>,
2314 #[serde(default, skip_serializing_if = "Option::is_none")]
2315 pub timeout_ms: Option<u64>,
2316}
2317
2318#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
2320#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
2321pub struct MobWaitMembersResult {
2322 pub members: Vec<Value>,
2323}
2324
2325#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
2327#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
2328pub struct MobCancelWorkResult {
2329 pub mob_id: String,
2330 pub ok: bool,
2331}
2332
2333#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
2335#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
2336pub struct MobCancelAllWorkResult {
2337 pub mob_id: String,
2338 pub ok: bool,
2339}
2340
2341#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
2343#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
2344#[serde(deny_unknown_fields)]
2345pub struct MobProfileCreateParams {
2346 pub name: String,
2347 pub profile: MobProfileInput,
2348}
2349
2350#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
2352#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
2353#[serde(deny_unknown_fields)]
2354pub struct MobProfileNameParams {
2355 pub name: String,
2356}
2357
2358#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
2360#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
2361#[serde(deny_unknown_fields)]
2362pub struct MobProfileUpdateParams {
2363 pub name: String,
2364 pub profile: MobProfileInput,
2365 pub expected_revision: u64,
2366}
2367
2368#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
2370#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
2371#[serde(deny_unknown_fields)]
2372pub struct MobProfileDeleteParams {
2373 pub name: String,
2374 pub expected_revision: u64,
2375}
2376
2377#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
2379#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
2380pub struct MobProfileLookupResult {
2381 #[serde(default)]
2382 pub not_found: bool,
2383 pub name: String,
2384 #[serde(default, skip_serializing_if = "Option::is_none")]
2385 pub profile: Option<WireMobProfile>,
2386 #[serde(default, skip_serializing_if = "Option::is_none")]
2387 pub revision: Option<u64>,
2388 #[serde(default, skip_serializing_if = "Option::is_none")]
2389 pub created_at: Option<String>,
2390 #[serde(default, skip_serializing_if = "Option::is_none")]
2391 pub updated_at: Option<String>,
2392}
2393
2394#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
2396#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
2397pub struct MobProfileListResult {
2398 pub profiles: Vec<MobProfileLookupResult>,
2399}
2400
2401#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
2403#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
2404pub struct MobProfileDeleteResult {
2405 pub name: String,
2406 pub deleted_revision: u64,
2407}
2408
2409#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
2411#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
2412#[serde(deny_unknown_fields)]
2413pub struct MobStreamOpenParams {
2414 pub mob_id: String,
2415 #[serde(default, skip_serializing_if = "Option::is_none")]
2416 pub agent_identity: Option<String>,
2417}
2418
2419#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
2421#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
2422pub struct MobStreamOpenResult {
2423 pub stream_id: String,
2424 pub opened: bool,
2425}
2426
2427#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
2429#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
2430#[serde(deny_unknown_fields)]
2431pub struct MobStreamCloseParams {
2432 pub stream_id: String,
2433}
2434
2435#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
2437#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
2438pub struct MobStreamCloseResult {
2439 pub stream_id: String,
2440 pub closed: bool,
2441 pub already_closed: bool,
2442}
2443
2444#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq, Default)]
2447#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
2448#[serde(rename_all = "snake_case")]
2449pub enum WireWorkOrigin {
2450 #[default]
2451 External,
2452 Internal,
2453}
2454
2455#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
2461#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
2462#[serde(deny_unknown_fields)]
2463pub struct MobSubmitWorkParams {
2464 pub member_ref: WireMemberRef,
2465 #[serde(default, skip_serializing_if = "Option::is_none")]
2468 pub work_ref: Option<String>,
2469 pub content: WireContentInput,
2470 #[serde(default)]
2471 pub origin: WireWorkOrigin,
2472 #[serde(default, skip_serializing_if = "Option::is_none")]
2478 pub injected_context: Option<Vec<WireContentInput>>,
2479 #[serde(default, skip_serializing_if = "Option::is_none")]
2481 pub transient_turn_context: Option<meerkat_core::lifecycle::run_primitive::TurnRequestContext>,
2482 #[serde(default, skip_serializing_if = "Option::is_none")]
2484 pub objective_id: Option<String>,
2485}
2486
2487#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
2489#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
2490pub struct MobSubmitWorkResult {
2491 pub mob_id: String,
2492 pub work_ref: String,
2493 pub member_ref: WireMemberRef,
2494 #[serde(default, skip_serializing_if = "Option::is_none")]
2495 pub objective_id: Option<String>,
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 MobConcludeObjectiveParams {
2503 pub member_ref: WireMemberRef,
2504 pub objective_id: String,
2505 pub outcome: String,
2506}
2507
2508#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
2509#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
2510pub struct MobConcludeObjectiveResult {
2511 pub member_ref: WireMemberRef,
2512 pub objective_id: String,
2513 pub concluded: bool,
2514}
2515
2516#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
2518#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
2519#[serde(deny_unknown_fields)]
2520pub struct MobCancelWorkParams {
2521 pub mob_id: String,
2522 pub work_ref: String,
2523}
2524
2525#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
2527#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
2528#[serde(deny_unknown_fields)]
2529pub struct MobCancelAllWorkParams {
2530 pub member_ref: WireMemberRef,
2531}
2532
2533#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq, Eq)]
2536#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
2537#[serde(deny_unknown_fields)]
2538pub struct MobMemberFilterWire {
2539 #[serde(default, skip_serializing_if = "BTreeMap::is_empty")]
2541 pub labels: BTreeMap<String, String>,
2542 #[serde(default, skip_serializing_if = "Option::is_none")]
2544 pub role: Option<String>,
2545 #[serde(default, skip_serializing_if = "Option::is_none")]
2547 pub status: Option<WireMobMemberStatus>,
2548}
2549
2550#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
2552#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
2553#[serde(deny_unknown_fields)]
2554pub struct MobListMembersMatchingParams {
2555 pub mob_id: String,
2556 #[serde(default)]
2557 pub filter: MobMemberFilterWire,
2558}
2559
2560#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq)]
2563#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
2564pub struct MobListMembersMatchingResult {
2565 #[serde(default)]
2566 pub members: Vec<Value>,
2567}
2568
2569#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq, Hash)]
2578#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
2579#[serde(rename_all = "snake_case")]
2580pub enum WireControlScope {
2581 List,
2582 ReadHistory,
2583 SubscribeEvents,
2584 SendCommand,
2585 Cancel,
2586 Retire,
2587 WireTopology,
2588 Live,
2589 AdminHost,
2590 AdminGrants,
2591}
2592
2593#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
2597#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
2598#[serde(rename_all = "snake_case")]
2599pub enum WireReachability {
2600 Reachable,
2601 Stale,
2602 Unreachable,
2603 Unknown,
2604}
2605
2606#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, Hash)]
2608#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
2609#[serde(transparent)]
2610pub struct WireHostRef(pub String);
2611
2612#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
2614#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
2615#[serde(deny_unknown_fields)]
2616pub struct WireMemberLifecycleCapabilities {
2617 pub transcript_edits: bool,
2618 pub revisions: bool,
2619 pub resume_after_restart: bool,
2620}
2621
2622#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
2624#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
2625#[serde(rename_all = "snake_case")]
2626pub enum WireHostBindPhase {
2627 Requested,
2628 Bound,
2629}
2630
2631#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
2640#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
2641#[serde(deny_unknown_fields)]
2642pub struct WireHostCapabilityFlags {
2643 pub protocol_min: u64,
2644 pub protocol_max: u64,
2645 pub engine_version: String,
2646 pub durable_sessions: bool,
2647 pub autonomous_members: bool,
2648 pub hard_cancel_member: bool,
2649 #[serde(default)]
2650 pub tracked_input_cancel: bool,
2651 pub memory_store: bool,
2652 pub mcp: bool,
2653 #[serde(default, skip_serializing_if = "std::collections::BTreeSet::is_empty")]
2654 pub resolvable_providers: std::collections::BTreeSet<String>,
2655 pub approval_forwarding: bool,
2656 #[serde(default, skip_serializing_if = "Option::is_none")]
2657 pub live_endpoint: Option<String>,
2658}
2659
2660#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
2672#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
2673pub struct MobHostStatus {
2674 pub host_id: WireHostRef,
2675 #[serde(default, skip_serializing_if = "Option::is_none")]
2676 pub endpoint: Option<String>,
2677 pub bind_phase: WireHostBindPhase,
2678 #[serde(default, skip_serializing_if = "Option::is_none")]
2679 pub authority_epoch: Option<u64>,
2680 #[serde(default, skip_serializing_if = "Option::is_none")]
2681 pub capabilities: Option<WireHostCapabilityFlags>,
2682 #[serde(default, skip_serializing_if = "Option::is_none")]
2683 pub control_reachability: Option<WireReachability>,
2684 #[serde(default, skip_serializing_if = "Option::is_none")]
2686 pub last_seen_ms: Option<u64>,
2687 #[serde(default, skip_serializing_if = "Option::is_none")]
2688 pub freshness_reason: Option<String>,
2689 pub materialized_member_count: u64,
2690}
2691
2692#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
2694#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
2695pub struct MobHostsResult {
2696 pub hosts: Vec<MobHostStatus>,
2697}
2698
2699#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
2701#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
2702pub struct WireRouteInstallObligation {
2703 pub edge_a: String,
2704 pub edge_b: String,
2705 pub host: WireHostRef,
2706}
2707
2708#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
2710#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
2711pub struct MobRouteInstallsResult {
2712 pub outstanding: Vec<WireRouteInstallObligation>,
2713 pub complete: bool,
2714}
2715
2716#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
2720#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
2721#[serde(rename_all = "snake_case")]
2722pub enum WireProjectionProvenance {
2723 HostClaimed,
2724 ControllingHostVerified,
2725}
2726
2727#[derive(Debug, Clone, Serialize, Deserialize)]
2736#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
2737#[serde(transparent)]
2738pub struct WireHistoryRow(pub super::session::WireSessionMessage);
2739
2740impl PartialEq for WireHistoryRow {
2741 fn eq(&self, other: &Self) -> bool {
2742 match (
2743 serde_json::to_value(&self.0),
2744 serde_json::to_value(&other.0),
2745 ) {
2746 (Ok(a), Ok(b)) => a == b,
2747 _ => false,
2751 }
2752 }
2753}
2754
2755impl Eq for WireHistoryRow {}
2756
2757#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
2761#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
2762#[serde(deny_unknown_fields)]
2763pub struct WireMemberHistoryPageBody {
2764 pub from_index: u64,
2765 pub messages: Vec<WireHistoryRow>,
2766 pub message_count: u64,
2769 #[serde(default, skip_serializing_if = "Option::is_none")]
2770 pub next_index: Option<u64>,
2771 pub complete: bool,
2772}
2773
2774impl WireMemberHistoryPageBody {
2775 pub fn try_from_history_page(
2780 page: &meerkat_core::service::SessionHistoryPage,
2781 ) -> Result<Self, super::error::WireConversionError> {
2782 let invalid =
2783 |reason: String| super::error::WireConversionError::MemberHistoryPage { debug: reason };
2784 let message_count = u64::try_from(page.message_count).map_err(|_| {
2785 invalid(format!(
2786 "message_count {} exceeds the u64 wire domain",
2787 page.message_count
2788 ))
2789 })?;
2790 let from_index = u64::try_from(page.offset).map_err(|_| {
2791 invalid(format!(
2792 "offset {} exceeds the u64 wire domain",
2793 page.offset
2794 ))
2795 })?;
2796 let served = u64::try_from(page.messages.len()).map_err(|_| {
2797 invalid(format!(
2798 "served row count {} exceeds the u64 wire domain",
2799 page.messages.len()
2800 ))
2801 })?;
2802 let next_index = if page.has_more {
2803 if served == 0 {
2804 return Err(invalid(format!(
2805 "page at offset {from_index} claims more rows but serves none"
2806 )));
2807 }
2808 Some(from_index.checked_add(served).ok_or_else(|| {
2809 invalid(format!(
2810 "offset {from_index} plus served row count {served} exhausts the u64 cursor domain"
2811 ))
2812 })?)
2813 } else {
2814 None
2815 };
2816 Ok(Self {
2817 from_index,
2818 messages: page
2819 .messages
2820 .iter()
2821 .map(|message| {
2822 WireHistoryRow(super::session::WireSessionMessage::from(message.clone()))
2823 })
2824 .collect(),
2825 message_count,
2826 next_index,
2827 complete: !page.has_more,
2828 })
2829 }
2830}
2831
2832#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
2834#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
2835#[serde(deny_unknown_fields)]
2836pub struct MobMemberHistoryParams {
2837 pub mob_id: String,
2838 pub agent_identity: String,
2839 #[serde(default, skip_serializing_if = "Option::is_none")]
2840 pub from_index: Option<u64>,
2841 #[serde(default, skip_serializing_if = "Option::is_none")]
2842 pub limit: Option<u32>,
2843}
2844
2845#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
2849#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
2850pub struct MobMemberHistoryResult {
2851 pub page: WireMemberHistoryPageBody,
2852 pub generation: u64,
2853 #[serde(default, skip_serializing_if = "Option::is_none")]
2854 pub placement: Option<WireHostRef>,
2855 pub provenance: WireProjectionProvenance,
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 MobBindHostParams {
2863 pub mob_id: String,
2864 pub descriptor: super::supervisor_bridge::WireHostBindingDescriptor,
2865}
2866
2867#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
2869#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
2870pub struct MobBindHostResult {
2871 pub host_id: WireHostRef,
2872 pub capabilities: WireHostCapabilityFlags,
2873 pub authority_epoch: u64,
2874}
2875
2876#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
2878#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
2879#[serde(deny_unknown_fields)]
2880pub struct MobRevokeHostParams {
2881 pub mob_id: String,
2882 pub host_id: WireHostRef,
2883}
2884
2885#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
2887#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
2888pub struct MobRevokeHostResult {
2889 pub host_id: WireHostRef,
2890 pub released_members: Vec<String>,
2893}
2894
2895#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
2897#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
2898pub struct WireGrantRecord {
2899 pub principal: String,
2900 pub scopes: Vec<WireControlScope>,
2901 #[serde(default, skip_serializing_if = "Option::is_none")]
2902 pub expires_at_ms: Option<u64>,
2903}
2904
2905#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
2907#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
2908#[serde(deny_unknown_fields)]
2909pub struct MobGrantScopesParams {
2910 pub mob_id: String,
2911 pub principal: String,
2912 pub scopes: Vec<WireControlScope>,
2913 #[serde(default, skip_serializing_if = "Option::is_none")]
2914 pub expires_at_ms: Option<u64>,
2915}
2916
2917#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
2919#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
2920pub struct MobGrantScopesResult {
2921 pub record: WireGrantRecord,
2922}
2923
2924#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
2927#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
2928#[serde(deny_unknown_fields)]
2929pub struct MobRevokeScopesParams {
2930 pub mob_id: String,
2931 pub principal: String,
2932 #[serde(default, skip_serializing_if = "Option::is_none")]
2933 pub scopes: Option<Vec<WireControlScope>>,
2934}
2935
2936#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
2938#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
2939pub struct MobRevokeScopesResult {
2940 pub removed: bool,
2941}
2942
2943#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
2945#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
2946pub struct MobGrantsResult {
2947 pub grants: Vec<WireGrantRecord>,
2948}
2949
2950#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
2957#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
2958#[serde(deny_unknown_fields)]
2959pub struct WireScopeDeniedDetail {
2960 pub required: WireControlScope,
2961 pub presented: Vec<WireControlScope>,
2962}
2963
2964#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
2967#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
2968#[serde(deny_unknown_fields)]
2969pub struct MobMemberLiveOpenParams {
2970 pub mob_id: String,
2971 pub agent_identity: String,
2972 #[serde(default, skip_serializing_if = "Option::is_none")]
2973 pub turning_mode: Option<super::realtime::RealtimeTurningMode>,
2974 #[serde(default, skip_serializing_if = "Option::is_none")]
2975 pub transport: Option<super::live::LiveOpenTransport>,
2976}
2977
2978#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
2984#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
2985#[serde(deny_unknown_fields)]
2986pub struct MobMemberLiveChannelParams {
2987 pub mob_id: String,
2988 pub agent_identity: String,
2989 pub channel_id: String,
2990}
2991
2992#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
2999#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
3000#[serde(deny_unknown_fields)]
3001pub struct MobMemberLiveStatusParams {
3002 pub mob_id: String,
3003 pub agent_identity: String,
3004 #[serde(default, skip_serializing_if = "Option::is_none")]
3005 pub channel_id: Option<String>,
3006}
3007
3008#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
3012#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
3013#[serde(deny_unknown_fields)]
3014pub struct MobHardCancelParams {
3015 pub mob_id: String,
3016 pub agent_identity: String,
3017 pub reason: String,
3018}
3019
3020#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
3024#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
3025pub struct MobHardCancelResult {
3026 pub cancelled: bool,
3027}
3028
3029#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
3031#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
3032#[serde(deny_unknown_fields)]
3033pub struct MobMemberLiveControlParams {
3034 pub mob_id: String,
3035 pub agent_identity: String,
3036 pub channel_id: String,
3037 pub verb: super::supervisor_bridge::BridgeLiveControlVerb,
3038}
3039
3040#[cfg(test)]
3041#[allow(clippy::expect_used, clippy::panic)]
3042mod tests {
3043 use super::*;
3044
3045 #[test]
3046 fn mob_helper_params_carry_structural_auth_binding() {
3047 let parsed: MobSpawnHelperParams = serde_json::from_value(serde_json::json!({
3048 "mob_id": "mob-1",
3049 "prompt": "help",
3050 "agent_identity": "helper",
3051 "auth_binding": {
3052 "realm": "dev",
3053 "binding": "default_anthropic",
3054 "profile": "console"
3055 }
3056 }))
3057 .expect("spawn helper params parse");
3058 let auth_binding = parsed.auth_binding.expect("auth_binding should parse");
3059 assert_eq!(auth_binding.realm.as_str(), "dev");
3060 assert_eq!(auth_binding.binding.as_str(), "default_anthropic");
3061 assert_eq!(
3062 auth_binding
3063 .profile
3064 .as_ref()
3065 .map(|profile| profile.as_str()),
3066 Some("console")
3067 );
3068
3069 let parsed: MobForkHelperParams = serde_json::from_value(serde_json::json!({
3070 "mob_id": "mob-1",
3071 "source_member_id": "source",
3072 "prompt": "help",
3073 "agent_identity": "helper",
3074 "auth_binding": {
3075 "realm": "dev",
3076 "binding": "default_anthropic"
3077 }
3078 }))
3079 .expect("fork helper params parse");
3080 let auth_binding = parsed.auth_binding.expect("auth_binding should parse");
3081 assert_eq!(auth_binding.realm.as_str(), "dev");
3082 assert_eq!(auth_binding.binding.as_str(), "default_anthropic");
3083 assert!(auth_binding.profile.is_none());
3084 }
3085
3086 #[test]
3087 fn wire_mob_profile_parses_provider_fields_fail_closed() {
3088 let legacy: WireMobProfile =
3090 serde_json::from_str(r#"{"model":"claude-opus-4-8"}"#).expect("legacy profile parses");
3091 assert_eq!(legacy.provider, None);
3092 assert!(legacy.resume_overrides.is_empty());
3093
3094 let full: WireMobProfile = serde_json::from_str(
3096 r#"{
3097 "model": "claude-internal-preview",
3098 "provider": "anthropic",
3099 "image_generation_provider": "gemini",
3100 "auto_compact_threshold": 60000,
3101 "resume_overrides": ["model", "provider"]
3102 }"#,
3103 )
3104 .expect("typed profile parses");
3105 assert_eq!(full.provider, Some(meerkat_core::Provider::Anthropic));
3106 assert_eq!(
3107 full.image_generation_provider,
3108 Some(meerkat_core::Provider::Gemini)
3109 );
3110 assert_eq!(
3111 full.resume_overrides,
3112 vec![
3113 WireMobResumeOverrideField::Model,
3114 WireMobResumeOverrideField::Provider
3115 ]
3116 );
3117
3118 assert!(
3120 serde_json::from_str::<WireMobProfile>(r#"{"model":"m","provider":"not-a-provider"}"#)
3121 .is_err(),
3122 "unknown provider names must fail closed at the wire boundary"
3123 );
3124 assert!(
3125 serde_json::from_str::<WireMobProfile>(r#"{"model":"m","auto_compact_threshold":0}"#)
3126 .is_err(),
3127 "zero auto_compact_threshold must fail closed at the wire boundary"
3128 );
3129 assert!(
3130 serde_json::from_str::<WireMobProfile>(
3131 r#"{"model":"m","resume_overrides":["everything"]}"#
3132 )
3133 .is_err(),
3134 "resume_overrides vocabulary is closed"
3135 );
3136 }
3137
3138 #[test]
3139 fn mob_definition_input_parses_custom_models() {
3140 let input: MobDefinitionInput = serde_json::from_str(
3141 r#"{
3142 "id": "m",
3143 "profiles": {"worker": {"model": "claude-internal-preview"}},
3144 "models": {
3145 "claude-internal-preview": {
3146 "provider": "anthropic",
3147 "context_window": 500000,
3148 "vision": true
3149 }
3150 },
3151 "image_generation_provider": "openai"
3152 }"#,
3153 )
3154 .expect("definition with custom models parses");
3155 let model = input
3156 .models
3157 .get("claude-internal-preview")
3158 .expect("custom model present");
3159 assert_eq!(model.provider, meerkat_core::Provider::Anthropic);
3160 assert_eq!(model.context_window, Some(500_000));
3161 assert_eq!(model.vision, Some(true));
3162 assert_eq!(
3163 input.image_generation_provider,
3164 Some(meerkat_core::Provider::OpenAI)
3165 );
3166 }
3167
3168 #[test]
3169 fn wire_member_ref_round_trips_through_encode_decode() {
3170 let token = WireMemberRef::encode("mob-42", "worker-1");
3171 let (mob_id, agent_identity) = token.decode().expect("decode round-trips");
3172 assert_eq!(mob_id, "mob-42");
3173 assert_eq!(agent_identity, "worker-1");
3174 }
3175
3176 #[test]
3177 fn wire_member_ref_rejects_malformed_token() {
3178 let err = WireMemberRef::from_token("not-a-token-payload")
3179 .decode()
3180 .expect_err("malformed tokens must fail to decode");
3181 assert!(matches!(err, WireMemberRefError::Malformed));
3182 }
3183
3184 #[test]
3185 fn mob_spawn_many_spec_placement_is_optional_and_round_trips() {
3186 let placed: MobSpawnSpecParams = serde_json::from_value(serde_json::json!({
3187 "profile": "worker",
3188 "agent_identity": "w1",
3189 "placement": "host-b-peer"
3190 }))
3191 .expect("placed spawn-many spec parses");
3192 assert_eq!(
3193 placed.placement.as_ref().map(|host| host.0.as_str()),
3194 Some("host-b-peer")
3195 );
3196 assert_eq!(
3197 serde_json::to_value(&placed).expect("placed spawn-many spec serializes")["placement"],
3198 "host-b-peer"
3199 );
3200
3201 let local: MobSpawnSpecParams = serde_json::from_value(serde_json::json!({
3202 "profile": "worker",
3203 "agent_identity": "w2"
3204 }))
3205 .expect("local spawn-many spec parses");
3206 assert!(local.placement.is_none());
3207 assert!(
3208 serde_json::to_value(&local)
3209 .expect("local spawn-many spec serializes")
3210 .get("placement")
3211 .is_none(),
3212 "absent placement must remain omitted for source and wire compatibility"
3213 );
3214 }
3215
3216 #[test]
3217 fn mob_member_spec_placement_is_optional_and_round_trips() {
3218 let placed: MobMemberSpecWire = serde_json::from_value(serde_json::json!({
3219 "profile": "worker",
3220 "agent_identity": "w1",
3221 "placement": "host-b-peer"
3222 }))
3223 .expect("placed declarative member spec parses");
3224 assert_eq!(
3225 placed.placement.as_ref().map(|host| host.0.as_str()),
3226 Some("host-b-peer")
3227 );
3228
3229 let local: MobMemberSpecWire = serde_json::from_value(serde_json::json!({
3230 "profile": "worker",
3231 "agent_identity": "w2"
3232 }))
3233 .expect("local declarative member spec parses");
3234 assert!(local.placement.is_none());
3235 }
3236
3237 #[test]
3238 fn mob_member_spec_exposes_shared_surface_metadata() {
3239 let spec = MobMemberSpecWire {
3240 profile: "worker".into(),
3241 agent_identity: "w1".into(),
3242 initial_message: None,
3243 runtime_mode: None,
3244 backend: None,
3245 placement: None,
3246 binding: None,
3247 context: Some(serde_json::json!({"client_ref": "member-card"})),
3248 labels: Some(BTreeMap::from([("client.member_id".into(), "w1".into())])),
3249 additional_instructions: None,
3250 auto_wire_parent: None,
3251 };
3252
3253 let metadata = spec.surface_metadata();
3254 assert_eq!(
3255 metadata.labels.get("client.member_id").map(String::as_str),
3256 Some("w1")
3257 );
3258 assert_eq!(
3259 metadata.app_context,
3260 Some(serde_json::json!({"client_ref": "member-card"}))
3261 );
3262 }
3263
3264 #[test]
3265 fn mob_member_spec_surface_metadata_rejects_reserved_keys() {
3266 let spec = MobMemberSpecWire {
3267 profile: "worker".into(),
3268 agent_identity: "w1".into(),
3269 initial_message: None,
3270 runtime_mode: None,
3271 backend: None,
3272 placement: None,
3273 binding: None,
3274 context: None,
3275 labels: Some(BTreeMap::from([("mob_id".into(), "spoof".into())])),
3276 additional_instructions: None,
3277 auto_wire_parent: None,
3278 };
3279
3280 assert!(spec.validate_public_surface_metadata().is_err());
3281 }
3282
3283 #[test]
3284 fn mob_reconcile_failure_stage_is_typed_wire_enum() {
3285 let failure = MobReconcileFailureWire {
3286 agent_identity: "worker-1".into(),
3287 stage: WireMobReconcileStage::Spawn,
3288 error: WireMobError {
3289 code: MobSpawnManyFailureCause::ProfileNotFound,
3290 message: "spawn failed".into(),
3291 },
3292 };
3293
3294 let json = serde_json::to_value(&failure).expect("serialize failure");
3295 assert_eq!(json["stage"], "spawn");
3296 assert_eq!(json["error"]["code"], "profile_not_found");
3297 assert_eq!(json["error"]["message"], "spawn failed");
3298
3299 let round_trip: MobReconcileFailureWire =
3300 serde_json::from_value(json).expect("deserialize failure");
3301 assert_eq!(round_trip.stage, WireMobReconcileStage::Spawn);
3302 assert_eq!(
3303 round_trip.error.code,
3304 MobSpawnManyFailureCause::ProfileNotFound
3305 );
3306
3307 let err = serde_json::from_value::<MobReconcileFailureWire>(serde_json::json!({
3308 "agent_identity": "worker-1",
3309 "stage": "restart",
3310 "error": { "code": "profile_not_found", "message": "bad stage" }
3311 }))
3312 .expect_err("unknown reconcile stage must be rejected");
3313 assert!(err.to_string().contains("unknown variant"));
3314 }
3315
3316 #[test]
3317 fn mob_lifecycle_params_reject_unknown_action_string() {
3318 let err = serde_json::from_value::<MobLifecycleParams>(serde_json::json!({
3319 "mob_id": "mob-1",
3320 "action": "explode"
3321 }))
3322 .expect_err("unknown lifecycle actions must fail at the typed wire boundary");
3323
3324 assert!(
3325 err.to_string().contains("unknown variant"),
3326 "unexpected error: {err}"
3327 );
3328 }
3329
3330 #[test]
3331 fn mob_lifecycle_result_round_trips_typed_action() {
3332 let result = MobLifecycleResult {
3333 mob_id: "mob-1".into(),
3334 action: WireMobLifecycleAction::Complete,
3335 ok: true,
3336 destroy_report: None,
3337 };
3338
3339 let json = serde_json::to_value(&result).expect("serialize lifecycle result");
3340 assert_eq!(json["action"], "complete");
3341
3342 let round_trip: MobLifecycleResult =
3343 serde_json::from_value(json).expect("deserialize lifecycle result");
3344 assert_eq!(round_trip.action, WireMobLifecycleAction::Complete);
3345 }
3346
3347 #[test]
3348 fn mob_wire_members_batch_contract_is_local_edge_native() {
3349 let params: MobWireMembersBatchParams = serde_json::from_value(serde_json::json!({
3350 "mob_id": "mob-1",
3351 "edges": [
3352 { "a": "lead", "b": "worker-b" },
3353 { "a": "worker-a", "b": "lead" }
3354 ]
3355 }))
3356 .expect("batch wire params deserialize");
3357
3358 assert_eq!(params.mob_id, "mob-1");
3359 assert_eq!(params.edges.len(), 2);
3360 assert_eq!(params.edges[0].a, "lead");
3361 assert_eq!(params.edges[0].b, "worker-b");
3362
3363 let result = MobWireMembersBatchResult {
3364 requested: 2,
3365 wired: vec![MobWireMembersBatchEdge {
3366 a: "lead".into(),
3367 b: "worker-a".into(),
3368 }],
3369 already_wired: vec![MobWireMembersBatchEdge {
3370 a: "lead".into(),
3371 b: "worker-b".into(),
3372 }],
3373 };
3374 let json = serde_json::to_value(&result).expect("serialize batch wire result");
3375 assert_eq!(json["requested"], 2);
3376 assert_eq!(json["wired"][0]["a"], "lead");
3377 assert_eq!(json["already_wired"][0]["b"], "worker-b");
3378
3379 let err = serde_json::from_value::<MobWireMembersBatchParams>(serde_json::json!({
3380 "mob_id": "mob-1",
3381 "edges": [{ "member": "lead", "peer": "worker-a" }]
3382 }))
3383 .expect_err("mixed local/external mob/wire shape must not deserialize");
3384 let message = err.to_string();
3385 assert!(
3386 message.contains("unknown field `member`") || message.contains("missing field `a`"),
3387 "unexpected error: {message}"
3388 );
3389 }
3390
3391 #[test]
3392 fn mob_spawn_many_result_entry_uses_typed_status_result_envelope() {
3393 let member_ref = WireMemberRef::encode("mob-1", "worker-1");
3394 let entry = MobSpawnManyResultEntry::spawned("worker-1", member_ref.clone());
3395
3396 let json = serde_json::to_value(&entry).expect("serialize typed spawn_many row");
3397 assert_eq!(json["status"], "spawned");
3398 assert_eq!(json["result"]["agent_identity"], "worker-1");
3399 assert_eq!(json["result"]["member_ref"], member_ref.as_str());
3400 assert!(json.get("ok").is_none());
3401 assert!(json.get("error").is_none());
3402
3403 let round_trip: MobSpawnManyResultEntry =
3404 serde_json::from_value(json).expect("deserialize typed spawn_many row");
3405 assert_eq!(round_trip, entry);
3406
3407 let failed = MobSpawnManyResultEntry::failed(
3408 MobSpawnManyFailureCause::ProfileNotFound,
3409 "profile missing",
3410 );
3411 let json = serde_json::to_value(&failed).expect("serialize typed failed spawn_many row");
3412 assert_eq!(json["status"], "failed");
3413 assert_eq!(json["result"]["cause"], "profile_not_found");
3414 assert_eq!(json["result"]["message"], "profile missing");
3415 assert!(json["result"].get("structured_data").is_none());
3416 assert!(json.get("ok").is_none());
3417 assert!(json.get("error").is_none());
3418
3419 let round_trip: MobSpawnManyResultEntry =
3420 serde_json::from_value(json).expect("deserialize typed failed spawn_many row");
3421 assert_eq!(round_trip, failed);
3422
3423 let failed_with_data = MobSpawnManyResultEntry::failed_with_structured_data(
3424 MobSpawnManyFailureCause::SessionError,
3425 "provider authentication failed",
3426 Some(serde_json::json!({
3427 "cause": "provider_auth",
3428 "kind": "interactive_login_required",
3429 "provider": "openai"
3430 })),
3431 );
3432 let json = serde_json::to_value(&failed_with_data).expect("serialize failed row with data");
3433 assert_eq!(json["result"]["structured_data"]["cause"], "provider_auth");
3434 let round_trip: MobSpawnManyResultEntry =
3435 serde_json::from_value(json).expect("deserialize failed row with data");
3436 assert_eq!(round_trip, failed_with_data);
3437
3438 let legacy_without_data = serde_json::json!({
3439 "status": "failed",
3440 "result": {
3441 "cause": "profile_not_found",
3442 "message": "profile missing"
3443 }
3444 });
3445 let decoded: MobSpawnManyResultEntry =
3446 serde_json::from_value(legacy_without_data).expect("read pre-data failed row");
3447 assert_eq!(decoded, failed);
3448 }
3449
3450 #[test]
3451 fn mob_spawn_many_result_entry_rejects_legacy_or_malformed_envelopes() {
3452 let legacy = serde_json::json!({
3453 "ok": true,
3454 "agent_identity": "worker-1",
3455 "member_ref": WireMemberRef::encode("mob-1", "worker-1"),
3456 });
3457 let err = serde_json::from_value::<MobSpawnManyResultEntry>(legacy)
3458 .expect_err("legacy ok carrier must not deserialize");
3459 assert!(
3460 err.to_string().contains("missing field `status`")
3461 || err.to_string().contains("unknown field"),
3462 "unexpected error: {err}"
3463 );
3464
3465 let missing_result = serde_json::json!({
3466 "status": "spawned"
3467 });
3468 let err = serde_json::from_value::<MobSpawnManyResultEntry>(missing_result)
3469 .expect_err("missing typed result must fail closed");
3470 assert!(
3471 err.to_string().contains("missing field `result`"),
3472 "unexpected error: {err}"
3473 );
3474
3475 let unknown_status = serde_json::json!({
3476 "status": "ok",
3477 "result": {
3478 "agent_identity": "worker-1",
3479 "member_ref": WireMemberRef::encode("mob-1", "worker-1"),
3480 }
3481 });
3482 let err = serde_json::from_value::<MobSpawnManyResultEntry>(unknown_status)
3483 .expect_err("unknown typed status must fail closed");
3484 assert!(
3485 err.to_string().contains("unknown variant"),
3486 "unexpected error: {err}"
3487 );
3488
3489 let mismatched = serde_json::json!({
3490 "status": "spawned",
3491 "result": {
3492 "cause": "profile_not_found",
3493 "message": "profile missing"
3494 }
3495 });
3496 let err = serde_json::from_value::<MobSpawnManyResultEntry>(mismatched)
3497 .expect_err("status/result mismatch must fail closed");
3498 assert!(
3499 err.to_string()
3500 .contains("status spawned requires spawned result"),
3501 "unexpected error: {err}"
3502 );
3503
3504 let message_only_failure = serde_json::json!({
3505 "status": "failed",
3506 "result": {
3507 "message": "profile missing"
3508 }
3509 });
3510 let err = serde_json::from_value::<MobSpawnManyResultEntry>(message_only_failure)
3511 .expect_err("string-only failure result must fail closed");
3512 assert!(
3513 err.to_string().contains("data did not match any variant")
3514 || err.to_string().contains("missing field `cause`"),
3515 "unexpected error: {err}"
3516 );
3517
3518 let unknown_failure_cause = serde_json::json!({
3519 "status": "failed",
3520 "result": {
3521 "cause": "future_failure",
3522 "message": "future failure"
3523 }
3524 });
3525 let err = serde_json::from_value::<MobSpawnManyResultEntry>(unknown_failure_cause)
3526 .expect_err("unknown failure cause must fail closed");
3527 assert!(
3528 err.to_string().contains("data did not match any variant")
3529 || err.to_string().contains("unknown variant"),
3530 "unexpected error: {err}"
3531 );
3532 }
3533
3534 #[test]
3535 fn mob_wire_params_reject_legacy_local_target_shape() {
3536 let err = serde_json::from_value::<MobWireParams>(serde_json::json!({
3537 "mob_id": "mob-1",
3538 "local": "member-a",
3539 "target": { "local": "member-b" }
3540 }))
3541 .expect_err("legacy local/target shape must be rejected");
3542
3543 let msg = err.to_string();
3544 assert!(
3545 msg.contains("unknown field `local`") || msg.contains("missing field `member`"),
3546 "unexpected error: {msg}"
3547 );
3548 }
3549
3550 #[test]
3551 fn mob_wire_params_accept_canonical_external_peer_identity() {
3552 let params = serde_json::from_value::<MobWireParams>(serde_json::json!({
3553 "mob_id": "mob-1",
3554 "member": "member-a",
3555 "peer": {
3556 "external": {
3557 "name": "external-worker",
3558 "address": "inproc://external-worker",
3559 "identity": {
3560 "kind": "ed25519_public_key",
3561 "public_key": "ed25519:BwcHBwcHBwcHBwcHBwcHBwcHBwcHBwcHBwcHBwcHBwc="
3562 }
3563 }
3564 }
3565 }))
3566 .expect("canonical external peer identity should deserialize");
3567
3568 let MobPeerTarget::External(spec) = params.peer else {
3569 panic!("expected external peer target");
3570 };
3571 assert_eq!(spec.name, "external-worker");
3572 }
3573
3574 #[test]
3575 fn mob_wire_params_reject_raw_external_peer_id_shape() {
3576 let err = serde_json::from_value::<MobWireParams>(serde_json::json!({
3577 "mob_id": "mob-1",
3578 "member": "member-a",
3579 "peer": {
3580 "external": {
3581 "name": "external-worker",
3582 "peer_id": meerkat_core::comms::PeerId::from_ed25519_pubkey(&[7u8; 32]).to_string(),
3583 "address": "inproc://external-worker",
3584 "pubkey": vec![7u8; 32]
3585 }
3586 }
3587 }))
3588 .expect_err("raw peer_id/pubkey external peer shape must be rejected");
3589
3590 let msg = err.to_string();
3591 assert!(
3592 msg.contains("peer_id") || msg.contains("identity"),
3593 "unexpected error: {msg}"
3594 );
3595 }
3596
3597 #[test]
3598 fn mob_wire_params_reject_missing_external_peer_pubkey_material() {
3599 let err = serde_json::from_value::<MobWireParams>(serde_json::json!({
3600 "mob_id": "mob-1",
3601 "member": "member-a",
3602 "peer": {
3603 "external": {
3604 "name": "external-worker",
3605 "address": "inproc://external-worker",
3606 "identity": {
3607 "kind": "ed25519_public_key"
3608 }
3609 }
3610 }
3611 }))
3612 .expect_err("missing external peer pubkey material must fail closed");
3613
3614 let msg = err.to_string();
3615 assert!(
3616 msg.contains("public_key") || msg.contains("identity"),
3617 "unexpected error: {msg}"
3618 );
3619 }
3620
3621 #[test]
3622 fn runtime_binding_accepts_canonical_external_peer_identity() {
3623 let binding = serde_json::from_value::<WireRuntimeBinding>(serde_json::json!({
3624 "kind": "external",
3625 "address": "inproc://external-worker",
3626 "identity": {
3627 "kind": "ed25519_public_key",
3628 "public_key": "ed25519:BwcHBwcHBwcHBwcHBwcHBwcHBwcHBwcHBwcHBwcHBwc="
3629 }
3630 }))
3631 .expect("canonical external runtime binding identity should deserialize");
3632
3633 let WireRuntimeBinding::External {
3634 identity, address, ..
3635 } = binding
3636 else {
3637 panic!("expected external runtime binding");
3638 };
3639 assert_eq!(address, "inproc://external-worker");
3640 assert_eq!(
3641 identity.resolve().expect("identity resolves").pubkey,
3642 [7u8; 32]
3643 );
3644 }
3645
3646 #[test]
3647 fn runtime_binding_rejects_raw_external_peer_id_shape() {
3648 let err = serde_json::from_value::<WireRuntimeBinding>(serde_json::json!({
3649 "kind": "external",
3650 "peer_id": meerkat_core::comms::PeerId::from_ed25519_pubkey(&[7u8; 32]).to_string(),
3651 "address": "inproc://external-worker",
3652 "pubkey": vec![7u8; 32]
3653 }))
3654 .expect_err("raw peer_id/pubkey external runtime binding shape must be rejected");
3655
3656 let msg = err.to_string();
3657 assert!(
3658 msg.contains("peer_id") || msg.contains("identity"),
3659 "unexpected error: {msg}"
3660 );
3661 }
3662
3663 #[test]
3664 fn runtime_binding_rejects_missing_external_peer_pubkey_material() {
3665 let err = serde_json::from_value::<WireRuntimeBinding>(serde_json::json!({
3666 "kind": "external",
3667 "address": "inproc://external-worker",
3668 "identity": {
3669 "kind": "ed25519_public_key"
3670 }
3671 }))
3672 .expect_err("missing external runtime binding pubkey material must fail closed");
3673
3674 let msg = err.to_string();
3675 assert!(
3676 msg.contains("public_key") || msg.contains("identity"),
3677 "unexpected error: {msg}"
3678 );
3679 }
3680
3681 #[test]
3682 fn mob_turn_start_params_capture_turn_override_fields() {
3683 let params = serde_json::from_value::<MobTurnStartParams>(serde_json::json!({
3684 "mob_id": "mob-1",
3685 "agent_identity": "worker",
3686 "prompt": "continue",
3687 "output_schema": { "type": "object" },
3688 "structured_output_retries": 2
3689 }))
3690 .expect("turn_start should accept explicit turn override fields");
3691
3692 assert_eq!(params.mob_id, "mob-1");
3693 assert_eq!(params.agent_identity, "worker");
3694 assert_eq!(params.prompt, WireContentInput::Text("continue".into()));
3695 assert_eq!(
3696 params.output_schema,
3697 Some(serde_json::json!({ "type": "object" }))
3698 );
3699 assert_eq!(params.structured_output_retries, Some(2));
3700
3701 let err = serde_json::from_value::<MobTurnStartParams>(serde_json::json!({
3702 "mob_id": "mob-1",
3703 "agent_identity": "worker",
3704 "prompt": "continue",
3705 "unknown_override": true
3706 }))
3707 .expect_err("turn_start must reject unknown override fields");
3708 assert!(
3709 err.to_string().contains("unknown field"),
3710 "unexpected error: {err}"
3711 );
3712 }
3713
3714 #[test]
3715 fn mob_create_params_reject_reserved_runtime_lifecycle_fields() {
3716 let err = serde_json::from_value::<MobCreateParams>(serde_json::json!({
3717 "definition": {
3718 "id": "mob-1",
3719 "owner_runtime_binding": "runtime:worker:0",
3720 "profiles": {
3721 "worker": { "model": "claude-sonnet-4-6" }
3722 }
3723 }
3724 }))
3725 .expect_err("reserved runtime lifecycle fields must be rejected");
3726
3727 assert!(
3728 err.to_string()
3729 .contains("unknown field `owner_runtime_binding`"),
3730 "unexpected error: {err}"
3731 );
3732 }
3733
3734 #[test]
3735 fn mob_create_params_reject_reserved_runtime_bridge_owner_field() {
3736 let err = serde_json::from_value::<MobCreateParams>(serde_json::json!({
3737 "definition": {
3738 "id": "mob-1",
3739 "owner_transport_binding": "transport:worker:0",
3740 "profiles": {
3741 "worker": { "model": "claude-sonnet-4-6" }
3742 }
3743 }
3744 }))
3745 .expect_err("reserved runtime bridge owner field must be rejected");
3746
3747 assert!(
3748 err.to_string()
3749 .contains("unknown field `owner_transport_binding`"),
3750 "unexpected error: {err}"
3751 );
3752 }
3753
3754 #[test]
3755 fn mob_create_params_reject_internal_profile_tool_bundles() {
3756 let err = serde_json::from_value::<MobCreateParams>(serde_json::json!({
3757 "definition": {
3758 "id": "mob-1",
3759 "profiles": {
3760 "worker": {
3761 "model": "claude-sonnet-4-6",
3762 "tools": {
3763 "rust_bundles": ["internal-only"]
3764 }
3765 }
3766 }
3767 }
3768 }))
3769 .expect_err("internal rust tool bundles must be rejected");
3770
3771 assert!(
3774 err.to_string().contains("did not match any variant")
3775 || err.to_string().contains("unknown field `rust_bundles`"),
3776 "unexpected error: {err}"
3777 );
3778 }
3779
3780 #[test]
3781 fn mob_create_params_accept_typed_nested_flow_definition() {
3782 let params = serde_json::from_value::<MobCreateParams>(serde_json::json!({
3783 "definition": {
3784 "id": "mob-1",
3785 "profiles": {
3786 "worker": { "model": "claude-sonnet-4-6" }
3787 },
3788 "flows": {
3789 "review": {
3790 "description": "review flow",
3791 "steps": {
3792 "draft": {
3793 "role": "worker",
3794 "message": "draft it"
3795 }
3796 }
3797 }
3798 }
3799 }
3800 }))
3801 .expect("typed nested flow definition should parse");
3802
3803 assert_eq!(
3804 params.definition.flows["review"].steps["draft"].role,
3805 "worker"
3806 );
3807 }
3808
3809 #[test]
3814 fn mob_spawn_params_reject_deleted_budget_split_policy() {
3815 let err = serde_json::from_value::<MobSpawnParams>(serde_json::json!({
3816 "mob_id": "mob-1",
3817 "profile": "worker",
3818 "agent_identity": "worker-1",
3819 "budget_split_policy": { "type": "equal" }
3820 }))
3821 .expect_err("deleted budget_split_policy must fail closed at the wire boundary");
3822 assert!(
3823 err.to_string()
3824 .contains("unknown field `budget_split_policy`"),
3825 "unexpected error: {err}"
3826 );
3827 }
3828
3829 #[test]
3833 fn mob_spawn_params_placement_round_trips_and_defaults_absent() {
3834 let params = serde_json::from_value::<MobSpawnParams>(serde_json::json!({
3835 "mob_id": "mob-1",
3836 "profile": "worker",
3837 "agent_identity": "worker-1"
3838 }))
3839 .expect("placement-less params must decode");
3840 assert_eq!(params.placement, None);
3841 let encoded = serde_json::to_value(¶ms).expect("serialize params");
3842 assert!(
3843 encoded.get("placement").is_none(),
3844 "absent placement must not serialize: {encoded}"
3845 );
3846
3847 let params = serde_json::from_value::<MobSpawnParams>(serde_json::json!({
3848 "mob_id": "mob-1",
3849 "profile": "worker",
3850 "agent_identity": "worker-1",
3851 "placement": "host-peer-b"
3852 }))
3853 .expect("placed params must decode");
3854 assert_eq!(params.placement.as_deref(), Some("host-peer-b"));
3855 let encoded = serde_json::to_value(¶ms).expect("serialize params");
3856 assert_eq!(encoded["placement"], serde_json::json!("host-peer-b"));
3857 }
3858
3859 fn minimal_member_status() -> MobMemberStatusResult {
3860 MobMemberStatusResult {
3861 status: WireMobMemberStatus::Active,
3862 member_ref: WireMemberRef::encode("mob-1", "worker-1"),
3863 output_preview: None,
3864 error: None,
3865 tokens_used: 0,
3866 is_final: false,
3867 current_session_id: None,
3868 peer_connectivity: None,
3869 kickoff: None,
3870 external_member: None,
3871 resolved_capabilities: None,
3872 progress: None,
3873 activity: None,
3874 detached_jobs: None,
3875 placement: None,
3876 control_reachability: None,
3877 comms_reachability: None,
3878 last_seen_ms: None,
3879 freshness_reason: None,
3880 lifecycle_capabilities: None,
3881 non_portable_disabled: None,
3882 }
3883 }
3884
3885 #[test]
3888 fn member_status_multi_host_fields_skip_when_absent_and_decode_legacy() {
3889 let value = serde_json::to_value(minimal_member_status()).expect("serialize member status");
3890 for absent in [
3891 "placement",
3892 "control_reachability",
3893 "comms_reachability",
3894 "last_seen_ms",
3895 "freshness_reason",
3896 "lifecycle_capabilities",
3897 "non_portable_disabled",
3898 ] {
3899 assert!(
3900 value.get(absent).is_none(),
3901 "absent {absent} must be omitted from the wire form: {value}"
3902 );
3903 }
3904
3905 let legacy = serde_json::json!({
3907 "status": "active",
3908 "member_ref": WireMemberRef::encode("mob-1", "worker-1"),
3909 "tokens_used": 3,
3910 "is_final": false,
3911 });
3912 let decoded: MobMemberStatusResult =
3913 serde_json::from_value(legacy).expect("legacy member status decodes");
3914 assert!(decoded.placement.is_none());
3915 assert!(decoded.lifecycle_capabilities.is_none());
3916 }
3917
3918 #[test]
3921 fn member_status_carries_placement_only_at_typed_keys() {
3922 let mut status = minimal_member_status();
3923 status.placement = Some(WireHostRef("host-b-peer".to_string()));
3924 status.control_reachability = Some(WireReachability::Stale);
3925 status.comms_reachability = Some(WireReachability::Reachable);
3926 status.last_seen_ms = Some(1_234);
3927 status.freshness_reason = Some("pump idle".to_string());
3928 status.lifecycle_capabilities = Some(WireMemberLifecycleCapabilities {
3929 transcript_edits: false,
3930 revisions: false,
3931 resume_after_restart: true,
3932 });
3933 status.non_portable_disabled = Some(vec![
3934 super::super::portable_spec::WireNonPortableResourceKind::WorkgraphTools,
3935 ]);
3936 status.external_member = Some(serde_json::json!({"endpoint": "tcp://10.0.0.2:7101"}));
3937
3938 let value = serde_json::to_value(&status).expect("serialize member status");
3939 assert_eq!(value["placement"], serde_json::json!("host-b-peer"));
3940 assert_eq!(value["control_reachability"], serde_json::json!("stale"));
3941 assert_eq!(
3942 value["non_portable_disabled"],
3943 serde_json::json!(["workgraph_tools"])
3944 );
3945 assert!(
3946 value["external_member"].get("placement").is_none(),
3947 "placement must not ride the opaque external_member value (SD-5)"
3948 );
3949
3950 let decoded: MobMemberStatusResult =
3951 serde_json::from_value(value).expect("decode member status");
3952 assert_eq!(decoded.placement, status.placement);
3953 assert_eq!(decoded.control_reachability, status.control_reachability);
3954 }
3955
3956 #[test]
3957 fn control_scope_and_reachability_round_trip_snake_case() {
3958 let scopes: &[(WireControlScope, &str)] = &[
3959 (WireControlScope::List, "list"),
3960 (WireControlScope::ReadHistory, "read_history"),
3961 (WireControlScope::SubscribeEvents, "subscribe_events"),
3962 (WireControlScope::SendCommand, "send_command"),
3963 (WireControlScope::Cancel, "cancel"),
3964 (WireControlScope::Retire, "retire"),
3965 (WireControlScope::WireTopology, "wire_topology"),
3966 (WireControlScope::Live, "live"),
3967 (WireControlScope::AdminHost, "admin_host"),
3968 (WireControlScope::AdminGrants, "admin_grants"),
3969 ];
3970 for (scope, expected) in scopes {
3971 let value = serde_json::to_value(scope).expect("serialize scope");
3972 assert_eq!(value, serde_json::json!(expected));
3973 let decoded: WireControlScope = serde_json::from_value(value).expect("decode scope");
3974 assert_eq!(decoded, *scope);
3975 }
3976 assert!(
3977 serde_json::from_value::<WireControlScope>(serde_json::json!("admin")).is_err(),
3978 "unknown scopes must fail decode (closed vocabulary)"
3979 );
3980
3981 let classes: &[(WireReachability, &str)] = &[
3982 (WireReachability::Reachable, "reachable"),
3983 (WireReachability::Stale, "stale"),
3984 (WireReachability::Unreachable, "unreachable"),
3985 (WireReachability::Unknown, "unknown"),
3986 ];
3987 for (class, expected) in classes {
3988 let value = serde_json::to_value(class).expect("serialize reachability");
3989 assert_eq!(value, serde_json::json!(expected));
3990 let decoded: WireReachability =
3991 serde_json::from_value(value).expect("decode reachability");
3992 assert_eq!(decoded, *class);
3993 }
3994 }
3995
3996 #[test]
3997 fn scope_denied_detail_round_trips_snake_case_and_denies_unknown_fields() {
3998 let detail = WireScopeDeniedDetail {
3999 required: WireControlScope::AdminGrants,
4000 presented: vec![WireControlScope::List, WireControlScope::SendCommand],
4001 };
4002 let value = serde_json::to_value(&detail).expect("serialize detail");
4003 assert_eq!(
4004 value,
4005 serde_json::json!({
4006 "required": "admin_grants",
4007 "presented": ["list", "send_command"],
4008 })
4009 );
4010 let decoded: WireScopeDeniedDetail = serde_json::from_value(value).expect("decode detail");
4011 assert_eq!(decoded, detail);
4012
4013 assert!(
4014 serde_json::from_value::<WireScopeDeniedDetail>(serde_json::json!({
4015 "required": "admin_grants",
4016 "presented": [],
4017 "reason": "extra",
4018 }))
4019 .is_err(),
4020 "unknown fields must be rejected (deny_unknown_fields)"
4021 );
4022 }
4023
4024 #[test]
4025 fn host_status_and_grants_round_trip() {
4026 let host = MobHostStatus {
4027 host_id: WireHostRef("host-b-peer".to_string()),
4028 endpoint: Some("tcp://10.0.0.2:7100".to_string()),
4029 bind_phase: WireHostBindPhase::Bound,
4030 authority_epoch: Some(4),
4031 capabilities: Some(WireHostCapabilityFlags {
4032 protocol_min: 2,
4033 protocol_max: 4,
4034 engine_version: "0.7.22".to_string(),
4035 durable_sessions: true,
4036 autonomous_members: true,
4037 hard_cancel_member: false,
4038 tracked_input_cancel: false,
4039 memory_store: false,
4040 mcp: true,
4041 resolvable_providers: std::collections::BTreeSet::from(["anthropic".to_string()]),
4042 approval_forwarding: false,
4043 live_endpoint: None,
4044 }),
4045 control_reachability: Some(WireReachability::Reachable),
4046 last_seen_ms: Some(250),
4047 freshness_reason: None,
4048 materialized_member_count: 2,
4049 };
4050 let result = MobHostsResult { hosts: vec![host] };
4051 let value = serde_json::to_value(&result).expect("serialize hosts");
4052 assert_eq!(value["hosts"][0]["bind_phase"], serde_json::json!("bound"));
4053 let decoded: MobHostsResult = serde_json::from_value(value).expect("decode hosts");
4054 assert_eq!(decoded, result);
4055
4056 let requested = MobHostStatus {
4059 host_id: WireHostRef("host-c-peer".to_string()),
4060 endpoint: None,
4061 bind_phase: WireHostBindPhase::Requested,
4062 authority_epoch: None,
4063 capabilities: None,
4064 control_reachability: None,
4065 last_seen_ms: None,
4066 freshness_reason: None,
4067 materialized_member_count: 0,
4068 };
4069 let value = serde_json::to_value(&requested).expect("serialize requested host");
4070 assert_eq!(value["bind_phase"], serde_json::json!("requested"));
4071 assert!(value.get("endpoint").is_none());
4072 assert!(value.get("authority_epoch").is_none());
4073 assert!(value.get("capabilities").is_none());
4074 let decoded: MobHostStatus = serde_json::from_value(value).expect("decode requested host");
4075 assert_eq!(decoded, requested);
4076
4077 let record = WireGrantRecord {
4078 principal: "console:luka".to_string(),
4079 scopes: vec![WireControlScope::List, WireControlScope::Live],
4080 expires_at_ms: None,
4081 };
4082 let value = serde_json::to_value(&record).expect("serialize grant");
4083 assert!(
4084 value.get("expires_at_ms").is_none(),
4085 "absent expiry must be omitted"
4086 );
4087 let decoded: WireGrantRecord = serde_json::from_value(value).expect("decode grant");
4088 assert_eq!(decoded, record);
4089 }
4090
4091 #[test]
4092 fn member_history_result_round_trips_with_provenance() {
4093 let result = MobMemberHistoryResult {
4094 page: WireMemberHistoryPageBody {
4095 from_index: 5,
4096 messages: Vec::new(),
4097 message_count: 12,
4098 next_index: Some(10),
4099 complete: false,
4100 },
4101 generation: 2,
4102 placement: Some(WireHostRef("host-b-peer".to_string())),
4103 provenance: WireProjectionProvenance::HostClaimed,
4104 };
4105 let value = serde_json::to_value(&result).expect("serialize history result");
4106 assert_eq!(value["provenance"], serde_json::json!("host_claimed"));
4107 assert_eq!(value["page"]["message_count"], serde_json::json!(12));
4108 let decoded: MobMemberHistoryResult =
4109 serde_json::from_value(value.clone()).expect("decode history result");
4110 let reencoded = serde_json::to_value(&decoded).expect("reserialize history result");
4111 assert_eq!(value, reencoded);
4112 }
4113
4114 #[test]
4115 fn member_history_projection_rejects_non_advancing_page() {
4116 let page = meerkat_core::service::SessionHistoryPage {
4117 session_id: meerkat_core::SessionId::new(),
4118 message_count: 1,
4119 offset: 0,
4120 limit: Some(1),
4121 has_more: true,
4122 messages: Vec::new(),
4123 };
4124 let error = WireMemberHistoryPageBody::try_from_history_page(&page)
4125 .expect_err("a page that claims more rows must advance its cursor");
4126 assert!(matches!(
4127 error,
4128 crate::wire::error::WireConversionError::MemberHistoryPage { debug }
4129 if debug.contains("serves none")
4130 ));
4131 }
4132
4133 #[cfg(target_pointer_width = "64")]
4134 #[test]
4135 fn member_history_projection_rejects_exhausted_cursor() {
4136 let page = meerkat_core::service::SessionHistoryPage {
4137 session_id: meerkat_core::SessionId::new(),
4138 message_count: usize::MAX,
4139 offset: usize::MAX,
4140 limit: Some(2),
4141 has_more: true,
4142 messages: vec![
4143 meerkat_core::types::Message::User(meerkat_core::types::UserMessage::text("first")),
4144 meerkat_core::types::Message::User(meerkat_core::types::UserMessage::text(
4145 "second",
4146 )),
4147 ],
4148 };
4149 let error = WireMemberHistoryPageBody::try_from_history_page(&page)
4150 .expect_err("MAX has no representable member-history successor");
4151 assert!(matches!(
4152 error,
4153 crate::wire::error::WireConversionError::MemberHistoryPage { debug }
4154 if debug.contains("exhausts the u64 cursor domain")
4155 ));
4156 }
4157
4158 #[test]
4163 fn hard_cancel_params_round_trip_and_reject_unknown_fields() {
4164 let params = MobHardCancelParams {
4165 mob_id: "mob-1".to_string(),
4166 agent_identity: "worker".to_string(),
4167 reason: "operator interrupt".to_string(),
4168 };
4169 let value = serde_json::to_value(¶ms).expect("serialize hard-cancel params");
4170 let decoded: MobHardCancelParams =
4171 serde_json::from_value(value).expect("decode hard-cancel params");
4172 assert_eq!(decoded, params);
4173
4174 serde_json::from_value::<MobHardCancelParams>(serde_json::json!({
4175 "mob_id": "mob-1",
4176 "agent_identity": "worker",
4177 "reason": "x",
4178 "force": true,
4179 }))
4180 .expect_err("unknown field must be rejected");
4181
4182 serde_json::from_value::<MobHardCancelParams>(serde_json::json!({
4185 "mob_id": "mob-1",
4186 "agent_identity": "worker",
4187 }))
4188 .expect_err("missing reason must be rejected");
4189
4190 let result = MobHardCancelResult { cancelled: true };
4191 let value = serde_json::to_value(&result).expect("serialize hard-cancel result");
4192 assert_eq!(value, serde_json::json!({ "cancelled": true }));
4193 }
4194
4195 #[test]
4196 fn member_live_status_params_keep_the_discovery_read() {
4197 let discovery: MobMemberLiveStatusParams = serde_json::from_value(serde_json::json!({
4200 "mob_id": "mob-1",
4201 "agent_identity": "worker",
4202 }))
4203 .expect("discovery status params parse");
4204 assert_eq!(discovery.channel_id, None);
4205 let value = serde_json::to_value(&discovery).expect("serialize discovery params");
4206 assert!(
4207 value.get("channel_id").is_none(),
4208 "absent channel_id must be omitted"
4209 );
4210
4211 let named: MobMemberLiveStatusParams = serde_json::from_value(serde_json::json!({
4212 "mob_id": "mob-1",
4213 "agent_identity": "worker",
4214 "channel_id": "chan-7",
4215 }))
4216 .expect("named status params parse");
4217 assert_eq!(named.channel_id.as_deref(), Some("chan-7"));
4218
4219 serde_json::from_value::<MobMemberLiveStatusParams>(serde_json::json!({
4220 "mob_id": "mob-1",
4221 "agent_identity": "worker",
4222 "chan": "chan-7",
4223 }))
4224 .expect_err("unknown field must be rejected");
4225
4226 serde_json::from_value::<MobMemberLiveChannelParams>(serde_json::json!({
4228 "mob_id": "mob-1",
4229 "agent_identity": "worker",
4230 }))
4231 .expect_err("close without channel_id must be rejected");
4232 }
4233}