Skip to main content

objectiveai_sdk/cli/command/user/request/
mod.rs

1//! `user request` — broadcast an outbound request to every connected
2//! user stream (`GET /user`) and BLOCK until the first ACCEPTED reply
3//! (or the base `--timeout` elapses).
4//!
5//! The daemon holds the request PENDING even with zero connected
6//! streams — a user surface that connects later receives it on
7//! replay. An optional `--validate-python` snippet gates replies:
8//! it runs with the full reply (`{"identity": …, "reply": …}`) as its
9//! `input` and must end in a trailing expression evaluating `True`
10//! for the reply to be accepted; anything else (False, no output, an
11//! exception) rejects it and the request stays pending. Without the
12//! base `--timeout` the wait is UNCAPPED.
13
14use crate::cli::command::CommandRequest;
15
16#[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize, schemars::JsonSchema)]
17#[schemars(rename = "cli.command.user.request.Request")]
18pub struct Request {
19    pub path_type: Path,
20    /// Caller-chosen discriminator (e.g. `"AskUserQuestion"`) — how a
21    /// user surface decides what UI the `details` drive.
22    pub key: String,
23    /// Arbitrary request payload, opaque to the daemon.
24    pub details: serde_json::Value,
25    /// Optional reply validator: python whose `input` is the full
26    /// reply and whose trailing expression must evaluate `True` to
27    /// accept it.
28    #[serde(default, skip_serializing_if = "Option::is_none")]
29    #[schemars(extend("omitempty" = true))]
30    pub validate_python: Option<String>,
31    #[serde(flatten)]
32    pub base: crate::cli::command::RequestBase,
33}
34
35#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize, schemars::JsonSchema)]
36#[schemars(rename = "cli.command.user.request.Path")]
37pub enum Path {
38    #[serde(rename = "user/request")]
39    UserRequest,
40}
41
42impl CommandRequest for Request {
43    fn request_base(&self) -> &crate::cli::command::RequestBase {
44        &self.base
45    }
46
47    fn request_base_mut(&mut self) -> Option<&mut crate::cli::command::RequestBase> {
48        Some(&mut self.base)
49    }
50}
51
52/// The winning reply.
53#[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize, schemars::JsonSchema)]
54#[schemars(rename = "cli.command.user.request.Response")]
55pub struct Response {
56    /// The replier's identity (from its `X-OBJECTIVEAI-*` headers).
57    pub identity: crate::cli::command::AgentArguments,
58    /// The accepted reply payload.
59    pub reply: serde_json::Value,
60}
61
62#[derive(clap::Args)]
63pub struct Args {
64    /// Caller-chosen discriminator (e.g. "AskUserQuestion").
65    #[arg(long)]
66    pub key: String,
67    /// The request payload as inline JSON.
68    #[arg(long)]
69    pub details: String,
70    /// Optional reply validator: python receiving the full reply as
71    /// `input`; its trailing expression must evaluate True to accept.
72    #[arg(long)]
73    pub validate_python: Option<String>,
74    #[command(flatten)]
75    pub base: crate::cli::command::RequestBaseArgs,
76}
77
78#[derive(clap::Args)]
79#[command(args_conflicts_with_subcommands = true)]
80pub struct Command {
81    #[command(flatten)]
82    pub args: Args,
83    #[command(subcommand)]
84    pub schema: Option<Schema>,
85}
86
87#[derive(clap::Subcommand)]
88pub enum Schema {
89    /// Emit the JSON Schema for this leaf's `Request` type and exit.
90    RequestSchema(request_schema::Args),
91    /// Emit the JSON Schema for this leaf's `Response` type and exit.
92    ResponseSchema(response_schema::Args),
93}
94
95impl TryFrom<Args> for Request {
96    type Error = crate::cli::command::FromArgsError;
97    fn try_from(args: Args) -> Result<Self, Self::Error> {
98        let mut de = serde_json::Deserializer::from_str(&args.details);
99        let details = serde_path_to_error::deserialize(&mut de)
100            .map_err(|e| crate::cli::command::FromArgsError::json("details", e))?;
101        Ok(Self {
102            path_type: Path::UserRequest,
103            key: args.key,
104            details,
105            validate_python: args.validate_python,
106            base: args.base.into(),
107        })
108    }
109}
110
111#[cfg(feature = "cli-executor")]
112pub async fn execute<E: crate::cli::command::CommandExecutor>(
113    executor: &E,
114    request: Request,
115
116        agent_arguments: Option<&crate::cli::command::AgentArguments>,
117    ) -> Result<Response, E::Error> {
118    executor.execute_one(request, agent_arguments).await
119}
120
121#[cfg(feature = "mcp")]
122impl crate::cli::command::CommandResponse for Response {
123    fn into_mcp(self) -> crate::cli::command::McpResponseItem {
124        crate::cli::command::McpResponseItem::JSONL(serde_json::to_value(self).unwrap())
125    }
126}
127
128pub mod request_schema;
129
130pub mod response_schema;
131
132#[cfg(feature = "cli-executor")]
133pub async fn execute_transform<E: crate::cli::command::CommandExecutor>(
134    executor: &E,
135    request: Request,
136    _transform: crate::cli::command::Transform,
137
138        agent_arguments: Option<&crate::cli::command::AgentArguments>,
139    ) -> Result<serde_json::Value, E::Error> {
140    let resp: Response = executor.execute_one(request, agent_arguments).await?;
141    Ok(serde_json::to_value(resp).expect("Response serializes"))
142}
143
144/// One `/listen` broadcast run of `user request`: the actual
145/// [`Request`], the producer's
146/// [`AgentArguments`](crate::cli::command::AgentArguments), and the
147/// unary response future. See [`crate::cli::broadcast_listener`].
148#[cfg(feature = "cli-listener")]
149pub struct ListenerExecution {
150    pub request: Request,
151    pub agent_arguments: crate::cli::command::AgentArguments,
152    pub response: crate::cli::broadcast_listener::UnaryResponse<Response>,
153}