@hourslabs/domovoi 0.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 (74) hide show
  1. package/LICENSE +201 -0
  2. package/NOTICE +4 -0
  3. package/README.md +312 -0
  4. package/dist/cache.d.ts +102 -0
  5. package/dist/cache.d.ts.map +1 -0
  6. package/dist/calibration/index.d.ts +45 -0
  7. package/dist/calibration/index.d.ts.map +1 -0
  8. package/dist/calibration/index.js +95 -0
  9. package/dist/calibration/index.js.map +1 -0
  10. package/dist/engine/abort.d.ts +43 -0
  11. package/dist/engine/abort.d.ts.map +1 -0
  12. package/dist/engine/config.d.ts +79 -0
  13. package/dist/engine/config.d.ts.map +1 -0
  14. package/dist/engine/decide.d.ts +18 -0
  15. package/dist/engine/decide.d.ts.map +1 -0
  16. package/dist/engine/distribution.d.ts +18 -0
  17. package/dist/engine/distribution.d.ts.map +1 -0
  18. package/dist/engine/error-recording.d.ts +35 -0
  19. package/dist/engine/error-recording.d.ts.map +1 -0
  20. package/dist/engine/finalize.d.ts +12 -0
  21. package/dist/engine/finalize.d.ts.map +1 -0
  22. package/dist/engine/hooks.d.ts +12 -0
  23. package/dist/engine/hooks.d.ts.map +1 -0
  24. package/dist/engine/index.d.ts +7 -0
  25. package/dist/engine/index.d.ts.map +1 -0
  26. package/dist/engine/meta.d.ts +37 -0
  27. package/dist/engine/meta.d.ts.map +1 -0
  28. package/dist/engine/threshold.d.ts +31 -0
  29. package/dist/engine/threshold.d.ts.map +1 -0
  30. package/dist/env.d.ts +46 -0
  31. package/dist/env.d.ts.map +1 -0
  32. package/dist/errors.d.ts +66 -0
  33. package/dist/errors.d.ts.map +1 -0
  34. package/dist/hash.d.ts +27 -0
  35. package/dist/hash.d.ts.map +1 -0
  36. package/dist/index.d.ts +34 -0
  37. package/dist/index.d.ts.map +1 -0
  38. package/dist/index.js +1263 -0
  39. package/dist/index.js.map +1 -0
  40. package/dist/prompt.d.ts +14 -0
  41. package/dist/prompt.d.ts.map +1 -0
  42. package/dist/providers/index.d.ts +6 -0
  43. package/dist/providers/index.d.ts.map +1 -0
  44. package/dist/providers/index.js +301 -0
  45. package/dist/providers/index.js.map +1 -0
  46. package/dist/providers/openai/adapter.d.ts +25 -0
  47. package/dist/providers/openai/adapter.d.ts.map +1 -0
  48. package/dist/providers/openai/distribution.d.ts +26 -0
  49. package/dist/providers/openai/distribution.d.ts.map +1 -0
  50. package/dist/providers/openai/factory.d.ts +76 -0
  51. package/dist/providers/openai/factory.d.ts.map +1 -0
  52. package/dist/providers/openai/index.d.ts +6 -0
  53. package/dist/providers/openai/index.d.ts.map +1 -0
  54. package/dist/providers/provider.d.ts +50 -0
  55. package/dist/providers/provider.d.ts.map +1 -0
  56. package/dist/testing/index.d.ts +40 -0
  57. package/dist/testing/index.d.ts.map +1 -0
  58. package/dist/testing/index.js +34 -0
  59. package/dist/testing/index.js.map +1 -0
  60. package/dist/tokenizer.d.ts +56 -0
  61. package/dist/tokenizer.d.ts.map +1 -0
  62. package/dist/types.d.ts +180 -0
  63. package/dist/types.d.ts.map +1 -0
  64. package/dist/validate.d.ts +47 -0
  65. package/dist/validate.d.ts.map +1 -0
  66. package/dist/verbs/boolean.d.ts +26 -0
  67. package/dist/verbs/boolean.d.ts.map +1 -0
  68. package/dist/verbs/classifier.d.ts +63 -0
  69. package/dist/verbs/classifier.d.ts.map +1 -0
  70. package/dist/verbs/classify.d.ts +24 -0
  71. package/dist/verbs/classify.d.ts.map +1 -0
  72. package/dist/verdict.d.ts +38 -0
  73. package/dist/verdict.d.ts.map +1 -0
  74. package/package.json +108 -0
