Skip to main content

objectiveai_sdk/agent/codex_sdk/
agent.rs

1//! Codex SDK Agent types and validation logic.
2
3use schemars::JsonSchema;
4use serde::{Deserialize, Serialize};
5use twox_hash::XxHash3_128;
6
7/// The base configuration for a Codex SDK Agent (without computed ID).
8#[derive(
9    Clone,
10    Debug,
11    Default,
12    PartialEq,
13    Serialize,
14    Deserialize,
15    JsonSchema,
16    arbitrary::Arbitrary,
17)]
18#[schemars(rename = "agent.codex_sdk.AgentBase")]
19pub struct AgentBase {
20    /// The upstream provider marker.
21    pub upstream: super::Upstream,
22
23    /// The upstream language model identifier (e.g. `gpt-5`).
24    pub model: String,
25
26    /// The output mode for vector completions. Ignored for agent completions.
27    pub output_mode: super::OutputMode,
28
29    /// Reasoning effort — maps to Codex's `model_reasoning_effort`.
30    #[serde(skip_serializing_if = "Option::is_none")]
31    #[schemars(extend("omitempty" = true))]
32    pub effort: Option<super::Effort>,
33
34    /// Whether this agent may use the codex binary's web-search tool.
35    #[serde(skip_serializing_if = "Option::is_none")]
36    #[schemars(extend("omitempty" = true))]
37    pub web_search_enabled: Option<bool>,
38
39    /// Rich content prepended to the user's prompt.
40    #[serde(skip_serializing_if = "Option::is_none")]
41    #[schemars(extend("omitempty" = true))]
42    pub prefix_content: Option<super::super::completions::message::RichContent>,
43
44    /// Rich content appended after the user's prompt.
45    #[serde(skip_serializing_if = "Option::is_none")]
46    #[schemars(extend("omitempty" = true))]
47    pub suffix_content: Option<super::super::completions::message::RichContent>,
48
49    /// MCP servers the agent can connect to.
50    #[serde(skip_serializing_if = "Option::is_none")]
51    #[schemars(extend("omitempty" = true))]
52    pub mcp_servers: Option<super::super::McpServers>,
53
54    /// Laboratories provisioned for the agent — each becomes a
55    /// client-side laboratory MCP server whose id DERIVES from the
56    /// agent's full id plus the spec (see
57    /// [`laboratories::derived_id`](super::super::laboratory::laboratories::derived_id)).
58    #[serde(skip_serializing_if = "Option::is_none")]
59    #[schemars(extend("omitempty" = true))]
60    pub laboratories: Option<super::super::Laboratories>,
61
62    /// Client-side ObjectiveAI MCP surface the calling client is
63    /// expected to expose locally back to the API (objectiveai
64    /// built-in, plus specific plugins / tools by owner+name+version).
65    #[serde(skip_serializing_if = "Option::is_none")]
66    #[schemars(extend("omitempty" = true))]
67    pub client_objectiveai_mcp: Option<super::super::ClientObjectiveaiMcp>,
68}
69
70impl AgentBase {
71    /// Normalizes the configuration for deterministic ID computation.
72    pub fn prepare(&mut self) {
73        self.effort = match self.effort.take() {
74            Some(effort) => effort.prepare(),
75            None => None,
76        };
77        self.web_search_enabled = match self.web_search_enabled {
78            Some(false) => None,
79            other => other,
80        };
81        self.prefix_content = match self.prefix_content.take() {
82            Some(prefix_content) if prefix_content.is_empty() => None,
83            Some(mut prefix_content) => {
84                prefix_content.prepare();
85                if prefix_content.is_empty() {
86                    None
87                } else {
88                    Some(prefix_content)
89                }
90            }
91            None => None,
92        };
93        self.suffix_content = match self.suffix_content.take() {
94            Some(suffix_content) if suffix_content.is_empty() => None,
95            Some(mut suffix_content) => {
96                suffix_content.prepare();
97                if suffix_content.is_empty() {
98                    None
99                } else {
100                    Some(suffix_content)
101                }
102            }
103            None => None,
104        };
105        self.mcp_servers = match self.mcp_servers.take() {
106            Some(mcp_servers) => {
107                super::super::mcp::mcp_servers::prepare(mcp_servers)
108            }
109            None => None,
110        };
111        self.laboratories = match self.laboratories.take() {
112            Some(laboratories) => {
113                super::super::laboratory::laboratories::prepare(laboratories)
114            }
115            None => None,
116        };
117        self.client_objectiveai_mcp = match self.client_objectiveai_mcp.take() {
118            Some(cm) => super::super::client_objectiveai_mcp::prepare(cm),
119            None => None,
120        };
121    }
122
123    /// Validates the configuration.
124    pub fn validate(&self) -> Result<(), String> {
125        if self.model.is_empty() {
126            return Err("`model` string cannot be empty".to_string());
127        }
128        if let Some(effort) = &self.effort {
129            effort.validate()?;
130        }
131        if let Some(prefix_content) = &self.prefix_content {
132            prefix_content
133                .validate_text_or_image_only()
134                .map_err(|e| format!("`prefix_content`: {e}"))?;
135        }
136        if let Some(suffix_content) = &self.suffix_content {
137            suffix_content
138                .validate_text_or_image_only()
139                .map_err(|e| format!("`suffix_content`: {e}"))?;
140        }
141        if let Some(mcp_servers) = &self.mcp_servers {
142            super::super::mcp::mcp_servers::validate(mcp_servers)?;
143        }
144        if let Some(laboratories) = &self.laboratories {
145            super::super::laboratory::laboratories::validate(laboratories)?;
146        }
147        if let Some(cm) = &self.client_objectiveai_mcp {
148            super::super::client_objectiveai_mcp::validate(cm)?;
149        }
150        Ok(())
151    }
152
153    /// Returns prefix content (if set) as a user message, then the provided
154    /// messages, then suffix content (if set) as a user message. Codex has
155    /// no native system role; system-prompt-style instructions belong on
156    /// the user message itself or in the calling layer's input rendering.
157    pub fn merged_messages(
158        &self,
159        messages: Vec<super::super::completions::message::Message>,
160    ) -> Vec<super::super::completions::message::Message> {
161        use super::super::completions::message::{Message, UserMessage};
162        let prefix_len = if self.prefix_content.is_some() { 1 } else { 0 };
163        let suffix_len = if self.suffix_content.is_some() { 1 } else { 0 };
164        let mut merged =
165            Vec::with_capacity(prefix_len + messages.len() + suffix_len);
166        if let Some(prefix_content) = &self.prefix_content {
167            merged.push(Message::User(UserMessage {
168                content: prefix_content.clone(),
169            }));
170        }
171        merged.extend(messages);
172        if let Some(suffix_content) = &self.suffix_content {
173            merged.push(Message::User(UserMessage {
174                content: suffix_content.clone(),
175            }));
176        }
177        merged
178    }
179
180    /// Computes the deterministic content-addressed ID.
181    pub fn id(&self) -> String {
182        let mut hasher = XxHash3_128::with_seed(0);
183        hasher.write(serde_json::to_string(self).unwrap().as_bytes());
184        format!("{:0>22}", base62::encode(hasher.finish_128()))
185    }
186}
187
188/// A validated Codex SDK Agent with its computed content-addressed ID.
189#[derive(Clone, Debug, PartialEq, Serialize, Deserialize, JsonSchema)]
190#[schemars(rename = "agent.codex_sdk.Agent")]
191pub struct Agent {
192    /// The deterministic content-addressed ID (22-character base62 string).
193    pub id: String,
194    /// The normalized configuration.
195    #[serde(flatten)]
196    pub base: AgentBase,
197}
198
199impl TryFrom<AgentBase> for Agent {
200    type Error = String;
201    fn try_from(mut base: AgentBase) -> Result<Self, Self::Error> {
202        base.prepare();
203        base.validate()?;
204        let id = base.id();
205        Ok(Agent { id, base })
206    }
207}