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