Skip to main content

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}