Skip to content

Feat: add RunSkillScriptErrorCode enum for RunSkillScriptTool - #356

Open
AmaadMartin wants to merge 2 commits into
mainfrom
feat/run-skill-script-error-code-enum
Open

Feat: add RunSkillScriptErrorCode enum for RunSkillScriptTool#356
AmaadMartin wants to merge 2 commits into
mainfrom
feat/run-skill-script-error-code-enum

Conversation

@AmaadMartin

Copy link
Copy Markdown
Owner

Please ensure you have read the contribution guide before creating a pull request.

Link to Issue or Description of Change

  1. Link to an existing issue (if applicable):
    No existing issue.
  2. Or, if no issue exists, describe the change:
    Problem: RunSkillScriptTool and RunSkillInlineScriptTool are sibling tools in SkillToolset that return the same shaped failure payload ({error, errorCode}). The inline tool already models its errorCode values as an exported string enum (RunSkillInlineScriptErrorCode, core/src/tools/skill/run_skill_inline_script_tool.ts:21), documented as part of the tool's response contract and re-exported from core/src/index.ts. The non-inline tool still scatters bare string literals across its seven failure return sites, so a consumer branching on the payload has to hardcode a magic string with no compiler help and no discoverability in the API reference — result.errorCode === 'SCRIPT_NOT_FUOND' compiles fine and is silently never true.

Solution: Declare and export RunSkillScriptErrorCode in run_skill_script_tool.ts, replace all seven literals with its members, and export it from core/src/index.ts. This follows the repo guideline "Error Codes Are Enums: declare an enum and use its members; never scatter bare string literals at throw sites."

The sibling enum is the sole reference for the shape (JSDoc with a {@link} back-reference plus an explicit stability statement, export enum, SCREAMING_SNAKE_CASE members each explicitly initialised to its own name, declared after the imports and above the @experimental class). This is deliberately not an adk-python parity change: adk-python's skill_toolset.py uses a different payload key (error_code) and a different code vocabulary (INVALID_ARGUMENTS, TOOL_ERROR), and aligning with it would move wire values.

Scope and non-goals:

  • Wire values are frozen. All seven strings are byte-identical before and after. A TypeScript string enum member is its string at runtime, so result.errorCode === 'SKILL_NOT_FOUND' keeps working. Purely additive: one new export, no removals, no renames, no signature changes.
  • The two enums are not merged. Only 2 of their 10 distinct members overlap (NO_CODE_EXECUTOR, EXECUTION_ERROR); a shared enum would expose members invalid for one tool or the other.
  • runAsync's return type stays Promise<unknown> — narrowing it to a discriminated union is a real API-surface change and is out of scope here.
  • Not added to core/src/common.ts, matching the inline sibling's placement.

Collision check (required by the pipeline, recorded here): gh pr list --repo AmaadMartin/adk-js --state open --limit 300 returned 260 open PRs; none adds a RunSkillScriptErrorCode enum or any error-code enum for this tool. Two PRs touch the same source file — #353 (fix/skill-script-explicit-output-dir) and #298 (fix/skill-script-output-dir), which appear to be competing implementations of each other. Both are confined to the materializeFiles output-directory call at the bottom of runAsync and the import at the top; gh pr diff 353 --repo ... | grep -iE 'ErrorCode' returns nothing for either. Since their hunks are disjoint from the error-code sites and neither lands this change, this PR branches from main rather than stacking on a PR that may not merge.

Testing Plan

Please describe the tests that you ran to verify your changes. This is required for all PRs that are not small documentation or typo fixes.
Unit Tests:
[x] I have added or updated unit tests for my change.
[x] All unit tests pass locally.

Two tests added to core/test/tools/skills/run_skill_script_tool_test.ts, both pure additions — no existing test was edited, and the existing ToolErrorResponse.errorCode stays typed string with every legacy toEqual still comparing against a raw literal. That is deliberate: those untouched assertions are the independent evidence that the wire values did not move, and retyping them to the enum would have deleted exactly that evidence.

  1. describe('error codes')exposes stable string values for the error-code enum, asserting all seven members, mirroring run_skill_inline_script_tool_test.ts:385-404.
  2. returns error if the code executor throws, covering the EXECUTION_ERROR return site. This closed a real gap rather than padding coverage: MockCodeExecutor.shouldThrow already existed in the file but was never set to true, so the catch branch — one of the seven lines this PR rewrites — had no test at all. Every EXECUTION_ERROR value could have been mistyped and the suite would still have been green.

Proof the tests can fail (mutation testing, both mutations reverted afterwards):

