@rulvar/core 1.61.0 → 1.63.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 +220 -2
  2. package/dist/index.js +1870 -1334
  3. package/package.json +1 -1
package/dist/index.js CHANGED
@@ -275,6 +275,47 @@ var LeaseHeldError = class extends RulvarError {
275
275
  }
276
276
  };
277
277
  /**
278
+ * The segment computed its outcome but a settlement write failed with a
279
+ * NON-fencing store error, so nothing durable records that the run
280
+ * settled. `handle.result` rejects with this instead of resolving,
281
+ * because a caller acting on an unrecorded outcome is exactly the split
282
+ * view an authoritative store exists to prevent. `stage` names the
283
+ * write that failed: 'run-settle' is the journal decision entry (when
284
+ * it fails the terminal meta write is SKIPPED, so the projection can
285
+ * never run ahead of the journal), 'meta' is the terminal RunMeta
286
+ * projection (the journal settle IS durable; only the projection is
287
+ * behind, the same residue a crash between the two writes leaves).
288
+ * Every entry the run appended before settlement is already durable,
289
+ * so recovery is deterministic: resume the run and replay re-settles
290
+ * the same outcome without a provider call, or reconcile the store
291
+ * with `rulvar runs audit [--repair]`. A superseded segment's fencing
292
+ * rejection (LeaseHeldError) is NOT this error and stays swallowed:
293
+ * the successor owns settlement. `data` records
294
+ * { runId, runStatus, stage }.
295
+ */
296
+ var SettlementError = class extends RulvarError {
297
+ code = "settlement";
298
+ /** The settlement write that failed first. */
299
+ stage;
300
+ runId;
301
+ /** The outcome status the segment computed and could not record. */
302
+ runStatus;
303
+ constructor(message, opts) {
304
+ super(message, {
305
+ retryable: true,
306
+ data: {
307
+ runId: opts.runId,
308
+ runStatus: opts.runStatus,
309
+ stage: opts.stage
310
+ },
311
+ cause: opts.cause
312
+ });
313
+ this.stage = opts.stage;
314
+ this.runId = opts.runId;
315
+ this.runStatus = opts.runStatus;
316
+ }
317
+ };
318
+ /**
278
319
  * commit() on a ModelKnowledgeStore against a snapshot version that is
279
320
  * no longer current. Retryable by contract: re-read current(), rebase
280
321
  * the ops, commit again, mirroring the lease fencing discipline.
@@ -8320,293 +8361,259 @@ function invoiceFromJournal(entries, priceUsd) {
8320
8361
  };
8321
8362
  }
8322
8363
  //#endregion
8323
- //#region src/engine/run-profiles.ts
8364
+ //#region src/model/router.ts
8324
8365
  /**
8325
- * The shipped presets (fast / standard / deep / ultra "and similar").
8326
- * Data only; a review-time assertion checks the
8327
- * engine has zero behavioral branches keyed on these names.
8366
+ * Model router core (M1-T05): the per-engine adapter registry, ModelRef
8367
+ * parsing, the per-invocation resolution chain, canonicalization into
8368
+ * CanonicalModelSpec, and caps scrubbing with visible scrub notes.
8369
+ *
8370
+ * Public contract: https://docs.rulvar.com/guide/model-routing.
8328
8371
  */
