Skip to main content

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

1//! `agents message` — unary delivery primitive.
2//!
3//! Addresses an agent with the same shape as `agents spawn` (ref /
4//! tag / instance). The handler races a lock: winning it (or
5//! targeting a plain ref) execs a detached `agents spawn` child
6//! with the lock TRANSFERRED into it and returns the child's first
7//! item (`Id`); losing it enqueues, then waits for whichever comes
8//! first — the queue row marked inactive (`Delivered`) or the lock
9//! freeing up (exec the spawn child after all → `Id`). For
10//! fire-and-forget parking without the race, see `agents enqueue`.
11
12use crate::agent::completions::message::RichContent;
13use crate::cli::command::CommandRequest;
14use crate::cli::command::agents::selector::AgentSelector;
15
16#[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize, schemars::JsonSchema)]
17#[schemars(rename = "cli.command.agents.message.Request")]
18pub struct Request {
19    pub path_type: Path,
20    /// Who receives the message — same shape as `agents spawn`'s
21    /// `agent`: a direct ref spawns a fresh agent carrying this
22    /// message; a tag resolves at call time (BOUND → its live
23    /// hierarchy, GROUPED → first message spawns the agent and
24    /// upgrades the group, ABSENT → error); an instance targets an
25    /// existing hierarchy.
26    pub agent: AgentSelector,
27    /// Required payload. The eventual enqueue / delivery / spawn
28    /// always carries this exact `RichContent` as its single
29    /// user message.
30    pub message: RequestMessage,
31    #[serde(default, skip_serializing_if = "Option::is_none")]
32    #[schemars(extend("omitempty" = true))]
33    pub dangerous_advanced: Option<RequestDangerousAdvanced>,
34    #[serde(flatten)]
35    pub base: crate::cli::command::RequestBase,
36}
37
38#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize, schemars::JsonSchema)]
39#[schemars(rename = "cli.command.agents.message.Path")]
40pub enum Path {
41    #[serde(rename = "agents/message")]
42    AgentsMessage,
43}
44
45#[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize, schemars::JsonSchema)]
46#[schemars(rename = "cli.command.agents.message.RequestMessage")]
47pub enum RequestMessage {
48    #[schemars(title = "Inline")]
49    Inline(RichContent),
50    #[schemars(title = "Simple")]
51    Simple(String),
52    #[schemars(title = "File")]
53    File(std::path::PathBuf),
54    #[schemars(title = "PythonInline")]
55    PythonInline(String),
56    #[schemars(title = "PythonFile")]
57    PythonFile(std::path::PathBuf),
58}
59
60impl RequestMessage {
61    /// Append the flag pair (`--simple <s>` / `--inline <json>` /
62    /// `--file <path>` / `--python-inline <code>` /
63    /// `--python-file <path>`) for this variant to `out`. Used by
64    /// both this leaf's [`CommandRequest::into_command`] and by
65    /// `agents queue add`'s — same wire shape, same five flags.
66    pub fn push_flags(&self, out: &mut Vec<String>) {
67        match self {
68            RequestMessage::Inline(rich) => {
69                out.push("--inline".to_string());
70                out.push(
71                    serde_json::to_string(rich)
72                        .expect("RichContent serializes to JSON cleanly"),
73                );
74            }
75            RequestMessage::Simple(s) => {
76                out.push("--simple".to_string());
77                out.push(s.clone());
78            }
79            RequestMessage::File(p) => {
80                out.push("--file".to_string());
81                out.push(p.to_string_lossy().into_owned());
82            }
83            RequestMessage::PythonInline(code) => {
84                out.push("--python-inline".to_string());
85                out.push(code.clone());
86            }
87            RequestMessage::PythonFile(p) => {
88                out.push("--python-file".to_string());
89                out.push(p.to_string_lossy().into_owned());
90            }
91        }
92    }
93}
94
95#[derive(Debug, Clone, Default, PartialEq, Eq, serde::Serialize, serde::Deserialize, schemars::JsonSchema)]
96#[schemars(rename = "cli.command.agents.message.RequestDangerousAdvanced")]
97pub struct RequestDangerousAdvanced {
98    /// Deterministic seed for the upstream model's RNG. Forwarded
99    /// onto the spawn child's `AgentCompletionCreateParams.seed`.
100    /// `None` here ⇒ the api picks; tests should always pin a
101    /// value to keep continuation turns reproducible.
102    #[serde(default, skip_serializing_if = "Option::is_none")]
103    #[schemars(extend("omitempty" = true))]
104    pub seed: Option<i64>,
105}
106
107impl CommandRequest for Request {
108    fn request_base(&self) -> &crate::cli::command::RequestBase {
109        &self.base
110    }
111
112    fn request_base_mut(&mut self) -> Option<&mut crate::cli::command::RequestBase> {
113        Some(&mut self.base)
114    }
115}
116
117/// Unary response. Exactly one of these per call. Internally tagged
118/// via `type`; bare unit variant `Delivered` serializes as
119/// `{"type":"delivered"}`.
120#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize, schemars::JsonSchema)]
121#[serde(tag = "type", rename_all = "snake_case")]
122#[schemars(rename = "cli.command.agents.message.Response")]
123pub enum Response {
124    /// The queue row reached a live agent (its row flipped to
125    /// inactive — the API stamped its id onto an assistant chunk's
126    /// `request_message_ids`) before the agent's lock freed up.
127    #[schemars(title = "Delivered")]
128    Delivered,
129    /// The handler execed a detached `agents spawn` child (with the
130    /// agent's lock transferred into it) and the child yielded its
131    /// `Id` first item — the bare `agent_instance_hierarchy` the
132    /// runner just minted or resumed.
133    #[schemars(title = "Id")]
134    Id { agent_instance_hierarchy: String },
135}
136
137#[derive(clap::Args)]
138pub struct Args {
139    #[command(flatten)]
140    pub agent: crate::cli::command::agents::selector::AgentSelectorArgs,
141    #[command(flatten)]
142    pub message: MessageArgs,
143    /// Raw JSON for [`RequestDangerousAdvanced`] (e.g.
144    /// `{"seed":42}`).
145    #[arg(long)]
146    pub dangerous_advanced: Option<String>,
147    #[command(flatten)]
148    pub base: crate::cli::command::RequestBaseArgs,
149}
150
151#[derive(clap::Args)]
152#[group(required = true, multiple = false)]
153pub struct MessageArgs {
154    /// Plain text — becomes one user message.
155    #[arg(long)]
156    pub simple: Option<String>,
157    /// Inline JSON `RichContent`.
158    #[arg(long)]
159    pub inline: Option<String>,
160    /// Path to a JSON file containing the rich content.
161    #[arg(long)]
162    pub file: Option<std::path::PathBuf>,
163    /// Inline Python code that produces the rich content.
164    #[arg(long)]
165    pub python_inline: Option<String>,
166    /// Path to a Python file that produces the rich content.
167    #[arg(long)]
168    pub python_file: Option<std::path::PathBuf>,
169}
170
171#[derive(clap::Args)]
172#[command(args_conflicts_with_subcommands = true)]
173pub struct Command {
174    #[command(flatten)]
175    pub args: Args,
176    #[command(subcommand)]
177    pub schema: Option<Schema>,
178}
179
180#[derive(clap::Subcommand)]
181pub enum Schema {
182    /// Emit the JSON Schema for this leaf's `Request` type and exit.
183    RequestSchema(request_schema::Args),
184    /// Emit the JSON Schema for this leaf's `Response` type and exit.
185    ResponseSchema(response_schema::Args),
186}
187
188impl TryFrom<Args> for Request {
189    type Error = crate::cli::command::FromArgsError;
190    fn try_from(args: Args) -> Result<Self, Self::Error> {
191        let message = if let Some(s) = args.message.simple {
192            RequestMessage::Simple(s)
193        } else if let Some(s) = args.message.inline {
194            let mut de = serde_json::Deserializer::from_str(&s);
195            let v = serde_path_to_error::deserialize(&mut de).map_err(|source| {
196                crate::cli::command::FromArgsError {
197                    field: "inline",
198                    source: source.into(),
199                }
200            })?;
201            RequestMessage::Inline(v)
202        } else if let Some(p) = args.message.file {
203            RequestMessage::File(p)
204        } else if let Some(s) = args.message.python_inline {
205            RequestMessage::PythonInline(s)
206        } else {
207            // Clap `required = true` on `MessageArgs` guarantees
208            // exactly one of the five flags is set.
209            RequestMessage::PythonFile(args.message.python_file.unwrap())
210        };
211        let agent = AgentSelector::try_from(args.agent)?;
212        let dangerous_advanced: Option<RequestDangerousAdvanced> =
213            if let Some(s) = args.dangerous_advanced {
214                let mut de = serde_json::Deserializer::from_str(&s);
215                let v = serde_path_to_error::deserialize(&mut de).map_err(|source| {
216                    crate::cli::command::FromArgsError {
217                        field: "dangerous_advanced",
218                        source: source.into(),
219                    }
220                })?;
221                Some(v)
222            } else {
223                None
224            };
225        Ok(Self {
226            path_type: Path::AgentsMessage,
227            agent,
228            message,
229            dangerous_advanced,
230            base: args.base.into(),
231        })
232    }
233}
234
235#[cfg(feature = "cli-executor")]
236pub async fn execute<E: crate::cli::command::CommandExecutor>(
237    executor: &E,
238    mut request: Request,
239
240        agent_arguments: Option<&crate::cli::command::AgentArguments>,
241    ) -> Result<Response, E::Error> {
242    request.base.clear_transform();
243    executor.execute_one(request, agent_arguments).await
244}
245
246#[cfg(feature = "cli-executor")]
247pub async fn execute_transform<E: crate::cli::command::CommandExecutor>(
248    executor: &E,
249    mut request: Request,
250    transform: crate::cli::command::Transform,
251
252        agent_arguments: Option<&crate::cli::command::AgentArguments>,
253    ) -> Result<serde_json::Value, E::Error> {
254    request.base.set_transform(transform);
255    executor.execute_one(request, agent_arguments).await
256}
257
258#[cfg(feature = "mcp")]
259impl crate::cli::command::CommandResponse for Response {
260    fn into_mcp(self) -> crate::cli::command::McpResponseItem {
261        crate::cli::command::McpResponseItem::JSONL(serde_json::to_value(self).unwrap())
262    }
263}
264
265pub mod request_schema;
266
267
268pub mod response_schema;