@rulvar/core 1.98.0 → 1.99.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
@@ -4440,6 +4440,17 @@ interface AgentResult<T> {
4440
4440
  * recoveries; absent when zero.
4441
4441
  */
4442
4442
  schemaRecoveredTerminalExchanges?: number;
4443
+ /**
4444
+ * The evidence floor refusal detail (RV507): present ONLY when an
4445
+ * enforced contract refused an otherwise-ok settle. The ctx layer
4446
+ * folds it into the journaled terminal error data and memoizes the
4447
+ * outcome (the refusal is deterministic from the paid transcript, so
4448
+ * a rerun would only re-pay the same bounded failure).
4449
+ */
4450
+ evidenceFloor?: {
4451
+ recordedEntries: number;
4452
+ minEntries: number;
4453
+ };
4443
4454
  }
4444
4455
  /** One 429's provider-normalized limits, per (provider, model). */
4445
4456
  interface RateLimitObservation {
@@ -4667,6 +4678,20 @@ interface RunAgentOptions<S extends SchemaSpec = JsonSchema> {
4667
4678
  save(state: CheckpointState): Promise<void>;
4668
4679
  };
4669
4680
  limits: EffectiveUsageLimits;
4681
+ /**
4682
+ * The resolved evidence contract of the invocation (RV507): under
4683
+ * enforce 'refuse' an ok settle whose message window carries fewer
4684
+ * successful `record_evidence` executions (result `recorded: true`)
4685
+ * than `minEntries` is refused as a typed 'terminal' error carrying
4686
+ * the machine-readable counter and threshold. Window-derived exactly
4687
+ * like the terminal partial, so live and resumed segments count the
4688
+ * same total. Absent, and under 'warn', the loop is byte-identical to
4689
+ * before.
4690
+ */
4691
+ evidenceContract?: {
4692
+ minEntries: number;
4693
+ enforce?: "warn" | "refuse";
4694
+ };
4670
4695
  /** Emits agent:stream deltas when true (telemetry only). */
4671
4696
  stream?: boolean;
4672
4697
  /** Host or sibling cancellation. */
@@ -6736,7 +6761,10 @@ declare const DEFAULT_EVIDENCE_MIN_SHARE = .95;
6736
6761
  * {@link DEFAULT_EVIDENCE_MIN_SHARE}, the plan's 95 percent gate,
6737
6762
  * compared as a ceiling on the required count so an exact boundary like
6738
6763
  * 19 of 20 passes) must appear literally in the result text. Zero child
6739
- * citations pass vacuously. With `requireKnown: true` the contract also
6764
+ * citations pass vacuously UNLESS `requireNonEmptyPool: true` (RV507):
6765
+ * for an evidence-critical run the empty pool IS the failure, so that
6766
+ * mode refuses it with an `empty child citation pool` reason instead of
6767
+ * the vacuous pass. With `requireKnown: true` the contract also
6740
6768
  * runs in reverse: every citation in the RESULT must appear in some
6741
6769
  * child's output, so a fabricated but pattern valid citation is
6742
6770
  * rejected instead of silently counting as evidence. Rejection reasons
@@ -6750,6 +6778,7 @@ declare function evidencePreservedValidator(options?: {
6750
6778
  flags?: string;
6751
6779
  minShare?: number;
6752
6780
  requireKnown?: boolean;
6781
+ requireNonEmptyPool?: boolean;
6753
6782
  name?: string;
6754
6783
  }): FinishValidator;
6755
6784
  /**
@@ -7500,6 +7529,19 @@ interface OrchestrateAcceptance {
7500
7529
  */
7501
7530
  acceptPartialChildren?: boolean;
