Skip to main content

agent_sdk_core/records/
output_delivery.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 delivery portion of that contract.
5//!
6use serde::{Deserialize, Serialize};
7use sha2::{Digest, Sha256};
8
9use crate::{
10    domain::{
11        AgentId, AttemptId, ContentRef, DedupeKey, DestinationRef, EffectId, EntityKind, EntityRef,
12        IdempotencyKey, MessageId, PolicyRef, PrivacyClass, RetentionClass, RunId, SourceRef,
13        TurnId, ValidatedOutputId,
14    },
15    effect::{EffectIntent, EffectKind, EffectResult, EffectTerminalStatus},
16    error::RetryClassification,
17    journal::{
18        EventIndexProjection, JOURNAL_SCHEMA_VERSION, JournalRecord, JournalRecordKind,
19        JournalRecordPayload,
20    },
21    package::RuntimePackageFingerprint,
22};
23
24#[derive(Clone, Debug, Deserialize, Eq, Hash, Ord, PartialEq, PartialOrd, Serialize)]
25#[serde(transparent)]
26/// Carries the output delivery id 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 OutputDeliveryId(String);
29
30impl OutputDeliveryId {
31    /// Creates a new records::output_delivery value with explicit
32    /// caller-provided inputs. This constructor is data-only and
33    /// performs no I/O or external side effects.
34    pub fn new(value: impl Into<String>) -> Self {
35        let value = value.into();
36        assert!(!value.is_empty(), "OutputDeliveryId must not be empty");
37        Self(value)
38    }
39
40    /// Returns this value as str. The accessor is side-effect free and
41    /// keeps ownership with the caller.
42    pub fn as_str(&self) -> &str {
43        &self.0
44    }
45}
46
47#[derive(Clone, Debug, Deserialize, Eq, Hash, Ord, PartialEq, PartialOrd, Serialize)]
48#[serde(transparent)]
49/// Carries the output sink ref record payload for journal, event, or fixture surfaces.
50/// 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.
51pub struct OutputSinkRef(String);
52
53impl OutputSinkRef {
54    /// Creates a new records::output_delivery value with explicit
55    /// caller-provided inputs. This constructor is data-only and
56    /// performs no I/O or external side effects.
57    pub fn new(value: impl Into<String>) -> Self {
58        let value = value.into();
59        assert!(!value.is_empty(), "OutputSinkRef must not be empty");
60        Self(value)
61    }
62
63    /// Returns this value as str. The accessor is side-effect free and
64    /// keeps ownership with the caller.
65    pub fn as_str(&self) -> &str {
66        &self.0
67    }
68}
69
70#[derive(Clone, Copy, Debug, Deserialize, Eq, PartialEq, Serialize)]
71#[serde(rename_all = "snake_case")]
72/// Enumerates the finite output delivery requirement cases.
73/// Serialized names are part of the SDK contract; update fixtures when variants change.
74pub enum OutputDeliveryRequirement {
75    /// Use this variant when the contract needs to represent disabled; selecting it has no side effect by itself.
76    Disabled,
77    /// Use this variant when the contract needs to represent optional; selecting it has no side effect by itself.
78    Optional,
79    /// Use this variant when the contract needs to represent required; selecting it has no side effect by itself.
80    Required,
81}
82
83#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
84#[serde(tag = "type", rename_all = "snake_case")]
85/// Enumerates the finite output delivery kind cases.
86/// Serialized names are part of the SDK contract; update fixtures when variants change.
87pub enum OutputDeliveryKind {
88    /// Use this variant when the contract needs to represent stream chunk; selecting it has no side effect by itself.
89    StreamChunk {
90        /// Cursor identifying a replay, export, or subscription position.
91        /// Use it to resume without widening the original scope.
92        stream_cursor: String,
93        /// Chunk index used by this record or request.
94        chunk_index: u64,
95    },
96    /// Use this variant when the contract needs to represent final message; selecting it has no side effect by itself.
97    FinalMessage,
98    /// Use this variant when the contract needs to represent final validated output; selecting it has no side effect by itself.
99    FinalValidatedOutput,
100}
101
102impl OutputDeliveryKind {
103    /// Reports whether this value is chunk. The check is pure and does
104    /// not mutate SDK or host state.
105    pub fn is_chunk(&self) -> bool {
106        matches!(self, Self::StreamChunk { .. })
107    }
108
109    /// Returns dedupe fragment derived from the supplied state.
110    /// This is data-only and does not perform I/O, call host ports, append journals, publish
111    /// events, or start processes.
112    pub(crate) fn dedupe_fragment(&self) -> String {
113        match self {
114            Self::StreamChunk {
115                stream_cursor,
116                chunk_index,
117            } => format!("stream:{stream_cursor}:{chunk_index}"),
118            Self::FinalMessage => "final:message".to_string(),
119            Self::FinalValidatedOutput => "final:validated_output".to_string(),
120        }
121    }
122}
123
124#[derive(Clone, Copy, Debug, Deserialize, Eq, PartialEq, Serialize)]
125#[serde(rename_all = "snake_case")]
126/// Enumerates the finite output content mode cases.
127/// Serialized names are part of the SDK contract; update fixtures when variants change.
128pub enum OutputContentMode {
129    /// Use this variant when the contract needs to represent content refs only; selecting it has no side effect by itself.
130    ContentRefsOnly,
131    /// Use this variant when the contract needs to represent redacted summary; selecting it has no side effect by itself.
132    RedactedSummary,
133    /// Use this variant when the contract needs to represent raw content if policy allows; selecting it has no side effect by itself.
134    RawContentIfPolicyAllows,
135}
136
137#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
138/// Carries the output delivery policy record payload for journal, event, or fixture surfaces.
139/// 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.
140pub struct OutputDeliveryPolicy {
141    /// Policy reference that must be resolved by the host or runtime before
142    /// execution.
143    pub policy_ref: PolicyRef,
144    /// Requirement used by this record or request.
145    pub requirement: OutputDeliveryRequirement,
146    /// Default content mode used by this record or request.
147    pub default_content_mode: OutputContentMode,
148    #[serde(default)]
149    /// Allowlist for this policy or contract.
150    /// Validation uses it to reject undeclared or policy-denied values.
151    pub allowed_content_modes: Vec<OutputContentMode>,
152    #[serde(skip_serializing_if = "Option::is_none")]
153    /// Typed required sink ref reference. Resolving or executing it is a
154    /// separate policy-gated step.
155    pub required_sink_ref: Option<OutputSinkRef>,
156    #[serde(skip_serializing_if = "Option::is_none")]
157    /// Typed retry policy ref reference. Resolving or executing it is a
158    /// separate policy-gated step.
159    pub retry_policy_ref: Option<PolicyRef>,
160    #[serde(skip_serializing_if = "Option::is_none")]
161    /// Typed reconciliation policy ref reference. Resolving or executing it
162    /// is a separate policy-gated step.
163    pub reconciliation_policy_ref: Option<PolicyRef>,
164    /// Raw content or raw-content control for this value.
165    /// Use it only when policy explicitly allows raw content capture or delivery.
166    pub raw_content_policy: RawOutputContentPolicy,
167}
168
169impl OutputDeliveryPolicy {
170    /// Returns an updated value with required configured.
171    /// This is data-only and does not perform I/O, call host ports, append journals, publish
172    /// events, or start processes.
173    pub fn required(policy_ref: PolicyRef, sink_ref: OutputSinkRef) -> Self {
174        Self {
175            policy_ref,
176            requirement: OutputDeliveryRequirement::Required,
177            default_content_mode: OutputContentMode::ContentRefsOnly,
178            allowed_content_modes: vec![
179                OutputContentMode::ContentRefsOnly,
180                OutputContentMode::RedactedSummary,
181            ],
182            required_sink_ref: Some(sink_ref),
183            retry_policy_ref: None,
184            reconciliation_policy_ref: None,
185            raw_content_policy: RawOutputContentPolicy::deny(),
186        }
187    }
188
189    /// Returns an updated value with optional configured.
190    /// This is data-only and does not perform I/O, call host ports, append journals, publish
191    /// events, or start processes.
192    pub fn optional(policy_ref: PolicyRef) -> Self {
193        Self {
194            policy_ref,
195            requirement: OutputDeliveryRequirement::Optional,
196            default_content_mode: OutputContentMode::RedactedSummary,
197            allowed_content_modes: vec![OutputContentMode::RedactedSummary],
198            required_sink_ref: None,
199            retry_policy_ref: None,
200            reconciliation_policy_ref: None,
201            raw_content_policy: RawOutputContentPolicy::deny(),
202        }
203    }
204
205    /// Returns an updated value with disabled configured.
206    /// This is data-only and does not perform I/O, call host ports, append journals, publish
207    /// events, or start processes.
208    pub fn disabled(policy_ref: PolicyRef) -> Self {
209        Self {
210            policy_ref,
211            requirement: OutputDeliveryRequirement::Disabled,
212            default_content_mode: OutputContentMode::ContentRefsOnly,
213            allowed_content_modes: Vec::new(),
214            required_sink_ref: None,
215            retry_policy_ref: None,
216            reconciliation_policy_ref: None,
217            raw_content_policy: RawOutputContentPolicy::deny(),
218        }
219    }
220
221    /// Returns whether allows mode applies for this contract.
222    /// This is data-only and does not perform I/O, call host ports, append journals, publish
223    /// events, or start processes.
224    pub fn allows_mode(&self, mode: OutputContentMode) -> bool {
225        self.allowed_content_modes.contains(&mode)
226    }
227
228    /// Returns policy refs for callers that need to inspect the contract state.
229    /// This is data-only and does not perform I/O, call host ports, append journals, publish
230    /// events, or start processes.
231    pub fn policy_refs(&self) -> Vec<PolicyRef> {
232        let mut refs = vec![self.policy_ref.clone()];
233        if let Some(policy_ref) = &self.retry_policy_ref {
234            refs.push(policy_ref.clone());
235        }
236        if let Some(policy_ref) = &self.reconciliation_policy_ref {
237            refs.push(policy_ref.clone());
238        }
239        refs
240    }
241}
242
243#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
244/// Carries the raw output content policy record payload for journal, event, or fixture surfaces.
245/// 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.
246pub struct RawOutputContentPolicy {
247    /// Policy reference that must be resolved by the host or runtime before
248    /// execution.
249    pub policy_ref: PolicyRef,
250    /// Boolean policy/capability flag for whether allow raw content is
251    /// enabled.
252    pub allow_raw_content: bool,
253    /// Retention class for referenced content or records.
254    /// Stores and telemetry sinks use it to decide how long evidence may be kept.
255    pub retention_named: bool,
256    /// Whether redaction policy named is enabled.
257    /// Policy, validation, or routing code uses this flag to choose the explicit behavior.
258    pub redaction_policy_named: bool,
259    #[serde(skip_serializing_if = "Option::is_none")]
260    /// Typed allowed sink ref reference. Resolving or executing it is a
261    /// separate policy-gated step.
262    pub allowed_sink_ref: Option<OutputSinkRef>,
263    /// Byte size or byte limit for byte limit.
264    /// Use it to enforce bounded reads, writes, summaries, or parser output.
265    pub byte_limit: u64,
266}
267
268impl RawOutputContentPolicy {
269    /// Returns an updated records::output_delivery value with deny applied.
270    /// This is data construction only and does not execute the configured
271    /// behavior.
272    pub fn deny() -> Self {
273        Self {
274            policy_ref: PolicyRef::new("policy.output_delivery.raw.deny"),
275            allow_raw_content: false,
276            retention_named: true,
277            redaction_policy_named: true,
278            allowed_sink_ref: None,
279            byte_limit: 0,
280        }
281    }
282
283    /// Returns an updated value with allow for sink configured.
284    /// This is data-only and does not perform I/O, call host ports, append journals, publish
285    /// events, or start processes.
286    pub fn allow_for_sink(policy_ref: PolicyRef, sink_ref: OutputSinkRef, byte_limit: u64) -> Self {
287        Self {
288            policy_ref,
289            allow_raw_content: true,
290            retention_named: true,
291            redaction_policy_named: true,
292            allowed_sink_ref: Some(sink_ref),
293            byte_limit,
294        }
295    }
296
297    /// Returns whether allows raw for applies for this contract.
298    /// This is data-only and does not perform I/O, call host ports, append journals, publish
299    /// events, or start processes.
300    pub fn allows_raw_for(&self, sink_ref: &OutputSinkRef, byte_len: usize) -> bool {
301        self.allow_raw_content
302            && self.retention_named
303            && self.redaction_policy_named
304            && self
305                .allowed_sink_ref
306                .as_ref()
307                .is_some_and(|allowed| allowed == sink_ref)
308            && self.byte_limit >= byte_len as u64
309    }
310}
311
312#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
313/// Carries the output delivery request record payload for journal, event, or fixture surfaces.
314/// 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.
315pub struct OutputDeliveryRequest {
316    /// Stable delivery id used for typed lineage, lookup, or dedupe.
317    pub delivery_id: OutputDeliveryId,
318    /// Stable effect id used for typed lineage, lookup, or dedupe.
319    pub effect_id: EffectId,
320    /// Run identifier used for lineage, filtering, replay, and dedupe.
321    pub run_id: RunId,
322    /// Agent identifier used for lineage, filtering, and ownership checks.
323    pub agent_id: AgentId,
324    #[serde(skip_serializing_if = "Option::is_none")]
325    /// Turn identifier for one loop turn within a run.
326    pub turn_id: Option<TurnId>,
327    #[serde(skip_serializing_if = "Option::is_none")]
328    /// Attempt identifier for retry, repair, provider, or tool execution
329    /// evidence.
330    pub attempt_id: Option<AttemptId>,
331    #[serde(skip_serializing_if = "Option::is_none")]
332    /// Stable source message id used for typed lineage, lookup, or dedupe.
333    pub source_message_id: Option<MessageId>,
334    #[serde(skip_serializing_if = "Option::is_none")]
335    /// Stable validated output id used for typed lineage, lookup, or dedupe.
336    pub validated_output_id: Option<ValidatedOutputId>,
337    /// Destination label or ref for this item; it is metadata and does not
338    /// deliver content by itself.
339    pub destination: DestinationRef,
340    /// Typed sink ref reference. Resolving or executing it is a separate
341    /// policy-gated step.
342    pub sink_ref: OutputSinkRef,
343    /// Output delivery setting or policy.
344    /// Delivery coordinators use it to decide sink mode, dedupe, and required evidence.
345    pub delivery_kind: OutputDeliveryKind,
346    /// Content mode used by this record or request.
347    pub content_mode: OutputContentMode,
348    #[serde(default, skip_serializing_if = "Vec::is_empty")]
349    /// Content references associated with this record; resolving them is a
350    /// separate policy-gated step.
351    pub content_refs: Vec<ContentRef>,
352    /// Redacted human-readable summary safe for events, telemetry, and logs.
353    pub redacted_summary: String,
354    #[serde(skip_serializing_if = "Option::is_none")]
355    /// Raw content or raw-content control for this value.
356    /// Use it only when policy explicitly allows raw content capture or delivery.
357    pub raw_content: Option<String>,
358    /// Privacy class used for projection, telemetry, and raw-content access
359    /// decisions.
360    pub privacy: PrivacyClass,
361    /// Retention class used by hosts and sinks when storing or exporting this
362    /// item.
363    pub retention: RetentionClass,
364    #[serde(default, skip_serializing_if = "Vec::is_empty")]
365    /// Policy references that govern admission, projection, execution, or
366    /// delivery.
367    pub policy_refs: Vec<PolicyRef>,
368    #[serde(skip_serializing_if = "Option::is_none")]
369    /// Idempotency setting or key for deduping retries.
370    /// Use it to prevent duplicate side effects during replay or repair.
371    pub idempotency_key: Option<IdempotencyKey>,
372    /// Dedupe policy or key for a side-effecting operation.
373    /// Replay and repair use it to avoid sending or executing the same effect twice.
374    pub dedupe_key: DedupeKey,
375    /// Fingerprint of the runtime package snapshot in force when this value was produced.
376    /// Use it for replay, dedupe, and package-lineage checks; the field is evidence and does
377    /// not execute package behavior.
378    pub runtime_package_fingerprint: RuntimePackageFingerprint,
379}
380
381impl OutputDeliveryRequest {
382    /// Returns effect intent derived from the supplied state.
383    /// This is data-only and does not perform I/O, call host ports, append journals, publish
384    /// events, or start processes.
385    pub fn effect_intent(&self) -> EffectIntent {
386        let mut intent = EffectIntent::new(
387            self.effect_id.clone(),
388            EffectKind::OutputDelivery,
389            EntityRef::new(EntityKind::OutputDelivery, self.delivery_id.as_str()),
390            SourceRef::with_kind(crate::domain::SourceKind::Sdk, "source.sdk.output_delivery"),
391            self.redacted_summary.clone(),
392        );
393        intent.destination = Some(self.destination.clone());
394        intent.policy_refs = self.policy_refs.clone();
395        intent.idempotency_key = self.idempotency_key.clone();
396        intent.dedupe_key = Some(self.dedupe_key.clone());
397        intent.content_refs = self.content_refs.clone();
398        intent
399    }
400
401    /// Returns whether carries raw content applies for this contract.
402    /// This is data-only and does not perform I/O, call host ports, append journals, publish
403    /// events, or start processes.
404    pub fn carries_raw_content(&self) -> bool {
405        self.raw_content.is_some()
406    }
407}
408
409#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
410/// Carries the output delivery receipt record payload for journal, event, or fixture surfaces.
411/// 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.
412pub struct OutputDeliveryReceipt {
413    /// Stable delivery id used for typed lineage, lookup, or dedupe.
414    pub delivery_id: OutputDeliveryId,
415    /// Finite status for this record or lifecycle stage.
416    pub status: OutputDispatchStatus,
417    #[serde(skip_serializing_if = "Option::is_none")]
418    /// Typed ack ref reference. Resolving or executing it is a separate
419    /// policy-gated step.
420    pub ack_ref: Option<String>,
421    #[serde(skip_serializing_if = "Option::is_none")]
422    /// Cursor identifying a replay, export, or subscription position.
423    /// Use it to resume without widening the original scope.
424    pub destination_cursor: Option<String>,
425    #[serde(skip_serializing_if = "Option::is_none")]
426    /// Stable external operation id used for typed lineage, lookup, or
427    /// dedupe.
428    pub external_operation_id: Option<String>,
429    #[serde(skip_serializing_if = "Option::is_none")]
430    /// Typed reconciliation ref reference. Resolving or executing it is a
431    /// separate policy-gated step.
432    pub reconciliation_ref: Option<String>,
433    /// Redacted human-readable summary safe for events, telemetry, and logs.
434    pub redacted_summary: String,
435}
436
437impl OutputDeliveryReceipt {
438    /// Returns an updated value with completed configured.
439    /// This is data-only and does not perform I/O, call host ports, append journals, publish
440    /// events, or start processes.
441    pub fn completed(delivery_id: OutputDeliveryId, ack_ref: impl Into<String>) -> Self {
442        Self {
443            delivery_id,
444            status: OutputDispatchStatus::Completed,
445            ack_ref: Some(ack_ref.into()),
446            destination_cursor: None,
447            external_operation_id: None,
448            reconciliation_ref: None,
449            redacted_summary: "output delivery completed".to_string(),
450        }
451    }
452
453    /// Builds the unknown record or result value.
454    /// This is data-only and does not perform I/O, call host ports, append journals, publish
455    /// events, or start processes.
456    pub fn unknown(delivery_id: OutputDeliveryId, reconciliation_ref: impl Into<String>) -> Self {
457        Self {
458            delivery_id,
459            status: OutputDispatchStatus::ReconciliationNeeded,
460            ack_ref: None,
461            destination_cursor: None,
462            external_operation_id: None,
463            reconciliation_ref: Some(reconciliation_ref.into()),
464            redacted_summary: "output delivery outcome unknown".to_string(),
465        }
466    }
467}
468
469#[derive(Clone, Copy, Debug, Deserialize, Eq, PartialEq, Serialize)]
470#[serde(rename_all = "snake_case")]
471/// Enumerates the finite output dispatch status cases.
472/// Serialized names are part of the SDK contract; update fixtures when variants change.
473pub enum OutputDispatchStatus {
474    /// Use this variant when the contract needs to represent requested; selecting it has no side effect by itself.
475    Requested,
476    /// Use this variant when the contract needs to represent completed; selecting it has no side effect by itself.
477    Completed,
478    /// Use this variant when the contract needs to represent failed; selecting it has no side effect by itself.
479    Failed,
480    /// Use this variant when the contract needs to represent deduped; selecting it has no side effect by itself.
481    Deduped,
482    /// Use this variant when the contract needs to represent host configuration needed; selecting it has no side effect by itself.
483    HostConfigurationNeeded,
484    /// Use this variant when the contract needs to represent policy denied; selecting it has no side effect by itself.
485    PolicyDenied,
486    /// Use this variant when the contract needs to represent skipped optional; selecting it has no side effect by itself.
487    SkippedOptional,
488    /// Use this variant when the contract needs to represent reconciliation needed; selecting it has no side effect by itself.
489    ReconciliationNeeded,
490}
491
492#[derive(Clone, Copy, Debug, Deserialize, Eq, PartialEq, Serialize)]
493#[serde(rename_all = "snake_case")]
494/// Enumerates the finite output delivery event kind cases.
495/// Serialized names are part of the SDK contract; update fixtures when variants change.
496pub enum OutputDeliveryEventKind {
497    /// Use this variant when the contract needs to represent output dispatch requested; selecting it has no side effect by itself.
498    OutputDispatchRequested,
499    /// Use this variant when the contract needs to represent output dispatch completed; selecting it has no side effect by itself.
500    OutputDispatchCompleted,
501    /// Use this variant when the contract needs to represent output dispatch failed; selecting it has no side effect by itself.
502    OutputDispatchFailed,
503    /// Use this variant when the contract needs to represent output dispatch deduped; selecting it has no side effect by itself.
504    OutputDispatchDeduped,
505}
506
507#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
508/// Carries the output delivery intent record record payload for journal, event, or fixture surfaces.
509/// 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.
510pub struct OutputDeliveryIntentRecord {
511    /// Stable delivery id used for typed lineage, lookup, or dedupe.
512    pub delivery_id: OutputDeliveryId,
513    /// Effect intent used by this record or request.
514    pub effect_intent: EffectIntent,
515    /// Destination label or ref for this item; it is metadata and does not
516    /// deliver content by itself.
517    pub destination: DestinationRef,
518    /// Typed sink ref reference. Resolving or executing it is a separate
519    /// policy-gated step.
520    pub sink_ref: OutputSinkRef,
521    /// Typed desired sink ref reference. Resolving or executing it is a
522    /// separate policy-gated step.
523    pub desired_sink_ref: OutputSinkRef,
524    /// Output delivery setting or policy.
525    /// Delivery coordinators use it to decide sink mode, dedupe, and required evidence.
526    pub delivery_kind: OutputDeliveryKind,
527    /// Content mode used by this record or request.
528    pub content_mode: OutputContentMode,
529    #[serde(default, skip_serializing_if = "Vec::is_empty")]
530    /// Content references associated with this record; resolving them is a
531    /// separate policy-gated step.
532    pub content_refs: Vec<ContentRef>,
533    /// Redacted human-readable summary safe for events, telemetry, and logs.
534    pub redacted_summary: String,
535    /// Privacy class used for projection, telemetry, and raw-content access
536    /// decisions.
537    pub privacy: PrivacyClass,
538    /// Retention class used by hosts and sinks when storing or exporting this
539    /// item.
540    pub retention: RetentionClass,
541    #[serde(default, skip_serializing_if = "Vec::is_empty")]
542    /// Policy references that govern admission, projection, execution, or
543    /// delivery.
544    pub policy_refs: Vec<PolicyRef>,
545    #[serde(skip_serializing_if = "Option::is_none")]
546    /// Idempotency setting or key for deduping retries.
547    /// Use it to prevent duplicate side effects during replay or repair.
548    pub idempotency_key: Option<IdempotencyKey>,
549    /// Dedupe policy or key for a side-effecting operation.
550    /// Replay and repair use it to avoid sending or executing the same effect twice.
551    pub dedupe_key: DedupeKey,
552    /// Fingerprint of the runtime package snapshot in force when this value was produced.
553    /// Use it for replay, dedupe, and package-lineage checks; the field is evidence and does
554    /// not execute package behavior.
555    pub runtime_package_fingerprint: RuntimePackageFingerprint,
556}
557
558impl OutputDeliveryIntentRecord {
559    /// Constructs this value from request. Use it when adapting
560    /// canonical SDK records without introducing a second behavior
561    /// path.
562    pub fn from_request(request: &OutputDeliveryRequest) -> Self {
563        Self {
564            delivery_id: request.delivery_id.clone(),
565            effect_intent: request.effect_intent(),
566            destination: request.destination.clone(),
567            sink_ref: request.sink_ref.clone(),
568            desired_sink_ref: request.sink_ref.clone(),
569            delivery_kind: request.delivery_kind.clone(),
570            content_mode: request.content_mode,
571            content_refs: request.content_refs.clone(),
572            redacted_summary: request.redacted_summary.clone(),
573            privacy: request.privacy,
574            retention: request.retention,
575            policy_refs: request.policy_refs.clone(),
576            idempotency_key: request.idempotency_key.clone(),
577            dedupe_key: request.dedupe_key.clone(),
578            runtime_package_fingerprint: request.runtime_package_fingerprint.clone(),
579        }
580    }
581
582    /// Converts this value into journal record data.
583    /// This is data-only and does not perform I/O, call host ports, append journals, publish
584    /// events, or start processes.
585    pub fn to_journal_record(&self, base: OutputDeliveryJournalBase) -> JournalRecord {
586        output_delivery_effect_record(
587            base,
588            JournalRecordKind::OutputDispatch,
589            "output_dispatch_requested",
590            Some(self.idempotency_key.clone()).flatten(),
591            Some(self.dedupe_key.clone()),
592            self.effect_intent.content_refs.clone(),
593            JournalRecordPayload::OutputDelivery(OutputDeliveryRecord::Intent(self.clone())),
594            EntityRef::new(EntityKind::OutputDelivery, self.delivery_id.as_str()),
595            self.destination.clone(),
596            self.policy_refs.clone(),
597            self.privacy,
598        )
599    }
600}
601
602#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
603/// Carries the output delivery result record record payload for journal, event, or fixture surfaces.
604/// 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.
605pub struct OutputDeliveryResultRecord {
606    /// Stable delivery id used for typed lineage, lookup, or dedupe.
607    pub delivery_id: OutputDeliveryId,
608    /// Effect result used by this record or request.
609    pub effect_result: EffectResult,
610    /// Destination label or ref for this item; it is metadata and does not
611    /// deliver content by itself.
612    pub destination: DestinationRef,
613    /// Typed sink ref reference. Resolving or executing it is a separate
614    /// policy-gated step.
615    pub sink_ref: OutputSinkRef,
616    /// Dispatch status used by this record or request.
617    pub dispatch_status: OutputDispatchStatus,
618    #[serde(skip_serializing_if = "Option::is_none")]
619    /// Typed ack ref reference. Resolving or executing it is a separate
620    /// policy-gated step.
621    pub ack_ref: Option<String>,
622    #[serde(skip_serializing_if = "Option::is_none")]
623    /// Stable external operation id used for typed lineage, lookup, or
624    /// dedupe.
625    pub external_operation_id: Option<String>,
626    #[serde(skip_serializing_if = "Option::is_none")]
627    /// Typed error ref reference. Resolving or executing it is a separate
628    /// policy-gated step.
629    pub error_ref: Option<String>,
630    #[serde(default, skip_serializing_if = "Vec::is_empty")]
631    /// Content references associated with this record; resolving them is a
632    /// separate policy-gated step.
633    pub content_refs: Vec<ContentRef>,
634    /// Redacted human-readable summary safe for events, telemetry, and logs.
635    pub redacted_summary: String,
636    /// Retry classification used by this record or request.
637    pub retry_classification: RetryClassification,
638}
639
640impl OutputDeliveryResultRecord {
641    /// Returns an updated value with completed configured.
642    /// This is data-only and does not perform I/O, call host ports, append journals, publish
643    /// events, or start processes.
644    pub fn completed(request: &OutputDeliveryRequest, receipt: &OutputDeliveryReceipt) -> Self {
645        Self::from_status(
646            request,
647            OutputDispatchStatus::Completed,
648            EffectTerminalStatus::Completed,
649            receipt.ack_ref.clone(),
650            receipt.external_operation_id.clone(),
651            None,
652            None,
653            RetryClassification::NotRetryable,
654            receipt.redacted_summary.clone(),
655        )
656    }
657
658    /// Returns an updated value with failed configured.
659    /// This is data-only and does not perform I/O, call host ports, append journals, publish
660    /// events, or start processes.
661    pub fn failed(
662        request: &OutputDeliveryRequest,
663        status: OutputDispatchStatus,
664        error_ref: impl Into<String>,
665        retry_classification: RetryClassification,
666    ) -> Self {
667        Self::from_status(
668            request,
669            status,
670            EffectTerminalStatus::Failed,
671            None,
672            None,
673            None,
674            Some(error_ref.into()),
675            retry_classification,
676            "output delivery failed before host send or at sink boundary",
677        )
678    }
679
680    /// Builds the reconciliation needed record or result value.
681    /// This is data-only and does not perform I/O, call host ports, append journals, publish
682    /// events, or start processes.
683    pub fn reconciliation_needed(
684        request: &OutputDeliveryRequest,
685        receipt: &OutputDeliveryReceipt,
686    ) -> Self {
687        Self::from_status(
688            request,
689            OutputDispatchStatus::ReconciliationNeeded,
690            EffectTerminalStatus::Unknown,
691            receipt.ack_ref.clone(),
692            receipt.external_operation_id.clone(),
693            receipt.reconciliation_ref.clone(),
694            None,
695            RetryClassification::RepairNeeded,
696            receipt.redacted_summary.clone(),
697        )
698    }
699
700    #[expect(
701        clippy::too_many_arguments,
702        reason = "status projection mirrors output-delivery effect fields; grouping belongs with a dedicated result-builder API"
703    )]
704    fn from_status(
705        request: &OutputDeliveryRequest,
706        dispatch_status: OutputDispatchStatus,
707        terminal_status: EffectTerminalStatus,
708        ack_ref: Option<String>,
709        external_operation_id: Option<String>,
710        reconciliation_ref: Option<String>,
711        error_ref: Option<String>,
712        retry_classification: RetryClassification,
713        redacted_summary: impl Into<String>,
714    ) -> Self {
715        let redacted_summary = redacted_summary.into();
716        Self {
717            delivery_id: request.delivery_id.clone(),
718            effect_result: EffectResult {
719                effect_id: request.effect_id.clone(),
720                terminal_status,
721                external_operation_id: external_operation_id.clone(),
722                reconciliation_ref,
723                error_ref: error_ref.clone(),
724                content_refs: request.content_refs.clone(),
725                redacted_summary: redacted_summary.clone(),
726            },
727            destination: request.destination.clone(),
728            sink_ref: request.sink_ref.clone(),
729            dispatch_status,
730            ack_ref,
731            external_operation_id,
732            error_ref,
733            content_refs: request.content_refs.clone(),
734            redacted_summary,
735            retry_classification,
736        }
737    }
738
739    /// Converts this value into journal record data.
740    /// This is data-only and does not perform I/O, call host ports, append journals, publish
741    /// events, or start processes.
742    pub fn to_journal_record(&self, base: OutputDeliveryJournalBase) -> JournalRecord {
743        let event_kind = match self.dispatch_status {
744            OutputDispatchStatus::Completed => "output_dispatch_completed",
745            OutputDispatchStatus::HostConfigurationNeeded
746            | OutputDispatchStatus::PolicyDenied
747            | OutputDispatchStatus::Failed => "output_dispatch_failed",
748            OutputDispatchStatus::ReconciliationNeeded => "output_dispatch_reconciliation_needed",
749            OutputDispatchStatus::Deduped => "output_dispatch_deduped",
750            OutputDispatchStatus::Requested | OutputDispatchStatus::SkippedOptional => {
751                "output_dispatch_status"
752            }
753        };
754        output_delivery_effect_record(
755            base,
756            JournalRecordKind::OutputDispatch,
757            event_kind,
758            None,
759            None,
760            self.effect_result.content_refs.clone(),
761            JournalRecordPayload::OutputDelivery(OutputDeliveryRecord::Result(self.clone())),
762            EntityRef::new(EntityKind::OutputDelivery, self.delivery_id.as_str()),
763            self.destination.clone(),
764            Vec::new(),
765            PrivacyClass::ContentRefsOnly,
766        )
767    }
768}
769
770#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
771/// Carries the output delivery dedupe record record payload for journal, event, or fixture surfaces.
772/// 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.
773pub struct OutputDeliveryDedupeRecord {
774    /// Stable delivery id used for typed lineage, lookup, or dedupe.
775    pub delivery_id: OutputDeliveryId,
776    /// Dedupe policy or key for a side-effecting operation.
777    /// Replay and repair use it to avoid sending or executing the same effect twice.
778    pub dedupe_key: DedupeKey,
779    #[serde(skip_serializing_if = "Option::is_none")]
780    /// Stable prior delivery id used for typed lineage, lookup, or dedupe.
781    pub prior_delivery_id: Option<OutputDeliveryId>,
782    #[serde(skip_serializing_if = "Option::is_none")]
783    /// External sink operation id from a previous delivery attempt, when known.
784    /// Reconciliation uses it to avoid duplicate sends and to connect repaired evidence to the
785    /// prior external operation.
786    pub prior_external_operation_id: Option<String>,
787    /// Prior terminal status used by this record or request.
788    pub prior_terminal_status: OutputDispatchStatus,
789    /// Current status used by this record or request.
790    pub current_status: OutputDispatchStatus,
791    /// Redacted human-readable summary safe for events, telemetry, and logs.
792    pub redacted_summary: String,
793    #[serde(default, skip_serializing_if = "Vec::is_empty")]
794    /// Policy references that govern admission, projection, execution, or
795    /// delivery.
796    pub policy_refs: Vec<PolicyRef>,
797}
798
799impl OutputDeliveryDedupeRecord {
800    /// Converts this value into journal record data.
801    /// This is data-only and does not perform I/O, call host ports, append journals, publish
802    /// events, or start processes.
803    pub fn to_journal_record(
804        &self,
805        base: OutputDeliveryJournalBase,
806        destination: DestinationRef,
807    ) -> JournalRecord {
808        output_delivery_effect_record(
809            base,
810            JournalRecordKind::OutputDispatch,
811            "output_dispatch_deduped",
812            None,
813            Some(self.dedupe_key.clone()),
814            Vec::new(),
815            JournalRecordPayload::OutputDelivery(OutputDeliveryRecord::Dedupe(self.clone())),
816            EntityRef::new(EntityKind::OutputDelivery, self.delivery_id.as_str()),
817            destination,
818            self.policy_refs.clone(),
819            PrivacyClass::ContentRefsOnly,
820        )
821    }
822}
823
824#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
825/// Carries the output delivery reconciliation record record payload for journal, event, or fixture surfaces.
826/// 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.
827pub struct OutputDeliveryReconciliationRecord {
828    /// Stable delivery id used for typed lineage, lookup, or dedupe.
829    pub delivery_id: OutputDeliveryId,
830    /// Stable intent record id used for typed lineage, lookup, or dedupe.
831    pub intent_record_id: String,
832    /// Kind discriminator for side effect kind.
833    /// Use it to route finite match arms without parsing display text.
834    pub side_effect_kind: EffectKind,
835    #[serde(skip_serializing_if = "Option::is_none")]
836    /// Idempotency setting or key for deduping retries.
837    /// Use it to prevent duplicate side effects during replay or repair.
838    pub idempotency_key: Option<IdempotencyKey>,
839    /// Dedupe policy or key for a side-effecting operation.
840    /// Replay and repair use it to avoid sending or executing the same effect twice.
841    pub dedupe_key: DedupeKey,
842    #[serde(skip_serializing_if = "Option::is_none")]
843    /// Stable external operation id used for typed lineage, lookup, or
844    /// dedupe.
845    pub external_operation_id: Option<String>,
846    /// Terminal status used by this record or request.
847    pub terminal_status: OutputDispatchStatus,
848    /// Terminal append status used by this record or request.
849    pub terminal_append_status: TerminalAppendStatus,
850    #[serde(skip_serializing_if = "Option::is_none")]
851    /// Optional reconciliation adapter value.
852    /// When absent, callers should use the documented default or skip that optional behavior.
853    pub reconciliation_adapter: Option<OutputSinkRef>,
854    /// Reason a pending side effect is unsafe to retry automatically.
855    /// Recovery uses it to require repair or reconciliation before continuing.
856    pub unsafe_pending_reason: String,
857    /// Replay decision used by this record or request.
858    pub replay_decision: ReplayRepairDecision,
859    /// Allowlist for this policy or contract.
860    /// Validation uses it to reject undeclared or policy-denied values.
861    pub resend_allowed: bool,
862}
863
864impl OutputDeliveryReconciliationRecord {
865    /// Converts this value into journal record data.
866    /// This is data-only and does not perform I/O, call host ports, append journals, publish
867    /// events, or start processes.
868    pub fn to_journal_record(
869        &self,
870        base: OutputDeliveryJournalBase,
871        destination: DestinationRef,
872    ) -> JournalRecord {
873        output_delivery_effect_record(
874            base,
875            JournalRecordKind::OutputDispatch,
876            "output_dispatch_reconciliation_needed",
877            self.idempotency_key.clone(),
878            Some(self.dedupe_key.clone()),
879            Vec::new(),
880            JournalRecordPayload::OutputDelivery(OutputDeliveryRecord::Reconciliation(
881                self.clone(),
882            )),
883            EntityRef::new(EntityKind::OutputDelivery, self.delivery_id.as_str()),
884            destination,
885            Vec::new(),
886            PrivacyClass::ContentRefsOnly,
887        )
888    }
889}
890
891#[derive(Clone, Copy, Debug, Deserialize, Eq, PartialEq, Serialize)]
892#[serde(rename_all = "snake_case")]
893/// Enumerates the finite terminal append status cases.
894/// Serialized names are part of the SDK contract; update fixtures when variants change.
895pub enum TerminalAppendStatus {
896    /// Use this variant when the contract needs to represent not attempted; selecting it has no side effect by itself.
897    NotAttempted,
898    /// Use this variant when the contract needs to represent appended; selecting it has no side effect by itself.
899    Appended,
900    /// Use this variant when the contract needs to represent append failed; selecting it has no side effect by itself.
901    AppendFailed,
902}
903
904#[derive(Clone, Copy, Debug, Deserialize, Eq, PartialEq, Serialize)]
905#[serde(rename_all = "snake_case")]
906/// Enumerates the finite replay repair decision cases.
907/// Serialized names are part of the SDK contract; update fixtures when variants change.
908pub enum ReplayRepairDecision {
909    /// Use this variant when the contract needs to represent completed by dedupe proof; selecting it has no side effect by itself.
910    CompletedByDedupeProof,
911    /// Use this variant when the contract needs to represent requires host reconciliation; selecting it has no side effect by itself.
912    RequiresHostReconciliation,
913    /// Use this variant when the contract needs to represent unsafe pending; selecting it has no side effect by itself.
914    UnsafePending,
915}
916
917#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
918/// Carries the output delivery event record record payload for journal, event, or fixture surfaces.
919/// 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.
920pub struct OutputDeliveryEventRecord {
921    /// Kind discriminator for event kind.
922    /// Use it to route finite match arms without parsing display text.
923    pub event_kind: OutputDeliveryEventKind,
924    /// Stable delivery id used for typed lineage, lookup, or dedupe.
925    pub delivery_id: OutputDeliveryId,
926    /// Destination label or ref for this item; it is metadata and does not
927    /// deliver content by itself.
928    pub destination: DestinationRef,
929    /// Typed sink ref reference. Resolving or executing it is a separate
930    /// policy-gated step.
931    pub sink_ref: OutputSinkRef,
932    /// Dedupe policy or key for a side-effecting operation.
933    /// Replay and repair use it to avoid sending or executing the same effect twice.
934    pub dedupe_key: DedupeKey,
935    #[serde(skip_serializing_if = "Option::is_none")]
936    /// Stable source message id used for typed lineage, lookup, or dedupe.
937    pub source_message_id: Option<MessageId>,
938    /// Dispatch status used by this record or request.
939    pub dispatch_status: OutputDispatchStatus,
940    #[serde(skip_serializing_if = "Option::is_none")]
941    /// Typed ack ref reference. Resolving or executing it is a separate
942    /// policy-gated step.
943    pub ack_ref: Option<String>,
944    #[serde(skip_serializing_if = "Option::is_none")]
945    /// Optional reconciliation status value.
946    /// When absent, callers should use the documented default or skip that optional behavior.
947    pub reconciliation_status: Option<ReplayRepairDecision>,
948    /// Redacted human-readable summary safe for events, telemetry, and logs.
949    pub redacted_summary: String,
950}
951
952#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
953#[serde(tag = "record_type", content = "record", rename_all = "snake_case")]
954/// Enumerates the finite output delivery record cases.
955/// Serialized names are part of the SDK contract; update fixtures when variants change.
956#[expect(
957    clippy::large_enum_variant,
958    reason = "output-delivery records are durable serde payloads; direct variants stay explicit until a fixture-reviewed envelope migration"
959)]
960pub enum OutputDeliveryRecord {
961    /// Use this variant when the contract needs to represent intent; selecting it has no side effect by itself.
962    Intent(OutputDeliveryIntentRecord),
963    /// Use this variant when the contract needs to represent result; selecting it has no side effect by itself.
964    Result(OutputDeliveryResultRecord),
965    /// Use this variant when the contract needs to represent dedupe; selecting it has no side effect by itself.
966    Dedupe(OutputDeliveryDedupeRecord),
967    /// Use this variant when the contract needs to represent reconciliation; selecting it has no side effect by itself.
968    Reconciliation(OutputDeliveryReconciliationRecord),
969    /// Use this variant when the contract needs to represent event; selecting it has no side effect by itself.
970    Event(OutputDeliveryEventRecord),
971}
972
973#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
974/// Carries the output delivery journal base record payload for journal, event, or fixture surfaces.
975/// 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.
976pub struct OutputDeliveryJournalBase {
977    /// Journal seq used by this record or request.
978    pub journal_seq: u64,
979    /// Stable record id used for typed lineage, lookup, or dedupe.
980    pub record_id: String,
981    /// Run identifier used for lineage, filtering, replay, and dedupe.
982    pub run_id: RunId,
983    /// Agent identifier used for lineage, filtering, and ownership checks.
984    pub agent_id: AgentId,
985    #[serde(skip_serializing_if = "Option::is_none")]
986    /// Turn identifier for one loop turn within a run.
987    pub turn_id: Option<TurnId>,
988    #[serde(skip_serializing_if = "Option::is_none")]
989    /// Attempt identifier for retry, repair, provider, or tool execution
990    /// evidence.
991    pub attempt_id: Option<AttemptId>,
992    /// Source label or ref for this item; it is metadata and does not fetch
993    /// content by itself.
994    pub source: SourceRef,
995    /// Destination label or ref for this item; it is metadata and does not
996    /// deliver content by itself.
997    pub destination: DestinationRef,
998    /// Timestamp in milliseconds associated with this record.
999    /// Use it for ordering and diagnostics; durable causality still comes from ids and cursors.
1000    pub timestamp_millis: u64,
1001    /// Fingerprint of the runtime package snapshot in force when this value was produced.
1002    /// Use it for replay, dedupe, and package-lineage checks; the field is evidence and does
1003    /// not execute package behavior.
1004    pub runtime_package_fingerprint: RuntimePackageFingerprint,
1005    /// Stable redaction policy id used for typed lineage, lookup, or dedupe.
1006    pub redaction_policy_id: String,
1007}
1008
1009/// Builds the build output delivery dedupe key value.
1010/// This is data construction and performs no I/O, journal append, event publication, or process
1011pub fn build_output_delivery_dedupe_key(request: &OutputDeliveryRequest) -> DedupeKey {
1012    let content_refs = request
1013        .content_refs
1014        .iter()
1015        .map(|content_ref| content_ref.as_str())
1016        .collect::<Vec<_>>()
1017        .join(",");
1018    let policy_refs = request
1019        .policy_refs
1020        .iter()
1021        .map(|policy_ref| {
1022            format!(
1023                "{}:{}",
1024                policy_ref.as_str(),
1025                policy_ref.version.as_deref().unwrap_or("unversioned")
1026            )
1027        })
1028        .collect::<Vec<_>>()
1029        .join(",");
1030    let preimage = format!(
1031        "run={}|destination_kind={:?}|destination={}|sink={}|kind={}|message={}|validated={}|content={}|policies={}|package={}",
1032        request.run_id.as_str(),
1033        request.destination.kind,
1034        request.destination.as_str(),
1035        request.sink_ref.as_str(),
1036        request.delivery_kind.dedupe_fragment(),
1037        request
1038            .source_message_id
1039            .as_ref()
1040            .map(|id| id.as_str())
1041            .unwrap_or("none"),
1042        request
1043            .validated_output_id
1044            .as_ref()
1045            .map(|id| id.as_str())
1046            .unwrap_or("none"),
1047        content_refs,
1048        policy_refs,
1049        request.runtime_package_fingerprint.as_str(),
1050    );
1051    let digest = Sha256::digest(preimage.as_bytes());
1052    DedupeKey::new(format!("dedupe.output_delivery.{digest:x}"))
1053}
1054
1055#[expect(
1056    clippy::too_many_arguments,
1057    reason = "private output-delivery journal constructor mirrors effect and event lineage fields for auditability"
1058)]
1059fn output_delivery_effect_record(
1060    base: OutputDeliveryJournalBase,
1061    record_kind: JournalRecordKind,
1062    event_kind: &str,
1063    idempotency_key: Option<IdempotencyKey>,
1064    dedupe_key: Option<DedupeKey>,
1065    content_refs: Vec<ContentRef>,
1066    payload: JournalRecordPayload,
1067    subject_ref: EntityRef,
1068    destination: DestinationRef,
1069    _policy_refs: Vec<PolicyRef>,
1070    privacy: PrivacyClass,
1071) -> JournalRecord {
1072    let related_refs = content_refs
1073        .iter()
1074        .map(|content_ref| EntityRef::new(EntityKind::Content, content_ref.as_str()))
1075        .collect::<Vec<_>>();
1076    JournalRecord {
1077        journal_schema_version: JOURNAL_SCHEMA_VERSION,
1078        journal_seq: base.journal_seq,
1079        record_id: base.record_id,
1080        record_kind,
1081        run_id: base.run_id.clone(),
1082        agent_id: base.agent_id.clone(),
1083        turn_id: base.turn_id.clone(),
1084        attempt_id: base.attempt_id.clone(),
1085        subject_ref: subject_ref.clone(),
1086        related_refs: related_refs.clone(),
1087        causal_refs: Vec::new(),
1088        source: base.source.clone(),
1089        destination: Some(destination.clone()),
1090        correlation_keys: Vec::new(),
1091        tags: vec!["output_delivery".to_string()],
1092        delivery_semantics: "journal_backed".to_string(),
1093        event_index: EventIndexProjection {
1094            run_id: base.run_id,
1095            agent_id: base.agent_id,
1096            turn_id: base.turn_id,
1097            event_family: "output_delivery".to_string(),
1098            event_kind: event_kind.to_string(),
1099            source: base.source,
1100            destination: Some(destination),
1101            subject_ref,
1102            related_refs,
1103            correlation_keys: Vec::new(),
1104            tags: vec!["output_delivery".to_string()],
1105            privacy_class: privacy,
1106            delivery_semantics: "journal_backed".to_string(),
1107        },
1108        timestamp_millis: base.timestamp_millis,
1109        runtime_package_fingerprint: base.runtime_package_fingerprint.as_str().to_string(),
1110        privacy,
1111        content_refs,
1112        redaction_policy_id: base.redaction_policy_id,
1113        idempotency_key,
1114        dedupe_key,
1115        checkpoint_ref: None,
1116        payload,
1117    }
1118}