Skip to content

docs: demonstrate dynamic tool injection via SkillToolset #5382

@caohy1988

Description

@caohy1988

Summary

ADK supports dynamic tool injection at runtime — a loaded skill can make new tools available in the next GenerateContent call within the same invocation. This is powered by SkillToolset (experimental) and the BaseToolset contract, but the mechanism is not documented or demonstrated anywhere.

This issue provides a concrete demonstration and explains the internal flow for future reference.

How It Works

The flow

  1. Agent is configured with a SkillToolset containing skills and an additional_tools pool
  2. Initially, the LLM only sees 4 core skill management tools (list_skills, load_skill, load_skill_resource, run_skill_script)
  3. LLM calls load_skill(skill_name="data_analysis") → tool writes activation to session state:
    state["_adk_activated_skill_{agent_name}"] = ["data_analysis"]
  4. On the very next LLM call (same invocation), SkillToolset.get_tools() re-reads state, finds the activated skill, checks its adk_additional_tools metadata, and returns matching tools from the pool
  5. New tools appear in the GenerateContent tool declarations — the LLM can now call them

Why it works within a single invocation

Two cache layers exist, both handled:

Cache Location Why it doesn't block
BaseToolset._use_invocation_cache base_toolset.py:120-125 SkillToolset sets self._use_invocation_cache = False at line 807
invocation_context.canonical_tools_cache invocation_context.py:214 Only used for grounding metadata check, NOT by _process_agent_tools()

_process_agent_tools() (base_llm_flow.py:409) calls get_tools_with_prefix()get_tools() on every iteration of the LLM loop. With caching disabled, state changes from tool calls are visible immediately.

Key code path

LLM loop iteration N:
  _run_one_step_async()
    → _preprocess_async()
      → _process_agent_tools()             # line 929
        → _convert_tool_union_to_tools()
          → SkillToolset.get_tools_with_prefix()  # cache disabled
            → SkillToolset.get_tools()             # line 828
              → _resolve_additional_tools_from_state()  # reads session state
                → returns new tools from additional_tools pool
        → tool.process_llm_request()       # registers declarations
    → _call_llm_async()
      → GenerateContent(tools=[...new tools included...])

Demonstration

Skill definition with adk_additional_tools

from google.adk.tools.skill_toolset import SkillToolset
from google.adk.tools.skill_toolset import models as skill_models

# A tool that becomes available after loading the "data_analysis" skill
def analyze_csv(file_path: str, query: str) -> str:
    """Analyze a CSV file with a natural language query."""
    return f"Analysis of {file_path}: {query} → 42"

# Define the skill — adk_additional_tools lists which tools to inject
data_skill = skill_models.Skill(
    name="data_analysis",
    display_name="Data Analysis",
    description="Loads CSV analysis capabilities",
    frontmatter=skill_models.SkillFrontmatter(
        metadata={
            "adk_additional_tools": ["analyze_csv"],  # ← key field
        }
    ),
    content="This skill enables CSV data analysis.",
)

# Build the toolset
skill_toolset = SkillToolset(
    skills=[data_skill],
    additional_tools=[analyze_csv],  # pool of injectable tools
)

agent = Agent(
    name="assistant",
    model="gemini-2.5-flash",
    instruction=(
        "You have skills you can load. Use list_skills to see available"
        " skills, then load_skill to activate them. Once a skill is"
        " loaded, its tools become available."
    ),
    tools=[skill_toolset],
)

What the LLM sees

Before load_skill("data_analysis"):

Available tools: list_skills, load_skill, load_skill_resource, run_skill_script

After load_skill("data_analysis") → next GenerateContent call:

Available tools: list_skills, load_skill, load_skill_resource, run_skill_script, analyze_csv

The analyze_csv tool was dynamically injected based on the skill's adk_additional_tools metadata.

Custom dynamic toolset (without SkillToolset)

The same pattern works with any BaseToolset subclass:

from google.adk.tools.base_toolset import BaseToolset
from google.adk.tools.base_tool import BaseTool
from google.adk.tools.function_tool import FunctionTool

class DynamicToolset(BaseToolset):
    """A toolset that exposes tools based on session state."""

    def __init__(self, tool_pool):
        super().__init__()
        self._use_invocation_cache = False  # critical
        self._pool = {t.name: t for t in tool_pool}
        self._activate_tool = FunctionTool(func=self._activate)

    def _activate(self, tool_name: str) -> str:
        """Activate a tool by name. It becomes available on the next turn."""
        if tool_name in self._pool:
            return f"Tool '{tool_name}' will be available on the next turn."
        return f"Unknown tool: {tool_name}"

    async def get_tools(self, ctx=None):
        base = [self._activate_tool]
        if not ctx:
            return base
        active = ctx.state.get("_active_tools", [])
        for name in active:
            if name in self._pool:
                base.append(self._pool[name])
        return base

The critical detail is self._use_invocation_cache = False — without it, get_tools() is only called once per invocation and mid-invocation state changes won't take effect.

Current Limitations

  • Tools must be pre-declared in the additional_tools pool — truly arbitrary tools cannot be created at runtime
  • before_model_callback cannot inject new tools: by the time it runs, tool resolution is already complete
  • agent.tools is a Pydantic field and cannot be mutated after construction

Relevant Files

File Key Lines What
src/google/adk/tools/skill_toolset.py 775-889 SkillToolset class and _resolve_additional_tools_from_state()
src/google/adk/tools/base_toolset.py 102-132 get_tools_with_prefix() with invocation cache
src/google/adk/flows/llm_flows/base_llm_flow.py 409-456 _process_agent_tools() — called every LLM loop iteration
src/google/adk/agents/llm_agent.py 139-189 _convert_tool_union_to_tools() — delegates to get_tools_with_prefix()

Metadata

Metadata

Labels

documentation[Component] This issue is related to documentation, it will be transferred to adk-docs

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions