tirea_contract/runtime/tool_call/
gate.rs

1use crate::runtime::tool_call::{PendingToolCall, Suspension, ToolCallResumeMode, ToolResult};
2use crate::thread::ToolCall;
3use serde::{Deserialize, Serialize};
4use serde_json::Value;
5
6/// Tool-gate extension: per-tool-call execution control.
7///
8/// Populated by `BlockTool`, `AllowTool`, `SuspendTool`,
9/// `OverrideToolResult` actions during `BeforeToolExecute`.
10#[derive(Debug, Clone)]
11pub struct ToolGate {
12    /// Tool call ID.
13    pub id: String,
14    /// Tool name.
15    pub name: String,
16    /// Tool arguments.
17    pub args: Value,
18    /// Tool execution result (set after execution or by override).
19    pub result: Option<ToolResult>,
20    /// Whether execution is blocked.
21    pub blocked: bool,
22    /// Block reason.
23    pub block_reason: Option<String>,
24    /// Whether execution is pending user confirmation.
25    pub pending: bool,
26    /// Canonical suspend ticket carrying pause payload.
27    pub suspend_ticket: Option<SuspendTicket>,
28}
29
30impl ToolGate {
31    /// Create a new tool gate from identifiers and arguments.
32    pub fn new(id: impl Into<String>, name: impl Into<String>, args: Value) -> Self {
33        Self {
34            id: id.into(),
35            name: name.into(),
36            args,
37            result: None,
38            blocked: false,
39            block_reason: None,
40            pending: false,
41            suspend_ticket: None,
42        }
43    }
44
45    /// Create from a `ToolCall`.
46    pub fn from_tool_call(call: &ToolCall) -> Self {
47        Self::new(&call.id, &call.name, call.arguments.clone())
48    }
49
50    /// Check if the tool execution is blocked.
51    pub fn is_blocked(&self) -> bool {
52        self.blocked
53    }
54
55    /// Check if the tool execution is pending.
56    pub fn is_pending(&self) -> bool {
57        self.pending
58    }
59
60    /// Stable idempotency key for this tool invocation.
61    pub fn idempotency_key(&self) -> &str {
62        &self.id
63    }
64}
65
66/// Suspension payload for `ToolCallAction::Suspend`.
67#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)]
68pub struct SuspendTicket {
69    /// External suspension payload.
70    #[serde(default)]
71    pub suspension: Suspension,
72    /// Pending call projection emitted to event stream.
73    #[serde(default)]
74    pub pending: PendingToolCall,
75    /// Resume mapping strategy.
76    #[serde(default)]
77    pub resume_mode: ToolCallResumeMode,
78}
79
80impl SuspendTicket {
81    pub fn new(
82        suspension: Suspension,
83        pending: PendingToolCall,
84        resume_mode: ToolCallResumeMode,
85    ) -> Self {
86        Self {
87            suspension,
88            pending,
89            resume_mode,
90        }
91    }
92
93    pub fn use_decision_as_tool_result(suspension: Suspension, pending: PendingToolCall) -> Self {
94        Self::new(
95            suspension,
96            pending,
97            ToolCallResumeMode::UseDecisionAsToolResult,
98        )
99    }
100
101    pub fn with_resume_mode(mut self, resume_mode: ToolCallResumeMode) -> Self {
102        self.resume_mode = resume_mode;
103        self
104    }
105
106    pub fn with_pending(mut self, pending: PendingToolCall) -> Self {
107        self.pending = pending;
108        self
109    }
110}
111
112/// Tool-call level control action emitted by plugins.
113#[derive(Debug, Clone, PartialEq)]
114pub enum ToolCallAction {
115    Proceed,
116    Suspend(Box<SuspendTicket>),
117    Block { reason: String },
118}
119
120impl ToolCallAction {
121    pub fn suspend(ticket: SuspendTicket) -> Self {
122        Self::Suspend(Box::new(ticket))
123    }
124}