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

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 (327) hide show
  1. package/README.md +102 -194
  2. package/README.zh-CN.md +115 -210
  3. package/bin/gotry-backend.js +53 -0
  4. package/bin/gotry-bootstrap.js +466 -35
  5. package/bin/gotry-inner.js +255 -36
  6. package/bin/gotry-process-liveness.d.ts +27 -0
  7. package/bin/gotry-process-liveness.js +97 -0
  8. package/bin/gotry-runtime-resolution.d.ts +1 -1
  9. package/bin/gotry-runtime-resolution.js +2 -2
  10. package/client/client.js +174 -0
  11. package/cordis.gotry-patch.yml +70 -93
  12. package/dist/capabilities/agent-reach-deep.js +45 -122
  13. package/dist/capabilities/agent-reach.js +63 -148
  14. package/dist/capabilities/anything.js +89 -75
  15. package/dist/capabilities/artifacts.js +123 -104
  16. package/dist/capabilities/channel-health.js +65 -45
  17. package/dist/capabilities/channel-registry.js +108 -87
  18. package/dist/capabilities/doctor.js +357 -235
  19. package/dist/capabilities/effect.js +223 -309
  20. package/dist/capabilities/fact-log.js +28 -14
  21. package/dist/capabilities/flyai.js +245 -156
  22. package/dist/capabilities/ground-transfer.js +509 -0
  23. package/dist/capabilities/hbcli.js +144 -148
  24. package/dist/capabilities/hotelbyte-transaction.js +194 -0
  25. package/dist/capabilities/incident-log.js +131 -61
  26. package/dist/capabilities/model-override.js +30 -10
  27. package/dist/capabilities/opensky.js +54 -43
  28. package/dist/capabilities/resilience.js +50 -32
  29. package/dist/capabilities/session/action-cache.js +50 -44
  30. package/dist/capabilities/session/adapters/ctrip-flight.js +119 -62
  31. package/dist/capabilities/session/adapters/ctrip-hotel.js +125 -110
  32. package/dist/capabilities/session/adapters/dida-portal.js +206 -0
  33. package/dist/capabilities/session/adapters/meituan-local.js +35 -54
  34. package/dist/capabilities/session/adapters/rail-12306.js +169 -112
  35. package/dist/capabilities/session/benchmark.js +85 -149
  36. package/dist/capabilities/session/extension-bridge.js +320 -259
  37. package/dist/capabilities/session/extension-channel.js +57 -76
  38. package/dist/capabilities/session/extension-distribution.js +115 -119
  39. package/dist/capabilities/session/extract.js +31 -21
  40. package/dist/capabilities/session/golden-score.js +55 -33
  41. package/dist/capabilities/session/health-watch.js +146 -154
  42. package/dist/capabilities/session/read-guard.js +55 -47
  43. package/dist/capabilities/session/static-flight-golden.js +49 -36
  44. package/dist/capabilities/session/transport.js +54 -61
  45. package/dist/capabilities/session/wizard.js +63 -93
  46. package/dist/capabilities/session-consent.js +143 -47
  47. package/dist/capabilities/session-login.js +95 -126
  48. package/dist/capabilities/session-search.js +391 -253
  49. package/dist/capabilities/sponsor-plugin.js +341 -0
  50. package/dist/capabilities/visa-policy.js +87 -73
  51. package/dist/capabilities/weather.js +163 -235
  52. package/dist/scripts/action-cache-tests.js +24 -59
  53. package/dist/scripts/agent-planning-turn-deadline-e2e.js +91 -142
  54. package/dist/scripts/agent-planning-turn-deadline-tests.js +80 -176
  55. package/dist/scripts/agent-reach-deep-tests.js +30 -40
  56. package/dist/scripts/agent-reach-tests.js +26 -18
  57. package/dist/scripts/agent-reach-wrapper-tests.js +46 -50
  58. package/dist/scripts/anything-tests.js +58 -62
  59. package/dist/scripts/artifact-client-contract-tests.js +69 -0
  60. package/dist/scripts/artifacts-capability-tests.js +235 -0
  61. package/dist/scripts/async-collect.js +26 -15
  62. package/dist/scripts/benchmark-environment-bridge-e2e.js +287 -684
  63. package/dist/scripts/benchmark-environment-bridge-tests.js +818 -3123
  64. package/dist/scripts/booking-copilot-availability-ledger-binding-tests.js +66 -279
  65. package/dist/scripts/booking-copilot-availability-policy-tests.js +153 -1257
  66. package/dist/scripts/booking-copilot-bin-proof-tests.js +22 -45
  67. package/dist/scripts/booking-copilot-crossrepo-fixture-server.js +64 -76
  68. package/dist/scripts/booking-copilot-dsh-core-proof-tests.js +69 -199
  69. package/dist/scripts/booking-copilot-dsh-planner-proof-tests.js +393 -759
  70. package/dist/scripts/booking-copilot-dsh-plugin-proof-tests.js +33 -75
  71. package/dist/scripts/booking-copilot-event-sequence-concurrency-proof-tests.js +60 -110
  72. package/dist/scripts/booking-copilot-gap-code-contract-proof-tests.js +28 -66
  73. package/dist/scripts/booking-copilot-operation-ledger-concurrency-proof-tests.js +72 -271
  74. package/dist/scripts/booking-copilot-receipt-ledger-concurrency-proof-tests.js +73 -275
  75. package/dist/scripts/booking-copilot-runtime-proof-tests.js +784 -3398
  76. package/dist/scripts/booking-copilot-server-proof-tests.js +63 -168
  77. package/dist/scripts/booking-copilot-startup-proof-tests.js +38 -124
  78. package/dist/scripts/booking-executor-tests.js +80 -0
  79. package/dist/scripts/booking-recovery-chain-tests.js +182 -0
  80. package/dist/scripts/booking-saga-tests.js +61 -123
  81. package/dist/scripts/booking-surface-contract-proof-tests.js +152 -1167
  82. package/dist/scripts/bootstrap-tests.js +1383 -128
  83. package/dist/scripts/build-changelog.js +89 -130
  84. package/dist/scripts/build-metrics-report.js +135 -143
  85. package/dist/scripts/changelog-tests.js +35 -82
  86. package/dist/scripts/channel-probe-tests.js +44 -116
  87. package/dist/scripts/channel-probe.js +84 -167
  88. package/dist/scripts/channel-registry-tests.js +97 -185
  89. package/dist/scripts/companion-tests.js +25 -70
  90. package/dist/scripts/dida-cookie-probe.js +87 -0
  91. package/dist/scripts/dida-sw-probe.js +114 -0
  92. package/dist/scripts/diff-test.js +17 -12
  93. package/dist/scripts/doctor-tests.js +428 -120
  94. package/dist/scripts/dsh-artifact-e2e.js +150 -0
  95. package/dist/scripts/dsh-artifact-web-e2e.js +443 -0
  96. package/dist/scripts/dsh-http-proxy-sse-proof.js +90 -0
  97. package/dist/scripts/dsh-runtime-closure-tests.js +110 -153
  98. package/dist/scripts/dsh-runtime-closure.js +75 -63
  99. package/dist/scripts/dsh-session-v3-migration-proof.js +106 -0
  100. package/dist/scripts/dsh-subprocess-local-proof.js +149 -0
  101. package/dist/scripts/dsh-target-closure-proof.js +178 -0
  102. package/dist/scripts/effect-tests.js +165 -552
  103. package/dist/scripts/engine-run.js +1 -3
  104. package/dist/scripts/engine-tests.js +16 -10
  105. package/dist/scripts/evaluation-cadence-tests.js +69 -242
  106. package/dist/scripts/evaluation-contract-tests.js +193 -499
  107. package/dist/scripts/extension-distribution-cli.js +19 -15
  108. package/dist/scripts/extension-distribution-tests.js +98 -211
  109. package/dist/scripts/extension-tests.js +366 -389
  110. package/dist/scripts/fact-gate-tests.js +725 -263
  111. package/dist/scripts/fixtures/extension-bridge-unref-child.mjs +21 -0
  112. package/dist/scripts/flight-hotel-anchor-363-registered-e2e.js +169 -0
  113. package/dist/scripts/flight-malformed-tests.js +159 -0
  114. package/dist/scripts/flyai-tests.js +325 -68
  115. package/dist/scripts/g5-guard-tests.js +131 -0
  116. package/dist/scripts/g5-guard.js +189 -0
  117. package/dist/scripts/gen-lunar-anchors.js +106 -0
  118. package/dist/scripts/gotry-backend-kernel-tests.js +79 -0
  119. package/dist/scripts/ground-transfer-tests.js +956 -0
  120. package/dist/scripts/hbcli-e2e-tests.js +71 -90
  121. package/dist/scripts/hbcli-live-optin-tests.js +96 -0
  122. package/dist/scripts/hbcli-release-contract-tests.js +122 -0
  123. package/dist/scripts/hbcli-tests.js +41 -115
  124. package/dist/scripts/health-watch-cli.js +42 -28
  125. package/dist/scripts/hotel-date-gate-tests.js +496 -0
  126. package/dist/scripts/hotelbyte-reconcile-tests.js +141 -0
  127. package/dist/scripts/hotelbyte-spawn-e2e-tests.js +373 -0
  128. package/dist/scripts/i18n-tests.js +16 -9
  129. package/dist/scripts/incident-tests.js +281 -87
  130. package/dist/scripts/issue-194-job-id-guard-tests.js +106 -0
  131. package/dist/scripts/issue-233-cancel-refund-commission-tests.js +392 -0
  132. package/dist/scripts/issue-271-liveness-tests.js +547 -0
  133. package/dist/scripts/issue-289-model-retry-real-tests.js +296 -0
  134. package/dist/scripts/issue-289-model-retry-tests.js +441 -0
  135. package/dist/scripts/issue-289-web-retry-e2e.js +630 -0
  136. package/dist/scripts/issue-338-home-city-e2e.js +577 -0
  137. package/dist/scripts/issue343-real-entry-e2e.js +160 -0
  138. package/dist/scripts/journey-tests.js +22 -27
  139. package/dist/scripts/kernel-manifest-evidence.js +109 -0
  140. package/dist/scripts/kernel-manifest-gate.js +126 -0
  141. package/dist/scripts/kernel-manifest-tests.js +212 -0
  142. package/dist/scripts/kernel-manifest-trace-entry.js +7 -0
  143. package/dist/scripts/kernel-manifest-trace-hook.mjs +28 -0
  144. package/dist/scripts/kernel-manifest.js +369 -0
  145. package/dist/scripts/ledger-repair-apply-tests.js +316 -0
  146. package/dist/scripts/ledger-repair-plan-tests.js +375 -0
  147. package/dist/scripts/ledger-tests.js +325 -340
  148. package/dist/scripts/ledger-workflow-crash.js +10 -20
  149. package/dist/scripts/map-tools-vendor-package-proof.js +206 -134
  150. package/dist/scripts/memory-capture-tests.js +242 -61
  151. package/dist/scripts/memory-decay-tests.js +18 -52
  152. package/dist/scripts/memory-lifecycle-tests.js +657 -0
  153. package/dist/scripts/memory-lifecycle.js +196 -0
  154. package/dist/scripts/memory-metrics.js +24 -12
  155. package/dist/scripts/memory-value-report.js +474 -222
  156. package/dist/scripts/memory-value-tests.js +328 -0
  157. package/dist/scripts/metrics-report-tests.js +62 -199
  158. package/dist/scripts/model-override-e2e.js +66 -105
  159. package/dist/scripts/nightly-evidence-tests.js +64 -89
  160. package/dist/scripts/nightly-evidence.js +108 -111
  161. package/dist/scripts/nudge-digest.js +39 -28
  162. package/dist/scripts/onboarding-tests.js +63 -88
  163. package/dist/scripts/opensky-check.js +18 -13
  164. package/dist/scripts/opensky-tests.js +28 -28
  165. package/dist/scripts/persona-surface-guard-tests.js +37 -12
  166. package/dist/scripts/pnpm-dsh-closure-proof.js +2 -5
  167. package/dist/scripts/policy-anchor-359-registered-e2e.js +214 -0
  168. package/dist/scripts/price-drift-tests.js +126 -227
  169. package/dist/scripts/price-drift-watch.js +209 -253
  170. package/dist/scripts/probe-poi-tests.js +52 -45
  171. package/dist/scripts/product-metrics.js +187 -273
  172. package/dist/scripts/publish-preverify.js +47 -41
  173. package/dist/scripts/realtime-pricing-tests.js +37 -76
  174. package/dist/scripts/replay-async.js +20 -18
  175. package/dist/scripts/replay-real.js +16 -18
  176. package/dist/scripts/replay.js +52 -82
  177. package/dist/scripts/session-attach-diagnose.js +16 -12
  178. package/dist/scripts/session-attach-poc.js +34 -37
  179. package/dist/scripts/session-benchmark.js +66 -147
  180. package/dist/scripts/session-dida-live-e2e.js +151 -0
  181. package/dist/scripts/session-extract-tests.js +14 -35
  182. package/dist/scripts/session-login.js +14 -13
  183. package/dist/scripts/session-tests.js +545 -583
  184. package/dist/scripts/sf-live-benchmark.js +92 -110
  185. package/dist/scripts/sf-live-cli-tests.js +2 -9
  186. package/dist/scripts/sf-soft-score-tests.js +19 -60
  187. package/dist/scripts/sf-summary-tests.js +149 -0
  188. package/dist/scripts/sf-summary.js +466 -84
  189. package/dist/scripts/skeleton-check.js +16 -14
  190. package/dist/scripts/skeleton-integration-test.js +11 -10
  191. package/dist/scripts/skills-contract-tests.js +51 -46
  192. package/dist/scripts/smoke-session-gate-tests.js +12 -20
  193. package/dist/scripts/smoke.js +541 -422
  194. package/dist/scripts/sponsor-reuse-tests.js +262 -0
  195. package/dist/scripts/state-cli-tests.js +226 -101
  196. package/dist/scripts/state-cli.js +549 -212
  197. package/dist/scripts/static-golden-tests.js +46 -180
  198. package/dist/scripts/time-eval-tests.js +210 -192
  199. package/dist/scripts/travel-timeline-tests.js +29 -93
  200. package/dist/scripts/turn-handoff-collect-tests.js +46 -93
  201. package/dist/scripts/turn-handoff-collect.js +71 -99
  202. package/dist/scripts/turn-policy-tests.js +28 -29
  203. package/dist/scripts/typed-contract-canary.js +77 -162
  204. package/dist/scripts/tz-resolver-sanity.js +50 -0
  205. package/dist/scripts/tz-resolver-tests.js +584 -0
  206. package/dist/scripts/unified-tests.js +23 -19
  207. package/dist/scripts/visa-policy-tests.js +31 -44
  208. package/dist/scripts/weather-tests.js +121 -543
  209. package/dist/scripts/wish-channel-gate-tests.js +29 -86
  210. package/dist/scripts/write-gate-tests.js +522 -0
  211. package/dist/scripts/z3-lifecycle-fault-tests.js +188 -0
  212. package/dist/scripts/z3-lifecycle-tests.js +170 -0
  213. package/dist/scripts/z3-race-repeat-tests.js +39 -0
  214. package/dist/scripts/z3-race-tests.js +18 -12
  215. package/dist/src/artifact-gate.js +652 -221
  216. package/dist/src/backend/kernel.js +91 -0
  217. package/dist/src/backend/modules/booking-copilot.js +55 -0
  218. package/dist/src/backend/modules/booking-executor.js +124 -0
  219. package/dist/src/backend/modules/session-search.js +194 -0
  220. package/dist/src/benchmark-agent-conformance.js +296 -262
  221. package/dist/src/benchmark-environment-bridge.js +298 -413
  222. package/dist/src/benchmark-headless-child-diagnostics.js +93 -100
  223. package/dist/src/benchmark-tool-isolation.js +94 -67
  224. package/dist/src/bookable-facts.js +299 -174
  225. package/dist/src/booking-saga.js +64 -120
  226. package/dist/src/booking-surface/availability-policy.js +443 -576
  227. package/dist/src/booking-surface/cancel-refund-commission.js +379 -0
  228. package/dist/src/booking-surface/contracts.js +24 -80
  229. package/dist/src/booking-surface/dsh-planner.js +510 -281
  230. package/dist/src/booking-surface/dsh-plugin.js +17 -1
  231. package/dist/src/booking-surface/error-codes.js +82 -10
  232. package/dist/src/booking-surface/index.js +0 -3
  233. package/dist/src/booking-surface/profile.js +10 -54
  234. package/dist/src/booking-surface/recovery-chain.js +367 -0
  235. package/dist/src/booking-surface/runtime.js +765 -891
  236. package/dist/src/booking-surface/server.js +200 -251
  237. package/dist/src/booking-surface/startup.js +58 -54
  238. package/dist/src/booking-surface/validation.js +150 -218
  239. package/dist/src/bridge.js +20 -13
  240. package/dist/src/companions.js +48 -74
  241. package/dist/src/contracts.js +21 -43
  242. package/dist/src/dsh-llm.js +82 -120
  243. package/dist/src/engine.js +253 -239
  244. package/dist/src/evaluation-cadence.js +72 -209
  245. package/dist/src/evaluation-contracts.js +220 -843
  246. package/dist/src/flight-pack-adapter.js +54 -0
  247. package/dist/src/flight-pack-contract.js +2 -0
  248. package/dist/src/gotry-backend.js +70 -0
  249. package/dist/src/hotel-date-gate.js +118 -0
  250. package/dist/src/i18n.js +18 -7
  251. package/dist/src/index.js +1425 -1629
  252. package/dist/src/journey.js +132 -81
  253. package/dist/src/ledger-repair-apply.js +538 -0
  254. package/dist/src/ledger-repair-plan.js +463 -0
  255. package/dist/src/loop.js +257 -166
  256. package/dist/src/memory-capture.js +180 -32
  257. package/dist/src/memory-decay.js +30 -14
  258. package/dist/src/memory-lifecycle.js +1310 -0
  259. package/dist/src/memory-utility.js +27 -40
  260. package/dist/src/mock-llm.js +36 -61
  261. package/dist/src/model.js +81 -36
  262. package/dist/src/realtime-pricing.js +63 -72
  263. package/dist/src/slot-spec.js +84 -106
  264. package/dist/src/state-ledger.js +310 -471
  265. package/dist/src/subagent-job-id-guard.js +58 -0
  266. package/dist/src/time-anchor.js +231 -33
  267. package/dist/src/tool-packet.js +20 -8
  268. package/dist/src/travel-slots.js +58 -35
  269. package/dist/src/travel-timeline.js +44 -63
  270. package/dist/src/turn-deadline.js +121 -127
  271. package/dist/src/turn-policy.js +61 -89
  272. package/dist/src/tz-resolver.js +143 -0
  273. package/dist/src/unified.js +504 -381
  274. package/dist/src/wish-pool.js +39 -14
  275. package/dist/src/write-gate.js +495 -0
  276. package/dist/src/z3-shared.js +273 -7
  277. package/extension/README.md +4 -3
  278. package/extension/background.js +125 -14
  279. package/extension/content-bridge.js +23 -1
  280. package/extension/content-main.js +15 -4
  281. package/extension/manifest.json +16 -8
  282. package/package.json +251 -220
  283. package/ts/capabilities/agent-reach.ts +4 -4
  284. package/ts/capabilities/anything.ts +1 -1
  285. package/ts/capabilities/artifacts.ts +46 -9
  286. package/ts/capabilities/flyai.ts +156 -37
  287. package/ts/capabilities/hbcli.ts +1 -1
  288. package/ts/capabilities/incident-log.ts +88 -59
  289. package/ts/capabilities/session/adapters/ctrip-flight.ts +99 -36
  290. package/ts/capabilities/session/adapters/dida-portal.ts +250 -0
  291. package/ts/capabilities/session/adapters/rail-12306.ts +123 -24
  292. package/ts/capabilities/session/extension-bridge.ts +226 -101
  293. package/ts/capabilities/session/extension-channel.ts +22 -9
  294. package/ts/capabilities/session/wizard.ts +2 -2
  295. package/ts/capabilities/session-consent.ts +98 -14
  296. package/ts/capabilities/session-login.ts +1 -0
  297. package/ts/capabilities/session-search.ts +315 -31
  298. package/ts/dsh-runtime/vendor/README.md +8 -4
  299. package/ts/dsh-runtime/vendor/dsh-map-tools/README.en.md +7 -1
  300. package/ts/dsh-runtime/vendor/dsh-map-tools/README.md +5 -3
  301. package/ts/dsh-runtime/vendor/dsh-map-tools/lib/settings-ns.js +1 -1
  302. package/ts/dsh-runtime/vendor/dsh-map-tools/lib/types/settings-ns.d.ts +1 -1
  303. package/ts/dsh-runtime/vendor/dsh-map-tools/package.json +6 -6
  304. package/ts/package.json +236 -16
  305. package/ts/scripts/map-tools-vendor-package-proof.ts +152 -31
  306. package/ts/scripts/state-cli.ts +418 -51
  307. package/ts/src/artifact-gate.ts +561 -15
  308. package/ts/src/benchmark-environment-bridge.ts +37 -20
  309. package/ts/src/bookable-facts.ts +183 -7
  310. package/ts/src/booking-surface/cancel-refund-commission.ts +488 -0
  311. package/ts/src/booking-surface/dsh-planner.ts +240 -32
  312. package/ts/src/booking-surface/dsh-plugin.js +17 -1
  313. package/ts/src/booking-surface/error-codes.ts +75 -0
  314. package/ts/src/booking-surface/recovery-chain.ts +409 -0
  315. package/ts/src/booking-surface/server.ts +84 -51
  316. package/ts/src/booking-surface/validation.ts +2 -0
  317. package/ts/src/contracts.ts +12 -2
  318. package/ts/src/dsh-llm.ts +4 -8
  319. package/ts/src/index.ts +609 -90
  320. package/ts/src/loop.ts +116 -7
  321. package/ts/src/memory-capture.ts +227 -3
  322. package/ts/src/mock-llm.ts +4 -8
  323. package/ts/src/model.ts +88 -2
  324. package/ts/src/state-ledger.ts +47 -29
  325. package/ts/src/subagent-job-id-guard.ts +72 -0
  326. package/ts/src/time-anchor.ts +217 -13
  327. package/ts/src/unified.ts +316 -54
