@ai-zen/agents-sdk 0.10.0 → 0.12.0

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 (28) hide show
  1. package/CHANGELOG.md +40 -0
  2. package/README.md +9 -6
  3. package/README.zh.md +9 -6
  4. package/dist/capabilities/implements/builtin/BatchEditTool.js +11 -9
  5. package/dist/capabilities/implements/builtin/ExecTool.js +33 -1
  6. package/dist/capabilities/implements/builtin/FindTextTool.d.ts +5 -0
  7. package/dist/capabilities/implements/builtin/FindTextTool.js +28 -3
  8. package/dist/capabilities/implements/builtin/GlobTool.d.ts +5 -0
  9. package/dist/capabilities/implements/builtin/GlobTool.js +28 -2
  10. package/dist/capabilities/implements/builtin/InspectFileTool.d.ts +17 -0
  11. package/dist/capabilities/implements/builtin/InspectFileTool.js +150 -0
  12. package/dist/capabilities/implements/builtin/LsTool.js +23 -2
  13. package/dist/capabilities/implements/builtin/ReadFileTool.d.ts +1 -0
  14. package/dist/capabilities/implements/builtin/ReadFileTool.js +60 -1
  15. package/dist/capabilities/implements/builtin/index.d.ts +3 -2
  16. package/dist/capabilities/implements/builtin/index.js +4 -2
  17. package/dist/capabilities/implements/builtin/outputGuard.d.ts +81 -0
  18. package/dist/capabilities/implements/builtin/outputGuard.js +73 -0
  19. package/dist/capabilities/implements/skillTools.d.ts +6 -4
  20. package/dist/capabilities/implements/skillTools.js +19 -35
  21. package/dist/config/ConfigManager.d.ts +3 -1
  22. package/dist/config/ConfigManager.js +31 -4
  23. package/dist/config/constants.js +6 -1
  24. package/dist/index.d.ts +1 -1
  25. package/dist/index.js +1 -1
  26. package/dist/runtime/Provider.js +1 -1
  27. package/dist/types/index.d.ts +15 -0
  28. package/package.json +2 -2
package/CHANGELOG.md CHANGED
@@ -1,5 +1,45 @@
1
1
  # Changelog
2
2
 
3
+ ## [0.12.0] - 2026-09-29
4
+
5
+ ### 🚀 New Features
6
+
7
+ - **统一工具输出保护(`maxToolOutput`)** — `AppConfig` 新增 `maxToolOutput?: number`(字符数,缺省 32768;出厂 `DEFAULT_APP_CONFIG` 已写入该值)。新增输出保护骨架 `guardOutput()`(`capabilities/implements/builtin/outputGuard.ts`):工具以三个回调自陈策略——`isOverLimit(content)`(工具自读 config 判定,含合计口径)、`dump(dir, content)`(落盘文件清单,缺省即"只警告不落盘")、`buildWarning(ctx)`(返回体,含预览与统计);骨架只负责"判定 → 未超限原样返回 / 超限时落盘"两件事。落盘目录为 `<os.tmpdir()>/ai-zen/tool-output/<工具名>-<时间戳>-<随机串>/`,每次调用独立互不覆盖,不做自清理(依赖系统对临时目录的策略)。已接入:
8
+ - **`exec`** — `stdout + stderr` 合计超限即分文件落盘 `stdout.log` / `stderr.log`,返回警告 + 各流头尾预览(各 1000 字符)+ 字符数/行数统计,原 `exitCode` / `killed` / `terminated` 字段保留;
9
+ - **`findText`** — 落盘 `result.json`,返回警告 + 头部预览(1000 字符);既有 `maxMatches` / `maxFileSize` / `maxLineLength` 参数保持不变;
10
+ - **`glob` / `ls`** — 落盘 `result.json`,返回警告 + 头部预览,并提示缩小范围的方式(更精确的 `pattern` / `exclude`,或改用 `glob`);
11
+ - **`readFile`** — 超限**不落盘**,仅返回警告并提示使用 `range` 分批读取。
12
+ - 另导出 `headPreview()` / `headTailPreview()` 供各工具按需组织预览(预览文本由工具自行生成)。
13
+ - **`readFile` 新增 `range` 参数(可选)** — `range: [起始行, 起始列, 结束行, 结束列]`,行列均 0-based、`-1` 表示末位;如 `[0, 0, 100, -1]` 表示"第 0 行第 0 列读到第 100 行最后一列"。300KB 读取阈值保持不变——读取阈值(能否读)与输出阈值(是否落盘)互相独立。
14
+ - **新增 `inspectFile` 工具(`InspectFileTool`)** — 勘察文件结构概况,不做内容读取,与 `readFile` 的 `range` 配套(先看结构,再决定读取范围)。返回 `path` / `bytes` / `lines` / `chars` / `maxCol` / `lineIndexOfMaxCol`(0-based)/ `avgCol` / `lineEnding`(`LF` / `CRLF` / `mixed`);可选 `withColCountMap: true` 返回"行索引 → 列数"对象映射。流式扫描,**不受 300KB 读取阈值限制**;列数按字符数计、不含行尾换行符;明细超限时不落盘,仅返回概况 + 提示"文件规模超出可处理范围"。内置工具类由 19 个增至 20 个。
15
+ - **`batchEdit` 输出精简** — 成功项不再回显原文与新文,全部成功时返回 `{ result: "success", replacedCount }`;存在未匹配项时返回 `{ result: "partial", replacedCount, failed: [{ oldText, newText, reason }] }`,仅回显有问题的原文与新文,避免大段文本回显撑大输出。
16
+ - **`AgentDefinition` 新增 `custom` 字段** — 为 `true` 时 SDK 初始化不触碰该 Agent 的内置内容(如默认提示词)。出厂 `DEFAULT_AGENT_DEFINITION` 携带 `custom: false`:`ensureDefaultAgent()` 在 `custom !== true` 时**仅同步出厂提示词**(`name` / `permissions` / `modelId` 等用户改动保留),提示词一致时不写盘、不动 `updatedAt`(比较时忽略随机生成的 message `id`,避免时间戳漂移)。
17
+ - **默认 Agent 提示词调整** — 去掉原有法则,改为两条:① 需要用户决策时,一次只问一个问题;② 当前对话基于 Node.js 驱动,可编写 Node.js 脚本执行复杂任务并通过 `exec` 工具运行。
18
+
19
+ ### ✅ Tests
20
+
21
+ - 新增 `outputGuard.test.ts`(骨架:未超限原样返回 / 超限落盘并返回警告 / 无 `dump` 时只警告 / 目录隔离 / `headPreview` 与 `headTailPreview`)、`InspectFileTool.test.ts`(概况字段、`colCountMap` 映射、CRLF 与 mixed、末行无换行、空文件、300KB 以上文件、明细超限降级、目录与不存在路径)及各工具的超限/范围读取用例;`ConfigManager.bootstrap.test.ts` 覆盖 `custom: true` 跳过、仅同步提示词、一致时不写盘。SDK 全量 **499 passed / 6 skipped**
22
+
23
+ ## [0.11.0] - 2026-09-28
24
+
25
+ ### 💥 Breaking Changes
26
+
27
+ - **`createCallSkillSubAgentTool` 移除第三参 `provider`** — Skill 子 Agent 不再经 `provider.buildTools(parent.definition, …)` 重新解析工具集,改为**直接沿用父 Agent 的工具集**(`ctx.agent.tools`),仅剔除 `call_skill_sub_agent` 自身:Skill 子 Agent 作为一次性对话分身,沿用调用者的能力而不再走第二遍权限过滤,同时避免 skill 链式自递归。传入 `Provider` 的调用方需删除该实参
28
+
29
+ ### 🚀 New Features
30
+
31
+ - **Skill 子 Agent 纳入统一委派边界** — `call_skill_sub_agent` 由 `AgentToolLazy` 承载(原为就地实现的 `CallbackTool`),因此与常规 SubAgent 工具共享同一套 `onSubAgentStart` / `onSubAgentEnd` 插件钩子与 `sub-agent-start` / `sub-agent-end` 事件。将护栏装配到 `ctx.subAgent` 的消费方(如上下文护栏)与渲染子 Agent 流式输出的消费方(如 CLI),自此均覆盖该路径
32
+
33
+ ### 🛠 Optimized
34
+
35
+ - **Skill 子 Agent 的构建收敛到 `buildAgent`** — 子 Agent 的初始消息(`System(SKILL.md 内容)` + `User(task)`)改由 `buildAgent` 决定(依托 core 4.3.0 的 `messages` 可选),原先手写的 abort 联动与返回值处理一并删除,交由工具基类统一实现
36
+ - **校验失败改为抛错** — `skill_id` 不存在、或 Skill 未声明 `sub-agent: true` 时抛出错误(原为返回提示文本);正常路径由 `skill_id` 枚举限定,不触发
37
+ - **依赖 `@ai-zen/agents-core` 升至 `^4.3.0`** — 依赖声明保持 `workspace:^`,由 pnpm 在发布时展开为 `^4.3.0`。core 4.3.0 使 `AgentToolLazy.messages` 变为可选
38
+
39
+ ### ✅ Tests
40
+
41
+ - `skillTools.test.ts` 中 `createCallSkillSubAgentTool` 用例重写为围绕 `AgentToolLazy`:工具契约不变、未知 Skill 抛错、非子 Agent Skill 抛错、委派边界(`onSubAgentStart` 与 `sub-agent-start` 分发、子 Agent 沿用父工具集且剔除自身、初始消息取自 SKILL.md 与 `task`);SDK 全量 **463 passed / 6 skipped**
42
+
3
43
  ## [0.10.0] - 2026-09-28
