@rulvar/core 1.72.0 → 1.73.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
@@ -4405,6 +4405,18 @@ interface RunAgentOptions<S extends SchemaSpec = JsonSchema> {
4405
4405
  ok: false;
4406
4406
  feedback: Record<string, unknown>;
4407
4407
  }>;
4408
+ /**
4409
+ * The repair reserve (the v1.71 experiment review, P0.4): max EXTRA
4410
+ * turns the loop may grant past limits.maxTurns, one per rejected
4411
+ * terminal-tool exchange, schema-invalid arguments and host
4412
+ * validation rejections alike. The grant count derives from the
4413
+ * message window itself (error tool results named after the
4414
+ * terminal tool, clamped to the reserve), so a resumed segment that
4415
+ * restored the window mid-exchange re-derives the same grants and
4416
+ * nothing needs journaling. Zero (or absent) keeps the ceiling
4417
+ * byte identical to the pre 1.73 loop.
4418
+ */
4419
+ repairTurnReserve?: number;
4408
4420
  };
4409
4421
  agentType?: string;
4410
4422
  /** The primary invocation role of the tool loop; default 'loop' (M6-T05; RV-211 adds synthesize). */
@@ -7038,6 +7050,23 @@ interface FinishValidationSpec {
7038
7050
  */
7039
7051
  maxRepairs?: number;
7040
7052
  /**
7053
+ * The repair turn reserve (the v1.71 experiment review, P0.4; the
7054
+ * reserve RV-204 deliberately deferred). A nonnegative integer,
7055
+ * default 0: max EXTRA turns the invocation the validators bind (the
7056
+ * synthesis invocation when `synthesis` is configured, the
7057
+ * coordination loop otherwise) may consume past its `maxTurns`, one
7058
+ * granted per rejected finish exchange, schema-invalid finish
7059
+ * arguments and host validation rejections alike. Without it, repair
7060
+ * exchanges and generation compete for the same turn budget: the
7061
+ * v1.71 experiment lost its whole run to one malformed finish plus
7062
+ * one validator rejection inside maxTurns 3. The reserve is bounded,
7063
+ * spends from the ordinary budget ceilings (a granted turn is a paid
7064
+ * provider turn), and folds into the preflight turn projection
7065
+ * (`projectedProviderTurns` and the run ceiling) when declared
7066
+ * there. Zero keeps the pre 1.73 ceiling byte identical.
7067
+ */
7068
+ repairTurnReserve?: number;
7069
+ /**
7041
7070
  * The unified output contract this validator set enforces (the v1.71
7042
7071
  * experiment review, P0.1/P0.2). Construction then runs the golden
7043
7072
  * self test with the contract's fixtures as defaults, the contract's
@@ -8749,6 +8778,23 @@ interface PreflightOrchestratorSpec {
8749
8778
  * root, so only they subtract it from spawn-admission headroom.
8750
8779
  */
8751
8780
  extension?: boolean;
8781
+ /**
8782
+ * The separate synthesis invocation (RV-211), when the orchestration
8783
+ * configures one (the v1.71 experiment review: the run ceiling used
8784
+ * to stop at the coordination loop, undercounting the synthesis
8785
+ * turns). `limits` mirrors OrchestrateSynthesis.limits exactly
8786
+ * (absent = the DEFAULT_SYNTHESIS_MAX_TURNS invocation), `model`
8787
+ * mirrors its model override (absent = defaults.routing.synthesize),
8788
+ * and `estInputTokens` is the prompt-size stand-in for the derived
8789
+ * synthesis prompt. When `finishValidation.repairTurnReserve` is
8790
+ * declared, the reserve folds into THIS invocation's projected turns,
8791
+ * because the validators bind the synthesis finish.
8792
+ */
8793
+ synthesis?: {
8794
+ model?: ModelSpec;
8795
+ limits?: UsageLimits;
8796
+ estInputTokens?: number;
8797
+ };
8752
8798
  }
8753
8799
  /** The full input: engine surface, run surface, and the declared wave. */
8754
8800
  interface PreflightInput {
@@ -8780,6 +8826,15 @@ interface PreflightInput {
8780
8826
  validators: FinishValidator[];
8781
8827
  contract?: FinishContract;
8782
8828
  selfTest?: FinishSelfTestFixtures;
8829
+ /**
8830
+ * Mirrors FinishValidationSpec.repairTurnReserve: folds the
8831
+ * declared repair headroom into the projected turns of the
8832
+ * invocation the validators bind (the synthesis invocation when
8833
+ * orchestrator.synthesis is declared, the coordination loop
8834
+ * otherwise), so the run ceiling prices the repair exchange the
8835
+ * runtime would actually grant.
8836
+ */
8837
+ repairTurnReserve?: number;
8783
8838
  };
8784
8839
  }
