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}