@loomwork/middleware 0.0.0-stage → 0.1.1

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 (59) hide show
  1. package/LICENSE +202 -0
  2. package/NOTICE +8 -0
  3. package/README.md +82 -2
  4. package/dist/claimCheck.d.ts +179 -0
  5. package/dist/claimCheck.d.ts.map +1 -0
  6. package/dist/claimCheck.js +241 -0
  7. package/dist/claimCheck.js.map +1 -0
  8. package/dist/index.d.ts +13 -0
  9. package/dist/index.d.ts.map +1 -0
  10. package/dist/index.js +48 -0
  11. package/dist/index.js.map +1 -0
  12. package/dist/killSwitchRegistry.d.ts +74 -0
  13. package/dist/killSwitchRegistry.d.ts.map +1 -0
  14. package/dist/killSwitchRegistry.js +108 -0
  15. package/dist/killSwitchRegistry.js.map +1 -0
  16. package/dist/metering.d.ts +81 -0
  17. package/dist/metering.d.ts.map +1 -0
  18. package/dist/metering.js +153 -0
  19. package/dist/metering.js.map +1 -0
  20. package/dist/nestedBudget.d.ts +60 -0
  21. package/dist/nestedBudget.d.ts.map +1 -0
  22. package/dist/nestedBudget.js +134 -0
  23. package/dist/nestedBudget.js.map +1 -0
  24. package/dist/profile.d.ts +38 -0
  25. package/dist/profile.d.ts.map +1 -0
  26. package/dist/profile.js +70 -0
  27. package/dist/profile.js.map +1 -0
  28. package/dist/rateLimit.d.ts +40 -0
  29. package/dist/rateLimit.d.ts.map +1 -0
  30. package/dist/rateLimit.js +83 -0
  31. package/dist/rateLimit.js.map +1 -0
  32. package/dist/redaction.d.ts +66 -0
  33. package/dist/redaction.d.ts.map +1 -0
  34. package/dist/redaction.js +141 -0
  35. package/dist/redaction.js.map +1 -0
  36. package/dist/retry.d.ts +50 -0
  37. package/dist/retry.d.ts.map +1 -0
  38. package/dist/retry.js +98 -0
  39. package/dist/retry.js.map +1 -0
  40. package/dist/safety.d.ts +93 -0
  41. package/dist/safety.d.ts.map +1 -0
  42. package/dist/safety.js +152 -0
  43. package/dist/safety.js.map +1 -0
  44. package/dist/streamTokenBudget.d.ts +58 -0
  45. package/dist/streamTokenBudget.d.ts.map +1 -0
  46. package/dist/streamTokenBudget.js +47 -0
  47. package/dist/streamTokenBudget.js.map +1 -0
  48. package/package.json +55 -3
  49. package/src/claimCheck.ts +410 -0
  50. package/src/index.ts +79 -0
  51. package/src/killSwitchRegistry.ts +168 -0
  52. package/src/metering.ts +226 -0
  53. package/src/nestedBudget.ts +200 -0
  54. package/src/profile.ts +86 -0
  55. package/src/rateLimit.ts +126 -0
  56. package/src/redaction.ts +184 -0
  57. package/src/retry.ts +133 -0
  58. package/src/safety.ts +235 -0
  59. package/src/streamTokenBudget.ts +98 -0
