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