4
44
 
5
45
  ### 🚀 New Features
package/README.md CHANGED
@@ -89,20 +89,21 @@ All built-in tools are classes (extending `SdkCallbackTool`), instantiated by th
89
89
  | Tool | Description |
90
90
  |------|-------------|
91
91
  | `cwd` | Get the current working directory |
92
- | `readFile` | Read a file |
92
+ | `readFile` | Read a file (optional `range` for line/column slicing; oversized output warns and suggests batched reads) |
93
+ | `inspectFile` | Inspect a file's structural overview (line count, column distribution, etc.); streamed scan, not limited by the 300KB read threshold |
93
94
  | `writeFile` | Write a file |
94
- | `exec` | Execute a command (supports `timeout`) |
95
+ | `exec` | Execute a command (supports `timeout`; oversized stdout+stderr is dumped to `stdout.log` / `stderr.log`) |
95
96
  | `exec_async` | Execute a command asynchronously, returns immediately without waiting |
96
97
  | `mkdir` | Create a directory |
97
98
  | `rm` | Delete a file or directory |
98
- | `glob` | Scan and find files using glob patterns |
99
- | `ls` | List directory contents |
99
+ | `glob` | Scan and find files using glob patterns (oversized output dumped to `result.json`) |
100
+ | `ls` | List directory contents (oversized output dumped to `result.json`) |
100
101
  | `exist` | Check whether a file or directory exists |
101
- | `findText` | Search for text or regex in files |
102
+ | `findText` | Search for text or regex in files (oversized output dumped to `result.json`) |
102
103
  | `downloadFile` | Download a file from a URL and save it locally |
103
104
  | `rename` | Rename or move a file/directory |
104
105
  | `copy` | Copy a file or directory |
105
- | `batchEdit` | Batch-edit file text |
106
+ | `batchEdit` | Batch-edit file text (only unmatched items are echoed back; successful ones are counted) |
106
107
  | `edit` | Edit text in a file |
107
108
  | `sleep` | Wait for a specified number of milliseconds |
108
109
 
@@ -113,6 +114,8 @@ Conditionally injected based on the active model / config:
113
114
  | `generateImage` | Only when `defaultImageModel` is configured | Generate an image from a text description |
114
115
  | `viewImage` | Only for vision models (the agent's `modelId` resolves to a model with `vision: true`) | View / analyze an image: local images are auto-uploaded via the Files API, network URLs are referenced directly |
115
116
 
117
+ Tool output protection: `AppConfig.maxToolOutput` (in characters, default 32768) is the unified ceiling for tool output. Each tool handles overflow itself — `exec` dumps `stdout.log` / `stderr.log` separately (returning head/tail previews per stream), `findText` / `glob` / `ls` dump `result.json` (returning a head preview), and `readFile` only warns and suggests batched reads via `range`. Dumps go to `<tmpdir>/ai-zen/tool-output/<tool>-<timestamp>-<random>/`, one directory per call, with no self-cleanup. See [`docs/sdk-design.md` §7](docs/sdk-design.md).
118
+
116
119
  ## Built-in Plugins
117
120
 
118
121
  | Plugin | Description |
package/README.zh.md CHANGED
@@ -88,20 +88,21 @@ await agent.send("你好");
88
88
  | 工具 | 说明 |
89
89
  |------|------|
90
90
  | `cwd` | 获取当前工作目录 |
91
- | `readFile` | 读取文件 |
91
+ | `readFile` | 读取文件(可选 `range` 按行列范围读取;输出超限仅警告并提示用 `range` 分批) |
92
+ | `inspectFile` | 勘察文件结构概况(行数、列数分布等),流式扫描不受 300KB 限制 |
92
93
  | `writeFile` | 写入文件 |
93
- | `exec` | 执行命令(支持 `timeout` 超时参数) |
94
+ | `exec` | 执行命令(支持 `timeout` 超时参数;stdout+stderr 超限时分文件落盘 `stdout.log` / `stderr.log`) |
94
95
  | `exec_async` | 异步执行命令,启动后立即返回,不等待结果 |
95
96
  | `mkdir` | 创建目录 |
96
97
  | `rm` | 删除文件或目录 |
97
- | `glob` | 使用 glob 模式扫描查找文件 |
98
- | `ls` | 列出目录内容 |
98
+ | `glob` | 使用 glob 模式扫描查找文件(输出超限落盘 `result.json`) |
99
+ | `ls` | 列出目录内容(输出超限落盘 `result.json`) |
99
100
  | `exist` | 检查文件或目录是否存在 |
100
- | `findText` | 在文件中搜索文本或正则 |
101
+ | `findText` | 在文件中搜索文本或正则(输出超限落盘 `result.json`) |
101
102
  | `downloadFile` | 从 URL 下载文件并保存到本地 |
102
103
  | `rename` | 重命名或移动文件/目录 |
103
104
  | `copy` | 复制文件或目录 |
104
- | `batchEdit` | 批量编辑文件文本 |
105
+ | `batchEdit` | 批量编辑文件文本(仅回显未匹配项,成功项只计数) |
105
106
  | `edit` | 编辑文件中的文本 |
106
107
  | `sleep` | 等待指定毫秒数后继续 |
107
108
 
@@ -112,6 +113,8 @@ await agent.send("你好");
112
113
  | `generateImage` | 配置了 `defaultImageModel` 才注册 | 根据文字描述生成图片 |
113
114
  | `viewImage` | 仅视觉模型可用(Agent 的 `modelId` 解析为 `vision: true` 的模型) | 查看/分析图片:本地图片自动经 Files API 上传,网络 URL 直接引用 |
114
115
 
116
+ 工具输出保护:`AppConfig.maxToolOutput`(字符数,缺省 32768)是工具输出的统一上限,超限时由各工具自行处置——`exec` 分文件落盘 `stdout.log` / `stderr.log`(返回各流头尾预览),`findText` / `glob` / `ls` 落盘 `result.json`(返回头部预览),`readFile` 仅警告并提示用 `range` 分批读取。落盘目录为 `<tmpdir>/ai-zen/tool-output/<工具名>-<时间戳>-<随机串>/`,每次调用独立、不自清理。详见 [`docs/sdk-design.md` §7](docs/sdk-design.md)。
117
+
115
118
  ## 内置插件
116
119
 
117
120
  | 插件 | 说明 |
@@ -48,13 +48,16 @@ export class BatchEditTool extends SdkCallbackTool {
48
48
  const filePath = this.resolve(input.path);
49
49
  const content = await fsp.readFile(filePath, "utf-8");
50
50
  let newContent = content;
51
- const results = [];
51
+ let replacedCount = 0;
52
+ // 仅回显"有问题"的替换项(未匹配到),便于模型据此修正;
53
+ // 成功项不回显原文与新文,仅计入成功数,避免大段文本回显撑大输出
54
+ const failed = [];
52
55
  for (const replacement of input.replacements) {
53
56
  if (!newContent.includes(replacement.oldText)) {
54
- results.push({
57
+ failed.push({
55
58
  oldText: replacement.oldText,
56
59
  newText: replacement.newText,
57
- result: "文件中未精确匹配到要替换的文本",
60
+ reason: "文件中未精确匹配到要替换的文本",
58
61
  });
59
62
  continue;
60
63
  }
@@ -64,14 +67,13 @@ export class BatchEditTool extends SdkCallbackTool {
64
67
  else {
65
68
  newContent = newContent.replace(replacement.oldText, replacement.newText);
66
69
  }
67
- results.push({
68
- oldText: replacement.oldText,
69
- newText: replacement.newText,
70
- result: "success",
71
- });
70
+ replacedCount++;
72
71
  }
73
72
  await fsp.writeFile(filePath, newContent);
74
- return JSON.stringify(results);
73
+ if (failed.length === 0) {
74
+ return JSON.stringify({ result: "success", replacedCount });
75
+ }
76
+ return JSON.stringify({ result: "partial", replacedCount, failed });
75
77
  }
76
78
  catch (error) {
77
79
  return error?.message;
@@ -1,5 +1,6 @@
1
1
  import { exec } from "child_process";
2
2
  import { SdkCallbackTool } from "../../../runtime/SdkCallbackTool.js";
3
+ import { DEFAULT_MAX_TOOL_OUTPUT, guardOutput, headTailPreview } from "./outputGuard.js";
3
4
  export class ExecTool extends SdkCallbackTool {
4
5
  function = {
5
6
  name: "exec",
@@ -81,6 +82,37 @@ export class ExecTool extends SdkCallbackTool {
81
82
  signal.addEventListener("abort", onAbort, { once: true });
82
83
  }
83
84
  });
84
- return JSON.stringify(result);
85
+ // 输出保护:stdout + stderr 合计超过 maxToolOutput 时,分文件落盘并返回警告 + 头尾预览
86
+ const limit = this.env.config.maxToolOutput ?? DEFAULT_MAX_TOOL_OUTPUT;
87
+ const guarded = await guardOutput({
88
+ tool: "exec",
89
+ content: result,
90
+ isOverLimit: (output) => output.stdout.length + output.stderr.length > limit,
91
+ dump: (_dir, output) => [
92
+ { name: "stdout.log", content: output.stdout },
93
+ { name: "stderr.log", content: output.stderr },
94
+ ],
95
+ buildWarning: (ctx) => {
96
+ const stdout = headTailPreview(result.stdout);
97
+ const stderr = headTailPreview(result.stderr);
98
+ return JSON.stringify({
99
+ outputTooLarge: true,
100
+ warning: `输出过大:stdout + stderr 合计 ${result.stdout.length + result.stderr.length} 字符,` +
101
+ `超过上限 ${limit} 字符。完整内容已分文件落盘(stdout.log / stderr.log),可用 readFile 分批读取。`,
102
+ files: ctx.files,
103
+ stdout: { chars: result.stdout.length, lines: countLines(result.stdout), ...stdout },
104
+ stderr: { chars: result.stderr.length, lines: countLines(result.stderr), ...stderr },
105
+ exitCode: result.exitCode,
106
+ killed: result.killed,
107
+ terminated: result.terminated,
108
+ });
109
+ },
110
+ });
111
+ // 未超限:content 为原始结果对象,按原有结构序列化;超限:buildWarning 已生成返回字符串
112
+ return typeof guarded === "string" ? guarded : JSON.stringify(guarded);
85
113
  }
86
114
  }
115
+ /** 统计文本行数(空文本记 0 行) */
116
+ function countLines(text) {
117
+ return text.length === 0 ? 0 : text.split("\n").length;
118
+ }
@@ -14,4 +14,9 @@ export declare class FindTextTool extends SdkCallbackTool {
14
14
  maxFileSize?: number;
15
15
  maxLineLength?: number;
16
16
  }, ctx?: ToolCallContext): Promise<string>;
17
+ /**
18
+ * 输出保护:结果 JSON 超过 maxToolOutput 时落盘为 result.json,
19
+ * 返回警告 + 头尾预览;未超限则原样返回。
20
+ */
21
+ private protect;
17
22
  }
@@ -1,6 +1,7 @@
1
1
  import * as fsp from "fs/promises";
2
2
  import * as path from "path";
3
3
  import { SdkCallbackTool } from "../../../runtime/SdkCallbackTool.js";
4
+ import { DEFAULT_MAX_TOOL_OUTPUT, guardOutput, headPreview } from "./outputGuard.js";
4
5
  /** 默认总匹配数上限 */
5
6
  const DEFAULT_MAX_MATCHES = 200;
6
7
  /** 默认文件大小上限(字节,与 ReadFileTool 的 300KB 口径一致) */
@@ -126,17 +127,41 @@ export class FindTextTool extends SdkCallbackTool {
126
127
  }
127
128
  }
