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