@rulvar/core 1.100.0 → 1.102.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
@@ -4719,26 +4719,47 @@ interface RunAgentOptions<S extends SchemaSpec = JsonSchema> {
4719
4719
  * already promised the raised cap), never re-admitted or re-announced,
4720
4720
  * and a restored window entry keeps the summary's
4721
4721
  * finalizationWindowEntered truthful even when a later grant moved the
4722
- * counts back out of the window. The hooks are fire-and-forget from
4723
- * the loop's view; pressure notices stay events and are never
4724
- * journaled. Absent, the loop is byte-identical to before.
4722
+ * counts back out of the window.
4723
+ *
4724
+ * Both hooks are AWAITED before the thing they authorize becomes
4725
+ * observable (RV601): a grant lifts no expiry and queues no notice
4726
+ * until its decision is durable, and the window regime binds no call
4727
+ * until its entry is. A rejected append therefore leaves the grant
4728
+ * unissued and the entry unrecorded, and the rejection propagates
4729
+ * exactly like a failed boundary checkpoint rather than being
4730
+ * swallowed. Pressure notices stay events and are never journaled.
4731
+ * Absent, the loop is byte-identical to before.
4725
4732
  */
4726
4733
  toolBudgetDurability?: {
4727
4734
  restored?: {
4728
4735
  extensionsGranted: number;
4729
4736
  finalizationWindowEntered: boolean;
4737
+ /**
4738
+ * The effective cap the journaled grant announced (RV602). It
4739
+ * anchors the resumed ceiling, because the live `maxToolCalls`
4740
+ * and `increment` are not part of the dispatch identity and may
4741
+ * legitimately drift between segments: without the anchor the two
4742
+ * recovery paths (pure replay, which reads the journal, and live
4743
+ * resume, which recomputed) disagreed, and a promise already made
4744
+ * to the model could be silently revoked. Validated as a
4745
+ * persistent inlet: a non-integer, or one below the base cap, is
4746
+ * ignored with a warning, leaving the count derivation as the
4747
+ * floor. Grants taken AFTER the restore point still measure the
4748
+ * current increment from this anchor.
4749
+ */
4750
+ cap?: number;
4730
4751
  };
4731
4752
  onExtensionGrant?: (grant: {
4732
4753
  grant: number;
4733
4754
  maxExtensions: number;
4734
4755
  toolCallsUsed: number;
4735
4756
  cap: number;
4736
- }) => void;
4757
+ }) => Promise<void>;
4737
4758
  onWindowEntry?: (entry: {
4738
4759
  remaining: number;
4739
4760
  reserveCalls: number;
4740
4761
  budget: FinalizationWindowBudget;
4741
- }) => void;
4762
+ }) => Promise<void>;
4742
4763
  };
4743
4764
  /** Emits agent:stream deltas when true (telemetry only). */
4744
4765
  stream?: boolean;
@@ -7909,6 +7930,32 @@ interface OrchestrateSynthesis {
7909
7930
  * accordingly).
7910
7931
  */
7911
7932
  context?: "digests" | "full";
7933
+ /**
7934
+ * The conditional synthesis gate (RV510, the ninth comparison
7935
+ * experiment: synthesis returned the byte-identical draft after
7936
+ * 101.3 s and 0.5512 USD, 57.3% of post-fan-in wall time). With
7937
+ * `true`, before the 'single' synthesis span starts the coordination
7938
+ * draft is run through the FULL declared finish contract (the same
7939
+ * `finishValidation.validators` that would bind the synthesis
7940
+ * finish): a draft that passes skips the synthesis invocation
7941
+ * entirely under a journaled 'orchestrator_synthesis_skip' decision
7942
+ * with reason 'synthesis_skipped_by_valid_draft' (the existing skip
7943
+ * vocabulary; the info log and the acceptance envelope carry it), and
7944
+ * a resume rolls the journaled skip forward with zero paid calls. A
7945
+ * draft that fails any validator goes to synthesis exactly as before,
7946
+ * with the repair budget untouched (the gate is a pre-pass, never a
7947
+ * journaled validation verdict). Deterministic by construction: only
7948
+ * the declared contract judges, never a semantic delta heuristic.
7949
+ * Requires `finishValidation` (a ConfigError at intake otherwise:
7950
+ * without a contract there is nothing to judge the draft valid by),
7951
+ * which transitively limits it to mode 'single'. With a configured
7952
+ * `budget.synthesisReserveUsd` the held money is released unconsumed
7953
+ * on the skip and no reserve lifecycle journals: there was no
7954
+ * synthesis invocation to account. Default false: the gate, the
7955
+ * decision entry, and the envelope field are all absent, byte for
7956
+ * byte.
7957
+ */
7958
+ skipWhenDraftValid?: boolean;
7912
7959
  }
