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}