Skip to main content

objectiveai_sdk/cli/command/agents/spawn/
mod.rs

1//! `agents spawn` — async handler stub.
2
3use crate::agent::InlineAgentBaseWithFallbacksOrRemoteCommitOptional;
4use crate::cli::command::CommandRequest;
5use crate::cli::command::agents::message::RequestMessage;
6
7#[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize, schemars::JsonSchema)]
8#[schemars(rename = "cli.command.agents.spawn.Request")]
9pub struct Request {
10    pub path_type: Path,
11    /// Initial user message. The CLI turns it into a single
12    /// `Message::User` at the head of the `messages` array on the
13    /// API call. Same wire shape as `agents message`'s
14    /// `RequestMessage` — `Simple`, `Inline(RichContent)`,
15    /// `File`, `PythonInline`, `PythonFile`.
16    pub message: RequestMessage,
17    /// How to resolve the agent for this spawn: either a directly-
18    /// specified `AgentSpec` (`--agent` / `--agent-inline`) or a
19    /// reference to an existing tag (`--agent-tag`). When a tag is
20    /// used, the conduit injects the tag at construction time so
21    /// every conduit read fires the tag-group upgrade — flipping
22    /// every tag in the group to BOUND on the spawn's
23    /// `agent_instance_hierarchy`.
24    pub agent: AgentResolution,
25    pub dangerous_advanced: Option<RequestDangerousAdvanced>,
26    pub jq: Option<String>,
27}
28
29/// Discriminated agent-resolution mode for `agents spawn`.
30///
31/// - `Direct` — `--agent <ref>` / `--agent-inline <json>` paths
32///   produce an `AgentSpec` directly. No tag is involved; the
33///   conduit constructs without an `agent_tag`.
34/// - `Tag` — `--agent-tag <name>` references an existing GROUPED
35///   tag (BOUND tags are rejected — there's already a live AIH for
36///   them, use `agents message` instead). The CLI handler
37///   resolves the tag → `tag_groups` row, takes the row's
38///   `agent_spec` to spawn, and threads the tag name into the
39///   conduit so the spawn's `agent_instance_hierarchy` flips the
40///   whole group to BOUND on the first conduit read.
41#[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize, schemars::JsonSchema)]
42#[serde(tag = "by", rename_all = "snake_case")]
43#[schemars(rename = "cli.command.agents.spawn.AgentResolution")]
44pub enum AgentResolution {
45    #[schemars(title = "Direct")]
46    Direct { agent_spec: AgentSpec },
47    #[schemars(title = "Tag")]
48    Tag { agent_tag: String },
49}
50
51#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize, schemars::JsonSchema)]
52#[schemars(rename = "cli.command.agents.spawn.Path")]
53pub enum Path {
54    #[serde(rename = "agents/spawn")]
55    AgentsSpawn,
56}
57
58/// CLI-surface form for the `--agent` / `--agent-inline` argument: either
59/// a fully resolved inline-or-remote spec, or a bare favorite name that
60/// the CLI resolves to one of those at handler time. Untagged: an inline
61/// agent object or a remote-path object deserializes into `Resolved`; a
62/// bare JSON string lands on `Favorite`.
63#[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize, schemars::JsonSchema)]
64#[serde(untagged)]
65#[schemars(rename = "cli.command.agents.spawn.AgentSpec")]
66pub enum AgentSpec {
67    #[schemars(title = "Resolved")]
68    Resolved(InlineAgentBaseWithFallbacksOrRemoteCommitOptional),
69    #[schemars(title = "Favorite")]
70    Favorite(String),
71}
72
73impl CommandRequest for Request {
74    fn into_command(&self) -> Vec<String> {
75        let mut argv = vec!["agents".to_string(), "spawn".to_string()];
76        // `RequestMessage` lives in `agents::message`
77        // and emits the same five flags spawn accepts.
78        self.message.push_flags(&mut argv);
79        // The agent argument group accepts exactly one of
80        // `--agent-inline <JSON>`, `--agent <REFERENCE>`, or
81        // `--agent-tag <name>`. We emit `--agent-inline` for the
82        // Direct path (the Request already holds the resolved typed
83        // value — the cli's parse hits the inline branch and
84        // round-trips identically for both Inline and Remote
85        // variants), and `--agent-tag` for the Tag path.
86        match &self.agent {
87            AgentResolution::Direct { agent_spec } => {
88                argv.push("--agent-inline".to_string());
89                argv.push(
90                    serde_json::to_string(agent_spec)
91                        .expect("AgentSpec serializes"),
92                );
93            }
94            AgentResolution::Tag { agent_tag } => {
95                argv.push("--agent-tag".to_string());
96                argv.push(agent_tag.clone());
97            }
98        }
99        if let Some(advanced) = &self.dangerous_advanced {
100            argv.push("--dangerous-advanced".to_string());
101            argv.push(
102                serde_json::to_string(advanced)
103                    .expect("RequestDangerousAdvanced serializes"),
104            );
105        }
106        if let Some(jq) = &self.jq {
107            argv.push("--jq".to_string());
108            argv.push(jq.clone());
109        }
110        argv
111    }
112}
113
114#[derive(Debug, Clone, Default, PartialEq, Eq, serde::Serialize, serde::Deserialize, schemars::JsonSchema)]
115#[schemars(rename = "cli.command.agents.spawn.RequestDangerousAdvanced")]
116pub struct RequestDangerousAdvanced {
117    #[serde(default, skip_serializing_if = "Option::is_none")]
118    #[schemars(extend("omitempty" = true))]
119    pub stream: Option<bool>,
120    /// Deterministic seed for the upstream model's RNG (mock
121    /// agents in particular). Plumbed onto
122    /// `AgentCompletionCreateParams.seed`. `None` here ⇒ the
123    /// api picks; tests should always pin a value.
124    #[serde(default, skip_serializing_if = "Option::is_none")]
125    #[schemars(extend("omitempty" = true))]
126    pub seed: Option<i64>,
127}
128
129#[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize, schemars::JsonSchema)]
130#[serde(untagged)]
131#[schemars(rename = "cli.command.agents.spawn.ResponseItem")]
132pub enum ResponseItem {
133    #[schemars(title = "Chunk")]
134    Chunk(crate::agent::completions::response::streaming::AgentCompletionChunk),
135    #[schemars(title = "Id")]
136    Id(String),
137}
138
139/// Non-chunk variant of [`ResponseItem`]. Returned by the unary `execute`
140/// path (with `dangerous_advanced.stream` cleared) when the cli emits a
141/// single bare id string.
142pub type Response = String;
143
144#[derive(clap::Args)]
145pub struct Args {
146    #[command(flatten)]
147    pub message: MessageArgs,
148    #[command(flatten)]
149    pub agent: AgentArgs,
150    /// Raw JSON for `RequestDangerousAdvanced` (e.g.
151    /// `{"stream":true,"seed":42}`).
152    #[arg(long)]
153    pub dangerous_advanced: Option<String>,
154    /// jq filter applied to the JSON output.
155    #[arg(long)]
156    pub jq: Option<String>,
157}
158
159/// Required user-message group. Mirrors `agents instances
160/// message`'s shape: exactly one of the five flags must be set.
161#[derive(clap::Args)]
162#[group(required = true, multiple = false)]
163pub struct MessageArgs {
164    /// Plain text — becomes the body of a single user message.
165    #[arg(long)]
166    pub simple: Option<String>,
167    /// Inline JSON `RichContent`.
168    #[arg(long)]
169    pub inline: Option<String>,
170    /// Path to a JSON file containing the rich content.
171    #[arg(long)]
172    pub file: Option<std::path::PathBuf>,
173    /// Inline Python code that produces the rich content.
174    #[arg(long)]
175    pub python_inline: Option<String>,
176    /// Path to a Python file that produces the rich content.
177    #[arg(long)]
178    pub python_file: Option<std::path::PathBuf>,
179}
180
181#[derive(clap::Args)]
182#[group(required = true, multiple = false)]
183pub struct AgentArgs {
184    /// Favorite-ref or remote-path string.
185    #[arg(long)]
186    pub agent: Option<String>,
187    /// Inline JSON for the full agent definition.
188    #[arg(long)]
189    pub agent_inline: Option<String>,
190    /// Existing tag whose `tag_groups` row provides the agent spec
191    /// to spawn. The tag's parent scope is used for the new
192    /// `agent_instance_hierarchy`; every tag in the same group
193    /// flips to BOUND on the spawn's hierarchy via the conduit-
194    /// driven upgrade on the first message-queue read.
195    #[arg(long)]
196    pub agent_tag: Option<String>,
197}
198
199#[derive(clap::Args)]
200#[command(args_conflicts_with_subcommands = true)]
201pub struct Command {
202    #[command(flatten)]
203    pub args: Args,
204    #[command(subcommand)]
205    pub schema: Option<Schema>,
206}
207
208#[derive(clap::Subcommand)]
209pub enum Schema {
210    /// Emit the JSON Schema for this leaf's `Request` type and exit.
211    RequestSchema(request_schema::Args),
212    /// Emit the JSON Schema for this leaf's `Response` type and exit.
213    ResponseSchema(response_schema::Args),
214}
215
216impl TryFrom<Args> for Request {
217    type Error = crate::cli::command::FromArgsError;
218    fn try_from(args: Args) -> Result<Self, Self::Error> {
219        let message = if let Some(s) = args.message.simple {
220            RequestMessage::Simple(s)
221        } else if let Some(s) = args.message.inline {
222            let mut de = serde_json::Deserializer::from_str(&s);
223            let v = serde_path_to_error::deserialize(&mut de).map_err(|source| {
224                crate::cli::command::FromArgsError {
225                    field: "inline",
226                    source: source.into(),
227                }
228            })?;
229            RequestMessage::Inline(v)
230        } else if let Some(p) = args.message.file {
231            RequestMessage::File(p)
232        } else if let Some(s) = args.message.python_inline {
233            RequestMessage::PythonInline(s)
234        } else {
235            // Clap `required = true` on the `MessageArgs` group
236            // guarantees exactly one of the five flags is set.
237            RequestMessage::PythonFile(args.message.python_file.unwrap())
238        };
239        let agent = if let Some(s) = args.agent.agent_inline {
240            let mut de = serde_json::Deserializer::from_str(&s);
241            let spec: AgentSpec = serde_path_to_error::deserialize(&mut de).map_err(|source| {
242                crate::cli::command::FromArgsError {
243                    field: "agent_inline",
244                    source: source.into(),
245                }
246            })?;
247            AgentResolution::Direct { agent_spec: spec }
248        } else if let Some(s) = args.agent.agent {
249            AgentResolution::Direct {
250                agent_spec: AgentSpec::Favorite(s),
251            }
252        } else {
253            // Clap `required = true` on `AgentArgs` guarantees
254            // exactly one of `--agent`, `--agent-inline`, or
255            // `--agent-tag` is set.
256            AgentResolution::Tag {
257                agent_tag: args.agent.agent_tag.unwrap(),
258            }
259        };
260        let dangerous_advanced: Option<RequestDangerousAdvanced> =
261            if let Some(s) = args.dangerous_advanced {
262                let mut de = serde_json::Deserializer::from_str(&s);
263                let v = serde_path_to_error::deserialize(&mut de).map_err(|source| {
264                    crate::cli::command::FromArgsError {
265                        field: "dangerous_advanced",
266                        source: source.into(),
267                    }
268                })?;
269                Some(v)
270            } else {
271                None
272            };
273        Ok(Self { path_type: Path::AgentsSpawn,
274            message,
275            agent,
276            dangerous_advanced,
277            jq: args.jq,
278        })
279    }
280}
281
282#[cfg(feature = "cli-executor")]
283pub async fn execute_streaming<E: crate::cli::command::CommandExecutor>(
284    executor: &E,
285    mut request: Request,
286
287        agent_arguments: Option<&crate::cli::command::AgentArguments>,
288    ) -> Result<E::Stream<ResponseItem>, E::Error> {
289    request.jq = None;
290    let mut advanced = request.dangerous_advanced.unwrap_or_default();
291    advanced.stream = Some(true);
292    request.dangerous_advanced = Some(advanced);
293    executor.execute(request, agent_arguments).await
294}
295
296#[cfg(feature = "cli-executor")]
297pub async fn execute_streaming_jq<E: crate::cli::command::CommandExecutor>(
298    executor: &E,
299    mut request: Request,
300    jq: String,
301
302        agent_arguments: Option<&crate::cli::command::AgentArguments>,
303    ) -> Result<E::Stream<serde_json::Value>, E::Error> {
304    request.jq = Some(jq);
305    let mut advanced = request.dangerous_advanced.unwrap_or_default();
306    advanced.stream = Some(true);
307    request.dangerous_advanced = Some(advanced);
308    executor.execute(request, agent_arguments).await
309}
310
311#[cfg(feature = "cli-executor")]
312pub async fn execute<E: crate::cli::command::CommandExecutor>(
313    executor: &E,
314    mut request: Request,
315
316        agent_arguments: Option<&crate::cli::command::AgentArguments>,
317    ) -> Result<Response, E::Error> {
318    request.jq = None;
319    if let Some(advanced) = request.dangerous_advanced.as_mut() {
320        advanced.stream = None;
321    }
322    executor.execute_one(request, agent_arguments).await
323}
324
325#[cfg(feature = "cli-executor")]
326pub async fn execute_jq<E: crate::cli::command::CommandExecutor>(
327    executor: &E,
328    mut request: Request,
329    jq: String,
330
331        agent_arguments: Option<&crate::cli::command::AgentArguments>,
332    ) -> Result<serde_json::Value, E::Error> {
333    request.jq = Some(jq);
334    if let Some(advanced) = request.dangerous_advanced.as_mut() {
335        advanced.stream = None;
336    }
337    executor.execute_one(request, agent_arguments).await
338}
339
340#[cfg(feature = "mcp")]
341impl crate::cli::command::CommandResponse for ResponseItem {
342    fn into_mcp(self) -> crate::cli::command::McpResponseItem {
343        crate::cli::command::McpResponseItem::JSONL(serde_json::to_value(self).unwrap())
344    }
345}
346
347pub mod request_schema;
348
349
350pub mod response_schema;