@@ -1,32 +1,29 @@
1
+ /**
2
+ * Embedded Booking planner adapter proof.
3
+ *
4
+ * The fake below stops at the DeepSeek Harness SDK event boundary: planner
5
+ * decisions come from typed dsh tool/call events, or from assistant
6
+ * finalResponse text only after it passes the same authority path
7
+ * (validation, allowedActions, contextRef, runtime-owned revision). A
8
+ * separate core proof boots the real dsh SDK runtime.
9
+ */
1
10
  import assert from 'node:assert/strict';
2
11
  import { createHash } from 'node:crypto';
3
- import { DSH_EMBEDDED_BOOKING_TOOL_NAMES, buildDshPlannerEnvironment, createDshEmbeddedBookingPlanner } from '../src/booking-surface/dsh-planner.js';
4
- const availability = {
5
- initialized: true,
6
- recoveryStarted: false,
7
- availabilityPhase: 'need_offers',
8
- activeHotelOrdinal: 0,
9
- hotelRefs: [],
10
- hotels: {},
11
- attempts: [],
12
- queryReservations: []
13
- };
12
+ import { DSH_EMBEDDED_BOOKING_TOOL_NAMES, buildDshEmbeddedBookingPatch, buildDshPlannerEnvironment, createDshEmbeddedBookingPlanner, formatUtcOffsetLabel, } from '../src/booking-surface/dsh-planner.js';
13
+ const availability = { initialized: true, recoveryStarted: false, availabilityPhase: 'need_offers', activeHotelOrdinal: 0, hotelRefs: [], hotels: {}, attempts: [], queryReservations: [] };
14
14
  const task = {
15
15
  schemaVersion: 'booking.surface',
16
16
  taskId: 'task-dsh-1',
17
17
  contextRef: 'ctx-dsh-1',
18
18
  surface: 'tenant',
19
19
  revision: 0,
20
- allowedActions: [
21
- 'search.patch',
22
- 'search.run'
23
- ],
20
+ allowedActions: ['search.patch', 'search.run'],
24
21
  userTurnCount: 1,
25
22
  lastTurnId: 'dsh-turn-1',
26
23
  operationCount: 0,
27
24
  phase: 'planning',
28
25
  lastSequence: 0,
29
- availability
26
+ availability,
30
27
  };
