@hourslabs/domovoi 0.1.0 → 0.2.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.
package/README.md CHANGED
@@ -37,32 +37,30 @@ Some decisions in software resist code. Is "SQ *COFFEE 0421" a restaurant or a g
37
37
 
38
38
  Rules and trained classifiers have tackled this for decades. Rules break on edge cases, then multiply until they collapse under their own weight. Classifiers return a confidence score and leave the handling to you. They don't know what they don't know.
39
39
 
40
- domovoi is a single function call at the decision point. Unlike agent frameworks or workflow engines, it doesn't restructure how you build — it drops into existing code like any other dependency. Ask it, get a typed `Verdict`, continue.
40
+ domovoi is a single function call at the decision point. Unlike agent frameworks or workflow engines, it doesn't restructure how you build — it drops into existing code like any other dependency. Ask it, get a typed `Verdict`, and dispatch.
41
41
 
42
- The `Verdict` is the core idea. Not a string, not a confidence score one of three typed states: `Classified` when confident, `Uncertain` when the top answer falls below threshold, `Unknown` when no answer is possible. Uncertainty becomes a first-class value your type system understands, not a silent wrong answer. Everything around the `Verdict` stays deterministic.
42
+ The `Verdict` is the core idea. Rather than a string or a confidence score, domovoi returns one of three typed states: `Classified` when confident, `Uncertain` when the top answer falls below threshold, `Unknown` when no answer is possible. Uncertainty becomes a first-class value your type system understands, not a silent wrong answer. Everything around the `Verdict` stays deterministic.
43
43
 
44
44
  ```tsx
45
45
  import { domovoi, match } from "@hourslabs/domovoi";
46
46
 
47
- async function processTransaction(txn: Transaction) {
48
- const account = await accounts.get(txn.accountId);
49
- if (await fraud.isSuspicious(txn)) return holds.queue(txn);
47
+ async function processTransaction(transaction: Transaction): Promise<void> {
48
+ if (await fraud.isSuspicious(transaction)) return holds.queue(transaction);
50
49
 
50
+ const account = await accounts.get(transaction.accountId);
51
51
  const verdict = await domovoi.classify(
52
- txn.merchant, // e.g. "NETFLIX.COM"
53
- account.budget.categories // e.g. ["shopping", "groceries", ...]
52
+ transaction.merchant, // e.g. "NETFLIX.COM"
53
+ account.budget.categories, // e.g. ["shopping", "groceries", ...]
54
54
  );
55
55
 
56
56
  await match(verdict, {
57
- classified: ({ value }) => budget.attribute(account, txn, value),
58
- uncertain: ({ top, runnerUp }) =>
59
- budget.attributePending(account, txn, top, runnerUp),
60
- unknown: ({ reason }) =>
61
- retryQueue.schedule(txn, { reason, delayMs: 5 * 60_000 }),
57
+ classified: ({ value }) => budget.attribute(account, transaction, value),
58
+ uncertain: ({ top, runnerUp }) => budget.attributePending(account, transaction, top, runnerUp),
59
+ unknown: ({ reason }) => transactions.markUncategorized(transaction, reason),
62
60
  });
63
61
 
64
- await receipts.archive(txn);
65
- events.emit("txn.processed", txn.id);
62
+ await receipts.archive(transaction);
63
+ events.emit("transaction.processed", transaction.id);
66
64
  }
67
65
  ```
68
66
 
@@ -101,14 +99,73 @@ The `AMZN MKTP` row shows why the third state exists: it could be shopping or gr
101
99
 
102
100
  ---
103
101
 
104
- ## When to Use It
102
+ ## Where Heuristics Break Down
105
103
 
106
- The pattern that fits: *a decision that's obvious to a person but impossible to write rules for, with a bounded downside when you get it wrong.*
104
+ Use domovoi for decisions that are obvious to a human, hard to encode in rules, and safe to get wrong within bounds.
107
105
 
108
- - **Intent routing** — refund, complaint, or question. Rules and regex can't cover the full input space.
106
+ - **Intent routing** — refund, complaint, or question. Rule sets and regex won't cover the full input space.
109
107
  - **Content classification** — tag an article, ticket, or submission against your taxonomy. Replace brittle keyword rules with a classifier that handles edge cases.
