@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
|
@@ -3,7 +3,7 @@ import { fileToContentSection, resolveModelAndEndpoint } from "../../../shared/c
|
|
|
3
3
|
import { createDisclosureParam } from "../../disclosure.js";
|
|
4
4
|
import { DEFAULT_MAX_TOOL_OUTPUT, guardOutput, headTailPreview } from "../builtin/outputGuard.js";
|
|
5
5
|
import { getLogger } from "../../../shared/logger.js";
|
|
6
|
-
const EMPTY_HINT = "
|
|
6
|
+
const EMPTY_HINT = "(No MCP servers available, ask the user to add one)";
|
|
7
7
|
/**
|
|
8
8
|
* 创建 load_mcp 工具。
|
|
9
9
|
* server 枚举由 filteredMcps (完整 McpServerConfig[])自动推导,
|
|
@@ -16,16 +16,16 @@ export function createLoadMcpTool(mcpManager, filteredMcps) {
|
|
|
16
16
|
configMap.set(s.id, s);
|
|
17
17
|
}
|
|
18
18
|
const ids = filteredMcps.map((s) => s.id);
|
|
19
|
-
const param = createDisclosureParam(ids, "
|
|
19
|
+
const param = createDisclosureParam(ids, "MCP server name", EMPTY_HINT);
|
|
20
20
|
// 拼接各 MCP 服务器的描述供 LLM 参考(对齐 load_skill 的处理方式)
|
|
21
21
|
const mcpDescriptions = filteredMcps
|
|
22
|
-
.map((s) => ` - ${s.id}: ${s.description || "
|
|
22
|
+
.map((s) => ` - ${s.id}: ${s.description || "No description"}`)
|
|
23
23
|
.join("\n");
|
|
24
|
-
const serverIdDescription = `${param.description}\n\
|
|
24
|
+
const serverIdDescription = `${param.description}\n\nAvailable MCP servers:\n${mcpDescriptions || " No MCP servers available"}`;
|
|
25
25
|
return new CallbackTool({
|
|
26
26
|
function: {
|
|
27
27
|
name: "load_mcp",
|
|
28
|
-
description: "
|
|
28
|
+
description: "Connect to the specified MCP server and get the list of tools and resources it exposes. Pass include_manifest=false to only establish the connection without returning the manifest (useful when the manifest is already in context).",
|
|
29
29
|
parameters: {
|
|
30
30
|
type: "object",
|
|
31
31
|
properties: {
|
|
@@ -36,7 +36,7 @@ export function createLoadMcpTool(mcpManager, filteredMcps) {
|
|
|
36
36
|
},
|
|
37
37
|
include_manifest: {
|
|
38
38
|
type: "boolean",
|
|
39
|
-
description: "
|
|
39
|
+
description: "Whether to return the server's full capability manifest (tools and resources). Optional, defaults to true. If the manifest is already in your context (for example it was loaded earlier in this session but the connection was dropped after a process restart), set it to false to only establish the connection and return a summary, saving context tokens",
|
|
40
40
|
default: true,
|
|
41
41
|
},
|
|
42
42
|
},
|
|
@@ -48,7 +48,7 @@ export function createLoadMcpTool(mcpManager, filteredMcps) {
|
|
|
48
48
|
const serverName = input.server;
|
|
49
49
|
const config = configMap.get(serverName);
|
|
50
50
|
if (!config) {
|
|
51
|
-
return `❌ MCP
|
|
51
|
+
return `❌ MCP server "${serverName}" is not configured`;
|
|
52
52
|
}
|
|
53
53
|
// 披露开关:默认返回完整清单;显式传 false 时仅建立连接、只返回摘要
|
|
54
54
|
const includeManifest = input.include_manifest !== false;
|
|
@@ -64,12 +64,12 @@ export function createLoadMcpTool(mcpManager, filteredMcps) {
|
|
|
64
64
|
manifest = await mcpManager.connect(serverName, config);
|
|
65
65
|
}
|
|
66
66
|
catch (error) {
|
|
67
|
-
return
|
|
67
|
+
return `Failed to connect to "${serverName}": ${error?.message ?? error}`;
|
|
68
68
|
}
|
|
69
69
|
}
|
|
70
70
|
// 静默模式:只回吐摘要(清单仍留在管理器内,供 call_mcp_tool 使用)
|
|
71
71
|
if (!includeManifest) {
|
|
72
|
-
getLogger().info(`[load_mcp] ${serverName}:
|
|
72
|
+
getLogger().info(`[load_mcp] ${serverName}: connected (manifest not disclosed: ${manifest.tools.length} tools, ${manifest.resources.length} resources)`);
|
|
73
73
|
return JSON.stringify({
|
|
74
74
|
server: serverName,
|
|
75
75
|
connected: true,
|
|
@@ -117,7 +117,7 @@ async function mediaSection(data, mime, media) {
|
|
|
117
117
|
catch (error) {
|
|
118
118
|
return {
|
|
119
119
|
type: "text",
|
|
120
|
-
text: `[
|
|
120
|
+
text: `[media] Failed to convert (${mime}): ${error?.message ?? error}`,
|
|
121
121
|
};
|
|
122
122
|
}
|
|
123
123
|
}
|
|
@@ -161,11 +161,11 @@ async function toMessageSection(block, media) {
|
|
|
161
161
|
const mime = r.mimeType ?? "application/octet-stream";
|
|
162
162
|
return await mediaSection(r.blob, mime, media);
|
|
163
163
|
}
|
|
164
|
-
return { type: "text", text: `[
|
|
164
|
+
return { type: "text", text: `[resource] ${r.uri ?? "(unknown URI)"}` };
|
|
165
165
|
}
|
|
166
166
|
case "resource_link": {
|
|
167
167
|
const parts = [block.name, block.uri, block.mimeType].filter(Boolean);
|
|
168
|
-
return { type: "text", text: `[
|
|
168
|
+
return { type: "text", text: `[resource_link] ${parts.join(" · ")}` };
|
|
169
169
|
}
|
|
170
170
|
case "text":
|
|
171
171
|
return { type: "text", text: block.text ?? "" };
|
|
@@ -200,8 +200,8 @@ async function protectCallOutput(output, env) {
|
|
|
200
200
|
return JSON.stringify({
|
|
201
201
|
outputTooLarge: true,
|
|
202
202
|
isError: isError || undefined,
|
|
203
|
-
warning:
|
|
204
|
-
|
|
203
|
+
warning: `Text output too large: ${chars} characters, exceeding the limit of ${limit} characters. ` +
|
|
204
|
+
`The full text has been written to disk (result.txt); use read to page through it.`,
|
|
205
205
|
files: ctx.files,
|
|
206
206
|
chars,
|
|
207
207
|
...preview,
|
|
@@ -228,9 +228,9 @@ async function protectCallOutput(output, env) {
|
|
|
228
228
|
// 未超限:无媒体 → 纯字符串;有媒体 → 原序块数组
|
|
229
229
|
const hasMedia = sections.some((s) => s.type !== "text");
|
|
230
230
|
if (!hasMedia)
|
|
231
|
-
return isError ? `❌
|
|
231
|
+
return isError ? `❌ Tool execution failed:\n${fullText}` : fullText;
|
|
232
232
|
return isError
|
|
233
|
-
? [{ type: "text", text: "❌
|
|
233
|
+
? [{ type: "text", text: "❌ Tool execution failed:" }, ...sections]
|
|
234
234
|
: sections;
|
|
235
235
|
}
|
|
236
236
|
/**
|
|
@@ -247,13 +247,13 @@ export function createCallMcpTool(mcpManager, env) {
|
|
|
247
247
|
return new CallbackTool({
|
|
248
248
|
function: {
|
|
249
249
|
name: "call_mcp_tool",
|
|
250
|
-
description: "
|
|
250
|
+
description: "Call a tool exposed by a connected MCP server. Connect the server with load_mcp first. Large text output is written to disk and only a head/tail preview is returned; images, audio and other media are preserved: uploaded and referenced by file_id when the endpoint supports the Files API, otherwise inlined as base64.",
|
|
251
251
|
parameters: {
|
|
252
252
|
type: "object",
|
|
253
253
|
properties: {
|
|
254
|
-
server: { type: "string", description: "MCP
|
|
255
|
-
tool: { type: "string", description: "
|
|
256
|
-
arguments: { type: "object", description: "
|
|
254
|
+
server: { type: "string", description: "MCP server name" },
|
|
255
|
+
tool: { type: "string", description: "Tool name" },
|
|
256
|
+
arguments: { type: "object", description: "Tool arguments" },
|
|
257
257
|
},
|
|
258
258
|
required: ["server", "tool", "arguments"],
|
|
259
259
|
additionalProperties: false,
|
|
@@ -263,11 +263,11 @@ export function createCallMcpTool(mcpManager, env) {
|
|
|
263
263
|
const serverName = input.server;
|
|
264
264
|
const state = mcpManager.getState(serverName);
|
|
265
265
|
if (state !== "connected") {
|
|
266
|
-
return
|
|
266
|
+
return `Call load_mcp to connect "${serverName}" first`;
|
|
267
267
|
}
|
|
268
268
|
const client = mcpManager.getClient(serverName);
|
|
269
269
|
if (!client) {
|
|
270
|
-
return `MCP
|
|
270
|
+
return `MCP server "${serverName}" client is not available`;
|
|
271
271
|
}
|
|
272
272
|
try {
|
|
273
273
|
mcpManager.touch(serverName);
|
|
@@ -294,7 +294,7 @@ export function createCallMcpTool(mcpManager, env) {
|
|
|
294
294
|
return await protectCallOutput({ sections, fullText, chars, isError }, env);
|
|
295
295
|
}
|
|
296
296
|
catch (error) {
|
|
297
|
-
return `❌
|
|
297
|
+
return `❌ Failed to call "${input.tool}": ${error?.message ?? error}`;
|
|
298
298
|
}
|
|
299
299
|
},
|
|
300
300
|
});
|
|
@@ -306,12 +306,12 @@ export function createReadMcpResourceTool(mcpManager, env) {
|
|
|
306
306
|
return new CallbackTool({
|
|
307
307
|
function: {
|
|
308
308
|
name: "read_mcp_resource",
|
|
309
|
-
description: "
|
|
309
|
+
description: "Read a resource (document, data, image, etc.) from a connected MCP server. Text is returned as-is; images and other media are uploaded (file_id) or inlined as base64 depending on endpoint capabilities, and are never dropped.",
|
|
310
310
|
parameters: {
|
|
311
311
|
type: "object",
|
|
312
312
|
properties: {
|
|
313
|
-
server: { type: "string", description: "MCP
|
|
314
|
-
uri: { type: "string", description: "
|
|
313
|
+
server: { type: "string", description: "MCP server name" },
|
|
314
|
+
uri: { type: "string", description: "Resource URI" },
|
|
315
315
|
},
|
|
316
316
|
required: ["server", "uri"],
|
|
317
317
|
additionalProperties: false,
|
|
@@ -321,11 +321,11 @@ export function createReadMcpResourceTool(mcpManager, env) {
|
|
|
321
321
|
const serverName = input.server;
|
|
322
322
|
const state = mcpManager.getState(serverName);
|
|
323
323
|
if (state !== "connected") {
|
|
324
|
-
return
|
|
324
|
+
return `Call load_mcp to connect "${serverName}" first`;
|
|
325
325
|
}
|
|
326
326
|
const client = mcpManager.getClient(serverName);
|
|
327
327
|
if (!client) {
|
|
328
|
-
return `MCP
|
|
328
|
+
return `MCP server "${serverName}" client is not available`;
|
|
329
329
|
}
|
|
330
330
|
try {
|
|
331
331
|
mcpManager.touch(serverName);
|
|
@@ -349,7 +349,7 @@ export function createReadMcpResourceTool(mcpManager, env) {
|
|
|
349
349
|
return await protectCallOutput({ sections, fullText, chars, isError: false }, env);
|
|
350
350
|
}
|
|
351
351
|
catch (error) {
|
|
352
|
-
return `❌
|
|
352
|
+
return `❌ Failed to read resource "${input.uri}": ${error?.message ?? error}`;
|
|
353
353
|
}
|
|
354
354
|
},
|
|
355
355
|
});
|
|
@@ -165,27 +165,27 @@ const VALID_NAME_RE = /^[a-z0-9]+(-[a-z0-9]+)*$/;
|
|
|
165
165
|
export function validateSkill(skillId, fm) {
|
|
166
166
|
const errors = [];
|
|
167
167
|
if (!fm.name || fm.name.length === 0) {
|
|
168
|
-
errors.push({ skillId, field: "name", message: "
|
|
168
|
+
errors.push({ skillId, field: "name", message: "Must be present and non-empty" });
|
|
169
169
|
}
|
|
170
170
|
else {
|
|
171
171
|
if (fm.name.length > 64) {
|
|
172
|
-
errors.push({ skillId, field: "name", message:
|
|
172
|
+
errors.push({ skillId, field: "name", message: `Exceeds 64 characters (currently ${fm.name.length})` });
|
|
173
173
|
}
|
|
174
174
|
if (!VALID_NAME_RE.test(fm.name)) {
|
|
175
|
-
errors.push({ skillId, field: "name", message: `"${fm.name}"
|
|
175
|
+
errors.push({ skillId, field: "name", message: `"${fm.name}" is not valid: only lowercase letters, digits and hyphens are allowed, and it must not start, end or contain consecutive hyphens` });
|
|
176
176
|
}
|
|
177
177
|
if (fm.name !== skillId) {
|
|
178
|
-
errors.push({ skillId, field: "name", message: `"${fm.name}"
|
|
178
|
+
errors.push({ skillId, field: "name", message: `"${fm.name}" does not match the directory name "${skillId}"` });
|
|
179
179
|
}
|
|
180
180
|
}
|
|
181
181
|
if (!fm.description || fm.description.length === 0) {
|
|
182
|
-
errors.push({ skillId, field: "description", message: "
|
|
182
|
+
errors.push({ skillId, field: "description", message: "Must be present and non-empty" });
|
|
183
183
|
}
|
|
184
184
|
else if (fm.description.length > 1024) {
|
|
185
|
-
errors.push({ skillId, field: "description", message:
|
|
185
|
+
errors.push({ skillId, field: "description", message: `Exceeds 1024 characters (currently ${fm.description.length})` });
|
|
186
186
|
}
|
|
187
187
|
if (fm.compatibility && fm.compatibility.length > 500) {
|
|
188
|
-
errors.push({ skillId, field: "compatibility", message:
|
|
188
|
+
errors.push({ skillId, field: "compatibility", message: `Exceeds 500 characters (currently ${fm.compatibility.length})` });
|
|
189
189
|
}
|
|
190
190
|
return errors;
|
|
191
191
|
}
|
|
@@ -2,23 +2,23 @@ import { CallbackTool, Agent, AgentToolLazy, Message } from "@ai-zen/agents-core
|
|
|
2
2
|
import { createDisclosureParam } from "../../disclosure.js";
|
|
3
3
|
import { readSkill } from "./discover.js";
|
|
4
4
|
import { promises as fs } from "node:fs";
|
|
5
|
-
const EMPTY_HINT = "
|
|
5
|
+
const EMPTY_HINT = "(No Skills are currently available, please ask the user to add one)";
|
|
6
6
|
/**
|
|
7
7
|
* 创建 load_skill 工具。
|
|
8
8
|
* 枚举所有允许的 skill(不限是否支持子 Agent),由 filteredSkills 提供完整信息。
|
|
9
9
|
*/
|
|
10
10
|
export function createLoadSkillTool(skillDirs, filteredSkills) {
|
|
11
11
|
const ids = filteredSkills.map((s) => s.id);
|
|
12
|
-
const param = createDisclosureParam(ids, "
|
|
12
|
+
const param = createDisclosureParam(ids, "Select a Skill", EMPTY_HINT);
|
|
13
13
|
// 拼接各 skill 的描述供 LLM 参考
|
|
14
14
|
const skillDescriptions = filteredSkills
|
|
15
|
-
.map((s) => ` - ${s.id}: ${s.description || "
|
|
15
|
+
.map((s) => ` - ${s.id}: ${s.description || "No description"}`)
|
|
16
16
|
.join("\n");
|
|
17
|
-
const skillIdDescription = `${param.description}\n\
|
|
17
|
+
const skillIdDescription = `${param.description}\n\nSkill descriptions:\n${skillDescriptions || " No Skills available"}`;
|
|
18
18
|
return new CallbackTool({
|
|
19
19
|
function: {
|
|
20
20
|
name: "load_skill",
|
|
21
|
-
description: "
|
|
21
|
+
description: "Load the full guidance document of a Skill. A Skill is pre-authored expert guidance that is expanded into the context once loaded.",
|
|
22
22
|
parameters: {
|
|
23
23
|
type: "object",
|
|
24
24
|
properties: {
|
|
@@ -36,7 +36,7 @@ export function createLoadSkillTool(skillDirs, filteredSkills) {
|
|
|
36
36
|
const skillId = input.skill_id;
|
|
37
37
|
const skill = await readSkill(skillDirs, skillId);
|
|
38
38
|
if (!skill) {
|
|
39
|
-
return `❌ Skill "${skillId}"
|
|
39
|
+
return `❌ Skill "${skillId}" not found, please verify the name`;
|
|
40
40
|
}
|
|
41
41
|
// 扫描 skill 目录下的文件列表
|
|
42
42
|
let fileList = "";
|
|
@@ -46,18 +46,18 @@ export function createLoadSkillTool(skillDirs, filteredSkills) {
|
|
|
46
46
|
const suffix = e.isDirectory() ? "/" : "";
|
|
47
47
|
return ` - ${e.name}${suffix}`;
|
|
48
48
|
});
|
|
49
|
-
fileList = `\n\nSkill
|
|
49
|
+
fileList = `\n\nSkill directory files:\n${files.join("\n")}`;
|
|
50
50
|
}
|
|
51
51
|
catch {
|
|
52
52
|
fileList = "";
|
|
53
53
|
}
|
|
54
54
|
return [
|
|
55
|
-
`Skill "${skillId}"
|
|
55
|
+
`Skill "${skillId}" loaded:`,
|
|
56
56
|
``,
|
|
57
|
-
`Skill
|
|
57
|
+
`Skill directory path: ${skill.dirPath}`,
|
|
58
58
|
fileList,
|
|
59
59
|
``,
|
|
60
|
-
`--- Skill
|
|
60
|
+
`--- Skill content starts ---`,
|
|
61
61
|
skill.content,
|
|
62
62
|
].join("\n");
|
|
63
63
|
},
|
|
@@ -76,16 +76,16 @@ export function createCallSkillSubAgentTool(skillDirs, filteredSkills) {
|
|
|
76
76
|
// 只保留支持子 Agent 模式的 skill
|
|
77
77
|
const subAgentSkills = filteredSkills.filter((s) => s.subAgent);
|
|
78
78
|
const ids = subAgentSkills.map((s) => s.id);
|
|
79
|
-
const param = createDisclosureParam(ids, "
|
|
79
|
+
const param = createDisclosureParam(ids, "Select a Skill that supports SubAgent mode", EMPTY_HINT);
|
|
80
80
|
// 拼接各 skill 的描述供 LLM 参考
|
|
81
81
|
const skillDescriptions = subAgentSkills
|
|
82
|
-
.map((s) => ` - ${s.id}: ${s.description || "
|
|
82
|
+
.map((s) => ` - ${s.id}: ${s.description || "No description"}`)
|
|
83
83
|
.join("\n");
|
|
84
|
-
const skillIdDescription = `${param.description}\n\
|
|
84
|
+
const skillIdDescription = `${param.description}\n\nSkill descriptions:\n${skillDescriptions || " No Skills available"}`;
|
|
85
85
|
return new AgentToolLazy({
|
|
86
86
|
function: {
|
|
87
87
|
name: "call_skill_sub_agent",
|
|
88
|
-
description: "
|
|
88
|
+
description: "Delegate a task to the specified Skill SubAgent, which completes it independently and returns the result.",
|
|
89
89
|
parameters: {
|
|
90
90
|
type: "object",
|
|
91
91
|
properties: {
|
|
@@ -96,7 +96,7 @@ export function createCallSkillSubAgentTool(skillDirs, filteredSkills) {
|
|
|
96
96
|
},
|
|
97
97
|
task: {
|
|
98
98
|
type: "string",
|
|
99
|
-
description: "
|
|
99
|
+
description: "Task description to delegate to the SubAgent",
|
|
100
100
|
},
|
|
101
101
|
},
|
|
102
102
|
required: ["skill_id", "task"],
|
|
@@ -110,10 +110,10 @@ export function createCallSkillSubAgentTool(skillDirs, filteredSkills) {
|
|
|
110
110
|
const task = parsedArgs.task;
|
|
111
111
|
const skill = await readSkill(skillDirs, skillId);
|
|
112
112
|
if (!skill) {
|
|
113
|
-
throw new Error(`❌ Skill "${skillId}"
|
|
113
|
+
throw new Error(`❌ Skill "${skillId}" not found, please verify the name`);
|
|
114
114
|
}
|
|
115
115
|
if (!skill.subAgent) {
|
|
116
|
-
throw new Error(`Skill "${skillId}"
|
|
116
|
+
throw new Error(`Skill "${skillId}" does not support SubAgent mode, use load_skill to load its guidance and handle the task yourself`);
|
|
117
117
|
}
|
|
118
118
|
// 沿用父 Agent 的工具能力(不做权限过滤),剔除自身以防自递归
|
|
119
119
|
const parentAgent = ctx.agent;
|
|
@@ -8,7 +8,7 @@ import { createModel } from "../../../runtime/createModel.js";
|
|
|
8
8
|
*/
|
|
9
9
|
export function createSubAgentTool(def, scope) {
|
|
10
10
|
if (!def.function) {
|
|
11
|
-
throw new Error(`Agent "${def.id}"
|
|
11
|
+
throw new Error(`Agent "${def.id}" has no function field, so it cannot be used as a SubAgent tool`);
|
|
12
12
|
}
|
|
13
13
|
const selfName = def.function.name;
|
|
14
14
|
const lazy = new AgentToolLazy({
|
|
@@ -33,7 +33,7 @@ export function createSubAgentTool(def, scope) {
|
|
|
33
33
|
else {
|
|
34
34
|
const parent = ctx.agent;
|
|
35
35
|
if (!parent) {
|
|
36
|
-
throw new Error(`SubAgent "${selfName}"
|
|
36
|
+
throw new Error(`SubAgent "${selfName}" does not specify a modelId, and the parent Agent has no reusable model`);
|
|
37
37
|
}
|
|
38
38
|
client = parent.client;
|
|
39
39
|
model = parent.model;
|
|
@@ -74,7 +74,7 @@ async function loadToolFile(filepath, silent) {
|
|
|
74
74
|
}
|
|
75
75
|
catch (err) {
|
|
76
76
|
if (!silent) {
|
|
77
|
-
getLogger().error(`[usertools]
|
|
77
|
+
getLogger().error(`[usertools] Failed to load tool file: ${filepath} — ${err?.message ?? err}`);
|
|
78
78
|
}
|
|
79
79
|
return null;
|
|
80
80
|
}
|
|
@@ -110,7 +110,7 @@ function normalizeToolExport(exported, filepath, silent) {
|
|
|
110
110
|
});
|
|
111
111
|
}
|
|
112
112
|
if (!silent) {
|
|
113
|
-
getLogger().error(`[usertools]
|
|
113
|
+
getLogger().error(`[usertools] Unrecognized tool format: ${filepath}`);
|
|
114
114
|
}
|
|
115
115
|
return null;
|
|
116
116
|
}
|
|
@@ -139,7 +139,7 @@ export async function fileToContentSection(input) {
|
|
|
139
139
|
// ---- 1. 网络 URL:直接引用,不经上传/内联 ----
|
|
140
140
|
if ("url" in source) {
|
|
141
141
|
if (!isRemoteUrl(source.url)) {
|
|
142
|
-
throw new Error(
|
|
142
|
+
throw new Error(`Unsupported local URL (${source.url}): the API provider cannot access it. For local files pass a file path, not a local link`);
|
|
143
143
|
}
|
|
144
144
|
return {
|
|
145
145
|
section: { type: "image_url", image_url: { url: source.url } },
|
|
@@ -160,10 +160,10 @@ export async function fileToContentSection(input) {
|
|
|
160
160
|
absPath = path.isAbsolute(source.path) ? source.path : path.resolve(source.path);
|
|
161
161
|
const st = await fs.stat(absPath);
|
|
162
162
|
if (!st.isFile())
|
|
163
|
-
throw new Error(`"${absPath}"
|
|
163
|
+
throw new Error(`"${absPath}" is not a file`);
|
|
164
164
|
const inferred = mimeOf(absPath);
|
|
165
165
|
if (!inferred) {
|
|
166
|
-
throw new Error(
|
|
166
|
+
throw new Error(`Unsupported file type: "${absPath}" (known types: ${Object.keys(MIME_BY_EXT).join(" ")})`);
|
|
167
167
|
}
|
|
168
168
|
mime = inferred;
|
|
169
169
|
stat = { mtimeMs: st.mtimeMs, size: st.size };
|
|
@@ -198,10 +198,10 @@ export async function fileToContentSection(input) {
|
|
|
198
198
|
if (isImage && model && model.vision !== true) {
|
|
199
199
|
const label = absPath ?? filename;
|
|
200
200
|
return {
|
|
201
|
-
section: textSection(`[
|
|
201
|
+
section: textSection(`[Image] ${label} (the current model does not support image input; use a vision-capable model to view it)`),
|
|
202
202
|
transport: "text",
|
|
203
203
|
bytes: byteLength,
|
|
204
|
-
warning: "
|
|
204
|
+
warning: "The current model does not support image input; degraded to a text hint",
|
|
205
205
|
};
|
|
206
206
|
}
|
|
207
207
|
// ---- 4. 端点支持 Files API:上传(带缓存),失败降级内联 ----
|
|
@@ -231,14 +231,14 @@ export async function fileToContentSection(input) {
|
|
|
231
231
|
catch (error) {
|
|
232
232
|
if (signal?.aborted)
|
|
233
233
|
throw error;
|
|
234
|
-
warning =
|
|
234
|
+
warning = `Files API upload failed, falling back to base64 inline: ${error?.message ?? error}`;
|
|
235
235
|
}
|
|
236
236
|
}
|
|
237
237
|
// ---- 5. 内联(base64 data URL)----
|
|
238
238
|
if (byteLength > maxInline) {
|
|
239
|
-
throw new Error(
|
|
240
|
-
`${(maxInline / 1024 / 1024).toFixed(1)} MiB
|
|
241
|
-
|
|
239
|
+
throw new Error(`File size ${(byteLength / 1024 / 1024).toFixed(1)} MiB exceeds the inline limit of ` +
|
|
240
|
+
`${(maxInline / 1024 / 1024).toFixed(1)} MiB and the endpoint does not declare the Files API capability ` +
|
|
241
|
+
`(capabilities.filesApi), so it cannot be sent`);
|
|
242
242
|
}
|
|
243
243
|
// base64 来源直接透传,不重新编码(保证字节级一致,也省一次编解码)
|
|
244
244
|
const dataUrl = toDataUrl(base64 ?? Buffer.from(ensureBytes()).toString("base64"), mime);
|
package/dist/shared/errors.js
CHANGED
|
@@ -24,9 +24,9 @@ export class ContextOverflowError extends Error {
|
|
|
24
24
|
/** 被越过的强行阻断阈值(`tokenThresholds.guard`,绝对值) */
|
|
25
25
|
guardTokens;
|
|
26
26
|
constructor(promptTokens, guardTokens) {
|
|
27
|
-
super(`🛑
|
|
28
|
-
|
|
29
|
-
|
|
27
|
+
super(`🛑 Context critically over the limit: ${promptTokens} tokens used, above the blocking threshold of ${guardTokens}.` +
|
|
28
|
+
` The conversation may have run away after reading an oversized file, so it has been interrupted — please inspect and start over.` +
|
|
29
|
+
` (If you really need a longer context, raise tokenThresholds.guard for this model in config.json.)`);
|
|
30
30
|
this.name = "ContextOverflowError";
|
|
31
31
|
this.promptTokens = promptTokens;
|
|
32
32
|
this.guardTokens = guardTokens;
|
package/dist/types/index.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { AgentNS } from "@ai-zen/agents-core";
|
|
1
|
+
import type { AgentNS, AbortedMessageStrategy, AbortedToolRoundStrategy } from "@ai-zen/agents-core";
|
|
2
2
|
/** 单维度权限策略:allow 或 deny 互斥 */
|
|
3
3
|
export type PermissionPolicy = {
|
|
4
4
|
allow: string[];
|
|
@@ -200,6 +200,27 @@ export interface AppConfig {
|
|
|
200
200
|
* (如 read/find_text 对 300KB 文件的读取上限)。
|
|
201
201
|
*/
|
|
202
202
|
maxToolOutput?: number;
|
|
203
|
+
/**
|
|
204
|
+
* 被中断的 assistant 消息(已输出的思考 / 正文)如何处置。
|
|
205
|
+
*
|
|
206
|
+
* - `keep`:保留已输出的部分(恢复为 `Completed`)—— 模型下一轮能接着半截往下说,
|
|
207
|
+
* 不会「失忆重来」。**出厂默认**。
|
|
208
|
+
* - `drop`:丢弃(保持 `Aborted`,不进下一轮上下文)。core 的库级默认。
|
|
209
|
+
*
|
|
210
|
+
* 只管**不带工具调用**的消息;工具轮次由 {@link abortedToolRoundStrategy} 决定。
|
|
211
|
+
* 缺省时由 `createAgent()` 回落 core 默认值(`drop`),出厂配置已写 `keep`。
|
|
212
|
+
*/
|
|
213
|
+
abortedMessageStrategy?: AbortedMessageStrategy;
|
|
214
|
+
/**
|
|
215
|
+
* 被中断 / 结果不完整的工具轮次如何归一。
|
|
216
|
+
*
|
|
217
|
+
* - `fill`:保留整轮(assistant 恢复为 `Completed`,未完成的结果补原因)——**出厂默认**,
|
|
218
|
+
* 服务端要求「N 个 tool_calls 对 N 条结果」,整轮进出才不会报错。
|
|
219
|
+
* - `drop`:整轮出列(assistant 与其结果都不发给模型)。
|
|
220
|
+
*
|
|
221
|
+
* 缺省时由 `createAgent()` 回落 core 默认值(`fill`)。
|
|
222
|
+
*/
|
|
223
|
+
abortedToolRoundStrategy?: AbortedToolRoundStrategy;
|
|
203
224
|
}
|
|
204
225
|
/**
|
|
205
226
|
* 工具环境 — Scope 的能力插件在实例化内置工具时注入的环境信息。
|
package/package.json
CHANGED
|
@@ -1,10 +1,19 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@ai-zen/agents-sdk",
|
|
3
|
-
"version": "1.0.0-alpha.
|
|
3
|
+
"version": "1.0.0-alpha.23",
|
|
4
4
|
"description": "AI-Zen Agents SDK — shared business logic for CLI and Desktop",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"types": "dist/index.d.ts",
|
|
7
7
|
"type": "module",
|
|
8
|
+
"repository": {
|
|
9
|
+
"type": "git",
|
|
10
|
+
"url": "git+https://github.com/ai-zen/agents.git",
|
|
11
|
+
"directory": "packages/sdk"
|
|
12
|
+
},
|
|
13
|
+
"homepage": "https://github.com/ai-zen/agents/tree/main/packages/sdk#readme",
|
|
14
|
+
"bugs": {
|
|
15
|
+
"url": "https://github.com/ai-zen/agents/issues"
|
|
16
|
+
},
|
|
8
17
|
"keywords": [
|
|
9
18
|
"ai",
|
|
10
19
|
"agent",
|