ai-capability-framework 1.0.3 → 1.1.0

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 (246) hide show
  1. package/CHANGELOG.md +42 -0
  2. package/README.md +38 -30
  3. package/ROADMAP.md +14 -0
  4. package/dist/cli.d.ts.map +1 -1
  5. package/dist/cli.js +32 -1
  6. package/dist/discovery/bindings.d.ts +7 -0
  7. package/dist/discovery/bindings.d.ts.map +1 -0
  8. package/dist/discovery/bindings.js +24 -0
  9. package/dist/discovery/budget.d.ts +17 -0
  10. package/dist/discovery/budget.d.ts.map +1 -0
  11. package/dist/discovery/budget.js +77 -0
  12. package/dist/discovery/configuration.d.ts +10 -0
  13. package/dist/discovery/configuration.d.ts.map +1 -0
  14. package/dist/discovery/configuration.js +63 -0
  15. package/dist/discovery/corpus.d.ts +8 -0
  16. package/dist/discovery/corpus.d.ts.map +1 -0
  17. package/dist/discovery/corpus.js +64 -0
  18. package/dist/discovery/diagnostics.d.ts +15 -0
  19. package/dist/discovery/diagnostics.d.ts.map +1 -0
  20. package/dist/discovery/diagnostics.js +11 -0
  21. package/dist/discovery/discoverer.d.ts +23 -0
  22. package/dist/discovery/discoverer.d.ts.map +1 -0
  23. package/dist/discovery/discoverer.js +261 -0
  24. package/dist/discovery/embedding-configuration.d.ts +12 -0
  25. package/dist/discovery/embedding-configuration.d.ts.map +1 -0
  26. package/dist/discovery/embedding-configuration.js +81 -0
  27. package/dist/discovery/embedding-types.d.ts +75 -0
  28. package/dist/discovery/embedding-types.d.ts.map +1 -0
  29. package/dist/discovery/embedding-types.js +1 -0
  30. package/dist/discovery/embeddings.d.ts +38 -0
  31. package/dist/discovery/embeddings.d.ts.map +1 -0
  32. package/dist/discovery/embeddings.js +71 -0
  33. package/dist/discovery/evaluation-validation.d.ts +8 -0
  34. package/dist/discovery/evaluation-validation.d.ts.map +1 -0
  35. package/dist/discovery/evaluation-validation.js +19 -0
  36. package/dist/discovery/evaluation.d.ts +84 -0
  37. package/dist/discovery/evaluation.d.ts.map +1 -0
  38. package/dist/discovery/evaluation.js +310 -0
  39. package/dist/discovery/export.d.ts +14 -0
  40. package/dist/discovery/export.d.ts.map +1 -0
  41. package/dist/discovery/export.js +81 -0
  42. package/dist/discovery/facade.d.ts +82 -0
  43. package/dist/discovery/facade.d.ts.map +1 -0
  44. package/dist/discovery/facade.js +266 -0
  45. package/dist/discovery/fingerprint.d.ts +71 -0
  46. package/dist/discovery/fingerprint.d.ts.map +1 -0
  47. package/dist/discovery/fingerprint.js +67 -0
  48. package/dist/discovery/index-builder.d.ts +3 -0
  49. package/dist/discovery/index-builder.d.ts.map +1 -0
  50. package/dist/discovery/index-builder.js +129 -0
  51. package/dist/discovery/index-validation.d.ts +12 -0
  52. package/dist/discovery/index-validation.d.ts.map +1 -0
  53. package/dist/discovery/index-validation.js +117 -0
  54. package/dist/discovery/index.d.ts +32 -0
  55. package/dist/discovery/index.d.ts.map +1 -0
  56. package/dist/discovery/index.js +18 -0
  57. package/dist/discovery/json-snapshot.d.ts +3 -0
  58. package/dist/discovery/json-snapshot.d.ts.map +1 -0
  59. package/dist/discovery/json-snapshot.js +11 -0
  60. package/dist/discovery/lexical.d.ts +12 -0
  61. package/dist/discovery/lexical.d.ts.map +1 -0
  62. package/dist/discovery/lexical.js +26 -0
  63. package/dist/discovery/metadata.d.ts +4 -0
  64. package/dist/discovery/metadata.d.ts.map +1 -0
  65. package/dist/discovery/metadata.js +33 -0
  66. package/dist/discovery/ranking.d.ts +7 -0
  67. package/dist/discovery/ranking.d.ts.map +1 -0
  68. package/dist/discovery/ranking.js +68 -0
  69. package/dist/discovery/replay.d.ts +50 -0
  70. package/dist/discovery/replay.d.ts.map +1 -0
  71. package/dist/discovery/replay.js +175 -0
  72. package/dist/discovery/retrieval-policy.d.ts +5 -0
  73. package/dist/discovery/retrieval-policy.d.ts.map +1 -0
  74. package/dist/discovery/retrieval-policy.js +10 -0
  75. package/dist/discovery/router.d.ts +39 -0
  76. package/dist/discovery/router.d.ts.map +1 -0
  77. package/dist/discovery/router.js +98 -0
  78. package/dist/discovery/selection-attempt.d.ts +20 -0
  79. package/dist/discovery/selection-attempt.d.ts.map +1 -0
  80. package/dist/discovery/selection-attempt.js +39 -0
  81. package/dist/discovery/selection-cards.d.ts +12 -0
  82. package/dist/discovery/selection-cards.d.ts.map +1 -0
  83. package/dist/discovery/selection-cards.js +79 -0
  84. package/dist/discovery/selection-configuration.d.ts +18 -0
  85. package/dist/discovery/selection-configuration.d.ts.map +1 -0
  86. package/dist/discovery/selection-configuration.js +71 -0
  87. package/dist/discovery/selection-response.d.ts +13 -0
  88. package/dist/discovery/selection-response.d.ts.map +1 -0
  89. package/dist/discovery/selection-response.js +47 -0
  90. package/dist/discovery/selection-types.d.ts +18 -0
  91. package/dist/discovery/selection-types.d.ts.map +1 -0
  92. package/dist/discovery/selection-types.js +1 -0
  93. package/dist/discovery/selection.d.ts +6 -0
  94. package/dist/discovery/selection.d.ts.map +1 -0
  95. package/dist/discovery/selection.js +145 -0
  96. package/dist/discovery/types.d.ts +291 -0
  97. package/dist/discovery/types.d.ts.map +1 -0
  98. package/dist/discovery/types.js +1 -0
  99. package/dist/discovery/usage.d.ts +2 -0
  100. package/dist/discovery/usage.d.ts.map +1 -0
  101. package/dist/discovery/usage.js +1 -0
  102. package/dist/discovery/vectors.d.ts +5 -0
  103. package/dist/discovery/vectors.d.ts.map +1 -0
  104. package/dist/discovery/vectors.js +43 -0
  105. package/dist/discovery/versions.d.ts +37 -0
  106. package/dist/discovery/versions.d.ts.map +1 -0
  107. package/dist/discovery/versions.js +36 -0
  108. package/dist/evals-live/live-eval-runner.d.ts +1 -0
  109. package/dist/evals-live/live-eval-runner.d.ts.map +1 -1
  110. package/dist/evals-live/live-eval-runner.js +12 -1
  111. package/dist/evals-live/types.d.ts +3 -0
  112. package/dist/evals-live/types.d.ts.map +1 -1
  113. package/dist/generated/manifest-types.d.ts +3655 -0
  114. package/dist/generated/manifest-types.d.ts.map +1 -1
  115. package/dist/loader.d.ts.map +1 -1
  116. package/dist/loader.js +8 -0
  117. package/dist/mcp-server/server.d.ts.map +1 -1
  118. package/dist/mcp-server/server.js +11 -2
  119. package/dist/mcp-server/types.d.ts +4 -0
  120. package/dist/mcp-server/types.d.ts.map +1 -1
  121. package/dist/observability/redaction.d.ts.map +1 -1
  122. package/dist/observability/redaction.js +6 -2
  123. package/dist/observability/types.d.ts +1 -1
  124. package/dist/observability/types.d.ts.map +1 -1
  125. package/dist/openai/index.d.ts +2 -0
  126. package/dist/openai/index.d.ts.map +1 -1
  127. package/dist/openai/index.js +1 -0
  128. package/dist/openai/responses-runner.d.ts +1 -1
  129. package/dist/openai/responses-runner.d.ts.map +1 -1
  130. package/dist/openai/responses-runner.js +134 -7
  131. package/dist/openai/selection-client.d.ts +28 -0
  132. package/dist/openai/selection-client.d.ts.map +1 -0
  133. package/dist/openai/selection-client.js +95 -0
  134. package/dist/openai/tool-loop.js +1 -1
  135. package/dist/openai/types.d.ts +19 -2
  136. package/dist/openai/types.d.ts.map +1 -1
  137. package/dist/replay/discovery-snapshot.d.ts +10 -0
  138. package/dist/replay/discovery-snapshot.d.ts.map +1 -0
  139. package/dist/replay/discovery-snapshot.js +17 -0
  140. package/dist/replay/recorder.d.ts.map +1 -1
  141. package/dist/replay/recorder.js +4 -0
  142. package/dist/replay/redaction.js +5 -1
  143. package/dist/replay/replayer.d.ts.map +1 -1
  144. package/dist/replay/replayer.js +14 -1
  145. package/dist/replay/types.d.ts +3 -0
  146. package/dist/replay/types.d.ts.map +1 -1
  147. package/dist/replay/validation.d.ts.map +1 -1
  148. package/dist/replay/validation.js +6 -0
  149. package/dist/runtime/bounded-json.d.ts +4 -0
  150. package/dist/runtime/bounded-json.d.ts.map +1 -0
  151. package/dist/runtime/bounded-json.js +74 -0
  152. package/dist/runtime/eligibility.d.ts +4 -0
  153. package/dist/runtime/eligibility.d.ts.map +1 -0
  154. package/dist/runtime/eligibility.js +128 -0
  155. package/dist/runtime/index.d.ts +5 -1
  156. package/dist/runtime/index.d.ts.map +1 -1
  157. package/dist/runtime/index.js +2 -0
  158. package/dist/runtime/provider-attempt.d.ts +11 -0
  159. package/dist/runtime/provider-attempt.d.ts.map +1 -0
  160. package/dist/runtime/provider-attempt.js +221 -0
  161. package/dist/runtime/provider-run.d.ts +10 -0
  162. package/dist/runtime/provider-run.d.ts.map +1 -0
  163. package/dist/runtime/provider-run.js +123 -0
  164. package/dist/runtime/provider-stop.d.ts +9 -0
  165. package/dist/runtime/provider-stop.d.ts.map +1 -0
  166. package/dist/runtime/provider-stop.js +28 -0
  167. package/dist/runtime/provider-types.d.ts +189 -0
  168. package/dist/runtime/provider-types.d.ts.map +1 -0
  169. package/dist/runtime/provider-types.js +1 -0
  170. package/dist/runtime/provider-usage.d.ts +9 -0
  171. package/dist/runtime/provider-usage.d.ts.map +1 -0
  172. package/dist/runtime/provider-usage.js +69 -0
  173. package/dist/runtime/router.d.ts.map +1 -1
  174. package/dist/runtime/router.js +3 -118
  175. package/dist/runtime/types.d.ts +9 -0
  176. package/dist/runtime/types.d.ts.map +1 -1
  177. package/dist/types.d.ts +1 -1
  178. package/dist/types.d.ts.map +1 -1
  179. package/dist/validator.d.ts +3 -0
  180. package/dist/validator.d.ts.map +1 -1
  181. package/dist/validator.js +32 -0
  182. package/docs/api/public-api-policy.md +10 -0
  183. package/docs/api/stable-workflow.md +58 -0
  184. package/docs/api.md +7 -0
  185. package/docs/capability-manifests.md +24 -0
  186. package/docs/development.md +22 -0
  187. package/docs/discovery.md +186 -0
  188. package/docs/evals/discovery-live.md +199 -0
  189. package/docs/evals/discovery-qualification.md +158 -0
  190. package/docs/evals/overview.md +9 -0
  191. package/docs/getting-started/installation.md +11 -12
  192. package/docs/getting-started/openai-quickstart.md +16 -5
  193. package/docs/getting-started/provider-neutral-quickstart.md +247 -88
  194. package/docs/getting-started/quickstart.md +8 -30
  195. package/docs/index.md +10 -0
  196. package/docs/release.md +29 -5
  197. package/docs/runtime.md +27 -0
  198. package/docs/start-here.md +23 -134
  199. package/examples/12-capability-discovery-selection/README.md +346 -0
  200. package/examples/12-capability-discovery-selection/customer-cases.mjs +52 -0
  201. package/examples/12-capability-discovery-selection/customer-harness.mjs +91 -0
  202. package/examples/12-capability-discovery-selection/customer-request-example.mjs +320 -0
  203. package/examples/12-capability-discovery-selection/discovery-example.mjs +175 -0
  204. package/examples/12-capability-discovery-selection/discovery-fixtures/three-domains.suite.json +7887 -0
  205. package/examples/12-capability-discovery-selection/discovery-fixtures/three-domains.v2.suite.json +8167 -0
  206. package/examples/12-capability-discovery-selection/discovery-fixtures/three-domains.v3.suite.json +8342 -0
  207. package/examples/12-capability-discovery-selection/evaluation/application.mjs +305 -0
  208. package/examples/12-capability-discovery-selection/evaluation/campaign.mjs +248 -0
  209. package/examples/12-capability-discovery-selection/evaluation/clients.mjs +433 -0
  210. package/examples/12-capability-discovery-selection/evaluation/continuation.mjs +208 -0
  211. package/examples/12-capability-discovery-selection/evaluation/durable-budget.mjs +420 -0
  212. package/examples/12-capability-discovery-selection/evaluation/freeze.mjs +9 -0
  213. package/examples/12-capability-discovery-selection/evaluation/harness.mjs +618 -0
  214. package/examples/12-capability-discovery-selection/evaluation/mock-host.mjs +260 -0
  215. package/examples/12-capability-discovery-selection/evaluation/preflight.mjs +115 -0
  216. package/examples/12-capability-discovery-selection/evaluation/qualification-workflows.mjs +87 -0
  217. package/examples/12-capability-discovery-selection/evaluation/regrade.mjs +281 -0
  218. package/examples/12-capability-discovery-selection/evaluation/report.mjs +517 -0
  219. package/examples/12-capability-discovery-selection/evaluation/runner.mjs +613 -0
  220. package/examples/12-capability-discovery-selection/evaluation/saved-campaign.mjs +385 -0
  221. package/examples/12-capability-discovery-selection/evaluation/wording.mjs +115 -0
  222. package/examples/12-capability-discovery-selection/evaluation/workflow-application.mjs +378 -0
  223. package/examples/12-capability-discovery-selection/evaluation/workflow-cases.mjs +90 -0
  224. package/examples/12-capability-discovery-selection/evaluation-example.mjs +154 -0
  225. package/examples/12-capability-discovery-selection/hybrid-example.mjs +174 -0
  226. package/examples/12-capability-discovery-selection/mock-embeddings.mjs +101 -0
  227. package/examples/12-capability-discovery-selection/mock-selection.mjs +78 -0
  228. package/examples/12-capability-discovery-selection/pipeline-example.mjs +496 -0
  229. package/examples/12-capability-discovery-selection/retrieval-cases.mjs +31 -0
  230. package/examples/12-capability-discovery-selection/run-campaign-mock.mjs +40 -0
  231. package/examples/12-capability-discovery-selection/run-customer-mock.mjs +15 -0
  232. package/examples/12-capability-discovery-selection/run-evaluation-live.mjs +10 -0
  233. package/examples/12-capability-discovery-selection/run-evaluation-mock.mjs +3 -0
  234. package/examples/12-capability-discovery-selection/run-evaluation-regrade.mjs +71 -0
  235. package/examples/12-capability-discovery-selection/run-hybrid-mock.mjs +3 -0
  236. package/examples/12-capability-discovery-selection/run-mock.mjs +3 -0
  237. package/examples/12-capability-discovery-selection/run-pipeline-mock.mjs +2 -0
  238. package/examples/12-capability-discovery-selection/run-selector-mock.mjs +3 -0
  239. package/examples/12-capability-discovery-selection/selection-example.mjs +88 -0
  240. package/package.json +17 -4
  241. package/schemas/discovery/embedding-index.schema.json +71 -0
  242. package/schemas/discovery/eval-observations.schema.json +409 -0
  243. package/schemas/discovery/eval-suite.schema.json +375 -0
  244. package/schemas/discovery/metadata.schema.json +30 -0
  245. package/schemas/discovery/selection-response.schema.json +47 -0
  246. package/schemas/replay/discovery-snapshot.schema.json +335 -0
