Skip to main content

adk_graph/
agent.rs

1//! GraphAgent - ADK Agent integration for graph workflows
2//!
3//! Provides a builder pattern similar to LlmAgent and RealtimeAgent.
4
5use crate::checkpoint::Checkpointer;
6use crate::deferred::DeferredNodeConfig;
7use crate::edge::{END, Edge, EdgeTarget, START};
8use crate::error::{GraphError, Result};
9use crate::graph::{CompiledGraph, StateGraph};
10use crate::node::{ExecutionConfig, FunctionNode, Node, NodeContext, NodeOutput};
11use crate::state::{State, StateSchema};
12use crate::stream::{StreamEvent, StreamMode};
13use crate::timeout::TimeoutPolicy;
14use adk_core::{
15    Agent, AgentCapabilities, AgentRelationshipKind, AgentTopology, AgentTopologyMember,
16    AgentTopologyRelationship, Content, Event, EventStream, InvocationContext,
17};
18use async_trait::async_trait;
19use serde_json::json;
20use std::collections::HashMap;
21use std::future::Future;
22use std::pin::Pin;
23use std::sync::Arc;
24
25/// Type alias for callbacks
26pub type BeforeAgentCallback = Arc<
27    dyn Fn(Arc<dyn InvocationContext>) -> Pin<Box<dyn Future<Output = adk_core::Result<()>> + Send>>
28        + Send
29        + Sync,
30>;
31
32pub type AfterAgentCallback = Arc<
33    dyn Fn(
34            Arc<dyn InvocationContext>,
35            Event,
36        ) -> Pin<Box<dyn Future<Output = adk_core::Result<()>> + Send>>
37        + Send
38        + Sync,
39>;
40
41/// Type alias for input mapper function
42pub type InputMapper = Arc<dyn Fn(&dyn InvocationContext) -> State + Send + Sync>;
43
44/// Type alias for output mapper function
45pub type OutputMapper = Arc<dyn Fn(&State) -> Vec<Event> + Send + Sync>;
46
47/// GraphAgent wraps a CompiledGraph as an ADK Agent
48///
49/// # Turns and checkpoints
50///
51/// Each [`Agent::run`] call is one turn on a thread whose id is the session id.
52/// With a checkpointer configured:
53///
54/// | Thread's latest checkpoint | What the turn does |
55/// |----------------------------|--------------------|
56/// | None | Runs the graph from its entry nodes. |
57/// | Paused (non-empty frontier) | Resumes the paused run, with the turn's input merged into its state. |
58/// | Finished (empty frontier) | Runs the graph again from its entry nodes, starting from the previous turn's final state with the turn's input merged on top. |
59///
60/// The recursion limit applies to each turn separately. [`GraphAgent::invoke`]
61/// keeps the direct-graph behaviour, where re-invoking a finished thread returns
62/// its final state without running anything.
63pub struct GraphAgent {
64    name: String,
65    description: String,
66    graph: Arc<CompiledGraph>,
67    /// Map InvocationContext to graph input state
68    input_mapper: InputMapper,
69    /// Map graph output state to ADK Events
70    output_mapper: OutputMapper,
71    /// Before agent callback
72    before_callback: Option<BeforeAgentCallback>,
73    /// After agent callback
74    after_callback: Option<AfterAgentCallback>,
75}
76
77impl GraphAgent {
78    /// Create a new GraphAgent builder
79    pub fn builder(name: &str) -> GraphAgentBuilder {
80        GraphAgentBuilder::new(name)
81    }
82
83    /// Create directly from a compiled graph
84    pub fn from_graph(name: &str, graph: CompiledGraph) -> Self {
85        Self {
86            name: name.to_string(),
87            description: String::new(),
88            graph: Arc::new(graph),
89            input_mapper: Arc::new(default_input_mapper),
90            output_mapper: Arc::new(default_output_mapper),
91            before_callback: None,
92            after_callback: None,
93        }
94    }
95
96    /// Build a `GraphAgent` from a `WorkflowSchema`.
97    ///
98    /// Delegates to `schema.build_graph()` to construct the graph from the
99    /// workflow schema's action nodes, edges, and conditions.
100    #[cfg(feature = "action")]
101    pub fn from_workflow_schema(
102        name: &str,
103        schema: &crate::workflow::WorkflowSchema,
104    ) -> Result<Self> {
105        schema.build_graph(name)
106    }
107
108    /// Get the underlying compiled graph
109    pub fn graph(&self) -> &CompiledGraph {
110        &self.graph
111    }
112
113    /// Execute the graph directly (bypassing Agent trait)
114    pub async fn invoke(&self, input: State, config: ExecutionConfig) -> Result<State> {
115        self.graph.invoke(input, config).await
116    }
117
118    /// Stream execution
119    pub fn stream(
120        &self,
121        input: State,
122        config: ExecutionConfig,
123        mode: StreamMode,
124    ) -> impl futures::Stream<Item = Result<StreamEvent>> + '_ {
125        self.graph.stream(input, config, mode)
126    }
127}
128
129#[async_trait]
130impl Agent for GraphAgent {
131    fn name(&self) -> &str {
132        &self.name
133    }
134
135    fn description(&self) -> &str {
136        &self.description
137    }
138
139    fn sub_agents(&self) -> &[Arc<dyn Agent>] {
140        &[]
141    }
142
143    fn supports_agent_transfer(&self) -> bool {
144        false
145    }
146
147    fn capabilities(&self) -> AgentCapabilities {
148        AgentCapabilities {
149            checkpoint_resume: self.graph.has_checkpointer(),
150            shared_state: true,
151            invocation_metadata: true,
152            ..AgentCapabilities::default()
153        }
154    }
155
156    fn topology(&self) -> Option<AgentTopology> {
157        let mut nodes = self.graph.nodes.values().collect::<Vec<_>>();
158        nodes.sort_by_key(|node| node.name().to_string());
159        let mut members = vec![AgentTopologyMember {
160            name: self.name.clone(),
161            description: self.description.clone(),
162            coordinator: true,
163            capabilities: self.capabilities(),
164        }];
165        members.extend(nodes.into_iter().map(|node| AgentTopologyMember {
166            name: node.name().to_string(),
167            description: node.description().to_string(),
168            coordinator: false,
169            capabilities: node.capabilities(),
170        }));
171
172        let mut relationships = self
173            .graph
174            .get_entry_nodes()
175            .into_iter()
176            .map(|entry| AgentTopologyRelationship {
177                from: self.name.clone(),
178                to: entry,
179                kind: AgentRelationshipKind::Flow,
180            })
181            .collect::<Vec<_>>();
182        for edge in &self.graph.edges {
183            match edge {
184                Edge::Direct { source, target: EdgeTarget::Node(target) } => {
185                    relationships.push(AgentTopologyRelationship {
186                        from: source.clone(),
187                        to: target.clone(),
188                        kind: AgentRelationshipKind::Flow,
189                    });
190                }
191                Edge::Conditional { source, targets, .. } => {
192                    relationships.extend(targets.values().filter_map(|target| {
193                        target.node_name().map(|target| AgentTopologyRelationship {
194                            from: source.clone(),
195                            to: target.to_string(),
196                            kind: AgentRelationshipKind::Flow,
197                        })
198                    }));
199                }
200                Edge::Direct { target: EdgeTarget::End, .. } | Edge::Entry { .. } => {}
201            }
202        }
203        relationships.sort_by(|left, right| (&left.from, &left.to).cmp(&(&right.from, &right.to)));
204        relationships.dedup_by(|left, right| left.from == right.from && left.to == right.to);
205
206        Some(AgentTopology {
207            root: self.name.clone(),
208            coordinator: self.name.clone(),
209            members,
210            relationships,
211        })
212    }
213
214    async fn run(&self, ctx: Arc<dyn InvocationContext>) -> adk_core::Result<EventStream> {
215        // Call before callback
216        if let Some(callback) = &self.before_callback {
217            callback(ctx.clone()).await?;
218        }
219
220        // Map context to input state
221        let input = (self.input_mapper)(ctx.as_ref());
222
223        // Create execution config from context, carrying the invocation so an
224        // `AgentNode` inside the graph presents this run's identity, services, and
225        // cancellation rather than a synthetic standalone context.
226        let config = ExecutionConfig::new(ctx.session_id()).with_parent_context(ctx.clone());
227
228        // Execute graph
229        let graph = self.graph.clone();
230        let output_mapper = self.output_mapper.clone();
231        let after_callback = self.after_callback.clone();
232        let ctx_clone = ctx.clone();
233
234        let stream = async_stream::stream! {
235            // Each call is one turn on the session's thread: a paused thread
236            // resumes, and a finished one runs again from its entry nodes with the
237            // previous turn's state carried over.
238            let mut executor =
239                crate::executor::PregelExecutor::new(&graph, config).restarting_completed_threads();
240            match executor.run(input).await {
241                Ok(state) => {
242                    let events = output_mapper(&state);
243                    for event in events {
244                        // Call after callback for each event
245                        if let Some(callback) = &after_callback
246                            && let Err(e) = callback(ctx_clone.clone(), event.clone()).await {
247                                yield Err(e);
248                                return;
249                            }
250                        yield Ok(event);
251                    }
252                }
253                Err(GraphError::Interrupted(interrupt)) => {
254                    // The `Agent` trait yields events, so an interrupt cannot be
255                    // returned as an error without ending the invocation. Emit one
256                    // event carrying the structured pause so a caller can read the
257                    // node, the payload, and the checkpoint to resume from.
258                    let tool_confirmation = crate::interrupt::GraphToolConfirmationPause::from_interrupted_execution(&interrupt);
259                    let payload = tool_confirmation.clone().map_or_else(
260                        || crate::interrupt::GraphInterruptPayload::new(
261                            &interrupt.interrupt,
262                            &interrupt.thread_id,
263                            &interrupt.checkpoint_id,
264                        ),
265                        crate::interrupt::GraphInterruptPayload::from_tool_confirmation_pause,
266                    );
267                    let mut event = Event::new("graph_interrupted");
268                    if let Some(pause) = tool_confirmation {
269                        event.llm_response.interrupted = true;
270                        event.llm_response.turn_complete = true;
271                        event.actions.tool_confirmation = Some(pause.request);
272                    }
273                    event.set_content(
274                        Content::new("assistant").with_text(interrupt.interrupt.to_string()),
275                    );
276                    event.provider_metadata.insert(
277                        crate::interrupt::INTERRUPT_METADATA_KEY.to_string(),
278                        payload.to_metadata_value(),
279                    );
280                    yield Ok(event);
281                }
282                Err(e) => {
283                    yield Err(adk_core::AdkError::agent(e.to_string()));
284                }
285            }
286        };
287
288        Ok(Box::pin(stream))
289    }
290}
291
292/// Default input mapper - extracts content from InvocationContext
293fn default_input_mapper(ctx: &dyn InvocationContext) -> State {
294    let mut state = State::new();
295
296    // Get user content
297    let content = ctx.user_content();
298    let text: String = content.parts.iter().filter_map(|p| p.text()).collect::<Vec<_>>().join("\n");
299
300    if !text.is_empty() {
301        state.insert("input".to_string(), json!(text));
302        state.insert("messages".to_string(), json!([{"role": "user", "content": text}]));
303    }
304
305    // Add session ID
306    state.insert("session_id".to_string(), json!(ctx.session_id()));
307
308    state
309}
310
311/// Default output mapper - creates events from state
312fn default_output_mapper(state: &State) -> Vec<Event> {
313    let mut events = Vec::new();
314
315    // Try to get output from common fields
316    let output_text = state
317        .get("output")
318        .and_then(|v| v.as_str())
319        .or_else(|| state.get("result").and_then(|v| v.as_str()))
320        .or_else(|| {
321            state
322                .get("messages")
323                .and_then(|v| v.as_array())
324                .and_then(|arr| arr.last())
325                .and_then(|msg| msg.get("content"))
326                .and_then(|c| c.as_str())
327        });
328
329    let text = if let Some(text) = output_text {
330        text.to_string()
331    } else {
332        // Return the full state as JSON
333        serde_json::to_string_pretty(state).unwrap_or_default()
334    };
335
336    let mut event = Event::new("graph_output");
337    event.set_content(Content::new("assistant").with_text(&text));
338    events.push(event);
339
340    events
341}
342
343/// Builder for GraphAgent
344pub struct GraphAgentBuilder {
345    name: String,
346    description: String,
347    schema: StateSchema,
348    nodes: Vec<Arc<dyn Node>>,
349    edges: Vec<Edge>,
350    checkpointer: Option<Arc<dyn Checkpointer>>,
351    interrupt_before: Vec<String>,
352    interrupt_after: Vec<String>,
353    recursion_limit: usize,
354    max_concurrency: Option<usize>,
355    input_mapper: Option<InputMapper>,
356    output_mapper: Option<OutputMapper>,
357    before_callback: Option<BeforeAgentCallback>,
358    after_callback: Option<AfterAgentCallback>,
359    timeout_policies: HashMap<String, TimeoutPolicy>,
360    default_timeout: Option<TimeoutPolicy>,
361    deferred_configs: HashMap<String, DeferredNodeConfig>,
362    #[cfg(feature = "node-cache")]
363    cache_policies: HashMap<String, crate::cache::NodeCachePolicy>,
364}
365
366impl GraphAgentBuilder {
367    /// Create a new builder
368    pub fn new(name: &str) -> Self {
369        Self {
370            name: name.to_string(),
371            description: String::new(),
372            schema: StateSchema::simple(&["input", "output", "messages"]),
373            nodes: vec![],
374            edges: vec![],
375            checkpointer: None,
376            interrupt_before: vec![],
377            interrupt_after: vec![],
378            recursion_limit: 100,
379            max_concurrency: None,
380            input_mapper: None,
381            output_mapper: None,
382            before_callback: None,
383            after_callback: None,
384            timeout_policies: HashMap::new(),
385            default_timeout: None,
386            deferred_configs: HashMap::new(),
387            #[cfg(feature = "node-cache")]
388            cache_policies: HashMap::new(),
389        }
390    }
391
392    /// Set description
393    pub fn description(mut self, desc: &str) -> Self {
394        self.description = desc.to_string();
395        self
396    }
397
398    /// Set state schema
399    pub fn state_schema(mut self, schema: StateSchema) -> Self {
400        self.schema = schema;
401        self
402    }
403
404    /// Add channels to state schema
405    pub fn channels(mut self, channels: &[&str]) -> Self {
406        self.schema = StateSchema::simple(channels);
407        self
408    }
409
410    /// Add a node
411    pub fn node<N: Node + 'static>(mut self, node: N) -> Self {
412        self.nodes.push(Arc::new(node));
413        self
414    }
415
416    /// Add a function as a node
417    pub fn node_fn<F, Fut>(mut self, name: &str, func: F) -> Self
418    where
419        F: Fn(NodeContext) -> Fut + Send + Sync + 'static,
420        Fut: Future<Output = Result<NodeOutput>> + Send + 'static,
421    {
422        self.nodes.push(Arc::new(FunctionNode::new(name, func)));
423        self
424    }
425
426    /// Add a direct edge
427    pub fn edge(mut self, source: &str, target: &str) -> Self {
428        let target =
429            if target == END { EdgeTarget::End } else { EdgeTarget::Node(target.to_string()) };
430
431        if source == START {
432            let entry_idx = self.edges.iter().position(|e| matches!(e, Edge::Entry { .. }));
433            match entry_idx {
434                Some(idx) => {
435                    if let Edge::Entry { targets } = &mut self.edges[idx]
436                        && let EdgeTarget::Node(node) = &target
437                        && !targets.contains(node)
438                    {
439                        targets.push(node.clone());
440                    }
441                }
442                None => {
443                    if let EdgeTarget::Node(node) = target {
444                        self.edges.push(Edge::Entry { targets: vec![node] });
445                    }
446                }
447            }
448        } else {
449            self.edges.push(Edge::Direct { source: source.to_string(), target });
450        }
451
452        self
453    }
454
455    /// Add a conditional edge
456    pub fn conditional_edge<F, I>(mut self, source: &str, router: F, targets: I) -> Self
457    where
458        F: Fn(&State) -> String + Send + Sync + 'static,
459        I: IntoIterator<Item = (&'static str, &'static str)>,
460    {
461        let targets_map: HashMap<String, EdgeTarget> = targets
462            .into_iter()
463            .map(|(k, v)| {
464                let target =
465                    if v == END { EdgeTarget::End } else { EdgeTarget::Node(v.to_string()) };
466                (k.to_string(), target)
467            })
468            .collect();
469
470        self.edges.push(Edge::Conditional {
471            source: source.to_string(),
472            router: Arc::new(router),
473            targets: targets_map,
474        });
475
476        self
477    }
478
479    /// Set checkpointer
480    pub fn checkpointer<C: Checkpointer + 'static>(mut self, checkpointer: C) -> Self {
481        self.checkpointer = Some(Arc::new(checkpointer));
482        self
483    }
484
485    /// Set checkpointer with Arc
486    pub fn checkpointer_arc(mut self, checkpointer: Arc<dyn Checkpointer>) -> Self {
487        self.checkpointer = Some(checkpointer);
488        self
489    }
490
491    /// Set nodes to interrupt before
492    pub fn interrupt_before(mut self, nodes: &[&str]) -> Self {
493        self.interrupt_before = nodes.iter().map(|s| s.to_string()).collect();
494        self
495    }
496
497    /// Set nodes to interrupt after
498    pub fn interrupt_after(mut self, nodes: &[&str]) -> Self {
499        self.interrupt_after = nodes.iter().map(|s| s.to_string()).collect();
500        self
501    }
502
503    /// Set recursion limit
504    /// Cap how many nodes execute concurrently within one super-step.
505    ///
506    /// See [`CompiledGraph::with_max_concurrency`](crate::graph::CompiledGraph::with_max_concurrency).
507    pub fn max_concurrency(mut self, limit: usize) -> Self {
508        self.max_concurrency = Some(limit.max(1));
509        self
510    }
511
512    pub fn recursion_limit(mut self, limit: usize) -> Self {
513        self.recursion_limit = limit;
514        self
515    }
516
517    /// Set a timeout policy for a specific node.
518    ///
519    /// The policy is applied when the named node executes, enforcing
520    /// wall-clock and/or idle timeouts with the configured recovery action.
521    ///
522    /// # Example
523    ///
524    /// ```rust,ignore
525    /// use std::time::Duration;
526    /// use adk_graph::timeout::{TimeoutPolicy, OnTimeout};
527    ///
528    /// let agent = GraphAgent::builder("my_graph")
529    ///     .node_timeout("slow_node", TimeoutPolicy {
530    ///         run_timeout: Some(Duration::from_secs(10)),
531    ///         idle_timeout: None,
532    ///         on_timeout: OnTimeout::Fail,
533    ///     })
534    ///     .build()?;
535    /// ```
536    pub fn node_timeout(mut self, node_name: &str, policy: TimeoutPolicy) -> Self {
537        self.timeout_policies.insert(node_name.to_string(), policy);
538        self
539    }
540
541    /// Set a default timeout policy applied to all nodes without an explicit override.
542    ///
543    /// Nodes that have a per-node policy set via [`node_timeout`](Self::node_timeout)
544    /// will use their specific policy instead of this default.
545    ///
546    /// # Example
547    ///
548    /// ```rust,ignore
549    /// use std::time::Duration;
550    /// use adk_graph::timeout::{TimeoutPolicy, OnTimeout};
551    ///
552    /// let agent = GraphAgent::builder("my_graph")
553    ///     .default_timeout(TimeoutPolicy {
554    ///         run_timeout: Some(Duration::from_secs(30)),
555    ///         idle_timeout: Some(Duration::from_secs(5)),
556    ///         on_timeout: OnTimeout::Skip,
557    ///     })
558    ///     .build()?;
559    /// ```
560    pub fn default_timeout(mut self, policy: TimeoutPolicy) -> Self {
561        self.default_timeout = Some(policy);
562        self
563    }
564
565    /// Add a deferred (fan-in barrier) node to the graph.
566    ///
567    /// A deferred node waits for all upstream parallel paths to complete before
568    /// executing. The provided function is wrapped as a [`FunctionNode`] and the
569    /// [`DeferredNodeConfig`] controls how upstream outputs are merged and how
570    /// long the node waits for all paths.
571    ///
572    /// # Arguments
573    ///
574    /// * `name` - The name of the deferred node.
575    /// * `func` - The async function to execute once all upstream paths complete.
576    /// * `config` - Configuration controlling merge strategy and fan-in timeout.
577    ///
578    /// # Example
579    ///
580    /// ```rust,ignore
581    /// use std::time::Duration;
582    /// use adk_graph::deferred::{DeferredNodeConfig, MergeStrategy};
583    /// use adk_graph::node::NodeOutput;
584    ///
585    /// let agent = GraphAgent::builder("scatter_gather")
586    ///     .deferred_node("aggregator", |_ctx| async {
587    ///         Ok(NodeOutput::new().with_update("status", serde_json::json!("merged")))
588    ///     }, DeferredNodeConfig {
589    ///         merge_strategy: MergeStrategy::Collect,
590    ///         fan_in_timeout: Some(Duration::from_secs(30)),
591    ///     })
592    ///     .build()?;
593    /// ```
594    /// Configure fan-in for a node already added with [`node`](Self::node).
595    ///
596    /// [`deferred_node`](Self::deferred_node) both adds and configures a node, so
597    /// a custom `Node` added through `node` had no way to set a merge strategy or
598    /// a fan-in timeout.
599    ///
600    /// A node reached by more than one unconditional edge is deferred
601    /// automatically; this overrides that default.
602    pub fn mark_deferred(mut self, name: &str, config: DeferredNodeConfig) -> Self {
603        self.deferred_configs.insert(name.to_string(), config);
604        self
605    }
606
607    pub fn deferred_node<F, Fut>(mut self, name: &str, func: F, config: DeferredNodeConfig) -> Self
608    where
609        F: Fn(NodeContext) -> Fut + Send + Sync + 'static,
610        Fut: Future<Output = Result<NodeOutput>> + Send + 'static,
611    {
612        self.nodes.push(Arc::new(FunctionNode::new(name, func)));
613        self.deferred_configs.insert(name.to_string(), config);
614        self
615    }
616
617    /// Set a cache policy for a specific node.
618    ///
619    /// When a node has a cache policy, its execution results are cached keyed
620    /// by a blake3 hash of the node name and input state. Subsequent executions
621    /// with identical inputs return the cached result without re-executing the
622    /// node.
623    ///
624    /// # Arguments
625    ///
626    /// * `name` — the name of the node to cache
627    /// * `policy` — the cache policy specifying backend and TTL
628    ///
629    /// # Example
630    ///
631    /// ```rust,ignore
632    /// use std::time::Duration;
633    /// use adk_graph::cache::{CacheBackend, NodeCachePolicy};
634    ///
635    /// let agent = GraphAgent::builder("cached_graph")
636    ///     .node_cache("expensive_node", NodeCachePolicy {
637    ///         backend: CacheBackend::InMemory { max_entries: 128 },
638    ///         ttl: Some(Duration::from_secs(300)),
639    ///     })
640    ///     .build()?;
641    /// ```
642    #[cfg(feature = "node-cache")]
643    pub fn node_cache(mut self, name: &str, policy: crate::cache::NodeCachePolicy) -> Self {
644        self.cache_policies.insert(name.to_string(), policy);
645        self
646    }
647
648    /// Set custom input mapper
649    pub fn input_mapper<F>(mut self, mapper: F) -> Self
650    where
651        F: Fn(&dyn InvocationContext) -> State + Send + Sync + 'static,
652    {
653        self.input_mapper = Some(Arc::new(mapper));
654        self
655    }
656
657    /// Set custom output mapper
658    pub fn output_mapper<F>(mut self, mapper: F) -> Self
659    where
660        F: Fn(&State) -> Vec<Event> + Send + Sync + 'static,
661    {
662        self.output_mapper = Some(Arc::new(mapper));
663        self
664    }
665
666    /// Set before agent callback
667    pub fn before_agent_callback<F, Fut>(mut self, callback: F) -> Self
668    where
669        F: Fn(Arc<dyn InvocationContext>) -> Fut + Send + Sync + 'static,
670        Fut: Future<Output = adk_core::Result<()>> + Send + 'static,
671    {
672        self.before_callback = Some(Arc::new(move |ctx| Box::pin(callback(ctx))));
673        self
674    }
675
676    /// Set after agent callback
677    ///
678    /// Note: The callback receives a cloned Event to avoid lifetime issues.
679    pub fn after_agent_callback<F, Fut>(mut self, callback: F) -> Self
680    where
681        F: Fn(Arc<dyn InvocationContext>, Event) -> Fut + Send + Sync + 'static,
682        Fut: Future<Output = adk_core::Result<()>> + Send + 'static,
683    {
684        self.after_callback = Some(Arc::new(move |ctx, event| {
685            let event_clone = event.clone();
686            Box::pin(callback(ctx, event_clone))
687        }));
688        self
689    }
690
691    /// Add an action node to the graph.
692    ///
693    /// Wraps the `ActionNodeConfig` in an `ActionNodeExecutor` and registers it
694    /// as a node. If the config is a `SwitchNodeConfig`, conditional edges are
695    /// also auto-registered from the switch conditions.
696    #[cfg(feature = "action")]
697    pub fn action_node(mut self, config: adk_action::ActionNodeConfig) -> Self {
698        use crate::action::ActionNodeExecutor;
699
700        // If this is a Switch node, register conditional edges
701        if let adk_action::ActionNodeConfig::Switch(ref switch_config) = config {
702            let conditions = switch_config.conditions.clone();
703            let eval_mode = switch_config.evaluation_mode.clone();
704            let default_branch = switch_config.default_branch.clone();
705            let source = config.standard().id.clone();
706
707            let mut targets_map: HashMap<String, EdgeTarget> = HashMap::new();
708            for condition in &conditions {
709                targets_map.insert(
710                    condition.output_port.clone(),
711                    EdgeTarget::Node(condition.output_port.clone()),
712                );
713            }
714            if let Some(ref default) = default_branch {
715                let target = if default == END {
716                    EdgeTarget::End
717                } else {
718                    EdgeTarget::Node(default.clone())
719                };
720                targets_map.insert(default.clone(), target);
721            }
722            targets_map.insert(END.to_string(), EdgeTarget::End);
723
724            let router = Arc::new(move |state: &State| -> String {
725                match crate::action::switch::evaluate_switch_conditions(
726                    &conditions,
727                    state,
728                    &eval_mode,
729                    default_branch.as_deref(),
730                ) {
731                    Ok(ports) => ports.into_iter().next().unwrap_or_else(|| END.to_string()),
732                    Err(_) => END.to_string(),
733                }
734            });
735
736            self.edges.push(Edge::Conditional { source, router, targets: targets_map });
737        }
738
739        let executor = ActionNodeExecutor::new(config);
740        self.nodes.push(Arc::new(executor));
741        self
742    }
743
744    /// Build the GraphAgent
745    pub fn build(self) -> Result<GraphAgent> {
746        // Build the graph
747        let mut graph = StateGraph::new(self.schema);
748
749        // Add nodes
750        for node in self.nodes {
751            graph.nodes.insert(node.name().to_string(), node);
752        }
753
754        // Add edges
755        graph.edges = self.edges;
756
757        // Compile
758        let mut compiled = graph.compile()?;
759
760        // Configure
761        if let Some(cp) = self.checkpointer {
762            compiled.checkpointer = Some(cp);
763        }
764        compiled.interrupt_before = self.interrupt_before.into_iter().collect();
765        compiled.interrupt_after = self.interrupt_after.into_iter().collect();
766        compiled.recursion_limit = self.recursion_limit;
767        compiled.max_concurrency = self.max_concurrency;
768        compiled.timeout_policies = self.timeout_policies;
769        compiled.default_timeout = self.default_timeout;
770        compiled.deferred_configs = self.deferred_configs;
771
772        #[cfg(feature = "node-cache")]
773        {
774            compiled.node_caches = self
775                .cache_policies
776                .iter()
777                .map(|(name, policy)| {
778                    (name.clone(), Arc::new(crate::cache::NodeCache::from_policy(policy)))
779                })
780                .collect();
781            compiled.cache_policies = self.cache_policies;
782        }
783
784        Ok(GraphAgent {
785            name: self.name,
786            description: self.description,
787            graph: Arc::new(compiled),
788            input_mapper: self.input_mapper.unwrap_or(Arc::new(default_input_mapper)),
789            output_mapper: self.output_mapper.unwrap_or(Arc::new(default_output_mapper)),
790            before_callback: self.before_callback,
791            after_callback: self.after_callback,
792        })
793    }
794}
795
796#[cfg(test)]
797mod tests {
798    use super::*;
799    use serde_json::json;
800
801    #[tokio::test]
802    async fn test_graph_agent_builder() {
803        let agent = GraphAgent::builder("test")
804            .description("Test agent")
805            .channels(&["value"])
806            .node_fn("set", |_ctx| async { Ok(NodeOutput::new().with_update("value", json!(42))) })
807            .edge(START, "set")
808            .edge("set", END)
809            .build()
810            .unwrap();
811
812        assert_eq!(agent.name(), "test");
813        assert_eq!(agent.description(), "Test agent");
814
815        // Test direct invocation
816        let result = agent.invoke(State::new(), ExecutionConfig::new("test")).await.unwrap();
817
818        assert_eq!(result.get("value"), Some(&json!(42)));
819
820        let topology = agent.topology().expect("graph topology");
821        assert_eq!(topology.root, "test");
822        assert_eq!(topology.coordinator, "test");
823        assert_eq!(topology.members.len(), 2);
824        assert_eq!(topology.relationships.len(), 1);
825        assert_eq!(topology.relationships[0].from, "test");
826        assert_eq!(topology.relationships[0].to, "set");
827        assert_eq!(topology.relationships[0].kind, AgentRelationshipKind::Flow);
828    }
829}