@rulvar/core 1.59.4 → 1.60.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 +21 -0
- package/dist/index.js +189 -11
- package/package.json +1 -1
package/dist/index.d.ts
CHANGED
|
@@ -3876,6 +3876,24 @@ interface UsageLimits {
|
|
|
3876
3876
|
max: number;
|
|
3877
3877
|
costs?: Record<string, number>;
|
|
3878
3878
|
};
|
|
3879
|
+
/**
|
|
3880
|
+
* The guaranteed finalization turn (the experiment-review P1.1): when
|
|
3881
|
+
* a TOOL budget limiter (maxToolCalls or toolUnits) expires, the
|
|
3882
|
+
* runtime closes the current batch's remaining calls with explicit
|
|
3883
|
+
* skipped-call error results instead of dropping them silently, then
|
|
3884
|
+
* grants the model exactly ONE summary turn with tools withheld
|
|
3885
|
+
* before the invocation settles as status 'limit' with the exact
|
|
3886
|
+
* limiter named in the terminal error. The summary text becomes the
|
|
3887
|
+
* limit result's output for schema-less calls; a ridden schema
|
|
3888
|
+
* validates into typed output when the summary parses (one attempt,
|
|
3889
|
+
* no re-prompt). `maxOutputTokens` bounds the summary turn only;
|
|
3890
|
+
* absent, the ordinary per-turn output policy applies. Off by
|
|
3891
|
+
* default: the skip results and the summary instruction enter the
|
|
3892
|
+
* conversation, so enabling it changes recorded model requests.
|
|
3893
|
+
*/
|
|
3894
|
+
finalizationReserve?: {
|
|
3895
|
+
maxOutputTokens?: number;
|
|
3896
|
+
};
|
|
3879
3897
|
}
|
|
3880
3898
|
declare const DEFAULT_MAX_TURNS = 32;
|
|
3881
3899
|
declare const DEFAULT_STREAM_IDLE_TIMEOUT_MS = 12e4;
|
|
@@ -3896,6 +3914,9 @@ interface EffectiveUsageLimits {
|
|
|
3896
3914
|
max: number;
|
|
3897
3915
|
costs?: Record<string, number>;
|
|
3898
3916
|
};
|
|
3917
|
+
finalizationReserve?: {
|
|
3918
|
+
maxOutputTokens?: number;
|
|
3919
|
+
};
|
|
3899
3920
|
}
|
|
3900
3921
|
/**
|
|
3901
3922
|
* Limits merge per spawn: AgentOpts.limits over profile limits over engine
|
package/dist/index.js
CHANGED
|
@@ -9328,6 +9328,8 @@ function mergeUsageLimits(call, profile, engine) {
|
|
|
9328
9328
|
if (maxCallsPerTool !== void 0) merged.maxCallsPerTool = maxCallsPerTool;
|
|
9329
9329
|
const toolUnits = pick("toolUnits");
|
|
9330
9330
|
if (toolUnits !== void 0) merged.toolUnits = toolUnits;
|
|
9331
|
+
const finalizationReserve = pick("finalizationReserve");
|
|
9332
|
+
if (finalizationReserve !== void 0) merged.finalizationReserve = finalizationReserve;
|
|
9331
9333
|
return merged;
|
|
9332
9334
|
}
|
|
9333
9335
|
/**
|
|
@@ -9367,6 +9369,12 @@ function validateUsageLimits(limits, site) {
|
|
|
9367
9369
|
for (const [name, cost] of Object.entries(costs)) requireNonNegativeInteger(cost, `${site}.toolUnits.costs['${name}']`);
|
|
9368
9370
|
}
|
|
9369
9371
|
}
|
|
9372
|
+
if (limits.finalizationReserve !== void 0) {
|
|
9373
|
+
const reserve = limits.finalizationReserve;
|
|
9374
|
+
if (typeof reserve !== "object" || reserve === null || Array.isArray(reserve)) throw new ConfigError(`${site}.finalizationReserve must be { maxOutputTokens? }`);
|
|
9375
|
+
const { maxOutputTokens } = reserve;
|
|
9376
|
+
if (maxOutputTokens !== void 0) requirePositiveInteger(maxOutputTokens, `${site}.finalizationReserve.maxOutputTokens`);
|
|
9377
|
+
}
|
|
9370
9378
|
}
|
|
9371
9379
|
//#endregion
|
|
9372
9380
|
//#region src/runtime/model-retry.ts
|
|
@@ -10530,8 +10538,27 @@ async function runAgent(options) {
|
|
|
10530
10538
|
let toolCallsUsed = 0;
|
|
10531
10539
|
let escalationRequest;
|
|
10532
10540
|
let abortClass;
|
|
10541
|
+
/**
|
|
10542
|
+
* Set at a tool-budget expiry when limits.finalizationReserve is
|
|
10543
|
+
* configured (P1.1); the reserve turn itself runs at ONE site after
|
|
10544
|
+
* the loop ends (the pending-turn path trips before the dispatch
|
|
10545
|
+
* machinery below is even defined), inside the still-open loop phase.
|
|
10546
|
+
*/
|
|
10547
|
+
let reserveRequest;
|
|
10533
10548
|
const noProgress = new NoProgressDetector(limits.noProgressTurns);
|
|
10534
10549
|
const guard = explorationTrackingEnabled(limits) ? new ExplorationGuard(limits) : void 0;
|
|
10550
|
+
/**
|
|
10551
|
+
* The exact limiter behind a tool-budget expiry, with its counts: the
|
|
10552
|
+
* wording rides the finalization-reserve instruction and the 'limit'
|
|
10553
|
+
* terminal's errorMessage (P1.1 criterion: the terminal names the
|
|
10554
|
+
* limiter, never a bare status).
|
|
10555
|
+
*/
|
|
10556
|
+
const toolBudgetDetail = (limiter) => {
|
|
10557
|
+
if (limiter === "maxToolCalls") return `maxToolCalls (${String(toolCallsUsed)}/${String(limits.maxToolCalls ?? 0)})`;
|
|
10558
|
+
const max = limits.toolUnits?.max ?? 0;
|
|
10559
|
+
const used = guard === void 0 ? max : guard.summary(toolCallsUsed).toolUnitsUsed ?? max;
|
|
10560
|
+
return `toolUnits (${String(used)}/${String(max)})`;
|
|
10561
|
+
};
|
|
10535
10562
|
if (limits.toolBudgetNotices === true && limits.maxToolCalls === void 0) events?.emit({
|
|
10536
10563
|
type: "log",
|
|
10537
10564
|
level: "warn",
|
|
@@ -10653,15 +10680,42 @@ async function runAgent(options) {
|
|
|
10653
10680
|
part.isError = true;
|
|
10654
10681
|
return part;
|
|
10655
10682
|
};
|
|
10683
|
+
/**
|
|
10684
|
+
* Closes the batch tail at a tool-budget expiry (P1.1): with the
|
|
10685
|
+
* finalization reserve configured every not-admitted call gets a
|
|
10686
|
+
* typed skipped-call error result naming the limiter, so the model
|
|
10687
|
+
* (and the transcript) sees exactly which calls never executed and
|
|
10688
|
+
* the summary turn's history stays well formed (providers reject
|
|
10689
|
+
* tool calls without matching results). Without the reserve the
|
|
10690
|
+
* tail stays unanswered, byte-identical to before.
|
|
10691
|
+
*/
|
|
10692
|
+
const closeSkippedTail = (skippedCalls, limiter) => {
|
|
10693
|
+
if (limits.finalizationReserve === void 0) return;
|
|
10694
|
+
for (const call of skippedCalls) parts.push(errorPart(call, {
|
|
10695
|
+
error: "skipped: the tool budget is exhausted; the call was not executed",
|
|
10696
|
+
limiter,
|
|
10697
|
+
skipped: true
|
|
10698
|
+
}));
|
|
10699
|
+
};
|
|
10656
10700
|
for (const [index, call] of calls.entries()) {
|
|
10657
|
-
if (limits.maxToolCalls !== void 0 && toolCallsUsed >= limits.maxToolCalls)
|
|
10658
|
-
|
|
10659
|
-
|
|
10660
|
-
|
|
10661
|
-
|
|
10662
|
-
|
|
10663
|
-
|
|
10664
|
-
|
|
10701
|
+
if (limits.maxToolCalls !== void 0 && toolCallsUsed >= limits.maxToolCalls) {
|
|
10702
|
+
closeSkippedTail(calls.slice(index), "maxToolCalls");
|
|
10703
|
+
return {
|
|
10704
|
+
parts,
|
|
10705
|
+
limitHit: true,
|
|
10706
|
+
limiter: "maxToolCalls",
|
|
10707
|
+
skipped: calls.length - index
|
|
10708
|
+
};
|
|
10709
|
+
}
|
|
10710
|
+
if (guard !== void 0 && guard.unitsExhausted()) {
|
|
10711
|
+
closeSkippedTail(calls.slice(index), "toolUnits");
|
|
10712
|
+
return {
|
|
10713
|
+
parts,
|
|
10714
|
+
limitHit: true,
|
|
10715
|
+
limiter: "toolUnits",
|
|
10716
|
+
skipped: calls.length - index
|
|
10717
|
+
};
|
|
10718
|
+
}
|
|
10665
10719
|
const def = runtime.defs.find((candidate) => candidate.name === call.name);
|
|
10666
10720
|
events?.emit({
|
|
10667
10721
|
type: "tool:start",
|
|
@@ -10863,7 +10917,7 @@ async function runAgent(options) {
|
|
|
10863
10917
|
if (record.isError === true) part.isError = true;
|
|
10864
10918
|
return part;
|
|
10865
10919
|
});
|
|
10866
|
-
const { parts, limitHit, escalated, finished, guardTrip } = await runToolCalls([restored.pending.awaiting, ...restored.pending.remaining], priorParts);
|
|
10920
|
+
const { parts, limitHit, escalated, finished, guardTrip, limiter, skipped } = await runToolCalls([restored.pending.awaiting, ...restored.pending.remaining], priorParts);
|
|
10867
10921
|
if (parts.length > 0) messages.push({
|
|
10868
10922
|
role: "tool",
|
|
10869
10923
|
parts
|
|
@@ -10884,6 +10938,16 @@ async function runAgent(options) {
|
|
|
10884
10938
|
retryable: false
|
|
10885
10939
|
};
|
|
10886
10940
|
errorMessage = guard.describeTrip();
|
|
10941
|
+
} else if (limiter !== void 0 && limits.finalizationReserve !== void 0) {
|
|
10942
|
+
agentError = {
|
|
10943
|
+
kind: "terminal",
|
|
10944
|
+
retryable: false
|
|
10945
|
+
};
|
|
10946
|
+
errorMessage = `tool budget exhausted: ${toolBudgetDetail(limiter)}; skipped tool calls: ${String(skipped ?? 0)}`;
|
|
10947
|
+
reserveRequest = {
|
|
10948
|
+
limiter,
|
|
10949
|
+
skipped: skipped ?? 0
|
|
10950
|
+
};
|
|
10887
10951
|
}
|
|
10888
10952
|
} else {
|
|
10889
10953
|
maybePushBudgetNotice();
|
|
@@ -11285,7 +11349,7 @@ async function runAgent(options) {
|
|
|
11285
11349
|
}
|
|
11286
11350
|
if (options.tools !== void 0 && outcome.turn.toolCalls.length > 0) {
|
|
11287
11351
|
noProgress.recordTurn({ toolCalls: outcome.turn.toolCalls.length });
|
|
11288
|
-
const { parts, limitHit, escalated, finished, guardTrip } = await runToolCalls(outcome.turn.toolCalls, []);
|
|
11352
|
+
const { parts, limitHit, escalated, finished, guardTrip, limiter, skipped } = await runToolCalls(outcome.turn.toolCalls, []);
|
|
11289
11353
|
if (parts.length > 0) messages.push({
|
|
11290
11354
|
role: "tool",
|
|
11291
11355
|
parts
|
|
@@ -11310,6 +11374,16 @@ async function runAgent(options) {
|
|
|
11310
11374
|
retryable: false
|
|
11311
11375
|
};
|
|
11312
11376
|
errorMessage = guard.describeTrip();
|
|
11377
|
+
} else if (limiter !== void 0 && limits.finalizationReserve !== void 0) {
|
|
11378
|
+
agentError = {
|
|
11379
|
+
kind: "terminal",
|
|
11380
|
+
retryable: false
|
|
11381
|
+
};
|
|
11382
|
+
errorMessage = `tool budget exhausted: ${toolBudgetDetail(limiter)}; skipped tool calls: ${String(skipped ?? 0)}`;
|
|
11383
|
+
reserveRequest = {
|
|
11384
|
+
limiter,
|
|
11385
|
+
skipped: skipped ?? 0
|
|
11386
|
+
};
|
|
11313
11387
|
}
|
|
11314
11388
|
break;
|
|
11315
11389
|
}
|
|
@@ -11495,6 +11569,110 @@ async function runAgent(options) {
|
|
|
11495
11569
|
await saveBoundary();
|
|
11496
11570
|
continue loop;
|
|
11497
11571
|
}
|
|
11572
|
+
if (status === "limit" && reserveRequest !== void 0) {
|
|
11573
|
+
const { limiter, skipped } = reserveRequest;
|
|
11574
|
+
let proceed = true;
|
|
11575
|
+
try {
|
|
11576
|
+
options.budget?.beforeTurn();
|
|
11577
|
+
} catch {
|
|
11578
|
+
events?.emit({
|
|
11579
|
+
type: "log",
|
|
11580
|
+
level: "warn",
|
|
11581
|
+
msg: "the finalization reserve turn was skipped: the budget blocks further turns"
|
|
11582
|
+
});
|
|
11583
|
+
proceed = false;
|
|
11584
|
+
}
|
|
11585
|
+
if (proceed) {
|
|
11586
|
+
turns += 1;
|
|
11587
|
+
const reserveMessages = [...messages, {
|
|
11588
|
+
role: "user",
|
|
11589
|
+
parts: [{
|
|
11590
|
+
type: "text",
|
|
11591
|
+
text: `The tool budget is exhausted (${toolBudgetDetail(limiter)}). Skipped tool calls: ${String(skipped)}; no further tool calls will execute. This is the final turn: produce your best final answer from the evidence already collected.`
|
|
11592
|
+
}]
|
|
11593
|
+
}];
|
|
11594
|
+
let reserveDispatch;
|
|
11595
|
+
try {
|
|
11596
|
+
reserveDispatch = await dispatchPhase({
|
|
11597
|
+
role: primaryRole,
|
|
11598
|
+
chain: loopChain,
|
|
11599
|
+
cursor: loopCursor,
|
|
11600
|
+
requestFor: (target) => {
|
|
11601
|
+
let req = buildRequest(target.resolved, projectHistory(reserveMessages, providerOf(target.adapter)), limits, options.tools?.contracts);
|
|
11602
|
+
if (options.schema !== void 0 && options.canonicalSchema !== void 0 && !separateExtract) req = applyStructuredOutputTier(req, rideTierFor(target), options.canonicalSchema);
|
|
11603
|
+
if (req.tools !== void 0) req = {
|
|
11604
|
+
...req,
|
|
11605
|
+
toolChoice: "none"
|
|
11606
|
+
};
|
|
11607
|
+
const reserveMax = limits.finalizationReserve?.maxOutputTokens;
|
|
11608
|
+
if (reserveMax !== void 0) req = {
|
|
11609
|
+
...req,
|
|
11610
|
+
maxOutputTokens: Math.min(req.maxOutputTokens ?? reserveMax, reserveMax)
|
|
11611
|
+
};
|
|
11612
|
+
return applyOutputBudget(req, target, options.budget);
|
|
11613
|
+
},
|
|
11614
|
+
streamOptionsFor: (target) => {
|
|
11615
|
+
const reserveStreamOptions = {
|
|
11616
|
+
idleTimeoutMs: limits.streamIdleTimeoutMs,
|
|
11617
|
+
signals: options.signal === void 0 ? [] : [options.signal],
|
|
11618
|
+
onUsage: (delta) => options.budget?.onUsage(delta, target.resolved.ref)
|
|
11619
|
+
};
|
|
11620
|
+
if (options.budget?.signal !== void 0) reserveStreamOptions.budgetSignal = options.budget.signal;
|
|
11621
|
+
if (options.stream === true) reserveStreamOptions.onDelta = (delta) => events?.emit({
|
|
11622
|
+
type: "agent:stream",
|
|
11623
|
+
delta
|
|
11624
|
+
});
|
|
11625
|
+
return reserveStreamOptions;
|
|
11626
|
+
}
|
|
11627
|
+
});
|
|
11628
|
+
} catch (thrown) {
|
|
11629
|
+
if (!(thrown instanceof BudgetExhaustedError)) throw thrown;
|
|
11630
|
+
events?.emit({
|
|
11631
|
+
type: "log",
|
|
11632
|
+
level: "warn",
|
|
11633
|
+
msg: `the finalization reserve turn was skipped: ${thrown.message}`
|
|
11634
|
+
});
|
|
11635
|
+
}
|
|
11636
|
+
if (reserveDispatch !== void 0) {
|
|
11637
|
+
const { outcome, target: reserveTarget } = reserveDispatch;
|
|
11638
|
+
servedBy = reserveTarget.resolved.ref;
|
|
11639
|
+
usageApprox = usageApprox || outcome.usageApprox;
|
|
11640
|
+
messages.push(assistantMsg(outcome.turn, liftRetainedParts(outcome.providerMetadata, reserveTarget.adapter)));
|
|
11641
|
+
if (invariantViolation !== void 0) {
|
|
11642
|
+
status = "error";
|
|
11643
|
+
agentError = {
|
|
11644
|
+
kind: "transport",
|
|
11645
|
+
retryable: false
|
|
11646
|
+
};
|
|
11647
|
+
errorMessage = invariantViolation;
|
|
11648
|
+
} else if (outcome.aborted === "external") status = "cancelled";
|
|
11649
|
+
else if (outcome.aborted === "budget") {
|
|
11650
|
+
status = "cancelled";
|
|
11651
|
+
agentError = {
|
|
11652
|
+
kind: "budget",
|
|
11653
|
+
retryable: false
|
|
11654
|
+
};
|
|
11655
|
+
} else {
|
|
11656
|
+
await saveBoundary();
|
|
11657
|
+
if (outcome.wireError !== void 0 || outcome.aborted === "idle") events?.emit({
|
|
11658
|
+
type: "log",
|
|
11659
|
+
level: "warn",
|
|
11660
|
+
msg: "the finalization reserve turn failed; the limit terminal stands" + (outcome.wireError === void 0 ? " (stream idle timeout)" : ` (${outcome.wireError.message})`)
|
|
11661
|
+
});
|
|
11662
|
+
else if (options.schema === void 0) {
|
|
11663
|
+
const summary = outcome.turn.text;
|
|
11664
|
+
if (summary.trim() !== "") output = summary;
|
|
11665
|
+
} else if (!separateExtract && options.canonicalSchema !== void 0) {
|
|
11666
|
+
const candidate = extractCandidate(outcome.turn, rideTierFor(reserveTarget));
|
|
11667
|
+
if (candidate !== void 0) {
|
|
11668
|
+
const validation = await validateSchemaSpec(options.schema, candidate.raw);
|
|
11669
|
+
if (validation.valid) output = validation.value;
|
|
11670
|
+
}
|
|
11671
|
+
}
|
|
11672
|
+
}
|
|
11673
|
+
}
|
|
11674
|
+
}
|
|
11675
|
+
}
|
|
11498
11676
|
endPhase(loopPhase, phaseOutcome(), servedBy);
|
|
11499
11677
|
if (status === "ok" && !finishedViaTool && options.finalize !== void 0) {
|
|
11500
11678
|
const finalizeResolved = options.finalize.resolved;
|
|
@@ -14209,7 +14387,7 @@ function createCtx(internals, rootWorkflow) {
|
|
|
14209
14387
|
transcriptRef: result.transcriptRef
|
|
14210
14388
|
};
|
|
14211
14389
|
if (result.status === "escalated" && result.escalation !== void 0) terminalPatch.escalation = result.escalation;
|
|
14212
|
-
if (result.output !== null && result.status === "ok") terminalPatch.value = result.output;
|
|
14390
|
+
if (result.output !== null && (result.status === "ok" || result.status === "limit")) terminalPatch.value = result.output;
|
|
14213
14391
|
if (result.error !== void 0) terminalPatch.error = agentErrorToWire(result.error, result.errorMessage ?? `agent terminated with status ${result.status}`);
|
|
14214
14392
|
const resultUsageApprox = result.usageApprox === true;
|
|
14215
14393
|
if (resultUsageApprox) terminalPatch.usageApprox = true;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@rulvar/core",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.60.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",
|