7502
7531
  /**
7532
+ * The spawned-roster floor (RV507): finish is rejected when FEWER
7533
+ * than this many children were spawned, under BOTH child policies.
7534
+ * 'all-ok' alone treats zero spawned children as vacuously complete
7535
+ * (spawn nothing you do not need to succeed), which lets a
7536
+ * fan-out-shaped task settle ok without ever fanning out; the floor
7537
+ * makes the intended decomposition binding. The journaled decision
7538
+ * (and a rejection's error data) carries the actual
7539
+ * `spawnedChildren` beside the configured floor, so a resume rolls
7540
+ * the same verdict forward. Positive integer; policy only, never
7541
+ * part of any identity.
7542
+ */
7543
+ minSpawnedChildren?: number;
7544
+ /**
7503
7545
  * The terminal-output salvage switch (the 1.64.0 experiment review,
7504
7546
  * P0.4 + P1.1; default false). When true, a child that settled
7505
7547
  * 'limit' CARRYING a terminal output counts as a successful child for
@@ -8129,21 +8171,26 @@ interface AgentProfile {
8129
8171
  estCost?: number;
8130
8172
  /**
8131
8173
  * The declared evidence contract of the profile's task (RV303, the
8132
- * seventh comparison experiment): how many evidence entries the
8133
- * spawned agent MUST record, and the declared call estimates behind
8134
- * them. Purely declarative, like estCost: the runtime never enforces
8135
- * it; {@link preflightEstimate} compares the resulting call floor
8174
+ * seventh comparison experiment; runtime enforcement RV507): how many
8175
+ * evidence entries the spawned agent MUST record, and the declared
8176
+ * call estimates behind them. Under the default `enforce: 'warn'` it
8177
+ * is purely declarative, like estCost: {@link preflightEstimate}
8178
+ * compares the resulting call floor
8136
8179
  * (`minEntries * estCallsPerEntry + overheadCalls`, defaults 3 and 8)
8137
8180
  * against the spawn's effective executed-call ceiling and warns
8138
8181
  * `tool-cap-below-evidence-floor` when the cap cannot fit the
8139
- * contract. The experiment shape: 14 mandatory entries against an
8140
- * 84-call cap that two workers exhausted at 10 recorded entries.
8182
+ * contract. Under `enforce: 'refuse'` the floor additionally binds at
8183
+ * the terminal: an ok settle with fewer successful `record_evidence`
8184
+ * executions than `minEntries` becomes a typed error terminal. The
8185
+ * experiment shape: 14 mandatory entries against an 84-call cap that
8186
+ * two workers exhausted at 10 recorded entries.
8141
8187
  */
8142
8188
  evidenceContract?: EvidenceContract;
8143
8189
  }
8144
8190
  /**
8145
- * A declared evidence floor for preflight to judge tool caps against
8146
- * (RV303). Declarative only; see {@link AgentProfile.evidenceContract}.
8191
+ * A declared evidence floor (RV303): preflight judges tool caps
8192
+ * against it, and under `enforce: 'refuse'` the runtime refuses an ok
8193
+ * settle below it (RV507); see {@link AgentProfile.evidenceContract}.
8147
8194
  */
