Skip to main content

everruns_provider/
tool_types.rs

1// Tool definitions and policies for agent execution
2//
3// Design Decision: Tools are identified by name (string) for extensibility.
4// The BuiltinToolKind enum has been removed to allow adding new tools
5// without code changes. Tool execution happens via the ToolRegistry
6// which looks up tools by name.
7
8use serde::{Deserialize, Serialize};
9use serde_json::Value;
10
11#[cfg(feature = "openapi")]
12use utoipa::ToSchema;
13
14pub const HUMAN_INTENT_ARGUMENT: &str = "human_intent";
15
16/// An image returned by a tool execution.
17///
18/// This allows tools (built-in or MCP) to return images that are sent
19/// to the LLM as native image content blocks, not stringified JSON.
20#[derive(Debug, Clone, Serialize, Deserialize)]
21pub struct ToolResultImage {
22    /// Base64-encoded image data
23    pub base64: String,
24    /// MIME type (e.g., "image/png", "image/jpeg")
25    pub media_type: String,
26}
27
28const HUMAN_INTENT_DESCRIPTION: &str = "Short user-facing narration of what this tool call will do, written as an action phrase like \"Listing all harnesses\". Do not include hidden reasoning, private chain of thought, secrets, or credential values.";
29
30/// Tool policy determines how tool calls are handled
31#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, Default)]
32#[cfg_attr(feature = "openapi", derive(ToSchema))]
33#[serde(rename_all = "snake_case")]
34pub enum ToolPolicy {
35    /// Execute immediately without user approval
36    #[default]
37    Auto,
38    /// Require user approval before execution (HITL)
39    RequiresApproval,
40    /// Client-side tool: pause workflow, send to client for execution
41    ClientSide,
42}
43
44/// Controls whether a tool's full schema can be deferred (tool_search).
45///
46/// When tool_search is active and a model supports it, tools marked as
47/// `Automatic` or `Always` will have `defer_loading: true` set, meaning
48/// only the name+description are sent upfront and full parameter schemas
49/// are loaded on-demand by the model.
50#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, Default)]
51#[cfg_attr(feature = "openapi", derive(ToSchema))]
52#[serde(rename_all = "snake_case")]
53pub enum DeferrablePolicy {
54    /// Never defer — always send full schema (e.g., high-frequency tools like write_todos)
55    Never,
56    /// Let the driver decide based on tool count threshold (default)
57    #[default]
58    Automatic,
59    /// Always defer when tool_search is active, regardless of threshold
60    Always,
61}
62
63impl DeferrablePolicy {
64    /// Returns true when the value is the default (`Automatic`).
65    pub fn is_default(&self) -> bool {
66        matches!(self, DeferrablePolicy::Automatic)
67    }
68}
69
70/// Tool definition in agent configuration
71#[derive(Debug, Clone, Serialize, Deserialize)]
72#[cfg_attr(feature = "openapi", derive(ToSchema))]
73#[serde(tag = "type", rename_all = "snake_case")]
74pub enum ToolDefinition {
75    /// Built-in tool - executed by the worker via ToolRegistry
76    Builtin(BuiltinTool),
77    /// Client-side tool - executed by the client, not the server
78    ClientSide(ClientSideTool),
79}
80
81/// Built-in tool configuration
82///
83/// Note: The `kind` field has been removed. Tools are now identified
84/// solely by their `name` field, and execution happens via the ToolRegistry
85/// which looks up tools by name.
86#[derive(Debug, Clone, Serialize, Deserialize)]
87#[cfg_attr(feature = "openapi", derive(ToSchema))]
88pub struct BuiltinTool {
89    /// Tool name (used by LLM and for registry lookup)
90    pub name: String,
91    /// Human-readable display name for UI rendering (e.g., "Get Current Time" for `get_current_time`)
92    #[serde(default, skip_serializing_if = "Option::is_none")]
93    pub display_name: Option<String>,
94    /// Tool description for LLM
95    pub description: String,
96    /// JSON schema for tool parameters
97    pub parameters: serde_json::Value,
98    /// Tool policy (auto or requires_approval)
99    #[serde(default)]
100    pub policy: ToolPolicy,
101    /// Category for tool_search namespace grouping (from parent capability)
102    #[serde(default, skip_serializing_if = "Option::is_none")]
103    pub category: Option<String>,
104    /// Whether this tool's schema can be deferred via tool_search
105    #[serde(default, skip_serializing_if = "DeferrablePolicy::is_default")]
106    pub deferrable: DeferrablePolicy,
107    /// Semantic hints describing the tool's behavioral properties
108    #[serde(default, skip_serializing_if = "ToolHints::is_empty")]
109    pub hints: ToolHints,
110    /// Original full parameter schema saved by `DeferSchemaHook` before stripping.
111    /// Serialized only when present so durable reason-to-act scheduling can
112    /// preserve deferred schemas for `tool_search` in the act phase.
113    #[serde(default, skip_serializing_if = "Option::is_none")]
114    pub full_parameters: Option<serde_json::Value>,
115}
116
117/// Client-side tool - executed by the client, not the server
118/// The server pauses execution and waits for the client to submit results.
119#[derive(Debug, Clone, Serialize, Deserialize)]
120#[cfg_attr(feature = "openapi", derive(ToSchema))]
121pub struct ClientSideTool {
122    /// Tool name (used by LLM and for correlation)
123    pub name: String,
124    /// Human-readable display name for UI rendering
125    #[serde(default, skip_serializing_if = "Option::is_none")]
126    pub display_name: Option<String>,
127    /// Tool description for LLM
128    pub description: String,
129    /// JSON schema for tool parameters
130    pub parameters: serde_json::Value,
131    /// Category for tool_search namespace grouping (from parent capability)
132    #[serde(default, skip_serializing_if = "Option::is_none")]
133    pub category: Option<String>,
134    /// Whether this tool's schema can be deferred via tool_search
135    #[serde(default, skip_serializing_if = "DeferrablePolicy::is_default")]
136    pub deferrable: DeferrablePolicy,
137    /// Semantic hints describing the tool's behavioral properties
138    #[serde(default, skip_serializing_if = "ToolHints::is_empty")]
139    pub hints: ToolHints,
140    /// Original full parameter schema saved by `DeferSchemaHook` before stripping.
141    /// Serialized only when present so durable reason-to-act scheduling can
142    /// preserve deferred schemas for `tool_search` in the act phase.
143    #[serde(default, skip_serializing_if = "Option::is_none")]
144    pub full_parameters: Option<serde_json::Value>,
145}
146
147impl ToolDefinition {
148    /// Get the tool name regardless of variant
149    pub fn name(&self) -> &str {
150        match self {
151            ToolDefinition::Builtin(b) => &b.name,
152            ToolDefinition::ClientSide(c) => &c.name,
153        }
154    }
155
156    /// Get the tool display name regardless of variant
157    pub fn display_name(&self) -> Option<&str> {
158        match self {
159            ToolDefinition::Builtin(b) => b.display_name.as_deref(),
160            ToolDefinition::ClientSide(c) => c.display_name.as_deref(),
161        }
162    }
163
164    /// Get the tool description regardless of variant
165    pub fn description(&self) -> &str {
166        match self {
167            ToolDefinition::Builtin(b) => &b.description,
168            ToolDefinition::ClientSide(c) => &c.description,
169        }
170    }
171
172    /// Get the tool parameters schema regardless of variant
173    pub fn parameters(&self) -> &serde_json::Value {
174        match self {
175            ToolDefinition::Builtin(b) => &b.parameters,
176            ToolDefinition::ClientSide(c) => &c.parameters,
177        }
178    }
179
180    /// Get the full (pre-deferral) parameter schema, falling back to `parameters()`.
181    ///
182    /// When `DeferSchemaHook` strips a tool's schema it saves the original in
183    /// `full_parameters`. Callers that need the real schema (e.g. `tool_search`)
184    /// should use this method so deferred tools still return useful results.
185    pub fn full_parameters(&self) -> &serde_json::Value {
186        match self {
187            ToolDefinition::Builtin(b) => b.full_parameters.as_ref().unwrap_or(&b.parameters),
188            ToolDefinition::ClientSide(c) => c.full_parameters.as_ref().unwrap_or(&c.parameters),
189        }
190    }
191
192    /// Get the tool policy regardless of variant
193    pub fn policy(&self) -> &ToolPolicy {
194        match self {
195            ToolDefinition::Builtin(b) => &b.policy,
196            ToolDefinition::ClientSide(_) => &ToolPolicy::ClientSide,
197        }
198    }
199
200    /// Get the tool category for namespace grouping
201    pub fn category(&self) -> Option<&str> {
202        match self {
203            ToolDefinition::Builtin(b) => b.category.as_deref(),
204            ToolDefinition::ClientSide(c) => c.category.as_deref(),
205        }
206    }
207
208    /// Get the deferrable policy for tool_search
209    pub fn deferrable(&self) -> &DeferrablePolicy {
210        match self {
211            ToolDefinition::Builtin(b) => &b.deferrable,
212            ToolDefinition::ClientSide(c) => &c.deferrable,
213        }
214    }
215
216    /// Get the tool hints
217    pub fn hints(&self) -> &ToolHints {
218        match self {
219            ToolDefinition::Builtin(b) => &b.hints,
220            ToolDefinition::ClientSide(c) => &c.hints,
221        }
222    }
223
224    /// Scheduling conflict key for this tool, if any (see
225    /// `ToolHints::concurrency_class`). `None` means the tool has no mutation
226    /// conflicts and may always run concurrently with others.
227    pub fn concurrency_class(&self) -> Option<&str> {
228        self.hints().concurrency_class.as_deref()
229    }
230
231    /// Whether this tool performs CPU-bound/non-yielding in-process work and
232    /// should be offloaded to its own task by the act scheduler.
233    pub fn is_cpu_bound(&self) -> bool {
234        self.hints().cpu_bound.unwrap_or(false)
235    }
236
237    /// Effective side-effect class for this tool (defaults to `AtMostOnce`).
238    pub fn side_effect_class(&self) -> SideEffectClass {
239        self.hints().effective_side_effect_class()
240    }
241
242    /// Get reporting attribution for the capability that contributed this tool.
243    pub fn capability_attribution(&self) -> Option<(&str, Option<&str>)> {
244        self.hints()
245            .capability_id
246            .as_deref()
247            .map(|id| (id, self.hints().capability_name.as_deref()))
248    }
249
250    /// Set the category on this tool definition (builder pattern)
251    pub fn with_category(mut self, category: impl Into<String>) -> Self {
252        match &mut self {
253            ToolDefinition::Builtin(b) => b.category = Some(category.into()),
254            ToolDefinition::ClientSide(c) => c.category = Some(category.into()),
255        }
256        self
257    }
258
259    /// Set the hints on this tool definition (builder pattern)
260    pub fn with_hints(mut self, hints: ToolHints) -> Self {
261        match &mut self {
262            ToolDefinition::Builtin(b) => b.hints = hints,
263            ToolDefinition::ClientSide(c) => c.hints = hints,
264        }
265        self
266    }
267
268    /// Set reporting attribution on this tool definition (builder pattern).
269    pub fn with_capability_attribution(
270        mut self,
271        capability_id: impl Into<String>,
272        capability_name: Option<impl Into<String>>,
273    ) -> Self {
274        let capability_id = capability_id.into();
275        let capability_name = capability_name.map(Into::into);
276        match &mut self {
277            ToolDefinition::Builtin(b) => {
278                b.hints.capability_id = Some(capability_id);
279                b.hints.capability_name = capability_name;
280            }
281            ToolDefinition::ClientSide(c) => {
282                c.hints.capability_id = Some(capability_id);
283                c.hints.capability_name = capability_name;
284            }
285        }
286        self
287    }
288
289    /// Add the cross-cutting `human_intent` argument to the tool's JSON schema.
290    ///
291    /// This field is model-authored narration for UI rendering. Tool execution
292    /// strips it before invoking the underlying tool implementation.
293    pub fn with_human_intent_argument(mut self) -> Self {
294        match &mut self {
295            ToolDefinition::Builtin(b) => add_human_intent_to_schema(&mut b.parameters),
296            ToolDefinition::ClientSide(c) => add_human_intent_to_schema(&mut c.parameters),
297        }
298        self
299    }
300}
301
302pub fn add_human_intent_to_tool_definitions(tools: &[ToolDefinition]) -> Vec<ToolDefinition> {
303    tools
304        .iter()
305        .cloned()
306        .map(ToolDefinition::with_human_intent_argument)
307        .collect()
308}
309
310pub fn human_intent(arguments: &Value) -> Option<&str> {
311    arguments
312        .get(HUMAN_INTENT_ARGUMENT)
313        .and_then(Value::as_str)
314        .map(str::trim)
315        .filter(|value| !value.is_empty())
316}
317
318pub fn strip_human_intent_argument(arguments: &Value) -> Value {
319    let mut stripped = arguments.clone();
320    if let Value::Object(ref mut object) = stripped {
321        object.remove(HUMAN_INTENT_ARGUMENT);
322    }
323    stripped
324}
325
326fn add_human_intent_to_schema(schema: &mut Value) {
327    let Value::Object(schema_obj) = schema else {
328        return;
329    };
330
331    schema_obj
332        .entry("type")
333        .or_insert_with(|| Value::String("object".to_string()));
334
335    let properties = schema_obj
336        .entry("properties")
337        .or_insert_with(|| Value::Object(serde_json::Map::new()));
338    if let Value::Object(properties_obj) = properties {
339        properties_obj.insert(
340            HUMAN_INTENT_ARGUMENT.to_string(),
341            serde_json::json!({
342                "type": "string",
343                "description": HUMAN_INTENT_DESCRIPTION,
344                "maxLength": 120,
345            }),
346        );
347    }
348
349    // `human_intent` is intentionally optional: models should provide it when
350    // useful, but old calls, provider quirks, and client-side calls remain valid.
351}
352
353/// How many times a tool call may safely be executed given the same inputs.
354///
355/// Used by the durable Act activity (EVE-530) to decide what to do when a
356/// prior execution attempt left a `running` claim in `durable_tool_results`:
357///
358/// * `Pure` / `Idempotent` — the running claim is stale; re-execute freely.
359/// * `AtMostOnce` — never re-execute from a stale running claim; settle it
360///   as `interrupted` and surface an uncertain result to the model instead.
361///
362/// When unset (`None`), the conservative default is `AtMostOnce`.
363#[derive(Debug, Clone, Serialize, Deserialize, Default, PartialEq, Eq)]
364#[cfg_attr(feature = "openapi", derive(ToSchema))]
365pub enum SideEffectClass {
366    /// No external side effects; always safe to re-execute (e.g. read-only queries).
367    Pure,
368    /// Idempotent side effects; safe to re-execute with the same arguments
369    /// (e.g. create-or-update, PUT-style writes).
370    Idempotent,
371    /// Exactly-once semantics required; must not be re-executed from a stale
372    /// running claim (e.g. charge a card, send an email, open a PR).
373    #[default]
374    AtMostOnce,
375}
376
377/// Semantic hints describing a tool's behavioral properties.
378///
379/// Follows the MCP tool annotations convention (readOnlyHint, destructiveHint,
380/// idempotentHint, openWorldHint) plus everruns-specific hints. All fields are
381/// optional booleans — `None` means "unknown/unspecified". Consumers should
382/// treat `None` as the conservative default (e.g., assume not readonly, assume
383/// not idempotent).
384///
385/// These hints are informational — they do not enforce policy. Use `ToolPolicy`
386/// for execution gating (auto vs requires_approval).
387// `Eq` is deliberately absent: `metadata` is an opaque `serde_json::Value`, which
388// is only `PartialEq`. Hints are compared for equality (`is_empty`), never hashed
389// or used as a map key, so `PartialEq` is sufficient.
390#[derive(Debug, Clone, Serialize, Deserialize, Default, PartialEq)]
391#[cfg_attr(feature = "openapi", derive(ToSchema))]
392pub struct ToolHints {
393    /// Tool does not modify any state (read-only queries, lookups).
394    /// When true: safe to call speculatively, result can be cached.
395    #[serde(default, skip_serializing_if = "Option::is_none")]
396    pub readonly: Option<bool>,
397
398    /// Tool may irreversibly destroy or delete data.
399    /// Subset of non-readonly — a tool can be non-readonly (writes) without
400    /// being destructive (e.g., create/update operations).
401    #[serde(default, skip_serializing_if = "Option::is_none")]
402    pub destructive: Option<bool>,
403
404    /// Calling the tool repeatedly with the same arguments produces the same
405    /// effect. Safe to retry on transient failures.
406    #[serde(default, skip_serializing_if = "Option::is_none")]
407    pub idempotent: Option<bool>,
408
409    /// Tool interacts with external entities beyond the local system
410    /// (network calls, third-party APIs, cloud services).
411    #[serde(default, skip_serializing_if = "Option::is_none")]
412    pub open_world: Option<bool>,
413
414    /// Tool requires API keys, credentials, or other secrets to function.
415    /// Useful for UI to show connection prompts and for LLMs to anticipate
416    /// authentication failures.
417    #[serde(default, skip_serializing_if = "Option::is_none")]
418    pub requires_secrets: Option<bool>,
419
420    /// Tool may take significant time to complete (> ~5s typical).
421    /// Useful for clients to show progress indicators and set timeouts.
422    #[serde(default, skip_serializing_if = "Option::is_none")]
423    pub long_running: Option<bool>,
424
425    /// Tool supports detached background execution via `spawn_background`.
426    /// When true, the tool may be executed asynchronously outside the current
427    /// foreground tool call and report status back later.
428    #[serde(default, skip_serializing_if = "Option::is_none")]
429    pub supports_background: Option<bool>,
430
431    /// Scheduling conflict key. Tool calls within the same act batch that share
432    /// a non-empty `concurrency_class` are executed sequentially in arrival
433    /// order; calls in different classes (or with no class) run concurrently.
434    ///
435    /// Set this on tools that mutate shared session state so that, e.g., two
436    /// file writes or two SQL mutations in one batch do not race. Read-only
437    /// tools should leave this `None` so they always parallelize. See
438    /// `crate::atoms::tool_scheduler` for how the act scheduler consumes it.
439    #[serde(default, skip_serializing_if = "Option::is_none")]
440    pub concurrency_class: Option<String>,
441
442    /// Tool performs significant CPU-bound or otherwise non-yielding work in
443    /// process (e.g. an in-process interpreter). When true, the act scheduler
444    /// runs the call on its own task (`tokio::spawn`) so a long CPU burst does
445    /// not starve the cooperative polling of I/O-bound tools in the same batch.
446    ///
447    /// Distinct from `long_running`, which describes wall-clock time for
448    /// I/O-bound work (those tools yield at await points and need no offload).
449    #[serde(default, skip_serializing_if = "Option::is_none")]
450    pub cpu_bound: Option<bool>,
451
452    /// Tool output should be persisted to session VFS before truncation.
453    /// When set, the `tool_output_persistence` capability (EVE-222, EVE-245) writes
454    /// stdout to `/outputs/{tool_call_id}.stdout` and stderr to
455    /// `/outputs/{tool_call_id}.stderr`, injecting `full_output`, `total_lines`,
456    /// and `output_files` into the result.
457    #[serde(default, skip_serializing_if = "Option::is_none")]
458    pub persist_output: Option<bool>,
459
460    /// Capability that contributed this tool definition.
461    ///
462    /// Reporting uses this attribution only as metadata. It must never contain
463    /// tool arguments, results, prompts, or any other sensitive payload.
464    #[serde(default, skip_serializing_if = "Option::is_none")]
465    pub capability_id: Option<String>,
466
467    /// Human-readable capability name snapshot for reporting.
468    #[serde(default, skip_serializing_if = "Option::is_none")]
469    pub capability_name: Option<String>,
470
471    /// Entity noun for operation-based narration (e.g. "agent", "harness").
472    /// When set, the narration system reads the `operation` argument and
473    /// produces verb-based narration like "Created agent: Neon Cartographer"
474    /// instead of the generic "Ran Manage Agents".
475    #[serde(default, skip_serializing_if = "Option::is_none")]
476    pub narration_noun: Option<String>,
477
478    /// Replay-safety class used by the durable Act activity (EVE-530).
479    ///
480    /// Controls what happens when a worker reclaims a stale `running` claim:
481    /// `Pure`/`Idempotent` tools are re-executed; `AtMostOnce` tools are
482    /// settled as `interrupted` to prevent double side-effects.
483    ///
484    /// `None` is treated conservatively as `AtMostOnce`.
485    #[serde(default, skip_serializing_if = "Option::is_none")]
486    pub side_effect_class: Option<SideEffectClass>,
487
488    /// Host-owned annotations that core does not interpret.
489    ///
490    /// The typed hints above are the vocabulary core itself reasons about. This
491    /// is the escape hatch for everything a *host* wants to carry alongside a
492    /// tool — risk tiers for an approval UI, presentation hints, an embedder's
493    /// routing keys — without adding a field to core for each one. Core reads
494    /// nothing here and no driver sends it to a provider; it travels with the
495    /// definition so a consumer sees it at the point of decision (e.g. a
496    /// `PreToolUseHook` gating on what the tool declared).
497    ///
498    /// The schema belongs to whoever writes it. Never put credentials or other
499    /// sensitive payload here: like the rest of the definition, it is persisted
500    /// and surfaced to clients.
501    #[serde(default, skip_serializing_if = "Option::is_none")]
502    pub metadata: Option<serde_json::Value>,
503}
504
505impl ToolHints {
506    /// Returns true when all fields are None (default/empty state).
507    pub fn is_empty(&self) -> bool {
508        *self == Self::default()
509    }
510
511    /// Builder: attach host-owned metadata (see [`ToolHints::metadata`]).
512    pub fn with_metadata(mut self, value: serde_json::Value) -> Self {
513        self.metadata = Some(value);
514        self
515    }
516
517    /// Builder: set readonly hint.
518    pub fn with_readonly(mut self, value: bool) -> Self {
519        self.readonly = Some(value);
520        self
521    }
522
523    /// Builder: set destructive hint.
524    pub fn with_destructive(mut self, value: bool) -> Self {
525        self.destructive = Some(value);
526        self
527    }
528
529    /// Builder: set idempotent hint.
530    pub fn with_idempotent(mut self, value: bool) -> Self {
531        self.idempotent = Some(value);
532        self
533    }
534
535    /// Builder: set open_world hint.
536    pub fn with_open_world(mut self, value: bool) -> Self {
537        self.open_world = Some(value);
538        self
539    }
540
541    /// Builder: set reporting attribution.
542    pub fn with_capability_attribution(
543        mut self,
544        capability_id: impl Into<String>,
545        capability_name: Option<impl Into<String>>,
546    ) -> Self {
547        self.capability_id = Some(capability_id.into());
548        self.capability_name = capability_name.map(Into::into);
549        self
550    }
551
552    /// Builder: set requires_secrets hint.
553    pub fn with_requires_secrets(mut self, value: bool) -> Self {
554        self.requires_secrets = Some(value);
555        self
556    }
557
558    /// Builder: set long_running hint.
559    pub fn with_long_running(mut self, value: bool) -> Self {
560        self.long_running = Some(value);
561        self
562    }
563
564    /// Builder: set supports_background hint.
565    pub fn with_supports_background(mut self, value: bool) -> Self {
566        self.supports_background = Some(value);
567        self
568    }
569
570    /// Builder: set the scheduling conflict key (see `concurrency_class`).
571    pub fn with_concurrency_class(mut self, class: impl Into<String>) -> Self {
572        self.concurrency_class = Some(class.into());
573        self
574    }
575
576    /// Builder: set the cpu_bound hint (see `cpu_bound`).
577    pub fn with_cpu_bound(mut self, value: bool) -> Self {
578        self.cpu_bound = Some(value);
579        self
580    }
581
582    /// Builder: set persist_output hint.
583    pub fn with_persist_output(mut self, value: bool) -> Self {
584        self.persist_output = Some(value);
585        self
586    }
587
588    /// Builder: set narration noun for operation-based narration.
589    pub fn with_narration_noun(mut self, noun: impl Into<String>) -> Self {
590        self.narration_noun = Some(noun.into());
591        self
592    }
593
594    /// Builder: set the replay-safety class (EVE-530).
595    pub fn with_side_effect_class(mut self, class: SideEffectClass) -> Self {
596        self.side_effect_class = Some(class);
597        self
598    }
599
600    /// Returns the effective side-effect class, defaulting to `AtMostOnce`
601    /// when unset (conservative default).
602    pub fn effective_side_effect_class(&self) -> SideEffectClass {
603        self.side_effect_class
604            .clone()
605            .unwrap_or(SideEffectClass::AtMostOnce)
606    }
607}
608
609/// Tool call from LLM response
610#[derive(Debug, Clone, Serialize, Deserialize)]
611#[cfg_attr(feature = "openapi", derive(ToSchema))]
612pub struct ToolCall {
613    /// Unique ID for this tool call
614    pub id: String,
615    /// Tool name to execute
616    pub name: String,
617    /// Arguments as JSON
618    #[cfg_attr(feature = "openapi", schema(value_type = Object))]
619    pub arguments: serde_json::Value,
620}
621
622impl ToolCall {
623    /// Arguments safe to pass to the actual tool implementation.
624    pub fn execution_arguments(&self) -> serde_json::Value {
625        strip_human_intent_argument(&self.arguments)
626    }
627
628    /// Convert tool call to OpenAI-compatible format
629    ///
630    /// Returns format: `{id, type: "function", function: {name, arguments}}`
631    /// where arguments is stringified JSON.
632    pub fn to_openai_format(&self) -> serde_json::Value {
633        serde_json::json!({
634            "id": self.id,
635            "type": "function",
636            "function": {
637                "name": self.name,
638                "arguments": serde_json::to_string(&self.arguments).unwrap_or_else(|_| "{}".to_string())
639            }
640        })
641    }
642}
643
644/// Tool execution result
645#[derive(Debug, Clone, Serialize, Deserialize)]
646pub struct ToolResult {
647    /// Tool call ID this result corresponds to
648    pub tool_call_id: String,
649    /// Result data (success)
650    pub result: Option<serde_json::Value>,
651    /// Images returned by the tool (sent as native image content to LLM)
652    #[serde(default, skip_serializing_if = "Option::is_none")]
653    pub images: Option<Vec<ToolResultImage>>,
654    /// Error message (failure)
655    pub error: Option<String>,
656    /// When set, indicates the tool requires a user connection for this provider.
657    /// The workflow should pause and prompt the user to configure the connection.
658    #[serde(default, skip_serializing_if = "Option::is_none")]
659    pub connection_required: Option<String>,
660    /// Pre-truncation cleaned output for persistence hooks.
661    /// Populated by exec tools (after ANSI strip + CR collapse, before truncation).
662    /// Consumed by PostToolExecHook (e.g. tool_output_persistence) then cleared.
663    /// Never serialized to messages or sent to LLM.
664    #[serde(skip)]
665    pub raw_output: Option<String>,
666}
667
668impl ToolResult {
669    /// Construct a minimal error-only ToolResult (used for fingerprinting error paths).
670    pub fn error(msg: &str) -> Self {
671        Self {
672            tool_call_id: String::new(),
673            result: None,
674            images: None,
675            error: Some(msg.to_string()),
676            connection_required: None,
677            raw_output: None,
678        }
679    }
680}
681
682#[cfg(test)]
683mod tests {
684    use super::*;
685
686    #[test]
687    fn test_scheduling_hints_builders_and_accessors() {
688        // A read-only tool declares no class and is not cpu-bound: the scheduler
689        // treats it as freely parallelizable.
690        let reader = ToolDefinition::Builtin(BuiltinTool {
691            name: "read_file".to_string(),
692            display_name: None,
693            description: "read".to_string(),
694            parameters: serde_json::json!({}),
695            policy: ToolPolicy::Auto,
696            category: None,
697            deferrable: DeferrablePolicy::default(),
698            hints: ToolHints::default().with_readonly(true),
699            full_parameters: None,
700        });
701        assert_eq!(reader.concurrency_class(), None);
702        assert!(!reader.is_cpu_bound());
703
704        // A mutating, CPU-bound tool surfaces both signals to the scheduler.
705        let bash = ToolDefinition::Builtin(BuiltinTool {
706            name: "bash".to_string(),
707            display_name: None,
708            description: "bash".to_string(),
709            parameters: serde_json::json!({}),
710            policy: ToolPolicy::Auto,
711            category: None,
712            deferrable: DeferrablePolicy::default(),
713            hints: ToolHints::default()
714                .with_concurrency_class("session_workspace")
715                .with_cpu_bound(true),
716            full_parameters: None,
717        });
718        assert_eq!(bash.concurrency_class(), Some("session_workspace"));
719        assert!(bash.is_cpu_bound());
720
721        // The new hint fields round-trip through serde.
722        let json = serde_json::to_string(bash.hints()).unwrap();
723        let parsed: ToolHints = serde_json::from_str(&json).unwrap();
724        assert_eq!(
725            parsed.concurrency_class.as_deref(),
726            Some("session_workspace")
727        );
728        assert_eq!(parsed.cpu_bound, Some(true));
729    }
730
731    #[test]
732    fn test_builtin_tool_serialization() {
733        let json = r#"{
734            "type": "builtin",
735            "name": "fetch_data",
736            "description": "Fetch data from URL",
737            "parameters": {"type": "object"}
738        }"#;
739
740        let tool: ToolDefinition = serde_json::from_str(json).unwrap();
741        match tool {
742            ToolDefinition::Builtin(builtin) => {
743                assert_eq!(builtin.name, "fetch_data");
744                assert_eq!(builtin.policy, ToolPolicy::Auto);
745            }
746            _ => panic!("expected Builtin variant"),
747        }
748    }
749
750    #[test]
751    fn test_builtin_tool_requires_approval() {
752        let json = r#"{
753            "type": "builtin",
754            "name": "delete_file",
755            "description": "Delete a file",
756            "parameters": {"type": "object"},
757            "policy": "requires_approval"
758        }"#;
759
760        let tool: ToolDefinition = serde_json::from_str(json).unwrap();
761        match tool {
762            ToolDefinition::Builtin(builtin) => {
763                assert_eq!(builtin.policy, ToolPolicy::RequiresApproval);
764            }
765            _ => panic!("expected Builtin variant"),
766        }
767    }
768
769    #[test]
770    fn test_tool_call_serialization() {
771        let tool_call = ToolCall {
772            id: "call_123".to_string(),
773            name: "get_weather".to_string(),
774            arguments: serde_json::json!({"city": "New York"}),
775        };
776
777        let json = serde_json::to_string(&tool_call).unwrap();
778        let parsed: ToolCall = serde_json::from_str(&json).unwrap();
779
780        assert_eq!(parsed.id, tool_call.id);
781        assert_eq!(parsed.name, tool_call.name);
782    }
783
784    #[test]
785    fn test_tool_result_serialization() {
786        let result = ToolResult {
787            tool_call_id: "call_123".to_string(),
788            result: Some(serde_json::json!({"temperature": 72})),
789            images: None,
790            error: None,
791            connection_required: None,
792            raw_output: None,
793        };
794
795        let json = serde_json::to_string(&result).unwrap();
796        let parsed: ToolResult = serde_json::from_str(&json).unwrap();
797
798        assert_eq!(parsed.tool_call_id, result.tool_call_id);
799        assert!(parsed.result.is_some());
800        assert!(parsed.error.is_none());
801    }
802
803    #[test]
804    fn test_tool_definition_accessor_methods() {
805        let tool = ToolDefinition::Builtin(BuiltinTool {
806            name: "test_tool".to_string(),
807            display_name: None,
808            description: "A test tool".to_string(),
809            parameters: serde_json::json!({"type": "object"}),
810            policy: ToolPolicy::RequiresApproval,
811            category: None,
812            deferrable: DeferrablePolicy::default(),
813            hints: ToolHints::default(),
814            full_parameters: None,
815        });
816
817        assert_eq!(tool.name(), "test_tool");
818        assert_eq!(tool.display_name(), None);
819        assert_eq!(tool.description(), "A test tool");
820        assert_eq!(tool.parameters(), &serde_json::json!({"type": "object"}));
821        assert_eq!(tool.policy(), &ToolPolicy::RequiresApproval);
822    }
823
824    #[test]
825    fn test_tool_definition_display_name_accessor() {
826        let builtin = ToolDefinition::Builtin(BuiltinTool {
827            name: "get_weather".to_string(),
828            display_name: Some("Get Weather".to_string()),
829            description: "Gets weather".to_string(),
830            parameters: serde_json::json!({}),
831            policy: ToolPolicy::Auto,
832            category: None,
833            deferrable: DeferrablePolicy::default(),
834            hints: ToolHints::default(),
835            full_parameters: None,
836        });
837        assert_eq!(builtin.display_name(), Some("Get Weather"));
838
839        let client = ToolDefinition::ClientSide(ClientSideTool {
840            name: "deploy".to_string(),
841            display_name: Some("Deploy".to_string()),
842            description: "Deploys".to_string(),
843            parameters: serde_json::json!({}),
844            category: None,
845            deferrable: DeferrablePolicy::default(),
846            hints: ToolHints::default(),
847            full_parameters: None,
848        });
849        assert_eq!(client.display_name(), Some("Deploy"));
850    }
851
852    #[test]
853    fn test_display_name_serialization_skip_none() {
854        let tool = BuiltinTool {
855            name: "test".to_string(),
856            display_name: None,
857            description: "test".to_string(),
858            parameters: serde_json::json!({}),
859            policy: ToolPolicy::Auto,
860            category: None,
861            deferrable: DeferrablePolicy::default(),
862            hints: ToolHints::default(),
863            full_parameters: None,
864        };
865        let json = serde_json::to_string(&tool).unwrap();
866        assert!(!json.contains("display_name"));
867
868        let tool_with = BuiltinTool {
869            name: "test".to_string(),
870            display_name: Some("Test".to_string()),
871            description: "test".to_string(),
872            parameters: serde_json::json!({}),
873            policy: ToolPolicy::Auto,
874            category: None,
875            deferrable: DeferrablePolicy::default(),
876            hints: ToolHints::default(),
877            full_parameters: None,
878        };
879        let json = serde_json::to_string(&tool_with).unwrap();
880        assert!(json.contains("\"display_name\":\"Test\""));
881    }
882
883    #[test]
884    fn test_tool_call_to_openai_format() {
885        let tool_call = ToolCall {
886            id: "call_123".to_string(),
887            name: "get_weather".to_string(),
888            arguments: serde_json::json!({"location": "Tokyo", "units": "celsius"}),
889        };
890
891        let converted = tool_call.to_openai_format();
892
893        assert_eq!(converted["id"], "call_123");
894        assert_eq!(converted["type"], "function");
895        assert_eq!(converted["function"]["name"], "get_weather");
896        // Arguments should be stringified JSON
897        let args: serde_json::Value =
898            serde_json::from_str(converted["function"]["arguments"].as_str().unwrap()).unwrap();
899        assert_eq!(args["location"], "Tokyo");
900        assert_eq!(args["units"], "celsius");
901    }
902
903    #[test]
904    fn test_tool_call_to_openai_format_empty_arguments() {
905        let tool_call = ToolCall {
906            id: "call_456".to_string(),
907            name: "list_files".to_string(),
908            arguments: serde_json::json!({}),
909        };
910
911        let converted = tool_call.to_openai_format();
912
913        assert_eq!(converted["id"], "call_456");
914        assert_eq!(converted["function"]["name"], "list_files");
915        assert_eq!(converted["function"]["arguments"], "{}");
916    }
917
918    #[test]
919    fn test_client_side_tool_serialization() {
920        let json = r#"{
921            "type": "client_side",
922            "name": "browser_click",
923            "description": "Click an element in the browser",
924            "parameters": {"type": "object", "properties": {"selector": {"type": "string"}}}
925        }"#;
926
927        let tool: ToolDefinition = serde_json::from_str(json).unwrap();
928        match &tool {
929            ToolDefinition::ClientSide(client) => {
930                assert_eq!(client.name, "browser_click");
931                assert_eq!(client.description, "Click an element in the browser");
932            }
933            _ => panic!("expected ClientSide variant"),
934        }
935
936        assert_eq!(tool.name(), "browser_click");
937        assert_eq!(tool.policy(), &ToolPolicy::ClientSide);
938    }
939
940    #[test]
941    fn test_client_side_tool_roundtrip() {
942        let tool = ToolDefinition::ClientSide(ClientSideTool {
943            name: "run_test".to_string(),
944            display_name: None,
945            description: "Run a test suite".to_string(),
946            parameters: serde_json::json!({"type": "object"}),
947            category: None,
948            deferrable: DeferrablePolicy::default(),
949            hints: ToolHints::default(),
950            full_parameters: None,
951        });
952
953        let json = serde_json::to_string(&tool).unwrap();
954        let parsed: ToolDefinition = serde_json::from_str(&json).unwrap();
955
956        assert_eq!(parsed.name(), "run_test");
957        assert_eq!(parsed.description(), "Run a test suite");
958        assert_eq!(parsed.policy(), &ToolPolicy::ClientSide);
959    }
960
961    #[test]
962    fn test_client_side_tool_accessor_methods() {
963        let tool = ToolDefinition::ClientSide(ClientSideTool {
964            name: "deploy_app".to_string(),
965            display_name: None,
966            description: "Deploy application to staging".to_string(),
967            parameters: serde_json::json!({
968                "type": "object",
969                "properties": {
970                    "env": {"type": "string"}
971                },
972                "required": ["env"]
973            }),
974            category: None,
975            deferrable: DeferrablePolicy::default(),
976            hints: ToolHints::default(),
977            full_parameters: None,
978        });
979
980        assert_eq!(tool.name(), "deploy_app");
981        assert_eq!(tool.description(), "Deploy application to staging");
982        assert_eq!(tool.policy(), &ToolPolicy::ClientSide);
983        assert!(tool.parameters().get("properties").is_some());
984    }
985
986    #[test]
987    fn test_client_side_tool_policy_always_client_side() {
988        // ClientSide variant always returns ClientSide policy regardless of content
989        let tool = ToolDefinition::ClientSide(ClientSideTool {
990            name: "any_tool".to_string(),
991            display_name: None,
992            description: "".to_string(),
993            parameters: serde_json::json!({}),
994            category: None,
995            deferrable: DeferrablePolicy::default(),
996            hints: ToolHints::default(),
997            full_parameters: None,
998        });
999        assert_eq!(tool.policy(), &ToolPolicy::ClientSide);
1000    }
1001
1002    #[test]
1003    fn test_tool_policy_serialization() {
1004        assert_eq!(
1005            serde_json::to_string(&ToolPolicy::ClientSide).unwrap(),
1006            r#""client_side""#
1007        );
1008        assert_eq!(
1009            serde_json::to_string(&ToolPolicy::Auto).unwrap(),
1010            r#""auto""#
1011        );
1012        assert_eq!(
1013            serde_json::to_string(&ToolPolicy::RequiresApproval).unwrap(),
1014            r#""requires_approval""#
1015        );
1016    }
1017
1018    #[test]
1019    fn test_mixed_tool_definitions_in_vec() {
1020        let tools = vec![
1021            ToolDefinition::Builtin(BuiltinTool {
1022                name: "server_tool".to_string(),
1023                display_name: None,
1024                description: "A server tool".to_string(),
1025                parameters: serde_json::json!({"type": "object"}),
1026                policy: ToolPolicy::Auto,
1027                category: None,
1028                deferrable: DeferrablePolicy::default(),
1029                hints: ToolHints::default(),
1030                full_parameters: None,
1031            }),
1032            ToolDefinition::ClientSide(ClientSideTool {
1033                name: "client_tool".to_string(),
1034                display_name: None,
1035                description: "A client tool".to_string(),
1036                parameters: serde_json::json!({"type": "object"}),
1037                category: None,
1038                deferrable: DeferrablePolicy::default(),
1039                hints: ToolHints::default(),
1040                full_parameters: None,
1041            }),
1042        ];
1043
1044        let json = serde_json::to_string(&tools).unwrap();
1045        let parsed: Vec<ToolDefinition> = serde_json::from_str(&json).unwrap();
1046
1047        assert_eq!(parsed.len(), 2);
1048        assert!(matches!(&parsed[0], ToolDefinition::Builtin(_)));
1049        assert!(matches!(&parsed[1], ToolDefinition::ClientSide(_)));
1050        assert_eq!(parsed[0].policy(), &ToolPolicy::Auto);
1051        assert_eq!(parsed[1].policy(), &ToolPolicy::ClientSide);
1052    }
1053
1054    #[test]
1055    fn test_tool_hints_default_is_empty() {
1056        let hints = ToolHints::default();
1057        assert!(hints.is_empty());
1058        assert_eq!(hints.readonly, None);
1059        assert_eq!(hints.destructive, None);
1060        assert_eq!(hints.idempotent, None);
1061        assert_eq!(hints.open_world, None);
1062        assert_eq!(hints.requires_secrets, None);
1063        assert_eq!(hints.long_running, None);
1064    }
1065
1066    #[test]
1067    fn test_tool_hints_builder() {
1068        let hints = ToolHints::default()
1069            .with_readonly(true)
1070            .with_destructive(false)
1071            .with_idempotent(true)
1072            .with_open_world(true)
1073            .with_requires_secrets(true)
1074            .with_long_running(false);
1075
1076        assert!(!hints.is_empty());
1077        assert_eq!(hints.readonly, Some(true));
1078        assert_eq!(hints.destructive, Some(false));
1079        assert_eq!(hints.idempotent, Some(true));
1080        assert_eq!(hints.open_world, Some(true));
1081        assert_eq!(hints.requires_secrets, Some(true));
1082        assert_eq!(hints.long_running, Some(false));
1083    }
1084
1085    #[test]
1086    fn test_tool_hints_serialization_skip_empty() {
1087        let tool = BuiltinTool {
1088            name: "test".to_string(),
1089            display_name: None,
1090            description: "test".to_string(),
1091            parameters: serde_json::json!({}),
1092            policy: ToolPolicy::Auto,
1093            category: None,
1094            deferrable: DeferrablePolicy::default(),
1095            hints: ToolHints::default(),
1096            full_parameters: None,
1097        };
1098        let json = serde_json::to_string(&tool).unwrap();
1099        assert!(!json.contains("hints"), "empty hints should be skipped");
1100    }
1101
1102    #[test]
1103    fn test_tool_hints_serialization_present() {
1104        let tool = BuiltinTool {
1105            name: "test".to_string(),
1106            display_name: None,
1107            description: "test".to_string(),
1108            parameters: serde_json::json!({}),
1109            policy: ToolPolicy::Auto,
1110            category: None,
1111            deferrable: DeferrablePolicy::default(),
1112            hints: ToolHints::default()
1113                .with_readonly(true)
1114                .with_idempotent(true),
1115            full_parameters: None,
1116        };
1117        let json = serde_json::to_string(&tool).unwrap();
1118        assert!(json.contains("\"hints\""));
1119        assert!(json.contains("\"readonly\":true"));
1120        assert!(json.contains("\"idempotent\":true"));
1121        // Unset hints should not appear
1122        assert!(!json.contains("destructive"));
1123        assert!(!json.contains("open_world"));
1124    }
1125
1126    #[test]
1127    fn test_tool_hints_deserialization_missing() {
1128        let json = r#"{
1129            "type": "builtin",
1130            "name": "test",
1131            "description": "test",
1132            "parameters": {}
1133        }"#;
1134        let tool: ToolDefinition = serde_json::from_str(json).unwrap();
1135        assert!(tool.hints().is_empty());
1136    }
1137
1138    #[test]
1139    fn test_tool_hints_deserialization_present() {
1140        let json = r#"{
1141            "type": "builtin",
1142            "name": "test",
1143            "description": "test",
1144            "parameters": {},
1145            "hints": {"readonly": true, "open_world": true, "requires_secrets": true}
1146        }"#;
1147        let tool: ToolDefinition = serde_json::from_str(json).unwrap();
1148        let hints = tool.hints();
1149        assert_eq!(hints.readonly, Some(true));
1150        assert_eq!(hints.open_world, Some(true));
1151        assert_eq!(hints.requires_secrets, Some(true));
1152        assert_eq!(hints.destructive, None);
1153        assert_eq!(hints.idempotent, None);
1154        assert_eq!(hints.long_running, None);
1155    }
1156
1157    #[test]
1158    fn test_tool_definition_with_hints_builder() {
1159        let tool = ToolDefinition::Builtin(BuiltinTool {
1160            name: "test".to_string(),
1161            display_name: None,
1162            description: "test".to_string(),
1163            parameters: serde_json::json!({}),
1164            policy: ToolPolicy::Auto,
1165            category: None,
1166            deferrable: DeferrablePolicy::default(),
1167            hints: ToolHints::default(),
1168            full_parameters: None,
1169        })
1170        .with_hints(ToolHints::default().with_readonly(true));
1171
1172        assert_eq!(tool.hints().readonly, Some(true));
1173    }
1174
1175    #[test]
1176    fn test_with_human_intent_argument_adds_optional_schema_property() {
1177        let tool = ToolDefinition::Builtin(BuiltinTool {
1178            name: "manage_harnesses".to_string(),
1179            display_name: Some("Manage Harnesses".to_string()),
1180            description: "Manage harnesses".to_string(),
1181            parameters: serde_json::json!({
1182                "type": "object",
1183                "properties": {
1184                    "operation": { "type": "string", "enum": ["list"] }
1185                },
1186                "required": ["operation"],
1187                "additionalProperties": false
1188            }),
1189            policy: ToolPolicy::Auto,
1190            category: None,
1191            deferrable: DeferrablePolicy::default(),
1192            hints: ToolHints::default(),
1193            full_parameters: None,
1194        })
1195        .with_human_intent_argument();
1196
1197        let params = tool.parameters();
1198        assert_eq!(
1199            params["properties"][HUMAN_INTENT_ARGUMENT]["type"],
1200            "string"
1201        );
1202        assert!(
1203            params["properties"][HUMAN_INTENT_ARGUMENT]["description"]
1204                .as_str()
1205                .unwrap()
1206                .contains("Listing all harnesses")
1207        );
1208        assert!(
1209            !params["required"]
1210                .as_array()
1211                .unwrap()
1212                .iter()
1213                .any(|item| item.as_str() == Some(HUMAN_INTENT_ARGUMENT))
1214        );
1215        assert_eq!(params["additionalProperties"], false);
1216    }
1217
1218    #[test]
1219    fn test_tool_call_execution_arguments_strip_human_intent() {
1220        let tool_call = ToolCall {
1221            id: "call_1".to_string(),
1222            name: "manage_harnesses".to_string(),
1223            arguments: serde_json::json!({
1224                "operation": "list",
1225                "human_intent": "Listing all harnesses"
1226            }),
1227        };
1228
1229        assert_eq!(
1230            tool_call.execution_arguments(),
1231            serde_json::json!({ "operation": "list" })
1232        );
1233        assert_eq!(
1234            human_intent(&tool_call.arguments),
1235            Some("Listing all harnesses")
1236        );
1237    }
1238
1239    #[test]
1240    fn tool_hints_metadata_is_an_opaque_host_owned_hatch() {
1241        let hints = ToolHints::default()
1242            .with_readonly(true)
1243            .with_metadata(serde_json::json!({"risk_tier": "high"}));
1244
1245        // Core does not interpret it, but it survives the definition's
1246        // serialization so a consumer sees it at the point of decision.
1247        let json = serde_json::to_value(&hints).unwrap();
1248        assert_eq!(json["metadata"]["risk_tier"], "high");
1249        let restored: ToolHints = serde_json::from_value(json).unwrap();
1250        assert_eq!(restored, hints);
1251
1252        // Absent metadata stays off the wire, so existing payloads are byte-identical.
1253        let bare = serde_json::to_value(ToolHints::default().with_readonly(true)).unwrap();
1254        assert!(bare.get("metadata").is_none());
1255    }
1256
1257    #[test]
1258    fn tool_hints_with_only_metadata_are_not_empty() {
1259        assert!(ToolHints::default().is_empty());
1260        assert!(
1261            !ToolHints::default()
1262                .with_metadata(serde_json::json!({"any": "thing"}))
1263                .is_empty(),
1264            "metadata alone must keep the hints serialized"
1265        );
1266    }
1267}