@rulvar/core 1.74.0 → 1.75.1
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 +11 -1
- package/dist/index.js +189 -16
- package/package.json +1 -1
package/dist/index.d.ts
CHANGED
|
@@ -1531,7 +1531,17 @@ type ModelCaps = {
|
|
|
1531
1531
|
supportsParallelTools: boolean; /** Canonical efforts this model accepts after mapping. */
|
|
1532
1532
|
reasoningEfforts: Effort[];
|
|
1533
1533
|
contextWindow: number;
|
|
1534
|
-
maxOutputTokens: number;
|
|
1534
|
+
maxOutputTokens: number;
|
|
1535
|
+
/**
|
|
1536
|
+
* The smallest request output cap the provider accepts (the v1.74
|
|
1537
|
+
* experiment review, P0.1): OpenAI's Responses API rejects
|
|
1538
|
+
* max_output_tokens below 16, so a dispatch under this floor is a
|
|
1539
|
+
* guaranteed 400. The runtime never sends a request output cap below
|
|
1540
|
+
* it: a budget last gasp dispatches the floor instead of one token,
|
|
1541
|
+
* and a remainder that cannot buy the floor is refused typed before
|
|
1542
|
+
* the wire. Absent means one, the historical floor.
|
|
1543
|
+
*/
|
|
1544
|
+
minOutputTokensPerTurn?: number; /** Adapter-reported fallback only; the versioned price table wins. */
|
|
1535
1545
|
pricing?: Pricing;
|
|
1536
1546
|
};
|
|
1537
1547
|
interface ProviderAdapter {
|
package/dist/index.js
CHANGED
|
@@ -10181,31 +10181,53 @@ function estimateInputTokens(messages) {
|
|
|
10181
10181
|
return Math.ceil(chars / 4);
|
|
10182
10182
|
}
|
|
10183
10183
|
/**
|
|
10184
|
+
* The serving model's declared minimum request output cap, default one.
|
|
10185
|
+
* OpenAI's Responses API rejects max_output_tokens below 16, so a
|
|
10186
|
+
* below-floor dispatch is a guaranteed provider 400: the v1.74
|
|
10187
|
+
* experiment's terminal repair died exactly there (the review, P0.1).
|
|
10188
|
+
* Defensive lookup: a caps() throw must not fail turns that never
|
|
10189
|
+
* consulted caps at this point before.
|
|
10190
|
+
*/
|
|
10191
|
+
function outputFloorOf(target) {
|
|
10192
|
+
try {
|
|
10193
|
+
const declared = target.adapter.caps(target.resolved.model).minOutputTokensPerTurn;
|
|
10194
|
+
return declared !== void 0 && Number.isInteger(declared) && declared > 1 ? declared : 1;
|
|
10195
|
+
} catch {
|
|
10196
|
+
return 1;
|
|
10197
|
+
}
|
|
10198
|
+
}
|
|
10199
|
+
/**
|
|
10184
10200
|
* Layer 2b at the wire boundary: clamps the outgoing request's
|
|
10185
10201
|
* maxOutputTokens to what the remaining budget affords from the serving
|
|
10186
|
-
* model
|
|
10187
|
-
*
|
|
10188
|
-
*
|
|
10189
|
-
*
|
|
10190
|
-
*
|
|
10191
|
-
*
|
|
10192
|
-
*
|
|
10193
|
-
*
|
|
10194
|
-
*
|
|
10195
|
-
*
|
|
10196
|
-
*
|
|
10202
|
+
* model, and holds every dispatch at or above the serving model's
|
|
10203
|
+
* output floor. The clamp uses the heuristic prompt estimate; the
|
|
10204
|
+
* DENIAL does not: a turn is refused (BudgetExhaustedError, never
|
|
10205
|
+
* dispatched) only when the remainder cannot buy the floor at zero
|
|
10206
|
+
* input, which is exact. Denying on the estimate would kill turns the
|
|
10207
|
+
* budget still funds, including the DEF-7 forced finish paid from the
|
|
10208
|
+
* released finalize reserve; when the estimate says the prompt alone
|
|
10209
|
+
* spends the remainder, the turn dispatches AT the floor and the exact
|
|
10210
|
+
* layers (2 and 3) settle the difference. A configured per-turn cap
|
|
10211
|
+
* below the floor is a ConfigError before any wire call: the provider
|
|
10212
|
+
* would reject every dispatch, so failing typed beats paying for a
|
|
10213
|
+
* guaranteed 400. A no-op without a hook or when the hook reports no
|
|
10214
|
+
* bound. The clamp touches only the wire request, exactly like
|
|
10215
|
+
* limits.maxOutputTokensPerTurn above it; identity is computed at the
|
|
10216
|
+
* ctx layer and never sees it.
|
|
10197
10217
|
*/
|
|
10198
10218
|
function applyOutputBudget(req, target, budget) {
|
|
10219
|
+
const floor = outputFloorOf(target);
|
|
10220
|
+
if (req.maxOutputTokens !== void 0 && req.maxOutputTokens < floor) throw new ConfigError(`the per-turn output cap ${String(req.maxOutputTokens)} is below the ${String(floor)} token output floor of ${target.resolved.ref}; the provider would reject every dispatch, so raise limits.maxOutputTokensPerTurn to at least the floor`);
|
|
10199
10221
|
const hook = budget?.maxAffordableOutputTokens;
|
|
10200
10222
|
if (hook === void 0) return req;
|
|
10201
10223
|
const affordable = hook(target.resolved.ref, estimateInputTokens(req.messages));
|
|
10202
10224
|
if (affordable === void 0) return req;
|
|
10203
|
-
if (affordable <
|
|
10225
|
+
if (affordable < floor) {
|
|
10204
10226
|
const zeroInputAffordable = hook(target.resolved.ref, 0);
|
|
10205
|
-
if (zeroInputAffordable !== void 0 && zeroInputAffordable <
|
|
10227
|
+
if (zeroInputAffordable !== void 0 && zeroInputAffordable < floor) throw new BudgetExhaustedError(floor === 1 ? `the remaining budget cannot afford one output token from ${target.resolved.ref}; the turn was not dispatched` : `the remaining budget cannot afford the ${String(floor)} token output floor of ${target.resolved.ref}; the turn was not dispatched`);
|
|
10206
10228
|
return {
|
|
10207
10229
|
...req,
|
|
10208
|
-
maxOutputTokens:
|
|
10230
|
+
maxOutputTokens: floor
|
|
10209
10231
|
};
|
|
10210
10232
|
}
|
|
10211
10233
|
if (req.maxOutputTokens === void 0 || affordable < req.maxOutputTokens) return {
|
|
@@ -10260,6 +10282,117 @@ function assistantMsg(turn, retained = []) {
|
|
|
10260
10282
|
};
|
|
10261
10283
|
}
|
|
10262
10284
|
/**
|
|
10285
|
+
* The adapter parse-failure wrapper, recognized by exact shape: both
|
|
10286
|
+
* first-class wires deliver tool arguments their single strict
|
|
10287
|
+
* JSON.parse rejected as `{__unparsed: raw}` and nothing else.
|
|
10288
|
+
*/
|
|
10289
|
+
function unparsedMarkerOf(args) {
|
|
10290
|
+
if (typeof args !== "object" || args === null || Array.isArray(args)) return;
|
|
10291
|
+
const keys = Object.keys(args);
|
|
10292
|
+
const raw = args.__unparsed;
|
|
10293
|
+
return keys.length === 1 && keys[0] === "__unparsed" && typeof raw === "string" ? raw : void 0;
|
|
10294
|
+
}
|
|
10295
|
+
/** JSON.parse to a plain object, or undefined; never throws. */
|
|
10296
|
+
function parsePlainObject(text) {
|
|
10297
|
+
try {
|
|
10298
|
+
const value = JSON.parse(text);
|
|
10299
|
+
return typeof value === "object" && value !== null && !Array.isArray(value) ? value : void 0;
|
|
10300
|
+
} catch {
|
|
10301
|
+
return;
|
|
10302
|
+
}
|
|
10303
|
+
}
|
|
10304
|
+
/**
|
|
10305
|
+
* One bounded deterministic normalization of a near-JSON arguments
|
|
10306
|
+
* string: strip a single markdown fence, cut at the end of the first
|
|
10307
|
+
* balanced top-level object (drops trailing prose), and escape raw
|
|
10308
|
+
* control characters inside string literals (models writing markdown
|
|
10309
|
+
* documents into arguments emit real newlines, which strict JSON
|
|
10310
|
+
* forbids). Truncated payloads stay unbalanced and still fail the
|
|
10311
|
+
* parse; nothing here can invent structure the model did not write.
|
|
10312
|
+
*/
|
|
10313
|
+
function normalizeNearJson(raw) {
|
|
10314
|
+
let text = raw.trim();
|
|
10315
|
+
const fence = /^```[a-zA-Z]*[\t ]*\r?\n([\s\S]*?)\r?\n?```$/.exec(text);
|
|
10316
|
+
if (fence?.[1] !== void 0) text = fence[1].trim();
|
|
10317
|
+
const start = text.indexOf("{");
|
|
10318
|
+
if (start >= 0) {
|
|
10319
|
+
let depth = 0;
|
|
10320
|
+
let inString = false;
|
|
10321
|
+
let escaped = false;
|
|
10322
|
+
for (let index = start; index < text.length; index += 1) {
|
|
10323
|
+
const char = text[index];
|
|
10324
|
+
if (escaped) {
|
|
10325
|
+
escaped = false;
|
|
10326
|
+
continue;
|
|
10327
|
+
}
|
|
10328
|
+
if (char === "\\") {
|
|
10329
|
+
escaped = inString;
|
|
10330
|
+
continue;
|
|
10331
|
+
}
|
|
10332
|
+
if (char === "\"") {
|
|
10333
|
+
inString = !inString;
|
|
10334
|
+
continue;
|
|
10335
|
+
}
|
|
10336
|
+
if (inString) continue;
|
|
10337
|
+
if (char === "{") depth += 1;
|
|
10338
|
+
else if (char === "}") {
|
|
10339
|
+
depth -= 1;
|
|
10340
|
+
if (depth === 0) {
|
|
10341
|
+
text = text.slice(start, index + 1);
|
|
10342
|
+
break;
|
|
10343
|
+
}
|
|
10344
|
+
}
|
|
10345
|
+
}
|
|
10346
|
+
}
|
|
10347
|
+
let escapedText = "";
|
|
10348
|
+
let inString = false;
|
|
10349
|
+
let escaped = false;
|
|
10350
|
+
for (const char of text) {
|
|
10351
|
+
if (!escaped && char === "\"") inString = !inString;
|
|
10352
|
+
escaped = inString && !escaped && char === "\\";
|
|
10353
|
+
const code = char.codePointAt(0) ?? 0;
|
|
10354
|
+
if (inString && code < 32) escapedText += char === "\n" ? "\\n" : char === "\r" ? "\\r" : char === " " ? "\\t" : `\\u${code.toString(16).padStart(4, "0")}`;
|
|
10355
|
+
else escapedText += char;
|
|
10356
|
+
}
|
|
10357
|
+
return escapedText;
|
|
10358
|
+
}
|
|
10359
|
+
/**
|
|
10360
|
+
* The deterministic second chance (the v1.74 experiment review, P1.5):
|
|
10361
|
+
* a strict re-parse of the wrapped raw string, then one normalization
|
|
10362
|
+
* pass. The v1.74 experiment durably proved three complete coordination
|
|
10363
|
+
* drafts were recoverable this way and were thrown away instead. Only a
|
|
10364
|
+
* plain object counts; the recovered value still faces the tool schema
|
|
10365
|
+
* at the caller.
|
|
10366
|
+
*/
|
|
10367
|
+
function recoverUnparsedArgs(raw) {
|
|
10368
|
+
const strict = parsePlainObject(raw);
|
|
10369
|
+
if (strict !== void 0) return {
|
|
10370
|
+
value: strict,
|
|
10371
|
+
pass: "strict"
|
|
10372
|
+
};
|
|
10373
|
+
const normalized = parsePlainObject(normalizeNearJson(raw));
|
|
10374
|
+
if (normalized !== void 0) return {
|
|
10375
|
+
value: normalized,
|
|
10376
|
+
pass: "normalized"
|
|
10377
|
+
};
|
|
10378
|
+
}
|
|
10379
|
+
/**
|
|
10380
|
+
* The recovery adoption warn line, shared verbatim by the regular tool
|
|
10381
|
+
* executor and the terminal tool interception so the two sites cannot
|
|
10382
|
+
* drift. The recovery itself stays INLINE at both call sites (v1.75.1):
|
|
10383
|
+
* an async wrapper around the common validation added one microtask
|
|
10384
|
+
* tick to EVERY tool validation and shifted quiescence-sensitive
|
|
10385
|
+
* cassette flows, and promise-tick identity is part of the byte
|
|
10386
|
+
* contract, so only the synchronous pieces are shared.
|
|
10387
|
+
*/
|
|
10388
|
+
function unparsedRecoveryLog(toolName, rawChars, pass) {
|
|
10389
|
+
return {
|
|
10390
|
+
type: "log",
|
|
10391
|
+
level: "warn",
|
|
10392
|
+
msg: `arguments for '${toolName}' arrived unparsed (${String(rawChars)} chars) and were recovered by a ${pass} JSON pass; the recovered object passed the tool schema and the call executed`
|
|
10393
|
+
};
|
|
10394
|
+
}
|
|
10395
|
+
/**
|
|
10263
10396
|
* Executes one model-issued tool call to a tool-result part. Failures are
|
|
10264
10397
|
* surfaced to the model as error tool results and never thrown past
|
|
10265
10398
|
* policy: unknown names, argument-validation issues, ModelRetry (bounded
|
|
@@ -10288,7 +10421,18 @@ async function executeToolCall(options) {
|
|
|
10288
10421
|
return part;
|
|
10289
10422
|
};
|
|
10290
10423
|
if (def === void 0) return finish({ error: `unknown tool '${call.name}'` }, "error");
|
|
10291
|
-
|
|
10424
|
+
let validation = await validateSchemaSpec(def.parameters, call.args);
|
|
10425
|
+
if (!validation.valid) {
|
|
10426
|
+
const unparsedRaw = unparsedMarkerOf(call.args);
|
|
10427
|
+
const recovered = unparsedRaw === void 0 ? void 0 : recoverUnparsedArgs(unparsedRaw);
|
|
10428
|
+
if (recovered !== void 0 && unparsedRaw !== void 0) {
|
|
10429
|
+
const revalidation = await validateSchemaSpec(def.parameters, recovered.value);
|
|
10430
|
+
if (revalidation.valid) {
|
|
10431
|
+
validation = revalidation;
|
|
10432
|
+
options.events?.emit(unparsedRecoveryLog(call.name, unparsedRaw.length, recovered.pass));
|
|
10433
|
+
}
|
|
10434
|
+
}
|
|
10435
|
+
}
|
|
10292
10436
|
if (!validation.valid) return finish({
|
|
10293
10437
|
error: `arguments for '${call.name}' failed validation`,
|
|
10294
10438
|
issues: validation.issues.map((issue) => issue.message)
|
|
@@ -10706,7 +10850,18 @@ async function runAgent(options) {
|
|
|
10706
10850
|
}
|
|
10707
10851
|
if (options.terminalTool !== void 0 && gatedCall.name === options.terminalTool.name) {
|
|
10708
10852
|
const terminalDef = runtime.defs.find((candidate) => candidate.name === gatedCall.name);
|
|
10709
|
-
|
|
10853
|
+
let validation = terminalDef === void 0 ? void 0 : await validateSchemaSpec(terminalDef.parameters, gatedCall.args);
|
|
10854
|
+
if (terminalDef !== void 0 && validation !== void 0 && !validation.valid) {
|
|
10855
|
+
const unparsedRaw = unparsedMarkerOf(gatedCall.args);
|
|
10856
|
+
const recovered = unparsedRaw === void 0 ? void 0 : recoverUnparsedArgs(unparsedRaw);
|
|
10857
|
+
if (recovered !== void 0 && unparsedRaw !== void 0) {
|
|
10858
|
+
const revalidation = await validateSchemaSpec(terminalDef.parameters, recovered.value);
|
|
10859
|
+
if (revalidation.valid) {
|
|
10860
|
+
validation = revalidation;
|
|
10861
|
+
events?.emit(unparsedRecoveryLog(gatedCall.name, unparsedRaw.length, recovered.pass));
|
|
10862
|
+
}
|
|
10863
|
+
}
|
|
10864
|
+
}
|
|
10710
10865
|
if (validation === void 0 || !validation.valid) {
|
|
10711
10866
|
events?.emit({
|
|
10712
10867
|
type: "tool:end",
|
|
@@ -17564,6 +17719,12 @@ function preflightEstimate(input) {
|
|
|
17564
17719
|
message: `spawn '${label}' sets maxOutputTokensPerTurn ${String(limits.maxOutputTokensPerTurn)} above the model's maxOutputTokens ${String(caps.maxOutputTokens)}: the model clamp wins`,
|
|
17565
17720
|
spawn: label
|
|
17566
17721
|
});
|
|
17722
|
+
if (caps?.minOutputTokensPerTurn !== void 0 && limits.maxOutputTokensPerTurn !== void 0 && limits.maxOutputTokensPerTurn < caps.minOutputTokensPerTurn) say({
|
|
17723
|
+
severity: "error",
|
|
17724
|
+
code: "output-cap-below-provider-minimum",
|
|
17725
|
+
message: `spawn '${label}' sets maxOutputTokensPerTurn ${String(limits.maxOutputTokensPerTurn)} below the ${String(caps.minOutputTokensPerTurn)} token output floor of '${servedBy ?? ""}': every dispatch would be refused typed before the wire; raise the cap to at least the floor`,
|
|
17726
|
+
spawn: label
|
|
17727
|
+
});
|
|
17567
17728
|
const turnFloorUsd = pricing === void 0 || outputBound === void 0 ? void 0 : priceUsdOf(pricing, {
|
|
17568
17729
|
inputTokens: spec.estInputTokens ?? 0,
|
|
17569
17730
|
outputTokens: outputBound,
|
|
@@ -17692,6 +17853,12 @@ function preflightEstimate(input) {
|
|
|
17692
17853
|
const pricing = pricingOf(servedBy);
|
|
17693
17854
|
const orchLimits = mergeUsageLimits(input.orchestrator.limits, void 0, defaults.limits);
|
|
17694
17855
|
const outputBound = caps === void 0 ? orchLimits.maxOutputTokensPerTurn : orchLimits.maxOutputTokensPerTurn === void 0 ? caps.maxOutputTokens : Math.min(caps.maxOutputTokens, orchLimits.maxOutputTokensPerTurn);
|
|
17856
|
+
if (caps?.minOutputTokensPerTurn !== void 0 && orchLimits.maxOutputTokensPerTurn !== void 0 && orchLimits.maxOutputTokensPerTurn < caps.minOutputTokensPerTurn) say({
|
|
17857
|
+
severity: "error",
|
|
17858
|
+
code: "output-cap-below-provider-minimum",
|
|
17859
|
+
message: `the orchestrator sets maxOutputTokensPerTurn ${String(orchLimits.maxOutputTokensPerTurn)} below the ${String(caps.minOutputTokensPerTurn)} token output floor of '${servedBy}': every dispatch would be refused typed before the wire; raise the cap to at least the floor`,
|
|
17860
|
+
spawn: "orchestrator"
|
|
17861
|
+
});
|
|
17695
17862
|
const { adapterId, model } = parseModelRef(servedBy);
|
|
17696
17863
|
const unit = {
|
|
17697
17864
|
label: "orchestrator",
|
|
@@ -17739,6 +17906,12 @@ function preflightEstimate(input) {
|
|
|
17739
17906
|
const caps = capsOf(servedBy);
|
|
17740
17907
|
const merged = mergeUsageLimits(synthesis.limits ?? { maxTurns: 4 }, void 0, defaults.limits);
|
|
17741
17908
|
const outputBound = caps === void 0 ? merged.maxOutputTokensPerTurn : merged.maxOutputTokensPerTurn === void 0 ? caps.maxOutputTokens : Math.min(caps.maxOutputTokens, merged.maxOutputTokensPerTurn);
|
|
17909
|
+
if (caps?.minOutputTokensPerTurn !== void 0 && merged.maxOutputTokensPerTurn !== void 0 && merged.maxOutputTokensPerTurn < caps.minOutputTokensPerTurn) say({
|
|
17910
|
+
severity: "error",
|
|
17911
|
+
code: "output-cap-below-provider-minimum",
|
|
17912
|
+
message: `the synthesis invocation sets maxOutputTokensPerTurn ${String(merged.maxOutputTokensPerTurn)} below the ${String(caps.minOutputTokensPerTurn)} token output floor of '${servedBy}': every dispatch would be refused typed before the wire; raise the cap to at least the floor`,
|
|
17913
|
+
spawn: "synthesis"
|
|
17914
|
+
});
|
|
17742
17915
|
const projected = projectedProviderTurnsOf(merged, overallExecutedCeiling(merged, toolCeilingsOf(merged))) + finishRepairReserve;
|
|
17743
17916
|
if (orchestratorEcho !== void 0) orchestratorEcho.synthesis = {
|
|
17744
17917
|
projectedProviderTurns: projected,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@rulvar/core",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.75.1",
|
|
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",
|