@rulvar/core 1.101.0 → 1.103.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.d.ts +45 -10
- package/dist/index.js +215 -91
- package/package.json +1 -1
package/dist/index.d.ts
CHANGED
|
@@ -828,14 +828,18 @@ interface EntryBillingFold {
|
|
|
828
828
|
/**
|
|
829
829
|
* The billing fold over one terminal entry (RV504), shared by the
|
|
830
830
|
* CostReport and invoice folds so the total, every breakdown, and the
|
|
831
|
-
* per-row prices can never disagree.
|
|
832
|
-
*
|
|
831
|
+
* per-row prices can never disagree. Coverage is decided per MODEL with
|
|
832
|
+
* the symmetric key (RV604): for every model whose per-dispatch
|
|
833
|
+
* `providerCalls` sum to exactly its usage, each call is priced
|
|
833
834
|
* individually, so a nonlinear long-context tier fires per REQUEST,
|
|
834
835
|
* which is the pricing contract's stated semantics; an aggregate that
|
|
835
836
|
* crossed a threshold no single request crossed no longer re-prices
|
|
836
|
-
*
|
|
837
|
-
* no records, or records that do not
|
|
838
|
-
* before: the per-model aggregate
|
|
837
|
+
* that model (the ninth-experiment 52% overreport, and the round-52
|
|
838
|
+
* multi-role default). A model with no records, or records that do not
|
|
839
|
+
* cover its usage, folds exactly as before: the per-model aggregate
|
|
840
|
+
* slices of {@link priceEntryUsage}. `fullyAttributed` is true only
|
|
841
|
+
* when every slice model is covered and no record names a model absent
|
|
842
|
+
* from the slices.
|
|
839
843
|
*/
|
|
840
844
|
declare function priceEntryBilling(entry: JournalEntry, priceUsd: (servedBy: ModelRef, usage: Usage, seq?: number) => number | undefined): EntryBillingFold;
|
|
841
845
|
/**
|
|
@@ -4719,26 +4723,47 @@ interface RunAgentOptions<S extends SchemaSpec = JsonSchema> {
|
|
|
4719
4723
|
* already promised the raised cap), never re-admitted or re-announced,
|
|
4720
4724
|
* and a restored window entry keeps the summary's
|
|
4721
4725
|
* finalizationWindowEntered truthful even when a later grant moved the
|
|
4722
|
-
* counts back out of the window.
|
|
4723
|
-
*
|
|
4724
|
-
*
|
|
4726
|
+
* counts back out of the window.
|
|
4727
|
+
*
|
|
4728
|
+
* Both hooks are AWAITED before the thing they authorize becomes
|
|
4729
|
+
* observable (RV601): a grant lifts no expiry and queues no notice
|
|
4730
|
+
* until its decision is durable, and the window regime binds no call
|
|
4731
|
+
* until its entry is. A rejected append therefore leaves the grant
|
|
4732
|
+
* unissued and the entry unrecorded, and the rejection propagates
|
|
4733
|
+
* exactly like a failed boundary checkpoint rather than being
|
|
4734
|
+
* swallowed. Pressure notices stay events and are never journaled.
|
|
4735
|
+
* Absent, the loop is byte-identical to before.
|
|
4725
4736
|
*/
|
|
4726
4737
|
toolBudgetDurability?: {
|
|
4727
4738
|
restored?: {
|
|
4728
4739
|
extensionsGranted: number;
|
|
4729
4740
|
finalizationWindowEntered: boolean;
|
|
4741
|
+
/**
|
|
4742
|
+
* The effective cap the journaled grant announced (RV602). It
|
|
4743
|
+
* anchors the resumed ceiling, because the live `maxToolCalls`
|
|
4744
|
+
* and `increment` are not part of the dispatch identity and may
|
|
4745
|
+
* legitimately drift between segments: without the anchor the two
|
|
4746
|
+
* recovery paths (pure replay, which reads the journal, and live
|
|
4747
|
+
* resume, which recomputed) disagreed, and a promise already made
|
|
4748
|
+
* to the model could be silently revoked. Validated as a
|
|
4749
|
+
* persistent inlet: a non-integer, or one below the base cap, is
|
|
4750
|
+
* ignored with a warning, leaving the count derivation as the
|
|
4751
|
+
* floor. Grants taken AFTER the restore point still measure the
|
|
4752
|
+
* current increment from this anchor.
|
|
4753
|
+
*/
|
|
4754
|
+
cap?: number;
|
|
4730
4755
|
};
|
|
4731
4756
|
onExtensionGrant?: (grant: {
|
|
4732
4757
|
grant: number;
|
|
4733
4758
|
maxExtensions: number;
|
|
4734
4759
|
toolCallsUsed: number;
|
|
4735
4760
|
cap: number;
|
|
4736
|
-
}) => void
|
|
4761
|
+
}) => Promise<void>;
|
|
4737
4762
|
onWindowEntry?: (entry: {
|
|
4738
4763
|
remaining: number;
|
|
4739
4764
|
reserveCalls: number;
|
|
4740
4765
|
budget: FinalizationWindowBudget;
|
|
4741
|
-
}) => void
|
|
4766
|
+
}) => Promise<void>;
|
|
4742
4767
|
};
|
|
4743
4768
|
/** Emits agent:stream deltas when true (telemetry only). */
|
|
4744
4769
|
stream?: boolean;
|
|
@@ -9542,6 +9567,16 @@ interface InvoiceExport {
|
|
|
9542
9567
|
}>;
|
|
9543
9568
|
/** Rows whose reconciliation is not 'provider-id-present'. */
|
|
9544
9569
|
reconciliationFailures: number;
|
|
9570
|
+
/**
|
|
9571
|
+
* USD of allocation pools that had a target and no row to carry it
|
|
9572
|
+
* (RV605). The dust pass refuses to move such dollars onto another
|
|
9573
|
+
* model's rows just to make the column sum, so on the (pathological)
|
|
9574
|
+
* journals where this happens the flat `allocatedUsd` sum reproduces
|
|
9575
|
+
* `totalUsd` minus this amount. Absent when zero, which is every
|
|
9576
|
+
* well-formed journal: the per-slice remainder rows guarantee a row
|
|
9577
|
+
* wherever a slice has usage.
|
|
9578
|
+
*/
|
|
9579
|
+
unallocatedUsd?: number;
|
|
9545
9580
|
/** Rows carrying `usageUnknown`; present when at least one does. */
|
|
9546
9581
|
usageUnknownRows?: number;
|
|
9547
9582
|
/** Present and true when any contributing entry carried approximate usage. */
|
package/dist/index.js
CHANGED
|
@@ -2401,12 +2401,11 @@ const BILLING_FIELDS = [
|
|
|
2401
2401
|
"cacheReadTokens",
|
|
2402
2402
|
"cacheWriteTokens"
|
|
2403
2403
|
];
|
|
2404
|
-
/**
|
|
2405
|
-
function
|
|
2406
|
-
if (slices.length === 0 || records.length === 0) return false;
|
|
2404
|
+
/** Per-model billing-counter sums of either side of the coverage test. */
|
|
2405
|
+
function sumUsageByModel(items) {
|
|
2407
2406
|
const sums = /* @__PURE__ */ new Map();
|
|
2408
|
-
for (const
|
|
2409
|
-
let sum = sums.get(
|
|
2407
|
+
for (const item of items) {
|
|
2408
|
+
let sum = sums.get(item.servedBy);
|
|
2410
2409
|
if (sum === void 0) {
|
|
2411
2410
|
sum = {
|
|
2412
2411
|
fields: {
|
|
@@ -2417,54 +2416,61 @@ function callsCoverSlices(slices, records) {
|
|
|
2417
2416
|
},
|
|
2418
2417
|
reasoning: 0
|
|
2419
2418
|
};
|
|
2420
|
-
sums.set(
|
|
2419
|
+
sums.set(item.servedBy, sum);
|
|
2421
2420
|
}
|
|
2422
|
-
for (const field of BILLING_FIELDS) sum.fields[field] = (sum.fields[field] ?? 0) +
|
|
2423
|
-
sum.reasoning +=
|
|
2421
|
+
for (const field of BILLING_FIELDS) sum.fields[field] = (sum.fields[field] ?? 0) + item.usage[field];
|
|
2422
|
+
sum.reasoning += item.usage.reasoningTokens ?? 0;
|
|
2424
2423
|
}
|
|
2425
|
-
|
|
2426
|
-
|
|
2427
|
-
|
|
2428
|
-
|
|
2429
|
-
|
|
2424
|
+
return sums;
|
|
2425
|
+
}
|
|
2426
|
+
/**
|
|
2427
|
+
* The models whose records exactly cover their usage, counter for
|
|
2428
|
+
* counter. BOTH sides aggregate by `servedBy` (RV604): the usage slices
|
|
2429
|
+
* split one model's spend by role (a schema fires a same-model extract
|
|
2430
|
+
* by default, so several slices of one model are the ordinary shape),
|
|
2431
|
+
* and comparing a per-role slice against the per-model record sum
|
|
2432
|
+
* refused coverage on exactly that default, re-tiering aggregates no
|
|
2433
|
+
* single request crossed. The symmetric key compares model sum against
|
|
2434
|
+
* model sum, so coverage is per model: a covered model prices per call
|
|
2435
|
+
* while an uncovered one honestly keeps the aggregate basis.
|
|
2436
|
+
*/
|
|
2437
|
+
function coveredModels(slices, records) {
|
|
2438
|
+
const covered = /* @__PURE__ */ new Set();
|
|
2439
|
+
if (slices.length === 0 || records.length === 0) return covered;
|
|
2440
|
+
const recordSums = sumUsageByModel(records);
|
|
2441
|
+
const sliceSums = sumUsageByModel(slices);
|
|
2442
|
+
for (const [model, sliceSum] of sliceSums) {
|
|
2443
|
+
const recordSum = recordSums.get(model);
|
|
2444
|
+
if (recordSum === void 0) continue;
|
|
2445
|
+
if (BILLING_FIELDS.every((field) => recordSum.fields[field] === sliceSum.fields[field]) && recordSum.reasoning === sliceSum.reasoning) covered.add(model);
|
|
2430
2446
|
}
|
|
2431
|
-
|
|
2432
|
-
return true;
|
|
2447
|
+
return covered;
|
|
2433
2448
|
}
|
|
2434
2449
|
/**
|
|
2435
2450
|
* The billing fold over one terminal entry (RV504), shared by the
|
|
2436
2451
|
* CostReport and invoice folds so the total, every breakdown, and the
|
|
2437
|
-
* per-row prices can never disagree.
|
|
2438
|
-
*
|
|
2452
|
+
* per-row prices can never disagree. Coverage is decided per MODEL with
|
|
2453
|
+
* the symmetric key (RV604): for every model whose per-dispatch
|
|
2454
|
+
* `providerCalls` sum to exactly its usage, each call is priced
|
|
2439
2455
|
* individually, so a nonlinear long-context tier fires per REQUEST,
|
|
2440
2456
|
* which is the pricing contract's stated semantics; an aggregate that
|
|
2441
2457
|
* crossed a threshold no single request crossed no longer re-prices
|
|
2442
|
-
*
|
|
2443
|
-
* no records, or records that do not
|
|
2444
|
-
* before: the per-model aggregate
|
|
2458
|
+
* that model (the ninth-experiment 52% overreport, and the round-52
|
|
2459
|
+
* multi-role default). A model with no records, or records that do not
|
|
2460
|
+
* cover its usage, folds exactly as before: the per-model aggregate
|
|
2461
|
+
* slices of {@link priceEntryUsage}. `fullyAttributed` is true only
|
|
2462
|
+
* when every slice model is covered and no record names a model absent
|
|
2463
|
+
* from the slices.
|
|
2445
2464
|
*/
|
|
2446
2465
|
function priceEntryBilling(entry, priceUsd) {
|
|
2447
2466
|
const slices = entryUsageSlices(entry);
|
|
2448
2467
|
const records = entry.providerCalls ?? [];
|
|
2449
|
-
|
|
2450
|
-
const aggregate = priceEntryUsage(entry, priceUsd);
|
|
2451
|
-
return {
|
|
2452
|
-
units: aggregate.priced.map((slice) => ({
|
|
2453
|
-
source: "slice",
|
|
2454
|
-
servedBy: slice.servedBy,
|
|
2455
|
-
usage: slice.usage,
|
|
2456
|
-
...slice.role === void 0 ? {} : { role: slice.role },
|
|
2457
|
-
usd: slice.usd
|
|
2458
|
-
})),
|
|
2459
|
-
usd: aggregate.usd,
|
|
2460
|
-
unpriced: aggregate.unpriced,
|
|
2461
|
-
fullyAttributed: false
|
|
2462
|
-
};
|
|
2463
|
-
}
|
|
2468
|
+
const covered = coveredModels(slices, records);
|
|
2464
2469
|
const units = [];
|
|
2465
2470
|
const unpricedByModel = /* @__PURE__ */ new Map();
|
|
2466
2471
|
let usd = 0;
|
|
2467
2472
|
for (const record of records) {
|
|
2473
|
+
if (!covered.has(record.servedBy)) continue;
|
|
2468
2474
|
const price = priceUsd(record.servedBy, record.usage, entry.seq);
|
|
2469
2475
|
if (price === void 0 || !Number.isFinite(price) || price < 0) {
|
|
2470
2476
|
const sum = unpricedByModel.get(record.servedBy) ?? {
|
|
@@ -2489,6 +2495,32 @@ function priceEntryBilling(entry, priceUsd) {
|
|
|
2489
2495
|
usd: price
|
|
2490
2496
|
});
|
|
2491
2497
|
}
|
|
2498
|
+
for (const slice of slices) {
|
|
2499
|
+
if (covered.has(slice.servedBy)) continue;
|
|
2500
|
+
const price = priceUsd(slice.servedBy, slice.usage, entry.seq);
|
|
2501
|
+
if (price === void 0 || !Number.isFinite(price) || price < 0) {
|
|
2502
|
+
const sum = unpricedByModel.get(slice.servedBy) ?? {
|
|
2503
|
+
inputTokens: 0,
|
|
2504
|
+
outputTokens: 0,
|
|
2505
|
+
cacheReadTokens: 0,
|
|
2506
|
+
cacheWriteTokens: 0
|
|
2507
|
+
};
|
|
2508
|
+
for (const field of BILLING_FIELDS) sum[field] += slice.usage[field];
|
|
2509
|
+
const reasoning = (sum.reasoningTokens ?? 0) + (slice.usage.reasoningTokens ?? 0);
|
|
2510
|
+
if (reasoning > 0) sum.reasoningTokens = reasoning;
|
|
2511
|
+
unpricedByModel.set(slice.servedBy, sum);
|
|
2512
|
+
continue;
|
|
2513
|
+
}
|
|
2514
|
+
usd += price;
|
|
2515
|
+
units.push({
|
|
2516
|
+
source: "slice",
|
|
2517
|
+
servedBy: slice.servedBy,
|
|
2518
|
+
usage: slice.usage,
|
|
2519
|
+
...slice.role === void 0 ? {} : { role: slice.role },
|
|
2520
|
+
usd: price
|
|
2521
|
+
});
|
|
2522
|
+
}
|
|
2523
|
+
const fullyAttributed = slices.length > 0 && slices.every((slice) => covered.has(slice.servedBy)) && records.every((record) => slices.some((slice) => slice.servedBy === record.servedBy));
|
|
2492
2524
|
return {
|
|
2493
2525
|
units,
|
|
2494
2526
|
usd,
|
|
@@ -2496,7 +2528,7 @@ function priceEntryBilling(entry, priceUsd) {
|
|
|
2496
2528
|
servedBy,
|
|
2497
2529
|
usage
|
|
2498
2530
|
})),
|
|
2499
|
-
fullyAttributed
|
|
2531
|
+
fullyAttributed
|
|
2500
2532
|
};
|
|
2501
2533
|
}
|
|
2502
2534
|
/**
|
|
@@ -8404,16 +8436,29 @@ const USAGE_FIELDS = [
|
|
|
8404
8436
|
"cacheReadTokens",
|
|
8405
8437
|
"cacheWriteTokens"
|
|
8406
8438
|
];
|
|
8407
|
-
/**
|
|
8408
|
-
|
|
8439
|
+
/**
|
|
8440
|
+
* One usage slice minus ITS OWN records' sum, clamped at zero per field
|
|
8441
|
+
* (RV605). A record belongs to a slice when it names the same serving
|
|
8442
|
+
* model, and the same role when the slice carries one; a slice written
|
|
8443
|
+
* without a role (the legacy whole-entry fallback) absorbs every record
|
|
8444
|
+
* of its model, which is exactly the pre-RV605 arithmetic for
|
|
8445
|
+
* single-model entries. Computing the remainder per slice instead of
|
|
8446
|
+
* per entry is what keeps an orphaned model's spend on a row of that
|
|
8447
|
+
* model: the whole-entry remainder was published under `entry.servedBy`,
|
|
8448
|
+
* so a slice with no records left its allocation pool rowless and the
|
|
8449
|
+
* dust pass moved its dollars onto another model's row.
|
|
8450
|
+
*/
|
|
8451
|
+
function sliceRemainder(slice, records) {
|
|
8409
8452
|
const remainder = {
|
|
8410
|
-
inputTokens:
|
|
8411
|
-
outputTokens:
|
|
8412
|
-
cacheReadTokens:
|
|
8413
|
-
cacheWriteTokens:
|
|
8453
|
+
inputTokens: slice.usage.inputTokens,
|
|
8454
|
+
outputTokens: slice.usage.outputTokens,
|
|
8455
|
+
cacheReadTokens: slice.usage.cacheReadTokens,
|
|
8456
|
+
cacheWriteTokens: slice.usage.cacheWriteTokens
|
|
8414
8457
|
};
|
|
8415
|
-
let reasoning =
|
|
8458
|
+
let reasoning = slice.usage.reasoningTokens ?? 0;
|
|
8416
8459
|
for (const record of records) {
|
|
8460
|
+
if (record.servedBy !== slice.servedBy) continue;
|
|
8461
|
+
if (slice.role !== void 0 && record.role !== slice.role) continue;
|
|
8417
8462
|
for (const field of USAGE_FIELDS) remainder[field] = Math.max(0, remainder[field] - record.usage[field]);
|
|
8418
8463
|
reasoning = Math.max(0, reasoning - (record.usage.reasoningTokens ?? 0));
|
|
8419
8464
|
}
|
|
@@ -8437,9 +8482,16 @@ function totalTokens(usage) {
|
|
|
8437
8482
|
* association so the flat sum over `rows` reproduces `totalUsd`
|
|
8438
8483
|
* exactly. Rows on unpriced models keep zero: their spend is in
|
|
8439
8484
|
* `unpriced`, not in `totalUsd`.
|
|
8485
|
+
*
|
|
8486
|
+
* A pool with a target and NO rows refuses the transfer (RV605): its
|
|
8487
|
+
* dollars are excluded from the dust reconciliation and returned as the
|
|
8488
|
+
* unallocated share, because dumping them on the globally largest row
|
|
8489
|
+
* moved one model's spend onto another model's line just to make the
|
|
8490
|
+
* column sum. The returned amount is zero on every well-formed journal
|
|
8491
|
+
* (the per-slice remainder rows above guarantee a row wherever a slice
|
|
8492
|
+
* has usage), so the flat sum still reproduces `totalUsd` exactly there.
|
|
8440
8493
|
*/
|
|
8441
8494
|
function allocateRows(rows, entries, priceUsd, totalUsd) {
|
|
8442
|
-
if (rows.length === 0) return;
|
|
8443
8495
|
const targets = /* @__PURE__ */ new Map();
|
|
8444
8496
|
for (const entry of entries) {
|
|
8445
8497
|
if (entry.status === "running" || entry.usage === void 0) continue;
|
|
@@ -8455,6 +8507,9 @@ function allocateRows(rows, entries, priceUsd, totalUsd) {
|
|
|
8455
8507
|
if (pool === void 0) pools.set(key, [row]);
|
|
8456
8508
|
else pool.push(row);
|
|
8457
8509
|
}
|
|
8510
|
+
let unallocated = 0;
|
|
8511
|
+
for (const [key, target] of targets) if (target !== 0 && !pools.has(key)) unallocated += target;
|
|
8512
|
+
if (rows.length === 0) return unallocated;
|
|
8458
8513
|
for (const [key, members] of pools) {
|
|
8459
8514
|
const target = targets.get(key) ?? 0;
|
|
8460
8515
|
if (target === 0) continue;
|
|
@@ -8471,12 +8526,14 @@ function allocateRows(rows, entries, priceUsd, totalUsd) {
|
|
|
8471
8526
|
}
|
|
8472
8527
|
let absorber;
|
|
8473
8528
|
for (const row of rows) if (absorber === void 0 || row.allocatedUsd > absorber.allocatedUsd) absorber = row;
|
|
8474
|
-
if (absorber === void 0) return;
|
|
8529
|
+
if (absorber === void 0) return unallocated;
|
|
8530
|
+
const goal = totalUsd - unallocated;
|
|
8475
8531
|
for (let pass = 0; pass < 8; pass += 1) {
|
|
8476
8532
|
const flat = rows.reduce((acc, row) => acc + row.allocatedUsd, 0);
|
|
8477
|
-
if (flat ===
|
|
8478
|
-
absorber.allocatedUsd +=
|
|
8533
|
+
if (flat === goal) break;
|
|
8534
|
+
absorber.allocatedUsd += goal - flat;
|
|
8479
8535
|
}
|
|
8536
|
+
return unallocated;
|
|
8480
8537
|
}
|
|
8481
8538
|
/** A single row priced at its own model's rate; broken rates fold as unpriced. */
|
|
8482
8539
|
function rowUsd(priceUsd, servedBy, usage, seq) {
|
|
@@ -8548,13 +8605,16 @@ function invoiceFromJournal(entries, priceUsd, options) {
|
|
|
8548
8605
|
});
|
|
8549
8606
|
continue;
|
|
8550
8607
|
}
|
|
8551
|
-
|
|
8552
|
-
|
|
8553
|
-
const
|
|
8608
|
+
let remainderOrdinal = records.length + 1;
|
|
8609
|
+
for (const slice of entryUsageSlices(entry)) {
|
|
8610
|
+
const remainder = sliceRemainder(slice, records);
|
|
8611
|
+
if (remainder === void 0) continue;
|
|
8612
|
+
const usd = rowUsd(priceUsd, slice.servedBy, remainder, entry.seq);
|
|
8554
8613
|
rows.push({
|
|
8555
8614
|
...base,
|
|
8556
|
-
ordinal:
|
|
8557
|
-
servedBy:
|
|
8615
|
+
ordinal: remainderOrdinal,
|
|
8616
|
+
servedBy: slice.servedBy,
|
|
8617
|
+
...slice.role === void 0 ? {} : { role: slice.role },
|
|
8558
8618
|
outcome: "unattributed",
|
|
8559
8619
|
usage: remainder,
|
|
8560
8620
|
...entry.usageApprox === true ? { usageApprox: true } : {},
|
|
@@ -8563,9 +8623,10 @@ function invoiceFromJournal(entries, priceUsd, options) {
|
|
|
8563
8623
|
...mark,
|
|
8564
8624
|
reconciliation: "unattributed"
|
|
8565
8625
|
});
|
|
8626
|
+
remainderOrdinal += 1;
|
|
8566
8627
|
}
|
|
8567
8628
|
}
|
|
8568
|
-
allocateRows(rows, entries, priceUsd, report.grossUsd);
|
|
8629
|
+
const unallocatedUsd = allocateRows(rows, entries, priceUsd, report.grossUsd);
|
|
8569
8630
|
const usageApprox = report.usageApprox === true || report.abandoned.usageApprox === true;
|
|
8570
8631
|
return {
|
|
8571
8632
|
rows,
|
|
@@ -8574,6 +8635,7 @@ function invoiceFromJournal(entries, priceUsd, options) {
|
|
|
8574
8635
|
abandonedUsd: report.abandoned.usd,
|
|
8575
8636
|
pricingBasis: "per-call",
|
|
8576
8637
|
rowUsdNonAdditive: !everyEntryFullyAttributed,
|
|
8638
|
+
...unallocatedUsd === 0 ? {} : { unallocatedUsd },
|
|
8577
8639
|
unpriced: [...report.unpriced, ...report.abandoned.unpriced],
|
|
8578
8640
|
reconciliationFailures: rows.filter((row) => row.reconciliation !== "provider-id-present").length,
|
|
8579
8641
|
...(() => {
|
|
@@ -10927,7 +10989,17 @@ async function runAgent(options) {
|
|
|
10927
10989
|
let extensionGrants = 0;
|
|
10928
10990
|
let extensionEvidenceAtLastGrant = 0;
|
|
10929
10991
|
const pendingExtensionNotices = [];
|
|
10930
|
-
|
|
10992
|
+
/**
|
|
10993
|
+
* The ceiling the effective cap is measured from (RV602). The base cap
|
|
10994
|
+
* normally, so a fresh loop is byte-identical arithmetic; the journaled
|
|
10995
|
+
* cap of the restored grant when a resume carried one, so drifting live
|
|
10996
|
+
* limits cannot revoke a raise the journal already recorded. Grants
|
|
10997
|
+
* taken after the restore point count from here at the CURRENT
|
|
10998
|
+
* increment, which is the live policy doing what it should.
|
|
10999
|
+
*/
|
|
11000
|
+
let capBase = limits.maxToolCalls;
|
|
11001
|
+
let grantsOverCapBase = 0;
|
|
11002
|
+
const effectiveMaxToolCalls = () => capBase === void 0 ? void 0 : extension === void 0 ? capBase : capBase + grantsOverCapBase * extension.increment;
|
|
10931
11003
|
/** The limiter that ended the loop; rides the RV304 pressure snapshot. */
|
|
10932
11004
|
let limitLimiter;
|
|
10933
11005
|
/** True once the finalization reserve summary turn actually ran. */
|
|
@@ -10979,7 +11051,7 @@ async function runAgent(options) {
|
|
|
10979
11051
|
* join the history.
|
|
10980
11052
|
*/
|
|
10981
11053
|
const tryToolBudgetGrant = () => {
|
|
10982
|
-
if (extension === void 0 || limits.maxToolCalls === void 0) return false;
|
|
11054
|
+
if (extension === void 0 || limits.maxToolCalls === void 0 || capBase === void 0) return false;
|
|
10983
11055
|
if (extensionGrants >= extension.maxExtensions) return false;
|
|
10984
11056
|
if (extension.requireNewEvidence !== false) {
|
|
10985
11057
|
if ((guard?.evidenceCount() ?? 0) <= extensionEvidenceAtLastGrant) return false;
|
|
@@ -10989,22 +11061,28 @@ async function runAgent(options) {
|
|
|
10989
11061
|
const floor = extension.minHeadroomUsd ?? 0;
|
|
10990
11062
|
if (floor > 0 ? remaining < floor : remaining <= 0) return false;
|
|
10991
11063
|
}
|
|
10992
|
-
extensionGrants
|
|
10993
|
-
|
|
10994
|
-
const
|
|
10995
|
-
|
|
10996
|
-
|
|
11064
|
+
const grant = extensionGrants + 1;
|
|
11065
|
+
const cap = capBase + (grantsOverCapBase + 1) * extension.increment;
|
|
11066
|
+
const commit = () => {
|
|
11067
|
+
extensionGrants = grant;
|
|
11068
|
+
grantsOverCapBase += 1;
|
|
11069
|
+
extensionEvidenceAtLastGrant = guard?.evidenceCount() ?? 0;
|
|
11070
|
+
pendingExtensionNotices.push(toolBudgetExtensionNoticeText(grant, extension.maxExtensions, toolCallsUsed, cap));
|
|
11071
|
+
events?.emit({
|
|
11072
|
+
type: "log",
|
|
11073
|
+
level: "info",
|
|
11074
|
+
msg: `tool budget extended (grant ${String(grant)}/${String(extension.maxExtensions)}): maxToolCalls now ${String(cap)}`
|
|
11075
|
+
});
|
|
11076
|
+
return true;
|
|
11077
|
+
};
|
|
11078
|
+
const durable = options.toolBudgetDurability?.onExtensionGrant;
|
|
11079
|
+
if (durable === void 0) return commit();
|
|
11080
|
+
return durable({
|
|
11081
|
+
grant,
|
|
10997
11082
|
maxExtensions: extension.maxExtensions,
|
|
10998
11083
|
toolCallsUsed,
|
|
10999
11084
|
cap
|
|
11000
|
-
});
|
|
11001
|
-
pendingExtensionNotices.push(toolBudgetExtensionNoticeText(extensionGrants, extension.maxExtensions, toolCallsUsed, cap));
|
|
11002
|
-
events?.emit({
|
|
11003
|
-
type: "log",
|
|
11004
|
-
level: "info",
|
|
11005
|
-
msg: `tool budget extended (grant ${String(extensionGrants)}/${String(extension.maxExtensions)}): maxToolCalls now ${String(cap)}`
|
|
11006
|
-
});
|
|
11007
|
-
return true;
|
|
11085
|
+
}).then(commit);
|
|
11008
11086
|
};
|
|
11009
11087
|
const flushExtensionNotices = () => {
|
|
11010
11088
|
for (const text of pendingExtensionNotices.splice(0)) messages.push({
|
|
@@ -11058,19 +11136,26 @@ async function runAgent(options) {
|
|
|
11058
11136
|
if (finalizationWindow === void 0 || windowNoticeFired) return;
|
|
11059
11137
|
const state = windowActive();
|
|
11060
11138
|
if (state === void 0) return;
|
|
11061
|
-
|
|
11062
|
-
|
|
11063
|
-
|
|
11139
|
+
const commit = () => {
|
|
11140
|
+
windowEntered = true;
|
|
11141
|
+
windowNoticeFired = true;
|
|
11142
|
+
pendingWindowNotices.push(finalizationWindowNoticeText(state.remaining, finalizationWindow.reserveCalls, state.budget));
|
|
11143
|
+
events?.emit({
|
|
11144
|
+
type: "log",
|
|
11145
|
+
level: "info",
|
|
11146
|
+
msg: `finalization window entered: ${String(state.remaining)} of the reserved final ${String(finalizationWindow.reserveCalls)} ${state.budget} remain`
|
|
11147
|
+
});
|
|
11148
|
+
};
|
|
11149
|
+
const durable = options.toolBudgetDurability?.onWindowEntry;
|
|
11150
|
+
if (durable === void 0) {
|
|
11151
|
+
commit();
|
|
11152
|
+
return;
|
|
11153
|
+
}
|
|
11154
|
+
return durable({
|
|
11064
11155
|
remaining: state.remaining,
|
|
11065
11156
|
reserveCalls: finalizationWindow.reserveCalls,
|
|
11066
11157
|
budget: state.budget
|
|
11067
|
-
});
|
|
11068
|
-
pendingWindowNotices.push(finalizationWindowNoticeText(state.remaining, finalizationWindow.reserveCalls, state.budget));
|
|
11069
|
-
events?.emit({
|
|
11070
|
-
type: "log",
|
|
11071
|
-
level: "info",
|
|
11072
|
-
msg: `finalization window entered: ${String(state.remaining)} of the reserved final ${String(finalizationWindow.reserveCalls)} ${state.budget} remain`
|
|
11073
|
-
});
|
|
11158
|
+
}).then(commit);
|
|
11074
11159
|
};
|
|
11075
11160
|
const flushWindowNotices = () => {
|
|
11076
11161
|
for (const text of pendingWindowNotices.splice(0)) messages.push({
|
|
@@ -11127,6 +11212,7 @@ async function runAgent(options) {
|
|
|
11127
11212
|
extensionGrants = Math.min(extension.maxExtensions, Math.ceil((toolCallsUsed - limits.maxToolCalls) / extension.increment));
|
|
11128
11213
|
extensionEvidenceAtLastGrant = guard?.evidenceCount() ?? 0;
|
|
11129
11214
|
}
|
|
11215
|
+
const derivedCap = limits.maxToolCalls === void 0 || extension === void 0 ? void 0 : limits.maxToolCalls + extensionGrants * extension.increment;
|
|
11130
11216
|
const durableRestored = options.toolBudgetDurability?.restored;
|
|
11131
11217
|
if (extension !== void 0 && durableRestored !== void 0) {
|
|
11132
11218
|
const journaled = Math.min(extension.maxExtensions, durableRestored.extensionsGranted);
|
|
@@ -11135,6 +11221,16 @@ async function runAgent(options) {
|
|
|
11135
11221
|
extensionEvidenceAtLastGrant = guard?.evidenceCount() ?? 0;
|
|
11136
11222
|
}
|
|
11137
11223
|
}
|
|
11224
|
+
grantsOverCapBase = extensionGrants;
|
|
11225
|
+
const journaledCap = durableRestored?.cap;
|
|
11226
|
+
if (journaledCap !== void 0 && limits.maxToolCalls !== void 0 && derivedCap !== void 0) if (Number.isSafeInteger(journaledCap) && journaledCap >= limits.maxToolCalls) {
|
|
11227
|
+
capBase = Math.max(journaledCap, derivedCap);
|
|
11228
|
+
grantsOverCapBase = 0;
|
|
11229
|
+
} else events?.emit({
|
|
11230
|
+
type: "log",
|
|
11231
|
+
level: "warn",
|
|
11232
|
+
msg: `restored tool budget cap ${String(journaledCap)} is not an integer at or above the base cap ${String(limits.maxToolCalls)}; ignoring it and deriving from the counts`
|
|
11233
|
+
});
|
|
11138
11234
|
if (windowActive() !== void 0 || finalizationWindow !== void 0 && durableRestored?.finalizationWindowEntered === true) {
|
|
11139
11235
|
windowEntered = true;
|
|
11140
11236
|
windowNoticeFired = true;
|
|
@@ -11231,7 +11327,10 @@ async function runAgent(options) {
|
|
|
11231
11327
|
return cap !== void 0 && toolCallsUsed >= cap ? "maxToolCalls" : guard !== void 0 && guard.unitsExhausted() ? "toolUnits" : void 0;
|
|
11232
11328
|
};
|
|
11233
11329
|
let expiredLimiter = expiryOf();
|
|
11234
|
-
if (expiredLimiter === "maxToolCalls" && call.name !== options.terminalTool?.name
|
|
11330
|
+
if (expiredLimiter === "maxToolCalls" && call.name !== options.terminalTool?.name) {
|
|
11331
|
+
const attempt = tryToolBudgetGrant();
|
|
11332
|
+
if (typeof attempt === "boolean" ? attempt : await attempt) expiredLimiter = expiryOf();
|
|
11333
|
+
}
|
|
11235
11334
|
if (expiredLimiter !== void 0) {
|
|
11236
11335
|
const tail = calls.slice(index);
|
|
11237
11336
|
const terminalName = options.terminalTool?.name;
|
|
@@ -11419,10 +11518,14 @@ async function runAgent(options) {
|
|
|
11419
11518
|
};
|
|
11420
11519
|
}
|
|
11421
11520
|
if (finalizationWindow !== void 0) {
|
|
11422
|
-
maybeMarkWindowEntry();
|
|
11521
|
+
const entry = maybeMarkWindowEntry();
|
|
11522
|
+
if (entry !== void 0) await entry;
|
|
11423
11523
|
let windowState = windowActive();
|
|
11424
11524
|
if (windowState !== void 0 && !windowAllows(gatedCall.name)) {
|
|
11425
|
-
if (windowState.budget === "tool calls" && extension !== void 0 && windowState.remaining + extension.increment > finalizationWindow.reserveCalls
|
|
11525
|
+
if (windowState.budget === "tool calls" && extension !== void 0 && windowState.remaining + extension.increment > finalizationWindow.reserveCalls) {
|
|
11526
|
+
const attempt = tryToolBudgetGrant();
|
|
11527
|
+
if (typeof attempt === "boolean" ? attempt : await attempt) windowState = windowActive();
|
|
11528
|
+
}
|
|
11426
11529
|
if (windowState !== void 0 && !windowAllows(gatedCall.name)) {
|
|
11427
11530
|
events?.emit({
|
|
11428
11531
|
type: "tool:end",
|
|
@@ -11492,7 +11595,8 @@ async function runAgent(options) {
|
|
|
11492
11595
|
});
|
|
11493
11596
|
}
|
|
11494
11597
|
}
|
|
11495
|
-
maybeMarkWindowEntry();
|
|
11598
|
+
const lastEntry = maybeMarkWindowEntry();
|
|
11599
|
+
if (lastEntry !== void 0) await lastEntry;
|
|
11496
11600
|
return {
|
|
11497
11601
|
parts,
|
|
11498
11602
|
limitHit: false
|
|
@@ -14847,10 +14951,11 @@ function createCtx(internals, rootWorkflow) {
|
|
|
14847
14951
|
runAgentOptions.toolBudgetDurability = {
|
|
14848
14952
|
...durableRestored === void 0 ? {} : { restored: {
|
|
14849
14953
|
extensionsGranted: durableRestored.extensionsGranted,
|
|
14850
|
-
finalizationWindowEntered: durableRestored.finalizationWindowEntered
|
|
14954
|
+
finalizationWindowEntered: durableRestored.finalizationWindowEntered,
|
|
14955
|
+
...durableRestored.cap === void 0 ? {} : { cap: durableRestored.cap }
|
|
14851
14956
|
} },
|
|
14852
|
-
onExtensionGrant: (grant) => {
|
|
14853
|
-
internals.replayer.appendSinglePhase({
|
|
14957
|
+
onExtensionGrant: async (grant) => {
|
|
14958
|
+
await internals.replayer.appendSinglePhase({
|
|
14854
14959
|
scope: state.scope,
|
|
14855
14960
|
key: "",
|
|
14856
14961
|
kind: "decision",
|
|
@@ -14863,8 +14968,8 @@ function createCtx(internals, rootWorkflow) {
|
|
|
14863
14968
|
}
|
|
14864
14969
|
});
|
|
14865
14970
|
},
|
|
14866
|
-
onWindowEntry: (entry) => {
|
|
14867
|
-
internals.replayer.appendSinglePhase({
|
|
14971
|
+
onWindowEntry: async (entry) => {
|
|
14972
|
+
await internals.replayer.appendSinglePhase({
|
|
14868
14973
|
scope: state.scope,
|
|
14869
14974
|
key: "",
|
|
14870
14975
|
kind: "decision",
|
|
@@ -18431,13 +18536,30 @@ function makeOrchestratorWorkflow(goal, opts) {
|
|
|
18431
18536
|
}
|
|
18432
18537
|
}, callingState.spanId);
|
|
18433
18538
|
};
|
|
18434
|
-
const
|
|
18435
|
-
|
|
18539
|
+
const draftValue = draft ?? null;
|
|
18540
|
+
const draftHash = createHash("sha256").update(jcsSerialize(draftValue), "utf8").digest("hex");
|
|
18541
|
+
const validatorNames = validationSpec.validators.map((validator) => validator.name);
|
|
18542
|
+
/**
|
|
18543
|
+
* A journaled skip is the authority only for the generation and
|
|
18544
|
+
* the draft it judged (RV603). The documented remedy for a
|
|
18545
|
+
* broken contract is to fix it and resume, and a verdict that
|
|
18546
|
+
* outlives its contract defeats exactly that: the run would
|
|
18547
|
+
* settle ok carrying output the CURRENT contract rejects.
|
|
18548
|
+
* Bound by three facts, in descending strength: the contract
|
|
18549
|
+
* identity (the same `contractGenerationCurrent` test the
|
|
18550
|
+
* finish-validation decisions already use), the draft the
|
|
18551
|
+
* verdict actually judged, and the validator names that
|
|
18552
|
+
* rendered it. Entries written before this field existed carry
|
|
18553
|
+
* no draftHash and stay reusable, so journals in flight roll
|
|
18554
|
+
* forward byte for byte.
|
|
18555
|
+
*/
|
|
18556
|
+
const applies = (value) => contractGenerationCurrent(value) && (value.draftHash === void 0 || value.draftHash === draftHash) && (!Array.isArray(value.validators) || JSON.stringify(value.validators) === JSON.stringify(validatorNames));
|
|
18557
|
+
const prior = internals.replayer.snapshot().filter((entry) => entry.kind === "decision" && entry.scope === callingState.scope && entry.key === skipKey).at(-1);
|
|
18558
|
+
if (prior !== void 0 && applies(prior.value)) {
|
|
18436
18559
|
synthesisSkippedByValidDraft = true;
|
|
18437
18560
|
announceSkip(prior.seq);
|
|
18438
18561
|
return draft;
|
|
18439
18562
|
}
|
|
18440
|
-
const draftValue = draft ?? null;
|
|
18441
18563
|
const input = {
|
|
18442
18564
|
result: draftValue,
|
|
18443
18565
|
text: typeof draftValue === "string" ? draftValue : JSON.stringify(draftValue),
|
|
@@ -18467,7 +18589,9 @@ function makeOrchestratorWorkflow(goal, opts) {
|
|
|
18467
18589
|
value: {
|
|
18468
18590
|
decisionType: "orchestrator_synthesis_skip",
|
|
18469
18591
|
reason: "synthesis_skipped_by_valid_draft",
|
|
18470
|
-
validators:
|
|
18592
|
+
validators: validatorNames,
|
|
18593
|
+
...validationSpec.contract === void 0 ? {} : { contractHash: validationSpec.contract.hash },
|
|
18594
|
+
draftHash
|
|
18471
18595
|
}
|
|
18472
18596
|
});
|
|
18473
18597
|
if (orchestratorAccount !== void 0 && (opts?.budget?.synthesisReserveUsd ?? 0) > 0) internals.budget.releaseSynthesisReserve(orchestratorAccount);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@rulvar/core",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.103.0",
|
|
4
4
|
"description": "Rulvar core: L0 contracts, journal kernel, ctx primitives, agent runtime, model router, tool system, dynamic orchestrator, InMemory and JSONL stores, event stream.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "Apache-2.0",
|