Skip to main content

adk_studio/codegen/
mod.rs

1//! Rust code generation from project schemas - Always uses adk-graph
2//!
3//! This module provides code generation for ADK Studio projects, converting
4//! visual workflow definitions into compilable Rust code using adk-graph.
5//!
6//! ## ADK 0.5.0 Features
7//!
8//! - Action nodes use `adk-graph`'s `ActionNodeExecutor` with feature-gated deps
9//! - `WorkflowSchema::build_graph()` for action-node-only workflows
10//! - `adk-action` shared types for cross-crate compatibility
11//! - Provider auto-detection with `provider_from_env()` support
12//! - Structured error envelope (`AdkError`) with retry hints
13//!
14//! ## Features
15//!
16//! - Workflow validation before code generation (Requirements 12.4, 12.5)
17//! - Explanatory comments in generated code (Requirement 12.2)
18//! - Environment variable warnings (Requirement 12.10)
19//! - Support for all agent types: LLM, Sequential, Loop, Parallel, Router
20//! - Action node code generation (Requirements 13.1, 13.2, 13.3)
21
22pub mod action_node_codegen;
23pub mod action_node_types;
24mod validation;
25
26// Backward-compatible re-export so `crate::codegen::action_nodes::*` still works
27pub use action_node_codegen as action_nodes;
28
29pub use validation::{
30    EnvVarRequirement, EnvVarWarning, ValidationError, ValidationErrorCode, ValidationResult,
31    check_env_vars, get_required_env_vars, validate_project,
32};
33
34use crate::schema::{AgentSchema, AgentType, ProjectSchema, ToolConfig};
35use anyhow::{Result, bail};
36
37const DEFAULT_ADK_VERSION: &str = "0.6.0";
38
39/// Detect the LLM provider from a model name string.
40/// Mirrors the TypeScript `detectProviderFromModel()` in `ui/src/data/models.ts`.
41fn detect_provider(model: &str) -> &'static str {
42    let m = model.to_lowercase();
43    if m.contains("gemini") || m.contains("gemma") {
44        "gemini"
45    } else if m.contains("gpt")
46        || m.contains("o1")
47        || m.contains("o3")
48        || m.contains("o4")
49        || m.contains("codex")
50    {
51        "openai"
52    } else if m.contains("claude")
53        && !m.starts_with("us.")
54        && !m.starts_with("eu.")
55        && !m.starts_with("ap.")
56    {
57        "anthropic"
58    } else if m.starts_with("us.") || m.starts_with("eu.") || m.starts_with("ap.") {
59        // Bedrock inference profile IDs (e.g. "us.anthropic.claude-sonnet-4-6")
60        "bedrock"
61    } else if m.contains("deepseek") && !m.contains(':') {
62        // DeepSeek API (no colon = not Ollama tag format)
63        "deepseek"
64    } else if m.contains("sonar") {
65        "perplexity"
66    } else if (m.contains("mistral-large")
67        || m.contains("mistral-small")
68        || m.contains("codestral"))
69        && !m.contains(':')
70    {
71        "mistral"
72    } else if m.contains("accounts/fireworks/") {
73        "fireworks"
74    } else if m.contains("-turbo") && m.contains('/') {
75        "together"
76    } else if m.contains("cohere-command") || (m.contains("mistral") && m.contains("2024")) {
77        "azure-ai"
78    } else if m.contains("llama") || m.contains("mixtral") {
79        // Ollama-style tags have colons (e.g. "llama3.2:3b")
80        if m.contains(':') {
81            "ollama"
82        } else if model.starts_with("Meta-Llama") {
83            "sambanova"
84        } else if m.starts_with("llama-")
85            && m.chars()
86                .nth(6)
87                .map(|c| c.is_ascii_digit())
88                .unwrap_or(false)
89        {
90            "cerebras"
91        } else {
92            "groq"
93        }
94    } else if m.contains("qwen")
95        || m.contains("mistral")
96        || m.contains("codellama")
97        || m.contains("devstral")
98    {
99        "ollama"
100    } else {
101        "gemini" // default
102    }
103}
104
105/// Collect the set of unique providers used across all agents in a project.
106fn collect_providers(project: &ProjectSchema) -> std::collections::HashSet<&'static str> {
107    let mut providers = std::collections::HashSet::new();
108    for agent in project.agents.values() {
109        let model = agent.model.as_deref().unwrap_or("gemini-3.1-flash-lite-preview");
110        providers.insert(detect_provider(model));
111    }
112    // If project has a default_provider set, include it
113    if let Some(ref dp) = project.settings.default_provider {
114        let p = match dp.as_str() {
115            "gemini" | "openai" | "anthropic" | "deepseek" | "groq" | "ollama" | "fireworks"
116            | "together" | "mistral" | "perplexity" | "cerebras" | "sambanova" | "bedrock"
117            | "azure-ai" => dp.as_str(),
118            _ => "gemini",
119        };
120        providers.insert(match p {
121            "openai" => "openai",
122            "anthropic" => "anthropic",
123            "deepseek" => "deepseek",
124            "groq" => "groq",
125            "ollama" => "ollama",
126            "fireworks" => "fireworks",
127            "together" => "together",
128            "mistral" => "mistral",
129            "perplexity" => "perplexity",
130            "cerebras" => "cerebras",
131            "sambanova" => "sambanova",
132            "bedrock" => "bedrock",
133            "azure-ai" => "azure-ai",
134            _ => "gemini",
135        });
136    }
137    providers
138}
139
140/// Generate a Rust project from a project schema
141///
142/// This function validates the project before generating code. If validation
143/// fails, it returns an error with details about what needs to be fixed.
144///
145/// # Arguments
146///
147/// * `project` - The project schema to generate code from
148///
149/// # Returns
150///
151/// * `Ok(GeneratedProject)` - The generated project files
152/// * `Err` - If validation fails or code generation encounters an error
153///
154/// # Requirements
155///
156/// - 12.1: Generate valid, compilable ADK-Rust code
157/// - 12.4: Validate workflow before code generation
158/// - 12.5: Display specific error messages if validation fails
159pub fn generate_rust_project(project: &ProjectSchema) -> Result<GeneratedProject> {
160    // Validate the project before generating code (Requirements 12.4, 12.5)
161    let validation = validate_project(project);
162    if !validation.is_valid() {
163        let error_messages: Vec<String> = validation.errors.iter().map(|e| e.to_string()).collect();
164        bail!("Workflow validation failed:\n{}", error_messages.join("\n"));
165    }
166
167    let files = vec![
168        GeneratedFile {
169            path: "src/main.rs".to_string(),
170            content: generate_main_rs(project),
171        },
172        GeneratedFile {
173            path: "Cargo.toml".to_string(),
174            content: generate_cargo_toml(project),
175        },
176    ];
177
178    Ok(GeneratedProject { files })
179}
180
181/// Generate a Rust project with validation result
182///
183/// This function returns both the generated project and the validation result,
184/// allowing callers to access warnings even when generation succeeds.
185pub fn generate_rust_project_with_validation(
186    project: &ProjectSchema,
187) -> Result<(GeneratedProject, ValidationResult, Vec<EnvVarWarning>)> {
188    let validation = validate_project(project);
189    if !validation.is_valid() {
190        let error_messages: Vec<String> = validation.errors.iter().map(|e| e.to_string()).collect();
191        bail!("Workflow validation failed:\n{}", error_messages.join("\n"));
192    }
193
194    // Check for missing environment variables (Requirement 12.10)
195    let env_warnings = check_env_vars(project);
196
197    let files = vec![
198        GeneratedFile {
199            path: "src/main.rs".to_string(),
200            content: generate_main_rs(project),
201        },
202        GeneratedFile {
203            path: "Cargo.toml".to_string(),
204            content: generate_cargo_toml(project),
205        },
206    ];
207
208    Ok((GeneratedProject { files }, validation, env_warnings))
209}
210
211/// Generate a header comment explaining the workflow structure
212///
213/// This creates a comprehensive comment block at the top of the generated code
214/// that explains:
215/// - Project name and description
216/// - Workflow type and structure
217/// - Agent roles and their connections
218/// - Tools used by each agent
219///
220/// Requirement: 12.2 - Include comments explaining the workflow structure
221fn generate_workflow_header_comment(project: &ProjectSchema) -> String {
222    let mut comment = String::new();
223
224    // Project header
225    comment.push_str("//! ");
226    comment.push_str(&project.name);
227    comment.push_str("\n//!\n");
228
229    if !project.description.is_empty() {
230        comment.push_str("//! ");
231        comment.push_str(&project.description);
232        comment.push_str("\n//!\n");
233    }
234
235    // Workflow structure overview
236    comment.push_str("//! ## Workflow Structure\n//!\n");
237
238    // Determine workflow type
239    let workflow_type = determine_workflow_type(project);
240    comment.push_str("//! **Type:** ");
241    comment.push_str(&workflow_type);
242    comment.push_str("\n//!\n");
243
244    // Agent summary
245    comment.push_str("//! ## Agents\n//!\n");
246
247    // Find top-level agents (not sub-agents)
248    let all_sub_agents: std::collections::HashSet<_> = project
249        .agents
250        .values()
251        .flat_map(|a| a.sub_agents.iter().cloned())
252        .collect();
253
254    for (agent_id, agent) in &project.agents {
255        let is_sub_agent = all_sub_agents.contains(agent_id);
256        let prefix = if is_sub_agent { "  - " } else { "- " };
257
258        comment.push_str("//! ");
259        comment.push_str(prefix);
260        comment.push_str("**");
261        comment.push_str(agent_id);
262        comment.push_str("** (");
263        comment.push_str(&format!("{:?}", agent.agent_type).to_lowercase());
264        comment.push(')');
265
266        // Add brief instruction summary if available
267        if !agent.instruction.is_empty() {
268            let brief = truncate_instruction(&agent.instruction, 60);
269            comment.push_str(": ");
270            comment.push_str(&brief);
271        }
272        comment.push('\n');
273
274        // List tools if any
275        if !agent.tools.is_empty() {
276            comment.push_str("//!   Tools: ");
277            comment.push_str(&agent.tools.join(", "));
278            comment.push('\n');
279        }
280
281        // List sub-agents if any
282        if !agent.sub_agents.is_empty() {
283            comment.push_str("//!   Sub-agents: ");
284            comment.push_str(&agent.sub_agents.join(" → "));
285            comment.push('\n');
286        }
287    }
288
289    // Execution flow
290    comment.push_str("//!\n//! ## Execution Flow\n//!\n");
291    comment.push_str("//! ```text\n");
292    comment.push_str(&generate_flow_diagram(project));
293    comment.push_str("//! ```\n//!\n");
294
295    // Generated timestamp
296    comment.push_str("//! Generated by ADK Studio v2.0\n//!\n");
297
298    // Environment variables section (Requirement 12.10)
299    let env_vars = get_required_env_vars(project);
300    if !env_vars.is_empty() {
301        comment.push_str("//! ## Required Environment Variables\n//!\n");
302        for env_var in &env_vars {
303            if env_var.required {
304                comment.push_str("//! - **");
305            } else {
306                comment.push_str("//! - ");
307            }
308            comment.push_str(&env_var.name);
309            if env_var.required {
310                comment.push_str("** (required)");
311            } else {
312                comment.push_str(" (optional)");
313            }
314            comment.push_str(": ");
315            comment.push_str(&env_var.description);
316            if !env_var.alternatives.is_empty() {
317                comment.push_str(" [alt: ");
318                comment.push_str(&env_var.alternatives.join(", "));
319                comment.push(']');
320            }
321            comment.push('\n');
322        }
323        comment.push_str("//!\n");
324    }
325
326    comment
327}
328
329/// Determine the workflow type based on agents and edges
330fn determine_workflow_type(project: &ProjectSchema) -> String {
331    let has_router = project
332        .agents
333        .values()
334        .any(|a| a.agent_type == AgentType::Router);
335    let has_loop = project
336        .agents
337        .values()
338        .any(|a| a.agent_type == AgentType::Loop);
339    let has_parallel = project
340        .agents
341        .values()
342        .any(|a| a.agent_type == AgentType::Parallel);
343    let has_sequential = project
344        .agents
345        .values()
346        .any(|a| a.agent_type == AgentType::Sequential);
347
348    // Find top-level agents
349    let all_sub_agents: std::collections::HashSet<_> = project
350        .agents
351        .values()
352        .flat_map(|a| a.sub_agents.iter().cloned())
353        .collect();
354    let top_level_count = project
355        .agents
356        .keys()
357        .filter(|id| !all_sub_agents.contains(*id))
358        .count();
359
360    if has_router {
361        "Router-based workflow with conditional branching".to_string()
362    } else if has_loop {
363        "Iterative loop workflow with refinement".to_string()
364    } else if has_parallel {
365        "Parallel execution workflow".to_string()
366    } else if has_sequential || top_level_count > 1 {
367        "Sequential pipeline workflow".to_string()
368    } else {
369        "Single agent workflow".to_string()
370    }
371}
372
373/// Truncate instruction to a brief summary
374fn truncate_instruction(instruction: &str, max_len: usize) -> String {
375    let clean = instruction.replace('\n', " ").trim().to_string();
376    if clean.len() <= max_len {
377        clean
378    } else {
379        format!("{}...", &clean[..max_len.saturating_sub(3)])
380    }
381}
382
383/// Strip {{var}} template variables from instruction text
384///
385/// This is needed because the agent's template system looks at session state,
386/// but Set nodes update graph state. By stripping the template variables from
387/// the instruction and injecting them via the input_mapper instead, we ensure
388/// the variables are properly resolved from graph state.
389fn strip_template_variables(instruction: &str) -> String {
390    let mut result = instruction.to_string();
391    // Find and remove all {{var}} patterns
392    while let Some(start) = result.find("{{") {
393        if let Some(end) = result[start..].find("}}") {
394            let end_pos = start + end + 2;
395            result = format!("{}{}", &result[..start], &result[end_pos..]);
396        } else {
397            break;
398        }
399    }
400    // Clean up any double spaces left behind
401    while result.contains("  ") {
402        result = result.replace("  ", " ");
403    }
404    result.trim().to_string()
405}
406
407/// Generate a simple ASCII flow diagram
408fn generate_flow_diagram(project: &ProjectSchema) -> String {
409    let mut diagram = String::new();
410
411    // Build a simple linear representation of the flow
412    let mut current = "START";
413    let mut visited = std::collections::HashSet::new();
414
415    diagram.push_str("//! START");
416
417    while current != "END" && !visited.contains(current) {
418        visited.insert(current);
419
420        // Find next node(s)
421        let next_edges: Vec<_> = project
422            .workflow
423            .edges
424            .iter()
425            .filter(|e| e.from == current)
426            .collect();
427
428        if next_edges.is_empty() {
429            break;
430        }
431
432        if next_edges.len() == 1 {
433            let next = &next_edges[0].to;
434            // Skip action nodes in the diagram (they're entry points, not execution nodes)
435            if project.action_nodes.contains_key(next) {
436                current = next;
437                continue;
438            }
439            diagram.push_str(" → ");
440            diagram.push_str(next);
441            current = next;
442        } else {
443            // Multiple branches (router)
444            diagram.push_str(" → [");
445            let targets: Vec<_> = next_edges
446                .iter()
447                .map(|e| e.to.as_str())
448                .filter(|t| !project.action_nodes.contains_key(*t))
449                .collect();
450            diagram.push_str(&targets.join(" | "));
451            diagram.push(']');
452            break; // Stop at branching point
453        }
454    }
455
456    diagram.push('\n');
457    diagram
458}
459
460#[derive(Debug, serde::Serialize)]
461pub struct GeneratedProject {
462    pub files: Vec<GeneratedFile>,
463}
464
465#[derive(Debug, serde::Serialize)]
466pub struct GeneratedFile {
467    pub path: String,
468    pub content: String,
469}
470
471fn generate_main_rs(project: &ProjectSchema) -> String {
472    let mut code = String::new();
473
474    // Generate file header with workflow documentation (Requirement 12.2)
475    code.push_str(&generate_workflow_header_comment(project));
476
477    code.push_str("#![allow(unused_imports, unused_variables)]\n\n");
478
479    // Check if any agent uses MCP (handles mcp, mcp_1, mcp_2, etc.)
480    let uses_mcp = project
481        .agents
482        .values()
483        .any(|a| a.tools.iter().any(|t| t == "mcp" || t.starts_with("mcp_")));
484    let uses_browser = project
485        .agents
486        .values()
487        .any(|a| a.tools.contains(&"browser".to_string()));
488
489    // Graph imports
490    code.push_str("use adk_agent::LlmAgentBuilder;\n");
491    code.push_str("use adk_core::ToolContext;\n");
492    code.push_str("use adk_graph::{\n");
493    code.push_str("    edge::{Router, END, START},\n");
494    code.push_str("    graph::StateGraph,\n");
495    code.push_str("    node::{AgentNode, ExecutionConfig, NodeOutput},\n");
496    code.push_str("    state::State,\n");
497    code.push_str("    StreamEvent,\n");
498    code.push_str("};\n");
499    // Import model providers based on what agents actually use
500    let providers = collect_providers(project);
501    if providers.contains("gemini") {
502        code.push_str("use adk_model::gemini::GeminiModel;\n");
503    }
504    if providers.contains("openai") {
505        code.push_str("use adk_model::openai::{OpenAIClient, OpenAIConfig};\n");
506    }
507    if providers.contains("anthropic") {
508        code.push_str("use adk_model::anthropic::{AnthropicClient, AnthropicConfig};\n");
509    }
510    if providers.contains("deepseek") {
511        code.push_str("use adk_model::deepseek::{DeepSeekClient, DeepSeekConfig};\n");
512    }
513    if providers.contains("groq") {
514        code.push_str("use adk_model::groq::{GroqClient, GroqConfig};\n");
515    }
516    if providers.contains("ollama") {
517        code.push_str("use adk_model::ollama::{OllamaModel, OllamaConfig};\n");
518    }
519    if providers.contains("fireworks") {
520        code.push_str("use adk_model::fireworks::{FireworksClient, FireworksConfig};\n");
521    }
522    if providers.contains("together") {
523        code.push_str("use adk_model::together::{TogetherClient, TogetherConfig};\n");
524    }
525    if providers.contains("mistral") {
526        code.push_str("use adk_model::mistral::{MistralClient, MistralConfig};\n");
527    }
528    if providers.contains("perplexity") {
529        code.push_str("use adk_model::perplexity::{PerplexityClient, PerplexityConfig};\n");
530    }
531    if providers.contains("cerebras") {
532        code.push_str("use adk_model::cerebras::{CerebrasClient, CerebrasConfig};\n");
533    }
534    if providers.contains("sambanova") {
535        code.push_str("use adk_model::sambanova::{SambaNovaClient, SambaNovaConfig};\n");
536    }
537    if providers.contains("bedrock") {
538        code.push_str("use adk_model::bedrock::{BedrockClient, BedrockConfig};\n");
539    }
540    if providers.contains("azure-ai") {
541        code.push_str("use adk_model::azure_ai::{AzureAIClient, AzureAIConfig};\n");
542    }
543    code.push_str(
544        "use adk_tool::{FunctionTool, GoogleSearchTool, ExitLoopTool, LoadArtifactsTool};\n",
545    );
546    if uses_mcp || uses_browser {
547        code.push_str("use adk_core::{ReadonlyContext, Toolset, Content};\n");
548    }
549    if uses_mcp {
550        code.push_str("use adk_tool::McpToolset;\n");
551        code.push_str("use rmcp::{ServiceExt, transport::TokioChildProcess};\n");
552        code.push_str("use tokio::process::Command;\n");
553    }
554    if uses_mcp || uses_browser {
555        code.push_str("use async_trait::async_trait;\n");
556    }
557    if uses_browser {
558        code.push_str("use adk_browser::{BrowserSession, BrowserConfig, BrowserToolset};\n");
559    }
560    code.push_str("use anyhow::Result;\n");
561    code.push_str("use serde_json::{json, Value};\n");
562    code.push_str("use std::sync::Arc;\n");
563    code.push_str("use tracing_subscriber::{fmt, EnvFilter};\n\n");
564
565    // Add MinimalContext for MCP/browser toolset initialization
566    if uses_mcp || uses_browser {
567        code.push_str("// Minimal context for toolset initialization\n");
568        code.push_str("struct MinimalContext { content: Content }\n");
569        code.push_str("impl MinimalContext { fn new() -> Self { Self { content: Content { role: String::new(), parts: vec![] } } } }\n");
570        code.push_str("#[async_trait]\n");
571        code.push_str("impl ReadonlyContext for MinimalContext {\n");
572        code.push_str("    fn invocation_id(&self) -> &str { \"init\" }\n");
573        code.push_str("    fn agent_name(&self) -> &str { \"init\" }\n");
574        code.push_str("    fn user_id(&self) -> &str { \"init\" }\n");
575        code.push_str("    fn app_name(&self) -> &str { \"init\" }\n");
576        code.push_str("    fn session_id(&self) -> &str { \"init\" }\n");
577        code.push_str("    fn branch(&self) -> &str { \"main\" }\n");
578        code.push_str("    fn user_content(&self) -> &Content { &self.content }\n");
579        code.push_str("}\n\n");
580    }
581
582    // Generate function tools with parameter schemas
583    for (agent_id, agent) in &project.agents {
584        for tool_type in &agent.tools {
585            if tool_type.starts_with("function") {
586                let tool_id = format!("{}_{}", agent_id, tool_type);
587                if let Some(ToolConfig::Function(config)) = project.tool_configs.get(&tool_id) {
588                    code.push_str(&generate_function_schema(config));
589                    code.push_str(&generate_function_tool(config));
590                }
591            }
592        }
593    }
594
595    // Add js_value_to_json helper if any JS/TS Code action nodes exist
596    {
597        use crate::codegen::action_node_types::CodeLanguage;
598        use crate::codegen::action_nodes::ActionNodeConfig;
599        let has_js_code_nodes = project.action_nodes.values().any(|n| {
600            matches!(
601                n,
602                ActionNodeConfig::Code(cfg) if matches!(cfg.language, CodeLanguage::Javascript | CodeLanguage::Typescript)
603            )
604        });
605        if has_js_code_nodes {
606            code.push_str("/// Convert a boa_engine JsValue to serde_json::Value\n");
607            code.push_str("fn js_value_to_json(val: &boa_engine::JsValue, ctx: &mut boa_engine::Context) -> serde_json::Value {\n");
608            code.push_str(
609                "    // Handle Undefined/Null before to_json (which panics on Undefined)\n",
610            );
611            code.push_str("    if val.is_undefined() || val.is_null() {\n");
612            code.push_str("        return serde_json::Value::Null;\n");
613            code.push_str("    }\n");
614            code.push_str("    match val.to_json(ctx) {\n");
615            code.push_str("        Ok(json) => json,\n");
616            code.push_str("        Err(_) => {\n");
617            code.push_str("            match val {\n");
618            code.push_str("                boa_engine::JsValue::Boolean(b) => json!(*b),\n");
619            code.push_str("                boa_engine::JsValue::Integer(n) => json!(*n),\n");
620            code.push_str("                boa_engine::JsValue::Rational(n) => if n.is_finite() { json!(*n) } else { serde_json::Value::Null },\n");
621            code.push_str("                boa_engine::JsValue::String(s) => json!(s.to_std_string_escaped()),\n");
622            code.push_str("                _ => json!(val.display().to_string()),\n");
623            code.push_str("            }\n");
624            code.push_str("        }\n");
625            code.push_str("    }\n");
626            code.push_str("}\n\n");
627        }
628    }
629
630    code.push_str("#[tokio::main]\n");
631    code.push_str("async fn main() -> Result<()> {\n");
632    // Initialize tracing with JSON output
633    code.push_str("    // Initialize tracing\n");
634    code.push_str("    fmt().with_env_filter(EnvFilter::from_default_env().add_directive(\"adk=info\".parse()?)).json().with_writer(std::io::stderr).init();\n\n");
635    // Resolve API keys for each provider used
636    if providers.contains("gemini") {
637        code.push_str("    let gemini_api_key = std::env::var(\"GOOGLE_API_KEY\")\n");
638        code.push_str("        .or_else(|_| std::env::var(\"GEMINI_API_KEY\"))\n");
639        code.push_str("        .expect(\"GOOGLE_API_KEY or GEMINI_API_KEY must be set\");\n\n");
640    }
641    if providers.contains("openai") {
642        code.push_str("    let openai_api_key = std::env::var(\"OPENAI_API_KEY\")\n");
643        code.push_str("        .expect(\"OPENAI_API_KEY must be set\");\n\n");
644    }
645    if providers.contains("anthropic") {
646        code.push_str("    let anthropic_api_key = std::env::var(\"ANTHROPIC_API_KEY\")\n");
647        code.push_str("        .expect(\"ANTHROPIC_API_KEY must be set\");\n\n");
648    }
649    if providers.contains("deepseek") {
650        code.push_str("    let deepseek_api_key = std::env::var(\"DEEPSEEK_API_KEY\")\n");
651        code.push_str("        .expect(\"DEEPSEEK_API_KEY must be set\");\n\n");
652    }
653    if providers.contains("groq") {
654        code.push_str("    let groq_api_key = std::env::var(\"GROQ_API_KEY\")\n");
655        code.push_str("        .expect(\"GROQ_API_KEY must be set\");\n\n");
656    }
657    if providers.contains("ollama") {
658        code.push_str("    let _ollama_host = std::env::var(\"OLLAMA_HOST\")\n");
659        code.push_str("        .unwrap_or_else(|_| \"http://localhost:11434\".to_string());\n\n");
660    }
661    if providers.contains("fireworks") {
662        code.push_str("    let fireworks_api_key = std::env::var(\"FIREWORKS_API_KEY\")\n");
663        code.push_str("        .expect(\"FIREWORKS_API_KEY must be set\");\n\n");
664    }
665    if providers.contains("together") {
666        code.push_str("    let together_api_key = std::env::var(\"TOGETHER_API_KEY\")\n");
667        code.push_str("        .expect(\"TOGETHER_API_KEY must be set\");\n\n");
668    }
669    if providers.contains("mistral") {
670        code.push_str("    let mistral_api_key = std::env::var(\"MISTRAL_API_KEY\")\n");
671        code.push_str("        .expect(\"MISTRAL_API_KEY must be set\");\n\n");
672    }
673    if providers.contains("perplexity") {
674        code.push_str("    let perplexity_api_key = std::env::var(\"PERPLEXITY_API_KEY\")\n");
675        code.push_str("        .expect(\"PERPLEXITY_API_KEY must be set\");\n\n");
676    }
677    if providers.contains("cerebras") {
678        code.push_str("    let cerebras_api_key = std::env::var(\"CEREBRAS_API_KEY\")\n");
679        code.push_str("        .expect(\"CEREBRAS_API_KEY must be set\");\n\n");
680    }
681    if providers.contains("sambanova") {
682        code.push_str("    let sambanova_api_key = std::env::var(\"SAMBANOVA_API_KEY\")\n");
683        code.push_str("        .expect(\"SAMBANOVA_API_KEY must be set\");\n\n");
684    }
685    if providers.contains("bedrock") {
686        code.push_str("    let bedrock_region = std::env::var(\"AWS_DEFAULT_REGION\")\n");
687        code.push_str("        .unwrap_or_else(|_| \"us-east-1\".to_string());\n\n");
688    }
689    if providers.contains("azure-ai") {
690        code.push_str("    let azure_ai_endpoint = std::env::var(\"AZURE_AI_ENDPOINT\")\n");
691        code.push_str("        .expect(\"AZURE_AI_ENDPOINT must be set\");\n");
692        code.push_str("    let azure_ai_api_key = std::env::var(\"AZURE_AI_API_KEY\")\n");
693        code.push_str("        .expect(\"AZURE_AI_API_KEY must be set\");\n\n");
694    }
695
696    // Initialize browser session if any agent uses browser
697    let uses_browser = project
698        .agents
699        .values()
700        .any(|a| a.tools.contains(&"browser".to_string()));
701    if uses_browser {
702        code.push_str("    // Initialize browser session\n");
703        code.push_str("    let browser_config = BrowserConfig::new().headless(true);\n");
704        code.push_str("    let browser = Arc::new(BrowserSession::new(browser_config));\n");
705        code.push_str("    browser.start().await?;\n");
706        code.push_str("    let browser_toolset = BrowserToolset::new(browser.clone());\n\n");
707    }
708
709    // Find top-level agents (not sub-agents of containers)
710    let all_sub_agents: std::collections::HashSet<_> = project
711        .agents
712        .values()
713        .flat_map(|a| a.sub_agents.iter().cloned())
714        .collect();
715    let top_level: Vec<_> = project
716        .agents
717        .keys()
718        .filter(|id| !all_sub_agents.contains(*id))
719        .collect();
720
721    // Build predecessor map from workflow edges (multi-predecessor for fan-in/merge support)
722    // This tells us what node(s) come before each node in the workflow.
723    //
724    // IMPORTANT: We must skip back-edges (edges TO loop nodes that create cycles)
725    // to avoid infinite loops when walking the predecessor chain.
726    use crate::codegen::action_nodes::ActionNodeConfig;
727    use crate::codegen::action_nodes::EvaluationMode;
728    let loop_node_ids: std::collections::HashSet<&str> = project
729        .action_nodes
730        .iter()
731        .filter(|(_, n)| matches!(n, ActionNodeConfig::Loop(_)))
732        .map(|(id, _)| id.as_str())
733        .collect();
734
735    let mut predecessor_map: std::collections::HashMap<&str, Vec<&str>> =
736        std::collections::HashMap::new();
737    for edge in &project.workflow.edges {
738        // Skip trigger nodes - they're entry points, not execution nodes
739        let from_is_trigger = project
740            .action_nodes
741            .get(&edge.from)
742            .map(|n| matches!(n, ActionNodeConfig::Trigger(_)))
743            .unwrap_or(false);
744
745        // Skip back-edges to loop nodes (these create cycles)
746        let is_back_edge = loop_node_ids.contains(edge.to.as_str())
747            && edge.from != "START"
748            && !edge.from.is_empty()
749            && project
750                .action_nodes
751                .get(&edge.from)
752                .map(|n| !matches!(n, ActionNodeConfig::Loop(_)))
753                .unwrap_or(true)
754            && !project
755                .workflow
756                .edges
757                .iter()
758                .any(|e2| e2.from == edge.to && e2.to == edge.from);
759
760        if from_is_trigger || is_back_edge {
761            continue;
762        }
763
764        if edge.from != "START" && edge.to != "END" {
765            let preds = predecessor_map.entry(edge.to.as_str()).or_default();
766            if !preds.contains(&edge.from.as_str()) {
767                preds.push(edge.from.as_str());
768            }
769        } else if edge.from == "START" {
770            let preds = predecessor_map.entry(edge.to.as_str()).or_default();
771            if !preds.contains(&"START") {
772                preds.push("START");
773            }
774        }
775    }
776
777    // Identify all_match switch nodes early — needed to detect parallel branch agents
778    let all_match_switch_nodes_early: std::collections::HashSet<&str> = project
779        .action_nodes
780        .iter()
781        .filter(|(_, n)| {
782            if let ActionNodeConfig::Switch(config) = n {
783                config.evaluation_mode == EvaluationMode::AllMatch
784            } else {
785                false
786            }
787        })
788        .map(|(id, _)| id.as_str())
789        .collect();
790
791    // Identify agents that are in parallel fan-out branches (predecessor is an all_match switch)
792    // These agents must write to unique output keys to avoid overwriting each other
793    let mut parallel_branch_agents: std::collections::HashSet<String> =
794        std::collections::HashSet::new();
795    for edge in &project.workflow.edges {
796        if all_match_switch_nodes_early.contains(edge.from.as_str()) {
797            // This edge goes from an all_match switch to a branch target
798            // If the target is an agent, it's in a parallel branch
799            if project.agents.contains_key(&edge.to) {
800                parallel_branch_agents.insert(edge.to.clone());
801            }
802        }
803    }
804
805    // Generate all agent nodes with their predecessors
806    for agent_id in &top_level {
807        if let Some(agent) = project.agents.get(*agent_id) {
808            // Get first predecessor for backward compat (most nodes have exactly one)
809            let predecessors = predecessor_map.get(agent_id.as_str());
810            let first_predecessor = predecessors.and_then(|v| v.first().copied());
811            let is_parallel = parallel_branch_agents.contains(*agent_id);
812            match agent.agent_type {
813                AgentType::Router => {
814                    code.push_str(&generate_router_node(agent_id, agent));
815                }
816                AgentType::Llm => {
817                    code.push_str(&generate_llm_node_v2(
818                        agent_id,
819                        agent,
820                        project,
821                        first_predecessor,
822                        &predecessor_map,
823                        is_parallel,
824                    ));
825                }
826                _ => {
827                    // Sequential/Loop/Parallel - generate as single node wrapping container
828                    code.push_str(&generate_container_node(agent_id, agent, project));
829                }
830            }
831        }
832    }
833
834    // Generate action node functions (Set, Transform, etc.) - excluding Trigger which is just an entry point
835    let executable_action_nodes: Vec<_> = project
836        .action_nodes
837        .iter()
838        .filter(|(_, node)| !matches!(node, ActionNodeConfig::Trigger(_)))
839        .collect();
840
841    for (node_id, node) in &executable_action_nodes {
842        code.push_str(&generate_action_node_function(
843            node_id,
844            node,
845            &predecessor_map,
846            &parallel_branch_agents,
847        ));
848    }
849
850    // Build graph
851    code.push_str("    // Build the graph\n");
852
853    // Collect additional state channels needed by action nodes
854    // Every key that an action node writes to must be registered as a channel
855    let mut extra_channels: Vec<String> = Vec::new();
856    for (node_id, node) in &project.action_nodes {
857        match node {
858            ActionNodeConfig::Set(config) => {
859                // Set nodes produce one channel per variable
860                for var in &config.variables {
861                    if !extra_channels.contains(&var.key) {
862                        extra_channels.push(var.key.clone());
863                    }
864                }
865            }
866            ActionNodeConfig::Transform(config) => {
867                // Transform nodes produce a single output channel
868                let key = &config.standard.mapping.output_key;
869                if !key.is_empty() && !extra_channels.contains(key) {
870                    extra_channels.push(key.clone());
871                }
872            }
873            ActionNodeConfig::Switch(config) => {
874                // Switch nodes produce a branch key channel
875                let key = if config.standard.mapping.output_key.is_empty() {
876                    "branch".to_string()
877                } else {
878                    config.standard.mapping.output_key.clone()
879                };
880                if !extra_channels.contains(&key) {
881                    extra_channels.push(key);
882                }
883            }
884            ActionNodeConfig::Loop(config) => {
885                extra_channels.push(format!("{}_loop_index", node_id));
886                extra_channels.push(format!("{}_loop_done", node_id));
887                if let Some(fe) = &config.for_each {
888                    if !extra_channels.contains(&fe.item_var) {
889                        extra_channels.push(fe.item_var.clone());
890                    }
891                    if !extra_channels.contains(&fe.index_var) {
892                        extra_channels.push(fe.index_var.clone());
893                    }
894                }
895                if config.results.collect {
896                    let agg_key = config
897                        .results
898                        .aggregation_key
899                        .as_deref()
900                        .unwrap_or("loop_results");
901                    if !extra_channels.contains(&agg_key.to_string()) {
902                        extra_channels.push(agg_key.to_string());
903                    }
904                }
905            }
906            ActionNodeConfig::Merge(config) => {
907                let key = if config.standard.mapping.output_key.is_empty() {
908                    "merged".to_string()
909                } else {
910                    config.standard.mapping.output_key.clone()
911                };
912                if !extra_channels.contains(&key) {
913                    extra_channels.push(key);
914                }
915                // Also add branch keys as channels (they're read from state)
916                if let Some(keys) = &config.branch_keys {
917                    for k in keys {
918                        if !extra_channels.contains(k) {
919                            extra_channels.push(k.clone());
920                        }
921                    }
922                }
923            }
924            _ => {
925                // Other action nodes: register their output_key if set
926                let key = &node.standard().mapping.output_key;
927                if !key.is_empty() && !extra_channels.contains(key) {
928                    extra_channels.push(key.clone());
929                }
930            }
931        }
932    }
933    let base_channels = vec![
934        "message".to_string(),
935        "classification".to_string(),
936        "response".to_string(),
937    ];
938    // Add unique output channels for parallel-branch agents
939    let parallel_channels: Vec<String> = parallel_branch_agents
940        .iter()
941        .map(|id| format!("{}_response", id))
942        .collect();
943    let all_channels: Vec<String> = base_channels
944        .into_iter()
945        .chain(extra_channels)
946        .chain(parallel_channels)
947        .collect();
948    let channel_list = all_channels
949        .iter()
950        .map(|c| format!("\"{}\"", c))
951        .collect::<Vec<_>>()
952        .join(", ");
953    code.push_str(&format!(
954        "    let graph = StateGraph::with_channels(&[{}])\n",
955        channel_list
956    ));
957
958    // Add all agent nodes
959    for agent_id in &top_level {
960        code.push_str(&format!("        .add_node({}_node)\n", agent_id));
961    }
962
963    // Add action nodes (Set, Transform, etc.) - excluding Trigger
964    for (node_id, _) in &executable_action_nodes {
965        code.push_str(&format!("        .add_node({}_node)\n", node_id));
966    }
967
968    // Add edges from workflow
969    // Now we properly include action nodes in the graph execution
970    // First, find what START connects to (may be a trigger that we need to skip)
971    let start_target = project
972        .workflow
973        .edges
974        .iter()
975        .find(|e| e.from == "START")
976        .map(|e| e.to.as_str());
977
978    // Check if START connects to a trigger - if so, find what the trigger connects to
979    let actual_start_target = if let Some(target) = start_target {
980        if project
981            .action_nodes
982            .get(target)
983            .map(|n| matches!(n, ActionNodeConfig::Trigger(_)))
984            .unwrap_or(false)
985        {
986            // Find what the trigger connects to
987            project
988                .workflow
989                .edges
990                .iter()
991                .find(|e| e.from == target)
992                .map(|e| e.to.as_str())
993        } else {
994            Some(target)
995        }
996    } else {
997        None
998    };
999
1000    // Collect switch node IDs so we can handle their edges specially
1001    // Separate first_match (conditional routing) from all_match (fan-out)
1002    let switch_nodes: std::collections::HashSet<&str> = project
1003        .action_nodes
1004        .iter()
1005        .filter(|(_, n)| matches!(n, ActionNodeConfig::Switch(_)))
1006        .map(|(id, _)| id.as_str())
1007        .collect();
1008
1009    let all_match_switch_nodes: std::collections::HashSet<&str> = project
1010        .action_nodes
1011        .iter()
1012        .filter(|(_, n)| {
1013            if let ActionNodeConfig::Switch(config) = n {
1014                config.evaluation_mode == EvaluationMode::AllMatch
1015            } else {
1016                false
1017            }
1018        })
1019        .map(|(id, _)| id.as_str())
1020        .collect();
1021
1022    let first_match_switch_nodes: std::collections::HashSet<&str> = switch_nodes
1023        .iter()
1024        .filter(|id| !all_match_switch_nodes.contains(*id))
1025        .copied()
1026        .collect();
1027
1028    // Collect loop node IDs so we can handle their edges specially
1029    let loop_nodes: std::collections::HashSet<&str> = project
1030        .action_nodes
1031        .iter()
1032        .filter(|(_, n)| matches!(n, ActionNodeConfig::Loop(_)))
1033        .map(|(id, _)| id.as_str())
1034        .collect();
1035
1036    // Build a map of first_match switch_node_id -> Vec<(from_port, target_node)> from edges
1037    // all_match switches use direct edges (fan-out), so they're NOT in this map
1038    let mut switch_edge_map: std::collections::HashMap<&str, Vec<(String, String)>> =
1039        std::collections::HashMap::new();
1040    for edge in &project.workflow.edges {
1041        if first_match_switch_nodes.contains(edge.from.as_str()) {
1042            let port = edge.from_port.clone().unwrap_or_default();
1043            let target = if edge.to == "END" {
1044                "END".to_string()
1045            } else {
1046                edge.to.clone()
1047            };
1048            switch_edge_map
1049                .entry(edge.from.as_str())
1050                .or_default()
1051                .push((port, target));
1052        }
1053    }
1054
1055    // Build loop edge maps: loop_id -> (body_target, exit_targets)
1056    // Edges FROM loop nodes go to the loop body; edges TO loop nodes from body create the cycle
1057    let mut loop_body_map: std::collections::HashMap<&str, Vec<String>> =
1058        std::collections::HashMap::new();
1059    let mut loop_back_edges: std::collections::HashSet<(String, String)> =
1060        std::collections::HashSet::new();
1061    for edge in &project.workflow.edges {
1062        if loop_nodes.contains(edge.from.as_str()) {
1063            loop_body_map
1064                .entry(edge.from.as_str())
1065                .or_default()
1066                .push(edge.to.clone());
1067        }
1068        // Detect back-edges: edges TO a loop node (cycle)
1069        if loop_nodes.contains(edge.to.as_str()) && edge.from != "START" {
1070            loop_back_edges.insert((edge.from.clone(), edge.to.clone()));
1071        }
1072    }
1073    // For each loop node, find the exit target: the node that the loop's downstream
1074    // chain eventually connects to AFTER the loop body. We look at edges from nodes
1075    // that also have a back-edge to the loop — their OTHER outgoing edges are exit targets.
1076    let mut loop_exit_map: std::collections::HashMap<&str, Vec<String>> =
1077        std::collections::HashMap::new();
1078    for (back_from, back_to) in &loop_back_edges {
1079        // Find edges from back_from that don't go to the loop node — those are exit targets
1080        for edge in &project.workflow.edges {
1081            if edge.from == *back_from && edge.to != *back_to {
1082                loop_exit_map
1083                    .entry(back_to.as_str())
1084                    .or_default()
1085                    .push(edge.to.clone());
1086            }
1087        }
1088    }
1089    // If no explicit exit targets found, default to END
1090    for loop_id in &loop_nodes {
1091        if !loop_exit_map.contains_key(loop_id) {
1092            // Check if there are edges from the loop's body nodes to non-loop targets
1093            // If not, the exit goes to END
1094            loop_exit_map
1095                .entry(loop_id)
1096                .or_default()
1097                .push("END".to_string());
1098        }
1099    }
1100
1101    for edge in &project.workflow.edges {
1102        // Skip edges from trigger nodes (they're entry points, not execution nodes)
1103        let from_is_trigger = project
1104            .action_nodes
1105            .get(&edge.from)
1106            .map(|n| matches!(n, ActionNodeConfig::Trigger(_)))
1107            .unwrap_or(false);
1108
1109        // Skip edges to trigger nodes (shouldn't happen)
1110        let to_is_trigger = project
1111            .action_nodes
1112            .get(&edge.to)
1113            .map(|n| matches!(n, ActionNodeConfig::Trigger(_)))
1114            .unwrap_or(false);
1115
1116        if from_is_trigger {
1117            continue;
1118        }
1119
1120        if to_is_trigger && edge.to != "END" {
1121            continue;
1122        }
1123
1124        // Skip individual edges FROM first_match switch nodes - they'll be handled as conditional edges
1125        // all_match switch nodes use regular direct edges for fan-out
1126        if first_match_switch_nodes.contains(edge.from.as_str()) {
1127            continue;
1128        }
1129
1130        // Skip individual edges FROM loop nodes - they'll be handled as conditional edges
1131        if loop_nodes.contains(edge.from.as_str()) {
1132            continue;
1133        }
1134
1135        // Skip back-edges TO loop nodes - they're part of the loop cycle
1136        if loop_back_edges.contains(&(edge.from.clone(), edge.to.clone())) {
1137            continue;
1138        }
1139
1140        // Handle START edge - connect to actual first node (skipping trigger if present)
1141        let (from, to) = if edge.from == "START" {
1142            if let Some(actual_target) = actual_start_target {
1143                ("START".to_string(), format!("\"{}\"", actual_target))
1144            } else {
1145                continue; // No valid target
1146            }
1147        } else {
1148            let from = format!("\"{}\"", edge.from);
1149            let to = if edge.to == "END" {
1150                "END".to_string()
1151            } else {
1152                format!("\"{}\"", edge.to)
1153            };
1154            (from, to)
1155        };
1156
1157        // Check if source is a router - use conditional edges
1158        if let Some(agent) = project.agents.get(&edge.from) {
1159            if agent.agent_type == AgentType::Router && !agent.routes.is_empty() {
1160                // Generate conditional edges for router
1161                let conditions: Vec<String> = agent
1162                    .routes
1163                    .iter()
1164                    .map(|r| {
1165                        let target = if r.target == "END" {
1166                            "END".to_string()
1167                        } else {
1168                            format!("\"{}\"", r.target)
1169                        };
1170                        format!("(\"{}\", {})", r.condition, target)
1171                    })
1172                    .collect();
1173
1174                code.push_str("        .add_conditional_edges(\n");
1175                code.push_str(&format!("            \"{}\",\n", edge.from));
1176                code.push_str("            Router::by_field(\"classification\"),\n");
1177                code.push_str(&format!("            [{}],\n", conditions.join(", ")));
1178                code.push_str("        )\n");
1179                continue;
1180            }
1181        }
1182
1183        code.push_str(&format!("        .add_edge({}, {})\n", from, to));
1184    }
1185
1186    // Generate conditional edges for each switch node
1187    for (switch_id, targets) in &switch_edge_map {
1188        if let Some(ActionNodeConfig::Switch(config)) = project.action_nodes.get(*switch_id) {
1189            let output_key = if config.standard.mapping.output_key.is_empty() {
1190                "branch"
1191            } else {
1192                &config.standard.mapping.output_key
1193            };
1194
1195            // Build condition -> target mapping from edges
1196            let conditions: Vec<String> = targets
1197                .iter()
1198                .map(|(port, target)| {
1199                    let target_str = if target == "END" {
1200                        "END".to_string()
1201                    } else {
1202                        format!("\"{}\"", target)
1203                    };
1204                    format!("(\"{}\", {})", port, target_str)
1205                })
1206                .collect();
1207
1208            code.push_str(&format!(
1209                "        // Switch node: {} - conditional routing by '{}'\n",
1210                switch_id, output_key
1211            ));
1212            code.push_str("        .add_conditional_edges(\n");
1213            code.push_str(&format!("            \"{}\",\n", switch_id));
1214            code.push_str(&format!(
1215                "            Router::by_field(\"{}\"),\n",
1216                output_key
1217            ));
1218            code.push_str(&format!("            [{}],\n", conditions.join(", ")));
1219            code.push_str("        )\n");
1220        }
1221    }
1222
1223    // Generate conditional edges for each loop node
1224    // Loop nodes use a done flag to decide: continue body or exit
1225    for loop_id in &loop_nodes {
1226        let done_key = format!("{}_loop_done", loop_id);
1227        let body_targets = loop_body_map.get(loop_id).cloned().unwrap_or_default();
1228        let exit_targets = loop_exit_map
1229            .get(loop_id)
1230            .cloned()
1231            .unwrap_or_else(|| vec!["END".to_string()]);
1232
1233        if let Some(body_first) = body_targets.first() {
1234            let exit_first = exit_targets.first().map(|s| s.as_str()).unwrap_or("END");
1235
1236            let body_target_str = format!("\"{}\"", body_first);
1237            let exit_target_str = if exit_first == "END" {
1238                "END".to_string()
1239            } else {
1240                format!("\"{}\"", exit_first)
1241            };
1242
1243            code.push_str(&format!(
1244                "        // Loop node: {} - conditional cycle by '{}'\n",
1245                loop_id, done_key
1246            ));
1247            code.push_str("        .add_conditional_edges(\n");
1248            code.push_str(&format!("            \"{}\",\n", loop_id));
1249            code.push_str(&format!(
1250                "            Router::by_bool(\"{}\", \"exit\", \"body\"),\n",
1251                done_key
1252            ));
1253            code.push_str(&format!(
1254                "            [(\"body\", {}), (\"exit\", {})],\n",
1255                body_target_str, exit_target_str
1256            ));
1257            code.push_str("        )\n");
1258
1259            // Add back-edge from last body node to loop node (cycle)
1260            for (back_from, back_to) in &loop_back_edges {
1261                if back_to.as_str() == *loop_id {
1262                    code.push_str(&format!(
1263                        "        .add_edge(\"{}\", \"{}\")\n",
1264                        back_from, loop_id
1265                    ));
1266                }
1267            }
1268        }
1269    }
1270
1271    code.push_str("        .compile()?;\n\n");
1272
1273    // Interactive loop with streaming and conversation memory
1274    code.push_str("    // Get session ID from args or generate new one\n");
1275    code.push_str("    let session_id = std::env::args().nth(1).unwrap_or_else(|| uuid::Uuid::new_v4().to_string());\n");
1276    code.push_str("    println!(\"SESSION:{}\", session_id);\n\n");
1277    code.push_str("    // Conversation history for memory\n");
1278    code.push_str("    let mut history: Vec<(String, String)> = Vec::new();\n\n");
1279    code.push_str("    // Interactive loop\n");
1280    code.push_str(
1281        "    println!(\"Graph workflow ready. Type your message (or 'quit' to exit):\");\n",
1282    );
1283    code.push_str("    let stdin = std::io::stdin();\n");
1284    code.push_str("    let mut input = String::new();\n");
1285    code.push_str("    let mut turn = 0;\n");
1286    code.push_str("    loop {\n");
1287    code.push_str("        input.clear();\n");
1288    code.push_str("        print!(\"> \");\n");
1289    code.push_str("        use std::io::Write;\n");
1290    code.push_str("        std::io::stdout().flush()?;\n");
1291    code.push_str("        stdin.read_line(&mut input)?;\n");
1292    code.push_str("        let msg = input.trim();\n");
1293    code.push_str("        if msg.is_empty() || msg == \"quit\" { break; }\n\n");
1294    code.push_str("        // Build message with conversation history\n");
1295    code.push_str("        let context = if history.is_empty() {\n");
1296    code.push_str("            msg.to_string()\n");
1297    code.push_str("        } else {\n");
1298    code.push_str("            let hist: String = history.iter().map(|(u, a)| format!(\"User: {}\\nAssistant: {}\\n\", u, a)).collect();\n");
1299    code.push_str("            format!(\"{}\\nUser: {}\", hist, msg)\n");
1300    code.push_str("        };\n\n");
1301    code.push_str("        let mut state = State::new();\n");
1302    code.push_str("        state.insert(\"message\".to_string(), json!(context));\n");
1303    code.push_str("        \n");
1304    code.push_str("        use adk_graph::StreamMode;\n");
1305    code.push_str("        use tokio_stream::StreamExt;\n");
1306    code.push_str("        let stream = graph.stream(state, ExecutionConfig::new(&format!(\"{}-turn-{}\", session_id, turn)), StreamMode::Messages);\n");
1307    code.push_str("        tokio::pin!(stream);\n");
1308    code.push_str("        let mut final_response = String::new();\n");
1309    code.push_str("        \n");
1310    code.push_str("        while let Some(event) = stream.next().await {\n");
1311    code.push_str("            match event {\n");
1312    code.push_str("                Ok(e) => {\n");
1313    code.push_str("                    // Stream Message events as chunks\n");
1314    code.push_str(
1315        "                    if let adk_graph::StreamEvent::Message { content, .. } = &e {\n",
1316    );
1317    code.push_str("                        final_response.push_str(content);\n");
1318    code.push_str("                        println!(\"CHUNK:{}\", serde_json::to_string(&final_response).unwrap_or_default());\n");
1319    code.push_str("                    }\n");
1320    code.push_str("                    // Emit THINKING: prefix for Part::Thinking content (Requirements 6.1, 6.2)\n");
1321    code.push_str("                    if let adk_graph::StreamEvent::Custom { event_type, data, .. } = &e {\n");
1322    code.push_str("                        if event_type == \"agent_event\" {\n");
1323    code.push_str("                            if let Some(parts) = data.pointer(\"/content/parts\").and_then(|v| v.as_array()) {\n");
1324    code.push_str("                                for part in parts {\n");
1325    code.push_str("                                    if let Some(thinking) = part.get(\"thinking\").and_then(|v| v.as_str()) {\n");
1326    code.push_str("                                        if !thinking.is_empty() {\n");
1327    code.push_str("                                            println!(\"THINKING:{}\", serde_json::to_string(&thinking).unwrap_or_default());\n");
1328    code.push_str("                                        }\n");
1329    code.push_str("                                    }\n");
1330    code.push_str("                                }\n");
1331    code.push_str("                            }\n");
1332    code.push_str("                        }\n");
1333    code.push_str("                    }\n");
1334    code.push_str("                    // Output trace event as JSON\n");
1335    code.push_str("                    if let Ok(json) = serde_json::to_string(&e) {\n");
1336    code.push_str("                        println!(\"TRACE:{}\", json);\n");
1337    code.push_str("                    }\n");
1338    code.push_str("                    // Capture final response from Done event\n");
1339    code.push_str("                    if let adk_graph::StreamEvent::Done { state, .. } = &e {\n");
1340    code.push_str("                        if let Some(resp) = state.get(\"response\").and_then(|v| v.as_str()) {\n");
1341    code.push_str("                            final_response = resp.to_string();\n");
1342    code.push_str("                        }\n");
1343    code.push_str("                    }\n");
1344    code.push_str("                }\n");
1345    code.push_str("                Err(e) => eprintln!(\"Error: {}\", e),\n");
1346    code.push_str("            }\n");
1347    code.push_str("        }\n");
1348    code.push_str("        turn += 1;\n\n");
1349    code.push_str("        // Save to history\n");
1350    code.push_str("        if !final_response.is_empty() {\n");
1351    code.push_str("            history.push((msg.to_string(), final_response.clone()));\n");
1352    code.push_str("            println!(\"RESPONSE:{}\", serde_json::to_string(&final_response).unwrap_or_default());\n");
1353    code.push_str("        }\n");
1354    code.push_str("    }\n\n");
1355
1356    code.push_str("    Ok(())\n");
1357    code.push_str("}\n");
1358
1359    code
1360}
1361
1362fn generate_router_node(id: &str, agent: &AgentSchema) -> String {
1363    let mut code = String::new();
1364    let model = agent.model.as_deref().unwrap_or("gemini-3.1-flash-lite-preview");
1365
1366    code.push_str(&format!("    // Router: {}\n", id));
1367    code.push_str(&format!("    let {}_llm = Arc::new(\n", id));
1368    code.push_str(&format!("        LlmAgentBuilder::new(\"{}\")\n", id));
1369
1370    // Generate provider-specific model construction
1371    let provider = detect_provider(model);
1372    match provider {
1373        "openai" => {
1374            code.push_str(&format!(
1375                "            .model(Arc::new(OpenAIClient::new(OpenAIConfig::new(&openai_api_key, \"{}\"))?))\n",
1376                model
1377            ));
1378        }
1379        "anthropic" => {
1380            code.push_str(&format!(
1381                "            .model(Arc::new(AnthropicClient::new(AnthropicConfig::new(&anthropic_api_key, \"{}\"))?))\n",
1382                model
1383            ));
1384        }
1385        "deepseek" => {
1386            let m = model.to_lowercase();
1387            if m.contains("reasoner") || m.contains("r1") {
1388                code.push_str(
1389                    "            .model(Arc::new(DeepSeekClient::reasoner(&deepseek_api_key)?))\n",
1390                );
1391            } else {
1392                code.push_str(
1393                    "            .model(Arc::new(DeepSeekClient::chat(&deepseek_api_key)?))\n",
1394                );
1395            }
1396        }
1397        "groq" => {
1398            code.push_str(&format!(
1399                "            .model(Arc::new(GroqClient::new(GroqConfig::new(&groq_api_key, \"{}\"))?))\n",
1400                model
1401            ));
1402        }
1403        "ollama" => {
1404            code.push_str(&format!(
1405                "            .model(Arc::new(OllamaModel::new(OllamaConfig::new(\"{}\"))?))\n",
1406                model
1407            ));
1408        }
1409        _ => {
1410            // Default: Gemini
1411            code.push_str(&format!(
1412                "            .model(Arc::new(GeminiModel::new(&gemini_api_key, \"{}\")?))\n",
1413                model
1414            ));
1415        }
1416    }
1417
1418    let route_options: Vec<&str> = agent.routes.iter().map(|r| r.condition.as_str()).collect();
1419    let instruction = if agent.instruction.is_empty() {
1420        format!(
1421            "Classify the input into one of: {}. Respond with ONLY the category name.",
1422            route_options.join(", ")
1423        )
1424    } else {
1425        agent.instruction.clone()
1426    };
1427    let escaped = instruction
1428        .replace('\\', "\\\\")
1429        .replace('"', "\\\"")
1430        .replace('\n', "\\n");
1431    code.push_str(&format!("            .instruction(\"{}\")\n", escaped));
1432    code.push_str("            .build()?\n");
1433    code.push_str("    );\n\n");
1434
1435    code.push_str(&format!(
1436        "    let {}_node = AgentNode::new({}_llm)\n",
1437        id, id
1438    ));
1439    code.push_str("        .with_input_mapper(|state| {\n");
1440    code.push_str(
1441        "            let msg = state.get(\"message\").and_then(|v| v.as_str()).unwrap_or(\"\");\n",
1442    );
1443    code.push_str("            adk_core::Content::new(\"user\").with_text(msg.to_string())\n");
1444    code.push_str("        })\n");
1445    code.push_str("        .with_output_mapper(|events| {\n");
1446    code.push_str("            let mut updates = std::collections::HashMap::new();\n");
1447    code.push_str("            for event in events {\n");
1448    code.push_str("                if let Some(content) = event.content() {\n");
1449    code.push_str("                    let text: String = content.parts.iter()\n");
1450    code.push_str("                        .filter_map(|p| p.text())\n");
1451    code.push_str("                        .collect::<Vec<_>>().join(\"\").to_lowercase();\n");
1452
1453    for (i, route) in agent.routes.iter().enumerate() {
1454        let cond = if i == 0 { "if" } else { "else if" };
1455        code.push_str(&format!(
1456            "                    {} text.contains(\"{}\") {{\n",
1457            cond,
1458            route.condition.to_lowercase()
1459        ));
1460        code.push_str(&format!("                        updates.insert(\"classification\".to_string(), json!(\"{}\"));\n", route.condition));
1461        code.push_str("                    }\n");
1462    }
1463    if let Some(first) = agent.routes.first() {
1464        code.push_str(&format!("                    else {{ updates.insert(\"classification\".to_string(), json!(\"{}\")); }}\n", first.condition));
1465    }
1466
1467    code.push_str("                }\n");
1468    code.push_str("            }\n");
1469    code.push_str("            updates\n");
1470    code.push_str("        });\n\n");
1471
1472    code
1473}
1474
1475/// Generate LLM node with predecessor-based input mapping
1476///
1477/// This version uses the workflow edges to determine what each agent reads from:
1478/// - If predecessor is START or a trigger: read from "message"
1479/// - If predecessor is another agent: read from "response"
1480/// - If predecessor is an action node (Set, Transform): read from "response" (action nodes pass through)
1481fn generate_llm_node_v2(
1482    id: &str,
1483    agent: &AgentSchema,
1484    project: &ProjectSchema,
1485    predecessor: Option<&str>,
1486    predecessor_map: &std::collections::HashMap<&str, Vec<&str>>,
1487    is_parallel_branch: bool,
1488) -> String {
1489    let mut code = String::new();
1490    let model = agent.model.as_deref().unwrap_or("gemini-3.1-flash-lite-preview");
1491
1492    code.push_str(&format!("    // Agent: {}\n", id));
1493
1494    // Generate MCP toolsets for all MCP tools (mcp, mcp_1, mcp_2, etc.)
1495    let mcp_tools: Vec<_> = agent
1496        .tools
1497        .iter()
1498        .filter(|t| *t == "mcp" || t.starts_with("mcp_"))
1499        .collect();
1500
1501    for (idx, mcp_tool) in mcp_tools.iter().enumerate() {
1502        let tool_id = format!("{}_{}", id, mcp_tool);
1503        if let Some(ToolConfig::Mcp(config)) = project.tool_configs.get(&tool_id) {
1504            let cmd = &config.server_command;
1505            let var_suffix = if idx == 0 {
1506                "mcp".to_string()
1507            } else {
1508                format!("mcp_{}", idx + 1)
1509            };
1510            code.push_str(&format!(
1511                "    let mut {}_{}_cmd = Command::new(\"{}\");\n",
1512                id, var_suffix, cmd
1513            ));
1514            for arg in &config.server_args {
1515                code.push_str(&format!(
1516                    "    {}_{}_cmd.arg(\"{}\");\n",
1517                    id, var_suffix, arg
1518                ));
1519            }
1520            code.push_str(&format!(
1521                "    let {}_{}_client = tokio::time::timeout(\n",
1522                id, var_suffix
1523            ));
1524            code.push_str("        std::time::Duration::from_secs(10),\n");
1525            code.push_str(&format!(
1526                "        ().serve(TokioChildProcess::new({}_{}_cmd)?)\n",
1527                id, var_suffix
1528            ));
1529            code.push_str(&format!("    ).await.map_err(|_| anyhow::anyhow!(\"MCP server '{}' failed to start within 10s\"))??;\n", cmd));
1530            code.push_str(&format!(
1531                "    let {}_{}_toolset = McpToolset::new({}_{}_client)",
1532                id, var_suffix, id, var_suffix
1533            ));
1534            if !config.tool_filter.is_empty() {
1535                code.push_str(&format!(
1536                    ".with_tools(&[{}])",
1537                    config
1538                        .tool_filter
1539                        .iter()
1540                        .map(|t| format!("\"{}\"", t))
1541                        .collect::<Vec<_>>()
1542                        .join(", ")
1543                ));
1544            }
1545            code.push_str(";\n");
1546            code.push_str(&format!("    let {}_{}_tools = {}_{}_toolset.tools(Arc::new(MinimalContext::new())).await?;\n", id, var_suffix, id, var_suffix));
1547            code.push_str(&format!(
1548                "    eprintln!(\"Loaded {{}} tools from MCP server '{}'\", {}_{}_tools.len());\n\n",
1549                cmd, id, var_suffix
1550            ));
1551        }
1552    }
1553
1554    code.push_str(&format!(
1555        "    let mut {}_builder = LlmAgentBuilder::new(\"{}\")\n",
1556        id, id
1557    ));
1558
1559    // Generate provider-specific model construction
1560    let provider = detect_provider(model);
1561    match provider {
1562        "openai" => {
1563            code.push_str(&format!(
1564                "        .model(Arc::new(OpenAIClient::new(OpenAIConfig::new(&openai_api_key, \"{}\"))?));\n",
1565                model
1566            ));
1567        }
1568        "anthropic" => {
1569            code.push_str(&format!(
1570                "        .model(Arc::new(AnthropicClient::new(AnthropicConfig::new(&anthropic_api_key, \"{}\"))?));\n",
1571                model
1572            ));
1573        }
1574        "deepseek" => {
1575            // Use DeepSeekClient convenience constructors for known models
1576            let m = model.to_lowercase();
1577            if m.contains("reasoner") || m.contains("r1") {
1578                code.push_str(
1579                    "        .model(Arc::new(DeepSeekClient::reasoner(&deepseek_api_key)?));\n",
1580                );
1581            } else {
1582                code.push_str(
1583                    "        .model(Arc::new(DeepSeekClient::chat(&deepseek_api_key)?));\n",
1584                );
1585            }
1586        }
1587        "groq" => {
1588            code.push_str(&format!(
1589                "        .model(Arc::new(GroqClient::new(GroqConfig::new(&groq_api_key, \"{}\"))?));\n",
1590                model
1591            ));
1592        }
1593        "ollama" => {
1594            code.push_str(&format!(
1595                "        .model(Arc::new(OllamaModel::new(OllamaConfig::new(\"{}\"))?));\n",
1596                model
1597            ));
1598        }
1599        "fireworks" => {
1600            code.push_str(&format!(
1601                "        .model(Arc::new(FireworksClient::new(FireworksConfig::new(&fireworks_api_key, \"{}\"))?));\n",
1602                model
1603            ));
1604        }
1605        "together" => {
1606            code.push_str(&format!(
1607                "        .model(Arc::new(TogetherClient::new(TogetherConfig::new(&together_api_key, \"{}\"))?));\n",
1608                model
1609            ));
1610        }
1611        "mistral" => {
1612            code.push_str(&format!(
1613                "        .model(Arc::new(MistralClient::new(MistralConfig::new(&mistral_api_key, \"{}\"))?));\n",
1614                model
1615            ));
1616        }
1617        "perplexity" => {
1618            code.push_str(&format!(
1619                "        .model(Arc::new(PerplexityClient::new(PerplexityConfig::new(&perplexity_api_key, \"{}\"))?));\n",
1620                model
1621            ));
1622        }
1623        "cerebras" => {
1624            code.push_str(&format!(
1625                "        .model(Arc::new(CerebrasClient::new(CerebrasConfig::new(&cerebras_api_key, \"{}\"))?));\n",
1626                model
1627            ));
1628        }
1629        "sambanova" => {
1630            code.push_str(&format!(
1631                "        .model(Arc::new(SambaNovaClient::new(SambaNovaConfig::new(&sambanova_api_key, \"{}\"))?));\n",
1632                model
1633            ));
1634        }
1635        "bedrock" => {
1636            code.push_str(&format!(
1637                "        .model(Arc::new(BedrockClient::new(BedrockConfig::new(&bedrock_region, \"{}\")).await?));\n",
1638                model
1639            ));
1640        }
1641        "azure-ai" => {
1642            code.push_str(&format!(
1643                "        .model(Arc::new(AzureAIClient::new(AzureAIConfig::new(&azure_ai_endpoint, &azure_ai_api_key, \"{}\"))?));\n",
1644                model
1645            ));
1646        }
1647        _ => {
1648            // Default: Gemini
1649            code.push_str(&format!(
1650                "        .model(Arc::new(GeminiModel::new(&gemini_api_key, \"{}\")?));\n",
1651                model
1652            ));
1653        }
1654    }
1655
1656    if !agent.instruction.is_empty() {
1657        // Strip {{var}} template variables from instruction - they'll be injected via input_mapper
1658        // This avoids the session state vs graph state mismatch where the agent's template
1659        // system looks at session state but Set nodes update graph state
1660        let instruction_clean = strip_template_variables(&agent.instruction);
1661        let escaped = instruction_clean
1662            .replace('\\', "\\\\")
1663            .replace('"', "\\\"")
1664            .replace('\n', "\\n");
1665        code.push_str(&format!(
1666            "    {}_builder = {}_builder.instruction(\"{}\");\n",
1667            id, id, escaped
1668        ));
1669    }
1670
1671    // Add MCP tools if present
1672    for (idx, mcp_tool) in mcp_tools.iter().enumerate() {
1673        let tool_id = format!("{}_{}", id, mcp_tool);
1674        if project.tool_configs.contains_key(&tool_id) {
1675            let var_suffix = if idx == 0 {
1676                "mcp".to_string()
1677            } else {
1678                format!("mcp_{}", idx + 1)
1679            };
1680            code.push_str(&format!("    for tool in {}_{}_tools {{\n", id, var_suffix));
1681            code.push_str(&format!(
1682                "        {}_builder = {}_builder.tool(tool);\n",
1683                id, id
1684            ));
1685            code.push_str("    }\n");
1686        }
1687    }
1688
1689    for tool_type in &agent.tools {
1690        if tool_type.starts_with("function") {
1691            let tool_id = format!("{}_{}", id, tool_type);
1692            if let Some(ToolConfig::Function(config)) = project.tool_configs.get(&tool_id) {
1693                let struct_name = to_pascal_case(&config.name);
1694                code.push_str(&format!("    {}_builder = {}_builder.tool(Arc::new(FunctionTool::new(\"{}\", \"{}\", {}_fn).with_parameters_schema::<{}Args>()));\n", 
1695                    id, id, config.name, config.description.replace('"', "\\\""), config.name, struct_name));
1696            }
1697        } else if !tool_type.starts_with("mcp") {
1698            match tool_type.as_str() {
1699                "google_search" => code.push_str(&format!(
1700                    "    {}_builder = {}_builder.tool(Arc::new(GoogleSearchTool::new()));\n",
1701                    id, id
1702                )),
1703                "exit_loop" => code.push_str(&format!(
1704                    "    {}_builder = {}_builder.tool(Arc::new(ExitLoopTool::new()));\n",
1705                    id, id
1706                )),
1707                "load_artifact" => code.push_str(&format!(
1708                    "    {}_builder = {}_builder.tool(Arc::new(LoadArtifactsTool::new()));\n",
1709                    id, id
1710                )),
1711                "browser" => {
1712                    code.push_str("    for tool in browser_toolset.tools(Arc::new(MinimalContext::new())).await? {\n");
1713                    code.push_str(&format!(
1714                        "        {}_builder = {}_builder.tool(tool);\n",
1715                        id, id
1716                    ));
1717                    code.push_str("    }\n");
1718                }
1719                _ => {}
1720            }
1721        }
1722    }
1723
1724    // Add generation config if set
1725    if let Some(temp) = agent.temperature {
1726        code.push_str(&format!(
1727            "    {}_builder = {}_builder.temperature({:.1});\n",
1728            id, id, temp
1729        ));
1730    }
1731    if let Some(top_p) = agent.top_p {
1732        code.push_str(&format!(
1733            "    {}_builder = {}_builder.top_p({:.2});\n",
1734            id, id, top_p
1735        ));
1736    }
1737    if let Some(top_k) = agent.top_k {
1738        code.push_str(&format!(
1739            "    {}_builder = {}_builder.top_k({});\n",
1740            id, id, top_k
1741        ));
1742    }
1743    if let Some(max_tokens) = agent.max_output_tokens {
1744        code.push_str(&format!(
1745            "    {}_builder = {}_builder.max_output_tokens({});\n",
1746            id, id, max_tokens
1747        ));
1748    }
1749
1750    code.push_str(&format!(
1751        "    let {}_llm = Arc::new({}_builder.build()?);\n\n",
1752        id, id
1753    ));
1754
1755    code.push_str(&format!(
1756        "    let {}_node = AgentNode::new({}_llm)\n",
1757        id, id
1758    ));
1759    code.push_str("        .with_input_mapper(|state| {\n");
1760
1761    // Determine what to read based on predecessor
1762    let is_first = predecessor == Some("START") || predecessor.is_none();
1763
1764    if is_first {
1765        code.push_str("            // First node: read from original message\n");
1766        code.push_str("            let msg = state.get(\"message\").and_then(|v| v.as_str()).unwrap_or(\"\");\n");
1767    } else {
1768        code.push_str(&format!(
1769            "            // Predecessor: {} - read from response\n",
1770            predecessor.unwrap_or("unknown")
1771        ));
1772        code.push_str("            let msg = state.get(\"response\").and_then(|v| v.as_str())\n");
1773        code.push_str("                .or_else(|| state.get(\"message\").and_then(|v| v.as_str())).unwrap_or(\"\");\n");
1774    }
1775
1776    // Collect state variables to inject into the agent's input:
1777    // 1. Variables from {{var}} references in the instruction
1778    // 2. Variables from predecessor Set/Transform action nodes
1779    let mut inject_vars: Vec<String> = Vec::new();
1780
1781    // Check if instruction references state variables via {{var}} syntax
1782    let var_refs: Vec<&str> = agent
1783        .instruction
1784        .match_indices("{{")
1785        .filter_map(|(start, _)| {
1786            let rest = &agent.instruction[start + 2..];
1787            rest.find("}}").map(|end| &rest[..end])
1788        })
1789        .collect();
1790    for var in &var_refs {
1791        if *var != "message" && *var != "response" && !inject_vars.contains(&var.to_string()) {
1792            inject_vars.push(var.to_string());
1793        }
1794    }
1795
1796    // Check if any predecessor in the chain is a Set or Transform action node
1797    // Walk backwards through the predecessor chain to find all Set nodes that feed into this agent
1798    // Supports multiple predecessors (fan-in from merge/parallel branches)
1799    {
1800        use crate::codegen::action_nodes::ActionNodeConfig;
1801        let mut queue: Vec<&str> = if let Some(pred) = predecessor {
1802            vec![pred]
1803        } else {
1804            vec![]
1805        };
1806        // Also add any additional predecessors (fan-in)
1807        if let Some(preds) = predecessor_map.get(id) {
1808            for p in preds {
1809                if !queue.contains(p) {
1810                    queue.push(p);
1811                }
1812            }
1813        }
1814        let mut visited_preds = std::collections::HashSet::new();
1815        while let Some(pred_id) = queue.pop() {
1816            if pred_id == "START" || visited_preds.contains(pred_id) {
1817                continue;
1818            }
1819            visited_preds.insert(pred_id);
1820            if let Some(action_node) = project.action_nodes.get(pred_id) {
1821                match action_node {
1822                    ActionNodeConfig::Set(set_config) => {
1823                        for var in &set_config.variables {
1824                            if !inject_vars.contains(&var.key) {
1825                                inject_vars.push(var.key.clone());
1826                            }
1827                        }
1828                    }
1829                    ActionNodeConfig::Transform(transform_config) => {
1830                        let out_key = &transform_config.standard.mapping.output_key;
1831                        if !inject_vars.contains(out_key) {
1832                            inject_vars.push(out_key.clone());
1833                        }
1834                    }
1835                    ActionNodeConfig::Merge(merge_config) => {
1836                        // Merge node output key contains the combined result
1837                        let out_key = if merge_config.standard.mapping.output_key.is_empty() {
1838                            "merged".to_string()
1839                        } else {
1840                            merge_config.standard.mapping.output_key.clone()
1841                        };
1842                        if !inject_vars.contains(&out_key) {
1843                            inject_vars.push(out_key);
1844                        }
1845                    }
1846                    _ => {
1847                        // For any other action node (Http, Wait, etc.),
1848                        // inject their output_key so the agent sees the result
1849                        let out_key = &action_node.standard().mapping.output_key;
1850                        if !out_key.is_empty() && !inject_vars.contains(out_key) {
1851                            inject_vars.push(out_key.clone());
1852                        }
1853                    }
1854                }
1855            }
1856            // Walk to all predecessors of this node
1857            if let Some(preds) = predecessor_map.get(pred_id) {
1858                for p in preds {
1859                    if !visited_preds.contains(*p) {
1860                        queue.push(p);
1861                    }
1862                }
1863            }
1864        }
1865    }
1866
1867    if !inject_vars.is_empty() {
1868        code.push_str(
1869            "            // Include state variables from Set nodes and instruction references\n",
1870        );
1871        code.push_str("            let mut full_msg = msg.to_string();\n");
1872        code.push_str("            let mut context_parts: Vec<String> = Vec::new();\n");
1873        for var in &inject_vars {
1874            code.push_str(&format!(
1875                "            if let Some(v) = state.get(\"{}\") {{\n",
1876                var
1877            ));
1878            code.push_str(&format!("                context_parts.push(format!(\"{}: {{}}\", v.as_str().unwrap_or(&v.to_string())));\n", var));
1879            code.push_str("            }\n");
1880        }
1881        code.push_str("            if !context_parts.is_empty() {\n");
1882        code.push_str("                full_msg = format!(\"{}\\n\\nContext:\\n{}\", full_msg, context_parts.join(\"\\n\"));\n");
1883        code.push_str("            }\n");
1884        code.push_str("            adk_core::Content::new(\"user\").with_text(full_msg)\n");
1885    } else {
1886        code.push_str("            adk_core::Content::new(\"user\").with_text(msg.to_string())\n");
1887    }
1888    code.push_str("        })\n");
1889    code.push_str("        .with_output_mapper(|events| {\n");
1890    code.push_str("            let mut updates = std::collections::HashMap::new();\n");
1891    code.push_str("            let mut full_text = String::new();\n");
1892    code.push_str("            for event in events {\n");
1893    code.push_str("                if let Some(content) = event.content() {\n");
1894    code.push_str("                    for part in &content.parts {\n");
1895    code.push_str("                        if let Some(text) = part.text() {\n");
1896    code.push_str("                            full_text.push_str(text);\n");
1897    code.push_str("                        }\n");
1898    code.push_str("                    }\n");
1899    code.push_str("                }\n");
1900    code.push_str("            }\n");
1901    code.push_str("            if !full_text.is_empty() {\n");
1902    if is_parallel_branch {
1903        // Parallel branch agents write to a unique key to avoid overwriting each other
1904        code.push_str(&format!(
1905            "                updates.insert(\"{}_response\".to_string(), json!(full_text));\n",
1906            id
1907        ));
1908    } else {
1909        code.push_str(
1910            "                updates.insert(\"response\".to_string(), json!(full_text));\n",
1911        );
1912    }
1913    code.push_str("            }\n");
1914    code.push_str("            updates\n");
1915    code.push_str("        });\n\n");
1916
1917    code
1918}
1919
1920fn generate_container_node(id: &str, agent: &AgentSchema, project: &ProjectSchema) -> String {
1921    let mut code = String::new();
1922
1923    // Generate sub-agents first
1924    for sub_id in &agent.sub_agents {
1925        if let Some(sub) = project.agents.get(sub_id) {
1926            let model = sub.model.as_deref().unwrap_or("gemini-3.1-flash-lite-preview");
1927            let has_tools = !sub.tools.is_empty();
1928            let has_instruction = !sub.instruction.is_empty();
1929            let mut_kw = if has_tools || has_instruction {
1930                "mut "
1931            } else {
1932                ""
1933            };
1934
1935            // Load MCP tools BEFORE creating builder (matching working pattern)
1936            for tool_type in &sub.tools {
1937                let tool_id = format!("{}_{}", sub_id, tool_type);
1938                if tool_type.starts_with("mcp") {
1939                    if let Some(ToolConfig::Mcp(config)) = project.tool_configs.get(&tool_id) {
1940                        let var_suffix = tool_type.replace("mcp_", "mcp");
1941                        code.push_str(&format!(
1942                            "    let mut {}_{}_cmd = Command::new(\"{}\");\n",
1943                            sub_id, var_suffix, config.server_command
1944                        ));
1945                        for arg in &config.server_args {
1946                            code.push_str(&format!(
1947                                "    {}_{}_cmd.arg(\"{}\");\n",
1948                                sub_id, var_suffix, arg
1949                            ));
1950                        }
1951                        code.push_str(&format!(
1952                            "    let {}_{}_client = tokio::time::timeout(\n",
1953                            sub_id, var_suffix
1954                        ));
1955                        code.push_str("        std::time::Duration::from_secs(10),\n");
1956                        code.push_str(&format!(
1957                            "        ().serve(TokioChildProcess::new({}_{}_cmd)?)\n",
1958                            sub_id, var_suffix
1959                        ));
1960                        code.push_str(&format!("    ).await.map_err(|_| anyhow::anyhow!(\"MCP server '{}' failed to start within 10s\"))??;\n", config.server_command));
1961                        code.push_str(&format!(
1962                            "    let {}_{}_toolset = McpToolset::new({}_{}_client);\n",
1963                            sub_id, var_suffix, sub_id, var_suffix
1964                        ));
1965                        code.push_str(&format!("    let {}_{}_tools = {}_{}_toolset.tools(Arc::new(MinimalContext::new())).await?;\n", sub_id, var_suffix, sub_id, var_suffix));
1966                        code.push_str(&format!("    eprintln!(\"Loaded {{}} tools from MCP server '{}'\", {}_{}_tools.len());\n\n", config.server_command, sub_id, var_suffix));
1967                    }
1968                }
1969            }
1970
1971            // Create builder
1972            code.push_str(&format!(
1973                "    let {}{}_builder = LlmAgentBuilder::new(\"{}\")\n",
1974                mut_kw, sub_id, sub_id
1975            ));
1976
1977            // Generate provider-specific model construction
1978            let provider = detect_provider(model);
1979            match provider {
1980                "openai" => {
1981                    code.push_str(&format!(
1982                        "        .model(Arc::new(OpenAIClient::new(OpenAIConfig::new(&openai_api_key, \"{}\"))?));\n",
1983                        model
1984                    ));
1985                }
1986                "anthropic" => {
1987                    code.push_str(&format!(
1988                        "        .model(Arc::new(AnthropicClient::new(AnthropicConfig::new(&anthropic_api_key, \"{}\"))?));\n",
1989                        model
1990                    ));
1991                }
1992                "deepseek" => {
1993                    let m = model.to_lowercase();
1994                    if m.contains("reasoner") || m.contains("r1") {
1995                        code.push_str(
1996                            "        .model(Arc::new(DeepSeekClient::reasoner(&deepseek_api_key)?));\n",
1997                        );
1998                    } else {
1999                        code.push_str(
2000                            "        .model(Arc::new(DeepSeekClient::chat(&deepseek_api_key)?));\n",
2001                        );
2002                    }
2003                }
2004                "groq" => {
2005                    code.push_str(&format!(
2006                        "        .model(Arc::new(GroqClient::new(GroqConfig::new(&groq_api_key, \"{}\"))?));\n",
2007                        model
2008                    ));
2009                }
2010                "ollama" => {
2011                    code.push_str(&format!(
2012                        "        .model(Arc::new(OllamaModel::new(OllamaConfig::new(\"{}\"))?));\n",
2013                        model
2014                    ));
2015                }
2016                _ => {
2017                    // Default: Gemini
2018                    code.push_str(&format!(
2019                        "        .model(Arc::new(GeminiModel::new(&gemini_api_key, \"{}\")?))",
2020                        model
2021                    ));
2022                    code.push_str(";\n");
2023                }
2024            }
2025
2026            // Add instruction separately (matching working pattern)
2027            if !sub.instruction.is_empty() {
2028                let escaped = sub
2029                    .instruction
2030                    .replace('\\', "\\\\")
2031                    .replace('"', "\\\"")
2032                    .replace('\n', "\\n");
2033                code.push_str(&format!(
2034                    "    {}_builder = {}_builder.instruction(\"{}\");\n",
2035                    sub_id, sub_id, escaped
2036                ));
2037            }
2038
2039            // Add tools
2040            for tool_type in &sub.tools {
2041                let tool_id = format!("{}_{}", sub_id, tool_type);
2042                if tool_type.starts_with("function") {
2043                    if let Some(ToolConfig::Function(config)) = project.tool_configs.get(&tool_id) {
2044                        let fn_name = &config.name;
2045                        let struct_name = to_pascal_case(fn_name);
2046                        code.push_str(&format!("    {}_builder = {}_builder.tool(Arc::new(FunctionTool::new(\"{}\", \"{}\", {}_fn).with_parameters_schema::<{}Args>()));\n", 
2047                            sub_id, sub_id, fn_name, config.description.replace('"', "\\\""), fn_name, struct_name));
2048                    }
2049                } else if tool_type.starts_with("mcp") {
2050                    // Only generate tool loop if config exists (MCP setup was generated above)
2051                    let tool_id = format!("{}_{}", sub_id, tool_type);
2052                    if project.tool_configs.contains_key(&tool_id) {
2053                        let var_suffix = tool_type.replace("mcp_", "mcp");
2054                        code.push_str(&format!(
2055                            "    for tool in {}_{}_tools {{\n",
2056                            sub_id, var_suffix
2057                        ));
2058                        code.push_str(&format!(
2059                            "        {}_builder = {}_builder.tool(tool);\n",
2060                            sub_id, sub_id
2061                        ));
2062                        code.push_str("    }\n");
2063                    }
2064                } else if tool_type == "google_search" {
2065                    code.push_str(&format!(
2066                        "    {}_builder = {}_builder.tool(Arc::new(GoogleSearchTool::new()));\n",
2067                        sub_id, sub_id
2068                    ));
2069                } else if tool_type == "exit_loop" {
2070                    code.push_str(&format!(
2071                        "    {}_builder = {}_builder.tool(Arc::new(ExitLoopTool::new()));\n",
2072                        sub_id, sub_id
2073                    ));
2074                } else if tool_type == "load_artifact" {
2075                    code.push_str(&format!(
2076                        "    {}_builder = {}_builder.tool(Arc::new(LoadArtifactsTool::new()));\n",
2077                        sub_id, sub_id
2078                    ));
2079                }
2080            }
2081
2082            // Add generation config if set
2083            if let Some(temp) = sub.temperature {
2084                code.push_str(&format!(
2085                    "    {}_builder = {}_builder.temperature({:.1});\n",
2086                    sub_id, sub_id, temp
2087                ));
2088            }
2089            if let Some(top_p) = sub.top_p {
2090                code.push_str(&format!(
2091                    "    {}_builder = {}_builder.top_p({:.2});\n",
2092                    sub_id, sub_id, top_p
2093                ));
2094            }
2095            if let Some(top_k) = sub.top_k {
2096                code.push_str(&format!(
2097                    "    {}_builder = {}_builder.top_k({});\n",
2098                    sub_id, sub_id, top_k
2099                ));
2100            }
2101            if let Some(max_tokens) = sub.max_output_tokens {
2102                code.push_str(&format!(
2103                    "    {}_builder = {}_builder.max_output_tokens({});\n",
2104                    sub_id, sub_id, max_tokens
2105                ));
2106            }
2107
2108            code.push_str(&format!(
2109                "    let {}_agent = {}_builder.build()?;\n\n",
2110                sub_id, sub_id
2111            ));
2112        }
2113    }
2114
2115    // Create container
2116    let subs: Vec<_> = agent
2117        .sub_agents
2118        .iter()
2119        .map(|s| format!("Arc::new({}_agent)", s))
2120        .collect();
2121    let container_type = match agent.agent_type {
2122        AgentType::Sequential => "adk_agent::SequentialAgent",
2123        AgentType::Loop => "adk_agent::LoopAgent",
2124        AgentType::Parallel => "adk_agent::ParallelAgent",
2125        _ => "adk_agent::SequentialAgent",
2126    };
2127
2128    code.push_str(&format!(
2129        "    // Container: {} ({:?})\n",
2130        id, agent.agent_type
2131    ));
2132    if agent.agent_type == AgentType::Loop {
2133        let max_iter = agent.max_iterations.unwrap_or(3);
2134        code.push_str(&format!(
2135            "    let {}_container = {}::new(\"{}\", vec![{}]).with_max_iterations({});\n\n",
2136            id,
2137            container_type,
2138            id,
2139            subs.join(", "),
2140            max_iter
2141        ));
2142    } else {
2143        code.push_str(&format!(
2144            "    let {}_container = {}::new(\"{}\", vec![{}]);\n\n",
2145            id,
2146            container_type,
2147            id,
2148            subs.join(", ")
2149        ));
2150    }
2151
2152    // Wrap in AgentNode
2153    code.push_str(&format!(
2154        "    let {}_node = AgentNode::new(Arc::new({}_container))\n",
2155        id, id
2156    ));
2157    code.push_str("        .with_input_mapper(|state| {\n");
2158    code.push_str(
2159        "            let msg = state.get(\"message\").and_then(|v| v.as_str()).unwrap_or(\"\");\n",
2160    );
2161    code.push_str("            adk_core::Content::new(\"user\").with_text(msg.to_string())\n");
2162    code.push_str("        })\n");
2163    code.push_str("        .with_output_mapper(|events| {\n");
2164    code.push_str("            let mut updates = std::collections::HashMap::new();\n");
2165    code.push_str("            let mut full_text = String::new();\n");
2166    code.push_str("            for event in events {\n");
2167    code.push_str("                if let Some(content) = event.content() {\n");
2168    code.push_str("                    for part in &content.parts {\n");
2169    code.push_str("                        if let Some(text) = part.text() {\n");
2170    code.push_str("                            full_text.push_str(text);\n");
2171    code.push_str("                        }\n");
2172    code.push_str("                    }\n");
2173    code.push_str("                }\n");
2174    code.push_str("            }\n");
2175    code.push_str("            // Filter out tool call artifacts\n");
2176    code.push_str("            let full_text = full_text.replace(\"exit_loop\", \"\");\n");
2177    code.push_str("            if !full_text.is_empty() {\n");
2178    code.push_str("                updates.insert(\"response\".to_string(), json!(full_text));\n");
2179    code.push_str("            }\n");
2180    code.push_str("            updates\n");
2181    code.push_str("        });\n\n");
2182
2183    code
2184}
2185
2186fn generate_function_tool(config: &crate::schema::FunctionToolConfig) -> String {
2187    let mut code = String::new();
2188    let fn_name = &config.name;
2189
2190    code.push_str(&format!("async fn {}_fn(_ctx: Arc<dyn ToolContext>, args: Value) -> Result<Value, adk_core::AdkError> {{\n", fn_name));
2191
2192    // Generate parameter extraction
2193    for param in &config.parameters {
2194        let extract = match param.param_type {
2195            crate::schema::ParamType::String => format!(
2196                "    let {} = args[\"{}\"].as_str().unwrap_or(\"\");\n",
2197                param.name, param.name
2198            ),
2199            crate::schema::ParamType::Number => format!(
2200                "    let {} = args[\"{}\"].as_f64().unwrap_or(0.0);\n",
2201                param.name, param.name
2202            ),
2203            crate::schema::ParamType::Boolean => format!(
2204                "    let {} = args[\"{}\"].as_bool().unwrap_or(false);\n",
2205                param.name, param.name
2206            ),
2207        };
2208        code.push_str(&extract);
2209    }
2210
2211    code.push('\n');
2212
2213    // Insert user's code or generate placeholder
2214    if config.code.is_empty() {
2215        // Generate placeholder that echoes parameters
2216        let param_json = config
2217            .parameters
2218            .iter()
2219            .map(|p| format!("        \"{}\": {}", p.name, p.name))
2220            .collect::<Vec<_>>()
2221            .join(",\n");
2222        code.push_str("    // TODO: Add function implementation\n");
2223        code.push_str("    Ok(json!({\n");
2224        code.push_str(&format!("        \"function\": \"{}\",\n", fn_name));
2225        if !param_json.is_empty() {
2226            code.push_str(&param_json);
2227            code.push_str(",\n");
2228        }
2229        code.push_str("        \"status\": \"not_implemented\"\n");
2230        code.push_str("    }))\n");
2231    } else {
2232        // Use user's actual code
2233        code.push_str("    // User-defined implementation\n");
2234        for line in config.code.lines() {
2235            code.push_str(&format!("    {}\n", line));
2236        }
2237    }
2238
2239    code.push_str("}\n\n");
2240    code
2241}
2242
2243fn generate_function_schema(config: &crate::schema::FunctionToolConfig) -> String {
2244    let mut code = String::new();
2245    let struct_name = to_pascal_case(&config.name);
2246
2247    code.push_str("#[derive(serde::Serialize, serde::Deserialize, schemars::JsonSchema)]\n");
2248    code.push_str(&format!("struct {}Args {{\n", struct_name));
2249
2250    for param in &config.parameters {
2251        if !param.description.is_empty() {
2252            code.push_str(&format!("    /// {}\n", param.description));
2253        }
2254        let rust_type = match param.param_type {
2255            crate::schema::ParamType::String => "String",
2256            crate::schema::ParamType::Number => "f64",
2257            crate::schema::ParamType::Boolean => "bool",
2258        };
2259        code.push_str(&format!("    {}: {},\n", param.name, rust_type));
2260    }
2261
2262    code.push_str("}\n\n");
2263    code
2264}
2265
2266fn to_pascal_case(s: &str) -> String {
2267    s.split('_')
2268        .map(|word| {
2269            let mut chars = word.chars();
2270            match chars.next() {
2271                None => String::new(),
2272                Some(c) => c.to_uppercase().chain(chars).collect(),
2273            }
2274        })
2275        .collect()
2276}
2277
2278/// Generate a graph node function for an action node (Set, Transform, etc.)
2279///
2280/// Helper: generate JSONPath-like extraction code for HTTP response.
2281/// Supports simple dot-notation paths like "data.items" or "result.name".
2282fn generate_json_path_extraction(code: &mut String, json_path: &str, output_key: &str) {
2283    // Strip leading "$." if present (JSONPath convention)
2284    let path = json_path.strip_prefix("$.").unwrap_or(json_path);
2285    let segments: Vec<&str> = path.split('.').collect();
2286
2287    if segments.is_empty() || (segments.len() == 1 && segments[0].is_empty()) {
2288        // No path — return the whole body
2289        code.push_str(&format!(
2290            "                Ok(NodeOutput::new().with_update(\"{}\", json!({{ \"status\": status, \"body\": body }})))\n",
2291            output_key
2292        ));
2293        return;
2294    }
2295
2296    code.push_str("                let mut extracted = body.clone();\n");
2297    for seg in &segments {
2298        code.push_str(&format!(
2299            "                extracted = extracted.get(\"{}\").cloned().unwrap_or(json!(null));\n",
2300            seg.replace('"', "\\\"")
2301        ));
2302    }
2303    code.push_str(&format!(
2304        "                Ok(NodeOutput::new().with_update(\"{}\", json!({{ \"status\": status, \"body\": extracted }})))\n",
2305        output_key
2306    ));
2307}
2308
2309/// Helper: generate a condition check for first_match Switch (sets matched_branch)
2310fn generate_condition_check(code: &mut String, op: &str, value_str: &str, port: &str, field: &str) {
2311    let escaped_val = value_str.replace('"', "\\\"");
2312    match op {
2313        "equals" | "==" | "eq" => {
2314            code.push_str(&format!(
2315                "            if field_val == \"{}\" {{ matched_branch = Some(\"{}\"); }}\n",
2316                escaped_val, port
2317            ));
2318        }
2319        "not_equals" | "!=" | "neq" => {
2320            code.push_str(&format!(
2321                "            if field_val != \"{}\" {{ matched_branch = Some(\"{}\"); }}\n",
2322                escaped_val, port
2323            ));
2324        }
2325        "contains" => {
2326            code.push_str(&format!(
2327                "            if field_val.contains(\"{}\") {{ matched_branch = Some(\"{}\"); }}\n",
2328                escaped_val, port
2329            ));
2330        }
2331        "greater_than" | ">" | "gt" => {
2332            code.push_str(&format!(
2333                "            if field_val > \"{}\" {{ matched_branch = Some(\"{}\"); }}\n",
2334                escaped_val, port
2335            ));
2336        }
2337        "less_than" | "<" | "lt" => {
2338            code.push_str(&format!(
2339                "            if field_val < \"{}\" {{ matched_branch = Some(\"{}\"); }}\n",
2340                escaped_val, port
2341            ));
2342        }
2343        "gte" | ">=" => {
2344            code.push_str(&format!(
2345                "            if field_val >= \"{}\" {{ matched_branch = Some(\"{}\"); }}\n",
2346                escaped_val, port
2347            ));
2348        }
2349        "lte" | "<=" => {
2350            code.push_str(&format!(
2351                "            if field_val <= \"{}\" {{ matched_branch = Some(\"{}\"); }}\n",
2352                escaped_val, port
2353            ));
2354        }
2355        "startsWith" => {
2356            code.push_str(&format!(
2357                "            if field_val.starts_with(\"{}\") {{ matched_branch = Some(\"{}\"); }}\n",
2358                escaped_val, port
2359            ));
2360        }
2361        "endsWith" => {
2362            code.push_str(&format!(
2363                "            if field_val.ends_with(\"{}\") {{ matched_branch = Some(\"{}\"); }}\n",
2364                escaped_val, port
2365            ));
2366        }
2367        "empty" => {
2368            code.push_str(&format!(
2369                "            if field_val.is_empty() {{ matched_branch = Some(\"{}\"); }}\n",
2370                port
2371            ));
2372        }
2373        "exists" => {
2374            code.push_str(&format!(
2375                "            if ctx.state.contains_key(\"{}\") {{ matched_branch = Some(\"{}\"); }}\n",
2376                field, port
2377            ));
2378        }
2379        _ => {
2380            code.push_str(&format!(
2381                "            if field_val == \"{}\" {{ matched_branch = Some(\"{}\"); }}\n",
2382                escaped_val, port
2383            ));
2384        }
2385    }
2386}
2387
2388/// Helper: generate a condition check for all_match Switch (pushes to matched_branches vec)
2389fn generate_all_match_condition_check(
2390    code: &mut String,
2391    op: &str,
2392    value_str: &str,
2393    port: &str,
2394    field: &str,
2395) {
2396    let escaped_val = value_str.replace('"', "\\\"");
2397    match op {
2398        "equals" | "==" | "eq" => {
2399            code.push_str(&format!(
2400                "            if field_val == \"{}\" {{ matched_branches.push(\"{}\".to_string()); }}\n",
2401                escaped_val, port
2402            ));
2403        }
2404        "not_equals" | "!=" | "neq" => {
2405            code.push_str(&format!(
2406                "            if field_val != \"{}\" {{ matched_branches.push(\"{}\".to_string()); }}\n",
2407                escaped_val, port
2408            ));
2409        }
2410        "contains" => {
2411            code.push_str(&format!(
2412                "            if field_val.contains(\"{}\") {{ matched_branches.push(\"{}\".to_string()); }}\n",
2413                escaped_val, port
2414            ));
2415        }
2416        "greater_than" | ">" | "gt" => {
2417            code.push_str(&format!(
2418                "            if field_val > \"{}\" {{ matched_branches.push(\"{}\".to_string()); }}\n",
2419                escaped_val, port
2420            ));
2421        }
2422        "less_than" | "<" | "lt" => {
2423            code.push_str(&format!(
2424                "            if field_val < \"{}\" {{ matched_branches.push(\"{}\".to_string()); }}\n",
2425                escaped_val, port
2426            ));
2427        }
2428        "gte" | ">=" => {
2429            code.push_str(&format!(
2430                "            if field_val >= \"{}\" {{ matched_branches.push(\"{}\".to_string()); }}\n",
2431                escaped_val, port
2432            ));
2433        }
2434        "lte" | "<=" => {
2435            code.push_str(&format!(
2436                "            if field_val <= \"{}\" {{ matched_branches.push(\"{}\".to_string()); }}\n",
2437                escaped_val, port
2438            ));
2439        }
2440        "startsWith" => {
2441            code.push_str(&format!(
2442                "            if field_val.starts_with(\"{}\") {{ matched_branches.push(\"{}\".to_string()); }}\n",
2443                escaped_val, port
2444            ));
2445        }
2446        "endsWith" => {
2447            code.push_str(&format!(
2448                "            if field_val.ends_with(\"{}\") {{ matched_branches.push(\"{}\".to_string()); }}\n",
2449                escaped_val, port
2450            ));
2451        }
2452        "empty" => {
2453            code.push_str(&format!(
2454                "            if field_val.is_empty() {{ matched_branches.push(\"{}\".to_string()); }}\n",
2455                port
2456            ));
2457        }
2458        "exists" => {
2459            code.push_str(&format!(
2460                "            if ctx.state.contains_key(\"{}\") {{ matched_branches.push(\"{}\".to_string()); }}\n",
2461                field, port
2462            ));
2463        }
2464        _ => {
2465            code.push_str(&format!(
2466                "            if field_val == \"{}\" {{ matched_branches.push(\"{}\".to_string()); }}\n",
2467                escaped_val, port
2468            ));
2469        }
2470    }
2471}
2472
2473fn generate_action_node_function(
2474    node_id: &str,
2475    node: &crate::codegen::action_nodes::ActionNodeConfig,
2476    predecessor_map: &std::collections::HashMap<&str, Vec<&str>>,
2477    parallel_branch_agents: &std::collections::HashSet<String>,
2478) -> String {
2479    use crate::codegen::action_nodes::ActionNodeConfig;
2480
2481    let mut code = String::new();
2482
2483    // Check if this node's predecessor is a parallel-branch agent
2484    // If so, expressions like {{response}} should be rewritten to {{predecessor_response}}
2485    let parallel_predecessor: Option<&str> = predecessor_map
2486        .get(node_id)
2487        .and_then(|preds| preds.iter().find(|p| parallel_branch_agents.contains(**p)))
2488        .copied();
2489
2490    match node {
2491        ActionNodeConfig::Set(config) => {
2492            code.push_str(&format!(
2493                "    // Action Node: {} (Set)\n",
2494                config.standard.name
2495            ));
2496            code.push_str(&format!("    let {}_node = adk_graph::node::FunctionNode::new(\"{}\", |ctx| async move {{\n", node_id, node_id));
2497            code.push_str("        let mut output = NodeOutput::new();\n");
2498
2499            // Generate variable setting logic
2500            for var in &config.variables {
2501                let key = &var.key;
2502                match var.value_type.as_str() {
2503                    "expression" => {
2504                        // Expression type - interpolate variables from state
2505                        let raw_expr = var.value.as_str().unwrap_or("");
2506                        // If predecessor is a parallel-branch agent, rewrite {{response}} to {{agent_response}}
2507                        let expr = if let Some(pred_id) = parallel_predecessor {
2508                            raw_expr
2509                                .replace("{{response}}", &format!("{{{{{}_response}}}}", pred_id))
2510                        } else {
2511                            raw_expr.to_string()
2512                        };
2513                        // Simple variable interpolation: replace {{var}} with state value
2514                        code.push_str(&format!("        // Set {} from expression\n", key));
2515                        code.push_str(&format!(
2516                            "        let mut {}_value = \"{}\".to_string();\n",
2517                            key,
2518                            expr.replace('"', "\\\"")
2519                        ));
2520                        code.push_str("        // Interpolate variables from state\n");
2521                        code.push_str("        for (k, v) in ctx.state.iter() {\n");
2522                        code.push_str("            let pattern = format!(\"{{{{{}}}}}\", k);\n");
2523                        code.push_str("            if let Some(s) = v.as_str() {\n");
2524                        code.push_str(&format!(
2525                            "                {}_value = {}_value.replace(&pattern, s);\n",
2526                            key, key
2527                        ));
2528                        code.push_str("            } else {\n");
2529                        code.push_str(&format!("                {}_value = {}_value.replace(&pattern, &v.to_string());\n", key, key));
2530                        code.push_str("            }\n");
2531                        code.push_str("        }\n");
2532                        code.push_str(&format!(
2533                            "        output = output.with_update(\"{}\", json!({}_value));\n",
2534                            key, key
2535                        ));
2536                    }
2537                    "json" => {
2538                        // JSON type - use value directly
2539                        code.push_str(&format!(
2540                            "        output = output.with_update(\"{}\", json!({}));\n",
2541                            key, var.value
2542                        ));
2543                    }
2544                    _ => {
2545                        // String or other types
2546                        let raw_val = var.value.as_str().unwrap_or("");
2547                        if raw_val.contains("{{") {
2548                            // String contains template variables — treat like expression
2549                            let val = if let Some(pred_id) = parallel_predecessor {
2550                                raw_val.replace(
2551                                    "{{response}}",
2552                                    &format!("{{{{{}_response}}}}", pred_id),
2553                                )
2554                            } else {
2555                                raw_val.to_string()
2556                            };
2557                            code.push_str(&format!(
2558                                "        // Set {} from string template\n",
2559                                key
2560                            ));
2561                            code.push_str(&format!(
2562                                "        let mut {}_value = \"{}\".to_string();\n",
2563                                key,
2564                                val.replace('"', "\\\"")
2565                            ));
2566                            code.push_str("        for (k, v) in ctx.state.iter() {\n");
2567                            code.push_str(
2568                                "            let pattern = format!(\"{{{{{}}}}}\", k);\n",
2569                            );
2570                            code.push_str("            if let Some(s) = v.as_str() {\n");
2571                            code.push_str(&format!(
2572                                "                {}_value = {}_value.replace(&pattern, s);\n",
2573                                key, key
2574                            ));
2575                            code.push_str("            } else {\n");
2576                            code.push_str(&format!("                {}_value = {}_value.replace(&pattern, &v.to_string());\n", key, key));
2577                            code.push_str("            }\n");
2578                            code.push_str("        }\n");
2579                            code.push_str(&format!(
2580                                "        output = output.with_update(\"{}\", json!({}_value));\n",
2581                                key, key
2582                            ));
2583                        } else {
2584                            code.push_str(&format!(
2585                                "        output = output.with_update(\"{}\", json!({}));\n",
2586                                key, var.value
2587                            ));
2588                        }
2589                    }
2590                }
2591            }
2592
2593            code.push_str("        Ok(output)\n");
2594            code.push_str("    });\n\n");
2595        }
2596        ActionNodeConfig::Transform(config) => {
2597            code.push_str(&format!(
2598                "    // Action Node: {} (Transform)\n",
2599                config.standard.name
2600            ));
2601            code.push_str(&format!("    let {}_node = adk_graph::node::FunctionNode::new(\"{}\", |ctx| async move {{\n", node_id, node_id));
2602
2603            // Simple template transformation
2604            let expr = &config.expression;
2605            code.push_str(&format!(
2606                "        let mut result = \"{}\".to_string();\n",
2607                expr.replace('"', "\\\"")
2608            ));
2609            code.push_str("        for (k, v) in ctx.state.iter() {\n");
2610            code.push_str("            let pattern = format!(\"{{{{{}}}}}\", k);\n");
2611            code.push_str("            if let Some(s) = v.as_str() {\n");
2612            code.push_str("                result = result.replace(&pattern, s);\n");
2613            code.push_str("            } else {\n");
2614            code.push_str("                result = result.replace(&pattern, &v.to_string());\n");
2615            code.push_str("            }\n");
2616            code.push_str("        }\n");
2617
2618            let output_key = &config.standard.mapping.output_key;
2619            code.push_str(&format!(
2620                "        Ok(NodeOutput::new().with_update(\"{}\", json!(result)))\n",
2621                output_key
2622            ));
2623            code.push_str("    });\n\n");
2624        }
2625        ActionNodeConfig::Switch(config) => {
2626            code.push_str(&format!(
2627                "    // Action Node: {} (Switch)\n",
2628                config.standard.name
2629            ));
2630            code.push_str(&format!("    let {}_node = adk_graph::node::FunctionNode::new(\"{}\", |ctx| async move {{\n", node_id, node_id));
2631
2632            let output_key = if config.standard.mapping.output_key.is_empty() {
2633                "branch"
2634            } else {
2635                &config.standard.mapping.output_key
2636            };
2637
2638            match config.evaluation_mode {
2639                crate::codegen::action_nodes::EvaluationMode::FirstMatch => {
2640                    // First match: evaluate conditions, return first matching branch for routing
2641                    code.push_str("        let mut matched_branch: Option<&str> = None;\n");
2642
2643                    for condition in &config.conditions {
2644                        let field = &condition.field;
2645                        let op = &condition.operator;
2646                        let port = &condition.output_port;
2647                        let value_str = condition
2648                            .value
2649                            .as_ref()
2650                            .map(|v| match v {
2651                                serde_json::Value::String(s) => s.clone(),
2652                                other => other.to_string(),
2653                            })
2654                            .unwrap_or_default();
2655
2656                        code.push_str(&format!(
2657                            "        // Condition: {} {} {}\n",
2658                            field, op, value_str
2659                        ));
2660                        code.push_str("        if matched_branch.is_none() {\n");
2661                        code.push_str(&format!(
2662                            "            let field_val = ctx.state.get(\"{}\").and_then(|v| v.as_str()).unwrap_or(\"\");\n",
2663                            field
2664                        ));
2665
2666                        generate_condition_check(&mut code, op, &value_str, port, field);
2667                        code.push_str("        }\n");
2668                    }
2669
2670                    // Default branch
2671                    if let Some(default) = &config.default_branch {
2672                        code.push_str(&format!(
2673                            "        let branch = matched_branch.unwrap_or(\"{}\");\n",
2674                            default
2675                        ));
2676                    } else {
2677                        code.push_str(
2678                            "        let branch = matched_branch.unwrap_or(\"default\");\n",
2679                        );
2680                    }
2681
2682                    code.push_str(&format!(
2683                        "        Ok(NodeOutput::new().with_update(\"{}\", json!(branch)))\n",
2684                        output_key
2685                    ));
2686                }
2687                crate::codegen::action_nodes::EvaluationMode::AllMatch => {
2688                    // All match (fan-out): evaluate all conditions, store matched branches in state
2689                    // All connected branches execute via direct edges regardless — this is for observability
2690                    code.push_str(
2691                        "        let mut matched_branches: Vec<String> = Vec::new();\n\n",
2692                    );
2693
2694                    for condition in &config.conditions {
2695                        let field = &condition.field;
2696                        let op = &condition.operator;
2697                        let port = &condition.output_port;
2698                        let value_str = condition
2699                            .value
2700                            .as_ref()
2701                            .map(|v| match v {
2702                                serde_json::Value::String(s) => s.clone(),
2703                                other => other.to_string(),
2704                            })
2705                            .unwrap_or_default();
2706
2707                        code.push_str(&format!(
2708                            "        // Condition: {} {} {}\n",
2709                            field, op, value_str
2710                        ));
2711                        code.push_str("        {\n");
2712                        code.push_str(&format!(
2713                            "            let field_val = ctx.state.get(\"{}\").and_then(|v| v.as_str()).unwrap_or(\"\");\n",
2714                            field
2715                        ));
2716
2717                        generate_all_match_condition_check(&mut code, op, &value_str, port, field);
2718                        code.push_str("        }\n");
2719                    }
2720
2721                    // Store matched branches as a JSON array for observability/debugging
2722                    code.push_str(&format!(
2723                        "\n        Ok(NodeOutput::new().with_update(\"{}\", json!(matched_branches)))\n",
2724                        output_key
2725                    ));
2726                }
2727            }
2728
2729            code.push_str("    });\n\n");
2730        }
2731        ActionNodeConfig::Loop(config) => {
2732            code.push_str(&format!(
2733                "    // Action Node: {} (Loop)\n",
2734                config.standard.name
2735            ));
2736            code.push_str(&format!("    let {}_node = adk_graph::node::FunctionNode::new(\"{}\", |ctx| async move {{\n", node_id, node_id));
2737            code.push_str("        let mut output = NodeOutput::new();\n");
2738
2739            match config.loop_type {
2740                crate::codegen::action_nodes::LoopType::ForEach => {
2741                    let source = config
2742                        .for_each
2743                        .as_ref()
2744                        .map(|f| f.source_array.as_str())
2745                        .unwrap_or("items");
2746                    let item_var = config
2747                        .for_each
2748                        .as_ref()
2749                        .map(|f| f.item_var.as_str())
2750                        .unwrap_or("item");
2751                    let index_var = config
2752                        .for_each
2753                        .as_ref()
2754                        .map(|f| f.index_var.as_str())
2755                        .unwrap_or("index");
2756                    let counter_key = format!("{}_loop_index", node_id);
2757                    let done_key = format!("{}_loop_done", node_id);
2758
2759                    code.push_str(&format!("        // forEach: iterate over '{}'\n", source));
2760                    code.push_str(&format!("        let source_arr = ctx.state.get(\"{}\").and_then(|v| v.as_array()).cloned().unwrap_or_default();\n", source));
2761                    code.push_str(&format!("        let idx = ctx.state.get(\"{}\").and_then(|v| v.as_u64()).unwrap_or(0) as usize;\n", counter_key));
2762                    code.push_str("        if idx < source_arr.len() {\n");
2763                    code.push_str("            let current_item = source_arr[idx].clone();\n");
2764                    code.push_str(&format!(
2765                        "            output = output.with_update(\"{}\", current_item);\n",
2766                        item_var
2767                    ));
2768                    code.push_str(&format!(
2769                        "            output = output.with_update(\"{}\", json!(idx));\n",
2770                        index_var
2771                    ));
2772                    code.push_str(&format!(
2773                        "            output = output.with_update(\"{}\", json!(idx + 1));\n",
2774                        counter_key
2775                    ));
2776                    code.push_str(&format!(
2777                        "            output = output.with_update(\"{}\", json!(false));\n",
2778                        done_key
2779                    ));
2780
2781                    // Collect results
2782                    if config.results.collect {
2783                        let agg_key = config
2784                            .results
2785                            .aggregation_key
2786                            .as_deref()
2787                            .unwrap_or("loop_results");
2788                        code.push_str("            // Collect previous iteration result if any\n");
2789                        code.push_str(&format!("            let mut results = ctx.state.get(\"{}\").and_then(|v| v.as_array()).cloned().unwrap_or_default();\n", agg_key));
2790                        code.push_str("            if idx > 0 {\n");
2791                        code.push_str(
2792                            "                // Capture the response from the previous iteration\n",
2793                        );
2794                        code.push_str(
2795                            "                if let Some(resp) = ctx.state.get(\"response\") {\n",
2796                        );
2797                        code.push_str("                    results.push(resp.clone());\n");
2798                        code.push_str("                }\n");
2799                        code.push_str("            }\n");
2800                        code.push_str(&format!(
2801                            "            output = output.with_update(\"{}\", json!(results));\n",
2802                            agg_key
2803                        ));
2804                    }
2805
2806                    code.push_str("        } else {\n");
2807                    code.push_str(&format!(
2808                        "            output = output.with_update(\"{}\", json!(true));\n",
2809                        done_key
2810                    ));
2811
2812                    // Final collection on completion
2813                    if config.results.collect {
2814                        let agg_key = config
2815                            .results
2816                            .aggregation_key
2817                            .as_deref()
2818                            .unwrap_or("loop_results");
2819                        code.push_str(&format!("            let mut results = ctx.state.get(\"{}\").and_then(|v| v.as_array()).cloned().unwrap_or_default();\n", agg_key));
2820                        code.push_str(
2821                            "            if let Some(resp) = ctx.state.get(\"response\") {\n",
2822                        );
2823                        code.push_str("                results.push(resp.clone());\n");
2824                        code.push_str("            }\n");
2825                        code.push_str(&format!(
2826                            "            output = output.with_update(\"{}\", json!(results));\n",
2827                            agg_key
2828                        ));
2829                    }
2830
2831                    code.push_str("        }\n");
2832                }
2833                crate::codegen::action_nodes::LoopType::Times => {
2834                    let count = config
2835                        .times
2836                        .as_ref()
2837                        .map(|t| match &t.count {
2838                            serde_json::Value::Number(n) => n.as_u64().unwrap_or(3) as usize,
2839                            _ => 3,
2840                        })
2841                        .unwrap_or(3);
2842                    let counter_key = format!("{}_loop_index", node_id);
2843                    let done_key = format!("{}_loop_done", node_id);
2844
2845                    code.push_str(&format!("        // times: repeat {} times\n", count));
2846                    code.push_str(&format!("        let idx = ctx.state.get(\"{}\").and_then(|v| v.as_u64()).unwrap_or(0) as usize;\n", counter_key));
2847                    code.push_str(&format!("        if idx < {} {{\n", count));
2848                    code.push_str(
2849                        "            output = output.with_update(\"index\", json!(idx));\n",
2850                    );
2851                    code.push_str(&format!(
2852                        "            output = output.with_update(\"{}\", json!(idx + 1));\n",
2853                        counter_key
2854                    ));
2855                    code.push_str(&format!(
2856                        "            output = output.with_update(\"{}\", json!(false));\n",
2857                        done_key
2858                    ));
2859
2860                    if config.results.collect {
2861                        let agg_key = config
2862                            .results
2863                            .aggregation_key
2864                            .as_deref()
2865                            .unwrap_or("loop_results");
2866                        code.push_str(&format!("            let mut results = ctx.state.get(\"{}\").and_then(|v| v.as_array()).cloned().unwrap_or_default();\n", agg_key));
2867                        code.push_str("            if idx > 0 {\n");
2868                        code.push_str(
2869                            "                if let Some(resp) = ctx.state.get(\"response\") {\n",
2870                        );
2871                        code.push_str("                    results.push(resp.clone());\n");
2872                        code.push_str("                }\n");
2873                        code.push_str("            }\n");
2874                        code.push_str(&format!(
2875                            "            output = output.with_update(\"{}\", json!(results));\n",
2876                            agg_key
2877                        ));
2878                    }
2879
2880                    code.push_str("        } else {\n");
2881                    code.push_str(&format!(
2882                        "            output = output.with_update(\"{}\", json!(true));\n",
2883                        done_key
2884                    ));
2885
2886                    if config.results.collect {
2887                        let agg_key = config
2888                            .results
2889                            .aggregation_key
2890                            .as_deref()
2891                            .unwrap_or("loop_results");
2892                        code.push_str(&format!("            let mut results = ctx.state.get(\"{}\").and_then(|v| v.as_array()).cloned().unwrap_or_default();\n", agg_key));
2893                        code.push_str(
2894                            "            if let Some(resp) = ctx.state.get(\"response\") {\n",
2895                        );
2896                        code.push_str("                results.push(resp.clone());\n");
2897                        code.push_str("            }\n");
2898                        code.push_str(&format!(
2899                            "            output = output.with_update(\"{}\", json!(results));\n",
2900                            agg_key
2901                        ));
2902                    }
2903
2904                    code.push_str("        }\n");
2905                }
2906                crate::codegen::action_nodes::LoopType::While => {
2907                    let condition_field = config
2908                        .while_config
2909                        .as_ref()
2910                        .map(|w| w.condition.as_str())
2911                        .unwrap_or("should_continue");
2912                    let counter_key = format!("{}_loop_index", node_id);
2913                    let done_key = format!("{}_loop_done", node_id);
2914
2915                    code.push_str(&format!(
2916                        "        // while: loop while '{}' is truthy\n",
2917                        condition_field
2918                    ));
2919                    code.push_str(&format!("        let idx = ctx.state.get(\"{}\").and_then(|v| v.as_u64()).unwrap_or(0) as usize;\n", counter_key));
2920                    code.push_str(&format!(
2921                        "        let cond_val = ctx.state.get(\"{}\");\n",
2922                        condition_field
2923                    ));
2924                    code.push_str("        let should_continue = match cond_val {\n");
2925                    code.push_str(
2926                        "            Some(v) if v.is_boolean() => v.as_bool().unwrap_or(false),\n",
2927                    );
2928                    code.push_str("            Some(v) if v.is_string() => !v.as_str().unwrap_or(\"\").is_empty() && v.as_str() != Some(\"false\"),\n");
2929                    code.push_str("            Some(v) if v.is_number() => v.as_f64().unwrap_or(0.0) != 0.0,\n");
2930                    code.push_str("            Some(v) if v.is_null() => false,\n");
2931                    code.push_str("            Some(_) => true,\n");
2932                    code.push_str("            None => false,\n");
2933                    code.push_str("        };\n");
2934                    code.push_str("        if should_continue {\n");
2935                    code.push_str(
2936                        "            output = output.with_update(\"index\", json!(idx));\n",
2937                    );
2938                    code.push_str(&format!(
2939                        "            output = output.with_update(\"{}\", json!(idx + 1));\n",
2940                        counter_key
2941                    ));
2942                    code.push_str(&format!(
2943                        "            output = output.with_update(\"{}\", json!(false));\n",
2944                        done_key
2945                    ));
2946                    code.push_str("        } else {\n");
2947                    code.push_str(&format!(
2948                        "            output = output.with_update(\"{}\", json!(true));\n",
2949                        done_key
2950                    ));
2951                    code.push_str("        }\n");
2952                }
2953            }
2954
2955            code.push_str("        Ok(output)\n");
2956            code.push_str("    });\n\n");
2957        }
2958        ActionNodeConfig::Merge(config) => {
2959            code.push_str(&format!(
2960                "    // Action Node: {} (Merge)\n",
2961                config.standard.name
2962            ));
2963            code.push_str(&format!("    let {}_node = adk_graph::node::FunctionNode::new(\"{}\", |ctx| async move {{\n", node_id, node_id));
2964            code.push_str("        let mut output = NodeOutput::new();\n");
2965
2966            let output_key = if config.standard.mapping.output_key.is_empty() {
2967                "merged"
2968            } else {
2969                &config.standard.mapping.output_key
2970            };
2971
2972            // Determine branch keys for collecting results
2973            let branch_keys = config.branch_keys.as_deref().unwrap_or(&[]);
2974
2975            match config.combine_strategy {
2976                crate::codegen::action_nodes::CombineStrategy::Array => {
2977                    code.push_str("        // Combine strategy: array — collect branch outputs into an array\n");
2978                    code.push_str(
2979                        "        let mut results: Vec<serde_json::Value> = Vec::new();\n",
2980                    );
2981                    if branch_keys.is_empty() {
2982                        // Collect all non-system state values
2983                        code.push_str("        for (k, v) in ctx.state.iter() {\n");
2984                        code.push_str(
2985                            "            if k != \"message\" && k != \"classification\" {\n",
2986                        );
2987                        code.push_str("                results.push(v.clone());\n");
2988                        code.push_str("            }\n");
2989                        code.push_str("        }\n");
2990                    } else {
2991                        for key in branch_keys {
2992                            code.push_str(&format!(
2993                                "        if let Some(v) = ctx.state.get(\"{}\") {{\n",
2994                                key
2995                            ));
2996                            code.push_str("            results.push(v.clone());\n");
2997                            code.push_str("        }\n");
2998                        }
2999                    }
3000                    code.push_str(&format!(
3001                        "        output = output.with_update(\"{}\", json!(results));\n",
3002                        output_key
3003                    ));
3004                }
3005                crate::codegen::action_nodes::CombineStrategy::Object => {
3006                    code.push_str("        // Combine strategy: object — merge branch outputs into an object\n");
3007                    code.push_str("        let mut merged = serde_json::Map::new();\n");
3008                    if branch_keys.is_empty() {
3009                        code.push_str("        for (k, v) in ctx.state.iter() {\n");
3010                        code.push_str(
3011                            "            if k != \"message\" && k != \"classification\" {\n",
3012                        );
3013                        code.push_str("                merged.insert(k.clone(), v.clone());\n");
3014                        code.push_str("            }\n");
3015                        code.push_str("        }\n");
3016                    } else {
3017                        for key in branch_keys {
3018                            code.push_str(&format!(
3019                                "        if let Some(v) = ctx.state.get(\"{}\") {{\n",
3020                                key
3021                            ));
3022                            code.push_str(&format!(
3023                                "            merged.insert(\"{}\".to_string(), v.clone());\n",
3024                                key
3025                            ));
3026                            code.push_str("        }\n");
3027                        }
3028                    }
3029                    code.push_str(&format!(
3030                        "        output = output.with_update(\"{}\", json!(merged));\n",
3031                        output_key
3032                    ));
3033                }
3034                crate::codegen::action_nodes::CombineStrategy::First => {
3035                    code.push_str(
3036                        "        // Combine strategy: first — use first available branch output\n",
3037                    );
3038                    if branch_keys.is_empty() {
3039                        code.push_str("        let first_val = ctx.state.get(\"response\").cloned().unwrap_or(json!(null));\n");
3040                    } else {
3041                        code.push_str("        let first_val = None\n");
3042                        for key in branch_keys {
3043                            code.push_str(&format!(
3044                                "            .or_else(|| ctx.state.get(\"{}\").cloned())\n",
3045                                key
3046                            ));
3047                        }
3048                        code.push_str("            .unwrap_or(json!(null));\n");
3049                    }
3050                    code.push_str(&format!(
3051                        "        output = output.with_update(\"{}\", first_val);\n",
3052                        output_key
3053                    ));
3054                }
3055                crate::codegen::action_nodes::CombineStrategy::Last => {
3056                    code.push_str(
3057                        "        // Combine strategy: last — use last available branch output\n",
3058                    );
3059                    if branch_keys.is_empty() {
3060                        code.push_str("        let last_val = ctx.state.get(\"response\").cloned().unwrap_or(json!(null));\n");
3061                    } else {
3062                        code.push_str("        let last_val = None\n");
3063                        for key in branch_keys.iter().rev() {
3064                            code.push_str(&format!(
3065                                "            .or_else(|| ctx.state.get(\"{}\").cloned())\n",
3066                                key
3067                            ));
3068                        }
3069                        code.push_str("            .unwrap_or(json!(null));\n");
3070                    }
3071                    code.push_str(&format!(
3072                        "        output = output.with_update(\"{}\", last_val);\n",
3073                        output_key
3074                    ));
3075                }
3076            }
3077
3078            code.push_str("        Ok(output)\n");
3079            code.push_str("    });\n\n");
3080        }
3081        ActionNodeConfig::Http(config) => {
3082            code.push_str(&format!(
3083                "    // Action Node: {} (HTTP)\n",
3084                config.standard.name
3085            ));
3086            code.push_str(&format!("    let {}_node = adk_graph::node::FunctionNode::new(\"{}\", |ctx| async move {{\n", node_id, node_id));
3087
3088            let output_key = if config.standard.mapping.output_key.is_empty() {
3089                "httpResult"
3090            } else {
3091                &config.standard.mapping.output_key
3092            };
3093
3094            // Build URL with variable interpolation
3095            let raw_url = &config.url;
3096            let url = if let Some(pred_id) = parallel_predecessor {
3097                raw_url.replace("{{response}}", &format!("{{{{{}_response}}}}", pred_id))
3098            } else {
3099                raw_url.to_string()
3100            };
3101
3102            code.push_str(&format!(
3103                "        let mut url = \"{}\".to_string();\n",
3104                url.replace('"', "\\\"")
3105            ));
3106            code.push_str("        for (k, v) in ctx.state.iter() {\n");
3107            code.push_str("            let pattern = format!(\"{{{{{}}}}}\", k);\n");
3108            code.push_str("            if let Some(s) = v.as_str() {\n");
3109            code.push_str("                url = url.replace(&pattern, s);\n");
3110            code.push_str("            } else {\n");
3111            code.push_str("                url = url.replace(&pattern, &v.to_string());\n");
3112            code.push_str("            }\n");
3113            code.push_str("        }\n\n");
3114
3115            // Build the request
3116            let method_fn = match config.method {
3117                crate::codegen::action_node_types::HttpMethod::Get => "get",
3118                crate::codegen::action_node_types::HttpMethod::Post => "post",
3119                crate::codegen::action_node_types::HttpMethod::Put => "put",
3120                crate::codegen::action_node_types::HttpMethod::Patch => "patch",
3121                crate::codegen::action_node_types::HttpMethod::Delete => "delete",
3122            };
3123
3124            code.push_str("        let client = reqwest::Client::new();\n");
3125
3126            // Only make req mutable if we'll modify it (headers, auth, or body)
3127            let needs_mut = !config.headers.is_empty()
3128                || config.auth.auth_type != "none"
3129                || config.body.body_type != "none";
3130            if needs_mut {
3131                code.push_str(&format!(
3132                    "        let mut req = client.{}(&url);\n\n",
3133                    method_fn
3134                ));
3135            } else {
3136                code.push_str(&format!(
3137                    "        let req = client.{}(&url);\n\n",
3138                    method_fn
3139                ));
3140            }
3141
3142            // Headers
3143            for (key, value) in &config.headers {
3144                let val = if let Some(pred_id) = parallel_predecessor {
3145                    value.replace("{{response}}", &format!("{{{{{}_response}}}}", pred_id))
3146                } else {
3147                    value.to_string()
3148                };
3149                if val.contains("{{") {
3150                    // Header value has template variables — interpolate from state
3151                    code.push_str(&format!(
3152                        "        let mut hdr_val = \"{}\".to_string();\n",
3153                        val.replace('"', "\\\"")
3154                    ));
3155                    code.push_str("        for (k, v) in ctx.state.iter() {\n");
3156                    code.push_str("            let pattern = format!(\"{{{{{}}}}}\", k);\n");
3157                    code.push_str("            if let Some(s) = v.as_str() {\n");
3158                    code.push_str("                hdr_val = hdr_val.replace(&pattern, s);\n");
3159                    code.push_str("            } else {\n");
3160                    code.push_str(
3161                        "                hdr_val = hdr_val.replace(&pattern, &v.to_string());\n",
3162                    );
3163                    code.push_str("            }\n");
3164                    code.push_str("        }\n");
3165                    code.push_str(&format!(
3166                        "        req = req.header(\"{}\", hdr_val);\n",
3167                        key.replace('"', "\\\"")
3168                    ));
3169                } else {
3170                    code.push_str(&format!(
3171                        "        req = req.header(\"{}\", \"{}\");\n",
3172                        key.replace('"', "\\\""),
3173                        val.replace('"', "\\\"")
3174                    ));
3175                }
3176            }
3177
3178            // Authentication
3179            match config.auth.auth_type.as_str() {
3180                "bearer" => {
3181                    if let Some(bearer) = &config.auth.bearer {
3182                        let token = &bearer.token;
3183                        if token.contains("{{") {
3184                            code.push_str(&format!(
3185                                "        let mut bearer_token = \"{}\".to_string();\n",
3186                                token.replace('"', "\\\"")
3187                            ));
3188                            code.push_str("        for (k, v) in ctx.state.iter() {\n");
3189                            code.push_str(
3190                                "            let pattern = format!(\"{{{{{}}}}}\", k);\n",
3191                            );
3192                            code.push_str("            if let Some(s) = v.as_str() {\n");
3193                            code.push_str("                bearer_token = bearer_token.replace(&pattern, s);\n");
3194                            code.push_str("            }\n");
3195                            code.push_str("        }\n");
3196                            code.push_str("        req = req.bearer_auth(&bearer_token);\n");
3197                        } else {
3198                            code.push_str(&format!(
3199                                "        req = req.bearer_auth(\"{}\");\n",
3200                                token.replace('"', "\\\"")
3201                            ));
3202                        }
3203                    }
3204                }
3205                "basic" => {
3206                    if let Some(basic) = &config.auth.basic {
3207                        code.push_str(&format!(
3208                            "        req = req.basic_auth(\"{}\", Some(\"{}\"));\n",
3209                            basic.username.replace('"', "\\\""),
3210                            basic.password.replace('"', "\\\"")
3211                        ));
3212                    }
3213                }
3214                "api_key" => {
3215                    if let Some(api_key) = &config.auth.api_key {
3216                        let val = &api_key.value;
3217                        if val.contains("{{") {
3218                            code.push_str(&format!(
3219                                "        let mut api_key_val = \"{}\".to_string();\n",
3220                                val.replace('"', "\\\"")
3221                            ));
3222                            code.push_str("        for (k, v) in ctx.state.iter() {\n");
3223                            code.push_str(
3224                                "            let pattern = format!(\"{{{{{}}}}}\", k);\n",
3225                            );
3226                            code.push_str("            if let Some(s) = v.as_str() {\n");
3227                            code.push_str(
3228                                "                api_key_val = api_key_val.replace(&pattern, s);\n",
3229                            );
3230                            code.push_str("            }\n");
3231                            code.push_str("        }\n");
3232                            code.push_str(&format!(
3233                                "        req = req.header(\"{}\", api_key_val);\n",
3234                                api_key.header_name.replace('"', "\\\"")
3235                            ));
3236                        } else {
3237                            code.push_str(&format!(
3238                                "        req = req.header(\"{}\", \"{}\");\n",
3239                                api_key.header_name.replace('"', "\\\""),
3240                                val.replace('"', "\\\"")
3241                            ));
3242                        }
3243                    }
3244                }
3245                _ => {} // "none" — no auth
3246            }
3247
3248            // Body
3249            match config.body.body_type.as_str() {
3250                "json" => {
3251                    if let Some(content) = &config.body.content {
3252                        let body_str = content.to_string();
3253                        if body_str.contains("{{") {
3254                            // Body has template variables — interpolate
3255                            code.push_str(&format!(
3256                                "        let mut body_str = r#\"{}\"#.to_string();\n",
3257                                body_str.replace('"', "\\\"")
3258                            ));
3259                            code.push_str("        for (k, v) in ctx.state.iter() {\n");
3260                            code.push_str(
3261                                "            let pattern = format!(\"{{{{{}}}}}\", k);\n",
3262                            );
3263                            code.push_str("            if let Some(s) = v.as_str() {\n");
3264                            code.push_str(
3265                                "                body_str = body_str.replace(&pattern, s);\n",
3266                            );
3267                            code.push_str("            } else {\n");
3268                            code.push_str("                body_str = body_str.replace(&pattern, &v.to_string());\n");
3269                            code.push_str("            }\n");
3270                            code.push_str("        }\n");
3271                            code.push_str("        let body_json: serde_json::Value = serde_json::from_str(&body_str).unwrap_or(json!(body_str));\n");
3272                            code.push_str("        req = req.json(&body_json);\n");
3273                        } else {
3274                            code.push_str(&format!(
3275                                "        req = req.json(&json!({}));\n",
3276                                body_str
3277                            ));
3278                        }
3279                    }
3280                }
3281                "form" => {
3282                    if let Some(content) = &config.body.content {
3283                        code.push_str(&format!("        req = req.form(&json!({}));\n", content));
3284                    }
3285                }
3286                "raw" => {
3287                    if let Some(content) = &config.body.content {
3288                        let raw = content
3289                            .as_str()
3290                            .map(|s| s.to_string())
3291                            .unwrap_or_else(|| content.to_string());
3292                        code.push_str(&format!(
3293                            "        req = req.body(\"{}\");\n",
3294                            raw.replace('"', "\\\"")
3295                        ));
3296                    }
3297                }
3298                _ => {} // "none" — no body
3299            }
3300
3301            // Send request and handle response
3302            code.push_str("\n        let resp = req.send().await;\n");
3303            code.push_str("        match resp {\n");
3304            code.push_str("            Ok(response) => {\n");
3305            code.push_str("                let status = response.status().as_u16();\n");
3306
3307            // Status validation
3308            if let Some(validation) = &config.response.status_validation {
3309                if validation.contains('-') {
3310                    let parts: Vec<&str> = validation.split('-').collect();
3311                    if parts.len() == 2 {
3312                        code.push_str(&format!(
3313                            "                if status < {} || status > {} {{\n",
3314                            parts[0].trim(),
3315                            parts[1].trim()
3316                        ));
3317                        code.push_str("                    let body = response.text().await.unwrap_or_default();\n");
3318                        code.push_str(&format!(
3319                            "                    return Ok(NodeOutput::new().with_update(\"{}\", json!({{\n",
3320                            output_key
3321                        ));
3322                        code.push_str("                        \"error\": true, \"status\": status, \"body\": body\n");
3323                        code.push_str("                    })));\n");
3324                        code.push_str("                }\n");
3325                    }
3326                }
3327            }
3328
3329            // Parse response based on type
3330            match config.response.response_type.as_str() {
3331                "text" => {
3332                    code.push_str(
3333                        "                let body = response.text().await.unwrap_or_default();\n",
3334                    );
3335                    if let Some(json_path) = &config.response.json_path {
3336                        // Even for text, if jsonPath is set, try to parse and extract
3337                        code.push_str("                let parsed: serde_json::Value = serde_json::from_str(&body).unwrap_or(json!(body));\n");
3338                        code.push_str(&format!(
3339                            "                // JSONPath extraction: {}\n",
3340                            json_path
3341                        ));
3342                        generate_json_path_extraction(&mut code, json_path, output_key);
3343                    } else {
3344                        code.push_str(&format!(
3345                            "                Ok(NodeOutput::new().with_update(\"{}\", json!({{ \"status\": status, \"body\": body }})))\n",
3346                            output_key
3347                        ));
3348                    }
3349                }
3350                _ => {
3351                    // Default: JSON — try JSON first, fall back to text
3352                    code.push_str(
3353                        "                let text = response.text().await.unwrap_or_default();\n",
3354                    );
3355                    code.push_str("                let body: serde_json::Value = serde_json::from_str(&text).unwrap_or_else(|_| json!(text));\n");
3356                    if let Some(json_path) = &config.response.json_path {
3357                        code.push_str(&format!(
3358                            "                // JSONPath extraction: {}\n",
3359                            json_path
3360                        ));
3361                        generate_json_path_extraction(&mut code, json_path, output_key);
3362                    } else {
3363                        code.push_str(&format!(
3364                            "                Ok(NodeOutput::new().with_update(\"{}\", json!({{ \"status\": status, \"body\": body }})))\n",
3365                            output_key
3366                        ));
3367                    }
3368                }
3369            }
3370
3371            code.push_str("            }\n");
3372            code.push_str("            Err(e) => {\n");
3373            code.push_str(&format!(
3374                "                Ok(NodeOutput::new().with_update(\"{}\", json!({{ \"error\": true, \"message\": e.to_string() }})))\n",
3375                output_key
3376            ));
3377            code.push_str("            }\n");
3378            code.push_str("        }\n");
3379            code.push_str("    });\n\n");
3380        }
3381        ActionNodeConfig::Wait(config) => {
3382            code.push_str(&format!(
3383                "    // Action Node: {} (Wait)\n",
3384                config.standard.name
3385            ));
3386            code.push_str(&format!("    let {}_node = adk_graph::node::FunctionNode::new(\"{}\", |_ctx| async move {{\n", node_id, node_id));
3387
3388            let output_key = if config.standard.mapping.output_key.is_empty() {
3389                "wait_result"
3390            } else {
3391                &config.standard.mapping.output_key
3392            };
3393
3394            match config.wait_type {
3395                crate::codegen::action_nodes::WaitType::Fixed => {
3396                    if let Some(fixed) = &config.fixed {
3397                        let ms = match fixed.unit.as_str() {
3398                            "ms" => fixed.duration,
3399                            "s" => fixed.duration * 1000,
3400                            "m" => fixed.duration * 60 * 1000,
3401                            "h" => fixed.duration * 60 * 60 * 1000,
3402                            _ => fixed.duration,
3403                        };
3404                        code.push_str(&format!(
3405                            "        // Fixed wait: {} {}\n",
3406                            fixed.duration, fixed.unit
3407                        ));
3408                        code.push_str(&format!("        tokio::time::sleep(std::time::Duration::from_millis({})).await;\n", ms));
3409                        code.push_str(&format!("        Ok(NodeOutput::new().with_update(\"{}\", json!({{ \"waited\": true, \"duration_ms\": {} }})))\n", output_key, ms));
3410                    } else {
3411                        // No fixed config — default 1 second
3412                        code.push_str("        // Fixed wait: default 1s\n");
3413                        code.push_str("        tokio::time::sleep(std::time::Duration::from_millis(1000)).await;\n");
3414                        code.push_str(&format!("        Ok(NodeOutput::new().with_update(\"{}\", json!({{ \"waited\": true, \"duration_ms\": 1000 }})))\n", output_key));
3415                    }
3416                }
3417                crate::codegen::action_nodes::WaitType::Until => {
3418                    if let Some(until) = &config.until {
3419                        code.push_str("        // Wait until timestamp\n");
3420                        code.push_str(&format!(
3421                            "        let target = chrono::DateTime::parse_from_rfc3339(\"{}\")\n",
3422                            until.timestamp
3423                        ));
3424                        code.push_str(
3425                            "            .unwrap_or_else(|_| chrono::Utc::now().fixed_offset());\n",
3426                        );
3427                        code.push_str("        let now = chrono::Utc::now();\n");
3428                        code.push_str("        if target > now {\n");
3429                        code.push_str("            let duration = (target - now).to_std().unwrap_or_default();\n");
3430                        code.push_str("            tokio::time::sleep(duration).await;\n");
3431                        code.push_str("        }\n");
3432                        code.push_str(&format!("        Ok(NodeOutput::new().with_update(\"{}\", json!({{ \"waited\": true }})))\n", output_key));
3433                    } else {
3434                        code.push_str(&format!("        Ok(NodeOutput::new().with_update(\"{}\", json!({{ \"waited\": false, \"reason\": \"no timestamp configured\" }})))\n", output_key));
3435                    }
3436                }
3437                crate::codegen::action_nodes::WaitType::Condition => {
3438                    if let Some(condition) = &config.condition {
3439                        code.push_str("        // Poll until condition is met\n");
3440                        code.push_str(&format!(
3441                            "        let poll_interval = std::time::Duration::from_millis({});\n",
3442                            condition.poll_interval
3443                        ));
3444                        code.push_str(&format!(
3445                            "        let max_wait = std::time::Duration::from_millis({});\n",
3446                            condition.max_wait
3447                        ));
3448                        code.push_str("        let start = std::time::Instant::now();\n");
3449                        code.push_str("        loop {\n");
3450                        code.push_str("            if start.elapsed() >= max_wait {\n");
3451                        code.push_str(&format!("                break Ok(NodeOutput::new().with_update(\"{}\", json!({{ \"waited\": true, \"timed_out\": true }})));\n", output_key));
3452                        code.push_str("            }\n");
3453                        code.push_str("            tokio::time::sleep(poll_interval).await;\n");
3454                        code.push_str("        }\n");
3455                    } else {
3456                        code.push_str(&format!("        Ok(NodeOutput::new().with_update(\"{}\", json!({{ \"waited\": false }})))\n", output_key));
3457                    }
3458                }
3459                crate::codegen::action_nodes::WaitType::Webhook => {
3460                    // Webhook wait is complex — generate a simple timeout-based placeholder
3461                    if let Some(webhook) = &config.webhook {
3462                        code.push_str("        // Webhook wait (simplified): sleep for timeout as placeholder\n");
3463                        code.push_str(&format!("        tokio::time::sleep(std::time::Duration::from_millis({})).await;\n", webhook.timeout.min(30000)));
3464                        code.push_str(&format!("        Ok(NodeOutput::new().with_update(\"{}\", json!({{ \"waited\": true, \"webhook\": \"{}\" }})))\n", output_key, webhook.path.replace('"', "\\\"")));
3465                    } else {
3466                        code.push_str(&format!("        Ok(NodeOutput::new().with_update(\"{}\", json!({{ \"waited\": false }})))\n", output_key));
3467                    }
3468                }
3469            }
3470
3471            code.push_str("    });\n\n");
3472        }
3473        ActionNodeConfig::Database(config) => {
3474            code.push_str(&format!(
3475                "    // Action Node: {} (Database - {:?})\n",
3476                config.standard.name, config.db_type
3477            ));
3478            code.push_str(&format!("    let {}_node = adk_graph::node::FunctionNode::new(\"{}\", |ctx| async move {{\n", node_id, node_id));
3479
3480            let output_key = if config.standard.mapping.output_key.is_empty() {
3481                "dbResult"
3482            } else {
3483                &config.standard.mapping.output_key
3484            };
3485
3486            // Connection string with variable interpolation
3487            let conn_str = &config.connection.connection_string;
3488            if conn_str.contains("{{") {
3489                code.push_str(&format!(
3490                    "        let mut conn_str = \"{}\".to_string();\n",
3491                    conn_str.replace('"', "\\\"")
3492                ));
3493                code.push_str("        for (k, v) in ctx.state.iter() {\n");
3494                code.push_str("            let pattern = format!(\"{{{{{}}}}}\", k);\n");
3495                code.push_str("            if let Some(s) = v.as_str() {\n");
3496                code.push_str("                conn_str = conn_str.replace(&pattern, s);\n");
3497                code.push_str("            } else {\n");
3498                code.push_str(
3499                    "                conn_str = conn_str.replace(&pattern, &v.to_string());\n",
3500                );
3501                code.push_str("            }\n");
3502                code.push_str("        }\n");
3503            } else {
3504                code.push_str(&format!(
3505                    "        let conn_str = \"{}\".to_string();\n",
3506                    conn_str.replace('"', "\\\"")
3507                ));
3508            }
3509
3510            // Also support credential_ref — override conn_str from state if ref is set
3511            if let Some(cred_ref) = &config.connection.credential_ref {
3512                if !cred_ref.is_empty() {
3513                    code.push_str(&format!(
3514                        "        let conn_str = ctx.state.get(\"{}\").and_then(|v| v.as_str()).map(|s| s.to_string()).unwrap_or(conn_str);\n",
3515                        cred_ref
3516                    ));
3517                }
3518            }
3519
3520            match config.db_type {
3521                crate::codegen::action_node_types::DatabaseType::Postgresql
3522                | crate::codegen::action_node_types::DatabaseType::Mysql
3523                | crate::codegen::action_node_types::DatabaseType::Sqlite => {
3524                    // SQL databases via sqlx
3525                    if let Some(sql) = &config.sql {
3526                        let query = &sql.query;
3527                        // Interpolate {{variables}} in the query
3528                        if query.contains("{{") {
3529                            code.push_str(&format!(
3530                                "        let mut query_str = \"{}\".to_string();\n",
3531                                query.replace('"', "\\\"")
3532                            ));
3533                            code.push_str("        for (k, v) in ctx.state.iter() {\n");
3534                            code.push_str(
3535                                "            let pattern = format!(\"{{{{{}}}}}\", k);\n",
3536                            );
3537                            code.push_str("            if let Some(s) = v.as_str() {\n");
3538                            code.push_str(
3539                                "                query_str = query_str.replace(&pattern, s);\n",
3540                            );
3541                            code.push_str("            } else {\n");
3542                            code.push_str("                query_str = query_str.replace(&pattern, &v.to_string());\n");
3543                            code.push_str("            }\n");
3544                            code.push_str("        }\n");
3545                        } else {
3546                            code.push_str(&format!(
3547                                "        let query_str = \"{}\".to_string();\n",
3548                                query.replace('"', "\\\"")
3549                            ));
3550                        }
3551
3552                        match config.db_type {
3553                            crate::codegen::action_node_types::DatabaseType::Sqlite => {
3554                                code.push_str("        let pool = sqlx::SqlitePool::connect(&conn_str).await\n");
3555                                code.push_str("            .map_err(|e| adk_graph::GraphError::NodeExecutionFailed { node: \"db\".into(), message: format!(\"SQLite connection failed: {}\", e) })?;\n");
3556                            }
3557                            crate::codegen::action_node_types::DatabaseType::Mysql => {
3558                                code.push_str("        let pool = sqlx::MySqlPool::connect(&conn_str).await\n");
3559                                code.push_str("            .map_err(|e| adk_graph::GraphError::NodeExecutionFailed { node: \"db\".into(), message: format!(\"MySQL connection failed: {}\", e) })?;\n");
3560                            }
3561                            _ => {
3562                                // PostgreSQL (default)
3563                                code.push_str(
3564                                    "        let pool = sqlx::PgPool::connect(&conn_str).await\n",
3565                                );
3566                                code.push_str("            .map_err(|e| adk_graph::GraphError::NodeExecutionFailed { node: \"db\".into(), message: format!(\"PostgreSQL connection failed: {}\", e) })?;\n");
3567                            }
3568                        }
3569
3570                        match sql.operation.as_str() {
3571                            "query" => {
3572                                // SELECT — returns rows as JSON array
3573                                // Use database-specific row type for proper type inference
3574                                code.push_str("        use sqlx::Row;\n");
3575                                code.push_str("        use sqlx::Column;\n");
3576                                code.push_str("        let raw_rows = sqlx::query(&query_str)\n");
3577                                code.push_str("            .fetch_all(&pool).await\n");
3578                                code.push_str("            .map_err(|e| adk_graph::GraphError::NodeExecutionFailed { node: \"db\".into(), message: format!(\"Query failed: {}\", e) })?;\n");
3579                                code.push_str("        let rows: Vec<serde_json::Value> = raw_rows.iter().map(|row| {\n");
3580                                code.push_str(
3581                                    "            let mut obj = serde_json::Map::new();\n",
3582                                );
3583                                code.push_str("            for col in row.columns() {\n");
3584                                code.push_str(
3585                                    "                let name = col.name().to_string();\n",
3586                                );
3587                                code.push_str("                let val: serde_json::Value = row.try_get::<String, _>(col.name())\n");
3588                                code.push_str("                    .map(|s| json!(s))\n");
3589                                code.push_str("                    .or_else(|_| row.try_get::<i64, _>(col.name()).map(|n| json!(n)))\n");
3590                                code.push_str("                    .or_else(|_| row.try_get::<f64, _>(col.name()).map(|n| json!(n)))\n");
3591                                code.push_str("                    .or_else(|_| row.try_get::<bool, _>(col.name()).map(|b| json!(b)))\n");
3592                                code.push_str("                    .unwrap_or(json!(null));\n");
3593                                code.push_str("                obj.insert(name, val);\n");
3594                                code.push_str("            }\n");
3595                                code.push_str("            serde_json::Value::Object(obj)\n");
3596                                code.push_str("        }).collect();\n");
3597                                code.push_str("        let row_count = rows.len();\n");
3598                                code.push_str(&format!(
3599                                    "        Ok(NodeOutput::new().with_update(\"{}\", json!({{ \"rows\": rows, \"count\": row_count }})))\n",
3600                                    output_key
3601                                ));
3602                            }
3603                            _ => {
3604                                // INSERT, UPDATE, DELETE, UPSERT — returns affected rows
3605                                code.push_str("        let result = sqlx::query(&query_str)\n");
3606                                code.push_str("            .execute(&pool).await\n");
3607                                code.push_str("            .map_err(|e| adk_graph::GraphError::NodeExecutionFailed { node: \"db\".into(), message: format!(\"Query failed: {}\", e) })?;\n");
3608                                code.push_str(&format!(
3609                                    "        Ok(NodeOutput::new().with_update(\"{}\", json!({{ \"affected\": result.rows_affected(), \"operation\": \"{}\" }})))\n",
3610                                    output_key, sql.operation
3611                                ));
3612                            }
3613                        }
3614                    } else {
3615                        code.push_str(&format!(
3616                            "        Ok(NodeOutput::new().with_update(\"{}\", json!({{ \"error\": true, \"message\": \"No SQL query configured\" }})))\n",
3617                            output_key
3618                        ));
3619                    }
3620                }
3621                crate::codegen::action_node_types::DatabaseType::Mongodb => {
3622                    if let Some(mongo) = &config.mongodb {
3623                        code.push_str(
3624                            "        let client = mongodb::Client::with_uri_str(&conn_str).await\n",
3625                        );
3626                        code.push_str("            .map_err(|e| adk_graph::GraphError::NodeExecutionFailed { node: \"db\".into(), message: format!(\"MongoDB connection failed: {}\", e) })?;\n");
3627                        // Extract database name from connection string
3628                        code.push_str("        let db_name = conn_str.rsplit('/').next()\n");
3629                        code.push_str("            .and_then(|s| s.split('?').next())\n");
3630                        code.push_str("            .filter(|s| !s.is_empty())\n");
3631                        code.push_str("            .unwrap_or(\"test\");\n");
3632                        code.push_str("        let db = client.database(db_name);\n");
3633                        code.push_str(&format!(
3634                            "        let collection = db.collection::<mongodb::bson::Document>(\"{}\");\n",
3635                            mongo.collection.replace('"', "\\\"")
3636                        ));
3637
3638                        let filter_str = mongo
3639                            .filter
3640                            .as_ref()
3641                            .map(|f| f.to_string())
3642                            .unwrap_or_else(|| "{}".to_string());
3643
3644                        match mongo.operation.as_str() {
3645                            "find" => {
3646                                code.push_str(&format!(
3647                                    "        let filter_json: serde_json::Value = serde_json::from_str(r#\"{}\"#).unwrap_or(json!({{}}));\n",
3648                                    filter_str.replace('"', "\\\"")
3649                                ));
3650                                code.push_str("        let filter_doc = mongodb::bson::to_document(&filter_json).unwrap_or_default();\n");
3651                                code.push_str("        use futures::TryStreamExt;\n");
3652                                code.push_str(
3653                                    "        let mut cursor = collection.find(filter_doc).await\n",
3654                                );
3655                                code.push_str("            .map_err(|e| adk_graph::GraphError::NodeExecutionFailed { node: \"db\".into(), message: format!(\"MongoDB find failed: {}\", e) })?;\n");
3656                                code.push_str(
3657                                    "        let mut docs: Vec<serde_json::Value> = Vec::new();\n",
3658                                );
3659                                code.push_str(
3660                                    "        while let Some(doc) = cursor.try_next().await\n",
3661                                );
3662                                code.push_str("            .map_err(|e| adk_graph::GraphError::NodeExecutionFailed { node: \"db\".into(), message: format!(\"Cursor error: {}\", e) })? {\n");
3663                                code.push_str("            if let Ok(json) = mongodb::bson::from_document::<serde_json::Value>(doc) {\n");
3664                                code.push_str("                docs.push(json);\n");
3665                                code.push_str("            }\n");
3666                                code.push_str("        }\n");
3667                                code.push_str(&format!(
3668                                    "        Ok(NodeOutput::new().with_update(\"{}\", json!({{ \"docs\": docs, \"count\": docs.len() }})))\n",
3669                                    output_key
3670                                ));
3671                            }
3672                            "findOne" => {
3673                                code.push_str(&format!(
3674                                    "        let filter_json: serde_json::Value = serde_json::from_str(r#\"{}\"#).unwrap_or(json!({{}}));\n",
3675                                    filter_str.replace('"', "\\\"")
3676                                ));
3677                                code.push_str("        let filter_doc = mongodb::bson::to_document(&filter_json).unwrap_or_default();\n");
3678                                code.push_str(
3679                                    "        let result = collection.find_one(filter_doc).await\n",
3680                                );
3681                                code.push_str("            .map_err(|e| adk_graph::GraphError::NodeExecutionFailed { node: \"db\".into(), message: format!(\"MongoDB findOne failed: {}\", e) })?;\n");
3682                                code.push_str("        let doc_json = result.map(|d| mongodb::bson::from_document::<serde_json::Value>(d).unwrap_or(json!(null))).unwrap_or(json!(null));\n");
3683                                code.push_str(&format!(
3684                                    "        Ok(NodeOutput::new().with_update(\"{}\", json!({{ \"doc\": doc_json }})))\n",
3685                                    output_key
3686                                ));
3687                            }
3688                            "insert" => {
3689                                let doc_str = mongo
3690                                    .document
3691                                    .as_ref()
3692                                    .map(|d| d.to_string())
3693                                    .unwrap_or_else(|| "{}".to_string());
3694                                code.push_str(&format!(
3695                                    "        let doc_json: serde_json::Value = serde_json::from_str(r#\"{}\"#).unwrap_or(json!({{}}));\n",
3696                                    doc_str.replace('"', "\\\"")
3697                                ));
3698                                code.push_str("        let doc = mongodb::bson::to_document(&doc_json).unwrap_or_default();\n");
3699                                code.push_str(
3700                                    "        let result = collection.insert_one(doc).await\n",
3701                                );
3702                                code.push_str("            .map_err(|e| adk_graph::GraphError::NodeExecutionFailed { node: \"db\".into(), message: format!(\"MongoDB insert failed: {}\", e) })?;\n");
3703                                code.push_str(&format!(
3704                                    "        Ok(NodeOutput::new().with_update(\"{}\", json!({{ \"inserted_id\": result.inserted_id.to_string() }})))\n",
3705                                    output_key
3706                                ));
3707                            }
3708                            "update" => {
3709                                code.push_str(&format!(
3710                                    "        let filter_json: serde_json::Value = serde_json::from_str(r#\"{}\"#).unwrap_or(json!({{}}));\n",
3711                                    filter_str.replace('"', "\\\"")
3712                                ));
3713                                code.push_str("        let filter_doc = mongodb::bson::to_document(&filter_json).unwrap_or_default();\n");
3714                                let doc_str = mongo
3715                                    .document
3716                                    .as_ref()
3717                                    .map(|d| d.to_string())
3718                                    .unwrap_or_else(|| "{}".to_string());
3719                                code.push_str(&format!(
3720                                    "        let update_json: serde_json::Value = serde_json::from_str(r#\"{}\"#).unwrap_or(json!({{}}));\n",
3721                                    doc_str.replace('"', "\\\"")
3722                                ));
3723                                code.push_str("        let update_doc = mongodb::bson::to_document(&update_json).unwrap_or_default();\n");
3724                                code.push_str("        let result = collection.update_many(filter_doc, update_doc).await\n");
3725                                code.push_str("            .map_err(|e| adk_graph::GraphError::NodeExecutionFailed { node: \"db\".into(), message: format!(\"MongoDB update failed: {}\", e) })?;\n");
3726                                code.push_str(&format!(
3727                                    "        Ok(NodeOutput::new().with_update(\"{}\", json!({{ \"matched\": result.matched_count, \"modified\": result.modified_count }})))\n",
3728                                    output_key
3729                                ));
3730                            }
3731                            "delete" => {
3732                                code.push_str(&format!(
3733                                    "        let filter_json: serde_json::Value = serde_json::from_str(r#\"{}\"#).unwrap_or(json!({{}}));\n",
3734                                    filter_str.replace('"', "\\\"")
3735                                ));
3736                                code.push_str("        let filter_doc = mongodb::bson::to_document(&filter_json).unwrap_or_default();\n");
3737                                code.push_str("        let result = collection.delete_many(filter_doc).await\n");
3738                                code.push_str("            .map_err(|e| adk_graph::GraphError::NodeExecutionFailed { node: \"db\".into(), message: format!(\"MongoDB delete failed: {}\", e) })?;\n");
3739                                code.push_str(&format!(
3740                                    "        Ok(NodeOutput::new().with_update(\"{}\", json!({{ \"deleted\": result.deleted_count }})))\n",
3741                                    output_key
3742                                ));
3743                            }
3744                            _ => {
3745                                code.push_str(&format!(
3746                                    "        Ok(NodeOutput::new().with_update(\"{}\", json!({{ \"error\": true, \"message\": \"Unknown MongoDB operation\" }})))\n",
3747                                    output_key
3748                                ));
3749                            }
3750                        }
3751                    } else {
3752                        code.push_str(&format!(
3753                            "        Ok(NodeOutput::new().with_update(\"{}\", json!({{ \"error\": true, \"message\": \"No MongoDB config\" }})))\n",
3754                            output_key
3755                        ));
3756                    }
3757                }
3758                crate::codegen::action_node_types::DatabaseType::Redis => {
3759                    if let Some(redis_cfg) = &config.redis {
3760                        code.push_str(
3761                            "        let client = redis::Client::open(conn_str.as_str())\n",
3762                        );
3763                        code.push_str("            .map_err(|e| adk_graph::GraphError::NodeExecutionFailed { node: \"db\".into(), message: format!(\"Redis connection failed: {}\", e) })?;\n");
3764                        code.push_str("        let mut con = client.get_multiplexed_async_connection().await\n");
3765                        code.push_str("            .map_err(|e| adk_graph::GraphError::NodeExecutionFailed { node: \"db\".into(), message: format!(\"Redis connect failed: {}\", e) })?;\n");
3766
3767                        // Key with variable interpolation
3768                        let key = &redis_cfg.key;
3769                        if key.contains("{{") {
3770                            code.push_str(&format!(
3771                                "        let mut redis_key = \"{}\".to_string();\n",
3772                                key.replace('"', "\\\"")
3773                            ));
3774                            code.push_str("        for (k, v) in ctx.state.iter() {\n");
3775                            code.push_str(
3776                                "            let pattern = format!(\"{{{{{}}}}}\", k);\n",
3777                            );
3778                            code.push_str("            if let Some(s) = v.as_str() {\n");
3779                            code.push_str(
3780                                "                redis_key = redis_key.replace(&pattern, s);\n",
3781                            );
3782                            code.push_str("            }\n");
3783                            code.push_str("        }\n");
3784                        } else {
3785                            code.push_str(&format!(
3786                                "        let redis_key = \"{}\".to_string();\n",
3787                                key.replace('"', "\\\"")
3788                            ));
3789                        }
3790
3791                        code.push_str("        use redis::AsyncCommands;\n");
3792
3793                        match redis_cfg.operation.as_str() {
3794                            "get" => {
3795                                code.push_str(
3796                                    "        let val: Option<String> = con.get(&redis_key).await\n",
3797                                );
3798                                code.push_str("            .map_err(|e| adk_graph::GraphError::NodeExecutionFailed { node: \"db\".into(), message: format!(\"Redis GET failed: {}\", e) })?;\n");
3799                                code.push_str(&format!(
3800                                    "        Ok(NodeOutput::new().with_update(\"{}\", json!({{ \"value\": val }})))\n",
3801                                    output_key
3802                                ));
3803                            }
3804                            "set" => {
3805                                let val_str = redis_cfg
3806                                    .value
3807                                    .as_ref()
3808                                    .map(|v| match v {
3809                                        serde_json::Value::String(s) => s.clone(),
3810                                        other => other.to_string(),
3811                                    })
3812                                    .unwrap_or_default();
3813                                code.push_str(&format!(
3814                                    "        let val = \"{}\";\n",
3815                                    val_str.replace('"', "\\\"")
3816                                ));
3817                                if let Some(ttl) = redis_cfg.ttl {
3818                                    code.push_str(&format!(
3819                                        "        let _: () = con.set_ex(&redis_key, val, {}).await\n",
3820                                        ttl
3821                                    ));
3822                                } else {
3823                                    code.push_str(
3824                                        "        let _: () = con.set(&redis_key, val).await\n",
3825                                    );
3826                                }
3827                                code.push_str("            .map_err(|e| adk_graph::GraphError::NodeExecutionFailed { node: \"db\".into(), message: format!(\"Redis SET failed: {}\", e) })?;\n");
3828                                code.push_str(&format!(
3829                                    "        Ok(NodeOutput::new().with_update(\"{}\", json!({{ \"set\": true, \"key\": redis_key }})))\n",
3830                                    output_key
3831                                ));
3832                            }
3833                            "del" => {
3834                                code.push_str(
3835                                    "        let deleted: i64 = con.del(&redis_key).await\n",
3836                                );
3837                                code.push_str("            .map_err(|e| adk_graph::GraphError::NodeExecutionFailed { node: \"db\".into(), message: format!(\"Redis DEL failed: {}\", e) })?;\n");
3838                                code.push_str(&format!(
3839                                    "        Ok(NodeOutput::new().with_update(\"{}\", json!({{ \"deleted\": deleted }})))\n",
3840                                    output_key
3841                                ));
3842                            }
3843                            "hget" => {
3844                                // For HGET, use value as field name
3845                                let field = redis_cfg
3846                                    .value
3847                                    .as_ref()
3848                                    .and_then(|v| v.as_str())
3849                                    .unwrap_or("field");
3850                                code.push_str(&format!(
3851                                    "        let val: Option<String> = con.hget(&redis_key, \"{}\").await\n",
3852                                    field.replace('"', "\\\"")
3853                                ));
3854                                code.push_str("            .map_err(|e| adk_graph::GraphError::NodeExecutionFailed { node: \"db\".into(), message: format!(\"Redis HGET failed: {}\", e) })?;\n");
3855                                code.push_str(&format!(
3856                                    "        Ok(NodeOutput::new().with_update(\"{}\", json!({{ \"value\": val }})))\n",
3857                                    output_key
3858                                ));
3859                            }
3860                            "hset" => {
3861                                let val_str = redis_cfg
3862                                    .value
3863                                    .as_ref()
3864                                    .map(|v| v.to_string())
3865                                    .unwrap_or_else(|| "{}".to_string());
3866                                code.push_str(&format!(
3867                                    "        let fields: serde_json::Value = serde_json::from_str(r#\"{}\"#).unwrap_or(json!({{}}));\n",
3868                                    val_str.replace('"', "\\\"")
3869                                ));
3870                                code.push_str("        if let Some(obj) = fields.as_object() {\n");
3871                                code.push_str("            for (field, val) in obj {\n");
3872                                code.push_str("                let val_str = val.as_str().map(|s| s.to_string()).unwrap_or_else(|| val.to_string());\n");
3873                                code.push_str("                let _: () = con.hset(&redis_key, field, &val_str).await\n");
3874                                code.push_str("                    .map_err(|e| adk_graph::GraphError::NodeExecutionFailed { node: \"db\".into(), message: format!(\"Redis HSET failed: {}\", e) })?;\n");
3875                                code.push_str("            }\n");
3876                                code.push_str("        }\n");
3877                                code.push_str(&format!(
3878                                    "        Ok(NodeOutput::new().with_update(\"{}\", json!({{ \"set\": true, \"key\": redis_key }})))\n",
3879                                    output_key
3880                                ));
3881                            }
3882                            "lpush" => {
3883                                let val_str = redis_cfg
3884                                    .value
3885                                    .as_ref()
3886                                    .map(|v| match v {
3887                                        serde_json::Value::String(s) => s.clone(),
3888                                        other => other.to_string(),
3889                                    })
3890                                    .unwrap_or_default();
3891                                code.push_str(&format!(
3892                                    "        let len: i64 = con.lpush(&redis_key, \"{}\").await\n",
3893                                    val_str.replace('"', "\\\"")
3894                                ));
3895                                code.push_str("            .map_err(|e| adk_graph::GraphError::NodeExecutionFailed { node: \"db\".into(), message: format!(\"Redis LPUSH failed: {}\", e) })?;\n");
3896                                code.push_str(&format!(
3897                                    "        Ok(NodeOutput::new().with_update(\"{}\", json!({{ \"length\": len }})))\n",
3898                                    output_key
3899                                ));
3900                            }
3901                            "rpop" => {
3902                                code.push_str("        let val: Option<String> = con.rpop(&redis_key, None).await\n");
3903                                code.push_str("            .map_err(|e| adk_graph::GraphError::NodeExecutionFailed { node: \"db\".into(), message: format!(\"Redis RPOP failed: {}\", e) })?;\n");
3904                                code.push_str(&format!(
3905                                    "        Ok(NodeOutput::new().with_update(\"{}\", json!({{ \"value\": val }})))\n",
3906                                    output_key
3907                                ));
3908                            }
3909                            _ => {
3910                                code.push_str(&format!(
3911                                    "        Ok(NodeOutput::new().with_update(\"{}\", json!({{ \"error\": true, \"message\": \"Unknown Redis operation\" }})))\n",
3912                                    output_key
3913                                ));
3914                            }
3915                        }
3916                    } else {
3917                        code.push_str(&format!(
3918                            "        Ok(NodeOutput::new().with_update(\"{}\", json!({{ \"error\": true, \"message\": \"No Redis config\" }})))\n",
3919                            output_key
3920                        ));
3921                    }
3922                }
3923            }
3924
3925            code.push_str("    });\n\n");
3926        }
3927        ActionNodeConfig::Email(config) => {
3928            code.push_str(&format!(
3929                "    // Action Node: {} (Email - {:?})\n",
3930                config.standard.name, config.mode
3931            ));
3932            code.push_str(&format!("    let {}_node = adk_graph::node::FunctionNode::new(\"{}\", |ctx| async move {{\n", node_id, node_id));
3933
3934            let output_key = if config.standard.mapping.output_key.is_empty() {
3935                "emailResult"
3936            } else {
3937                &config.standard.mapping.output_key
3938            };
3939
3940            match config.mode {
3941                crate::codegen::action_node_types::EmailMode::Send => {
3942                    if let (Some(smtp), Some(recipients), Some(content)) =
3943                        (&config.smtp, &config.recipients, &config.content)
3944                    {
3945                        // Variable interpolation helper for subject and body
3946                        code.push_str(
3947                            "        // Build email content with variable interpolation\n",
3948                        );
3949
3950                        // Subject interpolation
3951                        let subject = &content.subject;
3952                        if subject.contains("{{") {
3953                            code.push_str(&format!(
3954                                "        let mut subject = \"{}\".to_string();\n",
3955                                subject.replace('"', "\\\"")
3956                            ));
3957                            code.push_str("        for (k, v) in ctx.state.iter() {\n");
3958                            code.push_str(
3959                                "            let pattern = format!(\"{{{{{}}}}}\", k);\n",
3960                            );
3961                            code.push_str("            if let Some(s) = v.as_str() {\n");
3962                            code.push_str(
3963                                "                subject = subject.replace(&pattern, s);\n",
3964                            );
3965                            code.push_str("            } else {\n");
3966                            code.push_str("                subject = subject.replace(&pattern, &v.to_string());\n");
3967                            code.push_str("            }\n");
3968                            code.push_str("        }\n");
3969                        } else {
3970                            code.push_str(&format!(
3971                                "        let subject = \"{}\".to_string();\n",
3972                                subject.replace('"', "\\\"")
3973                            ));
3974                        }
3975
3976                        // Body interpolation
3977                        let body = &content.body;
3978                        if body.contains("{{") {
3979                            code.push_str(&format!(
3980                                "        let mut body = \"{}\".to_string();\n",
3981                                body.replace('"', "\\\"").replace('\n', "\\n")
3982                            ));
3983                            code.push_str("        for (k, v) in ctx.state.iter() {\n");
3984                            code.push_str(
3985                                "            let pattern = format!(\"{{{{{}}}}}\", k);\n",
3986                            );
3987                            code.push_str("            if let Some(s) = v.as_str() {\n");
3988                            code.push_str("                body = body.replace(&pattern, s);\n");
3989                            code.push_str("            } else {\n");
3990                            code.push_str(
3991                                "                body = body.replace(&pattern, &v.to_string());\n",
3992                            );
3993                            code.push_str("            }\n");
3994                            code.push_str("        }\n");
3995                        } else {
3996                            code.push_str(&format!(
3997                                "        let body = \"{}\".to_string();\n",
3998                                body.replace('"', "\\\"").replace('\n', "\\n")
3999                            ));
4000                        }
4001
4002                        // Build the email message
4003                        let from_addr = if let Some(ref name) = smtp.from_name {
4004                            format!(
4005                                "{} <{}>",
4006                                name.replace('"', "\\\""),
4007                                smtp.from_email.replace('"', "\\\"")
4008                            )
4009                        } else {
4010                            smtp.from_email.replace('"', "\\\"")
4011                        };
4012
4013                        code.push_str(&format!(
4014                            "        let from = \"{}\".parse::<lettre::message::Mailbox>()\n",
4015                            from_addr
4016                        ));
4017                        code.push_str("            .map_err(|e| adk_graph::GraphError::NodeExecutionFailed { node: \"email\".into(), message: format!(\"Invalid from address: {}\", e) })?;\n");
4018
4019                        // To recipients
4020                        code.push_str(&format!(
4021                            "        let to_addrs = \"{}\".to_string();\n",
4022                            recipients.to.replace('"', "\\\"")
4023                        ));
4024
4025                        code.push_str(
4026                            "        let mut email_builder = lettre::Message::builder()\n",
4027                        );
4028                        code.push_str("            .from(from)\n");
4029                        code.push_str("            .subject(&subject);\n");
4030
4031                        // Add To recipients (comma-separated)
4032                        code.push_str("        for addr in to_addrs.split(',').map(|s| s.trim()).filter(|s| !s.is_empty()) {\n");
4033                        code.push_str("            if let Ok(mbox) = addr.parse::<lettre::message::Mailbox>() {\n");
4034                        code.push_str("                email_builder = email_builder.to(mbox);\n");
4035                        code.push_str("            }\n");
4036                        code.push_str("        }\n");
4037
4038                        // CC recipients
4039                        if let Some(ref cc) = recipients.cc {
4040                            if !cc.is_empty() {
4041                                code.push_str(&format!(
4042                                    "        for addr in \"{}\".split(',').map(|s| s.trim()).filter(|s| !s.is_empty()) {{\n",
4043                                    cc.replace('"', "\\\"")
4044                                ));
4045                                code.push_str("            if let Ok(mbox) = addr.parse::<lettre::message::Mailbox>() {\n");
4046                                code.push_str(
4047                                    "                email_builder = email_builder.cc(mbox);\n",
4048                                );
4049                                code.push_str("            }\n");
4050                                code.push_str("        }\n");
4051                            }
4052                        }
4053
4054                        // BCC recipients
4055                        if let Some(ref bcc) = recipients.bcc {
4056                            if !bcc.is_empty() {
4057                                code.push_str(&format!(
4058                                    "        for addr in \"{}\".split(',').map(|s| s.trim()).filter(|s| !s.is_empty()) {{\n",
4059                                    bcc.replace('"', "\\\"")
4060                                ));
4061                                code.push_str("            if let Ok(mbox) = addr.parse::<lettre::message::Mailbox>() {\n");
4062                                code.push_str(
4063                                    "                email_builder = email_builder.bcc(mbox);\n",
4064                                );
4065                                code.push_str("            }\n");
4066                                code.push_str("        }\n");
4067                            }
4068                        }
4069
4070                        // Build message body based on type
4071                        match content.body_type {
4072                            crate::codegen::action_node_types::EmailBodyType::Html => {
4073                                code.push_str("        let email = email_builder\n");
4074                                code.push_str("            .header(lettre::message::header::ContentType::TEXT_HTML)\n");
4075                                code.push_str("            .body(body.clone())\n");
4076                                code.push_str("            .map_err(|e| adk_graph::GraphError::NodeExecutionFailed { node: \"email\".into(), message: format!(\"Failed to build email: {}\", e) })?;\n");
4077                            }
4078                            _ => {
4079                                code.push_str("        let email = email_builder\n");
4080                                code.push_str("            .header(lettre::message::header::ContentType::TEXT_PLAIN)\n");
4081                                code.push_str("            .body(body.clone())\n");
4082                                code.push_str("            .map_err(|e| adk_graph::GraphError::NodeExecutionFailed { node: \"email\".into(), message: format!(\"Failed to build email: {}\", e) })?;\n");
4083                            }
4084                        }
4085
4086                        // SMTP transport — connection string with variable interpolation for password
4087                        let host = smtp.host.replace('"', "\\\"");
4088                        let username = smtp.username.replace('"', "\\\"");
4089                        let password = smtp.password.replace('"', "\\\"");
4090
4091                        // Support password from state via {{variable}}
4092                        if password.contains("{{") {
4093                            code.push_str(&format!(
4094                                "        let mut smtp_password = \"{}\".to_string();\n",
4095                                password
4096                            ));
4097                            code.push_str("        for (k, v) in ctx.state.iter() {\n");
4098                            code.push_str(
4099                                "            let pattern = format!(\"{{{{{}}}}}\", k);\n",
4100                            );
4101                            code.push_str("            if let Some(s) = v.as_str() {\n");
4102                            code.push_str("                smtp_password = smtp_password.replace(&pattern, s);\n");
4103                            code.push_str("            }\n");
4104                            code.push_str("        }\n");
4105                        } else {
4106                            code.push_str(&format!(
4107                                "        let smtp_password = \"{}\".to_string();\n",
4108                                password
4109                            ));
4110                        }
4111
4112                        code.push_str("        use lettre::Transport;\n");
4113
4114                        if smtp.secure {
4115                            code.push_str(&format!(
4116                                "        let transport = lettre::SmtpTransport::relay(\"{}\")\n",
4117                                host
4118                            ));
4119                            code.push_str("            .map_err(|e| adk_graph::GraphError::NodeExecutionFailed { node: \"email\".into(), message: format!(\"SMTP relay failed: {}\", e) })?\n");
4120                            code.push_str(&format!("            .port({})\n", smtp.port));
4121                            code.push_str(&format!(
4122                                "            .credentials(lettre::transport::smtp::authentication::Credentials::new(\"{}\".to_string(), smtp_password))\n",
4123                                username
4124                            ));
4125                            code.push_str("            .build();\n");
4126                        } else {
4127                            code.push_str(&format!(
4128                                "        let transport = lettre::SmtpTransport::builder_dangerous(\"{}\")\n",
4129                                host
4130                            ));
4131                            code.push_str(&format!("            .port({})\n", smtp.port));
4132                            code.push_str(&format!(
4133                                "            .credentials(lettre::transport::smtp::authentication::Credentials::new(\"{}\".to_string(), smtp_password))\n",
4134                                username
4135                            ));
4136                            code.push_str("            .build();\n");
4137                        }
4138
4139                        // Send the email
4140                        code.push_str("        match transport.send(&email) {\n");
4141                        code.push_str("            Ok(response) => {\n");
4142                        code.push_str(&format!(
4143                            "                Ok(NodeOutput::new().with_update(\"{}\", json!({{\n",
4144                            output_key
4145                        ));
4146                        code.push_str("                    \"sent\": true,\n");
4147                        code.push_str("                    \"message\": format!(\"Email sent successfully: {:?}\", response),\n");
4148                        code.push_str(&format!(
4149                            "                    \"to\": \"{}\",\n",
4150                            recipients.to.replace('"', "\\\"")
4151                        ));
4152                        code.push_str("                    \"subject\": subject\n");
4153                        code.push_str("                })))\n");
4154                        code.push_str("            }\n");
4155                        code.push_str("            Err(e) => {\n");
4156                        code.push_str(&format!(
4157                            "                Ok(NodeOutput::new().with_update(\"{}\", json!({{\n",
4158                            output_key
4159                        ));
4160                        code.push_str("                    \"sent\": false,\n");
4161                        code.push_str("                    \"error\": format!(\"Failed to send email: {}\", e)\n");
4162                        code.push_str("                })))\n");
4163                        code.push_str("            }\n");
4164                        code.push_str("        }\n");
4165                    } else {
4166                        code.push_str(&format!(
4167                            "        Ok(NodeOutput::new().with_update(\"{}\", json!({{ \"error\": true, \"message\": \"Email send mode requires smtp, recipients, and content configuration\" }})))\n",
4168                            output_key
4169                        ));
4170                    }
4171                }
4172                crate::codegen::action_node_types::EmailMode::Monitor => {
4173                    // IMAP monitoring — check for new emails matching filters
4174                    if let Some(imap_cfg) = &config.imap {
4175                        let host = imap_cfg.host.replace('"', "\\\"");
4176                        let username = imap_cfg.username.replace('"', "\\\"");
4177                        let password = imap_cfg.password.replace('"', "\\\"");
4178                        let folder = imap_cfg.folder.replace('"', "\\\"");
4179
4180                        // Password interpolation
4181                        if password.contains("{{") {
4182                            code.push_str(&format!(
4183                                "        let mut imap_password = \"{}\".to_string();\n",
4184                                password
4185                            ));
4186                            code.push_str("        for (k, v) in ctx.state.iter() {\n");
4187                            code.push_str(
4188                                "            let pattern = format!(\"{{{{{}}}}}\", k);\n",
4189                            );
4190                            code.push_str("            if let Some(s) = v.as_str() {\n");
4191                            code.push_str("                imap_password = imap_password.replace(&pattern, s);\n");
4192                            code.push_str("            }\n");
4193                            code.push_str("        }\n");
4194                        } else {
4195                            code.push_str(&format!(
4196                                "        let imap_password = \"{}\".to_string();\n",
4197                                password
4198                            ));
4199                        }
4200
4201                        // Connect to IMAP
4202                        if imap_cfg.secure {
4203                            code.push_str(
4204                                "        let tls = native_tls::TlsConnector::builder().build()\n",
4205                            );
4206                            code.push_str("            .map_err(|e| adk_graph::GraphError::NodeExecutionFailed { node: \"email\".into(), message: format!(\"TLS error: {}\", e) })?;\n");
4207                            code.push_str(&format!(
4208                                "        let client = imap::ClientBuilder::new(\"{}\", {}).native_tls(tls)\n",
4209                                host, imap_cfg.port
4210                            ));
4211                            code.push_str("            .map_err(|e| adk_graph::GraphError::NodeExecutionFailed { node: \"email\".into(), message: format!(\"IMAP connection failed: {}\", e) })?;\n");
4212                        } else {
4213                            code.push_str(&format!(
4214                                "        let client = imap::ClientBuilder::new(\"{}\", {}).connect()\n",
4215                                host, imap_cfg.port
4216                            ));
4217                            code.push_str("            .map_err(|e| adk_graph::GraphError::NodeExecutionFailed { node: \"email\".into(), message: format!(\"IMAP connection failed: {}\", e) })?;\n");
4218                        }
4219
4220                        // Login
4221                        code.push_str(&format!(
4222                            "        let mut session = client.login(\"{}\", &imap_password)\n",
4223                            username
4224                        ));
4225                        code.push_str("            .map_err(|e| adk_graph::GraphError::NodeExecutionFailed { node: \"email\".into(), message: format!(\"IMAP login failed: {:?}\", e.0) })?;\n");
4226
4227                        // Select folder
4228                        code.push_str(&format!("        session.select(\"{}\")\n", folder));
4229                        code.push_str("            .map_err(|e| adk_graph::GraphError::NodeExecutionFailed { node: \"email\".into(), message: format!(\"Folder select failed: {}\", e) })?;\n");
4230
4231                        // Build search query from filters
4232                        let mut search_criteria = Vec::new();
4233                        if let Some(ref filters) = config.filters {
4234                            if filters.unread_only {
4235                                search_criteria.push("UNSEEN".to_string());
4236                            }
4237                            if let Some(ref from) = filters.from {
4238                                if !from.is_empty() {
4239                                    search_criteria.push(format!(
4240                                        "FROM \\\"{}\\\"",
4241                                        from.replace('"', "\\\\\\\"")
4242                                    ));
4243                                }
4244                            }
4245                            if let Some(ref subject) = filters.subject {
4246                                if !subject.is_empty() {
4247                                    search_criteria.push(format!(
4248                                        "SUBJECT \\\"{}\\\"",
4249                                        subject.replace('"', "\\\\\\\"")
4250                                    ));
4251                                }
4252                            }
4253                        }
4254                        let search_str = if search_criteria.is_empty() {
4255                            "ALL".to_string()
4256                        } else {
4257                            search_criteria.join(" ")
4258                        };
4259
4260                        code.push_str(&format!(
4261                            "        let messages = session.search(\"{}\")\n",
4262                            search_str
4263                        ));
4264                        code.push_str("            .map_err(|e| adk_graph::GraphError::NodeExecutionFailed { node: \"email\".into(), message: format!(\"IMAP search failed: {}\", e) })?;\n");
4265
4266                        // Fetch message details
4267                        code.push_str(
4268                            "        let mut emails: Vec<serde_json::Value> = Vec::new();\n",
4269                        );
4270                        code.push_str("        if !messages.is_empty() {\n");
4271                        code.push_str("            let seq_set: Vec<String> = messages.iter().map(|id| id.to_string()).collect();\n");
4272                        code.push_str("            let fetch_range = seq_set.join(\",\");\n");
4273                        code.push_str("            if let Ok(fetched) = session.fetch(&fetch_range, \"(RFC822.HEADER BODY[TEXT])\") {\n");
4274                        code.push_str("                for msg in fetched.iter() {\n");
4275                        code.push_str("                    let header = msg.header().map(|h| String::from_utf8_lossy(h).to_string()).unwrap_or_default();\n");
4276                        code.push_str("                    let body_text = msg.text().map(|b| String::from_utf8_lossy(b).to_string()).unwrap_or_default();\n");
4277                        code.push_str("                    // Parse basic headers\n");
4278                        code.push_str("                    let from_line = header.lines().find(|l| l.starts_with(\"From:\")).map(|l| l[5..].trim().to_string()).unwrap_or_default();\n");
4279                        code.push_str("                    let subject_line = header.lines().find(|l| l.starts_with(\"Subject:\")).map(|l| l[8..].trim().to_string()).unwrap_or_default();\n");
4280                        code.push_str("                    let date_line = header.lines().find(|l| l.starts_with(\"Date:\")).map(|l| l[5..].trim().to_string()).unwrap_or_default();\n");
4281                        code.push_str("                    emails.push(json!({\n");
4282                        code.push_str("                        \"from\": from_line,\n");
4283                        code.push_str("                        \"subject\": subject_line,\n");
4284                        code.push_str("                        \"date\": date_line,\n");
4285                        code.push_str("                        \"body\": body_text,\n");
4286                        code.push_str("                        \"uid\": msg.message\n");
4287                        code.push_str("                    }));\n");
4288                        code.push_str("                }\n");
4289                        code.push_str("            }\n");
4290
4291                        // Mark as read if configured
4292                        if imap_cfg.mark_as_read {
4293                            code.push_str("            // Mark fetched messages as read\n");
4294                            code.push_str("            let _ = session.store(&fetch_range, \"+FLAGS (\\\\Seen)\");\n");
4295                        }
4296
4297                        code.push_str("        }\n");
4298                        code.push_str("        let _ = session.logout();\n");
4299
4300                        code.push_str("        let email_count = emails.len();\n");
4301                        code.push_str(&format!(
4302                            "        Ok(NodeOutput::new().with_update(\"{}\", json!({{ \"emails\": emails, \"count\": email_count }})))\n",
4303                            output_key
4304                        ));
4305                    } else {
4306                        code.push_str(&format!(
4307                            "        Ok(NodeOutput::new().with_update(\"{}\", json!({{ \"error\": true, \"message\": \"Email monitor mode requires IMAP configuration\" }})))\n",
4308                            output_key
4309                        ));
4310                    }
4311                }
4312            }
4313
4314            code.push_str("    });\n\n");
4315        }
4316        ActionNodeConfig::Code(config) => {
4317            code.push_str(&format!(
4318                "    // Action Node: {} (Code - {:?})\n",
4319                config.standard.name, config.language
4320            ));
4321            code.push_str(&format!("    let {}_node = adk_graph::node::FunctionNode::new(\"{}\", |ctx| async move {{\n", node_id, node_id));
4322
4323            let output_key = if config.standard.mapping.output_key.is_empty() {
4324                "codeResult"
4325            } else {
4326                &config.standard.mapping.output_key
4327            };
4328
4329            if config.code.is_empty() {
4330                code.push_str(&format!(
4331                    "        Ok(NodeOutput::new().with_update(\"{}\", json!({{ \"error\": true, \"message\": \"No code provided\" }})))\n",
4332                    output_key
4333                ));
4334            } else {
4335                match config.language {
4336                    crate::codegen::action_node_types::CodeLanguage::Rust => {
4337                        // Rust-first: embed the authored Rust body directly.
4338                        // The user's code is expected to follow the contract:
4339                        //   fn run(input: serde_json::Value) -> serde_json::Value
4340                        // We wrap it in a module-level function and call it inline.
4341                        code.push_str(
4342                            "        // Rust-first: execute authored Rust body directly\n",
4343                        );
4344                        code.push_str("        let mut input_obj = serde_json::Map::new();\n");
4345                        code.push_str("        for (k, v) in ctx.state.iter() {\n");
4346                        code.push_str("            input_obj.insert(k.clone(), v.clone());\n");
4347                        code.push_str("        }\n");
4348                        code.push_str(
4349                            "        let input = serde_json::Value::Object(input_obj);\n\n",
4350                        );
4351                        code.push_str(&format!("        let result = {}_run(input);\n", node_id));
4352                        code.push_str(&format!(
4353                            "        Ok(NodeOutput::new().with_update(\"{}\", result))\n",
4354                            output_key
4355                        ));
4356                    }
4357                    _ => {
4358                        // JS/TS: use boa_engine as secondary scripting support
4359                        code.push_str(
4360                            "        // Secondary scripting: execute JS/TS via boa_engine\n",
4361                        );
4362                        code.push_str("        let mut input_obj = serde_json::Map::new();\n");
4363                        code.push_str("        for (k, v) in ctx.state.iter() {\n");
4364                        code.push_str("            input_obj.insert(k.clone(), v.clone());\n");
4365                        code.push_str("        }\n");
4366                        code.push_str(
4367                            "        let input_json = serde_json::Value::Object(input_obj);\n\n",
4368                        );
4369
4370                        let escaped_code = config.code.replace('\\', "\\\\");
4371                        let escaped_code = escaped_code.replace('"', "\\\"");
4372                        let escaped_code = escaped_code.replace('\n', "\\n");
4373                        let escaped_code = escaped_code.replace('\r', "");
4374                        let escaped_code = escaped_code.replace('\t', "\\t");
4375
4376                        code.push_str("        let js_result = std::thread::spawn(move || -> Result<serde_json::Value, String> {\n");
4377                        code.push_str(
4378                            "            use boa_engine::{Context, Source, JsValue, JsError};\n",
4379                        );
4380                        code.push_str("            use std::time::Instant;\n\n");
4381
4382                        let time_limit = config.sandbox.time_limit;
4383                        code.push_str(&format!(
4384                            "            let time_limit = std::time::Duration::from_millis({});\n",
4385                            time_limit
4386                        ));
4387                        code.push_str("            let start = Instant::now();\n\n");
4388                        code.push_str("            let mut context = Context::default();\n\n");
4389                        code.push_str("            let input_str = serde_json::to_string(&input_json).unwrap_or_else(|_| \"{}\".to_string());\n");
4390                        code.push_str(
4391                            "            let setup_code = format!(\"var input = {};\", input_str);\n",
4392                        );
4393                        code.push_str(
4394                            "            context.eval(Source::from_bytes(&setup_code))\n",
4395                        );
4396                        code.push_str("                .map_err(|e| format!(\"Failed to inject input: {:?}\", e))?;\n\n");
4397
4398                        code.push_str(&format!(
4399                            "            let user_code = \"{}\";\n",
4400                            escaped_code
4401                        ));
4402                        code.push_str(
4403                            "            let wrapped = format!(\"(function() {{ {} }})()\", user_code);\n",
4404                        );
4405                        code.push_str(
4406                            "            let result = context.eval(Source::from_bytes(&wrapped));\n\n",
4407                        );
4408
4409                        code.push_str("            if start.elapsed() > time_limit {\n");
4410                        code.push_str("                return Err(format!(\"Code execution exceeded time limit of {}ms\", time_limit.as_millis()));\n");
4411                        code.push_str("            }\n\n");
4412
4413                        code.push_str("            match result {\n");
4414                        code.push_str("                Ok(val) => {\n");
4415                        code.push_str(
4416                            "                    let json_val = js_value_to_json(&val, &mut context);\n",
4417                        );
4418                        code.push_str("                    Ok(json_val)\n");
4419                        code.push_str("                }\n");
4420                        code.push_str("                Err(e) => {\n");
4421                        code.push_str(
4422                            "                    Err(format!(\"JavaScript error: {:?}\", e))\n",
4423                        );
4424                        code.push_str("                }\n");
4425                        code.push_str("            }\n");
4426                        code.push_str("        }).join().unwrap_or_else(|_| Err(\"Code execution panicked\".to_string()));\n\n");
4427
4428                        code.push_str("        match js_result {\n");
4429                        code.push_str("            Ok(value) => {\n");
4430                        code.push_str(&format!(
4431                            "                Ok(NodeOutput::new().with_update(\"{}\", value))\n",
4432                            output_key
4433                        ));
4434                        code.push_str("            }\n");
4435                        code.push_str("            Err(err) => {\n");
4436                        code.push_str(&format!(
4437                            "                Ok(NodeOutput::new().with_update(\"{}\", json!({{ \"error\": true, \"message\": err }})))\n",
4438                            output_key
4439                        ));
4440                        code.push_str("            }\n");
4441                        code.push_str("        }\n");
4442                    }
4443                }
4444            }
4445
4446            code.push_str("    });\n\n");
4447
4448            // For Rust code nodes, emit the authored function at module level
4449            if !config.code.is_empty()
4450                && matches!(
4451                    config.language,
4452                    crate::codegen::action_node_types::CodeLanguage::Rust
4453                )
4454            {
4455                code.push_str(&format!(
4456                    "// Authored Rust body for Code node \"{}\"\n",
4457                    config.standard.name
4458                ));
4459                code.push_str(&format!(
4460                    "fn {}_run(input: serde_json::Value) -> serde_json::Value {{\n",
4461                    node_id
4462                ));
4463                code.push_str(&config.code);
4464                code.push_str("\n}\n\n");
4465            }
4466        }
4467        // Other action node types can be added here
4468        _ => {
4469            // For unsupported action nodes, generate a pass-through node
4470            let standard = node.standard();
4471            code.push_str(&format!(
4472                "    // Action Node: {} ({})\n",
4473                standard.name,
4474                node.node_type()
4475            ));
4476            code.push_str(&format!("    let {}_node = adk_graph::node::FunctionNode::new(\"{}\", |_ctx| async move {{\n", node_id, node_id));
4477            code.push_str("        Ok(NodeOutput::new())\n");
4478            code.push_str("    });\n\n");
4479        }
4480    }
4481
4482    code
4483}
4484
4485fn generate_cargo_toml(project: &ProjectSchema) -> String {
4486    let mut name = project
4487        .name
4488        .to_lowercase()
4489        .replace(' ', "_")
4490        .replace(|c: char| !c.is_alphanumeric() && c != '_', "");
4491    // Cargo package names can't start with a digit
4492    if name.is_empty()
4493        || name
4494            .chars()
4495            .next()
4496            .map(|c| c.is_ascii_digit())
4497            .unwrap_or(false)
4498    {
4499        name = format!("project_{}", name);
4500    }
4501
4502    // Get ADK version and Rust edition from project settings (with defaults)
4503    let adk_version = project
4504        .settings
4505        .adk_version
4506        .as_deref()
4507        .unwrap_or(DEFAULT_ADK_VERSION);
4508    let rust_edition = project.settings.rust_edition.as_deref().unwrap_or("2024");
4509
4510    // Check if any function tool code uses specific crates
4511    let code_uses = |pattern: &str| -> bool {
4512        project.tool_configs.values().any(|tc| {
4513            if let ToolConfig::Function(fc) = tc {
4514                fc.code.contains(pattern)
4515            } else {
4516                false
4517            }
4518        })
4519    };
4520
4521    let needs_reqwest = code_uses("reqwest::") || {
4522        use crate::codegen::action_nodes::ActionNodeConfig;
4523        project
4524            .action_nodes
4525            .values()
4526            .any(|n| matches!(n, ActionNodeConfig::Http(_)))
4527    };
4528    let needs_lettre = code_uses("lettre::");
4529    let needs_base64 = code_uses("base64::");
4530    let needs_imap = code_uses("imap::");
4531    let needs_native_tls = code_uses("native_tls::");
4532    let needs_boa = {
4533        use crate::codegen::action_node_types::CodeLanguage;
4534        use crate::codegen::action_nodes::ActionNodeConfig;
4535        project.action_nodes.values().any(|n| {
4536            matches!(
4537                n,
4538                ActionNodeConfig::Code(cfg) if matches!(cfg.language, CodeLanguage::Javascript | CodeLanguage::Typescript)
4539            )
4540        })
4541    };
4542
4543    // Database dependencies based on action node db_type
4544    let (needs_sqlx_pg, needs_sqlx_mysql, needs_sqlx_sqlite, needs_mongodb, needs_redis) = {
4545        use crate::codegen::action_node_types::DatabaseType;
4546        use crate::codegen::action_nodes::ActionNodeConfig;
4547        let mut pg = false;
4548        let mut mysql = false;
4549        let mut sqlite = false;
4550        let mut mongo = false;
4551        let mut redis = false;
4552        for node in project.action_nodes.values() {
4553            if let ActionNodeConfig::Database(cfg) = node {
4554                match cfg.db_type {
4555                    DatabaseType::Postgresql => pg = true,
4556                    DatabaseType::Mysql => mysql = true,
4557                    DatabaseType::Sqlite => sqlite = true,
4558                    DatabaseType::Mongodb => mongo = true,
4559                    DatabaseType::Redis => redis = true,
4560                }
4561            }
4562        }
4563        (pg, mysql, sqlite, mongo, redis)
4564    };
4565
4566    // Determine which adk-model features are needed based on providers used
4567    let providers = collect_providers(project);
4568    let mut model_features: Vec<&str> = Vec::new();
4569    if providers.contains("gemini") {
4570        model_features.push("gemini");
4571    }
4572    if providers.contains("openai") {
4573        model_features.push("openai");
4574    }
4575    if providers.contains("anthropic") {
4576        model_features.push("anthropic");
4577    }
4578    if providers.contains("deepseek") {
4579        model_features.push("deepseek");
4580    }
4581    if providers.contains("groq") {
4582        model_features.push("groq");
4583    }
4584    if providers.contains("ollama") {
4585        model_features.push("ollama");
4586    }
4587    if providers.contains("fireworks") {
4588        model_features.push("fireworks");
4589    }
4590    if providers.contains("together") {
4591        model_features.push("together");
4592    }
4593    if providers.contains("mistral") {
4594        model_features.push("mistral");
4595    }
4596    if providers.contains("perplexity") {
4597        model_features.push("perplexity");
4598    }
4599    if providers.contains("cerebras") {
4600        model_features.push("cerebras");
4601    }
4602    if providers.contains("sambanova") {
4603        model_features.push("sambanova");
4604    }
4605    if providers.contains("bedrock") {
4606        model_features.push("bedrock");
4607    }
4608    if providers.contains("azure-ai") {
4609        model_features.push("azure-ai");
4610    }
4611    // Default to gemini if no providers detected
4612    if model_features.is_empty() {
4613        model_features.push("gemini");
4614    }
4615    let features_str = model_features
4616        .iter()
4617        .map(|f| format!("\"{}\"", f))
4618        .collect::<Vec<_>>()
4619        .join(", ");
4620
4621    // Always use crates.io published dependencies (0.5.0+)
4622    let has_action_nodes = !project.action_nodes.is_empty();
4623    let graph_features_str = if has_action_nodes {
4624        ", features = [\"action-full\"]"
4625    } else {
4626        ""
4627    };
4628    let action_dep = if has_action_nodes {
4629        format!("\nadk-action = \"{}\"", adk_version)
4630    } else {
4631        String::new()
4632    };
4633
4634    let adk_deps = format!(
4635        r#"adk-agent = "{}"
4636adk-core = "{}"
4637adk-model = {{ version = "{}", default-features = false, features = [{}] }}
4638adk-tool = "{}"
4639adk-graph = {{ version = "{}"{} }}{}"#,
4640        adk_version, adk_version, adk_version, features_str, adk_version, adk_version, graph_features_str, action_dep
4641    );
4642
4643    let mut deps = format!(
4644        r#"[package]
4645name = "{}"
4646version = "0.1.0"
4647edition = "{}"
4648
4649[dependencies]
4650{}
4651tokio = {{ version = "1", features = ["full", "macros"] }}
4652tokio-stream = "0.1"
4653anyhow = "1"
4654serde = {{ version = "1", features = ["derive"] }}
4655serde_json = "1"
4656schemars = "0.8"
4657tracing-subscriber = {{ version = "0.3", features = ["json", "env-filter"] }}
4658uuid = {{ version = "1", features = ["v4"] }}
4659"#,
4660        name, rust_edition, adk_deps
4661    );
4662
4663    if needs_reqwest {
4664        deps.push_str("reqwest = { version = \"0.12\", features = [\"json\"] }\n");
4665    }
4666    if needs_lettre {
4667        deps.push_str("lettre = \"0.11\"\n");
4668    }
4669    if needs_base64 {
4670        deps.push_str("base64 = \"0.21\"\n");
4671    }
4672    if needs_imap {
4673        deps.push_str("imap = \"3\"\n");
4674    }
4675    if needs_native_tls {
4676        deps.push_str("native-tls = \"0.2\"\n");
4677    }
4678    if needs_boa {
4679        deps.push_str("boa_engine = { version = \"0.20\", features = [\"annex-b\"] }\n");
4680    }
4681
4682    // Database dependencies
4683    let needs_sqlx = needs_sqlx_pg || needs_sqlx_mysql || needs_sqlx_sqlite;
4684    if needs_sqlx {
4685        let mut sqlx_features = vec!["runtime-tokio"];
4686        if needs_sqlx_pg {
4687            sqlx_features.push("postgres");
4688        }
4689        if needs_sqlx_mysql {
4690            sqlx_features.push("mysql");
4691        }
4692        if needs_sqlx_sqlite {
4693            sqlx_features.push("sqlite");
4694        }
4695        let features_str = sqlx_features
4696            .iter()
4697            .map(|f| format!("\"{}\"", f))
4698            .collect::<Vec<_>>()
4699            .join(", ");
4700        deps.push_str(&format!(
4701            "sqlx = {{ version = \"0.8\", features = [{}] }}\n",
4702            features_str
4703        ));
4704    }
4705    if needs_mongodb {
4706        deps.push_str("mongodb = \"3\"\nfutures = \"0.3\"\n");
4707    }
4708    if needs_redis {
4709        deps.push_str("redis = { version = \"0.27\", features = [\"tokio-comp\"] }\n");
4710    }
4711
4712    // Add rmcp if any agent uses MCP (handles mcp, mcp_1, mcp_2, etc.)
4713    let uses_mcp = project
4714        .agents
4715        .values()
4716        .any(|a| a.tools.iter().any(|t| t == "mcp" || t.starts_with("mcp_")));
4717    if uses_mcp {
4718        deps.push_str(
4719            "rmcp = { version = \"1.3\", features = [\"client\", \"transport-child-process\"] }\n",
4720        );
4721        deps.push_str("async-trait = \"0.1\"\n");
4722    }
4723
4724    // Add adk-browser if any agent uses browser tool
4725    let uses_browser = project
4726        .agents
4727        .values()
4728        .any(|a| a.tools.contains(&"browser".to_string()));
4729    if uses_browser {
4730        deps.push_str(&format!("adk-browser = \"{}\"\n", adk_version));
4731        // async-trait needed for MinimalContext if not already added by MCP
4732        if !uses_mcp {
4733            deps.push_str("async-trait = \"0.1\"\n");
4734        }
4735    }
4736
4737    deps
4738}