@rulvar/core 1.63.0 → 1.65.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.
Files changed (3) hide show
  1. package/dist/index.d.ts +95 -4
  2. package/dist/index.js +2345 -2262
  3. package/package.json +1 -1
package/dist/index.js CHANGED
@@ -12246,6 +12246,20 @@ const DEFAULT_MAX_DEPTH = 1;
12246
12246
  const MAX_DEPTH_CEILING = 4;
12247
12247
  const DEFAULT_MAX_CHILDREN_PER_NODE = 16;
12248
12248
  const DEFAULT_CHILD_BUDGET_FRACTION = .3;
12249
+ /**
12250
+ * The ONE dispatch-projection reserve formula (the 1.63.0 experiment
12251
+ * review, P0.3): the spawn's declared estimate (a spawn tool has no
12252
+ * per-call estCost channel, so the estimate is the agentType profile's)
12253
+ * or the flat default, clamped by the explicit child budget when one
12254
+ * exists. This is the reserve the embedded layer-2 gate evaluates a
12255
+ * spawn_agent call against BEFORE dispatch, and the number
12256
+ * preflightEstimate projects for the same gate, so the linter and the
12257
+ * runtime cannot drift: both call this function.
12258
+ */
12259
+ function dispatchProjectionReserveUsd(spec, flatReserveUsd) {
12260
+ const base = spec.estCostUsd ?? flatReserveUsd;
12261
+ return spec.budgetUsd === void 0 ? base : Math.min(base, spec.budgetUsd);
12262
+ }
12249
12263
  /** Nesting depth of a child scope: its workflow, agent, and plan-node segments. */
