adk_studio/codegen/
mod.rs

1//! Rust code generation from project schemas - Always uses adk-graph
2
3use crate::schema::{AgentSchema, AgentType, ProjectSchema, ToolConfig};
4use anyhow::Result;
5
6pub fn generate_rust_project(project: &ProjectSchema) -> Result<GeneratedProject> {
7    let files = vec![
8        GeneratedFile { path: "src/main.rs".to_string(), content: generate_main_rs(project) },
9        GeneratedFile { path: "Cargo.toml".to_string(), content: generate_cargo_toml(project) },
10    ];
11
12    Ok(GeneratedProject { files })
13}
14
15#[derive(Debug, serde::Serialize)]
16pub struct GeneratedProject {
17    pub files: Vec<GeneratedFile>,
18}
19
20#[derive(Debug, serde::Serialize)]
21pub struct GeneratedFile {
22    pub path: String,
23    pub content: String,
24}
25
26fn generate_main_rs(project: &ProjectSchema) -> String {
27    let mut code = String::new();
28
29    code.push_str("#![allow(unused_imports, unused_variables)]\n\n");
30
31    // Check if any agent uses MCP (handles mcp, mcp_1, mcp_2, etc.)
32    let uses_mcp = project
33        .agents
34        .values()
35        .any(|a| a.tools.iter().any(|t| t == "mcp" || t.starts_with("mcp_")));
36    let uses_browser = project.agents.values().any(|a| a.tools.contains(&"browser".to_string()));
37
38    // Graph imports
39    code.push_str("use adk_agent::LlmAgentBuilder;\n");
40    code.push_str("use adk_core::ToolContext;\n");
41    code.push_str("use adk_graph::{\n");
42    code.push_str("    edge::{Router, END, START},\n");
43    code.push_str("    graph::StateGraph,\n");
44    code.push_str("    node::{AgentNode, ExecutionConfig, NodeOutput},\n");
45    code.push_str("    state::State,\n");
46    code.push_str("    StreamEvent,\n");
47    code.push_str("};\n");
48    code.push_str("use adk_model::gemini::GeminiModel;\n");
49    code.push_str(
50        "use adk_tool::{FunctionTool, GoogleSearchTool, ExitLoopTool, LoadArtifactsTool};\n",
51    );
52    if uses_mcp || uses_browser {
53        code.push_str("use adk_core::{ReadonlyContext, Toolset, Content};\n");
54    }
55    if uses_mcp {
56        code.push_str("use adk_tool::McpToolset;\n");
57        code.push_str("use rmcp::{ServiceExt, transport::TokioChildProcess};\n");
58        code.push_str("use tokio::process::Command;\n");
59    }
60    if uses_mcp || uses_browser {
61        code.push_str("use async_trait::async_trait;\n");
62    }
63    if uses_browser {
64        code.push_str("use adk_browser::{BrowserSession, BrowserConfig, BrowserToolset};\n");
65    }
66    code.push_str("use anyhow::Result;\n");
67    code.push_str("use serde_json::{json, Value};\n");
68    code.push_str("use std::sync::Arc;\n");
69    code.push_str("use tracing_subscriber::{fmt, EnvFilter};\n\n");
70
71    // Add MinimalContext for MCP/browser toolset initialization
72    if uses_mcp || uses_browser {
73        code.push_str("// Minimal context for toolset initialization\n");
74        code.push_str("struct MinimalContext { content: Content }\n");
75        code.push_str("impl MinimalContext { fn new() -> Self { Self { content: Content { role: String::new(), parts: vec![] } } } }\n");
76        code.push_str("#[async_trait]\n");
77        code.push_str("impl ReadonlyContext for MinimalContext {\n");
78        code.push_str("    fn invocation_id(&self) -> &str { \"init\" }\n");
79        code.push_str("    fn agent_name(&self) -> &str { \"init\" }\n");
80        code.push_str("    fn user_id(&self) -> &str { \"init\" }\n");
81        code.push_str("    fn app_name(&self) -> &str { \"init\" }\n");
82        code.push_str("    fn session_id(&self) -> &str { \"init\" }\n");
83        code.push_str("    fn branch(&self) -> &str { \"main\" }\n");
84        code.push_str("    fn user_content(&self) -> &Content { &self.content }\n");
85        code.push_str("}\n\n");
86    }
87
88    // Generate function tools with parameter schemas
89    for (agent_id, agent) in &project.agents {
90        for tool_type in &agent.tools {
91            if tool_type.starts_with("function") {
92                let tool_id = format!("{}_{}", agent_id, tool_type);
93                if let Some(ToolConfig::Function(config)) = project.tool_configs.get(&tool_id) {
94                    code.push_str(&generate_function_schema(config));
95                    code.push_str(&generate_function_tool(config));
96                }
97            }
98        }
99    }
100
101    code.push_str("#[tokio::main]\n");
102    code.push_str("async fn main() -> Result<()> {\n");
103    // Initialize tracing with JSON output
104    code.push_str("    // Initialize tracing\n");
105    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");
106    code.push_str("    let api_key = std::env::var(\"GOOGLE_API_KEY\")\n");
107    code.push_str("        .or_else(|_| std::env::var(\"GEMINI_API_KEY\"))\n");
108    code.push_str("        .expect(\"GOOGLE_API_KEY or GEMINI_API_KEY must be set\");\n\n");
109
110    // Initialize browser session if any agent uses browser
111    let uses_browser = project.agents.values().any(|a| a.tools.contains(&"browser".to_string()));
112    if uses_browser {
113        code.push_str("    // Initialize browser session\n");
114        code.push_str("    let browser_config = BrowserConfig::new().headless(true);\n");
115        code.push_str("    let browser = Arc::new(BrowserSession::new(browser_config));\n");
116        code.push_str("    browser.start().await?;\n");
117        code.push_str("    let browser_toolset = BrowserToolset::new(browser.clone());\n\n");
118    }
119
120    // Find top-level agents (not sub-agents of containers)
121    let all_sub_agents: std::collections::HashSet<_> =
122        project.agents.values().flat_map(|a| a.sub_agents.iter().cloned()).collect();
123    let top_level: Vec<_> =
124        project.agents.keys().filter(|id| !all_sub_agents.contains(*id)).collect();
125
126    // Find first agent (connected from START)
127    let first_agent: Option<&str> =
128        project.workflow.edges.iter().find(|e| e.from == "START").map(|e| e.to.as_str());
129
130    // Generate all agent nodes
131    for agent_id in &top_level {
132        if let Some(agent) = project.agents.get(*agent_id) {
133            let is_first = first_agent == Some(agent_id.as_str());
134            match agent.agent_type {
135                AgentType::Router => {
136                    code.push_str(&generate_router_node(agent_id, agent));
137                }
138                AgentType::Llm => {
139                    code.push_str(&generate_llm_node(agent_id, agent, project, is_first));
140                }
141                _ => {
142                    // Sequential/Loop/Parallel - generate as single node wrapping container
143                    code.push_str(&generate_container_node(agent_id, agent, project));
144                }
145            }
146        }
147    }
148
149    // Build graph
150    code.push_str("    // Build the graph\n");
151    code.push_str("    let graph = StateGraph::with_channels(&[\"message\", \"classification\", \"response\"])\n");
152
153    // Add all nodes
154    for agent_id in &top_level {
155        code.push_str(&format!("        .add_node({}_node)\n", agent_id));
156    }
157
158    // Add edges from workflow
159    for edge in &project.workflow.edges {
160        let from =
161            if edge.from == "START" { "START".to_string() } else { format!("\"{}\"", edge.from) };
162        let to = if edge.to == "END" { "END".to_string() } else { format!("\"{}\"", edge.to) };
163
164        // Check if source is a router - use conditional edges
165        if let Some(agent) = project.agents.get(&edge.from) {
166            if agent.agent_type == AgentType::Router && !agent.routes.is_empty() {
167                // Generate conditional edges for router
168                let conditions: Vec<String> = agent
169                    .routes
170                    .iter()
171                    .map(|r| {
172                        let target = if r.target == "END" {
173                            "END".to_string()
174                        } else {
175                            format!("\"{}\"", r.target)
176                        };
177                        format!("(\"{}\", {})", r.condition, target)
178                    })
179                    .collect();
180
181                code.push_str("        .add_conditional_edges(\n");
182                code.push_str(&format!("            \"{}\",\n", edge.from));
183                code.push_str("            Router::by_field(\"classification\"),\n");
184                code.push_str(&format!("            [{}],\n", conditions.join(", ")));
185                code.push_str("        )\n");
186                continue;
187            }
188        }
189
190        code.push_str(&format!("        .add_edge({}, {})\n", from, to));
191    }
192
193    code.push_str("        .compile()?;\n\n");
194
195    // Interactive loop with streaming and conversation memory
196    code.push_str("    // Get session ID from args or generate new one\n");
197    code.push_str("    let session_id = std::env::args().nth(1).unwrap_or_else(|| uuid::Uuid::new_v4().to_string());\n");
198    code.push_str("    println!(\"SESSION:{}\", session_id);\n\n");
199    code.push_str("    // Conversation history for memory\n");
200    code.push_str("    let mut history: Vec<(String, String)> = Vec::new();\n\n");
201    code.push_str("    // Interactive loop\n");
202    code.push_str(
203        "    println!(\"Graph workflow ready. Type your message (or 'quit' to exit):\");\n",
204    );
205    code.push_str("    let stdin = std::io::stdin();\n");
206    code.push_str("    let mut input = String::new();\n");
207    code.push_str("    let mut turn = 0;\n");
208    code.push_str("    loop {\n");
209    code.push_str("        input.clear();\n");
210    code.push_str("        print!(\"> \");\n");
211    code.push_str("        use std::io::Write;\n");
212    code.push_str("        std::io::stdout().flush()?;\n");
213    code.push_str("        stdin.read_line(&mut input)?;\n");
214    code.push_str("        let msg = input.trim();\n");
215    code.push_str("        if msg.is_empty() || msg == \"quit\" { break; }\n\n");
216    code.push_str("        // Build message with conversation history\n");
217    code.push_str("        let context = if history.is_empty() {\n");
218    code.push_str("            msg.to_string()\n");
219    code.push_str("        } else {\n");
220    code.push_str("            let hist: String = history.iter().map(|(u, a)| format!(\"User: {}\\nAssistant: {}\\n\", u, a)).collect();\n");
221    code.push_str("            format!(\"{}\\nUser: {}\", hist, msg)\n");
222    code.push_str("        };\n\n");
223    code.push_str("        let mut state = State::new();\n");
224    code.push_str("        state.insert(\"message\".to_string(), json!(context));\n");
225    code.push_str("        \n");
226    code.push_str("        use adk_graph::StreamMode;\n");
227    code.push_str("        use tokio_stream::StreamExt;\n");
228    code.push_str("        let stream = graph.stream(state, ExecutionConfig::new(&format!(\"{}-turn-{}\", session_id, turn)), StreamMode::Messages);\n");
229    code.push_str("        tokio::pin!(stream);\n");
230    code.push_str("        let mut final_response = String::new();\n");
231    code.push_str("        \n");
232    code.push_str("        while let Some(event) = stream.next().await {\n");
233    code.push_str("            match event {\n");
234    code.push_str("                Ok(e) => {\n");
235    code.push_str("                    // Stream Message events as chunks\n");
236    code.push_str(
237        "                    if let adk_graph::StreamEvent::Message { content, .. } = &e {\n",
238    );
239    code.push_str("                        final_response.push_str(content);\n");
240    code.push_str("                        println!(\"CHUNK:{}\", serde_json::to_string(&final_response).unwrap_or_default());\n");
241    code.push_str("                    }\n");
242    code.push_str("                    // Output trace event as JSON\n");
243    code.push_str("                    if let Ok(json) = serde_json::to_string(&e) {\n");
244    code.push_str("                        println!(\"TRACE:{}\", json);\n");
245    code.push_str("                    }\n");
246    code.push_str("                    // Capture final response from Done event\n");
247    code.push_str("                    if let adk_graph::StreamEvent::Done { state, .. } = &e {\n");
248    code.push_str("                        if let Some(resp) = state.get(\"response\").and_then(|v| v.as_str()) {\n");
249    code.push_str("                            final_response = resp.to_string();\n");
250    code.push_str("                        }\n");
251    code.push_str("                    }\n");
252    code.push_str("                }\n");
253    code.push_str("                Err(e) => eprintln!(\"Error: {}\", e),\n");
254    code.push_str("            }\n");
255    code.push_str("        }\n");
256    code.push_str("        turn += 1;\n\n");
257    code.push_str("        // Save to history\n");
258    code.push_str("        if !final_response.is_empty() {\n");
259    code.push_str("            history.push((msg.to_string(), final_response.clone()));\n");
260    code.push_str("            println!(\"RESPONSE:{}\", serde_json::to_string(&final_response).unwrap_or_default());\n");
261    code.push_str("        }\n");
262    code.push_str("    }\n\n");
263
264    code.push_str("    Ok(())\n");
265    code.push_str("}\n");
266
267    code
268}
269
270fn generate_router_node(id: &str, agent: &AgentSchema) -> String {
271    let mut code = String::new();
272    let model = agent.model.as_deref().unwrap_or("gemini-2.0-flash");
273
274    code.push_str(&format!("    // Router: {}\n", id));
275    code.push_str(&format!("    let {}_llm = Arc::new(\n", id));
276    code.push_str(&format!("        LlmAgentBuilder::new(\"{}\")\n", id));
277    code.push_str(&format!(
278        "            .model(Arc::new(GeminiModel::new(&api_key, \"{}\")?))\n",
279        model
280    ));
281
282    let route_options: Vec<&str> = agent.routes.iter().map(|r| r.condition.as_str()).collect();
283    let instruction = if agent.instruction.is_empty() {
284        format!(
285            "Classify the input into one of: {}. Respond with ONLY the category name.",
286            route_options.join(", ")
287        )
288    } else {
289        agent.instruction.clone()
290    };
291    let escaped = instruction.replace('\\', "\\\\").replace('"', "\\\"").replace('\n', "\\n");
292    code.push_str(&format!("            .instruction(\"{}\")\n", escaped));
293    code.push_str("            .build()?\n");
294    code.push_str("    );\n\n");
295
296    code.push_str(&format!("    let {}_node = AgentNode::new({}_llm)\n", id, id));
297    code.push_str("        .with_input_mapper(|state| {\n");
298    code.push_str(
299        "            let msg = state.get(\"message\").and_then(|v| v.as_str()).unwrap_or(\"\");\n",
300    );
301    code.push_str("            adk_core::Content::new(\"user\").with_text(msg.to_string())\n");
302    code.push_str("        })\n");
303    code.push_str("        .with_output_mapper(|events| {\n");
304    code.push_str("            let mut updates = std::collections::HashMap::new();\n");
305    code.push_str("            for event in events {\n");
306    code.push_str("                if let Some(content) = event.content() {\n");
307    code.push_str("                    let text: String = content.parts.iter()\n");
308    code.push_str("                        .filter_map(|p| p.text())\n");
309    code.push_str("                        .collect::<Vec<_>>().join(\"\").to_lowercase();\n");
310
311    for (i, route) in agent.routes.iter().enumerate() {
312        let cond = if i == 0 { "if" } else { "else if" };
313        code.push_str(&format!(
314            "                    {} text.contains(\"{}\") {{\n",
315            cond,
316            route.condition.to_lowercase()
317        ));
318        code.push_str(&format!("                        updates.insert(\"classification\".to_string(), json!(\"{}\"));\n", route.condition));
319        code.push_str("                    }\n");
320    }
321    if let Some(first) = agent.routes.first() {
322        code.push_str(&format!("                    else {{ updates.insert(\"classification\".to_string(), json!(\"{}\")); }}\n", first.condition));
323    }
324
325    code.push_str("                }\n");
326    code.push_str("            }\n");
327    code.push_str("            updates\n");
328    code.push_str("        });\n\n");
329
330    code
331}
332
333fn generate_llm_node(
334    id: &str,
335    agent: &AgentSchema,
336    project: &ProjectSchema,
337    is_first: bool,
338) -> String {
339    let mut code = String::new();
340    let model = agent.model.as_deref().unwrap_or("gemini-2.0-flash");
341
342    code.push_str(&format!("    // Agent: {}\n", id));
343
344    // Generate MCP toolsets for all MCP tools (mcp, mcp_1, mcp_2, etc.)
345    let mcp_tools: Vec<_> =
346        agent.tools.iter().filter(|t| *t == "mcp" || t.starts_with("mcp_")).collect();
347
348    for (idx, mcp_tool) in mcp_tools.iter().enumerate() {
349        let tool_id = format!("{}_{}", id, mcp_tool);
350        if let Some(ToolConfig::Mcp(config)) = project.tool_configs.get(&tool_id) {
351            let cmd = &config.server_command;
352            let var_suffix = if idx == 0 { "mcp".to_string() } else { format!("mcp_{}", idx + 1) };
353            // Build Command separately (arg() returns &mut, but TokioChildProcess needs owned)
354            code.push_str(&format!(
355                "    let mut {}_{}_cmd = Command::new(\"{}\");\n",
356                id, var_suffix, cmd
357            ));
358            for arg in &config.server_args {
359                code.push_str(&format!("    {}_{}_cmd.arg(\"{}\");\n", id, var_suffix, arg));
360            }
361            // Add timeout for MCP server initialization
362            code.push_str(&format!(
363                "    let {}_{}_client = tokio::time::timeout(\n",
364                id, var_suffix
365            ));
366            code.push_str("        std::time::Duration::from_secs(10),\n");
367            code.push_str(&format!(
368                "        ().serve(TokioChildProcess::new({}_{}_cmd)?)\n",
369                id, var_suffix
370            ));
371            code.push_str(&format!("    ).await.map_err(|_| anyhow::anyhow!(\"MCP server '{}' failed to start within 10s\"))??;\n", cmd));
372            code.push_str(&format!(
373                "    let {}_{}_toolset = McpToolset::new({}_{}_client)",
374                id, var_suffix, id, var_suffix
375            ));
376            if !config.tool_filter.is_empty() {
377                code.push_str(&format!(
378                    ".with_tools(&[{}])",
379                    config
380                        .tool_filter
381                        .iter()
382                        .map(|t| format!("\"{}\"", t))
383                        .collect::<Vec<_>>()
384                        .join(", ")
385                ));
386            }
387            code.push_str(";\n");
388            code.push_str(&format!("    let {}_{}_tools = {}_{}_toolset.tools(Arc::new(MinimalContext::new())).await?;\n", id, var_suffix, id, var_suffix));
389            code.push_str(&format!(
390                "    eprintln!(\"Loaded {{}} tools from MCP server '{}'\", {}_{}_tools.len());\n\n",
391                cmd, id, var_suffix
392            ));
393        }
394    }
395
396    code.push_str(&format!("    let mut {}_builder = LlmAgentBuilder::new(\"{}\")\n", id, id));
397    code.push_str(&format!(
398        "        .model(Arc::new(GeminiModel::new(&api_key, \"{}\")?));\n",
399        model
400    ));
401
402    if !agent.instruction.is_empty() {
403        let escaped =
404            agent.instruction.replace('\\', "\\\\").replace('"', "\\\"").replace('\n', "\\n");
405        code.push_str(&format!(
406            "    {}_builder = {}_builder.instruction(\"{}\");\n",
407            id, id, escaped
408        ));
409    }
410
411    // Add MCP tools if present (only for configured MCP tools)
412    for (idx, mcp_tool) in mcp_tools.iter().enumerate() {
413        let tool_id = format!("{}_{}", id, mcp_tool);
414        if project.tool_configs.contains_key(&tool_id) {
415            let var_suffix = if idx == 0 { "mcp".to_string() } else { format!("mcp_{}", idx + 1) };
416            code.push_str(&format!("    for tool in {}_{}_tools {{\n", id, var_suffix));
417            code.push_str(&format!("        {}_builder = {}_builder.tool(tool);\n", id, id));
418            code.push_str("    }\n");
419        }
420    }
421
422    for tool_type in &agent.tools {
423        // Handle function_1, function_2, etc.
424        if tool_type.starts_with("function") {
425            let tool_id = format!("{}_{}", id, tool_type);
426            if let Some(ToolConfig::Function(config)) = project.tool_configs.get(&tool_id) {
427                let struct_name = to_pascal_case(&config.name);
428                code.push_str(&format!("    {}_builder = {}_builder.tool(Arc::new(FunctionTool::new(\"{}\", \"{}\", {}_fn).with_parameters_schema::<{}Args>()));\n", 
429                    id, id, config.name, config.description.replace('"', "\\\""), config.name, struct_name));
430            }
431        } else if !tool_type.starts_with("mcp") {
432            match tool_type.as_str() {
433                "google_search" => code.push_str(&format!(
434                    "    {}_builder = {}_builder.tool(Arc::new(GoogleSearchTool::new()));\n",
435                    id, id
436                )),
437                "exit_loop" => code.push_str(&format!(
438                    "    {}_builder = {}_builder.tool(Arc::new(ExitLoopTool::new()));\n",
439                    id, id
440                )),
441                "load_artifact" => code.push_str(&format!(
442                    "    {}_builder = {}_builder.tool(Arc::new(LoadArtifactsTool::new()));\n",
443                    id, id
444                )),
445                "browser" => {
446                    code.push_str("    for tool in browser_toolset.tools(Arc::new(MinimalContext::new())).await? {\n");
447                    code.push_str(&format!(
448                        "        {}_builder = {}_builder.tool(tool);\n",
449                        id, id
450                    ));
451                    code.push_str("    }\n");
452                }
453                _ => {}
454            }
455        }
456    }
457
458    code.push_str(&format!("    let {}_llm = Arc::new({}_builder.build()?);\n\n", id, id));
459
460    code.push_str(&format!("    let {}_node = AgentNode::new({}_llm)\n", id, id));
461    code.push_str("        .with_input_mapper(|state| {\n");
462
463    // First agent reads from "message", subsequent agents read from "response"
464    if is_first {
465        code.push_str("            let msg = state.get(\"message\").and_then(|v| v.as_str()).unwrap_or(\"\");\n");
466    } else {
467        code.push_str(
468            "            // Read previous agent's response, fall back to original message\n",
469        );
470        code.push_str("            let msg = state.get(\"response\").and_then(|v| v.as_str())\n");
471        code.push_str("                .or_else(|| state.get(\"message\").and_then(|v| v.as_str())).unwrap_or(\"\");\n");
472    }
473
474    code.push_str("            adk_core::Content::new(\"user\").with_text(msg.to_string())\n");
475    code.push_str("        })\n");
476    code.push_str("        .with_output_mapper(|events| {\n");
477    code.push_str("            let mut updates = std::collections::HashMap::new();\n");
478    code.push_str("            let mut full_text = String::new();\n");
479    code.push_str("            for event in events {\n");
480    code.push_str("                if let Some(content) = event.content() {\n");
481    code.push_str("                    for part in &content.parts {\n");
482    code.push_str("                        if let Some(text) = part.text() {\n");
483    code.push_str("                            full_text.push_str(text);\n");
484    code.push_str("                        }\n");
485    code.push_str("                    }\n");
486    code.push_str("                }\n");
487    code.push_str("            }\n");
488    code.push_str("            if !full_text.is_empty() {\n");
489    code.push_str("                updates.insert(\"response\".to_string(), json!(full_text));\n");
490    code.push_str("            }\n");
491    code.push_str("            updates\n");
492    code.push_str("        });\n\n");
493
494    code
495}
496
497fn generate_container_node(id: &str, agent: &AgentSchema, project: &ProjectSchema) -> String {
498    let mut code = String::new();
499
500    // Generate sub-agents first
501    for sub_id in &agent.sub_agents {
502        if let Some(sub) = project.agents.get(sub_id) {
503            let model = sub.model.as_deref().unwrap_or("gemini-2.0-flash");
504            let has_tools = !sub.tools.is_empty();
505            let has_instruction = !sub.instruction.is_empty();
506            let mut_kw = if has_tools || has_instruction { "mut " } else { "" };
507
508            // Load MCP tools BEFORE creating builder (matching working pattern)
509            for tool_type in &sub.tools {
510                let tool_id = format!("{}_{}", sub_id, tool_type);
511                if tool_type.starts_with("mcp") {
512                    if let Some(ToolConfig::Mcp(config)) = project.tool_configs.get(&tool_id) {
513                        let var_suffix = tool_type.replace("mcp_", "mcp");
514                        code.push_str(&format!(
515                            "    let mut {}_{}_cmd = Command::new(\"{}\");\n",
516                            sub_id, var_suffix, config.server_command
517                        ));
518                        for arg in &config.server_args {
519                            code.push_str(&format!(
520                                "    {}_{}_cmd.arg(\"{}\");\n",
521                                sub_id, var_suffix, arg
522                            ));
523                        }
524                        code.push_str(&format!(
525                            "    let {}_{}_client = tokio::time::timeout(\n",
526                            sub_id, var_suffix
527                        ));
528                        code.push_str("        std::time::Duration::from_secs(10),\n");
529                        code.push_str(&format!(
530                            "        ().serve(TokioChildProcess::new({}_{}_cmd)?)\n",
531                            sub_id, var_suffix
532                        ));
533                        code.push_str(&format!("    ).await.map_err(|_| anyhow::anyhow!(\"MCP server '{}' failed to start within 10s\"))??;\n", config.server_command));
534                        code.push_str(&format!(
535                            "    let {}_{}_toolset = McpToolset::new({}_{}_client);\n",
536                            sub_id, var_suffix, sub_id, var_suffix
537                        ));
538                        code.push_str(&format!("    let {}_{}_tools = {}_{}_toolset.tools(Arc::new(MinimalContext::new())).await?;\n", sub_id, var_suffix, sub_id, var_suffix));
539                        code.push_str(&format!("    eprintln!(\"Loaded {{}} tools from MCP server '{}'\", {}_{}_tools.len());\n\n", config.server_command, sub_id, var_suffix));
540                    }
541                }
542            }
543
544            // Create builder
545            code.push_str(&format!(
546                "    let {}{}_builder = LlmAgentBuilder::new(\"{}\")\n",
547                mut_kw, sub_id, sub_id
548            ));
549            code.push_str(&format!(
550                "        .model(Arc::new(GeminiModel::new(&api_key, \"{}\")?))",
551                model
552            ));
553            code.push_str(";\n");
554
555            // Add instruction separately (matching working pattern)
556            if !sub.instruction.is_empty() {
557                let escaped =
558                    sub.instruction.replace('\\', "\\\\").replace('"', "\\\"").replace('\n', "\\n");
559                code.push_str(&format!(
560                    "    {}_builder = {}_builder.instruction(\"{}\");\n",
561                    sub_id, sub_id, escaped
562                ));
563            }
564
565            // Add tools
566            for tool_type in &sub.tools {
567                let tool_id = format!("{}_{}", sub_id, tool_type);
568                if tool_type.starts_with("function") {
569                    if let Some(ToolConfig::Function(config)) = project.tool_configs.get(&tool_id) {
570                        let fn_name = &config.name;
571                        let struct_name = to_pascal_case(fn_name);
572                        code.push_str(&format!("    {}_builder = {}_builder.tool(Arc::new(FunctionTool::new(\"{}\", \"{}\", {}_fn).with_parameters_schema::<{}Args>()));\n", 
573                            sub_id, sub_id, fn_name, config.description.replace('"', "\\\""), fn_name, struct_name));
574                    }
575                } else if tool_type.starts_with("mcp") {
576                    // Only generate tool loop if config exists (MCP setup was generated above)
577                    let tool_id = format!("{}_{}", sub_id, tool_type);
578                    if project.tool_configs.contains_key(&tool_id) {
579                        let var_suffix = tool_type.replace("mcp_", "mcp");
580                        code.push_str(&format!(
581                            "    for tool in {}_{}_tools {{\n",
582                            sub_id, var_suffix
583                        ));
584                        code.push_str(&format!(
585                            "        {}_builder = {}_builder.tool(tool);\n",
586                            sub_id, sub_id
587                        ));
588                        code.push_str("    }\n");
589                    }
590                } else if tool_type == "google_search" {
591                    code.push_str(&format!(
592                        "    {}_builder = {}_builder.tool(Arc::new(GoogleSearchTool::new()));\n",
593                        sub_id, sub_id
594                    ));
595                } else if tool_type == "exit_loop" {
596                    code.push_str(&format!(
597                        "    {}_builder = {}_builder.tool(Arc::new(ExitLoopTool::new()));\n",
598                        sub_id, sub_id
599                    ));
600                } else if tool_type == "load_artifact" {
601                    code.push_str(&format!(
602                        "    {}_builder = {}_builder.tool(Arc::new(LoadArtifactsTool::new()));\n",
603                        sub_id, sub_id
604                    ));
605                }
606            }
607
608            code.push_str(&format!("    let {}_agent = {}_builder.build()?;\n\n", sub_id, sub_id));
609        }
610    }
611
612    // Create container
613    let subs: Vec<_> = agent.sub_agents.iter().map(|s| format!("Arc::new({}_agent)", s)).collect();
614    let container_type = match agent.agent_type {
615        AgentType::Sequential => "adk_agent::SequentialAgent",
616        AgentType::Loop => "adk_agent::LoopAgent",
617        AgentType::Parallel => "adk_agent::ParallelAgent",
618        _ => "adk_agent::SequentialAgent",
619    };
620
621    code.push_str(&format!("    // Container: {} ({:?})\n", id, agent.agent_type));
622    if agent.agent_type == AgentType::Loop {
623        let max_iter = agent.max_iterations.unwrap_or(3);
624        code.push_str(&format!(
625            "    let {}_container = {}::new(\"{}\", vec![{}]).with_max_iterations({});\n\n",
626            id,
627            container_type,
628            id,
629            subs.join(", "),
630            max_iter
631        ));
632    } else {
633        code.push_str(&format!(
634            "    let {}_container = {}::new(\"{}\", vec![{}]);\n\n",
635            id,
636            container_type,
637            id,
638            subs.join(", ")
639        ));
640    }
641
642    // Wrap in AgentNode
643    code.push_str(&format!("    let {}_node = AgentNode::new(Arc::new({}_container))\n", id, id));
644    code.push_str("        .with_input_mapper(|state| {\n");
645    code.push_str(
646        "            let msg = state.get(\"message\").and_then(|v| v.as_str()).unwrap_or(\"\");\n",
647    );
648    code.push_str("            adk_core::Content::new(\"user\").with_text(msg.to_string())\n");
649    code.push_str("        })\n");
650    code.push_str("        .with_output_mapper(|events| {\n");
651    code.push_str("            let mut updates = std::collections::HashMap::new();\n");
652    code.push_str("            let mut full_text = String::new();\n");
653    code.push_str("            for event in events {\n");
654    code.push_str("                if let Some(content) = event.content() {\n");
655    code.push_str("                    for part in &content.parts {\n");
656    code.push_str("                        if let Some(text) = part.text() {\n");
657    code.push_str("                            full_text.push_str(text);\n");
658    code.push_str("                        }\n");
659    code.push_str("                    }\n");
660    code.push_str("                }\n");
661    code.push_str("            }\n");
662    code.push_str("            // Filter out tool call artifacts\n");
663    code.push_str("            let full_text = full_text.replace(\"exit_loop\", \"\");\n");
664    code.push_str("            if !full_text.is_empty() {\n");
665    code.push_str("                updates.insert(\"response\".to_string(), json!(full_text));\n");
666    code.push_str("            }\n");
667    code.push_str("            updates\n");
668    code.push_str("        });\n\n");
669
670    code
671}
672
673fn generate_function_tool(config: &crate::schema::FunctionToolConfig) -> String {
674    let mut code = String::new();
675    let fn_name = &config.name;
676
677    code.push_str(&format!("async fn {}_fn(_ctx: Arc<dyn ToolContext>, args: Value) -> Result<Value, adk_core::AdkError> {{\n", fn_name));
678
679    // Generate parameter extraction
680    for param in &config.parameters {
681        let extract = match param.param_type {
682            crate::schema::ParamType::String => format!(
683                "    let {} = args[\"{}\"].as_str().unwrap_or(\"\");\n",
684                param.name, param.name
685            ),
686            crate::schema::ParamType::Number => format!(
687                "    let {} = args[\"{}\"].as_f64().unwrap_or(0.0);\n",
688                param.name, param.name
689            ),
690            crate::schema::ParamType::Boolean => format!(
691                "    let {} = args[\"{}\"].as_bool().unwrap_or(false);\n",
692                param.name, param.name
693            ),
694        };
695        code.push_str(&extract);
696    }
697
698    code.push('\n');
699
700    // Insert user's code or generate placeholder
701    if config.code.is_empty() {
702        // Generate placeholder that echoes parameters
703        let param_json = config
704            .parameters
705            .iter()
706            .map(|p| format!("        \"{}\": {}", p.name, p.name))
707            .collect::<Vec<_>>()
708            .join(",\n");
709        code.push_str("    // TODO: Add function implementation\n");
710        code.push_str("    Ok(json!({\n");
711        code.push_str(&format!("        \"function\": \"{}\",\n", fn_name));
712        if !param_json.is_empty() {
713            code.push_str(&param_json);
714            code.push_str(",\n");
715        }
716        code.push_str("        \"status\": \"not_implemented\"\n");
717        code.push_str("    }))\n");
718    } else {
719        // Use user's actual code
720        code.push_str("    // User-defined implementation\n");
721        for line in config.code.lines() {
722            code.push_str(&format!("    {}\n", line));
723        }
724    }
725
726    code.push_str("}\n\n");
727    code
728}
729
730fn generate_function_schema(config: &crate::schema::FunctionToolConfig) -> String {
731    let mut code = String::new();
732    let struct_name = to_pascal_case(&config.name);
733
734    code.push_str("#[derive(serde::Serialize, serde::Deserialize, schemars::JsonSchema)]\n");
735    code.push_str(&format!("struct {}Args {{\n", struct_name));
736
737    for param in &config.parameters {
738        if !param.description.is_empty() {
739            code.push_str(&format!("    /// {}\n", param.description));
740        }
741        let rust_type = match param.param_type {
742            crate::schema::ParamType::String => "String",
743            crate::schema::ParamType::Number => "f64",
744            crate::schema::ParamType::Boolean => "bool",
745        };
746        code.push_str(&format!("    {}: {},\n", param.name, rust_type));
747    }
748
749    code.push_str("}\n\n");
750    code
751}
752
753fn to_pascal_case(s: &str) -> String {
754    s.split('_')
755        .map(|word| {
756            let mut chars = word.chars();
757            match chars.next() {
758                None => String::new(),
759                Some(c) => c.to_uppercase().chain(chars).collect(),
760            }
761        })
762        .collect()
763}
764
765fn generate_cargo_toml(project: &ProjectSchema) -> String {
766    let mut name = project
767        .name
768        .to_lowercase()
769        .replace(' ', "_")
770        .replace(|c: char| !c.is_alphanumeric() && c != '_', "");
771    // Cargo package names can't start with a digit
772    if name.is_empty() || name.chars().next().map(|c| c.is_ascii_digit()).unwrap_or(false) {
773        name = format!("project_{}", name);
774    }
775
776    // Check if any function tool code uses specific crates
777    let code_uses = |pattern: &str| -> bool {
778        project.tool_configs.values().any(|tc| {
779            if let ToolConfig::Function(fc) = tc { fc.code.contains(pattern) } else { false }
780        })
781    };
782
783    let needs_reqwest = code_uses("reqwest::");
784    let needs_lettre = code_uses("lettre::");
785    let needs_base64 = code_uses("base64::");
786
787    // Use path dependencies in dev mode, version dependencies in prod
788    let use_path_deps = std::env::var("ADK_DEV_MODE").is_ok();
789
790    let adk_root = "/data/projects/production/adk/adk-rust";
791
792    let adk_deps = if use_path_deps {
793        format!(
794            r#"adk-agent = {{ path = "{}/adk-agent" }}
795adk-core = {{ path = "{}/adk-core" }}
796adk-model = {{ path = "{}/adk-model" }}
797adk-tool = {{ path = "{}/adk-tool" }}
798adk-graph = {{ path = "{}/adk-graph" }}"#,
799            adk_root, adk_root, adk_root, adk_root, adk_root
800        )
801    } else {
802        r#"adk-agent = "0.2.0"
803adk-core = "0.2.0"
804adk-model = "0.2.0"
805adk-tool = "0.2.0"
806adk-graph = "0.2.0""#
807            .to_string()
808    };
809
810    // No patch section needed - adk-gemini is a workspace member
811    let patch_section = String::new();
812
813    let mut deps = format!(
814        r#"[package]
815name = "{}"
816version = "0.1.0"
817edition = "2021"
818
819[dependencies]
820{}
821tokio = {{ version = "1", features = ["full", "macros"] }}
822tokio-stream = "0.1"
823anyhow = "1"
824serde = {{ version = "1", features = ["derive"] }}
825serde_json = "1"
826schemars = "0.8"
827tracing-subscriber = {{ version = "0.3", features = ["json", "env-filter"] }}
828uuid = {{ version = "1", features = ["v4"] }}
829"#,
830        name, adk_deps
831    );
832
833    if needs_reqwest {
834        deps.push_str("reqwest = { version = \"0.12\", features = [\"json\"] }\n");
835    }
836    if needs_lettre {
837        deps.push_str("lettre = \"0.11\"\n");
838    }
839    if needs_base64 {
840        deps.push_str("base64 = \"0.21\"\n");
841    }
842
843    // Add rmcp if any agent uses MCP (handles mcp, mcp_1, mcp_2, etc.)
844    let uses_mcp = project
845        .agents
846        .values()
847        .any(|a| a.tools.iter().any(|t| t == "mcp" || t.starts_with("mcp_")));
848    if uses_mcp {
849        deps.push_str(
850            "rmcp = { version = \"0.9\", features = [\"client\", \"transport-child-process\"] }\n",
851        );
852        deps.push_str("async-trait = \"0.1\"\n");
853    }
854
855    // Add adk-browser if any agent uses browser tool
856    let uses_browser = project.agents.values().any(|a| a.tools.contains(&"browser".to_string()));
857    if uses_browser {
858        if use_path_deps {
859            deps.push_str(&format!("adk-browser = {{ path = \"{}/adk-browser\" }}\n", adk_root));
860        } else {
861            deps.push_str("adk-browser = \"0.1\"\n");
862        }
863        // async-trait needed for MinimalContext if not already added by MCP
864        if !uses_mcp {
865            deps.push_str("async-trait = \"0.1\"\n");
866        }
867    }
868
869    // Add patch section at the end (only in dev mode)
870    deps.push_str(&patch_section);
871
872    deps
873}