objectiveai_sdk/client_objectiveai_mcp/server_request/payload.rs
1use indexmap::IndexMap;
2use schemars::JsonSchema;
3use serde::{Deserialize, Serialize};
4
5/// Tagged union of every JSON-RPC request the API forwards to the
6/// client over the reverse-attach WS. Variant names follow the same
7/// snake_case convention `client_request::Payload` uses; the
8/// `serde(tag = "type")` discriminator pairs with
9/// [`super::super::server_response::Payload`] by name.
10///
11/// MCP-routed variants carry `mcp_kind` directly on the variant
12/// (alongside the typed params via `#[serde(flatten)]`). The non-MCP
13/// `ReadMessageQueue` variant doesn't carry `mcp_kind` at all — it
14/// hits the CLI's own local state and never routes to an upstream
15/// MCP server. Use [`Payload::mcp_kind`] to retrieve it generically.
16#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
17#[schemars(rename = "client_objectiveai_mcp.server_request.Payload")]
18#[serde(tag = "type", rename_all = "snake_case")]
19pub enum Payload {
20 /// POST `initialize`. The proxy's `protocolVersion` doesn't ride
21 /// across this hop — the API discards it on the way in and
22 /// substitutes its own `canonical_initialize_result` on the way
23 /// out. The variant carries the plugin arguments the CLI needs at
24 /// dial time (parsed by the API off the URL query string).
25 #[schemars(title = "Initialize")]
26 Initialize {
27 mcp_kind: super::super::McpKind,
28 #[serde(flatten)]
29 params: InitializeRequest,
30 },
31
32 /// POST `tools/list`.
33 #[schemars(title = "ToolsList")]
34 ToolsList {
35 mcp_kind: super::super::McpKind,
36 #[serde(flatten)]
37 params: crate::mcp::tool::ListToolsRequest,
38 },
39
40 /// POST `tools/call`.
41 #[schemars(title = "ToolsCall")]
42 ToolsCall {
43 mcp_kind: super::super::McpKind,
44 #[serde(flatten)]
45 params: crate::mcp::tool::CallToolRequestParams,
46 },
47
48 /// POST `resources/list`.
49 #[schemars(title = "ResourcesList")]
50 ResourcesList {
51 mcp_kind: super::super::McpKind,
52 #[serde(flatten)]
53 params: crate::mcp::resource::ListResourcesRequest,
54 },
55
56 /// POST `resources/read`.
57 #[schemars(title = "ResourcesRead")]
58 ResourcesRead {
59 mcp_kind: super::super::McpKind,
60 #[serde(flatten)]
61 params: crate::mcp::resource::ReadResourceRequestParams,
62 },
63
64 /// `DELETE` on the routed MCP URL — the proxy closing the
65 /// session. No body beyond `mcp_kind`.
66 #[schemars(title = "SessionTerminate")]
67 SessionTerminate { mcp_kind: super::super::McpKind },
68
69 /// Read the CLI's local message queue for a given agent
70 /// hierarchy. Non-MCP — no `mcp_kind`. Non-destructive: the
71 /// API stamps the consumed ids onto the first
72 /// `AssistantResponseChunk.request_message_ids` it emits so
73 /// the downstream consumer owns row deletion; no separate
74 /// release RPC.
75 #[schemars(title = "ReadMessageQueue")]
76 ReadMessageQueue(ReadMessageQueueRequest),
77
78 /// Resolve a `Client` remote from the client's own local storage
79 /// (agent / swarm / function / profile). Non-MCP — no `mcp_kind`.
80 #[schemars(title = "Retrieve")]
81 Retrieve(super::super::retrieve::Request),
82
83 /// Run a SCRIPT agent's code on the client and return its output
84 /// messages. Non-MCP — no `mcp_kind`: the CLI executes the code
85 /// in-process on its embedded runtime (the same one the `python`
86 /// command uses), with the FULL conversation as the script input.
87 #[schemars(title = "Script")]
88 Script(ScriptRequest),
89
90 /// Forceful bulk teardown of every upstream connection for one
91 /// objectiveai response id. Non-MCP — no `mcp_kind` (it spans all
92 /// kinds for that response id). The CLI removes the whole
93 /// response-id bucket from its connection registry, which drops
94 /// every connection and kills every plugin subprocess under it.
95 /// Distinct from `SessionTerminate` (graceful per-`mcp_kind` MCP
96 /// `DELETE`); `Drop` is drop = kill.
97 #[schemars(title = "Drop")]
98 Drop(DropRequest),
99
100 /// Begin streaming a tar export OUT of one laboratory on this
101 /// conduit. Non-MCP — carries the laboratory id directly. The
102 /// conduit opens the laboratory's `/export` and parks the byte
103 /// stream under a fresh `transfer_id`; the requester then PULLS
104 /// chunks with [`Payload::LaboratoryExportRead`]. Fully
105 /// independent of any import — the splice (if any) happens on
106 /// the requester's (API/proxy) side, so the peer laboratory may
107 /// live on any host.
108 #[schemars(title = "LaboratoryExportBegin")]
109 LaboratoryExportBegin(LaboratoryExportBeginRequest),
110 /// Pull the next chunk of a parked export stream. `eof: true` on
111 /// the reply means the stream completed and the entry is gone
112 /// (the final reply's `data` may still be non-empty).
113 #[schemars(title = "LaboratoryExportRead")]
114 LaboratoryExportRead(LaboratoryExportReadRequest),
115 /// Drop a parked export stream early (requester-side failure).
116 #[schemars(title = "LaboratoryExportAbort")]
117 LaboratoryExportAbort(LaboratoryExportAbortRequest),
118 /// Begin streaming a tar import INTO one laboratory on this
119 /// conduit: the conduit opens the laboratory's `/import` with a
120 /// channel-backed body and parks the sender under a fresh
121 /// `transfer_id`; the requester then PUSHES chunks with
122 /// [`Payload::LaboratoryImportWrite`] and closes with
123 /// [`Payload::LaboratoryImportEnd`]. Independent of any export.
124 #[schemars(title = "LaboratoryImportBegin")]
125 LaboratoryImportBegin(LaboratoryImportBeginRequest),
126 /// Push one chunk into a parked import body.
127 #[schemars(title = "LaboratoryImportWrite")]
128 LaboratoryImportWrite(LaboratoryImportWriteRequest),
129 /// Close a parked import body and await the laboratory's unpack
130 /// result. Replies with the total bytes fed.
131 #[schemars(title = "LaboratoryImportEnd")]
132 LaboratoryImportEnd(LaboratoryImportEndRequest),
133 /// Drop a parked import early — the truncated tar makes the
134 /// laboratory's unpack fail, so nothing partial is kept silently.
135 #[schemars(title = "LaboratoryImportAbort")]
136 LaboratoryImportAbort(LaboratoryImportAbortRequest),
137
138 /// Transfer a path between TWO client laboratories on (possibly)
139 /// different hosts: the CLI daemon drives the whole export→import
140 /// splice itself with the half-op vocabulary above, holding at
141 /// most one chunk in transit, and replies once with the byte
142 /// total. Sent by the API's proxy when both endpoints are CLIENT
143 /// laboratories that do NOT share a (machine, state) pair.
144 #[schemars(title = "LaboratoryTransfer")]
145 LaboratoryTransfer(LaboratoryTransferRequest),
146 /// Transfer a path between two client laboratories that share the
147 /// SAME (machine, state) — i.e. the same laboratory host. The CLI
148 /// daemon forwards this verbatim to that host, which pipes the
149 /// source's export stream straight into the destination's import
150 /// (no chunk staging anywhere outside the host). Sent by the
151 /// API's proxy when both endpoints are CLIENT laboratories with
152 /// equal (machine, state) pairs.
153 #[schemars(title = "LaboratoryLocalTransfer")]
154 LaboratoryLocalTransfer(LaboratoryLocalTransferRequest),
155
156}
157
158impl Payload {
159 /// Which CLI-hosted MCP server this payload targets. `Some` for
160 /// the MCP-routed variants; `None` for `ReadMessageQueue` which
161 /// hits the CLI's own local state.
162 pub fn mcp_kind(&self) -> Option<super::super::McpKind> {
163 match self {
164 Payload::Initialize { mcp_kind, .. }
165 | Payload::ToolsList { mcp_kind, .. }
166 | Payload::ToolsCall { mcp_kind, .. }
167 | Payload::ResourcesList { mcp_kind, .. }
168 | Payload::ResourcesRead { mcp_kind, .. }
169 | Payload::SessionTerminate { mcp_kind } => Some(mcp_kind.clone()),
170 Payload::ReadMessageQueue(_)
171 | Payload::Retrieve(_)
172 | Payload::Script(_)
173 | Payload::Drop(_)
174 | Payload::LaboratoryExportBegin(_)
175 | Payload::LaboratoryExportRead(_)
176 | Payload::LaboratoryExportAbort(_)
177 | Payload::LaboratoryImportBegin(_)
178 | Payload::LaboratoryImportWrite(_)
179 | Payload::LaboratoryImportEnd(_)
180 | Payload::LaboratoryImportAbort(_)
181 | Payload::LaboratoryTransfer(_)
182 | Payload::LaboratoryLocalTransfer(_)
183 => None,
184 }
185 }
186}
187
188/// Parameters for [`Payload::ReadMessageQueue`].
189///
190/// Two-rule predicate (now that PENDING is gone):
191/// 1. Direct hit — `message_queue.agent_instance_hierarchy =
192/// agent_instance_hierarchy`.
193/// 2. BOUND-tag hit — `message_queue.agent_tag` resolves to a tag
194/// whose `tags.agent_instance_hierarchy = agent_instance_hierarchy`.
195///
196/// The conduit-side spawn-with-tag flow pre-fires the tag-group
197/// upgrade ahead of every read, so any tags sharing the spawn's
198/// group become BOUND before the EXISTS-check runs and feed
199/// straight into rule 2.
200///
201/// Returns rows oldest-first (`message_queue.id ASC`, which also
202/// matches `message_queue.enqueued_at` ascending due to
203/// AUTOINCREMENT). Row deletion is the downstream consumer's job:
204/// the API stamps the consumed ids onto the first emitted
205/// `AssistantResponseChunk.request_message_ids` so the consumer
206/// knows which rows it owns.
207#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
208#[schemars(rename = "client_objectiveai_mcp.server_request.ReadMessageQueueRequest")]
209pub struct ReadMessageQueueRequest {
210 pub agent_instance_hierarchy: String,
211}
212
213/// Parameters for [`Payload::Drop`]. The objectiveai response id whose
214/// entire upstream-connection bucket the CLI should tear down. The id
215/// rides in the payload (not a header) because `Drop` is a control
216/// message identified solely by its argument.
217#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
218#[schemars(rename = "client_objectiveai_mcp.server_request.DropRequest")]
219pub struct DropRequest {
220 pub response_id: String,
221}
222
223/// Parameters for [`Payload::LaboratoryExportBegin`]. `path` exports
224/// cp-style (a directory is archived under its basename).
225#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
226#[schemars(rename = "client_objectiveai_mcp.server_request.LaboratoryExportBeginRequest")]
227pub struct LaboratoryExportBeginRequest {
228 pub laboratory_id: String,
229 /// The exact laboratory host: machine id + its state. Laboratory
230 /// ids are only unique per (machine, state); an absent pair falls
231 /// back to first-match-by-id routing.
232 #[serde(default, skip_serializing_if = "Option::is_none")]
233 #[schemars(extend("omitempty" = true))]
234 pub machine: Option<String>,
235 #[serde(default, skip_serializing_if = "Option::is_none")]
236 #[schemars(extend("omitempty" = true))]
237 pub machine_state: Option<String>,
238 pub path: String,
239}
240
241/// Parameters for [`Payload::LaboratoryExportRead`].
242#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
243#[schemars(rename = "client_objectiveai_mcp.server_request.LaboratoryExportReadRequest")]
244pub struct LaboratoryExportReadRequest {
245 pub transfer_id: String,
246}
247
248/// Parameters for [`Payload::LaboratoryExportAbort`].
249#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
250#[schemars(rename = "client_objectiveai_mcp.server_request.LaboratoryExportAbortRequest")]
251pub struct LaboratoryExportAbortRequest {
252 pub transfer_id: String,
253}
254
255/// Parameters for [`Payload::LaboratoryImportBegin`]. The tar is
256/// unpacked into `path` (created if missing).
257#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
258#[schemars(rename = "client_objectiveai_mcp.server_request.LaboratoryImportBeginRequest")]
259pub struct LaboratoryImportBeginRequest {
260 pub laboratory_id: String,
261 /// The exact laboratory host: machine id + its state. Laboratory
262 /// ids are only unique per (machine, state); an absent pair falls
263 /// back to first-match-by-id routing.
264 #[serde(default, skip_serializing_if = "Option::is_none")]
265 #[schemars(extend("omitempty" = true))]
266 pub machine: Option<String>,
267 #[serde(default, skip_serializing_if = "Option::is_none")]
268 #[schemars(extend("omitempty" = true))]
269 pub machine_state: Option<String>,
270 pub path: String,
271}
272
273/// Parameters for [`Payload::LaboratoryImportWrite`].
274#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
275#[schemars(rename = "client_objectiveai_mcp.server_request.LaboratoryImportWriteRequest")]
276pub struct LaboratoryImportWriteRequest {
277 pub transfer_id: String,
278 /// Base64-encoded tar bytes.
279 pub data: String,
280}
281
282/// Parameters for [`Payload::LaboratoryImportEnd`].
283#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
284#[schemars(rename = "client_objectiveai_mcp.server_request.LaboratoryImportEndRequest")]
285pub struct LaboratoryImportEndRequest {
286 pub transfer_id: String,
287}
288
289
290/// Parameters for [`Payload::LaboratoryImportAbort`].
291#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
292#[schemars(rename = "client_objectiveai_mcp.server_request.LaboratoryImportAbortRequest")]
293pub struct LaboratoryImportAbortRequest {
294 pub transfer_id: String,
295}
296
297/// Parameters for [`Payload::LaboratoryTransfer`] and
298/// [`Payload::LaboratoryLocalTransfer`] — both endpoints' RAW ids and
299/// exact host pairs (composite ids never ride the wire), plus the two
300/// paths, `cp`-style.
301#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
302#[schemars(rename = "client_objectiveai_mcp.server_request.LaboratoryTransferRequest")]
303pub struct LaboratoryTransferRequest {
304 pub source_id: String,
305 /// The source laboratory's exact host: machine id + its state.
306 /// Absent pair (=) first-match-by-id routing.
307 #[serde(default, skip_serializing_if = "Option::is_none")]
308 #[schemars(extend("omitempty" = true))]
309 pub source_machine: Option<String>,
310 #[serde(default, skip_serializing_if = "Option::is_none")]
311 #[schemars(extend("omitempty" = true))]
312 pub source_machine_state: Option<String>,
313 pub source_path: String,
314 pub destination_id: String,
315 /// The destination laboratory's exact host pair.
316 #[serde(default, skip_serializing_if = "Option::is_none")]
317 #[schemars(extend("omitempty" = true))]
318 pub destination_machine: Option<String>,
319 #[serde(default, skip_serializing_if = "Option::is_none")]
320 #[schemars(extend("omitempty" = true))]
321 pub destination_machine_state: Option<String>,
322 pub destination_path: String,
323}
324
325/// Parameters for [`Payload::LaboratoryLocalTransfer`] — identical
326/// shape to [`LaboratoryTransferRequest`]; the (machine, state) pairs
327/// are equal by construction (the proxy only picks the local variant
328/// when they match).
329#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
330#[schemars(rename = "client_objectiveai_mcp.server_request.LaboratoryLocalTransferRequest")]
331pub struct LaboratoryLocalTransferRequest {
332 pub source_id: String,
333 #[serde(default, skip_serializing_if = "Option::is_none")]
334 #[schemars(extend("omitempty" = true))]
335 pub source_machine: Option<String>,
336 #[serde(default, skip_serializing_if = "Option::is_none")]
337 #[schemars(extend("omitempty" = true))]
338 pub source_machine_state: Option<String>,
339 pub source_path: String,
340 pub destination_id: String,
341 #[serde(default, skip_serializing_if = "Option::is_none")]
342 #[schemars(extend("omitempty" = true))]
343 pub destination_machine: Option<String>,
344 #[serde(default, skip_serializing_if = "Option::is_none")]
345 #[schemars(extend("omitempty" = true))]
346 pub destination_machine_state: Option<String>,
347 pub destination_path: String,
348}
349
350/// Parameters for [`Payload::Initialize`].
351///
352/// Carries plugin arguments lifted off the inbound URL's query
353/// string (`?key=value&flag` → `{"key": Some("value"), "flag": None}`).
354/// Empty for [`super::super::McpKind::ObjectiveAi`] (the primary
355/// upstream takes no args).
356#[derive(Debug, Clone, Default, Serialize, Deserialize, JsonSchema)]
357#[schemars(rename = "client_objectiveai_mcp.server_request.InitializeRequest")]
358pub struct InitializeRequest {
359 /// Plugin arguments the CLI passes through to
360 /// `<plugin> mcp <mcp_name> begin --<key> [value]`. `None` value
361 /// means presence-only flag (`--key`); `Some(v)` means `--key v`.
362 #[serde(default, skip_serializing_if = "IndexMap::is_empty")]
363 #[schemars(extend("omitempty" = true))]
364 pub args: IndexMap<String, Option<String>>,
365}
366
367/// Parameters for [`Payload::Script`].
368///
369/// The identity fields mirror the MCP path's transient headers, but
370/// TYPED (the non-MCP convention — cf. [`ReadMessageQueueRequest`]'s
371/// `agent_instance_hierarchy`): the CLI applies them to the script's
372/// execution context, so anything the script runs via
373/// `objectiveai.execute` uses the calling agent's identity.
374#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
375#[schemars(rename = "client_objectiveai_mcp.server_request.ScriptRequest")]
376pub struct ScriptRequest {
377 /// The code to run — the agent's `type`-tagged script definition
378 /// (`{"type":"python","python":"…"}`).
379 pub script: crate::agent::script::Script,
380 /// The FULL conversation the script receives as its input: the
381 /// request messages plus everything the continuation carries, in
382 /// order.
383 pub messages: Vec<crate::agent::completions::message::Message>,
384 /// Full slash-separated lineage of the agent running this script.
385 pub agent_instance_hierarchy: String,
386 /// Leaf agent id of the attempt running this script.
387 pub agent_id: String,
388 /// WF-level id: the primary agent's id concatenated with all
389 /// fallback ids.
390 pub agent_full_id: String,
391 /// JSON-encoded `RemotePath` the agent was fetched from; `None`
392 /// when the agent was supplied inline.
393 #[serde(default, skip_serializing_if = "Option::is_none")]
394 #[schemars(extend("omitempty" = true))]
395 pub agent_remote: Option<String>,
396 /// The objectiveai response id of this agent run.
397 pub response_id: String,
398 /// The dash-joined sibling response-id group, when known. `None`
399 /// ⇒ the execution context carries no group.
400 #[serde(default, skip_serializing_if = "Option::is_none")]
401 #[schemars(extend("omitempty" = true))]
402 pub response_ids: Option<String>,
403}