@danceiny/gotry 0.0.1-rc.19 → 0.0.1-rc.20
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 +1 -1
- package/README.zh-CN.md +1 -1
- package/bin/gotry-bootstrap.js +83 -37
- package/bin/gotry-inner.js +11 -6
- package/cordis.gotry-patch.yml +7 -3
- package/dist/capabilities/anything.js +62 -29
- package/dist/capabilities/doctor.js +25 -3
- package/dist/capabilities/hbcli.js +28 -2
- 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 +136 -29
- package/dist/scripts/benchmark-environment-bridge-tests.js +1056 -269
- package/dist/scripts/bootstrap-tests.js +28 -2
- package/dist/scripts/doctor-tests.js +55 -1
- package/dist/scripts/hbcli-tests.js +38 -1
- package/dist/src/benchmark-agent-conformance.js +32 -12
- package/dist/src/benchmark-environment-bridge.js +317 -114
- package/dist/src/index.js +5 -2
- package/extension/manifest.json +2 -2
- package/package.json +2 -1
- package/ts/capabilities/anything.ts +89 -38
- package/ts/capabilities/hbcli.ts +53 -4
- 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 +9 -1
- package/ts/src/benchmark-agent-conformance.ts +40 -9
- package/ts/src/benchmark-environment-bridge.ts +235 -66
- package/ts/src/index.ts +4 -2
- package/ts/src/turn-deadline.ts +4 -0
|
@@ -1,17 +1,34 @@
|
|
|
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 JsonSchemaNode,
|
|
9
|
+
type ObjectJsonSchema,
|
|
10
|
+
type ToolDefinition,
|
|
11
|
+
} from '@deepseek-ai/dsh-tools'
|
|
5
12
|
import {
|
|
6
13
|
validateTerminalOutputConfig,
|
|
7
14
|
type BenchmarkBridgeProjection,
|
|
8
15
|
type TerminalOutputConfig,
|
|
9
16
|
} from './benchmark-agent-conformance.ts'
|
|
10
17
|
|
|
11
|
-
const SCHEMA_VERSION = '
|
|
18
|
+
const SCHEMA_VERSION = 'gotry_benchmark_environment_bridge_v3'
|
|
19
|
+
export const BENCHMARK_TOOL_RESULT_SCHEMA_VERSION = 'gotry_benchmark_tool_result_v1'
|
|
20
|
+
export const BENCHMARK_DOMAIN_RECOVERIES = ['none', 'retry_same', 'revise_arguments', 'choose_alternative'] as const
|
|
21
|
+
type BenchmarkDomainRecovery = typeof BENCHMARK_DOMAIN_RECOVERIES[number]
|
|
12
22
|
const IDENTIFIER = /^[A-Za-z][A-Za-z0-9_.-]*$/
|
|
13
|
-
const
|
|
14
|
-
const
|
|
23
|
+
const RECOVERIES = new Set<string>(BENCHMARK_DOMAIN_RECOVERIES)
|
|
24
|
+
const CONFIG_KEYS = ['argv_prefix', 'cwd', 'enabled', 'executable', 'isolation', 'max_output_bytes', 'schema_version', 'terminal_output', 'timeout_ms', 'tools']
|
|
25
|
+
const INPUT_SCHEMA_MAX_BYTES = 16 * 1024
|
|
26
|
+
const INPUT_SCHEMA_MAX_DEPTH = 8
|
|
27
|
+
const INPUT_SCHEMA_MAX_NODES = 256
|
|
28
|
+
const INPUT_SCHEMA_MAX_PROPERTIES = 256
|
|
29
|
+
const INPUT_SCHEMA_MAX_ENUM_VALUES = 256
|
|
30
|
+
const INPUT_SCHEMA_MAX_DESCRIPTION_LENGTH = 512
|
|
31
|
+
const INPUT_SCHEMA_TYPES = new Set(['string', 'number', 'integer', 'boolean', 'null', 'array', 'object'])
|
|
15
32
|
|
|
16
33
|
export interface BenchmarkEnvironmentBridgeConfig {
|
|
17
34
|
schema_version: typeof SCHEMA_VERSION
|
|
@@ -19,8 +36,7 @@ export interface BenchmarkEnvironmentBridgeConfig {
|
|
|
19
36
|
executable: string
|
|
20
37
|
cwd: string
|
|
21
38
|
argv_prefix: string[]
|
|
22
|
-
|
|
23
|
-
allowed_output_keys?: Record<string, string[]>
|
|
39
|
+
tools: BenchmarkToolDescriptor[]
|
|
24
40
|
timeout_ms: number
|
|
25
41
|
max_output_bytes: number
|
|
26
42
|
terminal_output: TerminalOutputConfig
|
|
@@ -31,10 +47,56 @@ export interface BenchmarkEnvironmentBridgeConfig {
|
|
|
31
47
|
}
|
|
32
48
|
}
|
|
33
49
|
|
|
50
|
+
export interface BenchmarkToolDescriptor {
|
|
51
|
+
name: string
|
|
52
|
+
description: string
|
|
53
|
+
input_schema: ObjectJsonSchema
|
|
54
|
+
output_keys: string[]
|
|
55
|
+
domain_outcomes: Array<{ status: 'miss' | 'error'; code: string; recovery: BenchmarkDomainRecovery }>
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function descriptorSchema(tools: readonly BenchmarkToolDescriptor[]): JsonSchemaNode {
|
|
59
|
+
return {
|
|
60
|
+
oneOf: [
|
|
61
|
+
{
|
|
62
|
+
type: 'object',
|
|
63
|
+
description: 'List the frozen benchmark tool descriptors.',
|
|
64
|
+
properties: { action: { type: 'string', const: 'tools' } },
|
|
65
|
+
required: ['action'],
|
|
66
|
+
additionalProperties: false,
|
|
67
|
+
},
|
|
68
|
+
{
|
|
69
|
+
type: 'object',
|
|
70
|
+
description: 'List the frozen bridge error contract.',
|
|
71
|
+
properties: { action: { type: 'string', const: 'errors' } },
|
|
72
|
+
required: ['action'],
|
|
73
|
+
additionalProperties: false,
|
|
74
|
+
},
|
|
75
|
+
...tools.map<JsonSchemaNode>(tool => ({
|
|
76
|
+
type: 'object',
|
|
77
|
+
description: tool.description,
|
|
78
|
+
properties: {
|
|
79
|
+
action: { type: 'string', const: 'call' },
|
|
80
|
+
tool: { type: 'string', const: tool.name },
|
|
81
|
+
arguments: tool.input_schema,
|
|
82
|
+
},
|
|
83
|
+
required: ['action', 'tool', 'arguments'],
|
|
84
|
+
additionalProperties: false,
|
|
85
|
+
})),
|
|
86
|
+
],
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
|
|
34
90
|
function plainObject(value: unknown): value is Record<string, unknown> {
|
|
35
91
|
return typeof value === 'object' && value !== null && !Array.isArray(value)
|
|
36
92
|
}
|
|
37
93
|
|
|
94
|
+
function deepFreezeJson<T>(value: T): T {
|
|
95
|
+
if (!value || typeof value !== 'object' || Object.isFrozen(value)) return value
|
|
96
|
+
for (const child of Object.values(value as Record<string, unknown>)) deepFreezeJson(child)
|
|
97
|
+
return Object.freeze(value)
|
|
98
|
+
}
|
|
99
|
+
|
|
38
100
|
function existingExecutable(value: unknown): value is string {
|
|
39
101
|
if (typeof value !== 'string' || !isAbsolute(value)) return false
|
|
40
102
|
try {
|
|
@@ -55,17 +117,111 @@ function existingDirectory(value: unknown): value is string {
|
|
|
55
117
|
}
|
|
56
118
|
}
|
|
57
119
|
|
|
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(
|
|
120
|
+
function identifiers(value: unknown, allowEmpty = false): value is string[] {
|
|
121
|
+
return Array.isArray(value) && (allowEmpty || value.length > 0) && value.length <= 64 && new Set(value).size === value.length && value.every(
|
|
60
122
|
item => typeof item === 'string' && item.length > 0 && IDENTIFIER.test(item),
|
|
61
123
|
)
|
|
62
124
|
}
|
|
63
125
|
|
|
64
|
-
function
|
|
126
|
+
function exactKeys(value: Record<string, unknown>, allowed: readonly string[]): boolean {
|
|
127
|
+
return Object.keys(value).every(key => allowed.includes(key))
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
function validDescription(value: unknown, required = false): boolean {
|
|
131
|
+
if (value === undefined) return !required
|
|
132
|
+
return typeof value === 'string'
|
|
133
|
+
&& (!required || value.trim().length > 0)
|
|
134
|
+
&& value.length <= INPUT_SCHEMA_MAX_DESCRIPTION_LENGTH
|
|
135
|
+
&& !/[\u0000-\u001f\u007f]/.test(value)
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
function scalarMatches(type: string, value: unknown): boolean {
|
|
139
|
+
if (type === 'string') return typeof value === 'string' && value.length <= 512
|
|
140
|
+
if (type === 'number') return typeof value === 'number' && Number.isFinite(value)
|
|
141
|
+
if (type === 'integer') return typeof value === 'number' && Number.isSafeInteger(value)
|
|
142
|
+
if (type === 'boolean') return typeof value === 'boolean'
|
|
143
|
+
return type === 'null' && value === null
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
function validInputSchema(value: unknown): value is ObjectJsonSchema {
|
|
65
147
|
if (!plainObject(value)) return false
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
148
|
+
let serialized: string
|
|
149
|
+
try {
|
|
150
|
+
serialized = JSON.stringify(value)
|
|
151
|
+
} catch {
|
|
152
|
+
return false
|
|
153
|
+
}
|
|
154
|
+
if (Buffer.byteLength(serialized, 'utf8') > INPUT_SCHEMA_MAX_BYTES) return false
|
|
155
|
+
|
|
156
|
+
const pending: Array<{ node: Record<string, unknown>; depth: number }> = [{ node: value, depth: 0 }]
|
|
157
|
+
const seen = new Set<object>()
|
|
158
|
+
let nodes = 0
|
|
159
|
+
let properties = 0
|
|
160
|
+
let enumValues = 0
|
|
161
|
+
while (pending.length > 0) {
|
|
162
|
+
const { node, depth } = pending.pop()!
|
|
163
|
+
if (seen.has(node) || ++nodes > INPUT_SCHEMA_MAX_NODES || depth > INPUT_SCHEMA_MAX_DEPTH) return false
|
|
164
|
+
seen.add(node)
|
|
165
|
+
if (!INPUT_SCHEMA_TYPES.has(String(node.type)) || !validDescription(node.description)) return false
|
|
166
|
+
|
|
167
|
+
if (node.type === 'object') {
|
|
168
|
+
if (!exactKeys(node, ['type', 'description', 'properties', 'required', 'additionalProperties'])
|
|
169
|
+
|| !plainObject(node.properties)
|
|
170
|
+
|| !Array.isArray(node.required)
|
|
171
|
+
|| node.additionalProperties !== false) return false
|
|
172
|
+
const propertyEntries = Object.entries(node.properties)
|
|
173
|
+
properties += propertyEntries.length
|
|
174
|
+
if (properties > INPUT_SCHEMA_MAX_PROPERTIES || propertyEntries.some(([key, child]) => !IDENTIFIER.test(key) || !plainObject(child))) return false
|
|
175
|
+
const required = node.required
|
|
176
|
+
if (required.length > propertyEntries.length
|
|
177
|
+
|| new Set(required).size !== required.length
|
|
178
|
+
|| required.some(key => typeof key !== 'string' || !Object.hasOwn(node.properties as Record<string, unknown>, key))) return false
|
|
179
|
+
for (const [, child] of propertyEntries) pending.push({ node: child as Record<string, unknown>, depth: depth + 1 })
|
|
180
|
+
continue
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
if (node.type === 'array') {
|
|
184
|
+
if (!exactKeys(node, ['type', 'description', 'items']) || !plainObject(node.items)) return false
|
|
185
|
+
pending.push({ node: node.items, depth: depth + 1 })
|
|
186
|
+
continue
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
if (!exactKeys(node, ['type', 'description', 'enum', 'const'])
|
|
190
|
+
|| (Object.hasOwn(node, 'enum') && Object.hasOwn(node, 'const'))) return false
|
|
191
|
+
if (Object.hasOwn(node, 'const') && !scalarMatches(String(node.type), node.const)) return false
|
|
192
|
+
if (Object.hasOwn(node, 'enum')) {
|
|
193
|
+
if (!Array.isArray(node.enum) || node.enum.length === 0 || node.enum.length > 64) return false
|
|
194
|
+
enumValues += node.enum.length
|
|
195
|
+
if (enumValues > INPUT_SCHEMA_MAX_ENUM_VALUES
|
|
196
|
+
|| new Set(node.enum).size !== node.enum.length
|
|
197
|
+
|| node.enum.some(item => !scalarMatches(String(node.type), item))) return false
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
return value.type === 'object'
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
function toolDescriptors(value: unknown): value is BenchmarkToolDescriptor[] {
|
|
204
|
+
if (!Array.isArray(value) || value.length === 0 || value.length > 64) return false
|
|
205
|
+
const names = new Set<string>()
|
|
206
|
+
return value.every(item => {
|
|
207
|
+
if (!plainObject(item)) return false
|
|
208
|
+
const keys = Object.keys(item).sort()
|
|
209
|
+
const required = ['description', 'domain_outcomes', 'input_schema', 'name']
|
|
210
|
+
if (JSON.stringify(keys) !== JSON.stringify([...required, 'output_keys'].sort())) return false
|
|
211
|
+
if (typeof item.name !== 'string' || !IDENTIFIER.test(item.name) || names.has(item.name)) return false
|
|
212
|
+
names.add(item.name)
|
|
213
|
+
if (!validDescription(item.description, true) || !validInputSchema(item.input_schema)) return false
|
|
214
|
+
if (!Array.isArray(item.domain_outcomes) || item.domain_outcomes.length > 64) return false
|
|
215
|
+
const outcomeKeys = new Set<string>()
|
|
216
|
+
if (!item.domain_outcomes.every(outcome => plainObject(outcome)
|
|
217
|
+
&& JSON.stringify(Object.keys(outcome).sort()) === JSON.stringify(['code', 'recovery', 'status'])
|
|
218
|
+
&& (outcome.status === 'miss' || outcome.status === 'error')
|
|
219
|
+
&& typeof outcome.code === 'string' && IDENTIFIER.test(outcome.code)
|
|
220
|
+
&& typeof outcome.recovery === 'string' && RECOVERIES.has(outcome.recovery)
|
|
221
|
+
&& !outcomeKeys.has(outcome.code)
|
|
222
|
+
&& (outcomeKeys.add(outcome.code), true))) return false
|
|
223
|
+
return identifiers(item.output_keys)
|
|
224
|
+
})
|
|
69
225
|
}
|
|
70
226
|
|
|
71
227
|
// These are passed as direct argv entries (never through a shell), so options
|
|
@@ -98,20 +254,19 @@ function validBound(value: unknown, maximum: number): value is number {
|
|
|
98
254
|
function validate(value: unknown): value is BenchmarkEnvironmentBridgeConfig {
|
|
99
255
|
const configKeys = plainObject(value) ? Object.keys(value).sort() : []
|
|
100
256
|
const baseKeys = [...CONFIG_KEYS].sort()
|
|
101
|
-
const extendedKeys = [...CONFIG_KEYS, ...OPTIONAL_CONFIG_KEYS].sort()
|
|
102
257
|
if (!plainObject(value)
|
|
103
|
-
||
|
|
258
|
+
|| JSON.stringify(configKeys) !== JSON.stringify(baseKeys)
|
|
104
259
|
|| value.schema_version !== SCHEMA_VERSION
|
|
105
260
|
|| value.enabled !== true
|
|
106
261
|
|| !existingExecutable(value.executable)
|
|
107
262
|
|| !existingDirectory(value.cwd)
|
|
108
263
|
|| !argvPrefix(value.argv_prefix)
|
|
109
|
-
|| !
|
|
264
|
+
|| !toolDescriptors(value.tools)
|
|
110
265
|
|| !validBound(value.timeout_ms, 120_000)
|
|
111
266
|
|| !validBound(value.max_output_bytes, 10 * 1024 * 1024)
|
|
112
267
|
|| !validateTerminalOutputConfig(value.terminal_output)
|
|
113
268
|
|| !exactIsolation(value.isolation)
|
|
114
|
-
|
|
269
|
+
) return false
|
|
115
270
|
return true
|
|
116
271
|
}
|
|
117
272
|
|
|
@@ -124,13 +279,13 @@ export function loadBenchmarkEnvironmentConfig(path: string): BenchmarkEnvironme
|
|
|
124
279
|
if (typeof getuid !== 'function' || metadata.uid !== getuid()) return null
|
|
125
280
|
if ((metadata.mode & 0o022) !== 0) return null
|
|
126
281
|
const parsed: unknown = JSON.parse(readFileSync(path, 'utf8'))
|
|
127
|
-
return validate(parsed) ? parsed : null
|
|
282
|
+
return validate(parsed) ? deepFreezeJson(parsed) : null
|
|
128
283
|
} catch {
|
|
129
284
|
return null
|
|
130
285
|
}
|
|
131
286
|
}
|
|
132
287
|
|
|
133
|
-
type Register = (tool:
|
|
288
|
+
type Register = (tool: ToolDefinition) => void
|
|
134
289
|
interface CollectedText {
|
|
135
290
|
readFrom(offset: number): { text: string; nextOffset: number; lossy?: boolean }
|
|
136
291
|
}
|
|
@@ -176,6 +331,19 @@ const FORBIDDEN_OUTPUT_KEY_FRAGMENTS = [
|
|
|
176
331
|
'loadermetadata', 'oracle', 'reference', 'reward', 'score',
|
|
177
332
|
]
|
|
178
333
|
|
|
334
|
+
export const BRIDGE_ERROR_CONTRACT: Record<string, { recoverable: boolean; remedy: string }> = Object.freeze({
|
|
335
|
+
invalid_action: { recoverable: true, remedy: 'action 只允许 tools、call 或 errors' },
|
|
336
|
+
disallowed_tool: { recoverable: true, remedy: '先 action=tools 列出 allowed_tools 清单,再从清单内选工具' },
|
|
337
|
+
invalid_arguments: { recoverable: true, remedy: 'arguments 必须是可序列化 JSON 对象(≤64KB、深度≤12);收窄后重试' },
|
|
338
|
+
timed_out: { recoverable: true, remedy: '上游超时;可原样重试一次或换其他工具' },
|
|
339
|
+
output_truncated: { recoverable: true, remedy: '输出超出上限;让被调工具收窄查询范围后重试' },
|
|
340
|
+
invalid_json: { recoverable: true, remedy: '上游输出不是合法 JSON;重试一次或换工具' },
|
|
341
|
+
invalid_output: { recoverable: true, remedy: '上游输出结构不符;重试或换工具' },
|
|
342
|
+
runner_failed: { recoverable: true, remedy: '上游进程非零退出;可重试一次,持续失败换工具' },
|
|
343
|
+
spawn_failed: { recoverable: false, remedy: '可执行文件不可用——环境问题,调用方不可恢复' },
|
|
344
|
+
forbidden_output: { recoverable: false, remedy: '输出含未授权键(策略边界)——不可恢复,换工具或放弃' },
|
|
345
|
+
})
|
|
346
|
+
|
|
179
347
|
function inspectOutput(value: unknown): 'ok' | 'forbidden_key' | 'structure_limit' {
|
|
180
348
|
const pending: Array<{ value: unknown; depth: number }> = [{ value, depth: 0 }]
|
|
181
349
|
let nodes = 0
|
|
@@ -199,19 +367,22 @@ function inspectOutput(value: unknown): 'ok' | 'forbidden_key' | 'structure_limi
|
|
|
199
367
|
|
|
200
368
|
function inspectAllowedOutput(value: unknown, allowedKeys: string[]): boolean {
|
|
201
369
|
const allowed = new Set(allowedKeys)
|
|
202
|
-
const pending: Array<{ value: unknown; depth: number }> = [{ value, depth: 0 }]
|
|
370
|
+
const pending: Array<{ value: unknown; depth: number; coveredByDeclaredKey: boolean }> = [{ value, depth: 0, coveredByDeclaredKey: false }]
|
|
203
371
|
let nodes = 0
|
|
204
372
|
while (pending.length > 0) {
|
|
205
373
|
const current = pending.pop()!
|
|
206
374
|
if (++nodes > 10_000 || current.depth > 24) return false
|
|
207
375
|
if (Array.isArray(current.value)) {
|
|
208
|
-
for (const child of current.value) pending.push({ value: child, depth: current.depth + 1 })
|
|
376
|
+
for (const child of current.value) pending.push({ value: child, depth: current.depth + 1, coveredByDeclaredKey: current.coveredByDeclaredKey })
|
|
377
|
+
continue
|
|
378
|
+
}
|
|
379
|
+
if (!plainObject(current.value)) {
|
|
380
|
+
if (!current.coveredByDeclaredKey) return false
|
|
209
381
|
continue
|
|
210
382
|
}
|
|
211
|
-
if (!plainObject(current.value)) continue
|
|
212
383
|
for (const [key, child] of Object.entries(current.value)) {
|
|
213
384
|
if (!allowed.has(key)) return false
|
|
214
|
-
pending.push({ value: child, depth: current.depth + 1 })
|
|
385
|
+
pending.push({ value: child, depth: current.depth + 1, coveredByDeclaredKey: true })
|
|
215
386
|
}
|
|
216
387
|
}
|
|
217
388
|
return true
|
|
@@ -244,21 +415,6 @@ function serializedArguments(value: Record<string, unknown>): { json: string; re
|
|
|
244
415
|
}
|
|
245
416
|
}
|
|
246
417
|
|
|
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
418
|
/** Register the opt-in model-facing bridge. */
|
|
263
419
|
export function registerBenchmarkEnvironmentBridge(
|
|
264
420
|
path: string,
|
|
@@ -269,35 +425,41 @@ export function registerBenchmarkEnvironmentBridge(
|
|
|
269
425
|
if (!bridge) throw new Error('benchmark environment bridge configuration unavailable')
|
|
270
426
|
if (!subprocess) throw new Error('benchmark environment bridge subprocess unavailable')
|
|
271
427
|
|
|
272
|
-
|
|
428
|
+
const parameters: Record<string, unknown> = deepFreezeJson({
|
|
429
|
+
oneOf: descriptorSchema(bridge.tools).oneOf,
|
|
430
|
+
})
|
|
431
|
+
assertSupportedJsonSchema(parameters)
|
|
432
|
+
|
|
433
|
+
const definition: ToolDefinition = {
|
|
273
434
|
name: 'gotry_benchmark_environment',
|
|
274
|
-
description: 'When a prompt asks to run agent_env.cli, use action=call here with the mapped tool; arbitrary shell is not exposed.
|
|
275
|
-
|
|
276
|
-
+ 'call action=errors to fetch the per-code recoverable flag and remedy before retrying.',
|
|
277
|
-
// Round 8(issue #100/#102):generic typed schema——模型可见逐字段契约(与产品面 D-30 同刀法);
|
|
278
|
-
// arguments 保持 additionalProperties:true(被调工具的参数面由其自身契约定义)
|
|
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)' },
|
|
283
|
-
},
|
|
435
|
+
description: 'When a prompt asks to run agent_env.cli, use action=call here with the mapped tool; arbitrary shell is not exposed.',
|
|
436
|
+
parameters,
|
|
284
437
|
output: {
|
|
285
|
-
schema: {
|
|
438
|
+
schema: {},
|
|
286
439
|
render: (_args, value) => [{ type: 'text', text: JSON.stringify(value) }],
|
|
287
440
|
},
|
|
288
|
-
async execute(args): Promise<Record<string, never>> {
|
|
289
|
-
const
|
|
290
|
-
if (
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
const
|
|
294
|
-
if (
|
|
441
|
+
async execute(args: unknown): Promise<Record<string, never>> {
|
|
442
|
+
const violations = validateJsonSchemaValue(parameters, args, 'arguments')
|
|
443
|
+
if (violations.length > 0) throw new ToolArgsError(violations)
|
|
444
|
+
const query = args as Record<string, unknown>
|
|
445
|
+
const tool = query.tool
|
|
446
|
+
const descriptor = bridge.tools.find(item => item.name === tool)
|
|
447
|
+
if (query.action === 'tools') return jsonObject({ ok: true, tools: bridge.tools })
|
|
448
|
+
if (query.action === 'errors') return jsonObject({ ok: true, errors: BRIDGE_ERROR_CONTRACT })
|
|
449
|
+
if (query.action !== 'call') {
|
|
450
|
+
return jsonObject({ ok: false, error: 'invalid_action' })
|
|
451
|
+
}
|
|
452
|
+
if (typeof tool !== 'string' || !descriptor) {
|
|
295
453
|
return jsonObject({ ok: false, error: 'disallowed_tool' })
|
|
296
454
|
}
|
|
297
|
-
if (
|
|
455
|
+
if (!plainObject(query.arguments)) {
|
|
456
|
+
return jsonObject({ ok: false, error: 'invalid_arguments' })
|
|
457
|
+
}
|
|
458
|
+
const callArguments = query.arguments
|
|
459
|
+
const argumentViolations = validateJsonSchemaValue(descriptor.input_schema, callArguments, 'arguments')
|
|
460
|
+
if (argumentViolations.length > 0) {
|
|
298
461
|
return jsonObject({ ok: false, error: 'invalid_arguments' })
|
|
299
462
|
}
|
|
300
|
-
const callArguments = query.arguments === undefined ? {} : query.arguments
|
|
301
463
|
const serialized = serializedArguments(callArguments)
|
|
302
464
|
if (serialized.reason) return jsonObject({ ok: false, error: 'invalid_arguments', reason: serialized.reason })
|
|
303
465
|
const controller = new AbortController()
|
|
@@ -315,7 +477,7 @@ export function registerBenchmarkEnvironmentBridge(
|
|
|
315
477
|
env.PYTHONDONTWRITEBYTECODE = '1'
|
|
316
478
|
env.PYTHONNOUSERSITE = '1'
|
|
317
479
|
handle = subprocess.spawn({
|
|
318
|
-
argv: [bridge.executable, ...bridge.argv_prefix, 'call',
|
|
480
|
+
argv: [bridge.executable, ...bridge.argv_prefix, 'call', tool, serialized.json!],
|
|
319
481
|
cwd: bridge.cwd,
|
|
320
482
|
stdio: {
|
|
321
483
|
stdin: 'ignore',
|
|
@@ -344,12 +506,18 @@ export function registerBenchmarkEnvironmentBridge(
|
|
|
344
506
|
const outputInspection = inspectOutput(parsed)
|
|
345
507
|
if (outputInspection === 'forbidden_key') return jsonObject({ ok: false, error: 'forbidden_output' })
|
|
346
508
|
if (outputInspection === 'structure_limit') return jsonObject({ ok: false, error: 'invalid_output' })
|
|
347
|
-
|
|
509
|
+
if (parsed.schema_version !== BENCHMARK_TOOL_RESULT_SCHEMA_VERSION || typeof parsed.status !== 'string') {
|
|
510
|
+
return jsonObject({ ok: false, error: 'invalid_output' })
|
|
511
|
+
}
|
|
512
|
+
if (parsed.status === 'miss' || parsed.status === 'error') {
|
|
513
|
+
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' })
|
|
514
|
+
if (Object.keys(parsed).sort().join(',') !== 'code,recovery,schema_version,status') return jsonObject({ ok: false, error: 'invalid_output' })
|
|
515
|
+
return jsonObject({ ok: true, outcome: parsed })
|
|
516
|
+
}
|
|
517
|
+
if (parsed.status !== 'ok' || !Object.hasOwn(parsed, 'result') || Object.keys(parsed).sort().join(',') !== 'result,schema_version,status') return jsonObject({ ok: false, error: 'invalid_output' })
|
|
518
|
+
const visibleResult = parsed.result
|
|
348
519
|
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' })
|
|
520
|
+
if (!inspectAllowedOutput(visibleResult, descriptor.output_keys)) return jsonObject({ ok: false, error: 'forbidden_output' })
|
|
353
521
|
return jsonObject({ ok: true, result: visibleResult })
|
|
354
522
|
} catch {
|
|
355
523
|
if (controller.signal.aborted) return jsonObject({ ok: false, error: 'timed_out' })
|
|
@@ -358,10 +526,11 @@ export function registerBenchmarkEnvironmentBridge(
|
|
|
358
526
|
clearTimeout(timer)
|
|
359
527
|
}
|
|
360
528
|
},
|
|
361
|
-
}
|
|
529
|
+
}
|
|
530
|
+
register(definition)
|
|
362
531
|
return Object.freeze({
|
|
363
532
|
toolName: 'gotry_benchmark_environment',
|
|
364
|
-
allowedTools: Object.freeze(
|
|
533
|
+
allowedTools: Object.freeze(bridge.tools.map(item => item.name)),
|
|
365
534
|
terminal: Object.freeze({ ...bridge.terminal_output }),
|
|
366
535
|
})
|
|
367
536
|
}
|
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
|
|