Skip to main content

adk_graph/
node.rs

1//! Node types for graph execution
2//!
3//! Nodes are the computational units in a graph. They receive state and return updates.
4
5use crate::error::Result;
6use crate::interrupt::Interrupt;
7use crate::state::State;
8use crate::stream::StreamEvent;
9use crate::timeout::ProgressHandle;
10use async_trait::async_trait;
11use serde_json::Value;
12use std::collections::HashMap;
13use std::future::Future;
14use std::pin::Pin;
15use std::sync::Arc;
16
17/// Configuration passed to nodes during execution
18#[derive(Clone)]
19pub struct ExecutionConfig {
20    /// Thread identifier for checkpointing
21    pub thread_id: String,
22    /// Resume from a specific checkpoint
23    pub resume_from: Option<String>,
24    /// Recursion limit for cycles
25    pub recursion_limit: usize,
26    /// Additional configuration
27    pub metadata: HashMap<String, Value>,
28    /// The invocation this graph run belongs to, when it has one.
29    ///
30    /// An [`AgentNode`] runs a real agent, and that agent expects the identity,
31    /// services, and cancellation of the run it belongs to. Without a parent the node
32    /// has to fabricate them, which makes an agent behave differently inside a graph
33    /// than outside it. Set this with
34    /// [`ExecutionConfig::with_parent_context`] to carry them through; leaving it
35    /// unset is standalone mode and is what a graph invoked outside a `Runner` gets.
36    pub parent_context: Option<Arc<dyn adk_core::InvocationContext>>,
37}
38
39impl ExecutionConfig {
40    /// Create a new config with the given thread ID
41    pub fn new(thread_id: &str) -> Self {
42        Self {
43            thread_id: thread_id.to_string(),
44            resume_from: None,
45            recursion_limit: 50,
46            metadata: HashMap::new(),
47            parent_context: None,
48        }
49    }
50
51    /// Carry the invocation this graph run belongs to into its nodes.
52    ///
53    /// An [`AgentNode`] then presents the caller's identity, services, request
54    /// context, and cancellation to the agent it runs, instead of a synthetic
55    /// standalone context.
56    #[must_use]
57    pub fn with_parent_context(mut self, parent: Arc<dyn adk_core::InvocationContext>) -> Self {
58        self.parent_context = Some(parent);
59        self
60    }
61
62    /// Set the recursion limit
63    pub fn with_recursion_limit(mut self, limit: usize) -> Self {
64        self.recursion_limit = limit;
65        self
66    }
67
68    /// Resume from a specific checkpoint
69    pub fn with_resume_from(mut self, checkpoint_id: &str) -> Self {
70        self.resume_from = Some(checkpoint_id.to_string());
71        self
72    }
73
74    /// Add metadata
75    pub fn with_metadata(mut self, key: &str, value: Value) -> Self {
76        self.metadata.insert(key.to_string(), value);
77        self
78    }
79}
80
81impl Default for ExecutionConfig {
82    fn default() -> Self {
83        Self::new(&uuid::Uuid::new_v4().to_string())
84    }
85}
86
87/// Context passed to nodes during execution
88pub struct NodeContext {
89    /// Current graph state (read-only view)
90    pub state: State,
91    /// Configuration for this execution
92    pub config: ExecutionConfig,
93    /// Current step number
94    pub step: usize,
95    /// Optional progress handle for idle timeout tracking.
96    /// When present, calling [`report_progress()`](Self::report_progress) resets the idle timeout counter.
97    progress_handle: Option<ProgressHandle>,
98    /// Set by the executor when this node may invoke other nodes.
99    children: Option<std::sync::Arc<crate::child::ChildInvoker>>,
100    /// The schema of the graph running this node, for a node that projects state.
101    parent_schema: Option<std::sync::Arc<crate::state::StateSchema>>,
102    /// ADK configuration supplied through an additive `CompiledGraph` API.
103    run_config: Option<adk_core::RunConfig>,
104}
105
106impl NodeContext {
107    /// Create a new node context
108    pub fn new(state: State, config: ExecutionConfig, step: usize) -> Self {
109        Self {
110            state,
111            config,
112            step,
113            progress_handle: None,
114            children: None,
115            parent_schema: None,
116            run_config: None,
117        }
118    }
119
120    /// The machinery for invoking other nodes, if this context has it.
121    /// The schema of the graph running this node.
122    ///
123    /// Attached by the executor. A node that projects state between two schemas
124    /// needs it; most nodes do not.
125    pub fn parent_schema(&self) -> Option<std::sync::Arc<crate::state::StateSchema>> {
126        self.parent_schema.clone()
127    }
128
129    /// Attaches the running graph's schema.
130    pub fn set_parent_schema(&mut self, schema: std::sync::Arc<crate::state::StateSchema>) {
131        self.parent_schema = Some(schema);
132    }
133
134    /// Attach per-run ADK configuration for an agent node.
135    pub(crate) fn set_run_config(&mut self, run_config: adk_core::RunConfig) {
136        self.run_config = Some(run_config);
137    }
138
139    pub(crate) fn run_config(&self) -> Option<adk_core::RunConfig> {
140        self.run_config.clone()
141    }
142
143    pub(crate) fn child_invoker(&self) -> Option<std::sync::Arc<crate::child::ChildInvoker>> {
144        self.children.clone()
145    }
146
147    /// Attach the machinery for invoking other nodes.
148    pub(crate) fn set_child_invoker(
149        &mut self,
150        invoker: std::sync::Arc<crate::child::ChildInvoker>,
151    ) {
152        self.children = Some(invoker);
153    }
154
155    /// Invoke another node and await its output.
156    ///
157    /// The child sees this node's state with `input` merged over it, and returns
158    /// its updates as one object. Nothing is applied to the graph's state: the
159    /// caller decides what to do with the result.
160    ///
161    /// A child that already completed under the same identity is not run again
162    /// after a resume. See [`crate::child`] for how that identity is formed, and
163    /// why a resumable parent should pass its own run id.
164    ///
165    /// # Errors
166    ///
167    /// Returns [`GraphError::NodeNotFound`](crate::error::GraphError::NodeNotFound)
168    /// when no node has that name, whatever the child returns, and
169    /// [`GraphError::Interrupted`](crate::error::GraphError::Interrupted) when the
170    /// child pauses.
171    pub async fn run_node(&self, child: &str, input: Value) -> Result<Value> {
172        self.run_node_with(child, input, crate::child::RunNodeOptions::default()).await
173    }
174
175    /// Invoke another node with an explicit run id.
176    ///
177    /// # Errors
178    ///
179    /// As [`run_node`](Self::run_node), and additionally when this node was not
180    /// given the ability to invoke children.
181    pub async fn run_node_with(
182        &self,
183        child: &str,
184        input: Value,
185        options: crate::child::RunNodeOptions,
186    ) -> Result<Value> {
187        let invoker = self.children.as_ref().ok_or_else(|| {
188            crate::error::GraphError::InvalidGraph(
189                "this node cannot invoke other nodes: no child invoker was attached".to_string(),
190            )
191        })?;
192        invoker.run(child, input, options, self).await
193    }
194
195    /// Get a value from state
196    pub fn get(&self, key: &str) -> Option<&Value> {
197        self.state.get(key)
198    }
199
200    /// Get a value from state as a specific type
201    pub fn get_as<T: serde::de::DeserializeOwned>(&self, key: &str) -> Option<T> {
202        self.state.get(key).and_then(|v| serde_json::from_value(v.clone()).ok())
203    }
204
205    /// Report progress, resetting the idle timeout counter.
206    ///
207    /// Nodes performing long-running work should call this periodically to
208    /// prevent the idle timeout from firing. If no progress handle is attached
209    /// (e.g., when no idle timeout is configured), this is a no-op.
210    ///
211    /// # Example
212    ///
213    /// ```rust,ignore
214    /// async fn execute(&self, ctx: &NodeContext) -> Result<NodeOutput> {
215    ///     for chunk in large_dataset.chunks(100) {
216    ///         process(chunk).await;
217    ///         ctx.report_progress(); // reset idle timeout
218    ///     }
219    ///     Ok(NodeOutput::new())
220    /// }
221    /// ```
222    pub fn report_progress(&self) {
223        if let Some(handle) = &self.progress_handle {
224            handle.report_progress();
225        }
226    }
227
228    /// Attach a progress handle for idle timeout tracking.
229    ///
230    /// This is called by the executor before running a node with an idle timeout
231    /// policy. Nodes do not need to call this directly.
232    pub fn set_progress_handle(&mut self, handle: ProgressHandle) {
233        self.progress_handle = Some(handle);
234    }
235
236    /// Get a reference to the attached progress handle, if any.
237    pub fn progress_handle(&self) -> Option<&ProgressHandle> {
238        self.progress_handle.as_ref()
239    }
240}
241
242/// Output from a node execution
243#[derive(Default)]
244pub struct NodeOutput {
245    /// State updates to apply
246    pub updates: HashMap<String, Value>,
247    /// Optional interrupt request
248    pub interrupt: Option<Interrupt>,
249    /// Custom stream events
250    pub events: Vec<StreamEvent>,
251    /// Nodes to run next, replacing this node's declared outgoing edges.
252    pub goto: Option<Vec<String>>,
253    /// Nodes of the *parent* graph to run next, when this graph is a subgraph.
254    ///
255    /// A node deep in a nested graph can end its own graph and hand control to a
256    /// node of the graph that holds it.
257    pub goto_parent: Option<Vec<String>>,
258}
259
260impl NodeOutput {
261    /// Create a new empty output
262    pub fn new() -> Self {
263        Self::default()
264    }
265
266    /// Names the nodes to run next, replacing this node's declared edges.
267    ///
268    /// A conditional edge fixes its targets when the graph is built. This does
269    /// not: a node reads state and names any node in the graph, including one it
270    /// has no edge to. Naming [`END`](crate::edge::END) stops the branch.
271    ///
272    /// The declared edges from this node do not also fire. Setting no goto leaves
273    /// the declared edges in charge, which is the default.
274    ///
275    /// # Example
276    ///
277    /// ```
278    /// use adk_graph::node::NodeOutput;
279    /// use serde_json::json;
280    ///
281    /// // Write state and choose the next node in one step.
282    /// let output = NodeOutput::new()
283    ///     .with_update("risk", json!("high"))
284    ///     .with_goto(["escalate"]);
285    /// assert_eq!(output.goto.as_deref(), Some(&["escalate".to_string()][..]));
286    /// ```
287    /// Names nodes of the *parent* graph to run next.
288    ///
289    /// Only meaningful inside a [`SubgraphNode`](crate::subgraph::SubgraphNode).
290    /// The subgraph finishes, its output channels are projected out as usual, and
291    /// the parent continues at the named nodes rather than following the
292    /// subgraph node's own edges. This is the counterpart to LangGraph's
293    /// `Command(goto=..., graph=Command.PARENT)`.
294    ///
295    /// A name the parent does not hold fails the run with
296    /// [`GraphError::UnknownRouteTarget`](crate::error::GraphError::UnknownRouteTarget),
297    /// checked by the parent, which is the only side that knows its own nodes.
298    ///
299    /// # Example
300    ///
301    /// ```
302    /// use adk_graph::node::NodeOutput;
303    /// use serde_json::json;
304    ///
305    /// // Inside a subgraph: give up, and let the parent's escalation path run.
306    /// let output = NodeOutput::new()
307    ///     .with_update("reason", json!("no confident answer"))
308    ///     .with_goto_parent(["escalate"]);
309    /// assert_eq!(output.goto_parent.as_deref(), Some(&["escalate".to_string()][..]));
310    /// ```
311    pub fn with_goto_parent<I, S>(mut self, targets: I) -> Self
312    where
313        I: IntoIterator<Item = S>,
314        S: Into<String>,
315    {
316        self.goto_parent = Some(targets.into_iter().map(Into::into).collect());
317        self
318    }
319
320    pub fn with_goto<I, S>(mut self, targets: I) -> Self
321    where
322        I: IntoIterator<Item = S>,
323        S: Into<String>,
324    {
325        self.goto = Some(targets.into_iter().map(Into::into).collect());
326        self
327    }
328
329    /// Add a state update
330    pub fn with_update(mut self, key: &str, value: impl Into<Value>) -> Self {
331        self.updates.insert(key.to_string(), value.into());
332        self
333    }
334
335    /// Add multiple state updates
336    pub fn with_updates(mut self, updates: HashMap<String, Value>) -> Self {
337        self.updates.extend(updates);
338        self
339    }
340
341    /// Set an interrupt
342    pub fn with_interrupt(mut self, interrupt: Interrupt) -> Self {
343        self.interrupt = Some(interrupt);
344        self
345    }
346
347    /// Add a custom stream event
348    pub fn with_event(mut self, event: StreamEvent) -> Self {
349        self.events.push(event);
350        self
351    }
352
353    /// Create output that triggers a dynamic interrupt
354    pub fn interrupt(message: &str) -> Self {
355        Self::new().with_interrupt(crate::interrupt::interrupt(message))
356    }
357
358    /// Create output that triggers a dynamic interrupt with data
359    pub fn interrupt_with_data(message: &str, data: Value) -> Self {
360        Self::new().with_interrupt(crate::interrupt::interrupt_with_data(message, data))
361    }
362}
363
364/// A node in the graph
365#[async_trait]
366pub trait Node: Send + Sync {
367    /// Node identifier
368    fn name(&self) -> &str;
369
370    /// Human-readable purpose shown by generic workflow inspectors.
371    fn description(&self) -> &str {
372        "Graph workflow node"
373    }
374
375    /// Runtime capabilities inherited by portable graph topology metadata.
376    fn capabilities(&self) -> adk_core::AgentCapabilities {
377        adk_core::AgentCapabilities::default()
378    }
379
380    /// Execute the node and return state updates
381    async fn execute(&self, ctx: &NodeContext) -> Result<NodeOutput>;
382
383    /// Rejects a node that cannot execute, before the graph runs.
384    ///
385    /// Called for every node by [`StateGraph::compile`](crate::graph::StateGraph::compile), so a configuration whose
386    /// backend is unavailable fails while the graph is being built rather than
387    /// part-way through a run, when earlier nodes may already have had side effects.
388    ///
389    /// # Errors
390    ///
391    /// Returns an error describing what is unavailable. The default accepts the node.
392    /// Checks this node against the schema of the graph that holds it.
393    ///
394    /// Called by [`StateGraph::compile`](crate::graph::StateGraph::compile) for
395    /// every node, so a node that has requirements on its parent states them
396    /// before anything runs. Defaults to accepting any parent.
397    ///
398    /// [`SubgraphNode`](crate::subgraph::SubgraphNode) uses this to reject a
399    /// channel mapping that names a channel neither side declares.
400    fn validate_against(&self, _parent: &crate::state::StateSchema) -> Result<()> {
401        Ok(())
402    }
403
404    fn validate(&self) -> Result<()> {
405        Ok(())
406    }
407
408    /// Streams execution events for this node.
409    ///
410    /// An implementation must report the node's state updates by yielding a
411    /// [`StreamEvent::Updates`] event, because a streaming executor takes the
412    /// updates from this stream rather than executing the node a second time.
413    /// The default implementation wraps [`Node::execute`] and does so.
414    fn execute_stream<'a>(
415        &'a self,
416        ctx: &'a NodeContext,
417    ) -> Pin<Box<dyn futures::Stream<Item = Result<StreamEvent>> + Send + 'a>> {
418        let name = self.name().to_string();
419        Box::pin(async_stream::stream! {
420            match self.execute(ctx).await {
421                Ok(output) => {
422                    for event in output.events {
423                        yield Ok(event);
424                    }
425                    // A goto and an interrupt have no other way through: this path
426                    // yields events, not a NodeOutput, so the executor reads both
427                    // back off the stream.
428                    if let Some(targets) = output.goto {
429                        yield Ok(StreamEvent::route_dispatched(&name, targets));
430                    }
431                    if let Some(interrupt) = output.interrupt {
432                        let (message, data) = match interrupt {
433                            crate::interrupt::Interrupt::Dynamic { message, data } => (message, data),
434                            other => (other.to_string(), None),
435                        };
436                        yield Ok(StreamEvent::node_interrupt(&name, &message, data));
437                    }
438                    yield Ok(StreamEvent::Updates { node: name, updates: output.updates });
439                }
440                Err(e) => yield Err(e),
441            }
442        })
443    }
444}
445
446/// Type alias for boxed node
447pub type BoxedNode = Box<dyn Node>;
448
449/// Type alias for async function signature
450pub type AsyncNodeFn = Box<
451    dyn Fn(NodeContext) -> Pin<Box<dyn Future<Output = Result<NodeOutput>> + Send>> + Send + Sync,
452>;
453
454/// Function node - wraps an async function as a node
455pub struct FunctionNode {
456    name: String,
457    func: AsyncNodeFn,
458}
459
460impl FunctionNode {
461    /// Create a new function node
462    pub fn new<F, Fut>(name: &str, func: F) -> Self
463    where
464        F: Fn(NodeContext) -> Fut + Send + Sync + 'static,
465        Fut: Future<Output = Result<NodeOutput>> + Send + 'static,
466    {
467        Self { name: name.to_string(), func: Box::new(move |ctx| Box::pin(func(ctx))) }
468    }
469}
470
471#[async_trait]
472impl Node for FunctionNode {
473    fn name(&self) -> &str {
474        &self.name
475    }
476
477    async fn execute(&self, ctx: &NodeContext) -> Result<NodeOutput> {
478        // The closure takes an owned context, so everything the executor attached
479        // has to be carried across or the node silently loses it.
480        let mut ctx_owned = NodeContext::new(ctx.state.clone(), ctx.config.clone(), ctx.step);
481        if let Some(handle) = ctx.progress_handle() {
482            ctx_owned.set_progress_handle(handle.clone());
483        }
484        if let Some(invoker) = ctx.child_invoker() {
485            ctx_owned.set_child_invoker(invoker);
486        }
487        if let Some(schema) = ctx.parent_schema() {
488            ctx_owned.set_parent_schema(schema);
489        }
490        if let Some(run_config) = ctx.run_config() {
491            ctx_owned.set_run_config(run_config);
492        }
493        (self.func)(ctx_owned).await
494    }
495}
496
497/// Passthrough node - just passes state through unchanged
498pub struct PassthroughNode {
499    name: String,
500}
501
502impl PassthroughNode {
503    /// Create a new passthrough node
504    pub fn new(name: &str) -> Self {
505        Self { name: name.to_string() }
506    }
507}
508
509#[async_trait]
510impl Node for PassthroughNode {
511    fn name(&self) -> &str {
512        &self.name
513    }
514
515    async fn execute(&self, _ctx: &NodeContext) -> Result<NodeOutput> {
516        Ok(NodeOutput::new())
517    }
518}
519
520/// Type alias for agent node input mapper
521pub type AgentInputMapper = Box<dyn Fn(&State) -> adk_core::Content + Send + Sync>;
522
523/// Type alias for agent node output mapper
524pub type AgentOutputMapper =
525    Box<dyn Fn(&[adk_core::Event]) -> HashMap<String, Value> + Send + Sync>;
526
527/// Chooses an [`AgentNode`]'s successors from the updates it just produced.
528///
529/// Returning `None` leaves the node's declared edges in charge.
530pub type AgentGotoMapper =
531    Box<dyn Fn(&HashMap<String, Value>) -> Option<Vec<String>> + Send + Sync>;
532
533/// Wrapper to use an existing ADK Agent as a graph node
534pub struct AgentNode {
535    name: String,
536    #[allow(dead_code)]
537    agent: Arc<dyn adk_core::Agent>,
538    /// Map state to agent input content
539    input_mapper: AgentInputMapper,
540    /// Map agent events to state updates
541    output_mapper: AgentOutputMapper,
542    /// Choose successors from the mapped updates, replacing declared edges.
543    goto_mapper: Option<AgentGotoMapper>,
544}
545
546impl AgentNode {
547    /// Create a new agent node
548    pub fn new(agent: Arc<dyn adk_core::Agent>) -> Self {
549        let name = agent.name().to_string();
550        Self {
551            name,
552            agent,
553            input_mapper: Box::new(default_input_mapper),
554            output_mapper: Box::new(default_output_mapper),
555            goto_mapper: None,
556        }
557    }
558
559    /// Set custom input mapper
560    pub fn with_input_mapper<F>(mut self, mapper: F) -> Self
561    where
562        F: Fn(&State) -> adk_core::Content + Send + Sync + 'static,
563    {
564        self.input_mapper = Box::new(mapper);
565        self
566    }
567
568    /// Set custom output mapper
569    pub fn with_output_mapper<F>(mut self, mapper: F) -> Self
570    where
571        F: Fn(&[adk_core::Event]) -> HashMap<String, Value> + Send + Sync + 'static,
572    {
573        self.output_mapper = Box::new(mapper);
574        self
575    }
576
577    /// Chooses this node's successors from the updates the output mapper produced.
578    ///
579    /// An agent's answer often decides where control goes next. The output mapper
580    /// turns the agent's events into state; this turns that state into a route,
581    /// so the classification is parsed once.
582    ///
583    /// Returning `None` leaves the declared edges in charge. Returning targets
584    /// replaces them, exactly as [`NodeOutput::with_goto`] does for a plain node.
585    ///
586    /// # Example
587    ///
588    /// ```no_run
589    /// # use adk_graph::node::AgentNode;
590    /// # use std::collections::HashMap;
591    /// # fn wire(node: AgentNode) -> AgentNode {
592    /// node.with_goto_mapper(|updates: &HashMap<String, serde_json::Value>| {
593    ///     match updates.get("category").and_then(|v| v.as_str()) {
594    ///         Some("refund") => Some(vec!["refund_desk".to_string()]),
595    ///         Some(_) => Some(vec!["general_desk".to_string()]),
596    ///         None => None,
597    ///     }
598    /// })
599    /// # }
600    /// ```
601    pub fn with_goto_mapper<F>(mut self, mapper: F) -> Self
602    where
603        F: Fn(&HashMap<String, Value>) -> Option<Vec<String>> + Send + Sync + 'static,
604    {
605        self.goto_mapper = Some(Box::new(mapper));
606        self
607    }
608}
609
610/// Default input mapper - looks for "messages" or "input" in state
611fn default_input_mapper(state: &State) -> adk_core::Content {
612    // Try to get messages first
613    if let Some(messages) = state.get("messages")
614        && let Some(arr) = messages.as_array()
615        && let Some(last) = arr.last()
616        && let Some(content) = last.get("content").and_then(|c| c.as_str())
617    {
618        return adk_core::Content::new("user").with_text(content);
619    }
620
621    // Try input field
622    if let Some(input) = state.get("input")
623        && let Some(text) = input.as_str()
624    {
625        return adk_core::Content::new("user").with_text(text);
626    }
627
628    adk_core::Content::new("user")
629}
630
631/// Default output mapper - extracts text content to "messages"
632fn default_output_mapper(events: &[adk_core::Event]) -> HashMap<String, Value> {
633    let mut updates = HashMap::new();
634
635    // Collect text from events
636    let mut messages = Vec::new();
637    for event in events {
638        if let Some(content) = event.content() {
639            let text = content.parts.iter().filter_map(|p| p.text()).collect::<Vec<_>>().join("");
640
641            if !text.is_empty() {
642                messages.push(serde_json::json!({
643                    "role": "assistant",
644                    "content": text
645                }));
646            }
647        }
648    }
649
650    if !messages.is_empty() {
651        updates.insert("messages".to_string(), serde_json::json!(messages));
652    }
653
654    updates
655}
656
657#[async_trait]
658impl Node for AgentNode {
659    fn name(&self) -> &str {
660        &self.name
661    }
662
663    fn description(&self) -> &str {
664        self.agent.description()
665    }
666
667    fn capabilities(&self) -> adk_core::AgentCapabilities {
668        self.agent.capabilities()
669    }
670
671    async fn execute(&self, ctx: &NodeContext) -> Result<NodeOutput> {
672        use futures::StreamExt;
673
674        // Map state to input content
675        let content = (self.input_mapper)(&ctx.state);
676
677        // Create a graph invocation context with the agent
678        let invocation_ctx = Arc::new(GraphInvocationContext::with_parent(
679            ctx.config.thread_id.clone(),
680            content,
681            self.agent.clone(),
682            ctx.config.parent_context.clone(),
683            ctx.run_config(),
684        ));
685
686        // Run the agent and collect events
687        let stream = self.agent.run(invocation_ctx).await.map_err(|e| {
688            crate::error::GraphError::NodeExecutionFailed {
689                node: self.name.clone(),
690                message: e.to_string(),
691            }
692        })?;
693
694        let events: Vec<adk_core::Event> = stream.filter_map(|r| async { r.ok() }).collect().await;
695
696        if let Some(request) =
697            events.iter().find_map(|event| event.actions.tool_confirmation.clone())
698        {
699            return Ok(NodeOutput::new().with_interrupt(
700                crate::interrupt::GraphToolConfirmationPause::pending_interrupt(
701                    self.name.clone(),
702                    request,
703                ),
704            ));
705        }
706
707        // Map events to state updates
708        let updates = (self.output_mapper)(&events);
709        let goto = self.goto_mapper.as_ref().and_then(|mapper| mapper(&updates));
710
711        // Convert agent events to stream events for tracing
712        let mut output = NodeOutput::new().with_updates(updates);
713        if let Some(targets) = goto {
714            output = output.with_goto(targets);
715        }
716        for event in &events {
717            if let Ok(json) = serde_json::to_value(event) {
718                output = output.with_event(StreamEvent::custom(&self.name, "agent_event", json));
719            }
720        }
721
722        Ok(output)
723    }
724
725    fn execute_stream<'a>(
726        &'a self,
727        ctx: &'a NodeContext,
728    ) -> Pin<Box<dyn futures::Stream<Item = Result<StreamEvent>> + Send + 'a>> {
729        use futures::StreamExt;
730        let name = self.name.clone();
731        let agent = self.agent.clone();
732        let input_mapper = &self.input_mapper;
733        let output_mapper = &self.output_mapper;
734        let goto_mapper = &self.goto_mapper;
735        let parent_context = ctx.config.parent_context.clone();
736        let thread_id = ctx.config.thread_id.clone();
737        let content = (input_mapper)(&ctx.state);
738
739        Box::pin(async_stream::stream! {
740            tracing::debug!("AgentNode::execute_stream called for {}", name);
741            let invocation_ctx = Arc::new(GraphInvocationContext::with_parent(
742                thread_id,
743                content,
744                agent.clone(),
745                parent_context,
746                ctx.run_config(),
747            ));
748
749            let stream = match agent.run(invocation_ctx).await {
750                Ok(s) => s,
751                Err(e) => {
752                    yield Err(crate::error::GraphError::NodeExecutionFailed {
753                        node: name.clone(),
754                        message: e.to_string(),
755                    });
756                    return;
757                }
758            };
759
760            tokio::pin!(stream);
761            let mut all_events = Vec::new();
762
763            while let Some(result) = stream.next().await {
764                match result {
765                    Ok(event) => {
766                        if let Some(request) = event.actions.tool_confirmation.clone() {
767                            yield Ok(StreamEvent::node_interrupt(
768                                &name,
769                                crate::interrupt::GraphToolConfirmationPause::KIND,
770                                Some(crate::interrupt::GraphToolConfirmationPause::pending_data(
771                                    name.clone(),
772                                    request,
773                                )),
774                            ));
775                            return;
776                        }
777                        // Emit streaming event immediately
778                        if let Some(content) = event.content() {
779                            let text: String = content.parts.iter().filter_map(|p| p.text()).collect();
780                            if !text.is_empty() {
781                                yield Ok(StreamEvent::Message {
782                                    node: name.clone(),
783                                    content: text,
784                                    is_final: false,
785                                });
786                            }
787                        }
788                        all_events.push(event);
789                    }
790                    Err(e) => {
791                        yield Err(crate::error::GraphError::NodeExecutionFailed {
792                            node: name.clone(),
793                            message: e.to_string(),
794                        });
795                        return;
796                    }
797                }
798            }
799
800            // Emit final events
801            for event in &all_events {
802                if let Ok(json) = serde_json::to_value(event) {
803                    yield Ok(StreamEvent::custom(&name, "agent_event", json));
804                }
805            }
806
807            // Report state updates from this run. Without this the streaming
808            // executor has no updates to apply and would have to run the agent
809            // a second time to obtain them.
810            let updates = (output_mapper)(&all_events);
811            // Same route the plain path uses: this yields events, not a NodeOutput.
812            if let Some(targets) = goto_mapper.as_ref().and_then(|mapper| mapper(&updates)) {
813                yield Ok(StreamEvent::route_dispatched(&name, targets));
814            }
815            yield Ok(StreamEvent::Updates { node: name.clone(), updates });
816        })
817    }
818}
819
820/// Full InvocationContext implementation for running agents within graph nodes
821struct GraphInvocationContext {
822    invocation_id: String,
823    user_content: adk_core::Content,
824    agent: Arc<dyn adk_core::Agent>,
825    session: Arc<GraphSession>,
826    run_config: adk_core::RunConfig,
827    ended: std::sync::atomic::AtomicBool,
828    /// The invocation this graph run belongs to, when it has one.
829    ///
830    /// Present: identity, services, request context, and cancellation come from the
831    /// caller, so an agent behaves the same inside a graph as outside it.
832    /// Absent: standalone mode, with the synthetic identity below.
833    parent: Option<Arc<dyn adk_core::InvocationContext>>,
834    /// Identity strings, owned because the trait returns them by reference.
835    user_id: String,
836    app_name: String,
837    branch: String,
838}
839
840/// Identity used when a graph runs with no parent invocation.
841const STANDALONE_USER_ID: &str = "graph_user";
842/// Application name used when a graph runs with no parent invocation.
843const STANDALONE_APP_NAME: &str = "graph_app";
844
845impl GraphInvocationContext {
846    fn with_parent(
847        session_id: String,
848        user_content: adk_core::Content,
849        agent: Arc<dyn adk_core::Agent>,
850        parent: Option<Arc<dyn adk_core::InvocationContext>>,
851        explicit_run_config: Option<adk_core::RunConfig>,
852    ) -> Self {
853        let invocation_id = uuid::Uuid::new_v4().to_string();
854        let session = Arc::new(GraphSession::new(session_id));
855        // Add user content to history
856        session.append_content(user_content.clone());
857
858        // A node runs on its own branch below the caller's, so events it produces are
859        // attributable and do not read as the parent agent's own turn.
860        let (user_id, app_name, branch, inherited_run_config) = match parent.as_ref() {
861            Some(parent) => (
862                parent.user_id().to_string(),
863                parent.app_name().to_string(),
864                match parent.branch() {
865                    "" => agent.name().to_string(),
866                    existing => format!("{existing}.{}", agent.name()),
867                },
868                parent.run_config().clone(),
869            ),
870            None => (
871                STANDALONE_USER_ID.to_string(),
872                STANDALONE_APP_NAME.to_string(),
873                "main".to_string(),
874                adk_core::RunConfig::default(),
875            ),
876        };
877
878        Self {
879            invocation_id,
880            user_content,
881            agent,
882            session,
883            run_config: explicit_run_config.unwrap_or(inherited_run_config),
884            ended: std::sync::atomic::AtomicBool::new(false),
885            parent,
886            user_id,
887            app_name,
888            branch,
889        }
890    }
891}
892
893// Implement ReadonlyContext (required by CallbackContext)
894impl adk_core::ReadonlyContext for GraphInvocationContext {
895    fn invocation_id(&self) -> &str {
896        &self.invocation_id
897    }
898
899    fn agent_name(&self) -> &str {
900        self.agent.name()
901    }
902
903    fn user_id(&self) -> &str {
904        &self.user_id
905    }
906
907    fn app_name(&self) -> &str {
908        &self.app_name
909    }
910
911    fn session_id(&self) -> &str {
912        &self.session.id
913    }
914
915    fn branch(&self) -> &str {
916        &self.branch
917    }
918
919    fn user_content(&self) -> &adk_core::Content {
920        &self.user_content
921    }
922}
923
924// Implement CallbackContext (required by InvocationContext)
925#[async_trait]
926impl adk_core::CallbackContext for GraphInvocationContext {
927    fn artifacts(&self) -> Option<Arc<dyn adk_core::Artifacts>> {
928        self.parent.as_ref().and_then(|parent| parent.artifacts())
929    }
930
931    fn shared_state(&self) -> Option<Arc<adk_core::SharedState>> {
932        self.parent.as_ref().and_then(|parent| parent.shared_state())
933    }
934}
935
936// Implement InvocationContext
937#[async_trait]
938impl adk_core::InvocationContext for GraphInvocationContext {
939    fn agent(&self) -> Arc<dyn adk_core::Agent> {
940        self.agent.clone()
941    }
942
943    fn memory(&self) -> Option<Arc<dyn adk_core::Memory>> {
944        self.parent.as_ref().and_then(|parent| parent.memory())
945    }
946
947    fn session(&self) -> &dyn adk_core::Session {
948        self.session.as_ref()
949    }
950
951    fn run_config(&self) -> &adk_core::RunConfig {
952        &self.run_config
953    }
954
955    fn end_invocation(&self) {
956        self.ended.store(true, std::sync::atomic::Ordering::SeqCst);
957        if let Some(parent) = &self.parent {
958            parent.end_invocation();
959        }
960    }
961
962    fn ended(&self) -> bool {
963        self.ended.load(std::sync::atomic::Ordering::SeqCst)
964            || self.parent.as_ref().is_some_and(|parent| parent.ended())
965    }
966
967    fn is_cancelled(&self) -> bool {
968        self.parent.as_ref().is_some_and(|parent| parent.is_cancelled())
969    }
970
971    fn user_scopes(&self) -> Vec<String> {
972        self.parent.as_ref().map(|parent| parent.user_scopes()).unwrap_or_default()
973    }
974
975    fn request_metadata(&self) -> std::collections::HashMap<String, Value> {
976        self.parent.as_ref().map(|parent| parent.request_metadata()).unwrap_or_default()
977    }
978
979    async fn get_secret(&self, name: &str) -> adk_core::Result<Option<String>> {
980        match &self.parent {
981            Some(parent) => parent.get_secret(name).await,
982            None => Ok(None),
983        }
984    }
985
986    async fn get_secret_for(
987        &self,
988        request: &adk_core::SecretRequest,
989    ) -> adk_core::Result<Option<String>> {
990        match &self.parent {
991            Some(parent) => parent.get_secret_for(request).await,
992            None => Ok(None),
993        }
994    }
995}
996
997/// Minimal Session implementation for graph execution
998struct GraphSession {
999    id: String,
1000    state: GraphState,
1001    history: std::sync::RwLock<Vec<adk_core::Content>>,
1002}
1003
1004impl GraphSession {
1005    fn new(id: String) -> Self {
1006        Self { id, state: GraphState::new(), history: std::sync::RwLock::new(Vec::new()) }
1007    }
1008
1009    fn append_content(&self, content: adk_core::Content) {
1010        if let Ok(mut h) = self.history.write() {
1011            h.push(content);
1012        }
1013    }
1014}
1015
1016impl adk_core::Session for GraphSession {
1017    fn id(&self) -> &str {
1018        &self.id
1019    }
1020
1021    fn app_name(&self) -> &str {
1022        "graph_app"
1023    }
1024
1025    fn user_id(&self) -> &str {
1026        "graph_user"
1027    }
1028
1029    fn state(&self) -> &dyn adk_core::State {
1030        &self.state
1031    }
1032
1033    fn conversation_history(&self) -> Vec<adk_core::Content> {
1034        self.history.read().ok().map(|h| h.clone()).unwrap_or_default()
1035    }
1036
1037    fn append_to_history(&self, content: adk_core::Content) {
1038        self.append_content(content);
1039    }
1040}
1041
1042/// Minimal State implementation for graph execution
1043struct GraphState {
1044    data: std::sync::RwLock<std::collections::HashMap<String, serde_json::Value>>,
1045}
1046
1047impl GraphState {
1048    fn new() -> Self {
1049        Self { data: std::sync::RwLock::new(std::collections::HashMap::new()) }
1050    }
1051}
1052
1053impl adk_core::State for GraphState {
1054    fn get(&self, key: &str) -> Option<serde_json::Value> {
1055        self.data.read().ok()?.get(key).cloned()
1056    }
1057
1058    fn set(&mut self, key: String, value: serde_json::Value) {
1059        if let Err(msg) = adk_core::validate_state_key(&key) {
1060            tracing::warn!(key = %key, "rejecting invalid state key: {msg}");
1061            return;
1062        }
1063        if let Ok(mut data) = self.data.write() {
1064            data.insert(key, value);
1065        }
1066    }
1067
1068    fn all(&self) -> std::collections::HashMap<String, serde_json::Value> {
1069        self.data.read().ok().map(|d| d.clone()).unwrap_or_default()
1070    }
1071}
1072
1073#[cfg(test)]
1074mod tests {
1075    use super::*;
1076
1077    #[tokio::test]
1078    async fn test_function_node() {
1079        let node = FunctionNode::new("test", |_ctx| async {
1080            Ok(NodeOutput::new().with_update("result", serde_json::json!("success")))
1081        });
1082
1083        assert_eq!(node.name(), "test");
1084
1085        let ctx = NodeContext::new(State::new(), ExecutionConfig::default(), 0);
1086        let output = node.execute(&ctx).await.unwrap();
1087
1088        assert_eq!(output.updates.get("result"), Some(&serde_json::json!("success")));
1089    }
1090
1091    #[tokio::test]
1092    async fn test_passthrough_node() {
1093        let node = PassthroughNode::new("pass");
1094        let ctx = NodeContext::new(State::new(), ExecutionConfig::default(), 0);
1095        let output = node.execute(&ctx).await.unwrap();
1096
1097        assert!(output.updates.is_empty());
1098        assert!(output.interrupt.is_none());
1099    }
1100
1101    #[test]
1102    fn test_node_output_builder() {
1103        let output = NodeOutput::new().with_update("a", 1).with_update("b", "hello");
1104
1105        assert_eq!(output.updates.get("a"), Some(&serde_json::json!(1)));
1106        assert_eq!(output.updates.get("b"), Some(&serde_json::json!("hello")));
1107    }
1108}