8329
- const RUN_PROFILES = {
8330
- fast: {
8331
- effortByRole: {
8332
- orchestrate: "low",
8333
- plan: "low",
8334
- summarize: "low",
8335
- extract: "low"
8336
- },
8337
- perRunConcurrency: 16,
8338
- permissionPreset: "standard",
8339
- lifetimeSpawnCap: 64,
8340
- maxDepth: 1
8341
- },
8342
- standard: {
8343
- effortByRole: {
8344
- orchestrate: "high",
8345
- plan: "high",
8346
- summarize: "low",
8347
- extract: "low"
8348
- },
8349
- perRunConcurrency: 12,
8350
- permissionPreset: "standard",
8351
- lifetimeSpawnCap: 500,
8352
- maxDepth: 1
8353
- },
8354
- deep: {
8355
- effortByRole: {
8356
- orchestrate: "high",
8357
- plan: "high",
8358
- summarize: "medium",
8359
- extract: "medium"
8360
- },
8361
- perRunConcurrency: 8,
8362
- permissionPreset: "standard",
8363
- lifetimeSpawnCap: 500,
8364
- maxDepth: 2
8365
- },
8366
- ultra: {
8367
- effortByRole: {
8368
- orchestrate: "max",
8369
- plan: "max",
8370
- summarize: "high",
8371
- extract: "high"
8372
- },
8373
- perRunConcurrency: 8,
8374
- permissionPreset: "strict",
8375
- lifetimeSpawnCap: 500,
8376
- maxDepth: 3
8372
+ /**
8373
+ * Per-engine adapter registry: strictly per engine, no global mutable
8374
+ * registry exists. A duplicate adapterId is a typed ConfigError.
8375
+ */
8376
+ function buildAdapterRegistry(adapters) {
8377
+ const registry = /* @__PURE__ */ new Map();
8378
+ for (const adapter of adapters) {
8379
+ if (registry.has(adapter.id)) throw new ConfigError(`duplicate adapterId '${adapter.id}' at createEngine`);
8380
+ registry.set(adapter.id, adapter);
8377
8381
  }
8378
- };
8379
- /** Looks up a shipped RunProfile by name; undefined for unknown names. */
8380
- function runProfile(name) {
8381
- return RUN_PROFILES[name];
8382
+ return registry;
8382
8383
  }
8383
- //#endregion
8384
- //#region src/model/caps.ts
8385
- const TIER_ORDER = {
8386
- native: 2,
8387
- "forced-tool": 1,
8388
- prompt: 0
8389
- };
8390
8384
  /**
8391
- * Strict-schema compatibility as both first-class providers define it:
8392
- * every object node declares `additionalProperties: false` and lists every
8393
- * property in `required`. Boolean schemas and
8394
- * non-object shapes are trivially compatible.
8385
+ * ModelRef is strictly 'adapterId:model', no query parameters. The wire
8386
+ * model id may itself contain colons (for example ollama tags), so only
8387
+ * the FIRST colon splits.
8395
8388
  */
8396
- function isStrictCompatibleSchema(schema) {
8397
- if (typeof schema === "boolean") return true;
8398
- if (schema.type === "object" || schema.properties !== void 0 || schema.additionalProperties !== void 0) {
8399
- if (schema.additionalProperties !== false) return false;
8400
- const properties = typeof schema.properties === "object" && schema.properties !== null ? schema.properties : {};
8401
- const required = Array.isArray(schema.required) ? schema.required : [];
8402
- for (const name of Object.keys(properties)) if (!required.includes(name)) return false;
8403
- for (const value of Object.values(properties)) if (typeof value === "object" && value !== null || typeof value === "boolean") {
8404
- if (!isStrictCompatibleSchema(value)) return false;
8405
- }
8406
- }
8407
- for (const key of [
8408
- "items",
8409
- "additionalProperties",
8410
- "contains"
8411
- ]) {
8412
- const value = schema[key];
8413
- if (typeof value === "object" && value !== null || typeof value === "boolean") {
8414
- if (!isStrictCompatibleSchema(value)) return false;
8415
- }
8416
- }
8417
- for (const key of [
8418
- "allOf",
8419
- "anyOf",
8420
- "oneOf",
8421
- "prefixItems"
8422
- ]) {
8423
- const value = schema[key];
8424
- if (Array.isArray(value)) {
8425
- for (const element of value) if (typeof element === "object" && element !== null || typeof element === "boolean") {
8426
- if (!isStrictCompatibleSchema(element)) return false;
8427
- }
8428
- }
8429
- }
8430
- return true;
8389
+ function parseModelRef(ref) {
8390
+ const colon = ref.indexOf(":");
8391
+ if (colon <= 0 || colon === ref.length - 1) throw new ConfigError(`invalid ModelRef '${ref}': expected the strict 'adapterId:model' form`);
8392
+ return {
8393
+ adapterId: ref.slice(0, colon),
8394
+ model: ref.slice(colon + 1)
8395
+ };
8431
8396
  }
8432
8397
  /**
8433
- * Tier selection: the model's declared ceiling
8434
- * bounds the tier; the native tier additionally requires a
8435
- * strict-compatible canonical schema (relying on silent server-side
8436
- * fallback is forbidden), degrading to forced-tool.
8437
- * Prefill is not a tier.
8398
+ * Role effort defaults: orchestrate and plan default to high; summarize and extract
8399
+ * default to low. loop and finalize have NO role default: when the chain
8400
+ * resolves nothing, the wire omits effort and identity records the spec
8401
+ * with the effort member absent.
8438
8402
  */
8439
- function selectStructuredOutputTier(caps, canonicalSchema) {
8440
- const ceiling = caps.structuredOutput;
8441
- if (ceiling === "native" && !isStrictCompatibleSchema(canonicalSchema)) return "forced-tool";
8442
- return ceiling;
8403
+ const ROLE_EFFORT_DEFAULTS = {
8404
+ orchestrate: "high",
8405
+ plan: "high",
8406
+ summarize: "low",
8407
+ extract: "low"
8408
+ };
8409
+ function contribution(spec, _role) {
8410
+ if (spec === void 0) return {};
8411
+ if (typeof spec === "string") return { model: spec };
8412
+ if ("ladder" in spec) return { ladder: spec.ladder };
8413
+ const choice = spec;
8414
+ const fields = { model: choice.model };
8415
+ if (choice.effort !== void 0) fields.effort = choice.effort;
8416
+ if (choice.providerOptions !== void 0) fields.providerOptions = choice.providerOptions;
8417
+ if (choice.fallbacks !== void 0) fields.fallbacks = choice.fallbacks;
8418
+ return fields;
8443
8419
  }
8444
- /** True when `tier` is at or below the model's declared ceiling. */
8445
- function tierWithinCaps(tier, caps) {
8446
- return TIER_ORDER[tier] <= TIER_ORDER[caps.structuredOutput];
8420
+ function layerFields(layer, role) {
8421
+ if (layer === void 0) return {};
8422
+ const fromModel = contribution(layer.model, role);
8423
+ const fromRouting = contribution(layer.routing?.[role], role);
8424
+ const merged = {
8425
+ ...fromModel,
8426
+ ...pruneUndefined(fromRouting)
8427
+ };
8428
+ if (layer.effort !== void 0) merged.effort = layer.effort;
8429
+ return merged;
8447
8430
  }
8448
- //#endregion
8449
- //#region src/engine/scheduler.ts
8431
+ function pruneUndefined(value) {
8432
+ const out = {};
8433
+ for (const [key, member] of Object.entries(value)) if (member !== void 0) out[key] = member;
8434
+ return out;
8435
+ }
8436
+ function mergeProviderOptions(lower, higher) {
8437
+ if (lower === void 0) return higher;
8438
+ if (higher === void 0) return lower;
8439
+ const merged = { ...lower };
8440
+ for (const [namespace, options] of Object.entries(higher)) merged[namespace] = {
8441
+ ...merged[namespace],
8442
+ ...options
8443
+ };
8444
+ return merged;
8445
+ }
8446
+ /** Sampling parameters both first-class providers reject on reasoning models. */
8447
+ const SAMPLING_KEYS = [
8448
+ "temperature",
8449
+ "top_p",
8450
+ "top_k"
8451
+ ];
8450
8452
  /**
8451
- * Scheduler and concurrency (M1-T08): the per-run semaphore with a FIFO
8452
- * queue (default 12 concurrent model calls). The engine lifetime spawn cap
8453
- * is enforced by the budget layer at admission; parallel/pipeline
8454
- * composition semantics live with ctx.
8455
- * Per-provider concurrency keys land with M4.
8453
+ * Resolution runs on every model invocation, not once per agent: a layered
8454
+ * merge of { model, effort, providerOptions, fallbacks } in the order call
8455
+ * override > agent profile > workflow defaults > engine defaults, with the
8456
+ * invocation role attached as a tag.
8457
+ * After resolution the router reads ModelCaps and scrubs illegal
8458
+ * parameters visibly: unsupported effort is removed from the wire but
8459
+ * kept in identity; sampling params rejected by the model are removed
8460
+ * from the adapter's namespace, never silently sent.
8456
8461
  */
8457
- /** FIFO semaphore; default per-run width is 12. */
8458
- const DEFAULT_PER_RUN_CONCURRENCY = 12;
8459
- var Semaphore = class {
8460
- limit;
8461
- active = 0;
8462
- waiters = [];
8463
- /**
8464
- * `limit` must be a positive integer: anything else (NaN included) is
8465
- * a typed ConfigError. Before this gate a NaN limit made
8466
- * `active < limit` permanently false, so the first acquire queued
8467
- * forever and the run could not settle, not even through cancel()
8468
- * (v1.34.0 review P2-4). Unlimited is expressed by not constructing a
8469
- * semaphore, never by a sentinel limit.
8470
- */
8471
- constructor(limit) {
8472
- requirePositiveInteger(limit, "Semaphore limit");
8473
- this.limit = limit;
8474
- }
8475
- get pending() {
8476
- return this.waiters.length;
8462
+ function resolveModelInvocation(options) {
8463
+ const { role } = options;
8464
+ const layers = [
8465
+ options.engine,
8466
+ options.workflow,
8467
+ options.profile,
8468
+ options.call
8469
+ ];
8470
+ let merged = {};
8471
+ for (const layer of layers) {
8472
+ const fields = layerFields(layer, role);
8473
+ merged = {
8474
+ ...merged,
8475
+ ...pruneUndefined(fields),
8476
+ providerOptions: mergeProviderOptions(merged.providerOptions, fields.providerOptions)
8477
+ };
8478
+ if (fields.ladder !== void 0) delete merged.model;
8479
+ else if (fields.model !== void 0) delete merged.ladder;
8477
8480
  }
8478
- /**
8479
- * Acquires a slot, resolving in FIFO order. `onQueued` fires only when
8480
- * the caller actually has to wait (feeds the agent:queued event).
8481
- * An aborted `signal` releases the caller from the queue without a
8482
- * slot: the returned release is a no-op, the remaining waiters keep
8483
- * their FIFO positions, and the caller proceeds to observe its own
8484
- * aborted signal (the model layers refuse dispatch under an aborted
8485
- * signal, so no provider call follows). Cancellation can therefore
8486
- * always drain a queued run (v1.34.0 review P2-4).
8487
- */
8488
- async acquire(onQueued, signal) {
8489
- if (this.active < this.limit) {
8490
- this.active += 1;
8491
- return () => this.release();
8492
- }
8493
- if (signal?.aborted === true) return () => void 0;
8494
- onQueued?.();
8495
- const waiter = {
8496
- resolve: () => void 0,
8497
- aborted: false
8498
- };
8499
- const wait = new Promise((resolve) => {
8500
- waiter.resolve = resolve;
8481
+ if (merged.ladder !== void 0) throw new ConfigError(`a ladder ModelSpec wins wire resolution for role '${role}': ladder execution is owned by the PlanRunner ladder driver, which resolves each rung attempt to a concrete model override; dispatch laddered profiles through orchestratePlanned or pass a plain ModelRef or ModelChoice`);
8482
+ if (merged.model === void 0) throw new ConfigError(`no model resolves for role '${role}': set AgentOpts.model, a profile model, or engine defaults.routing`);
8483
+ checkFloors({
8484
+ ref: merged.model,
8485
+ role,
8486
+ ...options.floors === void 0 ? {} : { floors: options.floors },
8487
+ ...options.taskClass === void 0 ? {} : { taskClass: options.taskClass }
8488
+ });
8489
+ const requestedEffort = merged.effort ?? ROLE_EFFORT_DEFAULTS[role];
8490
+ const { adapterId, model } = parseModelRef(merged.model);
8491
+ let caps;
8492
+ try {
8493
+ caps = options.capsOf(merged.model);
8494
+ } catch (thrown) {
8495
+ if (thrown instanceof ConfigError) throw new ConfigError(`role '${role}': ${thrown.message}`);
8496
+ throw thrown;
8497
+ }
8498
+ const scrubs = [];
8499
+ let wireEffort = requestedEffort;
8500
+ if (wireEffort !== void 0 && !caps.reasoningEfforts.includes(wireEffort)) {
8501
+ scrubs.push({
8502
+ scrubbed: "effort",
8503
+ model: merged.model,
8504
+ detail: `effort '${wireEffort}' is not in caps.reasoningEfforts for ${merged.model}; the request proceeds without it (identity keeps the requested effort)`
8501
8505
  });
8502
- this.waiters.push(waiter);
8503
- let onAbort;
8504
- if (signal !== void 0) {
8505
- onAbort = () => {
8506
- const index = this.waiters.indexOf(waiter);
8507
- if (index === -1) return;
8508
- this.waiters.splice(index, 1);
8509
- waiter.aborted = true;
8510
- waiter.resolve();
8511
- };
8512
- signal.addEventListener("abort", onAbort, { once: true });
8513
- }
8514
- try {
8515
- await wait;
8516
- } finally {
8517
- if (signal !== void 0 && onAbort !== void 0) signal.removeEventListener("abort", onAbort);
8518
- }
8519
- if (waiter.aborted) return () => void 0;
8520
- this.active += 1;
8521
- return () => this.release();
8506
+ wireEffort = void 0;
8522
8507
  }
8523
- async withSlot(fn, onQueued, signal) {
8524
- const release = await this.acquire(onQueued, signal);
8525
- try {
8526
- return await fn();
8527
- } finally {
8528
- release();
8508
+ let providerOptions = merged.providerOptions;
8509
+ if (providerOptions?.[adapterId] !== void 0 && !caps.supportsTemperature) {
8510
+ const namespace = { ...providerOptions[adapterId] };
8511
+ const removed = SAMPLING_KEYS.filter((key) => key in namespace);
8512
+ if (removed.length > 0) {
8513
+ for (const key of removed) delete namespace[key];
8514
+ providerOptions = {
8515
+ ...providerOptions,
8516
+ [adapterId]: namespace
8517
+ };
8518
+ scrubs.push({
8519
+ scrubbed: "sampling",
8520
+ model: merged.model,
8521
+ detail: `sampling parameter(s) ${removed.join(", ")} removed for ${merged.model}: the model rejects them (caps.supportsTemperature is false); never silently sent`
8522
+ });
8529
8523
  }
8530
8524
  }
8531
- release() {
8532
- this.active -= 1;
8533
- const next = this.waiters.shift();
8534
- if (next !== void 0) next.resolve();
8535
- }
8536
- };
8537
- //#endregion
8538
- //#region src/model/concurrency.ts
8539
- /**
8540
- * Per-provider concurrency keys (M4-T07): a keyed limiter beside the
8541
- * router, ENGINE-scoped (keys constrain calls
8542
- * across a single engine per adapter). The Appendix A default is
8543
- * unlimited: an embeddable library must not surprise-throttle hosts, so
8544
- * the per-run semaphore stays the only default bound and provider 429s
8545
- * ride RetryPolicy; hosts with known tier limits opt in per adapter id
8546
- * via createEngine concurrency.perProvider.
8547
- *
8548
- * This keyed limiter bounds PARALLELISM inside one engine only. Two
8549
- * processes sharing one API key coordinate through the QuotaLimiter
8550
- * SPI instead (RV-215, createEngine `quota`): rate and volume live
8551
- * there, in shared storage; in-flight slots live here.
8552
- */
8553
- var KeyedLimiter = class {
8554
- semaphores = /* @__PURE__ */ new Map();
8555
- constructor(caps) {
8556
- for (const [key, limit] of Object.entries(caps ?? {})) this.semaphores.set(key, new Semaphore(limit));
8557
- }
8558
- /** Queue depth for one key (0 for unlimited keys); telemetry only. */
8559
- pending(key) {
8560
- return this.semaphores.get(key)?.pending ?? 0;
8525
+ const canonical = requestedEffort === void 0 ? {
8526
+ kind: "model",
8527
+ model: merged.model
8528
+ } : {
8529
+ kind: "model",
8530
+ model: merged.model,
8531
+ effort: requestedEffort
8532
+ };
8533
+ const resolved = {
8534
+ ref: merged.model,
8535
+ adapterId,
8536
+ model,
8537
+ canonical,
8538
+ scrubs
8539
+ };
8540
+ if (wireEffort !== void 0) resolved.wireEffort = wireEffort;
8541
+ if (requestedEffort !== void 0) resolved.requestedEffort = requestedEffort;
8542
+ if (providerOptions !== void 0) resolved.providerOptions = providerOptions;
8543
+ if (merged.fallbacks !== void 0) resolved.fallbacks = merged.fallbacks;
8544
+ return resolved;
8545
+ }
8546
+ /** The closed trigger vocabulary guard. */
8547
+ const TRIGGER_CLASSES = [
8548
+ "error",
8549
+ "limit",
8550
+ "schema-exhausted",
8551
+ "verify-failed",
8552
+ "no-progress"
8553
+ ];
8554
+ function validateGate(gate, rungCount, index) {
8555
+ if (gate.kind === "mechanical") {
8556
+ if (typeof gate.profile !== "string" || gate.profile === "") throw new ConfigError(`ladder acceptance gate ${String(index)}: a mechanical gate names a registered gate profile`);
8557
+ return;
8561
8558
  }
8562
- /**
8563
- * Runs `fn` under the key's semaphore; keys without a configured cap
8564
- * run unlimited (no queueing, no overhead). An aborted `signal` frees
8565
- * a queued caller without a slot (the Semaphore contract), so run
8566
- * cancellation drains provider queues too (v1.34.0 review P2-4).
8567
- */
8568
- async withSlot(key, fn, onQueued, signal) {
8569
- const semaphore = this.semaphores.get(key);
8570
- if (semaphore === void 0) return fn();
8571
- return semaphore.withSlot(fn, onQueued, signal);
8559
+ if (gate.kind === "judge") {
8560
+ if (typeof gate.rung === "number") {
8561
+ if (!Number.isInteger(gate.rung) || gate.rung < 0 || gate.rung >= rungCount) throw new ConfigError(`ladder acceptance gate ${String(index)}: judge rung ${String(gate.rung)} is not a declared rung of a ${String(rungCount)}-rung ladder (FR-119)`);
8562
+ return;
8563
+ }
8564
+ parseModelRef(gate.rung);
8565
+ return;
8572
8566
  }
8573
- };
8574
- //#endregion
8575
- //#region src/model/failover.ts
8576
- /** Normalizes the author-facing ModelChoice.fallbacks list. */
8577
- function normalizeFallbacks(refs) {
8578
- return (refs ?? []).map((model) => ({ model }));
8579
- }
8580
- /**
8581
- * Maps a retry class to its failover trigger once retries exhaust.
8582
- * Overloaded (529) is transport-class for failover purposes; a
8583
- * non-retryable error never fails over.
8584
- */
8585
- function failoverTriggerOf(retryClass) {
8586
- if (retryClass === void 0) return;
8587
- return retryClass === "rate-limit" ? "rate-limit" : "transport";
8567
+ if (!(gate.fraction > 0 && gate.fraction <= 1)) throw new ConfigError(`ladder acceptance gate ${String(index)}: a spot-check fraction lies in (0, 1], got ${String(gate.fraction)}`);
8588
8568
  }
8589
8569
  /**
8590
- * The next target index past `from` that serves `trigger`, or undefined
8591
- * when the chain is exhausted. Index 0 is the primary; the chain never
8592
- * moves backwards (sticky failover).
8570
+ * Canonicalizes a declared LadderSpec: validates the
8571
+ * shape once (FR-119 judge declaration included) and resolves every rung's
8572
+ * effort to an explicit value. `chainEffort` is the effort the resolution
8573
+ * chain would contribute at the declaring layer; a rung that resolves no
8574
+ * effort at all is a ConfigError (the canonical form has no absent-effort
8575
+ * member by declaration).
8593
8576
  */
8594
- function nextFailover(targets, trigger, from) {
8595
- for (let index = from + 1; index < targets.length; index += 1) {
8596
- const on = targets[index]?.on;
8597
- if (on === void 0 || on.includes(trigger)) return index;
8598
- }
8577
+ function canonicalizeLadder(spec, options) {
8578
+ if (!Array.isArray(spec.rungs) || spec.rungs.length === 0) throw new ConfigError("a ladder declares at least one rung");
8579
+ if (!Number.isInteger(spec.startTier) || spec.startTier < 0 || spec.startTier >= spec.rungs.length) throw new ConfigError(`ladder startTier ${String(spec.startTier)} is not a declared rung index of a ${String(spec.rungs.length)}-rung ladder`);
8580
+ for (const trigger of spec.escalateOn) if (!TRIGGER_CLASSES.includes(trigger)) throw new ConfigError(`unknown ladder trigger '${String(trigger)}': the vocabulary is closed to ${TRIGGER_CLASSES.join(", ")}`);
8581
+ const rungs = spec.rungs.map((rung, index) => {
8582
+ parseModelRef(rung.model);
8583
+ if (!Number.isInteger(rung.maxTurns) || rung.maxTurns <= 0) throw new ConfigError(`ladder rung ${String(index)}: maxTurns is a positive integer`);
8584
+ if (!Number.isInteger(rung.maxTokens) || rung.maxTokens <= 0) throw new ConfigError(`ladder rung ${String(index)}: maxTokens is a positive integer`);
8585
+ if (rung.maxCostUsd !== void 0 && !(rung.maxCostUsd > 0)) throw new ConfigError(`ladder rung ${String(index)}: maxCostUsd is positive when present`);
8586
+ const effort = rung.effort ?? options?.chainEffort;
8587
+ if (effort === void 0) throw new ConfigError(`ladder rung ${String(index)} resolves no effort: the canonical ladder embeds explicit efforts; declare rung.effort or a chain effort`);
8588
+ return {
8589
+ model: rung.model,
8590
+ effort,
8591
+ maxTurns: rung.maxTurns,
8592
+ maxTokens: rung.maxTokens,
8593
+ ...rung.maxCostUsd === void 0 ? {} : { maxCostUsd: rung.maxCostUsd },
8594
+ ...rung.memoizeOutcome === void 0 ? {} : { memoizeOutcome: rung.memoizeOutcome }
8595
+ };
8596
+ });
8597
+ for (const [index, gate] of (spec.acceptance ?? []).entries()) validateGate(gate, spec.rungs.length, index);
8598
+ return {
8599
+ rungs,
8600
+ startTier: spec.startTier,
8601
+ escalateOn: [...spec.escalateOn],
8602
+ ...spec.acceptance === void 0 ? {} : { acceptance: spec.acceptance.map((gate) => gate) }
8603
+ };
8599
8604
  }
8600
8605
  /**
8601
- * Classifies a terminal agent outcome for the degenerate fallback:
8602
- * schema-mismatch errors are
8603
- * 'schema-exhausted'; any other error is 'error'; limit terminals (the
8604
- * no-progress abort included) are 'limit'; cancelled, escalated, and
8605
- * skipped never trigger.
8606
+ * The concrete ModelChoice of one rung attempt: each attempt is an
8607
+ * ordinary agent scope whose CanonicalModelSpec is that rung's
8608
+ * `{ kind: 'model' }` form.
8606
8609
  */
8607
- function fallbackTriggerOf(outcome) {
8608
- if (outcome.status === "error") return outcome.error?.kind === "schema-mismatch" ? "schema-exhausted" : "error";
8609
- if (outcome.status === "limit") return "limit";
8610
+ function ladderRungChoice(ladder, index) {
8611
+ const rung = ladder.rungs[index];
8612
+ if (rung === void 0) throw new ConfigError(`rung ${String(index)} is not declared on a ${String(ladder.rungs.length)}-rung ladder`);
8613
+ return {
8614
+ model: rung.model,
8615
+ effort: rung.effort
8616
+ };
8610
8617
  }
8611
8618
  //#endregion
8612
8619
  //#region src/model/pricing.ts
@@ -8665,91 +8672,7 @@ function affordableOutputTokens(pricing, remainingUsd, estimatedInputTokens) {
8665
8672
  return Math.floor((remainingUsd - inputUsd) / outputRate * 1e6);
8666
8673
  }
8667
8674
  //#endregion
8668
- //#region src/model/profile-card.ts
8669
- function toolNamesOf(profile) {
8670
- return (profile.tools ?? []).map((entry) => {
8671
- if (typeof entry === "string") return `${entry} (registered toolset)`;
8672
- if ("kind" in entry && entry.kind === "tool") return entry.name;
8673
- return `${entry.id}:* (tool source)`;
8674
- });
8675
- }
8676
- /**
8677
- * Renders the registry into the shared agent vocabulary card. Sorted,
8678
- * deterministic, byte-stable; an empty registry renders explicitly so
8679
- * the planner never guesses at unregistered agentTypes. When the engine
8680
- * registers toolsets, their names render as a closing line (v1.17.0
8681
- * review P1-3): those are the ONLY values valid as string entries of a
8682
- * tools option, so the planner never invents a registry name.
8683
- */
8684
- function profileCard(profiles, toolsets) {
8685
- const toolsetNames = Object.keys(toolsets ?? {}).sort();
8686
- const toolsetsLine = toolsetNames.length === 0 ? void 0 : `Registered toolsets (valid string entries of a tools option): ${toolsetNames.join(", ")}.`;
8687
- const names = Object.keys(profiles ?? {}).sort();
8688
- if (profiles === void 0 || names.length === 0) {
8689
- const empty = "Agent profiles: none registered. Calls take no agentType.";
8690
- return toolsetsLine === void 0 ? empty : `${empty}\n${toolsetsLine}`;
8691
- }
8692
- const lines = ["Agent profiles (agentType values):"];
8693
- for (const name of names) {
8694
- const profile = profiles[name];
8695
- const description = profile.description ?? "no description";
8696
- lines.push(`- ${name}: ${description}`);
8697
- const toolNames = toolNamesOf(profile);
8698
- if (toolNames.length > 0) lines.push(` tools: ${toolNames.join(", ")}`);
8699
- if (profile.taskClass !== void 0) lines.push(` taskClass: ${profile.taskClass}`);
8700
- if (profile.estCost !== void 0) lines.push(` estCost: ${profile.estCost.toFixed(2)} USD`);
8701
- if (profile.escalation !== void 0) lines.push(` escalation: flavor ${profile.escalation.flavor ?? "A"} (opt-in)`);
8702
- }
8703
- if (toolsetsLine !== void 0) lines.push(toolsetsLine);
8704
- return lines.join("\n");
8705
- }
8706
- //#endregion
8707
- //#region src/model/projector.ts
8708
- /** The provider family of an adapter: `provider` when set, else `id`. */
8709
- function providerOf(adapter) {
8710
- return adapter.provider ?? adapter.id;
8711
- }
8712
- /**
8713
- * Projects the canonical history into the target provider's view:
8714
- * provider-raw parts of a DIFFERENT provider are omitted; everything
8715
- * else (text, images, tool calls, tool results, compaction content)
8716
- * passes through untouched. Messages whose parts all belong to another
8717
- * provider vanish entirely rather than ride as empty messages.
8718
- */
8719
- function projectHistory(messages, targetProvider) {
8720
- const projected = [];
8721
- for (const msg of messages) {
8722
- const parts = msg.parts.filter((part) => part.type !== "provider-raw" || part.provider === targetProvider);
8723
- if (parts.length === 0 && msg.parts.length > 0) continue;
8724
- projected.push(parts.length === msg.parts.length ? msg : {
8725
- ...msg,
8726
- parts
8727
- });
8728
- }
8729
- return projected;
8730
- }
8731
- /**
8732
- * Lifts the adapter-shipped retention payload of one finished turn into
8733
- * provider-raw parts (the retention transport). Reads
8734
- * providerMetadata[<adapter id>].retainedParts and tags each block with
8735
- * the adapter's provider family. Returns [] when the adapter shipped
8736
- * nothing.
8737
- */
8738
- function liftRetainedParts(providerMetadata, adapter) {
8739
- const namespace = providerMetadata?.[adapter.id];
8740
- if (typeof namespace !== "object" || namespace === null) return [];
8741
- const retained = namespace.retainedParts;
8742
- if (!Array.isArray(retained)) return [];
8743
- const blocks = retained;
8744
- const provider = providerOf(adapter);
8745
- return blocks.map((block) => ({
8746
- type: "provider-raw",
8747
- provider,
8748
- block
8749
- }));
8750
- }
8751
- //#endregion
8752
- //#region src/model/quota.ts
8675
+ //#region src/model/quota.ts
8753
8676
  /**
8754
8677
  * Quota rules and the in-process reference QuotaLimiter (RV-215).
8755
8678
  * The rule model is shared by every reference implementation
@@ -8966,6 +8889,177 @@ function validateEngineQuotaConfig(config, site = "createEngine quota") {
8966
8889
  if (candidate.onLimiterError !== void 0 && candidate.onLimiterError !== "deny" && candidate.onLimiterError !== "allow") throw new ConfigError(`${site}.onLimiterError must be 'deny' or 'allow' when given`);
8967
8890
  }
8968
8891
  //#endregion
8892
+ //#region src/runtime/usage-limits.ts
8893
+ /**
8894
+ * UsageLimits (M1-T06): normative limit vocabulary and the per-spawn merge.
8895
+ *
8896
+ * Full contract: https://docs.rulvar.com/guide/agents. Expiry of maxTurns, maxToolCalls,
8897
+ * or timeoutMs produces the terminal status 'limit' (paid partial work);
8898
+ * streamIdleTimeoutMs expiry is a retryable transport-class AgentError,
8899
+ * never 'limit'. The run-level deadline is RunOptions.deadlineAt, not a
8900
+ * UsageLimits field.
8901
+ */
8902
+ const DEFAULT_MAX_TURNS = 32;
8903
+ const DEFAULT_STREAM_IDLE_TIMEOUT_MS = 12e4;
8904
+ /**
8905
+ * Limits merge per spawn: AgentOpts.limits over profile limits over engine
8906
+ * defaults.limits.
8907
+ */
8908
+ function mergeUsageLimits(call, profile, engine) {
8909
+ const pick = (key) => call?.[key] ?? profile?.[key] ?? engine?.[key];
8910
+ const merged = {
8911
+ maxTurns: pick("maxTurns") ?? 32,
8912
+ streamIdleTimeoutMs: pick("streamIdleTimeoutMs") ?? 12e4
8913
+ };
8914
+ const maxToolCalls = pick("maxToolCalls");
8915
+ if (maxToolCalls !== void 0) merged.maxToolCalls = maxToolCalls;
8916
+ const maxOutputTokensPerTurn = pick("maxOutputTokensPerTurn");
8917
+ if (maxOutputTokensPerTurn !== void 0) merged.maxOutputTokensPerTurn = maxOutputTokensPerTurn;
8918
+ const timeoutMs = pick("timeoutMs");
8919
+ if (timeoutMs !== void 0) merged.timeoutMs = timeoutMs;
8920
+ const noProgressTurns = pick("noProgressTurns");
8921
+ if (noProgressTurns !== void 0) merged.noProgressTurns = noProgressTurns;
8922
+ const toolBudgetNotices = pick("toolBudgetNotices");
8923
+ if (toolBudgetNotices !== void 0) merged.toolBudgetNotices = toolBudgetNotices;
8924
+ const maxRepeatedToolSignature = pick("maxRepeatedToolSignature");
8925
+ if (maxRepeatedToolSignature !== void 0) merged.maxRepeatedToolSignature = maxRepeatedToolSignature;
8926
+ const maxNoNewEvidenceCalls = pick("maxNoNewEvidenceCalls");
8927
+ if (maxNoNewEvidenceCalls !== void 0) merged.maxNoNewEvidenceCalls = maxNoNewEvidenceCalls;
8928
+ const maxCallsPerTool = pick("maxCallsPerTool");
8929
+ if (maxCallsPerTool !== void 0) merged.maxCallsPerTool = maxCallsPerTool;
8930
+ const toolUnits = pick("toolUnits");
8931
+ if (toolUnits !== void 0) merged.toolUnits = toolUnits;
8932
+ const finalizationReserve = pick("finalizationReserve");
8933
+ if (finalizationReserve !== void 0) merged.finalizationReserve = finalizationReserve;
8934
+ return merged;
8935
+ }
8936
+ /**
8937
+ * Validates one UsageLimits layer at its intake boundary (v1.34.0
8938
+ * review P2-3): a malformed field (NaN, Infinity, a negative, a
8939
+ * fraction) is a typed ConfigError before the merge, before any journal
8940
+ * entry, and before any provider dispatch. `site` names the layer in the
8941
+ * error text (e.g. `RunOptions.limits`). Counts are positive integers
8942
+ * (maxToolCalls may be 0: a spawn that must not call tools).
8943
+ * streamIdleTimeoutMs is handed to setTimeout as-is, so it is bounded by
8944
+ * the Node timer maximum like RetryPolicy delays; timeoutMs is a
8945
+ * wall-clock comparison, so it has no upper bound. Every present field
8946
+ * is checked; absent fields keep their defaults.
8947
+ */
8948
+ function validateUsageLimits(limits, site) {
8949
+ if (limits.maxTurns !== void 0) requirePositiveInteger(limits.maxTurns, `${site}.maxTurns`);
8950
+ if (limits.maxToolCalls !== void 0) requireNonNegativeInteger(limits.maxToolCalls, `${site}.maxToolCalls`);
8951
+ if (limits.maxOutputTokensPerTurn !== void 0) requirePositiveInteger(limits.maxOutputTokensPerTurn, `${site}.maxOutputTokensPerTurn`);
8952
+ if (limits.timeoutMs !== void 0) requirePositiveInteger(limits.timeoutMs, `${site}.timeoutMs`);
8953
+ if (limits.streamIdleTimeoutMs !== void 0) requireTimerDelayMs(limits.streamIdleTimeoutMs, `${site}.streamIdleTimeoutMs`);
8954
+ if (limits.noProgressTurns !== void 0) requirePositiveInteger(limits.noProgressTurns, `${site}.noProgressTurns`);
8955
+ if (limits.toolBudgetNotices !== void 0 && typeof limits.toolBudgetNotices !== "boolean") throw new ConfigError(`${site}.toolBudgetNotices must be a boolean; got ${typeof limits.toolBudgetNotices}`);
8956
+ if (limits.maxRepeatedToolSignature !== void 0) requirePositiveInteger(limits.maxRepeatedToolSignature, `${site}.maxRepeatedToolSignature`);
8957
+ if (limits.maxNoNewEvidenceCalls !== void 0) requirePositiveInteger(limits.maxNoNewEvidenceCalls, `${site}.maxNoNewEvidenceCalls`);
8958
+ if (limits.maxCallsPerTool !== void 0) {
8959
+ const caps = limits.maxCallsPerTool;
8960
+ if (typeof caps !== "object" || caps === null || Array.isArray(caps)) throw new ConfigError(`${site}.maxCallsPerTool must be a record of per-tool caps`);
8961
+ for (const [name, cap] of Object.entries(caps)) requireNonNegativeInteger(cap, `${site}.maxCallsPerTool['${name}']`);
8962
+ }
8963
+ if (limits.toolUnits !== void 0) {
8964
+ const units = limits.toolUnits;
8965
+ if (typeof units !== "object" || units === null || Array.isArray(units)) throw new ConfigError(`${site}.toolUnits must be { max, costs? }`);
8966
+ const { max, costs } = units;
8967
+ requirePositiveInteger(max, `${site}.toolUnits.max`);
8968
+ if (costs !== void 0) {
8969
+ if (typeof costs !== "object" || costs === null || Array.isArray(costs)) throw new ConfigError(`${site}.toolUnits.costs must be a record of per-tool costs`);
8970
+ for (const [name, cost] of Object.entries(costs)) requireNonNegativeInteger(cost, `${site}.toolUnits.costs['${name}']`);
8971
+ }
8972
+ }
8973
+ if (limits.finalizationReserve !== void 0) {
8974
+ const reserve = limits.finalizationReserve;
8975
+ if (typeof reserve !== "object" || reserve === null || Array.isArray(reserve)) throw new ConfigError(`${site}.finalizationReserve must be { maxOutputTokens? }`);
8976
+ const { maxOutputTokens } = reserve;
8977
+ if (maxOutputTokens !== void 0) requirePositiveInteger(maxOutputTokens, `${site}.finalizationReserve.maxOutputTokens`);
8978
+ }
8979
+ }
8980
+ //#endregion
8981
+ //#region src/model/failover.ts
8982
+ /** Normalizes the author-facing ModelChoice.fallbacks list. */
8983
+ function normalizeFallbacks(refs) {
8984
+ return (refs ?? []).map((model) => ({ model }));
8985
+ }
8986
+ /**
8987
+ * Maps a retry class to its failover trigger once retries exhaust.
8988
+ * Overloaded (529) is transport-class for failover purposes; a
8989
+ * non-retryable error never fails over.
8990
+ */
8991
+ function failoverTriggerOf(retryClass) {
8992
+ if (retryClass === void 0) return;
8993
+ return retryClass === "rate-limit" ? "rate-limit" : "transport";
8994
+ }
8995
+ /**
8996
+ * The next target index past `from` that serves `trigger`, or undefined
8997
+ * when the chain is exhausted. Index 0 is the primary; the chain never
8998
+ * moves backwards (sticky failover).
8999
+ */
9000
+ function nextFailover(targets, trigger, from) {
9001
+ for (let index = from + 1; index < targets.length; index += 1) {
9002
+ const on = targets[index]?.on;
9003
+ if (on === void 0 || on.includes(trigger)) return index;
9004
+ }
9005
+ }
9006
+ /**
9007
+ * Classifies a terminal agent outcome for the degenerate fallback:
9008
+ * schema-mismatch errors are
9009
+ * 'schema-exhausted'; any other error is 'error'; limit terminals (the
9010
+ * no-progress abort included) are 'limit'; cancelled, escalated, and
9011
+ * skipped never trigger.
9012
+ */
9013
+ function fallbackTriggerOf(outcome) {
9014
+ if (outcome.status === "error") return outcome.error?.kind === "schema-mismatch" ? "schema-exhausted" : "error";
9015
+ if (outcome.status === "limit") return "limit";
9016
+ }
9017
+ //#endregion
9018
+ //#region src/model/projector.ts
9019
+ /** The provider family of an adapter: `provider` when set, else `id`. */
9020
+ function providerOf(adapter) {
9021
+ return adapter.provider ?? adapter.id;
9022
+ }
9023
+ /**
9024
+ * Projects the canonical history into the target provider's view:
9025
+ * provider-raw parts of a DIFFERENT provider are omitted; everything
9026
+ * else (text, images, tool calls, tool results, compaction content)
9027
+ * passes through untouched. Messages whose parts all belong to another
9028
+ * provider vanish entirely rather than ride as empty messages.
9029
+ */
9030
+ function projectHistory(messages, targetProvider) {
9031
+ const projected = [];
9032
+ for (const msg of messages) {
9033
+ const parts = msg.parts.filter((part) => part.type !== "provider-raw" || part.provider === targetProvider);
9034
+ if (parts.length === 0 && msg.parts.length > 0) continue;
9035
+ projected.push(parts.length === msg.parts.length ? msg : {
9036
+ ...msg,
9037
+ parts
9038
+ });
9039
+ }
9040
+ return projected;
9041
+ }
9042
+ /**
9043
+ * Lifts the adapter-shipped retention payload of one finished turn into
9044
+ * provider-raw parts (the retention transport). Reads
9045
+ * providerMetadata[<adapter id>].retainedParts and tags each block with
9046
+ * the adapter's provider family. Returns [] when the adapter shipped
9047
+ * nothing.
9048
+ */
9049
+ function liftRetainedParts(providerMetadata, adapter) {
9050
+ const namespace = providerMetadata?.[adapter.id];
9051
+ if (typeof namespace !== "object" || namespace === null) return [];
9052
+ const retained = namespace.retainedParts;
9053
+ if (!Array.isArray(retained)) return [];
9054
+ const blocks = retained;
9055
+ const provider = providerOf(adapter);
9056
+ return blocks.map((block) => ({
9057
+ type: "provider-raw",
9058
+ provider,
9059
+ block
9060
+ }));
9061
+ }
9062
+ //#endregion
8969
9063
  //#region src/model/retry.ts
8970
9064
  /**
8971
9065
  * Transport RetryPolicy (M4-T05): retries live UNDER the journal. A
@@ -9100,14 +9194,227 @@ function retryDelayMs(policy, retryIndex, retryAfterMs, random = nativeRandom) {
9100
9194
  return timerSafe(base / 2 + random() * (base / 2));
9101
9195
  }
9102
9196
  //#endregion
9103
- //#region src/model/roles.ts
9197
+ //#region src/model/caps.ts
9198
+ const TIER_ORDER = {
9199
+ native: 2,
9200
+ "forced-tool": 1,
9201
+ prompt: 0
9202
+ };
9104
9203
  /**
9105
- * True when the given structured-output tier can ride the last loop turn.
9106
- * `native` and `prompt` coexist with tool availability; `forced-tool`
9107
- * pins toolChoice to the synthesized emit_result contract and therefore
9108
- * cannot ride while the agent's tools must remain available. For an
9109
- * agent with no tools every tier rides (the M1 behavior, unchanged).
9110
- */
9204
+ * Strict-schema compatibility as both first-class providers define it:
9205
+ * every object node declares `additionalProperties: false` and lists every
9206
+ * property in `required`. Boolean schemas and
9207
+ * non-object shapes are trivially compatible.
9208
+ */
9209
+ function isStrictCompatibleSchema(schema) {
9210
+ if (typeof schema === "boolean") return true;
9211
+ if (schema.type === "object" || schema.properties !== void 0 || schema.additionalProperties !== void 0) {
9212
+ if (schema.additionalProperties !== false) return false;
9213
+ const properties = typeof schema.properties === "object" && schema.properties !== null ? schema.properties : {};
9214
+ const required = Array.isArray(schema.required) ? schema.required : [];
9215
+ for (const name of Object.keys(properties)) if (!required.includes(name)) return false;
9216
+ for (const value of Object.values(properties)) if (typeof value === "object" && value !== null || typeof value === "boolean") {
9217
+ if (!isStrictCompatibleSchema(value)) return false;
9218
+ }
9219
+ }
9220
+ for (const key of [
9221
+ "items",
9222
+ "additionalProperties",
9223
+ "contains"
9224
+ ]) {
9225
+ const value = schema[key];
9226
+ if (typeof value === "object" && value !== null || typeof value === "boolean") {
9227
+ if (!isStrictCompatibleSchema(value)) return false;
9228
+ }
9229
+ }
9230
+ for (const key of [
9231
+ "allOf",
9232
+ "anyOf",
9233
+ "oneOf",
9234
+ "prefixItems"
9235
+ ]) {
9236
+ const value = schema[key];
9237
+ if (Array.isArray(value)) {
9238
+ for (const element of value) if (typeof element === "object" && element !== null || typeof element === "boolean") {
9239
+ if (!isStrictCompatibleSchema(element)) return false;
9240
+ }
9241
+ }
9242
+ }
9243
+ return true;
9244
+ }
9245
+ /**
9246
+ * Tier selection: the model's declared ceiling
9247
+ * bounds the tier; the native tier additionally requires a
9248
+ * strict-compatible canonical schema (relying on silent server-side
9249
+ * fallback is forbidden), degrading to forced-tool.
9250
+ * Prefill is not a tier.
9251
+ */
9252
+ function selectStructuredOutputTier(caps, canonicalSchema) {
9253
+ const ceiling = caps.structuredOutput;
9254
+ if (ceiling === "native" && !isStrictCompatibleSchema(canonicalSchema)) return "forced-tool";
9255
+ return ceiling;
9256
+ }
9257
+ /** True when `tier` is at or below the model's declared ceiling. */
9258
+ function tierWithinCaps(tier, caps) {
9259
+ return TIER_ORDER[tier] <= TIER_ORDER[caps.structuredOutput];
9260
+ }
9261
+ //#endregion
9262
+ //#region src/runtime/escalation.ts
9263
+ const ESCALATE_TOOL_NAME = "escalate";
9264
+ /**
9265
+ * The escalate tool's exact request schema. costToDate and salvage
9266
+ * MUST NOT appear here: additionalProperties false rejects model-authored
9267
+ * values for them at argument validation.
9268
+ */
9269
+ const ESCALATION_REQUEST_SCHEMA = {
9270
+ type: "object",
9271
+ additionalProperties: false,
9272
+ required: [
9273
+ "kind",
9274
+ "scopeDelta",
9275
+ "revisedEstimate"
9276
+ ],
9277
+ properties: {
9278
+ kind: { enum: [
9279
+ "scope_bigger",
9280
+ "scope_different",
9281
+ "blocked_with_evidence"
9282
+ ] },
9283
+ scopeDelta: { type: "string" },
9284
+ revisedEstimate: {
9285
+ type: "object",
9286
+ additionalProperties: false,
9287
+ required: ["usd", "turns"],
9288
+ properties: {
9289
+ usd: {
9290
+ type: "number",
9291
+ minimum: 0
9292
+ },
9293
+ turns: {
9294
+ type: "integer",
9295
+ minimum: 0
9296
+ }
9297
+ }
9298
+ },
9299
+ blockers: {
9300
+ type: "array",
9301
+ items: { type: "string" }
9302
+ },
9303
+ proposedDecomposition: {
9304
+ type: "array",
9305
+ items: { type: "object" }
9306
+ }
9307
+ }
9308
+ };
9309
+ /** The full-report schema applied BEFORE append. */
9310
+ const ESCALATION_REPORT_SCHEMA = {
9311
+ type: "object",
9312
+ additionalProperties: false,
9313
+ required: [
9314
+ "kind",
9315
+ "scopeDelta",
9316
+ "revisedEstimate",
9317
+ "blockers",
9318
+ "proposedDecomposition",
9319
+ "costToDate",
9320
+ "salvage"
9321
+ ],
9322
+ properties: {
9323
+ kind: { enum: [
9324
+ "scope_bigger",
9325
+ "scope_different",
9326
+ "blocked_with_evidence"
9327
+ ] },
9328
+ scopeDelta: { type: "string" },
9329
+ revisedEstimate: {
9330
+ type: "object",
9331
+ additionalProperties: false,
9332
+ required: ["usd", "turns"],
9333
+ properties: {
9334
+ usd: {
9335
+ type: "number",
9336
+ minimum: 0
9337
+ },
9338
+ turns: {
9339
+ type: "integer",
9340
+ minimum: 0
9341
+ }
9342
+ }
9343
+ },
9344
+ blockers: {
9345
+ type: "array",
9346
+ items: { type: "string" }
9347
+ },
9348
+ proposedDecomposition: {
9349
+ type: "array",
9350
+ items: { type: "object" }
9351
+ },
9352
+ costToDate: {
9353
+ type: "object",
9354
+ additionalProperties: false,
9355
+ required: ["usd", "turns"],
9356
+ properties: {
9357
+ usd: { type: "number" },
9358
+ turns: {
9359
+ type: "integer",
9360
+ minimum: 0
9361
+ }
9362
+ }
9363
+ },
9364
+ salvage: {
9365
+ type: "object",
9366
+ additionalProperties: false,
9367
+ required: ["transcriptRef", "artifacts"],
9368
+ properties: {
9369
+ transcriptRef: { type: "string" },
9370
+ artifacts: {
9371
+ type: "array",
9372
+ items: { type: "string" }
9373
+ },
9374
+ worktreePatchRef: { type: "string" }
9375
+ }
9376
+ }
9377
+ }
9378
+ };
9379
+ /**
9380
+ * The engine opt-in tool: registered through the
9381
+ * same path as any tool under escalation opt-in of EITHER flavor (the
9382
+ * worker's only authoring channel for a report), never available without
9383
+ * opt-in, and dispatched through the same permission chain. The loop
9384
+ * intercepts accepted calls; execute is unreachable by construction.
9385
+ */
9386
+ function escalateTool() {
9387
+ return tool({
9388
+ name: ESCALATE_TOOL_NAME,
9389
+ description: "Escalate to the owner of this task: the scope is bigger than estimated, materially different, or blocked with evidence. Escalating ends your turn loop; include everything the owner needs to decide.",
9390
+ parameters: ESCALATION_REQUEST_SCHEMA,
9391
+ execute: () => {
9392
+ throw new Error("escalate is intercepted by the agent runtime, never executed");
9393
+ }
9394
+ });
9395
+ }
9396
+ /** Validates the runtime-completed report BEFORE append; returns issues. */
9397
+ async function validateEscalationReport(report) {
9398
+ const validation = await validateSchemaSpec(ESCALATION_REPORT_SCHEMA, report);
9399
+ return validation.valid ? [] : validation.issues;
9400
+ }
9401
+ /**
9402
+ * countsAgainstLimit derivation (XF-06): true iff
9403
+ * scope_bigger; scope_different and blocked_with_evidence are exempt and
9404
+ * never debit the escalation counter.
9405
+ */
9406
+ function countsAgainstLimit(kind) {
9407
+ return kind === "scope_bigger";
9408
+ }
9409
+ //#endregion
9410
+ //#region src/model/roles.ts
9411
+ /**
9412
+ * True when the given structured-output tier can ride the last loop turn.
9413
+ * `native` and `prompt` coexist with tool availability; `forced-tool`
9414
+ * pins toolChoice to the synthesized emit_result contract and therefore
9415
+ * cannot ride while the agent's tools must remain available. For an
9416
+ * agent with no tools every tier rides (the M1 behavior, unchanged).
9417
+ */
9111
9418
  function canRideLoopTurn(tier, toolsAvailable) {
9112
9419
  return tier !== "forced-tool" || !toolsAvailable;
9113
9420
  }
@@ -9200,844 +9507,19 @@ function compactMessages(messages, summaryText) {
9200
9507
  return head === void 0 ? [summary] : [head, summary];
9201
9508
  }
9202
9509
  //#endregion
9203
- //#region src/model/router.ts
9204
- /**
9205
- * Model router core (M1-T05): the per-engine adapter registry, ModelRef
9206
- * parsing, the per-invocation resolution chain, canonicalization into
9207
- * CanonicalModelSpec, and caps scrubbing with visible scrub notes.
9208
- *
9209
- * Public contract: https://docs.rulvar.com/guide/model-routing.
9210
- */
9211
- /**
9212
- * Per-engine adapter registry: strictly per engine, no global mutable
9213
- * registry exists. A duplicate adapterId is a typed ConfigError.
9214
- */
9215
- function buildAdapterRegistry(adapters) {
9216
- const registry = /* @__PURE__ */ new Map();
9217
- for (const adapter of adapters) {
9218
- if (registry.has(adapter.id)) throw new ConfigError(`duplicate adapterId '${adapter.id}' at createEngine`);
9219
- registry.set(adapter.id, adapter);
9510
+ //#region src/runtime/model-retry.ts
9511
+ var ModelRetry = class extends Error {
9512
+ data;
9513
+ constructor(message, opts) {
9514
+ super(message);
9515
+ this.name = "ModelRetry";
9516
+ if (opts?.data !== void 0) this.data = opts.data;
9220
9517
  }
9221
- return registry;
9222
- }
9223
- /**
9224
- * ModelRef is strictly 'adapterId:model', no query parameters. The wire
9225
- * model id may itself contain colons (for example ollama tags), so only
9226
- * the FIRST colon splits.
9227
- */
9228
- function parseModelRef(ref) {
9229
- const colon = ref.indexOf(":");
9230
- if (colon <= 0 || colon === ref.length - 1) throw new ConfigError(`invalid ModelRef '${ref}': expected the strict 'adapterId:model' form`);
9231
- return {
9232
- adapterId: ref.slice(0, colon),
9233
- model: ref.slice(colon + 1)
9234
- };
9235
- }
9236
- /**
9237
- * Role effort defaults: orchestrate and plan default to high; summarize and extract
9238
- * default to low. loop and finalize have NO role default: when the chain
9239
- * resolves nothing, the wire omits effort and identity records the spec
9240
- * with the effort member absent.
9241
- */
9242
- const ROLE_EFFORT_DEFAULTS = {
9243
- orchestrate: "high",
9244
- plan: "high",
9245
- summarize: "low",
9246
- extract: "low"
9247
- };
9248
- function contribution(spec, _role) {
9249
- if (spec === void 0) return {};
9250
- if (typeof spec === "string") return { model: spec };
9251
- if ("ladder" in spec) return { ladder: spec.ladder };
9252
- const choice = spec;
9253
- const fields = { model: choice.model };
9254
- if (choice.effort !== void 0) fields.effort = choice.effort;
9255
- if (choice.providerOptions !== void 0) fields.providerOptions = choice.providerOptions;
9256
- if (choice.fallbacks !== void 0) fields.fallbacks = choice.fallbacks;
9257
- return fields;
9258
- }
9259
- function layerFields(layer, role) {
9260
- if (layer === void 0) return {};
9261
- const fromModel = contribution(layer.model, role);
9262
- const fromRouting = contribution(layer.routing?.[role], role);
9263
- const merged = {
9264
- ...fromModel,
9265
- ...pruneUndefined(fromRouting)
9266
- };
9267
- if (layer.effort !== void 0) merged.effort = layer.effort;
9268
- return merged;
9269
- }
9270
- function pruneUndefined(value) {
9271
- const out = {};
9272
- for (const [key, member] of Object.entries(value)) if (member !== void 0) out[key] = member;
9273
- return out;
9274
- }
9275
- function mergeProviderOptions(lower, higher) {
9276
- if (lower === void 0) return higher;
9277
- if (higher === void 0) return lower;
9278
- const merged = { ...lower };
9279
- for (const [namespace, options] of Object.entries(higher)) merged[namespace] = {
9280
- ...merged[namespace],
9281
- ...options
9282
- };
9283
- return merged;
9284
- }
9285
- /** Sampling parameters both first-class providers reject on reasoning models. */
9286
- const SAMPLING_KEYS = [
9287
- "temperature",
9288
- "top_p",
9289
- "top_k"
9290
- ];
9291
- /**
9292
- * Resolution runs on every model invocation, not once per agent: a layered
9293
- * merge of { model, effort, providerOptions, fallbacks } in the order call
9294
- * override > agent profile > workflow defaults > engine defaults, with the
9295
- * invocation role attached as a tag.
9296
- * After resolution the router reads ModelCaps and scrubs illegal
9297
- * parameters visibly: unsupported effort is removed from the wire but
9298
- * kept in identity; sampling params rejected by the model are removed
9299
- * from the adapter's namespace, never silently sent.
9300
- */
9301
- function resolveModelInvocation(options) {
9302
- const { role } = options;
9303
- const layers = [
9304
- options.engine,
9305
- options.workflow,
9306
- options.profile,
9307
- options.call
9308
- ];
9309
- let merged = {};
9310
- for (const layer of layers) {
9311
- const fields = layerFields(layer, role);
9312
- merged = {
9313
- ...merged,
9314
- ...pruneUndefined(fields),
9315
- providerOptions: mergeProviderOptions(merged.providerOptions, fields.providerOptions)
9316
- };
9317
- if (fields.ladder !== void 0) delete merged.model;
9318
- else if (fields.model !== void 0) delete merged.ladder;
9319
- }
9320
- if (merged.ladder !== void 0) throw new ConfigError(`a ladder ModelSpec wins wire resolution for role '${role}': ladder execution is owned by the PlanRunner ladder driver, which resolves each rung attempt to a concrete model override; dispatch laddered profiles through orchestratePlanned or pass a plain ModelRef or ModelChoice`);
9321
- if (merged.model === void 0) throw new ConfigError(`no model resolves for role '${role}': set AgentOpts.model, a profile model, or engine defaults.routing`);
9322
- checkFloors({
9323
- ref: merged.model,
9324
- role,
9325
- ...options.floors === void 0 ? {} : { floors: options.floors },
9326
- ...options.taskClass === void 0 ? {} : { taskClass: options.taskClass }
9327
- });
9328
- const requestedEffort = merged.effort ?? ROLE_EFFORT_DEFAULTS[role];
9329
- const { adapterId, model } = parseModelRef(merged.model);
9330
- let caps;
9331
- try {
9332
- caps = options.capsOf(merged.model);
9333
- } catch (thrown) {
9334
- if (thrown instanceof ConfigError) throw new ConfigError(`role '${role}': ${thrown.message}`);
9335
- throw thrown;
9336
- }
9337
- const scrubs = [];
9338
- let wireEffort = requestedEffort;
9339
- if (wireEffort !== void 0 && !caps.reasoningEfforts.includes(wireEffort)) {
9340
- scrubs.push({
9341
- scrubbed: "effort",
9342
- model: merged.model,
9343
- detail: `effort '${wireEffort}' is not in caps.reasoningEfforts for ${merged.model}; the request proceeds without it (identity keeps the requested effort)`
9344
- });
9345
- wireEffort = void 0;
9346
- }
9347
- let providerOptions = merged.providerOptions;
9348
- if (providerOptions?.[adapterId] !== void 0 && !caps.supportsTemperature) {
9349
- const namespace = { ...providerOptions[adapterId] };
9350
- const removed = SAMPLING_KEYS.filter((key) => key in namespace);
9351
- if (removed.length > 0) {
9352
- for (const key of removed) delete namespace[key];
9353
- providerOptions = {
9354
- ...providerOptions,
9355
- [adapterId]: namespace
9356
- };
9357
- scrubs.push({
9358
- scrubbed: "sampling",
9359
- model: merged.model,
9360
- detail: `sampling parameter(s) ${removed.join(", ")} removed for ${merged.model}: the model rejects them (caps.supportsTemperature is false); never silently sent`
9361
- });
9362
- }
9363
- }
9364
- const canonical = requestedEffort === void 0 ? {
9365
- kind: "model",
9366
- model: merged.model
9367
- } : {
9368
- kind: "model",
9369
- model: merged.model,
9370
- effort: requestedEffort
9371
- };
9372
- const resolved = {
9373
- ref: merged.model,
9374
- adapterId,
9375
- model,
9376
- canonical,
9377
- scrubs
9378
- };
9379
- if (wireEffort !== void 0) resolved.wireEffort = wireEffort;
9380
- if (requestedEffort !== void 0) resolved.requestedEffort = requestedEffort;
9381
- if (providerOptions !== void 0) resolved.providerOptions = providerOptions;
9382
- if (merged.fallbacks !== void 0) resolved.fallbacks = merged.fallbacks;
9383
- return resolved;
9384
- }
9385
- /** The closed trigger vocabulary guard. */
9386
- const TRIGGER_CLASSES = [
9387
- "error",
9388
- "limit",
9389
- "schema-exhausted",
9390
- "verify-failed",
9391
- "no-progress"
9392
- ];
9393
- function validateGate(gate, rungCount, index) {
9394
- if (gate.kind === "mechanical") {
9395
- if (typeof gate.profile !== "string" || gate.profile === "") throw new ConfigError(`ladder acceptance gate ${String(index)}: a mechanical gate names a registered gate profile`);
9396
- return;
9397
- }
9398
- if (gate.kind === "judge") {
9399
- if (typeof gate.rung === "number") {
9400
- if (!Number.isInteger(gate.rung) || gate.rung < 0 || gate.rung >= rungCount) throw new ConfigError(`ladder acceptance gate ${String(index)}: judge rung ${String(gate.rung)} is not a declared rung of a ${String(rungCount)}-rung ladder (FR-119)`);
9401
- return;
9402
- }
9403
- parseModelRef(gate.rung);
9404
- return;
9405
- }
9406
- if (!(gate.fraction > 0 && gate.fraction <= 1)) throw new ConfigError(`ladder acceptance gate ${String(index)}: a spot-check fraction lies in (0, 1], got ${String(gate.fraction)}`);
9407
- }
9408
- /**
9409
- * Canonicalizes a declared LadderSpec: validates the
9410
- * shape once (FR-119 judge declaration included) and resolves every rung's
9411
- * effort to an explicit value. `chainEffort` is the effort the resolution
9412
- * chain would contribute at the declaring layer; a rung that resolves no
9413
- * effort at all is a ConfigError (the canonical form has no absent-effort
9414
- * member by declaration).
9415
- */
9416
- function canonicalizeLadder(spec, options) {
9417
- if (!Array.isArray(spec.rungs) || spec.rungs.length === 0) throw new ConfigError("a ladder declares at least one rung");
9418
- if (!Number.isInteger(spec.startTier) || spec.startTier < 0 || spec.startTier >= spec.rungs.length) throw new ConfigError(`ladder startTier ${String(spec.startTier)} is not a declared rung index of a ${String(spec.rungs.length)}-rung ladder`);
9419
- for (const trigger of spec.escalateOn) if (!TRIGGER_CLASSES.includes(trigger)) throw new ConfigError(`unknown ladder trigger '${String(trigger)}': the vocabulary is closed to ${TRIGGER_CLASSES.join(", ")}`);
9420
- const rungs = spec.rungs.map((rung, index) => {
9421
- parseModelRef(rung.model);
9422
- if (!Number.isInteger(rung.maxTurns) || rung.maxTurns <= 0) throw new ConfigError(`ladder rung ${String(index)}: maxTurns is a positive integer`);
9423
- if (!Number.isInteger(rung.maxTokens) || rung.maxTokens <= 0) throw new ConfigError(`ladder rung ${String(index)}: maxTokens is a positive integer`);
9424
- if (rung.maxCostUsd !== void 0 && !(rung.maxCostUsd > 0)) throw new ConfigError(`ladder rung ${String(index)}: maxCostUsd is positive when present`);
9425
- const effort = rung.effort ?? options?.chainEffort;
9426
- if (effort === void 0) throw new ConfigError(`ladder rung ${String(index)} resolves no effort: the canonical ladder embeds explicit efforts; declare rung.effort or a chain effort`);
9427
- return {
9428
- model: rung.model,
9429
- effort,
9430
- maxTurns: rung.maxTurns,
9431
- maxTokens: rung.maxTokens,
9432
- ...rung.maxCostUsd === void 0 ? {} : { maxCostUsd: rung.maxCostUsd },
9433
- ...rung.memoizeOutcome === void 0 ? {} : { memoizeOutcome: rung.memoizeOutcome }
9434
- };
9435
- });
9436
- for (const [index, gate] of (spec.acceptance ?? []).entries()) validateGate(gate, spec.rungs.length, index);
9437
- return {
9438
- rungs,
9439
- startTier: spec.startTier,
9440
- escalateOn: [...spec.escalateOn],
9441
- ...spec.acceptance === void 0 ? {} : { acceptance: spec.acceptance.map((gate) => gate) }
9442
- };
9443
- }
9444
- /**
9445
- * The concrete ModelChoice of one rung attempt: each attempt is an
9446
- * ordinary agent scope whose CanonicalModelSpec is that rung's
9447
- * `{ kind: 'model' }` form.
9448
- */
9449
- function ladderRungChoice(ladder, index) {
9450
- const rung = ladder.rungs[index];
9451
- if (rung === void 0) throw new ConfigError(`rung ${String(index)} is not declared on a ${String(ladder.rungs.length)}-rung ladder`);
9452
- return {
9453
- model: rung.model,
9454
- effort: rung.effort
9455
- };
9456
- }
9457
- //#endregion
9458
- //#region src/runtime/usage-limits.ts
9459
- /**
9460
- * UsageLimits (M1-T06): normative limit vocabulary and the per-spawn merge.
9461
- *
9462
- * Full contract: https://docs.rulvar.com/guide/agents. Expiry of maxTurns, maxToolCalls,
9463
- * or timeoutMs produces the terminal status 'limit' (paid partial work);
9464
- * streamIdleTimeoutMs expiry is a retryable transport-class AgentError,
9465
- * never 'limit'. The run-level deadline is RunOptions.deadlineAt, not a
9466
- * UsageLimits field.
9467
- */
9468
- const DEFAULT_MAX_TURNS = 32;
9469
- const DEFAULT_STREAM_IDLE_TIMEOUT_MS = 12e4;
9470
- /**
9471
- * Limits merge per spawn: AgentOpts.limits over profile limits over engine
9472
- * defaults.limits.
9473
- */
9474
- function mergeUsageLimits(call, profile, engine) {
9475
- const pick = (key) => call?.[key] ?? profile?.[key] ?? engine?.[key];
9476
- const merged = {
9477
- maxTurns: pick("maxTurns") ?? 32,
9478
- streamIdleTimeoutMs: pick("streamIdleTimeoutMs") ?? 12e4
9479
- };
9480
- const maxToolCalls = pick("maxToolCalls");
9481
- if (maxToolCalls !== void 0) merged.maxToolCalls = maxToolCalls;
9482
- const maxOutputTokensPerTurn = pick("maxOutputTokensPerTurn");
9483
- if (maxOutputTokensPerTurn !== void 0) merged.maxOutputTokensPerTurn = maxOutputTokensPerTurn;
9484
- const timeoutMs = pick("timeoutMs");
9485
- if (timeoutMs !== void 0) merged.timeoutMs = timeoutMs;
9486
- const noProgressTurns = pick("noProgressTurns");
9487
- if (noProgressTurns !== void 0) merged.noProgressTurns = noProgressTurns;
9488
- const toolBudgetNotices = pick("toolBudgetNotices");
9489
- if (toolBudgetNotices !== void 0) merged.toolBudgetNotices = toolBudgetNotices;
9490
- const maxRepeatedToolSignature = pick("maxRepeatedToolSignature");
9491
- if (maxRepeatedToolSignature !== void 0) merged.maxRepeatedToolSignature = maxRepeatedToolSignature;
9492
- const maxNoNewEvidenceCalls = pick("maxNoNewEvidenceCalls");
9493
- if (maxNoNewEvidenceCalls !== void 0) merged.maxNoNewEvidenceCalls = maxNoNewEvidenceCalls;
9494
- const maxCallsPerTool = pick("maxCallsPerTool");
9495
- if (maxCallsPerTool !== void 0) merged.maxCallsPerTool = maxCallsPerTool;
9496
- const toolUnits = pick("toolUnits");
9497
- if (toolUnits !== void 0) merged.toolUnits = toolUnits;
9498
- const finalizationReserve = pick("finalizationReserve");
9499
- if (finalizationReserve !== void 0) merged.finalizationReserve = finalizationReserve;
9500
- return merged;
9501
- }
9502
- /**
9503
- * Validates one UsageLimits layer at its intake boundary (v1.34.0
9504
- * review P2-3): a malformed field (NaN, Infinity, a negative, a
9505
- * fraction) is a typed ConfigError before the merge, before any journal
9506
- * entry, and before any provider dispatch. `site` names the layer in the
9507
- * error text (e.g. `RunOptions.limits`). Counts are positive integers
9508
- * (maxToolCalls may be 0: a spawn that must not call tools).
9509
- * streamIdleTimeoutMs is handed to setTimeout as-is, so it is bounded by
9510
- * the Node timer maximum like RetryPolicy delays; timeoutMs is a
9511
- * wall-clock comparison, so it has no upper bound. Every present field
9512
- * is checked; absent fields keep their defaults.
9513
- */
9514
- function validateUsageLimits(limits, site) {
9515
- if (limits.maxTurns !== void 0) requirePositiveInteger(limits.maxTurns, `${site}.maxTurns`);
9516
- if (limits.maxToolCalls !== void 0) requireNonNegativeInteger(limits.maxToolCalls, `${site}.maxToolCalls`);
9517
- if (limits.maxOutputTokensPerTurn !== void 0) requirePositiveInteger(limits.maxOutputTokensPerTurn, `${site}.maxOutputTokensPerTurn`);
9518
- if (limits.timeoutMs !== void 0) requirePositiveInteger(limits.timeoutMs, `${site}.timeoutMs`);
9519
- if (limits.streamIdleTimeoutMs !== void 0) requireTimerDelayMs(limits.streamIdleTimeoutMs, `${site}.streamIdleTimeoutMs`);
9520
- if (limits.noProgressTurns !== void 0) requirePositiveInteger(limits.noProgressTurns, `${site}.noProgressTurns`);
9521
- if (limits.toolBudgetNotices !== void 0 && typeof limits.toolBudgetNotices !== "boolean") throw new ConfigError(`${site}.toolBudgetNotices must be a boolean; got ${typeof limits.toolBudgetNotices}`);
9522
- if (limits.maxRepeatedToolSignature !== void 0) requirePositiveInteger(limits.maxRepeatedToolSignature, `${site}.maxRepeatedToolSignature`);
9523
- if (limits.maxNoNewEvidenceCalls !== void 0) requirePositiveInteger(limits.maxNoNewEvidenceCalls, `${site}.maxNoNewEvidenceCalls`);
9524
- if (limits.maxCallsPerTool !== void 0) {
9525
- const caps = limits.maxCallsPerTool;
9526
- if (typeof caps !== "object" || caps === null || Array.isArray(caps)) throw new ConfigError(`${site}.maxCallsPerTool must be a record of per-tool caps`);
9527
- for (const [name, cap] of Object.entries(caps)) requireNonNegativeInteger(cap, `${site}.maxCallsPerTool['${name}']`);
9528
- }
9529
- if (limits.toolUnits !== void 0) {
9530
- const units = limits.toolUnits;
9531
- if (typeof units !== "object" || units === null || Array.isArray(units)) throw new ConfigError(`${site}.toolUnits must be { max, costs? }`);
9532
- const { max, costs } = units;
9533
- requirePositiveInteger(max, `${site}.toolUnits.max`);
9534
- if (costs !== void 0) {
9535
- if (typeof costs !== "object" || costs === null || Array.isArray(costs)) throw new ConfigError(`${site}.toolUnits.costs must be a record of per-tool costs`);
9536
- for (const [name, cost] of Object.entries(costs)) requireNonNegativeInteger(cost, `${site}.toolUnits.costs['${name}']`);
9537
- }
9538
- }
9539
- if (limits.finalizationReserve !== void 0) {
9540
- const reserve = limits.finalizationReserve;
9541
- if (typeof reserve !== "object" || reserve === null || Array.isArray(reserve)) throw new ConfigError(`${site}.finalizationReserve must be { maxOutputTokens? }`);
9542
- const { maxOutputTokens } = reserve;
9543
- if (maxOutputTokens !== void 0) requirePositiveInteger(maxOutputTokens, `${site}.finalizationReserve.maxOutputTokens`);
9544
- }
9545
- }
9546
- //#endregion
9547
- //#region src/runtime/model-retry.ts
9548
- var ModelRetry = class extends Error {
9549
- data;
9550
- constructor(message, opts) {
9551
- super(message);
9552
- this.name = "ModelRetry";
9553
- if (opts?.data !== void 0) this.data = opts.data;
9554
- }
9555
- };
9556
- /** Bounded semantic retries per tool call chain. */
9557
- const DEFAULT_MODEL_RETRY_ATTEMPTS = 2;
9558
- //#endregion
9559
- //#region src/runtime/escalation.ts
9560
- const ESCALATE_TOOL_NAME = "escalate";
9561
- /**
9562
- * The escalate tool's exact request schema. costToDate and salvage
9563
- * MUST NOT appear here: additionalProperties false rejects model-authored
9564
- * values for them at argument validation.
9565
- */
9566
- const ESCALATION_REQUEST_SCHEMA = {
9567
- type: "object",
9568
- additionalProperties: false,
9569
- required: [
9570
- "kind",
9571
- "scopeDelta",
9572
- "revisedEstimate"
9573
- ],
9574
- properties: {
9575
- kind: { enum: [
9576
- "scope_bigger",
9577
- "scope_different",
9578
- "blocked_with_evidence"
9579
- ] },
9580
- scopeDelta: { type: "string" },
9581
- revisedEstimate: {
9582
- type: "object",
9583
- additionalProperties: false,
9584
- required: ["usd", "turns"],
9585
- properties: {
9586
- usd: {
9587
- type: "number",
9588
- minimum: 0
9589
- },
9590
- turns: {
9591
- type: "integer",
9592
- minimum: 0
9593
- }
9594
- }
9595
- },
9596
- blockers: {
9597
- type: "array",
9598
- items: { type: "string" }
9599
- },
9600
- proposedDecomposition: {
9601
- type: "array",
9602
- items: { type: "object" }
9603
- }
9604
- }
9605
- };
9606
- /** The full-report schema applied BEFORE append. */
9607
- const ESCALATION_REPORT_SCHEMA = {
9608
- type: "object",
9609
- additionalProperties: false,
9610
- required: [
9611
- "kind",
9612
- "scopeDelta",
9613
- "revisedEstimate",
9614
- "blockers",
9615
- "proposedDecomposition",
9616
- "costToDate",
9617
- "salvage"
9618
- ],
9619
- properties: {
9620
- kind: { enum: [
9621
- "scope_bigger",
9622
- "scope_different",
9623
- "blocked_with_evidence"
9624
- ] },
9625
- scopeDelta: { type: "string" },
9626
- revisedEstimate: {
9627
- type: "object",
9628
- additionalProperties: false,
9629
- required: ["usd", "turns"],
9630
- properties: {
9631
- usd: {
9632
- type: "number",
9633
- minimum: 0
9634
- },
9635
- turns: {
9636
- type: "integer",
9637
- minimum: 0
9638
- }
9639
- }
9640
- },
9641
- blockers: {
9642
- type: "array",
9643
- items: { type: "string" }
9644
- },
9645
- proposedDecomposition: {
9646
- type: "array",
9647
- items: { type: "object" }
9648
- },
9649
- costToDate: {
9650
- type: "object",
9651
- additionalProperties: false,
9652
- required: ["usd", "turns"],
9653
- properties: {
9654
- usd: { type: "number" },
9655
- turns: {
9656
- type: "integer",
9657
- minimum: 0
9658
- }
9659
- }
9660
- },
9661
- salvage: {
9662
- type: "object",
9663
- additionalProperties: false,
9664
- required: ["transcriptRef", "artifacts"],
9665
- properties: {
9666
- transcriptRef: { type: "string" },
9667
- artifacts: {
9668
- type: "array",
9669
- items: { type: "string" }
9670
- },
9671
- worktreePatchRef: { type: "string" }
9672
- }
9673
- }
9674
- }
9675
- };
9676
- /**
9677
- * The engine opt-in tool: registered through the
9678
- * same path as any tool under escalation opt-in of EITHER flavor (the
9679
- * worker's only authoring channel for a report), never available without
9680
- * opt-in, and dispatched through the same permission chain. The loop
9681
- * intercepts accepted calls; execute is unreachable by construction.
9682
- */
9683
- function escalateTool() {
9684
- return tool({
9685
- name: ESCALATE_TOOL_NAME,
9686
- description: "Escalate to the owner of this task: the scope is bigger than estimated, materially different, or blocked with evidence. Escalating ends your turn loop; include everything the owner needs to decide.",
9687
- parameters: ESCALATION_REQUEST_SCHEMA,
9688
- execute: () => {
9689
- throw new Error("escalate is intercepted by the agent runtime, never executed");
9690
- }
9691
- });
9692
- }
9693
- /** Validates the runtime-completed report BEFORE append; returns issues. */
9694
- async function validateEscalationReport(report) {
9695
- const validation = await validateSchemaSpec(ESCALATION_REPORT_SCHEMA, report);
9696
- return validation.valid ? [] : validation.issues;
9697
- }
9698
- /**
9699
- * countsAgainstLimit derivation (XF-06): true iff
9700
- * scope_bigger; scope_different and blocked_with_evidence are exempt and
9701
- * never debit the escalation counter.
9702
- */
9703
- function countsAgainstLimit(kind) {
9704
- return kind === "scope_bigger";
9705
- }
9706
- //#endregion
9707
- //#region src/runtime/no-progress.ts
9708
- /**
9709
- * The no-progress abort class (M3-T08): an engine-defined detector
9710
- * journaled as a first-class terminal abort distinct from user
9711
- * cancellation (a cancelled entry always reruns; a no-progress abort
9712
- * must replay, or every resume would re-pay the stuck turns). The
9713
- * interim heuristic is committed: N consecutive
9714
- * turns without tool calls or artifact deltas, N = 3; the broader
9715
- * heuristic stays OQ-15, revisited on dogfood traces.
9716
- *
9717
- * Encoding: the abort is the agent's
9718
- * terminal entry with status 'limit', an error payload carrying
9719
- * abortClass 'no-progress', and memoizeOutcome stamped by the ENGINE on
9720
- * the terminal entry, so the frozen memoize-limit rule replays it on
9721
- * every subsequent resume without a live rerun. In M3 the runtime has no
9722
- * per-turn artifact channel, so the tool-call test subsumes artifact
9723
- * deltas; per-turn artifact producers arrive with M4 compaction.
9724
- */
9725
- /** The committed no-progress detector N. */
9726
- const DEFAULT_NO_PROGRESS_TURNS = 3;
9727
- /**
9728
- * Counts consecutive progress-free turns. A turn with at least one tool
9729
- * call (or, later, an artifact delta) resets the streak; a turn with
9730
- * neither lengthens it; the detector trips when the streak reaches the
9731
- * threshold AND the loop would otherwise continue.
9732
- */
9733
- var NoProgressDetector = class {
9734
- streakInternal = 0;
9735
- threshold;
9736
- constructor(threshold) {
9737
- this.threshold = threshold ?? 3;
9738
- }
9739
- get streak() {
9740
- return this.streakInternal;
9741
- }
9742
- /** Records one completed model turn. */
9743
- recordTurn(progress) {
9744
- if (progress.toolCalls > 0 || (progress.artifactDeltas ?? 0) > 0) this.streakInternal = 0;
9745
- else this.streakInternal += 1;
9746
- }
9747
- get tripped() {
9748
- return this.streakInternal >= this.threshold;
9749
- }
9750
- describe() {
9751
- return `no-progress abort after ${this.streakInternal} consecutive turns without tool calls or artifact deltas (threshold ${this.threshold}; https://docs.rulvar.com/guide/agents#the-agent-loop-and-turns)`;
9752
- }
9753
- };
9754
- //#endregion
9755
- //#region src/runtime/permission-chain.ts
9756
- /**
9757
- * The layered permission chain (M3-T03): the single approval surface for
9758
- * every tool dispatch, regardless of tool origin. The order is fixed and
9759
- * normative: hooks -> deny rules -> ask rules -> canUseTool -> terminal
9760
- * default (allow unless needsApproval, then ask). Evaluation is
9761
- * short-circuit; unconfigured layers are skipped. Rules never yield
9762
- * allow: allow is only ever falling through to canUseTool or the
9763
- * terminal default.
9764
- *
9765
- * Full contract: https://docs.rulvar.com/guide/tools.
9766
- * Risk presets, the argv shell matcher, domain rules, and the
9767
- * audit/dry-run surface land in M5.
9768
- */
9769
- /**
9770
- * Merges the engine-wide config and the profile config into one chain.
9771
- * Layers concatenate engine-first; since rules only deny or ask, ordering
9772
- * within a layer cannot change the verdict. The
9773
- * profile's canUseTool wins over the engine's (a single slot by
9774
- * construction). A declared preset compiles INTO the same layers, after
9775
- * the host-authored rules, never as a fifth layer (M5-T05).
9776
- */
9777
- function compilePermissionChain(engine, profile) {
9778
- const preset = profile?.preset === void 0 ? {
9779
- deny: [],
9780
- ask: []
9781
- } : compilePermissionPreset(profile.preset);
9782
- const deny = [
9783
- ...engine?.deny ?? [],
9784
- ...profile?.deny ?? [],
9785
- ...preset.deny
9786
- ];
9787
- const ask = [
9788
- ...engine?.ask ?? [],
9789
- ...profile?.ask ?? [],
9790
- ...preset.ask
9791
- ];
9792
- const canUseTool = profile?.canUseTool ?? engine?.canUseTool;
9793
- return {
9794
- hooks: [...engine?.hooks ?? [], ...profile?.hooks ?? []],
9795
- deny,
9796
- ask,
9797
- ...canUseTool === void 0 ? {} : { canUseTool }
9798
- };
9799
- }
9800
- /** The command text an argv rule matches against. */
9801
- function commandOf(input) {
9802
- if (typeof input === "string") return input;
9803
- if (typeof input === "object" && input !== null) {
9804
- const command = input.command;
9805
- if (typeof command === "string") return command;
9806
- }
9807
- }
9808
- function ruleMatches(rule, toolName, risk, input) {
9809
- if ("risk" in rule) {
9810
- const risks = Array.isArray(rule.risk) ? rule.risk : [rule.risk];
9811
- if (risks.includes("undeclared") && risk === void 0) return true;
9812
- return risk !== void 0 && risks.includes(risk);
9813
- }
9814
- if ("domains" in rule) return false;
9815
- if (!(Array.isArray(rule.tool) ? rule.tool : [rule.tool]).includes(toolName)) return false;
9816
- if ("argv" in rule) {
9817
- const command = commandOf(input);
9818
- if (command === void 0) return false;
9819
- const patterns = Array.isArray(rule.argv) ? rule.argv : [rule.argv];
9820
- return lexShellCommand(command).some((segment) => !segment.unmatchable && patterns.some((pattern) => matchArgvPattern(pattern, segment.argv)));
9821
- }
9822
- return true;
9823
- }
9824
- /**
9825
- * Advisory domain-rule matches for the audit payload:
9826
- * reported, never enforced in the current release.
9827
- */
9828
- function advisoryMatches(chain, toolName) {
9829
- return [...chain.deny, ...chain.ask].filter((rule) => "domains" in rule && rule.tool === toolName);
9830
- }
9831
- /**
9832
- * Unmatchable segments (command/process substitution, here-docs) yield
9833
- * ask, ALWAYS, for any tool that has argv rules.
9834
- */
9835
- function argvUnmatchableAsk(chain, toolName, input) {
9836
- if (![...chain.deny, ...chain.ask].some((rule) => "argv" in rule && (Array.isArray(rule.tool) ? rule.tool : [rule.tool]).includes(toolName))) return false;
9837
- const command = commandOf(input);
9838
- if (command === void 0) return true;
9839
- return lexShellCommand(command).some((segment) => segment.unmatchable);
9840
- }
9841
- /** A stub ToolContext for offline (dry-run) evaluations. */
9842
- function offlineContext(toolName) {
9843
- return {
9844
- runId: "dry-run",
9845
- spanId: `dry-run-${toolName}`,
9846
- agent: { agentType: "" },
9847
- cwd: process.cwd(),
9848
- isolation: "none",
9849
- signal: new AbortController().signal,
9850
- log: () => void 0
9851
- };
9852
- }
9853
- /**
9854
- * Evaluates the chain for one dispatch, or OFFLINE against a
9855
- * hypothetical call by tool name (the dry-run API: nothing executes;
9856
- * shells and tests read the verdict, the
9857
- * deciding layer, and the matched rule). Hooks run in deterministic
9858
- * registration order; { modifiedInput } substitutes the input and
9859
- * continues; the first decisive verdict wins. The returned input is what
9860
- * execute receives and what the approval identity hashes (post hook
9861
- * modification). Advisory domain-rule matches
9862
- * ride every verdict for the audit payload.
9863
- */
9864
- async function evaluatePermission(chain, tool, input, ctx) {
9865
- const def = typeof tool === "string" ? {
9866
- name: tool,
9867
- needsApproval: false
9868
- } : tool;
9869
- const risk = typeof tool === "string" ? void 0 : tool.risk;
9870
- const context = ctx ?? offlineContext(def.name);
9871
- const advisory = advisoryMatches(chain, def.name);
9872
- const withAdvisory = (verdict) => advisory.length === 0 ? verdict : {
9873
- ...verdict,
9874
- advisory
9875
- };
9876
- let effective = input;
9877
- for (const hook of chain.hooks) {
9878
- const verdict = await hook(def.name, effective, context);
9879
- if (verdict === void 0) continue;
9880
- if (verdict === "allow" || verdict === "deny" || verdict === "ask") return withAdvisory({
9881
- verdict,
9882
- decidedBy: "hook",
9883
- input: effective
9884
- });
9885
- effective = verdict.modifiedInput;
9886
- }
9887
- for (const rule of chain.deny) if (ruleMatches(rule, def.name, risk, effective)) return withAdvisory({
9888
- verdict: "deny",
9889
- decidedBy: "deny-rule",
9890
- rule,
9891
- input: effective
9892
- });
9893
- for (const rule of chain.ask) if (ruleMatches(rule, def.name, risk, effective)) return withAdvisory({
9894
- verdict: "ask",
9895
- decidedBy: "ask-rule",
9896
- rule,
9897
- input: effective
9898
- });
9899
- if (argvUnmatchableAsk(chain, def.name, effective)) return withAdvisory({
9900
- verdict: "ask",
9901
- decidedBy: "ask-rule",
9902
- input: effective
9903
- });
9904
- if (chain.canUseTool !== void 0) {
9905
- const verdict = await chain.canUseTool(def.name, effective, context);
9906
- if (verdict === "allow") return withAdvisory({
9907
- verdict: "allow",
9908
- decidedBy: "canUseTool",
9909
- input: effective
9910
- });
9911
- if (verdict === "deny") return withAdvisory({
9912
- verdict: "deny",
9913
- decidedBy: "canUseTool",
9914
- input: effective
9915
- });
9916
- effective = verdict.modifiedInput;
9917
- }
9918
- if (def.needsApproval) return withAdvisory({
9919
- verdict: "ask",
9920
- decidedBy: "default",
9921
- input: effective
9922
- });
9923
- return withAdvisory({
9924
- verdict: "allow",
9925
- decidedBy: "default",
9926
- input: effective
9927
- });
9928
- }
9929
- //#endregion
9930
- //#region src/runtime/structured-output.ts
9931
- /** The synthesized forced-tool contract name. */
9932
- const EMIT_RESULT_TOOL = "emit_result";
9933
- /**
9934
- * Applies the selected tier to an outgoing request. Native rides
9935
- * ChatRequest.schema; forced-tool synthesizes a single emit_result tool
9936
- * with toolChoice pinned to it; prompt injects the schema into the last
9937
- * user message.
9938
- */
9939
- function applyStructuredOutputTier(req, tier, schema) {
9940
- if (tier === "native") return {
9941
- ...req,
9942
- schema
9943
- };
9944
- if (tier === "forced-tool") {
9945
- const contract = {
9946
- name: EMIT_RESULT_TOOL,
9947
- description: "Emit the final structured result. Call exactly once with the complete answer.",
9948
- parameters: schema
9949
- };
9950
- return {
9951
- ...req,
9952
- tools: [...req.tools ?? [], contract],
9953
- toolChoice: { name: EMIT_RESULT_TOOL }
9954
- };
9955
- }
9956
- const instruction = "Respond with a single JSON value that validates against this JSON Schema, with no surrounding prose and no code fences:\n" + JSON.stringify(schema);
9957
- const messages = [...req.messages];
9958
- const last = messages[messages.length - 1];
9959
- if (last !== void 0 && last.role === "user") messages[messages.length - 1] = {
9960
- role: "user",
9961
- parts: [...last.parts, {
9962
- type: "text",
9963
- text: instruction
9964
- }]
9965
- };
9966
- else messages.push({
9967
- role: "user",
9968
- parts: [{
9969
- type: "text",
9970
- text: instruction
9971
- }]
9972
- });
9973
- return {
9974
- ...req,
9975
- messages
9976
- };
9977
- }
9978
- /**
9979
- * Extracts the structured-output candidate from a collected turn per tier.
9980
- * Returns `undefined` when the turn carries no candidate (for example the
9981
- * model answered prose without the forced tool call).
9982
- */
9983
- function extractCandidate(turn, tier) {
9984
- if (tier === "forced-tool") {
9985
- const call = turn.toolCalls.find((c) => c.name === EMIT_RESULT_TOOL);
9986
- return call === void 0 ? void 0 : { raw: call.args };
9987
- }
9988
- const text = turn.text.trim();
9989
- if (text === "") return;
9990
- try {
9991
- return { raw: JSON.parse(text) };
9992
- } catch {
9993
- const extracted = extractFirstJsonValue(text);
9994
- return extracted === void 0 ? void 0 : { raw: extracted };
9995
- }
9996
- }
9997
- /** Best-effort extraction of the first complete JSON object or array in prose. */
9998
- function extractFirstJsonValue(text) {
9999
- const start = text.search(/[[{]/);
10000
- if (start === -1) return;
10001
- const open = text[start];
10002
- const close = open === "{" ? "}" : "]";
10003
- let depth = 0;
10004
- let inString = false;
10005
- let escaped = false;
10006
- for (let i = start; i < text.length; i += 1) {
10007
- const ch = text[i];
10008
- if (inString) {
10009
- if (escaped) escaped = false;
10010
- else if (ch === "\\") escaped = true;
10011
- else if (ch === "\"") inString = false;
10012
- continue;
10013
- }
10014
- if (ch === "\"") inString = true;
10015
- else if (ch === open) depth += 1;
10016
- else if (ch === close) {
10017
- depth -= 1;
10018
- if (depth === 0) try {
10019
- return JSON.parse(text.slice(start, i + 1));
10020
- } catch {
10021
- return;
10022
- }
10023
- }
10024
- }
10025
- }
10026
- /** The bounded re-prompt message sent back to the model on a validation miss. */
10027
- function formatRePrompt(issues, attempt, maxAttempts) {
10028
- return {
10029
- role: "user",
10030
- parts: [{
10031
- type: "text",
10032
- text: `Your previous answer did not validate against the required schema (attempt ${attempt} of ${maxAttempts}). Issues:\n${issues.slice(0, 16).map((issue) => {
10033
- const path = issue.path === void 0 || issue.path.length === 0 ? "" : ` (at ${issue.path.map((seg) => String(typeof seg === "object" ? seg.key : seg)).join(".")})`;
10034
- return `- ${issue.message}${path}`;
10035
- }).join("\n")}\nRespond again with ONLY a corrected JSON value that validates.`
10036
- }]
10037
- };
10038
- }
10039
- //#endregion
10040
- //#region src/runtime/exploration.ts
9518
+ };
9519
+ /** Bounded semantic retries per tool call chain. */
9520
+ const DEFAULT_MODEL_RETRY_ATTEMPTS = 2;
9521
+ //#endregion
9522
+ //#region src/runtime/exploration.ts
10041
9523
  /**
10042
9524
  * Exploration guards (RV-210, first slice): the engine-side counters that
10043
9525
  * make an oscillating tool loop visible and boundable. The published gap:
@@ -10232,14 +9714,172 @@ const TOOL_BUDGET_NOTICE_THRESHOLDS = [.5, .8];
10232
9714
  function crossedNoticeThresholds(used, max) {
10233
9715
  return TOOL_BUDGET_NOTICE_THRESHOLDS.filter((threshold) => used >= Math.ceil(threshold * max)).map((threshold) => threshold);
10234
9716
  }
10235
- /**
10236
- * The model-visible budget notice. Deterministic for a given usage
10237
- * count, so a recorded conversation rebuilds byte-identically on
10238
- * resume and replay.
10239
- */
10240
- function toolBudgetNoticeText(used, max) {
10241
- const remaining = Math.max(0, max - used);
10242
- return `Tool budget notice: ${String(used)} of ${String(max)} tool calls used; ${String(remaining)} remaining. Prioritize the highest value calls and finish with what you have.`;
9717
+ /**
9718
+ * The model-visible budget notice. Deterministic for a given usage
9719
+ * count, so a recorded conversation rebuilds byte-identically on
9720
+ * resume and replay.
9721
+ */
9722
+ function toolBudgetNoticeText(used, max) {
9723
+ const remaining = Math.max(0, max - used);
9724
+ return `Tool budget notice: ${String(used)} of ${String(max)} tool calls used; ${String(remaining)} remaining. Prioritize the highest value calls and finish with what you have.`;
9725
+ }
9726
+ //#endregion
9727
+ //#region src/runtime/no-progress.ts
9728
+ /**
9729
+ * The no-progress abort class (M3-T08): an engine-defined detector
9730
+ * journaled as a first-class terminal abort distinct from user
9731
+ * cancellation (a cancelled entry always reruns; a no-progress abort
9732
+ * must replay, or every resume would re-pay the stuck turns). The
9733
+ * interim heuristic is committed: N consecutive
9734
+ * turns without tool calls or artifact deltas, N = 3; the broader
9735
+ * heuristic stays OQ-15, revisited on dogfood traces.
9736
+ *
9737
+ * Encoding: the abort is the agent's
9738
+ * terminal entry with status 'limit', an error payload carrying
9739
+ * abortClass 'no-progress', and memoizeOutcome stamped by the ENGINE on
9740
+ * the terminal entry, so the frozen memoize-limit rule replays it on
9741
+ * every subsequent resume without a live rerun. In M3 the runtime has no
9742
+ * per-turn artifact channel, so the tool-call test subsumes artifact
9743
+ * deltas; per-turn artifact producers arrive with M4 compaction.
9744
+ */
9745
+ /** The committed no-progress detector N. */
9746
+ const DEFAULT_NO_PROGRESS_TURNS = 3;
9747
+ /**
9748
+ * Counts consecutive progress-free turns. A turn with at least one tool
9749
+ * call (or, later, an artifact delta) resets the streak; a turn with
9750
+ * neither lengthens it; the detector trips when the streak reaches the
9751
+ * threshold AND the loop would otherwise continue.
9752
+ */
9753
+ var NoProgressDetector = class {
9754
+ streakInternal = 0;
9755
+ threshold;
9756
+ constructor(threshold) {
9757
+ this.threshold = threshold ?? 3;
9758
+ }
9759
+ get streak() {
9760
+ return this.streakInternal;
9761
+ }
9762
+ /** Records one completed model turn. */
9763
+ recordTurn(progress) {
9764
+ if (progress.toolCalls > 0 || (progress.artifactDeltas ?? 0) > 0) this.streakInternal = 0;
9765
+ else this.streakInternal += 1;
9766
+ }
9767
+ get tripped() {
9768
+ return this.streakInternal >= this.threshold;
9769
+ }
9770
+ describe() {
9771
+ return `no-progress abort after ${this.streakInternal} consecutive turns without tool calls or artifact deltas (threshold ${this.threshold}; https://docs.rulvar.com/guide/agents#the-agent-loop-and-turns)`;
9772
+ }
9773
+ };
9774
+ //#endregion
9775
+ //#region src/runtime/structured-output.ts
9776
+ /** The synthesized forced-tool contract name. */
9777
+ const EMIT_RESULT_TOOL = "emit_result";
9778
+ /**
9779
+ * Applies the selected tier to an outgoing request. Native rides
9780
+ * ChatRequest.schema; forced-tool synthesizes a single emit_result tool
9781
+ * with toolChoice pinned to it; prompt injects the schema into the last
9782
+ * user message.
9783
+ */
9784
+ function applyStructuredOutputTier(req, tier, schema) {
9785
+ if (tier === "native") return {
9786
+ ...req,
9787
+ schema
9788
+ };
9789
+ if (tier === "forced-tool") {
9790
+ const contract = {
9791
+ name: EMIT_RESULT_TOOL,
9792
+ description: "Emit the final structured result. Call exactly once with the complete answer.",
9793
+ parameters: schema
9794
+ };
9795
+ return {
9796
+ ...req,
9797
+ tools: [...req.tools ?? [], contract],
9798
+ toolChoice: { name: EMIT_RESULT_TOOL }
9799
+ };
9800
+ }
9801
+ const instruction = "Respond with a single JSON value that validates against this JSON Schema, with no surrounding prose and no code fences:\n" + JSON.stringify(schema);
9802
+ const messages = [...req.messages];
9803
+ const last = messages[messages.length - 1];
9804
+ if (last !== void 0 && last.role === "user") messages[messages.length - 1] = {
9805
+ role: "user",
9806
+ parts: [...last.parts, {
9807
+ type: "text",
9808
+ text: instruction
9809
+ }]
9810
+ };
9811
+ else messages.push({
9812
+ role: "user",
9813
+ parts: [{
9814
+ type: "text",
9815
+ text: instruction
9816
+ }]
9817
+ });
9818
+ return {
9819
+ ...req,
9820
+ messages
9821
+ };
9822
+ }
9823
+ /**
9824
+ * Extracts the structured-output candidate from a collected turn per tier.
9825
+ * Returns `undefined` when the turn carries no candidate (for example the
9826
+ * model answered prose without the forced tool call).
9827
+ */
9828
+ function extractCandidate(turn, tier) {
9829
+ if (tier === "forced-tool") {
9830
+ const call = turn.toolCalls.find((c) => c.name === EMIT_RESULT_TOOL);
9831
+ return call === void 0 ? void 0 : { raw: call.args };
9832
+ }
9833
+ const text = turn.text.trim();
9834
+ if (text === "") return;
9835
+ try {
9836
+ return { raw: JSON.parse(text) };
9837
+ } catch {
9838
+ const extracted = extractFirstJsonValue(text);
9839
+ return extracted === void 0 ? void 0 : { raw: extracted };
9840
+ }
9841
+ }
9842
+ /** Best-effort extraction of the first complete JSON object or array in prose. */
9843
+ function extractFirstJsonValue(text) {
9844
+ const start = text.search(/[[{]/);
9845
+ if (start === -1) return;
9846
+ const open = text[start];
9847
+ const close = open === "{" ? "}" : "]";
9848
+ let depth = 0;
9849
+ let inString = false;
9850
+ let escaped = false;
9851
+ for (let i = start; i < text.length; i += 1) {
9852
+ const ch = text[i];
9853
+ if (inString) {
9854
+ if (escaped) escaped = false;
9855
+ else if (ch === "\\") escaped = true;
9856
+ else if (ch === "\"") inString = false;
9857
+ continue;
9858
+ }
9859
+ if (ch === "\"") inString = true;
9860
+ else if (ch === open) depth += 1;
9861
+ else if (ch === close) {
9862
+ depth -= 1;
9863
+ if (depth === 0) try {
9864
+ return JSON.parse(text.slice(start, i + 1));
9865
+ } catch {
9866
+ return;
9867
+ }
9868
+ }
9869
+ }
9870
+ }
9871
+ /** The bounded re-prompt message sent back to the model on a validation miss. */
9872
+ function formatRePrompt(issues, attempt, maxAttempts) {
9873
+ return {
9874
+ role: "user",
9875
+ parts: [{
9876
+ type: "text",
9877
+ text: `Your previous answer did not validate against the required schema (attempt ${attempt} of ${maxAttempts}). Issues:\n${issues.slice(0, 16).map((issue) => {
9878
+ const path = issue.path === void 0 || issue.path.length === 0 ? "" : ` (at ${issue.path.map((seg) => String(typeof seg === "object" ? seg.key : seg)).join(".")})`;
9879
+ return `- ${issue.message}${path}`;
9880
+ }).join("\n")}\nRespond again with ONLY a corrected JSON value that validates.`
9881
+ }]
9882
+ };
10243
9883
  }
10244
9884
  //#endregion
10245
9885
  //#region src/runtime/agent-loop.ts
@@ -12791,140 +12431,1006 @@ var AdmissionController = class {
12791
12431
  statsBefore
12792
12432
  };
12793
12433
  }
12794
- if (depth > this.maxDepth) return {
12795
- verdict: {
12796
- kind: "reject",
12797
- reason: { code: "depth" }
12798
- },
12799
- statsBefore
12800
- };
12801
- if (childrenBefore >= this.maxChildrenPerNode) return {
12802
- verdict: {
12803
- kind: "reject",
12804
- reason: { code: "quota" }
12805
- },
12806
- statsBefore
12807
- };
12808
- if (this.maxTotalSpawns !== void 0 && this.admittedTotal >= this.maxTotalSpawns) return {
12809
- verdict: {
12810
- kind: "reject",
12811
- reason: { code: "lifetime" }
12812
- },
12813
- statsBefore
12814
- };
12815
- let childCeilingUsd;
12816
- const parentRemainder = this.budget.remainderOf(spec.parentAccountScope);
12817
- if (parentRemainder !== void 0) {
12818
- const fractionCap = this.childBudgetFraction * parentRemainder;
12819
- childCeilingUsd = spec.budgetUsd === void 0 ? fractionCap : Math.min(spec.budgetUsd, fractionCap);
12820
- } else if (spec.budgetUsd !== void 0) childCeilingUsd = spec.budgetUsd;
12821
- let reserveUsd = spec.estCostUsd ?? this.flatReserveUsd;
12822
- if (childCeilingUsd !== void 0) reserveUsd = Math.min(reserveUsd, childCeilingUsd);
12823
- const reserve = { reserveUsd };
12824
- if (childCeilingUsd !== void 0) reserve.childCeilingUsd = childCeilingUsd;
12825
- if (this.budget.spawnHeadroom <= 0) return {
12826
- verdict: {
12827
- kind: "reject",
12828
- reason: { code: "lifetime" }
12829
- },
12830
- statsBefore
12434
+ if (depth > this.maxDepth) return {
12435
+ verdict: {
12436
+ kind: "reject",
12437
+ reason: { code: "depth" }
12438
+ },
12439
+ statsBefore
12440
+ };
12441
+ if (childrenBefore >= this.maxChildrenPerNode) return {
12442
+ verdict: {
12443
+ kind: "reject",
12444
+ reason: { code: "quota" }
12445
+ },
12446
+ statsBefore
12447
+ };
12448
+ if (this.maxTotalSpawns !== void 0 && this.admittedTotal >= this.maxTotalSpawns) return {
12449
+ verdict: {
12450
+ kind: "reject",
12451
+ reason: { code: "lifetime" }
12452
+ },
12453
+ statsBefore
12454
+ };
12455
+ let childCeilingUsd;
12456
+ const parentRemainder = this.budget.remainderOf(spec.parentAccountScope);
12457
+ if (parentRemainder !== void 0) {
12458
+ const fractionCap = this.childBudgetFraction * parentRemainder;
12459
+ childCeilingUsd = spec.budgetUsd === void 0 ? fractionCap : Math.min(spec.budgetUsd, fractionCap);
12460
+ } else if (spec.budgetUsd !== void 0) childCeilingUsd = spec.budgetUsd;
12461
+ let reserveUsd = spec.estCostUsd ?? this.flatReserveUsd;
12462
+ if (childCeilingUsd !== void 0) reserveUsd = Math.min(reserveUsd, childCeilingUsd);
12463
+ const reserve = { reserveUsd };
12464
+ if (childCeilingUsd !== void 0) reserve.childCeilingUsd = childCeilingUsd;
12465
+ if (this.budget.spawnHeadroom <= 0) return {
12466
+ verdict: {
12467
+ kind: "reject",
12468
+ reason: { code: "lifetime" }
12469
+ },
12470
+ statsBefore
12471
+ };
12472
+ if (commitReserve) try {
12473
+ this.budget.admitSpawn(reserveUsd, spec.parentAccountScope);
12474
+ } catch {
12475
+ return {
12476
+ verdict: {
12477
+ kind: "reject",
12478
+ reason: { code: "budget" }
12479
+ },
12480
+ statsBefore
12481
+ };
12482
+ }
12483
+ else {
12484
+ const remainder = this.budget.remainderOf(spec.parentAccountScope);
12485
+ const projection = this.projectedDispatchReserveUsd(spec);
12486
+ if (remainder !== void 0 && (remainder <= 0 || remainder < projection + (spec.pendingReserveUsd ?? 0))) return {
12487
+ verdict: {
12488
+ kind: "reject",
12489
+ reason: { code: "budget" }
12490
+ },
12491
+ statsBefore
12492
+ };
12493
+ }
12494
+ this.childrenOf.set(nodeKey, childrenBefore + 1);
12495
+ this.admittedTotal += 1;
12496
+ const lineage = evaluated.decision.lineage;
12497
+ this.registerLineageAdmit(lineage.logicalTaskId);
12498
+ let spawnUnitsAfter = this.budget.spawnHeadroom;
12499
+ if (this.terminationAccount !== void 0) {
12500
+ const debited = this.terminationAccount.debitSpawn({
12501
+ logicalTaskId: lineage.logicalTaskId,
12502
+ isNew: spec.lineage === void 0,
12503
+ ladderLength: spec.ladderLength ?? 1
12504
+ });
12505
+ if (!debited.ok) return {
12506
+ verdict: {
12507
+ kind: "reject",
12508
+ reason: { code: "termination_exhausted" }
12509
+ },
12510
+ statsBefore
12511
+ };
12512
+ spawnUnitsAfter = debited.spawnUnitsAfter;
12513
+ }
12514
+ return {
12515
+ verdict: {
12516
+ kind: "admit",
12517
+ reserve,
12518
+ spawnUnitsAfter,
12519
+ lineage: {
12520
+ logicalTaskId: lineage.logicalTaskId,
12521
+ isNew: spec.lineage === void 0,
12522
+ depth
12523
+ }
12524
+ },
12525
+ statsBefore,
12526
+ nodeId: this.mintId(),
12527
+ lineage,
12528
+ ...this.terminationAccount === void 0 ? {} : { ladderLength: spec.ladderLength ?? 1 }
12529
+ };
12530
+ }
12531
+ /**
12532
+ * Resume roll-forward for an orchestrator child (M6-T07): restores the
12533
+ * children-quota counter only. The budget seed already counts settled
12534
+ * agent dispatches, and an in-flight child re-commits its reserve
12535
+ * through the ctx.agent dispatch path.
12536
+ */
12537
+ recoverChild(nodeKey) {
12538
+ this.childrenOf.set(nodeKey, (this.childrenOf.get(nodeKey) ?? 0) + 1);
12539
+ this.admittedTotal += 1;
12540
+ }
12541
+ /**
12542
+ * Resume roll-forward for a child that already SETTLED before the
12543
+ * resume: re-registers the counters (maxChildrenPerNode, the lifetime
12544
+ * cap, statsBefore fidelity) without committing any reserve; the spend
12545
+ * itself sits in the root ledger seed.
12546
+ */
12547
+ recoverSettled(parentAccountScope) {
12548
+ this.budget.admitRecovered(0, parentAccountScope);
12549
+ this.childrenOf.set(parentAccountScope, (this.childrenOf.get(parentAccountScope) ?? 0) + 1);
12550
+ this.admittedTotal += 1;
12551
+ }
12552
+ /**
12553
+ * Resume roll-forward for an admission whose decision entry exists but
12554
+ * whose child has NOT settled: re-applies the recorded reserve and
12555
+ * counters without re-evaluating any limit (replay never
12556
+ * re-evaluates admission; reserves are recovered, never
12557
+ * re-estimated).
12558
+ */
12559
+ recoverInFlight(parentAccountScope, verdict) {
12560
+ if (verdict.kind === "reject") return;
12561
+ const reserveUsd = verdict.kind === "reuse_full" ? 0 : verdict.reserve.reserveUsd;
12562
+ this.budget.admitRecovered(reserveUsd, parentAccountScope);
12563
+ this.childrenOf.set(parentAccountScope, (this.childrenOf.get(parentAccountScope) ?? 0) + 1);
12564
+ this.admittedTotal += 1;
12565
+ }
12566
+ };
12567
+ //#endregion
12568
+ //#region src/engine/scheduler.ts
12569
+ /**
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.
12575
+ */
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
+ }
12649
+ }
12650
+ release() {
12651
+ this.active -= 1;
12652
+ const next = this.waiters.shift();
12653
+ if (next !== void 0) next.resolve();
12654
+ }
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;
12664
+ }
12665
+ /**
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.
12671
+ */
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;
12710
+ }
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`);
12717
+ }
12718
+ /**
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.
12725
+ */
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);
12735
+ };
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);
12740
+ };
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
12831
12767
  };
12832
- if (commitReserve) try {
12833
- this.budget.admitSpawn(reserveUsd, spec.parentAccountScope);
12834
- } catch {
12835
- return {
12836
- verdict: {
12837
- kind: "reject",
12838
- reason: { code: "budget" }
12839
- },
12840
- statsBefore
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`
12777
+ });
12778
+ }
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
12793
+ });
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
12801
+ });
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
+ });
12866
+ }
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
12898
+ });
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)
12841
12903
  };
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);
12842
12911
  }