7913
7960
  /**
7914
7961
  * The deterministic reconciliation envelope an 'incremental' synthesis
@@ -7943,15 +7990,22 @@ interface IncrementalSynthesisResult {
7943
7990
  * notes were already paid during the run; the skipped step is the free
7944
7991
  * deterministic reconciliation). 'synthesis_skipped_by_budget_cap': the
7945
7992
  * orchestrator budget cap froze the plan, and a capped run settles
7946
- * through the reserved finalizer, never synthesis. The reason is frozen
7947
- * into the journaled decision that caused the skip (the acceptance
7948
- * decision or the budget-cap decision), spread into the typed
7949
- * FailRunError data on the failing paths, and announced by an info
7950
- * 'orchestrator synthesis skipped' log event; it is absent everywhere
7951
- * when synthesis is not configured or actually ran, so existing runs
7952
- * stay byte identical.
7953
- */
7954
- type OrchestrateSynthesisSkipReason = "synthesis_skipped_by_acceptance" | "synthesis_skipped_by_budget_cap";
7993
+ * through the reserved finalizer, never synthesis.
7994
+ * 'synthesis_skipped_by_valid_draft' (RV510): the opt-in
7995
+ * `synthesis.skipWhenDraftValid` gate ran the coordination draft
7996
+ * through the full declared finish contract and every validator
7997
+ * passed, so the synthesis invocation had nothing to add and never
7998
+ * started; unlike the other two reasons the run still settles ok with
7999
+ * the draft as its result. The reason is frozen into the journaled
8000
+ * decision that caused the skip (the acceptance decision, the
8001
+ * budget-cap decision, or the 'orchestrator_synthesis_skip' decision),
8002
+ * spread into the typed FailRunError data on the failing paths and
8003
+ * into the acceptance envelope on the valid-draft path, and announced
8004
+ * by an info 'orchestrator synthesis skipped' log event; it is absent
8005
+ * everywhere when synthesis is not configured or actually ran, so
8006
+ * existing runs stay byte identical.
8007
+ */
8008
+ type OrchestrateSynthesisSkipReason = "synthesis_skipped_by_acceptance" | "synthesis_skipped_by_budget_cap" | "synthesis_skipped_by_valid_draft";
7955
8009
  declare const ORCHESTRATE_WORKFLOW_NAME = "rulvar-orchestrate";
7956
8010
  /**
7957
8011
  * Resolves per-spawn dispatch options against the engine registries
package/dist/index.js CHANGED
@@ -10927,7 +10927,17 @@ async function runAgent(options) {
10927
10927
  let extensionGrants = 0;
10928
10928
  let extensionEvidenceAtLastGrant = 0;
10929
10929
  const pendingExtensionNotices = [];
10930
- const effectiveMaxToolCalls = () => limits.maxToolCalls === void 0 ? void 0 : extension === void 0 ? limits.maxToolCalls : limits.maxToolCalls + extensionGrants * extension.increment;
10930
+ /**
10931
+ * The ceiling the effective cap is measured from (RV602). The base cap
10932
+ * normally, so a fresh loop is byte-identical arithmetic; the journaled
10933
+ * cap of the restored grant when a resume carried one, so drifting live
10934
+ * limits cannot revoke a raise the journal already recorded. Grants
10935
+ * taken after the restore point count from here at the CURRENT
10936
+ * increment, which is the live policy doing what it should.
10937
+ */
10938
+ let capBase = limits.maxToolCalls;
10939
+ let grantsOverCapBase = 0;
10940
+ const effectiveMaxToolCalls = () => capBase === void 0 ? void 0 : extension === void 0 ? capBase : capBase + grantsOverCapBase * extension.increment;
10931
10941
  /** The limiter that ended the loop; rides the RV304 pressure snapshot. */
10932
10942
  let limitLimiter;
10933
10943
  /** True once the finalization reserve summary turn actually ran. */
