@agentscope-ai/agentscope 0.0.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (136) hide show
  1. package/dist/agent/index.d.mts +234 -0
  2. package/dist/agent/index.d.ts +234 -0
  3. package/dist/agent/index.js +1412 -0
  4. package/dist/agent/index.js.map +1 -0
  5. package/dist/agent/index.mjs +1375 -0
  6. package/dist/agent/index.mjs.map +1 -0
  7. package/dist/base-BOx3UzOl.d.mts +41 -0
  8. package/dist/base-BoIps2RL.d.ts +41 -0
  9. package/dist/base-C7jwyH4Z.d.mts +52 -0
  10. package/dist/base-Cwi4bjze.d.ts +127 -0
  11. package/dist/base-DYlBMCy_.d.mts +127 -0
  12. package/dist/base-NX-knWOv.d.ts +52 -0
  13. package/dist/block-VsnHrllL.d.mts +48 -0
  14. package/dist/block-VsnHrllL.d.ts +48 -0
  15. package/dist/event/index.d.mts +181 -0
  16. package/dist/event/index.d.ts +181 -0
  17. package/dist/event/index.js +58 -0
  18. package/dist/event/index.js.map +1 -0
  19. package/dist/event/index.mjs +33 -0
  20. package/dist/event/index.mjs.map +1 -0
  21. package/dist/formatter/index.d.mts +187 -0
  22. package/dist/formatter/index.d.ts +187 -0
  23. package/dist/formatter/index.js +647 -0
  24. package/dist/formatter/index.js.map +1 -0
  25. package/dist/formatter/index.mjs +616 -0
  26. package/dist/formatter/index.mjs.map +1 -0
  27. package/dist/index-BTJDlKvQ.d.mts +195 -0
  28. package/dist/index-BcatlwXQ.d.ts +195 -0
  29. package/dist/index-CAxQAkiP.d.mts +21 -0
  30. package/dist/index-CAxQAkiP.d.ts +21 -0
  31. package/dist/mcp/index.d.mts +9 -0
  32. package/dist/mcp/index.d.ts +9 -0
  33. package/dist/mcp/index.js +432 -0
  34. package/dist/mcp/index.js.map +1 -0
  35. package/dist/mcp/index.mjs +408 -0
  36. package/dist/mcp/index.mjs.map +1 -0
  37. package/dist/message/index.d.mts +10 -0
  38. package/dist/message/index.d.ts +10 -0
  39. package/dist/message/index.js +67 -0
  40. package/dist/message/index.js.map +1 -0
  41. package/dist/message/index.mjs +37 -0
  42. package/dist/message/index.mjs.map +1 -0
  43. package/dist/message-CkN21KaY.d.mts +99 -0
  44. package/dist/message-CzLeTlua.d.ts +99 -0
  45. package/dist/model/index.d.mts +377 -0
  46. package/dist/model/index.d.ts +377 -0
  47. package/dist/model/index.js +1880 -0
  48. package/dist/model/index.js.map +1 -0
  49. package/dist/model/index.mjs +1849 -0
  50. package/dist/model/index.mjs.map +1 -0
  51. package/dist/storage/index.d.mts +68 -0
  52. package/dist/storage/index.d.ts +68 -0
  53. package/dist/storage/index.js +250 -0
  54. package/dist/storage/index.js.map +1 -0
  55. package/dist/storage/index.mjs +212 -0
  56. package/dist/storage/index.mjs.map +1 -0
  57. package/dist/tool/index.d.mts +311 -0
  58. package/dist/tool/index.d.ts +311 -0
  59. package/dist/tool/index.js +1494 -0
  60. package/dist/tool/index.js.map +1 -0
  61. package/dist/tool/index.mjs +1447 -0
  62. package/dist/tool/index.mjs.map +1 -0
  63. package/dist/toolkit-CEpulFi0.d.ts +99 -0
  64. package/dist/toolkit-CGEZSZPa.d.mts +99 -0
  65. package/jest.config.js +11 -0
  66. package/package.json +92 -0
  67. package/src/_utils/common.ts +104 -0
  68. package/src/_utils/index.ts +1 -0
  69. package/src/agent/agent-base.ts +0 -0
  70. package/src/agent/agent.test.ts +1028 -0
  71. package/src/agent/agent.ts +1032 -0
  72. package/src/agent/index.ts +2 -0
  73. package/src/agent/interfaces.ts +23 -0
  74. package/src/agent/test-compression.ts +72 -0
  75. package/src/event/index.ts +250 -0
  76. package/src/formatter/base.ts +133 -0
  77. package/src/formatter/dashscope-chat-formatter.test.ts +372 -0
  78. package/src/formatter/dashscope-chat-formatter.ts +163 -0
  79. package/src/formatter/deepseek-chat-formatter.ts +130 -0
  80. package/src/formatter/index.ts +5 -0
  81. package/src/formatter/ollama-chat-formatter.ts +67 -0
  82. package/src/formatter/openai-chat-formatter.test.ts +263 -0
  83. package/src/formatter/openai-chat-formatter.ts +301 -0
  84. package/src/formatter/openai.md +767 -0
  85. package/src/mcp/base.ts +114 -0
  86. package/src/mcp/http.test.ts +303 -0
  87. package/src/mcp/http.ts +224 -0
  88. package/src/mcp/index.ts +2 -0
  89. package/src/mcp/stdio.test.ts +91 -0
  90. package/src/mcp/stdio.ts +119 -0
  91. package/src/message/block.ts +60 -0
  92. package/src/message/enums.ts +4 -0
  93. package/src/message/index.ts +12 -0
  94. package/src/message/message.test.ts +80 -0
  95. package/src/message/message.ts +131 -0
  96. package/src/model/base.ts +226 -0
  97. package/src/model/dashscope-model.test.ts +335 -0
  98. package/src/model/dashscope-model.ts +441 -0
  99. package/src/model/deepseek-model.test.ts +279 -0
  100. package/src/model/deepseek-model.ts +401 -0
  101. package/src/model/index.ts +7 -0
  102. package/src/model/ollama-model.test.ts +307 -0
  103. package/src/model/ollama-model.ts +356 -0
  104. package/src/model/openai-model.ts +327 -0
  105. package/src/model/response.ts +22 -0
  106. package/src/model/usage.ts +12 -0
  107. package/src/storage/base.ts +52 -0
  108. package/src/storage/file-system.test.ts +587 -0
  109. package/src/storage/file-system.ts +269 -0
  110. package/src/storage/index.ts +2 -0
  111. package/src/tool/base.ts +23 -0
  112. package/src/tool/bash.test.ts +174 -0
  113. package/src/tool/bash.ts +152 -0
  114. package/src/tool/edit.test.ts +83 -0
  115. package/src/tool/edit.ts +95 -0
  116. package/src/tool/glob.test.ts +63 -0
  117. package/src/tool/glob.ts +166 -0
  118. package/src/tool/grep.test.ts +74 -0
  119. package/src/tool/grep.ts +256 -0
  120. package/src/tool/index.ts +10 -0
  121. package/src/tool/read.test.ts +77 -0
  122. package/src/tool/read.ts +117 -0
  123. package/src/tool/response.ts +82 -0
  124. package/src/tool/task.test.ts +299 -0
  125. package/src/tool/task.ts +399 -0
  126. package/src/tool/toolkit.test.ts +636 -0
  127. package/src/tool/toolkit.ts +601 -0
  128. package/src/tool/write.test.ts +52 -0
  129. package/src/tool/write.ts +57 -0
  130. package/src/type/index.ts +52 -0
  131. package/tsconfig.build.json +4 -0
  132. package/tsconfig.cjs.json +11 -0
  133. package/tsconfig.esm.json +10 -0
  134. package/tsconfig.json +14 -0
  135. package/tsup.config.ts +20 -0
  136. package/typedoc.json +52 -0
