1use std::collections::BTreeMap;
2
3use serde::{de::Error as _, Deserialize, Deserializer, Serialize};
4use serde_json::Value;
5
6use crate::model_settings::ModelSettings;
7use crate::tools::common::trim_portable_whitespace;
8
9use super::{
10 json_value_from_serializable, AgentStatus, CompletionReason, Message, Metadata, NoToolPolicy,
11};
12
13pub const INVALID_SUB_AGENT_MODEL_CODE: &str = "invalid_sub_agent_model";
14pub const INVALID_SUB_AGENT_MODEL_MESSAGE: &str = "sub-agent model cannot be empty";
15pub const INVALID_SUB_AGENT_SYSTEM_PROMPT_CODE: &str = "invalid_sub_agent_system_prompt";
16pub const INVALID_SUB_AGENT_SYSTEM_PROMPT_MESSAGE: &str =
17 "sub-agent system_prompt cannot be empty when provided";
18
19#[derive(Debug, Clone, PartialEq, Eq)]
20pub struct SubAgentConfigValidationError {
21 code: &'static str,
22 message: &'static str,
23}
24
25impl SubAgentConfigValidationError {
26 fn new(code: &'static str, message: &'static str) -> Self {
27 Self { code, message }
28 }
29
30 pub fn code(&self) -> &'static str {
31 self.code
32 }
33
34 pub fn message(&self) -> &'static str {
35 self.message
36 }
37}
38
39impl std::fmt::Display for SubAgentConfigValidationError {
40 fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
41 formatter.write_str(self.message)
42 }
43}
44
45impl std::error::Error for SubAgentConfigValidationError {}
46
47#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
48pub struct SubAgentConfig {
49 pub model: String,
50 pub description: String,
51 pub backend: Option<String>,
52 pub system_prompt: Option<String>,
53 pub max_cycles: u32,
54 pub exclude_tools: Vec<String>,
55 pub metadata: Metadata,
56}
57
58#[derive(Deserialize)]
59struct SubAgentConfigWire {
60 model: String,
61 #[serde(default)]
62 description: String,
63 #[serde(default)]
64 backend: Option<String>,
65 #[serde(default)]
66 system_prompt: Option<String>,
67 #[serde(default = "default_sub_agent_max_cycles")]
68 max_cycles: u32,
69 #[serde(default)]
70 exclude_tools: Vec<String>,
71 #[serde(default)]
72 metadata: Metadata,
73}
74
75const fn default_sub_agent_max_cycles() -> u32 {
76 8
77}
78
79impl<'de> Deserialize<'de> for SubAgentConfig {
80 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
81 where
82 D: Deserializer<'de>,
83 {
84 let value = Value::deserialize(deserializer)?;
85 if !value.is_object() {
86 return Err(D::Error::custom("SubAgentConfig payload must be an object"));
87 }
88 let wire = serde_json::from_value::<SubAgentConfigWire>(value).map_err(D::Error::custom)?;
89 let config = Self {
90 model: trim_portable_whitespace(&wire.model).to_string(),
91 description: wire.description,
92 backend: wire.backend,
93 system_prompt: wire.system_prompt,
94 max_cycles: wire.max_cycles,
95 exclude_tools: wire.exclude_tools,
96 metadata: wire.metadata,
97 };
98 config.validate().map_err(D::Error::custom)?;
99 Ok(config)
100 }
101}
102
103impl SubAgentConfig {
104 pub fn new(model: impl Into<String>, description: impl Into<String>) -> Self {
105 let model = model.into();
106 Self {
107 model: trim_portable_whitespace(&model).to_string(),
108 description: description.into(),
109 backend: None,
110 system_prompt: None,
111 max_cycles: 8,
112 exclude_tools: Vec::new(),
113 metadata: Metadata::new(),
114 }
115 }
116
117 pub fn validate(&self) -> Result<(), SubAgentConfigValidationError> {
118 if trim_portable_whitespace(&self.model).is_empty() {
119 return Err(SubAgentConfigValidationError::new(
120 INVALID_SUB_AGENT_MODEL_CODE,
121 INVALID_SUB_AGENT_MODEL_MESSAGE,
122 ));
123 }
124 if self
125 .system_prompt
126 .as_deref()
127 .is_some_and(|prompt| trim_portable_whitespace(prompt).is_empty())
128 {
129 return Err(SubAgentConfigValidationError::new(
130 INVALID_SUB_AGENT_SYSTEM_PROMPT_CODE,
131 INVALID_SUB_AGENT_SYSTEM_PROMPT_MESSAGE,
132 ));
133 }
134 Ok(())
135 }
136}
137
138#[derive(Debug, Clone, PartialEq, Serialize)]
139pub struct AgentTask {
140 pub task_id: String,
141 pub model: String,
142 pub system_prompt: String,
143 pub user_prompt: String,
144 pub max_cycles: u32,
145 pub memory_compact_threshold: u64,
146 pub memory_threshold_percentage: u8,
147 pub no_tool_policy: NoToolPolicy,
148 pub allow_interruption: bool,
149 pub use_workspace: bool,
150 pub has_sub_agents: bool,
151 pub sub_agents: BTreeMap<String, SubAgentConfig>,
152 pub agent_type: Option<String>,
153 pub native_multimodal: bool,
154 pub extra_tool_names: Vec<String>,
155 pub exclude_tools: Vec<String>,
156 pub initial_messages: Vec<Message>,
157 pub initial_shared_state: Metadata,
158 pub model_settings: Option<ModelSettings>,
159 pub metadata: Metadata,
160}
161
162#[derive(Deserialize)]
163struct AgentTaskWire {
164 task_id: String,
165 model: String,
166 system_prompt: String,
167 user_prompt: String,
168 #[serde(default = "default_agent_task_max_cycles")]
169 max_cycles: u32,
170 #[serde(default = "default_memory_compact_threshold")]
171 memory_compact_threshold: u64,
172 #[serde(default = "default_memory_threshold_percentage")]
173 memory_threshold_percentage: u8,
174 #[serde(default)]
175 no_tool_policy: NoToolPolicy,
176 #[serde(default = "default_true")]
177 allow_interruption: bool,
178 #[serde(default = "default_true")]
179 use_workspace: bool,
180 #[serde(default)]
181 has_sub_agents: bool,
182 #[serde(default)]
183 sub_agents: BTreeMap<String, SubAgentConfig>,
184 #[serde(default)]
185 agent_type: Option<String>,
186 #[serde(default)]
187 native_multimodal: bool,
188 #[serde(default)]
189 extra_tool_names: Vec<String>,
190 #[serde(default)]
191 exclude_tools: Vec<String>,
192 #[serde(default, deserialize_with = "deserialize_agent_task_messages")]
193 initial_messages: Vec<Message>,
194 #[serde(default)]
195 initial_shared_state: Metadata,
196 #[serde(default, deserialize_with = "deserialize_agent_task_model_settings")]
197 model_settings: Option<ModelSettings>,
198 #[serde(default)]
199 metadata: Metadata,
200}
201
202impl<'de> Deserialize<'de> for AgentTask {
203 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
204 where
205 D: Deserializer<'de>,
206 {
207 let value = Value::deserialize(deserializer)?;
208 if !value.is_object() {
209 return Err(D::Error::custom("AgentTask payload must be an object"));
210 }
211 let wire = serde_json::from_value::<AgentTaskWire>(value).map_err(D::Error::custom)?;
212 Ok(Self {
213 task_id: wire.task_id,
214 model: wire.model,
215 system_prompt: wire.system_prompt,
216 user_prompt: wire.user_prompt,
217 max_cycles: wire.max_cycles,
218 memory_compact_threshold: wire.memory_compact_threshold,
219 memory_threshold_percentage: wire.memory_threshold_percentage,
220 no_tool_policy: wire.no_tool_policy,
221 allow_interruption: wire.allow_interruption,
222 use_workspace: wire.use_workspace,
223 has_sub_agents: wire.has_sub_agents,
224 sub_agents: wire.sub_agents,
225 agent_type: wire.agent_type,
226 native_multimodal: wire.native_multimodal,
227 extra_tool_names: wire.extra_tool_names,
228 exclude_tools: wire.exclude_tools,
229 initial_messages: wire.initial_messages,
230 initial_shared_state: wire.initial_shared_state,
231 model_settings: wire.model_settings,
232 metadata: wire.metadata,
233 })
234 }
235}
236
237const fn default_agent_task_max_cycles() -> u32 {
238 8
239}
240
241const fn default_memory_compact_threshold() -> u64 {
242 128_000
243}
244
245const fn default_memory_threshold_percentage() -> u8 {
246 90
247}
248
249const fn default_true() -> bool {
250 true
251}
252
253fn deserialize_agent_task_messages<'de, D>(deserializer: D) -> Result<Vec<Message>, D::Error>
254where
255 D: Deserializer<'de>,
256{
257 Vec::<Value>::deserialize(deserializer)?
258 .into_iter()
259 .enumerate()
260 .map(|(index, value)| {
261 validate_agent_task_message(&value, index).map_err(D::Error::custom)?;
262 Message::from_dict(&value).map_err(D::Error::custom)
263 })
264 .collect()
265}
266
267fn deserialize_agent_task_model_settings<'de, D>(
268 deserializer: D,
269) -> Result<Option<ModelSettings>, D::Error>
270where
271 D: Deserializer<'de>,
272{
273 let value = Option::<Value>::deserialize(deserializer)?;
274 value
275 .map(|value| {
276 if !value.is_object() {
277 return Err(D::Error::custom(
278 "AgentTask field 'model_settings' must be an object or null",
279 ));
280 }
281 serde_json::from_value(value).map_err(D::Error::custom)
282 })
283 .transpose()
284}
285
286fn validate_agent_task_message(value: &Value, index: usize) -> Result<(), String> {
287 let object = value
288 .as_object()
289 .ok_or_else(|| format!("AgentTask initial_messages[{index}] must be an object"))?;
290 let role = object
291 .get("role")
292 .and_then(Value::as_str)
293 .ok_or_else(|| format!("AgentTask initial_messages[{index}].role must be a string"))?;
294 if !matches!(role, "system" | "user" | "assistant" | "tool") {
295 return Err(format!(
296 "unknown AgentTask initial_messages[{index}].role: {role}"
297 ));
298 }
299 if object
300 .get("content")
301 .is_some_and(|value| !value.is_string())
302 {
303 return Err(format!(
304 "AgentTask initial_messages[{index}].content must be a string"
305 ));
306 }
307 for field_name in ["name", "tool_call_id", "reasoning_content", "image_url"] {
308 if object
309 .get(field_name)
310 .is_some_and(|value| !value.is_null() && !value.is_string())
311 {
312 return Err(format!(
313 "AgentTask initial_messages[{index}].{field_name} must be a string or null"
314 ));
315 }
316 }
317 if object.get("tool_calls").is_some_and(|value| {
318 !value
319 .as_array()
320 .is_some_and(|items| items.iter().all(Value::is_object))
321 }) {
322 return Err(format!(
323 "AgentTask initial_messages[{index}].tool_calls must be an array of objects"
324 ));
325 }
326 if object
327 .get("metadata")
328 .is_some_and(|value| !value.is_object())
329 {
330 return Err(format!(
331 "AgentTask initial_messages[{index}].metadata must be an object"
332 ));
333 }
334 Ok(())
335}
336
337impl AgentTask {
338 pub fn new(
339 task_id: impl Into<String>,
340 model: impl Into<String>,
341 system_prompt: impl Into<String>,
342 user_prompt: impl Into<String>,
343 ) -> Self {
344 Self {
345 task_id: task_id.into(),
346 model: model.into(),
347 system_prompt: system_prompt.into(),
348 user_prompt: user_prompt.into(),
349 max_cycles: 8,
350 memory_compact_threshold: 128_000,
351 memory_threshold_percentage: 90,
352 no_tool_policy: NoToolPolicy::Continue,
353 allow_interruption: true,
354 use_workspace: true,
355 has_sub_agents: false,
356 sub_agents: BTreeMap::new(),
357 agent_type: None,
358 native_multimodal: false,
359 extra_tool_names: Vec::new(),
360 exclude_tools: Vec::new(),
361 initial_messages: Vec::new(),
362 initial_shared_state: Metadata::new(),
363 model_settings: None,
364 metadata: Metadata::new(),
365 }
366 }
367
368 pub fn sub_agents_enabled(&self) -> bool {
369 self.has_sub_agents || !self.sub_agents.is_empty()
370 }
371}
372
373#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
374pub struct SubTaskRequest {
375 pub agent_name: String,
376 pub task_description: String,
377 pub output_requirements: String,
378 pub include_main_summary: bool,
379 pub exclude_files_pattern: Option<String>,
380 pub metadata: Metadata,
381}
382
383impl SubTaskRequest {
384 pub fn new(agent_name: impl Into<String>, task_description: impl Into<String>) -> Self {
385 Self {
386 agent_name: agent_name.into(),
387 task_description: task_description.into(),
388 output_requirements: String::new(),
389 include_main_summary: false,
390 exclude_files_pattern: None,
391 metadata: Metadata::new(),
392 }
393 }
394}
395
396#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
397pub struct SubTaskOutcome {
398 pub task_id: String,
399 pub agent_name: String,
400 pub status: AgentStatus,
401 pub session_id: Option<String>,
402 pub final_answer: Option<String>,
403 pub wait_reason: Option<String>,
404 pub error: Option<String>,
405 #[serde(default, skip_serializing_if = "Option::is_none")]
406 pub error_code: Option<String>,
407 #[serde(default, skip_serializing_if = "Option::is_none")]
408 pub completion_reason: Option<CompletionReason>,
409 #[serde(default, skip_serializing_if = "Option::is_none")]
410 pub completion_tool_name: Option<String>,
411 #[serde(default, skip_serializing_if = "Option::is_none")]
412 pub partial_output: Option<String>,
413 pub cycles: u32,
414 pub todo_list: Vec<Value>,
415 pub resolved: BTreeMap<String, String>,
416}
417
418impl Default for SubTaskOutcome {
419 fn default() -> Self {
420 Self {
421 task_id: String::new(),
422 agent_name: String::new(),
423 status: AgentStatus::Pending,
424 session_id: None,
425 final_answer: None,
426 wait_reason: None,
427 error: None,
428 error_code: None,
429 completion_reason: None,
430 completion_tool_name: None,
431 partial_output: None,
432 cycles: 0,
433 todo_list: Vec::new(),
434 resolved: BTreeMap::new(),
435 }
436 }
437}
438
439impl SubTaskOutcome {
440 pub fn to_dict(&self) -> Value {
441 self.to_value()
442 }
443
444 pub fn to_value(&self) -> Value {
445 json_value_from_serializable(self)
446 }
447}