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 core::fmt;
7
8use serde::{Deserialize, Serialize};
9
10use crate::{
11    capability::{CapabilityId, CapabilityNamespace, PackageSidecarRef},
12    context::ContextProjection,
13    domain::{
14        AgentError, AgentErrorKind, ContentRef, DestinationKind, OutputSchemaId, PolicyRef,
15        PrivacyClass, RetryClassification, SourceKind, ToolCallId,
16    },
17    output::{ContentHash, OutputContract, OutputSchemaRef, ProviderHintPolicy, SchemaVersion},
18    projection::project_context_projection,
19    tool_records::CanonicalToolName,
20};
21
22/// Port or behavior contract for provider adapter. Implementors should
23/// preserve policy, redaction, idempotency, and replay expectations
24/// from the surrounding module. Implementations may perform side
25/// effects only as described by the trait methods.
26pub trait ProviderAdapter: Send + Sync {
27    /// Returns adapter capability metadata for policy and package resolution.
28    /// This is data-only and does not perform I/O, call host ports, append journals, publish
29    /// events, or start processes.
30    fn capabilities(&self) -> ProviderCapabilities;
31
32    /// Projects admitted context into the provider's request shape.
33    /// This projects admitted context into a provider request and must not fetch hidden raw
34    /// content.
35    fn project_request(
36        &self,
37        projection: &ContextProjection,
38        policy: &ProviderProjectionPolicy,
39    ) -> Result<ProviderRequest, AgentError> {
40        project_context_projection(projection, policy)
41    }
42
43    /// Calls the provider for one non-streaming completion request.
44    /// Implementations may call the model provider; caller-owned runtime code must handle
45    /// policy, journaling, and event publication around it.
46    fn complete(&self, request: &ProviderRequest) -> Result<ProviderResponse, AgentError>;
47
48    /// Calls the provider for a streaming response.
49    /// Implementations may call the model provider; caller-owned runtime code must handle
50    /// policy, journaling, and event publication around it.
51    fn stream(&self, request: &ProviderRequest) -> Result<Vec<ProviderStreamChunk>, AgentError> {
52        let response = self.complete(request)?;
53        if response.tool_calls.is_empty() {
54            return Ok(vec![ProviderStreamChunk::final_text(
55                response.output_text.clone(),
56                response.stop_reason.clone(),
57                response.usage.clone(),
58            )]);
59        }
60
61        let mut chunks = Vec::new();
62        let mut chunk_index = 0;
63        if !response.output_text.is_empty() {
64            chunks.push(ProviderStreamChunk::text(
65                chunk_index,
66                response.output_text.clone(),
67            ));
68            chunk_index += 1;
69        }
70        chunks.push(ProviderStreamChunk::final_tool_calls(
71            chunk_index,
72            response.tool_calls.clone(),
73            response.stop_reason.clone(),
74            response.usage.clone(),
75        ));
76        Ok(chunks)
77    }
78
79    /// Extracts provider usage accounting from a response.
80    /// This derives usage accounting from a provider response and performs no provider call.
81    fn extract_usage(&self, response: &ProviderResponse) -> ProviderUsage {
82        response.usage.clone().unwrap_or_default()
83    }
84}
85
86/// Read/write store contract for raw provider tool-call arguments.
87///
88/// Implementations must keep raw arguments out of journals, events, summaries,
89/// and debug output, returning content refs for later policy-checked access.
90pub trait ProviderArgumentStore: Send + Sync {
91    /// Stores raw provider tool arguments and returns a content ref when the
92    /// host wants executors to resolve arguments through normal content policy.
93    fn store_provider_arguments(
94        &self,
95        provider_ref: &str,
96        call_id: &str,
97        canonical_tool_name: &CanonicalToolName,
98        raw_arguments: &str,
99    ) -> Result<Option<ContentRef>, AgentError>;
100
101    /// Loads stored provider tool arguments as JSON through the same content
102    /// ref returned by `store_provider_arguments`.
103    fn load_provider_arguments_json(
104        &self,
105        content_ref: &ContentRef,
106    ) -> Result<serde_json::Value, AgentError>;
107}
108
109#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
110/// Carries provider capabilities data across a host-port boundary.
111/// Constructing the value does not call the host; the port method that receives it documents any adapter, network, or storage effect.
112pub struct ProviderCapabilities {
113    /// Typed provider ref reference. Resolving or executing it is a separate
114    /// policy-gated step.
115    pub provider_ref: String,
116    /// Boolean policy/capability flag for whether supports streaming is
117    /// enabled.
118    pub supports_streaming: bool,
119    /// Boolean policy/capability flag for whether supports usage is enabled.
120    pub supports_usage: bool,
121    /// Maximum allowed input tokens.
122    /// Use it to keep execution, output, or diagnostics bounded.
123    pub max_input_tokens: Option<u32>,
124    /// Collection of supported modalities values.
125    /// Ordering and membership should be treated as part of the serialized contract when
126    /// relevant.
127    pub supported_modalities: Vec<ProviderModality>,
128}
129
130impl ProviderCapabilities {
131    /// Returns an updated value with text only configured.
132    /// This is data-only and does not perform I/O, call host ports, append journals, publish
133    /// events, or start processes.
134    pub fn text_only(provider_ref: impl Into<String>) -> Self {
135        Self {
136            provider_ref: provider_ref.into(),
137            supports_streaming: false,
138            supports_usage: true,
139            max_input_tokens: None,
140            supported_modalities: vec![ProviderModality::Text],
141        }
142    }
143}
144
145#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
146#[serde(rename_all = "snake_case")]
147/// Enumerates the finite provider modality cases.
148/// Serialized names are part of the SDK contract; update fixtures when variants change.
149pub enum ProviderModality {
150    /// Use this variant when the contract needs to represent text; selecting it has no side effect by itself.
151    Text,
152    /// Use this variant when the contract needs to represent image; selecting it has no side effect by itself.
153    Image,
154    /// Use this variant when the contract needs to represent audio; selecting it has no side effect by itself.
155    Audio,
156    /// Use this variant when the contract needs to represent video; selecting it has no side effect by itself.
157    Video,
158}
159
160#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
161/// Carries provider projection policy data across a host-port boundary.
162/// Constructing the value does not call the host; the port method that receives it documents any adapter, network, or storage effect.
163pub struct ProviderProjectionPolicy {
164    /// Whether provider projection may include private metadata shell fields.
165    /// This does not allow raw private content; raw content still requires explicit resolver
166    /// and policy gates.
167    pub allow_private_metadata_projection: bool,
168    /// Typed projection policy ref reference. Resolving or executing it is a
169    /// separate policy-gated step.
170    pub projection_policy_ref: String,
171}
172
173impl ProviderProjectionPolicy {
174    /// Returns an updated value with redacted configured.
175    /// This is data-only and does not perform I/O, call host ports, append journals, publish
176    /// events, or start processes.
177    pub fn redacted(policy_ref: impl Into<String>) -> Self {
178        Self {
179            allow_private_metadata_projection: false,
180            projection_policy_ref: policy_ref.into(),
181        }
182    }
183
184    /// Returns an updated value with allow private metadata configured.
185    /// This is data-only policy/configuration construction and does not call provider adapters,
186    /// sinks, journals, or event buses.
187    pub fn allow_private_metadata(policy_ref: impl Into<String>) -> Self {
188        Self {
189            allow_private_metadata_projection: true,
190            projection_policy_ref: policy_ref.into(),
191        }
192    }
193}
194
195#[derive(Clone, Deserialize, Eq, PartialEq, Serialize)]
196/// Carries provider request data across a host-port boundary.
197/// Constructing the value does not call the host; the port method that receives it documents any adapter, network, or storage effect.
198pub struct ProviderRequest {
199    /// Wire schema version used for compatibility checks.
200    pub schema_version: u16,
201    /// Typed projection policy ref reference. Resolving or executing it is a
202    /// separate policy-gated step.
203    pub projection_policy_ref: String,
204    /// Bounded messages included in this record. Limits and truncation are
205    /// represented by companion metadata when applicable.
206    pub messages: Vec<ProviderMessage>,
207    /// Count of projection item items observed or included in this record.
208    pub projection_item_count: usize,
209    #[serde(skip_serializing_if = "Option::is_none")]
210    /// Optional structured output hint value.
211    /// When absent, callers should use the documented default or skip that optional behavior.
212    pub structured_output_hint: Option<ProviderStructuredOutputHint>,
213    #[serde(default, skip_serializing_if = "Vec::is_empty")]
214    /// Provider-visible tool declarations projected from the effective runtime package.
215    /// These are declarations only: provider adapters may render them into native
216    /// function/tool shapes, but core remains authoritative for routing, approval,
217    /// execution, journaling, and redaction.
218    pub tools: Vec<ProviderToolSpec>,
219}
220
221impl ProviderRequest {
222    /// Constant value for the ports::provider contract. Use it to keep
223    /// SDK records and tests aligned on the same stable value.
224    pub const SCHEMA_VERSION: u16 = 1;
225
226    /// Returns this value with its structured output hint setting
227    /// replaced. The method follows builder-style data construction and
228    /// does not execute external work.
229    pub fn with_structured_output_hint(mut self, contract: &OutputContract) -> Self {
230        self.structured_output_hint = Some(ProviderStructuredOutputHint::from(contract));
231        self
232    }
233
234    /// Returns this value with provider-visible tool declarations attached.
235    /// This is data-only projection; it does not execute tools or resolve schema refs.
236    pub fn with_tools(mut self, tools: impl IntoIterator<Item = ProviderToolSpec>) -> Self {
237        self.tools = tools.into_iter().collect();
238        self
239    }
240}
241
242impl fmt::Debug for ProviderRequest {
243    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
244        formatter
245            .debug_struct("ProviderRequest")
246            .field("schema_version", &self.schema_version)
247            .field("projection_policy_ref", &self.projection_policy_ref)
248            .field("message_count", &self.messages.len())
249            .field("messages", &"<redacted>")
250            .field("projection_item_count", &self.projection_item_count)
251            .field("structured_output_hint", &self.structured_output_hint)
252            .field("tool_count", &self.tools.len())
253            .field("tools", &"<redacted>")
254            .finish()
255    }
256}
257
258#[derive(Clone, Deserialize, Eq, PartialEq, Serialize)]
259/// Provider-visible tool declaration projected from runtime-package state.
260///
261/// This DTO is safe for provider adapters because it carries only stable names,
262/// refs, policy identifiers, and optional redacted schema bodies. Raw tool
263/// arguments and executable handles never appear here.
264pub struct ProviderToolSpec {
265    /// Provider-visible canonical function/tool name.
266    pub name: String,
267    /// Stable capability id that owns this provider-visible declaration.
268    pub capability_id: CapabilityId,
269    /// Capability namespace carried for lineage and collision debugging.
270    pub namespace: CapabilityNamespace,
271    #[serde(default, skip_serializing_if = "Option::is_none")]
272    /// Bounded provider-visible tool description from the runtime package.
273    ///
274    /// This is projection metadata only. It does not affect tool execution,
275    /// approval, policy, or runtime-package identity.
276    pub description: Option<String>,
277    /// Tool input schema ref. Resolving it is separate from constructing a
278    /// provider request.
279    pub schema_ref: PackageSidecarRef,
280    #[serde(default, skip_serializing_if = "Vec::is_empty")]
281    /// Policy refs that must still be evaluated by core before execution.
282    pub policy_refs: Vec<PolicyRef>,
283    #[serde(default, skip_serializing_if = "Option::is_none")]
284    /// Redacted inline schema body, when a package helper has made one safe for
285    /// provider projection. Adapters must use a bounded fallback when absent.
286    pub redacted_schema: Option<serde_json::Value>,
287}
288
289impl ProviderToolSpec {
290    /// Builds a provider tool declaration from package and runtime routing data.
291    /// This is projection only and performs no provider call or tool execution.
292    pub fn new(
293        name: impl Into<String>,
294        capability_id: CapabilityId,
295        namespace: CapabilityNamespace,
296        schema_ref: PackageSidecarRef,
297        policy_refs: Vec<PolicyRef>,
298    ) -> Self {
299        Self {
300            name: name.into(),
301            capability_id,
302            namespace,
303            description: None,
304            schema_ref,
305            policy_refs,
306            redacted_schema: None,
307        }
308    }
309
310    /// Returns this declaration with a provider-visible description attached.
311    pub fn with_description(mut self, description: impl Into<String>) -> Self {
312        let description = description.into();
313        if !description.trim().is_empty() {
314            self.description = Some(description);
315        }
316        self
317    }
318
319    /// Returns this declaration with an inline redacted schema attached.
320    /// The caller is responsible for supplying only provider-safe schema data.
321    pub fn with_redacted_schema(mut self, schema: serde_json::Value) -> Self {
322        self.redacted_schema = Some(schema);
323        self
324    }
325
326    /// Returns a provider-safe JSON schema body. If no inline schema body is
327    /// present, the fallback is an object schema with SDK schema-ref metadata.
328    pub fn provider_schema(&self) -> serde_json::Value {
329        self.redacted_schema.clone().unwrap_or_else(|| {
330            serde_json::json!({
331                "type": "object",
332                "additionalProperties": true,
333                "x-agent-sdk-schema-ref": self.schema_ref.sidecar_id,
334                "x-agent-sdk-schema-kind": self.schema_ref.kind,
335                "x-agent-sdk-schema-version": self.schema_ref.version,
336                "x-agent-sdk-schema-content-hash": self.schema_ref.content_hash,
337            })
338        })
339    }
340}
341
342impl fmt::Debug for ProviderToolSpec {
343    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
344        formatter
345            .debug_struct("ProviderToolSpec")
346            .field("name", &self.name)
347            .field("capability_id", &self.capability_id)
348            .field("namespace", &self.namespace)
349            .field("description_present", &self.description.is_some())
350            .field("schema_ref", &self.schema_ref)
351            .field("policy_ref_count", &self.policy_refs.len())
352            .field("redacted_schema_present", &self.redacted_schema.is_some())
353            .finish()
354    }
355}
356
357#[derive(Clone, Deserialize, Eq, PartialEq, Serialize)]
358/// Carries provider structured output hint data across a host-port boundary.
359/// Constructing the value does not call the host; the port method that receives it documents any adapter, network, or storage effect.
360pub struct ProviderStructuredOutputHint {
361    /// Stable schema id used for typed lineage, lookup, or dedupe.
362    pub schema_id: OutputSchemaId,
363    /// Wire schema version used for compatibility checks.
364    pub schema_version: SchemaVersion,
365    /// Deterministic schema fingerprint used for stale checks, package
366    /// evidence, or replay comparisons.
367    pub schema_fingerprint: ContentHash,
368    /// Policy for provider-side structured-output hints.
369    /// Hints may guide prompting but cannot replace SDK-owned validation.
370    pub provider_hint_policy: ProviderHintPolicy,
371    /// Typed include schema ref reference. Resolving or executing it is a
372    /// separate policy-gated step.
373    pub include_schema_ref: bool,
374    #[serde(default, skip_serializing_if = "Option::is_none")]
375    /// Optional redacted inline schema body available for provider-native
376    /// structured-output hints. This is only populated when the output
377    /// contract already carries an inline schema safe for provider projection;
378    /// SDK-owned validation remains authoritative.
379    pub redacted_schema: Option<serde_json::Value>,
380}
381
382impl From<&OutputContract> for ProviderStructuredOutputHint {
383    fn from(contract: &OutputContract) -> Self {
384        let redacted_schema = match &contract.schema {
385            OutputSchemaRef::InlineJson {
386                redacted_schema, ..
387            } => Some(redacted_schema.clone()),
388            _ => None,
389        };
390        Self {
391            schema_id: contract.schema_id.clone(),
392            schema_version: contract.schema_version,
393            schema_fingerprint: contract.schema_fingerprint(),
394            provider_hint_policy: contract.projection_hint.provider_hint_policy.clone(),
395            include_schema_ref: contract.projection_hint.include_schema_ref,
396            redacted_schema,
397        }
398    }
399}
400
401impl fmt::Debug for ProviderStructuredOutputHint {
402    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
403        formatter
404            .debug_struct("ProviderStructuredOutputHint")
405            .field("schema_id", &self.schema_id)
406            .field("schema_version", &self.schema_version)
407            .field("schema_fingerprint", &self.schema_fingerprint)
408            .field("provider_hint_policy", &self.provider_hint_policy)
409            .field("include_schema_ref", &self.include_schema_ref)
410            .field("redacted_schema_present", &self.redacted_schema.is_some())
411            .finish()
412    }
413}
414
415#[derive(Clone, Deserialize, Eq, PartialEq, Serialize)]
416/// Carries provider message data across a host-port boundary.
417/// Constructing the value does not call the host; the port method that receives it documents any adapter, network, or storage effect.
418pub struct ProviderMessage {
419    /// Role used by this record or request.
420    pub role: ProviderMessageRole,
421    /// Bounded textual content extracted for caller use; absent for binary
422    /// summaries or denied raw access.
423    pub content: String,
424    /// Privacy class used for projection, telemetry, and raw-content access
425    /// decisions.
426    pub privacy: PrivacyClass,
427    #[serde(skip_serializing_if = "Option::is_none")]
428    /// Optional projected metadata value.
429    /// When absent, callers should use the documented default or skip that optional behavior.
430    pub projected_metadata: Option<ProviderProjectedMetadata>,
431}
432
433impl fmt::Debug for ProviderMessage {
434    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
435        formatter
436            .debug_struct("ProviderMessage")
437            .field("role", &self.role)
438            .field("content", &"<redacted>")
439            .field("content_chars", &self.content.chars().count())
440            .field("privacy", &self.privacy)
441            .field("projected_metadata", &self.projected_metadata)
442            .finish()
443    }
444}
445
446#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
447#[serde(rename_all = "snake_case")]
448/// Enumerates the finite provider message role cases.
449/// Serialized names are part of the SDK contract; update fixtures when variants change.
450pub enum ProviderMessageRole {
451    /// Use this variant when the contract needs to represent system; selecting it has no side effect by itself.
452    System,
453    /// Use this variant when the contract needs to represent developer; selecting it has no side effect by itself.
454    Developer,
455    /// Use this variant when the contract needs to represent user; selecting it has no side effect by itself.
456    User,
457    /// Use this variant when the contract needs to represent assistant; selecting it has no side effect by itself.
458    Assistant,
459    /// Use this variant when the contract needs to represent tool; selecting it has no side effect by itself.
460    Tool,
461    /// Use this variant when the contract needs to represent context; selecting it has no side effect by itself.
462    Context,
463}
464
465#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
466/// Carries provider projected metadata data across a host-port boundary.
467/// Constructing the value does not call the host; the port method that receives it documents any adapter, network, or storage effect.
468pub struct ProviderProjectedMetadata {
469    /// Kind discriminator for source kind.
470    /// Use it to route finite match arms without parsing display text.
471    pub source_kind: SourceKind,
472    /// Stable source id used for typed lineage, lookup, or dedupe.
473    pub source_id: String,
474    /// Kind discriminator for destination kind.
475    /// Use it to route finite match arms without parsing display text.
476    pub destination_kind: DestinationKind,
477    /// Stable destination id used for typed lineage, lookup, or dedupe.
478    pub destination_id: String,
479    /// Kind discriminator for subject kind.
480    /// Use it to route finite match arms without parsing display text.
481    pub subject_kind: String,
482    /// Stable subject id used for typed lineage, lookup, or dedupe.
483    pub subject_id: String,
484}
485
486#[derive(Clone, Deserialize, Eq, PartialEq, Serialize)]
487/// Carries provider response data across a host-port boundary.
488/// Constructing the value does not call the host; the port method that receives it documents any adapter, network, or storage effect.
489pub struct ProviderResponse {
490    /// Wire schema version used for compatibility checks.
491    pub schema_version: u16,
492    /// Output text used by this record or request.
493    pub output_text: String,
494    /// Stop reason used by this record or request.
495    pub stop_reason: ProviderStopReason,
496    #[serde(default, skip_serializing_if = "Vec::is_empty")]
497    /// Provider-requested tool calls. These are model-visible tool call
498    /// requests only; execution still must pass through the SDK tool router,
499    /// policy, journal intent/result, and effect contracts.
500    pub tool_calls: Vec<ProviderToolCall>,
501    #[serde(skip_serializing_if = "Option::is_none")]
502    /// Optional usage value.
503    /// When absent, callers should use the documented default or skip that optional behavior.
504    pub usage: Option<ProviderUsage>,
505}
506
507impl ProviderResponse {
508    /// Constant value for the ports::provider contract. Use it to keep
509    /// SDK records and tests aligned on the same stable value.
510    pub const SCHEMA_VERSION: u16 = 1;
511
512    /// Builds the text value.
513    /// This is data construction and performs no I/O, journal append, event publication, or
514    /// process work.
515    pub fn text(output_text: impl Into<String>) -> Self {
516        Self {
517            schema_version: Self::SCHEMA_VERSION,
518            output_text: output_text.into(),
519            stop_reason: ProviderStopReason::EndTurn,
520            tool_calls: Vec::new(),
521            usage: None,
522        }
523    }
524
525    /// Builds a provider response that requests tool execution through
526    /// canonical SDK tool-call material. This does not execute any tool;
527    /// runtime code must lower these requests through `ToolRouter`,
528    /// policy, journal, and effect contracts before calling an executor.
529    pub fn tool_use(tool_calls: impl IntoIterator<Item = ProviderToolCall>) -> Self {
530        Self {
531            schema_version: Self::SCHEMA_VERSION,
532            output_text: String::new(),
533            stop_reason: ProviderStopReason::ToolUse,
534            tool_calls: tool_calls.into_iter().collect(),
535            usage: None,
536        }
537    }
538
539    /// Returns this value with its usage setting replaced. The method
540    /// follows builder-style data construction and does not execute
541    /// external work.
542    pub fn with_usage(mut self, usage: ProviderUsage) -> Self {
543        self.usage = Some(usage);
544        self
545    }
546}
547
548impl fmt::Debug for ProviderResponse {
549    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
550        formatter
551            .debug_struct("ProviderResponse")
552            .field("schema_version", &self.schema_version)
553            .field("output_text", &"<redacted>")
554            .field("output_text_chars", &self.output_text.chars().count())
555            .field("stop_reason", &self.stop_reason)
556            .field("tool_calls", &self.tool_calls)
557            .field("usage", &self.usage)
558            .finish()
559    }
560}
561
562#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
563#[serde(rename_all = "snake_case")]
564/// Enumerates the finite provider stop reason cases.
565/// Serialized names are part of the SDK contract; update fixtures when variants change.
566pub enum ProviderStopReason {
567    /// Use this variant when the contract needs to represent end turn; selecting it has no side effect by itself.
568    EndTurn,
569    /// Use this variant when the contract needs to represent max tokens; selecting it has no side effect by itself.
570    MaxTokens,
571    /// Use this variant when the contract needs to represent a provider
572    /// request for tool execution. Selecting it does not execute a tool;
573    /// runtime code must route tool calls through SDK policy, journal, and
574    /// effect contracts.
575    ToolUse,
576    /// Use this variant when the contract needs to represent cancelled; selecting it has no side effect by itself.
577    Cancelled,
578    /// Use this variant when the contract needs to represent provider error; selecting it has no side effect by itself.
579    ProviderError,
580    /// Use this variant when the contract needs to represent unknown; selecting it has no side effect by itself.
581    Unknown,
582}
583
584#[derive(Clone, Deserialize, Eq, PartialEq, Serialize)]
585/// Carries provider stream chunk data across a host-port boundary.
586/// Constructing the value does not call the host; the port method that receives it documents any adapter, network, or storage effect.
587pub struct ProviderStreamChunk {
588    /// Wire schema version used for compatibility checks.
589    pub schema_version: u16,
590    /// Chunk index used by this record or request.
591    pub chunk_index: u32,
592    /// Delta used by this record or request.
593    pub delta: ProviderStreamDelta,
594    /// Whether is terminal is enabled.
595    /// Policy, validation, or routing code uses this flag to choose the explicit behavior.
596    pub is_terminal: bool,
597    #[serde(skip_serializing_if = "Option::is_none")]
598    /// Optional usage value.
599    /// When absent, callers should use the documented default or skip that optional behavior.
600    pub usage: Option<ProviderUsage>,
601}
602
603impl ProviderStreamChunk {
604    /// Constant value for the ports::provider contract. Use it to keep
605    /// SDK records and tests aligned on the same stable value.
606    pub const SCHEMA_VERSION: u16 = 1;
607
608    /// Builds the text value.
609    /// This is data construction and performs no I/O, journal append, event publication, or
610    /// process work.
611    pub fn text(chunk_index: u32, text: impl Into<String>) -> Self {
612        Self {
613            schema_version: Self::SCHEMA_VERSION,
614            chunk_index,
615            delta: ProviderStreamDelta::Text {
616                text: text.into(),
617                stop_reason: None,
618            },
619            is_terminal: false,
620            usage: None,
621        }
622    }
623
624    /// Builds the final text value.
625    /// This is data construction and performs no I/O, journal append, event publication, or
626    /// process work.
627    pub fn final_text(
628        text: impl Into<String>,
629        stop_reason: ProviderStopReason,
630        usage: Option<ProviderUsage>,
631    ) -> Self {
632        Self {
633            schema_version: Self::SCHEMA_VERSION,
634            chunk_index: 0,
635            delta: ProviderStreamDelta::Text {
636                text: text.into(),
637                stop_reason: Some(stop_reason),
638            },
639            is_terminal: true,
640            usage,
641        }
642    }
643
644    /// Builds a tool-call delta. This is provider output data only and
645    /// does not route, approve, journal, or execute tools.
646    pub fn tool_calls(
647        chunk_index: u32,
648        tool_calls: impl IntoIterator<Item = ProviderToolCall>,
649    ) -> Self {
650        Self {
651            schema_version: Self::SCHEMA_VERSION,
652            chunk_index,
653            delta: ProviderStreamDelta::ToolCalls {
654                tool_calls: tool_calls.into_iter().collect(),
655                stop_reason: None,
656            },
657            is_terminal: false,
658            usage: None,
659        }
660    }
661
662    /// Builds a terminal tool-call delta. This is provider output data
663    /// only and does not route, approve, journal, or execute tools.
664    pub fn final_tool_calls(
665        chunk_index: u32,
666        tool_calls: impl IntoIterator<Item = ProviderToolCall>,
667        stop_reason: ProviderStopReason,
668        usage: Option<ProviderUsage>,
669    ) -> Self {
670        Self {
671            schema_version: Self::SCHEMA_VERSION,
672            chunk_index,
673            delta: ProviderStreamDelta::ToolCalls {
674                tool_calls: tool_calls.into_iter().collect(),
675                stop_reason: Some(stop_reason),
676            },
677            is_terminal: true,
678            usage,
679        }
680    }
681}
682
683impl fmt::Debug for ProviderStreamChunk {
684    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
685        formatter
686            .debug_struct("ProviderStreamChunk")
687            .field("schema_version", &self.schema_version)
688            .field("chunk_index", &self.chunk_index)
689            .field("delta", &self.delta)
690            .field("is_terminal", &self.is_terminal)
691            .field("usage", &self.usage)
692            .finish()
693    }
694}
695
696#[derive(Clone, Deserialize, Eq, PartialEq, Serialize)]
697#[serde(tag = "kind", rename_all = "snake_case")]
698/// Enumerates the finite provider stream delta cases.
699/// Serialized names are part of the SDK contract; update fixtures when variants change.
700pub enum ProviderStreamDelta {
701    /// Use this variant when the contract needs to represent text; selecting it has no side effect by itself.
702    Text {
703        /// Text used by this record or request.
704        text: String,
705        #[serde(skip_serializing_if = "Option::is_none")]
706        /// Optional stop reason value.
707        /// When absent, callers should use the documented default or skip that optional
708        /// behavior.
709        stop_reason: Option<ProviderStopReason>,
710    },
711    /// Use this variant when the contract needs to represent usage; selecting it has no side effect by itself.
712    Usage {
713        /// Usage used by this record or request.
714        usage: ProviderUsage,
715    },
716    /// Provider requested one or more tool calls. This is a request
717    /// envelope only; runtime code must lower it into `ToolCallRequest`
718    /// values and route through package policy, journal, events, and
719    /// effect records before executing anything.
720    ToolCalls {
721        /// Tool calls requested by the provider output.
722        tool_calls: Vec<ProviderToolCall>,
723        #[serde(skip_serializing_if = "Option::is_none")]
724        /// Optional stop reason value.
725        /// When absent, callers should use the documented default or skip that optional
726        /// behavior.
727        stop_reason: Option<ProviderStopReason>,
728    },
729    /// Provider stream or transport error. The payload must stay
730    /// redacted so live event observers do not require raw content.
731    Error {
732        /// Redacted human-readable summary safe for events, telemetry, and
733        /// logs.
734        redacted_summary: String,
735    },
736}
737
738impl fmt::Debug for ProviderStreamDelta {
739    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
740        match self {
741            Self::Text { text, stop_reason } => formatter
742                .debug_struct("Text")
743                .field("text", &"<redacted>")
744                .field("text_chars", &text.chars().count())
745                .field("stop_reason", stop_reason)
746                .finish(),
747            Self::Usage { usage } => formatter
748                .debug_struct("Usage")
749                .field("usage", usage)
750                .finish(),
751            Self::ToolCalls {
752                tool_calls,
753                stop_reason,
754            } => formatter
755                .debug_struct("ToolCalls")
756                .field("tool_calls", tool_calls)
757                .field("stop_reason", stop_reason)
758                .finish(),
759            Self::Error { redacted_summary } => formatter
760                .debug_struct("Error")
761                .field("redacted_summary", redacted_summary)
762                .finish(),
763        }
764    }
765}
766
767#[derive(Clone, Deserialize, Eq, PartialEq, Serialize)]
768/// Canonical provider-side request for a tool call. Constructing this
769/// value does not execute a tool, resolve arguments, append journals, or
770/// publish events. Runtime code must lower it into the SDK tool router,
771/// policy, journal intent/result, event, and effect contracts.
772pub struct ProviderToolCall {
773    /// Stable tool call id used for typed lineage, lookup, or dedupe.
774    pub tool_call_id: ToolCallId,
775    /// Canonical tool name requested by the provider output.
776    pub canonical_tool_name: CanonicalToolName,
777    #[serde(default, skip_serializing_if = "Vec::is_empty")]
778    /// Typed requested args refs references. Resolving them is separate from
779    /// constructing this record.
780    pub requested_args_refs: Vec<ContentRef>,
781    /// Redacted summary for display, logs, events, or telemetry.
782    /// It should describe the value without exposing raw private content.
783    pub redacted_args_summary: String,
784}
785
786impl ProviderToolCall {
787    /// Creates a provider tool-call DTO. This is data construction only;
788    /// the runtime must perform routing, approval, journaling, and
789    /// execution separately.
790    pub fn new(
791        tool_call_id: ToolCallId,
792        canonical_tool_name: CanonicalToolName,
793        redacted_args_summary: impl Into<String>,
794    ) -> Self {
795        Self {
796            tool_call_id,
797            canonical_tool_name,
798            requested_args_refs: Vec::new(),
799            redacted_args_summary: redacted_args_summary.into(),
800        }
801    }
802
803    /// Returns this value with one requested argument content ref added.
804    /// The ref is metadata only; resolving it is a separate policy-gated
805    /// content operation.
806    pub fn with_args_ref(mut self, args_ref: ContentRef) -> Self {
807        self.requested_args_refs.push(args_ref);
808        self
809    }
810}
811
812impl fmt::Debug for ProviderToolCall {
813    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
814        formatter
815            .debug_struct("ProviderToolCall")
816            .field("tool_call_id", &self.tool_call_id)
817            .field("canonical_tool_name", &self.canonical_tool_name)
818            .field("requested_args_refs", &self.requested_args_refs)
819            .field(
820                "redacted_args_summary_chars",
821                &self.redacted_args_summary.chars().count(),
822            )
823            .finish()
824    }
825}
826
827#[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq, Serialize)]
828/// Carries provider usage data across a host-port boundary.
829/// Constructing the value does not call the host; the port method that receives it documents any adapter, network, or storage effect.
830pub struct ProviderUsage {
831    /// Optional input tokens value.
832    /// When absent, callers should use the documented default or skip that optional behavior.
833    pub input_tokens: Option<u32>,
834    /// Optional output tokens value.
835    /// When absent, callers should use the documented default or skip that optional behavior.
836    pub output_tokens: Option<u32>,
837    /// Optional total tokens value.
838    /// When absent, callers should use the documented default or skip that optional behavior.
839    pub total_tokens: Option<u32>,
840}
841
842#[derive(Clone, Debug)]
843/// Carries provider conformance case data across a host-port boundary.
844/// Constructing the value does not call the host; the port method that receives it documents any adapter, network, or storage effect.
845pub struct ProviderConformanceCase {
846    /// Projection controls for exposing data to a provider or subscriber.
847    /// Use it to keep provider-visible data separate from private SDK state.
848    pub projection: ContextProjection,
849    /// Policy used by this record or request.
850    pub policy: ProviderProjectionPolicy,
851}
852
853impl ProviderConformanceCase {
854    /// Creates a new ports::provider value with explicit
855    /// caller-provided inputs. This constructor is data-only and
856    /// performs no I/O or external side effects.
857    pub fn new(projection: ContextProjection) -> Self {
858        Self {
859            projection,
860            policy: ProviderProjectionPolicy::redacted("policy.provider.redacted"),
861        }
862    }
863
864    /// Builds the assert adapter value.
865    /// This is data construction and performs no I/O, journal append, event publication, or
866    /// process work.
867    pub fn assert_adapter<A: ProviderAdapter>(
868        &self,
869        adapter: &A,
870    ) -> Result<ProviderUsage, AgentError> {
871        let capabilities = adapter.capabilities();
872        if capabilities.provider_ref.is_empty() {
873            return Err(AgentError::new(
874                AgentErrorKind::ProviderFailure,
875                RetryClassification::HostConfigurationNeeded,
876                "provider capabilities must name a provider ref",
877            ));
878        }
879
880        let request = adapter.project_request(&self.projection, &self.policy)?;
881        if request.projection_item_count != self.projection.items.len() {
882            return Err(AgentError::new(
883                AgentErrorKind::ProjectionFailure,
884                RetryClassification::RepairNeeded,
885                "provider request item count must match the context projection",
886            ));
887        }
888
889        let response = adapter.complete(&request)?;
890        Ok(adapter.extract_usage(&response))
891    }
892}