@sema-agent/core 5.58.0 → 5.60.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/CHANGELOG.md +88 -0
- package/dist/brain/anthropic.js +15 -5
- package/dist/brain/errors.d.ts +18 -1
- package/dist/brain/errors.js +7 -1
- package/dist/brain/input-too-long.d.ts +57 -0
- package/dist/brain/input-too-long.js +35 -0
- package/dist/brain/route-adjudicator.d.ts +8 -1
- package/dist/brain/route-adjudicator.js +8 -1
- package/dist/brain/stream-engine.js +9 -1
- package/dist/core/auto-compaction.js +2 -2
- package/dist/core/checkpoint-store.d.ts +116 -19
- package/dist/core/checkpoint-store.js +15 -8
- package/dist/core/context-edit.d.ts +243 -41
- package/dist/core/context-edit.js +247 -32
- package/dist/core/governance-codes.d.ts +37 -10
- package/dist/core/governance-codes.js +57 -1
- package/dist/core/locked-config.d.ts +36 -4
- package/dist/core/locked-config.js +34 -1
- package/dist/core/mcp.js +10 -6
- package/dist/core/memory-engine/consolidation-driver.d.ts +6 -2
- package/dist/core/memory-engine/consolidation-driver.js +54 -5
- package/dist/core/memory-engine/consolidation.d.ts +73 -1
- package/dist/core/memory-engine/consolidation.js +21 -1
- package/dist/core/memory-engine/content-origin.d.ts +24 -2
- package/dist/core/memory-engine/content-origin.js +6 -1
- package/dist/core/memory-engine/engine.d.ts +97 -8
- package/dist/core/memory-engine/engine.js +112 -20
- package/dist/core/memory-engine/file-backend.d.ts +13 -1
- package/dist/core/memory-engine/file-backend.js +3 -0
- package/dist/core/memory-engine/index.d.ts +4 -3
- package/dist/core/memory-engine/layout.js +20 -6
- package/dist/core/memory-engine/types.d.ts +17 -0
- package/dist/core/memory.d.ts +10 -0
- package/dist/core/park-selfcheck.js +1 -0
- package/dist/core/permission-rule-consent.js +9 -5
- package/dist/core/permission-rule-model.d.ts +42 -1
- package/dist/core/permission-rule-model.js +12 -0
- package/dist/core/runner/prepare-config-doors.d.ts +22 -1
- package/dist/core/runner/prepare-config-doors.js +36 -0
- package/dist/core/runner/prepare-task.d.ts +28 -1
- package/dist/core/runner/prepare-task.js +109 -11
- package/dist/core/runner/runtask.js +45 -8
- package/dist/core/store-contracts/checkpoint-store-contract.js +32 -0
- package/dist/core/tool-policy.d.ts +74 -0
- package/dist/core/tool-policy.js +80 -1
- package/dist/core/tools.js +1 -1
- package/dist/core/trace.d.ts +36 -0
- package/dist/core/types.d.ts +172 -22
- package/dist/core/types.js +4 -3
- package/dist/core/untrusted-text.d.ts +11 -0
- package/dist/core/untrusted-text.js +1 -0
- package/dist/engine/llm/types.d.ts +21 -2
- package/dist/engine/loop/agent-loop.js +7 -1
- package/dist/engine/loop/types.d.ts +4 -1
- package/dist/index.d.ts +3 -3
- package/dist/index.js +2 -2
- package/dist/tools/fs/fs-bash.js +1 -2
- package/package.json +1 -1
- package/test/export-surface.snapshot.json +1771 -1
package/dist/core/tool-policy.js
CHANGED
|
@@ -900,6 +900,78 @@ export function coreMintedResolutionOf(d, call) {
|
|
|
900
900
|
return undefined;
|
|
901
901
|
return isAskDenyResolution(v.resolution) ? v.resolution : undefined;
|
|
902
902
|
}
|
|
903
|
+
const BIDI_CONTROL_RE = /[\u061C\u200E\u200F\u202A-\u202E\u2066-\u2069]/u;
|
|
904
|
+
const BIDI_SCAN_MAX_NODES = 5_000;
|
|
905
|
+
const BIDI_SCAN_MAX_CHARS = 1_000_000;
|
|
906
|
+
export function carriesBidiControls(value, limits) {
|
|
907
|
+
let budget = limits?.maxNodes ?? BIDI_SCAN_MAX_NODES;
|
|
908
|
+
let charBudget = limits?.maxChars ?? BIDI_SCAN_MAX_CHARS;
|
|
909
|
+
const seen = new WeakSet();
|
|
910
|
+
const scan = (s) => {
|
|
911
|
+
if (charBudget <= 0)
|
|
912
|
+
return false;
|
|
913
|
+
const window = s.length <= charBudget ? s : s.slice(0, charBudget);
|
|
914
|
+
charBudget -= window.length;
|
|
915
|
+
return BIDI_CONTROL_RE.test(window);
|
|
916
|
+
};
|
|
917
|
+
const walk = (v) => {
|
|
918
|
+
if (budget-- <= 0)
|
|
919
|
+
return false;
|
|
920
|
+
if (typeof v === "string")
|
|
921
|
+
return scan(v);
|
|
922
|
+
if (typeof v !== "object" || v === null)
|
|
923
|
+
return false;
|
|
924
|
+
if (seen.has(v))
|
|
925
|
+
return false;
|
|
926
|
+
seen.add(v);
|
|
927
|
+
if (Array.isArray(v)) {
|
|
928
|
+
for (const el of v) {
|
|
929
|
+
if (walk(el))
|
|
930
|
+
return true;
|
|
931
|
+
if (budget <= 0)
|
|
932
|
+
return false;
|
|
933
|
+
}
|
|
934
|
+
return false;
|
|
935
|
+
}
|
|
936
|
+
if (v instanceof Map) {
|
|
937
|
+
for (const [k, val] of v) {
|
|
938
|
+
if (walk(k) || walk(val))
|
|
939
|
+
return true;
|
|
940
|
+
if (budget <= 0)
|
|
941
|
+
return false;
|
|
942
|
+
}
|
|
943
|
+
return false;
|
|
944
|
+
}
|
|
945
|
+
if (v instanceof Set) {
|
|
946
|
+
for (const el of v) {
|
|
947
|
+
if (walk(el))
|
|
948
|
+
return true;
|
|
949
|
+
if (budget <= 0)
|
|
950
|
+
return false;
|
|
951
|
+
}
|
|
952
|
+
return false;
|
|
953
|
+
}
|
|
954
|
+
for (const k in v) {
|
|
955
|
+
if (budget-- <= 0)
|
|
956
|
+
return false;
|
|
957
|
+
if (!Object.prototype.hasOwnProperty.call(v, k))
|
|
958
|
+
continue;
|
|
959
|
+
if (scan(k))
|
|
960
|
+
return true;
|
|
961
|
+
if (walk(v[k]))
|
|
962
|
+
return true;
|
|
963
|
+
if (budget <= 0 || charBudget <= 0)
|
|
964
|
+
return false;
|
|
965
|
+
}
|
|
966
|
+
return false;
|
|
967
|
+
};
|
|
968
|
+
try {
|
|
969
|
+
return walk(value);
|
|
970
|
+
}
|
|
971
|
+
catch {
|
|
972
|
+
return false;
|
|
973
|
+
}
|
|
974
|
+
}
|
|
903
975
|
export async function resolveAsk(req, onAsk, signal) {
|
|
904
976
|
const r = await resolveAskArms(req, onAsk, signal);
|
|
905
977
|
if (r.action === "deny" && isAskDenyResolution(r.resolution))
|
|
@@ -954,7 +1026,14 @@ async function resolveAskArms(req, onAsk, signal) {
|
|
|
954
1026
|
settledBy: "aborted",
|
|
955
1027
|
};
|
|
956
1028
|
}
|
|
957
|
-
|
|
1029
|
+
const bidi = carriesBidiControls(presented.value) || carriesBidiControls(req.preview);
|
|
1030
|
+
const { hasBidiControls: _carried, ...bare } = req;
|
|
1031
|
+
ok = await onAsk({
|
|
1032
|
+
...bare,
|
|
1033
|
+
boundInputHash: boundInputHashOf(presented.value),
|
|
1034
|
+
args: approverView.value,
|
|
1035
|
+
...(bidi ? { hasBidiControls: true } : {}),
|
|
1036
|
+
}, signal);
|
|
958
1037
|
}
|
|
959
1038
|
catch (err) {
|
|
960
1039
|
return {
|
package/dist/core/tools.js
CHANGED
|
@@ -45,7 +45,7 @@ export function defineTool(spec, options) {
|
|
|
45
45
|
executionMode,
|
|
46
46
|
...(spec.isConcurrencySafe ? { isConcurrencySafe: spec.isConcurrencySafe } : {}),
|
|
47
47
|
...(spec.effect ? { effect: spec.effect } : {}),
|
|
48
|
-
...(spec.contentOrigin ? { contentOrigin: spec.contentOrigin } : {}),
|
|
48
|
+
...(spec.contentOrigin !== undefined ? { contentOrigin: spec.contentOrigin } : {}),
|
|
49
49
|
...(spec.egress ? { egress: true } : {}),
|
|
50
50
|
...(spec.irreversibility !== undefined ? { irreversibility: spec.irreversibility } : {}),
|
|
51
51
|
...(spec.reversibilityProbe ? { reversibilityProbe: spec.reversibilityProbe } : {}),
|
package/dist/core/trace.d.ts
CHANGED
|
@@ -729,6 +729,42 @@ export type TraceEvent = {
|
|
|
729
729
|
/** Messages dropped from THIS request view. */
|
|
730
730
|
dropped: number;
|
|
731
731
|
ts: number;
|
|
732
|
+
} | {
|
|
733
|
+
/** design/374 (G11) — the microCompact clearing machine FIRED: `clearedCount` tool-result
|
|
734
|
+
* occurrences were replaced with markers in one pass. `trigger` names the arm: `"frontier"`
|
|
735
|
+
* = the proactive request-build pass (anchored estimate crossed the edit budget),
|
|
736
|
+
* `"refusal"` = the MC-R rejection-recovery arm (provider said input-too-long), `"blocking"`
|
|
737
|
+
* = the slice-3 pre-guard arm (reserved; not emitted before slice 3). SCOPE (X1): emitted
|
|
738
|
+
* only when the design/374 machinery is enabled — the `"frontier"` arm requires
|
|
739
|
+
* `microCompact.machine: "cc"`, the `"refusal"` arm requires `microCompact.clearOnRejection`;
|
|
740
|
+
* the legacy DEFAULT machine clears silently exactly as pre-374 (its clears are ledger-less
|
|
741
|
+
* and re-fire per request, so a frame there would re-count the same occurrences and break
|
|
742
|
+
* this frame's cardinality clause). Cardinality: exactly ONE frame per firing pass — a retry
|
|
743
|
+
* chain re-sending an already-cleared view emits none. A `"refusal"` frame also marks one
|
|
744
|
+
* attempt of the shared prompt-too-long recovery budget spent (the MC-R arm's retry rides
|
|
745
|
+
* the same per-chain account the forced-compaction arm draws on — the knob doc on
|
|
746
|
+
* `RunnerDeps.microCompact` carries the full accounting). */
|
|
747
|
+
kind: "context.mc_clear";
|
|
748
|
+
version: 1;
|
|
749
|
+
taskId: string;
|
|
750
|
+
clearedCount: number;
|
|
751
|
+
/** The pass's structural savings estimate (the ≥20k gate's own number). */
|
|
752
|
+
tokensSavedEstimate: number;
|
|
753
|
+
trigger: "frontier" | "refusal" | "blocking";
|
|
754
|
+
ts: number;
|
|
755
|
+
} | {
|
|
756
|
+
/** design/374 (G11) — the MC-R rejection arm ran and DECLINED to clear: `reason` says why
|
|
757
|
+
* (`"no_candidates"` = nothing clearable beyond the keep window on the rejected projection;
|
|
758
|
+
* `"below_min_savings"` = clearable but under the 20k gate), `nextArm` names where the
|
|
759
|
+
* recovery chain goes instead (`"forced_compaction"` when the compaction arm is available,
|
|
760
|
+
* `"none"` when it is disabled/tripped — the turn will surface the provider error). Emitted
|
|
761
|
+
* only on the refusal arm; the frontier pass declines silently every request by design. */
|
|
762
|
+
kind: "context.mc_null";
|
|
763
|
+
version: 1;
|
|
764
|
+
taskId: string;
|
|
765
|
+
reason: "no_candidates" | "below_min_savings";
|
|
766
|
+
nextArm: "forced_compaction" | "none";
|
|
767
|
+
ts: number;
|
|
732
768
|
} | {
|
|
733
769
|
/** C8 — the per-turn aggregate tool-result budget capped a result (offloaded to the store, or
|
|
734
770
|
* degraded to a self-contained truncation preview when the store failed/absent). */
|
package/dist/core/types.d.ts
CHANGED
|
@@ -353,7 +353,14 @@ export interface ToolSpec<TParams extends TSchema = TSchema> {
|
|
|
353
353
|
* - `"local"` — purely local reads/computation.
|
|
354
354
|
* Classification is single-sourced with the tool definition: core built-ins declare theirs here;
|
|
355
355
|
* a HOST tool that omits it is classified fail-closed as `"external"` (unknown = external) unless
|
|
356
|
-
* the deployment exempts the name via `TaskSpec.memory.trustedTools`.
|
|
356
|
+
* the deployment exempts the name via `TaskSpec.memory.trustedTools`. A declaration BEATS that
|
|
357
|
+
* allowlist (the allowlist is the channel for UNDECLARED tools, never a lever to re-grade a tool
|
|
358
|
+
* that classified itself), and only a member of the closed vocabulary counts as one — an unreadable
|
|
359
|
+
* value classifies `"external"` rather than exempting anything.
|
|
360
|
+
*
|
|
361
|
+
* Tools core mints on the deployment's behalf get the same seat one level up: an MCP server's whole
|
|
362
|
+
* tool set is declared at its entry ({@link McpServerSpec.contentOrigin}, design/378), because the
|
|
363
|
+
* host never touches those tool objects.
|
|
357
364
|
*/
|
|
358
365
|
contentOrigin?: ToolContentOrigin;
|
|
359
366
|
/**
|
|
@@ -1299,7 +1306,11 @@ export interface McpServerSpec {
|
|
|
1299
1306
|
*/
|
|
1300
1307
|
principalHeader?: string;
|
|
1301
1308
|
};
|
|
1302
|
-
/** Optional allowlist of tool names to expose (others are dropped).
|
|
1309
|
+
/** Optional allowlist of tool names to expose (others are dropped). ORTHOGONAL to
|
|
1310
|
+
* {@link contentOrigin} and usefully paired with it (design/378 §2): the class declaration follows
|
|
1311
|
+
* the server's roster, so a high-assurance deployment that wants a CLOSED tool set writes both —
|
|
1312
|
+
* a tool a later refresh adds then lands outside this list and is simply not mounted. Existing
|
|
1313
|
+
* semantics, no new mechanism; the refresh receipt still names every added tool either way. */
|
|
1303
1314
|
allowTools?: string[];
|
|
1304
1315
|
/**
|
|
1305
1316
|
* design/99 §E23 — opt in to INBOUND elicitation for THIS server: when `true` AND a {@link RunnerDeps.onElicit}
|
|
@@ -1316,12 +1327,66 @@ export interface McpServerSpec {
|
|
|
1316
1327
|
* override is AUTHORITATIVE and may LOWER an effect (vouch a tool is `read`/`idempotent`) as well as raise it
|
|
1317
1328
|
* (`egress` / irreversible). This is the ONLY trusted way to drop an MCP tool below the fail-closed `write`
|
|
1318
1329
|
* default (design F). Folds over the server hints in prepare-task (caller > server hint > fail-closed write).
|
|
1330
|
+
*
|
|
1331
|
+
* A DIFFERENT AXIS from {@link contentOrigin}: this one is about what a call DOES (repeat-safety,
|
|
1332
|
+
* blast radius, reversibility — it feeds the approval gate); the content class is about what a call
|
|
1333
|
+
* BRINGS BACK (memory-write governance, no gate/policy/roster effect). Neither implies the other —
|
|
1334
|
+
* a read-only tool can return third-party text, and a deployment's own writer brings back nothing
|
|
1335
|
+
* external — so vouching on one axis never quietly vouches on the other.
|
|
1319
1336
|
*/
|
|
1320
1337
|
toolAxes?: Record<string, {
|
|
1321
1338
|
effect?: ToolEffect;
|
|
1322
1339
|
egress?: boolean;
|
|
1323
1340
|
irreversibility?: "always" | "never";
|
|
1324
1341
|
}>;
|
|
1342
|
+
/**
|
|
1343
|
+
* design/378 — declare {@link ToolSpec.contentOrigin} on behalf of THIS SERVER'S ENTIRE TOOL SET
|
|
1344
|
+
* (tools a mid-task refresh adds included), with the same authority and the same responsibility a
|
|
1345
|
+
* directly-mounted host tool's own declaration carries.
|
|
1346
|
+
*
|
|
1347
|
+
* WHY THE SEAT EXISTS. Without it the class keys on the MOUNT SHAPE rather than on lineage: a tool
|
|
1348
|
+
* the deployment wrote and runs itself is structurally `"external"` the moment it arrives over the
|
|
1349
|
+
* MCP protocol namespace, so every call marks the session's memory externally exposed. The
|
|
1350
|
+
* per-name channels cannot express the fact either — {@link import("./memory.js").MemorySpecInput.trustedTools}
|
|
1351
|
+
* is a per-REQUEST allowlist keyed on the MINTED name (the host would have to predict the charset
|
|
1352
|
+
* normalization) whose own definition is "the exception channel for UNDECLARED tools", and it says
|
|
1353
|
+
* nothing about tools the server adds later.
|
|
1354
|
+
*
|
|
1355
|
+
* THIS IS A TRUST DECLARATION, not a routing hint. Use it only for servers inside the deployment's
|
|
1356
|
+
* trust boundary — a process, socket or service the deployment itself runs. Declaring a THIRD-PARTY
|
|
1357
|
+
* server means treating its output as content the deployment wrote: the class names are
|
|
1358
|
+
* BOUNDARY-relative, never topological, so neither the transport kind nor the address is evidence
|
|
1359
|
+
* of lineage (a stdio child can be an untrusted package; a loopback URL can be your own service) and
|
|
1360
|
+
* core deliberately does not gate on either. The declaration covers THE PEER THIS ENTRY CONNECTS TO
|
|
1361
|
+
* — authenticating that peer (socket permissions, credentials) is the host's mounting duty.
|
|
1362
|
+
*
|
|
1363
|
+
* SEMANTICS. `"local"` ⇒ invocations no longer mark this session's memory; `"execution"` ⇒ the same,
|
|
1364
|
+
* except that {@link import("./memory.js").MemorySpecInput.execIsExternalContent} can still upgrade
|
|
1365
|
+
* the class for a strict deployment (which is why this seat takes the three-value vocabulary and not
|
|
1366
|
+
* a single "mine" flag — an execution-shaped tool mounted over MCP must stay inside that knob's
|
|
1367
|
+
* reach); `"external"` is an explicit PIN, and pins are not no-ops — a declaration beats the
|
|
1368
|
+
* `trustedTools` allowlist, so writing it forecloses the per-name exemption for this server's tools.
|
|
1369
|
+
* ABSENT ⇒ the pre-378 behavior byte for byte: the protocol namespace classifies the tools
|
|
1370
|
+
* `"external"` (fail-closed). A value outside the vocabulary is refused at the preparation door
|
|
1371
|
+
* (`config.mcp_content_class`), never folded to a class.
|
|
1372
|
+
*
|
|
1373
|
+
* COVERAGE, stated honestly: the class rides this server's own mounted tools. The cross-server
|
|
1374
|
+
* resource faces (ListMcpResourcesTool / ReadMcpResourceTool / ReadMcpResourceDirTool) aggregate
|
|
1375
|
+
* over every connected server in one call, so they stay `"external"` and still mark — over-marking,
|
|
1376
|
+
* the safe direction. A delegated child's pool is a separate static declaration surface
|
|
1377
|
+
* ({@link ToolSpec.agentToolPool} entries carry their own `contentOrigin`): a deployment handing
|
|
1378
|
+
* this server's tools to children mirrors the value there, and not mirroring it over-marks.
|
|
1379
|
+
*
|
|
1380
|
+
* TRUST SOURCE — a DEPLOYMENT-plane key. It redefines where the trust boundary runs, which puts it
|
|
1381
|
+
* on the same authority plane as {@link RunnerDeps} wiring, not on the request plane. Core sees one
|
|
1382
|
+
* `TaskSpec` and cannot tell a deployment-baseline entry from one a request supplied, so any
|
|
1383
|
+
* assembly layer that accepts REQUEST-side MCP entries must reject or strip this key from them:
|
|
1384
|
+
* "allowed to mount a server" is not "allowed to redefine the deployment's trust boundary", and a
|
|
1385
|
+
* caller-supplied `"local"` would otherwise be self-authorization around the session mark. A
|
|
1386
|
+
* single-tenant superuser surface (a host reading its own `--mcp-config` file) IS the deployment
|
|
1387
|
+
* plane and needs no such gate.
|
|
1388
|
+
*/
|
|
1389
|
+
contentOrigin?: ToolContentOrigin;
|
|
1325
1390
|
}
|
|
1326
1391
|
/**
|
|
1327
1392
|
* Definition of one A2A (agent-to-agent protocol) PEER to talk to for the duration of one task.
|
|
@@ -4830,6 +4895,11 @@ export interface ProjectMemoryLoad {
|
|
|
4830
4895
|
*/
|
|
4831
4896
|
export interface EngineNotice {
|
|
4832
4897
|
/** Stable machine-readable family, dot-namespaced. Current families:
|
|
4898
|
+
* - `"config.autocompact_window_clamped"` (design/374 slice 1b) — a model declared
|
|
4899
|
+
* `autoCompactTokens` ABOVE its physical window; the trigger-side geometry was clamped to the
|
|
4900
|
+
* physical window (an autocompact window only ever lowers the trigger) and the bad value is
|
|
4901
|
+
* announced once per prepared task; `detail: { modelId, declaredAutoCompactTokens,
|
|
4902
|
+
* physicalWindow, sessionId }`.
|
|
4833
4903
|
* - `"config.env_timeout_discarded"` — a Bash timeout knob (option or env) held a value that is not
|
|
4834
4904
|
* the value in force; `detail: { knob, raw, usedMs }`.
|
|
4835
4905
|
* - `"config.materialize_env_discarded"` — `SEMA_TOOL_MATERIALIZE_STRATEGY` held a value outside the
|
|
@@ -4850,8 +4920,9 @@ export interface EngineNotice {
|
|
|
4850
4920
|
* probe threw; MCP dispatch FAILS OPEN (revocation is a tightening face) and this announces
|
|
4851
4921
|
* once per materialization (a resume re-materializes and may announce again). `detail: { message }`
|
|
4852
4922
|
* — no sessionId (a deployment wiring fact, not session-attributed), `"operator"` audience by
|
|
4853
|
-
*
|
|
4854
|
-
*
|
|
4923
|
+
* its explicit {@link NOTICE_AUDIENCE} row (#433 made the registry total over the catalog —
|
|
4924
|
+
* no engine-minted code is audience-defaulted any more). The refusal itself
|
|
4925
|
+
* (`mcp.server_revoked`) is a tool RESULT code, not a notice.
|
|
4855
4926
|
* - `"config.models_swapped"` — `Runner.swapModels` replaced the model catalog generation
|
|
4856
4927
|
* (zero-restart model switching). `detail: { models, tiers }` — key COUNTS only, never the
|
|
4857
4928
|
* catalog itself. In-flight tasks finish on the models they resolved at prepare (natural
|
|
@@ -4860,8 +4931,11 @@ export interface EngineNotice {
|
|
|
4860
4931
|
* - `"route.fallback_to_primary"` (key↔URL pairing, `src/brain/route-adjudicator.ts`) — a
|
|
4861
4932
|
* DERIVED-leg model (role/tier/system-default resolution, never a caller-explicit one) failed
|
|
4862
4933
|
* the pairing pre-flight and the seat fell back to the primary model instead of sinking the
|
|
4863
|
-
* task; the notice is the loud half of that swap.
|
|
4864
|
-
* — `cause` is the refusal code
|
|
4934
|
+
* task; the notice is the loud half of that swap.
|
|
4935
|
+
* `detail: { seat, from, to, cause, fixHint, sessionId? }` — `cause` is the refusal code
|
|
4936
|
+
* (`route.credential_mismatch` / `route.credential_missing`); `sessionId` (#433, additive)
|
|
4937
|
+
* rides when the adjudicating seat knows its session — a correlation key only, the audience
|
|
4938
|
+
* stays `"operator"` (see the registry's entitlement-not-routability note).
|
|
4865
4939
|
* Explicitly-named models never mint this: they refuse at the brain's request gate instead.
|
|
4866
4940
|
* - `"route.base_url_changed_key_unchanged"` (key↔URL pairing) — `Runner.swapModels` moved a
|
|
4867
4941
|
* same-name entry's `baseUrl` while its Model-visible credential half (auth-bearing headers)
|
|
@@ -4906,8 +4980,10 @@ export interface EngineNotice {
|
|
|
4906
4980
|
* frame's two count keys). They are NOT redelivered (a steer aimed at a finished run must not
|
|
4907
4981
|
* fire at the next one — unlike ENGINE notes, which pend per session); the loud half of the
|
|
4908
4982
|
* #257 contract's "accepted = enqueued, not consumed" sentence;
|
|
4909
|
-
* `detail: { steer, taskId? }` / `{ followUp, taskId? }
|
|
4910
|
-
*
|
|
4983
|
+
* `detail: { steer, taskId?, sessionId? }` / `{ followUp, taskId?, sessionId? }` (`sessionId`
|
|
4984
|
+
* = #433's routing half for these two `"user"`-audience rows — see
|
|
4985
|
+
* {@link undrainedUserInputNotices}; omitted when the caller has none, never fabricated).
|
|
4986
|
+
* Per-run, at most once per family (the terminal sweep is a single site).
|
|
4911
4987
|
* **#389 (two corrections).** ① The family now fires on the INTERRUPT path too: `abort()` used
|
|
4912
4988
|
* to empty both queues before agent_end could count them, so the one loss path an operator most
|
|
4913
4989
|
* needs to hear about was the one path that stayed silent. ② On a DURABLE PARK the verdict is
|
|
@@ -4924,7 +5000,10 @@ export interface EngineNotice {
|
|
|
4924
5000
|
* notification would be the worse error); what is announced is that the knob's promise is not
|
|
4925
5001
|
* honored, so an operator can stop building on it. Once per run (the injection funnel is a
|
|
4926
5002
|
* single site, and a busy lane must not narrate the same gap once per frame);
|
|
4927
|
-
* `detail: { priority, taskId
|
|
5003
|
+
* `detail: { priority, taskId?, sessionId }` — `sessionId` (#433, ALWAYS present: the funnel
|
|
5004
|
+
* sits past prepare, which owns a session unconditionally) is a correlation key only, the
|
|
5005
|
+
* audience stays `"operator"` (the party building on the unhonored knob is whoever called
|
|
5006
|
+
* notify, not the end user). An UNKNOWN priority value is a different fact with a
|
|
4928
5007
|
* different posture — `TaskStream.notify` refuses it typed (`notify.invalid_payload`).
|
|
4929
5008
|
*
|
|
4930
5009
|
* - `"memory.session_polluted"` (design/178 §3, #324a; message mode-aware since design/336) —
|
|
@@ -4988,16 +5067,40 @@ export interface EngineNotice {
|
|
|
4988
5067
|
* `detail: { reason, subagentType?, sessionId? }` — `reason` is the same sentence the waived
|
|
4989
5068
|
* mark would have carried, neutralized/length-bounded (tool and agent-type names are
|
|
4990
5069
|
* host/model-controlled inputs).
|
|
5070
|
+
* - `"memory.content_class_declared"` (design/378) — an `McpServerSpec` entry carries an explicit
|
|
5071
|
+
* {@link McpServerSpec.contentOrigin}: the deployment declared this server's whole tool set to
|
|
5072
|
+
* be inside (or explicitly outside) its trust boundary, so the protocol namespace's structural
|
|
5073
|
+
* `"external"` no longer decides. The AUDIT line for a trust boundary an operator drew by
|
|
5074
|
+
* configuration — which is why it announces on the explicit `"external"` value too: pinning is
|
|
5075
|
+
* not a no-op (a declaration beats the `trustedTools` allowlist, so it forecloses the per-name
|
|
5076
|
+
* exemption for this server's tools). One line per DECLARED ENTRY per prepared task leg, minted
|
|
5077
|
+
* once at preparation and never per call (the declaration's whole effect is that calls stop
|
|
5078
|
+
* marking; narrating each call would trade the saved mark for equal noise). Armed on the SAME
|
|
5079
|
+
* condition as the classification it talks about — a leg with no engine-memory session and no
|
|
5080
|
+
* provenance recorder classifies nothing, so there is no posture to report and no line is
|
|
5081
|
+
* minted. `detail: { server,
|
|
5082
|
+
* contentOrigin, toolCount, execIsExternalContent?, sessionId? }` — `server` is the
|
|
5083
|
+
* host-authored entry name, neutralized/length-bounded; `toolCount` is what this entry actually
|
|
5084
|
+
* mounted (0 for a server that failed to connect — the declaration still stands and is still
|
|
5085
|
+
* disclosed); `execIsExternalContent` rides the `"execution"` value only and reports the strict
|
|
5086
|
+
* knob AS RESOLVED FOR THIS RUN, because that arm's posture depends on it. The message's closing
|
|
5087
|
+
* clause is written per value so it can never describe a posture the run does not have. The
|
|
5088
|
+
* entry's TRANSPORT is deliberately absent: it is a claim about a mutable host-owned object made
|
|
5089
|
+
* long after the dial, and D-10 already rules topology is not evidence of the lineage this line
|
|
5090
|
+
* audits — connection facts live on `MaterializedMcp.statuses` instead.
|
|
4991
5091
|
*
|
|
4992
5092
|
* - `"memory.consolidation_incomplete"` (design/376, LLM consolidation driver) — a driver run
|
|
4993
5093
|
* settled without reaching the fixpoint: `detail` names the stop reason (the closed
|
|
4994
5094
|
* `ConsolidationRunStopReason` set), cycles done, and the residue (write-failure or
|
|
4995
5095
|
* fuse-refused groups by name). Advisory: committed cycles stand (add-only, never rolled
|
|
4996
5096
|
* back); the recovery verb is re-running the host driver, which resumes the same pending run.
|
|
4997
|
-
* - `"memory.consolidation_driver_superseded"`
|
|
4998
|
-
*
|
|
4999
|
-
*
|
|
5000
|
-
*
|
|
5097
|
+
* - `"memory.consolidation_driver_superseded"` is an ERROR code, not a notice (named here only
|
|
5098
|
+
* to keep the family's spellings in one place): a concurrent driver invocation took over this
|
|
5099
|
+
* scope's run row (attempt fencing) and the losing worker's call THROWS with this `code` —
|
|
5100
|
+
* it never crosses {@link deliverEngineNotice}, is deliberately absent from
|
|
5101
|
+
* {@link ENGINE_NOTICE_CODES}/{@link NOTICE_AUDIENCE}, and sits in the non-governance
|
|
5102
|
+
* disposition table with the error dispositions. The loser MUST NOT retry into the winner's
|
|
5103
|
+
* account. A consumer diffing its own table against the catalog must not add a row for it.
|
|
5001
5104
|
*
|
|
5002
5105
|
* - `"delegation.transcript_integrity"` (subagent transcript persistence) — a durable agent row
|
|
5003
5106
|
* with a BOUND transcript sessionId met a session store that attests `not_found` for it: the
|
|
@@ -5005,8 +5108,7 @@ export interface EngineNotice {
|
|
|
5005
5108
|
* most once per (scope, handle, process) — `detail: { handle, scope? }`, scope = the resolved
|
|
5006
5109
|
* access scope of the read that found the gap — from the continuation read faces (SendMessage preflight /
|
|
5007
5110
|
* AgentTranscript's durable leg); the per-call honest refusals are unchanged, and the declared
|
|
5008
|
-
* tier is NOT auto-downgraded (declaration-制 — observation reports, it never re-adjudicates)
|
|
5009
|
-
* `detail: { handle }`.
|
|
5111
|
+
* tier is NOT auto-downgraded (declaration-制 — observation reports, it never re-adjudicates).
|
|
5010
5112
|
*
|
|
5011
5113
|
* - `"memory.consolidation_recommended"` (design/339 §2.2/§6.2) — the engine-minted per-scope
|
|
5012
5114
|
* session count crossed the consolidation thresholds (time gate open ∧ enough distinct
|
|
@@ -5039,11 +5141,15 @@ export interface EngineNotice {
|
|
|
5039
5141
|
message: string;
|
|
5040
5142
|
/** Machine-readable facts of the notice (knob names, arriving values, values in force). */
|
|
5041
5143
|
detail?: Record<string, unknown>;
|
|
5042
|
-
/** The owning session, when the notice HAS one —
|
|
5043
|
-
*
|
|
5044
|
-
*
|
|
5045
|
-
*
|
|
5046
|
-
*
|
|
5144
|
+
/** The owning session, when the notice HAS one — an ATTRIBUTION/correlation key, never a
|
|
5145
|
+
* projection permission: whether a notice may be surfaced onto that session's user-facing
|
|
5146
|
+
* stream is decided by the code's {@link NOTICE_AUDIENCE} row alone ("audience is entitlement,
|
|
5147
|
+
* not routability" — several `"operator"` rows carry a sessionId purely for correlation, and a
|
|
5148
|
+
* projector keying on presence would push deployment facts at end users). Presence only makes
|
|
5149
|
+
* routing structurally POSSIBLE; a session-less code structurally cannot be routed at all.
|
|
5150
|
+
* Mint sites do not set this: {@link deliverEngineNotice} — the ONE delivery throat — lifts a
|
|
5151
|
+
* string `detail.sessionId` here, so the typed key and the detail carriage can never disagree.
|
|
5152
|
+
* Optional and absent for process/config-scoped codes. */
|
|
5047
5153
|
sessionId?: string;
|
|
5048
5154
|
}
|
|
5049
5155
|
/** Test seam (mirrors `__resetBashTimeoutAnnouncements`): never called by production code. */
|
|
@@ -5073,11 +5179,19 @@ export declare function deliverEngineNotice(onNotice: ((notice: EngineNotice) =>
|
|
|
5073
5179
|
* a consumer routing on `code` alone must never mistake a stranded follow-up for a stranded steer, so
|
|
5074
5180
|
* the code carries exactly the semantics its name claims — the same two-key split the settled frame
|
|
5075
5181
|
* uses. Pure (the terminal sweep race window is not constructible deterministically; this seam is).
|
|
5182
|
+
*
|
|
5183
|
+
* `sessionId` (#433, additive and optional): these two codes are the `"user"` audience rows whose
|
|
5184
|
+
* subject is the END USER's own lost input, and a disclosure that cannot say WHICH session lost it
|
|
5185
|
+
* has nowhere to be delivered — the audience registry and the routing key are the two halves of one
|
|
5186
|
+
* answer. Carried in `detail` like every other session-attributed code, so {@link deliverEngineNotice}
|
|
5187
|
+
* — the one throat — lifts it to the typed top-level key and the two spellings cannot disagree.
|
|
5188
|
+
* Omitted when the caller has none (never fabricated: the message is what the run lost, and a made-up
|
|
5189
|
+
* routing key would deliver it to the wrong stream).
|
|
5076
5190
|
*/
|
|
5077
5191
|
export declare function undrainedUserInputNotices(counts: {
|
|
5078
5192
|
steer: number;
|
|
5079
5193
|
followUp: number;
|
|
5080
|
-
}, taskId?: string): EngineNotice[];
|
|
5194
|
+
}, taskId?: string, sessionId?: string): EngineNotice[];
|
|
5081
5195
|
/** Runtime dependencies shared across tasks. */
|
|
5082
5196
|
export interface RunnerDeps {
|
|
5083
5197
|
brain: Brain;
|
|
@@ -5094,7 +5208,9 @@ export interface RunnerDeps {
|
|
|
5094
5208
|
* probe must not brick every MCP call) with a once-per-MATERIALIZATION `mcp.revocation_probe_failed`
|
|
5095
5209
|
* notice (a resume re-materializes and may announce again — the standing condition is re-news at
|
|
5096
5210
|
* each fresh mount, never per-call). The notice is a deployment wiring fact: `detail: { message }`
|
|
5097
|
-
* only, no session attribution, `"operator"` audience by
|
|
5211
|
+
* only, no session attribution, `"operator"` audience by its explicit {@link NOTICE_AUDIENCE}
|
|
5212
|
+
* row (#433 made the registry total over the catalog — no engine-minted code is
|
|
5213
|
+
* audience-defaulted any more) — a
|
|
5098
5214
|
* wire projector forwards it operator-tier and needs no per-session de-duplication of its own.
|
|
5099
5215
|
*/
|
|
5100
5216
|
mcpRevocations?: {
|
|
@@ -6045,6 +6161,40 @@ export interface RunnerDeps {
|
|
|
6045
6161
|
* ~20000; set `0` or `Infinity` to disable offloading. Per-tool override via `ToolSpec.offloadThresholdChars`.
|
|
6046
6162
|
*/
|
|
6047
6163
|
toolResultThresholdChars?: number;
|
|
6164
|
+
/**
|
|
6165
|
+
* design/374 — microCompact machine-alignment knobs (EXPERIMENTAL until the slice-3 default
|
|
6166
|
+
* flip; both default OFF so a deployment that never touches this bag runs the pre-374 machine
|
|
6167
|
+
* byte for byte).
|
|
6168
|
+
*
|
|
6169
|
+
* - `machine`: which stale-tool-result clearing machine the request pipeline runs —
|
|
6170
|
+
* `"legacy"` (default; the historical keep-3 / clear-to-budget machine) or `"cc"` (the CC
|
|
6171
|
+
* 2.1.223 rejection-leg form: keep 5, ≥20k minimum-savings gate, one deep clear beyond the
|
|
6172
|
+
* keep window, CC marker bytes). ⚠️ Read the P-form warning on
|
|
6173
|
+
* {@link import("./context-edit.js").ContextEditMachine} before selecting `"cc"`: until the
|
|
6174
|
+
* slice-3 fallback re-ordering ships, the 20k gate sits in front of the only reduction while
|
|
6175
|
+
* the message-dropping guard trim still backstops — opting in is accepting that trade.
|
|
6176
|
+
* - `clearOnRejection` (MC-R, slice 2): on a provider input-too-long rejection, run ONE cheap
|
|
6177
|
+
* deterministic clear over the rejected projection (same cc machine, savings ≥20k or nothing)
|
|
6178
|
+
* and retry inside the turn BEFORE the forced-compaction recovery. Default false (X2: the
|
|
6179
|
+
* machinery lands dark; the default flips together with the machine in slice 3). Independent
|
|
6180
|
+
* of `machine` — an enabled MC-R always clears in the cc form (the rejection arm has no
|
|
6181
|
+
* budget coordinate for the legacy incremental form to stop at). BUDGET ACCOUNTING (design/374
|
|
6182
|
+
* §3.2.1, stated here because it is otherwise invisible to a deployment): a successful MC-R
|
|
6183
|
+
* clear-and-retry SPENDS one attempt of the shared prompt-too-long recovery budget (default 2
|
|
6184
|
+
* attempts per chain), so a chain that clears and is rejected AGAIN has one forced-compaction
|
|
6185
|
+
* attempt left where the knob-off chain nominally had two — the trade costs no effective
|
|
6186
|
+
* compaction pass, because the second forced-compaction call of the off chain is structurally
|
|
6187
|
+
* a no-op whenever the first one landed (the branch leaf is already a compaction entry).
|
|
6188
|
+
*
|
|
6189
|
+
* A declaration outside the closed vocabulary (a `machine` string not in the union, a
|
|
6190
|
+
* non-boolean `clearOnRejection` — JSON/env-derived config the type cannot guard) refuses the
|
|
6191
|
+
* whole prepare loudly (`code: "config.microcompact_invalid"`, no silent re-default): folding it
|
|
6192
|
+
* would run the pre-374 machine while the deployment believes it opted in.
|
|
6193
|
+
*/
|
|
6194
|
+
microCompact?: {
|
|
6195
|
+
machine?: "legacy" | "cc";
|
|
6196
|
+
clearOnRejection?: boolean;
|
|
6197
|
+
};
|
|
6048
6198
|
/**
|
|
6049
6199
|
* Two-phase prefix-cache-break detection (design/31): per turn, fingerprint the prefix and, on a
|
|
6050
6200
|
* confirmed `cacheRead` drop, emit a root-cause finding via `onError(phase:"prompt-cache")`. Cheap
|
package/dist/core/types.js
CHANGED
|
@@ -52,22 +52,23 @@ export function deliverEngineNotice(onNotice, notice) {
|
|
|
52
52
|
}
|
|
53
53
|
console.warn(notice.message);
|
|
54
54
|
}
|
|
55
|
-
export function undrainedUserInputNotices(counts, taskId) {
|
|
55
|
+
export function undrainedUserInputNotices(counts, taskId, sessionId) {
|
|
56
56
|
const tid = taskId !== undefined ? { taskId } : {};
|
|
57
|
+
const sid = sessionId !== undefined ? { sessionId } : {};
|
|
57
58
|
const tail = `accepted as "queued" were never consumed — the run ended first. They are NOT redelivered; re-send against a live run if still wanted.`;
|
|
58
59
|
const out = [];
|
|
59
60
|
if (counts.steer > 0) {
|
|
60
61
|
out.push({
|
|
61
62
|
code: "task.user_steer_undrained",
|
|
62
63
|
message: `${counts.steer} user steer(s) ${tail}`,
|
|
63
|
-
detail: { steer: counts.steer, ...tid },
|
|
64
|
+
detail: { steer: counts.steer, ...tid, ...sid },
|
|
64
65
|
});
|
|
65
66
|
}
|
|
66
67
|
if (counts.followUp > 0) {
|
|
67
68
|
out.push({
|
|
68
69
|
code: "task.user_followup_undrained",
|
|
69
70
|
message: `${counts.followUp} user follow-up(s) ${tail}`,
|
|
70
|
-
detail: { followUp: counts.followUp, ...tid },
|
|
71
|
+
detail: { followUp: counts.followUp, ...tid, ...sid },
|
|
71
72
|
});
|
|
72
73
|
}
|
|
73
74
|
return out;
|
|
@@ -393,6 +393,17 @@ export declare function defuseControlChars(text: string): string;
|
|
|
393
393
|
* Defense-in-depth, NOT a guarantee (same posture as the rest of this module).
|
|
394
394
|
*/
|
|
395
395
|
export declare function inlineUntrusted(text: string, maxLen?: number): string;
|
|
396
|
+
/** backlog #239: the per-ENTRY ceiling inside a `ProbeCause` operand family (checkpoint-store's
|
|
397
|
+
* structured probe account), sized like the descriptor's own `touchedPaths` entries (the same thing
|
|
398
|
+
* on the same card). Per entry, not per cause: that is the whole point of the structured shape —
|
|
399
|
+
* one pathological path costs only itself, where a single joined string let it consume every other
|
|
400
|
+
* entry's room. HOMED HERE (the leaf neutralizer module) rather than with the `ProbeCause` types:
|
|
401
|
+
* its two consumers — the shell probe that sanitizes at the entry boundary (tools/fs) and the
|
|
402
|
+
* descriptor builder that re-sanitizes at the persist boundary (core/checkpoint-store) — sit on
|
|
403
|
+
* opposite sides of a value-import boundary, and a value import from the shell tool back into
|
|
404
|
+
* checkpoint-store closed an ESM evaluation cycle (checkpoint-store → tool-policy → tools/fs →
|
|
405
|
+
* checkpoint-store, a latent TDZ). Both already import this module; one cap, no cycle. */
|
|
406
|
+
export declare const PROBE_CAUSE_PATH_MAX = 200;
|
|
396
407
|
/** The one body bound every "reviewer note" relay passes to {@link delimitUntrusted} — the decider's
|
|
397
408
|
* free text attached to a deny (sync `AskOutcome.reason` and the durable `ResumeOutcome` `reason`
|
|
398
409
|
* legs alike). A note is steering, not payload: unbounded it can flood the transcript/context the
|
|
@@ -568,6 +568,7 @@ export function inlineUntrusted(text, maxLen = LABEL_MAX) {
|
|
|
568
568
|
function sanitizeLabel(label) {
|
|
569
569
|
return inlineUntrusted(label, LABEL_MAX);
|
|
570
570
|
}
|
|
571
|
+
export const PROBE_CAUSE_PATH_MAX = 200;
|
|
571
572
|
export const REVIEWER_NOTE_MAX_BODY = 2048;
|
|
572
573
|
export function delimitUntrusted(label, text, maxBody) {
|
|
573
574
|
return delimitUntrustedWithClip(label, text, maxBody).text;
|
|
@@ -413,8 +413,27 @@ export interface AssistantMessage {
|
|
|
413
413
|
* ⚠️ Brain OBLIGATION (RB-482 #12, 5.1.0): stamping this field on every cut turn is part of the
|
|
414
414
|
* Brain contract — the sentinel-prose fallback readers are RETIRED, so a custom Brain that stamps
|
|
415
415
|
* only `errorMessage` prose is no longer recognized as a cut (terminal-cause.ts reads this field
|
|
416
|
-
* and nothing else). Widening note: exhaustive switches on this union gain arms as it grows.
|
|
417
|
-
|
|
416
|
+
* and nothing else). Widening note: exhaustive switches on this union gain arms as it grows.
|
|
417
|
+
*
|
|
418
|
+
* `"input_too_long"` (design/374 slice 2): the provider refused because the request INPUT alone
|
|
419
|
+
* exceeds its context limit — classified at the brain boundary from provenance-checked signals
|
|
420
|
+
* only (`brain/input-too-long.ts`: the diagnostic field of a JSON error envelope, anchored
|
|
421
|
+
* provider openings, the OpenAI `context_length_exceeded` structural code, HTTP 413, and the
|
|
422
|
+
* Anthropic in-band `model_context_window_exceeded` stop / streamed `invalid_request_error`
|
|
423
|
+
* twin). The loop's prompt-too-long recovery reads THIS seat first; the historic prose regex
|
|
424
|
+
* over `errorMessage` remains as the fallback for brains that do not stamp it (additive — the
|
|
425
|
+
* stamping obligation above extends to this member for cuts a Brain can classify structurally,
|
|
426
|
+
* but unlike the two cut kinds there is no retired fallback: prose detection still works). */
|
|
427
|
+
errorKind?: "length_empty" | "degenerate" | "input_too_long";
|
|
428
|
+
/** design/374 slice 2, the AUTHORITATIVE-NEGATIVE twin of `errorKind: "input_too_long"`: the
|
|
429
|
+
* brain's provenance-checked classifier RAN and ruled this failure NOT input-too-long (wrong
|
|
430
|
+
* status family, or the provider's own diagnostic envelope states a different error). The
|
|
431
|
+
* loop's prompt-too-long recovery treats it as a hard negative and SKIPS its prose fallback —
|
|
432
|
+
* a "prompt is too long" sentence reflected inside an unrelated error body must never drive a
|
|
433
|
+
* context-shrinking recovery (adversarial-review r1). Absent = classification could not
|
|
434
|
+
* establish provenance (prose-only gateway, truncated body, non-classifying custom Brain), and
|
|
435
|
+
* the historic prose fallback keeps its reach. Mutually exclusive with the positive stamp. */
|
|
436
|
+
inputTooLongRuledOut?: true;
|
|
418
437
|
/**
|
|
419
438
|
* design/124 tier A: this final message was PARTIALLY FINALIZED after a mid-stream connection
|
|
420
439
|
* loss/stall — the substantive streamed prefix (text and/or completed tool calls) was promoted to
|
|
@@ -39,7 +39,13 @@ function isBlankFailureContent(message) {
|
|
|
39
39
|
return message.content.every((c) => c.type === "text" ? c.text.trim() === "" : c.type === "thinking" ? c.thinking.trim() === "" : false);
|
|
40
40
|
}
|
|
41
41
|
function defaultDetectPromptTooLong(message) {
|
|
42
|
-
|
|
42
|
+
if (message.stopReason !== "error")
|
|
43
|
+
return false;
|
|
44
|
+
if (message.errorKind === "input_too_long")
|
|
45
|
+
return true;
|
|
46
|
+
if (message.inputTooLongRuledOut === true)
|
|
47
|
+
return false;
|
|
48
|
+
return PROMPT_TOO_LONG_RE.test(message.errorMessage ?? "");
|
|
43
49
|
}
|
|
44
50
|
function createPtlWithholdBuffer(emit, detect) {
|
|
45
51
|
const held = [];
|
|
@@ -161,7 +161,10 @@ export interface LoopPromptTooLongRecovery {
|
|
|
161
161
|
* Contract: must not throw or reject.
|
|
162
162
|
*/
|
|
163
163
|
recover: (messages: AgentMessage[], attempt: number) => Promise<AgentMessage[] | undefined>;
|
|
164
|
-
/** Override the prompt-too-long classifier. Default:
|
|
164
|
+
/** Override the prompt-too-long classifier. Default (design/374 slice 2): the TYPED cause first
|
|
165
|
+
* (`errorKind: "input_too_long"`, stamped by the brains from provenance-checked provider
|
|
166
|
+
* signals), then the conservative provider-message prose pattern as the fallback for brains
|
|
167
|
+
* that do not stamp it. */
|
|
165
168
|
detect?: (message: AssistantMessage) => boolean;
|
|
166
169
|
/** Max recovery retries per turn. Default: 2. */
|
|
167
170
|
maxRetries?: number;
|