31
28
  const workspace = {
32
29
  schemaVersion: 'booking.surface',
@@ -36,19 +33,11 @@ const workspace = {
36
33
  locale: 'zh-CN',
37
34
  currency: 'AED',
38
35
  searchDraft: {},
39
- results: {
40
- status: 'idle'
41
- },
36
+ results: { status: 'idle' },
42
37
  visibleHotels: [],
43
38
  loadedOffers: [],
44
39
  shortlistedOfferRefs: [],
45
- capabilities: {
46
- surface: 'tenant',
47
- allowedActions: [
48
- 'search.patch',
49
- 'search.run'
50
- ]
51
- }
40
+ capabilities: { surface: 'tenant', allowedActions: ['search.patch', 'search.run'] },
52
41
  };
53
42
  const searchRun = {
54
43
  schemaVersion: 'booking.surface',
@@ -58,90 +47,111 @@ const searchRun = {
58
47
  expectedRevision: 0,
59
48
  reason: 'Run the authoritative workspace search.',
60
49
  factRefs: [],
61
- input: {}
50
+ input: {},
62
51
  };
63
- const hotelSelect = {
64
- ...searchRun,
65
- kind: 'hotel.select',
66
- actionId: 'action-dsh-select-1',
67
- reason: 'Select the requested hotel.',
68
- input: {
69
- hotelRef: 'hotel-1'
52
+ const hotelSelect = { ...searchRun, kind: 'hotel.select', actionId: 'action-dsh-select-1', reason: 'Select the requested hotel.', input: { hotelRef: 'hotel-1' } };
53
+ function toolArgumentsPort(argumentsText) {
54
+ return {
55
+ async run() {
56
+ return {
57
+ finalResponse: '',
58
+ events: [{
59
+ type: 'tool/call',
60
+ data: { name: 'booking_search_hotels', arguments: argumentsText },
61
+ }],
62
+ };
63
+ },
64
+ async close() { },
65
+ };
66
+ }
67
+ async function runToolArgumentsCase(argumentsText, turnId) {
68
+ const planner = await createDshEmbeddedBookingPlanner({ runPort: toolArgumentsPort(argumentsText) });
69
+ try {
70
+ return await planner.plannerFactory(task).next({
71
+ task,
72
+ turn: {
73
+ schemaVersion: 'booking.surface',
74
+ kind: 'user.turn',
75
+ taskId: task.taskId,
76
+ turnId,
77
+ workspace,
78
+ request: { text: 'Find hotels' },
79
+ },
80
+ });
70
81
  }
71
- };
82
+ finally {
83
+ await planner.close();
84
+ }
85
+ }
86
+ const repeatedToolArguments = JSON.stringify({
87
+ decision: {
88
+ kind: 'operation',
89
+ action: {
90
+ ...searchRun,
91
+ reason: 'Preserve braces } {, escaped quotes " and a backslash \\ inside a JSON string.',
92
+ },
93
+ },
94
+ });
95
+ const repeatedDecision = await runToolArgumentsCase(`${repeatedToolArguments}${repeatedToolArguments}`, 'dsh-duplicate-identical');
96
+ assert.equal(repeatedDecision[0]?.kind, 'operation', 'identical complete JSON object repetitions are accepted through runPort -> tool/call');
97
+ assert.equal(repeatedDecision[0]?.kind === 'operation' ? repeatedDecision[0].action.reason : undefined, 'Preserve braces } {, escaped quotes " and a backslash \\ inside a JSON string.', 'balanced recovery respects braces and escapes inside JSON strings');
98
+ const spacedRepeatedDecision = await runToolArgumentsCase(`${repeatedToolArguments}\n \t ${repeatedToolArguments}`, 'dsh-duplicate-whitespace');
99
+ assert.equal(spacedRepeatedDecision[0]?.kind, 'operation', 'identical repetitions separated by whitespace are accepted');
100
+ await assert.rejects(runToolArgumentsCase(`${repeatedToolArguments}${JSON.stringify({
101
+ decision: {
102
+ kind: 'operation',
103
+ action: { ...searchRun, actionId: 'action-dsh-conflict' },
104
+ },
105
+ })}`, 'dsh-duplicate-conflict'), /planner_invalid_tool_arguments/, 'structurally different complete objects are rejected instead of selecting the first action');
106
+ await assert.rejects(runToolArgumentsCase(`${repeatedToolArguments}${repeatedToolArguments.slice(0, -1)}`, 'dsh-duplicate-truncated'), /planner_invalid_tool_arguments/, 'a truncated later object is rejected');
107
+ await assert.rejects(runToolArgumentsCase(`model preface ${repeatedToolArguments}${repeatedToolArguments}`, 'dsh-duplicate-prefix'), /planner_invalid_tool_arguments/, 'arbitrary prefix is rejected');
108
+ await assert.rejects(runToolArgumentsCase(`${repeatedToolArguments} trailing garbage`, 'dsh-duplicate-tail'), /planner_invalid_tool_arguments/, 'garbage suffix is rejected');
109
+ await assert.rejects(runToolArgumentsCase('{"decision":{"kind":"operation","action":{"reason":"raw\nline"}}}', 'dsh-single-invalid-control'), /planner_invalid_tool_arguments/, 'a single invalid object is not repaired by escaping raw control characters');
110
+ await assert.rejects(runToolArgumentsCase('[] []', 'dsh-non-object-sequence'), /planner_invalid_tool_arguments/, 'a non-object JSON sequence is rejected');
111
+ const profilePatch = buildDshEmbeddedBookingPatch('/tmp/gotry-booking-dsh-plugin.js');
72
112
  const prompts = [];
73
113
  const sessionIds = [];
114
+ const frozenNow = new Date(2026, 8, 9, 10, 30, 0, 0);
74
115
  let runIndex = 0;
75
116
  const runPort = {
76
- async run (prompt, options) {
117
+ async run(prompt, options) {
77
118
  prompts.push(prompt);
78
119
  sessionIds.push(options.sessionId);
79
120
  runIndex += 1;
80
121
  if (runIndex === 1) {
81
122
  return {
82
123
  finalResponse: '{"kind":"book","input":{}}',
83
- events: [
84
- {
124
+ events: [{
85
125
  type: 'tool/call',
86
126
  data: {
87
127
  name: 'booking_search_hotels',
88
- arguments: JSON.stringify({
89
- decision: {
90
- kind: 'operation',
91
- action: searchRun
92
- }
93
- })
94
- }
95
- }
96
- ]
128
+ arguments: JSON.stringify({ decision: { kind: 'operation', action: searchRun } }),
129
+ },
130
+ }],
97
131
  };
98
132
  }
99
133
  if (runIndex === 3) {
100
- return {
101
- finalResponse: '',
102
- events: [
103
- {
104
- type: 'tool/call',
105
- data: {
106
- name: 'booking_refine_results',
107
- arguments: JSON.stringify({
108
- decision: {
109
- kind: 'operation',
110
- action: hotelSelect
111
- }
112
- })
113
- }
114
- }
115
- ]
116
- };
134
+ return { finalResponse: '', events: [{ type: 'tool/call', data: { name: 'booking_refine_results', arguments: JSON.stringify({ decision: { kind: 'operation', action: hotelSelect } }) } }] };
117
135
  }
118
136
  return {
119
137
  finalResponse: '{"kind":"search.run","input":{}}',
120
- events: [
121
- {
138
+ events: [{
122
139
  type: 'tool/call',
123
140
  data: {
124
141
  name: 'booking_search_hotels',
125
142
  arguments: JSON.stringify({
126
143
  decision: {
127
144
  kind: 'terminal',
128
- terminal: {
129
- status: 'completed',
130
- summary: 'Stopped at search results.',
131
- factRefs: []
132
- }
133
- }
134
- })
135
- }
136
- }
137
- ]
145
+ terminal: { status: 'completed', summary: 'Stopped at search results.', factRefs: [] },
146
+ },
147
+ }),
148
+ },
149
+ }],
138
150
  };
139
151
  },
140
- async close () {}
152
+ async close() { },
141
153
  };
142
- const adapter = await createDshEmbeddedBookingPlanner({
143
- runPort
144
- });
154
+ const adapter = await createDshEmbeddedBookingPlanner({ runPort, now: frozenNow });
145
155
  const session = adapter.plannerFactory(task);
146
156
  const first = await session.next({
147
157
  task,
@@ -152,24 +162,36 @@ const first = await session.next({
152
162
  turnId: 'dsh-turn-1',
153
163
  workspace: {
154
164
  ...workspace,
155
- capabilities: {
156
- ...workspace.capabilities,
157
- allowedActions: [
158
- ...workspace.capabilities.allowedActions
159
- ]
160
- }
165
+ capabilities: { ...workspace.capabilities, allowedActions: [...workspace.capabilities.allowedActions] },
161
166
  },
162
- request: {
163
- text: '执行当前搜索'
164
- }
165
- }
167
+ request: { text: '执行当前搜索' },
168
+ },
166
169
  });
