1use std::future::Future;
17use std::panic::AssertUnwindSafe;
18use std::pin::Pin;
19
20use futures_util::future::BoxFuture;
21use futures_util::FutureExt;
22use tokio::sync::mpsc;
23use tokio_util::sync::CancellationToken;
24
25use crate::event::SessionId;
26use crate::tool::ToolResult;
27
28pub type AsyncTaskWork = BoxFuture<'static, ToolResult>;
33
34pub struct AsyncTaskDone {
37 pub task_id: String,
38 pub tool_call_id: String,
39 pub session_id: SessionId,
40 pub tool_name: String,
41 pub result: ToolResult,
42 pub cancelled: bool,
46}
47
48#[derive(Clone)]
54pub struct AsyncTaskRunner {
55 done_tx: mpsc::Sender<AsyncTaskDone>,
56}
57
58impl AsyncTaskRunner {
59 pub fn new(done_tx: mpsc::Sender<AsyncTaskDone>) -> Self {
61 Self { done_tx }
62 }
63
64 pub fn submit(
73 &self,
74 tool_call_id: String,
75 session_id: SessionId,
76 tool_name: String,
77 work: AsyncTaskWork,
78 cancel: CancellationToken,
79 ) -> String {
80 let task_id = uuid::Uuid::new_v4().to_string();
81 let done_tx = self.done_tx.clone();
82 let tid = task_id.clone();
83 let cancelled_msg = format!("异步任务 {} 已取消", tid);
84
85 tokio::spawn(async move {
86 let (result, cancelled) = tokio::select! {
98 biased;
99 _ = cancel.cancelled() => (ToolResult::error(cancelled_msg), true),
100 r = AssertUnwindSafe(work).catch_unwind() => match r {
101 Ok(tool_result) => (tool_result, false),
102 Err(panic_payload) => {
103 let msg = panic_payload_to_string(&panic_payload);
104 tracing::error!(
105 "[async] background task panicked: task_id={}, tool={}, session={}, panic={}",
106 tid, tool_name, session_id, msg
107 );
108 (
109 ToolResult::error(format!("异步任务执行时发生 panic: {}", msg)),
110 false,
111 )
112 }
113 },
114 };
115
116 let done = AsyncTaskDone {
117 task_id: tid,
118 tool_call_id,
119 session_id,
120 tool_name,
121 result,
122 cancelled,
123 };
124 if let Err(send_err) = done_tx.send(done).await {
127 let err_msg = send_err.to_string();
130 let lost = send_err.0;
131 tracing::error!(
132 "[async] FAILED to deliver task result to Agent (Agent likely exited): \
133 task_id={}, tool={}, session={}, cancelled={}, error={}. \
134 The task result is permanently lost.",
135 lost.task_id, lost.tool_name, lost.session_id, lost.cancelled, err_msg
136 );
137 }
138 });
139
140 task_id
141 }
142}
143
144fn panic_payload_to_string(payload: &Box<dyn std::any::Any + Send>) -> String {
150 if let Some(s) = payload.downcast_ref::<&str>() {
151 (*s).to_string()
152 } else if let Some(s) = payload.downcast_ref::<String>() {
153 s.clone()
154 } else {
155 "<non-string panic payload>".to_string()
156 }
157}
158
159#[allow(dead_code)]
162type _AnyFuture = Pin<Box<dyn Future<Output = ToolResult> + Send>>;
163
164#[cfg(test)]
165mod tests {
166 use super::*;
167 use std::sync::Arc;
168 use std::sync::Mutex as StdMutex;
169 use tokio::sync::Mutex;
170
171 async fn recv_n(rx: &Mutex<mpsc::Receiver<AsyncTaskDone>>, n: usize) -> Vec<AsyncTaskDone> {
172 let mut out = Vec::new();
173 for _ in 0..n {
174 match rx.lock().await.recv().await {
175 Some(d) => out.push(d),
176 None => break,
177 }
178 }
179 out
180 }
181
182 #[tokio::test]
183 async fn submit_returns_completed_result() {
184 let (tx, rx) = mpsc::channel(8);
185 let runner = AsyncTaskRunner::new(tx);
186 let counter: Arc<StdMutex<u32>> = Arc::new(StdMutex::new(0));
187 let c = counter.clone();
188 let task_id = runner.submit(
189 "tc1".into(),
190 "sess".into(),
191 "test_tool".into(),
192 Box::pin(async move {
193 *c.lock().unwrap() += 1;
194 ToolResult::success("done")
195 }),
196 CancellationToken::new(),
197 );
198 assert!(!task_id.is_empty());
199
200 let results = recv_n(&Mutex::new(rx), 1).await;
201 assert_eq!(results.len(), 1);
202 assert_eq!(results[0].task_id, task_id);
203 assert!(!results[0].result.is_error);
204 assert_eq!(results[0].result.content, "done");
205 assert_eq!(*counter.lock().unwrap(), 1);
206 }
207
208 #[tokio::test]
209 async fn cancel_returns_cancelled_result() {
210 let (tx, rx) = mpsc::channel(8);
211 let runner = AsyncTaskRunner::new(tx);
212 let cancel = CancellationToken::new();
213 let started = Arc::new(tokio::sync::Notify::new());
214 let started2 = started.clone();
215 let task_id = runner.submit(
216 "tc2".into(),
217 "sess".into(),
218 "test_tool".into(),
219 Box::pin(async move {
220 started2.notify_one();
221 tokio::time::sleep(std::time::Duration::from_secs(60)).await;
223 ToolResult::success("should not reach")
224 }),
225 cancel.clone(),
226 );
227
228 started.notified().await;
229 cancel.cancel();
230
231 let results = recv_n(&Mutex::new(rx), 1).await;
232 assert_eq!(results.len(), 1);
233 assert_eq!(results[0].task_id, task_id);
234 assert!(results[0].result.is_error);
235 assert!(results[0].result.content.contains("已取消"));
236 assert!(results[0].cancelled);
237 }
238
239 #[tokio::test]
240 async fn panicking_work_delivers_error_result() {
241 let (tx, rx) = mpsc::channel(8);
245 let runner = AsyncTaskRunner::new(tx);
246 let task_id = runner.submit(
247 "tc3".into(),
248 "sess".into(),
249 "test_tool".into(),
250 Box::pin(async {
251 panic!("boom from inside work");
252 }),
253 CancellationToken::new(),
254 );
255
256 let results = recv_n(&Mutex::new(rx), 1).await;
257 assert_eq!(results.len(), 1, "panic should still deliver a result");
258 assert_eq!(results[0].task_id, task_id);
259 assert!(results[0].result.is_error);
260 assert!(
261 results[0].result.content.contains("panic"),
262 "content was: {}",
263 results[0].result.content
264 );
265 assert!(
266 results[0].result.content.contains("boom from inside work"),
267 "content was: {}",
268 results[0].result.content
269 );
270 assert!(!results[0].cancelled);
271 }
272
273 #[test]
274 fn panic_payload_to_string_handles_common_payloads() {
275 let s: Box<dyn std::any::Any + Send> = Box::new("static literal");
276 assert_eq!(panic_payload_to_string(&s), "static literal");
277
278 let s: Box<dyn std::any::Any + Send> = Box::new("owned".to_string());
279 assert_eq!(panic_payload_to_string(&s), "owned");
280
281 let s: Box<dyn std::any::Any + Send> = Box::new(42i32);
282 assert_eq!(panic_payload_to_string(&s), "<non-string panic payload>");
283 }
284}