@danceiny/gotry 0.0.1-rc.15 → 0.0.1-rc.17
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 +51 -13
- package/README.zh-CN.md +26 -12
- package/bin/gotry-booking-copilot.js +53 -0
- package/bin/gotry-bootstrap.js +357 -8
- package/bin/gotry-inner.js +442 -60
- package/bin/gotry-runtime-resolution.d.ts +27 -0
- package/bin/gotry-runtime-resolution.js +50 -0
- package/bin/gotry.js +1 -1
- package/cordis.gotry-patch.yml +15 -1
- package/data/airline-airports.json +39 -0
- package/dist/capabilities/agent-reach-deep.js +1 -1
- package/dist/capabilities/agent-reach.js +1 -1
- package/dist/capabilities/anything.js +1 -1
- package/dist/capabilities/artifacts.js +1 -1
- package/dist/capabilities/effect.js +293 -0
- package/dist/capabilities/fact-log.js +41 -0
- package/dist/capabilities/flyai.js +20 -6
- package/dist/capabilities/hbcli.js +43 -1
- package/dist/capabilities/incident-log.js +1 -1
- package/dist/capabilities/model-override.js +18 -0
- package/dist/capabilities/opensky.js +1 -1
- package/dist/capabilities/resilience.js +90 -0
- package/dist/capabilities/session/action-cache.js +1 -1
- package/dist/capabilities/session/adapters/ctrip-flight.js +1 -1
- package/dist/capabilities/session/adapters/meituan-local.js +1 -1
- package/dist/capabilities/session/benchmark.js +2 -1
- package/dist/capabilities/session/extension-bridge.js +358 -0
- package/dist/capabilities/session/extension-channel.js +93 -0
- package/dist/capabilities/session/extension-distribution.js +234 -0
- package/dist/capabilities/session/extract.js +1 -1
- package/dist/capabilities/session/golden-score.js +92 -0
- package/dist/capabilities/session/health-watch.js +216 -0
- package/dist/capabilities/session/read-guard.js +1 -1
- package/dist/capabilities/session/static-flight-golden.js +137 -0
- package/dist/capabilities/session/transport.js +1 -1
- package/dist/capabilities/session/wizard.js +147 -0
- package/dist/capabilities/session-consent.js +1 -1
- package/dist/capabilities/session-login.js +83 -1
- package/dist/capabilities/session-search.js +111 -2
- package/dist/capabilities/weather.js +168 -46
- package/dist/data/session-golden-20.json +25 -0
- package/dist/data/sf-golden-manifest.json +102 -0
- package/dist/data/sf-static-routes.json +91 -0
- package/dist/scripts/action-cache-tests.js +1 -1
- package/dist/scripts/agent-planning-budget-e2e.js +227 -0
- package/dist/scripts/agent-planning-budget-tests.js +173 -0
- package/dist/scripts/agent-reach-deep-tests.js +1 -1
- package/dist/scripts/agent-reach-tests.js +1 -1
- package/dist/scripts/agent-reach-wrapper-tests.js +1 -1
- package/dist/scripts/anything-tests.js +1 -1
- package/dist/scripts/async-collect.js +1 -1
- package/dist/scripts/benchmark-environment-bridge-e2e.js +981 -0
- package/dist/scripts/benchmark-environment-bridge-tests.js +2544 -0
- package/dist/scripts/booking-copilot-availability-ledger-binding-tests.js +335 -0
- package/dist/scripts/booking-copilot-availability-policy-v2-tests.js +1355 -0
- package/dist/scripts/booking-copilot-bin-proof-tests.js +89 -0
- package/dist/scripts/booking-copilot-crossrepo-fixture-server.js +113 -0
- package/dist/scripts/booking-copilot-dsh-core-proof-tests.js +356 -0
- package/dist/scripts/booking-copilot-dsh-planner-proof-tests.js +557 -0
- package/dist/scripts/booking-copilot-dsh-plugin-proof-tests.js +140 -0
- package/dist/scripts/booking-copilot-event-sequence-concurrency-proof-tests.js +202 -0
- package/dist/scripts/booking-copilot-gap-code-contract-proof-tests.js +102 -0
- package/dist/scripts/booking-copilot-operation-ledger-concurrency-proof-tests.js +374 -0
- package/dist/scripts/booking-copilot-receipt-ledger-concurrency-proof-tests.js +447 -0
- package/dist/scripts/booking-copilot-runtime-proof-tests.js +205 -0
- package/dist/scripts/booking-copilot-server-proof-tests.js +317 -0
- package/dist/scripts/booking-copilot-startup-proof-tests.js +283 -0
- package/dist/scripts/booking-copilot-v2-runtime-proof-tests.js +3935 -0
- package/dist/scripts/booking-saga-tests.js +1 -1
- package/dist/scripts/booking-surface-contract-proof-tests.js +393 -0
- package/dist/scripts/booking-surface-v2-contract-proof-tests.js +1174 -0
- package/dist/scripts/bootstrap-tests.js +59 -3
- package/dist/scripts/build-changelog.js +279 -0
- package/dist/scripts/changelog-tests.js +165 -0
- package/dist/scripts/companion-tests.js +1 -1
- package/dist/scripts/diff-test.js +1 -1
- package/dist/scripts/dsh-runtime-closure-tests.js +225 -0
- package/dist/scripts/dsh-runtime-closure.js +150 -0
- package/dist/scripts/effect-tests.js +474 -0
- package/dist/scripts/engine-run.js +1 -1
- package/dist/scripts/engine-tests.js +1 -1
- package/dist/scripts/evaluation-cadence-tests.js +347 -0
- package/dist/scripts/evaluation-contract-tests.js +574 -0
- package/dist/scripts/extension-distribution-cli.js +35 -0
- package/dist/scripts/extension-distribution-tests.js +341 -0
- package/dist/scripts/extension-tests.js +573 -0
- package/dist/scripts/fact-gate-tests.js +318 -0
- package/dist/scripts/flyai-tests.js +1 -1
- package/dist/scripts/hbcli-e2e-tests.js +180 -0
- package/dist/scripts/hbcli-tests.js +1 -1
- package/dist/scripts/health-watch-cli.js +51 -0
- package/dist/scripts/i18n-tests.js +1 -1
- package/dist/scripts/incident-tests.js +1 -1
- package/dist/scripts/journey-tests.js +1 -1
- package/dist/scripts/ledger-tests.js +1 -1
- package/dist/scripts/ledger-workflow-crash.js +1 -1
- package/dist/scripts/memory-capture-tests.js +1 -1
- package/dist/scripts/memory-decay-tests.js +1 -1
- package/dist/scripts/memory-metrics.js +1 -1
- package/dist/scripts/memory-value-report.js +1 -1
- package/dist/scripts/model-override-e2e.js +176 -0
- package/dist/scripts/nightly-evidence-tests.js +67 -4
- package/dist/scripts/nightly-evidence.js +59 -3
- package/dist/scripts/nudge-digest.js +1 -1
- package/dist/scripts/onboarding-tests.js +186 -0
- package/dist/scripts/opensky-check.js +1 -1
- package/dist/scripts/opensky-tests.js +1 -1
- package/dist/scripts/pnpm-dsh-closure-proof.js +20 -0
- package/dist/scripts/price-drift-tests.js +441 -0
- package/dist/scripts/price-drift-watch.js +491 -0
- package/dist/scripts/probe-poi-tests.js +1 -1
- package/dist/scripts/product-metrics.js +1 -1
- package/dist/scripts/publish-preverify.js +40 -4
- package/dist/scripts/realtime-pricing-tests.js +1 -1
- package/dist/scripts/replay-async.js +1 -1
- package/dist/scripts/replay-real.js +1 -1
- package/dist/scripts/replay.js +1 -1
- package/dist/scripts/session-attach-diagnose.js +1 -1
- package/dist/scripts/session-attach-poc.js +1 -1
- package/dist/scripts/session-benchmark.js +1 -1
- package/dist/scripts/session-extract-tests.js +1 -1
- package/dist/scripts/session-login.js +1 -1
- package/dist/scripts/session-tests.js +70 -20
- package/dist/scripts/sf-live-benchmark.js +338 -0
- package/dist/scripts/sf-live-cli-tests.js +21 -0
- package/dist/scripts/sf-soft-score-tests.js +108 -0
- package/dist/scripts/sf-summary.js +93 -0
- package/dist/scripts/skeleton-check.js +1 -1
- package/dist/scripts/skeleton-integration-test.js +1 -1
- package/dist/scripts/skills-contract-tests.js +1 -1
- package/dist/scripts/smoke-session-gate-tests.js +29 -0
- package/dist/scripts/smoke.js +154 -33
- package/dist/scripts/state-cli-tests.js +1 -1
- package/dist/scripts/state-cli.js +1 -1
- package/dist/scripts/static-golden-tests.js +299 -0
- package/dist/scripts/time-eval-tests.js +1 -1
- package/dist/scripts/travel-timeline-tests.js +1 -1
- package/dist/scripts/unified-tests.js +1 -1
- package/dist/scripts/weather-tests.js +694 -44
- package/dist/scripts/z3-race-tests.js +1 -1
- package/dist/src/artifact-gate.js +310 -0
- package/dist/src/benchmark-agent-conformance.js +370 -0
- package/dist/src/benchmark-environment-bridge.js +384 -0
- package/dist/src/benchmark-headless-child-diagnostics.js +173 -0
- package/dist/src/benchmark-tool-isolation.js +124 -0
- package/dist/src/bookable-facts.js +349 -0
- package/dist/src/booking-saga.js +1 -1
- package/dist/src/booking-surface/availability-policy-v2.js +830 -0
- package/dist/src/booking-surface/canonical-schema.js +113 -0
- package/dist/src/booking-surface/contracts-v2.js +89 -0
- package/dist/src/booking-surface/contracts.js +46 -0
- package/dist/src/booking-surface/dsh-planner.js +453 -0
- package/dist/src/booking-surface/dsh-plugin.js +93 -0
- package/dist/src/booking-surface/error-codes.js +94 -0
- package/dist/src/booking-surface/index.js +15 -0
- package/dist/src/booking-surface/profile.js +68 -0
- package/dist/src/booking-surface/runtime-v2.js +1771 -0
- package/dist/src/booking-surface/runtime.js +351 -0
- package/dist/src/booking-surface/server-v2.js +334 -0
- package/dist/src/booking-surface/server.js +302 -0
- package/dist/src/booking-surface/startup.js +159 -0
- package/dist/src/booking-surface/validation-v2.js +319 -0
- package/dist/src/booking-surface/validation.js +809 -0
- package/dist/src/bridge.js +1 -1
- package/dist/src/companions.js +1 -1
- package/dist/src/contracts.js +1 -1
- package/dist/src/dsh-llm.js +1 -1
- package/dist/src/engine.js +1 -1
- package/dist/src/evaluation-cadence.js +234 -0
- package/dist/src/evaluation-contracts.js +906 -0
- package/dist/src/i18n.js +1 -1
- package/dist/src/index.js +234 -60
- package/dist/src/journey.js +1 -1
- package/dist/src/loop.js +1 -1
- package/dist/src/memory-capture.js +1 -1
- package/dist/src/memory-decay.js +1 -1
- package/dist/src/memory-utility.js +1 -1
- package/dist/src/mock-llm.js +1 -1
- package/dist/src/model.js +1 -1
- package/dist/src/realtime-pricing.js +14 -7
- package/dist/src/slot-spec.js +1 -1
- package/dist/src/state-ledger.js +2 -1
- package/dist/src/time-anchor.js +1 -1
- package/dist/src/tool-budget.js +136 -0
- package/dist/src/tool-packet.js +1 -1
- package/dist/src/travel-slots.js +1 -1
- package/dist/src/travel-timeline.js +1 -1
- package/dist/src/unified.js +1 -1
- package/dist/src/wish-pool.js +1 -1
- package/dist/src/z3-shared.js +1 -1
- package/extension/README.md +50 -0
- package/extension/background.js +160 -0
- package/extension/content-bridge.js +41 -0
- package/extension/content-main.js +62 -0
- package/extension/manifest.json +32 -0
- package/package.json +291 -12
- package/schemas/booking.surface.v1.schema.json +927 -0
- package/schemas/booking.surface.v2.schema.json +61 -0
- package/ts/capabilities/fact-log.ts +54 -0
- package/ts/capabilities/flyai.ts +16 -3
- package/ts/capabilities/hbcli.ts +46 -0
- package/ts/capabilities/session/benchmark.ts +4 -1
- package/ts/capabilities/session/extension-bridge.ts +377 -0
- package/ts/capabilities/session/extension-channel.ts +95 -0
- package/ts/capabilities/session/extension-distribution.ts +264 -0
- package/ts/capabilities/session/golden-score.ts +139 -0
- package/ts/capabilities/session/health-watch.ts +257 -0
- package/ts/capabilities/session/static-flight-golden.ts +209 -0
- package/ts/capabilities/session/wizard.ts +140 -0
- package/ts/capabilities/session-login.ts +63 -2
- package/ts/capabilities/session-search.ts +105 -3
- package/ts/capabilities/weather.ts +141 -52
- package/ts/package.json +3 -3
- package/ts/src/artifact-gate.ts +365 -0
- package/ts/src/benchmark-agent-conformance.ts +448 -0
- package/ts/src/benchmark-environment-bridge.ts +348 -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 +557 -0
- package/ts/src/booking-surface/availability-policy-v2.ts +523 -0
- package/ts/src/booking-surface/canonical-schema.js +113 -0
- package/ts/src/booking-surface/contracts-v2.ts +118 -0
- package/ts/src/booking-surface/contracts.ts +380 -0
- package/ts/src/booking-surface/dsh-planner.ts +452 -0
- package/ts/src/booking-surface/dsh-plugin.js +93 -0
- package/ts/src/booking-surface/error-codes.ts +101 -0
- package/ts/src/booking-surface/index.ts +12 -0
- package/ts/src/booking-surface/profile.ts +42 -0
- package/ts/src/booking-surface/runtime-v2.ts +1466 -0
- package/ts/src/booking-surface/runtime.ts +483 -0
- package/ts/src/booking-surface/server-v2.ts +247 -0
- package/ts/src/booking-surface/server.ts +324 -0
- package/ts/src/booking-surface/startup.ts +196 -0
- package/ts/src/booking-surface/validation-v2.ts +205 -0
- package/ts/src/booking-surface/validation.ts +453 -0
- package/ts/src/index.ts +161 -14
- package/ts/src/state-ledger.ts +1 -0
- package/ts/src/tool-budget.ts +165 -0
|
@@ -0,0 +1,483 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Durable, task-scoped runtime for the embedded Booking Copilot.
|
|
3
|
+
*
|
|
4
|
+
* This is deliberately not a chat transcript. The ledger stores task identity,
|
|
5
|
+
* typed operation metadata, typed receipts and digests of prose/payloads. Raw
|
|
6
|
+
* user text and raw action criteria never enter durable state.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import { createHash, randomUUID } from 'node:crypto'
|
|
10
|
+
import type { StateLedger } from '../state-ledger.ts'
|
|
11
|
+
import {
|
|
12
|
+
type ActionReceiptContinuationV1,
|
|
13
|
+
type ActionReceiptV1,
|
|
14
|
+
type BookingExplanationEventV1,
|
|
15
|
+
type BookingQuestionEventV1,
|
|
16
|
+
type BookingReadActionKindV1,
|
|
17
|
+
type BookingReadActionV1,
|
|
18
|
+
type BookingStatusEventV1,
|
|
19
|
+
type BookingSurfaceEventV1,
|
|
20
|
+
type BookingTerminalEventV1,
|
|
21
|
+
type BookingErrorEventV1,
|
|
22
|
+
type BookingOperationEventV1,
|
|
23
|
+
type BookingSurfaceV1,
|
|
24
|
+
type UserTurnV1,
|
|
25
|
+
} from './contracts.ts'
|
|
26
|
+
import {
|
|
27
|
+
validateBookingCopilotTurnV1,
|
|
28
|
+
validateActionReceiptV1,
|
|
29
|
+
validateBookingReadActionV1,
|
|
30
|
+
validateBookingSurfaceEventV1,
|
|
31
|
+
} from './validation.ts'
|
|
32
|
+
import { normalizeBookingErrorCode, safeBookingErrorMessage } from './error-codes.ts'
|
|
33
|
+
|
|
34
|
+
export type {
|
|
35
|
+
ActionReceiptContinuationV1,
|
|
36
|
+
ActionReceiptV1,
|
|
37
|
+
BookingReadActionV1,
|
|
38
|
+
BookingSurfaceEventV1,
|
|
39
|
+
UserTurnV1,
|
|
40
|
+
} from './contracts.ts'
|
|
41
|
+
|
|
42
|
+
const LEDGER_SCHEMA = 'booking.copilot.ledger.v1' as const
|
|
43
|
+
const LEDGER_ACTOR = 'system:booking-copilot'
|
|
44
|
+
const TASK_STARTED = 'booking.copilot.task.started'
|
|
45
|
+
const USER_TURN_OBSERVED = 'booking.copilot.user.turn.observed'
|
|
46
|
+
const ACTION_ISSUED = 'booking.copilot.action.issued'
|
|
47
|
+
const RECEIPT_OBSERVED = 'booking.copilot.receipt.observed'
|
|
48
|
+
const EVENT_EMITTED = 'booking.copilot.event.emitted'
|
|
49
|
+
|
|
50
|
+
export type BookingCopilotTaskPhaseV1 = 'planning' | 'waiting_receipt'
|
|
51
|
+
|
|
52
|
+
export interface BookingActionCheckpointV1 {
|
|
53
|
+
actionId: string
|
|
54
|
+
kind: BookingReadActionKindV1
|
|
55
|
+
contextRef: string
|
|
56
|
+
expectedRevision: number
|
|
57
|
+
factRefs: string[]
|
|
58
|
+
reasonDigest: string
|
|
59
|
+
inputDigest: string
|
|
60
|
+
actionDigest: string
|
|
61
|
+
eventId: string
|
|
62
|
+
sequence: number
|
|
63
|
+
emittedAt: string
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
export interface BookingCopilotTaskStateV1 {
|
|
67
|
+
schemaVersion: 'booking.surface.v1'
|
|
68
|
+
taskId: string
|
|
69
|
+
contextRef: string
|
|
70
|
+
surface: BookingSurfaceV1
|
|
71
|
+
revision: number
|
|
72
|
+
allowedActions: BookingReadActionKindV1[]
|
|
73
|
+
userTurnCount: number
|
|
74
|
+
lastUserTurnDigest?: string
|
|
75
|
+
phase: BookingCopilotTaskPhaseV1
|
|
76
|
+
lastSequence: number
|
|
77
|
+
pendingAction?: BookingActionCheckpointV1
|
|
78
|
+
lastReceipt?: ActionReceiptV1
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
export type BookingSurfaceEventDraftV1 =
|
|
82
|
+
| Pick<BookingStatusEventV1, 'kind' | 'status'>
|
|
83
|
+
| Pick<BookingQuestionEventV1, 'kind' | 'question'>
|
|
84
|
+
| Pick<BookingExplanationEventV1, 'kind' | 'explanation'>
|
|
85
|
+
| Pick<BookingTerminalEventV1, 'kind' | 'terminal'>
|
|
86
|
+
| Pick<BookingErrorEventV1, 'kind' | 'error'>
|
|
87
|
+
|
|
88
|
+
export interface BookingCopilotRuntimeOptions {
|
|
89
|
+
idFactory?: (prefix: string) => string
|
|
90
|
+
now?: () => string
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
interface LedgerPayloadBase {
|
|
94
|
+
schema: typeof LEDGER_SCHEMA
|
|
95
|
+
taskId: string
|
|
96
|
+
contextRef: string
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
interface TaskStartedPayload extends LedgerPayloadBase {
|
|
100
|
+
surface: BookingSurfaceV1
|
|
101
|
+
revision: number
|
|
102
|
+
allowedActions: BookingReadActionKindV1[]
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
interface UserTurnObservedPayload extends LedgerPayloadBase {
|
|
106
|
+
requestDigest: string
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
interface ActionIssuedPayload extends LedgerPayloadBase {
|
|
110
|
+
action: BookingActionCheckpointV1
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
interface ReceiptObservedPayload extends LedgerPayloadBase {
|
|
114
|
+
receipt: ActionReceiptV1
|
|
115
|
+
receiptDigest: string
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
interface EventEmittedPayload extends LedgerPayloadBase {
|
|
119
|
+
eventId: string
|
|
120
|
+
sequence: number
|
|
121
|
+
emittedAt: string
|
|
122
|
+
eventKind: Exclude<BookingSurfaceEventV1['kind'], 'operation'>
|
|
123
|
+
contentDigest: string
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
interface BookingLedgerRow {
|
|
127
|
+
seq: number
|
|
128
|
+
kind: string
|
|
129
|
+
payload: string
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
function stableValue(value: unknown): unknown {
|
|
133
|
+
if (Array.isArray(value)) return value.map(stableValue)
|
|
134
|
+
if (typeof value !== 'object' || value === null) return value
|
|
135
|
+
const source = value as Record<string, unknown>
|
|
136
|
+
const target: Record<string, unknown> = {}
|
|
137
|
+
for (const key of Object.keys(source).sort()) target[key] = stableValue(source[key])
|
|
138
|
+
return target
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
function digest(value: unknown): string {
|
|
142
|
+
return createHash('sha256').update(JSON.stringify(stableValue(value))).digest('hex')
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
function checkpointDigest(action: Pick<BookingActionCheckpointV1, 'actionId' | 'kind' | 'contextRef' | 'expectedRevision' | 'factRefs' | 'reasonDigest' | 'inputDigest' | 'eventId' | 'sequence' | 'emittedAt'>): string {
|
|
146
|
+
return digest({
|
|
147
|
+
actionId: action.actionId,
|
|
148
|
+
kind: action.kind,
|
|
149
|
+
contextRef: action.contextRef,
|
|
150
|
+
expectedRevision: action.expectedRevision,
|
|
151
|
+
factRefs: action.factRefs,
|
|
152
|
+
reasonDigest: action.reasonDigest,
|
|
153
|
+
inputDigest: action.inputDigest,
|
|
154
|
+
eventId: action.eventId,
|
|
155
|
+
sequence: action.sequence,
|
|
156
|
+
emittedAt: action.emittedAt,
|
|
157
|
+
})
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
function actionMatchesCheckpoint(action: BookingReadActionV1, checkpoint: BookingActionCheckpointV1): boolean {
|
|
161
|
+
return checkpoint.actionDigest === checkpointDigest({
|
|
162
|
+
actionId: checkpoint.actionId,
|
|
163
|
+
kind: action.kind,
|
|
164
|
+
contextRef: action.contextRef,
|
|
165
|
+
expectedRevision: action.expectedRevision,
|
|
166
|
+
factRefs: action.factRefs,
|
|
167
|
+
reasonDigest: digest(action.reason),
|
|
168
|
+
inputDigest: digest(action.input),
|
|
169
|
+
eventId: checkpoint.eventId,
|
|
170
|
+
sequence: checkpoint.sequence,
|
|
171
|
+
emittedAt: checkpoint.emittedAt,
|
|
172
|
+
})
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
function assertSafeFactRefs(action: BookingReadActionV1): void {
|
|
176
|
+
const opaque = (ref: string) => /^[A-Za-z0-9][A-Za-z0-9:._-]*$/.test(ref) && !/^eyJ[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+$/i.test(ref)
|
|
177
|
+
if (!opaque(action.actionId)) throw new Error('unsafe_action_id')
|
|
178
|
+
if (action.factRefs.some((ref) => !opaque(ref))) throw new Error('unsafe_fact_ref')
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
function validationMessage(result: ReturnType<typeof validateBookingCopilotTurnV1>): string {
|
|
182
|
+
return result.ok ? '' : result.errors.join('; ')
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
function assertTaskId(taskId: string): void {
|
|
186
|
+
if (!taskId) throw new Error('invalid_task_id')
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
function sameActionAllowlist(left: readonly BookingReadActionKindV1[], right: readonly BookingReadActionKindV1[]): boolean {
|
|
190
|
+
return left.length === right.length && [...left].sort().every((kind, index) => kind === [...right].sort()[index])
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
export class BookingCopilotTaskRuntime {
|
|
194
|
+
private readonly idFactory: (prefix: string) => string
|
|
195
|
+
private readonly now: () => string
|
|
196
|
+
|
|
197
|
+
constructor(
|
|
198
|
+
private readonly ledger: StateLedger,
|
|
199
|
+
options: BookingCopilotRuntimeOptions = {},
|
|
200
|
+
) {
|
|
201
|
+
this.idFactory = options.idFactory ?? ((prefix) => `${prefix}-${randomUUID()}`)
|
|
202
|
+
this.now = options.now ?? (() => new Date().toISOString())
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
startTask(turn: UserTurnV1): BookingCopilotTaskStateV1 {
|
|
206
|
+
const validation = validateBookingCopilotTurnV1(turn)
|
|
207
|
+
if (!validation.ok || turn.kind !== 'user.turn') throw new Error(`invalid_planner_turn:${validationMessage(validation)}`)
|
|
208
|
+
const taskId = turn.taskId ?? this.idFactory('task')
|
|
209
|
+
assertTaskId(taskId)
|
|
210
|
+
const requestDigest = digest(turn.request.text)
|
|
211
|
+
// The task fold and its user-turn append must share one write transaction.
|
|
212
|
+
// An immediate transaction serializes separate processes before either can
|
|
213
|
+
// derive state (and therefore an ordinal/next event sequence) from a stale
|
|
214
|
+
// snapshot.
|
|
215
|
+
const apply = this.ledger.db.transaction(() => {
|
|
216
|
+
const existing = this.resumeTask(taskId)
|
|
217
|
+
if (existing) {
|
|
218
|
+
if (existing.contextRef !== turn.workspace.contextRef) throw new Error('task_conflict:context_mismatch')
|
|
219
|
+
if (existing.surface !== turn.workspace.surface) throw new Error('task_conflict:surface_mismatch')
|
|
220
|
+
if (existing.revision !== turn.workspace.revision) throw new Error('task_conflict:revision_mismatch')
|
|
221
|
+
if (!sameActionAllowlist(existing.allowedActions, turn.workspace.capabilities.allowedActions)) {
|
|
222
|
+
throw new Error('task_conflict:capability_mismatch')
|
|
223
|
+
}
|
|
224
|
+
if (existing.phase === 'waiting_receipt') throw new Error('receipt_required')
|
|
225
|
+
this.observeUserTurn(taskId, existing.contextRef, requestDigest, existing.userTurnCount + 1)
|
|
226
|
+
return this.requireTask(taskId)
|
|
227
|
+
}
|
|
228
|
+
const foreign = this.ledger.db.prepare(
|
|
229
|
+
"SELECT 1 AS present FROM events WHERE tenant_id = ? AND run_id = ? AND kind LIKE 'booking.copilot.v2.%' LIMIT 1",
|
|
230
|
+
).get(this.ledger.tenant, taskId) as { present: number } | undefined
|
|
231
|
+
if (foreign) throw new Error('task_conflict:protocol_version')
|
|
232
|
+
const payload: TaskStartedPayload = {
|
|
233
|
+
schema: LEDGER_SCHEMA,
|
|
234
|
+
taskId,
|
|
235
|
+
contextRef: turn.workspace.contextRef,
|
|
236
|
+
surface: turn.workspace.surface,
|
|
237
|
+
revision: turn.workspace.revision,
|
|
238
|
+
allowedActions: [...turn.workspace.capabilities.allowedActions],
|
|
239
|
+
}
|
|
240
|
+
this.appendLedgerEvent(TASK_STARTED, taskId, payload, `booking-copilot:task:${taskId}`)
|
|
241
|
+
this.observeUserTurn(taskId, turn.workspace.contextRef, requestDigest, 1)
|
|
242
|
+
return this.requireTask(taskId)
|
|
243
|
+
})
|
|
244
|
+
return apply.immediate()
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
resumeTask(taskId: string): BookingCopilotTaskStateV1 | null {
|
|
248
|
+
assertTaskId(taskId)
|
|
249
|
+
const rows = this.readTaskRows(taskId)
|
|
250
|
+
if (rows.length === 0) return null
|
|
251
|
+
let state: BookingCopilotTaskStateV1 | null = null
|
|
252
|
+
for (const row of rows) {
|
|
253
|
+
const payload = JSON.parse(row.payload) as Record<string, unknown>
|
|
254
|
+
if (payload.schema !== LEDGER_SCHEMA || payload.taskId !== taskId) throw new Error(`ledger_corrupt:${taskId}:seq=${row.seq}`)
|
|
255
|
+
if (row.kind === TASK_STARTED) {
|
|
256
|
+
const started = payload as unknown as TaskStartedPayload
|
|
257
|
+
if (state) throw new Error(`ledger_corrupt:${taskId}:duplicate_start`)
|
|
258
|
+
state = {
|
|
259
|
+
schemaVersion: 'booking.surface.v1',
|
|
260
|
+
taskId,
|
|
261
|
+
contextRef: started.contextRef,
|
|
262
|
+
surface: started.surface,
|
|
263
|
+
revision: started.revision,
|
|
264
|
+
allowedActions: [...started.allowedActions],
|
|
265
|
+
userTurnCount: 0,
|
|
266
|
+
phase: 'planning',
|
|
267
|
+
lastSequence: 0,
|
|
268
|
+
}
|
|
269
|
+
continue
|
|
270
|
+
}
|
|
271
|
+
if (!state) throw new Error(`ledger_corrupt:${taskId}:event_before_start`)
|
|
272
|
+
if (payload.contextRef !== state.contextRef) throw new Error(`ledger_corrupt:${taskId}:context_drift`)
|
|
273
|
+
if (row.kind === USER_TURN_OBSERVED) {
|
|
274
|
+
const observed = payload as unknown as UserTurnObservedPayload
|
|
275
|
+
state.userTurnCount += 1
|
|
276
|
+
state.lastUserTurnDigest = observed.requestDigest
|
|
277
|
+
} else if (row.kind === ACTION_ISSUED) {
|
|
278
|
+
const issued = payload as unknown as ActionIssuedPayload
|
|
279
|
+
if (issued.action.actionDigest !== checkpointDigest(issued.action)) throw new Error(`ledger_corrupt:${taskId}:action`)
|
|
280
|
+
if (state.pendingAction) throw new Error(`ledger_corrupt:${taskId}:parallel_actions`)
|
|
281
|
+
state.pendingAction = issued.action
|
|
282
|
+
state.phase = 'waiting_receipt'
|
|
283
|
+
state.lastSequence = Math.max(state.lastSequence, issued.action.sequence)
|
|
284
|
+
} else if (row.kind === RECEIPT_OBSERVED) {
|
|
285
|
+
const observed = payload as unknown as ReceiptObservedPayload
|
|
286
|
+
if (observed.receiptDigest !== digest(observed.receipt) || !validateActionReceiptV1(observed.receipt).ok) {
|
|
287
|
+
throw new Error(`ledger_corrupt:${taskId}:receipt`)
|
|
288
|
+
}
|
|
289
|
+
if (!state.pendingAction || state.pendingAction.actionId !== observed.receipt.actionId) {
|
|
290
|
+
throw new Error(`ledger_corrupt:${taskId}:orphan_receipt`)
|
|
291
|
+
}
|
|
292
|
+
state.lastReceipt = observed.receipt
|
|
293
|
+
state.revision = observed.receipt.revision
|
|
294
|
+
delete state.pendingAction
|
|
295
|
+
state.phase = 'planning'
|
|
296
|
+
} else if (row.kind === EVENT_EMITTED) {
|
|
297
|
+
const emitted = payload as unknown as EventEmittedPayload
|
|
298
|
+
state.lastSequence = Math.max(state.lastSequence, emitted.sequence)
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
return state
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
issueOperation(taskId: string, action: BookingReadActionV1): BookingOperationEventV1 {
|
|
305
|
+
const validation = validateBookingReadActionV1(action)
|
|
306
|
+
if (!validation.ok) throw new Error(`invalid_action:${validation.errors.join('; ')}`)
|
|
307
|
+
assertSafeFactRefs(action)
|
|
308
|
+
const apply = this.ledger.db.transaction(() => {
|
|
309
|
+
const state = this.requireTask(taskId)
|
|
310
|
+
const reasonDigest = digest(action.reason)
|
|
311
|
+
const inputDigest = digest(action.input)
|
|
312
|
+
if (state.pendingAction) {
|
|
313
|
+
if (state.pendingAction.actionId !== action.actionId) throw new Error('receipt_required')
|
|
314
|
+
if (!actionMatchesCheckpoint(action, state.pendingAction)) throw new Error('action_conflict')
|
|
315
|
+
return {
|
|
316
|
+
schemaVersion: 'booking.surface.v1' as const, eventId: state.pendingAction.eventId, taskId,
|
|
317
|
+
contextRef: state.pendingAction.contextRef, sequence: state.pendingAction.sequence,
|
|
318
|
+
emittedAt: state.pendingAction.emittedAt, kind: 'operation' as const, action,
|
|
319
|
+
}
|
|
320
|
+
}
|
|
321
|
+
const historicalReceipt = this.ledger.db.prepare(
|
|
322
|
+
`SELECT payload FROM events WHERE tenant_id = ? AND run_id = ? AND kind = ?`,
|
|
323
|
+
).all(this.ledger.tenant, taskId, RECEIPT_OBSERVED) as Array<{ payload: string }>
|
|
324
|
+
if (historicalReceipt.some((row) => (JSON.parse(row.payload) as ReceiptObservedPayload).receipt?.actionId === action.actionId)) throw new Error('action_already_receipted')
|
|
325
|
+
if (state.lastReceipt?.actionId === action.actionId) throw new Error('action_already_receipted')
|
|
326
|
+
if (action.contextRef !== state.contextRef) throw new Error('context_mismatch')
|
|
327
|
+
if (!new Set<string>(state.allowedActions).has(action.kind)) throw new Error('unsupported_action')
|
|
328
|
+
if (action.expectedRevision !== state.revision) throw new Error('stale_revision')
|
|
329
|
+
const event: BookingOperationEventV1 = {
|
|
330
|
+
schemaVersion: 'booking.surface.v1', eventId: this.idFactory('event'), taskId,
|
|
331
|
+
contextRef: state.contextRef, sequence: state.lastSequence + 1, emittedAt: this.now(), kind: 'operation', action,
|
|
332
|
+
}
|
|
333
|
+
const eventValidation = validateBookingSurfaceEventV1(event)
|
|
334
|
+
if (!eventValidation.ok) throw new Error(`invalid_operation_event:${eventValidation.errors.join('; ')}`)
|
|
335
|
+
const checkpointBase = { actionId: action.actionId, kind: action.kind, contextRef: action.contextRef, expectedRevision: action.expectedRevision, factRefs: [...action.factRefs], reasonDigest, inputDigest, eventId: event.eventId, sequence: event.sequence, emittedAt: event.emittedAt }
|
|
336
|
+
const checkpoint: BookingActionCheckpointV1 = { ...checkpointBase, actionDigest: checkpointDigest(checkpointBase) }
|
|
337
|
+
const payload: ActionIssuedPayload = { schema: LEDGER_SCHEMA, taskId, contextRef: state.contextRef, action: checkpoint }
|
|
338
|
+
this.appendActionIssued(taskId, payload, `booking-copilot:action:${taskId.length}:${taskId}:${action.actionId.length}:${action.actionId}`)
|
|
339
|
+
return event
|
|
340
|
+
})
|
|
341
|
+
return apply.immediate()
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
continueWithReceipt(turn: ActionReceiptContinuationV1): BookingCopilotTaskStateV1 {
|
|
345
|
+
const validation = validateBookingCopilotTurnV1(turn)
|
|
346
|
+
if (!validation.ok || turn.kind !== 'action.receipt.continuation') {
|
|
347
|
+
throw new Error(`invalid_receipt_continuation:${validationMessage(validation)}`)
|
|
348
|
+
}
|
|
349
|
+
const receiptDigest = digest(turn.receipt)
|
|
350
|
+
const apply = this.ledger.db.transaction(() => {
|
|
351
|
+
const state = this.requireTask(turn.taskId)
|
|
352
|
+
if (turn.workspace.contextRef !== state.contextRef || turn.receipt.contextRef !== state.contextRef) throw new Error('context_mismatch')
|
|
353
|
+
// Receipt replay is keyed by task + action, not merely by the latest fold state.
|
|
354
|
+
const history = this.ledger.db.prepare(
|
|
355
|
+
`SELECT payload FROM events WHERE tenant_id = ? AND run_id = ? AND kind = ? ORDER BY seq`,
|
|
356
|
+
).all(this.ledger.tenant, turn.taskId, RECEIPT_OBSERVED) as Array<{ payload: string }>
|
|
357
|
+
for (const row of history) {
|
|
358
|
+
const prior = JSON.parse(row.payload) as ReceiptObservedPayload
|
|
359
|
+
if (prior.receipt?.actionId !== turn.receipt.actionId) continue
|
|
360
|
+
if (prior.schema !== LEDGER_SCHEMA || prior.taskId !== turn.taskId || prior.contextRef !== state.contextRef || prior.receiptDigest !== digest(prior.receipt) || !validateActionReceiptV1(prior.receipt).ok) throw new Error('ledger_corrupt:receipt')
|
|
361
|
+
if (prior.receiptDigest !== receiptDigest) throw new Error('receipt_conflict')
|
|
362
|
+
return state
|
|
363
|
+
}
|
|
364
|
+
if (!state.pendingAction) throw new Error('unexpected_receipt')
|
|
365
|
+
if (state.pendingAction.actionId !== turn.receipt.actionId) throw new Error('receipt_action_mismatch')
|
|
366
|
+
if (turn.receipt.revision < state.pendingAction.expectedRevision) throw new Error('revision_regression')
|
|
367
|
+
const payload: ReceiptObservedPayload = { schema: LEDGER_SCHEMA, taskId: turn.taskId, contextRef: state.contextRef, receipt: turn.receipt, receiptDigest }
|
|
368
|
+
const idemKey = `booking-copilot:receipt:${turn.taskId.length}:${turn.taskId}:${turn.receipt.actionId.length}:${turn.receipt.actionId}`
|
|
369
|
+
const info = this.ledger.db.prepare(
|
|
370
|
+
`INSERT INTO events (tenant_id, ts, actor, kind, subject_id, payload, idem_key, run_id) VALUES (?, ?, ?, ?, ?, ?, ?, ?)`,
|
|
371
|
+
).run(this.ledger.tenant, this.now(), LEDGER_ACTOR, RECEIPT_OBSERVED, turn.taskId, JSON.stringify(payload), idemKey, turn.taskId)
|
|
372
|
+
if (info.changes !== 1) throw new Error('ledger_write_failed:receipt')
|
|
373
|
+
return this.requireTask(turn.taskId)
|
|
374
|
+
})
|
|
375
|
+
return apply.immediate()
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
emitEvent(taskId: string, draft: BookingSurfaceEventDraftV1): Exclude<BookingSurfaceEventV1, BookingOperationEventV1> {
|
|
379
|
+
// Sequence allocation is part of the same immediate transaction as the
|
|
380
|
+
// durable append. This makes the fold read and next-sequence write one
|
|
381
|
+
// critical section across independent runtime processes.
|
|
382
|
+
const apply = this.ledger.db.transaction(() => {
|
|
383
|
+
const state = this.requireTask(taskId)
|
|
384
|
+
const safeDraft = draft.kind === 'error'
|
|
385
|
+
? { ...draft, error: { ...draft.error, code: normalizeBookingErrorCode(draft.error.code), message: safeBookingErrorMessage(normalizeBookingErrorCode(draft.error.code)) } }
|
|
386
|
+
: draft
|
|
387
|
+
const event = {
|
|
388
|
+
schemaVersion: 'booking.surface.v1',
|
|
389
|
+
eventId: this.idFactory('event'),
|
|
390
|
+
taskId,
|
|
391
|
+
contextRef: state.contextRef,
|
|
392
|
+
sequence: state.lastSequence + 1,
|
|
393
|
+
emittedAt: this.now(),
|
|
394
|
+
...safeDraft,
|
|
395
|
+
} as Exclude<BookingSurfaceEventV1, BookingOperationEventV1>
|
|
396
|
+
const validation = validateBookingSurfaceEventV1(event)
|
|
397
|
+
if (!validation.ok) throw new Error(`invalid_event:${validation.errors.join('; ')}`)
|
|
398
|
+
const payload: EventEmittedPayload = {
|
|
399
|
+
schema: LEDGER_SCHEMA,
|
|
400
|
+
taskId,
|
|
401
|
+
contextRef: state.contextRef,
|
|
402
|
+
eventId: event.eventId,
|
|
403
|
+
sequence: event.sequence,
|
|
404
|
+
emittedAt: event.emittedAt,
|
|
405
|
+
eventKind: event.kind,
|
|
406
|
+
contentDigest: digest(safeDraft),
|
|
407
|
+
}
|
|
408
|
+
this.appendLedgerEvent(EVENT_EMITTED, taskId, payload, `booking-copilot:event:${taskId}:${event.eventId}`)
|
|
409
|
+
return event
|
|
410
|
+
})
|
|
411
|
+
return apply.immediate()
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
private requireTask(taskId: string): BookingCopilotTaskStateV1 {
|
|
415
|
+
const state = this.resumeTask(taskId)
|
|
416
|
+
if (!state) throw new Error('task_not_found')
|
|
417
|
+
return state
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
private readTaskRows(taskId: string): BookingLedgerRow[] {
|
|
421
|
+
return this.ledger.db.prepare(
|
|
422
|
+
`SELECT seq, kind, payload FROM events
|
|
423
|
+
WHERE tenant_id = ? AND run_id = ?
|
|
424
|
+
AND kind IN (?, ?, ?, ?, ?)
|
|
425
|
+
ORDER BY seq`,
|
|
426
|
+
).all(
|
|
427
|
+
this.ledger.tenant,
|
|
428
|
+
taskId,
|
|
429
|
+
TASK_STARTED,
|
|
430
|
+
USER_TURN_OBSERVED,
|
|
431
|
+
ACTION_ISSUED,
|
|
432
|
+
RECEIPT_OBSERVED,
|
|
433
|
+
EVENT_EMITTED,
|
|
434
|
+
) as BookingLedgerRow[]
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
private observeUserTurn(taskId: string, contextRef: string, requestDigest: string, ordinal: number): void {
|
|
438
|
+
const payload: UserTurnObservedPayload = {
|
|
439
|
+
schema: LEDGER_SCHEMA,
|
|
440
|
+
taskId,
|
|
441
|
+
contextRef,
|
|
442
|
+
requestDigest,
|
|
443
|
+
}
|
|
444
|
+
this.appendLedgerEvent(
|
|
445
|
+
USER_TURN_OBSERVED,
|
|
446
|
+
taskId,
|
|
447
|
+
payload,
|
|
448
|
+
`booking-copilot:user-turn:${taskId}:${ordinal}`,
|
|
449
|
+
)
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
private appendLedgerEvent(kind: string, taskId: string, payload: LedgerPayloadBase, idemKey: string): number {
|
|
453
|
+
const result = this.ledger.db.prepare(
|
|
454
|
+
`INSERT OR IGNORE INTO events
|
|
455
|
+
(tenant_id, ts, actor, kind, subject_id, payload, idem_key, run_id)
|
|
456
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?)`,
|
|
457
|
+
).run(
|
|
458
|
+
this.ledger.tenant,
|
|
459
|
+
this.now(),
|
|
460
|
+
LEDGER_ACTOR,
|
|
461
|
+
kind,
|
|
462
|
+
taskId,
|
|
463
|
+
JSON.stringify(payload),
|
|
464
|
+
idemKey,
|
|
465
|
+
taskId,
|
|
466
|
+
)
|
|
467
|
+
if (result.changes > 0) return Number(result.lastInsertRowid)
|
|
468
|
+
const existing = this.ledger.db.prepare(
|
|
469
|
+
'SELECT seq FROM events WHERE tenant_id = ? AND idem_key = ?',
|
|
470
|
+
).get(this.ledger.tenant, idemKey) as { seq: number } | undefined
|
|
471
|
+
if (!existing) throw new Error(`ledger_write_failed:${kind}`)
|
|
472
|
+
return existing.seq
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
private appendActionIssued(taskId: string, payload: ActionIssuedPayload, idemKey: string): number {
|
|
476
|
+
const result = this.ledger.db.prepare(
|
|
477
|
+
`INSERT INTO events (tenant_id, ts, actor, kind, subject_id, payload, idem_key, run_id)
|
|
478
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?)`,
|
|
479
|
+
).run(this.ledger.tenant, this.now(), LEDGER_ACTOR, ACTION_ISSUED, taskId, JSON.stringify(payload), idemKey, taskId)
|
|
480
|
+
if (result.changes !== 1) throw new Error('ledger_write_failed:action')
|
|
481
|
+
return Number(result.lastInsertRowid)
|
|
482
|
+
}
|
|
483
|
+
}
|