@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,9 +1,74 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* BFF-only HTTP/SSE seam for the embedded Booking Copilot.
|
|
3
|
+
*
|
|
4
|
+
* The browser never calls this server directly. A same-origin HotelByte BFF
|
|
5
|
+
* authenticates the actor, mints contextRef and sends a strict planner turn
|
|
6
|
+
* with a deployment-level Bearer key. No portal token field exists here.
|
|
7
|
+
*/
|
|
1
8
|
import { timingSafeEqual } from 'node:crypto';
|
|
2
9
|
import { createServer } from 'node:http';
|
|
3
|
-
import { BOOKING_COPILOT_ACCEPTED_TURN_KINDS, BOOKING_COPILOT_INGRESS_MODES, BOOKING_READ_ACTION_KINDS, BOOKING_SURFACE_ALLOWED_ACTIONS, BOOKING_SURFACE_SCHEMA_SHA256, BOOKING_SURFACE_SCHEMA_SHA256_HEADER, BOOKING_SURFACE_SCHEMA_VERSION, BOOKING_SURFACE_VERSION_HEADER } from './contracts.js';
|
|
4
|
-
import { bookingDigest,
|
|
10
|
+
import { BOOKING_COPILOT_ACCEPTED_TURN_KINDS, BOOKING_COPILOT_INGRESS_MODES, BOOKING_READ_ACTION_KINDS, BOOKING_SURFACE_ALLOWED_ACTIONS, BOOKING_SURFACE_SCHEMA_SHA256, BOOKING_SURFACE_SCHEMA_SHA256_HEADER, BOOKING_SURFACE_SCHEMA_VERSION, BOOKING_SURFACE_VERSION_HEADER, } from './contracts.js';
|
|
11
|
+
import { bookingDigest, } from './runtime.js';
|
|
5
12
|
import { validateBookingSurface } from './validation.js';
|
|
6
|
-
import { normalizeBookingErrorCode, safeBookingErrorMessage } from './error-codes.js';
|
|
13
|
+
import { extractBookingDispatchReason, normalizeBookingErrorCode, safeBookingErrorMessage } from './error-codes.js';
|
|
14
|
+
/**
|
|
15
|
+
* 单请求处理闭包(startBookingCopilotServer 的 createServer 回调原样摘出):
|
|
16
|
+
* 探活(/healthz /status)+ 唯一 turn 路由 + 鉴权 + schema 头校验。
|
|
17
|
+
* gotry-backend 的 booking-copilot 模块复用同一闭包,保证两条挂载路径行为逐字一致。
|
|
18
|
+
*/
|
|
19
|
+
export function bookingCopilotTrafficHandler(deps) {
|
|
20
|
+
const { apiKey, composition, maxBodyBytes, artifactId, ingressMode, runningIdentity } = deps;
|
|
21
|
+
return async (req, res) => {
|
|
22
|
+
const isProbe = req.method === 'GET' && (req.url === '/healthz' || req.url === '/status');
|
|
23
|
+
if (!isProbe && (req.method !== 'POST' || req.url !== '/a2a/booking-copilot/turn')) {
|
|
24
|
+
sendJson(res, 404, { error: { code: 'not_found' } });
|
|
25
|
+
return;
|
|
26
|
+
}
|
|
27
|
+
const auth = String(req.headers.authorization ?? '');
|
|
28
|
+
if (!safeSecretEqual(auth, `Bearer ${apiKey}`)) {
|
|
29
|
+
sendJson(res, 401, { error: { code: 'unauthorized' } });
|
|
30
|
+
return;
|
|
31
|
+
}
|
|
32
|
+
if (isProbe) {
|
|
33
|
+
res.setHeader(BOOKING_SURFACE_VERSION_HEADER, BOOKING_SURFACE_SCHEMA_VERSION);
|
|
34
|
+
res.setHeader(BOOKING_SURFACE_SCHEMA_SHA256_HEADER, BOOKING_SURFACE_SCHEMA_SHA256);
|
|
35
|
+
if (artifactId)
|
|
36
|
+
res.setHeader('X-GoTry-Artifact-ID', artifactId);
|
|
37
|
+
res.setHeader('X-GoTry-Node-Version', runningIdentity.nodeVersion);
|
|
38
|
+
res.setHeader('X-GoTry-Node-Modules-ABI', runningIdentity.nodeModulesAbi);
|
|
39
|
+
res.setHeader('X-GoTry-Release-Tuple', runningIdentity.releaseTuple);
|
|
40
|
+
if (runningIdentity.glibcVersion)
|
|
41
|
+
res.setHeader('X-GoTry-Glibc-Version', runningIdentity.glibcVersion);
|
|
42
|
+
const acceptedTurnKinds = ingressMode === 'bff-ingress-binding'
|
|
43
|
+
? [...BOOKING_COPILOT_ACCEPTED_TURN_KINDS, 'user.turn.ingress']
|
|
44
|
+
: [...BOOKING_COPILOT_ACCEPTED_TURN_KINDS];
|
|
45
|
+
const healthBody = {
|
|
46
|
+
schemaVersion: BOOKING_SURFACE_SCHEMA_VERSION,
|
|
47
|
+
schemaSha256: BOOKING_SURFACE_SCHEMA_SHA256,
|
|
48
|
+
status: 'ready',
|
|
49
|
+
ingressMode,
|
|
50
|
+
acceptedTurnKinds,
|
|
51
|
+
};
|
|
52
|
+
res.setHeader('X-GoTry-Ingress-Mode', ingressMode);
|
|
53
|
+
res.setHeader('X-GoTry-Accepted-Turn-Kinds', acceptedTurnKinds.join(','));
|
|
54
|
+
sendJson(res, 200, healthBody);
|
|
55
|
+
return;
|
|
56
|
+
}
|
|
57
|
+
const schemaVersion = String(req.headers[BOOKING_SURFACE_VERSION_HEADER] ?? '');
|
|
58
|
+
const schemaHash = String(req.headers[BOOKING_SURFACE_SCHEMA_SHA256_HEADER] ?? '');
|
|
59
|
+
if (schemaVersion !== BOOKING_SURFACE_SCHEMA_VERSION || schemaHash !== BOOKING_SURFACE_SCHEMA_SHA256) {
|
|
60
|
+
sendJson(res, 409, {
|
|
61
|
+
error: {
|
|
62
|
+
code: 'booking_surface_schema_mismatch',
|
|
63
|
+
expectedVersion: BOOKING_SURFACE_SCHEMA_VERSION,
|
|
64
|
+
expectedSchemaSha256: BOOKING_SURFACE_SCHEMA_SHA256,
|
|
65
|
+
},
|
|
66
|
+
});
|
|
67
|
+
return;
|
|
68
|
+
}
|
|
69
|
+
await handleBookingCopilotRequest(req, res, composition, maxBodyBytes);
|
|
70
|
+
};
|
|
71
|
+
}
|
|
7
72
|
const sessionsByComposition = new WeakMap();
|
|
8
73
|
const decisionFlightsByComposition = new WeakMap();
|
|
9
74
|
function sessionsFor(composition) {
|
|
@@ -25,16 +90,19 @@ function decisionFlightsFor(composition) {
|
|
|
25
90
|
async function runDecisionSingleFlight(composition, key, work) {
|
|
26
91
|
const flights = decisionFlightsFor(composition);
|
|
27
92
|
const prior = flights.get(key);
|
|
28
|
-
if (prior)
|
|
93
|
+
if (prior)
|
|
94
|
+
return prior;
|
|
29
95
|
const current = work();
|
|
30
96
|
flights.set(key, current);
|
|
31
97
|
try {
|
|
32
98
|
return await current;
|
|
33
|
-
}
|
|
34
|
-
|
|
99
|
+
}
|
|
100
|
+
finally {
|
|
101
|
+
if (flights.get(key) === current)
|
|
102
|
+
flights.delete(key);
|
|
35
103
|
}
|
|
36
104
|
}
|
|
37
|
-
function runtimeIdentity() {
|
|
105
|
+
export function runtimeIdentity() {
|
|
38
106
|
const report = process.report?.getReport?.();
|
|
39
107
|
const header = report?.header;
|
|
40
108
|
const glibcVersion = header?.glibcVersionRuntime ?? '';
|
|
@@ -42,7 +110,7 @@ function runtimeIdentity() {
|
|
|
42
110
|
nodeVersion: process.version,
|
|
43
111
|
nodeModulesAbi: process.versions.modules ?? '',
|
|
44
112
|
releaseTuple: `${process.platform}-${process.arch}-${glibcVersion ? 'glibc' : 'unknown'}`,
|
|
45
|
-
glibcVersion
|
|
113
|
+
glibcVersion,
|
|
46
114
|
};
|
|
47
115
|
}
|
|
48
116
|
function safeSecretEqual(actual, expected) {
|
|
@@ -51,17 +119,15 @@ function safeSecretEqual(actual, expected) {
|
|
|
51
119
|
return actualBytes.length === expectedBytes.length && timingSafeEqual(actualBytes, expectedBytes);
|
|
52
120
|
}
|
|
53
121
|
function sendJson(res, status, body) {
|
|
54
|
-
res.writeHead(status, {
|
|
55
|
-
'content-type': 'application/json; charset=utf-8'
|
|
56
|
-
});
|
|
122
|
+
res.writeHead(status, { 'content-type': 'application/json; charset=utf-8' });
|
|
57
123
|
res.end(JSON.stringify(body));
|
|
58
124
|
}
|
|
59
125
|
function readBody(req, maxBodyBytes) {
|
|
60
|
-
return new Promise((resolve, reject)=>{
|
|
126
|
+
return new Promise((resolve, reject) => {
|
|
61
127
|
const chunks = [];
|
|
62
128
|
let size = 0;
|
|
63
129
|
let exceeded = false;
|
|
64
|
-
req.on('data', (chunk)=>{
|
|
130
|
+
req.on('data', (chunk) => {
|
|
65
131
|
size += chunk.length;
|
|
66
132
|
if (size > maxBodyBytes) {
|
|
67
133
|
exceeded = true;
|
|
@@ -69,7 +135,7 @@ function readBody(req, maxBodyBytes) {
|
|
|
69
135
|
}
|
|
70
136
|
chunks.push(chunk);
|
|
71
137
|
});
|
|
72
|
-
req.on('end', ()=>exceeded ? reject(new Error('payload_too_large')) : resolve(Buffer.concat(chunks).toString('utf8')));
|
|
138
|
+
req.on('end', () => exceeded ? reject(new Error('payload_too_large')) : resolve(Buffer.concat(chunks).toString('utf8')));
|
|
73
139
|
req.on('error', reject);
|
|
74
140
|
});
|
|
75
141
|
}
|
|
@@ -77,42 +143,43 @@ function write(res, event) {
|
|
|
77
143
|
res.write(`event: ${event.kind}\ndata: ${JSON.stringify(event)}\n\n`);
|
|
78
144
|
}
|
|
79
145
|
function sameCapabilities(a, b) {
|
|
80
|
-
return a.length === b.length && [
|
|
81
|
-
...a
|
|
82
|
-
].sort().every((kind, index)=>kind === [
|
|
83
|
-
...b
|
|
84
|
-
].sort()[index]);
|
|
146
|
+
return a.length === b.length && [...a].sort().every((kind, index) => kind === [...b].sort()[index]);
|
|
85
147
|
}
|
|
86
148
|
function exactKeys(value, allowed) {
|
|
87
|
-
return Object.keys(value).length === allowed.length && Object.keys(value).every((key)=>allowed.includes(key));
|
|
149
|
+
return Object.keys(value).length === allowed.length && Object.keys(value).every((key) => allowed.includes(key));
|
|
88
150
|
}
|
|
89
151
|
const SURFACE_ACTION_MATRIX = BOOKING_SURFACE_ALLOWED_ACTIONS;
|
|
90
152
|
function validSurfaceActions(surface, allowed) {
|
|
91
|
-
if (typeof surface !== 'string' || !Object.prototype.hasOwnProperty.call(SURFACE_ACTION_MATRIX, surface))
|
|
92
|
-
|
|
153
|
+
if (typeof surface !== 'string' || !Object.prototype.hasOwnProperty.call(SURFACE_ACTION_MATRIX, surface))
|
|
154
|
+
return false;
|
|
155
|
+
if (!Array.isArray(allowed) || allowed.length < 1 || new Set(allowed).size !== allowed.length)
|
|
156
|
+
return false;
|
|
93
157
|
const matrix = SURFACE_ACTION_MATRIX[surface];
|
|
94
|
-
return allowed.every((action)=>typeof action === 'string'
|
|
158
|
+
return allowed.every((action) => typeof action === 'string'
|
|
159
|
+
&& matrix.includes(action)
|
|
160
|
+
&& BOOKING_READ_ACTION_KINDS.includes(action));
|
|
95
161
|
}
|
|
96
162
|
function validBoundWorkspaceAuthority(workspace) {
|
|
97
|
-
return workspace.capabilities.surface === workspace.surface
|
|
163
|
+
return workspace.capabilities.surface === workspace.surface
|
|
164
|
+
&& validSurfaceActions(workspace.surface, workspace.capabilities.allowedActions);
|
|
98
165
|
}
|
|
99
166
|
function validIngressBinding(binding, ingress) {
|
|
100
|
-
if (!binding || typeof binding !== 'object' || Array.isArray(binding))
|
|
167
|
+
if (!binding || typeof binding !== 'object' || Array.isArray(binding))
|
|
168
|
+
return false;
|
|
101
169
|
const candidate = binding;
|
|
102
|
-
if (!exactKeys(candidate, [
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
])) return false;
|
|
109
|
-
if (typeof candidate.taskId !== 'string' || typeof candidate.turnId !== 'string' || typeof candidate.contextRef !== 'string') return false;
|
|
110
|
-
if (typeof candidate.surface !== 'string' || candidate.surface !== ingress.surfaceHint || !Object.prototype.hasOwnProperty.call(SURFACE_ACTION_MATRIX, candidate.surface)) return false;
|
|
170
|
+
if (!exactKeys(candidate, ['taskId', 'turnId', 'contextRef', 'surface', 'allowedActions']))
|
|
171
|
+
return false;
|
|
172
|
+
if (typeof candidate.taskId !== 'string' || typeof candidate.turnId !== 'string' || typeof candidate.contextRef !== 'string')
|
|
173
|
+
return false;
|
|
174
|
+
if (typeof candidate.surface !== 'string' || candidate.surface !== ingress.surfaceHint || !Object.prototype.hasOwnProperty.call(SURFACE_ACTION_MATRIX, candidate.surface))
|
|
175
|
+
return false;
|
|
111
176
|
return validSurfaceActions(candidate.surface, candidate.allowedActions);
|
|
112
177
|
}
|
|
113
178
|
function validIngressApprovalAuthority(ingress, binding) {
|
|
114
|
-
if (!ingress.request.approval)
|
|
115
|
-
|
|
179
|
+
if (!ingress.request.approval)
|
|
180
|
+
return true;
|
|
181
|
+
return ingress.request.approval.taskId === binding.taskId
|
|
182
|
+
&& ingress.request.approval.contextRef === binding.contextRef;
|
|
116
183
|
}
|
|
117
184
|
function fallbackErrorEvent(task, code) {
|
|
118
185
|
return {
|
|
@@ -123,37 +190,31 @@ function fallbackErrorEvent(task, code) {
|
|
|
123
190
|
sequence: task.lastSequence + 1,
|
|
124
191
|
emittedAt: new Date().toISOString(),
|
|
125
192
|
kind: 'error',
|
|
126
|
-
error: {
|
|
127
|
-
code: normalizeBookingErrorCode(code),
|
|
128
|
-
message: safeBookingErrorMessage(normalizeBookingErrorCode(code)),
|
|
129
|
-
retryable: false
|
|
130
|
-
}
|
|
193
|
+
error: { code: normalizeBookingErrorCode(code), message: safeBookingErrorMessage(normalizeBookingErrorCode(code)), retryable: false },
|
|
131
194
|
};
|
|
132
195
|
}
|
|
133
196
|
function writeTypedError(res, composition, task, error, requestKey, includeSubmitted = false) {
|
|
134
197
|
const code = normalizeBookingErrorCode(error);
|
|
198
|
+
// Operator-side diagnosability only: the raw boundary error stays on the
|
|
199
|
+
// server stderr/journal; clients keep receiving the safe typed vocabulary.
|
|
135
200
|
console.error(`[booking-copilot] planner boundary error (${code}):`, error instanceof Error ? error.stack || error.message : error);
|
|
136
201
|
try {
|
|
137
|
-
const decision = {
|
|
138
|
-
kind: 'error',
|
|
139
|
-
error: {
|
|
140
|
-
code,
|
|
141
|
-
message: safeBookingErrorMessage(code),
|
|
142
|
-
retryable: false
|
|
143
|
-
}
|
|
144
|
-
};
|
|
202
|
+
const decision = { kind: 'error', error: { code, message: safeBookingErrorMessage(code), retryable: false } };
|
|
145
203
|
if (requestKey) {
|
|
146
|
-
const events = composition.runtime.applyDecisionBatch(task.taskId, requestKey, [
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
for (const event of events)write(res, event);
|
|
204
|
+
const events = composition.runtime.applyDecisionBatch(task.taskId, requestKey, [decision], includeSubmitted);
|
|
205
|
+
for (const event of events)
|
|
206
|
+
write(res, event);
|
|
150
207
|
return;
|
|
151
208
|
}
|
|
152
209
|
write(res, composition.runtime.emitEvent(task.taskId, decision));
|
|
153
|
-
}
|
|
210
|
+
}
|
|
211
|
+
catch {
|
|
212
|
+
// The stream is already committed. Emit a schema-valid, non-durable
|
|
213
|
+
// typed envelope instead of silently returning an empty HTTP 200 stream.
|
|
154
214
|
write(res, fallbackErrorEvent(task, code));
|
|
155
215
|
}
|
|
156
216
|
}
|
|
217
|
+
/** Handles one already-authenticated request inside the sole HTTP listener. */
|
|
157
218
|
async function handleBookingCopilotRequest(req, res, composition, maxBodyBytes) {
|
|
158
219
|
let turn;
|
|
159
220
|
let browserRequestKey;
|
|
@@ -162,108 +223,53 @@ async function handleBookingCopilotRequest(req, res, composition, maxBodyBytes)
|
|
|
162
223
|
const parsed = JSON.parse(await readBody(req, maxBodyBytes));
|
|
163
224
|
const valid = validateBookingSurface(parsed);
|
|
164
225
|
if (!valid.ok) {
|
|
165
|
-
sendJson(res, 400, {
|
|
166
|
-
error: {
|
|
167
|
-
code: 'invalid_booking_surface_turn',
|
|
168
|
-
details: valid.errors
|
|
169
|
-
}
|
|
170
|
-
});
|
|
226
|
+
sendJson(res, 400, { error: { code: 'invalid_booking_surface_turn', details: valid.errors } });
|
|
171
227
|
return;
|
|
172
228
|
}
|
|
173
229
|
if (parsed.kind === 'user.turn.ingress') {
|
|
174
230
|
const ingress = parsed;
|
|
175
231
|
browserRequestKey = ingress.requestKey;
|
|
176
232
|
if (composition.ingressMode !== 'bff-ingress-binding' || !composition.ingressBinding || !composition.principal) {
|
|
177
|
-
sendJson(res, 503, {
|
|
178
|
-
error: {
|
|
179
|
-
code: 'trusted_ingress_binding_required',
|
|
180
|
-
mode: composition.ingressMode ?? 'bff-bound-turn-only',
|
|
181
|
-
acceptedTurnKinds: [
|
|
182
|
-
...BOOKING_COPILOT_ACCEPTED_TURN_KINDS
|
|
183
|
-
]
|
|
184
|
-
}
|
|
185
|
-
});
|
|
233
|
+
sendJson(res, 503, { error: { code: 'trusted_ingress_binding_required', mode: composition.ingressMode ?? 'bff-bound-turn-only', acceptedTurnKinds: [...BOOKING_COPILOT_ACCEPTED_TURN_KINDS] } });
|
|
186
234
|
return;
|
|
187
235
|
}
|
|
188
236
|
const binding = await composition.ingressBinding.bind(ingress, composition.principal);
|
|
189
237
|
if (!validIngressBinding(binding, ingress)) {
|
|
190
|
-
sendJson(res, 502, {
|
|
191
|
-
error: {
|
|
192
|
-
code: 'invalid_ingress_binding'
|
|
193
|
-
}
|
|
194
|
-
});
|
|
238
|
+
sendJson(res, 502, { error: { code: 'invalid_ingress_binding' } });
|
|
195
239
|
return;
|
|
196
240
|
}
|
|
197
241
|
if (!validIngressApprovalAuthority(ingress, binding)) {
|
|
198
|
-
sendJson(res, 400, {
|
|
199
|
-
error: {
|
|
200
|
-
code: 'invalid_ingress_approval_authority'
|
|
201
|
-
}
|
|
202
|
-
});
|
|
242
|
+
sendJson(res, 400, { error: { code: 'invalid_ingress_approval_authority' } });
|
|
203
243
|
return;
|
|
204
244
|
}
|
|
205
245
|
const boundWorkspace = {
|
|
206
246
|
...ingress.workspace,
|
|
207
247
|
contextRef: binding.contextRef,
|
|
208
248
|
surface: binding.surface,
|
|
209
|
-
capabilities: {
|
|
210
|
-
surface: binding.surface,
|
|
211
|
-
allowedActions: [
|
|
212
|
-
...binding.allowedActions
|
|
213
|
-
]
|
|
214
|
-
}
|
|
215
|
-
};
|
|
216
|
-
const boundRequest = ingress.request.approval ? {
|
|
217
|
-
text: ingress.request.text,
|
|
218
|
-
approval: {
|
|
219
|
-
...ingress.request.approval
|
|
220
|
-
}
|
|
221
|
-
} : {
|
|
222
|
-
text: ingress.request.text
|
|
223
|
-
};
|
|
224
|
-
turn = {
|
|
225
|
-
schemaVersion: BOOKING_SURFACE_SCHEMA_VERSION,
|
|
226
|
-
kind: 'user.turn',
|
|
227
|
-
taskId: binding.taskId,
|
|
228
|
-
turnId: binding.turnId,
|
|
229
|
-
workspace: boundWorkspace,
|
|
230
|
-
request: boundRequest
|
|
231
|
-
};
|
|
232
|
-
requestBinding = {
|
|
233
|
-
requestKey: ingress.requestKey,
|
|
234
|
-
principal: composition.principal,
|
|
235
|
-
...ingress.taskHandle ? {
|
|
236
|
-
taskHandle: ingress.taskHandle
|
|
237
|
-
} : {}
|
|
249
|
+
capabilities: { surface: binding.surface, allowedActions: [...binding.allowedActions] },
|
|
238
250
|
};
|
|
251
|
+
const boundRequest = ingress.request.approval
|
|
252
|
+
? { text: ingress.request.text, approval: { ...ingress.request.approval } }
|
|
253
|
+
: { text: ingress.request.text };
|
|
254
|
+
turn = { schemaVersion: BOOKING_SURFACE_SCHEMA_VERSION, kind: 'user.turn', taskId: binding.taskId, turnId: binding.turnId, workspace: boundWorkspace, request: boundRequest };
|
|
255
|
+
requestBinding = { requestKey: ingress.requestKey, principal: composition.principal, ...(ingress.taskHandle ? { taskHandle: ingress.taskHandle } : {}) };
|
|
239
256
|
const boundValid = validateBookingSurface(turn);
|
|
240
257
|
if (!boundValid.ok) {
|
|
241
|
-
sendJson(res, 502, {
|
|
242
|
-
error: {
|
|
243
|
-
code: 'invalid_ingress_binding',
|
|
244
|
-
details: boundValid.errors
|
|
245
|
-
}
|
|
246
|
-
});
|
|
258
|
+
sendJson(res, 502, { error: { code: 'invalid_ingress_binding', details: boundValid.errors } });
|
|
247
259
|
return;
|
|
248
260
|
}
|
|
249
|
-
}
|
|
261
|
+
}
|
|
262
|
+
else {
|
|
250
263
|
turn = parsed;
|
|
251
264
|
if (!validBoundWorkspaceAuthority(turn.workspace)) {
|
|
252
|
-
sendJson(res, 403, {
|
|
253
|
-
error: {
|
|
254
|
-
code: 'invalid_bound_turn_authority'
|
|
255
|
-
}
|
|
256
|
-
});
|
|
265
|
+
sendJson(res, 403, { error: { code: 'invalid_bound_turn_authority' } });
|
|
257
266
|
return;
|
|
258
267
|
}
|
|
259
268
|
}
|
|
260
|
-
}
|
|
269
|
+
}
|
|
270
|
+
catch (error) {
|
|
261
271
|
const code = normalizeBookingErrorCode(error);
|
|
262
|
-
sendJson(res, code === 'PAYLOAD_TOO_LARGE' ? 413 : 400, {
|
|
263
|
-
error: {
|
|
264
|
-
code
|
|
265
|
-
}
|
|
266
|
-
});
|
|
272
|
+
sendJson(res, code === 'PAYLOAD_TOO_LARGE' ? 413 : 400, { error: { code } });
|
|
267
273
|
return;
|
|
268
274
|
}
|
|
269
275
|
let task;
|
|
@@ -280,32 +286,47 @@ async function handleBookingCopilotRequest(req, res, composition, maxBodyBytes)
|
|
|
280
286
|
replayKey = composition.runtime.approvalPresentationRequestKey(turn.taskId);
|
|
281
287
|
}
|
|
282
288
|
replayEvents = composition.runtime.readDecisionBatch(turn.taskId, replayKey);
|
|
283
|
-
if (!replayEvents && task.phase === 'terminal')
|
|
284
|
-
|
|
289
|
+
if (!replayEvents && task.phase === 'terminal')
|
|
290
|
+
replayEvents = composition.runtime.terminalDecisionBatch(turn.taskId, replayKey);
|
|
291
|
+
}
|
|
292
|
+
else {
|
|
285
293
|
const existing = turn.taskId ? composition.runtime.resumeTask(turn.taskId) : null;
|
|
286
|
-
if (requestBinding && turn.kind === 'user.turn')
|
|
294
|
+
if (requestBinding && turn.kind === 'user.turn')
|
|
295
|
+
composition.runtime.assertRequestBinding(browserRequestKey, turn, requestBinding);
|
|
287
296
|
const requestReplayKey = existing && browserRequestKey ? `ingress:${existing.taskId}:${browserRequestKey}` : undefined;
|
|
288
297
|
const directReplayKey = existing && turn.kind === 'user.turn' && turn.turnId ? `turn:${existing.taskId}:${turn.turnId}` : undefined;
|
|
289
298
|
const stableReplayKey = requestReplayKey ?? directReplayKey;
|
|
290
299
|
const stableReplay = stableReplayKey ? composition.runtime.readDecisionBatch(existing.taskId, stableReplayKey) : null;
|
|
291
300
|
if (stableReplay) {
|
|
292
|
-
if (existing && turn.kind === 'user.turn' && directReplayKey)
|
|
301
|
+
if (existing && turn.kind === 'user.turn' && directReplayKey)
|
|
302
|
+
composition.runtime.assertTurnBinding(existing.taskId, turn);
|
|
293
303
|
task = existing;
|
|
294
304
|
replayKey = stableReplayKey;
|
|
295
305
|
replayEvents = stableReplay;
|
|
296
|
-
}
|
|
306
|
+
}
|
|
307
|
+
else if (existing?.phase === 'terminal' || existing?.phase === 'error') {
|
|
297
308
|
throw new Error('task_terminal');
|
|
298
|
-
}
|
|
299
|
-
|
|
309
|
+
}
|
|
310
|
+
else if (existing?.phase === 'waiting_receipt') {
|
|
311
|
+
if (turn.kind === 'user.turn' && turn.request.approval)
|
|
312
|
+
composition.runtime.assertApprovalNotConsumed(existing.taskId, turn.request.approval);
|
|
300
313
|
const workspace = turn.workspace;
|
|
301
|
-
if (workspace.contextRef !== existing.contextRef || workspace.surface !== existing.surface || workspace.revision !== existing.revision || workspace.capabilities.surface !== existing.surface || !sameCapabilities(workspace.capabilities.allowedActions, existing.allowedActions))
|
|
302
|
-
|
|
314
|
+
if (workspace.contextRef !== existing.contextRef || workspace.surface !== existing.surface || workspace.revision !== existing.revision || workspace.capabilities.surface !== existing.surface || !sameCapabilities(workspace.capabilities.allowedActions, existing.allowedActions))
|
|
315
|
+
throw new Error('task_conflict:workspace_mismatch');
|
|
316
|
+
// A waiting task can only be replayed with the caller's stable turn
|
|
317
|
+
// identity. Without it, an identical sentence is not distinguishable
|
|
318
|
+
// from a new user turn and must not receive an old operation batch.
|
|
319
|
+
if (!turn.turnId || existing.lastTurnId !== turn.turnId)
|
|
320
|
+
throw new Error('receipt_required');
|
|
303
321
|
replayKey = browserRequestKey ? `ingress:${existing.taskId}:${browserRequestKey}` : `turn:${existing.taskId}:${existing.lastTurnId ?? existing.userTurnCount}`;
|
|
304
322
|
replayEvents = composition.runtime.readDecisionBatch(existing.taskId, replayKey);
|
|
305
|
-
if (!replayEvents)
|
|
323
|
+
if (!replayEvents)
|
|
324
|
+
throw new Error('receipt_required');
|
|
306
325
|
task = existing;
|
|
307
|
-
}
|
|
308
|
-
|
|
326
|
+
}
|
|
327
|
+
else {
|
|
328
|
+
if (existing && turn.kind === 'user.turn' && turn.request.approval)
|
|
329
|
+
composition.runtime.assertApprovalNotConsumed(existing.taskId, turn.request.approval);
|
|
309
330
|
task = composition.runtime.startTask(turn, requestBinding);
|
|
310
331
|
if (turn.kind === 'user.turn' && turn.taskId) {
|
|
311
332
|
replayKey = browserRequestKey ? `ingress:${task.taskId}:${browserRequestKey}` : `turn:${task.taskId}:${turn.turnId ?? task.userTurnCount}`;
|
|
@@ -313,68 +334,49 @@ async function handleBookingCopilotRequest(req, res, composition, maxBodyBytes)
|
|
|
313
334
|
}
|
|
314
335
|
}
|
|
315
336
|
}
|
|
316
|
-
}
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
});
|
|
337
|
+
}
|
|
338
|
+
catch (error) {
|
|
339
|
+
const code = normalizeBookingErrorCode(error);
|
|
340
|
+
console.error(JSON.stringify({ code, reason: extractBookingDispatchReason(error) }));
|
|
341
|
+
sendJson(res, 409, { error: { code } });
|
|
322
342
|
return;
|
|
323
343
|
}
|
|
324
|
-
res.writeHead(200, {
|
|
325
|
-
'content-type': 'text/event-stream; charset=utf-8',
|
|
326
|
-
'cache-control': 'no-cache, no-transform',
|
|
327
|
-
connection: 'keep-alive',
|
|
328
|
-
'x-content-type-options': 'nosniff',
|
|
329
|
-
[BOOKING_SURFACE_VERSION_HEADER]: BOOKING_SURFACE_SCHEMA_VERSION,
|
|
330
|
-
[BOOKING_SURFACE_SCHEMA_SHA256_HEADER]: BOOKING_SURFACE_SCHEMA_SHA256
|
|
331
|
-
});
|
|
344
|
+
res.writeHead(200, { 'content-type': 'text/event-stream; charset=utf-8', 'cache-control': 'no-cache, no-transform', connection: 'keep-alive', 'x-content-type-options': 'nosniff', [BOOKING_SURFACE_VERSION_HEADER]: BOOKING_SURFACE_SCHEMA_VERSION, [BOOKING_SURFACE_SCHEMA_SHA256_HEADER]: BOOKING_SURFACE_SCHEMA_SHA256 });
|
|
332
345
|
try {
|
|
333
346
|
if (replayEvents) {
|
|
334
|
-
for (const event of replayEvents)
|
|
335
|
-
|
|
347
|
+
for (const event of replayEvents)
|
|
348
|
+
write(res, event);
|
|
349
|
+
}
|
|
350
|
+
else {
|
|
336
351
|
const requestKey = replayKey ?? `turn:${task.taskId}:${task.userTurnCount}`;
|
|
337
352
|
activeDecisionKey = requestKey;
|
|
338
353
|
const approval = turn.kind === 'user.turn' ? turn.request.approval : undefined;
|
|
339
|
-
const flightKey = JSON.stringify([
|
|
340
|
-
|
|
341
|
-
approval ? `approval:${bookingDigest(approval)}` : requestKey
|
|
342
|
-
]);
|
|
343
|
-
const decisionEvents = await runDecisionSingleFlight(composition, flightKey, async ()=>{
|
|
354
|
+
const flightKey = JSON.stringify([task.taskId, approval ? `approval:${bookingDigest(approval)}` : requestKey]);
|
|
355
|
+
const decisionEvents = await runDecisionSingleFlight(composition, flightKey, async () => {
|
|
344
356
|
if (turn.kind === 'action.receipt.continuation' && task.awaitingApproval) {
|
|
345
|
-
return composition.runtime.applyDecisionBatch(task.taskId, requestKey, [
|
|
346
|
-
composition.runtime.approvalQuestion(task.taskId)
|
|
347
|
-
], false);
|
|
357
|
+
return composition.runtime.applyDecisionBatch(task.taskId, requestKey, [composition.runtime.approvalQuestion(task.taskId)], false);
|
|
348
358
|
}
|
|
349
359
|
const sessions = sessionsFor(composition);
|
|
350
360
|
const session = sessions.get(task.taskId) ?? composition.plannerFactory(task);
|
|
351
361
|
sessions.set(task.taskId, session);
|
|
352
|
-
const decisions = await session.next({
|
|
353
|
-
|
|
354
|
-
task: composition.runtime.resumeTask(task.taskId) ?? task
|
|
355
|
-
});
|
|
356
|
-
const plannerDecisions = decisions.length ? decisions : [
|
|
357
|
-
{
|
|
358
|
-
kind: 'error',
|
|
359
|
-
error: {
|
|
360
|
-
code: 'PLANNER_NO_DECISION',
|
|
361
|
-
message: 'Planner returned no typed decision.',
|
|
362
|
-
retryable: false
|
|
363
|
-
}
|
|
364
|
-
}
|
|
365
|
-
];
|
|
362
|
+
const decisions = await session.next({ turn, task: composition.runtime.resumeTask(task.taskId) ?? task });
|
|
363
|
+
const plannerDecisions = decisions.length ? decisions : [{ kind: 'error', error: { code: 'PLANNER_NO_DECISION', message: 'Planner returned no typed decision.', retryable: false } }];
|
|
366
364
|
return composition.runtime.applyDecisionBatch(task.taskId, requestKey, plannerDecisions, fresh);
|
|
367
365
|
});
|
|
368
|
-
for (const event of decisionEvents)
|
|
366
|
+
for (const event of decisionEvents)
|
|
367
|
+
write(res, event);
|
|
369
368
|
}
|
|
370
|
-
}
|
|
369
|
+
}
|
|
370
|
+
catch (error) {
|
|
371
371
|
writeTypedError(res, composition, task, error, activeDecisionKey, fresh);
|
|
372
|
-
}
|
|
372
|
+
}
|
|
373
|
+
finally {
|
|
373
374
|
res.end();
|
|
374
375
|
}
|
|
375
376
|
}
|
|
376
377
|
export function startBookingCopilotServer(options) {
|
|
377
|
-
if (!options.apiKey)
|
|
378
|
+
if (!options.apiKey)
|
|
379
|
+
return Promise.reject(new Error('booking_copilot_api_key_required'));
|
|
378
380
|
if (options.artifactId !== undefined && !/^[0-9a-f]{40}$/.test(options.artifactId)) {
|
|
379
381
|
return Promise.reject(new Error('booking_copilot_artifact_id_invalid'));
|
|
380
382
|
}
|
|
@@ -386,7 +388,9 @@ export function startBookingCopilotServer(options) {
|
|
|
386
388
|
if (hasIngressBinding !== hasPrincipal) {
|
|
387
389
|
return Promise.reject(new Error('booking_copilot_ingress_binding_pair_required'));
|
|
388
390
|
}
|
|
389
|
-
if (hasIngressBinding && (typeof options.ingressBinding?.bind !== 'function'
|
|
391
|
+
if (hasIngressBinding && (typeof options.ingressBinding?.bind !== 'function'
|
|
392
|
+
|| typeof options.principal?.subject !== 'string' || options.principal.subject.length === 0 || options.principal.subject.length > 256
|
|
393
|
+
|| typeof options.principal?.scope !== 'string' || options.principal.scope.length === 0 || options.principal.scope.length > 256)) {
|
|
390
394
|
return Promise.reject(new Error('booking_copilot_ingress_binding_invalid'));
|
|
391
395
|
}
|
|
392
396
|
const ingressMode = options.ingressMode ?? (hasIngressBinding ? 'bff-ingress-binding' : 'bff-bound-turn-only');
|
|
@@ -402,87 +406,32 @@ export function startBookingCopilotServer(options) {
|
|
|
402
406
|
const composition = {
|
|
403
407
|
runtime: options.runtime,
|
|
404
408
|
plannerFactory: options.plannerFactory,
|
|
405
|
-
...hasIngressBinding ? {
|
|
406
|
-
|
|
407
|
-
principal: options.principal
|
|
408
|
-
} : {},
|
|
409
|
-
ingressMode
|
|
409
|
+
...(hasIngressBinding ? { ingressBinding: options.ingressBinding, principal: options.principal } : {}),
|
|
410
|
+
ingressMode,
|
|
410
411
|
};
|
|
411
412
|
const maxBodyBytes = options.maxBodyBytes ?? 1_000_000;
|
|
412
413
|
const runningIdentity = runtimeIdentity();
|
|
413
|
-
const server = createServer(
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
}
|
|
423
|
-
const auth = String(req.headers.authorization ?? '');
|
|
424
|
-
if (!safeSecretEqual(auth, `Bearer ${options.apiKey}`)) {
|
|
425
|
-
sendJson(res, 401, {
|
|
426
|
-
error: {
|
|
427
|
-
code: 'unauthorized'
|
|
428
|
-
}
|
|
429
|
-
});
|
|
430
|
-
return;
|
|
431
|
-
}
|
|
432
|
-
if (isProbe) {
|
|
433
|
-
res.setHeader(BOOKING_SURFACE_VERSION_HEADER, BOOKING_SURFACE_SCHEMA_VERSION);
|
|
434
|
-
res.setHeader(BOOKING_SURFACE_SCHEMA_SHA256_HEADER, BOOKING_SURFACE_SCHEMA_SHA256);
|
|
435
|
-
if (options.artifactId) res.setHeader('X-GoTry-Artifact-ID', options.artifactId);
|
|
436
|
-
res.setHeader('X-GoTry-Node-Version', runningIdentity.nodeVersion);
|
|
437
|
-
res.setHeader('X-GoTry-Node-Modules-ABI', runningIdentity.nodeModulesAbi);
|
|
438
|
-
res.setHeader('X-GoTry-Release-Tuple', runningIdentity.releaseTuple);
|
|
439
|
-
if (runningIdentity.glibcVersion) res.setHeader('X-GoTry-Glibc-Version', runningIdentity.glibcVersion);
|
|
440
|
-
const acceptedTurnKinds = ingressMode === 'bff-ingress-binding' ? [
|
|
441
|
-
...BOOKING_COPILOT_ACCEPTED_TURN_KINDS,
|
|
442
|
-
'user.turn.ingress'
|
|
443
|
-
] : [
|
|
444
|
-
...BOOKING_COPILOT_ACCEPTED_TURN_KINDS
|
|
445
|
-
];
|
|
446
|
-
const healthBody = {
|
|
447
|
-
schemaVersion: BOOKING_SURFACE_SCHEMA_VERSION,
|
|
448
|
-
schemaSha256: BOOKING_SURFACE_SCHEMA_SHA256,
|
|
449
|
-
status: 'ready',
|
|
450
|
-
ingressMode,
|
|
451
|
-
acceptedTurnKinds
|
|
452
|
-
};
|
|
453
|
-
res.setHeader('X-GoTry-Ingress-Mode', ingressMode);
|
|
454
|
-
res.setHeader('X-GoTry-Accepted-Turn-Kinds', acceptedTurnKinds.join(','));
|
|
455
|
-
sendJson(res, 200, healthBody);
|
|
456
|
-
return;
|
|
457
|
-
}
|
|
458
|
-
const schemaVersion = String(req.headers[BOOKING_SURFACE_VERSION_HEADER] ?? '');
|
|
459
|
-
const schemaHash = String(req.headers[BOOKING_SURFACE_SCHEMA_SHA256_HEADER] ?? '');
|
|
460
|
-
if (schemaVersion !== BOOKING_SURFACE_SCHEMA_VERSION || schemaHash !== BOOKING_SURFACE_SCHEMA_SHA256) {
|
|
461
|
-
sendJson(res, 409, {
|
|
462
|
-
error: {
|
|
463
|
-
code: 'booking_surface_schema_mismatch',
|
|
464
|
-
expectedVersion: BOOKING_SURFACE_SCHEMA_VERSION,
|
|
465
|
-
expectedSchemaSha256: BOOKING_SURFACE_SCHEMA_SHA256
|
|
466
|
-
}
|
|
467
|
-
});
|
|
468
|
-
return;
|
|
469
|
-
}
|
|
470
|
-
await handleBookingCopilotRequest(req, res, composition, maxBodyBytes);
|
|
414
|
+
const server = createServer((req, res) => {
|
|
415
|
+
void bookingCopilotTrafficHandler({
|
|
416
|
+
apiKey: options.apiKey,
|
|
417
|
+
composition,
|
|
418
|
+
maxBodyBytes,
|
|
419
|
+
...(options.artifactId !== undefined ? { artifactId: options.artifactId } : {}),
|
|
420
|
+
ingressMode,
|
|
421
|
+
runningIdentity,
|
|
422
|
+
})(req, res);
|
|
471
423
|
});
|
|
472
|
-
return new Promise((resolve, reject)=>{
|
|
424
|
+
return new Promise((resolve, reject) => {
|
|
473
425
|
server.once('error', reject);
|
|
474
|
-
server.listen(options.port ?? 0, options.host ?? '127.0.0.1', ()=>{
|
|
426
|
+
server.listen(options.port ?? 0, options.host ?? '127.0.0.1', () => {
|
|
475
427
|
server.off('error', reject);
|
|
476
428
|
const address = server.address();
|
|
477
|
-
const port = typeof address === 'object' && address ? address.port : options.port ?? 0;
|
|
429
|
+
const port = typeof address === 'object' && address ? address.port : (options.port ?? 0);
|
|
478
430
|
resolve({
|
|
479
431
|
server,
|
|
480
432
|
port,
|
|
481
|
-
close: ()=>new Promise((done, fail)=>server.close((error)=>error ? fail(error) : done()))
|
|
433
|
+
close: () => new Promise((done, fail) => server.close((error) => error ? fail(error) : done())),
|
|
482
434
|
});
|
|
483
435
|
});
|
|
484
436
|
});
|
|
485
437
|
}
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
//# sourceURL=ts/src/booking-surface/server.ts
|