@@ -0,0 +1,153 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.QuotaExceededError = void 0;
4
+ exports.isBuiltinDimension = isBuiltinDimension;
5
+ exports.isCustomDimensionName = isCustomDimensionName;
6
+ exports.assertMaxCustom = assertMaxCustom;
7
+ exports.overCustomBudget = overCustomBudget;
8
+ exports.meteredPayload = meteredPayload;
9
+ exports.llmUsageTokens = llmUsageTokens;
10
+ exports.meteringMiddleware = meteringMiddleware;
11
+ // ── Resource accounting & quotas (Loom 2.0 K3) ───────────────────────────────
12
+ //
13
+ // The scarce resource for intelligent software is tokens / $ / latency, not CPU
14
+ // cycles. This middleware meters every effect it wraps as a durable RESOURCE_METERED
15
+ // fact (folded into `variables._resources`), and — given a budget — enforces a quota
16
+ // by admission control: once an execution has spent its budget, the next effect is
17
+ // refused (QUOTA_EXCEEDED + a thrown QuotaExceededError that fails the step). Both
18
+ // the accounting and the enforcement are replay-safe (metered facts live in the log;
19
+ // the running total is read from the deterministic fold via `ctx.resources`).
20
+ const core_1 = require("@loomwork/core");
21
+ /** True when `name` is a built-in metering dimension (never a custom one). */
22
+ function isBuiltinDimension(name) {
23
+ return core_1.BUILTIN_METER_DIMENSIONS.has(name);
24
+ }
25
+ /** True when `name` is a valid custom dimension name (pattern shared with the fold's guard). */
26
+ function isCustomDimensionName(name) {
27
+ return (0, core_1.isVocabularyName)(name) && !core_1.BUILTIN_METER_DIMENSIONS.has(name);
28
+ }
29
+ /**
30
+ * Validate a `Budget.maxCustom` and return a FROZEN shallow copy to enforce against (so a later
31
+ * mutation of the caller's object cannot smuggle in a built-in name or a bad ceiling). Refuses a
32
+ * built-in dimension name (use `maxTokens` etc.: a custom ceiling on `tokens` would read
33
+ * `_resources.custom.tokens`, which nothing meters) and a non-numeric ceiling.
34
+ */
35
+ function assertMaxCustom(maxCustom) {
36
+ if (maxCustom === undefined)
37
+ return undefined;
38
+ for (const [name, max] of Object.entries(maxCustom)) {
39
+ if (core_1.BUILTIN_METER_DIMENSIONS.has(name)) {
40
+ throw new Error(`Budget.maxCustom."${name}" names a built-in dimension; use its own max field (e.g. maxTokens)`);
41
+ }
42
+ if (typeof max !== "number" || !Number.isFinite(max) || max < 0) {
43
+ throw new Error(`Budget.maxCustom."${name}" must be a finite non-negative number`);
44
+ }
45
+ }
46
+ return Object.freeze({ ...maxCustom });
47
+ }
48
+ /** The first custom ceiling the spend has reached, as a reason — own-property reads only. */
49
+ function overCustomBudget(custom, maxCustom) {
50
+ if (maxCustom === undefined)
51
+ return null;
52
+ for (const [name, max] of Object.entries(maxCustom)) {
53
+ const spent = custom !== undefined && Object.hasOwn(custom, name) ? custom[name] : 0;
54
+ if (spent >= max)
55
+ return `${name} ${spent} >= budget ${max}`;
56
+ }
57
+ return null;
58
+ }
59
+ class QuotaExceededError extends Error {
60
+ reason;
61
+ /** A budget refusal is a decision; retrying repeats it (`retryMiddleware` honours this). */
62
+ retryable = false;
63
+ constructor(reason) {
64
+ super(`Quota exceeded: ${reason}`);
65
+ this.name = "QuotaExceededError";
66
+ this.reason = reason;
67
+ }
68
+ }
69
+ exports.QuotaExceededError = QuotaExceededError;
70
+ function overBudget(r, b) {
71
+ if (b.maxCostUsd !== undefined && r.costUsd >= b.maxCostUsd) {
72
+ return `cost ${r.costUsd} >= budget ${b.maxCostUsd} USD`;
73
+ }
74
+ if (b.maxTokens !== undefined && r.tokens >= b.maxTokens) {
75
+ return `tokens ${r.tokens} >= budget ${b.maxTokens}`;
76
+ }
77
+ if (b.maxMemBytes !== undefined && (r.memBytes ?? 0) >= b.maxMemBytes) {
78
+ return `memBytes ${r.memBytes ?? 0} >= budget ${b.maxMemBytes}`;
79
+ }
80
+ if (b.maxWallMs !== undefined && (r.wallMs ?? 0) >= b.maxWallMs) {
81
+ return `wallMs ${r.wallMs ?? 0} >= budget ${b.maxWallMs}`;
82
+ }
83
+ if (b.maxNetBytes !== undefined && (r.netBytes ?? 0) >= b.maxNetBytes) {
84
+ return `netBytes ${r.netBytes ?? 0} >= budget ${b.maxNetBytes}`;
85
+ }
86
+ return overCustomBudget(r.custom, b.maxCustom);
87
+ }
88
+ /** The RESOURCE_METERED payload for a delta — only dimensions the pricing reported. */
89
+ function meteredPayload(delta, ms) {
90
+ return {
91
+ ...(delta.tokens !== undefined && { tokens: delta.tokens }),
92
+ ...(delta.costUsd !== undefined && { costUsd: delta.costUsd }),
93
+ ...(delta.memBytes !== undefined && { memBytes: delta.memBytes }),
94
+ ...(delta.wallMs !== undefined && { wallMs: delta.wallMs }),
95
+ ...(delta.netBytes !== undefined && { netBytes: delta.netBytes }),
96
+ ...(delta.custom !== undefined && { custom: delta.custom }),
97
+ ms: delta.ms ?? ms,
98
+ };
99
+ }
100
+ /** Read token usage from an LlmResponse-shaped result (`{ usage: { inputTokens, outputTokens } }`). */
101
+ function llmUsageTokens(result) {
102
+ const usage = result
103
+ ?.usage;
104
+ if (usage === undefined || usage === null)
105
+ return 0;
106
+ return (usage.inputTokens ?? 0) + (usage.outputTokens ?? 0);
107
+ }
108
+ function meteringMiddleware(options) {
109
+ const now = options.clock ?? (() => Date.now());
110
+ const frozenMax = assertMaxCustom(options.budget?.maxCustom);
111
+ const budget = options.budget === undefined
112
+ ? undefined
113
+ : { ...options.budget, ...(frozenMax !== undefined && { maxCustom: frozenMax }) };
114
+ return async (ctx, input, next) => {
115
+ // Admission control: refuse if the execution has already spent its budget.
116
+ if (budget !== undefined && ctx.resources !== undefined) {
117
+ const reason = overBudget(ctx.resources, budget);
118
+ if (reason !== null) {
119
+ ctx.emit("QUOTA_EXCEEDED", { stepId: ctx.stepId, reason });
120
+ throw new QuotaExceededError(reason);
121
+ }
122
+ }
123
+ const start = now();
124
+ let result;
125
+ try {
126
+ result = await next();
127
+ }
128
+ catch (err) {
129
+ // K8 — meter an aborted effect before rethrowing. Metering normally runs
130
+ // after next(), so a severed call would never be seen and
131
+ // start-expensive-effect → cancel → repeat could evade the budget forever.
132
+ // Only aborts are metered here; an ordinary failure keeps today's
133
+ // unmetered semantics.
134
+ if (ctx.signal?.aborted === true) {
135
+ const ms = now() - start;
136
+ const delta = options.pricing({
137
+ stepId: ctx.stepId,
138
+ input,
139
+ result: undefined,
140
+ ms,
141
+ aborted: true,
142
+ });
143
+ ctx.emit("RESOURCE_METERED", meteredPayload(delta, ms));
144
+ }
145
+ throw err;
146
+ }
147
+ const ms = now() - start;
148
+ const delta = options.pricing({ stepId: ctx.stepId, input, result, ms });
149
+ ctx.emit("RESOURCE_METERED", meteredPayload(delta, ms));
150
+ return result;
151
+ };
152
+ }
153
+ //# sourceMappingURL=metering.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"metering.js","sourceRoot":"","sources":["../src/metering.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,60 @@
1
+ import type { EffectContext, EffectMiddleware } from "@loomwork/effect-runtime";
2
+ import { type Budget, type Pricing, type ResourceDelta } from "./metering";
3
+ export interface ScopeSpend {
4
+ tokens: number;
5
+ costUsd: number;
6
+ memBytes?: number;
7
+ wallMs?: number;
8
+ netBytes?: number;
9
+ /** ADR 0115 — open dimensions, accumulated by name (own-property keys only). */
10
+ custom?: Record<string, number>;
11
+ }
12
+ /**
13
+ * A projection of spend per scope key (e.g. `tenant:acme`, `workflow:audit`).
14
+ * The app updates it from the RESOURCE_METERED event stream (across all of a
15
+ * tenant's executions) — typically in `defineConfig({ onEvent })`. It is
16
+ * eventually consistent by nature; two nodes may briefly under-count, which is
17
+ * the accepted trade-off for cross-execution budgets.
18
+ */
19
+ export declare class BudgetLedger {
20
+ private readonly spend;
21
+ add(scopeKey: string, delta: ResourceDelta): void;
22
+ spent(scopeKey: string): ScopeSpend;
23
+ /** Fold a RESOURCE_METERED event into the given scope keys (from an onEvent hook). */
24
+ applyMetered(scopeKeys: readonly string[], payload: unknown): void;
25
+ }
26
+ /** One level of the nesting: a labelled scope key + its budget. */
27
+ export interface BudgetScope {
28
+ /** e.g. "tenant", "workflow", "execution" — recorded in the trip for attribution. */
29
+ level: string;
30
+ /** The ledger key this level accumulates under (execution uses ctx.resources instead). */
31
+ key: string;
32
+ budget: Budget;
33
+ }
34
+ export interface NestedBudgetOptions {
35
+ /** Cost model — same shape as `meteringMiddleware`. */
36
+ pricing: Pricing;
37
+ /** The cross-execution spend projection (tenant/workflow levels). */
38
+ ledger: BudgetLedger;
39
+ /**
40
+ * Resolve the nested scopes for an effect, OUTERMOST (broadest) first. The
41
+ * execution level should use `key: ""` — its spend is read from the pure
42
+ * per-execution fold (`ctx.resources`), not the ledger.
43
+ */
44
+ scopesOf: (ctx: EffectContext) => BudgetScope[];
45
+ /** Injectable clock for the measured `ms`. */
46
+ clock?: () => number;
47
+ }
48
+ /**
49
+ * K12.3 — nested-budget admission control. Before an effect runs, every scope
50
+ * (tenant → workflow → execution) is checked; the FIRST (most-restrictive)
51
+ * breach denies with a `QUOTA_EXCEEDED` fact attributed to that `level`, and the
52
+ * effect never runs. Otherwise the effect runs, is priced, and its delta is
53
+ * recorded into the ledger's non-execution scopes (the execution level is
54
+ * metered by the durable fold as usual) and emitted as `RESOURCE_METERED`.
55
+ *
56
+ * The execution level reads spend from `ctx.resources` (the pure fold, K3); the
57
+ * broader levels read from the ledger projection.
58
+ */
59
+ export declare function nestedBudgetMiddleware(options: NestedBudgetOptions): EffectMiddleware;
60
+ //# sourceMappingURL=nestedBudget.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"nestedBudget.d.ts","sourceRoot":"","sources":["../src/nestedBudget.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,aAAa,EAAE,gBAAgB,EAAE,MAAM,0BAA0B,CAAC;AAChF,OAAO,EACL,KAAK,MAAM,EACX,KAAK,OAAO,EAEZ,KAAK,aAAa,EAInB,MAAM,YAAY,CAAC;AAcpB,MAAM,WAAW,UAAU;IACzB,MAAM,EAAE,MAAM,CAAC;IACf,OAAO,EAAE,MAAM,CAAC;IAEhB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,gFAAgF;IAChF,MAAM,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;CACjC;AAKD;;;;;;GAMG;AACH,qBAAa,YAAY;IACvB,OAAO,CAAC,QAAQ,CAAC,KAAK,CAAiC;IAEvD,GAAG,CAAC,QAAQ,EAAE,MAAM,EAAE,KAAK,EAAE,aAAa,GAAG,IAAI;IA4BjD,KAAK,CAAC,QAAQ,EAAE,MAAM,GAAG,UAAU;IAInC,sFAAsF;IACtF,YAAY,CAAC,SAAS,EAAE,SAAS,MAAM,EAAE,EAAE,OAAO,EAAE,OAAO,GAAG,IAAI;CAqBnE;AAED,mEAAmE;AACnE,MAAM,WAAW,WAAW;IAC1B,qFAAqF;IACrF,KAAK,EAAE,MAAM,CAAC;IACd,0FAA0F;IAC1F,GAAG,EAAE,MAAM,CAAC;IACZ,MAAM,EAAE,MAAM,CAAC;CAChB;AAED,MAAM,WAAW,mBAAmB;IAClC,uDAAuD;IACvD,OAAO,EAAE,OAAO,CAAC;IACjB,qEAAqE;IACrE,MAAM,EAAE,YAAY,CAAC;IACrB;;;;OAIG;IACH,QAAQ,EAAE,CAAC,GAAG,EAAE,aAAa,KAAK,WAAW,EAAE,CAAC;IAChD,8CAA8C;IAC9C,KAAK,CAAC,EAAE,MAAM,MAAM,CAAC;CACtB;AAaD;;;;;;;;;;GAUG;AACH,wBAAgB,sBAAsB,CAAC,OAAO,EAAE,mBAAmB,GAAG,gBAAgB,CAkDrF"}
@@ -0,0 +1,134 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.BudgetLedger = void 0;
4
+ exports.nestedBudgetMiddleware = nestedBudgetMiddleware;
5
+ const metering_1 = require("./metering");
6
+ /** Distinct custom dimension names one ledger scope keeps (the fold's own cap, ADR 0115). */
7
+ const LEDGER_CUSTOM_MAX = 32;
8
+ /**
9
+ * A projection of spend per scope key (e.g. `tenant:acme`, `workflow:audit`).
10
+ * The app updates it from the RESOURCE_METERED event stream (across all of a
11
+ * tenant's executions) — typically in `defineConfig({ onEvent })`. It is
12
+ * eventually consistent by nature; two nodes may briefly under-count, which is
13
+ * the accepted trade-off for cross-execution budgets.
14
+ */
15
+ class BudgetLedger {
16
+ spend = new Map();
17
+ add(scopeKey, delta) {
18
+ const cur = this.spend.get(scopeKey) ?? { tokens: 0, costUsd: 0 };
19
+ const memBytes = (cur.memBytes ?? 0) + (delta.memBytes ?? 0);
20
+ const wallMs = (cur.wallMs ?? 0) + (delta.wallMs ?? 0);
21
+ const netBytes = (cur.netBytes ?? 0) + (delta.netBytes ?? 0);
22
+ let custom = cur.custom;
23
+ if (delta.custom !== undefined) {
24
+ custom = { ...cur.custom };
25
+ for (const [name, v] of Object.entries(delta.custom)) {
26
+ if (typeof v !== "number" || !Number.isFinite(v) || v < 0)
27
+ continue;
28
+ if (name === "constructor" || name === "prototype" || !(0, metering_1.isCustomDimensionName)(name))
29
+ continue;
30
+ if (!Object.hasOwn(custom, name) && Object.keys(custom).length >= LEDGER_CUSTOM_MAX) {
31
+ continue;
32
+ }
33
+ custom[name] = (Object.hasOwn(custom, name) ? custom[name] : 0) + v;
34
+ }
35
+ }
36
+ this.spend.set(scopeKey, {
37
+ tokens: cur.tokens + (delta.tokens ?? 0),
38
+ ...(custom !== undefined && { custom }),
39
+ costUsd: cur.costUsd + (delta.costUsd ?? 0),
40
+ ...(memBytes > 0 && { memBytes }),
41
+ ...(wallMs > 0 && { wallMs }),
42
+ ...(netBytes > 0 && { netBytes }),
43
+ });
44
+ }
45
+ spent(scopeKey) {
46
+ return this.spend.get(scopeKey) ?? { tokens: 0, costUsd: 0 };
47
+ }
48
+ /** Fold a RESOURCE_METERED event into the given scope keys (from an onEvent hook). */
49
+ applyMetered(scopeKeys, payload) {
50
+ const p = payload;
51
+ const delta = {
52
+ ...(p.custom !== undefined &&
53
+ typeof p.custom === "object" &&
54
+ p.custom !== null && { custom: p.custom }),
55
+ ...(p.tokens !== undefined && { tokens: p.tokens }),
56
+ ...(p.costUsd !== undefined && { costUsd: p.costUsd }),
57
+ ...(p.memBytes !== undefined && { memBytes: p.memBytes }),
58
+ ...(p.wallMs !== undefined && { wallMs: p.wallMs }),
59
+ ...(p.netBytes !== undefined && { netBytes: p.netBytes }),
60
+ };
61
+ for (const key of scopeKeys)
62
+ this.add(key, delta);
63
+ }
64
+ }
65
+ exports.BudgetLedger = BudgetLedger;
66
+ function over(spent, b) {
67
+ return ((b.maxCostUsd !== undefined && spent.costUsd >= b.maxCostUsd) ||
68
+ (b.maxTokens !== undefined && spent.tokens >= b.maxTokens) ||
69
+ (b.maxMemBytes !== undefined && (spent.memBytes ?? 0) >= b.maxMemBytes) ||
70
+ (b.maxWallMs !== undefined && (spent.wallMs ?? 0) >= b.maxWallMs) ||
71
+ (b.maxNetBytes !== undefined && (spent.netBytes ?? 0) >= b.maxNetBytes) ||
72
+ (0, metering_1.overCustomBudget)(spent.custom, b.maxCustom) !== null);
73
+ }
74
+ /**
75
+ * K12.3 — nested-budget admission control. Before an effect runs, every scope
76
+ * (tenant → workflow → execution) is checked; the FIRST (most-restrictive)
77
+ * breach denies with a `QUOTA_EXCEEDED` fact attributed to that `level`, and the
78
+ * effect never runs. Otherwise the effect runs, is priced, and its delta is
79
+ * recorded into the ledger's non-execution scopes (the execution level is
80
+ * metered by the durable fold as usual) and emitted as `RESOURCE_METERED`.
81
+ *
82
+ * The execution level reads spend from `ctx.resources` (the pure fold, K3); the
83
+ * broader levels read from the ledger projection.
84
+ */
85
+ function nestedBudgetMiddleware(options) {
86
+ const now = options.clock ?? (() => Date.now());
87
+ return async (ctx, input, next) => {
88
+ const scopes = options.scopesOf(ctx);
89
+ // Scopes are resolved per effect, so their budgets cannot be checked at construction; refuse a
90
+ // bad `maxCustom` (built-in name / non-numeric ceiling) here, before any scope is evaluated,
91
+ // rather than part-way through the admission loop. Not memoised: a mutation cannot bypass it.
92
+ for (const scope of scopes)
93
+ (0, metering_1.assertMaxCustom)(scope.budget.maxCustom);
94
+ // Admission control — most-restrictive wins, attributed to its level.
95
+ for (const scope of scopes) {
96
+ const spent = scope.level === "execution"
97
+ ? {
98
+ tokens: ctx.resources?.tokens ?? 0,
99
+ costUsd: ctx.resources?.costUsd ?? 0,
100
+ ...(ctx.resources?.memBytes !== undefined && { memBytes: ctx.resources.memBytes }),
101
+ ...(ctx.resources?.wallMs !== undefined && { wallMs: ctx.resources.wallMs }),
102
+ ...(ctx.resources?.netBytes !== undefined && { netBytes: ctx.resources.netBytes }),
103
+ ...(ctx.resources?.custom !== undefined && {
104
+ custom: ctx.resources.custom,
105
+ }),
106
+ }
107
+ : options.ledger.spent(scope.key);
108
+ if (over(spent, scope.budget)) {
109
+ const reason = `${scope.level} budget exceeded (${scope.key || ctx.executionId})`;
110
+ ctx.emit("QUOTA_EXCEEDED", { stepId: ctx.stepId, reason, level: scope.level });
111
+ throw new metering_1.QuotaExceededError(reason);
112
+ }
113
+ }
114
+ const start = now();
115
+ const result = await next();
116
+ const ms = now() - start;
117
+ const delta = options.pricing({ stepId: ctx.stepId, input, result, ms });
118
+ ctx.emit("RESOURCE_METERED", {
119
+ ...(delta.tokens !== undefined && { tokens: delta.tokens }),
120
+ ...(delta.costUsd !== undefined && { costUsd: delta.costUsd }),
121
+ ...(delta.custom !== undefined && { custom: delta.custom }),
122
+ ms: delta.ms ?? ms,
123
+ });
124
+ // Update the projection for the broader (non-execution) levels immediately,
125
+ // so within a node successive effects see the running total (the durable
126
+ // fold + cross-node ledger reconcile via the RESOURCE_METERED stream).
127
+ for (const scope of scopes) {
128
+ if (scope.level !== "execution")
129
+ options.ledger.add(scope.key, delta);
130
+ }
131
+ return result;
132
+ };
133
+ }
134
+ //# sourceMappingURL=nestedBudget.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"nestedBudget.js","sourceRoot":"","sources":["../src/nestedBudget.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,38 @@
1
+ import type { EffectMiddleware } from "@loomwork/effect-runtime";
2
+ /** Effect-side timings, in milliseconds. */
3
+ export interface EffectProfile {
4
+ /** Effect invocations that completed (successfully or not). */
5
+ calls: number;
6
+ /** Summed wall time inside effects. */
7
+ totalEffectMs: number;
8
+ /** Longest single effect. */
9
+ maxEffectMs: number;
10
+ /** Greatest number of effects in flight at once. */
11
+ peakConcurrentEffects: number;
12
+ /** Per-tool breakdown, so a single slow tool is visible rather than averaged away. */
13
+ byTool: Record<string, {
14
+ calls: number;
15
+ totalMs: number;
16
+ }>;
17
+ }
18
+ export interface EffectProfiler {
19
+ middleware: EffectMiddleware;
20
+ profile(): EffectProfile;
21
+ reset(): void;
22
+ }
23
+ /**
24
+ * Measures what effects actually cost, as the counterpart to `ProfilingEventStore`.
25
+ *
26
+ * Together they answer [ADR 0065](../../../docs/adr/0065-group-commit-deferred-evidence-gate.md)
27
+ * gate 1: is **event-write throughput** the bottleneck for this workload, or is it effect
28
+ * latency? The gate requires that question be settled by profiling rather than intuition,
29
+ * and it is not rhetorical — `ExecutionJournal` queues appends on a serial tail, so the
30
+ * interpreter is decoupled from write latency and a workload can write a great deal without
31
+ * writes ever constraining it.
32
+ *
33
+ * Wire it as effect-runtime middleware in your own app, wrap your store in
34
+ * `ProfilingEventStore`, run real traffic, and compare `totalEffectMs` against
35
+ * `totalWriteMs`. Batching the write path can only help if the latter dominates.
36
+ */
37
+ export declare function effectTimingMiddleware(): EffectProfiler;
38
+ //# sourceMappingURL=profile.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"profile.d.ts","sourceRoot":"","sources":["../src/profile.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,0BAA0B,CAAC;AAEjE,4CAA4C;AAC5C,MAAM,WAAW,aAAa;IAC5B,+DAA+D;IAC/D,KAAK,EAAE,MAAM,CAAC;IACd,uCAAuC;IACvC,aAAa,EAAE,MAAM,CAAC;IACtB,6BAA6B;IAC7B,WAAW,EAAE,MAAM,CAAC;IACpB,oDAAoD;IACpD,qBAAqB,EAAE,MAAM,CAAC;IAC9B,sFAAsF;IACtF,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE;QAAE,KAAK,EAAE,MAAM,CAAC;QAAC,OAAO,EAAE,MAAM,CAAA;KAAE,CAAC,CAAC;CAC5D;AAED,MAAM,WAAW,cAAc;IAC7B,UAAU,EAAE,gBAAgB,CAAC;IAC7B,OAAO,IAAI,aAAa,CAAC;IACzB,KAAK,IAAI,IAAI,CAAC;CACf;AAED;;;;;;;;;;;;;GAaG;AACH,wBAAgB,sBAAsB,IAAI,cAAc,CAiDvD"}
@@ -0,0 +1,70 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.effectTimingMiddleware = effectTimingMiddleware;
4
+ /**
5
+ * Measures what effects actually cost, as the counterpart to `ProfilingEventStore`.
6
+ *
7
+ * Together they answer [ADR 0065](../../../docs/adr/0065-group-commit-deferred-evidence-gate.md)
8
+ * gate 1: is **event-write throughput** the bottleneck for this workload, or is it effect
9
+ * latency? The gate requires that question be settled by profiling rather than intuition,
10
+ * and it is not rhetorical — `ExecutionJournal` queues appends on a serial tail, so the
11
+ * interpreter is decoupled from write latency and a workload can write a great deal without
12
+ * writes ever constraining it.
13
+ *
14
+ * Wire it as effect-runtime middleware in your own app, wrap your store in
15
+ * `ProfilingEventStore`, run real traffic, and compare `totalEffectMs` against
16
+ * `totalWriteMs`. Batching the write path can only help if the latter dominates.
17
+ */
18
+ function effectTimingMiddleware() {
19
+ let calls = 0;
20
+ let totalEffectMs = 0;
21
+ let maxEffectMs = 0;
22
+ let inFlight = 0;
23
+ let peakConcurrentEffects = 0;
24
+ const byTool = new Map();
25
+ const middleware = async (ctx, input, next) => {
26
+ const started = performance.now();
27
+ inFlight++;
28
+ if (inFlight > peakConcurrentEffects)
29
+ peakConcurrentEffects = inFlight;
30
+ try {
31
+ return await next(input);
32
+ }
33
+ finally {
34
+ const elapsed = performance.now() - started;
35
+ inFlight--;
36
+ calls++;
37
+ totalEffectMs += elapsed;
38
+ if (elapsed > maxEffectMs)
39
+ maxEffectMs = elapsed;
40
+ // `stepId` is the tool identity at this seam; fall back so an unnamed effect still
41
+ // shows up rather than vanishing from the breakdown.
42
+ const tool = ctx.stepId || "<unknown>";
43
+ const entry = byTool.get(tool);
44
+ if (entry === undefined)
45
+ byTool.set(tool, { calls: 1, totalMs: elapsed });
46
+ else {
47
+ entry.calls++;
48
+ entry.totalMs += elapsed;
49
+ }
50
+ }
51
+ };
52
+ return {
53
+ middleware,
54
+ profile: () => ({
55
+ calls,
56
+ totalEffectMs,
57
+ maxEffectMs,
58
+ peakConcurrentEffects,
59
+ byTool: Object.fromEntries([...byTool].map(([k, v]) => [k, { ...v }])),
60
+ }),
61
+ reset: () => {
62
+ calls = 0;
63
+ totalEffectMs = 0;
64
+ maxEffectMs = 0;
65
+ peakConcurrentEffects = 0;
66
+ byTool.clear();
67
+ },
68
+ };
69
+ }
70
+ //# sourceMappingURL=profile.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"profile.js","sourceRoot":"","sources":["../src/profile.ts"],"names":[],"mappings":";;AAoCA,wDAiDC;AA/DD;;;;;;;;;;;;;GAaG;AACH,SAAgB,sBAAsB;IACpC,IAAI,KAAK,GAAG,CAAC,CAAC;IACd,IAAI,aAAa,GAAG,CAAC,CAAC;IACtB,IAAI,WAAW,GAAG,CAAC,CAAC;IACpB,IAAI,QAAQ,GAAG,CAAC,CAAC;IACjB,IAAI,qBAAqB,GAAG,CAAC,CAAC;IAC9B,MAAM,MAAM,GAAG,IAAI,GAAG,EAA8C,CAAC;IAErE,MAAM,UAAU,GAAqB,KAAK,EAAE,GAAG,EAAE,KAAK,EAAE,IAAI,EAAE,EAAE;QAC9D,MAAM,OAAO,GAAG,WAAW,CAAC,GAAG,EAAE,CAAC;QAClC,QAAQ,EAAE,CAAC;QACX,IAAI,QAAQ,GAAG,qBAAqB;YAAE,qBAAqB,GAAG,QAAQ,CAAC;QACvE,IAAI,CAAC;YACH,OAAO,MAAM,IAAI,CAAC,KAAK,CAAC,CAAC;QAC3B,CAAC;gBAAS,CAAC;YACT,MAAM,OAAO,GAAG,WAAW,CAAC,GAAG,EAAE,GAAG,OAAO,CAAC;YAC5C,QAAQ,EAAE,CAAC;YACX,KAAK,EAAE,CAAC;YACR,aAAa,IAAI,OAAO,CAAC;YACzB,IAAI,OAAO,GAAG,WAAW;gBAAE,WAAW,GAAG,OAAO,CAAC;YACjD,mFAAmF;YACnF,qDAAqD;YACrD,MAAM,IAAI,GAAG,GAAG,CAAC,MAAM,IAAI,WAAW,CAAC;YACvC,MAAM,KAAK,GAAG,MAAM,CAAC,GAAG,CAAC,IAAI,CAAC,CAAC;YAC/B,IAAI,KAAK,KAAK,SAAS;gBAAE,MAAM,CAAC,GAAG,CAAC,IAAI,EAAE,EAAE,KAAK,EAAE,CAAC,EAAE,OAAO,EAAE,OAAO,EAAE,CAAC,CAAC;iBACrE,CAAC;gBACJ,KAAK,CAAC,KAAK,EAAE,CAAC;gBACd,KAAK,CAAC,OAAO,IAAI,OAAO,CAAC;YAC3B,CAAC;QACH,CAAC;IACH,CAAC,CAAC;IAEF,OAAO;QACL,UAAU;QACV,OAAO,EAAE,GAAG,EAAE,CAAC,CAAC;YACd,KAAK;YACL,aAAa;YACb,WAAW;YACX,qBAAqB;YACrB,MAAM,EAAE,MAAM,CAAC,WAAW,CAAC,CAAC,GAAG,MAAM,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,EAAE,EAAE,GAAG,CAAC,EAAE,CAAC,CAAC,CAAC;SACvE,CAAC;QACF,KAAK,EAAE,GAAG,EAAE;YACV,KAAK,GAAG,CAAC,CAAC;YACV,aAAa,GAAG,CAAC,CAAC;YAClB,WAAW,GAAG,CAAC,CAAC;YAChB,qBAAqB,GAAG,CAAC,CAAC;YAC1B,MAAM,CAAC,KAAK,EAAE,CAAC;QACjB,CAAC;KACF,CAAC;AACJ,CAAC"}
@@ -0,0 +1,40 @@
1
+ import type { EffectMiddleware } from "@loomwork/effect-runtime";
2
+ /** Which requests share one bucket. */
3
+ export type RateLimitScope = "execution" | "tenant" | "workflow" | "global";
4
+ export interface RateLimitOptions {
5
+ /** Bucket size: the burst a scope may issue at once. Must be a positive integer. */
6
+ capacity: number;
7
+ /** Sustained rate: tokens added per second. Must be positive. */
8
+ refillPerSecond: number;
9
+ /**
10
+ * Who shares a bucket. Default `"execution"` (one runaway execution cannot starve the
11
+ * rest). `"tenant"` keys on `ctx.principal.tenant` — a request with no tenant shares ONE
12
+ * `"(no-tenant)"` bucket rather than escaping the limit. `"workflow"` keys on the flow id
13
+ * (the per-agent case); `"global"` is one bucket for the process.
14
+ */
15
+ scope?: RateLimitScope;
16
+ /** Injectable clock in ms (tests); defaults to `Date.now`. */
17
+ clock?: () => number;
18
+ /**
19
+ * Upper bound on live buckets. A fresh bucket is created per execution/tenant key, so an
20
+ * unbounded map would be a leak; the least-recently-used bucket is evicted past this size
21
+ * (an evicted key simply starts full again — it can only err toward permitting). Default 10_000.
22
+ */
23
+ maxBuckets?: number;
24
+ }
25
+ /**
26
+ * The typed, NON-retryable refusal for a rate-limit trip. It is the control plane speaking,
27
+ * not a transient fault: retrying immediately repeats it, so callers that classify errors by
28
+ * `name` (loom-ui's `isControlPlaneError`) must treat `"RateLimitExceededError"` as a refusal.
29
+ */
30
+ export declare class RateLimitExceededError extends Error {
31
+ readonly scope: RateLimitScope;
32
+ readonly key: string;
33
+ /** Milliseconds until one token is available again (a hint, never a promise). */
34
+ readonly retryAfterMs: number;
35
+ /** Honoured by `retryMiddleware`'s default classifier: an immediate retry repeats the refusal. */
36
+ readonly retryable = false;
37
+ constructor(scope: RateLimitScope, key: string, retryAfterMs: number);
38
+ }
39
+ export declare function rateLimitMiddleware(options: RateLimitOptions): EffectMiddleware;
40
+ //# sourceMappingURL=rateLimit.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"rateLimit.d.ts","sourceRoot":"","sources":["../src/rateLimit.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAiB,gBAAgB,EAAE,MAAM,0BAA0B,CAAC;AAgBhF,uCAAuC;AACvC,MAAM,MAAM,cAAc,GAAG,WAAW,GAAG,QAAQ,GAAG,UAAU,GAAG,QAAQ,CAAC;AAE5E,MAAM,WAAW,gBAAgB;IAC/B,oFAAoF;IACpF,QAAQ,EAAE,MAAM,CAAC;IACjB,iEAAiE;IACjE,eAAe,EAAE,MAAM,CAAC;IACxB;;;;;OAKG;IACH,KAAK,CAAC,EAAE,cAAc,CAAC;IACvB,8DAA8D;IAC9D,KAAK,CAAC,EAAE,MAAM,MAAM,CAAC;IACrB;;;;OAIG;IACH,UAAU,CAAC,EAAE,MAAM,CAAC;CACrB;AAED;;;;GAIG;AACH,qBAAa,sBAAuB,SAAQ,KAAK;IAC/C,QAAQ,CAAC,KAAK,EAAE,cAAc,CAAC;IAC/B,QAAQ,CAAC,GAAG,EAAE,MAAM,CAAC;IACrB,iFAAiF;IACjF,QAAQ,CAAC,YAAY,EAAE,MAAM,CAAC;IAC9B,kGAAkG;IAClG,QAAQ,CAAC,SAAS,SAAS;gBACf,KAAK,EAAE,cAAc,EAAE,GAAG,EAAE,MAAM,EAAE,YAAY,EAAE,MAAM;CAOrE;AAoBD,wBAAgB,mBAAmB,CAAC,OAAO,EAAE,gBAAgB,GAAG,gBAAgB,CA6C/E"}
@@ -0,0 +1,83 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.RateLimitExceededError = void 0;
4
+ exports.rateLimitMiddleware = rateLimitMiddleware;
5
+ /**
6
+ * The typed, NON-retryable refusal for a rate-limit trip. It is the control plane speaking,
7
+ * not a transient fault: retrying immediately repeats it, so callers that classify errors by
8
+ * `name` (loom-ui's `isControlPlaneError`) must treat `"RateLimitExceededError"` as a refusal.
9
+ */
10
+ class RateLimitExceededError extends Error {
11
+ scope;
12
+ key;
13
+ /** Milliseconds until one token is available again (a hint, never a promise). */
14
+ retryAfterMs;
15
+ /** Honoured by `retryMiddleware`'s default classifier: an immediate retry repeats the refusal. */
16
+ retryable = false;
17
+ constructor(scope, key, retryAfterMs) {
18
+ super(`Rate limit exceeded for ${scope} "${key}" — retry after ~${retryAfterMs}ms`);
19
+ this.name = "RateLimitExceededError";
20
+ this.scope = scope;
21
+ this.key = key;
22
+ this.retryAfterMs = retryAfterMs;
23
+ }
24
+ }
25
+ exports.RateLimitExceededError = RateLimitExceededError;
26
+ function keyOf(scope, ctx) {
27
+ switch (scope) {
28
+ case "execution":
29
+ return ctx.executionId;
30
+ case "workflow":
31
+ return ctx.workflowId;
32
+ case "tenant":
33
+ return ctx.principal?.tenant ?? "(no-tenant)";
34
+ case "global":
35
+ return "*";
36
+ }
37
+ }
38
+ function rateLimitMiddleware(options) {
39
+ const { capacity, refillPerSecond } = options;
40
+ if (!Number.isInteger(capacity) || capacity < 1) {
41
+ throw new Error(`rateLimitMiddleware: capacity must be a positive integer (got ${capacity})`);
42
+ }
43
+ if (!(refillPerSecond > 0) || !Number.isFinite(refillPerSecond)) {
44
+ throw new Error(`rateLimitMiddleware: refillPerSecond must be a positive number (got ${refillPerSecond})`);
45
+ }
46
+ const scope = options.scope ?? "execution";
47
+ const now = options.clock ?? (() => Date.now());
48
+ const maxBuckets = options.maxBuckets ?? 10_000;
49
+ // Map iteration order is insertion order, so delete+set on every touch keeps the OLDEST
50
+ // key first — an O(1) LRU without a second structure.
51
+ const buckets = new Map();
52
+ return async (ctx, _input, next) => {
53
+ const key = keyOf(scope, ctx);
54
+ const t = now();
55
+ const bucket = buckets.get(key) ?? { tokens: capacity, at: t };
56
+ buckets.delete(key);
57
+ const elapsedSec = Math.max(0, t - bucket.at) / 1000;
58
+ bucket.tokens = Math.min(capacity, bucket.tokens + elapsedSec * refillPerSecond);
59
+ bucket.at = t;
60
+ const admitted = bucket.tokens >= 1;
61
+ if (admitted)
62
+ bucket.tokens -= 1;
63
+ buckets.set(key, bucket);
64
+ if (buckets.size > maxBuckets) {
65
+ const oldest = buckets.keys().next().value;
66
+ if (oldest !== undefined)
67
+ buckets.delete(oldest);
68
+ }
69
+ if (!admitted) {
70
+ const retryAfterMs = Math.ceil(((1 - bucket.tokens) / refillPerSecond) * 1000);
71
+ ctx.emit("SAFETY_BLOCKED", {
72
+ stepId: ctx.stepId,
73
+ validatorId: "rate-limit",
74
+ phase: "input",
75
+ reason: `rate limit exceeded for ${scope} "${key}"`,
76
+ category: "rate-limit",
77
+ });
78
+ throw new RateLimitExceededError(scope, key, retryAfterMs);
79
+ }
80
+ return next();
81
+ };
82
+ }
83
+ //# sourceMappingURL=rateLimit.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"rateLimit.js","sourceRoot":"","sources":["../src/rateLimit.ts"],"names":[],"mappings":"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"}