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