Skip to main content

agent_sdk_toolkit/discovery/
executor.rs

1//! Tool discovery helpers for optional toolkit capabilities. Use these modules to
2//! index hidden candidates, return model-facing discovery results, and construct
3//! package deltas for host-approved activation. Searching is data-only; package
4//! mutation happens only when a delta is applied. This file contains the executor
5//! portion of that contract.
6//!
7use agent_sdk_core::{
8    AgentError, ExecutorRef, PolicyRef, ToolExecutionOutput, ToolExecutionRequest, ToolExecutor,
9    ToolPackId, ToolPackKind, ToolPackSnapshot,
10    domain::ContentRef,
11    policy::{CapabilityPermission, EffectClass, RiskClass},
12};
13
14use crate::{
15    packs::{ToolkitPackBundle, tool_snapshot},
16    testing::{InMemoryJsonArgumentStore, InMemoryToolkitContentStore},
17};
18
19use super::{
20    index::ToolDiscoveryIndex,
21    types::{ToolDiscoveryOutput, ToolDiscoveryRequest},
22};
23
24#[derive(Clone)]
25/// Discovery tool discovery executor request or result value.
26/// Creating the value does not register tools; discovery executors document catalog and package-bundle effects.
27pub struct ToolDiscoveryExecutor {
28    executor_ref: ExecutorRef,
29    index: ToolDiscoveryIndex,
30    arguments: InMemoryJsonArgumentStore,
31    content: InMemoryToolkitContentStore,
32}
33
34impl ToolDiscoveryExecutor {
35    /// Creates a new discovery::executor value with explicit
36    /// caller-provided inputs. This constructor is data-only and
37    /// performs no I/O or external side effects.
38    pub fn new(
39        index: ToolDiscoveryIndex,
40        arguments: InMemoryJsonArgumentStore,
41        content: InMemoryToolkitContentStore,
42    ) -> Self {
43        Self {
44            executor_ref: ExecutorRef::new("executor.toolkit.tool_discovery.v1"),
45            index,
46            arguments,
47            content,
48        }
49    }
50
51    /// Returns the pack bundle currently held by this value.
52    /// This returns the toolkit pack bundle that registers the executor routes; it does not
53    /// activate the pack itself.
54    pub fn pack_bundle(
55        source: agent_sdk_core::SourceRef,
56        policy_ref: PolicyRef,
57    ) -> Result<ToolkitPackBundle, AgentError> {
58        let snapshot = ToolPackSnapshot::new(
59            ToolPackId::new("toolpack.tool_discovery.v1"),
60            ToolPackKind::ToolDiscovery,
61            "v1",
62            source.clone(),
63        )
64        .with_discovery(agent_sdk_core::ToolDiscoverySnapshot {
65            discovery_index_id: "toolkit.discovery.default".to_string(),
66            activation_policy_ref: policy_ref.clone(),
67            package_delta_required: true,
68        })
69        .with_tool(tool_snapshot(
70            "cap.toolkit.tool_discovery",
71            "tool_discovery",
72            "executor.toolkit.tool_discovery.v1",
73            "schema.toolkit.tool_discovery.v1",
74            vec![policy_ref],
75            vec![CapabilityPermission::FilesystemRead],
76            EffectClass::Read,
77            RiskClass::Low,
78            &source,
79        ));
80        ToolkitPackBundle::from_snapshot(snapshot)
81    }
82}
83
84impl ToolExecutor for ToolDiscoveryExecutor {
85    fn executor_ref(&self) -> &ExecutorRef {
86        &self.executor_ref
87    }
88
89    fn execute(&self, request: &ToolExecutionRequest) -> Result<ToolExecutionOutput, AgentError> {
90        let args_ref = request.effect_intent.content_refs.first().ok_or_else(|| {
91            AgentError::missing_required_field("tool_discovery.argument_content_ref")
92        })?;
93        let discovery_request: ToolDiscoveryRequest = self.arguments.get(args_ref)?;
94        let output = ToolDiscoveryOutput {
95            query: discovery_request.query.clone(),
96            candidates: self.index.search(&discovery_request.query),
97            package_delta_required: true,
98        };
99        let content_ref = ContentRef::new(format!(
100            "content.{}.tool_discovery",
101            request.resolved_call.request.tool_call_id.as_str()
102        ));
103        self.content.put(content_ref.clone(), &output)?;
104        let mut envelope = ToolExecutionOutput::completed(
105            "tool discovery returned candidates without mutating active package",
106        );
107        envelope.content_refs.push(content_ref);
108        Ok(envelope)
109    }
110}