@rulvar/core 1.76.0 → 1.77.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
@@ -4197,6 +4197,16 @@ interface AgentResult<T> {
4197
4197
  * seeing a bare 'terminal status limit'.
4198
4198
  */
4199
4199
  partial?: ProgressReport;
4200
+ /**
4201
+ * Terminal-tool exchanges whose ARGUMENTS died at the schema gate
4202
+ * (the unparsed second chance included, when it did not recover): the
4203
+ * v1.74 experiment lost six finish payloads to exactly this class,
4204
+ * and nothing outside the transcript said so (host validation
4205
+ * rejections, by contrast, journal decision entries). Derived from
4206
+ * the message window like the repair-reserve grants, so live and
4207
+ * resumed segments count the same total; absent when zero.
4208
+ */
4209
+ schemaRejectedTerminalExchanges?: number;
4200
4210
  }
4201
4211
  /** One 429's provider-normalized limits, per (provider, model). */
4202
4212
  interface RateLimitObservation {
@@ -7162,8 +7172,17 @@ interface FinishValidationSpec {
7162
7172
  * hash and the validator names. A resumed segment whose live
7163
7173
  * contract hash differs appends a SUPERSEDING descriptor instead of
7164
7174
  * failing, because fixing a stale validator and resuming is the
7165
- * intended remedy, never a fault. Absent = byte identical pre 1.72
7166
- * behavior.
7175
+ * intended remedy, never a fault. The remedy is generation-scoped
7176
+ * (cycle 73): every decision entry written under a contract carries
7177
+ * `contractHash`, and only the CURRENT generation is judged, so
7178
+ * repairsUsed restarts under a fixed contract and a final rejection
7179
+ * a superseded generation left in the crash window neither rolls
7180
+ * forward at boot nor re-arms on replay (its exchange replays byte
7181
+ * identical and the loop continues to a live repair turn). Decisions
7182
+ * recorded before 1.77 carry no hash and bind to the current
7183
+ * contract only while the journal holds a single bundle descriptor;
7184
+ * once a supersession is recorded they are stale. Absent = byte
7185
+ * identical pre 1.72 behavior.
7167
7186
  */
7168
7187
  contract?: FinishContract;
7169
7188
  /**
@@ -8964,6 +8983,13 @@ interface PreflightInput {
8964
8983
  * runtime would actually grant.
8965
8984
  */
8966
8985
  repairTurnReserve?: number;
8986
+ /**
8987
+ * Mirrors FinishValidationSpec.maxRepairs (default
8988
+ * {@link DEFAULT_FINISH_MAX_REPAIRS}): with zero, the first
8989
+ * rejection is final and there is no repair exchange to fund, so
8990
+ * the repair-reserve-unfunded warning stays silent.
8991
+ */
8992
+ maxRepairs?: number;
8967
8993
  };
8968
8994
  }
8969
8995
  /** One linter verdict; `spawn` names the wave entry it is about. */
package/dist/index.js CHANGED
@@ -10181,6 +10181,15 @@ function estimateInputTokens(messages) {
10181
10181
  return Math.ceil(chars / 4);
10182
10182
  }