8785
8840
  /** One linter verdict; `spawn` names the wave entry it is about. */
@@ -8861,6 +8916,16 @@ interface PreflightReport {
8861
8916
  finalizeTurns: number; /** Whether the finalize reserve is committed against the run root (extension runs). */
8862
8917
  reserveCommitted: boolean; /** The orchestrator agent's own loop ceiling, derived exactly like a spawn's. */
8863
8918
  projectedProviderTurns: number;
8919
+ /**
8920
+ * The separate synthesis invocation's projection, present when
8921
+ * input.orchestrator.synthesis was declared and the role
8922
+ * resolves: its turn ceiling (the repair turn reserve folded in
8923
+ * when declared) and its serving model.
8924
+ */
8925
+ synthesis?: {
8926
+ projectedProviderTurns: number;
8927
+ servedBy?: ModelRef;
8928
+ };
8864
8929
  };
8865
8930
  };
8866
8931
  quota: {
package/dist/index.js CHANGED
@@ -11087,7 +11087,9 @@ async function runAgent(options) {
11087
11087
  status = "limit";
11088
11088
  break;
11089
11089
  }
11090
- if (turns >= limits.maxTurns) {
11090
+ const repairReserve = options.terminalTool?.repairTurnReserve ?? 0;
11091
+ const grantedRepairTurns = repairReserve === 0 ? 0 : Math.min(repairReserve, messages.reduce((count, message) => count + message.parts.filter((part) => part.type === "tool-result" && part.name === options.terminalTool?.name && part.isError === true).length, 0));
11092
+ if (turns >= limits.maxTurns + grantedRepairTurns) {
11091
11093
  status = "limit";
11092
11094
  break;
11093
11095
  }
@@ -15651,6 +15653,7 @@ function validateOrchestrateOptions(opts) {
15651
15653
  seen.add(validator.name);
15652
15654
  }
15653
15655
  if (fv.maxRepairs !== void 0) requireNonNegativeInteger(fv.maxRepairs, "orchestrate finishValidation.maxRepairs");
15656
+ if (fv.repairTurnReserve !== void 0) requireNonNegativeInteger(fv.repairTurnReserve, "orchestrate finishValidation.repairTurnReserve");
15654
15657
  const contract = fv.contract;
15655
15658
  if (contract !== void 0) {
15656
15659
  const shape = contract;
@@ -16756,7 +16759,10 @@ function makeOrchestratorWorkflow(goal, opts) {
16756
16759
  },
16757
16760
  [kTerminalTool]: {
16758
16761
  name: FINISH_TOOL_NAME,
16759
- ...validationSpec === void 0 || opts?.synthesis !== void 0 ? {} : { validate: validateFinish }
16762
+ ...validationSpec === void 0 || opts?.synthesis !== void 0 ? {} : {
16763
+ validate: validateFinish,
16764
+ ...validationSpec.repairTurnReserve === void 0 ? {} : { repairTurnReserve: validationSpec.repairTurnReserve }
16765
+ }
16760
16766
  },
16761
16767
  ...(() => {
16762
16768
  const priorCancelledRoot = internals.replayer.snapshot().filter((entry) => entry.kind === "agent" && entry.scope === callingState.scope && entry.status === "cancelled" && entry.checkpointRef !== void 0).at(-1);
@@ -17056,7 +17062,10 @@ function makeOrchestratorWorkflow(goal, opts) {
17056
17062
  ...spec.estCost === void 0 ? {} : { estCost: spec.estCost },
17057
17063
  [kTerminalTool]: {
17058
17064
  name: FINISH_TOOL_NAME,
17059
- ...validationSpec === void 0 ? {} : { validate: validateFinish }
17065
+ ...validationSpec === void 0 ? {} : {
17066
+ validate: validateFinish,
17067
+ ...validationSpec.repairTurnReserve === void 0 ? {} : { repairTurnReserve: validationSpec.repairTurnReserve }
17068
+ }
17060
17069
  }
17061
17070
  };
17062
17071
  const synthesized = await runtime.runInScope(synthesisState, () => ctx.agent(prompt, synthesisOpts));
@@ -17136,7 +17145,40 @@ function makeOrchestratorWorkflow(goal, opts) {
17136
17145
  if (validationTermination !== void 0) throw validationTermination;
17137
17146
  if (orchestratorAccount !== void 0) internals.cost.orchestrator.spentUsd = internals.budget.accountView(orchestratorAccount)?.spentUsd ?? 0;
17138
17147
  if (result.status !== "ok") throw new ConfigError(`the orchestrator agent terminated with status '${result.status}'` + (result.errorMessage === void 0 ? "" : `: ${result.errorMessage}`));
17139
- if (opts?.acceptance === void 0) return await runSynthesis(result.output);
17148
+ /**
17149
+ * The synthesis failure enrichment (the v1.71 experiment review,
17150
+ * P0.8 remainder + P1.7): every typed failure a synthesis
17151
+ * invocation throws gains the verdict-derived repair taxonomy read
17152
+ * from the JOURNALED validation decisions (identical live and on
17153
+ * replay), and, when an acceptance verdict exists, the acceptance
17154
+ * snapshot the children already earned; the completion mirror then
17155
+ * lifts completion and childStatusCounts onto the error outcome,
17156
+ * exactly like the acceptance rejection path. The experiment's
17157
+ * outcome showed completion null beside four accepted children;
17158
+ * these are the fields that say "the fan-out work is complete, the
17159
+ * failure is downstream". The rejection-past-the-bound error keeps
17160
+ * its own repairsUsed/maxRepairs/failed untouched.
17161
+ */
17162
+ const enrichSynthesisFailure = (thrown, snapshot) => {
17163
+ if (!(thrown instanceof FailRunError)) throw thrown;
17164
+ const base = thrown.data ?? {};
17165
+ const spent = validationDecisions().filter((candidate) => candidate.verdict !== "accepted");
17166
+ const rejectedValidators = [...new Set(spent.flatMap((candidate) => candidate.failed.map((f) => f.name)))];
17167
+ throw new FailRunError(thrown.message, { data: {
17168
+ ...base,
17169
+ ...snapshot ?? {},
17170
+ ...spent.length === 0 || base.repairsUsed !== void 0 ? {} : {
17171
+ repairsUsed: spent.length,
17172
+ maxRepairs: validationSpec?.maxRepairs ?? 1,
17173
+ rejectedValidators
17174
+ }
17175
+ } });
17176
+ };
17177
+ if (opts?.acceptance === void 0) try {
17178
+ return await runSynthesis(result.output);
17179
+ } catch (thrown) {
17180
+ return enrichSynthesisFailure(thrown);
17181
+ }
17140
17182
  const acceptanceKey = "acceptance";
17141
17183
  const priorAcceptance = internals.replayer.snapshot().find((entry) => entry.kind === "decision" && entry.scope === callingState.scope && entry.key === acceptanceKey);
17142
17184
  let decision;
@@ -17209,8 +17251,17 @@ function makeOrchestratorWorkflow(goal, opts) {
17209
17251
  ...decision.synthesisSkipped === void 0 ? {} : { synthesisSkipped: decision.synthesisSkipped }
17210
17252
  } });
17211
17253
  }
17254
+ let synthesizedFinal;
17255
+ try {
17256
+ synthesizedFinal = await runSynthesis(result.output);
17257
+ } catch (thrown) {
17258
+ enrichSynthesisFailure(thrown, {
17259
+ completion: decision.completion,
17260
+ childStatusCounts: decision.childStatusCounts
17261
+ });
17262
+ }
17212
17263
  return {
17213
- result: await runSynthesis(result.output),
17264
+ result: synthesizedFinal,
17214
17265
  completion: decision.completion,
17215
17266
  childStatusCounts: decision.childStatusCounts,
17216
17267
  degradedReasons: decision.degradedReasons,
@@ -17351,6 +17402,8 @@ function preflightEstimate(input) {
17351
17402
  const maxDepth = engine.budgetDefaults?.maxDepth ?? 1;
17352
17403
  const perRun = engine.concurrency?.perRun ?? 12;
17353
17404
  const runLimits = mergeUsageLimits(void 0, input.run?.limits, defaults.limits);
17405
+ const finishRepairReserve = input.finishValidation?.repairTurnReserve ?? 0;
17406
+ const coordinationRepairReserve = input.orchestrator?.synthesis === void 0 ? finishRepairReserve : 0;
17354
17407
  let orchestratorEcho;
17355
17408
  let reservedForFinalizationUsd = 0;
17356
17409
  let effectiveCapUsd;
@@ -17371,7 +17424,7 @@ function preflightEstimate(input) {
17371
17424
  finalizeReserveUsd,
17372
17425
  finalizeTurns,
17373
17426
  reserveCommitted,
17374
- projectedProviderTurns: projectedProviderTurnsOf(echoLimits, overallExecutedCeiling(echoLimits, toolCeilingsOf(echoLimits)))
17427
+ projectedProviderTurns: projectedProviderTurnsOf(echoLimits, overallExecutedCeiling(echoLimits, toolCeilingsOf(echoLimits))) + coordinationRepairReserve
17375
17428
  };
17376
17429
  if (spec?.capUsd !== void 0 && spec.capFraction === void 0 && effectiveCapUsd !== void 0 && effectiveCapUsd < spec.capUsd) say({
17377
17430
  severity: "warning",
@@ -17589,7 +17642,7 @@ function preflightEstimate(input) {
17589
17642
  model,
17590
17643
  inputFloor: input.orchestrator.estInputTokens ?? 0,
17591
17644
  outputBound: outputBound ?? 0,
17592
- turns: projectedProviderTurnsOf(orchLimits, overallExecutedCeiling(orchLimits, toolCeilingsOf(orchLimits))),
17645
+ turns: projectedProviderTurnsOf(orchLimits, overallExecutedCeiling(orchLimits, toolCeilingsOf(orchLimits))) + coordinationRepairReserve,
17593
17646
  count: 1
17594
17647
  });
17595
17648
  if (effectiveCapUsd !== void 0) orchestratorReserveUsd = Math.max(0, orchestratorAdmissionEstCostUsd(effectiveCapUsd, reservedForFinalizationUsd));
@@ -17601,6 +17654,38 @@ function preflightEstimate(input) {
17601
17654
  flatReserveUsd
17602
17655
  });
17603
17656
  }
17657
+ if (input.orchestrator.synthesis !== void 0) {
17658
+ const synthesis = input.orchestrator.synthesis;
17659
+ if (synthesis.limits !== void 0) validateUsageLimits(synthesis.limits, "preflight orchestrator.synthesis.limits");
17660
+ if (synthesis.estInputTokens !== void 0) requireNonNegativeInteger(synthesis.estInputTokens, "preflight orchestrator.synthesis.estInputTokens");
17661
+ const servedBy = resolveServing(synthesis.model) ?? resolveServing(defaults.routing?.synthesize);
17662
+ if (servedBy === void 0) say({
17663
+ severity: "error",
17664
+ code: "unrouted-role",
17665
+ message: "the synthesis invocation resolves no model for role 'synthesize': set defaults.routing.synthesize or a synthesis model on the call",
17666
+ spawn: "synthesis"
17667
+ });
17668
+ else {
17669
+ const caps = capsOf(servedBy);
17670
+ const merged = mergeUsageLimits(synthesis.limits ?? { maxTurns: 4 }, void 0, defaults.limits);
17671
+ const outputBound = caps === void 0 ? merged.maxOutputTokensPerTurn : merged.maxOutputTokensPerTurn === void 0 ? caps.maxOutputTokens : Math.min(caps.maxOutputTokens, merged.maxOutputTokensPerTurn);
17672
+ const projected = projectedProviderTurnsOf(merged, overallExecutedCeiling(merged, toolCeilingsOf(merged))) + finishRepairReserve;
17673
+ if (orchestratorEcho !== void 0) orchestratorEcho.synthesis = {
17674
+ projectedProviderTurns: projected,
17675
+ servedBy
17676
+ };
17677
+ const { adapterId, model } = parseModelRef(servedBy);
17678
+ shapes.push({
17679
+ label: "synthesis",
17680
+ provider: adapterId,
17681
+ model,
17682
+ inputFloor: synthesis.estInputTokens ?? 0,
17683
+ outputBound: outputBound ?? 0,
17684
+ turns: projected,
17685
+ count: 1
17686
+ });
17687
+ }
17688
+ }
17604
17689
  }
17605
17690
  const wave = [];
17606
17691
  let committed = 0;
@@ -17807,6 +17892,7 @@ function preflightEstimate(input) {
17807
17892
  if (typeof validator.validate !== "function") throw new ConfigError(`preflight finish validator '${validator.name}' has no validate function`);
17808
17893
  names.add(validator.name);
17809
17894
  }
17895
+ if (fv.repairTurnReserve !== void 0) requireNonNegativeInteger(fv.repairTurnReserve, "preflight finishValidation.repairTurnReserve");
17810
17896
  if (fv.contract !== void 0) {
17811
17897
  for (const contractValidator of fv.contract.validators) if (!names.has(contractValidator.name)) findings.push({
17812
17898
  severity: "error",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rulvar/core",
3
- "version": "1.72.0",
3
+ "version": "1.73.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",