110
- - **Tiered dispatch** — chain `[gpt-4o-mini, gpt-5]`. The cheap model handles the easy cases; the expensive one runs only on `Uncertain`. Cost savings are meaningful when 70–80% of calls resolve at the cheaper tier.
111
- - **Free-form validation + privacy filters** — does this description match the product? Does this profile bio violate guidelines? Does this user input contain PII or a prompt-injection attempt?
108
+ - **Tiered dispatch** — chain models (e.g., cheap → expensive). The cheaper model handles easy cases; the stronger model runs only on `Uncertain`. Costs drop meaningfully when ~70–80% resolve at the lower tier.
109
+ - **Free-form validation + privacy filters** — does this description match the product? Does this bio violate guidelines? Does this input contain PII or a prompt-injection attempt?
110
+
111
+ This same shape shows up across mainstream libraries: [Mozilla Readability](https://github.com/mozilla/readability) and [Mercury Parser](https://github.com/postlight/parser) for DOM-based article extraction, [GitHub Linguist](https://github.com/github-linguist/linguist) for language detection, and [email-reply-parser](https://github.com/crisp-oss/email-reply-parser/blob/master/lib/regex.ts) and [Talon](https://github.com/mailgun/talon) for email fragment parsing. Under the hood, they rely on dense stacks of regex and heuristics that grow in complexity without ever fully solving the problem.
112
+
113
+ Here's a paraphrased example from email-reply-parser:
114
+
115
+ <table>
116
+ <tr><th>Before</th><th>With domovoi</th></tr>
117
+ <tr>
118
+ <td valign="top">
119
+
120
+ ```ts
121
+ const QUOTE_HEADERS = [
122
+ /^-*\s*(On\s.+\swrote:{0,1})\s*-*$/m, // EN
123
+ /^-*\s*(Le\s.+\sécrit\s?:{0,1})\s*-*$/m, // FR
124
+ /^\s*(Am\s.+schrieb.+):$/m, // DE
125
+ /^(在[\s\S]+写道:)$/m, // ZH
126
+ /^(20[0-9]{2}\..+\s작성:)$/m, // KO
127
+ // ...25 more locale variants
128
+ ];
129
+
130
+ const SIGNATURE_SEPS = [
131
+ /^\s*-{2,4}$/, /^\s*_{2,4}$/, /^-- $/,
132
+ // ...18 patterns total
133
+ ];
134
+
135
+ function classify(line: string) {
136
+ if (QUOTE_HEADERS.some(r => r.test(line)))
137
+ return "quote";
138
+ if (SIGNATURE_SEPS.some(r => r.test(line)))
139
+ return "signature";
140
+ return "body";
141
+ }
142
+
143
+ // "-- pricing tier --" → signature.
144
+ // Body silently dropped.
145
+ ```
146
+
147
+ </td>
148
+ <td valign="top">
149
+
150
+ ```ts
151
+ const fragment = await domovoi.classify(
152
+ line,
153
+ ["quote", "signature", "body"],
154
+ );
155
+
156
+ await match(fragment, {
157
+ classified: ({ value }) =>
158
+ record(line, value),
159
+ uncertain: ({ top, runnerUp }) =>
160
+ record(line, top, { lowConfidence: runnerUp }),
161
+ unknown: () =>
162
+ record(line, "body"),
163
+ });
164
+ ```
165
+
166
+ </td>
167
+ </tr>
168
+ </table>
112
169
 
113
170
  ---
114
171
 
@@ -246,6 +303,192 @@ controller.abort("budget_exceeded");
246
303
 
247
304
  ---
248
305
 
306
+ ## Scopes
307
+
308
+ A `domovoi.classify` call deep in a request handler needs three things from its environment: a cost ceiling, a cancellation signal, and observability. Threading them through every layer of your stack as arguments is tedious. Scopes make them ambient.
309
+
310
+ ```ts
311
+ import { domovoi } from "@hourslabs/domovoi";
312
+
313
+ await domovoi.scope(
314
+ { budget: { tokens: 50_000 }, signal: req.signal, tracer },
315
+ async () => {
316
+ // Every classify inside this scope inherits the budget, the signal,
317
+ // and the tracer.
318
+ await processBatch(items);
319
+ },
320
+ );
321
+ ```
322
+
323
+ If `processBatch` calls helpers that classify, they share the same running budget. When the budget hits zero, the next classify returns `Unknown { reason: { type: "budget_exceeded", spent, limit } }` rather than spend more.
324
+
325
+ ### Predictable cost
326
+
327
+ The failure mode that makes finance teams nervous about LLM-backed code is the runaway loop — an infinite call quietly burning through a month of budget in an afternoon. Scope budgets are the circuit breaker:
328
+
329
+ ```ts
330
+ await domovoi.scope({ budget: { tokens: 10_000 } }, async () => {
331
+ for (const item of items) {
332
+ const v = await domovoi.classify(item.text, ["a", "b"]);
333
+ if (v.kind === "unknown" && v.reason.type === "budget_exceeded") break;
334
+ // ...
335
+ }
336
+ });
337
+ ```
338
+
339
+ Default mode is graceful: classify returns `Unknown` when the limit is hit. For hard-fail behavior, set `onExceeded: "throw"` — classify throws `BudgetExceededError` instead.
340
+
341
+ ### Scope-level cancellation
342
+
343
+ Scope signals combine with per-call signals via `AbortSignal.any`. Either firing aborts the in-flight provider call.
344
+
345
+ ```ts
346
+ const ac = new AbortController();
347
+ setTimeout(() => ac.abort("user navigated away"), 5_000);
348
+
349
+ await domovoi.scope({ signal: ac.signal }, async () => {
350
+ await domovoi.classify(input, space);
351
+ });
352
+ ```
353
+
354
+ ### Observability
355
+
356
+ Pass a `Tracer` and domovoi emits one span per provider call, following the [OpenTelemetry GenAI semantic conventions](https://opentelemetry.io/docs/specs/semconv/gen-ai/) for `gen_ai.*` fields and reserving `domovoi.*` for verdict-shaped concepts:
357
+
358
+ | Attribute | Carries |
359
+ |---|---|
360
+ | `gen_ai.provider.name` | `"openai"`, `"anthropic"`, etc. |
361
+ | `gen_ai.request.model` | requested model id |
362
+ | `gen_ai.usage.input_tokens` / `output_tokens` | per-call token counts |
363
+ | `domovoi.verdict.kind` | `"classified"` / `"uncertain"` / `"unknown"` |
364
+ | `domovoi.verdict.value` | selected label when classified |
365
+ | `domovoi.cache.hit` | whether the call was served from cache |
366
+
367
+ A short adapter wires your existing OpenTelemetry tracer in:
368
+
369
+ ```ts
370
+ import { trace } from "@opentelemetry/api";
371
+ import type { Tracer } from "@hourslabs/domovoi";
372
+
373
+ const otel = trace.getTracer("my-app");
374
+ const tracer: Tracer = {
375
+ startSpan: (name, attrs) => otel.startSpan(name, { attributes: attrs }),
376
+ };
377
+ ```
378
+
379
+ Datadog, Honeycomb, Grafana Cloud, and Dynatrace populate their AI Observability views from `gen_ai.*` attributes automatically.
380
+
381
+ ### Resolution order
382
+
383
+ Each `domovoi.classify` call resolves its budget, signal, and tracer in this order:
384
+
385
+ 1. Per-call option, e.g. `domovoi.classify(..., { signal })`
386
+ 2. Nearest enclosing `domovoi.scope`
387
+ 3. No enforcement, no tracing, no budget
388
+
389
+ `AbortSignal` is the one exception — per-call and scope signals combine, rather than the per-call value overriding the scope.
390
+
391
+ Nested scopes inherit unspecified fields from the parent. A child `budget` overrides the parent and starts a fresh counter. A child `tracer` overrides. A child `signal` combines.
392
+
393
+ ### Bind: scope across async boundaries
394
+
395
+ Queue workers, cron jobs, and `setTimeout` callbacks run *outside* the original async context. `domovoi.bind` captures the current scope and re-applies it on later invocation:
396
+
397
+ ```ts
398
+ await domovoi.scope({ budget: { tokens: 50_000 }, tracer }, async () => {
399
+ const job = domovoi.bind(async (item: Item) => {
400
+ return domovoi.classify(item.text, ["a", "b"]);
401
+ });
402
+
403
+ // Queue runs `job` later, in a different async context. The captured
404
+ // scope is re-applied: budget and tracer still flow through.
405
+ await queue.push(job, items);
406
+ });
407
+ ```
408
+
409
+ Mirrors Node's `AsyncLocalStorage.bind` and OpenTelemetry's `context.bind`. Outside any scope, `domovoi.bind(fn)` returns `fn` unchanged.
410
+
411
+ ### Backward compatibility
412
+
413
+ Calls outside any scope are unchanged: no enforcement, no tracing, no budget. Existing code keeps working without changes.
414
+
415
+ ---
416
+
417
+ ## Testing
418
+
419
+ Two primitives in `@hourslabs/domovoi/testing` cover the testing surface:
420
+
421
+ - `mockProvider({ behavior })` — a `Provider` stub for unit-testing engine logic without hitting a real LLM.
422
+ - `distribution(fn, { n })` — runs `n` real samples and returns Wilson-CI-backed assertions about behavior stability.
423
+
424
+ ### mockProvider — unit tests without an LLM
425
+
426
+ Most engine and threshold logic doesn't need a real model. `mockProvider` lets you supply a deterministic Distribution per call:
427
+
428
+ ```ts
429
+ import { mockProvider } from "@hourslabs/domovoi/testing";
430
+
431
+ const stub = mockProvider({
432
+ behavior: () => ({ probs: { yes: 0.92, no: 0.08 }, coverage: 0.95 }),
433
+ });
434
+
435
+ const c = domovoi.classifier({
436
+ space: ["yes", "no"] as const,
437
+ thresholds: { high: 0.7, low: 0.3 },
438
+ providers: [stub],
439
+ });
440
+
441
+ const v = await c("input that the mock ignores");
442
+ // v.kind === "classified", v.value === "yes" — deterministic, no network
443
+ ```
444
+
445
+ Useful for threshold logic, provider-chain fallback, calibrator math, error-handling paths — anything that's about *engine* behavior rather than *model* behavior. Zero LLM calls; runs anywhere.
446
+
447
+ ### distribution — assert against AI behavior
448
+
449
+ Single-sample assertions on AI behavior are meaningless: the model varies between runs. `distribution()` runs `n` real samples and turns "the classifier should reliably tag greetings" into a one-liner backed by a Wilson confidence interval:
450
+
451
+ ```ts
452
+ import { distribution } from "@hourslabs/domovoi/testing";
453
+
454
+ const dist = await distribution(
455
+ () => domovoi.classify("hello there", ["greeting", "request"] as const),
456
+ { n: 100 },
457
+ );
458
+
459
+ dist.coverage("greeting"); // 0.94
460
+ dist.confidenceInterval("greeting"); // [0.88, 0.98] — 95% Wilson CI
461
+ dist.modeKind(); // "classified" | "uncertain" | "unknown"
462
+
463
+ dist.expectStable({
464
+ minCoverage: 0.9, // OR per-label: { greeting: 0.9, request: 0.5 }
465
+ maxUncertain: 0.05,
466
+ maxUnknown: 0.02,
467
+ });
468
+ ```
469
+
470
+ Default concurrency is `Math.min(n, 5)` — `n=100` finishes in ~6 seconds against a 300ms p50 provider, well under typical rate limits. Pass `concurrency: 1` to serialize when running multiple `distribution()` tests in parallel.
471
+
472
+ `distribution()` makes `n` real LLM calls. At gpt-4o-mini and `n=100`, that's ~$0.005 per test — belongs in `test:e2e`, not the per-commit unit tier.
473
+
474
+ ---
475
+
476
+ ## Cost
477
+
478
+ A `domovoi.classify` call on gpt-4o-mini costs about $0.00004 — roughly 1/25 of a cent. ~180 input tokens (system prompt + label space + your input) at $0.15/M, plus ~15 output tokens at $0.60/M.
479
+
480
+ | Calls / month | Cost |
481
+ |---------------|---------|
482
+ | 100k | $4 |
483
+ | 10M | $400 |
484
+ | 1B | $40,000 |
485
+
486
+ The default `memoryCache` deduplicates byte-exact-match inputs within a process — significant savings on workloads with repeated inputs (log severity tags, predefined enums, boilerplate replies), little impact on free-form user content where every input is unique. The `Cache` extension point lets you back domovoi with any store for cross-process or persistent caching.
487
+
488
+ Reach for deterministic tools instead when: syntactic problems with stable rules (URL parsing, format validation, tokenizers), very high volume with thin margins (100B+ ad impressions / day), or hard-real-time loops where a cache miss (~300ms p50) blows the SLA.
489
+
490
+ ---
491
+
249
492
  ## Calibration
250
493
 
251
494
  Three closed-form scaling factories from `@hourslabs/domovoi/calibration`:
@@ -0,0 +1,72 @@
1
+ /**
2
+ * Running token-budget counter held inside a `ResolvedScope`. Shared across
3
+ * nested `domovoi.scope({...})` calls that inherit the parent's budget,
4
+ * which is why this is a class rather than a closure: the "shared mutable
5
+ * counter" model needs to be explicit.
6
+ *
7
+ * Two-phase enforcement:
8
+ * - `precheck()` before each provider call — short-circuits if already
9
+ * exhausted, so a runaway loop stops on its next iteration.
10
+ * - `charge(tokens)` after each provider call — accumulates real spend.
11
+ *
12
+ * Default mode is `"graceful"`: when exhausted, the engine returns
13
+ * `Unknown { reason: { type: "budget_exceeded", spent, limit } }` instead
14
+ * of throwing. Users who want hard-fail semantics opt into `"throw"` mode.
15
+ *
16
+ * Honest contract: `precheck` measures "have we already exceeded?", not
17
+ * "would this call exceed?" — a single in-flight call can overshoot the
18
+ * limit by one call's worth of tokens. Tighter pre-charge enforcement
19
+ * (estimate via tokenizer before call) is possible but defer; document
20
+ * the overshoot in the README so users know.
21
+ */
22
+ export type BudgetMode = "graceful" | "throw";
23
+ export type ScopeBudget = {
24
+ readonly tokens?: number;
25
+ readonly onExceeded?: BudgetMode;
26
+ };
27
+ export type BudgetSnapshot = {
28
+ readonly spent: number;
29
+ readonly limit: number;
30
+ readonly mode: BudgetMode;
31
+ };
32
+ type PrecheckResult = {
33
+ readonly ok: true;
34
+ } | {
35
+ readonly ok: false;
36
+ readonly spent: number;
37
+ readonly limit: number;
38
+ };
39
+ export declare class BudgetTracker {
40
+ private readonly limit;
41
+ readonly mode: BudgetMode;
42
+ private spent;
43
+ constructor(limit: number, mode: BudgetMode);
44
+ /**
45
+ * Build a tracker from a public `ScopeBudget`, or return `undefined` if
46
+ * the budget is absent / has no `tokens` field. Used during scope merge.
47
+ *
48
+ * Throws `ConfigError` if `tokens` is non-finite or non-positive — these
49
+ * are construction-time validation failures, not runtime budget exhaustion.
50
+ */
51
+ static from(budget: ScopeBudget | undefined): BudgetTracker | undefined;
52
+ /** Check whether the budget is already exhausted. Called pre-call. */
53
+ precheck(): PrecheckResult;
54
+ /**
55
+ * Accumulate spend. Called post-call with the actual token cost.
56
+ * Negative or non-finite values are silently clamped to 0 — provider
57
+ * adapters occasionally return malformed token counts, and a budget
58
+ * tracker shouldn't panic on bad telemetry from upstream.
59
+ */
60
+ charge(tokens: number): void;
61
+ /**
62
+ * Throw `BudgetExceededError` if mode is `"throw"` and budget is exhausted.
63
+ * Caller invokes after `charge()` to enforce hard-fail semantics; under
64
+ * `"graceful"` mode this is a no-op and the next `precheck()` returns
65
+ * `{ ok: false, ... }` for graceful Unknown construction.
66
+ */
67
+ enforce(): void;
68
+ snapshot(): BudgetSnapshot;
69
+ private isExhausted;
70
+ }
71
+ export {};
72
+ //# sourceMappingURL=budget-tracker.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"budget-tracker.d.ts","sourceRoot":"","sources":["../src/budget-tracker.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;GAoBG;AAIH,MAAM,MAAM,UAAU,GAAG,UAAU,GAAG,OAAO,CAAC;AAE9C,MAAM,MAAM,WAAW,GAAG;IACxB,QAAQ,CAAC,MAAM,CAAC,EAAE,MAAM,CAAC;IACzB,QAAQ,CAAC,UAAU,CAAC,EAAE,UAAU,CAAC;CAClC,CAAC;AAEF,MAAM,MAAM,cAAc,GAAG;IAC3B,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;IACvB,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;IACvB,QAAQ,CAAC,IAAI,EAAE,UAAU,CAAC;CAC3B,CAAC;AAEF,KAAK,cAAc,GACf;IAAE,QAAQ,CAAC,EAAE,EAAE,IAAI,CAAA;CAAE,GACrB;IAAE,QAAQ,CAAC,EAAE,EAAE,KAAK,CAAC;IAAC,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;IAAC,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAA;CAAE,CAAC;AAE3E,qBAAa,aAAa;IAItB,OAAO,CAAC,QAAQ,CAAC,KAAK;aACN,IAAI,EAAE,UAAU;IAJlC,OAAO,CAAC,KAAK,CAAK;gBAGC,KAAK,EAAE,MAAM,EACd,IAAI,EAAE,UAAU;IAGlC;;;;;;OAMG;IACH,MAAM,CAAC,IAAI,CAAC,MAAM,EAAE,WAAW,GAAG,SAAS,GAAG,aAAa,GAAG,SAAS;IAWvE,sEAAsE;IACtE,QAAQ,IAAI,cAAc;IAO1B;;;;;OAKG;IACH,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,IAAI;IAK5B;;;;;OAKG;IACH,OAAO,IAAI,IAAI;IAMf,QAAQ,IAAI,cAAc;IAI1B,OAAO,CAAC,WAAW;CAGpB"}
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/errors.ts","../../src/calibration/index.ts"],"names":[],"mappings":"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","file":"index.js","sourcesContent":["/**\n * Error taxonomy for domovoi.\n *\n * Four classes (`DomovoiError` base + `ProviderError`, `ConfigError`,\n * `BudgetExhaustedError`) with a stable `code` field for fine-grained\n * discrimination. All accept `{ cause }` for ES2022 chaining.\n *\n * The engine canonicalizes anything thrown from `Provider.sample` that isn't\n * already a `DomovoiError` into `ProviderError({ cause })`, so callers always\n * see a known error type. Under the default `onErrorPolicy: \"fallback\"`,\n * runtime errors become `Unknown` Verdict variants rather than throw;\n * `ConfigError` always throws.\n */\n\n/**\n * Stable error codes carried in `error.code`. Discriminate on these rather\n * than on `instanceof` of removed-historical subclasses.\n */\nexport type ErrorCode =\n // ConfigError — construction-time\n | \"decision_space_collision\"\n | \"decision_space_too_large\"\n | \"missing_provider_config\"\n | \"malformed_provider_config\"\n | \"unknown_provider_factory\"\n | \"missing_credential\"\n | \"incompatible_calibrator\"\n | \"invalid_classifier_name\"\n | \"invalid_thresholds\"\n | \"invalid_space\"\n | \"empty_providers\"\n // ProviderError — runtime\n | \"provider_network\"\n | \"provider_rate_limit\"\n | \"provider_timeout\"\n | \"provider_unauthorized\"\n | \"provider_server_error\"\n | \"provider_malformed_response\"\n | \"invalid_distribution\"\n // BudgetExhaustedError — runtime\n | \"per_call_timeout\"\n | \"chain_timeout\"\n | \"max_calls\";\n\nexport class DomovoiError extends Error {\n readonly code: string;\n\n constructor(message: string, options?: { code?: string; cause?: unknown }) {\n super(message, options?.cause !== undefined ? { cause: options.cause } : undefined);\n this.name = \"DomovoiError\";\n this.code = options?.code ?? \"unspecified\";\n }\n}\n\nexport class ProviderError extends DomovoiError {\n constructor(message: string, options?: { code?: string; cause?: unknown }) {\n super(message, options);\n this.name = \"ProviderError\";\n }\n}\n\nexport class ConfigError extends DomovoiError {\n constructor(message: string, options?: { code?: string; cause?: unknown }) {\n super(message, options);\n this.name = \"ConfigError\";\n }\n}\n\nexport class BudgetExhaustedError extends DomovoiError {\n readonly attemptedProviders: readonly string[];\n readonly elapsedMs: number;\n readonly scope: \"per_call_timeout\" | \"chain_timeout\" | \"max_calls\";\n\n constructor(\n message: string,\n options: {\n scope: \"per_call_timeout\" | \"chain_timeout\" | \"max_calls\";\n attemptedProviders: readonly string[];\n elapsedMs: number;\n cause?: unknown;\n },\n ) {\n super(message, { code: options.scope, cause: options.cause });\n this.name = \"BudgetExhaustedError\";\n this.scope = options.scope;\n this.attemptedProviders = options.attemptedProviders;\n this.elapsedMs = options.elapsedMs;\n }\n}\n\n/**\n * Wraps any non-DomovoiError thrown value in `ProviderError({ cause })`.\n * `DomovoiError` subtypes — including `ProviderError` subclasses defined by\n * external Provider implementations — pass through unchanged.\n */\nexport function canonicalizeProviderThrow(thrown: unknown): DomovoiError {\n if (thrown instanceof DomovoiError) return thrown;\n if (thrown instanceof Error) {\n return new ProviderError(thrown.message || \"Provider call failed\", {\n code: \"provider_network\",\n cause: thrown,\n });\n }\n return new ProviderError(String(thrown), {\n code: \"provider_network\",\n cause: thrown,\n });\n}\n\n/**\n * Convert an Error to the JSON-safe shape stored in\n * `Verdict.meta.providerErrors`. Cause chains are preserved recursively.\n */\nexport function serializeError(err: unknown): {\n readonly name: string;\n readonly message: string;\n readonly code?: string;\n readonly cause?: ReturnType<typeof serializeError>;\n readonly stack?: string;\n} {\n if (!(err instanceof Error)) {\n return { name: \"Error\", message: String(err) };\n }\n const code = err instanceof DomovoiError ? err.code : undefined;\n const cause = err.cause !== undefined ? serializeError(err.cause) : undefined;\n return {\n name: err.name,\n message: err.message,\n ...(code !== undefined ? { code } : {}),\n ...(cause !== undefined ? { cause } : {}),\n ...(err.stack !== undefined ? { stack: err.stack } : {}),\n };\n}\n","/**\n * Built-in calibrator factories.\n *\n * - `identity` — pass-through; the default.\n * - `temperatureScaling(T)` — `p^(1/T) / Σ p_j^(1/T)`; equivalent to\n * scaling logits before softmax.\n * - `plattScaling({ a, b })` — sigmoid scaling; binary spaces only.\n *\n * `Calibrator.apply` must be pure and stateless. The engine runs it\n * per-caller after the Distribution is loaded from the cache, so impurity\n * would leak across callers sharing a cache row.\n */\n\nimport { ConfigError } from \"../errors.js\";\nimport type { Distribution } from \"../types.js\";\n\nexport interface Calibrator {\n /** Identifier; `\"identity\"` is reserved and recognized by the engine. */\n readonly kind: string;\n apply<T extends string>(d: Distribution<T>): Distribution<T>;\n}\n\nexport const identity: Calibrator = {\n kind: \"identity\",\n apply<T extends string>(d: Distribution<T>): Distribution<T> {\n return d;\n },\n};\n\n/**\n * - `T = 1` → identity.\n * - `T > 1` → softens (less peaked).\n * - `T < 1` → sharpens (more peaked).\n *\n * Operates on probabilities (not logits) and leaves `coverage` unchanged.\n * Throws `ConfigError` on `T <= 0`.\n *\n * @example\n * calibrator: temperatureScaling(0.85) // user-fit on held-out eval set\n */\nexport function temperatureScaling(T: number): Calibrator {\n if (Number.isNaN(T) || T <= 0) {\n throw new ConfigError(`temperatureScaling(T): T must be > 0; got ${T}.`, {\n code: \"incompatible_calibrator\",\n });\n }\n const inverseT = 1 / T;\n return {\n kind: \"temperature\",\n apply<U extends string>(d: Distribution<U>): Distribution<U> {\n const scaledEntries = (Object.entries(d.probs) as [string, number][]).map(\n ([label, prob]) => [label, prob === 0 ? 0 : prob ** inverseT] as const,\n );\n const partitionFn = scaledEntries.reduce(\n (running, [, scaledProb]) => running + scaledProb,\n 0,\n );\n // Degenerate input (every prob is 0) — return as-is rather than divide by 0.\n if (partitionFn === 0) return d;\n const normalizedProbs = Object.fromEntries(\n scaledEntries.map(([label, scaledProb]) => [label, scaledProb / partitionFn] as const),\n );\n return {\n probs: normalizedProbs as Distribution<U>[\"probs\"],\n coverage: d.coverage,\n };\n },\n };\n}\n\n/**\n * Platt scaling: `sigmoid(a*z + b)` where `z = logit(p_pos)`.\n *\n * Binary-only — the second label in the distribution (in user-given order)\n * is treated as the positive class. Construction-time space-size validation\n * happens in the classifier; `apply()` re-checks at runtime as a defensive\n * guard against direct misuse.\n */\nexport function plattScaling(params: { a: number; b: number }): Calibrator {\n if (\n Number.isNaN(params.a) ||\n Number.isNaN(params.b) ||\n !Number.isFinite(params.a) ||\n !Number.isFinite(params.b)\n ) {\n throw new ConfigError(\n `plattScaling({ a, b }): a and b must be finite numbers; got a=${params.a}, b=${params.b}.`,\n { code: \"incompatible_calibrator\" },\n );\n }\n return {\n kind: \"platt\",\n apply<U extends string>(d: Distribution<U>): Distribution<U> {\n const labels = Object.keys(d.probs);\n if (labels.length !== 2) {\n throw new ConfigError(\n `plattScaling is binary-only; got distribution with ${labels.length} labels.`,\n { code: \"incompatible_calibrator\" },\n );\n }\n const [neg, pos] = labels as [string, string];\n const probs = d.probs as Record<string, number>;\n const pPos = probs[pos] ?? 0;\n // Clamp away from {0, 1} so `Math.log(p / (1 - p))` is finite.\n const eps = 1e-9;\n const clamped = Math.min(1 - eps, Math.max(eps, pPos));\n const logit = Math.log(clamped / (1 - clamped));\n const calibratedPos = sigmoid(params.a * logit + params.b);\n return {\n probs: {\n [neg]: 1 - calibratedPos,\n [pos]: calibratedPos,\n } as Distribution<U>[\"probs\"],\n coverage: d.coverage,\n };\n },\n };\n}\n\nfunction sigmoid(x: number): number {\n return 1 / (1 + Math.exp(-x));\n}\n\nexport function isIdentityCalibrator(c: Calibrator): boolean {\n return c.kind === \"identity\";\n}\n"]}
1
+ {"version":3,"sources":["../../src/errors.ts","../../src/calibration/index.ts"],"names":[],"mappings":"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","file":"index.js","sourcesContent":["/**\n * Error taxonomy for domovoi.\n *\n * Four classes (`DomovoiError` base + `ProviderError`, `ConfigError`,\n * `BudgetExhaustedError`) with a stable `code` field for fine-grained\n * discrimination. All accept `{ cause }` for ES2022 chaining.\n *\n * The engine canonicalizes anything thrown from `Provider.sample` that isn't\n * already a `DomovoiError` into `ProviderError({ cause })`, so callers always\n * see a known error type. Under the default `onErrorPolicy: \"fallback\"`,\n * runtime errors become `Unknown` Verdict variants rather than throw;\n * `ConfigError` always throws.\n */\n\n/**\n * Stable error codes carried in `error.code`. Discriminate on these rather\n * than on `instanceof` of removed-historical subclasses.\n */\nexport type ErrorCode =\n // ConfigError — construction-time\n | \"decision_space_collision\"\n | \"decision_space_too_large\"\n | \"missing_provider_config\"\n | \"malformed_provider_config\"\n | \"unknown_provider_factory\"\n | \"missing_credential\"\n | \"incompatible_calibrator\"\n | \"invalid_classifier_name\"\n | \"invalid_thresholds\"\n | \"invalid_space\"\n | \"empty_providers\"\n // ProviderError — runtime\n | \"provider_network\"\n | \"provider_rate_limit\"\n | \"provider_timeout\"\n | \"provider_unauthorized\"\n | \"provider_server_error\"\n | \"provider_malformed_response\"\n | \"invalid_distribution\"\n // BudgetExhaustedError — runtime (operational: time / call-count)\n | \"per_call_timeout\"\n | \"chain_timeout\"\n | \"max_calls\"\n // BudgetExceededError — runtime (scope token budget)\n | \"tokens_exceeded\"\n // ConfigError — scope budget validation\n | \"invalid_scope_budget\";\n\nexport class DomovoiError extends Error {\n readonly code: string;\n\n constructor(message: string, options?: { code?: string; cause?: unknown }) {\n super(message, options?.cause !== undefined ? { cause: options.cause } : undefined);\n this.name = \"DomovoiError\";\n this.code = options?.code ?? \"unspecified\";\n }\n}\n\nexport class ProviderError extends DomovoiError {\n constructor(message: string, options?: { code?: string; cause?: unknown }) {\n super(message, options);\n this.name = \"ProviderError\";\n }\n}\n\nexport class ConfigError extends DomovoiError {\n constructor(message: string, options?: { code?: string; cause?: unknown }) {\n super(message, options);\n this.name = \"ConfigError\";\n }\n}\n\nexport class BudgetExhaustedError extends DomovoiError {\n readonly attemptedProviders: readonly string[];\n readonly elapsedMs: number;\n readonly scope: \"per_call_timeout\" | \"chain_timeout\" | \"max_calls\";\n\n constructor(\n message: string,\n options: {\n scope: \"per_call_timeout\" | \"chain_timeout\" | \"max_calls\";\n attemptedProviders: readonly string[];\n elapsedMs: number;\n cause?: unknown;\n },\n ) {\n super(message, { code: options.scope, cause: options.cause });\n this.name = \"BudgetExhaustedError\";\n this.scope = options.scope;\n this.attemptedProviders = options.attemptedProviders;\n this.elapsedMs = options.elapsedMs;\n }\n}\n\n/**\n * Thrown when scope token budget is exceeded under `onExceeded: \"throw\"` mode.\n * Distinct from `BudgetExhaustedError` (operational time / call-count budgets):\n * this is the cost ceiling for a `domovoi.scope({ budget: { tokens } })` block.\n *\n * Default mode is `\"graceful\"` — classify returns\n * `Unknown { reason: { type: \"budget_exceeded\", spent, limit } }` instead.\n */\nexport class BudgetExceededError extends DomovoiError {\n readonly spent: number;\n readonly limit: number;\n\n constructor(options: { spent: number; limit: number; cause?: unknown }) {\n super(`Scope budget exceeded: ${options.spent} / ${options.limit} tokens`, {\n code: \"tokens_exceeded\",\n cause: options.cause,\n });\n this.name = \"BudgetExceededError\";\n this.spent = options.spent;\n this.limit = options.limit;\n }\n}\n\n/**\n * Wraps any non-DomovoiError thrown value in `ProviderError({ cause })`.\n * `DomovoiError` subtypes — including `ProviderError` subclasses defined by\n * external Provider implementations — pass through unchanged.\n */\nexport function canonicalizeProviderThrow(thrown: unknown): DomovoiError {\n if (thrown instanceof DomovoiError) return thrown;\n if (thrown instanceof Error) {\n return new ProviderError(thrown.message || \"Provider call failed\", {\n code: \"provider_network\",\n cause: thrown,\n });\n }\n return new ProviderError(String(thrown), {\n code: \"provider_network\",\n cause: thrown,\n });\n}\n\n/**\n * Convert an Error to the JSON-safe shape stored in\n * `Verdict.meta.providerErrors`. Cause chains are preserved recursively.\n */\nexport function serializeError(err: unknown): {\n readonly name: string;\n readonly message: string;\n readonly code?: string;\n readonly cause?: ReturnType<typeof serializeError>;\n readonly stack?: string;\n} {\n if (!(err instanceof Error)) {\n return { name: \"Error\", message: String(err) };\n }\n const code = err instanceof DomovoiError ? err.code : undefined;\n const cause = err.cause !== undefined ? serializeError(err.cause) : undefined;\n return {\n name: err.name,\n message: err.message,\n ...(code !== undefined ? { code } : {}),\n ...(cause !== undefined ? { cause } : {}),\n ...(err.stack !== undefined ? { stack: err.stack } : {}),\n };\n}\n","/**\n * Built-in calibrator factories.\n *\n * - `identity` — pass-through; the default.\n * - `temperatureScaling(T)` — `p^(1/T) / Σ p_j^(1/T)`; equivalent to\n * scaling logits before softmax.\n * - `plattScaling({ a, b })` — sigmoid scaling; binary spaces only.\n *\n * `Calibrator.apply` must be pure and stateless. The engine runs it\n * per-caller after the Distribution is loaded from the cache, so impurity\n * would leak across callers sharing a cache row.\n */\n\nimport { ConfigError } from \"../errors.js\";\nimport type { Distribution } from \"../types.js\";\n\nexport interface Calibrator {\n /** Identifier; `\"identity\"` is reserved and recognized by the engine. */\n readonly kind: string;\n apply<T extends string>(d: Distribution<T>): Distribution<T>;\n}\n\nexport const identity: Calibrator = {\n kind: \"identity\",\n apply<T extends string>(d: Distribution<T>): Distribution<T> {\n return d;\n },\n};\n\n/**\n * - `T = 1` → identity.\n * - `T > 1` → softens (less peaked).\n * - `T < 1` → sharpens (more peaked).\n *\n * Operates on probabilities (not logits) and leaves `coverage` unchanged.\n * Throws `ConfigError` on `T <= 0`.\n *\n * @example\n * calibrator: temperatureScaling(0.85) // user-fit on held-out eval set\n */\nexport function temperatureScaling(T: number): Calibrator {\n if (Number.isNaN(T) || T <= 0) {\n throw new ConfigError(`temperatureScaling(T): T must be > 0; got ${T}.`, {\n code: \"incompatible_calibrator\",\n });\n }\n const inverseT = 1 / T;\n return {\n kind: \"temperature\",\n apply<U extends string>(d: Distribution<U>): Distribution<U> {\n const scaledEntries = (Object.entries(d.probs) as [string, number][]).map(\n ([label, prob]) => [label, prob === 0 ? 0 : prob ** inverseT] as const,\n );\n const partitionFn = scaledEntries.reduce(\n (running, [, scaledProb]) => running + scaledProb,\n 0,\n );\n // Degenerate input (every prob is 0) — return as-is rather than divide by 0.\n if (partitionFn === 0) return d;\n const normalizedProbs = Object.fromEntries(\n scaledEntries.map(([label, scaledProb]) => [label, scaledProb / partitionFn] as const),\n );\n return {\n probs: normalizedProbs as Distribution<U>[\"probs\"],\n coverage: d.coverage,\n };\n },\n };\n}\n\n/**\n * Platt scaling: `sigmoid(a*z + b)` where `z = logit(p_pos)`.\n *\n * Binary-only — the second label in the distribution (in user-given order)\n * is treated as the positive class. Construction-time space-size validation\n * happens in the classifier; `apply()` re-checks at runtime as a defensive\n * guard against direct misuse.\n */\nexport function plattScaling(params: { a: number; b: number }): Calibrator {\n if (\n Number.isNaN(params.a) ||\n Number.isNaN(params.b) ||\n !Number.isFinite(params.a) ||\n !Number.isFinite(params.b)\n ) {\n throw new ConfigError(\n `plattScaling({ a, b }): a and b must be finite numbers; got a=${params.a}, b=${params.b}.`,\n { code: \"incompatible_calibrator\" },\n );\n }\n return {\n kind: \"platt\",\n apply<U extends string>(d: Distribution<U>): Distribution<U> {\n const labels = Object.keys(d.probs);\n if (labels.length !== 2) {\n throw new ConfigError(\n `plattScaling is binary-only; got distribution with ${labels.length} labels.`,\n { code: \"incompatible_calibrator\" },\n );\n }\n const [neg, pos] = labels as [string, string];\n const probs = d.probs as Record<string, number>;\n const pPos = probs[pos] ?? 0;\n // Clamp away from {0, 1} so `Math.log(p / (1 - p))` is finite.\n const eps = 1e-9;\n const clamped = Math.min(1 - eps, Math.max(eps, pPos));\n const logit = Math.log(clamped / (1 - clamped));\n const calibratedPos = sigmoid(params.a * logit + params.b);\n return {\n probs: {\n [neg]: 1 - calibratedPos,\n [pos]: calibratedPos,\n } as Distribution<U>[\"probs\"],\n coverage: d.coverage,\n };\n },\n };\n}\n\nfunction sigmoid(x: number): number {\n return 1 / (1 + Math.exp(-x));\n}\n\nexport function isIdentityCalibrator(c: Calibrator): boolean {\n return c.kind === \"identity\";\n}\n"]}
@@ -0,0 +1,44 @@
1
+ /**
2
+ * `ContextStorage<T>` — pluggable extension point for `domovoi.scope`'s
3
+ * ambient state. Default implementation wraps Node's
4
+ * `node:async_hooks.AsyncLocalStorage`, which works on Node 16+,
5
+ * Cloudflare Workers (with the `nodejs_compat` flag, since 2024), and
6
+ * Deno (native).
7
+ *
8
+ * Browser users or exotic runtimes call `configureContextStorage(custom)`
9
+ * at boot to swap the default. The interface mirrors `AsyncLocalStorage`
10
+ * minus methods domovoi doesn't need (`enterWith`, `disable`, etc.).
11
+ *
12
+ * `getContextStorage()` returns the *currently configured* storage —
13
+ * tests mutate it via `configureContextStorage` and reset between cases.
14
+ *
15
+ * Resolution semantics: `run` shadows nested scopes fully. There is no
16
+ * implicit merge — the engine's `mergeScopes()` (in `scope.ts`) builds the
17
+ * `ResolvedScope` value that `run` stores, so nested inheritance behavior
18
+ * lives in scope-merge logic, not in the context storage layer.
19
+ */
20
+ import type { ResolvedScope } from "./scope.js";
21
+ export interface ContextStorage<T> {
22
+ /** Run `fn` with `value` as the active store. Restored on return/throw. */
23
+ run<R>(value: T, fn: () => R | Promise<R>): R | Promise<R>;
24
+ /** Read the active store, or `undefined` if no enclosing `run()`. */
25
+ getStore(): T | undefined;
26
+ }
27
+ export declare function getContextStorage(): ContextStorage<ResolvedScope>;
28
+ /**
29
+ * Replace the default `AsyncLocalStorage`-backed storage with a custom
30
+ * implementation. Call once at boot, before any `domovoi.scope(...)` runs.
31
+ *
32
+ * Use cases:
33
+ * - browser runtimes without `node:async_hooks`
34
+ * - test harnesses that need deterministic single-tenant state
35
+ * - custom propagation layers (cross-process, queue handlers with their
36
+ * own context system)
37
+ */
38
+ export declare function configureContextStorage(custom: ContextStorage<ResolvedScope>): void;
39
+ /**
40
+ * Reset to the default `AsyncLocalStorage`-backed storage. Test-only
41
+ * affordance — production code should not need this.
42
+ */
43
+ export declare function resetContextStorage(): void;
44
+ //# sourceMappingURL=context-storage.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"context-storage.d.ts","sourceRoot":"","sources":["../src/context-storage.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;GAkBG;AAGH,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,YAAY,CAAC;AAEhD,MAAM,WAAW,cAAc,CAAC,CAAC;IAC/B,2EAA2E;IAC3E,GAAG,CAAC,CAAC,EAAE,KAAK,EAAE,CAAC,EAAE,EAAE,EAAE,MAAM,CAAC,GAAG,OAAO,CAAC,CAAC,CAAC,GAAG,CAAC,GAAG,OAAO,CAAC,CAAC,CAAC,CAAC;IAC3D,qEAAqE;IACrE,QAAQ,IAAI,CAAC,GAAG,SAAS,CAAC;CAC3B;AAID,wBAAgB,iBAAiB,IAAI,cAAc,CAAC,aAAa,CAAC,CAEjE;AAED;;;;;;;;;GASG;AACH,wBAAgB,uBAAuB,CAAC,MAAM,EAAE,cAAc,CAAC,aAAa,CAAC,GAAG,IAAI,CAEnF;AAED;;;GAGG;AACH,wBAAgB,mBAAmB,IAAI,IAAI,CAE1C"}
@@ -11,6 +11,15 @@
11
11
  * Errors during the chain become `Unknown` Verdicts under the default
12
12
  * `onErrorPolicy: "fallback"`; under `"throw"` they propagate as
13
13
  * `BudgetExhaustedError` / `AbortError` / `AggregateError`.
14
+ *
15
+ * v0.2 ambient context (via `domovoi.scope`):
16
+ * - reads `currentScope()` at entry
17
+ * - merges scope signal into per-call merged signal
18
+ * - pre-checks scope budget before each provider attempt; returns
19
+ * `Unknown { budget_exceeded }` (graceful) or throws
20
+ * `BudgetExceededError` (mode: "throw") on exhaustion
21
+ * - charges scope budget after each provider call (post-call estimate)
22
+ * - emits one OTel-shaped span per provider attempt via `scope.tracer`
14
23
  */
15
24
  import type { Verdict } from "../types.js";
16
25
  import { type DecideConfig } from "./config.js";
@@ -1 +1 @@
1
- {"version":3,"file":"decide.d.ts","sourceRoot":"","sources":["../../src/engine/decide.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;GAaG;AAGH,OAAO,KAAK,EAAgD,OAAO,EAAE,MAAM,aAAa,CAAC;AAGzF,OAAO,EAGL,KAAK,YAAY,EAClB,MAAM,aAAa,CAAC;AAYrB,wBAAsB,MAAM,CAAC,CAAC,SAAS,MAAM,EAC3C,cAAc,EAAE,MAAM,EACtB,MAAM,EAAE,YAAY,CAAC,CAAC,CAAC,EACvB,MAAM,CAAC,EAAE,WAAW,GACnB,OAAO,CAAC,OAAO,CAAC,CAAC,CAAC,CAAC,CA8DrB"}
1
+ {"version":3,"file":"decide.d.ts","sourceRoot":"","sources":["../../src/engine/decide.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;GAsBG;AAMH,OAAO,KAAK,EAAgD,OAAO,EAAE,MAAM,aAAa,CAAC;AAGzF,OAAO,EAGL,KAAK,YAAY,EAClB,MAAM,aAAa,CAAC;AAiBrB,wBAAsB,MAAM,CAAC,CAAC,SAAS,MAAM,EAC3C,cAAc,EAAE,MAAM,EACtB,MAAM,EAAE,YAAY,CAAC,CAAC,CAAC,EACvB,MAAM,CAAC,EAAE,WAAW,GACnB,OAAO,CAAC,OAAO,CAAC,CAAC,CAAC,CAAC,CAgFrB"}
@@ -8,7 +8,7 @@ import type { Distribution } from "../types.js";
8
8
  import { type DecideConfig } from "./config.js";
9
9
  import type { MetaBuilder } from "./meta.js";
10
10
  export declare function computeProviderCacheKey<T extends string>(provider: Provider, formattedInput: string, config: DecideConfig<T>): string;
11
- export declare function mergeSignals(userSignal: AbortSignal | undefined, timeoutSignal: AbortSignal): AbortSignal;
11
+ export declare function mergeSignals(userSignal: AbortSignal | undefined, timeoutSignal: AbortSignal, scopeSignal?: AbortSignal): AbortSignal;
12
12
  export declare function loadDistribution<T extends string>(provider: Provider, formattedInput: string, config: DecideConfig<T>, meta: MetaBuilder, signal: AbortSignal, cacheKey: string): Promise<Distribution<T>>;
13
13
  /**
14
14
  * Drop the in-flight slot for a key after a failed sample so a concurrent
@@ -1 +1 @@
1
- {"version":3,"file":"distribution.d.ts","sourceRoot":"","sources":["../../src/engine/distribution.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAQH,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,0BAA0B,CAAC;AACzD,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,aAAa,CAAC;AAChD,OAAO,EAA+B,KAAK,YAAY,EAAE,MAAM,aAAa,CAAC;AAC7E,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,WAAW,CAAC;AAS7C,wBAAgB,uBAAuB,CAAC,CAAC,SAAS,MAAM,EACtD,QAAQ,EAAE,QAAQ,EAClB,cAAc,EAAE,MAAM,EACtB,MAAM,EAAE,YAAY,CAAC,CAAC,CAAC,GACtB,MAAM,CAWR;AAED,wBAAgB,YAAY,CAC1B,UAAU,EAAE,WAAW,GAAG,SAAS,EACnC,aAAa,EAAE,WAAW,GACzB,WAAW,CAIb;AAED,wBAAsB,gBAAgB,CAAC,CAAC,SAAS,MAAM,EACrD,QAAQ,EAAE,QAAQ,EAClB,cAAc,EAAE,MAAM,EACtB,MAAM,EAAE,YAAY,CAAC,CAAC,CAAC,EACvB,IAAI,EAAE,WAAW,EACjB,MAAM,EAAE,WAAW,EACnB,QAAQ,EAAE,MAAM,GACf,OAAO,CAAC,YAAY,CAAC,CAAC,CAAC,CAAC,CAa1B;AAmBD;;;GAGG;AACH,wBAAgB,cAAc,CAAC,QAAQ,EAAE,MAAM,GAAG,IAAI,CAErD"}
1
+ {"version":3,"file":"distribution.d.ts","sourceRoot":"","sources":["../../src/engine/distribution.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAQH,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,0BAA0B,CAAC;AACzD,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,aAAa,CAAC;AAChD,OAAO,EAA+B,KAAK,YAAY,EAAE,MAAM,aAAa,CAAC;AAC7E,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,WAAW,CAAC;AAS7C,wBAAgB,uBAAuB,CAAC,CAAC,SAAS,MAAM,EACtD,QAAQ,EAAE,QAAQ,EAClB,cAAc,EAAE,MAAM,EACtB,MAAM,EAAE,YAAY,CAAC,CAAC,CAAC,GACtB,MAAM,CAWR;AAED,wBAAgB,YAAY,CAC1B,UAAU,EAAE,WAAW,GAAG,SAAS,EACnC,aAAa,EAAE,WAAW,EAC1B,WAAW,CAAC,EAAE,WAAW,GACxB,WAAW,CAKb;AAED,wBAAsB,gBAAgB,CAAC,CAAC,SAAS,MAAM,EACrD,QAAQ,EAAE,QAAQ,EAClB,cAAc,EAAE,MAAM,EACtB,MAAM,EAAE,YAAY,CAAC,CAAC,CAAC,EACvB,IAAI,EAAE,WAAW,EACjB,MAAM,EAAE,WAAW,EACnB,QAAQ,EAAE,MAAM,GACf,OAAO,CAAC,YAAY,CAAC,CAAC,CAAC,CAAC,CAa1B;AAmBD;;;GAGG;AACH,wBAAgB,cAAc,CAAC,QAAQ,EAAE,MAAM,GAAG,IAAI,CAErD"}
@@ -3,10 +3,17 @@
3
3
  * chain-budget exhaustion, pre/mid-loop cancellation, and chain-exhausted
4
4
  * (every-provider-errored vs last-provider-uncertain).
5
5
  */
6
+ import type { BudgetMode } from "../budget-tracker.js";
6
7
  import type { Distribution, Unknown, Verdict } from "../types.js";
7
8
  import type { DecideConfig } from "./config.js";
8
9
  import { type MetaBuilder } from "./meta.js";
9
10
  export declare function checkChainBudget<T extends string>(meta: MetaBuilder, chainStartMs: number, chainTimeoutMs: number, config: DecideConfig<T>): Unknown<T> | undefined;
10
11
  export declare function makeCancelledFromMeta<T extends string>(meta: MetaBuilder, reason: string): Unknown<T>;
12
+ /**
13
+ * Build the terminal verdict for scope-budget exhaustion. Under
14
+ * `BudgetMode.graceful` returns `Unknown { budget_exceeded }`; under
15
+ * `"throw"` mode throws `BudgetExceededError` for the caller to handle.
16
+ */
17
+ export declare function makeBudgetExceededVerdict<T extends string>(meta: MetaBuilder, spent: number, limit: number, mode: BudgetMode): Unknown<T>;
11
18
  export declare function finalizeChainExhausted<T extends string>(meta: MetaBuilder, lastCalibrated: Distribution<T> | undefined, attempts: number, config: DecideConfig<T>): Verdict<T>;
12
19
  //# sourceMappingURL=finalize.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"finalize.d.ts","sourceRoot":"","sources":["../../src/engine/finalize.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAGH,OAAO,KAAK,EAAE,YAAY,EAAE,OAAO,EAAE,OAAO,EAAE,MAAM,aAAa,CAAC;AAElE,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,aAAa,CAAC;AAEhD,OAAO,EAAuB,KAAK,WAAW,EAAE,MAAM,WAAW,CAAC;AAElE,wBAAgB,gBAAgB,CAAC,CAAC,SAAS,MAAM,EAC/C,IAAI,EAAE,WAAW,EACjB,YAAY,EAAE,MAAM,EACpB,cAAc,EAAE,MAAM,EACtB,MAAM,EAAE,YAAY,CAAC,CAAC,CAAC,GACtB,OAAO,CAAC,CAAC,CAAC,GAAG,SAAS,CAUxB;AAED,wBAAgB,qBAAqB,CAAC,CAAC,SAAS,MAAM,EACpD,IAAI,EAAE,WAAW,EACjB,MAAM,EAAE,MAAM,GACb,OAAO,CAAC,CAAC,CAAC,CAMZ;AAED,wBAAgB,sBAAsB,CAAC,CAAC,SAAS,MAAM,EACrD,IAAI,EAAE,WAAW,EACjB,cAAc,EAAE,YAAY,CAAC,CAAC,CAAC,GAAG,SAAS,EAC3C,QAAQ,EAAE,MAAM,EAChB,MAAM,EAAE,YAAY,CAAC,CAAC,CAAC,GACtB,OAAO,CAAC,CAAC,CAAC,CAqCZ"}
1
+ {"version":3,"file":"finalize.d.ts","sourceRoot":"","sources":["../../src/engine/finalize.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAEH,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,sBAAsB,CAAC;AAEvD,OAAO,KAAK,EAAE,YAAY,EAAE,OAAO,EAAE,OAAO,EAAE,MAAM,aAAa,CAAC;AAElE,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,aAAa,CAAC;AAEhD,OAAO,EAAuB,KAAK,WAAW,EAAE,MAAM,WAAW,CAAC;AAElE,wBAAgB,gBAAgB,CAAC,CAAC,SAAS,MAAM,EAC/C,IAAI,EAAE,WAAW,EACjB,YAAY,EAAE,MAAM,EACpB,cAAc,EAAE,MAAM,EACtB,MAAM,EAAE,YAAY,CAAC,CAAC,CAAC,GACtB,OAAO,CAAC,CAAC,CAAC,GAAG,SAAS,CAUxB;AAED,wBAAgB,qBAAqB,CAAC,CAAC,SAAS,MAAM,EACpD,IAAI,EAAE,WAAW,EACjB,MAAM,EAAE,MAAM,GACb,OAAO,CAAC,CAAC,CAAC,CAMZ;AAED;;;;GAIG;AACH,wBAAgB,yBAAyB,CAAC,CAAC,SAAS,MAAM,EACxD,IAAI,EAAE,WAAW,EACjB,KAAK,EAAE,MAAM,EACb,KAAK,EAAE,MAAM,EACb,IAAI,EAAE,UAAU,GACf,OAAO,CAAC,CAAC,CAAC,CASZ;AAED,wBAAgB,sBAAsB,CAAC,CAAC,SAAS,MAAM,EACrD,IAAI,EAAE,WAAW,EACjB,cAAc,EAAE,YAAY,CAAC,CAAC,CAAC,GAAG,SAAS,EAC3C,QAAQ,EAAE,MAAM,EAChB,MAAM,EAAE,YAAY,CAAC,CAAC,CAAC,GACtB,OAAO,CAAC,CAAC,CAAC,CAqCZ"}
package/dist/errors.d.ts CHANGED
@@ -15,7 +15,7 @@
15
15
  * Stable error codes carried in `error.code`. Discriminate on these rather
16
16
  * than on `instanceof` of removed-historical subclasses.
17
17
  */
18
- export type ErrorCode = "decision_space_collision" | "decision_space_too_large" | "missing_provider_config" | "malformed_provider_config" | "unknown_provider_factory" | "missing_credential" | "incompatible_calibrator" | "invalid_classifier_name" | "invalid_thresholds" | "invalid_space" | "empty_providers" | "provider_network" | "provider_rate_limit" | "provider_timeout" | "provider_unauthorized" | "provider_server_error" | "provider_malformed_response" | "invalid_distribution" | "per_call_timeout" | "chain_timeout" | "max_calls";
18
+ export type ErrorCode = "decision_space_collision" | "decision_space_too_large" | "missing_provider_config" | "malformed_provider_config" | "unknown_provider_factory" | "missing_credential" | "incompatible_calibrator" | "invalid_classifier_name" | "invalid_thresholds" | "invalid_space" | "empty_providers" | "provider_network" | "provider_rate_limit" | "provider_timeout" | "provider_unauthorized" | "provider_server_error" | "provider_malformed_response" | "invalid_distribution" | "per_call_timeout" | "chain_timeout" | "max_calls" | "tokens_exceeded" | "invalid_scope_budget";
19
19
  export declare class DomovoiError extends Error {
20
20
  readonly code: string;
21
21
  constructor(message: string, options?: {
@@ -46,6 +46,23 @@ export declare class BudgetExhaustedError extends DomovoiError {
46
46
  cause?: unknown;
47
47
  });
48
48
  }
49
+ /**
50
+ * Thrown when scope token budget is exceeded under `onExceeded: "throw"` mode.
51
+ * Distinct from `BudgetExhaustedError` (operational time / call-count budgets):
52
+ * this is the cost ceiling for a `domovoi.scope({ budget: { tokens } })` block.
53
+ *
54
+ * Default mode is `"graceful"` — classify returns
55
+ * `Unknown { reason: { type: "budget_exceeded", spent, limit } }` instead.
56
+ */
57
+ export declare class BudgetExceededError extends DomovoiError {
58
+ readonly spent: number;
59
+ readonly limit: number;
60
+ constructor(options: {
61
+ spent: number;
62
+ limit: number;
63
+ cause?: unknown;
64
+ });
65
+ }
49
66
  /**
50
67
  * Wraps any non-DomovoiError thrown value in `ProviderError({ cause })`.
51
68
  * `DomovoiError` subtypes — including `ProviderError` subclasses defined by
@@ -1 +1 @@
1
- {"version":3,"file":"errors.d.ts","sourceRoot":"","sources":["../src/errors.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;GAYG;AAEH;;;GAGG;AACH,MAAM,MAAM,SAAS,GAEjB,0BAA0B,GAC1B,0BAA0B,GAC1B,yBAAyB,GACzB,2BAA2B,GAC3B,0BAA0B,GAC1B,oBAAoB,GACpB,yBAAyB,GACzB,yBAAyB,GACzB,oBAAoB,GACpB,eAAe,GACf,iBAAiB,GAEjB,kBAAkB,GAClB,qBAAqB,GACrB,kBAAkB,GAClB,uBAAuB,GACvB,uBAAuB,GACvB,6BAA6B,GAC7B,sBAAsB,GAEtB,kBAAkB,GAClB,eAAe,GACf,WAAW,CAAC;AAEhB,qBAAa,YAAa,SAAQ,KAAK;IACrC,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;gBAEV,OAAO,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE;QAAE,IAAI,CAAC,EAAE,MAAM,CAAC;QAAC,KAAK,CAAC,EAAE,OAAO,CAAA;KAAE;CAK1E;AAED,qBAAa,aAAc,SAAQ,YAAY;gBACjC,OAAO,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE;QAAE,IAAI,CAAC,EAAE,MAAM,CAAC;QAAC,KAAK,CAAC,EAAE,OAAO,CAAA;KAAE;CAI1E;AAED,qBAAa,WAAY,SAAQ,YAAY;gBAC/B,OAAO,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE;QAAE,IAAI,CAAC,EAAE,MAAM,CAAC;QAAC,KAAK,CAAC,EAAE,OAAO,CAAA;KAAE;CAI1E;AAED,qBAAa,oBAAqB,SAAQ,YAAY;IACpD,QAAQ,CAAC,kBAAkB,EAAE,SAAS,MAAM,EAAE,CAAC;IAC/C,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAC;IAC3B,QAAQ,CAAC,KAAK,EAAE,kBAAkB,GAAG,eAAe,GAAG,WAAW,CAAC;gBAGjE,OAAO,EAAE,MAAM,EACf,OAAO,EAAE;QACP,KAAK,EAAE,kBAAkB,GAAG,eAAe,GAAG,WAAW,CAAC;QAC1D,kBAAkB,EAAE,SAAS,MAAM,EAAE,CAAC;QACtC,SAAS,EAAE,MAAM,CAAC;QAClB,KAAK,CAAC,EAAE,OAAO,CAAC;KACjB;CAQJ;AAED;;;;GAIG;AACH,wBAAgB,yBAAyB,CAAC,MAAM,EAAE,OAAO,GAAG,YAAY,CAYvE;AAED;;;GAGG;AACH,wBAAgB,cAAc,CAAC,GAAG,EAAE,OAAO,GAAG;IAC5C,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;IACzB,QAAQ,CAAC,IAAI,CAAC,EAAE,MAAM,CAAC;IACvB,QAAQ,CAAC,KAAK,CAAC,EAAE,UAAU,CAAC,OAAO,cAAc,CAAC,CAAC;IACnD,QAAQ,CAAC,KAAK,CAAC,EAAE,MAAM,CAAC;CACzB,CAaA"}
1
+ {"version":3,"file":"errors.d.ts","sourceRoot":"","sources":["../src/errors.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;GAYG;AAEH;;;GAGG;AACH,MAAM,MAAM,SAAS,GAEjB,0BAA0B,GAC1B,0BAA0B,GAC1B,yBAAyB,GACzB,2BAA2B,GAC3B,0BAA0B,GAC1B,oBAAoB,GACpB,yBAAyB,GACzB,yBAAyB,GACzB,oBAAoB,GACpB,eAAe,GACf,iBAAiB,GAEjB,kBAAkB,GAClB,qBAAqB,GACrB,kBAAkB,GAClB,uBAAuB,GACvB,uBAAuB,GACvB,6BAA6B,GAC7B,sBAAsB,GAEtB,kBAAkB,GAClB,eAAe,GACf,WAAW,GAEX,iBAAiB,GAEjB,sBAAsB,CAAC;AAE3B,qBAAa,YAAa,SAAQ,KAAK;IACrC,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;gBAEV,OAAO,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE;QAAE,IAAI,CAAC,EAAE,MAAM,CAAC;QAAC,KAAK,CAAC,EAAE,OAAO,CAAA;KAAE;CAK1E;AAED,qBAAa,aAAc,SAAQ,YAAY;gBACjC,OAAO,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE;QAAE,IAAI,CAAC,EAAE,MAAM,CAAC;QAAC,KAAK,CAAC,EAAE,OAAO,CAAA;KAAE;CAI1E;AAED,qBAAa,WAAY,SAAQ,YAAY;gBAC/B,OAAO,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE;QAAE,IAAI,CAAC,EAAE,MAAM,CAAC;QAAC,KAAK,CAAC,EAAE,OAAO,CAAA;KAAE;CAI1E;AAED,qBAAa,oBAAqB,SAAQ,YAAY;IACpD,QAAQ,CAAC,kBAAkB,EAAE,SAAS,MAAM,EAAE,CAAC;IAC/C,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAC;IAC3B,QAAQ,CAAC,KAAK,EAAE,kBAAkB,GAAG,eAAe,GAAG,WAAW,CAAC;gBAGjE,OAAO,EAAE,MAAM,EACf,OAAO,EAAE;QACP,KAAK,EAAE,kBAAkB,GAAG,eAAe,GAAG,WAAW,CAAC;QAC1D,kBAAkB,EAAE,SAAS,MAAM,EAAE,CAAC;QACtC,SAAS,EAAE,MAAM,CAAC;QAClB,KAAK,CAAC,EAAE,OAAO,CAAC;KACjB;CAQJ;AAED;;;;;;;GAOG;AACH,qBAAa,mBAAoB,SAAQ,YAAY;IACnD,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;IACvB,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;gBAEX,OAAO,EAAE;QAAE,KAAK,EAAE,MAAM,CAAC;QAAC,KAAK,EAAE,MAAM,CAAC;QAAC,KAAK,CAAC,EAAE,OAAO,CAAA;KAAE;CASvE;AAED;;;;GAIG;AACH,wBAAgB,yBAAyB,CAAC,MAAM,EAAE,OAAO,GAAG,YAAY,CAYvE;AAED;;;GAGG;AACH,wBAAgB,cAAc,CAAC,GAAG,EAAE,OAAO,GAAG;IAC5C,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;IACzB,QAAQ,CAAC,IAAI,CAAC,EAAE,MAAM,CAAC;IACvB,QAAQ,CAAC,KAAK,CAAC,EAAE,UAAU,CAAC,OAAO,cAAc,CAAC,CAAC;IACnD,QAAQ,CAAC,KAAK,CAAC,EAAE,MAAM,CAAC;CACzB,CAaA"}