10183
10183
  /**
10184
+ * The terminal tool's schema-rejection feedback line. One producer, two
10185
+ * readers: the interception writes it as the error result, and the
10186
+ * window-derived schemaRejectedTerminalExchanges counter recognizes the
10187
+ * exchange by exactly these bytes, so the two can never drift.
10188
+ */
10189
+ function terminalSchemaRejectionMessage(name) {
10190
+ return `the '${name}' call failed validation`;
10191
+ }
10192
+ /**
10184
10193
  * The serving model's declared minimum request output cap, default one.
10185
10194
  * OpenAI's Responses API rejects max_output_tokens below 16, so a
10186
10195
  * below-floor dispatch is a guaranteed provider 400: the v1.74
@@ -10870,7 +10879,7 @@ async function runAgent(options) {
10870
10879
  durationMs: now() - gateStartedAt
10871
10880
  });
10872
10881
  parts.push(errorPart(call, {
10873
- error: `the '${gatedCall.name}' call failed validation`,
10882
+ error: terminalSchemaRejectionMessage(gatedCall.name),
10874
10883
  issues: validation === void 0 ? [] : validation.issues.map((i) => i.message)
10875
10884
  }));
10876
10885
  continue;
@@ -12008,6 +12017,9 @@ async function runAgent(options) {
12008
12017
  if (errorMessage !== void 0) result.errorMessage = errorMessage;
12009
12018
  if (guard !== void 0) result.exploration = guard.summary(toolCallsUsed);
12010
12019
  if (limitPartial !== void 0) result.partial = limitPartial;
12020
+ const terminalName = options.terminalTool?.name;
12021
+ const schemaRejectedTerminalExchanges = terminalName === void 0 ? 0 : messages.reduce((count, message) => count + message.parts.filter((part) => part.type === "tool-result" && part.name === terminalName && part.isError === true && part.result?.error === terminalSchemaRejectionMessage(terminalName)).length, 0);
12022
+ if (schemaRejectedTerminalExchanges > 0) result.schemaRejectedTerminalExchanges = schemaRejectedTerminalExchanges;
12011
12023
  if (usageApprox) result.usageApprox = true;
12012
12024
  if (transportRetries > 0) result.transportRetries = transportRetries;
12013
12025
  if (rateLimitObservations.size > 0) result.rateLimitObservations = [...rateLimitObservations.values()];
@@ -16869,6 +16881,12 @@ function makeOrchestratorWorkflow(goal, opts) {
16869
16881
  const validationSpec = opts?.finishValidation;
16870
16882
  const validationAbort = new AbortController();
16871
16883
  let validationTermination;
16884
+ /**
16885
+ * The synthesis invocation's schema-dead finish exchanges (cycle
16886
+ * 73), captured from its full agent result so the failure
16887
+ * enrichment can fold BOTH windows into one honest counter.
16888
+ */
16889
+ let synthesisSchemaRejectedExchanges = 0;
16872
16890
  const finishValidationError = (decision) => new FailRunError(`the orchestrator finish failed host validation with all ${String(decision.maxRepairs)} repair attempts spent: ` + decision.failed.map((f) => `validator '${f.name}' rejected: ${f.reasons.join("; ")}`).join("; "), { data: {
16873
16891
  source: "orchestrator_finish_validation",
16874
16892
  callId: decision.callId,
@@ -16877,6 +16895,22 @@ function makeOrchestratorWorkflow(goal, opts) {
16877
16895
  maxRepairs: decision.maxRepairs
16878
16896
  } });
16879
16897
  const validationDecisions = () => internals.replayer.snapshot().filter((entry) => entry.kind === "decision" && entry.scope === callingState.scope && entry.value?.decisionType === "orchestrator_finish_validation").map((entry) => entry.value);
16898
+ /**
16899
+ * The contract generation membership test (cycle 73). Without a
16900
+ * contract there are no generations and every decision is current
16901
+ * (the pre 1.77 behavior, byte identical). With one, a decision
16902
+ * belongs to the current generation when its journaled hash matches
16903
+ * the live contract; a pre 1.77 decision carries no hash and counts
16904
+ * as current only while the journal has never recorded a
16905
+ * superseding bundle descriptor, because a single descriptor means
16906
+ * every decision was necessarily rendered under it.
16907
+ */
16908
+ const contractGenerationCurrent = (decision) => {
16909
+ const hash = validationSpec?.contract?.hash;
16910
+ if (hash === void 0) return true;
16911
+ if (decision.contractHash !== void 0) return decision.contractHash === hash;
16912
+ return internals.replayer.snapshot().filter((entry) => entry.kind === "decision" && entry.scope === callingState.scope && entry.value?.decisionType === "orchestrator_finish_validation_bundle").length <= 1;
16913
+ };
16880
16914
  const validateFinish = async (call) => {
16881
16915
  if (validationSpec === void 0) return { ok: true };
16882
16916
  const maxRepairs = validationSpec.maxRepairs ?? 1;
@@ -16915,14 +16949,15 @@ function makeOrchestratorWorkflow(goal, opts) {
16915
16949
  reasons: verdict.reasons
16916
16950
  });
16917
16951
  }
16918
- const repairsUsed = known.filter((candidate) => candidate.verdict !== "accepted").length;
16952
+ const repairsUsed = known.filter((candidate) => candidate.verdict !== "accepted" && contractGenerationCurrent(candidate)).length;
16919
16953
  decision = {
16920
16954
  decisionType: "orchestrator_finish_validation",
16921
16955
  callId: call.id,
16922
16956
  verdict: failed.length === 0 ? "accepted" : repairsUsed < maxRepairs ? "repair" : "rejected",
16923
16957
  failed,
16924
16958
  repairsUsed,
16925
- maxRepairs
16959
+ maxRepairs,
16960
+ ...validationSpec.contract === void 0 ? {} : { contractHash: validationSpec.contract.hash }
16926
16961
  };
