Skip to main content

agent_sdk_core/ports/
tool_pack.rs

1//! Host adapter boundaries for the SDK core. Use these traits and registries when
2//! hosts provide providers, journals, sinks, tools, isolation, extensions, telemetry,
3//! or subscriptions. Implementations may perform external side effects and must honor
4//! policy, redaction, idempotency, and replay contracts. This file contains the tool
5//! pack portion of that contract.
6//!
7use std::{collections::BTreeMap, sync::Arc};
8
9use serde::{Deserialize, Serialize};
10
11use crate::domain::{
12    AgentError, AgentErrorKind, ContentRef, PolicyRef, PrivacyClass, RetentionClass,
13    RetryClassification, SourceRef,
14};
15
16#[derive(Clone, Debug, Deserialize, Eq, Hash, Ord, PartialEq, PartialOrd, Serialize)]
17#[serde(transparent)]
18/// Carries resource scheme data across a host-port boundary.
19/// Constructing the value does not call the host; the port method that receives it documents any adapter, network, or storage effect.
20pub struct ResourceScheme(String);
21
22impl ResourceScheme {
23    /// Creates a new ports::tool_pack value with explicit
24    /// caller-provided inputs. This constructor is data-only and
25    /// performs no I/O or external side effects.
26    pub fn new(value: impl Into<String>) -> Self {
27        let value = value.into();
28        assert!(!value.is_empty(), "ResourceScheme must not be empty");
29        assert!(
30            value
31                .chars()
32                .all(|character| character.is_ascii_alphanumeric()
33                    || matches!(character, '+' | '-' | '.')),
34            "ResourceScheme must use URI scheme characters"
35        );
36        Self(value)
37    }
38
39    /// Returns this value as str. The accessor is side-effect free and
40    /// keeps ownership with the caller.
41    pub fn as_str(&self) -> &str {
42        &self.0
43    }
44}
45
46#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
47/// Carries resource read request data across a host-port boundary.
48/// Constructing the value does not call the host; the port method that receives it documents any adapter, network, or storage effect.
49pub struct ResourceReadRequest {
50    /// Resource URI selected for explicit resolution.
51    pub uri: String,
52    /// Source label or ref for this item; it is metadata and does not fetch
53    /// content by itself.
54    pub source: SourceRef,
55    /// Policy references that govern admission, projection, execution, or
56    /// delivery.
57    pub policy_refs: Vec<PolicyRef>,
58    /// Maximum byte budget the caller requested before truncation or summary
59    /// behavior is applied.
60    pub max_bytes: u64,
61}
62
63#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
64/// Carries resource resolution data across a host-port boundary.
65/// Constructing the value does not call the host; the port method that receives it documents any adapter, network, or storage effect.
66pub struct ResourceResolution {
67    /// Resource URI selected for explicit resolution.
68    pub uri: String,
69    /// URI scheme resolved by the resource reader.
70    pub scheme: ResourceScheme,
71    /// Content reference where payload bytes or structured tool output are
72    /// stored.
73    pub content_ref: ContentRef,
74    /// Source label or ref for this item; it is metadata and does not fetch
75    /// content by itself.
76    pub source: SourceRef,
77    /// Policy references that govern admission, projection, execution, or
78    /// delivery.
79    pub policy_refs: Vec<PolicyRef>,
80    /// Observed byte length for the source, sidecar, or extracted record.
81    pub byte_len: u64,
82    /// Whether output was shortened by byte, item, page, archive, or parser
83    /// limits.
84    pub truncated: bool,
85    /// Version string for this capability, package, or protocol surface.
86    /// Use it for compatibility checks during package or adapter resolution.
87    pub parser_version: String,
88    /// Privacy class used for projection, telemetry, and raw-content access
89    /// decisions.
90    pub privacy: PrivacyClass,
91    /// Retention class used by hosts and sinks when storing or exporting this
92    /// item.
93    pub retention: RetentionClass,
94    /// Redacted human-readable summary safe for events, telemetry, and logs.
95    pub redacted_summary: String,
96}
97
98/// Port or behavior contract for resource resolver. Implementors should
99/// preserve policy, redaction, idempotency, and replay expectations
100/// from the surrounding module. Implementations may perform side
101/// effects only as described by the trait methods.
102pub trait ResourceResolver: Send + Sync {
103    /// Returns the scheme identifier for this adapter.
104    /// This returns resource routing metadata and does not resolve the resource.
105    fn scheme(&self) -> &ResourceScheme;
106
107    /// Resolves resolve through the configured ports::tool_pack boundary.
108    /// Concrete implementations own any backing-store, filesystem, or network
109    /// side effects.
110    fn resolve(&self, request: &ResourceReadRequest) -> Result<ResourceResolution, AgentError>;
111}
112
113#[derive(Clone, Default)]
114/// Carries resource router data across a host-port boundary.
115/// Constructing the value does not call the host; the port method that receives it documents any adapter, network, or storage effect.
116pub struct ResourceRouter {
117    resolvers: BTreeMap<ResourceScheme, Arc<dyn ResourceResolver>>,
118}
119
120impl ResourceRouter {
121    /// Creates a new ports::tool_pack value with explicit
122    /// caller-provided inputs. This constructor is data-only and
123    /// performs no I/O or external side effects.
124    pub fn new() -> Self {
125        Self::default()
126    }
127
128    /// Adds data to this in-memory ports::tool_pack collection. It does not
129    /// perform external I/O, execute tools, or append journals.
130    pub fn register(&mut self, resolver: Arc<dyn ResourceResolver>) {
131        self.resolvers.insert(resolver.scheme().clone(), resolver);
132    }
133
134    /// Builds the register static value.
135    /// This is data construction and performs no I/O, journal append, event publication, or
136    /// process work.
137    pub fn register_static(
138        &mut self,
139        scheme: ResourceScheme,
140        content_ref: ContentRef,
141        source: SourceRef,
142        policy_ref: PolicyRef,
143    ) {
144        self.register(Arc::new(StaticResourceResolver {
145            scheme,
146            content_ref,
147            source,
148            policy_ref,
149        }));
150    }
151
152    /// Resolves resolve through the configured ports::tool_pack boundary.
153    /// Concrete implementations own any backing-store, filesystem, or network
154    /// side effects.
155    pub fn resolve(&self, request: &ResourceReadRequest) -> Result<ResourceResolution, AgentError> {
156        let scheme = parse_scheme(&request.uri)?;
157        let Some(resolver) = self.resolvers.get(&scheme) else {
158            return Err(AgentError::new(
159                AgentErrorKind::PolicyDenial,
160                RetryClassification::HostConfigurationNeeded,
161                "resource URI scheme has no registered resolver in the runtime package boundary",
162            ));
163        };
164        resolver.resolve(request)
165    }
166}
167
168#[derive(Clone)]
169struct StaticResourceResolver {
170    scheme: ResourceScheme,
171    content_ref: ContentRef,
172    source: SourceRef,
173    policy_ref: PolicyRef,
174}
175
176impl ResourceResolver for StaticResourceResolver {
177    fn scheme(&self) -> &ResourceScheme {
178        &self.scheme
179    }
180
181    fn resolve(&self, request: &ResourceReadRequest) -> Result<ResourceResolution, AgentError> {
182        Ok(ResourceResolution {
183            uri: request.uri.clone(),
184            scheme: self.scheme.clone(),
185            content_ref: self.content_ref.clone(),
186            source: self.source.clone(),
187            policy_refs: vec![self.policy_ref.clone()],
188            byte_len: 0,
189            truncated: false,
190            parser_version: "static.resource.resolver.v1".to_string(),
191            privacy: PrivacyClass::ContentRefsOnly,
192            retention: RetentionClass::RunScoped,
193            redacted_summary: "resource resolved to content ref".to_string(),
194        })
195    }
196}
197
198fn parse_scheme(uri: &str) -> Result<ResourceScheme, AgentError> {
199    let Some((scheme, _rest)) = uri.split_once("://") else {
200        return Err(AgentError::contract_violation(
201            "resource URI must include a scheme",
202        ));
203    };
204    if scheme.is_empty() {
205        return Err(AgentError::contract_violation(
206            "resource URI scheme must not be empty",
207        ));
208    }
209    Ok(ResourceScheme::new(scheme))
210}