Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 4 additions & 1 deletion core/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,10 @@ export {
RunSkillInlineScriptErrorCode,
RunSkillInlineScriptTool,
} from './tools/skill/run_skill_inline_script_tool.js';
export {RunSkillScriptTool} from './tools/skill/run_skill_script_tool.js';
export {
RunSkillScriptErrorCode,
RunSkillScriptTool,
} from './tools/skill/run_skill_script_tool.js';

export * from './integrations/agent_registry/agent_registry.js';
export * from './telemetry/google_cloud.js';
Expand Down
29 changes: 22 additions & 7 deletions core/src/tools/skill/run_skill_script_tool.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,21 @@ import {materializeFiles} from '../../utils/file_utils.js';
import {BaseTool, RunAsyncToolRequest} from '../base_tool.js';
import {SkillToolset} from './skill_toolset.js';

/**
* Error codes returned by {@link RunSkillScriptTool} when a call cannot be
* completed. The string values are part of the tool's response contract and
* must remain stable.
*/
export enum RunSkillScriptErrorCode {
MISSING_SKILL_NAME = 'MISSING_SKILL_NAME',
MISSING_SCRIPT_PATH = 'MISSING_SCRIPT_PATH',
REGISTRY_ERROR = 'REGISTRY_ERROR',
SKILL_NOT_FOUND = 'SKILL_NOT_FOUND',
SCRIPT_NOT_FOUND = 'SCRIPT_NOT_FOUND',
NO_CODE_EXECUTOR = 'NO_CODE_EXECUTOR',
EXECUTION_ERROR = 'EXECUTION_ERROR',
}

@experimental
export class RunSkillScriptTool extends BaseTool {
constructor(private toolset: SkillToolset) {
Expand Down Expand Up @@ -69,13 +84,13 @@ export class RunSkillScriptTool extends BaseTool {
if (!skillName) {
return {
error: 'Skill name is required.',
errorCode: 'MISSING_SKILL_NAME',
errorCode: RunSkillScriptErrorCode.MISSING_SKILL_NAME,
};
}
if (!scriptPath) {
return {
error: 'Script path is required.',
errorCode: 'MISSING_SCRIPT_PATH',
errorCode: RunSkillScriptErrorCode.MISSING_SCRIPT_PATH,
};
}

Expand All @@ -88,14 +103,14 @@ export class RunSkillScriptTool extends BaseTool {
} catch (e: unknown) {
return {
error: `Failed to fetch skill '${skillName}' from registry: ${(e as Error).message || e}`,
errorCode: 'REGISTRY_ERROR',
errorCode: RunSkillScriptErrorCode.REGISTRY_ERROR,
};
}

if (!skill) {
return {
error: `Skill '${skillName}' not found.`,
errorCode: 'SKILL_NOT_FOUND',
errorCode: RunSkillScriptErrorCode.SKILL_NOT_FOUND,
};
}

Expand All @@ -110,7 +125,7 @@ export class RunSkillScriptTool extends BaseTool {
if (!script) {
return {
error: `Script '${scriptPath}' not found in skill '${skillName}'.`,
errorCode: 'SCRIPT_NOT_FOUND',
errorCode: RunSkillScriptErrorCode.SCRIPT_NOT_FOUND,
};
}

Expand All @@ -125,7 +140,7 @@ export class RunSkillScriptTool extends BaseTool {
if (!codeExecutor) {
return {
error: 'No code executor configured.',
errorCode: 'NO_CODE_EXECUTOR',
errorCode: RunSkillScriptErrorCode.NO_CODE_EXECUTOR,
};
}

Expand All @@ -148,7 +163,7 @@ export class RunSkillScriptTool extends BaseTool {
} catch (e: unknown) {
return {
error: `Failed to execute script '${scriptPath}': ${(e as Error).message}`,
errorCode: 'EXECUTION_ERROR',
errorCode: RunSkillScriptErrorCode.EXECUTION_ERROR,
};
}
}
Expand Down
36 changes: 36 additions & 0 deletions core/test/tools/skills/run_skill_script_tool_test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import {
File,
InvocationContext,
LlmAgent,
RunSkillScriptErrorCode,
RunSkillScriptTool,
Skill,
SkillToolset,
Expand Down Expand Up @@ -158,6 +159,23 @@ describe('RunSkillScriptTool', () => {
});
});

it('returns error if the code executor throws', async () => {
const mockExecutor = new MockCodeExecutor();
mockExecutor.shouldThrow = true;
const toolset = new SkillToolset([mockSkill], {codeExecutor: mockExecutor});
const tool = new RunSkillScriptTool(toolset);
const result = (await tool.runAsync({
args: {skill_name: 'test-skill', script_path: 'scripts/setup.js'},
toolContext: createMockContext(),
})) as ToolErrorResponse;

expect(result).toEqual({
error:
"Failed to execute script 'scripts/setup.js': Mock execution failure",
errorCode: 'EXECUTION_ERROR',
});
});

it('executes script successfully via mock executor with JS wrapper', async () => {
const mockExecutor = new MockCodeExecutor();
const toolset = new SkillToolset([mockSkill], {codeExecutor: mockExecutor});
Expand Down Expand Up @@ -228,4 +246,22 @@ describe('RunSkillScriptTool', () => {

expect(materializeFiles).toHaveBeenCalledWith([testFile]);
});

describe('error codes', () => {
it('exposes stable string values for the error-code enum', () => {
// The error-code string values are part of the tool's response contract
// and must remain stable across releases.
expect(RunSkillScriptErrorCode.MISSING_SKILL_NAME).toBe(
'MISSING_SKILL_NAME',
);
expect(RunSkillScriptErrorCode.MISSING_SCRIPT_PATH).toBe(
'MISSING_SCRIPT_PATH',
);
expect(RunSkillScriptErrorCode.REGISTRY_ERROR).toBe('REGISTRY_ERROR');
expect(RunSkillScriptErrorCode.SKILL_NOT_FOUND).toBe('SKILL_NOT_FOUND');
expect(RunSkillScriptErrorCode.SCRIPT_NOT_FOUND).toBe('SCRIPT_NOT_FOUND');
expect(RunSkillScriptErrorCode.NO_CODE_EXECUTOR).toBe('NO_CODE_EXECUTOR');
expect(RunSkillScriptErrorCode.EXECUTION_ERROR).toBe('EXECUTION_ERROR');
});
});
});
Loading