@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.
- package/README.md +102 -194
- package/README.zh-CN.md +115 -210
- package/bin/gotry-backend.js +53 -0
- package/bin/gotry-bootstrap.js +466 -35
- package/bin/gotry-inner.js +255 -36
- package/bin/gotry-process-liveness.d.ts +27 -0
- package/bin/gotry-process-liveness.js +97 -0
- package/bin/gotry-runtime-resolution.d.ts +1 -1
- package/bin/gotry-runtime-resolution.js +2 -2
- package/client/client.js +174 -0
- package/cordis.gotry-patch.yml +70 -93
- package/dist/capabilities/agent-reach-deep.js +45 -122
- package/dist/capabilities/agent-reach.js +63 -148
- package/dist/capabilities/anything.js +89 -75
- package/dist/capabilities/artifacts.js +123 -104
- package/dist/capabilities/channel-health.js +65 -45
- package/dist/capabilities/channel-registry.js +108 -87
- package/dist/capabilities/doctor.js +357 -235
- package/dist/capabilities/effect.js +223 -309
- package/dist/capabilities/fact-log.js +28 -14
- package/dist/capabilities/flyai.js +245 -156
- package/dist/capabilities/ground-transfer.js +509 -0
- package/dist/capabilities/hbcli.js +144 -148
- package/dist/capabilities/hotelbyte-transaction.js +194 -0
- package/dist/capabilities/incident-log.js +131 -61
- package/dist/capabilities/model-override.js +30 -10
- package/dist/capabilities/opensky.js +54 -43
- package/dist/capabilities/resilience.js +50 -32
- package/dist/capabilities/session/action-cache.js +50 -44
- package/dist/capabilities/session/adapters/ctrip-flight.js +119 -62
- package/dist/capabilities/session/adapters/ctrip-hotel.js +125 -110
- package/dist/capabilities/session/adapters/dida-portal.js +206 -0
- package/dist/capabilities/session/adapters/meituan-local.js +35 -54
- package/dist/capabilities/session/adapters/rail-12306.js +169 -112
- package/dist/capabilities/session/benchmark.js +85 -149
- package/dist/capabilities/session/extension-bridge.js +320 -259
- package/dist/capabilities/session/extension-channel.js +57 -76
- package/dist/capabilities/session/extension-distribution.js +115 -119
- package/dist/capabilities/session/extract.js +31 -21
- package/dist/capabilities/session/golden-score.js +55 -33
- package/dist/capabilities/session/health-watch.js +146 -154
- package/dist/capabilities/session/read-guard.js +55 -47
- package/dist/capabilities/session/static-flight-golden.js +49 -36
- package/dist/capabilities/session/transport.js +54 -61
- package/dist/capabilities/session/wizard.js +63 -93
- package/dist/capabilities/session-consent.js +143 -47
- package/dist/capabilities/session-login.js +95 -126
- package/dist/capabilities/session-search.js +391 -253
- package/dist/capabilities/sponsor-plugin.js +341 -0
- package/dist/capabilities/visa-policy.js +87 -73
- package/dist/capabilities/weather.js +163 -235
- package/dist/scripts/action-cache-tests.js +24 -59
- package/dist/scripts/agent-planning-turn-deadline-e2e.js +91 -142
- package/dist/scripts/agent-planning-turn-deadline-tests.js +80 -176
- package/dist/scripts/agent-reach-deep-tests.js +30 -40
- package/dist/scripts/agent-reach-tests.js +26 -18
- package/dist/scripts/agent-reach-wrapper-tests.js +46 -50
- package/dist/scripts/anything-tests.js +58 -62
- package/dist/scripts/artifact-client-contract-tests.js +69 -0
- package/dist/scripts/artifacts-capability-tests.js +235 -0
- package/dist/scripts/async-collect.js +26 -15
- package/dist/scripts/benchmark-environment-bridge-e2e.js +287 -684
- package/dist/scripts/benchmark-environment-bridge-tests.js +818 -3123
- package/dist/scripts/booking-copilot-availability-ledger-binding-tests.js +66 -279
- package/dist/scripts/booking-copilot-availability-policy-tests.js +153 -1257
- package/dist/scripts/booking-copilot-bin-proof-tests.js +22 -45
- package/dist/scripts/booking-copilot-crossrepo-fixture-server.js +64 -76
- package/dist/scripts/booking-copilot-dsh-core-proof-tests.js +69 -199
- package/dist/scripts/booking-copilot-dsh-planner-proof-tests.js +393 -759
- package/dist/scripts/booking-copilot-dsh-plugin-proof-tests.js +33 -75
- package/dist/scripts/booking-copilot-event-sequence-concurrency-proof-tests.js +60 -110
- package/dist/scripts/booking-copilot-gap-code-contract-proof-tests.js +28 -66
- package/dist/scripts/booking-copilot-operation-ledger-concurrency-proof-tests.js +72 -271
- package/dist/scripts/booking-copilot-receipt-ledger-concurrency-proof-tests.js +73 -275
- package/dist/scripts/booking-copilot-runtime-proof-tests.js +784 -3398
- package/dist/scripts/booking-copilot-server-proof-tests.js +63 -168
- package/dist/scripts/booking-copilot-startup-proof-tests.js +38 -124
- package/dist/scripts/booking-executor-tests.js +80 -0
- package/dist/scripts/booking-recovery-chain-tests.js +182 -0
- package/dist/scripts/booking-saga-tests.js +61 -123
- package/dist/scripts/booking-surface-contract-proof-tests.js +152 -1167
- package/dist/scripts/bootstrap-tests.js +1383 -128
- package/dist/scripts/build-changelog.js +89 -130
- package/dist/scripts/build-metrics-report.js +135 -143
- package/dist/scripts/changelog-tests.js +35 -82
- package/dist/scripts/channel-probe-tests.js +44 -116
- package/dist/scripts/channel-probe.js +84 -167
- package/dist/scripts/channel-registry-tests.js +97 -185
- package/dist/scripts/companion-tests.js +25 -70
- package/dist/scripts/dida-cookie-probe.js +87 -0
- package/dist/scripts/dida-sw-probe.js +114 -0
- package/dist/scripts/diff-test.js +17 -12
- package/dist/scripts/doctor-tests.js +428 -120
- package/dist/scripts/dsh-artifact-e2e.js +150 -0
- package/dist/scripts/dsh-artifact-web-e2e.js +443 -0
- package/dist/scripts/dsh-http-proxy-sse-proof.js +90 -0
- package/dist/scripts/dsh-runtime-closure-tests.js +110 -153
- package/dist/scripts/dsh-runtime-closure.js +75 -63
- package/dist/scripts/dsh-session-v3-migration-proof.js +106 -0
- package/dist/scripts/dsh-subprocess-local-proof.js +149 -0
- package/dist/scripts/dsh-target-closure-proof.js +178 -0
- package/dist/scripts/effect-tests.js +165 -552
- package/dist/scripts/engine-run.js +1 -3
- package/dist/scripts/engine-tests.js +16 -10
- package/dist/scripts/evaluation-cadence-tests.js +69 -242
- package/dist/scripts/evaluation-contract-tests.js +193 -499
- package/dist/scripts/extension-distribution-cli.js +19 -15
- package/dist/scripts/extension-distribution-tests.js +98 -211
- package/dist/scripts/extension-tests.js +366 -389
- package/dist/scripts/fact-gate-tests.js +725 -263
- package/dist/scripts/fixtures/extension-bridge-unref-child.mjs +21 -0
- package/dist/scripts/flight-hotel-anchor-363-registered-e2e.js +169 -0
- package/dist/scripts/flight-malformed-tests.js +159 -0
- package/dist/scripts/flyai-tests.js +325 -68
- package/dist/scripts/g5-guard-tests.js +131 -0
- package/dist/scripts/g5-guard.js +189 -0
- package/dist/scripts/gen-lunar-anchors.js +106 -0
- package/dist/scripts/gotry-backend-kernel-tests.js +79 -0
- package/dist/scripts/ground-transfer-tests.js +956 -0
- package/dist/scripts/hbcli-e2e-tests.js +71 -90
- package/dist/scripts/hbcli-live-optin-tests.js +96 -0
- package/dist/scripts/hbcli-release-contract-tests.js +122 -0
- package/dist/scripts/hbcli-tests.js +41 -115
- package/dist/scripts/health-watch-cli.js +42 -28
- package/dist/scripts/hotel-date-gate-tests.js +496 -0
- package/dist/scripts/hotelbyte-reconcile-tests.js +141 -0
- package/dist/scripts/hotelbyte-spawn-e2e-tests.js +373 -0
- package/dist/scripts/i18n-tests.js +16 -9
- package/dist/scripts/incident-tests.js +281 -87
- package/dist/scripts/issue-194-job-id-guard-tests.js +106 -0
- package/dist/scripts/issue-233-cancel-refund-commission-tests.js +392 -0
- package/dist/scripts/issue-271-liveness-tests.js +547 -0
- package/dist/scripts/issue-289-model-retry-real-tests.js +296 -0
- package/dist/scripts/issue-289-model-retry-tests.js +441 -0
- package/dist/scripts/issue-289-web-retry-e2e.js +630 -0
- package/dist/scripts/issue-338-home-city-e2e.js +577 -0
- package/dist/scripts/issue343-real-entry-e2e.js +160 -0
- package/dist/scripts/journey-tests.js +22 -27
- package/dist/scripts/kernel-manifest-evidence.js +109 -0
- package/dist/scripts/kernel-manifest-gate.js +126 -0
- package/dist/scripts/kernel-manifest-tests.js +212 -0
- package/dist/scripts/kernel-manifest-trace-entry.js +7 -0
- package/dist/scripts/kernel-manifest-trace-hook.mjs +28 -0
- package/dist/scripts/kernel-manifest.js +369 -0
- package/dist/scripts/ledger-repair-apply-tests.js +316 -0
- package/dist/scripts/ledger-repair-plan-tests.js +375 -0
- package/dist/scripts/ledger-tests.js +325 -340
- package/dist/scripts/ledger-workflow-crash.js +10 -20
- package/dist/scripts/map-tools-vendor-package-proof.js +206 -134
- package/dist/scripts/memory-capture-tests.js +242 -61
- package/dist/scripts/memory-decay-tests.js +18 -52
- package/dist/scripts/memory-lifecycle-tests.js +657 -0
- package/dist/scripts/memory-lifecycle.js +196 -0
- package/dist/scripts/memory-metrics.js +24 -12
- package/dist/scripts/memory-value-report.js +474 -222
- package/dist/scripts/memory-value-tests.js +328 -0
- package/dist/scripts/metrics-report-tests.js +62 -199
- package/dist/scripts/model-override-e2e.js +66 -105
- package/dist/scripts/nightly-evidence-tests.js +64 -89
- package/dist/scripts/nightly-evidence.js +108 -111
- package/dist/scripts/nudge-digest.js +39 -28
- package/dist/scripts/onboarding-tests.js +63 -88
- package/dist/scripts/opensky-check.js +18 -13
- package/dist/scripts/opensky-tests.js +28 -28
- package/dist/scripts/persona-surface-guard-tests.js +37 -12
- package/dist/scripts/pnpm-dsh-closure-proof.js +2 -5
- package/dist/scripts/policy-anchor-359-registered-e2e.js +214 -0
- package/dist/scripts/price-drift-tests.js +126 -227
- package/dist/scripts/price-drift-watch.js +209 -253
- package/dist/scripts/probe-poi-tests.js +52 -45
- package/dist/scripts/product-metrics.js +187 -273
- package/dist/scripts/publish-preverify.js +47 -41
- package/dist/scripts/realtime-pricing-tests.js +37 -76
- package/dist/scripts/replay-async.js +20 -18
- package/dist/scripts/replay-real.js +16 -18
- package/dist/scripts/replay.js +52 -82
- package/dist/scripts/session-attach-diagnose.js +16 -12
- package/dist/scripts/session-attach-poc.js +34 -37
- package/dist/scripts/session-benchmark.js +66 -147
- package/dist/scripts/session-dida-live-e2e.js +151 -0
- package/dist/scripts/session-extract-tests.js +14 -35
- package/dist/scripts/session-login.js +14 -13
- package/dist/scripts/session-tests.js +545 -583
- package/dist/scripts/sf-live-benchmark.js +92 -110
- package/dist/scripts/sf-live-cli-tests.js +2 -9
- package/dist/scripts/sf-soft-score-tests.js +19 -60
- package/dist/scripts/sf-summary-tests.js +149 -0
- package/dist/scripts/sf-summary.js +466 -84
- package/dist/scripts/skeleton-check.js +16 -14
- package/dist/scripts/skeleton-integration-test.js +11 -10
- package/dist/scripts/skills-contract-tests.js +51 -46
- package/dist/scripts/smoke-session-gate-tests.js +12 -20
- package/dist/scripts/smoke.js +541 -422
- package/dist/scripts/sponsor-reuse-tests.js +262 -0
- package/dist/scripts/state-cli-tests.js +226 -101
- package/dist/scripts/state-cli.js +549 -212
- package/dist/scripts/static-golden-tests.js +46 -180
- package/dist/scripts/time-eval-tests.js +210 -192
- package/dist/scripts/travel-timeline-tests.js +29 -93
- package/dist/scripts/turn-handoff-collect-tests.js +46 -93
- package/dist/scripts/turn-handoff-collect.js +71 -99
- package/dist/scripts/turn-policy-tests.js +28 -29
- package/dist/scripts/typed-contract-canary.js +77 -162
- package/dist/scripts/tz-resolver-sanity.js +50 -0
- package/dist/scripts/tz-resolver-tests.js +584 -0
- package/dist/scripts/unified-tests.js +23 -19
- package/dist/scripts/visa-policy-tests.js +31 -44
- package/dist/scripts/weather-tests.js +121 -543
- package/dist/scripts/wish-channel-gate-tests.js +29 -86
- package/dist/scripts/write-gate-tests.js +522 -0
- package/dist/scripts/z3-lifecycle-fault-tests.js +188 -0
- package/dist/scripts/z3-lifecycle-tests.js +170 -0
- package/dist/scripts/z3-race-repeat-tests.js +39 -0
- package/dist/scripts/z3-race-tests.js +18 -12
- package/dist/src/artifact-gate.js +652 -221
- package/dist/src/backend/kernel.js +91 -0
- package/dist/src/backend/modules/booking-copilot.js +55 -0
- package/dist/src/backend/modules/booking-executor.js +124 -0
- package/dist/src/backend/modules/session-search.js +194 -0
- package/dist/src/benchmark-agent-conformance.js +296 -262
- package/dist/src/benchmark-environment-bridge.js +298 -413
- package/dist/src/benchmark-headless-child-diagnostics.js +93 -100
- package/dist/src/benchmark-tool-isolation.js +94 -67
- package/dist/src/bookable-facts.js +299 -174
- package/dist/src/booking-saga.js +64 -120
- package/dist/src/booking-surface/availability-policy.js +443 -576
- package/dist/src/booking-surface/cancel-refund-commission.js +379 -0
- package/dist/src/booking-surface/contracts.js +24 -80
- package/dist/src/booking-surface/dsh-planner.js +510 -281
- package/dist/src/booking-surface/dsh-plugin.js +17 -1
- package/dist/src/booking-surface/error-codes.js +82 -10
- package/dist/src/booking-surface/index.js +0 -3
- package/dist/src/booking-surface/profile.js +10 -54
- package/dist/src/booking-surface/recovery-chain.js +367 -0
- package/dist/src/booking-surface/runtime.js +765 -891
- package/dist/src/booking-surface/server.js +200 -251
- package/dist/src/booking-surface/startup.js +58 -54
- package/dist/src/booking-surface/validation.js +150 -218
- package/dist/src/bridge.js +20 -13
- package/dist/src/companions.js +48 -74
- package/dist/src/contracts.js +21 -43
- package/dist/src/dsh-llm.js +82 -120
- package/dist/src/engine.js +253 -239
- package/dist/src/evaluation-cadence.js +72 -209
- package/dist/src/evaluation-contracts.js +220 -843
- package/dist/src/flight-pack-adapter.js +54 -0
- package/dist/src/flight-pack-contract.js +2 -0
- package/dist/src/gotry-backend.js +70 -0
- package/dist/src/hotel-date-gate.js +118 -0
- package/dist/src/i18n.js +18 -7
- package/dist/src/index.js +1425 -1629
- package/dist/src/journey.js +132 -81
- package/dist/src/ledger-repair-apply.js +538 -0
- package/dist/src/ledger-repair-plan.js +463 -0
- package/dist/src/loop.js +257 -166
- package/dist/src/memory-capture.js +180 -32
- package/dist/src/memory-decay.js +30 -14
- package/dist/src/memory-lifecycle.js +1310 -0
- package/dist/src/memory-utility.js +27 -40
- package/dist/src/mock-llm.js +36 -61
- package/dist/src/model.js +81 -36
- package/dist/src/realtime-pricing.js +63 -72
- package/dist/src/slot-spec.js +84 -106
- package/dist/src/state-ledger.js +310 -471
- package/dist/src/subagent-job-id-guard.js +58 -0
- package/dist/src/time-anchor.js +231 -33
- package/dist/src/tool-packet.js +20 -8
- package/dist/src/travel-slots.js +58 -35
- package/dist/src/travel-timeline.js +44 -63
- package/dist/src/turn-deadline.js +121 -127
- package/dist/src/turn-policy.js +61 -89
- package/dist/src/tz-resolver.js +143 -0
- package/dist/src/unified.js +504 -381
- package/dist/src/wish-pool.js +39 -14
- package/dist/src/write-gate.js +495 -0
- package/dist/src/z3-shared.js +273 -7
- package/extension/README.md +4 -3
- package/extension/background.js +125 -14
- package/extension/content-bridge.js +23 -1
- package/extension/content-main.js +15 -4
- package/extension/manifest.json +16 -8
- package/package.json +251 -220
- package/ts/capabilities/agent-reach.ts +4 -4
- package/ts/capabilities/anything.ts +1 -1
- package/ts/capabilities/artifacts.ts +46 -9
- package/ts/capabilities/flyai.ts +156 -37
- package/ts/capabilities/hbcli.ts +1 -1
- package/ts/capabilities/incident-log.ts +88 -59
- package/ts/capabilities/session/adapters/ctrip-flight.ts +99 -36
- package/ts/capabilities/session/adapters/dida-portal.ts +250 -0
- package/ts/capabilities/session/adapters/rail-12306.ts +123 -24
- package/ts/capabilities/session/extension-bridge.ts +226 -101
- package/ts/capabilities/session/extension-channel.ts +22 -9
- package/ts/capabilities/session/wizard.ts +2 -2
- package/ts/capabilities/session-consent.ts +98 -14
- package/ts/capabilities/session-login.ts +1 -0
- package/ts/capabilities/session-search.ts +315 -31
- package/ts/dsh-runtime/vendor/README.md +8 -4
- package/ts/dsh-runtime/vendor/dsh-map-tools/README.en.md +7 -1
- package/ts/dsh-runtime/vendor/dsh-map-tools/README.md +5 -3
- package/ts/dsh-runtime/vendor/dsh-map-tools/lib/settings-ns.js +1 -1
- package/ts/dsh-runtime/vendor/dsh-map-tools/lib/types/settings-ns.d.ts +1 -1
- package/ts/dsh-runtime/vendor/dsh-map-tools/package.json +6 -6
- package/ts/package.json +236 -16
- package/ts/scripts/map-tools-vendor-package-proof.ts +152 -31
- package/ts/scripts/state-cli.ts +418 -51
- package/ts/src/artifact-gate.ts +561 -15
- package/ts/src/benchmark-environment-bridge.ts +37 -20
- package/ts/src/bookable-facts.ts +183 -7
- package/ts/src/booking-surface/cancel-refund-commission.ts +488 -0
- package/ts/src/booking-surface/dsh-planner.ts +240 -32
- package/ts/src/booking-surface/dsh-plugin.js +17 -1
- package/ts/src/booking-surface/error-codes.ts +75 -0
- package/ts/src/booking-surface/recovery-chain.ts +409 -0
- package/ts/src/booking-surface/server.ts +84 -51
- package/ts/src/booking-surface/validation.ts +2 -0
- package/ts/src/contracts.ts +12 -2
- package/ts/src/dsh-llm.ts +4 -8
- package/ts/src/index.ts +609 -90
- package/ts/src/loop.ts +116 -7
- package/ts/src/memory-capture.ts +227 -3
- package/ts/src/mock-llm.ts +4 -8
- package/ts/src/model.ts +88 -2
- package/ts/src/state-ledger.ts +47 -29
- package/ts/src/subagent-job-id-guard.ts +72 -0
- package/ts/src/time-anchor.ts +217 -13
- 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
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
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
|
|
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
|
-
|
|
90
|
-
|
|
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
|
-
|
|
130
|
-
|
|
131
|
-
|
|
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
|
-
|
|
164
|
-
}
|
|
165
|
-
}
|
|
167
|
+
request: { text: '执行当前搜索' },
|
|
168
|
+
},
|
|
166
169
|
});
|
|
167
|
-
assert.deepEqual(first, [
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
],
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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)
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
484
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
854
|
-
|
|
855
|
-
|
|
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
|
-
|
|
873
|
-
|
|
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
|
-
|
|
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
|
-
|
|
925
|
-
|
|
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
|
-
|
|
939
|
-
actionId: 'action-dsh-
|
|
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: '
|
|
956
|
-
|
|
957
|
-
|
|
958
|
-
|
|
959
|
-
|
|
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
|
|
971
|
-
|
|
972
|
-
|
|
973
|
-
|
|
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(
|
|
987
|
-
assert.
|
|
988
|
-
|
|
989
|
-
|
|
990
|
-
|
|
991
|
-
|
|
992
|
-
|
|
993
|
-
|
|
994
|
-
|
|
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
|
-
|
|
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
|
|
1018
|
-
|
|
1019
|
-
|
|
1020
|
-
|
|
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.
|
|
1026
|
-
|
|
1027
|
-
|
|
1028
|
-
|
|
1029
|
-
|
|
1030
|
-
|
|
1031
|
-
|
|
1032
|
-
|
|
1033
|
-
|
|
1034
|
-
|
|
1035
|
-
|
|
1036
|
-
|
|
1037
|
-
|
|
1038
|
-
|
|
1039
|
-
|
|
1040
|
-
|
|
1041
|
-
|
|
1042
|
-
|
|
1043
|
-
|
|
1044
|
-
|
|
1045
|
-
|
|
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
|
-
}
|
|
1060
|
-
|
|
1061
|
-
|
|
1062
|
-
|
|
1063
|
-
|
|
1064
|
-
|
|
1065
|
-
|
|
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
|