12912
+ }
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
+ });
12843
12921
  else {
12844
- const remainder = this.budget.remainderOf(spec.parentAccountScope);
12845
- const projection = this.projectedDispatchReserveUsd(spec);
12846
- if (remainder !== void 0 && (remainder <= 0 || remainder < projection + (spec.pendingReserveUsd ?? 0))) return {
12847
- verdict: {
12848
- kind: "reject",
12849
- reason: { code: "budget" }
12850
- },
12851
- statsBefore
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
12852
12932
  };
12853
- }
12854
- this.childrenOf.set(nodeKey, childrenBefore + 1);
12855
- this.admittedTotal += 1;
12856
- const lineage = evaluated.decision.lineage;
12857
- this.registerLineageAdmit(lineage.logicalTaskId);
12858
- let spawnUnitsAfter = this.budget.spawnHeadroom;
12859
- if (this.terminationAccount !== void 0) {
12860
- const debited = this.terminationAccount.debitSpawn({
12861
- logicalTaskId: lineage.logicalTaskId,
12862
- isNew: spec.lineage === void 0,
12863
- ladderLength: spec.ladderLength ?? 1
12933
+ if (pricing !== void 0 && outputBound !== void 0) unit.turnFloorUsd = priceUsdOf(pricing, {
12934
+ inputTokens: 0,
12935
+ outputTokens: outputBound,
12936
+ cacheReadTokens: 0,
12937
+ cacheWriteTokens: 0
12864
12938
  });
12865
- if (!debited.ok) return {
12866
- verdict: {
12867
- kind: "reject",
12868
- reason: { code: "termination_exhausted" }
12869
- },
12870
- statsBefore
12871
- };
12872
- spawnUnitsAfter = debited.spawnUnitsAfter;
12939
+ units.push(unit);
12873
12940
  }
12874
- return {
12875
- verdict: {
12876
- kind: "admit",
12877
- reserve,
12878
- spawnUnitsAfter,
12879
- lineage: {
12880
- logicalTaskId: lineage.logicalTaskId,
12881
- isNew: spec.lineage === void 0,
12882
- depth
12883
- }
12884
- },
12885
- statsBefore,
12886
- nodeId: this.mintId(),
12887
- lineage,
12888
- ...this.terminationAccount === void 0 ? {} : { ladderLength: spec.ladderLength ?? 1 }
12941
+ }
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);
13022
+ }
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)
12889
13030
  };