@@ -10979,7 +10989,7 @@ async function runAgent(options) {
10979
10989
  * join the history.
10980
10990
  */
10981
10991
  const tryToolBudgetGrant = () => {
10982
- if (extension === void 0 || limits.maxToolCalls === void 0) return false;
10992
+ if (extension === void 0 || limits.maxToolCalls === void 0 || capBase === void 0) return false;
10983
10993
  if (extensionGrants >= extension.maxExtensions) return false;
10984
10994
  if (extension.requireNewEvidence !== false) {
10985
10995
  if ((guard?.evidenceCount() ?? 0) <= extensionEvidenceAtLastGrant) return false;
@@ -10989,22 +10999,28 @@ async function runAgent(options) {
10989
10999
  const floor = extension.minHeadroomUsd ?? 0;
10990
11000
  if (floor > 0 ? remaining < floor : remaining <= 0) return false;
10991
11001
  }
10992
- extensionGrants += 1;
10993
- extensionEvidenceAtLastGrant = guard?.evidenceCount() ?? 0;
10994
- const cap = limits.maxToolCalls + extensionGrants * extension.increment;
10995
- options.toolBudgetDurability?.onExtensionGrant?.({
10996
- grant: extensionGrants,
11002
+ const grant = extensionGrants + 1;
11003
+ const cap = capBase + (grantsOverCapBase + 1) * extension.increment;
11004
+ const commit = () => {
11005
+ extensionGrants = grant;
11006
+ grantsOverCapBase += 1;
11007
+ extensionEvidenceAtLastGrant = guard?.evidenceCount() ?? 0;
11008
+ pendingExtensionNotices.push(toolBudgetExtensionNoticeText(grant, extension.maxExtensions, toolCallsUsed, cap));
11009
+ events?.emit({
11010
+ type: "log",
11011
+ level: "info",
11012
+ msg: `tool budget extended (grant ${String(grant)}/${String(extension.maxExtensions)}): maxToolCalls now ${String(cap)}`
11013
+ });
11014
+ return true;
11015
+ };
11016
+ const durable = options.toolBudgetDurability?.onExtensionGrant;
11017
+ if (durable === void 0) return commit();
11018
+ return durable({
11019
+ grant,
10997
11020
  maxExtensions: extension.maxExtensions,
10998
11021
  toolCallsUsed,
10999
11022
  cap
11000
- });
11001
- pendingExtensionNotices.push(toolBudgetExtensionNoticeText(extensionGrants, extension.maxExtensions, toolCallsUsed, cap));
11002
- events?.emit({
11003
- type: "log",
11004
- level: "info",
11005
- msg: `tool budget extended (grant ${String(extensionGrants)}/${String(extension.maxExtensions)}): maxToolCalls now ${String(cap)}`
11006
- });
11007
- return true;
11023
+ }).then(commit);
11008
11024
  };
11009
11025
  const flushExtensionNotices = () => {
11010
11026
  for (const text of pendingExtensionNotices.splice(0)) messages.push({
@@ -11058,19 +11074,26 @@ async function runAgent(options) {
11058
11074
  if (finalizationWindow === void 0 || windowNoticeFired) return;
11059
11075
  const state = windowActive();
11060
11076
  if (state === void 0) return;
11061
- windowEntered = true;
11062
- windowNoticeFired = true;
11063
- options.toolBudgetDurability?.onWindowEntry?.({
11077
+ const commit = () => {
11078
+ windowEntered = true;
11079
+ windowNoticeFired = true;
11080
+ pendingWindowNotices.push(finalizationWindowNoticeText(state.remaining, finalizationWindow.reserveCalls, state.budget));
11081
+ events?.emit({
11082
+ type: "log",
11083
+ level: "info",
11084
+ msg: `finalization window entered: ${String(state.remaining)} of the reserved final ${String(finalizationWindow.reserveCalls)} ${state.budget} remain`
11085
+ });
11086
+ };
11087
+ const durable = options.toolBudgetDurability?.onWindowEntry;
11088
+ if (durable === void 0) {
11089
+ commit();
11090
+ return;
11091
+ }
11092
+ return durable({
11064
11093
  remaining: state.remaining,
11065
11094
  reserveCalls: finalizationWindow.reserveCalls,
11066
11095
  budget: state.budget
11067
- });
11068
- pendingWindowNotices.push(finalizationWindowNoticeText(state.remaining, finalizationWindow.reserveCalls, state.budget));
11069
- events?.emit({
11070
- type: "log",
11071
- level: "info",
11072
- msg: `finalization window entered: ${String(state.remaining)} of the reserved final ${String(finalizationWindow.reserveCalls)} ${state.budget} remain`
11073
- });
11096
+ }).then(commit);
11074
11097
  };
11075
11098
  const flushWindowNotices = () => {
11076
11099
  for (const text of pendingWindowNotices.splice(0)) messages.push({
@@ -11127,6 +11150,7 @@ async function runAgent(options) {
11127
11150
  extensionGrants = Math.min(extension.maxExtensions, Math.ceil((toolCallsUsed - limits.maxToolCalls) / extension.increment));
11128
11151
  extensionEvidenceAtLastGrant = guard?.evidenceCount() ?? 0;
11129
11152
  }
11153
+ const derivedCap = limits.maxToolCalls === void 0 || extension === void 0 ? void 0 : limits.maxToolCalls + extensionGrants * extension.increment;
11130
11154
  const durableRestored = options.toolBudgetDurability?.restored;
11131
11155
  if (extension !== void 0 && durableRestored !== void 0) {
11132
11156
  const journaled = Math.min(extension.maxExtensions, durableRestored.extensionsGranted);
@@ -11135,6 +11159,16 @@ async function runAgent(options) {
11135
11159
  extensionEvidenceAtLastGrant = guard?.evidenceCount() ?? 0;
11136
11160
  }
11137
11161
  }
11162
+ grantsOverCapBase = extensionGrants;
11163
+ const journaledCap = durableRestored?.cap;
11164
+ if (journaledCap !== void 0 && limits.maxToolCalls !== void 0 && derivedCap !== void 0) if (Number.isSafeInteger(journaledCap) && journaledCap >= limits.maxToolCalls) {
11165
+ capBase = Math.max(journaledCap, derivedCap);
11166
+ grantsOverCapBase = 0;
11167
+ } else events?.emit({
11168
+ type: "log",
11169
+ level: "warn",
11170
+ msg: `restored tool budget cap ${String(journaledCap)} is not an integer at or above the base cap ${String(limits.maxToolCalls)}; ignoring it and deriving from the counts`
11171
+ });
11138
11172
  if (windowActive() !== void 0 || finalizationWindow !== void 0 && durableRestored?.finalizationWindowEntered === true) {
11139
11173
  windowEntered = true;
11140
11174
  windowNoticeFired = true;
@@ -11231,7 +11265,10 @@ async function runAgent(options) {
11231
11265
  return cap !== void 0 && toolCallsUsed >= cap ? "maxToolCalls" : guard !== void 0 && guard.unitsExhausted() ? "toolUnits" : void 0;
11232
11266
  };
11233
11267
  let expiredLimiter = expiryOf();
11234
- if (expiredLimiter === "maxToolCalls" && call.name !== options.terminalTool?.name && tryToolBudgetGrant()) expiredLimiter = expiryOf();
11268
+ if (expiredLimiter === "maxToolCalls" && call.name !== options.terminalTool?.name) {
11269
+ const attempt = tryToolBudgetGrant();
11270
+ if (typeof attempt === "boolean" ? attempt : await attempt) expiredLimiter = expiryOf();
11271
+ }
11235
11272
  if (expiredLimiter !== void 0) {
11236
11273
  const tail = calls.slice(index);
11237
11274
  const terminalName = options.terminalTool?.name;
@@ -11419,10 +11456,14 @@ async function runAgent(options) {
11419
11456
  };
11420
11457
  }
11421
11458
  if (finalizationWindow !== void 0) {
11422
- maybeMarkWindowEntry();
11459
+ const entry = maybeMarkWindowEntry();
11460
+ if (entry !== void 0) await entry;
11423
11461
  let windowState = windowActive();
11424
11462
  if (windowState !== void 0 && !windowAllows(gatedCall.name)) {
11425
- if (windowState.budget === "tool calls" && extension !== void 0 && windowState.remaining + extension.increment > finalizationWindow.reserveCalls && tryToolBudgetGrant()) windowState = windowActive();
11463
+ if (windowState.budget === "tool calls" && extension !== void 0 && windowState.remaining + extension.increment > finalizationWindow.reserveCalls) {
11464
+ const attempt = tryToolBudgetGrant();
11465
+ if (typeof attempt === "boolean" ? attempt : await attempt) windowState = windowActive();
11466
+ }
11426
11467
  if (windowState !== void 0 && !windowAllows(gatedCall.name)) {
11427
11468
  events?.emit({
11428
11469
  type: "tool:end",
@@ -11492,7 +11533,8 @@ async function runAgent(options) {
11492
11533
  });
11493
11534
  }
11494
11535
  }
11495
- maybeMarkWindowEntry();
11536
+ const lastEntry = maybeMarkWindowEntry();
11537
+ if (lastEntry !== void 0) await lastEntry;
11496
11538
  return {
11497
11539
  parts,
11498
11540
  limitHit: false
@@ -14847,10 +14889,11 @@ function createCtx(internals, rootWorkflow) {
14847
14889
  runAgentOptions.toolBudgetDurability = {
14848
14890
  ...durableRestored === void 0 ? {} : { restored: {
14849
14891
  extensionsGranted: durableRestored.extensionsGranted,
14850
- finalizationWindowEntered: durableRestored.finalizationWindowEntered
14892
+ finalizationWindowEntered: durableRestored.finalizationWindowEntered,
14893
+ ...durableRestored.cap === void 0 ? {} : { cap: durableRestored.cap }
14851
14894
  } },
14852
- onExtensionGrant: (grant) => {
14853
- internals.replayer.appendSinglePhase({
14895
+ onExtensionGrant: async (grant) => {
14896
+ await internals.replayer.appendSinglePhase({
14854
14897
  scope: state.scope,
14855
14898
  key: "",
14856
14899
  kind: "decision",
@@ -14863,8 +14906,8 @@ function createCtx(internals, rootWorkflow) {
14863
14906
  }
14864
14907
  });
14865
14908
  },
14866
- onWindowEntry: (entry) => {
14867
- internals.replayer.appendSinglePhase({
14909
+ onWindowEntry: async (entry) => {
14910
+ await internals.replayer.appendSinglePhase({
14868
14911
  scope: state.scope,
14869
14912
  key: "",
14870
14913
  kind: "decision",
@@ -16973,6 +17016,11 @@ function validateOrchestrateOptions(opts) {
16973
17016
  if (synthesis.dedupeClaims !== void 0 && typeof synthesis.dedupeClaims !== "boolean") throw new ConfigError("orchestrate synthesis.dedupeClaims must be a boolean; got " + typeof synthesis.dedupeClaims);
16974
17017
  const symmetry = synthesis;
16975
17018
  if (symmetry.exposeChildResultTools !== void 0 && typeof symmetry.exposeChildResultTools !== "boolean") throw new ConfigError("orchestrate synthesis.exposeChildResultTools must be a boolean; got " + typeof symmetry.exposeChildResultTools);
17019
+ const conditional = synthesis;
17020
+ if (conditional.skipWhenDraftValid !== void 0) {
17021
+ if (typeof conditional.skipWhenDraftValid !== "boolean") throw new ConfigError("orchestrate synthesis.skipWhenDraftValid must be a boolean; got " + typeof conditional.skipWhenDraftValid);
17022
+ if (conditional.skipWhenDraftValid && opts.finishValidation === void 0) throw new ConfigError("orchestrate synthesis.skipWhenDraftValid requires finishValidation: without a declared finish contract there is nothing to judge the draft valid by");
17023
+ }
16976
17024
  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));
16977
17025
  if (synthesis.noteLimits !== void 0) validateUsageLimits(synthesis.noteLimits, "orchestrate synthesis.noteLimits");
16978
17026
  if (synthesis.effort !== void 0 && ![
@@ -17980,6 +18028,24 @@ function makeOrchestratorWorkflow(goal, opts) {
17980
18028
  if (decision.contractHash !== void 0) return decision.contractHash === hash;
17981
18029
  return internals.replayer.snapshot().filter((entry) => entry.kind === "decision" && entry.scope === callingState.scope && entry.value?.decisionType === "orchestrator_finish_validation_bundle").length <= 1;
17982
18030
  };
18031
+ /**
18032
+ * The children snapshot (RV-202): spawn order, pure reads of the
18033
+ * records the orchestrator already tracks, so validators can hold
18034
+ * a finish result (or the RV510 draft pre-pass) against the
18035
+ * evidence the children produced. Only the JOURNALED verdict of
18036
+ * validateFinish survives; on replay the snapshot is never rebuilt
18037
+ * because the entry is read by call id there.
18038
+ */
18039
+ const validationChildren = () => {
18040
+ const salvageOutputOn = opts?.acceptance?.acceptValidatedTerminalOutputOnLimit === true;
18041
+ return [...records.values()].sort((a, b) => a.spawnOrdinal - b.spawnOrdinal).map((record) => ({
18042
+ handle: record.handle,
18043
+ nodeId: record.nodeId,
18044
+ status: record.settled?.status ?? "running",
18045
+ text: record.settled === void 0 ? "" : serializeChildOutput(record.settled),
18046
+ ...salvageOutputOn && record.settled?.status === "limit" && record.settled.output !== null && record.settled.output !== void 0 ? { salvageableOutput: true } : {}
18047
+ }));
18048
+ };
17983
18049
  const validateFinish = async (call) => {
17984
18050
  if (validationSpec === void 0) return { ok: true };
17985
18051
  const maxRepairs = validationSpec.maxRepairs ?? 1;
@@ -17987,18 +18053,10 @@ function makeOrchestratorWorkflow(goal, opts) {
17987
18053
  let decision = known.find((candidate) => candidate.callId === call.id);
17988
18054
  if (decision === void 0) {
17989
18055
  const result = call.result ?? null;
17990
- const salvageOutputOn = opts?.acceptance?.acceptValidatedTerminalOutputOnLimit === true;
17991
- const children = [...records.values()].sort((a, b) => a.spawnOrdinal - b.spawnOrdinal).map((record) => ({
17992
- handle: record.handle,
17993
- nodeId: record.nodeId,
17994
- status: record.settled?.status ?? "running",
17995
- text: record.settled === void 0 ? "" : serializeChildOutput(record.settled),
17996
- ...salvageOutputOn && record.settled?.status === "limit" && record.settled.output !== null && record.settled.output !== void 0 ? { salvageableOutput: true } : {}
17997
- }));
17998
18056
  const input = {
17999
18057
  result,
18000
18058
  text: typeof result === "string" ? result : JSON.stringify(result),
18001
- children
18059
+ children: validationChildren()
18002
18060
  };
18003
18061
  const failed = [];
18004
18062
  for (const validator of validationSpec.validators) {
@@ -18392,11 +18450,94 @@ function makeOrchestratorWorkflow(goal, opts) {
18392
18450
  * facts; absent everywhere when no reserve is configured.
18393
18451
  */
18394
18452
  let synthesisReserveLifecycle;
18453
+ /**
18454
+ * Set exactly when the RV510 gate skipped the synthesis span (live
18455
+ * pass and resume roll-forward alike); the acceptance envelope
18456
+ * reports the machine reason through it.
18457
+ */
18458
+ let synthesisSkippedByValidDraft = false;
18395
18459
  const runSynthesis = async (draft) => {
18396
18460
  const spec = opts?.synthesis;
18397
18461
  if (spec === void 0) return draft;
18398
18462
  await recoveryDone;
18399
18463
  if (spec.mode === "incremental") return await reconcileIncremental(draft, spec);
18464
+ if (spec.skipWhenDraftValid === true && validationSpec !== void 0) {
18465
+ const skipKey = "synthesis-draft-valid-skip";
18466
+ const announceSkip = (entryRef) => {
18467
+ internals.events.emit({
18468
+ type: "log",
18469
+ level: "info",
18470
+ msg: "orchestrator synthesis skipped",
18471
+ data: {
18472
+ reason: "synthesis_skipped_by_valid_draft",
18473
+ skipDecisionRef: entryRef
18474
+ }
18475
+ }, callingState.spanId);
18476
+ };
18477
+ const draftValue = draft ?? null;
18478
+ const draftHash = createHash("sha256").update(jcsSerialize(draftValue), "utf8").digest("hex");
18479
+ const validatorNames = validationSpec.validators.map((validator) => validator.name);
18480
+ /**
18481
+ * A journaled skip is the authority only for the generation and
18482
+ * the draft it judged (RV603). The documented remedy for a
18483
+ * broken contract is to fix it and resume, and a verdict that
18484
+ * outlives its contract defeats exactly that: the run would
18485
+ * settle ok carrying output the CURRENT contract rejects.
18486
+ * Bound by three facts, in descending strength: the contract
18487
+ * identity (the same `contractGenerationCurrent` test the
18488
+ * finish-validation decisions already use), the draft the
18489
+ * verdict actually judged, and the validator names that
18490
+ * rendered it. Entries written before this field existed carry
18491
+ * no draftHash and stay reusable, so journals in flight roll
18492
+ * forward byte for byte.
18493
+ */
18494
+ const applies = (value) => contractGenerationCurrent(value) && (value.draftHash === void 0 || value.draftHash === draftHash) && (!Array.isArray(value.validators) || JSON.stringify(value.validators) === JSON.stringify(validatorNames));
18495
+ const prior = internals.replayer.snapshot().filter((entry) => entry.kind === "decision" && entry.scope === callingState.scope && entry.key === skipKey).at(-1);
18496
+ if (prior !== void 0 && applies(prior.value)) {
18497
+ synthesisSkippedByValidDraft = true;
18498
+ announceSkip(prior.seq);
18499
+ return draft;
18500
+ }
18501
+ const input = {
18502
+ result: draftValue,
18503
+ text: typeof draftValue === "string" ? draftValue : JSON.stringify(draftValue),
18504
+ children: validationChildren()
18505
+ };
18506
+ let allPassed = true;
18507
+ for (const validator of validationSpec.validators) {
18508
+ let verdict;
18509
+ try {
18510
+ verdict = validator.validate(input);
18511
+ } catch (thrown) {
18512
+ throw new ConfigError(`finish validator '${validator.name}' threw instead of returning a verdict during the skipWhenDraftValid pre-pass: ` + (thrown instanceof Error ? thrown.message : String(thrown)));
18513
+ }
18514
+ if (!verdict.ok) {
18515
+ allPassed = false;
18516
+ break;
18517
+ }
18518
+ }
18519
+ if (allPassed) {
18520
+ const skipEntry = await internals.replayer.appendSinglePhase({
18521
+ scope: callingState.scope,
18522
+ key: skipKey,
18523
+ kind: "decision",
18524
+ status: "ok",
18525
+ spanId: internals.spans.mint(callingState.spanId),
18526
+ site: "orchestrator-synthesis-skip",
18527
+ value: {
18528
+ decisionType: "orchestrator_synthesis_skip",
18529
+ reason: "synthesis_skipped_by_valid_draft",
18530
+ validators: validatorNames,
18531
+ ...validationSpec.contract === void 0 ? {} : { contractHash: validationSpec.contract.hash },
18532
+ draftHash
18533
+ }
18534
+ });
18535
+ if (orchestratorAccount !== void 0 && (opts?.budget?.synthesisReserveUsd ?? 0) > 0) internals.budget.releaseSynthesisReserve(orchestratorAccount);
18536
+ synthesisSkippedByValidDraft = true;
18537
+ announceSkip(skipEntry.seq);
18538
+ return draft;
18539
+ }
18540
+ }
18400
18541
  const exposeTools = spec.exposeChildResultTools === true;
18401
18542
  const fullContext = spec.context === "full";
18402
18543
  const synthesisToolNames = /* @__PURE__ */ new Set([FINISH_TOOL_NAME, ...exposeTools ? [GET_CHILD_RESULT_TOOL_NAME, READ_CHILD_ARTIFACT_TOOL_NAME] : []]);
@@ -18746,7 +18887,8 @@ function makeOrchestratorWorkflow(goal, opts) {
18746
18887
  ...decision.salvagedPartialChildren === void 0 ? {} : { salvagedPartialChildren: decision.salvagedPartialChildren },
18747
18888
  ...decision.salvagedTerminalOutputChildren === void 0 ? {} : { salvagedTerminalOutputChildren: decision.salvagedTerminalOutputChildren },
18748
18889
  ...envelopeSchemaRecovered === 0 ? {} : { schemaRecoveredFinishExchanges: envelopeSchemaRecovered },
18749
- ...synthesisReserveLifecycle === void 0 ? {} : { synthesisReserve: synthesisReserveLifecycle }
18890
+ ...synthesisReserveLifecycle === void 0 ? {} : { synthesisReserve: synthesisReserveLifecycle },
18891
+ ...synthesisSkippedByValidDraft ? { synthesisSkipped: "synthesis_skipped_by_valid_draft" } : {}
18750
18892
  };
18751
18893
  });
18752
18894
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rulvar/core",
3
- "version": "1.100.0",
3
+ "version": "1.102.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",