Mutation Result
SCRIPT_NOT_FOUND = 'SCRIPT_NOT_FOUNDX' 2 failures. New contract test: AssertionError: expected 'SCRIPT_NOT_FOUNDX' to be 'SCRIPT_NOT_FOUND' // Object.is equality. Pre-existing untouched test returns error if script not found in skill: AssertionError: expected { …(2) } to deeply equal { …(2) }.
EXECUTION_ERROR = 'EXECUTION_ERRORX' 2 failures. New contract test: expected 'EXECUTION_ERRORX' to be 'EXECUTION_ERROR'. New executor-throws test: expected { …(2) } to deeply equal { …(2) }.

Each mutation fails both the new contract assertion and a payload assertion, which jointly prove the enum member is pinned and that the return site genuinely references the enum rather than a leftover literal. After reverting: 10/10 passing.

Commands run (targeted only, never the full suite):

npx vitest run --project unit:core core/test/tools/skills/run_skill_script_tool_test.ts       # 10 passed
npx vitest run --project unit:core core/test/tools/skills/skill_registry_test.ts \
                                   core/test/tools/skills/run_skill_inline_script_tool_test.ts # 47 passed
npx vitest run --project integration tests/integration/tools/run_skill_script_tool_test.ts     # 8 passed, 4 skipped
npm run lint          # clean, exit 0
npm run format:check  # "All matched files use Prettier code style!", exit 0
npm run docs:check    # clean, exit 0 (typedoc --treatWarningsAsErrors; this PR adds a public export)
npm run build         # exit 0

The skill_registry_test.ts and run_skill_inline_script_tool_test.ts runs are regression checks: the former asserts raw errorCode strings for this tool from another file (REGISTRY_ERROR is pinned only there), the latter confirms the sibling enum was not disturbed. Both pass unmodified.

npm run ts:check fails on this branch and fails identically on the base commit — it is a pre-existing repo-wide failure (~308 errors, addressed by other open PRs). Verified rather than assumed: diffing the error sets from a stashed clean tree against this branch, with line/column numbers normalised, yields an empty diff. This PR introduces zero new type errors. The one error reported inside a file I touch is the pre-existing as File cast at what is now line 227 (was 209 before my insertion), which is not in this diff.

No type-checker or linter suppressions were added anywhere — git diff fork/main -U0 | grep -E '@ts-expect-error|@ts-ignore|eslint-disable|as any|: any|as never|as unknown as' returns nothing.

Manual End-to-End (E2E) Tests:
Verifies the symbol is genuinely reachable from the published package entry point — this is what would break if the index.ts export were forgotten. The built entry points were resolved from core/package.json (main: ./dist/cjs/index.js, exports["."].import: ./dist/esm/index.js) rather than guessed:

npm run build
node -e "const {RunSkillScriptErrorCode} = require('./core/dist/cjs/index.js'); console.log(Object.values(RunSkillScriptErrorCode).join(','));"
node --input-type=module -e "import('./core/dist/esm/index.js').then(m=>console.log(Object.values(m.RunSkillScriptErrorCode).join(',')))"

Both print, exactly and in declaration order:

MISSING_SKILL_NAME,MISSING_SCRIPT_PATH,REGISTRY_ERROR,SKILL_NOT_FOUND,SCRIPT_NOT_FOUND,NO_CODE_EXECUTOR,EXECUTION_ERROR

A consumer can now write:

import {RunSkillScriptErrorCode} from '@google/adk';

if (result.errorCode === RunSkillScriptErrorCode.SCRIPT_NOT_FOUND) {
  // a typo is now a compile error, and the member set shows up in the API docs
}

Postconditions confirmed: grep -rn "errorCode: '" core/src/tools/skill/ returns nothing, so the whole skill/ directory is now literal-free.

Checklist

[x] I have read the CONTRIBUTING.md document.
[x] I have performed a self-review of my own code.
[x] I have commented my code, particularly in hard-to-understand areas.
[x] I have added tests that prove my fix is effective or that my feature works.
[x] New and existing unit tests pass locally with my changes.

Amaad Martin added 2 commits July 30, 2026 20:02
Replace the seven bare errorCode string literals in RunSkillScriptTool
with members of a new exported RunSkillScriptErrorCode enum, mirroring
the sibling RunSkillInlineScriptErrorCode. String values are unchanged,
so the tool's response contract is byte-identical on the wire.

Export the enum from core/src/index.ts so consumers can branch on the
failure payload with compiler support instead of magic strings.
…OR path

Add an 'error codes' contract test asserting each enum member's string
value, mirroring the inline sibling's block.

Also cover the executor-throws branch, which had no test at all: the
MockCodeExecutor.shouldThrow flag existed but was never set, so the
EXECUTION_ERROR return site was unpinned. Existing assertions are left
comparing against raw strings, which is what proves the wire values did
not move.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant