@rulvar/core 1.74.0 → 1.75.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 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; /** Adapter-reported fallback only; the versioned price table wins. */
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. The clamp uses the heuristic prompt estimate; the DENIAL does
10187
- * not: a turn is refused (BudgetExhaustedError, never dispatched) only
10188
- * when the remainder cannot buy even ONE output token at zero input,
10189
- * which is exact. Denying on the estimate would kill turns the budget
10190
- * still funds, including the DEF-7 forced finish paid from the released
10191
- * finalize reserve; when the estimate says the prompt alone spends the
10192
- * remainder, the turn dispatches with a one-token output floor and the
10193
- * exact layers (2 and 3) settle the difference. A no-op without a hook
10194
- * or when the hook reports no bound. The clamp touches only the wire
10195
- * request, exactly like limits.maxOutputTokensPerTurn above it; identity
10196
- * is computed at the ctx layer and never sees it.
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 < 1) {
10225
+ if (affordable < floor) {
10204
10226
  const zeroInputAffordable = hook(target.resolved.ref, 0);
10205
- if (zeroInputAffordable !== void 0 && zeroInputAffordable < 1) throw new BudgetExhaustedError(`the remaining budget cannot afford one output token from ${target.resolved.ref}; the turn was not dispatched`);
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: 1
10230
+ maxOutputTokens: floor
10209
10231
  };
10210
10232
  }
10211
10233
  if (req.maxOutputTokens === void 0 || affordable < req.maxOutputTokens) return {
@@ -10260,6 +10282,101 @@ 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
+ /**
10263
10380
  * Executes one model-issued tool call to a tool-result part. Failures are
10264
10381
  * surfaced to the model as error tool results and never thrown past
10265
10382
  * policy: unknown names, argument-validation issues, ModelRetry (bounded
@@ -10288,7 +10405,22 @@ async function executeToolCall(options) {
10288
10405
  return part;
10289
10406
  };
10290
10407
  if (def === void 0) return finish({ error: `unknown tool '${call.name}'` }, "error");
10291
- const validation = await validateSchemaSpec(def.parameters, call.args);
10408
+ let validation = await validateSchemaSpec(def.parameters, call.args);
10409
+ if (!validation.valid) {
10410
+ const unparsedRaw = unparsedMarkerOf(call.args);
10411
+ const recovered = unparsedRaw === void 0 ? void 0 : recoverUnparsedArgs(unparsedRaw);
10412
+ if (recovered !== void 0) {
10413
+ const revalidation = await validateSchemaSpec(def.parameters, recovered.value);
10414
+ if (revalidation.valid) {
10415
+ validation = revalidation;
10416
+ options.events?.emit({
10417
+ type: "log",
10418
+ level: "warn",
10419
+ msg: `arguments for '${call.name}' arrived unparsed (${String(unparsedRaw?.length ?? 0)} chars) and were recovered by a ${recovered.pass} JSON pass; the recovered object passed the tool schema and the call executed`
10420
+ });
10421
+ }
10422
+ }
10423
+ }
10292
10424
  if (!validation.valid) return finish({
10293
10425
  error: `arguments for '${call.name}' failed validation`,
10294
10426
  issues: validation.issues.map((issue) => issue.message)
@@ -17564,6 +17696,12 @@ function preflightEstimate(input) {
17564
17696
  message: `spawn '${label}' sets maxOutputTokensPerTurn ${String(limits.maxOutputTokensPerTurn)} above the model's maxOutputTokens ${String(caps.maxOutputTokens)}: the model clamp wins`,
17565
17697
  spawn: label
17566
17698
  });
17699
+ if (caps?.minOutputTokensPerTurn !== void 0 && limits.maxOutputTokensPerTurn !== void 0 && limits.maxOutputTokensPerTurn < caps.minOutputTokensPerTurn) say({
17700
+ severity: "error",
17701
+ code: "output-cap-below-provider-minimum",
17702
+ 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`,
17703
+ spawn: label
17704
+ });
17567
17705
  const turnFloorUsd = pricing === void 0 || outputBound === void 0 ? void 0 : priceUsdOf(pricing, {
17568
17706
  inputTokens: spec.estInputTokens ?? 0,
17569
17707
  outputTokens: outputBound,
@@ -17692,6 +17830,12 @@ function preflightEstimate(input) {
17692
17830
  const pricing = pricingOf(servedBy);
17693
17831
  const orchLimits = mergeUsageLimits(input.orchestrator.limits, void 0, defaults.limits);
17694
17832
  const outputBound = caps === void 0 ? orchLimits.maxOutputTokensPerTurn : orchLimits.maxOutputTokensPerTurn === void 0 ? caps.maxOutputTokens : Math.min(caps.maxOutputTokens, orchLimits.maxOutputTokensPerTurn);
17833
+ if (caps?.minOutputTokensPerTurn !== void 0 && orchLimits.maxOutputTokensPerTurn !== void 0 && orchLimits.maxOutputTokensPerTurn < caps.minOutputTokensPerTurn) say({
17834
+ severity: "error",
17835
+ code: "output-cap-below-provider-minimum",
17836
+ 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`,
17837
+ spawn: "orchestrator"
17838
+ });
17695
17839
  const { adapterId, model } = parseModelRef(servedBy);
17696
17840
  const unit = {
17697
17841
  label: "orchestrator",
@@ -17739,6 +17883,12 @@ function preflightEstimate(input) {
17739
17883
  const caps = capsOf(servedBy);
17740
17884
  const merged = mergeUsageLimits(synthesis.limits ?? { maxTurns: 4 }, void 0, defaults.limits);
17741
17885
  const outputBound = caps === void 0 ? merged.maxOutputTokensPerTurn : merged.maxOutputTokensPerTurn === void 0 ? caps.maxOutputTokens : Math.min(caps.maxOutputTokens, merged.maxOutputTokensPerTurn);
17886
+ if (caps?.minOutputTokensPerTurn !== void 0 && merged.maxOutputTokensPerTurn !== void 0 && merged.maxOutputTokensPerTurn < caps.minOutputTokensPerTurn) say({
17887
+ severity: "error",
17888
+ code: "output-cap-below-provider-minimum",
17889
+ 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`,
17890
+ spawn: "synthesis"
17891
+ });
17742
17892
  const projected = projectedProviderTurnsOf(merged, overallExecutedCeiling(merged, toolCeilingsOf(merged))) + finishRepairReserve;
17743
17893
  if (orchestratorEcho !== void 0) orchestratorEcho.synthesis = {
17744
17894
  projectedProviderTurns: projected,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rulvar/core",
3
- "version": "1.74.0",
3
+ "version": "1.75.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",