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