Skip to main content

vv_agent/types/
tasks.rs

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}