@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.
Files changed (229) hide show
  1. package/README.md +161 -116
  2. package/README.zh-CN.md +173 -144
  3. package/bin/gotry-booking-copilot.js +53 -0
  4. package/bin/gotry-bootstrap.js +113 -66
  5. package/bin/gotry-inner.js +436 -70
  6. package/bin/gotry-runtime-resolution.d.ts +27 -0
  7. package/bin/gotry-runtime-resolution.js +50 -0
  8. package/bin/gotry.js +1 -1
  9. package/cordis.gotry-patch.yml +5 -1
  10. package/dist/capabilities/agent-reach-deep.js +1 -1
  11. package/dist/capabilities/agent-reach.js +1 -1
  12. package/dist/capabilities/anything.js +1 -1
  13. package/dist/capabilities/artifacts.js +1 -1
  14. package/dist/capabilities/effect.js +1 -1
  15. package/dist/capabilities/fact-log.js +1 -1
  16. package/dist/capabilities/flyai.js +20 -6
  17. package/dist/capabilities/hbcli.js +1 -1
  18. package/dist/capabilities/incident-log.js +1 -1
  19. package/dist/capabilities/model-override.js +18 -0
  20. package/dist/capabilities/opensky.js +1 -1
  21. package/dist/capabilities/resilience.js +1 -1
  22. package/dist/capabilities/session/action-cache.js +1 -1
  23. package/dist/capabilities/session/adapters/ctrip-flight.js +1 -1
  24. package/dist/capabilities/session/adapters/meituan-local.js +1 -1
  25. package/dist/capabilities/session/benchmark.js +1 -1
  26. package/dist/capabilities/session/extension-bridge.js +19 -6
  27. package/dist/capabilities/session/extension-channel.js +3 -3
  28. package/dist/capabilities/session/extension-distribution.js +234 -0
  29. package/dist/capabilities/session/extract.js +1 -1
  30. package/dist/capabilities/session/golden-score.js +92 -0
  31. package/dist/capabilities/session/health-watch.js +1 -1
  32. package/dist/capabilities/session/read-guard.js +1 -1
  33. package/dist/capabilities/session/static-flight-golden.js +137 -0
  34. package/dist/capabilities/session/transport.js +1 -1
  35. package/dist/capabilities/session/wizard.js +21 -251
  36. package/dist/capabilities/session-consent.js +1 -1
  37. package/dist/capabilities/session-login.js +11 -5
  38. package/dist/capabilities/session-search.js +46 -4
  39. package/dist/capabilities/weather.js +168 -46
  40. package/dist/data/session-golden-20.json +25 -0
  41. package/dist/data/sf-golden-manifest.json +102 -0
  42. package/dist/data/sf-static-routes.json +91 -0
  43. package/dist/scripts/action-cache-tests.js +1 -1
  44. package/dist/scripts/agent-planning-budget-e2e.js +227 -0
  45. package/dist/scripts/agent-planning-budget-tests.js +173 -0
  46. package/dist/scripts/agent-reach-deep-tests.js +1 -1
  47. package/dist/scripts/agent-reach-tests.js +1 -1
  48. package/dist/scripts/agent-reach-wrapper-tests.js +1 -1
  49. package/dist/scripts/anything-tests.js +1 -1
  50. package/dist/scripts/async-collect.js +1 -1
  51. package/dist/scripts/benchmark-environment-bridge-e2e.js +981 -0
  52. package/dist/scripts/benchmark-environment-bridge-tests.js +2544 -0
  53. package/dist/scripts/booking-copilot-availability-ledger-binding-tests.js +335 -0
  54. package/dist/scripts/booking-copilot-availability-policy-v2-tests.js +1355 -0
  55. package/dist/scripts/booking-copilot-bin-proof-tests.js +89 -0
  56. package/dist/scripts/booking-copilot-crossrepo-fixture-server.js +113 -0
  57. package/dist/scripts/booking-copilot-dsh-core-proof-tests.js +356 -0
  58. package/dist/scripts/booking-copilot-dsh-planner-proof-tests.js +557 -0
  59. package/dist/scripts/booking-copilot-dsh-plugin-proof-tests.js +140 -0
  60. package/dist/scripts/booking-copilot-event-sequence-concurrency-proof-tests.js +202 -0
  61. package/dist/scripts/booking-copilot-gap-code-contract-proof-tests.js +102 -0
  62. package/dist/scripts/booking-copilot-operation-ledger-concurrency-proof-tests.js +374 -0
  63. package/dist/scripts/booking-copilot-receipt-ledger-concurrency-proof-tests.js +447 -0
  64. package/dist/scripts/booking-copilot-runtime-proof-tests.js +205 -0
  65. package/dist/scripts/booking-copilot-server-proof-tests.js +317 -0
  66. package/dist/scripts/booking-copilot-startup-proof-tests.js +283 -0
  67. package/dist/scripts/booking-copilot-v2-runtime-proof-tests.js +3935 -0
  68. package/dist/scripts/booking-saga-tests.js +1 -1
  69. package/dist/scripts/booking-surface-contract-proof-tests.js +393 -0
  70. package/dist/scripts/booking-surface-v2-contract-proof-tests.js +1174 -0
  71. package/dist/scripts/bootstrap-tests.js +45 -39
  72. package/dist/scripts/build-changelog.js +1 -1
  73. package/dist/scripts/changelog-tests.js +1 -1
  74. package/dist/scripts/companion-tests.js +1 -1
  75. package/dist/scripts/diff-test.js +1 -1
  76. package/dist/scripts/dsh-runtime-closure-tests.js +225 -0
  77. package/dist/scripts/dsh-runtime-closure.js +150 -0
  78. package/dist/scripts/effect-tests.js +1 -1
  79. package/dist/scripts/engine-run.js +1 -1
  80. package/dist/scripts/engine-tests.js +1 -1
  81. package/dist/scripts/evaluation-cadence-tests.js +347 -0
  82. package/dist/scripts/evaluation-contract-tests.js +574 -0
  83. package/dist/scripts/extension-distribution-cli.js +35 -0
  84. package/dist/scripts/extension-distribution-tests.js +341 -0
  85. package/dist/scripts/extension-tests.js +128 -23
  86. package/dist/scripts/fact-gate-tests.js +1 -1
  87. package/dist/scripts/flyai-tests.js +1 -1
  88. package/dist/scripts/hbcli-e2e-tests.js +1 -1
  89. package/dist/scripts/hbcli-tests.js +1 -1
  90. package/dist/scripts/health-watch-cli.js +1 -1
  91. package/dist/scripts/i18n-tests.js +1 -1
  92. package/dist/scripts/incident-tests.js +1 -1
  93. package/dist/scripts/journey-tests.js +1 -1
  94. package/dist/scripts/ledger-tests.js +1 -1
  95. package/dist/scripts/ledger-workflow-crash.js +1 -1
  96. package/dist/scripts/memory-capture-tests.js +1 -1
  97. package/dist/scripts/memory-decay-tests.js +1 -1
  98. package/dist/scripts/memory-metrics.js +1 -1
  99. package/dist/scripts/memory-value-report.js +1 -1
  100. package/dist/scripts/model-override-e2e.js +176 -0
  101. package/dist/scripts/nightly-evidence-tests.js +1 -1
  102. package/dist/scripts/nightly-evidence.js +1 -1
  103. package/dist/scripts/nudge-digest.js +1 -1
  104. package/dist/scripts/onboarding-tests.js +21 -53
  105. package/dist/scripts/opensky-check.js +1 -1
  106. package/dist/scripts/opensky-tests.js +1 -1
  107. package/dist/scripts/pnpm-dsh-closure-proof.js +20 -0
  108. package/dist/scripts/price-drift-tests.js +1 -1
  109. package/dist/scripts/price-drift-watch.js +1 -1
  110. package/dist/scripts/probe-poi-tests.js +1 -1
  111. package/dist/scripts/product-metrics.js +1 -1
  112. package/dist/scripts/publish-preverify.js +40 -4
  113. package/dist/scripts/realtime-pricing-tests.js +1 -1
  114. package/dist/scripts/replay-async.js +1 -1
  115. package/dist/scripts/replay-real.js +1 -1
  116. package/dist/scripts/replay.js +1 -1
  117. package/dist/scripts/session-attach-diagnose.js +1 -1
  118. package/dist/scripts/session-attach-poc.js +1 -1
  119. package/dist/scripts/session-benchmark.js +1 -1
  120. package/dist/scripts/session-extract-tests.js +1 -1
  121. package/dist/scripts/session-login.js +1 -1
  122. package/dist/scripts/session-tests.js +70 -20
  123. package/dist/scripts/sf-live-benchmark.js +338 -0
  124. package/dist/scripts/sf-live-cli-tests.js +21 -0
  125. package/dist/scripts/sf-soft-score-tests.js +108 -0
  126. package/dist/scripts/sf-summary.js +93 -0
  127. package/dist/scripts/skeleton-check.js +1 -1
  128. package/dist/scripts/skeleton-integration-test.js +1 -1
  129. package/dist/scripts/skills-contract-tests.js +1 -1
  130. package/dist/scripts/smoke-session-gate-tests.js +29 -0
  131. package/dist/scripts/smoke.js +79 -33
  132. package/dist/scripts/state-cli-tests.js +1 -1
  133. package/dist/scripts/state-cli.js +1 -1
  134. package/dist/scripts/static-golden-tests.js +299 -0
  135. package/dist/scripts/time-eval-tests.js +1 -1
  136. package/dist/scripts/travel-timeline-tests.js +1 -1
  137. package/dist/scripts/unified-tests.js +1 -1
  138. package/dist/scripts/weather-tests.js +694 -44
  139. package/dist/scripts/z3-race-tests.js +1 -1
  140. package/dist/src/artifact-gate.js +1 -1
  141. package/dist/src/benchmark-agent-conformance.js +370 -0
  142. package/dist/src/benchmark-environment-bridge.js +384 -0
  143. package/dist/src/benchmark-headless-child-diagnostics.js +173 -0
  144. package/dist/src/benchmark-tool-isolation.js +124 -0
  145. package/dist/src/bookable-facts.js +1 -1
  146. package/dist/src/booking-saga.js +1 -1
  147. package/dist/src/booking-surface/availability-policy-v2.js +830 -0
  148. package/dist/src/booking-surface/canonical-schema.js +113 -0
  149. package/dist/src/booking-surface/contracts-v2.js +89 -0
  150. package/dist/src/booking-surface/contracts.js +46 -0
  151. package/dist/src/booking-surface/dsh-planner.js +453 -0
  152. package/dist/src/booking-surface/dsh-plugin.js +93 -0
  153. package/dist/src/booking-surface/error-codes.js +94 -0
  154. package/dist/src/booking-surface/index.js +15 -0
  155. package/dist/src/booking-surface/profile.js +68 -0
  156. package/dist/src/booking-surface/runtime-v2.js +1771 -0
  157. package/dist/src/booking-surface/runtime.js +351 -0
  158. package/dist/src/booking-surface/server-v2.js +334 -0
  159. package/dist/src/booking-surface/server.js +302 -0
  160. package/dist/src/booking-surface/startup.js +159 -0
  161. package/dist/src/booking-surface/validation-v2.js +319 -0
  162. package/dist/src/booking-surface/validation.js +809 -0
  163. package/dist/src/bridge.js +1 -1
  164. package/dist/src/companions.js +1 -1
  165. package/dist/src/contracts.js +1 -1
  166. package/dist/src/dsh-llm.js +1 -1
  167. package/dist/src/engine.js +1 -1
  168. package/dist/src/evaluation-cadence.js +234 -0
  169. package/dist/src/evaluation-contracts.js +906 -0
  170. package/dist/src/i18n.js +1 -1
  171. package/dist/src/index.js +50 -19
  172. package/dist/src/journey.js +1 -1
  173. package/dist/src/loop.js +1 -1
  174. package/dist/src/memory-capture.js +1 -1
  175. package/dist/src/memory-decay.js +1 -1
  176. package/dist/src/memory-utility.js +1 -1
  177. package/dist/src/mock-llm.js +1 -1
  178. package/dist/src/model.js +1 -1
  179. package/dist/src/realtime-pricing.js +1 -1
  180. package/dist/src/slot-spec.js +1 -1
  181. package/dist/src/state-ledger.js +2 -1
  182. package/dist/src/time-anchor.js +1 -1
  183. package/dist/src/tool-budget.js +136 -0
  184. package/dist/src/tool-packet.js +1 -1
  185. package/dist/src/travel-slots.js +1 -1
  186. package/dist/src/travel-timeline.js +1 -1
  187. package/dist/src/unified.js +1 -1
  188. package/dist/src/wish-pool.js +1 -1
  189. package/dist/src/z3-shared.js +1 -1
  190. package/extension/README.md +31 -7
  191. package/package.json +286 -11
  192. package/schemas/booking.surface.v1.schema.json +927 -0
  193. package/schemas/booking.surface.v2.schema.json +61 -0
  194. package/ts/capabilities/flyai.ts +16 -3
  195. package/ts/capabilities/session/extension-bridge.ts +37 -14
  196. package/ts/capabilities/session/extension-channel.ts +6 -3
  197. package/ts/capabilities/session/extension-distribution.ts +264 -0
  198. package/ts/capabilities/session/golden-score.ts +139 -0
  199. package/ts/capabilities/session/health-watch.ts +1 -1
  200. package/ts/capabilities/session/static-flight-golden.ts +209 -0
  201. package/ts/capabilities/session/wizard.ts +34 -176
  202. package/ts/capabilities/session-login.ts +15 -5
  203. package/ts/capabilities/session-search.ts +40 -3
  204. package/ts/capabilities/weather.ts +141 -52
  205. package/ts/package.json +3 -3
  206. package/ts/src/benchmark-agent-conformance.ts +448 -0
  207. package/ts/src/benchmark-environment-bridge.ts +348 -0
  208. package/ts/src/benchmark-headless-child-diagnostics.ts +184 -0
  209. package/ts/src/benchmark-tool-isolation.ts +166 -0
  210. package/ts/src/booking-surface/availability-policy-v2.ts +523 -0
  211. package/ts/src/booking-surface/canonical-schema.js +113 -0
  212. package/ts/src/booking-surface/contracts-v2.ts +118 -0
  213. package/ts/src/booking-surface/contracts.ts +380 -0
  214. package/ts/src/booking-surface/dsh-planner.ts +452 -0
  215. package/ts/src/booking-surface/dsh-plugin.js +93 -0
  216. package/ts/src/booking-surface/error-codes.ts +101 -0
  217. package/ts/src/booking-surface/index.ts +12 -0
  218. package/ts/src/booking-surface/profile.ts +42 -0
  219. package/ts/src/booking-surface/runtime-v2.ts +1466 -0
  220. package/ts/src/booking-surface/runtime.ts +483 -0
  221. package/ts/src/booking-surface/server-v2.ts +247 -0
  222. package/ts/src/booking-surface/server.ts +324 -0
  223. package/ts/src/booking-surface/startup.ts +196 -0
  224. package/ts/src/booking-surface/validation-v2.ts +205 -0
  225. package/ts/src/booking-surface/validation.ts +453 -0
  226. package/ts/src/index.ts +64 -11
  227. package/ts/src/state-ledger.ts +1 -0
  228. package/ts/src/tool-budget.ts +165 -0
  229. 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