128
129
  if (signal?.aborted) {
129
- return JSON.stringify({ aborted: true, results: result });
130
+ return await this.protect(JSON.stringify({ aborted: true, results: result }));
130
131
  }
131
132
  if (truncated) {
132
- return JSON.stringify({ truncated: true, totalMatches, results: result });
133
+ return await this.protect(JSON.stringify({ truncated: true, totalMatches, results: result }));
133
134
  }
134
- return JSON.stringify(result);
135
+ return await this.protect(JSON.stringify(result));
135
136
  }
136
137
  catch (error) {
137
138
  return error?.message;
138
139
  }
139
140
  }
141
+ /**
142
+ * 输出保护:结果 JSON 超过 maxToolOutput 时落盘为 result.json,
143
+ * 返回警告 + 头尾预览;未超限则原样返回。
144
+ */
145
+ async protect(json) {
146
+ const limit = this.env.config.maxToolOutput ?? DEFAULT_MAX_TOOL_OUTPUT;
147
+ return await guardOutput({
148
+ tool: "findText",
149
+ content: json,
150
+ isOverLimit: (text) => text.length > limit,
151
+ dump: (_dir, text) => [{ name: "result.json", content: text }],
152
+ buildWarning: (ctx) => {
153
+ const preview = headPreview(json);
154
+ return JSON.stringify({
155
+ outputTooLarge: true,
156
+ warning: `输出过大:结果共 ${json.length} 字符,超过上限 ${limit} 字符。` +
157
+ `完整结果已落盘(result.json),可用 readFile 分批读取。`,
158
+ files: ctx.files,
159
+ chars: json.length,
160
+ ...preview,
161
+ });
162
+ },
163
+ });
164
+ }
140
165
  }
141
166
  /** 截断字符串到指定长度,超出部分以 "…" 结尾标记 */
142
167
  function truncate(s, max) {
@@ -9,4 +9,9 @@ export declare class GlobTool extends SdkCallbackTool {
9
9
  pattern: string;
10
10
  exclude?: string[];
11
11
  }, ctx?: ToolCallContext): Promise<string>;
12
+ /**
13
+ * 输出保护:结果 JSON 超过 maxToolOutput 时落盘为 result.json,
14
+ * 返回警告 + 头部预览;未超限则原样返回。
15
+ */
16
+ private protect;
12
17
  }
@@ -1,5 +1,6 @@
1
1
  import * as fsp from "fs/promises";
2
2
  import { SdkCallbackTool } from "../../../runtime/SdkCallbackTool.js";
3
+ import { DEFAULT_MAX_TOOL_OUTPUT, guardOutput, headPreview } from "./outputGuard.js";
3
4
  export class GlobTool extends SdkCallbackTool {
4
5
  function = {
5
6
  name: "glob",
@@ -44,12 +45,37 @@ export class GlobTool extends SdkCallbackTool {
44
45
  result.push(file);
45
46
  }
46
47
  if (signal?.aborted) {
47
- return JSON.stringify({ aborted: true, files: result });
48
+ return await this.protect(JSON.stringify({ aborted: true, files: result }));
48
49
  }
49
- return JSON.stringify(result);
50
+ return await this.protect(JSON.stringify(result));
50
51
  }
51
52
  catch (error) {
52
53
  return error?.message;
53
54
  }
54
55
  }
56
+ /**
57
+ * 输出保护:结果 JSON 超过 maxToolOutput 时落盘为 result.json,
58
+ * 返回警告 + 头部预览;未超限则原样返回。
59
+ */
60
+ async protect(json) {
61
+ const limit = this.env.config.maxToolOutput ?? DEFAULT_MAX_TOOL_OUTPUT;
62
+ return await guardOutput({
63
+ tool: "glob",
64
+ content: json,
65
+ isOverLimit: (text) => text.length > limit,
66
+ dump: (_dir, text) => [{ name: "result.json", content: text }],
67
+ buildWarning: (ctx) => {
68
+ const preview = headPreview(json);
69
+ return JSON.stringify({
70
+ outputTooLarge: true,
71
+ warning: `输出过大:匹配结果共 ${json.length} 字符,超过上限 ${limit} 字符。` +
72
+ `完整结果已落盘(result.json),可用 readFile 分批读取;` +
73
+ `也可用更精确的 pattern 或 exclude 缩小范围后重新查找。`,
74
+ files: ctx.files,
75
+ chars: json.length,
76
+ ...preview,
77
+ });
78
+ },
79
+ });
80
+ }
55
81
  }
