@rulvar/core 1.5.2 → 1.7.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/dist/index.js CHANGED
@@ -5484,6 +5484,7 @@ var Replayer = class {
5484
5484
  if (patch.usageApprox !== void 0) entry.usageApprox = patch.usageApprox;
5485
5485
  if (patch.servedBy !== void 0) entry.servedBy = patch.servedBy;
5486
5486
  if (patch.usageByModel !== void 0) entry.usageByModel = patch.usageByModel;
5487
+ if (patch.costAttribution !== void 0) entry.costAttribution = patch.costAttribution;
5487
5488
  if (patch.transcriptRef !== void 0) entry.transcriptRef = patch.transcriptRef;
5488
5489
  if (patch.checkpointRef !== void 0) entry.checkpointRef = patch.checkpointRef;
5489
5490
  if (patch.artifacts !== void 0) entry.artifacts = toJournalValue(patch.artifacts, "terminal artifacts");
@@ -6147,19 +6148,28 @@ var FileTranscriptStore = class {
6147
6148
  //#endregion
6148
6149
  //#region src/engine/cost-report.ts
6149
6150
  /**
6150
- * CostReport builders (M5-T03). Two
6151
- * sources, one shape:
6151
+ * CostReport builders (M5-T03; follow-up: one pure fold).
6152
6152
  *
6153
- * - `buildCostReport` folds the LIVE per-run attribution buckets (ctx
6154
- * accumulates byModel/byPhase/byAgentType/byRole per call) around the
6155
- * ledger-fold total, so report totals equal the budget ledger fold
6156
- * totals exactly at settle.
6157
- * - `costReportFromJournal` is the pure journal fold for STORED runs
6158
- * (shells, `rulvar inspect`): terminal usage priced per servedBy with
6159
- * abandoned subtrees contributing zero, exactly like the kernel's
6160
- * ledger fold. Phase, agentType, and role attribution are live-run
6161
- * facts that entries do not carry, so those buckets are empty here;
6162
- * byRole and the orchestrator block complete in M7 (DEF-7).
6153
+ * `costReportFromJournal` is THE report: a pure fold over terminal
6154
+ * entries that both the engine's settle path and stored-run inspection
6155
+ * (shells, `rulvar inspect`) use, so a replayed run reports the same
6156
+ * numbers byte for byte. Terminal entries carry their attribution facts
6157
+ * (`costAttribution`: phase, agent type, primary role, budget account,
6158
+ * finalize-reserve flag) exactly so this fold can reproduce every
6159
+ * breakdown without live state; entries written before the facts
6160
+ * shipped fold under the documented fallbacks (empty phase, 'unknown'
6161
+ * agent type, role 'loop').
6162
+ *
6163
+ * Inclusion policy, applied to the total and EVERY breakdown alike:
6164
+ * terminal usage exactly once, priced per serving slice, entries under
6165
+ * abandoned subtrees contribute zero (their spend is tracked separately
6166
+ * in the abandoned-spend ledger the orchestrator sees). Attempts that
6167
+ * were paid but never abandoned (a cancelled root attempt, a dangling
6168
+ * child) are real spend and stay included everywhere.
6169
+ *
6170
+ * `buildCostReport` folds the LIVE per-run attribution buckets around
6171
+ * the ledger total; it remains for hosts that accumulated their own
6172
+ * `CostAttribution`, but the engine no longer builds outcomes from it.
6163
6173
  *
6164
6174
  * Unpriced models surface in `unpriced`, never as a silent zero.
6165
6175
  */
@@ -6174,14 +6184,9 @@ const ROLES = [
6174
6184
  function emptyByRole() {
6175
6185
  return Object.fromEntries(ROLES.map((role) => [role, 0]));
6176
6186
  }
6177
- function zeroOrchestrator() {
6178
- return {
6179
- spentUsd: 0,
6180
- share: 0,
6181
- wakes: 0,
6182
- forcedFinish: false,
6183
- reserveUsedUsd: 0
6184
- };
6187
+ /** The orchestrator sub-account naming rule of makeOrchestratorWorkflow. */
6188
+ function isOrchestratorAccount(scope) {
6189
+ return scope === "orchestrator" || scope.endsWith("/orchestrator");
6185
6190
  }
6186
6191
  /** Folds the per-run attribution buckets into the normative CostReport. */
6187
6192
  function buildCostReport(attribution, totalUsd) {
@@ -6207,16 +6212,30 @@ function buildCostReport(attribution, totalUsd) {
6207
6212
  };
6208
6213
  }
6209
6214
  /**
6210
- * The pure journal fold: byModel and totals from terminal entries, the
6211
- * same summation the kernel ledger uses (terminal usage exactly once,
6212
- * priced per servedBy, abandoned subtrees contribute zero).
6215
+ * The pure journal fold: the complete CostReport from terminal entries,
6216
+ * the same summation the kernel ledger uses (terminal usage exactly
6217
+ * once, priced per servedBy slice, abandoned subtrees contribute zero).
6218
+ * The orchestrator block folds too: spend attributed to the
6219
+ * orchestrator sub-account, the reserve-funded share of it, the armed
6220
+ * wake count, and the at-cap freeze flag from the journaled cap
6221
+ * decision, so a replay-only resume reproduces the block instead of
6222
+ * reading this process's live accounts (which a replay never charges).
6213
6223
  */
6214
6224
  function costReportFromJournal(entries, priceUsd) {
6215
6225
  const abandonFold = buildAbandonFold(entries);
6216
6226
  const byModel = {};
6227
+ const byPhase = {};
6228
+ const byAgentType = {};
6229
+ const byRole = emptyByRole();
6217
6230
  const unpriced = [];
6218
6231
  let totalUsd = 0;
6232
+ let orchestratorSpentUsd = 0;
6233
+ let reserveUsedUsd = 0;
6234
+ let wakes = 0;
6235
+ let forcedFinish = false;
6219
6236
  for (const entry of entries) {
6237
+ if (entry.kind === "decision" && entry.value?.decisionType === "orchestrator_budget_cap") forcedFinish = true;
6238
+ if (entry.kind === "external" && entry.status === "suspended" && typeof entry.value?.key === "string" && (entry.value.key.startsWith("wake:") || entry.value.key.includes(":wake:"))) wakes += 1;
6220
6239
  if (entry.kind !== "resolution" && entry.kind !== "abandon" && abandonFold.isAbandoned(entry.ref ?? entry.seq)) continue;
6221
6240
  if (entry.status === "running" || entry.usage === void 0) continue;
6222
6241
  const priced = priceEntryUsage(entry, priceUsd);
@@ -6226,14 +6245,30 @@ function costReportFromJournal(entries, priceUsd) {
6226
6245
  });
6227
6246
  for (const slice of priced.priced) byModel[slice.servedBy] = (byModel[slice.servedBy] ?? 0) + slice.usd;
6228
6247
  totalUsd += priced.usd;
6248
+ const facts = entry.costAttribution;
6249
+ const phase = facts?.phase ?? "";
6250
+ byPhase[phase] = (byPhase[phase] ?? 0) + priced.usd;
6251
+ const agentType = facts?.agentType ?? "unknown";
6252
+ byAgentType[agentType] = (byAgentType[agentType] ?? 0) + priced.usd;
6253
+ byRole[facts?.role ?? "loop"] += priced.usd;
6254
+ if (facts?.budgetAccount !== void 0 && isOrchestratorAccount(facts.budgetAccount)) {
6255
+ orchestratorSpentUsd += priced.usd;
6256
+ if (facts.finalizeReserve === true) reserveUsedUsd += priced.usd;
6257
+ }
6229
6258
  }
6230
6259
  return {
6231
6260
  totalUsd,
6232
6261
  byModel,
6233
- byPhase: {},
6234
- byAgentType: {},
6235
- byRole: emptyByRole(),
6236
- orchestrator: zeroOrchestrator(),
6262
+ byPhase,
6263
+ byAgentType,
6264
+ byRole,
6265
+ orchestrator: {
6266
+ spentUsd: orchestratorSpentUsd,
6267
+ share: orchestratorSpentUsd / Math.max(totalUsd, .01),
6268
+ wakes,
6269
+ forcedFinish,
6270
+ reserveUsedUsd
6271
+ },
6237
6272
  unpriced
6238
6273
  };
6239
6274
  }
@@ -6495,15 +6530,51 @@ function fallbackTriggerOf(outcome) {
6495
6530
  function resolvePricing(ref, table, capsPricing) {
6496
6531
  return table?.models[ref] ?? capsPricing;
6497
6532
  }
6498
- /**
6499
- * Dollars from normalized usage against one pricing row (the adapter
6500
- * normalized the usage; inputTokens is the
6501
- * full prompt). Cache writes price at the 5m premium rate; the 1h rate
6502
- * applies where a provider distinguishes it in usage, which the
6533
+ /** The tier a full prompt lands in: the highest threshold strictly below it. */
6534
+ function tierFor(pricing, inputTokens) {
6535
+ let tier;
6536
+ for (const candidate of pricing.tiers ?? []) if (inputTokens > candidate.aboveInputTokens && (tier === void 0 || candidate.aboveInputTokens > tier.aboveInputTokens)) tier = candidate;
6537
+ return tier;
6538
+ }
6539
+ /**
6540
+ * Dollars from normalized usage against one pricing row. Under the Usage
6541
+ * invariant inputTokens is the FULL prompt including cache reads and
6542
+ * writes, so the input rate bills only the uncached remainder and cache
6543
+ * tokens bill at their own rates, never twice; a row that omits a cache
6544
+ * rate bills those tokens at the plain input rate rather than silently
6545
+ * for free. A row may carry long-context tiers: the highest threshold
6546
+ * strictly below the full prompt re-prices the ENTIRE request
6547
+ * (input-side rates scale by inputMultiplier, the output rate by
6548
+ * outputMultiplier). Cache writes price at the 5m premium rate; the 1h
6549
+ * rate applies where a provider distinguishes it in usage, which the
6503
6550
  * canonical Usage does not yet carry.
6504
6551
  */
6505
6552
  function priceUsdOf(pricing, usage) {
6506
- return usage.inputTokens / 1e6 * pricing.inputUsdPerMTok + usage.outputTokens / 1e6 * pricing.outputUsdPerMTok + usage.cacheReadTokens / 1e6 * (pricing.cacheReadUsdPerMTok ?? 0) + usage.cacheWriteTokens / 1e6 * (pricing.cacheWriteUsdPerMTok ?? 0);
6553
+ const tier = tierFor(pricing, usage.inputTokens);
6554
+ const inputMul = tier?.inputMultiplier ?? 1;
6555
+ const outputMul = tier?.outputMultiplier ?? 1;
6556
+ return Math.max(0, usage.inputTokens - usage.cacheReadTokens - usage.cacheWriteTokens) / 1e6 * pricing.inputUsdPerMTok * inputMul + usage.outputTokens / 1e6 * pricing.outputUsdPerMTok * outputMul + usage.cacheReadTokens / 1e6 * (pricing.cacheReadUsdPerMTok ?? pricing.inputUsdPerMTok) * inputMul + usage.cacheWriteTokens / 1e6 * (pricing.cacheWriteUsdPerMTok ?? pricing.inputUsdPerMTok) * inputMul;
6557
+ }
6558
+ /**
6559
+ * The output tokens `remainingUsd` still buys from one pricing row after
6560
+ * paying for an estimated prompt of `estimatedInputTokens`, priced with
6561
+ * the same tier rules as settlement (the tier is selected by the
6562
+ * estimated prompt). Floored to whole tokens; zero or negative means not
6563
+ * even one output token fits, so the turn must not be dispatched.
6564
+ * Undefined when the row prices output at zero (a free model needs no
6565
+ * output bound).
6566
+ */
6567
+ function affordableOutputTokens(pricing, remainingUsd, estimatedInputTokens) {
6568
+ const tier = tierFor(pricing, estimatedInputTokens);
6569
+ const outputRate = pricing.outputUsdPerMTok * (tier?.outputMultiplier ?? 1);
6570
+ if (outputRate <= 0) return;
6571
+ const inputUsd = priceUsdOf(pricing, {
6572
+ inputTokens: estimatedInputTokens,
6573
+ outputTokens: 0,
6574
+ cacheReadTokens: 0,
6575
+ cacheWriteTokens: 0
6576
+ });
6577
+ return Math.floor((remainingUsd - inputUsd) / outputRate * 1e6);
6507
6578
  }
6508
6579
  //#endregion
6509
6580
  //#region src/model/profile-card.ts
@@ -7681,6 +7752,50 @@ function buildRequest(resolved, messages, limits, tools) {
7681
7752
  return req;
7682
7753
  }
7683
7754
  /**
7755
+ * Cheap deterministic prompt-size estimate (about four serialized
7756
+ * characters per token) for the layer-2b output bound. Never used for
7757
+ * identity, accounting, or anything the journal records.
7758
+ */
7759
+ function estimateInputTokens(messages) {
7760
+ let chars = 0;
7761
+ for (const msg of messages) chars += JSON.stringify(msg.parts).length;
7762
+ return Math.ceil(chars / 4);
7763
+ }
7764
+ /**
7765
+ * Layer 2b at the wire boundary: clamps the outgoing request's
7766
+ * maxOutputTokens to what the remaining budget affords from the serving
7767
+ * model. The clamp uses the heuristic prompt estimate; the DENIAL does
7768
+ * not: a turn is refused (BudgetExhaustedError, never dispatched) only
7769
+ * when the remainder cannot buy even ONE output token at zero input,
7770
+ * which is exact. Denying on the estimate would kill turns the budget
7771
+ * still funds, including the DEF-7 forced finish paid from the released
7772
+ * finalize reserve; when the estimate says the prompt alone spends the
7773
+ * remainder, the turn dispatches with a one-token output floor and the
7774
+ * exact layers (2 and 3) settle the difference. A no-op without a hook
7775
+ * or when the hook reports no bound. The clamp touches only the wire
7776
+ * request, exactly like limits.maxOutputTokensPerTurn above it; identity
7777
+ * is computed at the ctx layer and never sees it.
7778
+ */
7779
+ function applyOutputBudget(req, target, budget) {
7780
+ const hook = budget?.maxAffordableOutputTokens;
7781
+ if (hook === void 0) return req;
7782
+ const affordable = hook(target.resolved.ref, estimateInputTokens(req.messages));
7783
+ if (affordable === void 0) return req;
7784
+ if (affordable < 1) {
7785
+ const zeroInputAffordable = hook(target.resolved.ref, 0);
7786
+ if (zeroInputAffordable !== void 0 && zeroInputAffordable < 1) throw new BudgetExhaustedError(`the remaining budget cannot afford one output token from ${target.resolved.ref}; the turn was not dispatched`);
7787
+ return {
7788
+ ...req,
7789
+ maxOutputTokens: 1
7790
+ };
7791
+ }
7792
+ if (req.maxOutputTokens === void 0 || affordable < req.maxOutputTokens) return {
7793
+ ...req,
7794
+ maxOutputTokens: affordable
7795
+ };
7796
+ return req;
7797
+ }
7798
+ /**
7684
7799
  * Builds the turn's canonical assistant message. Retained provider-raw
7685
7800
  * parts go at the HEAD: on both first-class providers the retained
7686
7801
  * blocks (thinking blocks, reasoning items) precede the turn's text and
@@ -8168,28 +8283,41 @@ async function runAgent(options) {
8168
8283
  turns += 1;
8169
8284
  const signals = [];
8170
8285
  if (options.signal !== void 0) signals.push(options.signal);
8171
- const { outcome, target: servedTarget } = await dispatchPhase({
8172
- chain: loopChain,
8173
- cursor: loopCursor,
8174
- requestFor: (target) => {
8175
- let req = buildRequest(target.resolved, projectHistory(messages, providerOf(target.adapter)), limits, options.tools?.contracts);
8176
- if (options.schema !== void 0 && options.canonicalSchema !== void 0 && !separateExtract) req = applyStructuredOutputTier(req, rideTierFor(target), options.canonicalSchema);
8177
- return req;
8178
- },
8179
- streamOptionsFor: (target) => {
8180
- const streamTurnOptions = {
8181
- idleTimeoutMs: limits.streamIdleTimeoutMs,
8182
- signals,
8183
- onUsage: (delta) => options.budget?.onUsage(delta, target.resolved.ref)
8184
- };
8185
- if (options.budget?.signal !== void 0) streamTurnOptions.budgetSignal = options.budget.signal;
8186
- if (options.stream === true) streamTurnOptions.onDelta = (delta) => events?.emit({
8187
- type: "agent:stream",
8188
- delta
8189
- });
8190
- return streamTurnOptions;
8191
- }
8192
- });
8286
+ let loopDispatch;
8287
+ try {
8288
+ loopDispatch = await dispatchPhase({
8289
+ chain: loopChain,
8290
+ cursor: loopCursor,
8291
+ requestFor: (target) => {
8292
+ let req = buildRequest(target.resolved, projectHistory(messages, providerOf(target.adapter)), limits, options.tools?.contracts);
8293
+ if (options.schema !== void 0 && options.canonicalSchema !== void 0 && !separateExtract) req = applyStructuredOutputTier(req, rideTierFor(target), options.canonicalSchema);
8294
+ return applyOutputBudget(req, target, options.budget);
8295
+ },
8296
+ streamOptionsFor: (target) => {
8297
+ const streamTurnOptions = {
8298
+ idleTimeoutMs: limits.streamIdleTimeoutMs,
8299
+ signals,
8300
+ onUsage: (delta) => options.budget?.onUsage(delta, target.resolved.ref)
8301
+ };
8302
+ if (options.budget?.signal !== void 0) streamTurnOptions.budgetSignal = options.budget.signal;
8303
+ if (options.stream === true) streamTurnOptions.onDelta = (delta) => events?.emit({
8304
+ type: "agent:stream",
8305
+ delta
8306
+ });
8307
+ return streamTurnOptions;
8308
+ }
8309
+ });
8310
+ } catch (thrown) {
8311
+ if (!(thrown instanceof BudgetExhaustedError)) throw thrown;
8312
+ status = "error";
8313
+ agentError = {
8314
+ kind: "budget",
8315
+ retryable: false
8316
+ };
8317
+ errorMessage = thrown.message;
8318
+ break;
8319
+ }
8320
+ const { outcome, target: servedTarget } = loopDispatch;
8193
8321
  servedBy = servedTarget.resolved.ref;
8194
8322
  usageApprox = usageApprox || outcome.usageApprox;
8195
8323
  lastTurnUsage = {
@@ -8346,30 +8474,43 @@ async function runAgent(options) {
8346
8474
  break;
8347
8475
  }
8348
8476
  turns += 1;
8349
- const { outcome: summary } = await dispatchPhase({
8350
- chain: [{
8351
- adapter: options.summarize.adapter,
8352
- resolved: options.summarize.resolved
8353
- }, ...options.summarize.fallbacks ?? []],
8354
- cursor: { index: 0 },
8355
- requestFor: (target) => {
8356
- let req = buildRequest(target.resolved, [...projectHistory(messages, providerOf(target.adapter)), summarizeInstruction()], limits, options.tools?.contracts);
8357
- if (req.tools !== void 0) req = {
8358
- ...req,
8359
- toolChoice: "none"
8360
- };
8361
- return req;
8362
- },
8363
- streamOptionsFor: (target) => {
8364
- const summarizeStreamOptions = {
8365
- idleTimeoutMs: limits.streamIdleTimeoutMs,
8366
- signals: options.signal === void 0 ? [] : [options.signal],
8367
- onUsage: (delta) => options.budget?.onUsage(delta, target.resolved.ref)
8368
- };
8369
- if (options.budget?.signal !== void 0) summarizeStreamOptions.budgetSignal = options.budget.signal;
8370
- return summarizeStreamOptions;
8371
- }
8372
- });
8477
+ let summaryDispatch;
8478
+ try {
8479
+ summaryDispatch = await dispatchPhase({
8480
+ chain: [{
8481
+ adapter: options.summarize.adapter,
8482
+ resolved: options.summarize.resolved
8483
+ }, ...options.summarize.fallbacks ?? []],
8484
+ cursor: { index: 0 },
8485
+ requestFor: (target) => {
8486
+ let req = buildRequest(target.resolved, [...projectHistory(messages, providerOf(target.adapter)), summarizeInstruction()], limits, options.tools?.contracts);
8487
+ if (req.tools !== void 0) req = {
8488
+ ...req,
8489
+ toolChoice: "none"
8490
+ };
8491
+ return applyOutputBudget(req, target, options.budget);
8492
+ },
8493
+ streamOptionsFor: (target) => {
8494
+ const summarizeStreamOptions = {
8495
+ idleTimeoutMs: limits.streamIdleTimeoutMs,
8496
+ signals: options.signal === void 0 ? [] : [options.signal],
8497
+ onUsage: (delta) => options.budget?.onUsage(delta, target.resolved.ref)
8498
+ };
8499
+ if (options.budget?.signal !== void 0) summarizeStreamOptions.budgetSignal = options.budget.signal;
8500
+ return summarizeStreamOptions;
8501
+ }
8502
+ });
8503
+ } catch (thrown) {
8504
+ if (!(thrown instanceof BudgetExhaustedError)) throw thrown;
8505
+ status = "error";
8506
+ agentError = {
8507
+ kind: "budget",
8508
+ retryable: false
8509
+ };
8510
+ errorMessage = thrown.message;
8511
+ break;
8512
+ }
8513
+ const { outcome: summary } = summaryDispatch;
8373
8514
  usageApprox = usageApprox || summary.usageApprox;
8374
8515
  if (summary.aborted === "budget") {
8375
8516
  status = "cancelled";
@@ -8404,6 +8545,28 @@ async function runAgent(options) {
8404
8545
  await saveBoundary();
8405
8546
  continue loop;
8406
8547
  }
8548
+ if (options.terminalTool !== void 0) {
8549
+ noProgress.recordTurn({ toolCalls: 0 });
8550
+ if (noProgress.tripped) {
8551
+ status = "limit";
8552
+ abortClass = "no-progress";
8553
+ agentError = {
8554
+ kind: "terminal",
8555
+ retryable: false
8556
+ };
8557
+ errorMessage = noProgress.describe();
8558
+ break;
8559
+ }
8560
+ messages.push({
8561
+ role: "user",
8562
+ parts: [{
8563
+ type: "text",
8564
+ text: outcome.finish?.reason === "max-tokens" ? `The turn was cut at the output token limit before any tool call. Be brief and call the '${options.terminalTool.name}' tool now; plain text is not a valid completion.` : `The turn ended without a tool call. Call the '${options.terminalTool.name}' tool to complete; plain text is not a valid completion.`
8565
+ }]
8566
+ });
8567
+ await saveBoundary();
8568
+ continue loop;
8569
+ }
8407
8570
  if (options.schema === void 0) {
8408
8571
  output = outcome.turn.text;
8409
8572
  break;
@@ -8473,66 +8636,80 @@ async function runAgent(options) {
8473
8636
  }
8474
8637
  if (proceed) {
8475
8638
  turns += 1;
8476
- const { outcome, target: finalizeTarget } = await dispatchPhase({
8477
- chain: [{
8478
- adapter: options.finalize.adapter,
8479
- resolved: options.finalize.resolved
8480
- }, ...options.finalize.fallbacks ?? []],
8481
- cursor: { index: 0 },
8482
- requestFor: (target) => ({
8483
- ...buildRequest(target.resolved, projectHistory(messages, providerOf(target.adapter)), limits, options.tools?.contracts),
8484
- toolChoice: "none"
8485
- }),
8486
- streamOptionsFor: (target) => {
8487
- const finalizeStreamOptions = {
8488
- idleTimeoutMs: limits.streamIdleTimeoutMs,
8489
- signals: options.signal === void 0 ? [] : [options.signal],
8490
- onUsage: (delta) => options.budget?.onUsage(delta, target.resolved.ref)
8491
- };
8492
- if (options.budget?.signal !== void 0) finalizeStreamOptions.budgetSignal = options.budget.signal;
8493
- if (options.stream === true) finalizeStreamOptions.onDelta = (delta) => events?.emit({
8494
- type: "agent:stream",
8495
- delta
8496
- });
8497
- return finalizeStreamOptions;
8498
- }
8499
- });
8500
- usageApprox = usageApprox || outcome.usageApprox;
8501
- messages.push(assistantMsg(outcome.turn, liftRetainedParts(outcome.providerMetadata, finalizeTarget.adapter)));
8502
- if (invariantViolation !== void 0) {
8639
+ let finalizeDispatch;
8640
+ try {
8641
+ finalizeDispatch = await dispatchPhase({
8642
+ chain: [{
8643
+ adapter: options.finalize.adapter,
8644
+ resolved: options.finalize.resolved
8645
+ }, ...options.finalize.fallbacks ?? []],
8646
+ cursor: { index: 0 },
8647
+ requestFor: (target) => applyOutputBudget({
8648
+ ...buildRequest(target.resolved, projectHistory(messages, providerOf(target.adapter)), limits, options.tools?.contracts),
8649
+ toolChoice: "none"
8650
+ }, target, options.budget),
8651
+ streamOptionsFor: (target) => {
8652
+ const finalizeStreamOptions = {
8653
+ idleTimeoutMs: limits.streamIdleTimeoutMs,
8654
+ signals: options.signal === void 0 ? [] : [options.signal],
8655
+ onUsage: (delta) => options.budget?.onUsage(delta, target.resolved.ref)
8656
+ };
8657
+ if (options.budget?.signal !== void 0) finalizeStreamOptions.budgetSignal = options.budget.signal;
8658
+ if (options.stream === true) finalizeStreamOptions.onDelta = (delta) => events?.emit({
8659
+ type: "agent:stream",
8660
+ delta
8661
+ });
8662
+ return finalizeStreamOptions;
8663
+ }
8664
+ });
8665
+ } catch (thrown) {
8666
+ if (!(thrown instanceof BudgetExhaustedError)) throw thrown;
8503
8667
  status = "error";
8504
8668
  agentError = {
8505
- kind: "transport",
8669
+ kind: "budget",
8506
8670
  retryable: false
8507
8671
  };
8508
- errorMessage = invariantViolation;
8509
- } else if (outcome.aborted !== void 0 || outcome.wireError !== void 0) {
8510
- status = outcome.aborted === "external" ? "cancelled" : "error";
8511
- if (outcome.wireError !== void 0) {
8512
- agentError = classifyWireError(outcome.wireError);
8513
- errorMessage = outcome.wireError.message;
8514
- } else if (outcome.aborted === "budget") {
8515
- status = "cancelled";
8672
+ errorMessage = thrown.message;
8673
+ }
8674
+ if (finalizeDispatch !== void 0) {
8675
+ const { outcome, target: finalizeTarget } = finalizeDispatch;
8676
+ usageApprox = usageApprox || outcome.usageApprox;
8677
+ messages.push(assistantMsg(outcome.turn, liftRetainedParts(outcome.providerMetadata, finalizeTarget.adapter)));
8678
+ if (invariantViolation !== void 0) {
8679
+ status = "error";
8516
8680
  agentError = {
8517
- kind: "budget",
8681
+ kind: "transport",
8518
8682
  retryable: false
8519
8683
  };
8520
- } else if (outcome.aborted === "idle") {
8684
+ errorMessage = invariantViolation;
8685
+ } else if (outcome.aborted !== void 0 || outcome.wireError !== void 0) {
8686
+ status = outcome.aborted === "external" ? "cancelled" : "error";
8687
+ if (outcome.wireError !== void 0) {
8688
+ agentError = classifyWireError(outcome.wireError);
8689
+ errorMessage = outcome.wireError.message;
8690
+ } else if (outcome.aborted === "budget") {
8691
+ status = "cancelled";
8692
+ agentError = {
8693
+ kind: "budget",
8694
+ retryable: false
8695
+ };
8696
+ } else if (outcome.aborted === "idle") {
8697
+ status = "error";
8698
+ agentError = {
8699
+ kind: "transport",
8700
+ retryable: true
8701
+ };
8702
+ errorMessage = `stream idle for ${limits.streamIdleTimeoutMs}ms`;
8703
+ }
8704
+ } else if (outcome.finish?.reason === "refusal" || outcome.finish?.reason === "context-window-exceeded") {
8521
8705
  status = "error";
8522
8706
  agentError = {
8523
- kind: "transport",
8524
- retryable: true
8707
+ kind: "terminal",
8708
+ retryable: false
8525
8709
  };
8526
- errorMessage = `stream idle for ${limits.streamIdleTimeoutMs}ms`;
8527
- }
8528
- } else if (outcome.finish?.reason === "refusal" || outcome.finish?.reason === "context-window-exceeded") {
8529
- status = "error";
8530
- agentError = {
8531
- kind: "terminal",
8532
- retryable: false
8533
- };
8534
- if (outcome.finish.reason === "refusal") errorMessage = `model refusal (${outcome.finish.refusal.provider})`;
8535
- } else if (options.schema === void 0) output = outcome.turn.text;
8710
+ if (outcome.finish.reason === "refusal") errorMessage = `model refusal (${outcome.finish.refusal.provider})`;
8711
+ } else if (options.schema === void 0) output = outcome.turn.text;
8712
+ }
8536
8713
  }
8537
8714
  }
8538
8715
  if (status === "ok" && !finishedViaTool && separateExtract && options.extract !== void 0 && options.schema !== void 0) {
@@ -8570,28 +8747,42 @@ async function runAgent(options) {
8570
8747
  break;
8571
8748
  }
8572
8749
  turns += 1;
8573
- const { outcome, target: extractTarget } = await dispatchPhase({
8574
- chain: extractChain,
8575
- cursor: extractCursor,
8576
- requestFor: (target) => {
8577
- const targetTier = extractTierFor(target);
8578
- let req = buildRequest(target.resolved, projectHistory(extractMessages, providerOf(target.adapter)), limits, options.tools?.contracts);
8579
- if (req.tools !== void 0 && targetTier !== "forced-tool") req = {
8580
- ...req,
8581
- toolChoice: "none"
8582
- };
8583
- return applyStructuredOutputTier(req, targetTier, options.canonicalSchema ?? {});
8584
- },
8585
- streamOptionsFor: (target) => {
8586
- const extractStreamOptions = {
8587
- idleTimeoutMs: limits.streamIdleTimeoutMs,
8588
- signals: options.signal === void 0 ? [] : [options.signal],
8589
- onUsage: (delta) => options.budget?.onUsage(delta, target.resolved.ref)
8590
- };
8591
- if (options.budget?.signal !== void 0) extractStreamOptions.budgetSignal = options.budget.signal;
8592
- return extractStreamOptions;
8593
- }
8594
- });
8750
+ let extractDispatch;
8751
+ try {
8752
+ extractDispatch = await dispatchPhase({
8753
+ chain: extractChain,
8754
+ cursor: extractCursor,
8755
+ requestFor: (target) => {
8756
+ const targetTier = extractTierFor(target);
8757
+ let req = buildRequest(target.resolved, projectHistory(extractMessages, providerOf(target.adapter)), limits, options.tools?.contracts);
8758
+ if (req.tools !== void 0 && targetTier !== "forced-tool") req = {
8759
+ ...req,
8760
+ toolChoice: "none"
8761
+ };
8762
+ req = applyStructuredOutputTier(req, targetTier, options.canonicalSchema ?? {});
8763
+ return applyOutputBudget(req, target, options.budget);
8764
+ },
8765
+ streamOptionsFor: (target) => {
8766
+ const extractStreamOptions = {
8767
+ idleTimeoutMs: limits.streamIdleTimeoutMs,
8768
+ signals: options.signal === void 0 ? [] : [options.signal],
8769
+ onUsage: (delta) => options.budget?.onUsage(delta, target.resolved.ref)
8770
+ };
8771
+ if (options.budget?.signal !== void 0) extractStreamOptions.budgetSignal = options.budget.signal;
8772
+ return extractStreamOptions;
8773
+ }
8774
+ });
8775
+ } catch (thrown) {
8776
+ if (!(thrown instanceof BudgetExhaustedError)) throw thrown;
8777
+ status = "error";
8778
+ agentError = {
8779
+ kind: "budget",
8780
+ retryable: false
8781
+ };
8782
+ errorMessage = thrown.message;
8783
+ break;
8784
+ }
8785
+ const { outcome, target: extractTarget } = extractDispatch;
8595
8786
  usageApprox = usageApprox || outcome.usageApprox;
8596
8787
  if (invariantViolation !== void 0) {
8597
8788
  status = "error";
@@ -8674,10 +8865,16 @@ async function runAgent(options) {
8674
8865
  //#region src/engine/budget.ts
8675
8866
  /**
8676
8867
  * Three-layer budget (M1-T09, hierarchical sub-accounts M6-T06;
8677
- * invariant I4). Layer 1: admission before spawn (spent + committedReserve
8678
- * >= ceiling blocks on ANY account in the ancestor chain). Layer 2: the
8679
- * per-turn guard against the spawn's own chain. Layer 3: the AbortSignal
8680
- * ceiling severing live streams, with partial usage written usageApprox.
8868
+ * invariant I4). Layer 1: PROJECTED admission before spawn: a spawn is
8869
+ * admitted only when spent + committedReserve + finalizeReserve + the
8870
+ * PROPOSED reserve fits the ceiling of EVERY account in the ancestor
8871
+ * chain (exact fill allowed), checked atomically before any commit.
8872
+ * Layer 2: the per-turn guard against the spawn's own chain, plus the
8873
+ * pre-dispatch output bound (layer 2b): every turn's maxOutputTokens is
8874
+ * clamped to what the remaining chain budget affords from the serving
8875
+ * model, and a turn that cannot afford one output token is denied before
8876
+ * dispatch. Layer 3: the AbortSignal ceiling severing live streams, with
8877
+ * partial usage written usageApprox.
8681
8878
  * B0 is immutable after start: no API tops it up.
8682
8879
  *
8683
8880
  * The account tree: the run root plus one
@@ -8702,15 +8899,27 @@ const ZERO_USAGE = {
8702
8899
  cacheWriteTokens: 0
8703
8900
  };
8704
8901
  /**
8705
- * The admission reserve for a spawn: opts.estCost, else profile.estCost, else
8706
- * price(countTokens(input) + caps.maxOutputTokens), else the engine flat
8707
- * default.
8902
+ * The admission reserve for a spawn: opts.estCost, else profile.estCost,
8903
+ * else price(countTokens(input) + one turn's worth of output), else the
8904
+ * engine flat default. The output term is caps.maxOutputTokens clamped to
8905
+ * limits.maxOutputTokensPerTurn when the spawn carries one, so a host can
8906
+ * bound reserves without hand-written estimates. The priced path uses the
8907
+ * SAME price function as settlement (priceUsdOf), so long-context tiers
8908
+ * apply to estimates too.
8708
8909
  */
8709
8910
  function admissionReserveUsd(options) {
8710
8911
  if (options.estCost !== void 0) return options.estCost;
8711
8912
  if (options.profileEstCost !== void 0) return options.profileEstCost;
8712
8913
  const pricing = options.caps?.pricing;
8713
- if (options.inputTokens !== void 0 && pricing !== void 0 && options.caps !== void 0) return options.inputTokens / 1e6 * pricing.inputUsdPerMTok + options.caps.maxOutputTokens / 1e6 * pricing.outputUsdPerMTok;
8914
+ if (options.inputTokens !== void 0 && pricing !== void 0 && options.caps !== void 0) {
8915
+ const outputTokens = options.maxOutputTokensPerTurn === void 0 ? options.caps.maxOutputTokens : Math.min(options.caps.maxOutputTokens, options.maxOutputTokensPerTurn);
8916
+ return priceUsdOf(pricing, {
8917
+ inputTokens: options.inputTokens,
8918
+ outputTokens,
8919
+ cacheReadTokens: 0,
8920
+ cacheWriteTokens: 0
8921
+ });
8922
+ }
8714
8923
  return options.flatReserveUsd ?? .5;
8715
8924
  }
8716
8925
  /**
@@ -8725,6 +8934,7 @@ var RunBudget = class {
8725
8934
  lifetimeSpawnCap;
8726
8935
  events;
8727
8936
  priceUsd;
8937
+ pricingOf;
8728
8938
  accounts = /* @__PURE__ */ new Map();
8729
8939
  usageInternal = { ...ZERO_USAGE };
8730
8940
  agentsSpawnedInternal = 0;
@@ -8736,6 +8946,7 @@ var RunBudget = class {
8736
8946
  this.lifetimeSpawnCap = options.lifetimeSpawnCap ?? 500;
8737
8947
  if (options.events !== void 0) this.events = options.events;
8738
8948
  if (options.priceUsd !== void 0) this.priceUsd = options.priceUsd;
8949
+ if (options.pricingOf !== void 0) this.pricingOf = options.pricingOf;
8739
8950
  const root = {
8740
8951
  scope: "run",
8741
8952
  spentUsd: 0,
@@ -8788,8 +8999,43 @@ var RunBudget = class {
8788
8999
  controller: new AbortController()
8789
9000
  };
8790
9001
  if (options.ceilingUsd !== void 0) account.ceilingUsd = options.ceilingUsd;
9002
+ if (options.kind !== void 0) account.kind = options.kind;
8791
9003
  this.accounts.set(scope, account);
8792
9004
  }
9005
+ /**
9006
+ * The diagnostic projection behind a ceiling error: the first CLOSED
9007
+ * account (projected commitments included, exactly the layer-1
9008
+ * closure test) walking from `scope` toward the root, plus the root
9009
+ * state. 'run budget ceiling reached' under a healthy root misled the
9010
+ * v1.6.0 follow-up review's live probe when only a 0.18 USD
9011
+ * orchestrator cap had crossed under a 0.90 USD root; the message can
9012
+ * now name the account that actually ended the work. An unknown scope
9013
+ * degrades to root-only diagnostics instead of throwing: this runs on
9014
+ * the error path.
9015
+ */
9016
+ exhaustionDiagnostics(scope) {
9017
+ let chain;
9018
+ try {
9019
+ chain = this.chainOf(scope);
9020
+ } catch {
9021
+ chain = [this.root];
9022
+ }
9023
+ const crossed = chain.find((account) => account.ceilingUsd !== void 0 && account.spentUsd + account.committedReserveUsd + account.finalizeReserveUsd >= account.ceilingUsd);
9024
+ const root = this.root;
9025
+ const diagnostics = { root: {
9026
+ spentUsd: root.spentUsd,
9027
+ ...root.ceilingUsd === void 0 ? {} : { ceilingUsd: root.ceilingUsd }
9028
+ } };
9029
+ if (crossed?.ceilingUsd !== void 0) diagnostics.crossed = {
9030
+ scope: crossed.scope,
9031
+ source: crossed.scope === "run" ? "root" : crossed.kind === "orchestrator-cap" ? "orchestrator-cap" : "child-account",
9032
+ ceilingUsd: crossed.ceilingUsd,
9033
+ spentUsd: crossed.spentUsd,
9034
+ committedReserveUsd: crossed.committedReserveUsd,
9035
+ finalizeReserveUsd: crossed.finalizeReserveUsd
9036
+ };
9037
+ return diagnostics;
9038
+ }
8793
9039
  accountView(scope) {
8794
9040
  const account = this.accounts.get(scope);
8795
9041
  if (account === void 0) return;
@@ -8841,9 +9087,15 @@ var RunBudget = class {
8841
9087
  return Math.max(0, this.lifetimeSpawnCap - this.agentsSpawnedInternal);
8842
9088
  }
8843
9089
  /**
8844
- * Layer 1: admission before spawn. Blocks when spent + committedReserve
8845
- * has reached the ceiling on ANY account in the ancestor chain of
8846
- * `accountScope`, otherwise commits the reserve along the whole chain.
9090
+ * Layer 1: PROJECTED admission before spawn. A spawn is admitted only
9091
+ * when every account in the ancestor chain of `accountScope` still has
9092
+ * admission headroom AND fits the PROPOSED reserve on top of spent +
9093
+ * committedReserve + finalizeReserve (the finalize reserve is
9094
+ * untouchable by admission, DEF-7). An exact fill is allowed; one
9095
+ * dollar past the ceiling is not: a spawn is never admitted on the
9096
+ * argument that the money it needs is merely not committed yet. The
9097
+ * whole chain is checked before anything commits, so a rejection
9098
+ * mutates no account, increments no counter, and journals nothing.
8847
9099
  * Also enforces the engine lifetime spawn cap.
8848
9100
  */
8849
9101
  admitSpawn(reserveUsd, accountScope = "run") {
@@ -8852,14 +9104,19 @@ var RunBudget = class {
8852
9104
  throw new BudgetExhaustedError(`engine lifetime spawn cap reached (${this.lifetimeSpawnCap} spawns per run; budgetDefaults.lifetimeSpawnCap)`, { data: { cap: this.lifetimeSpawnCap } });
8853
9105
  }
8854
9106
  const chain = this.chainOf(accountScope);
8855
- for (const account of chain) if (account.ceilingUsd !== void 0 && account.spentUsd + account.committedReserveUsd + account.finalizeReserveUsd >= account.ceilingUsd) {
8856
- if (account.scope === "run") this.exhaustedInternal = true;
8857
- throw new BudgetExhaustedError(`budget ceiling reached on account '${account.scope}': spent ${account.spentUsd.toFixed(4)} USD plus committed reserve ${account.committedReserveUsd.toFixed(4)} USD is at the ceiling ${account.ceilingUsd.toFixed(4)} USD`, { data: {
8858
- account: account.scope,
8859
- spentUsd: account.spentUsd,
8860
- committedReserveUsd: account.committedReserveUsd,
8861
- ceilingUsd: account.ceilingUsd
8862
- } });
9107
+ for (const account of chain) {
9108
+ if (account.ceilingUsd === void 0) continue;
9109
+ const committed = account.spentUsd + account.committedReserveUsd + account.finalizeReserveUsd;
9110
+ if (committed >= account.ceilingUsd || committed + reserveUsd > account.ceilingUsd) {
9111
+ if (account.scope === "run") this.exhaustedInternal = true;
9112
+ throw new BudgetExhaustedError(`budget ceiling reached on account '${account.scope}': spent ${account.spentUsd.toFixed(4)} USD plus committed reserves ${(account.committedReserveUsd + account.finalizeReserveUsd).toFixed(4)} USD plus the proposed reserve ${reserveUsd.toFixed(4)} USD does not fit the ceiling ${account.ceilingUsd.toFixed(4)} USD`, { data: {
9113
+ account: account.scope,
9114
+ spentUsd: account.spentUsd,
9115
+ committedReserveUsd: account.committedReserveUsd,
9116
+ proposedReserveUsd: reserveUsd,
9117
+ ceilingUsd: account.ceilingUsd
9118
+ } });
9119
+ }
8863
9120
  }
8864
9121
  this.agentsSpawnedInternal += 1;
8865
9122
  for (const account of chain) account.committedReserveUsd += reserveUsd;
@@ -8921,6 +9178,28 @@ var RunBudget = class {
8921
9178
  }
8922
9179
  }
8923
9180
  /**
9181
+ * Layer 2b, the pre-dispatch output bound: the output tokens the
9182
+ * remaining chain budget (min over capped ancestors of ceiling minus
9183
+ * spend) still affords from `servedBy` for an estimated prompt, priced
9184
+ * by the same function as settlement, long-context tiers included.
9185
+ * Undefined when no account in the chain carries a USD ceiling, when
9186
+ * the model has no price row (the once-per-model unpriced warning in
9187
+ * onUsage covers that hole), or when output is free. Zero or negative
9188
+ * means the turn cannot be dispatched within the budget.
9189
+ */
9190
+ maxAffordableOutputTokens(servedBy, estimatedInputTokens, accountScope = "run") {
9191
+ const pricing = this.pricingOf?.(servedBy);
9192
+ if (pricing === void 0) return;
9193
+ let remainingUsd;
9194
+ for (const account of this.chainOf(accountScope)) {
9195
+ if (account.ceilingUsd === void 0) continue;
9196
+ const headroom = account.ceilingUsd - account.spentUsd;
9197
+ remainingUsd = remainingUsd === void 0 ? headroom : Math.min(remainingUsd, headroom);
9198
+ }
9199
+ if (remainingUsd === void 0) return;
9200
+ return affordableOutputTokens(pricing, Math.max(0, remainingUsd), estimatedInputTokens);
9201
+ }
9202
+ /**
8924
9203
  * Live accounting; spend propagates from `accountScope` to every
8925
9204
  * ancestor. Crossing a ceiling severs the crossing account's subtree
8926
9205
  * via its layer-3 AbortSignal (overshoot bounded by one turn per
@@ -9193,13 +9472,16 @@ var AdmissionController = class {
9193
9472
  },
9194
9473
  statsBefore
9195
9474
  };
9196
- const reserveUsd = spec.estCostUsd ?? this.flatReserveUsd;
9197
- const reserve = { reserveUsd };
9475
+ let childCeilingUsd;
9198
9476
  const parentRemainder = this.budget.remainderOf(spec.parentAccountScope);
9199
9477
  if (parentRemainder !== void 0) {
9200
9478
  const fractionCap = this.childBudgetFraction * parentRemainder;
9201
- reserve.childCeilingUsd = spec.budgetUsd === void 0 ? fractionCap : Math.min(spec.budgetUsd, fractionCap);
9202
- } else if (spec.budgetUsd !== void 0) reserve.childCeilingUsd = spec.budgetUsd;
9479
+ childCeilingUsd = spec.budgetUsd === void 0 ? fractionCap : Math.min(spec.budgetUsd, fractionCap);
9480
+ } else if (spec.budgetUsd !== void 0) childCeilingUsd = spec.budgetUsd;
9481
+ let reserveUsd = spec.estCostUsd ?? this.flatReserveUsd;
9482
+ if (childCeilingUsd !== void 0) reserveUsd = Math.min(reserveUsd, childCeilingUsd);
9483
+ const reserve = { reserveUsd };
9484
+ if (childCeilingUsd !== void 0) reserve.childCeilingUsd = childCeilingUsd;
9203
9485
  if (this.budget.spawnHeadroom <= 0) return {
9204
9486
  verdict: {
9205
9487
  kind: "reject",
@@ -9601,6 +9883,13 @@ const kTerminalTool = Symbol("rulvar.terminalTool");
9601
9883
  * graft boot). Dangling redispatch checkpoints take precedence.
9602
9884
  */
9603
9885
  const kBootCheckpoint = Symbol("rulvar.bootCheckpoint");
9886
+ /**
9887
+ * Internal AgentOpts channel: marks the orchestrator forced-finish
9888
+ * dispatch, whose spend draws from the released finalize reserve
9889
+ * (DEF-7). Settlement stamps the flag into the terminal's cost
9890
+ * attribution so the journal fold reproduces reserveUsedUsd.
9891
+ */
9892
+ const kFinalizeReserve = Symbol("rulvar.finalizeReserve");
9604
9893
  /** Typed accessor used by the in-package consumers. */
9605
9894
  function runtimeOf(ctx) {
9606
9895
  const runtime = ctxRuntimes.get(ctx);
@@ -10172,6 +10461,7 @@ function createCtx(internals, rootWorkflow) {
10172
10461
  }
10173
10462
  const adapter = adapterOf(loopResolved);
10174
10463
  const caps = adapter.caps(loopResolved.model);
10464
+ const limits = mergeUsageLimits(opts.limits, profile?.limits, internals.defaults.limits);
10175
10465
  let inputTokens;
10176
10466
  if (opts.estCost === void 0 && profile?.estCost === void 0 && adapter.countTokens) try {
10177
10467
  inputTokens = await adapter.countTokens({
@@ -10191,8 +10481,9 @@ function createCtx(internals, rootWorkflow) {
10191
10481
  if (opts.estCost !== void 0) reserveOptions.estCost = opts.estCost;
10192
10482
  if (profile?.estCost !== void 0) reserveOptions.profileEstCost = profile.estCost;
10193
10483
  if (inputTokens !== void 0) reserveOptions.inputTokens = inputTokens;
10484
+ if (limits.maxOutputTokensPerTurn !== void 0) reserveOptions.maxOutputTokensPerTurn = limits.maxOutputTokensPerTurn;
10194
10485
  if (internals.flatReserveUsd !== void 0) reserveOptions.flatReserveUsd = internals.flatReserveUsd;
10195
- const reserve = admissionReserveUsd(reserveOptions);
10486
+ const reserve = internals.pricingOf !== void 0 && internals.pricingOf(loopResolved.ref) === void 0 && opts.estCost === void 0 && profile?.estCost === void 0 ? 0 : admissionReserveUsd(reserveOptions);
10196
10487
  const budgetAccount = state.budgetScope ?? "run";
10197
10488
  internals.budget.admitSpawn(reserve, budgetAccount);
10198
10489
  let acquired;
@@ -10222,7 +10513,6 @@ function createCtx(internals, rootWorkflow) {
10222
10513
  running = await internals.replayer.appendRunning(runningInput);
10223
10514
  }
10224
10515
  opts[kOnRunning]?.(running.seq);
10225
- const limits = mergeUsageLimits(opts.limits, profile?.limits, internals.defaults.limits);
10226
10516
  const agentSink = { emit: (body) => internals.events.emit(body, spanId) };
10227
10517
  const ckptRef = checkpointRefFor(internals.runId, running.seq);
10228
10518
  let checkpointWritten = false;
@@ -10338,6 +10628,7 @@ function createCtx(internals, rootWorkflow) {
10338
10628
  },
10339
10629
  budget: {
10340
10630
  beforeTurn: () => internals.budget.beforeTurn(budgetAccount),
10631
+ maxAffordableOutputTokens: (servedBy, estimatedInputTokens) => internals.budget.maxAffordableOutputTokens(servedBy, estimatedInputTokens, budgetAccount),
10341
10632
  onUsage: (usage, servedBy) => internals.budget.onUsage(usage, servedBy, budgetAccount),
10342
10633
  signal: budgetAccount === "run" ? internals.budget.signal : AbortSignal.any([internals.budget.signal, internals.budget.signalOf(budgetAccount)].filter((signal) => signal !== void 0))
10343
10634
  },
@@ -10461,6 +10752,13 @@ function createCtx(internals, rootWorkflow) {
10461
10752
  usage: result.usage,
10462
10753
  servedBy: result.servedBy,
10463
10754
  ...result.usageByModel === void 0 ? {} : { usageByModel: result.usageByModel },
10755
+ costAttribution: {
10756
+ ...state.phase === void 0 ? {} : { phase: state.phase },
10757
+ agentType,
10758
+ role: primaryRole,
10759
+ budgetAccount: state.budgetScope ?? "run",
10760
+ ...opts[kFinalizeReserve] === true ? { finalizeReserve: true } : {}
10761
+ },
10464
10762
  transcriptRef: result.transcriptRef
10465
10763
  };
10466
10764
  if (result.status === "escalated" && result.escalation !== void 0) terminalPatch.escalation = result.escalation;
@@ -10528,10 +10826,25 @@ function createCtx(internals, rootWorkflow) {
10528
10826
  bump(internals.cost.byPhase, state.phase ?? "", usd);
10529
10827
  bump(internals.cost.byAgentType, agentType, usd);
10530
10828
  internals.cost.byRole.set(primaryRole, (internals.cost.byRole.get(primaryRole) ?? 0) + usd);
10531
- if (result.error?.kind === "budget" || internals.budget.exhausted && result.status !== "ok") throw new BudgetExhaustedError("run budget ceiling reached during agent execution", { data: {
10532
- scope: state.scope,
10533
- entryRef: terminal.seq
10534
- } });
10829
+ if (result.error?.kind === "budget" || internals.budget.exhausted && result.status !== "ok") {
10830
+ const diagnostics = internals.budget.exhaustionDiagnostics(state.budgetScope ?? "run");
10831
+ const crossed = diagnostics.crossed;
10832
+ const rootSuffix = `run root: spent ${diagnostics.root.spentUsd.toFixed(4)}` + (diagnostics.root.ceilingUsd === void 0 ? " USD, no ceiling" : ` of ${diagnostics.root.ceilingUsd.toFixed(4)} USD`);
10833
+ throw new BudgetExhaustedError(crossed === void 0 || crossed.source === "root" ? "run budget ceiling reached during agent execution" : (crossed.source === "orchestrator-cap" ? "orchestrator budget cap reached during agent execution" : "budget sub-account ceiling reached during agent execution") + ` (account '${crossed.scope}': spent ${crossed.spentUsd.toFixed(4)}` + (crossed.committedReserveUsd + crossed.finalizeReserveUsd > 0 ? ` plus ${(crossed.committedReserveUsd + crossed.finalizeReserveUsd).toFixed(4)} reserved` : "") + ` of ${crossed.ceilingUsd.toFixed(4)} USD; ${rootSuffix})`, { data: {
10834
+ scope: state.scope,
10835
+ entryRef: terminal.seq,
10836
+ source: crossed?.source ?? "root",
10837
+ rootSpentUsd: diagnostics.root.spentUsd,
10838
+ ...diagnostics.root.ceilingUsd === void 0 ? {} : { rootCeilingUsd: diagnostics.root.ceilingUsd },
10839
+ ...crossed === void 0 ? {} : {
10840
+ crossedScope: crossed.scope,
10841
+ crossedCeilingUsd: crossed.ceilingUsd,
10842
+ crossedSpentUsd: crossed.spentUsd,
10843
+ crossedCommittedReserveUsd: crossed.committedReserveUsd,
10844
+ crossedFinalizeReserveUsd: crossed.finalizeReserveUsd
10845
+ }
10846
+ } });
10847
+ }
10535
10848
  if (opts.fallback !== void 0) {
10536
10849
  const trigger = fallbackTriggerOf(result);
10537
10850
  if (trigger !== void 0 && opts.fallback.on.includes(trigger)) return runFallbackAttempt(running.seq, trigger, spanId);
@@ -11121,10 +11434,16 @@ function makeOrchestratorWorkflow(goal, opts) {
11121
11434
  const finalizeTurns = spec?.finalizeTurns ?? 2;
11122
11435
  const finalizeReserveUsd = spec?.finalizeReserveUsd ?? finalizeTurns * turnEstimateUsd;
11123
11436
  if (extension !== void 0 && effectiveCapUsd < finalizeReserveUsd) throw new OrchestratorCapConfigError(`effectiveCap ${effectiveCapUsd.toFixed(4)} USD is below the finalize reserve ${finalizeReserveUsd.toFixed(4)} USD`);
11437
+ if (spec?.capUsd !== void 0 && spec.capFraction === void 0 && effectiveCapUsd < spec.capUsd) internals.events.emit({
11438
+ type: "log",
11439
+ level: "warn",
11440
+ msg: `orchestrator budget.capUsd ${spec.capUsd.toFixed(4)} USD is bounded to ${effectiveCapUsd.toFixed(4)} USD by the default capFraction 0.2 of the run ceiling (effectiveCap = min(capUsd, capFraction * ceiling)); pass capFraction: 1.0 to make capUsd the sole bound`
11441
+ }, callingState.spanId);
11124
11442
  orchestratorAccount = callingState.scope === "" ? "orchestrator" : `${callingState.scope}/orchestrator`;
11125
11443
  internals.budget.openAccount(orchestratorAccount, {
11126
11444
  parentScope: callingState.budgetScope ?? "run",
11127
- ceilingUsd: effectiveCapUsd
11445
+ ceilingUsd: effectiveCapUsd,
11446
+ kind: "orchestrator-cap"
11128
11447
  });
11129
11448
  if (extension !== void 0) internals.budget.commitFinalizeReserve(orchestratorAccount, finalizeReserveUsd);
11130
11449
  capState = {
@@ -11140,6 +11459,15 @@ function makeOrchestratorWorkflow(goal, opts) {
11140
11459
  const records = /* @__PURE__ */ new Map();
11141
11460
  const byOrdinal = /* @__PURE__ */ new Map();
11142
11461
  const rejectedByOrdinal = /* @__PURE__ */ new Map();
11462
+ /**
11463
+ * The journaled spec behind each recovered ordinal: the idempotent
11464
+ * re-execution guard compares it against the incoming call, because
11465
+ * after a cross-attempt resume a REGENERATED turn (the boundary
11466
+ * checkpoint predates the lost turn) may decide differently, and
11467
+ * handing it the prior ordinal's handle would bind the transcript
11468
+ * to a stranger's child.
11469
+ */
11470
+ const recoveredSpecByOrdinal = /* @__PURE__ */ new Map();
11143
11471
  let nextOrdinal = 0;
11144
11472
  let orchSeq;
11145
11473
  const deliveredNodeIds = /* @__PURE__ */ new Set();
@@ -11175,7 +11503,7 @@ function makeOrchestratorWorkflow(goal, opts) {
11175
11503
  const controller = new AbortController();
11176
11504
  const upstream = callingState.signal ?? internals.runSignal;
11177
11505
  const scope = placement?.childScope ?? childScopeOf();
11178
- if (placement !== void 0) internals.budget.openAccount(scope, {
11506
+ if (placement?.ownAccount === true) internals.budget.openAccount(scope, {
11179
11507
  parentScope: callingState.budgetScope ?? "run",
11180
11508
  ...placement.childCeilingUsd === void 0 ? {} : { ceilingUsd: placement.childCeilingUsd }
11181
11509
  });
@@ -11183,7 +11511,7 @@ function makeOrchestratorWorkflow(goal, opts) {
11183
11511
  scope,
11184
11512
  spanId: internals.spans.mint(callingState.spanId),
11185
11513
  signal: upstream === void 0 ? controller.signal : AbortSignal.any([upstream, controller.signal]),
11186
- budgetScope: placement !== void 0 ? scope : callingState.budgetScope ?? "run"
11514
+ budgetScope: placement?.ownAccount === true ? scope : callingState.budgetScope ?? "run"
11187
11515
  };
11188
11516
  let resolveHandle = () => void 0;
11189
11517
  const handlePromise = new Promise((resolve) => {
@@ -11265,6 +11593,7 @@ function makeOrchestratorWorkflow(goal, opts) {
11265
11593
  nextOrdinal += 1;
11266
11594
  return { handle: (await dispatchChild(spec, spawnOrdinal, identity, {
11267
11595
  childScope,
11596
+ ownAccount: true,
11268
11597
  ...spec.budgetUsd === void 0 ? {} : { childCeilingUsd: spec.budgetUsd }
11269
11598
  })).handle };
11270
11599
  },
@@ -11295,33 +11624,61 @@ function makeOrchestratorWorkflow(goal, opts) {
11295
11624
  handle
11296
11625
  };
11297
11626
  };
11298
- /** Rebuilds spawn records from the journal (the crash-resume contract). */
11627
+ /**
11628
+ * True when `scope` is a root-attempt scope of THIS orchestration:
11629
+ * agentScope(callingState.scope, n) for some dispatch seq n. Nested
11630
+ * orchestrations live under their own wf: child scopes and never
11631
+ * match a foreign calling scope.
11632
+ */
11633
+ const scopeOfThisOrchestration = (scope) => {
11634
+ const prefix = callingState.scope === "" ? "" : `${callingState.scope}/`;
11635
+ return scope.startsWith(prefix) && /^agent:\d+$/.test(scope.slice(prefix.length));
11636
+ };
11637
+ /**
11638
+ * Rebuilds spawn records from the journal (the crash-resume
11639
+ * contract). Recovery is ORCHESTRATION-scoped, not attempt-scoped:
11640
+ * decisions journal at the orchestrate call's own scope, which is
11641
+ * stable across root attempts, so a rerun after a cancelled root
11642
+ * (the budget-abort shape the v1.6.0 follow-up review resumed) sees
11643
+ * every prior decision instead of re-deciding and re-paying.
11644
+ * Recovered children re-dispatch PINNED to their journaled child
11645
+ * scope: settled ones forward-match and replay for free, a dangling
11646
+ * one redispatches live (at-least-once), and a decision without a
11647
+ * dispatch entry rolls forward to a fresh dispatch.
11648
+ */
11299
11649
  const recover = async () => {
11300
- const scope = childScopeOf();
11650
+ const currentScope = childScopeOf();
11301
11651
  const admissions = internals.replayer.snapshot().filter((entry) => {
11302
- if (entry.kind !== "decision") return false;
11652
+ if (entry.kind !== "decision" || entry.scope !== callingState.scope) return false;
11303
11653
  const value = entry.value;
11304
- return value?.decisionType === "spawn-admission" && (value.origin === "spawn_agent" || value.origin === "parallel_agents") && value.orchestratorScope === scope;
11654
+ return value?.decisionType === "spawn-admission" && (value.origin === "spawn_agent" || value.origin === "parallel_agents");
11305
11655
  }).map((entry) => entry.value).sort((a, b) => a.spawnOrdinal - b.spawnOrdinal);
11306
11656
  for (const value of admissions) {
11307
11657
  nextOrdinal = Math.max(nextOrdinal, value.spawnOrdinal + 1);
11308
11658
  const decision = value.decision;
11659
+ recoveredSpecByOrdinal.set(value.spawnOrdinal, value.spec);
11309
11660
  if (decision.verdict.kind !== "admit") {
11310
11661
  rejectedByOrdinal.set(value.spawnOrdinal, decision);
11311
11662
  continue;
11312
11663
  }
11313
- admission.recoverChild(scope);
11314
- await dispatchChild(value.spec, value.spawnOrdinal, {
11664
+ admission.recoverChild(currentScope);
11665
+ const childScope = value.childScope ?? value.orchestratorScope;
11666
+ const record = await dispatchChild(value.spec, value.spawnOrdinal, {
11315
11667
  nodeId: decision.nodeId ?? "unknown",
11316
11668
  logicalTaskId: decision.verdict.lineage.logicalTaskId
11317
- });
11669
+ }, { childScope });
11670
+ const dispatched = internals.replayer.snapshot().find((entry) => entry.seq === record.handle);
11671
+ if (dispatched !== void 0) {
11672
+ for (const prior of internals.replayer.snapshot()) if (prior.kind === "agent" && prior.status === "running" && prior.seq !== record.handle && prior.scope === dispatched.scope && prior.key === dispatched.key && prior.ordinal === dispatched.ordinal && !records.has(prior.seq)) records.set(prior.seq, record);
11673
+ }
11318
11674
  }
11319
- const wakePrefix = `wake:${String(orchSeq ?? -1)}:`;
11320
11675
  for (const entry of internals.replayer.snapshot()) {
11321
11676
  if (entry.status !== "suspended" || entry.kind !== "external") continue;
11677
+ if (!scopeOfThisOrchestration(entry.scope)) continue;
11322
11678
  const payload = entry.value;
11323
- if (typeof payload?.key !== "string" || !payload.key.startsWith(wakePrefix)) continue;
11324
- wakeOrdinal = Math.max(wakeOrdinal, Number(payload.key.slice(wakePrefix.length)) + 1);
11679
+ const match = typeof payload?.key === "string" ? /^wake:\d+:(\d+)$/.exec(payload.key) : null;
11680
+ if (match === null) continue;
11681
+ wakeOrdinal = Math.max(wakeOrdinal, Number(match[1]) + 1);
11325
11682
  const suspension = internals.replayer.suspensionState(entry.seq);
11326
11683
  if (suspension.state === "resolved") markDelivered(suspension.value);
11327
11684
  }
@@ -11449,10 +11806,12 @@ function makeOrchestratorWorkflow(goal, opts) {
11449
11806
  await recoveryDone;
11450
11807
  const spawnOrdinal = nextOrdinal;
11451
11808
  nextOrdinal += 1;
11809
+ const priorSpec = recoveredSpecByOrdinal.get(spawnOrdinal);
11810
+ const specMatches = priorSpec === void 0 || priorSpec.agentType === params.agentType && priorSpec.prompt === params.prompt;
11452
11811
  const recovered = byOrdinal.get(spawnOrdinal);
11453
- if (recovered !== void 0) return { handle: recovered.handle };
11812
+ if (recovered !== void 0 && specMatches) return { handle: recovered.handle };
11454
11813
  const recoveredRejection = rejectedByOrdinal.get(spawnOrdinal);
11455
- if (recoveredRejection !== void 0) throw new AdmissionRejectedError(`admission rejected spawn ordinal ${String(spawnOrdinal)} (recovered verdict)`, { data: { decision: recoveredRejection } });
11814
+ if (recoveredRejection !== void 0 && specMatches) throw new AdmissionRejectedError(`admission rejected spawn ordinal ${String(spawnOrdinal)} (recovered verdict)`, { data: { decision: recoveredRejection } });
11456
11815
  if (opts?.maxSpawns !== void 0 && spawnOrdinal >= opts.maxSpawns) {
11457
11816
  internals.events.emit({
11458
11817
  type: "spawn:rejected",
@@ -11702,7 +12061,7 @@ function makeOrchestratorWorkflow(goal, opts) {
11702
12061
  role: "orchestrate",
11703
12062
  result: "full",
11704
12063
  tools: [...buildOrchestratorTools(orchestratorRuntime, fullCardText), ...extension?.tools(io) ?? []],
11705
- ...capState === void 0 ? {} : { estCost: capState.effectiveCapUsd },
12064
+ ...capState === void 0 ? {} : { estCost: capState.effectiveCapUsd - (orchestratorAccount === void 0 ? 0 : internals.budget.accountView(orchestratorAccount)?.finalizeReserveUsd ?? 0) },
11706
12065
  ...opts?.model === void 0 ? {} : { model: opts.model },
11707
12066
  ...opts?.limits === void 0 ? {} : { limits: opts.limits },
11708
12067
  [kOnRunning]: (seq) => {
@@ -11710,7 +12069,11 @@ function makeOrchestratorWorkflow(goal, opts) {
11710
12069
  orchSeq = seq;
11711
12070
  recover().then(releaseRecovery, releaseRecovery);
11712
12071
  },
11713
- [kTerminalTool]: { name: FINISH_TOOL_NAME }
12072
+ [kTerminalTool]: { name: FINISH_TOOL_NAME },
12073
+ ...(() => {
12074
+ const priorCancelledRoot = internals.replayer.snapshot().filter((entry) => entry.kind === "agent" && entry.scope === callingState.scope && entry.status === "cancelled" && entry.checkpointRef !== void 0).at(-1);
12075
+ return priorCancelledRoot?.checkpointRef === void 0 ? {} : { [kBootCheckpoint]: priorCancelledRoot.checkpointRef };
12076
+ })()
11714
12077
  };
11715
12078
  const orchestratorState = { ...callingState };
11716
12079
  if (orchestratorAccount !== void 0) orchestratorState.budgetScope = orchestratorAccount;
@@ -11736,7 +12099,8 @@ function makeOrchestratorWorkflow(goal, opts) {
11736
12099
  limits: { maxTurns: capState?.finalizeTurns ?? 2 },
11737
12100
  ...capState === void 0 ? {} : { estCost: capState.finalizeReserveUsd },
11738
12101
  ...opts?.model === void 0 ? {} : { model: opts.model },
11739
- [kTerminalTool]: { name: FINISH_TOOL_NAME }
12102
+ [kTerminalTool]: { name: FINISH_TOOL_NAME },
12103
+ [kFinalizeReserve]: true
11740
12104
  };
11741
12105
  const finalState = { ...callingState };
11742
12106
  if (orchestratorAccount !== void 0) finalState.budgetScope = orchestratorAccount;
@@ -11904,13 +12268,81 @@ var EventBus = class {
11904
12268
  //#endregion
11905
12269
  //#region src/runner/inprocess.ts
11906
12270
  /**
12271
+ * ScriptRunner SPI and InProcessRunner (M1-T11).
12272
+ *
12273
+ * Script runner contract: https://docs.rulvar.com/guide/planner
12274
+ * Workflow (a closure value) runs in process only; CompiledWorkflow is the
12275
+ * only form admissible to the worker sandbox and first exists at M6
12276
+ * (compileScript in @rulvar/planner), so until then the engine accepts
12277
+ * only in-process Workflow values. The SPI's L0 listing refers
12278
+ * to its frozen-seam status; the declaration lives here with its types.
12279
+ */
12280
+ const detection = new AsyncLocalStorage();
12281
+ let globalsPatched = false;
12282
+ /**
12283
+ * Stack line 0 names the Error, line 1 this helper, line 2 the patched
12284
+ * global, line 3 the caller whose provenance decides (the layout is
12285
+ * pinned by construction: this helper is only ever called by the two
12286
+ * patched globals). Two origins are exempt: installed dependencies (a
12287
+ * provider SDK, any transitive package, rulvar's own published dist),
12288
+ * which live under node_modules, and Node's own machinery (the undici
12289
+ * transport behind fetch, timers, stream internals), whose frames carry
12290
+ * `node:` specifiers and inherit the run's async context. The guard
12291
+ * exists for workflow code, which imports from both but lives in
12292
+ * neither.
12293
+ */
12294
+ function libraryCaller() {
12295
+ const caller = (/* @__PURE__ */ new Error()).stack?.split("\n")[3];
12296
+ if (caller === void 0) return false;
12297
+ return caller.includes("node_modules") || /[(\s]node:/.test(caller);
12298
+ }
12299
+ /**
12300
+ * Patches Date.now and Math.random ONCE per process and never restores:
12301
+ * outside a workflow's async context the store is absent and the patch is
12302
+ * a transparent passthrough. The previous per-execute patch/restore pair
12303
+ * could race under concurrent runs (one run's restore removed another's
12304
+ * patch, and the second restore re-installed a stale patched function
12305
+ * PERMANENTLY, which could then warn on host code outside any run: the
12306
+ * false RULVAR_BARE_DATE_NOW class the 1.5.2 review reproduced).
12307
+ */
12308
+ function patchGlobalsOnce() {
12309
+ if (globalsPatched) return;
12310
+ globalsPatched = true;
12311
+ const priorNow = Date.now;
12312
+ const priorRandom = Math.random;
12313
+ Date.now = function rulvarPatchedDateNow() {
12314
+ const state = detection.getStore();
12315
+ if (state !== void 0 && !state.warnedNow && !libraryCaller()) {
12316
+ state.warnedNow = true;
12317
+ process.emitWarning("bare Date.now() called inside a rulvar run; use ctx.now() so the value is journaled and stable on replay", {
12318
+ code: "RULVAR_BARE_DATE_NOW",
12319
+ type: "RulvarWarning"
12320
+ });
12321
+ }
12322
+ return priorNow();
12323
+ };
12324
+ Math.random = function rulvarPatchedMathRandom() {
12325
+ const state = detection.getStore();
12326
+ if (state !== void 0 && !state.warnedRandom && !libraryCaller()) {
12327
+ state.warnedRandom = true;
12328
+ process.emitWarning("bare Math.random() called inside a rulvar run; use ctx.random() so the value is journaled and stable on replay", {
12329
+ code: "RULVAR_BARE_MATH_RANDOM",
12330
+ type: "RulvarWarning"
12331
+ });
12332
+ }
12333
+ return priorRandom();
12334
+ };
12335
+ }
12336
+ /**
11907
12337
  * The mode (a) runner for human-authored closures. Determinism is enforced
11908
12338
  * by convention, lint, and the ctx shims, NOT by a VM: only the sequence
11909
- * of keys must be stable. Dev mode (NODE_ENV !== 'production') patches
11910
- * Date.now and Math.random for the duration of execute to emit one warning
11911
- * per run pointing at ctx.now()/ctx.random(); the patch preserves behavior
11912
- * and restores the prior functions on exit (nesting-safe by capturing the
11913
- * prior value; concurrent runs may lose the warning, never correctness).
12339
+ * of keys must be stable. Dev mode (NODE_ENV !== 'production') detects
12340
+ * bare Date.now and Math.random and emits one warning per run pointing at
12341
+ * ctx.now()/ctx.random(). Detection is attributed by AsyncLocalStorage:
12342
+ * only code inside the workflow body's async context can trigger it, so
12343
+ * host code running concurrently, engine internals outside the body, and
12344
+ * other runs never produce a false warning, and nothing is ever restored,
12345
+ * so concurrent executes cannot race the patch state.
11914
12346
  */
11915
12347
  var InProcessRunner = class {
11916
12348
  onEscalation;
@@ -11923,47 +12355,14 @@ var InProcessRunner = class {
11923
12355
  }
11924
12356
  async execute(wf, ctx, args) {
11925
12357
  if (wf.kind !== "workflow") throw new TypeError("InProcessRunner executes closure Workflow values only; CompiledWorkflow runs in the worker sandbox (@rulvar/planner, M6)");
11926
- const devMode = process.env.NODE_ENV !== "production";
11927
- let restore;
11928
- if (devMode) {
11929
- const priorNow = Date.now;
11930
- const priorRandom = Math.random;
11931
- let warnedNow = false;
11932
- let warnedRandom = false;
11933
- const libraryCaller = () => {
11934
- const caller = (/* @__PURE__ */ new Error()).stack?.split("\n")[3];
11935
- return caller !== void 0 && caller.includes("node_modules");
11936
- };
11937
- Date.now = function rulvarPatchedDateNow() {
11938
- if (!warnedNow && !libraryCaller()) {
11939
- warnedNow = true;
11940
- process.emitWarning("bare Date.now() called inside a rulvar run; use ctx.now() so the value is journaled and stable on replay", {
11941
- code: "RULVAR_BARE_DATE_NOW",
11942
- type: "RulvarWarning"
11943
- });
11944
- }
11945
- return priorNow();
11946
- };
11947
- Math.random = function rulvarPatchedMathRandom() {
11948
- if (!warnedRandom && !libraryCaller()) {
11949
- warnedRandom = true;
11950
- process.emitWarning("bare Math.random() called inside a rulvar run; use ctx.random() so the value is journaled and stable on replay", {
11951
- code: "RULVAR_BARE_MATH_RANDOM",
11952
- type: "RulvarWarning"
11953
- });
11954
- }
11955
- return priorRandom();
11956
- };
11957
- restore = () => {
11958
- Date.now = priorNow;
11959
- Math.random = priorRandom;
11960
- };
11961
- }
11962
- try {
11963
- return await wf.body(ctx, args);
11964
- } finally {
11965
- restore?.();
12358
+ if (process.env.NODE_ENV !== "production") {
12359
+ patchGlobalsOnce();
12360
+ return detection.run({
12361
+ warnedNow: false,
12362
+ warnedRandom: false
12363
+ }, () => wf.body(ctx, args));
11966
12364
  }
12365
+ return await wf.body(ctx, args);
11967
12366
  }
11968
12367
  };
11969
12368
  //#endregion
@@ -12000,10 +12399,13 @@ function createEngine(options) {
12000
12399
  const runner = new InProcessRunner(options.onEscalation === void 0 ? void 0 : { onEscalation: options.onEscalation });
12001
12400
  const mintRunId = createCanonicalIdMinter();
12002
12401
  const realNow = Date.now.bind(globalThis);
12402
+ const pricingOf = (servedBy) => {
12403
+ const { adapterId, model } = parseModelRef(servedBy);
12404
+ return resolvePricing(servedBy, options.pricing, adapters.get(adapterId)?.caps(model).pricing);
12405
+ };
12003
12406
  const priceUsd = (servedBy, usage) => {
12004
12407
  if (servedBy === void 0) return;
12005
- const { adapterId, model } = parseModelRef(servedBy);
12006
- const pricing = resolvePricing(servedBy, options.pricing, adapters.get(adapterId)?.caps(model).pricing);
12408
+ const pricing = pricingOf(servedBy);
12007
12409
  if (pricing === void 0) return;
12008
12410
  return priceUsdOf(pricing, usage);
12009
12411
  };
@@ -12029,6 +12431,7 @@ function createEngine(options) {
12029
12431
  lifetimeSpawnCap: options.budgetDefaults?.lifetimeSpawnCap ?? 500,
12030
12432
  events: { emit: (body) => bus.emit(body, rootSpanId) },
12031
12433
  priceUsd,
12434
+ pricingOf,
12032
12435
  ...budgetSeed === void 0 ? {} : { seed: budgetSeed }
12033
12436
  });
12034
12437
  const invalidated = new Set(resumeCtx?.invalidate ?? []);
@@ -12132,6 +12535,7 @@ function createEngine(options) {
12132
12535
  }
12133
12536
  },
12134
12537
  priceUsd: (servedBy, usage) => priceUsd(servedBy, usage),
12538
+ pricingOf,
12135
12539
  runSignal: controller.signal,
12136
12540
  ...defaults.isolation === void 0 ? {} : { isolation: defaults.isolation },
12137
12541
  ...options.onEscalation === void 0 ? {} : { onEscalation: options.onEscalation },
@@ -12242,7 +12646,7 @@ function createEngine(options) {
12242
12646
  dropped: internals.dropped,
12243
12647
  pending,
12244
12648
  usage: ledger.usage,
12245
- cost: buildCostReport(internals.cost, ledger.usd)
12649
+ cost: costReportFromJournal(replayer.snapshot(), priceUsd)
12246
12650
  };
12247
12651
  if (value !== void 0 && (status === "ok" || status === "exhausted")) outcome.value = value;
12248
12652
  if (wireError !== void 0) outcome.error = wireError;
@@ -12675,4 +13079,4 @@ function createSandboxBridge(ctx, options) {
12675
13079
  };
12676
13080
  }
12677
13081
  //#endregion
12678
- export { AWAIT_SCHEMA, AdmissionController, AdmissionRejectedError, AgentCallError, BUDGET_ABORT_REASON, BudgetExhaustedError, CANCEL_AGENT_SCHEMA, CHECKPOINT_FORMAT_V1, CLAIM_STATEMENT_MAX_CHARS, CLAIM_TTL_DAYS, COMPACTION_SUMMARY_PREFIX, CURRENT_HASH_VERSION, ConfigError, DEFAULT_CHILD_BUDGET_FRACTION, DEFAULT_COMPACTION_THRESHOLD, DEFAULT_ESCALATION_LIMITS, DEFAULT_FLAT_RESERVE_USD, DEFAULT_MAX_CHILDREN_PER_NODE, DEFAULT_MAX_DEPTH, DEFAULT_MAX_OSCILLATIONS_PER_KEY, DEFAULT_MAX_PINNED_WORKTREES, DEFAULT_MAX_REVISIONS_PER_RUN, DEFAULT_MAX_TOTAL_SPAWNS, DEFAULT_MAX_TURNS, DEFAULT_MODEL_RETRY_ATTEMPTS, DEFAULT_NO_PROGRESS_TURNS, DEFAULT_PER_RUN_CONCURRENCY, DEFAULT_RETRY_POLICY, DEFAULT_STREAM_IDLE_TIMEOUT_MS, DedupIndex, EMIT_RESULT_TOOL, EMPTY_SCHEMA_HASH, EMPTY_TOOLSET_HASH, ESCALATE_TOOL_NAME, ESCALATION_REPORT_SCHEMA, ESCALATION_REQUEST_SCHEMA, EventBus, ExternalRegistry, FINISH_SCHEMA, FINISH_TOOL_NAME, FileModelKnowledgeStore, FileTranscriptStore, GitWorktreeProvider, INBOX_PROPOSAL_TTL_DAYS, InMemoryStore, InMemoryTranscriptStore, InProcessRunner, InvalidResolutionError, JournalCompatibilityError, JournalMatcher, JournalMissError, JournalOrderViolation, JsonlFileStore, KB_ACTIVE_CLAIMS_CAP, KB_CARD_RENDER_BUDGET_CHARS, KeyedLimiter, KnowledgeCasError, LARGE_VALUE_WARN_BYTES, LEGACY_LTID_PREFIX, LEGACY_SIGNATURE_INPUTS, LINEAGE_SIG_VERSION, LeaseHeldError, LineageIndex, MASKED_SECRET, MAX_DEPTH_CEILING, ModelRetry, NoProgressDetector, NonSerializableValueError, ORCHESTRATE_WORKFLOW_NAME, OrchestratorCapConfigError, PARALLEL_AGENTS_SCHEMA, ParallelSiteCounter, PlanInvariantError, ROLE_EFFORT_DEFAULTS, ROOT_ACCOUNT, ROOT_SCOPE, RUN_PROFILES, ReplayPlanHashMismatch, Replayer, ResolutionArbiter, ResolutionFold, RulvarError, RunBudget, SPAWN_AGENT_SCHEMA, SandboxError, ScriptRejected, Semaphore, SpanRegistry, TOOL_NAME_PATTERN, TerminationAccount, WAIT_FOR_EVENTS_SCHEMA, WAIT_FOR_EVENTS_TOOL_NAME, WAKE_SUMMARY_RENDER_BUDGET_CHARS, admissionReserveUsd, agentErrorFromWire, agentErrorToWire, agentScope, applyClaimOps, applyStructuredOutputTier, approachSigCoarse, approachSigOf, archiveDeprecatedModelOps, atCompactionThreshold, buildAbandonFold, buildAdapterRegistry, buildCostReport, buildDeriverRegistry, buildOrchestratorTools, buildTerminationInitValue, buildToolContext, canRideLoopTurn, canonicalIsolationTag, canonicalizeLadder, canonicalizeSchema, capIssues, capsHashOf, checkFloors, checkpointRefFor, childCoveragePrefix, claimExpired, claimExpiry, claimIssues, claimOpIssues, classifyAgentError, classifyAttemptOutcome, collectDeclaredLadders, compactMessages, compilePermissionChain, compilePermissionPreset, compileVerifiedLayer, costReportFromJournal, countsAgainstLimit, createCanonicalIdMinter, createCtx, createEngine, createSandboxBridge, currentOnlyKeyRing, decodeCheckpoint, defineWorkflow, deriveContentKey, deriverV1, deriverV2, digestOf, dispositionHook, emptyDigestBlocks, emptyToolset, encodeCheckpoint, entryUsageSlices, escalateTool, evaluatePermission, evaluateReuse, executeWorkflow, exhaustionCodeOf, extractCandidate, failoverTriggerOf, fallbackTriggerOf, filterClaimsForRun, finalizeFires, foldTermination, formatRePrompt, formatScopePath, hashWorkflowBody, hashWorkflowSource, identityJcs, isEscalated, isSchemaPairSpec, isStandardSchemaSpec, isStrictCompatibleSchema, kMaxOf, knowledgeHash, ladderLengthOf, ladderRungChoice, lexShellCommand, liftRetainedParts, lineageWeightOf, makeOrchestratorWorkflow, maskSecrets, maskSecretsDeep, maskSecretsJson, matchArgvPattern, matchShellCommand, mcp, mergeUsageLimits, modelEpochOf, modelKnowledgeCard, modelSpecIdentity, needsSeparateExtract, nextFailover, nodeLinkKey, normalizeApproachTag, normalizeEntry, normalizeFallbacks, orchestrate, parallelScope, parseModelRef, parseScopePath, phiInitialOf, pipelineScope, planNodeScope, priceEntryUsage, priceUsdOf, profileCard, profileRegistrySnapshotHash, projectHistory, projectIdentity, projectToJsonSchema, proposalStatement, providerOf, readTerminationInit, registryKeyRing, remeasureQueue, replayDisposition, resolveModelInvocation, resolvePricing, resolveToolset, retryClassOf, retryDelayMs, roleConfiguredInRouting, roundOneDisposition, runAgent, runProfile, scanJournalCompatibility, schemaHash, schemaHashOfSpec, selectStructuredOutputTier, shouldCompact, spawnDepthOf, summarizeInstruction, summarizeOutput, terminationConfigDrift, tierWithinCaps, toApprovalDecision, toJournalValue, tool, toolContract, toolsetHash, ttlState, validateEditorialCommit, validateEntryShape, validateEscalationLimits, validateEscalationReport, validateSchemaSpec, validateTerminationLimits, workflowScope, workflowSourceRef, wrapJournalStore, wrapTranscriptStore };
13082
+ export { AWAIT_SCHEMA, AdmissionController, AdmissionRejectedError, AgentCallError, BUDGET_ABORT_REASON, BudgetExhaustedError, CANCEL_AGENT_SCHEMA, CHECKPOINT_FORMAT_V1, CLAIM_STATEMENT_MAX_CHARS, CLAIM_TTL_DAYS, COMPACTION_SUMMARY_PREFIX, CURRENT_HASH_VERSION, ConfigError, DEFAULT_CHILD_BUDGET_FRACTION, DEFAULT_COMPACTION_THRESHOLD, DEFAULT_ESCALATION_LIMITS, DEFAULT_FLAT_RESERVE_USD, DEFAULT_MAX_CHILDREN_PER_NODE, DEFAULT_MAX_DEPTH, DEFAULT_MAX_OSCILLATIONS_PER_KEY, DEFAULT_MAX_PINNED_WORKTREES, DEFAULT_MAX_REVISIONS_PER_RUN, DEFAULT_MAX_TOTAL_SPAWNS, DEFAULT_MAX_TURNS, DEFAULT_MODEL_RETRY_ATTEMPTS, DEFAULT_NO_PROGRESS_TURNS, DEFAULT_PER_RUN_CONCURRENCY, DEFAULT_RETRY_POLICY, DEFAULT_STREAM_IDLE_TIMEOUT_MS, DedupIndex, EMIT_RESULT_TOOL, EMPTY_SCHEMA_HASH, EMPTY_TOOLSET_HASH, ESCALATE_TOOL_NAME, ESCALATION_REPORT_SCHEMA, ESCALATION_REQUEST_SCHEMA, EventBus, ExternalRegistry, FINISH_SCHEMA, FINISH_TOOL_NAME, FileModelKnowledgeStore, FileTranscriptStore, GitWorktreeProvider, INBOX_PROPOSAL_TTL_DAYS, InMemoryStore, InMemoryTranscriptStore, InProcessRunner, InvalidResolutionError, JournalCompatibilityError, JournalMatcher, JournalMissError, JournalOrderViolation, JsonlFileStore, KB_ACTIVE_CLAIMS_CAP, KB_CARD_RENDER_BUDGET_CHARS, KeyedLimiter, KnowledgeCasError, LARGE_VALUE_WARN_BYTES, LEGACY_LTID_PREFIX, LEGACY_SIGNATURE_INPUTS, LINEAGE_SIG_VERSION, LeaseHeldError, LineageIndex, MASKED_SECRET, MAX_DEPTH_CEILING, ModelRetry, NoProgressDetector, NonSerializableValueError, ORCHESTRATE_WORKFLOW_NAME, OrchestratorCapConfigError, PARALLEL_AGENTS_SCHEMA, ParallelSiteCounter, PlanInvariantError, ROLE_EFFORT_DEFAULTS, ROOT_ACCOUNT, ROOT_SCOPE, RUN_PROFILES, ReplayPlanHashMismatch, Replayer, ResolutionArbiter, ResolutionFold, RulvarError, RunBudget, SPAWN_AGENT_SCHEMA, SandboxError, ScriptRejected, Semaphore, SpanRegistry, TOOL_NAME_PATTERN, TerminationAccount, WAIT_FOR_EVENTS_SCHEMA, WAIT_FOR_EVENTS_TOOL_NAME, WAKE_SUMMARY_RENDER_BUDGET_CHARS, admissionReserveUsd, affordableOutputTokens, agentErrorFromWire, agentErrorToWire, agentScope, applyClaimOps, applyStructuredOutputTier, approachSigCoarse, approachSigOf, archiveDeprecatedModelOps, atCompactionThreshold, buildAbandonFold, buildAdapterRegistry, buildCostReport, buildDeriverRegistry, buildOrchestratorTools, buildTerminationInitValue, buildToolContext, canRideLoopTurn, canonicalIsolationTag, canonicalizeLadder, canonicalizeSchema, capIssues, capsHashOf, checkFloors, checkpointRefFor, childCoveragePrefix, claimExpired, claimExpiry, claimIssues, claimOpIssues, classifyAgentError, classifyAttemptOutcome, collectDeclaredLadders, compactMessages, compilePermissionChain, compilePermissionPreset, compileVerifiedLayer, costReportFromJournal, countsAgainstLimit, createCanonicalIdMinter, createCtx, createEngine, createSandboxBridge, currentOnlyKeyRing, decodeCheckpoint, defineWorkflow, deriveContentKey, deriverV1, deriverV2, digestOf, dispositionHook, emptyDigestBlocks, emptyToolset, encodeCheckpoint, entryUsageSlices, escalateTool, evaluatePermission, evaluateReuse, executeWorkflow, exhaustionCodeOf, extractCandidate, failoverTriggerOf, fallbackTriggerOf, filterClaimsForRun, finalizeFires, foldTermination, formatRePrompt, formatScopePath, hashWorkflowBody, hashWorkflowSource, identityJcs, isEscalated, isSchemaPairSpec, isStandardSchemaSpec, isStrictCompatibleSchema, kMaxOf, knowledgeHash, ladderLengthOf, ladderRungChoice, lexShellCommand, liftRetainedParts, lineageWeightOf, makeOrchestratorWorkflow, maskSecrets, maskSecretsDeep, maskSecretsJson, matchArgvPattern, matchShellCommand, mcp, mergeUsageLimits, modelEpochOf, modelKnowledgeCard, modelSpecIdentity, needsSeparateExtract, nextFailover, nodeLinkKey, normalizeApproachTag, normalizeEntry, normalizeFallbacks, orchestrate, parallelScope, parseModelRef, parseScopePath, phiInitialOf, pipelineScope, planNodeScope, priceEntryUsage, priceUsdOf, profileCard, profileRegistrySnapshotHash, projectHistory, projectIdentity, projectToJsonSchema, proposalStatement, providerOf, readTerminationInit, registryKeyRing, remeasureQueue, replayDisposition, resolveModelInvocation, resolvePricing, resolveToolset, retryClassOf, retryDelayMs, roleConfiguredInRouting, roundOneDisposition, runAgent, runProfile, scanJournalCompatibility, schemaHash, schemaHashOfSpec, selectStructuredOutputTier, shouldCompact, spawnDepthOf, summarizeInstruction, summarizeOutput, terminationConfigDrift, tierWithinCaps, toApprovalDecision, toJournalValue, tool, toolContract, toolsetHash, ttlState, validateEditorialCommit, validateEntryShape, validateEscalationLimits, validateEscalationReport, validateSchemaSpec, validateTerminationLimits, workflowScope, workflowSourceRef, wrapJournalStore, wrapTranscriptStore };