@danceiny/gotry 0.0.1-rc.18 → 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 (176) hide show
  1. package/README.md +42 -16
  2. package/README.zh-CN.md +37 -15
  3. package/bin/gotry-bootstrap.js +309 -30
  4. package/bin/gotry-inner.js +74 -23
  5. package/bin/gotry-runtime-resolution.d.ts +1 -1
  6. package/bin/gotry-runtime-resolution.js +7 -8
  7. package/cordis.gotry-patch.yml +22 -8
  8. package/data/stations-12306-verify.json +524 -0
  9. package/dist/capabilities/agent-reach.js +4 -4
  10. package/dist/capabilities/anything.js +62 -29
  11. package/dist/capabilities/channel-health.js +103 -0
  12. package/dist/capabilities/channel-registry.js +213 -0
  13. package/dist/capabilities/doctor.js +307 -0
  14. package/dist/capabilities/effect.js +178 -5
  15. package/dist/capabilities/flyai.js +13 -0
  16. package/dist/capabilities/hbcli.js +28 -2
  17. package/dist/capabilities/session/adapters/ctrip-hotel.js +207 -0
  18. package/dist/capabilities/session/adapters/rail-12306.js +285 -0
  19. package/dist/capabilities/session/extension-bridge.js +14 -1
  20. package/dist/capabilities/session/extension-channel.js +4 -2
  21. package/dist/capabilities/session/extension-distribution.js +1 -1
  22. package/dist/capabilities/session-search.js +311 -0
  23. package/dist/capabilities/visa-policy.js +126 -0
  24. package/dist/scripts/agent-planning-turn-deadline-e2e.js +305 -0
  25. package/dist/scripts/agent-planning-turn-deadline-tests.js +281 -0
  26. package/dist/scripts/anything-tests.js +70 -21
  27. package/dist/scripts/benchmark-environment-bridge-e2e.js +139 -37
  28. package/dist/scripts/benchmark-environment-bridge-tests.js +1099 -306
  29. package/dist/scripts/booking-copilot-availability-ledger-binding-tests.js +26 -26
  30. package/dist/scripts/{booking-copilot-availability-policy-v2-tests.js → booking-copilot-availability-policy-tests.js} +115 -115
  31. package/dist/scripts/booking-copilot-crossrepo-fixture-server.js +41 -1
  32. package/dist/scripts/booking-copilot-dsh-core-proof-tests.js +75 -120
  33. package/dist/scripts/booking-copilot-dsh-planner-proof-tests.js +370 -61
  34. package/dist/scripts/booking-copilot-dsh-plugin-proof-tests.js +2 -2
  35. package/dist/scripts/booking-copilot-event-sequence-concurrency-proof-tests.js +7 -3
  36. package/dist/scripts/booking-copilot-gap-code-contract-proof-tests.js +15 -9
  37. package/dist/scripts/booking-copilot-operation-ledger-concurrency-proof-tests.js +19 -11
  38. package/dist/scripts/booking-copilot-receipt-ledger-concurrency-proof-tests.js +16 -52
  39. package/dist/scripts/booking-copilot-runtime-proof-tests.js +3953 -155
  40. package/dist/scripts/booking-copilot-server-proof-tests.js +58 -23
  41. package/dist/scripts/booking-copilot-startup-proof-tests.js +10 -119
  42. package/dist/scripts/booking-surface-contract-proof-tests.js +1154 -326
  43. package/dist/scripts/bootstrap-tests.js +113 -2
  44. package/dist/scripts/build-metrics-report.js +339 -0
  45. package/dist/scripts/channel-probe-tests.js +148 -0
  46. package/dist/scripts/channel-probe.js +252 -0
  47. package/dist/scripts/channel-registry-tests.js +262 -0
  48. package/dist/scripts/doctor-tests.js +175 -0
  49. package/dist/scripts/effect-tests.js +274 -1
  50. package/dist/scripts/extension-tests.js +129 -11
  51. package/dist/scripts/fact-gate-tests.js +91 -2
  52. package/dist/scripts/flyai-tests.js +17 -1
  53. package/dist/scripts/hbcli-e2e-tests.js +1 -3
  54. package/dist/scripts/hbcli-tests.js +38 -1
  55. package/dist/scripts/metrics-report-tests.js +288 -0
  56. package/dist/scripts/persona-surface-guard-tests.js +19 -0
  57. package/dist/scripts/session-tests.js +401 -1
  58. package/dist/scripts/smoke.js +100 -83
  59. package/dist/scripts/turn-handoff-collect-tests.js +216 -0
  60. package/dist/scripts/turn-handoff-collect.js +189 -0
  61. package/dist/scripts/turn-policy-tests.js +56 -0
  62. package/dist/scripts/typed-contract-canary.js +261 -0
  63. package/dist/scripts/visa-policy-tests.js +75 -0
  64. package/dist/scripts/wish-channel-gate-tests.js +111 -0
  65. package/dist/src/artifact-gate.js +91 -3
  66. package/dist/src/benchmark-agent-conformance.js +39 -16
  67. package/dist/src/benchmark-environment-bridge.js +316 -52
  68. package/dist/src/bookable-facts.js +62 -3
  69. package/dist/src/booking-surface/{availability-policy-v2.js → availability-policy.js} +89 -89
  70. package/dist/src/booking-surface/canonical-schema.js +8 -22
  71. package/dist/src/booking-surface/contracts.js +70 -19
  72. package/dist/src/booking-surface/dsh-planner.js +310 -177
  73. package/dist/src/booking-surface/dsh-plugin.js +3 -3
  74. package/dist/src/booking-surface/index.js +1 -5
  75. package/dist/src/booking-surface/profile.js +1 -1
  76. package/dist/src/booking-surface/runtime.js +1682 -247
  77. package/dist/src/booking-surface/server.js +371 -185
  78. package/dist/src/booking-surface/startup.js +17 -27
  79. package/dist/src/booking-surface/validation.js +270 -760
  80. package/dist/src/index.js +739 -150
  81. package/dist/src/turn-deadline.js +292 -0
  82. package/dist/src/turn-policy.js +146 -0
  83. package/dist/src/wish-pool.js +5 -0
  84. package/extension/background.js +23 -4
  85. package/extension/content-main.js +47 -9
  86. package/extension/manifest.json +27 -9
  87. package/package.json +7 -26
  88. package/schemas/booking.surface.schema.json +2593 -0
  89. package/ts/capabilities/agent-reach.ts +4 -4
  90. package/ts/capabilities/anything.ts +89 -38
  91. package/ts/capabilities/flyai.ts +21 -3
  92. package/ts/capabilities/hbcli.ts +53 -4
  93. package/ts/capabilities/session/action-cache.ts +1 -1
  94. package/ts/capabilities/session/adapters/ctrip-hotel.ts +238 -0
  95. package/ts/capabilities/session/adapters/rail-12306.ts +293 -0
  96. package/ts/capabilities/session/extension-bridge.ts +32 -2
  97. package/ts/capabilities/session/extension-channel.ts +9 -8
  98. package/ts/capabilities/session/extension-distribution.ts +3 -1
  99. package/ts/capabilities/session/health-watch.ts +1 -1
  100. package/ts/capabilities/session/read-guard.ts +1 -1
  101. package/ts/capabilities/session/wizard.ts +1 -1
  102. package/ts/capabilities/session-search.ts +323 -1
  103. package/ts/dsh-runtime/vendor/README.md +60 -0
  104. package/ts/dsh-runtime/vendor/dsh-map-tools/LICENSE +21 -0
  105. package/ts/dsh-runtime/vendor/dsh-map-tools/README.en.md +200 -0
  106. package/ts/dsh-runtime/vendor/dsh-map-tools/README.md +202 -0
  107. package/ts/dsh-runtime/vendor/dsh-map-tools/client/client.js +216 -0
  108. package/ts/dsh-runtime/vendor/dsh-map-tools/cordis.patch.yml +5 -0
  109. package/ts/dsh-runtime/vendor/dsh-map-tools/lib/clients/amap.js +371 -0
  110. package/ts/dsh-runtime/vendor/dsh-map-tools/lib/clients/nominatim.js +63 -0
  111. package/ts/dsh-runtime/vendor/dsh-map-tools/lib/clients/osrm.js +56 -0
  112. package/ts/dsh-runtime/vendor/dsh-map-tools/lib/clients/photon.js +55 -0
  113. package/ts/dsh-runtime/vendor/dsh-map-tools/lib/config-file.js +87 -0
  114. package/ts/dsh-runtime/vendor/dsh-map-tools/lib/config-route.js +126 -0
  115. package/ts/dsh-runtime/vendor/dsh-map-tools/lib/config.js +16 -0
  116. package/ts/dsh-runtime/vendor/dsh-map-tools/lib/index.js +109 -0
  117. package/ts/dsh-runtime/vendor/dsh-map-tools/lib/settings-ns.js +27 -0
  118. package/ts/dsh-runtime/vendor/dsh-map-tools/lib/tools/geocode.js +127 -0
  119. package/ts/dsh-runtime/vendor/dsh-map-tools/lib/tools/poi.js +84 -0
  120. package/ts/dsh-runtime/vendor/dsh-map-tools/lib/tools/routes.js +170 -0
  121. package/ts/dsh-runtime/vendor/dsh-map-tools/lib/types/clients/amap.d.ts +53 -0
  122. package/ts/dsh-runtime/vendor/dsh-map-tools/lib/types/clients/nominatim.d.ts +17 -0
  123. package/ts/dsh-runtime/vendor/dsh-map-tools/lib/types/clients/osrm.d.ts +15 -0
  124. package/ts/dsh-runtime/vendor/dsh-map-tools/lib/types/clients/photon.d.ts +22 -0
  125. package/ts/dsh-runtime/vendor/dsh-map-tools/lib/types/config-file.d.ts +33 -0
  126. package/ts/dsh-runtime/vendor/dsh-map-tools/lib/types/config-route.d.ts +20 -0
  127. package/ts/dsh-runtime/vendor/dsh-map-tools/lib/types/config.d.ts +23 -0
  128. package/ts/dsh-runtime/vendor/dsh-map-tools/lib/types/index.d.ts +18 -0
  129. package/ts/dsh-runtime/vendor/dsh-map-tools/lib/types/settings-ns.d.ts +14 -0
  130. package/ts/dsh-runtime/vendor/dsh-map-tools/lib/types/tools/geocode.d.ts +11 -0
  131. package/ts/dsh-runtime/vendor/dsh-map-tools/lib/types/tools/poi.d.ts +9 -0
  132. package/ts/dsh-runtime/vendor/dsh-map-tools/lib/types/tools/routes.d.ts +16 -0
  133. package/ts/dsh-runtime/vendor/dsh-map-tools/lib/types/types.d.ts +55 -0
  134. package/ts/dsh-runtime/vendor/dsh-map-tools/lib/types.js +16 -0
  135. package/ts/dsh-runtime/vendor/dsh-map-tools/package.json +91 -0
  136. package/ts/package.json +9 -0
  137. package/ts/scripts/turn-handoff-collect.ts +178 -0
  138. package/ts/src/artifact-gate.ts +82 -2
  139. package/ts/src/benchmark-agent-conformance.ts +54 -15
  140. package/ts/src/benchmark-environment-bridge.ts +234 -46
  141. package/ts/src/bookable-facts.ts +109 -5
  142. package/ts/src/booking-saga.ts +1 -1
  143. package/ts/src/booking-surface/{availability-policy-v2.ts → availability-policy.ts} +124 -125
  144. package/ts/src/booking-surface/canonical-schema.js +8 -22
  145. package/ts/src/booking-surface/contracts.ts +278 -239
  146. package/ts/src/booking-surface/dsh-planner.ts +333 -194
  147. package/ts/src/booking-surface/dsh-plugin.js +3 -3
  148. package/ts/src/booking-surface/index.ts +1 -5
  149. package/ts/src/booking-surface/profile.ts +11 -11
  150. package/ts/src/booking-surface/runtime.ts +1351 -351
  151. package/ts/src/booking-surface/server.ts +282 -175
  152. package/ts/src/booking-surface/startup.ts +36 -48
  153. package/ts/src/booking-surface/validation.ts +194 -442
  154. package/ts/src/contracts.ts +1 -1
  155. package/ts/src/index.ts +492 -172
  156. package/ts/src/loop.ts +1 -1
  157. package/ts/src/state-ledger.ts +1 -1
  158. package/ts/src/turn-deadline.ts +365 -0
  159. package/ts/src/turn-policy.ts +154 -0
  160. package/ts/src/wish-pool.ts +17 -4
  161. package/dist/scripts/agent-planning-budget-e2e.js +0 -227
  162. package/dist/scripts/agent-planning-budget-tests.js +0 -173
  163. package/dist/scripts/booking-copilot-v2-runtime-proof-tests.js +0 -3935
  164. package/dist/scripts/booking-surface-v2-contract-proof-tests.js +0 -1174
  165. package/dist/src/booking-surface/contracts-v2.js +0 -89
  166. package/dist/src/booking-surface/runtime-v2.js +0 -1771
  167. package/dist/src/booking-surface/server-v2.js +0 -334
  168. package/dist/src/booking-surface/validation-v2.js +0 -319
  169. package/dist/src/tool-budget.js +0 -136
  170. package/schemas/booking.surface.v1.schema.json +0 -927
  171. package/schemas/booking.surface.v2.schema.json +0 -61
  172. package/ts/src/booking-surface/contracts-v2.ts +0 -118
  173. package/ts/src/booking-surface/runtime-v2.ts +0 -1466
  174. package/ts/src/booking-surface/server-v2.ts +0 -247
  175. package/ts/src/booking-surface/validation-v2.ts +0 -205
  176. package/ts/src/tool-budget.ts +0 -165
