Skip to main content

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}