zeph_subagent/manager/
collect.rs1use std::path::PathBuf;
5
6use zeph_config::SubAgentConfig;
7
8use super::SubAgentManager;
9use super::SubAgentStatus;
10use crate::def::SubAgentDef;
11use crate::error::SubAgentError;
12use crate::fleet::FleetSessionStatus;
13use crate::hooks::fire_hooks;
14use crate::manager::secrets::make_hook_env;
15use crate::state::SubAgentState;
16use crate::transcript::{TranscriptMeta, TranscriptWriter, sweep_old_transcripts};
17
18impl SubAgentManager {
19 #[tracing::instrument(name = "subagent.manager.collect", skip_all, fields(task_id = task_id))]
28 pub async fn collect(&mut self, task_id: &str) -> Result<String, SubAgentError> {
29 let mut handle = self
30 .agents
31 .remove(task_id)
32 .ok_or_else(|| SubAgentError::NotFound(task_id.to_owned()))?;
33
34 if !self.stop_hooks.is_empty() {
35 let stop_hooks = self.stop_hooks.clone();
36 let stop_env = make_hook_env(task_id, &handle.def.name, "");
37 self.spawn_hook_task(async move {
38 if let Err(e) = fire_hooks(&stop_hooks, &stop_env, None, None).await {
39 tracing::warn!(error = %e, "SubagentStop hook failed");
40 }
41 });
42 }
43
44 handle.grants_lock().revoke_all();
45
46 let result: Result<String, SubAgentError> = if let Some(jh) = handle.join_handle.take() {
52 match jh.join().await {
53 Ok(inner) => inner,
54 Err(e) => Err(SubAgentError::Spawn(e.to_string())),
55 }
56 } else {
57 Ok(String::new())
58 };
59
60 let final_state = {
61 let status = handle.status_rx.borrow();
62 if result.is_err() {
63 SubAgentState::Failed
64 } else if status.state == SubAgentState::Canceled {
65 SubAgentState::Canceled
66 } else {
67 SubAgentState::Completed
68 }
69 };
70
71 if let Some(ref registry) = self.fleet_registry {
72 let registry = std::sync::Arc::clone(registry);
73 let tid = task_id.to_owned();
74 let fleet_status = match final_state {
75 SubAgentState::Failed => FleetSessionStatus::Failed,
76 SubAgentState::Canceled => FleetSessionStatus::Cancelled,
77 _ => FleetSessionStatus::Completed,
78 };
79 self.spawn_hook_task(async move {
80 if let Err(e) = registry.mark_terminal(&tid, fleet_status).await {
81 tracing::warn!(error = %e, task_id = %tid, "fleet: mark_terminal failed");
82 }
83 });
84 }
85
86 if let Some(ref dir) = handle.transcript_dir.clone() {
87 let turns_used = handle.status_rx.borrow().turns_used;
88 let meta = TranscriptMeta {
89 agent_id: task_id.to_owned(),
90 agent_name: handle.def.name.clone(),
91 def_name: handle.def.name.clone(),
92 status: final_state,
93 started_at: handle.started_at_str.clone(),
94 finished_at: Some(crate::transcript::utc_now()),
95 resumed_from: None,
96 turns_used,
97 mcp_tool_names: handle.mcp_tool_names.clone(),
98 };
99 if let Err(e) = TranscriptWriter::write_meta_async(dir, task_id, &meta).await {
100 tracing::warn!(error = %e, task_id, "failed to write final transcript meta");
101 }
102 }
103
104 result
105 }
106
107 pub(crate) fn effective_transcript_dir(&self, config: &SubAgentConfig) -> PathBuf {
109 if let Some(ref dir) = self.transcript_dir {
110 dir.clone()
111 } else if let Some(ref dir) = config.transcript_dir {
112 dir.clone()
113 } else {
114 PathBuf::from(".zeph/subagents")
115 }
116 }
117
118 pub async fn def_name_for_resume(
128 &self,
129 id_prefix: &str,
130 config: &SubAgentConfig,
131 ) -> Result<String, SubAgentError> {
132 let dir = self.effective_transcript_dir(config);
133 let id_prefix = id_prefix.to_owned();
134 tokio::task::spawn_blocking(move || {
135 let original_id =
136 crate::transcript::TranscriptReader::find_by_prefix(&dir, &id_prefix)?;
137 let meta = crate::transcript::TranscriptReader::load_meta(&dir, &original_id)?;
138 Ok(meta.def_name)
139 })
140 .await
141 .map_err(|e| SubAgentError::Spawn(format!("spawn_blocking panicked: {e}")))?
142 }
143
144 #[must_use]
146 pub fn statuses(&self) -> Vec<(String, SubAgentStatus)> {
147 self.agents
148 .values()
149 .map(|h| {
150 let mut status = h.status_rx.borrow().clone();
151 if h.state == SubAgentState::Canceled {
152 status.state = SubAgentState::Canceled;
153 }
154 (h.task_id.clone(), status)
155 })
156 .collect()
157 }
158
159 #[must_use]
171 pub fn is_task_finished(&self, task_id: &str) -> bool {
172 self.agents
173 .get(task_id)
174 .and_then(|h| h.join_handle.as_ref())
175 .is_some_and(zeph_common::task_supervisor::BlockingHandle::is_finished)
176 }
177
178 #[must_use]
180 pub fn agents_def(&self, task_id: &str) -> Option<&SubAgentDef> {
181 self.agents.get(task_id).map(|h| &h.def)
182 }
183
184 #[must_use]
186 pub fn agent_transcript_dir(&self, task_id: &str) -> Option<&std::path::Path> {
187 self.agents
188 .get(task_id)
189 .and_then(|h| h.transcript_dir.as_deref())
190 }
191
192 #[must_use]
213 pub fn transcript_path_for(&self, config: &SubAgentConfig, agent_id: &str) -> PathBuf {
214 self.effective_transcript_dir(config)
215 .join(format!("{agent_id}.jsonl"))
216 }
217
218 pub(crate) fn create_transcript_writer(
225 &mut self,
226 config: &SubAgentConfig,
227 task_id: &str,
228 agent_name: &str,
229 resumed_from: Option<&str>,
230 ) -> Option<TranscriptWriter> {
231 if !config.transcript_enabled {
232 return None;
233 }
234 let dir = self.effective_transcript_dir(config);
235 let max_files = self.transcript_max_files;
236 let path = dir.join(format!("{task_id}.jsonl"));
237 let meta = TranscriptMeta {
238 agent_id: task_id.to_owned(),
239 agent_name: agent_name.to_owned(),
240 def_name: agent_name.to_owned(),
241 status: SubAgentState::Submitted,
242 started_at: crate::transcript::utc_now(),
243 finished_at: None,
244 resumed_from: resumed_from.map(str::to_owned),
245 turns_used: 0,
246 mcp_tool_names: Vec::new(),
247 };
248 let task_id = task_id.to_owned();
249 run_blocking(move || {
250 if max_files > 0
251 && let Err(e) = sweep_old_transcripts(&dir, max_files)
252 {
253 tracing::warn!(error = %e, "transcript sweep failed");
254 }
255 match TranscriptWriter::new(&path) {
256 Ok(w) => {
257 if let Err(e) = TranscriptWriter::write_meta(&dir, &task_id, &meta) {
258 tracing::warn!(error = %e, "failed to write initial transcript meta");
259 }
260 Some(w)
261 }
262 Err(e) => {
263 tracing::warn!(error = %e, "failed to create transcript writer");
264 None
265 }
266 }
267 })
268 }
269}
270
271fn run_blocking<T>(f: impl FnOnce() -> T) -> T {
278 if tokio::runtime::Handle::try_current()
279 .is_ok_and(|h| h.runtime_flavor() == tokio::runtime::RuntimeFlavor::MultiThread)
280 {
281 tokio::task::block_in_place(f)
282 } else {
283 f()
284 }
285}