Skip to content

Commit bcb83e9

Browse files
committed
refactor: Move createTool from SkillManager to skillTool.ts
1 parent 96abde7 commit bcb83e9

5 files changed

Lines changed: 252 additions & 180 deletions

File tree

packages/agent-sdk/src/managers/skillManager.ts

Lines changed: 7 additions & 96 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,6 @@ import type {
1111
SkillInvocationContext,
1212
Logger,
1313
} from "../types.js";
14-
import type { ToolPlugin, ToolResult } from "../tools/types.js";
1514
import { parseSkillFile, formatSkillError } from "../utils/skillParser.js";
1615

1716
/**
@@ -76,6 +75,13 @@ export class SkillManager {
7675
}
7776
}
7877

78+
/**
79+
* Check if the skill manager is initialized
80+
*/
81+
isInitialized(): boolean {
82+
return this.initialized;
83+
}
84+
7985
/**
8086
* Get all available skills metadata
8187
*/
@@ -233,101 +239,6 @@ export class SkillManager {
233239
return directories;
234240
}
235241

236-
/**
237-
* Create a tool plugin for registering with ToolManager
238-
*/
239-
createTool(): ToolPlugin {
240-
// Initialize skill manager asynchronously
241-
let initializationPromise: Promise<void> | null = null;
242-
243-
const ensureInitialized = async (): Promise<void> => {
244-
if (!initializationPromise) {
245-
initializationPromise = this.initialize();
246-
}
247-
await initializationPromise;
248-
};
249-
250-
const getToolDescription = (): string => {
251-
if (!this.initialized) {
252-
return "Invoke a Wave skill by name. Skills are user-defined automation templates that can be personal or project-specific. Skills will be loaded during initialization.";
253-
}
254-
255-
const availableSkills = this.getAvailableSkills();
256-
257-
if (availableSkills.length === 0) {
258-
return "Invoke a Wave skill by name. Skills are user-defined automation templates that can be personal or project-specific. No skills are currently available.";
259-
}
260-
261-
const skillList = availableSkills
262-
.map(
263-
(skill) =>
264-
`• **${skill.name}** (${skill.type}): ${skill.description}`,
265-
)
266-
.join("\n");
267-
268-
return `Invoke a Wave skill by name. Skills are user-defined automation templates that can be personal or project-specific.\n\nAvailable skills:\n${skillList}`;
269-
};
270-
271-
return {
272-
name: "skill",
273-
config: {
274-
type: "function",
275-
function: {
276-
name: "skill",
277-
description: getToolDescription(),
278-
parameters: {
279-
type: "object",
280-
properties: {
281-
skill_name: {
282-
type: "string",
283-
description: "Name of the skill to invoke",
284-
enum: this.initialized
285-
? this.getAvailableSkills().map((skill) => skill.name)
286-
: [],
287-
},
288-
},
289-
required: ["skill_name"],
290-
},
291-
},
292-
},
293-
execute: async (args: Record<string, unknown>): Promise<ToolResult> => {
294-
try {
295-
// Ensure skill manager is initialized
296-
await ensureInitialized();
297-
298-
// Validate arguments
299-
const skillName = args.skill_name as string;
300-
if (!skillName || typeof skillName !== "string") {
301-
return {
302-
success: false,
303-
content: "",
304-
error: "skill_name parameter is required and must be a string",
305-
};
306-
}
307-
308-
// Execute the skill
309-
const result = await this.executeSkill({ skill_name: skillName });
310-
311-
return {
312-
success: true,
313-
content: result.content,
314-
shortResult: `Invoked skill: ${skillName}`,
315-
};
316-
} catch (error) {
317-
return {
318-
success: false,
319-
content: "",
320-
error: error instanceof Error ? error.message : String(error),
321-
};
322-
}
323-
},
324-
formatCompactParams: (params: Record<string, unknown>) => {
325-
const skillName = params.skill_name as string;
326-
return skillName || "unknown-skill";
327-
},
328-
};
329-
}
330-
331242
/**
332243
* Execute a skill by name
333244
*/

