@danceiny/gotry 0.0.1-rc.20 → 0.0.1-rc.21
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/README.md +14 -3
- package/README.zh-CN.md +15 -4
- package/bin/gotry-bootstrap.js +53 -8
- package/cordis.gotry-patch.yml +12 -5
- package/dist/capabilities/doctor.js +88 -16
- package/dist/capabilities/session/health-watch.js +10 -3
- package/dist/scripts/benchmark-environment-bridge-e2e.js +53 -15
- package/dist/scripts/benchmark-environment-bridge-tests.js +450 -28
- package/dist/scripts/booking-copilot-dsh-planner-proof-tests.js +211 -2
- package/dist/scripts/booking-copilot-runtime-proof-tests.js +132 -2
- package/dist/scripts/bootstrap-tests.js +2 -1
- package/dist/scripts/hbcli-e2e-tests.js +1 -1
- package/dist/scripts/health-watch-cli.js +9 -1
- package/dist/scripts/map-tools-vendor-package-proof.js +276 -0
- package/dist/scripts/persona-surface-guard-tests.js +8 -3
- package/dist/src/benchmark-agent-conformance.js +155 -5
- package/dist/src/benchmark-environment-bridge.js +53 -71
- package/dist/src/booking-surface/dsh-planner.js +24 -8
- package/extension/manifest.json +2 -2
- package/package.json +2 -1
- package/ts/capabilities/session/health-watch.ts +9 -2
- package/ts/package.json +15 -7
- package/ts/scripts/map-tools-vendor-package-proof.ts +268 -0
- package/ts/src/benchmark-agent-conformance.ts +135 -4
- package/ts/src/benchmark-environment-bridge.ts +25 -40
- package/ts/src/booking-surface/dsh-planner.ts +25 -13
|
@@ -0,0 +1,276 @@
|
|
|
1
|
+
import { execFileSync } from 'node:child_process';
|
|
2
|
+
import { createHash } from 'node:crypto';
|
|
3
|
+
import { existsSync, mkdirSync, mkdtempSync, readFileSync, readdirSync, realpathSync, rmSync, symlinkSync } from 'node:fs';
|
|
4
|
+
import { createRequire } from 'node:module';
|
|
5
|
+
import { basename, dirname, isAbsolute, join, resolve } from 'node:path';
|
|
6
|
+
import { fileURLToPath, pathToFileURL } from 'node:url';
|
|
7
|
+
import assert from 'node:assert/strict';
|
|
8
|
+
const repoRoot = resolve(dirname(fileURLToPath(import.meta.url)), '../..');
|
|
9
|
+
const upstreamLicenseSha256 = 'b6bbb0c73a02cf8d2c304e9f208b41c32f0a810fabe366986e2b14ac3338f618';
|
|
10
|
+
const upstreamVendorFileCount = 32;
|
|
11
|
+
const upstreamVendorAggregateSha256 = 'd82a38adace8bfe574dcecb4222da5c994a1b9e8792f78dc2dad3a7f50c9925a';
|
|
12
|
+
const expectedTools = [
|
|
13
|
+
'map_bicycling_route',
|
|
14
|
+
'map_driving_route',
|
|
15
|
+
'map_geocode',
|
|
16
|
+
'map_poi_search',
|
|
17
|
+
'map_reverse_geocode',
|
|
18
|
+
'map_transit_route',
|
|
19
|
+
'map_walking_route'
|
|
20
|
+
];
|
|
21
|
+
function sha256(path) {
|
|
22
|
+
return createHash('sha256').update(readFileSync(path)).digest('hex');
|
|
23
|
+
}
|
|
24
|
+
function vendorAggregate(root) {
|
|
25
|
+
const files = [];
|
|
26
|
+
const walk = (directory)=>{
|
|
27
|
+
for (const entry of readdirSync(directory, {
|
|
28
|
+
withFileTypes: true
|
|
29
|
+
})){
|
|
30
|
+
const path = join(directory, entry.name);
|
|
31
|
+
if (entry.isDirectory()) walk(path);
|
|
32
|
+
else files.push(path.slice(root.length + 1).replaceAll('\\', '/'));
|
|
33
|
+
}
|
|
34
|
+
};
|
|
35
|
+
walk(root);
|
|
36
|
+
files.sort();
|
|
37
|
+
const digest = createHash('sha256');
|
|
38
|
+
for (const relative of files){
|
|
39
|
+
digest.update(relative);
|
|
40
|
+
digest.update('\0');
|
|
41
|
+
digest.update(readFileSync(join(root, relative)));
|
|
42
|
+
digest.update('\0');
|
|
43
|
+
}
|
|
44
|
+
return {
|
|
45
|
+
count: files.length,
|
|
46
|
+
sha256: digest.digest('hex')
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
function assertAlpha3LockClosure() {
|
|
50
|
+
const manifest = JSON.parse(readFileSync(join(repoRoot, 'ts/package.json'), 'utf8'));
|
|
51
|
+
assert.equal(manifest.dependencies?.['dsh-map-tools'], undefined, 'external map-tools dependency must stay removed');
|
|
52
|
+
const lock = JSON.parse(readFileSync(join(repoRoot, 'ts/package-lock.json'), 'utf8'));
|
|
53
|
+
assert.equal(lock.packages?.['']?.dependencies?.['dsh-map-tools'], undefined, 'lock root must not resolve external map-tools');
|
|
54
|
+
assert.equal(lock.packages?.['node_modules/dsh-map-tools'], undefined, 'lock must not carry external map-tools');
|
|
55
|
+
const dshEntries = Object.entries(lock.packages ?? {}).filter(([path])=>/(?:^|\/)node_modules\/@deepseek-ai\/(?:dsh|dsh-[^/]+)$/.test(path));
|
|
56
|
+
assert.ok(dshEntries.length >= 10, 'expected dsh-tools peer closure in the ts lock');
|
|
57
|
+
for (const [path, entry] of dshEntries){
|
|
58
|
+
assert.equal(entry.version, '0.1.2-alpha.3', `${path} drifted outside the alpha.3 closure`);
|
|
59
|
+
}
|
|
60
|
+
return {
|
|
61
|
+
packages: dshEntries.length,
|
|
62
|
+
version: '0.1.2-alpha.3'
|
|
63
|
+
};
|
|
64
|
+
}
|
|
65
|
+
function findPackageRoot(entry) {
|
|
66
|
+
let cursor = dirname(entry);
|
|
67
|
+
for(;;){
|
|
68
|
+
const manifestPath = join(cursor, 'package.json');
|
|
69
|
+
if (existsSync(manifestPath)) {
|
|
70
|
+
const manifest = JSON.parse(readFileSync(manifestPath, 'utf8'));
|
|
71
|
+
if (manifest.name === '@danceiny/gotry') return cursor;
|
|
72
|
+
}
|
|
73
|
+
const parent = dirname(cursor);
|
|
74
|
+
if (parent === cursor) throw new Error(`cannot locate @danceiny/gotry package root from ${entry}`);
|
|
75
|
+
cursor = parent;
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
function assertBinTargetsPackage(binPath, expectedTarget) {
|
|
79
|
+
const resolvedExpectedTarget = realpathSync(expectedTarget);
|
|
80
|
+
if (realpathSync(binPath) === resolvedExpectedTarget) return;
|
|
81
|
+
const shim = readFileSync(binPath, 'utf8');
|
|
82
|
+
assert.ok(Buffer.byteLength(shim, 'utf8') <= 32_768, 'clean-install gotry shim is unexpectedly large');
|
|
83
|
+
const lines = shim.trimEnd().split(/\r?\n/);
|
|
84
|
+
const markers = lines.filter((line)=>line.startsWith('# cmd-shim-target='));
|
|
85
|
+
assert.equal(markers.length, 1, 'pnpm gotry shim must contain exactly one target marker');
|
|
86
|
+
assert.equal(lines.at(-1), markers[0], 'pnpm gotry shim target marker must be the final line');
|
|
87
|
+
const markerTarget = markers[0].slice('# cmd-shim-target='.length);
|
|
88
|
+
assert.ok(isAbsolute(markerTarget), 'pnpm gotry shim target must be absolute');
|
|
89
|
+
assert.equal(realpathSync(markerTarget), resolvedExpectedTarget, 'GOTRY_MAP_TOOLS_E2E_BIN shim must target this installed GoTry package');
|
|
90
|
+
}
|
|
91
|
+
function prepareProofPackage(cleanInstalledBin) {
|
|
92
|
+
if (cleanInstalledBin) {
|
|
93
|
+
const binPath = resolve(cleanInstalledBin);
|
|
94
|
+
assert.equal(basename(binPath), 'gotry', 'clean-install executable must use the gotry bin name');
|
|
95
|
+
assert.equal(basename(dirname(binPath)), '.bin', 'clean-install executable must come from node_modules/.bin');
|
|
96
|
+
assert.equal(basename(dirname(dirname(binPath))), 'node_modules', 'clean-install executable must come from a consumer install');
|
|
97
|
+
const consumerRoot = dirname(dirname(dirname(binPath)));
|
|
98
|
+
const packageMain = createRequire(join(consumerRoot, 'package.json')).resolve('@danceiny/gotry');
|
|
99
|
+
const packageRoot = findPackageRoot(packageMain);
|
|
100
|
+
const installedManifest = JSON.parse(readFileSync(join(packageRoot, 'package.json'), 'utf8'));
|
|
101
|
+
assert.equal(installedManifest.name, '@danceiny/gotry', 'clean-install executable must belong to GoTry');
|
|
102
|
+
const declaredBin = typeof installedManifest.bin === 'string' ? installedManifest.bin : installedManifest.bin?.gotry;
|
|
103
|
+
assert.equal(declaredBin, 'bin/gotry-inner.js', 'installed package must declare the production gotry bin');
|
|
104
|
+
assertBinTargetsPackage(binPath, join(packageRoot, declaredBin));
|
|
105
|
+
return {
|
|
106
|
+
packageRoot
|
|
107
|
+
};
|
|
108
|
+
}
|
|
109
|
+
const proofRoot = mkdtempSync(join(repoRoot, '.tmp-map-tools-proof-'));
|
|
110
|
+
try {
|
|
111
|
+
const packOutput = execFileSync('npm', [
|
|
112
|
+
'pack',
|
|
113
|
+
'--ignore-scripts',
|
|
114
|
+
'--json',
|
|
115
|
+
'--pack-destination',
|
|
116
|
+
proofRoot
|
|
117
|
+
], {
|
|
118
|
+
cwd: repoRoot,
|
|
119
|
+
encoding: 'utf8'
|
|
120
|
+
});
|
|
121
|
+
const packInfo = JSON.parse(packOutput);
|
|
122
|
+
assert.equal(packInfo.length, 1, 'npm pack must produce one artifact');
|
|
123
|
+
const tarball = join(proofRoot, packInfo[0].filename);
|
|
124
|
+
assert.ok(existsSync(tarball), `packed artifact missing: ${tarball}`);
|
|
125
|
+
const unpackRoot = join(proofRoot, 'unpacked');
|
|
126
|
+
mkdirSync(unpackRoot);
|
|
127
|
+
execFileSync('tar', [
|
|
128
|
+
'-xzf',
|
|
129
|
+
tarball,
|
|
130
|
+
'-C',
|
|
131
|
+
unpackRoot
|
|
132
|
+
]);
|
|
133
|
+
const packageRoot = join(unpackRoot, 'package');
|
|
134
|
+
symlinkSync(join(repoRoot, 'node_modules'), join(packageRoot, 'node_modules'), process.platform === 'win32' ? 'junction' : 'dir');
|
|
135
|
+
return {
|
|
136
|
+
packageRoot,
|
|
137
|
+
proofRoot
|
|
138
|
+
};
|
|
139
|
+
} catch (error) {
|
|
140
|
+
rmSync(proofRoot, {
|
|
141
|
+
recursive: true,
|
|
142
|
+
force: true
|
|
143
|
+
});
|
|
144
|
+
throw error;
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
const cleanInstalledBin = process.env.GOTRY_MAP_TOOLS_E2E_BIN;
|
|
148
|
+
const { packageRoot, proofRoot } = prepareProofPackage(cleanInstalledBin);
|
|
149
|
+
try {
|
|
150
|
+
const lockedDsh = assertAlpha3LockClosure();
|
|
151
|
+
const vendorRoot = join(packageRoot, 'ts/dsh-runtime/vendor/dsh-map-tools');
|
|
152
|
+
const requiredFiles = [
|
|
153
|
+
'LICENSE',
|
|
154
|
+
'package.json',
|
|
155
|
+
'cordis.patch.yml',
|
|
156
|
+
'lib/index.js',
|
|
157
|
+
'lib/settings-ns.js',
|
|
158
|
+
'lib/config.js',
|
|
159
|
+
'lib/tools/geocode.js',
|
|
160
|
+
'lib/tools/routes.js',
|
|
161
|
+
'lib/tools/poi.js'
|
|
162
|
+
];
|
|
163
|
+
for (const relative of requiredFiles){
|
|
164
|
+
assert.ok(existsSync(join(vendorRoot, relative)), `required vendor file missing: ${relative}`);
|
|
165
|
+
}
|
|
166
|
+
const vendorTree = vendorAggregate(vendorRoot);
|
|
167
|
+
assert.equal(vendorTree.count, upstreamVendorFileCount, 'vendored payload file count drifted');
|
|
168
|
+
assert.equal(vendorTree.sha256, upstreamVendorAggregateSha256, 'vendored payload aggregate drifted');
|
|
169
|
+
const vendorPackage = JSON.parse(readFileSync(join(vendorRoot, 'package.json'), 'utf8'));
|
|
170
|
+
assert.equal(vendorPackage.name, 'dsh-map-tools');
|
|
171
|
+
assert.equal(vendorPackage.version, '0.5.1');
|
|
172
|
+
assert.equal(vendorPackage.license, 'MIT');
|
|
173
|
+
assert.equal(sha256(join(vendorRoot, 'LICENSE')), upstreamLicenseSha256, 'upstream MIT license changed');
|
|
174
|
+
const packageRequire = createRequire(join(packageRoot, 'package.json'));
|
|
175
|
+
const dshTools = JSON.parse(readFileSync(packageRequire.resolve('@deepseek-ai/dsh-tools/package.json'), 'utf8'));
|
|
176
|
+
const dshSettings = JSON.parse(readFileSync(packageRequire.resolve('@deepseek-ai/dsh-settings/package.json'), 'utf8'));
|
|
177
|
+
assert.equal(dshTools.version, '0.1.2-alpha.3');
|
|
178
|
+
assert.equal(dshSettings.version, '0.1.2-alpha.3');
|
|
179
|
+
const settingsApi = await import(pathToFileURL(packageRequire.resolve('@deepseek-ai/dsh-settings')).href);
|
|
180
|
+
assert.equal(typeof settingsApi.SettingsProvider?.prototype?.installSection, 'function', 'real alpha.3 SettingsProvider.installSection API must be present');
|
|
181
|
+
const plugin = await import(pathToFileURL(join(vendorRoot, 'lib/index.js')).href);
|
|
182
|
+
const registered = [];
|
|
183
|
+
const settingsCalls = [];
|
|
184
|
+
const context = {
|
|
185
|
+
tools: {
|
|
186
|
+
register (tool) {
|
|
187
|
+
registered.push(tool);
|
|
188
|
+
return ()=>undefined;
|
|
189
|
+
}
|
|
190
|
+
},
|
|
191
|
+
effect (effect) {
|
|
192
|
+
effect();
|
|
193
|
+
},
|
|
194
|
+
inject (dependencies, callback) {
|
|
195
|
+
if (dependencies.includes('settings')) {
|
|
196
|
+
callback({
|
|
197
|
+
settings: {
|
|
198
|
+
installSection: (...args)=>{
|
|
199
|
+
settingsCalls.push(args);
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
});
|
|
203
|
+
} else if (dependencies.includes('webServer')) {
|
|
204
|
+
callback({
|
|
205
|
+
webServer: {
|
|
206
|
+
register: ()=>undefined
|
|
207
|
+
}
|
|
208
|
+
});
|
|
209
|
+
} else {
|
|
210
|
+
throw new Error(`unexpected dependency injection: ${dependencies.join(',')}`);
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
};
|
|
214
|
+
plugin.apply(context, {
|
|
215
|
+
provider: 'osm',
|
|
216
|
+
amapKey: '',
|
|
217
|
+
timeoutMs: 1000,
|
|
218
|
+
maxQps: 2,
|
|
219
|
+
defaultMode: 'driving',
|
|
220
|
+
language: 'zh'
|
|
221
|
+
});
|
|
222
|
+
assert.deepEqual(registered.map((tool)=>tool.name).sort(), expectedTools, 'exactly seven map tools must register');
|
|
223
|
+
assert.equal(settingsCalls.length, 1, 'alpha.3 settings section must be installed once');
|
|
224
|
+
assert.equal(settingsCalls[0][1], 'dsh-map-tools', 'settings namespace must be stable');
|
|
225
|
+
assert.equal(typeof settingsCalls[0][4], 'object', 'alpha.3 installSection hooks must be supplied');
|
|
226
|
+
const originalFetch = globalThis.fetch;
|
|
227
|
+
globalThis.fetch = async ()=>{
|
|
228
|
+
throw new Error('network forbidden by package proof');
|
|
229
|
+
};
|
|
230
|
+
try {
|
|
231
|
+
const geocode = registered.find((tool)=>tool.name === 'map_geocode');
|
|
232
|
+
assert.ok(geocode, 'map_geocode must be registered');
|
|
233
|
+
const result = await geocode.execute({
|
|
234
|
+
address: '116.397428,39.90923'
|
|
235
|
+
}, {
|
|
236
|
+
signal: new AbortController().signal
|
|
237
|
+
});
|
|
238
|
+
assert.equal(result.provider, 'inline');
|
|
239
|
+
assert.deepEqual(result.location, [
|
|
240
|
+
116.397428,
|
|
241
|
+
39.90923
|
|
242
|
+
]);
|
|
243
|
+
} finally{
|
|
244
|
+
globalThis.fetch = originalFetch;
|
|
245
|
+
}
|
|
246
|
+
console.log(JSON.stringify({
|
|
247
|
+
proof: 'map-tools-vendor-package',
|
|
248
|
+
status: 'PASS',
|
|
249
|
+
artifactMode: cleanInstalledBin ? 'clean-installed-tarball' : 'packed-unpacked-focused',
|
|
250
|
+
vendored: {
|
|
251
|
+
sourcePackage: 'dsh-map-tools@0.5.1',
|
|
252
|
+
licenseSha256: upstreamLicenseSha256,
|
|
253
|
+
vendorFileCount: vendorTree.count,
|
|
254
|
+
vendorAggregateSha256: vendorTree.sha256
|
|
255
|
+
},
|
|
256
|
+
artifactVendor: vendorRoot,
|
|
257
|
+
dshClosure: {
|
|
258
|
+
tools: dshTools.version,
|
|
259
|
+
settings: dshSettings.version,
|
|
260
|
+
...lockedDsh
|
|
261
|
+
},
|
|
262
|
+
tools: registered.map((tool)=>tool.name).sort(),
|
|
263
|
+
inlineCoordinate: {
|
|
264
|
+
provider: 'inline',
|
|
265
|
+
network: 'forbidden'
|
|
266
|
+
}
|
|
267
|
+
}, null, 2));
|
|
268
|
+
} finally{
|
|
269
|
+
if (proofRoot) rmSync(proofRoot, {
|
|
270
|
+
recursive: true,
|
|
271
|
+
force: true
|
|
272
|
+
});
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
|
|
276
|
+
//# sourceURL=ts/scripts/map-tools-vendor-package-proof.ts
|
|
@@ -8,12 +8,17 @@ assert.ok(patch.includes('绝不把 gotry_* 名字传给 skill 加载器'), '应
|
|
|
8
8
|
const markers = new Set([
|
|
9
9
|
...patch.matchAll(/\((\d{1,2})\)/g)
|
|
10
10
|
].map((m)=>Number(m[1])));
|
|
11
|
-
for(let i = 1; i <=
|
|
11
|
+
for(let i = 1; i <= 23; i += 1){
|
|
12
12
|
assert.ok(markers.has(i), `行为契约 (${i}) 应存在`);
|
|
13
13
|
}
|
|
14
|
-
assert.equal(markers.size,
|
|
14
|
+
assert.equal(markers.size, 23, `契约条目应恰为 23 条,实际 ${markers.size}`);
|
|
15
15
|
assert.ok(patch.includes('改回 tool call'), '应指引失败后改回 tool call');
|
|
16
|
-
|
|
16
|
+
assert.ok(patch.includes('澄清卡/访谈/选项里向用户列出的候选时段示例'), '#2 回归:候选时段示例条款应存在');
|
|
17
|
+
assert.ok(patch.includes('已过的节日不进示例枚举'), '应禁止已过节日进澄清卡示例枚举');
|
|
18
|
+
assert.ok(patch.includes('(23)子任务等待纪律'), '#194 回归:子任务等待纪律条款应存在');
|
|
19
|
+
assert.ok(patch.includes('对子代理绝不调用 job_output/job_kill'), '应禁止对子代理调用 job_output/job_kill');
|
|
20
|
+
assert.ok(patch.includes("job_output/job_kill 只用于"), '应限定 job_output/job_kill 的适用面');
|
|
21
|
+
console.log('PERSONA SURFACE GUARD TESTS: 5/5 OK(表层规则句 / 23 条契约完整 / skill 失败行为指引 / 澄清卡示例过锚点卡 / 子任务等待纪律)');
|
|
17
22
|
|
|
18
23
|
|
|
19
24
|
//# sourceURL=ts/scripts/persona-surface-guard-tests.ts
|
|
@@ -19,6 +19,19 @@ const BENCHMARK_DOMAIN_RECOVERIES = new Set([
|
|
|
19
19
|
'revise_arguments',
|
|
20
20
|
'choose_alternative'
|
|
21
21
|
]);
|
|
22
|
+
const TERMINAL_BODY_SCHEMA_MAX_BYTES = 16 * 1024;
|
|
23
|
+
const TERMINAL_BODY_SCHEMA_MAX_DEPTH = 8;
|
|
24
|
+
const TERMINAL_BODY_SCHEMA_MAX_NODES = 256;
|
|
25
|
+
const TERMINAL_BODY_SCHEMA_MAX_PROPERTIES = 256;
|
|
26
|
+
const TERMINAL_BODY_SCALAR_TYPES = new Set([
|
|
27
|
+
'string',
|
|
28
|
+
'number',
|
|
29
|
+
'integer',
|
|
30
|
+
'boolean',
|
|
31
|
+
'null'
|
|
32
|
+
]);
|
|
33
|
+
const TERMINAL_BODY_VALUE_MAX_NODES = 10_000;
|
|
34
|
+
const TERMINAL_BODY_VALUE_MAX_DEPTH = 24;
|
|
22
35
|
function plainObject(value) {
|
|
23
36
|
return typeof value === 'object' && value !== null && !Array.isArray(value);
|
|
24
37
|
}
|
|
@@ -26,9 +39,143 @@ export function validateTerminalOutputConfig(value) {
|
|
|
26
39
|
if (!plainObject(value)) return false;
|
|
27
40
|
const keys = Object.keys(value).sort();
|
|
28
41
|
return JSON.stringify(keys) === JSON.stringify([
|
|
42
|
+
'body_schema',
|
|
29
43
|
'max_bytes',
|
|
30
44
|
'tag'
|
|
31
|
-
]) && typeof value.tag === 'string' && IDENTIFIER.test(value.tag) && typeof value.max_bytes === 'number' && Number.isInteger(value.max_bytes) && value.max_bytes >= 1 && value.max_bytes <= MAX_TERMINAL_BYTES;
|
|
45
|
+
]) && typeof value.tag === 'string' && IDENTIFIER.test(value.tag) && typeof value.max_bytes === 'number' && Number.isInteger(value.max_bytes) && value.max_bytes >= 1 && value.max_bytes <= MAX_TERMINAL_BYTES && validateTerminalBodySchema(value.body_schema);
|
|
46
|
+
}
|
|
47
|
+
export function validateTerminalBodySchema(value) {
|
|
48
|
+
let serialized;
|
|
49
|
+
try {
|
|
50
|
+
serialized = JSON.stringify(value);
|
|
51
|
+
} catch {
|
|
52
|
+
return false;
|
|
53
|
+
}
|
|
54
|
+
if (typeof serialized !== 'string' || Buffer.byteLength(serialized, 'utf8') > TERMINAL_BODY_SCHEMA_MAX_BYTES) return false;
|
|
55
|
+
const pending = [
|
|
56
|
+
{
|
|
57
|
+
node: value,
|
|
58
|
+
depth: 0
|
|
59
|
+
}
|
|
60
|
+
];
|
|
61
|
+
const seen = new Set();
|
|
62
|
+
let nodes = 0;
|
|
63
|
+
let properties = 0;
|
|
64
|
+
while(pending.length > 0){
|
|
65
|
+
const { node, depth } = pending.pop();
|
|
66
|
+
if (!plainObject(node) || seen.has(node) || ++nodes > TERMINAL_BODY_SCHEMA_MAX_NODES || depth > TERMINAL_BODY_SCHEMA_MAX_DEPTH) return false;
|
|
67
|
+
seen.add(node);
|
|
68
|
+
const type = node.type;
|
|
69
|
+
if (type === 'object') {
|
|
70
|
+
if (!exactKeys(node, [
|
|
71
|
+
'type',
|
|
72
|
+
'properties',
|
|
73
|
+
'required',
|
|
74
|
+
'additionalProperties'
|
|
75
|
+
]) || !plainObject(node.properties) || !Array.isArray(node.required) || node.additionalProperties !== false) return false;
|
|
76
|
+
const entries = Object.entries(node.properties);
|
|
77
|
+
properties += entries.length;
|
|
78
|
+
if (properties > TERMINAL_BODY_SCHEMA_MAX_PROPERTIES || entries.some(([key, child])=>key.length === 0 || key.length > 64 || !plainObject(child))) return false;
|
|
79
|
+
if (node.required.length > entries.length || new Set(node.required).size !== node.required.length || node.required.some((key)=>typeof key !== 'string' || !Object.hasOwn(node.properties, key))) return false;
|
|
80
|
+
for (const [, child] of entries)pending.push({
|
|
81
|
+
node: child,
|
|
82
|
+
depth: depth + 1
|
|
83
|
+
});
|
|
84
|
+
continue;
|
|
85
|
+
}
|
|
86
|
+
if (type === 'array') {
|
|
87
|
+
if (!exactKeys(node, [
|
|
88
|
+
'type',
|
|
89
|
+
'items'
|
|
90
|
+
]) || !plainObject(node.items)) return false;
|
|
91
|
+
pending.push({
|
|
92
|
+
node: node.items,
|
|
93
|
+
depth: depth + 1
|
|
94
|
+
});
|
|
95
|
+
continue;
|
|
96
|
+
}
|
|
97
|
+
if (typeof type !== 'string' || !TERMINAL_BODY_SCALAR_TYPES.has(type) || !exactKeys(node, [
|
|
98
|
+
'type'
|
|
99
|
+
])) return false;
|
|
100
|
+
}
|
|
101
|
+
return plainObject(value) && value.type === 'object';
|
|
102
|
+
}
|
|
103
|
+
function exactKeys(value, allowed) {
|
|
104
|
+
return Object.keys(value).every((key)=>allowed.includes(key));
|
|
105
|
+
}
|
|
106
|
+
export function validateTerminalBodyValue(value, schema) {
|
|
107
|
+
const pending = [
|
|
108
|
+
{
|
|
109
|
+
value,
|
|
110
|
+
schema,
|
|
111
|
+
depth: 0
|
|
112
|
+
}
|
|
113
|
+
];
|
|
114
|
+
let nodes = 0;
|
|
115
|
+
while(pending.length > 0){
|
|
116
|
+
const current = pending.pop();
|
|
117
|
+
if (++nodes > TERMINAL_BODY_VALUE_MAX_NODES || current.depth > TERMINAL_BODY_VALUE_MAX_DEPTH) return false;
|
|
118
|
+
const { schema: node } = current;
|
|
119
|
+
if (node.type === 'object') {
|
|
120
|
+
const record = current.value;
|
|
121
|
+
if (!plainObject(record)) return false;
|
|
122
|
+
const properties = node.properties;
|
|
123
|
+
for (const key of node.required){
|
|
124
|
+
if (!Object.hasOwn(record, key)) return false;
|
|
125
|
+
}
|
|
126
|
+
for (const [key, child] of Object.entries(record)){
|
|
127
|
+
const childSchema = properties[key];
|
|
128
|
+
if (!childSchema) return false;
|
|
129
|
+
pending.push({
|
|
130
|
+
value: child,
|
|
131
|
+
schema: childSchema,
|
|
132
|
+
depth: current.depth + 1
|
|
133
|
+
});
|
|
134
|
+
}
|
|
135
|
+
continue;
|
|
136
|
+
}
|
|
137
|
+
if (node.type === 'array') {
|
|
138
|
+
if (!Array.isArray(current.value)) return false;
|
|
139
|
+
for (const item of current.value)pending.push({
|
|
140
|
+
value: item,
|
|
141
|
+
schema: node.items,
|
|
142
|
+
depth: current.depth + 1
|
|
143
|
+
});
|
|
144
|
+
continue;
|
|
145
|
+
}
|
|
146
|
+
if (node.type === 'string') {
|
|
147
|
+
if (typeof current.value !== 'string') return false;
|
|
148
|
+
continue;
|
|
149
|
+
}
|
|
150
|
+
if (node.type === 'boolean') {
|
|
151
|
+
if (typeof current.value !== 'boolean') return false;
|
|
152
|
+
continue;
|
|
153
|
+
}
|
|
154
|
+
if (node.type === 'null') {
|
|
155
|
+
if (current.value !== null) return false;
|
|
156
|
+
continue;
|
|
157
|
+
}
|
|
158
|
+
if (node.type === 'integer') {
|
|
159
|
+
if (typeof current.value !== 'number' || !Number.isSafeInteger(current.value)) return false;
|
|
160
|
+
continue;
|
|
161
|
+
}
|
|
162
|
+
if (typeof current.value !== 'number' || !Number.isFinite(current.value)) return false;
|
|
163
|
+
}
|
|
164
|
+
return true;
|
|
165
|
+
}
|
|
166
|
+
export function terminalSchemaOutline(schema) {
|
|
167
|
+
const render = (node, depth)=>{
|
|
168
|
+
if (depth > TERMINAL_BODY_SCHEMA_MAX_DEPTH) return '…';
|
|
169
|
+
if (node.type === 'object') {
|
|
170
|
+
const properties = node.properties;
|
|
171
|
+
const required = new Set(node.required);
|
|
172
|
+
const parts = Object.entries(properties).map(([key, child])=>`${required.has(key) ? '' : '?'}${key}:${render(child, depth + 1)}`);
|
|
173
|
+
return `object{${parts.join(',')}}`;
|
|
174
|
+
}
|
|
175
|
+
if (node.type === 'array') return `array<${render(node.items, depth + 1)}>`;
|
|
176
|
+
return node.type;
|
|
177
|
+
};
|
|
178
|
+
return render(schema, 0);
|
|
32
179
|
}
|
|
33
180
|
function invalidTerminal() {
|
|
34
181
|
return {
|
|
@@ -51,10 +198,12 @@ export function parseBenchmarkTerminal(raw, config) {
|
|
|
51
198
|
if (body.length === 0 || body.includes('```')) return invalidTerminal();
|
|
52
199
|
try {
|
|
53
200
|
const value = JSON.parse(body);
|
|
54
|
-
|
|
201
|
+
if (!plainObject(value)) return invalidTerminal();
|
|
202
|
+
if (!validateTerminalBodyValue(value, config.body_schema)) return invalidTerminal();
|
|
203
|
+
return {
|
|
55
204
|
ok: true,
|
|
56
205
|
value
|
|
57
|
-
}
|
|
206
|
+
};
|
|
58
207
|
} catch {
|
|
59
208
|
return invalidTerminal();
|
|
60
209
|
}
|
|
@@ -263,7 +412,7 @@ function requireDisposer(value, capability) {
|
|
|
263
412
|
return value;
|
|
264
413
|
}
|
|
265
414
|
function correctionMessage(mode, projection) {
|
|
266
|
-
const text = mode === 'call' ? `BENCHMARK_CONFORMANCE_CALL: Execute exactly one native ${projection.toolName} action:"call" now. Describing an intended CLI, shell, or Python command does not execute it.` : `BENCHMARK_CONFORMANCE_TERMINAL: Reuse the existing successful tool result. Do not call any tool. Reply only <${projection.terminal.tag}>{
|
|
415
|
+
const text = mode === 'call' ? `BENCHMARK_CONFORMANCE_CALL: Execute exactly one native ${projection.toolName} action:"call" now. Describing an intended CLI, shell, or Python command does not execute it.` : `BENCHMARK_CONFORMANCE_TERMINAL: Reuse the existing successful tool result. Do not call any tool. Reply only <${projection.terminal.tag}> with one JSON object matching exactly ${terminalSchemaOutline(projection.terminal.body_schema)} — no extra keys, no missing keys, exact types.`;
|
|
267
416
|
return createUserMessage({
|
|
268
417
|
content: [
|
|
269
418
|
{
|
|
@@ -279,6 +428,7 @@ function correctionMessage(mode, projection) {
|
|
|
279
428
|
}
|
|
280
429
|
function systemSection(projection) {
|
|
281
430
|
const allowed = projection.allowedTools.join(', ');
|
|
431
|
+
const outline = terminalSchemaOutline(projection.terminal.body_schema);
|
|
282
432
|
return {
|
|
283
433
|
name: 'benchmark:agent-conformance',
|
|
284
434
|
text: [
|
|
@@ -286,7 +436,7 @@ function systemSection(projection) {
|
|
|
286
436
|
`- Translate every task instruction to use a CLI, shell, Python, or agent_env.cli into the native tool ${projection.toolName}; do not merely describe the intended command.`,
|
|
287
437
|
`- Call it with exactly {"action":"call","tool":"<one of: ${allowed}>","arguments":{...}}.`,
|
|
288
438
|
'- action:"tools" is discovery only and does not satisfy the required environment call.',
|
|
289
|
-
`- After a successful tool result, reply only <${projection.terminal.tag}>
|
|
439
|
+
`- After a successful tool result, reply only <${projection.terminal.tag}> with one JSON object matching exactly ${outline} — no extra keys, no missing keys, exact types; no prose or code fence.`,
|
|
290
440
|
'- If a terminal-format correction arrives, reuse the existing result and do not call the tool again.'
|
|
291
441
|
].join('\n')
|
|
292
442
|
};
|
|
@@ -3,7 +3,7 @@ import { getuid } from 'node:process';
|
|
|
3
3
|
import { isAbsolute } from 'node:path';
|
|
4
4
|
import { assertSupportedJsonSchema, validateJsonSchemaValue, ToolArgsError } from '@deepseek-ai/dsh-tools';
|
|
5
5
|
import { validateTerminalOutputConfig } from './benchmark-agent-conformance.js';
|
|
6
|
-
const SCHEMA_VERSION = '
|
|
6
|
+
const SCHEMA_VERSION = 'gotry_benchmark_environment_bridge_v4';
|
|
7
7
|
export const BENCHMARK_TOOL_RESULT_SCHEMA_VERSION = 'gotry_benchmark_tool_result_v1';
|
|
8
8
|
export const BENCHMARK_DOMAIN_RECOVERIES = [
|
|
9
9
|
'none',
|
|
@@ -40,61 +40,6 @@ const INPUT_SCHEMA_TYPES = new Set([
|
|
|
40
40
|
'array',
|
|
41
41
|
'object'
|
|
42
42
|
]);
|
|
43
|
-
function descriptorSchema(tools) {
|
|
44
|
-
return {
|
|
45
|
-
oneOf: [
|
|
46
|
-
{
|
|
47
|
-
type: 'object',
|
|
48
|
-
description: 'List the frozen benchmark tool descriptors.',
|
|
49
|
-
properties: {
|
|
50
|
-
action: {
|
|
51
|
-
type: 'string',
|
|
52
|
-
const: 'tools'
|
|
53
|
-
}
|
|
54
|
-
},
|
|
55
|
-
required: [
|
|
56
|
-
'action'
|
|
57
|
-
],
|
|
58
|
-
additionalProperties: false
|
|
59
|
-
},
|
|
60
|
-
{
|
|
61
|
-
type: 'object',
|
|
62
|
-
description: 'List the frozen bridge error contract.',
|
|
63
|
-
properties: {
|
|
64
|
-
action: {
|
|
65
|
-
type: 'string',
|
|
66
|
-
const: 'errors'
|
|
67
|
-
}
|
|
68
|
-
},
|
|
69
|
-
required: [
|
|
70
|
-
'action'
|
|
71
|
-
],
|
|
72
|
-
additionalProperties: false
|
|
73
|
-
},
|
|
74
|
-
...tools.map((tool)=>({
|
|
75
|
-
type: 'object',
|
|
76
|
-
description: tool.description,
|
|
77
|
-
properties: {
|
|
78
|
-
action: {
|
|
79
|
-
type: 'string',
|
|
80
|
-
const: 'call'
|
|
81
|
-
},
|
|
82
|
-
tool: {
|
|
83
|
-
type: 'string',
|
|
84
|
-
const: tool.name
|
|
85
|
-
},
|
|
86
|
-
arguments: tool.input_schema
|
|
87
|
-
},
|
|
88
|
-
required: [
|
|
89
|
-
'action',
|
|
90
|
-
'tool',
|
|
91
|
-
'arguments'
|
|
92
|
-
],
|
|
93
|
-
additionalProperties: false
|
|
94
|
-
}))
|
|
95
|
-
]
|
|
96
|
-
};
|
|
97
|
-
}
|
|
98
43
|
function plainObject(value) {
|
|
99
44
|
return typeof value === 'object' && value !== null && !Array.isArray(value);
|
|
100
45
|
}
|
|
@@ -445,7 +390,33 @@ export function registerBenchmarkEnvironmentBridge(path, register, subprocess) {
|
|
|
445
390
|
if (!bridge) throw new Error('benchmark environment bridge configuration unavailable');
|
|
446
391
|
if (!subprocess) throw new Error('benchmark environment bridge subprocess unavailable');
|
|
447
392
|
const parameters = deepFreezeJson({
|
|
448
|
-
|
|
393
|
+
type: 'object',
|
|
394
|
+
description: 'Flat benchmark bridge wire: choose an action, then provide the mapped tool and its arguments.',
|
|
395
|
+
properties: {
|
|
396
|
+
action: {
|
|
397
|
+
type: 'string',
|
|
398
|
+
enum: [
|
|
399
|
+
'tools',
|
|
400
|
+
'call',
|
|
401
|
+
'errors'
|
|
402
|
+
],
|
|
403
|
+
description: 'tools=列出可调工具;call=执行工具;errors=拉取错误契约'
|
|
404
|
+
},
|
|
405
|
+
tool: {
|
|
406
|
+
type: 'string',
|
|
407
|
+
enum: bridge.tools.map((item)=>item.name),
|
|
408
|
+
description: 'action=call 时选择的冻结工具名'
|
|
409
|
+
},
|
|
410
|
+
arguments: {
|
|
411
|
+
type: 'object',
|
|
412
|
+
additionalProperties: true,
|
|
413
|
+
description: 'action=call 时传给工具的参数对象'
|
|
414
|
+
}
|
|
415
|
+
},
|
|
416
|
+
required: [
|
|
417
|
+
'action'
|
|
418
|
+
],
|
|
419
|
+
additionalProperties: false
|
|
449
420
|
});
|
|
450
421
|
assertSupportedJsonSchema(parameters);
|
|
451
422
|
const definition = {
|
|
@@ -467,20 +438,36 @@ export function registerBenchmarkEnvironmentBridge(path, register, subprocess) {
|
|
|
467
438
|
const query = args;
|
|
468
439
|
const tool = query.tool;
|
|
469
440
|
const descriptor = bridge.tools.find((item)=>item.name === tool);
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
441
|
+
const keys = Object.keys(query);
|
|
442
|
+
if (query.action === 'tools') {
|
|
443
|
+
if (keys.length !== 1) throw new ToolArgsError([
|
|
444
|
+
'arguments: tools action accepts no tool or arguments'
|
|
445
|
+
]);
|
|
446
|
+
return jsonObject({
|
|
447
|
+
ok: true,
|
|
448
|
+
tools: bridge.tools
|
|
449
|
+
});
|
|
450
|
+
}
|
|
451
|
+
if (query.action === 'errors') {
|
|
452
|
+
if (keys.length !== 1) throw new ToolArgsError([
|
|
453
|
+
'arguments: errors action accepts no tool or arguments'
|
|
454
|
+
]);
|
|
455
|
+
return jsonObject({
|
|
456
|
+
ok: true,
|
|
457
|
+
errors: BRIDGE_ERROR_CONTRACT
|
|
458
|
+
});
|
|
459
|
+
}
|
|
478
460
|
if (query.action !== 'call') {
|
|
479
461
|
return jsonObject({
|
|
480
462
|
ok: false,
|
|
481
463
|
error: 'invalid_action'
|
|
482
464
|
});
|
|
483
465
|
}
|
|
466
|
+
if (keys.length !== 3 || !Object.hasOwn(query, 'tool') || !Object.hasOwn(query, 'arguments')) {
|
|
467
|
+
throw new ToolArgsError([
|
|
468
|
+
'arguments: call action requires exactly tool and arguments'
|
|
469
|
+
]);
|
|
470
|
+
}
|
|
484
471
|
if (typeof tool !== 'string' || !descriptor) {
|
|
485
472
|
return jsonObject({
|
|
486
473
|
ok: false,
|
|
@@ -495,12 +482,7 @@ export function registerBenchmarkEnvironmentBridge(path, register, subprocess) {
|
|
|
495
482
|
}
|
|
496
483
|
const callArguments = query.arguments;
|
|
497
484
|
const argumentViolations = validateJsonSchemaValue(descriptor.input_schema, callArguments, 'arguments');
|
|
498
|
-
if (argumentViolations.length > 0)
|
|
499
|
-
return jsonObject({
|
|
500
|
-
ok: false,
|
|
501
|
-
error: 'invalid_arguments'
|
|
502
|
-
});
|
|
503
|
-
}
|
|
485
|
+
if (argumentViolations.length > 0) throw new ToolArgsError(argumentViolations);
|
|
504
486
|
const serialized = serializedArguments(callArguments);
|
|
505
487
|
if (serialized.reason) return jsonObject({
|
|
506
488
|
ok: false,
|