Skip to main content

talos_core/tool/
result_presentation.rs

1use std::collections::HashSet;
2
3use schemars::JsonSchema;
4use serde::{Deserialize, Serialize};
5
6use super::AgentTool;
7
8/// Provenance of a registered tool.
9#[derive(Debug, Clone, PartialEq, Eq, Default, Serialize, Deserialize)]
10#[serde(tag = "type", rename_all = "snake_case")]
11pub enum ToolProvenance {
12    /// A native tool registered within the main process.
13    #[default]
14    Native,
15    /// A tool provided by a remote MCP server.
16    McpRemote { server: String },
17    /// A tool supplied by a plugin package (ADR-028).
18    ///
19    /// `carrier` is a free-form string (e.g. `"wasm"`) governed by ADR-027 so
20    /// future carriers can be introduced without forcing downstream exhaustive
21    /// updates. Plugin provenance is descriptive only and does not grant
22    /// permissions.
23    Plugin {
24        name: String,
25        version: String,
26        carrier: String,
27    },
28}
29
30/// A structured request for the runtime to disclose a narrower tool backend or
31/// a specific tool on a later turn.
32///
33/// Continuations are advisory presentation updates. They are not permission
34/// grants and must not cause a higher-risk backend to execute implicitly.
35#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
36pub struct ToolContinuation {
37    /// Tool that should be disclosed or whose backend should be disclosed on a later provider turn.
38    pub tool: String,
39    /// Backend id to disclose.
40    ///
41    /// Empty means disclose the tool itself, not a conditional backend. This
42    /// preserves the pre-existing field type while supporting tool-level
43    /// progressive disclosure.
44    pub backend: String,
45    /// Machine-readable reason, such as `login_redirect` or `js_rendered_empty`.
46    pub reason: String,
47    /// Optional human-readable permission preview.
48    #[serde(default)]
49    pub permission_preview: Option<String>,
50}
51
52impl ToolContinuation {
53    /// Creates a backend-disclosure continuation.
54    #[must_use]
55    pub fn disclose_backend(
56        tool: impl Into<String>,
57        backend: impl Into<String>,
58        reason: impl Into<String>,
59    ) -> Self {
60        Self {
61            tool: tool.into(),
62            backend: backend.into(),
63            reason: reason.into(),
64            permission_preview: None,
65        }
66    }
67
68    /// Creates a tool-disclosure continuation.
69    #[must_use]
70    pub fn disclose_tool(tool: impl Into<String>, reason: impl Into<String>) -> Self {
71        Self {
72            tool: tool.into(),
73            backend: String::new(),
74            reason: reason.into(),
75            permission_preview: None,
76        }
77    }
78
79    /// Returns true when this continuation discloses a whole tool instead of a backend.
80    #[must_use]
81    pub fn is_tool_disclosure(&self) -> bool {
82        self.backend.is_empty()
83    }
84
85    /// Adds display-oriented permission preview text.
86    #[must_use]
87    pub fn with_permission_preview(mut self, preview: impl Into<String>) -> Self {
88        self.permission_preview = Some(preview.into());
89        self
90    }
91}
92
93/// The result of executing a tool.
94#[derive(Debug, Clone)]
95pub struct ToolResult {
96    /// The output content produced by the tool.
97    pub content: String,
98    /// Whether the execution resulted in an error.
99    pub is_error: bool,
100    /// Runtime-only continuation hints for later tool presentation.
101    pub continuations: Vec<ToolContinuation>,
102}
103
104/// Output of an authorized tool execution that may carry a
105/// provider-neutral continuation artifact (ADR-051).
106///
107/// Most tools produce only the normal [`ToolResult`]. A tool like
108/// `read_image` additionally returns `next_provider_parts` — a
109/// `Vec<ContentPart>` that the agent delivers to the immediately
110/// following provider request exactly once and then discards.
111///
112/// The continuation artifact is **never** persisted in the session
113/// transcript, TLOG, UI, hooks, exports, or compaction. It exists
114/// solely for the next `stream_with_tools` call.
115#[derive(Debug, Clone)]
116pub struct ToolExecutionOutput {
117    /// The normal textual tool result (same as `ToolResult`).
118    pub result: ToolResult,
119    /// Provider-neutral content parts to carry to the next provider
120    /// request. Empty for all existing tools.
121    pub next_provider_parts: Vec<crate::message::ContentPart>,
122}
123
124impl ToolExecutionOutput {
125    /// Creates an output with a successful text result and no
126    /// continuation parts.
127    pub fn success(content: impl Into<String>) -> Self {
128        Self {
129            result: ToolResult::success(content),
130            next_provider_parts: Vec::new(),
131        }
132    }
133
134    /// Creates an output with an error text result and no continuation
135    /// parts.
136    pub fn error(content: impl Into<String>) -> Self {
137        Self {
138            result: ToolResult::error(content),
139            next_provider_parts: Vec::new(),
140        }
141    }
142
143    /// Wraps an existing [`ToolResult`] with no continuation parts.
144    pub fn from_result(result: ToolResult) -> Self {
145        Self {
146            result,
147            next_provider_parts: Vec::new(),
148        }
149    }
150}
151
152/// Model, display, and persistence views of one tool result.
153///
154/// Most tools use the same content for all three views. Tools that return
155/// transient model-only coordination data can override
156/// [`AgentTool::project_result`] so that UI and durable history receive a
157/// sanitized representation without changing the provider-facing result.
158#[derive(Debug, Clone, PartialEq, Eq)]
159pub struct ToolResultProjection {
160    /// Content supplied to the model during the active turn.
161    pub model_content: String,
162    /// Content emitted to user-facing runtime event projections.
163    pub display_content: String,
164    /// Content eligible for session persistence and replay.
165    pub persistence_content: String,
166}
167
168impl ToolResultProjection {
169    /// Creates a projection whose three views are identical.
170    #[must_use]
171    pub fn shared(content: impl Into<String>) -> Self {
172        let content = content.into();
173        Self {
174            model_content: content.clone(),
175            display_content: content.clone(),
176            persistence_content: content,
177        }
178    }
179}
180
181impl ToolResult {
182    /// Creates a successful tool result with the given content.
183    pub fn success(content: impl Into<String>) -> Self {
184        Self {
185            content: content.into(),
186            is_error: false,
187            continuations: Vec::new(),
188        }
189    }
190
191    /// Creates an error tool result with the given error message.
192    pub fn error(content: impl Into<String>) -> Self {
193        Self {
194            content: content.into(),
195            is_error: true,
196            continuations: Vec::new(),
197        }
198    }
199
200    /// Adds one runtime continuation hint to this tool result.
201    #[must_use]
202    pub fn with_continuation(mut self, continuation: ToolContinuation) -> Self {
203        self.continuations.push(continuation);
204        self
205    }
206}
207
208/// Categorizes a tool by its operational nature for permission decisions.
209#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
210pub enum ToolNature {
211    /// Read-only: inspects files/code without side effects.
212    #[default]
213    Read,
214    /// Writes or modifies files.
215    Write,
216    /// Executes external processes or commands.
217    Execute,
218    /// Makes network requests (HTTP, API calls).
219    Network,
220    /// Session-internal plumbing (todo list, scratch state). Always allowed.
221    Internal,
222}
223
224/// Stable presentation family for a tool.
225///
226/// Families are model-presentation metadata, not execution registration. The
227/// registry remains the source of executable tools; presentation policy decides
228/// which registered tools are shown to the provider for a turn/session.
229#[derive(
230    Debug,
231    Clone,
232    Copy,
233    Default,
234    PartialEq,
235    Eq,
236    Hash,
237    PartialOrd,
238    Ord,
239    Serialize,
240    Deserialize,
241    JsonSchema,
242)]
243#[serde(rename_all = "snake_case")]
244pub enum ToolFamily {
245    /// File and directory operations.
246    #[default]
247    File,
248    /// Text search and file inspection operations.
249    Search,
250    /// AST/code-structure tools.
251    CodeIntelligence,
252    /// Git repository tools.
253    Git,
254    /// Network, web, and URL tools.
255    Network,
256    /// Advanced network/API debugging tools that should be disclosed only when needed.
257    AdvancedNetwork,
258    /// Shell or command execution tools.
259    Shell,
260    /// Tools supplied by extensions, MCP, or unknown sources.
261    Extension,
262    /// Plugin tools that must be explicitly disclosed before model presentation.
263    Plugin,
264}
265
266/// A named conditional backend behind a model-visible tool.
267///
268/// Backends let one tool expose narrow capabilities only when a presentation
269/// policy discloses them. For example, a unified web-reading tool can keep its
270/// ordinary HTTP path visible while disclosing an authenticated browser-page
271/// backend only after a continuation or strong user intent.
272#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
273pub struct ToolBackend {
274    /// Stable backend id within the owning tool.
275    pub id: String,
276    /// Short model-facing description of when this backend is available.
277    pub description: String,
278}
279
280impl ToolBackend {
281    /// Creates a backend descriptor.
282    #[must_use]
283    pub fn new(id: impl Into<String>, description: impl Into<String>) -> Self {
284        Self {
285            id: id.into(),
286            description: description.into(),
287        }
288    }
289}
290
291/// A policy entry that discloses one backend for one tool.
292#[derive(
293    Debug, Clone, PartialEq, Eq, Hash, PartialOrd, Ord, Serialize, Deserialize, JsonSchema,
294)]
295pub struct ToolBackendDisclosure {
296    /// Tool name that owns the backend.
297    pub tool: String,
298    /// Backend id disclosed for the tool.
299    pub backend: String,
300}
301
302impl ToolBackendDisclosure {
303    /// Creates a backend disclosure entry.
304    #[must_use]
305    pub fn new(tool: impl Into<String>, backend: impl Into<String>) -> Self {
306        Self {
307            tool: tool.into(),
308            backend: backend.into(),
309        }
310    }
311}
312
313/// Policy for selecting model-visible tool families.
314#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
315pub struct ToolPresentationPolicy {
316    /// If true, every registered tool is presented.
317    pub include_all: bool,
318    /// If true, the always-on baseline is presented even when not in `families`.
319    pub include_always_on: bool,
320    /// Additional families to present.
321    #[serde(default)]
322    pub families: Vec<ToolFamily>,
323    /// Additional individual tools to present.
324    #[serde(default)]
325    pub tools: Vec<String>,
326    /// Conditional backends to present for specific tools.
327    #[serde(default)]
328    pub backends: Vec<ToolBackendDisclosure>,
329}
330
331impl ToolPresentationPolicy {
332    /// Presents every registered tool. This preserves pre-TOOL-012 behavior.
333    #[must_use]
334    pub fn full() -> Self {
335        Self {
336            include_all: true,
337            include_always_on: true,
338            families: Vec::new(),
339            tools: Vec::new(),
340            backends: Vec::new(),
341        }
342    }
343
344    /// Presents the always-on baseline only.
345    #[must_use]
346    pub fn always_on() -> Self {
347        Self {
348            include_all: false,
349            include_always_on: true,
350            families: Vec::new(),
351            tools: Vec::new(),
352            backends: Vec::new(),
353        }
354    }
355
356    /// Presents the default runtime surface while keeping advanced tools hidden
357    /// unless explicitly disclosed.
358    #[must_use]
359    pub fn runtime_default() -> Self {
360        Self {
361            include_all: false,
362            include_always_on: true,
363            families: vec![
364                ToolFamily::File,
365                ToolFamily::Search,
366                ToolFamily::CodeIntelligence,
367                ToolFamily::Git,
368                ToolFamily::Network,
369                ToolFamily::Shell,
370                ToolFamily::Extension,
371            ],
372            tools: Vec::new(),
373            backends: Vec::new(),
374        }
375    }
376
377    /// Presents the always-on baseline plus specific families.
378    #[must_use]
379    pub fn with_families(families: impl IntoIterator<Item = ToolFamily>) -> Self {
380        Self {
381            include_all: false,
382            include_always_on: true,
383            families: families.into_iter().collect(),
384            tools: Vec::new(),
385            backends: Vec::new(),
386        }
387    }
388
389    /// Presents the always-on baseline plus a specific conditional backend.
390    #[must_use]
391    pub fn with_backend(tool: impl Into<String>, backend: impl Into<String>) -> Self {
392        Self {
393            include_all: false,
394            include_always_on: true,
395            families: Vec::new(),
396            tools: Vec::new(),
397            backends: vec![ToolBackendDisclosure::new(tool, backend)],
398        }
399    }
400
401    /// Presents the always-on baseline plus one specific tool.
402    #[must_use]
403    pub fn with_tool(tool: impl Into<String>) -> Self {
404        Self {
405            include_all: false,
406            include_always_on: true,
407            families: Vec::new(),
408            tools: vec![tool.into()],
409            backends: Vec::new(),
410        }
411    }
412
413    /// Adds a tool disclosure entry to this policy.
414    #[must_use]
415    pub fn disclose_tool(mut self, tool: impl Into<String>) -> Self {
416        self.tools.push(tool.into());
417        self
418    }
419
420    /// Adds a backend disclosure entry to this policy.
421    #[must_use]
422    pub fn disclose_backend(mut self, tool: impl Into<String>, backend: impl Into<String>) -> Self {
423        self.backends
424            .push(ToolBackendDisclosure::new(tool, backend));
425        self
426    }
427
428    /// Returns true when this policy presents the given tool.
429    #[must_use]
430    pub fn allows_tool(&self, tool: &dyn AgentTool) -> bool {
431        self.include_all
432            || (self.include_always_on && tool.is_always_on())
433            || self.families.contains(&tool.family())
434            || self.tools.iter().any(|name| name == tool.name())
435            || self.backends.iter().any(|entry| entry.tool == tool.name())
436    }
437
438    /// Returns true when a backend is disclosed for execution.
439    #[must_use]
440    pub fn allows_backend(&self, tool: &str, backend: &str) -> bool {
441        self.include_all
442            || self
443                .backends
444                .iter()
445                .any(|entry| entry.tool == tool && entry.backend == backend)
446    }
447
448    /// Returns the family set explicitly enabled by this policy.
449    #[must_use]
450    pub fn family_set(&self) -> HashSet<ToolFamily> {
451        self.families.iter().copied().collect()
452    }
453
454    /// Returns the disclosed backend ids for one tool.
455    #[must_use]
456    pub fn backend_set_for(&self, tool: &str) -> HashSet<String> {
457        self.backends
458            .iter()
459            .filter(|entry| entry.tool == tool)
460            .map(|entry| entry.backend.clone())
461            .collect()
462    }
463}
464
465impl Default for ToolPresentationPolicy {
466    fn default() -> Self {
467        Self::full()
468    }
469}