8148
8195
  interface EvidenceContract {
8149
8196
  /** Evidence entries the task must record; positive integer. */
@@ -8152,6 +8199,19 @@ interface EvidenceContract {
8152
8199
  estCallsPerEntry?: number;
8153
8200
  /** Estimated non-evidence overhead calls; default 8. */
8154
8201
  overheadCalls?: number;
8202
+ /**
8203
+ * What the floor does at the child's terminal settle (RV507). The
8204
+ * default 'warn' keeps the historical behavior: the contract is a
8205
+ * preflight signal only. 'refuse' turns an ok finish whose message
8206
+ * window carries fewer successful `record_evidence` executions
8207
+ * (result `recorded: true`; duplicates and verification errors never
8208
+ * count) than `minEntries` into a typed error terminal (kind
8209
+ * 'terminal') whose journaled error data carries the machine-readable
8210
+ * `evidenceFloor: { recordedEntries, minEntries }`; the outcome is
8211
+ * memoized, so a resume rolls the refusal forward instead of
8212
+ * re-paying the invocation. Non-ok terminals are never re-judged.
8213
+ */
8214
+ enforce?: "warn" | "refuse";
8155
8215
  }
8156
8216
  /**
8157
8217
  * Per-spawn options. The
package/dist/index.js CHANGED
@@ -2781,17 +2781,19 @@ function requireTimerDelayMs(value, site) {
2781
2781
  if (typeof value !== "number" || !Number.isInteger(value) || value < 1 || value > 2147483647) refuse(site, "an integer between 1 and 2147483647 ms (the Node timer maximum)", value);
2782
2782
  }
2783
2783
  /**
2784
- * A declared evidence contract (RV303): minEntries and
2785
- * estCallsPerEntry positive integers, overheadCalls a nonnegative
2786
- * integer. Shared by the profile intake and the preflight spawn spec so
2787
- * both boundaries refuse the same shapes with the same wording.
2784
+ * A declared evidence contract (RV303, enforcement RV507): minEntries
2785
+ * and estCallsPerEntry positive integers, overheadCalls a nonnegative
2786
+ * integer, enforce one of 'warn' | 'refuse'. Shared by the profile
2787
+ * intake and the preflight spawn spec so both boundaries refuse the
2788
+ * same shapes with the same wording.
2788
2789
  */
2789
2790
  function validateEvidenceContract(value, site) {
2790
- if (typeof value !== "object" || value === null || Array.isArray(value)) throw new ConfigError(`${site} must be { minEntries, estCallsPerEntry?, overheadCalls? }; got ${typeof value}`);
2791
- const { minEntries, estCallsPerEntry, overheadCalls } = value;
2791
+ if (typeof value !== "object" || value === null || Array.isArray(value)) throw new ConfigError(`${site} must be { minEntries, estCallsPerEntry?, overheadCalls?, enforce? }; got ${typeof value}`);
2792
+ const { minEntries, estCallsPerEntry, overheadCalls, enforce } = value;
2792
2793
  requirePositiveInteger(minEntries, `${site}.minEntries`);
2793
2794
  if (estCallsPerEntry !== void 0) requirePositiveInteger(estCallsPerEntry, `${site}.estCallsPerEntry`);
2794
2795
  if (overheadCalls !== void 0) requireNonNegativeInteger(overheadCalls, `${site}.overheadCalls`);
2796
+ if (enforce !== void 0 && enforce !== "warn" && enforce !== "refuse") throw new ConfigError(`${site}.enforce must be 'warn' or 'refuse'; got ${JSON.stringify(enforce)}`);
2795
2797
  }
2796
2798
  //#endregion
2797
2799
  //#region src/knowledge/file-store.ts
@@ -12515,6 +12517,24 @@ async function runAgent(options) {
12515
12517
  }
12516
12518
  endPhase(extractPhase, phaseOutcome(), extractServed);
12517
12519
  }
12520
+ const evidenceFloor = options.evidenceContract;
12521
+ let evidenceRefusal;
12522
+ if (evidenceFloor?.enforce === "refuse" && status === "ok") {
12523
+ const recordedEntries = messages.reduce((count, message) => count + message.parts.filter((part) => part.type === "tool-result" && part.name === "record_evidence" && part.result?.recorded === true).length, 0);
12524
+ if (recordedEntries < evidenceFloor.minEntries) {
12525
+ status = "error";
12526
+ output = null;
12527
+ evidenceRefusal = {
12528
+ recordedEntries,
12529
+ minEntries: evidenceFloor.minEntries
12530
+ };
12531
+ agentError = {
12532
+ kind: "terminal",
12533
+ retryable: false
12534
+ };
12535
+ errorMessage = `evidence contract unmet: ${String(recordedEntries)} of ${String(evidenceFloor.minEntries)} required evidence entries recorded`;
12536
+ }
12537
+ }
12518
12538
  const limitPartial = status === "limit" ? latestProgressReport(messages) : void 0;
12519
12539
  if (limitPartial !== void 0) await saveBoundary();
12520
12540
  let transcriptRef = "";
@@ -12560,6 +12580,7 @@ async function runAgent(options) {
12560
12580
  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);
12561
12581
  if (schemaRejectedTerminalExchanges > 0) result.schemaRejectedTerminalExchanges = schemaRejectedTerminalExchanges;
12562
12582
  if (schemaRecoveredTerminalExchanges > 0) result.schemaRecoveredTerminalExchanges = schemaRecoveredTerminalExchanges;
12583
+ if (evidenceRefusal !== void 0) result.evidenceFloor = evidenceRefusal;
12563
12584
  if (usageApprox) result.usageApprox = true;
12564
12585
  if (transportRetries > 0) result.transportRetries = transportRetries;
12565
12586
  if (rateLimitObservations.size > 0) result.rateLimitObservations = [...rateLimitObservations.values()];
