Skip to main content

awaken_runtime/child_agent/
mod.rs

1//! Plumbing for invoking a sub-agent run from inside a tool.
2//!
3//! [`run_child_agent`] is the single canonical entry point for spawning a
4//! child agent run. It routes delegate execution through the resolved backend
5//! ([`ExecutionBackend`](crate::backend::ExecutionBackend)), so local and
6//! remote (A2A) children share one call shape, and returns the full
7//! [`BackendRunResult`] so the calling tool can decode child output,
8//! propagate suspensions, or read the child's final persisted state.
9//!
10//! State exchange between parent and child is the caller's responsibility:
11//! - **Inbound**: build a [`PersistedState`] from parent state + tool args
12//!   and pass via `initial_state_seed`.
13//! - **Outbound**: read `BackendRunResult.state` after the call, then publish
14//!   to parent state via `ToolOutput.command`.
15//!
16//! Backend capabilities still apply, but parent → child state seeding is a
17//! local-execution rule rather than a `BackendProfile` dimension. The
18//! in-process Local backend applies the seed before the child's first step.
19//! A2A and custom remote backends have no agreed seed-passing wire protocol;
20//! `run_child_agent` rejects seeded delegate requests against non-local
21//! execution plans rather than silently dropping the seed. If you need to ship
22//! data to a remote child, encode it into the prompt yourself.
23//!
24//! For tools that want to stream the child's tokens into their own output,
25//! wrap the activity sink with [`StreamingPassthroughSink`].
26
27pub mod sink;
28
29pub use sink::{ChildErrorForwarding, StreamingPassthroughSink};
30
31use std::sync::Arc;
32
33use awaken_runtime_contract::contract::event_sink::EventSink;
34use awaken_runtime_contract::contract::message::Message;
35use awaken_runtime_contract::state::PersistedState;
36
37use crate::RuntimeError;
38use crate::backend::{
39    BackendControl, BackendDelegatePolicy, BackendDelegateRunRequest, BackendParentContext,
40    BackendRunResult, BackendRunStatus, ExecutionBackendError, execute_resolved_delegate_execution,
41};
42use crate::cancellation::CancellationToken;
43use crate::registry::AgentResolver;
44
45/// Parameters for [`run_child_agent`].
46///
47/// Construct with [`Self::new`] instead of struct literal syntax. The type is
48/// `#[non_exhaustive]` so future support for resumed child transcripts can
49/// add explicit transcript/new-turn fields without breaking callers.
50///
51/// `initial_state_seed` is applied to the child's store after plugin
52/// activation but before the first inference step — see
53/// [`StateStore::apply_seed`](crate::state::StateStore::apply_seed).
54#[non_exhaustive]
55pub struct ChildAgentParams<'a> {
56    pub resolver: &'a dyn AgentResolver,
57    pub agent_id: &'a str,
58
59    /// Initial conversation seed for a **fresh** delegate run.
60    ///
61    /// `run_child_agent` does not support resuming a prior delegate
62    /// transcript: there is no full-history vs new-turn split. The vec you
63    /// pass here is what the child sees as its starting input — typically
64    /// a single [`Message::user`] built from your tool args. Internally it
65    /// is forwarded as both `BackendDelegateRunRequest.messages` and
66    /// `.new_messages`, mirroring how `AgentTool` invokes delegates.
67    ///
68    /// If a future revision needs to distinguish prior context from the
69    /// new turn (continuation, multi-turn handoff), it will add dedicated
70    /// input fields; do not rely on the current internal duplication to mean
71    /// otherwise.
72    pub initial_messages: Vec<Message>,
73
74    pub parent: BackendParentContext,
75    pub initial_state_seed: Option<PersistedState>,
76    pub sink: Arc<dyn EventSink>,
77    pub control: BackendControl,
78    pub policy: BackendDelegatePolicy,
79}
80
81impl<'a> ChildAgentParams<'a> {
82    /// Build parameters for a fresh child run.
83    ///
84    /// `initial_messages` is the child run's starting input. It is not a
85    /// transcript/new-turn split and does not resume an existing delegate
86    /// transcript.
87    #[must_use]
88    pub fn new(
89        resolver: &'a dyn AgentResolver,
90        agent_id: &'a str,
91        initial_messages: Vec<Message>,
92        parent: BackendParentContext,
93        sink: Arc<dyn EventSink>,
94    ) -> Self {
95        Self {
96            resolver,
97            agent_id,
98            initial_messages,
99            parent,
100            initial_state_seed: None,
101            sink,
102            control: BackendControl::default(),
103            policy: BackendDelegatePolicy::default(),
104        }
105    }
106
107    /// Seed the child's state store before its first inference step.
108    #[must_use]
109    pub fn with_initial_state_seed(mut self, seed: PersistedState) -> Self {
110        self.initial_state_seed = Some(seed);
111        self
112    }
113
114    /// Override cooperative backend controls for the child run.
115    #[must_use]
116    pub fn with_control(mut self, control: BackendControl) -> Self {
117        self.control = control;
118        self
119    }
120
121    /// Propagate a parent cancellation token without rebuilding
122    /// [`BackendControl`] by hand.
123    #[must_use]
124    pub fn with_cancellation_token(mut self, token: Option<CancellationToken>) -> Self {
125        self.control.cancellation_token = token;
126        self
127    }
128
129    /// Override delegate execution policy for the child run.
130    #[must_use]
131    pub fn with_policy(mut self, policy: BackendDelegatePolicy) -> Self {
132        self.policy = policy;
133        self
134    }
135}
136
137/// Error returned by [`run_child_agent_checked`].
138#[derive(Debug, thiserror::Error)]
139pub enum ChildAgentError {
140    #[error(transparent)]
141    Backend(#[from] ExecutionBackendError),
142    #[error("child agent did not complete: {}", .0.status)]
143    Terminal(Box<BackendRunResult>),
144}
145
146impl ChildAgentError {
147    /// Return the terminal child result when the backend run finished in a
148    /// non-success status.
149    #[must_use]
150    pub fn terminal_result(&self) -> Option<&BackendRunResult> {
151        match self {
152            Self::Backend(_) => None,
153            Self::Terminal(result) => Some(result),
154        }
155    }
156}
157
158/// Spawn a child agent run and await its terminal state.
159///
160/// Returns the canonical [`BackendRunResult`] including final persisted state,
161/// status, response, and any suspension reason. Callers decide how to map
162/// these into a `ToolOutput` (typically packaging child state as a
163/// `StateCommand` for the parent store).
164///
165/// This helper treats backend dispatch failures as `Err`, but it does **not**
166/// treat a terminal child status such as `Failed`, `Cancelled`, `Timeout`, or
167/// `Suspended` as an error. Callers must inspect [`BackendRunResult::status`]
168/// before returning a successful parent tool result, or call
169/// [`run_child_agent_checked`] when only `Completed` is acceptable.
170pub async fn run_child_agent(
171    params: ChildAgentParams<'_>,
172) -> Result<BackendRunResult, ExecutionBackendError> {
173    let ChildAgentParams {
174        resolver,
175        agent_id,
176        initial_messages,
177        parent,
178        initial_state_seed,
179        sink,
180        control,
181        policy,
182    } = params;
183
184    let resolved = resolver
185        .resolve_execution(agent_id)
186        .map_err(|error| map_resolve_error(agent_id, error))?;
187
188    let request = BackendDelegateRunRequest {
189        agent_id,
190        new_messages: initial_messages.clone(),
191        messages: initial_messages,
192        sink,
193        resolver,
194        parent,
195        control,
196        policy,
197        state_seed: initial_state_seed,
198    };
199
200    execute_resolved_delegate_execution(&resolved, request).await
201}
202
203fn map_resolve_error(agent_id: &str, error: RuntimeError) -> ExecutionBackendError {
204    match error {
205        RuntimeError::AgentNotFound { agent_id } => ExecutionBackendError::AgentNotFound(agent_id),
206        other => ExecutionBackendError::ExecutionFailed(format!(
207            "failed to resolve agent '{agent_id}': {other}"
208        )),
209    }
210}
211
212/// Spawn a child agent run and require it to complete successfully.
213///
214/// This is a convenience wrapper around [`run_child_agent`] for tools that
215/// should fail when the child reaches any non-`Completed` terminal status.
216pub async fn run_child_agent_checked(
217    params: ChildAgentParams<'_>,
218) -> Result<BackendRunResult, ChildAgentError> {
219    let result = run_child_agent(params).await?;
220    if matches!(result.status, BackendRunStatus::Completed) {
221        Ok(result)
222    } else {
223        Err(ChildAgentError::Terminal(Box::new(result)))
224    }
225}