agent_sdk_core/records/output.rs
1//! Durable and observable SDK records. Use these DTOs for events, journals, effects,
2//! context, output, and feature evidence. Constructing records is data-only;
3//! persistence, publication, and external actions happen through ports or application
4//! coordinators. This file contains the output portion of that contract.
5//!
6use core::fmt;
7use std::collections::BTreeMap;
8
9use serde::{Deserialize, Deserializer, Serialize, de::Error as DeError};
10use serde_json::{Map, Value};
11use sha2::{Digest, Sha256};
12
13use crate::{
14 domain::{AgentError, ContentRef, IdValidationError, OutputSchemaId, PolicyKind, PolicyRef},
15 ids::validate_identifier,
16 policy::ContentCapturePolicy,
17 typed_output_ports::TypedOutputModel,
18};
19
20#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
21/// Carries the output contract record payload for journal, event, or fixture surfaces.
22/// Creating or cloning it only preserves serialized SDK state; append, publish, replay, or export effects are documented on the runtime and port methods that store it.
23pub struct OutputContract {
24 /// Stable schema id used for typed lineage, lookup, or dedupe.
25 pub schema_id: OutputSchemaId,
26 /// Wire schema version used for compatibility checks.
27 pub schema_version: SchemaVersion,
28 /// Schema dialect used to interpret the output schema.
29 /// Validators use it to select the supported JSON-schema subset and compatibility rules.
30 pub dialect: OutputSchemaDialect,
31 /// Schema reference or inline schema used to validate structured output.
32 /// The runtime resolves this before treating model output as typed data.
33 pub schema: OutputSchemaRef,
34 /// Mode that selects how this operation or contract should behave.
35 /// Callers use it to choose the explicit execution path instead of relying on hidden
36 /// defaults.
37 pub mode: OutputMode,
38 /// Validation policy applied before output is accepted as typed data.
39 /// It controls validator selection, bounds, failure visibility, and local validation
40 /// behavior.
41 pub validation: ValidationPolicy,
42 /// Repair policy used after structured output validation fails.
43 /// It controls whether repair is attempted and which policy gates must approve it.
44 pub repair: RepairPolicy,
45 /// Retry budget for validation, repair, or adapter attempts.
46 /// Runtimes use it to stop bounded loops deterministically.
47 pub retry_budget: RetryBudget,
48 /// Content-capture policy that governs raw content, summaries, redaction, and retention.
49 /// Projection, telemetry, and delivery paths must honor it before exposing content.
50 pub content_policy: ContentCapturePolicy,
51 /// Provider-facing projection hint for structured output requests.
52 /// It can guide model prompting but does not replace local validation policy.
53 pub projection_hint: OutputProjectionHint,
54}
55
56impl OutputContract {
57 /// Builds the for type value with the documented defaults.
58 /// This is data-only and does not perform I/O, call host ports, append journals, publish
59 /// events, or start processes.
60 pub fn for_type<T: TypedOutputModel>() -> Self {
61 Self::from_typed_model::<T>(OutputPreset::StrictJsonSchema)
62 }
63
64 /// Returns an updated value with strict json schema configured.
65 /// This is data-only and does not perform I/O, call host ports, append journals, publish
66 /// events, or start processes.
67 pub fn strict_json_schema<T: TypedOutputModel>() -> Self {
68 Self::from_typed_model::<T>(OutputPreset::StrictJsonSchema)
69 }
70
71 /// Builds the fast lenient value.
72 /// This is data construction and performs no I/O, journal append, event publication, or
73 /// process work.
74 pub fn fast_lenient<T: TypedOutputModel>() -> Self {
75 Self::from_typed_model::<T>(OutputPreset::FastLenient)
76 }
77
78 /// Builds the provider assisted value.
79 /// This is data construction and performs no I/O, journal append, event publication, or
80 /// process work.
81 pub fn provider_assisted<T: TypedOutputModel>() -> Self {
82 Self::from_typed_model::<T>(OutputPreset::ProviderAssisted)
83 }
84
85 /// Builds the inline json schema value.
86 /// This is data construction and performs no I/O, journal append, event publication, or
87 /// process work.
88 pub fn inline_json_schema(
89 schema_id: OutputSchemaId,
90 schema_version: SchemaVersion,
91 redacted_schema: Value,
92 ) -> Self {
93 let content_hash = canonical_content_hash(&redacted_schema);
94 Self::new(
95 schema_id,
96 schema_version,
97 OutputSchemaDialect::JsonSchema2020_12Subset,
98 OutputSchemaRef::InlineJson {
99 content_hash,
100 redacted_schema,
101 },
102 OutputPreset::StrictJsonSchema,
103 )
104 }
105
106 /// Creates a new records::output value with explicit
107 /// caller-provided inputs. This constructor is data-only and
108 /// performs no I/O or external side effects.
109 pub fn new(
110 schema_id: OutputSchemaId,
111 schema_version: SchemaVersion,
112 dialect: OutputSchemaDialect,
113 schema: OutputSchemaRef,
114 preset: OutputPreset,
115 ) -> Self {
116 let mut validation = ValidationPolicy::strict_defaults();
117 let mut repair = RepairPolicy::safe_defaults();
118 let mut retry_budget = RetryBudget::attempts(2);
119 let mut projection_hint = OutputProjectionHint::schema_ref_only();
120
121 match preset {
122 OutputPreset::StrictJsonSchema => {}
123 OutputPreset::FastLenient => {
124 validation.allow_additional_properties = true;
125 validation.max_errors_returned = 8;
126 repair.max_repair_attempts = 1;
127 retry_budget = RetryBudget::attempts(1);
128 projection_hint.provider_hint_policy = ProviderHintPolicy::SchemaOptional;
129 }
130 OutputPreset::ProviderAssisted => {
131 projection_hint.provider_hint_policy = ProviderHintPolicy::ProviderNativeHint;
132 }
133 }
134
135 Self {
136 schema_id,
137 schema_version,
138 dialect,
139 schema,
140 mode: OutputMode::FinalOnly,
141 validation,
142 repair,
143 retry_budget,
144 content_policy: ContentCapturePolicy::safe_defaults(PolicyRef::with_kind(
145 PolicyKind::Privacy,
146 "policy.output.content_capture.safe_defaults",
147 )),
148 projection_hint,
149 }
150 }
151
152 /// Computes the stable schema fingerprint for this records::output
153 /// value. The computation is deterministic and side-effect free so
154 /// it can be used in package, journal, or test evidence.
155 pub fn schema_fingerprint(&self) -> ContentHash {
156 self.schema.content_hash()
157 }
158
159 /// Validates the records::output invariants and returns a typed
160 /// error on failure. Validation is pure and does not perform I/O,
161 /// dispatch, journal appends, or adapter calls.
162 pub fn validate_shape(&self) -> Result<(), AgentError> {
163 if self.retry_budget.max_attempts < self.repair.max_repair_attempts {
164 return Err(AgentError::contract_violation(
165 "output retry budget must cover repair attempts",
166 ));
167 }
168 if self.validation.max_candidate_bytes == 0 {
169 return Err(AgentError::missing_required_field(
170 "output.validation.max_candidate_bytes",
171 ));
172 }
173 self.schema.validate_shape()
174 }
175
176 fn from_typed_model<T: TypedOutputModel>(preset: OutputPreset) -> Self {
177 Self::new(
178 OutputSchemaId::new(T::SCHEMA_ID),
179 T::SCHEMA_VERSION,
180 T::DIALECT,
181 T::schema_ref(),
182 preset,
183 )
184 }
185}
186
187#[derive(Clone, Copy, Debug, Deserialize, Eq, Ord, PartialEq, PartialOrd, Serialize)]
188/// Carries the schema version record payload for journal, event, or fixture surfaces.
189/// Creating or cloning it only preserves serialized SDK state; append, publish, replay, or export effects are documented on the runtime and port methods that store it.
190pub struct SchemaVersion {
191 /// Major used by this record or request.
192 pub major: u16,
193 /// Minor used by this record or request.
194 pub minor: u16,
195 /// Patch used by this record or request.
196 pub patch: u16,
197}
198
199impl SchemaVersion {
200 /// Constant value for the records::output contract. Use it to keep
201 /// SDK records and tests aligned on the same stable value.
202 pub const fn new(major: u16, minor: u16, patch: u16) -> Self {
203 Self {
204 major,
205 minor,
206 patch,
207 }
208 }
209}
210
211#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
212#[serde(rename_all = "snake_case")]
213/// Enumerates the finite output schema dialect cases.
214/// Serialized names are part of the SDK contract; update fixtures when variants change.
215pub enum OutputSchemaDialect {
216 /// Use this variant when the contract needs to represent json schema2020 12 subset; selecting it has no side effect by itself.
217 JsonSchema2020_12Subset,
218 /// Use this variant when the contract needs to represent rust serde type name; selecting it has no side effect by itself.
219 RustSerdeTypeName,
220 /// Use this variant when the contract needs to represent host registered; selecting it has no side effect by itself.
221 HostRegistered,
222}
223
224#[derive(Clone, Debug, Deserialize, PartialEq, Serialize)]
225#[serde(tag = "type", rename_all = "snake_case")]
226/// Enumerates the finite output schema ref cases.
227/// Serialized names are part of the SDK contract; update fixtures when variants change.
228pub enum OutputSchemaRef {
229 /// Use this variant when the contract needs to represent inline json; selecting it has no side effect by itself.
230 InlineJson {
231 /// Stable hash for the bytes or canonical payload used for stale
232 /// checks and fingerprints.
233 content_hash: ContentHash,
234 /// Schema body safe to expose after redaction.
235 /// It can be logged or shown without revealing private schema content beyond policy.
236 redacted_schema: Value,
237 },
238 /// Use this variant when the contract needs to represent content store; selecting it has no side effect by itself.
239 ContentStore {
240 /// Content reference where payload bytes or structured tool output
241 /// are stored.
242 content_ref: ContentRef,
243 /// Stable hash for the bytes or canonical payload used for stale
244 /// checks and fingerprints.
245 content_hash: ContentHash,
246 },
247 /// Use this variant when the contract needs to represent rust serde; selecting it has no side effect by itself.
248 RustSerde {
249 /// Type name used by this record or request.
250 type_name: TypeName,
251 /// Crate name used by this record or request.
252 crate_name: CrateName,
253 /// Version string for this capability, package, or protocol surface.
254 /// Use it for compatibility checks during package or adapter resolution.
255 crate_version: String,
256 /// Typed generated schema ref reference. Resolving or executing it is
257 /// a separate policy-gated step.
258 generated_schema_ref: ContentRef,
259 /// Stable hash for the bytes or canonical payload used for stale
260 /// checks and fingerprints.
261 content_hash: ContentHash,
262 },
263 /// Use this variant when the contract needs to represent host registered; selecting it has no side effect by itself.
264 HostRegistered {
265 /// Typed validator ref reference. Resolving or executing it is a
266 /// separate policy-gated step.
267 validator_ref: OutputValidatorRef,
268 /// Version string for this capability, package, or protocol surface.
269 /// Use it for compatibility checks during package or adapter resolution.
270 contract_version: SchemaVersion,
271 /// Stable hash for the bytes or canonical payload used for stale
272 /// checks and fingerprints.
273 content_hash: ContentHash,
274 },
275}
276
277impl Eq for OutputSchemaRef {}
278
279impl OutputSchemaRef {
280 /// Computes the stable content hash for this records::output value.
281 /// The computation is deterministic and side-effect free so it can
282 /// be used in package, journal, or test evidence.
283 pub fn content_hash(&self) -> ContentHash {
284 match self {
285 Self::InlineJson { content_hash, .. }
286 | Self::ContentStore { content_hash, .. }
287 | Self::RustSerde { content_hash, .. }
288 | Self::HostRegistered { content_hash, .. } => content_hash.clone(),
289 }
290 }
291
292 /// Validates the records::output invariants and returns a typed
293 /// error on failure. Validation is pure and does not perform I/O,
294 /// dispatch, journal appends, or adapter calls.
295 pub fn validate_shape(&self) -> Result<(), AgentError> {
296 match self {
297 Self::InlineJson {
298 content_hash,
299 redacted_schema,
300 } => {
301 content_hash.validate_shape()?;
302 if redacted_schema.is_null() {
303 return Err(AgentError::missing_required_field(
304 "output.schema.redacted_schema",
305 ));
306 }
307 }
308 Self::ContentStore { content_hash, .. }
309 | Self::RustSerde { content_hash, .. }
310 | Self::HostRegistered { content_hash, .. } => content_hash.validate_shape()?,
311 }
312 Ok(())
313 }
314}
315
316#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
317#[serde(rename_all = "snake_case")]
318/// Enumerates the finite output mode cases.
319/// Serialized names are part of the SDK contract; update fixtures when variants change.
320pub enum OutputMode {
321 /// Use this variant when the contract needs to represent final only; selecting it has no side effect by itself.
322 FinalOnly,
323 /// Use this variant when the contract needs to represent incremental preview; selecting it has no side effect by itself.
324 IncrementalPreview,
325}
326
327#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
328/// Carries the validation policy record payload for journal, event, or fixture surfaces.
329/// Creating or cloning it only preserves serialized SDK state; append, publish, replay, or export effects are documented on the runtime and port methods that store it.
330pub struct ValidationPolicy {
331 /// Typed validator ref reference. Resolving or executing it is a separate
332 /// policy-gated step.
333 pub validator_ref: OutputValidatorRef,
334 /// max candidate bytes used for bounds checks, summaries, or truncation
335 /// evidence.
336 pub max_candidate_bytes: u64,
337 /// Maximum number of validation errors to expose in a report.
338 /// Use it to keep failure output bounded and safe for logs or events.
339 pub max_errors_returned: u16,
340 /// Whether JSON-schema validation permits properties not declared by the schema.
341 /// Strict SDK output contracts should usually keep this false for deterministic
342 /// typed-output validation.
343 pub allow_additional_properties: bool,
344 #[serde(default, skip_serializing_if = "Vec::is_empty")]
345 /// Additional semantic validator refs to run after schema validation.
346 /// Each validator still resolves through policy-gated validator infrastructure.
347 pub semantic_validators: Vec<SemanticValidatorRef>,
348 /// Timeout budget in milliseconds for the requested operation.
349 pub timeout_ms: u64,
350 /// Visibility policy for validation failure details.
351 /// It controls how much error detail can appear in reports, events, and repair prompts.
352 pub failure_visibility: ValidationFailureVisibility,
353}
354
355impl ValidationPolicy {
356 /// Returns an updated value with strict defaults configured.
357 /// This is data-only and does not perform I/O, call host ports, append journals, publish
358 /// events, or start processes.
359 pub fn strict_defaults() -> Self {
360 Self {
361 validator_ref: OutputValidatorRef::new("validator.output.json_schema.local.v1"),
362 max_candidate_bytes: 32 * 1024,
363 max_errors_returned: 32,
364 allow_additional_properties: false,
365 semantic_validators: Vec::new(),
366 timeout_ms: 10_000,
367 failure_visibility: ValidationFailureVisibility::RedactedSummary,
368 }
369 }
370
371 /// Returns validator ref policy for callers that need to inspect the contract state.
372 /// This is data-only and does not perform I/O, call host ports, append journals, publish
373 /// events, or start processes.
374 pub fn validator_ref_policy(&self) -> PolicyRef {
375 PolicyRef::with_kind(PolicyKind::RuntimePackage, self.validator_ref.as_str())
376 }
377}
378
379#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
380#[serde(rename_all = "snake_case")]
381/// Enumerates the finite validation failure visibility cases.
382/// Serialized names are part of the SDK contract; update fixtures when variants change.
383pub enum ValidationFailureVisibility {
384 /// Use this variant when the contract needs to represent redacted summary; selecting it has no side effect by itself.
385 RedactedSummary,
386 /// Only stable validation error codes should be exposed. Selecting
387 /// this variant is data-only and does not publish or persist errors.
388 ErrorCodesOnly,
389}
390
391#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
392/// Carries the repair policy record payload for journal, event, or fixture surfaces.
393/// Creating or cloning it only preserves serialized SDK state; append, publish, replay, or export effects are documented on the runtime and port methods that store it.
394pub struct RepairPolicy {
395 /// Typed repair adapter ref reference. Resolving or executing it is a
396 /// separate policy-gated step.
397 pub repair_adapter_ref: RepairAdapterRef,
398 /// Attempt identifier or attempt history for bounded retry/repair.
399 /// Use it to preserve ordering and avoid retry loops that cannot be audited.
400 pub max_repair_attempts: u8,
401 /// Whether include schema in prompt is enabled.
402 /// Policy, validation, or routing code uses this flag to choose the explicit behavior.
403 pub include_schema_in_prompt: bool,
404 /// Whether include redacted errors is enabled.
405 /// Policy, validation, or routing code uses this flag to choose the explicit behavior.
406 pub include_redacted_errors: bool,
407 /// Whether include candidate content is enabled.
408 /// Policy, validation, or routing code uses this flag to choose the explicit behavior.
409 pub include_candidate_content: CandidateContentRepairPolicy,
410 /// Backoff used by this record or request.
411 pub backoff: RetryBackoff,
412 /// On exhausted used by this record or request.
413 pub on_exhausted: RepairExhaustedBehavior,
414}
415
416impl RepairPolicy {
417 /// Returns an updated value with safe defaults configured.
418 /// This is data-only and does not perform I/O, call host ports, append journals, publish
419 /// events, or start processes.
420 pub fn safe_defaults() -> Self {
421 Self {
422 repair_adapter_ref: RepairAdapterRef::new("repair.output.provider_redacted.v1"),
423 max_repair_attempts: 2,
424 include_schema_in_prompt: true,
425 include_redacted_errors: true,
426 include_candidate_content: CandidateContentRepairPolicy::ContentRefOnly,
427 backoff: RetryBackoff::None,
428 on_exhausted: RepairExhaustedBehavior::FailRun,
429 }
430 }
431
432 /// Returns repair adapter ref policy for callers that need to inspect the contract state.
433 /// This is data-only and does not perform I/O, call host ports, append journals, publish
434 /// events, or start processes.
435 pub fn repair_adapter_ref_policy(&self) -> PolicyRef {
436 PolicyRef::with_kind(PolicyKind::RuntimePackage, self.repair_adapter_ref.as_str())
437 }
438}
439
440#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
441/// Carries the retry budget record payload for journal, event, or fixture surfaces.
442/// Creating or cloning it only preserves serialized SDK state; append, publish, replay, or export effects are documented on the runtime and port methods that store it.
443pub struct RetryBudget {
444 /// Attempt identifier or attempt history for bounded retry/repair.
445 /// Use it to preserve ordering and avoid retry loops that cannot be audited.
446 pub max_attempts: u8,
447}
448
449impl RetryBudget {
450 /// Builds the attempts value.
451 /// This is data construction and performs no I/O, journal append, event publication, or
452 /// process work.
453 pub fn attempts(max_attempts: u8) -> Self {
454 Self { max_attempts }
455 }
456}
457
458#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
459#[serde(rename_all = "snake_case")]
460/// Enumerates the finite candidate content repair policy cases.
461/// Serialized names are part of the SDK contract; update fixtures when variants change.
462pub enum CandidateContentRepairPolicy {
463 /// Use this variant when the contract needs to represent content ref only; selecting it has no side effect by itself.
464 ContentRefOnly,
465 /// Use this variant when the contract needs to represent redacted candidate; selecting it has no side effect by itself.
466 RedactedCandidate,
467 /// Use this variant when the contract needs to represent omit candidate; selecting it has no side effect by itself.
468 OmitCandidate,
469}
470
471#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
472#[serde(rename_all = "snake_case")]
473/// Enumerates the finite retry backoff cases.
474/// Serialized names are part of the SDK contract; update fixtures when variants change.
475pub enum RetryBackoff {
476 /// Use this variant when the contract needs to represent none; selecting it has no side effect by itself.
477 None,
478 /// Use this variant when the contract needs to represent linear; selecting it has no side effect by itself.
479 Linear,
480}
481
482#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
483#[serde(rename_all = "snake_case")]
484/// Enumerates the finite repair exhausted behavior cases.
485/// Serialized names are part of the SDK contract; update fixtures when variants change.
486pub enum RepairExhaustedBehavior {
487 /// Use this variant when the contract needs to represent fail run; selecting it has no side effect by itself.
488 FailRun,
489 /// Use this variant when the contract needs to represent return validation error; selecting it has no side effect by itself.
490 ReturnValidationError,
491}
492
493#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
494/// Carries the output projection hint record payload for journal, event, or fixture surfaces.
495/// Creating or cloning it only preserves serialized SDK state; append, publish, replay, or export effects are documented on the runtime and port methods that store it.
496pub struct OutputProjectionHint {
497 /// Policy for provider-side structured-output hints.
498 /// Hints may guide prompting but cannot replace SDK-owned validation.
499 pub provider_hint_policy: ProviderHintPolicy,
500 /// Typed include schema ref reference. Resolving or executing it is a
501 /// separate policy-gated step.
502 pub include_schema_ref: bool,
503}
504
505impl OutputProjectionHint {
506 /// Returns an updated value with schema ref only configured.
507 /// This is data-only and does not perform I/O, call host ports, append journals, publish
508 /// events, or start processes.
509 pub fn schema_ref_only() -> Self {
510 Self {
511 provider_hint_policy: ProviderHintPolicy::SchemaRequired,
512 include_schema_ref: true,
513 }
514 }
515}
516
517#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
518#[serde(rename_all = "snake_case")]
519/// Enumerates the finite provider hint policy cases.
520/// Serialized names are part of the SDK contract; update fixtures when variants change.
521pub enum ProviderHintPolicy {
522 /// Use this variant when the contract needs to represent schema required; selecting it has no side effect by itself.
523 SchemaRequired,
524 /// Use this variant when the contract needs to represent schema optional; selecting it has no side effect by itself.
525 SchemaOptional,
526 /// Use this variant when the contract needs to represent provider native hint; selecting it has no side effect by itself.
527 ProviderNativeHint,
528}
529
530#[derive(Clone, Copy, Debug, Eq, PartialEq)]
531/// Enumerates the finite output preset cases.
532/// Serialized names are part of the SDK contract; update fixtures when variants change.
533pub enum OutputPreset {
534 /// Use this variant when the contract needs to represent strict json schema; selecting it has no side effect by itself.
535 StrictJsonSchema,
536 /// Use this variant when the contract needs to represent fast lenient; selecting it has no side effect by itself.
537 FastLenient,
538 /// Use this variant when the contract needs to represent provider assisted; selecting it has no side effect by itself.
539 ProviderAssisted,
540}
541
542macro_rules! output_string {
543 ($name:ident) => {
544 #[doc = concat!(
545 "Typed output-string wrapper for `",
546 stringify!($name),
547 "`. Use it where output contracts need stable schema, hash, or validator refs; ",
548 "constructing it is data-only and performs no side effects."
549 )]
550 #[derive(Clone, Eq, Hash, Ord, PartialEq, PartialOrd, Serialize)]
551 #[serde(transparent)]
552 pub struct $name(String);
553
554 impl $name {
555 /// Creates a new records::output value with explicit
556 /// caller-provided inputs. This constructor is data-only
557 /// and performs no I/O or external side effects.
558 ///
559 /// # Panics
560 ///
561 /// Panics if constructor invariants fail, such as invalid identifier
562 /// text or constructor-specific bounds. Use a fallible constructor such as
563 /// `try_new` when one is available for untrusted input.
564 pub fn new(value: impl Into<String>) -> Self {
565 Self::try_new(value).expect(concat!(stringify!($name), " must be valid"))
566 }
567
568 /// Creates a new records::output value after validation.
569 /// Returns an SDK error instead of panicking when the
570 /// identifier or input does not satisfy the contract.
571 pub fn try_new(value: impl Into<String>) -> Result<Self, IdValidationError> {
572 let value = value.into();
573 validate_identifier(&value)?;
574 Ok(Self(value))
575 }
576
577 /// Returns this value as str. The accessor is side-effect
578 /// free and keeps ownership with the caller.
579 pub fn as_str(&self) -> &str {
580 &self.0
581 }
582 }
583
584 impl<'de> Deserialize<'de> for $name {
585 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
586 where
587 D: Deserializer<'de>,
588 {
589 let value = String::deserialize(deserializer)?;
590 Self::try_new(value).map_err(D::Error::custom)
591 }
592 }
593
594 impl fmt::Debug for $name {
595 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
596 formatter.write_str(concat!(stringify!($name), "(redacted)"))
597 }
598 }
599
600 impl fmt::Display for $name {
601 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
602 formatter.write_str(concat!(stringify!($name), "(redacted)"))
603 }
604 }
605 };
606}
607
608output_string!(ContentHash);
609output_string!(OutputValidatorRef);
610output_string!(RepairAdapterRef);
611output_string!(SemanticValidatorRef);
612output_string!(TypeName);
613output_string!(CrateName);
614
615impl ContentHash {
616 fn validate_shape(&self) -> Result<(), AgentError> {
617 let Some(digest) = self.as_str().strip_prefix("sha256:") else {
618 return Err(AgentError::contract_violation(
619 "output schema content_hash must be sha256-prefixed",
620 ));
621 };
622 if digest.len() != 64 || !digest.bytes().all(|byte| byte.is_ascii_hexdigit()) {
623 return Err(AgentError::contract_violation(
624 "output schema content_hash must be a sha256 hex digest",
625 ));
626 }
627 Ok(())
628 }
629}
630
631fn canonical_content_hash(value: &Value) -> ContentHash {
632 let normalized = normalize_json_value(value.clone());
633 let bytes = serde_json::to_vec(&normalized).expect("serde_json::Value serializes");
634 ContentHash::new(format!("sha256:{:x}", Sha256::digest(bytes)))
635}
636
637fn normalize_json_value(value: Value) -> Value {
638 match value {
639 Value::Array(items) => Value::Array(items.into_iter().map(normalize_json_value).collect()),
640 Value::Object(fields) => {
641 let mut sorted = BTreeMap::new();
642 for (key, value) in fields {
643 sorted.insert(key, value);
644 }
645 let mut normalized = Map::new();
646 for (key, value) in sorted {
647 normalized.insert(key, normalize_json_value(value));
648 }
649 Value::Object(normalized)
650 }
651 scalar => scalar,
652 }
653}