- * 对齐已发布 @deepseek-ai/dsh-tools@0.0.1-rc.1 的契约:
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
- + 'verdict logged-in (tickets detected) | pending (login tab opened, user not done yet — offer to re-check later) | needs-extension (one-time extension install: npx gotry setup, then chrome://extensions → load unpacked ~/.gotry/extension). '
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 浏览器扩展(约 30 秒,装完零弹窗):跑 npx gotry setup 拿安装指引,在 chrome://extensions 开发者模式「加载已解压的扩展程序」指向 ~/.gotry/extension,装好后说一声"重试"。${r.evidence}`
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 label = r.verdict === 'logged-in' ? `已登录(${(r.tickets ?? []).length} 票据)` : r.verdict === 'pending' ? '等待你在携程页面完成登录' : r.verdict ?? '降级'
813
- return { card: 'generic', title: `账号登录:${label}`, content: [{ type: 'text', text: String((value as { summary?: string }).summary ?? '') }] }
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
- + 'needs-extension = one-time browser-extension install (npx gotry setup for instructions) — the DEFAULT transport; cdp (chrome://inspect remote debugging) is a diagnostic fallback only via GOTRY_SESSION_TRANSPORT=cdp. '
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
- : r.verdict === 'needs-extension'
865
- ? '需装扩展(一次性)'
915
+ : needsExt
916
+ ? '🧩 需装 GoTry Session Bridge 扩展(浏览器商店一键装,自动更新)'
866
917
  : r.verdict ?? '降级'
867
- return { card: 'generic', title: `会话检索:${label}`, content: [{ type: 'text', text: String((value as { summary?: string }).summary ?? '') }] }
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
 
@@ -681,6 +681,7 @@ export class StateLedger {
681
681
 
682
682
  close(): void {
683
683
  this.db.close()
684
+ for (const [key, value] of openLedgers) if (value === this) openLedgers.delete(key)
684
685
  }
685
686
  }
686
687