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 into_command(&self) -> Vec<String> {
109        let mut argv = vec!["agents".to_string(), "message".to_string()];
110        self.agent.push_flags(&mut argv);
111        self.message.push_flags(&mut argv);
112        if let Some(advanced) = &self.dangerous_advanced {
113            argv.push("--dangerous-advanced".to_string());
114            argv.push(
115                serde_json::to_string(advanced)
116                    .expect("RequestDangerousAdvanced serializes"),
117            );
118        }
119        self.base.push_flags(&mut argv);
120        argv
121    }
122
123    fn request_base(&self) -> &crate::cli::command::RequestBase {
124        &self.base
125    }
126
127    fn request_base_mut(&mut self) -> Option<&mut crate::cli::command::RequestBase> {
128        Some(&mut self.base)
129    }
130}
131
132/// Unary response. Exactly one of these per call. Internally tagged
133/// via `type`; bare unit variant `Delivered` serializes as
134/// `{"type":"delivered"}`.
135#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize, schemars::JsonSchema)]
136#[serde(tag = "type", rename_all = "snake_case")]
137#[schemars(rename = "cli.command.agents.message.Response")]
138pub enum Response {
139    /// The queue row reached a live agent (its row flipped to
140    /// inactive — the API stamped its id onto an assistant chunk's
141    /// `request_message_ids`) before the agent's lock freed up.
142    #[schemars(title = "Delivered")]
143    Delivered,
144    /// The handler execed a detached `agents spawn` child (with the
145    /// agent's lock transferred into it) and the child yielded its
146    /// `Id` first item — the bare `agent_instance_hierarchy` the
147    /// runner just minted or resumed.
148    #[schemars(title = "Id")]
149    Id { agent_instance_hierarchy: String },
150}
151
152#[derive(clap::Args)]
153pub struct Args {
154    #[command(flatten)]
155    pub agent: crate::cli::command::agents::selector::AgentSelectorArgs,
156    #[command(flatten)]
157    pub message: MessageArgs,
158    /// Raw JSON for [`RequestDangerousAdvanced`] (e.g.
159    /// `{"seed":42}`).
160    #[arg(long)]
161    pub dangerous_advanced: Option<String>,
162    #[command(flatten)]
163    pub base: crate::cli::command::RequestBaseArgs,
164}
165
166#[derive(clap::Args)]
167#[group(required = true, multiple = false)]
168pub struct MessageArgs {
169    /// Plain text — becomes one user message.
170    #[arg(long)]
171    pub simple: Option<String>,
172    /// Inline JSON `RichContent`.
173    #[arg(long)]
174    pub inline: Option<String>,
175    /// Path to a JSON file containing the rich content.
176    #[arg(long)]
177    pub file: Option<std::path::PathBuf>,
178    /// Inline Python code that produces the rich content.
179    #[arg(long)]
180    pub python_inline: Option<String>,
181    /// Path to a Python file that produces the rich content.
182    #[arg(long)]
183    pub python_file: Option<std::path::PathBuf>,
184}
185
186#[derive(clap::Args)]
187#[command(args_conflicts_with_subcommands = true)]
188pub struct Command {
189    #[command(flatten)]
190    pub args: Args,
191    #[command(subcommand)]
192    pub schema: Option<Schema>,
193}
194
195#[derive(clap::Subcommand)]
196pub enum Schema {
197    /// Emit the JSON Schema for this leaf's `Request` type and exit.
198    RequestSchema(request_schema::Args),
199    /// Emit the JSON Schema for this leaf's `Response` type and exit.
200    ResponseSchema(response_schema::Args),
201}
202
203impl TryFrom<Args> for Request {
204    type Error = crate::cli::command::FromArgsError;
205    fn try_from(args: Args) -> Result<Self, Self::Error> {
206        let message = if let Some(s) = args.message.simple {
207            RequestMessage::Simple(s)
208        } else if let Some(s) = args.message.inline {
209            let mut de = serde_json::Deserializer::from_str(&s);
210            let v = serde_path_to_error::deserialize(&mut de).map_err(|source| {
211                crate::cli::command::FromArgsError {
212                    field: "inline",
213                    source: source.into(),
214                }
215            })?;
216            RequestMessage::Inline(v)
217        } else if let Some(p) = args.message.file {
218            RequestMessage::File(p)
219        } else if let Some(s) = args.message.python_inline {
220            RequestMessage::PythonInline(s)
221        } else {
222            // Clap `required = true` on `MessageArgs` guarantees
223            // exactly one of the five flags is set.
224            RequestMessage::PythonFile(args.message.python_file.unwrap())
225        };
226        let agent = AgentSelector::try_from(args.agent)?;
227        let dangerous_advanced: Option<RequestDangerousAdvanced> =
228            if let Some(s) = args.dangerous_advanced {
229                let mut de = serde_json::Deserializer::from_str(&s);
230                let v = serde_path_to_error::deserialize(&mut de).map_err(|source| {
231                    crate::cli::command::FromArgsError {
232                        field: "dangerous_advanced",
233                        source: source.into(),
234                    }
235                })?;
236                Some(v)
237            } else {
238                None
239            };
240        Ok(Self {
241            path_type: Path::AgentsMessage,
242            agent,
243            message,
244            dangerous_advanced,
245            base: args.base.into(),
246        })
247    }
248}
249
250#[cfg(feature = "cli-executor")]
251pub async fn execute<E: crate::cli::command::CommandExecutor>(
252    executor: &E,
253    mut request: Request,
254
255        agent_arguments: Option<&crate::cli::command::AgentArguments>,
256    ) -> Result<Response, E::Error> {
257    request.base.clear_transform();
258    executor.execute_one(request, agent_arguments).await
259}
260
261#[cfg(feature = "cli-executor")]
262pub async fn execute_transform<E: crate::cli::command::CommandExecutor>(
263    executor: &E,
264    mut request: Request,
265    transform: crate::cli::command::Transform,
266
267        agent_arguments: Option<&crate::cli::command::AgentArguments>,
268    ) -> Result<serde_json::Value, E::Error> {
269    request.base.set_transform(transform);
270    executor.execute_one(request, agent_arguments).await
271}
272
273#[cfg(feature = "mcp")]
274impl crate::cli::command::CommandResponse for Response {
275    fn into_mcp(self) -> crate::cli::command::McpResponseItem {
276        crate::cli::command::McpResponseItem::JSONL(serde_json::to_value(self).unwrap())
277    }
278}
279
280pub mod request_schema;
281
282
283pub mod response_schema;