@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.
- package/dist/index.d.ts +220 -2
- package/dist/index.js +1870 -1334
- 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/
|
|
8364
|
+
//#region src/model/router.ts
|
|
8324
8365
|
/**
|
|
8325
|
-
*
|
|
8326
|
-
*
|
|
8327
|
-
*
|
|
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
|
-
|
|
8330
|
-
|
|
8331
|
-
|
|
8332
|
-
|
|
8333
|
-
|
|
8334
|
-
|
|
8335
|
-
|
|
8336
|
-
}
|
|
8337
|
-
|
|
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
|
-
*
|
|
8392
|
-
*
|
|
8393
|
-
*
|
|
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
|
|
8397
|
-
|
|
8398
|
-
if (
|
|
8399
|
-
|
|
8400
|
-
|
|
8401
|
-
|
|
8402
|
-
|
|
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
|
-
*
|
|
8434
|
-
*
|
|
8435
|
-
*
|
|
8436
|
-
*
|
|
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
|
-
|
|
8440
|
-
|
|
8441
|
-
|
|
8442
|
-
|
|
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
|
-
|
|
8445
|
-
|
|
8446
|
-
|
|
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
|
-
|
|
8449
|
-
|
|
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
|
-
*
|
|
8452
|
-
*
|
|
8453
|
-
*
|
|
8454
|
-
*
|
|
8455
|
-
*
|
|
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
|
-
|
|
8458
|
-
const
|
|
8459
|
-
|
|
8460
|
-
|
|
8461
|
-
|
|
8462
|
-
|
|
8463
|
-
|
|
8464
|
-
|
|
8465
|
-
|
|
8466
|
-
|
|
8467
|
-
|
|
8468
|
-
|
|
8469
|
-
|
|
8470
|
-
|
|
8471
|
-
|
|
8472
|
-
|
|
8473
|
-
|
|
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
|
-
|
|
8480
|
-
|
|
8481
|
-
|
|
8482
|
-
|
|
8483
|
-
|
|
8484
|
-
|
|
8485
|
-
|
|
8486
|
-
|
|
8487
|
-
|
|
8488
|
-
|
|
8489
|
-
|
|
8490
|
-
|
|
8491
|
-
|
|
8492
|
-
}
|
|
8493
|
-
|
|
8494
|
-
|
|
8495
|
-
|
|
8496
|
-
|
|
8497
|
-
|
|
8498
|
-
|
|
8499
|
-
|
|
8500
|
-
|
|
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
|
-
|
|
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
|
-
|
|
8524
|
-
|
|
8525
|
-
|
|
8526
|
-
|
|
8527
|
-
|
|
8528
|
-
|
|
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
|
-
|
|
8532
|
-
|
|
8533
|
-
|
|
8534
|
-
|
|
8535
|
-
|
|
8536
|
-
|
|
8537
|
-
|
|
8538
|
-
|
|
8539
|
-
|
|
8540
|
-
|
|
8541
|
-
|
|
8542
|
-
|
|
8543
|
-
|
|
8544
|
-
|
|
8545
|
-
|
|
8546
|
-
|
|
8547
|
-
|
|
8548
|
-
|
|
8549
|
-
|
|
8550
|
-
|
|
8551
|
-
|
|
8552
|
-
*/
|
|
8553
|
-
|
|
8554
|
-
|
|
8555
|
-
|
|
8556
|
-
|
|
8557
|
-
|
|
8558
|
-
|
|
8559
|
-
|
|
8560
|
-
|
|
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
|
-
|
|
8564
|
-
|
|
8565
|
-
|
|
8566
|
-
|
|
8567
|
-
|
|
8568
|
-
|
|
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
|
-
*
|
|
8591
|
-
*
|
|
8592
|
-
*
|
|
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
|
|
8595
|
-
|
|
8596
|
-
|
|
8597
|
-
|
|
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
|
-
*
|
|
8602
|
-
*
|
|
8603
|
-
*
|
|
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
|
|
8608
|
-
|
|
8609
|
-
if (
|
|
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/
|
|
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/
|
|
9197
|
+
//#region src/model/caps.ts
|
|
9198
|
+
const TIER_ORDER = {
|
|
9199
|
+
native: 2,
|
|
9200
|
+
"forced-tool": 1,
|
|
9201
|
+
prompt: 0
|
|
9202
|
+
};
|
|
9104
9203
|
/**
|
|
9105
|
-
*
|
|
9106
|
-
*
|
|
9107
|
-
*
|
|
9108
|
-
*
|
|
9109
|
-
|
|
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
|
|
9204
|
-
|
|
9205
|
-
|
|
9206
|
-
|
|
9207
|
-
|
|
9208
|
-
|
|
9209
|
-
|
|
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
|
-
|
|
9222
|
-
|
|
9223
|
-
|
|
9224
|
-
|
|
9225
|
-
|
|
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 (
|
|
12833
|
-
|
|
12834
|
-
|
|
12835
|
-
|
|
12836
|
-
|
|
12837
|
-
|
|
12838
|
-
|
|
12839
|
-
|
|
12840
|
-
|
|
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
|
|
12845
|
-
const
|
|
12846
|
-
|
|
12847
|
-
|
|
12848
|
-
|
|
12849
|
-
|
|
12850
|
-
|
|
12851
|
-
|
|
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
|
-
|
|
12855
|
-
|
|
12856
|
-
|
|
12857
|
-
|
|
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
|
-
|
|
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
|
-
|
|
12875
|
-
|
|
12876
|
-
|
|
12877
|
-
|
|
12878
|
-
|
|
12879
|
-
|
|
12880
|
-
|
|
12881
|
-
|
|
12882
|
-
|
|
12883
|
-
|
|
12884
|
-
|
|
12885
|
-
|
|
12886
|
-
|
|
12887
|
-
|
|
12888
|
-
|
|
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
|
-
|
|
12893
|
-
|
|
12894
|
-
|
|
12895
|
-
|
|
12896
|
-
|
|
12897
|
-
|
|
12898
|
-
|
|
12899
|
-
|
|
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
|
-
|
|
12903
|
-
|
|
12904
|
-
|
|
12905
|
-
|
|
12906
|
-
|
|
12907
|
-
|
|
12908
|
-
|
|
12909
|
-
|
|
12910
|
-
|
|
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
|
-
*
|
|
12914
|
-
*
|
|
12915
|
-
*
|
|
12916
|
-
*
|
|
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
|
-
|
|
12920
|
-
|
|
12921
|
-
|
|
12922
|
-
|
|
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
|
|
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
|
-
|
|
17909
|
-
|
|
17910
|
-
|
|
17911
|
-
|
|
17912
|
-
|
|
17913
|
-
|
|
17914
|
-
|
|
17915
|
-
|
|
17916
|
-
|
|
17917
|
-
|
|
17918
|
-
|
|
17919
|
-
|
|
17920
|
-
|
|
17921
|
-
|
|
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
|
-
|
|
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 };
|