@rulvar/core 1.75.0 → 1.76.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
@@ -7126,6 +7126,30 @@ interface FinishValidationSpec {
7126
7126
  */
7127
7127
  repairTurnReserve?: number;
7128
7128
  /**
7129
+ * The coordination draft gate (the v1.74 experiment review, P0.3),
7130
+ * meaningful ONLY with `synthesis` configured: with validators bound
7131
+ * to the synthesis finish, the coordination finish is an unvalidated
7132
+ * draft, and the experiment's model escaped six failed finish
7133
+ * exchanges with the schema-valid draft 'test', which then starved
7134
+ * synthesis of every citation the validators demanded. The policy
7135
+ * runs deterministic library checks on each coordination finish
7136
+ * (whitespace-token `minWords`, literal `requireSections` markers,
7137
+ * the wordCountValidator and requiredSectionsValidator semantics);
7138
+ * a failing draft returns to the model as the finish call's error
7139
+ * result and the turn continues, exactly like a host validation
7140
+ * rejection, and `repairTurnReserve` grants coordination the same
7141
+ * per-rejected-exchange headroom it grants the synthesis finish.
7142
+ * Pure text checks over the durable exchange: nothing journals, a
7143
+ * resumed segment recounts identically, and `maxRepairs` is not
7144
+ * consumed (it belongs to the synthesis-bound validators). Absent =
7145
+ * byte identical pre 1.76 behavior; configured without `synthesis` =
7146
+ * ConfigError.
7147
+ */
7148
+ draftPolicy?: {
7149
+ /** Minimum whitespace-separated words the draft must carry. */minWords?: number; /** Literal markers the draft text must contain. */
7150
+ requireSections?: string[];
7151
+ };
7152
+ /**
7129
7153
  * The unified output contract this validator set enforces (the v1.71
7130
7154
  * experiment review, P0.1/P0.2). Construction then runs the golden
7131
7155
  * self test with the contract's fixtures as defaults, the contract's
@@ -7286,6 +7310,31 @@ interface OrchestrateSynthesis {
7286
7310
  * { maxTurns: 2 }. Ignored in 'single' mode.
7287
7311
  */
7288
7312
  noteLimits?: UsageLimits;
7313
+ /**
7314
+ * Give the 'single' synthesis invocation the RV-201 evidence tools
7315
+ * `get_child_result` and `read_child_artifact` beside `finish` (the
7316
+ * v1.74 experiment review, P0.2): the finish validators hold the
7317
+ * result against the FULL child outputs while the synthesis model
7318
+ * sees 400 char digests, so when the coordination draft collapses the
7319
+ * evidence the validators demand is model-invisible. With the tools
7320
+ * exposed the digest rows in the synthesis prompt carry each child's
7321
+ * `handle`, and the model pages any settled child's full output or
7322
+ * artifacts before finishing. Off by default: the synthesis toolset
7323
+ * and prompt stay byte identical, exactly like the coordination
7324
+ * `exposeChildResultTools`.
7325
+ */
7326
+ exposeChildResultTools?: boolean;
7327
+ /**
7328
+ * What the 'single' synthesis prompt embeds beside the draft (the
7329
+ * v1.74 experiment review, P0.2). Default 'digests': the 400 char
7330
+ * settled digest rows, byte identical to pre 1.76. 'full' appends a
7331
+ * CHILD OUTPUTS section carrying every settled child's FULL
7332
+ * serialized output after the digest rows: the whole evidence pool
7333
+ * the validators judge against rides the prompt, paid as input
7334
+ * tokens (declare `estCost` or the preflight `estInputTokens`
7335
+ * accordingly).
7336
+ */
7337
+ context?: "digests" | "full";
7289
7338
  }
7290
7339
  /**
7291
7340
  * The deterministic reconciliation envelope an 'incremental' synthesis
@@ -8866,6 +8915,14 @@ interface PreflightOrchestratorSpec {
8866
8915
  model?: ModelSpec;
8867
8916
  limits?: UsageLimits;
8868
8917
  estInputTokens?: number;
8918
+ /**
8919
+ * Mirrors OrchestrateSynthesis.exposeChildResultTools (the v1.74
8920
+ * experiment review, P0.2): declaring it lets the evidence
8921
+ * asymmetry check see that the synthesis model can page the full
8922
+ * child outputs the validators judge against.
8923
+ */
8924
+ exposeChildResultTools?: boolean; /** Mirrors OrchestrateSynthesis.context; default 'digests'. */
8925
+ context?: "digests" | "full";
8869
8926
  };