16927
16962
  await internals.replayer.appendSinglePhase({
16928
16963
  scope: callingState.scope,
@@ -16936,8 +16971,10 @@ function makeOrchestratorWorkflow(goal, opts) {
16936
16971
  }
16937
16972
  if (decision.verdict === "accepted") return { ok: true };
16938
16973
  if (decision.verdict === "rejected") {
16939
- validationTermination = finishValidationError(decision);
16940
- validationAbort.abort("rulvar:finish-validation");
16974
+ if (contractGenerationCurrent(decision)) {
16975
+ validationTermination = finishValidationError(decision);
16976
+ validationAbort.abort("rulvar:finish-validation");
16977
+ }
16941
16978
  return {
16942
16979
  ok: false,
16943
16980
  feedback: {
@@ -17357,6 +17394,7 @@ function makeOrchestratorWorkflow(goal, opts) {
17357
17394
  }
17358
17395
  };
17359
17396
  const synthesized = await runtime.runInScope(synthesisState, () => ctx.agent(prompt, synthesisOpts));
17397
+ synthesisSchemaRejectedExchanges = synthesized.schemaRejectedTerminalExchanges ?? 0;
17360
17398
  if (validationTermination !== void 0) throw validationTermination;
17361
17399
  if (synthesized.status === "ok") return synthesized.output;
17362
17400
  if (validationSpec !== void 0) throw new FailRunError(`the synthesis invocation terminated with status '${synthesized.status}'` + (synthesized.errorMessage === void 0 ? "" : `: ${synthesized.errorMessage}`) + "; finish validators are configured, so the unvalidated draft cannot stand", { data: {
@@ -17419,7 +17457,10 @@ function makeOrchestratorWorkflow(goal, opts) {
17419
17457
  if (capDecisionRef !== void 0) return await settleCapOutcome();
17420
17458
  if (validationSpec !== void 0) {
17421
17459
  const priorRejection = validationDecisions().find((decision) => decision.verdict === "rejected");
17422
- if (priorRejection !== void 0) throw finishValidationError(priorRejection);
17460
+ if (priorRejection !== void 0) {
17461
+ const settle = lastRunSettle(internals.replayer.snapshot());
17462
+ if (settle !== void 0 && settle.runStatus !== "running" && settle.runStatus !== "suspended" || contractGenerationCurrent(priorRejection)) throw finishValidationError(priorRejection);
17463
+ }
17423
17464
  }
17424
17465
  const promptLines = [
17425
17466
  ...extension?.promptLines?.() ?? [],
@@ -17430,28 +17471,28 @@ function makeOrchestratorWorkflow(goal, opts) {
17430
17471
  const liveTermination = extensionTermination;
17431
17472
  if (liveTermination !== void 0) throw liveTermination;
17432
17473
  if (capDecisionRef !== void 0) return await settleCapOutcome();
17433
- if (validationTermination !== void 0) throw validationTermination;
17434
- if (orchestratorAccount !== void 0) internals.cost.orchestrator.spentUsd = internals.budget.accountView(orchestratorAccount)?.spentUsd ?? 0;
17435
- if (result.status !== "ok") throw new ConfigError(`the orchestrator agent terminated with status '${result.status}'` + (result.errorMessage === void 0 ? "" : `: ${result.errorMessage}`));
17436
17474
  /**
17437
- * The synthesis failure enrichment (the v1.71 experiment review,
17438
- * P0.8 remainder + P1.7): every typed failure a synthesis
17439
- * invocation throws gains the verdict-derived repair taxonomy read
17440
- * from the JOURNALED validation decisions (identical live and on
17441
- * replay), and, when an acceptance verdict exists, the acceptance
17442
- * snapshot the children already earned; the completion mirror then
17443
- * lifts completion and childStatusCounts onto the error outcome,
17444
- * exactly like the acceptance rejection path. The experiment's
17445
- * outcome showed completion null beside four accepted children;
17446
- * these are the fields that say "the fan-out work is complete, the
17447
- * failure is downstream". The rejection-past-the-bound error keeps
17448
- * its own repairsUsed/maxRepairs/failed untouched.
17475
+ * The finish-validation failure enrichment (the v1.71 experiment
17476
+ * review, P0.8 remainder + P1.7; widened by cycle 73): every typed
17477
+ * failure a finish rejection produces gains the verdict-derived
17478
+ * repair taxonomy read from the JOURNALED validation decisions of
17479
+ * the CURRENT contract generation (identical live and on replay),
17480
+ * the schema-dead finish exchange counter derived from the
17481
+ * coordination and synthesis windows (the class the v1.74 run lost
17482
+ * six payloads to, invisible in every other field), and, when an
17483
+ * acceptance verdict exists, the acceptance snapshot the children
17484
+ * already earned: completion and childStatusCounts, and now the
17485
+ * degradation facts beside them (degradedReasons and the salvage
17486
+ * lists), exactly what the ok envelope reports. The
17487
+ * rejection-past-the-bound error keeps its own
17488
+ * repairsUsed/maxRepairs/failed untouched.
17449
17489
  */
17450
17490
  const enrichSynthesisFailure = (thrown, snapshot) => {
17451
17491
  if (!(thrown instanceof FailRunError)) throw thrown;
17452
17492
  const base = thrown.data ?? {};
17453
- const spent = validationDecisions().filter((candidate) => candidate.verdict !== "accepted");
17493
+ const spent = validationDecisions().filter((candidate) => candidate.verdict !== "accepted" && contractGenerationCurrent(candidate));
17454
17494
  const rejectedValidators = [...new Set(spent.flatMap((candidate) => candidate.failed.map((f) => f.name)))];
17495
+ const schemaRejected = (result.schemaRejectedTerminalExchanges ?? 0) + synthesisSchemaRejectedExchanges;
17455
17496
  throw new FailRunError(thrown.message, { data: {
17456
17497
  ...base,
17457
17498
  ...snapshot ?? {},
@@ -17459,9 +17500,13 @@ function makeOrchestratorWorkflow(goal, opts) {
17459
17500
  repairsUsed: spent.length,
17460
17501
  maxRepairs: validationSpec?.maxRepairs ?? 1,
17461
17502
  rejectedValidators
17462
- }
17503
+ },
17504
+ ...schemaRejected === 0 || base.schemaRejectedFinishExchanges !== void 0 ? {} : { schemaRejectedFinishExchanges: schemaRejected }
17463
17505
  } });
17464
17506
  };
17507
+ if (validationTermination !== void 0) enrichSynthesisFailure(validationTermination);
17508
+ if (orchestratorAccount !== void 0) internals.cost.orchestrator.spentUsd = internals.budget.accountView(orchestratorAccount)?.spentUsd ?? 0;
17509
+ if (result.status !== "ok") throw new ConfigError(`the orchestrator agent terminated with status '${result.status}'` + (result.errorMessage === void 0 ? "" : `: ${result.errorMessage}`));
17465
17510
  if (opts?.acceptance === void 0) try {
17466
17511
  return await runSynthesis(result.output);
17467
17512
  } catch (thrown) {
@@ -17545,7 +17590,10 @@ function makeOrchestratorWorkflow(goal, opts) {
17545
17590
  } catch (thrown) {
17546
17591
  enrichSynthesisFailure(thrown, {
17547
17592
  completion: decision.completion,
17548
- childStatusCounts: decision.childStatusCounts
17593
+ childStatusCounts: decision.childStatusCounts,
17594
+ degradedReasons: decision.degradedReasons,
17595
+ ...decision.salvagedPartialChildren === void 0 ? {} : { salvagedPartialChildren: decision.salvagedPartialChildren },
17596
+ ...decision.salvagedTerminalOutputChildren === void 0 ? {} : { salvagedTerminalOutputChildren: decision.salvagedTerminalOutputChildren }
17549
17597
  });
17550
17598
  }
17551
17599
  return {
@@ -17677,6 +17725,39 @@ function preflightEstimate(input) {
17677
17725
  const say = (finding) => {
17678
17726
  findings.push(finding);
17679
17727
  };
17728
+ /**
17729
+ * The contract turn feasibility check (the v1.74 experiment review,
17730
+ * cycle 73), run against the invocation the validators bind. The
17731
+ * floor is the contract's own minimal accepting payload, serialized
17732
+ * exactly as the model must emit it ({ result }) and priced at the
17733
+ * loop's four-characters-per-token output heuristic: the v1.74 run's
17734
+ * conforming payloads truncated at their 9000 token turn cap, and its
17735
+ * contract's minimum alone prices above that cap. A minimum at or
17736
+ * over the bound is an error (every conforming finish truncates mid
17737
+ * payload); a minimum within double of the bound is a warning,
17738
+ * because real conforming payloads run richer than the minimum.
17739
+ */
17740
+ const contractFeasibilityFindings = (outputBound, spawn, servedBy) => {
17741
+ const contract = input.finishValidation?.contract;
17742
+ if (contract === void 0 || outputBound === void 0) return;
17743
+ const minTokens = Math.ceil(JSON.stringify({ result: contract.goldenAccept.text }).length / 4);
17744
+ const which = spawn === "synthesis" ? "the synthesis invocation" : "the coordination loop";
17745
+ if (minTokens >= outputBound) {
17746
+ say({
17747
+ severity: "error",
17748
+ code: "output-contract-turn-infeasible",
17749
+ message: `the finish contract's minimal accepting payload is about ${String(minTokens)} tokens (four characters per token over the golden accept fixture) but ${which} dispatches at most ${String(outputBound)} output tokens per turn ('${servedBy}'): every conforming finish truncates mid payload; raise maxOutputTokensPerTurn or shrink the contract`,
17750
+ spawn
17751
+ });
17752
+ return;
17753
+ }
17754
+ if (minTokens * 2 > outputBound) say({
17755
+ severity: "warning",
17756
+ code: "output-contract-turn-headroom",
17757
+ message: `the finish contract's minimal accepting payload is about ${String(minTokens)} tokens against the ${String(outputBound)} token output bound of ${which} ('${servedBy}'): real conforming payloads run richer than the minimum and the margin is under double; raise the bound or trim the contract`,
17758
+ spawn
17759
+ });
17760
+ };
17680
17761
  const adapters = new Map((engine.adapters ?? []).map((adapter) => [adapter.id, adapter]));
17681
17762
  const capsOf = (ref) => {
17682
17763
  const { adapterId, model } = parseModelRef(ref);
@@ -17922,6 +18003,7 @@ function preflightEstimate(input) {
17922
18003
  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`,
17923
18004
  spawn: "orchestrator"
17924
18005
  });
18006
+ if (input.orchestrator.synthesis === void 0) contractFeasibilityFindings(outputBound, "orchestrator", servedBy);
17925
18007
  const { adapterId, model } = parseModelRef(servedBy);
17926
18008
  const unit = {
17927
18009
  label: "orchestrator",
@@ -17990,6 +18072,7 @@ function preflightEstimate(input) {
17990
18072
  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`,
17991
18073
  spawn: "synthesis"
17992
18074
  });
18075
+ contractFeasibilityFindings(outputBound, "synthesis", servedBy);
17993
18076
  const projected = projectedProviderTurnsOf(merged, overallExecutedCeiling(merged, toolCeilingsOf(merged))) + finishRepairReserve;
17994
18077
  if (orchestratorEcho !== void 0) orchestratorEcho.synthesis = {
17995
18078
  projectedProviderTurns: projected,
@@ -18214,6 +18297,15 @@ function preflightEstimate(input) {
18214
18297
  names.add(validator.name);
18215
18298
  }
18216
18299
  if (fv.repairTurnReserve !== void 0) requireNonNegativeInteger(fv.repairTurnReserve, "preflight finishValidation.repairTurnReserve");
18300
+ if (fv.maxRepairs !== void 0) requireNonNegativeInteger(fv.maxRepairs, "preflight finishValidation.maxRepairs");
18301
+ {
18302
+ const grantedRepairs = fv.maxRepairs ?? 1;
18303
+ if (grantedRepairs > 0 && fv.repairTurnReserve === void 0) findings.push({
18304
+ severity: "warning",
18305
+ code: "repair-reserve-unfunded",
18306
+ message: `finishValidation grants up to ${String(grantedRepairs)} repair exchange(s) but declares no repairTurnReserve: a rejected finish burns an ordinary turn and a window at maxTurns settles 'limit' with the repair unspent; set finishValidation.repairTurnReserve to fund the repair exchanges`
18307
+ });
18308
+ }
18217
18309
  if (fv.contract !== void 0) {
18218
18310
  for (const contractValidator of fv.contract.validators) if (!names.has(contractValidator.name)) findings.push({
18219
18311
  severity: "error",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rulvar/core",
3
- "version": "1.76.0",
3
+ "version": "1.77.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",