167
- assert.deepEqual(first, [
168
- {
169
- kind: 'operation',
170
- action: searchRun
171
- }
172
- ], 'typed dsh tool call becomes one operation');
170
+ assert.deepEqual(first, [{ kind: 'operation', action: searchRun }], 'typed dsh tool call becomes one operation');
171
+ assert.match(prompts[0], /Time anchor: today is 2026-09-09 \(周三, UTC[+-]\d{2}:\d{2}\)/, 'planner prompt uses the injected host-local date anchor');
172
+ assert.match(prompts[0], /process host-local anchor used only for relative-date parsing/, 'planner prompt identifies host-local time as a parsing anchor');
173
+ assert.match(prompts[0], /do not treat it as the traveler\/user timezone/, 'planner prompt does not masquerade host time as user timezone');
174
+ for (const field of ['destination', 'hotel', 'stay', 'occupancy', 'budget', 'starRating', 'guestRating', 'facilities']) {
175
+ assert.match(prompts[0], new RegExp(`\\b${field}\\b`), `planner prompt names SearchCriteriaPatch field ${field}`);
176
+ }
177
+ assert.ok(!/under criteria/i.test(prompts[0]), 'planner prompt does not revive the stale under-criteria routing wording');
178
+ assert.ok(!profilePatch.includes('2026-09-10') && !profilePatch.includes('2026-09-13'), 'shape example no longer carries stale concrete 2026 dates');
179
+ assert.ok(!/under criteria/i.test(profilePatch), 'planner persona does not revive the stale under-criteria routing wording');
180
+ assert.match(profilePatch, /Shape-only example/i, 'planner persona marks the example as shape-only');
181
+ assert.match(profilePatch, /do not copy literal/i, 'planner persona tells the model not to copy placeholder sample values');
182
+ assert.match(profilePatch, /"stay":\{"checkIn":"<computed YYYY-MM-DD from the host-local time anchor>","checkOut":"<computed YYYY-MM-DD from nights\/check-in>"\}/, 'shape example keeps the stay object shape');
183
+ assert.match(profilePatch, /"starRating":\{"strength":"must","value":\{"min":3,"max":3\}\}/, 'shape example keeps the starRating criterion shape');
184
+ assert.match(profilePatch, /"occupancy":\{"rooms":\[\{"adults":2,"childAges":\[\]\}\]\}/, 'shape example includes a well-formed occupancy block');
185
+ assert.ok(!/Bali/.test(profilePatch), 'shape example carries no literal destination');
186
+ assert.ok(!/\b20\d{2}-\d{2}-\d{2}\b/.test(profilePatch), 'shape example carries no concrete YYYY-MM-DD dates');
187
+ const personaPrefixKeyMatches = profilePatch.match(/^ {4}personaPrefix:\s*>-/gm) ?? [];
188
+ assert.equal(personaPrefixKeyMatches.length, 1, `planner patch must carry exactly one personaPrefix: >- key (observed=${personaPrefixKeyMatches.length})`);
189
+ assert.ok(!/(^|\n) {4}persona:\s*>-/.test(profilePatch), 'planner patch must not carry the legacy persona: >- key');
190
+ assert.match(profilePatch, /You are GoTry's embedded booking planner/, 'planner patch retains the intended embedded booking persona text');
191
+ assert.match(profilePatch, /Select exactly one of the six booking/, 'planner patch retains the six-tool discipline');
192
+ assert.equal(formatUtcOffsetLabel(345), 'UTC+05:45', 'timezone formatter preserves positive minute offsets');
193
+ assert.equal(formatUtcOffsetLabel(-210), 'UTC-03:30', 'timezone formatter preserves negative minute offsets');
194
+ assert.equal(formatUtcOffsetLabel(0), 'UTC+00:00', 'timezone formatter zero-pads whole-hour offsets');
173
195
  const receipt = {
174
196
  schemaVersion: 'booking.surface',
175
197
  kind: 'action.receipt',
@@ -177,25 +199,10 @@ const receipt = {
177
199
  contextRef: task.contextRef,
178
200
  status: 'applied',
179
201
  revision: 1,
180
- observation: {
181
- kind: 'search.state',
182
- searchSessionRef: 'search-dsh-1',
183
- resultCount: 4
184
- },
185
- resultContract: {
186
- outcome: 'complete',
187
- hardCriteriaMet: true,
188
- factRefs: [],
189
- gapCodes: [],
190
- blockers: [],
191
- relaxationsApplied: []
192
- }
193
- };
194
- const continuedTask = {
195
- ...task,
196
- revision: 1,
197
- lastReceipt: receipt
202
+ observation: { kind: 'search.state', searchSessionRef: 'search-dsh-1', resultCount: 4 },
203
+ resultContract: { outcome: 'complete', hardCriteriaMet: true, factRefs: [], gapCodes: [], blockers: [], relaxationsApplied: [] },
198
204
  };
205
+ const continuedTask = { ...task, revision: 1, lastReceipt: receipt };
199
206
  const second = await session.next({
200
207
  task: continuedTask,
201
208
  turn: {
@@ -205,75 +212,30 @@ const second = await session.next({
205
212
  workspace: {
206
213
  ...workspace,
207
214
  revision: 1,
208
- capabilities: {
209
- ...workspace.capabilities,
210
- allowedActions: [
211
- ...workspace.capabilities.allowedActions
212
- ]
213
- }
215
+ capabilities: { ...workspace.capabilities, allowedActions: [...workspace.capabilities.allowedActions] },
214
216
  },
215
- receipt
216
- }
217
+ receipt,
218
+ },
217
219
  });
218
- assert.deepEqual(second, [
219
- {
220
+ assert.deepEqual(second, [{
220
221
  kind: 'terminal',
221
- terminal: {
222
- status: 'completed',
223
- summary: 'Stopped at search results.',
224
- factRefs: []
225
- }
226
- }
227
- ]);
222
+ terminal: { status: 'completed', summary: 'Stopped at search results.', factRefs: [] },
223
+ }]);
228
224
  assert.equal(sessionIds[0], sessionIds[1], 'one task keeps one dsh session across receipt continuation');
229
225
  assert.match(prompts[1], /action-dsh-1/, 'receipt continuation reaches the same task-scoped planner session');
230
- const paymentTask = {
231
- ...task,
232
- taskId: 'task-payment-1',
233
- surface: 'payment_link',
234
- allowedActions: [
235
- 'hotel.select'
236
- ],
237
- revision: 0
238
- };
226
+ const paymentTask = { ...task, taskId: 'task-payment-1', surface: 'payment_link', allowedActions: ['hotel.select'], revision: 0 };
239
227
  const paymentSession = adapter.plannerFactory(paymentTask);
240
- const selected = await paymentSession.next({
241
- task: paymentTask,
242
- turn: {
243
- schemaVersion: 'booking.surface',
244
- kind: 'user.turn',
245
- taskId: paymentTask.taskId,
246
- turnId: 'payment-turn-1',
247
- workspace: {
248
- ...workspace,
249
- surface: 'payment_link',
250
- capabilities: {
251
- surface: 'payment_link',
252
- allowedActions: [
253
- 'hotel.select'
254
- ]
255
- }
256
- },
257
- request: {
258
- text: '选择酒店'
259
- }
260
- }
261
- });
262
- assert.deepEqual(selected, [
263
- {
264
- kind: 'operation',
265
- action: hotelSelect
266
- }
267
- ], 'refine-results can emit typed hotel.select on payment_link');
228
+ const selected = await paymentSession.next({ task: paymentTask, turn: { schemaVersion: 'booking.surface', kind: 'user.turn', taskId: paymentTask.taskId, turnId: 'payment-turn-1', workspace: { ...workspace, surface: 'payment_link', capabilities: { surface: 'payment_link', allowedActions: ['hotel.select'] } }, request: { text: '选择酒店' } } });
229
+ assert.deepEqual(selected, [{ kind: 'operation', action: hotelSelect }], 'refine-results can emit typed hotel.select on payment_link');
268
230
  assert.deepEqual(DSH_EMBEDDED_BOOKING_TOOL_NAMES, [
269
231
  'booking_search_hotels',
270
232
  'booking_refine_results',
271
233
  'booking_find_room_offers',
272
234
  'booking_compare_offers',
273
235
  'booking_prepare_booking',
274
- 'booking_observe_booking'
236
+ 'booking_observe_booking',
275
237
  ]);
276
- assert.ok(!DSH_EMBEDDED_BOOKING_TOOL_NAMES.some((name)=>/book$|trade|payment/i.test(name)), 'profile has no Book/payment tool');
238
+ assert.ok(!DSH_EMBEDDED_BOOKING_TOOL_NAMES.some((name) => /book$|trade|payment/i.test(name)), 'profile has no Book/payment tool');
277
239
  const childEnv = buildDshPlannerEnvironment({
278
240
  PATH: '/usr/bin',
279
241
  LANG: 'en_US.UTF-8',
@@ -281,7 +243,7 @@ const childEnv = buildDshPlannerEnvironment({
281
243
  LLM_BASE_URL: 'http://model.invalid/v1',
282
244
  PORTAL_TOKEN: 'forbidden',
283
245
  HOTELBYTE_TOKEN: 'forbidden',
284
- GOTRY_BOOKING_COPILOT_API_KEY: 'bff-only'
246
+ GOTRY_BOOKING_COPILOT_API_KEY: 'bff-only',
285
247
  });
286
248
  assert.equal(childEnv.DEEPSEEK_API_KEY, 'model-key');
287
249
  assert.equal(childEnv.DEEPSEEK_BASE_URL, 'http://model.invalid/v1');
@@ -289,20 +251,12 @@ assert.equal(childEnv.PORTAL_TOKEN, undefined);
289
251
  assert.equal(childEnv.HOTELBYTE_TOKEN, undefined);
290
252
  assert.equal(childEnv.GOTRY_BOOKING_COPILOT_API_KEY, undefined);
291
253
  const textChannelPort = {
292
- async run () {
293
- return {
294
- finalResponse: JSON.stringify({
295
- kind: 'operation',
296
- action: searchRun
297
- }),
298
- events: []
299
- };
254
+ async run() {
255
+ return { finalResponse: JSON.stringify({ kind: 'operation', action: searchRun }), events: [] };
300
256
  },
301
- async close () {}
257
+ async close() { },
302
258
  };
303
- const textChannel = await createDshEmbeddedBookingPlanner({
304
- runPort: textChannelPort
305
- });
259
+ const textChannel = await createDshEmbeddedBookingPlanner({ runPort: textChannelPort });
306
260
  const textDecisions = await textChannel.plannerFactory(task).next({
307
261
  task,
308
262
  turn: {
@@ -312,54 +266,26 @@ const textDecisions = await textChannel.plannerFactory(task).next({
312
266
  turnId: 'dsh-turn-3',
313
267
  workspace: {
314
268
  ...workspace,
315
- capabilities: {
316
- ...workspace.capabilities,
317
- allowedActions: [
318
- ...workspace.capabilities.allowedActions
319
- ]
320
- }
269
+ capabilities: { ...workspace.capabilities, allowedActions: [...workspace.capabilities.allowedActions] },
321
270
  },
322
- request: {
323
- text: 'JSON prose must stay prose'
324
- }
325
- }
271
+ request: { text: 'JSON prose must stay prose' },
272
+ },
326
273
  });
327
274
  assert.equal(textDecisions[0]?.kind, 'operation', 'text-channel typed decisions execute through the same authority path');
328
- assert.deepEqual(textDecisions[0].action, {
329
- ...searchRun,
330
- expectedRevision: 0
331
- });
275
+ assert.deepEqual(textDecisions[0].action, { ...searchRun, expectedRevision: 0 });
332
276
  const sanitizedRefPort = {
333
- async run () {
277
+ async run() {
334
278
  return {
335
279
  finalResponse: '',
336
- events: [
337
- {
280
+ events: [{
338
281
  type: 'tool/call',
339
- data: {
340
- name: 'booking_search_hotels',
341
- arguments: JSON.stringify({
342
- decision: {
343
- kind: 'operation',
344
- action: {
345
- ...searchRun,
346
- factRefs: [
347
- 'draft:destination=Dubai',
348
- 'draft:destination.Dubai'
349
- ]
350
- }
351
- }
352
- })
353
- }
354
- }
355
- ]
282
+ data: { name: 'booking_search_hotels', arguments: JSON.stringify({ decision: { kind: 'operation', action: { ...searchRun, factRefs: ['draft:destination=Dubai', 'draft:destination.Dubai'] } } }) },
283
+ }],
356
284
  };
357
285
  },
358
- async close () {}
286
+ async close() { },
359
287
  };
360
- const sanitizedRef = await createDshEmbeddedBookingPlanner({
361
- runPort: sanitizedRefPort
362
- });
288
+ const sanitizedRef = await createDshEmbeddedBookingPlanner({ runPort: sanitizedRefPort });
363
289
  const sanitizedDecisions = await sanitizedRef.plannerFactory(task).next({
364
290
  task,
365
291
  turn: {
@@ -368,66 +294,24 @@ const sanitizedDecisions = await sanitizedRef.plannerFactory(task).next({
368
294
  taskId: task.taskId,
369
295
  turnId: 'dsh-turn-6b',
370
296
  workspace,
371
- request: {
372
- text: 'Find hotels'
373
- }
374
- }
297
+ request: { text: 'Find hotels' },
298
+ },
375
299
  });
376
300
  assert.equal(sanitizedDecisions[0]?.kind, 'operation', 'sanitizer maps off-charset characters deterministically');
377
- assert.deepEqual(sanitizedDecisions[0].action?.factRefs, [
378
- 'modelref:43b07dd7fc80f4a9889b6c672c450a911086293cab2ef4058b95b53559a2d100',
379
- 'draft:destination.Dubai'
380
- ]);
301
+ assert.deepEqual(sanitizedDecisions[0].action?.factRefs, ['modelref:43b07dd7fc80f4a9889b6c672c450a911086293cab2ef4058b95b53559a2d100', 'draft:destination.Dubai']);
381
302
  await sanitizedRef.close();
382
- const collisionRawRefs = [
383
- 'fact://a/b',
384
- 'fact:..a?b'
385
- ];
303
+ const collisionRawRefs = ['fact://a/b', 'fact:..a?b'];
386
304
  const collisionResults = [];
387
- for (const [index, rawRef] of collisionRawRefs.entries()){
305
+ for (const [index, rawRef] of collisionRawRefs.entries()) {
388
306
  const collisionPlanner = await createDshEmbeddedBookingPlanner({
389
307
  runPort: {
390
- async run () {
391
- return {
392
- finalResponse: '',
393
- events: [
394
- {
395
- type: 'tool/call',
396
- data: {
397
- name: 'booking_search_hotels',
398
- arguments: JSON.stringify({
399
- decision: {
400
- kind: 'operation',
401
- action: {
402
- ...searchRun,
403
- actionId: `action-dsh-collision-${index}`,
404
- factRefs: [
405
- rawRef
406
- ]
407
- }
408
- }
409
- })
410
- }
411
- }
412
- ]
413
- };
308
+ async run() {
309
+ return { finalResponse: '', events: [{ type: 'tool/call', data: { name: 'booking_search_hotels', arguments: JSON.stringify({ decision: { kind: 'operation', action: { ...searchRun, actionId: `action-dsh-collision-${index}`, factRefs: [rawRef] } } }) } }] };
414
310
  },
415
- async close () {}
416
- }
417
- });
418
- const decisions = await collisionPlanner.plannerFactory(task).next({
419
- task,
420
- turn: {
421
- schemaVersion: 'booking.surface',
422
- kind: 'user.turn',
423
- taskId: task.taskId,
424
- turnId: `dsh-collision-${index}`,
425
- workspace,
426
- request: {
427
- text: 'Find hotels'
428
- }
429
- }
311
+ async close() { },
312
+ },
430
313
  });
314
+ const decisions = await collisionPlanner.plannerFactory(task).next({ task, turn: { schemaVersion: 'booking.surface', kind: 'user.turn', taskId: task.taskId, turnId: `dsh-collision-${index}`, workspace, request: { text: 'Find hotels' } } });
431
315
  const ref = decisions[0].action?.factRefs?.[0];
432
316
  assert.match(ref ?? '', /^[A-Za-z0-9][A-Za-z0-9:._-]*$/, 'unsafe refs map to the runtime-safe pattern');
433
317
  collisionResults.push(ref ?? '');
@@ -438,168 +322,52 @@ assert.equal(collisionRawRefs[0].replace(/#/g, ':').replace(/[^A-Za-z0-9:._-]/g,
438
322
  async function assertFactRefsRejected(factRefs, message) {
439
323
  const planner = await createDshEmbeddedBookingPlanner({
440
324
  runPort: {
441
- async run () {
442
- return {
443
- finalResponse: '',
444
- events: [
445
- {
446
- type: 'tool/call',
447
- data: {
448
- name: 'booking_search_hotels',
449
- arguments: JSON.stringify({
450
- decision: {
451
- kind: 'operation',
452
- action: {
453
- ...searchRun,
454
- factRefs
455
- }
456
- }
457
- })
458
- }
459
- }
460
- ]
461
- };
325
+ async run() {
326
+ return { finalResponse: '', events: [{ type: 'tool/call', data: { name: 'booking_search_hotels', arguments: JSON.stringify({ decision: { kind: 'operation', action: { ...searchRun, factRefs } } }) } }] };
462
327
  },
463
- async close () {}
464
- }
328
+ async close() { },
329
+ },
465
330
  });
466
- await assert.rejects(planner.plannerFactory(task).next({
467
- task,
468
- turn: {
469
- schemaVersion: 'booking.surface',
470
- kind: 'user.turn',
471
- taskId: task.taskId,
472
- turnId: 'dsh-ref-rejection',
473
- workspace,
474
- request: {
475
- text: 'Find hotels'
476
- }
477
- }
478
- }), /planner_invalid_action/, message);
331
+ await assert.rejects(planner.plannerFactory(task).next({ task, turn: { schemaVersion: 'booking.surface', kind: 'user.turn', taskId: task.taskId, turnId: 'dsh-ref-rejection', workspace, request: { text: 'Find hotels' } } }), /planner_invalid_action/, message);
479
332
  await planner.close();
480
333
  }
481
334
  const firstAlias = 'modelref:'.concat(createHash('sha256').update(collisionRawRefs[0], 'utf8').digest('hex'));
482
- await assertFactRefsRejected([
483
- collisionRawRefs[0],
484
- firstAlias
485
- ], 'unsafe raw ref cannot alias a same-action reserved modelref');
486
- await assertFactRefsRejected([
487
- collisionRawRefs[0],
488
- collisionRawRefs[0]
489
- ], 'repair-time duplicate factRefs are rejected by canonical validation');
490
- await assertFactRefsRejected([
491
- 'modelref:'.concat('a'.repeat(64))
492
- ], 'direct modelref namespace input is reserved and rejected');
335
+ await assertFactRefsRejected([collisionRawRefs[0], firstAlias], 'unsafe raw ref cannot alias a same-action reserved modelref');
336
+ await assertFactRefsRejected([collisionRawRefs[0], collisionRawRefs[0]], 'repair-time duplicate factRefs are rejected by canonical validation');
337
+ await assertFactRefsRejected(['modelref:'.concat('a'.repeat(64))], 'direct modelref namespace input is reserved and rejected');
493
338
  const reservedRecovery = await createDshEmbeddedBookingPlanner({
494
339
  runPort: {
495
- async run () {
496
- return {
497
- finalResponse: JSON.stringify({
498
- kind: 'operation',
499
- action: {
500
- ...searchRun,
501
- factRefs: [
502
- 'modelref:'.concat('b'.repeat(64))
503
- ]
504
- }
505
- }),
506
- events: []
507
- };
340
+ async run() {
341
+ return { finalResponse: JSON.stringify({ kind: 'operation', action: { ...searchRun, factRefs: ['modelref:'.concat('b'.repeat(64))] } }), events: [] };
508
342
  },
509
- async close () {}
510
- }
511
- });
512
- const reservedRecoveryDecision = await reservedRecovery.plannerFactory(task).next({
513
- task,
514
- turn: {
515
- schemaVersion: 'booking.surface',
516
- kind: 'user.turn',
517
- taskId: task.taskId,
518
- turnId: 'dsh-reserved-recovery',
519
- workspace,
520
- request: {
521
- text: 'Find hotels'
522
- }
523
- }
343
+ async close() { },
344
+ },
524
345
  });
346
+ const reservedRecoveryDecision = await reservedRecovery.plannerFactory(task).next({ task, turn: { schemaVersion: 'booking.surface', kind: 'user.turn', taskId: task.taskId, turnId: 'dsh-reserved-recovery', workspace, request: { text: 'Find hotels' } } });
525
347
  assert.equal(reservedRecoveryDecision[0]?.kind, 'error', 'finalResponse recovery rejects direct reserved modelref aliases');
526
348
  await reservedRecovery.close();
527
349
  let stableRefCall = 0;
528
350
  const stableRawRef = 'fact://same/raw';
529
351
  const stablePlanner = await createDshEmbeddedBookingPlanner({
530
352
  runPort: {
531
- async run () {
353
+ async run() {
532
354
  stableRefCall += 1;
533
- return {
534
- finalResponse: '',
535
- events: [
536
- {
537
- type: 'tool/call',
538
- data: {
539
- name: 'booking_search_hotels',
540
- arguments: JSON.stringify({
541
- decision: {
542
- kind: 'operation',
543
- action: {
544
- ...searchRun,
545
- actionId: `action-dsh-stable-${stableRefCall}`,
546
- factRefs: [
547
- stableRawRef
548
- ]
549
- }
550
- }
551
- })
552
- }
553
- }
554
- ]
555
- };
355
+ return { finalResponse: '', events: [{ type: 'tool/call', data: { name: 'booking_search_hotels', arguments: JSON.stringify({ decision: { kind: 'operation', action: { ...searchRun, actionId: `action-dsh-stable-${stableRefCall}`, factRefs: [stableRawRef] } } }) } }] };
556
356
  },
557
- async close () {}
558
- }
559
- });
560
- const stableFirst = await stablePlanner.plannerFactory(task).next({
561
- task,
562
- turn: {
563
- schemaVersion: 'booking.surface',
564
- kind: 'user.turn',
565
- taskId: task.taskId,
566
- turnId: 'dsh-stable-1',
567
- workspace,
568
- request: {
569
- text: 'Find hotels'
570
- }
571
- }
572
- });
573
- const stableSecond = await stablePlanner.plannerFactory(task).next({
574
- task,
575
- turn: {
576
- schemaVersion: 'booking.surface',
577
- kind: 'user.turn',
578
- taskId: task.taskId,
579
- turnId: 'dsh-stable-2',
580
- workspace,
581
- request: {
582
- text: 'Find hotels again'
583
- }
584
- }
357
+ async close() { },
358
+ },
585
359
  });
360
+ const stableFirst = await stablePlanner.plannerFactory(task).next({ task, turn: { schemaVersion: 'booking.surface', kind: 'user.turn', taskId: task.taskId, turnId: 'dsh-stable-1', workspace, request: { text: 'Find hotels' } } });
361
+ const stableSecond = await stablePlanner.plannerFactory(task).next({ task, turn: { schemaVersion: 'booking.surface', kind: 'user.turn', taskId: task.taskId, turnId: 'dsh-stable-2', workspace, request: { text: 'Find hotels again' } } });
586
362
  assert.equal(stableFirst[0].action?.factRefs?.[0], stableSecond[0].action?.factRefs?.[0], 'same raw ref is stable across actions and turns');
587
363
  await stablePlanner.close();
588
364
  const unauthorisedPort = {
589
- async run () {
590
- return {
591
- finalResponse: JSON.stringify({
592
- kind: 'operation',
593
- action: hotelSelect
594
- }),
595
- events: []
596
- };
365
+ async run() {
366
+ return { finalResponse: JSON.stringify({ kind: 'operation', action: hotelSelect }), events: [] };
597
367
  },
598
- async close () {}
368
+ async close() { },
599
369
  };
600
- const unauthorised = await createDshEmbeddedBookingPlanner({
601
- runPort: unauthorisedPort
602
- });
370
+ const unauthorised = await createDshEmbeddedBookingPlanner({ runPort: unauthorisedPort });
603
371
  const unauthorisedDecisions = await unauthorised.plannerFactory(task).next({
604
372
  task,
605
373
  turn: {
@@ -609,49 +377,25 @@ const unauthorisedDecisions = await unauthorised.plannerFactory(task).next({
609
377
  turnId: 'dsh-turn-4',
610
378
  workspace: {
611
379
  ...workspace,
612
- capabilities: {
613
- ...workspace.capabilities,
614
- allowedActions: [
615
- ...workspace.capabilities.allowedActions
616
- ]
617
- }
380
+ capabilities: { ...workspace.capabilities, allowedActions: [...workspace.capabilities.allowedActions] },
618
381
  },
619
- request: {
620
- text: 'Select a hotel'
621
- }
622
- }
382
+ request: { text: 'Select a hotel' },
383
+ },
623
384
  });
624
385
  assert.equal(unauthorisedDecisions[0]?.kind, 'error', 'text-channel decisions outside allowedActions stay non-executable');
625
386
  const fragmentRefPort = {
626
- async run () {
387
+ async run() {
627
388
  return {
628
389
  finalResponse: '',
629
- events: [
630
- {
390
+ events: [{
631
391
  type: 'tool/call',
632
- data: {
633
- name: 'booking_search_hotels',
634
- arguments: JSON.stringify({
635
- decision: {
636
- kind: 'operation',
637
- action: {
638
- ...searchRun,
639
- factRefs: [
640
- `fact://turn_${task.lastTurnId}/request`
641
- ]
642
- }
643
- }
644
- })
645
- }
646
- }
647
- ]
392
+ data: { name: 'booking_search_hotels', arguments: JSON.stringify({ decision: { kind: 'operation', action: { ...searchRun, factRefs: [`fact://turn_${task.lastTurnId}/request`] } } }) },
393
+ }],
648
394
  };
649
395
  },
650
- async close () {}
396
+ async close() { },
651
397
  };
652
- const fragmentRef = await createDshEmbeddedBookingPlanner({
653
- runPort: fragmentRefPort
654
- });
398
+ const fragmentRef = await createDshEmbeddedBookingPlanner({ runPort: fragmentRefPort });
655
399
  const fragmentDecisions = await fragmentRef.plannerFactory(task).next({
656
400
  task,
657
401
  turn: {
@@ -660,39 +404,22 @@ const fragmentDecisions = await fragmentRef.plannerFactory(task).next({
660
404
  taskId: task.taskId,
661
405
  turnId: 'dsh-turn-7',
662
406
  workspace,
663
- request: {
664
- text: 'Find hotels'
665
- }
666
- }
407
+ request: { text: 'Find hotels' },
408
+ },
667
409
  });
668
410
  assert.equal(fragmentDecisions[0]?.kind, 'operation', 'JSON-pointer fragment factRefs repair into the safe charset');
669
- assert.deepEqual(fragmentDecisions[0].action?.factRefs, [
670
- `modelref:c7819d3c0c375fe1bd389006338b99be890fa8e3ddd9390f243cba8b5c139d9a`
671
- ]);
411
+ assert.deepEqual(fragmentDecisions[0].action?.factRefs, [`modelref:c7819d3c0c375fe1bd389006338b99be890fa8e3ddd9390f243cba8b5c139d9a`]);
672
412
  await fragmentRef.close();
673
413
  const truncatedPort = {
674
- async run () {
675
- const truncated = JSON.stringify({
676
- decision: {
677
- action: {
678
- ...searchRun,
679
- factRefs: [
680
- 'turn_cap-request'
681
- ]
682
- },
683
- kind: 'operation'
684
- }
685
- }).replace(/}+$/, '');
686
- return {
687
- finalResponse: truncated,
688
- events: []
689
- };
414
+ async run() {
415
+ // Real UAT capture: a reasoning-token budget cut the visible JSON before
416
+ // its closing braces; the authority path must still judge the payload.
417
+ const truncated = JSON.stringify({ decision: { action: { ...searchRun, factRefs: ['turn_cap-request'] }, kind: 'operation' } }).replace(/}+$/, '');
418
+ return { finalResponse: truncated, events: [] };
690
419
  },
691
- async close () {}
420
+ async close() { },
692
421
  };
693
- const truncatedRecovery = await createDshEmbeddedBookingPlanner({
694
- runPort: truncatedPort
695
- });
422
+ const truncatedRecovery = await createDshEmbeddedBookingPlanner({ runPort: truncatedPort });
696
423
  const truncatedDecisions = await truncatedRecovery.plannerFactory(task).next({
697
424
  task,
698
425
  turn: {
@@ -701,43 +428,24 @@ const truncatedDecisions = await truncatedRecovery.plannerFactory(task).next({
701
428
  taskId: task.taskId,
702
429
  turnId: 'dsh-turn-8',
703
430
  workspace,
704
- request: {
705
- text: 'Find hotels'
706
- }
707
- }
431
+ request: { text: 'Find hotels' },
432
+ },
708
433
  });
709
434
  assert.equal(truncatedDecisions[0]?.kind, 'operation', 'truncated-but-reconstructable finalResponse recovers through the authority path');
710
435
  await truncatedRecovery.close();
711
436
  const unsafeRefPort = {
712
- async run () {
437
+ async run() {
713
438
  return {
714
439
  finalResponse: '',
715
- events: [
716
- {
440
+ events: [{
717
441
  type: 'tool/call',
718
- data: {
719
- name: 'booking_search_hotels',
720
- arguments: JSON.stringify({
721
- decision: {
722
- kind: 'operation',
723
- action: {
724
- ...searchRun,
725
- factRefs: [
726
- 42
727
- ]
728
- }
729
- }
730
- })
731
- }
732
- }
733
- ]
442
+ data: { name: 'booking_search_hotels', arguments: JSON.stringify({ decision: { kind: 'operation', action: { ...searchRun, factRefs: [42] } } }) },
443
+ }],
734
444
  };
735
445
  },
736
- async close () {}
446
+ async close() { },
737
447
  };
738
- const unsafeRef = await createDshEmbeddedBookingPlanner({
739
- runPort: unsafeRefPort
740
- });
448
+ const unsafeRef = await createDshEmbeddedBookingPlanner({ runPort: unsafeRefPort });
741
449
  await assert.rejects(unsafeRef.plannerFactory(task).next({
742
450
  task,
743
451
  turn: {
@@ -746,64 +454,40 @@ await assert.rejects(unsafeRef.plannerFactory(task).next({
746
454
  taskId: task.taskId,
747
455
  turnId: 'dsh-turn-6',
748
456
  workspace,
749
- request: {
750
- text: 'Find hotels'
751
- }
752
- }
457
+ request: { text: 'Find hotels' },
458
+ },
753
459
  }), /planner_invalid_action/, 'non-string factRef entries retry as parse-class failures, not ledger-boundary crashes');
754
460
  await unsafeRef.close();
461
+ // A workspace with loadedOffers for a hotel absent from the availability
462
+ // state (UI-loaded offers) must not crash the prompt projection.
755
463
  const foreignOfferWorkspace = {
756
464
  ...workspace,
757
- loadedOffers: [
758
- {
759
- offerRef: "offer-ui-1",
760
- offerVersionRef: "offerv-ui-1",
761
- hotelRef: "hotel-ui-9",
762
- evidenceLevel: "rate_loaded",
763
- factRefs: []
764
- }
765
- ]
465
+ loadedOffers: [{ offerRef: "offer-ui-1", offerVersionRef: "offerv-ui-1", hotelRef: "hotel-ui-9", evidenceLevel: "rate_loaded", factRefs: [] }],
766
466
  };
467
+ let uiOffersRuns = 0;
767
468
  const uiOffersPort = {
768
- async run (prompt) {
769
- assert.ok(prompt.includes("hotel-ui-9"), "workspace loadedOffers reach the planner payload");
770
- return {
771
- finalResponse: "",
772
- events: []
773
- };
469
+ async run(prompt) {
470
+ uiOffersRuns += 1;
471
+ if (uiOffersRuns === 1)
472
+ assert.ok(prompt.includes("hotel-ui-9"), "workspace loadedOffers reach the planner payload");
473
+ return { finalResponse: "", events: [] };
774
474
  },
775
- async close () {}
475
+ async close() { },
776
476
  };
777
- const uiOffers = await createDshEmbeddedBookingPlanner({
778
- runPort: uiOffersPort
779
- });
477
+ const uiOffers = await createDshEmbeddedBookingPlanner({ runPort: uiOffersPort });
780
478
  const uiOffersDecisions = await uiOffers.plannerFactory(task).next({
781
479
  task,
782
- turn: {
783
- schemaVersion: "booking.surface",
784
- kind: "user.turn",
785
- taskId: task.taskId,
786
- turnId: "dsh-turn-ui",
787
- workspace: foreignOfferWorkspace,
788
- request: {
789
- text: "Prepare the loaded offer"
790
- }
791
- }
480
+ turn: { schemaVersion: "booking.surface", kind: "user.turn", taskId: task.taskId, turnId: "dsh-turn-ui", workspace: foreignOfferWorkspace, request: { text: "Prepare the loaded offer" } },
792
481
  });
793
482
  assert.equal(uiOffersDecisions[0]?.kind, "error", "foreign loadedOffers reach the payload without crashing the projection");
794
483
  await uiOffers.close();
795
484
  const plainProsePort = {
796
- async run () {
797
- return {
798
- finalResponse: 'I would search hotels in Dubai for you.',
799
- events: []
800
- };
485
+ async run() {
486
+ return { finalResponse: 'I would search hotels in Dubai for you.', events: [] };
801
487
  },
802
- async close () {}
488
+ async close() { },
803
489
  };
804
- const plainProse = await createDshEmbeddedBookingPlanner({
805
- runPort: plainProsePort
806
- });
490
+ const plainProse = await createDshEmbeddedBookingPlanner({ runPort: plainProsePort });
807
491
  const plainProseDecisions = await plainProse.plannerFactory(task).next({
808
492
  task,
809
493
  turn: {
@@ -812,120 +496,68 @@ const plainProseDecisions = await plainProse.plannerFactory(task).next({
812
496
  taskId: task.taskId,
813
497
  turnId: 'dsh-turn-5',
814
498
  workspace,
815
- request: {
816
- text: 'Find hotels'
817
- }
818
- }
499
+ request: { text: 'Find hotels' },
500
+ },
819
501
  });
820
502
  assert.equal(plainProseDecisions[0]?.kind, 'error', 'prose without a typed decision envelope is never executable');
821
503
  const forbiddenPort = {
822
- async run () {
504
+ async run() {
823
505
  return {
824
506
  finalResponse: '',
825
- events: [
826
- {
507
+ events: [{
827
508
  type: 'tool/call',
828
- data: {
829
- name: 'gotry_book',
830
- arguments: JSON.stringify({
831
- decision: {
832
- kind: 'operation',
833
- action: {
834
- ...searchRun,
835
- kind: 'book'
836
- }
837
- }
838
- })
839
- }
840
- }
841
- ]
509
+ data: { name: 'gotry_book', arguments: JSON.stringify({ decision: { kind: 'operation', action: { ...searchRun, kind: 'book' } } }) },
510
+ }],
842
511
  };
843
512
  },
844
- async close () {}
513
+ async close() { },
845
514
  };
846
- const forbidden = await createDshEmbeddedBookingPlanner({
847
- runPort: forbiddenPort
848
- });
515
+ const forbidden = await createDshEmbeddedBookingPlanner({ runPort: forbiddenPort });
849
516
  await assert.rejects(forbidden.plannerFactory(task).next({
850
517
  task,
851
518
  turn: {
852
- schemaVersion: 'booking.surface',
853
- kind: 'user.turn',
854
- taskId: task.taskId,
855
- turnId: 'dsh-turn-4',
856
- workspace: {
857
- ...workspace,
858
- capabilities: {
859
- ...workspace.capabilities,
860
- allowedActions: [
861
- ...workspace.capabilities.allowedActions
862
- ]
863
- }
864
- },
865
- request: {
866
- text: '帮我下单'
867
- }
868
- }
519
+ schemaVersion: 'booking.surface', kind: 'user.turn', taskId: task.taskId, turnId: 'dsh-turn-4',
520
+ workspace: { ...workspace, capabilities: { ...workspace.capabilities, allowedActions: [...workspace.capabilities.allowedActions] } },
521
+ request: { text: '帮我下单' },
522
+ },
869
523
  }), /planner_forbidden_tool/);
870
524
  const terminalTask = {
871
- schemaVersion: 'booking.surface',
872
- taskId: 'task-dsh-terminal',
873
- contextRef: 'ctx-dsh-terminal',
874
- surface: 'tenant',
875
- revision: 0,
876
- allowedActions: [
877
- 'search.run'
878
- ],
879
- userTurnCount: 1,
880
- operationCount: 0,
881
- phase: 'planning',
882
- lastSequence: 0,
883
- availability: {
884
- initialized: true,
885
- recoveryStarted: true,
886
- availabilityPhase: 'terminal',
887
- activeHotelOrdinal: 0,
888
- hotelRefs: [],
889
- hotels: {},
890
- attempts: [],
891
- queryReservations: [],
892
- terminal: {
893
- code: 'availability_exhausted_complete',
894
- hotelRefs: [],
895
- reason: 'no_current_offers',
896
- evidence: 'conclusive'
897
- }
898
- }
525
+ schemaVersion: 'booking.surface', taskId: 'task-dsh-terminal', contextRef: 'ctx-dsh-terminal', surface: 'tenant', revision: 0,
526
+ allowedActions: ['search.run'], userTurnCount: 1, operationCount: 0, phase: 'planning', lastSequence: 0,
527
+ availability: { initialized: true, recoveryStarted: true, availabilityPhase: 'terminal', activeHotelOrdinal: 0, hotelRefs: [], hotels: {}, attempts: [], queryReservations: [], terminal: { code: 'availability_exhausted_complete', hotelRefs: [], reason: 'no_current_offers', evidence: 'conclusive' } },
899
528
  };
900
529
  const terminalWorkspace = {
901
- schemaVersion: 'booking.surface',
902
- contextRef: terminalTask.contextRef,
903
- surface: 'tenant',
904
- revision: 0,
905
- locale: 'en-US',
906
- currency: 'AED',
907
- searchDraft: {},
908
- results: {
909
- status: 'idle'
910
- },
911
- visibleHotels: [],
912
- loadedOffers: [],
913
- shortlistedOfferRefs: [],
914
- capabilities: {
915
- surface: 'tenant',
916
- allowedActions: [
917
- 'search.run'
918
- ]
919
- }
530
+ schemaVersion: 'booking.surface', contextRef: terminalTask.contextRef, surface: 'tenant', revision: 0, locale: 'en-US', currency: 'AED',
531
+ searchDraft: {}, results: { status: 'idle' }, visibleHotels: [], loadedOffers: [], shortlistedOfferRefs: [],
532
+ capabilities: { surface: 'tenant', allowedActions: ['search.run'] },
920
533
  };
921
534
  let terminalRuns = 0;
922
535
  const terminalPort = {
923
- async run () {
924
- terminalRuns++;
925
- return terminalRuns === 1 ? {
536
+ async run() { terminalRuns++; return terminalRuns === 1 ? { finalResponse: '', events: [{ type: 'tool/call', data: { name: 'booking_search_hotels', arguments: JSON.stringify({ decision: { kind: 'operation', action: { ...searchRun, schemaVersion: 'booking.surface', contextRef: terminalTask.contextRef, actionId: 'action-dsh-terminal' } } }) } }] } : { finalResponse: '', events: [{ type: 'tool/call', data: { name: 'booking_search_hotels', arguments: JSON.stringify({ decision: { kind: 'terminal', terminal: { status: 'stopped', summary: 'receipt continuation handled', factRefs: [] } } }) } }] }; },
537
+ async close() { },
538
+ };
539
+ const terminalAdapter = await createDshEmbeddedBookingPlanner({ runPort: terminalPort });
540
+ const terminalDecisions = await terminalAdapter.plannerFactory(terminalTask).next({ task: terminalTask, turn: { schemaVersion: 'booking.surface', kind: 'user.turn', taskId: terminalTask.taskId, turnId: 'dsh-terminal-turn-1', workspace: terminalWorkspace, request: { text: 'find hotels' } } });
541
+ assert.equal(terminalDecisions[0]?.kind, 'operation', 'real DSH adapter accepts the canonical typed action');
542
+ assert.equal(terminalDecisions[0]?.kind === 'operation' ? terminalDecisions[0].action.schemaVersion : '', 'booking.surface');
543
+ const terminalContinuation = { schemaVersion: 'booking.surface', kind: 'action.receipt.continuation', taskId: terminalTask.taskId, workspace: { ...terminalWorkspace, revision: 1 }, receipt: { schemaVersion: 'booking.surface', kind: 'action.receipt', actionId: 'action-dsh-terminal', contextRef: terminalTask.contextRef, status: 'applied', revision: 1, observation: { kind: 'search.state', resultCount: 1 }, resultContract: { outcome: 'complete', hardCriteriaMet: true, factRefs: [], gapCodes: [], blockers: [], relaxationsApplied: [] } } };
544
+ const terminalContinuationDecision = await terminalAdapter.plannerFactory(terminalTask).next({ task: { ...terminalTask, revision: 1, lastReceipt: terminalContinuation.receipt }, turn: terminalContinuation });
545
+ assert.deepEqual(terminalContinuationDecision, [{ kind: 'terminal', terminal: { status: 'stopped', summary: 'receipt continuation handled', factRefs: [] } }], 'real DSH planner handles receipt continuation through the typed seam');
546
+ await assert.rejects(terminalAdapter.plannerFactory(terminalTask).next({
547
+ task: terminalTask,
548
+ turn: { schemaVersion: 'booking.surface', kind: 'user.turn.ingress', requestKey: 'browser-request', surfaceHint: 'tenant', workspace: { schemaVersion: 'booking.surface', revision: 0, locale: 'en-US', currency: 'AED', searchDraft: {}, results: { status: 'idle' }, visibleHotels: [], loadedOffers: [], shortlistedOfferRefs: [] }, request: { text: 'browser ingress must be BFF-bound' } },
549
+ }), /planner_identity_required/);
550
+ // Regression: MiniMax-M3 serializes arrays as index-keyed objects and emits
551
+ // information-free empty room objects. The repair pass must convert
552
+ // {"0":{"adults":2,"childAges":{}},"1":{}} into rooms [{adults:2,childAges:[]}]
553
+ // in index order, dropping the empty room, and must not leak the empty
554
+ // childAges object as a stringified singleton. Drives the same runPort ->
555
+ // tool/call -> parseToolDecision boundary the real adapter uses.
556
+ const indexKeyedRoomsPort = {
557
+ async run() {
558
+ return {
926
559
  finalResponse: '',
927
- events: [
928
- {
560
+ events: [{
929
561
  type: 'tool/call',
930
562
  data: {
931
563
  name: 'booking_search_hotels',
@@ -933,143 +565,145 @@ const terminalPort = {
933
565
  decision: {
934
566
  kind: 'operation',
935
567
  action: {
936
- ...searchRun,
937
568
  schemaVersion: 'booking.surface',
938
- contextRef: terminalTask.contextRef,
939
- actionId: 'action-dsh-terminal'
940
- }
941
- }
942
- })
943
- }
944
- }
945
- ]
946
- } : {
569
+ kind: 'search.patch',
570
+ actionId: 'action-dsh-index-keyed-rooms',
571
+ contextRef: task.contextRef,
572
+ expectedRevision: 0,
573
+ reason: 'Patch occupancy from an index-keyed model payload.',
574
+ factRefs: [],
575
+ input: { patch: { occupancy: { rooms: { '0': { adults: 2, childAges: {} }, '1': {} } } } },
576
+ },
577
+ },
578
+ }),
579
+ },
580
+ }],
581
+ };
582
+ },
583
+ async close() { },
584
+ };
585
+ const indexKeyedRooms = await createDshEmbeddedBookingPlanner({ runPort: indexKeyedRoomsPort });
586
+ const indexKeyedDecisions = await indexKeyedRooms.plannerFactory(task).next({
587
+ task,
588
+ turn: {
589
+ schemaVersion: 'booking.surface',
590
+ kind: 'user.turn',
591
+ taskId: task.taskId,
592
+ turnId: 'dsh-turn-index-keyed-rooms',
593
+ workspace: { ...workspace, capabilities: { ...workspace.capabilities, allowedActions: [...workspace.capabilities.allowedActions] } },
594
+ request: { text: 'Two adults, one room' },
595
+ },
596
+ });
597
+ assert.equal(indexKeyedDecisions[0]?.kind, 'operation', 'index-keyed occupancy rooms repair yields an executable search.patch');
598
+ assert.deepEqual(indexKeyedDecisions[0].action?.input?.patch?.occupancy?.rooms, [{ adults: 2, childAges: [] }], 'index-keyed rooms object becomes an ordered array with the empty room dropped and empty childAges normalized to []');
599
+ // Regression for #282: a non-empty room that carries child ages but omits
600
+ // adults is a semantic validation failure, not disposable representation.
601
+ // The provider must repair that room in the next counted session call.
602
+ let occupancyRepairRuns = 0;
603
+ const occupancyRepairPort = {
604
+ async run(prompt) {
605
+ occupancyRepairRuns += 1;
606
+ if (occupancyRepairRuns === 1) {
607
+ return {
608
+ finalResponse: '',
609
+ events: [{
610
+ type: 'tool/call',
611
+ data: {
612
+ name: 'booking_search_hotels',
613
+ arguments: JSON.stringify({
614
+ decision: {
615
+ kind: 'operation',
616
+ action: {
617
+ ...searchRun,
618
+ kind: 'search.patch',
619
+ actionId: 'action-dsh-282-occupancy-invalid',
620
+ reason: 'Preserve both requested rooms and their child ages.',
621
+ input: { patch: { occupancy: { rooms: [{ adults: 2, childAges: [6] }, { childAges: [4] }] } } },
622
+ },
623
+ },
624
+ }),
625
+ },
626
+ }],
627
+ };
628
+ }
629
+ assert.match(prompt, /schema validation/, 'occupancy correction is sent as the next counted prompt');
630
+ return {
947
631
  finalResponse: '',
948
- events: [
949
- {
632
+ events: [{
950
633
  type: 'tool/call',
951
634
  data: {
952
635
  name: 'booking_search_hotels',
953
636
  arguments: JSON.stringify({
954
637
  decision: {
955
- kind: 'terminal',
956
- terminal: {
957
- status: 'stopped',
958
- summary: 'receipt continuation handled',
959
- factRefs: []
960
- }
961
- }
962
- })
963
- }
964
- }
965
- ]
638
+ kind: 'operation',
639
+ action: {
640
+ ...searchRun,
641
+ kind: 'search.patch',
642
+ actionId: 'action-dsh-282-occupancy-valid',
643
+ reason: 'Preserve both requested rooms and their child ages.',
644
+ input: { patch: { occupancy: { rooms: [{ adults: 2, childAges: [6] }, { adults: 1, childAges: [4] }] } } },
645
+ },
646
+ },
647
+ }),
648
+ },
649
+ }],
966
650
  };
967
651
  },
968
- async close () {}
652
+ async close() { },
969
653
  };
970
- const terminalAdapter = await createDshEmbeddedBookingPlanner({
971
- runPort: terminalPort
972
- });
973
- const terminalDecisions = await terminalAdapter.plannerFactory(terminalTask).next({
974
- task: terminalTask,
975
- turn: {
976
- schemaVersion: 'booking.surface',
977
- kind: 'user.turn',
978
- taskId: terminalTask.taskId,
979
- turnId: 'dsh-terminal-turn-1',
980
- workspace: terminalWorkspace,
981
- request: {
982
- text: 'find hotels'
983
- }
984
- }
654
+ const occupancyRepair = await createDshEmbeddedBookingPlanner({ runPort: occupancyRepairPort });
655
+ const occupancyRepairDecision = await occupancyRepair.plannerFactory(task).next({
656
+ task,
657
+ turn: { schemaVersion: 'booking.surface', kind: 'user.turn', taskId: task.taskId, turnId: 'dsh-turn-282-occupancy', workspace, request: { text: '请保留两个房间和各自的儿童年龄:第一间2成人1儿童6岁,第二间1成人1儿童4岁' } },
985
658
  });
986
- assert.equal(terminalDecisions[0]?.kind, 'operation', 'real DSH adapter accepts the canonical typed action');
987
- assert.equal(terminalDecisions[0]?.kind === 'operation' ? terminalDecisions[0].action.schemaVersion : '', 'booking.surface');
988
- const terminalContinuation = {
989
- schemaVersion: 'booking.surface',
990
- kind: 'action.receipt.continuation',
991
- taskId: terminalTask.taskId,
992
- workspace: {
993
- ...terminalWorkspace,
994
- revision: 1
659
+ assert.equal(occupancyRepairRuns, 2, 'missing adults triggers one counted correction call');
660
+ assert.deepEqual(occupancyRepairDecision[0]?.kind === 'operation' ? occupancyRepairDecision[0].action.input : undefined, { patch: { occupancy: { rooms: [{ adults: 2, childAges: [6] }, { adults: 1, childAges: [4] }] } } }, 'corrected occupancy preserves both non-empty rooms and child criteria');
661
+ // A prose-only first response receives the same bounded correction treatment
662
+ // as a malformed typed call; a valid second response is returned immediately.
663
+ let proseRecoveryRuns = 0;
664
+ const proseRecoveryPort = {
665
+ async run(prompt) {
666
+ proseRecoveryRuns += 1;
667
+ if (proseRecoveryRuns === 1)
668
+ return { finalResponse: '我会为你搜索酒店。', events: [] };
669
+ assert.match(prompt, /no booking capability tool call/, 'prose correction is sent as the next counted prompt');
670
+ return { finalResponse: '', events: [{ type: 'tool/call', data: { name: 'booking_search_hotels', arguments: JSON.stringify({ decision: { kind: 'operation', action: searchRun } }) } }] };
995
671
  },
996
- receipt: {
997
- schemaVersion: 'booking.surface',
998
- kind: 'action.receipt',
999
- actionId: 'action-dsh-terminal',
1000
- contextRef: terminalTask.contextRef,
1001
- status: 'applied',
1002
- revision: 1,
1003
- observation: {
1004
- kind: 'search.state',
1005
- resultCount: 1
1006
- },
1007
- resultContract: {
1008
- outcome: 'complete',
1009
- hardCriteriaMet: true,
1010
- factRefs: [],
1011
- gapCodes: [],
1012
- blockers: [],
1013
- relaxationsApplied: []
1014
- }
1015
- }
672
+ async close() { },
1016
673
  };
1017
- const terminalContinuationDecision = await terminalAdapter.plannerFactory(terminalTask).next({
1018
- task: {
1019
- ...terminalTask,
1020
- revision: 1,
1021
- lastReceipt: terminalContinuation.receipt
1022
- },
1023
- turn: terminalContinuation
674
+ const proseRecovery = await createDshEmbeddedBookingPlanner({ runPort: proseRecoveryPort });
675
+ const proseRecoveryDecision = await proseRecovery.plannerFactory(task).next({
676
+ task,
677
+ turn: { schemaVersion: 'booking.surface', kind: 'user.turn', taskId: task.taskId, turnId: 'dsh-turn-282-prose', workspace, request: { text: 'Find hotels' } },
1024
678
  });
1025
- assert.deepEqual(terminalContinuationDecision, [
1026
- {
1027
- kind: 'terminal',
1028
- terminal: {
1029
- status: 'stopped',
1030
- summary: 'receipt continuation handled',
1031
- factRefs: []
1032
- }
1033
- }
1034
- ], 'real DSH planner handles receipt continuation through the typed seam');
1035
- await assert.rejects(terminalAdapter.plannerFactory(terminalTask).next({
1036
- task: terminalTask,
1037
- turn: {
1038
- schemaVersion: 'booking.surface',
1039
- kind: 'user.turn.ingress',
1040
- requestKey: 'browser-request',
1041
- surfaceHint: 'tenant',
1042
- workspace: {
1043
- schemaVersion: 'booking.surface',
1044
- revision: 0,
1045
- locale: 'en-US',
1046
- currency: 'AED',
1047
- searchDraft: {},
1048
- results: {
1049
- status: 'idle'
1050
- },
1051
- visibleHotels: [],
1052
- loadedOffers: [],
1053
- shortlistedOfferRefs: []
1054
- },
1055
- request: {
1056
- text: 'browser ingress must be BFF-bound'
679
+ assert.equal(proseRecoveryRuns, 2, 'prose correction uses the second counted call');
680
+ assert.deepEqual(proseRecoveryDecision, [{ kind: 'operation', action: searchRun }], 'valid correction is returned instead of discarded');
681
+ // Repeated malformed responses stop at the three-call budget. A provider
682
+ // failure propagates on the first call instead of being silently swallowed.
683
+ let malformedRuns = 0;
684
+ const malformedPort = {
685
+ async run() {
686
+ malformedRuns += 1;
687
+ return { finalResponse: '', events: [{ type: 'tool/call', data: { name: 'booking_search_hotels', arguments: JSON.stringify({ decision: { kind: 'operation', action: { ...searchRun, input: { patch: { occupancy: { rooms: [{ childAges: [4] }] } } } } } }) } }] };
688
+ },
689
+ async close() { },
690
+ };
691
+ const malformed = await createDshEmbeddedBookingPlanner({ runPort: malformedPort });
692
+ await assert.rejects(malformed.plannerFactory(task).next({ task, turn: { schemaVersion: 'booking.surface', kind: 'user.turn', taskId: task.taskId, turnId: 'dsh-turn-282-malformed', workspace, request: { text: 'Find hotels' } } }), /planner_invalid_action/, 'repeated malformed output fails closed');
693
+ assert.equal(malformedRuns, 3, 'repeated malformed output consumes exactly three calls');
694
+ let providerRuns = 0;
695
+ const providerErrorPort = {
696
+ async run() {
697
+ providerRuns += 1;
698
+ if (providerRuns === 1) {
699
+ return { finalResponse: '', events: [{ type: 'tool/call', data: { name: 'booking_search_hotels', arguments: JSON.stringify({ decision: { kind: 'operation', action: { ...searchRun, input: { patch: { occupancy: { rooms: [{ childAges: [4] }] } } } } } }) } }] };
1057
700
  }
1058
- }
1059
- }), /planner_identity_required/);
1060
- await Promise.all([
1061
- adapter.close(),
1062
- textChannel.close(),
1063
- unauthorised.close(),
1064
- fragmentRef.close(),
1065
- truncatedRecovery.close(),
1066
- sanitizedRef.close(),
1067
- unsafeRef.close(),
1068
- uiOffers.close(),
1069
- forbidden.close(),
1070
- terminalAdapter.close()
1071
- ]);
701
+ throw new Error('provider_transport_failure');
702
+ },
703
+ async close() { },
704
+ };
705
+ const providerError = await createDshEmbeddedBookingPlanner({ runPort: providerErrorPort });
706
+ await assert.rejects(providerError.plannerFactory(task).next({ task, turn: { schemaVersion: 'booking.surface', kind: 'user.turn', taskId: task.taskId, turnId: 'dsh-turn-282-provider', workspace, request: { text: 'Find hotels' } } }), /provider_transport_failure/, 'provider failures remain visible to the caller');
707
+ assert.equal(providerRuns, 2, 'provider failures on a counted correction are not silently swallowed or retried');
708
+ await Promise.all([adapter.close(), textChannel.close(), unauthorised.close(), fragmentRef.close(), truncatedRecovery.close(), sanitizedRef.close(), unsafeRef.close(), uiOffers.close(), forbidden.close(), terminalAdapter.close(), indexKeyedRooms.close(), occupancyRepair.close(), proseRecovery.close(), malformed.close(), providerError.close()]);
1072
709
  console.log('BOOKING COPILOT DSH PLANNER PROOF: task session/typed tool decisions/no Book/no prose parser/no portal token OK');
1073
-
1074
-
1075
- //# sourceURL=ts/scripts/booking-copilot-dsh-planner-proof-tests.ts