{"version":3,"file":"budget-ledger.d.ts","sourceRoot":"","sources":["../../../../src/core/long-horizon/adaptive/budget-ledger.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;GAYG;AAEH,OAAO,KAAK,EACX,iBAAiB,EACjB,cAAc,EACd,gBAAgB,EAChB,eAAe,EACf,gBAAgB,EAChB,MAAM,YAAY,CAAC;AAEpB,MAAM,WAAW,YAAY;IAC5B,KAAK,EAAE,MAAM,CAAC;IACd,OAAO,EAAE,SAAS,iBAAiB,EAAE,CAAC;CACtC;AAwBD,wBAAgB,iBAAiB,IAAI,SAAS,cAAc,EAAE,CAE7D;AAED,wBAAgB,kBAAkB,CAAC,KAAK,EAAE,MAAM,GAAG,YAAY,CAE9D;AAkBD,MAAM,WAAW,YAAY;IAC5B,MAAM,EAAE,YAAY,CAAC;IACrB,QAAQ,EAAE,OAAO,CAAC;IAClB,MAAM,CAAC,EAAE,MAAM,CAAC;CAChB;AAED;;;;;;GAMG;AACH,wBAAgB,WAAW,CAAC,MAAM,EAAE,YAAY,EAAE,KAAK,EAAE,iBAAiB,GAAG,YAAY,CAoBxF;AAED;;;;;;;GAOG;AACH,wBAAgB,cAAc,CAC7B,MAAM,EAAE,YAAY,EACpB,gBAAgB,EAAE,MAAM,EACxB,MAAM,EAAE,IAAI,CAAC,iBAAiB,EAAE,mBAAmB,GAAG,SAAS,GAAG,OAAO,CAAC,GAAG;IAAE,OAAO,CAAC,EAAE,MAAM,CAAA;CAAE,GAC/F,YAAY,CAcd;AAED,MAAM,WAAW,KAAK;IACrB,KAAK,EAAE,MAAM,CAAC;IACd,SAAS,EAAE,MAAM,CAAC;IAClB,MAAM,EAAE,MAAM,CAAC;CACf;AAED;;;;GAIG;AACH,wBAAgB,QAAQ,CAAC,MAAM,EAAE,YAAY,EAAE,QAAQ,EAAE,cAAc,GAAG,KAAK,CAW9E;AAED,wBAAgB,WAAW,CAAC,MAAM,EAAE,YAAY,EAAE,QAAQ,EAAE,cAAc,GAAG,MAAM,CAElF;AAED;;;GAGG;AACH,wBAAgB,oBAAoB,CAAC,MAAM,EAAE,YAAY,GAAG,MAAM,CAYjE;AAED,MAAM,WAAW,eAAe;IAC/B,QAAQ,EAAE,gBAAgB,EAAE,CAAC;IAC7B,OAAO,EAAE,OAAO,CAAC;IACjB,WAAW,EAAE,gBAAgB,EAAE,CAAC;IAChC,WAAW,EAAE,gBAAgB,EAAE,CAAC;CAChC;AAED;;;;GAIG;AACH,wBAAgB,kBAAkB,CACjC,MAAM,EAAE,YAAY,EACpB,UAAU,EAAE,gBAAgB,EAC5B,OAAO,GAAE;IAAE,QAAQ,CAAC,EAAE,MAAM,CAAA;CAAO,GACjC,eAAe,CAgCjB;AAMD,MAAM,WAAW,qBAAqB;IACrC,MAAM,EAAE,OAAO,CAAC,eAAe,CAAC,CAAC;IACjC,IAAI,CAAC,EAAE,OAAO,CAAC,eAAe,CAAC,CAAC;IAChC,GAAG,CAAC,EAAE,OAAO,CAAC,eAAe,CAAC,CAAC;IAC/B,KAAK,CAAC,EAAE,OAAO,CAAC,eAAe,CAAC,CAAC;IACjC,IAAI,CAAC,EAAE,OAAO,CAAC,eAAe,CAAC,CAAC;IAChC,QAAQ,CAAC,EAAE,OAAO,CAAC,eAAe,CAAC,CAAC;CACpC;AAED,MAAM,WAAW,gBAAgB;IAChC,SAAS,EAAE,OAAO,CAAC,eAAe,CAAC,CAAC;IACpC,OAAO,EAAE,OAAO,CAAC;IACjB,OAAO,EAAE,MAAM,EAAE,CAAC;IAClB,8EAA8E;IAC9E,iBAAiB,EAAE,MAAM,EAAE,CAAC;CAC5B;AAED;;;;GAIG;AACH,wBAAgB,sBAAsB,CAAC,KAAK,EAAE,qBAAqB,GAAG,gBAAgB,CAsCrF;AAED;;;;GAIG;AACH,wBAAgB,mBAAmB,CAAC,KAAK,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,GAAG;IAAE,aAAa,EAAE,MAAM,CAAC;IAAC,OAAO,EAAE,MAAM,CAAA;CAAE,CAG9G;AAED,6EAA6E;AAC7E,wBAAgB,WAAW,CAC1B,KAAK,EAAE,MAAM,EACb,QAAQ,EAAE,cAAc,EACxB,IAAI,EAAE,MAAM,EACZ,SAAS,EAAE,MAAM,EACjB,4BAA4B,EAAE,OAAO;;;;;;;;EAWrC","sourcesContent":["/**\n * Durable execution-budget ledger.\n *\n * An append-only, idempotent, replay-safe accounting ledger for durable\n * execution budgets. Totals are always derived by replaying entries — never by\n * mutating a running counter — so a resume after interruption cannot double\n * charge and a cancelled run does not erase consumed budget.\n *\n * Estimated amounts are recorded first and reconciled against provider actuals\n * via `reconcileEntry` (idempotent). Unknown pricing is classified explicitly\n * rather than guessed. Cached-input usage is tracked separately. Subagent usage\n * is charged to both the child and the parent.\n */\n\nimport type {\n\tBudgetLedgerEntry,\n\tBudgetResource,\n\tBudgetThresholds,\n\tExecutionBudget,\n\tThresholdVerdict,\n} from \"./types.js\";\n\nexport interface BudgetLedger {\n\trunId: string;\n\tentries: readonly BudgetLedgerEntry[];\n}\n\n/** Allocatable budget dimensions (numeric, non-nesting). */\nconst RESOURCE_ORDER: readonly BudgetResource[] = [\n\t\"maxCostUsd\",\n\t\"maxInputTokens\",\n\t\"maxOutputTokens\",\n\t\"maxCachedInputTokens\",\n\t\"maxModelTurns\",\n\t\"maxToolCalls\",\n\t\"maxMutatingToolCalls\",\n\t\"maxWallTimeMs\",\n\t\"maxProviderRetries\",\n\t\"maxStrategyPivots\",\n\t\"maxModelEscalations\",\n\t\"maxSubagentRuns\",\n\t\"maxConcurrentSubagents\",\n\t\"maxWebSearches\",\n\t\"maxWebFetches\",\n\t\"maxBrowserRenders\",\n\t\"maxLspRequests\",\n\t\"maxBackgroundJobs\",\n];\n\nexport function resourcesOfBudget(): readonly BudgetResource[] {\n\treturn RESOURCE_ORDER;\n}\n\nexport function createBudgetLedger(runId: string): BudgetLedger {\n\treturn { runId, entries: Object.freeze([]) };\n}\n\nfunction freezeEntry(e: BudgetLedgerEntry): BudgetLedgerEntry {\n\treturn Object.freeze({\n\t\tentryId: String(e.entryId),\n\t\trunId: String(e.runId),\n\t\tphaseId: e.phaseId === undefined ? undefined : String(e.phaseId),\n\t\trole: e.role === undefined ? undefined : String(e.role),\n\t\tresource: e.resource,\n\t\tamount: e.amount,\n\t\testimatedOrActual: e.estimatedOrActual,\n\t\tprovider: e.provider === undefined ? undefined : String(e.provider),\n\t\tmodel: e.model === undefined ? undefined : String(e.model),\n\t\tsourceEventId: String(e.sourceEventId),\n\t\trecordedAt: String(e.recordedAt),\n\t});\n}\n\nexport interface AppendResult {\n\tledger: BudgetLedger;\n\tappended: boolean;\n\treason?: string;\n}\n\n/**\n * Append a single durable entry.\n *\n * Idempotent by `entryId`: appending an entry that already exists returns the\n * same ledger with `appended: false`, so replay or resume can never double\n * charge. Amounts are snapshotted defensively.\n */\nexport function appendEntry(ledger: BudgetLedger, entry: BudgetLedgerEntry): AppendResult {\n\tif (!RESOURCE_ORDER.includes(entry.resource)) {\n\t\treturn { ledger, appended: false, reason: `UNKNOWN_RESOURCE: ${entry.resource}` };\n\t}\n\tif (!Number.isFinite(entry.amount)) {\n\t\treturn { ledger, appended: false, reason: \"NON_FINITE_AMOUNT\" };\n\t}\n\tif (entry.estimatedOrActual !== \"estimated\" && entry.estimatedOrActual !== \"actual\") {\n\t\treturn { ledger, appended: false, reason: \"INVALID_ACCOUNTING_KIND\" };\n\t}\n\tfor (const existing of ledger.entries) {\n\t\tif (existing.entryId === entry.entryId && existing.runId === entry.runId) {\n\t\t\treturn { ledger, appended: false, reason: \"DUPLICATE_ENTRY_ID\" };\n\t\t}\n\t}\n\tconst next: BudgetLedger = {\n\t\trunId: ledger.runId,\n\t\tentries: Object.freeze([...ledger.entries, freezeEntry(entry)]),\n\t};\n\treturn { ledger: next, appended: true };\n}\n\n/**\n * Reconcile an estimated entry against a provider actual.\n *\n * The actual is appended as a new (separate) entry referencing the same\n * `sourceEventId` with kind `actual`. Reformatting of the provisional estimate\n * preserves the audit trail. Idempotent: re-applying the same actual is\n * rejected as a duplicate entryId.\n */\nexport function reconcileEntry(\n\tledger: BudgetLedger,\n\testimatedEntryId: string,\n\tactual: Omit<BudgetLedgerEntry, \"estimatedOrActual\" | \"entryId\" | \"runId\"> & { entryId?: string },\n): AppendResult {\n\tconst estimate = ledger.entries.find((e) => e.entryId === estimatedEntryId);\n\tif (!estimate || estimate.estimatedOrActual !== \"estimated\") {\n\t\treturn { ledger, appended: false, reason: \"ESTIMATE_NOT_FOUND\" };\n\t}\n\tconst actualEntryId = actual.entryId ?? `${estimatedEntryId}:actual`;\n\treturn appendEntry(ledger, {\n\t\t...actual,\n\t\tentryId: actualEntryId,\n\t\trunId: estimate.runId,\n\t\tresource: actual.resource,\n\t\testimatedOrActual: \"actual\",\n\t\tsourceEventId: estimate.sourceEventId,\n\t});\n}\n\nexport interface Usage {\n\ttotal: number;\n\testimated: number;\n\tactual: number;\n}\n\n/**\n * Derive usage for a resource by replay (sum only the most authoritative kind\n * per entry). Actual supersedes the estimated entry that shares a sourceEventId\n * only for reporting clarity; totals count each entry once by idempotency.\n */\nexport function getUsage(ledger: BudgetLedger, resource: BudgetResource): Usage {\n\tlet total = 0;\n\tlet estimated = 0;\n\tlet actual = 0;\n\tfor (const e of ledger.entries) {\n\t\tif (e.resource !== resource) continue;\n\t\ttotal += e.amount;\n\t\tif (e.estimatedOrActual === \"actual\") actual += e.amount;\n\t\telse estimated += e.amount;\n\t}\n\treturn { total, estimated, actual };\n}\n\nexport function sumResource(ledger: BudgetLedger, resource: BudgetResource): number {\n\treturn getUsage(ledger, resource).total;\n}\n\n/**\n * Count distinct provider/model-billed estimated entries reconciled with an\n * actual for the same sourceEventId, so we can assert no-persistent-overshoot.\n */\nexport function resolvedSourceEvents(ledger: BudgetLedger): number {\n\tconst actuals = new Set<string>();\n\tfor (const e of ledger.entries) {\n\t\tif (e.estimatedOrActual === \"actual\") actuals.add(e.sourceEventId);\n\t}\n\tconst resolved = new Set<string>();\n\tfor (const e of ledger.entries) {\n\t\tif (e.estimatedOrActual === \"estimated\" && actuals.has(e.sourceEventId)) {\n\t\t\tresolved.add(e.sourceEventId);\n\t\t}\n\t}\n\treturn resolved.size;\n}\n\nexport interface ThresholdOutput {\n\tverdicts: ThresholdVerdict[];\n\tblocked: boolean;\n\thardReached: ThresholdVerdict[];\n\tsoftReached: ThresholdVerdict[];\n}\n\n/**\n * Evaluate soft/hard thresholds for every budget dimension given thresholds.\n * Reserved capacity (finalization + recovery) is reserved from the hard\n * threshold before ordinary consumption is judged.\n */\nexport function evaluateThresholds(\n\tledger: BudgetLedger,\n\tthresholds: BudgetThresholds,\n\toptions: { reserved?: number } = {},\n): ThresholdOutput {\n\tconst reserved = options.reserved ?? 0;\n\tconst verdicts: ThresholdVerdict[] = [];\n\tconst hardReached: ThresholdVerdict[] = [];\n\tconst softReached: ThresholdVerdict[] = [];\n\tlet blocked = false;\n\n\tfor (const resource of RESOURCE_ORDER) {\n\t\tconst used = sumResource(ledger, resource);\n\t\tconst soft = thresholds.soft;\n\t\tconst hard = thresholds.hard;\n\n\t\tif (hard !== undefined) {\n\t\t\tconst effectiveHard = Math.max(0, hard - reserved);\n\t\t\tif (used >= effectiveHard) {\n\t\t\t\tconst v: ThresholdVerdict = { kind: \"hard\", resource, used, threshold: effectiveHard };\n\t\t\t\tverdicts.push(v);\n\t\t\t\thardReached.push(v);\n\t\t\t\tblocked = true;\n\t\t\t\tcontinue;\n\t\t\t}\n\t\t}\n\t\tif (soft !== undefined && used >= soft) {\n\t\t\tconst v: ThresholdVerdict = { kind: \"soft\", resource, used, threshold: soft };\n\t\t\tverdicts.push(v);\n\t\t\tsoftReached.push(v);\n\t\t\tcontinue;\n\t\t}\n\t\tverdicts.push({ kind: \"ok\" });\n\t}\n\n\treturn { verdicts, blocked, hardReached, softReached };\n}\n\n// =============================================================================\n// Budget hierarchy allocation\n// =============================================================================\n\nexport interface BudgetAllocationInput {\n\tglobal: Partial<ExecutionBudget>;\n\tuser?: Partial<ExecutionBudget>;\n\trun?: Partial<ExecutionBudget>;\n\tphase?: Partial<ExecutionBudget>;\n\trole?: Partial<ExecutionBudget>;\n\tsubagent?: Partial<ExecutionBudget>;\n}\n\nexport interface AllocationResult {\n\teffective: Partial<ExecutionBudget>;\n\tblocked: boolean;\n\treasons: string[];\n\t/** A manually-configured value may never exceed the unallocated remainder. */\n\tremainderOverflow: string[];\n}\n\n/**\n * Resolve the effective budget by intersecting the hierarchy: each level is\n * capped by the unallocated remainder of its parent. A child can never request\n * more than the parent has left. Lower levels may only reduce.\n */\nexport function resolveBudgetHierarchy(input: BudgetAllocationInput): AllocationResult {\n\tconst chain: Array<Partial<ExecutionBudget> | undefined> = [\n\t\tinput.global,\n\t\tinput.user,\n\t\tinput.run,\n\t\tinput.phase,\n\t\tinput.role,\n\t\tinput.subagent,\n\t];\n\tconst effective: Partial<ExecutionBudget> = {};\n\tconst remainderOverflow: string[] = [];\n\n\tfor (const resource of RESOURCE_ORDER) {\n\t\tlet remaining: number | undefined;\n\t\tlet finalValue: number | undefined;\n\t\tfor (const layer of chain) {\n\t\t\tconst layerValue = layer?.[resource];\n\t\t\tif (layerValue === undefined) continue;\n\t\t\tif (remaining === undefined) {\n\t\t\t\tremaining = layerValue;\n\t\t\t\tfinalValue = layerValue;\n\t\t\t\tcontinue;\n\t\t\t}\n\t\t\t// Child cannot exceed unallocated parent remainder.\n\t\t\tif (layerValue > remaining) {\n\t\t\t\tremainderOverflow.push(`${resource}: child ${layerValue} exceeds remainder ${remaining}`);\n\t\t\t\tfinalValue = remaining;\n\t\t\t\tcontinue;\n\t\t\t}\n\t\t\tfinalValue = layerValue;\n\t\t\tremaining = layerValue;\n\t\t}\n\t\tif (finalValue !== undefined) {\n\t\t\t(effective as Record<string, unknown>)[resource] = finalValue;\n\t\t}\n\t}\n\n\treturn { effective, blocked: false, reasons: [], remainderOverflow };\n}\n\n/**\n * Reserve capacity for finalization. Returns the allowed-discretionary value\n * (hard minus reserve) and the reserve value, and never lets ordinary execution\n * spend the reserve: ordinary use is bounded by the reserved-adjusted hard.\n */\nexport function reserveFinalization(limit: number, reserve: number): { discretionary: number; reserve: number } {\n\tconst r = Math.max(0, reserve);\n\treturn { discretionary: Math.max(0, limit - r), reserve: r };\n}\n\n/** Mark a typed budget block when a hard limit is reached for a resource. */\nexport function budgetBlock(\n\trunId: string,\n\tresource: BudgetResource,\n\tused: number,\n\tthreshold: number,\n\tfinalizationReserveAvailable: boolean,\n) {\n\treturn {\n\t\tblocked: true as const,\n\t\trunId,\n\t\tresource,\n\t\tused,\n\t\tthreshold,\n\t\treasonCode: \"HARD_LIMIT_REACHED\" as const,\n\t\tfinalizationReserveAvailable,\n\t};\n}\n"]}