@danceiny/gotry 0.0.1-rc.18 → 0.0.1-rc.19
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 +42 -16
- package/README.zh-CN.md +37 -15
- package/bin/gotry-bootstrap.js +236 -3
- package/bin/gotry-inner.js +66 -20
- package/bin/gotry-runtime-resolution.d.ts +1 -1
- package/bin/gotry-runtime-resolution.js +7 -8
- package/cordis.gotry-patch.yml +15 -5
- package/data/stations-12306-verify.json +524 -0
- package/dist/capabilities/agent-reach.js +4 -4
- package/dist/capabilities/channel-health.js +103 -0
- package/dist/capabilities/channel-registry.js +213 -0
- package/dist/capabilities/doctor.js +285 -0
- package/dist/capabilities/effect.js +178 -5
- package/dist/capabilities/flyai.js +13 -0
- package/dist/capabilities/session/adapters/ctrip-hotel.js +207 -0
- package/dist/capabilities/session/adapters/rail-12306.js +285 -0
- package/dist/capabilities/session/extension-bridge.js +14 -1
- package/dist/capabilities/session/extension-channel.js +4 -2
- package/dist/capabilities/session/extension-distribution.js +1 -1
- package/dist/capabilities/session-search.js +311 -0
- package/dist/capabilities/visa-policy.js +126 -0
- package/dist/scripts/agent-planning-turn-deadline-e2e.js +304 -0
- package/dist/scripts/agent-planning-turn-deadline-tests.js +281 -0
- package/dist/scripts/benchmark-environment-bridge-e2e.js +5 -10
- package/dist/scripts/benchmark-environment-bridge-tests.js +148 -142
- package/dist/scripts/booking-copilot-availability-ledger-binding-tests.js +26 -26
- package/dist/scripts/{booking-copilot-availability-policy-v2-tests.js → booking-copilot-availability-policy-tests.js} +115 -115
- package/dist/scripts/booking-copilot-crossrepo-fixture-server.js +41 -1
- package/dist/scripts/booking-copilot-dsh-core-proof-tests.js +75 -120
- package/dist/scripts/booking-copilot-dsh-planner-proof-tests.js +370 -61
- package/dist/scripts/booking-copilot-dsh-plugin-proof-tests.js +2 -2
- package/dist/scripts/booking-copilot-event-sequence-concurrency-proof-tests.js +7 -3
- package/dist/scripts/booking-copilot-gap-code-contract-proof-tests.js +15 -9
- package/dist/scripts/booking-copilot-operation-ledger-concurrency-proof-tests.js +19 -11
- package/dist/scripts/booking-copilot-receipt-ledger-concurrency-proof-tests.js +16 -52
- package/dist/scripts/booking-copilot-runtime-proof-tests.js +3953 -155
- package/dist/scripts/booking-copilot-server-proof-tests.js +58 -23
- package/dist/scripts/booking-copilot-startup-proof-tests.js +10 -119
- package/dist/scripts/booking-surface-contract-proof-tests.js +1154 -326
- package/dist/scripts/bootstrap-tests.js +87 -2
- package/dist/scripts/build-metrics-report.js +339 -0
- package/dist/scripts/channel-probe-tests.js +148 -0
- package/dist/scripts/channel-probe.js +252 -0
- package/dist/scripts/channel-registry-tests.js +262 -0
- package/dist/scripts/doctor-tests.js +121 -0
- package/dist/scripts/effect-tests.js +274 -1
- package/dist/scripts/extension-tests.js +129 -11
- package/dist/scripts/fact-gate-tests.js +91 -2
- package/dist/scripts/flyai-tests.js +17 -1
- package/dist/scripts/hbcli-e2e-tests.js +1 -3
- package/dist/scripts/metrics-report-tests.js +288 -0
- package/dist/scripts/persona-surface-guard-tests.js +19 -0
- package/dist/scripts/session-tests.js +401 -1
- package/dist/scripts/smoke.js +100 -83
- package/dist/scripts/turn-handoff-collect-tests.js +216 -0
- package/dist/scripts/turn-handoff-collect.js +189 -0
- package/dist/scripts/turn-policy-tests.js +56 -0
- package/dist/scripts/typed-contract-canary.js +261 -0
- package/dist/scripts/visa-policy-tests.js +75 -0
- package/dist/scripts/wish-channel-gate-tests.js +111 -0
- package/dist/src/artifact-gate.js +91 -3
- package/dist/src/benchmark-agent-conformance.js +7 -4
- package/dist/src/benchmark-environment-bridge.js +68 -7
- package/dist/src/bookable-facts.js +62 -3
- package/dist/src/booking-surface/{availability-policy-v2.js → availability-policy.js} +89 -89
- package/dist/src/booking-surface/canonical-schema.js +8 -22
- package/dist/src/booking-surface/contracts.js +70 -19
- package/dist/src/booking-surface/dsh-planner.js +310 -177
- package/dist/src/booking-surface/dsh-plugin.js +3 -3
- package/dist/src/booking-surface/index.js +1 -5
- package/dist/src/booking-surface/profile.js +1 -1
- package/dist/src/booking-surface/runtime.js +1682 -247
- package/dist/src/booking-surface/server.js +371 -185
- package/dist/src/booking-surface/startup.js +17 -27
- package/dist/src/booking-surface/validation.js +270 -760
- package/dist/src/index.js +735 -149
- package/dist/src/turn-deadline.js +292 -0
- package/dist/src/turn-policy.js +146 -0
- package/dist/src/wish-pool.js +5 -0
- package/extension/background.js +23 -4
- package/extension/content-main.js +47 -9
- package/extension/manifest.json +27 -9
- package/package.json +6 -26
- package/schemas/booking.surface.schema.json +2593 -0
- package/ts/capabilities/agent-reach.ts +4 -4
- package/ts/capabilities/flyai.ts +21 -3
- package/ts/capabilities/session/action-cache.ts +1 -1
- package/ts/capabilities/session/adapters/ctrip-hotel.ts +238 -0
- package/ts/capabilities/session/adapters/rail-12306.ts +293 -0
- package/ts/capabilities/session/extension-bridge.ts +32 -2
- package/ts/capabilities/session/extension-channel.ts +9 -8
- package/ts/capabilities/session/extension-distribution.ts +3 -1
- package/ts/capabilities/session/health-watch.ts +1 -1
- package/ts/capabilities/session/read-guard.ts +1 -1
- package/ts/capabilities/session/wizard.ts +1 -1
- package/ts/capabilities/session-search.ts +323 -1
- package/ts/package.json +1 -0
- package/ts/scripts/turn-handoff-collect.ts +178 -0
- package/ts/src/artifact-gate.ts +82 -2
- package/ts/src/benchmark-agent-conformance.ts +14 -6
- package/ts/src/benchmark-environment-bridge.ts +29 -10
- package/ts/src/bookable-facts.ts +109 -5
- package/ts/src/booking-saga.ts +1 -1
- package/ts/src/booking-surface/{availability-policy-v2.ts → availability-policy.ts} +124 -125
- package/ts/src/booking-surface/canonical-schema.js +8 -22
- package/ts/src/booking-surface/contracts.ts +278 -239
- package/ts/src/booking-surface/dsh-planner.ts +333 -194
- package/ts/src/booking-surface/dsh-plugin.js +3 -3
- package/ts/src/booking-surface/index.ts +1 -5
- package/ts/src/booking-surface/profile.ts +11 -11
- package/ts/src/booking-surface/runtime.ts +1351 -351
- package/ts/src/booking-surface/server.ts +282 -175
- package/ts/src/booking-surface/startup.ts +36 -48
- package/ts/src/booking-surface/validation.ts +194 -442
- package/ts/src/contracts.ts +1 -1
- package/ts/src/index.ts +489 -171
- package/ts/src/loop.ts +1 -1
- package/ts/src/state-ledger.ts +1 -1
- package/ts/src/turn-deadline.ts +361 -0
- package/ts/src/turn-policy.ts +154 -0
- package/ts/src/wish-pool.ts +17 -4
- package/dist/scripts/agent-planning-budget-e2e.js +0 -227
- package/dist/scripts/agent-planning-budget-tests.js +0 -173
- package/dist/scripts/booking-copilot-v2-runtime-proof-tests.js +0 -3935
- package/dist/scripts/booking-surface-v2-contract-proof-tests.js +0 -1174
- package/dist/src/booking-surface/contracts-v2.js +0 -89
- package/dist/src/booking-surface/runtime-v2.js +0 -1771
- package/dist/src/booking-surface/server-v2.js +0 -334
- package/dist/src/booking-surface/validation-v2.js +0 -319
- package/dist/src/tool-budget.js +0 -136
- package/schemas/booking.surface.v1.schema.json +0 -927
- package/schemas/booking.surface.v2.schema.json +0 -61
- package/ts/src/booking-surface/contracts-v2.ts +0 -118
- package/ts/src/booking-surface/runtime-v2.ts +0 -1466
- package/ts/src/booking-surface/server-v2.ts +0 -247
- package/ts/src/booking-surface/validation-v2.ts +0 -205
- package/ts/src/tool-budget.ts +0 -165
|
@@ -9,75 +9,89 @@
|
|
|
9
9
|
import { timingSafeEqual } from 'node:crypto'
|
|
10
10
|
import { createServer, type IncomingMessage, type Server, type ServerResponse } from 'node:http'
|
|
11
11
|
import {
|
|
12
|
+
BOOKING_COPILOT_ACCEPTED_TURN_KINDS,
|
|
13
|
+
BOOKING_COPILOT_INGRESS_MODES,
|
|
14
|
+
BOOKING_READ_ACTION_KINDS,
|
|
15
|
+
BOOKING_SURFACE_ALLOWED_ACTIONS,
|
|
12
16
|
BOOKING_SURFACE_SCHEMA_SHA256,
|
|
13
17
|
BOOKING_SURFACE_SCHEMA_SHA256_HEADER,
|
|
14
18
|
BOOKING_SURFACE_SCHEMA_VERSION,
|
|
15
19
|
BOOKING_SURFACE_VERSION_HEADER,
|
|
16
|
-
type
|
|
17
|
-
type
|
|
18
|
-
type
|
|
20
|
+
type BookingCopilotIngressMode,
|
|
21
|
+
type BookingCopilotTurn,
|
|
22
|
+
type BookingIngressBinding,
|
|
23
|
+
type BookingIngressPrincipal,
|
|
24
|
+
type BookingReadActionKind,
|
|
25
|
+
type BookingSurfaceEvent,
|
|
26
|
+
type BookingWorkspaceSnapshot,
|
|
27
|
+
type IngressTurn,
|
|
19
28
|
} from './contracts.ts'
|
|
20
29
|
import {
|
|
30
|
+
bookingDigest,
|
|
21
31
|
BookingCopilotTaskRuntime,
|
|
22
|
-
type
|
|
23
|
-
type
|
|
32
|
+
type BookingCopilotTaskState,
|
|
33
|
+
type BookingIngressRequestBindingInput,
|
|
34
|
+
type BookingPlannerSessionFactory,
|
|
24
35
|
} from './runtime.ts'
|
|
25
|
-
import {
|
|
26
|
-
import { handleBookingCopilotV2Request, type BookingCopilotV2Adapter } from './server-v2.ts'
|
|
36
|
+
import { validateBookingSurface } from './validation.ts'
|
|
27
37
|
import { normalizeBookingErrorCode, safeBookingErrorMessage } from './error-codes.ts'
|
|
28
|
-
import {
|
|
29
|
-
BOOKING_COPILOT_V2_ACCEPTED_TURN_KINDS,
|
|
30
|
-
BOOKING_COPILOT_V2_INGRESS_TURN_KIND,
|
|
31
|
-
BOOKING_COPILOT_V2_INGRESS_MODES,
|
|
32
|
-
BOOKING_SURFACE_SCHEMA_V2_SHA256,
|
|
33
|
-
BOOKING_SURFACE_SCHEMA_VERSION_V2,
|
|
34
|
-
type BookingCopilotV2IngressMode,
|
|
35
|
-
} from './contracts-v2.ts'
|
|
36
|
-
|
|
37
|
-
export type BookingPlannerDecisionV1 =
|
|
38
|
-
| { kind: 'operation'; action: BookingReadActionV1 }
|
|
39
|
-
| BookingSurfaceEventDraftV1
|
|
40
38
|
|
|
41
|
-
export interface
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
39
|
+
export interface BookingCopilotComposition {
|
|
40
|
+
runtime: BookingCopilotTaskRuntime
|
|
41
|
+
plannerFactory: BookingPlannerSessionFactory
|
|
42
|
+
/** Optional BFF-owned identity binding. GoTry never accepts browser identity. */
|
|
43
|
+
ingressBinding?: BookingIngressBinding
|
|
44
|
+
/** Principal authenticated by the HotelByte BFF, never by browser input. */
|
|
45
|
+
principal?: BookingIngressPrincipal
|
|
46
|
+
/** Explicit process composition mode; omitted means infer from a complete pair. */
|
|
47
|
+
ingressMode?: BookingCopilotIngressMode
|
|
46
48
|
}
|
|
47
49
|
|
|
48
|
-
|
|
49
|
-
export type BookingPlannerSessionFactoryV1 = (
|
|
50
|
-
initialTask: BookingCopilotTaskStateV1,
|
|
51
|
-
) => BookingPlannerSessionV1
|
|
52
|
-
|
|
53
|
-
export interface BookingCopilotServerOptionsV1 {
|
|
50
|
+
export interface BookingCopilotServerOptions extends BookingCopilotComposition {
|
|
54
51
|
/** Deployment credential shared only with the HotelByte BFF. */
|
|
55
52
|
apiKey: string
|
|
56
|
-
runtime?: BookingCopilotTaskRuntime
|
|
57
|
-
plannerFactory?: BookingPlannerSessionFactoryV1
|
|
58
53
|
host?: string
|
|
59
54
|
port?: number
|
|
60
55
|
maxBodyBytes?: number
|
|
61
56
|
/** GoTry source commit loaded from the root-owned release artifact. */
|
|
62
57
|
artifactId?: string
|
|
63
|
-
/** Optional v2 adapter dispatched by the same listener after version/hash selection. */
|
|
64
|
-
v2?: BookingCopilotV2Adapter
|
|
65
58
|
}
|
|
66
59
|
|
|
67
|
-
export interface
|
|
60
|
+
export interface BookingCopilotServerHandle {
|
|
68
61
|
server: Server
|
|
69
62
|
port: number
|
|
70
63
|
close(): Promise<void>
|
|
71
64
|
}
|
|
72
65
|
|
|
73
|
-
interface
|
|
66
|
+
interface BookingCopilotRuntimeIdentity {
|
|
74
67
|
nodeVersion: string
|
|
75
68
|
nodeModulesAbi: string
|
|
76
69
|
releaseTuple: string
|
|
77
70
|
glibcVersion: string
|
|
78
71
|
}
|
|
79
72
|
|
|
80
|
-
|
|
73
|
+
const sessionsByComposition = new WeakMap<BookingCopilotComposition, Map<string, ReturnType<BookingPlannerSessionFactory>>>()
|
|
74
|
+
const decisionFlightsByComposition = new WeakMap<BookingCopilotComposition, Map<string, Promise<BookingSurfaceEvent[]>>>()
|
|
75
|
+
function sessionsFor(composition: BookingCopilotComposition): Map<string, ReturnType<BookingPlannerSessionFactory>> {
|
|
76
|
+
let sessions = sessionsByComposition.get(composition)
|
|
77
|
+
if (!sessions) { sessions = new Map(); sessionsByComposition.set(composition, sessions) }
|
|
78
|
+
return sessions
|
|
79
|
+
}
|
|
80
|
+
function decisionFlightsFor(composition: BookingCopilotComposition): Map<string, Promise<BookingSurfaceEvent[]>> {
|
|
81
|
+
let flights = decisionFlightsByComposition.get(composition)
|
|
82
|
+
if (!flights) { flights = new Map(); decisionFlightsByComposition.set(composition, flights) }
|
|
83
|
+
return flights
|
|
84
|
+
}
|
|
85
|
+
async function runDecisionSingleFlight(composition: BookingCopilotComposition, key: string, work: () => Promise<BookingSurfaceEvent[]>): Promise<BookingSurfaceEvent[]> {
|
|
86
|
+
const flights = decisionFlightsFor(composition)
|
|
87
|
+
const prior = flights.get(key)
|
|
88
|
+
if (prior) return prior
|
|
89
|
+
const current = work()
|
|
90
|
+
flights.set(key, current)
|
|
91
|
+
try { return await current } finally { if (flights.get(key) === current) flights.delete(key) }
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
function runtimeIdentity(): BookingCopilotRuntimeIdentity {
|
|
81
95
|
const report = process.report?.getReport?.() as { header?: { glibcVersionRuntime?: string } } | undefined
|
|
82
96
|
const header = report?.header
|
|
83
97
|
const glibcVersion = header?.glibcVersionRuntime ?? ''
|
|
@@ -118,40 +132,237 @@ function readBody(req: IncomingMessage, maxBodyBytes: number): Promise<string> {
|
|
|
118
132
|
})
|
|
119
133
|
}
|
|
120
134
|
|
|
121
|
-
function
|
|
135
|
+
function write(res: ServerResponse, event: BookingSurfaceEvent): void {
|
|
122
136
|
res.write(`event: ${event.kind}\ndata: ${JSON.stringify(event)}\n\n`)
|
|
123
137
|
}
|
|
124
138
|
|
|
125
|
-
|
|
139
|
+
function sameCapabilities(a: readonly string[], b: readonly string[]): boolean {
|
|
140
|
+
return a.length === b.length && [...a].sort().every((kind, index) => kind === [...b].sort()[index])
|
|
141
|
+
}
|
|
142
|
+
function exactKeys(value: Record<string, unknown>, allowed: readonly string[]): boolean {
|
|
143
|
+
return Object.keys(value).length === allowed.length && Object.keys(value).every((key) => allowed.includes(key))
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
const SURFACE_ACTION_MATRIX = BOOKING_SURFACE_ALLOWED_ACTIONS
|
|
147
|
+
|
|
148
|
+
function validSurfaceActions(surface: unknown, allowed: unknown): allowed is BookingWorkspaceSnapshot['capabilities']['allowedActions'] {
|
|
149
|
+
if (typeof surface !== 'string' || !Object.prototype.hasOwnProperty.call(SURFACE_ACTION_MATRIX, surface)) return false
|
|
150
|
+
if (!Array.isArray(allowed) || allowed.length < 1 || new Set(allowed).size !== allowed.length) return false
|
|
151
|
+
const matrix = SURFACE_ACTION_MATRIX[surface as BookingWorkspaceSnapshot['surface']]
|
|
152
|
+
return allowed.every((action) => typeof action === 'string'
|
|
153
|
+
&& matrix.includes(action as BookingReadActionKind)
|
|
154
|
+
&& (BOOKING_READ_ACTION_KINDS as readonly string[]).includes(action))
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
function validBoundWorkspaceAuthority(workspace: BookingWorkspaceSnapshot): boolean {
|
|
158
|
+
return workspace.capabilities.surface === workspace.surface
|
|
159
|
+
&& validSurfaceActions(workspace.surface, workspace.capabilities.allowedActions)
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
function validIngressBinding(binding: unknown, ingress: IngressTurn): binding is {
|
|
163
|
+
taskId: string
|
|
164
|
+
turnId: string
|
|
165
|
+
contextRef: string
|
|
166
|
+
surface: BookingWorkspaceSnapshot['surface']
|
|
167
|
+
allowedActions: BookingWorkspaceSnapshot['capabilities']['allowedActions']
|
|
168
|
+
} {
|
|
169
|
+
if (!binding || typeof binding !== 'object' || Array.isArray(binding)) return false
|
|
170
|
+
const candidate = binding as Record<string, unknown>
|
|
171
|
+
if (!exactKeys(candidate, ['taskId', 'turnId', 'contextRef', 'surface', 'allowedActions'])) return false
|
|
172
|
+
if (typeof candidate.taskId !== 'string' || typeof candidate.turnId !== 'string' || typeof candidate.contextRef !== 'string') return false
|
|
173
|
+
if (typeof candidate.surface !== 'string' || candidate.surface !== ingress.surfaceHint || !Object.prototype.hasOwnProperty.call(SURFACE_ACTION_MATRIX, candidate.surface)) return false
|
|
174
|
+
return validSurfaceActions(candidate.surface, candidate.allowedActions)
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
function validIngressApprovalAuthority(ingress: IngressTurn, binding: { taskId: string; contextRef: string }): boolean {
|
|
178
|
+
if (!ingress.request.approval) return true
|
|
179
|
+
return ingress.request.approval.taskId === binding.taskId
|
|
180
|
+
&& ingress.request.approval.contextRef === binding.contextRef
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
function fallbackErrorEvent(task: BookingCopilotTaskState, code: string): BookingSurfaceEvent {
|
|
184
|
+
return {
|
|
185
|
+
schemaVersion: BOOKING_SURFACE_SCHEMA_VERSION,
|
|
186
|
+
eventId: `error-${task.taskId}-${task.lastSequence + 1}`,
|
|
187
|
+
taskId: task.taskId,
|
|
188
|
+
contextRef: task.contextRef,
|
|
189
|
+
sequence: task.lastSequence + 1,
|
|
190
|
+
emittedAt: new Date().toISOString(),
|
|
191
|
+
kind: 'error',
|
|
192
|
+
error: { code: normalizeBookingErrorCode(code), message: safeBookingErrorMessage(normalizeBookingErrorCode(code)), retryable: false },
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
function writeTypedError(res: ServerResponse, composition: BookingCopilotComposition, task: BookingCopilotTaskState, error: unknown, requestKey?: string, includeSubmitted = false): void {
|
|
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.
|
|
200
|
+
console.error(`[booking-copilot] planner boundary error (${code}):`, error instanceof Error ? error.stack || error.message : error)
|
|
201
|
+
try {
|
|
202
|
+
const decision = { kind: 'error' as const, error: { code, message: safeBookingErrorMessage(code), retryable: false } }
|
|
203
|
+
if (requestKey) {
|
|
204
|
+
const events = composition.runtime.applyDecisionBatch(task.taskId, requestKey, [decision], includeSubmitted)
|
|
205
|
+
for (const event of events) write(res, event)
|
|
206
|
+
return
|
|
207
|
+
}
|
|
208
|
+
write(res, composition.runtime.emitEvent(task.taskId, decision))
|
|
209
|
+
} catch {
|
|
210
|
+
// The stream is already committed. Emit a schema-valid, non-durable
|
|
211
|
+
// typed envelope instead of silently returning an empty HTTP 200 stream.
|
|
212
|
+
write(res, fallbackErrorEvent(task, code))
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
/** Handles one already-authenticated request inside the sole HTTP listener. */
|
|
217
|
+
async function handleBookingCopilotRequest(req: IncomingMessage, res: ServerResponse, composition: BookingCopilotComposition, maxBodyBytes: number): Promise<void> {
|
|
218
|
+
let turn: Exclude<BookingCopilotTurn, IngressTurn>
|
|
219
|
+
let browserRequestKey: string | undefined
|
|
220
|
+
let requestBinding: BookingIngressRequestBindingInput | undefined
|
|
221
|
+
try {
|
|
222
|
+
const parsed = JSON.parse(await readBody(req, maxBodyBytes)) as unknown
|
|
223
|
+
const valid = validateBookingSurface(parsed)
|
|
224
|
+
if (!valid.ok) { sendJson(res, 400, { error: { code: 'invalid_booking_surface_turn', details: valid.errors } }); return }
|
|
225
|
+
if ((parsed as BookingCopilotTurn).kind === 'user.turn.ingress') {
|
|
226
|
+
const ingress = parsed as IngressTurn
|
|
227
|
+
browserRequestKey = ingress.requestKey
|
|
228
|
+
if (composition.ingressMode !== 'bff-ingress-binding' || !composition.ingressBinding || !composition.principal) {
|
|
229
|
+
sendJson(res, 503, { error: { code: 'trusted_ingress_binding_required', mode: composition.ingressMode ?? 'bff-bound-turn-only', acceptedTurnKinds: [...BOOKING_COPILOT_ACCEPTED_TURN_KINDS] } })
|
|
230
|
+
return
|
|
231
|
+
}
|
|
232
|
+
const binding = await composition.ingressBinding.bind(ingress, composition.principal)
|
|
233
|
+
if (!validIngressBinding(binding, ingress)) { sendJson(res, 502, { error: { code: 'invalid_ingress_binding' } }); return }
|
|
234
|
+
if (!validIngressApprovalAuthority(ingress, binding)) { sendJson(res, 400, { error: { code: 'invalid_ingress_approval_authority' } }); return }
|
|
235
|
+
const boundWorkspace = {
|
|
236
|
+
...ingress.workspace,
|
|
237
|
+
contextRef: binding.contextRef,
|
|
238
|
+
surface: binding.surface,
|
|
239
|
+
capabilities: { surface: binding.surface, allowedActions: [...binding.allowedActions] },
|
|
240
|
+
} as BookingWorkspaceSnapshot
|
|
241
|
+
const boundRequest = ingress.request.approval
|
|
242
|
+
? { text: ingress.request.text, approval: { ...ingress.request.approval } }
|
|
243
|
+
: { text: ingress.request.text }
|
|
244
|
+
turn = { schemaVersion: BOOKING_SURFACE_SCHEMA_VERSION, kind: 'user.turn', taskId: binding.taskId, turnId: binding.turnId, workspace: boundWorkspace, request: boundRequest }
|
|
245
|
+
requestBinding = { requestKey: ingress.requestKey, principal: composition.principal, ...(ingress.taskHandle ? { taskHandle: ingress.taskHandle } : {}) }
|
|
246
|
+
const boundValid = validateBookingSurface(turn)
|
|
247
|
+
if (!boundValid.ok) { sendJson(res, 502, { error: { code: 'invalid_ingress_binding', details: boundValid.errors } }); return }
|
|
248
|
+
} else {
|
|
249
|
+
turn = parsed as Exclude<BookingCopilotTurn, IngressTurn>
|
|
250
|
+
if (!validBoundWorkspaceAuthority(turn.workspace)) {
|
|
251
|
+
sendJson(res, 403, { error: { code: 'invalid_bound_turn_authority' } })
|
|
252
|
+
return
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
} catch (error) { const code = normalizeBookingErrorCode(error); sendJson(res, code === 'PAYLOAD_TOO_LARGE' ? 413 : 400, { error: { code } }); return }
|
|
256
|
+
|
|
257
|
+
let task: BookingCopilotTaskState; let fresh = false; let replayKey: string | undefined; let replayEvents: BookingSurfaceEvent[] | null = null; let activeDecisionKey: string | undefined
|
|
258
|
+
try {
|
|
259
|
+
fresh = turn.kind === 'action.receipt.continuation' ? false : !turn.taskId || composition.runtime.resumeTask(turn.taskId) === null
|
|
260
|
+
if (turn.kind === 'action.receipt.continuation') {
|
|
261
|
+
replayKey = `receipt:${turn.receipt.actionId}:${composition.runtime.receiptDigest(turn.receipt)}`
|
|
262
|
+
task = composition.runtime.continueWithReceipt(turn)
|
|
263
|
+
if (task.awaitingApproval) {
|
|
264
|
+
replayKey = composition.runtime.approvalPresentationRequestKey(turn.taskId)
|
|
265
|
+
}
|
|
266
|
+
replayEvents = composition.runtime.readDecisionBatch(turn.taskId, replayKey)
|
|
267
|
+
if (!replayEvents && task.phase === 'terminal') replayEvents = composition.runtime.terminalDecisionBatch(turn.taskId, replayKey)
|
|
268
|
+
} else {
|
|
269
|
+
const existing = turn.taskId ? composition.runtime.resumeTask(turn.taskId) : null
|
|
270
|
+
if (requestBinding && turn.kind === 'user.turn') composition.runtime.assertRequestBinding(browserRequestKey!, turn, requestBinding)
|
|
271
|
+
const requestReplayKey = existing && browserRequestKey ? `ingress:${existing.taskId}:${browserRequestKey}` : undefined
|
|
272
|
+
const directReplayKey = existing && turn.kind === 'user.turn' && turn.turnId ? `turn:${existing.taskId}:${turn.turnId}` : undefined
|
|
273
|
+
const stableReplayKey = requestReplayKey ?? directReplayKey
|
|
274
|
+
const stableReplay = stableReplayKey ? composition.runtime.readDecisionBatch(existing!.taskId, stableReplayKey) : null
|
|
275
|
+
if (stableReplay) {
|
|
276
|
+
if (existing && turn.kind === 'user.turn' && directReplayKey) composition.runtime.assertTurnBinding(existing.taskId, turn)
|
|
277
|
+
task = existing!
|
|
278
|
+
replayKey = stableReplayKey
|
|
279
|
+
replayEvents = stableReplay
|
|
280
|
+
} else if (existing?.phase === 'terminal' || existing?.phase === 'error') {
|
|
281
|
+
throw new Error('task_terminal')
|
|
282
|
+
} else if (existing?.phase === 'waiting_receipt') {
|
|
283
|
+
if (turn.kind === 'user.turn' && turn.request.approval) composition.runtime.assertApprovalNotConsumed(existing.taskId, turn.request.approval)
|
|
284
|
+
const workspace = turn.workspace as BookingWorkspaceSnapshot
|
|
285
|
+
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')
|
|
286
|
+
// A waiting task can only be replayed with the caller's stable turn
|
|
287
|
+
// identity. Without it, an identical sentence is not distinguishable
|
|
288
|
+
// from a new user turn and must not receive an old operation batch.
|
|
289
|
+
if (!turn.turnId || existing.lastTurnId !== turn.turnId) throw new Error('receipt_required')
|
|
290
|
+
replayKey = browserRequestKey ? `ingress:${existing.taskId}:${browserRequestKey}` : `turn:${existing.taskId}:${existing.lastTurnId ?? existing.userTurnCount}`
|
|
291
|
+
replayEvents = composition.runtime.readDecisionBatch(existing.taskId, replayKey)
|
|
292
|
+
if (!replayEvents) throw new Error('receipt_required')
|
|
293
|
+
task = existing
|
|
294
|
+
} else {
|
|
295
|
+
if (existing && turn.kind === 'user.turn' && turn.request.approval) composition.runtime.assertApprovalNotConsumed(existing.taskId, turn.request.approval)
|
|
296
|
+
task = composition.runtime.startTask(turn, requestBinding)
|
|
297
|
+
if (turn.kind === 'user.turn' && turn.taskId) {
|
|
298
|
+
replayKey = browserRequestKey ? `ingress:${task.taskId}:${browserRequestKey}` : `turn:${task.taskId}:${turn.turnId ?? task.userTurnCount}`
|
|
299
|
+
replayEvents = composition.runtime.readDecisionBatch(task.taskId, replayKey)
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
} catch (error) { sendJson(res, 409, { error: { code: normalizeBookingErrorCode(error) } }); return }
|
|
304
|
+
|
|
305
|
+
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 })
|
|
306
|
+
try {
|
|
307
|
+
if (replayEvents) {
|
|
308
|
+
for (const event of replayEvents) write(res, event)
|
|
309
|
+
} else {
|
|
310
|
+
const requestKey = replayKey ?? `turn:${task.taskId}:${task.userTurnCount}`
|
|
311
|
+
activeDecisionKey = requestKey
|
|
312
|
+
const approval = turn.kind === 'user.turn' ? turn.request.approval : undefined
|
|
313
|
+
const flightKey = JSON.stringify([task.taskId, approval ? `approval:${bookingDigest(approval)}` : requestKey])
|
|
314
|
+
const decisionEvents = await runDecisionSingleFlight(composition, flightKey, async () => {
|
|
315
|
+
if (turn.kind === 'action.receipt.continuation' && task.awaitingApproval) {
|
|
316
|
+
return composition.runtime.applyDecisionBatch(task.taskId, requestKey, [composition.runtime.approvalQuestion(task.taskId)], false)
|
|
317
|
+
}
|
|
318
|
+
const sessions = sessionsFor(composition)
|
|
319
|
+
const session = sessions.get(task.taskId) ?? composition.plannerFactory(task)
|
|
320
|
+
sessions.set(task.taskId, session)
|
|
321
|
+
const decisions = await session.next({ turn, task: composition.runtime.resumeTask(task.taskId) ?? task })
|
|
322
|
+
const plannerDecisions = decisions.length ? decisions : [{ kind: 'error' as const, error: { code: 'PLANNER_NO_DECISION', message: 'Planner returned no typed decision.', retryable: false } }]
|
|
323
|
+
return composition.runtime.applyDecisionBatch(task.taskId, requestKey, plannerDecisions, fresh)
|
|
324
|
+
})
|
|
325
|
+
for (const event of decisionEvents) write(res, event)
|
|
326
|
+
}
|
|
327
|
+
} catch (error) {
|
|
328
|
+
writeTypedError(res, composition, task, error, activeDecisionKey, fresh)
|
|
329
|
+
} finally { res.end() }
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
export function startBookingCopilotServer(options: BookingCopilotServerOptions): Promise<BookingCopilotServerHandle> {
|
|
126
333
|
if (!options.apiKey) return Promise.reject(new Error('booking_copilot_api_key_required'))
|
|
127
334
|
if (options.artifactId !== undefined && !/^[0-9a-f]{40}$/.test(options.artifactId)) {
|
|
128
335
|
return Promise.reject(new Error('booking_copilot_artifact_id_invalid'))
|
|
129
336
|
}
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
337
|
+
if (!options.runtime || !options.plannerFactory) {
|
|
338
|
+
return Promise.reject(new Error('booking_copilot_runtime_and_planner_required'))
|
|
339
|
+
}
|
|
340
|
+
const hasIngressBinding = Boolean(options.ingressBinding)
|
|
341
|
+
const hasPrincipal = Boolean(options.principal)
|
|
342
|
+
if (hasIngressBinding !== hasPrincipal) {
|
|
343
|
+
return Promise.reject(new Error('booking_copilot_ingress_binding_pair_required'))
|
|
344
|
+
}
|
|
345
|
+
if (hasIngressBinding && (typeof options.ingressBinding?.bind !== 'function'
|
|
346
|
+
|| typeof options.principal?.subject !== 'string' || options.principal.subject.length === 0 || options.principal.subject.length > 256
|
|
347
|
+
|| typeof options.principal?.scope !== 'string' || options.principal.scope.length === 0 || options.principal.scope.length > 256)) {
|
|
348
|
+
return Promise.reject(new Error('booking_copilot_ingress_binding_invalid'))
|
|
134
349
|
}
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
return Promise.reject(new Error('booking_copilot_v2_ingress_binding_invalid'))
|
|
350
|
+
const ingressMode: BookingCopilotIngressMode = options.ingressMode ?? (hasIngressBinding ? 'bff-ingress-binding' : 'bff-bound-turn-only')
|
|
351
|
+
if (!BOOKING_COPILOT_INGRESS_MODES.includes(ingressMode)) {
|
|
352
|
+
return Promise.reject(new Error('booking_copilot_ingress_mode_invalid'))
|
|
139
353
|
}
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
: undefined
|
|
143
|
-
if (options.v2 && !BOOKING_COPILOT_V2_INGRESS_MODES.includes(ingressMode!)) {
|
|
144
|
-
return Promise.reject(new Error('booking_copilot_v2_ingress_mode_invalid'))
|
|
354
|
+
if (ingressMode === 'bff-ingress-binding' && !hasIngressBinding) {
|
|
355
|
+
return Promise.reject(new Error('booking_copilot_ingress_binding_required'))
|
|
145
356
|
}
|
|
146
|
-
if (
|
|
147
|
-
return Promise.reject(new Error('
|
|
357
|
+
if (ingressMode === 'bff-bound-turn-only' && hasIngressBinding) {
|
|
358
|
+
return Promise.reject(new Error('booking_copilot_ingress_mode_conflict'))
|
|
148
359
|
}
|
|
149
|
-
|
|
150
|
-
|
|
360
|
+
const composition: BookingCopilotComposition = {
|
|
361
|
+
runtime: options.runtime,
|
|
362
|
+
plannerFactory: options.plannerFactory,
|
|
363
|
+
...(hasIngressBinding ? { ingressBinding: options.ingressBinding, principal: options.principal } : {}),
|
|
364
|
+
ingressMode,
|
|
151
365
|
}
|
|
152
|
-
const v2 = options.v2 ? { ...options.v2, ingressMode } : undefined
|
|
153
|
-
const v2Only = Boolean(v2 && !options.runtime && !options.plannerFactory)
|
|
154
|
-
const sessions = new Map<string, BookingPlannerSessionV1>()
|
|
155
366
|
const maxBodyBytes = options.maxBodyBytes ?? 1_000_000
|
|
156
367
|
const runningIdentity = runtimeIdentity()
|
|
157
368
|
|
|
@@ -167,42 +378,30 @@ export function startBookingCopilotServer(options: BookingCopilotServerOptionsV1
|
|
|
167
378
|
return
|
|
168
379
|
}
|
|
169
380
|
if (isProbe) {
|
|
170
|
-
|
|
171
|
-
res.setHeader(
|
|
172
|
-
res.setHeader(BOOKING_SURFACE_SCHEMA_SHA256_HEADER, activeV2Only ? BOOKING_SURFACE_SCHEMA_V2_SHA256 : BOOKING_SURFACE_SCHEMA_SHA256)
|
|
381
|
+
res.setHeader(BOOKING_SURFACE_VERSION_HEADER, BOOKING_SURFACE_SCHEMA_VERSION)
|
|
382
|
+
res.setHeader(BOOKING_SURFACE_SCHEMA_SHA256_HEADER, BOOKING_SURFACE_SCHEMA_SHA256)
|
|
173
383
|
if (options.artifactId) res.setHeader('X-GoTry-Artifact-ID', options.artifactId)
|
|
174
384
|
res.setHeader('X-GoTry-Node-Version', runningIdentity.nodeVersion)
|
|
175
385
|
res.setHeader('X-GoTry-Node-Modules-ABI', runningIdentity.nodeModulesAbi)
|
|
176
386
|
res.setHeader('X-GoTry-Release-Tuple', runningIdentity.releaseTuple)
|
|
177
387
|
if (runningIdentity.glibcVersion) res.setHeader('X-GoTry-Glibc-Version', runningIdentity.glibcVersion)
|
|
388
|
+
const acceptedTurnKinds = ingressMode === 'bff-ingress-binding'
|
|
389
|
+
? [...BOOKING_COPILOT_ACCEPTED_TURN_KINDS, 'user.turn.ingress' as const]
|
|
390
|
+
: [...BOOKING_COPILOT_ACCEPTED_TURN_KINDS]
|
|
178
391
|
const healthBody: Record<string, unknown> = {
|
|
179
|
-
schemaVersion:
|
|
180
|
-
schemaSha256:
|
|
392
|
+
schemaVersion: BOOKING_SURFACE_SCHEMA_VERSION,
|
|
393
|
+
schemaSha256: BOOKING_SURFACE_SCHEMA_SHA256,
|
|
181
394
|
status: 'ready',
|
|
395
|
+
ingressMode,
|
|
396
|
+
acceptedTurnKinds,
|
|
182
397
|
}
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
const acceptedTurnKinds = v2.ingressMode === 'bff-ingress-binding'
|
|
186
|
-
? [...BOOKING_COPILOT_V2_ACCEPTED_TURN_KINDS, BOOKING_COPILOT_V2_INGRESS_TURN_KIND]
|
|
187
|
-
: [...BOOKING_COPILOT_V2_ACCEPTED_TURN_KINDS]
|
|
188
|
-
healthBody.ingressMode = v2.ingressMode
|
|
189
|
-
healthBody.acceptedTurnKinds = acceptedTurnKinds
|
|
190
|
-
res.setHeader('X-GoTry-Ingress-Mode', v2.ingressMode!)
|
|
191
|
-
res.setHeader('X-GoTry-Accepted-Turn-Kinds', acceptedTurnKinds.join(','))
|
|
192
|
-
}
|
|
398
|
+
res.setHeader('X-GoTry-Ingress-Mode', ingressMode)
|
|
399
|
+
res.setHeader('X-GoTry-Accepted-Turn-Kinds', acceptedTurnKinds.join(','))
|
|
193
400
|
sendJson(res, 200, healthBody)
|
|
194
401
|
return
|
|
195
402
|
}
|
|
196
403
|
const schemaVersion = String(req.headers[BOOKING_SURFACE_VERSION_HEADER] ?? '')
|
|
197
404
|
const schemaHash = String(req.headers[BOOKING_SURFACE_SCHEMA_SHA256_HEADER] ?? '')
|
|
198
|
-
if (schemaVersion === BOOKING_SURFACE_SCHEMA_VERSION_V2) {
|
|
199
|
-
if (!v2 || schemaHash !== BOOKING_SURFACE_SCHEMA_V2_SHA256) {
|
|
200
|
-
sendJson(res, 409, { error: { code: 'booking_surface_schema_mismatch', expectedVersion: BOOKING_SURFACE_SCHEMA_VERSION_V2, expectedSchemaSha256: BOOKING_SURFACE_SCHEMA_V2_SHA256 } })
|
|
201
|
-
return
|
|
202
|
-
}
|
|
203
|
-
await handleBookingCopilotV2Request(req, res, v2, maxBodyBytes)
|
|
204
|
-
return
|
|
205
|
-
}
|
|
206
405
|
if (schemaVersion !== BOOKING_SURFACE_SCHEMA_VERSION || schemaHash !== BOOKING_SURFACE_SCHEMA_SHA256) {
|
|
207
406
|
sendJson(res, 409, {
|
|
208
407
|
error: {
|
|
@@ -213,99 +412,7 @@ export function startBookingCopilotServer(options: BookingCopilotServerOptionsV1
|
|
|
213
412
|
})
|
|
214
413
|
return
|
|
215
414
|
}
|
|
216
|
-
|
|
217
|
-
sendJson(res, 503, { error: { code: 'booking_surface_v1_not_configured' } })
|
|
218
|
-
return
|
|
219
|
-
}
|
|
220
|
-
|
|
221
|
-
let turn: BookingCopilotTurnV1
|
|
222
|
-
try {
|
|
223
|
-
const raw = await readBody(req, maxBodyBytes)
|
|
224
|
-
const parsed = JSON.parse(raw) as unknown
|
|
225
|
-
const validation = validateBookingCopilotTurnV1(parsed)
|
|
226
|
-
if (!validation.ok) {
|
|
227
|
-
sendJson(res, 400, { error: { code: 'invalid_booking_surface_turn', details: validation.errors } })
|
|
228
|
-
return
|
|
229
|
-
}
|
|
230
|
-
turn = parsed as BookingCopilotTurnV1
|
|
231
|
-
} catch (error) {
|
|
232
|
-
const code = normalizeBookingErrorCode(error)
|
|
233
|
-
sendJson(res, code === 'PAYLOAD_TOO_LARGE' ? 413 : 400, { error: { code } })
|
|
234
|
-
return
|
|
235
|
-
}
|
|
236
|
-
|
|
237
|
-
let task: BookingCopilotTaskStateV1
|
|
238
|
-
let isNewTask = false
|
|
239
|
-
try {
|
|
240
|
-
const taskId = turn.taskId
|
|
241
|
-
if (v2 && taskId && v2.runtime.resumeTask(taskId)) throw new Error('task_conflict:protocol_version')
|
|
242
|
-
if (turn.kind === 'user.turn') {
|
|
243
|
-
const suppliedTaskId = turn.taskId
|
|
244
|
-
isNewTask = !suppliedTaskId || options.runtime.resumeTask(suppliedTaskId) === null
|
|
245
|
-
task = options.runtime.startTask(turn)
|
|
246
|
-
} else {
|
|
247
|
-
task = options.runtime.continueWithReceipt(turn)
|
|
248
|
-
}
|
|
249
|
-
} catch (error) {
|
|
250
|
-
sendJson(res, 409, { error: { code: normalizeBookingErrorCode(error) } })
|
|
251
|
-
return
|
|
252
|
-
}
|
|
253
|
-
|
|
254
|
-
res.writeHead(200, {
|
|
255
|
-
'content-type': 'text/event-stream; charset=utf-8',
|
|
256
|
-
'cache-control': 'no-cache, no-transform',
|
|
257
|
-
connection: 'keep-alive',
|
|
258
|
-
'x-content-type-options': 'nosniff',
|
|
259
|
-
[BOOKING_SURFACE_VERSION_HEADER]: BOOKING_SURFACE_SCHEMA_VERSION,
|
|
260
|
-
[BOOKING_SURFACE_SCHEMA_SHA256_HEADER]: BOOKING_SURFACE_SCHEMA_SHA256,
|
|
261
|
-
})
|
|
262
|
-
|
|
263
|
-
try {
|
|
264
|
-
if (isNewTask) writeSse(res, options.runtime.emitEvent(task.taskId, { kind: 'status', status: 'submitted' }))
|
|
265
|
-
writeSse(res, options.runtime.emitEvent(task.taskId, { kind: 'status', status: 'working' }))
|
|
266
|
-
task = options.runtime.resumeTask(task.taskId) ?? task
|
|
267
|
-
let session = sessions.get(task.taskId)
|
|
268
|
-
if (!session) {
|
|
269
|
-
session = options.plannerFactory(task)
|
|
270
|
-
sessions.set(task.taskId, session)
|
|
271
|
-
}
|
|
272
|
-
const decisions = await session.next({ turn, task })
|
|
273
|
-
if (decisions.length === 0) {
|
|
274
|
-
writeSse(res, options.runtime.emitEvent(task.taskId, {
|
|
275
|
-
kind: 'error',
|
|
276
|
-
error: { code: 'PLANNER_NO_DECISION', message: 'Planner returned no typed decision.', retryable: false },
|
|
277
|
-
}))
|
|
278
|
-
} else {
|
|
279
|
-
for (const decision of decisions) {
|
|
280
|
-
const event = decision.kind === 'operation'
|
|
281
|
-
? options.runtime.issueOperation(task.taskId, decision.action)
|
|
282
|
-
: options.runtime.emitEvent(task.taskId, decision)
|
|
283
|
-
writeSse(res, event)
|
|
284
|
-
}
|
|
285
|
-
}
|
|
286
|
-
} catch (error) {
|
|
287
|
-
try {
|
|
288
|
-
writeSse(res, options.runtime.emitEvent(task.taskId, {
|
|
289
|
-
kind: 'error',
|
|
290
|
-
error: { code: normalizeBookingErrorCode(error), message: safeBookingErrorMessage(normalizeBookingErrorCode(error)), retryable: false },
|
|
291
|
-
}))
|
|
292
|
-
} catch {
|
|
293
|
-
// Headers are already committed. Preserve a non-empty typed SSE error
|
|
294
|
-
// even when the durable event append is unavailable.
|
|
295
|
-
writeSse(res, {
|
|
296
|
-
schemaVersion: 'booking.surface.v1',
|
|
297
|
-
eventId: `error-${task.taskId}-${task.lastSequence + 1}`,
|
|
298
|
-
taskId: task.taskId,
|
|
299
|
-
contextRef: task.contextRef,
|
|
300
|
-
sequence: task.lastSequence + 1,
|
|
301
|
-
emittedAt: new Date().toISOString(),
|
|
302
|
-
kind: 'error',
|
|
303
|
-
error: { code: normalizeBookingErrorCode(error), message: safeBookingErrorMessage(normalizeBookingErrorCode(error)), retryable: false },
|
|
304
|
-
})
|
|
305
|
-
}
|
|
306
|
-
} finally {
|
|
307
|
-
res.end()
|
|
308
|
-
}
|
|
415
|
+
await handleBookingCopilotRequest(req, res, composition, maxBodyBytes)
|
|
309
416
|
})
|
|
310
417
|
|
|
311
418
|
return new Promise((resolve, reject) => {
|