vv_agent/runtime/sub_task_manager/
record.rs1use 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;
13
14pub struct ManagedSubTask {
15 pub(super) task_id: String,
16 pub(super) session_id: String,
17 pub(super) agent_name: String,
18 pub(super) task_title: String,
19 pub(super) workspace_backend: Option<Arc<dyn WorkspaceBackend>>,
20 pub(super) session: Option<Arc<dyn SubAgentSession>>,
21 pub(super) outcome: Option<SubTaskOutcome>,
22 pub(super) resolved: BTreeMap<String, String>,
23 pub(super) current_cycle_index: Option<u32>,
24 pub(super) recent_activity: Option<String>,
25 pub(super) latest_cycle: Option<Value>,
26 pub(super) latest_tool_call: Option<Value>,
27 pub(super) handle: Option<JoinHandle<()>>,
28 pub(super) updated_at: String,
29 pub(super) manager_listener_attached: bool,
30}
31
32impl ManagedSubTask {
33 pub(super) fn is_running(&self) -> bool {
34 self.handle
35 .as_ref()
36 .is_some_and(|handle| !handle.is_finished())
37 }
38
39 pub(super) fn to_status_entry(&self, detail_level: &str, workspace_file_limit: usize) -> Value {
40 let status = self.status_label();
41 let mut entry = json!({
42 "task_id": self.task_id,
43 "session_id": self.session_id,
44 "agent_name": self.agent_name,
45 "status": status,
46 "task_description": self.task_title,
47 });
48 if let Some(outcome) = &self.outcome {
49 if let Some(final_answer) = &outcome.final_answer {
50 entry["final_answer"] = Value::String(final_answer.clone());
51 }
52 if let Some(wait_reason) = &outcome.wait_reason {
53 entry["wait_reason"] = Value::String(wait_reason.clone());
54 }
55 if let Some(error) = &outcome.error {
56 entry["error"] = Value::String(error.clone());
57 }
58 if outcome.cycles > 0 {
59 entry["cycles"] = Value::Number(outcome.cycles.into());
60 }
61 if !outcome.todo_list.is_empty() {
62 entry["todo_list"] = Value::Array(outcome.todo_list.clone());
63 }
64 if !outcome.resolved.is_empty() {
65 entry["resolved"] = json!(outcome.resolved);
66 }
67 }
68 if detail_level == "snapshot" {
69 let recent_activity = self
70 .recent_activity
71 .clone()
72 .or_else(|| {
73 self.outcome.as_ref().and_then(|outcome| {
74 outcome
75 .final_answer
76 .clone()
77 .or_else(|| outcome.wait_reason.clone())
78 .or_else(|| outcome.error.clone())
79 })
80 })
81 .unwrap_or_else(|| self.task_title.clone());
82 let workspace_snapshot = self.workspace_snapshot(workspace_file_limit);
83 entry["snapshot"] = json!({
84 "current_cycle_index": self.current_cycle_index,
85 "task_title": self.task_title,
86 "recent_activity": recent_activity,
87 "updated_at": self.updated_at,
88 "workspace_files": workspace_snapshot.files,
89 "workspace_file_count": workspace_snapshot.file_count,
90 "workspace_files_truncated": workspace_snapshot.truncated,
91 });
92 if let Some(latest_cycle) = &self.latest_cycle {
93 entry["snapshot"]["latest_cycle"] = latest_cycle.clone();
94 }
95 if let Some(latest_tool_call) = &self.latest_tool_call {
96 entry["snapshot"]["latest_tool_call"] = latest_tool_call.clone();
97 }
98 }
99 entry
100 }
101
102 pub(super) fn snapshot(&self) -> ManagedSubTaskSnapshot {
103 ManagedSubTaskSnapshot {
104 task_id: self.task_id.clone(),
105 session_id: self.session_id.clone(),
106 agent_name: self.agent_name.clone(),
107 task_title: self.task_title.clone(),
108 status: self.status_label().to_string(),
109 running: self.is_running(),
110 outcome: self.outcome.clone(),
111 resolved: self.resolved.clone(),
112 current_cycle_index: self.current_cycle_index,
113 recent_activity: self.recent_activity.clone(),
114 latest_cycle: self.latest_cycle.clone(),
115 latest_tool_call: self.latest_tool_call.clone(),
116 updated_at: self.updated_at.clone(),
117 }
118 }
119
120 pub(super) fn status_label(&self) -> &'static str {
121 self.outcome
122 .as_ref()
123 .map(|outcome| status_label(outcome.status))
124 .unwrap_or_else(|| {
125 if self.is_running() {
126 "running"
127 } else {
128 "pending"
129 }
130 })
131 }
132
133 pub(super) fn update_from_outcome(&mut self, outcome: &SubTaskOutcome) {
134 if outcome.cycles > 0 {
135 self.current_cycle_index = Some(outcome.cycles);
136 }
137 self.set_latest_cycle_status(status_label(outcome.status));
138 if let Some(detail) = outcome
139 .final_answer
140 .as_ref()
141 .or(outcome.wait_reason.as_ref())
142 .or(outcome.error.as_ref())
143 .and_then(|value| preview_text(Some(&Value::String(value.clone()))))
144 {
145 self.recent_activity = Some(detail);
146 }
147 }
148
149 pub(super) fn set_latest_cycle_status(&mut self, status: &str) {
150 let mut latest_cycle = self
151 .latest_cycle
152 .as_ref()
153 .and_then(Value::as_object)
154 .cloned()
155 .unwrap_or_default();
156 latest_cycle.insert("status".to_string(), Value::String(status.to_string()));
157 if let Some(current_cycle_index) = self.current_cycle_index {
158 latest_cycle
159 .entry("cycle_index".to_string())
160 .or_insert_with(|| Value::from(current_cycle_index));
161 }
162 self.latest_cycle = Some(Value::Object(latest_cycle));
163 }
164
165 pub(super) fn mark_terminal_state(&mut self, status: &str, detail: Option<&str>) {
166 self.set_latest_cycle_status(status);
167 if let Some(detail) = detail {
168 self.recent_activity = Some(detail.to_string());
169 }
170 }
171
172 fn workspace_snapshot(&self, workspace_file_limit: usize) -> WorkspaceSnapshot {
173 let Some(workspace_backend) = &self.workspace_backend else {
174 return WorkspaceSnapshot::default();
175 };
176 let Ok(raw_files) = workspace_backend.list_files(".", "**/*") else {
177 return WorkspaceSnapshot::default();
178 };
179 let visible_files = raw_files
180 .into_iter()
181 .filter(|path| !is_internal_workspace_file(path))
182 .collect::<Vec<_>>();
183 let file_count = visible_files.len();
184 WorkspaceSnapshot {
185 files: visible_files
186 .into_iter()
187 .take(workspace_file_limit)
188 .collect::<Vec<_>>(),
189 file_count,
190 truncated: file_count > workspace_file_limit,
191 }
192 }
193}
194
195#[derive(Default)]
196struct WorkspaceSnapshot {
197 files: Vec<String>,
198 file_count: usize,
199 truncated: bool,
200}