agent_sdk_core/ports/provider.rs
1//! Provider adapter contract and provider-facing projection DTOs. Hosts implement
2//! this port to call model providers after core has projected context and policy-safe
3//! metadata. Implementations may perform network I/O and must preserve redaction and
4//! stop/usage semantics.
5//!
6use serde::{Deserialize, Serialize};
7
8use crate::{
9 context::ContextProjection,
10 domain::{
11 AgentError, AgentErrorKind, DestinationKind, OutputSchemaId, PrivacyClass,
12 RetryClassification, SourceKind,
13 },
14 output::{ContentHash, OutputContract, ProviderHintPolicy, SchemaVersion},
15 projection::project_context_projection,
16};
17
18/// Port or behavior contract for provider adapter. Implementors should
19/// preserve policy, redaction, idempotency, and replay expectations
20/// from the surrounding module. Implementations may perform side
21/// effects only as described by the trait methods.
22pub trait ProviderAdapter: Send + Sync {
23 /// Returns adapter capability metadata for policy and package resolution.
24 /// This is data-only and does not perform I/O, call host ports, append journals, publish
25 /// events, or start processes.
26 fn capabilities(&self) -> ProviderCapabilities;
27
28 /// Projects admitted context into the provider's request shape.
29 /// This projects admitted context into a provider request and must not fetch hidden raw
30 /// content.
31 fn project_request(
32 &self,
33 projection: &ContextProjection,
34 policy: &ProviderProjectionPolicy,
35 ) -> Result<ProviderRequest, AgentError> {
36 project_context_projection(projection, policy)
37 }
38
39 /// Calls the provider for one non-streaming completion request.
40 /// Implementations may call the model provider; caller-owned runtime code must handle
41 /// policy, journaling, and event publication around it.
42 fn complete(&self, request: &ProviderRequest) -> Result<ProviderResponse, AgentError>;
43
44 /// Calls the provider for a streaming response.
45 /// Implementations may call the model provider; caller-owned runtime code must handle
46 /// policy, journaling, and event publication around it.
47 fn stream(&self, request: &ProviderRequest) -> Result<Vec<ProviderStreamChunk>, AgentError> {
48 let response = self.complete(request)?;
49 Ok(vec![ProviderStreamChunk::final_text(
50 response.output_text.clone(),
51 response.stop_reason.clone(),
52 response.usage.clone(),
53 )])
54 }
55
56 /// Extracts provider usage accounting from a response.
57 /// This derives usage accounting from a provider response and performs no provider call.
58 fn extract_usage(&self, response: &ProviderResponse) -> ProviderUsage {
59 response.usage.clone().unwrap_or_default()
60 }
61}
62
63#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
64/// Carries provider capabilities 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 ProviderCapabilities {
67 /// Typed provider ref reference. Resolving or executing it is a separate
68 /// policy-gated step.
69 pub provider_ref: String,
70 /// Boolean policy/capability flag for whether supports streaming is
71 /// enabled.
72 pub supports_streaming: bool,
73 /// Boolean policy/capability flag for whether supports usage is enabled.
74 pub supports_usage: bool,
75 /// Maximum allowed input tokens.
76 /// Use it to keep execution, output, or diagnostics bounded.
77 pub max_input_tokens: Option<u32>,
78 /// Collection of supported modalities values.
79 /// Ordering and membership should be treated as part of the serialized contract when
80 /// relevant.
81 pub supported_modalities: Vec<ProviderModality>,
82}
83
84impl ProviderCapabilities {
85 /// Returns an updated value with text only configured.
86 /// This is data-only and does not perform I/O, call host ports, append journals, publish
87 /// events, or start processes.
88 pub fn text_only(provider_ref: impl Into<String>) -> Self {
89 Self {
90 provider_ref: provider_ref.into(),
91 supports_streaming: false,
92 supports_usage: true,
93 max_input_tokens: None,
94 supported_modalities: vec![ProviderModality::Text],
95 }
96 }
97}
98
99#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
100#[serde(rename_all = "snake_case")]
101/// Enumerates the finite provider modality cases.
102/// Serialized names are part of the SDK contract; update fixtures when variants change.
103pub enum ProviderModality {
104 /// Use this variant when the contract needs to represent text; selecting it has no side effect by itself.
105 Text,
106 /// Use this variant when the contract needs to represent image; selecting it has no side effect by itself.
107 Image,
108 /// Use this variant when the contract needs to represent audio; selecting it has no side effect by itself.
109 Audio,
110 /// Use this variant when the contract needs to represent video; selecting it has no side effect by itself.
111 Video,
112}
113
114#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
115/// Carries provider projection policy data across a host-port boundary.
116/// Constructing the value does not call the host; the port method that receives it documents any adapter, network, or storage effect.
117pub struct ProviderProjectionPolicy {
118 /// Whether provider projection may include private metadata shell fields.
119 /// This does not allow raw private content; raw content still requires explicit resolver
120 /// and policy gates.
121 pub allow_private_metadata_projection: bool,
122 /// Typed projection policy ref reference. Resolving or executing it is a
123 /// separate policy-gated step.
124 pub projection_policy_ref: String,
125}
126
127impl ProviderProjectionPolicy {
128 /// Returns an updated value with redacted configured.
129 /// This is data-only and does not perform I/O, call host ports, append journals, publish
130 /// events, or start processes.
131 pub fn redacted(policy_ref: impl Into<String>) -> Self {
132 Self {
133 allow_private_metadata_projection: false,
134 projection_policy_ref: policy_ref.into(),
135 }
136 }
137
138 /// Returns an updated value with allow private metadata configured.
139 /// This is data-only policy/configuration construction and does not call provider adapters,
140 /// sinks, journals, or event buses.
141 pub fn allow_private_metadata(policy_ref: impl Into<String>) -> Self {
142 Self {
143 allow_private_metadata_projection: true,
144 projection_policy_ref: policy_ref.into(),
145 }
146 }
147}
148
149#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
150/// Carries provider request data across a host-port boundary.
151/// Constructing the value does not call the host; the port method that receives it documents any adapter, network, or storage effect.
152pub struct ProviderRequest {
153 /// Wire schema version used for compatibility checks.
154 pub schema_version: u16,
155 /// Typed projection policy ref reference. Resolving or executing it is a
156 /// separate policy-gated step.
157 pub projection_policy_ref: String,
158 /// Bounded messages included in this record. Limits and truncation are
159 /// represented by companion metadata when applicable.
160 pub messages: Vec<ProviderMessage>,
161 /// Count of projection item items observed or included in this record.
162 pub projection_item_count: usize,
163 #[serde(skip_serializing_if = "Option::is_none")]
164 /// Optional structured output hint value.
165 /// When absent, callers should use the documented default or skip that optional behavior.
166 pub structured_output_hint: Option<ProviderStructuredOutputHint>,
167}
168
169impl ProviderRequest {
170 /// Constant value for the ports::provider contract. Use it to keep
171 /// SDK records and tests aligned on the same stable value.
172 pub const SCHEMA_VERSION: u16 = 1;
173
174 /// Returns this value with its structured output hint setting
175 /// replaced. The method follows builder-style data construction and
176 /// does not execute external work.
177 pub fn with_structured_output_hint(mut self, contract: &OutputContract) -> Self {
178 self.structured_output_hint = Some(ProviderStructuredOutputHint::from(contract));
179 self
180 }
181}
182
183#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
184/// Carries provider structured output hint data across a host-port boundary.
185/// Constructing the value does not call the host; the port method that receives it documents any adapter, network, or storage effect.
186pub struct ProviderStructuredOutputHint {
187 /// Stable schema id used for typed lineage, lookup, or dedupe.
188 pub schema_id: OutputSchemaId,
189 /// Wire schema version used for compatibility checks.
190 pub schema_version: SchemaVersion,
191 /// Deterministic schema fingerprint used for stale checks, package
192 /// evidence, or replay comparisons.
193 pub schema_fingerprint: ContentHash,
194 /// Policy for provider-side structured-output hints.
195 /// Hints may guide prompting but cannot replace SDK-owned validation.
196 pub provider_hint_policy: ProviderHintPolicy,
197 /// Typed include schema ref reference. Resolving or executing it is a
198 /// separate policy-gated step.
199 pub include_schema_ref: bool,
200}
201
202impl From<&OutputContract> for ProviderStructuredOutputHint {
203 fn from(contract: &OutputContract) -> Self {
204 Self {
205 schema_id: contract.schema_id.clone(),
206 schema_version: contract.schema_version,
207 schema_fingerprint: contract.schema_fingerprint(),
208 provider_hint_policy: contract.projection_hint.provider_hint_policy.clone(),
209 include_schema_ref: contract.projection_hint.include_schema_ref,
210 }
211 }
212}
213
214#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
215/// Carries provider message data across a host-port boundary.
216/// Constructing the value does not call the host; the port method that receives it documents any adapter, network, or storage effect.
217pub struct ProviderMessage {
218 /// Role used by this record or request.
219 pub role: ProviderMessageRole,
220 /// Bounded textual content extracted for caller use; absent for binary
221 /// summaries or denied raw access.
222 pub content: String,
223 /// Privacy class used for projection, telemetry, and raw-content access
224 /// decisions.
225 pub privacy: PrivacyClass,
226 #[serde(skip_serializing_if = "Option::is_none")]
227 /// Optional projected metadata value.
228 /// When absent, callers should use the documented default or skip that optional behavior.
229 pub projected_metadata: Option<ProviderProjectedMetadata>,
230}
231
232#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
233#[serde(rename_all = "snake_case")]
234/// Enumerates the finite provider message role cases.
235/// Serialized names are part of the SDK contract; update fixtures when variants change.
236pub enum ProviderMessageRole {
237 /// Use this variant when the contract needs to represent system; selecting it has no side effect by itself.
238 System,
239 /// Use this variant when the contract needs to represent developer; selecting it has no side effect by itself.
240 Developer,
241 /// Use this variant when the contract needs to represent user; selecting it has no side effect by itself.
242 User,
243 /// Use this variant when the contract needs to represent assistant; selecting it has no side effect by itself.
244 Assistant,
245 /// Use this variant when the contract needs to represent tool; selecting it has no side effect by itself.
246 Tool,
247 /// Use this variant when the contract needs to represent context; selecting it has no side effect by itself.
248 Context,
249}
250
251#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
252/// Carries provider projected metadata data across a host-port boundary.
253/// Constructing the value does not call the host; the port method that receives it documents any adapter, network, or storage effect.
254pub struct ProviderProjectedMetadata {
255 /// Kind discriminator for source kind.
256 /// Use it to route finite match arms without parsing display text.
257 pub source_kind: SourceKind,
258 /// Stable source id used for typed lineage, lookup, or dedupe.
259 pub source_id: String,
260 /// Kind discriminator for destination kind.
261 /// Use it to route finite match arms without parsing display text.
262 pub destination_kind: DestinationKind,
263 /// Stable destination id used for typed lineage, lookup, or dedupe.
264 pub destination_id: String,
265 /// Kind discriminator for subject kind.
266 /// Use it to route finite match arms without parsing display text.
267 pub subject_kind: String,
268 /// Stable subject id used for typed lineage, lookup, or dedupe.
269 pub subject_id: String,
270}
271
272#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
273/// Carries provider response data across a host-port boundary.
274/// Constructing the value does not call the host; the port method that receives it documents any adapter, network, or storage effect.
275pub struct ProviderResponse {
276 /// Wire schema version used for compatibility checks.
277 pub schema_version: u16,
278 /// Output text used by this record or request.
279 pub output_text: String,
280 /// Stop reason used by this record or request.
281 pub stop_reason: ProviderStopReason,
282 #[serde(skip_serializing_if = "Option::is_none")]
283 /// Optional usage value.
284 /// When absent, callers should use the documented default or skip that optional behavior.
285 pub usage: Option<ProviderUsage>,
286}
287
288impl ProviderResponse {
289 /// Constant value for the ports::provider contract. Use it to keep
290 /// SDK records and tests aligned on the same stable value.
291 pub const SCHEMA_VERSION: u16 = 1;
292
293 /// Builds the text value.
294 /// This is data construction and performs no I/O, journal append, event publication, or
295 /// process work.
296 pub fn text(output_text: impl Into<String>) -> Self {
297 Self {
298 schema_version: Self::SCHEMA_VERSION,
299 output_text: output_text.into(),
300 stop_reason: ProviderStopReason::EndTurn,
301 usage: None,
302 }
303 }
304
305 /// Returns this value with its usage setting replaced. The method
306 /// follows builder-style data construction and does not execute
307 /// external work.
308 pub fn with_usage(mut self, usage: ProviderUsage) -> Self {
309 self.usage = Some(usage);
310 self
311 }
312}
313
314#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
315#[serde(rename_all = "snake_case")]
316/// Enumerates the finite provider stop reason cases.
317/// Serialized names are part of the SDK contract; update fixtures when variants change.
318pub enum ProviderStopReason {
319 /// Use this variant when the contract needs to represent end turn; selecting it has no side effect by itself.
320 EndTurn,
321 /// Use this variant when the contract needs to represent max tokens; selecting it has no side effect by itself.
322 MaxTokens,
323 /// Use this variant when the contract needs to represent cancelled; selecting it has no side effect by itself.
324 Cancelled,
325 /// Use this variant when the contract needs to represent provider error; selecting it has no side effect by itself.
326 ProviderError,
327 /// Use this variant when the contract needs to represent unknown; selecting it has no side effect by itself.
328 Unknown,
329}
330
331#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
332/// Carries provider stream chunk data across a host-port boundary.
333/// Constructing the value does not call the host; the port method that receives it documents any adapter, network, or storage effect.
334pub struct ProviderStreamChunk {
335 /// Wire schema version used for compatibility checks.
336 pub schema_version: u16,
337 /// Chunk index used by this record or request.
338 pub chunk_index: u32,
339 /// Delta used by this record or request.
340 pub delta: ProviderStreamDelta,
341 /// Whether is terminal is enabled.
342 /// Policy, validation, or routing code uses this flag to choose the explicit behavior.
343 pub is_terminal: bool,
344 #[serde(skip_serializing_if = "Option::is_none")]
345 /// Optional usage value.
346 /// When absent, callers should use the documented default or skip that optional behavior.
347 pub usage: Option<ProviderUsage>,
348}
349
350impl ProviderStreamChunk {
351 /// Constant value for the ports::provider contract. Use it to keep
352 /// SDK records and tests aligned on the same stable value.
353 pub const SCHEMA_VERSION: u16 = 1;
354
355 /// Builds the text value.
356 /// This is data construction and performs no I/O, journal append, event publication, or
357 /// process work.
358 pub fn text(chunk_index: u32, text: impl Into<String>) -> Self {
359 Self {
360 schema_version: Self::SCHEMA_VERSION,
361 chunk_index,
362 delta: ProviderStreamDelta::Text {
363 text: text.into(),
364 stop_reason: None,
365 },
366 is_terminal: false,
367 usage: None,
368 }
369 }
370
371 /// Builds the final text value.
372 /// This is data construction and performs no I/O, journal append, event publication, or
373 /// process work.
374 pub fn final_text(
375 text: impl Into<String>,
376 stop_reason: ProviderStopReason,
377 usage: Option<ProviderUsage>,
378 ) -> Self {
379 Self {
380 schema_version: Self::SCHEMA_VERSION,
381 chunk_index: 0,
382 delta: ProviderStreamDelta::Text {
383 text: text.into(),
384 stop_reason: Some(stop_reason),
385 },
386 is_terminal: true,
387 usage,
388 }
389 }
390}
391
392#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
393#[serde(tag = "kind", rename_all = "snake_case")]
394/// Enumerates the finite provider stream delta cases.
395/// Serialized names are part of the SDK contract; update fixtures when variants change.
396pub enum ProviderStreamDelta {
397 /// Use this variant when the contract needs to represent text; selecting it has no side effect by itself.
398 Text {
399 /// Text used by this record or request.
400 text: String,
401 #[serde(skip_serializing_if = "Option::is_none")]
402 /// Optional stop reason value.
403 /// When absent, callers should use the documented default or skip that optional
404 /// behavior.
405 stop_reason: Option<ProviderStopReason>,
406 },
407 /// Use this variant when the contract needs to represent usage; selecting it has no side effect by itself.
408 Usage {
409 /// Usage used by this record or request.
410 usage: ProviderUsage,
411 },
412 /// Provider stream or transport error. The payload must stay
413 /// redacted so live event observers do not require raw content.
414 Error {
415 /// Redacted human-readable summary safe for events, telemetry, and
416 /// logs.
417 redacted_summary: String,
418 },
419}
420
421#[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq, Serialize)]
422/// Carries provider usage data across a host-port boundary.
423/// Constructing the value does not call the host; the port method that receives it documents any adapter, network, or storage effect.
424pub struct ProviderUsage {
425 /// Optional input tokens value.
426 /// When absent, callers should use the documented default or skip that optional behavior.
427 pub input_tokens: Option<u32>,
428 /// Optional output tokens value.
429 /// When absent, callers should use the documented default or skip that optional behavior.
430 pub output_tokens: Option<u32>,
431 /// Optional total tokens value.
432 /// When absent, callers should use the documented default or skip that optional behavior.
433 pub total_tokens: Option<u32>,
434}
435
436#[derive(Clone, Debug)]
437/// Carries provider conformance case data across a host-port boundary.
438/// Constructing the value does not call the host; the port method that receives it documents any adapter, network, or storage effect.
439pub struct ProviderConformanceCase {
440 /// Projection controls for exposing data to a provider or subscriber.
441 /// Use it to keep provider-visible data separate from private SDK state.
442 pub projection: ContextProjection,
443 /// Policy used by this record or request.
444 pub policy: ProviderProjectionPolicy,
445}
446
447impl ProviderConformanceCase {
448 /// Creates a new ports::provider value with explicit
449 /// caller-provided inputs. This constructor is data-only and
450 /// performs no I/O or external side effects.
451 pub fn new(projection: ContextProjection) -> Self {
452 Self {
453 projection,
454 policy: ProviderProjectionPolicy::redacted("policy.provider.redacted"),
455 }
456 }
457
458 /// Builds the assert adapter value.
459 /// This is data construction and performs no I/O, journal append, event publication, or
460 /// process work.
461 pub fn assert_adapter<A: ProviderAdapter>(
462 &self,
463 adapter: &A,
464 ) -> Result<ProviderUsage, AgentError> {
465 let capabilities = adapter.capabilities();
466 if capabilities.provider_ref.is_empty() {
467 return Err(AgentError::new(
468 AgentErrorKind::ProviderFailure,
469 RetryClassification::HostConfigurationNeeded,
470 "provider capabilities must name a provider ref",
471 ));
472 }
473
474 let request = adapter.project_request(&self.projection, &self.policy)?;
475 if request.projection_item_count != self.projection.items.len() {
476 return Err(AgentError::new(
477 AgentErrorKind::ProjectionFailure,
478 RetryClassification::RepairNeeded,
479 "provider request item count must match the context projection",
480 ));
481 }
482
483 let response = adapter.complete(&request)?;
484 Ok(adapter.extract_usage(&response))
485 }
486}