Skip to main content

agent_sdk_core/package/
subagent.rs

1//! Runtime-package records and builders. Use these items to describe the immutable
2//! per-run package that freezes provider route, capabilities, policies, sidecars,
3//! catalogs, and fingerprints. Builders are data-only and must not perform discovery
4//! or execution side effects. This file contains the subagent portion of that
5//! contract.
6//!
7use serde::{Deserialize, Serialize};
8use sha2::{Digest, Sha256};
9
10use crate::{
11    capability::{CapabilityId, CapabilityKind, CapabilitySpec, PackageSidecarRef},
12    domain::{AgentError, AgentId, ContentRef as ContentRefId, PolicyKind, PolicyRef},
13    package::{
14        AgentSnapshot, PackageSidecarSnapshot, ProviderRouteSnapshot, RuntimePackage,
15        RuntimePackageFingerprint, RuntimePackageId,
16    },
17};
18
19#[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq, Serialize)]
20#[serde(tag = "type", rename_all = "snake_case")]
21/// Enumerates the finite context handoff policy cases.
22/// Serialized names are part of the SDK contract; update fixtures when variants change.
23pub enum ContextHandoffPolicy {
24    /// Use this variant when the contract needs to represent none; selecting it has no side effect by itself.
25    #[default]
26    None,
27    /// Use this variant when the contract needs to represent summary only; selecting it has no side effect by itself.
28    SummaryOnly {
29        /// Typed summary ref reference. Resolving or executing it is a
30        /// separate policy-gated step.
31        summary_ref: ContentRefId,
32        /// Maximum allowed tokens.
33        /// Use it to keep execution, output, or diagnostics bounded.
34        max_tokens: u32,
35        /// Policy reference that must be resolved by the host or runtime
36        /// before execution.
37        policy_ref: PolicyRef,
38    },
39    /// Use this variant when the contract needs to represent selected refs; selecting it has no side effect by itself.
40    SelectedRefs {
41        /// References associated with refs.
42        /// Resolve them through the appropriate registry or content store before using
43        /// referenced data.
44        refs: Vec<ContentRefId>,
45        /// Policy reference that must be resolved by the host or runtime
46        /// before execution.
47        policy_ref: PolicyRef,
48    },
49    /// Use this variant when the contract needs to represent full history with policy; selecting it has no side effect by itself.
50    FullHistoryWithPolicy {
51        /// Policy reference that must be resolved by the host or runtime
52        /// before execution.
53        policy_ref: PolicyRef,
54        /// Whether child context handoff must include provider-projection audit evidence.
55        /// Use this to fail closed when subagent context would otherwise cross a policy
56        /// boundary without audit proof.
57        projection_audit_required: bool,
58    },
59}
60
61impl ContextHandoffPolicy {
62    /// Validates the package::subagent invariants and returns a typed
63    /// error on failure. Validation is pure and does not perform I/O,
64    /// dispatch, journal appends, or adapter calls.
65    pub fn validate(&self) -> Result<(), AgentError> {
66        match self {
67            Self::None => Ok(()),
68            Self::SummaryOnly {
69                max_tokens,
70                policy_ref,
71                ..
72            } => {
73                if *max_tokens == 0 {
74                    return Err(AgentError::contract_violation(
75                        "summary handoff requires a positive token budget",
76                    ));
77                }
78                validate_non_host_policy(policy_ref, "summary handoff")
79            }
80            Self::SelectedRefs { refs, policy_ref } => {
81                if refs.is_empty() {
82                    return Err(AgentError::contract_violation(
83                        "selected refs handoff requires at least one content ref",
84                    ));
85                }
86                validate_non_host_policy(policy_ref, "selected refs handoff")
87            }
88            Self::FullHistoryWithPolicy {
89                policy_ref,
90                projection_audit_required,
91            } => {
92                if !projection_audit_required {
93                    return Err(AgentError::contract_violation(
94                        "full history handoff requires a projection audit",
95                    ));
96                }
97                validate_non_host_policy(policy_ref, "full history handoff")
98            }
99        }
100    }
101
102    /// Returns policy refs for the current value.
103    /// This is a read-only or data-construction helper unless the method body explicitly calls
104    /// a port or store.
105    pub fn policy_refs(&self) -> Vec<PolicyRef> {
106        match self {
107            Self::None => Vec::new(),
108            Self::SummaryOnly { policy_ref, .. }
109            | Self::SelectedRefs { policy_ref, .. }
110            | Self::FullHistoryWithPolicy { policy_ref, .. } => vec![policy_ref.clone()],
111        }
112    }
113
114    /// Returns selected content refs for callers that need to inspect the contract state.
115    /// This is data-only and does not perform I/O, call host ports, append journals, publish
116    /// events, or start processes.
117    pub fn selected_content_refs(&self) -> Vec<ContentRefId> {
118        match self {
119            Self::None | Self::FullHistoryWithPolicy { .. } => Vec::new(),
120            Self::SummaryOnly { summary_ref, .. } => vec![summary_ref.clone()],
121            Self::SelectedRefs { refs, .. } => refs.clone(),
122        }
123    }
124
125    /// Returns variant name for the current value.
126    /// This is a read-only or data-construction helper unless the method body explicitly calls
127    /// a port or store.
128    pub fn variant_name(&self) -> &'static str {
129        match self {
130            Self::None => "none",
131            Self::SummaryOnly { .. } => "summary_only",
132            Self::SelectedRefs { .. } => "selected_refs",
133            Self::FullHistoryWithPolicy { .. } => "full_history_with_policy",
134        }
135    }
136}
137
138#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
139#[serde(rename_all = "snake_case")]
140/// Enumerates the finite route inheritance mode cases.
141/// Serialized names are part of the SDK contract; update fixtures when variants change.
142pub enum RouteInheritanceMode {
143    /// Use this variant when the contract needs to represent inherit parent; selecting it has no side effect by itself.
144    InheritParent,
145    /// Use this variant when the contract needs to represent explicit override only; selecting it has no side effect by itself.
146    ExplicitOverrideOnly,
147}
148
149#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
150#[serde(tag = "type", rename_all = "snake_case")]
151/// Enumerates the finite subagent route policy cases.
152/// Serialized names are part of the SDK contract; update fixtures when variants change.
153pub enum SubagentRoutePolicy {
154    /// Use this variant when the contract needs to represent inherit parent; selecting it has no side effect by itself.
155    InheritParent,
156    /// Use this variant when the contract needs to represent use allowed override; selecting it has no side effect by itself.
157    UseAllowedOverride {
158        /// Stable route id used for typed lineage, lookup, or dedupe.
159        route_id: String,
160        /// Stable model id used for typed lineage, lookup, or dedupe.
161        model_id: String,
162    },
163}
164
165impl SubagentRoutePolicy {
166    /// Returns selected route for the current value.
167    /// This is a read-only or data-construction helper unless the method body explicitly calls
168    /// a port or store.
169    pub fn selected_route(
170        &self,
171        parent: &ProviderRouteSnapshot,
172        child_policy: &ChildRuntimePackagePolicy,
173    ) -> Result<ProviderRouteSnapshot, AgentError> {
174        match self {
175            Self::InheritParent => Ok(parent.clone()),
176            Self::UseAllowedOverride { route_id, model_id } => {
177                if !child_policy.allowed_route_overrides.contains(route_id) {
178                    return Err(AgentError::contract_violation(
179                        "child provider route override is not allowed by package policy",
180                    ));
181                }
182                if route_id.is_empty() || model_id.is_empty() {
183                    return Err(AgentError::missing_required_field(
184                        "subagent.route_override.route_id_or_model_id",
185                    ));
186                }
187                Ok(ProviderRouteSnapshot::new(route_id, model_id))
188            }
189        }
190    }
191}
192
193#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
194/// Describes the child runtime package policy portion of a runtime package snapshot.
195/// Use it when package authors or tests need explicit package configuration; validation and activation happen in package/runtime coordinators.
196pub struct ChildRuntimePackagePolicy {
197    /// Source parent package used by this record or request.
198    pub source_parent_package: RuntimePackageFingerprint,
199    /// Inherit provider route used by this record or request.
200    pub inherit_provider_route: RouteInheritanceMode,
201    #[serde(default, skip_serializing_if = "Vec::is_empty")]
202    /// Allowlist for this policy or contract.
203    /// Validation uses it to reject undeclared or policy-denied values.
204    pub allowed_route_overrides: Vec<String>,
205    /// Whether strip recursive subagents is enabled.
206    /// Policy, validation, or routing code uses this flag to choose the explicit behavior.
207    pub strip_recursive_subagents: bool,
208    /// Allowlist for this policy or contract.
209    /// Validation uses it to reject undeclared or policy-denied values.
210    pub strip_disallowed_tools: bool,
211    /// Child lifecycle bounds used by this record or request.
212    pub child_lifecycle_bounds: PolicyRef,
213    /// Typed redaction policy ref reference. Resolving or executing it is a
214    /// separate policy-gated step.
215    pub redaction_policy_ref: PolicyRef,
216    #[serde(default, skip_serializing_if = "Vec::is_empty")]
217    /// Identifiers used to select or correlate parent control tool values.
218    /// Use them for typed lookup, filtering, or lineage instead of stringly typed matching.
219    pub parent_control_tool_ids: Vec<CapabilityId>,
220}
221
222impl ChildRuntimePackagePolicy {
223    /// Returns an updated value with strip recursive defaults configured.
224    /// This is data-only and does not perform I/O, call host ports, append journals, publish
225    /// events, or start processes.
226    pub fn strip_recursive_defaults(source_parent_package: RuntimePackageFingerprint) -> Self {
227        Self {
228            source_parent_package,
229            inherit_provider_route: RouteInheritanceMode::InheritParent,
230            allowed_route_overrides: Vec::new(),
231            strip_recursive_subagents: true,
232            strip_disallowed_tools: true,
233            child_lifecycle_bounds: PolicyRef::with_kind(
234                PolicyKind::RuntimePackage,
235                "policy.child.parent_owned",
236            ),
237            redaction_policy_ref: PolicyRef::with_kind(
238                PolicyKind::Redaction,
239                "policy.redaction.subagent.default",
240            ),
241            parent_control_tool_ids: default_parent_control_tool_ids(),
242        }
243    }
244}
245
246#[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq, Serialize)]
247#[serde(tag = "type", rename_all = "snake_case")]
248/// Enumerates the finite subagent tool policy cases.
249/// Serialized names are part of the SDK contract; update fixtures when variants change.
250pub enum SubagentToolPolicy {
251    /// Use this variant when the contract needs to represent inherit allowlist; selecting it has no side effect by itself.
252    InheritAllowlist,
253    /// Use this variant when the contract needs to represent read only; selecting it has no side effect by itself.
254    #[default]
255    ReadOnly,
256    /// Use this variant when the contract needs to represent no tools; selecting it has no side effect by itself.
257    NoTools,
258    /// Use this variant when the contract needs to represent custom allowlist; selecting it has no side effect by itself.
259    CustomAllowlist {
260        /// Capability identifiers affected by this package or sidecar record.
261        capability_ids: Vec<CapabilityId>,
262    },
263}
264
265impl SubagentToolPolicy {
266    fn retains(&self, capability: &CapabilitySpec) -> bool {
267        match self {
268            Self::InheritAllowlist | Self::ReadOnly => true,
269            Self::NoTools => capability.kind != CapabilityKind::Tool,
270            Self::CustomAllowlist { capability_ids } => {
271                capability.kind != CapabilityKind::Tool
272                    || capability_ids.contains(&capability.capability_id)
273            }
274        }
275    }
276}
277
278#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
279/// Describes the depth budget portion of a runtime package snapshot.
280/// Use it when package authors or tests need explicit package configuration; validation and activation happen in package/runtime coordinators.
281pub struct DepthBudget {
282    /// Current depth used by this record or request.
283    pub current_depth: u32,
284    /// Maximum allowed depth.
285    /// Use it to keep execution, output, or diagnostics bounded.
286    pub max_depth: u32,
287    /// Maximum allowed children.
288    /// Use it to keep execution, output, or diagnostics bounded.
289    pub max_children: u32,
290}
291
292impl DepthBudget {
293    /// Returns max depth for the current value.
294    /// This is a read-only or data-construction helper unless the method body explicitly calls
295    /// a port or store.
296    pub fn max_depth(max_depth: u32) -> Self {
297        Self {
298            current_depth: 0,
299            max_depth,
300            max_children: 1,
301        }
302    }
303
304    /// Validates the package::subagent invariants and returns a typed
305    /// error on failure. Validation is pure and does not perform I/O,
306    /// dispatch, journal appends, or adapter calls.
307    pub fn validate_child_start(&self) -> Result<(), AgentError> {
308        if self.max_depth == 0 || self.current_depth >= self.max_depth {
309            return Err(AgentError::contract_violation(
310                "subagent depth budget exhausted before child start",
311            ));
312        }
313        if self.max_children == 0 {
314            return Err(AgentError::contract_violation(
315                "subagent child count budget exhausted before child start",
316            ));
317        }
318        Ok(())
319    }
320
321    /// Returns child budget for the current value.
322    /// This is a read-only or data-construction helper unless the method body explicitly calls
323    /// a port or store.
324    pub fn child_budget(&self) -> Self {
325        Self {
326            current_depth: self.current_depth + 1,
327            max_depth: self.max_depth,
328            max_children: self.max_children,
329        }
330    }
331}
332
333#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
334/// Describes the child runtime package portion of a runtime package snapshot.
335/// Use it when package authors or tests need explicit package configuration; validation and activation happen in package/runtime coordinators.
336pub struct ChildRuntimePackage {
337    /// Package used by this record or request.
338    pub package: RuntimePackage,
339    /// Deterministic fingerprint for package, event, telemetry, or validation
340    /// evidence.
341    pub fingerprint: RuntimePackageFingerprint,
342    /// Strip manifest used by this record or request.
343    pub strip_manifest: ChildPackageStripManifest,
344}
345
346#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
347/// Describes the child package strip manifest portion of a runtime package snapshot.
348/// Use it when package authors or tests need explicit package configuration; validation and activation happen in package/runtime coordinators.
349pub struct ChildPackageStripManifest {
350    /// Deterministic parent package fingerprint used for stale checks,
351    /// package evidence, or replay comparisons.
352    pub parent_package_fingerprint: RuntimePackageFingerprint,
353    /// Stable child agent id used for typed lineage, lookup, or dedupe.
354    pub child_agent_id: AgentId,
355    /// Stable selected provider route id used for typed lineage, lookup, or
356    /// dedupe.
357    pub selected_provider_route_id: String,
358    /// Handoff policy variant used by this record or request.
359    pub handoff_policy_variant: String,
360    /// Tool policy used by this record or request.
361    pub tool_policy: SubagentToolPolicy,
362    /// Whether recursive subagent strip is enabled.
363    /// Policy, validation, or routing code uses this flag to choose the explicit behavior.
364    pub recursive_subagent_strip: bool,
365    #[serde(default, skip_serializing_if = "Vec::is_empty")]
366    /// Identifiers used to select or correlate stripped capability values.
367    /// Use them for typed lookup, filtering, or lineage instead of stringly typed matching.
368    pub stripped_capability_ids: Vec<CapabilityId>,
369    #[serde(default, skip_serializing_if = "Vec::is_empty")]
370    /// Identifiers used to select or correlate retained capability values.
371    /// Use them for typed lookup, filtering, or lineage instead of stringly typed matching.
372    pub retained_capability_ids: Vec<CapabilityId>,
373    /// Typed lifecycle policy ref reference. Resolving or executing it is a
374    /// separate policy-gated step.
375    pub lifecycle_policy_ref: PolicyRef,
376    /// Typed redaction policy ref reference. Resolving or executing it is a
377    /// separate policy-gated step.
378    pub redaction_policy_ref: PolicyRef,
379}
380
381impl ChildPackageStripManifest {
382    /// Computes the stable content hash for this package::subagent
383    /// value. The computation is deterministic and side-effect free so
384    /// it can be used in package, journal, or test evidence.
385    pub fn content_hash(&self) -> Result<String, AgentError> {
386        let bytes = serde_json::to_vec(self).map_err(|error| {
387            AgentError::contract_violation(format!(
388                "child package strip manifest serialization failed: {error}"
389            ))
390        })?;
391        Ok(format!("sha256:{:x}", Sha256::digest(bytes)))
392    }
393}
394
395/// Builds the build child runtime package value.
396/// This is data construction and performs no I/O, journal append, event publication, or process
397pub fn build_child_runtime_package(
398    parent: &RuntimePackage,
399    child_agent_id: AgentId,
400    route_policy: &SubagentRoutePolicy,
401    handoff_policy: &ContextHandoffPolicy,
402    child_policy: &ChildRuntimePackagePolicy,
403    tool_policy: &SubagentToolPolicy,
404) -> Result<ChildRuntimePackage, AgentError> {
405    parent.validate()?;
406    handoff_policy.validate()?;
407
408    if !child_policy.strip_recursive_subagents {
409        return Err(AgentError::contract_violation(
410            "recursive subagent tools are denied by the core SDK contract",
411        ));
412    }
413
414    let parent_fingerprint = parent.fingerprint()?;
415    if parent_fingerprint != child_policy.source_parent_package {
416        return Err(AgentError::contract_violation(
417            "child package policy source parent fingerprint does not match parent package",
418        ));
419    }
420
421    let selected_route = route_policy.selected_route(&parent.provider_route, child_policy)?;
422    let mut child = parent.clone();
423    child.package_id = child_package_id(&child_agent_id, &parent_fingerprint);
424    child.agent = AgentSnapshot {
425        agent_id: child_agent_id.clone(),
426        name: child_agent_id.as_str().to_string(),
427        default_behavior_refs: parent.agent.default_behavior_refs.clone(),
428    };
429    child.provider_route = selected_route.clone();
430    child.child_lifecycle.policy_ref = child_policy.child_lifecycle_bounds.clone();
431    child.child_lifecycle.detach_policy_ref = child_policy.child_lifecycle_bounds.clone();
432
433    let mut stripped = Vec::new();
434    let mut retained = Vec::new();
435    child.capabilities.retain(|capability| {
436        let strip = is_recursive_subagent_capability(capability, child_policy)
437            || (child_policy.strip_disallowed_tools && !tool_policy.retains(capability));
438        if strip {
439            stripped.push(capability.capability_id.clone());
440            false
441        } else {
442            retained.push(capability.capability_id.clone());
443            true
444        }
445    });
446
447    stripped.sort();
448    retained.sort();
449    let manifest = ChildPackageStripManifest {
450        parent_package_fingerprint: parent_fingerprint,
451        child_agent_id,
452        selected_provider_route_id: selected_route.route_id.clone(),
453        handoff_policy_variant: handoff_policy.variant_name().to_string(),
454        tool_policy: tool_policy.clone(),
455        recursive_subagent_strip: true,
456        stripped_capability_ids: stripped,
457        retained_capability_ids: retained,
458        lifecycle_policy_ref: child_policy.child_lifecycle_bounds.clone(),
459        redaction_policy_ref: child_policy.redaction_policy_ref.clone(),
460    };
461
462    child.sidecars.push(PackageSidecarSnapshot {
463        sidecar_id: "sidecar.subagent.child_package_strip_manifest".to_string(),
464        kind: "subagent_child_package_strip_manifest".to_string(),
465        version: "v1".to_string(),
466        refs: vec![PackageSidecarRef::new(
467            "sidecar.subagent.child_package_strip_manifest",
468            "subagent_child_package_strip_manifest",
469            "v1",
470        )],
471        policy_refs: vec![
472            child_policy.child_lifecycle_bounds.clone(),
473            child_policy.redaction_policy_ref.clone(),
474        ],
475        content_hash: manifest.content_hash()?,
476    });
477    child
478        .policies
479        .policy_refs
480        .push(child_policy.redaction_policy_ref.clone());
481    child.fingerprint_manifest = child.computed_fingerprint_manifest();
482    child.validate()?;
483    let fingerprint = child.fingerprint()?;
484
485    Ok(ChildRuntimePackage {
486        package: child,
487        fingerprint,
488        strip_manifest: manifest,
489    })
490}
491
492fn is_recursive_subagent_capability(
493    capability: &CapabilitySpec,
494    policy: &ChildRuntimePackagePolicy,
495) -> bool {
496    capability.kind == CapabilityKind::AgentAsTool
497        || policy
498            .parent_control_tool_ids
499            .contains(&capability.capability_id)
500}
501
502fn default_parent_control_tool_ids() -> Vec<CapabilityId> {
503    [
504        "tool.subagent_send_message",
505        "tool.subagent_reply_to_clarification",
506        "tool.subagent_ask_parent",
507        "tool.subagent_read_parent_messages",
508        "tool.subagent_monitor",
509    ]
510    .into_iter()
511    .map(CapabilityId::new)
512    .collect()
513}
514
515fn child_package_id(
516    child_agent_id: &AgentId,
517    parent_fingerprint: &RuntimePackageFingerprint,
518) -> RuntimePackageId {
519    let digest = Sha256::digest(parent_fingerprint.as_str().as_bytes());
520    let suffix = format!("{:x}", digest);
521    RuntimePackageId::new(format!(
522        "package.subagent.{}.{}",
523        child_agent_id.as_str(),
524        &suffix[..16]
525    ))
526}
527
528fn validate_non_host_policy(policy_ref: &PolicyRef, label: &str) -> Result<(), AgentError> {
529    if policy_ref.kind == PolicyKind::Host {
530        Err(AgentError::contract_violation(format!(
531            "{label} requires an explicit SDK policy ref"
532        )))
533    } else {
534        Ok(())
535    }
536}