Skip to main content

vv_agent/runtime/sub_task_manager/
record.rs

1use std::collections::BTreeMap;
2use std::sync::Arc;
3use std::thread::JoinHandle;
4
5use serde_json::{json, Value};
6
7use crate::runtime::sub_agent_sessions::SubAgentSession;
8use crate::types::SubTaskOutcome;
9use crate::workspace::WorkspaceBackend;
10
11use super::helpers::{is_internal_workspace_file, preview_text, status_label};
12use super::types::{ManagedSubTaskSnapshot, SubTaskLineage};
13
14#[derive(Clone)]
15pub(super) struct ManagedSubAgentSession {
16    pub(super) session: Arc<dyn SubAgentSession>,
17    pub(super) initial_lineage: SubTaskLineage,
18}
19
20#[derive(Clone)]
21pub(super) struct ContinuationAdmissionState {
22    task_title: String,
23    outcome: Option<SubTaskOutcome>,
24    recent_activity: Option<String>,
25    parent_run_id: Option<String>,
26    parent_tool_call_id: Option<String>,
27    running: bool,
28    worker_owned_run: bool,
29    updated_at: String,
30}
31
32pub struct ManagedSubTask {
33    pub(super) task_id: String,
34    pub(super) session_id: String,
35    pub(super) agent_name: String,
36    pub(super) task_title: String,
37    pub(super) workspace_backend: Option<Arc<dyn WorkspaceBackend>>,
38    pub(super) session: Option<ManagedSubAgentSession>,
39    pub(super) outcome: Option<SubTaskOutcome>,
40    pub(super) resolved: BTreeMap<String, String>,
41    pub(super) current_cycle_index: Option<u32>,
42    pub(super) recent_activity: Option<String>,
43    pub(super) latest_cycle: Option<Value>,
44    pub(super) latest_tool_call: Option<Value>,
45    pub(super) parent_run_id: Option<String>,
46    pub(super) parent_tool_call_id: Option<String>,
47    pub(super) running: bool,
48    pub(super) worker_owned_run: bool,
49    pub(super) handle: Option<JoinHandle<()>>,
50    pub(super) updated_at: String,
51    pub(super) session_generation: u64,
52    pub(super) manager_listener_generation: Option<u64>,
53}
54
55impl ManagedSubTask {
56    pub(super) fn is_running(&self) -> bool {
57        self.running
58    }
59
60    pub(super) fn to_status_entry(&self, detail_level: &str, workspace_file_limit: usize) -> Value {
61        let status = self.status_label();
62        let mut entry = json!({
63            "task_id": self.task_id,
64            "session_id": self.session_id,
65            "agent_name": self.agent_name,
66            "status": status,
67        });
68        if !self.task_title.is_empty() {
69            entry["task_description"] = Value::String(self.task_title.clone());
70        }
71        if !self.is_running() {
72            if let Some(outcome) = &self.outcome {
73                if let Some(final_answer) = &outcome.final_answer {
74                    entry["final_answer"] = Value::String(final_answer.clone());
75                }
76                if let Some(wait_reason) = &outcome.wait_reason {
77                    entry["wait_reason"] = Value::String(wait_reason.clone());
78                }
79                if let Some(error) = &outcome.error {
80                    entry["error"] = Value::String(error.clone());
81                }
82                if let Some(error_code) = &outcome.error_code {
83                    entry["error_code"] = Value::String(error_code.clone());
84                }
85                if let Some(completion_reason) = outcome.completion_reason {
86                    entry["completion_reason"] =
87                        Value::String(completion_reason.as_str().to_string());
88                }
89                if let Some(completion_tool_name) = &outcome.completion_tool_name {
90                    entry["completion_tool_name"] = Value::String(completion_tool_name.clone());
91                }
92                if let Some(partial_output) = &outcome.partial_output {
93                    entry["partial_output"] = Value::String(partial_output.clone());
94                }
95                if outcome.cycles > 0 {
96                    entry["cycles"] = Value::Number(outcome.cycles.into());
97                }
98                if !outcome.todo_list.is_empty() {
99                    entry["todo_list"] = Value::Array(outcome.todo_list.clone());
100                }
101                if !outcome.resolved.is_empty() {
102                    entry["resolved"] = json!(outcome.resolved);
103                }
104            }
105        }
106        if let Some(parent_run_id) = &self.parent_run_id {
107            entry["parent_run_id"] = Value::String(parent_run_id.clone());
108        }
109        if let Some(parent_tool_call_id) = &self.parent_tool_call_id {
110            entry["parent_tool_call_id"] = Value::String(parent_tool_call_id.clone());
111        }
112        if detail_level == "snapshot" {
113            let workspace_snapshot = self.workspace_snapshot(workspace_file_limit);
114            entry["snapshot"] = json!({
115                "current_cycle_index": self.current_cycle_index,
116                "updated_at": self.updated_at,
117                "workspace_files": workspace_snapshot.files,
118                "workspace_file_count": workspace_snapshot.file_count,
119                "workspace_files_truncated": workspace_snapshot.truncated,
120            });
121            if !self.task_title.is_empty() {
122                entry["snapshot"]["task_title"] = Value::String(self.task_title.clone());
123            }
124            if let Some(recent_activity) = &self.recent_activity {
125                entry["snapshot"]["recent_activity"] = Value::String(recent_activity.clone());
126            }
127            if let Some(latest_cycle) = &self.latest_cycle {
128                entry["snapshot"]["latest_cycle"] = latest_cycle.clone();
129            }
130            if let Some(latest_tool_call) = &self.latest_tool_call {
131                entry["snapshot"]["latest_tool_call"] = latest_tool_call.clone();
132            }
133        }
134        entry
135    }
136
137    pub(super) fn snapshot(&self) -> ManagedSubTaskSnapshot {
138        ManagedSubTaskSnapshot {
139            task_id: self.task_id.clone(),
140            session_id: self.session_id.clone(),
141            agent_name: self.agent_name.clone(),
142            task_title: self.task_title.clone(),
143            status: self.status_label().to_string(),
144            running: self.is_running(),
145            outcome: (!self.is_running()).then(|| self.outcome.clone()).flatten(),
146            resolved: self.resolved.clone(),
147            current_cycle_index: self.current_cycle_index,
148            recent_activity: self.recent_activity.clone(),
149            latest_cycle: self.latest_cycle.clone(),
150            latest_tool_call: self.latest_tool_call.clone(),
151            parent_run_id: self.parent_run_id.clone(),
152            parent_tool_call_id: self.parent_tool_call_id.clone(),
153            updated_at: self.updated_at.clone(),
154        }
155    }
156
157    pub(super) fn status_label(&self) -> &'static str {
158        if self.is_running() {
159            return "running";
160        }
161        self.outcome
162            .as_ref()
163            .map(|outcome| status_label(outcome.status))
164            .unwrap_or("pending")
165    }
166
167    pub(super) fn update_from_outcome(&mut self, outcome: &SubTaskOutcome) {
168        if outcome.cycles > 0 {
169            self.current_cycle_index = Some(outcome.cycles);
170        }
171        self.set_latest_cycle_status(status_label(outcome.status));
172        if let Some(detail) = outcome
173            .final_answer
174            .as_ref()
175            .or(outcome.wait_reason.as_ref())
176            .or(outcome.error.as_ref())
177            .and_then(|value| preview_text(Some(&Value::String(value.clone()))))
178        {
179            self.recent_activity = Some(detail);
180        }
181    }
182
183    pub(super) fn admit_continuation(
184        &mut self,
185        prompt: &str,
186        lineage: &SubTaskLineage,
187        updated_at: String,
188    ) -> ContinuationAdmissionState {
189        let previous = ContinuationAdmissionState {
190            task_title: self.task_title.clone(),
191            outcome: self.outcome.clone(),
192            recent_activity: self.recent_activity.clone(),
193            parent_run_id: self.parent_run_id.clone(),
194            parent_tool_call_id: self.parent_tool_call_id.clone(),
195            running: self.running,
196            worker_owned_run: self.worker_owned_run,
197            updated_at: self.updated_at.clone(),
198        };
199        self.task_title = prompt.to_string();
200        self.outcome = None;
201        self.recent_activity = Some(prompt.to_string());
202        self.parent_run_id = lineage.parent_run_id.clone();
203        self.parent_tool_call_id = lineage.parent_tool_call_id.clone();
204        self.running = true;
205        self.worker_owned_run = true;
206        self.updated_at = updated_at;
207        previous
208    }
209
210    pub(super) fn rollback_continuation(&mut self, previous: ContinuationAdmissionState) {
211        self.task_title = previous.task_title;
212        self.outcome = previous.outcome;
213        self.recent_activity = previous.recent_activity;
214        self.parent_run_id = previous.parent_run_id;
215        self.parent_tool_call_id = previous.parent_tool_call_id;
216        self.running = previous.running;
217        self.worker_owned_run = previous.worker_owned_run;
218        self.handle = None;
219        self.updated_at = previous.updated_at;
220    }
221
222    pub(super) fn set_latest_cycle_status(&mut self, status: &str) {
223        let mut latest_cycle = self
224            .latest_cycle
225            .as_ref()
226            .and_then(Value::as_object)
227            .cloned()
228            .unwrap_or_default();
229        latest_cycle.insert("status".to_string(), Value::String(status.to_string()));
230        if let Some(current_cycle_index) = self.current_cycle_index {
231            latest_cycle
232                .entry("cycle_index".to_string())
233                .or_insert_with(|| Value::from(current_cycle_index));
234        }
235        self.latest_cycle = Some(Value::Object(latest_cycle));
236    }
237
238    pub(super) fn mark_terminal_state(&mut self, status: &str, detail: Option<&str>) {
239        self.set_latest_cycle_status(status);
240        if let Some(detail) = detail {
241            self.recent_activity = Some(detail.to_string());
242        }
243    }
244
245    fn workspace_snapshot(&self, workspace_file_limit: usize) -> WorkspaceSnapshot {
246        let Some(workspace_backend) = &self.workspace_backend else {
247            return WorkspaceSnapshot::default();
248        };
249        let Ok(raw_files) = workspace_backend.list_files(".", "**/*") else {
250            return WorkspaceSnapshot::default();
251        };
252        let visible_files = raw_files
253            .into_iter()
254            .filter(|path| !is_internal_workspace_file(path))
255            .collect::<Vec<_>>();
256        let file_count = visible_files.len();
257        WorkspaceSnapshot {
258            files: visible_files
259                .into_iter()
260                .take(workspace_file_limit)
261                .collect::<Vec<_>>(),
262            file_count,
263            truncated: file_count > workspace_file_limit,
264        }
265    }
266}
267
268#[derive(Default)]
269struct WorkspaceSnapshot {
270    files: Vec<String>,
271    file_count: usize,
272    truncated: bool,
273}