Fix: stop repeated skill-script lookup retries with SCRIPT_NOT_FOUND_FATAL - #729
Open
AmaadMartin wants to merge 1 commit into
Open
Conversation
Every failed script lookup returned the same retry-friendly SCRIPT_NOT_FOUND response, so a model guessing script paths could burn a whole invocation on hallucinated paths. Count the misses per invocation and escalate to SCRIPT_NOT_FOUND_FATAL from the second miss onward, matching the adk-python run_skill_script guard. The counter key is temp:-prefixed so it never reaches durable session storage, and carries the invocation id so a new invocation starts over.
This was referenced Aug 9, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Please ensure you have read the contribution guide before creating a pull request.
Link to Issue or Description of Change
None.
Problem:
run_skill_scriptreturns the sameSCRIPT_NOT_FOUNDresponse on every failed script lookup. Nothing in that response tells the model to stop, so a model that guesses paths (scripts/setup.js, thenscripts/setup.ts, thensetup/main.js) gets an identical retry-friendly error each time and can burn a whole invocation. The Python SDK already guards against this insrc/google/adk/tools/skill_toolset.py; adk-js does not.Solution: Count script-lookup misses per invocation and escalate to
SCRIPT_NOT_FOUND_FATALfrom the second miss onward. The counter is keyed only by invocation id, not by skill or path, so the guard still fires when the model varies the path on each retry. The key istemp:-prefixed so session services strip it before persisting, and the invocation id suffix stops in-memory backends from carrying a count into the next invocation.Stacked on #356. That PR introduces
RunSkillScriptErrorCodeand converts the existing literals. This branch adds theSCRIPT_NOT_FOUND_FATALmember and the guard on top of it, so the enum work is not duplicated. Merge #356 first. The diff below is only this change.Collision check. I listed all 621 open PRs on the fork and read every one whose title or branch mentioned skill, script, not-found, fatal or retry. #356 (
feat/run-skill-script-error-code-enum) touches the same three files and is the stack base. #634 (fix/skill-script-wrapper-path-normalization) rewrites the lines just above theif (!script)block but leaves the block body alone, so the two changes are independent. No open PR implements this guard.Parity vs. local convention. Parity wins for the cross-boundary observables: the state key string, the
SCRIPT_NOT_FOUND_FATALvalue and the message text are byte-identical to Python, em dash included. Local TypeScript convention wins for the enum, the camelCaseerrorCoderesponse key and the module layout.Testing Plan
Unit Tests:
npx vitest run --project unit:core core/test/tools/skills/run_skill_script_tool_test.ts— 15 passed.Five new cases in
core/test/tools/skills/run_skill_script_tool_test.ts: first miss staysSCRIPT_NOT_FOUND; second miss on the same path escalates; second miss on a different path also escalates; a new invocation id resets the count; and the new enum member's string value. The existing'returns error if script not found in skill'test is unchanged and still pins first-miss behaviour. The only edit to existing test code iscreateMockContext, which gained two optional trailing parameters, so all previous call sites are untouched.New lines and branches in
run_skill_script_tool.tsare at 100% coverage. Whole-file coverage reads 79.32% because this one test file does not reach_getDeclaration, the registry-error catch orgetSkillResourceFiles; those gaps are pre-existing.Mutation results — every new test was proven able to fail:
expected 'SCRIPT_NOT_FOUND' to be 'SCRIPT_NOT_FOUND_FATAL'failCount > 1->failCount > 2expected 'SCRIPT_NOT_FOUND' to be 'SCRIPT_NOT_FOUND_FATAL'${toolContext.invocationId}from the keyexpected { …(2) } to deeply equal { …(2) }on the state-key assertionscriptPathexpected { …(2) } to deeply equal { …(2) }Manual End-to-End (E2E) Tests:
No integration test. The guard is in-memory state arithmetic on an existing error path with no service, executor or network dependency, and the unit tests drive it through the tool's public
runAsync.CI on this PR is absent:
validation.yamlruns onpull_request: branches: [main], and this PR targetsfeat/run-skill-script-error-code-enum. Validated locally on commit14a44f65instead:Root
npx tsc --noEmitreports 308 errors on this branch and 308 on the base branch, so this change adds none.Checklist