12890
13031
  }
12891
- /**
12892
- * Resume roll-forward for an orchestrator child (M6-T07): restores the
12893
- * children-quota counter only. The budget seed already counts settled
12894
- * agent dispatches, and an in-flight child re-commits its reserve
12895
- * through the ctx.agent dispatch path.
12896
- */
12897
- recoverChild(nodeKey) {
12898
- this.childrenOf.set(nodeKey, (this.childrenOf.get(nodeKey) ?? 0) + 1);
12899
- this.admittedTotal += 1;
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
+ }
13062
+ }
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
+ };
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
13119
+ };
13120
+ }
13121
+ //#endregion
13122
+ //#region src/engine/run-profiles.ts
13123
+ /**
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
12900
13176
  }
12901
- /**
12902
- * Resume roll-forward for a child that already SETTLED before the
12903
- * resume: re-registers the counters (maxChildrenPerNode, the lifetime
12904
- * cap, statsBefore fidelity) without committing any reserve; the spend
12905
- * itself sits in the root ledger seed.
12906
- */
12907
- recoverSettled(parentAccountScope) {
12908
- this.budget.admitRecovered(0, parentAccountScope);
12909
- this.childrenOf.set(parentAccountScope, (this.childrenOf.get(parentAccountScope) ?? 0) + 1);
12910
- this.admittedTotal += 1;
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;
12911
13206
  }
