1use super::*;
2
3pub(super) struct ParallelToolOptions<'a> {
4 pub(super) parent_session_id: Option<&'a str>,
5 pub(super) timeout_ms: Option<u64>,
6 pub(super) min_success_count: Option<usize>,
7 pub(super) allow_partial_failure: bool,
8 pub(super) parent_cancellation: Option<&'a CancellationToken>,
9}
10
11impl TaskExecutor {
12 pub async fn execute_parallel(
20 self: &Arc<Self>,
21 tasks: Vec<TaskParams>,
22 event_tx: Option<broadcast::Sender<AgentEvent>>,
23 parent_session_id: Option<&str>,
24 ) -> Vec<TaskResult> {
25 self.execute_parallel_with_parent_cancellation(
26 tasks,
27 event_tx,
28 parent_session_id,
29 self.parent_cancellation.as_ref(),
30 )
31 .await
32 }
33
34 async fn execute_parallel_with_parent_cancellation(
35 self: &Arc<Self>,
36 tasks: Vec<TaskParams>,
37 event_tx: Option<broadcast::Sender<AgentEvent>>,
38 parent_session_id: Option<&str>,
39 parent_cancellation: Option<&CancellationToken>,
40 ) -> Vec<TaskResult> {
41 let parent = parent_session_id.map(|s| s.to_string());
42 let specs = tasks
43 .into_iter()
44 .map(|params| AgentStepSpec {
45 task_id: format!("task-{}", uuid::Uuid::new_v4()),
46 agent: params.agent,
47 description: params.description,
48 prompt: params.prompt,
49 max_steps: params.max_steps,
50 parent_session_id: parent.clone(),
51 output_schema: params.output_schema,
52 })
53 .collect();
54
55 let executor: Arc<dyn AgentExecutor> = match parent_cancellation {
56 Some(cancellation) => Arc::new(ScopedTaskExecutor {
57 executor: Arc::clone(self),
58 parent_cancellation: cancellation.clone(),
59 }),
60 None => Arc::<Self>::clone(self),
61 };
62 crate::orchestration::execute_steps_parallel(executor, specs, event_tx)
63 .await
64 .into_iter()
65 .map(TaskResult::from)
66 .collect()
67 }
68
69 pub(super) async fn execute_parallel_for_tool(
70 self: &Arc<Self>,
71 tasks: Vec<TaskParams>,
72 event_tx: Option<broadcast::Sender<AgentEvent>>,
73 options: ParallelToolOptions<'_>,
74 ) -> ParallelTaskRun {
75 let ParallelToolOptions {
76 parent_session_id,
77 timeout_ms,
78 min_success_count,
79 allow_partial_failure,
80 parent_cancellation,
81 } = options;
82 let parallel_cancellation = parent_cancellation
83 .map(CancellationToken::child_token)
84 .unwrap_or_default();
85 let should_return_early = allow_partial_failure && min_success_count.is_some();
86 if timeout_ms.is_none() && !should_return_early {
87 return ParallelTaskRun {
88 results: self
89 .execute_parallel_with_parent_cancellation(
90 tasks,
91 event_tx,
92 parent_session_id,
93 Some(¶llel_cancellation),
94 )
95 .await,
96 timed_out: false,
97 returned_early: false,
98 timeout_ms: None,
99 min_success_count: None,
100 };
101 }
102
103 let task_count = tasks.len();
104 let parent = parent_session_id.map(ToString::to_string);
105 let specs = tasks
106 .into_iter()
107 .map(|params| AgentStepSpec {
108 task_id: format!("task-{}", uuid::Uuid::new_v4()),
109 agent: params.agent,
110 description: params.description,
111 prompt: params.prompt,
112 max_steps: params.max_steps,
113 parent_session_id: parent.clone(),
114 output_schema: params.output_schema,
115 })
116 .collect::<Vec<_>>();
117 let labels = specs
118 .iter()
119 .map(|spec| (spec.task_id.clone(), spec.agent.clone()))
120 .collect::<Vec<_>>();
121 let target_successes = min_success_count
122 .unwrap_or(task_count)
123 .clamp(1, task_count.max(1));
124
125 let max_concurrency = self.max_parallel_tasks.max(1);
126 let scoped_executor: Arc<dyn AgentExecutor> = Arc::new(ScopedTaskExecutor {
127 executor: Arc::clone(self),
128 parent_cancellation: parallel_cancellation.clone(),
129 });
130 let mut pending = specs.into_iter().enumerate();
131 let mut join_set = JoinSet::new();
132 let mut active_count = 0usize;
133 while active_count < max_concurrency {
134 let Some((index, spec)) = pending.next() else {
135 break;
136 };
137 spawn_parallel_task_step(
138 &mut join_set,
139 Arc::clone(&scoped_executor),
140 event_tx.clone(),
141 index,
142 spec,
143 );
144 active_count += 1;
145 }
146
147 let mut results: Vec<Option<TaskResult>> = vec![None; task_count];
148 let mut completed_count = 0usize;
149 let mut success_count = 0usize;
150 let mut timed_out = false;
151 let mut returned_early = false;
152 let deadline = timeout_ms.map(|timeout| {
153 tokio::time::Instant::now() + std::time::Duration::from_millis(timeout.max(1))
154 });
155
156 while completed_count < task_count {
157 if should_return_early && success_count >= target_successes {
158 returned_early = true;
159 break;
160 }
161
162 let next = match deadline {
163 Some(deadline) => {
164 tokio::select! {
165 result = join_set.join_next() => result,
166 _ = tokio::time::sleep_until(deadline) => {
167 timed_out = true;
168 break;
169 }
170 }
171 }
172 None => join_set.join_next().await,
173 };
174
175 let Some(joined) = next else {
176 break;
177 };
178 active_count = active_count.saturating_sub(1);
179 let (index, outcome) = match joined {
180 Ok((index, Ok(outcome))) => (index, outcome),
181 Ok((index, Err(error))) => {
182 let (task_id, agent) = labels
183 .get(index)
184 .cloned()
185 .unwrap_or_else(|| ("unknown".to_string(), "unknown".to_string()));
186 (index, StepOutcome::failed(task_id, agent, error))
187 }
188 Err(error) => {
189 let index = completed_count;
190 let (task_id, agent) = labels
191 .get(index)
192 .cloned()
193 .unwrap_or_else(|| ("unknown".to_string(), "unknown".to_string()));
194 (
195 index,
196 StepOutcome::failed(task_id, agent, error.to_string()),
197 )
198 }
199 };
200 if index >= task_count || results[index].is_some() {
201 continue;
202 }
203 if outcome.success {
204 success_count += 1;
205 }
206 results[index] = Some(TaskResult::from(outcome));
207 completed_count += 1;
208
209 if should_return_early && success_count >= target_successes {
210 returned_early = true;
211 break;
212 }
213
214 while active_count < max_concurrency {
215 let Some((index, spec)) = pending.next() else {
216 break;
217 };
218 spawn_parallel_task_step(
219 &mut join_set,
220 Arc::clone(&scoped_executor),
221 event_tx.clone(),
222 index,
223 spec,
224 );
225 active_count += 1;
226 }
227 }
228
229 if timed_out || returned_early || active_count > 0 {
230 parallel_cancellation.cancel();
231 join_set.abort_all();
232 }
233
234 let unfinished_message = if timed_out {
235 format!(
236 "Task timed out before parallel_task finished collecting child results after {} ms.",
237 timeout_ms.unwrap_or_default()
238 )
239 } else if returned_early {
240 format!(
241 "Task cancelled after parallel_task collected {success_count} successful child result(s)."
242 )
243 } else {
244 "Task did not return a result before parallel_task ended.".to_string()
245 };
246 let results = results
247 .into_iter()
248 .enumerate()
249 .map(|(index, result)| {
250 result.unwrap_or_else(|| {
251 let (task_id, agent) = labels
252 .get(index)
253 .cloned()
254 .unwrap_or_else(|| ("unknown".to_string(), "unknown".to_string()));
255 TaskResult::from(StepOutcome::failed(
256 task_id,
257 agent,
258 unfinished_message.clone(),
259 ))
260 })
261 })
262 .collect();
263
264 ParallelTaskRun {
265 results,
266 timed_out,
267 returned_early,
268 timeout_ms,
269 min_success_count,
270 }
271 }
272}
273
274fn spawn_parallel_task_step(
275 join_set: &mut JoinSet<(usize, std::result::Result<StepOutcome, String>)>,
276 executor: Arc<dyn AgentExecutor>,
277 event_tx: Option<broadcast::Sender<AgentEvent>>,
278 index: usize,
279 spec: AgentStepSpec,
280) {
281 join_set.spawn(async move {
282 let outcome = AssertUnwindSafe(executor.execute_step(spec, event_tx))
283 .catch_unwind()
284 .await
285 .map_err(panic_payload_to_string);
286 (index, outcome)
287 });
288}
289
290fn panic_payload_to_string(payload: Box<dyn Any + Send>) -> String {
291 if let Some(message) = payload.downcast_ref::<&str>() {
292 return format!("parallel branch panicked: {message}");
293 }
294 if let Some(message) = payload.downcast_ref::<String>() {
295 return format!("parallel branch panicked: {message}");
296 }
297 "parallel branch panicked: unknown panic payload".to_string()
298}
299
300pub(super) struct ParallelTaskRun {
301 pub(super) results: Vec<TaskResult>,
302 pub(super) timed_out: bool,
303 pub(super) returned_early: bool,
304 pub(super) timeout_ms: Option<u64>,
305 pub(super) min_success_count: Option<usize>,
306}
307
308impl From<TaskResult> for StepOutcome {
309 fn from(r: TaskResult) -> Self {
310 StepOutcome {
311 task_id: r.task_id,
312 session_id: r.session_id,
313 agent: r.agent,
314 output: r.output,
315 success: r.success,
316 structured: r.structured,
317 source_anchors: r.source_anchors,
318 }
319 }
320}
321
322impl From<StepOutcome> for TaskResult {
323 fn from(o: StepOutcome) -> Self {
324 TaskResult {
325 output: o.output,
326 session_id: o.session_id,
327 agent: o.agent,
328 success: o.success,
329 task_id: o.task_id,
330 structured: o.structured,
331 source_anchors: o.source_anchors,
332 }
333 }
334}
335
336#[async_trait]
340impl AgentExecutor for TaskExecutor {
341 async fn execute_step(
342 &self,
343 spec: AgentStepSpec,
344 event_tx: Option<broadcast::Sender<AgentEvent>>,
345 ) -> StepOutcome {
346 self.execute_step_with_parent_cancellation(
347 spec,
348 event_tx,
349 self.parent_cancellation.as_ref(),
350 )
351 .await
352 }
353
354 fn concurrency_hint(&self) -> usize {
355 self.max_parallel_tasks
356 }
357}
358
359impl TaskExecutor {
360 async fn execute_step_with_parent_cancellation(
361 &self,
362 spec: AgentStepSpec,
363 event_tx: Option<broadcast::Sender<AgentEvent>>,
364 parent_cancellation: Option<&CancellationToken>,
365 ) -> StepOutcome {
366 let agent = spec.agent.clone();
367 let task_id = spec.task_id.clone();
368 let params = TaskParams {
369 agent: spec.agent,
370 description: spec.description,
371 prompt: spec.prompt,
372 background: false,
373 max_steps: spec.max_steps,
374 output_schema: spec.output_schema,
375 };
376 match self
377 .execute_with_task_id_scoped(
378 task_id.clone(),
379 params,
380 event_tx,
381 spec.parent_session_id.as_deref(),
382 true,
383 parent_cancellation,
384 )
385 .await
386 {
387 Ok(result) => result.into(),
388 Err(e) => StepOutcome::failed(task_id, agent, format!("Task failed: {e}")),
389 }
390 }
391
392 pub(super) async fn coerce_to_schema(
396 llm_client: &dyn LlmClient,
397 output: &str,
398 schema: serde_json::Value,
399 cancellation: &CancellationToken,
400 ) -> Result<serde_json::Value> {
401 let req = StructuredRequest {
402 prompt: format!(
403 "Convert the following task result into a single JSON object that conforms to \
404 the required schema. Use only information present in the result.\n\n\
405 --- TASK RESULT ---\n{output}"
406 ),
407 system: Some(
408 "You output exactly one JSON object matching the provided schema.".to_string(),
409 ),
410 schema,
411 schema_name: "step_output".to_string(),
412 schema_description: None,
413 mode: StructuredMode::Tool,
416 max_repair_attempts: 2,
417 };
418 let result = tokio::select! {
419 biased;
420 _ = cancellation.cancelled() => anyhow::bail!("Operation cancelled by user"),
421 result = generate_blocking(llm_client, &req) => result?,
422 };
423 Ok(result.object)
424 }
425}
426
427struct ScopedTaskExecutor {
428 executor: Arc<TaskExecutor>,
429 parent_cancellation: CancellationToken,
430}
431
432#[async_trait]
433impl AgentExecutor for ScopedTaskExecutor {
434 async fn execute_step(
435 &self,
436 spec: AgentStepSpec,
437 event_tx: Option<broadcast::Sender<AgentEvent>>,
438 ) -> StepOutcome {
439 self.executor
440 .execute_step_with_parent_cancellation(spec, event_tx, Some(&self.parent_cancellation))
441 .await
442 }
443
444 fn concurrency_hint(&self) -> usize {
445 self.executor.max_parallel_tasks
446 }
447}