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}