Skip to main content

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}