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}