@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.
Files changed (60) hide show
  1. package/README.md +1 -1
  2. package/README.zh-CN.md +1 -1
  3. package/bin/gotry-bootstrap.js +83 -37
  4. package/bin/gotry-inner.js +11 -6
  5. package/cordis.gotry-patch.yml +7 -3
  6. package/dist/capabilities/anything.js +62 -29
  7. package/dist/capabilities/doctor.js +25 -3
  8. package/dist/capabilities/hbcli.js +28 -2
  9. package/dist/scripts/agent-planning-turn-deadline-e2e.js +2 -1
  10. package/dist/scripts/anything-tests.js +70 -21
  11. package/dist/scripts/benchmark-environment-bridge-e2e.js +136 -29
  12. package/dist/scripts/benchmark-environment-bridge-tests.js +1056 -269
  13. package/dist/scripts/bootstrap-tests.js +28 -2
  14. package/dist/scripts/doctor-tests.js +55 -1
  15. package/dist/scripts/hbcli-tests.js +38 -1
  16. package/dist/src/benchmark-agent-conformance.js +32 -12
  17. package/dist/src/benchmark-environment-bridge.js +317 -114
  18. package/dist/src/index.js +5 -2
  19. package/extension/manifest.json +2 -2
  20. package/package.json +2 -1
  21. package/ts/capabilities/anything.ts +89 -38
  22. package/ts/capabilities/hbcli.ts +53 -4
  23. package/ts/dsh-runtime/vendor/README.md +60 -0
  24. package/ts/dsh-runtime/vendor/dsh-map-tools/LICENSE +21 -0
  25. package/ts/dsh-runtime/vendor/dsh-map-tools/README.en.md +200 -0
  26. package/ts/dsh-runtime/vendor/dsh-map-tools/README.md +202 -0
  27. package/ts/dsh-runtime/vendor/dsh-map-tools/client/client.js +216 -0
  28. package/ts/dsh-runtime/vendor/dsh-map-tools/cordis.patch.yml +5 -0
  29. package/ts/dsh-runtime/vendor/dsh-map-tools/lib/clients/amap.js +371 -0
  30. package/ts/dsh-runtime/vendor/dsh-map-tools/lib/clients/nominatim.js +63 -0
  31. package/ts/dsh-runtime/vendor/dsh-map-tools/lib/clients/osrm.js +56 -0
  32. package/ts/dsh-runtime/vendor/dsh-map-tools/lib/clients/photon.js +55 -0
  33. package/ts/dsh-runtime/vendor/dsh-map-tools/lib/config-file.js +87 -0
  34. package/ts/dsh-runtime/vendor/dsh-map-tools/lib/config-route.js +126 -0
  35. package/ts/dsh-runtime/vendor/dsh-map-tools/lib/config.js +16 -0
  36. package/ts/dsh-runtime/vendor/dsh-map-tools/lib/index.js +109 -0
  37. package/ts/dsh-runtime/vendor/dsh-map-tools/lib/settings-ns.js +27 -0
  38. package/ts/dsh-runtime/vendor/dsh-map-tools/lib/tools/geocode.js +127 -0
  39. package/ts/dsh-runtime/vendor/dsh-map-tools/lib/tools/poi.js +84 -0
  40. package/ts/dsh-runtime/vendor/dsh-map-tools/lib/tools/routes.js +170 -0
  41. package/ts/dsh-runtime/vendor/dsh-map-tools/lib/types/clients/amap.d.ts +53 -0
  42. package/ts/dsh-runtime/vendor/dsh-map-tools/lib/types/clients/nominatim.d.ts +17 -0
  43. package/ts/dsh-runtime/vendor/dsh-map-tools/lib/types/clients/osrm.d.ts +15 -0
  44. package/ts/dsh-runtime/vendor/dsh-map-tools/lib/types/clients/photon.d.ts +22 -0
  45. package/ts/dsh-runtime/vendor/dsh-map-tools/lib/types/config-file.d.ts +33 -0
  46. package/ts/dsh-runtime/vendor/dsh-map-tools/lib/types/config-route.d.ts +20 -0
  47. package/ts/dsh-runtime/vendor/dsh-map-tools/lib/types/config.d.ts +23 -0
  48. package/ts/dsh-runtime/vendor/dsh-map-tools/lib/types/index.d.ts +18 -0
  49. package/ts/dsh-runtime/vendor/dsh-map-tools/lib/types/settings-ns.d.ts +14 -0
  50. package/ts/dsh-runtime/vendor/dsh-map-tools/lib/types/tools/geocode.d.ts +11 -0
  51. package/ts/dsh-runtime/vendor/dsh-map-tools/lib/types/tools/poi.d.ts +9 -0
  52. package/ts/dsh-runtime/vendor/dsh-map-tools/lib/types/tools/routes.d.ts +16 -0
  53. package/ts/dsh-runtime/vendor/dsh-map-tools/lib/types/types.d.ts +55 -0
  54. package/ts/dsh-runtime/vendor/dsh-map-tools/lib/types.js +16 -0
  55. package/ts/dsh-runtime/vendor/dsh-map-tools/package.json +91 -0
  56. package/ts/package.json +9 -1
  57. package/ts/src/benchmark-agent-conformance.ts +40 -9
  58. package/ts/src/benchmark-environment-bridge.ts +235 -66
  59. package/ts/src/index.ts +4 -2
  60. 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 { defineTool } from '@deepseek-ai/dsh-tools'
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 = 'gotry_benchmark_environment_bridge_v2'
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 CONFIG_KEYS = ['allowed_tools', 'argv_prefix', 'cwd', 'enabled', 'executable', 'isolation', 'max_output_bytes', 'schema_version', 'terminal_output', 'timeout_ms']
14
- const OPTIONAL_CONFIG_KEYS = ['allowed_output_keys']
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
- allowed_tools: string[]
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 allowedOutputKeys(value: unknown, allowedTools: string[]): value is Record<string, string[]> {
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
- const keys = Object.keys(value)
67
- return keys.length > 0 && keys.length <= 64
68
- && keys.every(tool => allowedTools.includes(tool) && identifiers(value[tool]))
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
- || ![baseKeys, extendedKeys].some(keys => JSON.stringify(configKeys) === JSON.stringify(keys))
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
- || !identifiers(value.allowed_tools)
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
- || (Object.hasOwn(value, 'allowed_output_keys') && !allowedOutputKeys(value.allowed_output_keys, value.allowed_tools))) return false
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: ReturnType<typeof defineTool>) => void
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
- register(defineTool({
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
- + 'Recoverable domain-error contract: every failure returns { ok:false, error:<code>, ... } from a closed vocabulary; '
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: { type: 'json' },
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 action = args?.action
290
- if (action === 'errors') return jsonObject({ ok: true, errors: BRIDGE_ERROR_CONTRACT })
291
- if (action === 'tools') return jsonObject({ ok: true, tools: bridge.allowed_tools })
292
- if (action !== 'call') return jsonObject({ ok: false, error: 'invalid_action' })
293
- const query = args as unknown as { tool?: unknown; arguments?: unknown }
294
- if (typeof query.tool !== 'string' || !bridge.allowed_tools.includes(query.tool)) {
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 (query.arguments !== undefined && !plainObject(query.arguments)) {
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', query.tool, serialized.json!],
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
- const visibleResult = parsed.result ?? parsed
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
- const outputKeys = bridge.allowed_output_keys && Object.hasOwn(bridge.allowed_output_keys, query.tool)
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([...bridge.allowed_tools]),
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
- const itpA = await interpretEffect({ effect: 'ANYTHING_SEARCH', params: q })
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}`
@@ -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