@ai-zen/agents-sdk 1.0.0-alpha.21 → 1.0.0-alpha.23
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/CHANGELOG.md +223 -171
- package/README.md +15 -0
- package/README.zh.md +15 -0
- package/dist/agent-plugins/AutoMigratePlugin.js +1 -1
- package/dist/agent-plugins/UnknownToolHintPlugin.js +4 -4
- package/dist/config/constants.js +29 -24
- package/dist/config/factory-skills.js +56 -56
- package/dist/config/migrate.d.ts +10 -2
- package/dist/config/migrate.js +23 -2
- package/dist/runtime/HandoffGuardPlugin.js +2 -2
- package/dist/runtime/HandoffService.js +22 -22
- package/dist/runtime/createAgent.d.ts +7 -3
- package/dist/runtime/createAgent.js +7 -5
- package/dist/runtime/createModel.js +3 -3
- package/dist/scope/PermissionEvaluator.js +1 -1
- package/dist/scope/Scope.js +1 -1
- package/dist/scope/plugins/builtin/BatchEditTool.js +8 -8
- package/dist/scope/plugins/builtin/CopyTool.js +4 -4
- package/dist/scope/plugins/builtin/CwdTool.js +1 -1
- package/dist/scope/plugins/builtin/DownloadFileTool.js +8 -8
- package/dist/scope/plugins/builtin/EditTool.js +9 -9
- package/dist/scope/plugins/builtin/ExecTool.js +8 -8
- package/dist/scope/plugins/builtin/ExistTool.js +2 -2
- package/dist/scope/plugins/builtin/FindTextTool.js +10 -10
- package/dist/scope/plugins/builtin/GenerateImageTool.js +13 -13
- package/dist/scope/plugins/builtin/GlobTool.js +4 -4
- package/dist/scope/plugins/builtin/InspectFileTool.js +7 -7
- package/dist/scope/plugins/builtin/LsTool.js +5 -5
- package/dist/scope/plugins/builtin/MkdirTool.js +3 -3
- package/dist/scope/plugins/builtin/ReadTool.js +8 -8
- package/dist/scope/plugins/builtin/RenameTool.js +3 -3
- package/dist/scope/plugins/builtin/RmTool.js +3 -3
- package/dist/scope/plugins/builtin/SleepTool.js +6 -6
- package/dist/scope/plugins/builtin/SpawnTool.js +9 -9
- package/dist/scope/plugins/builtin/ViewImageTool.js +12 -12
- package/dist/scope/plugins/builtin/WriteTool.js +3 -3
- package/dist/scope/plugins/mcp/McpConnectionManager.js +7 -7
- package/dist/scope/plugins/mcp/discover.js +1 -1
- package/dist/scope/plugins/mcp/mcpTools.js +29 -29
- package/dist/scope/plugins/skills/discover.js +7 -7
- package/dist/scope/plugins/skills/skillTools.js +17 -17
- package/dist/scope/plugins/subagents/subAgentTools.js +2 -2
- package/dist/scope/plugins/usertools/discover.js +2 -2
- package/dist/shared/content-blocks.js +9 -9
- package/dist/shared/errors.js +3 -3
- package/dist/types/index.d.ts +22 -1
- package/package.json +10 -1
|
@@ -16,7 +16,7 @@ import { HandoffGuardPlugin } from "./HandoffGuardPlugin.js";
|
|
|
16
16
|
*/
|
|
17
17
|
export class HandoffAbortedError extends Error {
|
|
18
18
|
constructor() {
|
|
19
|
-
super("
|
|
19
|
+
super("Handoff cancelled");
|
|
20
20
|
this.name = "HandoffAbortedError";
|
|
21
21
|
}
|
|
22
22
|
}
|
|
@@ -115,14 +115,14 @@ export class HandoffService {
|
|
|
115
115
|
const { skillContent, handoffPath, toolsAvailable } = params;
|
|
116
116
|
const tail = toolsAvailable
|
|
117
117
|
? [
|
|
118
|
-
"
|
|
119
|
-
"
|
|
118
|
+
"Start writing now: use the write tool to put the complete document at the path given above (use the path exactly as given, do not rename it and do not move it to another directory).",
|
|
119
|
+
"After writing, read it back once with the read tool to confirm it is complete, then reply with a single confirmation of no more than 40 words, **do not restate the document content**.",
|
|
120
120
|
]
|
|
121
121
|
: [
|
|
122
|
-
"
|
|
122
|
+
"Output the complete document body directly: no explanations, do not wrap it in a code block, the caller will persist it to disk itself.",
|
|
123
123
|
];
|
|
124
124
|
return [
|
|
125
|
-
"
|
|
125
|
+
"You are now to write a task handoff document for this conversation. The following handoff rules are mandatory.",
|
|
126
126
|
"",
|
|
127
127
|
"---",
|
|
128
128
|
"",
|
|
@@ -130,11 +130,11 @@ export class HandoffService {
|
|
|
130
130
|
"",
|
|
131
131
|
"---",
|
|
132
132
|
"",
|
|
133
|
-
"##
|
|
134
|
-
`-
|
|
135
|
-
`-
|
|
136
|
-
? "
|
|
137
|
-
: "
|
|
133
|
+
"## Handoff parameters",
|
|
134
|
+
`- Handoff document absolute path: ${handoffPath}`,
|
|
135
|
+
`- Tools available: ${toolsAvailable
|
|
136
|
+
? "yes (read / exec, etc., use them to verify the actual state)"
|
|
137
|
+
: "no (you cannot inspect the disk, so for any state information always write '⚠️ Unverified', never speculate)"}`,
|
|
138
138
|
"",
|
|
139
139
|
...tail,
|
|
140
140
|
].join("\n");
|
|
@@ -147,10 +147,10 @@ export class HandoffService {
|
|
|
147
147
|
*/
|
|
148
148
|
static createPostMessages(handoffPath) {
|
|
149
149
|
const content = [
|
|
150
|
-
"
|
|
150
|
+
"The previous conversation has been handed off, the handoff document is saved at:",
|
|
151
151
|
handoffPath,
|
|
152
152
|
"",
|
|
153
|
-
"
|
|
153
|
+
"Read that file first with the read tool, and once you understand the context and the unfinished work, continue helping the user with the task.",
|
|
154
154
|
].join("\n");
|
|
155
155
|
return [Message.User(content)];
|
|
156
156
|
}
|
|
@@ -213,7 +213,7 @@ export class HandoffService {
|
|
|
213
213
|
/** 串行化 + 防重入:交接与迁移共用同一把锁 */
|
|
214
214
|
async withBusy(fn) {
|
|
215
215
|
if (this._busy)
|
|
216
|
-
throw new Error("
|
|
216
|
+
throw new Error("A handoff is already in progress, wait for it to finish");
|
|
217
217
|
this._busy = true;
|
|
218
218
|
try {
|
|
219
219
|
return await fn();
|
|
@@ -298,13 +298,13 @@ export class HandoffService {
|
|
|
298
298
|
let handoffDoc;
|
|
299
299
|
let historyText = "";
|
|
300
300
|
if (mode === "self" && !this.supportsSelfHandoff(agent)) {
|
|
301
|
-
this.logger.warn("[handoff] agent
|
|
301
|
+
this.logger.warn("[handoff] agent has no write tool or send capability, falling back to standalone summary mode");
|
|
302
302
|
mode = "summary";
|
|
303
303
|
}
|
|
304
304
|
if (mode === "self") {
|
|
305
305
|
const ok = await this.runSelfHandoff(agent, handoffPath, signal);
|
|
306
306
|
if (!ok) {
|
|
307
|
-
this.logger.warn("[handoff]
|
|
307
|
+
this.logger.warn("[handoff] self handoff failed, falling back to standalone summary mode");
|
|
308
308
|
mode = "summary";
|
|
309
309
|
}
|
|
310
310
|
}
|
|
@@ -375,10 +375,10 @@ export class HandoffService {
|
|
|
375
375
|
this.throwIfAborted(signal);
|
|
376
376
|
if (await this.isHandoffFileReady(handoffPath))
|
|
377
377
|
return true;
|
|
378
|
-
this.logger.warn(`[handoff]
|
|
378
|
+
this.logger.warn(`[handoff] handoff document missing or too small, retrying once: ${handoffPath}`);
|
|
379
379
|
await agent.send([
|
|
380
|
-
|
|
381
|
-
"
|
|
380
|
+
`The handoff document does not exist or is too small to deliver: ${handoffPath}`,
|
|
381
|
+
"Use the write tool right now to write the **complete** handoff document to that path (use the path exactly as given) and do nothing else.",
|
|
382
382
|
].join("\n"));
|
|
383
383
|
this.throwIfAborted(signal);
|
|
384
384
|
return await this.isHandoffFileReady(handoffPath);
|
|
@@ -388,7 +388,7 @@ export class HandoffService {
|
|
|
388
388
|
await this.discardPartialFile(handoffPath, existedBefore);
|
|
389
389
|
throw new HandoffAbortedError();
|
|
390
390
|
}
|
|
391
|
-
this.logger.error(`[handoff]
|
|
391
|
+
this.logger.error(`[handoff] self handoff round failed: ${err?.message ?? err}`);
|
|
392
392
|
return false;
|
|
393
393
|
}
|
|
394
394
|
finally {
|
|
@@ -421,7 +421,7 @@ export class HandoffService {
|
|
|
421
421
|
}, signal ? { signal } : undefined);
|
|
422
422
|
const handoffDoc = response.choices[0]?.message?.content;
|
|
423
423
|
if (!handoffDoc || !handoffDoc.trim()) {
|
|
424
|
-
throw new Error("
|
|
424
|
+
throw new Error("Failed to generate the handoff document");
|
|
425
425
|
}
|
|
426
426
|
return handoffDoc;
|
|
427
427
|
}
|
|
@@ -446,7 +446,7 @@ export class HandoffService {
|
|
|
446
446
|
if (skill?.content?.trim())
|
|
447
447
|
return skill.content;
|
|
448
448
|
}
|
|
449
|
-
this.logger.warn(`[handoff]
|
|
449
|
+
this.logger.warn(`[handoff] ${HANDOFF_SKILL_ID} skill not found, using the built-in fallback template (paths: ${this.skillDirs.join(", ") || "none configured"})`);
|
|
450
450
|
return FALLBACK_HANDOFF_TEMPLATE;
|
|
451
451
|
}
|
|
452
452
|
/** 守卫按 agent 注册一次(`Agent.use()` 无移除 API,故复用实例 + 开关) */
|
|
@@ -466,7 +466,7 @@ export class HandoffService {
|
|
|
466
466
|
await hook(ctx);
|
|
467
467
|
}
|
|
468
468
|
catch (err) {
|
|
469
|
-
this.logger.error(`[handoff]
|
|
469
|
+
this.logger.error(`[handoff] hook execution failed: ${err?.message ?? err}`);
|
|
470
470
|
}
|
|
471
471
|
}
|
|
472
472
|
}
|
|
@@ -2,7 +2,10 @@ import { SdkAgent } from "./SdkAgent.js";
|
|
|
2
2
|
import type { Scope } from "../scope/Scope.js";
|
|
3
3
|
import type { AbortedMessageStrategy, AbortedToolRoundStrategy } from "@ai-zen/agents-core";
|
|
4
4
|
/**
|
|
5
|
-
* `createAgent`
|
|
5
|
+
* `createAgent` 的可选行为开关。
|
|
6
|
+
*
|
|
7
|
+
* 不传时先取 `scope.config`(`config.json`)里的同名配置,再往下才是 core 默认值:
|
|
8
|
+
* 中断策略属于**产品配置**,调用方不该自己读配置再转一道参数。
|
|
6
9
|
*/
|
|
7
10
|
export interface CreateAgentOptions {
|
|
8
11
|
/** 工具轮次被中断(或结果不完整)时的归一策略(core 默认 `fill`) */
|
|
@@ -10,8 +13,9 @@ export interface CreateAgentOptions {
|
|
|
10
13
|
/** `fill` 策略下,未完成工具结果的填充原因 */
|
|
11
14
|
abortedToolRoundReason?: string;
|
|
12
15
|
/**
|
|
13
|
-
* 被中断的 assistant 消息(已输出的内容 /
|
|
14
|
-
*
|
|
16
|
+
* 被中断的 assistant 消息(已输出的内容 / 思考)如何处置(未传时取 `scope.config`,
|
|
17
|
+
* 再缺则用 core 默认 `drop`)。
|
|
18
|
+
* `keep` 可让模型在下一轮接着半截往下说,而不是忘掉自己说过什么。
|
|
15
19
|
*/
|
|
16
20
|
abortedMessageStrategy?: AbortedMessageStrategy;
|
|
17
21
|
}
|
|
@@ -14,10 +14,10 @@ import { createModel } from "./createModel.js";
|
|
|
14
14
|
export async function createAgent(scope, agentId, options = {}) {
|
|
15
15
|
const definition = await new AgentRepository(scope.agentsDir).read(agentId);
|
|
16
16
|
if (!definition)
|
|
17
|
-
throw new Error(`Agent "${agentId}"
|
|
17
|
+
throw new Error(`Agent "${agentId}" not found`);
|
|
18
18
|
const modelId = definition.modelId;
|
|
19
19
|
if (!modelId) {
|
|
20
|
-
throw new Error(`Agent "${agentId}"
|
|
20
|
+
throw new Error(`Agent "${agentId}" has no model bound (modelId is missing). Assign a model to it in the config`);
|
|
21
21
|
}
|
|
22
22
|
const { client, model, modelConfig } = createModel(scope, modelId, definition.modelParams);
|
|
23
23
|
const tools = scope.buildTools(definition, {
|
|
@@ -40,10 +40,12 @@ export async function createAgent(scope, agentId, options = {}) {
|
|
|
40
40
|
// 快照后,运行时改动只作用于会话副本,模板保持纯净。
|
|
41
41
|
messages: [...(definition.messages ?? [])],
|
|
42
42
|
tools,
|
|
43
|
-
//
|
|
44
|
-
|
|
43
|
+
// 中断相关策略:**显式参数 > `scope.config` > core 默认值**。
|
|
44
|
+
// 中间那一层是「配置即事实」:策略写在 `config.json` 里,各端(CLI / Desktop)
|
|
45
|
+
// 不必自己读配置再转成参数,子 Agent 创建时也能自然继承同一份策略。
|
|
46
|
+
abortedToolRoundStrategy: options.abortedToolRoundStrategy ?? scope.config?.abortedToolRoundStrategy,
|
|
45
47
|
abortedToolRoundReason: options.abortedToolRoundReason,
|
|
46
|
-
abortedMessageStrategy: options.abortedMessageStrategy,
|
|
48
|
+
abortedMessageStrategy: options.abortedMessageStrategy ?? scope.config?.abortedMessageStrategy,
|
|
47
49
|
});
|
|
48
50
|
return agent;
|
|
49
51
|
}
|
|
@@ -26,12 +26,12 @@ export function createModel(scope, modelId, overrides) {
|
|
|
26
26
|
const config = scope.config;
|
|
27
27
|
const modelConfig = config.models.find((m) => m.id === modelId);
|
|
28
28
|
if (!modelConfig)
|
|
29
|
-
throw new Error(
|
|
29
|
+
throw new Error(`Model "${modelId}" not found`);
|
|
30
30
|
const endpointConfig = config.endpoints.find((e) => e.id === modelConfig.endpointId);
|
|
31
31
|
if (!endpointConfig)
|
|
32
|
-
throw new Error(
|
|
32
|
+
throw new Error(`Endpoint "${modelConfig.endpointId}" is not configured`);
|
|
33
33
|
if (!endpointConfig.apiKey) {
|
|
34
|
-
throw new Error(
|
|
34
|
+
throw new Error(`API key for endpoint "${endpointConfig.name}" is not set`);
|
|
35
35
|
}
|
|
36
36
|
const client = new OpenAI({
|
|
37
37
|
apiKey: endpointConfig.apiKey,
|
package/dist/scope/Scope.js
CHANGED
|
@@ -57,7 +57,7 @@ export class Scope {
|
|
|
57
57
|
*/
|
|
58
58
|
use(...plugins) {
|
|
59
59
|
if (this._initialized) {
|
|
60
|
-
throw new Error("Scope.use()
|
|
60
|
+
throw new Error("Scope.use() must be called before init()");
|
|
61
61
|
}
|
|
62
62
|
for (const plugin of plugins) {
|
|
63
63
|
this._plugins.push(plugin);
|
|
@@ -4,33 +4,33 @@ export class BatchEditTool extends SdkCallbackTool {
|
|
|
4
4
|
function = {
|
|
5
5
|
name: "batch_edit",
|
|
6
6
|
description: [
|
|
7
|
-
"
|
|
8
|
-
"
|
|
7
|
+
"Edit text in a file in batches. Prefer this tool when making edits to a file.",
|
|
8
|
+
"Submit all changes to the same file in a single call (the replacements array) instead of calling edit in parallel multiple times — parallel edits to the same file overwrite each other.",
|
|
9
9
|
].join("\n"),
|
|
10
10
|
parameters: {
|
|
11
11
|
type: "object",
|
|
12
12
|
properties: {
|
|
13
13
|
path: {
|
|
14
14
|
type: "string",
|
|
15
|
-
description: "
|
|
15
|
+
description: "File path",
|
|
16
16
|
},
|
|
17
17
|
replacements: {
|
|
18
18
|
type: "array",
|
|
19
|
-
description: "
|
|
19
|
+
description: "Array of replacements",
|
|
20
20
|
items: {
|
|
21
21
|
type: "object",
|
|
22
22
|
properties: {
|
|
23
23
|
oldText: {
|
|
24
24
|
type: "string",
|
|
25
|
-
description: "
|
|
25
|
+
description: "Text to replace",
|
|
26
26
|
},
|
|
27
27
|
newText: {
|
|
28
28
|
type: "string",
|
|
29
|
-
description: "
|
|
29
|
+
description: "Replacement text",
|
|
30
30
|
},
|
|
31
31
|
isReplaceAll: {
|
|
32
32
|
type: "boolean",
|
|
33
|
-
description: "
|
|
33
|
+
description: "Whether to replace all matches (by default only the first match is replaced). Make sure the oldText you provide is precise enough to avoid unintended replacements",
|
|
34
34
|
default: false,
|
|
35
35
|
},
|
|
36
36
|
},
|
|
@@ -60,7 +60,7 @@ export class BatchEditTool extends SdkCallbackTool {
|
|
|
60
60
|
failed.push({
|
|
61
61
|
oldText: replacement.oldText,
|
|
62
62
|
newText: replacement.newText,
|
|
63
|
-
reason: "
|
|
63
|
+
reason: "No exact match found for the text to replace",
|
|
64
64
|
});
|
|
65
65
|
continue;
|
|
66
66
|
}
|
|
@@ -3,21 +3,21 @@ import { SdkCallbackTool } from "./SdkCallbackTool.js";
|
|
|
3
3
|
export class CopyTool extends SdkCallbackTool {
|
|
4
4
|
function = {
|
|
5
5
|
name: "copy",
|
|
6
|
-
description: "
|
|
6
|
+
description: "Copy a file or directory (equivalent to the cp command)",
|
|
7
7
|
parameters: {
|
|
8
8
|
type: "object",
|
|
9
9
|
properties: {
|
|
10
10
|
src: {
|
|
11
11
|
type: "string",
|
|
12
|
-
description: "
|
|
12
|
+
description: "Source path",
|
|
13
13
|
},
|
|
14
14
|
dest: {
|
|
15
15
|
type: "string",
|
|
16
|
-
description: "
|
|
16
|
+
description: "Destination path",
|
|
17
17
|
},
|
|
18
18
|
recursive: {
|
|
19
19
|
type: "boolean",
|
|
20
|
-
description: "
|
|
20
|
+
description: "Whether to copy directories recursively; must be true when copying a directory",
|
|
21
21
|
},
|
|
22
22
|
},
|
|
23
23
|
required: ["src", "dest"],
|
|
@@ -2,7 +2,7 @@ import { SdkCallbackTool } from "./SdkCallbackTool.js";
|
|
|
2
2
|
export class CwdTool extends SdkCallbackTool {
|
|
3
3
|
function = {
|
|
4
4
|
name: "cwd",
|
|
5
|
-
description: "
|
|
5
|
+
description: "Get the current working directory (cwd)",
|
|
6
6
|
parameters: {
|
|
7
7
|
type: "object",
|
|
8
8
|
properties: {},
|
|
@@ -4,17 +4,17 @@ import { SdkCallbackTool } from "./SdkCallbackTool.js";
|
|
|
4
4
|
export class DownloadFileTool extends SdkCallbackTool {
|
|
5
5
|
function = {
|
|
6
6
|
name: "download_file",
|
|
7
|
-
description: "
|
|
7
|
+
description: "Download a file from a URL (images, PDFs and other binary files) and save it to the local disk. The file name is inferred from the URL, or you can specify the save path.",
|
|
8
8
|
parameters: {
|
|
9
9
|
type: "object",
|
|
10
10
|
properties: {
|
|
11
11
|
url: {
|
|
12
12
|
type: "string",
|
|
13
|
-
description: "
|
|
13
|
+
description: "URL to download the file from",
|
|
14
14
|
},
|
|
15
15
|
outputPath: {
|
|
16
16
|
type: "string",
|
|
17
|
-
description: "
|
|
17
|
+
description: "Save path (optional). May be a directory or a full file path. If omitted, the file is saved to the current working directory with the file name inferred from the URL.",
|
|
18
18
|
},
|
|
19
19
|
},
|
|
20
20
|
required: ["url"],
|
|
@@ -30,7 +30,7 @@ export class DownloadFileTool extends SdkCallbackTool {
|
|
|
30
30
|
try {
|
|
31
31
|
const url = input.url;
|
|
32
32
|
if (!url)
|
|
33
|
-
throw new Error("url
|
|
33
|
+
throw new Error("url must not be empty");
|
|
34
34
|
if (input.outputPath) {
|
|
35
35
|
savePath = this.resolve(input.outputPath);
|
|
36
36
|
const stat = await fsp.stat(savePath).catch(() => null);
|
|
@@ -48,7 +48,7 @@ export class DownloadFileTool extends SdkCallbackTool {
|
|
|
48
48
|
await fsp.mkdir(path.dirname(savePath), { recursive: true });
|
|
49
49
|
const response = await fetch(url, { signal });
|
|
50
50
|
if (!response.ok) {
|
|
51
|
-
throw new Error(
|
|
51
|
+
throw new Error(`Download failed: HTTP ${response.status} ${response.statusText}`);
|
|
52
52
|
}
|
|
53
53
|
const contentType = response.headers.get("content-type") || "";
|
|
54
54
|
const arrayBuffer = await response.arrayBuffer();
|
|
@@ -61,7 +61,7 @@ export class DownloadFileTool extends SdkCallbackTool {
|
|
|
61
61
|
filePath: path.resolve(savePath),
|
|
62
62
|
fileSize: `${fileSizeKB} KB`,
|
|
63
63
|
contentType,
|
|
64
|
-
note: "
|
|
64
|
+
note: "File saved to local disk",
|
|
65
65
|
});
|
|
66
66
|
}
|
|
67
67
|
catch (error) {
|
|
@@ -71,12 +71,12 @@ export class DownloadFileTool extends SdkCallbackTool {
|
|
|
71
71
|
return JSON.stringify({
|
|
72
72
|
success: false,
|
|
73
73
|
aborted: true,
|
|
74
|
-
error: "
|
|
74
|
+
error: "Download aborted",
|
|
75
75
|
});
|
|
76
76
|
}
|
|
77
77
|
return JSON.stringify({
|
|
78
78
|
success: false,
|
|
79
|
-
error: error?.message || "
|
|
79
|
+
error: error?.message || "Download failed",
|
|
80
80
|
});
|
|
81
81
|
}
|
|
82
82
|
}
|
|
@@ -16,29 +16,29 @@ export class EditTool extends SdkCallbackTool {
|
|
|
16
16
|
function = {
|
|
17
17
|
name: "edit",
|
|
18
18
|
description: [
|
|
19
|
-
"
|
|
19
|
+
"Edit text in a file, supporting exact replacement or replacing all matches.",
|
|
20
20
|
"",
|
|
21
|
-
"
|
|
22
|
-
"
|
|
21
|
+
"Concurrency limit (important): never edit the same file in parallel. Do not issue multiple edit calls for the same path within one reply — each call reads the old content and writes it back, so the later write overwrites the earlier one, losing changes or corrupting the file.",
|
|
22
|
+
"To apply multiple changes to one file, use batch_edit instead and submit all replacements in a single call. Only edit different files in parallel.",
|
|
23
23
|
].join("\n"),
|
|
24
24
|
parameters: {
|
|
25
25
|
type: "object",
|
|
26
26
|
properties: {
|
|
27
27
|
path: {
|
|
28
28
|
type: "string",
|
|
29
|
-
description: "
|
|
29
|
+
description: "File path",
|
|
30
30
|
},
|
|
31
31
|
oldText: {
|
|
32
32
|
type: "string",
|
|
33
|
-
description: "
|
|
33
|
+
description: "Text to replace",
|
|
34
34
|
},
|
|
35
35
|
newText: {
|
|
36
36
|
type: "string",
|
|
37
|
-
description: "
|
|
37
|
+
description: "Replacement text",
|
|
38
38
|
},
|
|
39
39
|
isReplaceAll: {
|
|
40
40
|
type: "boolean",
|
|
41
|
-
description: "
|
|
41
|
+
description: "Whether to replace all matches (default false, only the first match is replaced). Make sure the oldText you provide is precise enough to avoid unintended replacements",
|
|
42
42
|
default: false,
|
|
43
43
|
},
|
|
44
44
|
},
|
|
@@ -59,7 +59,7 @@ export class EditTool extends SdkCallbackTool {
|
|
|
59
59
|
const count = countOccurrences(content, oldText);
|
|
60
60
|
if (count === 0) {
|
|
61
61
|
return JSON.stringify({
|
|
62
|
-
result: "
|
|
62
|
+
result: "No exact match found for the text to replace",
|
|
63
63
|
matchCount: 0,
|
|
64
64
|
});
|
|
65
65
|
}
|
|
@@ -69,7 +69,7 @@ export class EditTool extends SdkCallbackTool {
|
|
|
69
69
|
await fsp.writeFile(filePath, newContent);
|
|
70
70
|
if (!isReplaceAll && count > 1) {
|
|
71
71
|
return JSON.stringify({
|
|
72
|
-
result:
|
|
72
|
+
result: `Warning: replace-all is off (isReplaceAll=false), but the text "${oldText}" occurs ${count} times in the file; only the first match was replaced. Set isReplaceAll to true to replace all matches`,
|
|
73
73
|
matchCount: count,
|
|
74
74
|
replacedCount: 1,
|
|
75
75
|
});
|
|
@@ -4,17 +4,17 @@ import { DEFAULT_MAX_TOOL_OUTPUT, guardOutput, headTailPreview } from "./outputG
|
|
|
4
4
|
export class ExecTool extends SdkCallbackTool {
|
|
5
5
|
function = {
|
|
6
6
|
name: "exec",
|
|
7
|
-
description: "
|
|
7
|
+
description: "Execute a shell command",
|
|
8
8
|
parameters: {
|
|
9
9
|
type: "object",
|
|
10
10
|
properties: {
|
|
11
11
|
command: {
|
|
12
12
|
type: "string",
|
|
13
|
-
description: "
|
|
13
|
+
description: "Command to execute",
|
|
14
14
|
},
|
|
15
15
|
timeout: {
|
|
16
16
|
type: "number",
|
|
17
|
-
description: "
|
|
17
|
+
description: "Timeout in milliseconds (required). The process is killed on timeout. For long-running commands, prefer spawn to run them asynchronously.",
|
|
18
18
|
},
|
|
19
19
|
},
|
|
20
20
|
required: ["command", "timeout"],
|
|
@@ -29,7 +29,7 @@ export class ExecTool extends SdkCallbackTool {
|
|
|
29
29
|
const { timeout } = input;
|
|
30
30
|
// 运行时双校验:timeout 必填且必须为正数
|
|
31
31
|
if (typeof timeout !== "number" || !Number.isFinite(timeout) || timeout <= 0) {
|
|
32
|
-
throw new Error(`exec:
|
|
32
|
+
throw new Error(`exec: timeout is required and must be a positive number (milliseconds), got: ${JSON.stringify(timeout)}`);
|
|
33
33
|
}
|
|
34
34
|
const signal = ctx?.signal;
|
|
35
35
|
const result = await new Promise((resolve) => {
|
|
@@ -37,7 +37,7 @@ export class ExecTool extends SdkCallbackTool {
|
|
|
37
37
|
if (signal?.aborted) {
|
|
38
38
|
resolve({
|
|
39
39
|
stdout: "",
|
|
40
|
-
stderr: "
|
|
40
|
+
stderr: "Process aborted",
|
|
41
41
|
exitCode: null,
|
|
42
42
|
killed: true,
|
|
43
43
|
terminated: "aborted",
|
|
@@ -72,7 +72,7 @@ export class ExecTool extends SdkCallbackTool {
|
|
|
72
72
|
// 即便 exec 回调因信号处理而未能及时触发,也立即返回中断结果,避免挂起
|
|
73
73
|
settle({
|
|
74
74
|
stdout: "",
|
|
75
|
-
stderr: "
|
|
75
|
+
stderr: "Process aborted",
|
|
76
76
|
exitCode: null,
|
|
77
77
|
killed: true,
|
|
78
78
|
terminated: "aborted",
|
|
@@ -97,8 +97,8 @@ export class ExecTool extends SdkCallbackTool {
|
|
|
97
97
|
const stderr = headTailPreview(result.stderr);
|
|
98
98
|
return JSON.stringify({
|
|
99
99
|
outputTooLarge: true,
|
|
100
|
-
warning:
|
|
101
|
-
|
|
100
|
+
warning: `Output too large: stdout + stderr total ${result.stdout.length + result.stderr.length} characters, ` +
|
|
101
|
+
`exceeds the limit of ${limit} characters. Full content was written to separate files (stdout.log / stderr.log); use read to fetch it in chunks`,
|
|
102
102
|
files: ctx.files,
|
|
103
103
|
stdout: { chars: result.stdout.length, lines: countLines(result.stdout), ...stdout },
|
|
104
104
|
stderr: { chars: result.stderr.length, lines: countLines(result.stderr), ...stderr },
|
|
@@ -3,13 +3,13 @@ import { SdkCallbackTool } from "./SdkCallbackTool.js";
|
|
|
3
3
|
export class ExistTool extends SdkCallbackTool {
|
|
4
4
|
function = {
|
|
5
5
|
name: "exist",
|
|
6
|
-
description: "
|
|
6
|
+
description: "Check whether a file or directory exists",
|
|
7
7
|
parameters: {
|
|
8
8
|
type: "object",
|
|
9
9
|
properties: {
|
|
10
10
|
path: {
|
|
11
11
|
type: "string",
|
|
12
|
-
description: "
|
|
12
|
+
description: "File or directory path",
|
|
13
13
|
},
|
|
14
14
|
},
|
|
15
15
|
required: ["path"],
|
|
@@ -11,7 +11,7 @@ const DEFAULT_MAX_LINE_LENGTH = 200;
|
|
|
11
11
|
export class FindTextTool extends SdkCallbackTool {
|
|
12
12
|
function = {
|
|
13
13
|
name: "find_text",
|
|
14
|
-
description: "
|
|
14
|
+
description: "Find where text occurs, supporting plain text or regex matching; returns file names with line numbers and line content. To avoid blowing up the context, the total match count, per-file size and line content length are limited by default, and results beyond the limits are truncated and marked truncated.",
|
|
15
15
|
parameters: {
|
|
16
16
|
type: "object",
|
|
17
17
|
properties: {
|
|
@@ -25,30 +25,30 @@ export class FindTextTool extends SdkCallbackTool {
|
|
|
25
25
|
},
|
|
26
26
|
text: {
|
|
27
27
|
type: "string",
|
|
28
|
-
description: "
|
|
28
|
+
description: "Text to search for (mutually exclusive with regex)",
|
|
29
29
|
},
|
|
30
30
|
regex: {
|
|
31
31
|
type: "string",
|
|
32
|
-
description: "
|
|
32
|
+
description: "Regular expression (mutually exclusive with text, e.g. \\bconst\\s+\\w+)",
|
|
33
33
|
},
|
|
34
34
|
exclude: {
|
|
35
35
|
type: "array",
|
|
36
|
-
description: "
|
|
36
|
+
description: "Array of glob patterns to exclude",
|
|
37
37
|
items: {
|
|
38
38
|
type: "string",
|
|
39
39
|
},
|
|
40
40
|
},
|
|
41
41
|
maxMatches: {
|
|
42
42
|
type: "number",
|
|
43
|
-
description: "
|
|
43
|
+
description: "Maximum total matches (default 200)",
|
|
44
44
|
},
|
|
45
45
|
maxFileSize: {
|
|
46
46
|
type: "number",
|
|
47
|
-
description: "
|
|
47
|
+
description: "Maximum file size in bytes; larger files are skipped (default 307200, i.e. 300KB)",
|
|
48
48
|
},
|
|
49
49
|
maxLineLength: {
|
|
50
50
|
type: "number",
|
|
51
|
-
description: "
|
|
51
|
+
description: "Truncation length for line content and matched substrings (default 200)",
|
|
52
52
|
},
|
|
53
53
|
},
|
|
54
54
|
required: ["path", "pattern"],
|
|
@@ -65,7 +65,7 @@ export class FindTextTool extends SdkCallbackTool {
|
|
|
65
65
|
const text = input.text;
|
|
66
66
|
const regexStr = input.regex;
|
|
67
67
|
if (!text && !regexStr) {
|
|
68
|
-
return "
|
|
68
|
+
return "Provide either the text or the regex parameter";
|
|
69
69
|
}
|
|
70
70
|
const regex = regexStr ? new RegExp(regexStr) : null;
|
|
71
71
|
const maxMatches = input.maxMatches ?? DEFAULT_MAX_MATCHES;
|
|
@@ -153,8 +153,8 @@ export class FindTextTool extends SdkCallbackTool {
|
|
|
153
153
|
const preview = headPreview(json);
|
|
154
154
|
return JSON.stringify({
|
|
155
155
|
outputTooLarge: true,
|
|
156
|
-
warning:
|
|
157
|
-
|
|
156
|
+
warning: `Output too large: the results total ${json.length} characters, exceeding the limit of ${limit} characters. ` +
|
|
157
|
+
`The full result has been written to disk (result.json) and can be read in chunks with read.`,
|
|
158
158
|
files: ctx.files,
|
|
159
159
|
chars: json.length,
|
|
160
160
|
...preview,
|