Skip to main content

agent_sdk_core/records/
structured_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 structured output portion of that contract.
5//!
6use serde::{Deserialize, Serialize};
7
8use crate::{
9    domain::{
10        AttemptId, ContentRef, OutputSchemaId, PrivacyClass, RepairAttemptId, ValidationAttemptId,
11    },
12    output::{ContentHash, RepairAdapterRef, SchemaVersion},
13};
14
15/// Constant value for the records::structured_output contract. Use it
16/// to keep SDK records and tests aligned on the same stable value.
17pub const VALIDATION_RECORD_SCHEMA_VERSION: u16 = 1;
18/// Constant value for the records::structured_output contract. Use it
19/// to keep SDK records and tests aligned on the same stable value.
20pub const REPAIR_RECORD_SCHEMA_VERSION: u16 = 1;
21/// Constant value for the records::structured_output contract. Use it
22/// to keep SDK records and tests aligned on the same stable value.
23pub const STRUCTURED_OUTPUT_LIFECYCLE_RECORD_SCHEMA_VERSION: u16 = 1;
24
25#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
26/// Carries the structured output lifecycle record record payload for journal, event, or fixture surfaces.
27/// 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.
28pub struct StructuredOutputLifecycleRecord {
29    /// Wire schema version for this record shape.
30    /// Use it for compatibility checks before deserializing or replaying stored data.
31    pub record_schema_version: u16,
32    /// Kind discriminator for record kind.
33    /// Use it to route finite match arms without parsing display text.
34    pub record_kind: StructuredOutputLifecycleKind,
35    /// Stable schema id used for typed lineage, lookup, or dedupe.
36    pub schema_id: OutputSchemaId,
37    /// Wire schema version for this record shape.
38    /// Use it for compatibility checks before deserializing or replaying stored data.
39    pub output_schema_version: SchemaVersion,
40    /// Deterministic schema fingerprint used for stale checks, package
41    /// evidence, or replay comparisons.
42    pub schema_fingerprint: ContentHash,
43    #[serde(skip_serializing_if = "Option::is_none")]
44    /// Stable source attempt id used for typed lineage, lookup, or dedupe.
45    pub source_attempt_id: Option<AttemptId>,
46    #[serde(skip_serializing_if = "Option::is_none")]
47    /// Content reference for the candidate value being validated.
48    pub candidate_content_ref: Option<ContentRef>,
49    /// Privacy class used for projection, telemetry, and raw-content access
50    /// decisions.
51    pub privacy: PrivacyClass,
52    /// Redacted human-readable summary safe for events, telemetry, and logs.
53    pub redacted_summary: String,
54}
55
56impl StructuredOutputLifecycleRecord {
57    /// Builds the requested record or result value.
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 requested(
61        schema_id: OutputSchemaId,
62        output_schema_version: SchemaVersion,
63        schema_fingerprint: ContentHash,
64    ) -> Self {
65        Self {
66            record_schema_version: STRUCTURED_OUTPUT_LIFECYCLE_RECORD_SCHEMA_VERSION,
67            record_kind: StructuredOutputLifecycleKind::Requested,
68            schema_id,
69            output_schema_version,
70            schema_fingerprint,
71            source_attempt_id: None,
72            candidate_content_ref: None,
73            privacy: PrivacyClass::ContentRefsOnly,
74            redacted_summary: "structured output requested".to_string(),
75        }
76    }
77
78    /// Builds the validation started record or result value.
79    /// This is data-only and does not perform I/O, call host ports, append journals, publish
80    /// events, or start processes.
81    pub fn validation_started(
82        schema_id: OutputSchemaId,
83        output_schema_version: SchemaVersion,
84        schema_fingerprint: ContentHash,
85        source_attempt_id: AttemptId,
86        candidate_content_ref: ContentRef,
87    ) -> Self {
88        Self {
89            record_schema_version: STRUCTURED_OUTPUT_LIFECYCLE_RECORD_SCHEMA_VERSION,
90            record_kind: StructuredOutputLifecycleKind::ValidationStarted,
91            schema_id,
92            output_schema_version,
93            schema_fingerprint,
94            source_attempt_id: Some(source_attempt_id),
95            candidate_content_ref: Some(candidate_content_ref),
96            privacy: PrivacyClass::ContentRefsOnly,
97            redacted_summary: "structured output validation started".to_string(),
98        }
99    }
100}
101
102#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
103#[serde(rename_all = "snake_case")]
104/// Enumerates the finite structured output lifecycle kind cases.
105/// Serialized names are part of the SDK contract; update fixtures when variants change.
106pub enum StructuredOutputLifecycleKind {
107    /// Use this variant when the contract needs to represent requested; selecting it has no side effect by itself.
108    Requested,
109    /// Use this variant when the contract needs to represent validation started; selecting it has no side effect by itself.
110    ValidationStarted,
111}
112
113#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
114/// Carries the validation record record payload for journal, event, or fixture surfaces.
115/// 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.
116pub struct ValidationRecord {
117    /// Wire schema version for this record shape.
118    /// Use it for compatibility checks before deserializing or replaying stored data.
119    pub record_schema_version: u16,
120    /// Kind discriminator for record kind.
121    /// Use it to route finite match arms without parsing display text.
122    pub record_kind: ValidationRecordKind,
123    /// Stable schema id used for typed lineage, lookup, or dedupe.
124    pub schema_id: OutputSchemaId,
125    /// Wire schema version for this record shape.
126    /// Use it for compatibility checks before deserializing or replaying stored data.
127    pub output_schema_version: SchemaVersion,
128    /// Deterministic schema fingerprint used for stale checks, package
129    /// evidence, or replay comparisons.
130    pub schema_fingerprint: ContentHash,
131    /// Stable validation attempt id used for typed lineage, lookup, or
132    /// dedupe.
133    pub validation_attempt_id: ValidationAttemptId,
134    /// Stable source attempt id used for typed lineage, lookup, or dedupe.
135    pub source_attempt_id: AttemptId,
136    /// Content reference for the candidate value being validated.
137    pub candidate_content_ref: ContentRef,
138    /// Privacy class used for projection, telemetry, and raw-content access
139    /// decisions.
140    pub privacy: PrivacyClass,
141    /// Redacted human-readable summary safe for events, telemetry, and logs.
142    pub redacted_summary: String,
143    #[serde(default, skip_serializing_if = "Vec::is_empty")]
144    /// Bounded errors included in this record. Limits and truncation are
145    /// represented by companion metadata when applicable.
146    pub errors: Vec<ValidationErrorSummary>,
147    #[serde(default, skip_serializing_if = "Vec::is_empty")]
148    /// Validation policy applied before output is accepted as typed data.
149    /// It controls validator selection, bounds, failure visibility, and local validation
150    /// behavior.
151    pub validation_attempts: Vec<ValidationAttemptId>,
152    #[serde(default, skip_serializing_if = "Vec::is_empty")]
153    /// Attempt identifier or attempt history for bounded retry/repair.
154    /// Use it to preserve ordering and avoid retry loops that cannot be audited.
155    pub repair_attempts: Vec<RepairAttemptId>,
156    #[serde(default, skip_serializing_if = "Vec::is_empty")]
157    /// Attempt identifier or attempt history for bounded retry/repair.
158    /// Use it to preserve ordering and avoid retry loops that cannot be audited.
159    pub source_attempt_ids: Vec<AttemptId>,
160    #[serde(skip_serializing_if = "Option::is_none")]
161    /// Optional retry exhausted value.
162    /// When absent, callers should use the documented default or skip that optional behavior.
163    pub retry_exhausted: Option<bool>,
164}
165
166#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
167#[serde(rename_all = "snake_case")]
168/// Enumerates the finite validation record kind cases.
169/// Serialized names are part of the SDK contract; update fixtures when variants change.
170pub enum ValidationRecordKind {
171    /// Use this variant when the contract needs to represent validation succeeded; selecting it has no side effect by itself.
172    ValidationSucceeded,
173    /// Use this variant when the contract needs to represent validation failed; selecting it has no side effect by itself.
174    ValidationFailed,
175    /// Use this variant when the contract needs to represent schema rejected; selecting it has no side effect by itself.
176    SchemaRejected,
177    /// Use this variant when the contract needs to represent terminal failure; selecting it has no side effect by itself.
178    TerminalFailure,
179}
180
181#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
182/// Carries the validation error summary record payload for journal, event, or fixture surfaces.
183/// 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.
184pub struct ValidationErrorSummary {
185    /// Code used by this record or request.
186    pub code: ValidationErrorCode,
187    /// Workspace-relative or resource path selected by the request or result.
188    pub path: String,
189    /// Redacted human-readable summary safe for events, telemetry, and logs.
190    pub redacted_summary: String,
191}
192
193impl ValidationErrorSummary {
194    /// Creates a new records::structured_output value with explicit
195    /// caller-provided inputs. This constructor is data-only and
196    /// performs no I/O or external side effects.
197    pub fn new(
198        code: ValidationErrorCode,
199        path: impl Into<String>,
200        redacted_summary: impl Into<String>,
201    ) -> Self {
202        Self {
203            code,
204            path: path.into(),
205            redacted_summary: redacted_summary.into(),
206        }
207    }
208}
209
210#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
211#[serde(rename_all = "snake_case")]
212/// Enumerates the finite validation error code cases.
213/// Serialized names are part of the SDK contract; update fixtures when variants change.
214pub enum ValidationErrorCode {
215    /// Use this variant when the contract needs to represent candidate too large; selecting it has no side effect by itself.
216    CandidateTooLarge,
217    /// Use this variant when the contract needs to represent invalid json; selecting it has no side effect by itself.
218    InvalidJson,
219    /// Use this variant when the contract needs to represent unsupported dialect; selecting it has no side effect by itself.
220    UnsupportedDialect,
221    /// Use this variant when the contract needs to represent unsupported schema ref; selecting it has no side effect by itself.
222    UnsupportedSchemaRef,
223    /// Use this variant when the contract needs to represent schema contract violation; selecting it has no side effect by itself.
224    SchemaContractViolation,
225    /// Use this variant when the contract needs to represent hostile schema; selecting it has no side effect by itself.
226    HostileSchema,
227    /// Use this variant when the contract needs to represent missing required field; selecting it has no side effect by itself.
228    MissingRequiredField,
229    /// Use this variant when the contract needs to represent type mismatch; selecting it has no side effect by itself.
230    TypeMismatch,
231    /// Use this variant when the contract needs to represent additional property denied; selecting it has no side effect by itself.
232    AdditionalPropertyDenied,
233    /// Use this variant when the contract needs to represent enum mismatch; selecting it has no side effect by itself.
234    EnumMismatch,
235    /// Use this variant when the contract needs to represent min length violation; selecting it has no side effect by itself.
236    MinLengthViolation,
237    /// Use this variant when the contract needs to represent max length violation; selecting it has no side effect by itself.
238    MaxLengthViolation,
239    /// Use this variant when the contract needs to represent semantic validator unavailable; selecting it has no side effect by itself.
240    SemanticValidatorUnavailable,
241}
242
243impl ValidationErrorCode {
244    /// Reports whether this value is schema rejection. The check is
245    /// pure and does not mutate SDK or host state.
246    pub(crate) fn is_schema_rejection(&self) -> bool {
247        matches!(
248            self,
249            Self::UnsupportedDialect
250                | Self::UnsupportedSchemaRef
251                | Self::SchemaContractViolation
252                | Self::HostileSchema
253                | Self::SemanticValidatorUnavailable
254        )
255    }
256}
257
258#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
259/// Carries the repair prompt record payload for journal, event, or fixture surfaces.
260/// 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.
261pub struct RepairPrompt {
262    /// Wire schema version for this record shape.
263    /// Use it for compatibility checks before deserializing or replaying stored data.
264    pub record_schema_version: u16,
265    /// Stable repair attempt id used for typed lineage, lookup, or dedupe.
266    pub repair_attempt_id: RepairAttemptId,
267    /// Stable validation attempt id used for typed lineage, lookup, or
268    /// dedupe.
269    pub validation_attempt_id: ValidationAttemptId,
270    /// Stable source attempt id used for typed lineage, lookup, or dedupe.
271    pub source_attempt_id: AttemptId,
272    /// Stable schema id used for typed lineage, lookup, or dedupe.
273    pub schema_id: OutputSchemaId,
274    /// Wire schema version for this record shape.
275    /// Use it for compatibility checks before deserializing or replaying stored data.
276    pub output_schema_version: SchemaVersion,
277    /// Deterministic schema fingerprint used for stale checks, package
278    /// evidence, or replay comparisons.
279    pub schema_fingerprint: ContentHash,
280    /// Typed repair adapter ref reference. Resolving or executing it is a
281    /// separate policy-gated step.
282    pub repair_adapter_ref: RepairAdapterRef,
283    /// Attempt identifier or attempt history for bounded retry/repair.
284    /// Use it to preserve ordering and avoid retry loops that cannot be audited.
285    pub attempt_index: u8,
286    /// Attempt identifier or attempt history for bounded retry/repair.
287    /// Use it to preserve ordering and avoid retry loops that cannot be audited.
288    pub max_repair_attempts: u8,
289    /// Whether include schema in prompt is enabled.
290    /// Policy, validation, or routing code uses this flag to choose the explicit behavior.
291    pub include_schema_in_prompt: bool,
292    /// Collection of redacted errors values.
293    /// Ordering and membership should be treated as part of the serialized contract when
294    /// relevant.
295    pub redacted_errors: Vec<ValidationErrorSummary>,
296    /// Candidate content used by this record or request.
297    pub candidate_content: RepairPromptCandidateContent,
298    /// Redacted summary for display, logs, events, or telemetry.
299    /// It should describe the value without exposing raw private content.
300    pub prompt_summary: String,
301}
302
303#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
304#[serde(tag = "type", rename_all = "snake_case")]
305/// Enumerates the finite repair prompt candidate content cases.
306/// Serialized names are part of the SDK contract; update fixtures when variants change.
307pub enum RepairPromptCandidateContent {
308    /// Use this variant when the contract needs to represent content ref only; selecting it has no side effect by itself.
309    ContentRefOnly {
310        /// Content reference for the candidate value being validated.
311        candidate_content_ref: ContentRef,
312    },
313    /// Use this variant when the contract needs to represent redacted candidate; selecting it has no side effect by itself.
314    RedactedCandidate {
315        /// Redacted human-readable summary safe for events, telemetry, and
316        /// logs.
317        redacted_summary: String,
318    },
319    /// Use this variant when the contract needs to represent omitted; selecting it has no side effect by itself.
320    Omitted,
321}
322
323#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
324/// Carries the repair record record payload for journal, event, or fixture surfaces.
325/// 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.
326pub struct RepairRecord {
327    /// Wire schema version for this record shape.
328    /// Use it for compatibility checks before deserializing or replaying stored data.
329    pub record_schema_version: u16,
330    /// Kind discriminator for record kind.
331    /// Use it to route finite match arms without parsing display text.
332    pub record_kind: RepairRecordKind,
333    /// Stable repair attempt id used for typed lineage, lookup, or dedupe.
334    pub repair_attempt_id: RepairAttemptId,
335    /// Stable validation attempt id used for typed lineage, lookup, or
336    /// dedupe.
337    pub validation_attempt_id: ValidationAttemptId,
338    /// Stable source attempt id used for typed lineage, lookup, or dedupe.
339    pub source_attempt_id: AttemptId,
340    /// Stable schema id used for typed lineage, lookup, or dedupe.
341    pub schema_id: OutputSchemaId,
342    /// Wire schema version for this record shape.
343    /// Use it for compatibility checks before deserializing or replaying stored data.
344    pub output_schema_version: SchemaVersion,
345    /// Deterministic schema fingerprint used for stale checks, package
346    /// evidence, or replay comparisons.
347    pub schema_fingerprint: ContentHash,
348    /// Typed repair adapter ref reference. Resolving or executing it is a
349    /// separate policy-gated step.
350    pub repair_adapter_ref: RepairAdapterRef,
351    /// Attempt identifier or attempt history for bounded retry/repair.
352    /// Use it to preserve ordering and avoid retry loops that cannot be audited.
353    pub attempt_index: u8,
354    /// Attempt identifier or attempt history for bounded retry/repair.
355    /// Use it to preserve ordering and avoid retry loops that cannot be audited.
356    pub max_repair_attempts: u8,
357    /// Prompt used by this record or request.
358    pub prompt: RepairPrompt,
359    /// Redacted human-readable summary safe for events, telemetry, and logs.
360    pub redacted_summary: String,
361    /// Privacy class used for projection, telemetry, and raw-content access
362    /// decisions.
363    pub privacy: PrivacyClass,
364}
365
366#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
367/// Carries the repair exhaustion record record payload for journal, event, or fixture surfaces.
368/// 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.
369pub struct RepairExhaustionRecord {
370    /// Wire schema version for this record shape.
371    /// Use it for compatibility checks before deserializing or replaying stored data.
372    pub record_schema_version: u16,
373    /// Kind discriminator for record kind.
374    /// Use it to route finite match arms without parsing display text.
375    pub record_kind: RepairRecordKind,
376    /// Stable schema id used for typed lineage, lookup, or dedupe.
377    pub schema_id: OutputSchemaId,
378    /// Wire schema version for this record shape.
379    /// Use it for compatibility checks before deserializing or replaying stored data.
380    pub output_schema_version: SchemaVersion,
381    /// Validation policy applied before output is accepted as typed data.
382    /// It controls validator selection, bounds, failure visibility, and local validation
383    /// behavior.
384    pub validation_attempts: Vec<ValidationAttemptId>,
385    /// Attempt identifier or attempt history for bounded retry/repair.
386    /// Use it to preserve ordering and avoid retry loops that cannot be audited.
387    pub repair_attempts: Vec<RepairAttemptId>,
388    /// Attempt identifier or attempt history for bounded retry/repair.
389    /// Use it to preserve ordering and avoid retry loops that cannot be audited.
390    pub source_attempt_ids: Vec<AttemptId>,
391    /// Content reference for the candidate value being validated.
392    pub candidate_content_ref: ContentRef,
393    /// Whether retry exhausted is enabled.
394    /// Policy, validation, or routing code uses this flag to choose the explicit behavior.
395    pub retry_exhausted: bool,
396    /// Redacted human-readable summary safe for events, telemetry, and logs.
397    pub redacted_summary: String,
398    /// Redacted explanation for a denial, failure, status, or package delta.
399    pub reason: String,
400    /// Privacy class used for projection, telemetry, and raw-content access
401    /// decisions.
402    pub privacy: PrivacyClass,
403}
404
405#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
406#[serde(rename_all = "snake_case")]
407/// Enumerates the finite repair record kind cases.
408/// Serialized names are part of the SDK contract; update fixtures when variants change.
409pub enum RepairRecordKind {
410    /// Use this variant when the contract needs to represent repair requested; selecting it has no side effect by itself.
411    RepairRequested,
412    /// Use this variant when the contract needs to represent repair exhausted; selecting it has no side effect by itself.
413    RepairExhausted,
414}