Skip to main content

objectiveai_sdk/cli/command/agents/
selector.rs

1//! Shared agent-input shape for `agents spawn` and `agents message`.
2//!
3//! Both commands address an agent the same three ways:
4//!
5//! - **Ref** — a concrete agent definition: a remote-path string
6//!   (`--agent`), inline JSON (`--agent-inline`), a JSON file
7//!   (`--agent-file`), or Python producing the JSON
8//!   (`--agent-python-inline` / `--agent-python-file`). File/Python
9//!   variants travel UNRESOLVED on the wire; the CLI resolves them
10//!   into a typed agent at execution time.
11//! - **Tag** — `--agent-tag <name>`, resolved against the tags DB at
12//!   call time: BOUND yields the live `agent_instance_hierarchy`
13//!   (historic instance), GROUPED yields the group's stored agent
14//!   spec (fresh spawn that upgrades the group on first conduit
15//!   read), ABSENT is an error.
16//! - **Instance** — `--agent-instance <leaf>` plus optional
17//!   `--parent-agent-instance-hierarchy` (the CLI substitutes its own
18//!   `Config.agent_instance_hierarchy` when omitted): an existing
19//!   agent instance, resumed via its stored session + continuation.
20
21use crate::agent::InlineAgentBaseWithFallbacksOrRemoteCommitOptional;
22
23#[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize, schemars::JsonSchema)]
24#[serde(tag = "by", rename_all = "snake_case")]
25#[schemars(rename = "cli.command.agents.AgentSelector")]
26pub enum AgentSelector {
27    #[schemars(title = "Ref")]
28    Ref { agent: AgentRef },
29    #[schemars(title = "Tag")]
30    Tag { agent_tag: String },
31    #[schemars(title = "Instance")]
32    Instance {
33        /// Lineage prefix to prepend to `agent_instance`. When
34        /// `None`, the CLI substitutes its own
35        /// `Config.agent_instance_hierarchy`.
36        #[serde(default, skip_serializing_if = "Option::is_none")]
37        #[schemars(extend("omitempty" = true))]
38        parent_agent_instance_hierarchy: Option<String>,
39        /// Leaf id of the target agent instance.
40        agent_instance: String,
41    },
42}
43
44/// Wire form of a direct agent definition. `Resolved` carries the
45/// typed inline-or-remote spec (the `--agent <ref>` string is parsed
46/// into `Resolved(Remote(..))` at argv-parse time); the other
47/// variants defer IO / Python execution to the CLI handler.
48#[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize, schemars::JsonSchema)]
49#[schemars(rename = "cli.command.agents.AgentRef")]
50pub enum AgentRef {
51    #[schemars(title = "Resolved")]
52    Resolved(InlineAgentBaseWithFallbacksOrRemoteCommitOptional),
53    #[schemars(title = "File")]
54    File(std::path::PathBuf),
55    #[schemars(title = "PythonInline")]
56    PythonInline(String),
57    #[schemars(title = "PythonFile")]
58    PythonFile(std::path::PathBuf),
59}
60
61impl AgentSelector {
62    /// Append this selector's flag pair(s) to `out`. Mirrors
63    /// [`super::message::RequestMessage::push_flags`] — both
64    /// `agents spawn` and `agents message` accept the same flags.
65    /// `Resolved` refs are emitted as `--agent-inline <json>` (the
66    /// typed value round-trips identically for both Inline and
67    /// Remote variants).
68    pub fn push_flags(&self, out: &mut Vec<String>) {
69        match self {
70            AgentSelector::Ref { agent } => match agent {
71                AgentRef::Resolved(spec) => {
72                    out.push("--agent-inline".to_string());
73                    out.push(
74                        serde_json::to_string(spec)
75                            .expect("agent spec serializes"),
76                    );
77                }
78                AgentRef::File(p) => {
79                    out.push("--agent-file".to_string());
80                    out.push(p.to_string_lossy().into_owned());
81                }
82                AgentRef::PythonInline(code) => {
83                    out.push("--agent-python-inline".to_string());
84                    out.push(code.clone());
85                }
86                AgentRef::PythonFile(p) => {
87                    out.push("--agent-python-file".to_string());
88                    out.push(p.to_string_lossy().into_owned());
89                }
90            },
91            AgentSelector::Tag { agent_tag } => {
92                out.push("--agent-tag".to_string());
93                out.push(agent_tag.clone());
94            }
95            AgentSelector::Instance {
96                parent_agent_instance_hierarchy,
97                agent_instance,
98            } => {
99                out.push("--agent-instance".to_string());
100                out.push(agent_instance.clone());
101                if let Some(parent) = parent_agent_instance_hierarchy {
102                    out.push("--parent-agent-instance-hierarchy".to_string());
103                    out.push(parent.clone());
104                }
105            }
106        }
107    }
108}
109
110/// Required agent-selector group: exactly one of the seven selector
111/// flags must be set. `--parent-agent-instance-hierarchy` rides
112/// OUTSIDE the group (it modifies `--agent-instance` rather than
113/// competing with it).
114#[derive(clap::Args)]
115#[command(group(
116    clap::ArgGroup::new("agent_selector")
117        .required(true)
118        .multiple(false)
119        .args([
120            "agent",
121            "agent_inline",
122            "agent_file",
123            "agent_python_inline",
124            "agent_python_file",
125            "agent_tag",
126            "agent_instance",
127        ])
128))]
129pub struct AgentSelectorArgs {
130    /// Remote-path string.
131    #[arg(long)]
132    pub agent: Option<String>,
133    /// Inline JSON for the full agent definition.
134    #[arg(long)]
135    pub agent_inline: Option<String>,
136    /// Path to a JSON file containing the agent definition.
137    #[arg(long)]
138    pub agent_file: Option<std::path::PathBuf>,
139    /// Inline Python code that produces the agent definition.
140    #[arg(long)]
141    pub agent_python_inline: Option<String>,
142    /// Path to a Python file that produces the agent definition.
143    #[arg(long)]
144    pub agent_python_file: Option<std::path::PathBuf>,
145    /// Tag name, resolved against the tags DB at execution time:
146    /// BOUND → the live hierarchy, GROUPED → the group's stored
147    /// agent spec (the whole group flips to BOUND on the spawn's
148    /// hierarchy via the conduit-driven upgrade), ABSENT → error.
149    #[arg(long)]
150    pub agent_tag: Option<String>,
151    /// Leaf id of an existing agent instance.
152    #[arg(long)]
153    pub agent_instance: Option<String>,
154    /// Optional lineage prefix to prepend to `--agent-instance`.
155    /// When omitted, the cli substitutes its own
156    /// `Config.agent_instance_hierarchy`.
157    #[arg(long, requires = "agent_instance")]
158    pub parent_agent_instance_hierarchy: Option<String>,
159}
160
161impl TryFrom<AgentSelectorArgs> for AgentSelector {
162    type Error = crate::cli::command::FromArgsError;
163    fn try_from(args: AgentSelectorArgs) -> Result<Self, Self::Error> {
164        if let Some(s) = args.agent_inline {
165            let mut de = serde_json::Deserializer::from_str(&s);
166            let spec: InlineAgentBaseWithFallbacksOrRemoteCommitOptional =
167                serde_path_to_error::deserialize(&mut de).map_err(|source| {
168                    crate::cli::command::FromArgsError {
169                        field: "agent_inline",
170                        source: source.into(),
171                    }
172                })?;
173            Ok(AgentSelector::Ref {
174                agent: AgentRef::Resolved(spec),
175            })
176        } else if let Some(s) = args.agent {
177            let path: crate::RemotePathCommitOptional = s
178                .parse()
179                .map_err(|e| crate::cli::command::FromArgsError::path_parse("agent", e))?;
180            Ok(AgentSelector::Ref {
181                agent: AgentRef::Resolved(
182                    InlineAgentBaseWithFallbacksOrRemoteCommitOptional::Remote(path),
183                ),
184            })
185        } else if let Some(p) = args.agent_file {
186            Ok(AgentSelector::Ref {
187                agent: AgentRef::File(p),
188            })
189        } else if let Some(code) = args.agent_python_inline {
190            Ok(AgentSelector::Ref {
191                agent: AgentRef::PythonInline(code),
192            })
193        } else if let Some(p) = args.agent_python_file {
194            Ok(AgentSelector::Ref {
195                agent: AgentRef::PythonFile(p),
196            })
197        } else if let Some(agent_tag) = args.agent_tag {
198            Ok(AgentSelector::Tag { agent_tag })
199        } else {
200            // Clap `required = true` on the `agent_selector` group
201            // guarantees exactly one of the seven flags is set.
202            Ok(AgentSelector::Instance {
203                parent_agent_instance_hierarchy: args.parent_agent_instance_hierarchy,
204                agent_instance: args.agent_instance.unwrap(),
205            })
206        }
207    }
208}