Skip to main content

adk_agent/
llm_agent.rs

1use adk_core::{
2    AfterAgentCallback, AfterModelCallback, AfterToolCallback, AfterToolCallbackFull, Agent,
3    BeforeAgentCallback, BeforeModelCallback, BeforeModelResult, BeforeToolCallback,
4    CallbackContext, Content, Event, EventActions, FunctionResponseData, GlobalInstructionProvider,
5    InstructionProvider, InvocationContext, Llm, LlmRequest, LlmResponse, MemoryEntry,
6    OnToolErrorCallback, Part, ReadonlyContext, Result, RetryBudget, Tool, ToolCallbackContext,
7    ToolConfirmationDecision, ToolConfirmationPolicy, ToolConfirmationRequest, ToolContext,
8    ToolExecutionStrategy, ToolOutcome, Toolset,
9};
10use async_stream::stream;
11use async_trait::async_trait;
12use std::{
13    collections::HashMap,
14    sync::{Arc, Mutex},
15};
16use tracing::Instrument;
17
18#[cfg(feature = "enhanced-plugins")]
19use adk_plugin::{
20    BeforeModelCallResult, BeforeToolCallResult, EnhancedPlugin, EnhancedPluginManager,
21};
22
23#[cfg(feature = "skills")]
24use crate::skill_shim::load_skill_index;
25use crate::{
26    guardrails::{GuardrailSet, enforce_guardrails},
27    skill_shim::{SelectionPolicy, SkillIndex, apply_skill_injection},
28    tool_call_markup::normalize_option_content,
29    workflow::with_user_content_override,
30};
31
32/// Default maximum number of LLM round-trips (iterations) before the agent stops.
33pub const DEFAULT_MAX_ITERATIONS: u32 = 100;
34
35/// Default tool execution timeout (5 minutes).
36pub const DEFAULT_TOOL_TIMEOUT: std::time::Duration = std::time::Duration::from_secs(300);
37
38fn trace_json_payload<T: serde::Serialize>(
39    value: &T,
40    record_payloads: bool,
41    max_bytes: usize,
42) -> String {
43    let json = serde_json::to_string(value).unwrap_or_default();
44    if cfg!(feature = "record-payloads") && record_payloads {
45        return json;
46    }
47
48    let max_bytes = max_bytes.max(32);
49    if json.len() <= max_bytes {
50        return json;
51    }
52
53    let mut end = max_bytes;
54    while !json.is_char_boundary(end) {
55        end -= 1;
56    }
57    format!("{}...[truncated {} bytes]", &json[..end], json.len() - end)
58}
59
60#[derive(Debug, Clone)]
61struct PendingToolCall {
62    index: usize,
63    name: String,
64    args: serde_json::Value,
65    id: Option<String>,
66    function_call_id: String,
67}
68
69fn build_generation_config(
70    base: Option<&adk_core::GenerateContentConfig>,
71    output_schema: Option<&serde_json::Value>,
72    cached_content: Option<&str>,
73) -> Option<adk_core::GenerateContentConfig> {
74    let mut config = base.cloned().unwrap_or_default();
75    if let Some(schema) = output_schema {
76        config.response_schema = Some(schema.clone());
77    }
78    if config.cached_content.is_none()
79        && let Some(cached_content) = cached_content
80    {
81        config.cached_content = Some(cached_content.to_string());
82    }
83
84    if base.is_some() || output_schema.is_some() || cached_content.is_some() {
85        Some(config)
86    } else {
87        None
88    }
89}
90
91fn collect_function_calls(content: &Content, invocation_id: &str) -> Vec<PendingToolCall> {
92    content
93        .parts
94        .iter()
95        .filter_map(|part| {
96            if let Part::FunctionCall { name, args, id, .. } = part {
97                Some((name, args, id))
98            } else {
99                None
100            }
101        })
102        .enumerate()
103        .map(|(index, (name, args, id))| PendingToolCall {
104            index,
105            name: name.clone(),
106            args: args.clone(),
107            id: id.clone(),
108            function_call_id: id
109                .clone()
110                .unwrap_or_else(|| format!("{invocation_id}_{name}_{index}")),
111        })
112        .collect()
113}
114
115fn build_partial_llm_event(
116    event_id: &str,
117    invocation_id: &str,
118    agent_name: &str,
119    request_json: &str,
120    chunk: &LlmResponse,
121    long_running_tool_ids: Vec<String>,
122) -> Event {
123    let mut event = Event::with_id(event_id, invocation_id);
124    event.author = agent_name.to_string();
125    event.llm_request = Some(request_json.to_string());
126    event
127        .provider_metadata
128        .insert("gcp.vertex.agent.llm_request".to_string(), request_json.to_string());
129    event.provider_metadata.insert(
130        "gcp.vertex.agent.llm_response".to_string(),
131        serde_json::to_string(chunk).unwrap_or_default(),
132    );
133    event.llm_response.partial = chunk.partial;
134    event.llm_response.turn_complete = chunk.turn_complete;
135    event.llm_response.finish_reason = chunk.finish_reason;
136    event.llm_response.usage_metadata = chunk.usage_metadata.clone();
137    event.llm_response.content = chunk.content.clone();
138    event.llm_response.provider_metadata = chunk.provider_metadata.clone();
139    event.llm_response.interaction_id = chunk.interaction_id.clone();
140    // Provider failures delivered as `Ok(LlmResponse { error_code, .. })` must
141    // remain observable on the streamed event.
142    event.llm_response.interrupted = chunk.interrupted;
143    event.llm_response.error_code = chunk.error_code.clone();
144    event.llm_response.error_message = chunk.error_message.clone();
145    event.long_running_tool_ids = long_running_tool_ids;
146    event
147}
148
149fn build_final_llm_event(
150    event_id: &str,
151    invocation_id: &str,
152    agent_name: &str,
153    request_json: &str,
154    content: Option<&Content>,
155    last_chunk: Option<&LlmResponse>,
156    long_running_tool_ids: Vec<String>,
157) -> Event {
158    let mut event = Event::with_id(event_id, invocation_id);
159    event.author = agent_name.to_string();
160    event.llm_request = Some(request_json.to_string());
161    event
162        .provider_metadata
163        .insert("gcp.vertex.agent.llm_request".to_string(), request_json.to_string());
164    event.llm_response.content = content.cloned();
165    event.llm_response.partial = false;
166    event.llm_response.turn_complete = true;
167
168    if let Some(last_chunk) = last_chunk {
169        event.llm_response.finish_reason = last_chunk.finish_reason;
170        event.llm_response.usage_metadata = last_chunk.usage_metadata.clone();
171        event.llm_response.provider_metadata = last_chunk.provider_metadata.clone();
172        event.llm_response.interaction_id = last_chunk.interaction_id.clone();
173        event.llm_response.interrupted = last_chunk.interrupted;
174        event.llm_response.error_code = last_chunk.error_code.clone();
175        event.llm_response.error_message = last_chunk.error_message.clone();
176        event.provider_metadata.insert(
177            "gcp.vertex.agent.llm_response".to_string(),
178            serde_json::to_string(last_chunk).unwrap_or_default(),
179        );
180    }
181
182    event.long_running_tool_ids = long_running_tool_ids;
183    event
184}
185
186/// An LLM-powered agent that orchestrates tool calls and sub-agent delegation.
187///
188/// `LlmAgent` is the primary agent type in ADK. It sends requests to an LLM,
189/// executes tool calls from the response, and iterates until the model produces
190/// a final text response or the iteration limit is reached.
191///
192/// Use [`LlmAgentBuilder`] (via `LlmAgent::builder()`) to construct instances.
193pub struct LlmAgent {
194    name: String,
195    description: String,
196    model: Arc<dyn Llm>,
197    instruction: Option<String>,
198    instruction_provider: Option<Arc<InstructionProvider>>,
199    global_instruction: Option<String>,
200    global_instruction_provider: Option<Arc<GlobalInstructionProvider>>,
201    skills_index: Option<Arc<SkillIndex>>,
202    skill_policy: SelectionPolicy,
203    max_skill_chars: usize,
204    #[allow(dead_code)] // Part of public API via builder
205    input_schema: Option<serde_json::Value>,
206    output_schema: Option<serde_json::Value>,
207    /// Maximum retry attempts for output schema validation (default: 3).
208    output_max_retries: usize,
209    disallow_transfer_to_parent: bool,
210    disallow_transfer_to_peers: bool,
211    include_contents: adk_core::IncludeContents,
212    tools: Vec<Arc<dyn Tool>>,
213    toolsets: Vec<Arc<dyn Toolset>>,
214    sub_agents: Vec<Arc<dyn Agent>>,
215    output_key: Option<String>,
216    /// Default generation config (temperature, top_p, etc.) applied to every LLM request.
217    generate_content_config: Option<adk_core::GenerateContentConfig>,
218    /// Maximum number of LLM round-trips before stopping
219    max_iterations: u32,
220    /// Timeout for individual tool executions
221    tool_timeout: std::time::Duration,
222    before_callbacks: Arc<Vec<BeforeAgentCallback>>,
223    after_callbacks: Arc<Vec<AfterAgentCallback>>,
224    before_model_callbacks: Arc<Vec<BeforeModelCallback>>,
225    after_model_callbacks: Arc<Vec<AfterModelCallback>>,
226    before_tool_callbacks: Arc<Vec<BeforeToolCallback>>,
227    after_tool_callbacks: Arc<Vec<AfterToolCallback>>,
228    on_tool_error_callbacks: Arc<Vec<OnToolErrorCallback>>,
229    /// Rich after-tool callbacks that receive tool, args, and response.
230    after_tool_callbacks_full: Arc<Vec<AfterToolCallbackFull>>,
231    /// Default retry budget applied to all tools without a per-tool override.
232    default_retry_budget: Option<RetryBudget>,
233    /// Per-tool retry budget overrides, keyed by tool name.
234    tool_retry_budgets: std::collections::HashMap<String, RetryBudget>,
235    /// Circuit breaker failure threshold. When set, tools are temporarily disabled
236    /// after this many consecutive failures within a single invocation.
237    circuit_breaker_threshold: Option<u32>,
238    tool_confirmation_policy: ToolConfirmationPolicy,
239    /// Per-agent tool execution strategy override. When `Some`, overrides the
240    /// `RunConfig` strategy for this agent's dispatch loop.
241    tool_execution_strategy: Option<ToolExecutionStrategy>,
242    input_guardrails: Arc<GuardrailSet>,
243    output_guardrails: Arc<GuardrailSet>,
244    /// Enhanced plugin manager for fine-grained tool/model call interception.
245    /// Only created when enhanced plugins are registered (zero overhead otherwise).
246    #[cfg(feature = "enhanced-plugins")]
247    enhanced_plugin_manager: Option<Arc<EnhancedPluginManager>>,
248    /// Optional sandbox configuration for workspace lifecycle management.
249    /// When present, the SandboxRunner uses this to provision and bind tools.
250    /// The config does NOT add tools directly — that is SandboxRunner's responsibility.
251    #[cfg(feature = "sandbox")]
252    sandbox_config: Option<adk_sandbox::workspace::SandboxConfig>,
253}
254
255struct PromptConfig {
256    instruction: Option<String>,
257    instruction_provider: Option<Arc<InstructionProvider>>,
258    global_instruction: Option<String>,
259    global_instruction_provider: Option<Arc<GlobalInstructionProvider>>,
260    skills_index: Option<Arc<SkillIndex>>,
261    skill_policy: SelectionPolicy,
262    max_skill_chars: usize,
263    output_schema: Option<serde_json::Value>,
264    include_contents: adk_core::IncludeContents,
265}
266
267impl PromptConfig {
268    fn from_agent(agent: &LlmAgent) -> Self {
269        Self {
270            instruction: agent.instruction.clone(),
271            instruction_provider: agent.instruction_provider.clone(),
272            global_instruction: agent.global_instruction.clone(),
273            global_instruction_provider: agent.global_instruction_provider.clone(),
274            skills_index: agent.skills_index.clone(),
275            skill_policy: agent.skill_policy.clone(),
276            max_skill_chars: agent.max_skill_chars,
277            output_schema: agent.output_schema.clone(),
278            include_contents: agent.include_contents,
279        }
280    }
281
282    async fn prepare_conversation(
283        &self,
284        ctx: &Arc<dyn InvocationContext>,
285        agent_name: &str,
286    ) -> Result<Vec<Content>> {
287        let mut preamble = Vec::new();
288
289        if let Some(provider) = &self.global_instruction_provider {
290            let instruction = provider(ctx.clone() as Arc<dyn ReadonlyContext>).await?;
291            if !instruction.is_empty() {
292                preamble.push(Content::new("user").with_text(instruction));
293            }
294        } else if let Some(template) = &self.global_instruction {
295            let instruction = adk_core::inject_session_state(ctx.as_ref(), template).await?;
296            if !instruction.is_empty() {
297                preamble.push(Content::new("user").with_text(instruction));
298            }
299        }
300
301        if let Some(provider) = &self.instruction_provider {
302            let instruction = provider(ctx.clone() as Arc<dyn ReadonlyContext>).await?;
303            if !instruction.is_empty() {
304                preamble.push(Content::new("user").with_text(instruction));
305            }
306        } else if let Some(template) = &self.instruction {
307            let instruction = adk_core::inject_session_state(ctx.as_ref(), template).await?;
308            if !instruction.is_empty() {
309                preamble.push(Content::new("user").with_text(instruction));
310            }
311        }
312
313        if let Some(schema) = &self.output_schema {
314            preamble.push(Content::new("user").with_text(format!(
315                "You MUST respond with valid JSON conforming to this schema: {schema}. Do not include any text outside the JSON object."
316            )));
317        }
318
319        let agent_filter =
320            if ctx.run_config().transfer_targets.is_empty() { None } else { Some(agent_name) };
321        let mut session_history =
322            ctx.session().conversation_history_scoped(agent_filter, ctx.branch());
323        let mut current_user_content = ctx.user_content().clone();
324        if let Some(index) = &self.skills_index {
325            apply_skill_injection(
326                &mut current_user_content,
327                index.as_ref(),
328                &self.skill_policy,
329                self.max_skill_chars,
330            );
331        }
332        if let Some(index) = session_history.iter().rposition(|content| content.role == "user") {
333            session_history[index] = current_user_content.clone();
334        } else {
335            session_history.push(current_user_content.clone());
336        }
337
338        Ok(match self.include_contents {
339            adk_core::IncludeContents::None => {
340                preamble.push(current_user_content);
341                preamble
342            }
343            adk_core::IncludeContents::Default => {
344                preamble.extend(session_history);
345                preamble
346            }
347        })
348    }
349}
350
351struct ToolSetup {
352    tools: Vec<Arc<dyn Tool>>,
353    toolsets: Vec<Arc<dyn Toolset>>,
354    sub_agents: Vec<Arc<dyn Agent>>,
355    disallow_transfer_to_parent: bool,
356    disallow_transfer_to_peers: bool,
357}
358
359struct ResolvedTools {
360    map: HashMap<String, Arc<dyn Tool>>,
361    declarations: HashMap<String, serde_json::Value>,
362    transfer_targets: Vec<String>,
363}
364
365impl ToolSetup {
366    fn from_agent(agent: &LlmAgent) -> Self {
367        Self {
368            tools: agent.tools.clone(),
369            toolsets: agent.toolsets.clone(),
370            sub_agents: agent.sub_agents.clone(),
371            disallow_transfer_to_parent: agent.disallow_transfer_to_parent,
372            disallow_transfer_to_peers: agent.disallow_transfer_to_peers,
373        }
374    }
375
376    async fn resolve(&self, ctx: &Arc<dyn InvocationContext>) -> Result<ResolvedTools> {
377        let mut tools = self.tools.clone();
378        let static_tool_names: std::collections::HashSet<_> =
379            tools.iter().map(|tool| tool.name().to_string()).collect();
380        let mut toolset_sources = std::collections::HashMap::<String, String>::new();
381        let mut active_toolsets: Vec<&dyn Toolset> =
382            self.toolsets.iter().map(AsRef::as_ref).collect();
383        active_toolsets.extend(
384            ctx.run_config().runtime_toolsets.iter().map(|runtime| runtime.toolset().as_ref()),
385        );
386
387        for toolset in active_toolsets {
388            for tool in toolset.tools(ctx.clone() as Arc<dyn ReadonlyContext>).await? {
389                let name = tool.name().to_string();
390                if static_tool_names.contains(&name) {
391                    return Err(adk_core::AdkError::agent(format!(
392                        "Duplicate tool name '{name}': conflict between static tool and toolset '{}'",
393                        toolset.name()
394                    )));
395                }
396                if let Some(other_toolset) = toolset_sources.get(&name) {
397                    return Err(adk_core::AdkError::agent(format!(
398                        "Duplicate tool name '{name}': conflict between toolset '{other_toolset}' and toolset '{}'",
399                        toolset.name()
400                    )));
401                }
402                toolset_sources.insert(name, toolset.name().to_string());
403                tools.push(tool);
404            }
405        }
406
407        let map = tools.iter().map(|tool| (tool.name().to_string(), tool.clone())).collect();
408        let mut declarations = tools
409            .iter()
410            .map(|tool| (tool.name().to_string(), tool.declaration()))
411            .collect::<std::collections::HashMap<_, _>>();
412        let mut transfer_targets: Vec<String> =
413            self.sub_agents.iter().map(|agent| agent.name().to_string()).collect();
414        let child_names: std::collections::HashSet<_> =
415            self.sub_agents.iter().map(|agent| agent.name()).collect();
416        let parent_name = ctx.run_config().parent_agent.as_deref();
417
418        for target in &ctx.run_config().transfer_targets {
419            if child_names.contains(target.as_str()) {
420                continue;
421            }
422            let is_parent = parent_name == Some(target.as_str());
423            if (is_parent && self.disallow_transfer_to_parent)
424                || (!is_parent && self.disallow_transfer_to_peers)
425            {
426                continue;
427            }
428            transfer_targets.push(target.clone());
429        }
430
431        if !transfer_targets.is_empty() {
432            declarations.insert(
433                "transfer_to_agent".to_string(),
434                serde_json::json!({
435                    "name": "transfer_to_agent",
436                    "description": format!(
437                        "Transfer execution to another agent. Valid targets: {}",
438                        transfer_targets.join(", ")
439                    ),
440                    "parameters": {
441                        "type": "object",
442                        "properties": {
443                            "agent_name": {
444                                "type": "string",
445                                "description": "The name of the agent to transfer to.",
446                                "enum": transfer_targets
447                            }
448                        },
449                        "required": ["agent_name"]
450                    }
451                }),
452            );
453        }
454
455        Ok(ResolvedTools { map, declarations, transfer_targets })
456    }
457}
458
459impl std::fmt::Debug for LlmAgent {
460    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
461        f.debug_struct("LlmAgent")
462            .field("name", &self.name)
463            .field("description", &self.description)
464            .field("model", &self.model.name())
465            .field("instruction", &self.instruction)
466            .field("tools_count", &self.tools.len())
467            .field("sub_agents_count", &self.sub_agents.len())
468            .finish()
469    }
470}
471
472/// Resolves a static confirmation decision for one exact tool call.
473///
474/// Decisions are keyed by function call ID rather than tool name, so an approval
475/// cannot be replayed onto a different call that happens to use the same tool. When
476/// the run also supplies a fingerprint for that ID, the call's own fingerprint must
477/// match it; a mismatch is treated as no decision, which leaves the call
478/// unconfirmed rather than silently authorising different arguments.
479fn static_confirmation_decision(
480    decisions: &std::collections::HashMap<String, ToolConfirmationDecision>,
481    fingerprints: &std::collections::HashMap<String, String>,
482    function_call_id: &str,
483    tool_name: &str,
484    args: &serde_json::Value,
485) -> Option<ToolConfirmationDecision> {
486    let decision = decisions.get(function_call_id).copied()?;
487    if let Some(expected) = fingerprints.get(function_call_id) {
488        let actual = adk_core::tool_call_fingerprint(tool_name, args);
489        if &actual != expected {
490            tracing::warn!(
491                tool.name = %tool_name,
492                function_call.id = %function_call_id,
493                "confirmation decision does not match this call's arguments, treating as unconfirmed"
494            );
495            return None;
496        }
497    }
498    Some(decision)
499}
500
501impl LlmAgent {
502    /// Returns the sandbox configuration attached to this agent, if any.
503    ///
504    /// The `SandboxRunner` uses this to provision a workspace and bind tools.
505    /// Returns `None` when no sandbox config was set on the builder.
506    ///
507    /// Requires the `sandbox` feature.
508    #[cfg(feature = "sandbox")]
509    pub fn sandbox_config(&self) -> Option<&adk_sandbox::workspace::SandboxConfig> {
510        self.sandbox_config.as_ref()
511    }
512
513    async fn apply_input_guardrails(
514        ctx: Arc<dyn InvocationContext>,
515        input_guardrails: Arc<GuardrailSet>,
516    ) -> Result<Arc<dyn InvocationContext>> {
517        let content =
518            enforce_guardrails(input_guardrails.as_ref(), ctx.user_content(), "input").await?;
519        if content.role != ctx.user_content().role || content.parts != ctx.user_content().parts {
520            Ok(with_user_content_override(ctx, content))
521        } else {
522            Ok(ctx)
523        }
524    }
525
526    async fn apply_output_guardrails(
527        output_guardrails: &GuardrailSet,
528        content: Content,
529    ) -> Result<Content> {
530        enforce_guardrails(output_guardrails, &content, "output").await
531    }
532
533    fn history_parts_from_provider_metadata(
534        provider_metadata: Option<&serde_json::Value>,
535    ) -> Vec<Part> {
536        let Some(provider_metadata) = provider_metadata else {
537            return Vec::new();
538        };
539
540        let history_parts = provider_metadata
541            .get("conversation_history_parts")
542            .or_else(|| {
543                provider_metadata
544                    .get("openai")
545                    .and_then(|openai| openai.get("conversation_history_parts"))
546            })
547            .and_then(serde_json::Value::as_array);
548
549        history_parts
550            .into_iter()
551            .flatten()
552            .filter_map(|value| serde_json::from_value::<Part>(value.clone()).ok())
553            .collect()
554    }
555
556    fn augment_content_for_history(
557        content: &Content,
558        provider_metadata: Option<&serde_json::Value>,
559    ) -> Content {
560        let mut augmented = content.clone();
561        augmented.parts.extend(Self::history_parts_from_provider_metadata(provider_metadata));
562        augmented
563    }
564}
565
566/// Validate a JSON string against an output schema.
567///
568/// Returns `Ok(valid_json)` if the text parses as valid JSON and passes schema
569/// validation. Returns `Err(error_message)` describing the validation failure.
570fn validate_output_against_schema(
571    text: &str,
572    schema: &serde_json::Value,
573) -> std::result::Result<serde_json::Value, String> {
574    let parsed: serde_json::Value =
575        serde_json::from_str(text).map_err(|e| format!("Response is not valid JSON: {e}"))?;
576
577    let validator =
578        jsonschema::validator_for(schema).map_err(|e| format!("Invalid schema: {e}"))?;
579
580    let errors: Vec<String> = validator.iter_errors(&parsed).map(|e| e.to_string()).collect();
581
582    if errors.is_empty() { Ok(parsed) } else { Err(errors.join("; ")) }
583}
584
585/// Extract the text content from a series of events.
586///
587/// Scans events in reverse order for the last non-empty text content
588/// produced by the agent. Used internally for output schema validation.
589fn extract_text_from_events(events: &[Event]) -> Option<String> {
590    for event in events.iter().rev() {
591        if let Some(ref content) = event.llm_response.content {
592            let text: String =
593                content
594                    .parts
595                    .iter()
596                    .filter_map(|p| {
597                        if let Part::Text { text } = p { Some(text.as_str()) } else { None }
598                    })
599                    .collect::<Vec<_>>()
600                    .join("");
601            if !text.is_empty() {
602                return Some(text);
603            }
604        }
605    }
606    None
607}
608
609/// Extract a typed value from agent events.
610///
611/// Scans events for the last text content and deserializes it into `T`.
612/// This is useful after running an agent with `output_schema` set to
613/// extract the structured result.
614///
615/// # Example
616///
617/// ```rust,ignore
618/// use serde::Deserialize;
619/// use adk_agent::extract_typed;
620///
621/// #[derive(Deserialize)]
622/// struct Weather {
623///     temperature: f64,
624///     condition: String,
625/// }
626///
627/// let events: Vec<Event> = collect_events_from_stream(stream).await?;
628/// let weather: Weather = extract_typed(&events)?;
629/// ```
630pub fn extract_typed<T: serde::de::DeserializeOwned>(events: &[Event]) -> Result<T> {
631    let text = extract_text_from_events(events).ok_or_else(|| {
632        adk_core::AdkError::agent("no text content found in events for typed extraction")
633    })?;
634
635    serde_json::from_str(&text)
636        .map_err(|e| adk_core::AdkError::agent(format!("output deserialization failed: {e}")))
637}
638
639/// Builder for constructing an [`LlmAgent`] with all configuration options.
640pub struct LlmAgentBuilder {
641    name: String,
642    description: Option<String>,
643    model: Option<Arc<dyn Llm>>,
644    instruction: Option<String>,
645    instruction_provider: Option<Arc<InstructionProvider>>,
646    global_instruction: Option<String>,
647    global_instruction_provider: Option<Arc<GlobalInstructionProvider>>,
648    skills_index: Option<Arc<SkillIndex>>,
649    skill_policy: SelectionPolicy,
650    max_skill_chars: usize,
651    input_schema: Option<serde_json::Value>,
652    output_schema: Option<serde_json::Value>,
653    output_max_retries: usize,
654    disallow_transfer_to_parent: bool,
655    disallow_transfer_to_peers: bool,
656    include_contents: adk_core::IncludeContents,
657    tools: Vec<Arc<dyn Tool>>,
658    toolsets: Vec<Arc<dyn Toolset>>,
659    sub_agents: Vec<Arc<dyn Agent>>,
660    output_key: Option<String>,
661    generate_content_config: Option<adk_core::GenerateContentConfig>,
662    max_iterations: u32,
663    tool_timeout: std::time::Duration,
664    before_callbacks: Vec<BeforeAgentCallback>,
665    after_callbacks: Vec<AfterAgentCallback>,
666    before_model_callbacks: Vec<BeforeModelCallback>,
667    after_model_callbacks: Vec<AfterModelCallback>,
668    before_tool_callbacks: Vec<BeforeToolCallback>,
669    after_tool_callbacks: Vec<AfterToolCallback>,
670    on_tool_error_callbacks: Vec<OnToolErrorCallback>,
671    after_tool_callbacks_full: Vec<AfterToolCallbackFull>,
672    default_retry_budget: Option<RetryBudget>,
673    tool_retry_budgets: std::collections::HashMap<String, RetryBudget>,
674    circuit_breaker_threshold: Option<u32>,
675    tool_confirmation_policy: ToolConfirmationPolicy,
676    tool_execution_strategy: Option<ToolExecutionStrategy>,
677    input_guardrails: GuardrailSet,
678    output_guardrails: GuardrailSet,
679    /// Enhanced plugins to register on the built agent.
680    #[cfg(feature = "enhanced-plugins")]
681    enhanced_plugins: Vec<Arc<dyn EnhancedPlugin>>,
682    /// Optional sandbox configuration for workspace lifecycle management.
683    #[cfg(feature = "sandbox")]
684    sandbox_config: Option<adk_sandbox::workspace::SandboxConfig>,
685}
686
687impl LlmAgentBuilder {
688    /// Create a new builder with the given agent name.
689    pub fn new(name: impl Into<String>) -> Self {
690        Self {
691            name: name.into(),
692            description: None,
693            model: None,
694            instruction: None,
695            instruction_provider: None,
696            global_instruction: None,
697            global_instruction_provider: None,
698            skills_index: None,
699            skill_policy: SelectionPolicy::default(),
700            max_skill_chars: 2000,
701            input_schema: None,
702            output_schema: None,
703            output_max_retries: 3,
704            disallow_transfer_to_parent: false,
705            disallow_transfer_to_peers: false,
706            include_contents: adk_core::IncludeContents::Default,
707            tools: Vec::new(),
708            toolsets: Vec::new(),
709            sub_agents: Vec::new(),
710            output_key: None,
711            generate_content_config: None,
712            max_iterations: DEFAULT_MAX_ITERATIONS,
713            tool_timeout: DEFAULT_TOOL_TIMEOUT,
714            before_callbacks: Vec::new(),
715            after_callbacks: Vec::new(),
716            before_model_callbacks: Vec::new(),
717            after_model_callbacks: Vec::new(),
718            before_tool_callbacks: Vec::new(),
719            after_tool_callbacks: Vec::new(),
720            on_tool_error_callbacks: Vec::new(),
721            after_tool_callbacks_full: Vec::new(),
722            default_retry_budget: None,
723            tool_retry_budgets: std::collections::HashMap::new(),
724            circuit_breaker_threshold: None,
725            tool_confirmation_policy: ToolConfirmationPolicy::Never,
726            tool_execution_strategy: None,
727            input_guardrails: GuardrailSet::new(),
728            output_guardrails: GuardrailSet::new(),
729            #[cfg(feature = "enhanced-plugins")]
730            enhanced_plugins: Vec::new(),
731            #[cfg(feature = "sandbox")]
732            sandbox_config: None,
733        }
734    }
735
736    /// Set the agent description.
737    pub fn description(mut self, desc: impl Into<String>) -> Self {
738        self.description = Some(desc.into());
739        self
740    }
741
742    /// Set the LLM model for this agent.
743    pub fn model(mut self, model: Arc<dyn Llm>) -> Self {
744        self.model = Some(model);
745        self
746    }
747
748    /// Set the system instruction for this agent.
749    pub fn instruction(mut self, instruction: impl Into<String>) -> Self {
750        self.instruction = Some(instruction.into());
751        self
752    }
753
754    /// Set a dynamic instruction provider evaluated per invocation.
755    pub fn instruction_provider(mut self, provider: InstructionProvider) -> Self {
756        self.instruction_provider = Some(Arc::new(provider));
757        self
758    }
759
760    /// Set a global instruction prepended to all requests.
761    pub fn global_instruction(mut self, instruction: impl Into<String>) -> Self {
762        self.global_instruction = Some(instruction.into());
763        self
764    }
765
766    /// Set a dynamic global instruction provider evaluated per invocation.
767    pub fn global_instruction_provider(mut self, provider: GlobalInstructionProvider) -> Self {
768        self.global_instruction_provider = Some(Arc::new(provider));
769        self
770    }
771
772    /// Set a preloaded skills index for this agent.
773    ///
774    /// The best matching skill is injected into the current user turn so stable
775    /// instructions and conversation history remain available for prompt caching.
776    #[cfg(feature = "skills")]
777    pub fn with_skills(mut self, index: SkillIndex) -> Self {
778        self.skills_index = Some(Arc::new(index));
779        self
780    }
781
782    /// Auto-load skills from `.skills/` in the current working directory.
783    #[cfg(feature = "skills")]
784    pub fn with_auto_skills(self) -> Result<Self> {
785        self.with_skills_from_root(".")
786    }
787
788    /// Auto-load skills from `.skills/` under a custom root directory.
789    #[cfg(feature = "skills")]
790    pub fn with_skills_from_root(mut self, root: impl AsRef<std::path::Path>) -> Result<Self> {
791        let index = load_skill_index(root).map_err(|e| adk_core::AdkError::agent(e.to_string()))?;
792        self.skills_index = Some(Arc::new(index));
793        Ok(self)
794    }
795
796    /// Customize skill selection behavior.
797    #[cfg(feature = "skills")]
798    pub fn with_skill_policy(mut self, policy: SelectionPolicy) -> Self {
799        self.skill_policy = policy;
800        self
801    }
802
803    /// Limit injected skill content length.
804    #[cfg(feature = "skills")]
805    pub fn with_skill_budget(mut self, max_chars: usize) -> Self {
806        self.max_skill_chars = max_chars;
807        self
808    }
809
810    /// Set a JSON schema for validating user input.
811    pub fn input_schema(mut self, schema: serde_json::Value) -> Self {
812        self.input_schema = Some(schema);
813        self
814    }
815
816    /// Set a JSON schema for structured output from the LLM.
817    pub fn output_schema(mut self, schema: serde_json::Value) -> Self {
818        self.output_schema = Some(schema);
819        self
820    }
821
822    /// Derive the output schema from a Rust type using `schemars`.
823    ///
824    /// This is a convenience method that generates a JSON Schema from `T`'s
825    /// `JsonSchema` implementation and sets it as the output schema.
826    ///
827    /// # Example
828    ///
829    /// ```rust,ignore
830    /// use schemars::JsonSchema;
831    /// use serde::Deserialize;
832    ///
833    /// #[derive(JsonSchema, Deserialize)]
834    /// struct MyOutput {
835    ///     name: String,
836    ///     score: f64,
837    /// }
838    ///
839    /// let agent = LlmAgentBuilder::new("my-agent")
840    ///     .model(model)
841    ///     .output_type::<MyOutput>()
842    ///     .build()?;
843    /// ```
844    pub fn output_type<T: schemars::JsonSchema>(mut self) -> Self {
845        let schema = schemars::schema_for!(T);
846        self.output_schema =
847            Some(serde_json::to_value(schema).expect("schema serialization cannot fail"));
848        self
849    }
850
851    /// Set the maximum number of retry attempts for output schema validation.
852    ///
853    /// When the LLM produces output that fails schema validation, the agent
854    /// will retry up to this many times with a correction prompt. Default is 3.
855    pub fn output_max_retries(mut self, n: usize) -> Self {
856        self.output_max_retries = n;
857        self
858    }
859
860    /// Prevent this agent from transferring control back to its parent.
861    pub fn disallow_transfer_to_parent(mut self, disallow: bool) -> Self {
862        self.disallow_transfer_to_parent = disallow;
863        self
864    }
865
866    /// Prevent this agent from transferring control to peer agents.
867    pub fn disallow_transfer_to_peers(mut self, disallow: bool) -> Self {
868        self.disallow_transfer_to_peers = disallow;
869        self
870    }
871
872    /// Control which conversation history contents are included in LLM requests.
873    pub fn include_contents(mut self, include: adk_core::IncludeContents) -> Self {
874        self.include_contents = include;
875        self
876    }
877
878    /// Set a state key where the agent's final output will be stored.
879    pub fn output_key(mut self, key: impl Into<String>) -> Self {
880        self.output_key = Some(key.into());
881        self
882    }
883
884    /// Set default generation parameters (temperature, top_p, top_k, max_output_tokens)
885    /// applied to every LLM request made by this agent.
886    ///
887    /// These defaults are merged with any per-request config. If `output_schema` is also
888    /// set, the schema is preserved alongside these generation parameters.
889    ///
890    /// # Example
891    ///
892    /// ```rust,ignore
893    /// use adk_core::GenerateContentConfig;
894    ///
895    /// let agent = LlmAgentBuilder::new("my-agent")
896    ///     .model(model)
897    ///     .generate_content_config(GenerateContentConfig {
898    ///         temperature: Some(0.7),
899    ///         max_output_tokens: Some(2048),
900    ///         ..Default::default()
901    ///     })
902    ///     .build()?;
903    /// ```
904    pub fn generate_content_config(mut self, config: adk_core::GenerateContentConfig) -> Self {
905        self.generate_content_config = Some(config);
906        self
907    }
908
909    /// Set the default temperature for LLM requests.
910    /// Shorthand for setting just temperature without a full `GenerateContentConfig`.
911    pub fn temperature(mut self, temperature: f32) -> Self {
912        self.generate_content_config
913            .get_or_insert(adk_core::GenerateContentConfig::default())
914            .temperature = Some(temperature);
915        self
916    }
917
918    /// Set the default top_p for LLM requests.
919    pub fn top_p(mut self, top_p: f32) -> Self {
920        self.generate_content_config
921            .get_or_insert(adk_core::GenerateContentConfig::default())
922            .top_p = Some(top_p);
923        self
924    }
925
926    /// Set the default top_k for LLM requests.
927    pub fn top_k(mut self, top_k: i32) -> Self {
928        self.generate_content_config
929            .get_or_insert(adk_core::GenerateContentConfig::default())
930            .top_k = Some(top_k);
931        self
932    }
933
934    /// Set the default max output tokens for LLM requests.
935    pub fn max_output_tokens(mut self, max_tokens: i32) -> Self {
936        self.generate_content_config
937            .get_or_insert(adk_core::GenerateContentConfig::default())
938            .max_output_tokens = Some(max_tokens);
939        self
940    }
941
942    /// Set the maximum number of LLM round-trips (iterations) before the agent stops.
943    /// Default is 100.
944    pub fn max_iterations(mut self, max: u32) -> Self {
945        self.max_iterations = max;
946        self
947    }
948
949    /// Set the timeout for individual tool executions.
950    /// Default is 5 minutes. Tools that exceed this timeout will return an error.
951    pub fn tool_timeout(mut self, timeout: std::time::Duration) -> Self {
952        self.tool_timeout = timeout;
953        self
954    }
955
956    /// Add a tool to this agent's toolbox.
957    pub fn tool(mut self, tool: Arc<dyn Tool>) -> Self {
958        self.tools.push(tool);
959        self
960    }
961
962    /// Register a dynamic toolset for per-invocation tool resolution.
963    ///
964    /// Toolsets are resolved at the start of each `run()` call using the
965    /// invocation's `ReadonlyContext`. This enables context-dependent tools
966    /// like per-user browser sessions from a pool.
967    pub fn toolset(mut self, toolset: Arc<dyn Toolset>) -> Self {
968        self.toolsets.push(toolset);
969        self
970    }
971
972    /// Add a sub-agent that this agent can delegate to.
973    pub fn sub_agent(mut self, agent: Arc<dyn Agent>) -> Self {
974        self.sub_agents.push(agent);
975        self
976    }
977
978    /// Add a before-agent callback.
979    pub fn before_callback(mut self, callback: BeforeAgentCallback) -> Self {
980        self.before_callbacks.push(callback);
981        self
982    }
983
984    /// Add an after-agent callback.
985    pub fn after_callback(mut self, callback: AfterAgentCallback) -> Self {
986        self.after_callbacks.push(callback);
987        self
988    }
989
990    /// Add a before-model callback invoked before each LLM request.
991    pub fn before_model_callback(mut self, callback: BeforeModelCallback) -> Self {
992        self.before_model_callbacks.push(callback);
993        self
994    }
995
996    /// Add an after-model callback invoked after each LLM response.
997    pub fn after_model_callback(mut self, callback: AfterModelCallback) -> Self {
998        self.after_model_callbacks.push(callback);
999        self
1000    }
1001
1002    /// Add a before-tool callback invoked before each tool execution.
1003    pub fn before_tool_callback(mut self, callback: BeforeToolCallback) -> Self {
1004        self.before_tool_callbacks.push(callback);
1005        self
1006    }
1007
1008    /// Add an after-tool callback invoked after each tool execution.
1009    pub fn after_tool_callback(mut self, callback: AfterToolCallback) -> Self {
1010        self.after_tool_callbacks.push(callback);
1011        self
1012    }
1013
1014    /// Register a rich after-tool callback that receives the tool, arguments,
1015    /// and response value.
1016    ///
1017    /// This is the V2 callback surface aligned with the Python/Go ADK model
1018    /// where `after_tool_callback` receives the full tool execution context.
1019    /// Unlike [`after_tool_callback`](Self::after_tool_callback) (which only
1020    /// receives `CallbackContext`), this callback can inspect and modify tool
1021    /// results directly.
1022    ///
1023    /// Return `Ok(None)` to keep the original response, or `Ok(Some(value))`
1024    /// to replace the function response sent to the LLM.
1025    ///
1026    /// These callbacks run after the legacy `after_tool_callback` chain.
1027    /// `ToolOutcome` is available via `ctx.tool_outcome()`.
1028    pub fn after_tool_callback_full(mut self, callback: AfterToolCallbackFull) -> Self {
1029        self.after_tool_callbacks_full.push(callback);
1030        self
1031    }
1032
1033    /// Register a callback invoked when a tool execution fails
1034    /// (after retries are exhausted).
1035    ///
1036    /// If the callback returns `Ok(Some(value))`, the value is used as a
1037    /// fallback function response to the LLM. If it returns `Ok(None)`,
1038    /// the next callback in the chain is tried. If no callback provides a
1039    /// fallback, the original error is reported to the LLM.
1040    pub fn on_tool_error(mut self, callback: OnToolErrorCallback) -> Self {
1041        self.on_tool_error_callbacks.push(callback);
1042        self
1043    }
1044
1045    /// Set a default retry budget applied to all tools that do not have
1046    /// a per-tool override.
1047    ///
1048    /// When a tool execution fails and a retry budget applies, the agent
1049    /// retries up to `budget.max_retries` times with the configured delay
1050    /// between attempts.
1051    pub fn default_retry_budget(mut self, budget: RetryBudget) -> Self {
1052        self.default_retry_budget = Some(budget);
1053        self
1054    }
1055
1056    /// Set a per-tool retry budget that overrides the default for the
1057    /// named tool.
1058    ///
1059    /// Per-tool budgets take precedence over the default retry budget.
1060    pub fn tool_retry_budget(mut self, tool_name: impl Into<String>, budget: RetryBudget) -> Self {
1061        self.tool_retry_budgets.insert(tool_name.into(), budget);
1062        self
1063    }
1064
1065    /// Configure a circuit breaker that temporarily disables tools after
1066    /// `threshold` consecutive failures within a single invocation.
1067    ///
1068    /// When a tool's consecutive failure count reaches the threshold, subsequent
1069    /// calls to that tool are short-circuited with an immediate error response
1070    /// until the next invocation (which resets the state).
1071    pub fn circuit_breaker_threshold(mut self, threshold: u32) -> Self {
1072        self.circuit_breaker_threshold = Some(threshold);
1073        self
1074    }
1075
1076    /// Configure tool confirmation requirements for this agent.
1077    pub fn tool_confirmation_policy(mut self, policy: ToolConfirmationPolicy) -> Self {
1078        self.tool_confirmation_policy = policy;
1079        self
1080    }
1081
1082    /// Require confirmation for a specific tool name.
1083    pub fn require_tool_confirmation(mut self, tool_name: impl Into<String>) -> Self {
1084        self.tool_confirmation_policy = self.tool_confirmation_policy.with_tool(tool_name);
1085        self
1086    }
1087
1088    /// Require confirmation for all tool calls.
1089    pub fn require_tool_confirmation_for_all(mut self) -> Self {
1090        self.tool_confirmation_policy = ToolConfirmationPolicy::Always;
1091        self
1092    }
1093
1094    /// Set the tool execution strategy for this agent.
1095    ///
1096    /// When set, this overrides the `RunConfig`'s `tool_execution_strategy`
1097    /// for this agent's dispatch loop. When `None` (the default), the
1098    /// `RunConfig` value is used. [`ToolExecutionStrategy::Parallel`] is an
1099    /// explicit override that bypasses tool safety metadata, so the caller owns
1100    /// concurrency safety.
1101    pub fn tool_execution_strategy(mut self, strategy: ToolExecutionStrategy) -> Self {
1102        self.tool_execution_strategy = Some(strategy);
1103        self
1104    }
1105
1106    /// Set input guardrails to validate user input before processing.
1107    ///
1108    /// Input guardrails run before the agent processes the request and can:
1109    /// - Block harmful or off-topic content
1110    /// - Redact PII from user input
1111    /// - Enforce input length limits
1112    ///
1113    /// Requires the `guardrails` feature.
1114    pub fn input_guardrails(mut self, guardrails: GuardrailSet) -> Self {
1115        self.input_guardrails = guardrails;
1116        self
1117    }
1118
1119    /// Set output guardrails to validate agent responses.
1120    ///
1121    /// Output guardrails run after the agent generates a response and can:
1122    /// - Enforce JSON schema compliance
1123    /// - Redact PII from responses
1124    /// - Block harmful content in responses
1125    ///
1126    /// Requires the `guardrails` feature.
1127    pub fn output_guardrails(mut self, guardrails: GuardrailSet) -> Self {
1128        self.output_guardrails = guardrails;
1129        self
1130    }
1131
1132    /// Register a single enhanced plugin for fine-grained tool/model call interception.
1133    ///
1134    /// Enhanced plugins can inspect and modify tool arguments, tool results,
1135    /// model requests, and model responses. They execute in priority order
1136    /// (lower priority values execute first).
1137    ///
1138    /// Requires the `enhanced-plugins` feature.
1139    ///
1140    /// # Example
1141    ///
1142    /// ```rust,ignore
1143    /// use std::sync::Arc;
1144    /// use adk_plugin::EnhancedPlugin;
1145    ///
1146    /// let agent = LlmAgentBuilder::new("my-agent")
1147    ///     .model(model)
1148    ///     .enhanced_plugin(Arc::new(MyPlugin::new()))
1149    ///     .build()?;
1150    /// ```
1151    #[cfg(feature = "enhanced-plugins")]
1152    pub fn enhanced_plugin(mut self, plugin: Arc<dyn EnhancedPlugin>) -> Self {
1153        self.enhanced_plugins.push(plugin);
1154        self
1155    }
1156
1157    /// Register multiple enhanced plugins at once.
1158    ///
1159    /// Plugins are sorted by priority when the agent is built. Lower priority
1160    /// values execute first. Same-priority plugins execute in registration order.
1161    ///
1162    /// Requires the `enhanced-plugins` feature.
1163    ///
1164    /// # Example
1165    ///
1166    /// ```rust,ignore
1167    /// use std::sync::Arc;
1168    /// use adk_plugin::EnhancedPlugin;
1169    ///
1170    /// let agent = LlmAgentBuilder::new("my-agent")
1171    ///     .model(model)
1172    ///     .enhanced_plugins(vec![
1173    ///         Arc::new(SecurityPlugin::new()),  // priority = 10
1174    ///         Arc::new(LoggingPlugin::new()),   // priority = 100
1175    ///     ])
1176    ///     .build()?;
1177    /// ```
1178    #[cfg(feature = "enhanced-plugins")]
1179    pub fn enhanced_plugins(mut self, plugins: Vec<Arc<dyn EnhancedPlugin>>) -> Self {
1180        self.enhanced_plugins.extend(plugins);
1181        self
1182    }
1183
1184    /// Attach a sandbox configuration for workspace lifecycle management.
1185    ///
1186    /// When a `SandboxConfig` is attached, the `SandboxRunner` will provision
1187    /// a workspace, bind tools based on enabled capabilities, and manage the
1188    /// session lifecycle. The config does NOT add tools directly to the agent —
1189    /// tool binding is the responsibility of the `SandboxRunner`.
1190    ///
1191    /// When no `SandboxConfig` is attached, the agent behaves identically to
1192    /// its behavior before this feature was introduced.
1193    ///
1194    /// Requires the `sandbox` feature.
1195    ///
1196    /// # Example
1197    ///
1198    /// ```rust,ignore
1199    /// use adk_sandbox::workspace::{SandboxConfig, Capability, Manifest};
1200    /// use std::collections::HashSet;
1201    /// use std::sync::Arc;
1202    /// use std::time::Duration;
1203    ///
1204    /// let config = SandboxConfig {
1205    ///     client: Arc::new(my_client),
1206    ///     manifest: Manifest { entries: vec![] },
1207    ///     capabilities: HashSet::from([Capability::Shell, Capability::Filesystem]),
1208    ///     snapshot_on_stop: true,
1209    ///     session_timeout: Duration::from_secs(600),
1210    ///     command_timeout: Duration::from_secs(120),
1211    /// };
1212    ///
1213    /// let agent = LlmAgentBuilder::new("coding-agent")
1214    ///     .model(model)
1215    ///     .sandbox_config(config)
1216    ///     .build()?;
1217    /// ```
1218    #[cfg(feature = "sandbox")]
1219    pub fn sandbox_config(mut self, config: adk_sandbox::workspace::SandboxConfig) -> Self {
1220        self.sandbox_config = Some(config);
1221        self
1222    }
1223
1224    /// Build the [`LlmAgent`], returning an error if no model was set.
1225    pub fn build(self) -> Result<LlmAgent> {
1226        let model = self.model.ok_or_else(|| adk_core::AdkError::agent("Model is required"))?;
1227
1228        let mut seen_names = std::collections::HashSet::new();
1229        for agent in &self.sub_agents {
1230            if !seen_names.insert(agent.name()) {
1231                return Err(adk_core::AdkError::agent(format!(
1232                    "Duplicate sub-agent name: {}",
1233                    agent.name()
1234                )));
1235            }
1236        }
1237
1238        // Validate: Gemini Interactions API + client-side sandbox tools conflict.
1239        // These provide competing filesystems and would produce nondeterministic behavior.
1240        #[cfg(feature = "sandbox")]
1241        if let Some(ref sandbox_cfg) = self.sandbox_config {
1242            use adk_sandbox::workspace::Capability;
1243            if model.uses_interactions_api()
1244                && (sandbox_cfg.capabilities.contains(&Capability::Shell)
1245                    || sandbox_cfg.capabilities.contains(&Capability::Filesystem))
1246            {
1247                return Err(adk_core::AdkError::new(
1248                    adk_core::ErrorComponent::Agent,
1249                    adk_core::ErrorCategory::InvalidInput,
1250                    "code.gemini_interactions_conflict",
1251                    "Cannot combine Gemini Interactions API (server-managed environment) \
1252                     with client-side sandbox tools (Shell/Filesystem). These provide \
1253                     competing filesystems and would produce nondeterministic behavior. \
1254                     Either disable use_interactions_api or remove sandbox capabilities.",
1255                ));
1256            }
1257        }
1258
1259        // Construct EnhancedPluginManager only when plugins are registered (zero overhead otherwise)
1260        #[cfg(feature = "enhanced-plugins")]
1261        let enhanced_plugin_manager = if self.enhanced_plugins.is_empty() {
1262            None
1263        } else {
1264            Some(Arc::new(EnhancedPluginManager::new(self.enhanced_plugins)))
1265        };
1266
1267        Ok(LlmAgent {
1268            name: self.name,
1269            description: self.description.unwrap_or_default(),
1270            model,
1271            instruction: self.instruction,
1272            instruction_provider: self.instruction_provider,
1273            global_instruction: self.global_instruction,
1274            global_instruction_provider: self.global_instruction_provider,
1275            skills_index: self.skills_index,
1276            skill_policy: self.skill_policy,
1277            max_skill_chars: self.max_skill_chars,
1278            input_schema: self.input_schema,
1279            output_schema: self.output_schema,
1280            output_max_retries: self.output_max_retries,
1281            disallow_transfer_to_parent: self.disallow_transfer_to_parent,
1282            disallow_transfer_to_peers: self.disallow_transfer_to_peers,
1283            include_contents: self.include_contents,
1284            tools: self.tools,
1285            toolsets: self.toolsets,
1286            sub_agents: self.sub_agents,
1287            output_key: self.output_key,
1288            generate_content_config: self.generate_content_config,
1289            max_iterations: self.max_iterations,
1290            tool_timeout: self.tool_timeout,
1291            before_callbacks: Arc::new(self.before_callbacks),
1292            after_callbacks: Arc::new(self.after_callbacks),
1293            before_model_callbacks: Arc::new(self.before_model_callbacks),
1294            after_model_callbacks: Arc::new(self.after_model_callbacks),
1295            before_tool_callbacks: Arc::new(self.before_tool_callbacks),
1296            after_tool_callbacks: Arc::new(self.after_tool_callbacks),
1297            on_tool_error_callbacks: Arc::new(self.on_tool_error_callbacks),
1298            after_tool_callbacks_full: Arc::new(self.after_tool_callbacks_full),
1299            default_retry_budget: self.default_retry_budget,
1300            tool_retry_budgets: self.tool_retry_budgets,
1301            circuit_breaker_threshold: self.circuit_breaker_threshold,
1302            tool_confirmation_policy: self.tool_confirmation_policy,
1303            tool_execution_strategy: self.tool_execution_strategy,
1304            input_guardrails: Arc::new(self.input_guardrails),
1305            output_guardrails: Arc::new(self.output_guardrails),
1306            #[cfg(feature = "enhanced-plugins")]
1307            enhanced_plugin_manager,
1308            #[cfg(feature = "sandbox")]
1309            sandbox_config: self.sandbox_config,
1310        })
1311    }
1312}
1313
1314// AgentToolContext wraps the parent InvocationContext and preserves all context
1315// instead of throwing it away like SimpleToolContext did
1316/// Progress events held in flight for one tool batch.
1317///
1318/// The queue exists to decouple a tool that writes progress from a client that
1319/// reads it. It is bounded so a verbose tool cannot grow the queue without limit
1320/// while the consumer is slow.
1321const TOOL_PROGRESS_CAPACITY: usize = 256;
1322
1323/// How long a tool waits for queue space before its chunk is dropped.
1324///
1325/// A short wait applies real backpressure to a tool that outruns its consumer,
1326/// without letting a stalled consumer block the tool indefinitely.
1327const TOOL_PROGRESS_SEND_TIMEOUT: std::time::Duration = std::time::Duration::from_millis(100);
1328
1329/// Largest single progress chunk forwarded, in bytes.
1330const TOOL_PROGRESS_MAX_CHUNK_BYTES: usize = 8 * 1024;
1331
1332/// Total progress bytes forwarded for one tool call.
1333const TOOL_PROGRESS_MAX_TOTAL_BYTES: usize = 1024 * 1024;
1334
1335/// Text appended in place of progress output that was not forwarded.
1336const TOOL_PROGRESS_TRUNCATION_MARKER: &str = "[adk: tool progress truncated]";
1337
1338/// Truncates `text` to at most `max_bytes`, never splitting a character.
1339fn truncate_on_char_boundary(text: &str, max_bytes: usize) -> &str {
1340    if text.len() <= max_bytes {
1341        return text;
1342    }
1343    let mut end = max_bytes;
1344    while end > 0 && !text.is_char_boundary(end) {
1345        end -= 1;
1346    }
1347    &text[..end]
1348}
1349
1350struct AgentToolContext {
1351    parent_ctx: Arc<dyn InvocationContext>,
1352    function_call_id: String,
1353    /// The tool this context was built for, recorded by the dispatcher so secret
1354    /// requests carry an identity the tool cannot choose.
1355    tool_name: Option<String>,
1356    actions: Mutex<EventActions>,
1357    progress_tx: Option<tokio::sync::mpsc::Sender<Event>>,
1358    /// Progress bytes forwarded so far for this call.
1359    progress_bytes: std::sync::atomic::AtomicUsize,
1360    /// Set once the truncation marker has been emitted, so it is emitted once.
1361    progress_truncated: std::sync::atomic::AtomicBool,
1362}
1363
1364impl AgentToolContext {
1365    fn new(parent_ctx: Arc<dyn InvocationContext>, function_call_id: String) -> Self {
1366        Self {
1367            parent_ctx,
1368            function_call_id,
1369            tool_name: None,
1370            actions: Mutex::new(EventActions::default()),
1371            progress_tx: None,
1372            progress_bytes: std::sync::atomic::AtomicUsize::new(0),
1373            progress_truncated: std::sync::atomic::AtomicBool::new(false),
1374        }
1375    }
1376
1377    /// Record which tool this context serves.
1378    fn with_tool_name(mut self, tool_name: impl Into<String>) -> Self {
1379        self.tool_name = Some(tool_name.into());
1380        self
1381    }
1382
1383    /// Attach a progress sink so [`ToolContext::emit_progress`] forwards chunks
1384    /// as partial [`Event`]s onto the agent's `EventStream`.
1385    fn with_progress(mut self, tx: tokio::sync::mpsc::Sender<Event>) -> Self {
1386        self.progress_tx = Some(tx);
1387        self
1388    }
1389
1390    /// Builds a described secret access from the identity the framework holds.
1391    ///
1392    /// The tool name comes from the dispatch record rather than from anything the tool
1393    /// supplied, so one tool cannot request a secret under another tool's identity.
1394    async fn request_secret(&self, name: &str, purpose: Option<&str>) -> Result<Option<String>> {
1395        let mut request = adk_core::SecretRequest::new(name)
1396            .with_identity(
1397                self.parent_ctx.app_name(),
1398                self.parent_ctx.user_id(),
1399                self.parent_ctx.session_id(),
1400            )
1401            .with_invocation_id(self.parent_ctx.invocation_id());
1402        if let Some(tool_name) = &self.tool_name {
1403            request = request.with_tool_name(tool_name);
1404        }
1405        if let Some(purpose) = purpose {
1406            request = request.with_purpose(purpose);
1407        }
1408        self.parent_ctx.get_secret_for(&request).await
1409    }
1410
1411    /// Forwards one progress chunk under this call's budget.
1412    ///
1413    /// The policy is bounded and lossy by design: memory is capped, and output that
1414    /// does not fit is replaced by a single truncation marker rather than stalling
1415    /// the tool or growing without limit. A tool that outruns its consumer waits
1416    /// briefly, which slows the tool rather than the whole run.
1417    async fn forward_progress(
1418        &self,
1419        tx: &tokio::sync::mpsc::Sender<Event>,
1420        stream: &str,
1421        chunk: &str,
1422    ) {
1423        use std::sync::atomic::Ordering;
1424
1425        if self.progress_truncated.load(Ordering::Relaxed) {
1426            return;
1427        }
1428
1429        // Cap one chunk, then cap the call. Truncation respects char boundaries so
1430        // multi-byte text is never split mid-character.
1431        let payload = truncate_on_char_boundary(chunk, TOOL_PROGRESS_MAX_CHUNK_BYTES);
1432        let forwarded = self.progress_bytes.fetch_add(payload.len(), Ordering::Relaxed);
1433        if forwarded.saturating_add(payload.len()) > TOOL_PROGRESS_MAX_TOTAL_BYTES {
1434            self.mark_progress_truncated(tx, stream).await;
1435            return;
1436        }
1437
1438        let event = Event::tool_progress(
1439            self.parent_ctx.invocation_id(),
1440            self.parent_ctx.agent_name(),
1441            &self.function_call_id,
1442            stream,
1443            payload,
1444        );
1445
1446        match tx.try_send(event) {
1447            Ok(()) => {}
1448            Err(tokio::sync::mpsc::error::TrySendError::Full(event)) => {
1449                // Wait briefly for the consumer to catch up, then give up on this
1450                // chunk so a stalled consumer cannot block the tool.
1451                match tokio::time::timeout(TOOL_PROGRESS_SEND_TIMEOUT, tx.send(event)).await {
1452                    Ok(Ok(())) => {}
1453                    Ok(Err(_)) => {}
1454                    Err(_) => self.mark_progress_truncated(tx, stream).await,
1455                }
1456            }
1457            Err(tokio::sync::mpsc::error::TrySendError::Closed(_)) => {}
1458        }
1459    }
1460
1461    /// Emits the truncation marker once for this call.
1462    async fn mark_progress_truncated(&self, tx: &tokio::sync::mpsc::Sender<Event>, stream: &str) {
1463        use std::sync::atomic::Ordering;
1464        if self.progress_truncated.swap(true, Ordering::Relaxed) {
1465            return;
1466        }
1467        tracing::warn!(
1468            function_call.id = %self.function_call_id,
1469            progress.stream = %stream,
1470            "tool progress exceeded its budget, remaining output is not forwarded"
1471        );
1472        let marker = Event::tool_progress(
1473            self.parent_ctx.invocation_id(),
1474            self.parent_ctx.agent_name(),
1475            &self.function_call_id,
1476            stream,
1477            TOOL_PROGRESS_TRUNCATION_MARKER,
1478        );
1479        let _ = tx.try_send(marker);
1480    }
1481
1482    fn actions_guard(&self) -> std::sync::MutexGuard<'_, EventActions> {
1483        self.actions.lock().unwrap_or_else(|e| e.into_inner())
1484    }
1485}
1486
1487#[async_trait]
1488impl ReadonlyContext for AgentToolContext {
1489    fn invocation_id(&self) -> &str {
1490        self.parent_ctx.invocation_id()
1491    }
1492
1493    fn agent_name(&self) -> &str {
1494        self.parent_ctx.agent_name()
1495    }
1496
1497    fn user_id(&self) -> &str {
1498        // ✅ Delegate to parent - now tools get the real user_id!
1499        self.parent_ctx.user_id()
1500    }
1501
1502    fn app_name(&self) -> &str {
1503        // ✅ Delegate to parent - now tools get the real app_name!
1504        self.parent_ctx.app_name()
1505    }
1506
1507    fn session_id(&self) -> &str {
1508        // ✅ Delegate to parent - now tools get the real session_id!
1509        self.parent_ctx.session_id()
1510    }
1511
1512    fn branch(&self) -> &str {
1513        self.parent_ctx.branch()
1514    }
1515
1516    fn user_content(&self) -> &Content {
1517        self.parent_ctx.user_content()
1518    }
1519}
1520
1521#[async_trait]
1522impl CallbackContext for AgentToolContext {
1523    fn artifacts(&self) -> Option<Arc<dyn adk_core::Artifacts>> {
1524        // ✅ Delegate to parent - tools can now access artifacts!
1525        self.parent_ctx.artifacts()
1526    }
1527
1528    fn shared_state(&self) -> Option<Arc<adk_core::SharedState>> {
1529        self.parent_ctx.shared_state()
1530    }
1531}
1532
1533#[async_trait]
1534impl ToolContext for AgentToolContext {
1535    fn function_call_id(&self) -> &str {
1536        &self.function_call_id
1537    }
1538
1539    fn actions(&self) -> EventActions {
1540        self.actions_guard().clone()
1541    }
1542
1543    fn set_actions(&self, actions: EventActions) {
1544        *self.actions_guard() = actions;
1545    }
1546
1547    async fn search_memory(&self, query: &str) -> Result<Vec<MemoryEntry>> {
1548        // ✅ Delegate to parent's memory if available
1549        if let Some(memory) = self.parent_ctx.memory() {
1550            memory.search(query).await
1551        } else {
1552            Ok(vec![])
1553        }
1554    }
1555
1556    fn user_scopes(&self) -> Vec<String> {
1557        self.parent_ctx.user_scopes()
1558    }
1559
1560    async fn get_secret(&self, name: &str) -> Result<Option<String>> {
1561        self.request_secret(name, None).await
1562    }
1563
1564    async fn get_secret_for_purpose(&self, name: &str, purpose: &str) -> Result<Option<String>> {
1565        self.request_secret(name, Some(purpose)).await
1566    }
1567
1568    async fn emit_progress(&self, stream: &str, chunk: &str) {
1569        // Primary path: forward as a partial Event on the agent's EventStream so
1570        // UIs consume tool progress through the same channel as everything else.
1571        if let Some(tx) = &self.progress_tx {
1572            // A closed receiver means nobody is listening, so stop building events.
1573            if !tx.is_closed() {
1574                self.forward_progress(tx, stream, chunk).await;
1575            }
1576        }
1577        // Secondary path: structured trace for log-based observability.
1578        tracing::debug!(
1579            target: "adk_agent::tool_progress",
1580            tool_call_id = %self.function_call_id,
1581            stream = %stream,
1582            "{chunk}",
1583        );
1584    }
1585}
1586
1587/// Wrapper that adds ToolOutcome to an existing CallbackContext.
1588/// Used only during after-tool callback invocation so callbacks
1589/// can inspect structured metadata about the completed tool execution.
1590struct ToolOutcomeCallbackContext {
1591    inner: Arc<dyn CallbackContext>,
1592    outcome: ToolOutcome,
1593}
1594
1595#[async_trait]
1596impl ReadonlyContext for ToolOutcomeCallbackContext {
1597    fn invocation_id(&self) -> &str {
1598        self.inner.invocation_id()
1599    }
1600
1601    fn agent_name(&self) -> &str {
1602        self.inner.agent_name()
1603    }
1604
1605    fn user_id(&self) -> &str {
1606        self.inner.user_id()
1607    }
1608
1609    fn app_name(&self) -> &str {
1610        self.inner.app_name()
1611    }
1612
1613    fn session_id(&self) -> &str {
1614        self.inner.session_id()
1615    }
1616
1617    fn branch(&self) -> &str {
1618        self.inner.branch()
1619    }
1620
1621    fn user_content(&self) -> &Content {
1622        self.inner.user_content()
1623    }
1624}
1625
1626#[async_trait]
1627impl CallbackContext for ToolOutcomeCallbackContext {
1628    fn artifacts(&self) -> Option<Arc<dyn adk_core::Artifacts>> {
1629        self.inner.artifacts()
1630    }
1631
1632    fn tool_outcome(&self) -> Option<ToolOutcome> {
1633        Some(self.outcome.clone())
1634    }
1635}
1636
1637/// Per-invocation circuit breaker state.
1638///
1639/// Tracks consecutive failures per tool name within a single agent
1640/// invocation. When a tool's consecutive failure count reaches the
1641/// configured threshold the breaker "opens" and subsequent calls to
1642/// that tool are short-circuited with an immediate error response.
1643///
1644/// The state is created fresh at the start of each `run()` call so
1645/// it automatically resets between invocations.
1646struct CircuitBreakerState {
1647    threshold: u32,
1648    /// tool_name → consecutive failure count
1649    failures: std::collections::HashMap<String, u32>,
1650}
1651
1652impl CircuitBreakerState {
1653    fn new(threshold: u32) -> Self {
1654        Self { threshold, failures: std::collections::HashMap::new() }
1655    }
1656
1657    /// Returns `true` if the tool is currently tripped (open state).
1658    fn is_open(&self, tool_name: &str) -> bool {
1659        self.failures.get(tool_name).copied().unwrap_or(0) >= self.threshold
1660    }
1661
1662    /// Record a tool outcome. Resets count on success, increments on failure.
1663    fn record(&mut self, outcome: &ToolOutcome) {
1664        if outcome.success {
1665            self.failures.remove(&outcome.tool_name);
1666        } else {
1667            let count = self.failures.entry(outcome.tool_name.clone()).or_insert(0);
1668            *count += 1;
1669        }
1670    }
1671}
1672
1673struct ToolExecutionResult {
1674    index: usize,
1675    content: Content,
1676    actions: EventActions,
1677    escalate_or_skip: bool,
1678}
1679
1680struct ToolExecutor<'a> {
1681    ctx: Arc<dyn InvocationContext>,
1682    tool_map: &'a std::collections::HashMap<String, Arc<dyn Tool>>,
1683    tool_retry_budgets: &'a std::collections::HashMap<String, RetryBudget>,
1684    default_retry_budget: &'a Option<RetryBudget>,
1685    before_tool_callbacks: &'a Arc<Vec<BeforeToolCallback>>,
1686    after_tool_callbacks: &'a Arc<Vec<AfterToolCallback>>,
1687    after_tool_callbacks_full: &'a Arc<Vec<AfterToolCallbackFull>>,
1688    on_tool_error_callbacks: &'a Arc<Vec<OnToolErrorCallback>>,
1689    tool_confirmation_policy: &'a ToolConfirmationPolicy,
1690    cb_mutex: &'a std::sync::Mutex<Option<CircuitBreakerState>>,
1691    invocation_id: &'a str,
1692    concurrency_manager: &'a adk_core::ToolConcurrencyManager,
1693    progress_tx: tokio::sync::mpsc::Sender<Event>,
1694    tool_timeout: std::time::Duration,
1695    confirmation_decisions: &'a std::collections::HashMap<String, ToolConfirmationDecision>,
1696    confirmation_fingerprints: &'a std::collections::HashMap<String, String>,
1697    live_confirmation_decisions: &'a std::collections::HashMap<String, ToolConfirmationDecision>,
1698    #[cfg(feature = "enhanced-plugins")]
1699    enhanced_plugin_manager: &'a Option<Arc<EnhancedPluginManager>>,
1700}
1701
1702impl ToolExecutor<'_> {
1703    async fn execute(&self, call: PendingToolCall) -> ToolExecutionResult {
1704        let PendingToolCall { index, name, args, id, function_call_id } = call;
1705        let mut tool_actions = EventActions::default();
1706        let mut response_content: Option<Content> = None;
1707        let mut run_after_tool_callbacks = true;
1708        let mut tool_outcome_for_callback: Option<ToolOutcome> = None;
1709        let mut executed_tool: Option<Arc<dyn Tool>> = None;
1710        let mut executed_tool_response: Option<serde_json::Value> = None;
1711
1712        // Acquire concurrency permit before tool execution.
1713        // The permit is held for the entire duration of this tool call
1714        // and released on drop when this async block completes.
1715        let _concurrency_permit = match self.concurrency_manager.acquire(&name).await {
1716            Ok(permit) => Some(permit),
1717            Err(e) => {
1718                // Concurrency limit reached with Fail policy — return error
1719                let error_content = Content {
1720                    role: "function".to_string(),
1721                    parts: vec![Part::FunctionResponse {
1722                        function_response: FunctionResponseData::new(
1723                            name.clone(),
1724                            serde_json::json!({ "error": e.to_string() }),
1725                        ),
1726                        id: id.clone(),
1727                        annotations: None,
1728                    }],
1729                };
1730                return ToolExecutionResult {
1731                    index,
1732                    content: error_content,
1733                    actions: tool_actions,
1734                    escalate_or_skip: false,
1735                };
1736            }
1737        };
1738
1739        // Tool confirmation (deny case; None handled by pre-check)
1740        if self.tool_confirmation_policy.requires_confirmation(&name) {
1741            match self.live_confirmation_decisions.get(&function_call_id).copied().or_else(|| {
1742                static_confirmation_decision(
1743                    self.confirmation_decisions,
1744                    self.confirmation_fingerprints,
1745                    &function_call_id,
1746                    &name,
1747                    &args,
1748                )
1749            }) {
1750                Some(ToolConfirmationDecision::Approve) => {
1751                    tool_actions.tool_confirmation_decision =
1752                        Some(ToolConfirmationDecision::Approve);
1753                }
1754                Some(ToolConfirmationDecision::Deny) => {
1755                    tool_actions.tool_confirmation_decision = Some(ToolConfirmationDecision::Deny);
1756                    response_content = Some(Content {
1757                        role: "function".to_string(),
1758                        parts: vec![Part::FunctionResponse {
1759                            function_response: FunctionResponseData::new(
1760                                name.clone(),
1761                                serde_json::json!({
1762                                    "error": format!("Tool '{}' execution denied by confirmation policy", name)
1763                                }),
1764                            ),
1765                            id: id.clone(),
1766                            annotations: None,
1767                        }],
1768                    });
1769                    run_after_tool_callbacks = false;
1770                }
1771                None => {
1772                    response_content = Some(Content {
1773                        role: "function".to_string(),
1774                        parts: vec![Part::FunctionResponse {
1775                            function_response: FunctionResponseData::new(
1776                                name.clone(),
1777                                serde_json::json!({
1778                                    "error": format!("Tool '{}' requires confirmation", name)
1779                                }),
1780                            ),
1781                            id: id.clone(),
1782                            annotations: None,
1783                        }],
1784                    });
1785                    run_after_tool_callbacks = false;
1786                }
1787            }
1788        }
1789
1790        // Before-tool callbacks
1791        // Track potentially modified args for enhanced plugin after-hook
1792        #[allow(unused_mut)]
1793        let mut final_args = args.clone();
1794
1795        // ===== ENHANCED PLUGIN: BEFORE TOOL CALL =====
1796        #[cfg(feature = "enhanced-plugins")]
1797        if response_content.is_none()
1798            && let Some(epm) = self.enhanced_plugin_manager.as_ref()
1799            && let Some(tool_ref) = self.tool_map.get(&name)
1800        {
1801            match epm
1802                .run_before_tool_call(
1803                    tool_ref.clone(),
1804                    final_args.clone(),
1805                    self.ctx.clone() as Arc<dyn CallbackContext>,
1806                )
1807                .await
1808            {
1809                Ok(BeforeToolCallResult::Continue(modified_args)) => {
1810                    final_args = modified_args;
1811                }
1812                Ok(BeforeToolCallResult::ShortCircuit(synthetic_result)) => {
1813                    // Short-circuit: use synthetic result, skip tool execution
1814                    response_content = Some(Content {
1815                        role: "function".to_string(),
1816                        parts: vec![Part::FunctionResponse {
1817                            function_response: FunctionResponseData::from_tool_result(
1818                                name.clone(),
1819                                synthetic_result,
1820                            ),
1821                            id: id.clone(),
1822                            annotations: None,
1823                        }],
1824                    });
1825                    executed_tool = Some(tool_ref.clone());
1826                }
1827                Err(e) => {
1828                    response_content = Some(Content {
1829                        role: "function".to_string(),
1830                        parts: vec![Part::FunctionResponse {
1831                            function_response: FunctionResponseData::new(
1832                                name.clone(),
1833                                serde_json::json!({ "error": e.to_string() }),
1834                            ),
1835                            id: id.clone(),
1836                            annotations: None,
1837                        }],
1838                    });
1839                    run_after_tool_callbacks = false;
1840                }
1841            }
1842        }
1843
1844        if response_content.is_none() {
1845            let tool_ctx = Arc::new(ToolCallbackContext::new(
1846                self.ctx.clone(),
1847                name.clone(),
1848                final_args.clone(),
1849            ));
1850            for callback in self.before_tool_callbacks.as_ref() {
1851                match callback(tool_ctx.clone() as Arc<dyn CallbackContext>).await {
1852                    Ok(Some(c)) => {
1853                        response_content = Some(c);
1854                        break;
1855                    }
1856                    Ok(None) => continue,
1857                    Err(e) => {
1858                        response_content = Some(Content {
1859                            role: "function".to_string(),
1860                            parts: vec![Part::FunctionResponse {
1861                                function_response: FunctionResponseData::new(
1862                                    name.clone(),
1863                                    serde_json::json!({ "error": e.to_string() }),
1864                                ),
1865                                id: id.clone(),
1866                                annotations: None,
1867                            }],
1868                        });
1869                        run_after_tool_callbacks = false;
1870                        break;
1871                    }
1872                }
1873            }
1874        }
1875
1876        // Circuit breaker check
1877        if response_content.is_none() {
1878            let guard = self.cb_mutex.lock().unwrap_or_else(|e| e.into_inner());
1879            if let Some(ref cb_state) = *guard
1880                && cb_state.is_open(&name)
1881            {
1882                let msg = format!(
1883                    "Tool '{}' is temporarily disabled after {} consecutive failures",
1884                    name, cb_state.threshold
1885                );
1886                tracing::warn!(tool.name = %name, "circuit breaker open, skipping tool execution");
1887                response_content = Some(Content {
1888                    role: "function".to_string(),
1889                    parts: vec![Part::FunctionResponse {
1890                        function_response: FunctionResponseData::new(
1891                            name.clone(),
1892                            serde_json::json!({ "error": msg }),
1893                        ),
1894                        id: id.clone(),
1895                        annotations: None,
1896                    }],
1897                });
1898                run_after_tool_callbacks = false;
1899            }
1900            drop(guard);
1901        }
1902
1903        // Execute tool with retry budget and tracing
1904        if response_content.is_none() {
1905            if let Some(tool) = self.tool_map.get(&name) {
1906                let tool_ctx: Arc<dyn ToolContext> = Arc::new(
1907                    AgentToolContext::new(self.ctx.clone(), function_call_id.clone())
1908                        .with_tool_name(tool.name())
1909                        .with_progress(self.progress_tx.clone()),
1910                );
1911                let span_name = format!("execute_tool {name}");
1912                let tool_span = tracing::info_span!(
1913                    "",
1914                    otel.name = %span_name,
1915                    tool.name = %name,
1916                    "gcp.vertex.agent.event_id" = %format!("{}_{}", self.invocation_id, name),
1917                    "gcp.vertex.agent.invocation_id" = %self.invocation_id,
1918                    "gcp.vertex.agent.session_id" = %self.ctx.session_id(),
1919                    "gen_ai.conversation.id" = %self.ctx.session_id()
1920                );
1921
1922                let budget =
1923                    self.tool_retry_budgets.get(&name).or(self.default_retry_budget.as_ref());
1924                let max_attempts = budget.map(|b| b.max_retries + 1).unwrap_or(1);
1925                let retry_delay = budget.map(|b| b.delay).unwrap_or_default();
1926
1927                let tool_clone = tool.clone();
1928                let tool_start = std::time::Instant::now();
1929                let mut last_error = String::new();
1930                let mut final_attempt: u32 = 0;
1931                let mut retry_result: Option<serde_json::Value> = None;
1932
1933                for attempt in 0..max_attempts {
1934                    final_attempt = attempt;
1935                    if attempt > 0 {
1936                        tokio::time::sleep(retry_delay).await;
1937                    }
1938                    match async {
1939                        let args_payload = trace_json_payload(
1940                            &final_args,
1941                            self.ctx.run_config().record_payloads,
1942                            self.ctx.run_config().trace_payload_max_bytes,
1943                        );
1944                        tracing::debug!(tool.name = %name, tool.args = %args_payload, attempt = attempt, "tool_call");
1945                        let exec_future = tool_clone.execute(tool_ctx.clone(), final_args.clone());
1946                        let unwind_safe_future = std::panic::AssertUnwindSafe(
1947                            tokio::time::timeout(self.tool_timeout, exec_future),
1948                        );
1949                        match futures::FutureExt::catch_unwind(unwind_safe_future).await {
1950                            Ok(result) => result,
1951                            Err(_panic) => Ok(Err(adk_core::AdkError::tool(format!(
1952                                "tool '{}' panicked during execution",
1953                                name
1954                            )))),
1955                        }
1956                    }
1957                    .instrument(tool_span.clone())
1958                    .await
1959                    {
1960                        Ok(Ok(value)) => {
1961                            let result_payload = trace_json_payload(
1962                                &value,
1963                                self.ctx.run_config().record_payloads,
1964                                self.ctx.run_config().trace_payload_max_bytes,
1965                            );
1966                            tracing::debug!(tool.name = %name, tool.result = %result_payload, "tool_result");
1967                            retry_result = Some(value);
1968                            break;
1969                        }
1970                        Ok(Err(e)) => {
1971                            last_error = e.to_string();
1972                            if attempt + 1 < max_attempts {
1973                                tracing::warn!(tool.name = %name, attempt = attempt, error = %last_error, "tool execution failed, retrying");
1974                            } else {
1975                                tracing::warn!(tool.name = %name, error = %last_error, "tool_error");
1976                            }
1977                        }
1978                        Err(_) => {
1979                            last_error = format!(
1980                                "Tool '{}' timed out after {} seconds",
1981                                name,
1982                                self.tool_timeout.as_secs()
1983                            );
1984                            if attempt + 1 < max_attempts {
1985                                tracing::warn!(tool.name = %name, attempt = attempt, timeout_secs = self.tool_timeout.as_secs(), "tool timed out, retrying");
1986                            } else {
1987                                tracing::warn!(tool.name = %name, timeout_secs = self.tool_timeout.as_secs(), "tool_timeout");
1988                            }
1989                        }
1990                    }
1991                }
1992
1993                let tool_duration = tool_start.elapsed();
1994                let (tool_success, tool_error_message, function_response) = match retry_result {
1995                    Some(value) => (true, None, value),
1996                    None => (
1997                        false,
1998                        Some(last_error.clone()),
1999                        serde_json::json!({ "error": last_error }),
2000                    ),
2001                };
2002
2003                let outcome = ToolOutcome {
2004                    tool_name: name.clone(),
2005                    tool_args: final_args.clone(),
2006                    success: tool_success,
2007                    duration: tool_duration,
2008                    error_message: tool_error_message.clone(),
2009                    attempt: final_attempt,
2010                };
2011                tool_outcome_for_callback = Some(outcome);
2012
2013                // Circuit breaker recording
2014                {
2015                    let mut guard = self.cb_mutex.lock().unwrap_or_else(|e| e.into_inner());
2016                    if let Some(ref mut cb_state) = *guard {
2017                        cb_state.record(tool_outcome_for_callback.as_ref().unwrap());
2018                    }
2019                }
2020
2021                // On-tool-error callbacks
2022                let final_function_response = if !tool_success {
2023                    let mut fallback_result = None;
2024                    let error_msg = tool_error_message.clone().unwrap_or_default();
2025                    for callback in self.on_tool_error_callbacks.as_ref() {
2026                        match callback(
2027                            self.ctx.clone() as Arc<dyn CallbackContext>,
2028                            tool.clone(),
2029                            final_args.clone(),
2030                            error_msg.clone(),
2031                        )
2032                        .await
2033                        {
2034                            Ok(Some(result)) => {
2035                                fallback_result = Some(result);
2036                                break;
2037                            }
2038                            Ok(None) => continue,
2039                            Err(e) => {
2040                                tracing::warn!(error = %e, "on_tool_error callback failed");
2041                                break;
2042                            }
2043                        }
2044                    }
2045                    fallback_result.unwrap_or(function_response)
2046                } else {
2047                    function_response
2048                };
2049
2050                let confirmation_decision = tool_actions.tool_confirmation_decision;
2051                tool_actions = tool_ctx.actions();
2052                if tool_actions.tool_confirmation_decision.is_none() {
2053                    tool_actions.tool_confirmation_decision = confirmation_decision;
2054                }
2055                executed_tool = Some(tool.clone());
2056                executed_tool_response = Some(final_function_response.clone());
2057                response_content = Some(Content {
2058                    role: "function".to_string(),
2059                    parts: vec![Part::FunctionResponse {
2060                        function_response: FunctionResponseData::from_tool_result(
2061                            name.clone(),
2062                            final_function_response,
2063                        ),
2064                        id: id.clone(),
2065                        annotations: None,
2066                    }],
2067                });
2068            } else {
2069                response_content = Some(Content {
2070                    role: "function".to_string(),
2071                    parts: vec![Part::FunctionResponse {
2072                        function_response: FunctionResponseData::new(
2073                            name.clone(),
2074                            serde_json::json!({
2075                                "error": format!("Tool {} not found", name)
2076                            }),
2077                        ),
2078                        id: id.clone(),
2079                        annotations: None,
2080                    }],
2081                });
2082            }
2083        }
2084
2085        // After-tool callbacks
2086        let mut response_content = response_content.expect("tool response content is set");
2087        if run_after_tool_callbacks {
2088            let outcome_ctx: Arc<dyn CallbackContext> = match tool_outcome_for_callback {
2089                Some(outcome) => Arc::new(ToolOutcomeCallbackContext {
2090                    inner: self.ctx.clone() as Arc<dyn CallbackContext>,
2091                    outcome,
2092                }),
2093                None => self.ctx.clone() as Arc<dyn CallbackContext>,
2094            };
2095            let cb_ctx: Arc<dyn CallbackContext> =
2096                Arc::new(ToolCallbackContext::new(outcome_ctx, name.clone(), final_args.clone()));
2097            for callback in self.after_tool_callbacks.as_ref() {
2098                match callback(cb_ctx.clone()).await {
2099                    Ok(Some(modified)) => {
2100                        response_content = modified;
2101                        break;
2102                    }
2103                    Ok(None) => continue,
2104                    Err(e) => {
2105                        response_content = Content {
2106                            role: "function".to_string(),
2107                            parts: vec![Part::FunctionResponse {
2108                                function_response: FunctionResponseData::new(
2109                                    name.clone(),
2110                                    serde_json::json!({ "error": e.to_string() }),
2111                                ),
2112                                id: id.clone(),
2113                                annotations: None,
2114                            }],
2115                        };
2116                        break;
2117                    }
2118                }
2119            }
2120            if let (Some(tool_ref), Some(tool_resp)) = (&executed_tool, executed_tool_response) {
2121                for callback in self.after_tool_callbacks_full.as_ref() {
2122                    match callback(
2123                        cb_ctx.clone(),
2124                        tool_ref.clone(),
2125                        final_args.clone(),
2126                        tool_resp.clone(),
2127                    )
2128                    .await
2129                    {
2130                        Ok(Some(modified_value)) => {
2131                            response_content = Content {
2132                                role: "function".to_string(),
2133                                parts: vec![Part::FunctionResponse {
2134                                    function_response: FunctionResponseData::from_tool_result(
2135                                        name.clone(),
2136                                        modified_value,
2137                                    ),
2138                                    id: id.clone(),
2139                                    annotations: None,
2140                                }],
2141                            };
2142                            break;
2143                        }
2144                        Ok(None) => continue,
2145                        Err(e) => {
2146                            response_content = Content {
2147                                role: "function".to_string(),
2148                                parts: vec![Part::FunctionResponse {
2149                                    function_response: FunctionResponseData::new(
2150                                        name.clone(),
2151                                        serde_json::json!({ "error": e.to_string() }),
2152                                    ),
2153                                    id: id.clone(),
2154                                    annotations: None,
2155                                }],
2156                            };
2157                            break;
2158                        }
2159                    }
2160                }
2161            }
2162
2163            // ===== ENHANCED PLUGIN: AFTER TOOL CALL =====
2164            // Enhanced plugins can modify the tool result after legacy callbacks.
2165            #[cfg(feature = "enhanced-plugins")]
2166            if let Some(epm) = self.enhanced_plugin_manager.as_ref()
2167                && let Some(tool_ref) = &executed_tool
2168            {
2169                // Extract the result value from the response content
2170                let result_value = response_content
2171                    .parts
2172                    .iter()
2173                    .find_map(|p| {
2174                        if let Part::FunctionResponse { function_response, .. } = p {
2175                            Some(function_response.response.clone())
2176                        } else {
2177                            None
2178                        }
2179                    })
2180                    .unwrap_or(serde_json::json!(null));
2181
2182                match epm
2183                    .run_after_tool_call(
2184                        tool_ref.clone(),
2185                        &final_args,
2186                        result_value,
2187                        self.ctx.clone() as Arc<dyn CallbackContext>,
2188                    )
2189                    .await
2190                {
2191                    Ok(adk_plugin::AfterToolCallResult::Continue(modified_result)) => {
2192                        response_content = Content {
2193                            role: "function".to_string(),
2194                            parts: vec![Part::FunctionResponse {
2195                                function_response: FunctionResponseData::from_tool_result(
2196                                    name.clone(),
2197                                    modified_result,
2198                                ),
2199                                id: id.clone(),
2200                                annotations: None,
2201                            }],
2202                        };
2203                    }
2204                    Err(e) => {
2205                        response_content = Content {
2206                            role: "function".to_string(),
2207                            parts: vec![Part::FunctionResponse {
2208                                function_response: FunctionResponseData::new(
2209                                    name.clone(),
2210                                    serde_json::json!({ "error": e.to_string() }),
2211                                ),
2212                                id: id.clone(),
2213                                annotations: None,
2214                            }],
2215                        };
2216                    }
2217                }
2218            }
2219        }
2220
2221        let escalate_or_skip = tool_actions.escalate || tool_actions.skip_summarization;
2222        ToolExecutionResult {
2223            index,
2224            content: response_content,
2225            actions: tool_actions,
2226            escalate_or_skip,
2227        }
2228    }
2229}
2230
2231#[async_trait]
2232impl Agent for LlmAgent {
2233    fn name(&self) -> &str {
2234        &self.name
2235    }
2236
2237    fn description(&self) -> &str {
2238        &self.description
2239    }
2240
2241    fn sub_agents(&self) -> &[Arc<dyn Agent>] {
2242        &self.sub_agents
2243    }
2244
2245    #[adk_telemetry::instrument(
2246        skip(self, ctx),
2247        fields(
2248            agent.name = %self.name,
2249            agent.description = %self.description,
2250            invocation.id = %ctx.invocation_id(),
2251            user.id = %ctx.user_id(),
2252            session.id = %ctx.session_id()
2253        )
2254    )]
2255    async fn run(&self, ctx: Arc<dyn InvocationContext>) -> Result<adk_core::EventStream> {
2256        adk_telemetry::info!("Starting agent execution");
2257        let ctx = Self::apply_input_guardrails(ctx, self.input_guardrails.clone()).await?;
2258
2259        let agent_name = self.name.clone();
2260        let invocation_id = ctx.invocation_id().to_string();
2261        let model = self.model.clone();
2262        let prompt_config = PromptConfig::from_agent(self);
2263        let tool_setup = ToolSetup::from_agent(self);
2264        let output_key = self.output_key.clone();
2265        let output_max_retries = self.output_max_retries;
2266        let generate_content_config = self.generate_content_config.clone();
2267        let max_iterations = self.max_iterations;
2268        let tool_timeout = self.tool_timeout;
2269        // Clone Arc references (cheap)
2270        let before_agent_callbacks = self.before_callbacks.clone();
2271        let after_agent_callbacks = self.after_callbacks.clone();
2272        let before_model_callbacks = self.before_model_callbacks.clone();
2273        let after_model_callbacks = self.after_model_callbacks.clone();
2274        let before_tool_callbacks = self.before_tool_callbacks.clone();
2275        let after_tool_callbacks = self.after_tool_callbacks.clone();
2276        let on_tool_error_callbacks = self.on_tool_error_callbacks.clone();
2277        let after_tool_callbacks_full = self.after_tool_callbacks_full.clone();
2278        let default_retry_budget = self.default_retry_budget.clone();
2279        let tool_retry_budgets = self.tool_retry_budgets.clone();
2280        let circuit_breaker_threshold = self.circuit_breaker_threshold;
2281        let tool_confirmation_policy = self.tool_confirmation_policy.clone();
2282        let output_guardrails = self.output_guardrails.clone();
2283        let agent_tool_execution_strategy = self.tool_execution_strategy;
2284        #[cfg(feature = "enhanced-plugins")]
2285        let enhanced_plugin_manager = self.enhanced_plugin_manager.clone();
2286
2287        let s = stream! {
2288            let confirmation_decisions =
2289                ctx.run_config().tool_confirmation_decisions.clone();
2290            let confirmation_fingerprints =
2291                ctx.run_config().tool_confirmation_fingerprints.clone();
2292            let mut live_confirmation_decisions =
2293                std::collections::HashMap::<String, ToolConfirmationDecision>::new();
2294            let confirmation_handler = ctx.run_config().tool_confirmation_handler.clone();
2295
2296            // ===== BEFORE AGENT CALLBACKS =====
2297            // Execute before the agent starts running
2298            // If any returns content, skip agent execution
2299            for callback in before_agent_callbacks.as_ref() {
2300                match callback(ctx.clone() as Arc<dyn CallbackContext>).await {
2301                    Ok(Some(content)) => {
2302                        // Callback returned content - yield it and skip agent execution
2303                        let mut early_event = Event::new(&invocation_id);
2304                        early_event.author = agent_name.clone();
2305                        early_event.llm_response.content = Some(content);
2306                        yield Ok(early_event);
2307
2308                        // Skip rest of agent execution and go to after callbacks
2309                        for after_callback in after_agent_callbacks.as_ref() {
2310                            match after_callback(ctx.clone() as Arc<dyn CallbackContext>).await {
2311                                Ok(Some(after_content)) => {
2312                                    let mut after_event = Event::new(&invocation_id);
2313                                    after_event.author = agent_name.clone();
2314                                    after_event.llm_response.content = Some(after_content);
2315                                    yield Ok(after_event);
2316                                    return;
2317                                }
2318                                Ok(None) => continue,
2319                                Err(e) => {
2320                                    yield Err(e);
2321                                    return;
2322                                }
2323                            }
2324                        }
2325                        return;
2326                    }
2327                    Ok(None) => {
2328                        // Continue to next callback
2329                        continue;
2330                    }
2331                    Err(e) => {
2332                        // Callback failed - propagate error
2333                        yield Err(e);
2334                        return;
2335                    }
2336                }
2337            }
2338
2339            // ===== MAIN AGENT EXECUTION =====
2340            let mut conversation_history = match prompt_config
2341                .prepare_conversation(&ctx, &agent_name)
2342                .await
2343            {
2344                Ok(history) => history,
2345                Err(error) => {
2346                    yield Err(error);
2347                    return;
2348                }
2349            };
2350
2351            let resolved_tools = match tool_setup.resolve(&ctx).await {
2352                Ok(tools) => tools,
2353                Err(error) => {
2354                    yield Err(error);
2355                    return;
2356                }
2357            };
2358            let tool_map = resolved_tools.map;
2359            let tool_declarations = resolved_tools.declarations;
2360            let valid_transfer_targets = resolved_tools.transfer_targets;
2361
2362            let collect_long_running_ids = |content: &Content| -> Vec<String> {
2363                content
2364                    .parts
2365                    .iter()
2366                    .filter_map(|part| {
2367                        if let Part::FunctionCall { name, .. } = part
2368                            && let Some(tool) = tool_map.get(name)
2369                            && tool.is_long_running()
2370                        {
2371                            return Some(name.clone());
2372                        }
2373                        None
2374                    })
2375                    .collect()
2376            };
2377
2378
2379            // ===== CIRCUIT BREAKER STATE =====
2380            // Created fresh per invocation so it resets between runs.
2381            let mut circuit_breaker_state = circuit_breaker_threshold.map(CircuitBreakerState::new);
2382
2383            // ===== RESPONSE-ID CONTINUITY (provider-neutral) =====
2384            // Tracks the `interaction_id` carried by the most recent model
2385            // response so the next request can continue the conversation via
2386            // `LlmRequest.previous_response_id`. This is generic plumbing: it
2387            // contains no Gemini- or transport-specific logic. Providers that
2388            // do not support response chaining leave `interaction_id` as `None`,
2389            // so this stays `None` and `previous_response_id` is never set
2390            // (a no-op for generateContent and all other providers).
2391            let mut last_interaction_id: Option<String> = None;
2392
2393            // Multi-turn loop with max iterations
2394            let mut iteration = 0;
2395            let mut schema_retry_count: usize = 0;
2396
2397            loop {
2398                // Cooperative cancellation: exit before starting another turn
2399                // if the invocation was cancelled (e.g. Runner::interrupt()).
2400                if ctx.is_cancelled() {
2401                    tracing::info!(agent.name = %agent_name, "invocation cancelled — stopping agent loop");
2402                    return;
2403                }
2404                iteration += 1;
2405                if iteration > max_iterations {
2406                    yield Err(adk_core::AdkError::agent(
2407                        format!("Max iterations ({max_iterations}) exceeded")
2408                    ));
2409                    return;
2410                }
2411
2412                let config = build_generation_config(
2413                    generate_content_config.as_ref(),
2414                    prompt_config.output_schema.as_ref(),
2415                    ctx.run_config().cached_content.as_deref(),
2416                );
2417
2418                let request = LlmRequest {
2419                    model: model.name().to_string(),
2420                    contents: conversation_history.clone(),
2421                    tools: tool_declarations.clone(),
2422                    config,
2423                    // Provider-neutral continuity: carry the most recent
2424                    // response's `interaction_id` forward so transports that
2425                    // support response chaining (e.g. the Gemini Interactions
2426                    // transport, which maps this to `previous_interaction_id`)
2427                    // can continue server-side. `None` for the first turn and
2428                    // for providers that never populate `interaction_id`.
2429                    previous_response_id: last_interaction_id.clone(),
2430                };
2431
2432                // ===== ENHANCED PLUGIN: BEFORE MODEL CALL =====
2433                // Enhanced plugins can modify the request or short-circuit the model call.
2434                // They run before legacy before_model_callbacks.
2435                #[cfg(feature = "enhanced-plugins")]
2436                let (request, model_response_override_from_plugin) = {
2437                    if let Some(epm) = &enhanced_plugin_manager {
2438                        match epm.run_before_model_call(request, ctx.clone() as Arc<dyn CallbackContext>).await {
2439                            Ok(BeforeModelCallResult::Continue(modified_request)) => {
2440                                (modified_request, None)
2441                            }
2442                            Ok(BeforeModelCallResult::ShortCircuit(response)) => {
2443                                // Use a default request since we're short-circuiting
2444                                (LlmRequest::new("", vec![]), Some(response))
2445                            }
2446                            Err(e) => {
2447                                yield Err(e);
2448                                return;
2449                            }
2450                        }
2451                    } else {
2452                        (request, None)
2453                    }
2454                };
2455                #[cfg(not(feature = "enhanced-plugins"))]
2456                let model_response_override_from_plugin: Option<LlmResponse> = None;
2457
2458                // ===== BEFORE MODEL CALLBACKS =====
2459                // These can modify the request or skip the model call by returning a response
2460                let mut current_request = request;
2461                let mut model_response_override = model_response_override_from_plugin;
2462                if model_response_override.is_none() {
2463                    for callback in before_model_callbacks.as_ref() {
2464                        match callback(ctx.clone() as Arc<dyn CallbackContext>, current_request.clone()).await {
2465                            Ok(BeforeModelResult::Continue(modified_request)) => {
2466                                // Callback may have modified the request, continue with it
2467                                current_request = modified_request;
2468                            }
2469                            Ok(BeforeModelResult::Skip(response)) => {
2470                                // Callback returned a response - skip model call
2471                                model_response_override = Some(response);
2472                                break;
2473                            }
2474                            Err(e) => {
2475                                // Callback failed - propagate error
2476                                yield Err(e);
2477                                return;
2478                            }
2479                        }
2480                    }
2481                }
2482                let request = current_request;
2483
2484                // Determine streaming source: cached response or real model
2485                let mut accumulated_content: Option<Content> = None;
2486                let mut final_provider_metadata: Option<serde_json::Value> = None;
2487
2488                if let Some(cached_response) = model_response_override {
2489                    // Use callback-provided response (e.g., from cache)
2490                    // Yield it as an event
2491                    accumulated_content = cached_response.content.clone();
2492                    final_provider_metadata = cached_response.provider_metadata.clone();
2493                    normalize_option_content(&mut accumulated_content);
2494                    if let Some(content) = accumulated_content.take() {
2495                        let has_function_calls = content
2496                            .parts
2497                            .iter()
2498                            .any(|part| matches!(part, Part::FunctionCall { .. }));
2499                        let content = if has_function_calls {
2500                            content
2501                        } else {
2502                            Self::apply_output_guardrails(output_guardrails.as_ref(), content).await?
2503                        };
2504                        accumulated_content = Some(content);
2505                    }
2506
2507                    let mut cached_event = Event::new(&invocation_id);
2508                    cached_event.author = agent_name.clone();
2509                    cached_event.llm_response.content = accumulated_content.clone();
2510                    cached_event.llm_response.provider_metadata = cached_response.provider_metadata.clone();
2511                    // Surface and track the response id for provider-neutral continuity.
2512                    cached_event.llm_response.interaction_id = cached_response.interaction_id.clone();
2513                    if cached_response.interaction_id.is_some() {
2514                        last_interaction_id = cached_response.interaction_id.clone();
2515                    }
2516                    cached_event.llm_request = Some(serde_json::to_string(&request).unwrap_or_default());
2517                    cached_event.provider_metadata.insert("gcp.vertex.agent.llm_request".to_string(), serde_json::to_string(&request).unwrap_or_default());
2518                    cached_event.provider_metadata.insert("gcp.vertex.agent.llm_response".to_string(), serde_json::to_string(&cached_response).unwrap_or_default());
2519
2520                    // Populate long_running_tool_ids for function calls from long-running tools
2521                    if let Some(ref content) = accumulated_content {
2522                        cached_event.long_running_tool_ids = collect_long_running_ids(content);
2523                    }
2524
2525                    yield Ok(cached_event);
2526                } else {
2527                    // Record LLM request for tracing
2528                    let request_json = serde_json::to_string(&request).unwrap_or_default();
2529                    let trace_request_json = trace_json_payload(
2530                        &request,
2531                        ctx.run_config().record_payloads,
2532                        ctx.run_config().trace_payload_max_bytes,
2533                    );
2534
2535                    // Create call_llm span with GCP attributes (works for all model types)
2536                    let llm_ts = std::time::SystemTime::now()
2537                        .duration_since(std::time::UNIX_EPOCH)
2538                        .unwrap_or_default()
2539                        .as_nanos();
2540                    let llm_event_id = format!("{}_llm_{}", invocation_id, llm_ts);
2541                    let llm_span = tracing::info_span!(
2542                        "call_llm",
2543                        "gcp.vertex.agent.event_id" = %llm_event_id,
2544                        "gcp.vertex.agent.invocation_id" = %invocation_id,
2545                        "gcp.vertex.agent.session_id" = %ctx.session_id(),
2546                        "gen_ai.conversation.id" = %ctx.session_id(),
2547                        "gcp.vertex.agent.llm_request" = %trace_request_json,
2548                        "gcp.vertex.agent.llm_response" = tracing::field::Empty  // Placeholder for later recording
2549                    );
2550                    let _llm_guard = llm_span.enter();
2551
2552                    // Check streaming mode from run config
2553                    use adk_core::StreamingMode;
2554                    let streaming_mode = ctx.run_config().streaming_mode;
2555                    let should_stream_to_client = matches!(streaming_mode, StreamingMode::SSE | StreamingMode::Bidi)
2556                        && output_guardrails.is_empty();
2557
2558                    // Always use streaming internally for LLM calls
2559                    let mut response_stream = model.generate_content(request, true).await?;
2560
2561                    use futures::StreamExt;
2562
2563                    // Track last chunk for final event metadata (used in None mode)
2564                    let mut last_chunk: Option<LlmResponse> = None;
2565
2566                    // Stream and process chunks with AfterModel callbacks
2567                    while let Some(chunk_result) = response_stream.next().await {
2568                        // Cooperative cancellation: stop consuming the model
2569                        // stream promptly when the invocation is cancelled. This
2570                        // drops `response_stream`, releasing the provider connection.
2571                        if ctx.is_cancelled() {
2572                            tracing::info!(agent.name = %agent_name, "invocation cancelled during LLM streaming");
2573                            return;
2574                        }
2575                        let mut chunk = match chunk_result {
2576                            Ok(c) => c,
2577                            Err(e) => {
2578                                yield Err(e);
2579                                return;
2580                            }
2581                        };
2582
2583                        // ===== AFTER MODEL CALLBACKS (per chunk) =====
2584                        // Callbacks can modify each streaming chunk
2585                        for callback in after_model_callbacks.as_ref() {
2586                            match callback(ctx.clone() as Arc<dyn CallbackContext>, chunk.clone()).await {
2587                                Ok(Some(modified_chunk)) => {
2588                                    // Callback modified this chunk
2589                                    chunk = modified_chunk;
2590                                    break;
2591                                }
2592                                Ok(None) => {
2593                                    // Continue to next callback
2594                                    continue;
2595                                }
2596                                Err(e) => {
2597                                    // Callback failed - propagate error
2598                                    yield Err(e);
2599                                    return;
2600                                }
2601                            }
2602                        }
2603
2604                        normalize_option_content(&mut chunk.content);
2605
2606                        // Accumulate content for conversation history (always needed)
2607                        if let Some(chunk_content) = chunk.content.clone() {
2608                            if let Some(ref mut acc) = accumulated_content {
2609                                acc.parts.extend(chunk_content.parts);
2610                            } else {
2611                                accumulated_content = Some(chunk_content);
2612                            }
2613                        }
2614
2615                        // For SSE/Bidi mode: yield each chunk immediately with stable event ID
2616                        if should_stream_to_client {
2617                            let long_running_tool_ids = chunk
2618                                .content
2619                                .as_ref()
2620                                .map(&collect_long_running_ids)
2621                                .unwrap_or_default();
2622                            yield Ok(build_partial_llm_event(
2623                                &llm_event_id,
2624                                &invocation_id,
2625                                &agent_name,
2626                                &request_json,
2627                                &chunk,
2628                                long_running_tool_ids,
2629                            ));
2630                        }
2631
2632                        // Track the response id for provider-neutral continuity.
2633                        // Transports that support response chaining populate
2634                        // `interaction_id`; others leave it `None` (no-op).
2635                        if chunk.interaction_id.is_some() {
2636                            last_interaction_id = chunk.interaction_id.clone();
2637                        }
2638
2639                        // Store last chunk for final event metadata
2640                        last_chunk = Some(chunk.clone());
2641
2642                        // Check if turn is complete
2643                        if chunk.turn_complete {
2644                            break;
2645                        }
2646                    }
2647
2648                    // For None mode: yield single final event with accumulated content
2649                    if !should_stream_to_client {
2650                        if let Some(content) = accumulated_content.take() {
2651                            let has_function_calls = content
2652                                .parts
2653                                .iter()
2654                                .any(|part| matches!(part, Part::FunctionCall { .. }));
2655                            let content = if has_function_calls {
2656                                content
2657                            } else {
2658                                Self::apply_output_guardrails(output_guardrails.as_ref(), content).await?
2659                            };
2660                            accumulated_content = Some(content);
2661                        }
2662
2663                        if let Some(last) = &last_chunk {
2664                            final_provider_metadata = last.provider_metadata.clone();
2665                        }
2666                        let long_running_tool_ids = accumulated_content
2667                            .as_ref()
2668                            .map(&collect_long_running_ids)
2669                            .unwrap_or_default();
2670                        yield Ok(build_final_llm_event(
2671                            &llm_event_id,
2672                            &invocation_id,
2673                            &agent_name,
2674                            &request_json,
2675                            accumulated_content.as_ref(),
2676                            last_chunk.as_ref(),
2677                            long_running_tool_ids,
2678                        ));
2679                    }
2680
2681                    // A provider that reports a terminal error inside an `Ok`
2682                    // response ends the turn. The event above already carries the
2683                    // error fields so the failure is observable and persisted;
2684                    // this converts it into a `Result` failure so callers, retry
2685                    // policy, and telemetry see it rather than reading an empty
2686                    // turn as success.
2687                    //
2688                    // In this workspace `error_code` marks a genuine failure —
2689                    // truncation is reported through `finish_reason`
2690                    // (`FinishReason::MaxTokens`), not through `error_code`.
2691                    if let Some(ref last) = last_chunk
2692                        && let Some(ref code) = last.error_code
2693                    {
2694                        let message = last
2695                            .error_message
2696                            .clone()
2697                            .unwrap_or_else(|| "provider reported a terminal error".to_string());
2698                        tracing::error!(
2699                            error.code = %code,
2700                            error.message = %message,
2701                            agent = %agent_name,
2702                            "model reported a terminal error"
2703                        );
2704                        // The provider's own code is preserved in the ADK error code
2705                        // so retry policy and telemetry can key on it.
2706                        // Built before the yield point: a borrow may not cross it.
2707                        // `AdkError::code` is `&'static str`, so the provider's own
2708                        // code travels in the details metadata instead, where retry
2709                        // policy and telemetry can read it.
2710                        let mut details = adk_core::ErrorDetails::default();
2711                        details
2712                            .metadata
2713                            .insert("provider_error_code".to_string(), serde_json::json!(code));
2714                        let provider_error = adk_core::AdkError::new(
2715                            adk_core::ErrorComponent::Model,
2716                            adk_core::ErrorCategory::Internal,
2717                            "model.provider_error",
2718                            format!("{code}: {message}"),
2719                        )
2720                        .with_details(details);
2721                        yield Err(provider_error);
2722                        return;
2723                    }
2724
2725                    // Record LLM response to span before guard drops
2726                    if let Some(ref content) = accumulated_content {
2727                        let response_json = trace_json_payload(
2728                            content,
2729                            ctx.run_config().record_payloads,
2730                            ctx.run_config().trace_payload_max_bytes,
2731                        );
2732                        llm_span.record("gcp.vertex.agent.llm_response", &response_json);
2733                    }
2734                }
2735
2736                // ===== ENHANCED PLUGIN: AFTER MODEL CALL =====
2737                // Enhanced plugins can modify the accumulated model response.
2738                // They run after the full response is accumulated (not per-chunk).
2739                #[cfg(feature = "enhanced-plugins")]
2740                if let Some(epm) = &enhanced_plugin_manager
2741                    && let Some(ref content) = accumulated_content {
2742                        let response_for_hook = LlmResponse {
2743                            content: Some(content.clone()),
2744                            provider_metadata: final_provider_metadata.clone(),
2745                            ..Default::default()
2746                        };
2747                        match epm.run_after_model_call(response_for_hook, ctx.clone() as Arc<dyn CallbackContext>).await {
2748                            Ok(adk_plugin::AfterModelCallResult::Continue(modified_response)) => {
2749                                accumulated_content = modified_response.content;
2750                                if modified_response.provider_metadata.is_some() {
2751                                    final_provider_metadata = modified_response.provider_metadata;
2752                                }
2753                            }
2754                            Err(e) => {
2755                                yield Err(e);
2756                                return;
2757                            }
2758                        }
2759                    }
2760
2761                // After streaming/caching completes, check for function calls in accumulated content
2762                let function_call_names: Vec<String> = accumulated_content.as_ref()
2763                    .map(|c| c.parts.iter()
2764                        .filter_map(|p| {
2765                            if let Part::FunctionCall { name, .. } = p {
2766                                Some(name.clone())
2767                            } else {
2768                                None
2769                            }
2770                        })
2771                        .collect())
2772                    .unwrap_or_default();
2773
2774                let has_function_calls = !function_call_names.is_empty();
2775
2776                // Check if ALL function calls are from long-running tools
2777                // If so, we should NOT continue the loop - the tool returned a pending status
2778                // and the agent/client will poll for completion later
2779                let all_calls_are_long_running = has_function_calls && function_call_names.iter().all(|name| {
2780                    tool_map.get(name)
2781                        .map(|t| t.is_long_running())
2782                        .unwrap_or(false)
2783                });
2784
2785                // Add final content to history
2786                if let Some(ref content) = accumulated_content {
2787                    conversation_history.push(Self::augment_content_for_history(
2788                        content,
2789                        final_provider_metadata.as_ref(),
2790                    ));
2791
2792                    // Handle output_key: save final agent output to state_delta
2793                    if let Some(ref output_key) = output_key
2794                        && !has_function_calls
2795                    {
2796                        let mut text_parts = String::new();
2797                        for part in &content.parts {
2798                            if let Part::Text { text } = part {
2799                                text_parts.push_str(text);
2800                            }
2801                        }
2802                        if !text_parts.is_empty() {
2803                            // Yield a final state update event
2804                            let mut state_event = Event::new(&invocation_id);
2805                            state_event.author = agent_name.clone();
2806                            state_event.actions.state_delta.insert(
2807                                output_key.clone(),
2808                                serde_json::Value::String(text_parts),
2809                            );
2810                            yield Ok(state_event);
2811                        }
2812                    }
2813                }
2814
2815                if !has_function_calls {
2816                    // ===== OUTPUT SCHEMA VALIDATION =====
2817                    // When output_schema is set, validate the response text against
2818                    // the schema. If invalid, retry with a correction prompt up to
2819                    // output_max_retries times.
2820                    if let Some(schema) = &prompt_config.output_schema {
2821                        let text = accumulated_content
2822                            .as_ref()
2823                            .map(|c| {
2824                                c.parts
2825                                    .iter()
2826                                    .filter_map(|p| {
2827                                        if let Part::Text { text } = p {
2828                                            Some(text.as_str())
2829                                        } else {
2830                                            None
2831                                        }
2832                                    })
2833                                    .collect::<Vec<_>>()
2834                                    .join("")
2835                            })
2836                            .unwrap_or_default();
2837
2838                        if !text.is_empty()
2839                            && let Err(validation_error) = validate_output_against_schema(&text, schema)
2840                        {
2841                                if schema_retry_count >= output_max_retries {
2842                                    yield Err(adk_core::AdkError::agent(format!(
2843                                        "output schema validation failed after {} attempts",
2844                                        output_max_retries
2845                                    )));
2846                                    return;
2847                                }
2848                                schema_retry_count += 1;
2849
2850                                // Append a correction prompt and retry
2851                                let correction = format!(
2852                                    "Your output did not match the required schema. Error: {}. Please produce valid JSON matching the schema.",
2853                                    validation_error
2854                                );
2855                                conversation_history.push(Content {
2856                                    role: "user".to_string(),
2857                                    parts: vec![Part::Text { text: correction }],
2858                                });
2859                                continue;
2860                        }
2861                    }
2862
2863                    // No function calls, we're done
2864                    // Record LLM response for tracing
2865                    if let Some(ref content) = accumulated_content {
2866                        let response_json = trace_json_payload(
2867                            content,
2868                            ctx.run_config().record_payloads,
2869                            ctx.run_config().trace_payload_max_bytes,
2870                        );
2871                        tracing::Span::current().record("gcp.vertex.agent.llm_response", &response_json);
2872                    }
2873
2874                    tracing::info!(agent.name = %agent_name, "Agent execution complete");
2875                    break;
2876                }
2877
2878                // Execute function calls and add responses to history
2879                if let Some(content) = &accumulated_content {
2880                    // ===== RESOLVE TOOL EXECUTION STRATEGY =====
2881                    // Per-agent override; defaults to Sequential if not set.
2882                    let strategy = agent_tool_execution_strategy
2883                        .unwrap_or(ToolExecutionStrategy::Sequential);
2884
2885                    let fc_parts = collect_function_calls(content, &invocation_id);
2886
2887                    // ===== HANDLE transfer_to_agent BEFORE DISPATCH =====
2888                    // Transfer calls cause an immediate return from the stream,
2889                    // so they must be handled inline regardless of strategy.
2890                    let mut transfer_handled = false;
2891                    for call in &fc_parts {
2892                        if call.name == "transfer_to_agent" {
2893                            let target_agent = call
2894                                .args
2895                                .get("agent_name")
2896                                .and_then(|value| value.as_str())
2897                                .unwrap_or_default()
2898                                .to_string();
2899
2900                            let valid_target = valid_transfer_targets.iter().any(|n| n == &target_agent);
2901                            if !valid_target {
2902                                let error_content = Content {
2903                                    role: "function".to_string(),
2904                                    parts: vec![Part::FunctionResponse {
2905                                        function_response: FunctionResponseData::new(
2906                                            call.name.clone(),
2907                                            serde_json::json!({
2908                                                "error": format!(
2909                                                    "Agent '{}' not found. Available agents: {:?}",
2910                                                    target_agent, valid_transfer_targets
2911                                                )
2912                                            }),
2913                                        ),
2914                                        id: call.id.clone(),
2915                                        annotations: None,
2916                                    }],
2917                                };
2918                                conversation_history.push(error_content.clone());
2919                                let mut error_event = Event::new(&invocation_id);
2920                                error_event.author = agent_name.clone();
2921                                error_event.llm_response.content = Some(error_content);
2922                                yield Ok(error_event);
2923                                continue;
2924                            }
2925
2926                            let mut transfer_event = Event::new(&invocation_id);
2927                            transfer_event.author = agent_name.clone();
2928                            transfer_event.actions.transfer_to_agent = Some(target_agent);
2929                            yield Ok(transfer_event);
2930                            transfer_handled = true;
2931                            break;
2932                        }
2933                    }
2934                    if transfer_handled {
2935                        return;
2936                    }
2937
2938                    // Filter out transfer_to_agent and built-in tools
2939                    let fc_parts: Vec<_> = fc_parts
2940                        .into_iter()
2941                        .filter(|call| {
2942                            if call.name == "transfer_to_agent" {
2943                                return false;
2944                            }
2945                            if let Some(tool) = tool_map.get(&call.name)
2946                                && tool.is_builtin()
2947                            {
2948                                adk_telemetry::debug!(tool.name = %call.name, "skipping built-in tool execution");
2949                                return false;
2950                            }
2951                            true
2952                        })
2953                        .collect();
2954
2955                    // ===== TOOL CONFIRMATION PRE-CHECK =====
2956                    // Tool confirmation interrupts cause an immediate return,
2957                    // so check before parallel dispatch.
2958                    let mut confirmation_interrupted = false;
2959                    for call in &fc_parts {
2960                        if tool_confirmation_policy.requires_confirmation(&call.name)
2961                            && static_confirmation_decision(
2962                                &confirmation_decisions,
2963                                &confirmation_fingerprints,
2964                                &call.function_call_id,
2965                                &call.name,
2966                                &call.args,
2967                            )
2968                            .is_none()
2969                            && live_confirmation_decisions
2970                                .get(&call.function_call_id)
2971                                .copied()
2972                                .is_none()
2973                        {
2974                            let request = ToolConfirmationRequest {
2975                                tool_name: call.name.clone(),
2976                                function_call_id: Some(call.function_call_id.clone()),
2977                                args: call.args.clone(),
2978                            };
2979                            if let Some(handler) = confirmation_handler.as_ref() {
2980                                match handler.decide(&request).await {
2981                                    Ok(decision) => {
2982                                        live_confirmation_decisions
2983                                            .insert(call.function_call_id.clone(), decision);
2984                                        continue;
2985                                    }
2986                                    Err(error) => {
2987                                        yield Err(error);
2988                                        return;
2989                                    }
2990                                }
2991                            }
2992
2993                                let mut ce = Event::new(&invocation_id);
2994                                ce.author = agent_name.clone();
2995                                ce.llm_response.interrupted = true;
2996                                ce.llm_response.turn_complete = true;
2997                                ce.llm_response.content = Some(Content {
2998                                    role: "model".to_string(),
2999                                    parts: vec![Part::Text {
3000                                        text: format!(
3001                                            "Tool confirmation required for '{}'. Provide approve/deny decision to continue.",
3002                                            call.name
3003                                        ),
3004                                    }],
3005                                });
3006                                ce.actions.tool_confirmation = Some(request);
3007                                yield Ok(ce);
3008                                confirmation_interrupted = true;
3009                                break;
3010                        }
3011                    }
3012                    if confirmation_interrupted {
3013                        return;
3014                    }
3015
3016                    // Wrap circuit breaker in Mutex for shared access across parallel futures.
3017                    let cb_mutex = std::sync::Mutex::new(circuit_breaker_state.take());
3018
3019                    // Create concurrency manager for semaphore-based tool dispatch enforcement.
3020                    // Per-tool overrides take precedence over the global limit.
3021                    let concurrency_manager = adk_core::ToolConcurrencyManager::new(
3022                        &ctx.run_config().tool_concurrency,
3023                    );
3024
3025                    // Channel for streaming tool progress (stdout/stderr) onto the
3026                    // agent's EventStream while tools are still executing. Each
3027                    // AgentToolContext gets a clone; the dispatch loop below drains
3028                    // it concurrently and yields progress events to the client.
3029                    let (progress_tx, mut progress_rx) =
3030                        tokio::sync::mpsc::channel::<Event>(TOOL_PROGRESS_CAPACITY);
3031
3032                    let executor = ToolExecutor {
3033                        ctx: ctx.clone(),
3034                        tool_map: &tool_map,
3035                        tool_retry_budgets: &tool_retry_budgets,
3036                        default_retry_budget: &default_retry_budget,
3037                        before_tool_callbacks: &before_tool_callbacks,
3038                        after_tool_callbacks: &after_tool_callbacks,
3039                        after_tool_callbacks_full: &after_tool_callbacks_full,
3040                        on_tool_error_callbacks: &on_tool_error_callbacks,
3041                        tool_confirmation_policy: &tool_confirmation_policy,
3042                        cb_mutex: &cb_mutex,
3043                        invocation_id: &invocation_id,
3044                        concurrency_manager: &concurrency_manager,
3045                        progress_tx: progress_tx.clone(),
3046                        tool_timeout,
3047                        confirmation_decisions: &confirmation_decisions,
3048                        confirmation_fingerprints: &confirmation_fingerprints,
3049                        live_confirmation_decisions: &live_confirmation_decisions,
3050                        #[cfg(feature = "enhanced-plugins")]
3051                        enhanced_plugin_manager: &enhanced_plugin_manager,
3052                    };
3053
3054                    // Cooperative cancellation: skip tool execution if the
3055                    // invocation was cancelled while the model was streaming.
3056                    if ctx.is_cancelled() {
3057                        tracing::info!(agent.name = %agent_name, "invocation cancelled before tool dispatch");
3058                        return;
3059                    }
3060
3061                    // ===== DISPATCH BASED ON STRATEGY =====
3062                    // Scoped so the dispatch future (which borrows the executor
3063                    // and circuit-breaker mutex) is dropped before we reclaim
3064                    // `cb_mutex` below.
3065                    let mut results = {
3066                        let dispatch = async {
3067                            let results: Vec<ToolExecutionResult> = match strategy {
3068                                ToolExecutionStrategy::Sequential => {
3069                                    let mut results = Vec::with_capacity(fc_parts.len());
3070                                    for call in fc_parts {
3071                                        results.push(executor.execute(call).await);
3072                                    }
3073                                    results
3074                                }
3075                                ToolExecutionStrategy::Parallel => {
3076                                    use futures::StreamExt as _;
3077                                    // Parallel is an explicit caller override. Tool
3078                                    // safety metadata is intentionally not inspected.
3079                                    // All concurrency enforcement is handled by the
3080                                    // ToolConcurrencyManager semaphore inside ToolExecutor.
3081                                    // Use fc_parts.len() as buffer so all futures can start
3082                                    // and queue on the semaphore for proper per-tool limiting.
3083                                    let buffer_size = fc_parts.len().max(1);
3084                                    futures::stream::iter(
3085                                        fc_parts.into_iter().map(|call| executor.execute(call)),
3086                                    )
3087                                    .buffer_unordered(buffer_size)
3088                                    .collect()
3089                                    .await
3090                                }
3091                                ToolExecutionStrategy::Auto => {
3092                                    // A call may overlap another only when its tool is
3093                                    // read-only *and* declares concurrency safety.
3094                                    let (concurrent_fcs, sequential_fcs): (Vec<_>, Vec<_>) =
3095                                        fc_parts.into_iter().partition(|call| {
3096                                            tool_map.get(&call.name).is_some_and(|tool| {
3097                                                tool.is_read_only() && tool.is_concurrency_safe()
3098                                            })
3099                                        });
3100                                    let mut all_results = Vec::new();
3101
3102                                    // Concurrency enforcement is handled by the semaphore
3103                                    // inside ToolExecutor.
3104                                    if !concurrent_fcs.is_empty() {
3105                                        use futures::StreamExt as _;
3106                                        let buffer_size = concurrent_fcs.len().max(1);
3107                                        all_results.extend(
3108                                            futures::stream::iter(
3109                                                concurrent_fcs
3110                                                    .into_iter()
3111                                                    .map(|call| executor.execute(call)),
3112                                            )
3113                                            .buffer_unordered(buffer_size)
3114                                            .collect::<Vec<_>>()
3115                                            .await,
3116                                        );
3117                                    }
3118
3119                                    // Everything else runs one at a time.
3120                                    for call in sequential_fcs {
3121                                        all_results.push(executor.execute(call).await);
3122                                    }
3123                                    all_results
3124                                }
3125                            };
3126                            results
3127                        };
3128
3129                        // Drain tool progress concurrently with execution, yielding
3130                        // each chunk as a partial Event the moment it arrives. The
3131                        // dispatch future and the progress receiver are polled together
3132                        // so output streams live rather than buffering until the tool
3133                        // finishes.
3134                        tokio::pin!(dispatch);
3135                        let results = loop {
3136                            tokio::select! {
3137                                biased;
3138                                Some(progress_event) = progress_rx.recv() => {
3139                                    yield Ok(progress_event);
3140                                }
3141                                done = &mut dispatch => break done,
3142                            }
3143                        };
3144                        // Flush any progress chunks buffered between the last poll and completion.
3145                        while let Ok(progress_event) = progress_rx.try_recv() {
3146                            yield Ok(progress_event);
3147                        }
3148                        results
3149                    };
3150                    // Preserve LLM-returned order even when tool futures finish out of order.
3151                    results.sort_by_key(|r| r.index);
3152
3153                    // Restore circuit breaker state from the mutex
3154                    circuit_breaker_state = cb_mutex.into_inner().unwrap_or_else(|e| e.into_inner());
3155
3156                    // Yield results in original order
3157                    for result in results {
3158                        let mut tool_event = Event::new(&invocation_id);
3159                        tool_event.author = agent_name.clone();
3160                        tool_event.actions = result.actions;
3161                        tool_event.llm_response.content = Some(result.content.clone());
3162                        yield Ok(tool_event);
3163
3164                        if result.escalate_or_skip {
3165                            return;
3166                        }
3167
3168                        conversation_history.push(result.content);
3169                    }
3170                }
3171
3172                // If all function calls were from long-running tools, we need ONE more model call
3173                // to let the model generate a user-friendly response about the pending task
3174                // But we mark this as the final iteration to prevent infinite loops
3175                if all_calls_are_long_running {
3176                    // Continue to next iteration for model to respond, but this will be the last
3177                    // The model will see the tool response and generate text like "Started task X..."
3178                    // On next iteration, there won't be function calls, so we'll break naturally
3179                }
3180            }
3181
3182            // ===== AFTER AGENT CALLBACKS =====
3183            // Execute after the agent completes
3184            for callback in after_agent_callbacks.as_ref() {
3185                match callback(ctx.clone() as Arc<dyn CallbackContext>).await {
3186                    Ok(Some(content)) => {
3187                        // Callback returned content - yield it
3188                        let mut after_event = Event::new(&invocation_id);
3189                        after_event.author = agent_name.clone();
3190                        after_event.llm_response.content = Some(content);
3191                        yield Ok(after_event);
3192                        break; // First callback that returns content wins
3193                    }
3194                    Ok(None) => {
3195                        // Continue to next callback
3196                        continue;
3197                    }
3198                    Err(e) => {
3199                        // Callback failed - propagate error
3200                        yield Err(e);
3201                        return;
3202                    }
3203                }
3204            }
3205        };
3206
3207        Ok(Box::pin(s))
3208    }
3209}
3210
3211#[cfg(test)]
3212mod run_helper_tests {
3213    use super::*;
3214
3215    #[test]
3216    fn generation_config_layers_schema_and_cached_content() {
3217        let base =
3218            adk_core::GenerateContentConfig { temperature: Some(0.25), ..Default::default() };
3219        let schema = serde_json::json!({"type": "object"});
3220
3221        let config = build_generation_config(Some(&base), Some(&schema), Some("cached/example"))
3222            .expect("config should be present");
3223
3224        assert_eq!(config.temperature, Some(0.25));
3225        assert_eq!(config.response_schema, Some(schema));
3226        assert_eq!(config.cached_content.as_deref(), Some("cached/example"));
3227    }
3228
3229    #[test]
3230    fn function_calls_preserve_order_and_create_fallback_ids() {
3231        let content = Content {
3232            role: "model".to_string(),
3233            parts: vec![
3234                Part::Text { text: "before".to_string() },
3235                Part::FunctionCall {
3236                    name: "first".to_string(),
3237                    args: serde_json::json!({"value": 1}),
3238                    id: None,
3239                    thought_signature: None,
3240                },
3241                Part::FunctionCall {
3242                    name: "second".to_string(),
3243                    args: serde_json::json!({"value": 2}),
3244                    id: Some("provider-id".to_string()),
3245                    thought_signature: None,
3246                },
3247            ],
3248        };
3249
3250        let calls = collect_function_calls(&content, "invocation");
3251
3252        assert_eq!(calls.len(), 2);
3253        assert_eq!(calls[0].index, 0);
3254        assert_eq!(calls[0].name, "first");
3255        assert_eq!(calls[0].function_call_id, "invocation_first_0");
3256        assert_eq!(calls[1].index, 1);
3257        assert_eq!(calls[1].name, "second");
3258        assert_eq!(calls[1].function_call_id, "provider-id");
3259    }
3260}