@rulvar/core 1.59.3 → 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 +67 -1
- package/dist/index.js +283 -16
- package/package.json +1 -1
package/dist/index.d.ts
CHANGED
|
@@ -2652,6 +2652,7 @@ declare class Replayer {
|
|
|
2652
2652
|
private readonly runId;
|
|
2653
2653
|
private readonly store;
|
|
2654
2654
|
private readonly lease?;
|
|
2655
|
+
private readonly leaseOf?;
|
|
2655
2656
|
private readonly now;
|
|
2656
2657
|
private readonly priceUsd?;
|
|
2657
2658
|
private readonly onWarn?;
|
|
@@ -2683,6 +2684,14 @@ declare class Replayer {
|
|
|
2683
2684
|
* is asserted instead of fenced (the embedded default).
|
|
2684
2685
|
*/
|
|
2685
2686
|
lease?: Lease;
|
|
2687
|
+
/**
|
|
2688
|
+
* Late-bound lease lookup (P0.2): consulted at EVERY append,
|
|
2689
|
+
* winning over the static `lease` when it returns one. The engine
|
|
2690
|
+
* passes its segment-lease holder here, because the
|
|
2691
|
+
* engine-acquired genesis lease exists only after the ownership
|
|
2692
|
+
* boot, which runs after this constructor.
|
|
2693
|
+
*/
|
|
2694
|
+
leaseOf?: () => Lease | undefined;
|
|
2686
2695
|
});
|
|
2687
2696
|
/**
|
|
2688
2697
|
* Forward-matches one live call against the prior journal. Fresh
|
|
@@ -3867,6 +3876,24 @@ interface UsageLimits {
|
|
|
3867
3876
|
max: number;
|
|
3868
3877
|
costs?: Record<string, number>;
|
|
3869
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
|
+
};
|
|
3870
3897
|
}
|
|
3871
3898
|
declare const DEFAULT_MAX_TURNS = 32;
|
|
3872
3899
|
declare const DEFAULT_STREAM_IDLE_TIMEOUT_MS = 12e4;
|
|
@@ -3887,6 +3914,9 @@ interface EffectiveUsageLimits {
|
|
|
3887
3914
|
max: number;
|
|
3888
3915
|
costs?: Record<string, number>;
|
|
3889
3916
|
};
|
|
3917
|
+
finalizationReserve?: {
|
|
3918
|
+
maxOutputTokens?: number;
|
|
3919
|
+
};
|
|
3890
3920
|
}
|
|
3891
3921
|
/**
|
|
3892
3922
|
* Limits merge per spawn: AgentOpts.limits over profile limits over engine
|
|
@@ -5682,6 +5712,25 @@ interface CreateEngineOptions {
|
|
|
5682
5712
|
security?: {
|
|
5683
5713
|
argsHashSalt?: string;
|
|
5684
5714
|
};
|
|
5715
|
+
/**
|
|
5716
|
+
* The genesis ownership protocol (P0.2): over a journal store with
|
|
5717
|
+
* the lease capability, a run or resume segment that was NOT handed
|
|
5718
|
+
* a lease acquires its own before its first durable mutation, renews
|
|
5719
|
+
* it at ttl/3 exactly like a queue worker, and releases it at
|
|
5720
|
+
* settle. Fresh start, in-process resume, and worker takeover then
|
|
5721
|
+
* share ONE owner/lease contract: at most one live driver per run
|
|
5722
|
+
* across processes, a second driver's acquire rejects with the typed
|
|
5723
|
+
* LeaseHeldError before any write or provider dispatch, and a
|
|
5724
|
+
* crashed owner's lease expires after the store ttl so a worker
|
|
5725
|
+
* sweep recovers the run. Default 'auto'. 'none' restores the
|
|
5726
|
+
* pre-1.59.4 behavior (no engine-acquired leases) for hosts that
|
|
5727
|
+
* coordinate ownership entirely outside the engine; a lease passed
|
|
5728
|
+
* via RunOptions.lease or ResumeOptions.lease always wins over both
|
|
5729
|
+
* modes (the caller owns acquire, renew, and release). Stores
|
|
5730
|
+
* without the lease capability are unaffected: the embedded
|
|
5731
|
+
* single-process default keeps the single-writer precondition.
|
|
5732
|
+
*/
|
|
5733
|
+
ownership?: "auto" | "none";
|
|
5685
5734
|
}
|
|
5686
5735
|
interface RunOptions {
|
|
5687
5736
|
/** Explicit id; otherwise the engine mints a ULID. */
|
|
@@ -5713,6 +5762,19 @@ interface RunOptions {
|
|
|
5713
5762
|
tags?: string[];
|
|
5714
5763
|
/** Host-initiated cancellation. */
|
|
5715
5764
|
signal?: AbortSignal;
|
|
5765
|
+
/**
|
|
5766
|
+
* A lease the caller already holds for this run (the genesis side of
|
|
5767
|
+
* the ResumeOptions.lease contract): the engine carries it on EVERY
|
|
5768
|
+
* durable mutation of the fresh segment (every journal append, every
|
|
5769
|
+
* putMeta, every transcript blob write) and never acquires, renews,
|
|
5770
|
+
* or releases it itself; lifecycle stays with the caller. Passing it
|
|
5771
|
+
* disables the engine's own ownership acquisition for this run
|
|
5772
|
+
* regardless of the `ownership` mode. Hosts that admit runs through
|
|
5773
|
+
* an external queue acquire the lease at admission time and hand it
|
|
5774
|
+
* here, so admission and the first dispatch are covered by ONE
|
|
5775
|
+
* fencing epoch.
|
|
5776
|
+
*/
|
|
5777
|
+
lease?: Lease;
|
|
5716
5778
|
}
|
|
5717
5779
|
/** Resume-time hit/miss/orphan accounting. */
|
|
5718
5780
|
interface ResumePreview extends ResumeReport {
|
|
@@ -7334,8 +7396,12 @@ interface RunInternals {
|
|
|
7334
7396
|
* worktree patches) exactly as the Replayer threads it into every
|
|
7335
7397
|
* journal append, so a store declaring fencedWrites refuses a
|
|
7336
7398
|
* superseded segment's blob overwrites (fenced run state RFC, F2).
|
|
7399
|
+
* The engine binds this as a live getter over its segment-lease
|
|
7400
|
+
* holder (P0.2), so the union with undefined is explicit: before
|
|
7401
|
+
* the ownership boot (and on non-leasable stores) it reads
|
|
7402
|
+
* undefined.
|
|
7337
7403
|
*/
|
|
7338
|
-
lease?: Lease;
|
|
7404
|
+
lease?: Lease | undefined;
|
|
7339
7405
|
adapters: ReadonlyMap<string, ProviderAdapter>;
|
|
7340
7406
|
defaults: {
|
|
7341
7407
|
routing?: Partial<Record<InvocationRole, ModelSpec>>;
|
package/dist/index.js
CHANGED
|
@@ -420,6 +420,7 @@ function wrapJournalStore(inner, hook) {
|
|
|
420
420
|
wrapped.acquire = (runId, owner) => inner.acquire(runId, owner);
|
|
421
421
|
wrapped.renew = (l) => inner.renew(l);
|
|
422
422
|
wrapped.release = (l) => inner.release(l);
|
|
423
|
+
if (typeof leasable.leaseTtlMs === "number") wrapped.leaseTtlMs = leasable.leaseTtlMs;
|
|
423
424
|
}
|
|
424
425
|
if (typeof inner.getMeta === "function") wrapped.getMeta = (runId) => inner.getMeta(runId);
|
|
425
426
|
return wrapped;
|
|
@@ -6779,6 +6780,7 @@ var Replayer = class {
|
|
|
6779
6780
|
runId;
|
|
6780
6781
|
store;
|
|
6781
6782
|
lease;
|
|
6783
|
+
leaseOf;
|
|
6782
6784
|
now;
|
|
6783
6785
|
priceUsd;
|
|
6784
6786
|
onWarn;
|
|
@@ -6796,6 +6798,7 @@ var Replayer = class {
|
|
|
6796
6798
|
this.runId = options.runId;
|
|
6797
6799
|
this.store = options.store;
|
|
6798
6800
|
if (options.lease !== void 0) this.lease = options.lease;
|
|
6801
|
+
if (options.leaseOf !== void 0) this.leaseOf = options.leaseOf;
|
|
6799
6802
|
this.now = options.now ?? realNow;
|
|
6800
6803
|
if (options.priceUsd !== void 0) this.priceUsd = options.priceUsd;
|
|
6801
6804
|
if (options.onWarn !== void 0) this.onWarn = options.onWarn;
|
|
@@ -7083,7 +7086,7 @@ var Replayer = class {
|
|
|
7083
7086
|
} });
|
|
7084
7087
|
const shapeIssues = validateEntryShape(entry);
|
|
7085
7088
|
if (shapeIssues.length > 0) throw new ConfigError(`journal entry shape violation (kind '${entry.kind}'): ` + shapeIssues.map((i) => i.message).join("; "));
|
|
7086
|
-
await this.store.append(this.runId, entry, this.lease);
|
|
7089
|
+
await this.store.append(this.runId, entry, this.leaseOf?.() ?? this.lease);
|
|
7087
7090
|
this.entries.push(entry);
|
|
7088
7091
|
if (entry.status === "suspended") this.foldInternal.registerSuspended(entry);
|
|
7089
7092
|
else if (entry.kind !== "resolution" && entry.kind !== "abandon") this.foldInternal.registerEntry(entry);
|
|
@@ -9325,6 +9328,8 @@ function mergeUsageLimits(call, profile, engine) {
|
|
|
9325
9328
|
if (maxCallsPerTool !== void 0) merged.maxCallsPerTool = maxCallsPerTool;
|
|
9326
9329
|
const toolUnits = pick("toolUnits");
|
|
9327
9330
|
if (toolUnits !== void 0) merged.toolUnits = toolUnits;
|
|
9331
|
+
const finalizationReserve = pick("finalizationReserve");
|
|
9332
|
+
if (finalizationReserve !== void 0) merged.finalizationReserve = finalizationReserve;
|
|
9328
9333
|
return merged;
|
|
9329
9334
|
}
|
|
9330
9335
|
/**
|
|
@@ -9364,6 +9369,12 @@ function validateUsageLimits(limits, site) {
|
|
|
9364
9369
|
for (const [name, cost] of Object.entries(costs)) requireNonNegativeInteger(cost, `${site}.toolUnits.costs['${name}']`);
|
|
9365
9370
|
}
|
|
9366
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
|
+
}
|
|
9367
9378
|
}
|
|
9368
9379
|
//#endregion
|
|
9369
9380
|
//#region src/runtime/model-retry.ts
|
|
@@ -10527,8 +10538,27 @@ async function runAgent(options) {
|
|
|
10527
10538
|
let toolCallsUsed = 0;
|
|
10528
10539
|
let escalationRequest;
|
|
10529
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;
|
|
10530
10548
|
const noProgress = new NoProgressDetector(limits.noProgressTurns);
|
|
10531
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
|
+
};
|
|
10532
10562
|
if (limits.toolBudgetNotices === true && limits.maxToolCalls === void 0) events?.emit({
|
|
10533
10563
|
type: "log",
|
|
10534
10564
|
level: "warn",
|
|
@@ -10650,15 +10680,42 @@ async function runAgent(options) {
|
|
|
10650
10680
|
part.isError = true;
|
|
10651
10681
|
return part;
|
|
10652
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
|
+
};
|
|
10653
10700
|
for (const [index, call] of calls.entries()) {
|
|
10654
|
-
if (limits.maxToolCalls !== void 0 && toolCallsUsed >= limits.maxToolCalls)
|
|
10655
|
-
|
|
10656
|
-
|
|
10657
|
-
|
|
10658
|
-
|
|
10659
|
-
|
|
10660
|
-
|
|
10661
|
-
|
|
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
|
+
}
|
|
10662
10719
|
const def = runtime.defs.find((candidate) => candidate.name === call.name);
|
|
10663
10720
|
events?.emit({
|
|
10664
10721
|
type: "tool:start",
|
|
@@ -10860,7 +10917,7 @@ async function runAgent(options) {
|
|
|
10860
10917
|
if (record.isError === true) part.isError = true;
|
|
10861
10918
|
return part;
|
|
10862
10919
|
});
|
|
10863
|
-
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);
|
|
10864
10921
|
if (parts.length > 0) messages.push({
|
|
10865
10922
|
role: "tool",
|
|
10866
10923
|
parts
|
|
@@ -10881,6 +10938,16 @@ async function runAgent(options) {
|
|
|
10881
10938
|
retryable: false
|
|
10882
10939
|
};
|
|
10883
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
|
+
};
|
|
10884
10951
|
}
|
|
10885
10952
|
} else {
|
|
10886
10953
|
maybePushBudgetNotice();
|
|
@@ -11282,7 +11349,7 @@ async function runAgent(options) {
|
|
|
11282
11349
|
}
|
|
11283
11350
|
if (options.tools !== void 0 && outcome.turn.toolCalls.length > 0) {
|
|
11284
11351
|
noProgress.recordTurn({ toolCalls: outcome.turn.toolCalls.length });
|
|
11285
|
-
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, []);
|
|
11286
11353
|
if (parts.length > 0) messages.push({
|
|
11287
11354
|
role: "tool",
|
|
11288
11355
|
parts
|
|
@@ -11307,6 +11374,16 @@ async function runAgent(options) {
|
|
|
11307
11374
|
retryable: false
|
|
11308
11375
|
};
|
|
11309
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
|
+
};
|
|
11310
11387
|
}
|
|
11311
11388
|
break;
|
|
11312
11389
|
}
|
|
@@ -11492,6 +11569,110 @@ async function runAgent(options) {
|
|
|
11492
11569
|
await saveBoundary();
|
|
11493
11570
|
continue loop;
|
|
11494
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
|
+
}
|
|
11495
11676
|
endPhase(loopPhase, phaseOutcome(), servedBy);
|
|
11496
11677
|
if (status === "ok" && !finishedViaTool && options.finalize !== void 0) {
|
|
11497
11678
|
const finalizeResolved = options.finalize.resolved;
|
|
@@ -14206,7 +14387,7 @@ function createCtx(internals, rootWorkflow) {
|
|
|
14206
14387
|
transcriptRef: result.transcriptRef
|
|
14207
14388
|
};
|
|
14208
14389
|
if (result.status === "escalated" && result.escalation !== void 0) terminalPatch.escalation = result.escalation;
|
|
14209
|
-
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;
|
|
14210
14391
|
if (result.error !== void 0) terminalPatch.error = agentErrorToWire(result.error, result.errorMessage ?? `agent terminated with status ${result.status}`);
|
|
14211
14392
|
const resultUsageApprox = result.usageApprox === true;
|
|
14212
14393
|
if (resultUsageApprox) terminalPatch.usageApprox = true;
|
|
@@ -17119,6 +17300,28 @@ function hashRunOutput(value) {
|
|
|
17119
17300
|
return;
|
|
17120
17301
|
}
|
|
17121
17302
|
}
|
|
17303
|
+
/**
|
|
17304
|
+
* Engine ownership identity: a process-local counter, not Math.random()
|
|
17305
|
+
* (the queue worker's identity convention): owner strings need
|
|
17306
|
+
* uniqueness within the store, and the dev-mode bare-randomness guard
|
|
17307
|
+
* stays armed while any run is live.
|
|
17308
|
+
*/
|
|
17309
|
+
let engineOrdinal = 0;
|
|
17310
|
+
function engineIdentity() {
|
|
17311
|
+
engineOrdinal += 1;
|
|
17312
|
+
return `rulvar-engine:${process.pid}:${engineOrdinal}`;
|
|
17313
|
+
}
|
|
17314
|
+
/** Lease capability guard, mirroring createWorker's detection. */
|
|
17315
|
+
function leaseCapable(store) {
|
|
17316
|
+
const candidate = store;
|
|
17317
|
+
return typeof candidate.acquire === "function" && typeof candidate.renew === "function" && typeof candidate.release === "function";
|
|
17318
|
+
}
|
|
17319
|
+
/**
|
|
17320
|
+
* The renew-cadence fallback when a leasable store exposes no
|
|
17321
|
+
* leaseTtlMs: the Appendix A interim reference ttl the shipped stores
|
|
17322
|
+
* default to (60000 ms).
|
|
17323
|
+
*/
|
|
17324
|
+
const ENGINE_DEFAULT_LEASE_TTL_MS = 6e4;
|
|
17122
17325
|
function createEngine(options) {
|
|
17123
17326
|
const adapters = buildAdapterRegistry(options.adapters);
|
|
17124
17327
|
const rawJournal = options.stores?.journal ?? new InMemoryStore();
|
|
@@ -17128,6 +17331,17 @@ function createEngine(options) {
|
|
|
17128
17331
|
const maskEvents = options.redaction?.maskEvents ?? true;
|
|
17129
17332
|
const eventMasker = options.redaction?.patterns === void 0 ? void 0 : compileSecretMasker(options.redaction.patterns, "createEngine redaction.patterns");
|
|
17130
17333
|
const defaults = options.defaults ?? {};
|
|
17334
|
+
const ownership = options.ownership ?? "auto";
|
|
17335
|
+
if (ownership !== "auto" && ownership !== "none") throw new ConfigError(`createEngine ownership must be 'auto' or 'none'; got '${String(ownership)}'`);
|
|
17336
|
+
const engineOwner = engineIdentity();
|
|
17337
|
+
let ownershipRenewMs = Math.max(1, Math.floor(ENGINE_DEFAULT_LEASE_TTL_MS / 3));
|
|
17338
|
+
if (ownership === "auto" && leaseCapable(journal)) {
|
|
17339
|
+
const storeTtlMs = journal.leaseTtlMs;
|
|
17340
|
+
if (storeTtlMs !== void 0) {
|
|
17341
|
+
if (!Number.isInteger(storeTtlMs) || storeTtlMs < 1 || storeTtlMs > 2147483647) throw new ConfigError(`the journal store's leaseTtlMs capability must report an integer between 1 and 2147483647 ms for the engine's ownership renew cadence; got ${String(storeTtlMs)}`);
|
|
17342
|
+
ownershipRenewMs = Math.max(1, Math.floor(storeTtlMs / 3));
|
|
17343
|
+
}
|
|
17344
|
+
}
|
|
17131
17345
|
if (defaults.retry !== void 0) validateRetryPolicy(defaults.retry, "createEngine defaults.retry");
|
|
17132
17346
|
if (options.concurrency?.perRun !== void 0) requirePositiveInteger(options.concurrency.perRun, "createEngine concurrency.perRun");
|
|
17133
17347
|
for (const [adapterId, cap] of Object.entries(options.concurrency?.perProvider ?? {})) requirePositiveInteger(cap, `createEngine concurrency.perProvider['${adapterId}']`);
|
|
@@ -17182,6 +17396,10 @@ function createEngine(options) {
|
|
|
17182
17396
|
if (compiled !== void 0 && options.runners?.sandbox === void 0) throw new ConfigError("running a CompiledWorkflow requires a sandbox runner: pass createEngine({ runners: { sandbox: new WorkerSandboxRunner() } }) from @rulvar/planner ");
|
|
17183
17397
|
const runId = resumeCtx?.runId ?? opts?.runId ?? mintRunId();
|
|
17184
17398
|
assertSafeRunId(runId, "engine.run");
|
|
17399
|
+
const suppliedLease = resumeCtx?.lease ?? opts?.lease;
|
|
17400
|
+
if (suppliedLease !== void 0 && suppliedLease.runId !== runId) throw new ConfigError(`the supplied lease is for run '${suppliedLease.runId}', not '${runId}'; a lease fences exactly the run it was acquired for`);
|
|
17401
|
+
const segmentLease = {};
|
|
17402
|
+
if (suppliedLease !== void 0) segmentLease.current = suppliedLease;
|
|
17185
17403
|
const registry = buildDeriverRegistry(options.extraDerivers);
|
|
17186
17404
|
const segmentsBefore = resumeCtx?.segmentsBefore ?? 0;
|
|
17187
17405
|
const telemetryBase = segmentsBefore * EVENT_SEGMENT_STRIDE;
|
|
@@ -17218,7 +17436,7 @@ function createEngine(options) {
|
|
|
17218
17436
|
}, rootSpanId),
|
|
17219
17437
|
keyRing: registryKeyRing(registry),
|
|
17220
17438
|
...resumeCtx === void 0 ? {} : { priorEntries: resumeCtx.priorEntries },
|
|
17221
|
-
|
|
17439
|
+
leaseOf: () => segmentLease.current,
|
|
17222
17440
|
strict: resumeCtx?.strict ?? false
|
|
17223
17441
|
});
|
|
17224
17442
|
for (const seqToInvalidate of invalidated) replayer.invalidate(seqToInvalidate);
|
|
@@ -17313,7 +17531,9 @@ function createEngine(options) {
|
|
|
17313
17531
|
external,
|
|
17314
17532
|
mintTranscriptRef: () => `${runId}/t${transcriptCounter++}`,
|
|
17315
17533
|
now: realNow,
|
|
17316
|
-
|
|
17534
|
+
get lease() {
|
|
17535
|
+
return segmentLease.current;
|
|
17536
|
+
}
|
|
17317
17537
|
};
|
|
17318
17538
|
const argsBinding = {};
|
|
17319
17539
|
if (resumeCtx === void 0) {
|
|
@@ -17341,15 +17561,60 @@ function createEngine(options) {
|
|
|
17341
17561
|
workflowName: wf.name,
|
|
17342
17562
|
workflowHash: compiled === void 0 ? hashWorkflowBody(wf) : hashWorkflowSource(compiled.source),
|
|
17343
17563
|
...compiled === void 0 ? {} : { workflowSourceRef: workflowSourceRef(runId) }
|
|
17344
|
-
},
|
|
17564
|
+
}, segmentLease.current);
|
|
17345
17565
|
if (activeSegments.has(runId)) throw new ConfigError(`run '${runId}' already has a live execution segment in this engine; await its settled result before starting another one (exactly one segment owns a run; https://docs.rulvar.com/guide/durability#resolving-a-settled-run)`);
|
|
17346
17566
|
activeSegments.add(runId);
|
|
17567
|
+
let ownershipTeardown = () => Promise.resolve();
|
|
17347
17568
|
const result = (async () => {
|
|
17569
|
+
let ownedLease;
|
|
17570
|
+
let renewTimer;
|
|
17571
|
+
const settleOwnership = async () => {
|
|
17572
|
+
if (renewTimer !== void 0) {
|
|
17573
|
+
clearInterval(renewTimer);
|
|
17574
|
+
renewTimer = void 0;
|
|
17575
|
+
}
|
|
17576
|
+
const held = ownedLease;
|
|
17577
|
+
ownedLease = void 0;
|
|
17578
|
+
if (held !== void 0) {
|
|
17579
|
+
segmentLease.current = void 0;
|
|
17580
|
+
try {
|
|
17581
|
+
await journal.release(held);
|
|
17582
|
+
} catch {}
|
|
17583
|
+
}
|
|
17584
|
+
};
|
|
17585
|
+
ownershipTeardown = settleOwnership;
|
|
17586
|
+
if (ownership === "auto" && segmentLease.current === void 0 && resumeCtx?.strict !== true && leaseCapable(journal)) {
|
|
17587
|
+
let acquired;
|
|
17588
|
+
try {
|
|
17589
|
+
acquired = await journal.acquire(runId, engineOwner);
|
|
17590
|
+
} catch (thrown) {
|
|
17591
|
+
if (deadlineTimer !== void 0) deadlineTimer.cancel();
|
|
17592
|
+
external.close();
|
|
17593
|
+
bus.end();
|
|
17594
|
+
throw thrown;
|
|
17595
|
+
}
|
|
17596
|
+
ownedLease = acquired;
|
|
17597
|
+
segmentLease.current = acquired;
|
|
17598
|
+
renewTimer = setInterval(() => {
|
|
17599
|
+
journal.renew(acquired).catch(() => {
|
|
17600
|
+
bus.emit({
|
|
17601
|
+
type: "log",
|
|
17602
|
+
level: "warn",
|
|
17603
|
+
msg: `run '${runId}' ownership lost: the lease could not be renewed and its fencing epoch may be superseded`
|
|
17604
|
+
}, rootSpanId);
|
|
17605
|
+
requestCancel("run ownership lost: lease fencing epoch superseded");
|
|
17606
|
+
if (renewTimer !== void 0) {
|
|
17607
|
+
clearInterval(renewTimer);
|
|
17608
|
+
renewTimer = void 0;
|
|
17609
|
+
}
|
|
17610
|
+
});
|
|
17611
|
+
}, ownershipRenewMs);
|
|
17612
|
+
}
|
|
17348
17613
|
let status = "ok";
|
|
17349
17614
|
let value;
|
|
17350
17615
|
let wireError;
|
|
17351
17616
|
let pending = [];
|
|
17352
|
-
if (compiled !== void 0 && resumeCtx?.strict !== true) await transcripts.put(workflowSourceRef(runId), new TextEncoder().encode(compiled.source),
|
|
17617
|
+
if (compiled !== void 0 && resumeCtx?.strict !== true) await transcripts.put(workflowSourceRef(runId), new TextEncoder().encode(compiled.source), segmentLease.current);
|
|
17353
17618
|
await putMeta("running");
|
|
17354
17619
|
bus.emit({
|
|
17355
17620
|
type: "run:start",
|
|
@@ -17476,9 +17741,11 @@ function createEngine(options) {
|
|
|
17476
17741
|
...replayer.resumeReport(),
|
|
17477
17742
|
invalidResolutions: replayer.fold.invalidResolutions()
|
|
17478
17743
|
});
|
|
17744
|
+
await settleOwnership();
|
|
17479
17745
|
return outcome;
|
|
17480
17746
|
})();
|
|
17481
17747
|
result.catch(() => void 0).finally(() => {
|
|
17748
|
+
ownershipTeardown();
|
|
17482
17749
|
activeSegments.delete(runId);
|
|
17483
17750
|
});
|
|
17484
17751
|
return {
|
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",
|