Skip to main content

agent_sdk_toolkit/packs/
ergonomic.rs

1//! Ergonomic tool declaration wrappers for toolkit-owned tool packs.
2//! These helpers are data-only: they assemble core tool-pack snapshots,
3//! capabilities, and routes, but never execute a tool or bypass core policy,
4//! journal, event, or effect contracts.
5//!
6use agent_sdk_core::{
7    AgentError, CapabilityId, CapabilityNamespace, CapabilityPermission, ExecutorRef,
8    PackageSidecarRef, PolicyKind, PolicyRef, PrivacyClass, SourceRef, ToolPackId, ToolPackKind,
9    ToolPackSnapshot, ToolPackToolSnapshot, TrustClass,
10    policy::{EffectClass, RiskClass},
11    tool_records::CanonicalToolName,
12};
13
14use crate::packs::ToolkitPackBundle;
15
16#[derive(Clone, Debug, Eq, PartialEq)]
17/// Selects how a declared toolkit tool is expected to complete.
18/// This is package metadata only; execution still goes through the core
19/// `ToolExecutor` and `ToolExecutionCoordinator`.
20pub enum ToolkitToolExecutionMode {
21    /// Synchronous or short-lived tool execution.
22    Sync,
23    /// Longer-running or externally continued execution.
24    Async,
25}
26
27#[derive(Clone, Debug, Eq, PartialEq)]
28/// Data-only declaration for a synchronous toolkit tool.
29/// Lower it into a `ToolkitPackBundle`; do not execute it directly.
30pub struct Tool {
31    snapshot: ToolPackToolSnapshot,
32    mode: ToolkitToolExecutionMode,
33}
34
35impl Tool {
36    /// Starts a data-only builder for a synchronous toolkit tool.
37    pub fn builder(
38        tool_name: impl Into<String>,
39        executor_ref: impl Into<String>,
40        schema_id: impl Into<String>,
41        policy_ref: PolicyRef,
42    ) -> ToolBuilder {
43        ToolBuilder::new(tool_name, executor_ref, schema_id, policy_ref)
44    }
45
46    /// Returns the canonical tool name exposed to providers.
47    pub fn canonical_tool_name(&self) -> &CanonicalToolName {
48        &self.snapshot.canonical_tool_name
49    }
50
51    /// Returns the declared executor ref without resolving or executing it.
52    pub fn executor_ref(&self) -> &ExecutorRef {
53        &self.snapshot.executor_ref
54    }
55
56    /// Returns the declaration mode.
57    pub fn mode(&self) -> &ToolkitToolExecutionMode {
58        &self.mode
59    }
60
61    /// Returns the core tool-pack snapshot this wrapper lowers to.
62    pub fn snapshot(&self) -> &ToolPackToolSnapshot {
63        &self.snapshot
64    }
65
66    /// Consumes this wrapper into the core tool-pack snapshot.
67    pub fn into_snapshot(self) -> ToolPackToolSnapshot {
68        self.snapshot
69    }
70}
71
72#[derive(Clone, Debug, Eq, PartialEq)]
73/// Data-only declaration for an async or externally continued toolkit tool.
74/// The SDK still requires a core `ToolExecutor` and journaled terminal result
75/// before provider continuation.
76pub struct AsyncTool {
77    snapshot: ToolPackToolSnapshot,
78    mode: ToolkitToolExecutionMode,
79}
80
81impl AsyncTool {
82    /// Starts a data-only builder for an async toolkit tool.
83    pub fn builder(
84        tool_name: impl Into<String>,
85        executor_ref: impl Into<String>,
86        schema_id: impl Into<String>,
87        policy_ref: PolicyRef,
88    ) -> ToolBuilder {
89        ToolBuilder::new(tool_name, executor_ref, schema_id, policy_ref).async_mode()
90    }
91
92    /// Returns the canonical tool name exposed to providers.
93    pub fn canonical_tool_name(&self) -> &CanonicalToolName {
94        &self.snapshot.canonical_tool_name
95    }
96
97    /// Returns the declared executor ref without resolving or executing it.
98    pub fn executor_ref(&self) -> &ExecutorRef {
99        &self.snapshot.executor_ref
100    }
101
102    /// Returns the declaration mode.
103    pub fn mode(&self) -> &ToolkitToolExecutionMode {
104        &self.mode
105    }
106
107    /// Returns the core tool-pack snapshot this wrapper lowers to.
108    pub fn snapshot(&self) -> &ToolPackToolSnapshot {
109        &self.snapshot
110    }
111
112    /// Consumes this wrapper into the core tool-pack snapshot.
113    pub fn into_snapshot(self) -> ToolPackToolSnapshot {
114        self.snapshot
115    }
116}
117
118#[derive(Clone, Debug)]
119/// Builder for ergonomic toolkit tool declarations.
120/// The builder requires an explicit effect and risk class before it can
121/// produce a `Tool` or `AsyncTool`.
122pub struct ToolBuilder {
123    tool_name: String,
124    executor_ref: String,
125    schema_id: String,
126    policy_refs: Vec<PolicyRef>,
127    capability_id: Option<CapabilityId>,
128    namespace: Option<CapabilityNamespace>,
129    required_permissions: Vec<CapabilityPermission>,
130    effect_class: Option<EffectClass>,
131    risk_class: Option<RiskClass>,
132    redaction_policy_ref: PolicyRef,
133    timeout_ms: u64,
134    cancellation: String,
135    reconciliation: String,
136    privacy: PrivacyClass,
137    mode: ToolkitToolExecutionMode,
138}
139
140impl ToolBuilder {
141    /// Creates a new data-only tool declaration builder.
142    pub fn new(
143        tool_name: impl Into<String>,
144        executor_ref: impl Into<String>,
145        schema_id: impl Into<String>,
146        policy_ref: PolicyRef,
147    ) -> Self {
148        Self {
149            tool_name: tool_name.into(),
150            executor_ref: executor_ref.into(),
151            schema_id: schema_id.into(),
152            policy_refs: vec![policy_ref],
153            capability_id: None,
154            namespace: None,
155            required_permissions: Vec::new(),
156            effect_class: None,
157            risk_class: None,
158            redaction_policy_ref: PolicyRef::with_kind(
159                PolicyKind::Redaction,
160                "policy.redaction.tool_result.refs_only",
161            ),
162            timeout_ms: 10_000,
163            cancellation: "best_effort".to_string(),
164            reconciliation: "effect_lineage_required".to_string(),
165            privacy: PrivacyClass::ContentRefsOnly,
166            mode: ToolkitToolExecutionMode::Sync,
167        }
168    }
169
170    /// Marks this declaration as a read-like tool.
171    pub fn read_only(mut self) -> Self {
172        self.effect_class = Some(EffectClass::Read);
173        self.risk_class = Some(RiskClass::Low);
174        self
175    }
176
177    /// Marks this declaration as a write-like tool.
178    pub fn write_effect(mut self) -> Self {
179        self.effect_class = Some(EffectClass::Write);
180        self.risk_class = Some(RiskClass::High);
181        self
182    }
183
184    /// Sets an explicit effect and risk class.
185    pub fn effect(mut self, effect_class: EffectClass, risk_class: RiskClass) -> Self {
186        self.effect_class = Some(effect_class);
187        self.risk_class = Some(risk_class);
188        self
189    }
190
191    /// Sets the stable capability id used by the runtime package.
192    pub fn capability_id(mut self, capability_id: CapabilityId) -> Self {
193        self.capability_id = Some(capability_id);
194        self
195    }
196
197    /// Sets the capability namespace.
198    pub fn namespace(mut self, namespace: CapabilityNamespace) -> Self {
199        self.namespace = Some(namespace);
200        self
201    }
202
203    /// Adds a required permission to the provider-visible tool snapshot.
204    pub fn required_permission(mut self, permission: CapabilityPermission) -> Self {
205        self.required_permissions.push(permission);
206        self
207    }
208
209    /// Adds another policy ref that must travel with the tool declaration.
210    pub fn policy_ref(mut self, policy_ref: PolicyRef) -> Self {
211        self.policy_refs.push(policy_ref);
212        self
213    }
214
215    /// Sets the redaction policy ref for tool results.
216    pub fn redaction_policy(mut self, policy_ref: PolicyRef) -> Self {
217        self.redaction_policy_ref = policy_ref;
218        self
219    }
220
221    /// Sets the execution timeout metadata.
222    pub fn timeout_ms(mut self, timeout_ms: u64) -> Self {
223        self.timeout_ms = timeout_ms;
224        self
225    }
226
227    /// Sets the cancellation metadata.
228    pub fn cancellation(mut self, cancellation: impl Into<String>) -> Self {
229        self.cancellation = cancellation.into();
230        self
231    }
232
233    /// Sets the reconciliation metadata.
234    pub fn reconciliation(mut self, reconciliation: impl Into<String>) -> Self {
235        self.reconciliation = reconciliation.into();
236        self
237    }
238
239    /// Sets the privacy class used for the tool snapshot and route.
240    pub fn privacy(mut self, privacy: PrivacyClass) -> Self {
241        self.privacy = privacy;
242        self
243    }
244
245    /// Marks the declaration as async metadata.
246    pub fn async_mode(mut self) -> Self {
247        self.mode = ToolkitToolExecutionMode::Async;
248        self.timeout_ms = self.timeout_ms.max(60_000);
249        self.cancellation = "cooperative".to_string();
250        self
251    }
252
253    /// Builds a synchronous tool declaration.
254    pub fn build(self) -> Result<Tool, AgentError> {
255        let mode = self.mode.clone();
256        Ok(Tool {
257            snapshot: self.build_snapshot()?,
258            mode,
259        })
260    }
261
262    /// Builds an async tool declaration.
263    pub fn build_async(self) -> Result<AsyncTool, AgentError> {
264        let builder = self.async_mode();
265        let mode = builder.mode.clone();
266        Ok(AsyncTool {
267            snapshot: builder.build_snapshot()?,
268            mode,
269        })
270    }
271
272    fn build_snapshot(self) -> Result<ToolPackToolSnapshot, AgentError> {
273        let effect_class = self
274            .effect_class
275            .ok_or_else(|| AgentError::missing_required_field("tool.effect_class"))?;
276        let risk_class = self
277            .risk_class
278            .ok_or_else(|| AgentError::missing_required_field("tool.risk_class"))?;
279        let capability_id = self
280            .capability_id
281            .unwrap_or_else(|| CapabilityId::new(format!("cap.tool.{}", self.tool_name)));
282        let namespace = self
283            .namespace
284            .unwrap_or_else(|| CapabilityNamespace::new(format!("tool.{}", self.tool_name)));
285        Ok(ToolPackToolSnapshot {
286            capability_id,
287            canonical_tool_name: CanonicalToolName::new(self.tool_name),
288            namespace,
289            schema_ref: PackageSidecarRef::new(self.schema_id, "tool_schema", "v1"),
290            executor_ref: ExecutorRef::new(self.executor_ref),
291            policy_refs: self.policy_refs,
292            required_permissions: self.required_permissions,
293            effect_class,
294            risk_class,
295            redaction_policy_ref: self.redaction_policy_ref,
296            timeout_ms: self.timeout_ms,
297            cancellation: self.cancellation,
298            reconciliation: self.reconciliation,
299            privacy: self.privacy,
300        })
301    }
302}
303
304#[derive(Clone, Debug)]
305/// Builder for a toolkit pack assembled from ergonomic `Tool` and
306/// `AsyncTool` declarations. Registering declarations with `listen` is
307/// data-only; execution still requires a core runtime and executor registry.
308pub struct ToolPackBuilder {
309    pack_id: ToolPackId,
310    kind: ToolPackKind,
311    version: String,
312    source: SourceRef,
313    trust: TrustClass,
314    tools: Vec<ToolPackToolSnapshot>,
315}
316
317impl ToolPackBuilder {
318    /// Creates a data-only toolkit pack builder.
319    pub fn new(
320        pack_id: ToolPackId,
321        kind: ToolPackKind,
322        version: impl Into<String>,
323        source: SourceRef,
324    ) -> Self {
325        Self {
326            pack_id,
327            kind,
328            version: version.into(),
329            source,
330            trust: TrustClass::SdkGenerated,
331            tools: Vec::new(),
332        }
333    }
334
335    /// Sets the trust class for the generated tool-pack snapshot.
336    pub fn trust(mut self, trust: TrustClass) -> Self {
337        self.trust = trust;
338        self
339    }
340
341    /// Listens to a synchronous tool declaration by adding it to the
342    /// generated tool-pack snapshot. This does not start an executor.
343    pub fn listen(mut self, tool: Tool) -> Self {
344        self.tools.push(tool.into_snapshot());
345        self
346    }
347
348    /// Listens to an async tool declaration by adding it to the generated
349    /// tool-pack snapshot. This does not start a task or runtime.
350    pub fn listen_async(mut self, tool: AsyncTool) -> Self {
351        self.tools.push(tool.into_snapshot());
352        self
353    }
354
355    /// Builds the toolkit pack bundle that core can install into a
356    /// `RuntimePackageBuilder`.
357    pub fn build(self) -> Result<ToolkitPackBundle, AgentError> {
358        let mut snapshot =
359            ToolPackSnapshot::new(self.pack_id, self.kind, self.version, self.source)
360                .with_trust(self.trust);
361        for tool in self.tools {
362            snapshot = snapshot.with_tool(tool);
363        }
364        ToolkitPackBundle::from_snapshot(snapshot)
365    }
366}