@rulvar/core 1.75.1 → 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 {
@@ -7126,6 +7136,30 @@ interface FinishValidationSpec {
7126
7136
  */
7127
7137
  repairTurnReserve?: number;
7128
7138
  /**
7139
+ * The coordination draft gate (the v1.74 experiment review, P0.3),
7140
+ * meaningful ONLY with `synthesis` configured: with validators bound
7141
+ * to the synthesis finish, the coordination finish is an unvalidated
7142
+ * draft, and the experiment's model escaped six failed finish
7143
+ * exchanges with the schema-valid draft 'test', which then starved
7144
+ * synthesis of every citation the validators demanded. The policy
7145
+ * runs deterministic library checks on each coordination finish
7146
+ * (whitespace-token `minWords`, literal `requireSections` markers,
7147
+ * the wordCountValidator and requiredSectionsValidator semantics);
7148
+ * a failing draft returns to the model as the finish call's error
7149
+ * result and the turn continues, exactly like a host validation
7150
+ * rejection, and `repairTurnReserve` grants coordination the same
7151
+ * per-rejected-exchange headroom it grants the synthesis finish.
7152
+ * Pure text checks over the durable exchange: nothing journals, a
7153
+ * resumed segment recounts identically, and `maxRepairs` is not
7154
+ * consumed (it belongs to the synthesis-bound validators). Absent =
7155
+ * byte identical pre 1.76 behavior; configured without `synthesis` =
7156
+ * ConfigError.
7157
+ */
7158
+ draftPolicy?: {
7159
+ /** Minimum whitespace-separated words the draft must carry. */minWords?: number; /** Literal markers the draft text must contain. */
7160
+ requireSections?: string[];
7161
+ };
7162
+ /**
7129
7163
  * The unified output contract this validator set enforces (the v1.71
7130
7164
  * experiment review, P0.1/P0.2). Construction then runs the golden
7131
7165
  * self test with the contract's fixtures as defaults, the contract's
@@ -7138,8 +7172,17 @@ interface FinishValidationSpec {
7138
7172
  * hash and the validator names. A resumed segment whose live
7139
7173
  * contract hash differs appends a SUPERSEDING descriptor instead of
7140
7174
  * failing, because fixing a stale validator and resuming is the
7141
- * intended remedy, never a fault. Absent = byte identical pre 1.72
7142
- * 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.
7143
7186
  */
7144
7187
  contract?: FinishContract;
7145
7188
  /**
@@ -7286,6 +7329,31 @@ interface OrchestrateSynthesis {
7286
7329
  * { maxTurns: 2 }. Ignored in 'single' mode.
7287
7330
  */
7288
7331
  noteLimits?: UsageLimits;
7332
+ /**
7333
+ * Give the 'single' synthesis invocation the RV-201 evidence tools
7334
+ * `get_child_result` and `read_child_artifact` beside `finish` (the
7335
+ * v1.74 experiment review, P0.2): the finish validators hold the
7336
+ * result against the FULL child outputs while the synthesis model
7337
+ * sees 400 char digests, so when the coordination draft collapses the
7338
+ * evidence the validators demand is model-invisible. With the tools
7339
+ * exposed the digest rows in the synthesis prompt carry each child's
7340
+ * `handle`, and the model pages any settled child's full output or
7341
+ * artifacts before finishing. Off by default: the synthesis toolset
7342
+ * and prompt stay byte identical, exactly like the coordination
7343
+ * `exposeChildResultTools`.
7344
+ */
7345
+ exposeChildResultTools?: boolean;
7346
+ /**
7347
+ * What the 'single' synthesis prompt embeds beside the draft (the
7348
+ * v1.74 experiment review, P0.2). Default 'digests': the 400 char
7349
+ * settled digest rows, byte identical to pre 1.76. 'full' appends a
7350
+ * CHILD OUTPUTS section carrying every settled child's FULL
7351
+ * serialized output after the digest rows: the whole evidence pool
7352
+ * the validators judge against rides the prompt, paid as input
7353
+ * tokens (declare `estCost` or the preflight `estInputTokens`
7354
+ * accordingly).
7355
+ */
7356
+ context?: "digests" | "full";
7289
7357
  }
7290
7358
  /**
7291
7359
  * The deterministic reconciliation envelope an 'incremental' synthesis
@@ -8866,6 +8934,14 @@ interface PreflightOrchestratorSpec {
8866
8934
  model?: ModelSpec;
8867
8935
  limits?: UsageLimits;
8868
8936
  estInputTokens?: number;
8937
+ /**
8938
+ * Mirrors OrchestrateSynthesis.exposeChildResultTools (the v1.74
8939
+ * experiment review, P0.2): declaring it lets the evidence
8940
+ * asymmetry check see that the synthesis model can page the full
8941
+ * child outputs the validators judge against.
8942
+ */
8943
+ exposeChildResultTools?: boolean; /** Mirrors OrchestrateSynthesis.context; default 'digests'. */
8944
+ context?: "digests" | "full";
8869
8945
  };
