@danceiny/gotry 0.0.1-rc.20 → 0.0.1-rc.21

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -5,7 +5,6 @@ import {
5
5
  assertSupportedJsonSchema,
6
6
  validateJsonSchemaValue,
7
7
  ToolArgsError,
8
- type JsonSchemaNode,
9
8
  type ObjectJsonSchema,
10
9
  type ToolDefinition,
11
10
  } from '@deepseek-ai/dsh-tools'
@@ -15,7 +14,9 @@ import {
15
14
  type TerminalOutputConfig,
16
15
  } from './benchmark-agent-conformance.ts'
17
16
 
18
- const SCHEMA_VERSION = 'gotry_benchmark_environment_bridge_v3'
17
+ // v4(#215 Round 12):terminal_output 携带 closed body schema(结构关键字白名单,
18
+ // 禁数据值注解面);v3 及更早配置缺该面,按契约不兼容 fail-closed。
19
+ const SCHEMA_VERSION = 'gotry_benchmark_environment_bridge_v4'
19
20
  export const BENCHMARK_TOOL_RESULT_SCHEMA_VERSION = 'gotry_benchmark_tool_result_v1'
20
21
  export const BENCHMARK_DOMAIN_RECOVERIES = ['none', 'retry_same', 'revise_arguments', 'choose_alternative'] as const
21
22
  type BenchmarkDomainRecovery = typeof BENCHMARK_DOMAIN_RECOVERIES[number]
@@ -55,38 +56,6 @@ export interface BenchmarkToolDescriptor {
55
56
  domain_outcomes: Array<{ status: 'miss' | 'error'; code: string; recovery: BenchmarkDomainRecovery }>
56
57
  }
57
58
 
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
-
90
59
  function plainObject(value: unknown): value is Record<string, unknown> {
91
60
  return typeof value === 'object' && value !== null && !Array.isArray(value)
92
61
  }
@@ -426,7 +395,15 @@ export function registerBenchmarkEnvironmentBridge(
426
395
  if (!subprocess) throw new Error('benchmark environment bridge subprocess unavailable')
427
396
 
428
397
  const parameters: Record<string, unknown> = deepFreezeJson({
429
- oneOf: descriptorSchema(bridge.tools).oneOf,
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 时传给工具的参数对象' },
404
+ },
405
+ required: ['action'],
406
+ additionalProperties: false,
430
407
  })
431
408
  assertSupportedJsonSchema(parameters)
432
409
 
@@ -444,11 +421,21 @@ export function registerBenchmarkEnvironmentBridge(
444
421
  const query = args as Record<string, unknown>
445
422
  const tool = query.tool
446
423
  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 })
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
+ }
449
433
  if (query.action !== 'call') {
450
434
  return jsonObject({ ok: false, error: 'invalid_action' })
451
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
+ }
452
439
  if (typeof tool !== 'string' || !descriptor) {
453
440
  return jsonObject({ ok: false, error: 'disallowed_tool' })
454
441
  }
@@ -457,9 +444,7 @@ export function registerBenchmarkEnvironmentBridge(
457
444
  }
458
445
  const callArguments = query.arguments
459
446
  const argumentViolations = validateJsonSchemaValue(descriptor.input_schema, callArguments, 'arguments')
460
- if (argumentViolations.length > 0) {
461
- return jsonObject({ ok: false, error: 'invalid_arguments' })
462
- }
447
+ if (argumentViolations.length > 0) throw new ToolArgsError(argumentViolations)
463
448
  const serialized = serializedArguments(callArguments)
464
449
  if (serialized.reason) return jsonObject({ ok: false, error: 'invalid_arguments', reason: serialized.reason })
465
450
  const controller = new AbortController()
@@ -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`); characters outside the runtime ref charset are mapped
430
- // deterministically to '.' so repair succeeds without burning the retry
431
- // budget. Anything the sanitizer cannot make unique enough still fails the
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
- if (!Array.isArray(factRefs)) return
436
- action.factRefs = factRefs.map((ref) => (typeof ref === 'string' ? ref.replace(/#/g, ':').replace(/[^A-Za-z0-9:._-]/g, '.') : ref))
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
- assertPlannerSafeRefs(decision.action)
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)) {