minimal-agent 0.5.1 → 0.5.3
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/package.json +1 -1
- package/skills/config/SKILL.md +3 -0
- package/src/cli/configWizard.js +19 -0
- package/src/prompts/system.js +11 -0
- package/src/tools/edit/edit.js +26 -2
- package/src/tools/edit/multi-edit.js +24 -3
- package/src/tools/shared/fileState.js +2 -0
- package/src/tools/shared/fileUtils.js +61 -0
- package/src/utils/zodToJson.js +44 -1
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "minimal-agent",
|
|
3
|
-
"version": "0.5.
|
|
3
|
+
"version": "0.5.3",
|
|
4
4
|
"description": "最小化 Agent 系统 —— 10 工具 + 插件系统 + workflow DSL + 自动压缩 + OpenAI 兼容 + Ink TUI;NodeNext + tsc 原地编译,dev 用 Bun .ts、install 用 Node .js(学习/教学用)",
|
|
5
5
|
"license": "SEE LICENSE IN LICENSE",
|
|
6
6
|
"author": "Bill Wang <leiwang0359@gmail.com>",
|
package/skills/config/SKILL.md
CHANGED
|
@@ -53,8 +53,11 @@ Read({ file_path: "~/.minimal-agent/config.json" })
|
|
|
53
53
|
| moonshot | moonshot-v1-8k | 8000 |
|
|
54
54
|
| moonshot | moonshot-v1-32k | 32000 |
|
|
55
55
|
| moonshot | moonshot-v1-128k | 128000 |
|
|
56
|
+
| volces | doubao-seed-2.0-pro / glm-5.1 / kimi-k2.6 / deepseek-v3.2 / minimax-latest | 204800 |
|
|
56
57
|
| 其它 / custom | — | 128000 |
|
|
57
58
|
|
|
59
|
+
⚠️ 火山方舟 ARK Coding Plan:一个 endpoint (`https://ark.cn-beijing.volces.com/api/coding/v3`) 后挂多个 model,**model 字段大小写严格匹配**(`glm-5.1` 而非 `GLM-5.1`)。常见 model id 见 `src/cli/configWizard.tsx::PRESETS` 的 `volces` 项;部分 model(如 `glm-5.1`)走 draft-2020-12 严校验,由 `src/utils/zodToJson.ts::modernizeExclusiveBounds` 透明兼容。
|
|
60
|
+
|
|
58
61
|
⚠️ 询问 `contextWindow` 时,**先按上表给出推荐值并明确告诉用户**(例:「按你选的 `MiniMax-M2.7`,推荐 `contextWindow = 204800`,直接回车采纳」)。**不要让用户裸填 token 数**;用户主要关心 `baseURL` / `apiKey` / `model`,contextWindow 应优先按 provider+model 推断。
|
|
59
62
|
|
|
60
63
|
如果用户改了 model 但没改 contextWindow,**主动重新计算推荐值**并提示;用户旧 contextWindow 与新 model 不匹配时(如把 model 从 `moonshot-v1-8k` 改成 `moonshot-v1-128k`),明确建议同步更新。
|
package/src/cli/configWizard.js
CHANGED
|
@@ -37,6 +37,25 @@ const PRESETS = [
|
|
|
37
37
|
models: ['MiniMax-M2.7', 'MiniMax-M1', 'abab6.5s-chat'],
|
|
38
38
|
contextWindow: 204_800,
|
|
39
39
|
},
|
|
40
|
+
{
|
|
41
|
+
// 火山方舟 ARK Coding Plan —— 一个 endpoint 后挂多个 model id,
|
|
42
|
+
// model 字段大小写严格匹配('glm-5.1' 而非 'GLM-5.1')。
|
|
43
|
+
// 部分 model(如 glm-5.1)走 draft-2020-12 严校验,靠 zodToJson 的
|
|
44
|
+
// modernizeExclusiveBounds 兼容。
|
|
45
|
+
name: 'volces',
|
|
46
|
+
label: '火山方舟 Coding Plan (ARK)',
|
|
47
|
+
baseURL: 'https://ark.cn-beijing.volces.com/api/coding/v3',
|
|
48
|
+
models: [
|
|
49
|
+
'doubao-seed-2.0-pro',
|
|
50
|
+
'doubao-seed-2.0-code',
|
|
51
|
+
'doubao-seed-code',
|
|
52
|
+
'glm-5.1',
|
|
53
|
+
'minimax-latest',
|
|
54
|
+
'deepseek-v3.2',
|
|
55
|
+
'kimi-k2.6',
|
|
56
|
+
],
|
|
57
|
+
contextWindow: 204_800,
|
|
58
|
+
},
|
|
40
59
|
{
|
|
41
60
|
name: 'deepseek',
|
|
42
61
|
label: 'DeepSeek',
|
package/src/prompts/system.js
CHANGED
|
@@ -32,6 +32,16 @@ export async function getSystemPrompt(cwd, tools) {
|
|
|
32
32
|
return `你是 minimal-agent,一个最小化的 AI 编程助手。
|
|
33
33
|
你运行在用户的本地终端里,可以读取、修改、创建文件,可以搜索文件名和文件内容,可以联网搜索最新信息。
|
|
34
34
|
|
|
35
|
+
# 思维与立场(核心:像专家一样独立思考)
|
|
36
|
+
- **你是工程协作者,不是奉承者**。不无条件夸赞、不为附和而附和、不同意时直说。不用"很好的问题"、"你说得对"、"完全同意"这类前缀——直接答内容。
|
|
37
|
+
- **基于证据,不基于揣测用户想听什么**。没读过的代码先 Read,没验证过的行为先验证,不凭印象答。回答前先问自己:这个判断有具体证据吗,还是我在猜?
|
|
38
|
+
- **不知道就说不知道**。明确边界:"这个版本号我不确定"、"这个 API 我没看过文档"、"这部分行为我没验证过"。胡编一个看起来对的答案,代价远高于承认不知道。
|
|
39
|
+
- **明示 confidence 分层**。结论按确定度分层表达——"已 verify / 高度确信"、"基于代码合理推测"、"未验证猜测"——让用户基于你的不确定程度决策,不要把推测说成事实。
|
|
40
|
+
- **主动质疑用户的前提 / 方案 / 诊断**。发现方向不对时直说"这里我觉得不对,理由是 ...",给出你的看法 + 证据 + 让用户拍板。不要为了维持氛围而顺着错的方向走。
|
|
41
|
+
- **给根因不给表面答案**。bug 报告先定位 root cause 再给 fix,不要先给个让症状消失的 workaround 蒙混过去。修复要能解释"为什么这样改就对了"。
|
|
42
|
+
- **任何方案都有代价**。主动说 trade-off(性能 vs 可读、激进 vs 保守、本期 vs 长期),让用户基于全貌决策,不要只夸方案的优点。
|
|
43
|
+
- **被指出错误时直接承认 + 修正**。不解释、不防御、不找借口。承认 → 给出修正 → 避免下次。错误是 update prior 的信号,不是要消化的尴尬。
|
|
44
|
+
|
|
35
45
|
# 工作环境
|
|
36
46
|
- 当前工作目录: ${cwd}
|
|
37
47
|
- 用户主目录: ${homedir()}
|
|
@@ -52,6 +62,7 @@ ${toolList}
|
|
|
52
62
|
- 修改既有文件之前,必须用 Read 工具读取相关代码的最新内容。
|
|
53
63
|
"我记得"、"我之前看过"、"应该差不多"都不算读过——必须在本轮任务中实际调用 Read。
|
|
54
64
|
尤其当你要参考某个文件的写法来实现类似功能时,必须先重读该文件的关键部分(函数实现、判断逻辑、数据流),理解清楚后再动手。
|
|
65
|
+
- Edit / MultiEdit 拦截 fallback:如果你忘了先 Read 就直接调 Edit/MultiEdit,错误响应里会**自动附带当前文件前 200 行 / 32KB 以内的 snippet**(带行号)。这种情况下**直接基于 snippet 重新构造 old_string 再发一次 Edit 即可,不要再额外调 Read**。只有当 snippet 显示"未注入内容"(文件过大或二进制)时才需要显式 Read。Write 不附带 snippet——整文件覆盖语义下必须先显式 Read,避免基于片段重写导致截断范围外内容丢失。
|
|
55
66
|
- Edit 工具的 old_string 必须在文件中唯一;不唯一时请扩大上下文或显式 replace_all=true。
|
|
56
67
|
- 同一文件需要修改多处(3 处及以上)时优先用 MultiEdit:所有 edit 按顺序在内存中应用,**全部成功才落盘**;任一失败磁盘不动,避免中间状态污染。单点修改继续用 Edit。
|
|
57
68
|
- Write 覆盖既有文件前同样必须先 Read(与 Edit 对称,未先 Read 会被拒绝并提示"请先 Read");写新文件无此要求。
|
package/src/tools/edit/edit.js
CHANGED
|
@@ -17,7 +17,7 @@ import { dirname } from 'node:path';
|
|
|
17
17
|
import { z } from 'zod';
|
|
18
18
|
import { DEFAULT_MAX_RESULT_SIZE_CHARS } from '../types.js';
|
|
19
19
|
import { toToolParameters } from '../../utils/zodToJson.js';
|
|
20
|
-
import { resolveToolPath, findActualString, preserveQuoteStyle, detectLineEndingsForString, applyLineEnding, countOccurrences, splitReplaceAll, } from '../shared/fileUtils.js';
|
|
20
|
+
import { resolveToolPath, findActualString, preserveQuoteStyle, detectLineEndingsForString, applyLineEnding, countOccurrences, splitReplaceAll, tryAutoSnapshot, } from '../shared/fileUtils.js';
|
|
21
21
|
import { filePathField } from '../shared/schemas.js';
|
|
22
22
|
import { assertFresh, recordRead, wasTruncated } from '../shared/fileState.js';
|
|
23
23
|
/** 编辑文件大小上限:1GB。防止模型试图 Edit 超大文件撑爆内存。 */
|
|
@@ -59,9 +59,33 @@ async function call(input) {
|
|
|
59
59
|
return pathResult;
|
|
60
60
|
const filePath = pathResult.resolvedPath;
|
|
61
61
|
// 5a-bis. Pre-read guard:文件已存在则必须先 Read(创建新文件场景下 assertFresh 返回 ok:true)
|
|
62
|
+
// 拦截路径自动注入 snippet:让模型在同一轮 tool result 里看到当前内容,无需多绕一次 Read。
|
|
63
|
+
// 安全语义保留:模型必须显式重发 Edit 才会落盘;snippet 只是给模型看的,不静默放行。
|
|
62
64
|
const freshness = assertFresh(filePath);
|
|
63
65
|
if (!freshness.ok) {
|
|
64
|
-
|
|
66
|
+
const snap = await tryAutoSnapshot(filePath);
|
|
67
|
+
if (snap.ok) {
|
|
68
|
+
// 关键:truncated=true 时 wasTruncated 会守住 Write 路径,防止基于残缺 snippet 重写整文件
|
|
69
|
+
recordRead(filePath, { truncated: snap.truncated });
|
|
70
|
+
const tail = snap.truncated
|
|
71
|
+
? `\n\n⚠️ 仅注入了前 200 行(文件共 ${snap.totalLines} 行 / 上限 32KB)。若要改的位置在范围外,请显式 Read offset=X limit=Y 后再 Edit。`
|
|
72
|
+
: '';
|
|
73
|
+
return {
|
|
74
|
+
ok: false,
|
|
75
|
+
error: freshness.error +
|
|
76
|
+
`\n\n以下是当前文件${snap.truncated ? '前 200 行' : '全部'}内容(带行号),请基于此重新构造 old_string 后立即重试 Edit,不要再单独调 Read:\n\n${snap.snippet}` +
|
|
77
|
+
tail,
|
|
78
|
+
};
|
|
79
|
+
}
|
|
80
|
+
const why = snap.reason === 'too-large'
|
|
81
|
+
? `文件 ${((snap.sizeBytes ?? 0) / 1024).toFixed(1)} KB 超过 32KB 自动注入上限`
|
|
82
|
+
: snap.reason === 'binary'
|
|
83
|
+
? '文件命中二进制扩展名黑名单'
|
|
84
|
+
: '文件读取失败';
|
|
85
|
+
return {
|
|
86
|
+
ok: false,
|
|
87
|
+
error: `${freshness.error}\n\n(${why},未注入内容快照,请用 Read 工具显式读取后再 Edit)`,
|
|
88
|
+
};
|
|
65
89
|
}
|
|
66
90
|
if (old_string === new_string) {
|
|
67
91
|
return { ok: false, error: 'old_string 与 new_string 完全相同,没有可改的内容。' };
|
|
@@ -18,7 +18,7 @@ import { existsSync } from 'node:fs';
|
|
|
18
18
|
import { z } from 'zod';
|
|
19
19
|
import { DEFAULT_MAX_RESULT_SIZE_CHARS } from '../types.js';
|
|
20
20
|
import { toToolParameters } from '../../utils/zodToJson.js';
|
|
21
|
-
import { resolveToolPath, detectFileLineEndings, applyLineEnding, findActualString, preserveQuoteStyle, countOccurrences, splitReplaceAll, } from '../shared/fileUtils.js';
|
|
21
|
+
import { resolveToolPath, detectFileLineEndings, applyLineEnding, findActualString, preserveQuoteStyle, countOccurrences, splitReplaceAll, tryAutoSnapshot, } from '../shared/fileUtils.js';
|
|
22
22
|
import { filePathField } from '../shared/schemas.js';
|
|
23
23
|
import { assertFresh, recordRead, wasTruncated } from '../shared/fileState.js';
|
|
24
24
|
// ---------------- 1. Zod 输入 schema ----------------
|
|
@@ -58,10 +58,31 @@ async function call(input) {
|
|
|
58
58
|
if (!pathResult.ok)
|
|
59
59
|
return pathResult;
|
|
60
60
|
const filePath = pathResult.resolvedPath;
|
|
61
|
-
// 4b. Pre-read guard
|
|
61
|
+
// 4b. Pre-read guard —— 入口处一次性检查 + 注入 snippet(循环内多个 edit 不重复注入)
|
|
62
62
|
const freshness = assertFresh(filePath);
|
|
63
63
|
if (!freshness.ok) {
|
|
64
|
-
|
|
64
|
+
const snap = await tryAutoSnapshot(filePath);
|
|
65
|
+
if (snap.ok) {
|
|
66
|
+
recordRead(filePath, { truncated: snap.truncated });
|
|
67
|
+
const tail = snap.truncated
|
|
68
|
+
? `\n\n⚠️ 仅注入了前 200 行(文件共 ${snap.totalLines} 行 / 上限 32KB)。若要改的位置在范围外,请显式 Read offset=X limit=Y 后再 MultiEdit。`
|
|
69
|
+
: '';
|
|
70
|
+
return {
|
|
71
|
+
ok: false,
|
|
72
|
+
error: freshness.error +
|
|
73
|
+
`\n\n以下是当前文件${snap.truncated ? '前 200 行' : '全部'}内容(带行号),请基于此重新构造各 edit 的 old_string 后立即重试 MultiEdit,不要再单独调 Read:\n\n${snap.snippet}` +
|
|
74
|
+
tail,
|
|
75
|
+
};
|
|
76
|
+
}
|
|
77
|
+
const why = snap.reason === 'too-large'
|
|
78
|
+
? `文件 ${((snap.sizeBytes ?? 0) / 1024).toFixed(1)} KB 超过 32KB 自动注入上限`
|
|
79
|
+
: snap.reason === 'binary'
|
|
80
|
+
? '文件命中二进制扩展名黑名单'
|
|
81
|
+
: '文件读取失败';
|
|
82
|
+
return {
|
|
83
|
+
ok: false,
|
|
84
|
+
error: `${freshness.error}\n\n(${why},未注入内容快照,请用 Read 工具显式读取后再 MultiEdit)`,
|
|
85
|
+
};
|
|
65
86
|
}
|
|
66
87
|
// 4c. 文件必须存在(MultiEdit 不做"创建新文件"语义)
|
|
67
88
|
if (!existsSync(filePath)) {
|
|
@@ -39,6 +39,7 @@ export function assertFresh(absPath) {
|
|
|
39
39
|
if (existsSync(absPath)) {
|
|
40
40
|
return {
|
|
41
41
|
ok: false,
|
|
42
|
+
reason: 'never-read',
|
|
42
43
|
error: `文件 ${absPath} 已存在但未在本会话 Read 过。请先用 Read 工具读取,确认内容后再修改。`,
|
|
43
44
|
};
|
|
44
45
|
}
|
|
@@ -50,6 +51,7 @@ export function assertFresh(absPath) {
|
|
|
50
51
|
if (st.mtimeMs > entry.timestamp) {
|
|
51
52
|
return {
|
|
52
53
|
ok: false,
|
|
54
|
+
reason: 'mtime-drift',
|
|
53
55
|
error: `${absPath} 在 Read 后被外部修改(mtime 漂移)。请重新用 Read 工具读取最新内容。`,
|
|
54
56
|
};
|
|
55
57
|
}
|
|
@@ -9,6 +9,7 @@
|
|
|
9
9
|
* 4. 引号风格保留(弯引号 ↔ 直引号归一化)
|
|
10
10
|
* ============================================================
|
|
11
11
|
*/
|
|
12
|
+
import { statSync } from 'node:fs';
|
|
12
13
|
import { open, readFile } from 'node:fs/promises';
|
|
13
14
|
import { homedir } from 'node:os';
|
|
14
15
|
import { extname, resolve } from 'node:path';
|
|
@@ -129,6 +130,66 @@ export function hasBinaryExtension(filePath) {
|
|
|
129
130
|
const ext = extname(filePath).toLowerCase();
|
|
130
131
|
return BINARY_EXTENSIONS.has(ext);
|
|
131
132
|
}
|
|
133
|
+
// -------------------- 3.7 自动 snapshot(Edit/MultiEdit 拦截路径用) --------------------
|
|
134
|
+
/**
|
|
135
|
+
* 自动读取文件前 N 行作为 snippet,给 Edit/MultiEdit 在 assertFresh 拦截时注入到错误响应里。
|
|
136
|
+
*
|
|
137
|
+
* 双限制:
|
|
138
|
+
* - SNAPSHOT_MAX_LINES = 200 行(防止小行长文件吃 context)
|
|
139
|
+
* - SNAPSHOT_MAX_BYTES = 32KB(防止超长单行 minified 文件撑爆 context)
|
|
140
|
+
* 两个阈值任一命中都拒绝读全量。
|
|
141
|
+
*
|
|
142
|
+
* 失败 reason:
|
|
143
|
+
* - 'binary' → 命中二进制扩展名黑名单
|
|
144
|
+
* - 'too-large' → 文件 > 32KB(不读全文,让模型走显式 Read)
|
|
145
|
+
* - 'unreadable' → stat 或 read 失败
|
|
146
|
+
*
|
|
147
|
+
* 调用方负责:
|
|
148
|
+
* 1. 在拿到 ok=true 后调 recordRead(filePath, { truncated: snap.truncated })
|
|
149
|
+
* 2. 把 snippet 拼到 error 字符串里
|
|
150
|
+
* 3. ok=false 时根据 reason 给模型可读的 fallback 消息
|
|
151
|
+
*/
|
|
152
|
+
const SNAPSHOT_MAX_LINES = 200;
|
|
153
|
+
const SNAPSHOT_MAX_BYTES = 32 * 1024;
|
|
154
|
+
export async function tryAutoSnapshot(filePath) {
|
|
155
|
+
// 1. 二进制扩展名拒绝
|
|
156
|
+
if (hasBinaryExtension(filePath)) {
|
|
157
|
+
return { ok: false, reason: 'binary' };
|
|
158
|
+
}
|
|
159
|
+
// 2. stat 拿大小
|
|
160
|
+
let sizeBytes;
|
|
161
|
+
try {
|
|
162
|
+
const st = statSync(filePath);
|
|
163
|
+
if (!st.isFile())
|
|
164
|
+
return { ok: false, reason: 'unreadable' };
|
|
165
|
+
sizeBytes = st.size;
|
|
166
|
+
}
|
|
167
|
+
catch {
|
|
168
|
+
return { ok: false, reason: 'unreadable' };
|
|
169
|
+
}
|
|
170
|
+
// 3. 字节阈值
|
|
171
|
+
if (sizeBytes > SNAPSHOT_MAX_BYTES) {
|
|
172
|
+
return { ok: false, reason: 'too-large', sizeBytes };
|
|
173
|
+
}
|
|
174
|
+
// 4. 读全文(已知 <= 32KB)
|
|
175
|
+
let content;
|
|
176
|
+
try {
|
|
177
|
+
content = await readFile(filePath, 'utf8');
|
|
178
|
+
}
|
|
179
|
+
catch {
|
|
180
|
+
return { ok: false, reason: 'unreadable' };
|
|
181
|
+
}
|
|
182
|
+
// 5. 行数阈值:超过 200 行截断
|
|
183
|
+
const allLines = content.split('\n');
|
|
184
|
+
const totalLines = allLines.length;
|
|
185
|
+
const truncated = totalLines > SNAPSHOT_MAX_LINES;
|
|
186
|
+
const lines = truncated ? allLines.slice(0, SNAPSHOT_MAX_LINES) : allLines;
|
|
187
|
+
// 拼带行号的 snippet(与 Read 工具输出风格一致)
|
|
188
|
+
const numbered = lines
|
|
189
|
+
.map((line, i) => `${String(i + 1).padStart(6, ' ')}\t${line}`)
|
|
190
|
+
.join('\n');
|
|
191
|
+
return { ok: true, snippet: numbered, truncated, totalLines };
|
|
192
|
+
}
|
|
132
193
|
/**
|
|
133
194
|
* 读文件前 3 字节,返回 BOM 类型。
|
|
134
195
|
* - EF BB BF → utf8-bom
|
package/src/utils/zodToJson.js
CHANGED
|
@@ -11,12 +11,55 @@
|
|
|
11
11
|
* ============================================================
|
|
12
12
|
*/
|
|
13
13
|
import { zodToJsonSchema } from 'zod-to-json-schema';
|
|
14
|
+
/**
|
|
15
|
+
* 把 zodToJsonSchema 输出的 draft-04 boolean 形式 exclusiveMin/Max 升级为
|
|
16
|
+
* draft-2020-12 number 形式,让生成的 schema 在严校验 LLM provider 也能通过。
|
|
17
|
+
*
|
|
18
|
+
* 背景:`target: 'openApi3'` 走 OpenAPI 3.0 / JSON Schema draft-04 规范,
|
|
19
|
+
* `.positive()` 之类的 Zod 约束生成 `{minimum: 0, exclusiveMinimum: true}`。
|
|
20
|
+
* 严校验 provider(如火山 ARK Coding 后挂的 GLM-5.1)按 draft-2020-12 要求
|
|
21
|
+
* `exclusiveMinimum` 必须是 number,遇 boolean 直接 400 InvalidParameter。
|
|
22
|
+
*
|
|
23
|
+
* 转换规则:
|
|
24
|
+
* {minimum: N, exclusiveMinimum: true} → {exclusiveMinimum: N}
|
|
25
|
+
* {maximum: N, exclusiveMaximum: true} → {exclusiveMaximum: N}
|
|
26
|
+
*
|
|
27
|
+
* Fallback:boolean 形式但同级没有 minimum/maximum 时(理论 edge case),
|
|
28
|
+
* 静默删除 boolean 字段——避免污染 schema,运行时 Zod 兜底校验。
|
|
29
|
+
*/
|
|
30
|
+
function modernizeExclusiveBounds(node) {
|
|
31
|
+
if (Array.isArray(node))
|
|
32
|
+
return node.map(modernizeExclusiveBounds);
|
|
33
|
+
if (node === null || typeof node !== 'object')
|
|
34
|
+
return node;
|
|
35
|
+
const obj = node;
|
|
36
|
+
const out = {};
|
|
37
|
+
for (const [k, v] of Object.entries(obj)) {
|
|
38
|
+
if (k === 'exclusiveMinimum' && typeof v === 'boolean') {
|
|
39
|
+
if (v && typeof obj.minimum === 'number')
|
|
40
|
+
out.exclusiveMinimum = obj.minimum;
|
|
41
|
+
continue;
|
|
42
|
+
}
|
|
43
|
+
if (k === 'minimum' && obj.exclusiveMinimum === true)
|
|
44
|
+
continue;
|
|
45
|
+
if (k === 'exclusiveMaximum' && typeof v === 'boolean') {
|
|
46
|
+
if (v && typeof obj.maximum === 'number')
|
|
47
|
+
out.exclusiveMaximum = obj.maximum;
|
|
48
|
+
continue;
|
|
49
|
+
}
|
|
50
|
+
if (k === 'maximum' && obj.exclusiveMaximum === true)
|
|
51
|
+
continue;
|
|
52
|
+
out[k] = modernizeExclusiveBounds(v);
|
|
53
|
+
}
|
|
54
|
+
return out;
|
|
55
|
+
}
|
|
14
56
|
/**
|
|
15
57
|
* 从 Zod schema 派生出 OpenAI tools[] 兼容的 JSON Schema 对象。
|
|
16
58
|
*
|
|
17
59
|
* 做了以下清理:
|
|
18
60
|
* - 移除 `$schema` 元数据字段(LLM 不需要)
|
|
19
61
|
* - `additionalProperties` 默认 false(防止模型传多余字段)
|
|
62
|
+
* - `modernizeExclusiveBounds` 把 boolean exclusiveMin/Max 升级 draft-2020-12 number 形式
|
|
20
63
|
*/
|
|
21
64
|
export function toToolParameters(schema) {
|
|
22
65
|
const json = zodToJsonSchema(schema, {
|
|
@@ -25,5 +68,5 @@ export function toToolParameters(schema) {
|
|
|
25
68
|
});
|
|
26
69
|
// 移除 LLM 不需要的元数据
|
|
27
70
|
const { $schema, ...rest } = json;
|
|
28
|
-
return rest;
|
|
71
|
+
return modernizeExclusiveBounds(rest);
|
|
29
72
|
}
|