agent_types/tool.rs
1//! Tool-related pure types: Content, ToolMetadata, ToolExposure, ActivationContext.
2
3use serde::{Deserialize, Serialize};
4use std::path::PathBuf;
5
6use crate::session::SessionId;
7
8/// Structured content returned by a tool, aligned with the MCP `content`
9/// array shape (no envelope, no orchestration/failure/truncation semantics).
10///
11/// Only `Text` is consumed by the first LLM adapter; `Image` is shape-reserved
12/// and the adapter reports "not supported" rather than silently dropping it.
13/// `Detail` carries machine-readable metadata (e.g. edit line numbers) that
14/// the UI can consume but the LLM never sees.
15#[derive(Clone, Debug, Serialize, Deserialize)]
16#[serde(tag = "type", rename_all = "snake_case")]
17pub enum Content {
18 Text {
19 text: String,
20 },
21 /// Base64-encoded image payload.
22 Image {
23 data: String,
24 mime_type: String,
25 },
26 /// Machine-readable metadata for UI consumption (not sent to the LLM).
27 Detail {
28 data: serde_json::Value,
29 },
30}
31
32impl Content {
33 pub fn text(s: impl Into<String>) -> Self {
34 Content::Text { text: s.into() }
35 }
36
37 pub fn image(data: impl Into<String>, mime_type: impl Into<String>) -> Self {
38 Content::Image {
39 data: data.into(),
40 mime_type: mime_type.into(),
41 }
42 }
43
44 pub fn detail(data: serde_json::Value) -> Self {
45 Content::Detail { data }
46 }
47}
48
49impl From<Content> for Vec<Content> {
50 fn from(c: Content) -> Self {
51 vec![c]
52 }
53}
54
55/// Join the textual portion of tool output into a single string for display
56/// and session history. Non-text variants (e.g. `Image`, `Detail`) are skipped.
57pub fn content_text(contents: &[Content]) -> String {
58 contents
59 .iter()
60 .filter_map(|c| match c {
61 Content::Text { text } => Some(text.as_str()),
62 Content::Image { .. } | Content::Detail { .. } => None,
63 })
64 .collect::<Vec<_>>()
65 .join("\n")
66}
67
68/// Extract structured detail metadata from tool output. When multiple `Detail`
69/// entries exist, they are merged into a single object. Returns `None` if no
70/// `Detail` entries are present.
71pub fn content_details(contents: &[Content]) -> Option<serde_json::Value> {
72 let details: Vec<&serde_json::Value> = contents
73 .iter()
74 .filter_map(|c| match c {
75 Content::Detail { data } => Some(data),
76 _ => None,
77 })
78 .collect();
79 match details.len() {
80 0 => None,
81 1 => Some(details[0].clone()),
82 _ => {
83 // Merge multiple detail objects into one
84 let mut merged = serde_json::Map::new();
85 for d in details {
86 if let Some(obj) = d.as_object() {
87 for (k, v) in obj {
88 merged.insert(k.clone(), v.clone());
89 }
90 }
91 }
92 Some(serde_json::Value::Object(merged))
93 }
94 }
95}
96
97/// Machine-readable metadata for a registered tool — origin, version, and
98/// runtime requirements in a stable shape consumers can inspect without
99/// parsing the LLM-facing definition JSON.
100#[derive(Clone, Debug, Serialize, Deserialize)]
101pub struct ToolMetadata {
102 /// Tool name (matches `Tool::name`).
103 pub name: String,
104 /// Human-readable description.
105 pub description: String,
106 /// Where this tool comes from: a crate name (e.g. `"phi-tools"`), a
107 /// framework identifier (`"agent-base"`, `"agent-works"`), or
108 /// `"custom"` for user-defined tools.
109 pub origin: String,
110 /// Crate / package version, or `"unknown"` when built outside a crate.
111 pub version: String,
112 /// Optional runtime requirements or capabilities this tool depends on.
113 pub requirements: Vec<String>,
114}
115
116/// Visibility level of a tool to the LLM model.
117///
118/// Controls whether a tool appears in the tool definitions sent to the model
119/// each turn. Defaults to `Direct` for backward compatibility.
120#[derive(Clone, Debug, PartialEq, Eq)]
121pub enum ToolExposure {
122 /// Always visible to the model. This is the default.
123 Direct,
124 /// Conditionally visible — the tool decides via `Tool::should_activate`.
125 Deferred,
126 /// Never visible to the model (internal/framework tools).
127 Hidden,
128}
129
130/// Context passed to `Tool::should_activate` for Deferred tools.
131///
132/// Built once per react-loop iteration; tools inspect it to decide
133/// whether they should be exposed to the model this turn.
134#[derive(Clone, Debug)]
135pub struct ActivationContext {
136 /// Current session ID.
137 pub session_id: SessionId,
138 /// Names of tools already activated (Direct + activated Deferred) this turn.
139 pub current_tools: Vec<String>,
140 /// Workspace / working directory path.
141 pub workspace: PathBuf,
142}