12912
13207
  /**
12913
- * Resume roll-forward for an admission whose decision entry exists but
12914
- * whose child has NOT settled: re-applies the recorded reserve and
12915
- * counters without re-evaluating any limit (replay never
12916
- * re-evaluates admission; reserves are recovered, never
12917
- * re-estimated).
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).
12918
13212
  */
12919
- recoverInFlight(parentAccountScope, verdict) {
12920
- if (verdict.kind === "reject") return;
12921
- const reserveUsd = verdict.kind === "reuse_full" ? 0 : verdict.reserve.reserveUsd;
12922
- this.budget.admitRecovered(reserveUsd, parentAccountScope);
12923
- this.childrenOf.set(parentAccountScope, (this.childrenOf.get(parentAccountScope) ?? 0) + 1);
12924
- this.admittedTotal += 1;
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);
12925
13217
  }
12926
13218
  };
12927
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).
13280
+ */
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;
13297
+ return {
13298
+ hooks: [...engine?.hooks ?? [], ...profile?.hooks ?? []],
13299
+ deny,
13300
+ ask,
13301
+ ...canUseTool === void 0 ? {} : { canUseTool }
13302
+ };
13303
+ }
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
+ /**
13336
+ * Unmatchable segments (command/process substitution, here-docs) yield
13337
+ * ask, ALWAYS, for any tool that has argv rules.
13338
+ */
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
13355
+ };
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
12928
13434
  //#region src/orchestrator/finish-validators.ts