package/dist/index.js ADDED
@@ -0,0 +1,1263 @@
1
+ import { createHash } from 'crypto';
2
+ import OpenAI from 'openai';
3
+ import { get_encoding } from 'tiktoken';
4
+
5
+ // src/hash.ts
6
+ function sha256(s) {
7
+ return createHash("sha256").update(s).digest("hex");
8
+ }
9
+ function canonicalJSON(value) {
10
+ return JSON.stringify(canonicalize(value));
11
+ }
12
+ function canonicalize(value) {
13
+ if (value === null) return null;
14
+ if (Array.isArray(value)) return value.map(canonicalize);
15
+ if (typeof value !== "object") return value;
16
+ const record = value;
17
+ return Object.fromEntries(
18
+ Object.keys(record).sort().filter((key) => record[key] !== void 0).map((key) => [key, canonicalize(record[key])])
19
+ );
20
+ }
21
+ function normalizeInput(input) {
22
+ return input.normalize("NFC").trim();
23
+ }
24
+
25
+ // src/cache.ts
26
+ var CACHE_SCHEMA_VERSION = 1;
27
+ function computeCacheKey(inputs) {
28
+ return sha256(
29
+ canonicalJSON({
30
+ cache_schema_version: CACHE_SCHEMA_VERSION,
31
+ provider_id: inputs.providerId,
32
+ model_id: inputs.modelId,
33
+ tokenizer_id: inputs.tokenizerId,
34
+ template_hash: inputs.templateHash,
35
+ decision_space: inputs.decisionSpace,
36
+ temperature: inputs.temperature,
37
+ provider_config_hash: inputs.providerConfigHash,
38
+ input_hash: sha256(normalizeInput(inputs.formattedInput))
39
+ })
40
+ );
41
+ }
42
+ function serializeCachedValue(d) {
43
+ const value = {
44
+ schemaVersion: CACHE_SCHEMA_VERSION,
45
+ distribution: d,
46
+ storedAt: Date.now()
47
+ };
48
+ return JSON.stringify(value);
49
+ }
50
+ function deserializeCachedValue(raw) {
51
+ try {
52
+ const parsed = JSON.parse(raw);
53
+ if (parsed.schemaVersion !== CACHE_SCHEMA_VERSION) return void 0;
54
+ return parsed.distribution;
55
+ } catch {
56
+ return void 0;
57
+ }
58
+ }
59
+ function memoryCache(options) {
60
+ const maxEntries = options?.maxEntries ?? 1e4;
61
+ const defaultTtlMs = options?.defaultTtlMs;
62
+ const store = /* @__PURE__ */ new Map();
63
+ let hits = 0;
64
+ let misses = 0;
65
+ let evictions = 0;
66
+ function isExpired(entry) {
67
+ return entry.expiresAt !== void 0 && entry.expiresAt <= Date.now();
68
+ }
69
+ return {
70
+ async get(key) {
71
+ const entry = store.get(key);
72
+ if (entry === void 0) {
73
+ misses++;
74
+ return void 0;
75
+ }
76
+ if (isExpired(entry)) {
77
+ store.delete(key);
78
+ misses++;
79
+ return void 0;
80
+ }
81
+ store.delete(key);
82
+ store.set(key, entry);
83
+ hits++;
84
+ return entry.value;
85
+ },
86
+ async set(key, value, ttlMs) {
87
+ const ttl = ttlMs ?? defaultTtlMs;
88
+ const expiresAt = ttl !== void 0 ? Date.now() + ttl : void 0;
89
+ if (store.has(key)) store.delete(key);
90
+ store.set(key, { value, expiresAt });
91
+ while (store.size > maxEntries) {
92
+ const oldest = store.keys().next().value;
93
+ if (oldest === void 0) break;
94
+ store.delete(oldest);
95
+ evictions++;
96
+ }
97
+ },
98
+ async delete(key) {
99
+ store.delete(key);
100
+ },
101
+ stats() {
102
+ return {
103
+ size: store.size,
104
+ hits,
105
+ misses,
106
+ evictions
107
+ };
108
+ }
109
+ };
110
+ }
111
+ var InFlight = class {
112
+ map = /* @__PURE__ */ new Map();
113
+ async run(key, factory) {
114
+ const existing = this.map.get(key);
115
+ if (existing !== void 0) return existing;
116
+ const promise = factory().finally(() => {
117
+ this.map.delete(key);
118
+ });
119
+ this.map.set(key, promise);
120
+ return promise;
121
+ }
122
+ /** Drop the in-flight slot for `key`, allowing the next caller to retry. */
123
+ forget(key) {
124
+ this.map.delete(key);
125
+ }
126
+ };
127
+
128
+ // src/errors.ts
129
+ var DomovoiError = class extends Error {
130
+ code;
131
+ constructor(message, options) {
132
+ super(message, options?.cause !== void 0 ? { cause: options.cause } : void 0);
133
+ this.name = "DomovoiError";
134
+ this.code = options?.code ?? "unspecified";
135
+ }
136
+ };
137
+ var ProviderError = class extends DomovoiError {
138
+ constructor(message, options) {
139
+ super(message, options);
140
+ this.name = "ProviderError";
141
+ }
142
+ };
143
+ var ConfigError = class extends DomovoiError {
144
+ constructor(message, options) {
145
+ super(message, options);
146
+ this.name = "ConfigError";
147
+ }
148
+ };
149
+ var BudgetExhaustedError = class extends DomovoiError {
150
+ attemptedProviders;
151
+ elapsedMs;
152
+ scope;
153
+ constructor(message, options) {
154
+ super(message, { code: options.scope, cause: options.cause });
155
+ this.name = "BudgetExhaustedError";
156
+ this.scope = options.scope;
157
+ this.attemptedProviders = options.attemptedProviders;
158
+ this.elapsedMs = options.elapsedMs;
159
+ }
160
+ };
161
+ function canonicalizeProviderThrow(thrown) {
162
+ if (thrown instanceof DomovoiError) return thrown;
163
+ if (thrown instanceof Error) {
164
+ return new ProviderError(thrown.message || "Provider call failed", {
165
+ code: "provider_network",
166
+ cause: thrown
167
+ });
168
+ }
169
+ return new ProviderError(String(thrown), {
170
+ code: "provider_network",
171
+ cause: thrown
172
+ });
173
+ }
174
+ function serializeError(err) {
175
+ if (!(err instanceof Error)) {
176
+ return { name: "Error", message: String(err) };
177
+ }
178
+ const code = err instanceof DomovoiError ? err.code : void 0;
179
+ const cause = err.cause !== void 0 ? serializeError(err.cause) : void 0;
180
+ return {
181
+ name: err.name,
182
+ message: err.message,
183
+ ...code !== void 0 ? { code } : {},
184
+ ...cause !== void 0 ? { cause } : {},
185
+ ...err.stack !== void 0 ? { stack: err.stack } : {}
186
+ };
187
+ }
188
+
189
+ // src/calibration/index.ts
190
+ var identity = {
191
+ kind: "identity",
192
+ apply(d) {
193
+ return d;
194
+ }
195
+ };
196
+ function isIdentityCalibrator(c) {
197
+ return c.kind === "identity";
198
+ }
199
+
200
+ // src/prompt.ts
201
+ var DEFAULT_TEMPLATE_HASH = "domovoi/v0-default";
202
+ var defaultTemplate = {
203
+ systemPrompt: "You are a careful classifier. Output exactly one of: {labels_csv}. No other text.",
204
+ userTemplate: (input, _space, question) => question === void 0 ? input : `${question}
205
+ ${input}`,
206
+ templateHash: DEFAULT_TEMPLATE_HASH
207
+ };
208
+ function renderSystemPrompt(template, space) {
209
+ if (template.systemPrompt === void 0) return void 0;
210
+ return template.systemPrompt.replace("{labels_csv}", space.join(", "));
211
+ }
212
+ function renderUserPrompt(template, input, space, question) {
213
+ return template.userTemplate(input, space, question);
214
+ }
215
+
216
+ // src/validate.ts
217
+ function validateSpace(space) {
218
+ if (space.length < 2) {
219
+ throw new ConfigError(`Decision space must have at least 2 labels; got ${space.length}.`, {
220
+ code: "invalid_space"
221
+ });
222
+ }
223
+ const seen = /* @__PURE__ */ new Set();
224
+ for (let i = 0; i < space.length; i++) {
225
+ const label = space[i];
226
+ if (label !== label.trim()) {
227
+ throw new ConfigError(
228
+ `Decision space label at index ${i} has leading/trailing whitespace: ${JSON.stringify(label)}.`,
229
+ { code: "invalid_space" }
230
+ );
231
+ }
232
+ const normalized = label.normalize("NFC");
233
+ if (!normalized) {
234
+ throw new ConfigError(`Decision space label at index ${i} is empty.`, {
235
+ code: "invalid_space"
236
+ });
237
+ }
238
+ if (seen.has(normalized)) {
239
+ throw new ConfigError(`Decision space contains duplicate label: ${JSON.stringify(label)}.`, {
240
+ code: "invalid_space"
241
+ });
242
+ }
243
+ seen.add(normalized);
244
+ }
245
+ }
246
+ function validateThresholds(thresholds, spaceLength) {
247
+ const t = thresholds;
248
+ inRange01("high", t.high);
249
+ if (t.coverageMin !== void 0) inRange01("coverageMin", t.coverageMin);
250
+ if (spaceLength === 2) {
251
+ if (t.low === void 0) {
252
+ throw new ConfigError("Binary classifier requires `thresholds.low`.", {
253
+ code: "invalid_thresholds"
254
+ });
255
+ }
256
+ inRange01("low", t.low);
257
+ if (!(t.high > t.low)) {
258
+ throw new ConfigError(
259
+ `Binary deadband requires high > low strict; got high=${t.high}, low=${t.low}.`,
260
+ { code: "invalid_thresholds" }
261
+ );
262
+ }
263
+ } else {
264
+ if (t.margin !== void 0) {
265
+ if (t.margin < 0 || t.margin > 1 || Number.isNaN(t.margin)) {
266
+ throw new ConfigError(`thresholds.margin must be in [0, 1]; got ${t.margin}.`, {
267
+ code: "invalid_thresholds"
268
+ });
269
+ }
270
+ }
271
+ }
272
+ }
273
+ function inRange01(name, value) {
274
+ if (Number.isNaN(value) || value < 0 || value > 1) {
275
+ throw new ConfigError(`thresholds.${name} must be in [0, 1] inclusive; got ${value}.`, {
276
+ code: "invalid_thresholds"
277
+ });
278
+ }
279
+ }
280
+ var CLASSIFIER_NAME_REGEX = /^[a-z][a-z0-9_]*$/;
281
+ function validateClassifierName(name) {
282
+ if (!CLASSIFIER_NAME_REGEX.test(name)) {
283
+ throw new ConfigError(
284
+ `Classifier name must match /^[a-z][a-z0-9_]*$/; got ${JSON.stringify(name)}.`,
285
+ { code: "invalid_classifier_name" }
286
+ );
287
+ }
288
+ }
289
+ function validateProviderChain(providers, spaceLength) {
290
+ if (providers.length === 0) {
291
+ throw new ConfigError("Provider chain is empty; supply at least one provider.", {
292
+ code: "empty_providers"
293
+ });
294
+ }
295
+ const logprobsCaps = providers.filter((p) => p.capabilities.distributionSource === "logprobs").map((p) => ({ id: p.id, cap: p.capabilities.maxTopLogprobs }));
296
+ if (logprobsCaps.length === 0) return;
297
+ const min = logprobsCaps.reduce(
298
+ (acc, p) => p.cap < acc.cap ? p : acc,
299
+ logprobsCaps[0]
300
+ );
301
+ if (spaceLength > min.cap) {
302
+ throw new ConfigError(
303
+ `Decision space size (${spaceLength}) exceeds provider ${min.id}'s top-K cap (${min.cap}). Reduce space size or replace the provider.`,
304
+ { code: "decision_space_too_large" }
305
+ );
306
+ }
307
+ }
308
+ function validateCalibratorCompatibility(calibratorIsIdentity, providers) {
309
+ if (calibratorIsIdentity) return;
310
+ const multiSample = providers.find((p) => p.capabilities.distributionSource === "multi_sample");
311
+ if (multiSample) {
312
+ throw new ConfigError(
313
+ `Provider ${multiSample.id} uses distributionSource: "multi_sample"; non-identity calibrators are not supported on multi-sample providers in v0. Use 'identity' calibrator or remove the multi-sample provider from the chain.`,
314
+ { code: "incompatible_calibrator" }
315
+ );
316
+ }
317
+ }
318
+ var SUM_TOLERANCE = 1e-3;
319
+ function validateDistribution(d, space) {
320
+ if (Number.isNaN(d.coverage) || d.coverage < 0 || d.coverage > 1) {
321
+ throw new ProviderError(`Invalid Distribution.coverage: ${d.coverage} (must be in [0, 1]).`, {
322
+ code: "invalid_distribution"
323
+ });
324
+ }
325
+ const probs = d.probs;
326
+ for (const [label, prob] of Object.entries(probs)) {
327
+ if (prob === void 0) continue;
328
+ if (Number.isNaN(prob) || prob < 0 || prob > 1) {
329
+ throw new ProviderError(
330
+ `Invalid probability for label ${JSON.stringify(label)}: ${prob} (must be in [0, 1]).`,
331
+ { code: "invalid_distribution" }
332
+ );
333
+ }
334
+ }
335
+ for (const label of space) {
336
+ if (!(label in probs)) {
337
+ probs[label] = 0;
338
+ }
339
+ }
340
+ let sum = 0;
341
+ for (const label of space) {
342
+ sum += probs[label] ?? 0;
343
+ }
344
+ if (Math.abs(sum - 1) > SUM_TOLERANCE) {
345
+ throw new ProviderError(
346
+ `Distribution probs sum to ${sum.toFixed(6)}; expected 1 \xB1 ${SUM_TOLERANCE}.`,
347
+ { code: "invalid_distribution" }
348
+ );
349
+ }
350
+ }
351
+
352
+ // src/engine/config.ts
353
+ var DEFAULT_PER_CALL_TIMEOUT_MS = 1e4;
354
+ var DEFAULT_CHAIN_TIMEOUT_MS = 3e4;
355
+ function withDefaults(input) {
356
+ return {
357
+ space: input.space,
358
+ thresholds: input.thresholds,
359
+ providers: input.providers,
360
+ calibrator: input.calibrator ?? identity,
361
+ cache: input.cache ?? memoryCache(),
362
+ template: input.template ?? defaultTemplate,
363
+ ...input.question !== void 0 ? { question: input.question } : {},
364
+ ...input.budget !== void 0 ? { budget: input.budget } : {},
365
+ onErrorPolicy: input.onErrorPolicy ?? "fallback",
366
+ ...input.onProviderError !== void 0 ? { onProviderError: input.onProviderError } : {},
367
+ ...input.hooks !== void 0 ? { hooks: input.hooks } : {},
368
+ providerConfigHash: input.providerConfigHash ?? "",
369
+ temperature: 0
370
+ };
371
+ }
372
+ function validateClassifierConfig(input) {
373
+ if (input.name !== void 0) validateClassifierName(input.name);
374
+ validateSpace(input.space);
375
+ validateProviderChain(input.providers, input.space.length);
376
+ validateThresholds(input.thresholds, input.space.length);
377
+ validateCalibratorCompatibility(isIdentityCalibrator(input.calibrator), input.providers);
378
+ for (const provider of input.providers) {
379
+ provider.validate?.(input.space);
380
+ }
381
+ }
382
+
383
+ // src/engine/meta.ts
384
+ function makeMetaBuilder() {
385
+ return {
386
+ providersAttempted: [],
387
+ providerErrors: [],
388
+ startedAtMs: Date.now(),
389
+ cacheHit: false
390
+ };
391
+ }
392
+ function buildMeta(builder, provider) {
393
+ return {
394
+ providerUsed: provider.id,
395
+ providersAttempted: [...builder.providersAttempted],
396
+ providerErrors: [...builder.providerErrors],
397
+ latencyMs: Date.now() - builder.startedAtMs,
398
+ cacheHit: builder.cacheHit,
399
+ coverageQuality: provider.capabilities.coverageMeasurement,
400
+ distributionSource: provider.capabilities.distributionSource
401
+ };
402
+ }
403
+ function buildMetaForFailure(builder, fallbackProvider) {
404
+ const providerUsed = fallbackProvider?.id ?? builder.providersAttempted.at(-1) ?? "";
405
+ return {
406
+ providerUsed,
407
+ providersAttempted: [...builder.providersAttempted],
408
+ providerErrors: [...builder.providerErrors],
409
+ latencyMs: Date.now() - builder.startedAtMs,
410
+ cacheHit: builder.cacheHit,
411
+ coverageQuality: fallbackProvider?.capabilities.coverageMeasurement ?? "none",
412
+ distributionSource: fallbackProvider?.capabilities.distributionSource ?? "logprobs"
413
+ };
414
+ }
415
+
416
+ // src/engine/abort.ts
417
+ function abortReason(signal) {
418
+ if (signal === void 0 || !signal.aborted) return void 0;
419
+ const reason = signal.reason;
420
+ if (typeof reason === "string") return reason;
421
+ if (reason instanceof Error) return reason.message;
422
+ if (reason !== void 0) return String(reason);
423
+ return "aborted";
424
+ }
425
+ function isTimeoutAbort(signal) {
426
+ const reason = signal.reason;
427
+ return reason instanceof Error && reason.name === "TimeoutError";
428
+ }
429
+ function buildCancelledVerdict(meta, reason, provider) {
430
+ return {
431
+ kind: "unknown",
432
+ reason: { type: "cancelled", reason },
433
+ meta: buildMetaForFailure(meta, provider)
434
+ };
435
+ }
436
+ function buildBudgetExhaustedVerdict(meta, scope, policy, provider) {
437
+ if (policy === "throw") return void 0;
438
+ return {
439
+ kind: "unknown",
440
+ reason: { type: "budget_exhausted", scope },
441
+ meta: buildMetaForFailure(meta, provider)
442
+ };
443
+ }
444
+ function deserializeForAggregate(serialized) {
445
+ const err = new Error(serialized.message);
446
+ err.name = serialized.name;
447
+ if (serialized.stack !== void 0) err.stack = serialized.stack;
448
+ if (serialized.cause !== void 0) {
449
+ Object.defineProperty(err, "cause", {
450
+ value: deserializeForAggregate(serialized.cause),
451
+ enumerable: false,
452
+ writable: true
453
+ });
454
+ }
455
+ return err;
456
+ }
457
+
458
+ // src/engine/distribution.ts
459
+ var globalInFlight = new InFlight();
460
+ function computeProviderCacheKey(provider, formattedInput, config) {
461
+ return computeCacheKey({
462
+ providerId: provider.id,
463
+ modelId: provider.modelId,
464
+ tokenizerId: provider.tokenizerId,
465
+ templateHash: config.template.templateHash,
466
+ decisionSpace: config.space,
467
+ temperature: config.temperature,
468
+ providerConfigHash: config.providerConfigHash,
469
+ formattedInput
470
+ });
471
+ }
472
+ function mergeSignals(userSignal, timeoutSignal) {
473
+ const signals = [timeoutSignal];
474
+ if (userSignal !== void 0) signals.push(userSignal);
475
+ return AbortSignal.any(signals);
476
+ }
477
+ async function loadDistribution(provider, formattedInput, config, meta, signal, cacheKey) {
478
+ const cached = await config.cache.get(cacheKey);
479
+ if (cached !== void 0) {
480
+ const parsed = deserializeCachedValue(cached);
481
+ if (parsed !== void 0) {
482
+ meta.cacheHit = true;
483
+ return parsed;
484
+ }
485
+ }
486
+ const fresh = await fetchFresh(provider, formattedInput, config, signal, cacheKey);
487
+ await config.cache.set(cacheKey, serializeCachedValue(fresh));
488
+ return fresh;
489
+ }
490
+ async function fetchFresh(provider, formattedInput, config, signal, cacheKey) {
491
+ return globalInFlight.run(
492
+ cacheKey,
493
+ () => provider.sample(formattedInput, config.space, {
494
+ template: config.template,
495
+ temperature: config.temperature,
496
+ timeoutMs: config.budget?.perCallTimeoutMs ?? DEFAULT_PER_CALL_TIMEOUT_MS,
497
+ signal
498
+ })
499
+ );
500
+ }
501
+ function forgetInFlight(cacheKey) {
502
+ globalInFlight.forget(cacheKey);
503
+ }
504
+
505
+ // src/engine/hooks.ts
506
+ function fireAndForget(fn, ...args) {
507
+ if (fn === void 0) return;
508
+ try {
509
+ const result = fn(...args);
510
+ if (result instanceof Promise) {
511
+ result.catch(() => {
512
+ });
513
+ }
514
+ } catch {
515
+ }
516
+ }
517
+
518
+ // src/engine/error-recording.ts
519
+ function handleDistributionError(err, provider, meta, config, userSignal, timeoutSignal, index, chainStartMs, cacheKey) {
520
+ if (timeoutSignal.aborted && isTimeoutAbort(timeoutSignal)) {
521
+ const verdict = buildBudgetExhaustedVerdict(
522
+ meta,
523
+ "per_call_timeout",
524
+ config.onErrorPolicy,
525
+ provider
526
+ );
527
+ if (verdict !== void 0) return { kind: "verdict", verdict };
528
+ throw new BudgetExhaustedError("per_call_timeout exceeded", {
529
+ scope: "per_call_timeout",
530
+ attemptedProviders: meta.providersAttempted,
531
+ elapsedMs: Date.now() - chainStartMs,
532
+ cause: err
533
+ });
534
+ }
535
+ const userAbort = abortReason(userSignal);
536
+ if (userAbort !== void 0) {
537
+ const verdict = buildCancelledVerdict(meta, userAbort, provider);
538
+ return { kind: "verdict", verdict };
539
+ }
540
+ return recordProviderError(err, provider, meta, config, index, cacheKey);
541
+ }
542
+ function recordProviderError(err, provider, meta, config, index, cacheKey) {
543
+ const wrapped = canonicalizeProviderThrow(err);
544
+ if (!(wrapped instanceof ProviderError)) {
545
+ throw wrapped;
546
+ }
547
+ meta.providerErrors.push({ providerId: provider.id, error: serializeError(wrapped) });
548
+ fireAndForget(config.onProviderError, wrapped, {
549
+ providerId: provider.id,
550
+ attempt: index + 1
551
+ });
552
+ forgetInFlight(cacheKey);
553
+ return { kind: "continue" };
554
+ }
555
+ function recordValidationError(err, provider, meta, config, index, cacheKey) {
556
+ const wrapped = canonicalizeProviderThrow(err);
557
+ if (!(wrapped instanceof ProviderError)) throw wrapped;
558
+ meta.providerErrors.push({ providerId: provider.id, error: serializeError(wrapped) });
559
+ fireAndForget(config.onProviderError, wrapped, {
560
+ providerId: provider.id,
561
+ attempt: index + 1
562
+ });
563
+ forgetInFlight(cacheKey);
564
+ return { kind: "continue" };
565
+ }
566
+
567
+ // src/engine/finalize.ts
568
+ function checkChainBudget(meta, chainStartMs, chainTimeoutMs, config) {
569
+ const elapsed = Date.now() - chainStartMs;
570
+ if (elapsed < chainTimeoutMs) return void 0;
571
+ const verdict = buildBudgetExhaustedVerdict(meta, "chain_timeout", config.onErrorPolicy);
572
+ if (verdict !== void 0) return verdict;
573
+ throw new BudgetExhaustedError("chain_timeout exceeded", {
574
+ scope: "chain_timeout",
575
+ attemptedProviders: meta.providersAttempted,
576
+ elapsedMs: elapsed
577
+ });
578
+ }
579
+ function makeCancelledFromMeta(meta, reason) {
580
+ return {
581
+ kind: "unknown",
582
+ reason: { type: "cancelled", reason },
583
+ meta: buildMetaForFailure(meta)
584
+ };
585
+ }
586
+ function finalizeChainExhausted(meta, lastCalibrated, attempts, config) {
587
+ const allErrored = lastCalibrated === void 0 && meta.providerErrors.length > 0 && meta.providerErrors.length === meta.providersAttempted.length;
588
+ if (allErrored) {
589
+ if (config.onErrorPolicy === "throw") {
590
+ const errors = meta.providerErrors.map((e) => deserializeForAggregate(e.error));
591
+ throw new AggregateError(errors, "All providers failed.");
592
+ }
593
+ const verdict2 = {
594
+ kind: "unknown",
595
+ reason: {
596
+ type: "provider_failure",
597
+ errors: meta.providerErrors.map((e) => e.error)
598
+ },
599
+ meta: buildMetaForFailure(meta)
600
+ };
601
+ fireAndForget(config.hooks?.onResult, verdict2);
602
+ return verdict2;
603
+ }
604
+ const verdict = {
605
+ kind: "unknown",
606
+ reason: {
607
+ type: "chain_exhausted",
608
+ lastDistribution: lastCalibrated ?? { probs: {}, coverage: 0 },
609
+ providersAttempted: attempts
610
+ },
611
+ meta: buildMetaForFailure(meta)
612
+ };
613
+ fireAndForget(config.hooks?.onResult, verdict);
614
+ return verdict;
615
+ }
616
+
617
+ // src/engine/threshold.ts
618
+ var DEFAULT_COVERAGE_MIN = 0.5;
619
+ function applyThresholds(d, thresholds, space) {
620
+ const t = thresholds;
621
+ const sorted = sortBySpaceProbability(d, space);
622
+ const top = sorted[0];
623
+ const second = sorted[1];
624
+ const coverageMin = t.coverageMin ?? DEFAULT_COVERAGE_MIN;
625
+ if (d.coverage < coverageMin) {
626
+ return {
627
+ kind: "out_of_distribution",
628
+ coverage: d.coverage,
629
+ topIfRenormalized: top.label,
630
+ probabilityIfRenormalized: top.prob
631
+ };
632
+ }
633
+ if (space.length === 2 && t.low !== void 0) {
634
+ return applyBinaryDeadband(top, second, t);
635
+ }
636
+ return applyMultiClassRule(top, second, t);
637
+ }
638
+ function sortBySpaceProbability(d, space) {
639
+ const probs = d.probs;
640
+ const items = space.map((label) => ({
641
+ label,
642
+ prob: probs[label]
643
+ }));
644
+ items.sort((a, b) => b.prob - a.prob);
645
+ return items;
646
+ }
647
+ function applyBinaryDeadband(top, second, t) {
648
+ if (top.prob >= t.high) {
649
+ return { kind: "classified", value: top.label, probability: top.prob };
650
+ }
651
+ if (t.low !== void 0 && top.prob <= t.low) {
652
+ return { kind: "classified", value: second.label, probability: second.prob };
653
+ }
654
+ return { kind: "uncertain", top: top.label, probability: top.prob, runnerUp: second.label };
655
+ }
656
+ function applyMultiClassRule(top, second, t) {
657
+ if (top.prob < t.high) {
658
+ return { kind: "uncertain", top: top.label, probability: top.prob, runnerUp: second.label };
659
+ }
660
+ if (t.margin !== void 0 && top.prob - second.prob < t.margin) {
661
+ return { kind: "uncertain", top: top.label, probability: top.prob, runnerUp: second.label };
662
+ }
663
+ return { kind: "classified", value: top.label, probability: top.prob };
664
+ }
665
+
666
+ // src/engine/decide.ts
667
+ async function decide(formattedInput, config, signal) {
668
+ const meta = makeMetaBuilder();
669
+ const preAbort = abortReason(signal);
670
+ if (preAbort !== void 0) {
671
+ return makeCancelledFromMeta(meta, preAbort);
672
+ }
673
+ fireAndForget(config.hooks?.onCall, formattedInput, {
674
+ providers: config.providers.map((p) => p.id)
675
+ });
676
+ const limits = computeLimits(config);
677
+ const chainStartMs = Date.now();
678
+ let lastCalibrated;
679
+ let attempts = 0;
680
+ for (let i = 0; i < config.providers.length; i++) {
681
+ const provider = config.providers[i];
682
+ if (attempts >= limits.maxCalls) break;
683
+ attempts++;
684
+ meta.providersAttempted.push(provider.id);
685
+ const chainBudgetVerdict = checkChainBudget(meta, chainStartMs, limits.chainTimeoutMs, config);
686
+ if (chainBudgetVerdict !== void 0) {
687
+ fireAndForget(config.hooks?.onResult, chainBudgetVerdict);
688
+ return chainBudgetVerdict;
689
+ }
690
+ const midAbort = abortReason(signal);
691
+ if (midAbort !== void 0) {
692
+ const verdict = buildCancelledVerdict(meta, midAbort, provider);
693
+ fireAndForget(config.hooks?.onResult, verdict);
694
+ return verdict;
695
+ }
696
+ const isLastProvider = i === config.providers.length - 1;
697
+ const outcome = await attemptProvider(
698
+ provider,
699
+ formattedInput,
700
+ config,
701
+ meta,
702
+ signal,
703
+ i,
704
+ chainStartMs,
705
+ limits.perCallTimeoutMs,
706
+ isLastProvider
707
+ );
708
+ if (outcome.kind === "verdict") {
709
+ fireAndForget(config.hooks?.onResult, outcome.verdict);
710
+ return outcome.verdict;
711
+ }
712
+ if (outcome.kind === "lastUncertain") {
713
+ lastCalibrated = outcome.calibrated;
714
+ fireAndForget(config.hooks?.onResult, outcome.verdict);
715
+ return outcome.verdict;
716
+ }
717
+ if (outcome.calibrated !== void 0) lastCalibrated = outcome.calibrated;
718
+ }
719
+ return finalizeChainExhausted(meta, lastCalibrated, attempts, config);
720
+ }
721
+ function computeLimits(config) {
722
+ return {
723
+ perCallTimeoutMs: config.budget?.perCallTimeoutMs ?? DEFAULT_PER_CALL_TIMEOUT_MS,
724
+ chainTimeoutMs: config.budget?.chainTimeoutMs ?? DEFAULT_CHAIN_TIMEOUT_MS,
725
+ maxCalls: config.budget?.maxCalls ?? config.providers.length
726
+ };
727
+ }
728
+ async function attemptProvider(provider, formattedInput, config, meta, userSignal, index, chainStartMs, perCallTimeoutMs, isLastProvider) {
729
+ const cacheKey = computeProviderCacheKey(provider, formattedInput, config);
730
+ const timeoutSignal = AbortSignal.timeout(perCallTimeoutMs);
731
+ const mergedSignal = mergeSignals(userSignal, timeoutSignal);
732
+ let distribution;
733
+ try {
734
+ distribution = await loadDistribution(
735
+ provider,
736
+ formattedInput,
737
+ config,
738
+ meta,
739
+ mergedSignal,
740
+ cacheKey
741
+ );
742
+ } catch (err) {
743
+ return handleDistributionError(
744
+ err,
745
+ provider,
746
+ meta,
747
+ config,
748
+ userSignal,
749
+ timeoutSignal,
750
+ index,
751
+ chainStartMs,
752
+ cacheKey
753
+ );
754
+ }
755
+ try {
756
+ validateDistribution(distribution, config.space);
757
+ } catch (err) {
758
+ return recordValidationError(err, provider, meta, config, index, cacheKey);
759
+ }
760
+ const calibrated = config.calibrator.apply(distribution);
761
+ const result = applyThresholds(calibrated, config.thresholds, config.space);
762
+ if (result.kind === "classified") {
763
+ const verdict = {
764
+ kind: "classified",
765
+ value: result.value,
766
+ probability: result.probability,
767
+ meta: buildMeta(meta, provider)
768
+ };
769
+ return { kind: "verdict", verdict };
770
+ }
771
+ if (result.kind === "out_of_distribution") {
772
+ const verdict = {
773
+ kind: "unknown",
774
+ reason: {
775
+ type: "out_of_distribution",
776
+ coverage: result.coverage,
777
+ topIfRenormalized: result.topIfRenormalized,
778
+ probabilityIfRenormalized: result.probabilityIfRenormalized
779
+ },
780
+ meta: buildMeta(meta, provider)
781
+ };
782
+ return { kind: "verdict", verdict };
783
+ }
784
+ if (isLastProvider) {
785
+ const verdict = {
786
+ kind: "uncertain",
787
+ top: result.top,
788
+ probability: result.probability,
789
+ runnerUp: result.runnerUp,
790
+ distribution: calibrated,
791
+ meta: buildMeta(meta, provider)
792
+ };
793
+ return { kind: "lastUncertain", verdict, calibrated };
794
+ }
795
+ return { kind: "continue", calibrated };
796
+ }
797
+ var cl100kSingleton;
798
+ function cl100kTokenizer() {
799
+ if (cl100kSingleton !== void 0) return cl100kSingleton;
800
+ const enc = get_encoding("cl100k_base");
801
+ cl100kSingleton = {
802
+ id: "openai/cl100k_base",
803
+ encode(label) {
804
+ const withLeadingSpace = label.startsWith(" ") ? label : ` ${label}`;
805
+ const ids = enc.encode(withLeadingSpace);
806
+ return Array.from(ids);
807
+ },
808
+ firstTokenId(label) {
809
+ const ids = this.encode(label);
810
+ const first = ids[0];
811
+ if (first === void 0) {
812
+ throw new Error(`Tokenizer produced empty encoding for label ${JSON.stringify(label)}`);
813
+ }
814
+ return first;
815
+ }
816
+ };
817
+ return cl100kSingleton;
818
+ }
819
+ function findFirstTokenCollision(tokenizer, space) {
820
+ const seenByTokenId = /* @__PURE__ */ new Map();
821
+ for (const label of space) {
822
+ const tokenId = tokenizer.firstTokenId(label);
823
+ const priorLabel = seenByTokenId.get(tokenId);
824
+ if (priorLabel !== void 0 && priorLabel !== label) {
825
+ return { a: priorLabel, b: label, tokenId };
826
+ }
827
+ seenByTokenId.set(tokenId, label);
828
+ }
829
+ return void 0;
830
+ }
831
+ function buildLogitBias(tokenizer, space, bias = 100) {
832
+ return Object.fromEntries(
833
+ space.map((label) => [String(tokenizer.firstTokenId(label)), bias])
834
+ );
835
+ }
836
+
837
+ // src/providers/openai/distribution.ts
838
+ function buildDistributionByTokenId(space, tokenLogprobs, inSpaceIds, tokenizer) {
839
+ const inSpace = /* @__PURE__ */ new Map();
840
+ let inSpaceMass = 0;
841
+ for (const entry of tokenLogprobs) {
842
+ const ids = tokenizer.encode(entry.token);
843
+ const firstId = ids[0];
844
+ if (firstId === void 0) continue;
845
+ const label = inSpaceIds.get(firstId);
846
+ if (label === void 0) continue;
847
+ const prob = Math.exp(entry.logprob);
848
+ const previous = inSpace.get(label) ?? 0;
849
+ if (prob > previous) {
850
+ inSpace.set(label, prob);
851
+ inSpaceMass += prob - previous;
852
+ }
853
+ }
854
+ return renormalize(space, inSpace, inSpaceMass);
855
+ }
856
+ function buildDistributionByStringMatch(space, tokenLogprobs) {
857
+ const inSpace = /* @__PURE__ */ new Map();
858
+ let inSpaceMass = 0;
859
+ for (const label of space) {
860
+ const trimmed = label.trim();
861
+ let bestProb = 0;
862
+ for (const entry of tokenLogprobs) {
863
+ const tok = entry.token.trim();
864
+ if (tok === trimmed || tok && trimmed.startsWith(tok)) {
865
+ const prob = Math.exp(entry.logprob);
866
+ if (prob > bestProb) bestProb = prob;
867
+ }
868
+ }
869
+ if (bestProb > 0) {
870
+ inSpace.set(label, bestProb);
871
+ inSpaceMass += bestProb;
872
+ }
873
+ }
874
+ return renormalize(space, inSpace, inSpaceMass);
875
+ }
876
+ function renormalize(space, inSpace, inSpaceMass) {
877
+ const coverage = Math.min(1, inSpaceMass);
878
+ const probs = {};
879
+ for (const label of space) {
880
+ const raw = inSpace.get(label) ?? 0;
881
+ probs[label] = inSpaceMass > 0 ? raw / inSpaceMass : 0;
882
+ }
883
+ return {
884
+ probs,
885
+ coverage
886
+ };
887
+ }
888
+
889
+ // src/providers/openai/adapter.ts
890
+ var LOGIT_BIAS_VALUE = 100;
891
+ function buildAdapter(args) {
892
+ const collisionMemo = /* @__PURE__ */ new Set();
893
+ const tokenizer = args.tokenizer;
894
+ const eagerValidate = tokenizer === void 0 ? {} : {
895
+ validate: (space) => {
896
+ ensureNoCollisions(tokenizer, space, collisionMemo);
897
+ }
898
+ };
899
+ return {
900
+ id: args.id,
901
+ modelId: args.modelId,
902
+ tokenizerId: args.tokenizerId,
903
+ capabilities: args.capabilities,
904
+ ...eagerValidate,
905
+ async sample(input, space, opts) {
906
+ let logitBias;
907
+ let inSpaceFirstTokenIds;
908
+ if (tokenizer !== void 0) {
909
+ ensureNoCollisions(tokenizer, space, collisionMemo);
910
+ logitBias = buildLogitBias(tokenizer, space, LOGIT_BIAS_VALUE);
911
+ inSpaceFirstTokenIds = mapFirstTokenIds(tokenizer, space);
912
+ }
913
+ const messages = [];
914
+ const system = renderSystemPrompt(opts.template, space);
915
+ if (system !== void 0) {
916
+ messages.push({ role: "system", content: system });
917
+ }
918
+ messages.push({ role: "user", content: renderUserPrompt(opts.template, input, space) });
919
+ let response;
920
+ try {
921
+ const params = {
922
+ model: args.modelId,
923
+ messages,
924
+ temperature: opts.temperature,
925
+ logprobs: true,
926
+ top_logprobs: Math.min(args.capabilities.maxTopLogprobs, Math.max(space.length * 2, 5)),
927
+ // One label is one short word; 16 tokens is enough headroom.
928
+ max_completion_tokens: 16
929
+ };
930
+ if (opts.seed !== void 0) params.seed = opts.seed;
931
+ if (logitBias !== void 0) params.logit_bias = logitBias;
932
+ const requestOpts = {
933
+ timeout: opts.timeoutMs
934
+ };
935
+ if (opts.signal !== void 0) requestOpts.signal = opts.signal;
936
+ response = await args.client.chat.completions.create(params, requestOpts);
937
+ } catch (err) {
938
+ throw canonicalizeProviderThrow(err);
939
+ }
940
+ const choice = response.choices[0];
941
+ if (choice === void 0) {
942
+ throw new ProviderError("OpenAI response had no choices.", {
943
+ code: "provider_malformed_response"
944
+ });
945
+ }
946
+ const tokenLogprobs = choice.logprobs?.content?.[0]?.top_logprobs;
947
+ if (tokenLogprobs === void 0 || tokenLogprobs.length === 0) {
948
+ throw new ProviderError("OpenAI response missing first-token logprobs.", {
949
+ code: "provider_malformed_response"
950
+ });
951
+ }
952
+ return tokenizer !== void 0 && inSpaceFirstTokenIds !== void 0 ? buildDistributionByTokenId(space, tokenLogprobs, inSpaceFirstTokenIds, tokenizer) : buildDistributionByStringMatch(space, tokenLogprobs);
953
+ }
954
+ };
955
+ }
956
+ function ensureNoCollisions(tokenizer, space, memo) {
957
+ const memoKey = JSON.stringify(space);
958
+ if (memo.has(memoKey)) return;
959
+ const collision = findFirstTokenCollision(tokenizer, space);
960
+ if (collision !== void 0) {
961
+ throw new ConfigError(
962
+ `Decision space contains first-token collision: ${JSON.stringify(collision.a)} and ${JSON.stringify(collision.b)} both encode to token id ${collision.tokenId}. Prefix-disambiguate the labels (e.g., 'A_yes' / 'A_no') or pick alternatives.`,
963
+ { code: "decision_space_collision" }
964
+ );
965
+ }
966
+ memo.add(memoKey);
967
+ }
968
+ function mapFirstTokenIds(tokenizer, space) {
969
+ const map = /* @__PURE__ */ new Map();
970
+ for (const label of space) {
971
+ map.set(tokenizer.firstTokenId(label), label);
972
+ }
973
+ return map;
974
+ }
975
+
976
+ // src/providers/openai/factory.ts
977
+ var LOGPROBS_CAPABILITIES = {
978
+ distributionSource: "logprobs",
979
+ coverageMeasurement: "exact",
980
+ // OpenAI hosted caps top_logprobs at 20; most OpenAI-compat backends match.
981
+ maxTopLogprobs: 20
982
+ };
983
+ function openai(model, opts) {
984
+ const client = new OpenAI({
985
+ apiKey: process.env.OPENAI_API_KEY,
986
+ baseURL: opts?.baseURL,
987
+ timeout: opts?.timeout
988
+ });
989
+ return buildAdapter({
990
+ id: `openai/${model}`,
991
+ modelId: model,
992
+ tokenizerId: "openai/cl100k_base",
993
+ capabilities: LOGPROBS_CAPABILITIES,
994
+ client,
995
+ tokenizer: cl100kTokenizer()
996
+ });
997
+ }
998
+ var OLLAMA_DEFAULT_BASE_URL = "http://localhost:11434/v1";
999
+ var OLLAMA_DEFAULT_API_KEY = "ollama";
1000
+ function ollama(model, opts) {
1001
+ const client = new OpenAI({
1002
+ apiKey: OLLAMA_DEFAULT_API_KEY,
1003
+ baseURL: OLLAMA_DEFAULT_BASE_URL,
1004
+ timeout: opts?.timeout
1005
+ });
1006
+ return buildAdapter({
1007
+ id: `ollama/${model}`,
1008
+ modelId: model,
1009
+ // Tokenizer id for cache-key composition; treated opaquely.
1010
+ tokenizerId: `ollama/${model}`,
1011
+ capabilities: LOGPROBS_CAPABILITIES,
1012
+ client
1013
+ // No tokenizer — string-based fallback matches Ollama's varied tokenizers.
1014
+ });
1015
+ }
1016
+
1017
+ // src/env.ts
1018
+ var BUILTIN_FACTORIES = {
1019
+ openai: (model) => openai(model),
1020
+ ollama: (model) => ollama(model)
1021
+ };
1022
+ function parseProvidersEnv(raw) {
1023
+ if (raw === void 0) return [];
1024
+ const trimmed = raw.trim();
1025
+ if (!trimmed) return [];
1026
+ return trimmed.split(",").map((part) => part.trim()).filter(Boolean).map(parseProviderEntry);
1027
+ }
1028
+ function parseProviderEntry(entry) {
1029
+ const slashIndex = entry.indexOf("/");
1030
+ if (slashIndex < 0) {
1031
+ throw new ConfigError(
1032
+ `DOMOVOI_PROVIDERS: malformed entry ${JSON.stringify(entry)}; expected factory/model format.`,
1033
+ { code: "malformed_provider_config" }
1034
+ );
1035
+ }
1036
+ const factory = entry.slice(0, slashIndex).trim();
1037
+ const model = entry.slice(slashIndex + 1).trim();
1038
+ if (!factory || !model) {
1039
+ throw new ConfigError(
1040
+ `DOMOVOI_PROVIDERS: malformed entry ${JSON.stringify(entry)}; factory or model is empty.`,
1041
+ { code: "malformed_provider_config" }
1042
+ );
1043
+ }
1044
+ return { factory, model };
1045
+ }
1046
+ function resolveProvidersFromEnv(raw) {
1047
+ const entries = parseProvidersEnv(raw);
1048
+ if (entries.length === 0) {
1049
+ throw new ConfigError(
1050
+ "Cannot resolve provider chain: DOMOVOI_PROVIDERS is unset or empty. Set the env variable or pass `providers` explicitly.",
1051
+ { code: "missing_provider_config" }
1052
+ );
1053
+ }
1054
+ return entries.map((entry) => {
1055
+ const factory = BUILTIN_FACTORIES[entry.factory];
1056
+ if (factory === void 0) {
1057
+ throw new ConfigError(
1058
+ `DOMOVOI_PROVIDERS: unknown factory ${JSON.stringify(entry.factory)}. Known: ${Object.keys(BUILTIN_FACTORIES).join(", ")}. For other providers, supply { providers } explicitly in code.`,
1059
+ { code: "unknown_provider_factory" }
1060
+ );
1061
+ }
1062
+ return factory(entry.model);
1063
+ });
1064
+ }
1065
+ function resolveDefaultProviders(name) {
1066
+ if (name !== void 0) {
1067
+ const namedKey = `DOMOVOI_PROVIDERS_${name.toUpperCase()}`;
1068
+ const namedRaw = process.env[namedKey];
1069
+ if (namedRaw?.trim()) {
1070
+ return resolveProvidersFromEnv(namedRaw);
1071
+ }
1072
+ }
1073
+ return resolveProvidersFromEnv(process.env.DOMOVOI_PROVIDERS);
1074
+ }
1075
+
1076
+ // src/verbs/boolean.ts
1077
+ var ONE_SHOT_BINARY_THRESHOLDS = { high: 0.7, low: 0.3, coverageMin: 0.3 };
1078
+ var YES_NO_SPACE = ["yes", "no"];
1079
+ async function boolean(input, question, opts) {
1080
+ const providers = opts?.providers !== void 0 && opts.providers.length > 0 ? opts.providers : resolveDefaultProviders();
1081
+ const calibrator = opts?.calibrator ?? identity;
1082
+ const cache = opts?.cache ?? memoryCache();
1083
+ const thresholds = opts?.thresholds ?? ONE_SHOT_BINARY_THRESHOLDS;
1084
+ validateClassifierConfig({
1085
+ space: YES_NO_SPACE,
1086
+ thresholds,
1087
+ providers,
1088
+ calibrator
1089
+ });
1090
+ const config = withDefaults({
1091
+ space: YES_NO_SPACE,
1092
+ thresholds,
1093
+ providers,
1094
+ calibrator,
1095
+ cache,
1096
+ template: defaultTemplate,
1097
+ question,
1098
+ ...opts?.budget !== void 0 ? { budget: opts.budget } : {}
1099
+ });
1100
+ const verdict = await decide(input, config, opts?.signal);
1101
+ return toBooleanVerdict(verdict);
1102
+ }
1103
+ function toBooleanVerdict(v) {
1104
+ switch (v.kind) {
1105
+ case "classified":
1106
+ return { ...v, value: v.value === "yes" };
1107
+ case "uncertain":
1108
+ return {
1109
+ ...v,
1110
+ top: v.top === "yes",
1111
+ runnerUp: v.runnerUp === "yes",
1112
+ distribution: rekey(v.distribution)
1113
+ };
1114
+ case "unknown":
1115
+ return { ...v, reason: convertCause(v.reason) };
1116
+ }
1117
+ }
1118
+ function convertCause(cause) {
1119
+ switch (cause.type) {
1120
+ case "out_of_distribution":
1121
+ return { ...cause, topIfRenormalized: cause.topIfRenormalized === "yes" };
1122
+ case "chain_exhausted":
1123
+ return { ...cause, lastDistribution: rekey(cause.lastDistribution) };
1124
+ // Remaining variants don't reference T — payload is structurally identical
1125
+ // between UnknownVerdictCause<YesNo> and UnknownVerdictCause<boolean>.
1126
+ case "predicate_rejected":
1127
+ case "provider_failure":
1128
+ case "budget_exhausted":
1129
+ case "cancelled":
1130
+ return cause;
1131
+ }
1132
+ }
1133
+ function rekey(d) {
1134
+ return {
1135
+ probs: { true: d.probs.yes, false: d.probs.no },
1136
+ coverage: d.coverage
1137
+ };
1138
+ }
1139
+
1140
+ // src/verbs/classifier.ts
1141
+ var DEFAULT_BATCH_CONCURRENCY = 5;
1142
+ function classifier(config) {
1143
+ const providers = config.providers !== void 0 && config.providers.length > 0 ? config.providers : resolveDefaultProviders(config.name);
1144
+ const calibrator = config.calibrator ?? identity;
1145
+ const cache = config.cache ?? memoryCache();
1146
+ const template = config.template ?? defaultTemplate;
1147
+ validateClassifierConfig({
1148
+ ...config.name !== void 0 ? { name: config.name } : {},
1149
+ space: config.space,
1150
+ thresholds: config.thresholds,
1151
+ providers,
1152
+ calibrator
1153
+ });
1154
+ const format = config.format ?? ((x) => x);
1155
+ const decideConfig = withDefaults({
1156
+ space: config.space,
1157
+ thresholds: config.thresholds,
1158
+ providers,
1159
+ calibrator,
1160
+ cache,
1161
+ template,
1162
+ ...config.question !== void 0 ? { question: config.question } : {},
1163
+ ...config.budget !== void 0 ? { budget: config.budget } : {},
1164
+ ...config.onErrorPolicy !== void 0 ? { onErrorPolicy: config.onErrorPolicy } : {},
1165
+ ...config.onProviderError !== void 0 ? { onProviderError: config.onProviderError } : {},
1166
+ ...config.hooks !== void 0 ? { hooks: config.hooks } : {}
1167
+ });
1168
+ const single = async (input, opts) => {
1169
+ const formatted = format(input);
1170
+ return decide(formatted, decideConfig, opts?.signal);
1171
+ };
1172
+ const batch = async (items, opts) => {
1173
+ const concurrency = opts?.concurrency ?? DEFAULT_BATCH_CONCURRENCY;
1174
+ const results = new Array(items.length);
1175
+ let next = 0;
1176
+ async function worker() {
1177
+ while (true) {
1178
+ const idx = next++;
1179
+ if (idx >= items.length) return;
1180
+ const item = items[idx];
1181
+ results[idx] = await single(
1182
+ item,
1183
+ opts?.signal !== void 0 ? { signal: opts.signal } : {}
1184
+ );
1185
+ }
1186
+ }
1187
+ const workers = Array.from({ length: Math.min(concurrency, items.length) }, () => worker());
1188
+ await Promise.all(workers);
1189
+ return results;
1190
+ };
1191
+ const callable = single;
1192
+ return Object.assign(callable, { batch });
1193
+ }
1194
+
1195
+ // src/verbs/classify.ts
1196
+ var ONE_SHOT_DEFAULT_THRESHOLDS = { high: 0.5, coverageMin: 0.3 };
1197
+ async function classify(input, space, opts) {
1198
+ const providers = opts?.providers !== void 0 && opts.providers.length > 0 ? opts.providers : resolveDefaultProviders();
1199
+ const calibrator = opts?.calibrator ?? identity;
1200
+ const cache = opts?.cache ?? memoryCache();
1201
+ const thresholds = opts?.thresholds ?? ONE_SHOT_DEFAULT_THRESHOLDS;
1202
+ validateClassifierConfig({
1203
+ space,
1204
+ thresholds,
1205
+ providers,
1206
+ calibrator
1207
+ });
1208
+ const config = withDefaults({
1209
+ space,
1210
+ thresholds,
1211
+ providers,
1212
+ calibrator,
1213
+ cache,
1214
+ template: defaultTemplate,
1215
+ ...opts?.question !== void 0 ? { question: opts.question } : {},
1216
+ ...opts?.budget !== void 0 ? { budget: opts.budget } : {}
1217
+ });
1218
+ return decide(input, config, opts?.signal);
1219
+ }
1220
+
1221
+ // src/verdict.ts
1222
+ function isClassified(v) {
1223
+ return v.kind === "classified";
1224
+ }
1225
+ function isUncertain(v) {
1226
+ return v.kind === "uncertain";
1227
+ }
1228
+ function isUnknown(v) {
1229
+ return v.kind === "unknown";
1230
+ }
1231
+ function match(v, handlers) {
1232
+ switch (v.kind) {
1233
+ case "classified":
1234
+ return handlers.classified(v);
1235
+ case "uncertain":
1236
+ return handlers.uncertain(v);
1237
+ case "unknown":
1238
+ return handlers.unknown(v);
1239
+ }
1240
+ }
1241
+ function filter(pred) {
1242
+ return (v) => {
1243
+ if (v.kind === "unknown") return v;
1244
+ if (pred(v)) return v;
1245
+ return {
1246
+ kind: "unknown",
1247
+ reason: { type: "predicate_rejected", previousKind: v.kind },
1248
+ meta: v.meta
1249
+ };
1250
+ };
1251
+ }
1252
+
1253
+ // src/index.ts
1254
+ var domovoi = {
1255
+ classify,
1256
+ boolean,
1257
+ classifier,
1258
+ memoryCache
1259
+ };
1260
+
1261
+ export { BudgetExhaustedError, ConfigError, DomovoiError, ProviderError, domovoi, filter, isClassified, isUncertain, isUnknown, match };
1262
+ //# sourceMappingURL=index.js.map
1263
+ //# sourceMappingURL=index.js.map