Skip to content

SkillToolset exposes run_skill_script tool causing hallucination loop when skill has no scripts/ directory #6281

Closed

Description

@deepakbatham572

When a skill loaded by [SkillToolset] has no scripts/ directory, run_skill_script is still exposed as a callable tool. When confirmtool (or any expected next-step tool) is unavailable, adk hallucinates a reason to call run_skill_script repeatedly, entering a new infinite loop.

To reproduce:

Same setup as Issue 1. After load_skill_resource successfully loads all 4 reference files, the LLM attempts confirmtool, fails to find it, then calls run_skill_script in a loop with fabricated arguments.

Expected behavior:

run_skill_script should only be included in the tool list if the activated skill actually has a scripts/ directory with at least one file.

Actual behavior:
[ADK_EVENT] content=[FunctionCall: load_skill_resource] (×4) ← success
[ADK_EVENT] content=[FunctionCall: run_skill_script] ← hallucinated
[ADK_EVENT] content=[FunctionCall: run_skill_script] ← loop
[ADK_EVENT] content=[FunctionCall: run_skill_script]
... repeats indefinitely

Activity

  1. added theissue type on Jul 3, 2026
  2. godququ5-code commented on Jul 3, 2026

    @godququ5-code

    Hi! I verified this on current main. SkillToolset always adds RunSkillScriptTool to its core tool list, including when every configured local skill reports an empty scripts list. The existing skill-toolset suite passes but does not cover this availability rule. I can add a focused regression and conditionally omit run_skill_script when no statically configured skill has scripts. One edge case needs maintainer confirmation: if a registry is configured, dynamically fetched skills may contain scripts, so I would keep the tool available unless the desired behavior is to add it only after such a skill is activated. Which behavior fits the intended tool-discovery contract, and is anyone already working on this?

  3. kronos3610 commented on Jul 3, 2026

    @kronos3610
    Contributor

    @sanketpatil06 I'd like to take this one — I authored the related retry-guard fixes #5651 (load_skill_resource / RESOURCE_NOT_FOUND, merged as bc45ee6) and #5683 (run_skill_script / SCRIPT_NOT_FOUND, merged as 89d9bda), so I'm familiar with this code path. (@godququ5-code — I see you offered as well; happy to defer to whoever the maintainers assign, but leaving my proposal below either way.)

    First, a version check for the reporter. @deepakbatham572, could you share your google-adk version? Since v2.3.0, run_skill_script carries an invocation-wide failure counter: the second SCRIPT_NOT_FOUND in an invocation returns SCRIPT_NOT_FOUND_FATAL with an explicit stop instruction, which caps exactly the loop in your trace. If you're on ≥ v2.3.0 and still see an unbounded loop, please also share the tool responses (do they show SCRIPT_NOT_FOUND / SCRIPT_NOT_FOUND_FATAL?) and which model you're using — that would mean the model is ignoring the fatal error and is a distinct, important data point.

    Proposal for the remaining issue (conditional exposure). Independent of the guard, the expected behavior in this report is valid: run_skill_script shouldn't be offered when nothing is runnable. The toolset already has the machinery for this — get_tools() is called per LLM request, activation state is tracked in _adk_activated_skill_{agent_name}, and _use_invocation_cache is disabled precisely for mid-turn tool reloading. Concretely:

    1. In get_tools(), include RunSkillScriptTool only if at least one known skill has scripts, where "known" = statically configured skills (skill.resources.list_scripts() is available at construction time) plus any registry skills already activated/fetched this invocation (their resources are known post-load_skill via the fetch cache).
    2. This answers the registry edge case @godququ5-code raised: with a registry configured, the tool stays hidden until an activated skill actually has scripts — before activation there is nothing runnable, so hiding it is correct rather than lossy. No extra registry round-trips are needed.
    3. Tailor the skill system instruction in process_llm_request so the run_skill_script guidance (step 4) is only appended when the tool is exposed — otherwise the instruction itself invites hallucinated calls even with the tool absent.
    4. Keep the SCRIPT_NOT_FOUND invocation-wide guard as defense in depth: if any one skill has scripts, the tool must remain exposed, so the loop on a scriptless sibling skill is still possible and the guard is what bounds it.

    One design question for maintainers before I open a PR: is a request-to-request change in the exposed tool list acceptable for all supported model backends, or would you prefer the conservative variant (always expose when a registry is configured, filter only in the static-skills case)?

  4. godququ5-code commented on Jul 3, 2026

    @godququ5-code

    Thanks for the detailed analysis. Given your prior work on this code path, I am happy to defer the implementation to you. I will step back unless the maintainers prefer otherwise. I would be glad to help test or review the registry edge cases if useful.

  5. deepakbatham572 commented on Jul 4, 2026

    @deepakbatham572
    Author

    Hi @Raman369AI , please check the #6280 as well if you found that is also similar

    currently i am using :
    Version: 2.2.0

  6. surajksharma07 commented on Jul 6, 2026

    @surajksharma07
    Collaborator

    Confirmed — SkillToolset always registers run_skill_script regardless of whether any skill actually has a scripts/ folder, so the model sees a callable tool it can never satisfy.
    PR #6300 fixes exactly this by only exposing it when a skill has runnable scripts.

    @deepakbatham572 could you confirm your exact version?

    You mentioned 2.2.0 — the SCRIPT_NOT_FOUND fatal-guard (which caps retries at 2 per invocation) landed around 2.3.0, so upgrading might already shrink the loop while we wait on #6300 to merge!

  7. added
    request clarification[Status] The maintainer need clarification or more information from the author
    on Jul 6, 2026
  8. self-assigned this
    on Jul 22, 2026
  9. nicobytes commented on Jul 22, 2026

    @nicobytes
    Contributor

    Cross-linking: we hit the same hallucination of run_skill_script for instruction-only skills (no scripts/), including on Vertex Agent Runtime with Gemini.

    We also found that tool_filter=["list_skills", "load_skill"] is insufficient as a workaround by itself — SkillToolset.process_llm_request still injects a system instruction that advertises run_skill_script / load_skill_resource, so the model keeps calling a tool that is no longer in tools_dict. Details + repro: #6448

    In short: #6281 (don't expose the tool when there are no scripts) + #6448 (keep the injected instruction consistent with tool_filter) seem complementary.

  10. surajksharma07 commented on Jul 28, 2026

    @surajksharma07
    Collaborator

    @nicobytes thanks for confirming this reproduces on Vertex Agent Runtime too — helps rule out a Gemini-specific quirk!

    Agreed that #6281 and #6448 are complementary rather than duplicates: #6300 stops run_skill_script from being exposed in get_tools() when no skill has scripts, while #6449 fixes process_llm_request() still advertising it in the system instruction even when tool_filter excludes it — that's exactly why your filter-only workaround didn't hold.

    Both PRs are open and under review (#6300 has an LGTM pending formal approval; #6449 awaiting review) — flagging to reviewers so they land together, since fixing only one still leaves half the hallucination surface exposed!

    Team is looking more into it.
    Thanks!

  11. removed
    request clarification[Status] The maintainer need clarification or more information from the author
    on Jul 28, 2026
  12. surajksharma07 commented on Aug 26, 2026

    @surajksharma07
    Collaborator

    @deepakbatham572 closing this for now since it's been quiet a while but wanted to flag: this is a confirmed real bug not resolved yet. The fix needs both #6300 (hides run_skill_script when no scripts exist) and #6449 (fixes the system instruction still advertising it), both still open/under review as of today neither merged. Pinging so they don't get lost with the issue closed. Reopen or comment here once either lands or if the loop is still hitting you in the meantime.

  13. added a commit that references this issue on Sep 23, 2026
    d8d9715
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Labels

tools[Component] This issue is related to tools

Type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions