@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.
- package/dist/agent/index.d.mts +234 -0
- package/dist/agent/index.d.ts +234 -0
- package/dist/agent/index.js +1412 -0
- package/dist/agent/index.js.map +1 -0
- package/dist/agent/index.mjs +1375 -0
- package/dist/agent/index.mjs.map +1 -0
- package/dist/base-BOx3UzOl.d.mts +41 -0
- package/dist/base-BoIps2RL.d.ts +41 -0
- package/dist/base-C7jwyH4Z.d.mts +52 -0
- package/dist/base-Cwi4bjze.d.ts +127 -0
- package/dist/base-DYlBMCy_.d.mts +127 -0
- package/dist/base-NX-knWOv.d.ts +52 -0
- package/dist/block-VsnHrllL.d.mts +48 -0
- package/dist/block-VsnHrllL.d.ts +48 -0
- package/dist/event/index.d.mts +181 -0
- package/dist/event/index.d.ts +181 -0
- package/dist/event/index.js +58 -0
- package/dist/event/index.js.map +1 -0
- package/dist/event/index.mjs +33 -0
- package/dist/event/index.mjs.map +1 -0
- package/dist/formatter/index.d.mts +187 -0
- package/dist/formatter/index.d.ts +187 -0
- package/dist/formatter/index.js +647 -0
- package/dist/formatter/index.js.map +1 -0
- package/dist/formatter/index.mjs +616 -0
- package/dist/formatter/index.mjs.map +1 -0
- package/dist/index-BTJDlKvQ.d.mts +195 -0
- package/dist/index-BcatlwXQ.d.ts +195 -0
- package/dist/index-CAxQAkiP.d.mts +21 -0
- package/dist/index-CAxQAkiP.d.ts +21 -0
- package/dist/mcp/index.d.mts +9 -0
- package/dist/mcp/index.d.ts +9 -0
- package/dist/mcp/index.js +432 -0
- package/dist/mcp/index.js.map +1 -0
- package/dist/mcp/index.mjs +408 -0
- package/dist/mcp/index.mjs.map +1 -0
- package/dist/message/index.d.mts +10 -0
- package/dist/message/index.d.ts +10 -0
- package/dist/message/index.js +67 -0
- package/dist/message/index.js.map +1 -0
- package/dist/message/index.mjs +37 -0
- package/dist/message/index.mjs.map +1 -0
- package/dist/message-CkN21KaY.d.mts +99 -0
- package/dist/message-CzLeTlua.d.ts +99 -0
- package/dist/model/index.d.mts +377 -0
- package/dist/model/index.d.ts +377 -0
- package/dist/model/index.js +1880 -0
- package/dist/model/index.js.map +1 -0
- package/dist/model/index.mjs +1849 -0
- package/dist/model/index.mjs.map +1 -0
- package/dist/storage/index.d.mts +68 -0
- package/dist/storage/index.d.ts +68 -0
- package/dist/storage/index.js +250 -0
- package/dist/storage/index.js.map +1 -0
- package/dist/storage/index.mjs +212 -0
- package/dist/storage/index.mjs.map +1 -0
- package/dist/tool/index.d.mts +311 -0
- package/dist/tool/index.d.ts +311 -0
- package/dist/tool/index.js +1494 -0
- package/dist/tool/index.js.map +1 -0
- package/dist/tool/index.mjs +1447 -0
- package/dist/tool/index.mjs.map +1 -0
- package/dist/toolkit-CEpulFi0.d.ts +99 -0
- package/dist/toolkit-CGEZSZPa.d.mts +99 -0
- package/jest.config.js +11 -0
- package/package.json +92 -0
- package/src/_utils/common.ts +104 -0
- package/src/_utils/index.ts +1 -0
- package/src/agent/agent-base.ts +0 -0
- package/src/agent/agent.test.ts +1028 -0
- package/src/agent/agent.ts +1032 -0
- package/src/agent/index.ts +2 -0
- package/src/agent/interfaces.ts +23 -0
- package/src/agent/test-compression.ts +72 -0
- package/src/event/index.ts +250 -0
- package/src/formatter/base.ts +133 -0
- package/src/formatter/dashscope-chat-formatter.test.ts +372 -0
- package/src/formatter/dashscope-chat-formatter.ts +163 -0
- package/src/formatter/deepseek-chat-formatter.ts +130 -0
- package/src/formatter/index.ts +5 -0
- package/src/formatter/ollama-chat-formatter.ts +67 -0
- package/src/formatter/openai-chat-formatter.test.ts +263 -0
- package/src/formatter/openai-chat-formatter.ts +301 -0
- package/src/formatter/openai.md +767 -0
- package/src/mcp/base.ts +114 -0
- package/src/mcp/http.test.ts +303 -0
- package/src/mcp/http.ts +224 -0
- package/src/mcp/index.ts +2 -0
- package/src/mcp/stdio.test.ts +91 -0
- package/src/mcp/stdio.ts +119 -0
- package/src/message/block.ts +60 -0
- package/src/message/enums.ts +4 -0
- package/src/message/index.ts +12 -0
- package/src/message/message.test.ts +80 -0
- package/src/message/message.ts +131 -0
- package/src/model/base.ts +226 -0
- package/src/model/dashscope-model.test.ts +335 -0
- package/src/model/dashscope-model.ts +441 -0
- package/src/model/deepseek-model.test.ts +279 -0
- package/src/model/deepseek-model.ts +401 -0
- package/src/model/index.ts +7 -0
- package/src/model/ollama-model.test.ts +307 -0
- package/src/model/ollama-model.ts +356 -0
- package/src/model/openai-model.ts +327 -0
- package/src/model/response.ts +22 -0
- package/src/model/usage.ts +12 -0
- package/src/storage/base.ts +52 -0
- package/src/storage/file-system.test.ts +587 -0
- package/src/storage/file-system.ts +269 -0
- package/src/storage/index.ts +2 -0
- package/src/tool/base.ts +23 -0
- package/src/tool/bash.test.ts +174 -0
- package/src/tool/bash.ts +152 -0
- package/src/tool/edit.test.ts +83 -0
- package/src/tool/edit.ts +95 -0
- package/src/tool/glob.test.ts +63 -0
- package/src/tool/glob.ts +166 -0
- package/src/tool/grep.test.ts +74 -0
- package/src/tool/grep.ts +256 -0
- package/src/tool/index.ts +10 -0
- package/src/tool/read.test.ts +77 -0
- package/src/tool/read.ts +117 -0
- package/src/tool/response.ts +82 -0
- package/src/tool/task.test.ts +299 -0
- package/src/tool/task.ts +399 -0
- package/src/tool/toolkit.test.ts +636 -0
- package/src/tool/toolkit.ts +601 -0
- package/src/tool/write.test.ts +52 -0
- package/src/tool/write.ts +57 -0
- package/src/type/index.ts +52 -0
- package/tsconfig.build.json +4 -0
- package/tsconfig.cjs.json +11 -0
- package/tsconfig.esm.json +10 -0
- package/tsconfig.json +14 -0
- package/tsup.config.ts +20 -0
- 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
|
+
});
|