Skip to main content

adk_graph/
child.rs

1//! Invoking a node from inside another node's body.
2//!
3//! Declared edges express a topology decided before the run. A supervisor often
4//! cannot: how many workers to start, and in what order, depends on what the
5//! first one found. Both adk-python and adk-go answer this the same way, and
6//! neither mutates the graph — a node body calls other nodes directly and awaits
7//! their output.
8//!
9//! # Identity and replay
10//!
11//! Each invocation is recorded under a path, `<parent>/<child>@<run_id>`. When a
12//! parent is re-executed after a resume it runs from the top, so without a record
13//! every child would run again. A path already in the ledger returns its recorded
14//! output instead.
15//!
16//! Only a successful invocation is recorded. A child that failed or interrupted
17//! has to run again, because its work did not finish.
18//!
19//! The default `run_id` counts invocations of that child name within one
20//! activation, so it is stable only while the parent runs once. **A parent that
21//! may be resumed must supply its own `run_id`**, or the counter will hand the
22//! same identity to a different unit of work. adk-go documents the same trap.
23
24use std::collections::HashMap;
25use std::sync::{Arc, Mutex};
26
27use serde_json::Value;
28
29use crate::error::{GraphError, Result};
30use crate::node::{Node, NodeContext};
31
32/// How one child invocation behaves.
33#[derive(Debug, Clone, Default)]
34pub struct RunNodeOptions {
35    /// Stable identity for replay.
36    ///
37    /// Defaults to a counter over invocations of this child name within the
38    /// current activation. Supply one when the parent may be resumed.
39    pub run_id: Option<String>,
40}
41
42impl RunNodeOptions {
43    /// Options with an explicit run id.
44    pub fn with_run_id(run_id: impl Into<String>) -> Self {
45        Self { run_id: Some(run_id.into()) }
46    }
47}
48
49/// The machinery a [`NodeContext`] needs to invoke another node.
50///
51/// Built by the executor for each node it runs, so a node body reaches only the
52/// graph it belongs to.
53pub(crate) struct ChildInvoker {
54    /// Every node in the graph, including any reachable by no edge.
55    nodes: HashMap<String, Arc<dyn Node>>,
56    /// Outputs already recorded, keyed by child path. Shared with the executor so
57    /// the run's checkpoint carries them.
58    ledger: Arc<Mutex<HashMap<String, Value>>>,
59    /// The invoking node's path, which prefixes its children's.
60    parent_path: String,
61    /// Invocations so far per child name, for the default run id.
62    counters: Mutex<HashMap<String, u32>>,
63}
64
65impl ChildInvoker {
66    pub(crate) fn new(
67        nodes: HashMap<String, Arc<dyn Node>>,
68        ledger: Arc<Mutex<HashMap<String, Value>>>,
69        parent_path: String,
70    ) -> Self {
71        Self { nodes, ledger, parent_path, counters: Mutex::new(HashMap::new()) }
72    }
73
74    /// The path this invocation is recorded under.
75    fn path_for(&self, child: &str, options: &RunNodeOptions) -> String {
76        let run_id = match &options.run_id {
77            Some(id) => id.clone(),
78            None => {
79                let mut counters = self.counters.lock().expect("child counters");
80                let count = counters.entry(child.to_string()).or_insert(0);
81                *count += 1;
82                count.to_string()
83            }
84        };
85        format!("{}/{}@{}", self.parent_path, child, run_id)
86    }
87
88    /// Invoke a child and return its updates as one value.
89    pub(crate) async fn run(
90        &self,
91        child: &str,
92        input: Value,
93        options: RunNodeOptions,
94        parent: &NodeContext,
95    ) -> Result<Value> {
96        let path = self.path_for(child, &options);
97
98        // A child that already completed under this identity is not run again.
99        if let Some(recorded) = self.ledger.lock().expect("child ledger").get(&path) {
100            tracing::debug!(path = %path, "child already completed, serving its recorded output");
101            return Ok(recorded.clone());
102        }
103
104        let node = self
105            .nodes
106            .get(child)
107            .ok_or_else(|| GraphError::NodeNotFound(child.to_string()))?
108            .clone();
109
110        // The child sees the parent's state, with its input merged over it.
111        let mut state = parent.state.clone();
112        if let Value::Object(map) = input {
113            for (key, value) in map {
114                state.insert(key, value);
115            }
116        }
117        let child_ctx = NodeContext::new(state, parent.config.clone(), parent.step);
118
119        let output = node.execute(&child_ctx).await?;
120
121        // An interrupt is not a result: the child has not finished, so nothing is
122        // recorded and it runs again on resume.
123        if let Some(interrupt) = output.interrupt {
124            return Err(GraphError::Interrupted(Box::new(
125                crate::error::InterruptedExecution::new(
126                    parent.config.thread_id.clone(),
127                    String::new(),
128                    interrupt,
129                    child_ctx.state.clone(),
130                    parent.step,
131                ),
132            )));
133        }
134
135        let value = Value::Object(output.updates.into_iter().collect());
136        self.ledger.lock().expect("child ledger").insert(path.clone(), value.clone());
137        tracing::debug!(path = %path, "child completed");
138        Ok(value)
139    }
140}