@@ -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
@@ -254,31 +425,41 @@ export function registerBenchmarkEnvironmentBridge(
254
425
  if (!bridge) throw new Error('benchmark environment bridge configuration unavailable')
255
426
  if (!subprocess) throw new Error('benchmark environment bridge subprocess unavailable')
256
427
 
257
- register(defineTool({
428
+ const parameters: Record<string, unknown> = deepFreezeJson({
429
+ oneOf: descriptorSchema(bridge.tools).oneOf,
430
+ })
431
+ assertSupportedJsonSchema(parameters)
432
+
433
+ const definition: ToolDefinition = {
258
434
  name: 'gotry_benchmark_environment',
259
435
  description: 'When a prompt asks to run agent_env.cli, use action=call here with the mapped tool; arbitrary shell is not exposed.',
260
- parameters: {
261
- query: {
262
- type: 'json',
263
- required: true,
264
- description: '{ action: "tools" | "call", tool?: string, arguments?: object }',
265
- },
266
- },
436
+ parameters,
267
437
  output: {
268
- schema: { type: 'json' },
438
+ schema: {},
269
439
  render: (_args, value) => [{ type: 'text', text: JSON.stringify(value) }],
270
440
  },
271
- async execute(args: { query?: unknown }): Promise<Record<string, never>> {
272
- const query = plainObject(args?.query) ? args.query : {}
273
- if (query.action === 'tools') return jsonObject({ ok: true, tools: bridge.allowed_tools })
274
- if (query.action !== 'call') return jsonObject({ ok: false, error: 'invalid_action' })
275
- 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) {
276
453
  return jsonObject({ ok: false, error: 'disallowed_tool' })
277
454
  }
278
- 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) {
279
461
  return jsonObject({ ok: false, error: 'invalid_arguments' })
280
462
  }
281
- const callArguments = query.arguments === undefined ? {} : query.arguments
282
463
  const serialized = serializedArguments(callArguments)
283
464
  if (serialized.reason) return jsonObject({ ok: false, error: 'invalid_arguments', reason: serialized.reason })
284
465
  const controller = new AbortController()
@@ -296,7 +477,7 @@ export function registerBenchmarkEnvironmentBridge(
296
477
  env.PYTHONDONTWRITEBYTECODE = '1'
297
478
  env.PYTHONNOUSERSITE = '1'
298
479
  handle = subprocess.spawn({
299
- argv: [bridge.executable, ...bridge.argv_prefix, 'call', query.tool, serialized.json!],
480
+ argv: [bridge.executable, ...bridge.argv_prefix, 'call', tool, serialized.json!],
300
481
  cwd: bridge.cwd,
301
482
  stdio: {
302
483
  stdin: 'ignore',
@@ -325,12 +506,18 @@ export function registerBenchmarkEnvironmentBridge(
325
506
  const outputInspection = inspectOutput(parsed)
326
507
  if (outputInspection === 'forbidden_key') return jsonObject({ ok: false, error: 'forbidden_output' })
327
508
  if (outputInspection === 'structure_limit') return jsonObject({ ok: false, error: 'invalid_output' })
328
- 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
329
519
  if (!plainObject(visibleResult) && !Array.isArray(visibleResult)) return jsonObject({ ok: false, error: 'invalid_output' })
330
- const outputKeys = bridge.allowed_output_keys && Object.hasOwn(bridge.allowed_output_keys, query.tool)
331
- ? bridge.allowed_output_keys[query.tool]
332
- : undefined
333
- 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' })
334
521
  return jsonObject({ ok: true, result: visibleResult })
335
522
  } catch {
336
523
  if (controller.signal.aborted) return jsonObject({ ok: false, error: 'timed_out' })
@@ -339,10 +526,11 @@ export function registerBenchmarkEnvironmentBridge(
339
526
  clearTimeout(timer)
340
527
  }
341
528
  },
342
- }))
529
+ }
530
+ register(definition)
343
531
  return Object.freeze({
344
532
  toolName: 'gotry_benchmark_environment',
345
- allowedTools: Object.freeze([...bridge.allowed_tools]),
533
+ allowedTools: Object.freeze(bridge.tools.map(item => item.name)),
346
534
  terminal: Object.freeze({ ...bridge.terminal_output }),
347
535
  })
348
536
  }
@@ -89,7 +89,34 @@ export interface PolicyFact {
89
89
  review_by?: string
90
90
  }
91
91
 
92
- export type BookableFact = FlightFact | PolicyFact
92
+ export type BookableFact = FlightFact | PolicyFact | HotelFact
93
+
94
+ /**
95
+ * 酒店事实(D-26,issue #118):exact-date 酒店检索(hit/miss)的唯一事实形态。
96
+ * 纪律与机/火同源:①未定档期的摸底检索不落账(无 exact-date 语义);
97
+ * ②needs-setup/error 是传输失败,永不落负事实;③价格上游打码(¥7xx)——
98
+ * 本事实**不落数字价**,只记在架家数,真实价以落地页为准(打码价保真纪律)。
99
+ */
100
+ export interface HotelFact {
101
+ schema: typeof BOOKABLE_FACT_SCHEMA
102
+ /** 语义派生 id:sha(destination|checkIn|checkOut|query_id)——同查询重放幂等 */
103
+ fact_id: string
104
+ kind: 'hotel'
105
+ /** 目的地中文(flyai/session 的 to;闸侧 claim 匹配键) */
106
+ destination: string
107
+ check_in?: string // YYYY-MM-DD
108
+ check_out?: string // YYYY-MM-DD
109
+ verdict: 'hit' | 'miss'
110
+ bookability: Bookability
111
+ /** 在架条数(hit 时;价格打码不落数字价) */
112
+ options_masked: number
113
+ source: string // flyai-hotel / session:ctrip-hotel
114
+ /** 可重放查询 id(如 flyai:hotel:大理:2026-10-01_2026-10-03) */
115
+ query_id: string
116
+ fetched_at: string
117
+ as_of: string
118
+ tier: EvidenceTier
119
+ }
93
120
 
94
121
  // ---------------------------------------------------------------------------
95
122
  // id 与查询词(可重放:query_id 即重放凭证)
@@ -133,7 +160,8 @@ export interface FlyaiLikeOption {
133
160
  }
134
161
 
135
162
  export interface FlyaiLikeResult {
136
- verdict: 'hit' | 'miss' | 'error'
163
+ /** needs-setup(试用额度达限)同 error:无结论不是证据,不落事实 */
164
+ verdict: 'hit' | 'miss' | 'error' | 'needs-setup'
137
165
  options?: FlyaiLikeOption[]
138
166
  evidence?: string
139
167
  }
@@ -190,7 +218,7 @@ export function factsFromFlyai(q: { kind: 'flight' | 'train'; origin: string; de
190
218
  const queryId = makeQueryId(source, q.kind, q.origin, q.destination, q.date)
191
219
  const origin = iataOf(alias, q.origin)
192
220
  const destination = iataOf(alias, q.destination)
193
- if (r.verdict === 'error') return []
221
+ if (r.verdict === 'error' || r.verdict === 'needs-setup') return []
194
222
  if (r.verdict === 'miss') return [negativeFact(queryId, q.kind, q.origin, q.destination, q.date, source, fetchedAt, alias)]
195
223
  const facts: FlightFact[] = []
196
224
  for (const o of r.options ?? []) {
@@ -278,7 +306,7 @@ export function dedupeFacts<T extends BookableFact>(facts: T[]): T[] {
278
306
  /** route+date 的最新查询结论(同日多次查询:最新一次为准,避免「昨天 miss 今天 hit」陈旧否定) */
279
307
  export function latestFactsForRouteDate(facts: BookableFact[], origin: string, destination: string, date: string): FlightFact[] {
280
308
  const hits = facts.filter((f): f is FlightFact =>
281
- f.kind !== 'policy'
309
+ (f.kind === 'flight' || f.kind === 'train')
282
310
  && f.route.origin === origin && f.route.destination === destination && f.date === date)
283
311
  const latestQueryTs = new Map<string, string>()
284
312
  for (const f of hits) {
@@ -310,7 +338,7 @@ export interface FlightClaim {
310
338
  export function flightClaimVerdict(facts: BookableFact[], claim: FlightClaim): { verdict: FlightClaimVerdict; fact?: FlightFact; reason: string } {
311
339
  const no = claim.flight_no.toUpperCase().replace(/\s+/g, '')
312
340
  const sameNo = facts.filter((f): f is FlightFact =>
313
- f.kind !== 'policy' && f.flight_no.toUpperCase() === no && f.bookability === 'bookable_exact_date')
341
+ (f.kind === 'flight' || f.kind === 'train') && f.flight_no.toUpperCase() === no && f.bookability === 'bookable_exact_date')
314
342
  const routeMatched = sameNo.filter(f =>
315
343
  (!claim.origin || f.route.origin === claim.origin || f.route.origin_airport === claim.origin)
316
344
  && (!claim.destination || f.route.destination === claim.destination || f.route.dest_airport === claim.destination)
@@ -502,6 +530,12 @@ export function defaultReviewBy(tripDate: string): string {
502
530
  * 永不输出无条件 ✓;证据链(source/query_id/fetched_at)随行必达。
503
531
  */
504
532
  export function renderFlightFact(f: FlightFact): string {
533
+ return renderFlightFactInner(f) + ` <!-- fact:${f.fact_id} -->`
534
+ }
535
+
536
+ function renderFlightFactInner(f: FlightFact): string {
537
+ // 单向生成锚点(D-26):每条事实行内嵌 fact_id,闸侧锚点匹配 = 确定性回溯
538
+ const anchor = ` <!-- fact:${f.fact_id} -->`
505
539
  const route = `${f.route.origin}→${f.route.destination}`
506
540
  const evidence = `[${f.source}@${f.fetched_at} #${f.query_id}]`
507
541
  if (f.bookability === 'unavailable_exact_date') {
@@ -555,3 +589,73 @@ export function renderNightsLine(it: ItineraryFacts): string {
555
589
  const flightLegs = it.od_segments.filter(s => s.mode === 'flight').reduce((acc, s) => acc + s.legs, 0)
556
590
  return `共 ${daysBetween(it.trip_start, it.trip_end) + 1} 天 ${totalNights} 晚 = ${hotelNights} 个酒店夜 + ${it.onboard_nights} 个机上夜;航班按 O&D ${flightOd} 段(共 ${flightLegs} 个 flight legs)`
557
591
  }
592
+
593
+ // ---------------------------------------------------------------------------
594
+ // 酒店事实(D-26,issue #118):exact-date 酒店检索 hit/miss 落账
595
+ // 纪律:摸底(未定档期)不落账(无 exact-date 语义);needs-setup/error 传输失败永不落负事实
596
+ // ---------------------------------------------------------------------------
597
+
598
+ export function factsFromHotel(input: {
599
+ source: string
600
+ destination: string
601
+ checkIn?: string
602
+ checkOut?: string
603
+ verdict: 'hit' | 'miss' | 'needs-setup' | 'error'
604
+ options: number
605
+ evidence: string
606
+ fetchedAt: string
607
+ }): HotelFact[] {
608
+ if (!input.checkIn || !input.checkOut) return []
609
+ if (input.verdict !== 'hit' && input.verdict !== 'miss') return []
610
+ const queryId = `hotel:${input.source}:${input.destination}:${input.checkIn}_${input.checkOut}`
611
+ return [{
612
+ schema: BOOKABLE_FACT_SCHEMA,
613
+ kind: 'hotel',
614
+ fact_id: makeFactId([input.destination, input.checkIn, input.checkOut, queryId]),
615
+ destination: input.destination,
616
+ check_in: input.checkIn,
617
+ check_out: input.checkOut,
618
+ verdict: input.verdict,
619
+ bookability: input.verdict === 'hit' ? 'bookable_exact_date' : 'unavailable_exact_date',
620
+ options_masked: input.options,
621
+ source: input.source,
622
+ query_id: queryId,
623
+ fetched_at: input.fetchedAt,
624
+ as_of: input.fetchedAt.slice(0, 10),
625
+ tier: 'live_inventory',
626
+ }]
627
+ }
628
+
629
+ /** 酒店 claim 回溯:同目的地+同档期的 exact-date 事实裁决(无事实=unverified,fail-closed) */
630
+ export function hotelClaimVerdict(
631
+ facts: BookableFact[],
632
+ claim: { destination: string; check_in?: string; check_out?: string },
633
+ ): { verdict: 'traceable' | 'not_in_source' | 'unverified'; fact?: HotelFact; reason: string } {
634
+ const scoped = facts.filter((f): f is HotelFact =>
635
+ f.kind === 'hotel' && f.destination === claim.destination
636
+ && (!claim.check_in || f.check_in === claim.check_in)
637
+ && (!claim.check_out || f.check_out === claim.check_out))
638
+ if (scoped.length === 0) {
639
+ return {
640
+ verdict: 'unverified',
641
+ reason: `酒店 claim(${claim.destination}${claim.check_in ? ` ${claim.check_in}→${claim.check_out ?? '?'}` : ''})未经 exact-date 源核验——先检索再写结论`,
642
+ }
643
+ }
644
+ const hit = scoped.find(f => f.bookability === 'bookable_exact_date')
645
+ if (hit) return { verdict: 'traceable', fact: hit, reason: 'exact-date 命中' }
646
+ const latest = scoped[scoped.length - 1]!
647
+ return { verdict: 'not_in_source', fact: latest, reason: `该档期 exact-date 源正常返回 0 条(${latest.fetched_at})——不得写有房/可订` }
648
+ }
649
+
650
+ // ---------------------------------------------------------------------------
651
+ // 渲染原语(单向生成,issue #118):事实 → 行,锚点内嵌 fact_id;
652
+ // 产物经原语渲染即自带溯源锚,闸侧锚点匹配 = 确定性回溯,不靠行文猜测
653
+ // ---------------------------------------------------------------------------
654
+
655
+
656
+ export function renderHotelFact(f: HotelFact): string {
657
+ if (f.verdict !== 'hit') {
658
+ return `- ${f.destination} ${f.check_in}→${f.check_out}:exact-date 源 0 条在架(不可订) <!-- fact:${f.fact_id} -->`
659
+ }
660
+ return `- ${f.destination} ${f.check_in}→${f.check_out}:住宿已核验(在架 ${f.options_masked} 家;价格上游打码,真实价以落地页为准) <!-- fact:${f.fact_id} -->`
661
+ }
@@ -1,7 +1,7 @@
1
1
  /**
2
2
  * 预订 saga 状态机(booking_saga_fsm.v1)——issue #17 采纳三点的具名化落点。
3
3
  *
4
- * 立场(docs/booking-saga-fsm.md / ADR-17):不引入编排框架;预订 saga 的状态字母表与边表
4
+ * 立场(docs/design/booking-saga-fsm.md / ADR-17):不引入编排框架;预订 saga 的状态字母表与边表
5
5
  * 就是账本 pending_writes(status CHECK:pending|confirmed|compensated)的显式化。
6
6
  * 本模块是纯函数词汇层,零依赖、零行为、不接线任何 seam——M5 拍板 WriteGate 生产化时
7
7
  * 以此为唯一状态推进词汇(具名化 = 状态与边不再散落在 SQL 字符串里)。