zeph_core/agent/speculative/prediction.rs
1// SPDX-FileCopyrightText: 2026 Andrei G <bug-ops>
2// SPDX-License-Identifier: MIT OR Apache-2.0
3
4//! Decoding-level speculation: `ToolCallPredictor`.
5//!
6//! Drains the LLM `ToolStream` and fires speculative dispatches when the partial
7//! JSON parser reports that all required fields are present for a tool call.
8//!
9//! Gate invariants enforced before dispatch (in order):
10//! 1. `SpeculationMode` is `Decoding` or `Both`.
11//! 2. `executor.is_tool_speculatable(tool_id)` returns `true`.
12//! 3. `trust_level == TrustLevel::Trusted` (forwarded from the agent's current state).
13//! 4. Calling `executor.execute_tool_call(call)` would NOT return `ConfirmationRequired`
14//! — checked by attempting a dry-run classification (see `is_confirmation_required`).
15
16#![allow(dead_code)]
17
18use zeph_common::ToolName;
19use zeph_tools::{ToolCall, ToolError};
20
21/// Source of a tool call prediction.
22#[derive(Debug, Clone, PartialEq, Eq)]
23pub enum PredictionSource {
24 /// Produced from live `ToolStream` partial tokens (issue #2290).
25 StreamPartial,
26 /// Produced from `SQLite` `tool_pattern_transitions` table (issue #2409).
27 HistoryPattern { skill: String, rank: u8 },
28}
29
30/// A predicted tool call, ready for speculative dispatch.
31#[derive(Debug, Clone)]
32pub struct Prediction {
33 /// Tool identifier.
34 pub tool_id: ToolName,
35 /// Reconstructed argument map from partial JSON or pattern history.
36 pub args: serde_json::Map<String, serde_json::Value>,
37 /// Confidence score in `[0.0, 1.0]`.
38 pub confidence: f32,
39 /// Where this prediction came from.
40 pub source: PredictionSource,
41}
42
43impl Prediction {
44 /// Convert to a [`ToolCall`] for dispatch through the executor.
45 #[must_use]
46 pub fn to_tool_call(&self, call_id: impl Into<String>) -> ToolCall {
47 ToolCall {
48 tool_id: self.tool_id.clone(),
49 params: self.args.clone(),
50 caller_id: Some(call_id.into()),
51 }
52 }
53}
54
55/// Check whether a `ToolError` represents a confirmation gate hit.
56///
57/// Used as a gate before speculative dispatch: if the executor would return
58/// `ConfirmationRequired`, speculation is skipped for that call.
59#[must_use]
60pub fn is_confirmation_error(err: &ToolError) -> bool {
61 matches!(err, ToolError::ConfirmationRequired { .. })
62}