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    /// Execute the node and return state updates
352    async fn execute(&self, ctx: &NodeContext) -> Result<NodeOutput>;
353
354    /// Rejects a node that cannot execute, before the graph runs.
355    ///
356    /// Called for every node by [`StateGraph::compile`](crate::graph::StateGraph::compile), so a configuration whose
357    /// backend is unavailable fails while the graph is being built rather than
358    /// part-way through a run, when earlier nodes may already have had side effects.
359    ///
360    /// # Errors
361    ///
362    /// Returns an error describing what is unavailable. The default accepts the node.
363    /// Checks this node against the schema of the graph that holds it.
364    ///
365    /// Called by [`StateGraph::compile`](crate::graph::StateGraph::compile) for
366    /// every node, so a node that has requirements on its parent states them
367    /// before anything runs. Defaults to accepting any parent.
368    ///
369    /// [`SubgraphNode`](crate::subgraph::SubgraphNode) uses this to reject a
370    /// channel mapping that names a channel neither side declares.
371    fn validate_against(&self, _parent: &crate::state::StateSchema) -> Result<()> {
372        Ok(())
373    }
374
375    fn validate(&self) -> Result<()> {
376        Ok(())
377    }
378
379    /// Streams execution events for this node.
380    ///
381    /// An implementation must report the node's state updates by yielding a
382    /// [`StreamEvent::Updates`] event, because a streaming executor takes the
383    /// updates from this stream rather than executing the node a second time.
384    /// The default implementation wraps [`Node::execute`] and does so.
385    fn execute_stream<'a>(
386        &'a self,
387        ctx: &'a NodeContext,
388    ) -> Pin<Box<dyn futures::Stream<Item = Result<StreamEvent>> + Send + 'a>> {
389        let name = self.name().to_string();
390        Box::pin(async_stream::stream! {
391            match self.execute(ctx).await {
392                Ok(output) => {
393                    for event in output.events {
394                        yield Ok(event);
395                    }
396                    // A goto and an interrupt have no other way through: this path
397                    // yields events, not a NodeOutput, so the executor reads both
398                    // back off the stream.
399                    if let Some(targets) = output.goto {
400                        yield Ok(StreamEvent::route_dispatched(&name, targets));
401                    }
402                    if let Some(interrupt) = output.interrupt {
403                        let (message, data) = match interrupt {
404                            crate::interrupt::Interrupt::Dynamic { message, data } => (message, data),
405                            other => (other.to_string(), None),
406                        };
407                        yield Ok(StreamEvent::node_interrupt(&name, &message, data));
408                    }
409                    yield Ok(StreamEvent::Updates { node: name, updates: output.updates });
410                }
411                Err(e) => yield Err(e),
412            }
413        })
414    }
415}
416
417/// Type alias for boxed node
418pub type BoxedNode = Box<dyn Node>;
419
420/// Type alias for async function signature
421pub type AsyncNodeFn = Box<
422    dyn Fn(NodeContext) -> Pin<Box<dyn Future<Output = Result<NodeOutput>> + Send>> + Send + Sync,
423>;
424
425/// Function node - wraps an async function as a node
426pub struct FunctionNode {
427    name: String,
428    func: AsyncNodeFn,
429}
430
431impl FunctionNode {
432    /// Create a new function node
433    pub fn new<F, Fut>(name: &str, func: F) -> Self
434    where
435        F: Fn(NodeContext) -> Fut + Send + Sync + 'static,
436        Fut: Future<Output = Result<NodeOutput>> + Send + 'static,
437    {
438        Self { name: name.to_string(), func: Box::new(move |ctx| Box::pin(func(ctx))) }
439    }
440}
441
442#[async_trait]
443impl Node for FunctionNode {
444    fn name(&self) -> &str {
445        &self.name
446    }
447
448    async fn execute(&self, ctx: &NodeContext) -> Result<NodeOutput> {
449        // The closure takes an owned context, so everything the executor attached
450        // has to be carried across or the node silently loses it.
451        let mut ctx_owned = NodeContext::new(ctx.state.clone(), ctx.config.clone(), ctx.step);
452        if let Some(handle) = ctx.progress_handle() {
453            ctx_owned.set_progress_handle(handle.clone());
454        }
455        if let Some(invoker) = ctx.child_invoker() {
456            ctx_owned.set_child_invoker(invoker);
457        }
458        if let Some(schema) = ctx.parent_schema() {
459            ctx_owned.set_parent_schema(schema);
460        }
461        (self.func)(ctx_owned).await
462    }
463}
464
465/// Passthrough node - just passes state through unchanged
466pub struct PassthroughNode {
467    name: String,
468}
469
470impl PassthroughNode {
471    /// Create a new passthrough node
472    pub fn new(name: &str) -> Self {
473        Self { name: name.to_string() }
474    }
475}
476
477#[async_trait]
478impl Node for PassthroughNode {
479    fn name(&self) -> &str {
480        &self.name
481    }
482
483    async fn execute(&self, _ctx: &NodeContext) -> Result<NodeOutput> {
484        Ok(NodeOutput::new())
485    }
486}
487
488/// Type alias for agent node input mapper
489pub type AgentInputMapper = Box<dyn Fn(&State) -> adk_core::Content + Send + Sync>;
490
491/// Type alias for agent node output mapper
492pub type AgentOutputMapper =
493    Box<dyn Fn(&[adk_core::Event]) -> HashMap<String, Value> + Send + Sync>;
494
495/// Chooses an [`AgentNode`]'s successors from the updates it just produced.
496///
497/// Returning `None` leaves the node's declared edges in charge.
498pub type AgentGotoMapper =
499    Box<dyn Fn(&HashMap<String, Value>) -> Option<Vec<String>> + Send + Sync>;
500
501/// Wrapper to use an existing ADK Agent as a graph node
502pub struct AgentNode {
503    name: String,
504    #[allow(dead_code)]
505    agent: Arc<dyn adk_core::Agent>,
506    /// Map state to agent input content
507    input_mapper: AgentInputMapper,
508    /// Map agent events to state updates
509    output_mapper: AgentOutputMapper,
510    /// Choose successors from the mapped updates, replacing declared edges.
511    goto_mapper: Option<AgentGotoMapper>,
512}
513
514impl AgentNode {
515    /// Create a new agent node
516    pub fn new(agent: Arc<dyn adk_core::Agent>) -> Self {
517        let name = agent.name().to_string();
518        Self {
519            name,
520            agent,
521            input_mapper: Box::new(default_input_mapper),
522            output_mapper: Box::new(default_output_mapper),
523            goto_mapper: None,
524        }
525    }
526
527    /// Set custom input mapper
528    pub fn with_input_mapper<F>(mut self, mapper: F) -> Self
529    where
530        F: Fn(&State) -> adk_core::Content + Send + Sync + 'static,
531    {
532        self.input_mapper = Box::new(mapper);
533        self
534    }
535
536    /// Set custom output mapper
537    pub fn with_output_mapper<F>(mut self, mapper: F) -> Self
538    where
539        F: Fn(&[adk_core::Event]) -> HashMap<String, Value> + Send + Sync + 'static,
540    {
541        self.output_mapper = Box::new(mapper);
542        self
543    }
544
545    /// Chooses this node's successors from the updates the output mapper produced.
546    ///
547    /// An agent's answer often decides where control goes next. The output mapper
548    /// turns the agent's events into state; this turns that state into a route,
549    /// so the classification is parsed once.
550    ///
551    /// Returning `None` leaves the declared edges in charge. Returning targets
552    /// replaces them, exactly as [`NodeOutput::with_goto`] does for a plain node.
553    ///
554    /// # Example
555    ///
556    /// ```no_run
557    /// # use adk_graph::node::AgentNode;
558    /// # use std::collections::HashMap;
559    /// # fn wire(node: AgentNode) -> AgentNode {
560    /// node.with_goto_mapper(|updates: &HashMap<String, serde_json::Value>| {
561    ///     match updates.get("category").and_then(|v| v.as_str()) {
562    ///         Some("refund") => Some(vec!["refund_desk".to_string()]),
563    ///         Some(_) => Some(vec!["general_desk".to_string()]),
564    ///         None => None,
565    ///     }
566    /// })
567    /// # }
568    /// ```
569    pub fn with_goto_mapper<F>(mut self, mapper: F) -> Self
570    where
571        F: Fn(&HashMap<String, Value>) -> Option<Vec<String>> + Send + Sync + 'static,
572    {
573        self.goto_mapper = Some(Box::new(mapper));
574        self
575    }
576}
577
578/// Default input mapper - looks for "messages" or "input" in state
579fn default_input_mapper(state: &State) -> adk_core::Content {
580    // Try to get messages first
581    if let Some(messages) = state.get("messages")
582        && let Some(arr) = messages.as_array()
583        && let Some(last) = arr.last()
584        && let Some(content) = last.get("content").and_then(|c| c.as_str())
585    {
586        return adk_core::Content::new("user").with_text(content);
587    }
588
589    // Try input field
590    if let Some(input) = state.get("input")
591        && let Some(text) = input.as_str()
592    {
593        return adk_core::Content::new("user").with_text(text);
594    }
595
596    adk_core::Content::new("user")
597}
598
599/// Default output mapper - extracts text content to "messages"
600fn default_output_mapper(events: &[adk_core::Event]) -> HashMap<String, Value> {
601    let mut updates = HashMap::new();
602
603    // Collect text from events
604    let mut messages = Vec::new();
605    for event in events {
606        if let Some(content) = event.content() {
607            let text = content.parts.iter().filter_map(|p| p.text()).collect::<Vec<_>>().join("");
608
609            if !text.is_empty() {
610                messages.push(serde_json::json!({
611                    "role": "assistant",
612                    "content": text
613                }));
614            }
615        }
616    }
617
618    if !messages.is_empty() {
619        updates.insert("messages".to_string(), serde_json::json!(messages));
620    }
621
622    updates
623}
624
625#[async_trait]
626impl Node for AgentNode {
627    fn name(&self) -> &str {
628        &self.name
629    }
630
631    async fn execute(&self, ctx: &NodeContext) -> Result<NodeOutput> {
632        use futures::StreamExt;
633
634        // Map state to input content
635        let content = (self.input_mapper)(&ctx.state);
636
637        // Create a graph invocation context with the agent
638        let invocation_ctx = Arc::new(GraphInvocationContext::with_parent(
639            ctx.config.thread_id.clone(),
640            content,
641            self.agent.clone(),
642            ctx.config.parent_context.clone(),
643        ));
644
645        // Run the agent and collect events
646        let stream = self.agent.run(invocation_ctx).await.map_err(|e| {
647            crate::error::GraphError::NodeExecutionFailed {
648                node: self.name.clone(),
649                message: e.to_string(),
650            }
651        })?;
652
653        let events: Vec<adk_core::Event> = stream.filter_map(|r| async { r.ok() }).collect().await;
654
655        // Map events to state updates
656        let updates = (self.output_mapper)(&events);
657        let goto = self.goto_mapper.as_ref().and_then(|mapper| mapper(&updates));
658
659        // Convert agent events to stream events for tracing
660        let mut output = NodeOutput::new().with_updates(updates);
661        if let Some(targets) = goto {
662            output = output.with_goto(targets);
663        }
664        for event in &events {
665            if let Ok(json) = serde_json::to_value(event) {
666                output = output.with_event(StreamEvent::custom(&self.name, "agent_event", json));
667            }
668        }
669
670        Ok(output)
671    }
672
673    fn execute_stream<'a>(
674        &'a self,
675        ctx: &'a NodeContext,
676    ) -> Pin<Box<dyn futures::Stream<Item = Result<StreamEvent>> + Send + 'a>> {
677        use futures::StreamExt;
678        let name = self.name.clone();
679        let agent = self.agent.clone();
680        let input_mapper = &self.input_mapper;
681        let output_mapper = &self.output_mapper;
682        let goto_mapper = &self.goto_mapper;
683        let parent_context = ctx.config.parent_context.clone();
684        let thread_id = ctx.config.thread_id.clone();
685        let content = (input_mapper)(&ctx.state);
686
687        Box::pin(async_stream::stream! {
688            tracing::debug!("AgentNode::execute_stream called for {}", name);
689            let invocation_ctx = Arc::new(GraphInvocationContext::with_parent(
690                thread_id,
691                content,
692                agent.clone(),
693                parent_context,
694            ));
695
696            let stream = match agent.run(invocation_ctx).await {
697                Ok(s) => s,
698                Err(e) => {
699                    yield Err(crate::error::GraphError::NodeExecutionFailed {
700                        node: name.clone(),
701                        message: e.to_string(),
702                    });
703                    return;
704                }
705            };
706
707            tokio::pin!(stream);
708            let mut all_events = Vec::new();
709
710            while let Some(result) = stream.next().await {
711                match result {
712                    Ok(event) => {
713                        // Emit streaming event immediately
714                        if let Some(content) = event.content() {
715                            let text: String = content.parts.iter().filter_map(|p| p.text()).collect();
716                            if !text.is_empty() {
717                                yield Ok(StreamEvent::Message {
718                                    node: name.clone(),
719                                    content: text,
720                                    is_final: false,
721                                });
722                            }
723                        }
724                        all_events.push(event);
725                    }
726                    Err(e) => {
727                        yield Err(crate::error::GraphError::NodeExecutionFailed {
728                            node: name.clone(),
729                            message: e.to_string(),
730                        });
731                        return;
732                    }
733                }
734            }
735
736            // Emit final events
737            for event in &all_events {
738                if let Ok(json) = serde_json::to_value(event) {
739                    yield Ok(StreamEvent::custom(&name, "agent_event", json));
740                }
741            }
742
743            // Report state updates from this run. Without this the streaming
744            // executor has no updates to apply and would have to run the agent
745            // a second time to obtain them.
746            let updates = (output_mapper)(&all_events);
747            // Same route the plain path uses: this yields events, not a NodeOutput.
748            if let Some(targets) = goto_mapper.as_ref().and_then(|mapper| mapper(&updates)) {
749                yield Ok(StreamEvent::route_dispatched(&name, targets));
750            }
751            yield Ok(StreamEvent::Updates { node: name.clone(), updates });
752        })
753    }
754}
755
756/// Full InvocationContext implementation for running agents within graph nodes
757struct GraphInvocationContext {
758    invocation_id: String,
759    user_content: adk_core::Content,
760    agent: Arc<dyn adk_core::Agent>,
761    session: Arc<GraphSession>,
762    run_config: adk_core::RunConfig,
763    ended: std::sync::atomic::AtomicBool,
764    /// The invocation this graph run belongs to, when it has one.
765    ///
766    /// Present: identity, services, request context, and cancellation come from the
767    /// caller, so an agent behaves the same inside a graph as outside it.
768    /// Absent: standalone mode, with the synthetic identity below.
769    parent: Option<Arc<dyn adk_core::InvocationContext>>,
770    /// Identity strings, owned because the trait returns them by reference.
771    user_id: String,
772    app_name: String,
773    branch: String,
774}
775
776/// Identity used when a graph runs with no parent invocation.
777const STANDALONE_USER_ID: &str = "graph_user";
778/// Application name used when a graph runs with no parent invocation.
779const STANDALONE_APP_NAME: &str = "graph_app";
780
781impl GraphInvocationContext {
782    fn with_parent(
783        session_id: String,
784        user_content: adk_core::Content,
785        agent: Arc<dyn adk_core::Agent>,
786        parent: Option<Arc<dyn adk_core::InvocationContext>>,
787    ) -> Self {
788        let invocation_id = uuid::Uuid::new_v4().to_string();
789        let session = Arc::new(GraphSession::new(session_id));
790        // Add user content to history
791        session.append_content(user_content.clone());
792
793        // A node runs on its own branch below the caller's, so events it produces are
794        // attributable and do not read as the parent agent's own turn.
795        let (user_id, app_name, branch, run_config) = match parent.as_ref() {
796            Some(parent) => (
797                parent.user_id().to_string(),
798                parent.app_name().to_string(),
799                match parent.branch() {
800                    "" => agent.name().to_string(),
801                    existing => format!("{existing}.{}", agent.name()),
802                },
803                parent.run_config().clone(),
804            ),
805            None => (
806                STANDALONE_USER_ID.to_string(),
807                STANDALONE_APP_NAME.to_string(),
808                "main".to_string(),
809                adk_core::RunConfig::default(),
810            ),
811        };
812
813        Self {
814            invocation_id,
815            user_content,
816            agent,
817            session,
818            run_config,
819            ended: std::sync::atomic::AtomicBool::new(false),
820            parent,
821            user_id,
822            app_name,
823            branch,
824        }
825    }
826}
827
828// Implement ReadonlyContext (required by CallbackContext)
829impl adk_core::ReadonlyContext for GraphInvocationContext {
830    fn invocation_id(&self) -> &str {
831        &self.invocation_id
832    }
833
834    fn agent_name(&self) -> &str {
835        self.agent.name()
836    }
837
838    fn user_id(&self) -> &str {
839        &self.user_id
840    }
841
842    fn app_name(&self) -> &str {
843        &self.app_name
844    }
845
846    fn session_id(&self) -> &str {
847        &self.session.id
848    }
849
850    fn branch(&self) -> &str {
851        &self.branch
852    }
853
854    fn user_content(&self) -> &adk_core::Content {
855        &self.user_content
856    }
857}
858
859// Implement CallbackContext (required by InvocationContext)
860#[async_trait]
861impl adk_core::CallbackContext for GraphInvocationContext {
862    fn artifacts(&self) -> Option<Arc<dyn adk_core::Artifacts>> {
863        self.parent.as_ref().and_then(|parent| parent.artifacts())
864    }
865
866    fn shared_state(&self) -> Option<Arc<adk_core::SharedState>> {
867        self.parent.as_ref().and_then(|parent| parent.shared_state())
868    }
869}
870
871// Implement InvocationContext
872#[async_trait]
873impl adk_core::InvocationContext for GraphInvocationContext {
874    fn agent(&self) -> Arc<dyn adk_core::Agent> {
875        self.agent.clone()
876    }
877
878    fn memory(&self) -> Option<Arc<dyn adk_core::Memory>> {
879        self.parent.as_ref().and_then(|parent| parent.memory())
880    }
881
882    fn session(&self) -> &dyn adk_core::Session {
883        self.session.as_ref()
884    }
885
886    fn run_config(&self) -> &adk_core::RunConfig {
887        &self.run_config
888    }
889
890    fn end_invocation(&self) {
891        self.ended.store(true, std::sync::atomic::Ordering::SeqCst);
892        if let Some(parent) = &self.parent {
893            parent.end_invocation();
894        }
895    }
896
897    fn ended(&self) -> bool {
898        self.ended.load(std::sync::atomic::Ordering::SeqCst)
899            || self.parent.as_ref().is_some_and(|parent| parent.ended())
900    }
901
902    fn is_cancelled(&self) -> bool {
903        self.parent.as_ref().is_some_and(|parent| parent.is_cancelled())
904    }
905
906    fn user_scopes(&self) -> Vec<String> {
907        self.parent.as_ref().map(|parent| parent.user_scopes()).unwrap_or_default()
908    }
909
910    fn request_metadata(&self) -> std::collections::HashMap<String, Value> {
911        self.parent.as_ref().map(|parent| parent.request_metadata()).unwrap_or_default()
912    }
913
914    async fn get_secret(&self, name: &str) -> adk_core::Result<Option<String>> {
915        match &self.parent {
916            Some(parent) => parent.get_secret(name).await,
917            None => Ok(None),
918        }
919    }
920
921    async fn get_secret_for(
922        &self,
923        request: &adk_core::SecretRequest,
924    ) -> adk_core::Result<Option<String>> {
925        match &self.parent {
926            Some(parent) => parent.get_secret_for(request).await,
927            None => Ok(None),
928        }
929    }
930}
931
932/// Minimal Session implementation for graph execution
933struct GraphSession {
934    id: String,
935    state: GraphState,
936    history: std::sync::RwLock<Vec<adk_core::Content>>,
937}
938
939impl GraphSession {
940    fn new(id: String) -> Self {
941        Self { id, state: GraphState::new(), history: std::sync::RwLock::new(Vec::new()) }
942    }
943
944    fn append_content(&self, content: adk_core::Content) {
945        if let Ok(mut h) = self.history.write() {
946            h.push(content);
947        }
948    }
949}
950
951impl adk_core::Session for GraphSession {
952    fn id(&self) -> &str {
953        &self.id
954    }
955
956    fn app_name(&self) -> &str {
957        "graph_app"
958    }
959
960    fn user_id(&self) -> &str {
961        "graph_user"
962    }
963
964    fn state(&self) -> &dyn adk_core::State {
965        &self.state
966    }
967
968    fn conversation_history(&self) -> Vec<adk_core::Content> {
969        self.history.read().ok().map(|h| h.clone()).unwrap_or_default()
970    }
971
972    fn append_to_history(&self, content: adk_core::Content) {
973        self.append_content(content);
974    }
975}
976
977/// Minimal State implementation for graph execution
978struct GraphState {
979    data: std::sync::RwLock<std::collections::HashMap<String, serde_json::Value>>,
980}
981
982impl GraphState {
983    fn new() -> Self {
984        Self { data: std::sync::RwLock::new(std::collections::HashMap::new()) }
985    }
986}
987
988impl adk_core::State for GraphState {
989    fn get(&self, key: &str) -> Option<serde_json::Value> {
990        self.data.read().ok()?.get(key).cloned()
991    }
992
993    fn set(&mut self, key: String, value: serde_json::Value) {
994        if let Err(msg) = adk_core::validate_state_key(&key) {
995            tracing::warn!(key = %key, "rejecting invalid state key: {msg}");
996            return;
997        }
998        if let Ok(mut data) = self.data.write() {
999            data.insert(key, value);
1000        }
1001    }
1002
1003    fn all(&self) -> std::collections::HashMap<String, serde_json::Value> {
1004        self.data.read().ok().map(|d| d.clone()).unwrap_or_default()
1005    }
1006}
1007
1008#[cfg(test)]
1009mod tests {
1010    use super::*;
1011
1012    #[tokio::test]
1013    async fn test_function_node() {
1014        let node = FunctionNode::new("test", |_ctx| async {
1015            Ok(NodeOutput::new().with_update("result", serde_json::json!("success")))
1016        });
1017
1018        assert_eq!(node.name(), "test");
1019
1020        let ctx = NodeContext::new(State::new(), ExecutionConfig::default(), 0);
1021        let output = node.execute(&ctx).await.unwrap();
1022
1023        assert_eq!(output.updates.get("result"), Some(&serde_json::json!("success")));
1024    }
1025
1026    #[tokio::test]
1027    async fn test_passthrough_node() {
1028        let node = PassthroughNode::new("pass");
1029        let ctx = NodeContext::new(State::new(), ExecutionConfig::default(), 0);
1030        let output = node.execute(&ctx).await.unwrap();
1031
1032        assert!(output.updates.is_empty());
1033        assert!(output.interrupt.is_none());
1034    }
1035
1036    #[test]
1037    fn test_node_output_builder() {
1038        let output = NodeOutput::new().with_update("a", 1).with_update("b", "hello");
1039
1040        assert_eq!(output.updates.get("a"), Some(&serde_json::json!(1)));
1041        assert_eq!(output.updates.get("b"), Some(&serde_json::json!("hello")));
1042    }
1043}