packages/agent-sdk/src/managers/toolManager.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import { grepTool } from "../tools/grepTool.js";
1010
import { lsTool } from "../tools/lsTool.js";
1111
import { readTool } from "../tools/readTool.js";
1212
import { SkillManager } from "./skillManager.js";
13+
import { createSkillTool } from "../tools/skillTool.js";
1314
import { McpManager } from "./mcpManager.js";
1415
import { ChatCompletionFunctionTool } from "openai/resources.js";
1516
import type { Logger } from "../types.js";
@@ -48,7 +49,7 @@ class ToolManager {
4849
grepTool,
4950
lsTool,
5051
readTool,
51-
new SkillManager({ logger: this.logger }).createTool(),
52+
createSkillTool(new SkillManager({ logger: this.logger })),
5253
];
5354

5455
for (const tool of builtInTools) {
Lines changed: 98 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,98 @@
1+
import type { ToolPlugin, ToolResult } from "./types.js";
2+
import type { SkillManager } from "../managers/skillManager.js";
3+
4+
/**
5+
* Create a skill tool plugin that uses the provided SkillManager
6+
*/
7+
export function createSkillTool(skillManager: SkillManager): ToolPlugin {
8+
// Initialize skill manager asynchronously
9+
let initializationPromise: Promise<void> | null = null;
10+
11+
const ensureInitialized = async (): Promise<void> => {
12+
if (!initializationPromise) {
13+
initializationPromise = skillManager.initialize();
14+
}
15+
await initializationPromise;
16+
};
17+
18+
const getToolDescription = (): string => {
19+
if (!skillManager.isInitialized()) {
20+
return "Invoke a Wave skill by name. Skills are user-defined automation templates that can be personal or project-specific. Skills will be loaded during initialization.";
21+
}
22+
23+
const availableSkills = skillManager.getAvailableSkills();
24+
25+
if (availableSkills.length === 0) {
26+
return "Invoke a Wave skill by name. Skills are user-defined automation templates that can be personal or project-specific. No skills are currently available.";
27+
}
28+
29+
const skillList = availableSkills
30+
.map(
31+
(skill) => `• **${skill.name}** (${skill.type}): ${skill.description}`,
32+
)
33+
.join("\n");
34+
35+
return `Invoke a Wave skill by name. Skills are user-defined automation templates that can be personal or project-specific.\n\nAvailable skills:\n${skillList}`;
36+
};
37+
38+
return {
39+
name: "skill",
40+
config: {
41+
type: "function",
42+
function: {
43+
name: "skill",
44+
description: getToolDescription(),
45+
parameters: {
46+
type: "object",
47+
properties: {
48+
skill_name: {
49+
type: "string",
50+
description: "Name of the skill to invoke",
51+
enum: skillManager.isInitialized()
52+
? skillManager.getAvailableSkills().map((skill) => skill.name)
53+
: [],
54+
},
55+
},
56+
required: ["skill_name"],
57+
},
58+
},
59+
},
60+
execute: async (args: Record<string, unknown>): Promise<ToolResult> => {
61+
try {
62+
// Ensure skill manager is initialized
63+
await ensureInitialized();
64+
65+
// Validate arguments
66+
const skillName = args.skill_name as string;
67+
if (!skillName || typeof skillName !== "string") {
68+
return {
69+
success: false,
70+
content: "",
71+
error: "skill_name parameter is required and must be a string",
72+
};
73+
}
74+
75+
// Execute the skill
76+
const result = await skillManager.executeSkill({
77+
skill_name: skillName,
78+
});
79+
80+
return {
81+
success: true,
82+
content: result.content,
83+
shortResult: `Invoked skill: ${skillName}`,
84+
};
85+
} catch (error) {
86+
return {
87+
success: false,
88+
content: "",
89+
error: error instanceof Error ? error.message : String(error),
90+
};
91+
}
92+
},
93+
formatCompactParams: (params: Record<string, unknown>) => {
94+
const skillName = params.skill_name as string;
95+
return skillName || "unknown-skill";
96+
},
97+
};
98+
}

packages/agent-sdk/tests/managers/skillManager.test.ts

Lines changed: 4 additions & 83 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,10 @@ describe("SkillManager", () => {
4040
});
4141

4242
describe("initialization checks", () => {
43+
it("should return false when not initialized", () => {
44+
expect(skillManager.isInitialized()).toBe(false);
45+
});
46+
4347
it("should throw error if not initialized when getting skills", () => {
4448
expect(() => skillManager.getAvailableSkills()).toThrow(
4549
"SkillManager not initialized. Call initialize() first.",
@@ -53,89 +57,6 @@ describe("SkillManager", () => {
5357
});
5458
});
5559

56-
describe("createTool", () => {
57-
it("should create a tool plugin with correct structure when not initialized", () => {
58-
const tool = skillManager.createTool();
59-
60-
expect(tool.name).toBe("skill");
61-
expect(tool.config).toBeDefined();
62-
expect(tool.config.type).toBe("function");
63-
expect(tool.config.function.name).toBe("skill");
64-
expect(typeof tool.execute).toBe("function");
65-
expect(typeof tool.formatCompactParams).toBe("function");
66-
67-
// When not initialized, enum should be empty
68-
const params = tool.config.function?.parameters as Record<
69-
string,
70-
unknown
71-
>;
72-
const properties = params?.properties as Record<string, unknown>;
73-
const skillName = properties?.skill_name as Record<string, unknown>;
74-
expect(skillName?.enum).toEqual([]);
75-
});
76-
77-
it("should format compact params correctly", () => {
78-
const tool = skillManager.createTool();
79-
const context = { workdir: "/test" };
80-
81-
expect(
82-
tool.formatCompactParams?.({ skill_name: "test-skill" }, context),
83-
).toBe("test-skill");
84-
expect(tool.formatCompactParams?.({}, context)).toBe("unknown-skill");
85-
});
86-
87-
it("should handle tool execution when not initialized", async () => {
88-
// Mock initialization to succeed
89-
vi.spyOn(skillManager, "initialize").mockResolvedValue(undefined);
90-
vi.spyOn(skillManager, "executeSkill").mockResolvedValue({
91-
content: "Test result",
92-
context: { skillName: "test-skill" },
93-
});
94-
95-
const tool = skillManager.createTool();
96-
const context = { workdir: "/test" };
97-
const result = await tool.execute?.(
98-
{ skill_name: "test-skill" },
99-
context,
100-
);
101-
102-
expect(result.success).toBe(true);
103-
expect(result.content).toBe("Test result");
104-
expect(result.shortResult).toBe("Invoked skill: test-skill");
105-
});
106-
107-
it("should validate skill_name parameter", async () => {
108-
const tool = skillManager.createTool();
109-
const context = { workdir: "/test" };
110-
111-
// Mock initialization
112-
vi.spyOn(skillManager, "initialize").mockResolvedValue(undefined);
113-
114-
const result = await tool.execute?.({}, context);
115-
116-
expect(result.success).toBe(false);
117-
expect(result.error).toBe(
118-
"skill_name parameter is required and must be a string",
119-
);
120-
});
121-
122-
it("should handle initialization errors", async () => {
123-
vi.spyOn(skillManager, "initialize").mockRejectedValue(
124-
new Error("Init failed"),
125-
);
126-
127-
const tool = skillManager.createTool();
128-
const context = { workdir: "/test" };
129-
const result = await tool.execute?.(
130-
{ skill_name: "test-skill" },
131-
context,
132-
);
133-
134-
expect(result.success).toBe(false);
135-
expect(result.error).toBe("Init failed");
136-
});
137-
});
138-
13960
describe("executeSkill", () => {
14061
beforeEach(() => {
14162
// Mock initialization to make other methods available

0 commit comments

Comments
 (0)