@danceiny/gotry 0.0.1-rc.16 → 0.0.1-rc.18
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 +161 -116
- package/README.zh-CN.md +173 -144
- package/bin/gotry-booking-copilot.js +53 -0
- package/bin/gotry-bootstrap.js +113 -66
- package/bin/gotry-inner.js +436 -70
- 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 +5 -1
- 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 +1 -1
- package/dist/capabilities/fact-log.js +1 -1
- package/dist/capabilities/flyai.js +20 -6
- package/dist/capabilities/hbcli.js +1 -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 +1 -1
- 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 +1 -1
- package/dist/capabilities/session/extension-bridge.js +19 -6
- package/dist/capabilities/session/extension-channel.js +3 -3
- 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 +1 -1
- 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 +21 -251
- package/dist/capabilities/session-consent.js +1 -1
- package/dist/capabilities/session-login.js +11 -5
- package/dist/capabilities/session-search.js +46 -4
- 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 +45 -39
- package/dist/scripts/build-changelog.js +1 -1
- package/dist/scripts/changelog-tests.js +1 -1
- 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 +1 -1
- 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 +128 -23
- package/dist/scripts/fact-gate-tests.js +1 -1
- package/dist/scripts/flyai-tests.js +1 -1
- package/dist/scripts/hbcli-e2e-tests.js +1 -1
- package/dist/scripts/hbcli-tests.js +1 -1
- package/dist/scripts/health-watch-cli.js +1 -1
- 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 +1 -1
- package/dist/scripts/nightly-evidence.js +1 -1
- package/dist/scripts/nudge-digest.js +1 -1
- package/dist/scripts/onboarding-tests.js +21 -53
- 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 +1 -1
- package/dist/scripts/price-drift-watch.js +1 -1
- 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 +79 -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 +1 -1
- 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 +1 -1
- 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 +50 -19
- 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 +1 -1
- 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 +31 -7
- package/package.json +286 -11
- package/schemas/booking.surface.v1.schema.json +927 -0
- package/schemas/booking.surface.v2.schema.json +61 -0
- package/ts/capabilities/flyai.ts +16 -3
- package/ts/capabilities/session/extension-bridge.ts +37 -14
- package/ts/capabilities/session/extension-channel.ts +6 -3
- 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 +1 -1
- package/ts/capabilities/session/static-flight-golden.ts +209 -0
- package/ts/capabilities/session/wizard.ts +34 -176
- package/ts/capabilities/session-login.ts +15 -5
- package/ts/capabilities/session-search.ts +40 -3
- package/ts/capabilities/weather.ts +141 -52
- package/ts/package.json +3 -3
- 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/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 +64 -11
- package/ts/src/state-ledger.ts +1 -0
- package/ts/src/tool-budget.ts +165 -0
- package/dist/scripts/wizard-bootstrap.js +0 -32
|
@@ -0,0 +1,453 @@
|
|
|
1
|
+
import {
|
|
2
|
+
BOOKING_READ_ACTION_KINDS,
|
|
3
|
+
BOOKING_RECEIPT_STATUSES,
|
|
4
|
+
BOOKING_SURFACES,
|
|
5
|
+
BOOKING_SURFACE_EVENT_KINDS,
|
|
6
|
+
type ActionReceiptV1,
|
|
7
|
+
type BookingCopilotIngressTurnV1,
|
|
8
|
+
type BookingCopilotTurnV1,
|
|
9
|
+
type BookingReadActionKindV1,
|
|
10
|
+
type BookingReadActionV1,
|
|
11
|
+
type BookingSurfaceEventV1,
|
|
12
|
+
type BookingWorkspaceIngressSnapshotV1,
|
|
13
|
+
type BookingWorkspaceSnapshotV1,
|
|
14
|
+
} from './contracts.ts'
|
|
15
|
+
import { Ajv2020, type ErrorObject, type ValidateFunction } from 'ajv/dist/2020.js'
|
|
16
|
+
import { readFileSync } from 'node:fs'
|
|
17
|
+
|
|
18
|
+
const CANONICAL_SCHEMA = JSON.parse(readFileSync(
|
|
19
|
+
new URL('../../../schemas/booking.surface.v1.schema.json', import.meta.url),
|
|
20
|
+
'utf8',
|
|
21
|
+
)) as Record<string, unknown>
|
|
22
|
+
const CANONICAL_SCHEMA_ID = 'https://gotry.dev/schemas/booking.surface.v1.schema.json'
|
|
23
|
+
const canonicalAjv = new Ajv2020({ allErrors: true, strict: true })
|
|
24
|
+
canonicalAjv.addSchema(CANONICAL_SCHEMA)
|
|
25
|
+
const validateCanonicalAction = canonicalAjv.compile({
|
|
26
|
+
$ref: `${CANONICAL_SCHEMA_ID}#/$defs/BookingReadActionV1`,
|
|
27
|
+
}) as ValidateFunction
|
|
28
|
+
const validateCanonicalReceipt = canonicalAjv.compile({
|
|
29
|
+
$ref: `${CANONICAL_SCHEMA_ID}#/$defs/ActionReceiptV1`,
|
|
30
|
+
}) as ValidateFunction
|
|
31
|
+
const validateCanonicalWorkspace = canonicalAjv.compile({ $ref: `${CANONICAL_SCHEMA_ID}#/$defs/BookingWorkspaceSnapshotV1` }) as ValidateFunction
|
|
32
|
+
const validateCanonicalIngressTurn = canonicalAjv.compile({ $ref: `${CANONICAL_SCHEMA_ID}#/$defs/BookingCopilotIngressTurnV1` }) as ValidateFunction
|
|
33
|
+
const validateCanonicalTurn = canonicalAjv.compile({ $ref: `${CANONICAL_SCHEMA_ID}#/$defs/BookingCopilotTurnV1` }) as ValidateFunction
|
|
34
|
+
|
|
35
|
+
export type BookingSurfaceValidationResult =
|
|
36
|
+
| { ok: true }
|
|
37
|
+
| { ok: false; errors: string[] }
|
|
38
|
+
|
|
39
|
+
const ACTION_KINDS = new Set<string>(BOOKING_READ_ACTION_KINDS)
|
|
40
|
+
const RECEIPT_STATUSES = new Set<string>(BOOKING_RECEIPT_STATUSES)
|
|
41
|
+
const SURFACES = new Set<string>(BOOKING_SURFACES)
|
|
42
|
+
const EVENT_KINDS = new Set<string>(BOOKING_SURFACE_EVENT_KINDS)
|
|
43
|
+
|
|
44
|
+
function isRecord(value: unknown): value is Record<string, unknown> {
|
|
45
|
+
return typeof value === 'object' && value !== null && !Array.isArray(value)
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function isNonEmptyString(value: unknown): value is string {
|
|
49
|
+
return typeof value === 'string' && value.length > 0
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function isRevision(value: unknown): value is number {
|
|
53
|
+
return Number.isInteger(value) && Number(value) >= 0
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function isStringArray(value: unknown): value is string[] {
|
|
57
|
+
return Array.isArray(value) && value.every(isNonEmptyString)
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function isCount(value: unknown): value is number {
|
|
61
|
+
return Number.isInteger(value) && Number(value) >= 0
|
|
62
|
+
}
|
|
63
|
+
function isSafeCode(value: unknown): value is string { return typeof value === 'string' && /^[a-z][a-z0-9._-]*$/.test(value) }
|
|
64
|
+
|
|
65
|
+
function receiptContainsUnsafeOpaqueText(value: unknown): boolean {
|
|
66
|
+
if (typeof value === 'string') return /[^\s@]+@[^\s@]+\.[^\s@]+|Bearer\s+[^\s]+|\beyJ[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+\b/i.test(value)
|
|
67
|
+
if (Array.isArray(value)) return value.some(receiptContainsUnsafeOpaqueText)
|
|
68
|
+
if (isRecord(value)) return Object.values(value).some(receiptContainsUnsafeOpaqueText)
|
|
69
|
+
return false
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
function exactKeys(
|
|
73
|
+
value: Record<string, unknown>,
|
|
74
|
+
allowed: readonly string[],
|
|
75
|
+
required: readonly string[],
|
|
76
|
+
at: string,
|
|
77
|
+
errors: string[],
|
|
78
|
+
): void {
|
|
79
|
+
const allow = new Set(allowed)
|
|
80
|
+
for (const key of Object.keys(value)) if (!allow.has(key)) errors.push(`${at}.${key}: additional property is forbidden`)
|
|
81
|
+
for (const key of required) if (!Object.hasOwn(value, key)) errors.push(`${at}.${key}: required`)
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
function validateIngressWorkspace(value: unknown, at: string, errors: string[]): value is BookingWorkspaceIngressSnapshotV1 {
|
|
85
|
+
if (!isRecord(value)) { errors.push(`${at}: object required`); return false }
|
|
86
|
+
exactKeys(value, [
|
|
87
|
+
'schemaVersion', 'revision', 'locale', 'currency', 'searchDraft', 'results',
|
|
88
|
+
'visibleHotels', 'loadedOffers', 'focusedHotelRef', 'shortlistedOfferRefs',
|
|
89
|
+
'selectedOfferRef', 'verifiedOfferRef',
|
|
90
|
+
], [
|
|
91
|
+
'schemaVersion', 'revision', 'locale', 'currency', 'searchDraft', 'results',
|
|
92
|
+
'visibleHotels', 'loadedOffers', 'shortlistedOfferRefs',
|
|
93
|
+
], at, errors)
|
|
94
|
+
if (value.schemaVersion !== 'booking.surface.v1') errors.push(`${at}.schemaVersion: booking.surface.v1 required`)
|
|
95
|
+
if (!isRevision(value.revision)) errors.push(`${at}.revision: non-negative integer required`)
|
|
96
|
+
if (!isNonEmptyString(value.locale)) errors.push(`${at}.locale: non-empty string required`)
|
|
97
|
+
if (typeof value.currency !== 'string' || !/^[A-Z]{3}$/.test(value.currency)) errors.push(`${at}.currency: ISO-4217 code required`)
|
|
98
|
+
if (!isRecord(value.searchDraft)) errors.push(`${at}.searchDraft: object required`)
|
|
99
|
+
if (!isRecord(value.results)) errors.push(`${at}.results: object required`)
|
|
100
|
+
if (!Array.isArray(value.visibleHotels)) errors.push(`${at}.visibleHotels: array required`)
|
|
101
|
+
if (!Array.isArray(value.loadedOffers)) errors.push(`${at}.loadedOffers: array required`)
|
|
102
|
+
if (!isStringArray(value.shortlistedOfferRefs)) errors.push(`${at}.shortlistedOfferRefs: opaque-ref array required`)
|
|
103
|
+
return errors.length === 0
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
function validateWorkspace(value: unknown, at: string, errors: string[]): value is BookingWorkspaceSnapshotV1 {
|
|
107
|
+
if (!isRecord(value)) { errors.push(`${at}: object required`); return false }
|
|
108
|
+
const {
|
|
109
|
+
contextRef: _contextRef,
|
|
110
|
+
surface: _surface,
|
|
111
|
+
capabilities: _capabilities,
|
|
112
|
+
...ingressShape
|
|
113
|
+
} = value
|
|
114
|
+
validateIngressWorkspace(ingressShape, at, errors)
|
|
115
|
+
const allowed = [
|
|
116
|
+
'schemaVersion', 'contextRef', 'surface', 'revision', 'locale', 'currency',
|
|
117
|
+
'searchDraft', 'results', 'visibleHotels', 'loadedOffers', 'focusedHotelRef',
|
|
118
|
+
'shortlistedOfferRefs', 'selectedOfferRef', 'verifiedOfferRef', 'capabilities',
|
|
119
|
+
]
|
|
120
|
+
exactKeys(value, allowed, [
|
|
121
|
+
'schemaVersion', 'contextRef', 'surface', 'revision', 'locale', 'currency',
|
|
122
|
+
'searchDraft', 'results', 'visibleHotels', 'loadedOffers', 'shortlistedOfferRefs',
|
|
123
|
+
'capabilities',
|
|
124
|
+
], at, errors)
|
|
125
|
+
if (!isNonEmptyString(value.contextRef)) errors.push(`${at}.contextRef: server-minted opaque ref required`)
|
|
126
|
+
if (typeof value.surface !== 'string' || !SURFACES.has(value.surface)) errors.push(`${at}.surface: unsupported surface`)
|
|
127
|
+
if (!isRecord(value.capabilities)) {
|
|
128
|
+
errors.push(`${at}.capabilities: object required`)
|
|
129
|
+
} else {
|
|
130
|
+
exactKeys(value.capabilities, ['surface', 'allowedActions'], ['surface', 'allowedActions'], `${at}.capabilities`, errors)
|
|
131
|
+
if (value.capabilities.surface !== value.surface) errors.push(`${at}.capabilities.surface: must equal workspace.surface`)
|
|
132
|
+
const actionKinds = value.capabilities.allowedActions
|
|
133
|
+
if (!Array.isArray(actionKinds) || !actionKinds.every((kind) => typeof kind === 'string' && ACTION_KINDS.has(kind))) {
|
|
134
|
+
errors.push(`${at}.capabilities.allowedActions: closed action-kind array required`)
|
|
135
|
+
} else if (new Set(actionKinds).size !== actionKinds.length) {
|
|
136
|
+
errors.push(`${at}.capabilities.allowedActions: duplicates forbidden`)
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
appendCanonicalErrors(validateCanonicalWorkspace, value, at, errors)
|
|
140
|
+
return errors.length === 0
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
const ACTION_INPUT_KEYS: Record<BookingReadActionKindV1, { allowed: readonly string[]; required: readonly string[] }> = {
|
|
144
|
+
'search.patch': { allowed: ['patch'], required: ['patch'] },
|
|
145
|
+
'search.run': { allowed: [], required: [] },
|
|
146
|
+
'results.view.patch': { allowed: ['patch'], required: ['patch'] },
|
|
147
|
+
'hotel.focus': { allowed: ['hotelRef'], required: ['hotelRef'] },
|
|
148
|
+
'hotel.select': { allowed: ['hotelRef'], required: ['hotelRef'] },
|
|
149
|
+
'offers.query': { allowed: ['hotelRefs', 'criteria'], required: ['hotelRefs', 'criteria'] },
|
|
150
|
+
'offers.view.patch': { allowed: ['hotelRef', 'criteria'], required: ['hotelRef', 'criteria'] },
|
|
151
|
+
'offers.compare': { allowed: ['offerRefs', 'requestedCount'], required: ['offerRefs', 'requestedCount'] },
|
|
152
|
+
'offer.select': { allowed: ['offerRef'], required: ['offerRef'] },
|
|
153
|
+
'offer.check': { allowed: ['offerRef'], required: ['offerRef'] },
|
|
154
|
+
'checkout.prepare': { allowed: ['offerRef', 'verifiedOfferRef'], required: ['offerRef', 'verifiedOfferRef'] },
|
|
155
|
+
'order.observe': { allowed: ['orderRef'], required: ['orderRef'] },
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
function collectBookingReadActionErrors(value: unknown, at: string): string[] {
|
|
159
|
+
const errors: string[] = []
|
|
160
|
+
if (!isRecord(value)) return [`${at}: typed action object required; text is never executable`]
|
|
161
|
+
exactKeys(value, [
|
|
162
|
+
'schemaVersion', 'kind', 'actionId', 'contextRef', 'expectedRevision',
|
|
163
|
+
'reason', 'factRefs', 'input',
|
|
164
|
+
], [
|
|
165
|
+
'schemaVersion', 'kind', 'actionId', 'contextRef', 'expectedRevision',
|
|
166
|
+
'reason', 'factRefs', 'input',
|
|
167
|
+
], at, errors)
|
|
168
|
+
if (value.schemaVersion !== 'booking.surface.v1') errors.push(`${at}.schemaVersion: booking.surface.v1 required`)
|
|
169
|
+
if (typeof value.kind !== 'string' || !ACTION_KINDS.has(value.kind)) {
|
|
170
|
+
errors.push(`${at}.kind: unregistered read action`)
|
|
171
|
+
}
|
|
172
|
+
if (!isNonEmptyString(value.actionId)) errors.push(`${at}.actionId: opaque ref required`)
|
|
173
|
+
if (!isNonEmptyString(value.contextRef)) errors.push(`${at}.contextRef: opaque ref required`)
|
|
174
|
+
if (!isRevision(value.expectedRevision)) errors.push(`${at}.expectedRevision: non-negative integer required`)
|
|
175
|
+
if (!isNonEmptyString(value.reason)) errors.push(`${at}.reason: non-empty explanation required`)
|
|
176
|
+
if (!isStringArray(value.factRefs)) errors.push(`${at}.factRefs: opaque-ref array required`)
|
|
177
|
+
if (!isRecord(value.input)) {
|
|
178
|
+
errors.push(`${at}.input: typed object required`)
|
|
179
|
+
} else if (typeof value.kind === 'string' && ACTION_KINDS.has(value.kind)) {
|
|
180
|
+
const spec = ACTION_INPUT_KEYS[value.kind as BookingReadActionKindV1]
|
|
181
|
+
exactKeys(value.input, spec.allowed, spec.required, `${at}.input`, errors)
|
|
182
|
+
}
|
|
183
|
+
if (!validateCanonicalAction(value)) {
|
|
184
|
+
for (const error of validateCanonicalAction.errors ?? []) {
|
|
185
|
+
errors.push(canonicalError(error, at))
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
return errors
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
function canonicalError(error: ErrorObject, at: string): string {
|
|
192
|
+
const path = error.instancePath ? error.instancePath.replaceAll('/', '.') : ''
|
|
193
|
+
const property = error.keyword === 'additionalProperties'
|
|
194
|
+
? `.${String((error.params as { additionalProperty?: unknown }).additionalProperty ?? '')}`
|
|
195
|
+
: ''
|
|
196
|
+
return `${at}${path}${property}: ${error.message ?? error.keyword}`
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
function appendCanonicalErrors(validate: ValidateFunction, value: unknown, at: string, errors: string[]): void {
|
|
200
|
+
if (!validate(value)) for (const error of validate.errors ?? []) errors.push(canonicalError(error, at))
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
export function validateBookingReadActionV1(value: unknown): BookingSurfaceValidationResult {
|
|
204
|
+
const errors = collectBookingReadActionErrors(value, '$')
|
|
205
|
+
return errors.length === 0 ? { ok: true } : { ok: false, errors }
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
function collectReceiptErrors(value: unknown, at: string): string[] {
|
|
209
|
+
const errors: string[] = []
|
|
210
|
+
if (!isRecord(value)) return [`${at}: typed receipt object required`]
|
|
211
|
+
exactKeys(value, [
|
|
212
|
+
'schemaVersion', 'kind', 'actionId', 'contextRef', 'status', 'revision',
|
|
213
|
+
'observation', 'resultContract', 'undoToken',
|
|
214
|
+
], [
|
|
215
|
+
'schemaVersion', 'kind', 'actionId', 'contextRef', 'status', 'revision',
|
|
216
|
+
'observation', 'resultContract',
|
|
217
|
+
], at, errors)
|
|
218
|
+
if (value.schemaVersion !== 'booking.surface.v1') errors.push(`${at}.schemaVersion: booking.surface.v1 required`)
|
|
219
|
+
if (value.kind !== 'action.receipt') errors.push(`${at}.kind: action.receipt required`)
|
|
220
|
+
if (!isNonEmptyString(value.actionId)) errors.push(`${at}.actionId: opaque ref required`)
|
|
221
|
+
if (!isNonEmptyString(value.contextRef)) errors.push(`${at}.contextRef: opaque ref required`)
|
|
222
|
+
if (typeof value.status !== 'string' || !RECEIPT_STATUSES.has(value.status)) errors.push(`${at}.status: unregistered receipt status`)
|
|
223
|
+
if (!isRevision(value.revision)) errors.push(`${at}.revision: non-negative integer required`)
|
|
224
|
+
if (value.undoToken !== undefined && !isNonEmptyString(value.undoToken)) errors.push(`${at}.undoToken: opaque ref required`)
|
|
225
|
+
validateActionObservation(value.observation, `${at}.observation`, errors)
|
|
226
|
+
validateResultContract(value.resultContract, `${at}.resultContract`, errors)
|
|
227
|
+
return errors
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
function validateActionObservation(value: unknown, at: string, errors: string[]): void {
|
|
231
|
+
if (!isRecord(value)) {
|
|
232
|
+
errors.push(`${at}: discriminated object required`)
|
|
233
|
+
return
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
switch (value.kind) {
|
|
237
|
+
case 'search.state':
|
|
238
|
+
exactKeys(value, ['kind', 'searchSessionRef', 'resultCount'], ['kind'], at, errors)
|
|
239
|
+
if (value.searchSessionRef !== undefined && !isNonEmptyString(value.searchSessionRef)) errors.push(`${at}.searchSessionRef: opaque ref required`)
|
|
240
|
+
if (value.resultCount !== undefined && !isCount(value.resultCount)) errors.push(`${at}.resultCount: non-negative integer required`)
|
|
241
|
+
return
|
|
242
|
+
case 'results.state':
|
|
243
|
+
exactKeys(value, ['kind', 'matchedHotelRefs', 'visibleCount'], ['kind', 'matchedHotelRefs', 'visibleCount'], at, errors)
|
|
244
|
+
if (!isStringArray(value.matchedHotelRefs)) errors.push(`${at}.matchedHotelRefs: opaque-ref array required`)
|
|
245
|
+
if (!isCount(value.visibleCount)) errors.push(`${at}.visibleCount: non-negative integer required`)
|
|
246
|
+
return
|
|
247
|
+
case 'hotel.focus':
|
|
248
|
+
exactKeys(value, ['kind', 'hotelRef'], ['kind', 'hotelRef'], at, errors)
|
|
249
|
+
if (!isNonEmptyString(value.hotelRef)) errors.push(`${at}.hotelRef: opaque ref required`)
|
|
250
|
+
return
|
|
251
|
+
case 'hotel.selection':
|
|
252
|
+
exactKeys(value, ['kind', 'hotelRef'], ['kind', 'hotelRef'], at, errors)
|
|
253
|
+
if (!isNonEmptyString(value.hotelRef)) errors.push(`${at}.hotelRef: opaque ref required`)
|
|
254
|
+
return
|
|
255
|
+
case 'offers.state':
|
|
256
|
+
exactKeys(value, ['kind', 'hotelRefs', 'offerRefs', 'loadedHotelCount'], ['kind', 'hotelRefs', 'offerRefs', 'loadedHotelCount'], at, errors)
|
|
257
|
+
if (!isStringArray(value.hotelRefs)) errors.push(`${at}.hotelRefs: opaque-ref array required`)
|
|
258
|
+
if (!isStringArray(value.offerRefs)) errors.push(`${at}.offerRefs: opaque-ref array required`)
|
|
259
|
+
if (!isCount(value.loadedHotelCount)) errors.push(`${at}.loadedHotelCount: non-negative integer required`)
|
|
260
|
+
return
|
|
261
|
+
case 'offer.selection':
|
|
262
|
+
exactKeys(value, ['kind', 'offerRef'], ['kind', 'offerRef'], at, errors)
|
|
263
|
+
if (!isNonEmptyString(value.offerRef)) errors.push(`${at}.offerRef: opaque ref required`)
|
|
264
|
+
return
|
|
265
|
+
case 'offer.availability':
|
|
266
|
+
exactKeys(value, ['kind', 'offerRef', 'verifiedOfferRef', 'available', 'changedFactRefs'], ['kind', 'offerRef', 'available', 'changedFactRefs'], at, errors)
|
|
267
|
+
if (!isNonEmptyString(value.offerRef)) errors.push(`${at}.offerRef: opaque ref required`)
|
|
268
|
+
if (value.verifiedOfferRef !== undefined && !isNonEmptyString(value.verifiedOfferRef)) errors.push(`${at}.verifiedOfferRef: opaque ref required`)
|
|
269
|
+
if (typeof value.available !== 'boolean') errors.push(`${at}.available: boolean required`)
|
|
270
|
+
if (!isStringArray(value.changedFactRefs)) errors.push(`${at}.changedFactRefs: opaque-ref array required`)
|
|
271
|
+
return
|
|
272
|
+
case 'checkout.handoff':
|
|
273
|
+
exactKeys(value, ['kind', 'offerRef', 'verifiedOfferRef', 'handoffRef'], ['kind', 'offerRef', 'verifiedOfferRef', 'handoffRef'], at, errors)
|
|
274
|
+
if (!isNonEmptyString(value.offerRef)) errors.push(`${at}.offerRef: opaque ref required`)
|
|
275
|
+
if (!isNonEmptyString(value.verifiedOfferRef)) errors.push(`${at}.verifiedOfferRef: opaque ref required`)
|
|
276
|
+
if (!isNonEmptyString(value.handoffRef)) errors.push(`${at}.handoffRef: opaque ref required`)
|
|
277
|
+
return
|
|
278
|
+
case 'order.state': {
|
|
279
|
+
exactKeys(value, ['kind', 'orderRef', 'state'], ['kind', 'orderRef', 'state'], at, errors)
|
|
280
|
+
const states = new Set(['pending', 'verified', 'failed', 'unknown'])
|
|
281
|
+
if (!isNonEmptyString(value.orderRef)) errors.push(`${at}.orderRef: opaque ref required`)
|
|
282
|
+
if (typeof value.state !== 'string' || !states.has(value.state)) errors.push(`${at}.state: unregistered order state`)
|
|
283
|
+
return
|
|
284
|
+
}
|
|
285
|
+
case 'gap':
|
|
286
|
+
exactKeys(value, ['kind', 'code', 'factRefs'], ['kind', 'code', 'factRefs'], at, errors)
|
|
287
|
+
if (!isSafeCode(value.code)) errors.push(`${at}.code: safe identifier required`)
|
|
288
|
+
if (!isStringArray(value.factRefs)) errors.push(`${at}.factRefs: opaque-ref array required`)
|
|
289
|
+
return
|
|
290
|
+
default:
|
|
291
|
+
errors.push(`${at}.kind: unregistered observation kind`)
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
function validateResultContract(value: unknown, at: string, errors: string[]): void {
|
|
296
|
+
if (!isRecord(value)) {
|
|
297
|
+
errors.push(`${at}: object required`)
|
|
298
|
+
return
|
|
299
|
+
}
|
|
300
|
+
exactKeys(value, ['outcome', 'requestedCount', 'actualCount', 'hardCriteriaMet', 'factRefs', 'gapCodes'], ['outcome', 'hardCriteriaMet', 'factRefs', 'gapCodes'], at, errors)
|
|
301
|
+
const outcomes = new Set(['complete', 'partial', 'empty'])
|
|
302
|
+
if (typeof value.outcome !== 'string' || !outcomes.has(value.outcome)) errors.push(`${at}.outcome: complete, partial, or empty required`)
|
|
303
|
+
if (value.requestedCount !== undefined && !isCount(value.requestedCount)) errors.push(`${at}.requestedCount: non-negative integer required`)
|
|
304
|
+
if (value.actualCount !== undefined && !isCount(value.actualCount)) errors.push(`${at}.actualCount: non-negative integer required`)
|
|
305
|
+
if (typeof value.hardCriteriaMet !== 'boolean') errors.push(`${at}.hardCriteriaMet: boolean required`)
|
|
306
|
+
if (!isStringArray(value.factRefs)) errors.push(`${at}.factRefs: opaque-ref array required`)
|
|
307
|
+
if (!Array.isArray(value.gapCodes) || !value.gapCodes.every(isSafeCode)) errors.push(`${at}.gapCodes: safe identifier array required`)
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
export function validateActionReceiptV1(value: unknown): BookingSurfaceValidationResult {
|
|
311
|
+
const errors = collectReceiptErrors(value, '$')
|
|
312
|
+
if (receiptContainsUnsafeOpaqueText(value)) errors.push('$: receipt contains unsafe non-opaque text')
|
|
313
|
+
if (!validateCanonicalReceipt(value)) {
|
|
314
|
+
for (const error of validateCanonicalReceipt.errors ?? []) errors.push(canonicalError(error, '$'))
|
|
315
|
+
}
|
|
316
|
+
return errors.length === 0 ? { ok: true } : { ok: false, errors }
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
export function validateBookingCopilotIngressTurnV1(value: unknown): BookingSurfaceValidationResult {
|
|
320
|
+
const errors: string[] = []
|
|
321
|
+
if (!isRecord(value)) return { ok: false, errors: ['$: object required'] }
|
|
322
|
+
exactKeys(value, [
|
|
323
|
+
'schemaVersion', 'kind', 'taskId', 'contextRef', 'surfaceHint', 'workspace', 'request',
|
|
324
|
+
], ['schemaVersion', 'kind', 'surfaceHint', 'workspace', 'request'], '$', errors)
|
|
325
|
+
if (value.schemaVersion !== 'booking.surface.v1') errors.push('$.schemaVersion: booking.surface.v1 required')
|
|
326
|
+
if (value.kind !== 'user.turn.ingress') errors.push('$.kind: user.turn.ingress required')
|
|
327
|
+
if (value.contextRef !== undefined && value.contextRef !== null) errors.push('$.contextRef: bootstrap may only omit it or send null')
|
|
328
|
+
if (typeof value.surfaceHint !== 'string' || !SURFACES.has(value.surfaceHint)) errors.push('$.surfaceHint: unsupported surface hint')
|
|
329
|
+
validateIngressWorkspace(value.workspace, '$.workspace', errors)
|
|
330
|
+
if (!isRecord(value.request)) {
|
|
331
|
+
errors.push('$.request: object required')
|
|
332
|
+
} else {
|
|
333
|
+
exactKeys(value.request, ['text'], ['text'], '$.request', errors)
|
|
334
|
+
if (!isNonEmptyString(value.request.text)) errors.push('$.request.text: non-empty string required')
|
|
335
|
+
}
|
|
336
|
+
appendCanonicalErrors(validateCanonicalIngressTurn, value, '$', errors)
|
|
337
|
+
return errors.length === 0 ? { ok: true } : { ok: false, errors }
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
export function validateBookingCopilotTurnV1(value: unknown): BookingSurfaceValidationResult {
|
|
341
|
+
const errors: string[] = []
|
|
342
|
+
if (!isRecord(value)) return { ok: false, errors: ['$: object required'] }
|
|
343
|
+
if (value.kind === 'user.turn') {
|
|
344
|
+
exactKeys(value, ['schemaVersion', 'kind', 'taskId', 'workspace', 'request'], ['schemaVersion', 'kind', 'workspace', 'request'], '$', errors)
|
|
345
|
+
if (!isRecord(value.request)) errors.push('$.request: object required')
|
|
346
|
+
else {
|
|
347
|
+
exactKeys(value.request, ['text'], ['text'], '$.request', errors)
|
|
348
|
+
if (!isNonEmptyString(value.request.text)) errors.push('$.request.text: non-empty string required')
|
|
349
|
+
}
|
|
350
|
+
} else if (value.kind === 'action.receipt.continuation') {
|
|
351
|
+
exactKeys(value, ['schemaVersion', 'kind', 'taskId', 'workspace', 'receipt'], ['schemaVersion', 'kind', 'taskId', 'workspace', 'receipt'], '$', errors)
|
|
352
|
+
if (!isNonEmptyString(value.taskId)) errors.push('$.taskId: opaque ref required')
|
|
353
|
+
errors.push(...collectReceiptErrors(value.receipt, '$.receipt'))
|
|
354
|
+
const receiptValidation = validateActionReceiptV1(value.receipt)
|
|
355
|
+
if (!receiptValidation.ok) errors.push(...receiptValidation.errors.map((error) => `$.receipt${error.slice(1)}`))
|
|
356
|
+
} else {
|
|
357
|
+
errors.push('$.kind: user.turn or action.receipt.continuation required')
|
|
358
|
+
}
|
|
359
|
+
if (value.schemaVersion !== 'booking.surface.v1') errors.push('$.schemaVersion: booking.surface.v1 required')
|
|
360
|
+
validateWorkspace(value.workspace, '$.workspace', errors)
|
|
361
|
+
if (value.kind === 'action.receipt.continuation' && isRecord(value.workspace) && isRecord(value.receipt)) {
|
|
362
|
+
if (value.receipt.contextRef !== value.workspace.contextRef) errors.push('$.receipt.contextRef: must equal workspace.contextRef')
|
|
363
|
+
if (value.receipt.revision !== value.workspace.revision) errors.push('$.receipt.revision: must equal workspace.revision')
|
|
364
|
+
}
|
|
365
|
+
appendCanonicalErrors(validateCanonicalTurn, value, '$', errors)
|
|
366
|
+
return errors.length === 0 ? { ok: true } : { ok: false, errors }
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
export function validateBookingSurfaceEventV1(value: unknown): BookingSurfaceValidationResult {
|
|
370
|
+
const errors: string[] = []
|
|
371
|
+
if (!isRecord(value)) return { ok: false, errors: ['$: typed event object required'] }
|
|
372
|
+
const common = ['schemaVersion', 'eventId', 'taskId', 'contextRef', 'sequence', 'emittedAt', 'kind'] as const
|
|
373
|
+
const branchKeys: Record<string, string> = {
|
|
374
|
+
status: 'status',
|
|
375
|
+
question: 'question',
|
|
376
|
+
operation: 'action',
|
|
377
|
+
explanation: 'explanation',
|
|
378
|
+
terminal: 'terminal',
|
|
379
|
+
error: 'error',
|
|
380
|
+
}
|
|
381
|
+
const branchKey = typeof value.kind === 'string' ? branchKeys[value.kind] : undefined
|
|
382
|
+
exactKeys(value, [...common, ...(branchKey ? [branchKey] : [])], [...common, ...(branchKey ? [branchKey] : [])], '$', errors)
|
|
383
|
+
if (value.schemaVersion !== 'booking.surface.v1') errors.push('$.schemaVersion: booking.surface.v1 required')
|
|
384
|
+
if (!isNonEmptyString(value.eventId)) errors.push('$.eventId: opaque ref required')
|
|
385
|
+
if (!isNonEmptyString(value.taskId)) errors.push('$.taskId: opaque ref required')
|
|
386
|
+
if (!isNonEmptyString(value.contextRef)) errors.push('$.contextRef: server-minted opaque ref required')
|
|
387
|
+
if (!Number.isInteger(value.sequence) || Number(value.sequence) < 1) errors.push('$.sequence: positive integer required')
|
|
388
|
+
if (!isNonEmptyString(value.emittedAt)) errors.push('$.emittedAt: timestamp required')
|
|
389
|
+
if (typeof value.kind !== 'string' || !EVENT_KINDS.has(value.kind)) errors.push('$.kind: unregistered event kind')
|
|
390
|
+
if (value.kind === 'status') {
|
|
391
|
+
const statuses = new Set(['submitted', 'working', 'waiting_receipt', 'input_required'])
|
|
392
|
+
if (typeof value.status !== 'string' || !statuses.has(value.status)) errors.push('$.status: unregistered task status')
|
|
393
|
+
}
|
|
394
|
+
if (value.kind === 'question') {
|
|
395
|
+
if (!isRecord(value.question)) {
|
|
396
|
+
errors.push('$.question: typed object required')
|
|
397
|
+
} else {
|
|
398
|
+
exactKeys(value.question, ['questionId', 'prompt', 'missingFields'], ['questionId', 'prompt', 'missingFields'], '$.question', errors)
|
|
399
|
+
if (!isNonEmptyString(value.question.questionId)) errors.push('$.question.questionId: opaque ref required')
|
|
400
|
+
if (!isNonEmptyString(value.question.prompt)) errors.push('$.question.prompt: non-empty string required')
|
|
401
|
+
if (!isStringArray(value.question.missingFields)) errors.push('$.question.missingFields: string array required')
|
|
402
|
+
}
|
|
403
|
+
}
|
|
404
|
+
if (value.kind === 'operation') errors.push(...collectBookingReadActionErrors(value.action, '$.action'))
|
|
405
|
+
if (value.kind === 'explanation') {
|
|
406
|
+
if (!isRecord(value.explanation)) {
|
|
407
|
+
errors.push('$.explanation: typed object required')
|
|
408
|
+
} else {
|
|
409
|
+
exactKeys(value.explanation, ['text', 'factRefs'], ['text', 'factRefs'], '$.explanation', errors)
|
|
410
|
+
if (!isNonEmptyString(value.explanation.text)) errors.push('$.explanation.text: non-empty string required')
|
|
411
|
+
if (!isStringArray(value.explanation.factRefs)) errors.push('$.explanation.factRefs: opaque-ref array required')
|
|
412
|
+
}
|
|
413
|
+
}
|
|
414
|
+
if (value.kind === 'terminal') {
|
|
415
|
+
if (!isRecord(value.terminal)) {
|
|
416
|
+
errors.push('$.terminal: typed object required')
|
|
417
|
+
} else {
|
|
418
|
+
exactKeys(value.terminal, ['status', 'summary', 'factRefs'], ['status', 'summary', 'factRefs'], '$.terminal', errors)
|
|
419
|
+
const statuses = new Set(['completed', 'stopped'])
|
|
420
|
+
if (typeof value.terminal.status !== 'string' || !statuses.has(value.terminal.status)) errors.push('$.terminal.status: completed or stopped required')
|
|
421
|
+
if (!isNonEmptyString(value.terminal.summary)) errors.push('$.terminal.summary: non-empty string required')
|
|
422
|
+
if (!isStringArray(value.terminal.factRefs)) errors.push('$.terminal.factRefs: opaque-ref array required')
|
|
423
|
+
}
|
|
424
|
+
}
|
|
425
|
+
if (value.kind === 'error') {
|
|
426
|
+
if (!isRecord(value.error)) {
|
|
427
|
+
errors.push('$.error: typed object required')
|
|
428
|
+
} else {
|
|
429
|
+
exactKeys(value.error, ['code', 'message', 'retryable'], ['code', 'message', 'retryable'], '$.error', errors)
|
|
430
|
+
if (!isNonEmptyString(value.error.code)) errors.push('$.error.code: non-empty string required')
|
|
431
|
+
if (!isNonEmptyString(value.error.message)) errors.push('$.error.message: non-empty string required')
|
|
432
|
+
if (typeof value.error.retryable !== 'boolean') errors.push('$.error.retryable: boolean required')
|
|
433
|
+
}
|
|
434
|
+
}
|
|
435
|
+
return errors.length === 0 ? { ok: true } : { ok: false, errors }
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
/**
|
|
439
|
+
* The only executable event seam. It performs no text parsing: non-operation
|
|
440
|
+
* events always return null even when their prose happens to look like JSON.
|
|
441
|
+
*/
|
|
442
|
+
export function bookingOperationFromEvent(value: unknown): BookingReadActionV1 | null {
|
|
443
|
+
if (!isRecord(value) || value.kind !== 'operation') return null
|
|
444
|
+
if (!validateBookingSurfaceEventV1(value).ok) return null
|
|
445
|
+
return value.action as BookingReadActionV1
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
// Keep the public predicates assignable to their declared wire types.
|
|
449
|
+
void (null as unknown as BookingCopilotIngressTurnV1)
|
|
450
|
+
void (null as unknown as BookingCopilotTurnV1)
|
|
451
|
+
void (null as unknown as BookingReadActionV1)
|
|
452
|
+
void (null as unknown as ActionReceiptV1)
|
|
453
|
+
void (null as unknown as BookingSurfaceEventV1)
|
package/ts/src/index.ts
CHANGED
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
* - gotry_wish_pool_add 「下一次出发」清单(憧憬不被拒绝)
|
|
8
8
|
*
|
|
9
9
|
* 插件形态遵循 dsh 约定(name/inject/Config/apply + ctx.tools.register(defineTool(...))),
|
|
10
|
-
*
|
|
10
|
+
* 对齐 @deepseek-ai/dsh-tools@0.1.2-alpha.3 的契约:
|
|
11
11
|
* render 位于 output 对象内,参数属性是 ValueSchemaSpec(支持 type:'json')。
|
|
12
12
|
*
|
|
13
13
|
* @module @gotry/plugin
|
|
@@ -35,12 +35,18 @@ import { anythingSearch } from '../capabilities/anything.ts'
|
|
|
35
35
|
import { readUrl, reach, reachStatus } from '../capabilities/agent-reach.ts'
|
|
36
36
|
import { videoSubtitle, githubSearch } from '../capabilities/agent-reach-deep.ts'
|
|
37
37
|
import { sessionLogin } from '../capabilities/session-login.ts'
|
|
38
|
+
import { EXTENSION_STORE_URL } from '../capabilities/session/extension-bridge.ts'
|
|
38
39
|
import { createConsentGate, approvalFromContext } from '../capabilities/session-consent.ts'
|
|
40
|
+
import { installModelOverride } from '../capabilities/model-override.ts'
|
|
39
41
|
import { listArtifacts, readArtifact } from '../capabilities/artifacts.ts'
|
|
40
42
|
import { interpretEffect, declinedObservation } from '../capabilities/effect.ts'
|
|
41
43
|
import { appendFacts, loadFactRegistry } from '../capabilities/fact-log.ts'
|
|
42
44
|
import { factsFromFlyai, factsFromSession } from './bookable-facts.ts'
|
|
43
45
|
import { gateArtifact, type AirlineAirportMap } from './artifact-gate.ts'
|
|
46
|
+
import { installToolBudget } from './tool-budget.ts'
|
|
47
|
+
import { registerBenchmarkEnvironmentBridge, type BenchmarkSubprocessService } from './benchmark-environment-bridge.ts'
|
|
48
|
+
import { installBenchmarkToolIsolation } from './benchmark-tool-isolation.ts'
|
|
49
|
+
import { installBenchmarkAgentConformance } from './benchmark-agent-conformance.ts'
|
|
44
50
|
|
|
45
51
|
/** 航司→机场映射表(issue #46 冲突检测面;data/airline-airports.json,as_of 快照) */
|
|
46
52
|
let airlineAirportMapCache: AirlineAirportMap | null = null
|
|
@@ -58,6 +64,9 @@ async function loadAirlineAirportMap(): Promise<AirlineAirportMap | null> {
|
|
|
58
64
|
}
|
|
59
65
|
|
|
60
66
|
export const name = 'gotry-tools'
|
|
67
|
+
// The bridge obtains subprocess through an optional Cordis lookup at apply
|
|
68
|
+
// time. Declaring it as a required injection would make the whole plugin
|
|
69
|
+
// depend on a service that is only needed for explicit benchmark opt-in.
|
|
61
70
|
export const inject = ['tools', 'systemPrompt']
|
|
62
71
|
|
|
63
72
|
export interface Config {
|
|
@@ -69,6 +78,8 @@ export interface Config {
|
|
|
69
78
|
hbcliBin: string
|
|
70
79
|
/** 账号会话检索总闸(RFC 支柱④「用户明示授权+随时可关」):ask=gotry_session_search 每会话每站点首次调用弹审批卡、会话内记住(默认);allow=用户已在配置明示预授权(直接放行);off=总闸关闭,直接拒绝 */
|
|
71
80
|
sessionAccess: string
|
|
81
|
+
/** Owner-local benchmark environment bridge config; empty disables the bridge. */
|
|
82
|
+
benchmarkEnvironmentConfigPath?: string
|
|
72
83
|
}
|
|
73
84
|
|
|
74
85
|
export const Config: z<Config> = z.object({
|
|
@@ -76,6 +87,7 @@ export const Config: z<Config> = z.object({
|
|
|
76
87
|
timeoutMs: z.number().default(30_000),
|
|
77
88
|
hbcliBin: z.string().default('hbcli'),
|
|
78
89
|
sessionAccess: z.string().default('ask'),
|
|
90
|
+
benchmarkEnvironmentConfigPath: z.string().default(''),
|
|
79
91
|
})
|
|
80
92
|
|
|
81
93
|
interface FeasibilityResult {
|
|
@@ -152,6 +164,29 @@ type Json = string | number | boolean | null | Json[] | { [k: string]: Json }
|
|
|
152
164
|
type JsonObject = { [k: string]: Json }
|
|
153
165
|
|
|
154
166
|
export function apply(ctx: Context, config: Config): void {
|
|
167
|
+
installToolBudget(ctx)
|
|
168
|
+
|
|
169
|
+
const rawBenchmarkEnvironmentConfigPath = config.benchmarkEnvironmentConfigPath ?? ''
|
|
170
|
+
if (rawBenchmarkEnvironmentConfigPath.trim()) {
|
|
171
|
+
// Benchmark mode is a deliberately minimal kernel: only the model
|
|
172
|
+
// override and the environment bridge are installed. In particular,
|
|
173
|
+
// product prompt variables, process/consent guards, and guarded product
|
|
174
|
+
// tools must not be observable on this path.
|
|
175
|
+
installModelOverride(ctx as unknown as Parameters<typeof installModelOverride>[0])
|
|
176
|
+
const getService = (ctx as unknown as { get?: (name: string, fallback?: unknown) => unknown }).get
|
|
177
|
+
const directSubprocess = (typeof getService === 'function'
|
|
178
|
+
? getService.call(ctx, 'subprocess')
|
|
179
|
+
: undefined) as BenchmarkSubprocessService | undefined
|
|
180
|
+
const projection = registerBenchmarkEnvironmentBridge(
|
|
181
|
+
rawBenchmarkEnvironmentConfigPath,
|
|
182
|
+
tool => ctx.tools.register(tool),
|
|
183
|
+
directSubprocess,
|
|
184
|
+
)
|
|
185
|
+
installBenchmarkToolIsolation(ctx)
|
|
186
|
+
installBenchmarkAgentConformance(ctx, projection)
|
|
187
|
+
return
|
|
188
|
+
}
|
|
189
|
+
|
|
155
190
|
// 时间感知:注册动态变量,persona 里用 {{current_date}} 引用。
|
|
156
191
|
// 每次 assemble 时取系统时钟——LLM 始终知道「今天是几号」。
|
|
157
192
|
const sp = (ctx as unknown as Record<string, unknown>)['systemPrompt'] as {
|
|
@@ -193,6 +228,10 @@ export function apply(ctx: Context, config: Config): void {
|
|
|
193
228
|
}))
|
|
194
229
|
}
|
|
195
230
|
|
|
231
|
+
// LLM_MODEL → dsh 会话面模型覆盖(issue #77;机制与分层见
|
|
232
|
+
// capabilities/model-override.ts 头注)。GOTRY_LLM_MODEL 未设时零行为变化。
|
|
233
|
+
installModelOverride(ctx as unknown as Parameters<typeof installModelOverride>[0])
|
|
234
|
+
|
|
196
235
|
// D-NEW 收尾:全部工具 execute 统一异常隔离——单个工具抛错/拒绝不再沿 cordis
|
|
197
236
|
// 传到 dsh 主循环,降级为结构化错误返回给 LLM + incident 落盘(incident-log.ts)。
|
|
198
237
|
const registerGuarded = (tool: ReturnType<typeof defineTool>): void => {
|
|
@@ -786,7 +825,7 @@ export function apply(ctx: Context, config: Config): void {
|
|
|
786
825
|
+ 'OPENS the site login entry in the USER\'S OWN Chrome (foreground tab, left open for the user) and waits for the user to finish logging in on the external site. '
|
|
787
826
|
+ 'GoTry NEVER collects, stores, or transmits credentials: no passwords, no SMS codes, no cookie values — it only checks the boolean fact "already logged in" (reads cookie NAMES only, zero values). '
|
|
788
827
|
+ 'Transport: the GoTry Session Bridge browser extension (one-time install, ZERO Chrome system dialogs). '
|
|
789
|
-
+
|
|
828
|
+
+ `verdict logged-in (tickets detected) | pending (login tab opened, user not done yet — offer to re-check later) | needs-extension (one-time extension install: the verdict surfaces the Chrome Web Store installUrl as a clickable link for dsh UI to render — installation is a browser concern, not gotry's). `
|
|
790
829
|
+ 'Evidence [会话:<site>-login@ts].',
|
|
791
830
|
parameters: {
|
|
792
831
|
query: { type: 'json', required: true, description: '可空对象 {}: { waitSeconds?: number }(等待用户完成登录的上限秒数,默认 90,至多 300)' },
|
|
@@ -800,7 +839,7 @@ export function apply(ctx: Context, config: Config): void {
|
|
|
800
839
|
: r.verdict === 'pending'
|
|
801
840
|
? `登录入口已在你的 Chrome 打开;请在弹出的标签页里正常登录携程(登录由你在官网完成,不属于 gotry)。完成后说一声"继续",我再确认。gotry 只检查"是否已登录",永不收集你的账号信息。${r.evidence}`
|
|
802
841
|
: r.verdict === 'needs-extension'
|
|
803
|
-
? `需要一次性安装 GoTry Session Bridge
|
|
842
|
+
? `需要一次性安装 GoTry Session Bridge 浏览器扩展:这是浏览器的事,gotry 不弹面板不开剪贴板——请直接在 Chrome 应用商店一键装(add-to-chrome 自动更新)。storeUrl=${EXTENSION_STORE_URL}. 装好即生效,装完后告诉我「重试」即可。${r.evidence}`
|
|
804
843
|
: r.verdict === 'needs-attach'
|
|
805
844
|
? `cdp 车道需要一次性开启你 Chrome 的远程调试开关:在你的 Chrome 地址栏打开 chrome://inspect/#remote-debugging 并打开开关,然后说一声"重试"(默认走扩展车道,无需此步)。${r.evidence}`
|
|
806
845
|
: `登录引导未完成:${r.error ?? '未知原因'} ${r.evidence}`
|
|
@@ -808,9 +847,20 @@ export function apply(ctx: Context, config: Config): void {
|
|
|
808
847
|
},
|
|
809
848
|
presentCall: _args => ({ card: 'generic', title: '登录携程(用你自己的浏览器,不在 gotry 输入)', kind: 'fetch', rawInput: {} }),
|
|
810
849
|
presentResult: (_args, value) => {
|
|
811
|
-
const r = value as { verdict?: string; tickets?: string[] }
|
|
812
|
-
const
|
|
813
|
-
|
|
850
|
+
const r = value as { verdict?: string; tickets?: string[]; installUrl?: string }
|
|
851
|
+
const needsExt = r.verdict === 'needs-extension'
|
|
852
|
+
const label = r.verdict === 'logged-in'
|
|
853
|
+
? `已登录(${(r.tickets ?? []).length} 票据)`
|
|
854
|
+
: r.verdict === 'pending'
|
|
855
|
+
? '等待你在携程页面完成登录'
|
|
856
|
+
: needsExt
|
|
857
|
+
? '🧩 需装 GoTry Session Bridge 扩展(浏览器商店一键装,自动更新)'
|
|
858
|
+
: r.verdict ?? '降级'
|
|
859
|
+
const content: Array<{ type: 'text'; text: string }> = [{ type: 'text', text: String((value as { summary?: string }).summary ?? '') }]
|
|
860
|
+
// needs-extension:把商店 URL 渲成可点链接(浏览器自己当安装器,gotry 不插手)
|
|
861
|
+
// dsh presentResult 若支持 actions 字段,URL 可被原生渲染;否则 content 里也保留文本兜底
|
|
862
|
+
if (needsExt && r.installUrl) content.push({ type: 'text', text: `安装链接:${r.installUrl}` })
|
|
863
|
+
return { card: 'generic', title: `账号登录:${label}`, content }
|
|
814
864
|
},
|
|
815
865
|
}))
|
|
816
866
|
|
|
@@ -822,7 +872,7 @@ export function apply(ctx: Context, config: Config): void {
|
|
|
822
872
|
+ 'Transport: GoTry Session Bridge browser extension (one-time install) — the agent side never talks to Chrome debugging, ZERO system dialogs; read-only by construction (the extension never issues requests; it only passively forwards the site\'s own search responses; agent NEVER touches credentials/captcha; on captcha it stops and returns challenged). '
|
|
823
873
|
+ 'Currently ctrip-flight: sniffs the site search API for structured options. Evidence [会话:ctrip-flight@ts]. '
|
|
824
874
|
+ 'verdict needs-login = call gotry_session_login (opens the Ctrip login entry in the user\'s own foreground tab — no terminal, no credentials through GoTry); '
|
|
825
|
-
+
|
|
875
|
+
+ `needs-extension = one-time browser-extension install (Chrome Web Store one-click, installUrl is also surfaced as a clickable link in the verdict field for dsh UI to render) — the DEFAULT transport; cdp (chrome://inspect remote debugging) is a diagnostic fallback only via GOTRY_SESSION_TRANSPORT=cdp. `
|
|
826
876
|
+ 'Rate-limited (≥30s between same-site calls; a challenged/timeout verdict means STOP — never retry, fall back to other tools).',
|
|
827
877
|
parameters: {
|
|
828
878
|
query: { type: 'json', required: true, description: '{ from: "上海", to: "丽江", date: "2026-10-01" }' },
|
|
@@ -856,15 +906,18 @@ export function apply(ctx: Context, config: Config): void {
|
|
|
856
906
|
},
|
|
857
907
|
presentCall: args => ({ card: 'generic', title: `会话检索:${String((args.query as { from?: string })?.from ?? '')}`, kind: 'fetch', rawInput: args.query }),
|
|
858
908
|
presentResult: (_args, value) => {
|
|
859
|
-
const r = value as { verdict?: string; options?: unknown[] }
|
|
909
|
+
const r = value as { verdict?: string; options?: unknown[]; installUrl?: string }
|
|
910
|
+
const needsExt = r.verdict === 'needs-extension'
|
|
860
911
|
const label = r.verdict === 'hit'
|
|
861
912
|
? `会话 ${r.options!.length} 条`
|
|
862
913
|
: r.verdict === 'needs-login'
|
|
863
914
|
? '需登录'
|
|
864
|
-
:
|
|
865
|
-
? '
|
|
915
|
+
: needsExt
|
|
916
|
+
? '🧩 需装 GoTry Session Bridge 扩展(浏览器商店一键装,自动更新)'
|
|
866
917
|
: r.verdict ?? '降级'
|
|
867
|
-
|
|
918
|
+
const content: Array<{ type: 'text'; text: string }> = [{ type: 'text', text: String((value as { summary?: string }).summary ?? '') }]
|
|
919
|
+
if (needsExt && r.installUrl) content.push({ type: 'text', text: `安装链接:${r.installUrl}` })
|
|
920
|
+
return { card: 'generic', title: `会话检索:${label}`, content }
|
|
868
921
|
},
|
|
869
922
|
}))
|
|
870
923
|
|