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