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}