chio_kernel/kernel/evaluator.rs
1//! Tool-call evaluation surface.
2//!
3//! Defines the [`ToolEvaluator`] trait that names the four logical phases of
4//! a tool-call evaluation (capability validation, guard pipeline, dispatch,
5//! receipt signing). The public `ChioKernel::evaluate_tool_call().await`
6//! entrypoint uses the async-native kernel path directly. The
7//! [`BlockingToolEvaluator`] remains for compatibility surfaces that
8//! intentionally enter the synchronous bridge.
9//!
10//! Futures dropped after budget admission are handled by the post-admission
11//! drop guard: a cancellation receipt is recorded whenever dispatch was in
12//! flight and runtime-admission reservations get an explicit fail-closed
13//! disposition. Hard process death mid-dispatch remains the charter of the
14//! dispatch-intent journal.
15
16use crate::kernel::ChioKernel;
17use crate::{
18 ChioReceipt, ChioReceiptBody, KernelError, ToolCallRequest, ToolCallResponse,
19 ToolInvocationCost, ToolServerOutput, Verdict,
20};
21
22/// The four logical phases of a tool-call evaluation, surfaced as an
23/// async-capable trait so each phase can be replaced with an async-native
24/// implementation without re-shaping the public surface.
25///
26/// The default [`BlockingToolEvaluator`] preserves the semantics of
27/// `ChioKernel::evaluate_tool_call_sync_inner` by delegating to the
28/// `evaluate_tool_call_sync` shim. The four step methods
29/// (`validate_capability`, `run_guards`, `dispatch`, `sign_receipt`) default
30/// to forwarding through the full synchronous pipeline; override them to swap
31/// in async-native step bodies.
32#[allow(async_fn_in_trait)]
33pub trait ToolEvaluator: Send + Sync {
34 /// Run the full evaluation pipeline for `request` against `kernel` and
35 /// return the resulting `ToolCallResponse`.
36 async fn evaluate(
37 &self,
38 kernel: &ChioKernel,
39 request: &ToolCallRequest,
40 ) -> Result<ToolCallResponse, KernelError>;
41
42 /// Run the full evaluation pipeline with additional receipt metadata.
43 async fn evaluate_with_metadata(
44 &self,
45 kernel: &ChioKernel,
46 request: &ToolCallRequest,
47 extra_metadata: Option<serde_json::Value>,
48 ) -> Result<ToolCallResponse, KernelError> {
49 let _ = extra_metadata;
50 self.evaluate(kernel, request).await
51 }
52
53 /// Validate the capability token attached to `request`.
54 async fn validate_capability(
55 &self,
56 kernel: &ChioKernel,
57 request: &ToolCallRequest,
58 ) -> Result<Verdict, KernelError> {
59 let response = self.evaluate(kernel, request).await?;
60 Ok(response.verdict)
61 }
62
63 /// Run the registered guard pipeline against `request`.
64 async fn run_guards(
65 &self,
66 kernel: &ChioKernel,
67 request: &ToolCallRequest,
68 ) -> Result<Verdict, KernelError> {
69 let response = self.evaluate(kernel, request).await?;
70 Ok(response.verdict)
71 }
72
73 /// Direct phase dispatch is unavailable because it cannot retain the
74 /// admission operation, compensation, outcome, and receipt as one durable
75 /// lifecycle. Use [`ToolEvaluator::evaluate`] instead.
76 ///
77 /// This default denies every direct dispatch, not only monetary ones.
78 /// Implementors that override `dispatch` are unaffected; those that relied on
79 /// the default should read `docs/migrations/kernel-embedder-surface.md`.
80 async fn dispatch(
81 &self,
82 kernel: &ChioKernel,
83 request: &ToolCallRequest,
84 has_monetary_grant: bool,
85 ) -> Result<(ToolServerOutput, Option<ToolInvocationCost>), KernelError> {
86 let _ = (kernel, request, has_monetary_grant);
87 Err(KernelError::DirectDispatchUnavailable)
88 }
89
90 /// Sign the receipt for the (allow or deny) outcome of a tool call.
91 ///
92 /// Accepts a fully-constructed [`ChioReceiptBody`] plus the exact byte
93 /// preimage its `content_hash` was derived from, and returns the signed
94 /// [`ChioReceipt`]. The default body routes through
95 /// `kernel.sign_receipt_via_channel` (the mpsc-backed signing task);
96 /// producers wait on bounded backpressure, never on a receipt-log mutex.
97 /// The signed receipt is byte-identical to the inline
98 /// `build_and_sign_receipt` path, and equally fail-closed: both delegate to
99 /// `chio_kernel_core::sign_receipt_with_handle`, which recomputes
100 /// `content_hash` over `canonical_content` and refuses to sign on mismatch
101 /// (WYSIWYS).
102 async fn sign_receipt(
103 &self,
104 kernel: &ChioKernel,
105 body: ChioReceiptBody,
106 canonical_content: Vec<u8>,
107 ) -> Result<ChioReceipt, KernelError> {
108 kernel
109 .sign_receipt_via_channel(body, canonical_content)
110 .await
111 }
112}
113
114/// Compatibility [`ToolEvaluator`] implementation: delegates the entire
115/// pipeline to the synchronous flow on [`ChioKernel`].
116///
117/// Inside a multi-threaded tokio runtime the call is wrapped in
118/// `tokio::task::block_in_place` so the worker thread is released back to
119/// the scheduler while the synchronous body runs. Outside such a runtime
120/// the call is direct; if that direct path enters a current-thread runtime,
121/// the kernel bridge returns a typed sync-bridge incompatibility error before
122/// dispatch side effects.
123#[allow(dead_code)]
124#[derive(Debug, Default, Clone, Copy)]
125pub struct BlockingToolEvaluator;
126
127impl ToolEvaluator for BlockingToolEvaluator {
128 async fn evaluate(
129 &self,
130 kernel: &ChioKernel,
131 request: &ToolCallRequest,
132 ) -> Result<ToolCallResponse, KernelError> {
133 // Reach into the existing synchronous pipeline. The wrapper isolates
134 // the (potentially blocking) sync work from the async runtime when
135 // we are inside one; otherwise it is a direct call.
136 match tokio::runtime::Handle::try_current() {
137 Ok(handle) if handle.runtime_flavor() == tokio::runtime::RuntimeFlavor::MultiThread => {
138 tokio::task::block_in_place(|| kernel.evaluate_tool_call_sync(request))
139 }
140 _ => kernel.evaluate_tool_call_sync(request),
141 }
142 }
143
144 async fn evaluate_with_metadata(
145 &self,
146 kernel: &ChioKernel,
147 request: &ToolCallRequest,
148 extra_metadata: Option<serde_json::Value>,
149 ) -> Result<ToolCallResponse, KernelError> {
150 match tokio::runtime::Handle::try_current() {
151 Ok(handle) if handle.runtime_flavor() == tokio::runtime::RuntimeFlavor::MultiThread => {
152 tokio::task::block_in_place(|| {
153 kernel.evaluate_tool_call_blocking_with_metadata(request, extra_metadata)
154 })
155 }
156 _ => kernel.evaluate_tool_call_blocking_with_metadata(request, extra_metadata),
157 }
158 }
159}