12250
12264
  function spawnDepthOf(childScope) {
12251
12265
  return parseScopePath(childScope).filter((segment) => segment.kind === "workflow" || segment.kind === "agent" || segment.kind === "plan-node").length;
@@ -12390,11 +12404,13 @@ var AdmissionController = class {
12390
12404
  * never materializes as an account and must not shrink the
12391
12405
  * projection. The token-count-priced estimate of ctx.agent is
12392
12406
  * unreachable here (async); a divergence there lands as a journaled
12393
- * dispatch rejection instead of a strand.
12407
+ * dispatch rejection instead of a strand. Delegates to the exported
12408
+ * {@link dispatchProjectionReserveUsd} so the live gate and
12409
+ * preflightEstimate share ONE formula (the 1.63.0 experiment review,
12410
+ * P0.3).
12394
12411
  */
12395
12412
  projectedDispatchReserveUsd(spec) {
12396
- const base = spec.estCostUsd ?? this.flatReserveUsd;
12397
- return spec.budgetUsd === void 0 ? base : Math.min(base, spec.budgetUsd);
12413
+ return dispatchProjectionReserveUsd(spec, this.flatReserveUsd);
12398
12414
  }
12399
12415
  admit(spec, options) {
12400
12416
  const commitReserve = options?.commitReserve ?? true;
@@ -12565,1996 +12581,924 @@ var AdmissionController = class {
12565
12581
  }
12566
12582
  };
12567
12583
  //#endregion
12568
- //#region src/engine/scheduler.ts
12584
+ //#region src/model/profile-card.ts
12585
+ function toolNamesOf(profile) {
12586
+ return (profile.tools ?? []).map((entry) => {
12587
+ if (typeof entry === "string") return `${entry} (registered toolset)`;
12588
+ if ("kind" in entry && entry.kind === "tool") return entry.name;
12589
+ return `${entry.id}:* (tool source)`;
12590
+ });
12591
+ }
12569
12592
  /**
12570
- * Scheduler and concurrency (M1-T08): the per-run semaphore with a FIFO
12571
- * queue (default 12 concurrent model calls). The engine lifetime spawn cap
12572
- * is enforced by the budget layer at admission; parallel/pipeline
12573
- * composition semantics live with ctx.
12574
- * Per-provider concurrency keys land with M4.
12593
+ * Renders the registry into the shared agent vocabulary card. Sorted,
12594
+ * deterministic, byte-stable; an empty registry renders explicitly so
12595
+ * the planner never guesses at unregistered agentTypes. When the engine
12596
+ * registers toolsets, their names render as a closing line (v1.17.0
12597
+ * review P1-3): those are the ONLY values valid as string entries of a
12598
+ * tools option, so the planner never invents a registry name.
12575
12599
  */
12576
- /** FIFO semaphore; default per-run width is 12. */
12577
- const DEFAULT_PER_RUN_CONCURRENCY = 12;
12578
- var Semaphore = class {
12579
- limit;
12580
- active = 0;
12581
- waiters = [];
12582
- /**
12583
- * `limit` must be a positive integer: anything else (NaN included) is
12584
- * a typed ConfigError. Before this gate a NaN limit made
12585
- * `active < limit` permanently false, so the first acquire queued
12586
- * forever and the run could not settle, not even through cancel()
12587
- * (v1.34.0 review P2-4). Unlimited is expressed by not constructing a
12588
- * semaphore, never by a sentinel limit.
12589
- */
12590
- constructor(limit) {
12591
- requirePositiveInteger(limit, "Semaphore limit");
12592
- this.limit = limit;
12593
- }
12594
- get pending() {
12595
- return this.waiters.length;
12596
- }
12597
- /**
12598
- * Acquires a slot, resolving in FIFO order. `onQueued` fires only when
12599
- * the caller actually has to wait (feeds the agent:queued event).
12600
- * An aborted `signal` releases the caller from the queue without a
12601
- * slot: the returned release is a no-op, the remaining waiters keep
12602
- * their FIFO positions, and the caller proceeds to observe its own
12603
- * aborted signal (the model layers refuse dispatch under an aborted
12604
- * signal, so no provider call follows). Cancellation can therefore
12605
- * always drain a queued run (v1.34.0 review P2-4).
12606
- */
12607
- async acquire(onQueued, signal) {
12608
- if (this.active < this.limit) {
12609
- this.active += 1;
12610
- return () => this.release();
12611
- }
12612
- if (signal?.aborted === true) return () => void 0;
12613
- onQueued?.();
12614
- const waiter = {
12615
- resolve: () => void 0,
12616
- aborted: false
12617
- };
12618
- const wait = new Promise((resolve) => {
12619
- waiter.resolve = resolve;
12620
- });
12621
- this.waiters.push(waiter);
12622
- let onAbort;
12623
- if (signal !== void 0) {
12624
- onAbort = () => {
12625
- const index = this.waiters.indexOf(waiter);
12626
- if (index === -1) return;
12627
- this.waiters.splice(index, 1);
12628
- waiter.aborted = true;
12629
- waiter.resolve();
12630
- };
12631
- signal.addEventListener("abort", onAbort, { once: true });
12632
- }
12633
- try {
12634
- await wait;
12635
- } finally {
12636
- if (signal !== void 0 && onAbort !== void 0) signal.removeEventListener("abort", onAbort);
12637
- }
12638
- if (waiter.aborted) return () => void 0;
12639
- this.active += 1;
12640
- return () => this.release();
12641
- }
12642
- async withSlot(fn, onQueued, signal) {
12643
- const release = await this.acquire(onQueued, signal);
12644
- try {
12645
- return await fn();
12646
- } finally {
12647
- release();
12648
- }
12600
+ function profileCard(profiles, toolsets) {
12601
+ const toolsetNames = Object.keys(toolsets ?? {}).sort();
12602
+ const toolsetsLine = toolsetNames.length === 0 ? void 0 : `Registered toolsets (valid string entries of a tools option): ${toolsetNames.join(", ")}.`;
12603
+ const names = Object.keys(profiles ?? {}).sort();
12604
+ if (profiles === void 0 || names.length === 0) {
12605
+ const empty = "Agent profiles: none registered. Calls take no agentType.";
12606
+ return toolsetsLine === void 0 ? empty : `${empty}\n${toolsetsLine}`;
12649
12607
  }
12650
- release() {
12651
- this.active -= 1;
12652
- const next = this.waiters.shift();
12653
- if (next !== void 0) next.resolve();
12608
+ const lines = ["Agent profiles (agentType values):"];
12609
+ for (const name of names) {
12610
+ const profile = profiles[name];
12611
+ const description = profile.description ?? "no description";
12612
+ lines.push(`- ${name}: ${description}`);
12613
+ const toolNames = toolNamesOf(profile);
12614
+ if (toolNames.length > 0) lines.push(` tools: ${toolNames.join(", ")}`);
12615
+ if (profile.taskClass !== void 0) lines.push(` taskClass: ${profile.taskClass}`);
12616
+ if (profile.estCost !== void 0) lines.push(` estCost: ${profile.estCost.toFixed(2)} USD`);
12617
+ if (profile.escalation !== void 0) lines.push(` escalation: flavor ${profile.escalation.flavor ?? "A"} (opt-in)`);
12654
12618
  }
12655
- };
12656
- //#endregion
12657
- //#region src/engine/preflight.ts
12658
- const ANY_TOOL = "(any)";
12659
- function resolveServing(spec) {
12660
- if (spec === void 0) return;
12661
- if (typeof spec === "string") return spec;
12662
- if ("model" in spec) return spec.model;
12663
- return spec.ladder.rungs[spec.ladder.startTier]?.model;
12619
+ if (toolsetsLine !== void 0) lines.push(toolsetsLine);
12620
+ return lines.join("\n");
12664
12621
  }
12622
+ //#endregion
12623
+ //#region src/engine/internal.ts
12624
+ /** Registered by createCtx; keyed by the ctx object identity. */
12625
+ const ctxRuntimes = /* @__PURE__ */ new WeakMap();
12665
12626
  /**
12666
- * Per-tool executed-call ceilings from the merged limits: for every
12667
- * tool a per-tool cap or a unit cost names (plus the '(any)' tool that
12668
- * nothing names, unit cost 1), the smallest of maxCallsPerTool[T],
12669
- * floor(toolUnits.max / cost(T)) for a positive cost (a zero cost is
12670
- * free), and maxToolCalls.
12627
+ * Internal AgentOpts channel (M6-T07): agentImpl reports the agent
12628
+ * dispatch seq (the spawn handle) through this symbol-keyed callback on
12629
+ * the running append, on a dangling redispatch, AND on the replay
12630
+ * branch, so the orchestrator learns handles that are stable across
12631
+ * resume. Never part of the public AgentOpts surface.
12671
12632
  */
12672
- function toolCeilingsOf(limits) {
12673
- const names = /* @__PURE__ */ new Set();
12674
- for (const name of Object.keys(limits.maxCallsPerTool ?? {})) names.add(name);
12675
- for (const name of Object.keys(limits.toolUnits?.costs ?? {})) names.add(name);
12676
- const rows = [];
12677
- for (const tool of [...[...names].sort(), ANY_TOOL]) {
12678
- const terms = [];
12679
- const cap = tool === ANY_TOOL ? void 0 : limits.maxCallsPerTool?.[tool];
12680
- if (cap !== void 0) terms.push({
12681
- boundBy: "maxCallsPerTool",
12682
- ceiling: cap
12683
- });
12684
- if (limits.toolUnits !== void 0) {
12685
- const cost = tool === ANY_TOOL ? 1 : limits.toolUnits.costs?.[tool] ?? 1;
12686
- if (cost > 0) terms.push({
12687
- boundBy: "toolUnits",
12688
- ceiling: Math.floor(limits.toolUnits.max / cost)
12689
- });
12690
- }
12691
- if (limits.maxToolCalls !== void 0) terms.push({
12692
- boundBy: "maxToolCalls",
12693
- ceiling: limits.maxToolCalls
12694
- });
12695
- if (terms.length === 0) {
12696
- rows.push({
12697
- tool,
12698
- ceiling: null
12699
- });
12700
- continue;
12701
- }
12702
- const min = terms.reduce((best, term) => term.ceiling < best.ceiling ? term : best);
12703
- rows.push({
12704
- tool,
12705
- ceiling: min.ceiling,
12706
- boundBy: min.boundBy
12707
- });
12708
- }
12709
- return rows;
12633
+ const kOnRunning = Symbol("rulvar.onRunning");
12634
+ /**
12635
+ * Internal AgentOpts channel (M6-T07): names the terminal tool whose
12636
+ * accepted call ends the loop with status ok (the orchestrator finish
12637
+ * tool), plus the optional host validation hook over the accepted call
12638
+ * (the RV-204 finish validators). Never part of the public AgentOpts
12639
+ * surface.
12640
+ */
12641
+ const kTerminalTool = Symbol("rulvar.terminalTool");
12642
+ /**
12643
+ * Internal AgentOpts channel (M7-T08): a transcript checkpoint ref the
12644
+ * fresh dispatch boots from (park/unpark continuation and the DEF-5
12645
+ * graft boot). Dangling redispatch checkpoints take precedence.
12646
+ */
12647
+ const kBootCheckpoint = Symbol("rulvar.bootCheckpoint");
12648
+ /**
12649
+ * Internal AgentOpts channel: marks the orchestrator forced-finish
12650
+ * dispatch, whose spend draws from the released finalize reserve
12651
+ * (DEF-7). Settlement stamps the flag into the terminal's cost
12652
+ * attribution so the journal fold reproduces reserveUsedUsd.
12653
+ */
12654
+ const kFinalizeReserve = Symbol("rulvar.finalizeReserve");
12655
+ /** Typed accessor used by the in-package consumers. */
12656
+ function runtimeOf(ctx) {
12657
+ const runtime = ctxRuntimes.get(ctx);
12658
+ if (runtime === void 0) throw new Error("ctx runtime missing: the ctx value was not created by createCtx (engine run context)");
12659
+ return runtime;
12710
12660
  }
12711
- function validateSpawnSpec(spec, index) {
12712
- const site = `preflight.spawns[${index}]`;
12713
- if (spec.limits !== void 0) validateUsageLimits(spec.limits, `${site}.limits`);
12714
- if (spec.estCost !== void 0) requireNonNegativeNumber(spec.estCost, `${site}.estCost`);
12715
- if (spec.estInputTokens !== void 0) requireNonNegativeInteger(spec.estInputTokens, `${site}.estInputTokens`);
12716
- if (spec.count !== void 0) requirePositiveInteger(spec.count, `${site}.count`);
12661
+ //#endregion
12662
+ //#region src/engine/spawn-events.ts
12663
+ function emitSpawnAdmitted(events, input) {
12664
+ events.emit({
12665
+ type: "spawn:admitted",
12666
+ entryRef: input.entryRef,
12667
+ verdict: input.verdict,
12668
+ agentType: input.agentType,
12669
+ logicalTaskId: input.logicalTaskId,
12670
+ ...input.spawnUnitsAfter === void 0 ? {} : { spawnUnitsAfter: input.spawnUnitsAfter }
12671
+ }, input.spanId, input.replayed);
12717
12672
  }
12673
+ function emitSpawnRejected(events, input) {
12674
+ events.emit({
12675
+ type: "spawn:rejected",
12676
+ ...input.entryRef === void 0 ? {} : { entryRef: input.entryRef },
12677
+ code: input.code,
12678
+ agentType: input.agentType
12679
+ }, input.spanId, input.replayed);
12680
+ }
12681
+ //#endregion
12682
+ //#region src/l0/long-timer.ts
12718
12683
  /**
12719
- * Computes the preflight report: the effective merged limits per
12720
- * declared spawn, the layer-1 admission projection over the declared
12721
- * wave, the per-tool and weighted-unit bottleneck ordering, the
12722
- * concurrency and quota exposure at the declared estimates, and the
12723
- * linter findings. Pure: no engine is constructed, no store is opened,
12724
- * no adapter stream is dispatched, and no journal entry is written.
12684
+ * Sliced timers for absolute wall-clock deadlines (v1.34.0 review P2-2).
12685
+ *
12686
+ * Node clamps a setTimeout delay above 2147483647 ms (about 24.8 days)
12687
+ * to 1 ms, so a naive timer for a far-future deadline fires immediately.
12688
+ * setLongTimeout never hands Node more than one MAX_TIMER_DELAY_MS
12689
+ * slice, re-checks the wall clock when a slice fires, and re-arms until
12690
+ * the clock actually reaches the deadline: firing a slice is never taken
12691
+ * as proof the deadline arrived. A deadline already in the past fires on
12692
+ * the next macrotask (delay 0), matching the plain setTimeout behavior
12693
+ * the callers had for near deadlines.
12725
12694
  */
12726
- function preflightEstimate(input) {
12727
- const engine = input.engine ?? {};
12728
- const defaults = engine.defaults ?? {};
12729
- if (defaults.limits !== void 0) validateUsageLimits(defaults.limits, "preflight.engine.defaults.limits");
12730
- if (input.run?.limits !== void 0) validateUsageLimits(input.run.limits, "preflight.run.limits");
12731
- if (input.orchestrator?.limits !== void 0) validateUsageLimits(input.orchestrator.limits, "preflight.orchestrator.limits");
12732
- const findings = [];
12733
- const say = (finding) => {
12734
- findings.push(finding);
12695
+ /**
12696
+ * Schedules `onDue` for the absolute wall-clock instant `dueAtMs` as
12697
+ * reported by `now` (default Date.now), slicing delays beyond the Node
12698
+ * timer maximum.
12699
+ */
12700
+ function setLongTimeout(onDue, dueAtMs, now = Date.now) {
12701
+ let handle;
12702
+ let cancelled = false;
12703
+ const arm = () => {
12704
+ const remaining = Math.max(0, dueAtMs - now());
12705
+ handle = setTimeout(() => {
12706
+ if (cancelled) return;
12707
+ if (now() >= dueAtMs) {
12708
+ onDue();
12709
+ return;
12710
+ }
12711
+ arm();
12712
+ }, Math.min(remaining, MAX_TIMER_DELAY_MS));
12735
12713
  };
12736
- const adapters = new Map((engine.adapters ?? []).map((adapter) => [adapter.id, adapter]));
12737
- const capsOf = (ref) => {
12738
- const { adapterId, model } = parseModelRef(ref);
12739
- return adapters.get(adapterId)?.caps(model);
12714
+ arm();
12715
+ return { cancel: () => {
12716
+ cancelled = true;
12717
+ if (handle !== void 0) clearTimeout(handle);
12718
+ } };
12719
+ }
12720
+ //#endregion
12721
+ //#region src/runtime/permission-chain.ts
12722
+ /**
12723
+ * The layered permission chain (M3-T03): the single approval surface for
12724
+ * every tool dispatch, regardless of tool origin. The order is fixed and
12725
+ * normative: hooks -> deny rules -> ask rules -> canUseTool -> terminal
12726
+ * default (allow unless needsApproval, then ask). Evaluation is
12727
+ * short-circuit; unconfigured layers are skipped. Rules never yield
12728
+ * allow: allow is only ever falling through to canUseTool or the
12729
+ * terminal default.
12730
+ *
12731
+ * Full contract: https://docs.rulvar.com/guide/tools.
12732
+ * Risk presets, the argv shell matcher, domain rules, and the
12733
+ * audit/dry-run surface land in M5.
12734
+ */
12735
+ /**
12736
+ * Merges the engine-wide config and the profile config into one chain.
12737
+ * Layers concatenate engine-first; since rules only deny or ask, ordering
12738
+ * within a layer cannot change the verdict. The
12739
+ * profile's canUseTool wins over the engine's (a single slot by
12740
+ * construction). A declared preset compiles INTO the same layers, after
12741
+ * the host-authored rules, never as a fifth layer (M5-T05).
12742
+ */
12743
+ function compilePermissionChain(engine, profile) {
12744
+ const preset = profile?.preset === void 0 ? {
12745
+ deny: [],
12746
+ ask: []
12747
+ } : compilePermissionPreset(profile.preset);
12748
+ const deny = [
12749
+ ...engine?.deny ?? [],
12750
+ ...profile?.deny ?? [],
12751
+ ...preset.deny
12752
+ ];
12753
+ const ask = [
12754
+ ...engine?.ask ?? [],
12755
+ ...profile?.ask ?? [],
12756
+ ...preset.ask
12757
+ ];
12758
+ const canUseTool = profile?.canUseTool ?? engine?.canUseTool;
12759
+ return {
12760
+ hooks: [...engine?.hooks ?? [], ...profile?.hooks ?? []],
12761
+ deny,
12762
+ ask,
12763
+ ...canUseTool === void 0 ? {} : { canUseTool }
12740
12764
  };
12741
- const pricingOf = (ref) => resolvePricing(ref, engine.pricing, capsOf(ref)?.pricing);
12742
- const ceilingUsd = input.run?.budgetUsd;
12743
- const flatReserveUsd = engine.budgetDefaults?.flatReserveUsd ?? .5;
12744
- const lifetimeSpawnCap = engine.budgetDefaults?.lifetimeSpawnCap ?? 500;
12745
- const childBudgetFraction = engine.budgetDefaults?.childBudgetFraction ?? .3;
12746
- const maxDepth = engine.budgetDefaults?.maxDepth ?? 1;
12747
- const perRun = engine.concurrency?.perRun ?? 12;
12748
- const runLimits = mergeUsageLimits(void 0, input.run?.limits, defaults.limits);
12749
- let orchestratorEcho;
12750
- let reservedForFinalizationUsd = 0;
12751
- let effectiveCapUsd;
12752
- if (input.orchestrator !== void 0) {
12753
- const spec = input.orchestrator.budget;
12754
- const fraction = spec?.capFraction ?? .2;
12755
- const fromFraction = ceilingUsd === void 0 ? void 0 : fraction * ceilingUsd;
12756
- const bounds = [spec?.capUsd, fromFraction].filter((bound) => bound !== void 0);
12757
- effectiveCapUsd = bounds.length === 0 ? void 0 : Math.min(...bounds);
12758
- const finalizeTurns = spec?.finalizeTurns ?? 2;
12759
- const finalizeReserveUsd = spec?.finalizeReserveUsd ?? finalizeTurns * flatReserveUsd;
12760
- const reserveCommitted = input.orchestrator.extension === true;
12761
- if (reserveCommitted) reservedForFinalizationUsd = finalizeReserveUsd;
12762
- orchestratorEcho = {
12763
- ...effectiveCapUsd === void 0 ? {} : { effectiveCapUsd },
12764
- finalizeReserveUsd,
12765
- finalizeTurns,
12766
- reserveCommitted
12767
- };
12768
- if (spec?.capUsd !== void 0 && spec.capFraction === void 0 && effectiveCapUsd !== void 0 && effectiveCapUsd < spec.capUsd) say({
12769
- severity: "warning",
12770
- code: "orchestrator-cap-fraction-bound",
12771
- message: `orchestrator budget.capUsd ${spec.capUsd.toFixed(4)} USD is bounded to ${effectiveCapUsd.toFixed(4)} USD by the default capFraction 0.2 of the run ceiling; pass capFraction: 1.0 to make capUsd the sole bound`
12772
- });
12773
- if (input.orchestrator.extension === true && effectiveCapUsd !== void 0 && effectiveCapUsd < finalizeReserveUsd) say({
12774
- severity: "error",
12775
- code: "orchestrator-cap-below-finalize-reserve",
12776
- message: `effectiveCap ${effectiveCapUsd.toFixed(4)} USD is below the finalize reserve ${finalizeReserveUsd.toFixed(4)} USD: the run would refuse to start`
12765
+ }
12766
+ /** The command text an argv rule matches against. */
12767
+ function commandOf(input) {
12768
+ if (typeof input === "string") return input;
12769
+ if (typeof input === "object" && input !== null) {
12770
+ const command = input.command;
12771
+ if (typeof command === "string") return command;
12772
+ }
12773
+ }
12774
+ function ruleMatches(rule, toolName, risk, input) {
12775
+ if ("risk" in rule) {
12776
+ const risks = Array.isArray(rule.risk) ? rule.risk : [rule.risk];
12777
+ if (risks.includes("undeclared") && risk === void 0) return true;
12778
+ return risk !== void 0 && risks.includes(risk);
12779
+ }
12780
+ if ("domains" in rule) return false;
12781
+ if (!(Array.isArray(rule.tool) ? rule.tool : [rule.tool]).includes(toolName)) return false;
12782
+ if ("argv" in rule) {
12783
+ const command = commandOf(input);
12784
+ if (command === void 0) return false;
12785
+ const patterns = Array.isArray(rule.argv) ? rule.argv : [rule.argv];
12786
+ return lexShellCommand(command).some((segment) => !segment.unmatchable && patterns.some((pattern) => matchArgvPattern(pattern, segment.argv)));
12787
+ }
12788
+ return true;
12789
+ }
12790
+ /**
12791
+ * Advisory domain-rule matches for the audit payload:
12792
+ * reported, never enforced in the current release.
12793
+ */
12794
+ function advisoryMatches(chain, toolName) {
12795
+ return [...chain.deny, ...chain.ask].filter((rule) => "domains" in rule && rule.tool === toolName);
12796
+ }
12797
+ /**
12798
+ * Unmatchable segments (command/process substitution, here-docs) yield
12799
+ * ask, ALWAYS, for any tool that has argv rules.
12800
+ */
12801
+ function argvUnmatchableAsk(chain, toolName, input) {
12802
+ if (![...chain.deny, ...chain.ask].some((rule) => "argv" in rule && (Array.isArray(rule.tool) ? rule.tool : [rule.tool]).includes(toolName))) return false;
12803
+ const command = commandOf(input);
12804
+ if (command === void 0) return true;
12805
+ return lexShellCommand(command).some((segment) => segment.unmatchable);
12806
+ }
12807
+ /** A stub ToolContext for offline (dry-run) evaluations. */
12808
+ function offlineContext(toolName) {
12809
+ return {
12810
+ runId: "dry-run",
12811
+ spanId: `dry-run-${toolName}`,
12812
+ agent: { agentType: "" },
12813
+ cwd: process.cwd(),
12814
+ isolation: "none",
12815
+ signal: new AbortController().signal,
12816
+ log: () => void 0
12817
+ };
12818
+ }
12819
+ /**
12820
+ * Evaluates the chain for one dispatch, or OFFLINE against a
12821
+ * hypothetical call by tool name (the dry-run API: nothing executes;
12822
+ * shells and tests read the verdict, the
12823
+ * deciding layer, and the matched rule). Hooks run in deterministic
12824
+ * registration order; { modifiedInput } substitutes the input and
12825
+ * continues; the first decisive verdict wins. The returned input is what
12826
+ * execute receives and what the approval identity hashes (post hook
12827
+ * modification). Advisory domain-rule matches
12828
+ * ride every verdict for the audit payload.
12829
+ */
12830
+ async function evaluatePermission(chain, tool, input, ctx) {
12831
+ const def = typeof tool === "string" ? {
12832
+ name: tool,
12833
+ needsApproval: false
12834
+ } : tool;
12835
+ const risk = typeof tool === "string" ? void 0 : tool.risk;
12836
+ const context = ctx ?? offlineContext(def.name);
12837
+ const advisory = advisoryMatches(chain, def.name);
12838
+ const withAdvisory = (verdict) => advisory.length === 0 ? verdict : {
12839
+ ...verdict,
12840
+ advisory
12841
+ };
12842
+ let effective = input;
12843
+ for (const hook of chain.hooks) {
12844
+ const verdict = await hook(def.name, effective, context);
12845
+ if (verdict === void 0) continue;
12846
+ if (verdict === "allow" || verdict === "deny" || verdict === "ask") return withAdvisory({
12847
+ verdict,
12848
+ decidedBy: "hook",
12849
+ input: effective
12777
12850
  });
12851
+ effective = verdict.modifiedInput;
12778
12852
  }
12779
- const spawnSpecs = input.spawns ?? [];
12780
- spawnSpecs.forEach(validateSpawnSpec);
12781
- const spawnReports = [];
12782
- const units = [];
12783
- for (const spec of spawnSpecs) {
12784
- const role = spec.role ?? "loop";
12785
- const label = spec.label ?? role;
12786
- const count = spec.count ?? 1;
12787
- const profile = spec.profile === void 0 ? void 0 : defaults.profiles?.[spec.profile];
12788
- if (spec.profile !== void 0 && profile === void 0) say({
12789
- severity: "error",
12790
- code: "unknown-profile",
12791
- message: `spawn '${label}' names profile '${spec.profile}', which defaults.profiles does not register`,
12792
- spawn: label
12853
+ for (const rule of chain.deny) if (ruleMatches(rule, def.name, risk, effective)) return withAdvisory({
12854
+ verdict: "deny",
12855
+ decidedBy: "deny-rule",
12856
+ rule,
12857
+ input: effective
12858
+ });
12859
+ for (const rule of chain.ask) if (ruleMatches(rule, def.name, risk, effective)) return withAdvisory({
12860
+ verdict: "ask",
12861
+ decidedBy: "ask-rule",
12862
+ rule,
12863
+ input: effective
12864
+ });
12865
+ if (argvUnmatchableAsk(chain, def.name, effective)) return withAdvisory({
12866
+ verdict: "ask",
12867
+ decidedBy: "ask-rule",
12868
+ input: effective
12869
+ });
12870
+ if (chain.canUseTool !== void 0) {
12871
+ const verdict = await chain.canUseTool(def.name, effective, context);
12872
+ if (verdict === "allow") return withAdvisory({
12873
+ verdict: "allow",
12874
+ decidedBy: "canUseTool",
12875
+ input: effective
12793
12876
  });
12794
- const limits = mergeUsageLimits(spec.limits, profile?.limits, defaults.limits);
12795
- const servedBy = resolveServing(spec.model ?? profile?.routing?.[role] ?? profile?.model ?? defaults.routing?.[role]);
12796
- if (servedBy === void 0) say({
12797
- severity: "error",
12798
- code: "unrouted-role",
12799
- message: `spawn '${label}' resolves no model for role '${role}': the run would fail with a ConfigError at spawn time; set a model, a profile model, or defaults.routing.${role}`,
12800
- spawn: label
12877
+ if (verdict === "deny") return withAdvisory({
12878
+ verdict: "deny",
12879
+ decidedBy: "canUseTool",
12880
+ input: effective
12801
12881
  });
12802
- const caps = servedBy === void 0 ? void 0 : capsOf(servedBy);
12803
- const pricing = servedBy === void 0 ? void 0 : pricingOf(servedBy);
12804
- const unpriced = servedBy !== void 0 && pricing === void 0;
12805
- let reserveSource;
12806
- let reserveUsd;
12807
- if (unpriced && spec.estCost === void 0 && profile?.estCost === void 0) {
12808
- reserveSource = "unpriced-zero";
12809
- reserveUsd = 0;
12810
- } else {
12811
- reserveSource = spec.estCost !== void 0 ? "estCost" : profile?.estCost !== void 0 ? "profile-estCost" : spec.estInputTokens !== void 0 && caps?.pricing !== void 0 ? "priced-estimate" : "flat-default";
12812
- reserveUsd = admissionReserveUsd({
12813
- ...spec.estCost === void 0 ? {} : { estCost: spec.estCost },
12814
- ...profile?.estCost === void 0 ? {} : { profileEstCost: profile.estCost },
12815
- ...spec.estInputTokens === void 0 ? {} : { inputTokens: spec.estInputTokens },
12816
- ...caps === void 0 ? {} : { caps },
12817
- ...limits.maxOutputTokensPerTurn === void 0 ? {} : { maxOutputTokensPerTurn: limits.maxOutputTokensPerTurn },
12818
- flatReserveUsd
12819
- });
12820
- }
12821
- const outputBound = caps === void 0 ? limits.maxOutputTokensPerTurn : limits.maxOutputTokensPerTurn === void 0 ? caps.maxOutputTokens : Math.min(caps.maxOutputTokens, limits.maxOutputTokensPerTurn);
12822
- if (caps !== void 0 && limits.maxOutputTokensPerTurn !== void 0 && limits.maxOutputTokensPerTurn > caps.maxOutputTokens) say({
12823
- severity: "warning",
12824
- code: "output-cap-above-model",
12825
- message: `spawn '${label}' sets maxOutputTokensPerTurn ${String(limits.maxOutputTokensPerTurn)} above the model's maxOutputTokens ${String(caps.maxOutputTokens)}: the model clamp wins`,
12826
- spawn: label
12827
- });
12828
- const turnFloorUsd = pricing === void 0 || outputBound === void 0 ? void 0 : priceUsdOf(pricing, {
12829
- inputTokens: spec.estInputTokens ?? 0,
12830
- outputTokens: outputBound,
12831
- cacheReadTokens: 0,
12832
- cacheWriteTokens: 0
12833
- });
12834
- const toolCeilings = toolCeilingsOf(limits);
12835
- const overall = toolCeilings.reduce((best, row) => row.ceiling === null ? best : best === null ? row.ceiling : Math.max(best, row.ceiling), null);
12836
- const executedToolCallCeiling = limits.maxToolCalls !== void 0 && (overall === null || limits.maxToolCalls < overall) ? limits.maxToolCalls : overall;
12837
- for (const row of toolCeilings) {
12838
- if (row.tool === ANY_TOOL) continue;
12839
- const cost = limits.toolUnits?.costs?.[row.tool];
12840
- if (cost !== void 0 && cost > 0 && limits.toolUnits !== void 0 && cost > limits.toolUnits.max) {
12841
- say({
12842
- severity: "warning",
12843
- code: "tool-unaffordable",
12844
- message: `spawn '${label}' prices tool '${row.tool}' at ${String(cost)} units against toolUnits.max ${String(limits.toolUnits.max)}: the tool can never execute`,
12845
- spawn: label
12846
- });
12847
- continue;
12848
- }
12849
- if (row.boundBy === "toolUnits" && row.ceiling !== null) {
12850
- const nominal = limits.maxToolCalls;
12851
- const cap = limits.maxCallsPerTool?.[row.tool];
12852
- if (nominal !== void 0 && row.ceiling < nominal || cap !== void 0 && row.ceiling < cap) say({
12853
- severity: "warning",
12854
- code: "weighted-units-bind-first",
12855
- message: `spawn '${label}': toolUnits is the first bottleneck for '${row.tool}': ${String(row.ceiling)} executed calls (cost ${String(limits.toolUnits?.costs?.[row.tool] ?? 1)} of max ${String(limits.toolUnits?.max ?? 0)})` + (nominal === void 0 ? "" : ` while maxToolCalls suggests ${String(nominal)}`),
12856
- spawn: label
12857
- });
12858
- }
12859
- const cap = limits.maxCallsPerTool?.[row.tool];
12860
- if (cap !== void 0 && cap > 0 && row.ceiling !== null && row.boundBy !== "maxCallsPerTool") say({
12861
- severity: "info",
12862
- code: "per-tool-cap-unreachable",
12863
- message: `spawn '${label}': maxCallsPerTool['${row.tool}'] ${String(cap)} can never bind: ${row.boundBy ?? "another limiter"} already stops at ${String(row.ceiling)}`,
12864
- spawn: label
12865
- });
12882
+ effective = verdict.modifiedInput;
12883
+ }
12884
+ if (def.needsApproval) return withAdvisory({
12885
+ verdict: "ask",
12886
+ decidedBy: "default",
12887
+ input: effective
12888
+ });
12889
+ return withAdvisory({
12890
+ verdict: "allow",
12891
+ decidedBy: "default",
12892
+ input: effective
12893
+ });
12894
+ }
12895
+ //#endregion
12896
+ //#region src/runtime/executor.ts
12897
+ /**
12898
+ * Isolated-executor dispatch helpers (RV-216). The engine routes a
12899
+ * non-inprocess tool call through the registered ToolExecutorProvider;
12900
+ * this module derives the stable per-call idempotency key the provider
12901
+ * receives, so an at-least-once retry of a side-effecting tool can be
12902
+ * folded into effectively-once.
12903
+ *
12904
+ * Public contract: https://docs.rulvar.com/guide/isolated-executor.
12905
+ */
12906
+ /**
12907
+ * Derives the idempotency key for one isolated tool dispatch. The key is
12908
+ * a pure function of the run, the LOGICAL INVOCATION (the seq of the
12909
+ * containing agent's journal entry plus that call's ordinal within the
12910
+ * agent's tool loop), the tool name, and the JCS-canonical arguments.
12911
+ *
12912
+ * The logical-invocation component is what makes the key both stable and
12913
+ * distinguishing (v1.59.x review P0.4): the agent-entry seq and the
12914
+ * per-agent tool-call ordinal are journal- and checkpoint-stable, so a
12915
+ * crash-and-resume re-dispatch of the SAME logical call (the at-least-
12916
+ * once window between execution and the turn checkpoint) reuses the same
12917
+ * dispatch entry and the restored ordinal, and therefore the same key;
12918
+ * while two SEPARATE calls in one run, even with byte-identical
12919
+ * arguments, occupy different ordinals and never collide. Without it two
12920
+ * intended effects sharing arguments would fold into one under external
12921
+ * deduplication.
12922
+ *
12923
+ * The key never enters run identity (it is absent from every content key
12924
+ * and toolset hash); it exists only for the provider's own side-effect
12925
+ * deduplication.
12926
+ */
12927
+ function deriveExecIdempotencyKey(runId, agentSeq, ordinal, tool, args) {
12928
+ const canonical = jcsSerialize({
12929
+ runId,
12930
+ agentSeq,
12931
+ ordinal,
12932
+ tool,
12933
+ args
12934
+ });
12935
+ return createHash("sha256").update(canonical, "utf8").digest("hex");
12936
+ }
12937
+ //#endregion
12938
+ //#region src/engine/scheduler.ts
12939
+ /**
12940
+ * Scheduler and concurrency (M1-T08): the per-run semaphore with a FIFO
12941
+ * queue (default 12 concurrent model calls). The engine lifetime spawn cap
12942
+ * is enforced by the budget layer at admission; parallel/pipeline
12943
+ * composition semantics live with ctx.
12944
+ * Per-provider concurrency keys land with M4.
12945
+ */
12946
+ /** FIFO semaphore; default per-run width is 12. */
12947
+ const DEFAULT_PER_RUN_CONCURRENCY = 12;
12948
+ var Semaphore = class {
12949
+ limit;
12950
+ active = 0;
12951
+ waiters = [];
12952
+ /**
12953
+ * `limit` must be a positive integer: anything else (NaN included) is
12954
+ * a typed ConfigError. Before this gate a NaN limit made
12955
+ * `active < limit` permanently false, so the first acquire queued
12956
+ * forever and the run could not settle, not even through cancel()
12957
+ * (v1.34.0 review P2-4). Unlimited is expressed by not constructing a
12958
+ * semaphore, never by a sentinel limit.
12959
+ */
12960
+ constructor(limit) {
12961
+ requirePositiveInteger(limit, "Semaphore limit");
12962
+ this.limit = limit;
12963
+ }
12964
+ get pending() {
12965
+ return this.waiters.length;
12966
+ }
12967
+ /**
12968
+ * Acquires a slot, resolving in FIFO order. `onQueued` fires only when
12969
+ * the caller actually has to wait (feeds the agent:queued event).
12970
+ * An aborted `signal` releases the caller from the queue without a
12971
+ * slot: the returned release is a no-op, the remaining waiters keep
12972
+ * their FIFO positions, and the caller proceeds to observe its own
12973
+ * aborted signal (the model layers refuse dispatch under an aborted
12974
+ * signal, so no provider call follows). Cancellation can therefore
12975
+ * always drain a queued run (v1.34.0 review P2-4).
12976
+ */
12977
+ async acquire(onQueued, signal) {
12978
+ if (this.active < this.limit) {
12979
+ this.active += 1;
12980
+ return () => this.release();
12866
12981
  }
12867
- if (limits.finalizationReserve !== void 0 && limits.maxToolCalls === void 0 && limits.toolUnits === void 0) say({
12868
- severity: "warning",
12869
- code: "inert-finalization-reserve",
12870
- message: `spawn '${label}' sets finalizationReserve without maxToolCalls or toolUnits: no tool budget limiter exists for it to fire on`,
12871
- spawn: label
12872
- });
12873
- if (limits.toolBudgetNotices === true && limits.maxToolCalls === void 0) say({
12874
- severity: "warning",
12875
- code: "inert-tool-budget-notices",
12876
- message: `spawn '${label}' sets toolBudgetNotices without maxToolCalls: the notices never fire`,
12877
- spawn: label
12878
- });
12879
- if (unpriced && ceilingUsd !== void 0) say({
12880
- severity: "warning",
12881
- code: "unpriced-under-ceiling",
12882
- message: `spawn '${label}' is served by '${servedBy ?? ""}' with no price row: the ${ceilingUsd.toFixed(4)} USD run ceiling does NOT bound it and its admission reserve is zero`,
12883
- spawn: label
12884
- });
12885
- spawnReports.push({
12886
- label,
12887
- role,
12888
- count,
12889
- ...servedBy === void 0 ? {} : { servedBy },
12890
- ...unpriced ? { unpriced: true } : {},
12891
- limits,
12892
- admissionReserveUsd: reserveUsd,
12893
- reserveSource,
12894
- ...outputBound === void 0 ? {} : { maxOutputTokensPerTurn: outputBound },
12895
- ...turnFloorUsd === void 0 ? {} : { turnFloorUsd },
12896
- executedToolCallCeiling,
12897
- toolCeilings
12982
+ if (signal?.aborted === true) return () => void 0;
12983
+ onQueued?.();
12984
+ const waiter = {
12985
+ resolve: () => void 0,
12986
+ aborted: false
12987
+ };
12988
+ const wait = new Promise((resolve) => {
12989
+ waiter.resolve = resolve;
12898
12990
  });
12899
- for (let i = 0; i < count; i += 1) {
12900
- const unit = {
12901
- label: count === 1 ? label : `${label}#${String(i + 1)}`,
12902
- tokensFloor: (spec.estInputTokens ?? 0) + (outputBound ?? 0)
12991
+ this.waiters.push(waiter);
12992
+ let onAbort;
12993
+ if (signal !== void 0) {
12994
+ onAbort = () => {
12995
+ const index = this.waiters.indexOf(waiter);
12996
+ if (index === -1) return;
12997
+ this.waiters.splice(index, 1);
12998
+ waiter.aborted = true;
12999
+ waiter.resolve();
12903
13000
  };
12904
- if (servedBy !== void 0) {
12905
- const { adapterId, model } = parseModelRef(servedBy);
12906
- unit.provider = adapterId;
12907
- unit.model = model;
12908
- }
12909
- if (turnFloorUsd !== void 0) unit.turnFloorUsd = turnFloorUsd;
12910
- units.push(unit);
13001
+ signal.addEventListener("abort", onAbort, { once: true });
13002
+ }
13003
+ try {
13004
+ await wait;
13005
+ } finally {
13006
+ if (signal !== void 0 && onAbort !== void 0) signal.removeEventListener("abort", onAbort);
12911
13007
  }
13008
+ if (waiter.aborted) return () => void 0;
13009
+ this.active += 1;
13010
+ return () => this.release();
12912
13011
  }
12913
- if (input.orchestrator !== void 0) {
12914
- const servedBy = resolveServing(defaults.routing?.orchestrate);
12915
- if (servedBy === void 0) say({
12916
- severity: "error",
12917
- code: "unrouted-role",
12918
- message: "the orchestrator resolves no model for role 'orchestrate': set defaults.routing.orchestrate or an orchestrate model on the call",
12919
- spawn: "orchestrator"
12920
- });
12921
- else {
12922
- const caps = capsOf(servedBy);
12923
- const pricing = pricingOf(servedBy);
12924
- const orchLimits = mergeUsageLimits(input.orchestrator.limits, void 0, defaults.limits);
12925
- const outputBound = caps === void 0 ? orchLimits.maxOutputTokensPerTurn : orchLimits.maxOutputTokensPerTurn === void 0 ? caps.maxOutputTokens : Math.min(caps.maxOutputTokens, orchLimits.maxOutputTokensPerTurn);
12926
- const { adapterId, model } = parseModelRef(servedBy);
12927
- const unit = {
12928
- label: "orchestrator",
12929
- provider: adapterId,
12930
- model,
12931
- tokensFloor: outputBound ?? 0
12932
- };
12933
- if (pricing !== void 0 && outputBound !== void 0) unit.turnFloorUsd = priceUsdOf(pricing, {
12934
- inputTokens: 0,
12935
- outputTokens: outputBound,
12936
- cacheReadTokens: 0,
12937
- cacheWriteTokens: 0
12938
- });
12939
- units.push(unit);
13012
+ async withSlot(fn, onQueued, signal) {
13013
+ const release = await this.acquire(onQueued, signal);
13014
+ try {
13015
+ return await fn();
13016
+ } finally {
13017
+ release();
12940
13018
  }
12941
13019
  }
12942
- const wave = [];
12943
- let committed = 0;
12944
- let spawned = 0;
12945
- let children = 0;
12946
- const admitAgainstRoot = (reserveUsd) => {
12947
- if (ceilingUsd === void 0) return true;
12948
- const held = committed + reservedForFinalizationUsd;
12949
- return !(held >= ceilingUsd || held + reserveUsd > ceilingUsd);
12950
- };
12951
- if (input.orchestrator !== void 0) {
12952
- const reserveUsd = flatReserveUsd;
12953
- let deniedBy;
12954
- if (spawned >= lifetimeSpawnCap) deniedBy = "spawn-cap";
12955
- else if (effectiveCapUsd !== void 0 && reserveUsd > effectiveCapUsd) deniedBy = "orchestrator-cap";
12956
- else if (!admitAgainstRoot(reserveUsd)) deniedBy = "budget";
12957
- wave.push({
12958
- label: "orchestrator",
12959
- reserveUsd,
12960
- admitted: deniedBy === void 0,
12961
- ...deniedBy === void 0 ? {} : { deniedBy }
12962
- });
12963
- if (deniedBy === void 0) {
12964
- committed += reserveUsd;
12965
- spawned += 1;
12966
- } else if (deniedBy === "orchestrator-cap") say({
12967
- severity: "error",
12968
- code: "orchestrator-cap-below-reserve",
12969
- message: `the orchestrator's own admission reserve ${reserveUsd.toFixed(4)} USD does not fit its effective cap ${(effectiveCapUsd ?? 0).toFixed(4)} USD: the run cannot start`
12970
- });
12971
- }
12972
- const maxSpawns = input.orchestrator?.maxSpawns;
12973
- for (const report of spawnReports) for (let i = 0; i < report.count; i += 1) {
12974
- const label = report.count === 1 ? report.label : `${report.label}#${String(i + 1)}`;
12975
- const reserveUsd = report.admissionReserveUsd;
12976
- let deniedBy;
12977
- if (spawned >= lifetimeSpawnCap) deniedBy = "spawn-cap";
12978
- else if (maxSpawns !== void 0 && children >= maxSpawns) deniedBy = "orchestrator-max-spawns";
12979
- else if (!admitAgainstRoot(reserveUsd)) deniedBy = "budget";
12980
- wave.push({
12981
- label,
12982
- reserveUsd,
12983
- admitted: deniedBy === void 0,
12984
- ...deniedBy === void 0 ? {} : { deniedBy }
12985
- });
12986
- if (deniedBy === void 0) {
12987
- committed += reserveUsd;
12988
- spawned += 1;
12989
- children += 1;
12990
- }
12991
- }
12992
- const admitted = wave.filter((row) => row.admitted).length;
12993
- const denied = wave.length - admitted;
12994
- if (wave.length > 0 && denied > 0) {
12995
- const deniedLabels = wave.filter((row) => !row.admitted).map((row) => row.label);
12996
- if (admitted === 0) say({
12997
- severity: "error",
12998
- code: "nothing-admitted",
12999
- message: `the declared wave admits NOTHING: every spawn is denied (${deniedLabels.join(", ")}); no paid work can start`
13000
- });
13001
- else say({
13002
- severity: "warning",
13003
- code: "partial-admission",
13004
- message: `the declared wave admits ${String(admitted)} of ${String(wave.length)} spawns; denied before any work: ${deniedLabels.join(", ")}`
13005
- });
13006
- }
13007
- if (ceilingUsd === void 0 && wave.length > 0) say({
13008
- severity: "info",
13009
- code: "no-usd-ceiling",
13010
- message: "the run has no budgetUsd ceiling: only turn, tool, and time limits bound spend, and the whole declared wave admits"
13011
- });
13012
- const declaredUnits = units.length;
13013
- const maxInFlight = declaredUnits === 0 ? perRun : Math.min(perRun, declaredUnits);
13014
- const perProviderCaps = engine.concurrency?.perProvider;
13015
- const perProvider = {};
13016
- const byProvider = /* @__PURE__ */ new Map();
13017
- for (const unit of units) {
13018
- if (unit.provider === void 0) continue;
13019
- const list = byProvider.get(unit.provider) ?? [];
13020
- list.push(unit);
13021
- byProvider.set(unit.provider, list);
13020
+ release() {
13021
+ this.active -= 1;
13022
+ const next = this.waiters.shift();
13023
+ if (next !== void 0) next.resolve();
13022
13024
  }
13023
- for (const [provider, list] of [...byProvider.entries()].sort(([a], [b]) => a.localeCompare(b))) {
13024
- const cap = perProviderCaps?.[provider];
13025
- const inFlight = Math.min(list.length, maxInFlight, cap ?? Number.POSITIVE_INFINITY);
13026
- perProvider[provider] = {
13027
- inFlight,
13028
- requestsPerWave: inFlight,
13029
- tokensPerWaveFloor: [...list].sort((a, b) => b.tokensFloor - a.tokensFloor).slice(0, inFlight).reduce((sum, unit) => sum + unit.tokensFloor, 0)
13025
+ };
13026
+ //#endregion
13027
+ //#region src/engine/ctx.ts
13028
+ /**
13029
+ * Ctx primitives (M1-T07) plus the parallel/pipeline composition semantics
13030
+ * of the scheduler (M1-T08): the canonical authoring surface bound to the
13031
+ * journal write path, the model router, the agent runtime, and the
13032
+ * three-layer budget. M1 ships agent, parallel, pipeline, step, phase,
13033
+ * log, budget, and the deterministic shims; workflow/orchestrate/
13034
+ * awaitExternal/brief land with their milestones (M2/M6).
13035
+ *
13036
+ * Public contract: https://docs.rulvar.com/guide/workflows.
13037
+ */
13038
+ /**
13039
+ * The rejection carrier of ctx.agent value-form calls: a real Error that
13040
+ * structurally satisfies the typed AgentError and carries the full
13041
+ * AgentResult for Settled mapping. Deliberately not a RulvarError:
13042
+ * AgentError is not in the closed code registry.
13043
+ */
13044
+ var AgentCallError = class extends Error {
13045
+ kind;
13046
+ retryable;
13047
+ retryAfterMs;
13048
+ issues;
13049
+ result;
13050
+ scope;
13051
+ entryRef;
13052
+ constructor(message, result, scope, entryRef) {
13053
+ super(message);
13054
+ this.name = "AgentCallError";
13055
+ const error = result.error ?? {
13056
+ kind: "terminal",
13057
+ retryable: false
13030
13058
  };
13059
+ this.kind = error.kind;
13060
+ this.retryable = error.retryable;
13061
+ if (error.retryAfterMs !== void 0) this.retryAfterMs = error.retryAfterMs;
13062
+ if (error.issues !== void 0) this.issues = error.issues;
13063
+ this.result = result;
13064
+ this.scope = scope;
13065
+ if (entryRef !== void 0) this.entryRef = entryRef;
13031
13066
  }
13032
- const pricedTurns = units.map((unit) => unit.turnFloorUsd).filter((usd) => usd !== void 0).sort((a, b) => b - a).slice(0, maxInFlight);
13033
- const overshootOneTurnFloorUsd = pricedTurns.length === 0 ? void 0 : pricedTurns.reduce((sum, usd) => sum + usd, 0);
13034
- if (ceilingUsd !== void 0 && overshootOneTurnFloorUsd !== void 0 && units.length > 0) say({
13035
- severity: "info",
13036
- code: "overshoot-exposure",
13037
- message: `past a ceiling crossing, up to ${String(Math.min(maxInFlight, units.length))} in-flight turns may still complete: at least ${overshootOneTurnFloorUsd.toFixed(4)} USD past the ${ceilingUsd.toFixed(4)} USD ceiling at the declared estimates, growing with prompt size`
13038
- });
13039
- const quotaConfigured = engine.quota !== void 0;
13040
- if (!quotaConfigured && maxInFlight > 1 && units.length > 0) say({
13041
- severity: "info",
13042
- code: "no-quota",
13043
- message: `no shared quota limiter is configured while up to ${String(maxInFlight)} turns run concurrently: provider-side rate limits are unprotected (createEngine quota)`
13044
- });
13045
- if (input.quotaRules !== void 0) input.quotaRules.forEach((rule, index) => {
13046
- let requests = 0;
13047
- let tokens = 0;
13048
- for (const unit of units) {
13049
- if (unit.provider === void 0 || unit.model === void 0) continue;
13050
- if (quotaRuleMatches(rule, {
13051
- provider: unit.provider,
13052
- model: unit.model,
13053
- ...engine.quota?.tenant === void 0 ? {} : { tenant: engine.quota.tenant },
13054
- estimate: {
13055
- requests: 1,
13056
- inputTokens: 0
13057
- }
13058
- })) {
13059
- requests += 1;
13060
- tokens += unit.tokensFloor;
13061
- }
13067
+ };
13068
+ /**
13069
+ * Projects a settled AgentResult's error to its wire form, carrying the
13070
+ * engine-decided abort class in data. AgentError itself has no data
13071
+ * field, so without this every projection past the terminal entry (the
13072
+ * run-level outcome.error, thrown AgentCallError wires, dropped items)
13073
+ * would keep only the message text and lose the typed class (v1.9.0
13074
+ * follow-up review).
13075
+ */
13076
+ function agentResultWire(result, fallbackMessage) {
13077
+ const wire = agentErrorToWire(result.error ?? {
13078
+ kind: "terminal",
13079
+ retryable: false
13080
+ }, result.errorMessage ?? fallbackMessage);
13081
+ if (result.abortClass === void 0) return wire;
13082
+ const data = typeof wire.data === "object" && wire.data !== null && !Array.isArray(wire.data) ? wire.data : {};
13083
+ return {
13084
+ ...wire,
13085
+ data: {
13086
+ ...data,
13087
+ abortClass: result.abortClass
13062
13088
  }
13063
- const dims = [
13064
- rule.provider === void 0 ? void 0 : `provider=${rule.provider}`,
13065
- rule.model === void 0 ? void 0 : `model=${rule.model}`,
13066
- rule.tenant === void 0 ? void 0 : `tenant=${rule.tenant}`
13067
- ].filter((dim) => dim !== void 0).join(" ");
13068
- const name = dims === "" ? `rule[${String(index)}]` : `rule[${String(index)}] (${dims})`;
13069
- if (rule.requestsPerMinute !== void 0 && requests > rule.requestsPerMinute) say({
13070
- severity: "warning",
13071
- code: "quota-requests-below-wave",
13072
- message: `${name}: the declared wave holds ${String(requests)} matching dispatches against requestsPerMinute ${String(rule.requestsPerMinute)}: expect synthetic rate-limit denials and backoff inside one window`
13073
- });
13074
- if (rule.tokensPerMinute !== void 0 && tokens > rule.tokensPerMinute) say({
13075
- severity: "warning",
13076
- code: "quota-tokens-below-wave",
13077
- message: `${name}: the declared wave demands at least ${String(tokens)} tokens against tokensPerMinute ${String(rule.tokensPerMinute)}: expect estimate-driven throttling inside one window`
13078
- });
13079
- });
13080
- const severityRank = {
13081
- error: 0,
13082
- warning: 1,
13083
- info: 2
13084
13089
  };
13085
- findings.sort((a, b) => severityRank[a.severity] - severityRank[b.severity]);
13086
- return {
13087
- concurrency: {
13088
- perRun,
13089
- ...perProviderCaps === void 0 ? {} : { perProvider: { ...perProviderCaps } }
13090
- },
13091
- budget: {
13092
- ...ceilingUsd === void 0 ? {} : { ceilingUsd },
13093
- flatReserveUsd,
13094
- lifetimeSpawnCap,
13095
- childBudgetFraction,
13096
- maxDepth,
13097
- ...orchestratorEcho === void 0 ? {} : { orchestrator: orchestratorEcho }
13098
- },
13099
- quota: {
13100
- configured: quotaConfigured,
13101
- ...engine.quota?.tenant === void 0 ? {} : { tenant: engine.quota.tenant },
13102
- ...input.quotaRules === void 0 ? {} : { rules: input.quotaRules.length }
13103
- },
13104
- runLimits,
13105
- spawns: spawnReports,
13106
- admission: {
13107
- ...ceilingUsd === void 0 ? {} : { ceilingUsd },
13108
- reservedForFinalizationUsd,
13109
- wave,
13110
- admitted,
13111
- denied
13112
- },
13113
- exposure: {
13114
- maxInFlight,
13115
- ...overshootOneTurnFloorUsd === void 0 ? {} : { overshootOneTurnFloorUsd },
13116
- perProvider
13117
- },
13118
- findings
13090
+ }
13091
+ /** The workflow-defaults layer a Workflow value contributes, or nothing. */
13092
+ function workflowLayerOf(wf) {
13093
+ const layer = {};
13094
+ if (wf.model !== void 0) layer.model = wf.model;
13095
+ if (wf.routing !== void 0) layer.routing = wf.routing;
13096
+ if (wf.effort !== void 0) layer.effort = wf.effort;
13097
+ return Object.keys(layer).length === 0 ? void 0 : layer;
13098
+ }
13099
+ function defineWorkflow(meta, body) {
13100
+ const wf = {
13101
+ kind: "workflow",
13102
+ name: meta.name,
13103
+ errorPolicy: meta.errorPolicy ?? "strict",
13104
+ ...meta.model === void 0 ? {} : { model: meta.model },
13105
+ ...meta.routing === void 0 ? {} : { routing: meta.routing },
13106
+ ...meta.effort === void 0 ? {} : { effort: meta.effort },
13107
+ body
13108
+ };
13109
+ if (meta.args !== void 0) return {
13110
+ ...wf,
13111
+ argsSchema: meta.args
13119
13112
  };
13113
+ return wf;
13114
+ }
13115
+ function bump(map, key, usd) {
13116
+ map.set(key, (map.get(key) ?? 0) + usd);
13120
13117
  }
13121
- //#endregion
13122
- //#region src/engine/run-profiles.ts
13123
13118
  /**
13124
- * The shipped presets (fast / standard / deep / ultra "and similar").
13125
- * Data only; a review-time assertion checks the
13126
- * engine has zero behavioral branches keyed on these names.
13127
- */
13128
- const RUN_PROFILES = {
13129
- fast: {
13130
- effortByRole: {
13131
- orchestrate: "low",
13132
- plan: "low",
13133
- summarize: "low",
13134
- extract: "low"
13135
- },
13136
- perRunConcurrency: 16,
13137
- permissionPreset: "standard",
13138
- lifetimeSpawnCap: 64,
13139
- maxDepth: 1
13140
- },
13141
- standard: {
13142
- effortByRole: {
13143
- orchestrate: "high",
13144
- plan: "high",
13145
- summarize: "low",
13146
- extract: "low"
13147
- },
13148
- perRunConcurrency: 12,
13149
- permissionPreset: "standard",
13150
- lifetimeSpawnCap: 500,
13151
- maxDepth: 1
13152
- },
13153
- deep: {
13154
- effortByRole: {
13155
- orchestrate: "high",
13156
- plan: "high",
13157
- summarize: "medium",
13158
- extract: "medium"
13159
- },
13160
- perRunConcurrency: 8,
13161
- permissionPreset: "standard",
13162
- lifetimeSpawnCap: 500,
13163
- maxDepth: 2
13164
- },
13165
- ultra: {
13166
- effortByRole: {
13167
- orchestrate: "max",
13168
- plan: "max",
13169
- summarize: "high",
13170
- extract: "high"
13171
- },
13172
- perRunConcurrency: 8,
13173
- permissionPreset: "strict",
13174
- lifetimeSpawnCap: 500,
13175
- maxDepth: 3
13176
- }
13177
- };
13178
- /** Looks up a shipped RunProfile by name; undefined for unknown names. */
13179
- function runProfile(name) {
13180
- return RUN_PROFILES[name];
13181
- }
13182
- //#endregion
13183
- //#region src/model/concurrency.ts
13184
- /**
13185
- * Per-provider concurrency keys (M4-T07): a keyed limiter beside the
13186
- * router, ENGINE-scoped (keys constrain calls
13187
- * across a single engine per adapter). The Appendix A default is
13188
- * unlimited: an embeddable library must not surprise-throttle hosts, so
13189
- * the per-run semaphore stays the only default bound and provider 429s
13190
- * ride RetryPolicy; hosts with known tier limits opt in per adapter id
13191
- * via createEngine concurrency.perProvider.
13192
- *
13193
- * This keyed limiter bounds PARALLELISM inside one engine only. Two
13194
- * processes sharing one API key coordinate through the QuotaLimiter
13195
- * SPI instead (RV-215, createEngine `quota`): rate and volume live
13196
- * there, in shared storage; in-flight slots live here.
13197
- */
13198
- var KeyedLimiter = class {
13199
- semaphores = /* @__PURE__ */ new Map();
13200
- constructor(caps) {
13201
- for (const [key, limit] of Object.entries(caps ?? {})) this.semaphores.set(key, new Semaphore(limit));
13202
- }
13203
- /** Queue depth for one key (0 for unlimited keys); telemetry only. */
13204
- pending(key) {
13205
- return this.semaphores.get(key)?.pending ?? 0;
13206
- }
13207
- /**
13208
- * Runs `fn` under the key's semaphore; keys without a configured cap
13209
- * run unlimited (no queueing, no overhead). An aborted `signal` frees
13210
- * a queued caller without a slot (the Semaphore contract), so run
13211
- * cancellation drains provider queues too (v1.34.0 review P2-4).
13212
- */
13213
- async withSlot(key, fn, onQueued, signal) {
13214
- const semaphore = this.semaphores.get(key);
13215
- if (semaphore === void 0) return fn();
13216
- return semaphore.withSlot(fn, onQueued, signal);
13217
- }
13218
- };
13219
- //#endregion
13220
- //#region src/model/profile-card.ts
13221
- function toolNamesOf(profile) {
13222
- return (profile.tools ?? []).map((entry) => {
13223
- if (typeof entry === "string") return `${entry} (registered toolset)`;
13224
- if ("kind" in entry && entry.kind === "tool") return entry.name;
13225
- return `${entry.id}:* (tool source)`;
13226
- });
13227
- }
13228
- /**
13229
- * Renders the registry into the shared agent vocabulary card. Sorted,
13230
- * deterministic, byte-stable; an empty registry renders explicitly so
13231
- * the planner never guesses at unregistered agentTypes. When the engine
13232
- * registers toolsets, their names render as a closing line (v1.17.0
13233
- * review P1-3): those are the ONLY values valid as string entries of a
13234
- * tools option, so the planner never invents a registry name.
13235
- */
13236
- function profileCard(profiles, toolsets) {
13237
- const toolsetNames = Object.keys(toolsets ?? {}).sort();
13238
- const toolsetsLine = toolsetNames.length === 0 ? void 0 : `Registered toolsets (valid string entries of a tools option): ${toolsetNames.join(", ")}.`;
13239
- const names = Object.keys(profiles ?? {}).sort();
13240
- if (profiles === void 0 || names.length === 0) {
13241
- const empty = "Agent profiles: none registered. Calls take no agentType.";
13242
- return toolsetsLine === void 0 ? empty : `${empty}\n${toolsetsLine}`;
13243
- }
13244
- const lines = ["Agent profiles (agentType values):"];
13245
- for (const name of names) {
13246
- const profile = profiles[name];
13247
- const description = profile.description ?? "no description";
13248
- lines.push(`- ${name}: ${description}`);
13249
- const toolNames = toolNamesOf(profile);
13250
- if (toolNames.length > 0) lines.push(` tools: ${toolNames.join(", ")}`);
13251
- if (profile.taskClass !== void 0) lines.push(` taskClass: ${profile.taskClass}`);
13252
- if (profile.estCost !== void 0) lines.push(` estCost: ${profile.estCost.toFixed(2)} USD`);
13253
- if (profile.escalation !== void 0) lines.push(` escalation: flavor ${profile.escalation.flavor ?? "A"} (opt-in)`);
13254
- }
13255
- if (toolsetsLine !== void 0) lines.push(toolsetsLine);
13256
- return lines.join("\n");
13257
- }
13258
- //#endregion
13259
- //#region src/runtime/permission-chain.ts
13260
- /**
13261
- * The layered permission chain (M3-T03): the single approval surface for
13262
- * every tool dispatch, regardless of tool origin. The order is fixed and
13263
- * normative: hooks -> deny rules -> ask rules -> canUseTool -> terminal
13264
- * default (allow unless needsApproval, then ask). Evaluation is
13265
- * short-circuit; unconfigured layers are skipped. Rules never yield
13266
- * allow: allow is only ever falling through to canUseTool or the
13267
- * terminal default.
13268
- *
13269
- * Full contract: https://docs.rulvar.com/guide/tools.
13270
- * Risk presets, the argv shell matcher, domain rules, and the
13271
- * audit/dry-run surface land in M5.
13272
- */
13273
- /**
13274
- * Merges the engine-wide config and the profile config into one chain.
13275
- * Layers concatenate engine-first; since rules only deny or ask, ordering
13276
- * within a layer cannot change the verdict. The
13277
- * profile's canUseTool wins over the engine's (a single slot by
13278
- * construction). A declared preset compiles INTO the same layers, after
13279
- * the host-authored rules, never as a fifth layer (M5-T05).
13119
+ * Completes a model-authored escalation request into the full report:
13120
+ * costToDate and salvage are runtime-filled, never model-filled. The
13121
+ * worktree patch ref lands after collect(); the pre-dispose preview for
13122
+ * flavor B decision-makers omits it.
13280
13123
  */
13281
- function compilePermissionChain(engine, profile) {
13282
- const preset = profile?.preset === void 0 ? {
13283
- deny: [],
13284
- ask: []
13285
- } : compilePermissionPreset(profile.preset);
13286
- const deny = [
13287
- ...engine?.deny ?? [],
13288
- ...profile?.deny ?? [],
13289
- ...preset.deny
13290
- ];
13291
- const ask = [
13292
- ...engine?.ask ?? [],
13293
- ...profile?.ask ?? [],
13294
- ...preset.ask
13295
- ];
13296
- const canUseTool = profile?.canUseTool ?? engine?.canUseTool;
13124
+ function buildEscalationReport(request, result, worktreePatchRef) {
13297
13125
  return {
13298
- hooks: [...engine?.hooks ?? [], ...profile?.hooks ?? []],
13299
- deny,
13300
- ask,
13301
- ...canUseTool === void 0 ? {} : { canUseTool }
13126
+ kind: request.kind,
13127
+ scopeDelta: request.scopeDelta,
13128
+ revisedEstimate: request.revisedEstimate,
13129
+ blockers: request.blockers ?? [],
13130
+ proposedDecomposition: request.proposedDecomposition ?? [],
13131
+ costToDate: {
13132
+ usd: result.costUsd,
13133
+ turns: result.turns
13134
+ },
13135
+ salvage: {
13136
+ transcriptRef: result.transcriptRef,
13137
+ artifacts: (result.artifacts ?? []).map((artifact) => artifact.id),
13138
+ ...worktreePatchRef === void 0 ? {} : { worktreePatchRef }
13139
+ }
13302
13140
  };
13303
13141
  }
13304
- /** The command text an argv rule matches against. */
13305
- function commandOf(input) {
13306
- if (typeof input === "string") return input;
13307
- if (typeof input === "object" && input !== null) {
13308
- const command = input.command;
13309
- if (typeof command === "string") return command;
13310
- }
13311
- }
13312
- function ruleMatches(rule, toolName, risk, input) {
13313
- if ("risk" in rule) {
13314
- const risks = Array.isArray(rule.risk) ? rule.risk : [rule.risk];
13315
- if (risks.includes("undeclared") && risk === void 0) return true;
13316
- return risk !== void 0 && risks.includes(risk);
13317
- }
13318
- if ("domains" in rule) return false;
13319
- if (!(Array.isArray(rule.tool) ? rule.tool : [rule.tool]).includes(toolName)) return false;
13320
- if ("argv" in rule) {
13321
- const command = commandOf(input);
13322
- if (command === void 0) return false;
13323
- const patterns = Array.isArray(rule.argv) ? rule.argv : [rule.argv];
13324
- return lexShellCommand(command).some((segment) => !segment.unmatchable && patterns.some((pattern) => matchArgvPattern(pattern, segment.argv)));
13325
- }
13326
- return true;
13327
- }
13328
- /**
13329
- * Advisory domain-rule matches for the audit payload:
13330
- * reported, never enforced in the current release.
13331
- */
13332
- function advisoryMatches(chain, toolName) {
13333
- return [...chain.deny, ...chain.ask].filter((rule) => "domains" in rule && rule.tool === toolName);
13334
- }
13335
13142
  /**
13336
- * Unmatchable segments (command/process substitution, here-docs) yield
13337
- * ask, ALWAYS, for any tool that has argv rules.
13143
+ * Creates the per-run Ctx bound to `internals`. The current scope travels
13144
+ * through AsyncLocalStorage so parallel branches and pipeline stages keep
13145
+ * one ctx object while journaling under their own scope paths (I3:
13146
+ * structure from call-and-return only).
13338
13147
  */
13339
- function argvUnmatchableAsk(chain, toolName, input) {
13340
- if (![...chain.deny, ...chain.ask].some((rule) => "argv" in rule && (Array.isArray(rule.tool) ? rule.tool : [rule.tool]).includes(toolName))) return false;
13341
- const command = commandOf(input);
13342
- if (command === void 0) return true;
13343
- return lexShellCommand(command).some((segment) => segment.unmatchable);
13344
- }
13345
- /** A stub ToolContext for offline (dry-run) evaluations. */
13346
- function offlineContext(toolName) {
13347
- return {
13348
- runId: "dry-run",
13349
- spanId: `dry-run-${toolName}`,
13350
- agent: { agentType: "" },
13351
- cwd: process.cwd(),
13352
- isolation: "none",
13353
- signal: new AbortController().signal,
13354
- log: () => void 0
13148
+ function createCtx(internals, rootWorkflow) {
13149
+ const als = new AsyncLocalStorage();
13150
+ const sites = new ParallelSiteCounter();
13151
+ const rootWorkflowLayer = rootWorkflow === void 0 ? void 0 : workflowLayerOf(rootWorkflow);
13152
+ const rootState = {
13153
+ scope: "",
13154
+ spanId: internals.rootSpanId,
13155
+ ...rootWorkflowLayer === void 0 ? {} : { workflowLayer: rootWorkflowLayer }
13355
13156
  };
13356
- }
13357
- /**
13358
- * Evaluates the chain for one dispatch, or OFFLINE against a
13359
- * hypothetical call by tool name (the dry-run API: nothing executes;
13360
- * shells and tests read the verdict, the
13361
- * deciding layer, and the matched rule). Hooks run in deterministic
13362
- * registration order; { modifiedInput } substitutes the input and
13363
- * continues; the first decisive verdict wins. The returned input is what
13364
- * execute receives and what the approval identity hashes (post hook
13365
- * modification). Advisory domain-rule matches
13366
- * ride every verdict for the audit payload.
13367
- */
13368
- async function evaluatePermission(chain, tool, input, ctx) {
13369
- const def = typeof tool === "string" ? {
13370
- name: tool,
13371
- needsApproval: false
13372
- } : tool;
13373
- const risk = typeof tool === "string" ? void 0 : tool.risk;
13374
- const context = ctx ?? offlineContext(def.name);
13375
- const advisory = advisoryMatches(chain, def.name);
13376
- const withAdvisory = (verdict) => advisory.length === 0 ? verdict : {
13377
- ...verdict,
13378
- advisory
13379
- };
13380
- let effective = input;
13381
- for (const hook of chain.hooks) {
13382
- const verdict = await hook(def.name, effective, context);
13383
- if (verdict === void 0) continue;
13384
- if (verdict === "allow" || verdict === "deny" || verdict === "ask") return withAdvisory({
13385
- verdict,
13386
- decidedBy: "hook",
13387
- input: effective
13388
- });
13389
- effective = verdict.modifiedInput;
13390
- }
13391
- for (const rule of chain.deny) if (ruleMatches(rule, def.name, risk, effective)) return withAdvisory({
13392
- verdict: "deny",
13393
- decidedBy: "deny-rule",
13394
- rule,
13395
- input: effective
13396
- });
13397
- for (const rule of chain.ask) if (ruleMatches(rule, def.name, risk, effective)) return withAdvisory({
13398
- verdict: "ask",
13399
- decidedBy: "ask-rule",
13400
- rule,
13401
- input: effective
13402
- });
13403
- if (argvUnmatchableAsk(chain, def.name, effective)) return withAdvisory({
13404
- verdict: "ask",
13405
- decidedBy: "ask-rule",
13406
- input: effective
13407
- });
13408
- if (chain.canUseTool !== void 0) {
13409
- const verdict = await chain.canUseTool(def.name, effective, context);
13410
- if (verdict === "allow") return withAdvisory({
13411
- verdict: "allow",
13412
- decidedBy: "canUseTool",
13413
- input: effective
13414
- });
13415
- if (verdict === "deny") return withAdvisory({
13416
- verdict: "deny",
13417
- decidedBy: "canUseTool",
13418
- input: effective
13419
- });
13420
- effective = verdict.modifiedInput;
13421
- }
13422
- if (def.needsApproval) return withAdvisory({
13423
- verdict: "ask",
13424
- decidedBy: "default",
13425
- input: effective
13426
- });
13427
- return withAdvisory({
13428
- verdict: "allow",
13429
- decidedBy: "default",
13430
- input: effective
13431
- });
13432
- }
13433
- //#endregion
13434
- //#region src/orchestrator/finish-validators.ts
13435
- /**
13436
- * Deterministic host validation of the orchestrator finish result (the
13437
- * v1.40.0 improvement plan's RV-204 slice). A validator is plain
13438
- * synchronous host code judging the finish({ result }) argument; the
13439
- * orchestrator runtime runs the configured set on every schema valid
13440
- * finish call, returns the failure reasons to the model as the call's
13441
- * error tool result (a bounded repair turn), and fails the run with a
13442
- * typed error when the repair bound is exhausted. Verdicts journal as
13443
- * decision entries, so a resume rolls the SAME verdicts forward without
13444
- * re-running validator code.
13445
- */
13446
- const ok = { ok: true };
13447
- function requireNonEmptyStrings(values, what) {
13448
- if (!Array.isArray(values) || values.length === 0) throw new ConfigError(`${what} must be a non empty array of strings`);
13449
- for (const value of values) if (typeof value !== "string" || value.length === 0) throw new ConfigError(`${what} must contain only non empty strings`);
13450
- return values;
13451
- }
13452
- /**
13453
- * Requires every named section to appear LITERALLY in the result text
13454
- * (a heading like 'FINDINGS' or any marker the goal demands). Default
13455
- * name 'required-sections'; pass `name` to run several instances.
13456
- */
13457
- function requiredSectionsValidator(options) {
13458
- const sections = requireNonEmptyStrings(options.sections, "requiredSectionsValidator sections");
13459
- return {
13460
- name: options.name ?? "required-sections",
13461
- validate: (input) => {
13462
- const missing = sections.filter((section) => !input.text.includes(section));
13463
- return missing.length === 0 ? ok : {
13464
- ok: false,
13465
- reasons: missing.map((section) => `required section '${section}' is missing`)
13466
- };
13157
+ const current = () => als.getStore() ?? rootState;
13158
+ const capsOf = (ref) => {
13159
+ const colon = ref.indexOf(":");
13160
+ const adapterId = ref.slice(0, colon);
13161
+ const adapter = internals.adapters.get(adapterId);
13162
+ if (adapter === void 0) {
13163
+ const registered = [...internals.adapters.keys()].sort();
13164
+ throw new ConfigError(`no adapter registered for '${adapterId}' (ModelRef '${ref}'); registered: ${registered.length === 0 ? "(none)" : registered.join(", ")}. Pass the adapter to createEngine, or route this role to a registered adapter through defaults.routing`);
13467
13165
  }
13166
+ return adapter.caps(ref.slice(colon + 1));
13468
13167
  };
13469
- }
13470
- /**
13471
- * Requires the result to be a JSON object carrying every named field
13472
- * with a substantial value: present, not null, and not an empty or
13473
- * whitespace only string (empty arrays, zero, and false COUNT as
13474
- * present; emptiness rules beyond strings belong to a custom
13475
- * validator). Default name 'required-fields'.
13476
- */
13477
- function requiredFieldsValidator(options) {
13478
- const fields = requireNonEmptyStrings(options.fields, "requiredFieldsValidator fields");
13479
- return {
13480
- name: options.name ?? "required-fields",
13481
- validate: (input) => {
13482
- const result = input.result;
13483
- if (typeof result !== "object" || result === null || Array.isArray(result)) return {
13484
- ok: false,
13485
- reasons: ["the finish result is not a JSON object"]
13486
- };
13487
- const record = result;
13488
- const reasons = [];
13489
- for (const field of fields) {
13490
- const value = record[field];
13491
- if (value === void 0 || value === null) reasons.push(`required field '${field}' is missing`);
13492
- else if (typeof value === "string" && value.trim().length === 0) reasons.push(`required field '${field}' is empty`);
13493
- }
13494
- return reasons.length === 0 ? ok : {
13495
- ok: false,
13496
- reasons
13497
- };
13498
- }
13168
+ const adapterOf = (resolved) => {
13169
+ const adapter = internals.adapters.get(resolved.adapterId);
13170
+ if (adapter === void 0) throw new ConfigError(`no adapter registered for '${resolved.adapterId}'`);
13171
+ return adapter;
13499
13172
  };
13500
- }
13501
- /** The default citation shape: a path with an extension, a colon, a line number. */
13502
- const DEFAULT_CITATION_PATTERN = "[\\w./-]+\\.\\w+:\\d+";
13503
- /** The default preserved share, the improvement plan's RV-202 gate. */
13504
- const DEFAULT_EVIDENCE_MIN_SHARE = .95;
13505
- const MAX_LISTED_CITATIONS = 20;
13506
- function listCitations(values) {
13507
- return values.length <= MAX_LISTED_CITATIONS ? values.join(", ") : `${values.slice(0, MAX_LISTED_CITATIONS).join(", ")} and ${String(values.length - MAX_LISTED_CITATIONS)} more`;
13508
- }
13509
- /**
13510
- * The RV-202 evidence preservation contract: the finish result must
13511
- * PRESERVE the citations the children actually produced. Distinct
13512
- * matches of `pattern` are collected across the outputs of children
13513
- * settled 'ok' (spawn order); at least `minShare` of them (default
13514
- * {@link DEFAULT_EVIDENCE_MIN_SHARE}, the plan's 95 percent gate,
13515
- * compared as a ceiling on the required count so an exact boundary like
13516
- * 19 of 20 passes) must appear literally in the result text. Zero child
13517
- * citations pass vacuously. With `requireKnown: true` the contract also
13518
- * runs in reverse: every citation in the RESULT must appear in some
13519
- * child's output, so a fabricated but pattern valid citation is
13520
- * rejected instead of silently counting as evidence. Rejection reasons
13521
- * list the missing (and unknown) citations, capped at 20, so the repair
13522
- * turn can restore them. Purely textual and deterministic; checking
13523
- * that cited targets EXIST on disk is host territory (a custom
13524
- * validator), not this contract. Default name 'evidence-preserved'.
13525
- */
13526
- function evidencePreservedValidator(options) {
13527
- const pattern = options?.pattern ?? "[\\w./-]+\\.\\w+:\\d+";
13528
- const flags = options?.flags ?? "";
13529
- const globalFlags = flags.includes("g") ? flags : `${flags}g`;
13530
- try {
13531
- new RegExp(pattern, globalFlags);
13532
- } catch (thrown) {
13533
- throw new ConfigError(`evidencePreservedValidator pattern does not compile: ${thrown instanceof Error ? thrown.message : String(thrown)}`);
13173
+ function randValue(subtype, generate, key) {
13174
+ const state = current();
13175
+ const identity = key === void 0 ? {
13176
+ kind: "rand",
13177
+ subtype
13178
+ } : {
13179
+ kind: "rand",
13180
+ subtype,
13181
+ key
13182
+ };
13183
+ const matched = internals.replayer.match(state.scope, identity, "scoped");
13184
+ if (matched.kind === "replay") return matched.terminal.value.value;
13185
+ const value = generate();
13186
+ const payload = {
13187
+ subtype,
13188
+ value
13189
+ };
13190
+ if (key !== void 0) payload.key = key;
13191
+ internals.replayer.appendSinglePhase({
13192
+ scope: state.scope,
13193
+ key: deriveContentKey(identity),
13194
+ kind: "rand",
13195
+ status: "ok",
13196
+ spanId: state.spanId,
13197
+ value: payload
13198
+ });
13199
+ return value;
13534
13200
  }
13535
- const minShare = options?.minShare ?? .95;
13536
- if (typeof minShare !== "number" || !Number.isFinite(minShare) || minShare <= 0 || minShare > 1) throw new ConfigError(`evidencePreservedValidator minShare must be a number in (0, 1]; got ${String(minShare)}`);
13537
- return {
13538
- name: options?.name ?? "evidence-preserved",
13539
- validate: (input) => {
13540
- const cited = /* @__PURE__ */ new Set();
13541
- for (const child of input.children ?? []) {
13542
- if (child.status !== "ok") continue;
13543
- for (const match of child.text.match(new RegExp(pattern, globalFlags)) ?? []) cited.add(match);
13544
- }
13545
- const reasons = [];
13546
- if (cited.size > 0) {
13547
- const missing = [...cited].filter((citation) => !input.text.includes(citation));
13548
- const preserved = cited.size - missing.length;
13549
- if (preserved < Math.ceil(minShare * cited.size - 1e-9)) reasons.push(`evidence preservation ${String(preserved)} of ${String(cited.size)} child citations is below the required share ${String(minShare)}; missing: ${listCitations(missing)}`);
13550
- }
13551
- if (options?.requireKnown === true) {
13552
- const fabricated = [...new Set(input.text.match(new RegExp(pattern, globalFlags)) ?? [])].filter((citation) => !cited.has(citation));
13553
- if (fabricated.length > 0) reasons.push(`unknown citations not present in any child report: ${listCitations(fabricated)}`);
13554
- }
13555
- return reasons.length === 0 ? ok : {
13556
- ok: false,
13557
- reasons
13558
- };
13201
+ async function agentImpl(prompt, opts = {}) {
13202
+ const state = current();
13203
+ const agentType = opts.agentType ?? "";
13204
+ let profile;
13205
+ if (opts.agentType !== void 0) {
13206
+ profile = internals.defaults.profiles?.[opts.agentType];
13207
+ if (profile === void 0) throw new ConfigError(`unknown agentType '${opts.agentType}': register it under defaults.profiles`);
13559
13208
  }
13560
- };
13561
- }
13562
- /**
13563
- * Requires at least `min` matches of `pattern` in the result text (the
13564
- * plan's citation and source count checks: a file:line pattern, a URL
13565
- * pattern). The pattern compiles at construction (invalid patterns are a
13566
- * ConfigError before any run exists) and matches globally; `min` is a
13567
- * positive integer. Default name 'min-matches'; pass `name` to run
13568
- * several instances, because names must be unique per orchestrate call.
13569
- */
13570
- function minMatchesValidator(options) {
13571
- const flags = options.flags ?? "";
13572
- const globalFlags = flags.includes("g") ? flags : `${flags}g`;
13573
- try {
13574
- new RegExp(options.pattern, globalFlags);
13575
- } catch (thrown) {
13576
- throw new ConfigError(`minMatchesValidator pattern does not compile: ${thrown instanceof Error ? thrown.message : String(thrown)}`);
13577
- }
13578
- if (!Number.isInteger(options.min) || options.min < 1) throw new ConfigError(`minMatchesValidator min must be a positive integer; got ${String(options.min)}`);
13579
- return {
13580
- name: options.name ?? "min-matches",
13581
- validate: (input) => {
13582
- const found = input.text.match(new RegExp(options.pattern, globalFlags))?.length ?? 0;
13583
- return found >= options.min ? ok : {
13584
- ok: false,
13585
- reasons: [`expected at least ${String(options.min)} matches of /${options.pattern}/${flags}; found ${String(found)}`]
13586
- };
13587
- }
13588
- };
13589
- }
13590
- //#endregion
13591
- //#region src/orchestrator/handles.ts
13592
- /**
13593
- * The committed WakeDigest render budget (Appendix A: 400
13594
- * chars per outputSummary row, the character measure; committed at M10
13595
- * entry by adopting the implemented distillation cap unchanged, the
13596
- * value frozen into every cassette since M6). One value serves both
13597
- * stages: the deterministic distillation cap here and the digest
13598
- * render default in orchestrate (renderBudgetChars).
13599
- */
13600
- const WAKE_SUMMARY_RENDER_BUDGET_CHARS = 400;
13601
- /**
13602
- * The M6 outputSummary: a deterministic truncation of the child's
13603
- * output (or error message), identical live and on replay (distillation
13604
- * lives with the child, ordered by
13605
- * spawn ordinal; the LLM distillation upgrade is M7 territory).
13606
- */
13607
- function summarizeOutput(result) {
13608
- let raw;
13609
- if (result.status === "ok") raw = typeof result.output === "string" ? result.output : JSON.stringify(result.output ?? null);
13610
- else {
13611
- raw = result.errorMessage ?? `terminal status ${result.status}`;
13612
- if (result.partial !== void 0) raw = `${raw}; partial: ${JSON.stringify(result.partial)}`;
13613
- }
13614
- return truncateToBudget(raw, 400);
13615
- }
13616
- /** Folds one settled child into its digest (spawn-ordinal ordering is the caller's). */
13617
- function digestOf(record, result) {
13618
- return {
13619
- nodeId: record.nodeId,
13620
- logicalTaskId: record.logicalTaskId,
13621
- status: result.status,
13622
- outputSummary: summarizeOutput(result),
13623
- costUsd: result.costUsd,
13624
- artifactsIndex: (result.artifacts ?? []).map((artifact) => artifact.id)
13625
- };
13626
- }
13627
- //#endregion
13628
- //#region src/orchestrator/wake.ts
13629
- /** The wait_for_events parameter schema (normative). */
13630
- const WAIT_FOR_EVENTS_SCHEMA = {
13631
- type: "object",
13632
- additionalProperties: false,
13633
- required: ["triggers"],
13634
- properties: { triggers: {
13635
- type: "array",
13636
- minItems: 1,
13637
- items: { oneOf: [
13638
- {
13639
- type: "object",
13640
- additionalProperties: false,
13641
- required: ["kind"],
13642
- properties: { kind: { const: "quiescence" } }
13643
- },
13644
- {
13645
- type: "object",
13646
- additionalProperties: false,
13647
- required: ["kind"],
13648
- properties: {
13649
- kind: { const: "child_terminal" },
13650
- handles: {
13651
- type: "array",
13652
- items: {
13653
- type: "integer",
13654
- minimum: 1
13655
- }
13209
+ const runFallbackAttempt = async (targetRef, trigger, decisionSpanId) => {
13210
+ const fallback = opts.fallback;
13211
+ if (internals.replayer.snapshot().find((entry) => {
13212
+ if (entry.kind !== "decision") return false;
13213
+ const value = entry.value;
13214
+ return value?.decisionType === "model.fallback" && value.targetRef === targetRef;
13215
+ }) === void 0) {
13216
+ internals.events.emit({
13217
+ type: "log",
13218
+ level: "warn",
13219
+ msg: `model.fallback: re-attempting on ${fallback.model} after ${trigger}`
13220
+ }, decisionSpanId);
13221
+ await internals.replayer.appendSinglePhase({
13222
+ scope: state.scope,
13223
+ key: "",
13224
+ kind: "decision",
13225
+ status: "ok",
13226
+ spanId: decisionSpanId,
13227
+ value: {
13228
+ decisionType: "model.fallback",
13229
+ targetRef,
13230
+ trigger,
13231
+ model: fallback.model,
13232
+ ...internals.pricingVersion === void 0 ? {} : { pricingVersion: internals.pricingVersion }
13656
13233
  }
13657
- }
13658
- },
13659
- {
13660
- type: "object",
13661
- additionalProperties: false,
13662
- required: ["kind"],
13663
- properties: { kind: { const: "escalation" } }
13664
- },
13665
- {
13666
- type: "object",
13667
- additionalProperties: false,
13668
- required: ["kind", "percent"],
13669
- properties: {
13670
- kind: { const: "budget_threshold" },
13671
- percent: { enum: [50, 80] }
13672
- }
13234
+ });
13673
13235
  }
13674
- ] }
13675
- } }
13676
- };
13677
- const WAIT_FOR_EVENTS_TOOL_NAME = "wait_for_events";
13678
- /** The all-zero blocks of runs without the PlanRunner extension. */
13679
- function emptyDigestBlocks() {
13680
- return {
13681
- planHash: "",
13682
- termination: {
13683
- revisionUnitsRemaining: 0,
13684
- spawnUnitsRemaining: 0,
13685
- perLineage: {},
13686
- phi: 0
13687
- },
13688
- budget: {
13689
- runSpentUsd: 0,
13690
- runCeilingUsd: 0,
13691
- orchestratorSpentUsd: 0,
13692
- orchestratorCapUsd: 0,
13693
- finalizeReserveUsd: 0,
13694
- orchestratorShare: 0,
13695
- softWarning: false
13696
- },
13697
- reuse: {
13698
- abandonedUsd: 0,
13699
- reclaimedUsd: 0,
13700
- netLostUsd: 0
13701
- }
13702
- };
13703
- }
13704
- //#endregion
13705
- //#region src/orchestrator/spawn-tools.ts
13706
- /** The spawn_agent parameter schema (normative). */
13707
- const SPAWN_AGENT_SCHEMA = {
13708
- type: "object",
13709
- additionalProperties: false,
13710
- required: ["agentType", "prompt"],
13711
- properties: {
13712
- agentType: { type: "string" },
13713
- prompt: { type: "string" },
13714
- outputSchemaRef: { type: "string" },
13715
- toolsetRef: { type: "string" },
13716
- budgetUsd: {
13717
- type: "number",
13718
- exclusiveMinimum: 0
13719
- },
13720
- model_hint: {
13721
- type: "object",
13722
- additionalProperties: false,
13723
- properties: { startTier: {
13724
- type: "integer",
13725
- minimum: 0
13726
- } }
13727
- },
13728
- approach: {
13729
- type: "string",
13730
- maxLength: 64
13731
- },
13732
- lineage: {
13733
- type: "object",
13734
- additionalProperties: false,
13735
- required: ["continues", "causeRef"],
13736
- properties: {
13737
- continues: {
13738
- type: "string",
13739
- description: "LogicalTaskId to continue"
13740
- },
13741
- relation: { enum: [
13742
- "respawn",
13743
- "rung-retry",
13744
- "decompose-child",
13745
- "unpark-restart"
13746
- ] },
13747
- causeRef: {
13748
- type: "integer",
13749
- minimum: 1,
13750
- description: "seq of the journal entry that caused the rebirth"
13751
- }
13236
+ const { fallback: _fallback, routing: _routing, ...rest } = opts;
13237
+ return agentImpl(prompt, {
13238
+ ...rest,
13239
+ model: fallback.model
13240
+ });
13241
+ };
13242
+ const isolation = opts.isolation ?? profile?.isolation ?? "none";
13243
+ if (typeof isolation === "object" && isolation.kind === "worktree" && internals.isolation === void 0) throw new ConfigError("worktree isolation requires an IsolationProvider: pass defaults.isolation to createEngine");
13244
+ const floorContext = {
13245
+ ...internals.floors === void 0 ? {} : { floors: internals.floors },
13246
+ ...profile?.taskClass === void 0 ? {} : { taskClass: profile.taskClass }
13247
+ };
13248
+ const callLayer = {};
13249
+ if (opts.model !== void 0) callLayer.model = opts.model;
13250
+ if (opts.routing !== void 0) callLayer.routing = opts.routing;
13251
+ if (opts.effort !== void 0) callLayer.effort = opts.effort;
13252
+ const profileLayer = {};
13253
+ if (profile?.model !== void 0) profileLayer.model = profile.model;
13254
+ if (profile?.routing !== void 0) profileLayer.routing = profile.routing;
13255
+ if (profile?.effort !== void 0) profileLayer.effort = profile.effort;
13256
+ const engineLayer = {};
13257
+ if (internals.defaults.routing !== void 0) engineLayer.routing = internals.defaults.routing;
13258
+ const workflowLayer = state.workflowLayer;
13259
+ const telemetryNamespace = { agentType };
13260
+ if (opts.label !== void 0) telemetryNamespace.label = opts.label;
13261
+ const withTelemetry = (resolved) => ({
13262
+ ...resolved,
13263
+ providerOptions: {
13264
+ ...resolved.providerOptions,
13265
+ rulvar: telemetryNamespace
13752
13266
  }
13753
- },
13754
- taskClass: { type: "string" }
13755
- }
13756
- };
13757
- /** parallel_agents wraps the spawn_agent params. */
13758
- const PARALLEL_AGENTS_SCHEMA = {
13759
- type: "object",
13760
- additionalProperties: false,
13761
- required: ["tasks"],
13762
- properties: { tasks: {
13763
- type: "array",
13764
- minItems: 1,
13765
- items: { $ref: "#/$defs/spawnAgentParams" }
13766
- } },
13767
- $defs: { spawnAgentParams: SPAWN_AGENT_SCHEMA }
13768
- };
13769
- /** await_any and await_all share one parameter shape. */
13770
- const AWAIT_SCHEMA = {
13771
- type: "object",
13772
- additionalProperties: false,
13773
- required: ["handles"],
13774
- properties: { handles: {
13775
- type: "array",
13776
- minItems: 1,
13777
- items: {
13778
- type: "integer",
13779
- minimum: 1
13267
+ });
13268
+ const primaryRole = opts.role ?? "loop";
13269
+ const loopResolved = withTelemetry(resolveModelInvocation({
13270
+ role: primaryRole,
13271
+ call: callLayer,
13272
+ profile: profileLayer,
13273
+ workflow: workflowLayer,
13274
+ engine: engineLayer,
13275
+ capsOf,
13276
+ ...floorContext
13277
+ }));
13278
+ for (const scrub of loopResolved.scrubs) internals.events.emit({
13279
+ type: "log",
13280
+ level: "warn",
13281
+ msg: scrub.detail
13282
+ }, state.spanId);
13283
+ let canonicalSchema;
13284
+ let derivedSchemaHash = EMPTY_SCHEMA_HASH;
13285
+ if (opts.schema !== void 0) {
13286
+ canonicalSchema = canonicalizeSchema(projectToJsonSchema(opts.schema));
13287
+ derivedSchemaHash = schemaHash(canonicalSchema);
13780
13288
  }
13781
- } }
13782
- };
13783
- /** The cancel_agent parameter schema. */
13784
- const CANCEL_AGENT_SCHEMA = {
13785
- type: "object",
13786
- additionalProperties: false,
13787
- required: ["handle"],
13788
- properties: {
13789
- handle: {
13790
- type: "integer",
13791
- minimum: 1
13792
- },
13793
- reason: { type: "string" }
13794
- }
13795
- };
13796
- /** Default and hard-max characters per child-result / artifact page. */
13797
- const DEFAULT_CHILD_RESULT_PAGE_CHARS = 4e3;
13798
- const MAX_CHILD_RESULT_PAGE_CHARS = 2e4;
13799
- const PAGING_PROPS = {
13800
- offset: {
13801
- type: "integer",
13802
- minimum: 0
13803
- },
13804
- maxChars: {
13805
- type: "integer",
13806
- minimum: 1
13807
- }
13808
- };
13809
- const GET_CHILD_RESULT_SCHEMA = {
13810
- type: "object",
13811
- additionalProperties: false,
13812
- required: ["handle"],
13813
- properties: {
13814
- handle: {
13815
- type: "integer",
13816
- minimum: 1
13817
- },
13818
- ...PAGING_PROPS
13819
- }
13820
- };
13821
- const READ_CHILD_ARTIFACT_SCHEMA = {
13822
- type: "object",
13823
- additionalProperties: false,
13824
- required: ["handle", "artifactId"],
13825
- properties: {
13826
- handle: {
13827
- type: "integer",
13828
- minimum: 1
13829
- },
13830
- artifactId: { type: "string" },
13831
- ...PAGING_PROPS
13832
- }
13833
- };
13834
- const GET_CHILD_RESULT_TOOL_NAME = "get_child_result";
13835
- const READ_CHILD_ARTIFACT_TOOL_NAME = "read_child_artifact";
13836
- /** finish; result validates against the declared output schema. */
13837
- const FINISH_SCHEMA = {
13838
- type: "object",
13839
- additionalProperties: false,
13840
- required: ["result"],
13841
- properties: {
13842
- result: { $comment: "validated against the declared output SchemaSpec of the orchestrate call; free-form JSON when none is declared" },
13843
- summary: { type: "string" }
13844
- }
13845
- };
13846
- const FINISH_TOOL_NAME = "finish";
13847
- /**
13848
- * Builds the mode (c) toolset over the per-call runtime. profileCardText
13849
- * rides the spawn tools' descriptions so both modes speak one agent
13850
- * vocabulary (M6-T04).
13851
- */
13852
- function buildOrchestratorTools(runtime, profileCardText, options) {
13853
- const spawnAgent = tool({
13854
- name: "spawn_agent",
13855
- description: `Admit and schedule one child agent. ${profileCardText}`,
13856
- parameters: SPAWN_AGENT_SCHEMA,
13857
- execute: (input) => runtime.spawn(input)
13858
- });
13859
- const parallelAgents = tool({
13860
- name: "parallel_agents",
13861
- description: "Admit and schedule several children at once (submission order).",
13862
- parameters: PARALLEL_AGENTS_SCHEMA,
13863
- execute: async (input) => {
13864
- const tasks = input.tasks;
13865
- const handles = [];
13866
- for (const task of tasks) {
13867
- const spawned = await runtime.spawn(task);
13868
- handles.push(spawned.handle);
13869
- }
13870
- return { handles };
13289
+ const escalation = opts.escalation ?? profile?.escalation;
13290
+ if (escalation !== void 0) {
13291
+ if (escalation.flavor === "B" && escalation.deadlineMs === void 0) throw new ConfigError("flavor 'B' escalation requires an explicit deadlineMs: the suspension deadline has no engine default");
13292
+ if (escalation.deadlineMs !== void 0) requirePositiveInteger(escalation.deadlineMs, "escalation.deadlineMs");
13293
+ if (escalation.minSpendUsd !== void 0) requireNonNegativeNumber(escalation.minSpendUsd, "escalation.minSpendUsd");
13294
+ if (opts.result !== "full" && internals.onEscalation === void 0) throw new ConfigError("a spawn that opts into escalation from a plain value-form call needs an onEscalation hook (or use result: 'full')");
13295
+ if (escalation.flavor === "B" && escalation.deadlineMs === void 0) throw new ConfigError("escalation flavor 'B' requires an explicit deadlineMs");
13871
13296
  }
13872
- });
13873
- const awaitAny = tool({
13874
- name: "await_any",
13875
- description: "Wait for the FIRST of the handles to settle; returns its TaskDigest.",
13876
- parameters: AWAIT_SCHEMA,
13877
- execute: (input) => runtime.awaitAny(input.handles)
13878
- });
13879
- const awaitAll = tool({
13880
- name: "await_all",
13881
- description: "Wait for ALL handles to settle; returns their TaskDigests in handle order.",
13882
- parameters: AWAIT_SCHEMA,
13883
- execute: (input) => runtime.awaitAll(input.handles)
13884
- });
13885
- const cancelAgent = tool({
13886
- name: "cancel_agent",
13887
- description: "Cancel an in-flight child. Cancellation is caller intent: the entry journals cancelled and reruns on a later resume unless covered by abandon (M7).",
13888
- parameters: CANCEL_AGENT_SCHEMA,
13889
- execute: (input) => {
13890
- const params = input;
13891
- return runtime.cancel(params.handle, params.reason);
13297
+ const declaredTools = opts.tools ?? profile?.tools ?? [];
13298
+ const toolset = await resolveToolset(escalation === void 0 ? declaredTools : [...declaredTools, escalateTool()], { runId: internals.runId }, internals.defaults.toolsets, internals.executors === void 0 ? void 0 : new Set(Object.keys(internals.executors)));
13299
+ const layers = [
13300
+ callLayer,
13301
+ profileLayer,
13302
+ engineLayer
13303
+ ];
13304
+ const toolsAvailable = toolset.contracts.length > 0;
13305
+ const finalizeRouted = roleConfiguredInRouting("finalize", layers);
13306
+ let extract;
13307
+ if (opts.schema !== void 0 && canonicalSchema !== void 0) {
13308
+ const extractResolved = withTelemetry(resolveModelInvocation({
13309
+ role: "extract",
13310
+ call: callLayer,
13311
+ profile: profileLayer,
13312
+ workflow: workflowLayer,
13313
+ engine: engineLayer,
13314
+ capsOf,
13315
+ ...floorContext
13316
+ }));
13317
+ const loopTier = selectStructuredOutputTier(capsOf(loopResolved.ref), canonicalSchema);
13318
+ if (needsSeparateExtract({
13319
+ schemaSet: true,
13320
+ loopRef: loopResolved.ref,
13321
+ extractRef: extractResolved.ref,
13322
+ loopTier,
13323
+ toolsAvailable,
13324
+ finalizeRouted
13325
+ })) {
13326
+ extract = {
13327
+ adapter: adapterOf(extractResolved),
13328
+ resolved: extractResolved
13329
+ };
13330
+ for (const scrub of extractResolved.scrubs) internals.events.emit({
13331
+ type: "log",
13332
+ level: "warn",
13333
+ msg: scrub.detail
13334
+ }, state.spanId);
13335
+ }
13892
13336
  }
13893
- });
13894
- const waitForEvents = tool({
13895
- name: WAIT_FOR_EVENTS_TOOL_NAME,
13896
- description: "Sleep until a coalesced WakeDigest: quiescence (always armed), child_terminal, escalation, or budget_threshold at 50/80 percent. A trigger set that can never fire is a typed error.",
13897
- parameters: WAIT_FOR_EVENTS_SCHEMA,
13898
- execute: (input) => runtime.waitForEvents(input.triggers)
13899
- });
13900
- const finish = tool({
13901
- name: FINISH_TOOL_NAME,
13902
- description: "Terminate the orchestration with a result (run outcome ok).",
13903
- parameters: FINISH_SCHEMA,
13904
- execute: () => {
13905
- throw new Error("finish is intercepted by the agent runtime, never executed");
13337
+ let finalize;
13338
+ if (finalizeFires({
13339
+ routed: finalizeRouted,
13340
+ toolsAvailable
13341
+ })) {
13342
+ const finalizeResolved = withTelemetry(resolveModelInvocation({
13343
+ role: "finalize",
13344
+ call: callLayer,
13345
+ profile: profileLayer,
13346
+ workflow: workflowLayer,
13347
+ engine: engineLayer,
13348
+ capsOf,
13349
+ ...floorContext
13350
+ }));
13351
+ finalize = {
13352
+ adapter: adapterOf(finalizeResolved),
13353
+ resolved: finalizeResolved
13354
+ };
13355
+ for (const scrub of finalizeResolved.scrubs) internals.events.emit({
13356
+ type: "log",
13357
+ level: "warn",
13358
+ msg: scrub.detail
13359
+ }, state.spanId);
13906
13360
  }
13907
- });
13908
- const tools = [
13909
- spawnAgent,
13910
- parallelAgents,
13911
- awaitAny,
13912
- awaitAll,
13913
- cancelAgent,
13914
- waitForEvents
13915
- ];
13916
- if (options?.childResultTools === true) tools.push(tool({
13917
- name: GET_CHILD_RESULT_TOOL_NAME,
13918
- description: "Read a page of a SETTLED child's FULL output (the digest is truncated to 400 chars). Pages with offset and maxChars; the reply reports totalChars and hasMore.",
13919
- parameters: GET_CHILD_RESULT_SCHEMA,
13920
- execute: (input) => {
13921
- const p = input;
13922
- return runtime.getChildResult(p.handle, {
13923
- offset: p.offset,
13924
- maxChars: p.maxChars
13361
+ let summarizeResolved;
13362
+ try {
13363
+ summarizeResolved = resolveModelInvocation({
13364
+ role: "summarize",
13365
+ call: callLayer,
13366
+ profile: profileLayer,
13367
+ workflow: workflowLayer,
13368
+ engine: engineLayer,
13369
+ capsOf,
13370
+ ...floorContext
13371
+ });
13372
+ } catch {
13373
+ summarizeResolved = resolveModelInvocation({
13374
+ role: "summarize",
13375
+ call: callLayer,
13376
+ profile: profileLayer,
13377
+ workflow: workflowLayer,
13378
+ engine: {
13379
+ ...engineLayer,
13380
+ model: loopResolved.ref
13381
+ },
13382
+ capsOf,
13383
+ ...floorContext
13925
13384
  });
13926
13385
  }
13927
- }), tool({
13928
- name: READ_CHILD_ARTIFACT_TOOL_NAME,
13929
- description: "Read a page of a SETTLED child's artifact content by id (ids come from get_child_result or a digest). Pages with offset and maxChars.",
13930
- parameters: READ_CHILD_ARTIFACT_SCHEMA,
13931
- execute: (input) => {
13932
- const p = input;
13933
- return runtime.readChildArtifact(p.handle, p.artifactId, {
13934
- offset: p.offset,
13935
- maxChars: p.maxChars
13936
- });
13937
- }
13938
- }));
13939
- tools.push(finish);
13940
- return tools;
13941
- }
13942
- //#endregion
13943
- //#region src/engine/internal.ts
13944
- /** Registered by createCtx; keyed by the ctx object identity. */
13945
- const ctxRuntimes = /* @__PURE__ */ new WeakMap();
13946
- /**
13947
- * Internal AgentOpts channel (M6-T07): agentImpl reports the agent
13948
- * dispatch seq (the spawn handle) through this symbol-keyed callback on
13949
- * the running append, on a dangling redispatch, AND on the replay
13950
- * branch, so the orchestrator learns handles that are stable across
13951
- * resume. Never part of the public AgentOpts surface.
13952
- */
13953
- const kOnRunning = Symbol("rulvar.onRunning");
13954
- /**
13955
- * Internal AgentOpts channel (M6-T07): names the terminal tool whose
13956
- * accepted call ends the loop with status ok (the orchestrator finish
13957
- * tool), plus the optional host validation hook over the accepted call
13958
- * (the RV-204 finish validators). Never part of the public AgentOpts
13959
- * surface.
13960
- */
13961
- const kTerminalTool = Symbol("rulvar.terminalTool");
13962
- /**
13963
- * Internal AgentOpts channel (M7-T08): a transcript checkpoint ref the
13964
- * fresh dispatch boots from (park/unpark continuation and the DEF-5
13965
- * graft boot). Dangling redispatch checkpoints take precedence.
13966
- */
13967
- const kBootCheckpoint = Symbol("rulvar.bootCheckpoint");
13968
- /**
13969
- * Internal AgentOpts channel: marks the orchestrator forced-finish
13970
- * dispatch, whose spend draws from the released finalize reserve
13971
- * (DEF-7). Settlement stamps the flag into the terminal's cost
13972
- * attribution so the journal fold reproduces reserveUsedUsd.
13973
- */
13974
- const kFinalizeReserve = Symbol("rulvar.finalizeReserve");
13975
- /** Typed accessor used by the in-package consumers. */
13976
- function runtimeOf(ctx) {
13977
- const runtime = ctxRuntimes.get(ctx);
13978
- if (runtime === void 0) throw new Error("ctx runtime missing: the ctx value was not created by createCtx (engine run context)");
13979
- return runtime;
13980
- }
13981
- //#endregion
13982
- //#region src/engine/spawn-events.ts
13983
- function emitSpawnAdmitted(events, input) {
13984
- events.emit({
13985
- type: "spawn:admitted",
13986
- entryRef: input.entryRef,
13987
- verdict: input.verdict,
13988
- agentType: input.agentType,
13989
- logicalTaskId: input.logicalTaskId,
13990
- ...input.spawnUnitsAfter === void 0 ? {} : { spawnUnitsAfter: input.spawnUnitsAfter }
13991
- }, input.spanId, input.replayed);
13992
- }
13993
- function emitSpawnRejected(events, input) {
13994
- events.emit({
13995
- type: "spawn:rejected",
13996
- ...input.entryRef === void 0 ? {} : { entryRef: input.entryRef },
13997
- code: input.code,
13998
- agentType: input.agentType
13999
- }, input.spanId, input.replayed);
14000
- }
14001
- //#endregion
14002
- //#region src/l0/long-timer.ts
14003
- /**
14004
- * Sliced timers for absolute wall-clock deadlines (v1.34.0 review P2-2).
14005
- *
14006
- * Node clamps a setTimeout delay above 2147483647 ms (about 24.8 days)
14007
- * to 1 ms, so a naive timer for a far-future deadline fires immediately.
14008
- * setLongTimeout never hands Node more than one MAX_TIMER_DELAY_MS
14009
- * slice, re-checks the wall clock when a slice fires, and re-arms until
14010
- * the clock actually reaches the deadline: firing a slice is never taken
14011
- * as proof the deadline arrived. A deadline already in the past fires on
14012
- * the next macrotask (delay 0), matching the plain setTimeout behavior
14013
- * the callers had for near deadlines.
14014
- */
14015
- /**
14016
- * Schedules `onDue` for the absolute wall-clock instant `dueAtMs` as
14017
- * reported by `now` (default Date.now), slicing delays beyond the Node
14018
- * timer maximum.
14019
- */
14020
- function setLongTimeout(onDue, dueAtMs, now = Date.now) {
14021
- let handle;
14022
- let cancelled = false;
14023
- const arm = () => {
14024
- const remaining = Math.max(0, dueAtMs - now());
14025
- handle = setTimeout(() => {
14026
- if (cancelled) return;
14027
- if (now() >= dueAtMs) {
14028
- onDue();
14029
- return;
14030
- }
14031
- arm();
14032
- }, Math.min(remaining, MAX_TIMER_DELAY_MS));
14033
- };
14034
- arm();
14035
- return { cancel: () => {
14036
- cancelled = true;
14037
- if (handle !== void 0) clearTimeout(handle);
14038
- } };
14039
- }
14040
- //#endregion
14041
- //#region src/runtime/executor.ts
14042
- /**
14043
- * Isolated-executor dispatch helpers (RV-216). The engine routes a
14044
- * non-inprocess tool call through the registered ToolExecutorProvider;
14045
- * this module derives the stable per-call idempotency key the provider
14046
- * receives, so an at-least-once retry of a side-effecting tool can be
14047
- * folded into effectively-once.
14048
- *
14049
- * Public contract: https://docs.rulvar.com/guide/isolated-executor.
14050
- */
14051
- /**
14052
- * Derives the idempotency key for one isolated tool dispatch. The key is
14053
- * a pure function of the run, the LOGICAL INVOCATION (the seq of the
14054
- * containing agent's journal entry plus that call's ordinal within the
14055
- * agent's tool loop), the tool name, and the JCS-canonical arguments.
14056
- *
14057
- * The logical-invocation component is what makes the key both stable and
14058
- * distinguishing (v1.59.x review P0.4): the agent-entry seq and the
14059
- * per-agent tool-call ordinal are journal- and checkpoint-stable, so a
14060
- * crash-and-resume re-dispatch of the SAME logical call (the at-least-
14061
- * once window between execution and the turn checkpoint) reuses the same
14062
- * dispatch entry and the restored ordinal, and therefore the same key;
14063
- * while two SEPARATE calls in one run, even with byte-identical
14064
- * arguments, occupy different ordinals and never collide. Without it two
14065
- * intended effects sharing arguments would fold into one under external
14066
- * deduplication.
14067
- *
14068
- * The key never enters run identity (it is absent from every content key
14069
- * and toolset hash); it exists only for the provider's own side-effect
14070
- * deduplication.
14071
- */
14072
- function deriveExecIdempotencyKey(runId, agentSeq, ordinal, tool, args) {
14073
- const canonical = jcsSerialize({
14074
- runId,
14075
- agentSeq,
14076
- ordinal,
14077
- tool,
14078
- args
14079
- });
14080
- return createHash("sha256").update(canonical, "utf8").digest("hex");
14081
- }
14082
- //#endregion
14083
- //#region src/engine/ctx.ts
14084
- /**
14085
- * Ctx primitives (M1-T07) plus the parallel/pipeline composition semantics
14086
- * of the scheduler (M1-T08): the canonical authoring surface bound to the
14087
- * journal write path, the model router, the agent runtime, and the
14088
- * three-layer budget. M1 ships agent, parallel, pipeline, step, phase,
14089
- * log, budget, and the deterministic shims; workflow/orchestrate/
14090
- * awaitExternal/brief land with their milestones (M2/M6).
14091
- *
14092
- * Public contract: https://docs.rulvar.com/guide/workflows.
14093
- */
14094
- /**
14095
- * The rejection carrier of ctx.agent value-form calls: a real Error that
14096
- * structurally satisfies the typed AgentError and carries the full
14097
- * AgentResult for Settled mapping. Deliberately not a RulvarError:
14098
- * AgentError is not in the closed code registry.
14099
- */
14100
- var AgentCallError = class extends Error {
14101
- kind;
14102
- retryable;
14103
- retryAfterMs;
14104
- issues;
14105
- result;
14106
- scope;
14107
- entryRef;
14108
- constructor(message, result, scope, entryRef) {
14109
- super(message);
14110
- this.name = "AgentCallError";
14111
- const error = result.error ?? {
14112
- kind: "terminal",
14113
- retryable: false
13386
+ const summarize = {
13387
+ adapter: adapterOf(summarizeResolved),
13388
+ resolved: withTelemetry(summarizeResolved)
14114
13389
  };
14115
- this.kind = error.kind;
14116
- this.retryable = error.retryable;
14117
- if (error.retryAfterMs !== void 0) this.retryAfterMs = error.retryAfterMs;
14118
- if (error.issues !== void 0) this.issues = error.issues;
14119
- this.result = result;
14120
- this.scope = scope;
14121
- if (entryRef !== void 0) this.entryRef = entryRef;
14122
- }
14123
- };
14124
- /**
14125
- * Projects a settled AgentResult's error to its wire form, carrying the
14126
- * engine-decided abort class in data. AgentError itself has no data
14127
- * field, so without this every projection past the terminal entry (the
14128
- * run-level outcome.error, thrown AgentCallError wires, dropped items)
14129
- * would keep only the message text and lose the typed class (v1.9.0
14130
- * follow-up review).
14131
- */
14132
- function agentResultWire(result, fallbackMessage) {
14133
- const wire = agentErrorToWire(result.error ?? {
14134
- kind: "terminal",
14135
- retryable: false
14136
- }, result.errorMessage ?? fallbackMessage);
14137
- if (result.abortClass === void 0) return wire;
14138
- const data = typeof wire.data === "object" && wire.data !== null && !Array.isArray(wire.data) ? wire.data : {};
14139
- return {
14140
- ...wire,
14141
- data: {
14142
- ...data,
14143
- abortClass: result.abortClass
13390
+ const failoverChainFor = (role, resolved) => (resolved.fallbacks ?? []).map((ref) => {
13391
+ const fallbackLayer = { model: ref };
13392
+ if (callLayer.effort !== void 0) fallbackLayer.effort = callLayer.effort;
13393
+ const fallbackResolved = withTelemetry(resolveModelInvocation({
13394
+ role,
13395
+ call: fallbackLayer,
13396
+ profile: profileLayer,
13397
+ workflow: workflowLayer,
13398
+ engine: engineLayer,
13399
+ capsOf,
13400
+ ...floorContext
13401
+ }));
13402
+ return {
13403
+ adapter: adapterOf(fallbackResolved),
13404
+ resolved: fallbackResolved
13405
+ };
13406
+ });
13407
+ const loopFallbacks = failoverChainFor(primaryRole, loopResolved);
13408
+ if (extract !== void 0) {
13409
+ const chain = failoverChainFor("extract", extract.resolved);
13410
+ if (chain.length > 0) extract.fallbacks = chain;
14144
13411
  }
14145
- };
14146
- }
14147
- /** The workflow-defaults layer a Workflow value contributes, or nothing. */
14148
- function workflowLayerOf(wf) {
14149
- const layer = {};
14150
- if (wf.model !== void 0) layer.model = wf.model;
14151
- if (wf.routing !== void 0) layer.routing = wf.routing;
14152
- if (wf.effort !== void 0) layer.effort = wf.effort;
14153
- return Object.keys(layer).length === 0 ? void 0 : layer;
14154
- }
14155
- function defineWorkflow(meta, body) {
14156
- const wf = {
14157
- kind: "workflow",
14158
- name: meta.name,
14159
- errorPolicy: meta.errorPolicy ?? "strict",
14160
- ...meta.model === void 0 ? {} : { model: meta.model },
14161
- ...meta.routing === void 0 ? {} : { routing: meta.routing },
14162
- ...meta.effort === void 0 ? {} : { effort: meta.effort },
14163
- body
14164
- };
14165
- if (meta.args !== void 0) return {
14166
- ...wf,
14167
- argsSchema: meta.args
14168
- };
14169
- return wf;
14170
- }
14171
- function bump(map, key, usd) {
14172
- map.set(key, (map.get(key) ?? 0) + usd);
14173
- }
14174
- /**
14175
- * Completes a model-authored escalation request into the full report:
14176
- * costToDate and salvage are runtime-filled, never model-filled. The
14177
- * worktree patch ref lands after collect(); the pre-dispose preview for
14178
- * flavor B decision-makers omits it.
14179
- */
14180
- function buildEscalationReport(request, result, worktreePatchRef) {
14181
- return {
14182
- kind: request.kind,
14183
- scopeDelta: request.scopeDelta,
14184
- revisedEstimate: request.revisedEstimate,
14185
- blockers: request.blockers ?? [],
14186
- proposedDecomposition: request.proposedDecomposition ?? [],
14187
- costToDate: {
14188
- usd: result.costUsd,
14189
- turns: result.turns
14190
- },
14191
- salvage: {
14192
- transcriptRef: result.transcriptRef,
14193
- artifacts: (result.artifacts ?? []).map((artifact) => artifact.id),
14194
- ...worktreePatchRef === void 0 ? {} : { worktreePatchRef }
13412
+ if (finalize !== void 0) {
13413
+ const chain = failoverChainFor("finalize", finalize.resolved);
13414
+ if (chain.length > 0) finalize.fallbacks = chain;
14195
13415
  }
14196
- };
14197
- }
14198
- /**
14199
- * Creates the per-run Ctx bound to `internals`. The current scope travels
14200
- * through AsyncLocalStorage so parallel branches and pipeline stages keep
14201
- * one ctx object while journaling under their own scope paths (I3:
14202
- * structure from call-and-return only).
14203
- */
14204
- function createCtx(internals, rootWorkflow) {
14205
- const als = new AsyncLocalStorage();
14206
- const sites = new ParallelSiteCounter();
14207
- const rootWorkflowLayer = rootWorkflow === void 0 ? void 0 : workflowLayerOf(rootWorkflow);
14208
- const rootState = {
14209
- scope: "",
14210
- spanId: internals.rootSpanId,
14211
- ...rootWorkflowLayer === void 0 ? {} : { workflowLayer: rootWorkflowLayer }
14212
- };
14213
- const current = () => als.getStore() ?? rootState;
14214
- const capsOf = (ref) => {
14215
- const colon = ref.indexOf(":");
14216
- const adapterId = ref.slice(0, colon);
14217
- const adapter = internals.adapters.get(adapterId);
14218
- if (adapter === void 0) {
14219
- const registered = [...internals.adapters.keys()].sort();
14220
- throw new ConfigError(`no adapter registered for '${adapterId}' (ModelRef '${ref}'); registered: ${registered.length === 0 ? "(none)" : registered.join(", ")}. Pass the adapter to createEngine, or route this role to a registered adapter through defaults.routing`);
13416
+ {
13417
+ const chain = failoverChainFor("summarize", summarizeResolved);
13418
+ if (chain.length > 0) summarize.fallbacks = chain;
14221
13419
  }
14222
- return adapter.caps(ref.slice(colon + 1));
14223
- };
14224
- const adapterOf = (resolved) => {
14225
- const adapter = internals.adapters.get(resolved.adapterId);
14226
- if (adapter === void 0) throw new ConfigError(`no adapter registered for '${resolved.adapterId}'`);
14227
- return adapter;
14228
- };
14229
- function randValue(subtype, generate, key) {
14230
- const state = current();
14231
- const identity = key === void 0 ? {
14232
- kind: "rand",
14233
- subtype
14234
- } : {
14235
- kind: "rand",
14236
- subtype,
14237
- key
14238
- };
14239
- const matched = internals.replayer.match(state.scope, identity, "scoped");
14240
- if (matched.kind === "replay") return matched.terminal.value.value;
14241
- const value = generate();
14242
- const payload = {
14243
- subtype,
14244
- value
13420
+ const retryPolicy = opts.retry ?? profile?.retry ?? internals.defaults.retry;
13421
+ if (retryPolicy !== void 0) validateRetryPolicy(retryPolicy, opts.retry !== void 0 ? "the agent retry option" : profile?.retry !== void 0 ? `the retry of profile '${String(opts.agentType)}'` : "engine defaults.retry");
13422
+ if (opts.estCost !== void 0) requireNonNegativeNumber(opts.estCost, "the agent estCost option");
13423
+ if (opts.limits !== void 0) validateUsageLimits(opts.limits, "the agent limits option");
13424
+ const identityInput = {
13425
+ kind: "agent",
13426
+ agentType,
13427
+ modelSpec: loopResolved.canonical,
13428
+ prompt: opts.key ?? prompt,
13429
+ schemaHash: derivedSchemaHash,
13430
+ toolsetHash: toolset.hash,
13431
+ isolation
14245
13432
  };
14246
- if (key !== void 0) payload.key = key;
14247
- internals.replayer.appendSinglePhase({
14248
- scope: state.scope,
14249
- key: deriveContentKey(identity),
14250
- kind: "rand",
14251
- status: "ok",
14252
- spanId: state.spanId,
14253
- value: payload
14254
- });
14255
- return value;
14256
- }
14257
- async function agentImpl(prompt, opts = {}) {
14258
- const state = current();
14259
- const agentType = opts.agentType ?? "";
14260
- let profile;
14261
- if (opts.agentType !== void 0) {
14262
- profile = internals.defaults.profiles?.[opts.agentType];
14263
- if (profile === void 0) throw new ConfigError(`unknown agentType '${opts.agentType}': register it under defaults.profiles`);
14264
- }
14265
- const runFallbackAttempt = async (targetRef, trigger, decisionSpanId) => {
14266
- const fallback = opts.fallback;
14267
- if (internals.replayer.snapshot().find((entry) => {
14268
- if (entry.kind !== "decision") return false;
14269
- const value = entry.value;
14270
- return value?.decisionType === "model.fallback" && value.targetRef === targetRef;
14271
- }) === void 0) {
14272
- internals.events.emit({
14273
- type: "log",
14274
- level: "warn",
14275
- msg: `model.fallback: re-attempting on ${fallback.model} after ${trigger}`
14276
- }, decisionSpanId);
14277
- await internals.replayer.appendSinglePhase({
14278
- scope: state.scope,
14279
- key: "",
14280
- kind: "decision",
14281
- status: "ok",
14282
- spanId: decisionSpanId,
14283
- value: {
14284
- decisionType: "model.fallback",
14285
- targetRef,
14286
- trigger,
14287
- model: fallback.model,
14288
- ...internals.pricingVersion === void 0 ? {} : { pricingVersion: internals.pricingVersion }
13433
+ const identityKey = deriveContentKey(identityInput);
13434
+ const matched = internals.replayer.match(state.scope, identityInput, opts.replay ?? "scoped");
13435
+ if (matched.kind === "replay" || matched.kind === "skip") {
13436
+ opts[kOnRunning]?.(matched.running.seq);
13437
+ const terminal = matched.kind === "replay" ? matched.terminal : matched.terminal;
13438
+ const spanId = internals.spans.mint(state.spanId);
13439
+ const usage = terminal?.usage ?? {
13440
+ inputTokens: 0,
13441
+ outputTokens: 0,
13442
+ cacheReadTokens: 0,
13443
+ cacheWriteTokens: 0
13444
+ };
13445
+ const replayPriced = terminal === void 0 ? void 0 : priceEntryUsage(terminal, (ref, sliceUsage) => internals.priceUsd(ref, sliceUsage));
13446
+ const costUsd = replayPriced?.usd ?? 0;
13447
+ const result = {
13448
+ status: matched.kind === "skip" ? "skipped" : terminal?.status ?? "ok",
13449
+ output: matched.kind === "skip" ? null : terminal?.value ?? null,
13450
+ usage,
13451
+ costUsd,
13452
+ servedBy: terminal?.servedBy ?? loopResolved.ref,
13453
+ turns: 0,
13454
+ transcriptRef: terminal?.transcriptRef ?? ""
13455
+ };
13456
+ if (terminal?.error !== void 0) {
13457
+ result.error = agentErrorFromWire(terminal.error);
13458
+ result.errorMessage = terminal.error.message;
13459
+ }
13460
+ if (terminal?.artifacts !== void 0) result.artifacts = terminal.artifacts;
13461
+ if (terminal?.providerCalls !== void 0) result.providerCalls = terminal.providerCalls;
13462
+ if (terminal?.status === "escalated" && terminal.escalation !== void 0) result.escalation = terminal.escalation;
13463
+ {
13464
+ const stampedData = terminal?.error?.data;
13465
+ if (stampedData?.abortClass !== void 0) result.abortClass = stampedData.abortClass;
13466
+ if (stampedData?.exploration !== void 0) result.exploration = stampedData.exploration;
13467
+ }
13468
+ let replayedToolResults = [];
13469
+ if (matched.kind === "replay" && terminal?.checkpointRef !== void 0) {
13470
+ const blob = await internals.transcripts.get(terminal.checkpointRef);
13471
+ const checkpoint = blob === null ? void 0 : decodeCheckpoint(blob);
13472
+ if (checkpoint !== void 0) {
13473
+ result.turns = checkpoint.turns;
13474
+ if (result.status === "limit") {
13475
+ const partialReport = latestProgressReport(checkpoint.messages);
13476
+ if (partialReport !== void 0) result.partial = partialReport;
14289
13477
  }
14290
- });
13478
+ replayedToolResults = checkpoint.messages.filter((msg) => msg.role === "tool").flatMap((msg) => msg.parts).filter((part) => part.type === "tool-result").map((part) => ({
13479
+ name: part.name,
13480
+ isError: part.isError === true
13481
+ }));
13482
+ }
14291
13483
  }
14292
- const { fallback: _fallback, routing: _routing, ...rest } = opts;
14293
- return agentImpl(prompt, {
14294
- ...rest,
14295
- model: fallback.model
14296
- });
14297
- };
14298
- const isolation = opts.isolation ?? profile?.isolation ?? "none";
14299
- if (typeof isolation === "object" && isolation.kind === "worktree" && internals.isolation === void 0) throw new ConfigError("worktree isolation requires an IsolationProvider: pass defaults.isolation to createEngine");
14300
- const floorContext = {
14301
- ...internals.floors === void 0 ? {} : { floors: internals.floors },
14302
- ...profile?.taskClass === void 0 ? {} : { taskClass: profile.taskClass }
14303
- };
14304
- const callLayer = {};
14305
- if (opts.model !== void 0) callLayer.model = opts.model;
14306
- if (opts.routing !== void 0) callLayer.routing = opts.routing;
14307
- if (opts.effort !== void 0) callLayer.effort = opts.effort;
14308
- const profileLayer = {};
14309
- if (profile?.model !== void 0) profileLayer.model = profile.model;
14310
- if (profile?.routing !== void 0) profileLayer.routing = profile.routing;
14311
- if (profile?.effort !== void 0) profileLayer.effort = profile.effort;
14312
- const engineLayer = {};
14313
- if (internals.defaults.routing !== void 0) engineLayer.routing = internals.defaults.routing;
14314
- const workflowLayer = state.workflowLayer;
14315
- const telemetryNamespace = { agentType };
14316
- if (opts.label !== void 0) telemetryNamespace.label = opts.label;
14317
- const withTelemetry = (resolved) => ({
14318
- ...resolved,
14319
- providerOptions: {
14320
- ...resolved.providerOptions,
14321
- rulvar: telemetryNamespace
14322
- }
14323
- });
14324
- const primaryRole = opts.role ?? "loop";
14325
- const loopResolved = withTelemetry(resolveModelInvocation({
14326
- role: primaryRole,
14327
- call: callLayer,
14328
- profile: profileLayer,
14329
- workflow: workflowLayer,
14330
- engine: engineLayer,
14331
- capsOf,
14332
- ...floorContext
14333
- }));
14334
- for (const scrub of loopResolved.scrubs) internals.events.emit({
14335
- type: "log",
14336
- level: "warn",
14337
- msg: scrub.detail
14338
- }, state.spanId);
14339
- let canonicalSchema;
14340
- let derivedSchemaHash = EMPTY_SCHEMA_HASH;
14341
- if (opts.schema !== void 0) {
14342
- canonicalSchema = canonicalizeSchema(projectToJsonSchema(opts.schema));
14343
- derivedSchemaHash = schemaHash(canonicalSchema);
14344
- }
14345
- const escalation = opts.escalation ?? profile?.escalation;
14346
- if (escalation !== void 0) {
14347
- if (escalation.flavor === "B" && escalation.deadlineMs === void 0) throw new ConfigError("flavor 'B' escalation requires an explicit deadlineMs: the suspension deadline has no engine default");
14348
- if (escalation.deadlineMs !== void 0) requirePositiveInteger(escalation.deadlineMs, "escalation.deadlineMs");
14349
- if (escalation.minSpendUsd !== void 0) requireNonNegativeNumber(escalation.minSpendUsd, "escalation.minSpendUsd");
14350
- if (opts.result !== "full" && internals.onEscalation === void 0) throw new ConfigError("a spawn that opts into escalation from a plain value-form call needs an onEscalation hook (or use result: 'full')");
14351
- if (escalation.flavor === "B" && escalation.deadlineMs === void 0) throw new ConfigError("escalation flavor 'B' requires an explicit deadlineMs");
14352
- }
14353
- const declaredTools = opts.tools ?? profile?.tools ?? [];
14354
- const toolset = await resolveToolset(escalation === void 0 ? declaredTools : [...declaredTools, escalateTool()], { runId: internals.runId }, internals.defaults.toolsets, internals.executors === void 0 ? void 0 : new Set(Object.keys(internals.executors)));
14355
- const layers = [
14356
- callLayer,
14357
- profileLayer,
14358
- engineLayer
14359
- ];
14360
- const toolsAvailable = toolset.contracts.length > 0;
14361
- const finalizeRouted = roleConfiguredInRouting("finalize", layers);
14362
- let extract;
14363
- if (opts.schema !== void 0 && canonicalSchema !== void 0) {
14364
- const extractResolved = withTelemetry(resolveModelInvocation({
14365
- role: "extract",
14366
- call: callLayer,
14367
- profile: profileLayer,
14368
- workflow: workflowLayer,
14369
- engine: engineLayer,
14370
- capsOf,
14371
- ...floorContext
14372
- }));
14373
- const loopTier = selectStructuredOutputTier(capsOf(loopResolved.ref), canonicalSchema);
14374
- if (needsSeparateExtract({
14375
- schemaSet: true,
14376
- loopRef: loopResolved.ref,
14377
- extractRef: extractResolved.ref,
14378
- loopTier,
14379
- toolsAvailable,
14380
- finalizeRouted
14381
- })) {
14382
- extract = {
14383
- adapter: adapterOf(extractResolved),
14384
- resolved: extractResolved
14385
- };
14386
- for (const scrub of extractResolved.scrubs) internals.events.emit({
14387
- type: "log",
14388
- level: "warn",
14389
- msg: scrub.detail
14390
- }, state.spanId);
14391
- }
14392
- }
14393
- let finalize;
14394
- if (finalizeFires({
14395
- routed: finalizeRouted,
14396
- toolsAvailable
14397
- })) {
14398
- const finalizeResolved = withTelemetry(resolveModelInvocation({
14399
- role: "finalize",
14400
- call: callLayer,
14401
- profile: profileLayer,
14402
- workflow: workflowLayer,
14403
- engine: engineLayer,
14404
- capsOf,
14405
- ...floorContext
14406
- }));
14407
- finalize = {
14408
- adapter: adapterOf(finalizeResolved),
14409
- resolved: finalizeResolved
14410
- };
14411
- for (const scrub of finalizeResolved.scrubs) internals.events.emit({
14412
- type: "log",
14413
- level: "warn",
14414
- msg: scrub.detail
14415
- }, state.spanId);
14416
- }
14417
- let summarizeResolved;
14418
- try {
14419
- summarizeResolved = resolveModelInvocation({
14420
- role: "summarize",
14421
- call: callLayer,
14422
- profile: profileLayer,
14423
- workflow: workflowLayer,
14424
- engine: engineLayer,
14425
- capsOf,
14426
- ...floorContext
14427
- });
14428
- } catch {
14429
- summarizeResolved = resolveModelInvocation({
14430
- role: "summarize",
14431
- call: callLayer,
14432
- profile: profileLayer,
14433
- workflow: workflowLayer,
14434
- engine: {
14435
- ...engineLayer,
14436
- model: loopResolved.ref
14437
- },
14438
- capsOf,
14439
- ...floorContext
14440
- });
14441
- }
14442
- const summarize = {
14443
- adapter: adapterOf(summarizeResolved),
14444
- resolved: withTelemetry(summarizeResolved)
14445
- };
14446
- const failoverChainFor = (role, resolved) => (resolved.fallbacks ?? []).map((ref) => {
14447
- const fallbackLayer = { model: ref };
14448
- if (callLayer.effort !== void 0) fallbackLayer.effort = callLayer.effort;
14449
- const fallbackResolved = withTelemetry(resolveModelInvocation({
14450
- role,
14451
- call: fallbackLayer,
14452
- profile: profileLayer,
14453
- workflow: workflowLayer,
14454
- engine: engineLayer,
14455
- capsOf,
14456
- ...floorContext
14457
- }));
14458
- return {
14459
- adapter: adapterOf(fallbackResolved),
14460
- resolved: fallbackResolved
14461
- };
14462
- });
14463
- const loopFallbacks = failoverChainFor(primaryRole, loopResolved);
14464
- if (extract !== void 0) {
14465
- const chain = failoverChainFor("extract", extract.resolved);
14466
- if (chain.length > 0) extract.fallbacks = chain;
14467
- }
14468
- if (finalize !== void 0) {
14469
- const chain = failoverChainFor("finalize", finalize.resolved);
14470
- if (chain.length > 0) finalize.fallbacks = chain;
14471
- }
14472
- {
14473
- const chain = failoverChainFor("summarize", summarizeResolved);
14474
- if (chain.length > 0) summarize.fallbacks = chain;
14475
- }
14476
- const retryPolicy = opts.retry ?? profile?.retry ?? internals.defaults.retry;
14477
- if (retryPolicy !== void 0) validateRetryPolicy(retryPolicy, opts.retry !== void 0 ? "the agent retry option" : profile?.retry !== void 0 ? `the retry of profile '${String(opts.agentType)}'` : "engine defaults.retry");
14478
- if (opts.estCost !== void 0) requireNonNegativeNumber(opts.estCost, "the agent estCost option");
14479
- if (opts.limits !== void 0) validateUsageLimits(opts.limits, "the agent limits option");
14480
- const identityInput = {
14481
- kind: "agent",
14482
- agentType,
14483
- modelSpec: loopResolved.canonical,
14484
- prompt: opts.key ?? prompt,
14485
- schemaHash: derivedSchemaHash,
14486
- toolsetHash: toolset.hash,
14487
- isolation
14488
- };
14489
- const identityKey = deriveContentKey(identityInput);
14490
- const matched = internals.replayer.match(state.scope, identityInput, opts.replay ?? "scoped");
14491
- if (matched.kind === "replay" || matched.kind === "skip") {
14492
- opts[kOnRunning]?.(matched.running.seq);
14493
- const terminal = matched.kind === "replay" ? matched.terminal : matched.terminal;
14494
- const spanId = internals.spans.mint(state.spanId);
14495
- const usage = terminal?.usage ?? {
14496
- inputTokens: 0,
14497
- outputTokens: 0,
14498
- cacheReadTokens: 0,
14499
- cacheWriteTokens: 0
14500
- };
14501
- const replayPriced = terminal === void 0 ? void 0 : priceEntryUsage(terminal, (ref, sliceUsage) => internals.priceUsd(ref, sliceUsage));
14502
- const costUsd = replayPriced?.usd ?? 0;
14503
- const result = {
14504
- status: matched.kind === "skip" ? "skipped" : terminal?.status ?? "ok",
14505
- output: matched.kind === "skip" ? null : terminal?.value ?? null,
14506
- usage,
14507
- costUsd,
14508
- servedBy: terminal?.servedBy ?? loopResolved.ref,
14509
- turns: 0,
14510
- transcriptRef: terminal?.transcriptRef ?? ""
14511
- };
14512
- if (terminal?.error !== void 0) {
14513
- result.error = agentErrorFromWire(terminal.error);
14514
- result.errorMessage = terminal.error.message;
14515
- }
14516
- if (terminal?.artifacts !== void 0) result.artifacts = terminal.artifacts;
14517
- if (terminal?.providerCalls !== void 0) result.providerCalls = terminal.providerCalls;
14518
- if (terminal?.status === "escalated" && terminal.escalation !== void 0) result.escalation = terminal.escalation;
14519
- {
14520
- const stampedData = terminal?.error?.data;
14521
- if (stampedData?.abortClass !== void 0) result.abortClass = stampedData.abortClass;
14522
- if (stampedData?.exploration !== void 0) result.exploration = stampedData.exploration;
14523
- }
14524
- let replayedToolResults = [];
14525
- if (matched.kind === "replay" && terminal?.checkpointRef !== void 0) {
14526
- const blob = await internals.transcripts.get(terminal.checkpointRef);
14527
- const checkpoint = blob === null ? void 0 : decodeCheckpoint(blob);
14528
- if (checkpoint !== void 0) {
14529
- result.turns = checkpoint.turns;
14530
- if (result.status === "limit") {
14531
- const partialReport = latestProgressReport(checkpoint.messages);
14532
- if (partialReport !== void 0) result.partial = partialReport;
14533
- }
14534
- replayedToolResults = checkpoint.messages.filter((msg) => msg.role === "tool").flatMap((msg) => msg.parts).filter((part) => part.type === "tool-result").map((part) => ({
14535
- name: part.name,
14536
- isError: part.isError === true
14537
- }));
14538
- }
14539
- }
14540
- internals.events.emit({
14541
- type: "agent:start",
14542
- agentType,
14543
- label: opts.label,
14544
- model: loopResolved.ref,
14545
- role: primaryRole
14546
- }, spanId, true);
14547
- for (const toolResult of replayedToolResults) {
14548
- internals.events.emit({
14549
- type: "tool:start",
14550
- toolName: toolResult.name
14551
- }, spanId, true);
14552
- internals.events.emit({
14553
- type: "tool:end",
14554
- toolName: toolResult.name,
14555
- outcome: toolResult.isError ? "error" : "ok",
14556
- durationMs: 0
14557
- }, spanId, true);
13484
+ internals.events.emit({
13485
+ type: "agent:start",
13486
+ agentType,
13487
+ label: opts.label,
13488
+ model: loopResolved.ref,
13489
+ role: primaryRole
13490
+ }, spanId, true);
13491
+ for (const toolResult of replayedToolResults) {
13492
+ internals.events.emit({
13493
+ type: "tool:start",
13494
+ toolName: toolResult.name
13495
+ }, spanId, true);
13496
+ internals.events.emit({
13497
+ type: "tool:end",
13498
+ toolName: toolResult.name,
13499
+ outcome: toolResult.isError ? "error" : "ok",
13500
+ durationMs: 0
13501
+ }, spanId, true);
14558
13502
  }
14559
13503
  if (terminal !== void 0) entryUsageSlices(terminal).forEach((slice, index) => {
14560
13504
  const priced = internals.priceUsd(slice.servedBy, slice.usage) ?? 0;
@@ -15724,216 +14668,592 @@ function dedupeRepeatedClaims(rows) {
15724
14668
  };
15725
14669
  }
15726
14670
  //#endregion
15727
- //#region src/orchestrator/orchestrate.ts
14671
+ //#region src/orchestrator/handles.ts
15728
14672
  /**
15729
- * The mode (c) dynamic orchestrator (M6-T07/T08).
15730
- *
15731
- * Full contract: https://docs.rulvar.com/guide/adaptive-orchestration. An ordinary
15732
- * workflow whose agent (role 'orchestrate') holds the typed spawn tools;
15733
- * both surfaces (top-level orchestrate() and ctx.orchestrate) share this
15734
- * one implementation, the nested surface riding ctx.workflow so the
15735
- * AdmissionController clamps depth and budget for free.
15736
- *
15737
- * Resume semantics (the M6 gate): orchestrator turns checkpoint at every
15738
- * turn boundary (mandatory for the orchestrate role); every spawn is an
15739
- * ordinary kind 'agent' entry; a crashed orchestrate() restores its
15740
- * history from the checkpoint and finds child results by content keys,
15741
- * WITHOUT regenerating spawn decisions and without re-paying children.
15742
- * Non-PlanRunner applicability: only the lifetime
15743
- * cap, maxDepth, and the budget layers apply; no termination.init is
15744
- * written; escalated children simply settle into their digests.
14673
+ * The committed WakeDigest render budget (Appendix A: 400
14674
+ * chars per outputSummary row, the character measure; committed at M10
14675
+ * entry by adopting the implemented distillation cap unchanged, the
14676
+ * value frozen into every cassette since M6). One value serves both
14677
+ * stages: the deterministic distillation cap here and the digest
14678
+ * render default in orchestrate (renderBudgetChars).
15745
14679
  */
15746
- /** How many rejected finishes are repaired by default: the plan's repair once. */
15747
- const DEFAULT_FINISH_MAX_REPAIRS = 1;
14680
+ const WAKE_SUMMARY_RENDER_BUDGET_CHARS = 400;
15748
14681
  /**
15749
- * Default maxTurns of the synthesize invocation (RV-211): the finish
15750
- * call plus headroom for one validator repair exchange.
14682
+ * The M6 outputSummary: a deterministic truncation of the child's
14683
+ * output (or error message), identical live and on replay (distillation
14684
+ * lives with the child, ordered by
14685
+ * spawn ordinal; the LLM distillation upgrade is M7 territory).
15751
14686
  */
15752
- const DEFAULT_SYNTHESIS_MAX_TURNS = 4;
15753
- /**
15754
- * Default maxTurns of ONE incremental synthesis note (RV-211 remainder):
15755
- * a note summarizes a single settled child into a bounded finish call,
15756
- * so it needs less headroom than the full synthesis invocation.
15757
- */
15758
- const DEFAULT_SYNTHESIS_NOTE_MAX_TURNS = 2;
15759
- const ORCHESTRATE_WORKFLOW_NAME = "rulvar-orchestrate";
15760
- /**
15761
- * One page of a string, for the child result evidence tools: maxChars is
15762
- * clamped to [1, MAX] and offset to [0, length], so a hostile or absent
15763
- * paging argument can never throw or read past the end. The window is measured in
15764
- * UTF-16 code units, the same unit the model counts, so hasMore and the
15765
- * next offset are exact.
15766
- */
15767
- function pageOf(content, rawOffset, rawMaxChars) {
15768
- const totalChars = content.length;
15769
- const offset = Math.min(Math.max(0, Math.trunc(rawOffset ?? 0)), totalChars);
15770
- const end = Math.min(offset + Math.min(Math.max(1, Math.trunc(rawMaxChars ?? 4e3)), MAX_CHILD_RESULT_PAGE_CHARS), totalChars);
14687
+ function summarizeOutput(result) {
14688
+ let raw;
14689
+ if (result.status === "ok") raw = typeof result.output === "string" ? result.output : JSON.stringify(result.output ?? null);
14690
+ else {
14691
+ raw = result.errorMessage ?? `terminal status ${result.status}`;
14692
+ if (result.status === "limit" && result.output !== null && result.output !== void 0) {
14693
+ const final = typeof result.output === "string" ? result.output : JSON.stringify(result.output);
14694
+ raw = `${raw}; final: ${final}`;
14695
+ }
14696
+ if (result.partial !== void 0) raw = `${raw}; partial: ${JSON.stringify(result.partial)}`;
14697
+ }
14698
+ return truncateToBudget(raw, 400);
14699
+ }
14700
+ /** Folds one settled child into its digest (spawn-ordinal ordering is the caller's). */
14701
+ function digestOf(record, result) {
15771
14702
  return {
15772
- totalChars,
15773
- offset,
15774
- content: content.slice(offset, end),
15775
- hasMore: end < totalChars
14703
+ nodeId: record.nodeId,
14704
+ logicalTaskId: record.logicalTaskId,
14705
+ status: result.status,
14706
+ outputSummary: summarizeOutput(result),
14707
+ costUsd: result.costUsd,
14708
+ artifactsIndex: (result.artifacts ?? []).map((artifact) => artifact.id)
15776
14709
  };
15777
14710
  }
15778
- /** The serialized full result of a settled child: the raw string, or JSON. */
15779
- function serializeChildOutput(result) {
15780
- if (result.status !== "ok") {
15781
- const base = result.errorMessage ?? `terminal status ${result.status}`;
15782
- if (result.partial !== void 0) return JSON.stringify({
15783
- error: base,
15784
- partial: result.partial
15785
- });
15786
- return base;
15787
- }
15788
- return typeof result.output === "string" ? result.output : JSON.stringify(result.output ?? null);
14711
+ //#endregion
14712
+ //#region src/orchestrator/wake.ts
14713
+ /** The wait_for_events parameter schema (normative). */
14714
+ const WAIT_FOR_EVENTS_SCHEMA = {
14715
+ type: "object",
14716
+ additionalProperties: false,
14717
+ required: ["triggers"],
14718
+ properties: { triggers: {
14719
+ type: "array",
14720
+ minItems: 1,
14721
+ items: { oneOf: [
14722
+ {
14723
+ type: "object",
14724
+ additionalProperties: false,
14725
+ required: ["kind"],
14726
+ properties: { kind: { const: "quiescence" } }
14727
+ },
14728
+ {
14729
+ type: "object",
14730
+ additionalProperties: false,
14731
+ required: ["kind"],
14732
+ properties: {
14733
+ kind: { const: "child_terminal" },
14734
+ handles: {
14735
+ type: "array",
14736
+ items: {
14737
+ type: "integer",
14738
+ minimum: 1
14739
+ }
14740
+ }
14741
+ }
14742
+ },
14743
+ {
14744
+ type: "object",
14745
+ additionalProperties: false,
14746
+ required: ["kind"],
14747
+ properties: { kind: { const: "escalation" } }
14748
+ },
14749
+ {
14750
+ type: "object",
14751
+ additionalProperties: false,
14752
+ required: ["kind", "percent"],
14753
+ properties: {
14754
+ kind: { const: "budget_threshold" },
14755
+ percent: { enum: [50, 80] }
14756
+ }
14757
+ }
14758
+ ] }
14759
+ } }
14760
+ };
14761
+ const WAIT_FOR_EVENTS_TOOL_NAME = "wait_for_events";
14762
+ /** The all-zero blocks of runs without the PlanRunner extension. */
14763
+ function emptyDigestBlocks() {
14764
+ return {
14765
+ planHash: "",
14766
+ termination: {
14767
+ revisionUnitsRemaining: 0,
14768
+ spawnUnitsRemaining: 0,
14769
+ perLineage: {},
14770
+ phi: 0
14771
+ },
14772
+ budget: {
14773
+ runSpentUsd: 0,
14774
+ runCeilingUsd: 0,
14775
+ orchestratorSpentUsd: 0,
14776
+ orchestratorCapUsd: 0,
14777
+ finalizeReserveUsd: 0,
14778
+ orchestratorShare: 0,
14779
+ softWarning: false
14780
+ },
14781
+ reuse: {
14782
+ abandonedUsd: 0,
14783
+ reclaimedUsd: 0,
14784
+ netLostUsd: 0
14785
+ }
14786
+ };
15789
14787
  }
15790
- /**
15791
- * The orchestrate intake gate (v1.35.0 review P2-2): every numeric
15792
- * option and the atCap literal validate SYNCHRONOUSLY at workflow
15793
- * construction, shared by both surfaces (the top level orchestrate() throws
15794
- * before a run exists; ctx.orchestrate throws before any journal entry,
15795
- * provider call, or child dispatch). A NaN here previously disabled the
15796
- * spawn cap (`spawnOrdinal >= NaN` is false forever) and the digest
15797
- * render bound, and a negative finalize reserve WIDENED the soft cap
15798
- * boundary instead of reserving from it.
15799
- */
15800
- function validateOrchestrateOptions(opts) {
15801
- if (opts === void 0) return;
15802
- if (opts.maxSpawns !== void 0) requireNonNegativeInteger(opts.maxSpawns, "orchestrate maxSpawns");
15803
- if (opts.renderBudgetChars !== void 0) requireNonNegativeInteger(opts.renderBudgetChars, "orchestrate renderBudgetChars");
15804
- if (opts.acceptance !== void 0) {
15805
- const policy = opts.acceptance.childPolicy;
15806
- const minSuccessful = typeof policy === "object" && policy !== null && !Array.isArray(policy) ? policy.minSuccessful : void 0;
15807
- if (policy !== "all-ok" && minSuccessful === void 0) throw new ConfigError(`orchestrate acceptance.childPolicy must be 'all-ok' or { minSuccessful: N }; got ${JSON.stringify(policy)}`);
15808
- if (policy !== "all-ok") requirePositiveInteger(minSuccessful, "orchestrate acceptance.childPolicy.minSuccessful");
15809
- const acceptPartial = opts.acceptance.acceptPartialChildren;
15810
- if (acceptPartial !== void 0 && typeof acceptPartial !== "boolean") throw new ConfigError(`orchestrate acceptance.acceptPartialChildren must be a boolean; got ${typeof acceptPartial}`);
14788
+ //#endregion
14789
+ //#region src/orchestrator/spawn-tools.ts
14790
+ /** The spawn_agent parameter schema (normative). */
14791
+ const SPAWN_AGENT_SCHEMA = {
14792
+ type: "object",
14793
+ additionalProperties: false,
14794
+ required: ["agentType", "prompt"],
14795
+ properties: {
14796
+ agentType: { type: "string" },
14797
+ prompt: { type: "string" },
14798
+ outputSchemaRef: { type: "string" },
14799
+ toolsetRef: { type: "string" },
14800
+ budgetUsd: {
14801
+ type: "number",
14802
+ exclusiveMinimum: 0
14803
+ },
14804
+ model_hint: {
14805
+ type: "object",
14806
+ additionalProperties: false,
14807
+ properties: { startTier: {
14808
+ type: "integer",
14809
+ minimum: 0
14810
+ } }
14811
+ },
14812
+ approach: {
14813
+ type: "string",
14814
+ maxLength: 64
14815
+ },
14816
+ lineage: {
14817
+ type: "object",
14818
+ additionalProperties: false,
14819
+ required: ["continues", "causeRef"],
14820
+ properties: {
14821
+ continues: {
14822
+ type: "string",
14823
+ description: "LogicalTaskId to continue"
14824
+ },
14825
+ relation: { enum: [
14826
+ "respawn",
14827
+ "rung-retry",
14828
+ "decompose-child",
14829
+ "unpark-restart"
14830
+ ] },
14831
+ causeRef: {
14832
+ type: "integer",
14833
+ minimum: 1,
14834
+ description: "seq of the journal entry that caused the rebirth"
14835
+ }
14836
+ }
14837
+ },
14838
+ taskClass: { type: "string" }
15811
14839
  }
15812
- if (opts.finishValidation !== void 0) {
15813
- const fv = opts.finishValidation;
15814
- if (!Array.isArray(fv.validators) || fv.validators.length === 0) throw new ConfigError("orchestrate finishValidation.validators must be a non empty array of validators");
15815
- const seen = /* @__PURE__ */ new Set();
15816
- for (const candidate of fv.validators) {
15817
- const validator = candidate;
15818
- if (typeof validator.name !== "string" || validator.name.length === 0) throw new ConfigError("every orchestrate finish validator must carry a non empty string name");
15819
- if (typeof validator.validate !== "function") throw new ConfigError(`orchestrate finish validator '${validator.name}' has no validate function`);
15820
- if (seen.has(validator.name)) throw new ConfigError(`orchestrate finishValidation.validators names must be unique; '${validator.name}' repeats (pass name to the factory to run several instances)`);
15821
- seen.add(validator.name);
14840
+ };
14841
+ /** parallel_agents wraps the spawn_agent params. */
14842
+ const PARALLEL_AGENTS_SCHEMA = {
14843
+ type: "object",
14844
+ additionalProperties: false,
14845
+ required: ["tasks"],
14846
+ properties: { tasks: {
14847
+ type: "array",
14848
+ minItems: 1,
14849
+ items: { $ref: "#/$defs/spawnAgentParams" }
14850
+ } },
14851
+ $defs: { spawnAgentParams: SPAWN_AGENT_SCHEMA }
14852
+ };
14853
+ /** await_any and await_all share one parameter shape. */
14854
+ const AWAIT_SCHEMA = {
14855
+ type: "object",
14856
+ additionalProperties: false,
14857
+ required: ["handles"],
14858
+ properties: { handles: {
14859
+ type: "array",
14860
+ minItems: 1,
14861
+ items: {
14862
+ type: "integer",
14863
+ minimum: 1
15822
14864
  }
15823
- if (fv.maxRepairs !== void 0) requireNonNegativeInteger(fv.maxRepairs, "orchestrate finishValidation.maxRepairs");
14865
+ } }
14866
+ };
14867
+ /** The cancel_agent parameter schema. */
14868
+ const CANCEL_AGENT_SCHEMA = {
14869
+ type: "object",
14870
+ additionalProperties: false,
14871
+ required: ["handle"],
14872
+ properties: {
14873
+ handle: {
14874
+ type: "integer",
14875
+ minimum: 1
14876
+ },
14877
+ reason: { type: "string" }
15824
14878
  }
15825
- if (opts.synthesis !== void 0) {
15826
- const synthesis = opts.synthesis;
15827
- 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));
15828
- 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");
15829
- if (synthesis.dedupeClaims !== void 0 && typeof synthesis.dedupeClaims !== "boolean") throw new ConfigError("orchestrate synthesis.dedupeClaims must be a boolean; got " + typeof synthesis.dedupeClaims);
15830
- if (synthesis.noteLimits !== void 0) validateUsageLimits(synthesis.noteLimits, "orchestrate synthesis.noteLimits");
15831
- if (synthesis.effort !== void 0 && ![
15832
- "low",
15833
- "medium",
15834
- "high",
15835
- "xhigh",
15836
- "max"
15837
- ].includes(synthesis.effort)) throw new ConfigError(`orchestrate synthesis.effort must be one of 'low' | 'medium' | 'high' | 'xhigh' | 'max'; got ${JSON.stringify(synthesis.effort)}`);
15838
- if (synthesis.limits !== void 0) validateUsageLimits(synthesis.limits, "orchestrate synthesis.limits");
15839
- if (synthesis.instructions !== void 0 && typeof synthesis.instructions !== "string") throw new ConfigError(`orchestrate synthesis.instructions must be a string; got ${typeof synthesis.instructions}`);
15840
- if (synthesis.estCost !== void 0) requireNonNegativeNumber(synthesis.estCost, "orchestrate synthesis.estCost");
14879
+ };
14880
+ /** Default and hard-max characters per child-result / artifact page. */
14881
+ const DEFAULT_CHILD_RESULT_PAGE_CHARS = 4e3;
14882
+ const MAX_CHILD_RESULT_PAGE_CHARS = 2e4;
14883
+ const PAGING_PROPS = {
14884
+ offset: {
14885
+ type: "integer",
14886
+ minimum: 0
14887
+ },
14888
+ maxChars: {
14889
+ type: "integer",
14890
+ minimum: 1
15841
14891
  }
15842
- const spec = opts.budget;
15843
- if (spec === void 0) return;
15844
- if (spec.capUsd !== void 0) requireNonNegativeNumber(spec.capUsd, "orchestrate budget.capUsd");
15845
- if (spec.capFraction !== void 0) requireFraction(spec.capFraction, "orchestrate budget.capFraction");
15846
- if (spec.finalizeReserveUsd !== void 0) requireNonNegativeNumber(spec.finalizeReserveUsd, "orchestrate budget.finalizeReserveUsd");
15847
- if (spec.finalizeTurns !== void 0) requirePositiveInteger(spec.finalizeTurns, "orchestrate budget.finalizeTurns");
15848
- if (spec.atCap !== void 0 && spec.atCap !== "finish-with-partial" && spec.atCap !== "fail-run") throw new ConfigError(`orchestrate budget.atCap must be 'finish-with-partial' or 'fail-run'; got ${String(spec.atCap)}`);
15849
- }
15850
- function orchestratorPrompt(goal, maxSpawns, extensionLines) {
15851
- return [
15852
- "You are the orchestrator of a multi-agent run.",
15853
- `GOAL: ${goal}`,
15854
- "",
15855
- "Decompose the goal into child agents with spawn_agent or parallel_agents,",
15856
- "wait on their handles with await_any or await_all, cancel stragglers with",
15857
- "cancel_agent, and terminate with finish({ result }) when the goal is met.",
15858
- maxSpawns === void 0 ? "Spawn only what the goal needs." : `You may spawn at most ${String(maxSpawns)} children.`,
15859
- ...extensionLines ?? []
15860
- ].join("\n");
15861
- }
14892
+ };
14893
+ const GET_CHILD_RESULT_SCHEMA = {
14894
+ type: "object",
14895
+ additionalProperties: false,
14896
+ required: ["handle"],
14897
+ properties: {
14898
+ handle: {
14899
+ type: "integer",
14900
+ minimum: 1
14901
+ },
14902
+ ...PAGING_PROPS
14903
+ }
14904
+ };
14905
+ const READ_CHILD_ARTIFACT_SCHEMA = {
14906
+ type: "object",
14907
+ additionalProperties: false,
14908
+ required: ["handle", "artifactId"],
14909
+ properties: {
14910
+ handle: {
14911
+ type: "integer",
14912
+ minimum: 1
14913
+ },
14914
+ artifactId: { type: "string" },
14915
+ ...PAGING_PROPS
14916
+ }
14917
+ };
14918
+ const GET_CHILD_RESULT_TOOL_NAME = "get_child_result";
14919
+ const READ_CHILD_ARTIFACT_TOOL_NAME = "read_child_artifact";
14920
+ /** finish; result validates against the declared output schema. */
14921
+ const FINISH_SCHEMA = {
14922
+ type: "object",
14923
+ additionalProperties: false,
14924
+ required: ["result"],
14925
+ properties: {
14926
+ result: { $comment: "validated against the declared output SchemaSpec of the orchestrate call; free-form JSON when none is declared" },
14927
+ summary: { type: "string" }
14928
+ }
14929
+ };
14930
+ const FINISH_TOOL_NAME = "finish";
15862
14931
  /**
15863
- * The finish validation contract rides the PROMPT, never the toolset:
15864
- * the finish tool definition stays byte identical in every
15865
- * configuration, so the orchestrator toolset hash never moves (stricter
15866
- * than the evidence tools opt in, which changes it by design).
14932
+ * Builds the mode (c) toolset over the per-call runtime. profileCardText
14933
+ * rides the spawn tools' descriptions so both modes speak one agent
14934
+ * vocabulary (M6-T04).
15867
14935
  */
15868
- function finishValidationPromptLines(spec) {
15869
- if (spec === void 0) return [];
15870
- const names = spec.validators.map((validator) => validator.name).join(", ");
15871
- const repairs = spec.maxRepairs ?? 1;
15872
- return [`The host validates every finish({ result }) with deterministic validators: ${names}.`, "A rejected finish returns the failure reasons as the tool error result; repair the result and call finish again. " + (repairs === 0 ? "No repair attempt is granted: the first rejected finish fails the run." : repairs === 1 ? "At most one repair attempt is granted before the run fails." : `At most ${String(repairs)} repair attempts are granted before the run fails.`)];
14936
+ function buildOrchestratorTools(runtime, profileCardText, options) {
14937
+ const spawnAgent = tool({
14938
+ name: "spawn_agent",
14939
+ description: `Admit and schedule one child agent. ${profileCardText}`,
14940
+ parameters: SPAWN_AGENT_SCHEMA,
14941
+ execute: (input) => runtime.spawn(input)
14942
+ });
14943
+ const parallelAgents = tool({
14944
+ name: "parallel_agents",
14945
+ description: "Admit and schedule several children at once (submission order).",
14946
+ parameters: PARALLEL_AGENTS_SCHEMA,
14947
+ execute: async (input) => {
14948
+ const tasks = input.tasks;
14949
+ const handles = [];
14950
+ for (const task of tasks) {
14951
+ const spawned = await runtime.spawn(task);
14952
+ handles.push(spawned.handle);
14953
+ }
14954
+ return { handles };
14955
+ }
14956
+ });
14957
+ const awaitAny = tool({
14958
+ name: "await_any",
14959
+ description: "Wait for the FIRST of the handles to settle; returns its TaskDigest.",
14960
+ parameters: AWAIT_SCHEMA,
14961
+ execute: (input) => runtime.awaitAny(input.handles)
14962
+ });
14963
+ const awaitAll = tool({
14964
+ name: "await_all",
14965
+ description: "Wait for ALL handles to settle; returns their TaskDigests in handle order.",
14966
+ parameters: AWAIT_SCHEMA,
14967
+ execute: (input) => runtime.awaitAll(input.handles)
14968
+ });
14969
+ const cancelAgent = tool({
14970
+ name: "cancel_agent",
14971
+ description: "Cancel an in-flight child. Cancellation is caller intent: the entry journals cancelled and reruns on a later resume unless covered by abandon (M7).",
14972
+ parameters: CANCEL_AGENT_SCHEMA,
14973
+ execute: (input) => {
14974
+ const params = input;
14975
+ return runtime.cancel(params.handle, params.reason);
14976
+ }
14977
+ });
14978
+ const waitForEvents = tool({
14979
+ name: WAIT_FOR_EVENTS_TOOL_NAME,
14980
+ description: "Sleep until a coalesced WakeDigest: quiescence (always armed), child_terminal, escalation, or budget_threshold at 50/80 percent. A trigger set that can never fire is a typed error.",
14981
+ parameters: WAIT_FOR_EVENTS_SCHEMA,
14982
+ execute: (input) => runtime.waitForEvents(input.triggers)
14983
+ });
14984
+ const finish = tool({
14985
+ name: FINISH_TOOL_NAME,
14986
+ description: "Terminate the orchestration with a result (run outcome ok).",
14987
+ parameters: FINISH_SCHEMA,
14988
+ execute: () => {
14989
+ throw new Error("finish is intercepted by the agent runtime, never executed");
14990
+ }
14991
+ });
14992
+ const tools = [
14993
+ spawnAgent,
14994
+ parallelAgents,
14995
+ awaitAny,
14996
+ awaitAll,
14997
+ cancelAgent,
14998
+ waitForEvents
14999
+ ];
15000
+ if (options?.childResultTools === true) tools.push(tool({
15001
+ name: GET_CHILD_RESULT_TOOL_NAME,
15002
+ description: "Read a page of a SETTLED child's FULL output (the digest is truncated to 400 chars). Pages with offset and maxChars; the reply reports totalChars and hasMore.",
15003
+ parameters: GET_CHILD_RESULT_SCHEMA,
15004
+ execute: (input) => {
15005
+ const p = input;
15006
+ return runtime.getChildResult(p.handle, {
15007
+ offset: p.offset,
15008
+ maxChars: p.maxChars
15009
+ });
15010
+ }
15011
+ }), tool({
15012
+ name: READ_CHILD_ARTIFACT_TOOL_NAME,
15013
+ description: "Read a page of a SETTLED child's artifact content by id (ids come from get_child_result or a digest). Pages with offset and maxChars.",
15014
+ parameters: READ_CHILD_ARTIFACT_SCHEMA,
15015
+ execute: (input) => {
15016
+ const p = input;
15017
+ return runtime.readChildArtifact(p.handle, p.artifactId, {
15018
+ offset: p.offset,
15019
+ maxChars: p.maxChars
15020
+ });
15021
+ }
15022
+ }));
15023
+ tools.push(finish);
15024
+ return tools;
15873
15025
  }
15026
+ //#endregion
15027
+ //#region src/orchestrator/orchestrate.ts
15874
15028
  /**
15875
- * The partial-salvage contract rides the PROMPT exactly like finish
15876
- * validation (RV-210 close-out): present only when
15877
- * acceptance.acceptPartialChildren is set, so every other configuration
15878
- * keeps byte-identical coordination prompts.
15029
+ * The mode (c) dynamic orchestrator (M6-T07/T08).
15030
+ *
15031
+ * Full contract: https://docs.rulvar.com/guide/adaptive-orchestration. An ordinary
15032
+ * workflow whose agent (role 'orchestrate') holds the typed spawn tools;
15033
+ * both surfaces (top-level orchestrate() and ctx.orchestrate) share this
15034
+ * one implementation, the nested surface riding ctx.workflow so the
15035
+ * AdmissionController clamps depth and budget for free.
15036
+ *
15037
+ * Resume semantics (the M6 gate): orchestrator turns checkpoint at every
15038
+ * turn boundary (mandatory for the orchestrate role); every spawn is an
15039
+ * ordinary kind 'agent' entry; a crashed orchestrate() restores its
15040
+ * history from the checkpoint and finds child results by content keys,
15041
+ * WITHOUT regenerating spawn decisions and without re-paying children.
15042
+ * Non-PlanRunner applicability: only the lifetime
15043
+ * cap, maxDepth, and the budget layers apply; no termination.init is
15044
+ * written; escalated children simply settle into their digests.
15879
15045
  */
15880
- function acceptancePromptLines(acceptance) {
15881
- if (acceptance?.acceptPartialChildren !== true) return [];
15882
- return ["Partial salvage is on: a child that ends at its limit AFTER recording progress with report_progress counts as a partial success for acceptance; its digest carries the partial and get_child_result (when enabled) pages the full report. When the gap matters, respawn a NARROWED child carrying the partial instead of repeating the task."];
15883
- }
15046
+ /** How many rejected finishes are repaired by default: the plan's repair once. */
15047
+ const DEFAULT_FINISH_MAX_REPAIRS = 1;
15884
15048
  /**
15885
- * Resolves per-spawn dispatch options against the engine registries
15886
- * (registered SchemaSpec and tool profile names; M7-T05). An
15887
- * unknown ref is a typed ConfigError, surfaced as a tool error to the
15888
- * orchestrator and never a run failure.
15049
+ * Default maxTurns of the synthesize invocation (RV-211): the finish
15050
+ * call plus headroom for one validator repair exchange.
15889
15051
  */
15890
- function resolveDispatchOpts(spec, defaults) {
15891
- const opts = {};
15892
- if (spec.outputSchemaRef !== void 0) {
15893
- const schema = defaults.schemas?.[spec.outputSchemaRef];
15894
- if (schema === void 0) throw new ConfigError(`unknown outputSchemaRef '${spec.outputSchemaRef}': register it under defaults.schemas`);
15895
- opts.schema = schema;
15896
- }
15897
- if (spec.toolsetRef !== void 0) {
15898
- const tools = defaults.toolsets?.[spec.toolsetRef];
15899
- if (tools === void 0) throw new ConfigError(`unknown toolsetRef '${spec.toolsetRef}': register it under defaults.toolsets (https://docs.rulvar.com/guide/tools)`);
15900
- opts.tools = tools;
15901
- }
15902
- const extended = spec;
15903
- if (extended.isolation !== void 0) opts.isolation = extended.isolation;
15904
- if (extended.usageLimits !== void 0) opts.limits = extended.usageLimits;
15905
- if (extended.escalation !== void 0) opts.escalation = extended.escalation;
15906
- if (extended.bootCheckpointRef !== void 0) opts[kBootCheckpoint] = extended.bootCheckpointRef;
15907
- if (extended.model !== void 0) opts.model = extended.model;
15908
- if (extended.memoizeOutcome !== void 0) opts.memoizeOutcome = extended.memoizeOutcome;
15909
- if (extended.schema !== void 0 && opts.schema === void 0) opts.schema = extended.schema;
15910
- return opts;
15911
- }
15912
- function filterProfiles(registered, names) {
15913
- if (registered === void 0) return {};
15914
- if (names === void 0) return registered;
15915
- const filtered = {};
15916
- for (const name of names) if (registered[name] !== void 0) filtered[name] = registered[name];
15917
- return filtered;
15918
- }
15052
+ const DEFAULT_SYNTHESIS_MAX_TURNS = 4;
15919
15053
  /**
15920
- * Builds the orchestrator workflow: ONE implementation behind both
15921
- * surfaces. The body wires the spawn tools over the per-call runtime,
15922
- * recovers spawn records from the journal on resume, and runs the
15923
- * orchestrator agent with the finish terminal tool.
15054
+ * Default maxTurns of ONE incremental synthesis note (RV-211 remainder):
15055
+ * a note summarizes a single settled child into a bounded finish call,
15056
+ * so it needs less headroom than the full synthesis invocation.
15924
15057
  */
15925
- function makeOrchestratorWorkflow(goal, opts) {
15926
- validateOrchestrateOptions(opts);
15927
- return defineWorkflow({ name: ORCHESTRATE_WORKFLOW_NAME }, async (ctx) => {
15928
- const runtime = runtimeOf(ctx);
15929
- const { internals } = runtime;
15930
- if (internals.admission === void 0) throw new ConfigError("orchestrate requires the engine run context (createEngine)");
15931
- const admission = internals.admission;
15932
- const callingState = runtime.currentState();
15933
- const advertisedProfiles = filterProfiles(internals.defaults.profiles, opts?.profiles);
15934
- const spawnableProfiles = {};
15935
- const declaredLadderNames = [];
15936
- for (const [name, profile] of Object.entries(advertisedProfiles)) {
15058
+ const DEFAULT_SYNTHESIS_NOTE_MAX_TURNS = 2;
15059
+ const ORCHESTRATE_WORKFLOW_NAME = "rulvar-orchestrate";
15060
+ /**
15061
+ * One page of a string, for the child result evidence tools: maxChars is
15062
+ * clamped to [1, MAX] and offset to [0, length], so a hostile or absent
15063
+ * paging argument can never throw or read past the end. The window is measured in
15064
+ * UTF-16 code units, the same unit the model counts, so hasMore and the
15065
+ * next offset are exact.
15066
+ */
15067
+ function pageOf(content, rawOffset, rawMaxChars) {
15068
+ const totalChars = content.length;
15069
+ const offset = Math.min(Math.max(0, Math.trunc(rawOffset ?? 0)), totalChars);
15070
+ const end = Math.min(offset + Math.min(Math.max(1, Math.trunc(rawMaxChars ?? 4e3)), MAX_CHILD_RESULT_PAGE_CHARS), totalChars);
15071
+ return {
15072
+ totalChars,
15073
+ offset,
15074
+ content: content.slice(offset, end),
15075
+ hasMore: end < totalChars
15076
+ };
15077
+ }
15078
+ /** The serialized full result of a settled child: the raw string, or JSON. */
15079
+ function serializeChildOutput(result) {
15080
+ if (result.status !== "ok") {
15081
+ const base = result.errorMessage ?? `terminal status ${result.status}`;
15082
+ const limitOutput = result.status === "limit" && result.output !== null && result.output !== void 0;
15083
+ if (limitOutput || result.partial !== void 0) return JSON.stringify({
15084
+ error: base,
15085
+ ...limitOutput ? { output: result.output } : {},
15086
+ ...result.partial === void 0 ? {} : { partial: result.partial }
15087
+ });
15088
+ return base;
15089
+ }
15090
+ return typeof result.output === "string" ? result.output : JSON.stringify(result.output ?? null);
15091
+ }
15092
+ /**
15093
+ * The orchestrate intake gate (v1.35.0 review P2-2): every numeric
15094
+ * option and the atCap literal validate SYNCHRONOUSLY at workflow
15095
+ * construction, shared by both surfaces (the top level orchestrate() throws
15096
+ * before a run exists; ctx.orchestrate throws before any journal entry,
15097
+ * provider call, or child dispatch). A NaN here previously disabled the
15098
+ * spawn cap (`spawnOrdinal >= NaN` is false forever) and the digest
15099
+ * render bound, and a negative finalize reserve WIDENED the soft cap
15100
+ * boundary instead of reserving from it.
15101
+ */
15102
+ function validateOrchestrateOptions(opts) {
15103
+ if (opts === void 0) return;
15104
+ if (opts.maxSpawns !== void 0) requireNonNegativeInteger(opts.maxSpawns, "orchestrate maxSpawns");
15105
+ if (opts.renderBudgetChars !== void 0) requireNonNegativeInteger(opts.renderBudgetChars, "orchestrate renderBudgetChars");
15106
+ if (opts.acceptance !== void 0) {
15107
+ const policy = opts.acceptance.childPolicy;
15108
+ const minSuccessful = typeof policy === "object" && policy !== null && !Array.isArray(policy) ? policy.minSuccessful : void 0;
15109
+ if (policy !== "all-ok" && minSuccessful === void 0) throw new ConfigError(`orchestrate acceptance.childPolicy must be 'all-ok' or { minSuccessful: N }; got ${JSON.stringify(policy)}`);
15110
+ if (policy !== "all-ok") requirePositiveInteger(minSuccessful, "orchestrate acceptance.childPolicy.minSuccessful");
15111
+ const acceptPartial = opts.acceptance.acceptPartialChildren;
15112
+ if (acceptPartial !== void 0 && typeof acceptPartial !== "boolean") throw new ConfigError(`orchestrate acceptance.acceptPartialChildren must be a boolean; got ${typeof acceptPartial}`);
15113
+ const acceptOutput = opts.acceptance.acceptValidatedTerminalOutputOnLimit;
15114
+ if (acceptOutput !== void 0 && typeof acceptOutput !== "boolean") throw new ConfigError("orchestrate acceptance.acceptValidatedTerminalOutputOnLimit must be a boolean; got " + typeof acceptOutput);
15115
+ }
15116
+ if (opts.finishValidation !== void 0) {
15117
+ const fv = opts.finishValidation;
15118
+ if (!Array.isArray(fv.validators) || fv.validators.length === 0) throw new ConfigError("orchestrate finishValidation.validators must be a non empty array of validators");
15119
+ const seen = /* @__PURE__ */ new Set();
15120
+ for (const candidate of fv.validators) {
15121
+ const validator = candidate;
15122
+ if (typeof validator.name !== "string" || validator.name.length === 0) throw new ConfigError("every orchestrate finish validator must carry a non empty string name");
15123
+ if (typeof validator.validate !== "function") throw new ConfigError(`orchestrate finish validator '${validator.name}' has no validate function`);
15124
+ if (seen.has(validator.name)) throw new ConfigError(`orchestrate finishValidation.validators names must be unique; '${validator.name}' repeats (pass name to the factory to run several instances)`);
15125
+ seen.add(validator.name);
15126
+ }
15127
+ if (fv.maxRepairs !== void 0) requireNonNegativeInteger(fv.maxRepairs, "orchestrate finishValidation.maxRepairs");
15128
+ }
15129
+ if (opts.synthesis !== void 0) {
15130
+ const synthesis = opts.synthesis;
15131
+ 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));
15132
+ 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");
15133
+ if (synthesis.dedupeClaims !== void 0 && typeof synthesis.dedupeClaims !== "boolean") throw new ConfigError("orchestrate synthesis.dedupeClaims must be a boolean; got " + typeof synthesis.dedupeClaims);
15134
+ if (synthesis.noteLimits !== void 0) validateUsageLimits(synthesis.noteLimits, "orchestrate synthesis.noteLimits");
15135
+ if (synthesis.effort !== void 0 && ![
15136
+ "low",
15137
+ "medium",
15138
+ "high",
15139
+ "xhigh",
15140
+ "max"
15141
+ ].includes(synthesis.effort)) throw new ConfigError(`orchestrate synthesis.effort must be one of 'low' | 'medium' | 'high' | 'xhigh' | 'max'; got ${JSON.stringify(synthesis.effort)}`);
15142
+ if (synthesis.limits !== void 0) validateUsageLimits(synthesis.limits, "orchestrate synthesis.limits");
15143
+ if (synthesis.instructions !== void 0 && typeof synthesis.instructions !== "string") throw new ConfigError(`orchestrate synthesis.instructions must be a string; got ${typeof synthesis.instructions}`);
15144
+ if (synthesis.estCost !== void 0) requireNonNegativeNumber(synthesis.estCost, "orchestrate synthesis.estCost");
15145
+ }
15146
+ const spec = opts.budget;
15147
+ if (spec === void 0) return;
15148
+ if (spec.capUsd !== void 0) requireNonNegativeNumber(spec.capUsd, "orchestrate budget.capUsd");
15149
+ if (spec.capFraction !== void 0) requireFraction(spec.capFraction, "orchestrate budget.capFraction");
15150
+ if (spec.finalizeReserveUsd !== void 0) requireNonNegativeNumber(spec.finalizeReserveUsd, "orchestrate budget.finalizeReserveUsd");
15151
+ if (spec.finalizeTurns !== void 0) requirePositiveInteger(spec.finalizeTurns, "orchestrate budget.finalizeTurns");
15152
+ if (spec.atCap !== void 0 && spec.atCap !== "finish-with-partial" && spec.atCap !== "fail-run") throw new ConfigError(`orchestrate budget.atCap must be 'finish-with-partial' or 'fail-run'; got ${String(spec.atCap)}`);
15153
+ }
15154
+ function orchestratorPrompt(goal, maxSpawns, extensionLines) {
15155
+ return [
15156
+ "You are the orchestrator of a multi-agent run.",
15157
+ `GOAL: ${goal}`,
15158
+ "",
15159
+ "Decompose the goal into child agents with spawn_agent or parallel_agents,",
15160
+ "wait on their handles with await_any or await_all, cancel stragglers with",
15161
+ "cancel_agent, and terminate with finish({ result }) when the goal is met.",
15162
+ maxSpawns === void 0 ? "Spawn only what the goal needs." : `You may spawn at most ${String(maxSpawns)} children.`,
15163
+ ...extensionLines ?? []
15164
+ ].join("\n");
15165
+ }
15166
+ /**
15167
+ * The finish validation contract rides the PROMPT, never the toolset:
15168
+ * the finish tool definition stays byte identical in every
15169
+ * configuration, so the orchestrator toolset hash never moves (stricter
15170
+ * than the evidence tools opt in, which changes it by design).
15171
+ */
15172
+ function finishValidationPromptLines(spec) {
15173
+ if (spec === void 0) return [];
15174
+ const names = spec.validators.map((validator) => validator.name).join(", ");
15175
+ const repairs = spec.maxRepairs ?? 1;
15176
+ return [`The host validates every finish({ result }) with deterministic validators: ${names}.`, "A rejected finish returns the failure reasons as the tool error result; repair the result and call finish again. " + (repairs === 0 ? "No repair attempt is granted: the first rejected finish fails the run." : repairs === 1 ? "At most one repair attempt is granted before the run fails." : `At most ${String(repairs)} repair attempts are granted before the run fails.`)];
15177
+ }
15178
+ /**
15179
+ * The partial-salvage contract rides the PROMPT exactly like finish
15180
+ * validation (RV-210 close-out): present only when
15181
+ * acceptance.acceptPartialChildren is set, so every other configuration
15182
+ * keeps byte-identical coordination prompts.
15183
+ */
15184
+ function acceptancePromptLines(acceptance) {
15185
+ const lines = [];
15186
+ if (acceptance?.acceptPartialChildren === true) lines.push("Partial salvage is on: a child that ends at its limit AFTER recording progress with report_progress counts as a partial success for acceptance; its digest carries the partial and get_child_result (when enabled) pages the full report. When the gap matters, respawn a NARROWED child carrying the partial instead of repeating the task.");
15187
+ if (acceptance?.acceptValidatedTerminalOutputOnLimit === true) lines.push("Terminal-output salvage is on: a child that ends at its limit WITH a final answer (its finalization reserve summary, already validated against the declared output schema) counts as a successful child for acceptance; its digest carries it after the 'final:' marker and get_child_result (when enabled) pages it in full.");
15188
+ return lines;
15189
+ }
15190
+ /**
15191
+ * Resolves per-spawn dispatch options against the engine registries
15192
+ * (registered SchemaSpec and tool profile names; M7-T05). An
15193
+ * unknown ref is a typed ConfigError, surfaced as a tool error to the
15194
+ * orchestrator and never a run failure.
15195
+ */
15196
+ /**
15197
+ * The capped orchestrator's own admission estimate (the 1.63.0
15198
+ * experiment review, P0.3): the effective cap MINUS the finalize
15199
+ * carve-out already committed on the cap account, so the dispatch
15200
+ * admits at EXACT FILL by construction (a capped orchestrator can never
15201
+ * spend past its effectiveCap, and pricing the model's full
15202
+ * maxOutputTokens instead pinned small run ceilings at zero remainder;
15203
+ * the M12 checkpoint measured a self-solving orchestrator because no
15204
+ * child was ever admitted). Exported so the live dispatch and
15205
+ * preflightEstimate share ONE formula: both call this function.
15206
+ */
15207
+ function orchestratorAdmissionEstCostUsd(effectiveCapUsd, committedFinalizeReserveUsd) {
15208
+ return effectiveCapUsd - committedFinalizeReserveUsd;
15209
+ }
15210
+ function resolveDispatchOpts(spec, defaults) {
15211
+ const opts = {};
15212
+ if (spec.outputSchemaRef !== void 0) {
15213
+ const schema = defaults.schemas?.[spec.outputSchemaRef];
15214
+ if (schema === void 0) throw new ConfigError(`unknown outputSchemaRef '${spec.outputSchemaRef}': register it under defaults.schemas`);
15215
+ opts.schema = schema;
15216
+ }
15217
+ if (spec.toolsetRef !== void 0) {
15218
+ const tools = defaults.toolsets?.[spec.toolsetRef];
15219
+ if (tools === void 0) throw new ConfigError(`unknown toolsetRef '${spec.toolsetRef}': register it under defaults.toolsets (https://docs.rulvar.com/guide/tools)`);
15220
+ opts.tools = tools;
15221
+ }
15222
+ const extended = spec;
15223
+ if (extended.isolation !== void 0) opts.isolation = extended.isolation;
15224
+ if (extended.usageLimits !== void 0) opts.limits = extended.usageLimits;
15225
+ if (extended.escalation !== void 0) opts.escalation = extended.escalation;
15226
+ if (extended.bootCheckpointRef !== void 0) opts[kBootCheckpoint] = extended.bootCheckpointRef;
15227
+ if (extended.model !== void 0) opts.model = extended.model;
15228
+ if (extended.memoizeOutcome !== void 0) opts.memoizeOutcome = extended.memoizeOutcome;
15229
+ if (extended.schema !== void 0 && opts.schema === void 0) opts.schema = extended.schema;
15230
+ return opts;
15231
+ }
15232
+ function filterProfiles(registered, names) {
15233
+ if (registered === void 0) return {};
15234
+ if (names === void 0) return registered;
15235
+ const filtered = {};
15236
+ for (const name of names) if (registered[name] !== void 0) filtered[name] = registered[name];
15237
+ return filtered;
15238
+ }
15239
+ /**
15240
+ * Builds the orchestrator workflow: ONE implementation behind both
15241
+ * surfaces. The body wires the spawn tools over the per-call runtime,
15242
+ * recovers spawn records from the journal on resume, and runs the
15243
+ * orchestrator agent with the finish terminal tool.
15244
+ */
15245
+ function makeOrchestratorWorkflow(goal, opts) {
15246
+ validateOrchestrateOptions(opts);
15247
+ return defineWorkflow({ name: ORCHESTRATE_WORKFLOW_NAME }, async (ctx) => {
15248
+ const runtime = runtimeOf(ctx);
15249
+ const { internals } = runtime;
15250
+ if (internals.admission === void 0) throw new ConfigError("orchestrate requires the engine run context (createEngine)");
15251
+ const admission = internals.admission;
15252
+ const callingState = runtime.currentState();
15253
+ const advertisedProfiles = filterProfiles(internals.defaults.profiles, opts?.profiles);
15254
+ const spawnableProfiles = {};
15255
+ const declaredLadderNames = [];
15256
+ for (const [name, profile] of Object.entries(advertisedProfiles)) {
15937
15257
  const spec = profile.model;
15938
15258
  if (spec !== void 0 && typeof spec !== "string" && "ladder" in spec) declaredLadderNames.push(name);
15939
15259
  else spawnableProfiles[name] = profile;
@@ -16770,11 +16090,13 @@ function makeOrchestratorWorkflow(goal, opts) {
16770
16090
  let decision = known.find((candidate) => candidate.callId === call.id);
16771
16091
  if (decision === void 0) {
16772
16092
  const result = call.result ?? null;
16093
+ const salvageOutputOn = opts?.acceptance?.acceptValidatedTerminalOutputOnLimit === true;
16773
16094
  const children = [...records.values()].sort((a, b) => a.spawnOrdinal - b.spawnOrdinal).map((record) => ({
16774
16095
  handle: record.handle,
16775
16096
  nodeId: record.nodeId,
16776
16097
  status: record.settled?.status ?? "running",
16777
- text: record.settled === void 0 ? "" : serializeChildOutput(record.settled)
16098
+ text: record.settled === void 0 ? "" : serializeChildOutput(record.settled),
16099
+ ...salvageOutputOn && record.settled?.status === "limit" && record.settled.output !== null && record.settled.output !== void 0 ? { salvageableOutput: true } : {}
16778
16100
  }));
16779
16101
  const input = {
16780
16102
  result,
@@ -16843,7 +16165,7 @@ function makeOrchestratorWorkflow(goal, opts) {
16843
16165
  role: "orchestrate",
16844
16166
  result: "full",
16845
16167
  tools,
16846
- ...capState === void 0 ? {} : { estCost: capState.effectiveCapUsd - (orchestratorAccount === void 0 ? 0 : internals.budget.accountView(orchestratorAccount)?.finalizeReserveUsd ?? 0) },
16168
+ ...capState === void 0 ? {} : { estCost: orchestratorAdmissionEstCostUsd(capState.effectiveCapUsd, orchestratorAccount === void 0 ? 0 : internals.budget.accountView(orchestratorAccount)?.finalizeReserveUsd ?? 0) },
16847
16169
  ...opts?.model === void 0 ? {} : { model: opts.model },
16848
16170
  ...opts?.limits === void 0 ? {} : { limits: opts.limits },
16849
16171
  [kOnRunning]: (seq) => {
@@ -17156,151 +16478,912 @@ function makeOrchestratorWorkflow(goal, opts) {
17156
16478
  ...validationSpec === void 0 ? {} : { validate: validateFinish }
17157
16479
  }
17158
16480
  };
17159
- const synthesized = await runtime.runInScope(synthesisState, () => ctx.agent(prompt, synthesisOpts));
17160
- if (validationTermination !== void 0) throw validationTermination;
17161
- if (synthesized.status === "ok") return synthesized.output;
17162
- 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: {
17163
- source: "orchestrator_synthesis",
17164
- status: synthesized.status,
17165
- turnsUsed: synthesized.turns
17166
- } });
17167
- const fallbackKey = deriverV2.deriveKey({ kind: "orchestrator-synthesis-fallback" });
17168
- if (!internals.replayer.snapshot().some((entry) => entry.kind === "decision" && entry.key === fallbackKey)) await internals.replayer.appendSinglePhase({
17169
- scope: callingState.scope,
17170
- key: fallbackKey,
17171
- kind: "decision",
17172
- status: "ok",
17173
- spanId: internals.spans.mint(callingState.spanId),
17174
- site: "orchestrator-synthesis",
17175
- value: {
17176
- decisionType: "orchestrator_synthesis_fallback",
17177
- status: synthesized.status,
17178
- turnsUsed: synthesized.turns
17179
- }
17180
- });
17181
- internals.events.emit({
17182
- type: "log",
17183
- level: "warn",
17184
- msg: `the synthesis invocation terminated with status '${synthesized.status}'; falling back to the coordination draft (journaled decision 'orchestrator_synthesis_fallback')`
17185
- }, callingState.spanId);
17186
- return draft;
17187
- };
17188
- /**
17189
- * The settle at the cap: the JOURNALED cap decision drives the policy
17190
- * branch (its `fallback` field froze budget.atCap when the cap
17191
- * tripped), so a crash between the decision and its effect rolls the
17192
- * SAME outcome forward on resume, immune to drift of the live options.
17193
- * 'finish-with-partial' runs the reserved finalizer;
17194
- * 'fail-run' skips it and fails the run typed (v1.35.0 review P2-1:
17195
- * the policy used to be journaled and then ignored).
17196
- */
17197
- const settleCapOutcome = async () => {
17198
- const capValue = internals.replayer.snapshot().find((entry) => entry.seq === capDecisionRef)?.value;
17199
- if (capValue?.fallback === "fail-run") throw new FailRunError(`the orchestrator budget cap was reached (decision entry ${String(capDecisionRef ?? -1)}) and budget.atCap is 'fail-run': the reserved finalizer is skipped and the run fails instead of returning a partial result`, { data: {
17200
- source: "orchestrator_budget_cap",
17201
- capDecisionRef: capDecisionRef ?? -1,
17202
- spentUsd: capValue.spentUsd ?? 0,
17203
- capUsd: capValue.capUsd ?? 0
17204
- } });
17205
- return await runForcedFinish();
17206
- };
17207
- const bootTermination = extensionTermination;
17208
- if (bootTermination !== void 0) throw bootTermination;
17209
- if (capDecisionRef !== void 0) return await settleCapOutcome();
17210
- if (validationSpec !== void 0) {
17211
- const priorRejection = validationDecisions().find((decision) => decision.verdict === "rejected");
17212
- if (priorRejection !== void 0) throw finishValidationError(priorRejection);
16481
+ const synthesized = await runtime.runInScope(synthesisState, () => ctx.agent(prompt, synthesisOpts));
16482
+ if (validationTermination !== void 0) throw validationTermination;
16483
+ if (synthesized.status === "ok") return synthesized.output;
16484
+ 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: {
16485
+ source: "orchestrator_synthesis",
16486
+ status: synthesized.status,
16487
+ turnsUsed: synthesized.turns
16488
+ } });
16489
+ const fallbackKey = deriverV2.deriveKey({ kind: "orchestrator-synthesis-fallback" });
16490
+ if (!internals.replayer.snapshot().some((entry) => entry.kind === "decision" && entry.key === fallbackKey)) await internals.replayer.appendSinglePhase({
16491
+ scope: callingState.scope,
16492
+ key: fallbackKey,
16493
+ kind: "decision",
16494
+ status: "ok",
16495
+ spanId: internals.spans.mint(callingState.spanId),
16496
+ site: "orchestrator-synthesis",
16497
+ value: {
16498
+ decisionType: "orchestrator_synthesis_fallback",
16499
+ status: synthesized.status,
16500
+ turnsUsed: synthesized.turns
16501
+ }
16502
+ });
16503
+ internals.events.emit({
16504
+ type: "log",
16505
+ level: "warn",
16506
+ msg: `the synthesis invocation terminated with status '${synthesized.status}'; falling back to the coordination draft (journaled decision 'orchestrator_synthesis_fallback')`
16507
+ }, callingState.spanId);
16508
+ return draft;
16509
+ };
16510
+ /**
16511
+ * The settle at the cap: the JOURNALED cap decision drives the policy
16512
+ * branch (its `fallback` field froze budget.atCap when the cap
16513
+ * tripped), so a crash between the decision and its effect rolls the
16514
+ * SAME outcome forward on resume, immune to drift of the live options.
16515
+ * 'finish-with-partial' runs the reserved finalizer;
16516
+ * 'fail-run' skips it and fails the run typed (v1.35.0 review P2-1:
16517
+ * the policy used to be journaled and then ignored).
16518
+ */
16519
+ const settleCapOutcome = async () => {
16520
+ const capValue = internals.replayer.snapshot().find((entry) => entry.seq === capDecisionRef)?.value;
16521
+ if (capValue?.fallback === "fail-run") throw new FailRunError(`the orchestrator budget cap was reached (decision entry ${String(capDecisionRef ?? -1)}) and budget.atCap is 'fail-run': the reserved finalizer is skipped and the run fails instead of returning a partial result`, { data: {
16522
+ source: "orchestrator_budget_cap",
16523
+ capDecisionRef: capDecisionRef ?? -1,
16524
+ spentUsd: capValue.spentUsd ?? 0,
16525
+ capUsd: capValue.capUsd ?? 0
16526
+ } });
16527
+ return await runForcedFinish();
16528
+ };
16529
+ const bootTermination = extensionTermination;
16530
+ if (bootTermination !== void 0) throw bootTermination;
16531
+ if (capDecisionRef !== void 0) return await settleCapOutcome();
16532
+ if (validationSpec !== void 0) {
16533
+ const priorRejection = validationDecisions().find((decision) => decision.verdict === "rejected");
16534
+ if (priorRejection !== void 0) throw finishValidationError(priorRejection);
16535
+ }
16536
+ const promptLines = [
16537
+ ...extension?.promptLines?.() ?? [],
16538
+ ...finishValidationPromptLines(validationSpec),
16539
+ ...acceptancePromptLines(opts?.acceptance)
16540
+ ];
16541
+ const result = await runtime.runInScope(orchestratorState, () => ctx.agent(orchestratorPrompt(goal, opts?.maxSpawns, promptLines.length === 0 ? void 0 : promptLines), agentOpts));
16542
+ const liveTermination = extensionTermination;
16543
+ if (liveTermination !== void 0) throw liveTermination;
16544
+ if (capDecisionRef !== void 0) return await settleCapOutcome();
16545
+ if (validationTermination !== void 0) throw validationTermination;
16546
+ if (orchestratorAccount !== void 0) internals.cost.orchestrator.spentUsd = internals.budget.accountView(orchestratorAccount)?.spentUsd ?? 0;
16547
+ if (result.status !== "ok") throw new ConfigError(`the orchestrator agent terminated with status '${result.status}'` + (result.errorMessage === void 0 ? "" : `: ${result.errorMessage}`));
16548
+ if (opts?.acceptance === void 0) return await runSynthesis(result.output);
16549
+ const acceptanceKey = "acceptance";
16550
+ const priorAcceptance = internals.replayer.snapshot().find((entry) => entry.kind === "decision" && entry.scope === callingState.scope && entry.key === acceptanceKey);
16551
+ let decision;
16552
+ if (priorAcceptance !== void 0) decision = priorAcceptance.value;
16553
+ else {
16554
+ const childStatusCounts = {};
16555
+ const degradedReasons = [];
16556
+ const salvaged = [];
16557
+ const salvagedOutput = [];
16558
+ let hardDegraded = 0;
16559
+ const acceptPartial = opts.acceptance.acceptPartialChildren === true;
16560
+ const acceptOutput = opts.acceptance.acceptValidatedTerminalOutputOnLimit === true;
16561
+ const sortedRecords = [...records.values()].sort((a, b) => a.spawnOrdinal - b.spawnOrdinal);
16562
+ for (const record of sortedRecords) {
16563
+ const status = record.settled?.status ?? "running";
16564
+ childStatusCounts[status] = (childStatusCounts[status] ?? 0) + 1;
16565
+ if (status === "ok") continue;
16566
+ if (acceptOutput && status === "limit" && record.settled?.output !== null && record.settled?.output !== void 0) {
16567
+ salvagedOutput.push(record.nodeId);
16568
+ degradedReasons.push(`child ${record.nodeId} accepted with its validated terminal output (settled 'limit' after the finalization reserve summary)`);
16569
+ continue;
16570
+ }
16571
+ if (acceptPartial && status === "limit" && record.settled?.partial !== void 0) {
16572
+ salvaged.push(record.nodeId);
16573
+ degradedReasons.push(`child ${record.nodeId} accepted as partial (settled 'limit' with a structured partial)`);
16574
+ continue;
16575
+ }
16576
+ hardDegraded += 1;
16577
+ degradedReasons.push(status === "running" ? `child ${record.nodeId} was still running when finish validated` : `child ${record.nodeId} settled '${status}'`);
16578
+ }
16579
+ const childPolicy = opts.acceptance.childPolicy;
16580
+ const accepted = childPolicy === "all-ok" ? hardDegraded === 0 : (childStatusCounts.ok ?? 0) + salvaged.length + salvagedOutput.length >= childPolicy.minSuccessful;
16581
+ decision = {
16582
+ decisionType: "orchestrator_acceptance",
16583
+ verdict: accepted ? "accepted" : "rejected",
16584
+ completion: !accepted ? "rejected" : degradedReasons.length === 0 ? "complete" : "partial",
16585
+ childPolicy,
16586
+ childStatusCounts,
16587
+ degradedReasons,
16588
+ ...salvaged.length === 0 ? {} : { salvagedPartialChildren: salvaged },
16589
+ ...salvagedOutput.length === 0 ? {} : { salvagedTerminalOutputChildren: salvagedOutput }
16590
+ };
16591
+ await internals.replayer.appendSinglePhase({
16592
+ scope: callingState.scope,
16593
+ key: acceptanceKey,
16594
+ kind: "decision",
16595
+ status: "ok",
16596
+ spanId: internals.spans.mint(callingState.spanId),
16597
+ site: "orchestrator-acceptance",
16598
+ value: decision
16599
+ });
16600
+ }
16601
+ if (decision.verdict === "rejected") {
16602
+ const required = decision.childPolicy === "all-ok" ? "every child ok" : `at least ${String(decision.childPolicy.minSuccessful)} children ok`;
16603
+ 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: {
16604
+ source: "orchestrator_acceptance",
16605
+ completion: "rejected",
16606
+ childPolicy: decision.childPolicy,
16607
+ childStatusCounts: decision.childStatusCounts,
16608
+ degradedReasons: decision.degradedReasons,
16609
+ ...decision.salvagedPartialChildren === void 0 ? {} : { salvagedPartialChildren: decision.salvagedPartialChildren },
16610
+ ...decision.salvagedTerminalOutputChildren === void 0 ? {} : { salvagedTerminalOutputChildren: decision.salvagedTerminalOutputChildren }
16611
+ } });
16612
+ }
16613
+ return {
16614
+ result: await runSynthesis(result.output),
16615
+ completion: decision.completion,
16616
+ childStatusCounts: decision.childStatusCounts,
16617
+ degradedReasons: decision.degradedReasons,
16618
+ ...decision.salvagedPartialChildren === void 0 ? {} : { salvagedPartialChildren: decision.salvagedPartialChildren },
16619
+ ...decision.salvagedTerminalOutputChildren === void 0 ? {} : { salvagedTerminalOutputChildren: decision.salvagedTerminalOutputChildren }
16620
+ };
16621
+ });
16622
+ }
16623
+ /**
16624
+ * Top-level surface: creates a run. `runOptions` are the ordinary
16625
+ * engine {@link RunOptions} of the created run; in particular
16626
+ * `runOptions.budgetUsd` is the ROOT hard ceiling over the WHOLE tree
16627
+ * (the orchestrator and every child), immutable after start, while
16628
+ * `opts.budget` only shapes the orchestrator's own sub-account inside
16629
+ * that ceiling. The shortcut previously accepted no RunOptions at all,
16630
+ * so the canonical entry point could not set a root ceiling without
16631
+ * dropping to `engine.run(makeOrchestratorWorkflow(...))` (v1.18.0
16632
+ * review P1-5).
16633
+ */
16634
+ function orchestrate(engine, goal, opts, runOptions) {
16635
+ return engine.run(makeOrchestratorWorkflow(goal, opts), void 0, runOptions);
16636
+ }
16637
+ //#endregion
16638
+ //#region src/engine/preflight.ts
16639
+ const ANY_TOOL = "(any)";
16640
+ function resolveServing(spec) {
16641
+ if (spec === void 0) return;
16642
+ if (typeof spec === "string") return spec;
16643
+ if ("model" in spec) return spec.model;
16644
+ return spec.ladder.rungs[spec.ladder.startTier]?.model;
16645
+ }
16646
+ /**
16647
+ * Per-tool executed-call ceilings from the merged limits: for every
16648
+ * tool a per-tool cap or a unit cost names (plus the '(any)' tool that
16649
+ * nothing names, unit cost 1), the smallest of maxCallsPerTool[T],
16650
+ * floor(toolUnits.max / cost(T)) for a positive cost (a zero cost is
16651
+ * free), and maxToolCalls.
16652
+ */
16653
+ function toolCeilingsOf(limits) {
16654
+ const names = /* @__PURE__ */ new Set();
16655
+ for (const name of Object.keys(limits.maxCallsPerTool ?? {})) names.add(name);
16656
+ for (const name of Object.keys(limits.toolUnits?.costs ?? {})) names.add(name);
16657
+ const rows = [];
16658
+ for (const tool of [...[...names].sort(), ANY_TOOL]) {
16659
+ const terms = [];
16660
+ const cap = tool === ANY_TOOL ? void 0 : limits.maxCallsPerTool?.[tool];
16661
+ if (cap !== void 0) terms.push({
16662
+ boundBy: "maxCallsPerTool",
16663
+ ceiling: cap
16664
+ });
16665
+ if (limits.toolUnits !== void 0) {
16666
+ const cost = tool === ANY_TOOL ? 1 : limits.toolUnits.costs?.[tool] ?? 1;
16667
+ if (cost > 0) terms.push({
16668
+ boundBy: "toolUnits",
16669
+ ceiling: Math.floor(limits.toolUnits.max / cost)
16670
+ });
16671
+ }
16672
+ if (limits.maxToolCalls !== void 0) terms.push({
16673
+ boundBy: "maxToolCalls",
16674
+ ceiling: limits.maxToolCalls
16675
+ });
16676
+ if (terms.length === 0) {
16677
+ rows.push({
16678
+ tool,
16679
+ ceiling: null
16680
+ });
16681
+ continue;
16682
+ }
16683
+ const min = terms.reduce((best, term) => term.ceiling < best.ceiling ? term : best);
16684
+ rows.push({
16685
+ tool,
16686
+ ceiling: min.ceiling,
16687
+ boundBy: min.boundBy
16688
+ });
16689
+ }
16690
+ return rows;
16691
+ }
16692
+ function validateSpawnSpec(spec, index) {
16693
+ const site = `preflight.spawns[${index}]`;
16694
+ if (spec.limits !== void 0) validateUsageLimits(spec.limits, `${site}.limits`);
16695
+ if (spec.estCost !== void 0) requireNonNegativeNumber(spec.estCost, `${site}.estCost`);
16696
+ if (spec.estInputTokens !== void 0) requireNonNegativeInteger(spec.estInputTokens, `${site}.estInputTokens`);
16697
+ if (spec.count !== void 0) requirePositiveInteger(spec.count, `${site}.count`);
16698
+ if (spec.budgetUsd !== void 0) requireNonNegativeNumber(spec.budgetUsd, `${site}.budgetUsd`);
16699
+ }
16700
+ /**
16701
+ * Computes the preflight report: the effective merged limits per
16702
+ * declared spawn, the layer-1 admission projection over the declared
16703
+ * wave, the per-tool and weighted-unit bottleneck ordering, the
16704
+ * concurrency and quota exposure at the declared estimates, and the
16705
+ * linter findings. Pure: no engine is constructed, no store is opened,
16706
+ * no adapter stream is dispatched, and no journal entry is written.
16707
+ */
16708
+ function preflightEstimate(input) {
16709
+ const engine = input.engine ?? {};
16710
+ const defaults = engine.defaults ?? {};
16711
+ if (defaults.limits !== void 0) validateUsageLimits(defaults.limits, "preflight.engine.defaults.limits");
16712
+ if (input.run?.limits !== void 0) validateUsageLimits(input.run.limits, "preflight.run.limits");
16713
+ if (input.orchestrator?.limits !== void 0) validateUsageLimits(input.orchestrator.limits, "preflight.orchestrator.limits");
16714
+ const findings = [];
16715
+ const say = (finding) => {
16716
+ findings.push(finding);
16717
+ };
16718
+ const adapters = new Map((engine.adapters ?? []).map((adapter) => [adapter.id, adapter]));
16719
+ const capsOf = (ref) => {
16720
+ const { adapterId, model } = parseModelRef(ref);
16721
+ return adapters.get(adapterId)?.caps(model);
16722
+ };
16723
+ const pricingOf = (ref) => resolvePricing(ref, engine.pricing, capsOf(ref)?.pricing);
16724
+ const ceilingUsd = input.run?.budgetUsd;
16725
+ const flatReserveUsd = engine.budgetDefaults?.flatReserveUsd ?? .5;
16726
+ const lifetimeSpawnCap = engine.budgetDefaults?.lifetimeSpawnCap ?? 500;
16727
+ const childBudgetFraction = engine.budgetDefaults?.childBudgetFraction ?? .3;
16728
+ const maxDepth = engine.budgetDefaults?.maxDepth ?? 1;
16729
+ const perRun = engine.concurrency?.perRun ?? 12;
16730
+ const runLimits = mergeUsageLimits(void 0, input.run?.limits, defaults.limits);
16731
+ let orchestratorEcho;
16732
+ let reservedForFinalizationUsd = 0;
16733
+ let effectiveCapUsd;
16734
+ if (input.orchestrator !== void 0) {
16735
+ if (input.orchestrator.estInputTokens !== void 0) requireNonNegativeInteger(input.orchestrator.estInputTokens, "preflight.orchestrator.estInputTokens");
16736
+ const spec = input.orchestrator.budget;
16737
+ const fraction = spec?.capFraction ?? .2;
16738
+ const fromFraction = ceilingUsd === void 0 ? void 0 : fraction * ceilingUsd;
16739
+ const bounds = [spec?.capUsd, fromFraction].filter((bound) => bound !== void 0);
16740
+ effectiveCapUsd = bounds.length === 0 ? void 0 : Math.min(...bounds);
16741
+ const finalizeTurns = spec?.finalizeTurns ?? 2;
16742
+ const finalizeReserveUsd = spec?.finalizeReserveUsd ?? finalizeTurns * flatReserveUsd;
16743
+ const reserveCommitted = input.orchestrator.extension === true;
16744
+ if (reserveCommitted) reservedForFinalizationUsd = finalizeReserveUsd;
16745
+ orchestratorEcho = {
16746
+ ...effectiveCapUsd === void 0 ? {} : { effectiveCapUsd },
16747
+ finalizeReserveUsd,
16748
+ finalizeTurns,
16749
+ reserveCommitted
16750
+ };
16751
+ if (spec?.capUsd !== void 0 && spec.capFraction === void 0 && effectiveCapUsd !== void 0 && effectiveCapUsd < spec.capUsd) say({
16752
+ severity: "warning",
16753
+ code: "orchestrator-cap-fraction-bound",
16754
+ message: `orchestrator budget.capUsd ${spec.capUsd.toFixed(4)} USD is bounded to ${effectiveCapUsd.toFixed(4)} USD by the default capFraction 0.2 of the run ceiling; pass capFraction: 1.0 to make capUsd the sole bound`
16755
+ });
16756
+ if (input.orchestrator.extension === true && effectiveCapUsd !== void 0 && effectiveCapUsd < finalizeReserveUsd) say({
16757
+ severity: "error",
16758
+ code: "orchestrator-cap-below-finalize-reserve",
16759
+ message: `effectiveCap ${effectiveCapUsd.toFixed(4)} USD is below the finalize reserve ${finalizeReserveUsd.toFixed(4)} USD: the run would refuse to start`
16760
+ });
16761
+ }
16762
+ const spawnSpecs = input.spawns ?? [];
16763
+ spawnSpecs.forEach(validateSpawnSpec);
16764
+ const spawnReports = [];
16765
+ /**
16766
+ * The layer-2 spawn-gate inputs per report, in report order: the
16767
+ * DECLARED estimate (estCost or the profile's; never the priced
16768
+ * arm, which the embedded gate cannot reach) and the explicit spawn
16769
+ * budget. Only an orchestrate wave consumes them.
16770
+ */
16771
+ const waveGateInputs = [];
16772
+ const units = [];
16773
+ for (const spec of spawnSpecs) {
16774
+ const role = spec.role ?? "loop";
16775
+ const label = spec.label ?? role;
16776
+ const count = spec.count ?? 1;
16777
+ const profile = spec.profile === void 0 ? void 0 : defaults.profiles?.[spec.profile];
16778
+ if (spec.profile !== void 0 && profile === void 0) say({
16779
+ severity: "error",
16780
+ code: "unknown-profile",
16781
+ message: `spawn '${label}' names profile '${spec.profile}', which defaults.profiles does not register`,
16782
+ spawn: label
16783
+ });
16784
+ const limits = mergeUsageLimits(spec.limits, profile?.limits, defaults.limits);
16785
+ const servedBy = resolveServing(spec.model ?? profile?.routing?.[role] ?? profile?.model ?? defaults.routing?.[role]);
16786
+ if (servedBy === void 0) say({
16787
+ severity: "error",
16788
+ code: "unrouted-role",
16789
+ message: `spawn '${label}' resolves no model for role '${role}': the run would fail with a ConfigError at spawn time; set a model, a profile model, or defaults.routing.${role}`,
16790
+ spawn: label
16791
+ });
16792
+ const caps = servedBy === void 0 ? void 0 : capsOf(servedBy);
16793
+ const pricing = servedBy === void 0 ? void 0 : pricingOf(servedBy);
16794
+ const unpriced = servedBy !== void 0 && pricing === void 0;
16795
+ let reserveSource;
16796
+ let reserveUsd;
16797
+ if (unpriced && spec.estCost === void 0 && profile?.estCost === void 0) {
16798
+ reserveSource = "unpriced-zero";
16799
+ reserveUsd = 0;
16800
+ } else {
16801
+ reserveSource = spec.estCost !== void 0 ? "estCost" : profile?.estCost !== void 0 ? "profile-estCost" : spec.estInputTokens !== void 0 && caps?.pricing !== void 0 ? "priced-estimate" : "flat-default";
16802
+ reserveUsd = admissionReserveUsd({
16803
+ ...spec.estCost === void 0 ? {} : { estCost: spec.estCost },
16804
+ ...profile?.estCost === void 0 ? {} : { profileEstCost: profile.estCost },
16805
+ ...spec.estInputTokens === void 0 ? {} : { inputTokens: spec.estInputTokens },
16806
+ ...caps === void 0 ? {} : { caps },
16807
+ ...limits.maxOutputTokensPerTurn === void 0 ? {} : { maxOutputTokensPerTurn: limits.maxOutputTokensPerTurn },
16808
+ flatReserveUsd
16809
+ });
16810
+ }
16811
+ const outputBound = caps === void 0 ? limits.maxOutputTokensPerTurn : limits.maxOutputTokensPerTurn === void 0 ? caps.maxOutputTokens : Math.min(caps.maxOutputTokens, limits.maxOutputTokensPerTurn);
16812
+ if (caps !== void 0 && limits.maxOutputTokensPerTurn !== void 0 && limits.maxOutputTokensPerTurn > caps.maxOutputTokens) say({
16813
+ severity: "warning",
16814
+ code: "output-cap-above-model",
16815
+ message: `spawn '${label}' sets maxOutputTokensPerTurn ${String(limits.maxOutputTokensPerTurn)} above the model's maxOutputTokens ${String(caps.maxOutputTokens)}: the model clamp wins`,
16816
+ spawn: label
16817
+ });
16818
+ const turnFloorUsd = pricing === void 0 || outputBound === void 0 ? void 0 : priceUsdOf(pricing, {
16819
+ inputTokens: spec.estInputTokens ?? 0,
16820
+ outputTokens: outputBound,
16821
+ cacheReadTokens: 0,
16822
+ cacheWriteTokens: 0
16823
+ });
16824
+ const toolCeilings = toolCeilingsOf(limits);
16825
+ const overall = toolCeilings.reduce((best, row) => row.ceiling === null ? best : best === null ? row.ceiling : Math.max(best, row.ceiling), null);
16826
+ const executedToolCallCeiling = limits.maxToolCalls !== void 0 && (overall === null || limits.maxToolCalls < overall) ? limits.maxToolCalls : overall;
16827
+ for (const row of toolCeilings) {
16828
+ if (row.tool === ANY_TOOL) continue;
16829
+ const cost = limits.toolUnits?.costs?.[row.tool];
16830
+ if (cost !== void 0 && cost > 0 && limits.toolUnits !== void 0 && cost > limits.toolUnits.max) {
16831
+ say({
16832
+ severity: "warning",
16833
+ code: "tool-unaffordable",
16834
+ message: `spawn '${label}' prices tool '${row.tool}' at ${String(cost)} units against toolUnits.max ${String(limits.toolUnits.max)}: the tool can never execute`,
16835
+ spawn: label
16836
+ });
16837
+ continue;
16838
+ }
16839
+ if (row.boundBy === "toolUnits" && row.ceiling !== null) {
16840
+ const nominal = limits.maxToolCalls;
16841
+ const cap = limits.maxCallsPerTool?.[row.tool];
16842
+ if (nominal !== void 0 && row.ceiling < nominal || cap !== void 0 && row.ceiling < cap) say({
16843
+ severity: "warning",
16844
+ code: "weighted-units-bind-first",
16845
+ message: `spawn '${label}': toolUnits is the first bottleneck for '${row.tool}': ${String(row.ceiling)} executed calls (cost ${String(limits.toolUnits?.costs?.[row.tool] ?? 1)} of max ${String(limits.toolUnits?.max ?? 0)})` + (nominal === void 0 ? "" : ` while maxToolCalls suggests ${String(nominal)}`),
16846
+ spawn: label
16847
+ });
16848
+ }
16849
+ const cap = limits.maxCallsPerTool?.[row.tool];
16850
+ if (cap !== void 0 && cap > 0 && row.ceiling !== null && row.boundBy !== "maxCallsPerTool") say({
16851
+ severity: "info",
16852
+ code: "per-tool-cap-unreachable",
16853
+ message: `spawn '${label}': maxCallsPerTool['${row.tool}'] ${String(cap)} can never bind: ${row.boundBy ?? "another limiter"} already stops at ${String(row.ceiling)}`,
16854
+ spawn: label
16855
+ });
16856
+ }
16857
+ if (limits.finalizationReserve !== void 0 && limits.maxToolCalls === void 0 && limits.toolUnits === void 0) say({
16858
+ severity: "warning",
16859
+ code: "inert-finalization-reserve",
16860
+ message: `spawn '${label}' sets finalizationReserve without maxToolCalls or toolUnits: no tool budget limiter exists for it to fire on`,
16861
+ spawn: label
16862
+ });
16863
+ if (limits.toolBudgetNotices === true && limits.maxToolCalls === void 0) say({
16864
+ severity: "warning",
16865
+ code: "inert-tool-budget-notices",
16866
+ message: `spawn '${label}' sets toolBudgetNotices without maxToolCalls: the notices never fire`,
16867
+ spawn: label
16868
+ });
16869
+ if (unpriced && ceilingUsd !== void 0) say({
16870
+ severity: "warning",
16871
+ code: "unpriced-under-ceiling",
16872
+ message: `spawn '${label}' is served by '${servedBy ?? ""}' with no price row: the ${ceilingUsd.toFixed(4)} USD run ceiling does NOT bound it and its admission reserve is zero`,
16873
+ spawn: label
16874
+ });
16875
+ spawnReports.push({
16876
+ label,
16877
+ role,
16878
+ count,
16879
+ ...servedBy === void 0 ? {} : { servedBy },
16880
+ ...unpriced ? { unpriced: true } : {},
16881
+ limits,
16882
+ admissionReserveUsd: reserveUsd,
16883
+ reserveSource,
16884
+ ...outputBound === void 0 ? {} : { maxOutputTokensPerTurn: outputBound },
16885
+ ...turnFloorUsd === void 0 ? {} : { turnFloorUsd },
16886
+ executedToolCallCeiling,
16887
+ toolCeilings
16888
+ });
16889
+ {
16890
+ const declaredEstCostUsd = spec.estCost ?? profile?.estCost;
16891
+ waveGateInputs.push({
16892
+ ...declaredEstCostUsd === void 0 ? {} : { estCostUsd: declaredEstCostUsd },
16893
+ ...spec.budgetUsd === void 0 ? {} : { budgetUsd: spec.budgetUsd }
16894
+ });
16895
+ }
16896
+ for (let i = 0; i < count; i += 1) {
16897
+ const unit = {
16898
+ label: count === 1 ? label : `${label}#${String(i + 1)}`,
16899
+ tokensFloor: (spec.estInputTokens ?? 0) + (outputBound ?? 0)
16900
+ };
16901
+ if (servedBy !== void 0) {
16902
+ const { adapterId, model } = parseModelRef(servedBy);
16903
+ unit.provider = adapterId;
16904
+ unit.model = model;
16905
+ }
16906
+ if (turnFloorUsd !== void 0) unit.turnFloorUsd = turnFloorUsd;
16907
+ units.push(unit);
16908
+ }
16909
+ }
16910
+ let orchestratorReserveUsd;
16911
+ if (input.orchestrator !== void 0) {
16912
+ const servedBy = resolveServing(defaults.routing?.orchestrate);
16913
+ if (servedBy === void 0) say({
16914
+ severity: "error",
16915
+ code: "unrouted-role",
16916
+ message: "the orchestrator resolves no model for role 'orchestrate': set defaults.routing.orchestrate or an orchestrate model on the call",
16917
+ spawn: "orchestrator"
16918
+ });
16919
+ else {
16920
+ const caps = capsOf(servedBy);
16921
+ const pricing = pricingOf(servedBy);
16922
+ const orchLimits = mergeUsageLimits(input.orchestrator.limits, void 0, defaults.limits);
16923
+ const outputBound = caps === void 0 ? orchLimits.maxOutputTokensPerTurn : orchLimits.maxOutputTokensPerTurn === void 0 ? caps.maxOutputTokens : Math.min(caps.maxOutputTokens, orchLimits.maxOutputTokensPerTurn);
16924
+ const { adapterId, model } = parseModelRef(servedBy);
16925
+ const unit = {
16926
+ label: "orchestrator",
16927
+ provider: adapterId,
16928
+ model,
16929
+ tokensFloor: outputBound ?? 0
16930
+ };
16931
+ if (pricing !== void 0 && outputBound !== void 0) unit.turnFloorUsd = priceUsdOf(pricing, {
16932
+ inputTokens: 0,
16933
+ outputTokens: outputBound,
16934
+ cacheReadTokens: 0,
16935
+ cacheWriteTokens: 0
16936
+ });
16937
+ units.push(unit);
16938
+ if (effectiveCapUsd !== void 0) orchestratorReserveUsd = Math.max(0, orchestratorAdmissionEstCostUsd(effectiveCapUsd, reservedForFinalizationUsd));
16939
+ else if (pricing === void 0) orchestratorReserveUsd = 0;
16940
+ else orchestratorReserveUsd = admissionReserveUsd({
16941
+ ...input.orchestrator.estInputTokens === void 0 ? {} : { inputTokens: input.orchestrator.estInputTokens },
16942
+ ...caps === void 0 ? {} : { caps },
16943
+ ...orchLimits.maxOutputTokensPerTurn === void 0 ? {} : { maxOutputTokensPerTurn: orchLimits.maxOutputTokensPerTurn },
16944
+ flatReserveUsd
16945
+ });
16946
+ }
16947
+ }
16948
+ const wave = [];
16949
+ let committed = 0;
16950
+ let spawned = 0;
16951
+ let children = 0;
16952
+ const admitAgainstRoot = (reserveUsd) => {
16953
+ if (ceilingUsd === void 0) return true;
16954
+ const held = committed + reservedForFinalizationUsd;
16955
+ return !(held >= ceilingUsd || held + reserveUsd > ceilingUsd);
16956
+ };
16957
+ if (input.orchestrator !== void 0) {
16958
+ const reserveUsd = orchestratorReserveUsd ?? flatReserveUsd;
16959
+ let deniedBy;
16960
+ if (spawned >= lifetimeSpawnCap) deniedBy = "spawn-cap";
16961
+ else if (!admitAgainstRoot(reserveUsd)) deniedBy = "budget";
16962
+ wave.push({
16963
+ label: "orchestrator",
16964
+ reserveUsd,
16965
+ admitted: deniedBy === void 0,
16966
+ ...deniedBy === void 0 ? {} : { deniedBy }
16967
+ });
16968
+ if (deniedBy === void 0) {
16969
+ committed += reserveUsd;
16970
+ spawned += 1;
16971
+ }
16972
+ }
16973
+ const maxSpawns = input.orchestrator?.maxSpawns;
16974
+ const orchestrateWave = input.orchestrator !== void 0;
16975
+ for (const [reportIndex, report] of spawnReports.entries()) {
16976
+ const gate = waveGateInputs[reportIndex] ?? {};
16977
+ for (let i = 0; i < report.count; i += 1) {
16978
+ const label = report.count === 1 ? report.label : `${report.label}#${String(i + 1)}`;
16979
+ const reserveUsd = report.admissionReserveUsd;
16980
+ let deniedBy;
16981
+ if (spawned >= lifetimeSpawnCap) deniedBy = "spawn-cap";
16982
+ else if (maxSpawns !== void 0 && children >= maxSpawns) deniedBy = "orchestrator-max-spawns";
16983
+ else {
16984
+ if (orchestrateWave && ceilingUsd !== void 0) {
16985
+ const remainder = ceilingUsd - committed - reservedForFinalizationUsd;
16986
+ const projection = dispatchProjectionReserveUsd(gate, flatReserveUsd);
16987
+ if (remainder <= 0 || remainder < projection) deniedBy = "budget";
16988
+ }
16989
+ if (deniedBy === void 0 && !admitAgainstRoot(reserveUsd)) deniedBy = "budget";
16990
+ }
16991
+ wave.push({
16992
+ label,
16993
+ reserveUsd,
16994
+ admitted: deniedBy === void 0,
16995
+ ...deniedBy === void 0 ? {} : { deniedBy }
16996
+ });
16997
+ if (deniedBy === void 0) {
16998
+ committed += reserveUsd;
16999
+ spawned += 1;
17000
+ children += 1;
17001
+ }
17002
+ }
17003
+ }
17004
+ const admitted = wave.filter((row) => row.admitted).length;
17005
+ const denied = wave.length - admitted;
17006
+ if (wave.length > 0 && denied > 0) {
17007
+ const deniedLabels = wave.filter((row) => !row.admitted).map((row) => row.label);
17008
+ if (admitted === 0) say({
17009
+ severity: "error",
17010
+ code: "nothing-admitted",
17011
+ message: `the declared wave admits NOTHING: every spawn is denied (${deniedLabels.join(", ")}); no paid work can start`
17012
+ });
17013
+ else say({
17014
+ severity: "warning",
17015
+ code: "partial-admission",
17016
+ message: `the declared wave admits ${String(admitted)} of ${String(wave.length)} spawns; denied before any work: ${deniedLabels.join(", ")}`
17017
+ });
17018
+ }
17019
+ if (ceilingUsd === void 0 && wave.length > 0) say({
17020
+ severity: "info",
17021
+ code: "no-usd-ceiling",
17022
+ message: "the run has no budgetUsd ceiling: only turn, tool, and time limits bound spend, and the whole declared wave admits"
17023
+ });
17024
+ const declaredUnits = units.length;
17025
+ const maxInFlight = declaredUnits === 0 ? perRun : Math.min(perRun, declaredUnits);
17026
+ const perProviderCaps = engine.concurrency?.perProvider;
17027
+ const perProvider = {};
17028
+ const byProvider = /* @__PURE__ */ new Map();
17029
+ for (const unit of units) {
17030
+ if (unit.provider === void 0) continue;
17031
+ const list = byProvider.get(unit.provider) ?? [];
17032
+ list.push(unit);
17033
+ byProvider.set(unit.provider, list);
17034
+ }
17035
+ for (const [provider, list] of [...byProvider.entries()].sort(([a], [b]) => a.localeCompare(b))) {
17036
+ const cap = perProviderCaps?.[provider];
17037
+ const inFlight = Math.min(list.length, maxInFlight, cap ?? Number.POSITIVE_INFINITY);
17038
+ perProvider[provider] = {
17039
+ inFlight,
17040
+ requestsPerWave: inFlight,
17041
+ tokensPerWaveFloor: [...list].sort((a, b) => b.tokensFloor - a.tokensFloor).slice(0, inFlight).reduce((sum, unit) => sum + unit.tokensFloor, 0)
17042
+ };
17043
+ }
17044
+ const pricedTurns = units.map((unit) => unit.turnFloorUsd).filter((usd) => usd !== void 0).sort((a, b) => b - a).slice(0, maxInFlight);
17045
+ const overshootOneTurnFloorUsd = pricedTurns.length === 0 ? void 0 : pricedTurns.reduce((sum, usd) => sum + usd, 0);
17046
+ if (ceilingUsd !== void 0 && overshootOneTurnFloorUsd !== void 0 && units.length > 0) say({
17047
+ severity: "info",
17048
+ code: "overshoot-exposure",
17049
+ message: `past a ceiling crossing, up to ${String(Math.min(maxInFlight, units.length))} in-flight turns may still complete: at least ${overshootOneTurnFloorUsd.toFixed(4)} USD past the ${ceilingUsd.toFixed(4)} USD ceiling at the declared estimates, growing with prompt size`
17050
+ });
17051
+ const quotaConfigured = engine.quota !== void 0;
17052
+ if (!quotaConfigured && maxInFlight > 1 && units.length > 0) say({
17053
+ severity: "info",
17054
+ code: "no-quota",
17055
+ message: `no shared quota limiter is configured while up to ${String(maxInFlight)} turns run concurrently: provider-side rate limits are unprotected (createEngine quota)`
17056
+ });
17057
+ if (input.quotaRules !== void 0) input.quotaRules.forEach((rule, index) => {
17058
+ let requests = 0;
17059
+ let tokens = 0;
17060
+ for (const unit of units) {
17061
+ if (unit.provider === void 0 || unit.model === void 0) continue;
17062
+ if (quotaRuleMatches(rule, {
17063
+ provider: unit.provider,
17064
+ model: unit.model,
17065
+ ...engine.quota?.tenant === void 0 ? {} : { tenant: engine.quota.tenant },
17066
+ estimate: {
17067
+ requests: 1,
17068
+ inputTokens: 0
17069
+ }
17070
+ })) {
17071
+ requests += 1;
17072
+ tokens += unit.tokensFloor;
17073
+ }
17074
+ }
17075
+ const dims = [
17076
+ rule.provider === void 0 ? void 0 : `provider=${rule.provider}`,
17077
+ rule.model === void 0 ? void 0 : `model=${rule.model}`,
17078
+ rule.tenant === void 0 ? void 0 : `tenant=${rule.tenant}`
17079
+ ].filter((dim) => dim !== void 0).join(" ");
17080
+ const name = dims === "" ? `rule[${String(index)}]` : `rule[${String(index)}] (${dims})`;
17081
+ if (rule.requestsPerMinute !== void 0 && requests > rule.requestsPerMinute) say({
17082
+ severity: "warning",
17083
+ code: "quota-requests-below-wave",
17084
+ message: `${name}: the declared wave holds ${String(requests)} matching dispatches against requestsPerMinute ${String(rule.requestsPerMinute)}: expect synthetic rate-limit denials and backoff inside one window`
17085
+ });
17086
+ if (rule.tokensPerMinute !== void 0 && tokens > rule.tokensPerMinute) say({
17087
+ severity: "warning",
17088
+ code: "quota-tokens-below-wave",
17089
+ message: `${name}: the declared wave demands at least ${String(tokens)} tokens against tokensPerMinute ${String(rule.tokensPerMinute)}: expect estimate-driven throttling inside one window`
17090
+ });
17091
+ });
17092
+ const severityRank = {
17093
+ error: 0,
17094
+ warning: 1,
17095
+ info: 2
17096
+ };
17097
+ findings.sort((a, b) => severityRank[a.severity] - severityRank[b.severity]);
17098
+ return {
17099
+ concurrency: {
17100
+ perRun,
17101
+ ...perProviderCaps === void 0 ? {} : { perProvider: { ...perProviderCaps } }
17102
+ },
17103
+ budget: {
17104
+ ...ceilingUsd === void 0 ? {} : { ceilingUsd },
17105
+ flatReserveUsd,
17106
+ lifetimeSpawnCap,
17107
+ childBudgetFraction,
17108
+ maxDepth,
17109
+ ...orchestratorEcho === void 0 ? {} : { orchestrator: orchestratorEcho }
17110
+ },
17111
+ quota: {
17112
+ configured: quotaConfigured,
17113
+ ...engine.quota?.tenant === void 0 ? {} : { tenant: engine.quota.tenant },
17114
+ ...input.quotaRules === void 0 ? {} : { rules: input.quotaRules.length }
17115
+ },
17116
+ runLimits,
17117
+ spawns: spawnReports,
17118
+ admission: {
17119
+ ...ceilingUsd === void 0 ? {} : { ceilingUsd },
17120
+ reservedForFinalizationUsd,
17121
+ wave,
17122
+ admitted,
17123
+ denied
17124
+ },
17125
+ exposure: {
17126
+ maxInFlight,
17127
+ ...overshootOneTurnFloorUsd === void 0 ? {} : { overshootOneTurnFloorUsd },
17128
+ perProvider
17129
+ },
17130
+ findings
17131
+ };
17132
+ }
17133
+ //#endregion
17134
+ //#region src/engine/run-profiles.ts
17135
+ /**
17136
+ * The shipped presets (fast / standard / deep / ultra "and similar").
17137
+ * Data only; a review-time assertion checks the
17138
+ * engine has zero behavioral branches keyed on these names.
17139
+ */
17140
+ const RUN_PROFILES = {
17141
+ fast: {
17142
+ effortByRole: {
17143
+ orchestrate: "low",
17144
+ plan: "low",
17145
+ summarize: "low",
17146
+ extract: "low"
17147
+ },
17148
+ perRunConcurrency: 16,
17149
+ permissionPreset: "standard",
17150
+ lifetimeSpawnCap: 64,
17151
+ maxDepth: 1
17152
+ },
17153
+ standard: {
17154
+ effortByRole: {
17155
+ orchestrate: "high",
17156
+ plan: "high",
17157
+ summarize: "low",
17158
+ extract: "low"
17159
+ },
17160
+ perRunConcurrency: 12,
17161
+ permissionPreset: "standard",
17162
+ lifetimeSpawnCap: 500,
17163
+ maxDepth: 1
17164
+ },
17165
+ deep: {
17166
+ effortByRole: {
17167
+ orchestrate: "high",
17168
+ plan: "high",
17169
+ summarize: "medium",
17170
+ extract: "medium"
17171
+ },
17172
+ perRunConcurrency: 8,
17173
+ permissionPreset: "standard",
17174
+ lifetimeSpawnCap: 500,
17175
+ maxDepth: 2
17176
+ },
17177
+ ultra: {
17178
+ effortByRole: {
17179
+ orchestrate: "max",
17180
+ plan: "max",
17181
+ summarize: "high",
17182
+ extract: "high"
17183
+ },
17184
+ perRunConcurrency: 8,
17185
+ permissionPreset: "strict",
17186
+ lifetimeSpawnCap: 500,
17187
+ maxDepth: 3
17188
+ }
17189
+ };
17190
+ /** Looks up a shipped RunProfile by name; undefined for unknown names. */
17191
+ function runProfile(name) {
17192
+ return RUN_PROFILES[name];
17193
+ }
17194
+ //#endregion
17195
+ //#region src/model/concurrency.ts
17196
+ /**
17197
+ * Per-provider concurrency keys (M4-T07): a keyed limiter beside the
17198
+ * router, ENGINE-scoped (keys constrain calls
17199
+ * across a single engine per adapter). The Appendix A default is
17200
+ * unlimited: an embeddable library must not surprise-throttle hosts, so
17201
+ * the per-run semaphore stays the only default bound and provider 429s
17202
+ * ride RetryPolicy; hosts with known tier limits opt in per adapter id
17203
+ * via createEngine concurrency.perProvider.
17204
+ *
17205
+ * This keyed limiter bounds PARALLELISM inside one engine only. Two
17206
+ * processes sharing one API key coordinate through the QuotaLimiter
17207
+ * SPI instead (RV-215, createEngine `quota`): rate and volume live
17208
+ * there, in shared storage; in-flight slots live here.
17209
+ */
17210
+ var KeyedLimiter = class {
17211
+ semaphores = /* @__PURE__ */ new Map();
17212
+ constructor(caps) {
17213
+ for (const [key, limit] of Object.entries(caps ?? {})) this.semaphores.set(key, new Semaphore(limit));
17214
+ }
17215
+ /** Queue depth for one key (0 for unlimited keys); telemetry only. */
17216
+ pending(key) {
17217
+ return this.semaphores.get(key)?.pending ?? 0;
17218
+ }
17219
+ /**
17220
+ * Runs `fn` under the key's semaphore; keys without a configured cap
17221
+ * run unlimited (no queueing, no overhead). An aborted `signal` frees
17222
+ * a queued caller without a slot (the Semaphore contract), so run
17223
+ * cancellation drains provider queues too (v1.34.0 review P2-4).
17224
+ */
17225
+ async withSlot(key, fn, onQueued, signal) {
17226
+ const semaphore = this.semaphores.get(key);
17227
+ if (semaphore === void 0) return fn();
17228
+ return semaphore.withSlot(fn, onQueued, signal);
17229
+ }
17230
+ };
17231
+ //#endregion
17232
+ //#region src/orchestrator/finish-validators.ts
17233
+ /**
17234
+ * Deterministic host validation of the orchestrator finish result (the
17235
+ * v1.40.0 improvement plan's RV-204 slice). A validator is plain
17236
+ * synchronous host code judging the finish({ result }) argument; the
17237
+ * orchestrator runtime runs the configured set on every schema valid
17238
+ * finish call, returns the failure reasons to the model as the call's
17239
+ * error tool result (a bounded repair turn), and fails the run with a
17240
+ * typed error when the repair bound is exhausted. Verdicts journal as
17241
+ * decision entries, so a resume rolls the SAME verdicts forward without
17242
+ * re-running validator code.
17243
+ */
17244
+ const ok = { ok: true };
17245
+ function requireNonEmptyStrings(values, what) {
17246
+ if (!Array.isArray(values) || values.length === 0) throw new ConfigError(`${what} must be a non empty array of strings`);
17247
+ for (const value of values) if (typeof value !== "string" || value.length === 0) throw new ConfigError(`${what} must contain only non empty strings`);
17248
+ return values;
17249
+ }
17250
+ /**
17251
+ * Requires every named section to appear LITERALLY in the result text
17252
+ * (a heading like 'FINDINGS' or any marker the goal demands). Default
17253
+ * name 'required-sections'; pass `name` to run several instances.
17254
+ */
17255
+ function requiredSectionsValidator(options) {
17256
+ const sections = requireNonEmptyStrings(options.sections, "requiredSectionsValidator sections");
17257
+ return {
17258
+ name: options.name ?? "required-sections",
17259
+ validate: (input) => {
17260
+ const missing = sections.filter((section) => !input.text.includes(section));
17261
+ return missing.length === 0 ? ok : {
17262
+ ok: false,
17263
+ reasons: missing.map((section) => `required section '${section}' is missing`)
17264
+ };
17213
17265
  }
17214
- const promptLines = [
17215
- ...extension?.promptLines?.() ?? [],
17216
- ...finishValidationPromptLines(validationSpec),
17217
- ...acceptancePromptLines(opts?.acceptance)
17218
- ];
17219
- const result = await runtime.runInScope(orchestratorState, () => ctx.agent(orchestratorPrompt(goal, opts?.maxSpawns, promptLines.length === 0 ? void 0 : promptLines), agentOpts));
17220
- const liveTermination = extensionTermination;
17221
- if (liveTermination !== void 0) throw liveTermination;
17222
- if (capDecisionRef !== void 0) return await settleCapOutcome();
17223
- if (validationTermination !== void 0) throw validationTermination;
17224
- if (orchestratorAccount !== void 0) internals.cost.orchestrator.spentUsd = internals.budget.accountView(orchestratorAccount)?.spentUsd ?? 0;
17225
- if (result.status !== "ok") throw new ConfigError(`the orchestrator agent terminated with status '${result.status}'` + (result.errorMessage === void 0 ? "" : `: ${result.errorMessage}`));
17226
- if (opts?.acceptance === void 0) return await runSynthesis(result.output);
17227
- const acceptanceKey = "acceptance";
17228
- const priorAcceptance = internals.replayer.snapshot().find((entry) => entry.kind === "decision" && entry.scope === callingState.scope && entry.key === acceptanceKey);
17229
- let decision;
17230
- if (priorAcceptance !== void 0) decision = priorAcceptance.value;
17231
- else {
17232
- const childStatusCounts = {};
17233
- const degradedReasons = [];
17234
- const salvaged = [];
17235
- let hardDegraded = 0;
17236
- const acceptPartial = opts.acceptance.acceptPartialChildren === true;
17237
- const sortedRecords = [...records.values()].sort((a, b) => a.spawnOrdinal - b.spawnOrdinal);
17238
- for (const record of sortedRecords) {
17239
- const status = record.settled?.status ?? "running";
17240
- childStatusCounts[status] = (childStatusCounts[status] ?? 0) + 1;
17241
- if (status === "ok") continue;
17242
- if (acceptPartial && status === "limit" && record.settled?.partial !== void 0) {
17243
- salvaged.push(record.nodeId);
17244
- degradedReasons.push(`child ${record.nodeId} accepted as partial (settled 'limit' with a structured partial)`);
17245
- continue;
17246
- }
17247
- hardDegraded += 1;
17248
- degradedReasons.push(status === "running" ? `child ${record.nodeId} was still running when finish validated` : `child ${record.nodeId} settled '${status}'`);
17266
+ };
17267
+ }
17268
+ /**
17269
+ * Requires the result to be a JSON object carrying every named field
17270
+ * with a substantial value: present, not null, and not an empty or
17271
+ * whitespace only string (empty arrays, zero, and false COUNT as
17272
+ * present; emptiness rules beyond strings belong to a custom
17273
+ * validator). Default name 'required-fields'.
17274
+ */
17275
+ function requiredFieldsValidator(options) {
17276
+ const fields = requireNonEmptyStrings(options.fields, "requiredFieldsValidator fields");
17277
+ return {
17278
+ name: options.name ?? "required-fields",
17279
+ validate: (input) => {
17280
+ const result = input.result;
17281
+ if (typeof result !== "object" || result === null || Array.isArray(result)) return {
17282
+ ok: false,
17283
+ reasons: ["the finish result is not a JSON object"]
17284
+ };
17285
+ const record = result;
17286
+ const reasons = [];
17287
+ for (const field of fields) {
17288
+ const value = record[field];
17289
+ if (value === void 0 || value === null) reasons.push(`required field '${field}' is missing`);
17290
+ else if (typeof value === "string" && value.trim().length === 0) reasons.push(`required field '${field}' is empty`);
17249
17291
  }
17250
- const childPolicy = opts.acceptance.childPolicy;
17251
- const accepted = childPolicy === "all-ok" ? hardDegraded === 0 : (childStatusCounts.ok ?? 0) + salvaged.length >= childPolicy.minSuccessful;
17252
- decision = {
17253
- decisionType: "orchestrator_acceptance",
17254
- verdict: accepted ? "accepted" : "rejected",
17255
- completion: !accepted ? "rejected" : degradedReasons.length === 0 ? "complete" : "partial",
17256
- childPolicy,
17257
- childStatusCounts,
17258
- degradedReasons,
17259
- ...salvaged.length === 0 ? {} : { salvagedPartialChildren: salvaged }
17292
+ return reasons.length === 0 ? ok : {
17293
+ ok: false,
17294
+ reasons
17260
17295
  };
17261
- await internals.replayer.appendSinglePhase({
17262
- scope: callingState.scope,
17263
- key: acceptanceKey,
17264
- kind: "decision",
17265
- status: "ok",
17266
- spanId: internals.spans.mint(callingState.spanId),
17267
- site: "orchestrator-acceptance",
17268
- value: decision
17269
- });
17270
17296
  }
17271
- if (decision.verdict === "rejected") {
17272
- const required = decision.childPolicy === "all-ok" ? "every child ok" : `at least ${String(decision.childPolicy.minSuccessful)} children ok`;
17273
- 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: {
17274
- source: "orchestrator_acceptance",
17275
- completion: "rejected",
17276
- childPolicy: decision.childPolicy,
17277
- childStatusCounts: decision.childStatusCounts,
17278
- degradedReasons: decision.degradedReasons,
17279
- ...decision.salvagedPartialChildren === void 0 ? {} : { salvagedPartialChildren: decision.salvagedPartialChildren }
17280
- } });
17297
+ };
17298
+ }
17299
+ /** The default citation shape: a path with an extension, a colon, a line number. */
17300
+ const DEFAULT_CITATION_PATTERN = "[\\w./-]+\\.\\w+:\\d+";
17301
+ /** The default preserved share, the improvement plan's RV-202 gate. */
17302
+ const DEFAULT_EVIDENCE_MIN_SHARE = .95;
17303
+ const MAX_LISTED_CITATIONS = 20;
17304
+ function listCitations(values) {
17305
+ return values.length <= MAX_LISTED_CITATIONS ? values.join(", ") : `${values.slice(0, MAX_LISTED_CITATIONS).join(", ")} and ${String(values.length - MAX_LISTED_CITATIONS)} more`;
17306
+ }
17307
+ /**
17308
+ * The RV-202 evidence preservation contract: the finish result must
17309
+ * PRESERVE the citations the children actually produced. Distinct
17310
+ * matches of `pattern` are collected across the outputs of children
17311
+ * settled 'ok' (spawn order); at least `minShare` of them (default
17312
+ * {@link DEFAULT_EVIDENCE_MIN_SHARE}, the plan's 95 percent gate,
17313
+ * compared as a ceiling on the required count so an exact boundary like
17314
+ * 19 of 20 passes) must appear literally in the result text. Zero child
17315
+ * citations pass vacuously. With `requireKnown: true` the contract also
17316
+ * runs in reverse: every citation in the RESULT must appear in some
17317
+ * child's output, so a fabricated but pattern valid citation is
17318
+ * rejected instead of silently counting as evidence. Rejection reasons
17319
+ * list the missing (and unknown) citations, capped at 20, so the repair
17320
+ * turn can restore them. Purely textual and deterministic; checking
17321
+ * that cited targets EXIST on disk is host territory (a custom
17322
+ * validator), not this contract. Default name 'evidence-preserved'.
17323
+ */
17324
+ function evidencePreservedValidator(options) {
17325
+ const pattern = options?.pattern ?? "[\\w./-]+\\.\\w+:\\d+";
17326
+ const flags = options?.flags ?? "";
17327
+ const globalFlags = flags.includes("g") ? flags : `${flags}g`;
17328
+ try {
17329
+ new RegExp(pattern, globalFlags);
17330
+ } catch (thrown) {
17331
+ throw new ConfigError(`evidencePreservedValidator pattern does not compile: ${thrown instanceof Error ? thrown.message : String(thrown)}`);
17332
+ }
17333
+ const minShare = options?.minShare ?? .95;
17334
+ if (typeof minShare !== "number" || !Number.isFinite(minShare) || minShare <= 0 || minShare > 1) throw new ConfigError(`evidencePreservedValidator minShare must be a number in (0, 1]; got ${String(minShare)}`);
17335
+ return {
17336
+ name: options?.name ?? "evidence-preserved",
17337
+ validate: (input) => {
17338
+ const cited = /* @__PURE__ */ new Set();
17339
+ for (const child of input.children ?? []) {
17340
+ if (child.status !== "ok" && child.salvageableOutput !== true) continue;
17341
+ for (const match of child.text.match(new RegExp(pattern, globalFlags)) ?? []) cited.add(match);
17342
+ }
17343
+ const reasons = [];
17344
+ if (cited.size > 0) {
17345
+ const missing = [...cited].filter((citation) => !input.text.includes(citation));
17346
+ const preserved = cited.size - missing.length;
17347
+ if (preserved < Math.ceil(minShare * cited.size - 1e-9)) reasons.push(`evidence preservation ${String(preserved)} of ${String(cited.size)} child citations is below the required share ${String(minShare)}; missing: ${listCitations(missing)}`);
17348
+ }
17349
+ if (options?.requireKnown === true) {
17350
+ const fabricated = [...new Set(input.text.match(new RegExp(pattern, globalFlags)) ?? [])].filter((citation) => !cited.has(citation));
17351
+ if (fabricated.length > 0) reasons.push(`unknown citations not present in any child report: ${listCitations(fabricated)}`);
17352
+ }
17353
+ return reasons.length === 0 ? ok : {
17354
+ ok: false,
17355
+ reasons
17356
+ };
17281
17357
  }
17282
- return {
17283
- result: await runSynthesis(result.output),
17284
- completion: decision.completion,
17285
- childStatusCounts: decision.childStatusCounts,
17286
- degradedReasons: decision.degradedReasons,
17287
- ...decision.salvagedPartialChildren === void 0 ? {} : { salvagedPartialChildren: decision.salvagedPartialChildren }
17288
- };
17289
- });
17358
+ };
17290
17359
  }
17291
17360
  /**
17292
- * Top-level surface: creates a run. `runOptions` are the ordinary
17293
- * engine {@link RunOptions} of the created run; in particular
17294
- * `runOptions.budgetUsd` is the ROOT hard ceiling over the WHOLE tree
17295
- * (the orchestrator and every child), immutable after start, while
17296
- * `opts.budget` only shapes the orchestrator's own sub-account inside
17297
- * that ceiling. The shortcut previously accepted no RunOptions at all,
17298
- * so the canonical entry point could not set a root ceiling without
17299
- * dropping to `engine.run(makeOrchestratorWorkflow(...))` (v1.18.0
17300
- * review P1-5).
17361
+ * Requires at least `min` matches of `pattern` in the result text (the
17362
+ * plan's citation and source count checks: a file:line pattern, a URL
17363
+ * pattern). The pattern compiles at construction (invalid patterns are a
17364
+ * ConfigError before any run exists) and matches globally; `min` is a
17365
+ * positive integer. Default name 'min-matches'; pass `name` to run
17366
+ * several instances, because names must be unique per orchestrate call.
17301
17367
  */
17302
- function orchestrate(engine, goal, opts, runOptions) {
17303
- return engine.run(makeOrchestratorWorkflow(goal, opts), void 0, runOptions);
17368
+ function minMatchesValidator(options) {
17369
+ const flags = options.flags ?? "";
17370
+ const globalFlags = flags.includes("g") ? flags : `${flags}g`;
17371
+ try {
17372
+ new RegExp(options.pattern, globalFlags);
17373
+ } catch (thrown) {
17374
+ throw new ConfigError(`minMatchesValidator pattern does not compile: ${thrown instanceof Error ? thrown.message : String(thrown)}`);
17375
+ }
17376
+ if (!Number.isInteger(options.min) || options.min < 1) throw new ConfigError(`minMatchesValidator min must be a positive integer; got ${String(options.min)}`);
17377
+ return {
17378
+ name: options.name ?? "min-matches",
17379
+ validate: (input) => {
17380
+ const found = input.text.match(new RegExp(options.pattern, globalFlags))?.length ?? 0;
17381
+ return found >= options.min ? ok : {
17382
+ ok: false,
17383
+ reasons: [`expected at least ${String(options.min)} matches of /${options.pattern}/${flags}; found ${String(found)}`]
17384
+ };
17385
+ }
17386
+ };
17304
17387
  }
17305
17388
  //#endregion
17306
17389
  //#region src/engine/events.ts
@@ -18976,4 +19059,4 @@ function createSandboxBridge(ctx, options) {
18976
19059
  };
18977
19060
  }
18978
19061
  //#endregion
18979
- export { AWAIT_SCHEMA, AdmissionController, AdmissionRejectedError, AgentCallError, BUDGET_ABORT_REASON, BudgetExhaustedError, CANCEL_AGENT_SCHEMA, CHECKPOINT_FORMAT_V1, CLAIM_STATEMENT_MAX_CHARS, CLAIM_TTL_DAYS, COMPACTION_SUMMARY_PREFIX, CURRENT_HASH_VERSION, ConfigError, DEFAULT_CHILD_BUDGET_FRACTION, DEFAULT_CHILD_RESULT_PAGE_CHARS, DEFAULT_CITATION_PATTERN, DEFAULT_COMPACTION_THRESHOLD, DEFAULT_ESCALATION_LIMITS, DEFAULT_EVIDENCE_MIN_SHARE, DEFAULT_FINISH_MAX_REPAIRS, DEFAULT_FLAT_RESERVE_USD, DEFAULT_MAX_CHILDREN_PER_NODE, DEFAULT_MAX_DEPTH, DEFAULT_MAX_OSCILLATIONS_PER_KEY, DEFAULT_MAX_PINNED_WORKTREES, DEFAULT_MAX_REVISIONS_PER_RUN, DEFAULT_MAX_TOTAL_SPAWNS, DEFAULT_MAX_TURNS, DEFAULT_MODEL_RETRY_ATTEMPTS, DEFAULT_NO_PROGRESS_TURNS, DEFAULT_PER_RUN_CONCURRENCY, DEFAULT_RETRY_POLICY, DEFAULT_STREAM_IDLE_TIMEOUT_MS, DEFAULT_SYNTHESIS_MAX_TURNS, DEFAULT_SYNTHESIS_NOTE_MAX_TURNS, DedupIndex, DeterminismError, EMIT_RESULT_TOOL, EMPTY_SCHEMA_HASH, EMPTY_TOOLSET_HASH, ESCALATE_TOOL_NAME, ESCALATION_REPORT_SCHEMA, ESCALATION_REQUEST_SCHEMA, EVENT_SEGMENT_STRIDE, EscalationDecisionAbortedError, EventBus, ExternalRegistry, FINALIZE_SYNTHESIS_INSTRUCTION, FINISH_SCHEMA, FINISH_TOOL_NAME, FailRunError, FileModelKnowledgeStore, FileTranscriptStore, GET_CHILD_RESULT_SCHEMA, GET_CHILD_RESULT_TOOL_NAME, GitWorktreeProvider, IMPLEMENTATION_PROFILE_LIMITS, INBOX_PROPOSAL_TTL_DAYS, InMemoryStore, InMemoryTranscriptStore, InProcessRunner, InvalidResolutionError, JOURNAL_ENVELOPE_MARKER, JournalCompatibilityError, JournalMatcher, JournalMissError, JournalOrderViolation, JsonlFileStore, KB_ACTIVE_CLAIMS_CAP, KB_CARD_RENDER_BUDGET_CHARS, KeyedLimiter, KnowledgeCasError, LARGE_VALUE_WARN_BYTES, LEGACY_LTID_PREFIX, LEGACY_SIGNATURE_INPUTS, LINEAGE_SIG_VERSION, LeaseHeldError, LineageIndex, MASKED_SECRET, MAX_CHILD_RESULT_PAGE_CHARS, MAX_DEPTH_CEILING, ModelRetry, NoProgressDetector, NonSerializableValueError, ORCHESTRATE_WORKFLOW_NAME, OrchestratorCapConfigError, PARALLEL_AGENTS_SCHEMA, PROGRESS_REPORT_TOOL_NAME, ParallelSiteCounter, PlanInvariantError, QUOTA_WINDOW_MS, READ_CHILD_ARTIFACT_SCHEMA, READ_CHILD_ARTIFACT_TOOL_NAME, RESEARCH_PROFILE_LIMITS, REVIEW_PROFILE_LIMITS, ROLE_EFFORT_DEFAULTS, ROOT_ACCOUNT, ROOT_SCOPE, RUN_PROFILES, RUN_SETTLE_DECISION_TYPE, ReplayPlanHashMismatch, Replayer, ResolutionArbiter, ResolutionFold, RulvarError, RunBudget, SANDBOX_AGENT_OPT_KEYS, SPAWN_AGENT_SCHEMA, SandboxError, ScriptRejected, Semaphore, SettlementError, SpanRegistry, TOOL_NAME_PATTERN, TerminationAccount, WAIT_FOR_EVENTS_SCHEMA, WAIT_FOR_EVENTS_TOOL_NAME, WAKE_SUMMARY_RENDER_BUDGET_CHARS, admissionReserveUsd, affordableOutputTokens, agentErrorFromWire, agentErrorToWire, agentResultWire, agentScope, applyClaimOps, applyStructuredOutputTier, approachSigCoarse, approachSigOf, archiveDeprecatedModelOps, assertFencedWrites, atCompactionThreshold, auditRun, auditRuns, buildAbandonFold, buildAdapterRegistry, buildCostReport, buildDeriverRegistry, buildOrchestratorTools, buildTerminationInitValue, buildToolContext, canRideLoopTurn, canonicalIsolationTag, canonicalizeLadder, canonicalizeSchema, capIssues, capsHashOf, checkFloors, checkpointRefFor, childCoveragePrefix, claimExpired, claimExpiry, claimIssues, claimOpIssues, classifyAgentError, classifyAttemptOutcome, collectDeclaredLadders, compactMessages, compilePermissionChain, compilePermissionPreset, compileSecretMasker, compileVerifiedLayer, constantTimeEqual, costReportFromJournal, countsAgainstLimit, createCanonicalIdMinter, createCtx, createEngine, createEnvelopeEncryption, createSandboxBridge, currentOnlyKeyRing, decodeCheckpoint, dedupeRepeatedClaims, defineWorkflow, deriveContentKey, deriverV1, deriverV2, digestOf, dispositionHook, emptyDigestBlocks, emptyToolset, encodeCheckpoint, entryUsageSlices, escalateTool, evaluatePermission, evaluateReuse, evidencePreservedValidator, executeWorkflow, exhaustionCodeOf, extractCandidate, failoverTriggerOf, fallbackTriggerOf, filterClaimsForRun, finalizeFires, foldTermination, formatRePrompt, formatScopePath, hasFencedWrites, hasMetaLookup, hashRunArgs, hashRunOutput, hashWorkflowBody, hashWorkflowSource, identityJcs, implementationAgentProfile, invoiceFromJournal, isEscalated, isSchemaPairSpec, isStandardSchemaSpec, isStrictCompatibleSchema, kMaxOf, knowledgeHash, ladderLengthOf, ladderRungChoice, lastRunSettle, latestProgressReport, lexShellCommand, liftRetainedParts, lineageWeightOf, localKeyProvider, makeOrchestratorWorkflow, maskSecrets, maskSecretsDeep, maskSecretsJson, matchArgvPattern, matchShellCommand, mcp, memoryQuotaLimiter, mergeQuotaDenial, mergeUsageLimits, metaMatchesFilter, minMatchesValidator, modelEpochOf, modelKnowledgeCard, modelSpecIdentity, needsSeparateExtract, nextFailover, nodeLinkKey, normalizeApproachTag, normalizeEntry, normalizeFallbacks, orchestrate, parallelScope, parseModelRef, parseScopePath, phiInitialOf, pipelineScope, planNodeScope, preflightEstimate, priceEntryUsage, priceUsdOf, profileCard, profileRegistrySnapshotHash, progressReportTool, projectHistory, projectIdentity, projectToJsonSchema, proposalStatement, providerOf, quotaActualTokens, quotaEstimateTokens, quotaRuleAdmission, quotaRuleMatches, readRunMeta, readTerminationInit, reconcileRunMeta, reduceAuditTrail, reduceCriticalPath, reduceInvocationTable, registryKeyRing, remeasureQueue, replayDisposition, repositoryResearchToolset, requiredFieldsValidator, requiredSectionsValidator, researchAgentProfile, resolveModelInvocation, resolvePricing, resolveToolset, retryClassOf, retryDelayMs, reviewAgentProfile, roleConfiguredInRouting, roundOneDisposition, runAgent, runProfile, sanitizeTerminalText, sanitizeTokenCount, sanitizeUsage, sanitizeUsageDelta, scanJournalCompatibility, schemaHash, schemaHashOfSpec, selectStructuredOutputTier, shouldCompact, snapshotUsage, spawnDepthOf, summarizeInstruction, summarizeOutput, terminationConfigDrift, tierWithinCaps, toApprovalDecision, toJournalValue, tool, toolContract, toolsetHash, ttlState, usageViolations, validateEditorialCommit, validateEngineQuotaConfig, validateEntryShape, validateEscalationLimits, validateEscalationReport, validateQuotaRules, validateRetryPolicy, validateSchemaSpec, validateTerminationLimits, validateUsageLimits, workflowScope, workflowSourceRef, wrapJournalStore, wrapTranscriptStore };
19062
+ export { AWAIT_SCHEMA, AdmissionController, AdmissionRejectedError, AgentCallError, BUDGET_ABORT_REASON, BudgetExhaustedError, CANCEL_AGENT_SCHEMA, CHECKPOINT_FORMAT_V1, CLAIM_STATEMENT_MAX_CHARS, CLAIM_TTL_DAYS, COMPACTION_SUMMARY_PREFIX, CURRENT_HASH_VERSION, ConfigError, DEFAULT_CHILD_BUDGET_FRACTION, DEFAULT_CHILD_RESULT_PAGE_CHARS, DEFAULT_CITATION_PATTERN, DEFAULT_COMPACTION_THRESHOLD, DEFAULT_ESCALATION_LIMITS, DEFAULT_EVIDENCE_MIN_SHARE, DEFAULT_FINISH_MAX_REPAIRS, DEFAULT_FLAT_RESERVE_USD, DEFAULT_MAX_CHILDREN_PER_NODE, DEFAULT_MAX_DEPTH, DEFAULT_MAX_OSCILLATIONS_PER_KEY, DEFAULT_MAX_PINNED_WORKTREES, DEFAULT_MAX_REVISIONS_PER_RUN, DEFAULT_MAX_TOTAL_SPAWNS, DEFAULT_MAX_TURNS, DEFAULT_MODEL_RETRY_ATTEMPTS, DEFAULT_NO_PROGRESS_TURNS, DEFAULT_PER_RUN_CONCURRENCY, DEFAULT_RETRY_POLICY, DEFAULT_STREAM_IDLE_TIMEOUT_MS, DEFAULT_SYNTHESIS_MAX_TURNS, DEFAULT_SYNTHESIS_NOTE_MAX_TURNS, DedupIndex, DeterminismError, EMIT_RESULT_TOOL, EMPTY_SCHEMA_HASH, EMPTY_TOOLSET_HASH, ESCALATE_TOOL_NAME, ESCALATION_REPORT_SCHEMA, ESCALATION_REQUEST_SCHEMA, EVENT_SEGMENT_STRIDE, EscalationDecisionAbortedError, EventBus, ExternalRegistry, FINALIZE_SYNTHESIS_INSTRUCTION, FINISH_SCHEMA, FINISH_TOOL_NAME, FailRunError, FileModelKnowledgeStore, FileTranscriptStore, GET_CHILD_RESULT_SCHEMA, GET_CHILD_RESULT_TOOL_NAME, GitWorktreeProvider, IMPLEMENTATION_PROFILE_LIMITS, INBOX_PROPOSAL_TTL_DAYS, InMemoryStore, InMemoryTranscriptStore, InProcessRunner, InvalidResolutionError, JOURNAL_ENVELOPE_MARKER, JournalCompatibilityError, JournalMatcher, JournalMissError, JournalOrderViolation, JsonlFileStore, KB_ACTIVE_CLAIMS_CAP, KB_CARD_RENDER_BUDGET_CHARS, KeyedLimiter, KnowledgeCasError, LARGE_VALUE_WARN_BYTES, LEGACY_LTID_PREFIX, LEGACY_SIGNATURE_INPUTS, LINEAGE_SIG_VERSION, LeaseHeldError, LineageIndex, MASKED_SECRET, MAX_CHILD_RESULT_PAGE_CHARS, MAX_DEPTH_CEILING, ModelRetry, NoProgressDetector, NonSerializableValueError, ORCHESTRATE_WORKFLOW_NAME, OrchestratorCapConfigError, PARALLEL_AGENTS_SCHEMA, PROGRESS_REPORT_TOOL_NAME, ParallelSiteCounter, PlanInvariantError, QUOTA_WINDOW_MS, READ_CHILD_ARTIFACT_SCHEMA, READ_CHILD_ARTIFACT_TOOL_NAME, RESEARCH_PROFILE_LIMITS, REVIEW_PROFILE_LIMITS, ROLE_EFFORT_DEFAULTS, ROOT_ACCOUNT, ROOT_SCOPE, RUN_PROFILES, RUN_SETTLE_DECISION_TYPE, ReplayPlanHashMismatch, Replayer, ResolutionArbiter, ResolutionFold, RulvarError, RunBudget, SANDBOX_AGENT_OPT_KEYS, SPAWN_AGENT_SCHEMA, SandboxError, ScriptRejected, Semaphore, SettlementError, SpanRegistry, TOOL_NAME_PATTERN, TerminationAccount, WAIT_FOR_EVENTS_SCHEMA, WAIT_FOR_EVENTS_TOOL_NAME, WAKE_SUMMARY_RENDER_BUDGET_CHARS, admissionReserveUsd, affordableOutputTokens, agentErrorFromWire, agentErrorToWire, agentResultWire, agentScope, applyClaimOps, applyStructuredOutputTier, approachSigCoarse, approachSigOf, archiveDeprecatedModelOps, assertFencedWrites, atCompactionThreshold, auditRun, auditRuns, buildAbandonFold, buildAdapterRegistry, buildCostReport, buildDeriverRegistry, buildOrchestratorTools, buildTerminationInitValue, buildToolContext, canRideLoopTurn, canonicalIsolationTag, canonicalizeLadder, canonicalizeSchema, capIssues, capsHashOf, checkFloors, checkpointRefFor, childCoveragePrefix, claimExpired, claimExpiry, claimIssues, claimOpIssues, classifyAgentError, classifyAttemptOutcome, collectDeclaredLadders, compactMessages, compilePermissionChain, compilePermissionPreset, compileSecretMasker, compileVerifiedLayer, constantTimeEqual, costReportFromJournal, countsAgainstLimit, createCanonicalIdMinter, createCtx, createEngine, createEnvelopeEncryption, createSandboxBridge, currentOnlyKeyRing, decodeCheckpoint, dedupeRepeatedClaims, defineWorkflow, deriveContentKey, deriverV1, deriverV2, digestOf, dispatchProjectionReserveUsd, dispositionHook, emptyDigestBlocks, emptyToolset, encodeCheckpoint, entryUsageSlices, escalateTool, evaluatePermission, evaluateReuse, evidencePreservedValidator, executeWorkflow, exhaustionCodeOf, extractCandidate, failoverTriggerOf, fallbackTriggerOf, filterClaimsForRun, finalizeFires, foldTermination, formatRePrompt, formatScopePath, hasFencedWrites, hasMetaLookup, hashRunArgs, hashRunOutput, hashWorkflowBody, hashWorkflowSource, identityJcs, implementationAgentProfile, invoiceFromJournal, isEscalated, isSchemaPairSpec, isStandardSchemaSpec, isStrictCompatibleSchema, kMaxOf, knowledgeHash, ladderLengthOf, ladderRungChoice, lastRunSettle, latestProgressReport, lexShellCommand, liftRetainedParts, lineageWeightOf, localKeyProvider, makeOrchestratorWorkflow, maskSecrets, maskSecretsDeep, maskSecretsJson, matchArgvPattern, matchShellCommand, mcp, memoryQuotaLimiter, mergeQuotaDenial, mergeUsageLimits, metaMatchesFilter, minMatchesValidator, modelEpochOf, modelKnowledgeCard, modelSpecIdentity, needsSeparateExtract, nextFailover, nodeLinkKey, normalizeApproachTag, normalizeEntry, normalizeFallbacks, orchestrate, orchestratorAdmissionEstCostUsd, parallelScope, parseModelRef, parseScopePath, phiInitialOf, pipelineScope, planNodeScope, preflightEstimate, priceEntryUsage, priceUsdOf, profileCard, profileRegistrySnapshotHash, progressReportTool, projectHistory, projectIdentity, projectToJsonSchema, proposalStatement, providerOf, quotaActualTokens, quotaEstimateTokens, quotaRuleAdmission, quotaRuleMatches, readRunMeta, readTerminationInit, reconcileRunMeta, reduceAuditTrail, reduceCriticalPath, reduceInvocationTable, registryKeyRing, remeasureQueue, replayDisposition, repositoryResearchToolset, requiredFieldsValidator, requiredSectionsValidator, researchAgentProfile, resolveModelInvocation, resolvePricing, resolveToolset, retryClassOf, retryDelayMs, reviewAgentProfile, roleConfiguredInRouting, roundOneDisposition, runAgent, runProfile, sanitizeTerminalText, sanitizeTokenCount, sanitizeUsage, sanitizeUsageDelta, scanJournalCompatibility, schemaHash, schemaHashOfSpec, selectStructuredOutputTier, shouldCompact, snapshotUsage, spawnDepthOf, summarizeInstruction, summarizeOutput, terminationConfigDrift, tierWithinCaps, toApprovalDecision, toJournalValue, tool, toolContract, toolsetHash, ttlState, usageViolations, validateEditorialCommit, validateEngineQuotaConfig, validateEntryShape, validateEscalationLimits, validateEscalationReport, validateQuotaRules, validateRetryPolicy, validateSchemaSpec, validateTerminationLimits, validateUsageLimits, workflowScope, workflowSourceRef, wrapJournalStore, wrapTranscriptStore };