@@ -0,0 +1,601 @@
1
+ import * as fs from 'fs';
2
+ import * as path from 'path';
3
+
4
+ import { Validator } from '@cfworker/json-schema';
5
+ import matter from 'gray-matter';
6
+ import { z } from 'zod';
7
+
8
+ import { createToolResponse, isToolResponse, ToolResponse } from './response';
9
+ import { HTTPMCPClient, StdioMCPClient } from '../mcp';
10
+ import { ToolCallBlock } from '../message';
11
+ import { ToolInputSchema, ToolSchema } from '../type';
12
+ import { Tool } from './base';
13
+ import { _jsonLoadsWithRepair } from '../_utils';
14
+
15
+ interface RegisteredTool extends Tool {
16
+ type: 'function' | 'mcp';
17
+ mcpName?: string;
18
+ }
19
+
20
+ /**
21
+ * The toolkit module in AgentScope, which is responsible for registering tool functions, MCP, and agent skills.
22
+ * It also provides group-wise management of tools.
23
+ */
24
+ export class Toolkit {
25
+ tools: RegisteredTool[];
26
+ skills: string[];
27
+ skillDirs: string[];
28
+
29
+ // The cache mapping from the skill name to its corresponding tool name in the toolkit.
30
+ private _skillCache: { [name: string]: string };
31
+
32
+ /**
33
+ * Initializes a new instance of the Toolkit class.
34
+ * @param config - The configuration object for initializing the toolkit, which can include an array of tools, an array of skill paths, an array of skill directory paths, and a boolean indicating whether to include the built-in skill tool for reading SKILL.md files.
35
+ * @param config.tools - An array of tool definitions to register in the toolkit.
36
+ * @param config.skills - An array of file paths pointing to individual skills.
37
+ * @param config.skillDirs - An array of directory paths, where each directory can contain multiple skills in its subdirectories.
38
+ * @param config.builtInSkillTool - A boolean flag indicating whether to include the built-in skill tool for reading SKILL.md files.
39
+ */
40
+ constructor(config?: {
41
+ tools?: Tool[];
42
+ skills?: string[];
43
+ skillDirs?: string[];
44
+ builtInSkillTool?: boolean;
45
+ }) {
46
+ const { tools = [], skills = [], skillDirs = [], builtInSkillTool = true } = config || {};
47
+
48
+ this.tools = [];
49
+
50
+ if (builtInSkillTool) {
51
+ this.tools.push({
52
+ type: 'function',
53
+ name: 'Skill',
54
+ description: `Retrieves the full content of a skill by reading its SKILL.md file. Skills are packages of domain expertise that extend agent capabilities. Use this tool to access detailed instructions, examples, and guidelines for a specific skill.
55
+
56
+ Usage:
57
+ - Provide the skill name as the input parameter
58
+ - The tool will return the complete SKILL.md file content for that skill
59
+ - If the skill is not found, an error message with available skills will be returned
60
+ - Available skills are listed in the skills-system section of the agent prompt`,
61
+ inputSchema: z.object({ name: z.string().describe('The name of the skill') }),
62
+ call: this._skillTool.bind(this),
63
+ requireUserConfirm: false,
64
+ });
65
+ }
66
+
67
+ tools.map(tool => {
68
+ this.tools.push({
69
+ type: 'function',
70
+ ...tool,
71
+ });
72
+ });
73
+
74
+ this.skills = skills;
75
+ this.skillDirs = skillDirs;
76
+
77
+ this._skillCache = {};
78
+ }
79
+
80
+ /**
81
+ * Registers a tool function to the toolkit. The function can be either a plain function that adheres to the ToolFunction type, or an instance of a class that extends ToolBase. When registering a plain function, the name, description, and input schema must be provided explicitly. When registering a ToolBase instance, these properties will be extracted from the instance itself.
82
+ *
83
+ * @params tool - The tool function to register, which can be either a plain function with explicit properties or an instance of a class that extends ToolBase.
84
+ * @returns The Toolkit instance with the new tool function registered
85
+ * @param tool
86
+ */
87
+ registerToolFunction(tool: Tool): Toolkit {
88
+ this.tools.push({
89
+ type: 'function',
90
+ ...tool,
91
+ });
92
+ return this;
93
+ }
94
+
95
+ /**
96
+ * Registers functions from a given MCP client.
97
+ *
98
+ * @param root0
99
+ * @param root0.client
100
+ * @param root0.enabledTools
101
+ * @param root0.disabledTools
102
+ * @param root0.requireUserConfirm
103
+ * @returns The Toolkit instance with the new tools registered
104
+ */
105
+ async registerMCPClient({
106
+ client,
107
+ enabledTools,
108
+ disabledTools = [],
109
+ requireUserConfirm = false,
110
+ }: {
111
+ client: HTTPMCPClient | StdioMCPClient;
112
+ enabledTools?: string[];
113
+ disabledTools?: string[];
114
+ requireUserConfirm?: boolean;
115
+ }): Promise<Toolkit> {
116
+ const tools = await client.listTools();
117
+
118
+ const appendTools: string[] = [];
119
+ tools
120
+ .filter(
121
+ tool =>
122
+ !(enabledTools && !enabledTools.includes(tool.name)) &&
123
+ !disabledTools.includes(tool.name)
124
+ )
125
+ .forEach(tool => {
126
+ this.tools.push({
127
+ type: 'mcp',
128
+ mcpName: client.name,
129
+ ...tool,
130
+ requireUserConfirm,
131
+ });
132
+ appendTools.push(tool.name);
133
+ });
134
+ console.log(`Registered tools from MCP client '${client.name}': ${appendTools.join(', ')}`);
135
+ return this;
136
+ }
137
+
138
+ /**
139
+ * Executes a registered tool function based on the provided ToolUseBlock.
140
+ * Note this method always returns an AsyncGenerator of ToolResponse, regardless of the tool function type.
141
+ *
142
+ * @param toolCall - The ToolUseBlock containing the tool name and input arguments
143
+ * @yields Incremental ToolResponse objects as they are produced by the tool function
144
+ * @returns The final complete ToolResponse after the tool function execution is finished
145
+ */
146
+ async *callToolFunction(toolCall: ToolCallBlock): AsyncGenerator<ToolResponse, ToolResponse> {
147
+ // If the tool is registered
148
+ const tool = this.tools.find(tool => tool.name === toolCall.name);
149
+
150
+ if (!tool) {
151
+ const notFoundRes = createToolResponse({
152
+ content: [
153
+ {
154
+ id: crypto.randomUUID(),
155
+ type: 'text',
156
+ text: `FunctionNotFoundError: Cannot find the function named ${toolCall.name}`,
157
+ },
158
+ ],
159
+ state: 'error',
160
+ });
161
+ yield notFoundRes;
162
+ return notFoundRes;
163
+ }
164
+
165
+ // Parse the input arguments using the tool's schema
166
+ let parsedInput: Record<string, unknown>;
167
+ try {
168
+ parsedInput = _jsonLoadsWithRepair(toolCall.input);
169
+ if (tool.inputSchema instanceof z.ZodObject) {
170
+ tool.inputSchema.parse(parsedInput);
171
+ } else {
172
+ //
173
+ const validator = new Validator(tool.inputSchema);
174
+ const validation = validator.validate(parsedInput);
175
+ if (!validation.valid) {
176
+ throw new Error(`Invalid input arguments: ${validation.errors}`);
177
+ }
178
+ }
179
+ } catch (error) {
180
+ const parseErrorRes = createToolResponse({
181
+ content: [
182
+ {
183
+ id: crypto.randomUUID(),
184
+ type: 'text',
185
+ text: `InvalidArgumentError: ${String(error)}`,
186
+ },
187
+ ],
188
+ state: 'error',
189
+ });
190
+ yield parseErrorRes;
191
+ return parseErrorRes;
192
+ }
193
+
194
+ // Log the tool call with parsed input
195
+ if (!tool.call) {
196
+ throw new Error(
197
+ `Cannot execute external tool '${toolCall.name}' because no call method is defined for it in the toolkit.`
198
+ );
199
+ }
200
+
201
+ // Execute the tool function and await the result
202
+ // Note: await on a non-Promise value returns the value itself
203
+ let finalRes: ToolResponse | null = null;
204
+ try {
205
+ const res = await tool.call(parsedInput);
206
+
207
+ // If res is a string
208
+ if (typeof res === 'string') {
209
+ const textRes = createToolResponse({
210
+ content: [
211
+ {
212
+ id: crypto.randomUUID(),
213
+ type: 'text',
214
+ text: res,
215
+ },
216
+ ],
217
+ state: 'success',
218
+ });
219
+ yield textRes;
220
+ finalRes = textRes;
221
+ } else if (isToolResponse(res)) {
222
+ // If res is a ToolResponse
223
+ yield res as ToolResponse;
224
+ finalRes = res as ToolResponse;
225
+ } else if (Symbol.asyncIterator in res) {
226
+ // If res is an AsyncGenerator of string or ToolResponse
227
+ const accContent: ToolResponse['content'] = [];
228
+ let nextResult = await (res as AsyncGenerator<string | ToolResponse>).next();
229
+
230
+ while (!nextResult.done) {
231
+ const currentValue = nextResult.value;
232
+ // Peek ahead to determine if this is the last value
233
+ nextResult = await (res as AsyncGenerator<string | ToolResponse>).next();
234
+ const isLastValue = nextResult.done;
235
+
236
+ if (typeof currentValue === 'string') {
237
+ const itemRes = createToolResponse({
238
+ content: [
239
+ {
240
+ id: crypto.randomUUID(),
241
+ type: 'text',
242
+ text: currentValue,
243
+ },
244
+ ],
245
+ isLast: isLastValue,
246
+ state: 'running',
247
+ });
248
+ yield itemRes;
249
+
250
+ // Accumulate the text content into finalRes
251
+ accContent.push({
252
+ id: crypto.randomUUID(),
253
+ type: 'text',
254
+ text: currentValue,
255
+ });
256
+ } else if (isToolResponse(currentValue)) {
257
+ // Use the isLast from the ToolResponse if set, otherwise use our calculated value
258
+ currentValue.isLast = currentValue.isLast ?? isLastValue;
259
+ yield currentValue as ToolResponse;
260
+
261
+ // Accumulate the content of the ToolResponse into finalRes
262
+ accContent.push(...currentValue.content);
263
+ }
264
+ }
265
+ finalRes = createToolResponse({
266
+ content: accContent,
267
+ state: 'success',
268
+ });
269
+ } else if (Symbol.iterator in res) {
270
+ // If res is a Generator of string or ToolResponse
271
+ const accContent: ToolResponse['content'] = [];
272
+ let nextResult = (res as Generator<string | ToolResponse>).next();
273
+
274
+ while (!nextResult.done) {
275
+ const currentValue = nextResult.value;
276
+ // Peek ahead to determine if this is the last value
277
+ nextResult = (res as Generator<string | ToolResponse>).next();
278
+ const isLastValue = nextResult.done;
279
+
280
+ if (typeof currentValue === 'string') {
281
+ const itemRes = createToolResponse({
282
+ content: [
283
+ {
284
+ id: crypto.randomUUID(),
285
+ type: 'text',
286
+ text: currentValue,
287
+ },
288
+ ],
289
+ isLast: isLastValue,
290
+ state: 'running',
291
+ });
292
+ yield itemRes;
293
+ // Accumulate the text content into finalRes
294
+ accContent.push({
295
+ id: crypto.randomUUID(),
296
+ type: 'text',
297
+ text: currentValue,
298
+ });
299
+ } else if (isToolResponse(currentValue)) {
300
+ // Use the isLast from the ToolResponse if set, otherwise use our calculated value
301
+ currentValue.isLast = currentValue.isLast ?? isLastValue;
302
+ yield currentValue as ToolResponse;
303
+ // Accumulate the content of the ToolResponse into finalRes
304
+ accContent.push(...currentValue.content);
305
+ }
306
+ }
307
+ finalRes = createToolResponse({
308
+ content: accContent,
309
+ state: 'success',
310
+ });
311
+ } else {
312
+ const invalidRes = createToolResponse({
313
+ content: [
314
+ {
315
+ id: crypto.randomUUID(),
316
+ type: 'text',
317
+ text: String(res),
318
+ },
319
+ ],
320
+ state: 'running',
321
+ });
322
+ yield invalidRes;
323
+ finalRes = invalidRes;
324
+ }
325
+ } catch (error) {
326
+ const errorRes = createToolResponse({
327
+ content: [
328
+ {
329
+ id: crypto.randomUUID(),
330
+ type: 'text',
331
+ text: `ToolExecutionError: ${String(error)}`,
332
+ },
333
+ ],
334
+ state: 'error',
335
+ });
336
+ yield errorRes;
337
+ finalRes = errorRes;
338
+ }
339
+
340
+ if (!finalRes) {
341
+ return createToolResponse({
342
+ content: [
343
+ {
344
+ id: crypto.randomUUID(),
345
+ type: 'text',
346
+ text: `Tool ${toolCall.name} executed successfully.`,
347
+ },
348
+ ],
349
+ state: 'success',
350
+ });
351
+ }
352
+
353
+ // Clean the finalRes by merging the adjacent text blocks into one block, leaving
354
+ // multimodal content blocks (e.g. image, audio) unchanged
355
+ const cleanedContent: ToolResponse['content'] = [];
356
+ let textBuffer = '';
357
+ for (const block of finalRes.content) {
358
+ if (block.type === 'text') {
359
+ textBuffer += block.text;
360
+ } else {
361
+ if (textBuffer) {
362
+ cleanedContent.push({
363
+ id: crypto.randomUUID(),
364
+ type: 'text',
365
+ text: textBuffer,
366
+ });
367
+ textBuffer = '';
368
+ }
369
+ cleanedContent.push(block);
370
+ }
371
+ }
372
+ // The remaining text in the buffer, if any, should also be pushed to the cleanedContent
373
+ if (textBuffer) {
374
+ cleanedContent.push({
375
+ id: crypto.randomUUID(),
376
+ type: 'text',
377
+ text: textBuffer,
378
+ });
379
+ }
380
+
381
+ return {
382
+ ...finalRes,
383
+ content: cleanedContent,
384
+ };
385
+ }
386
+
387
+ /**
388
+ * Returns the JSON schemas for all registered tools in a format compatible with LLM APIs.
389
+ *
390
+ * @returns An array of ToolJSONSchema objects
391
+ */
392
+ getJSONSchemas(): ToolSchema[] {
393
+ return this.tools.map(tool => {
394
+ const inputSchema =
395
+ tool.inputSchema instanceof z.ZodObject
396
+ ? tool.inputSchema.toJSONSchema({ target: 'openapi-3.0' })
397
+ : tool.inputSchema;
398
+
399
+ return {
400
+ type: 'function',
401
+ function: {
402
+ name: tool.name,
403
+ description: tool.description,
404
+ parameters: inputSchema as ToolInputSchema,
405
+ },
406
+ };
407
+ });
408
+ }
409
+
410
+ /**
411
+ * Get the instruction prompt for the agent to use the skills.
412
+ *
413
+ * @returns A string containing the instruction prompt of the available skills and how to use them.
414
+ */
415
+ getSkillsPrompt(): string {
416
+ this._skillCache = {};
417
+ if (this.skills.length === 0 && this.skillDirs.length === 0) return '';
418
+
419
+ if (typeof process !== 'undefined' && process.versions && process.versions.node) {
420
+ const skillsInfo: { name: string; description: string; location: string }[] = [];
421
+ this.skills.forEach(skillPath => {
422
+ // 首先获取绝对路径
423
+ const absSkillPath = path.resolve(skillPath);
424
+
425
+ // Check if directory exists
426
+ if (!fs.existsSync(absSkillPath) || !fs.statSync(absSkillPath).isDirectory()) {
427
+ return;
428
+ }
429
+
430
+ // First, check if SKILL.md exists directly in this directory
431
+ const skillMdPath = path.join(absSkillPath, 'SKILL.md');
432
+ if (!fs.existsSync(skillMdPath)) return;
433
+
434
+ // Read the SKILL.md file and extract the name and description from the YAML front matter
435
+ try {
436
+ const content = fs.readFileSync(skillMdPath, 'utf-8');
437
+ const { data } = matter(content);
438
+
439
+ const name = data.name || path.basename(skillPath);
440
+ const description = data.description || 'No description provided';
441
+
442
+ skillsInfo.push({
443
+ name,
444
+ description,
445
+ location: absSkillPath,
446
+ });
447
+
448
+ this._skillCache[name] = absSkillPath;
449
+ } catch (e) {
450
+ console.error(`Error reading SKILL.md for skill at ${skillPath}:`, e);
451
+ }
452
+ });
453
+
454
+ this.skillDirs.forEach(skillDir => {
455
+ const absSkillDir = path.resolve(skillDir);
456
+
457
+ // Check if directory exists
458
+ if (!fs.existsSync(absSkillDir) || !fs.statSync(absSkillDir).isDirectory()) {
459
+ return;
460
+ }
461
+
462
+ // Read all subdirectories in the skillDir
463
+ const subdirs = fs.readdirSync(absSkillDir).filter(subdir => {
464
+ const subdirPath = path.join(absSkillDir, subdir);
465
+ return fs.statSync(subdirPath).isDirectory();
466
+ });
467
+
468
+ subdirs.forEach(subdir => {
469
+ const skillMdPath = path.join(absSkillDir, subdir, 'SKILL.md');
470
+ if (!fs.existsSync(skillMdPath)) return;
471
+
472
+ try {
473
+ const content = fs.readFileSync(skillMdPath, 'utf-8');
474
+ const { data } = matter(content);
475
+
476
+ const name = data.name || subdir;
477
+ const description = data.description || 'No description provided';
478
+
479
+ skillsInfo.push({
480
+ name,
481
+ description,
482
+ location: path.join(skillDir, subdir),
483
+ });
484
+
485
+ this._skillCache[name] = path.join(absSkillDir, subdir);
486
+ } catch (e) {
487
+ console.error(
488
+ `Error reading SKILL.md for skill at ${path.join(skillDir, subdir)}:`,
489
+ e
490
+ );
491
+ }
492
+ });
493
+ });
494
+
495
+ if (skillsInfo.length === 0) return '';
496
+
497
+ const skillsXml = skillsInfo
498
+ .map(
499
+ skill => `<skill>
500
+ <name>${skill.name}</name>
501
+ <description>${skill.description}</description>
502
+ <location>${skill.location}</location>
503
+ </skill>`
504
+ )
505
+ .reduce((acc, skillInfo) => acc + `\n${skillInfo}\n`, '');
506
+
507
+ return `<skills-system>
508
+ ## What are Skills?
509
+ Skills are packages of domain expertise that extend your capabilities.
510
+
511
+ ## Important: How to Use Skills
512
+ **Skill names are NOT callable functions.** You cannot call a skill directly by its name.
513
+ ${skillsXml}
514
+ </skills-system>`;
515
+ }
516
+
517
+ return '';
518
+ }
519
+
520
+ /**
521
+ * The agent skill tool to read SKILL.md file content based on the skill name.
522
+ * @param root0
523
+ * @param root0.name
524
+ * @returns The content of the SKILL.md file for the specified skill, or an error message if the skill is not
525
+ * found or the SKILL.md file cannot be read.
526
+ */
527
+ private async _skillTool({ name }: { name: string }): Promise<ToolResponse> {
528
+ if (this._skillCache[name]) {
529
+ // Look up the skill name in the cache to get the corresponding directory path
530
+ const skillDir = this._skillCache[name];
531
+ // Read the SKILL.md file in the skill directory and return its content as the tool response
532
+ const skillMdPath = path.join(skillDir, 'SKILL.md');
533
+ if (!fs.existsSync(skillMdPath)) {
534
+ try {
535
+ const fileContent = fs.readFileSync(skillMdPath, 'utf-8');
536
+ return createToolResponse({
537
+ content: [
538
+ {
539
+ id: crypto.randomUUID(),
540
+ type: 'text',
541
+ text: fileContent,
542
+ },
543
+ ],
544
+ state: 'success',
545
+ });
546
+ } catch {}
547
+ }
548
+ }
549
+
550
+ // Scan the skills and skillDirs again to find the skill if it's not in the cache and refresh the cache at the same time
551
+ this.getSkillsPrompt();
552
+ const refreshedSkillDir = this._skillCache[name];
553
+ if (refreshedSkillDir) {
554
+ const skillMdPath = path.join(refreshedSkillDir, 'SKILL.md');
555
+ try {
556
+ const fileContent = fs.readFileSync(skillMdPath, 'utf-8');
557
+ return createToolResponse({
558
+ content: [
559
+ {
560
+ id: crypto.randomUUID(),
561
+ type: 'text',
562
+ text: fileContent,
563
+ },
564
+ ],
565
+ state: 'success',
566
+ });
567
+ } catch {}
568
+ }
569
+
570
+ return createToolResponse({
571
+ content: [
572
+ {
573
+ id: crypto.randomUUID(),
574
+ type: 'text',
575
+ text: `SkillNotFoundError: Cannot find the skill named ${name}, current available skills are ${Object.keys(this._skillCache).join(', ')}`,
576
+ },
577
+ ],
578
+ state: 'error',
579
+ });
580
+ }
581
+
582
+ /**
583
+ * Checks if a tool requires user confirmation before execution based on its name.
584
+ * @param toolName The name of the tool to check for user confirmation requirement.
585
+ * @returns A boolean indicating whether the specified tool requires user confirmation before execution. If the tool is not found, it returns false.
586
+ */
587
+ requireUserConfirm(toolName: string): boolean {
588
+ const tool = this.tools.find(tool => tool.name === toolName);
589
+ return tool ? (tool.requireUserConfirm ?? false) : false;
590
+ }
591
+
592
+ /**
593
+ * Checks if a tool requires external execution (e.g., by an MCP client) based on its name.
594
+ * @param toolName
595
+ * @returns A boolean indicating whether the specified tool requires external execution. If the tool is not found, it returns false.
596
+ */
597
+ requireExternalExecution(toolName: string): boolean {
598
+ const tool = this.tools.find(tool => tool.name === toolName);
599
+ return tool ? !tool.call : false;
600
+ }
601
+ }
@@ -0,0 +1,52 @@
1
+ import * as fs from 'fs';
2
+ import * as os from 'os';
3
+ import * as path from 'path';
4
+
5
+ import { Tool } from './base';
6
+ import { Write } from './write';
7
+
8
+ describe('Write', () => {
9
+ let tmpDir: string;
10
+ let write: Tool;
11
+
12
+ beforeEach(() => {
13
+ write = Write();
14
+ tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'write-test-'));
15
+ });
16
+
17
+ afterEach(() => {
18
+ fs.rmSync(tmpDir, { recursive: true, force: true });
19
+ });
20
+
21
+ it('writes a new file', () => {
22
+ const filePath = path.join(tmpDir, 'hello.txt');
23
+ const result = write.call!({ file_path: filePath, content: 'hello world' });
24
+ expect(fs.readFileSync(filePath, 'utf-8')).toBe('hello world');
25
+ expect(result).toContain('written successfully');
26
+ });
27
+
28
+ it('overwrites an existing file', () => {
29
+ const filePath = path.join(tmpDir, 'existing.txt');
30
+ fs.writeFileSync(filePath, 'old content');
31
+ write.call!({ file_path: filePath, content: 'new content' });
32
+ expect(fs.readFileSync(filePath, 'utf-8')).toBe('new content');
33
+ });
34
+
35
+ it('creates intermediate directories', () => {
36
+ const filePath = path.join(tmpDir, 'a', 'b', 'c.txt');
37
+ write.call!({ file_path: filePath, content: 'nested' });
38
+ expect(fs.readFileSync(filePath, 'utf-8')).toBe('nested');
39
+ });
40
+
41
+ it('throws on relative path', () => {
42
+ expect(() => write.call!({ file_path: 'relative/path.txt', content: 'x' })).toThrow(
43
+ 'absolute path'
44
+ );
45
+ });
46
+
47
+ it('reports correct line count', () => {
48
+ const filePath = path.join(tmpDir, 'lines.txt');
49
+ const result = write.call!({ file_path: filePath, content: 'line1\nline2\nline3' });
50
+ expect(result).toContain('3 lines');
51
+ });
52
+ });