Skip to main content

agent_sdk_core/records/
subagent.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 subagent portion of that contract.
5//!
6use serde::{Deserialize, Serialize};
7
8use crate::{
9    domain::{
10        AgentId, ContentRef as ContentRefId, EffectId, EventId, IdempotencyKey, MessageId,
11        PolicyRef, PrivacyClass, RunId, ToolCallId, WakeConditionId,
12    },
13    effect::{EffectIntent, EffectKind, EffectResult, EffectTerminalStatus},
14    event::EventKind,
15    package::{ContextHandoffPolicy, RuntimePackageFingerprint, SubagentToolPolicy},
16    subagent::SubagentRequestId,
17};
18
19#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
20#[serde(tag = "record_type", content = "record", rename_all = "snake_case")]
21/// Enumerates the finite subagent record cases.
22/// Serialized names are part of the SDK contract; update fixtures when variants change.
23pub enum SubagentRecord {
24    /// Use this variant when the contract needs to represent started; selecting it has no side effect by itself.
25    Started(SubagentStartedRecord),
26    /// Use this variant when the contract needs to represent handoff; selecting it has no side effect by itself.
27    Handoff(SubagentHandoffRecord),
28    /// Use this variant when the contract needs to represent wrapped event; selecting it has no side effect by itself.
29    WrappedEvent(SubagentWrappedEventRecord),
30    /// Use this variant when the contract needs to represent usage rolled up; selecting it has no side effect by itself.
31    UsageRolledUp(SubagentUsageRolledUpRecord),
32    /// Use this variant when the contract needs to represent completed; selecting it has no side effect by itself.
33    Completed(SubagentCompletedRecord),
34    /// Use this variant when the contract needs to represent child lifecycle; selecting it has no side effect by itself.
35    ChildLifecycle(ChildLifecycleRecord),
36}
37
38impl SubagentRecord {
39    /// Returns the kind currently held by this value.
40    /// This is data-only and does not perform I/O, call host ports, append journals, publish
41    /// events, or start processes.
42    pub fn kind(&self) -> &'static str {
43        match self {
44            Self::Started(_) => "subagent_started",
45            Self::Handoff(_) => "subagent_handoff",
46            Self::WrappedEvent(_) => "subagent_event",
47            Self::UsageRolledUp(_) => "subagent_usage_rolled_up",
48            Self::Completed(_) => "subagent_completed",
49            Self::ChildLifecycle(_) => "child_lifecycle",
50        }
51    }
52}
53
54#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
55/// Carries the run journal ref record payload for journal, event, or fixture surfaces.
56/// 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.
57pub struct RunJournalRef {
58    /// Run identifier used for lineage, filtering, replay, and dedupe.
59    pub run_id: RunId,
60    /// Typed journal partition ref reference. Resolving or executing it is a
61    /// separate policy-gated step.
62    pub journal_partition_ref: String,
63}
64
65impl RunJournalRef {
66    /// Builds the for run value.
67    /// This is data construction and performs no I/O, journal append, event publication, or
68    /// process work.
69    pub fn for_run(run_id: RunId) -> Self {
70        Self {
71            journal_partition_ref: format!("journal.run.{}", run_id.as_str()),
72            run_id,
73        }
74    }
75}
76
77#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
78/// Carries the subagent started record record payload for journal, event, or fixture surfaces.
79/// 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.
80pub struct SubagentStartedRecord {
81    /// Stable request id used for typed lineage, lookup, or dedupe.
82    pub request_id: SubagentRequestId,
83    /// Stable parent run id used for typed lineage, lookup, or dedupe.
84    pub parent_run_id: RunId,
85    /// Stable child run id used for typed lineage, lookup, or dedupe.
86    pub child_run_id: RunId,
87    /// Stable parent tool call id used for typed lineage, lookup, or dedupe.
88    pub parent_tool_call_id: ToolCallId,
89    /// Stable child agent id used for typed lineage, lookup, or dedupe.
90    pub child_agent_id: AgentId,
91    /// Deterministic child package fingerprint used for stale checks, package
92    /// evidence, or replay comparisons.
93    pub child_package_fingerprint: RuntimePackageFingerprint,
94    /// Typed child journal ref reference. Resolving or executing it is a
95    /// separate policy-gated step.
96    pub child_journal_ref: RunJournalRef,
97    /// Handoff policy used by this record or request.
98    pub handoff_policy: ContextHandoffPolicy,
99    /// Tool policy used by this record or request.
100    pub tool_policy: SubagentToolPolicy,
101    #[serde(default, skip_serializing_if = "Vec::is_empty")]
102    /// Identifiers used to select or correlate message values.
103    /// Use them for typed lookup, filtering, or lineage instead of stringly typed matching.
104    pub message_ids: Vec<MessageId>,
105    #[serde(default, skip_serializing_if = "Vec::is_empty")]
106    /// Identifiers used to select or correlate wake condition values.
107    /// Use them for typed lookup, filtering, or lineage instead of stringly typed matching.
108    pub wake_condition_ids: Vec<WakeConditionId>,
109    /// Effect intent used by this record or request.
110    pub effect_intent: EffectIntent,
111}
112
113#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
114/// Carries the subagent handoff record record payload for journal, event, or fixture surfaces.
115/// Creating or cloning it only preserves serialized SDK state; append, publish, replay, or export effects are documented on the runtime and port methods that store it.
116pub struct SubagentHandoffRecord {
117    /// Stable request id used for typed lineage, lookup, or dedupe.
118    pub request_id: SubagentRequestId,
119    /// Stable parent run id used for typed lineage, lookup, or dedupe.
120    pub parent_run_id: RunId,
121    /// Stable child run id used for typed lineage, lookup, or dedupe.
122    pub child_run_id: RunId,
123    /// Handoff policy used by this record or request.
124    pub handoff_policy: ContextHandoffPolicy,
125    #[serde(default, skip_serializing_if = "Vec::is_empty")]
126    /// Typed selected content refs references. Resolving them is separate
127    /// from constructing this record.
128    pub selected_content_refs: Vec<ContentRefId>,
129    #[serde(skip_serializing_if = "Option::is_none")]
130    /// Typed projection audit ref reference. Resolving or executing it is a
131    /// separate policy-gated step.
132    pub projection_audit_ref: Option<String>,
133    #[serde(default, skip_serializing_if = "Vec::is_empty")]
134    /// Policy references that govern admission, projection, execution, or
135    /// delivery.
136    pub policy_refs: Vec<PolicyRef>,
137    /// Stable redaction policy id used for typed lineage, lookup, or dedupe.
138    pub redaction_policy_id: String,
139}
140
141#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
142/// Carries the subagent wrapped event record record payload for journal, event, or fixture surfaces.
143/// 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.
144pub struct SubagentWrappedEventRecord {
145    /// Stable parent run id used for typed lineage, lookup, or dedupe.
146    pub parent_run_id: RunId,
147    /// Stable child run id used for typed lineage, lookup, or dedupe.
148    pub child_run_id: RunId,
149    /// Stable child agent id used for typed lineage, lookup, or dedupe.
150    pub child_agent_id: AgentId,
151    /// Stable original child event id used for typed lineage, lookup, or
152    /// dedupe.
153    pub original_child_event_id: EventId,
154    /// Kind discriminator for original child event kind.
155    /// Use it to route finite match arms without parsing display text.
156    pub original_child_event_kind: EventKind,
157    /// Typed wrapped event ref reference. Resolving or executing it is a
158    /// separate policy-gated step.
159    pub wrapped_event_ref: String,
160    #[serde(skip_serializing_if = "Option::is_none")]
161    /// Cursor identifying a replay, export, or subscription position.
162    /// Use it to resume without widening the original scope.
163    pub child_journal_cursor: Option<crate::domain::JournalCursor>,
164    /// Typed child journal ref reference. Resolving or executing it is a
165    /// separate policy-gated step.
166    pub child_journal_ref: RunJournalRef,
167    /// Privacy class used for projection, telemetry, and raw-content access
168    /// decisions.
169    pub privacy: PrivacyClass,
170}
171
172#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
173/// Carries the subagent usage rolled up record record payload for journal, event, or fixture surfaces.
174/// 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.
175pub struct SubagentUsageRolledUpRecord {
176    /// Stable child run id used for typed lineage, lookup, or dedupe.
177    pub child_run_id: RunId,
178    /// Stable parent run id used for typed lineage, lookup, or dedupe.
179    pub parent_run_id: RunId,
180    /// Typed child usage ref reference. Resolving or executing it is a
181    /// separate policy-gated step.
182    pub child_usage_ref: String,
183    /// Typed parent usage ref reference. Resolving or executing it is a
184    /// separate policy-gated step.
185    pub parent_usage_ref: String,
186    /// Input tokens used by this record or request.
187    pub input_tokens: u32,
188    /// Output tokens used by this record or request.
189    pub output_tokens: u32,
190    /// Total tokens used by this record or request.
191    pub total_tokens: u32,
192    #[serde(skip_serializing_if = "Option::is_none")]
193    /// Optional cost micros value.
194    /// When absent, callers should use the documented default or skip that optional behavior.
195    pub cost_micros: Option<u64>,
196    #[serde(skip_serializing_if = "Option::is_none")]
197    /// Currency code for the cost amount.
198    /// Cost accounting uses it with amount micros and rate-table version.
199    pub currency: Option<String>,
200    /// Terminal status used by this record or request.
201    pub terminal_status: SubagentTerminalStatus,
202}
203
204#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
205/// Carries the subagent completed record record payload for journal, event, or fixture surfaces.
206/// 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.
207pub struct SubagentCompletedRecord {
208    /// Stable child run id used for typed lineage, lookup, or dedupe.
209    pub child_run_id: RunId,
210    /// Stable parent run id used for typed lineage, lookup, or dedupe.
211    pub parent_run_id: RunId,
212    /// Terminal status used by this record or request.
213    pub terminal_status: SubagentTerminalStatus,
214    #[serde(skip_serializing_if = "Option::is_none")]
215    /// Typed result ref reference. Resolving or executing it is a separate
216    /// policy-gated step.
217    pub result_ref: Option<ContentRefId>,
218    #[serde(skip_serializing_if = "Option::is_none")]
219    /// Typed error ref reference. Resolving or executing it is a separate
220    /// policy-gated step.
221    pub error_ref: Option<String>,
222    /// Policy outcome used by this record or request.
223    pub policy_outcome: String,
224    /// Effect result used by this record or request.
225    pub effect_result: EffectResult,
226}
227
228#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
229/// Carries the child lifecycle record record payload for journal, event, or fixture surfaces.
230/// 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.
231pub struct ChildLifecycleRecord {
232    /// Stable child run id used for typed lineage, lookup, or dedupe.
233    pub child_run_id: RunId,
234    /// Stable parent run id used for typed lineage, lookup, or dedupe.
235    pub parent_run_id: RunId,
236    /// Kind discriminator for artifact kind.
237    /// Use it to route finite match arms without parsing display text.
238    pub artifact_kind: ChildArtifactKind,
239    /// Action used by this record or request.
240    pub action: ChildLifecycleAction,
241    /// Finite status for this record or lifecycle stage.
242    pub status: ChildLifecycleStatus,
243    #[serde(default, skip_serializing_if = "Vec::is_empty")]
244    /// Policy references that govern admission, projection, execution, or
245    /// delivery.
246    pub policy_refs: Vec<PolicyRef>,
247    #[serde(skip_serializing_if = "Option::is_none")]
248    /// Typed host ack ref reference. Resolving or executing it is a separate
249    /// policy-gated step.
250    pub host_ack_ref: Option<String>,
251    #[serde(skip_serializing_if = "Option::is_none")]
252    /// Typed reclaim policy ref reference. Resolving or executing it is a
253    /// separate policy-gated step.
254    pub reclaim_policy_ref: Option<PolicyRef>,
255    #[serde(skip_serializing_if = "Option::is_none")]
256    /// Optional effect intent value.
257    /// When absent, callers should use the documented default or skip that optional behavior.
258    pub effect_intent: Option<EffectIntent>,
259    #[serde(skip_serializing_if = "Option::is_none")]
260    /// Optional effect result value.
261    /// When absent, callers should use the documented default or skip that optional behavior.
262    pub effect_result: Option<EffectResult>,
263    /// Idempotency setting or key for deduping retries.
264    /// Use it to prevent duplicate side effects during replay or repair.
265    pub idempotency_key: IdempotencyKey,
266}
267
268impl ChildLifecycleRecord {
269    /// Builds the shutdown intent record or result value.
270    /// This is data-only and does not perform I/O, call host ports, append journals, publish
271    /// events, or start processes.
272    pub fn shutdown_intent(
273        parent_run_id: RunId,
274        child_run_id: RunId,
275        policy_refs: Vec<PolicyRef>,
276        idempotency_key: IdempotencyKey,
277    ) -> Self {
278        let effect_id = shutdown_effect_id(&child_run_id);
279        let mut intent = EffectIntent::new(
280            effect_id,
281            EffectKind::ChildArtifactShutdown,
282            crate::domain::EntityRef::new(
283                crate::domain::EntityKind::SubagentRun,
284                child_run_id.as_str(),
285            ),
286            crate::domain::SourceRef::with_kind(
287                crate::domain::SourceKind::Sdk,
288                "source.sdk.subagent",
289            ),
290            "parent requested child subagent shutdown",
291        );
292        intent.policy_refs = policy_refs.clone();
293        intent.idempotency_key = Some(idempotency_key.clone());
294
295        Self {
296            child_run_id,
297            parent_run_id,
298            artifact_kind: ChildArtifactKind::SubagentRun,
299            action: ChildLifecycleAction::ShutdownIntent,
300            status: ChildLifecycleStatus::Requested,
301            policy_refs,
302            host_ack_ref: None,
303            reclaim_policy_ref: None,
304            effect_intent: Some(intent),
305            effect_result: None,
306            idempotency_key,
307        }
308    }
309
310    /// Builds the shutdown completed record or result value.
311    /// This is data-only and does not perform I/O, call host ports, append journals, publish
312    /// events, or start processes.
313    pub fn shutdown_completed(&self) -> Self {
314        let mut completed = self.clone();
315        completed.action = ChildLifecycleAction::ShutdownCompleted;
316        completed.status = ChildLifecycleStatus::Completed;
317        completed.effect_result = Some(EffectResult {
318            effect_id: shutdown_effect_id(&completed.child_run_id),
319            terminal_status: EffectTerminalStatus::Cancelled,
320            external_operation_id: None,
321            reconciliation_ref: None,
322            error_ref: None,
323            content_refs: Vec::new(),
324            redacted_summary: "parent-owned child subagent shutdown completed".to_string(),
325        });
326        completed
327    }
328
329    /// Detach intent.
330    /// This is data-only and does not perform I/O, call host ports, append journals, publish
331    /// events, or start processes.
332    pub fn detach_intent(
333        parent_run_id: RunId,
334        child_run_id: RunId,
335        policy_refs: Vec<PolicyRef>,
336        host_ack_ref: String,
337        reclaim_policy_ref: PolicyRef,
338        idempotency_key: IdempotencyKey,
339    ) -> Self {
340        let effect_id = detach_effect_id(&child_run_id);
341        let mut intent = EffectIntent::new(
342            effect_id,
343            EffectKind::DetachTransfer,
344            crate::domain::EntityRef::new(
345                crate::domain::EntityKind::SubagentRun,
346                child_run_id.as_str(),
347            ),
348            crate::domain::SourceRef::with_kind(
349                crate::domain::SourceKind::Sdk,
350                "source.sdk.subagent",
351            ),
352            "parent requested explicit child subagent detach",
353        );
354        intent.policy_refs = policy_refs.clone();
355        intent.idempotency_key = Some(idempotency_key.clone());
356
357        Self {
358            child_run_id,
359            parent_run_id,
360            artifact_kind: ChildArtifactKind::SubagentRun,
361            action: ChildLifecycleAction::DetachIntent,
362            status: ChildLifecycleStatus::Requested,
363            policy_refs,
364            host_ack_ref: Some(host_ack_ref),
365            reclaim_policy_ref: Some(reclaim_policy_ref),
366            effect_intent: Some(intent),
367            effect_result: None,
368            idempotency_key,
369        }
370    }
371
372    /// Detached.
373    /// This is data-only and does not perform I/O, call host ports, append journals, publish
374    /// events, or start processes.
375    pub fn detached(&self) -> Self {
376        let mut detached = self.clone();
377        detached.action = ChildLifecycleAction::Detached;
378        detached.status = ChildLifecycleStatus::Detached;
379        detached.effect_result = Some(EffectResult {
380            effect_id: detach_effect_id(&detached.child_run_id),
381            terminal_status: EffectTerminalStatus::Completed,
382            external_operation_id: detached.host_ack_ref.clone(),
383            reconciliation_ref: detached
384                .reclaim_policy_ref
385                .as_ref()
386                .map(|policy| policy.as_str().to_string()),
387            error_ref: None,
388            content_refs: Vec::new(),
389            redacted_summary: "parent-owned child subagent detach completed".to_string(),
390        });
391        detached
392    }
393}
394
395#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
396#[serde(rename_all = "snake_case")]
397/// Enumerates the finite child artifact kind cases.
398/// Serialized names are part of the SDK contract; update fixtures when variants change.
399pub enum ChildArtifactKind {
400    /// Use this variant when the contract needs to represent subagent run; selecting it has no side effect by itself.
401    SubagentRun,
402}
403
404#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
405#[serde(rename_all = "snake_case")]
406/// Enumerates the finite child lifecycle action cases.
407/// Serialized names are part of the SDK contract; update fixtures when variants change.
408pub enum ChildLifecycleAction {
409    /// Use this variant when the contract needs to represent shutdown intent; selecting it has no side effect by itself.
410    ShutdownIntent,
411    /// Use this variant when the contract needs to represent shutdown completed; selecting it has no side effect by itself.
412    ShutdownCompleted,
413    /// Use this variant when the contract needs to represent detach intent; selecting it has no side effect by itself.
414    DetachIntent,
415    /// Use this variant when the contract needs to represent detached; selecting it has no side effect by itself.
416    Detached,
417}
418
419#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
420#[serde(rename_all = "snake_case")]
421/// Enumerates the finite child lifecycle status cases.
422/// Serialized names are part of the SDK contract; update fixtures when variants change.
423pub enum ChildLifecycleStatus {
424    /// Use this variant when the contract needs to represent requested; selecting it has no side effect by itself.
425    Requested,
426    /// Use this variant when the contract needs to represent completed; selecting it has no side effect by itself.
427    Completed,
428    /// Use this variant when the contract needs to represent detached; selecting it has no side effect by itself.
429    Detached,
430    /// Use this variant when the contract needs to represent failed; selecting it has no side effect by itself.
431    Failed,
432}
433
434#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
435#[serde(rename_all = "snake_case")]
436/// Enumerates the finite subagent terminal status cases.
437/// Serialized names are part of the SDK contract; update fixtures when variants change.
438pub enum SubagentTerminalStatus {
439    /// Use this variant when the contract needs to represent completed; selecting it has no side effect by itself.
440    Completed,
441    /// Use this variant when the contract needs to represent failed; selecting it has no side effect by itself.
442    Failed,
443    /// Use this variant when the contract needs to represent cancelled; selecting it has no side effect by itself.
444    Cancelled,
445    /// Use this variant when the contract needs to represent detached; selecting it has no side effect by itself.
446    Detached,
447}
448
449fn shutdown_effect_id(child_run_id: &RunId) -> EffectId {
450    EffectId::new(format!(
451        "effect.subagent.shutdown.{}",
452        child_run_id.as_str()
453    ))
454}
455
456fn detach_effect_id(child_run_id: &RunId) -> EffectId {
457    EffectId::new(format!("effect.subagent.detach.{}", child_run_id.as_str()))
458}