8870
8946
  }
8871
8947
  /** The full input: engine surface, run surface, and the declared wave. */
@@ -8907,6 +8983,13 @@ interface PreflightInput {
8907
8983
  * runtime would actually grant.
8908
8984
  */
8909
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;
8910
8993
  };
8911
8994
  }
8912
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()];
@@ -15879,6 +15891,19 @@ function validateOrchestrateOptions(opts) {
15879
15891
  }
15880
15892
  if (fv.maxRepairs !== void 0) requireNonNegativeInteger(fv.maxRepairs, "orchestrate finishValidation.maxRepairs");
15881
15893
  if (fv.repairTurnReserve !== void 0) requireNonNegativeInteger(fv.repairTurnReserve, "orchestrate finishValidation.repairTurnReserve");
15894
+ const draftPolicy = fv.draftPolicy;
15895
+ if (draftPolicy !== void 0) {
15896
+ if (typeof draftPolicy !== "object" || draftPolicy === null) throw new ConfigError("orchestrate finishValidation.draftPolicy must be an object");
15897
+ if (opts.synthesis === void 0) throw new ConfigError("orchestrate finishValidation.draftPolicy requires synthesis: without a synthesis invocation the validators bind the coordination finish itself and there is no unvalidated draft to gate");
15898
+ const policy = draftPolicy;
15899
+ if (policy.minWords === void 0 && policy.requireSections === void 0) throw new ConfigError("orchestrate finishValidation.draftPolicy must declare minWords, requireSections, or both");
15900
+ if (policy.minWords !== void 0) {
15901
+ if (typeof policy.minWords !== "number" || !Number.isInteger(policy.minWords) || policy.minWords < 1) throw new ConfigError(`orchestrate finishValidation.draftPolicy.minWords must be a positive integer; got ${JSON.stringify(policy.minWords)}`);
15902
+ }
15903
+ if (policy.requireSections !== void 0) {
15904
+ if (!Array.isArray(policy.requireSections) || policy.requireSections.length === 0 || policy.requireSections.some((section) => typeof section !== "string" || section.length === 0)) throw new ConfigError("orchestrate finishValidation.draftPolicy.requireSections must be a non empty array of non empty strings");
15905
+ }
15906
+ }
15882
15907
  const contract = fv.contract;
