Skip to main content

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}