Skip to main content

harn_vm/agent_events/
tool.rs

1use serde::{Deserialize, Serialize};
2
3/// Status of a tool call. Mirrors ACP's `toolCallStatus`.
4#[derive(Clone, Copy, Debug, Eq, PartialEq, Hash, Serialize, Deserialize)]
5#[serde(rename_all = "snake_case")]
6pub enum ToolCallStatus {
7    /// Dispatched by the model but not yet started.
8    Pending,
9    /// Dispatch is actively running.
10    InProgress,
11    /// Finished successfully.
12    Completed,
13    /// Finished with an error.
14    Failed,
15}
16
17impl ToolCallStatus {
18    pub const ALL: [Self; 4] = [
19        Self::Pending,
20        Self::InProgress,
21        Self::Completed,
22        Self::Failed,
23    ];
24
25    pub fn as_str(self) -> &'static str {
26        match self {
27            Self::Pending => "pending",
28            Self::InProgress => "in_progress",
29            Self::Completed => "completed",
30            Self::Failed => "failed",
31        }
32    }
33}
34
35/// Wire-level classification of a `ToolCallUpdate` failure. Pairs with the
36/// human-readable `error` string so clients can render each failure type
37/// distinctly (e.g. surface a "permission denied" badge, or a different
38/// retry affordance for `network` vs `tool_error`). The enum is
39/// deliberately extensible — `unknown` is the default when the runtime
40/// could not classify a failure.
41#[derive(Clone, Copy, Debug, Eq, PartialEq, Hash, Serialize, Deserialize)]
42#[serde(rename_all = "snake_case")]
43pub enum ToolCallErrorCategory {
44    /// Host-side validation rejected the args (missing required field,
45    /// invalid type, malformed JSON).
46    SchemaValidation,
47    /// The tool ran and returned an error result (e.g. `read_file` on a
48    /// missing path) — distinguished from a transport failure.
49    ToolError,
50    /// MCP transport / server-protocol error.
51    McpServerError,
52    /// Burin Swift host bridge returned an error during dispatch.
53    HostBridgeError,
54    /// `session/request_permission` denied by the client, or a policy
55    /// rule (static or dynamic) refused the call.
56    PermissionDenied,
57    /// The harn loop detector skipped this call because the same
58    /// (tool, args) pair repeated past the configured threshold.
59    RejectedLoop,
60    /// Streaming text candidate was detected (bare `name(` or
61    /// `<tool_call>` opener) but never resolved into a parseable call:
62    /// args parsed as malformed, the heredoc body broke, the tag closed
63    /// without a balanced expression, or the stream ended mid-call.
64    /// Used by the streaming candidate detector (harn#692) to retract a
65    /// `tool_call` candidate that turned out to be prose or syntactically
66    /// broken so clients can dismiss the in-flight chip.
67    ParseAborted,
68    /// The tool exceeded its time budget.
69    Timeout,
70    /// Transient network / rate-limited / 5xx provider failure.
71    Network,
72    /// The tool was cancelled (e.g. session aborted).
73    Cancelled,
74    /// Default when classification was not performed.
75    Unknown,
76}
77
78impl ToolCallErrorCategory {
79    pub const ALL: [Self; 11] = [
80        Self::SchemaValidation,
81        Self::ToolError,
82        Self::McpServerError,
83        Self::HostBridgeError,
84        Self::PermissionDenied,
85        Self::RejectedLoop,
86        Self::ParseAborted,
87        Self::Timeout,
88        Self::Network,
89        Self::Cancelled,
90        Self::Unknown,
91    ];
92
93    pub fn as_str(self) -> &'static str {
94        match self {
95            Self::SchemaValidation => "schema_validation",
96            Self::ToolError => "tool_error",
97            Self::McpServerError => "mcp_server_error",
98            Self::HostBridgeError => "host_bridge_error",
99            Self::PermissionDenied => "permission_denied",
100            Self::RejectedLoop => "rejected_loop",
101            Self::ParseAborted => "parse_aborted",
102            Self::Timeout => "timeout",
103            Self::Network => "network",
104            Self::Cancelled => "cancelled",
105            Self::Unknown => "unknown",
106        }
107    }
108
109    /// Map an internal `ErrorCategory` (used by the VM's `VmError`
110    /// classification) onto the wire enum. The internal taxonomy is
111    /// finer-grained — several transient categories collapse onto
112    /// `Network`, and the auth/quota family becomes `HostBridgeError`
113    /// because at the tool-dispatch boundary those errors come from
114    /// the bridge transport rather than the tool itself.
115    pub fn from_internal(category: &crate::value::ErrorCategory) -> Self {
116        use crate::value::ErrorCategory as Internal;
117        match category {
118            Internal::Timeout => Self::Timeout,
119            Internal::RateLimit
120            | Internal::Overloaded
121            | Internal::ServerError
122            | Internal::TransientNetwork => Self::Network,
123            Internal::SchemaValidation | Internal::SchemaStreamAborted => Self::SchemaValidation,
124            Internal::ToolError => Self::ToolError,
125            Internal::ToolRejected => Self::PermissionDenied,
126            Internal::Cancelled => Self::Cancelled,
127            Internal::Auth
128            | Internal::EgressBlocked
129            | Internal::NotFound
130            | Internal::CircuitOpen
131            | Internal::BudgetExceeded
132            | Internal::Generic => Self::HostBridgeError,
133        }
134    }
135}
136
137/// Where a tool actually ran. Tags `ToolCallUpdate` so clients can render
138/// "via mcp:linear" / "via host bridge" badges, attribute latency by
139/// transport, and route errors to the right surface (harn#691).
140///
141/// On the wire this serializes adjacently-tagged so the `mcp_server`
142/// case carries the configured server name. The ACP adapter rewrites
143/// unit variants as bare strings (`"harn_builtin"`, `"host_bridge"`,
144/// `"provider_native"`) and the `McpServer` case as
145/// `{"kind": "mcp_server", "serverName": "..."}` to match the protocol's
146/// camelCase convention.
147#[derive(Clone, Debug, Eq, PartialEq, Hash, Serialize, Deserialize)]
148#[serde(tag = "kind", rename_all = "snake_case")]
149pub enum ToolExecutor {
150    /// VM-stdlib (`read_file`, `write_file`, `exec`, `http_*`, `mcp_*`)
151    /// or any Harn-side handler closure registered in `tools_val`.
152    HarnBuiltin,
153    /// Capability provided by the host through `HostBridge.builtin_call`
154    /// (Swift-side IDE bridge, BurinApp, BurinCLI host shells).
155    HostBridge,
156    /// Tool dispatched against a configured MCP server. Detected by the
157    /// `_mcp_server` tag that `mcp_list_tools` injects on every tool
158    /// dict before the agent loop sees it.
159    McpServer { server_name: String },
160    /// Provider-side server-side tool execution — currently OpenAI
161    /// Responses-API server tools (e.g. native `tool_search`). The
162    /// runtime never dispatches these locally; the model returns the
163    /// already-executed result inline.
164    ProviderNative,
165}