@danceiny/gotry 0.0.1-rc.19 → 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 +136 -45
- package/bin/gotry-inner.js +11 -6
- package/cordis.gotry-patch.yml +19 -8
- package/dist/capabilities/anything.js +62 -29
- package/dist/capabilities/doctor.js +113 -19
- package/dist/capabilities/hbcli.js +28 -2
- package/dist/capabilities/session/health-watch.js +10 -3
- package/dist/scripts/agent-planning-turn-deadline-e2e.js +2 -1
- package/dist/scripts/anything-tests.js +70 -21
- package/dist/scripts/benchmark-environment-bridge-e2e.js +178 -33
- package/dist/scripts/benchmark-environment-bridge-tests.js +1480 -271
- 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 +30 -3
- package/dist/scripts/doctor-tests.js +55 -1
- package/dist/scripts/hbcli-e2e-tests.js +1 -1
- package/dist/scripts/hbcli-tests.js +38 -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 +187 -17
- package/dist/src/benchmark-environment-bridge.js +287 -102
- package/dist/src/booking-surface/dsh-planner.js +24 -8
- package/dist/src/index.js +5 -2
- package/extension/manifest.json +2 -2
- package/package.json +3 -1
- package/ts/capabilities/anything.ts +89 -38
- package/ts/capabilities/hbcli.ts +53 -4
- package/ts/capabilities/session/health-watch.ts +9 -2
- package/ts/dsh-runtime/vendor/README.md +60 -0
- package/ts/dsh-runtime/vendor/dsh-map-tools/LICENSE +21 -0
- package/ts/dsh-runtime/vendor/dsh-map-tools/README.en.md +200 -0
- package/ts/dsh-runtime/vendor/dsh-map-tools/README.md +202 -0
- package/ts/dsh-runtime/vendor/dsh-map-tools/client/client.js +216 -0
- package/ts/dsh-runtime/vendor/dsh-map-tools/cordis.patch.yml +5 -0
- package/ts/dsh-runtime/vendor/dsh-map-tools/lib/clients/amap.js +371 -0
- package/ts/dsh-runtime/vendor/dsh-map-tools/lib/clients/nominatim.js +63 -0
- package/ts/dsh-runtime/vendor/dsh-map-tools/lib/clients/osrm.js +56 -0
- package/ts/dsh-runtime/vendor/dsh-map-tools/lib/clients/photon.js +55 -0
- package/ts/dsh-runtime/vendor/dsh-map-tools/lib/config-file.js +87 -0
- package/ts/dsh-runtime/vendor/dsh-map-tools/lib/config-route.js +126 -0
- package/ts/dsh-runtime/vendor/dsh-map-tools/lib/config.js +16 -0
- package/ts/dsh-runtime/vendor/dsh-map-tools/lib/index.js +109 -0
- package/ts/dsh-runtime/vendor/dsh-map-tools/lib/settings-ns.js +27 -0
- package/ts/dsh-runtime/vendor/dsh-map-tools/lib/tools/geocode.js +127 -0
- package/ts/dsh-runtime/vendor/dsh-map-tools/lib/tools/poi.js +84 -0
- package/ts/dsh-runtime/vendor/dsh-map-tools/lib/tools/routes.js +170 -0
- package/ts/dsh-runtime/vendor/dsh-map-tools/lib/types/clients/amap.d.ts +53 -0
- package/ts/dsh-runtime/vendor/dsh-map-tools/lib/types/clients/nominatim.d.ts +17 -0
- package/ts/dsh-runtime/vendor/dsh-map-tools/lib/types/clients/osrm.d.ts +15 -0
- package/ts/dsh-runtime/vendor/dsh-map-tools/lib/types/clients/photon.d.ts +22 -0
- package/ts/dsh-runtime/vendor/dsh-map-tools/lib/types/config-file.d.ts +33 -0
- package/ts/dsh-runtime/vendor/dsh-map-tools/lib/types/config-route.d.ts +20 -0
- package/ts/dsh-runtime/vendor/dsh-map-tools/lib/types/config.d.ts +23 -0
- package/ts/dsh-runtime/vendor/dsh-map-tools/lib/types/index.d.ts +18 -0
- package/ts/dsh-runtime/vendor/dsh-map-tools/lib/types/settings-ns.d.ts +14 -0
- package/ts/dsh-runtime/vendor/dsh-map-tools/lib/types/tools/geocode.d.ts +11 -0
- package/ts/dsh-runtime/vendor/dsh-map-tools/lib/types/tools/poi.d.ts +9 -0
- package/ts/dsh-runtime/vendor/dsh-map-tools/lib/types/tools/routes.d.ts +16 -0
- package/ts/dsh-runtime/vendor/dsh-map-tools/lib/types/types.d.ts +55 -0
- package/ts/dsh-runtime/vendor/dsh-map-tools/lib/types.js +16 -0
- package/ts/dsh-runtime/vendor/dsh-map-tools/package.json +91 -0
- package/ts/package.json +17 -1
- package/ts/scripts/map-tools-vendor-package-proof.ts +268 -0
- package/ts/src/benchmark-agent-conformance.ts +175 -13
- package/ts/src/benchmark-environment-bridge.ts +220 -66
- package/ts/src/booking-surface/dsh-planner.ts +25 -13
- package/ts/src/index.ts +4 -2
- package/ts/src/turn-deadline.ts +4 -0
|
@@ -0,0 +1,268 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Package proof for issue #202.
|
|
3
|
+
*
|
|
4
|
+
* The full suite passes the executable from its clean tarball install; focused
|
|
5
|
+
* runs pack/unpack locally and attach the already-installed root closure. Both
|
|
6
|
+
* paths load the plugin from the artifact, never from the checkout vendor path.
|
|
7
|
+
* No map provider or paid endpoint is contacted; inline-coordinate geocoding is
|
|
8
|
+
* asserted with fetch disabled.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import { execFileSync } from 'node:child_process'
|
|
12
|
+
import { createHash } from 'node:crypto'
|
|
13
|
+
import { existsSync, mkdirSync, mkdtempSync, readFileSync, readdirSync, realpathSync, rmSync, symlinkSync } from 'node:fs'
|
|
14
|
+
import { createRequire } from 'node:module'
|
|
15
|
+
import { basename, dirname, isAbsolute, join, resolve } from 'node:path'
|
|
16
|
+
import { fileURLToPath, pathToFileURL } from 'node:url'
|
|
17
|
+
import assert from 'node:assert/strict'
|
|
18
|
+
|
|
19
|
+
const repoRoot = resolve(dirname(fileURLToPath(import.meta.url)), '../..')
|
|
20
|
+
const upstreamLicenseSha256 = 'b6bbb0c73a02cf8d2c304e9f208b41c32f0a810fabe366986e2b14ac3338f618'
|
|
21
|
+
const upstreamVendorFileCount = 32
|
|
22
|
+
const upstreamVendorAggregateSha256 = 'd82a38adace8bfe574dcecb4222da5c994a1b9e8792f78dc2dad3a7f50c9925a'
|
|
23
|
+
const expectedTools = [
|
|
24
|
+
'map_bicycling_route',
|
|
25
|
+
'map_driving_route',
|
|
26
|
+
'map_geocode',
|
|
27
|
+
'map_poi_search',
|
|
28
|
+
'map_reverse_geocode',
|
|
29
|
+
'map_transit_route',
|
|
30
|
+
'map_walking_route',
|
|
31
|
+
]
|
|
32
|
+
|
|
33
|
+
function sha256(path: string): string {
|
|
34
|
+
return createHash('sha256').update(readFileSync(path)).digest('hex')
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function vendorAggregate(root: string): { count: number; sha256: string } {
|
|
38
|
+
const files: string[] = []
|
|
39
|
+
const walk = (directory: string) => {
|
|
40
|
+
for (const entry of readdirSync(directory, { withFileTypes: true })) {
|
|
41
|
+
const path = join(directory, entry.name)
|
|
42
|
+
if (entry.isDirectory()) walk(path)
|
|
43
|
+
else files.push(path.slice(root.length + 1).replaceAll('\\', '/'))
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
walk(root)
|
|
47
|
+
files.sort()
|
|
48
|
+
const digest = createHash('sha256')
|
|
49
|
+
for (const relative of files) {
|
|
50
|
+
digest.update(relative)
|
|
51
|
+
digest.update('\0')
|
|
52
|
+
digest.update(readFileSync(join(root, relative)))
|
|
53
|
+
digest.update('\0')
|
|
54
|
+
}
|
|
55
|
+
return { count: files.length, sha256: digest.digest('hex') }
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function assertAlpha3LockClosure(): { packages: number; version: string } {
|
|
59
|
+
const manifest = JSON.parse(readFileSync(join(repoRoot, 'ts/package.json'), 'utf8')) as {
|
|
60
|
+
dependencies?: Record<string, string>
|
|
61
|
+
}
|
|
62
|
+
assert.equal(manifest.dependencies?.['dsh-map-tools'], undefined, 'external map-tools dependency must stay removed')
|
|
63
|
+
|
|
64
|
+
const lock = JSON.parse(readFileSync(join(repoRoot, 'ts/package-lock.json'), 'utf8')) as {
|
|
65
|
+
packages?: Record<string, { version?: string; dependencies?: Record<string, string> }>
|
|
66
|
+
}
|
|
67
|
+
assert.equal(lock.packages?.['']?.dependencies?.['dsh-map-tools'], undefined, 'lock root must not resolve external map-tools')
|
|
68
|
+
assert.equal(lock.packages?.['node_modules/dsh-map-tools'], undefined, 'lock must not carry external map-tools')
|
|
69
|
+
const dshEntries = Object.entries(lock.packages ?? {}).filter(([path]) =>
|
|
70
|
+
/(?:^|\/)node_modules\/@deepseek-ai\/(?:dsh|dsh-[^/]+)$/.test(path),
|
|
71
|
+
)
|
|
72
|
+
assert.ok(dshEntries.length >= 10, 'expected dsh-tools peer closure in the ts lock')
|
|
73
|
+
for (const [path, entry] of dshEntries) {
|
|
74
|
+
assert.equal(entry.version, '0.1.2-alpha.3', `${path} drifted outside the alpha.3 closure`)
|
|
75
|
+
}
|
|
76
|
+
return { packages: dshEntries.length, version: '0.1.2-alpha.3' }
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
function findPackageRoot(entry: string): string {
|
|
80
|
+
let cursor = dirname(entry)
|
|
81
|
+
for (;;) {
|
|
82
|
+
const manifestPath = join(cursor, 'package.json')
|
|
83
|
+
if (existsSync(manifestPath)) {
|
|
84
|
+
const manifest = JSON.parse(readFileSync(manifestPath, 'utf8')) as { name?: string }
|
|
85
|
+
if (manifest.name === '@danceiny/gotry') return cursor
|
|
86
|
+
}
|
|
87
|
+
const parent = dirname(cursor)
|
|
88
|
+
if (parent === cursor) throw new Error(`cannot locate @danceiny/gotry package root from ${entry}`)
|
|
89
|
+
cursor = parent
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
function assertBinTargetsPackage(binPath: string, expectedTarget: string): void {
|
|
94
|
+
const resolvedExpectedTarget = realpathSync(expectedTarget)
|
|
95
|
+
if (realpathSync(binPath) === resolvedExpectedTarget) return
|
|
96
|
+
|
|
97
|
+
// pnpm writes an executable shell shim instead of a symlink. Its final
|
|
98
|
+
// machine-readable marker is safer to verify than accepting any occurrence
|
|
99
|
+
// of the package path in an otherwise unrelated executable.
|
|
100
|
+
const shim = readFileSync(binPath, 'utf8')
|
|
101
|
+
assert.ok(Buffer.byteLength(shim, 'utf8') <= 32_768, 'clean-install gotry shim is unexpectedly large')
|
|
102
|
+
const lines = shim.trimEnd().split(/\r?\n/)
|
|
103
|
+
const markers = lines.filter(line => line.startsWith('# cmd-shim-target='))
|
|
104
|
+
assert.equal(markers.length, 1, 'pnpm gotry shim must contain exactly one target marker')
|
|
105
|
+
assert.equal(lines.at(-1), markers[0], 'pnpm gotry shim target marker must be the final line')
|
|
106
|
+
const markerTarget = markers[0].slice('# cmd-shim-target='.length)
|
|
107
|
+
assert.ok(isAbsolute(markerTarget), 'pnpm gotry shim target must be absolute')
|
|
108
|
+
assert.equal(
|
|
109
|
+
realpathSync(markerTarget),
|
|
110
|
+
resolvedExpectedTarget,
|
|
111
|
+
'GOTRY_MAP_TOOLS_E2E_BIN shim must target this installed GoTry package',
|
|
112
|
+
)
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
function prepareProofPackage(cleanInstalledBin?: string): { packageRoot: string; proofRoot?: string } {
|
|
116
|
+
if (cleanInstalledBin) {
|
|
117
|
+
const binPath = resolve(cleanInstalledBin)
|
|
118
|
+
assert.equal(basename(binPath), 'gotry', 'clean-install executable must use the gotry bin name')
|
|
119
|
+
assert.equal(basename(dirname(binPath)), '.bin', 'clean-install executable must come from node_modules/.bin')
|
|
120
|
+
assert.equal(basename(dirname(dirname(binPath))), 'node_modules', 'clean-install executable must come from a consumer install')
|
|
121
|
+
const consumerRoot = dirname(dirname(dirname(binPath)))
|
|
122
|
+
const packageMain = createRequire(join(consumerRoot, 'package.json')).resolve('@danceiny/gotry')
|
|
123
|
+
const packageRoot = findPackageRoot(packageMain)
|
|
124
|
+
const installedManifest = JSON.parse(readFileSync(join(packageRoot, 'package.json'), 'utf8')) as {
|
|
125
|
+
name?: string
|
|
126
|
+
bin?: string | Record<string, string>
|
|
127
|
+
}
|
|
128
|
+
assert.equal(installedManifest.name, '@danceiny/gotry', 'clean-install executable must belong to GoTry')
|
|
129
|
+
const declaredBin = typeof installedManifest.bin === 'string'
|
|
130
|
+
? installedManifest.bin
|
|
131
|
+
: installedManifest.bin?.gotry
|
|
132
|
+
assert.equal(declaredBin, 'bin/gotry-inner.js', 'installed package must declare the production gotry bin')
|
|
133
|
+
assertBinTargetsPackage(binPath, join(packageRoot, declaredBin))
|
|
134
|
+
return { packageRoot }
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
const proofRoot = mkdtempSync(join(repoRoot, '.tmp-map-tools-proof-'))
|
|
138
|
+
try {
|
|
139
|
+
const packOutput = execFileSync('npm', [
|
|
140
|
+
'pack', '--ignore-scripts', '--json', '--pack-destination', proofRoot,
|
|
141
|
+
], { cwd: repoRoot, encoding: 'utf8' })
|
|
142
|
+
const packInfo = JSON.parse(packOutput) as Array<{ filename: string }>
|
|
143
|
+
assert.equal(packInfo.length, 1, 'npm pack must produce one artifact')
|
|
144
|
+
const tarball = join(proofRoot, packInfo[0].filename)
|
|
145
|
+
assert.ok(existsSync(tarball), `packed artifact missing: ${tarball}`)
|
|
146
|
+
const unpackRoot = join(proofRoot, 'unpacked')
|
|
147
|
+
mkdirSync(unpackRoot)
|
|
148
|
+
execFileSync('tar', ['-xzf', tarball, '-C', unpackRoot])
|
|
149
|
+
const packageRoot = join(unpackRoot, 'package')
|
|
150
|
+
symlinkSync(join(repoRoot, 'node_modules'), join(packageRoot, 'node_modules'), process.platform === 'win32' ? 'junction' : 'dir')
|
|
151
|
+
return { packageRoot, proofRoot }
|
|
152
|
+
} catch (error) {
|
|
153
|
+
rmSync(proofRoot, { recursive: true, force: true })
|
|
154
|
+
throw error
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
const cleanInstalledBin = process.env.GOTRY_MAP_TOOLS_E2E_BIN
|
|
159
|
+
const { packageRoot, proofRoot } = prepareProofPackage(cleanInstalledBin)
|
|
160
|
+
|
|
161
|
+
try {
|
|
162
|
+
const lockedDsh = assertAlpha3LockClosure()
|
|
163
|
+
const vendorRoot = join(packageRoot, 'ts/dsh-runtime/vendor/dsh-map-tools')
|
|
164
|
+
const requiredFiles = [
|
|
165
|
+
'LICENSE',
|
|
166
|
+
'package.json',
|
|
167
|
+
'cordis.patch.yml',
|
|
168
|
+
'lib/index.js',
|
|
169
|
+
'lib/settings-ns.js',
|
|
170
|
+
'lib/config.js',
|
|
171
|
+
'lib/tools/geocode.js',
|
|
172
|
+
'lib/tools/routes.js',
|
|
173
|
+
'lib/tools/poi.js',
|
|
174
|
+
]
|
|
175
|
+
for (const relative of requiredFiles) {
|
|
176
|
+
assert.ok(existsSync(join(vendorRoot, relative)), `required vendor file missing: ${relative}`)
|
|
177
|
+
}
|
|
178
|
+
const vendorTree = vendorAggregate(vendorRoot)
|
|
179
|
+
assert.equal(vendorTree.count, upstreamVendorFileCount, 'vendored payload file count drifted')
|
|
180
|
+
assert.equal(vendorTree.sha256, upstreamVendorAggregateSha256, 'vendored payload aggregate drifted')
|
|
181
|
+
const vendorPackage = JSON.parse(readFileSync(join(vendorRoot, 'package.json'), 'utf8')) as {
|
|
182
|
+
name?: string
|
|
183
|
+
version?: string
|
|
184
|
+
license?: string
|
|
185
|
+
}
|
|
186
|
+
assert.equal(vendorPackage.name, 'dsh-map-tools')
|
|
187
|
+
assert.equal(vendorPackage.version, '0.5.1')
|
|
188
|
+
assert.equal(vendorPackage.license, 'MIT')
|
|
189
|
+
assert.equal(sha256(join(vendorRoot, 'LICENSE')), upstreamLicenseSha256, 'upstream MIT license changed')
|
|
190
|
+
|
|
191
|
+
const packageRequire = createRequire(join(packageRoot, 'package.json'))
|
|
192
|
+
const dshTools = JSON.parse(readFileSync(packageRequire.resolve('@deepseek-ai/dsh-tools/package.json'), 'utf8')) as { version: string }
|
|
193
|
+
const dshSettings = JSON.parse(readFileSync(packageRequire.resolve('@deepseek-ai/dsh-settings/package.json'), 'utf8')) as { version: string }
|
|
194
|
+
assert.equal(dshTools.version, '0.1.2-alpha.3')
|
|
195
|
+
assert.equal(dshSettings.version, '0.1.2-alpha.3')
|
|
196
|
+
const settingsApi = await import(pathToFileURL(packageRequire.resolve('@deepseek-ai/dsh-settings')).href) as {
|
|
197
|
+
SettingsProvider?: { prototype?: { installSection?: unknown } }
|
|
198
|
+
}
|
|
199
|
+
assert.equal(
|
|
200
|
+
typeof settingsApi.SettingsProvider?.prototype?.installSection,
|
|
201
|
+
'function',
|
|
202
|
+
'real alpha.3 SettingsProvider.installSection API must be present',
|
|
203
|
+
)
|
|
204
|
+
|
|
205
|
+
const plugin = await import(pathToFileURL(join(vendorRoot, 'lib/index.js')).href)
|
|
206
|
+
const registered: Array<Record<string, unknown>> = []
|
|
207
|
+
const settingsCalls: unknown[][] = []
|
|
208
|
+
const context = {
|
|
209
|
+
tools: {
|
|
210
|
+
register(tool: Record<string, unknown>) {
|
|
211
|
+
registered.push(tool)
|
|
212
|
+
return () => undefined
|
|
213
|
+
},
|
|
214
|
+
},
|
|
215
|
+
effect(effect: () => (() => void) | void) {
|
|
216
|
+
effect()
|
|
217
|
+
},
|
|
218
|
+
inject(dependencies: string[], callback: (scope: Record<string, unknown>) => void) {
|
|
219
|
+
if (dependencies.includes('settings')) {
|
|
220
|
+
callback({ settings: { installSection: (...args: unknown[]) => { settingsCalls.push(args) } } })
|
|
221
|
+
} else if (dependencies.includes('webServer')) {
|
|
222
|
+
callback({ webServer: { register: () => undefined } })
|
|
223
|
+
} else {
|
|
224
|
+
throw new Error(`unexpected dependency injection: ${dependencies.join(',')}`)
|
|
225
|
+
}
|
|
226
|
+
},
|
|
227
|
+
}
|
|
228
|
+
plugin.apply(context, {
|
|
229
|
+
provider: 'osm', amapKey: '', timeoutMs: 1000, maxQps: 2,
|
|
230
|
+
defaultMode: 'driving', language: 'zh',
|
|
231
|
+
})
|
|
232
|
+
assert.deepEqual(registered.map(tool => tool.name).sort(), expectedTools, 'exactly seven map tools must register')
|
|
233
|
+
assert.equal(settingsCalls.length, 1, 'alpha.3 settings section must be installed once')
|
|
234
|
+
assert.equal(settingsCalls[0][1], 'dsh-map-tools', 'settings namespace must be stable')
|
|
235
|
+
assert.equal(typeof settingsCalls[0][4], 'object', 'alpha.3 installSection hooks must be supplied')
|
|
236
|
+
|
|
237
|
+
const originalFetch = globalThis.fetch
|
|
238
|
+
globalThis.fetch = (async () => { throw new Error('network forbidden by package proof') }) as typeof fetch
|
|
239
|
+
try {
|
|
240
|
+
const geocode = registered.find(tool => tool.name === 'map_geocode')
|
|
241
|
+
assert.ok(geocode, 'map_geocode must be registered')
|
|
242
|
+
const result = await (geocode.execute as (args: unknown, execution: unknown) => Promise<unknown>)(
|
|
243
|
+
{ address: '116.397428,39.90923' },
|
|
244
|
+
{ signal: new AbortController().signal },
|
|
245
|
+
) as { provider?: string; location?: number[] }
|
|
246
|
+
assert.equal(result.provider, 'inline')
|
|
247
|
+
assert.deepEqual(result.location, [116.397428, 39.90923])
|
|
248
|
+
} finally {
|
|
249
|
+
globalThis.fetch = originalFetch
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
console.log(JSON.stringify({
|
|
253
|
+
proof: 'map-tools-vendor-package',
|
|
254
|
+
status: 'PASS',
|
|
255
|
+
artifactMode: cleanInstalledBin ? 'clean-installed-tarball' : 'packed-unpacked-focused',
|
|
256
|
+
vendored: {
|
|
257
|
+
sourcePackage: 'dsh-map-tools@0.5.1',
|
|
258
|
+
licenseSha256: upstreamLicenseSha256, vendorFileCount: vendorTree.count,
|
|
259
|
+
vendorAggregateSha256: vendorTree.sha256,
|
|
260
|
+
},
|
|
261
|
+
artifactVendor: vendorRoot,
|
|
262
|
+
dshClosure: { tools: dshTools.version, settings: dshSettings.version, ...lockedDsh },
|
|
263
|
+
tools: registered.map(tool => tool.name).sort(),
|
|
264
|
+
inlineCoordinate: { provider: 'inline', network: 'forbidden' },
|
|
265
|
+
}, null, 2))
|
|
266
|
+
} finally {
|
|
267
|
+
if (proofRoot) rmSync(proofRoot, { recursive: true, force: true })
|
|
268
|
+
}
|
|
@@ -22,10 +22,38 @@ export const BENCHMARK_CONFORMANCE_STATE_UNAVAILABLE = 'BENCHMARK_CONFORMANCE_ST
|
|
|
22
22
|
|
|
23
23
|
const IDENTIFIER = /^[A-Za-z][A-Za-z0-9_.-]*$/
|
|
24
24
|
const MAX_TERMINAL_BYTES = 1024 * 1024
|
|
25
|
+
const BENCHMARK_TOOL_RESULT_SCHEMA_VERSION = 'gotry_benchmark_tool_result_v1'
|
|
26
|
+
const BENCHMARK_DOMAIN_RECOVERIES = new Set(['none', 'retry_same', 'revise_arguments', 'choose_alternative'])
|
|
27
|
+
|
|
28
|
+
// Round 12(issue #215):terminal body schema 是「无数据值的 closed 结构合同」——
|
|
29
|
+
// 只允许结构关键字(type/properties/required/additionalProperties/items),enum/
|
|
30
|
+
// const/example/default 等可夹带数据值的注解面一律禁止,对象必须 additionalProperties:
|
|
31
|
+
// false,大小/深度/节点数有界。校验语义 fail-closed:不补键、不删多余键、不转类型。
|
|
32
|
+
const TERMINAL_BODY_SCHEMA_MAX_BYTES = 16 * 1024
|
|
33
|
+
const TERMINAL_BODY_SCHEMA_MAX_DEPTH = 8
|
|
34
|
+
const TERMINAL_BODY_SCHEMA_MAX_NODES = 256
|
|
35
|
+
const TERMINAL_BODY_SCHEMA_MAX_PROPERTIES = 256
|
|
36
|
+
const TERMINAL_BODY_SCALAR_TYPES = new Set(['string', 'number', 'integer', 'boolean', 'null'])
|
|
37
|
+
const TERMINAL_BODY_VALUE_MAX_NODES = 10_000
|
|
38
|
+
const TERMINAL_BODY_VALUE_MAX_DEPTH = 24
|
|
39
|
+
|
|
40
|
+
/** Structural-only, closed JSON schema for the terminal body (no data-bearing keywords). */
|
|
41
|
+
export type TerminalBodySchema = {
|
|
42
|
+
type: 'object'
|
|
43
|
+
properties: Record<string, TerminalBodySchema>
|
|
44
|
+
required: string[]
|
|
45
|
+
additionalProperties: false
|
|
46
|
+
} | {
|
|
47
|
+
type: 'array'
|
|
48
|
+
items: TerminalBodySchema
|
|
49
|
+
} | {
|
|
50
|
+
type: 'string' | 'number' | 'integer' | 'boolean' | 'null'
|
|
51
|
+
}
|
|
25
52
|
|
|
26
53
|
export interface TerminalOutputConfig {
|
|
27
54
|
tag: string
|
|
28
55
|
max_bytes: number
|
|
56
|
+
body_schema: TerminalBodySchema
|
|
29
57
|
}
|
|
30
58
|
|
|
31
59
|
export type TerminalOutputValue =
|
|
@@ -46,6 +74,8 @@ interface TurnState {
|
|
|
46
74
|
retryCount: number
|
|
47
75
|
retryMode: RetryMode
|
|
48
76
|
successfulResultStep?: number
|
|
77
|
+
domainOutcomeStep?: number
|
|
78
|
+
lastBridgeResponseStep?: number
|
|
49
79
|
bridgeCallFailed: boolean
|
|
50
80
|
bridgeFailureCode?: string
|
|
51
81
|
retryRedispatchAttempted: boolean
|
|
@@ -76,13 +106,113 @@ function plainObject(value: unknown): value is Record<string, unknown> {
|
|
|
76
106
|
export function validateTerminalOutputConfig(value: unknown): value is TerminalOutputConfig {
|
|
77
107
|
if (!plainObject(value)) return false
|
|
78
108
|
const keys = Object.keys(value).sort()
|
|
79
|
-
return JSON.stringify(keys) === JSON.stringify(['max_bytes', 'tag'])
|
|
109
|
+
return JSON.stringify(keys) === JSON.stringify(['body_schema', 'max_bytes', 'tag'])
|
|
80
110
|
&& typeof value.tag === 'string'
|
|
81
111
|
&& IDENTIFIER.test(value.tag)
|
|
82
112
|
&& typeof value.max_bytes === 'number'
|
|
83
113
|
&& Number.isInteger(value.max_bytes)
|
|
84
114
|
&& value.max_bytes >= 1
|
|
85
115
|
&& value.max_bytes <= MAX_TERMINAL_BYTES
|
|
116
|
+
&& validateTerminalBodySchema(value.body_schema)
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/** Accept only the closed structural dialect: exact keyword sets per node, bounded size. */
|
|
120
|
+
export function validateTerminalBodySchema(value: unknown): value is TerminalBodySchema {
|
|
121
|
+
let serialized: string
|
|
122
|
+
try {
|
|
123
|
+
serialized = JSON.stringify(value)
|
|
124
|
+
} catch {
|
|
125
|
+
return false
|
|
126
|
+
}
|
|
127
|
+
if (typeof serialized !== 'string' || Buffer.byteLength(serialized, 'utf8') > TERMINAL_BODY_SCHEMA_MAX_BYTES) return false
|
|
128
|
+
const pending: Array<{ node: unknown; depth: number }> = [{ node: value, depth: 0 }]
|
|
129
|
+
const seen = new Set<object>()
|
|
130
|
+
let nodes = 0
|
|
131
|
+
let properties = 0
|
|
132
|
+
while (pending.length > 0) {
|
|
133
|
+
const { node, depth } = pending.pop()!
|
|
134
|
+
if (!plainObject(node) || seen.has(node) || ++nodes > TERMINAL_BODY_SCHEMA_MAX_NODES || depth > TERMINAL_BODY_SCHEMA_MAX_DEPTH) return false
|
|
135
|
+
seen.add(node)
|
|
136
|
+
const type = node.type
|
|
137
|
+
if (type === 'object') {
|
|
138
|
+
if (!exactKeys(node, ['type', 'properties', 'required', 'additionalProperties'])
|
|
139
|
+
|| !plainObject(node.properties)
|
|
140
|
+
|| !Array.isArray(node.required)
|
|
141
|
+
|| node.additionalProperties !== false) return false
|
|
142
|
+
const entries = Object.entries(node.properties)
|
|
143
|
+
properties += entries.length
|
|
144
|
+
if (properties > TERMINAL_BODY_SCHEMA_MAX_PROPERTIES
|
|
145
|
+
|| entries.some(([key, child]) => key.length === 0 || key.length > 64 || !plainObject(child))) return false
|
|
146
|
+
if (node.required.length > entries.length
|
|
147
|
+
|| new Set(node.required).size !== node.required.length
|
|
148
|
+
|| node.required.some(key => typeof key !== 'string' || !Object.hasOwn(node.properties as Record<string, unknown>, key))) return false
|
|
149
|
+
for (const [, child] of entries) pending.push({ node: child, depth: depth + 1 })
|
|
150
|
+
continue
|
|
151
|
+
}
|
|
152
|
+
if (type === 'array') {
|
|
153
|
+
if (!exactKeys(node, ['type', 'items']) || !plainObject(node.items)) return false
|
|
154
|
+
pending.push({ node: node.items, depth: depth + 1 })
|
|
155
|
+
continue
|
|
156
|
+
}
|
|
157
|
+
if (typeof type !== 'string' || !TERMINAL_BODY_SCALAR_TYPES.has(type) || !exactKeys(node, ['type'])) return false
|
|
158
|
+
}
|
|
159
|
+
return plainObject(value) && value.type === 'object'
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
function exactKeys(value: Record<string, unknown>, allowed: readonly string[]): boolean {
|
|
163
|
+
return Object.keys(value).every(key => allowed.includes(key))
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
/** Strict structural validation: exact keys, exact types, no coercion, no autofix. */
|
|
167
|
+
export function validateTerminalBodyValue(value: unknown, schema: TerminalBodySchema): boolean {
|
|
168
|
+
const pending: Array<{ value: unknown; schema: TerminalBodySchema; depth: number }> = [{ value, schema, depth: 0 }]
|
|
169
|
+
let nodes = 0
|
|
170
|
+
while (pending.length > 0) {
|
|
171
|
+
const current = pending.pop()!
|
|
172
|
+
if (++nodes > TERMINAL_BODY_VALUE_MAX_NODES || current.depth > TERMINAL_BODY_VALUE_MAX_DEPTH) return false
|
|
173
|
+
const { schema: node } = current
|
|
174
|
+
if (node.type === 'object') {
|
|
175
|
+
const record = current.value
|
|
176
|
+
if (!plainObject(record)) return false
|
|
177
|
+
const properties = node.properties as Record<string, TerminalBodySchema>
|
|
178
|
+
for (const key of node.required) {
|
|
179
|
+
if (!Object.hasOwn(record, key)) return false
|
|
180
|
+
}
|
|
181
|
+
for (const [key, child] of Object.entries(record)) {
|
|
182
|
+
const childSchema = properties[key]
|
|
183
|
+
if (!childSchema) return false
|
|
184
|
+
pending.push({ value: child, schema: childSchema, depth: current.depth + 1 })
|
|
185
|
+
}
|
|
186
|
+
continue
|
|
187
|
+
}
|
|
188
|
+
if (node.type === 'array') {
|
|
189
|
+
if (!Array.isArray(current.value)) return false
|
|
190
|
+
for (const item of current.value) pending.push({ value: item, schema: node.items, depth: current.depth + 1 })
|
|
191
|
+
continue
|
|
192
|
+
}
|
|
193
|
+
if (node.type === 'string') { if (typeof current.value !== 'string') return false; continue }
|
|
194
|
+
if (node.type === 'boolean') { if (typeof current.value !== 'boolean') return false; continue }
|
|
195
|
+
if (node.type === 'null') { if (current.value !== null) return false; continue }
|
|
196
|
+
if (node.type === 'integer') { if (typeof current.value !== 'number' || !Number.isSafeInteger(current.value)) return false; continue }
|
|
197
|
+
if (typeof current.value !== 'number' || !Number.isFinite(current.value)) return false
|
|
198
|
+
}
|
|
199
|
+
return true
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
/** Deterministic single-source structural outline projected into system prompt and correction alike. */
|
|
203
|
+
export function terminalSchemaOutline(schema: TerminalBodySchema): string {
|
|
204
|
+
const render = (node: TerminalBodySchema, depth: number): string => {
|
|
205
|
+
if (depth > TERMINAL_BODY_SCHEMA_MAX_DEPTH) return '…'
|
|
206
|
+
if (node.type === 'object') {
|
|
207
|
+
const properties = node.properties as Record<string, TerminalBodySchema>
|
|
208
|
+
const required = new Set(node.required)
|
|
209
|
+
const parts = Object.entries(properties).map(([key, child]) => `${required.has(key) ? '' : '?'}${key}:${render(child, depth + 1)}`)
|
|
210
|
+
return `object{${parts.join(',')}}`
|
|
211
|
+
}
|
|
212
|
+
if (node.type === 'array') return `array<${render(node.items, depth + 1)}>`
|
|
213
|
+
return node.type
|
|
214
|
+
}
|
|
215
|
+
return render(schema, 0)
|
|
86
216
|
}
|
|
87
217
|
|
|
88
218
|
function invalidTerminal(): TerminalOutputValue {
|
|
@@ -112,7 +242,11 @@ export function parseBenchmarkTerminal(raw: string, config: TerminalOutputConfig
|
|
|
112
242
|
|
|
113
243
|
try {
|
|
114
244
|
const value: unknown = JSON.parse(body)
|
|
115
|
-
|
|
245
|
+
if (!plainObject(value)) return invalidTerminal()
|
|
246
|
+
// Round 12(#215):同一份 closed body schema 在接受终态前 fail-closed 校验——
|
|
247
|
+
// 结构不合法的终态永不进入正式计分链,不做任何 autofix。
|
|
248
|
+
if (!validateTerminalBodyValue(value, config.body_schema)) return invalidTerminal()
|
|
249
|
+
return { ok: true, value }
|
|
116
250
|
} catch {
|
|
117
251
|
return invalidTerminal()
|
|
118
252
|
}
|
|
@@ -150,7 +284,9 @@ function assistantText(data: Record<string, unknown>): { text: string; interrupt
|
|
|
150
284
|
return { text, interrupted: data.interrupted === true }
|
|
151
285
|
}
|
|
152
286
|
|
|
153
|
-
|
|
287
|
+
type BridgeResultStatus = { kind: 'result' } | { kind: 'domain' } | { kind: 'failure'; code: string }
|
|
288
|
+
|
|
289
|
+
function bridgeResultStatus(data: Record<string, unknown>, callId: string): BridgeResultStatus | undefined {
|
|
154
290
|
if (!plainObject(data.message) || !plainObject(data.message.source)) return undefined
|
|
155
291
|
if (data.message.source.callId !== callId || !Array.isArray(data.message.content)) return undefined
|
|
156
292
|
const block = data.message.content.find(candidate => plainObject(candidate)
|
|
@@ -164,8 +300,28 @@ function bridgeResultStatus(data: Record<string, unknown>, callId: string): { ok
|
|
|
164
300
|
try {
|
|
165
301
|
const parsed: unknown = JSON.parse(text)
|
|
166
302
|
if (!plainObject(parsed)) return undefined
|
|
167
|
-
|
|
303
|
+
const keys = Object.keys(parsed).sort().join(',')
|
|
304
|
+
if (parsed.ok === true && keys === 'ok,result' && (plainObject(parsed.result) || Array.isArray(parsed.result))) return { kind: 'result' }
|
|
305
|
+
if (parsed.ok === true && keys === 'ok,outcome' && plainObject(parsed.outcome)) {
|
|
306
|
+
const outcomeKeys = Object.keys(parsed.outcome).sort().join(',')
|
|
307
|
+
if (outcomeKeys === 'code,recovery,schema_version,status'
|
|
308
|
+
&& parsed.outcome.schema_version === BENCHMARK_TOOL_RESULT_SCHEMA_VERSION
|
|
309
|
+
&& (parsed.outcome.status === 'miss' || parsed.outcome.status === 'error')
|
|
310
|
+
&& typeof parsed.outcome.code === 'string'
|
|
311
|
+
&& IDENTIFIER.test(parsed.outcome.code)
|
|
312
|
+
&& typeof parsed.outcome.recovery === 'string'
|
|
313
|
+
&& BENCHMARK_DOMAIN_RECOVERIES.has(parsed.outcome.recovery)) return { kind: 'domain' }
|
|
314
|
+
return undefined
|
|
315
|
+
}
|
|
168
316
|
if (parsed.ok === false && typeof parsed.error === 'string') {
|
|
317
|
+
const exactFailure = parsed.error === 'runner_failed'
|
|
318
|
+
? (keys === 'error,ok' || (keys === 'error,exit_code,ok,signal'
|
|
319
|
+
&& (parsed.exit_code === null || (typeof parsed.exit_code === 'number' && Number.isInteger(parsed.exit_code)))
|
|
320
|
+
&& (parsed.signal === null || typeof parsed.signal === 'string')))
|
|
321
|
+
: parsed.error === 'invalid_arguments'
|
|
322
|
+
? keys === 'error,ok,reason' && typeof parsed.reason === 'string'
|
|
323
|
+
: keys === 'error,ok'
|
|
324
|
+
if (!exactFailure) return undefined
|
|
169
325
|
const code = parsed.error === 'timed_out'
|
|
170
326
|
? BENCHMARK_BRIDGE_TIMED_OUT
|
|
171
327
|
: parsed.error === 'runner_failed'
|
|
@@ -175,7 +331,7 @@ function bridgeResultStatus(data: Record<string, unknown>, callId: string): { ok
|
|
|
175
331
|
: parsed.error === 'output_truncated'
|
|
176
332
|
? BENCHMARK_BRIDGE_OUTPUT_TRUNCATED
|
|
177
333
|
: BENCHMARK_BRIDGE_CALL_FAILED
|
|
178
|
-
return {
|
|
334
|
+
return { kind: 'failure', code }
|
|
179
335
|
}
|
|
180
336
|
return undefined
|
|
181
337
|
} catch {
|
|
@@ -249,10 +405,15 @@ export function createBenchmarkAgentConformance(projection: BenchmarkBridgeProje
|
|
|
249
405
|
const callId = event.data.message.source.callId
|
|
250
406
|
if (typeof callId !== 'string' || !state.validCallIds.has(callId)) return
|
|
251
407
|
const resultStatus = bridgeResultStatus(event.data, callId)
|
|
252
|
-
if (
|
|
408
|
+
if (typeof event.data.step === 'number') state.lastBridgeResponseStep = event.data.step
|
|
409
|
+
if (resultStatus?.kind === 'result') {
|
|
253
410
|
state.successfulResultStep = typeof event.data.step === 'number'
|
|
254
411
|
? event.data.step
|
|
255
412
|
: state.successfulResultStep
|
|
413
|
+
} else if (resultStatus?.kind === 'domain') {
|
|
414
|
+
state.domainOutcomeStep = typeof event.data.step === 'number'
|
|
415
|
+
? event.data.step
|
|
416
|
+
: state.domainOutcomeStep
|
|
256
417
|
} else {
|
|
257
418
|
state.bridgeCallFailed = true
|
|
258
419
|
state.bridgeFailureCode = resultStatus?.code ?? BENCHMARK_BRIDGE_CALL_FAILED
|
|
@@ -274,10 +435,10 @@ export function createBenchmarkAgentConformance(projection: BenchmarkBridgeProje
|
|
|
274
435
|
if (state.retryRedispatchAttempted) {
|
|
275
436
|
return { kind: 'reject', code: BENCHMARK_BRIDGE_RETRY_CALL_NOT_ALLOWED }
|
|
276
437
|
}
|
|
277
|
-
if (state.successfulResultStep === undefined) {
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
438
|
+
if (state.bridgeCallFailed && state.successfulResultStep === undefined) {
|
|
439
|
+
return { kind: 'reject', code: state.bridgeFailureCode ?? BENCHMARK_BRIDGE_CALL_FAILED }
|
|
440
|
+
}
|
|
441
|
+
if (state.successfulResultStep === undefined && state.domainOutcomeStep === undefined) {
|
|
281
442
|
if (state.retryCount >= MAX_CONFORMANCE_RETRIES) {
|
|
282
443
|
return { kind: 'reject', code: BENCHMARK_BRIDGE_CALL_REQUIRED }
|
|
283
444
|
}
|
|
@@ -289,7 +450,7 @@ export function createBenchmarkAgentConformance(projection: BenchmarkBridgeProje
|
|
|
289
450
|
const terminal = state.lastAssistant
|
|
290
451
|
const terminalIsValid = terminal !== undefined
|
|
291
452
|
&& !terminal.interrupted
|
|
292
|
-
&& terminal.step > state.
|
|
453
|
+
&& terminal.step > state.lastBridgeResponseStep!
|
|
293
454
|
&& parseBenchmarkTerminal(terminal.text, projection.terminal).ok
|
|
294
455
|
if (terminalIsValid) return { kind: 'accept' }
|
|
295
456
|
if (state.retryCount >= MAX_CONFORMANCE_RETRIES) {
|
|
@@ -332,7 +493,7 @@ function requireDisposer(value: unknown, capability: string): Disposer {
|
|
|
332
493
|
function correctionMessage(mode: Exclude<RetryMode, 'none'>, projection: BenchmarkBridgeProjection) {
|
|
333
494
|
const text = mode === 'call'
|
|
334
495
|
? `BENCHMARK_CONFORMANCE_CALL: Execute exactly one native ${projection.toolName} action:"call" now. Describing an intended CLI, shell, or Python command does not execute it.`
|
|
335
|
-
: `BENCHMARK_CONFORMANCE_TERMINAL: Reuse the existing successful tool result. Do not call any tool. Reply only <${projection.terminal.tag}>{
|
|
496
|
+
: `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.`
|
|
336
497
|
return createUserMessage({
|
|
337
498
|
content: [{ type: 'text' as const, text }],
|
|
338
499
|
source: { kind: 'plugin' as const, plugin: 'gotry-benchmark-agent-conformance' },
|
|
@@ -341,6 +502,7 @@ function correctionMessage(mode: Exclude<RetryMode, 'none'>, projection: Benchma
|
|
|
341
502
|
|
|
342
503
|
function systemSection(projection: BenchmarkBridgeProjection): { name: string; text: string } {
|
|
343
504
|
const allowed = projection.allowedTools.join(', ')
|
|
505
|
+
const outline = terminalSchemaOutline(projection.terminal.body_schema)
|
|
344
506
|
return {
|
|
345
507
|
name: 'benchmark:agent-conformance',
|
|
346
508
|
text: [
|
|
@@ -348,7 +510,7 @@ function systemSection(projection: BenchmarkBridgeProjection): { name: string; t
|
|
|
348
510
|
`- 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.`,
|
|
349
511
|
`- Call it with exactly {"action":"call","tool":"<one of: ${allowed}>","arguments":{...}}.`,
|
|
350
512
|
'- action:"tools" is discovery only and does not satisfy the required environment call.',
|
|
351
|
-
`- After a successful tool result, reply only <${projection.terminal.tag}>
|
|
513
|
+
`- 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.`,
|
|
352
514
|
'- If a terminal-format correction arrives, reuse the existing result and do not call the tool again.',
|
|
353
515
|
].join('\n'),
|
|
354
516
|
}
|