12929
13435
  /**
12930
13436
  * Deterministic host validation of the orchestrator finish result (the
@@ -17899,29 +18405,50 @@ function createEngine(options) {
17899
18405
  };
17900
18406
  if (value !== void 0 && (status === "ok" || status === "exhausted")) outcome.value = value;
17901
18407
  if (wireError !== void 0) outcome.error = wireError;
18408
+ let settlementFailure;
17902
18409
  if (resumeCtx?.strict !== true) {
17903
18410
  const priorCount = resumeCtx?.priorEntries.length ?? 0;
17904
- const appendedHere = replayer.snapshot().length - priorCount;
18411
+ const snapshotLength = replayer.snapshot().length;
18412
+ const appendedHere = snapshotLength - priorCount;
17905
18413
  const recorded = lastRunSettle(replayer.snapshot());
17906
- if (appendedHere > 0 || recorded !== void 0 && recorded.runStatus !== status) {
18414
+ if (appendedHere > 0 || recorded !== void 0 && recorded.runStatus !== status || recorded === void 0 && snapshotLength > 0) {
17907
18415
  const outputHash = hashRunOutput(outcome.value);
17908
- await replayer.appendSinglePhase({
17909
- scope: "",
17910
- key: deriverV2.deriveKey({ kind: "run-settle" }),
17911
- kind: "decision",
17912
- status: "ok",
17913
- spanId: rootSpanId,
17914
- site: "run-settle",
17915
- value: {
17916
- decisionType: RUN_SETTLE_DECISION_TYPE,
17917
- runStatus: status,
17918
- segment: segmentsBefore + 1,
17919
- ...outputHash === void 0 ? {} : { outputHash }
17920
- }
17921
- }).catch(() => void 0);
18416
+ try {
18417
+ await replayer.appendSinglePhase({
18418
+ scope: "",
18419
+ key: deriverV2.deriveKey({ kind: "run-settle" }),
18420
+ kind: "decision",
18421
+ status: "ok",
18422
+ spanId: rootSpanId,
18423
+ site: "run-settle",
18424
+ value: {
18425
+ decisionType: RUN_SETTLE_DECISION_TYPE,
18426
+ runStatus: status,
18427
+ segment: segmentsBefore + 1,
18428
+ ...outputHash === void 0 ? {} : { outputHash }
18429
+ }
18430
+ });
18431
+ } catch (settleErr) {
18432
+ if (!(settleErr instanceof LeaseHeldError)) settlementFailure = {
18433
+ stage: "run-settle",
18434
+ cause: settleErr
18435
+ };
18436
+ }
17922
18437
  }
17923
18438
  }
17924
- await putMeta(status).catch(() => void 0);
18439
+ if (settlementFailure === void 0) try {
18440
+ await putMeta(status);
18441
+ } catch (metaErr) {
18442
+ if (!(metaErr instanceof LeaseHeldError)) settlementFailure = {
18443
+ stage: "meta",
18444
+ cause: metaErr
18445
+ };
18446
+ }
18447
+ if (settlementFailure !== void 0) bus.emit({
18448
+ type: "log",
18449
+ level: "warn",
18450
+ msg: `settlement write failed (${settlementFailure.stage}); handle.result rejects with SettlementError; resume re-settles by replay without a provider call`
18451
+ }, rootSpanId);
17925
18452
  const lifted = liftRunCompletion(status === "ok" || status === "exhausted" ? outcome.value : status === "error" ? wireError?.data : void 0);
17926
18453
  bus.emit({
17927
18454
  type: "run:end",
@@ -17936,6 +18463,15 @@ function createEngine(options) {
17936
18463
  invalidResolutions: replayer.fold.invalidResolutions()
17937
18464
  });
17938
18465
  await settleOwnership();
18466
+ if (settlementFailure !== void 0) {
18467
+ const causeText = settlementFailure.cause instanceof Error ? settlementFailure.cause.message : String(settlementFailure.cause);
18468
+ throw new SettlementError(`run '${runId}' computed status '${status}' but the ` + (settlementFailure.stage === "run-settle" ? "run_settle journal append failed" : "terminal RunMeta write failed") + `: ${causeText}; nothing durable records the settlement, so the outcome is withheld. The journal keeps every entry the run appended: resume the run to re-settle by replay (no provider call is paid), or reconcile the store with 'rulvar runs audit'`, {
18469
+ stage: settlementFailure.stage,
18470
+ runId,
18471
+ runStatus: status,
18472
+ cause: settlementFailure.cause
18473
+ });
18474
+ }
17939
18475
  return outcome;
17940
18476
  })();
17941
18477
  result.catch(() => void 0).finally(() => {
@@ -18440,4 +18976,4 @@ function createSandboxBridge(ctx, options) {
18440
18976
  };
18441
18977
  }
18442
18978
  //#endregion
18443
- 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, 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, 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 };
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 };