Skip to main content

agent_sdk_core/records/
telemetry.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 telemetry portion of that contract.
5//!
6use serde::{Deserialize, Serialize};
7
8use crate::{
9    domain::{
10        AgentId, AttemptId, DedupeKey, DestinationRef, EntityRef, EventId, JournalCursor,
11        PolicyRef, PrivacyClass, RetentionClass, RunId, SourceRef, SpanId, TraceId, TurnId,
12    },
13    event::{EventCursor, EventFamily, EventKind},
14    provider::ProviderUsage,
15};
16
17/// Constant value for the records::telemetry contract. Use it to keep
18/// SDK records and tests aligned on the same stable value.
19pub const TELEMETRY_SCHEMA_VERSION: u16 = 1;
20
21macro_rules! telemetry_id {
22    ($name:ident, $debug:literal) => {
23        #[doc = concat!(
24                            "Typed telemetry identifier for `",
25                            stringify!($name),
26                            "`. Use it for telemetry projection, export, usage, or cost records; ",
27                            "constructing it is data-only and performs no side effects."
28                        )]
29        #[derive(Clone, Deserialize, Eq, Hash, Ord, PartialEq, PartialOrd, Serialize)]
30        #[serde(transparent)]
31        pub struct $name(String);
32
33        impl $name {
34            /// Creates a new records::telemetry value with explicit
35            /// caller-provided inputs. This constructor is data-only
36            /// and performs no I/O or external side effects.
37            pub fn new(value: impl Into<String>) -> Self {
38                Self(value.into())
39            }
40
41            /// Returns this value as str. The accessor is side-effect
42            /// free and keeps ownership with the caller.
43            pub fn as_str(&self) -> &str {
44                &self.0
45            }
46        }
47
48        impl core::fmt::Debug for $name {
49            fn fmt(&self, formatter: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
50                formatter.write_str(concat!($debug, "(redacted)"))
51            }
52        }
53
54        impl core::fmt::Display for $name {
55            fn fmt(&self, formatter: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
56                formatter.write_str(concat!($debug, "(redacted)"))
57            }
58        }
59    };
60}
61
62telemetry_id!(TelemetryProjectionId, "TelemetryProjectionId");
63telemetry_id!(TelemetryRecordId, "TelemetryRecordId");
64telemetry_id!(TelemetrySinkId, "TelemetrySinkId");
65telemetry_id!(TelemetryExportAttemptId, "TelemetryExportAttemptId");
66telemetry_id!(TelemetryUsageRecordId, "TelemetryUsageRecordId");
67telemetry_id!(TelemetryCostRecordId, "TelemetryCostRecordId");
68telemetry_id!(RateTableVersion, "RateTableVersion");
69
70#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
71#[serde(rename_all = "snake_case")]
72/// Enumerates the finite telemetry sink kind cases.
73/// Serialized names are part of the SDK contract; update fixtures when variants change.
74pub enum TelemetrySinkKind {
75    /// Use this variant when the contract needs to represent open telemetry; selecting it has no side effect by itself.
76    OpenTelemetry,
77    /// Use this variant when the contract needs to represent durable trace; selecting it has no side effect by itself.
78    DurableTrace,
79    /// Use this variant when the contract needs to represent local diagnostic; selecting it has no side effect by itself.
80    LocalDiagnostic,
81    /// Use this variant when the contract needs to represent cli summary; selecting it has no side effect by itself.
82    CliSummary,
83    /// Use this variant when the contract needs to represent test; selecting it has no side effect by itself.
84    Test,
85}
86
87#[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq, Serialize)]
88#[serde(rename_all = "snake_case")]
89/// Enumerates the finite telemetry content capture mode cases.
90/// Serialized names are part of the SDK contract; update fixtures when variants change.
91pub enum TelemetryContentCaptureMode {
92    /// Use this variant when the contract needs to represent off; selecting it has no side effect by itself.
93    #[default]
94    Off,
95    /// Use this variant when the contract needs to represent metadata only; selecting it has no side effect by itself.
96    MetadataOnly,
97    /// Use this variant when the contract needs to represent redacted summary; selecting it has no side effect by itself.
98    RedactedSummary,
99    /// Use this variant when the contract needs to represent payload refs; selecting it has no side effect by itself.
100    PayloadRefs,
101    /// Use this variant when the contract needs to represent raw content; selecting it has no side effect by itself.
102    RawContent,
103}
104
105impl TelemetryContentCaptureMode {
106    /// Returns whether captures raw content applies for this contract.
107    /// This is data-only and does not perform I/O, call host ports, append journals, publish
108    /// events, or start processes.
109    pub fn captures_raw_content(&self) -> bool {
110        matches!(self, Self::RawContent)
111    }
112}
113
114#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
115#[serde(rename_all = "snake_case")]
116/// Enumerates the finite telemetry projection kind cases.
117/// Serialized names are part of the SDK contract; update fixtures when variants change.
118pub enum TelemetryProjectionKind {
119    /// Use this variant when the contract needs to represent progress; selecting it has no side effect by itself.
120    Progress,
121    /// Use this variant when the contract needs to represent run terminal; selecting it has no side effect by itself.
122    RunTerminal,
123    /// Use this variant when the contract needs to represent usage; selecting it has no side effect by itself.
124    Usage,
125    /// Use this variant when the contract needs to represent cost estimate; selecting it has no side effect by itself.
126    CostEstimate,
127    /// Use this variant when the contract needs to represent cost correction; selecting it has no side effect by itself.
128    CostCorrection,
129    /// Use this variant when the contract needs to represent sink health; selecting it has no side effect by itself.
130    SinkHealth,
131    /// Use this variant when the contract needs to represent repair cursor; selecting it has no side effect by itself.
132    RepairCursor,
133}
134
135impl TelemetryProjectionKind {
136    /// Reports whether this value is terminal preserved. The check is
137    /// pure and does not mutate SDK or host state.
138    pub fn is_terminal_preserved(&self) -> bool {
139        matches!(
140            self,
141            Self::RunTerminal
142                | Self::Usage
143                | Self::CostEstimate
144                | Self::CostCorrection
145                | Self::SinkHealth
146                | Self::RepairCursor
147        )
148    }
149}
150
151#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
152/// Carries the telemetry projection record payload for journal, event, or fixture surfaces.
153/// 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.
154pub struct TelemetryProjection {
155    /// Wire schema version used for compatibility checks.
156    pub schema_version: u16,
157    /// Stable projection id used for typed lineage, lookup, or dedupe.
158    pub projection_id: TelemetryProjectionId,
159    /// Projection controls for exposing data to a provider or subscriber.
160    /// Use it to keep provider-visible data separate from private SDK state.
161    pub projection_kind: TelemetryProjectionKind,
162    /// Source record used by this record or request.
163    pub source_record: TelemetrySourceRecord,
164    /// Run identifier used for lineage, filtering, replay, and dedupe.
165    pub run_id: RunId,
166    /// Agent identifier used for lineage, filtering, and ownership checks.
167    pub agent_id: AgentId,
168    #[serde(skip_serializing_if = "Option::is_none")]
169    /// Turn identifier for one loop turn within a run.
170    pub turn_id: Option<TurnId>,
171    #[serde(skip_serializing_if = "Option::is_none")]
172    /// Attempt identifier for retry, repair, provider, or tool execution
173    /// evidence.
174    pub attempt_id: Option<AttemptId>,
175    #[serde(skip_serializing_if = "Option::is_none")]
176    /// Event identifier used to correlate live events with journal or replay
177    /// evidence.
178    pub event_id: Option<EventId>,
179    #[serde(skip_serializing_if = "Option::is_none")]
180    /// Cursor identifying a replay, export, or subscription position.
181    /// Use it to resume without widening the original scope.
182    pub journal_cursor: Option<JournalCursor>,
183    #[serde(skip_serializing_if = "Option::is_none")]
184    /// Stable trace id used for typed lineage, lookup, or dedupe.
185    pub trace_id: Option<TraceId>,
186    #[serde(skip_serializing_if = "Option::is_none")]
187    /// Stable span id used for typed lineage, lookup, or dedupe.
188    pub span_id: Option<SpanId>,
189    /// Fingerprint of the runtime package snapshot in force when this value was produced.
190    /// Use it for replay, dedupe, and package-lineage checks; the field is evidence and does
191    /// not execute package behavior.
192    pub runtime_package_fingerprint: String,
193    /// Source label or ref for this item; it is metadata and does not fetch
194    /// content by itself.
195    pub source: SourceRef,
196    #[serde(skip_serializing_if = "Option::is_none")]
197    /// Destination label or ref for this item; it is metadata and does not
198    /// deliver content by itself.
199    pub destination: Option<DestinationRef>,
200    /// Typed subject ref reference. Resolving or executing it is a separate
201    /// policy-gated step.
202    pub subject_ref: EntityRef,
203    #[serde(default, skip_serializing_if = "Vec::is_empty")]
204    /// Policy references that govern admission, projection, execution, or
205    /// delivery.
206    pub policy_refs: Vec<PolicyRef>,
207    /// Privacy class used for projection, telemetry, and raw-content access
208    /// decisions.
209    pub privacy: PrivacyClass,
210    /// Retention class used by hosts and sinks when storing or exporting this
211    /// item.
212    pub retention: RetentionClass,
213    /// Content capture used by this record or request.
214    pub content_capture: TelemetryContentCaptureMode,
215    /// Stable redaction policy id used for typed lineage, lookup, or dedupe.
216    pub redaction_policy_id: String,
217    #[serde(skip_serializing_if = "Option::is_none")]
218    /// Stable provider id used for typed lineage, lookup, or dedupe.
219    pub provider_id: Option<String>,
220    #[serde(skip_serializing_if = "Option::is_none")]
221    /// Stable model id used for typed lineage, lookup, or dedupe.
222    pub model_id: Option<String>,
223    #[serde(skip_serializing_if = "Option::is_none")]
224    /// Optional tool name value.
225    /// When absent, callers should use the documented default or skip that optional behavior.
226    pub tool_name: Option<String>,
227    #[serde(skip_serializing_if = "Option::is_none")]
228    /// Optional usage value.
229    /// When absent, callers should use the documented default or skip that optional behavior.
230    pub usage: Option<UsageUnits>,
231    #[serde(skip_serializing_if = "Option::is_none")]
232    /// Optional cost value.
233    /// When absent, callers should use the documented default or skip that optional behavior.
234    pub cost: Option<CostUnits>,
235    #[serde(skip_serializing_if = "Option::is_none")]
236    /// Optional terminal status value.
237    /// When absent, callers should use the documented default or skip that optional behavior.
238    pub terminal_status: Option<TelemetryTerminalStatus>,
239    #[serde(skip_serializing_if = "Option::is_none")]
240    /// Optional sink health value.
241    /// When absent, callers should use the documented default or skip that optional behavior.
242    pub sink_health: Option<TelemetrySinkHealth>,
243    /// Redacted human-readable summary safe for events, telemetry, and logs.
244    pub redacted_summary: String,
245    #[serde(skip_serializing_if = "Option::is_none")]
246    /// Raw content or raw-content control for this value.
247    /// Use it only when policy explicitly allows raw content capture or delivery.
248    pub raw_content: Option<String>,
249}
250
251impl TelemetryProjection {
252    /// Reports whether this value is terminal preserved. The check is
253    /// pure and does not mutate SDK or host state.
254    pub fn is_terminal_preserved(&self) -> bool {
255        self.projection_kind.is_terminal_preserved()
256    }
257
258    /// Returns an updated value with without raw content configured.
259    /// This is data-only and does not perform I/O, call host ports, append journals, publish
260    /// events, or start processes.
261    pub fn without_raw_content(mut self) -> Self {
262        self.raw_content = None;
263        if self.content_capture.captures_raw_content() {
264            self.content_capture = TelemetryContentCaptureMode::RedactedSummary;
265        }
266        self
267    }
268}
269
270#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
271/// Carries the telemetry source record record payload for journal, event, or fixture surfaces.
272/// 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.
273pub struct TelemetrySourceRecord {
274    /// Event family used by this record or request.
275    pub event_family: EventFamily,
276    /// Kind discriminator for event kind.
277    /// Use it to route finite match arms without parsing display text.
278    pub event_kind: EventKind,
279    #[serde(skip_serializing_if = "Option::is_none")]
280    /// Cursor identifying a replay, export, or subscription position.
281    /// Use it to resume without widening the original scope.
282    pub event_cursor: Option<EventCursor>,
283    #[serde(skip_serializing_if = "Option::is_none")]
284    /// Cursor identifying the source event or journal position.
285    /// Use it to connect projections back to durable evidence.
286    pub source_cursor: Option<TelemetrySourceCursor>,
287}
288
289#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
290#[serde(tag = "type", content = "cursor", rename_all = "snake_case")]
291/// Enumerates the finite telemetry source cursor cases.
292/// Serialized names are part of the SDK contract; update fixtures when variants change.
293pub enum TelemetrySourceCursor {
294    /// Use this variant when the contract needs to represent journal; selecting it has no side effect by itself.
295    Journal(JournalCursor),
296    /// Use this variant when the contract needs to represent event; selecting it has no side effect by itself.
297    Event(EventCursor),
298    /// Use this variant when the contract needs to represent archive; selecting it has no side effect by itself.
299    Archive(String),
300    /// Use this variant when the contract needs to represent usage; selecting it has no side effect by itself.
301    Usage(TelemetryUsageRecordId),
302    /// Use this variant when the contract needs to represent cost; selecting it has no side effect by itself.
303    Cost(TelemetryCostRecordId),
304}
305
306#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
307/// Carries the telemetry export cursor record payload for journal, event, or fixture surfaces.
308/// 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.
309pub struct TelemetryExportCursor {
310    /// Stable sink id used for typed lineage, lookup, or dedupe.
311    pub sink_id: TelemetrySinkId,
312    /// Export seq used by this record or request.
313    pub export_seq: u64,
314    #[serde(skip_serializing_if = "Option::is_none")]
315    /// Optional last acknowledged source value.
316    /// When absent, callers should use the documented default or skip that optional behavior.
317    pub last_acknowledged_source: Option<TelemetrySourceCursor>,
318    #[serde(skip_serializing_if = "Option::is_none")]
319    /// Attempt identifier or attempt history for bounded retry/repair.
320    /// Use it to preserve ordering and avoid retry loops that cannot be audited.
321    pub last_attempted_source: Option<TelemetrySourceCursor>,
322    #[serde(skip_serializing_if = "Option::is_none")]
323    /// Dedupe policy or key for a side-effecting operation.
324    /// Replay and repair use it to avoid sending or executing the same effect twice.
325    pub sink_dedupe_key: Option<DedupeKey>,
326}
327
328impl TelemetryExportCursor {
329    /// Creates a new records::telemetry value with explicit
330    /// caller-provided inputs. This constructor is data-only and
331    /// performs no I/O or external side effects.
332    pub fn new(sink_id: TelemetrySinkId) -> Self {
333        Self {
334            sink_id,
335            export_seq: 0,
336            last_acknowledged_source: None,
337            last_attempted_source: None,
338            sink_dedupe_key: None,
339        }
340    }
341
342    /// Returns an updated value with attempted configured.
343    /// This is data-only and does not perform I/O, call host ports, append journals, publish
344    /// events, or start processes.
345    pub fn attempted(mut self, source: Option<TelemetrySourceCursor>) -> Self {
346        self.last_attempted_source = source;
347        self
348    }
349
350    /// Returns an updated value with acknowledged configured.
351    /// This is data-only and does not perform I/O, call host ports, append journals, publish
352    /// events, or start processes.
353    pub fn acknowledged(mut self, source: Option<TelemetrySourceCursor>) -> Self {
354        self.export_seq += 1;
355        self.last_acknowledged_source = source;
356        self.last_attempted_source = None;
357        self
358    }
359}
360
361#[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq, Serialize)]
362/// Carries the usage units record payload for journal, event, or fixture surfaces.
363/// 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.
364pub struct UsageUnits {
365    #[serde(skip_serializing_if = "Option::is_none")]
366    /// Optional input tokens value.
367    /// When absent, callers should use the documented default or skip that optional behavior.
368    pub input_tokens: Option<u32>,
369    #[serde(skip_serializing_if = "Option::is_none")]
370    /// Optional output tokens value.
371    /// When absent, callers should use the documented default or skip that optional behavior.
372    pub output_tokens: Option<u32>,
373    #[serde(skip_serializing_if = "Option::is_none")]
374    /// Optional total tokens value.
375    /// When absent, callers should use the documented default or skip that optional behavior.
376    pub total_tokens: Option<u32>,
377    #[serde(skip_serializing_if = "Option::is_none")]
378    /// Byte size or byte limit for bytes.
379    /// Use it to enforce bounded reads, writes, summaries, or parser output.
380    pub bytes: Option<u64>,
381    #[serde(skip_serializing_if = "Option::is_none")]
382    /// media duration ms duration in milliseconds.
383    pub media_duration_ms: Option<u64>,
384}
385
386impl From<ProviderUsage> for UsageUnits {
387    fn from(usage: ProviderUsage) -> Self {
388        Self {
389            input_tokens: usage.input_tokens,
390            output_tokens: usage.output_tokens,
391            total_tokens: usage.total_tokens,
392            bytes: None,
393            media_duration_ms: None,
394        }
395    }
396}
397
398#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
399/// Carries the cost units record payload for journal, event, or fixture surfaces.
400/// 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.
401pub struct CostUnits {
402    /// Cost amount expressed in millionths of the currency unit.
403    /// Use micros to keep cost accounting deterministic and integer-based.
404    pub amount_micros: u64,
405    /// Currency code for the cost amount.
406    /// Cost accounting uses it with amount micros and rate-table version.
407    pub currency: String,
408    /// Version of the rate table used for cost estimation.
409    /// This distinguishes estimated cost from provider-reported billing.
410    pub rate_table_version: RateTableVersion,
411    /// Whether cost or usage values are estimated or provider-reported.
412    /// Use it to avoid treating estimates as final billing truth.
413    pub estimate_status: CostEstimateStatus,
414}
415
416#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
417#[serde(rename_all = "snake_case")]
418/// Enumerates the finite cost estimate status cases.
419/// Serialized names are part of the SDK contract; update fixtures when variants change.
420pub enum CostEstimateStatus {
421    /// Use this variant when the contract needs to represent estimated; selecting it has no side effect by itself.
422    Estimated,
423    /// Use this variant when the contract needs to represent provider reported; selecting it has no side effect by itself.
424    ProviderReported,
425    /// Use this variant when the contract needs to represent corrected; selecting it has no side effect by itself.
426    Corrected,
427}
428
429#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
430#[serde(rename_all = "snake_case")]
431/// Enumerates the finite telemetry terminal status cases.
432/// Serialized names are part of the SDK contract; update fixtures when variants change.
433pub enum TelemetryTerminalStatus {
434    /// Use this variant when the contract needs to represent completed; selecting it has no side effect by itself.
435    Completed,
436    /// Use this variant when the contract needs to represent failed; selecting it has no side effect by itself.
437    Failed,
438    /// Use this variant when the contract needs to represent cancelled; selecting it has no side effect by itself.
439    Cancelled,
440    /// Use this variant when the contract needs to represent timed out; selecting it has no side effect by itself.
441    TimedOut,
442    /// Use this variant when the contract needs to represent unknown; selecting it has no side effect by itself.
443    Unknown,
444}
445
446#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
447/// Carries the telemetry sink health record payload for journal, event, or fixture surfaces.
448/// 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.
449pub struct TelemetrySinkHealth {
450    /// Stable sink id used for typed lineage, lookup, or dedupe.
451    pub sink_id: TelemetrySinkId,
452    /// Kind discriminator for sink kind.
453    /// Use it to route finite match arms without parsing display text.
454    pub sink_kind: TelemetrySinkKind,
455    /// State used by this record or request.
456    pub state: TelemetrySinkHealthState,
457    /// Kind discriminator for failure kind.
458    /// Use it to route finite match arms without parsing display text.
459    pub failure_kind: Option<TelemetrySinkFailureKind>,
460    /// Whether terminal preserved is enabled.
461    /// Policy, validation, or routing code uses this flag to choose the explicit behavior.
462    pub terminal_preserved: bool,
463    /// Count of dropped items observed or included in this record.
464    pub dropped_count: u64,
465    #[serde(skip_serializing_if = "Option::is_none")]
466    /// Cursor for telemetry export acknowledgement.
467    /// Sinks use it to ack exactly which projection was exported.
468    pub export_cursor: Option<TelemetryExportCursor>,
469    #[serde(skip_serializing_if = "Option::is_none")]
470    /// Reason a pending side effect is unsafe to retry automatically.
471    /// Recovery uses it to require repair or reconciliation before continuing.
472    pub unsafe_pending_reason: Option<String>,
473}
474
475#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
476#[serde(rename_all = "snake_case")]
477/// Enumerates the finite telemetry sink health state cases.
478/// Serialized names are part of the SDK contract; update fixtures when variants change.
479pub enum TelemetrySinkHealthState {
480    /// Use this variant when the contract needs to represent healthy; selecting it has no side effect by itself.
481    Healthy,
482    /// Use this variant when the contract needs to represent failed; selecting it has no side effect by itself.
483    Failed,
484    /// Use this variant when the contract needs to represent recovered; selecting it has no side effect by itself.
485    Recovered,
486}
487
488#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
489#[serde(rename_all = "snake_case")]
490/// Enumerates the finite telemetry sink failure kind cases.
491/// Serialized names are part of the SDK contract; update fixtures when variants change.
492pub enum TelemetrySinkFailureKind {
493    /// Use this variant when the contract needs to represent overflow; selecting it has no side effect by itself.
494    Overflow,
495    /// Use this variant when the contract needs to represent export rejected; selecting it has no side effect by itself.
496    ExportRejected,
497    /// Use this variant when the contract needs to represent serialization; selecting it has no side effect by itself.
498    Serialization,
499    /// Use this variant when the contract needs to represent schema mismatch; selecting it has no side effect by itself.
500    SchemaMismatch,
501    /// Use this variant when the contract needs to represent sink unavailable; selecting it has no side effect by itself.
502    SinkUnavailable,
503}
504
505#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
506/// Carries the telemetry record record payload for journal, event, or fixture surfaces.
507/// 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.
508pub struct TelemetryRecord {
509    /// Wire schema version used for compatibility checks.
510    pub schema_version: u16,
511    /// Stable record id used for typed lineage, lookup, or dedupe.
512    pub record_id: TelemetryRecordId,
513    /// Run identifier used for lineage, filtering, replay, and dedupe.
514    pub run_id: RunId,
515    /// Agent identifier used for lineage, filtering, and ownership checks.
516    pub agent_id: AgentId,
517    #[serde(skip_serializing_if = "Option::is_none")]
518    /// Cursor identifying the source event or journal position.
519    /// Use it to connect projections back to durable evidence.
520    pub source_cursor: Option<TelemetrySourceCursor>,
521    /// Fingerprint of the runtime package snapshot in force when this value was produced.
522    /// Use it for replay, dedupe, and package-lineage checks; the field is evidence and does
523    /// not execute package behavior.
524    pub runtime_package_fingerprint: String,
525    /// Privacy class used for projection, telemetry, and raw-content access
526    /// decisions.
527    pub privacy: PrivacyClass,
528    /// Retention class used by hosts and sinks when storing or exporting this
529    /// item.
530    pub retention: RetentionClass,
531    /// Content capture used by this record or request.
532    pub content_capture: TelemetryContentCaptureMode,
533    /// Stable redaction policy id used for typed lineage, lookup, or dedupe.
534    pub redaction_policy_id: String,
535    #[serde(default, skip_serializing_if = "Vec::is_empty")]
536    /// Policy references that govern admission, projection, execution, or
537    /// delivery.
538    pub policy_refs: Vec<PolicyRef>,
539    /// Payload carried by this record.
540    /// Use the surrounding policy and redaction fields to decide whether it can be exposed.
541    pub payload: TelemetryRecordPayload,
542}
543
544impl TelemetryRecord {
545    /// Returns usage derived from the supplied state.
546    /// This is data-only and does not perform I/O, call host ports, append journals, publish
547    /// events, or start processes.
548    pub fn usage(
549        record_id: TelemetryRecordId,
550        projection: &TelemetryProjection,
551        usage_record_id: TelemetryUsageRecordId,
552    ) -> Self {
553        Self::from_projection(
554            record_id,
555            projection,
556            TelemetryRecordPayload::Usage(UsageTelemetryRecord {
557                usage_record_id,
558                units: projection.usage.clone().unwrap_or_default(),
559                provider_id: projection.provider_id.clone(),
560                model_id: projection.model_id.clone(),
561            }),
562        )
563    }
564
565    /// Returns cost derived from the supplied state.
566    /// This is data-only and does not perform I/O, call host ports, append journals, publish
567    /// events, or start processes.
568    pub fn cost(
569        record_id: TelemetryRecordId,
570        projection: &TelemetryProjection,
571        cost_record_id: TelemetryCostRecordId,
572        correction_ref: Option<TelemetryCostRecordId>,
573    ) -> Self {
574        let payload = CostTelemetryRecord {
575            cost_record_id,
576            units: projection
577                .cost
578                .clone()
579                .expect("cost projection has cost units"),
580            correction_ref,
581        };
582        Self::from_projection(record_id, projection, TelemetryRecordPayload::Cost(payload))
583    }
584
585    /// Returns an updated value with sink failed configured.
586    /// This is data-only and does not perform I/O, call host ports, append journals, publish
587    /// events, or start processes.
588    pub fn sink_failed(
589        record_id: TelemetryRecordId,
590        projection: &TelemetryProjection,
591        failure: TelemetrySinkFailureRecord,
592    ) -> Self {
593        Self::from_projection(
594            record_id,
595            projection,
596            TelemetryRecordPayload::SinkFailed(failure),
597        )
598    }
599
600    /// Returns an updated value with sink recovered configured.
601    /// This is data-only and does not perform I/O, call host ports, append journals, publish
602    /// events, or start processes.
603    pub fn sink_recovered(
604        record_id: TelemetryRecordId,
605        projection: &TelemetryProjection,
606        recovery: TelemetrySinkRecoveryRecord,
607    ) -> Self {
608        Self::from_projection(
609            record_id,
610            projection,
611            TelemetryRecordPayload::SinkRecovered(recovery),
612        )
613    }
614
615    /// Returns an updated value with export cursor configured.
616    /// This is data-only and does not perform I/O, call host ports, append journals, publish
617    /// events, or start processes.
618    pub fn export_cursor(
619        record_id: TelemetryRecordId,
620        projection: &TelemetryProjection,
621        cursor: TelemetryExportCursor,
622    ) -> Self {
623        Self::from_projection(
624            record_id,
625            projection,
626            TelemetryRecordPayload::ExportCursor(TelemetryExportCursorRecord { cursor }),
627        )
628    }
629
630    fn from_projection(
631        record_id: TelemetryRecordId,
632        projection: &TelemetryProjection,
633        payload: TelemetryRecordPayload,
634    ) -> Self {
635        Self {
636            schema_version: TELEMETRY_SCHEMA_VERSION,
637            record_id,
638            run_id: projection.run_id.clone(),
639            agent_id: projection.agent_id.clone(),
640            source_cursor: projection.source_record.source_cursor.clone(),
641            runtime_package_fingerprint: projection.runtime_package_fingerprint.clone(),
642            privacy: projection.privacy,
643            retention: projection.retention,
644            content_capture: projection.content_capture.clone(),
645            redaction_policy_id: projection.redaction_policy_id.clone(),
646            policy_refs: projection.policy_refs.clone(),
647            payload,
648        }
649    }
650}
651
652#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
653#[serde(tag = "type", content = "record", rename_all = "snake_case")]
654/// Enumerates the finite telemetry record payload cases.
655/// Serialized names are part of the SDK contract; update fixtures when variants change.
656pub enum TelemetryRecordPayload {
657    /// Use this variant when the contract needs to represent usage; selecting it has no side effect by itself.
658    Usage(UsageTelemetryRecord),
659    /// Use this variant when the contract needs to represent cost; selecting it has no side effect by itself.
660    Cost(CostTelemetryRecord),
661    /// Use this variant when the contract needs to represent sink failed; selecting it has no side effect by itself.
662    SinkFailed(TelemetrySinkFailureRecord),
663    /// Use this variant when the contract needs to represent sink recovered; selecting it has no side effect by itself.
664    SinkRecovered(TelemetrySinkRecoveryRecord),
665    /// Use this variant when the contract needs to represent export cursor; selecting it has no side effect by itself.
666    ExportCursor(TelemetryExportCursorRecord),
667}
668
669#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
670/// Carries the usage telemetry record record payload for journal, event, or fixture surfaces.
671/// 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.
672pub struct UsageTelemetryRecord {
673    /// Stable usage record id used for typed lineage, lookup, or dedupe.
674    pub usage_record_id: TelemetryUsageRecordId,
675    /// Units used by this record or request.
676    pub units: UsageUnits,
677    #[serde(skip_serializing_if = "Option::is_none")]
678    /// Stable provider id used for typed lineage, lookup, or dedupe.
679    pub provider_id: Option<String>,
680    #[serde(skip_serializing_if = "Option::is_none")]
681    /// Stable model id used for typed lineage, lookup, or dedupe.
682    pub model_id: Option<String>,
683}
684
685#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
686/// Carries the cost telemetry record record payload for journal, event, or fixture surfaces.
687/// 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.
688pub struct CostTelemetryRecord {
689    /// Stable cost record id used for typed lineage, lookup, or dedupe.
690    pub cost_record_id: TelemetryCostRecordId,
691    /// Units used by this record or request.
692    pub units: CostUnits,
693    #[serde(skip_serializing_if = "Option::is_none")]
694    /// Typed correction ref reference. Resolving or executing it is a
695    /// separate policy-gated step.
696    pub correction_ref: Option<TelemetryCostRecordId>,
697}
698
699#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
700/// Carries the telemetry sink failure record record payload for journal, event, or fixture surfaces.
701/// 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.
702pub struct TelemetrySinkFailureRecord {
703    /// Stable sink id used for typed lineage, lookup, or dedupe.
704    pub sink_id: TelemetrySinkId,
705    /// Kind discriminator for sink kind.
706    /// Use it to route finite match arms without parsing display text.
707    pub sink_kind: TelemetrySinkKind,
708    /// Kind discriminator for failure kind.
709    /// Use it to route finite match arms without parsing display text.
710    pub failure_kind: TelemetrySinkFailureKind,
711    /// Whether terminal preserved is enabled.
712    /// Policy, validation, or routing code uses this flag to choose the explicit behavior.
713    pub terminal_preserved: bool,
714    /// Count of dropped items observed or included in this record.
715    pub dropped_count: u64,
716    /// Last telemetry export cursor acknowledged by the sink.
717    /// Repair uses it to resume after the last confirmed export.
718    pub last_acknowledged_cursor: Option<TelemetryExportCursor>,
719    /// Cursor where repair or reconciliation should resume.
720    /// Use it to continue recovery without replaying unrelated records.
721    pub repair_cursor: Option<TelemetrySourceCursor>,
722    /// Reason a pending side effect is unsafe to retry automatically.
723    /// Recovery uses it to require repair or reconciliation before continuing.
724    pub unsafe_pending_reason: Option<String>,
725    /// Redacted human-readable summary safe for events, telemetry, and logs.
726    pub redacted_summary: String,
727}
728
729#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
730/// Carries the telemetry sink recovery record record payload for journal, event, or fixture surfaces.
731/// 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.
732pub struct TelemetrySinkRecoveryRecord {
733    /// Stable sink id used for typed lineage, lookup, or dedupe.
734    pub sink_id: TelemetrySinkId,
735    /// Kind discriminator for sink kind.
736    /// Use it to route finite match arms without parsing display text.
737    pub sink_kind: TelemetrySinkKind,
738    /// Cursor for telemetry export acknowledgement.
739    /// Sinks use it to ack exactly which projection was exported.
740    pub export_cursor: TelemetryExportCursor,
741    /// Redacted human-readable summary safe for events, telemetry, and logs.
742    pub redacted_summary: String,
743}
744
745#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
746/// Carries the telemetry export cursor record record payload for journal, event, or fixture surfaces.
747/// 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.
748pub struct TelemetryExportCursorRecord {
749    /// Cursor identifying a replay, export, or subscription position.
750    /// Use it to resume without widening the original scope.
751    pub cursor: TelemetryExportCursor,
752}