8870
8927
  }
8871
8928
  /** The full input: engine surface, run surface, and the declared wave. */
package/dist/index.js CHANGED
@@ -10377,6 +10377,22 @@ function recoverUnparsedArgs(raw) {
10377
10377
  };
10378
10378
  }
10379
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
+ /**
10380
10396
  * Executes one model-issued tool call to a tool-result part. Failures are
10381
10397
  * surfaced to the model as error tool results and never thrown past
10382
10398
  * policy: unknown names, argument-validation issues, ModelRetry (bounded
@@ -10409,15 +10425,11 @@ async function executeToolCall(options) {
10409
10425
  if (!validation.valid) {
10410
10426
  const unparsedRaw = unparsedMarkerOf(call.args);
10411
10427
  const recovered = unparsedRaw === void 0 ? void 0 : recoverUnparsedArgs(unparsedRaw);
10412
- if (recovered !== void 0) {
10428
+ if (recovered !== void 0 && unparsedRaw !== void 0) {
10413
10429
  const revalidation = await validateSchemaSpec(def.parameters, recovered.value);
10414
10430
  if (revalidation.valid) {
10415
10431
  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
- });
10432
+ options.events?.emit(unparsedRecoveryLog(call.name, unparsedRaw.length, recovered.pass));
10421
10433
  }
10422
10434
  }
10423
10435
  }
@@ -10838,7 +10850,18 @@ async function runAgent(options) {
10838
10850
  }
10839
10851
  if (options.terminalTool !== void 0 && gatedCall.name === options.terminalTool.name) {
10840
10852
  const terminalDef = runtime.defs.find((candidate) => candidate.name === gatedCall.name);
10841
- const validation = terminalDef === void 0 ? void 0 : await validateSchemaSpec(terminalDef.parameters, gatedCall.args);
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
+ }
10842
10865
  if (validation === void 0 || !validation.valid) {
10843
10866
  events?.emit({
10844
10867
  type: "tool:end",
@@ -15856,6 +15879,19 @@ function validateOrchestrateOptions(opts) {
15856
15879
  }
15857
15880
  if (fv.maxRepairs !== void 0) requireNonNegativeInteger(fv.maxRepairs, "orchestrate finishValidation.maxRepairs");
15858
15881
  if (fv.repairTurnReserve !== void 0) requireNonNegativeInteger(fv.repairTurnReserve, "orchestrate finishValidation.repairTurnReserve");
15882
+ const draftPolicy = fv.draftPolicy;
15883
+ if (draftPolicy !== void 0) {
15884
+ if (typeof draftPolicy !== "object" || draftPolicy === null) throw new ConfigError("orchestrate finishValidation.draftPolicy must be an object");
15885
+ 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");
15886
+ const policy = draftPolicy;
15887
+ if (policy.minWords === void 0 && policy.requireSections === void 0) throw new ConfigError("orchestrate finishValidation.draftPolicy must declare minWords, requireSections, or both");
15888
+ if (policy.minWords !== void 0) {
15889
+ 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)}`);
15890
+ }
15891
+ if (policy.requireSections !== void 0) {
15892
+ 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");
15893
+ }
15894
+ }
15859
15895
  const contract = fv.contract;
15860
15896
  if (contract !== void 0) {
15861
15897
  const shape = contract;
@@ -15881,6 +15917,9 @@ function validateOrchestrateOptions(opts) {
15881
15917
  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));
15882
15918
  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");
15883
15919
  if (synthesis.dedupeClaims !== void 0 && typeof synthesis.dedupeClaims !== "boolean") throw new ConfigError("orchestrate synthesis.dedupeClaims must be a boolean; got " + typeof synthesis.dedupeClaims);
15920
+ const symmetry = synthesis;
15921
+ if (symmetry.exposeChildResultTools !== void 0 && typeof symmetry.exposeChildResultTools !== "boolean") throw new ConfigError("orchestrate synthesis.exposeChildResultTools must be a boolean; got " + typeof symmetry.exposeChildResultTools);
15922
+ 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));
15884
15923
  if (synthesis.noteLimits !== void 0) validateUsageLimits(synthesis.noteLimits, "orchestrate synthesis.noteLimits");
15885
15924
  if (synthesis.effort !== void 0 && ![
15886
15925
  "low",
@@ -16917,6 +16956,37 @@ function makeOrchestratorWorkflow(goal, opts) {
16917
16956
  };
16918
16957
  };
16919
16958
  /**
16959
+ * The coordination draft gate (the v1.74 experiment review, P0.3):
16960
+ * deterministic library text checks over the draft finish, run only
16961
+ * when the validators bind synthesis. Unlike validateFinish nothing
16962
+ * journals: the checks are pure functions of the exchange, the
16963
+ * rejected exchange is durable in the transcript itself, and a
16964
+ * resumed segment recounts identically. The rejection is the finish
16965
+ * call's error result, so the loop's repair-reserve grants count it
16966
+ * exactly like a host validation rejection.
16967
+ */
16968
+ const validateDraft = (call) => {
16969
+ const policy = validationSpec?.draftPolicy;
16970
+ if (policy === void 0) return Promise.resolve({ ok: true });
16971
+ const result = call.result ?? null;
16972
+ const text = typeof result === "string" ? result : JSON.stringify(result);
16973
+ const reasons = [];
16974
+ if (policy.minWords !== void 0) {
16975
+ const trimmed = text.trim();
16976
+ const words = trimmed === "" ? 0 : trimmed.split(/\s+/).length;
16977
+ if (words < policy.minWords) reasons.push(`the draft carries ${String(words)} words, below the required ${String(policy.minWords)}`);
16978
+ }
16979
+ for (const section of policy.requireSections ?? []) if (!text.includes(section)) reasons.push(`required draft section '${section}' is missing`);
16980
+ if (reasons.length === 0) return Promise.resolve({ ok: true });
16981
+ return Promise.resolve({
16982
+ ok: false,
16983
+ feedback: {
16984
+ 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",
16985
+ reasons
16986
+ }
16987
+ });
16988
+ };
16989
+ /**
16920
16990
  * The frozen bundle descriptor (the v1.71 experiment review,
16921
16991
  * P0.2): with a contract configured, the run durably records WHAT
16922
16992
  * validates it. One decision entry per distinct contract hash, in
@@ -16961,7 +17031,10 @@ function makeOrchestratorWorkflow(goal, opts) {
16961
17031
  },
16962
17032
  [kTerminalTool]: {
16963
17033
  name: FINISH_TOOL_NAME,
16964
- ...validationSpec === void 0 || opts?.synthesis !== void 0 ? {} : {
17034
+ ...validationSpec === void 0 || opts?.synthesis !== void 0 ? validationSpec?.draftPolicy !== void 0 && opts?.synthesis !== void 0 ? {
17035
+ validate: validateDraft,
17036
+ ...validationSpec.repairTurnReserve === void 0 ? {} : { repairTurnReserve: validationSpec.repairTurnReserve }
17037
+ } : {} : {
16965
17038
  validate: validateFinish,
16966
17039
  ...validationSpec.repairTurnReserve === void 0 ? {} : { repairTurnReserve: validationSpec.repairTurnReserve }
16967
17040
  }
@@ -17206,8 +17279,15 @@ function makeOrchestratorWorkflow(goal, opts) {
17206
17279
  if (spec === void 0) return draft;
17207
17280
  await recoveryDone;
17208
17281
  if (spec.mode === "incremental") return await reconcileIncremental(draft, spec);
17209
- const finishOnly = buildOrchestratorTools(orchestratorRuntime, fullCardText).filter((tool) => tool.name === FINISH_TOOL_NAME);
17210
- const settledDigests = [...records.values()].filter((record) => record.settled !== void 0).sort((a, b) => a.spawnOrdinal - b.spawnOrdinal).map((record) => digestOf(record, record.settled));
17282
+ const exposeTools = spec.exposeChildResultTools === true;
17283
+ const fullContext = spec.context === "full";
17284
+ const synthesisToolNames = /* @__PURE__ */ new Set([FINISH_TOOL_NAME, ...exposeTools ? [GET_CHILD_RESULT_TOOL_NAME, READ_CHILD_ARTIFACT_TOOL_NAME] : []]);
17285
+ const synthesisTools = buildOrchestratorTools(orchestratorRuntime, fullCardText, { childResultTools: exposeTools }).filter((tool) => synthesisToolNames.has(tool.name));
17286
+ const settledEntries = [...records.entries()].filter((entry) => entry[1].settled !== void 0).sort((a, b) => a[1].spawnOrdinal - b[1].spawnOrdinal);
17287
+ const settledDigests = settledEntries.map(([handle, record]) => ({
17288
+ ...exposeTools ? { handle } : {},
17289
+ ...digestOf(record, record.settled)
17290
+ }));
17211
17291
  let digestRows = settledDigests;
