| import { KnowledgeBudgetExceeded } from "./errors.ts"; |
| import type { KnowledgeKind } from "./types.ts"; |
|
|
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| const DEFAULT_LIMIT = 100; |
| const DEFAULT_WINDOW_MS = 60_000; |
| const DEFAULT_WARN_RATIO = 0.8; |
|
|
| const queues = new Map<string, number[]>(); |
| |
| |
| const warnedAt = new Map<string, number>(); |
|
|
| function effectiveLimit(): number { |
| const raw = process.env["DOATLAS_EK_BUDGET_LIMIT"]; |
| if (!raw) return DEFAULT_LIMIT; |
| const n = Number(raw); |
| return Number.isFinite(n) && n > 0 ? Math.floor(n) : DEFAULT_LIMIT; |
| } |
|
|
| function effectiveWindowMs(): number { |
| const raw = process.env["DOATLAS_EK_BUDGET_WINDOW_MS"]; |
| if (!raw) return DEFAULT_WINDOW_MS; |
| const n = Number(raw); |
| return Number.isFinite(n) && n > 0 ? Math.floor(n) : DEFAULT_WINDOW_MS; |
| } |
|
|
| function effectiveWarnRatio(): number { |
| const raw = process.env["DOATLAS_EK_BUDGET_WARN_RATIO"]; |
| if (!raw) return DEFAULT_WARN_RATIO; |
| const n = Number(raw); |
| return Number.isFinite(n) && n > 0 && n <= 1 ? n : DEFAULT_WARN_RATIO; |
| } |
|
|
| |
| |
| |
| |
| |
| |
| export function registerCall( |
| capabilityId: string, |
| adapter: KnowledgeKind, |
| nowOverride?: number, |
| ): { exceeded: boolean; count: number; limit: number; windowMs: number; warnedThisCall: boolean } { |
| const limit = effectiveLimit(); |
| const windowMs = effectiveWindowMs(); |
| const warnAt = Math.max(1, Math.floor(limit * effectiveWarnRatio())); |
| const key = capabilityId; |
| const now = nowOverride ?? Date.now(); |
| let q = queues.get(key); |
| if (!q) { |
| q = []; |
| queues.set(key, q); |
| } |
| |
| const cutoff = now - windowMs; |
| while (q.length > 0 && q[0]! <= cutoff) q.shift(); |
| q.push(now); |
| const count = q.length; |
| const exceeded = count > limit; |
| |
| let warnedThisCall = false; |
| if (count >= warnAt && !exceeded) { |
| const oldest = q[0] ?? now; |
| const lastWarnedFor = warnedAt.get(key); |
| if (lastWarnedFor === undefined || lastWarnedFor < oldest) { |
| |
| warnedAt.set(key, oldest); |
| warnedThisCall = true; |
| |
| console.warn( |
| `[ek.budget.warn] capability=${capabilityId} adapter=${adapter} ` + |
| `count=${count}/${limit} window_ms=${windowMs} warn_at=${warnAt} ` + |
| `— approaching per-capability EK budget`, |
| ); |
| } |
| } |
| return { exceeded, count, limit, windowMs, warnedThisCall }; |
| } |
|
|
| |
| |
| |
| |
| |
| |
| |
| |
| export function enforceBudget(capabilityId: string, adapter: KnowledgeKind): void { |
| const r = registerCall(capabilityId, adapter); |
| if (r.exceeded) { |
| |
| console.warn( |
| `[ek.budget.exceeded] capability=${capabilityId} adapter=${adapter} ` + |
| `count=${r.count}/${r.limit} window_ms=${r.windowMs} — throwing KnowledgeBudgetExceeded`, |
| ); |
| |
| |
| void import("./telemetry.ts") |
| .then((mod) => { |
| try { |
| return mod.recordCall({ |
| adapter, |
| capabilityId, |
| latencyMs: 0, |
| hitCount: 0, |
| cacheHit: null, |
| error: "budget_exceeded", |
| paramsFingerprint: "", |
| }); |
| } catch { |
| return undefined; |
| } |
| }) |
| .catch(() => undefined); |
| throw new KnowledgeBudgetExceeded( |
| capabilityId, |
| adapter, |
| Math.round(r.windowMs / 1000), |
| r.limit, |
| ); |
| } |
| } |
|
|
| |
| export function _resetBudgetCounters(): void { |
| queues.clear(); |
| warnedAt.clear(); |
| } |
|
|