@rulvar/core 1.3.2 → 1.4.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
@@ -697,6 +697,14 @@ type RunMeta = {
697
697
  workflowName?: string; /** Content hash of the body or of the compiled source. */
698
698
  workflowHash?: string; /** TranscriptStore ref of the persisted CompiledWorkflow source. */
699
699
  workflowSourceRef?: string;
700
+ /**
701
+ * The run's immutable USD ceiling (RunOptions.budgetUsd), recorded so
702
+ * resume restores the original invocation's bound. Absent when the
703
+ * run started without a ceiling. Stores must round-trip the field
704
+ * (the conformance kit checks); a store that drops it degrades a
705
+ * resumed run to uncapped.
706
+ */
707
+ budgetUsd?: number;
700
708
  };
701
709
  type RunFilter = {
702
710
  status?: string;
@@ -2867,8 +2875,10 @@ type PermissionHook = (toolName: string, input: unknown, ctx: ToolContext) => Ho
2867
2875
  * position matches every tool WITHOUT declared risk: presets treat the
2868
2876
  * undeclared state conservatively. Argv rules
2869
2877
  * match through the real shell matcher; domain rules are
2870
- * ADVISORY outside the first-party fetch tool: they never
2871
- * change a verdict in M5, and matches surface in audit events.
2878
+ * ADVISORY for every tool in the current release: they never
2879
+ * change a verdict, and matches surface in the tool:end audit
2880
+ * fields (enforcement will live in a first-party fetch tool
2881
+ * when one ships).
2872
2882
  */
2873
2883
  type RiskRuleValue = ToolRisk | "undeclared";
2874
2884
  type PermissionRule = {
@@ -2928,8 +2938,8 @@ type PermissionVerdict = ({
2928
2938
  input: unknown;
2929
2939
  }) & {
2930
2940
  /**
2931
- * Advisory domain-rule matches: reported in audit
2932
- * events, never enforced outside the first-party fetch tool.
2941
+ * Advisory domain-rule matches: reported in the tool:end
2942
+ * audit fields, never enforced in the current release.
2933
2943
  */
2934
2944
  advisory?: PermissionRule[];
2935
2945
  };
@@ -3893,12 +3903,23 @@ type AdaptiveEvents = {
3893
3903
  coversToOrdinal: number;
3894
3904
  renderSize: number;
3895
3905
  } | {
3906
+ /**
3907
+ * Two emitted shapes share the discriminant: the cap-freeze form
3908
+ * carries { atCap: true, spentUsd, capUsd, finalizeReserveUsd },
3909
+ * and the per-wake digest form carries atCap plus the passive
3910
+ * WakeBudgetBlock fields (runSpentUsd .. softWarning).
3911
+ */
3896
3912
  type: "orchestrator:budget";
3897
- entryRef: number;
3898
- spentUsd: number;
3899
- effectiveCapUsd: number;
3900
- reserveUsedUsd: number;
3901
- frozen: boolean;
3913
+ atCap: boolean;
3914
+ spentUsd?: number;
3915
+ capUsd?: number;
3916
+ finalizeReserveUsd?: number;
3917
+ runSpentUsd?: number;
3918
+ runCeilingUsd?: number;
3919
+ orchestratorSpentUsd?: number;
3920
+ orchestratorCapUsd?: number;
3921
+ orchestratorShare?: number;
3922
+ softWarning?: boolean;
3902
3923
  } | {
3903
3924
  type: "escalation:raised";
3904
3925
  entryRef: number;
@@ -3920,7 +3941,12 @@ type AdaptiveEvents = {
3920
3941
  spawnUnitsAfter: number;
3921
3942
  } | {
3922
3943
  type: "spawn:rejected";
3923
- entryRef: number;
3944
+ /**
3945
+ * The journaled admission decision entry; absent for the
3946
+ * pre-admission config gates (orchestrate maxSpawns), which
3947
+ * reject before anything is journaled.
3948
+ */
3949
+ entryRef?: number;
3924
3950
  code: string;
3925
3951
  agentType: string;
3926
3952
  logicalTaskId?: string;
@@ -3971,6 +3997,12 @@ type AdaptiveEvents = {
3971
3997
  frozenValue: Json;
3972
3998
  liveValue: Json;
3973
3999
  } | {
4000
+ /**
4001
+ * Declared for hosts; not emitted today. The compatibility scan
4002
+ * runs strictly before a run's event stream exists, so the
4003
+ * refusal travels only as the typed JournalCompatibilityError
4004
+ * (which carries the same fields).
4005
+ */
3974
4006
  type: "journal:compat";
3975
4007
  code: "HASH_VERSION_TOO_OLD" | "HASH_VERSION_TOO_NEW";
3976
4008
  found: number;
@@ -4773,7 +4805,14 @@ declare class ExternalRegistry {
4773
4805
  private activity;
4774
4806
  private quiesceListener?;
4775
4807
  private quiesceScheduled;
4776
- constructor(replayer: Replayer);
4808
+ private readonly emitEvent?;
4809
+ constructor(replayer: Replayer, emitEvent?: (body: WorkflowEventBody) => void);
4810
+ /**
4811
+ * Live resolution telemetry: applied when the attempt won the
4812
+ * first-closing-wins fold, superseded when it lost. Emitted for live
4813
+ * attempts only; folds of prior entries at resume re-emit nothing.
4814
+ */
4815
+ private emitResolutionOutcome;
4777
4816
  /** Wraps every non-suspension async operation (agents, steps). */
4778
4817
  enter(): () => void;
4779
4818
  /**
@@ -5568,6 +5607,9 @@ declare class InMemoryStore implements JournalStore {
5568
5607
  private readonly runs;
5569
5608
  private readonly metas;
5570
5609
  private warned;
5610
+ constructor(options?: {
5611
+ quiet?: boolean;
5612
+ });
5571
5613
  append(runId: string, e: JournalEntry): Promise<void>;
5572
5614
  load(runId: string): Promise<JournalEntry[]>;
5573
5615
  putMeta(m: RunMeta): Promise<void>;
package/dist/index.js CHANGED
@@ -2583,7 +2583,7 @@ async function resolveToolset(specs, session) {
2583
2583
  if (specs === void 0 || specs.length === 0) return emptyToolset();
2584
2584
  const tools = [];
2585
2585
  for (const spec of specs) {
2586
- if (typeof spec === "string") throw new ConfigError(`tools by registered name ('${spec}') resolve only inside the worker sandbox; name-based tool registries exist for compiled scripts only (https://docs.rulvar.com/guide/planner)`);
2586
+ if (typeof spec === "string") throw new ConfigError(`tools by registered name ('${spec}') are not supported here: pass ToolDef or ToolSource values. Registered toolset names exist only for the dynamic orchestrator's spawn_agent toolsetRef (https://docs.rulvar.com/guide/tools)`);
2587
2587
  if (isToolDef(spec)) {
2588
2588
  tools.push(spec);
2589
2589
  continue;
@@ -5576,8 +5576,31 @@ var ExternalRegistry = class ExternalRegistry {
5576
5576
  activity = 0;
5577
5577
  quiesceListener;
5578
5578
  quiesceScheduled = false;
5579
- constructor(replayer) {
5579
+ emitEvent;
5580
+ constructor(replayer, emitEvent) {
5580
5581
  this.replayer = replayer;
5582
+ this.emitEvent = emitEvent;
5583
+ }
5584
+ /**
5585
+ * Live resolution telemetry: applied when the attempt won the
5586
+ * first-closing-wins fold, superseded when it lost. Emitted for live
5587
+ * attempts only; folds of prior entries at resume re-emit nothing.
5588
+ */
5589
+ emitResolutionOutcome(targetRef, by, outcome) {
5590
+ if (this.emitEvent === void 0) return;
5591
+ if (outcome.applied) this.emitEvent({
5592
+ type: "resolution:applied",
5593
+ targetRef,
5594
+ entryRef: outcome.seq,
5595
+ by
5596
+ });
5597
+ else this.emitEvent({
5598
+ type: "resolution:superseded",
5599
+ targetRef,
5600
+ entryRef: outcome.seq,
5601
+ supersededBy: outcome.supersededBy,
5602
+ reason: outcome.reason
5603
+ });
5581
5604
  }
5582
5605
  /** Wraps every non-suspension async operation (agents, steps). */
5583
5606
  enter() {
@@ -5805,6 +5828,7 @@ var ExternalRegistry = class ExternalRegistry {
5805
5828
  */
5806
5829
  async submitResolution(entryRef, attempt) {
5807
5830
  const outcome = await this.replayer.resolveSuspended(entryRef, attempt);
5831
+ this.emitResolutionOutcome(entryRef, attempt.by, outcome);
5808
5832
  if (outcome.applied) {
5809
5833
  const waiter = this.waiters.get(entryRef);
5810
5834
  if (waiter !== void 0) {
@@ -5838,6 +5862,7 @@ var ExternalRegistry = class ExternalRegistry {
5838
5862
  by: "external",
5839
5863
  value
5840
5864
  });
5865
+ this.emitResolutionOutcome(waiter.entryRef, "external", outcome);
5841
5866
  if (outcome.applied) {
5842
5867
  this.waiters.delete(waiter.entryRef);
5843
5868
  waiter.resolve(value);
@@ -5853,7 +5878,10 @@ function deepCopy(value) {
5853
5878
  var InMemoryStore = class {
5854
5879
  runs = /* @__PURE__ */ new Map();
5855
5880
  metas = /* @__PURE__ */ new Map();
5856
- warned = false;
5881
+ warned;
5882
+ constructor(options) {
5883
+ this.warned = options?.quiet === true;
5884
+ }
5857
5885
  append(runId, e) {
5858
5886
  this.warnOnce();
5859
5887
  const entries = this.runs.get(runId) ?? [];
@@ -5885,7 +5913,7 @@ var InMemoryStore = class {
5885
5913
  warnOnce() {
5886
5914
  if (this.warned) return;
5887
5915
  this.warned = true;
5888
- process.emitWarning("InMemoryStore keeps journals in process memory: nothing survives the process and resume is disabled. Use JsonlFileStore (M2) or @rulvar/store-sqlite (M5) for durable runs.", {
5916
+ process.emitWarning("InMemoryStore keeps journals in process memory: nothing survives a process exit, and a run cannot be resumed from another process. Use JsonlFileStore (M2) or @rulvar/store-sqlite (M5) for durable runs.", {
5889
5917
  code: "RULVAR_INMEMORY_STORE",
5890
5918
  type: "RulvarWarning"
5891
5919
  });
@@ -6518,6 +6546,12 @@ function liftRetainedParts(providerMetadata, adapter) {
6518
6546
  }
6519
6547
  //#endregion
6520
6548
  //#region src/model/retry.ts
6549
+ /**
6550
+ * Captured at module load, before the InProcessRunner's nondeterminism
6551
+ * guard can patch the global: the engine's own jitter is journal
6552
+ * invisible and must never be blamed on workflow code.
6553
+ */
6554
+ const nativeRandom = Math.random;
6521
6555
  /** Appendix A committed defaults (M4 entry gate, PR #26). */
6522
6556
  const DEFAULT_RETRY_POLICY = {
6523
6557
  attempts: 3,
@@ -6553,7 +6587,7 @@ function retryClassOf(error) {
6553
6587
  * equal-jitter: half the backoff is deterministic, half random, so a
6554
6588
  * jittered delay never collapses to zero.
6555
6589
  */
6556
- function retryDelayMs(policy, retryIndex, retryAfterMs, random = Math.random) {
6590
+ function retryDelayMs(policy, retryIndex, retryAfterMs, random = nativeRandom) {
6557
6591
  if (retryAfterMs !== void 0) return retryAfterMs;
6558
6592
  const { initialMs, factor, maxMs, jitter } = policy.backoff;
6559
6593
  const base = Math.min(maxMs, initialMs * factor ** retryIndex);
@@ -7213,7 +7247,7 @@ function ruleMatches(rule, toolName, risk, input) {
7213
7247
  }
7214
7248
  /**
7215
7249
  * Advisory domain-rule matches for the audit payload:
7216
- * reported, never enforced outside first-party fetch.
7250
+ * reported, never enforced in the current release.
7217
7251
  */
7218
7252
  function advisoryMatches(chain, toolName) {
7219
7253
  return [...chain.deny, ...chain.ask].filter((rule) => "domains" in rule && rule.tool === toolName);
@@ -9977,7 +10011,15 @@ function createCtx(internals) {
9977
10011
  if (prior !== void 0) {
9978
10012
  claimed.add(prior.seq);
9979
10013
  const recorded = prior.value;
9980
- if (recorded.reject !== void 0) throw new AdmissionRejectedError(`lineage admission rejected agent spawn (${recorded.reject.code}; recorded verdict)`, { data: { reason: recorded.reject } });
10014
+ if (recorded.reject !== void 0) {
10015
+ internals.events.emit({
10016
+ type: "spawn:rejected",
10017
+ entryRef: prior.seq,
10018
+ code: recorded.reject.code,
10019
+ agentType
10020
+ }, state.spanId, true);
10021
+ throw new AdmissionRejectedError(`lineage admission rejected agent spawn (${recorded.reject.code}; recorded verdict)`, { data: { reason: recorded.reject } });
10022
+ }
9981
10023
  } else {
9982
10024
  const evaluated = admission.evaluateLineage({
9983
10025
  name: agentType,
@@ -10000,7 +10042,7 @@ function createCtx(internals) {
10000
10042
  };
10001
10043
  if (evaluated.decision.kind === "reject") decisionValue.reject = { code: evaluated.decision.reason.code };
10002
10044
  else decisionValue.lineage = evaluated.decision.lineage;
10003
- await internals.replayer.appendSinglePhase({
10045
+ const decisionEntry = await internals.replayer.appendSinglePhase({
10004
10046
  scope: state.scope,
10005
10047
  key: "",
10006
10048
  kind: "decision",
@@ -10008,7 +10050,15 @@ function createCtx(internals) {
10008
10050
  spanId: internals.spans.mint(state.spanId),
10009
10051
  value: decisionValue
10010
10052
  });
10011
- if (evaluated.decision.kind === "reject") throw new AdmissionRejectedError(`lineage admission rejected agent spawn (${evaluated.decision.reason.code})`, { data: { reason: evaluated.decision.reason } });
10053
+ if (evaluated.decision.kind === "reject") {
10054
+ internals.events.emit({
10055
+ type: "spawn:rejected",
10056
+ entryRef: decisionEntry.seq,
10057
+ code: evaluated.decision.reason.code,
10058
+ agentType
10059
+ }, state.spanId);
10060
+ throw new AdmissionRejectedError(`lineage admission rejected agent spawn (${evaluated.decision.reason.code})`, { data: { reason: evaluated.decision.reason } });
10061
+ }
10012
10062
  admission.registerLineageAdmit(evaluated.decision.lineage.logicalTaskId);
10013
10063
  }
10014
10064
  }
@@ -10112,7 +10162,12 @@ function createCtx(internals) {
10112
10162
  data
10113
10163
  }, toolSpanId)
10114
10164
  });
10115
- const chain = compilePermissionChain(internals.defaults.permissions, profile?.permissions);
10165
+ const compiledChain = compilePermissionChain(internals.defaults.permissions, profile?.permissions);
10166
+ const readonlyDeny = { risk: ["write", "destructive"] };
10167
+ const chain = isolation === "readonly" ? {
10168
+ ...compiledChain,
10169
+ deny: [...compiledChain.deny, readonlyDeny]
10170
+ } : compiledChain;
10116
10171
  toolRuntime = {
10117
10172
  defs: toolset.tools,
10118
10173
  contracts: toolset.contracts,
@@ -11278,7 +11333,14 @@ function makeOrchestratorWorkflow(goal, opts) {
11278
11333
  if (recovered !== void 0) return { handle: recovered.handle };
11279
11334
  const recoveredRejection = rejectedByOrdinal.get(spawnOrdinal);
11280
11335
  if (recoveredRejection !== void 0) throw new AdmissionRejectedError(`admission rejected spawn ordinal ${String(spawnOrdinal)} (recovered verdict)`, { data: { decision: recoveredRejection } });
11281
- if (opts?.maxSpawns !== void 0 && spawnOrdinal >= opts.maxSpawns) throw new AdmissionRejectedError(`orchestrate maxSpawns ${String(opts.maxSpawns)} reached`, { data: { reason: { code: "lifetime" } } });
11336
+ if (opts?.maxSpawns !== void 0 && spawnOrdinal >= opts.maxSpawns) {
11337
+ internals.events.emit({
11338
+ type: "spawn:rejected",
11339
+ code: "lifetime",
11340
+ agentType: params.agentType
11341
+ }, callingState.spanId);
11342
+ throw new AdmissionRejectedError(`orchestrate maxSpawns ${String(opts.maxSpawns)} reached`, { data: { reason: { code: "lifetime" } } });
11343
+ }
11282
11344
  const scope = childScopeOf();
11283
11345
  const profile = internals.defaults.profiles?.[params.agentType];
11284
11346
  const profileModel = profile?.model;
@@ -11312,7 +11374,7 @@ function makeOrchestratorWorkflow(goal, opts) {
11312
11374
  spec: params,
11313
11375
  decision
11314
11376
  };
11315
- await internals.replayer.appendSinglePhase({
11377
+ const decisionEntry = await internals.replayer.appendSinglePhase({
11316
11378
  scope: callingState.scope,
11317
11379
  key: "",
11318
11380
  kind: "decision",
@@ -11322,11 +11384,19 @@ function makeOrchestratorWorkflow(goal, opts) {
11322
11384
  });
11323
11385
  if (decision.verdict.kind === "reject") {
11324
11386
  rejectedByOrdinal.set(spawnOrdinal, decision);
11387
+ internals.events.emit({
11388
+ type: "spawn:rejected",
11389
+ entryRef: decisionEntry.seq,
11390
+ code: decision.verdict.reason.code,
11391
+ agentType: params.agentType
11392
+ }, callingState.spanId);
11325
11393
  throw new AdmissionRejectedError(`admission rejected spawn_agent '${params.agentType}' (${decision.verdict.reason.code})`, { data: { reason: decision.verdict.reason } });
11326
11394
  }
11327
11395
  if (decision.verdict.kind !== "admit") throw new ConfigError(`admission verdict '${decision.verdict.kind}' has no producer before M7 (DEF-5)`);
11328
11396
  internals.events.emit({
11329
11397
  type: "spawn:admitted",
11398
+ entryRef: decisionEntry.seq,
11399
+ verdict: decision.verdict.kind,
11330
11400
  agentType: params.agentType,
11331
11401
  logicalTaskId: decision.verdict.lineage.logicalTaskId,
11332
11402
  spawnUnitsAfter: decision.verdict.spawnUnitsAfter
@@ -11740,8 +11810,12 @@ var InProcessRunner = class {
11740
11810
  const priorRandom = Math.random;
11741
11811
  let warnedNow = false;
11742
11812
  let warnedRandom = false;
11813
+ const libraryCaller = () => {
11814
+ const caller = (/* @__PURE__ */ new Error()).stack?.split("\n")[3];
11815
+ return caller !== void 0 && caller.includes("node_modules");
11816
+ };
11743
11817
  Date.now = function rulvarPatchedDateNow() {
11744
- if (!warnedNow) {
11818
+ if (!warnedNow && !libraryCaller()) {
11745
11819
  warnedNow = true;
11746
11820
  process.emitWarning("bare Date.now() called inside a rulvar run; use ctx.now() so the value is journaled and stable on replay", {
11747
11821
  code: "RULVAR_BARE_DATE_NOW",
@@ -11751,7 +11825,7 @@ var InProcessRunner = class {
11751
11825
  return priorNow();
11752
11826
  };
11753
11827
  Math.random = function rulvarPatchedMathRandom() {
11754
- if (!warnedRandom) {
11828
+ if (!warnedRandom && !libraryCaller()) {
11755
11829
  warnedRandom = true;
11756
11830
  process.emitWarning("bare Math.random() called inside a rulvar run; use ctx.random() so the value is journaled and stable on replay", {
11757
11831
  code: "RULVAR_BARE_MATH_RANDOM",
@@ -11829,8 +11903,9 @@ function createEngine(options) {
11829
11903
  });
11830
11904
  const rootSpanId = spans.mint();
11831
11905
  let budgetSeed;
11906
+ const ceilingUsd = opts?.budgetUsd ?? resumeCtx?.budgetUsd;
11832
11907
  const makeBudget = () => new RunBudget({
11833
- ...opts?.budgetUsd === void 0 ? {} : { ceilingUsd: opts.budgetUsd },
11908
+ ...ceilingUsd === void 0 ? {} : { ceilingUsd },
11834
11909
  lifetimeSpawnCap: options.budgetDefaults?.lifetimeSpawnCap ?? 500,
11835
11910
  events: { emit: (body) => bus.emit(body, rootSpanId) },
11836
11911
  priceUsd,
@@ -11889,7 +11964,7 @@ function createEngine(options) {
11889
11964
  ...options.budgetDefaults?.lineage === void 0 ? {} : { limits: options.budgetDefaults.lineage }
11890
11965
  }
11891
11966
  });
11892
- const external = new ExternalRegistry(replayer);
11967
+ const external = new ExternalRegistry(replayer, (body) => bus.emit(body, rootSpanId));
11893
11968
  let transcriptCounter = 0;
11894
11969
  const internals = {
11895
11970
  runId,
@@ -11950,6 +12025,7 @@ function createEngine(options) {
11950
12025
  updatedAt: new Date(realNow()).toISOString(),
11951
12026
  ...opts?.name === void 0 ? {} : { name: opts.name },
11952
12027
  ...opts?.tags === void 0 ? {} : { tags: opts.tags },
12028
+ ...ceilingUsd === void 0 ? {} : { budgetUsd: ceilingUsd },
11953
12029
  workflowName: wf.name,
11954
12030
  workflowHash: compiled === void 0 ? hashWorkflowBody(wf) : hashWorkflowSource(compiled.source),
11955
12031
  ...compiled === void 0 ? {} : { workflowSourceRef: workflowSourceRef(runId) }
@@ -12125,6 +12201,7 @@ function createEngine(options) {
12125
12201
  strict: resumeOptions?.dryRun ?? false,
12126
12202
  invalidate: resumeOptions?.invalidate ?? [],
12127
12203
  ...resumeOptions?.lease === void 0 ? {} : { lease: resumeOptions.lease },
12204
+ ...typeof meta?.budgetUsd === "number" ? { budgetUsd: meta.budgetUsd } : {},
12128
12205
  previewResolve
12129
12206
  });
12130
12207
  })();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rulvar/core",
3
- "version": "1.3.2",
3
+ "version": "1.4.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",