17212
17292
  let repeatedClaims;
17213
17293
  if (spec.dedupeClaims === true) {
@@ -17225,13 +17305,19 @@ function makeOrchestratorWorkflow(goal, opts) {
17225
17305
  const draftJson = JSON.stringify(draft ?? null);
17226
17306
  const digestJson = JSON.stringify(digestRows);
17227
17307
  const prompt = [
17228
- "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.",
17308
+ "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."),
17229
17309
  ...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."],
17230
17310
  ...spec.instructions === void 0 ? [] : [spec.instructions],
17231
17311
  ...finishValidationPromptLines(validationSpec),
17232
17312
  `GOAL: ${goal}`,
17233
17313
  `DRAFT: ${draftJson}`,
17234
17314
  `DIGEST: ${digestJson}`,
17315
+ ...fullContext ? [`CHILD OUTPUTS: ${JSON.stringify(settledEntries.map(([handle, record]) => ({
17316
+ ...exposeTools ? { handle } : {},
17317
+ nodeId: record.nodeId,
17318
+ status: record.settled.status,
17319
+ text: serializeChildOutput(record.settled)
17320
+ })))}`] : [],
17235
17321
  ...repeatedClaims === void 0 ? [] : [`REPEATED CLAIMS: ${JSON.stringify(repeatedClaims)}`]
17236
17322
  ].join("\n");
17237
17323
  internals.events.emit({
@@ -17257,7 +17343,7 @@ function makeOrchestratorWorkflow(goal, opts) {
17257
17343
  const synthesisOpts = {
17258
17344
  role: "synthesize",
17259
17345
  result: "full",
17260
- tools: finishOnly,
17346
+ tools: synthesisTools,
17261
17347
  limits: spec.limits ?? { maxTurns: 4 },
17262
17348
  ...spec.model === void 0 ? {} : { model: spec.model },
17263
17349
  ...spec.effort === void 0 ? {} : { effort: spec.effort },
@@ -17872,6 +17958,21 @@ function preflightEstimate(input) {
17872
17958
  const synthesis = input.orchestrator.synthesis;
17873
17959
  if (synthesis.limits !== void 0) validateUsageLimits(synthesis.limits, "preflight orchestrator.synthesis.limits");
17874
17960
  if (synthesis.estInputTokens !== void 0) requireNonNegativeInteger(synthesis.estInputTokens, "preflight orchestrator.synthesis.estInputTokens");
17961
+ 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));
17962
+ {
17963
+ const evidenceNames = /* @__PURE__ */ new Set([
17964
+ "evidence-preserved",
17965
+ "contract-citations",
17966
+ "contract-section-citations"
17967
+ ]);
17968
+ const evidenceValidators = (input.finishValidation?.validators ?? []).map((validator) => validator.name).filter((name) => evidenceNames.has(name));
17969
+ if ((evidenceValidators.length > 0 || input.finishValidation?.contract?.manifest.citations !== void 0) && synthesis.exposeChildResultTools !== true && (synthesis.context ?? "digests") === "digests") say({
17970
+ severity: "warning",
17971
+ code: "synthesis-evidence-asymmetry",
17972
+ 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`,
17973
+ spawn: "synthesis"
17974
+ });
17975
+ }
17875
17976
  const servedBy = resolveServing(synthesis.model) ?? resolveServing(defaults.routing?.synthesize);
17876
17977
  if (servedBy === void 0) say({
17877
17978
  severity: "error",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rulvar/core",
3
- "version": "1.75.0",
3
+ "version": "1.76.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",