1use super::owned_process_registry::OwnedProcessRegistry;
4use super::resolved_tools::ResolvedToolSet;
5use super::tool_capacity::{SharedToolCapacity, ToolPermit, TransactionToolCapacity};
6use super::tool_handler::{OwnedProcessLease, ToolExecutionControl, ToolKillHandle};
7use std::sync::atomic::AtomicU32;
8
9pub struct OrphanToolPermitSet {
18 orphans: std::sync::Mutex<Vec<OrphanPermit>>,
19}
20
21struct OrphanPermit {
22 #[allow(dead_code)]
24 permit: ToolPermit,
25 #[allow(dead_code)]
27 process_lease: Option<OwnedProcessLease>,
28}
29
30impl OrphanToolPermitSet {
34 pub fn new() -> Self {
36 Self {
37 orphans: std::sync::Mutex::new(Vec::new()),
38 }
39 }
40
41 fn park_orphan_permit(&self, permit: ToolPermit) {
43 self.park_orphan_permit_with_lease(permit, None);
44 }
45
46 fn park_orphan_permit_with_lease(
48 &self,
49 permit: ToolPermit,
50 process_lease: Option<OwnedProcessLease>,
51 ) {
52 let mut orphans = self.orphans.lock().unwrap_or_else(|e| e.into_inner());
53 orphans.push(OrphanPermit {
54 permit,
55 process_lease,
56 });
57 }
58
59 pub fn shutdown_progress(&self) -> usize {
63 let mut orphans = self.orphans.lock().unwrap_or_else(|e| e.into_inner());
64 orphans.clear();
65 0
66 }
67
68 pub fn pending_permits(&self) -> usize {
70 self.orphans.lock().unwrap_or_else(|e| e.into_inner()).len()
71 }
72
73 pub fn is_empty(&self) -> bool {
75 self.pending_count() == 0
76 }
77
78 pub fn pending_count(&self) -> usize {
80 self.pending_permits()
81 }
82}
83
84impl Default for OrphanToolPermitSet {
85 fn default() -> Self {
86 Self::new()
87 }
88}
89
90impl Drop for OrphanToolPermitSet {
91 fn drop(&mut self) {
92 let _ = std::mem::take(self.orphans.get_mut().unwrap_or_else(|e| e.into_inner()));
94 }
95}
96
97use super::validation::{
98 validate_tool_completion, validate_tool_input, InputValidationFailure, DEFAULT_MAX_JSON_DEPTH,
99};
100use monoloop_contracts::{
101 CanonicalToolError, CanonicalToolOutput, CanonicalToolResult, CanonicalToolResultOutcome,
102 ExchangeId, SessionKey, ToolActionId, ToolCall, ToolCallContext, ToolCompletion,
103 ToolExecutionClass, ToolId, ToolLifecycleEvent, ToolName, ToolRuntimeError, ToolStartError,
104 TransactionId,
105};
106use std::future::Future;
107use std::panic::{catch_unwind, AssertUnwindSafe};
108use std::pin::Pin;
109use std::sync::Arc;
110use std::time::{Duration, Instant};
111
112#[derive(Clone, Debug)]
114pub struct DispatchRequest {
115 pub exchange_id: ExchangeId,
117 pub tool_action_id: ToolActionId,
119 pub tool_name: ToolName,
121 pub provider_tool_call_id: String,
123 pub request_ordinal: u32,
125 pub arguments_json: String,
127 pub transaction_deadline: Instant,
130}
131
132#[derive(Clone, Debug)]
134pub enum DispatchOutcome {
135 Canonical {
137 result: CanonicalToolResult,
139 lifecycle: Vec<ToolLifecycleEvent>,
141 },
142 Rejected {
144 tool_action_id: ToolActionId,
146 code: &'static str,
148 message: String,
150 lifecycle: Vec<ToolLifecycleEvent>,
152 },
153 RuntimeFailed {
155 tool_action_id: ToolActionId,
157 tool_id: Option<ToolId>,
159 code: String,
161 lifecycle: Vec<ToolLifecycleEvent>,
163 },
164}
165
166#[derive(Clone, Copy, Debug)]
168pub struct DispatcherLimits {
169 pub max_concurrent_tools: usize,
171 pub max_queued_tools: usize,
173 pub max_tool_payload_bytes: usize,
175 pub max_tool_output_bytes: usize,
177}
178
179impl Default for DispatcherLimits {
180 fn default() -> Self {
181 Self {
182 max_concurrent_tools: 16,
183 max_queued_tools: 64,
184 max_tool_payload_bytes: usize::MAX,
185 max_tool_output_bytes: usize::MAX,
186 }
187 }
188}
189
190pub struct TransactionToolDispatcher {
192 transaction_id: TransactionId,
193 session_key: std::sync::Mutex<SessionKey>,
195 tools: ResolvedToolSet,
196 capacity: Arc<TransactionToolCapacity>,
197 tool_spill: Arc<OrphanToolPermitSet>,
199 owned_processes: Arc<AtomicU32>,
201 process_registry: Arc<OwnedProcessRegistry>,
203 max_tool_payload_bytes: usize,
205 max_tool_output_bytes: usize,
207 max_error_message_bytes: usize,
208 max_json_depth: u32,
209}
210
211impl TransactionToolDispatcher {
212 pub fn new(
214 transaction_id: TransactionId,
215 session_key: SessionKey,
216 tools: ResolvedToolSet,
217 shared_capacity: Arc<SharedToolCapacity>,
218 max_concurrent_tools: usize,
219 max_queued_tools: usize,
220 ) -> Arc<Self> {
221 Self::with_limits(
222 transaction_id,
223 session_key,
224 tools,
225 shared_capacity,
226 DispatcherLimits {
227 max_concurrent_tools,
228 max_queued_tools,
229 max_tool_payload_bytes: usize::MAX,
230 max_tool_output_bytes: usize::MAX,
231 },
232 )
233 }
234
235 pub fn with_limits(
237 transaction_id: TransactionId,
238 session_key: SessionKey,
239 tools: ResolvedToolSet,
240 shared_capacity: Arc<SharedToolCapacity>,
241 limits: DispatcherLimits,
242 ) -> Arc<Self> {
243 Self::with_limits_and_spill(
244 transaction_id,
245 session_key,
246 tools,
247 shared_capacity,
248 limits,
249 Arc::new(OrphanToolPermitSet::new()),
250 Arc::new(AtomicU32::new(0)),
251 Arc::new(OwnedProcessRegistry::new()),
252 )
253 }
254
255 pub fn with_runtime_spill(
257 transaction_id: TransactionId,
258 session_key: SessionKey,
259 tools: ResolvedToolSet,
260 shared_capacity: Arc<SharedToolCapacity>,
261 tool_spill: Arc<OrphanToolPermitSet>,
262 limits: DispatcherLimits,
263 ) -> Arc<Self> {
264 Self::with_runtime_resources(
265 transaction_id,
266 session_key,
267 tools,
268 shared_capacity,
269 tool_spill,
270 Arc::new(AtomicU32::new(0)),
271 Arc::new(OwnedProcessRegistry::new()),
272 limits,
273 )
274 }
275
276 #[allow(clippy::too_many_arguments)]
278 pub fn with_runtime_resources(
279 transaction_id: TransactionId,
280 session_key: SessionKey,
281 tools: ResolvedToolSet,
282 shared_capacity: Arc<SharedToolCapacity>,
283 tool_spill: Arc<OrphanToolPermitSet>,
284 owned_processes: Arc<AtomicU32>,
285 process_registry: Arc<OwnedProcessRegistry>,
286 limits: DispatcherLimits,
287 ) -> Arc<Self> {
288 Self::with_limits_and_spill(
289 transaction_id,
290 session_key,
291 tools,
292 shared_capacity,
293 limits,
294 tool_spill,
295 owned_processes,
296 process_registry,
297 )
298 }
299
300 pub fn limits_from_transaction(
302 limits: &monoloop_contracts::TransactionLimits,
303 ) -> DispatcherLimits {
304 DispatcherLimits {
305 max_concurrent_tools: limits.max_concurrent_tools_per_transaction.max(1),
306 max_queued_tools: limits.max_queued_tools_per_transaction.max(1),
307 max_tool_payload_bytes: limits.max_tool_payload_bytes.max(1),
308 max_tool_output_bytes: limits.max_tool_output_bytes.max(1),
309 }
310 }
311
312 #[allow(clippy::too_many_arguments)]
314 fn with_limits_and_spill(
315 transaction_id: TransactionId,
316 session_key: SessionKey,
317 tools: ResolvedToolSet,
318 shared_capacity: Arc<SharedToolCapacity>,
319 limits: DispatcherLimits,
320 tool_spill: Arc<OrphanToolPermitSet>,
321 owned_processes: Arc<AtomicU32>,
322 process_registry: Arc<OwnedProcessRegistry>,
323 ) -> Arc<Self> {
324 let capacity = TransactionToolCapacity::new(
325 shared_capacity,
326 limits.max_concurrent_tools,
327 limits.max_queued_tools,
328 );
329 for spec in tools.specs() {
330 capacity.configure_tool(spec.id.clone(), spec.limits.max_concurrent);
331 }
332 Arc::new(Self {
333 transaction_id,
334 session_key: std::sync::Mutex::new(session_key),
335 tools,
336 capacity,
337 tool_spill,
338 owned_processes,
339 process_registry,
340 max_tool_payload_bytes: limits.max_tool_payload_bytes.max(1),
341 max_tool_output_bytes: limits.max_tool_output_bytes.max(1),
342 max_error_message_bytes: 1024,
343 max_json_depth: DEFAULT_MAX_JSON_DEPTH,
344 })
345 }
346
347 pub fn tools(&self) -> &ResolvedToolSet {
349 &self.tools
350 }
351
352 pub fn transaction_id(&self) -> TransactionId {
354 self.transaction_id
355 }
356
357 pub fn session_key(&self) -> SessionKey {
359 self.session_key
360 .lock()
361 .unwrap_or_else(|e| e.into_inner())
362 .clone()
363 }
364
365 pub fn rebind_session(&self, session_key: SessionKey) {
367 *self.session_key.lock().unwrap_or_else(|e| e.into_inner()) = session_key;
368 }
369
370 pub fn vault_pending_permits(&self) -> usize {
372 self.tool_spill.pending_permits()
373 }
374
375 pub fn tool_spill(&self) -> &Arc<OrphanToolPermitSet> {
377 &self.tool_spill
378 }
379
380 pub fn active_tools(&self) -> usize {
382 self.capacity.active()
383 }
384
385 pub fn queued_tools(&self) -> usize {
387 self.capacity.queued()
388 }
389
390 pub async fn dispatch(self: &Arc<Self>, request: DispatchRequest) -> DispatchOutcome {
392 self.dispatch_with_cancel(request, None).await
393 }
394
395 pub async fn dispatch_with_cancel(
400 self: &Arc<Self>,
401 request: DispatchRequest,
402 cancel: Option<Arc<super::sticky_cancel::StickyCancel>>,
403 ) -> DispatchOutcome {
404 let action = request.tool_action_id.clone();
405
406 let Some(resolved) = self.tools.get_by_name(&request.tool_name) else {
408 return DispatchOutcome::Rejected {
409 tool_action_id: action,
410 code: "tool_not_allowed",
411 message: "tool not in resolved set".into(),
412 lifecycle: vec![],
413 };
414 };
415 let tool_id = resolved.spec.id.clone();
416 let spec = resolved.spec.clone();
417 let handler = Arc::clone(&resolved.handler);
418
419 if !self.capacity.try_enqueue() {
420 return DispatchOutcome::Rejected {
421 tool_action_id: action,
422 code: "tool_queue_full",
423 message: "per-transaction tool queue full".into(),
424 lifecycle: vec![],
425 };
426 }
427
428 let max_payload = spec.limits.max_input_bytes.min(self.max_tool_payload_bytes);
431 let arguments = match validate_tool_input(
432 &request.arguments_json,
433 &spec.input_schema,
434 max_payload,
435 self.max_json_depth,
436 ) {
437 Ok(v) => v,
438 Err(f) => {
439 self.capacity.dequeue();
440 return reject_input(action, f);
441 }
442 };
443
444 let permit = {
446 let mut acquired = None;
447 let deadline = Instant::now() + Duration::from_millis(50);
448 while Instant::now() < deadline {
449 if let Some(p) = self.capacity.try_acquire(&tool_id) {
450 acquired = Some(p);
451 break;
452 }
453 tokio::task::yield_now().await;
454 }
455 match acquired {
456 Some(p) => p,
457 None => {
458 self.capacity.dequeue();
460 return DispatchOutcome::Rejected {
461 tool_action_id: action,
462 code: "tool_capacity_exceeded",
463 message: "tool concurrency capacity exceeded".into(),
464 lifecycle: vec![],
465 };
466 }
467 }
468 };
469 let mut dispatch_guard = DispatchGuard {
471 permit: Some(permit),
472 kill: None,
473 process_registry: Arc::clone(&self.process_registry),
474 };
475
476 let mut lifecycle = vec![ToolLifecycleEvent::Started {
477 tool_action_id: action.clone(),
478 tool_id: tool_id.clone(),
479 tool_name: request.tool_name.clone(),
480 provider_tool_call_id: request.provider_tool_call_id.clone(),
481 request_ordinal: request.request_ordinal,
482 }];
483
484 let call = ToolCall {
485 tool_name: request.tool_name.clone(),
486 tool_id: tool_id.clone(),
487 provider_tool_call_id: request.provider_tool_call_id.clone(),
488 arguments,
489 request_ordinal: request.request_ordinal,
490 };
491 let now = Instant::now();
493 if now >= request.transaction_deadline {
494 drop(dispatch_guard);
495 return DispatchOutcome::RuntimeFailed {
496 tool_action_id: action,
497 tool_id: Some(tool_id),
498 code: "transaction_deadline_exceeded".into(),
499 lifecycle: vec![],
500 };
501 }
502 let tool_cap = now + spec.limits.execution_deadline;
503 let absolute_tool_deadline = tool_cap.min(request.transaction_deadline);
504 let context = ToolCallContext {
505 transaction_id: self.transaction_id,
506 session_key: self.session_key(),
507 exchange_id: Some(request.exchange_id),
508 tool_action_id: action.clone(),
509 tool_id: tool_id.clone(),
510 deadline: absolute_tool_deadline,
511 };
512
513 let deadline = absolute_tool_deadline.saturating_duration_since(now);
515 let policy = spec.execution_class.clone();
516
517 let supports_required_termination = match &policy {
520 ToolExecutionClass::AbortableAtYield { .. } => {
522 handler.runtime_owns_abortable_drive() && handler.supports_abort()
523 }
524 ToolExecutionClass::ProcessIsolated { .. } => {
526 handler.os_process_isolated() && handler.supports_isolated_kill()
527 }
528 ToolExecutionClass::CooperativeInProcess { .. } => true,
529 };
530 if !supports_required_termination {
531 drop(dispatch_guard);
532 lifecycle.push(ToolLifecycleEvent::RuntimeFailed {
533 tool_action_id: action.clone(),
534 tool_id: tool_id.clone(),
535 code: "missing_kill_handle".into(),
536 });
537 return DispatchOutcome::RuntimeFailed {
538 tool_action_id: action,
539 tool_id: Some(tool_id),
540 code: "missing_kill_handle".into(),
541 lifecycle,
542 };
543 }
544
545 let start_result = catch_unwind(AssertUnwindSafe(|| handler.start(call, context)));
546 let handle = match start_result {
547 Ok(Ok(h)) => h,
548 Ok(Err(ToolStartError::CapacityExceeded)) => {
549 drop(dispatch_guard);
550 return DispatchOutcome::Rejected {
551 tool_action_id: action,
552 code: "tool_capacity_exceeded",
553 message: "handler capacity exceeded".into(),
554 lifecycle,
555 };
556 }
557 Ok(Err(ToolStartError::Rejected(reason))) => {
558 drop(dispatch_guard);
559 lifecycle.push(ToolLifecycleEvent::RuntimeFailed {
560 tool_action_id: action.clone(),
561 tool_id: tool_id.clone(),
562 code: "tool_start_rejected".into(),
563 });
564 return DispatchOutcome::RuntimeFailed {
565 tool_action_id: action,
566 tool_id: Some(tool_id),
567 code: format!("start_rejected:{reason}"),
568 lifecycle,
569 };
570 }
571 Err(_) => {
572 drop(dispatch_guard);
573 lifecycle.push(ToolLifecycleEvent::RuntimeFailed {
574 tool_action_id: action.clone(),
575 tool_id: tool_id.clone(),
576 code: "tool_panicked".into(),
577 });
578 return DispatchOutcome::RuntimeFailed {
579 tool_action_id: action,
580 tool_id: Some(tool_id),
581 code: "panicked".into(),
582 lifecycle,
583 };
584 }
585 };
586
587 let control = handle.control.clone();
588 dispatch_guard.kill = handle.kill.clone();
589 let kill = dispatch_guard.kill.clone();
590 if let Some(ref k) = kill {
592 if k.is_process_isolated() {
593 k.register_owned_process(Arc::clone(&self.owned_processes));
594 }
595 }
596 let ownership_ok = match &policy {
599 ToolExecutionClass::AbortableAtYield { .. } => {
600 kill.as_ref().is_some_and(|k| k.is_cancel_only()) && handle.drive.is_some()
601 }
602 ToolExecutionClass::ProcessIsolated { .. } => {
603 kill.as_ref().is_some_and(|k| k.is_process_isolated()) && handle.drive.is_some()
604 }
605 ToolExecutionClass::CooperativeInProcess { .. } => true,
606 };
607 if !ownership_ok {
608 control.cancel();
612 let completion = handle.completion;
613 let wait = completion.wait();
614 tokio::pin!(wait);
615 match tokio::time::timeout(Duration::from_millis(200), &mut wait).await {
616 Ok(_) => {
617 dispatch_guard.release_if_idle();
619 }
620 Err(_) => {
621 if let Some(permit) = dispatch_guard.permit.take() {
624 self.tool_spill.park_orphan_permit(permit);
625 }
626 }
627 }
628 lifecycle.push(ToolLifecycleEvent::RuntimeFailed {
629 tool_action_id: action.clone(),
630 tool_id: tool_id.clone(),
631 code: "missing_kill_handle".into(),
632 });
633 return DispatchOutcome::RuntimeFailed {
634 tool_action_id: action,
635 tool_id: Some(tool_id),
636 code: "missing_kill_handle".into(),
637 lifecycle,
638 };
639 }
640 let mut drive = handle.drive;
641 let wait = handle.completion.wait();
642 tokio::pin!(wait);
643 let cancel_fut = async {
644 if let Some(n) = cancel.as_ref() {
645 n.cancelled().await;
646 } else {
647 std::future::pending::<()>().await;
648 }
649 };
650 let completion = if let Some(drive_fut) = drive.take() {
651 tokio::pin!(drive_fut);
653 tokio::select! {
654 biased;
655 c = &mut wait => c,
656 _ = &mut drive_fut => wait.await,
657 _ = cancel_fut => {
658 await_tool_termination_driven(
659 &mut wait,
660 &mut drive_fut,
661 &control,
662 kill.as_ref(),
663 &policy,
664 )
665 .await
666 }
667 _ = tokio::time::sleep(deadline) => {
668 await_tool_termination_driven(
669 &mut wait,
670 &mut drive_fut,
671 &control,
672 kill.as_ref(),
673 &policy,
674 )
675 .await
676 }
677 }
678 } else {
679 tokio::select! {
680 biased;
681 c = &mut wait => c,
682 _ = cancel_fut => {
683 await_tool_termination(&mut wait, &control, kill.as_ref(), &policy).await
684 }
685 _ = tokio::time::sleep(deadline) => {
686 await_tool_termination(&mut wait, &control, kill.as_ref(), &policy).await
687 }
688 }
689 };
690 let cooperative_deadline =
694 matches!(policy, ToolExecutionClass::CooperativeInProcess { .. })
695 && matches!(
696 &completion,
697 ToolCompletion::RuntimeFailed(ToolRuntimeError::DeadlineExceeded)
698 );
699 if let Some(ref k) = dispatch_guard.kill {
700 let _ = k.join_timeout(Duration::from_millis(50)).await;
701 if cooperative_deadline && k.is_cancel_only() {
702 if let Some(permit) = dispatch_guard.permit.take() {
704 self.tool_spill.park_orphan_permit(permit);
705 }
706 }
707 } else if cooperative_deadline {
708 if let Some(permit) = dispatch_guard.permit.take() {
709 self.tool_spill.park_orphan_permit(permit);
710 }
711 }
712 dispatch_guard.release_if_idle();
713
714 let max_output = spec.limits.max_output_bytes.min(self.max_tool_output_bytes);
715 let validated = match validate_tool_completion(
716 completion,
717 &spec.output_contract,
718 max_output,
719 self.max_error_message_bytes,
720 self.max_json_depth,
721 ) {
722 Ok(c) => c,
723 Err(_) => {
724 lifecycle.push(ToolLifecycleEvent::RuntimeFailed {
725 tool_action_id: action.clone(),
726 tool_id: tool_id.clone(),
727 code: "output_contract_violated".into(),
728 });
729 return DispatchOutcome::RuntimeFailed {
730 tool_action_id: action,
731 tool_id: Some(tool_id),
732 code: "output_contract_violated".into(),
733 lifecycle,
734 };
735 }
736 };
737
738 match validated {
739 ToolCompletion::Succeeded(output) => {
740 let result = CanonicalToolResult {
741 transaction_id: self.transaction_id,
742 session_key: self.session_key(),
743 exchange_id: request.exchange_id,
744 tool_action_id: action.clone(),
745 tool_id: tool_id.clone(),
746 provider_tool_call_id: request.provider_tool_call_id,
747 request_ordinal: request.request_ordinal,
748 outcome: CanonicalToolResultOutcome::Succeeded(output),
749 };
750 lifecycle.push(ToolLifecycleEvent::Completed {
751 result: result.clone(),
752 });
753 DispatchOutcome::Canonical { result, lifecycle }
754 }
755 ToolCompletion::DomainFailed(err) => {
756 let result = CanonicalToolResult {
757 transaction_id: self.transaction_id,
758 session_key: self.session_key(),
759 exchange_id: request.exchange_id,
760 tool_action_id: action.clone(),
761 tool_id: tool_id.clone(),
762 provider_tool_call_id: request.provider_tool_call_id,
763 request_ordinal: request.request_ordinal,
764 outcome: CanonicalToolResultOutcome::DomainFailed(err),
765 };
766 lifecycle.push(ToolLifecycleEvent::Completed {
767 result: result.clone(),
768 });
769 DispatchOutcome::Canonical { result, lifecycle }
770 }
771 ToolCompletion::RuntimeFailed(e) => {
772 let code = match e {
773 ToolRuntimeError::Panicked => "panicked",
774 ToolRuntimeError::CompletionLost => "completion_lost",
775 ToolRuntimeError::OutputContractViolated => "output_contract_violated",
776 ToolRuntimeError::TerminationFailed => "termination_failed",
777 ToolRuntimeError::DeadlineExceeded => "deadline_exceeded",
778 };
779 lifecycle.push(ToolLifecycleEvent::RuntimeFailed {
780 tool_action_id: action.clone(),
781 tool_id: tool_id.clone(),
782 code: code.into(),
783 });
784 DispatchOutcome::RuntimeFailed {
785 tool_action_id: action,
786 tool_id: Some(tool_id),
787 code: code.into(),
788 lifecycle,
789 }
790 }
791 }
792 }
793}
794
795async fn await_tool_termination(
797 wait: &mut Pin<&mut impl Future<Output = ToolCompletion>>,
798 control: &ToolExecutionControl,
799 kill: Option<&ToolKillHandle>,
800 policy: &ToolExecutionClass,
801) -> ToolCompletion {
802 control.cancel();
803 let join_grace = Duration::from_millis(200);
804 match policy {
805 ToolExecutionClass::AbortableAtYield { .. } => {
806 if let Some(k) = kill {
807 k.kill();
808 if k.join_timeout(join_grace).await.is_err() {
812 return ToolCompletion::RuntimeFailed(ToolRuntimeError::TerminationFailed);
813 }
814 }
815 match tokio::time::timeout(join_grace, wait).await {
816 Ok(c) => c,
817 Err(_) => ToolCompletion::RuntimeFailed(ToolRuntimeError::DeadlineExceeded),
818 }
819 }
820 ToolExecutionClass::CooperativeInProcess { grace } => {
821 match tokio::time::timeout(*grace, &mut *wait).await {
822 Ok(c) => c,
823 Err(_) => ToolCompletion::RuntimeFailed(ToolRuntimeError::DeadlineExceeded),
824 }
825 }
826 ToolExecutionClass::ProcessIsolated {
827 grace,
828 kill_deadline,
829 } => match tokio::time::timeout(*grace, &mut *wait).await {
830 Ok(c) => c,
831 Err(_) => {
832 if let Some(k) = kill {
833 k.kill();
834 if k.join_timeout(*kill_deadline).await.is_err() {
835 return ToolCompletion::RuntimeFailed(ToolRuntimeError::TerminationFailed);
836 }
837 }
838 match tokio::time::timeout(*kill_deadline, wait).await {
839 Ok(c) => c,
840 Err(_) => ToolCompletion::RuntimeFailed(ToolRuntimeError::TerminationFailed),
841 }
842 }
843 },
844 }
845}
846
847async fn await_tool_termination_driven(
849 wait: &mut Pin<&mut impl Future<Output = ToolCompletion>>,
850 drive: &mut Pin<&mut impl Future<Output = ()>>,
851 control: &ToolExecutionControl,
852 kill: Option<&ToolKillHandle>,
853 policy: &ToolExecutionClass,
854) -> ToolCompletion {
855 control.cancel();
856 match policy {
857 ToolExecutionClass::ProcessIsolated {
858 grace,
859 kill_deadline,
860 } => {
861 tokio::select! {
863 biased;
864 c = &mut *wait => c,
865 _ = &mut *drive => wait.await,
866 _ = tokio::time::sleep(*grace) => {
867 if let Some(k) = kill {
868 k.kill();
869 }
870 tokio::select! {
871 biased;
872 c = &mut *wait => c,
873 _ = &mut *drive => wait.await,
874 _ = tokio::time::sleep(*kill_deadline) => {
875 if let Some(k) = kill {
876 let _ = k.join_timeout(Duration::from_millis(50)).await;
877 }
878 ToolCompletion::RuntimeFailed(ToolRuntimeError::TerminationFailed)
879 }
880 }
881 }
882 }
883 }
884 ToolExecutionClass::AbortableAtYield { grace }
885 | ToolExecutionClass::CooperativeInProcess { grace } => {
886 if let Some(k) = kill {
887 k.kill();
888 }
889 let join_grace = Duration::from_millis(200).max(*grace);
890 tokio::select! {
892 biased;
893 c = &mut *wait => c,
894 _ = &mut *drive => wait.await,
895 _ = tokio::time::sleep(join_grace) => {
896 if let Some(k) = kill {
897 let _ = k.join_timeout(Duration::from_millis(50)).await;
898 }
899 ToolCompletion::RuntimeFailed(ToolRuntimeError::DeadlineExceeded)
901 }
902 }
903 }
904 }
905}
906
907struct DispatchGuard {
912 permit: Option<ToolPermit>,
913 kill: Option<ToolKillHandle>,
914 process_registry: Arc<OwnedProcessRegistry>,
915}
916
917impl DispatchGuard {
918 fn release_if_idle(&mut self) {
920 if self.kill.as_ref().is_some_and(ToolKillHandle::has_join) {
921 return;
922 }
923 self.kill.take();
924 drop(self.permit.take());
925 }
926}
927
928impl Drop for DispatchGuard {
929 fn drop(&mut self) {
930 if let Some(k) = self.kill.take() {
931 k.kill(); if k.is_process_isolated() && k.has_join() {
933 let permit = self.permit.take();
936 self.process_registry.park(k, permit);
937 return;
938 }
939 }
940 drop(self.permit.take());
941 }
942}
943
944fn reject_input(action: ToolActionId, f: InputValidationFailure) -> DispatchOutcome {
945 let (code, message) = match f {
946 InputValidationFailure::OversizedInput => ("oversized_input", "tool input exceeds limit"),
947 InputValidationFailure::InvalidJson => {
948 ("invalid_json", "tool arguments are not valid JSON")
949 }
950 InputValidationFailure::DepthExceeded => ("json_depth_exceeded", "tool arguments too deep"),
951 InputValidationFailure::SchemaInvalid => {
952 ("schema_invalid", "tool arguments fail input schema")
953 }
954 };
955 let _ = CanonicalToolError::try_new(code, message, None, 256);
957 let _ = CanonicalToolOutput::Text(String::new());
958 DispatchOutcome::Rejected {
959 tool_action_id: action,
960 code,
961 message: message.into(),
962 lifecycle: vec![],
963 }
964}