@danceiny/gotry 0.0.1-rc.9 → 0.2.0-rc.28
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 +172 -219
- package/README.zh-CN.md +222 -0
- package/bin/gotry-backend.js +53 -0
- package/bin/gotry-booking-copilot.js +53 -0
- package/bin/gotry-bootstrap.js +1788 -0
- package/bin/gotry-inner.js +733 -83
- package/bin/gotry-process-liveness.d.ts +27 -0
- package/bin/gotry-process-liveness.js +97 -0
- package/bin/gotry-runtime-resolution.d.ts +27 -0
- package/bin/gotry-runtime-resolution.js +49 -0
- package/bin/gotry.js +1 -1
- package/client/client.js +203 -0
- package/cordis.gotry-patch.yml +80 -56
- package/data/airline-airports.json +39 -0
- package/data/stations-12306-verify.json +524 -0
- package/dist/capabilities/agent-reach-deep.js +45 -122
- package/dist/capabilities/agent-reach.js +101 -146
- package/dist/capabilities/anything.js +161 -94
- package/dist/capabilities/artifacts.js +370 -0
- package/dist/capabilities/channel-health.js +131 -0
- package/dist/capabilities/channel-registry.js +256 -0
- package/dist/capabilities/doctor.js +553 -0
- package/dist/capabilities/effect.js +394 -0
- package/dist/capabilities/external-event.js +149 -0
- package/dist/capabilities/fact-log.js +55 -0
- package/dist/capabilities/flyai-config.js +337 -0
- package/dist/capabilities/flyai.js +947 -110
- package/dist/capabilities/fx-contract.js +293 -0
- package/dist/capabilities/geo-atlas.js +246 -0
- package/dist/capabilities/ground-transfer.js +676 -0
- package/dist/capabilities/hbcli.js +260 -123
- package/dist/capabilities/hotelbyte-transaction.js +194 -0
- package/dist/capabilities/incident-log.js +131 -61
- package/dist/capabilities/itinerary-artifact.js +190 -0
- package/dist/capabilities/itinerary-deck-artifact.js +194 -0
- package/dist/capabilities/itinerary-deck-export.js +339 -0
- package/dist/capabilities/lavish-local.js +924 -0
- package/dist/capabilities/model-override.js +38 -0
- package/dist/capabilities/opensky.js +54 -43
- package/dist/capabilities/provider-detail-sanitize.js +64 -0
- package/dist/capabilities/resilience.js +191 -0
- package/dist/capabilities/route-provider-conformance.js +341 -0
- package/dist/capabilities/session/action-cache.js +50 -44
- package/dist/capabilities/session/adapters/ctrip-flight.js +120 -62
- package/dist/capabilities/session/adapters/ctrip-hotel.js +222 -0
- package/dist/capabilities/session/adapters/dida-portal.js +274 -0
- package/dist/capabilities/session/adapters/meituan-local.js +35 -54
- package/dist/capabilities/session/adapters/rail-12306.js +342 -0
- package/dist/capabilities/session/benchmark.js +189 -0
- package/dist/capabilities/session/bridge-ledger.js +224 -0
- package/dist/capabilities/session/extension-bridge.js +636 -0
- package/dist/capabilities/session/extension-channel.js +109 -0
- package/dist/capabilities/session/extension-distribution.js +230 -0
- package/dist/capabilities/session/extract.js +31 -21
- package/dist/capabilities/session/golden-score.js +114 -0
- package/dist/capabilities/session/health-watch.js +215 -0
- package/dist/capabilities/session/read-guard.js +75 -24
- package/dist/capabilities/session/static-flight-golden.js +150 -0
- package/dist/capabilities/session/transport.js +97 -83
- package/dist/capabilities/session/wizard.js +117 -0
- package/dist/capabilities/session-consent.js +178 -0
- package/dist/capabilities/session-login.js +170 -0
- package/dist/capabilities/session-search.js +635 -44
- package/dist/capabilities/spawn-bounded.js +203 -0
- package/dist/capabilities/sponsor-plugin.js +341 -0
- package/dist/capabilities/visa-policy.js +140 -0
- package/dist/capabilities/weather.js +243 -138
- package/dist/data/session-golden-20.json +25 -0
- package/dist/data/sf-golden-manifest.json +103 -0
- package/dist/data/sf-static-routes.json +91 -0
- package/dist/scripts/action-cache-tests.js +24 -59
- package/dist/scripts/agent-planning-turn-deadline-e2e.js +242 -0
- package/dist/scripts/agent-planning-turn-deadline-tests.js +185 -0
- package/dist/scripts/agent-reach-deep-tests.js +30 -40
- package/dist/scripts/agent-reach-error-tests.js +354 -0
- package/dist/scripts/agent-reach-tests.js +26 -18
- package/dist/scripts/agent-reach-wrapper-tests.js +50 -50
- package/dist/scripts/anything-path-measure-tests.js +133 -0
- package/dist/scripts/anything-path-measure.js +150 -0
- package/dist/scripts/anything-tests.js +250 -60
- package/dist/scripts/artifact-client-contract-tests.js +220 -0
- package/dist/scripts/artifacts-capability-tests.js +727 -0
- package/dist/scripts/async-collect.js +50 -11
- package/dist/scripts/benchmark-environment-bridge-e2e.js +741 -0
- package/dist/scripts/benchmark-environment-bridge-tests.js +1452 -0
- package/dist/scripts/booking-copilot-availability-ledger-binding-tests.js +126 -0
- package/dist/scripts/booking-copilot-availability-policy-tests.js +265 -0
- package/dist/scripts/booking-copilot-bin-proof-tests.js +66 -0
- package/dist/scripts/booking-copilot-cleanup-proof-helper.js +76 -0
- package/dist/scripts/booking-copilot-crossrepo-fixture-server.js +144 -0
- package/dist/scripts/booking-copilot-dsh-core-proof-tests.js +424 -0
- package/dist/scripts/booking-copilot-dsh-planner-proof-tests.js +2839 -0
- package/dist/scripts/booking-copilot-dsh-plugin-proof-tests.js +309 -0
- package/dist/scripts/booking-copilot-dsh-readiness-proof.js +238 -0
- package/dist/scripts/booking-copilot-dsh-warmer-proof.js +126 -0
- package/dist/scripts/booking-copilot-event-sequence-concurrency-proof-tests.js +156 -0
- package/dist/scripts/booking-copilot-gap-code-contract-proof-tests.js +70 -0
- package/dist/scripts/booking-copilot-intent-proof-tests.js +422 -0
- package/dist/scripts/booking-copilot-operation-ledger-concurrency-proof-tests.js +184 -0
- package/dist/scripts/booking-copilot-planner-live-probe.js +95 -0
- package/dist/scripts/booking-copilot-proof-runtime.js +42 -0
- package/dist/scripts/booking-copilot-receipt-ledger-concurrency-proof-tests.js +210 -0
- package/dist/scripts/booking-copilot-runtime-proof-tests.js +2349 -0
- package/dist/scripts/booking-copilot-server-proof-tests.js +784 -0
- package/dist/scripts/booking-copilot-startup-proof-tests.js +94 -0
- 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 +125 -0
- package/dist/scripts/booking-surface-contract-proof-tests.js +212 -0
- package/dist/scripts/bootstrap-tests.js +1549 -0
- package/dist/scripts/bridge-events-tests.js +239 -0
- package/dist/scripts/bridge-ledger-tests.js +428 -0
- package/dist/scripts/build-changelog.js +238 -0
- package/dist/scripts/build-metrics-report.js +331 -0
- package/dist/scripts/changelog-tests.js +118 -0
- package/dist/scripts/channel-probe-tests.js +103 -0
- package/dist/scripts/channel-probe.js +169 -0
- package/dist/scripts/channel-registry-tests.js +208 -0
- package/dist/scripts/city-scenario-tier-tests.js +234 -0
- package/dist/scripts/companion-tests.js +25 -70
- package/dist/scripts/dida-cookie-probe.js +87 -0
- package/dist/scripts/dida-runner-stop-tests.js +311 -0
- package/dist/scripts/dida-sw-probe.js +114 -0
- package/dist/scripts/diff-test.js +17 -12
- package/dist/scripts/doctor-tests.js +600 -0
- package/dist/scripts/drill-multiuser-ledger-tests.js +1076 -0
- package/dist/scripts/drill-sdk-descendant-cleanup-tests.js +606 -0
- package/dist/scripts/drill-w2a-sensor-tests.js +788 -0
- package/dist/scripts/dsh-artifact-e2e.js +279 -0
- package/dist/scripts/dsh-artifact-web-e2e.js +675 -0
- package/dist/scripts/dsh-http-proxy-sse-proof.js +90 -0
- package/dist/scripts/dsh-llm-tests.js +437 -0
- package/dist/scripts/dsh-runtime-closure-tests.js +182 -0
- package/dist/scripts/dsh-runtime-closure.js +162 -0
- package/dist/scripts/dsh-session-v3-migration-proof.js +109 -0
- package/dist/scripts/dsh-subprocess-local-proof.js +150 -0
- package/dist/scripts/dsh-target-closure-proof.js +178 -0
- package/dist/scripts/effect-tests.js +539 -0
- package/dist/scripts/engine-run.js +1 -3
- package/dist/scripts/engine-tests.js +16 -10
- package/dist/scripts/evaluation-cadence-tests.js +174 -0
- package/dist/scripts/evaluation-contract-tests.js +268 -0
- package/dist/scripts/extension-distribution-cli.js +39 -0
- package/dist/scripts/extension-distribution-tests.js +443 -0
- package/dist/scripts/extension-tests.js +988 -0
- package/dist/scripts/external-event-tests.js +350 -0
- package/dist/scripts/fact-gate-tests.js +869 -0
- 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-setup-tests.js +228 -0
- package/dist/scripts/flyai-setup-tool-tests.js +54 -0
- package/dist/scripts/flyai-tests.js +692 -0
- package/dist/scripts/flyai-tool-registration-contract-tests.js +91 -0
- package/dist/scripts/fx-contract-tests.js +201 -0
- package/dist/scripts/g5-guard-tests.js +150 -0
- package/dist/scripts/g5-guard.js +193 -0
- package/dist/scripts/gen-lunar-anchors.js +106 -0
- package/dist/scripts/geo-atlas-tests.js +283 -0
- package/dist/scripts/gotry-backend-kernel-tests.js +206 -0
- package/dist/scripts/gotry-try-demo-tests.js +79 -0
- package/dist/scripts/gotry-try-demo.js +49 -0
- package/dist/scripts/ground-transfer-tests.js +956 -0
- package/dist/scripts/hbcli-e2e-tests.js +159 -0
- 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 +121 -69
- package/dist/scripts/health-watch-cli.js +73 -0
- 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 +41 -0
- 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 +569 -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/issue436-persisted-routing-e2e.js +247 -0
- package/dist/scripts/itinerary-artifact-tests.js +368 -0
- package/dist/scripts/itinerary-deck-artifact-tests.js +416 -0
- package/dist/scripts/itinerary-deck-export-tests.js +552 -0
- package/dist/scripts/itinerary-deck-tests.js +435 -0
- package/dist/scripts/itinerary-html-tests.js +426 -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/lavish-local-tests.js +1226 -0
- package/dist/scripts/lavish-product-tools-tests.js +537 -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 +382 -246
- package/dist/scripts/ledger-workflow-crash.js +10 -20
- package/dist/scripts/m3-cohort-tests.js +714 -0
- package/dist/scripts/m3-cohort.js +310 -0
- package/dist/scripts/managed-dsh-cleanup-diagnostic-proof.js +184 -0
- package/dist/scripts/managed-dsh-close-failure-proof.js +172 -0
- package/dist/scripts/managed-dsh-run-port-proof.js +153 -0
- package/dist/scripts/managed-dsh-terminal-proof.js +252 -0
- package/dist/scripts/map-tools-vendor-package-proof.js +365 -0
- 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 +631 -0
- package/dist/scripts/memory-value-tests.js +328 -0
- package/dist/scripts/metrics-report-tests.js +151 -0
- package/dist/scripts/model-override-e2e.js +137 -0
- package/dist/scripts/nightly-evidence-tests.js +203 -0
- package/dist/scripts/nightly-evidence.js +303 -0
- package/dist/scripts/nudge-digest.js +39 -28
- package/dist/scripts/observability-tests.js +381 -0
- package/dist/scripts/onboarding-tests.js +161 -0
- package/dist/scripts/opensky-check.js +18 -13
- package/dist/scripts/opensky-tests.js +28 -28
- package/dist/scripts/outcome-projection-tests.js +427 -0
- package/dist/scripts/persona-sim-tests.js +603 -0
- package/dist/scripts/persona-sim.js +1081 -0
- package/dist/scripts/persona-surface-guard-tests.js +49 -0
- package/dist/scripts/pnpm-dsh-closure-proof.js +17 -0
- package/dist/scripts/policy-anchor-359-registered-e2e.js +214 -0
- package/dist/scripts/price-drift-tests.js +340 -0
- package/dist/scripts/price-drift-watch.js +447 -0
- package/dist/scripts/probe-poi-tests.js +53 -45
- package/dist/scripts/product-metrics.js +483 -0
- package/dist/scripts/publish-preverify.js +110 -0
- package/dist/scripts/realtime-pricing-tests.js +76 -0
- package/dist/scripts/recall-tests.js +513 -0
- package/dist/scripts/replay-async.js +22 -18
- package/dist/scripts/replay-real.js +18 -18
- package/dist/scripts/replay.js +54 -82
- package/dist/scripts/route-provider-conformance-tests.js +391 -0
- package/dist/scripts/session-attach-diagnose.js +20 -20
- package/dist/scripts/session-attach-poc.js +34 -37
- package/dist/scripts/session-benchmark.js +180 -0
- package/dist/scripts/session-dida-live-e2e.js +199 -0
- package/dist/scripts/session-extract-tests.js +14 -35
- package/dist/scripts/session-link-tests.js +478 -0
- package/dist/scripts/session-login.js +27 -46
- package/dist/scripts/session-tests.js +796 -96
- package/dist/scripts/session-zone-crash.js +49 -0
- package/dist/scripts/session-zone-ledger-tests.js +432 -0
- package/dist/scripts/session-zone-observation-tests.js +492 -0
- package/dist/scripts/session-zone-observe.js +135 -0
- package/dist/scripts/session-zone-wiring-tests.js +566 -0
- package/dist/scripts/session-zones-tests.js +374 -0
- package/dist/scripts/sf-live-benchmark.js +367 -0
- package/dist/scripts/sf-live-challenge-stop-tests.js +453 -0
- package/dist/scripts/sf-live-cli-tests.js +92 -0
- package/dist/scripts/sf-live-options.js +46 -0
- package/dist/scripts/sf-manifest-tests.js +30 -0
- package/dist/scripts/sf-manifest.js +33 -0
- package/dist/scripts/sf-soft-score-tests.js +67 -0
- package/dist/scripts/sf-summary-tests.js +324 -0
- package/dist/scripts/sf-summary.js +525 -0
- package/dist/scripts/share-tests.js +432 -0
- package/dist/scripts/skeleton-check.js +16 -14
- package/dist/scripts/skeleton-integration-test.js +28 -11
- package/dist/scripts/skills-contract-tests.js +51 -46
- package/dist/scripts/smoke-session-gate-tests.js +21 -0
- package/dist/scripts/smoke.js +690 -246
- package/dist/scripts/spawn-bounded-abort-tests.js +244 -0
- package/dist/scripts/sponsor-reuse-tests.js +262 -0
- package/dist/scripts/state-cli-tests.js +226 -101
- package/dist/scripts/state-cli.js +565 -198
- package/dist/scripts/state-repair-cli.js +66 -0
- package/dist/scripts/state-repair-tests.js +140 -0
- package/dist/scripts/static-golden-tests.js +165 -0
- package/dist/scripts/time-eval-tests.js +212 -192
- package/dist/scripts/travel-timeline-tests.js +29 -93
- package/dist/scripts/turn-handoff-collect-tests.js +169 -0
- package/dist/scripts/turn-handoff-collect.js +161 -0
- package/dist/scripts/turn-policy-tests.js +55 -0
- package/dist/scripts/typed-contract-canary.js +176 -0
- package/dist/scripts/tz-resolver-sanity.js +50 -0
- package/dist/scripts/tz-resolver-tests.js +584 -0
- package/dist/scripts/unified-tests.js +60 -20
- package/dist/scripts/visa-policy-tests.js +62 -0
- package/dist/scripts/weather-tests.js +276 -41
- package/dist/scripts/wish-channel-gate-tests.js +54 -0
- 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 +46 -0
- package/dist/src/artifact-gate.js +829 -0
- package/dist/src/backend/kernel.js +91 -0
- package/dist/src/backend/modules/booking-copilot.js +92 -0
- package/dist/src/backend/modules/booking-executor.js +124 -0
- package/dist/src/backend/modules/session-search.js +551 -0
- package/dist/src/benchmark-agent-conformance.js +578 -0
- package/dist/src/benchmark-environment-bridge.js +515 -0
- package/dist/src/benchmark-headless-child-diagnostics.js +166 -0
- package/dist/src/benchmark-tool-isolation.js +151 -0
- package/dist/src/bookable-facts.js +533 -0
- package/dist/src/booking-saga.js +97 -0
- package/dist/src/booking-surface/availability-policy.js +697 -0
- package/dist/src/booking-surface/booking-intent.js +43 -0
- package/dist/src/booking-surface/cancel-refund-commission.js +379 -0
- package/dist/src/booking-surface/canonical-schema.js +116 -0
- package/dist/src/booking-surface/contracts.js +75 -0
- package/dist/src/booking-surface/dsh-planner.js +1854 -0
- package/dist/src/booking-surface/dsh-plugin.js +378 -0
- package/dist/src/booking-surface/dsh-worker.js +98 -0
- package/dist/src/booking-surface/error-codes.js +231 -0
- package/dist/src/booking-surface/index.js +9 -0
- package/dist/src/booking-surface/managed-dsh-cleanup-diagnostic.js +97 -0
- package/dist/src/booking-surface/managed-dsh-run-port.js +244 -0
- package/dist/src/booking-surface/profile.js +24 -0
- package/dist/src/booking-surface/recovery-chain.js +367 -0
- package/dist/src/booking-surface/runtime.js +1968 -0
- package/dist/src/booking-surface/server.js +674 -0
- package/dist/src/booking-surface/startup.js +168 -0
- package/dist/src/booking-surface/validation.js +261 -0
- package/dist/src/bridge.js +20 -13
- package/dist/src/city-scenario-tier.js +378 -0
- package/dist/src/companions.js +48 -74
- package/dist/src/contracts.js +21 -43
- package/dist/src/dsh-llm.js +171 -115
- package/dist/src/engine.js +276 -193
- package/dist/src/evaluation-cadence.js +97 -0
- package/dist/src/evaluation-contracts.js +283 -0
- package/dist/src/flight-pack-adapter.js +54 -0
- package/dist/src/flight-pack-contract.js +2 -0
- package/dist/src/flyai-setup-tool.js +70 -0
- package/dist/src/gotry-backend.js +125 -0
- package/dist/src/hotel-date-gate.js +118 -0
- package/dist/src/i18n.js +88 -0
- package/dist/src/index.js +2385 -881
- package/dist/src/itinerary-deck.js +139 -0
- package/dist/src/itinerary-doc-shared.js +661 -0
- package/dist/src/itinerary-html.js +131 -0
- package/dist/src/journey.js +155 -108
- package/dist/src/lavish-tools.js +400 -0
- package/dist/src/ledger-repair-apply.js +538 -0
- package/dist/src/ledger-repair-plan.js +463 -0
- package/dist/src/loop.js +331 -161
- package/dist/src/m3-cohort.js +1577 -0
- 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/outcome-projection.js +746 -0
- package/dist/src/realtime-pricing.js +103 -0
- package/dist/src/recall/card.js +74 -0
- package/dist/src/recall/evaluator.js +140 -0
- package/dist/src/recall/integration.js +17 -0
- package/dist/src/recall/tick.js +166 -0
- package/dist/src/session-link/plan-it-action.js +48 -0
- package/dist/src/session-link/session-link.js +248 -0
- package/dist/src/session-zone-ledger.js +312 -0
- package/dist/src/session-zone-observation.js +354 -0
- package/dist/src/session-zone-wiring.js +658 -0
- package/dist/src/session-zones.js +510 -0
- package/dist/src/share/adapters.js +51 -0
- package/dist/src/share/share-consent.js +76 -0
- package/dist/src/share/share-deck.js +58 -0
- package/dist/src/share/share-token.js +146 -0
- package/dist/src/slot-spec.js +84 -106
- package/dist/src/state-ledger.js +696 -524
- package/dist/src/state-repair.js +299 -0
- 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 +287 -0
- package/dist/src/turn-policy.js +118 -0
- package/dist/src/tz-resolver.js +143 -0
- package/dist/src/unified.js +600 -360
- package/dist/src/wish-pool.js +42 -12
- package/dist/src/write-gate.js +495 -0
- package/dist/src/z3-shared.js +284 -0
- package/extension/README.md +21 -0
- package/extension/README.zh-CN.md +21 -0
- package/extension/background.js +463 -0
- package/extension/content-bridge.js +270 -0
- package/extension/content-main.js +111 -0
- package/extension/manifest.json +60 -0
- package/package.json +407 -14
- package/schemas/booking.intent.schema.json +193 -0
- package/schemas/booking.surface.schema.json +2622 -0
- package/ts/capabilities/agent-reach-deep.ts +114 -0
- package/ts/capabilities/agent-reach.ts +198 -0
- package/ts/capabilities/anything.ts +282 -0
- package/ts/capabilities/artifacts.ts +428 -0
- package/ts/capabilities/fact-log.ts +54 -0
- package/ts/capabilities/flyai-config.ts +388 -0
- package/ts/capabilities/flyai.ts +1274 -0
- package/ts/capabilities/hbcli.ts +253 -71
- package/ts/capabilities/incident-log.ts +88 -59
- package/ts/capabilities/itinerary-artifact.ts +214 -0
- package/ts/capabilities/itinerary-deck-artifact.ts +218 -0
- package/ts/capabilities/itinerary-deck-export.ts +378 -0
- package/ts/capabilities/opensky.ts +121 -0
- package/ts/capabilities/session/action-cache.ts +148 -0
- package/ts/capabilities/session/adapters/ctrip-flight.ts +181 -0
- package/ts/capabilities/session/adapters/ctrip-hotel.ts +238 -0
- package/ts/capabilities/session/adapters/dida-portal.ts +338 -0
- package/ts/capabilities/session/adapters/meituan-local.ts +70 -0
- package/ts/capabilities/session/adapters/rail-12306.ts +392 -0
- package/ts/capabilities/session/benchmark.ts +276 -0
- package/ts/capabilities/session/bridge-ledger.ts +333 -0
- package/ts/capabilities/session/extension-bridge.ts +794 -0
- package/ts/capabilities/session/extension-channel.ts +144 -0
- package/ts/capabilities/session/extension-distribution.ts +266 -0
- package/ts/capabilities/session/extract.ts +54 -0
- package/ts/capabilities/session/golden-score.ts +139 -0
- package/ts/capabilities/session/health-watch.ts +264 -0
- package/ts/capabilities/session/read-guard.ts +141 -0
- package/ts/capabilities/session/static-flight-golden.ts +209 -0
- package/ts/capabilities/session/transport.ts +146 -0
- package/ts/capabilities/session/wizard.ts +140 -0
- package/ts/capabilities/session-consent.ts +211 -0
- package/ts/capabilities/session-login.ts +208 -0
- package/ts/capabilities/session-search.ts +893 -0
- package/ts/capabilities/weather.ts +330 -0
- package/ts/dsh-runtime/vendor/README.md +68 -0
- package/ts/dsh-runtime/vendor/dsh-map-tools/LICENSE +21 -0
- package/ts/dsh-runtime/vendor/dsh-map-tools/README.en.md +405 -0
- package/ts/dsh-runtime/vendor/dsh-map-tools/README.md +404 -0
- package/ts/dsh-runtime/vendor/dsh-map-tools/client/client.js +1584 -0
- package/ts/dsh-runtime/vendor/dsh-map-tools/cordis.patch.yml +5 -0
- package/ts/dsh-runtime/vendor/dsh-map-tools/icon.svg +5 -0
- package/ts/dsh-runtime/vendor/dsh-map-tools/lib/clients/amap.js +595 -0
- package/ts/dsh-runtime/vendor/dsh-map-tools/lib/clients/nominatim.js +63 -0
- package/ts/dsh-runtime/vendor/dsh-map-tools/lib/clients/osrm.js +58 -0
- package/ts/dsh-runtime/vendor/dsh-map-tools/lib/clients/photon.js +55 -0
- package/ts/dsh-runtime/vendor/dsh-map-tools/lib/config-file.js +87 -0
- package/ts/dsh-runtime/vendor/dsh-map-tools/lib/config-route.js +131 -0
- package/ts/dsh-runtime/vendor/dsh-map-tools/lib/config.js +16 -0
- package/ts/dsh-runtime/vendor/dsh-map-tools/lib/geo.js +232 -0
- package/ts/dsh-runtime/vendor/dsh-map-tools/lib/index.js +120 -0
- package/ts/dsh-runtime/vendor/dsh-map-tools/lib/settings-ns.js +40 -0
- package/ts/dsh-runtime/vendor/dsh-map-tools/lib/staticmap-route.js +115 -0
- package/ts/dsh-runtime/vendor/dsh-map-tools/lib/tools/geocode.js +127 -0
- package/ts/dsh-runtime/vendor/dsh-map-tools/lib/tools/poi.js +84 -0
- package/ts/dsh-runtime/vendor/dsh-map-tools/lib/tools/routes.js +267 -0
- package/ts/dsh-runtime/vendor/dsh-map-tools/lib/types/clients/amap.d.ts +90 -0
- package/ts/dsh-runtime/vendor/dsh-map-tools/lib/types/clients/nominatim.d.ts +17 -0
- package/ts/dsh-runtime/vendor/dsh-map-tools/lib/types/clients/osrm.d.ts +15 -0
- package/ts/dsh-runtime/vendor/dsh-map-tools/lib/types/clients/photon.d.ts +22 -0
- package/ts/dsh-runtime/vendor/dsh-map-tools/lib/types/config-file.d.ts +33 -0
- package/ts/dsh-runtime/vendor/dsh-map-tools/lib/types/config-route.d.ts +23 -0
- package/ts/dsh-runtime/vendor/dsh-map-tools/lib/types/config.d.ts +23 -0
- package/ts/dsh-runtime/vendor/dsh-map-tools/lib/types/geo.d.ts +61 -0
- package/ts/dsh-runtime/vendor/dsh-map-tools/lib/types/index.d.ts +18 -0
- package/ts/dsh-runtime/vendor/dsh-map-tools/lib/types/settings-ns.d.ts +14 -0
- package/ts/dsh-runtime/vendor/dsh-map-tools/lib/types/staticmap-route.d.ts +31 -0
- package/ts/dsh-runtime/vendor/dsh-map-tools/lib/types/tools/geocode.d.ts +11 -0
- package/ts/dsh-runtime/vendor/dsh-map-tools/lib/types/tools/poi.d.ts +9 -0
- package/ts/dsh-runtime/vendor/dsh-map-tools/lib/types/tools/routes.d.ts +75 -0
- package/ts/dsh-runtime/vendor/dsh-map-tools/lib/types/types.d.ts +68 -0
- package/ts/dsh-runtime/vendor/dsh-map-tools/lib/types.js +16 -0
- package/ts/dsh-runtime/vendor/dsh-map-tools/package.json +97 -0
- package/ts/package.json +293 -3
- package/ts/scripts/async-collect.ts +91 -0
- package/ts/scripts/map-tools-vendor-package-proof.ts +404 -0
- package/ts/scripts/skeleton-check.ts +1 -1
- package/ts/scripts/skeleton-integration-test.ts +23 -1
- package/ts/scripts/state-cli.ts +586 -0
- package/ts/scripts/turn-handoff-collect.ts +178 -0
- package/ts/src/artifact-gate.ts +991 -0
- package/ts/src/benchmark-agent-conformance.ts +619 -0
- package/ts/src/benchmark-environment-bridge.ts +538 -0
- package/ts/src/benchmark-headless-child-diagnostics.ts +184 -0
- package/ts/src/benchmark-tool-isolation.ts +166 -0
- package/ts/src/bookable-facts.ts +837 -0
- package/ts/src/booking-saga.ts +133 -0
- package/ts/src/booking-surface/availability-policy.ts +522 -0
- package/ts/src/booking-surface/booking-intent.ts +86 -0
- package/ts/src/booking-surface/cancel-refund-commission.ts +488 -0
- package/ts/src/booking-surface/canonical-schema.js +116 -0
- package/ts/src/booking-surface/contracts.ts +475 -0
- package/ts/src/booking-surface/dsh-planner.ts +1949 -0
- package/ts/src/booking-surface/dsh-plugin.js +378 -0
- package/ts/src/booking-surface/dsh-worker.ts +110 -0
- package/ts/src/booking-surface/error-codes.ts +241 -0
- package/ts/src/booking-surface/index.ts +9 -0
- package/ts/src/booking-surface/managed-dsh-cleanup-diagnostic.ts +151 -0
- package/ts/src/booking-surface/managed-dsh-run-port.ts +273 -0
- package/ts/src/booking-surface/profile.ts +42 -0
- package/ts/src/booking-surface/recovery-chain.ts +409 -0
- package/ts/src/booking-surface/runtime.ts +1793 -0
- package/ts/src/booking-surface/server.ts +733 -0
- package/ts/src/booking-surface/startup.ts +201 -0
- package/ts/src/booking-surface/validation.ts +217 -0
- package/ts/src/companions.ts +113 -0
- package/ts/src/contracts.ts +17 -3
- package/ts/src/dsh-llm.ts +152 -17
- package/ts/src/flyai-setup-tool.ts +77 -0
- package/ts/src/index.ts +2123 -200
- package/ts/src/itinerary-deck.ts +189 -0
- package/ts/src/itinerary-doc-shared.ts +741 -0
- package/ts/src/itinerary-html.ts +171 -0
- package/ts/src/loop.ts +234 -26
- package/ts/src/memory-capture.ts +283 -0
- package/ts/src/memory-decay.ts +55 -0
- package/ts/src/memory-utility.ts +70 -0
- package/ts/src/mock-llm.ts +4 -8
- package/ts/src/model.ts +88 -2
- package/ts/src/recall/card.ts +106 -0
- package/ts/src/recall/evaluator.ts +170 -0
- package/ts/src/recall/integration.ts +35 -0
- package/ts/src/recall/tick.ts +213 -0
- package/ts/src/session-link/plan-it-action.ts +78 -0
- package/ts/src/session-link/session-link.ts +275 -0
- package/ts/src/share/adapters.ts +93 -0
- package/ts/src/share/share-consent.ts +105 -0
- package/ts/src/share/share-deck.ts +85 -0
- package/ts/src/share/share-token.ts +166 -0
- package/ts/src/slot-spec.ts +172 -0
- package/ts/src/state-ledger.ts +1166 -0
- package/ts/src/subagent-job-id-guard.ts +72 -0
- package/ts/src/time-anchor.ts +338 -0
- package/ts/src/tool-packet.ts +32 -0
- package/ts/src/travel-slots.ts +219 -0
- package/ts/src/travel-timeline.ts +83 -0
- package/ts/src/turn-deadline.ts +366 -0
- package/ts/src/turn-policy.ts +154 -0
- package/ts/src/unified.ts +444 -105
- package/ts/src/wish-pool.ts +79 -0
- package/dist/scripts/session-attach-wait.js +0 -41
|
@@ -0,0 +1,733 @@
|
|
|
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
|
+
*/
|
|
8
|
+
|
|
9
|
+
import { timingSafeEqual } from 'node:crypto'
|
|
10
|
+
import { createServer, type IncomingMessage, type Server, type ServerResponse } from 'node:http'
|
|
11
|
+
import {
|
|
12
|
+
BOOKING_COPILOT_ACCEPTED_TURN_KINDS,
|
|
13
|
+
BOOKING_COPILOT_INGRESS_MODES,
|
|
14
|
+
BOOKING_READ_ACTION_KINDS,
|
|
15
|
+
BOOKING_SURFACE_ALLOWED_ACTIONS,
|
|
16
|
+
BOOKING_SURFACE_FEATURES,
|
|
17
|
+
BOOKING_SURFACE_FEATURES_HEADER,
|
|
18
|
+
BOOKING_SURFACE_SCHEMA_SHA256,
|
|
19
|
+
BOOKING_SURFACE_SCHEMA_SHA256_HEADER,
|
|
20
|
+
BOOKING_SURFACE_SCHEMA_VERSION,
|
|
21
|
+
BOOKING_SURFACE_VERSION_HEADER,
|
|
22
|
+
bookingSurfaceActionsForFeatures,
|
|
23
|
+
type BookingCopilotIngressMode,
|
|
24
|
+
type BookingCopilotTurn,
|
|
25
|
+
type BookingIngressBinding,
|
|
26
|
+
type BookingIngressPrincipal,
|
|
27
|
+
type BookingReadActionKind,
|
|
28
|
+
type BookingSurfaceFeature,
|
|
29
|
+
type BookingSurfaceEvent,
|
|
30
|
+
type BookingWorkspaceSnapshot,
|
|
31
|
+
type ObservableOrderFact,
|
|
32
|
+
type IngressTurn,
|
|
33
|
+
} from './contracts.ts'
|
|
34
|
+
import {
|
|
35
|
+
bookingDigest,
|
|
36
|
+
BookingCopilotTaskRuntime,
|
|
37
|
+
type BookingCopilotTaskState,
|
|
38
|
+
type BookingIngressRequestBindingInput,
|
|
39
|
+
type BookingPlannerSessionFactory,
|
|
40
|
+
} from './runtime.ts'
|
|
41
|
+
import { validateBookingSurface } from './validation.ts'
|
|
42
|
+
import { extractBookingDispatchReason, normalizeBookingErrorCode, safeBookingErrorMessage } from './error-codes.ts'
|
|
43
|
+
|
|
44
|
+
export interface BookingCopilotComposition {
|
|
45
|
+
runtime: BookingCopilotTaskRuntime
|
|
46
|
+
plannerFactory: BookingPlannerSessionFactory
|
|
47
|
+
/** Optional BFF-owned identity binding. GoTry never accepts browser identity. */
|
|
48
|
+
ingressBinding?: BookingIngressBinding
|
|
49
|
+
/** Principal authenticated by the HotelByte BFF, never by browser input. */
|
|
50
|
+
principal?: BookingIngressPrincipal
|
|
51
|
+
/** Explicit process composition mode; omitted means infer from a complete pair. */
|
|
52
|
+
ingressMode?: BookingCopilotIngressMode
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export interface BookingCopilotServerOptions extends BookingCopilotComposition {
|
|
56
|
+
/** Deployment credential shared only with the HotelByte BFF. */
|
|
57
|
+
apiKey: string
|
|
58
|
+
host?: string
|
|
59
|
+
port?: number
|
|
60
|
+
maxBodyBytes?: number
|
|
61
|
+
/** GoTry source commit loaded from the root-owned release artifact. */
|
|
62
|
+
artifactId?: string
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
export interface BookingCopilotServerHandle {
|
|
66
|
+
server: Server
|
|
67
|
+
port: number
|
|
68
|
+
close(): Promise<void>
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
export interface BookingCopilotRuntimeIdentity {
|
|
72
|
+
nodeVersion: string
|
|
73
|
+
nodeModulesAbi: string
|
|
74
|
+
releaseTuple: string
|
|
75
|
+
glibcVersion: string
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/** gotry-backend 模块挂载用的流量处理依赖(startBookingCopilotServer 与 backend 模块共用) */
|
|
79
|
+
export interface BookingCopilotTrafficDeps {
|
|
80
|
+
apiKey: string
|
|
81
|
+
composition: BookingCopilotComposition
|
|
82
|
+
maxBodyBytes: number
|
|
83
|
+
artifactId?: string
|
|
84
|
+
ingressMode: BookingCopilotIngressMode
|
|
85
|
+
runningIdentity: BookingCopilotRuntimeIdentity
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* 单请求处理闭包(startBookingCopilotServer 的 createServer 回调原样摘出):
|
|
90
|
+
* 探活(/healthz /status)+ 唯一 turn 路由 + 鉴权 + schema 头校验。
|
|
91
|
+
* gotry-backend 的 booking-copilot 模块复用同一闭包,保证两条挂载路径行为逐字一致。
|
|
92
|
+
*/
|
|
93
|
+
export function bookingCopilotTrafficHandler(deps: BookingCopilotTrafficDeps): (req: IncomingMessage, res: ServerResponse) => Promise<void> {
|
|
94
|
+
const { apiKey, composition, maxBodyBytes, artifactId, ingressMode, runningIdentity } = deps
|
|
95
|
+
return async (req, res) => {
|
|
96
|
+
const isProbe = req.method === 'GET' && (req.url === '/healthz' || req.url === '/status')
|
|
97
|
+
if (!isProbe && (req.method !== 'POST' || req.url !== '/a2a/booking-copilot/turn')) {
|
|
98
|
+
sendJson(res, 404, { error: { code: 'not_found' } })
|
|
99
|
+
return
|
|
100
|
+
}
|
|
101
|
+
const auth = String(req.headers.authorization ?? '')
|
|
102
|
+
if (!safeSecretEqual(auth, `Bearer ${apiKey}`)) {
|
|
103
|
+
sendJson(res, 401, { error: { code: 'unauthorized' } })
|
|
104
|
+
return
|
|
105
|
+
}
|
|
106
|
+
if (lifecycleFor(composition).shuttingDown) {
|
|
107
|
+
sendJson(res, 503, { error: { code: 'booking_copilot_shutting_down' } })
|
|
108
|
+
return
|
|
109
|
+
}
|
|
110
|
+
if (isProbe) {
|
|
111
|
+
res.setHeader(BOOKING_SURFACE_VERSION_HEADER, BOOKING_SURFACE_SCHEMA_VERSION)
|
|
112
|
+
res.setHeader(BOOKING_SURFACE_SCHEMA_SHA256_HEADER, BOOKING_SURFACE_SCHEMA_SHA256)
|
|
113
|
+
res.setHeader(BOOKING_SURFACE_FEATURES_HEADER, BOOKING_SURFACE_FEATURES.join(','))
|
|
114
|
+
if (artifactId) res.setHeader('X-GoTry-Artifact-ID', artifactId)
|
|
115
|
+
res.setHeader('X-GoTry-Node-Version', runningIdentity.nodeVersion)
|
|
116
|
+
res.setHeader('X-GoTry-Node-Modules-ABI', runningIdentity.nodeModulesAbi)
|
|
117
|
+
res.setHeader('X-GoTry-Release-Tuple', runningIdentity.releaseTuple)
|
|
118
|
+
if (runningIdentity.glibcVersion) res.setHeader('X-GoTry-Glibc-Version', runningIdentity.glibcVersion)
|
|
119
|
+
const acceptedTurnKinds = ingressMode === 'bff-ingress-binding'
|
|
120
|
+
? [...BOOKING_COPILOT_ACCEPTED_TURN_KINDS, 'user.turn.ingress' as const]
|
|
121
|
+
: [...BOOKING_COPILOT_ACCEPTED_TURN_KINDS]
|
|
122
|
+
const healthBody: Record<string, unknown> = {
|
|
123
|
+
schemaVersion: BOOKING_SURFACE_SCHEMA_VERSION,
|
|
124
|
+
schemaSha256: BOOKING_SURFACE_SCHEMA_SHA256,
|
|
125
|
+
status: 'ready',
|
|
126
|
+
ingressMode,
|
|
127
|
+
acceptedTurnKinds,
|
|
128
|
+
features: [...BOOKING_SURFACE_FEATURES],
|
|
129
|
+
}
|
|
130
|
+
res.setHeader('X-GoTry-Ingress-Mode', ingressMode)
|
|
131
|
+
res.setHeader('X-GoTry-Accepted-Turn-Kinds', acceptedTurnKinds.join(','))
|
|
132
|
+
sendJson(res, 200, healthBody)
|
|
133
|
+
return
|
|
134
|
+
}
|
|
135
|
+
const schemaVersion = String(req.headers[BOOKING_SURFACE_VERSION_HEADER] ?? '')
|
|
136
|
+
const schemaHash = String(req.headers[BOOKING_SURFACE_SCHEMA_SHA256_HEADER] ?? '')
|
|
137
|
+
// `booking.surface` is the v1 compatibility line. Compatible additive
|
|
138
|
+
// releases keep this version and may have different schema bytes; the
|
|
139
|
+
// canonical payload validator below remains authoritative. Only a breaking
|
|
140
|
+
// contract line changes this version and is rejected at the version gate.
|
|
141
|
+
if (schemaVersion !== BOOKING_SURFACE_SCHEMA_VERSION) {
|
|
142
|
+
sendJson(res, 409, {
|
|
143
|
+
error: {
|
|
144
|
+
code: 'booking_surface_schema_mismatch',
|
|
145
|
+
expectedVersion: BOOKING_SURFACE_SCHEMA_VERSION,
|
|
146
|
+
},
|
|
147
|
+
})
|
|
148
|
+
return
|
|
149
|
+
}
|
|
150
|
+
if (schemaHash && !/^[0-9a-f]{64}$/i.test(schemaHash)) {
|
|
151
|
+
sendJson(res, 400, { error: { code: 'booking_surface_schema_hash_invalid' } })
|
|
152
|
+
return
|
|
153
|
+
}
|
|
154
|
+
const lifecycle = lifecycleFor(composition)
|
|
155
|
+
lifecycle.acceptedRequests += 1
|
|
156
|
+
try {
|
|
157
|
+
await handleBookingCopilotRequest(req, res, composition, maxBodyBytes, negotiatedBookingSurfaceFeatures(req))
|
|
158
|
+
} finally {
|
|
159
|
+
finishAcceptedRequest(lifecycle)
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
type PlannerSession = ReturnType<BookingPlannerSessionFactory>
|
|
165
|
+
|
|
166
|
+
interface PlannerLifecycle {
|
|
167
|
+
active: Map<string, PlannerSession>
|
|
168
|
+
/** Close operations detached from request latency, but still owned by the composition. */
|
|
169
|
+
closing: Set<Promise<void>>
|
|
170
|
+
/** Sticky cleanup failures surfaced by shutdown; close is not assumed retryable. */
|
|
171
|
+
cleanupFailures: Map<PlannerSession, unknown>
|
|
172
|
+
/** Turn handlers admitted before shutdown began. */
|
|
173
|
+
acceptedRequests: number
|
|
174
|
+
acceptedRequestDrainWaiters: Set<() => void>
|
|
175
|
+
shuttingDown: boolean
|
|
176
|
+
shutdownPromise?: Promise<void>
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
const plannerLifecycles = new WeakMap<BookingCopilotComposition, PlannerLifecycle>()
|
|
180
|
+
const decisionFlightsByComposition = new WeakMap<BookingCopilotComposition, Map<string, Promise<BookingSurfaceEvent[]>>>()
|
|
181
|
+
|
|
182
|
+
function lifecycleFor(composition: BookingCopilotComposition): PlannerLifecycle {
|
|
183
|
+
let lifecycle = plannerLifecycles.get(composition)
|
|
184
|
+
if (!lifecycle) {
|
|
185
|
+
lifecycle = {
|
|
186
|
+
active: new Map(),
|
|
187
|
+
closing: new Set(),
|
|
188
|
+
cleanupFailures: new Map(),
|
|
189
|
+
acceptedRequests: 0,
|
|
190
|
+
acceptedRequestDrainWaiters: new Set(),
|
|
191
|
+
shuttingDown: false,
|
|
192
|
+
}
|
|
193
|
+
plannerLifecycles.set(composition, lifecycle)
|
|
194
|
+
}
|
|
195
|
+
return lifecycle
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
function finishAcceptedRequest(lifecycle: PlannerLifecycle): void {
|
|
199
|
+
lifecycle.acceptedRequests -= 1
|
|
200
|
+
if (lifecycle.acceptedRequests !== 0) return
|
|
201
|
+
for (const resolve of lifecycle.acceptedRequestDrainWaiters) resolve()
|
|
202
|
+
lifecycle.acceptedRequestDrainWaiters.clear()
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
function drainAcceptedRequests(composition: BookingCopilotComposition): Promise<void> {
|
|
206
|
+
const lifecycle = lifecycleFor(composition)
|
|
207
|
+
if (lifecycle.acceptedRequests === 0) return Promise.resolve()
|
|
208
|
+
return new Promise((resolve) => lifecycle.acceptedRequestDrainWaiters.add(resolve))
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
function sessionsFor(composition: BookingCopilotComposition): Map<string, PlannerSession> {
|
|
212
|
+
return lifecycleFor(composition).active
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
function schedulePlannerSessionClose(composition: BookingCopilotComposition, session: PlannerSession): void {
|
|
216
|
+
const lifecycle = lifecycleFor(composition)
|
|
217
|
+
const cleanup = Promise.resolve().then(() => session.close?.()).then(
|
|
218
|
+
() => { lifecycle.cleanupFailures.delete(session) },
|
|
219
|
+
(error: unknown) => {
|
|
220
|
+
lifecycle.cleanupFailures.set(session, error)
|
|
221
|
+
// Cleanup must not replace an already-durable booking outcome. Keep the
|
|
222
|
+
// failure machine-readable and free of task/prompt/PII values. Session
|
|
223
|
+
// close promises are allowed to be sticky, so shutdown reports the
|
|
224
|
+
// original failure rather than retrying and laundering it into success.
|
|
225
|
+
console.error(JSON.stringify({ code: 'PLANNER_SESSION_CLOSE_FAILED' }))
|
|
226
|
+
},
|
|
227
|
+
)
|
|
228
|
+
lifecycle.closing.add(cleanup)
|
|
229
|
+
void cleanup.then(() => lifecycle.closing.delete(cleanup))
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
/** Detach a session from the request path and close it in the composition lifecycle. */
|
|
233
|
+
function releasePlannerSession(composition: BookingCopilotComposition, taskId: string): void {
|
|
234
|
+
const sessions = sessionsFor(composition)
|
|
235
|
+
const session = sessions.get(taskId)
|
|
236
|
+
if (!session) return
|
|
237
|
+
sessions.delete(taskId)
|
|
238
|
+
schedulePlannerSessionClose(composition, session)
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
async function drainPlannerSessions(composition: BookingCopilotComposition): Promise<void> {
|
|
242
|
+
const lifecycle = lifecycleFor(composition)
|
|
243
|
+
for (const [taskId, session] of lifecycle.active) {
|
|
244
|
+
lifecycle.active.delete(taskId)
|
|
245
|
+
schedulePlannerSessionClose(composition, session)
|
|
246
|
+
}
|
|
247
|
+
while (lifecycle.closing.size > 0) {
|
|
248
|
+
await Promise.all([...lifecycle.closing])
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
if (lifecycle.cleanupFailures.size > 0) {
|
|
252
|
+
throw new AggregateError([...lifecycle.cleanupFailures.values()], 'planner session cleanup failed during shutdown')
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
async function drainDecisionFlights(composition: BookingCopilotComposition): Promise<void> {
|
|
257
|
+
const flights = decisionFlightsFor(composition)
|
|
258
|
+
while (flights.size > 0) {
|
|
259
|
+
await Promise.allSettled([...flights.values()])
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
/**
|
|
264
|
+
* Stop admitting turns and drain every planner resource owned by a composition.
|
|
265
|
+
* This is shared by the standalone listener and the unified gotry-backend
|
|
266
|
+
* module, whose listener is owned by the backend kernel.
|
|
267
|
+
*/
|
|
268
|
+
export function shutdownBookingCopilotTraffic(composition: BookingCopilotComposition): Promise<void> {
|
|
269
|
+
const lifecycle = lifecycleFor(composition)
|
|
270
|
+
if (lifecycle.shutdownPromise) return lifecycle.shutdownPromise
|
|
271
|
+
lifecycle.shuttingDown = true
|
|
272
|
+
lifecycle.shutdownPromise = (async () => {
|
|
273
|
+
await drainAcceptedRequests(composition)
|
|
274
|
+
await drainDecisionFlights(composition)
|
|
275
|
+
await drainPlannerSessions(composition)
|
|
276
|
+
})()
|
|
277
|
+
return lifecycle.shutdownPromise
|
|
278
|
+
}
|
|
279
|
+
function decisionFlightsFor(composition: BookingCopilotComposition): Map<string, Promise<BookingSurfaceEvent[]>> {
|
|
280
|
+
let flights = decisionFlightsByComposition.get(composition)
|
|
281
|
+
if (!flights) { flights = new Map(); decisionFlightsByComposition.set(composition, flights) }
|
|
282
|
+
return flights
|
|
283
|
+
}
|
|
284
|
+
async function runDecisionSingleFlight(composition: BookingCopilotComposition, key: string, work: () => Promise<BookingSurfaceEvent[]>): Promise<BookingSurfaceEvent[]> {
|
|
285
|
+
const flights = decisionFlightsFor(composition)
|
|
286
|
+
const prior = flights.get(key)
|
|
287
|
+
if (prior) return prior
|
|
288
|
+
const current = work()
|
|
289
|
+
flights.set(key, current)
|
|
290
|
+
try { return await current } finally { if (flights.get(key) === current) flights.delete(key) }
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
export function runtimeIdentity(): BookingCopilotRuntimeIdentity {
|
|
294
|
+
const report = process.report?.getReport?.() as { header?: { glibcVersionRuntime?: string } } | undefined
|
|
295
|
+
const header = report?.header
|
|
296
|
+
const glibcVersion = header?.glibcVersionRuntime ?? ''
|
|
297
|
+
return {
|
|
298
|
+
nodeVersion: process.version,
|
|
299
|
+
nodeModulesAbi: process.versions.modules ?? '',
|
|
300
|
+
releaseTuple: `${process.platform}-${process.arch}-${glibcVersion ? 'glibc' : 'unknown'}`,
|
|
301
|
+
glibcVersion,
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
function safeSecretEqual(actual: string, expected: string): boolean {
|
|
306
|
+
const actualBytes = Buffer.from(actual)
|
|
307
|
+
const expectedBytes = Buffer.from(expected)
|
|
308
|
+
return actualBytes.length === expectedBytes.length && timingSafeEqual(actualBytes, expectedBytes)
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
function sendJson(res: ServerResponse, status: number, body: unknown): void {
|
|
312
|
+
res.writeHead(status, { 'content-type': 'application/json; charset=utf-8' })
|
|
313
|
+
res.end(JSON.stringify(body))
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
function readBody(req: IncomingMessage, maxBodyBytes: number): Promise<string> {
|
|
317
|
+
return new Promise((resolve, reject) => {
|
|
318
|
+
const chunks: Buffer[] = []
|
|
319
|
+
let size = 0
|
|
320
|
+
let exceeded = false
|
|
321
|
+
req.on('data', (chunk: Buffer) => {
|
|
322
|
+
size += chunk.length
|
|
323
|
+
if (size > maxBodyBytes) {
|
|
324
|
+
exceeded = true
|
|
325
|
+
return
|
|
326
|
+
}
|
|
327
|
+
chunks.push(chunk)
|
|
328
|
+
})
|
|
329
|
+
req.on('end', () => exceeded ? reject(new Error('payload_too_large')) : resolve(Buffer.concat(chunks).toString('utf8')))
|
|
330
|
+
req.on('error', reject)
|
|
331
|
+
})
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
function write(res: ServerResponse, event: BookingSurfaceEvent): void {
|
|
335
|
+
res.write(`event: ${event.kind}\ndata: ${JSON.stringify(event)}\n\n`)
|
|
336
|
+
res.flushHeaders()
|
|
337
|
+
res.socket?.setNoDelay(true)
|
|
338
|
+
res.socket?.uncork()
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
function flush(res: ServerResponse): void {
|
|
342
|
+
res.flushHeaders()
|
|
343
|
+
const maybeFlush = res as ServerResponse & { flush?: () => void }
|
|
344
|
+
maybeFlush.flush?.()
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
function sameCapabilities(a: readonly string[], b: readonly string[]): boolean {
|
|
348
|
+
return a.length === b.length && [...a].sort().every((kind, index) => kind === [...b].sort()[index])
|
|
349
|
+
}
|
|
350
|
+
function exactKeys(value: Record<string, unknown>, allowed: readonly string[]): boolean {
|
|
351
|
+
return Object.keys(value).length === allowed.length && Object.keys(value).every((key) => allowed.includes(key))
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
const SURFACE_ACTION_MATRIX = BOOKING_SURFACE_ALLOWED_ACTIONS
|
|
355
|
+
|
|
356
|
+
/**
|
|
357
|
+
* The authenticated BFF sends the fresh fleet-wide feature intersection in
|
|
358
|
+
* the same header that GoTry advertises on probes. Missing, malformed, and
|
|
359
|
+
* unknown values fail closed to the empty optional-feature set.
|
|
360
|
+
*/
|
|
361
|
+
function negotiatedBookingSurfaceFeatures(req: IncomingMessage): BookingSurfaceFeature[] {
|
|
362
|
+
const raw = req.headers[BOOKING_SURFACE_FEATURES_HEADER]
|
|
363
|
+
if (typeof raw !== 'string' || raw.trim() === '') return []
|
|
364
|
+
const values = new Set(raw.split(',').map((value) => value.trim()).filter(Boolean))
|
|
365
|
+
return BOOKING_SURFACE_FEATURES.filter((feature) => values.has(feature))
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
function validSurfaceActions(surface: unknown, allowed: unknown): allowed is BookingWorkspaceSnapshot['capabilities']['allowedActions'] {
|
|
369
|
+
if (typeof surface !== 'string' || !Object.prototype.hasOwnProperty.call(SURFACE_ACTION_MATRIX, surface)) return false
|
|
370
|
+
if (!Array.isArray(allowed) || allowed.length < 1 || new Set(allowed).size !== allowed.length) return false
|
|
371
|
+
const matrix = SURFACE_ACTION_MATRIX[surface as BookingWorkspaceSnapshot['surface']]
|
|
372
|
+
return allowed.every((action) => typeof action === 'string'
|
|
373
|
+
&& matrix.includes(action as BookingReadActionKind)
|
|
374
|
+
&& (BOOKING_READ_ACTION_KINDS as readonly string[]).includes(action))
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
function validObservableOrders(value: unknown): value is ObservableOrderFact[] {
|
|
378
|
+
if (!Array.isArray(value) || value.length > 100) return false
|
|
379
|
+
const orderRefs = new Set<string>()
|
|
380
|
+
return value.every((item) => {
|
|
381
|
+
if (!item || typeof item !== 'object' || Array.isArray(item)) return false
|
|
382
|
+
const candidate = item as Record<string, unknown>
|
|
383
|
+
if (!exactKeys(candidate, ['orderRef', 'factRefs']) || typeof candidate.orderRef !== 'string' || !/^[A-Za-z0-9][A-Za-z0-9:._-]*$/.test(candidate.orderRef) || orderRefs.has(candidate.orderRef)) return false
|
|
384
|
+
if (!Array.isArray(candidate.factRefs) || candidate.factRefs.length > 64 || new Set(candidate.factRefs).size !== candidate.factRefs.length || candidate.factRefs.some((ref) => typeof ref !== 'string' || !/^[A-Za-z0-9][A-Za-z0-9:._-]*$/.test(ref))) return false
|
|
385
|
+
orderRefs.add(candidate.orderRef)
|
|
386
|
+
return true
|
|
387
|
+
})
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
function validBoundWorkspaceAuthority(workspace: BookingWorkspaceSnapshot): boolean {
|
|
391
|
+
return workspace.capabilities.surface === workspace.surface
|
|
392
|
+
&& validSurfaceActions(workspace.surface, workspace.capabilities.allowedActions)
|
|
393
|
+
&& (workspace.observableOrders === undefined || validObservableOrders(workspace.observableOrders))
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
/**
|
|
397
|
+
* Old compatible BFF releases may still send the base action matrix. Fresh
|
|
398
|
+
* tasks are narrowed to the negotiated optional features instead of failing
|
|
399
|
+
* the whole turn. Existing task allowlists remain stable across a rolling
|
|
400
|
+
* deploy, while unsupported trusted projections are still removed so no new
|
|
401
|
+
* optional action can gain authority.
|
|
402
|
+
*/
|
|
403
|
+
function narrowWorkspaceOptionalAuthority(
|
|
404
|
+
workspace: BookingWorkspaceSnapshot,
|
|
405
|
+
features: readonly BookingSurfaceFeature[],
|
|
406
|
+
existingTaskActions?: readonly BookingReadActionKind[],
|
|
407
|
+
): BookingWorkspaceSnapshot | null {
|
|
408
|
+
const negotiated = new Set(bookingSurfaceActionsForFeatures(workspace.surface, features))
|
|
409
|
+
if (existingTaskActions?.some((action) => !workspace.capabilities.allowedActions.includes(action))) return null
|
|
410
|
+
const allowedActions = existingTaskActions
|
|
411
|
+
? [...existingTaskActions]
|
|
412
|
+
: workspace.capabilities.allowedActions.filter((action) => negotiated.has(action))
|
|
413
|
+
if (allowedActions.length === 0) return null
|
|
414
|
+
const { observableOrders, ...base } = workspace
|
|
415
|
+
return {
|
|
416
|
+
...base,
|
|
417
|
+
capabilities: { surface: workspace.surface, allowedActions },
|
|
418
|
+
...(features.includes('trusted-order-observation-v1') && observableOrders !== undefined
|
|
419
|
+
? { observableOrders: observableOrders.map((order) => ({ orderRef: order.orderRef, factRefs: [...order.factRefs] })) }
|
|
420
|
+
: {}),
|
|
421
|
+
}
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
function validIngressBinding(binding: unknown, ingress: IngressTurn): binding is {
|
|
425
|
+
taskId: string
|
|
426
|
+
turnId: string
|
|
427
|
+
contextRef: string
|
|
428
|
+
surface: BookingWorkspaceSnapshot['surface']
|
|
429
|
+
allowedActions: BookingWorkspaceSnapshot['capabilities']['allowedActions']
|
|
430
|
+
observableOrders?: ObservableOrderFact[]
|
|
431
|
+
} {
|
|
432
|
+
if (!binding || typeof binding !== 'object' || Array.isArray(binding)) return false
|
|
433
|
+
const candidate = binding as Record<string, unknown>
|
|
434
|
+
if (!exactKeys(candidate, ['taskId', 'turnId', 'contextRef', 'surface', 'allowedActions'])
|
|
435
|
+
&& !exactKeys(candidate, ['taskId', 'turnId', 'contextRef', 'surface', 'allowedActions', 'observableOrders'])) return false
|
|
436
|
+
if (typeof candidate.taskId !== 'string' || typeof candidate.turnId !== 'string' || typeof candidate.contextRef !== 'string') return false
|
|
437
|
+
if (typeof candidate.surface !== 'string' || candidate.surface !== ingress.surfaceHint || !Object.prototype.hasOwnProperty.call(SURFACE_ACTION_MATRIX, candidate.surface)) return false
|
|
438
|
+
return validSurfaceActions(candidate.surface, candidate.allowedActions)
|
|
439
|
+
&& (candidate.observableOrders === undefined || validObservableOrders(candidate.observableOrders))
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
function validIngressApprovalAuthority(ingress: IngressTurn, binding: { taskId: string; contextRef: string }): boolean {
|
|
443
|
+
if (!ingress.request.approval) return true
|
|
444
|
+
return ingress.request.approval.taskId === binding.taskId
|
|
445
|
+
&& ingress.request.approval.contextRef === binding.contextRef
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
function fallbackErrorEvent(task: BookingCopilotTaskState, code: string): BookingSurfaceEvent {
|
|
449
|
+
return {
|
|
450
|
+
schemaVersion: BOOKING_SURFACE_SCHEMA_VERSION,
|
|
451
|
+
eventId: `error-${task.taskId}-${task.lastSequence + 1}`,
|
|
452
|
+
taskId: task.taskId,
|
|
453
|
+
contextRef: task.contextRef,
|
|
454
|
+
sequence: task.lastSequence + 1,
|
|
455
|
+
emittedAt: new Date().toISOString(),
|
|
456
|
+
kind: 'error',
|
|
457
|
+
error: { code: normalizeBookingErrorCode(code), message: safeBookingErrorMessage(normalizeBookingErrorCode(code)), retryable: false },
|
|
458
|
+
}
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
function writeTypedError(res: ServerResponse, composition: BookingCopilotComposition, task: BookingCopilotTaskState, error: unknown, requestKey?: string, includeSubmitted = false): void {
|
|
462
|
+
const code = normalizeBookingErrorCode(error)
|
|
463
|
+
// Provider/adapter exceptions can contain response bodies, URLs or request
|
|
464
|
+
// context. Preserve only the closed code and coarse thrown-value class.
|
|
465
|
+
console.error('[booking-copilot] planner boundary error:', JSON.stringify({
|
|
466
|
+
code,
|
|
467
|
+
thrownType: error instanceof Error ? 'error' : typeof error,
|
|
468
|
+
}))
|
|
469
|
+
try {
|
|
470
|
+
const decision = { kind: 'error' as const, error: { code, message: safeBookingErrorMessage(code), retryable: false } }
|
|
471
|
+
if (requestKey) {
|
|
472
|
+
const events = composition.runtime.applyDecisionBatch(task.taskId, requestKey, [decision], includeSubmitted, { suppressProgressStatuses: true })
|
|
473
|
+
for (const event of events) write(res, event)
|
|
474
|
+
return
|
|
475
|
+
}
|
|
476
|
+
write(res, composition.runtime.emitEvent(task.taskId, decision))
|
|
477
|
+
} catch {
|
|
478
|
+
// The stream is already committed. Emit a schema-valid, non-durable
|
|
479
|
+
// typed envelope instead of silently returning an empty HTTP 200 stream.
|
|
480
|
+
write(res, fallbackErrorEvent(task, code))
|
|
481
|
+
}
|
|
482
|
+
}
|
|
483
|
+
|
|
484
|
+
/** Handles one already-authenticated request inside the sole HTTP listener. */
|
|
485
|
+
async function handleBookingCopilotRequest(
|
|
486
|
+
req: IncomingMessage,
|
|
487
|
+
res: ServerResponse,
|
|
488
|
+
composition: BookingCopilotComposition,
|
|
489
|
+
maxBodyBytes: number,
|
|
490
|
+
negotiatedFeatures: readonly BookingSurfaceFeature[],
|
|
491
|
+
): Promise<void> {
|
|
492
|
+
let turn: Exclude<BookingCopilotTurn, IngressTurn>
|
|
493
|
+
let browserRequestKey: string | undefined
|
|
494
|
+
let requestBinding: BookingIngressRequestBindingInput | undefined
|
|
495
|
+
let authorityFailureStatus = 403
|
|
496
|
+
try {
|
|
497
|
+
const parsed = JSON.parse(await readBody(req, maxBodyBytes)) as unknown
|
|
498
|
+
const valid = validateBookingSurface(parsed)
|
|
499
|
+
if (!valid.ok) { sendJson(res, 400, { error: { code: 'invalid_booking_surface_turn', details: valid.errors } }); return }
|
|
500
|
+
if ((parsed as BookingCopilotTurn).kind === 'user.turn.ingress') {
|
|
501
|
+
const ingress = parsed as IngressTurn
|
|
502
|
+
browserRequestKey = ingress.requestKey
|
|
503
|
+
if (composition.ingressMode !== 'bff-ingress-binding' || !composition.ingressBinding || !composition.principal) {
|
|
504
|
+
sendJson(res, 503, { error: { code: 'trusted_ingress_binding_required', mode: composition.ingressMode ?? 'bff-bound-turn-only', acceptedTurnKinds: [...BOOKING_COPILOT_ACCEPTED_TURN_KINDS] } })
|
|
505
|
+
return
|
|
506
|
+
}
|
|
507
|
+
const binding = await composition.ingressBinding.bind(ingress, composition.principal)
|
|
508
|
+
if (!validIngressBinding(binding, ingress)) { sendJson(res, 502, { error: { code: 'invalid_ingress_binding' } }); return }
|
|
509
|
+
if (!validIngressApprovalAuthority(ingress, binding)) { sendJson(res, 400, { error: { code: 'invalid_ingress_approval_authority' } }); return }
|
|
510
|
+
const proposedBoundWorkspace = {
|
|
511
|
+
...ingress.workspace,
|
|
512
|
+
contextRef: binding.contextRef,
|
|
513
|
+
surface: binding.surface,
|
|
514
|
+
capabilities: { surface: binding.surface, allowedActions: [...binding.allowedActions] },
|
|
515
|
+
...(binding.observableOrders !== undefined ? { observableOrders: binding.observableOrders.map((order) => ({ orderRef: order.orderRef, factRefs: [...order.factRefs] })) } : {}),
|
|
516
|
+
} as BookingWorkspaceSnapshot
|
|
517
|
+
const boundRequest = ingress.request.approval
|
|
518
|
+
? { text: ingress.request.text, approval: { ...ingress.request.approval } }
|
|
519
|
+
: { text: ingress.request.text }
|
|
520
|
+
turn = { schemaVersion: BOOKING_SURFACE_SCHEMA_VERSION, kind: 'user.turn', taskId: binding.taskId, turnId: binding.turnId, workspace: proposedBoundWorkspace, request: boundRequest }
|
|
521
|
+
requestBinding = { requestKey: ingress.requestKey, principal: composition.principal, ...(ingress.taskHandle ? { taskHandle: ingress.taskHandle } : {}) }
|
|
522
|
+
authorityFailureStatus = 502
|
|
523
|
+
const boundValid = validateBookingSurface(turn)
|
|
524
|
+
if (!boundValid.ok) { sendJson(res, 502, { error: { code: 'invalid_ingress_binding', details: boundValid.errors } }); return }
|
|
525
|
+
} else {
|
|
526
|
+
const boundTurn = parsed as Exclude<BookingCopilotTurn, IngressTurn>
|
|
527
|
+
if (!validBoundWorkspaceAuthority(boundTurn.workspace)) {
|
|
528
|
+
sendJson(res, 403, { error: { code: 'invalid_bound_turn_authority' } })
|
|
529
|
+
return
|
|
530
|
+
}
|
|
531
|
+
turn = boundTurn
|
|
532
|
+
}
|
|
533
|
+
} catch (error) { const code = normalizeBookingErrorCode(error); sendJson(res, code === 'PAYLOAD_TOO_LARGE' ? 413 : 400, { error: { code } }); return }
|
|
534
|
+
|
|
535
|
+
let task: BookingCopilotTaskState; let fresh = false; let replayKey: string | undefined; let replayEvents: BookingSurfaceEvent[] | null = null; let activeDecisionKey: string | undefined
|
|
536
|
+
try {
|
|
537
|
+
const existingAtAdmission = turn.taskId ? composition.runtime.resumeTask(turn.taskId) : null
|
|
538
|
+
const narrowedWorkspace = narrowWorkspaceOptionalAuthority(turn.workspace, negotiatedFeatures, existingAtAdmission?.allowedActions)
|
|
539
|
+
if (!narrowedWorkspace) {
|
|
540
|
+
const code = authorityFailureStatus === 502 ? 'invalid_ingress_binding' : 'invalid_bound_turn_authority'
|
|
541
|
+
sendJson(res, authorityFailureStatus, { error: { code } })
|
|
542
|
+
return
|
|
543
|
+
}
|
|
544
|
+
turn = { ...turn, workspace: narrowedWorkspace }
|
|
545
|
+
fresh = turn.kind === 'action.receipt.continuation' ? false : existingAtAdmission === null
|
|
546
|
+
if (turn.kind === 'action.receipt.continuation') {
|
|
547
|
+
replayKey = `receipt:${turn.receipt.actionId}:${composition.runtime.receiptDigest(turn.receipt)}`
|
|
548
|
+
task = composition.runtime.continueWithReceipt(turn)
|
|
549
|
+
if (task.awaitingApproval) {
|
|
550
|
+
replayKey = composition.runtime.approvalPresentationRequestKey(turn.taskId)
|
|
551
|
+
}
|
|
552
|
+
replayEvents = composition.runtime.readDecisionBatch(turn.taskId, replayKey)
|
|
553
|
+
if (!replayEvents && task.phase === 'terminal') replayEvents = composition.runtime.terminalDecisionBatch(turn.taskId, replayKey)
|
|
554
|
+
} else {
|
|
555
|
+
const existing = existingAtAdmission
|
|
556
|
+
if (requestBinding && turn.kind === 'user.turn') composition.runtime.assertRequestBinding(browserRequestKey!, turn, requestBinding)
|
|
557
|
+
const requestReplayKey = existing && browserRequestKey ? `ingress:${existing.taskId}:${browserRequestKey}` : undefined
|
|
558
|
+
const directReplayKey = existing && turn.kind === 'user.turn' && turn.turnId ? `turn:${existing.taskId}:${turn.turnId}` : undefined
|
|
559
|
+
const stableReplayKey = requestReplayKey ?? directReplayKey
|
|
560
|
+
const stableReplay = stableReplayKey ? composition.runtime.readDecisionBatch(existing!.taskId, stableReplayKey) : null
|
|
561
|
+
if (stableReplay) {
|
|
562
|
+
if (existing && turn.kind === 'user.turn' && directReplayKey) composition.runtime.assertTurnBinding(existing.taskId, turn)
|
|
563
|
+
task = existing!
|
|
564
|
+
replayKey = stableReplayKey
|
|
565
|
+
replayEvents = stableReplay
|
|
566
|
+
} else if (existing?.phase === 'terminal' || existing?.phase === 'error') {
|
|
567
|
+
throw new Error('task_terminal')
|
|
568
|
+
} else if (existing?.phase === 'waiting_receipt') {
|
|
569
|
+
if (turn.kind === 'user.turn' && turn.request.approval) composition.runtime.assertApprovalNotConsumed(existing.taskId, turn.request.approval)
|
|
570
|
+
const workspace = turn.workspace as BookingWorkspaceSnapshot
|
|
571
|
+
if (workspace.contextRef !== existing.contextRef || workspace.surface !== existing.surface || workspace.revision !== existing.revision || workspace.capabilities.surface !== existing.surface || !sameCapabilities(workspace.capabilities.allowedActions, existing.allowedActions)) throw new Error('task_conflict:workspace_mismatch')
|
|
572
|
+
// A waiting task can only be replayed with the caller's stable turn
|
|
573
|
+
// identity. Without it, an identical sentence is not distinguishable
|
|
574
|
+
// from a new user turn and must not receive an old operation batch.
|
|
575
|
+
if (!turn.turnId || existing.lastTurnId !== turn.turnId) throw new Error('receipt_required')
|
|
576
|
+
replayKey = browserRequestKey ? `ingress:${existing.taskId}:${browserRequestKey}` : `turn:${existing.taskId}:${existing.lastTurnId ?? existing.userTurnCount}`
|
|
577
|
+
replayEvents = composition.runtime.readDecisionBatch(existing.taskId, replayKey)
|
|
578
|
+
if (!replayEvents) throw new Error('receipt_required')
|
|
579
|
+
task = existing
|
|
580
|
+
} else {
|
|
581
|
+
if (existing && turn.kind === 'user.turn' && turn.request.approval) composition.runtime.assertApprovalNotConsumed(existing.taskId, turn.request.approval)
|
|
582
|
+
task = composition.runtime.startTask(turn, requestBinding)
|
|
583
|
+
if (turn.kind === 'user.turn' && turn.taskId) {
|
|
584
|
+
replayKey = browserRequestKey ? `ingress:${task.taskId}:${browserRequestKey}` : `turn:${task.taskId}:${turn.turnId ?? task.userTurnCount}`
|
|
585
|
+
replayEvents = composition.runtime.readDecisionBatch(task.taskId, replayKey)
|
|
586
|
+
}
|
|
587
|
+
}
|
|
588
|
+
}
|
|
589
|
+
} catch (error) {
|
|
590
|
+
const code = normalizeBookingErrorCode(error)
|
|
591
|
+
console.error(JSON.stringify({ code, reason: extractBookingDispatchReason(error) }))
|
|
592
|
+
sendJson(res, 409, { error: { code } }); return
|
|
593
|
+
}
|
|
594
|
+
|
|
595
|
+
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 })
|
|
596
|
+
res.flushHeaders()
|
|
597
|
+
try {
|
|
598
|
+
if (replayEvents) {
|
|
599
|
+
const progressEvents = composition.runtime.readProgressBatch(task.taskId, replayKey ?? `turn:${task.taskId}:${task.userTurnCount}`) ?? []
|
|
600
|
+
for (const event of progressEvents) write(res, event)
|
|
601
|
+
if (progressEvents.length) {
|
|
602
|
+
flush(res)
|
|
603
|
+
await new Promise<void>((resolve) => setImmediate(resolve))
|
|
604
|
+
}
|
|
605
|
+
if (task.phase === 'terminal' || task.phase === 'error') releasePlannerSession(composition, task.taskId)
|
|
606
|
+
for (const event of replayEvents) write(res, event)
|
|
607
|
+
} else {
|
|
608
|
+
const requestKey = replayKey ?? `turn:${task.taskId}:${task.userTurnCount}`
|
|
609
|
+
activeDecisionKey = requestKey
|
|
610
|
+
// A concurrent identical request may arrive after the first request has
|
|
611
|
+
// committed progress but before its planner flight finishes. Re-serve
|
|
612
|
+
// the durable progress batch so it receives the same prefix instead of
|
|
613
|
+
// exposing a terminal-only tail.
|
|
614
|
+
const newlyPersistedProgress = composition.runtime.ensureProgressBatch(task.taskId, requestKey, fresh)
|
|
615
|
+
const progressEvents = newlyPersistedProgress.length > 0
|
|
616
|
+
? newlyPersistedProgress
|
|
617
|
+
: (composition.runtime.readProgressBatch(task.taskId, requestKey) ?? [])
|
|
618
|
+
const approval = turn.kind === 'user.turn' ? turn.request.approval : undefined
|
|
619
|
+
const flightKey = JSON.stringify([task.taskId, approval ? `approval:${bookingDigest(approval)}` : requestKey])
|
|
620
|
+
// Register or join the decision flight before yielding after the progress
|
|
621
|
+
// flush. A duplicate that observed the durable progress must never miss
|
|
622
|
+
// an about-to-finish flight and pay for a second model invocation.
|
|
623
|
+
const decisionOutcome = runDecisionSingleFlight(composition, flightKey, async () => {
|
|
624
|
+
// Cover the complementary late-join case: the prior flight may have
|
|
625
|
+
// committed its exact batch and left the in-memory map between request
|
|
626
|
+
// admission and this work starting.
|
|
627
|
+
const durableDecision = composition.runtime.readDecisionBatch(task.taskId, requestKey)
|
|
628
|
+
if (durableDecision) return durableDecision
|
|
629
|
+
if (turn.kind === 'action.receipt.continuation' && task.awaitingApproval) {
|
|
630
|
+
return composition.runtime.applyDecisionBatch(task.taskId, requestKey, [composition.runtime.approvalQuestion(task.taskId)], false)
|
|
631
|
+
}
|
|
632
|
+
const sessions = sessionsFor(composition)
|
|
633
|
+
const session = sessions.get(task.taskId) ?? composition.plannerFactory(task)
|
|
634
|
+
sessions.set(task.taskId, session)
|
|
635
|
+
const decisions = await session.next({ turn, task: composition.runtime.resumeTask(task.taskId) ?? task })
|
|
636
|
+
const plannerDecisions = decisions.length ? decisions : [{ kind: 'error' as const, error: { code: 'PLANNER_NO_DECISION', message: 'Planner returned no typed decision.', retryable: false } }]
|
|
637
|
+
const events = composition.runtime.applyDecisionBatch(task.taskId, requestKey, plannerDecisions, fresh, { suppressProgressStatuses: true })
|
|
638
|
+
const updated = composition.runtime.resumeTask(task.taskId)
|
|
639
|
+
if (updated?.phase === 'terminal' || updated?.phase === 'error') releasePlannerSession(composition, task.taskId)
|
|
640
|
+
return events
|
|
641
|
+
}).then(
|
|
642
|
+
(events) => ({ ok: true as const, events }),
|
|
643
|
+
(error: unknown) => ({ ok: false as const, error }),
|
|
644
|
+
)
|
|
645
|
+
for (const event of progressEvents) write(res, event)
|
|
646
|
+
if (progressEvents.length) {
|
|
647
|
+
flush(res)
|
|
648
|
+
await new Promise<void>((resolve) => setImmediate(resolve))
|
|
649
|
+
}
|
|
650
|
+
const outcome = await decisionOutcome
|
|
651
|
+
if (!outcome.ok) throw outcome.error
|
|
652
|
+
for (const event of outcome.events) write(res, event)
|
|
653
|
+
}
|
|
654
|
+
} catch (error) {
|
|
655
|
+
writeTypedError(res, composition, task, error, activeDecisionKey, fresh)
|
|
656
|
+
const updated = composition.runtime.resumeTask(task.taskId)
|
|
657
|
+
if (updated?.phase === 'terminal' || updated?.phase === 'error') releasePlannerSession(composition, task.taskId)
|
|
658
|
+
} finally { res.end() }
|
|
659
|
+
}
|
|
660
|
+
|
|
661
|
+
export function startBookingCopilotServer(options: BookingCopilotServerOptions): Promise<BookingCopilotServerHandle> {
|
|
662
|
+
if (!options.apiKey) return Promise.reject(new Error('booking_copilot_api_key_required'))
|
|
663
|
+
if (options.artifactId !== undefined && !/^[0-9a-f]{40}$/.test(options.artifactId)) {
|
|
664
|
+
return Promise.reject(new Error('booking_copilot_artifact_id_invalid'))
|
|
665
|
+
}
|
|
666
|
+
if (!options.runtime || !options.plannerFactory) {
|
|
667
|
+
return Promise.reject(new Error('booking_copilot_runtime_and_planner_required'))
|
|
668
|
+
}
|
|
669
|
+
const hasIngressBinding = Boolean(options.ingressBinding)
|
|
670
|
+
const hasPrincipal = Boolean(options.principal)
|
|
671
|
+
if (hasIngressBinding !== hasPrincipal) {
|
|
672
|
+
return Promise.reject(new Error('booking_copilot_ingress_binding_pair_required'))
|
|
673
|
+
}
|
|
674
|
+
if (hasIngressBinding && (typeof options.ingressBinding?.bind !== 'function'
|
|
675
|
+
|| typeof options.principal?.subject !== 'string' || options.principal.subject.length === 0 || options.principal.subject.length > 256
|
|
676
|
+
|| typeof options.principal?.scope !== 'string' || options.principal.scope.length === 0 || options.principal.scope.length > 256)) {
|
|
677
|
+
return Promise.reject(new Error('booking_copilot_ingress_binding_invalid'))
|
|
678
|
+
}
|
|
679
|
+
const ingressMode: BookingCopilotIngressMode = options.ingressMode ?? (hasIngressBinding ? 'bff-ingress-binding' : 'bff-bound-turn-only')
|
|
680
|
+
if (!BOOKING_COPILOT_INGRESS_MODES.includes(ingressMode)) {
|
|
681
|
+
return Promise.reject(new Error('booking_copilot_ingress_mode_invalid'))
|
|
682
|
+
}
|
|
683
|
+
if (ingressMode === 'bff-ingress-binding' && !hasIngressBinding) {
|
|
684
|
+
return Promise.reject(new Error('booking_copilot_ingress_binding_required'))
|
|
685
|
+
}
|
|
686
|
+
if (ingressMode === 'bff-bound-turn-only' && hasIngressBinding) {
|
|
687
|
+
return Promise.reject(new Error('booking_copilot_ingress_mode_conflict'))
|
|
688
|
+
}
|
|
689
|
+
const composition: BookingCopilotComposition = {
|
|
690
|
+
runtime: options.runtime,
|
|
691
|
+
plannerFactory: options.plannerFactory,
|
|
692
|
+
...(hasIngressBinding ? { ingressBinding: options.ingressBinding, principal: options.principal } : {}),
|
|
693
|
+
ingressMode,
|
|
694
|
+
}
|
|
695
|
+
const maxBodyBytes = options.maxBodyBytes ?? 1_000_000
|
|
696
|
+
const runningIdentity = runtimeIdentity()
|
|
697
|
+
|
|
698
|
+
const server = createServer((req, res) => {
|
|
699
|
+
void bookingCopilotTrafficHandler({
|
|
700
|
+
apiKey: options.apiKey,
|
|
701
|
+
composition,
|
|
702
|
+
maxBodyBytes,
|
|
703
|
+
...(options.artifactId !== undefined ? { artifactId: options.artifactId } : {}),
|
|
704
|
+
ingressMode,
|
|
705
|
+
runningIdentity,
|
|
706
|
+
})(req, res)
|
|
707
|
+
})
|
|
708
|
+
let closePromise: Promise<void> | undefined
|
|
709
|
+
|
|
710
|
+
return new Promise((resolve, reject) => {
|
|
711
|
+
server.once('error', reject)
|
|
712
|
+
server.listen(options.port ?? 0, options.host ?? '127.0.0.1', () => {
|
|
713
|
+
server.off('error', reject)
|
|
714
|
+
const address = server.address()
|
|
715
|
+
const port = typeof address === 'object' && address ? address.port : (options.port ?? 0)
|
|
716
|
+
resolve({
|
|
717
|
+
server,
|
|
718
|
+
port,
|
|
719
|
+
close: () => {
|
|
720
|
+
if (closePromise) return closePromise
|
|
721
|
+
const trafficShutdown = shutdownBookingCopilotTraffic(composition)
|
|
722
|
+
closePromise = (async () => {
|
|
723
|
+
const listenerShutdown = new Promise<void>((done, fail) => server.close((error) => error ? fail(error) : done()))
|
|
724
|
+
const results = await Promise.allSettled([listenerShutdown, trafficShutdown])
|
|
725
|
+
const failures = results.flatMap((result) => result.status === 'rejected' ? [result.reason] : [])
|
|
726
|
+
if (failures.length > 0) throw new AggregateError(failures, 'booking_copilot_server_close_failed')
|
|
727
|
+
})()
|
|
728
|
+
return closePromise
|
|
729
|
+
},
|
|
730
|
+
})
|
|
731
|
+
})
|
|
732
|
+
})
|
|
733
|
+
}
|