@@ -0,0 +1,17 @@
1
+ import { SdkCallbackTool } from "../../../runtime/SdkCallbackTool.js";
2
+ import type { ToolEnv } from "../../../types/index.js";
3
+ import type { AgentNS } from "@ai-zen/agents-core";
4
+ /**
5
+ * 勘察文件结构概况(行数、每行列数等),不做内容读取。
6
+ *
7
+ * 定位与 readFile 的 range 配套:大文件先看结构(有多少行、每行多长),
8
+ * 再决定 readFile 的读取范围。流式扫描,故不受 readFile 的 300KB 读取阈值限制。
9
+ */
10
+ export declare class InspectFileTool extends SdkCallbackTool {
11
+ function: AgentNS.FunctionDefine;
12
+ constructor(env: ToolEnv);
13
+ call(input: {
14
+ path: string;
15
+ withColCountMap?: boolean;
16
+ }): Promise<string>;
17
+ }
@@ -0,0 +1,150 @@
1
+ import * as fsp from "fs/promises";
2
+ import { createReadStream } from "fs";
3
+ import { StringDecoder } from "string_decoder";
4
+ import { SdkCallbackTool } from "../../../runtime/SdkCallbackTool.js";
5
+ import { DEFAULT_MAX_TOOL_OUTPUT } from "./outputGuard.js";
6
+ /**
7
+ * 勘察文件结构概况(行数、每行列数等),不做内容读取。
8
+ *
9
+ * 定位与 readFile 的 range 配套:大文件先看结构(有多少行、每行多长),
10
+ * 再决定 readFile 的读取范围。流式扫描,故不受 readFile 的 300KB 读取阈值限制。
11
+ */
12
+ export class InspectFileTool extends SdkCallbackTool {
13
+ function = {
14
+ name: "inspectFile",
15
+ description: "勘察文件的结构概况:总行数、总字符数、最长行的列数及其行索引、平均列数、行尾类型(LF/CRLF)。可选返回每行的列数映射。适用于在 readFile 之前先了解文件规模,再决定用 range 读取哪一段。仅做流式扫描,不受 readFile 的 300KB 读取阈值限制。",
16
+ parameters: {
17
+ type: "object",
18
+ properties: {
19
+ path: {
20
+ type: "string",
21
+ description: "文件路径",
22
+ },
23
+ withColCountMap: {
24
+ type: "boolean",
25
+ description: "是否返回每行列数映射(对象:行索引 → 列数,行索引从 0 开始)。默认 false。行数很多时该映射可能过大,此时将只返回概况字段。",
26
+ default: false,
27
+ },
28
+ },
29
+ required: ["path"],
30
+ additionalProperties: false,
31
+ },
32
+ };
33
+ constructor(env) {
34
+ super({ env });
35
+ }
36
+ async call(input) {
37
+ try {
38
+ const filePath = this.resolve(input.path);
39
+ const stats = await fsp.stat(filePath);
40
+ if (!stats.isFile()) {
41
+ return `"${filePath}" 不是文件`;
42
+ }
43
+ const withColCountMap = input.withColCountMap ?? false;
44
+ const result = await scan(filePath, withColCountMap);
45
+ const summary = {
46
+ path: filePath,
47
+ bytes: stats.size,
48
+ lines: result.lines,
49
+ chars: result.chars,
50
+ maxCol: result.maxCol,
51
+ lineIndexOfMaxCol: result.lineIndexOfMaxCol,
52
+ avgCol: result.lines === 0 ? 0 : Math.round(result.chars / result.lines),
53
+ lineEnding: result.lineEnding,
54
+ };
55
+ if (!withColCountMap)
56
+ return JSON.stringify(summary);
57
+ // 明细超限时只返回概况 + 提示(不落盘):文件规模已超出可处理范围
58
+ const limit = this.env.config.maxToolOutput ?? DEFAULT_MAX_TOOL_OUTPUT;
59
+ const payload = JSON.stringify({ ...summary, colCountMap: result.colCountMap });
60
+ if (payload.length > limit) {
61
+ return JSON.stringify({
62
+ ...summary,
63
+ colCountMapOmitted: true,
64
+ warning: `行长度明细过大:概况与 colCountMap 合计 ${payload.length} 字符,` +
65
+ `超过上限 ${limit} 字符,未落盘返回。文件规模超出可处理范围,` +
66
+ `建议改用 findText 定位后再按 range 分段读取。`,
67
+ });
68
+ }
69
+ return payload;
70
+ }
71
+ catch (error) {
72
+ return error?.message;
73
+ }
74
+ }
75
+ }
76
+ /**
77
+ * 流式扫描文件,统计行数、字符数与每行列数。
78
+ *
79
+ * 口径:行索引 0-based;列数按字符数(UTF-16 长度)计、不含行尾换行符(CRLF 的 \r 不计)。
80
+ */
81
+ async function scan(filePath, withColCountMap) {
82
+ const decoder = new StringDecoder("utf8");
83
+ const colCountMap = {};
84
+ let lines = 0;
85
+ let chars = 0;
86
+ let col = 0;
87
+ let maxCol = 0;
88
+ let lineIndexOfMaxCol = 0;
89
+ let sawLf = false;
90
+ let sawCrlf = false;
91
+ let lastWasNewline = false;
92
+ let sawAnyChar = false;
93
+ /** 上一个字符是 \r,等待判定它是否属于 CRLF */
94
+ let pendingCr = false;
95
+ const endLine = () => {
96
+ if (withColCountMap)
97
+ colCountMap[String(lines)] = col;
98
+ if (col > maxCol) {
99
+ maxCol = col;
100
+ lineIndexOfMaxCol = lines;
101
+ }
102
+ chars += col;
103
+ lines++;
104
+ col = 0;
105
+ lastWasNewline = true;
106
+ };
107
+ const processText = (text) => {
108
+ // 按 code unit 遍历,与 String.length 口径一致
109
+ for (let index = 0; index < text.length; index++) {
110
+ const char = text[index];
111
+ sawAnyChar = true;
112
+ if (char === "\n") {
113
+ if (pendingCr) {
114
+ sawCrlf = true;
115
+ pendingCr = false;
116
+ }
117
+ else {
118
+ sawLf = true;
119
+ }
120
+ endLine();
121
+ continue;
122
+ }
123
+ // 前一个字符是 \r 且其后不是 \n:属孤立的 \r,作为行内字符计入
124
+ if (pendingCr) {
125
+ col++;
126
+ pendingCr = false;
127
+ }
128
+ if (char === "\r") {
129
+ pendingCr = true;
130
+ lastWasNewline = false;
131
+ continue;
132
+ }
133
+ col++;
134
+ lastWasNewline = false;
135
+ }
136
+ };
137
+ const stream = createReadStream(filePath);
138
+ for await (const chunk of stream) {
139
+ processText(decoder.write(chunk));
140
+ }
141
+ processText(decoder.end());
142
+ // 文件末尾残留的孤立 \r
143
+ if (pendingCr)
144
+ col++;
145
+ // 末行未以换行结束(空文件不计行)
146
+ if (sawAnyChar && !lastWasNewline)
147
+ endLine();
148
+ const lineEnding = sawCrlf && sawLf ? "mixed" : sawCrlf ? "CRLF" : "LF";
149
+ return { lines, chars, maxCol, lineIndexOfMaxCol, lineEnding, colCountMap };
150
+ }
@@ -1,5 +1,6 @@
1
1
  import * as fsp from "fs/promises";
2
2
  import { SdkCallbackTool } from "../../../runtime/SdkCallbackTool.js";
