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    description: Option<String>,
127    redacted_schema: Option<serde_json::Value>,
128    policy_refs: Vec<PolicyRef>,
129    requires_approval: bool,
130    capability_id: Option<CapabilityId>,
131    namespace: Option<CapabilityNamespace>,
132    required_permissions: Vec<CapabilityPermission>,
133    effect_class: Option<EffectClass>,
134    risk_class: Option<RiskClass>,
135    redaction_policy_ref: PolicyRef,
136    timeout_ms: u64,
137    cancellation: String,
138    reconciliation: String,
139    privacy: PrivacyClass,
140    mode: ToolkitToolExecutionMode,
141}
142
143impl ToolBuilder {
144    /// Creates a new data-only tool declaration builder.
145    pub fn new(
146        tool_name: impl Into<String>,
147        executor_ref: impl Into<String>,
148        schema_id: impl Into<String>,
149        policy_ref: PolicyRef,
150    ) -> Self {
151        Self {
152            tool_name: tool_name.into(),
153            executor_ref: executor_ref.into(),
154            schema_id: schema_id.into(),
155            description: None,
156            redacted_schema: None,
157            policy_refs: vec![policy_ref],
158            requires_approval: false,
159            capability_id: None,
160            namespace: None,
161            required_permissions: Vec::new(),
162            effect_class: None,
163            risk_class: None,
164            redaction_policy_ref: PolicyRef::with_kind(
165                PolicyKind::Redaction,
166                "policy.redaction.tool_result.refs_only",
167            ),
168            timeout_ms: 10_000,
169            cancellation: "best_effort".to_string(),
170            reconciliation: "effect_lineage_required".to_string(),
171            privacy: PrivacyClass::ContentRefsOnly,
172            mode: ToolkitToolExecutionMode::Sync,
173        }
174    }
175
176    /// Marks this declaration as a read-like tool.
177    pub fn read_only(mut self) -> Self {
178        self.effect_class = Some(EffectClass::Read);
179        self.risk_class = Some(RiskClass::Low);
180        self
181    }
182
183    /// Marks this declaration as a write-like tool.
184    pub fn write_effect(mut self) -> Self {
185        self.effect_class = Some(EffectClass::Write);
186        self.risk_class = Some(RiskClass::High);
187        self
188    }
189
190    /// Sets an explicit effect and risk class.
191    pub fn effect(mut self, effect_class: EffectClass, risk_class: RiskClass) -> Self {
192        self.effect_class = Some(effect_class);
193        self.risk_class = Some(risk_class);
194        self
195    }
196
197    /// Sets the stable capability id used by the runtime package.
198    pub fn capability_id(mut self, capability_id: CapabilityId) -> Self {
199        self.capability_id = Some(capability_id);
200        self
201    }
202
203    /// Sets the capability namespace.
204    pub fn namespace(mut self, namespace: CapabilityNamespace) -> Self {
205        self.namespace = Some(namespace);
206        self
207    }
208
209    /// Adds a required permission to the provider-visible tool snapshot.
210    pub fn required_permission(mut self, permission: CapabilityPermission) -> Self {
211        self.required_permissions.push(permission);
212        self
213    }
214
215    /// Attaches a provider-safe JSON schema body to this tool declaration.
216    pub fn redacted_schema(mut self, schema: serde_json::Value) -> Self {
217        self.redacted_schema = Some(schema);
218        self
219    }
220
221    /// Attaches a bounded provider-visible description to this declaration.
222    pub fn description(mut self, description: impl Into<String>) -> Self {
223        let description = description.into();
224        if !description.trim().is_empty() {
225            self.description = Some(description);
226        }
227        self
228    }
229
230    /// Attaches a provider-visible description when one is present.
231    pub fn description_opt(mut self, description: Option<String>) -> Self {
232        self.description = description.filter(|description| !description.trim().is_empty());
233        self
234    }
235
236    /// Adds another policy ref that must travel with the tool declaration.
237    pub fn policy_ref(mut self, policy_ref: PolicyRef) -> Self {
238        self.policy_refs.push(policy_ref);
239        self
240    }
241
242    /// Requires host approval before the core runtime releases the executor.
243    pub fn require_approval(mut self) -> Self {
244        self.requires_approval = true;
245        self.policy_refs.push(PolicyRef::with_kind(
246            PolicyKind::Approval,
247            format!("policy.approval.{}", self.tool_name),
248        ));
249        self.risk_class = Some(RiskClass::High);
250        self
251    }
252
253    /// Sets the redaction policy ref for tool results.
254    pub fn redaction_policy(mut self, policy_ref: PolicyRef) -> Self {
255        self.redaction_policy_ref = policy_ref;
256        self
257    }
258
259    /// Sets the execution timeout metadata.
260    pub fn timeout_ms(mut self, timeout_ms: u64) -> Self {
261        self.timeout_ms = timeout_ms;
262        self
263    }
264
265    /// Sets the cancellation metadata.
266    pub fn cancellation(mut self, cancellation: impl Into<String>) -> Self {
267        self.cancellation = cancellation.into();
268        self
269    }
270
271    /// Sets the reconciliation metadata.
272    pub fn reconciliation(mut self, reconciliation: impl Into<String>) -> Self {
273        self.reconciliation = reconciliation.into();
274        self
275    }
276
277    /// Sets the privacy class used for the tool snapshot and route.
278    pub fn privacy(mut self, privacy: PrivacyClass) -> Self {
279        self.privacy = privacy;
280        self
281    }
282
283    /// Marks the declaration as async metadata.
284    pub fn async_mode(mut self) -> Self {
285        self.mode = ToolkitToolExecutionMode::Async;
286        self.timeout_ms = self.timeout_ms.max(60_000);
287        self.cancellation = "cooperative".to_string();
288        self
289    }
290
291    /// Builds a synchronous tool declaration.
292    pub fn build(self) -> Result<Tool, AgentError> {
293        let mode = self.mode.clone();
294        Ok(Tool {
295            snapshot: self.build_snapshot()?,
296            mode,
297        })
298    }
299
300    /// Builds an async tool declaration.
301    pub fn build_async(self) -> Result<AsyncTool, AgentError> {
302        let builder = self.async_mode();
303        let mode = builder.mode.clone();
304        Ok(AsyncTool {
305            snapshot: builder.build_snapshot()?,
306            mode,
307        })
308    }
309
310    fn build_snapshot(self) -> Result<ToolPackToolSnapshot, AgentError> {
311        let effect_class = self
312            .effect_class
313            .ok_or_else(|| AgentError::missing_required_field("tool.effect_class"))?;
314        let risk_class = self
315            .risk_class
316            .ok_or_else(|| AgentError::missing_required_field("tool.risk_class"))?;
317        let capability_id = self
318            .capability_id
319            .unwrap_or_else(|| CapabilityId::new(format!("cap.tool.{}", self.tool_name)));
320        let namespace = self
321            .namespace
322            .unwrap_or_else(|| CapabilityNamespace::new(format!("tool.{}", self.tool_name)));
323        Ok(ToolPackToolSnapshot {
324            capability_id,
325            canonical_tool_name: CanonicalToolName::new(self.tool_name),
326            namespace,
327            description: self.description,
328            schema_ref: PackageSidecarRef::new(self.schema_id, "tool_schema", "v1"),
329            redacted_schema: self.redacted_schema,
330            executor_ref: ExecutorRef::new(self.executor_ref),
331            policy_refs: self.policy_refs,
332            requires_approval: self.requires_approval,
333            required_permissions: self.required_permissions,
334            effect_class,
335            risk_class,
336            redaction_policy_ref: self.redaction_policy_ref,
337            timeout_ms: self.timeout_ms,
338            cancellation: self.cancellation,
339            reconciliation: self.reconciliation,
340            privacy: self.privacy,
341        })
342    }
343}
344
345#[derive(Clone, Debug)]
346/// Builder for a toolkit pack assembled from ergonomic `Tool` and
347/// `AsyncTool` declarations. Registering declarations with `listen` is
348/// data-only; execution still requires a core runtime and executor registry.
349pub struct ToolPackBuilder {
350    pack_id: ToolPackId,
351    kind: ToolPackKind,
352    version: String,
353    source: SourceRef,
354    trust: TrustClass,
355    tools: Vec<ToolPackToolSnapshot>,
356}
357
358impl ToolPackBuilder {
359    /// Creates a data-only toolkit pack builder.
360    pub fn new(
361        pack_id: ToolPackId,
362        kind: ToolPackKind,
363        version: impl Into<String>,
364        source: SourceRef,
365    ) -> Self {
366        Self {
367            pack_id,
368            kind,
369            version: version.into(),
370            source,
371            trust: TrustClass::SdkGenerated,
372            tools: Vec::new(),
373        }
374    }
375
376    /// Sets the trust class for the generated tool-pack snapshot.
377    pub fn trust(mut self, trust: TrustClass) -> Self {
378        self.trust = trust;
379        self
380    }
381
382    /// Listens to a synchronous tool declaration by adding it to the
383    /// generated tool-pack snapshot. This does not start an executor.
384    pub fn listen(mut self, tool: Tool) -> Self {
385        self.tools.push(tool.into_snapshot());
386        self
387    }
388
389    /// Listens to an async tool declaration by adding it to the generated
390    /// tool-pack snapshot. This does not start a task or runtime.
391    pub fn listen_async(mut self, tool: AsyncTool) -> Self {
392        self.tools.push(tool.into_snapshot());
393        self
394    }
395
396    /// Builds the toolkit pack bundle that core can install into a
397    /// `RuntimePackageBuilder`.
398    pub fn build(self) -> Result<ToolkitPackBundle, AgentError> {
399        let mut snapshot =
400            ToolPackSnapshot::new(self.pack_id, self.kind, self.version, self.source)
401                .with_trust(self.trust);
402        for tool in self.tools {
403            snapshot = snapshot.with_tool(tool);
404        }
405        ToolkitPackBundle::from_snapshot(snapshot)
406    }
407}