@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
|
@@ -1,17 +1,35 @@
|
|
|
1
1
|
import { accessSync, constants, lstatSync, readFileSync, statSync } from 'node:fs'
|
|
2
2
|
import { getuid } from 'node:process'
|
|
3
3
|
import { isAbsolute } from 'node:path'
|
|
4
|
-
import {
|
|
4
|
+
import {
|
|
5
|
+
assertSupportedJsonSchema,
|
|
6
|
+
validateJsonSchemaValue,
|
|
7
|
+
ToolArgsError,
|
|
8
|
+
type ObjectJsonSchema,
|
|
9
|
+
type ToolDefinition,
|
|
10
|
+
} from '@deepseek-ai/dsh-tools'
|
|
5
11
|
import {
|
|
6
12
|
validateTerminalOutputConfig,
|
|
7
13
|
type BenchmarkBridgeProjection,
|
|
8
14
|
type TerminalOutputConfig,
|
|
9
15
|
} from './benchmark-agent-conformance.ts'
|
|
10
16
|
|
|
11
|
-
|
|
17
|
+
// v4(#215 Round 12):terminal_output 携带 closed body schema(结构关键字白名单,
|
|
18
|
+
// 禁数据值注解面);v3 及更早配置缺该面,按契约不兼容 fail-closed。
|
|
19
|
+
const SCHEMA_VERSION = 'gotry_benchmark_environment_bridge_v4'
|
|
20
|
+
export const BENCHMARK_TOOL_RESULT_SCHEMA_VERSION = 'gotry_benchmark_tool_result_v1'
|
|
21
|
+
export const BENCHMARK_DOMAIN_RECOVERIES = ['none', 'retry_same', 'revise_arguments', 'choose_alternative'] as const
|
|
22
|
+
type BenchmarkDomainRecovery = typeof BENCHMARK_DOMAIN_RECOVERIES[number]
|
|
12
23
|
const IDENTIFIER = /^[A-Za-z][A-Za-z0-9_.-]*$/
|
|
13
|
-
const
|
|
14
|
-
const
|
|
24
|
+
const RECOVERIES = new Set<string>(BENCHMARK_DOMAIN_RECOVERIES)
|
|
25
|
+
const CONFIG_KEYS = ['argv_prefix', 'cwd', 'enabled', 'executable', 'isolation', 'max_output_bytes', 'schema_version', 'terminal_output', 'timeout_ms', 'tools']
|
|
26
|
+
const INPUT_SCHEMA_MAX_BYTES = 16 * 1024
|
|
27
|
+
const INPUT_SCHEMA_MAX_DEPTH = 8
|
|
28
|
+
const INPUT_SCHEMA_MAX_NODES = 256
|
|
29
|
+
const INPUT_SCHEMA_MAX_PROPERTIES = 256
|
|
30
|
+
const INPUT_SCHEMA_MAX_ENUM_VALUES = 256
|
|
31
|
+
const INPUT_SCHEMA_MAX_DESCRIPTION_LENGTH = 512
|
|
32
|
+
const INPUT_SCHEMA_TYPES = new Set(['string', 'number', 'integer', 'boolean', 'null', 'array', 'object'])
|
|
15
33
|
|
|
16
34
|
export interface BenchmarkEnvironmentBridgeConfig {
|
|
17
35
|
schema_version: typeof SCHEMA_VERSION
|
|
@@ -19,8 +37,7 @@ export interface BenchmarkEnvironmentBridgeConfig {
|
|
|
19
37
|
executable: string
|
|
20
38
|
cwd: string
|
|
21
39
|
argv_prefix: string[]
|
|
22
|
-
|
|
23
|
-
allowed_output_keys?: Record<string, string[]>
|
|
40
|
+
tools: BenchmarkToolDescriptor[]
|
|
24
41
|
timeout_ms: number
|
|
25
42
|
max_output_bytes: number
|
|
26
43
|
terminal_output: TerminalOutputConfig
|
|
@@ -31,10 +48,24 @@ export interface BenchmarkEnvironmentBridgeConfig {
|
|
|
31
48
|
}
|
|
32
49
|
}
|
|
33
50
|
|
|
51
|
+
export interface BenchmarkToolDescriptor {
|
|
52
|
+
name: string
|
|
53
|
+
description: string
|
|
54
|
+
input_schema: ObjectJsonSchema
|
|
55
|
+
output_keys: string[]
|
|
56
|
+
domain_outcomes: Array<{ status: 'miss' | 'error'; code: string; recovery: BenchmarkDomainRecovery }>
|
|
57
|
+
}
|
|
58
|
+
|
|
34
59
|
function plainObject(value: unknown): value is Record<string, unknown> {
|
|
35
60
|
return typeof value === 'object' && value !== null && !Array.isArray(value)
|
|
36
61
|
}
|
|
37
62
|
|
|
63
|
+
function deepFreezeJson<T>(value: T): T {
|
|
64
|
+
if (!value || typeof value !== 'object' || Object.isFrozen(value)) return value
|
|
65
|
+
for (const child of Object.values(value as Record<string, unknown>)) deepFreezeJson(child)
|
|
66
|
+
return Object.freeze(value)
|
|
67
|
+
}
|
|
68
|
+
|
|
38
69
|
function existingExecutable(value: unknown): value is string {
|
|
39
70
|
if (typeof value !== 'string' || !isAbsolute(value)) return false
|
|
40
71
|
try {
|
|
@@ -55,17 +86,111 @@ function existingDirectory(value: unknown): value is string {
|
|
|
55
86
|
}
|
|
56
87
|
}
|
|
57
88
|
|
|
58
|
-
function identifiers(value: unknown): value is string[] {
|
|
59
|
-
return Array.isArray(value) && value.length > 0 && value.length <= 64 && new Set(value).size === value.length && value.every(
|
|
89
|
+
function identifiers(value: unknown, allowEmpty = false): value is string[] {
|
|
90
|
+
return Array.isArray(value) && (allowEmpty || value.length > 0) && value.length <= 64 && new Set(value).size === value.length && value.every(
|
|
60
91
|
item => typeof item === 'string' && item.length > 0 && IDENTIFIER.test(item),
|
|
61
92
|
)
|
|
62
93
|
}
|
|
63
94
|
|
|
64
|
-
function
|
|
95
|
+
function exactKeys(value: Record<string, unknown>, allowed: readonly string[]): boolean {
|
|
96
|
+
return Object.keys(value).every(key => allowed.includes(key))
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
function validDescription(value: unknown, required = false): boolean {
|
|
100
|
+
if (value === undefined) return !required
|
|
101
|
+
return typeof value === 'string'
|
|
102
|
+
&& (!required || value.trim().length > 0)
|
|
103
|
+
&& value.length <= INPUT_SCHEMA_MAX_DESCRIPTION_LENGTH
|
|
104
|
+
&& !/[\u0000-\u001f\u007f]/.test(value)
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
function scalarMatches(type: string, value: unknown): boolean {
|
|
108
|
+
if (type === 'string') return typeof value === 'string' && value.length <= 512
|
|
109
|
+
if (type === 'number') return typeof value === 'number' && Number.isFinite(value)
|
|
110
|
+
if (type === 'integer') return typeof value === 'number' && Number.isSafeInteger(value)
|
|
111
|
+
if (type === 'boolean') return typeof value === 'boolean'
|
|
112
|
+
return type === 'null' && value === null
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
function validInputSchema(value: unknown): value is ObjectJsonSchema {
|
|
65
116
|
if (!plainObject(value)) return false
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
117
|
+
let serialized: string
|
|
118
|
+
try {
|
|
119
|
+
serialized = JSON.stringify(value)
|
|
120
|
+
} catch {
|
|
121
|
+
return false
|
|
122
|
+
}
|
|
123
|
+
if (Buffer.byteLength(serialized, 'utf8') > INPUT_SCHEMA_MAX_BYTES) return false
|
|
124
|
+
|
|
125
|
+
const pending: Array<{ node: Record<string, unknown>; depth: number }> = [{ node: value, depth: 0 }]
|
|
126
|
+
const seen = new Set<object>()
|
|
127
|
+
let nodes = 0
|
|
128
|
+
let properties = 0
|
|
129
|
+
let enumValues = 0
|
|
130
|
+
while (pending.length > 0) {
|
|
131
|
+
const { node, depth } = pending.pop()!
|
|
132
|
+
if (seen.has(node) || ++nodes > INPUT_SCHEMA_MAX_NODES || depth > INPUT_SCHEMA_MAX_DEPTH) return false
|
|
133
|
+
seen.add(node)
|
|
134
|
+
if (!INPUT_SCHEMA_TYPES.has(String(node.type)) || !validDescription(node.description)) return false
|
|
135
|
+
|
|
136
|
+
if (node.type === 'object') {
|
|
137
|
+
if (!exactKeys(node, ['type', 'description', 'properties', 'required', 'additionalProperties'])
|
|
138
|
+
|| !plainObject(node.properties)
|
|
139
|
+
|| !Array.isArray(node.required)
|
|
140
|
+
|| node.additionalProperties !== false) return false
|
|
141
|
+
const propertyEntries = Object.entries(node.properties)
|
|
142
|
+
properties += propertyEntries.length
|
|
143
|
+
if (properties > INPUT_SCHEMA_MAX_PROPERTIES || propertyEntries.some(([key, child]) => !IDENTIFIER.test(key) || !plainObject(child))) return false
|
|
144
|
+
const required = node.required
|
|
145
|
+
if (required.length > propertyEntries.length
|
|
146
|
+
|| new Set(required).size !== required.length
|
|
147
|
+
|| required.some(key => typeof key !== 'string' || !Object.hasOwn(node.properties as Record<string, unknown>, key))) return false
|
|
148
|
+
for (const [, child] of propertyEntries) pending.push({ node: child as Record<string, unknown>, depth: depth + 1 })
|
|
149
|
+
continue
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
if (node.type === 'array') {
|
|
153
|
+
if (!exactKeys(node, ['type', 'description', 'items']) || !plainObject(node.items)) return false
|
|
154
|
+
pending.push({ node: node.items, depth: depth + 1 })
|
|
155
|
+
continue
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
if (!exactKeys(node, ['type', 'description', 'enum', 'const'])
|
|
159
|
+
|| (Object.hasOwn(node, 'enum') && Object.hasOwn(node, 'const'))) return false
|
|
160
|
+
if (Object.hasOwn(node, 'const') && !scalarMatches(String(node.type), node.const)) return false
|
|
161
|
+
if (Object.hasOwn(node, 'enum')) {
|
|
162
|
+
if (!Array.isArray(node.enum) || node.enum.length === 0 || node.enum.length > 64) return false
|
|
163
|
+
enumValues += node.enum.length
|
|
164
|
+
if (enumValues > INPUT_SCHEMA_MAX_ENUM_VALUES
|
|
165
|
+
|| new Set(node.enum).size !== node.enum.length
|
|
166
|
+
|| node.enum.some(item => !scalarMatches(String(node.type), item))) return false
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
return value.type === 'object'
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
function toolDescriptors(value: unknown): value is BenchmarkToolDescriptor[] {
|
|
173
|
+
if (!Array.isArray(value) || value.length === 0 || value.length > 64) return false
|
|
174
|
+
const names = new Set<string>()
|
|
175
|
+
return value.every(item => {
|
|
176
|
+
if (!plainObject(item)) return false
|
|
177
|
+
const keys = Object.keys(item).sort()
|
|
178
|
+
const required = ['description', 'domain_outcomes', 'input_schema', 'name']
|
|
179
|
+
if (JSON.stringify(keys) !== JSON.stringify([...required, 'output_keys'].sort())) return false
|
|
180
|
+
if (typeof item.name !== 'string' || !IDENTIFIER.test(item.name) || names.has(item.name)) return false
|
|
181
|
+
names.add(item.name)
|
|
182
|
+
if (!validDescription(item.description, true) || !validInputSchema(item.input_schema)) return false
|
|
183
|
+
if (!Array.isArray(item.domain_outcomes) || item.domain_outcomes.length > 64) return false
|
|
184
|
+
const outcomeKeys = new Set<string>()
|
|
185
|
+
if (!item.domain_outcomes.every(outcome => plainObject(outcome)
|
|
186
|
+
&& JSON.stringify(Object.keys(outcome).sort()) === JSON.stringify(['code', 'recovery', 'status'])
|
|
187
|
+
&& (outcome.status === 'miss' || outcome.status === 'error')
|
|
188
|
+
&& typeof outcome.code === 'string' && IDENTIFIER.test(outcome.code)
|
|
189
|
+
&& typeof outcome.recovery === 'string' && RECOVERIES.has(outcome.recovery)
|
|
190
|
+
&& !outcomeKeys.has(outcome.code)
|
|
191
|
+
&& (outcomeKeys.add(outcome.code), true))) return false
|
|
192
|
+
return identifiers(item.output_keys)
|
|
193
|
+
})
|
|
69
194
|
}
|
|
70
195
|
|
|
71
196
|
// These are passed as direct argv entries (never through a shell), so options
|
|
@@ -98,20 +223,19 @@ function validBound(value: unknown, maximum: number): value is number {
|
|
|
98
223
|
function validate(value: unknown): value is BenchmarkEnvironmentBridgeConfig {
|
|
99
224
|
const configKeys = plainObject(value) ? Object.keys(value).sort() : []
|
|
100
225
|
const baseKeys = [...CONFIG_KEYS].sort()
|
|
101
|
-
const extendedKeys = [...CONFIG_KEYS, ...OPTIONAL_CONFIG_KEYS].sort()
|
|
102
226
|
if (!plainObject(value)
|
|
103
|
-
||
|
|
227
|
+
|| JSON.stringify(configKeys) !== JSON.stringify(baseKeys)
|
|
104
228
|
|| value.schema_version !== SCHEMA_VERSION
|
|
105
229
|
|| value.enabled !== true
|
|
106
230
|
|| !existingExecutable(value.executable)
|
|
107
231
|
|| !existingDirectory(value.cwd)
|
|
108
232
|
|| !argvPrefix(value.argv_prefix)
|
|
109
|
-
|| !
|
|
233
|
+
|| !toolDescriptors(value.tools)
|
|
110
234
|
|| !validBound(value.timeout_ms, 120_000)
|
|
111
235
|
|| !validBound(value.max_output_bytes, 10 * 1024 * 1024)
|
|
112
236
|
|| !validateTerminalOutputConfig(value.terminal_output)
|
|
113
237
|
|| !exactIsolation(value.isolation)
|
|
114
|
-
|
|
238
|
+
) return false
|
|
115
239
|
return true
|
|
116
240
|
}
|
|
117
241
|
|
|
@@ -124,13 +248,13 @@ export function loadBenchmarkEnvironmentConfig(path: string): BenchmarkEnvironme
|
|
|
124
248
|
if (typeof getuid !== 'function' || metadata.uid !== getuid()) return null
|
|
125
249
|
if ((metadata.mode & 0o022) !== 0) return null
|
|
126
250
|
const parsed: unknown = JSON.parse(readFileSync(path, 'utf8'))
|
|
127
|
-
return validate(parsed) ? parsed : null
|
|
251
|
+
return validate(parsed) ? deepFreezeJson(parsed) : null
|
|
128
252
|
} catch {
|
|
129
253
|
return null
|
|
130
254
|
}
|
|
131
255
|
}
|
|
132
256
|
|
|
133
|
-
type Register = (tool:
|
|
257
|
+
type Register = (tool: ToolDefinition) => void
|
|
134
258
|
interface CollectedText {
|
|
135
259
|
readFrom(offset: number): { text: string; nextOffset: number; lossy?: boolean }
|
|
136
260
|
}
|
|
@@ -176,6 +300,19 @@ const FORBIDDEN_OUTPUT_KEY_FRAGMENTS = [
|
|
|
176
300
|
'loadermetadata', 'oracle', 'reference', 'reward', 'score',
|
|
177
301
|
]
|
|
178
302
|
|
|
303
|
+
export const BRIDGE_ERROR_CONTRACT: Record<string, { recoverable: boolean; remedy: string }> = Object.freeze({
|
|
304
|
+
invalid_action: { recoverable: true, remedy: 'action 只允许 tools、call 或 errors' },
|
|
305
|
+
disallowed_tool: { recoverable: true, remedy: '先 action=tools 列出 allowed_tools 清单,再从清单内选工具' },
|
|
306
|
+
invalid_arguments: { recoverable: true, remedy: 'arguments 必须是可序列化 JSON 对象(≤64KB、深度≤12);收窄后重试' },
|
|
307
|
+
timed_out: { recoverable: true, remedy: '上游超时;可原样重试一次或换其他工具' },
|
|
308
|
+
output_truncated: { recoverable: true, remedy: '输出超出上限;让被调工具收窄查询范围后重试' },
|
|
309
|
+
invalid_json: { recoverable: true, remedy: '上游输出不是合法 JSON;重试一次或换工具' },
|
|
310
|
+
invalid_output: { recoverable: true, remedy: '上游输出结构不符;重试或换工具' },
|
|
311
|
+
runner_failed: { recoverable: true, remedy: '上游进程非零退出;可重试一次,持续失败换工具' },
|
|
312
|
+
spawn_failed: { recoverable: false, remedy: '可执行文件不可用——环境问题,调用方不可恢复' },
|
|
313
|
+
forbidden_output: { recoverable: false, remedy: '输出含未授权键(策略边界)——不可恢复,换工具或放弃' },
|
|
314
|
+
})
|
|
315
|
+
|
|
179
316
|
function inspectOutput(value: unknown): 'ok' | 'forbidden_key' | 'structure_limit' {
|
|
180
317
|
const pending: Array<{ value: unknown; depth: number }> = [{ value, depth: 0 }]
|
|
181
318
|
let nodes = 0
|
|
@@ -199,19 +336,22 @@ function inspectOutput(value: unknown): 'ok' | 'forbidden_key' | 'structure_limi
|
|
|
199
336
|
|
|
200
337
|
function inspectAllowedOutput(value: unknown, allowedKeys: string[]): boolean {
|
|
201
338
|
const allowed = new Set(allowedKeys)
|
|
202
|
-
const pending: Array<{ value: unknown; depth: number }> = [{ value, depth: 0 }]
|
|
339
|
+
const pending: Array<{ value: unknown; depth: number; coveredByDeclaredKey: boolean }> = [{ value, depth: 0, coveredByDeclaredKey: false }]
|
|
203
340
|
let nodes = 0
|
|
204
341
|
while (pending.length > 0) {
|
|
205
342
|
const current = pending.pop()!
|
|
206
343
|
if (++nodes > 10_000 || current.depth > 24) return false
|
|
207
344
|
if (Array.isArray(current.value)) {
|
|
208
|
-
for (const child of current.value) pending.push({ value: child, depth: current.depth + 1 })
|
|
345
|
+
for (const child of current.value) pending.push({ value: child, depth: current.depth + 1, coveredByDeclaredKey: current.coveredByDeclaredKey })
|
|
346
|
+
continue
|
|
347
|
+
}
|
|
348
|
+
if (!plainObject(current.value)) {
|
|
349
|
+
if (!current.coveredByDeclaredKey) return false
|
|
209
350
|
continue
|
|
210
351
|
}
|
|
211
|
-
if (!plainObject(current.value)) continue
|
|
212
352
|
for (const [key, child] of Object.entries(current.value)) {
|
|
213
353
|
if (!allowed.has(key)) return false
|
|
214
|
-
pending.push({ value: child, depth: current.depth + 1 })
|
|
354
|
+
pending.push({ value: child, depth: current.depth + 1, coveredByDeclaredKey: true })
|
|
215
355
|
}
|
|
216
356
|
}
|
|
217
357
|
return true
|
|
@@ -244,21 +384,6 @@ function serializedArguments(value: Record<string, unknown>): { json: string; re
|
|
|
244
384
|
}
|
|
245
385
|
}
|
|
246
386
|
|
|
247
|
-
/** 可恢复 domain-error 契约(Round 8,issue #100/#102):封闭词表 + 逐码可恢复性与补救指引。
|
|
248
|
-
* 模型经 action=errors 拉取全表;失败返回的扁平形状保持不变(benchmark 诊断面依赖)。 */
|
|
249
|
-
export const BRIDGE_ERROR_CONTRACT: Record<string, { recoverable: boolean; remedy: string }> = Object.freeze({
|
|
250
|
-
invalid_action: { recoverable: true, remedy: 'action 只允许 tools 或 call' },
|
|
251
|
-
disallowed_tool: { recoverable: true, remedy: '先 action=tools 列出 allowed_tools 清单,再从清单内选工具' },
|
|
252
|
-
invalid_arguments: { recoverable: true, remedy: 'arguments 必须是可序列化 JSON 对象(≤64KB、深度≤12);收窄后重试' },
|
|
253
|
-
timed_out: { recoverable: true, remedy: '上游超时;可原样重试一次或换其他工具' },
|
|
254
|
-
output_truncated: { recoverable: true, remedy: '输出超出上限;让被调工具收窄查询范围后重试' },
|
|
255
|
-
invalid_json: { recoverable: true, remedy: '上游输出不是合法 JSON;重试一次或换工具' },
|
|
256
|
-
invalid_output: { recoverable: true, remedy: '上游输出结构不符(如原始字符串);重试或换工具' },
|
|
257
|
-
runner_failed: { recoverable: true, remedy: '上游进程非零退出;可重试一次,持续失败换工具' },
|
|
258
|
-
spawn_failed: { recoverable: false, remedy: '可执行文件不可用——环境问题,调用方不可恢复' },
|
|
259
|
-
forbidden_output: { recoverable: false, remedy: '输出含未授权键(策略边界)——不可恢复,换工具或放弃' },
|
|
260
|
-
})
|
|
261
|
-
|
|
262
387
|
/** Register the opt-in model-facing bridge. */
|
|
263
388
|
export function registerBenchmarkEnvironmentBridge(
|
|
264
389
|
path: string,
|
|
@@ -269,35 +394,57 @@ export function registerBenchmarkEnvironmentBridge(
|
|
|
269
394
|
if (!bridge) throw new Error('benchmark environment bridge configuration unavailable')
|
|
270
395
|
if (!subprocess) throw new Error('benchmark environment bridge subprocess unavailable')
|
|
271
396
|
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
description: '
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
parameters: {
|
|
280
|
-
action: { type: 'string', enum: ['tools', 'call', 'errors'], required: true, description: 'tools=列出可调工具;call=执行被映射工具;errors=拉取可恢复错误契约表' },
|
|
281
|
-
tool: { type: 'string', description: '被调工具名(必须在 allowed_tools 清单内;action=call 时必填)' },
|
|
282
|
-
arguments: { type: 'object', additionalProperties: true, description: '传给被调工具的参数对象(action=call 时可选;≤64KB、深度≤12)' },
|
|
397
|
+
const parameters: Record<string, unknown> = deepFreezeJson({
|
|
398
|
+
type: 'object',
|
|
399
|
+
description: 'Flat benchmark bridge wire: choose an action, then provide the mapped tool and its arguments.',
|
|
400
|
+
properties: {
|
|
401
|
+
action: { type: 'string', enum: ['tools', 'call', 'errors'], description: 'tools=列出可调工具;call=执行工具;errors=拉取错误契约' },
|
|
402
|
+
tool: { type: 'string', enum: bridge.tools.map(item => item.name), description: 'action=call 时选择的冻结工具名' },
|
|
403
|
+
arguments: { type: 'object', additionalProperties: true, description: 'action=call 时传给工具的参数对象' },
|
|
283
404
|
},
|
|
405
|
+
required: ['action'],
|
|
406
|
+
additionalProperties: false,
|
|
407
|
+
})
|
|
408
|
+
assertSupportedJsonSchema(parameters)
|
|
409
|
+
|
|
410
|
+
const definition: ToolDefinition = {
|
|
411
|
+
name: 'gotry_benchmark_environment',
|
|
412
|
+
description: 'When a prompt asks to run agent_env.cli, use action=call here with the mapped tool; arbitrary shell is not exposed.',
|
|
413
|
+
parameters,
|
|
284
414
|
output: {
|
|
285
|
-
schema: {
|
|
415
|
+
schema: {},
|
|
286
416
|
render: (_args, value) => [{ type: 'text', text: JSON.stringify(value) }],
|
|
287
417
|
},
|
|
288
|
-
async execute(args): Promise<Record<string, never>> {
|
|
289
|
-
const
|
|
290
|
-
if (
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
const
|
|
294
|
-
|
|
418
|
+
async execute(args: unknown): Promise<Record<string, never>> {
|
|
419
|
+
const violations = validateJsonSchemaValue(parameters, args, 'arguments')
|
|
420
|
+
if (violations.length > 0) throw new ToolArgsError(violations)
|
|
421
|
+
const query = args as Record<string, unknown>
|
|
422
|
+
const tool = query.tool
|
|
423
|
+
const descriptor = bridge.tools.find(item => item.name === tool)
|
|
424
|
+
const keys = Object.keys(query)
|
|
425
|
+
if (query.action === 'tools') {
|
|
426
|
+
if (keys.length !== 1) throw new ToolArgsError(['arguments: tools action accepts no tool or arguments'])
|
|
427
|
+
return jsonObject({ ok: true, tools: bridge.tools })
|
|
428
|
+
}
|
|
429
|
+
if (query.action === 'errors') {
|
|
430
|
+
if (keys.length !== 1) throw new ToolArgsError(['arguments: errors action accepts no tool or arguments'])
|
|
431
|
+
return jsonObject({ ok: true, errors: BRIDGE_ERROR_CONTRACT })
|
|
432
|
+
}
|
|
433
|
+
if (query.action !== 'call') {
|
|
434
|
+
return jsonObject({ ok: false, error: 'invalid_action' })
|
|
435
|
+
}
|
|
436
|
+
if (keys.length !== 3 || !Object.hasOwn(query, 'tool') || !Object.hasOwn(query, 'arguments')) {
|
|
437
|
+
throw new ToolArgsError(['arguments: call action requires exactly tool and arguments'])
|
|
438
|
+
}
|
|
439
|
+
if (typeof tool !== 'string' || !descriptor) {
|
|
295
440
|
return jsonObject({ ok: false, error: 'disallowed_tool' })
|
|
296
441
|
}
|
|
297
|
-
if (
|
|
442
|
+
if (!plainObject(query.arguments)) {
|
|
298
443
|
return jsonObject({ ok: false, error: 'invalid_arguments' })
|
|
299
444
|
}
|
|
300
|
-
const callArguments = query.arguments
|
|
445
|
+
const callArguments = query.arguments
|
|
446
|
+
const argumentViolations = validateJsonSchemaValue(descriptor.input_schema, callArguments, 'arguments')
|
|
447
|
+
if (argumentViolations.length > 0) throw new ToolArgsError(argumentViolations)
|
|
301
448
|
const serialized = serializedArguments(callArguments)
|
|
302
449
|
if (serialized.reason) return jsonObject({ ok: false, error: 'invalid_arguments', reason: serialized.reason })
|
|
303
450
|
const controller = new AbortController()
|
|
@@ -315,7 +462,7 @@ export function registerBenchmarkEnvironmentBridge(
|
|
|
315
462
|
env.PYTHONDONTWRITEBYTECODE = '1'
|
|
316
463
|
env.PYTHONNOUSERSITE = '1'
|
|
317
464
|
handle = subprocess.spawn({
|
|
318
|
-
argv: [bridge.executable, ...bridge.argv_prefix, 'call',
|
|
465
|
+
argv: [bridge.executable, ...bridge.argv_prefix, 'call', tool, serialized.json!],
|
|
319
466
|
cwd: bridge.cwd,
|
|
320
467
|
stdio: {
|
|
321
468
|
stdin: 'ignore',
|
|
@@ -344,12 +491,18 @@ export function registerBenchmarkEnvironmentBridge(
|
|
|
344
491
|
const outputInspection = inspectOutput(parsed)
|
|
345
492
|
if (outputInspection === 'forbidden_key') return jsonObject({ ok: false, error: 'forbidden_output' })
|
|
346
493
|
if (outputInspection === 'structure_limit') return jsonObject({ ok: false, error: 'invalid_output' })
|
|
347
|
-
|
|
494
|
+
if (parsed.schema_version !== BENCHMARK_TOOL_RESULT_SCHEMA_VERSION || typeof parsed.status !== 'string') {
|
|
495
|
+
return jsonObject({ ok: false, error: 'invalid_output' })
|
|
496
|
+
}
|
|
497
|
+
if (parsed.status === 'miss' || parsed.status === 'error') {
|
|
498
|
+
if (typeof parsed.code !== 'string' || typeof parsed.recovery !== 'string' || !descriptor.domain_outcomes.some(outcome => outcome.status === parsed.status && outcome.code === parsed.code && outcome.recovery === parsed.recovery)) return jsonObject({ ok: false, error: 'invalid_output' })
|
|
499
|
+
if (Object.keys(parsed).sort().join(',') !== 'code,recovery,schema_version,status') return jsonObject({ ok: false, error: 'invalid_output' })
|
|
500
|
+
return jsonObject({ ok: true, outcome: parsed })
|
|
501
|
+
}
|
|
502
|
+
if (parsed.status !== 'ok' || !Object.hasOwn(parsed, 'result') || Object.keys(parsed).sort().join(',') !== 'result,schema_version,status') return jsonObject({ ok: false, error: 'invalid_output' })
|
|
503
|
+
const visibleResult = parsed.result
|
|
348
504
|
if (!plainObject(visibleResult) && !Array.isArray(visibleResult)) return jsonObject({ ok: false, error: 'invalid_output' })
|
|
349
|
-
|
|
350
|
-
? bridge.allowed_output_keys[query.tool]
|
|
351
|
-
: undefined
|
|
352
|
-
if (outputKeys && !inspectAllowedOutput(visibleResult, outputKeys)) return jsonObject({ ok: false, error: 'forbidden_output' })
|
|
505
|
+
if (!inspectAllowedOutput(visibleResult, descriptor.output_keys)) return jsonObject({ ok: false, error: 'forbidden_output' })
|
|
353
506
|
return jsonObject({ ok: true, result: visibleResult })
|
|
354
507
|
} catch {
|
|
355
508
|
if (controller.signal.aborted) return jsonObject({ ok: false, error: 'timed_out' })
|
|
@@ -358,10 +511,11 @@ export function registerBenchmarkEnvironmentBridge(
|
|
|
358
511
|
clearTimeout(timer)
|
|
359
512
|
}
|
|
360
513
|
},
|
|
361
|
-
}
|
|
514
|
+
}
|
|
515
|
+
register(definition)
|
|
362
516
|
return Object.freeze({
|
|
363
517
|
toolName: 'gotry_benchmark_environment',
|
|
364
|
-
allowedTools: Object.freeze(
|
|
518
|
+
allowedTools: Object.freeze(bridge.tools.map(item => item.name)),
|
|
365
519
|
terminal: Object.freeze({ ...bridge.terminal_output }),
|
|
366
520
|
})
|
|
367
521
|
}
|
|
@@ -407,9 +407,11 @@ function recoverFinalResponseDecision(response: string, task: BookingCopilotTask
|
|
|
407
407
|
console.error('[booking-copilot] finalResponse recovery rejected (invalid action):', JSON.stringify({ kind: action.kind, errors: validation.errors.slice(0, 6) }).slice(0, 600))
|
|
408
408
|
return null
|
|
409
409
|
}
|
|
410
|
-
repairPlannerFactRefs(action)
|
|
410
|
+
const repairedRefs = repairPlannerFactRefs(action)
|
|
411
|
+
const repairedValidation = validateBookingReadAction(action as unknown as BookingReadAction)
|
|
412
|
+
if (!repairedValidation.ok) return null
|
|
411
413
|
try {
|
|
412
|
-
assertPlannerSafeRefs(action)
|
|
414
|
+
assertPlannerSafeRefs(action, repairedRefs)
|
|
413
415
|
} catch (error) {
|
|
414
416
|
console.error('[booking-copilot] finalResponse recovery rejected (unsafe ref):', JSON.stringify({ actionId: action.actionId, factRefs: action.factRefs }).slice(0, 600))
|
|
415
417
|
return null
|
|
@@ -426,17 +428,22 @@ function recoverFinalResponseDecision(response: string, task: BookingCopilotTask
|
|
|
426
428
|
const PLANNER_SAFE_REF_PATTERN = /^[A-Za-z0-9][A-Za-z0-9:._-]*$/
|
|
427
429
|
|
|
428
430
|
// Models cite prompt facts in URI-ish syntax (`fact://turn_X/request`,
|
|
429
|
-
// `turn_X#request`)
|
|
430
|
-
//
|
|
431
|
-
|
|
432
|
-
// safe-ref gate into the retry path.
|
|
433
|
-
function repairPlannerFactRefs(action: Record<string, unknown>): void {
|
|
431
|
+
// `turn_X#request`). Preserve already-safe refs exactly; unsafe refs enter the
|
|
432
|
+
// reserved modelref namespace with the full SHA-256 of the raw UTF-8 value.
|
|
433
|
+
function repairPlannerFactRefs(action: Record<string, unknown>): Set<string> {
|
|
434
434
|
const factRefs = action.factRefs
|
|
435
|
-
|
|
436
|
-
|
|
435
|
+
const repairedRefs = new Set<string>()
|
|
436
|
+
if (!Array.isArray(factRefs)) return repairedRefs
|
|
437
|
+
action.factRefs = factRefs.map((ref) => {
|
|
438
|
+
if (typeof ref !== 'string' || PLANNER_SAFE_REF_PATTERN.test(ref)) return ref
|
|
439
|
+
const alias = `modelref:${createHash('sha256').update(ref, 'utf8').digest('hex')}`
|
|
440
|
+
repairedRefs.add(alias)
|
|
441
|
+
return alias
|
|
442
|
+
})
|
|
443
|
+
return repairedRefs
|
|
437
444
|
}
|
|
438
445
|
|
|
439
|
-
function assertPlannerSafeRefs(action: Record<string, unknown>): void {
|
|
446
|
+
function assertPlannerSafeRefs(action: Record<string, unknown>, repairedRefs: Set<string>): void {
|
|
440
447
|
const actionId = action.actionId
|
|
441
448
|
if (typeof actionId !== 'string' || !PLANNER_SAFE_REF_PATTERN.test(actionId)) {
|
|
442
449
|
throw new Error(`planner_invalid_action:unsafe_action_id:${String(actionId).slice(0, 60)}`)
|
|
@@ -444,7 +451,7 @@ function assertPlannerSafeRefs(action: Record<string, unknown>): void {
|
|
|
444
451
|
const factRefs = action.factRefs
|
|
445
452
|
if (Array.isArray(factRefs)) {
|
|
446
453
|
for (const ref of factRefs) {
|
|
447
|
-
if (typeof ref !== 'string' || !PLANNER_SAFE_REF_PATTERN.test(ref)) {
|
|
454
|
+
if (typeof ref !== 'string' || !PLANNER_SAFE_REF_PATTERN.test(ref) || (ref.startsWith('modelref:') && !repairedRefs.has(ref)) || ref.length > 512) {
|
|
448
455
|
throw new Error(`planner_invalid_action:unsafe_fact_ref:${String(ref).slice(0, 60)}`)
|
|
449
456
|
}
|
|
450
457
|
}
|
|
@@ -502,8 +509,13 @@ function parseToolDecision(event: unknown, task: BookingCopilotTaskState): Booki
|
|
|
502
509
|
// boundary, past the retry budget. Repair the common fragment syntax first,
|
|
503
510
|
// then enforce the same charset here so remaining violations retry as
|
|
504
511
|
// parse-class failures instead of failing the turn as PLANNER_FAILED.
|
|
505
|
-
repairPlannerFactRefs(decision.action)
|
|
506
|
-
|
|
512
|
+
const repairedRefs = repairPlannerFactRefs(decision.action)
|
|
513
|
+
const repairedValidation = validateBookingReadAction(decision.action)
|
|
514
|
+
if (!repairedValidation.ok) {
|
|
515
|
+
console.error(`[booking-copilot] repaired action rejected:`, JSON.stringify({ errors: repairedValidation.errors.slice(0, 8) }).slice(0, 800))
|
|
516
|
+
throw new Error(`planner_invalid_action:${repairedValidation.errors.join('; ')}`)
|
|
517
|
+
}
|
|
518
|
+
assertPlannerSafeRefs(decision.action, repairedRefs)
|
|
507
519
|
const action = decision.action as unknown as BookingReadAction
|
|
508
520
|
const capability = TOOL_TO_CAPABILITY.get(name as DshEmbeddedBookingToolName)
|
|
509
521
|
if (!capability || !actionsForEmbeddedCapability(capability).includes(action.kind)) {
|
package/ts/src/index.ts
CHANGED
|
@@ -1210,14 +1210,16 @@ export function apply(ctx: Context, config: Config): void {
|
|
|
1210
1210
|
if (!q.keyword) {
|
|
1211
1211
|
return JSON.parse(JSON.stringify({ ok: false, verdict: 'error', summary: 'keyword 必填', evidence: '[hbcli-anything@error] empty' })) as Record<string, never>
|
|
1212
1212
|
}
|
|
1213
|
-
|
|
1213
|
+
// hbcliBin 直通 config(缺省 'hbcli',能力层按已知安装位回退)——issue #195 教训:
|
|
1214
|
+
// 此前 params 只透 q,config.hbcliBin 永远到不了能力层
|
|
1215
|
+
const itpA = await interpretEffect({ effect: 'ANYTHING_SEARCH', params: { ...q, hbcliBin: config.hbcliBin } })
|
|
1214
1216
|
if (!itpA.result) return declinedObservation('ANYTHING_SEARCH', itpA.trace)
|
|
1215
1217
|
const r = itpA.result
|
|
1216
1218
|
const dir = await ensureStateDir(config.stateRoot)
|
|
1217
1219
|
await recordLatency(join(dir, 'bridge-latency.jsonl'), Date.now() - started, `anything:${r.via}`).catch(() => {})
|
|
1218
1220
|
const top5 = (r.hits ?? []).slice(0, 5)
|
|
1219
1221
|
const summary = r.verdict === 'hit'
|
|
1220
|
-
? `${q.keyword} → hit (${r.hits?.length ?? 0} 候选项)\n${top5.map((h, i) => ` ${i + 1}. [${h.type}] ${h.name}${h.latitude !== undefined && h.longitude !== undefined ? ` @ (${h.latitude.toFixed(3)},${h.longitude.toFixed(3)})` : ''}`).join('\n')}\n${r.evidence}`
|
|
1222
|
+
? `${q.keyword} → hit (${r.hits?.length ?? 0} 候选项)\n${top5.map((h, i) => ` ${i + 1}. [${h.type}] ${h.name}${h.star ? ` ${h.star}★` : ''}${h.latitude !== undefined && h.longitude !== undefined ? ` @ (${h.latitude.toFixed(3)},${h.longitude.toFixed(3)})` : ''}${h.hotelId ? ` hotelId=${h.hotelId}` : h.destinationId ? ` destinationId=${h.destinationId}` : ''}`).join('\n')}\n${r.evidence}`
|
|
1221
1223
|
: r.verdict === 'miss'
|
|
1222
1224
|
? `${q.keyword} → miss (酒店-be 一切正常但无候选)\n${r.evidence}`
|
|
1223
1225
|
: `${q.keyword} → unavailable (${r.error})\n${r.evidence}`
|
package/ts/src/turn-deadline.ts
CHANGED
|
@@ -22,6 +22,10 @@ import { mkdir, readdir, readFile, rename, writeFile } from 'node:fs/promises'
|
|
|
22
22
|
import { join } from 'node:path'
|
|
23
23
|
import type { Context } from '@deepseek-ai/cordis'
|
|
24
24
|
import type { Agent } from '@deepseek-ai/dsh-agent'
|
|
25
|
+
// 类型面显式依赖:session/event、session/disposed 的事件表声明在 dsh-session 的
|
|
26
|
+
// cordis Events augmentation 里——不显式导入时,该声明只在 peer 恰好物化进
|
|
27
|
+
// ts/node_modules 的机器上可见(legacy-peer-deps 安装则缺,5 个 TS2345/TS7006)。
|
|
28
|
+
import type {} from '@deepseek-ai/dsh-session'
|
|
25
29
|
import type { ToolExecutionResult, ToolExecutionToken } from '@deepseek-ai/dsh-tools'
|
|
26
30
|
import { classifyTurn, TURN_HANDOFF_ETA_LABEL, turnPolicyFor, type TurnPolicy } from './turn-policy.ts'
|
|
27
31
|
|