1use std::collections::{BTreeMap, VecDeque};
2use std::sync::Arc;
3use std::sync::atomic::{AtomicU64, Ordering};
4use std::time::Duration;
5
6use agentkit_core::{
7 Item, MetadataMap, TaskId, ToolCallId, ToolResultPart, TurnCancellation, TurnId,
8};
9use agentkit_tools_core::{
10 ApprovalRequest, OwnedToolContext, ToolError, ToolExecutionOutcome, ToolExecutor, ToolRequest,
11};
12use async_trait::async_trait;
13use thiserror::Error;
14use tokio::sync::{Mutex, Notify, mpsc};
15use tokio::task::JoinHandle;
16
17pub const TOOL_RESULT_FAILURE_KIND_METADATA_KEY: &str = "agentkit.tool.failure_kind";
18pub const TOOL_RESULT_FAILURE_KIND_PERMISSION_DENIED: &str = "permission_denied";
19
20#[derive(Clone, Copy, Debug, PartialEq, Eq)]
21pub enum TaskKind {
22 Foreground,
23 Background,
24}
25
26#[derive(Clone, Copy, Debug, PartialEq, Eq)]
27pub enum ContinuePolicy {
28 NotifyOnly,
29 RequestContinue,
30}
31
32#[derive(Clone, Copy, Debug, PartialEq, Eq)]
33pub enum DeliveryMode {
34 ToLoop,
35 Manual,
36}
37
38#[derive(Clone, Debug, PartialEq, Eq)]
39pub struct TaskSnapshot {
40 pub id: TaskId,
41 pub turn_id: TurnId,
42 pub call_id: ToolCallId,
43 pub tool_name: String,
44 pub kind: TaskKind,
45 pub metadata: MetadataMap,
46}
47
48#[derive(Clone, Debug, PartialEq)]
49pub enum TaskEvent {
50 Started(TaskSnapshot),
51 Detached(TaskSnapshot),
52 Completed(TaskSnapshot, ToolResultPart),
53 Cancelled(TaskSnapshot),
54 Failed(TaskSnapshot, ToolError),
55 ContinueRequested,
56}
57
58#[derive(Clone, Debug, PartialEq)]
59pub struct TaskApproval {
60 pub task_id: TaskId,
61 pub tool_request: ToolRequest,
62 pub approval: ApprovalRequest,
63}
64
65#[derive(Clone, Debug, PartialEq)]
66pub enum TaskResolution {
67 Item(Item),
68 Approval(TaskApproval),
69}
70
71#[derive(Clone, Debug, PartialEq)]
72pub enum TaskStartOutcome {
73 Ready(Box<TaskResolution>),
74 Pending { task_id: TaskId, kind: TaskKind },
75}
76
77#[derive(Clone, Debug, PartialEq)]
78pub enum TurnTaskUpdate {
79 Resolution(Box<TaskResolution>),
80 Detached(TaskSnapshot),
81}
82
83#[derive(Clone, Debug, Default, PartialEq)]
84pub struct PendingLoopUpdates {
85 pub resolutions: VecDeque<TaskResolution>,
86}
87
88#[derive(Clone, Debug, Default)]
91pub enum TaskLaunchKind {
92 #[default]
94 Plain,
95 Approved(ApprovalRequest),
97}
98
99#[derive(Clone, Debug)]
100pub struct TaskLaunchRequest {
101 pub task_id: Option<TaskId>,
102 pub request: ToolRequest,
103 pub kind: TaskLaunchKind,
104}
105
106impl TaskLaunchRequest {
107 pub fn plain(task_id: Option<TaskId>, request: ToolRequest) -> Self {
109 Self {
110 task_id,
111 request,
112 kind: TaskLaunchKind::Plain,
113 }
114 }
115
116 pub fn approved(
118 task_id: Option<TaskId>,
119 request: ToolRequest,
120 approval: ApprovalRequest,
121 ) -> Self {
122 Self {
123 task_id,
124 request,
125 kind: TaskLaunchKind::Approved(approval),
126 }
127 }
128}
129
130#[derive(Clone)]
131pub struct TaskStartContext {
132 pub executor: Arc<dyn ToolExecutor>,
133 pub tool_context: OwnedToolContext,
134}
135
136#[derive(Debug, Error, Clone, PartialEq, Eq)]
137pub enum TaskManagerError {
138 #[error("task not found: {0}")]
139 NotFound(TaskId),
140 #[error("task manager internal error: {0}")]
141 Internal(String),
142}
143
144pub trait TaskRoutingPolicy: Send + Sync {
145 fn route(&self, request: &ToolRequest) -> RoutingDecision;
146}
147
148impl<F> TaskRoutingPolicy for F
149where
150 F: Fn(&ToolRequest) -> RoutingDecision + Send + Sync,
151{
152 fn route(&self, request: &ToolRequest) -> RoutingDecision {
153 self(request)
154 }
155}
156
157#[derive(Clone, Copy, Debug, PartialEq, Eq)]
158pub enum RoutingDecision {
159 Foreground,
160 Background,
161 ForegroundThenDetachAfter(Duration),
162}
163
164struct DefaultRoutingPolicy;
165
166impl TaskRoutingPolicy for DefaultRoutingPolicy {
167 fn route(&self, _request: &ToolRequest) -> RoutingDecision {
168 RoutingDecision::Foreground
169 }
170}
171
172#[async_trait]
173pub trait TaskManager: Send + Sync {
174 async fn start_task(
175 &self,
176 request: TaskLaunchRequest,
177 ctx: TaskStartContext,
178 ) -> Result<TaskStartOutcome, TaskManagerError>;
179
180 async fn wait_for_turn(
181 &self,
182 turn_id: &TurnId,
183 cancellation: Option<TurnCancellation>,
184 ) -> Result<Option<TurnTaskUpdate>, TaskManagerError>;
185
186 async fn take_pending_loop_updates(&self) -> Result<PendingLoopUpdates, TaskManagerError>;
187
188 async fn on_turn_interrupted(&self, turn_id: &TurnId) -> Result<(), TaskManagerError>;
189
190 fn handle(&self) -> TaskManagerHandle;
191}
192
193#[async_trait]
194trait TaskManagerControl: Send + Sync {
195 async fn next_event(&self) -> Option<TaskEvent>;
196 async fn cancel(&self, task_id: TaskId) -> Result<(), TaskManagerError>;
197 async fn list_running(&self) -> Vec<TaskSnapshot>;
198 async fn list_completed(&self) -> Vec<TaskSnapshot>;
199 async fn drain_ready_items(&self) -> Vec<Item>;
200 async fn set_continue_policy(
201 &self,
202 task_id: TaskId,
203 policy: ContinuePolicy,
204 ) -> Result<(), TaskManagerError>;
205 async fn set_delivery_mode(
206 &self,
207 task_id: TaskId,
208 mode: DeliveryMode,
209 ) -> Result<(), TaskManagerError>;
210 async fn wait_for_idle(&self);
211}
212
213#[derive(Clone)]
214pub struct TaskManagerHandle {
215 inner: Arc<dyn TaskManagerControl>,
216}
217
218impl TaskManagerHandle {
219 pub async fn next_event(&self) -> Option<TaskEvent> {
220 self.inner.next_event().await
221 }
222
223 pub async fn cancel(&self, task_id: TaskId) -> Result<(), TaskManagerError> {
224 self.inner.cancel(task_id).await
225 }
226
227 pub async fn list_running(&self) -> Vec<TaskSnapshot> {
228 self.inner.list_running().await
229 }
230
231 pub async fn list_completed(&self) -> Vec<TaskSnapshot> {
232 self.inner.list_completed().await
233 }
234
235 pub async fn drain_ready_items(&self) -> Vec<Item> {
236 self.inner.drain_ready_items().await
237 }
238
239 pub async fn set_continue_policy(
240 &self,
241 task_id: TaskId,
242 policy: ContinuePolicy,
243 ) -> Result<(), TaskManagerError> {
244 self.inner.set_continue_policy(task_id, policy).await
245 }
246
247 pub async fn set_delivery_mode(
248 &self,
249 task_id: TaskId,
250 mode: DeliveryMode,
251 ) -> Result<(), TaskManagerError> {
252 self.inner.set_delivery_mode(task_id, mode).await
253 }
254
255 pub async fn wait_for_idle(&self) {
257 self.inner.wait_for_idle().await
258 }
259}
260
261pub struct SimpleTaskManager {
262 state: Arc<HandleState>,
263}
264
265impl SimpleTaskManager {
266 pub fn new() -> Self {
267 Self {
268 state: Arc::new(HandleState::default()),
269 }
270 }
271}
272
273impl Default for SimpleTaskManager {
274 fn default() -> Self {
275 Self::new()
276 }
277}
278
279#[async_trait]
280impl TaskManager for SimpleTaskManager {
281 async fn start_task(
282 &self,
283 request: TaskLaunchRequest,
284 ctx: TaskStartContext,
285 ) -> Result<TaskStartOutcome, TaskManagerError> {
286 let task_id = request
287 .task_id
288 .clone()
289 .unwrap_or_else(|| self.state.next_task_id());
290 let outcome = match &request.kind {
291 TaskLaunchKind::Approved(approved) => {
292 ctx.executor
293 .execute_approved_owned(request.request.clone(), approved, ctx.tool_context)
294 .await
295 }
296 TaskLaunchKind::Plain => {
297 ctx.executor
298 .execute_owned(request.request.clone(), ctx.tool_context)
299 .await
300 }
301 };
302 Ok(TaskStartOutcome::Ready(Box::new(
303 map_outcome_to_resolution(Some(task_id), request.request, outcome),
304 )))
305 }
306
307 async fn wait_for_turn(
308 &self,
309 _turn_id: &TurnId,
310 _cancellation: Option<TurnCancellation>,
311 ) -> Result<Option<TurnTaskUpdate>, TaskManagerError> {
312 Ok(None)
313 }
314
315 async fn take_pending_loop_updates(&self) -> Result<PendingLoopUpdates, TaskManagerError> {
316 Ok(PendingLoopUpdates::default())
317 }
318
319 async fn on_turn_interrupted(&self, _turn_id: &TurnId) -> Result<(), TaskManagerError> {
320 Ok(())
321 }
322
323 fn handle(&self) -> TaskManagerHandle {
324 TaskManagerHandle {
325 inner: self.state.clone(),
326 }
327 }
328}
329
330#[derive(Default)]
331struct HandleState {
332 next_task_index: AtomicU64,
333 events_rx: Mutex<Option<mpsc::UnboundedReceiver<TaskEvent>>>,
334}
335
336impl HandleState {
337 fn next_task_id(&self) -> TaskId {
338 let next = self.next_task_index.fetch_add(1, Ordering::SeqCst) + 1;
339 TaskId::new(format!("task-{}", next))
340 }
341}
342
343#[async_trait]
344impl TaskManagerControl for HandleState {
345 async fn next_event(&self) -> Option<TaskEvent> {
346 let mut rx = self.events_rx.lock().await;
347 match rx.as_mut() {
348 Some(inner) => inner.recv().await,
349 None => None,
350 }
351 }
352
353 async fn cancel(&self, task_id: TaskId) -> Result<(), TaskManagerError> {
354 Err(TaskManagerError::NotFound(task_id))
355 }
356
357 async fn list_running(&self) -> Vec<TaskSnapshot> {
358 Vec::new()
359 }
360
361 async fn list_completed(&self) -> Vec<TaskSnapshot> {
362 Vec::new()
363 }
364
365 async fn drain_ready_items(&self) -> Vec<Item> {
366 Vec::new()
367 }
368
369 async fn set_continue_policy(
370 &self,
371 task_id: TaskId,
372 _policy: ContinuePolicy,
373 ) -> Result<(), TaskManagerError> {
374 Err(TaskManagerError::NotFound(task_id))
375 }
376
377 async fn set_delivery_mode(
378 &self,
379 task_id: TaskId,
380 _mode: DeliveryMode,
381 ) -> Result<(), TaskManagerError> {
382 Err(TaskManagerError::NotFound(task_id))
383 }
384
385 async fn wait_for_idle(&self) {}
386}
387
388pub struct AsyncTaskManager {
389 inner: Arc<AsyncInner>,
390 routing: Arc<dyn TaskRoutingPolicy>,
391}
392
393impl AsyncTaskManager {
394 pub fn new() -> Self {
395 let (event_tx, event_rx) = mpsc::unbounded_channel();
396 Self {
397 inner: Arc::new(AsyncInner {
398 state: Mutex::new(AsyncState::default()),
399 host_event_tx: event_tx,
400 host_event_rx: Mutex::new(event_rx),
401 notify: Notify::new(),
402 }),
403 routing: Arc::new(DefaultRoutingPolicy),
404 }
405 }
406
407 pub fn routing(mut self, policy: impl TaskRoutingPolicy + 'static) -> Self {
408 self.routing = Arc::new(policy);
409 self
410 }
411}
412
413impl Default for AsyncTaskManager {
414 fn default() -> Self {
415 Self::new()
416 }
417}
418
419#[derive(Default)]
420struct AsyncState {
421 next_task_index: u64,
422 tasks: BTreeMap<TaskId, TaskRecord>,
423 per_turn_running: BTreeMap<TurnId, usize>,
424 per_turn_updates: BTreeMap<TurnId, VecDeque<TurnTaskUpdate>>,
425 pending_loop_updates: VecDeque<TaskResolution>,
426 manual_ready_items: Vec<Item>,
427}
428
429struct TaskRecord {
430 snapshot: TaskSnapshot,
431 continue_policy: ContinuePolicy,
432 delivery_mode: DeliveryMode,
433 running: bool,
434 completed: bool,
435 join: Option<JoinHandle<()>>,
436}
437
438struct AsyncInner {
439 state: Mutex<AsyncState>,
440 host_event_tx: mpsc::UnboundedSender<TaskEvent>,
441 host_event_rx: Mutex<mpsc::UnboundedReceiver<TaskEvent>>,
442 notify: Notify,
443}
444
445impl AsyncInner {
446 async fn next_task_id(&self) -> TaskId {
447 let mut state = self.state.lock().await;
448 state.next_task_index += 1;
449 TaskId::new(format!("task-{}", state.next_task_index))
450 }
451}
452
453#[async_trait]
454impl TaskManager for AsyncTaskManager {
455 async fn start_task(
456 &self,
457 request: TaskLaunchRequest,
458 ctx: TaskStartContext,
459 ) -> Result<TaskStartOutcome, TaskManagerError> {
460 let route = self.routing.route(&request.request);
461 let task_id = match request.task_id.clone() {
462 Some(existing) => existing,
463 None => self.inner.next_task_id().await,
464 };
465 let initial_kind = match route {
466 RoutingDecision::Background => TaskKind::Background,
467 _ => TaskKind::Foreground,
468 };
469 let snapshot = TaskSnapshot {
470 id: task_id.clone(),
471 turn_id: request.request.turn_id.clone(),
472 call_id: request.request.call_id.clone(),
473 tool_name: request.request.tool_name.to_string(),
474 kind: initial_kind,
475 metadata: request.request.metadata.clone(),
476 };
477 let _ = self
478 .inner
479 .host_event_tx
480 .send(TaskEvent::Started(snapshot.clone()));
481
482 let mut state = self.inner.state.lock().await;
483 state.tasks.insert(
484 task_id.clone(),
485 TaskRecord {
486 snapshot: snapshot.clone(),
487 continue_policy: ContinuePolicy::NotifyOnly,
488 delivery_mode: DeliveryMode::ToLoop,
489 running: true,
490 completed: false,
491 join: None,
492 },
493 );
494 if initial_kind == TaskKind::Foreground {
495 *state
496 .per_turn_running
497 .entry(snapshot.turn_id.clone())
498 .or_default() += 1;
499 }
500 drop(state);
501
502 let event_tx = self.inner.host_event_tx.clone();
503 let inner = self.inner.clone();
504 let task_id_for_future = task_id.clone();
505 let turn_id = snapshot.turn_id.clone();
506 let kind = request.kind.clone();
507 let exec_request = request.request.clone();
508 let owned_ctx = ctx.tool_context.clone();
509 let executor = ctx.executor.clone();
510 let route_copy = route;
511 let join = tokio::spawn(async move {
512 if let RoutingDecision::ForegroundThenDetachAfter(duration) = route_copy {
513 let event_tx = event_tx.clone();
514 let inner = inner.clone();
515 let task_id = task_id_for_future.clone();
516 let turn_id = turn_id.clone();
517 tokio::spawn(async move {
518 tokio::time::sleep(duration).await;
519 let mut state = inner.state.lock().await;
520 let snapshot = if let Some(record) = state.tasks.get_mut(&task_id)
521 && record.running
522 && record.snapshot.kind == TaskKind::Foreground
523 {
524 record.snapshot.kind = TaskKind::Background;
525 Some(record.snapshot.clone())
526 } else {
527 None
528 };
529 if let Some(snapshot) = snapshot {
530 if let Some(count) = state.per_turn_running.get_mut(&turn_id) {
531 *count = count.saturating_sub(1);
532 if *count == 0 {
533 state.per_turn_running.remove(&turn_id);
534 }
535 }
536 state
537 .per_turn_updates
538 .entry(turn_id.clone())
539 .or_default()
540 .push_back(TurnTaskUpdate::Detached(snapshot.clone()));
541 let _ = event_tx.send(TaskEvent::Detached(snapshot));
542 inner.notify.notify_waiters();
543 }
544 });
545 }
546
547 let outcome = match &kind {
548 TaskLaunchKind::Approved(approval) => {
549 executor
550 .execute_approved_owned(exec_request.clone(), approval, owned_ctx)
551 .await
552 }
553 TaskLaunchKind::Plain => {
554 executor
555 .execute_owned(exec_request.clone(), owned_ctx)
556 .await
557 }
558 };
559
560 let resolution =
561 map_outcome_to_resolution(Some(task_id_for_future.clone()), exec_request, outcome);
562 let completed_result = match &resolution {
563 TaskResolution::Item(item) => item.parts.iter().find_map(|part| match part {
564 agentkit_core::Part::ToolResult(result) => Some(result.clone()),
565 _ => None,
566 }),
567 TaskResolution::Approval(_) => None,
568 };
569
570 let (snapshot, should_request_continue) = {
571 let mut state = inner.state.lock().await;
572 let Some(record) = state.tasks.get_mut(&task_id_for_future) else {
573 return;
574 };
575 record.running = false;
576 record.completed = true;
577 let snapshot = record.snapshot.clone();
578 let continue_policy = record.continue_policy;
579 let delivery_mode = record.delivery_mode;
580 let current_kind = snapshot.kind;
581
582 if current_kind == TaskKind::Foreground {
583 if let Some(count) = state.per_turn_running.get_mut(&turn_id) {
584 *count = count.saturating_sub(1);
585 if *count == 0 {
586 state.per_turn_running.remove(&turn_id);
587 }
588 }
589 state
590 .per_turn_updates
591 .entry(turn_id.clone())
592 .or_default()
593 .push_back(TurnTaskUpdate::Resolution(Box::new(resolution.clone())));
594 } else {
595 match &resolution {
596 TaskResolution::Item(_) if delivery_mode == DeliveryMode::ToLoop => {
597 state.pending_loop_updates.push_back(resolution.clone());
598 }
599 TaskResolution::Approval(_) if delivery_mode == DeliveryMode::ToLoop => {
600 state.pending_loop_updates.push_back(resolution.clone());
601 }
602 TaskResolution::Item(item) => {
603 state.manual_ready_items.push(item.clone());
604 }
605 TaskResolution::Approval(_) => {}
606 }
607 }
608
609 (
610 snapshot,
611 current_kind == TaskKind::Background
612 && delivery_mode == DeliveryMode::ToLoop
613 && continue_policy == ContinuePolicy::RequestContinue,
614 )
615 };
616
617 if let Some(result) = completed_result {
618 let _ = event_tx.send(TaskEvent::Completed(snapshot.clone(), result));
619 }
620 if should_request_continue {
621 let _ = event_tx.send(TaskEvent::ContinueRequested);
622 }
623 inner.notify.notify_waiters();
624 });
625
626 let mut state = self.inner.state.lock().await;
627 if let Some(record) = state.tasks.get_mut(&task_id) {
628 record.join = Some(join);
629 }
630 Ok(TaskStartOutcome::Pending {
631 task_id,
632 kind: initial_kind,
633 })
634 }
635
636 async fn wait_for_turn(
637 &self,
638 turn_id: &TurnId,
639 cancellation: Option<TurnCancellation>,
640 ) -> Result<Option<TurnTaskUpdate>, TaskManagerError> {
641 loop {
642 {
643 let mut state = self.inner.state.lock().await;
644 if let Some(queue) = state.per_turn_updates.get_mut(turn_id)
645 && let Some(update) = queue.pop_front()
646 {
647 return Ok(Some(update));
648 }
649 if state
650 .per_turn_running
651 .get(turn_id)
652 .copied()
653 .unwrap_or_default()
654 == 0
655 {
656 return Ok(None);
657 }
658 }
659 if cancellation
660 .as_ref()
661 .is_some_and(TurnCancellation::is_cancelled)
662 {
663 return Ok(None);
664 }
665 if let Some(cancellation) = cancellation.as_ref() {
666 tokio::select! {
667 _ = self.inner.notify.notified() => {}
668 _ = cancellation.cancelled() => return Ok(None),
669 }
670 } else {
671 self.inner.notify.notified().await;
672 }
673 }
674 }
675
676 async fn take_pending_loop_updates(&self) -> Result<PendingLoopUpdates, TaskManagerError> {
677 let mut state = self.inner.state.lock().await;
678 Ok(PendingLoopUpdates {
679 resolutions: std::mem::take(&mut state.pending_loop_updates),
680 })
681 }
682
683 async fn on_turn_interrupted(&self, turn_id: &TurnId) -> Result<(), TaskManagerError> {
684 let mut state = self.inner.state.lock().await;
685 let interrupted: Vec<TaskId> = state
686 .tasks
687 .iter()
688 .filter_map(|(id, record)| {
689 (record.snapshot.turn_id == *turn_id
690 && record.snapshot.kind == TaskKind::Foreground
691 && record.running)
692 .then_some(id.clone())
693 })
694 .collect();
695 for task_id in interrupted {
696 if let Some(record) = state.tasks.get_mut(&task_id) {
697 record.running = false;
698 if let Some(join) = record.join.take() {
699 join.abort();
700 }
701 let snapshot = record.snapshot.clone();
702 let _ = self
703 .inner
704 .host_event_tx
705 .send(TaskEvent::Cancelled(snapshot));
706 }
707 }
708 state.per_turn_running.remove(turn_id);
709 self.inner.notify.notify_waiters();
710 Ok(())
711 }
712
713 fn handle(&self) -> TaskManagerHandle {
714 TaskManagerHandle {
715 inner: self.inner.clone(),
716 }
717 }
718}
719
720#[async_trait]
721impl TaskManagerControl for AsyncInner {
722 async fn next_event(&self) -> Option<TaskEvent> {
723 self.host_event_rx.lock().await.recv().await
724 }
725
726 async fn cancel(&self, task_id: TaskId) -> Result<(), TaskManagerError> {
727 let mut state = self.state.lock().await;
728 let record = state
729 .tasks
730 .get_mut(&task_id)
731 .ok_or_else(|| TaskManagerError::NotFound(task_id.clone()))?;
732 if let Some(join) = record.join.take() {
733 join.abort();
734 }
735 record.running = false;
736 let snapshot = record.snapshot.clone();
737 if record.snapshot.kind == TaskKind::Foreground
738 && let Some(count) = state.per_turn_running.get_mut(&snapshot.turn_id)
739 {
740 *count = count.saturating_sub(1);
741 if *count == 0 {
742 state.per_turn_running.remove(&snapshot.turn_id);
743 }
744 }
745 let _ = self.host_event_tx.send(TaskEvent::Cancelled(snapshot));
746 self.notify.notify_waiters();
747 Ok(())
748 }
749
750 async fn list_running(&self) -> Vec<TaskSnapshot> {
751 let state = self.state.lock().await;
752 state
753 .tasks
754 .values()
755 .filter(|record| record.running)
756 .map(|record| record.snapshot.clone())
757 .collect()
758 }
759
760 async fn list_completed(&self) -> Vec<TaskSnapshot> {
761 let state = self.state.lock().await;
762 state
763 .tasks
764 .values()
765 .filter(|record| record.completed)
766 .map(|record| record.snapshot.clone())
767 .collect()
768 }
769
770 async fn drain_ready_items(&self) -> Vec<Item> {
771 let mut state = self.state.lock().await;
772 std::mem::take(&mut state.manual_ready_items)
773 }
774
775 async fn set_continue_policy(
776 &self,
777 task_id: TaskId,
778 policy: ContinuePolicy,
779 ) -> Result<(), TaskManagerError> {
780 let mut state = self.state.lock().await;
781 let record = state
782 .tasks
783 .get_mut(&task_id)
784 .ok_or_else(|| TaskManagerError::NotFound(task_id.clone()))?;
785 record.continue_policy = policy;
786 Ok(())
787 }
788
789 async fn set_delivery_mode(
790 &self,
791 task_id: TaskId,
792 mode: DeliveryMode,
793 ) -> Result<(), TaskManagerError> {
794 let mut state = self.state.lock().await;
795 let record = state
796 .tasks
797 .get_mut(&task_id)
798 .ok_or_else(|| TaskManagerError::NotFound(task_id.clone()))?;
799 record.delivery_mode = mode;
800 Ok(())
801 }
802
803 async fn wait_for_idle(&self) {
804 loop {
805 {
806 let state = self.state.lock().await;
807 if !state.tasks.values().any(|r| r.running) {
808 return;
809 }
810 }
811 self.notify.notified().await;
812 }
813 }
814}
815
816fn map_outcome_to_resolution(
817 task_id: Option<TaskId>,
818 request: ToolRequest,
819 outcome: ToolExecutionOutcome,
820) -> TaskResolution {
821 match outcome {
822 ToolExecutionOutcome::Completed(result) => TaskResolution::Item(Item {
823 id: None,
824 kind: agentkit_core::ItemKind::Tool,
825 parts: vec![agentkit_core::Part::ToolResult(result.result)],
826 metadata: result.metadata,
827 usage: None,
828 finish_reason: None,
829 created_at: None,
830 }),
831 ToolExecutionOutcome::Interrupted(
832 agentkit_tools_core::ToolInterruption::ApprovalRequired(mut approval),
833 ) => {
834 let task_id = task_id.unwrap_or_default();
835 approval.task_id = Some(task_id.clone());
836 TaskResolution::Approval(TaskApproval {
837 task_id,
838 tool_request: request,
839 approval,
840 })
841 }
842 ToolExecutionOutcome::FailedBeforeInvocation(error) => {
843 let mut metadata = request.metadata;
844 if matches!(error, ToolError::PermissionDenied(_)) {
845 metadata.insert(
846 TOOL_RESULT_FAILURE_KIND_METADATA_KEY.into(),
847 TOOL_RESULT_FAILURE_KIND_PERMISSION_DENIED.into(),
848 );
849 }
850 TaskResolution::Item(Item {
851 id: None,
852 kind: agentkit_core::ItemKind::Tool,
853 parts: vec![agentkit_core::Part::ToolResult(ToolResultPart {
854 call_id: request.call_id,
855 output: agentkit_core::ToolOutput::Text(error.to_string()),
856 is_error: true,
857 metadata,
858 })],
859 metadata: MetadataMap::new(),
860 usage: None,
861 finish_reason: None,
862 created_at: None,
863 })
864 }
865 ToolExecutionOutcome::Failed(error) => TaskResolution::Item(Item {
866 id: None,
867 kind: agentkit_core::ItemKind::Tool,
868 parts: vec![agentkit_core::Part::ToolResult(ToolResultPart {
869 call_id: request.call_id,
870 output: agentkit_core::ToolOutput::Text(error.to_string()),
871 is_error: true,
872 metadata: request.metadata,
873 })],
874 metadata: MetadataMap::new(),
875 usage: None,
876 finish_reason: None,
877 created_at: None,
878 }),
879 }
880}
881
882#[cfg(test)]
883mod tests {
884 use std::collections::BTreeMap;
885 use std::sync::Arc as StdArc;
886 use std::sync::atomic::{AtomicBool, Ordering as AtomicOrdering};
887
888 use agentkit_core::{
889 CancellationController, ItemKind, Part, SessionId, ToolOutput, TurnCancellation,
890 };
891 use agentkit_tools_core::{
892 ApprovalReason, PermissionChecker, PermissionDecision, ToolAnnotations, ToolInterruption,
893 ToolName, ToolResult, ToolSpec,
894 };
895 use serde_json::json;
896 use tokio::sync::Notify;
897 use tokio::time::{Duration, timeout};
898
899 use super::*;
900
901 struct AllowAllPermissions;
902
903 impl PermissionChecker for AllowAllPermissions {
904 fn evaluate(
905 &self,
906 _request: &dyn agentkit_tools_core::PermissionRequest,
907 ) -> PermissionDecision {
908 PermissionDecision::Allow
909 }
910 }
911
912 #[derive(Clone)]
913 enum TestBehavior {
914 Block {
915 entered: StdArc<AtomicBool>,
916 release: StdArc<Notify>,
917 output: &'static str,
918 },
919 Approval,
920 }
921
922 #[derive(Clone)]
923 struct TestExecutor {
924 behaviors: BTreeMap<String, TestBehavior>,
925 }
926
927 impl TestExecutor {
928 fn new(behaviors: impl IntoIterator<Item = (impl Into<String>, TestBehavior)>) -> Self {
929 Self {
930 behaviors: behaviors
931 .into_iter()
932 .map(|(name, behavior)| (name.into(), behavior))
933 .collect(),
934 }
935 }
936 }
937
938 #[async_trait]
939 impl ToolExecutor for TestExecutor {
940 fn specs(&self) -> Vec<ToolSpec> {
941 self.behaviors
942 .keys()
943 .map(|name| ToolSpec {
944 name: ToolName::new(name),
945 description: format!("test tool {name}"),
946 input_schema: json!({
947 "type": "object",
948 "properties": {},
949 "additionalProperties": false
950 }),
951 output_schema: None,
952 annotations: ToolAnnotations::default(),
953 metadata: MetadataMap::new(),
954 })
955 .collect()
956 }
957
958 async fn execute(
959 &self,
960 request: ToolRequest,
961 _ctx: &mut agentkit_tools_core::ToolContext<'_>,
962 ) -> ToolExecutionOutcome {
963 match self.behaviors.get(request.tool_name.0.as_str()) {
964 Some(TestBehavior::Block {
965 entered,
966 release,
967 output,
968 }) => {
969 entered.store(true, AtomicOrdering::SeqCst);
970 release.notified().await;
971 ToolExecutionOutcome::Completed(ToolResult {
972 result: ToolResultPart {
973 call_id: request.call_id,
974 output: ToolOutput::Text((*output).into()),
975 is_error: false,
976 metadata: request.metadata,
977 },
978 duration: None,
979 metadata: MetadataMap::new(),
980 })
981 }
982 Some(TestBehavior::Approval) => ToolExecutionOutcome::Interrupted(
983 ToolInterruption::ApprovalRequired(ApprovalRequest {
984 task_id: None,
985 call_id: Some(request.call_id.clone()),
986 id: "approval:test".into(),
987 request_kind: "tool.test".into(),
988 reason: ApprovalReason::SensitivePath,
989 summary: "requires approval".into(),
990 metadata: MetadataMap::new(),
991 }),
992 ),
993 None => ToolExecutionOutcome::Failed(ToolError::Unavailable(
994 request.tool_name.0.clone(),
995 )),
996 }
997 }
998 }
999
1000 struct NameRoutingPolicy {
1001 routes: BTreeMap<String, RoutingDecision>,
1002 }
1003
1004 impl NameRoutingPolicy {
1005 fn new(routes: impl IntoIterator<Item = (impl Into<String>, RoutingDecision)>) -> Self {
1006 Self {
1007 routes: routes
1008 .into_iter()
1009 .map(|(name, decision)| (name.into(), decision))
1010 .collect(),
1011 }
1012 }
1013 }
1014
1015 impl TaskRoutingPolicy for NameRoutingPolicy {
1016 fn route(&self, request: &ToolRequest) -> RoutingDecision {
1017 self.routes
1018 .get(request.tool_name.0.as_str())
1019 .copied()
1020 .unwrap_or(RoutingDecision::Foreground)
1021 }
1022 }
1023
1024 fn make_request(tool_name: &str, turn_id: &str, call_id: &str) -> ToolRequest {
1025 ToolRequest {
1026 call_id: ToolCallId::new(call_id),
1027 tool_name: ToolName::new(tool_name),
1028 input: json!({}),
1029 session_id: SessionId::new("session-1"),
1030 turn_id: TurnId::new(turn_id),
1031 metadata: MetadataMap::new(),
1032 }
1033 }
1034
1035 fn make_context(
1036 executor: Arc<dyn ToolExecutor>,
1037 turn_id: &TurnId,
1038 cancellation: Option<TurnCancellation>,
1039 ) -> TaskStartContext {
1040 TaskStartContext {
1041 executor,
1042 tool_context: OwnedToolContext {
1043 session_id: SessionId::new("session-1"),
1044 turn_id: turn_id.clone(),
1045 metadata: MetadataMap::new(),
1046 permissions: Arc::new(AllowAllPermissions),
1047 resources: Arc::new(()),
1048 cancellation,
1049 execution_scope: None,
1050 approved_request: None,
1051 },
1052 }
1053 }
1054
1055 async fn next_event(handle: &TaskManagerHandle) -> TaskEvent {
1056 timeout(Duration::from_secs(1), handle.next_event())
1057 .await
1058 .expect("timed out waiting for task event")
1059 .expect("task event stream ended unexpectedly")
1060 }
1061
1062 async fn wait_until_entered(entered: &AtomicBool) {
1063 timeout(Duration::from_secs(1), async {
1064 while !entered.load(AtomicOrdering::SeqCst) {
1065 tokio::task::yield_now().await;
1066 }
1067 })
1068 .await
1069 .expect("task never entered execution");
1070 }
1071
1072 #[tokio::test]
1073 async fn simple_task_manager_executes_inline_and_assigns_task_ids() {
1074 let manager = SimpleTaskManager::new();
1075 let executor: Arc<dyn ToolExecutor> = Arc::new(TestExecutor::new([(
1076 "needs-approval",
1077 TestBehavior::Approval,
1078 )]));
1079 let request = make_request("needs-approval", "turn-1", "call-1");
1080
1081 let outcome = manager
1082 .start_task(
1083 TaskLaunchRequest {
1084 task_id: None,
1085 request: request.clone(),
1086 kind: TaskLaunchKind::Plain,
1087 },
1088 make_context(executor, &request.turn_id, None),
1089 )
1090 .await
1091 .unwrap();
1092
1093 match outcome {
1094 TaskStartOutcome::Ready(resolution) => match *resolution {
1095 TaskResolution::Approval(task) => {
1096 assert!(!task.task_id.0.is_empty());
1097 assert_eq!(task.approval.task_id.as_ref(), Some(&task.task_id));
1098 assert_eq!(task.tool_request.call_id, request.call_id);
1099 }
1100 other => panic!("unexpected task resolution: {other:?}"),
1101 },
1102 other => panic!("unexpected start outcome: {other:?}"),
1103 }
1104
1105 assert!(manager.handle().list_running().await.is_empty());
1106 }
1107
1108 #[tokio::test]
1109 async fn async_manager_interrupt_cancels_foreground_only() {
1110 let fg_release = StdArc::new(Notify::new());
1111 let fg_entered = StdArc::new(AtomicBool::new(false));
1112 let bg_release = StdArc::new(Notify::new());
1113 let bg_entered = StdArc::new(AtomicBool::new(false));
1114 let executor: Arc<dyn ToolExecutor> = Arc::new(TestExecutor::new([
1115 (
1116 "foreground",
1117 TestBehavior::Block {
1118 entered: fg_entered.clone(),
1119 release: fg_release.clone(),
1120 output: "foreground-done",
1121 },
1122 ),
1123 (
1124 "background",
1125 TestBehavior::Block {
1126 entered: bg_entered.clone(),
1127 release: bg_release.clone(),
1128 output: "background-done",
1129 },
1130 ),
1131 ]));
1132 let manager = AsyncTaskManager::new().routing(NameRoutingPolicy::new([
1133 ("foreground", RoutingDecision::Foreground),
1134 ("background", RoutingDecision::Background),
1135 ]));
1136 let handle = manager.handle();
1137 let turn_id = TurnId::new("turn-1");
1138
1139 let foreground = manager
1140 .start_task(
1141 TaskLaunchRequest {
1142 task_id: None,
1143 request: make_request("foreground", "turn-1", "call-fg"),
1144 kind: TaskLaunchKind::Plain,
1145 },
1146 make_context(executor.clone(), &turn_id, None),
1147 )
1148 .await
1149 .unwrap();
1150 let background = manager
1151 .start_task(
1152 TaskLaunchRequest {
1153 task_id: None,
1154 request: make_request("background", "turn-1", "call-bg"),
1155 kind: TaskLaunchKind::Plain,
1156 },
1157 make_context(executor.clone(), &turn_id, None),
1158 )
1159 .await
1160 .unwrap();
1161
1162 assert!(matches!(
1163 foreground,
1164 TaskStartOutcome::Pending {
1165 kind: TaskKind::Foreground,
1166 ..
1167 }
1168 ));
1169 let background_id = match background {
1170 TaskStartOutcome::Pending {
1171 task_id,
1172 kind: TaskKind::Background,
1173 } => task_id,
1174 other => panic!("unexpected background outcome: {other:?}"),
1175 };
1176
1177 let _ = next_event(&handle).await;
1178 let _ = next_event(&handle).await;
1179 wait_until_entered(fg_entered.as_ref()).await;
1180 wait_until_entered(bg_entered.as_ref()).await;
1181
1182 manager.on_turn_interrupted(&turn_id).await.unwrap();
1183
1184 match next_event(&handle).await {
1185 TaskEvent::Cancelled(snapshot) => assert_eq!(snapshot.tool_name, "foreground"),
1186 other => panic!("unexpected event after interrupt: {other:?}"),
1187 }
1188
1189 let running = handle.list_running().await;
1190 assert_eq!(running.len(), 1);
1191 assert_eq!(running[0].id, background_id);
1192 assert_eq!(running[0].tool_name, "background");
1193
1194 bg_release.notify_waiters();
1195 match next_event(&handle).await {
1196 TaskEvent::Completed(snapshot, result) => {
1197 assert_eq!(snapshot.id, background_id);
1198 assert_eq!(result.output, ToolOutput::Text("background-done".into()));
1199 }
1200 other => panic!("unexpected completion event: {other:?}"),
1201 }
1202 }
1203
1204 #[tokio::test]
1205 async fn async_manager_can_cancel_background_tasks_by_id() {
1206 let release = StdArc::new(Notify::new());
1207 let entered = StdArc::new(AtomicBool::new(false));
1208 let executor: Arc<dyn ToolExecutor> = Arc::new(TestExecutor::new([(
1209 "background",
1210 TestBehavior::Block {
1211 entered: entered.clone(),
1212 release,
1213 output: "done",
1214 },
1215 )]));
1216 let manager = AsyncTaskManager::new().routing(NameRoutingPolicy::new([(
1217 "background",
1218 RoutingDecision::Background,
1219 )]));
1220 let handle = manager.handle();
1221 let request = make_request("background", "turn-1", "call-1");
1222
1223 let task_id = match manager
1224 .start_task(
1225 TaskLaunchRequest {
1226 task_id: None,
1227 request: request.clone(),
1228 kind: TaskLaunchKind::Plain,
1229 },
1230 make_context(executor, &request.turn_id, None),
1231 )
1232 .await
1233 .unwrap()
1234 {
1235 TaskStartOutcome::Pending { task_id, .. } => task_id,
1236 other => panic!("unexpected start outcome: {other:?}"),
1237 };
1238
1239 let _ = next_event(&handle).await;
1240 wait_until_entered(entered.as_ref()).await;
1241 handle.cancel(task_id.clone()).await.unwrap();
1242
1243 match next_event(&handle).await {
1244 TaskEvent::Cancelled(snapshot) => assert_eq!(snapshot.id, task_id),
1245 other => panic!("unexpected event after cancel: {other:?}"),
1246 }
1247
1248 assert!(handle.list_running().await.is_empty());
1249 }
1250
1251 #[tokio::test]
1252 async fn async_manager_manual_delivery_keeps_results_out_of_loop_updates() {
1253 let release = StdArc::new(Notify::new());
1254 let entered = StdArc::new(AtomicBool::new(false));
1255 let executor: Arc<dyn ToolExecutor> = Arc::new(TestExecutor::new([(
1256 "background",
1257 TestBehavior::Block {
1258 entered: entered.clone(),
1259 release: release.clone(),
1260 output: "manual-done",
1261 },
1262 )]));
1263 let manager = AsyncTaskManager::new().routing(NameRoutingPolicy::new([(
1264 "background",
1265 RoutingDecision::Background,
1266 )]));
1267 let handle = manager.handle();
1268 let request = make_request("background", "turn-1", "call-1");
1269
1270 let task_id = match manager
1271 .start_task(
1272 TaskLaunchRequest {
1273 task_id: None,
1274 request: request.clone(),
1275 kind: TaskLaunchKind::Plain,
1276 },
1277 make_context(executor, &request.turn_id, None),
1278 )
1279 .await
1280 .unwrap()
1281 {
1282 TaskStartOutcome::Pending { task_id, .. } => task_id,
1283 other => panic!("unexpected start outcome: {other:?}"),
1284 };
1285
1286 let _ = next_event(&handle).await;
1287 wait_until_entered(entered.as_ref()).await;
1288 handle
1289 .set_continue_policy(task_id.clone(), ContinuePolicy::RequestContinue)
1290 .await
1291 .unwrap();
1292 handle
1293 .set_delivery_mode(task_id, DeliveryMode::Manual)
1294 .await
1295 .unwrap();
1296
1297 release.notify_waiters();
1298 match next_event(&handle).await {
1299 TaskEvent::Completed(_, result) => {
1300 assert_eq!(result.output, ToolOutput::Text("manual-done".into()))
1301 }
1302 other => panic!("unexpected event: {other:?}"),
1303 }
1304
1305 assert!(
1306 timeout(Duration::from_millis(50), handle.next_event())
1307 .await
1308 .is_err()
1309 );
1310 assert!(
1311 manager
1312 .take_pending_loop_updates()
1313 .await
1314 .unwrap()
1315 .resolutions
1316 .is_empty()
1317 );
1318
1319 let ready_items = handle.drain_ready_items().await;
1320 assert_eq!(ready_items.len(), 1);
1321 assert_eq!(ready_items[0].kind, ItemKind::Tool);
1322 match &ready_items[0].parts[0] {
1323 Part::ToolResult(result) => {
1324 assert_eq!(result.output, ToolOutput::Text("manual-done".into()))
1325 }
1326 other => panic!("unexpected ready item: {other:?}"),
1327 }
1328 }
1329
1330 #[tokio::test]
1331 async fn async_manager_to_loop_delivery_can_request_continue() {
1332 let release = StdArc::new(Notify::new());
1333 let entered = StdArc::new(AtomicBool::new(false));
1334 let executor: Arc<dyn ToolExecutor> = Arc::new(TestExecutor::new([(
1335 "background",
1336 TestBehavior::Block {
1337 entered: entered.clone(),
1338 release: release.clone(),
1339 output: "loop-done",
1340 },
1341 )]));
1342 let manager = AsyncTaskManager::new().routing(NameRoutingPolicy::new([(
1343 "background",
1344 RoutingDecision::Background,
1345 )]));
1346 let handle = manager.handle();
1347 let request = make_request("background", "turn-1", "call-1");
1348
1349 let task_id = match manager
1350 .start_task(
1351 TaskLaunchRequest {
1352 task_id: None,
1353 request: request.clone(),
1354 kind: TaskLaunchKind::Plain,
1355 },
1356 make_context(
1357 executor,
1358 &request.turn_id,
1359 Some(TurnCancellation::new(
1360 CancellationController::new().handle(),
1361 )),
1362 ),
1363 )
1364 .await
1365 .unwrap()
1366 {
1367 TaskStartOutcome::Pending { task_id, .. } => task_id,
1368 other => panic!("unexpected start outcome: {other:?}"),
1369 };
1370
1371 let _ = next_event(&handle).await;
1372 wait_until_entered(entered.as_ref()).await;
1373 handle
1374 .set_continue_policy(task_id, ContinuePolicy::RequestContinue)
1375 .await
1376 .unwrap();
1377
1378 release.notify_waiters();
1379 match next_event(&handle).await {
1380 TaskEvent::Completed(_, result) => {
1381 assert_eq!(result.output, ToolOutput::Text("loop-done".into()))
1382 }
1383 other => panic!("unexpected completion event: {other:?}"),
1384 }
1385 match next_event(&handle).await {
1386 TaskEvent::ContinueRequested => {}
1387 other => panic!("unexpected follow-up event: {other:?}"),
1388 }
1389
1390 let updates = manager.take_pending_loop_updates().await.unwrap();
1391 assert_eq!(updates.resolutions.len(), 1);
1392 assert!(handle.drain_ready_items().await.is_empty());
1393 }
1394
1395 #[tokio::test]
1396 async fn wait_for_idle_returns_after_loop_updates_are_queued() {
1397 let release = StdArc::new(Notify::new());
1398 let entered = StdArc::new(AtomicBool::new(false));
1399 let executor: Arc<dyn ToolExecutor> = Arc::new(TestExecutor::new([(
1400 "background",
1401 TestBehavior::Block {
1402 entered: entered.clone(),
1403 release: release.clone(),
1404 output: "idle-done",
1405 },
1406 )]));
1407 let manager = AsyncTaskManager::new().routing(NameRoutingPolicy::new([(
1408 "background",
1409 RoutingDecision::Background,
1410 )]));
1411 let handle = manager.handle();
1412 let request = make_request("background", "turn-1", "call-1");
1413
1414 let outcome = manager
1415 .start_task(
1416 TaskLaunchRequest {
1417 task_id: None,
1418 request: request.clone(),
1419 kind: TaskLaunchKind::Plain,
1420 },
1421 make_context(executor, &request.turn_id, None),
1422 )
1423 .await
1424 .unwrap();
1425 assert!(matches!(outcome, TaskStartOutcome::Pending { .. }));
1426
1427 let _ = next_event(&handle).await;
1428 wait_until_entered(entered.as_ref()).await;
1429 release.notify_waiters();
1430
1431 timeout(Duration::from_secs(1), handle.wait_for_idle())
1432 .await
1433 .expect("wait_for_idle timed out");
1434
1435 let updates = manager.take_pending_loop_updates().await.unwrap();
1436 assert_eq!(updates.resolutions.len(), 1);
1437 match &updates.resolutions[0] {
1438 TaskResolution::Item(item) => match &item.parts[0] {
1439 Part::ToolResult(result) => {
1440 assert_eq!(result.call_id, request.call_id);
1441 assert_eq!(result.output, ToolOutput::Text("idle-done".into()));
1442 }
1443 other => panic!("unexpected tool item: {other:?}"),
1444 },
1445 other => panic!("unexpected pending update: {other:?}"),
1446 }
1447 }
1448}