3
+ import { DEFAULT_MAX_TOOL_OUTPUT, guardOutput, headPreview } from "./outputGuard.js";
3
4
  export class LsTool extends SdkCallbackTool {
4
5
  function = {
5
6
  name: "ls",
@@ -21,8 +22,28 @@ export class LsTool extends SdkCallbackTool {
21
22
  }
22
23
  async call(input) {
23
24
  try {
24
- const result = await fsp.readdir(this.resolve(input.path));
25
- return JSON.stringify(result);
25
+ const entries = await fsp.readdir(this.resolve(input.path));
26
+ const json = JSON.stringify(entries);
27
+ // 输出保护:结果超过 maxToolOutput 时落盘为 result.json,返回警告 + 头部预览
28
+ const limit = this.env.config.maxToolOutput ?? DEFAULT_MAX_TOOL_OUTPUT;
29
+ return await guardOutput({
30
+ tool: "ls",
31
+ content: json,
32
+ isOverLimit: (text) => text.length > limit,
33
+ dump: (_dir, text) => [{ name: "result.json", content: text }],
34
+ buildWarning: (ctx) => {
35
+ const preview = headPreview(json);
36
+ return JSON.stringify({
37
+ outputTooLarge: true,
38
+ warning: `输出过大:目录条目共 ${entries.length} 项、${json.length} 字符,超过上限 ${limit} 字符。` +
39
+ `完整列表已落盘(result.json),可用 readFile 分批读取;` +
40
+ `也可改用 glob 按模式筛选。`,
41
+ files: ctx.files,
42
+ chars: json.length,
43
+ ...preview,
44
+ });
45
+ },
46
+ });
26
47
  }
27
48
  catch (error) {
28
49
  return error?.message;
@@ -6,5 +6,6 @@ export declare class ReadFileTool extends SdkCallbackTool {
6
6
  constructor(env: ToolEnv);
7
7
  call(input: {
8
8
  path: string;
9
+ range?: number[];
9
10
  }): Promise<string>;
10
11
  }
@@ -1,5 +1,6 @@
1
1
  import * as fsp from "fs/promises";
2
2
  import { SdkCallbackTool } from "../../../runtime/SdkCallbackTool.js";
3
+ import { DEFAULT_MAX_TOOL_OUTPUT, guardOutput } from "./outputGuard.js";
3
4
  export class ReadFileTool extends SdkCallbackTool {
4
5
  function = {
5
6
  name: "readFile",
@@ -11,6 +12,14 @@ export class ReadFileTool extends SdkCallbackTool {
11
12
  type: "string",
12
13
  description: "文件路径",
13
14
  },
15
+ range: {
16
+ type: "array",
17
+ description: "读取范围 [起始行, 起始列, 结束行, 结束列],行列均从 0 开始,-1 表示末位;" +
18
+ "例如 [0, 0, 100, -1] 表示从第 0 行第 0 列读到第 100 行最后一列。省略则读取整个文件。",
19
+ items: {
20
+ type: "number",
21
+ },
22
+ },
14
23
  },
15
24
  required: ["path"],
16
25
  additionalProperties: false,
@@ -23,13 +32,63 @@ export class ReadFileTool extends SdkCallbackTool {
23
32
  try {
24
33
  const filePath = this.resolve(input.path);
25
34
  const stats = await fsp.stat(filePath);
35
+ // 读取阈值:文件过大时拒绝读取(与输出阈值 maxToolOutput 无关)
26
36
  if (stats.size > 300 * 1024) {
27
37
  throw new Error(`文件过大,无法读取,当前文件大小 ${stats.size} 字节`);
28
38
  }
29
- return await fsp.readFile(filePath, "utf-8");
39
+ const content = await fsp.readFile(filePath, "utf-8");
40
+ const output = input.range ? sliceByRange(content, input.range) : content;
41
+ // 输出保护:本次返回内容超过上限时只警告、不落盘,并提示用 range 分批读取
42
+ const limit = this.env.config.maxToolOutput ?? DEFAULT_MAX_TOOL_OUTPUT;
43
+ return await guardOutput({
44
+ tool: "readFile",
45
+ content: output,
46
+ isOverLimit: (text) => text.length > limit,
47
+ buildWarning: () => `读取内容过大:本次输出 ${output.length} 字符,超过上限 ${limit} 字符,未落盘。` +
48
+ `文件共 ${countLines(content)} 行、${content.length} 字符,` +
49
+ `请用 range 参数分范围读取(形如 [起始行, 起始列, 结束行, 结束列],行列从 0 开始,-1 表示末位)。`,
50
+ });
30
51
  }
31
52
  catch (error) {
32
53
  return error?.message;
33
54
  }
34
55
  }
35
56
  }
57
+ /**
58
+ * 按 [起始行, 起始列, 结束行, 结束列] 切片(0-based,-1 表末位)。
59
+ *
60
+ * - 起始行至结束行逐行取出;起始行从起始列起截取,结束行截取到结束列(含);
61
+ * - 结束列为 -1 时取到行末,故 [0, 0, 100, -1] 即"第 0 行第 0 列到第 100 行末";
62
+ * - 越界索引自动夹取到有效范围。
63
+ */
64
+ function sliceByRange(content, range) {
65
+ const lines = content.split("\n");
66
+ const [rawStartLine = 0, rawStartCol = 0, rawEndLine = -1, rawEndCol = -1] = range;
67
+ const startLine = resolveIndex(rawStartLine, lines.length);
68
+ const endLine = resolveIndex(rawEndLine, lines.length);
69
+ if (endLine < startLine)
70
+ return "";
71
+ const selected = [];
72
+ for (let index = startLine; index <= endLine; index++) {
73
+ const line = lines[index] ?? "";
74
+ const from = index === startLine ? resolveIndex(rawStartCol, line.length) : 0;
75
+ const to = index === endLine ? resolveIndex(rawEndCol, line.length) : line.length - 1;
76
+ selected.push(line.slice(from, to + 1));
77
+ }
78
+ return selected.join("\n");
79
+ }
80
+ /** 解析行列索引:-1 表末位,其余负值自末尾倒数,越界夹取到有效范围 */
81
+ function resolveIndex(index, length) {
82
+ if (!Number.isFinite(index) || length === 0)
83
+ return 0;
84
+ const normalized = index < 0 ? length + index : index;
85
+ if (normalized < 0)
86
+ return 0;
87
+ if (normalized > length - 1)
88
+ return length - 1;
89
+ return normalized;
90
+ }
91
+ /** 统计文本行数(空文本记 0 行) */
92
+ function countLines(text) {
93
+ return text.length === 0 ? 0 : text.split("\n").length;
94
+ }
@@ -2,6 +2,7 @@ import type { SdkCallbackTool } from "../../../runtime/SdkCallbackTool.js";
2
2
  import type { ToolEnv } from "../../../types/index.js";
3
3
  import { CwdTool } from "./CwdTool.js";
4
4
  import { ReadFileTool } from "./ReadFileTool.js";
5
+ import { InspectFileTool } from "./InspectFileTool.js";
5
6
  import { WriteFileTool } from "./WriteFileTool.js";
6
7
  import { ExecTool } from "./ExecTool.js";
7
8
  import { MkdirTool } from "./MkdirTool.js";
@@ -20,7 +21,7 @@ import { SleepTool } from "./SleepTool.js";
20
21
  import { ViewImageTool } from "./ViewImageTool.js";
21
22
  import { GenerateImageTool } from "./GenerateImageTool.js";
22
23
  /**
23
- * 全部内置工具类(19 个)。
24
+ * 全部内置工具类(20 个)。
24
25
  * 工具类由 Provider 用 ToolEnv 实例化(每个 Provider 一套实例,注入其 cwd)。
25
26
  *
26
27
  * 发现层不做任何过滤——工具的可用性由各工具自行声明(SdkCallbackTool.isAvailable,
@@ -28,4 +29,4 @@ import { GenerateImageTool } from "./GenerateImageTool.js";
28
29
  * 如 GenerateImageTool 依赖 defaultImageModel、ViewImageTool 仅视觉模型可用。
29
30
  */
30
31
  export declare const BUILTIN_TOOL_CLASSES: Array<new (env: ToolEnv) => SdkCallbackTool>;
31
- export { CwdTool, ReadFileTool, WriteFileTool, ExecTool, MkdirTool, RmTool, GlobTool, LsTool, ExistTool, FindTextTool, DownloadFileTool, RenameTool, CopyTool, BatchEditTool, EditTool, ExecAsyncTool, SleepTool, ViewImageTool, GenerateImageTool, };
32
+ export { CwdTool, ReadFileTool, InspectFileTool, WriteFileTool, ExecTool, MkdirTool, RmTool, GlobTool, LsTool, ExistTool, FindTextTool, DownloadFileTool, RenameTool, CopyTool, BatchEditTool, EditTool, ExecAsyncTool, SleepTool, ViewImageTool, GenerateImageTool, };
@@ -1,5 +1,6 @@
1
1
  import { CwdTool } from "./CwdTool.js";
2
2
  import { ReadFileTool } from "./ReadFileTool.js";
3
+ import { InspectFileTool } from "./InspectFileTool.js";
3
4
  import { WriteFileTool } from "./WriteFileTool.js";
4
5
  import { ExecTool } from "./ExecTool.js";
5
6
  import { MkdirTool } from "./MkdirTool.js";
@@ -18,7 +19,7 @@ import { SleepTool } from "./SleepTool.js";
18
19
  import { ViewImageTool } from "./ViewImageTool.js";
19
20
  import { GenerateImageTool } from "./GenerateImageTool.js";
20
21
  /**
21
- * 全部内置工具类(19 个)。
22
+ * 全部内置工具类(20 个)。
22
23
  * 工具类由 Provider 用 ToolEnv 实例化(每个 Provider 一套实例,注入其 cwd)。
23
24
  *
24
25
  * 发现层不做任何过滤——工具的可用性由各工具自行声明(SdkCallbackTool.isAvailable,
@@ -28,6 +29,7 @@ import { GenerateImageTool } from "./GenerateImageTool.js";
28
29
  export const BUILTIN_TOOL_CLASSES = [
29
30
  CwdTool,
30
31
  ReadFileTool,
32
+ InspectFileTool,
31
33
  WriteFileTool,
32
34
  ExecTool,
33
35
  MkdirTool,
@@ -46,4 +48,4 @@ export const BUILTIN_TOOL_CLASSES = [
46
48
  ViewImageTool,
47
49
  GenerateImageTool,
48
50
  ];
49
- export { CwdTool, ReadFileTool, WriteFileTool, ExecTool, MkdirTool, RmTool, GlobTool, LsTool, ExistTool, FindTextTool, DownloadFileTool, RenameTool, CopyTool, BatchEditTool, EditTool, ExecAsyncTool, SleepTool, ViewImageTool, GenerateImageTool, };
51
+ export { CwdTool, ReadFileTool, InspectFileTool, WriteFileTool, ExecTool, MkdirTool, RmTool, GlobTool, LsTool, ExistTool, FindTextTool, DownloadFileTool, RenameTool, CopyTool, BatchEditTool, EditTool, ExecAsyncTool, SleepTool, ViewImageTool, GenerateImageTool, };
@@ -0,0 +1,81 @@
1
+ /**
2
+ * 工具输出上限的缺省值(字符数)。
3
+ *
4
+ * 可由 AppConfig.maxToolOutput 覆盖;上限由各工具自行读取并判定,
5
+ * 骨架不感知配置,以便不同工具按自身口径(如 stdout/stderr 合计)判断。
6
+ */
7
+ export declare const DEFAULT_MAX_TOOL_OUTPUT = 32768;
8
+ /** 待落盘的文件:文件名(不含目录)+ 内容 */
9
+ export interface GuardDumpFile {
10
+ /** 文件名,如 "stdout.log" */
11
+ name: string;
12
+ /** 文件内容 */
13
+ content: string;
14
+ }
15
+ /** buildWarning 回调收到的落盘结果 */
16
+ export interface GuardWarningContext {
17
+ /** 本次落盘目录;未落盘(工具未提供 dump)时为 null */
18
+ dir: string | null;
19
+ /** 已落盘文件:文件名 → 绝对路径;未落盘时为空对象 */
20
+ files: Record<string, string>;
21
+ }
22
+ /** guardOutput 入参:骨架所需的一切(判定、落盘、警告)均由工具以回调提供 */
23
+ export interface OutputGuardOptions<T> {
24
+ /** 工具名,用于落盘目录命名(如 "exec") */
25
+ tool: string;
26
+ /** 工具本应返回的完整内容 */
27
+ content: T;
28
+ /** 超限判定:上限由工具自行从 config 读取,骨架不传参 */
29
+ isOverLimit: (content: T) => boolean;
30
+ /**
31
+ * 落盘内容组织(可选)。缺省表示"只警告、不落盘"(如 readFile)。
32
+ * 返回的文件按顺序写入本次调用专属的目录。
33
+ */
34
+ dump?: (dir: string, content: T) => GuardDumpFile[] | Promise<GuardDumpFile[]>;
35
+ /** 生成超限时的返回体(各工具自定义,可含头尾预览、统计、原有字段等) */
36
+ buildWarning: (ctx: GuardWarningContext) => string;
37
+ }
38
+ /**
39
+ * 标准输出保护骨架。
40
+ *
41
+ * 各工具在 call() 出口调用,只需提供三个回调:
42
+ * 1) isOverLimit —— 是否超过上限(工具自读 config 判定,未被超限即原样返回 content);
43
+ * 2) dump —— 超限时落盘哪些文件(可省略 = 只警告不落盘);
44
+ * 3) buildWarning —— 超限时返回给模型的内容。
45
+ *
46
+ * 骨架职责:判定 → 未超限原样返回 / 超限时创建独立目录并写文件 → 交给 buildWarning。
47
+ *
48
+ * 落盘目录:<os.tmpdir()>/ai-zen/tool-output/<tool>-<时间戳>-<随机串>/,
49
+ * 每次调用独立目录,互不覆盖;不做自清理,依赖系统对临时目录的清理策略。
50
+ *
51
+ * 落盘失败(磁盘满、权限不足等)不吞异常,直接向上抛出。
52
+ */
53
+ export declare function guardOutput<T>(options: OutputGuardOptions<T>): Promise<T | string>;
54
+ /** 头部预览 */
55
+ export interface HeadPreview {
56
+ /** 头部内容(最多 size 字符) */
57
+ head: string;
58
+ /** 被省略的字符数;为 0 表示无省略(此时 head 为全文) */
59
+ omitted: number;
60
+ }
61
+ /**
62
+ * 生成头部预览:最多 size 字符。
63
+ *
64
+ * 适用于结果按出现顺序排列、尾部参考价值低的工具(如 findText)。
65
+ */
66
+ export declare function headPreview(text: string, size?: number): HeadPreview;
67
+ /** 头尾预览 */
68
+ export interface HeadTailPreview {
69
+ /** 头部内容(最多 size 字符) */
70
+ head: string;
71
+ /** 尾部内容(最多 size 字符);无省略时为空串 */
72
+ tail: string;
73
+ /** 被省略的字符数;为 0 表示无省略(此时 head 为全文) */
74
+ omitted: number;
75
+ }
76
+ /**
77
+ * 生成头尾预览:头部最多 size 字符 + 尾部最多 size 字符。
78
+ *
79
+ * 供各工具的 buildWarning 按需取用(也可自行组织预览,骨架不做限制)。
80
+ */
81
+ export declare function headTailPreview(text: string, size?: number): HeadTailPreview;
@@ -0,0 +1,73 @@
1
+ import * as fsp from "fs/promises";
2
+ import * as os from "os";
3
+ import * as path from "path";
4
+ /**
5
+ * 工具输出上限的缺省值(字符数)。
6
+ *
7
+ * 可由 AppConfig.maxToolOutput 覆盖;上限由各工具自行读取并判定,
8
+ * 骨架不感知配置,以便不同工具按自身口径(如 stdout/stderr 合计)判断。
9
+ */
10
+ export const DEFAULT_MAX_TOOL_OUTPUT = 32_768;
11
+ /**
12
+ * 标准输出保护骨架。
13
+ *
14
+ * 各工具在 call() 出口调用,只需提供三个回调:
15
+ * 1) isOverLimit —— 是否超过上限(工具自读 config 判定,未被超限即原样返回 content);
16
+ * 2) dump —— 超限时落盘哪些文件(可省略 = 只警告不落盘);
17
+ * 3) buildWarning —— 超限时返回给模型的内容。
18
+ *
19
+ * 骨架职责:判定 → 未超限原样返回 / 超限时创建独立目录并写文件 → 交给 buildWarning。
20
+ *
21
+ * 落盘目录:<os.tmpdir()>/ai-zen/tool-output/<tool>-<时间戳>-<随机串>/,
22
+ * 每次调用独立目录,互不覆盖;不做自清理,依赖系统对临时目录的清理策略。
23
+ *
24
+ * 落盘失败(磁盘满、权限不足等)不吞异常,直接向上抛出。
25
+ */
26
+ export async function guardOutput(options) {
27
+ const { tool, content, isOverLimit, dump, buildWarning } = options;
28
+ if (!isOverLimit(content))
29
+ return content;
30
+ if (!dump)
31
+ return buildWarning({ dir: null, files: {} });
32
+ const dir = await createDumpDir(tool);
33
+ const files = await dump(dir, content);
34
+ const paths = {};
35
+ for (const file of files) {
36
+ const target = path.join(dir, file.name);
37
+ await fsp.writeFile(target, file.content, "utf-8");
38
+ paths[file.name] = target;
39
+ }
40
+ return buildWarning({ dir, files: paths });
41
+ }
42
+ /**
43
+ * 生成头部预览:最多 size 字符。
44
+ *
45
+ * 适用于结果按出现顺序排列、尾部参考价值低的工具(如 findText)。
46
+ */
47
+ export function headPreview(text, size = 1000) {
48
+ if (text.length <= size)
49
+ return { head: text, omitted: 0 };
50
+ return { head: text.slice(0, size), omitted: text.length - size };
51
+ }
52
+ /**
53
+ * 生成头尾预览:头部最多 size 字符 + 尾部最多 size 字符。
54
+ *
55
+ * 供各工具的 buildWarning 按需取用(也可自行组织预览,骨架不做限制)。
56
+ */
57
+ export function headTailPreview(text, size = 1000) {
58
+ if (text.length <= size * 2)
59
+ return { head: text, tail: "", omitted: 0 };
60
+ return {
61
+ head: text.slice(0, size),
62
+ tail: text.slice(-size),
63
+ omitted: text.length - size * 2,
64
+ };
65
+ }
66
+ /** 创建本次调用专属的落盘目录 */
67
+ async function createDumpDir(tool) {
68
+ const stamp = new Date().toISOString().replace(/[:.]/g, "-");
69
+ const rand = Math.random().toString(36).slice(2, 8);
70
+ const dir = path.join(os.tmpdir(), "ai-zen", "tool-output", `${tool}-${stamp}-${rand}`);
71
+ await fsp.mkdir(dir, { recursive: true });
72
+ return dir;
73
+ }
@@ -1,6 +1,5 @@
1
- import { CallbackTool } from "@ai-zen/agents-core";
1
+ import { CallbackTool, AgentToolLazy } from "@ai-zen/agents-core";
2
2
  import type { SkillInfo } from "../discovery/skills.js";
3
- import type { Provider } from "../../runtime/Provider.js";
4
3
  /**
5
4
  * 创建 load_skill 工具。
6
5
  * 枚举所有允许的 skill(不限是否支持子 Agent),由 filteredSkills 提供完整信息。
@@ -10,6 +9,9 @@ export declare function createLoadSkillTool(skillDirs: string[], filteredSkills:
10
9
  * 创建 call_skill_sub_agent 工具。
11
10
  * 只展示 subAgent: true 的 Skill,枚举中自动排除不支持子 Agent 模式的 skill。
12
11
  *
13
- * 回调中创建独立的 Skill 子 Agent,通过 caps 按父 Agent 权限独立解析工具集。
12
+ * 工具由 AgentToolLazy 承载:子 Agent 在 exec 时构建,从而复用统一的委派边界
13
+ * (onSubAgentStart / onSubAgentEnd 钩子与 sub-agent-start / sub-agent-end 事件)。
14
+ * 子 Agent 沿用父 Agent 的工具能力(不做权限过滤),但剔除 call_skill_sub_agent 自身,
15
+ * 避免 skill 链式自递归。
14
16
  */
15
- export declare function createCallSkillSubAgentTool(skillDirs: string[], filteredSkills: SkillInfo[], provider?: Provider): CallbackTool;
17
+ export declare function createCallSkillSubAgentTool(skillDirs: string[], filteredSkills: SkillInfo[]): AgentToolLazy;
@@ -1,5 +1,4 @@
1
- import { CallbackTool, Agent, Message } from "@ai-zen/agents-core";
2
- import { SdkAgent } from "../../runtime/SdkAgent.js";
1
+ import { CallbackTool, Agent, AgentToolLazy, Message } from "@ai-zen/agents-core";
3
2
  import { createDisclosureParam } from "../disclosure.js";
4
3
  import { readSkill } from "../discovery/skills.js";
5
4
  import { getLogger } from "../../shared/logger.js";
@@ -70,9 +69,12 @@ export function createLoadSkillTool(skillDirs, filteredSkills) {
70
69
  * 创建 call_skill_sub_agent 工具。
71
70
  * 只展示 subAgent: true 的 Skill,枚举中自动排除不支持子 Agent 模式的 skill。
72
71
  *
73
- * 回调中创建独立的 Skill 子 Agent,通过 caps 按父 Agent 权限独立解析工具集。
72
+ * 工具由 AgentToolLazy 承载:子 Agent 在 exec 时构建,从而复用统一的委派边界
73
+ * (onSubAgentStart / onSubAgentEnd 钩子与 sub-agent-start / sub-agent-end 事件)。
74
+ * 子 Agent 沿用父 Agent 的工具能力(不做权限过滤),但剔除 call_skill_sub_agent 自身,
75
+ * 避免 skill 链式自递归。
74
76
  */
75
- export function createCallSkillSubAgentTool(skillDirs, filteredSkills, provider) {
77
+ export function createCallSkillSubAgentTool(skillDirs, filteredSkills) {
76
78
  // 只保留支持子 Agent 模式的 skill
77
79
  const subAgentSkills = filteredSkills.filter((s) => s.subAgent);
78
80
  const ids = subAgentSkills.map((s) => s.id);
@@ -82,7 +84,7 @@ export function createCallSkillSubAgentTool(skillDirs, filteredSkills, provider)
82
84
  .map((s) => ` - ${s.id}: ${s.description || "无描述"}`)
83
85
  .join("\n");
84
86
  const skillIdDescription = `${param.description}\n\n各 Skill 说明:\n${skillDescriptions || " 无可用 Skill"}`;
85
- return new CallbackTool({
87
+ return new AgentToolLazy({
86
88
  function: {
87
89
  name: "call_skill_sub_agent",
88
90
  description: "将任务委派给指定的 Skill 子 Agent,由其独立完成并返回结果。",
@@ -103,46 +105,28 @@ export function createCallSkillSubAgentTool(skillDirs, filteredSkills, provider)
103
105
  additionalProperties: false,
104
106
  },
105
107
  },
106
- async callback(input, ctx) {
107
- const skillId = input.skill_id;
108
- const task = input.task;
108
+ // 初始消息依赖运行时参数(skill_id → SKILL.md 内容),故不传 messages 模板,
109
+ // 由 buildAgent 读取 SKILL.md 后自行决定;abort 联动与委派边界由 AgentToolLazy 统一处理
110
+ buildAgent: async (parsedArgs, ctx) => {
111
+ const skillId = parsedArgs.skill_id;
112
+ const task = parsedArgs.task;
109
113
  const skill = await readSkill(skillDirs, skillId);
110
114
  if (!skill) {
111
- return `❌ Skill "${skillId}" 不存在,请确认名称是否正确`;
115
+ throw new Error(`❌ Skill "${skillId}" 不存在,请确认名称是否正确`);
112
116
  }
113
117
  if (!skill.subAgent) {
114
- return `Skill "${skillId}" 不支持子 Agent 模式,请使用 load_skill 加载指导后自行处理`;
118
+ throw new Error(`Skill "${skillId}" 不支持子 Agent 模式,请使用 load_skill 加载指导后自行处理`);
115
119
  }
116
- // 临时 Skill 子 Agent 复用父 Agent 定义(权限 + 模型),排除当前 Skill 防自调用
120
+ // 沿用父 Agent 的工具能力(不做权限过滤),剔除自身以防自递归
117
121
  const parentAgent = ctx.agent;
118
- const skillTools = provider && parentAgent instanceof SdkAgent
119
- ? provider.buildTools(parentAgent.definition, {
120
- exclude: { skills: [skillId] },
121
- })
122
- : [];
123
- const subAgent = new Agent({
122
+ const tools = parentAgent.tools.filter((t) => t.function.name !== "call_skill_sub_agent");
123
+ return new Agent({
124
124
  client: parentAgent.client,
125
125
  model: parentAgent.model,
126
126
  modelConfig: parentAgent.modelConfig,
127
- messages: [
128
- Message.System(skill.content),
129
- Message.User(task),
130
- ],
131
- tools: skillTools,
127
+ messages: [Message.System(skill.content), Message.User(task)],
128
+ tools,
132
129
  });
133
- // 子 Agent 中断联动:外层 abort → subAgent.abort()
134
- const onAbort = () => subAgent.abort();
135
- if (ctx.signal) {
136
- ctx.signal.addEventListener("abort", onAbort, { once: true });
137
- }
138
- try {
139
- await subAgent.run();
140
- }
141
- finally {
142
- ctx.signal?.removeEventListener("abort", onAbort);
143
- }
144
- const lastMsg = subAgent.messages.at(-1);
145
- return typeof lastMsg?.content === "string" ? lastMsg.content : "";
146
130
  },
147
131
  });
148
132
  }
@@ -23,7 +23,9 @@ export declare class ConfigManager {
23
23
  /**
24
24
  * 确保 basePath/agents/default.json 存在。
25
25
  *
26
- * - default.json 已存在 → 返回已有定义,不覆盖
26
+ * - default.json 已存在且 custom === true → 用户声明为自定义,原样返回,不做任何改动
27
+ * - default.json 已存在且 custom !== true → 仅同步出厂提示词(messages),
28
+ * 其余字段(name/permissions/modelId 等)保留;提示词一致时不写盘
27
29
  * - agents/ 为空 → 写入默认 Agent
28
30
  * - 已有其他 Agent → 返回 null
29
31
  */
@@ -69,7 +69,9 @@ export class ConfigManager {
69
69
  /**
70
70
  * 确保 basePath/agents/default.json 存在。
71
71
  *
72
- * - default.json 已存在 → 返回已有定义,不覆盖
72
+ * - default.json 已存在且 custom === true → 用户声明为自定义,原样返回,不做任何改动
73
+ * - default.json 已存在且 custom !== true → 仅同步出厂提示词(messages),
74
+ * 其余字段(name/permissions/modelId 等)保留;提示词一致时不写盘
73
75
  * - agents/ 为空 → 写入默认 Agent
74
76
  * - 已有其他 Agent → 返回 null
75
77
  */
@@ -77,11 +79,24 @@ export class ConfigManager {
77
79
  const agentsDir = join(this.basePath, "agents");
78
80
  const defaultPath = join(agentsDir, `${DEFAULT_AGENT_ID}.json`);
79
81
  try {
80
- await fs.access(defaultPath);
81
- return JSON.parse(await fs.readFile(defaultPath, "utf-8"));
82
+ const existing = JSON.parse(await fs.readFile(defaultPath, "utf-8"));
83
+ // 用户声明为自定义 → 完全跳过替换
84
+ if (existing.custom === true)
85
+ return existing;
86
+ // 仅同步提示词;内容一致则不写盘、不动时间戳
87
+ const messages = DEFAULT_AGENT_DEFINITION.messages;
88
+ if (sameMessages(existing.messages ?? [], messages))
89
+ return existing;
90
+ const updated = {
91
+ ...existing,
92
+ messages,
93
+ updatedAt: new Date().toISOString(),
94
+ };
95
+ await fs.writeFile(defaultPath, JSON.stringify(updated, null, 2), "utf-8");
96
+ return updated;
82
97
  }
83
98
  catch {
84
- // default.json 不存在,继续
99
+ // 文件不存在或不可解析,继续走初始化流程
85
100
  }
86
101
  await fs.mkdir(agentsDir, { recursive: true });
87
102
  let existing;
@@ -219,4 +234,16 @@ export class ConfigManager {
219
234
  await fs.rename(tmpPath, path);
220
235
  }
221
236
  }
237
+ /**
238
+ * 比较两段消息列表的提示词内容是否一致。
239
+ *
240
+ * 只比较 role 与 content,忽略 id 等每次生成都可能变化的字段,
241
+ * 避免因 id 差异导致无意义的重复写盘与时间戳漂移。
242
+ */
243
+ function sameMessages(a, b) {
244
+ if (a.length !== b.length)
245
+ return false;
246
+ return a.every((message, index) => message.role === b[index].role &&
247
+ JSON.stringify(message.content) === JSON.stringify(b[index].content));
248
+ }
222
249
  export { DEFAULT_AGENT_ID, DEFAULT_AGENT_DEFINITION, DEFAULT_SUBAGENT_ID, DEFAULT_SUBAGENT_DEFINITION, DEFAULT_APP_CONFIG, DEFAULT_MCP_CONFIG, CONFIG_SUB_DIRS, } from "./constants.js";
@@ -12,8 +12,11 @@ export const DEFAULT_AGENT_DEFINITION = {
12
12
  messages: [
13
13
  Message.System(`你是一个幽默风趣、严谨可靠的智能助手,请用中文回复。当前操作系统:${process.platform}。
14
14
 
15
- 惜字如金:只交代结果和必要说明,执行过程(工具调用、读写、搜索等)用户均已可见,不必再复述。遇到任何不明确、不确定、有矛盾或可能影响结果的状况,立即停下并直接询问用户,而不是自行脑补、猜测或硬干。除此之外,主动调用工具、如实汇报,并只在你被指定的工作范围内行事。`),
15
+ 一、需要用户决策时,一次只问一个问题。
16
+ 二、当前对话基于 Node.js 驱动,你可以编写 Node.js 脚本执行复杂任务,并通过 exec 工具运行。`),
16
17
  ],
18
+ // 出厂默认 Agent:custom 为 false,SDK 初始化时会同步出厂提示词(用户设为 true 则跳过)
19
+ custom: false,
17
20
  permissions: {
18
21
  tools: { allow: ["*"] },
19
22
  skills: { allow: ["*"] },
@@ -177,6 +180,8 @@ export const DEFAULT_APP_CONFIG = {
177
180
  defaultImageModel: "cogview-4",
178
181
  defaultAgent: "default",
179
182
  defaultMigrationModel: "deepseek-v4-flash",
183
+ // 工具输出上限(字符数),与 DEFAULT_MAX_TOOL_OUTPUT 保持一致
184
+ maxToolOutput: 32_768,
180
185
  };
181
186
  // ---------------------------------------------------------------------------
182
187
  // 默认 MCP 配置(socket-pty 终端 MCP)
package/dist/index.d.ts CHANGED
@@ -31,7 +31,7 @@ export { ContextGuardPlugin } from "./plugin/ContextGuardPlugin.js";
31
31
  export type { ContextGuardOptions } from "./plugin/ContextGuardPlugin.js";
32
32
  export { UnknownToolHintPlugin } from "./plugin/UnknownToolHintPlugin.js";
33
33
  export { BUILTIN_TOOL_CLASSES } from "./capabilities/implements/builtin/index.js";
34
- export { CwdTool, ReadFileTool, WriteFileTool, ExecTool, MkdirTool, RmTool, GlobTool, LsTool, ExistTool, FindTextTool, DownloadFileTool, RenameTool, CopyTool, BatchEditTool, EditTool, ExecAsyncTool, SleepTool, } from "./capabilities/implements/builtin/index.js";
34
+ export { CwdTool, ReadFileTool, InspectFileTool, WriteFileTool, ExecTool, MkdirTool, RmTool, GlobTool, LsTool, ExistTool, FindTextTool, DownloadFileTool, RenameTool, CopyTool, BatchEditTool, EditTool, ExecAsyncTool, SleepTool, } from "./capabilities/implements/builtin/index.js";
35
35
  export { GenerateImageTool } from "./capabilities/implements/builtin/GenerateImageTool.js";
36
36
  export { ViewImageTool } from "./capabilities/implements/builtin/ViewImageTool.js";
37
37
  export { createLoadSkillTool, createCallSkillSubAgentTool } from "./capabilities/implements/skillTools.js";
package/dist/index.js CHANGED
@@ -27,7 +27,7 @@ export { ContextGuardPlugin } from "./plugin/ContextGuardPlugin.js";
27
27
  export { UnknownToolHintPlugin } from "./plugin/UnknownToolHintPlugin.js";
28
28
  // 工具 — 内置工具类 + 动态工具工厂
29
29
  export { BUILTIN_TOOL_CLASSES } from "./capabilities/implements/builtin/index.js";
30
- export { CwdTool, ReadFileTool, WriteFileTool, ExecTool, MkdirTool, RmTool, GlobTool, LsTool, ExistTool, FindTextTool, DownloadFileTool, RenameTool, CopyTool, BatchEditTool, EditTool, ExecAsyncTool, SleepTool, } from "./capabilities/implements/builtin/index.js";
30
+ export { CwdTool, ReadFileTool, InspectFileTool, WriteFileTool, ExecTool, MkdirTool, RmTool, GlobTool, LsTool, ExistTool, FindTextTool, DownloadFileTool, RenameTool, CopyTool, BatchEditTool, EditTool, ExecAsyncTool, SleepTool, } from "./capabilities/implements/builtin/index.js";
31
31
  export { GenerateImageTool } from "./capabilities/implements/builtin/GenerateImageTool.js";
32
32
  export { ViewImageTool } from "./capabilities/implements/builtin/ViewImageTool.js";
33
33
  export { createLoadSkillTool, createCallSkillSubAgentTool } from "./capabilities/implements/skillTools.js";
@@ -170,7 +170,7 @@ export class Provider {
170
170
  // call_skill_sub_agent:只要有支持子 Agent 的 skill 就注册
171
171
  const hasSubAgentSkills = filteredSkills.some((s) => s.subAgent);
172
172
  if (allowedToolNames.has("call_skill_sub_agent") && hasSubAgentSkills) {
173
- result.push(createCallSkillSubAgentTool(this.skillsPaths, filteredSkills, this));
173
+ result.push(createCallSkillSubAgentTool(this.skillsPaths, filteredSkills));
174
174
  }
175
175
  const mcpManager = this.mcpManager;
176
176
  if (allowedToolNames.has("load_mcp") && mcpManager && filteredMcps.length > 0) {
@@ -46,6 +46,13 @@ export interface AgentDefinition {
46
46
  createdAt: string;
47
47
  updatedAt: string;
48
48
  version?: number;
49
+ /**
50
+ * 是否用户自定义。
51
+ *
52
+ * 为 true 时 SDK 初始化不会同步/替换该 Agent 的内置内容(如默认提示词);
53
+ * 缺省(或为 false)时,初始化会以出厂定义同步其提示词,其余字段保留。
54
+ */
55
+ custom?: boolean;
49
56
  }
50
57
  /** 图片生成模型配置 */
51
58
  export interface ImageModel {
@@ -71,6 +78,14 @@ export interface AppConfig {
71
78
  defaultAgent?: string;
72
79
  /** 默认迁移模型 ID */
73
80
  defaultMigrationModel?: string;
81
+ /**
82
+ * 工具输出上限(字符数),缺省取 32768。
83
+ *
84
+ * 工具本次输出超过该值时,由工具自身的保护策略处置(落盘或仅警告),
85
+ * 避免超长输出撑爆上下文。仅作用于"输出",与工具的读取阈值无关
86
+ * (如 readFile/findText 对 300KB 文件的读取上限)。
87
+ */
88
+ maxToolOutput?: number;
74
89
  }
75
90
  /**
76
91
  * 工具环境 — Provider 在实例化内置工具时注入的环境信息。
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ai-zen/agents-sdk",
3
- "version": "0.10.0",
3
+ "version": "0.12.0",
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",
@@ -16,7 +16,7 @@
16
16
  "author": "LZQCN",
17
17
  "license": "MIT",
18
18
  "dependencies": {
19
- "@ai-zen/agents-core": "^4.2.0",
19
+ "@ai-zen/agents-core": "^4.3.0",
20
20
  "@modelcontextprotocol/sdk": "^1.29.0",
21
21
  "dayjs": "^1.11.21",
22
22
  "openai": "^7.5.0"