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