@@ -14754,6 +14775,7 @@ function createCtx(internals, rootWorkflow) {
14754
14775
  if (finalize !== void 0) runAgentOptions.finalize = finalize;
14755
14776
  runAgentOptions.summarize = summarize;
14756
14777
  if (profile?.compaction !== void 0) runAgentOptions.compaction = profile.compaction;
14778
+ if (profile?.evidenceContract !== void 0) runAgentOptions.evidenceContract = profile.evidenceContract;
14757
14779
  if (loopFallbacks.length > 0) runAgentOptions.fallbacks = loopFallbacks;
14758
14780
  if (retryPolicy !== void 0) runAgentOptions.retry = { policy: retryPolicy };
14759
14781
  if (internals.quota !== void 0) {
@@ -14974,6 +14996,18 @@ function createCtx(internals, rootWorkflow) {
14974
14996
  };
14975
14997
  }
14976
14998
  }
14999
+ if (result.evidenceFloor !== void 0 && terminalPatch.error !== void 0) {
15000
+ terminalPatch.memoizeOutcome = true;
15001
+ const priorData = terminalPatch.error.data;
15002
+ const dataRecord = typeof priorData === "object" && priorData !== null && !Array.isArray(priorData) ? priorData : {};
15003
+ terminalPatch.error = {
15004
+ ...terminalPatch.error,
15005
+ data: {
15006
+ ...dataRecord,
15007
+ evidenceFloor: result.evidenceFloor
15008
+ }
15009
+ };
15010
+ }
14977
15011
  if (checkpointWritten) terminalPatch.checkpointRef = ckptRef;
14978
15012
  const terminal = await internals.replayer.appendTerminal(running.seq, terminalPatch);
