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#[non_exhaustive]
22/// Source of a tool call prediction.
23#[derive(Debug, Clone, PartialEq, Eq)]
24pub enum PredictionSource {
25    /// Produced from live `ToolStream` partial tokens (issue #2290).
26    StreamPartial,
27    /// Produced from `SQLite` `tool_pattern_transitions` table (issue #2409).
28    HistoryPattern { skill: String, rank: u8 },
29}
30
31/// A predicted tool call, ready for speculative dispatch.
32#[derive(Debug, Clone)]
33pub struct Prediction {
34    /// Tool identifier.
35    pub tool_id: ToolName,
36    /// Reconstructed argument map from partial JSON or pattern history.
37    pub args: serde_json::Map<String, serde_json::Value>,
38    /// Confidence score in `[0.0, 1.0]`.
39    pub confidence: f32,
40    /// Where this prediction came from.
41    pub source: PredictionSource,
42}
43
44impl Prediction {
45    /// Convert to a [`ToolCall`] for dispatch through the executor.
46    #[must_use]
47    pub fn to_tool_call(&self, call_id: impl Into<String>) -> ToolCall {
48        ToolCall {
49            tool_id: self.tool_id.clone(),
50            params: self.args.clone(),
51            caller_id: Some(call_id.into()),
52            context: None,
53            tool_call_id: String::new(),
54            skill_name: None,
55        }
56    }
57}
58
59/// Check whether a `ToolError` represents a confirmation gate hit.
60///
61/// Used as a gate before speculative dispatch: if the executor would return
62/// `ConfirmationRequired`, speculation is skipped for that call.
63#[must_use]
64pub fn is_confirmation_error(err: &ToolError) -> bool {
65    matches!(err, ToolError::ConfirmationRequired { .. })
66}