@rulvar/core 1.61.0 → 1.62.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 +179 -1
- package/dist/index.js +2502 -2037
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -8320,293 +8320,259 @@ function invoiceFromJournal(entries, priceUsd) {
|
|
|
8320
8320
|
};
|
|
8321
8321
|
}
|
|
8322
8322
|
//#endregion
|
|
8323
|
-
//#region src/
|
|
8323
|
+
//#region src/model/router.ts
|
|
8324
8324
|
/**
|
|
8325
|
-
*
|
|
8326
|
-
*
|
|
8327
|
-
*
|
|
8325
|
+
* Model router core (M1-T05): the per-engine adapter registry, ModelRef
|
|
8326
|
+
* parsing, the per-invocation resolution chain, canonicalization into
|
|
8327
|
+
* CanonicalModelSpec, and caps scrubbing with visible scrub notes.
|
|
8328
|
+
*
|
|
8329
|
+
* Public contract: https://docs.rulvar.com/guide/model-routing.
|
|
8328
8330
|
*/
|
|
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
|
|
8331
|
+
/**
|
|
8332
|
+
* Per-engine adapter registry: strictly per engine, no global mutable
|
|
8333
|
+
* registry exists. A duplicate adapterId is a typed ConfigError.
|
|
8334
|
+
*/
|
|
8335
|
+
function buildAdapterRegistry(adapters) {
|
|
8336
|
+
const registry = /* @__PURE__ */ new Map();
|
|
8337
|
+
for (const adapter of adapters) {
|
|
8338
|
+
if (registry.has(adapter.id)) throw new ConfigError(`duplicate adapterId '${adapter.id}' at createEngine`);
|
|
8339
|
+
registry.set(adapter.id, adapter);
|
|
8377
8340
|
}
|
|
8378
|
-
|
|
8379
|
-
/** Looks up a shipped RunProfile by name; undefined for unknown names. */
|
|
8380
|
-
function runProfile(name) {
|
|
8381
|
-
return RUN_PROFILES[name];
|
|
8341
|
+
return registry;
|
|
8382
8342
|
}
|
|
8383
|
-
//#endregion
|
|
8384
|
-
//#region src/model/caps.ts
|
|
8385
|
-
const TIER_ORDER = {
|
|
8386
|
-
native: 2,
|
|
8387
|
-
"forced-tool": 1,
|
|
8388
|
-
prompt: 0
|
|
8389
|
-
};
|
|
8390
8343
|
/**
|
|
8391
|
-
*
|
|
8392
|
-
*
|
|
8393
|
-
*
|
|
8394
|
-
* non-object shapes are trivially compatible.
|
|
8344
|
+
* ModelRef is strictly 'adapterId:model', no query parameters. The wire
|
|
8345
|
+
* model id may itself contain colons (for example ollama tags), so only
|
|
8346
|
+
* the FIRST colon splits.
|
|
8395
8347
|
*/
|
|
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;
|
|
8348
|
+
function parseModelRef(ref) {
|
|
8349
|
+
const colon = ref.indexOf(":");
|
|
8350
|
+
if (colon <= 0 || colon === ref.length - 1) throw new ConfigError(`invalid ModelRef '${ref}': expected the strict 'adapterId:model' form`);
|
|
8351
|
+
return {
|
|
8352
|
+
adapterId: ref.slice(0, colon),
|
|
8353
|
+
model: ref.slice(colon + 1)
|
|
8354
|
+
};
|
|
8431
8355
|
}
|
|
8432
8356
|
/**
|
|
8433
|
-
*
|
|
8434
|
-
*
|
|
8435
|
-
*
|
|
8436
|
-
*
|
|
8437
|
-
* Prefill is not a tier.
|
|
8357
|
+
* Role effort defaults: orchestrate and plan default to high; summarize and extract
|
|
8358
|
+
* default to low. loop and finalize have NO role default: when the chain
|
|
8359
|
+
* resolves nothing, the wire omits effort and identity records the spec
|
|
8360
|
+
* with the effort member absent.
|
|
8438
8361
|
*/
|
|
8439
|
-
|
|
8440
|
-
|
|
8441
|
-
|
|
8442
|
-
|
|
8362
|
+
const ROLE_EFFORT_DEFAULTS = {
|
|
8363
|
+
orchestrate: "high",
|
|
8364
|
+
plan: "high",
|
|
8365
|
+
summarize: "low",
|
|
8366
|
+
extract: "low"
|
|
8367
|
+
};
|
|
8368
|
+
function contribution(spec, _role) {
|
|
8369
|
+
if (spec === void 0) return {};
|
|
8370
|
+
if (typeof spec === "string") return { model: spec };
|
|
8371
|
+
if ("ladder" in spec) return { ladder: spec.ladder };
|
|
8372
|
+
const choice = spec;
|
|
8373
|
+
const fields = { model: choice.model };
|
|
8374
|
+
if (choice.effort !== void 0) fields.effort = choice.effort;
|
|
8375
|
+
if (choice.providerOptions !== void 0) fields.providerOptions = choice.providerOptions;
|
|
8376
|
+
if (choice.fallbacks !== void 0) fields.fallbacks = choice.fallbacks;
|
|
8377
|
+
return fields;
|
|
8443
8378
|
}
|
|
8444
|
-
|
|
8445
|
-
|
|
8446
|
-
|
|
8379
|
+
function layerFields(layer, role) {
|
|
8380
|
+
if (layer === void 0) return {};
|
|
8381
|
+
const fromModel = contribution(layer.model, role);
|
|
8382
|
+
const fromRouting = contribution(layer.routing?.[role], role);
|
|
8383
|
+
const merged = {
|
|
8384
|
+
...fromModel,
|
|
8385
|
+
...pruneUndefined(fromRouting)
|
|
8386
|
+
};
|
|
8387
|
+
if (layer.effort !== void 0) merged.effort = layer.effort;
|
|
8388
|
+
return merged;
|
|
8447
8389
|
}
|
|
8448
|
-
|
|
8449
|
-
|
|
8390
|
+
function pruneUndefined(value) {
|
|
8391
|
+
const out = {};
|
|
8392
|
+
for (const [key, member] of Object.entries(value)) if (member !== void 0) out[key] = member;
|
|
8393
|
+
return out;
|
|
8394
|
+
}
|
|
8395
|
+
function mergeProviderOptions(lower, higher) {
|
|
8396
|
+
if (lower === void 0) return higher;
|
|
8397
|
+
if (higher === void 0) return lower;
|
|
8398
|
+
const merged = { ...lower };
|
|
8399
|
+
for (const [namespace, options] of Object.entries(higher)) merged[namespace] = {
|
|
8400
|
+
...merged[namespace],
|
|
8401
|
+
...options
|
|
8402
|
+
};
|
|
8403
|
+
return merged;
|
|
8404
|
+
}
|
|
8405
|
+
/** Sampling parameters both first-class providers reject on reasoning models. */
|
|
8406
|
+
const SAMPLING_KEYS = [
|
|
8407
|
+
"temperature",
|
|
8408
|
+
"top_p",
|
|
8409
|
+
"top_k"
|
|
8410
|
+
];
|
|
8450
8411
|
/**
|
|
8451
|
-
*
|
|
8452
|
-
*
|
|
8453
|
-
*
|
|
8454
|
-
*
|
|
8455
|
-
*
|
|
8412
|
+
* Resolution runs on every model invocation, not once per agent: a layered
|
|
8413
|
+
* merge of { model, effort, providerOptions, fallbacks } in the order call
|
|
8414
|
+
* override > agent profile > workflow defaults > engine defaults, with the
|
|
8415
|
+
* invocation role attached as a tag.
|
|
8416
|
+
* After resolution the router reads ModelCaps and scrubs illegal
|
|
8417
|
+
* parameters visibly: unsupported effort is removed from the wire but
|
|
8418
|
+
* kept in identity; sampling params rejected by the model are removed
|
|
8419
|
+
* from the adapter's namespace, never silently sent.
|
|
8456
8420
|
*/
|
|
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;
|
|
8421
|
+
function resolveModelInvocation(options) {
|
|
8422
|
+
const { role } = options;
|
|
8423
|
+
const layers = [
|
|
8424
|
+
options.engine,
|
|
8425
|
+
options.workflow,
|
|
8426
|
+
options.profile,
|
|
8427
|
+
options.call
|
|
8428
|
+
];
|
|
8429
|
+
let merged = {};
|
|
8430
|
+
for (const layer of layers) {
|
|
8431
|
+
const fields = layerFields(layer, role);
|
|
8432
|
+
merged = {
|
|
8433
|
+
...merged,
|
|
8434
|
+
...pruneUndefined(fields),
|
|
8435
|
+
providerOptions: mergeProviderOptions(merged.providerOptions, fields.providerOptions)
|
|
8436
|
+
};
|
|
8437
|
+
if (fields.ladder !== void 0) delete merged.model;
|
|
8438
|
+
else if (fields.model !== void 0) delete merged.ladder;
|
|
8477
8439
|
}
|
|
8478
|
-
|
|
8479
|
-
|
|
8480
|
-
|
|
8481
|
-
|
|
8482
|
-
|
|
8483
|
-
|
|
8484
|
-
|
|
8485
|
-
|
|
8486
|
-
|
|
8487
|
-
|
|
8488
|
-
|
|
8489
|
-
|
|
8490
|
-
|
|
8491
|
-
|
|
8492
|
-
}
|
|
8493
|
-
|
|
8494
|
-
|
|
8495
|
-
|
|
8496
|
-
|
|
8497
|
-
|
|
8498
|
-
|
|
8499
|
-
|
|
8500
|
-
|
|
8440
|
+
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`);
|
|
8441
|
+
if (merged.model === void 0) throw new ConfigError(`no model resolves for role '${role}': set AgentOpts.model, a profile model, or engine defaults.routing`);
|
|
8442
|
+
checkFloors({
|
|
8443
|
+
ref: merged.model,
|
|
8444
|
+
role,
|
|
8445
|
+
...options.floors === void 0 ? {} : { floors: options.floors },
|
|
8446
|
+
...options.taskClass === void 0 ? {} : { taskClass: options.taskClass }
|
|
8447
|
+
});
|
|
8448
|
+
const requestedEffort = merged.effort ?? ROLE_EFFORT_DEFAULTS[role];
|
|
8449
|
+
const { adapterId, model } = parseModelRef(merged.model);
|
|
8450
|
+
let caps;
|
|
8451
|
+
try {
|
|
8452
|
+
caps = options.capsOf(merged.model);
|
|
8453
|
+
} catch (thrown) {
|
|
8454
|
+
if (thrown instanceof ConfigError) throw new ConfigError(`role '${role}': ${thrown.message}`);
|
|
8455
|
+
throw thrown;
|
|
8456
|
+
}
|
|
8457
|
+
const scrubs = [];
|
|
8458
|
+
let wireEffort = requestedEffort;
|
|
8459
|
+
if (wireEffort !== void 0 && !caps.reasoningEfforts.includes(wireEffort)) {
|
|
8460
|
+
scrubs.push({
|
|
8461
|
+
scrubbed: "effort",
|
|
8462
|
+
model: merged.model,
|
|
8463
|
+
detail: `effort '${wireEffort}' is not in caps.reasoningEfforts for ${merged.model}; the request proceeds without it (identity keeps the requested effort)`
|
|
8501
8464
|
});
|
|
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();
|
|
8465
|
+
wireEffort = void 0;
|
|
8522
8466
|
}
|
|
8523
|
-
|
|
8524
|
-
|
|
8525
|
-
|
|
8526
|
-
|
|
8527
|
-
|
|
8528
|
-
|
|
8467
|
+
let providerOptions = merged.providerOptions;
|
|
8468
|
+
if (providerOptions?.[adapterId] !== void 0 && !caps.supportsTemperature) {
|
|
8469
|
+
const namespace = { ...providerOptions[adapterId] };
|
|
8470
|
+
const removed = SAMPLING_KEYS.filter((key) => key in namespace);
|
|
8471
|
+
if (removed.length > 0) {
|
|
8472
|
+
for (const key of removed) delete namespace[key];
|
|
8473
|
+
providerOptions = {
|
|
8474
|
+
...providerOptions,
|
|
8475
|
+
[adapterId]: namespace
|
|
8476
|
+
};
|
|
8477
|
+
scrubs.push({
|
|
8478
|
+
scrubbed: "sampling",
|
|
8479
|
+
model: merged.model,
|
|
8480
|
+
detail: `sampling parameter(s) ${removed.join(", ")} removed for ${merged.model}: the model rejects them (caps.supportsTemperature is false); never silently sent`
|
|
8481
|
+
});
|
|
8529
8482
|
}
|
|
8530
8483
|
}
|
|
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
|
-
|
|
8484
|
+
const canonical = requestedEffort === void 0 ? {
|
|
8485
|
+
kind: "model",
|
|
8486
|
+
model: merged.model
|
|
8487
|
+
} : {
|
|
8488
|
+
kind: "model",
|
|
8489
|
+
model: merged.model,
|
|
8490
|
+
effort: requestedEffort
|
|
8491
|
+
};
|
|
8492
|
+
const resolved = {
|
|
8493
|
+
ref: merged.model,
|
|
8494
|
+
adapterId,
|
|
8495
|
+
model,
|
|
8496
|
+
canonical,
|
|
8497
|
+
scrubs
|
|
8498
|
+
};
|
|
8499
|
+
if (wireEffort !== void 0) resolved.wireEffort = wireEffort;
|
|
8500
|
+
if (requestedEffort !== void 0) resolved.requestedEffort = requestedEffort;
|
|
8501
|
+
if (providerOptions !== void 0) resolved.providerOptions = providerOptions;
|
|
8502
|
+
if (merged.fallbacks !== void 0) resolved.fallbacks = merged.fallbacks;
|
|
8503
|
+
return resolved;
|
|
8504
|
+
}
|
|
8505
|
+
/** The closed trigger vocabulary guard. */
|
|
8506
|
+
const TRIGGER_CLASSES = [
|
|
8507
|
+
"error",
|
|
8508
|
+
"limit",
|
|
8509
|
+
"schema-exhausted",
|
|
8510
|
+
"verify-failed",
|
|
8511
|
+
"no-progress"
|
|
8512
|
+
];
|
|
8513
|
+
function validateGate(gate, rungCount, index) {
|
|
8514
|
+
if (gate.kind === "mechanical") {
|
|
8515
|
+
if (typeof gate.profile !== "string" || gate.profile === "") throw new ConfigError(`ladder acceptance gate ${String(index)}: a mechanical gate names a registered gate profile`);
|
|
8516
|
+
return;
|
|
8561
8517
|
}
|
|
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);
|
|
8518
|
+
if (gate.kind === "judge") {
|
|
8519
|
+
if (typeof gate.rung === "number") {
|
|
8520
|
+
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)`);
|
|
8521
|
+
return;
|
|
8522
|
+
}
|
|
8523
|
+
parseModelRef(gate.rung);
|
|
8524
|
+
return;
|
|
8572
8525
|
}
|
|
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";
|
|
8526
|
+
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
8527
|
}
|
|
8589
8528
|
/**
|
|
8590
|
-
*
|
|
8591
|
-
*
|
|
8592
|
-
*
|
|
8529
|
+
* Canonicalizes a declared LadderSpec: validates the
|
|
8530
|
+
* shape once (FR-119 judge declaration included) and resolves every rung's
|
|
8531
|
+
* effort to an explicit value. `chainEffort` is the effort the resolution
|
|
8532
|
+
* chain would contribute at the declaring layer; a rung that resolves no
|
|
8533
|
+
* effort at all is a ConfigError (the canonical form has no absent-effort
|
|
8534
|
+
* member by declaration).
|
|
8593
8535
|
*/
|
|
8594
|
-
function
|
|
8595
|
-
|
|
8596
|
-
|
|
8597
|
-
|
|
8598
|
-
|
|
8536
|
+
function canonicalizeLadder(spec, options) {
|
|
8537
|
+
if (!Array.isArray(spec.rungs) || spec.rungs.length === 0) throw new ConfigError("a ladder declares at least one rung");
|
|
8538
|
+
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`);
|
|
8539
|
+
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(", ")}`);
|
|
8540
|
+
const rungs = spec.rungs.map((rung, index) => {
|
|
8541
|
+
parseModelRef(rung.model);
|
|
8542
|
+
if (!Number.isInteger(rung.maxTurns) || rung.maxTurns <= 0) throw new ConfigError(`ladder rung ${String(index)}: maxTurns is a positive integer`);
|
|
8543
|
+
if (!Number.isInteger(rung.maxTokens) || rung.maxTokens <= 0) throw new ConfigError(`ladder rung ${String(index)}: maxTokens is a positive integer`);
|
|
8544
|
+
if (rung.maxCostUsd !== void 0 && !(rung.maxCostUsd > 0)) throw new ConfigError(`ladder rung ${String(index)}: maxCostUsd is positive when present`);
|
|
8545
|
+
const effort = rung.effort ?? options?.chainEffort;
|
|
8546
|
+
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`);
|
|
8547
|
+
return {
|
|
8548
|
+
model: rung.model,
|
|
8549
|
+
effort,
|
|
8550
|
+
maxTurns: rung.maxTurns,
|
|
8551
|
+
maxTokens: rung.maxTokens,
|
|
8552
|
+
...rung.maxCostUsd === void 0 ? {} : { maxCostUsd: rung.maxCostUsd },
|
|
8553
|
+
...rung.memoizeOutcome === void 0 ? {} : { memoizeOutcome: rung.memoizeOutcome }
|
|
8554
|
+
};
|
|
8555
|
+
});
|
|
8556
|
+
for (const [index, gate] of (spec.acceptance ?? []).entries()) validateGate(gate, spec.rungs.length, index);
|
|
8557
|
+
return {
|
|
8558
|
+
rungs,
|
|
8559
|
+
startTier: spec.startTier,
|
|
8560
|
+
escalateOn: [...spec.escalateOn],
|
|
8561
|
+
...spec.acceptance === void 0 ? {} : { acceptance: spec.acceptance.map((gate) => gate) }
|
|
8562
|
+
};
|
|
8599
8563
|
}
|
|
8600
8564
|
/**
|
|
8601
|
-
*
|
|
8602
|
-
*
|
|
8603
|
-
*
|
|
8604
|
-
* no-progress abort included) are 'limit'; cancelled, escalated, and
|
|
8605
|
-
* skipped never trigger.
|
|
8565
|
+
* The concrete ModelChoice of one rung attempt: each attempt is an
|
|
8566
|
+
* ordinary agent scope whose CanonicalModelSpec is that rung's
|
|
8567
|
+
* `{ kind: 'model' }` form.
|
|
8606
8568
|
*/
|
|
8607
|
-
function
|
|
8608
|
-
|
|
8609
|
-
if (
|
|
8569
|
+
function ladderRungChoice(ladder, index) {
|
|
8570
|
+
const rung = ladder.rungs[index];
|
|
8571
|
+
if (rung === void 0) throw new ConfigError(`rung ${String(index)} is not declared on a ${String(ladder.rungs.length)}-rung ladder`);
|
|
8572
|
+
return {
|
|
8573
|
+
model: rung.model,
|
|
8574
|
+
effort: rung.effort
|
|
8575
|
+
};
|
|
8610
8576
|
}
|
|
8611
8577
|
//#endregion
|
|
8612
8578
|
//#region src/model/pricing.ts
|
|
@@ -8665,91 +8631,7 @@ function affordableOutputTokens(pricing, remainingUsd, estimatedInputTokens) {
|
|
|
8665
8631
|
return Math.floor((remainingUsd - inputUsd) / outputRate * 1e6);
|
|
8666
8632
|
}
|
|
8667
8633
|
//#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
|
|
8634
|
+
//#region src/model/quota.ts
|
|
8753
8635
|
/**
|
|
8754
8636
|
* Quota rules and the in-process reference QuotaLimiter (RV-215).
|
|
8755
8637
|
* The rule model is shared by every reference implementation
|
|
@@ -8966,6 +8848,177 @@ function validateEngineQuotaConfig(config, site = "createEngine quota") {
|
|
|
8966
8848
|
if (candidate.onLimiterError !== void 0 && candidate.onLimiterError !== "deny" && candidate.onLimiterError !== "allow") throw new ConfigError(`${site}.onLimiterError must be 'deny' or 'allow' when given`);
|
|
8967
8849
|
}
|
|
8968
8850
|
//#endregion
|
|
8851
|
+
//#region src/runtime/usage-limits.ts
|
|
8852
|
+
/**
|
|
8853
|
+
* UsageLimits (M1-T06): normative limit vocabulary and the per-spawn merge.
|
|
8854
|
+
*
|
|
8855
|
+
* Full contract: https://docs.rulvar.com/guide/agents. Expiry of maxTurns, maxToolCalls,
|
|
8856
|
+
* or timeoutMs produces the terminal status 'limit' (paid partial work);
|
|
8857
|
+
* streamIdleTimeoutMs expiry is a retryable transport-class AgentError,
|
|
8858
|
+
* never 'limit'. The run-level deadline is RunOptions.deadlineAt, not a
|
|
8859
|
+
* UsageLimits field.
|
|
8860
|
+
*/
|
|
8861
|
+
const DEFAULT_MAX_TURNS = 32;
|
|
8862
|
+
const DEFAULT_STREAM_IDLE_TIMEOUT_MS = 12e4;
|
|
8863
|
+
/**
|
|
8864
|
+
* Limits merge per spawn: AgentOpts.limits over profile limits over engine
|
|
8865
|
+
* defaults.limits.
|
|
8866
|
+
*/
|
|
8867
|
+
function mergeUsageLimits(call, profile, engine) {
|
|
8868
|
+
const pick = (key) => call?.[key] ?? profile?.[key] ?? engine?.[key];
|
|
8869
|
+
const merged = {
|
|
8870
|
+
maxTurns: pick("maxTurns") ?? 32,
|
|
8871
|
+
streamIdleTimeoutMs: pick("streamIdleTimeoutMs") ?? 12e4
|
|
8872
|
+
};
|
|
8873
|
+
const maxToolCalls = pick("maxToolCalls");
|
|
8874
|
+
if (maxToolCalls !== void 0) merged.maxToolCalls = maxToolCalls;
|
|
8875
|
+
const maxOutputTokensPerTurn = pick("maxOutputTokensPerTurn");
|
|
8876
|
+
if (maxOutputTokensPerTurn !== void 0) merged.maxOutputTokensPerTurn = maxOutputTokensPerTurn;
|
|
8877
|
+
const timeoutMs = pick("timeoutMs");
|
|
8878
|
+
if (timeoutMs !== void 0) merged.timeoutMs = timeoutMs;
|
|
8879
|
+
const noProgressTurns = pick("noProgressTurns");
|
|
8880
|
+
if (noProgressTurns !== void 0) merged.noProgressTurns = noProgressTurns;
|
|
8881
|
+
const toolBudgetNotices = pick("toolBudgetNotices");
|
|
8882
|
+
if (toolBudgetNotices !== void 0) merged.toolBudgetNotices = toolBudgetNotices;
|
|
8883
|
+
const maxRepeatedToolSignature = pick("maxRepeatedToolSignature");
|
|
8884
|
+
if (maxRepeatedToolSignature !== void 0) merged.maxRepeatedToolSignature = maxRepeatedToolSignature;
|
|
8885
|
+
const maxNoNewEvidenceCalls = pick("maxNoNewEvidenceCalls");
|
|
8886
|
+
if (maxNoNewEvidenceCalls !== void 0) merged.maxNoNewEvidenceCalls = maxNoNewEvidenceCalls;
|
|
8887
|
+
const maxCallsPerTool = pick("maxCallsPerTool");
|
|
8888
|
+
if (maxCallsPerTool !== void 0) merged.maxCallsPerTool = maxCallsPerTool;
|
|
8889
|
+
const toolUnits = pick("toolUnits");
|
|
8890
|
+
if (toolUnits !== void 0) merged.toolUnits = toolUnits;
|
|
8891
|
+
const finalizationReserve = pick("finalizationReserve");
|
|
8892
|
+
if (finalizationReserve !== void 0) merged.finalizationReserve = finalizationReserve;
|
|
8893
|
+
return merged;
|
|
8894
|
+
}
|
|
8895
|
+
/**
|
|
8896
|
+
* Validates one UsageLimits layer at its intake boundary (v1.34.0
|
|
8897
|
+
* review P2-3): a malformed field (NaN, Infinity, a negative, a
|
|
8898
|
+
* fraction) is a typed ConfigError before the merge, before any journal
|
|
8899
|
+
* entry, and before any provider dispatch. `site` names the layer in the
|
|
8900
|
+
* error text (e.g. `RunOptions.limits`). Counts are positive integers
|
|
8901
|
+
* (maxToolCalls may be 0: a spawn that must not call tools).
|
|
8902
|
+
* streamIdleTimeoutMs is handed to setTimeout as-is, so it is bounded by
|
|
8903
|
+
* the Node timer maximum like RetryPolicy delays; timeoutMs is a
|
|
8904
|
+
* wall-clock comparison, so it has no upper bound. Every present field
|
|
8905
|
+
* is checked; absent fields keep their defaults.
|
|
8906
|
+
*/
|
|
8907
|
+
function validateUsageLimits(limits, site) {
|
|
8908
|
+
if (limits.maxTurns !== void 0) requirePositiveInteger(limits.maxTurns, `${site}.maxTurns`);
|
|
8909
|
+
if (limits.maxToolCalls !== void 0) requireNonNegativeInteger(limits.maxToolCalls, `${site}.maxToolCalls`);
|
|
8910
|
+
if (limits.maxOutputTokensPerTurn !== void 0) requirePositiveInteger(limits.maxOutputTokensPerTurn, `${site}.maxOutputTokensPerTurn`);
|
|
8911
|
+
if (limits.timeoutMs !== void 0) requirePositiveInteger(limits.timeoutMs, `${site}.timeoutMs`);
|
|
8912
|
+
if (limits.streamIdleTimeoutMs !== void 0) requireTimerDelayMs(limits.streamIdleTimeoutMs, `${site}.streamIdleTimeoutMs`);
|
|
8913
|
+
if (limits.noProgressTurns !== void 0) requirePositiveInteger(limits.noProgressTurns, `${site}.noProgressTurns`);
|
|
8914
|
+
if (limits.toolBudgetNotices !== void 0 && typeof limits.toolBudgetNotices !== "boolean") throw new ConfigError(`${site}.toolBudgetNotices must be a boolean; got ${typeof limits.toolBudgetNotices}`);
|
|
8915
|
+
if (limits.maxRepeatedToolSignature !== void 0) requirePositiveInteger(limits.maxRepeatedToolSignature, `${site}.maxRepeatedToolSignature`);
|
|
8916
|
+
if (limits.maxNoNewEvidenceCalls !== void 0) requirePositiveInteger(limits.maxNoNewEvidenceCalls, `${site}.maxNoNewEvidenceCalls`);
|
|
8917
|
+
if (limits.maxCallsPerTool !== void 0) {
|
|
8918
|
+
const caps = limits.maxCallsPerTool;
|
|
8919
|
+
if (typeof caps !== "object" || caps === null || Array.isArray(caps)) throw new ConfigError(`${site}.maxCallsPerTool must be a record of per-tool caps`);
|
|
8920
|
+
for (const [name, cap] of Object.entries(caps)) requireNonNegativeInteger(cap, `${site}.maxCallsPerTool['${name}']`);
|
|
8921
|
+
}
|
|
8922
|
+
if (limits.toolUnits !== void 0) {
|
|
8923
|
+
const units = limits.toolUnits;
|
|
8924
|
+
if (typeof units !== "object" || units === null || Array.isArray(units)) throw new ConfigError(`${site}.toolUnits must be { max, costs? }`);
|
|
8925
|
+
const { max, costs } = units;
|
|
8926
|
+
requirePositiveInteger(max, `${site}.toolUnits.max`);
|
|
8927
|
+
if (costs !== void 0) {
|
|
8928
|
+
if (typeof costs !== "object" || costs === null || Array.isArray(costs)) throw new ConfigError(`${site}.toolUnits.costs must be a record of per-tool costs`);
|
|
8929
|
+
for (const [name, cost] of Object.entries(costs)) requireNonNegativeInteger(cost, `${site}.toolUnits.costs['${name}']`);
|
|
8930
|
+
}
|
|
8931
|
+
}
|
|
8932
|
+
if (limits.finalizationReserve !== void 0) {
|
|
8933
|
+
const reserve = limits.finalizationReserve;
|
|
8934
|
+
if (typeof reserve !== "object" || reserve === null || Array.isArray(reserve)) throw new ConfigError(`${site}.finalizationReserve must be { maxOutputTokens? }`);
|
|
8935
|
+
const { maxOutputTokens } = reserve;
|
|
8936
|
+
if (maxOutputTokens !== void 0) requirePositiveInteger(maxOutputTokens, `${site}.finalizationReserve.maxOutputTokens`);
|
|
8937
|
+
}
|
|
8938
|
+
}
|
|
8939
|
+
//#endregion
|
|
8940
|
+
//#region src/model/failover.ts
|
|
8941
|
+
/** Normalizes the author-facing ModelChoice.fallbacks list. */
|
|
8942
|
+
function normalizeFallbacks(refs) {
|
|
8943
|
+
return (refs ?? []).map((model) => ({ model }));
|
|
8944
|
+
}
|
|
8945
|
+
/**
|
|
8946
|
+
* Maps a retry class to its failover trigger once retries exhaust.
|
|
8947
|
+
* Overloaded (529) is transport-class for failover purposes; a
|
|
8948
|
+
* non-retryable error never fails over.
|
|
8949
|
+
*/
|
|
8950
|
+
function failoverTriggerOf(retryClass) {
|
|
8951
|
+
if (retryClass === void 0) return;
|
|
8952
|
+
return retryClass === "rate-limit" ? "rate-limit" : "transport";
|
|
8953
|
+
}
|
|
8954
|
+
/**
|
|
8955
|
+
* The next target index past `from` that serves `trigger`, or undefined
|
|
8956
|
+
* when the chain is exhausted. Index 0 is the primary; the chain never
|
|
8957
|
+
* moves backwards (sticky failover).
|
|
8958
|
+
*/
|
|
8959
|
+
function nextFailover(targets, trigger, from) {
|
|
8960
|
+
for (let index = from + 1; index < targets.length; index += 1) {
|
|
8961
|
+
const on = targets[index]?.on;
|
|
8962
|
+
if (on === void 0 || on.includes(trigger)) return index;
|
|
8963
|
+
}
|
|
8964
|
+
}
|
|
8965
|
+
/**
|
|
8966
|
+
* Classifies a terminal agent outcome for the degenerate fallback:
|
|
8967
|
+
* schema-mismatch errors are
|
|
8968
|
+
* 'schema-exhausted'; any other error is 'error'; limit terminals (the
|
|
8969
|
+
* no-progress abort included) are 'limit'; cancelled, escalated, and
|
|
8970
|
+
* skipped never trigger.
|
|
8971
|
+
*/
|
|
8972
|
+
function fallbackTriggerOf(outcome) {
|
|
8973
|
+
if (outcome.status === "error") return outcome.error?.kind === "schema-mismatch" ? "schema-exhausted" : "error";
|
|
8974
|
+
if (outcome.status === "limit") return "limit";
|
|
8975
|
+
}
|
|
8976
|
+
//#endregion
|
|
8977
|
+
//#region src/model/projector.ts
|
|
8978
|
+
/** The provider family of an adapter: `provider` when set, else `id`. */
|
|
8979
|
+
function providerOf(adapter) {
|
|
8980
|
+
return adapter.provider ?? adapter.id;
|
|
8981
|
+
}
|
|
8982
|
+
/**
|
|
8983
|
+
* Projects the canonical history into the target provider's view:
|
|
8984
|
+
* provider-raw parts of a DIFFERENT provider are omitted; everything
|
|
8985
|
+
* else (text, images, tool calls, tool results, compaction content)
|
|
8986
|
+
* passes through untouched. Messages whose parts all belong to another
|
|
8987
|
+
* provider vanish entirely rather than ride as empty messages.
|
|
8988
|
+
*/
|
|
8989
|
+
function projectHistory(messages, targetProvider) {
|
|
8990
|
+
const projected = [];
|
|
8991
|
+
for (const msg of messages) {
|
|
8992
|
+
const parts = msg.parts.filter((part) => part.type !== "provider-raw" || part.provider === targetProvider);
|
|
8993
|
+
if (parts.length === 0 && msg.parts.length > 0) continue;
|
|
8994
|
+
projected.push(parts.length === msg.parts.length ? msg : {
|
|
8995
|
+
...msg,
|
|
8996
|
+
parts
|
|
8997
|
+
});
|
|
8998
|
+
}
|
|
8999
|
+
return projected;
|
|
9000
|
+
}
|
|
9001
|
+
/**
|
|
9002
|
+
* Lifts the adapter-shipped retention payload of one finished turn into
|
|
9003
|
+
* provider-raw parts (the retention transport). Reads
|
|
9004
|
+
* providerMetadata[<adapter id>].retainedParts and tags each block with
|
|
9005
|
+
* the adapter's provider family. Returns [] when the adapter shipped
|
|
9006
|
+
* nothing.
|
|
9007
|
+
*/
|
|
9008
|
+
function liftRetainedParts(providerMetadata, adapter) {
|
|
9009
|
+
const namespace = providerMetadata?.[adapter.id];
|
|
9010
|
+
if (typeof namespace !== "object" || namespace === null) return [];
|
|
9011
|
+
const retained = namespace.retainedParts;
|
|
9012
|
+
if (!Array.isArray(retained)) return [];
|
|
9013
|
+
const blocks = retained;
|
|
9014
|
+
const provider = providerOf(adapter);
|
|
9015
|
+
return blocks.map((block) => ({
|
|
9016
|
+
type: "provider-raw",
|
|
9017
|
+
provider,
|
|
9018
|
+
block
|
|
9019
|
+
}));
|
|
9020
|
+
}
|
|
9021
|
+
//#endregion
|
|
8969
9022
|
//#region src/model/retry.ts
|
|
8970
9023
|
/**
|
|
8971
9024
|
* Transport RetryPolicy (M4-T05): retries live UNDER the journal. A
|
|
@@ -9100,21 +9153,234 @@ function retryDelayMs(policy, retryIndex, retryAfterMs, random = nativeRandom) {
|
|
|
9100
9153
|
return timerSafe(base / 2 + random() * (base / 2));
|
|
9101
9154
|
}
|
|
9102
9155
|
//#endregion
|
|
9103
|
-
//#region src/model/
|
|
9156
|
+
//#region src/model/caps.ts
|
|
9157
|
+
const TIER_ORDER = {
|
|
9158
|
+
native: 2,
|
|
9159
|
+
"forced-tool": 1,
|
|
9160
|
+
prompt: 0
|
|
9161
|
+
};
|
|
9104
9162
|
/**
|
|
9105
|
-
*
|
|
9106
|
-
*
|
|
9107
|
-
*
|
|
9108
|
-
*
|
|
9109
|
-
* agent with no tools every tier rides (the M1 behavior, unchanged).
|
|
9163
|
+
* Strict-schema compatibility as both first-class providers define it:
|
|
9164
|
+
* every object node declares `additionalProperties: false` and lists every
|
|
9165
|
+
* property in `required`. Boolean schemas and
|
|
9166
|
+
* non-object shapes are trivially compatible.
|
|
9110
9167
|
*/
|
|
9111
|
-
function
|
|
9112
|
-
|
|
9113
|
-
|
|
9114
|
-
|
|
9115
|
-
|
|
9116
|
-
|
|
9117
|
-
|
|
9168
|
+
function isStrictCompatibleSchema(schema) {
|
|
9169
|
+
if (typeof schema === "boolean") return true;
|
|
9170
|
+
if (schema.type === "object" || schema.properties !== void 0 || schema.additionalProperties !== void 0) {
|
|
9171
|
+
if (schema.additionalProperties !== false) return false;
|
|
9172
|
+
const properties = typeof schema.properties === "object" && schema.properties !== null ? schema.properties : {};
|
|
9173
|
+
const required = Array.isArray(schema.required) ? schema.required : [];
|
|
9174
|
+
for (const name of Object.keys(properties)) if (!required.includes(name)) return false;
|
|
9175
|
+
for (const value of Object.values(properties)) if (typeof value === "object" && value !== null || typeof value === "boolean") {
|
|
9176
|
+
if (!isStrictCompatibleSchema(value)) return false;
|
|
9177
|
+
}
|
|
9178
|
+
}
|
|
9179
|
+
for (const key of [
|
|
9180
|
+
"items",
|
|
9181
|
+
"additionalProperties",
|
|
9182
|
+
"contains"
|
|
9183
|
+
]) {
|
|
9184
|
+
const value = schema[key];
|
|
9185
|
+
if (typeof value === "object" && value !== null || typeof value === "boolean") {
|
|
9186
|
+
if (!isStrictCompatibleSchema(value)) return false;
|
|
9187
|
+
}
|
|
9188
|
+
}
|
|
9189
|
+
for (const key of [
|
|
9190
|
+
"allOf",
|
|
9191
|
+
"anyOf",
|
|
9192
|
+
"oneOf",
|
|
9193
|
+
"prefixItems"
|
|
9194
|
+
]) {
|
|
9195
|
+
const value = schema[key];
|
|
9196
|
+
if (Array.isArray(value)) {
|
|
9197
|
+
for (const element of value) if (typeof element === "object" && element !== null || typeof element === "boolean") {
|
|
9198
|
+
if (!isStrictCompatibleSchema(element)) return false;
|
|
9199
|
+
}
|
|
9200
|
+
}
|
|
9201
|
+
}
|
|
9202
|
+
return true;
|
|
9203
|
+
}
|
|
9204
|
+
/**
|
|
9205
|
+
* Tier selection: the model's declared ceiling
|
|
9206
|
+
* bounds the tier; the native tier additionally requires a
|
|
9207
|
+
* strict-compatible canonical schema (relying on silent server-side
|
|
9208
|
+
* fallback is forbidden), degrading to forced-tool.
|
|
9209
|
+
* Prefill is not a tier.
|
|
9210
|
+
*/
|
|
9211
|
+
function selectStructuredOutputTier(caps, canonicalSchema) {
|
|
9212
|
+
const ceiling = caps.structuredOutput;
|
|
9213
|
+
if (ceiling === "native" && !isStrictCompatibleSchema(canonicalSchema)) return "forced-tool";
|
|
9214
|
+
return ceiling;
|
|
9215
|
+
}
|
|
9216
|
+
/** True when `tier` is at or below the model's declared ceiling. */
|
|
9217
|
+
function tierWithinCaps(tier, caps) {
|
|
9218
|
+
return TIER_ORDER[tier] <= TIER_ORDER[caps.structuredOutput];
|
|
9219
|
+
}
|
|
9220
|
+
//#endregion
|
|
9221
|
+
//#region src/runtime/escalation.ts
|
|
9222
|
+
const ESCALATE_TOOL_NAME = "escalate";
|
|
9223
|
+
/**
|
|
9224
|
+
* The escalate tool's exact request schema. costToDate and salvage
|
|
9225
|
+
* MUST NOT appear here: additionalProperties false rejects model-authored
|
|
9226
|
+
* values for them at argument validation.
|
|
9227
|
+
*/
|
|
9228
|
+
const ESCALATION_REQUEST_SCHEMA = {
|
|
9229
|
+
type: "object",
|
|
9230
|
+
additionalProperties: false,
|
|
9231
|
+
required: [
|
|
9232
|
+
"kind",
|
|
9233
|
+
"scopeDelta",
|
|
9234
|
+
"revisedEstimate"
|
|
9235
|
+
],
|
|
9236
|
+
properties: {
|
|
9237
|
+
kind: { enum: [
|
|
9238
|
+
"scope_bigger",
|
|
9239
|
+
"scope_different",
|
|
9240
|
+
"blocked_with_evidence"
|
|
9241
|
+
] },
|
|
9242
|
+
scopeDelta: { type: "string" },
|
|
9243
|
+
revisedEstimate: {
|
|
9244
|
+
type: "object",
|
|
9245
|
+
additionalProperties: false,
|
|
9246
|
+
required: ["usd", "turns"],
|
|
9247
|
+
properties: {
|
|
9248
|
+
usd: {
|
|
9249
|
+
type: "number",
|
|
9250
|
+
minimum: 0
|
|
9251
|
+
},
|
|
9252
|
+
turns: {
|
|
9253
|
+
type: "integer",
|
|
9254
|
+
minimum: 0
|
|
9255
|
+
}
|
|
9256
|
+
}
|
|
9257
|
+
},
|
|
9258
|
+
blockers: {
|
|
9259
|
+
type: "array",
|
|
9260
|
+
items: { type: "string" }
|
|
9261
|
+
},
|
|
9262
|
+
proposedDecomposition: {
|
|
9263
|
+
type: "array",
|
|
9264
|
+
items: { type: "object" }
|
|
9265
|
+
}
|
|
9266
|
+
}
|
|
9267
|
+
};
|
|
9268
|
+
/** The full-report schema applied BEFORE append. */
|
|
9269
|
+
const ESCALATION_REPORT_SCHEMA = {
|
|
9270
|
+
type: "object",
|
|
9271
|
+
additionalProperties: false,
|
|
9272
|
+
required: [
|
|
9273
|
+
"kind",
|
|
9274
|
+
"scopeDelta",
|
|
9275
|
+
"revisedEstimate",
|
|
9276
|
+
"blockers",
|
|
9277
|
+
"proposedDecomposition",
|
|
9278
|
+
"costToDate",
|
|
9279
|
+
"salvage"
|
|
9280
|
+
],
|
|
9281
|
+
properties: {
|
|
9282
|
+
kind: { enum: [
|
|
9283
|
+
"scope_bigger",
|
|
9284
|
+
"scope_different",
|
|
9285
|
+
"blocked_with_evidence"
|
|
9286
|
+
] },
|
|
9287
|
+
scopeDelta: { type: "string" },
|
|
9288
|
+
revisedEstimate: {
|
|
9289
|
+
type: "object",
|
|
9290
|
+
additionalProperties: false,
|
|
9291
|
+
required: ["usd", "turns"],
|
|
9292
|
+
properties: {
|
|
9293
|
+
usd: {
|
|
9294
|
+
type: "number",
|
|
9295
|
+
minimum: 0
|
|
9296
|
+
},
|
|
9297
|
+
turns: {
|
|
9298
|
+
type: "integer",
|
|
9299
|
+
minimum: 0
|
|
9300
|
+
}
|
|
9301
|
+
}
|
|
9302
|
+
},
|
|
9303
|
+
blockers: {
|
|
9304
|
+
type: "array",
|
|
9305
|
+
items: { type: "string" }
|
|
9306
|
+
},
|
|
9307
|
+
proposedDecomposition: {
|
|
9308
|
+
type: "array",
|
|
9309
|
+
items: { type: "object" }
|
|
9310
|
+
},
|
|
9311
|
+
costToDate: {
|
|
9312
|
+
type: "object",
|
|
9313
|
+
additionalProperties: false,
|
|
9314
|
+
required: ["usd", "turns"],
|
|
9315
|
+
properties: {
|
|
9316
|
+
usd: { type: "number" },
|
|
9317
|
+
turns: {
|
|
9318
|
+
type: "integer",
|
|
9319
|
+
minimum: 0
|
|
9320
|
+
}
|
|
9321
|
+
}
|
|
9322
|
+
},
|
|
9323
|
+
salvage: {
|
|
9324
|
+
type: "object",
|
|
9325
|
+
additionalProperties: false,
|
|
9326
|
+
required: ["transcriptRef", "artifacts"],
|
|
9327
|
+
properties: {
|
|
9328
|
+
transcriptRef: { type: "string" },
|
|
9329
|
+
artifacts: {
|
|
9330
|
+
type: "array",
|
|
9331
|
+
items: { type: "string" }
|
|
9332
|
+
},
|
|
9333
|
+
worktreePatchRef: { type: "string" }
|
|
9334
|
+
}
|
|
9335
|
+
}
|
|
9336
|
+
}
|
|
9337
|
+
};
|
|
9338
|
+
/**
|
|
9339
|
+
* The engine opt-in tool: registered through the
|
|
9340
|
+
* same path as any tool under escalation opt-in of EITHER flavor (the
|
|
9341
|
+
* worker's only authoring channel for a report), never available without
|
|
9342
|
+
* opt-in, and dispatched through the same permission chain. The loop
|
|
9343
|
+
* intercepts accepted calls; execute is unreachable by construction.
|
|
9344
|
+
*/
|
|
9345
|
+
function escalateTool() {
|
|
9346
|
+
return tool({
|
|
9347
|
+
name: ESCALATE_TOOL_NAME,
|
|
9348
|
+
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.",
|
|
9349
|
+
parameters: ESCALATION_REQUEST_SCHEMA,
|
|
9350
|
+
execute: () => {
|
|
9351
|
+
throw new Error("escalate is intercepted by the agent runtime, never executed");
|
|
9352
|
+
}
|
|
9353
|
+
});
|
|
9354
|
+
}
|
|
9355
|
+
/** Validates the runtime-completed report BEFORE append; returns issues. */
|
|
9356
|
+
async function validateEscalationReport(report) {
|
|
9357
|
+
const validation = await validateSchemaSpec(ESCALATION_REPORT_SCHEMA, report);
|
|
9358
|
+
return validation.valid ? [] : validation.issues;
|
|
9359
|
+
}
|
|
9360
|
+
/**
|
|
9361
|
+
* countsAgainstLimit derivation (XF-06): true iff
|
|
9362
|
+
* scope_bigger; scope_different and blocked_with_evidence are exempt and
|
|
9363
|
+
* never debit the escalation counter.
|
|
9364
|
+
*/
|
|
9365
|
+
function countsAgainstLimit(kind) {
|
|
9366
|
+
return kind === "scope_bigger";
|
|
9367
|
+
}
|
|
9368
|
+
//#endregion
|
|
9369
|
+
//#region src/model/roles.ts
|
|
9370
|
+
/**
|
|
9371
|
+
* True when the given structured-output tier can ride the last loop turn.
|
|
9372
|
+
* `native` and `prompt` coexist with tool availability; `forced-tool`
|
|
9373
|
+
* pins toolChoice to the synthesized emit_result contract and therefore
|
|
9374
|
+
* cannot ride while the agent's tools must remain available. For an
|
|
9375
|
+
* agent with no tools every tier rides (the M1 behavior, unchanged).
|
|
9376
|
+
*/
|
|
9377
|
+
function canRideLoopTurn(tier, toolsAvailable) {
|
|
9378
|
+
return tier !== "forced-tool" || !toolsAvailable;
|
|
9379
|
+
}
|
|
9380
|
+
/**
|
|
9381
|
+
* The completed extract-necessity rule: a separate final structured-output
|
|
9382
|
+
* invocation fires only when a schema is set AND (routing directs extract
|
|
9383
|
+
* to a different model OR the loop model's caps cannot serve the required
|
|
9118
9384
|
* tier OR finalize is routed, in which case the schema never rides a loop
|
|
9119
9385
|
* or synthesis turn). Otherwise the schema rides the last loop turn with
|
|
9120
9386
|
* no extra call (as amended in M4-T01).
|
|
@@ -9200,1646 +9466,979 @@ function compactMessages(messages, summaryText) {
|
|
|
9200
9466
|
return head === void 0 ? [summary] : [head, summary];
|
|
9201
9467
|
}
|
|
9202
9468
|
//#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);
|
|
9469
|
+
//#region src/runtime/model-retry.ts
|
|
9470
|
+
var ModelRetry = class extends Error {
|
|
9471
|
+
data;
|
|
9472
|
+
constructor(message, opts) {
|
|
9473
|
+
super(message);
|
|
9474
|
+
this.name = "ModelRetry";
|
|
9475
|
+
if (opts?.data !== void 0) this.data = opts.data;
|
|
9220
9476
|
}
|
|
9221
|
-
|
|
9222
|
-
|
|
9477
|
+
};
|
|
9478
|
+
/** Bounded semantic retries per tool call chain. */
|
|
9479
|
+
const DEFAULT_MODEL_RETRY_ATTEMPTS = 2;
|
|
9480
|
+
//#endregion
|
|
9481
|
+
//#region src/runtime/exploration.ts
|
|
9223
9482
|
/**
|
|
9224
|
-
*
|
|
9225
|
-
*
|
|
9226
|
-
* the
|
|
9227
|
-
|
|
9228
|
-
|
|
9229
|
-
|
|
9230
|
-
|
|
9231
|
-
|
|
9232
|
-
|
|
9233
|
-
|
|
9234
|
-
|
|
9235
|
-
|
|
9236
|
-
|
|
9237
|
-
*
|
|
9238
|
-
*
|
|
9239
|
-
*
|
|
9240
|
-
* with
|
|
9483
|
+
* Exploration guards (RV-210, first slice): the engine-side counters that
|
|
9484
|
+
* make an oscillating tool loop visible and boundable. The published gap:
|
|
9485
|
+
* an agent that repeats the byte-identical tool call, or keeps receiving
|
|
9486
|
+
* pages it has already seen, burns its whole tool budget with zero signal
|
|
9487
|
+
* and dies as a bare 'limit' terminal; the no-progress detector never
|
|
9488
|
+
* trips because tool calls reset it.
|
|
9489
|
+
*
|
|
9490
|
+
* Three opt-in UsageLimits fields drive this module:
|
|
9491
|
+
*
|
|
9492
|
+
* - `maxRepeatedToolSignature`: how many times the SAME signature (tool
|
|
9493
|
+
* name + RFC 8785 canonical args) may execute per invocation. The call
|
|
9494
|
+
* that would exceed it is not dispatched; the model receives a typed
|
|
9495
|
+
* error tool result instead (visible, bounded, never terminal), and the
|
|
9496
|
+
* denial does not consume the tool budget.
|
|
9497
|
+
* - `maxNoNewEvidenceCalls`: how many consecutive successful executions
|
|
9498
|
+
* may return only already-seen result digests before the loop aborts as
|
|
9499
|
+
* status 'limit' with abortClass 'exploration' (paid partial work; the
|
|
9500
|
+
* executed results stand and the terminal memoizes like every
|
|
9501
|
+
* engine-decided abort).
|
|
9502
|
+
* - `toolBudgetNotices`: soft 50%/80% thresholds over `maxToolCalls`,
|
|
9503
|
+
* surfaced to the model as a plain user message with the exact
|
|
9504
|
+
* remaining count, so pacing is possible before the hard cap.
|
|
9505
|
+
*
|
|
9506
|
+
* Determinism: signatures and digests derive from the canonical JCS
|
|
9507
|
+
* serialization; values JCS cannot serialize never match anything (a
|
|
9508
|
+
* unique signature; a fresh-evidence result), so the guards fail open,
|
|
9509
|
+
* never spuriously. On resume the guard state is rebuilt from the
|
|
9510
|
+
* restored checkpoint messages (successful executions only, and only the
|
|
9511
|
+
* window a compaction kept), which is the same source the model itself
|
|
9512
|
+
* sees; enforcement is engine-side and live-only, while a replayed
|
|
9513
|
+
* guard abort is re-stamped from the journaled terminal like every other
|
|
9514
|
+
* abort class.
|
|
9241
9515
|
*/
|
|
9242
|
-
|
|
9243
|
-
|
|
9244
|
-
|
|
9245
|
-
|
|
9246
|
-
|
|
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;
|
|
9516
|
+
/** The docs anchor cited by guard denials and the guard abort. */
|
|
9517
|
+
const GUARD_DOCS_URL = "https://docs.rulvar.com/guide/agents#exploration-guards";
|
|
9518
|
+
/** True when any exploration guard field asks for tracking. */
|
|
9519
|
+
function explorationTrackingEnabled(limits) {
|
|
9520
|
+
return limits.maxRepeatedToolSignature !== void 0 || limits.maxNoNewEvidenceCalls !== void 0 || limits.toolBudgetNotices === true || limits.maxCallsPerTool !== void 0 || limits.toolUnits !== void 0;
|
|
9284
9521
|
}
|
|
9285
|
-
|
|
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;
|
|
9522
|
+
function digestOf$1(value) {
|
|
9331
9523
|
try {
|
|
9332
|
-
|
|
9333
|
-
} catch
|
|
9334
|
-
|
|
9335
|
-
throw thrown;
|
|
9524
|
+
return createHash("sha256").update(jcsSerialize(value), "utf8").digest("hex");
|
|
9525
|
+
} catch {
|
|
9526
|
+
return;
|
|
9336
9527
|
}
|
|
9337
|
-
|
|
9338
|
-
|
|
9339
|
-
|
|
9340
|
-
|
|
9341
|
-
|
|
9342
|
-
|
|
9343
|
-
|
|
9344
|
-
|
|
9345
|
-
|
|
9528
|
+
}
|
|
9529
|
+
var ExplorationGuard = class {
|
|
9530
|
+
config;
|
|
9531
|
+
signatureExecutions = /* @__PURE__ */ new Map();
|
|
9532
|
+
seenDigests = /* @__PURE__ */ new Set();
|
|
9533
|
+
byTool = /* @__PURE__ */ new Map();
|
|
9534
|
+
noNewEvidenceStreak = 0;
|
|
9535
|
+
executed = 0;
|
|
9536
|
+
repeated = 0;
|
|
9537
|
+
duplicateResults = 0;
|
|
9538
|
+
denied = 0;
|
|
9539
|
+
deniedToolCap = 0;
|
|
9540
|
+
unitsUsed = 0;
|
|
9541
|
+
unserializableSeq = 0;
|
|
9542
|
+
constructor(config) {
|
|
9543
|
+
this.config = config;
|
|
9346
9544
|
}
|
|
9347
|
-
|
|
9348
|
-
|
|
9349
|
-
|
|
9350
|
-
|
|
9351
|
-
|
|
9352
|
-
|
|
9353
|
-
|
|
9354
|
-
|
|
9355
|
-
|
|
9356
|
-
|
|
9357
|
-
|
|
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
|
-
});
|
|
9545
|
+
/**
|
|
9546
|
+
* The canonical signature: tool name + JCS args. Args JCS cannot
|
|
9547
|
+
* serialize get a unique per-occurrence signature, so they never
|
|
9548
|
+
* repeat and the guard fails open.
|
|
9549
|
+
*/
|
|
9550
|
+
signatureOf(name, args) {
|
|
9551
|
+
try {
|
|
9552
|
+
return `${name}\u0000${jcsSerialize(args ?? null)}`;
|
|
9553
|
+
} catch {
|
|
9554
|
+
this.unserializableSeq += 1;
|
|
9555
|
+
return `${name}\u0000<unserializable:${String(this.unserializableSeq)}>`;
|
|
9362
9556
|
}
|
|
9363
9557
|
}
|
|
9364
|
-
|
|
9365
|
-
|
|
9366
|
-
|
|
9367
|
-
|
|
9368
|
-
|
|
9369
|
-
|
|
9370
|
-
|
|
9371
|
-
|
|
9372
|
-
|
|
9373
|
-
|
|
9374
|
-
|
|
9375
|
-
|
|
9376
|
-
|
|
9377
|
-
|
|
9378
|
-
|
|
9379
|
-
|
|
9380
|
-
|
|
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;
|
|
9558
|
+
/**
|
|
9559
|
+
* Rebuilds guard state from restored checkpoint messages: assistant
|
|
9560
|
+
* tool-call parts paired with their successful tool results by id.
|
|
9561
|
+
* Error results (denials, tool failures) are skipped, so a resume
|
|
9562
|
+
* never over-counts; a compaction naturally narrows the window to
|
|
9563
|
+
* what the model itself still sees.
|
|
9564
|
+
*/
|
|
9565
|
+
restore(messages) {
|
|
9566
|
+
const callsById = /* @__PURE__ */ new Map();
|
|
9567
|
+
for (const msg of messages) for (const part of msg.parts) if (part.type === "tool-call") callsById.set(part.id, {
|
|
9568
|
+
name: part.name,
|
|
9569
|
+
args: part.args
|
|
9570
|
+
});
|
|
9571
|
+
else if (part.type === "tool-result" && part.isError !== true) {
|
|
9572
|
+
const call = callsById.get(part.id);
|
|
9573
|
+
if (call === void 0) continue;
|
|
9574
|
+
this.recordExecution(call.name, call.args, part.result, true);
|
|
9402
9575
|
}
|
|
9403
|
-
parseModelRef(gate.rung);
|
|
9404
|
-
return;
|
|
9405
9576
|
}
|
|
9406
|
-
|
|
9407
|
-
|
|
9408
|
-
|
|
9409
|
-
*
|
|
9410
|
-
*
|
|
9411
|
-
|
|
9412
|
-
|
|
9413
|
-
|
|
9414
|
-
|
|
9415
|
-
|
|
9416
|
-
|
|
9417
|
-
|
|
9418
|
-
|
|
9419
|
-
|
|
9420
|
-
|
|
9421
|
-
|
|
9422
|
-
|
|
9423
|
-
|
|
9424
|
-
|
|
9425
|
-
|
|
9426
|
-
|
|
9577
|
+
/**
|
|
9578
|
+
* The pre-dispatch verdict: denies the call that would exceed its
|
|
9579
|
+
* tool's maxCallsPerTool cap, then the call that would exceed
|
|
9580
|
+
* maxRepeatedToolSignature executions of the same signature. A denial
|
|
9581
|
+
* never consumes maxToolCalls or tool units.
|
|
9582
|
+
*/
|
|
9583
|
+
beforeExecute(name, args) {
|
|
9584
|
+
const cap = this.config.maxCallsPerTool?.[name];
|
|
9585
|
+
if (cap !== void 0) {
|
|
9586
|
+
const executions = this.byTool.get(name) ?? 0;
|
|
9587
|
+
if (executions >= cap) {
|
|
9588
|
+
this.deniedToolCap += 1;
|
|
9589
|
+
return {
|
|
9590
|
+
deny: true,
|
|
9591
|
+
guard: "per-tool-cap",
|
|
9592
|
+
executions,
|
|
9593
|
+
reason: `exploration guard: '${name}' already executed ${String(executions)} time(s) this invocation (maxCallsPerTool ${String(cap)}). Use what you have or a different tool (${GUARD_DOCS_URL}).`
|
|
9594
|
+
};
|
|
9595
|
+
}
|
|
9596
|
+
}
|
|
9597
|
+
const max = this.config.maxRepeatedToolSignature;
|
|
9598
|
+
if (max === void 0) return { deny: false };
|
|
9599
|
+
const executions = this.signatureExecutions.get(this.signatureOf(name, args)) ?? 0;
|
|
9600
|
+
if (executions < max) return { deny: false };
|
|
9601
|
+
this.denied += 1;
|
|
9427
9602
|
return {
|
|
9428
|
-
|
|
9429
|
-
|
|
9430
|
-
|
|
9431
|
-
|
|
9432
|
-
...rung.maxCostUsd === void 0 ? {} : { maxCostUsd: rung.maxCostUsd },
|
|
9433
|
-
...rung.memoizeOutcome === void 0 ? {} : { memoizeOutcome: rung.memoizeOutcome }
|
|
9603
|
+
deny: true,
|
|
9604
|
+
guard: "repeated-signature",
|
|
9605
|
+
executions,
|
|
9606
|
+
reason: `exploration guard: this exact '${name}' call already executed ${String(executions)} time(s) this invocation (maxRepeatedToolSignature ${String(max)}). Reuse the earlier result or change the arguments (${GUARD_DOCS_URL}).`
|
|
9434
9607
|
};
|
|
9435
|
-
}
|
|
9436
|
-
|
|
9437
|
-
|
|
9438
|
-
|
|
9439
|
-
|
|
9440
|
-
|
|
9441
|
-
|
|
9442
|
-
|
|
9608
|
+
}
|
|
9609
|
+
/**
|
|
9610
|
+
* Records one dispatched execution and answers whether the
|
|
9611
|
+
* no-new-evidence guard trips. Only successful results feed the
|
|
9612
|
+
* evidence chain: an error result neither resets nor lengthens it
|
|
9613
|
+
* (repeated failing calls are the signature guard's job), and a
|
|
9614
|
+
* result JCS cannot digest counts as fresh evidence.
|
|
9615
|
+
*/
|
|
9616
|
+
afterExecute(name, args, result, isError) {
|
|
9617
|
+
return this.recordExecution(name, args, result, !isError);
|
|
9618
|
+
}
|
|
9619
|
+
recordExecution(name, args, result, successful) {
|
|
9620
|
+
this.executed += 1;
|
|
9621
|
+
this.byTool.set(name, (this.byTool.get(name) ?? 0) + 1);
|
|
9622
|
+
if (this.config.toolUnits !== void 0) this.unitsUsed += this.config.toolUnits.costs?.[name] ?? 1;
|
|
9623
|
+
const signature = this.signatureOf(name, args);
|
|
9624
|
+
const prior = this.signatureExecutions.get(signature) ?? 0;
|
|
9625
|
+
if (prior > 0) this.repeated += 1;
|
|
9626
|
+
this.signatureExecutions.set(signature, prior + 1);
|
|
9627
|
+
if (!successful) return false;
|
|
9628
|
+
const digest = digestOf$1(result);
|
|
9629
|
+
if (digest === void 0 || !this.seenDigests.has(digest)) {
|
|
9630
|
+
if (digest !== void 0) this.seenDigests.add(digest);
|
|
9631
|
+
this.noNewEvidenceStreak = 0;
|
|
9632
|
+
return false;
|
|
9633
|
+
}
|
|
9634
|
+
this.duplicateResults += 1;
|
|
9635
|
+
this.noNewEvidenceStreak += 1;
|
|
9636
|
+
const max = this.config.maxNoNewEvidenceCalls;
|
|
9637
|
+
return max !== void 0 && this.noNewEvidenceStreak >= max;
|
|
9638
|
+
}
|
|
9639
|
+
/**
|
|
9640
|
+
* True once the spent tool units reached the weighted budget: the
|
|
9641
|
+
* loop's pre-dispatch check, mirroring maxToolCalls (terminal 'limit',
|
|
9642
|
+
* paid partial work). Never true without toolUnits configured.
|
|
9643
|
+
*/
|
|
9644
|
+
unitsExhausted() {
|
|
9645
|
+
return this.config.toolUnits !== void 0 && this.unitsUsed >= this.config.toolUnits.max;
|
|
9646
|
+
}
|
|
9647
|
+
/** The abort message for a tripped no-new-evidence guard. */
|
|
9648
|
+
describeTrip() {
|
|
9649
|
+
return `exploration guard: ${String(this.noNewEvidenceStreak)} consecutive tool calls returned no new evidence (maxNoNewEvidenceCalls ${String(this.config.maxNoNewEvidenceCalls ?? this.noNewEvidenceStreak)}; every result was already seen this invocation). The executed work is kept; narrow the scope, vary the queries, or raise the limit (${GUARD_DOCS_URL}).`;
|
|
9650
|
+
}
|
|
9651
|
+
/** The structured summary; `toolCallsUsed` is the loop's own counter. */
|
|
9652
|
+
summary(toolCallsUsed) {
|
|
9653
|
+
const byTool = {};
|
|
9654
|
+
for (const [name, count] of [...this.byTool.entries()].sort(([a], [b]) => a < b ? -1 : a > b ? 1 : 0)) byTool[name] = count;
|
|
9655
|
+
return {
|
|
9656
|
+
toolCallsUsed,
|
|
9657
|
+
distinctSignatures: this.signatureExecutions.size,
|
|
9658
|
+
repeatedCalls: this.repeated,
|
|
9659
|
+
duplicateResultCalls: this.duplicateResults,
|
|
9660
|
+
deniedRepeats: this.denied,
|
|
9661
|
+
byTool,
|
|
9662
|
+
...this.config.maxCallsPerTool === void 0 ? {} : { deniedToolCap: this.deniedToolCap },
|
|
9663
|
+
...this.config.toolUnits === void 0 ? {} : { toolUnitsUsed: this.unitsUsed }
|
|
9664
|
+
};
|
|
9665
|
+
}
|
|
9666
|
+
};
|
|
9667
|
+
/** The soft notice thresholds over maxToolCalls, in ascending order. */
|
|
9668
|
+
const TOOL_BUDGET_NOTICE_THRESHOLDS = [.5, .8];
|
|
9669
|
+
/**
|
|
9670
|
+
* Which notice thresholds `used` calls out of `max` have crossed
|
|
9671
|
+
* (ceil-based, so a threshold fires no earlier than its exact fraction).
|
|
9672
|
+
*/
|
|
9673
|
+
function crossedNoticeThresholds(used, max) {
|
|
9674
|
+
return TOOL_BUDGET_NOTICE_THRESHOLDS.filter((threshold) => used >= Math.ceil(threshold * max)).map((threshold) => threshold);
|
|
9443
9675
|
}
|
|
9444
9676
|
/**
|
|
9445
|
-
* The
|
|
9446
|
-
*
|
|
9447
|
-
*
|
|
9677
|
+
* The model-visible budget notice. Deterministic for a given usage
|
|
9678
|
+
* count, so a recorded conversation rebuilds byte-identically on
|
|
9679
|
+
* resume and replay.
|
|
9448
9680
|
*/
|
|
9449
|
-
function
|
|
9450
|
-
const
|
|
9451
|
-
|
|
9452
|
-
return {
|
|
9453
|
-
model: rung.model,
|
|
9454
|
-
effort: rung.effort
|
|
9455
|
-
};
|
|
9681
|
+
function toolBudgetNoticeText(used, max) {
|
|
9682
|
+
const remaining = Math.max(0, max - used);
|
|
9683
|
+
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.`;
|
|
9456
9684
|
}
|
|
9457
9685
|
//#endregion
|
|
9458
|
-
//#region src/runtime/
|
|
9686
|
+
//#region src/runtime/no-progress.ts
|
|
9459
9687
|
/**
|
|
9460
|
-
*
|
|
9688
|
+
* The no-progress abort class (M3-T08): an engine-defined detector
|
|
9689
|
+
* journaled as a first-class terminal abort distinct from user
|
|
9690
|
+
* cancellation (a cancelled entry always reruns; a no-progress abort
|
|
9691
|
+
* must replay, or every resume would re-pay the stuck turns). The
|
|
9692
|
+
* interim heuristic is committed: N consecutive
|
|
9693
|
+
* turns without tool calls or artifact deltas, N = 3; the broader
|
|
9694
|
+
* heuristic stays OQ-15, revisited on dogfood traces.
|
|
9461
9695
|
*
|
|
9462
|
-
*
|
|
9463
|
-
*
|
|
9464
|
-
*
|
|
9465
|
-
*
|
|
9466
|
-
*
|
|
9467
|
-
|
|
9468
|
-
|
|
9469
|
-
const DEFAULT_STREAM_IDLE_TIMEOUT_MS = 12e4;
|
|
9470
|
-
/**
|
|
9471
|
-
* Limits merge per spawn: AgentOpts.limits over profile limits over engine
|
|
9472
|
-
* defaults.limits.
|
|
9696
|
+
* Encoding: the abort is the agent's
|
|
9697
|
+
* terminal entry with status 'limit', an error payload carrying
|
|
9698
|
+
* abortClass 'no-progress', and memoizeOutcome stamped by the ENGINE on
|
|
9699
|
+
* the terminal entry, so the frozen memoize-limit rule replays it on
|
|
9700
|
+
* every subsequent resume without a live rerun. In M3 the runtime has no
|
|
9701
|
+
* per-turn artifact channel, so the tool-call test subsumes artifact
|
|
9702
|
+
* deltas; per-turn artifact producers arrive with M4 compaction.
|
|
9473
9703
|
*/
|
|
9474
|
-
|
|
9475
|
-
|
|
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
|
-
}
|
|
9704
|
+
/** The committed no-progress detector N. */
|
|
9705
|
+
const DEFAULT_NO_PROGRESS_TURNS = 3;
|
|
9502
9706
|
/**
|
|
9503
|
-
*
|
|
9504
|
-
*
|
|
9505
|
-
*
|
|
9506
|
-
*
|
|
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.
|
|
9707
|
+
* Counts consecutive progress-free turns. A turn with at least one tool
|
|
9708
|
+
* call (or, later, an artifact delta) resets the streak; a turn with
|
|
9709
|
+
* neither lengthens it; the detector trips when the streak reaches the
|
|
9710
|
+
* threshold AND the loop would otherwise continue.
|
|
9513
9711
|
*/
|
|
9514
|
-
|
|
9515
|
-
|
|
9516
|
-
|
|
9517
|
-
|
|
9518
|
-
|
|
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}']`);
|
|
9712
|
+
var NoProgressDetector = class {
|
|
9713
|
+
streakInternal = 0;
|
|
9714
|
+
threshold;
|
|
9715
|
+
constructor(threshold) {
|
|
9716
|
+
this.threshold = threshold ?? 3;
|
|
9528
9717
|
}
|
|
9529
|
-
|
|
9530
|
-
|
|
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
|
-
}
|
|
9718
|
+
get streak() {
|
|
9719
|
+
return this.streakInternal;
|
|
9538
9720
|
}
|
|
9539
|
-
|
|
9540
|
-
|
|
9541
|
-
if (
|
|
9542
|
-
|
|
9543
|
-
if (maxOutputTokens !== void 0) requirePositiveInteger(maxOutputTokens, `${site}.finalizationReserve.maxOutputTokens`);
|
|
9721
|
+
/** Records one completed model turn. */
|
|
9722
|
+
recordTurn(progress) {
|
|
9723
|
+
if (progress.toolCalls > 0 || (progress.artifactDeltas ?? 0) > 0) this.streakInternal = 0;
|
|
9724
|
+
else this.streakInternal += 1;
|
|
9544
9725
|
}
|
|
9545
|
-
|
|
9546
|
-
|
|
9547
|
-
|
|
9548
|
-
|
|
9549
|
-
|
|
9550
|
-
constructor(message, opts) {
|
|
9551
|
-
super(message);
|
|
9552
|
-
this.name = "ModelRetry";
|
|
9553
|
-
if (opts?.data !== void 0) this.data = opts.data;
|
|
9726
|
+
get tripped() {
|
|
9727
|
+
return this.streakInternal >= this.threshold;
|
|
9728
|
+
}
|
|
9729
|
+
describe() {
|
|
9730
|
+
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)`;
|
|
9554
9731
|
}
|
|
9555
9732
|
};
|
|
9556
|
-
/** Bounded semantic retries per tool call chain. */
|
|
9557
|
-
const DEFAULT_MODEL_RETRY_ATTEMPTS = 2;
|
|
9558
9733
|
//#endregion
|
|
9559
|
-
//#region src/runtime/
|
|
9560
|
-
|
|
9734
|
+
//#region src/runtime/structured-output.ts
|
|
9735
|
+
/** The synthesized forced-tool contract name. */
|
|
9736
|
+
const EMIT_RESULT_TOOL = "emit_result";
|
|
9561
9737
|
/**
|
|
9562
|
-
*
|
|
9563
|
-
*
|
|
9564
|
-
*
|
|
9738
|
+
* Applies the selected tier to an outgoing request. Native rides
|
|
9739
|
+
* ChatRequest.schema; forced-tool synthesizes a single emit_result tool
|
|
9740
|
+
* with toolChoice pinned to it; prompt injects the schema into the last
|
|
9741
|
+
* user message.
|
|
9565
9742
|
*/
|
|
9566
|
-
|
|
9567
|
-
|
|
9568
|
-
|
|
9569
|
-
|
|
9570
|
-
|
|
9571
|
-
|
|
9572
|
-
|
|
9573
|
-
|
|
9574
|
-
|
|
9575
|
-
|
|
9576
|
-
|
|
9577
|
-
|
|
9578
|
-
|
|
9579
|
-
|
|
9580
|
-
|
|
9581
|
-
|
|
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
|
-
}
|
|
9743
|
+
function applyStructuredOutputTier(req, tier, schema) {
|
|
9744
|
+
if (tier === "native") return {
|
|
9745
|
+
...req,
|
|
9746
|
+
schema
|
|
9747
|
+
};
|
|
9748
|
+
if (tier === "forced-tool") {
|
|
9749
|
+
const contract = {
|
|
9750
|
+
name: EMIT_RESULT_TOOL,
|
|
9751
|
+
description: "Emit the final structured result. Call exactly once with the complete answer.",
|
|
9752
|
+
parameters: schema
|
|
9753
|
+
};
|
|
9754
|
+
return {
|
|
9755
|
+
...req,
|
|
9756
|
+
tools: [...req.tools ?? [], contract],
|
|
9757
|
+
toolChoice: { name: EMIT_RESULT_TOOL }
|
|
9758
|
+
};
|
|
9674
9759
|
}
|
|
9675
|
-
|
|
9676
|
-
|
|
9677
|
-
|
|
9678
|
-
|
|
9679
|
-
|
|
9680
|
-
|
|
9681
|
-
|
|
9682
|
-
|
|
9683
|
-
|
|
9684
|
-
|
|
9685
|
-
|
|
9686
|
-
|
|
9687
|
-
|
|
9688
|
-
|
|
9689
|
-
|
|
9690
|
-
}
|
|
9760
|
+
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);
|
|
9761
|
+
const messages = [...req.messages];
|
|
9762
|
+
const last = messages[messages.length - 1];
|
|
9763
|
+
if (last !== void 0 && last.role === "user") messages[messages.length - 1] = {
|
|
9764
|
+
role: "user",
|
|
9765
|
+
parts: [...last.parts, {
|
|
9766
|
+
type: "text",
|
|
9767
|
+
text: instruction
|
|
9768
|
+
}]
|
|
9769
|
+
};
|
|
9770
|
+
else messages.push({
|
|
9771
|
+
role: "user",
|
|
9772
|
+
parts: [{
|
|
9773
|
+
type: "text",
|
|
9774
|
+
text: instruction
|
|
9775
|
+
}]
|
|
9691
9776
|
});
|
|
9692
|
-
|
|
9693
|
-
|
|
9694
|
-
|
|
9695
|
-
|
|
9696
|
-
return validation.valid ? [] : validation.issues;
|
|
9777
|
+
return {
|
|
9778
|
+
...req,
|
|
9779
|
+
messages
|
|
9780
|
+
};
|
|
9697
9781
|
}
|
|
9698
9782
|
/**
|
|
9699
|
-
*
|
|
9700
|
-
*
|
|
9701
|
-
*
|
|
9783
|
+
* Extracts the structured-output candidate from a collected turn per tier.
|
|
9784
|
+
* Returns `undefined` when the turn carries no candidate (for example the
|
|
9785
|
+
* model answered prose without the forced tool call).
|
|
9702
9786
|
*/
|
|
9703
|
-
function
|
|
9704
|
-
|
|
9787
|
+
function extractCandidate(turn, tier) {
|
|
9788
|
+
if (tier === "forced-tool") {
|
|
9789
|
+
const call = turn.toolCalls.find((c) => c.name === EMIT_RESULT_TOOL);
|
|
9790
|
+
return call === void 0 ? void 0 : { raw: call.args };
|
|
9791
|
+
}
|
|
9792
|
+
const text = turn.text.trim();
|
|
9793
|
+
if (text === "") return;
|
|
9794
|
+
try {
|
|
9795
|
+
return { raw: JSON.parse(text) };
|
|
9796
|
+
} catch {
|
|
9797
|
+
const extracted = extractFirstJsonValue(text);
|
|
9798
|
+
return extracted === void 0 ? void 0 : { raw: extracted };
|
|
9799
|
+
}
|
|
9800
|
+
}
|
|
9801
|
+
/** Best-effort extraction of the first complete JSON object or array in prose. */
|
|
9802
|
+
function extractFirstJsonValue(text) {
|
|
9803
|
+
const start = text.search(/[[{]/);
|
|
9804
|
+
if (start === -1) return;
|
|
9805
|
+
const open = text[start];
|
|
9806
|
+
const close = open === "{" ? "}" : "]";
|
|
9807
|
+
let depth = 0;
|
|
9808
|
+
let inString = false;
|
|
9809
|
+
let escaped = false;
|
|
9810
|
+
for (let i = start; i < text.length; i += 1) {
|
|
9811
|
+
const ch = text[i];
|
|
9812
|
+
if (inString) {
|
|
9813
|
+
if (escaped) escaped = false;
|
|
9814
|
+
else if (ch === "\\") escaped = true;
|
|
9815
|
+
else if (ch === "\"") inString = false;
|
|
9816
|
+
continue;
|
|
9817
|
+
}
|
|
9818
|
+
if (ch === "\"") inString = true;
|
|
9819
|
+
else if (ch === open) depth += 1;
|
|
9820
|
+
else if (ch === close) {
|
|
9821
|
+
depth -= 1;
|
|
9822
|
+
if (depth === 0) try {
|
|
9823
|
+
return JSON.parse(text.slice(start, i + 1));
|
|
9824
|
+
} catch {
|
|
9825
|
+
return;
|
|
9826
|
+
}
|
|
9827
|
+
}
|
|
9828
|
+
}
|
|
9829
|
+
}
|
|
9830
|
+
/** The bounded re-prompt message sent back to the model on a validation miss. */
|
|
9831
|
+
function formatRePrompt(issues, attempt, maxAttempts) {
|
|
9832
|
+
return {
|
|
9833
|
+
role: "user",
|
|
9834
|
+
parts: [{
|
|
9835
|
+
type: "text",
|
|
9836
|
+
text: `Your previous answer did not validate against the required schema (attempt ${attempt} of ${maxAttempts}). Issues:\n${issues.slice(0, 16).map((issue) => {
|
|
9837
|
+
const path = issue.path === void 0 || issue.path.length === 0 ? "" : ` (at ${issue.path.map((seg) => String(typeof seg === "object" ? seg.key : seg)).join(".")})`;
|
|
9838
|
+
return `- ${issue.message}${path}`;
|
|
9839
|
+
}).join("\n")}\nRespond again with ONLY a corrected JSON value that validates.`
|
|
9840
|
+
}]
|
|
9841
|
+
};
|
|
9705
9842
|
}
|
|
9706
9843
|
//#endregion
|
|
9707
|
-
//#region src/runtime/
|
|
9844
|
+
//#region src/runtime/agent-loop.ts
|
|
9708
9845
|
/**
|
|
9709
|
-
*
|
|
9710
|
-
*
|
|
9711
|
-
*
|
|
9712
|
-
*
|
|
9713
|
-
*
|
|
9714
|
-
*
|
|
9715
|
-
*
|
|
9846
|
+
* Agent runtime v1 (M1-T06): the single subagent loop shared by every
|
|
9847
|
+
* orchestration mode. A model turn; structured output in three tiers with
|
|
9848
|
+
* client validation and a bounded re-prompt; typed AgentResult; beyond the
|
|
9849
|
+
* configured policy the runtime never throws: failures become typed
|
|
9850
|
+
* AgentResult statuses. Tool dispatch, checkpoints, and compaction arrive
|
|
9851
|
+
* with M3/M4; the escalated status arrives in M3 as the flagged breaking
|
|
9852
|
+
* change.
|
|
9716
9853
|
*
|
|
9717
|
-
*
|
|
9718
|
-
*
|
|
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.
|
|
9854
|
+
* Docs: https://docs.rulvar.com/guide/agents (agent runtime binding);
|
|
9855
|
+
* https://docs.rulvar.com/guide/model-routing (roles, tiers, refusal).
|
|
9732
9856
|
*/
|
|
9733
|
-
|
|
9734
|
-
|
|
9735
|
-
|
|
9736
|
-
|
|
9737
|
-
|
|
9738
|
-
|
|
9739
|
-
|
|
9740
|
-
|
|
9741
|
-
|
|
9742
|
-
|
|
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
|
-
}
|
|
9857
|
+
function isEscalated(r) {
|
|
9858
|
+
return r.status === "escalated";
|
|
9859
|
+
}
|
|
9860
|
+
/** Reason marker distinguishing a budget-ceiling abort from host cancellation. */
|
|
9861
|
+
const BUDGET_ABORT_REASON = "rulvar:budget-ceiling";
|
|
9862
|
+
const ZERO_USAGE$1 = {
|
|
9863
|
+
inputTokens: 0,
|
|
9864
|
+
outputTokens: 0,
|
|
9865
|
+
cacheReadTokens: 0,
|
|
9866
|
+
cacheWriteTokens: 0
|
|
9753
9867
|
};
|
|
9754
|
-
|
|
9755
|
-
|
|
9756
|
-
|
|
9757
|
-
|
|
9758
|
-
|
|
9759
|
-
|
|
9760
|
-
|
|
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 }
|
|
9868
|
+
const wallRandom = Math.random.bind(globalThis);
|
|
9869
|
+
function addUsage$1(total, turn) {
|
|
9870
|
+
const sum = {
|
|
9871
|
+
inputTokens: total.inputTokens + turn.inputTokens,
|
|
9872
|
+
outputTokens: total.outputTokens + turn.outputTokens,
|
|
9873
|
+
cacheReadTokens: total.cacheReadTokens + turn.cacheReadTokens,
|
|
9874
|
+
cacheWriteTokens: total.cacheWriteTokens + turn.cacheWriteTokens
|
|
9798
9875
|
};
|
|
9799
|
-
|
|
9800
|
-
|
|
9801
|
-
|
|
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;
|
|
9876
|
+
const reasoning = (total.reasoningTokens ?? 0) + (turn.reasoningTokens ?? 0);
|
|
9877
|
+
if (reasoning > 0) sum.reasoningTokens = reasoning;
|
|
9878
|
+
return sum;
|
|
9823
9879
|
}
|
|
9824
9880
|
/**
|
|
9825
|
-
*
|
|
9826
|
-
*
|
|
9881
|
+
* The Usage invariant is verified at the adapter boundary: inputTokens is
|
|
9882
|
+
* the FULL prompt including cache reads and writes.
|
|
9827
9883
|
*/
|
|
9828
|
-
function advisoryMatches(chain, toolName) {
|
|
9829
|
-
return [...chain.deny, ...chain.ask].filter((rule) => "domains" in rule && rule.tool === toolName);
|
|
9830
|
-
}
|
|
9831
9884
|
/**
|
|
9832
|
-
*
|
|
9833
|
-
*
|
|
9885
|
+
* The full canonical invariant at the adapter boundary (v1.20.0 review
|
|
9886
|
+
* P1-1): every count finite, integral, and nonnegative, and the cache
|
|
9887
|
+
* subsets inside the input. One violation message covers every adapter,
|
|
9888
|
+
* injected clients and mocks included; the financial invariant never
|
|
9889
|
+
* depends on the good faith of an external transport.
|
|
9834
9890
|
*/
|
|
9835
|
-
function
|
|
9836
|
-
|
|
9837
|
-
|
|
9838
|
-
|
|
9839
|
-
return lexShellCommand(command).some((segment) => segment.unmatchable);
|
|
9891
|
+
function usageInvariantViolation(usage, adapterId) {
|
|
9892
|
+
const violations = usageViolations(usage);
|
|
9893
|
+
if (violations.length === 0) return;
|
|
9894
|
+
return `adapter '${adapterId}' violated the Usage invariant: ${violations.join("; ")}`;
|
|
9840
9895
|
}
|
|
9841
|
-
|
|
9842
|
-
|
|
9843
|
-
|
|
9844
|
-
|
|
9845
|
-
|
|
9846
|
-
|
|
9847
|
-
|
|
9848
|
-
|
|
9849
|
-
signal: new AbortController().signal,
|
|
9850
|
-
log: () => void 0
|
|
9896
|
+
async function streamTurn(adapter, req, options) {
|
|
9897
|
+
const idle = new AbortController();
|
|
9898
|
+
const all = [...options.signals, idle.signal];
|
|
9899
|
+
if (options.budgetSignal !== void 0) all.push(options.budgetSignal);
|
|
9900
|
+
const combined = AbortSignal.any(all);
|
|
9901
|
+
const turn = {
|
|
9902
|
+
text: "",
|
|
9903
|
+
toolCalls: []
|
|
9851
9904
|
};
|
|
9852
|
-
|
|
9853
|
-
|
|
9854
|
-
|
|
9855
|
-
|
|
9856
|
-
|
|
9857
|
-
|
|
9858
|
-
|
|
9859
|
-
|
|
9860
|
-
|
|
9861
|
-
|
|
9862
|
-
|
|
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
|
|
9905
|
+
const pendingArgs = /* @__PURE__ */ new Map();
|
|
9906
|
+
let usage = ZERO_USAGE$1;
|
|
9907
|
+
let reported = ZERO_USAGE$1;
|
|
9908
|
+
let usageViolation;
|
|
9909
|
+
let sawFinish = false;
|
|
9910
|
+
let finish;
|
|
9911
|
+
let providerMetadata;
|
|
9912
|
+
let wireError;
|
|
9913
|
+
let idleTimer;
|
|
9914
|
+
const armIdle = () => {
|
|
9915
|
+
if (idleTimer !== void 0) clearTimeout(idleTimer);
|
|
9916
|
+
idleTimer = setTimeout(() => idle.abort("rulvar:stream-idle"), options.idleTimeoutMs);
|
|
9875
9917
|
};
|
|
9876
|
-
|
|
9877
|
-
|
|
9878
|
-
const
|
|
9879
|
-
|
|
9880
|
-
|
|
9881
|
-
|
|
9882
|
-
|
|
9883
|
-
|
|
9884
|
-
|
|
9885
|
-
|
|
9886
|
-
|
|
9887
|
-
|
|
9888
|
-
|
|
9889
|
-
|
|
9890
|
-
|
|
9891
|
-
|
|
9892
|
-
|
|
9893
|
-
|
|
9894
|
-
|
|
9895
|
-
|
|
9896
|
-
|
|
9897
|
-
|
|
9898
|
-
|
|
9899
|
-
|
|
9900
|
-
|
|
9901
|
-
|
|
9902
|
-
|
|
9903
|
-
|
|
9904
|
-
|
|
9905
|
-
|
|
9906
|
-
|
|
9907
|
-
|
|
9908
|
-
|
|
9909
|
-
|
|
9910
|
-
|
|
9911
|
-
|
|
9912
|
-
|
|
9913
|
-
|
|
9914
|
-
|
|
9915
|
-
|
|
9916
|
-
|
|
9917
|
-
|
|
9918
|
-
|
|
9919
|
-
|
|
9920
|
-
|
|
9921
|
-
|
|
9922
|
-
|
|
9923
|
-
|
|
9924
|
-
|
|
9925
|
-
|
|
9926
|
-
|
|
9927
|
-
|
|
9918
|
+
try {
|
|
9919
|
+
armIdle();
|
|
9920
|
+
for await (const event of adapter.stream(req, combined)) {
|
|
9921
|
+
armIdle();
|
|
9922
|
+
switch (event.type) {
|
|
9923
|
+
case "text-delta":
|
|
9924
|
+
turn.text += event.text;
|
|
9925
|
+
options.onDelta?.(event.text);
|
|
9926
|
+
break;
|
|
9927
|
+
case "reasoning-delta":
|
|
9928
|
+
options.onDelta?.(event.text);
|
|
9929
|
+
break;
|
|
9930
|
+
case "tool-call-start":
|
|
9931
|
+
pendingArgs.set(event.id, {
|
|
9932
|
+
name: event.name,
|
|
9933
|
+
argsText: ""
|
|
9934
|
+
});
|
|
9935
|
+
break;
|
|
9936
|
+
case "tool-call-delta": {
|
|
9937
|
+
const pending = pendingArgs.get(event.id);
|
|
9938
|
+
if (pending !== void 0) pending.argsText += event.argsTextDelta;
|
|
9939
|
+
break;
|
|
9940
|
+
}
|
|
9941
|
+
case "tool-call-end": {
|
|
9942
|
+
const pending = pendingArgs.get(event.id);
|
|
9943
|
+
turn.toolCalls.push({
|
|
9944
|
+
id: event.id,
|
|
9945
|
+
name: pending?.name ?? "",
|
|
9946
|
+
args: event.args
|
|
9947
|
+
});
|
|
9948
|
+
pendingArgs.delete(event.id);
|
|
9949
|
+
break;
|
|
9950
|
+
}
|
|
9951
|
+
case "usage": {
|
|
9952
|
+
const cleaned = {};
|
|
9953
|
+
for (const field of [
|
|
9954
|
+
"inputTokens",
|
|
9955
|
+
"outputTokens",
|
|
9956
|
+
"cacheReadTokens",
|
|
9957
|
+
"cacheWriteTokens",
|
|
9958
|
+
"reasoningTokens"
|
|
9959
|
+
]) {
|
|
9960
|
+
const value = event.usage[field];
|
|
9961
|
+
if (value === void 0) continue;
|
|
9962
|
+
if (Number.isInteger(value) && value >= 0) cleaned[field] = value;
|
|
9963
|
+
else {
|
|
9964
|
+
usageViolation ??= `mid-stream usage event carried invalid ${field} (${String(value)})`;
|
|
9965
|
+
cleaned[field] = sanitizeTokenCount(value);
|
|
9966
|
+
}
|
|
9967
|
+
}
|
|
9968
|
+
usage = {
|
|
9969
|
+
...usage,
|
|
9970
|
+
...cleaned
|
|
9971
|
+
};
|
|
9972
|
+
const delta = {
|
|
9973
|
+
inputTokens: cleaned.inputTokens ?? 0,
|
|
9974
|
+
outputTokens: cleaned.outputTokens ?? 0,
|
|
9975
|
+
cacheReadTokens: cleaned.cacheReadTokens ?? 0,
|
|
9976
|
+
cacheWriteTokens: cleaned.cacheWriteTokens ?? 0
|
|
9977
|
+
};
|
|
9978
|
+
if (cleaned.reasoningTokens !== void 0) delta.reasoningTokens = cleaned.reasoningTokens;
|
|
9979
|
+
reported = addUsage$1(reported, delta);
|
|
9980
|
+
options.onUsage?.(delta);
|
|
9981
|
+
break;
|
|
9982
|
+
}
|
|
9983
|
+
case "finish":
|
|
9984
|
+
sawFinish = true;
|
|
9985
|
+
finish = event.finish;
|
|
9986
|
+
usage = event.usage;
|
|
9987
|
+
providerMetadata = event.providerMetadata;
|
|
9988
|
+
break;
|
|
9989
|
+
case "error":
|
|
9990
|
+
wireError = event.error;
|
|
9991
|
+
break;
|
|
9992
|
+
}
|
|
9993
|
+
if (sawFinish || wireError !== void 0) break;
|
|
9994
|
+
}
|
|
9995
|
+
} catch (thrown) {
|
|
9996
|
+
if (!combined.aborted) wireError = {
|
|
9997
|
+
code: "agent",
|
|
9998
|
+
message: thrown instanceof Error ? thrown.message : String(thrown),
|
|
9999
|
+
retryable: true,
|
|
10000
|
+
data: { kind: "transport" }
|
|
10001
|
+
};
|
|
10002
|
+
} finally {
|
|
10003
|
+
if (idleTimer !== void 0) clearTimeout(idleTimer);
|
|
10004
|
+
}
|
|
10005
|
+
if (combined.aborted && !sawFinish && wireError === void 0) {
|
|
10006
|
+
const aborted = options.budgetSignal?.aborted === true ? "budget" : idle.signal.aborted ? "idle" : "external";
|
|
10007
|
+
const outcome = {
|
|
10008
|
+
turn,
|
|
10009
|
+
usage,
|
|
10010
|
+
reported,
|
|
10011
|
+
usageApprox: true,
|
|
10012
|
+
aborted
|
|
10013
|
+
};
|
|
10014
|
+
if (finish !== void 0) outcome.finish = finish;
|
|
10015
|
+
if (usageViolation !== void 0) outcome.usageViolation = usageViolation;
|
|
10016
|
+
return outcome;
|
|
10017
|
+
}
|
|
10018
|
+
if (!sawFinish && wireError === void 0) wireError = {
|
|
10019
|
+
code: "agent",
|
|
10020
|
+
message: `adapter '${adapter.id}' stream ended without a terminal finish or error event; the adapter contract requires exactly one per stream, so the partial turn is discarded as a retryable transport fault`,
|
|
10021
|
+
retryable: true,
|
|
10022
|
+
data: { kind: "transport" }
|
|
10023
|
+
};
|
|
10024
|
+
const outcome = {
|
|
10025
|
+
turn,
|
|
10026
|
+
usage,
|
|
10027
|
+
reported,
|
|
10028
|
+
usageApprox: !sawFinish
|
|
10029
|
+
};
|
|
10030
|
+
if (finish !== void 0) outcome.finish = finish;
|
|
10031
|
+
if (usageViolation !== void 0) outcome.usageViolation = usageViolation;
|
|
10032
|
+
if (providerMetadata !== void 0) outcome.providerMetadata = providerMetadata;
|
|
10033
|
+
if (wireError !== void 0) outcome.wireError = wireError;
|
|
10034
|
+
return outcome;
|
|
10035
|
+
}
|
|
10036
|
+
function classifyWireError(wire) {
|
|
10037
|
+
const data = wire.data ?? {};
|
|
10038
|
+
const error = {
|
|
10039
|
+
kind: data.kind === "rate-limit" || wire.code === "rate-limit" ? "rate-limit" : data.kind ?? "transport",
|
|
10040
|
+
retryable: wire.retryable
|
|
10041
|
+
};
|
|
10042
|
+
if (typeof data.retryAfterMs === "number") error.retryAfterMs = data.retryAfterMs;
|
|
10043
|
+
return error;
|
|
10044
|
+
}
|
|
10045
|
+
function buildRequest(resolved, messages, limits, tools) {
|
|
10046
|
+
const req = {
|
|
10047
|
+
model: resolved.model,
|
|
10048
|
+
messages
|
|
10049
|
+
};
|
|
10050
|
+
if (resolved.wireEffort !== void 0) req.effort = resolved.wireEffort;
|
|
10051
|
+
if (resolved.providerOptions !== void 0) req.providerOptions = resolved.providerOptions;
|
|
10052
|
+
if (limits.maxOutputTokensPerTurn !== void 0) req.maxOutputTokens = limits.maxOutputTokensPerTurn;
|
|
10053
|
+
if (tools !== void 0 && tools.length > 0) req.tools = tools;
|
|
10054
|
+
return req;
|
|
9928
10055
|
}
|
|
9929
|
-
//#endregion
|
|
9930
|
-
//#region src/runtime/structured-output.ts
|
|
9931
|
-
/** The synthesized forced-tool contract name. */
|
|
9932
|
-
const EMIT_RESULT_TOOL = "emit_result";
|
|
9933
10056
|
/**
|
|
9934
|
-
*
|
|
9935
|
-
*
|
|
9936
|
-
*
|
|
9937
|
-
* user message.
|
|
10057
|
+
* Cheap deterministic prompt-size estimate (about four serialized
|
|
10058
|
+
* characters per token) for the layer-2b output bound. Never used for
|
|
10059
|
+
* identity, accounting, or anything the journal records.
|
|
9938
10060
|
*/
|
|
9939
|
-
function
|
|
9940
|
-
|
|
9941
|
-
|
|
9942
|
-
|
|
9943
|
-
|
|
9944
|
-
|
|
9945
|
-
|
|
9946
|
-
|
|
9947
|
-
|
|
9948
|
-
|
|
9949
|
-
|
|
10061
|
+
function estimateInputTokens(messages) {
|
|
10062
|
+
let chars = 0;
|
|
10063
|
+
for (const msg of messages) chars += JSON.stringify(msg.parts).length;
|
|
10064
|
+
return Math.ceil(chars / 4);
|
|
10065
|
+
}
|
|
10066
|
+
/**
|
|
10067
|
+
* Layer 2b at the wire boundary: clamps the outgoing request's
|
|
10068
|
+
* maxOutputTokens to what the remaining budget affords from the serving
|
|
10069
|
+
* model. The clamp uses the heuristic prompt estimate; the DENIAL does
|
|
10070
|
+
* not: a turn is refused (BudgetExhaustedError, never dispatched) only
|
|
10071
|
+
* when the remainder cannot buy even ONE output token at zero input,
|
|
10072
|
+
* which is exact. Denying on the estimate would kill turns the budget
|
|
10073
|
+
* still funds, including the DEF-7 forced finish paid from the released
|
|
10074
|
+
* finalize reserve; when the estimate says the prompt alone spends the
|
|
10075
|
+
* remainder, the turn dispatches with a one-token output floor and the
|
|
10076
|
+
* exact layers (2 and 3) settle the difference. A no-op without a hook
|
|
10077
|
+
* or when the hook reports no bound. The clamp touches only the wire
|
|
10078
|
+
* request, exactly like limits.maxOutputTokensPerTurn above it; identity
|
|
10079
|
+
* is computed at the ctx layer and never sees it.
|
|
10080
|
+
*/
|
|
10081
|
+
function applyOutputBudget(req, target, budget) {
|
|
10082
|
+
const hook = budget?.maxAffordableOutputTokens;
|
|
10083
|
+
if (hook === void 0) return req;
|
|
10084
|
+
const affordable = hook(target.resolved.ref, estimateInputTokens(req.messages));
|
|
10085
|
+
if (affordable === void 0) return req;
|
|
10086
|
+
if (affordable < 1) {
|
|
10087
|
+
const zeroInputAffordable = hook(target.resolved.ref, 0);
|
|
10088
|
+
if (zeroInputAffordable !== void 0 && zeroInputAffordable < 1) throw new BudgetExhaustedError(`the remaining budget cannot afford one output token from ${target.resolved.ref}; the turn was not dispatched`);
|
|
9950
10089
|
return {
|
|
9951
10090
|
...req,
|
|
9952
|
-
|
|
9953
|
-
toolChoice: { name: EMIT_RESULT_TOOL }
|
|
10091
|
+
maxOutputTokens: 1
|
|
9954
10092
|
};
|
|
9955
10093
|
}
|
|
9956
|
-
|
|
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 {
|
|
10094
|
+
if (req.maxOutputTokens === void 0 || affordable < req.maxOutputTokens) return {
|
|
9974
10095
|
...req,
|
|
9975
|
-
|
|
10096
|
+
maxOutputTokens: affordable
|
|
9976
10097
|
};
|
|
10098
|
+
return req;
|
|
9977
10099
|
}
|
|
9978
10100
|
/**
|
|
9979
|
-
*
|
|
9980
|
-
*
|
|
9981
|
-
*
|
|
10101
|
+
* The output-truncation abort message (v1.9.0 follow-up review). The
|
|
10102
|
+
* constraint is named neutrally as the turn's output token allowance:
|
|
10103
|
+
* the effective request cap can come from limits.maxOutputTokensPerTurn,
|
|
10104
|
+
* the budget clamp above, or the adapter's own default, and the provider
|
|
10105
|
+
* can also cut at its model maximum with no request cap at all.
|
|
9982
10106
|
*/
|
|
9983
|
-
|
|
9984
|
-
|
|
9985
|
-
|
|
9986
|
-
|
|
9987
|
-
|
|
9988
|
-
|
|
9989
|
-
|
|
9990
|
-
|
|
9991
|
-
|
|
9992
|
-
|
|
9993
|
-
|
|
9994
|
-
|
|
9995
|
-
}
|
|
10107
|
+
/**
|
|
10108
|
+
* The deterministic synthesis instruction appended (as a user message)
|
|
10109
|
+
* to the finalize REQUEST only, never to the durable transcript. A
|
|
10110
|
+
* transcript that simply ends at an assistant message reads to a real
|
|
10111
|
+
* model as a fresh conversation opening, so an uninstructed synthesis
|
|
10112
|
+
* call can replace the loop's correct answer with a greeting (v1.18.0
|
|
10113
|
+
* review P1-1); the extract arm has carried its own instruction since
|
|
10114
|
+
* M4, and this is its finalize twin. The wording is part of the wire
|
|
10115
|
+
* request: keep it stable.
|
|
10116
|
+
*/
|
|
10117
|
+
const FINALIZE_SYNTHESIS_INSTRUCTION = "Write the final answer to the original request, synthesized only from the conversation and tool results above. Do not start a new conversation and do not add greetings; respond with the final answer only.";
|
|
10118
|
+
function outputTruncatedMessage(invocation) {
|
|
10119
|
+
return `the ${invocation} ended at its output token allowance (finish reason 'max-tokens') before producing visible output; raise limits.maxOutputTokensPerTurn, reduce the reasoning effort, or free budget for the turn (https://docs.rulvar.com/guide/agents#output-truncation)`;
|
|
9996
10120
|
}
|
|
9997
|
-
/**
|
|
9998
|
-
|
|
9999
|
-
|
|
10000
|
-
|
|
10001
|
-
|
|
10002
|
-
|
|
10003
|
-
|
|
10004
|
-
|
|
10005
|
-
|
|
10006
|
-
|
|
10007
|
-
|
|
10008
|
-
|
|
10009
|
-
|
|
10010
|
-
|
|
10011
|
-
|
|
10012
|
-
|
|
10013
|
-
|
|
10014
|
-
|
|
10015
|
-
|
|
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) {
|
|
10121
|
+
/**
|
|
10122
|
+
* Builds the turn's canonical assistant message. Retained provider-raw
|
|
10123
|
+
* parts go at the HEAD: on both first-class providers the retained
|
|
10124
|
+
* blocks (thinking blocks, reasoning items) precede the turn's text and
|
|
10125
|
+
* tool calls, and head placement reproduces that order on re-projection
|
|
10126
|
+
* (M4-T02).
|
|
10127
|
+
*/
|
|
10128
|
+
function assistantMsg(turn, retained = []) {
|
|
10129
|
+
const parts = [...retained];
|
|
10130
|
+
if (turn.text !== "") parts.push({
|
|
10131
|
+
type: "text",
|
|
10132
|
+
text: turn.text
|
|
10133
|
+
});
|
|
10134
|
+
for (const call of turn.toolCalls) parts.push({
|
|
10135
|
+
type: "tool-call",
|
|
10136
|
+
id: call.id,
|
|
10137
|
+
name: call.name,
|
|
10138
|
+
args: call.args
|
|
10139
|
+
});
|
|
10028
10140
|
return {
|
|
10029
|
-
role: "
|
|
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
|
-
}]
|
|
10141
|
+
role: "assistant",
|
|
10142
|
+
parts
|
|
10037
10143
|
};
|
|
10038
10144
|
}
|
|
10039
|
-
//#endregion
|
|
10040
|
-
//#region src/runtime/exploration.ts
|
|
10041
10145
|
/**
|
|
10042
|
-
*
|
|
10043
|
-
*
|
|
10044
|
-
*
|
|
10045
|
-
*
|
|
10046
|
-
*
|
|
10047
|
-
* trips because tool calls reset it.
|
|
10048
|
-
*
|
|
10049
|
-
* Three opt-in UsageLimits fields drive this module:
|
|
10050
|
-
*
|
|
10051
|
-
* - `maxRepeatedToolSignature`: how many times the SAME signature (tool
|
|
10052
|
-
* name + RFC 8785 canonical args) may execute per invocation. The call
|
|
10053
|
-
* that would exceed it is not dispatched; the model receives a typed
|
|
10054
|
-
* error tool result instead (visible, bounded, never terminal), and the
|
|
10055
|
-
* denial does not consume the tool budget.
|
|
10056
|
-
* - `maxNoNewEvidenceCalls`: how many consecutive successful executions
|
|
10057
|
-
* may return only already-seen result digests before the loop aborts as
|
|
10058
|
-
* status 'limit' with abortClass 'exploration' (paid partial work; the
|
|
10059
|
-
* executed results stand and the terminal memoizes like every
|
|
10060
|
-
* engine-decided abort).
|
|
10061
|
-
* - `toolBudgetNotices`: soft 50%/80% thresholds over `maxToolCalls`,
|
|
10062
|
-
* surfaced to the model as a plain user message with the exact
|
|
10063
|
-
* remaining count, so pacing is possible before the hard cap.
|
|
10064
|
-
*
|
|
10065
|
-
* Determinism: signatures and digests derive from the canonical JCS
|
|
10066
|
-
* serialization; values JCS cannot serialize never match anything (a
|
|
10067
|
-
* unique signature; a fresh-evidence result), so the guards fail open,
|
|
10068
|
-
* never spuriously. On resume the guard state is rebuilt from the
|
|
10069
|
-
* restored checkpoint messages (successful executions only, and only the
|
|
10070
|
-
* window a compaction kept), which is the same source the model itself
|
|
10071
|
-
* sees; enforcement is engine-side and live-only, while a replayed
|
|
10072
|
-
* guard abort is re-stamped from the journaled terminal like every other
|
|
10073
|
-
* abort class.
|
|
10146
|
+
* Executes one model-issued tool call to a tool-result part. Failures are
|
|
10147
|
+
* surfaced to the model as error tool results and never thrown past
|
|
10148
|
+
* policy: unknown names, argument-validation issues, ModelRetry (bounded
|
|
10149
|
+
* per tool call chain), NonSerializableValueError, and arbitrary execute
|
|
10150
|
+
* throws all land as { isError: true } results.
|
|
10074
10151
|
*/
|
|
10075
|
-
|
|
10076
|
-
const
|
|
10077
|
-
|
|
10078
|
-
|
|
10079
|
-
|
|
10080
|
-
|
|
10081
|
-
|
|
10152
|
+
async function executeToolCall(options) {
|
|
10153
|
+
const { call, runtime } = options;
|
|
10154
|
+
const def = runtime.defs.find((candidate) => candidate.name === call.name);
|
|
10155
|
+
const startedAt = options.now();
|
|
10156
|
+
const finish = (result, outcome) => {
|
|
10157
|
+
options.events?.emit({
|
|
10158
|
+
type: "tool:end",
|
|
10159
|
+
toolName: call.name,
|
|
10160
|
+
outcome,
|
|
10161
|
+
durationMs: options.now() - startedAt,
|
|
10162
|
+
...options.audit
|
|
10163
|
+
});
|
|
10164
|
+
const part = {
|
|
10165
|
+
type: "tool-result",
|
|
10166
|
+
id: call.id,
|
|
10167
|
+
name: call.name,
|
|
10168
|
+
result
|
|
10169
|
+
};
|
|
10170
|
+
if (outcome !== "ok") part.isError = true;
|
|
10171
|
+
return part;
|
|
10172
|
+
};
|
|
10173
|
+
if (def === void 0) return finish({ error: `unknown tool '${call.name}'` }, "error");
|
|
10174
|
+
const validation = await validateSchemaSpec(def.parameters, call.args);
|
|
10175
|
+
if (!validation.valid) return finish({
|
|
10176
|
+
error: `arguments for '${call.name}' failed validation`,
|
|
10177
|
+
issues: validation.issues.map((issue) => issue.message)
|
|
10178
|
+
}, "error");
|
|
10082
10179
|
try {
|
|
10083
|
-
|
|
10084
|
-
|
|
10085
|
-
|
|
10086
|
-
|
|
10087
|
-
}
|
|
10088
|
-
|
|
10089
|
-
|
|
10090
|
-
|
|
10091
|
-
|
|
10092
|
-
|
|
10093
|
-
|
|
10094
|
-
|
|
10095
|
-
|
|
10096
|
-
|
|
10097
|
-
|
|
10098
|
-
|
|
10099
|
-
|
|
10100
|
-
unserializableSeq = 0;
|
|
10101
|
-
constructor(config) {
|
|
10102
|
-
this.config = config;
|
|
10103
|
-
}
|
|
10104
|
-
/**
|
|
10105
|
-
* The canonical signature: tool name + JCS args. Args JCS cannot
|
|
10106
|
-
* serialize get a unique per-occurrence signature, so they never
|
|
10107
|
-
* repeat and the guard fails open.
|
|
10108
|
-
*/
|
|
10109
|
-
signatureOf(name, args) {
|
|
10110
|
-
try {
|
|
10111
|
-
return `${name}\u0000${jcsSerialize(args ?? null)}`;
|
|
10112
|
-
} catch {
|
|
10113
|
-
this.unserializableSeq += 1;
|
|
10114
|
-
return `${name}\u0000<unserializable:${String(this.unserializableSeq)}>`;
|
|
10180
|
+
let value;
|
|
10181
|
+
if (def.executor === "inprocess") value = await def.execute(validation.value, runtime.contextFor(call.name));
|
|
10182
|
+
else if (runtime.executeExternal !== void 0) value = await runtime.executeExternal(def, validation.value, options.ordinal);
|
|
10183
|
+
else return finish({ error: `tool '${call.name}' declares executor '${def.executor}' but no executor is registered` }, "error");
|
|
10184
|
+
const serialized = toJournalValue(value === void 0 ? null : value, `tool '${call.name}'`);
|
|
10185
|
+
options.retryCounts.delete(call.name);
|
|
10186
|
+
return finish(serialized, "ok");
|
|
10187
|
+
} catch (thrown) {
|
|
10188
|
+
if (thrown instanceof ModelRetry) {
|
|
10189
|
+
const used = options.retryCounts.get(call.name) ?? 0;
|
|
10190
|
+
options.retryCounts.set(call.name, used + 1);
|
|
10191
|
+
const exhausted = used >= options.maxModelRetries;
|
|
10192
|
+
return finish({
|
|
10193
|
+
error: thrown.message,
|
|
10194
|
+
...thrown.data === void 0 ? {} : { data: thrown.data },
|
|
10195
|
+
...exhausted ? { retriesExhausted: true } : {}
|
|
10196
|
+
}, "error");
|
|
10115
10197
|
}
|
|
10198
|
+
if (thrown instanceof NonSerializableValueError) return finish({ error: thrown.message }, "error");
|
|
10199
|
+
return finish({ error: thrown instanceof Error ? thrown.message : String(thrown) }, "error");
|
|
10116
10200
|
}
|
|
10117
|
-
|
|
10118
|
-
|
|
10119
|
-
|
|
10120
|
-
|
|
10121
|
-
|
|
10122
|
-
|
|
10123
|
-
|
|
10124
|
-
|
|
10125
|
-
|
|
10126
|
-
|
|
10127
|
-
|
|
10128
|
-
|
|
10201
|
+
}
|
|
10202
|
+
/**
|
|
10203
|
+
* Runs one agent to a typed AgentResult. Never throws past policy: every
|
|
10204
|
+
* failure mode becomes a typed status on the result.
|
|
10205
|
+
*/
|
|
10206
|
+
async function runAgent(options) {
|
|
10207
|
+
const now = options.now ?? realNow;
|
|
10208
|
+
const startedAt = now();
|
|
10209
|
+
const limits = options.limits;
|
|
10210
|
+
const maxSchemaAttempts = (options.schemaRetryAttempts ?? 2) + 1;
|
|
10211
|
+
const events = options.events;
|
|
10212
|
+
const agentType = options.agentType ?? "";
|
|
10213
|
+
const messages = [{
|
|
10214
|
+
role: "user",
|
|
10215
|
+
parts: [{
|
|
10216
|
+
type: "text",
|
|
10217
|
+
text: options.prompt
|
|
10218
|
+
}]
|
|
10219
|
+
}];
|
|
10220
|
+
let totalUsage = ZERO_USAGE$1;
|
|
10221
|
+
const primaryRole = options.role ?? "loop";
|
|
10222
|
+
const usageByPhaseModel = /* @__PURE__ */ new Map();
|
|
10223
|
+
const addPhaseUsage = (role, ref, usage) => {
|
|
10224
|
+
const key = `${role}\u0000${ref}`;
|
|
10225
|
+
const prior = usageByPhaseModel.get(key);
|
|
10226
|
+
usageByPhaseModel.set(key, {
|
|
10227
|
+
role,
|
|
10228
|
+
servedBy: ref,
|
|
10229
|
+
usage: addUsage$1(prior?.usage ?? ZERO_USAGE$1, usage)
|
|
10129
10230
|
});
|
|
10130
|
-
|
|
10131
|
-
|
|
10132
|
-
|
|
10133
|
-
|
|
10134
|
-
|
|
10135
|
-
|
|
10136
|
-
|
|
10137
|
-
|
|
10138
|
-
|
|
10139
|
-
|
|
10140
|
-
|
|
10141
|
-
|
|
10142
|
-
|
|
10143
|
-
|
|
10144
|
-
|
|
10145
|
-
|
|
10146
|
-
if (executions >= cap) {
|
|
10147
|
-
this.deniedToolCap += 1;
|
|
10148
|
-
return {
|
|
10149
|
-
deny: true,
|
|
10150
|
-
guard: "per-tool-cap",
|
|
10151
|
-
executions,
|
|
10152
|
-
reason: `exploration guard: '${name}' already executed ${String(executions)} time(s) this invocation (maxCallsPerTool ${String(cap)}). Use what you have or a different tool (${GUARD_DOCS_URL}).`
|
|
10153
|
-
};
|
|
10154
|
-
}
|
|
10155
|
-
}
|
|
10156
|
-
const max = this.config.maxRepeatedToolSignature;
|
|
10157
|
-
if (max === void 0) return { deny: false };
|
|
10158
|
-
const executions = this.signatureExecutions.get(this.signatureOf(name, args)) ?? 0;
|
|
10159
|
-
if (executions < max) return { deny: false };
|
|
10160
|
-
this.denied += 1;
|
|
10161
|
-
return {
|
|
10162
|
-
deny: true,
|
|
10163
|
-
guard: "repeated-signature",
|
|
10164
|
-
executions,
|
|
10165
|
-
reason: `exploration guard: this exact '${name}' call already executed ${String(executions)} time(s) this invocation (maxRepeatedToolSignature ${String(max)}). Reuse the earlier result or change the arguments (${GUARD_DOCS_URL}).`
|
|
10231
|
+
};
|
|
10232
|
+
const providerCalls = [];
|
|
10233
|
+
let invocationCounter = 0;
|
|
10234
|
+
let transportRetries = 0;
|
|
10235
|
+
const roleUsageSnapshot = (role) => {
|
|
10236
|
+
const snapshot = /* @__PURE__ */ new Map();
|
|
10237
|
+
for (const [key, slice] of usageByPhaseModel) if (slice.role === role) snapshot.set(key, slice.usage);
|
|
10238
|
+
return snapshot;
|
|
10239
|
+
};
|
|
10240
|
+
const usageDelta = (after, before) => {
|
|
10241
|
+
const base = before ?? ZERO_USAGE$1;
|
|
10242
|
+
const delta = {
|
|
10243
|
+
inputTokens: Math.max(0, after.inputTokens - base.inputTokens),
|
|
10244
|
+
outputTokens: Math.max(0, after.outputTokens - base.outputTokens),
|
|
10245
|
+
cacheReadTokens: Math.max(0, after.cacheReadTokens - base.cacheReadTokens),
|
|
10246
|
+
cacheWriteTokens: Math.max(0, after.cacheWriteTokens - base.cacheWriteTokens)
|
|
10166
10247
|
};
|
|
10167
|
-
|
|
10248
|
+
const reasoning = (after.reasoningTokens ?? 0) - (base.reasoningTokens ?? 0);
|
|
10249
|
+
if (reasoning > 0) delta.reasoningTokens = reasoning;
|
|
10250
|
+
return delta;
|
|
10251
|
+
};
|
|
10252
|
+
const beginPhase = (role, model) => {
|
|
10253
|
+
invocationCounter += 1;
|
|
10254
|
+
events?.emit({
|
|
10255
|
+
type: "agent:phase:start",
|
|
10256
|
+
agentType,
|
|
10257
|
+
label: options.label,
|
|
10258
|
+
role,
|
|
10259
|
+
model,
|
|
10260
|
+
invocation: invocationCounter
|
|
10261
|
+
});
|
|
10262
|
+
return {
|
|
10263
|
+
invocation: invocationCounter,
|
|
10264
|
+
role,
|
|
10265
|
+
model,
|
|
10266
|
+
before: roleUsageSnapshot(role),
|
|
10267
|
+
startedAtMs: now(),
|
|
10268
|
+
retriesBefore: transportRetries
|
|
10269
|
+
};
|
|
10270
|
+
};
|
|
10271
|
+
const endPhase = (phase, outcome, servedModel) => {
|
|
10272
|
+
let phaseUsage = ZERO_USAGE$1;
|
|
10273
|
+
let phaseUsd = 0;
|
|
10274
|
+
for (const [key, slice] of usageByPhaseModel) {
|
|
10275
|
+
if (slice.role !== phase.role) continue;
|
|
10276
|
+
const delta = usageDelta(slice.usage, phase.before.get(key));
|
|
10277
|
+
phaseUsage = addUsage$1(phaseUsage, delta);
|
|
10278
|
+
const priced = options.priceUsd?.(slice.servedBy, delta) ?? 0;
|
|
10279
|
+
if (Number.isFinite(priced) && priced > 0) phaseUsd += priced;
|
|
10280
|
+
}
|
|
10281
|
+
const retries = transportRetries - phase.retriesBefore;
|
|
10282
|
+
events?.emit({
|
|
10283
|
+
type: "agent:phase:end",
|
|
10284
|
+
agentType,
|
|
10285
|
+
label: options.label,
|
|
10286
|
+
role: phase.role,
|
|
10287
|
+
model: servedModel ?? phase.model,
|
|
10288
|
+
invocation: phase.invocation,
|
|
10289
|
+
durationMs: Math.max(0, now() - phase.startedAtMs),
|
|
10290
|
+
usage: phaseUsage,
|
|
10291
|
+
costUsd: phaseUsd,
|
|
10292
|
+
outcome,
|
|
10293
|
+
...retries > 0 ? { retries } : {}
|
|
10294
|
+
});
|
|
10295
|
+
};
|
|
10296
|
+
const phaseOutcome = () => status === "error" || status === "cancelled" ? "error" : "ok";
|
|
10297
|
+
let turns = 0;
|
|
10298
|
+
let schemaAttempts = 0;
|
|
10299
|
+
let output = null;
|
|
10300
|
+
let status = "ok";
|
|
10301
|
+
let finishedViaTool = false;
|
|
10302
|
+
let agentError;
|
|
10303
|
+
let errorMessage;
|
|
10304
|
+
let usageApprox = false;
|
|
10305
|
+
let toolCallsUsed = 0;
|
|
10306
|
+
let escalationRequest;
|
|
10307
|
+
let abortClass;
|
|
10168
10308
|
/**
|
|
10169
|
-
*
|
|
10170
|
-
*
|
|
10171
|
-
*
|
|
10172
|
-
*
|
|
10173
|
-
* result JCS cannot digest counts as fresh evidence.
|
|
10309
|
+
* Set at a tool-budget expiry when limits.finalizationReserve is
|
|
10310
|
+
* configured (P1.1); the reserve turn itself runs at ONE site after
|
|
10311
|
+
* the loop ends (the pending-turn path trips before the dispatch
|
|
10312
|
+
* machinery below is even defined), inside the still-open loop phase.
|
|
10174
10313
|
*/
|
|
10175
|
-
|
|
10176
|
-
|
|
10177
|
-
|
|
10178
|
-
|
|
10179
|
-
|
|
10180
|
-
|
|
10181
|
-
|
|
10182
|
-
|
|
10183
|
-
|
|
10184
|
-
|
|
10185
|
-
|
|
10186
|
-
|
|
10187
|
-
const
|
|
10188
|
-
|
|
10189
|
-
|
|
10190
|
-
|
|
10191
|
-
|
|
10314
|
+
let reserveRequest;
|
|
10315
|
+
const noProgress = new NoProgressDetector(limits.noProgressTurns);
|
|
10316
|
+
const guard = explorationTrackingEnabled(limits) ? new ExplorationGuard(limits) : void 0;
|
|
10317
|
+
/**
|
|
10318
|
+
* The exact limiter behind a tool-budget expiry, with its counts: the
|
|
10319
|
+
* wording rides the finalization-reserve instruction and the 'limit'
|
|
10320
|
+
* terminal's errorMessage (P1.1 criterion: the terminal names the
|
|
10321
|
+
* limiter, never a bare status).
|
|
10322
|
+
*/
|
|
10323
|
+
const toolBudgetDetail = (limiter) => {
|
|
10324
|
+
if (limiter === "maxToolCalls") return `maxToolCalls (${String(toolCallsUsed)}/${String(limits.maxToolCalls ?? 0)})`;
|
|
10325
|
+
const max = limits.toolUnits?.max ?? 0;
|
|
10326
|
+
const used = guard === void 0 ? max : guard.summary(toolCallsUsed).toolUnitsUsed ?? max;
|
|
10327
|
+
return `toolUnits (${String(used)}/${String(max)})`;
|
|
10328
|
+
};
|
|
10329
|
+
if (limits.toolBudgetNotices === true && limits.maxToolCalls === void 0) events?.emit({
|
|
10330
|
+
type: "log",
|
|
10331
|
+
level: "warn",
|
|
10332
|
+
msg: "toolBudgetNotices is enabled but maxToolCalls is not set; the notices are inert"
|
|
10333
|
+
});
|
|
10334
|
+
const firedNotices = /* @__PURE__ */ new Set();
|
|
10335
|
+
/**
|
|
10336
|
+
* Pushes the soft tool-budget notice when an unfired threshold has
|
|
10337
|
+
* been crossed (one message per boundary, carrying the exact counts,
|
|
10338
|
+
* so the model can pace itself before the hard cap). The notice is an
|
|
10339
|
+
* ordinary user message: it rides checkpoints and transcripts, so a
|
|
10340
|
+
* resume never re-fires a threshold the restored count already
|
|
10341
|
+
* crossed.
|
|
10342
|
+
*/
|
|
10343
|
+
const maybePushBudgetNotice = () => {
|
|
10344
|
+
if (limits.toolBudgetNotices !== true || limits.maxToolCalls === void 0) return;
|
|
10345
|
+
const crossed = crossedNoticeThresholds(toolCallsUsed, limits.maxToolCalls).filter((threshold) => !firedNotices.has(threshold));
|
|
10346
|
+
if (crossed.length === 0) return;
|
|
10347
|
+
for (const threshold of crossed) firedNotices.add(threshold);
|
|
10348
|
+
messages.push({
|
|
10349
|
+
role: "user",
|
|
10350
|
+
parts: [{
|
|
10351
|
+
type: "text",
|
|
10352
|
+
text: toolBudgetNoticeText(toolCallsUsed, limits.maxToolCalls)
|
|
10353
|
+
}]
|
|
10354
|
+
});
|
|
10355
|
+
};
|
|
10356
|
+
const modelRetryCounts = /* @__PURE__ */ new Map();
|
|
10357
|
+
let lastTurnUsage = {
|
|
10358
|
+
inputTokens: 0,
|
|
10359
|
+
outputTokens: 0
|
|
10360
|
+
};
|
|
10361
|
+
let compactionDisabled = false;
|
|
10362
|
+
const compactionPoints = [];
|
|
10363
|
+
let servedBy = options.resolved.ref;
|
|
10364
|
+
const restored = await options.checkpoint?.load();
|
|
10365
|
+
if (restored !== void 0) {
|
|
10366
|
+
messages.length = 0;
|
|
10367
|
+
messages.push(...restored.messages);
|
|
10368
|
+
turns = restored.turns;
|
|
10369
|
+
totalUsage = usageViolations(restored.usage).length === 0 ? restored.usage : sanitizeUsage(restored.usage);
|
|
10370
|
+
toolCallsUsed = restored.toolCallsUsed;
|
|
10371
|
+
schemaAttempts = restored.schemaAttempts;
|
|
10372
|
+
compactionPoints.push(...restored.compaction);
|
|
10373
|
+
const restoredSlices = restored.usageByModel ?? [{
|
|
10374
|
+
servedBy,
|
|
10375
|
+
usage: totalUsage
|
|
10376
|
+
}];
|
|
10377
|
+
for (const slice of restoredSlices) {
|
|
10378
|
+
const sliceUsage = usageViolations(slice.usage).length === 0 ? slice.usage : sanitizeUsage(slice.usage);
|
|
10379
|
+
addPhaseUsage(slice.role ?? primaryRole, slice.servedBy, sliceUsage);
|
|
10380
|
+
options.budget?.onUsage(sliceUsage, slice.servedBy);
|
|
10192
10381
|
}
|
|
10193
|
-
|
|
10194
|
-
|
|
10195
|
-
|
|
10196
|
-
|
|
10382
|
+
for (const record of restored.providerCalls ?? []) providerCalls.push(usageViolations(record.usage).length === 0 ? record : {
|
|
10383
|
+
...record,
|
|
10384
|
+
usage: sanitizeUsage(record.usage)
|
|
10385
|
+
});
|
|
10386
|
+
guard?.restore(messages);
|
|
10387
|
+
if (limits.toolBudgetNotices === true && limits.maxToolCalls !== void 0) for (const threshold of crossedNoticeThresholds(toolCallsUsed, limits.maxToolCalls)) firedNotices.add(threshold);
|
|
10197
10388
|
}
|
|
10389
|
+
const usageSlices = () => [...usageByPhaseModel.values()].map(({ role, servedBy: sliceServedBy, usage }) => ({
|
|
10390
|
+
servedBy: sliceServedBy,
|
|
10391
|
+
usage,
|
|
10392
|
+
role
|
|
10393
|
+
}));
|
|
10198
10394
|
/**
|
|
10199
|
-
*
|
|
10200
|
-
*
|
|
10201
|
-
*
|
|
10395
|
+
* Every slice priced at ITS OWN model's rate. An unpriced model
|
|
10396
|
+
* contributes zero here and surfaces through CostReport.unpriced, never
|
|
10397
|
+
* as a silent zero.
|
|
10202
10398
|
*/
|
|
10203
|
-
|
|
10204
|
-
|
|
10205
|
-
|
|
10206
|
-
|
|
10207
|
-
|
|
10208
|
-
|
|
10209
|
-
|
|
10210
|
-
|
|
10211
|
-
|
|
10212
|
-
|
|
10213
|
-
|
|
10214
|
-
return
|
|
10399
|
+
const priceRecordedUsage = () => {
|
|
10400
|
+
const price = options.priceUsd;
|
|
10401
|
+
if (price === void 0) return 0;
|
|
10402
|
+
let usd = 0;
|
|
10403
|
+
for (const slice of usageByPhaseModel.values()) {
|
|
10404
|
+
const sliceUsd = price(slice.servedBy, slice.usage) ?? 0;
|
|
10405
|
+
if (Number.isFinite(sliceUsd) && sliceUsd > 0) usd += sliceUsd;
|
|
10406
|
+
}
|
|
10407
|
+
return usd;
|
|
10408
|
+
};
|
|
10409
|
+
const saveBoundary = async (pending) => {
|
|
10410
|
+
if (options.checkpoint === void 0) return;
|
|
10411
|
+
await options.checkpoint.save({
|
|
10412
|
+
v: 1,
|
|
10413
|
+
messages: [...messages],
|
|
10414
|
+
turns,
|
|
10415
|
+
usage: totalUsage,
|
|
10416
|
+
usageByModel: usageSlices(),
|
|
10215
10417
|
toolCallsUsed,
|
|
10216
|
-
|
|
10217
|
-
|
|
10218
|
-
|
|
10219
|
-
|
|
10220
|
-
|
|
10221
|
-
|
|
10222
|
-
|
|
10223
|
-
|
|
10224
|
-
|
|
10225
|
-
|
|
10226
|
-
|
|
10227
|
-
|
|
10228
|
-
/**
|
|
10229
|
-
*
|
|
10230
|
-
*
|
|
10231
|
-
|
|
10232
|
-
|
|
10233
|
-
|
|
10234
|
-
|
|
10235
|
-
|
|
10236
|
-
|
|
10237
|
-
|
|
10238
|
-
|
|
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.`;
|
|
10243
|
-
}
|
|
10244
|
-
//#endregion
|
|
10245
|
-
//#region src/runtime/agent-loop.ts
|
|
10246
|
-
/**
|
|
10247
|
-
* Agent runtime v1 (M1-T06): the single subagent loop shared by every
|
|
10248
|
-
* orchestration mode. A model turn; structured output in three tiers with
|
|
10249
|
-
* client validation and a bounded re-prompt; typed AgentResult; beyond the
|
|
10250
|
-
* configured policy the runtime never throws: failures become typed
|
|
10251
|
-
* AgentResult statuses. Tool dispatch, checkpoints, and compaction arrive
|
|
10252
|
-
* with M3/M4; the escalated status arrives in M3 as the flagged breaking
|
|
10253
|
-
* change.
|
|
10254
|
-
*
|
|
10255
|
-
* Docs: https://docs.rulvar.com/guide/agents (agent runtime binding);
|
|
10256
|
-
* https://docs.rulvar.com/guide/model-routing (roles, tiers, refusal).
|
|
10257
|
-
*/
|
|
10258
|
-
function isEscalated(r) {
|
|
10259
|
-
return r.status === "escalated";
|
|
10260
|
-
}
|
|
10261
|
-
/** Reason marker distinguishing a budget-ceiling abort from host cancellation. */
|
|
10262
|
-
const BUDGET_ABORT_REASON = "rulvar:budget-ceiling";
|
|
10263
|
-
const ZERO_USAGE$1 = {
|
|
10264
|
-
inputTokens: 0,
|
|
10265
|
-
outputTokens: 0,
|
|
10266
|
-
cacheReadTokens: 0,
|
|
10267
|
-
cacheWriteTokens: 0
|
|
10268
|
-
};
|
|
10269
|
-
const wallRandom = Math.random.bind(globalThis);
|
|
10270
|
-
function addUsage$1(total, turn) {
|
|
10271
|
-
const sum = {
|
|
10272
|
-
inputTokens: total.inputTokens + turn.inputTokens,
|
|
10273
|
-
outputTokens: total.outputTokens + turn.outputTokens,
|
|
10274
|
-
cacheReadTokens: total.cacheReadTokens + turn.cacheReadTokens,
|
|
10275
|
-
cacheWriteTokens: total.cacheWriteTokens + turn.cacheWriteTokens
|
|
10276
|
-
};
|
|
10277
|
-
const reasoning = (total.reasoningTokens ?? 0) + (turn.reasoningTokens ?? 0);
|
|
10278
|
-
if (reasoning > 0) sum.reasoningTokens = reasoning;
|
|
10279
|
-
return sum;
|
|
10280
|
-
}
|
|
10281
|
-
/**
|
|
10282
|
-
* The Usage invariant is verified at the adapter boundary: inputTokens is
|
|
10283
|
-
* the FULL prompt including cache reads and writes.
|
|
10284
|
-
*/
|
|
10285
|
-
/**
|
|
10286
|
-
* The full canonical invariant at the adapter boundary (v1.20.0 review
|
|
10287
|
-
* P1-1): every count finite, integral, and nonnegative, and the cache
|
|
10288
|
-
* subsets inside the input. One violation message covers every adapter,
|
|
10289
|
-
* injected clients and mocks included; the financial invariant never
|
|
10290
|
-
* depends on the good faith of an external transport.
|
|
10291
|
-
*/
|
|
10292
|
-
function usageInvariantViolation(usage, adapterId) {
|
|
10293
|
-
const violations = usageViolations(usage);
|
|
10294
|
-
if (violations.length === 0) return;
|
|
10295
|
-
return `adapter '${adapterId}' violated the Usage invariant: ${violations.join("; ")}`;
|
|
10296
|
-
}
|
|
10297
|
-
async function streamTurn(adapter, req, options) {
|
|
10298
|
-
const idle = new AbortController();
|
|
10299
|
-
const all = [...options.signals, idle.signal];
|
|
10300
|
-
if (options.budgetSignal !== void 0) all.push(options.budgetSignal);
|
|
10301
|
-
const combined = AbortSignal.any(all);
|
|
10302
|
-
const turn = {
|
|
10303
|
-
text: "",
|
|
10304
|
-
toolCalls: []
|
|
10305
|
-
};
|
|
10306
|
-
const pendingArgs = /* @__PURE__ */ new Map();
|
|
10307
|
-
let usage = ZERO_USAGE$1;
|
|
10308
|
-
let reported = ZERO_USAGE$1;
|
|
10309
|
-
let usageViolation;
|
|
10310
|
-
let sawFinish = false;
|
|
10311
|
-
let finish;
|
|
10312
|
-
let providerMetadata;
|
|
10313
|
-
let wireError;
|
|
10314
|
-
let idleTimer;
|
|
10315
|
-
const armIdle = () => {
|
|
10316
|
-
if (idleTimer !== void 0) clearTimeout(idleTimer);
|
|
10317
|
-
idleTimer = setTimeout(() => idle.abort("rulvar:stream-idle"), options.idleTimeoutMs);
|
|
10318
|
-
};
|
|
10319
|
-
try {
|
|
10320
|
-
armIdle();
|
|
10321
|
-
for await (const event of adapter.stream(req, combined)) {
|
|
10322
|
-
armIdle();
|
|
10323
|
-
switch (event.type) {
|
|
10324
|
-
case "text-delta":
|
|
10325
|
-
turn.text += event.text;
|
|
10326
|
-
options.onDelta?.(event.text);
|
|
10327
|
-
break;
|
|
10328
|
-
case "reasoning-delta":
|
|
10329
|
-
options.onDelta?.(event.text);
|
|
10330
|
-
break;
|
|
10331
|
-
case "tool-call-start":
|
|
10332
|
-
pendingArgs.set(event.id, {
|
|
10333
|
-
name: event.name,
|
|
10334
|
-
argsText: ""
|
|
10335
|
-
});
|
|
10336
|
-
break;
|
|
10337
|
-
case "tool-call-delta": {
|
|
10338
|
-
const pending = pendingArgs.get(event.id);
|
|
10339
|
-
if (pending !== void 0) pending.argsText += event.argsTextDelta;
|
|
10340
|
-
break;
|
|
10341
|
-
}
|
|
10342
|
-
case "tool-call-end": {
|
|
10343
|
-
const pending = pendingArgs.get(event.id);
|
|
10344
|
-
turn.toolCalls.push({
|
|
10345
|
-
id: event.id,
|
|
10346
|
-
name: pending?.name ?? "",
|
|
10347
|
-
args: event.args
|
|
10348
|
-
});
|
|
10349
|
-
pendingArgs.delete(event.id);
|
|
10350
|
-
break;
|
|
10351
|
-
}
|
|
10352
|
-
case "usage": {
|
|
10353
|
-
const cleaned = {};
|
|
10354
|
-
for (const field of [
|
|
10355
|
-
"inputTokens",
|
|
10356
|
-
"outputTokens",
|
|
10357
|
-
"cacheReadTokens",
|
|
10358
|
-
"cacheWriteTokens",
|
|
10359
|
-
"reasoningTokens"
|
|
10360
|
-
]) {
|
|
10361
|
-
const value = event.usage[field];
|
|
10362
|
-
if (value === void 0) continue;
|
|
10363
|
-
if (Number.isInteger(value) && value >= 0) cleaned[field] = value;
|
|
10364
|
-
else {
|
|
10365
|
-
usageViolation ??= `mid-stream usage event carried invalid ${field} (${String(value)})`;
|
|
10366
|
-
cleaned[field] = sanitizeTokenCount(value);
|
|
10367
|
-
}
|
|
10368
|
-
}
|
|
10369
|
-
usage = {
|
|
10370
|
-
...usage,
|
|
10371
|
-
...cleaned
|
|
10372
|
-
};
|
|
10373
|
-
const delta = {
|
|
10374
|
-
inputTokens: cleaned.inputTokens ?? 0,
|
|
10375
|
-
outputTokens: cleaned.outputTokens ?? 0,
|
|
10376
|
-
cacheReadTokens: cleaned.cacheReadTokens ?? 0,
|
|
10377
|
-
cacheWriteTokens: cleaned.cacheWriteTokens ?? 0
|
|
10378
|
-
};
|
|
10379
|
-
if (cleaned.reasoningTokens !== void 0) delta.reasoningTokens = cleaned.reasoningTokens;
|
|
10380
|
-
reported = addUsage$1(reported, delta);
|
|
10381
|
-
options.onUsage?.(delta);
|
|
10382
|
-
break;
|
|
10383
|
-
}
|
|
10384
|
-
case "finish":
|
|
10385
|
-
sawFinish = true;
|
|
10386
|
-
finish = event.finish;
|
|
10387
|
-
usage = event.usage;
|
|
10388
|
-
providerMetadata = event.providerMetadata;
|
|
10389
|
-
break;
|
|
10390
|
-
case "error":
|
|
10391
|
-
wireError = event.error;
|
|
10392
|
-
break;
|
|
10393
|
-
}
|
|
10394
|
-
if (sawFinish || wireError !== void 0) break;
|
|
10395
|
-
}
|
|
10396
|
-
} catch (thrown) {
|
|
10397
|
-
if (!combined.aborted) wireError = {
|
|
10398
|
-
code: "agent",
|
|
10399
|
-
message: thrown instanceof Error ? thrown.message : String(thrown),
|
|
10400
|
-
retryable: true,
|
|
10401
|
-
data: { kind: "transport" }
|
|
10402
|
-
};
|
|
10403
|
-
} finally {
|
|
10404
|
-
if (idleTimer !== void 0) clearTimeout(idleTimer);
|
|
10405
|
-
}
|
|
10406
|
-
if (combined.aborted && !sawFinish && wireError === void 0) {
|
|
10407
|
-
const aborted = options.budgetSignal?.aborted === true ? "budget" : idle.signal.aborted ? "idle" : "external";
|
|
10408
|
-
const outcome = {
|
|
10409
|
-
turn,
|
|
10410
|
-
usage,
|
|
10411
|
-
reported,
|
|
10412
|
-
usageApprox: true,
|
|
10413
|
-
aborted
|
|
10414
|
-
};
|
|
10415
|
-
if (finish !== void 0) outcome.finish = finish;
|
|
10416
|
-
if (usageViolation !== void 0) outcome.usageViolation = usageViolation;
|
|
10417
|
-
return outcome;
|
|
10418
|
-
}
|
|
10419
|
-
if (!sawFinish && wireError === void 0) wireError = {
|
|
10420
|
-
code: "agent",
|
|
10421
|
-
message: `adapter '${adapter.id}' stream ended without a terminal finish or error event; the adapter contract requires exactly one per stream, so the partial turn is discarded as a retryable transport fault`,
|
|
10422
|
-
retryable: true,
|
|
10423
|
-
data: { kind: "transport" }
|
|
10424
|
-
};
|
|
10425
|
-
const outcome = {
|
|
10426
|
-
turn,
|
|
10427
|
-
usage,
|
|
10428
|
-
reported,
|
|
10429
|
-
usageApprox: !sawFinish
|
|
10430
|
-
};
|
|
10431
|
-
if (finish !== void 0) outcome.finish = finish;
|
|
10432
|
-
if (usageViolation !== void 0) outcome.usageViolation = usageViolation;
|
|
10433
|
-
if (providerMetadata !== void 0) outcome.providerMetadata = providerMetadata;
|
|
10434
|
-
if (wireError !== void 0) outcome.wireError = wireError;
|
|
10435
|
-
return outcome;
|
|
10436
|
-
}
|
|
10437
|
-
function classifyWireError(wire) {
|
|
10438
|
-
const data = wire.data ?? {};
|
|
10439
|
-
const error = {
|
|
10440
|
-
kind: data.kind === "rate-limit" || wire.code === "rate-limit" ? "rate-limit" : data.kind ?? "transport",
|
|
10441
|
-
retryable: wire.retryable
|
|
10442
|
-
};
|
|
10443
|
-
if (typeof data.retryAfterMs === "number") error.retryAfterMs = data.retryAfterMs;
|
|
10444
|
-
return error;
|
|
10445
|
-
}
|
|
10446
|
-
function buildRequest(resolved, messages, limits, tools) {
|
|
10447
|
-
const req = {
|
|
10448
|
-
model: resolved.model,
|
|
10449
|
-
messages
|
|
10450
|
-
};
|
|
10451
|
-
if (resolved.wireEffort !== void 0) req.effort = resolved.wireEffort;
|
|
10452
|
-
if (resolved.providerOptions !== void 0) req.providerOptions = resolved.providerOptions;
|
|
10453
|
-
if (limits.maxOutputTokensPerTurn !== void 0) req.maxOutputTokens = limits.maxOutputTokensPerTurn;
|
|
10454
|
-
if (tools !== void 0 && tools.length > 0) req.tools = tools;
|
|
10455
|
-
return req;
|
|
10456
|
-
}
|
|
10457
|
-
/**
|
|
10458
|
-
* Cheap deterministic prompt-size estimate (about four serialized
|
|
10459
|
-
* characters per token) for the layer-2b output bound. Never used for
|
|
10460
|
-
* identity, accounting, or anything the journal records.
|
|
10461
|
-
*/
|
|
10462
|
-
function estimateInputTokens(messages) {
|
|
10463
|
-
let chars = 0;
|
|
10464
|
-
for (const msg of messages) chars += JSON.stringify(msg.parts).length;
|
|
10465
|
-
return Math.ceil(chars / 4);
|
|
10466
|
-
}
|
|
10467
|
-
/**
|
|
10468
|
-
* Layer 2b at the wire boundary: clamps the outgoing request's
|
|
10469
|
-
* maxOutputTokens to what the remaining budget affords from the serving
|
|
10470
|
-
* model. The clamp uses the heuristic prompt estimate; the DENIAL does
|
|
10471
|
-
* not: a turn is refused (BudgetExhaustedError, never dispatched) only
|
|
10472
|
-
* when the remainder cannot buy even ONE output token at zero input,
|
|
10473
|
-
* which is exact. Denying on the estimate would kill turns the budget
|
|
10474
|
-
* still funds, including the DEF-7 forced finish paid from the released
|
|
10475
|
-
* finalize reserve; when the estimate says the prompt alone spends the
|
|
10476
|
-
* remainder, the turn dispatches with a one-token output floor and the
|
|
10477
|
-
* exact layers (2 and 3) settle the difference. A no-op without a hook
|
|
10478
|
-
* or when the hook reports no bound. The clamp touches only the wire
|
|
10479
|
-
* request, exactly like limits.maxOutputTokensPerTurn above it; identity
|
|
10480
|
-
* is computed at the ctx layer and never sees it.
|
|
10481
|
-
*/
|
|
10482
|
-
function applyOutputBudget(req, target, budget) {
|
|
10483
|
-
const hook = budget?.maxAffordableOutputTokens;
|
|
10484
|
-
if (hook === void 0) return req;
|
|
10485
|
-
const affordable = hook(target.resolved.ref, estimateInputTokens(req.messages));
|
|
10486
|
-
if (affordable === void 0) return req;
|
|
10487
|
-
if (affordable < 1) {
|
|
10488
|
-
const zeroInputAffordable = hook(target.resolved.ref, 0);
|
|
10489
|
-
if (zeroInputAffordable !== void 0 && zeroInputAffordable < 1) throw new BudgetExhaustedError(`the remaining budget cannot afford one output token from ${target.resolved.ref}; the turn was not dispatched`);
|
|
10490
|
-
return {
|
|
10491
|
-
...req,
|
|
10492
|
-
maxOutputTokens: 1
|
|
10493
|
-
};
|
|
10494
|
-
}
|
|
10495
|
-
if (req.maxOutputTokens === void 0 || affordable < req.maxOutputTokens) return {
|
|
10496
|
-
...req,
|
|
10497
|
-
maxOutputTokens: affordable
|
|
10498
|
-
};
|
|
10499
|
-
return req;
|
|
10500
|
-
}
|
|
10501
|
-
/**
|
|
10502
|
-
* The output-truncation abort message (v1.9.0 follow-up review). The
|
|
10503
|
-
* constraint is named neutrally as the turn's output token allowance:
|
|
10504
|
-
* the effective request cap can come from limits.maxOutputTokensPerTurn,
|
|
10505
|
-
* the budget clamp above, or the adapter's own default, and the provider
|
|
10506
|
-
* can also cut at its model maximum with no request cap at all.
|
|
10507
|
-
*/
|
|
10508
|
-
/**
|
|
10509
|
-
* The deterministic synthesis instruction appended (as a user message)
|
|
10510
|
-
* to the finalize REQUEST only, never to the durable transcript. A
|
|
10511
|
-
* transcript that simply ends at an assistant message reads to a real
|
|
10512
|
-
* model as a fresh conversation opening, so an uninstructed synthesis
|
|
10513
|
-
* call can replace the loop's correct answer with a greeting (v1.18.0
|
|
10514
|
-
* review P1-1); the extract arm has carried its own instruction since
|
|
10515
|
-
* M4, and this is its finalize twin. The wording is part of the wire
|
|
10516
|
-
* request: keep it stable.
|
|
10517
|
-
*/
|
|
10518
|
-
const FINALIZE_SYNTHESIS_INSTRUCTION = "Write the final answer to the original request, synthesized only from the conversation and tool results above. Do not start a new conversation and do not add greetings; respond with the final answer only.";
|
|
10519
|
-
function outputTruncatedMessage(invocation) {
|
|
10520
|
-
return `the ${invocation} ended at its output token allowance (finish reason 'max-tokens') before producing visible output; raise limits.maxOutputTokensPerTurn, reduce the reasoning effort, or free budget for the turn (https://docs.rulvar.com/guide/agents#output-truncation)`;
|
|
10521
|
-
}
|
|
10522
|
-
/**
|
|
10523
|
-
* Builds the turn's canonical assistant message. Retained provider-raw
|
|
10524
|
-
* parts go at the HEAD: on both first-class providers the retained
|
|
10525
|
-
* blocks (thinking blocks, reasoning items) precede the turn's text and
|
|
10526
|
-
* tool calls, and head placement reproduces that order on re-projection
|
|
10527
|
-
* (M4-T02).
|
|
10528
|
-
*/
|
|
10529
|
-
function assistantMsg(turn, retained = []) {
|
|
10530
|
-
const parts = [...retained];
|
|
10531
|
-
if (turn.text !== "") parts.push({
|
|
10532
|
-
type: "text",
|
|
10533
|
-
text: turn.text
|
|
10534
|
-
});
|
|
10535
|
-
for (const call of turn.toolCalls) parts.push({
|
|
10536
|
-
type: "tool-call",
|
|
10537
|
-
id: call.id,
|
|
10538
|
-
name: call.name,
|
|
10539
|
-
args: call.args
|
|
10540
|
-
});
|
|
10541
|
-
return {
|
|
10542
|
-
role: "assistant",
|
|
10543
|
-
parts
|
|
10544
|
-
};
|
|
10545
|
-
}
|
|
10546
|
-
/**
|
|
10547
|
-
* Executes one model-issued tool call to a tool-result part. Failures are
|
|
10548
|
-
* surfaced to the model as error tool results and never thrown past
|
|
10549
|
-
* policy: unknown names, argument-validation issues, ModelRetry (bounded
|
|
10550
|
-
* per tool call chain), NonSerializableValueError, and arbitrary execute
|
|
10551
|
-
* throws all land as { isError: true } results.
|
|
10552
|
-
*/
|
|
10553
|
-
async function executeToolCall(options) {
|
|
10554
|
-
const { call, runtime } = options;
|
|
10555
|
-
const def = runtime.defs.find((candidate) => candidate.name === call.name);
|
|
10556
|
-
const startedAt = options.now();
|
|
10557
|
-
const finish = (result, outcome) => {
|
|
10558
|
-
options.events?.emit({
|
|
10559
|
-
type: "tool:end",
|
|
10560
|
-
toolName: call.name,
|
|
10561
|
-
outcome,
|
|
10562
|
-
durationMs: options.now() - startedAt,
|
|
10563
|
-
...options.audit
|
|
10564
|
-
});
|
|
10565
|
-
const part = {
|
|
10566
|
-
type: "tool-result",
|
|
10567
|
-
id: call.id,
|
|
10568
|
-
name: call.name,
|
|
10569
|
-
result
|
|
10570
|
-
};
|
|
10571
|
-
if (outcome !== "ok") part.isError = true;
|
|
10572
|
-
return part;
|
|
10573
|
-
};
|
|
10574
|
-
if (def === void 0) return finish({ error: `unknown tool '${call.name}'` }, "error");
|
|
10575
|
-
const validation = await validateSchemaSpec(def.parameters, call.args);
|
|
10576
|
-
if (!validation.valid) return finish({
|
|
10577
|
-
error: `arguments for '${call.name}' failed validation`,
|
|
10578
|
-
issues: validation.issues.map((issue) => issue.message)
|
|
10579
|
-
}, "error");
|
|
10580
|
-
try {
|
|
10581
|
-
let value;
|
|
10582
|
-
if (def.executor === "inprocess") value = await def.execute(validation.value, runtime.contextFor(call.name));
|
|
10583
|
-
else if (runtime.executeExternal !== void 0) value = await runtime.executeExternal(def, validation.value, options.ordinal);
|
|
10584
|
-
else return finish({ error: `tool '${call.name}' declares executor '${def.executor}' but no executor is registered` }, "error");
|
|
10585
|
-
const serialized = toJournalValue(value === void 0 ? null : value, `tool '${call.name}'`);
|
|
10586
|
-
options.retryCounts.delete(call.name);
|
|
10587
|
-
return finish(serialized, "ok");
|
|
10588
|
-
} catch (thrown) {
|
|
10589
|
-
if (thrown instanceof ModelRetry) {
|
|
10590
|
-
const used = options.retryCounts.get(call.name) ?? 0;
|
|
10591
|
-
options.retryCounts.set(call.name, used + 1);
|
|
10592
|
-
const exhausted = used >= options.maxModelRetries;
|
|
10593
|
-
return finish({
|
|
10594
|
-
error: thrown.message,
|
|
10595
|
-
...thrown.data === void 0 ? {} : { data: thrown.data },
|
|
10596
|
-
...exhausted ? { retriesExhausted: true } : {}
|
|
10597
|
-
}, "error");
|
|
10598
|
-
}
|
|
10599
|
-
if (thrown instanceof NonSerializableValueError) return finish({ error: thrown.message }, "error");
|
|
10600
|
-
return finish({ error: thrown instanceof Error ? thrown.message : String(thrown) }, "error");
|
|
10601
|
-
}
|
|
10602
|
-
}
|
|
10603
|
-
/**
|
|
10604
|
-
* Runs one agent to a typed AgentResult. Never throws past policy: every
|
|
10605
|
-
* failure mode becomes a typed status on the result.
|
|
10606
|
-
*/
|
|
10607
|
-
async function runAgent(options) {
|
|
10608
|
-
const now = options.now ?? realNow;
|
|
10609
|
-
const startedAt = now();
|
|
10610
|
-
const limits = options.limits;
|
|
10611
|
-
const maxSchemaAttempts = (options.schemaRetryAttempts ?? 2) + 1;
|
|
10612
|
-
const events = options.events;
|
|
10613
|
-
const agentType = options.agentType ?? "";
|
|
10614
|
-
const messages = [{
|
|
10615
|
-
role: "user",
|
|
10616
|
-
parts: [{
|
|
10617
|
-
type: "text",
|
|
10618
|
-
text: options.prompt
|
|
10619
|
-
}]
|
|
10620
|
-
}];
|
|
10621
|
-
let totalUsage = ZERO_USAGE$1;
|
|
10622
|
-
const primaryRole = options.role ?? "loop";
|
|
10623
|
-
const usageByPhaseModel = /* @__PURE__ */ new Map();
|
|
10624
|
-
const addPhaseUsage = (role, ref, usage) => {
|
|
10625
|
-
const key = `${role}\u0000${ref}`;
|
|
10626
|
-
const prior = usageByPhaseModel.get(key);
|
|
10627
|
-
usageByPhaseModel.set(key, {
|
|
10628
|
-
role,
|
|
10629
|
-
servedBy: ref,
|
|
10630
|
-
usage: addUsage$1(prior?.usage ?? ZERO_USAGE$1, usage)
|
|
10631
|
-
});
|
|
10632
|
-
};
|
|
10633
|
-
const providerCalls = [];
|
|
10634
|
-
let invocationCounter = 0;
|
|
10635
|
-
let transportRetries = 0;
|
|
10636
|
-
const roleUsageSnapshot = (role) => {
|
|
10637
|
-
const snapshot = /* @__PURE__ */ new Map();
|
|
10638
|
-
for (const [key, slice] of usageByPhaseModel) if (slice.role === role) snapshot.set(key, slice.usage);
|
|
10639
|
-
return snapshot;
|
|
10640
|
-
};
|
|
10641
|
-
const usageDelta = (after, before) => {
|
|
10642
|
-
const base = before ?? ZERO_USAGE$1;
|
|
10643
|
-
const delta = {
|
|
10644
|
-
inputTokens: Math.max(0, after.inputTokens - base.inputTokens),
|
|
10645
|
-
outputTokens: Math.max(0, after.outputTokens - base.outputTokens),
|
|
10646
|
-
cacheReadTokens: Math.max(0, after.cacheReadTokens - base.cacheReadTokens),
|
|
10647
|
-
cacheWriteTokens: Math.max(0, after.cacheWriteTokens - base.cacheWriteTokens)
|
|
10648
|
-
};
|
|
10649
|
-
const reasoning = (after.reasoningTokens ?? 0) - (base.reasoningTokens ?? 0);
|
|
10650
|
-
if (reasoning > 0) delta.reasoningTokens = reasoning;
|
|
10651
|
-
return delta;
|
|
10652
|
-
};
|
|
10653
|
-
const beginPhase = (role, model) => {
|
|
10654
|
-
invocationCounter += 1;
|
|
10655
|
-
events?.emit({
|
|
10656
|
-
type: "agent:phase:start",
|
|
10657
|
-
agentType,
|
|
10658
|
-
label: options.label,
|
|
10659
|
-
role,
|
|
10660
|
-
model,
|
|
10661
|
-
invocation: invocationCounter
|
|
10662
|
-
});
|
|
10663
|
-
return {
|
|
10664
|
-
invocation: invocationCounter,
|
|
10665
|
-
role,
|
|
10666
|
-
model,
|
|
10667
|
-
before: roleUsageSnapshot(role),
|
|
10668
|
-
startedAtMs: now(),
|
|
10669
|
-
retriesBefore: transportRetries
|
|
10670
|
-
};
|
|
10671
|
-
};
|
|
10672
|
-
const endPhase = (phase, outcome, servedModel) => {
|
|
10673
|
-
let phaseUsage = ZERO_USAGE$1;
|
|
10674
|
-
let phaseUsd = 0;
|
|
10675
|
-
for (const [key, slice] of usageByPhaseModel) {
|
|
10676
|
-
if (slice.role !== phase.role) continue;
|
|
10677
|
-
const delta = usageDelta(slice.usage, phase.before.get(key));
|
|
10678
|
-
phaseUsage = addUsage$1(phaseUsage, delta);
|
|
10679
|
-
const priced = options.priceUsd?.(slice.servedBy, delta) ?? 0;
|
|
10680
|
-
if (Number.isFinite(priced) && priced > 0) phaseUsd += priced;
|
|
10681
|
-
}
|
|
10682
|
-
const retries = transportRetries - phase.retriesBefore;
|
|
10683
|
-
events?.emit({
|
|
10684
|
-
type: "agent:phase:end",
|
|
10685
|
-
agentType,
|
|
10686
|
-
label: options.label,
|
|
10687
|
-
role: phase.role,
|
|
10688
|
-
model: servedModel ?? phase.model,
|
|
10689
|
-
invocation: phase.invocation,
|
|
10690
|
-
durationMs: Math.max(0, now() - phase.startedAtMs),
|
|
10691
|
-
usage: phaseUsage,
|
|
10692
|
-
costUsd: phaseUsd,
|
|
10693
|
-
outcome,
|
|
10694
|
-
...retries > 0 ? { retries } : {}
|
|
10695
|
-
});
|
|
10696
|
-
};
|
|
10697
|
-
const phaseOutcome = () => status === "error" || status === "cancelled" ? "error" : "ok";
|
|
10698
|
-
let turns = 0;
|
|
10699
|
-
let schemaAttempts = 0;
|
|
10700
|
-
let output = null;
|
|
10701
|
-
let status = "ok";
|
|
10702
|
-
let finishedViaTool = false;
|
|
10703
|
-
let agentError;
|
|
10704
|
-
let errorMessage;
|
|
10705
|
-
let usageApprox = false;
|
|
10706
|
-
let toolCallsUsed = 0;
|
|
10707
|
-
let escalationRequest;
|
|
10708
|
-
let abortClass;
|
|
10709
|
-
/**
|
|
10710
|
-
* Set at a tool-budget expiry when limits.finalizationReserve is
|
|
10711
|
-
* configured (P1.1); the reserve turn itself runs at ONE site after
|
|
10712
|
-
* the loop ends (the pending-turn path trips before the dispatch
|
|
10713
|
-
* machinery below is even defined), inside the still-open loop phase.
|
|
10714
|
-
*/
|
|
10715
|
-
let reserveRequest;
|
|
10716
|
-
const noProgress = new NoProgressDetector(limits.noProgressTurns);
|
|
10717
|
-
const guard = explorationTrackingEnabled(limits) ? new ExplorationGuard(limits) : void 0;
|
|
10718
|
-
/**
|
|
10719
|
-
* The exact limiter behind a tool-budget expiry, with its counts: the
|
|
10720
|
-
* wording rides the finalization-reserve instruction and the 'limit'
|
|
10721
|
-
* terminal's errorMessage (P1.1 criterion: the terminal names the
|
|
10722
|
-
* limiter, never a bare status).
|
|
10723
|
-
*/
|
|
10724
|
-
const toolBudgetDetail = (limiter) => {
|
|
10725
|
-
if (limiter === "maxToolCalls") return `maxToolCalls (${String(toolCallsUsed)}/${String(limits.maxToolCalls ?? 0)})`;
|
|
10726
|
-
const max = limits.toolUnits?.max ?? 0;
|
|
10727
|
-
const used = guard === void 0 ? max : guard.summary(toolCallsUsed).toolUnitsUsed ?? max;
|
|
10728
|
-
return `toolUnits (${String(used)}/${String(max)})`;
|
|
10729
|
-
};
|
|
10730
|
-
if (limits.toolBudgetNotices === true && limits.maxToolCalls === void 0) events?.emit({
|
|
10731
|
-
type: "log",
|
|
10732
|
-
level: "warn",
|
|
10733
|
-
msg: "toolBudgetNotices is enabled but maxToolCalls is not set; the notices are inert"
|
|
10734
|
-
});
|
|
10735
|
-
const firedNotices = /* @__PURE__ */ new Set();
|
|
10736
|
-
/**
|
|
10737
|
-
* Pushes the soft tool-budget notice when an unfired threshold has
|
|
10738
|
-
* been crossed (one message per boundary, carrying the exact counts,
|
|
10739
|
-
* so the model can pace itself before the hard cap). The notice is an
|
|
10740
|
-
* ordinary user message: it rides checkpoints and transcripts, so a
|
|
10741
|
-
* resume never re-fires a threshold the restored count already
|
|
10742
|
-
* crossed.
|
|
10743
|
-
*/
|
|
10744
|
-
const maybePushBudgetNotice = () => {
|
|
10745
|
-
if (limits.toolBudgetNotices !== true || limits.maxToolCalls === void 0) return;
|
|
10746
|
-
const crossed = crossedNoticeThresholds(toolCallsUsed, limits.maxToolCalls).filter((threshold) => !firedNotices.has(threshold));
|
|
10747
|
-
if (crossed.length === 0) return;
|
|
10748
|
-
for (const threshold of crossed) firedNotices.add(threshold);
|
|
10749
|
-
messages.push({
|
|
10750
|
-
role: "user",
|
|
10751
|
-
parts: [{
|
|
10752
|
-
type: "text",
|
|
10753
|
-
text: toolBudgetNoticeText(toolCallsUsed, limits.maxToolCalls)
|
|
10754
|
-
}]
|
|
10755
|
-
});
|
|
10756
|
-
};
|
|
10757
|
-
const modelRetryCounts = /* @__PURE__ */ new Map();
|
|
10758
|
-
let lastTurnUsage = {
|
|
10759
|
-
inputTokens: 0,
|
|
10760
|
-
outputTokens: 0
|
|
10761
|
-
};
|
|
10762
|
-
let compactionDisabled = false;
|
|
10763
|
-
const compactionPoints = [];
|
|
10764
|
-
let servedBy = options.resolved.ref;
|
|
10765
|
-
const restored = await options.checkpoint?.load();
|
|
10766
|
-
if (restored !== void 0) {
|
|
10767
|
-
messages.length = 0;
|
|
10768
|
-
messages.push(...restored.messages);
|
|
10769
|
-
turns = restored.turns;
|
|
10770
|
-
totalUsage = usageViolations(restored.usage).length === 0 ? restored.usage : sanitizeUsage(restored.usage);
|
|
10771
|
-
toolCallsUsed = restored.toolCallsUsed;
|
|
10772
|
-
schemaAttempts = restored.schemaAttempts;
|
|
10773
|
-
compactionPoints.push(...restored.compaction);
|
|
10774
|
-
const restoredSlices = restored.usageByModel ?? [{
|
|
10775
|
-
servedBy,
|
|
10776
|
-
usage: totalUsage
|
|
10777
|
-
}];
|
|
10778
|
-
for (const slice of restoredSlices) {
|
|
10779
|
-
const sliceUsage = usageViolations(slice.usage).length === 0 ? slice.usage : sanitizeUsage(slice.usage);
|
|
10780
|
-
addPhaseUsage(slice.role ?? primaryRole, slice.servedBy, sliceUsage);
|
|
10781
|
-
options.budget?.onUsage(sliceUsage, slice.servedBy);
|
|
10782
|
-
}
|
|
10783
|
-
for (const record of restored.providerCalls ?? []) providerCalls.push(usageViolations(record.usage).length === 0 ? record : {
|
|
10784
|
-
...record,
|
|
10785
|
-
usage: sanitizeUsage(record.usage)
|
|
10786
|
-
});
|
|
10787
|
-
guard?.restore(messages);
|
|
10788
|
-
if (limits.toolBudgetNotices === true && limits.maxToolCalls !== void 0) for (const threshold of crossedNoticeThresholds(toolCallsUsed, limits.maxToolCalls)) firedNotices.add(threshold);
|
|
10789
|
-
}
|
|
10790
|
-
const usageSlices = () => [...usageByPhaseModel.values()].map(({ role, servedBy: sliceServedBy, usage }) => ({
|
|
10791
|
-
servedBy: sliceServedBy,
|
|
10792
|
-
usage,
|
|
10793
|
-
role
|
|
10794
|
-
}));
|
|
10795
|
-
/**
|
|
10796
|
-
* Every slice priced at ITS OWN model's rate. An unpriced model
|
|
10797
|
-
* contributes zero here and surfaces through CostReport.unpriced, never
|
|
10798
|
-
* as a silent zero.
|
|
10799
|
-
*/
|
|
10800
|
-
const priceRecordedUsage = () => {
|
|
10801
|
-
const price = options.priceUsd;
|
|
10802
|
-
if (price === void 0) return 0;
|
|
10803
|
-
let usd = 0;
|
|
10804
|
-
for (const slice of usageByPhaseModel.values()) {
|
|
10805
|
-
const sliceUsd = price(slice.servedBy, slice.usage) ?? 0;
|
|
10806
|
-
if (Number.isFinite(sliceUsd) && sliceUsd > 0) usd += sliceUsd;
|
|
10807
|
-
}
|
|
10808
|
-
return usd;
|
|
10809
|
-
};
|
|
10810
|
-
const saveBoundary = async (pending) => {
|
|
10811
|
-
if (options.checkpoint === void 0) return;
|
|
10812
|
-
await options.checkpoint.save({
|
|
10813
|
-
v: 1,
|
|
10814
|
-
messages: [...messages],
|
|
10815
|
-
turns,
|
|
10816
|
-
usage: totalUsage,
|
|
10817
|
-
usageByModel: usageSlices(),
|
|
10818
|
-
toolCallsUsed,
|
|
10819
|
-
schemaAttempts,
|
|
10820
|
-
compaction: [...compactionPoints],
|
|
10821
|
-
...providerCalls.length === 0 ? {} : { providerCalls: [...providerCalls] },
|
|
10822
|
-
...pending === void 0 ? {} : { pending }
|
|
10823
|
-
});
|
|
10824
|
-
};
|
|
10825
|
-
const toPendingRecords = (parts) => parts.filter((part) => part.type === "tool-result").map((part) => ({
|
|
10826
|
-
id: part.id,
|
|
10827
|
-
name: part.name,
|
|
10828
|
-
result: part.result,
|
|
10829
|
-
...part.isError === true ? { isError: true } : {}
|
|
10830
|
-
}));
|
|
10831
|
-
/**
|
|
10832
|
-
* Gates and executes one turn's tool calls in source order. priorParts
|
|
10833
|
-
* carries results already executed before a mid-turn suspension; the
|
|
10834
|
-
* pending state checkpointed at an ask verdict stores RAW model args so
|
|
10835
|
-
* a resume re-runs the chain (hooks apply exactly once) and re-matches
|
|
10836
|
-
* the same approval identity.
|
|
10837
|
-
*/
|
|
10838
|
-
const runToolCalls = async (calls, priorParts) => {
|
|
10839
|
-
const runtime = options.tools;
|
|
10840
|
-
if (runtime === void 0) return {
|
|
10841
|
-
parts: priorParts,
|
|
10842
|
-
limitHit: false
|
|
10418
|
+
schemaAttempts,
|
|
10419
|
+
compaction: [...compactionPoints],
|
|
10420
|
+
...providerCalls.length === 0 ? {} : { providerCalls: [...providerCalls] },
|
|
10421
|
+
...pending === void 0 ? {} : { pending }
|
|
10422
|
+
});
|
|
10423
|
+
};
|
|
10424
|
+
const toPendingRecords = (parts) => parts.filter((part) => part.type === "tool-result").map((part) => ({
|
|
10425
|
+
id: part.id,
|
|
10426
|
+
name: part.name,
|
|
10427
|
+
result: part.result,
|
|
10428
|
+
...part.isError === true ? { isError: true } : {}
|
|
10429
|
+
}));
|
|
10430
|
+
/**
|
|
10431
|
+
* Gates and executes one turn's tool calls in source order. priorParts
|
|
10432
|
+
* carries results already executed before a mid-turn suspension; the
|
|
10433
|
+
* pending state checkpointed at an ask verdict stores RAW model args so
|
|
10434
|
+
* a resume re-runs the chain (hooks apply exactly once) and re-matches
|
|
10435
|
+
* the same approval identity.
|
|
10436
|
+
*/
|
|
10437
|
+
const runToolCalls = async (calls, priorParts) => {
|
|
10438
|
+
const runtime = options.tools;
|
|
10439
|
+
if (runtime === void 0) return {
|
|
10440
|
+
parts: priorParts,
|
|
10441
|
+
limitHit: false
|
|
10843
10442
|
};
|
|
10844
10443
|
const parts = [...priorParts];
|
|
10845
10444
|
const errorPart = (call, payload) => {
|
|
@@ -12791,140 +12390,1006 @@ var AdmissionController = class {
|
|
|
12791
12390
|
statsBefore
|
|
12792
12391
|
};
|
|
12793
12392
|
}
|
|
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
|
|
12393
|
+
if (depth > this.maxDepth) return {
|
|
12394
|
+
verdict: {
|
|
12395
|
+
kind: "reject",
|
|
12396
|
+
reason: { code: "depth" }
|
|
12397
|
+
},
|
|
12398
|
+
statsBefore
|
|
12399
|
+
};
|
|
12400
|
+
if (childrenBefore >= this.maxChildrenPerNode) return {
|
|
12401
|
+
verdict: {
|
|
12402
|
+
kind: "reject",
|
|
12403
|
+
reason: { code: "quota" }
|
|
12404
|
+
},
|
|
12405
|
+
statsBefore
|
|
12406
|
+
};
|
|
12407
|
+
if (this.maxTotalSpawns !== void 0 && this.admittedTotal >= this.maxTotalSpawns) return {
|
|
12408
|
+
verdict: {
|
|
12409
|
+
kind: "reject",
|
|
12410
|
+
reason: { code: "lifetime" }
|
|
12411
|
+
},
|
|
12412
|
+
statsBefore
|
|
12413
|
+
};
|
|
12414
|
+
let childCeilingUsd;
|
|
12415
|
+
const parentRemainder = this.budget.remainderOf(spec.parentAccountScope);
|
|
12416
|
+
if (parentRemainder !== void 0) {
|
|
12417
|
+
const fractionCap = this.childBudgetFraction * parentRemainder;
|
|
12418
|
+
childCeilingUsd = spec.budgetUsd === void 0 ? fractionCap : Math.min(spec.budgetUsd, fractionCap);
|
|
12419
|
+
} else if (spec.budgetUsd !== void 0) childCeilingUsd = spec.budgetUsd;
|
|
12420
|
+
let reserveUsd = spec.estCostUsd ?? this.flatReserveUsd;
|
|
12421
|
+
if (childCeilingUsd !== void 0) reserveUsd = Math.min(reserveUsd, childCeilingUsd);
|
|
12422
|
+
const reserve = { reserveUsd };
|
|
12423
|
+
if (childCeilingUsd !== void 0) reserve.childCeilingUsd = childCeilingUsd;
|
|
12424
|
+
if (this.budget.spawnHeadroom <= 0) return {
|
|
12425
|
+
verdict: {
|
|
12426
|
+
kind: "reject",
|
|
12427
|
+
reason: { code: "lifetime" }
|
|
12428
|
+
},
|
|
12429
|
+
statsBefore
|
|
12430
|
+
};
|
|
12431
|
+
if (commitReserve) try {
|
|
12432
|
+
this.budget.admitSpawn(reserveUsd, spec.parentAccountScope);
|
|
12433
|
+
} catch {
|
|
12434
|
+
return {
|
|
12435
|
+
verdict: {
|
|
12436
|
+
kind: "reject",
|
|
12437
|
+
reason: { code: "budget" }
|
|
12438
|
+
},
|
|
12439
|
+
statsBefore
|
|
12440
|
+
};
|
|
12441
|
+
}
|
|
12442
|
+
else {
|
|
12443
|
+
const remainder = this.budget.remainderOf(spec.parentAccountScope);
|
|
12444
|
+
const projection = this.projectedDispatchReserveUsd(spec);
|
|
12445
|
+
if (remainder !== void 0 && (remainder <= 0 || remainder < projection + (spec.pendingReserveUsd ?? 0))) return {
|
|
12446
|
+
verdict: {
|
|
12447
|
+
kind: "reject",
|
|
12448
|
+
reason: { code: "budget" }
|
|
12449
|
+
},
|
|
12450
|
+
statsBefore
|
|
12451
|
+
};
|
|
12452
|
+
}
|
|
12453
|
+
this.childrenOf.set(nodeKey, childrenBefore + 1);
|
|
12454
|
+
this.admittedTotal += 1;
|
|
12455
|
+
const lineage = evaluated.decision.lineage;
|
|
12456
|
+
this.registerLineageAdmit(lineage.logicalTaskId);
|
|
12457
|
+
let spawnUnitsAfter = this.budget.spawnHeadroom;
|
|
12458
|
+
if (this.terminationAccount !== void 0) {
|
|
12459
|
+
const debited = this.terminationAccount.debitSpawn({
|
|
12460
|
+
logicalTaskId: lineage.logicalTaskId,
|
|
12461
|
+
isNew: spec.lineage === void 0,
|
|
12462
|
+
ladderLength: spec.ladderLength ?? 1
|
|
12463
|
+
});
|
|
12464
|
+
if (!debited.ok) return {
|
|
12465
|
+
verdict: {
|
|
12466
|
+
kind: "reject",
|
|
12467
|
+
reason: { code: "termination_exhausted" }
|
|
12468
|
+
},
|
|
12469
|
+
statsBefore
|
|
12470
|
+
};
|
|
12471
|
+
spawnUnitsAfter = debited.spawnUnitsAfter;
|
|
12472
|
+
}
|
|
12473
|
+
return {
|
|
12474
|
+
verdict: {
|
|
12475
|
+
kind: "admit",
|
|
12476
|
+
reserve,
|
|
12477
|
+
spawnUnitsAfter,
|
|
12478
|
+
lineage: {
|
|
12479
|
+
logicalTaskId: lineage.logicalTaskId,
|
|
12480
|
+
isNew: spec.lineage === void 0,
|
|
12481
|
+
depth
|
|
12482
|
+
}
|
|
12483
|
+
},
|
|
12484
|
+
statsBefore,
|
|
12485
|
+
nodeId: this.mintId(),
|
|
12486
|
+
lineage,
|
|
12487
|
+
...this.terminationAccount === void 0 ? {} : { ladderLength: spec.ladderLength ?? 1 }
|
|
12488
|
+
};
|
|
12489
|
+
}
|
|
12490
|
+
/**
|
|
12491
|
+
* Resume roll-forward for an orchestrator child (M6-T07): restores the
|
|
12492
|
+
* children-quota counter only. The budget seed already counts settled
|
|
12493
|
+
* agent dispatches, and an in-flight child re-commits its reserve
|
|
12494
|
+
* through the ctx.agent dispatch path.
|
|
12495
|
+
*/
|
|
12496
|
+
recoverChild(nodeKey) {
|
|
12497
|
+
this.childrenOf.set(nodeKey, (this.childrenOf.get(nodeKey) ?? 0) + 1);
|
|
12498
|
+
this.admittedTotal += 1;
|
|
12499
|
+
}
|
|
12500
|
+
/**
|
|
12501
|
+
* Resume roll-forward for a child that already SETTLED before the
|
|
12502
|
+
* resume: re-registers the counters (maxChildrenPerNode, the lifetime
|
|
12503
|
+
* cap, statsBefore fidelity) without committing any reserve; the spend
|
|
12504
|
+
* itself sits in the root ledger seed.
|
|
12505
|
+
*/
|
|
12506
|
+
recoverSettled(parentAccountScope) {
|
|
12507
|
+
this.budget.admitRecovered(0, parentAccountScope);
|
|
12508
|
+
this.childrenOf.set(parentAccountScope, (this.childrenOf.get(parentAccountScope) ?? 0) + 1);
|
|
12509
|
+
this.admittedTotal += 1;
|
|
12510
|
+
}
|
|
12511
|
+
/**
|
|
12512
|
+
* Resume roll-forward for an admission whose decision entry exists but
|
|
12513
|
+
* whose child has NOT settled: re-applies the recorded reserve and
|
|
12514
|
+
* counters without re-evaluating any limit (replay never
|
|
12515
|
+
* re-evaluates admission; reserves are recovered, never
|
|
12516
|
+
* re-estimated).
|
|
12517
|
+
*/
|
|
12518
|
+
recoverInFlight(parentAccountScope, verdict) {
|
|
12519
|
+
if (verdict.kind === "reject") return;
|
|
12520
|
+
const reserveUsd = verdict.kind === "reuse_full" ? 0 : verdict.reserve.reserveUsd;
|
|
12521
|
+
this.budget.admitRecovered(reserveUsd, parentAccountScope);
|
|
12522
|
+
this.childrenOf.set(parentAccountScope, (this.childrenOf.get(parentAccountScope) ?? 0) + 1);
|
|
12523
|
+
this.admittedTotal += 1;
|
|
12524
|
+
}
|
|
12525
|
+
};
|
|
12526
|
+
//#endregion
|
|
12527
|
+
//#region src/engine/scheduler.ts
|
|
12528
|
+
/**
|
|
12529
|
+
* Scheduler and concurrency (M1-T08): the per-run semaphore with a FIFO
|
|
12530
|
+
* queue (default 12 concurrent model calls). The engine lifetime spawn cap
|
|
12531
|
+
* is enforced by the budget layer at admission; parallel/pipeline
|
|
12532
|
+
* composition semantics live with ctx.
|
|
12533
|
+
* Per-provider concurrency keys land with M4.
|
|
12534
|
+
*/
|
|
12535
|
+
/** FIFO semaphore; default per-run width is 12. */
|
|
12536
|
+
const DEFAULT_PER_RUN_CONCURRENCY = 12;
|
|
12537
|
+
var Semaphore = class {
|
|
12538
|
+
limit;
|
|
12539
|
+
active = 0;
|
|
12540
|
+
waiters = [];
|
|
12541
|
+
/**
|
|
12542
|
+
* `limit` must be a positive integer: anything else (NaN included) is
|
|
12543
|
+
* a typed ConfigError. Before this gate a NaN limit made
|
|
12544
|
+
* `active < limit` permanently false, so the first acquire queued
|
|
12545
|
+
* forever and the run could not settle, not even through cancel()
|
|
12546
|
+
* (v1.34.0 review P2-4). Unlimited is expressed by not constructing a
|
|
12547
|
+
* semaphore, never by a sentinel limit.
|
|
12548
|
+
*/
|
|
12549
|
+
constructor(limit) {
|
|
12550
|
+
requirePositiveInteger(limit, "Semaphore limit");
|
|
12551
|
+
this.limit = limit;
|
|
12552
|
+
}
|
|
12553
|
+
get pending() {
|
|
12554
|
+
return this.waiters.length;
|
|
12555
|
+
}
|
|
12556
|
+
/**
|
|
12557
|
+
* Acquires a slot, resolving in FIFO order. `onQueued` fires only when
|
|
12558
|
+
* the caller actually has to wait (feeds the agent:queued event).
|
|
12559
|
+
* An aborted `signal` releases the caller from the queue without a
|
|
12560
|
+
* slot: the returned release is a no-op, the remaining waiters keep
|
|
12561
|
+
* their FIFO positions, and the caller proceeds to observe its own
|
|
12562
|
+
* aborted signal (the model layers refuse dispatch under an aborted
|
|
12563
|
+
* signal, so no provider call follows). Cancellation can therefore
|
|
12564
|
+
* always drain a queued run (v1.34.0 review P2-4).
|
|
12565
|
+
*/
|
|
12566
|
+
async acquire(onQueued, signal) {
|
|
12567
|
+
if (this.active < this.limit) {
|
|
12568
|
+
this.active += 1;
|
|
12569
|
+
return () => this.release();
|
|
12570
|
+
}
|
|
12571
|
+
if (signal?.aborted === true) return () => void 0;
|
|
12572
|
+
onQueued?.();
|
|
12573
|
+
const waiter = {
|
|
12574
|
+
resolve: () => void 0,
|
|
12575
|
+
aborted: false
|
|
12576
|
+
};
|
|
12577
|
+
const wait = new Promise((resolve) => {
|
|
12578
|
+
waiter.resolve = resolve;
|
|
12579
|
+
});
|
|
12580
|
+
this.waiters.push(waiter);
|
|
12581
|
+
let onAbort;
|
|
12582
|
+
if (signal !== void 0) {
|
|
12583
|
+
onAbort = () => {
|
|
12584
|
+
const index = this.waiters.indexOf(waiter);
|
|
12585
|
+
if (index === -1) return;
|
|
12586
|
+
this.waiters.splice(index, 1);
|
|
12587
|
+
waiter.aborted = true;
|
|
12588
|
+
waiter.resolve();
|
|
12589
|
+
};
|
|
12590
|
+
signal.addEventListener("abort", onAbort, { once: true });
|
|
12591
|
+
}
|
|
12592
|
+
try {
|
|
12593
|
+
await wait;
|
|
12594
|
+
} finally {
|
|
12595
|
+
if (signal !== void 0 && onAbort !== void 0) signal.removeEventListener("abort", onAbort);
|
|
12596
|
+
}
|
|
12597
|
+
if (waiter.aborted) return () => void 0;
|
|
12598
|
+
this.active += 1;
|
|
12599
|
+
return () => this.release();
|
|
12600
|
+
}
|
|
12601
|
+
async withSlot(fn, onQueued, signal) {
|
|
12602
|
+
const release = await this.acquire(onQueued, signal);
|
|
12603
|
+
try {
|
|
12604
|
+
return await fn();
|
|
12605
|
+
} finally {
|
|
12606
|
+
release();
|
|
12607
|
+
}
|
|
12608
|
+
}
|
|
12609
|
+
release() {
|
|
12610
|
+
this.active -= 1;
|
|
12611
|
+
const next = this.waiters.shift();
|
|
12612
|
+
if (next !== void 0) next.resolve();
|
|
12613
|
+
}
|
|
12614
|
+
};
|
|
12615
|
+
//#endregion
|
|
12616
|
+
//#region src/engine/preflight.ts
|
|
12617
|
+
const ANY_TOOL = "(any)";
|
|
12618
|
+
function resolveServing(spec) {
|
|
12619
|
+
if (spec === void 0) return;
|
|
12620
|
+
if (typeof spec === "string") return spec;
|
|
12621
|
+
if ("model" in spec) return spec.model;
|
|
12622
|
+
return spec.ladder.rungs[spec.ladder.startTier]?.model;
|
|
12623
|
+
}
|
|
12624
|
+
/**
|
|
12625
|
+
* Per-tool executed-call ceilings from the merged limits: for every
|
|
12626
|
+
* tool a per-tool cap or a unit cost names (plus the '(any)' tool that
|
|
12627
|
+
* nothing names, unit cost 1), the smallest of maxCallsPerTool[T],
|
|
12628
|
+
* floor(toolUnits.max / cost(T)) for a positive cost (a zero cost is
|
|
12629
|
+
* free), and maxToolCalls.
|
|
12630
|
+
*/
|
|
12631
|
+
function toolCeilingsOf(limits) {
|
|
12632
|
+
const names = /* @__PURE__ */ new Set();
|
|
12633
|
+
for (const name of Object.keys(limits.maxCallsPerTool ?? {})) names.add(name);
|
|
12634
|
+
for (const name of Object.keys(limits.toolUnits?.costs ?? {})) names.add(name);
|
|
12635
|
+
const rows = [];
|
|
12636
|
+
for (const tool of [...[...names].sort(), ANY_TOOL]) {
|
|
12637
|
+
const terms = [];
|
|
12638
|
+
const cap = tool === ANY_TOOL ? void 0 : limits.maxCallsPerTool?.[tool];
|
|
12639
|
+
if (cap !== void 0) terms.push({
|
|
12640
|
+
boundBy: "maxCallsPerTool",
|
|
12641
|
+
ceiling: cap
|
|
12642
|
+
});
|
|
12643
|
+
if (limits.toolUnits !== void 0) {
|
|
12644
|
+
const cost = tool === ANY_TOOL ? 1 : limits.toolUnits.costs?.[tool] ?? 1;
|
|
12645
|
+
if (cost > 0) terms.push({
|
|
12646
|
+
boundBy: "toolUnits",
|
|
12647
|
+
ceiling: Math.floor(limits.toolUnits.max / cost)
|
|
12648
|
+
});
|
|
12649
|
+
}
|
|
12650
|
+
if (limits.maxToolCalls !== void 0) terms.push({
|
|
12651
|
+
boundBy: "maxToolCalls",
|
|
12652
|
+
ceiling: limits.maxToolCalls
|
|
12653
|
+
});
|
|
12654
|
+
if (terms.length === 0) {
|
|
12655
|
+
rows.push({
|
|
12656
|
+
tool,
|
|
12657
|
+
ceiling: null
|
|
12658
|
+
});
|
|
12659
|
+
continue;
|
|
12660
|
+
}
|
|
12661
|
+
const min = terms.reduce((best, term) => term.ceiling < best.ceiling ? term : best);
|
|
12662
|
+
rows.push({
|
|
12663
|
+
tool,
|
|
12664
|
+
ceiling: min.ceiling,
|
|
12665
|
+
boundBy: min.boundBy
|
|
12666
|
+
});
|
|
12667
|
+
}
|
|
12668
|
+
return rows;
|
|
12669
|
+
}
|
|
12670
|
+
function validateSpawnSpec(spec, index) {
|
|
12671
|
+
const site = `preflight.spawns[${index}]`;
|
|
12672
|
+
if (spec.limits !== void 0) validateUsageLimits(spec.limits, `${site}.limits`);
|
|
12673
|
+
if (spec.estCost !== void 0) requireNonNegativeNumber(spec.estCost, `${site}.estCost`);
|
|
12674
|
+
if (spec.estInputTokens !== void 0) requireNonNegativeInteger(spec.estInputTokens, `${site}.estInputTokens`);
|
|
12675
|
+
if (spec.count !== void 0) requirePositiveInteger(spec.count, `${site}.count`);
|
|
12676
|
+
}
|
|
12677
|
+
/**
|
|
12678
|
+
* Computes the preflight report: the effective merged limits per
|
|
12679
|
+
* declared spawn, the layer-1 admission projection over the declared
|
|
12680
|
+
* wave, the per-tool and weighted-unit bottleneck ordering, the
|
|
12681
|
+
* concurrency and quota exposure at the declared estimates, and the
|
|
12682
|
+
* linter findings. Pure: no engine is constructed, no store is opened,
|
|
12683
|
+
* no adapter stream is dispatched, and no journal entry is written.
|
|
12684
|
+
*/
|
|
12685
|
+
function preflightEstimate(input) {
|
|
12686
|
+
const engine = input.engine ?? {};
|
|
12687
|
+
const defaults = engine.defaults ?? {};
|
|
12688
|
+
if (defaults.limits !== void 0) validateUsageLimits(defaults.limits, "preflight.engine.defaults.limits");
|
|
12689
|
+
if (input.run?.limits !== void 0) validateUsageLimits(input.run.limits, "preflight.run.limits");
|
|
12690
|
+
if (input.orchestrator?.limits !== void 0) validateUsageLimits(input.orchestrator.limits, "preflight.orchestrator.limits");
|
|
12691
|
+
const findings = [];
|
|
12692
|
+
const say = (finding) => {
|
|
12693
|
+
findings.push(finding);
|
|
12694
|
+
};
|
|
12695
|
+
const adapters = new Map((engine.adapters ?? []).map((adapter) => [adapter.id, adapter]));
|
|
12696
|
+
const capsOf = (ref) => {
|
|
12697
|
+
const { adapterId, model } = parseModelRef(ref);
|
|
12698
|
+
return adapters.get(adapterId)?.caps(model);
|
|
12699
|
+
};
|
|
12700
|
+
const pricingOf = (ref) => resolvePricing(ref, engine.pricing, capsOf(ref)?.pricing);
|
|
12701
|
+
const ceilingUsd = input.run?.budgetUsd;
|
|
12702
|
+
const flatReserveUsd = engine.budgetDefaults?.flatReserveUsd ?? .5;
|
|
12703
|
+
const lifetimeSpawnCap = engine.budgetDefaults?.lifetimeSpawnCap ?? 500;
|
|
12704
|
+
const childBudgetFraction = engine.budgetDefaults?.childBudgetFraction ?? .3;
|
|
12705
|
+
const maxDepth = engine.budgetDefaults?.maxDepth ?? 1;
|
|
12706
|
+
const perRun = engine.concurrency?.perRun ?? 12;
|
|
12707
|
+
const runLimits = mergeUsageLimits(void 0, input.run?.limits, defaults.limits);
|
|
12708
|
+
let orchestratorEcho;
|
|
12709
|
+
let reservedForFinalizationUsd = 0;
|
|
12710
|
+
let effectiveCapUsd;
|
|
12711
|
+
if (input.orchestrator !== void 0) {
|
|
12712
|
+
const spec = input.orchestrator.budget;
|
|
12713
|
+
const fraction = spec?.capFraction ?? .2;
|
|
12714
|
+
const fromFraction = ceilingUsd === void 0 ? void 0 : fraction * ceilingUsd;
|
|
12715
|
+
const bounds = [spec?.capUsd, fromFraction].filter((bound) => bound !== void 0);
|
|
12716
|
+
effectiveCapUsd = bounds.length === 0 ? void 0 : Math.min(...bounds);
|
|
12717
|
+
const finalizeTurns = spec?.finalizeTurns ?? 2;
|
|
12718
|
+
const finalizeReserveUsd = spec?.finalizeReserveUsd ?? finalizeTurns * flatReserveUsd;
|
|
12719
|
+
const reserveCommitted = input.orchestrator.extension === true;
|
|
12720
|
+
if (reserveCommitted) reservedForFinalizationUsd = finalizeReserveUsd;
|
|
12721
|
+
orchestratorEcho = {
|
|
12722
|
+
...effectiveCapUsd === void 0 ? {} : { effectiveCapUsd },
|
|
12723
|
+
finalizeReserveUsd,
|
|
12724
|
+
finalizeTurns,
|
|
12725
|
+
reserveCommitted
|
|
12831
12726
|
};
|
|
12832
|
-
if (
|
|
12833
|
-
|
|
12834
|
-
|
|
12835
|
-
|
|
12836
|
-
|
|
12837
|
-
|
|
12838
|
-
|
|
12839
|
-
|
|
12840
|
-
|
|
12727
|
+
if (spec?.capUsd !== void 0 && spec.capFraction === void 0 && effectiveCapUsd !== void 0 && effectiveCapUsd < spec.capUsd) say({
|
|
12728
|
+
severity: "warning",
|
|
12729
|
+
code: "orchestrator-cap-fraction-bound",
|
|
12730
|
+
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`
|
|
12731
|
+
});
|
|
12732
|
+
if (input.orchestrator.extension === true && effectiveCapUsd !== void 0 && effectiveCapUsd < finalizeReserveUsd) say({
|
|
12733
|
+
severity: "error",
|
|
12734
|
+
code: "orchestrator-cap-below-finalize-reserve",
|
|
12735
|
+
message: `effectiveCap ${effectiveCapUsd.toFixed(4)} USD is below the finalize reserve ${finalizeReserveUsd.toFixed(4)} USD: the run would refuse to start`
|
|
12736
|
+
});
|
|
12737
|
+
}
|
|
12738
|
+
const spawnSpecs = input.spawns ?? [];
|
|
12739
|
+
spawnSpecs.forEach(validateSpawnSpec);
|
|
12740
|
+
const spawnReports = [];
|
|
12741
|
+
const units = [];
|
|
12742
|
+
for (const spec of spawnSpecs) {
|
|
12743
|
+
const role = spec.role ?? "loop";
|
|
12744
|
+
const label = spec.label ?? role;
|
|
12745
|
+
const count = spec.count ?? 1;
|
|
12746
|
+
const profile = spec.profile === void 0 ? void 0 : defaults.profiles?.[spec.profile];
|
|
12747
|
+
if (spec.profile !== void 0 && profile === void 0) say({
|
|
12748
|
+
severity: "error",
|
|
12749
|
+
code: "unknown-profile",
|
|
12750
|
+
message: `spawn '${label}' names profile '${spec.profile}', which defaults.profiles does not register`,
|
|
12751
|
+
spawn: label
|
|
12752
|
+
});
|
|
12753
|
+
const limits = mergeUsageLimits(spec.limits, profile?.limits, defaults.limits);
|
|
12754
|
+
const servedBy = resolveServing(spec.model ?? profile?.routing?.[role] ?? profile?.model ?? defaults.routing?.[role]);
|
|
12755
|
+
if (servedBy === void 0) say({
|
|
12756
|
+
severity: "error",
|
|
12757
|
+
code: "unrouted-role",
|
|
12758
|
+
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}`,
|
|
12759
|
+
spawn: label
|
|
12760
|
+
});
|
|
12761
|
+
const caps = servedBy === void 0 ? void 0 : capsOf(servedBy);
|
|
12762
|
+
const pricing = servedBy === void 0 ? void 0 : pricingOf(servedBy);
|
|
12763
|
+
const unpriced = servedBy !== void 0 && pricing === void 0;
|
|
12764
|
+
let reserveSource;
|
|
12765
|
+
let reserveUsd;
|
|
12766
|
+
if (unpriced && spec.estCost === void 0 && profile?.estCost === void 0) {
|
|
12767
|
+
reserveSource = "unpriced-zero";
|
|
12768
|
+
reserveUsd = 0;
|
|
12769
|
+
} else {
|
|
12770
|
+
reserveSource = spec.estCost !== void 0 ? "estCost" : profile?.estCost !== void 0 ? "profile-estCost" : spec.estInputTokens !== void 0 && caps?.pricing !== void 0 ? "priced-estimate" : "flat-default";
|
|
12771
|
+
reserveUsd = admissionReserveUsd({
|
|
12772
|
+
...spec.estCost === void 0 ? {} : { estCost: spec.estCost },
|
|
12773
|
+
...profile?.estCost === void 0 ? {} : { profileEstCost: profile.estCost },
|
|
12774
|
+
...spec.estInputTokens === void 0 ? {} : { inputTokens: spec.estInputTokens },
|
|
12775
|
+
...caps === void 0 ? {} : { caps },
|
|
12776
|
+
...limits.maxOutputTokensPerTurn === void 0 ? {} : { maxOutputTokensPerTurn: limits.maxOutputTokensPerTurn },
|
|
12777
|
+
flatReserveUsd
|
|
12778
|
+
});
|
|
12779
|
+
}
|
|
12780
|
+
const outputBound = caps === void 0 ? limits.maxOutputTokensPerTurn : limits.maxOutputTokensPerTurn === void 0 ? caps.maxOutputTokens : Math.min(caps.maxOutputTokens, limits.maxOutputTokensPerTurn);
|
|
12781
|
+
if (caps !== void 0 && limits.maxOutputTokensPerTurn !== void 0 && limits.maxOutputTokensPerTurn > caps.maxOutputTokens) say({
|
|
12782
|
+
severity: "warning",
|
|
12783
|
+
code: "output-cap-above-model",
|
|
12784
|
+
message: `spawn '${label}' sets maxOutputTokensPerTurn ${String(limits.maxOutputTokensPerTurn)} above the model's maxOutputTokens ${String(caps.maxOutputTokens)}: the model clamp wins`,
|
|
12785
|
+
spawn: label
|
|
12786
|
+
});
|
|
12787
|
+
const turnFloorUsd = pricing === void 0 || outputBound === void 0 ? void 0 : priceUsdOf(pricing, {
|
|
12788
|
+
inputTokens: spec.estInputTokens ?? 0,
|
|
12789
|
+
outputTokens: outputBound,
|
|
12790
|
+
cacheReadTokens: 0,
|
|
12791
|
+
cacheWriteTokens: 0
|
|
12792
|
+
});
|
|
12793
|
+
const toolCeilings = toolCeilingsOf(limits);
|
|
12794
|
+
const overall = toolCeilings.reduce((best, row) => row.ceiling === null ? best : best === null ? row.ceiling : Math.max(best, row.ceiling), null);
|
|
12795
|
+
const executedToolCallCeiling = limits.maxToolCalls !== void 0 && (overall === null || limits.maxToolCalls < overall) ? limits.maxToolCalls : overall;
|
|
12796
|
+
for (const row of toolCeilings) {
|
|
12797
|
+
if (row.tool === ANY_TOOL) continue;
|
|
12798
|
+
const cost = limits.toolUnits?.costs?.[row.tool];
|
|
12799
|
+
if (cost !== void 0 && cost > 0 && limits.toolUnits !== void 0 && cost > limits.toolUnits.max) {
|
|
12800
|
+
say({
|
|
12801
|
+
severity: "warning",
|
|
12802
|
+
code: "tool-unaffordable",
|
|
12803
|
+
message: `spawn '${label}' prices tool '${row.tool}' at ${String(cost)} units against toolUnits.max ${String(limits.toolUnits.max)}: the tool can never execute`,
|
|
12804
|
+
spawn: label
|
|
12805
|
+
});
|
|
12806
|
+
continue;
|
|
12807
|
+
}
|
|
12808
|
+
if (row.boundBy === "toolUnits" && row.ceiling !== null) {
|
|
12809
|
+
const nominal = limits.maxToolCalls;
|
|
12810
|
+
const cap = limits.maxCallsPerTool?.[row.tool];
|
|
12811
|
+
if (nominal !== void 0 && row.ceiling < nominal || cap !== void 0 && row.ceiling < cap) say({
|
|
12812
|
+
severity: "warning",
|
|
12813
|
+
code: "weighted-units-bind-first",
|
|
12814
|
+
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)}`),
|
|
12815
|
+
spawn: label
|
|
12816
|
+
});
|
|
12817
|
+
}
|
|
12818
|
+
const cap = limits.maxCallsPerTool?.[row.tool];
|
|
12819
|
+
if (cap !== void 0 && cap > 0 && row.ceiling !== null && row.boundBy !== "maxCallsPerTool") say({
|
|
12820
|
+
severity: "info",
|
|
12821
|
+
code: "per-tool-cap-unreachable",
|
|
12822
|
+
message: `spawn '${label}': maxCallsPerTool['${row.tool}'] ${String(cap)} can never bind: ${row.boundBy ?? "another limiter"} already stops at ${String(row.ceiling)}`,
|
|
12823
|
+
spawn: label
|
|
12824
|
+
});
|
|
12825
|
+
}
|
|
12826
|
+
if (limits.finalizationReserve !== void 0 && limits.maxToolCalls === void 0 && limits.toolUnits === void 0) say({
|
|
12827
|
+
severity: "warning",
|
|
12828
|
+
code: "inert-finalization-reserve",
|
|
12829
|
+
message: `spawn '${label}' sets finalizationReserve without maxToolCalls or toolUnits: no tool budget limiter exists for it to fire on`,
|
|
12830
|
+
spawn: label
|
|
12831
|
+
});
|
|
12832
|
+
if (limits.toolBudgetNotices === true && limits.maxToolCalls === void 0) say({
|
|
12833
|
+
severity: "warning",
|
|
12834
|
+
code: "inert-tool-budget-notices",
|
|
12835
|
+
message: `spawn '${label}' sets toolBudgetNotices without maxToolCalls: the notices never fire`,
|
|
12836
|
+
spawn: label
|
|
12837
|
+
});
|
|
12838
|
+
if (unpriced && ceilingUsd !== void 0) say({
|
|
12839
|
+
severity: "warning",
|
|
12840
|
+
code: "unpriced-under-ceiling",
|
|
12841
|
+
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`,
|
|
12842
|
+
spawn: label
|
|
12843
|
+
});
|
|
12844
|
+
spawnReports.push({
|
|
12845
|
+
label,
|
|
12846
|
+
role,
|
|
12847
|
+
count,
|
|
12848
|
+
...servedBy === void 0 ? {} : { servedBy },
|
|
12849
|
+
...unpriced ? { unpriced: true } : {},
|
|
12850
|
+
limits,
|
|
12851
|
+
admissionReserveUsd: reserveUsd,
|
|
12852
|
+
reserveSource,
|
|
12853
|
+
...outputBound === void 0 ? {} : { maxOutputTokensPerTurn: outputBound },
|
|
12854
|
+
...turnFloorUsd === void 0 ? {} : { turnFloorUsd },
|
|
12855
|
+
executedToolCallCeiling,
|
|
12856
|
+
toolCeilings
|
|
12857
|
+
});
|
|
12858
|
+
for (let i = 0; i < count; i += 1) {
|
|
12859
|
+
const unit = {
|
|
12860
|
+
label: count === 1 ? label : `${label}#${String(i + 1)}`,
|
|
12861
|
+
tokensFloor: (spec.estInputTokens ?? 0) + (outputBound ?? 0)
|
|
12841
12862
|
};
|
|
12863
|
+
if (servedBy !== void 0) {
|
|
12864
|
+
const { adapterId, model } = parseModelRef(servedBy);
|
|
12865
|
+
unit.provider = adapterId;
|
|
12866
|
+
unit.model = model;
|
|
12867
|
+
}
|
|
12868
|
+
if (turnFloorUsd !== void 0) unit.turnFloorUsd = turnFloorUsd;
|
|
12869
|
+
units.push(unit);
|
|
12842
12870
|
}
|
|
12871
|
+
}
|
|
12872
|
+
if (input.orchestrator !== void 0) {
|
|
12873
|
+
const servedBy = resolveServing(defaults.routing?.orchestrate);
|
|
12874
|
+
if (servedBy === void 0) say({
|
|
12875
|
+
severity: "error",
|
|
12876
|
+
code: "unrouted-role",
|
|
12877
|
+
message: "the orchestrator resolves no model for role 'orchestrate': set defaults.routing.orchestrate or an orchestrate model on the call",
|
|
12878
|
+
spawn: "orchestrator"
|
|
12879
|
+
});
|
|
12843
12880
|
else {
|
|
12844
|
-
const
|
|
12845
|
-
const
|
|
12846
|
-
|
|
12847
|
-
|
|
12848
|
-
|
|
12849
|
-
|
|
12850
|
-
|
|
12851
|
-
|
|
12881
|
+
const caps = capsOf(servedBy);
|
|
12882
|
+
const pricing = pricingOf(servedBy);
|
|
12883
|
+
const orchLimits = mergeUsageLimits(input.orchestrator.limits, void 0, defaults.limits);
|
|
12884
|
+
const outputBound = caps === void 0 ? orchLimits.maxOutputTokensPerTurn : orchLimits.maxOutputTokensPerTurn === void 0 ? caps.maxOutputTokens : Math.min(caps.maxOutputTokens, orchLimits.maxOutputTokensPerTurn);
|
|
12885
|
+
const { adapterId, model } = parseModelRef(servedBy);
|
|
12886
|
+
const unit = {
|
|
12887
|
+
label: "orchestrator",
|
|
12888
|
+
provider: adapterId,
|
|
12889
|
+
model,
|
|
12890
|
+
tokensFloor: outputBound ?? 0
|
|
12852
12891
|
};
|
|
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
|
|
12892
|
+
if (pricing !== void 0 && outputBound !== void 0) unit.turnFloorUsd = priceUsdOf(pricing, {
|
|
12893
|
+
inputTokens: 0,
|
|
12894
|
+
outputTokens: outputBound,
|
|
12895
|
+
cacheReadTokens: 0,
|
|
12896
|
+
cacheWriteTokens: 0
|
|
12864
12897
|
});
|
|
12865
|
-
|
|
12866
|
-
verdict: {
|
|
12867
|
-
kind: "reject",
|
|
12868
|
-
reason: { code: "termination_exhausted" }
|
|
12869
|
-
},
|
|
12870
|
-
statsBefore
|
|
12871
|
-
};
|
|
12872
|
-
spawnUnitsAfter = debited.spawnUnitsAfter;
|
|
12898
|
+
units.push(unit);
|
|
12873
12899
|
}
|
|
12874
|
-
|
|
12875
|
-
|
|
12876
|
-
|
|
12877
|
-
|
|
12878
|
-
|
|
12879
|
-
|
|
12880
|
-
|
|
12881
|
-
|
|
12882
|
-
|
|
12883
|
-
|
|
12884
|
-
|
|
12885
|
-
|
|
12886
|
-
|
|
12887
|
-
|
|
12888
|
-
|
|
12900
|
+
}
|
|
12901
|
+
const wave = [];
|
|
12902
|
+
let committed = 0;
|
|
12903
|
+
let spawned = 0;
|
|
12904
|
+
let children = 0;
|
|
12905
|
+
const admitAgainstRoot = (reserveUsd) => {
|
|
12906
|
+
if (ceilingUsd === void 0) return true;
|
|
12907
|
+
const held = committed + reservedForFinalizationUsd;
|
|
12908
|
+
return !(held >= ceilingUsd || held + reserveUsd > ceilingUsd);
|
|
12909
|
+
};
|
|
12910
|
+
if (input.orchestrator !== void 0) {
|
|
12911
|
+
const reserveUsd = flatReserveUsd;
|
|
12912
|
+
let deniedBy;
|
|
12913
|
+
if (spawned >= lifetimeSpawnCap) deniedBy = "spawn-cap";
|
|
12914
|
+
else if (effectiveCapUsd !== void 0 && reserveUsd > effectiveCapUsd) deniedBy = "orchestrator-cap";
|
|
12915
|
+
else if (!admitAgainstRoot(reserveUsd)) deniedBy = "budget";
|
|
12916
|
+
wave.push({
|
|
12917
|
+
label: "orchestrator",
|
|
12918
|
+
reserveUsd,
|
|
12919
|
+
admitted: deniedBy === void 0,
|
|
12920
|
+
...deniedBy === void 0 ? {} : { deniedBy }
|
|
12921
|
+
});
|
|
12922
|
+
if (deniedBy === void 0) {
|
|
12923
|
+
committed += reserveUsd;
|
|
12924
|
+
spawned += 1;
|
|
12925
|
+
} else if (deniedBy === "orchestrator-cap") say({
|
|
12926
|
+
severity: "error",
|
|
12927
|
+
code: "orchestrator-cap-below-reserve",
|
|
12928
|
+
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`
|
|
12929
|
+
});
|
|
12930
|
+
}
|
|
12931
|
+
const maxSpawns = input.orchestrator?.maxSpawns;
|
|
12932
|
+
for (const report of spawnReports) for (let i = 0; i < report.count; i += 1) {
|
|
12933
|
+
const label = report.count === 1 ? report.label : `${report.label}#${String(i + 1)}`;
|
|
12934
|
+
const reserveUsd = report.admissionReserveUsd;
|
|
12935
|
+
let deniedBy;
|
|
12936
|
+
if (spawned >= lifetimeSpawnCap) deniedBy = "spawn-cap";
|
|
12937
|
+
else if (maxSpawns !== void 0 && children >= maxSpawns) deniedBy = "orchestrator-max-spawns";
|
|
12938
|
+
else if (!admitAgainstRoot(reserveUsd)) deniedBy = "budget";
|
|
12939
|
+
wave.push({
|
|
12940
|
+
label,
|
|
12941
|
+
reserveUsd,
|
|
12942
|
+
admitted: deniedBy === void 0,
|
|
12943
|
+
...deniedBy === void 0 ? {} : { deniedBy }
|
|
12944
|
+
});
|
|
12945
|
+
if (deniedBy === void 0) {
|
|
12946
|
+
committed += reserveUsd;
|
|
12947
|
+
spawned += 1;
|
|
12948
|
+
children += 1;
|
|
12949
|
+
}
|
|
12950
|
+
}
|
|
12951
|
+
const admitted = wave.filter((row) => row.admitted).length;
|
|
12952
|
+
const denied = wave.length - admitted;
|
|
12953
|
+
if (wave.length > 0 && denied > 0) {
|
|
12954
|
+
const deniedLabels = wave.filter((row) => !row.admitted).map((row) => row.label);
|
|
12955
|
+
if (admitted === 0) say({
|
|
12956
|
+
severity: "error",
|
|
12957
|
+
code: "nothing-admitted",
|
|
12958
|
+
message: `the declared wave admits NOTHING: every spawn is denied (${deniedLabels.join(", ")}); no paid work can start`
|
|
12959
|
+
});
|
|
12960
|
+
else say({
|
|
12961
|
+
severity: "warning",
|
|
12962
|
+
code: "partial-admission",
|
|
12963
|
+
message: `the declared wave admits ${String(admitted)} of ${String(wave.length)} spawns; denied before any work: ${deniedLabels.join(", ")}`
|
|
12964
|
+
});
|
|
12965
|
+
}
|
|
12966
|
+
if (ceilingUsd === void 0 && wave.length > 0) say({
|
|
12967
|
+
severity: "info",
|
|
12968
|
+
code: "no-usd-ceiling",
|
|
12969
|
+
message: "the run has no budgetUsd ceiling: only turn, tool, and time limits bound spend, and the whole declared wave admits"
|
|
12970
|
+
});
|
|
12971
|
+
const declaredUnits = units.length;
|
|
12972
|
+
const maxInFlight = declaredUnits === 0 ? perRun : Math.min(perRun, declaredUnits);
|
|
12973
|
+
const perProviderCaps = engine.concurrency?.perProvider;
|
|
12974
|
+
const perProvider = {};
|
|
12975
|
+
const byProvider = /* @__PURE__ */ new Map();
|
|
12976
|
+
for (const unit of units) {
|
|
12977
|
+
if (unit.provider === void 0) continue;
|
|
12978
|
+
const list = byProvider.get(unit.provider) ?? [];
|
|
12979
|
+
list.push(unit);
|
|
12980
|
+
byProvider.set(unit.provider, list);
|
|
12981
|
+
}
|
|
12982
|
+
for (const [provider, list] of [...byProvider.entries()].sort(([a], [b]) => a.localeCompare(b))) {
|
|
12983
|
+
const cap = perProviderCaps?.[provider];
|
|
12984
|
+
const inFlight = Math.min(list.length, maxInFlight, cap ?? Number.POSITIVE_INFINITY);
|
|
12985
|
+
perProvider[provider] = {
|
|
12986
|
+
inFlight,
|
|
12987
|
+
requestsPerWave: inFlight,
|
|
12988
|
+
tokensPerWaveFloor: [...list].sort((a, b) => b.tokensFloor - a.tokensFloor).slice(0, inFlight).reduce((sum, unit) => sum + unit.tokensFloor, 0)
|
|
12889
12989
|
};
|
|
12890
12990
|
}
|
|
12891
|
-
|
|
12892
|
-
|
|
12893
|
-
|
|
12894
|
-
|
|
12895
|
-
|
|
12896
|
-
|
|
12897
|
-
|
|
12898
|
-
|
|
12899
|
-
|
|
12991
|
+
const pricedTurns = units.map((unit) => unit.turnFloorUsd).filter((usd) => usd !== void 0).sort((a, b) => b - a).slice(0, maxInFlight);
|
|
12992
|
+
const overshootOneTurnFloorUsd = pricedTurns.length === 0 ? void 0 : pricedTurns.reduce((sum, usd) => sum + usd, 0);
|
|
12993
|
+
if (ceilingUsd !== void 0 && overshootOneTurnFloorUsd !== void 0 && units.length > 0) say({
|
|
12994
|
+
severity: "info",
|
|
12995
|
+
code: "overshoot-exposure",
|
|
12996
|
+
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`
|
|
12997
|
+
});
|
|
12998
|
+
const quotaConfigured = engine.quota !== void 0;
|
|
12999
|
+
if (!quotaConfigured && maxInFlight > 1 && units.length > 0) say({
|
|
13000
|
+
severity: "info",
|
|
13001
|
+
code: "no-quota",
|
|
13002
|
+
message: `no shared quota limiter is configured while up to ${String(maxInFlight)} turns run concurrently: provider-side rate limits are unprotected (createEngine quota)`
|
|
13003
|
+
});
|
|
13004
|
+
if (input.quotaRules !== void 0) input.quotaRules.forEach((rule, index) => {
|
|
13005
|
+
let requests = 0;
|
|
13006
|
+
let tokens = 0;
|
|
13007
|
+
for (const unit of units) {
|
|
13008
|
+
if (unit.provider === void 0 || unit.model === void 0) continue;
|
|
13009
|
+
if (quotaRuleMatches(rule, {
|
|
13010
|
+
provider: unit.provider,
|
|
13011
|
+
model: unit.model,
|
|
13012
|
+
...engine.quota?.tenant === void 0 ? {} : { tenant: engine.quota.tenant },
|
|
13013
|
+
estimate: {
|
|
13014
|
+
requests: 1,
|
|
13015
|
+
inputTokens: 0
|
|
13016
|
+
}
|
|
13017
|
+
})) {
|
|
13018
|
+
requests += 1;
|
|
13019
|
+
tokens += unit.tokensFloor;
|
|
13020
|
+
}
|
|
13021
|
+
}
|
|
13022
|
+
const dims = [
|
|
13023
|
+
rule.provider === void 0 ? void 0 : `provider=${rule.provider}`,
|
|
13024
|
+
rule.model === void 0 ? void 0 : `model=${rule.model}`,
|
|
13025
|
+
rule.tenant === void 0 ? void 0 : `tenant=${rule.tenant}`
|
|
13026
|
+
].filter((dim) => dim !== void 0).join(" ");
|
|
13027
|
+
const name = dims === "" ? `rule[${String(index)}]` : `rule[${String(index)}] (${dims})`;
|
|
13028
|
+
if (rule.requestsPerMinute !== void 0 && requests > rule.requestsPerMinute) say({
|
|
13029
|
+
severity: "warning",
|
|
13030
|
+
code: "quota-requests-below-wave",
|
|
13031
|
+
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`
|
|
13032
|
+
});
|
|
13033
|
+
if (rule.tokensPerMinute !== void 0 && tokens > rule.tokensPerMinute) say({
|
|
13034
|
+
severity: "warning",
|
|
13035
|
+
code: "quota-tokens-below-wave",
|
|
13036
|
+
message: `${name}: the declared wave demands at least ${String(tokens)} tokens against tokensPerMinute ${String(rule.tokensPerMinute)}: expect estimate-driven throttling inside one window`
|
|
13037
|
+
});
|
|
13038
|
+
});
|
|
13039
|
+
const severityRank = {
|
|
13040
|
+
error: 0,
|
|
13041
|
+
warning: 1,
|
|
13042
|
+
info: 2
|
|
13043
|
+
};
|
|
13044
|
+
findings.sort((a, b) => severityRank[a.severity] - severityRank[b.severity]);
|
|
13045
|
+
return {
|
|
13046
|
+
concurrency: {
|
|
13047
|
+
perRun,
|
|
13048
|
+
...perProviderCaps === void 0 ? {} : { perProvider: { ...perProviderCaps } }
|
|
13049
|
+
},
|
|
13050
|
+
budget: {
|
|
13051
|
+
...ceilingUsd === void 0 ? {} : { ceilingUsd },
|
|
13052
|
+
flatReserveUsd,
|
|
13053
|
+
lifetimeSpawnCap,
|
|
13054
|
+
childBudgetFraction,
|
|
13055
|
+
maxDepth,
|
|
13056
|
+
...orchestratorEcho === void 0 ? {} : { orchestrator: orchestratorEcho }
|
|
13057
|
+
},
|
|
13058
|
+
quota: {
|
|
13059
|
+
configured: quotaConfigured,
|
|
13060
|
+
...engine.quota?.tenant === void 0 ? {} : { tenant: engine.quota.tenant },
|
|
13061
|
+
...input.quotaRules === void 0 ? {} : { rules: input.quotaRules.length }
|
|
13062
|
+
},
|
|
13063
|
+
runLimits,
|
|
13064
|
+
spawns: spawnReports,
|
|
13065
|
+
admission: {
|
|
13066
|
+
...ceilingUsd === void 0 ? {} : { ceilingUsd },
|
|
13067
|
+
reservedForFinalizationUsd,
|
|
13068
|
+
wave,
|
|
13069
|
+
admitted,
|
|
13070
|
+
denied
|
|
13071
|
+
},
|
|
13072
|
+
exposure: {
|
|
13073
|
+
maxInFlight,
|
|
13074
|
+
...overshootOneTurnFloorUsd === void 0 ? {} : { overshootOneTurnFloorUsd },
|
|
13075
|
+
perProvider
|
|
13076
|
+
},
|
|
13077
|
+
findings
|
|
13078
|
+
};
|
|
13079
|
+
}
|
|
13080
|
+
//#endregion
|
|
13081
|
+
//#region src/engine/run-profiles.ts
|
|
13082
|
+
/**
|
|
13083
|
+
* The shipped presets (fast / standard / deep / ultra "and similar").
|
|
13084
|
+
* Data only; a review-time assertion checks the
|
|
13085
|
+
* engine has zero behavioral branches keyed on these names.
|
|
13086
|
+
*/
|
|
13087
|
+
const RUN_PROFILES = {
|
|
13088
|
+
fast: {
|
|
13089
|
+
effortByRole: {
|
|
13090
|
+
orchestrate: "low",
|
|
13091
|
+
plan: "low",
|
|
13092
|
+
summarize: "low",
|
|
13093
|
+
extract: "low"
|
|
13094
|
+
},
|
|
13095
|
+
perRunConcurrency: 16,
|
|
13096
|
+
permissionPreset: "standard",
|
|
13097
|
+
lifetimeSpawnCap: 64,
|
|
13098
|
+
maxDepth: 1
|
|
13099
|
+
},
|
|
13100
|
+
standard: {
|
|
13101
|
+
effortByRole: {
|
|
13102
|
+
orchestrate: "high",
|
|
13103
|
+
plan: "high",
|
|
13104
|
+
summarize: "low",
|
|
13105
|
+
extract: "low"
|
|
13106
|
+
},
|
|
13107
|
+
perRunConcurrency: 12,
|
|
13108
|
+
permissionPreset: "standard",
|
|
13109
|
+
lifetimeSpawnCap: 500,
|
|
13110
|
+
maxDepth: 1
|
|
13111
|
+
},
|
|
13112
|
+
deep: {
|
|
13113
|
+
effortByRole: {
|
|
13114
|
+
orchestrate: "high",
|
|
13115
|
+
plan: "high",
|
|
13116
|
+
summarize: "medium",
|
|
13117
|
+
extract: "medium"
|
|
13118
|
+
},
|
|
13119
|
+
perRunConcurrency: 8,
|
|
13120
|
+
permissionPreset: "standard",
|
|
13121
|
+
lifetimeSpawnCap: 500,
|
|
13122
|
+
maxDepth: 2
|
|
13123
|
+
},
|
|
13124
|
+
ultra: {
|
|
13125
|
+
effortByRole: {
|
|
13126
|
+
orchestrate: "max",
|
|
13127
|
+
plan: "max",
|
|
13128
|
+
summarize: "high",
|
|
13129
|
+
extract: "high"
|
|
13130
|
+
},
|
|
13131
|
+
perRunConcurrency: 8,
|
|
13132
|
+
permissionPreset: "strict",
|
|
13133
|
+
lifetimeSpawnCap: 500,
|
|
13134
|
+
maxDepth: 3
|
|
13135
|
+
}
|
|
13136
|
+
};
|
|
13137
|
+
/** Looks up a shipped RunProfile by name; undefined for unknown names. */
|
|
13138
|
+
function runProfile(name) {
|
|
13139
|
+
return RUN_PROFILES[name];
|
|
13140
|
+
}
|
|
13141
|
+
//#endregion
|
|
13142
|
+
//#region src/model/concurrency.ts
|
|
13143
|
+
/**
|
|
13144
|
+
* Per-provider concurrency keys (M4-T07): a keyed limiter beside the
|
|
13145
|
+
* router, ENGINE-scoped (keys constrain calls
|
|
13146
|
+
* across a single engine per adapter). The Appendix A default is
|
|
13147
|
+
* unlimited: an embeddable library must not surprise-throttle hosts, so
|
|
13148
|
+
* the per-run semaphore stays the only default bound and provider 429s
|
|
13149
|
+
* ride RetryPolicy; hosts with known tier limits opt in per adapter id
|
|
13150
|
+
* via createEngine concurrency.perProvider.
|
|
13151
|
+
*
|
|
13152
|
+
* This keyed limiter bounds PARALLELISM inside one engine only. Two
|
|
13153
|
+
* processes sharing one API key coordinate through the QuotaLimiter
|
|
13154
|
+
* SPI instead (RV-215, createEngine `quota`): rate and volume live
|
|
13155
|
+
* there, in shared storage; in-flight slots live here.
|
|
13156
|
+
*/
|
|
13157
|
+
var KeyedLimiter = class {
|
|
13158
|
+
semaphores = /* @__PURE__ */ new Map();
|
|
13159
|
+
constructor(caps) {
|
|
13160
|
+
for (const [key, limit] of Object.entries(caps ?? {})) this.semaphores.set(key, new Semaphore(limit));
|
|
12900
13161
|
}
|
|
12901
|
-
/**
|
|
12902
|
-
|
|
12903
|
-
|
|
12904
|
-
* cap, statsBefore fidelity) without committing any reserve; the spend
|
|
12905
|
-
* itself sits in the root ledger seed.
|
|
12906
|
-
*/
|
|
12907
|
-
recoverSettled(parentAccountScope) {
|
|
12908
|
-
this.budget.admitRecovered(0, parentAccountScope);
|
|
12909
|
-
this.childrenOf.set(parentAccountScope, (this.childrenOf.get(parentAccountScope) ?? 0) + 1);
|
|
12910
|
-
this.admittedTotal += 1;
|
|
13162
|
+
/** Queue depth for one key (0 for unlimited keys); telemetry only. */
|
|
13163
|
+
pending(key) {
|
|
13164
|
+
return this.semaphores.get(key)?.pending ?? 0;
|
|
12911
13165
|
}
|
|
12912
13166
|
/**
|
|
12913
|
-
*
|
|
12914
|
-
*
|
|
12915
|
-
*
|
|
12916
|
-
*
|
|
12917
|
-
* re-estimated).
|
|
13167
|
+
* Runs `fn` under the key's semaphore; keys without a configured cap
|
|
13168
|
+
* run unlimited (no queueing, no overhead). An aborted `signal` frees
|
|
13169
|
+
* a queued caller without a slot (the Semaphore contract), so run
|
|
13170
|
+
* cancellation drains provider queues too (v1.34.0 review P2-4).
|
|
12918
13171
|
*/
|
|
12919
|
-
|
|
12920
|
-
|
|
12921
|
-
|
|
12922
|
-
|
|
12923
|
-
this.childrenOf.set(parentAccountScope, (this.childrenOf.get(parentAccountScope) ?? 0) + 1);
|
|
12924
|
-
this.admittedTotal += 1;
|
|
13172
|
+
async withSlot(key, fn, onQueued, signal) {
|
|
13173
|
+
const semaphore = this.semaphores.get(key);
|
|
13174
|
+
if (semaphore === void 0) return fn();
|
|
13175
|
+
return semaphore.withSlot(fn, onQueued, signal);
|
|
12925
13176
|
}
|
|
12926
13177
|
};
|
|
12927
13178
|
//#endregion
|
|
13179
|
+
//#region src/model/profile-card.ts
|
|
13180
|
+
function toolNamesOf(profile) {
|
|
13181
|
+
return (profile.tools ?? []).map((entry) => {
|
|
13182
|
+
if (typeof entry === "string") return `${entry} (registered toolset)`;
|
|
13183
|
+
if ("kind" in entry && entry.kind === "tool") return entry.name;
|
|
13184
|
+
return `${entry.id}:* (tool source)`;
|
|
13185
|
+
});
|
|
13186
|
+
}
|
|
13187
|
+
/**
|
|
13188
|
+
* Renders the registry into the shared agent vocabulary card. Sorted,
|
|
13189
|
+
* deterministic, byte-stable; an empty registry renders explicitly so
|
|
13190
|
+
* the planner never guesses at unregistered agentTypes. When the engine
|
|
13191
|
+
* registers toolsets, their names render as a closing line (v1.17.0
|
|
13192
|
+
* review P1-3): those are the ONLY values valid as string entries of a
|
|
13193
|
+
* tools option, so the planner never invents a registry name.
|
|
13194
|
+
*/
|
|
13195
|
+
function profileCard(profiles, toolsets) {
|
|
13196
|
+
const toolsetNames = Object.keys(toolsets ?? {}).sort();
|
|
13197
|
+
const toolsetsLine = toolsetNames.length === 0 ? void 0 : `Registered toolsets (valid string entries of a tools option): ${toolsetNames.join(", ")}.`;
|
|
13198
|
+
const names = Object.keys(profiles ?? {}).sort();
|
|
13199
|
+
if (profiles === void 0 || names.length === 0) {
|
|
13200
|
+
const empty = "Agent profiles: none registered. Calls take no agentType.";
|
|
13201
|
+
return toolsetsLine === void 0 ? empty : `${empty}\n${toolsetsLine}`;
|
|
13202
|
+
}
|
|
13203
|
+
const lines = ["Agent profiles (agentType values):"];
|
|
13204
|
+
for (const name of names) {
|
|
13205
|
+
const profile = profiles[name];
|
|
13206
|
+
const description = profile.description ?? "no description";
|
|
13207
|
+
lines.push(`- ${name}: ${description}`);
|
|
13208
|
+
const toolNames = toolNamesOf(profile);
|
|
13209
|
+
if (toolNames.length > 0) lines.push(` tools: ${toolNames.join(", ")}`);
|
|
13210
|
+
if (profile.taskClass !== void 0) lines.push(` taskClass: ${profile.taskClass}`);
|
|
13211
|
+
if (profile.estCost !== void 0) lines.push(` estCost: ${profile.estCost.toFixed(2)} USD`);
|
|
13212
|
+
if (profile.escalation !== void 0) lines.push(` escalation: flavor ${profile.escalation.flavor ?? "A"} (opt-in)`);
|
|
13213
|
+
}
|
|
13214
|
+
if (toolsetsLine !== void 0) lines.push(toolsetsLine);
|
|
13215
|
+
return lines.join("\n");
|
|
13216
|
+
}
|
|
13217
|
+
//#endregion
|
|
13218
|
+
//#region src/runtime/permission-chain.ts
|
|
13219
|
+
/**
|
|
13220
|
+
* The layered permission chain (M3-T03): the single approval surface for
|
|
13221
|
+
* every tool dispatch, regardless of tool origin. The order is fixed and
|
|
13222
|
+
* normative: hooks -> deny rules -> ask rules -> canUseTool -> terminal
|
|
13223
|
+
* default (allow unless needsApproval, then ask). Evaluation is
|
|
13224
|
+
* short-circuit; unconfigured layers are skipped. Rules never yield
|
|
13225
|
+
* allow: allow is only ever falling through to canUseTool or the
|
|
13226
|
+
* terminal default.
|
|
13227
|
+
*
|
|
13228
|
+
* Full contract: https://docs.rulvar.com/guide/tools.
|
|
13229
|
+
* Risk presets, the argv shell matcher, domain rules, and the
|
|
13230
|
+
* audit/dry-run surface land in M5.
|
|
13231
|
+
*/
|
|
13232
|
+
/**
|
|
13233
|
+
* Merges the engine-wide config and the profile config into one chain.
|
|
13234
|
+
* Layers concatenate engine-first; since rules only deny or ask, ordering
|
|
13235
|
+
* within a layer cannot change the verdict. The
|
|
13236
|
+
* profile's canUseTool wins over the engine's (a single slot by
|
|
13237
|
+
* construction). A declared preset compiles INTO the same layers, after
|
|
13238
|
+
* the host-authored rules, never as a fifth layer (M5-T05).
|
|
13239
|
+
*/
|
|
13240
|
+
function compilePermissionChain(engine, profile) {
|
|
13241
|
+
const preset = profile?.preset === void 0 ? {
|
|
13242
|
+
deny: [],
|
|
13243
|
+
ask: []
|
|
13244
|
+
} : compilePermissionPreset(profile.preset);
|
|
13245
|
+
const deny = [
|
|
13246
|
+
...engine?.deny ?? [],
|
|
13247
|
+
...profile?.deny ?? [],
|
|
13248
|
+
...preset.deny
|
|
13249
|
+
];
|
|
13250
|
+
const ask = [
|
|
13251
|
+
...engine?.ask ?? [],
|
|
13252
|
+
...profile?.ask ?? [],
|
|
13253
|
+
...preset.ask
|
|
13254
|
+
];
|
|
13255
|
+
const canUseTool = profile?.canUseTool ?? engine?.canUseTool;
|
|
13256
|
+
return {
|
|
13257
|
+
hooks: [...engine?.hooks ?? [], ...profile?.hooks ?? []],
|
|
13258
|
+
deny,
|
|
13259
|
+
ask,
|
|
13260
|
+
...canUseTool === void 0 ? {} : { canUseTool }
|
|
13261
|
+
};
|
|
13262
|
+
}
|
|
13263
|
+
/** The command text an argv rule matches against. */
|
|
13264
|
+
function commandOf(input) {
|
|
13265
|
+
if (typeof input === "string") return input;
|
|
13266
|
+
if (typeof input === "object" && input !== null) {
|
|
13267
|
+
const command = input.command;
|
|
13268
|
+
if (typeof command === "string") return command;
|
|
13269
|
+
}
|
|
13270
|
+
}
|
|
13271
|
+
function ruleMatches(rule, toolName, risk, input) {
|
|
13272
|
+
if ("risk" in rule) {
|
|
13273
|
+
const risks = Array.isArray(rule.risk) ? rule.risk : [rule.risk];
|
|
13274
|
+
if (risks.includes("undeclared") && risk === void 0) return true;
|
|
13275
|
+
return risk !== void 0 && risks.includes(risk);
|
|
13276
|
+
}
|
|
13277
|
+
if ("domains" in rule) return false;
|
|
13278
|
+
if (!(Array.isArray(rule.tool) ? rule.tool : [rule.tool]).includes(toolName)) return false;
|
|
13279
|
+
if ("argv" in rule) {
|
|
13280
|
+
const command = commandOf(input);
|
|
13281
|
+
if (command === void 0) return false;
|
|
13282
|
+
const patterns = Array.isArray(rule.argv) ? rule.argv : [rule.argv];
|
|
13283
|
+
return lexShellCommand(command).some((segment) => !segment.unmatchable && patterns.some((pattern) => matchArgvPattern(pattern, segment.argv)));
|
|
13284
|
+
}
|
|
13285
|
+
return true;
|
|
13286
|
+
}
|
|
13287
|
+
/**
|
|
13288
|
+
* Advisory domain-rule matches for the audit payload:
|
|
13289
|
+
* reported, never enforced in the current release.
|
|
13290
|
+
*/
|
|
13291
|
+
function advisoryMatches(chain, toolName) {
|
|
13292
|
+
return [...chain.deny, ...chain.ask].filter((rule) => "domains" in rule && rule.tool === toolName);
|
|
13293
|
+
}
|
|
13294
|
+
/**
|
|
13295
|
+
* Unmatchable segments (command/process substitution, here-docs) yield
|
|
13296
|
+
* ask, ALWAYS, for any tool that has argv rules.
|
|
13297
|
+
*/
|
|
13298
|
+
function argvUnmatchableAsk(chain, toolName, input) {
|
|
13299
|
+
if (![...chain.deny, ...chain.ask].some((rule) => "argv" in rule && (Array.isArray(rule.tool) ? rule.tool : [rule.tool]).includes(toolName))) return false;
|
|
13300
|
+
const command = commandOf(input);
|
|
13301
|
+
if (command === void 0) return true;
|
|
13302
|
+
return lexShellCommand(command).some((segment) => segment.unmatchable);
|
|
13303
|
+
}
|
|
13304
|
+
/** A stub ToolContext for offline (dry-run) evaluations. */
|
|
13305
|
+
function offlineContext(toolName) {
|
|
13306
|
+
return {
|
|
13307
|
+
runId: "dry-run",
|
|
13308
|
+
spanId: `dry-run-${toolName}`,
|
|
13309
|
+
agent: { agentType: "" },
|
|
13310
|
+
cwd: process.cwd(),
|
|
13311
|
+
isolation: "none",
|
|
13312
|
+
signal: new AbortController().signal,
|
|
13313
|
+
log: () => void 0
|
|
13314
|
+
};
|
|
13315
|
+
}
|
|
13316
|
+
/**
|
|
13317
|
+
* Evaluates the chain for one dispatch, or OFFLINE against a
|
|
13318
|
+
* hypothetical call by tool name (the dry-run API: nothing executes;
|
|
13319
|
+
* shells and tests read the verdict, the
|
|
13320
|
+
* deciding layer, and the matched rule). Hooks run in deterministic
|
|
13321
|
+
* registration order; { modifiedInput } substitutes the input and
|
|
13322
|
+
* continues; the first decisive verdict wins. The returned input is what
|
|
13323
|
+
* execute receives and what the approval identity hashes (post hook
|
|
13324
|
+
* modification). Advisory domain-rule matches
|
|
13325
|
+
* ride every verdict for the audit payload.
|
|
13326
|
+
*/
|
|
13327
|
+
async function evaluatePermission(chain, tool, input, ctx) {
|
|
13328
|
+
const def = typeof tool === "string" ? {
|
|
13329
|
+
name: tool,
|
|
13330
|
+
needsApproval: false
|
|
13331
|
+
} : tool;
|
|
13332
|
+
const risk = typeof tool === "string" ? void 0 : tool.risk;
|
|
13333
|
+
const context = ctx ?? offlineContext(def.name);
|
|
13334
|
+
const advisory = advisoryMatches(chain, def.name);
|
|
13335
|
+
const withAdvisory = (verdict) => advisory.length === 0 ? verdict : {
|
|
13336
|
+
...verdict,
|
|
13337
|
+
advisory
|
|
13338
|
+
};
|
|
13339
|
+
let effective = input;
|
|
13340
|
+
for (const hook of chain.hooks) {
|
|
13341
|
+
const verdict = await hook(def.name, effective, context);
|
|
13342
|
+
if (verdict === void 0) continue;
|
|
13343
|
+
if (verdict === "allow" || verdict === "deny" || verdict === "ask") return withAdvisory({
|
|
13344
|
+
verdict,
|
|
13345
|
+
decidedBy: "hook",
|
|
13346
|
+
input: effective
|
|
13347
|
+
});
|
|
13348
|
+
effective = verdict.modifiedInput;
|
|
13349
|
+
}
|
|
13350
|
+
for (const rule of chain.deny) if (ruleMatches(rule, def.name, risk, effective)) return withAdvisory({
|
|
13351
|
+
verdict: "deny",
|
|
13352
|
+
decidedBy: "deny-rule",
|
|
13353
|
+
rule,
|
|
13354
|
+
input: effective
|
|
13355
|
+
});
|
|
13356
|
+
for (const rule of chain.ask) if (ruleMatches(rule, def.name, risk, effective)) return withAdvisory({
|
|
13357
|
+
verdict: "ask",
|
|
13358
|
+
decidedBy: "ask-rule",
|
|
13359
|
+
rule,
|
|
13360
|
+
input: effective
|
|
13361
|
+
});
|
|
13362
|
+
if (argvUnmatchableAsk(chain, def.name, effective)) return withAdvisory({
|
|
13363
|
+
verdict: "ask",
|
|
13364
|
+
decidedBy: "ask-rule",
|
|
13365
|
+
input: effective
|
|
13366
|
+
});
|
|
13367
|
+
if (chain.canUseTool !== void 0) {
|
|
13368
|
+
const verdict = await chain.canUseTool(def.name, effective, context);
|
|
13369
|
+
if (verdict === "allow") return withAdvisory({
|
|
13370
|
+
verdict: "allow",
|
|
13371
|
+
decidedBy: "canUseTool",
|
|
13372
|
+
input: effective
|
|
13373
|
+
});
|
|
13374
|
+
if (verdict === "deny") return withAdvisory({
|
|
13375
|
+
verdict: "deny",
|
|
13376
|
+
decidedBy: "canUseTool",
|
|
13377
|
+
input: effective
|
|
13378
|
+
});
|
|
13379
|
+
effective = verdict.modifiedInput;
|
|
13380
|
+
}
|
|
13381
|
+
if (def.needsApproval) return withAdvisory({
|
|
13382
|
+
verdict: "ask",
|
|
13383
|
+
decidedBy: "default",
|
|
13384
|
+
input: effective
|
|
13385
|
+
});
|
|
13386
|
+
return withAdvisory({
|
|
13387
|
+
verdict: "allow",
|
|
13388
|
+
decidedBy: "default",
|
|
13389
|
+
input: effective
|
|
13390
|
+
});
|
|
13391
|
+
}
|
|
13392
|
+
//#endregion
|
|
12928
13393
|
//#region src/orchestrator/finish-validators.ts
|
|
12929
13394
|
/**
|
|
12930
13395
|
* Deterministic host validation of the orchestrator finish result (the
|
|
@@ -18440,4 +18905,4 @@ function createSandboxBridge(ctx, options) {
|
|
|
18440
18905
|
};
|
|
18441
18906
|
}
|
|
18442
18907
|
//#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 };
|
|
18908
|
+
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, 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 };
|