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