@@ -0,0 +1,433 @@
1
+ import { createOpenAISelectionClient } from "ai-capability-framework/openai";
2
+ import { hashDiscoveryContent } from "ai-capability-framework/discovery";
3
+ import { EXECUTION } from "./campaign.mjs";
4
+
5
+ const inputItems = (value) =>
6
+ typeof value === "string"
7
+ ? [{ role: "user", content: value }]
8
+ : structuredClone(value ?? []);
9
+
10
+ function usableOutput(response) {
11
+ if (!Array.isArray(response?.output) || response.output.length > 64) return false;
12
+ let actionable = false;
13
+ for (const item of response.output) {
14
+ if (item?.type === "function_call") {
15
+ if (
16
+ typeof item.name !== "string" ||
17
+ typeof item.call_id !== "string" ||
18
+ typeof item.arguments !== "string" ||
19
+ item.arguments.length > 128_000
20
+ )
21
+ return false;
22
+ actionable = true;
23
+ } else if (item?.type === "message") {
24
+ if (!Array.isArray(item.content) || item.content.length > 64) return false;
25
+ for (const content of item.content) {
26
+ if (
27
+ content?.type !== "output_text" ||
28
+ typeof content.text !== "string" ||
29
+ content.text.length > 128_000
30
+ )
31
+ return false;
32
+ actionable ||= !!content.text.trim();
33
+ }
34
+ } else if (item?.type !== "reasoning") return false;
35
+ }
36
+ return actionable;
37
+ }
38
+
39
+ export function decorateRequest(request, history = []) {
40
+ const decorated = {
41
+ ...structuredClone(request),
42
+ reasoning: { effort: EXECUTION.reasoning },
43
+ service_tier: "default",
44
+ parallel_tool_calls: false,
45
+ truncation: "disabled",
46
+ stream: false,
47
+ store: false
48
+ };
49
+ // Replay complete prior inputs/outputs explicitly; no server-side stored conversation is required.
50
+ if (request.previous_response_id) {
51
+ if (!history.length || history.at(-1).response.id !== request.previous_response_id)
52
+ throw new Error("Missing continuation fixtures.");
53
+ decorated.input = [
54
+ ...history.flatMap((turn) => [
55
+ ...inputItems(turn.request.input),
56
+ ...structuredClone(turn.response.output ?? [])
57
+ ]),
58
+ ...inputItems(request.input)
59
+ ];
60
+ delete decorated.previous_response_id;
61
+ }
62
+ delete decorated.temperature;
63
+ return decorated;
64
+ }
65
+
66
+ /** No credentials or SDK are discovered here. The host supplies both client and embedding tokenizer. */
67
+ export function createEvaluationClients({
68
+ sdk,
69
+ ledger,
70
+ mock,
71
+ tokenizer,
72
+ tokenizerIdentity,
73
+ pricing,
74
+ scope = {}
75
+ }) {
76
+ if (
77
+ typeof mock !== "boolean" ||
78
+ typeof tokenizer !== "function" ||
79
+ typeof tokenizerIdentity !== "string" ||
80
+ !tokenizerIdentity.trim() ||
81
+ typeof sdk?.responses?.create !== "function" ||
82
+ typeof sdk?.responses?.inputTokens?.count !== "function" ||
83
+ typeof sdk?.embeddings?.create !== "function"
84
+ )
85
+ throw new Error("Explicit clients and token preflights are required.");
86
+ if (
87
+ !pricing ||
88
+ Object.keys(pricing).some(
89
+ (key) =>
90
+ ![
91
+ "inputPerMillion",
92
+ "outputPerMillion",
93
+ "embeddingPerMillion",
94
+ "tokenCountPerRequest",
95
+ "premiumMultiplier",
96
+ "checkedAt"
97
+ ].includes(key)
98
+ )
99
+ ) {
100
+ throw new Error(
101
+ "Pricing accepts only explicit numeric rates and its verification timestamp."
102
+ );
103
+ }
104
+ for (const key of [
105
+ "inputPerMillion",
106
+ "outputPerMillion",
107
+ "embeddingPerMillion",
108
+ "tokenCountPerRequest",
109
+ "premiumMultiplier"
110
+ ]) {
111
+ if (
112
+ typeof pricing?.[key] !== "number" ||
113
+ !Number.isFinite(pricing[key]) ||
114
+ pricing[key] < 0
115
+ )
116
+ throw new Error("Explicit finite pricing is required.");
117
+ }
118
+ if (
119
+ pricing.premiumMultiplier < 1 ||
120
+ (!mock &&
121
+ (!pricing.checkedAt ||
122
+ Date.now() - Date.parse(pricing.checkedAt) > 24 * 60 * 60_000 ||
123
+ !Number.isFinite(Date.parse(pricing.checkedAt)) ||
124
+ Date.parse(pricing.checkedAt) > Date.now() + 300_000))
125
+ )
126
+ throw new Error("Live pricing must have been checked in the last 24 hours.");
127
+ // Campaign-specific conservative floors, verified against official Standard pricing on 2026-10-16.
128
+ // They are not framework pricing defaults. Hosts must raise them for higher current rates/premiums.
129
+ if (
130
+ !mock &&
131
+ (pricing.inputPerMillion < 0.125 ||
132
+ pricing.outputPerMillion < 0.5 ||
133
+ pricing.embeddingPerMillion < 0.02)
134
+ ) {
135
+ throw new Error(
136
+ "Live pricing is below the campaign's verified conservative floor."
137
+ );
138
+ }
139
+ const pricingRates = Object.fromEntries(
140
+ Object.entries(pricing).filter(([key]) => key !== "checkedAt")
141
+ );
142
+ const identity = hashDiscoveryContent({
143
+ execution: EXECUTION,
144
+ tokenizerIdentity,
145
+ pricing: pricingRates,
146
+ mock
147
+ });
148
+ const configuration = Object.freeze({
149
+ provider: "openai",
150
+ model: EXECUTION.model,
151
+ configurationId: `evaluation.responses.${identity.slice(7, 23)}`
152
+ });
153
+ const embeddingConfiguration = Object.freeze({
154
+ provider: "openai",
155
+ model: EXECUTION.embeddingModel,
156
+ configurationId: `evaluation.embedding.${identity.slice(7, 23)}`,
157
+ dimensions: EXECUTION.dimensions
158
+ });
159
+ const prepared = new WeakMap();
160
+ const check = () => {
161
+ if (
162
+ scope.signal?.aborted ||
163
+ Date.now() >= (scope.deadlineAt ?? Infinity) ||
164
+ scope.guard?.()
165
+ )
166
+ throw Object.assign(new Error("Evaluation binding is stale or cancelled."), {
167
+ providerAttempts: 0
168
+ });
169
+ const control = scope.controls?.evaluate({
170
+ operation: "provider_call",
171
+ providerId: "openai",
172
+ model: EXECUTION.model,
173
+ registry: scope.registry,
174
+ runtimeContext: scope.runtimeContext,
175
+ usage: {
176
+ providerCalls: ledger.snapshot().httpRequests,
177
+ retries: 0,
178
+ runtimeMs: 0,
179
+ toolCalls: 0
180
+ }
181
+ });
182
+ if (control?.status === "denied")
183
+ throw Object.assign(new Error("Evaluation provider control denied dispatch."), {
184
+ providerAttempts: 0
185
+ });
186
+ };
187
+ const estimateCost = (usage, config) => {
188
+ if (
189
+ usage.inputTokens === undefined ||
190
+ (config.model !== EXECUTION.embeddingModel && usage.outputTokens === undefined)
191
+ )
192
+ return undefined;
193
+ const inputRate =
194
+ config.model === EXECUTION.embeddingModel
195
+ ? pricing.embeddingPerMillion
196
+ : pricing.inputPerMillion;
197
+ return (
198
+ ((usage.inputTokens * inputRate +
199
+ (config.model === EXECUTION.embeddingModel
200
+ ? 0
201
+ : usage.outputTokens * pricing.outputPerMillion)) /
202
+ 1_000_000) *
203
+ pricing.premiumMultiplier
204
+ );
205
+ };
206
+ const maxCost = (input = 8000, output = 1000, embedding = false) =>
207
+ estimateCost(
208
+ { inputTokens: input, outputTokens: output },
209
+ embedding ? embeddingConfiguration : configuration
210
+ );
211
+ const inputTokenPreflight = async ({
212
+ request,
213
+ history,
214
+ maxInputTokens,
215
+ signal,
216
+ deadlineAt
217
+ }) => {
218
+ check();
219
+ const value = decorateRequest(request, history);
220
+ const allowed = [
221
+ "model",
222
+ "input",
223
+ "instructions",
224
+ "parallel_tool_calls",
225
+ "reasoning",
226
+ "text",
227
+ "tool_choice",
228
+ "tools",
229
+ "truncation"
230
+ ];
231
+ const payload = Object.fromEntries(
232
+ allowed.filter((key) => value[key] !== undefined).map((key) => [key, value[key]])
233
+ );
234
+ const response = await ledger
235
+ .transport(
236
+ "token_count",
237
+ () =>
238
+ sdk.responses.inputTokens.count(payload, {
239
+ signal,
240
+ timeout: Math.max(1, deadlineAt - Date.now()),
241
+ maxRetries: 0
242
+ }),
243
+ { signal, deadlineAt, fee: pricing.tokenCountPerRequest }
244
+ )
245
+ .catch(() => {
246
+ throw Object.assign(new Error("Input token preflight unavailable."), {
247
+ providerAttempts: 0,
248
+ completedResponses: 0,
249
+ usage: {}
250
+ });
251
+ });
252
+ if (
253
+ !Number.isSafeInteger(response?.input_tokens) ||
254
+ response.input_tokens < 0 ||
255
+ response.input_tokens > maxInputTokens
256
+ )
257
+ throw Object.assign(new Error("Complete input exceeds its token ceiling."), {
258
+ providerAttempts: 0
259
+ });
260
+ check();
261
+ prepared.set(request, value);
262
+ return response.input_tokens;
263
+ };
264
+ const client = {
265
+ responses: {
266
+ async create(request, options = {}) {
267
+ const value = prepared.get(request);
268
+ if (!value)
269
+ throw Object.assign(
270
+ new Error("A complete input preflight must precede generation."),
271
+ { providerAttempts: 0 }
272
+ );
273
+ prepared.delete(request);
274
+ check();
275
+ const response = await ledger.transport(
276
+ "generation",
277
+ () =>
278
+ sdk.responses.create(value, {
279
+ ...options,
280
+ maxRetries: 0,
281
+ timeout: Math.max(
282
+ 1,
283
+ Math.min(
284
+ options.timeout ?? 30_000,
285
+ (scope.deadlineAt ?? Date.now() + 30_000) - Date.now()
286
+ )
287
+ )
288
+ }),
289
+ {
290
+ signal: options.signal,
291
+ deadlineAt: Math.min(
292
+ scope.deadlineAt ?? Infinity,
293
+ Date.now() + (options.timeout ?? 30_000)
294
+ )
295
+ }
296
+ );
297
+ if (
298
+ response?.status !== "completed" ||
299
+ typeof response.model !== "string" ||
300
+ (response.model !== EXECUTION.model &&
301
+ !response.model.startsWith(EXECUTION.model + "-")) ||
302
+ !usableOutput(response) ||
303
+ response.output.some((item) =>
304
+ item?.content?.some?.((content) => content?.type === "refusal")
305
+ )
306
+ ) {
307
+ throw Object.assign(
308
+ new Error("Evaluation generation response is incompatible or refused."),
309
+ {
310
+ usage: Object.fromEntries(
311
+ Object.entries({
312
+ inputTokens: response?.usage?.input_tokens,
313
+ outputTokens: response?.usage?.output_tokens,
314
+ totalTokens: response?.usage?.total_tokens,
315
+ cachedInputTokens:
316
+ response?.usage?.input_tokens_details?.cached_tokens,
317
+ reasoningTokens:
318
+ response?.usage?.output_tokens_details?.reasoning_tokens
319
+ }).filter(([, number]) => Number.isSafeInteger(number) && number >= 0)
320
+ ),
321
+ providerAttempts: 1,
322
+ completedResponses: 1
323
+ }
324
+ );
325
+ }
326
+ return response;
327
+ }
328
+ }
329
+ };
330
+ const embeddingClient = Object.freeze({
331
+ mock,
332
+ configuration: embeddingConfiguration,
333
+ async embed(input) {
334
+ check();
335
+ const counts = input.texts.map((text) => tokenizer(text));
336
+ if (
337
+ counts.some((count) => !Number.isSafeInteger(count) || count < 1) ||
338
+ counts.reduce((a, b) => a + b, 0) > input.maxInputTokens
339
+ )
340
+ throw Object.assign(new Error("Embedding input exceeds its token ceiling."), {
341
+ providerAttempts: 0
342
+ });
343
+ input.beforeProviderCall?.();
344
+ const response = await ledger.transport(
345
+ "embedding",
346
+ () =>
347
+ sdk.embeddings.create(
348
+ {
349
+ model: embeddingConfiguration.model,
350
+ input: input.texts,
351
+ dimensions: EXECUTION.dimensions,
352
+ encoding_format: "float"
353
+ },
354
+ {
355
+ signal: input.signal,
356
+ timeout: Math.max(1, input.deadlineAt - Date.now()),
357
+ maxRetries: 0
358
+ }
359
+ ),
360
+ { signal: input.signal, deadlineAt: input.deadlineAt }
361
+ );
362
+ const usage = {};
363
+ if (
364
+ Number.isSafeInteger(response?.usage?.prompt_tokens) &&
365
+ response.usage.prompt_tokens >= 0
366
+ )
367
+ usage.inputTokens = response.usage.prompt_tokens;
368
+ if (
369
+ Number.isSafeInteger(response?.usage?.total_tokens) &&
370
+ response.usage.total_tokens >= 0
371
+ )
372
+ usage.totalTokens = response.usage.total_tokens;
373
+ // These text-generation categories are inapplicable to this embedding operation.
374
+ Object.assign(usage, {
375
+ outputTokens: 0,
376
+ cachedInputTokens: 0,
377
+ reasoningTokens: 0
378
+ });
379
+ const cost = estimateCost(usage, embeddingConfiguration);
380
+ if (cost !== undefined) usage.estimatedCost = cost;
381
+ if (
382
+ response?.model !== EXECUTION.embeddingModel ||
383
+ !Array.isArray(response.data) ||
384
+ response.data.length !== input.texts.length ||
385
+ response.data.some(
386
+ (entry, index) => entry.index !== index || !Array.isArray(entry.embedding)
387
+ )
388
+ ) {
389
+ throw Object.assign(new Error("Embedding response is incompatible."), {
390
+ usage,
391
+ providerAttempts: 1,
392
+ completedResponses: 1
393
+ });
394
+ }
395
+ return {
396
+ vectors: response.data.map((entry) => entry.embedding),
397
+ configuration: embeddingConfiguration,
398
+ vectorDimension: EXECUTION.dimensions,
399
+ providerAttempts: 1,
400
+ usage
401
+ };
402
+ }
403
+ });
404
+ return Object.freeze({
405
+ mock,
406
+ configuration,
407
+ embeddingConfiguration,
408
+ client,
409
+ embeddingClient,
410
+ inputTokenPreflight,
411
+ selectionClient: createOpenAISelectionClient({
412
+ client,
413
+ configuration,
414
+ mock,
415
+ inputTokenPreflight
416
+ }),
417
+ estimateCost,
418
+ maxCost,
419
+ tokenizerIdentity,
420
+ identity,
421
+ forScope(next) {
422
+ return createEvaluationClients({
423
+ sdk,
424
+ ledger,
425
+ mock,
426
+ tokenizer,
427
+ tokenizerIdentity,
428
+ pricing,
429
+ scope: next
430
+ });
431
+ }
432
+ });
433
+ }
@@ -0,0 +1,208 @@
1
+ import { createHash } from "node:crypto";
2
+ import {
3
+ closeSync,
4
+ existsSync,
5
+ fsyncSync,
6
+ openSync,
7
+ readFileSync,
8
+ statSync,
9
+ writeFileSync
10
+ } from "node:fs";
11
+ import path from "node:path";
12
+ import { hashDiscoveryContent } from "ai-capability-framework/discovery";
13
+ import { CAPS } from "./campaign.mjs";
14
+ import { openCampaignLedger } from "./durable-budget.mjs";
15
+
16
+ /** Read-only reconciliation; the caller pins both identity and exact journal bytes. */
17
+ export function verifyPredecessor(predecessor) {
18
+ if (
19
+ !predecessor ||
20
+ Object.keys(predecessor).sort().join() !== "directory,fingerprint,journalHash"
21
+ )
22
+ throw new Error("A pinned predecessor is required.");
23
+ const directory = path.resolve(predecessor.directory),
24
+ filename = path.join(directory, "journal.jsonl");
25
+ if (existsSync(path.join(directory, "campaign.lock")))
26
+ throw new Error("Predecessor is still owned.");
27
+ if (statSync(filename).size > 64 * 1024 * 1024)
28
+ throw new Error("Predecessor journal exceeds bound.");
29
+ const bytes = readFileSync(filename);
30
+ const journalHash = `sha256:${createHash("sha256").update(bytes).digest("hex")}`;
31
+ if (journalHash !== predecessor.journalHash || !bytes.toString().endsWith("\n"))
32
+ throw new Error("Predecessor journal hash mismatch.");
33
+ let sequence = 0,
34
+ previous = null,
35
+ head,
36
+ halted = false;
37
+ const reservations = new Map(),
38
+ pendingHttp = new Set(),
39
+ pendingTrials = new Set(),
40
+ settled = new Set();
41
+ const consumed = Object.fromEntries(Object.keys(CAPS).map((key) => [key, 0]));
42
+ for (const line of bytes.toString().trimEnd().split("\n")) {
43
+ if (line.length > 128_000) throw new Error("Oversized predecessor record.");
44
+ const { hash, ...record } = JSON.parse(line),
45
+ d = record.data;
46
+ if (
47
+ record.sequence !== ++sequence ||
48
+ record.previous !== previous ||
49
+ hashDiscoveryContent(record) !== hash
50
+ )
51
+ throw new Error("Predecessor integrity failure.");
52
+ previous = hash;
53
+ if (sequence === 1) {
54
+ if (
55
+ record.type !== "campaign" ||
56
+ d.fingerprint !== predecessor.fingerprint ||
57
+ hashDiscoveryContent({ binding: d.binding, caps: d.caps }) !== d.fingerprint ||
58
+ hashDiscoveryContent(d.caps) !== hashDiscoveryContent(CAPS)
59
+ )
60
+ throw new Error("Predecessor campaign mismatch.");
61
+ head = d;
62
+ }
63
+ if (record.type === "reservation") {
64
+ if (reservations.has(d.id) || settled.has(d.id))
65
+ throw new Error("Duplicate predecessor reservation.");
66
+ reservations.set(d.id, d);
67
+ }
68
+ if (record.type === "settlement") {
69
+ const reservation = reservations.get(d.id);
70
+ if (!reservation || settled.has(d.id))
71
+ throw new Error("Duplicate predecessor settlement.");
72
+ const attempts = Math.max(d.providerAttempts, d.completedResponses);
73
+ if (!Number.isSafeInteger(attempts) || attempts < 0 || attempts > 1)
74
+ throw new Error("Invalid predecessor attempts.");
75
+ for (const key of [
76
+ "providerCalls",
77
+ "inputTokens",
78
+ "outputTokens",
79
+ "estimatedCost"
80
+ ]) {
81
+ const expected =
82
+ key === "providerCalls"
83
+ ? attempts
84
+ : (d.usage[key] ?? (attempts ? reservation.amounts[key] : 0));
85
+ if (!Number.isFinite(expected) || expected < 0 || d.charged[key] !== expected)
86
+ throw new Error("Predecessor charge mismatch.");
87
+ consumed[key] += expected;
88
+ }
89
+ reservations.delete(d.id);
90
+ settled.add(d.id);
91
+ }
92
+ if (record.type === "http.start") {
93
+ if (pendingHttp.has(d.id) || !Number.isFinite(d.fee) || d.fee < 0)
94
+ throw new Error("Invalid predecessor HTTP charge.");
95
+ consumed.httpRequests++;
96
+ consumed.estimatedCost += d.fee;
97
+ pendingHttp.add(d.id);
98
+ }
99
+ if (record.type === "http.end") {
100
+ if (!pendingHttp.delete(d.id))
101
+ throw new Error("Invalid predecessor HTTP completion.");
102
+ }
103
+ if (record.type === "trial.start") pendingTrials.add(d.key);
104
+ if (record.type === "trial.end") {
105
+ if (!pendingTrials.delete(d.key))
106
+ throw new Error("Invalid predecessor trial completion.");
107
+ }
108
+ if (record.type === "halt") halted = true;
109
+ }
110
+ if (!head || halted || reservations.size || pendingHttp.size || pendingTrials.size)
111
+ throw new Error("Predecessor requires reconciliation.");
112
+ const remaining = Object.fromEntries(
113
+ Object.keys(CAPS).map((key) => [
114
+ key,
115
+ key === "estimatedCost"
116
+ ? Math.floor((CAPS[key] - consumed[key]) * 1e12) / 1e12
117
+ : CAPS[key] - consumed[key]
118
+ ])
119
+ );
120
+ if (Object.values(remaining).some((value) => value < 0))
121
+ throw new Error("Predecessor exhausted campaign caps.");
122
+ return {
123
+ fingerprint: predecessor.fingerprint,
124
+ journalHash,
125
+ lastRecordHash: previous,
126
+ directory,
127
+ consumed,
128
+ remaining
129
+ };
130
+ }
131
+
132
+ export function openContinuationLedger({ predecessor, directory, binding, caps }) {
133
+ const prior = verifyPredecessor(predecessor),
134
+ resolvedCaps = { ...prior.remaining, ...caps };
135
+ if (
136
+ Object.entries(resolvedCaps).some(
137
+ ([key, value]) =>
138
+ !(key in CAPS) ||
139
+ !Number.isFinite(value) ||
140
+ value < 0 ||
141
+ value > prior.remaining[key]
142
+ )
143
+ )
144
+ throw new Error("Continuation cannot increase remaining caps.");
145
+ const continuationBinding = {
146
+ ...binding,
147
+ predecessor: {
148
+ fingerprint: prior.fingerprint,
149
+ journalHash: prior.journalHash,
150
+ lastRecordHash: prior.lastRecordHash
151
+ }
152
+ };
153
+ const fingerprint = hashDiscoveryContent({
154
+ binding: continuationBinding,
155
+ caps: resolvedCaps
156
+ });
157
+ const claim = {
158
+ fingerprint,
159
+ directory: path.resolve(directory),
160
+ predecessorJournalHash: prior.journalHash
161
+ };
162
+ const claimPath = path.join(prior.directory, "continuation.json");
163
+ // Permanent exclusive ownership: a crash may resume this identity, never start a sibling.
164
+ try {
165
+ const fd = openSync(claimPath, "wx");
166
+ try {
167
+ writeFileSync(fd, JSON.stringify(claim));
168
+ fsyncSync(fd);
169
+ } finally {
170
+ closeSync(fd);
171
+ }
172
+ } catch (error) {
173
+ if (error.code !== "EEXIST") throw error;
174
+ if (
175
+ statSync(claimPath).size > 4096 ||
176
+ hashDiscoveryContent(JSON.parse(readFileSync(claimPath, "utf8"))) !==
177
+ hashDiscoveryContent(claim)
178
+ )
179
+ throw new Error("Predecessor already has a different continuation.");
180
+ }
181
+ if (existsSync(path.join(prior.directory, "campaign.lock")))
182
+ throw new Error("Predecessor became active; reconcile ownership before resuming.");
183
+ const ledger = openCampaignLedger({
184
+ directory,
185
+ binding: continuationBinding,
186
+ caps: resolvedCaps
187
+ });
188
+ return Object.freeze({
189
+ ...ledger,
190
+ snapshot() {
191
+ const current = ledger.snapshot();
192
+ const cumulative = Object.fromEntries(
193
+ Object.keys(CAPS).map((key) => [
194
+ key,
195
+ prior.consumed[key] +
196
+ (key === "httpRequests" ? current.httpRequests : current.charged[key])
197
+ ])
198
+ );
199
+ return structuredClone({
200
+ ...current,
201
+ caps: resolvedCaps,
202
+ prior: prior.consumed,
203
+ cumulative,
204
+ predecessor: { fingerprint: prior.fingerprint, journalHash: prior.journalHash }
205
+ });
206
+ }
207
+ });
208
+ }