14979
15013
  internals.events.emit({
@@ -16216,7 +16250,10 @@ function listCitations(values) {
16216
16250
  * {@link DEFAULT_EVIDENCE_MIN_SHARE}, the plan's 95 percent gate,
16217
16251
  * compared as a ceiling on the required count so an exact boundary like
16218
16252
  * 19 of 20 passes) must appear literally in the result text. Zero child
16219
- * citations pass vacuously. With `requireKnown: true` the contract also
16253
+ * citations pass vacuously UNLESS `requireNonEmptyPool: true` (RV507):
16254
+ * for an evidence-critical run the empty pool IS the failure, so that
16255
+ * mode refuses it with an `empty child citation pool` reason instead of
16256
+ * the vacuous pass. With `requireKnown: true` the contract also
16220
16257
  * runs in reverse: every citation in the RESULT must appear in some
16221
16258
  * child's output, so a fabricated but pattern valid citation is
16222
16259
  * rejected instead of silently counting as evidence. Rejection reasons
@@ -16245,6 +16282,7 @@ function evidencePreservedValidator(options) {
16245
16282
  for (const match of child.text.match(new RegExp(pattern, globalFlags)) ?? []) cited.add(match);
16246
16283
  }
16247
16284
  const reasons = [];
16285
+ if (cited.size === 0 && options?.requireNonEmptyPool === true) reasons.push("empty child citation pool: no ok child output contains a citation matching the pattern");
16248
16286
  if (cited.size > 0) {
16249
16287
  const missing = [...cited].filter((citation) => !input.text.includes(citation));
16250
16288
  const preserved = cited.size - missing.length;
@@ -16774,6 +16812,8 @@ function validateOrchestrateOptions(opts) {
16774
16812
  if (acceptPartial !== void 0 && typeof acceptPartial !== "boolean") throw new ConfigError(`orchestrate acceptance.acceptPartialChildren must be a boolean; got ${typeof acceptPartial}`);
16775
16813
  const acceptOutput = opts.acceptance.acceptValidatedTerminalOutputOnLimit;
16776
16814
  if (acceptOutput !== void 0 && typeof acceptOutput !== "boolean") throw new ConfigError("orchestrate acceptance.acceptValidatedTerminalOutputOnLimit must be a boolean; got " + typeof acceptOutput);
16815
+ const minSpawned = opts.acceptance.minSpawnedChildren;
16816
+ if (minSpawned !== void 0) requirePositiveInteger(minSpawned, "orchestrate acceptance.minSpawnedChildren");
16777
16817
  }
16778
16818
  if (opts.finishValidation !== void 0) {
16779
16819
  const fv = opts.finishValidation;
@@ -18530,7 +18570,10 @@ function makeOrchestratorWorkflow(goal, opts) {
18530
18570
  degradedReasons.push(status === "running" ? `child ${record.nodeId} was still running when finish validated` : `child ${record.nodeId} settled '${status}'`);
18531
18571
  }
18532
18572
  const childPolicy = opts.acceptance.childPolicy;
18533
- const accepted = childPolicy === "all-ok" ? hardDegraded === 0 : (childStatusCounts.ok ?? 0) + salvaged.length + salvagedOutput.length >= childPolicy.minSuccessful;
18573
+ const minSpawned = opts.acceptance.minSpawnedChildren;
18574
+ const rosterMet = minSpawned === void 0 || sortedRecords.length >= minSpawned;
18575
+ if (!rosterMet) degradedReasons.push(`only ${String(sortedRecords.length)} children were spawned; the acceptance policy requires at least ${String(minSpawned)} spawned children`);
18576
+ const accepted = rosterMet && (childPolicy === "all-ok" ? hardDegraded === 0 : (childStatusCounts.ok ?? 0) + salvaged.length + salvagedOutput.length >= childPolicy.minSuccessful);
18534
18577
  decision = {
18535
18578
  decisionType: "orchestrator_acceptance",
18536
18579
  verdict: accepted ? "accepted" : "rejected",
@@ -18538,6 +18581,10 @@ function makeOrchestratorWorkflow(goal, opts) {
18538
18581
  childPolicy,
18539
18582
  childStatusCounts,
18540
18583
  degradedReasons,
18584
+ ...minSpawned === void 0 ? {} : {
18585
+ minSpawnedChildren: minSpawned,
18586
+ spawnedChildren: sortedRecords.length
18587
+ },
18541
18588
  ...salvaged.length === 0 ? {} : { salvagedPartialChildren: salvaged },
18542
18589
  ...salvagedOutput.length === 0 ? {} : { salvagedTerminalOutputChildren: salvagedOutput },
18543
18590
  ...accepted || opts.synthesis === void 0 ? {} : { synthesisSkipped: "synthesis_skipped_by_acceptance" }
@@ -18559,13 +18606,17 @@ function makeOrchestratorWorkflow(goal, opts) {
18559
18606
  msg: "orchestrator synthesis skipped",
18560
18607
  data: { reason: decision.synthesisSkipped }
18561
18608
  }, callingState.spanId);
18562
- const required = decision.childPolicy === "all-ok" ? "every child ok" : `at least ${String(decision.childPolicy.minSuccessful)} children ok`;
18609
+ const required = (decision.childPolicy === "all-ok" ? "every child ok" : `at least ${String(decision.childPolicy.minSuccessful)} children ok`) + (decision.minSpawnedChildren === void 0 ? "" : `, with at least ${String(decision.minSpawnedChildren)} spawned children`);
18563
18610
  throw new FailRunError(`the orchestrator acceptance policy rejected the finish: ${String(decision.childStatusCounts.ok ?? 0)} children settled 'ok' but the policy requires ${required}; degraded: ${decision.degradedReasons.join("; ")}`, { data: {
18564
18611
  source: "orchestrator_acceptance",
18565
18612
  completion: "rejected",
18566
18613
  childPolicy: decision.childPolicy,
18567
18614
  childStatusCounts: decision.childStatusCounts,
18568
18615
  degradedReasons: decision.degradedReasons,
18616
+ ...decision.minSpawnedChildren === void 0 ? {} : {
18617
+ minSpawnedChildren: decision.minSpawnedChildren,
18618
+ spawnedChildren: decision.spawnedChildren ?? 0
18619
+ },
18569
18620
  ...decision.salvagedPartialChildren === void 0 ? {} : { salvagedPartialChildren: decision.salvagedPartialChildren },
18570
18621
  ...decision.salvagedTerminalOutputChildren === void 0 ? {} : { salvagedTerminalOutputChildren: decision.salvagedTerminalOutputChildren },
18571
18622
  ...decision.synthesisSkipped === void 0 ? {} : { synthesisSkipped: decision.synthesisSkipped }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rulvar/core",
3
- "version": "1.98.0",
3
+ "version": "1.99.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",