objectiveai_sdk/agent/codex_sdk/
agent.rs1use schemars::JsonSchema;
4use serde::{Deserialize, Serialize};
5use twox_hash::XxHash3_128;
6
7#[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 pub upstream: super::Upstream,
22
23 pub model: String,
25
26 pub output_mode: super::OutputMode,
28
29 #[serde(skip_serializing_if = "Option::is_none")]
31 #[schemars(extend("omitempty" = true))]
32 pub effort: Option<super::Effort>,
33
34 #[serde(skip_serializing_if = "Option::is_none")]
36 #[schemars(extend("omitempty" = true))]
37 pub web_search_enabled: Option<bool>,
38
39 #[serde(skip_serializing_if = "Option::is_none")]
41 #[schemars(extend("omitempty" = true))]
42 pub prefix_content: Option<super::super::completions::message::RichContent>,
43
44 #[serde(skip_serializing_if = "Option::is_none")]
46 #[schemars(extend("omitempty" = true))]
47 pub suffix_content: Option<super::super::completions::message::RichContent>,
48
49 #[serde(skip_serializing_if = "Option::is_none")]
51 #[schemars(extend("omitempty" = true))]
52 pub mcp_servers: Option<super::super::McpServers>,
53
54 #[serde(skip_serializing_if = "Option::is_none")]
58 #[schemars(extend("omitempty" = true))]
59 pub client_objectiveai_mcp: Option<super::super::ClientObjectiveaiMcp>,
60}
61
62impl AgentBase {
63 pub fn prepare(&mut self) {
65 self.effort = match self.effort.take() {
66 Some(effort) => effort.prepare(),
67 None => None,
68 };
69 self.web_search_enabled = match self.web_search_enabled {
70 Some(false) => None,
71 other => other,
72 };
73 self.prefix_content = match self.prefix_content.take() {
74 Some(prefix_content) if prefix_content.is_empty() => None,
75 Some(mut prefix_content) => {
76 prefix_content.prepare();
77 if prefix_content.is_empty() {
78 None
79 } else {
80 Some(prefix_content)
81 }
82 }
83 None => None,
84 };
85 self.suffix_content = match self.suffix_content.take() {
86 Some(suffix_content) if suffix_content.is_empty() => None,
87 Some(mut suffix_content) => {
88 suffix_content.prepare();
89 if suffix_content.is_empty() {
90 None
91 } else {
92 Some(suffix_content)
93 }
94 }
95 None => None,
96 };
97 self.mcp_servers = match self.mcp_servers.take() {
98 Some(mcp_servers) => {
99 super::super::mcp::mcp_servers::prepare(mcp_servers)
100 }
101 None => None,
102 };
103 self.client_objectiveai_mcp = match self.client_objectiveai_mcp.take() {
104 Some(cm) => super::super::client_objectiveai_mcp::prepare(cm),
105 None => None,
106 };
107 }
108
109 pub fn validate(&self) -> Result<(), String> {
111 if self.model.is_empty() {
112 return Err("`model` string cannot be empty".to_string());
113 }
114 if let Some(effort) = &self.effort {
115 effort.validate()?;
116 }
117 if let Some(prefix_content) = &self.prefix_content {
118 prefix_content
119 .validate_text_or_image_only()
120 .map_err(|e| format!("`prefix_content`: {e}"))?;
121 }
122 if let Some(suffix_content) = &self.suffix_content {
123 suffix_content
124 .validate_text_or_image_only()
125 .map_err(|e| format!("`suffix_content`: {e}"))?;
126 }
127 if let Some(mcp_servers) = &self.mcp_servers {
128 super::super::mcp::mcp_servers::validate(mcp_servers)?;
129 }
130 if let Some(cm) = &self.client_objectiveai_mcp {
131 super::super::client_objectiveai_mcp::validate(cm)?;
132 }
133 Ok(())
134 }
135
136 pub fn merged_messages(
141 &self,
142 messages: Vec<super::super::completions::message::Message>,
143 ) -> Vec<super::super::completions::message::Message> {
144 use super::super::completions::message::{Message, UserMessage};
145 let prefix_len = if self.prefix_content.is_some() { 1 } else { 0 };
146 let suffix_len = if self.suffix_content.is_some() { 1 } else { 0 };
147 let mut merged =
148 Vec::with_capacity(prefix_len + messages.len() + suffix_len);
149 let mut prefix_inserted = self.prefix_content.is_none();
150 for msg in messages {
151 if !prefix_inserted {
152 if !matches!(msg, Message::System(_) | Message::Developer(_)) {
153 merged.push(Message::User(UserMessage {
154 content: self.prefix_content.clone().unwrap(),
155 name: None,
156 }));
157 prefix_inserted = true;
158 }
159 }
160 merged.push(msg);
161 }
162 if !prefix_inserted {
163 merged.push(Message::User(UserMessage {
164 content: self.prefix_content.clone().unwrap(),
165 name: None,
166 }));
167 }
168 if let Some(suffix_content) = &self.suffix_content {
169 merged.push(Message::User(UserMessage {
170 content: suffix_content.clone(),
171 name: None,
172 }));
173 }
174 merged
175 }
176
177 pub fn id(&self) -> String {
179 let mut hasher = XxHash3_128::with_seed(0);
180 hasher.write(serde_json::to_string(self).unwrap().as_bytes());
181 format!("{:0>22}", base62::encode(hasher.finish_128()))
182 }
183}
184
185#[derive(Clone, Debug, PartialEq, Serialize, Deserialize, JsonSchema)]
187#[schemars(rename = "agent.codex_sdk.Agent")]
188pub struct Agent {
189 pub id: String,
191 #[serde(flatten)]
193 pub base: AgentBase,
194}
195
196impl TryFrom<AgentBase> for Agent {
197 type Error = String;
198 fn try_from(mut base: AgentBase) -> Result<Self, Self::Error> {
199 base.prepare();
200 base.validate()?;
201 let id = base.id();
202 Ok(Agent { id, base })
203 }
204}