15883
15908
  if (contract !== void 0) {
15884
15909
  const shape = contract;
@@ -15904,6 +15929,9 @@ function validateOrchestrateOptions(opts) {
15904
15929
  if (synthesis.mode !== void 0 && synthesis.mode !== "single" && synthesis.mode !== "incremental") throw new ConfigError("orchestrate synthesis.mode must be 'single' or 'incremental'; got " + JSON.stringify(synthesis.mode));
15905
15930
  if (synthesis.mode === "incremental" && opts.finishValidation !== void 0) throw new ConfigError("orchestrate synthesis.mode 'incremental' reconciles deterministically and has no model-composed final finish for finishValidation to bind; configure validators with mode 'single', or drop them");
15906
15931
  if (synthesis.dedupeClaims !== void 0 && typeof synthesis.dedupeClaims !== "boolean") throw new ConfigError("orchestrate synthesis.dedupeClaims must be a boolean; got " + typeof synthesis.dedupeClaims);
15932
+ const symmetry = synthesis;
15933
+ if (symmetry.exposeChildResultTools !== void 0 && typeof symmetry.exposeChildResultTools !== "boolean") throw new ConfigError("orchestrate synthesis.exposeChildResultTools must be a boolean; got " + typeof symmetry.exposeChildResultTools);
15934
+ if (symmetry.context !== void 0 && symmetry.context !== "digests" && symmetry.context !== "full") throw new ConfigError("orchestrate synthesis.context must be 'digests' or 'full'; got " + JSON.stringify(symmetry.context));
15907
15935
  if (synthesis.noteLimits !== void 0) validateUsageLimits(synthesis.noteLimits, "orchestrate synthesis.noteLimits");
15908
15936
  if (synthesis.effort !== void 0 && ![
15909
15937
  "low",
@@ -16853,6 +16881,12 @@ function makeOrchestratorWorkflow(goal, opts) {
16853
16881
  const validationSpec = opts?.finishValidation;
16854
16882
  const validationAbort = new AbortController();
16855
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;
16856
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: {
16857
16891
  source: "orchestrator_finish_validation",
16858
16892
  callId: decision.callId,
@@ -16861,6 +16895,22 @@ function makeOrchestratorWorkflow(goal, opts) {
16861
16895
  maxRepairs: decision.maxRepairs
16862
16896
  } });
16863
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
+ };
16864
16914
  const validateFinish = async (call) => {
16865
16915
  if (validationSpec === void 0) return { ok: true };
16866
16916
  const maxRepairs = validationSpec.maxRepairs ?? 1;
@@ -16899,14 +16949,15 @@ function makeOrchestratorWorkflow(goal, opts) {
16899
16949
  reasons: verdict.reasons
16900
16950
  });
16901
16951
  }
16902
- const repairsUsed = known.filter((candidate) => candidate.verdict !== "accepted").length;
16952
+ const repairsUsed = known.filter((candidate) => candidate.verdict !== "accepted" && contractGenerationCurrent(candidate)).length;
16903
16953
  decision = {
16904
16954
  decisionType: "orchestrator_finish_validation",
16905
16955
  callId: call.id,
16906
16956
  verdict: failed.length === 0 ? "accepted" : repairsUsed < maxRepairs ? "repair" : "rejected",
16907
16957
  failed,
16908
16958
  repairsUsed,
16909
- maxRepairs
16959
+ maxRepairs,
16960
+ ...validationSpec.contract === void 0 ? {} : { contractHash: validationSpec.contract.hash }
16910
16961
  };
16911
16962
  await internals.replayer.appendSinglePhase({
16912
16963
  scope: callingState.scope,
@@ -16920,8 +16971,10 @@ function makeOrchestratorWorkflow(goal, opts) {
16920
16971
  }
16921
16972
  if (decision.verdict === "accepted") return { ok: true };
16922
16973
  if (decision.verdict === "rejected") {
16923
- validationTermination = finishValidationError(decision);
16924
- validationAbort.abort("rulvar:finish-validation");
16974
+ if (contractGenerationCurrent(decision)) {
16975
+ validationTermination = finishValidationError(decision);
16976
+ validationAbort.abort("rulvar:finish-validation");
16977
+ }
16925
16978
  return {
16926
16979
  ok: false,
16927
16980
  feedback: {
@@ -16940,6 +16993,37 @@ function makeOrchestratorWorkflow(goal, opts) {
16940
16993
  };
16941
16994
  };
16942
16995
  /**
16996
+ * The coordination draft gate (the v1.74 experiment review, P0.3):
16997
+ * deterministic library text checks over the draft finish, run only
16998
+ * when the validators bind synthesis. Unlike validateFinish nothing
16999
+ * journals: the checks are pure functions of the exchange, the
17000
+ * rejected exchange is durable in the transcript itself, and a
17001
+ * resumed segment recounts identically. The rejection is the finish
17002
+ * call's error result, so the loop's repair-reserve grants count it
17003
+ * exactly like a host validation rejection.
17004
+ */
17005
+ const validateDraft = (call) => {
17006
+ const policy = validationSpec?.draftPolicy;
17007
+ if (policy === void 0) return Promise.resolve({ ok: true });
17008
+ const result = call.result ?? null;
17009
+ const text = typeof result === "string" ? result : JSON.stringify(result);
17010
+ const reasons = [];
17011
+ if (policy.minWords !== void 0) {
17012
+ const trimmed = text.trim();
17013
+ const words = trimmed === "" ? 0 : trimmed.split(/\s+/).length;
17014
+ if (words < policy.minWords) reasons.push(`the draft carries ${String(words)} words, below the required ${String(policy.minWords)}`);
17015
+ }
17016
+ for (const section of policy.requireSections ?? []) if (!text.includes(section)) reasons.push(`required draft section '${section}' is missing`);
17017
+ if (reasons.length === 0) return Promise.resolve({ ok: true });
17018
+ return Promise.resolve({
17019
+ ok: false,
17020
+ feedback: {
17021
+ error: "the coordination draft failed the draft policy; repair the draft and call finish again: the synthesis invocation composes the FINAL result from this draft, and a collapsed draft starves it of the evidence the validators demand",
17022
+ reasons
17023
+ }
17024
+ });
17025
+ };
17026
+ /**
16943
17027
  * The frozen bundle descriptor (the v1.71 experiment review,
16944
17028
  * P0.2): with a contract configured, the run durably records WHAT
16945
17029
  * validates it. One decision entry per distinct contract hash, in
@@ -16984,7 +17068,10 @@ function makeOrchestratorWorkflow(goal, opts) {
16984
17068
  },
16985
17069
  [kTerminalTool]: {
16986
17070
  name: FINISH_TOOL_NAME,
16987
- ...validationSpec === void 0 || opts?.synthesis !== void 0 ? {} : {
17071
+ ...validationSpec === void 0 || opts?.synthesis !== void 0 ? validationSpec?.draftPolicy !== void 0 && opts?.synthesis !== void 0 ? {
17072
+ validate: validateDraft,
17073
+ ...validationSpec.repairTurnReserve === void 0 ? {} : { repairTurnReserve: validationSpec.repairTurnReserve }
17074
+ } : {} : {
16988
17075
  validate: validateFinish,
16989
17076
  ...validationSpec.repairTurnReserve === void 0 ? {} : { repairTurnReserve: validationSpec.repairTurnReserve }
16990
17077
  }
@@ -17229,8 +17316,15 @@ function makeOrchestratorWorkflow(goal, opts) {
17229
17316
  if (spec === void 0) return draft;
17230
17317
  await recoveryDone;
17231
17318
  if (spec.mode === "incremental") return await reconcileIncremental(draft, spec);
17232
- const finishOnly = buildOrchestratorTools(orchestratorRuntime, fullCardText).filter((tool) => tool.name === FINISH_TOOL_NAME);
17233
- const settledDigests = [...records.values()].filter((record) => record.settled !== void 0).sort((a, b) => a.spawnOrdinal - b.spawnOrdinal).map((record) => digestOf(record, record.settled));
17319
+ const exposeTools = spec.exposeChildResultTools === true;
17320
+ const fullContext = spec.context === "full";
17321
+ const synthesisToolNames = /* @__PURE__ */ new Set([FINISH_TOOL_NAME, ...exposeTools ? [GET_CHILD_RESULT_TOOL_NAME, READ_CHILD_ARTIFACT_TOOL_NAME] : []]);
17322
+ const synthesisTools = buildOrchestratorTools(orchestratorRuntime, fullCardText, { childResultTools: exposeTools }).filter((tool) => synthesisToolNames.has(tool.name));
17323
+ const settledEntries = [...records.entries()].filter((entry) => entry[1].settled !== void 0).sort((a, b) => a[1].spawnOrdinal - b[1].spawnOrdinal);
17324
+ const settledDigests = settledEntries.map(([handle, record]) => ({
17325
+ ...exposeTools ? { handle } : {},
17326
+ ...digestOf(record, record.settled)
17327
+ }));
17234
17328
  let digestRows = settledDigests;
17235
17329
  let repeatedClaims;
17236
17330
  if (spec.dedupeClaims === true) {
@@ -17248,13 +17342,19 @@ function makeOrchestratorWorkflow(goal, opts) {
17248
17342
  const draftJson = JSON.stringify(draft ?? null);
17249
17343
  const digestJson = JSON.stringify(digestRows);
17250
17344
  const prompt = [
17251
- "You are the synthesis invocation of an orchestrated run. Compose the FINAL result of the run from the goal, the coordination draft, and the settled child evidence below by calling finish({ result }) EXACTLY once. Preserve the evidence and citations the draft relies on; do not invent findings. No other tool exists.",
17345
+ "You are the synthesis invocation of an orchestrated run. Compose the FINAL result of the run from the goal, the coordination draft, and the settled child evidence below by calling finish({ result }) EXACTLY once. Preserve the evidence and citations the draft relies on; do not invent findings. " + (exposeTools ? "Beside finish, get_child_result and read_child_artifact page any SETTLED child's FULL output and artifacts by handle (each DIGEST row carries its handle); read what the validators will hold you to before finishing." : "No other tool exists."),
17252
17346
  ...repeatedClaims === void 0 ? [] : ["Repeated claims across children were deduplicated before this prompt: only the first occurrence of each repeated line remains in the digest, and the REPEATED CLAIMS index below lists each one with its reporters."],
17253
17347
  ...spec.instructions === void 0 ? [] : [spec.instructions],
17254
17348
  ...finishValidationPromptLines(validationSpec),
17255
17349
  `GOAL: ${goal}`,
17256
17350
  `DRAFT: ${draftJson}`,
17257
17351
  `DIGEST: ${digestJson}`,
17352
+ ...fullContext ? [`CHILD OUTPUTS: ${JSON.stringify(settledEntries.map(([handle, record]) => ({
17353
+ ...exposeTools ? { handle } : {},
17354
+ nodeId: record.nodeId,
17355
+ status: record.settled.status,
17356
+ text: serializeChildOutput(record.settled)
17357
+ })))}`] : [],
17258
17358
  ...repeatedClaims === void 0 ? [] : [`REPEATED CLAIMS: ${JSON.stringify(repeatedClaims)}`]
17259
17359
  ].join("\n");
17260
17360
  internals.events.emit({
@@ -17280,7 +17380,7 @@ function makeOrchestratorWorkflow(goal, opts) {
17280
17380
  const synthesisOpts = {
17281
17381
  role: "synthesize",
17282
17382
  result: "full",
17283
- tools: finishOnly,
17383
+ tools: synthesisTools,
17284
17384
  limits: spec.limits ?? { maxTurns: 4 },
17285
17385
  ...spec.model === void 0 ? {} : { model: spec.model },
17286
17386
  ...spec.effort === void 0 ? {} : { effort: spec.effort },
@@ -17294,6 +17394,7 @@ function makeOrchestratorWorkflow(goal, opts) {
17294
17394
  }
17295
17395
  };
17296
17396
  const synthesized = await runtime.runInScope(synthesisState, () => ctx.agent(prompt, synthesisOpts));
17397
+ synthesisSchemaRejectedExchanges = synthesized.schemaRejectedTerminalExchanges ?? 0;
17297
17398
  if (validationTermination !== void 0) throw validationTermination;
17298
17399
  if (synthesized.status === "ok") return synthesized.output;
17299
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: {
@@ -17356,7 +17457,10 @@ function makeOrchestratorWorkflow(goal, opts) {
17356
17457
  if (capDecisionRef !== void 0) return await settleCapOutcome();
17357
17458
  if (validationSpec !== void 0) {
17358
17459
  const priorRejection = validationDecisions().find((decision) => decision.verdict === "rejected");
17359
- 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
+ }
17360
17464
  }
17361
17465
  const promptLines = [
17362
17466
  ...extension?.promptLines?.() ?? [],
@@ -17367,28 +17471,28 @@ function makeOrchestratorWorkflow(goal, opts) {
17367
17471
  const liveTermination = extensionTermination;
17368
17472
  if (liveTermination !== void 0) throw liveTermination;
17369
17473
  if (capDecisionRef !== void 0) return await settleCapOutcome();
17370
- if (validationTermination !== void 0) throw validationTermination;
17371
- if (orchestratorAccount !== void 0) internals.cost.orchestrator.spentUsd = internals.budget.accountView(orchestratorAccount)?.spentUsd ?? 0;
17372
- if (result.status !== "ok") throw new ConfigError(`the orchestrator agent terminated with status '${result.status}'` + (result.errorMessage === void 0 ? "" : `: ${result.errorMessage}`));
17373
17474
  /**
17374
- * The synthesis failure enrichment (the v1.71 experiment review,
17375
- * P0.8 remainder + P1.7): every typed failure a synthesis
17376
- * invocation throws gains the verdict-derived repair taxonomy read
17377
- * from the JOURNALED validation decisions (identical live and on
17378
- * replay), and, when an acceptance verdict exists, the acceptance
17379
- * snapshot the children already earned; the completion mirror then
17380
- * lifts completion and childStatusCounts onto the error outcome,
17381
- * exactly like the acceptance rejection path. The experiment's
17382
- * outcome showed completion null beside four accepted children;
17383
- * these are the fields that say "the fan-out work is complete, the
17384
- * failure is downstream". The rejection-past-the-bound error keeps
17385
- * 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.
17386
17489
  */
17387
17490
  const enrichSynthesisFailure = (thrown, snapshot) => {
17388
17491
  if (!(thrown instanceof FailRunError)) throw thrown;
17389
17492
  const base = thrown.data ?? {};
17390
- const spent = validationDecisions().filter((candidate) => candidate.verdict !== "accepted");
17493
+ const spent = validationDecisions().filter((candidate) => candidate.verdict !== "accepted" && contractGenerationCurrent(candidate));
17391
17494
  const rejectedValidators = [...new Set(spent.flatMap((candidate) => candidate.failed.map((f) => f.name)))];
17495
+ const schemaRejected = (result.schemaRejectedTerminalExchanges ?? 0) + synthesisSchemaRejectedExchanges;
17392
17496
  throw new FailRunError(thrown.message, { data: {
17393
17497
  ...base,
17394
17498
  ...snapshot ?? {},
@@ -17396,9 +17500,13 @@ function makeOrchestratorWorkflow(goal, opts) {
17396
17500
  repairsUsed: spent.length,
17397
17501
  maxRepairs: validationSpec?.maxRepairs ?? 1,
17398
17502
  rejectedValidators
17399
- }
17503
+ },
17504
+ ...schemaRejected === 0 || base.schemaRejectedFinishExchanges !== void 0 ? {} : { schemaRejectedFinishExchanges: schemaRejected }
17400
17505
  } });
17401
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}`));
17402
17510
  if (opts?.acceptance === void 0) try {
17403
17511
  return await runSynthesis(result.output);
17404
17512
  } catch (thrown) {
@@ -17482,7 +17590,10 @@ function makeOrchestratorWorkflow(goal, opts) {
17482
17590
  } catch (thrown) {
17483
17591
  enrichSynthesisFailure(thrown, {
17484
17592
  completion: decision.completion,
17485
- 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 }
17486
17597
  });
17487
17598
  }
17488
17599
  return {
@@ -17614,6 +17725,39 @@ function preflightEstimate(input) {
17614
17725
  const say = (finding) => {
17615
17726
  findings.push(finding);
17616
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
+ };
17617
17761
  const adapters = new Map((engine.adapters ?? []).map((adapter) => [adapter.id, adapter]));
17618
17762
  const capsOf = (ref) => {
17619
17763
  const { adapterId, model } = parseModelRef(ref);
@@ -17859,6 +18003,7 @@ function preflightEstimate(input) {
17859
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`,
17860
18004
  spawn: "orchestrator"
17861
18005
  });
18006
+ if (input.orchestrator.synthesis === void 0) contractFeasibilityFindings(outputBound, "orchestrator", servedBy);
17862
18007
  const { adapterId, model } = parseModelRef(servedBy);
17863
18008
  const unit = {
17864
18009
  label: "orchestrator",
@@ -17895,6 +18040,21 @@ function preflightEstimate(input) {
17895
18040
  const synthesis = input.orchestrator.synthesis;
17896
18041
  if (synthesis.limits !== void 0) validateUsageLimits(synthesis.limits, "preflight orchestrator.synthesis.limits");
17897
18042
  if (synthesis.estInputTokens !== void 0) requireNonNegativeInteger(synthesis.estInputTokens, "preflight orchestrator.synthesis.estInputTokens");
18043
+ if (synthesis.context !== void 0 && synthesis.context !== "digests" && synthesis.context !== "full") throw new ConfigError("preflight orchestrator.synthesis.context must be 'digests' or 'full'; got " + JSON.stringify(synthesis.context));
18044
+ {
18045
+ const evidenceNames = /* @__PURE__ */ new Set([
18046
+ "evidence-preserved",
18047
+ "contract-citations",
18048
+ "contract-section-citations"
18049
+ ]);
18050
+ const evidenceValidators = (input.finishValidation?.validators ?? []).map((validator) => validator.name).filter((name) => evidenceNames.has(name));
18051
+ if ((evidenceValidators.length > 0 || input.finishValidation?.contract?.manifest.citations !== void 0) && synthesis.exposeChildResultTools !== true && (synthesis.context ?? "digests") === "digests") say({
18052
+ severity: "warning",
18053
+ code: "synthesis-evidence-asymmetry",
18054
+ message: `the finish validators demand child evidence (${evidenceValidators.length > 0 ? evidenceValidators.join(", ") : "contract citations"}) but the synthesis invocation sees only truncated digest rows and has no child read tools: a collapsed coordination draft starves it of every citation (the v1.74 experiment shape); set synthesis.exposeChildResultTools, synthesis.context 'full', or bind the validators to coordination by dropping synthesis`,
18055
+ spawn: "synthesis"
18056
+ });
18057
+ }
17898
18058
  const servedBy = resolveServing(synthesis.model) ?? resolveServing(defaults.routing?.synthesize);
17899
18059
  if (servedBy === void 0) say({
17900
18060
  severity: "error",
@@ -17912,6 +18072,7 @@ function preflightEstimate(input) {
17912
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`,
17913
18073
  spawn: "synthesis"
17914
18074
  });
18075
+ contractFeasibilityFindings(outputBound, "synthesis", servedBy);
17915
18076
  const projected = projectedProviderTurnsOf(merged, overallExecutedCeiling(merged, toolCeilingsOf(merged))) + finishRepairReserve;
17916
18077
  if (orchestratorEcho !== void 0) orchestratorEcho.synthesis = {
17917
18078
  projectedProviderTurns: projected,
@@ -18136,6 +18297,15 @@ function preflightEstimate(input) {
18136
18297
  names.add(validator.name);
18137
18298
  }
18138
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
+ }
18139
18309
  if (fv.contract !== void 0) {
18140
18310
  for (const contractValidator of fv.contract.validators) if (!names.has(contractValidator.name)) findings.push({
18141
18311
  severity: "error",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rulvar/core",
3
- "version": "1.75.1",
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",