@arnilo/prism 0.9.0 → 0.11.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 +50 -1
- package/README.md +19 -16
- package/dist/agent-approval.d.ts +7 -1
- package/dist/agent-approval.js +15 -6
- package/dist/agent-run-lifecycle.d.ts +2 -1
- package/dist/agent-run-lifecycle.js +20 -6
- package/dist/agent-run-state.d.ts +26 -5
- package/dist/agent-run-state.js +97 -1
- package/dist/agent-session/event-subscriber.d.ts +2 -0
- package/dist/agent-session/event-subscriber.js +3 -0
- package/dist/agent-session/session/assemble.js +165 -16
- package/dist/agent-session/session/persist.js +11 -5
- package/dist/agent-session/session/provider-round.js +54 -13
- package/dist/agent-session/session/tool-round.d.ts +2 -2
- package/dist/agent-session/session/tool-round.js +86 -23
- package/dist/agent-session/session/types.d.ts +21 -2
- package/dist/agent-session/session.d.ts +66 -4
- package/dist/agent-session/session.js +159 -18
- package/dist/checkpoint-restore.d.ts +50 -14
- package/dist/checkpoint-restore.js +104 -28
- package/dist/context-budget.d.ts +11 -0
- package/dist/context-budget.js +33 -2
- package/dist/contracts-core/agent.d.ts +26 -5
- package/dist/contracts-core/extensions.d.ts +3 -0
- package/dist/contracts-core/guardrail-packs.d.ts +8 -3
- package/dist/contracts-core/loop.d.ts +36 -0
- package/dist/contracts-core/provider.d.ts +6 -1
- package/dist/contracts-core/run-limits.d.ts +10 -1
- package/dist/contracts-core/session.d.ts +2 -1
- package/dist/contracts-protocol.d.ts +6 -4
- package/dist/contracts-run-state.d.ts +48 -6
- package/dist/contributions.d.ts +2 -1
- package/dist/contributions.js +1 -0
- package/dist/extensions.d.ts +15 -1
- package/dist/extensions.js +68 -0
- package/dist/guardrail-packs/types.d.ts +10 -0
- package/dist/guardrail-packs/validation-respect.js +16 -0
- package/dist/guardrails.d.ts +42 -1
- package/dist/guardrails.js +124 -15
- package/dist/index.d.ts +7 -7
- package/dist/index.js +4 -4
- package/dist/leases.js +32 -6
- package/dist/middleware.d.ts +1 -1
- package/dist/node/contribution-discovery.d.ts +16 -1
- package/dist/node/contribution-discovery.js +47 -0
- package/dist/node/session-store-jsonl.js +67 -17
- package/dist/run-bundle.d.ts +6 -1
- package/dist/run-bundle.js +4 -1
- package/dist/run-limits.d.ts +11 -5
- package/dist/run-limits.js +13 -0
- package/dist/session-stores.js +61 -12
- package/dist/testing/prefix-stability-conformance.d.ts +73 -1
- package/dist/testing/prefix-stability-conformance.js +158 -27
- package/dist/tools.js +10 -3
- package/dist/usage-estimation.d.ts +7 -1
- package/dist/usage-estimation.js +16 -10
- package/docs/acp.md +2 -2
- package/docs/agent-events.md +15 -10
- package/docs/agent-session-runtime.md +10 -7
- package/docs/coding-agent-tools.md +1 -1
- package/docs/coding-tools.md +7 -11
- package/docs/compaction-llm.md +2 -0
- package/docs/compaction-observational-memory.md +21 -1
- package/docs/context-and-skills.md +6 -7
- package/docs/contribution-discovery.md +13 -0
- package/docs/durable-runs.md +14 -6
- package/docs/embeddings.md +7 -1
- package/docs/execution-timeline.md +9 -2
- package/docs/extensions.md +21 -5
- package/docs/guardrails.md +16 -6
- package/docs/hooks.md +282 -0
- package/docs/impeccable.md +1 -2
- package/docs/index.md +28 -21
- package/docs/input-and-prompt-assembly.md +1 -1
- package/docs/instruction-injection.md +1 -0
- package/docs/live-testing.md +3 -2
- package/docs/memory-fabric.md +29 -0
- package/docs/middleware-hooks.md +54 -4
- package/docs/migrate-to-0.11.md +65 -0
- package/docs/migration.md +24 -0
- package/docs/node-jsonl-session-store.md +4 -3
- package/docs/operations.md +1 -1
- package/docs/options-index.md +3 -1
- package/docs/peer-dependencies.md +3 -5
- package/docs/policy-and-audit.md +15 -2
- package/docs/prefix-stability-conformance.md +82 -9
- package/docs/provider-packages.md +20 -20
- package/docs/public-contracts.md +2 -1
- package/docs/rag.md +94 -7
- package/docs/release-and-install.md +62 -59
- package/docs/runs-and-usage.md +21 -10
- package/docs/scoped-agent-memory.md +17 -9
- package/docs/scoped-memory.md +138 -0
- package/docs/session-stores.md +2 -2
- package/docs/supervisors.md +14 -6
- package/docs/testing.md +17 -9
- package/docs/tools.md +1 -1
- package/docs/wiki.md +4 -2
- package/docs/workflows.md +2 -2
- package/package.json +8 -5
- package/docs/caveman.md +0 -130
- package/docs/graft.md +0 -149
- package/docs/ponytail.md +0 -129
|
@@ -5,9 +5,46 @@
|
|
|
5
5
|
// Throws plain Error; no test runner, no network, no credentials.
|
|
6
6
|
import assert from "node:assert/strict";
|
|
7
7
|
import { createAgent } from "../agent-session/create-agent.js";
|
|
8
|
-
import { providerDone, toolCallContent } from "../provider-events.js";
|
|
8
|
+
import { providerDone, providerThinkingDelta, toolCallContent } from "../provider-events.js";
|
|
9
9
|
import { createLoadSkillTool } from "../skill-load.js";
|
|
10
10
|
import { createSkillRegistry } from "../skills.js";
|
|
11
|
+
/**
|
|
12
|
+
* Score an already-captured request list. One pass, no session, no provider call.
|
|
13
|
+
* A wrong `tailSegments` map yields a wrong number, not a throw.
|
|
14
|
+
*/
|
|
15
|
+
export function scorePrefixStability(requests, options) {
|
|
16
|
+
const first = requests[0];
|
|
17
|
+
if (requests.length < 2 || first === undefined) {
|
|
18
|
+
throw new Error(`scorePrefixStability needs at least 2 captured requests, got ${requests.length}`);
|
|
19
|
+
}
|
|
20
|
+
const minContinuity = options?.minContinuity ?? 0.95;
|
|
21
|
+
const assertOn = options?.assertOn ?? "providerPrefix";
|
|
22
|
+
const isTail = tailClassifier(options?.tailSegments ?? new Map());
|
|
23
|
+
let observed = 1;
|
|
24
|
+
let cacheableObserved = 1;
|
|
25
|
+
const resetDetails = [];
|
|
26
|
+
let previous = measureRequest(first, isTail);
|
|
27
|
+
for (let index = 1; index < requests.length; index += 1) {
|
|
28
|
+
const current = requests[index];
|
|
29
|
+
if (current === undefined)
|
|
30
|
+
continue;
|
|
31
|
+
const next = measureRequest(current, isTail);
|
|
32
|
+
const fraction = sharedPrefixFraction(previous.providerPrefix, next.providerPrefix);
|
|
33
|
+
const cacheableFraction = sharedPrefixFraction(previous.cacheablePrefix, next.cacheablePrefix);
|
|
34
|
+
observed = Math.min(observed, fraction);
|
|
35
|
+
cacheableObserved = Math.min(cacheableObserved, cacheableFraction);
|
|
36
|
+
const measured = assertOn === "cacheablePrefix" ? cacheableFraction : fraction;
|
|
37
|
+
if (measured < minContinuity)
|
|
38
|
+
resetDetails.push(projectResetDetail(index + 1, fraction, cacheableFraction, assertOn));
|
|
39
|
+
previous = next;
|
|
40
|
+
}
|
|
41
|
+
return {
|
|
42
|
+
minContinuity: observed,
|
|
43
|
+
cacheableContinuity: cacheableObserved,
|
|
44
|
+
resets: resetDetails.map((gap) => gap.request),
|
|
45
|
+
resetDetails,
|
|
46
|
+
};
|
|
47
|
+
}
|
|
11
48
|
/**
|
|
12
49
|
* Drive a real session through two staggered skill loads and assert that each
|
|
13
50
|
* provider request keeps a byte-identical leading prefix (messages **and** tool
|
|
@@ -15,6 +52,10 @@ import { createSkillRegistry } from "../skills.js";
|
|
|
15
52
|
* after the stable prefix, so the shared prefix stays intact; a host that
|
|
16
53
|
* rewrites the context block, the skill catalog, or any leading message per
|
|
17
54
|
* request fails with the offending request pair and the measured fraction.
|
|
55
|
+
* Reports both the provider-visible fraction and the same fraction with the
|
|
56
|
+
* session's tail segments removed; `assertOn` picks which one gates the run.
|
|
57
|
+
* A gap below the minimum is collected as a reset instead of failing in the
|
|
58
|
+
* loop, so `allowedResets` can permit the one boundary an assembly folds at.
|
|
18
59
|
*/
|
|
19
60
|
export async function runPrefixStabilityConformance(options) {
|
|
20
61
|
const { host, skills } = options;
|
|
@@ -28,13 +69,19 @@ export async function runPrefixStabilityConformance(options) {
|
|
|
28
69
|
bodies.push(instructions);
|
|
29
70
|
}
|
|
30
71
|
const requests = [];
|
|
72
|
+
const foldableBytes = options.foldableToolResultBytes;
|
|
73
|
+
assert.ok(foldableBytes === undefined || (Number.isSafeInteger(foldableBytes) && foldableBytes > 0), "prefix stability conformance foldableToolResultBytes must be a positive safe integer");
|
|
31
74
|
const registry = createSkillRegistry([...skills]);
|
|
32
75
|
const hostTools = host.tools && "list" in host.tools ? host.tools.list() : (host.tools ?? []);
|
|
33
76
|
const agent = createAgent({
|
|
34
77
|
...host,
|
|
35
78
|
skills: registry,
|
|
36
|
-
tools: [
|
|
37
|
-
|
|
79
|
+
tools: [
|
|
80
|
+
...hostTools,
|
|
81
|
+
createLoadSkillTool({ registry }),
|
|
82
|
+
...(foldableBytes === undefined ? [] : [createFoldableToolResultTool(foldableBytes)]),
|
|
83
|
+
],
|
|
84
|
+
provider: fixtureProvider(requests, [first.name, second.name], host.attentionCompiler === true || typeof host.attentionCompiler === "object", foldableBytes !== undefined),
|
|
38
85
|
});
|
|
39
86
|
const session = agent.createSession();
|
|
40
87
|
const [firstInput, secondInput] = options.inputs ?? ["Prefix stability turn one", "Prefix stability turn two"];
|
|
@@ -42,27 +89,81 @@ export async function runPrefixStabilityConformance(options) {
|
|
|
42
89
|
await session.run(firstInput, runOptions);
|
|
43
90
|
await session.run(secondInput, runOptions);
|
|
44
91
|
assert.equal(requests.length, 4, `prefix stability conformance expected 4 provider requests (2 per staggered turn), captured ${requests.length}`);
|
|
45
|
-
|
|
92
|
+
// The session's own map holds the exact `Message` objects `appendTailSegment` allocated, so the
|
|
93
|
+
// classification is exact rather than a heuristic over host-authored content. (`tailSegments` is
|
|
94
|
+
// runtime-session state, not part of the public `AgentSession` contract, hence the narrow above.)
|
|
95
|
+
const assertOn = options.assertOn ?? "providerPrefix";
|
|
96
|
+
// One measurement. The assertion below reads the sample; it does not serialize again.
|
|
97
|
+
const sample = scorePrefixStability(requests, { tailSegments: session.tailSegments, minContinuity, assertOn });
|
|
46
98
|
// Guard against a vacuous pass: both bodies must have been disclosed by the end.
|
|
47
|
-
const last =
|
|
99
|
+
const last = (requests.at(-1)?.messages ?? []).map((message) => JSON.stringify(message)).join("\n");
|
|
48
100
|
for (const [index, skill] of skills.entries()) {
|
|
49
101
|
assert.ok(last.includes(bodies[index] ?? ""), `prefix stability conformance: skill ${skill.name} body never reached the provider request — progressive disclosure did not expand it`);
|
|
50
102
|
}
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
103
|
+
const allowedResets = options.allowedResets ?? 0;
|
|
104
|
+
assert.ok(Number.isSafeInteger(allowedResets) && allowedResets >= 0, "prefix stability conformance allowedResets must be a non-negative safe integer");
|
|
105
|
+
const observed = sample.minContinuity;
|
|
106
|
+
const cacheableObserved = sample.cacheableContinuity;
|
|
107
|
+
const gaps = sample.resetDetails;
|
|
108
|
+
const resets = sample.resets;
|
|
109
|
+
const measuredLabel = assertOn === "cacheablePrefix" ? "previous cacheable prefix (tail segments excluded)" : "previous provider prefix";
|
|
110
|
+
const minimum = (minContinuity * 100).toFixed(1);
|
|
111
|
+
const observedResets = `resets ${formatResets(resets)} of ${requests.length - 1} request pairs`;
|
|
112
|
+
const firstGap = gaps[0];
|
|
113
|
+
if (firstGap !== undefined && gaps.length > allowedResets) {
|
|
114
|
+
const measured = firstGap.fraction;
|
|
115
|
+
assert.fail(`prefix stability conformance: request ${firstGap.request - 1} → ${firstGap.request} kept ${(measured * 100).toFixed(1)}% of the ${measuredLabel} ` +
|
|
116
|
+
`(minimum ${minimum}%), and ${gaps.length} pair(s) broke below it (${observedResets}, allowedResets ${allowedResets}). ` +
|
|
117
|
+
"Late skill bodies must append after the stable prefix; recomposed context, an in-place skill-catalog rewrite, or any leading-message mutation invalidates it. " +
|
|
118
|
+
"Pass allowedResets for the fold, compaction, or eviction the assembly performs per run, or fix the assembly so every other gap stays byte-stable.");
|
|
61
119
|
}
|
|
62
|
-
|
|
120
|
+
if (gaps.length < allowedResets) {
|
|
121
|
+
assert.fail(`prefix stability conformance: allowedResets is ${allowedResets} but only ${gaps.length} pair(s) broke below the minimum (${minimum}% of the ${measuredLabel}); ${observedResets}. ` +
|
|
122
|
+
"The fixture was supposed to invalidate the prefix at those boundaries — drop allowedResets for an append-only assembly, or check the fold trigger or eviction condition actually fired.");
|
|
123
|
+
}
|
|
124
|
+
return {
|
|
125
|
+
requests: requests.length,
|
|
126
|
+
minContinuity: observed,
|
|
127
|
+
cacheableContinuity: cacheableObserved,
|
|
128
|
+
resets,
|
|
129
|
+
resetDetails: sample.resetDetails,
|
|
130
|
+
};
|
|
131
|
+
}
|
|
132
|
+
/** One gap row. `fraction` is the metric the caller selected; `cacheableFraction` stays the tail-excluded pair. */
|
|
133
|
+
function projectResetDetail(request, providerFraction, cacheableFraction, assertOn) {
|
|
134
|
+
return {
|
|
135
|
+
request,
|
|
136
|
+
fraction: assertOn === "cacheablePrefix" ? cacheableFraction : providerFraction,
|
|
137
|
+
cacheableFraction,
|
|
138
|
+
};
|
|
63
139
|
}
|
|
64
|
-
/**
|
|
65
|
-
|
|
140
|
+
/**
|
|
141
|
+
* Deterministic reasoning block the fixture provider emits before each skill load when the host
|
|
142
|
+
* runs an attention compiler. Sized to be a real fraction of the request so the compiler's
|
|
143
|
+
* thinking stage (`thinkingKeepTurns`) has something to strip and the resulting fold is visible
|
|
144
|
+
* in the measured prefix.
|
|
145
|
+
*/
|
|
146
|
+
const FIXTURE_THINKING = "Prefix-stability fixture reasoning: the harness measures a byte-shared provider prefix, so this block exists only to give the attention-compiler thinking stage deterministic content to strip. ".repeat(17);
|
|
147
|
+
/** Runner-owned fixture tool (plan 110 Task 4): the tool-result stage needs a row worth stubbing. */
|
|
148
|
+
const PREFIX_STABILITY_BULK_TOOL_NAME = "prefix_stability_bulk";
|
|
149
|
+
function createFoldableToolResultTool(bytes) {
|
|
150
|
+
return {
|
|
151
|
+
name: PREFIX_STABILITY_BULK_TOOL_NAME,
|
|
152
|
+
description: "Deterministic bulk payload for the tool-result fold fixture.",
|
|
153
|
+
parameters: { type: "object", properties: {} },
|
|
154
|
+
execute(_args, context) {
|
|
155
|
+
const text = "x".repeat(bytes);
|
|
156
|
+
return { toolCallId: context.toolCallId, name: PREFIX_STABILITY_BULK_TOOL_NAME, value: text, content: [{ type: "text", text }] };
|
|
157
|
+
},
|
|
158
|
+
};
|
|
159
|
+
}
|
|
160
|
+
/**
|
|
161
|
+
* Fixture provider: turn 1 loads `skillNames[0]`, turn 2 loads `skillNames[1]`, everything else
|
|
162
|
+
* completes. With `reasoning` (the host runs an attention compiler) each skill-load round also
|
|
163
|
+
* carries a thinking block, so the compiler's thinking stage has real content to fold. With `bulk`
|
|
164
|
+
* the same round also calls the runner-owned bulk tool, so the tool-result stage has a foldable row.
|
|
165
|
+
*/
|
|
166
|
+
function fixtureProvider(requests, skillNames, reasoning, bulk) {
|
|
66
167
|
let call = 0;
|
|
67
168
|
return {
|
|
68
169
|
id: "prefix-stability-fixture",
|
|
@@ -72,22 +173,52 @@ function fixtureProvider(requests, skillNames) {
|
|
|
72
173
|
call += 1;
|
|
73
174
|
const skillName = skillNames[index >> 1];
|
|
74
175
|
if (index % 2 === 0 && skillName !== undefined) {
|
|
176
|
+
if (reasoning)
|
|
177
|
+
yield providerThinkingDelta(FIXTURE_THINKING);
|
|
75
178
|
yield { type: "tool_call", call: toolCallContent(`prefix-stability-${index}`, "load_skill", { name: skillName }) };
|
|
179
|
+
if (bulk) {
|
|
180
|
+
yield {
|
|
181
|
+
type: "tool_call",
|
|
182
|
+
call: toolCallContent(`prefix-stability-bulk-${index}`, PREFIX_STABILITY_BULK_TOOL_NAME, {}),
|
|
183
|
+
};
|
|
184
|
+
}
|
|
76
185
|
return;
|
|
77
186
|
}
|
|
78
187
|
yield providerDone();
|
|
79
188
|
},
|
|
80
189
|
};
|
|
81
190
|
}
|
|
82
|
-
/**
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
];
|
|
90
|
-
return
|
|
191
|
+
/**
|
|
192
|
+
* Classifies a captured message as a tail segment: by object identity first (the default builder
|
|
193
|
+
* passes the session's own `Message` objects through), then by serialized equality for builders
|
|
194
|
+
* that clone messages. Takes the pre-serialized fragment so each message is serialized once.
|
|
195
|
+
*/
|
|
196
|
+
function tailClassifier(tailSegments) {
|
|
197
|
+
const identities = new Set(tailSegments.values());
|
|
198
|
+
const values = new Set([...identities].map((message) => JSON.stringify(message)));
|
|
199
|
+
return (message, fragment) => identities.has(message) || values.has(fragment);
|
|
200
|
+
}
|
|
201
|
+
/**
|
|
202
|
+
* Provider-visible payload only — messages plus the tool schema fields sent on the wire — measured
|
|
203
|
+
* twice: whole, and with tail segments dropped. One JSON fragment per message/tool so a structural
|
|
204
|
+
* array boundary never reads as a byte divergence: an appended message list stays an exact prefix
|
|
205
|
+
* of the next request.
|
|
206
|
+
*/
|
|
207
|
+
function measureRequest(request, isTail) {
|
|
208
|
+
const toolParts = (request.tools ?? []).map((tool) => JSON.stringify({ name: tool.name, description: tool.description, parameters: tool.parameters }));
|
|
209
|
+
const providerParts = [...toolParts];
|
|
210
|
+
const cacheableParts = [...toolParts];
|
|
211
|
+
for (const message of request.messages) {
|
|
212
|
+
const fragment = JSON.stringify(message);
|
|
213
|
+
providerParts.push(fragment);
|
|
214
|
+
if (!isTail(message, fragment))
|
|
215
|
+
cacheableParts.push(fragment);
|
|
216
|
+
}
|
|
217
|
+
return { providerPrefix: providerParts.join("\n"), cacheablePrefix: cacheableParts.join("\n") };
|
|
218
|
+
}
|
|
219
|
+
/** Bracket form for reset lists, e.g. `[3]` or `[3, 4]`. */
|
|
220
|
+
function formatResets(resets) {
|
|
221
|
+
return `[${resets.join(", ")}]`;
|
|
91
222
|
}
|
|
92
223
|
/** Byte-shared prefix as a fraction of the previous request, so a shrink is a cache miss. */
|
|
93
224
|
function sharedPrefixFraction(previous, next) {
|
package/dist/tools.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { isJsonObject } from "./config.js";
|
|
2
|
-
import { GuardrailError, runGuardrails } from "./guardrails.js";
|
|
2
|
+
import { GuardrailError, guardrailRefusalText, runGuardrails } from "./guardrails.js";
|
|
3
3
|
import { assertIdentityActive, assertIdentityMatchesOwnership, ownershipFromIdentity } from "./identity.js";
|
|
4
4
|
import { createId } from "./ids.js";
|
|
5
5
|
import { errorToErrorInfo, redactRunLedgerRecord, redactSecrets } from "./redaction.js";
|
|
@@ -133,7 +133,7 @@ export async function dispatchToolCall(options) {
|
|
|
133
133
|
if (inputGuards.terminal) {
|
|
134
134
|
if (inputGuards.terminal.action !== "block")
|
|
135
135
|
throw new GuardrailError(inputGuards.terminal);
|
|
136
|
-
return blocked(mediatedCall, options.context, "guardrail_blocked", { message:
|
|
136
|
+
return blocked(mediatedCall, options.context, "guardrail_blocked", { message: guardrailBlockMessage(inputGuards.terminal) }, options, startedAt);
|
|
137
137
|
}
|
|
138
138
|
const tool = options.registry.get(mediatedCall.name);
|
|
139
139
|
const postcheck = await checkCall(mediatedCall, options, startedAt);
|
|
@@ -240,7 +240,7 @@ export async function dispatchToolCall(options) {
|
|
|
240
240
|
throw new GuardrailError(outputGuards.terminal);
|
|
241
241
|
if (effect)
|
|
242
242
|
return finishUnknownEffect(effect, mediatedCall, context, options, startedAt);
|
|
243
|
-
return blocked(mediatedCall, context, "guardrail_blocked", { message:
|
|
243
|
+
return blocked(mediatedCall, context, "guardrail_blocked", { message: guardrailBlockMessage(outputGuards.terminal) }, options, startedAt);
|
|
244
244
|
}
|
|
245
245
|
if (effect && mediatedResult.error)
|
|
246
246
|
return finishUnknownEffect(effect, mediatedCall, context, options, startedAt);
|
|
@@ -450,6 +450,13 @@ function isSuspended(error) {
|
|
|
450
450
|
function isLoopStateError(error) {
|
|
451
451
|
return typeof error?.code === "string" && error.code.startsWith("ERR_PRISM_LOOP_");
|
|
452
452
|
}
|
|
453
|
+
/**
|
|
454
|
+
* Plan 104 T3/T4: the model-visible refusal line for a terminal guardrail decision. `guardrailRefusalText`
|
|
455
|
+
* names a compiled pack rule (bounded, redacted); any other guardrail keeps the neutral stage text.
|
|
456
|
+
*/
|
|
457
|
+
function guardrailBlockMessage(record) {
|
|
458
|
+
return (guardrailRefusalText(record) ?? (record.stage === "tool_output" ? "Tool result blocked by guardrail" : "Tool call blocked by guardrail"));
|
|
459
|
+
}
|
|
453
460
|
function isDelegationSuspended(error) {
|
|
454
461
|
return error?.code === "ERR_PRISM_DELEGATION_SUSPENDED";
|
|
455
462
|
}
|
|
@@ -19,7 +19,13 @@ export interface ModelFamilyTokens {
|
|
|
19
19
|
readonly perMessageOverhead: number;
|
|
20
20
|
readonly confidence: TokenEstimateConfidence;
|
|
21
21
|
}
|
|
22
|
-
/** Per-family chars/token tables (plan 091 Task 1). Estimates only, never billing.
|
|
22
|
+
/** Per-family chars/token tables (plan 091 Task 1). Estimates only, never billing.
|
|
23
|
+
*
|
|
24
|
+
* Deep-frozen — each row, then the table. `Readonly<Record<…>>` is compile-time only, and a
|
|
25
|
+
* runtime write to a nested row silently changes token accounting: an under-counted input
|
|
26
|
+
* estimate is what `maxInputTokens`/`maxCost` are checked against, and a replaced row makes the
|
|
27
|
+
* estimate `NaN`, which compares false. Recalibration is a source change plus the live calibration
|
|
28
|
+
* leg (`scripts/usage-calibration-live.test.mjs`), never a runtime override. */
|
|
23
29
|
export declare const MODEL_FAMILY_TOKENS: Readonly<Record<ModelFamily, ModelFamilyTokens>>;
|
|
24
30
|
/** Resolve a model id (e.g. `"claude-sonnet-4.5"`), provider id, or family name
|
|
25
31
|
* to a table key. Unmatched input is `"unknown"` — never a throw. */
|
package/dist/usage-estimation.js
CHANGED
|
@@ -2,16 +2,22 @@
|
|
|
2
2
|
const CJK_CHARS_PER_TOKEN = 1.5;
|
|
3
3
|
/** Fenced code tokenizes worse than prose: code ratio = prose ratio * this factor. */
|
|
4
4
|
const CODE_RATIO_FACTOR = 0.88;
|
|
5
|
-
/** Per-family chars/token tables (plan 091 Task 1). Estimates only, never billing.
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
}
|
|
5
|
+
/** Per-family chars/token tables (plan 091 Task 1). Estimates only, never billing.
|
|
6
|
+
*
|
|
7
|
+
* Deep-frozen — each row, then the table. `Readonly<Record<…>>` is compile-time only, and a
|
|
8
|
+
* runtime write to a nested row silently changes token accounting: an under-counted input
|
|
9
|
+
* estimate is what `maxInputTokens`/`maxCost` are checked against, and a replaced row makes the
|
|
10
|
+
* estimate `NaN`, which compares false. Recalibration is a source change plus the live calibration
|
|
11
|
+
* leg (`scripts/usage-calibration-live.test.mjs`), never a runtime override. */
|
|
12
|
+
export const MODEL_FAMILY_TOKENS = Object.freeze({
|
|
13
|
+
anthropic: Object.freeze({ charsPerToken: 3.7, perMessageOverhead: 4, confidence: "medium" }),
|
|
14
|
+
openai: Object.freeze({ charsPerToken: 5.0, perMessageOverhead: 3, confidence: "medium" }),
|
|
15
|
+
google: Object.freeze({ charsPerToken: 3.9, perMessageOverhead: 4, confidence: "medium" }),
|
|
16
|
+
deepseek: Object.freeze({ charsPerToken: 3.8, perMessageOverhead: 4, confidence: "medium" }),
|
|
17
|
+
"openrouter-generic": Object.freeze({ charsPerToken: 4.4, perMessageOverhead: 4, confidence: "medium" }),
|
|
18
|
+
mistral: Object.freeze({ charsPerToken: 3.9, perMessageOverhead: 3, confidence: "medium" }),
|
|
19
|
+
unknown: Object.freeze({ charsPerToken: 3.5, perMessageOverhead: 6, confidence: "low" }),
|
|
20
|
+
});
|
|
15
21
|
/** Model-id patterns per family. Family names themselves also resolve (see `resolveModelFamily`). */
|
|
16
22
|
const FAMILY_PATTERNS = [
|
|
17
23
|
["anthropic", /claude|anthropic/i],
|
package/docs/acp.md
CHANGED
|
@@ -57,7 +57,7 @@ In-stream `SessionUpdate`s:
|
|
|
57
57
|
| `permission_denied` lifecycle | `tool_call_update` status `failed` (never raw args; synthesized id `prism:denied:<approvalId>` when no `toolCallId`) |
|
|
58
58
|
| `configuration_changed` lifecycle | `config_option_update` with the full current set, per streaming session |
|
|
59
59
|
| Plan lifecycle (F5, UNSTABLE-gated) | `plan_changed` → `plan_update` with `plan: { type: "items", planId = planPath, entries: [{ content, priority: "medium", status }] }` — the complete entry list per update (client replaces its plan wholesale); `plan_removed` → `plan_removed` with `planId = planPath`. Emitted only when the client advertised `ClientCapabilities.plan`; mapper stays capability-agnostic (gate in the agent wiring). Entries come from `writeCodingPlanFile`'s `onEvent` (parsed via `parseCodingPlanTodos`) or host-emitted through their `CodingLifecycleEmitter`; text passes the shared redactor and byte caps. |
|
|
60
|
-
| Subagent lifecycle | `subagent_started` / `subagent_stopped` → `agent_message_chunk` with only redacted child/delegation ids and terminal status. Wire `observeSupervisorLifecycle()` to emit them; child inputs, outputs, paths, and error text stay absent. |
|
|
60
|
+
| Subagent lifecycle | `subagent_started` / `subagent_stopped` → `agent_message_chunk` with only redacted child/delegation ids and terminal status. Wire `observeSupervisorLifecycle()` to emit them; child inputs, outputs, paths, and error text stay absent. The coding event's opt-in `failure` / `recovery` fields are host-side only — this mapping reads ids, depth, and status. |
|
|
61
61
|
| Session title (F6) | `sessions.title({ sessionId, prompt, signal })` resolves on `session/prompt`; a defined value differing from the last emitted title produces `session_info_update` with `{ sessionUpdate: "session_info_update", title }`. Best-effort: `undefined` or a throw means no title and no update (requests never fail on titles); the host owns title storage. Titles pass the shared redactor and are truncated at `maxTextBytes`/`maxEventBytes`. |
|
|
62
62
|
| Slash commands (F9) | `commands.list({ sessionId, signal })` on `session/new`/`load`/`resume` produces `available_commands_update` with `{ name, description, input?: { hint } }` (SDK `AvailableCommand`; description is required). Names/descriptions/hints pass the shared redactor and `maxTextBytes`; the list is sliced at `acpCommandsPerUpdate`. Best-effort: a throw or non-array omits the update (session start never fails on commands). |
|
|
63
63
|
| Session mode/config switch | `current_mode_update` / `config_option_update` |
|
|
@@ -121,7 +121,7 @@ const agent = createPrismAcpAgent({
|
|
|
121
121
|
|
|
122
122
|
### Persistence and ownership
|
|
123
123
|
|
|
124
|
-
- **Active-run recovery (0.2.6, plan 026 Task 5).** When `sessionStore` and the `recovery` seam (checkpoints + leases + ownerId, all three together) are wired, the agent records a bounded `activeRun` reference on `PersistedAcpSession` while a durable run is live (first run event → `running`, suspension → `suspended` + version, finish/deny/error → `terminal`; frozen 512-byte cap; advisory only — the authoritative status is re-queried from `AgentRunLifecycle.status`). After a restart, `restore` re-attaches the ref to the live session and hosts re-resolve it with `createAcpRunRecovery` (exported from `@arnilo/prism-ag-ui/acp`): suspended runs report their pending approval ids and durable version, terminal runs report terminal, and unprovable in-flight streams report `unknown` — the prompt is never restarted automatically. Durable cancellation (`recovery.cancel`) is ownership/version/fence checked, terminal/idempotent, aborts no unrelated run, and never replays a pending/dispatched tool: a cancelled run reports `cancelled` and must not be resumed. `session/cancel` on a live agent aborts the controller (0.2.5 parity) and, for restored runs, writes the durable marker under the session's ownership. Cancel markers live in `prism.coding-agent.cancel.v1` (schemaVersion 1, CAS + lease fenced). A host-side terminal whose managed process is unattestable after restart reports `unknown` (exitCode null); the agent never fabricates an exit or replays input (`terminal-client`).
|
|
124
|
+
- **Active-run recovery (0.2.6, plan 026 Task 5).** When `sessionStore` and the `recovery` seam (checkpoints + leases + ownerId, all three together) are wired, the agent records a bounded `activeRun` reference on `PersistedAcpSession` while a durable run is live (first run event → `running`, suspension → `suspended` + version, finish/deny/error → `terminal`; frozen 512-byte cap; advisory only — the authoritative status is re-queried from `AgentRunLifecycle.status`). After a restart, `restore` re-attaches the ref to the live session and hosts re-resolve it with `createAcpRunRecovery` (exported from `@arnilo/prism-ag-ui/acp`): suspended runs report their pending approval ids and durable version, terminal runs report terminal, and unprovable in-flight streams report `unknown` — the prompt is never restarted automatically. Durable cancellation (`recovery.cancel`) is ownership/version/fence checked, terminal/idempotent, aborts no unrelated run, and never replays a pending/dispatched tool: a cancelled run reports `cancelled` and must not be resumed. `session/cancel` on a live agent aborts the controller (0.2.5 parity) and, for restored runs, writes the durable marker under the session's ownership; that write is not tied to the connection, so it lands even when the client disconnects immediately after `session/cancel`; `session/close` likewise does not abort a durable cancel write already in flight — it stays bounded by the cancel-lease TTL. Cancel markers live in `prism.coding-agent.cancel.v1` (schemaVersion 1, CAS + lease fenced). A host-side terminal whose managed process is unattestable after restart reports `unknown` (exitCode null); the agent never fabricates an exit or replays input (`terminal-client`).
|
|
125
125
|
|
|
126
126
|
- **Without the durability seam the agent never persists `modeId`/`configValues`.** Defaults are recomputed per session from the `modes`/`configOptions` seams — a fresh `session/new`, `load`, or `resume` always starts from `defaultModeId` / option `defaultValue`, and the agent's per-session registry is in-memory only. Persisting mode/config across sessions is a **host** decision, and host-side persistence MUST be ownership-scoped.
|
|
127
127
|
- **Host persistence MUST key by `sessions.ownership`.** `authorize` binds transport identity to ownership; a host store that persists `modeId`/`configValues` must refuse any restore whose stored ownership differs from the current session's ownership — a `sessionId` alone is never a sufficient key (session ids may collide across tenants). A cross-tenant restore rejects with `ERR_PRISM_ACP_INPUT` and never returns the other tenant's mode/config.
|
package/docs/agent-events.md
CHANGED
|
@@ -10,7 +10,7 @@ Events are emitted by the runtime and by loops through `LoopContext.emit`, both
|
|
|
10
10
|
|
|
11
11
|
## When to use it
|
|
12
12
|
|
|
13
|
-
Subscribe via `session.stream()` for a single owned run, or `session.subscribe()` when a host needs a long-lived observer across runs: render streamed assistant text in a UI, react to tool execution, drive observability/telemetry, or audit artifact validation outcomes. Do not parse provider stream events directly for these — `AgentEvent` is the stable, normalized surface across providers and loops.
|
|
13
|
+
Subscribe via `session.stream()` for a single owned run, or `session.subscribe({ acrossRuns: true })` when a host needs a long-lived observer across runs (a default `session.subscribe()` is run-scoped: run end — finish, suspension, or denial — closes it): render streamed assistant text in a UI, react to tool execution, drive observability/telemetry, or audit artifact validation outcomes. Do not parse provider stream events directly for these — `AgentEvent` is the stable, normalized surface across providers and loops.
|
|
14
14
|
|
|
15
15
|
Do not use live `session.subscribe()` for cross-replica reconnect — use durable `AgentEventSource` below. Live subscribe remains process-local.
|
|
16
16
|
|
|
@@ -97,7 +97,7 @@ Agent / turn / message events:
|
|
|
97
97
|
| Variant | Fields |
|
|
98
98
|
| --- | --- |
|
|
99
99
|
| `agent_started` | `sessionId`, `runId` |
|
|
100
|
-
| `agent_finished` | `sessionId`, `runId`, `usage?: Usage` (aggregate of all usage-bearing provider turns), `finishReason?: "turn_limit" \| "token_limit" \| "refusal"` (why a limit/ceiling ended the run cleanly — F4; absent = natural end) |
|
|
100
|
+
| `agent_finished` | `sessionId`, `runId`, `usage?: Usage` (aggregate of all usage-bearing provider turns), `finishReason?: "turn_limit" \| "token_limit" \| "refusal" \| "host_policy" \| "hook_limit"` (why a limit/ceiling/hook cap ended the run cleanly — F4, plan 106 R1; absent = natural end) |
|
|
101
101
|
| `agent_suspended` | `sessionId`, `runId`, redacted `interruption`, checkpoint `version`; no tool side effect has started. |
|
|
102
102
|
| `agent_resumed` | `sessionId`, `runId`, checkpoint `version`. |
|
|
103
103
|
| `agent_denied` | `sessionId`, `runId`, redacted `interruption`, checkpoint `version`; no tool side effect runs. |
|
|
@@ -110,9 +110,9 @@ Agent / turn / message events:
|
|
|
110
110
|
|
|
111
111
|
Adapters should call `createDelegatedAgentStep({ sessionId, runId, adapterId, externalConversationId, stepIndex, state, kind, usage })` rather than forwarding external JSON. The constructor allow-lists fields and fails closed on malformed or oversized identifiers/counters.
|
|
112
112
|
|
|
113
|
-
Coding hosts call `observeSupervisorLifecycle(supervisor, { onEvent, delegatedAgentStep })` to turn supervisor milestones into `subagent_started` / `subagent_stopped` coding lifecycle events. Both carry only redacted `childId`, `delegationId`, and `depth`; stopped events add terminal `AgentRunStatus`. Supplying `delegatedAgentStep` emits the bounded `delegated_agent_step` records AG-UI already maps. Child inputs, outputs, paths, and delegation error text never cross either bridge.
|
|
113
|
+
Coding hosts call `observeSupervisorLifecycle(supervisor, { onEvent, delegatedAgentStep })` to turn supervisor milestones into `subagent_started` / `subagent_stopped` coding lifecycle events. Both carry only redacted `childId`, `delegationId`, and `depth`; stopped events add terminal `AgentRunStatus`. Two independent opt-ins extend the stopped event without changing the default: `includeFailure: true` attaches `failure: { reason, limit?, stopReason? }` to the stop a `child_failed` produced — the supervisor-redacted reason truncated to `DEFAULT_LIFECYCLE_MAX_REASON_BYTES`, the fired `RunLimitName`, and the child's stop reason (`delegation_finished` and `delegation_rejected` never carry it) — and `includeRecovery: true` attaches the child's `summary()` row as `recovery: { attempts, retries, failures, failureRadius, outcome }`, omitted when the source exposes no `summary()`. Supplying `delegatedAgentStep` emits the bounded `delegated_agent_step` records AG-UI already maps. Child inputs, outputs, paths, and delegation error text never cross either bridge: with both options off the stopped event is byte-identical to before, and the opt-in failure fields are counts, enums, and the already-redacted reason.
|
|
114
114
|
|
|
115
|
-
Supervisor child reporting is opt-in per child (`SupervisorChild.policy.report` ceiling; a request can only lower it). With `report: "milestones"` the supervisor publishes `child_milestone` (`childId`, `delegationId`, `depth`, `turn`, redacted `childEvent`) at the configured `milestone.everyTurns` cadence or host predicate; with `report: "stream"` it publishes `delegation_child_event` for every per-turn provider/tool/turn child event (never per-token `message_delta`). Both are redacted, count/byte-capped, and rate-coalesced (`delegation_child_events_coalesced` reports dropped events); the cap marker is `delegation_child_events_capped`. `child_failed` carries failure attribution for any child that died on an error or a limit: the redacted `reason`, the terminal `status`/`stopReason`, and the plan-086/087 `RunLimitBreach` in `limit` when a configured ceiling fired. Host cancels, policy denials, and hook rejections are not failures and never emit it. Hosts that want recovery counters rather than events read `supervisor.summary()` (`attempts`, `retries`, `failures`, `failureRadius`, `outcome` per child). Child events stay on the supervisor stream unless the host passes `childEventSink`, which receives the identical payload tagged with `child: { childId, delegationId, depth }` (`ChildEventOrigin`) for routing onto a parent session stream; they are not native `AgentEvent`s of the parent session, and hosts that surface them there re-attach the parent `sessionId`/`runId` themselves if needed.
|
|
115
|
+
Supervisor child reporting is opt-in per child (`SupervisorChild.policy.report` ceiling; a request can only lower it). With `report: "milestones"` the supervisor publishes `child_milestone` (`childId`, `delegationId`, `depth`, `turn`, redacted `childEvent`) at the configured `milestone.everyTurns` cadence or host predicate; with `report: "stream"` it publishes `delegation_child_event` for every per-turn provider/tool/turn child event (never per-token `message_delta`). Both are redacted, count/byte-capped, and rate-coalesced (`delegation_child_events_coalesced` reports dropped events); the cap marker is `delegation_child_events_capped`. `child_failed` carries failure attribution for any child that died on an error or a limit: the redacted `reason`, the terminal `status`/`stopReason`, and the plan-086/087 `RunLimitBreach` in `limit` when a configured ceiling fired, plus that death's exhaustion attribution (`consumed`, `closestOtherAxes`, `recentToolCalls`, the payload the child's own `budget_exhausted` event and `AgentRunResult.attribution` carry). Host cancels, policy denials, and hook rejections are not failures and never emit it. Hosts that want recovery counters rather than events read `supervisor.summary()` (`attempts`, `retries`, `failures`, `failureRadius`, `outcome` per child), or pass `includeRecovery` to the lifecycle bridge, which attaches that row to each stopped event. Child events stay on the supervisor stream unless the host passes `childEventSink`, which receives the identical payload tagged with `child: { childId, delegationId, depth }` (`ChildEventOrigin`) for routing onto a parent session stream; they are not native `AgentEvent`s of the parent session, and hosts that surface them there re-attach the parent `sessionId`/`runId` themselves if needed.
|
|
116
116
|
|
|
117
117
|
`message_delta.content.type === "tool_call_delta"` carries `{ index, id?, name?, argumentsText? }`. Treat it as a streaming fragment. The runtime reconstructs and persists a final `tool_call` before executing tools. Deltas missing `id`/`name` at stream end fail the provider turn with `ErrorInfo.code: "incomplete_delta"` (typed `ProviderTransportError`); they never throw a bare `Error`. Malformed JSON with id+name present recovers as a blocked tool result (`invalid_json_arguments`) instead.
|
|
118
118
|
|
|
@@ -124,7 +124,7 @@ Tool execution events:
|
|
|
124
124
|
| `tool_execution_progress` | `sessionId`, `runId`, `toolCallId`, `name`, `progress?`, `metadata?` |
|
|
125
125
|
| `tool_execution_finished` | `sessionId`, `runId`, `result: ToolResult`, `metadata: ToolExecutionMetadata` |
|
|
126
126
|
| `tool_execution_error` | `sessionId`, `runId`, `call: ToolCallContent`, `error: ErrorInfo`, `metadata: ToolExecutionMetadata` |
|
|
127
|
-
| `tool_execution_blocked` | `sessionId`, `runId`, `toolCallId`, `name`, `reason: string
|
|
127
|
+
| `tool_execution_blocked` | `sessionId`, `runId`, `toolCallId`, `name`, `reason: string` (machine code, e.g. `guardrail_blocked`), `error: ErrorInfo` (model-visible text — for a pack rule `Blocked by guardrail rule pack:<pack>/<rule>`, bounded and redacted), `metadata: ToolExecutionMetadata` |
|
|
128
128
|
| `tool_narrowing_clamped` | `sessionId`, `runId`, `turn`, `dropped: readonly string[]` (names the host returned outside the run grant; no tool args) |
|
|
129
129
|
|
|
130
130
|
Guardrail events:
|
|
@@ -197,11 +197,14 @@ value when the adapter saw a native reason.
|
|
|
197
197
|
| `unknown` | Unmapped or absent native reason |
|
|
198
198
|
|
|
199
199
|
`provider_turn_finished.metadata.budgets` is an O(1) snapshot from the run limit tracker:
|
|
200
|
-
`{ inputTokens?, inputCap?, runInputBudget?, runInputUsed, turns, maxTurns }` —
|
|
201
|
-
|
|
202
|
-
against `limits.maxInputTokens`, and provider turns against `limits.maxTurns` (`null` when
|
|
203
|
-
|
|
204
|
-
|
|
200
|
+
`{ inputTokens?, inputTokensSource?, inputCap?, runInputBudget?, runInputUsed, turns, maxTurns }` —
|
|
201
|
+
current-turn charged input tokens against the resolved per-request input cap, cumulative run input
|
|
202
|
+
against `limits.maxInputTokens`, and provider turns against `limits.maxTurns` (`null` when disabled).
|
|
203
|
+
`inputTokensSource` is `"reported"` or `"estimated"` and is absent together with `inputTokens`; what
|
|
204
|
+
produces each is documented in
|
|
205
|
+
[Runs and usage § Automatic fallback](runs-and-usage.md#automatic-fallback-agentconfigusageestimation).
|
|
206
|
+
Optional fields are absent when the provider reported no usage or no input cap can be derived; hosts
|
|
207
|
+
that ignore the fields are unaffected.
|
|
205
208
|
|
|
206
209
|
`provider_turn_started` / `provider_turn_finished` metadata includes `tools: { count, idsHash }` for the
|
|
207
210
|
effective menu sent on that request (after run scoping, per-turn `toolNarrowing`, and disclosure).
|
|
@@ -298,6 +301,8 @@ for await (const event of session.stream("draft", { loop: { strategy: "generate-
|
|
|
298
301
|
|
|
299
302
|
## Extension and configuration notes
|
|
300
303
|
|
|
304
|
+
Extension packages can subscribe to lifecycle events on the extension bus; `forwardAgentEvents(session.subscribe(), kernel.events)` maps this stream onto that bus as read-only notifications (`agent_started` → `before_agent_start`, turns → `turn`, tool execution → `tool_call`/`tool_result`). See [Extension kernel and event bus](extensions.md) and [Hooks](hooks.md).
|
|
305
|
+
|
|
301
306
|
- All events flow through `redactAgentEvent(event, activeRedactor)` before subscribers observe them. Configure `AgentConfig.redactor` / `RunOptions.redactor` via `createSecretRedactor([...knownSecretStrings])` so secret values are redacted in `message` content, `errors[].message`, `metadata`, and artifact `result`/`failure` payloads.
|
|
302
307
|
- The artifact variants are emitted only by `generateValidateReviseLoop`. `singleShotLoop` (the default when no `AgentConfig.loop` / `RunOptions.loop` is set) emits zero artifact events. See [Agent loops](agent-loops.md).
|
|
303
308
|
- Subscribers are in-process; the broadcaster is in-memory and live-only. Multiple `subscribe()` calls receive the same stream. `resumeAgentRunStream()` and `AgentRunLifecycle.resumeStream()` subscribe before resumed execution and yield only their selected durable `runId`; approval emits the normal `agent_started` then `agent_resumed` envelope, denial emits only `agent_denied`.
|
|
@@ -14,6 +14,7 @@ The agent/session runtime adds the minimal shared SDK surface for running provid
|
|
|
14
14
|
- `session.compact(options?)`
|
|
15
15
|
- `session.contextMeter()` → `ContextMeter` (latest provider-turn input tokens, reported or labeled estimate, with cap/budget/ratio)
|
|
16
16
|
- `session.subscribe(options?)`
|
|
17
|
+
- `session.close()` → dispatches `session_shutdown` middleware once, then closes every subscriber
|
|
17
18
|
- `session.abort()`
|
|
18
19
|
- `session.entries()`
|
|
19
20
|
- `session.checkout(leafId?)`
|
|
@@ -58,13 +59,15 @@ string | Message | readonly Message[]
|
|
|
58
59
|
|
|
59
60
|
`session.fork(options?)` / `session.clone(options?)` take `AgentSessionForkOptions` / `AgentSessionCloneOptions` (leaf id, new session id, metadata, and store overrides), and `session.steer(input, options?)` takes `SteerOptions`. See [Public contracts](public-contracts.md) for the field tables, and the [options index](options-index.md) for every session option surface.
|
|
60
61
|
|
|
61
|
-
`session.
|
|
62
|
+
`session.close()` is the session teardown seam: it dispatches `session_shutdown` middleware exactly once (idempotent — a second `close()` dispatches nothing) and then closes every subscriber, run-scoped and `acrossRuns` alike. It does not abort an active run, so call it after the run settles. `session_start` middleware, the mirror dispatch, runs once at the session's first run start (the two hooks are the only per-session middleware calls — every other hook is per turn or per boundary). See [Middleware hooks](middleware-hooks.md).
|
|
62
63
|
|
|
63
|
-
`session.
|
|
64
|
+
`session.run()` / `session.prompt()` resolve to an `AgentRunResult` with `sessionId`, `runId`, `status`, `text`, `content`, optional `message`/`usage`/`leafId`, `limit`/`attribution` when the run died on a configured ceiling, and terminal `error`/`abortReason` when applicable. Callers may ignore the return value. Failed and aborted runs still emit their terminal events, then reject with `AgentRunError` whose `.result` carries the same shape.
|
|
65
|
+
|
|
66
|
+
`session.stream(input, options?)` subscribes first, starts exactly one run, yields only that run's events, and terminates when the run succeeds, fails, or aborts. The subscription belongs to `stream()`: it closes it when the owned run settles, so even a run that fails before its first event (a pre-flight validation rejection returns before run-end cleanup) ends the consumer instead of parking it, and no run-end close is required for `stream()` to be correct. Early consumer return aborts the owned run and releases the session. `SubscribeOptions.maxQueuedEvents` / `overflow` may be passed alongside `RunOptions`.
|
|
64
67
|
|
|
65
68
|
`resumeAgentRunStream(agent, ref, resume, options?)` does the same for one existing suspended durable run. It validates checkpoint ownership, revision/fingerprint, and `expectedVersion`, then subscribes before emitting `agent_started` / `agent_resumed` and resumed message/tool/terminal events. `AgentRunResumeStreamOptions` combines `AgentRunResumeOptions` (including the optional `onSession` observer seam a supervisor uses to attach a child event pump to the rebuilt session) with `maxQueuedEvents` and `overflow`; early return aborts only resumed execution. Since 0.8.0 (plan 080 Task 3), `AgentRunResumeOptions.signal` is inherited by both entrypoints, so `resumeAgentRun()` aborts a live resumed provider/tool turn the same way `resumeAgentRunStream()` does — checked before each preparation step and threaded into the resumed execution. It does not replay a claimed/dispatched tool, poll a ledger, or retain a worker. `createAgentRunLifecycle().resumeStream(ref, resume, request?)` adds the same behavior after host agent-capability resolution.
|
|
66
69
|
|
|
67
|
-
`session.subscribe(options?)`
|
|
70
|
+
`session.subscribe(options?)` returns an in-memory live subscription. By default it is **run-scoped**: the run-end cleanup (`cleanupRun`), a durable suspension, and a durable denial all close it, which is what `stream()` and the examples rely on. `SubscribeOptions.acrossRuns: true` opts one subscriber out of that close, so it keeps receiving the next run's events on the same session; it is then ended only by the host (`break` out of the `for await`, or the iterator's `return()`/`[Symbol.asyncIterator]().return()`), by `closeSubscribers()` on session teardown, or by an overflow under the default `close` policy. The run-scoped close is `closeRunSubscribers()`; `closeSubscribers()` still means every subscriber. Subscribe before `run()` to observe that run's events. The consumer loop and `session.run()` must run concurrently (e.g. start the `for await` consumer, then `await Promise.all([consumer, session.run("Hi")])`): events are only emitted during a live run, so awaiting the subscribe loop before calling `run()` deadlocks. Prefer `session.stream()` when you only need one run's events. `SubscribeOptions.maxQueuedEvents` defaults to `1024` (minimum `1`) and caps events queued while the consumer is not awaiting `next()`. `SubscribeOptions.overflow` defaults to `"close"`; it clears queued payload events, delivers one `event_subscriber_overflow` notice to that subscriber, then closes it. `"drop_oldest"` keeps newest events; `"drop_newest"` ignores new events while full.
|
|
68
71
|
|
|
69
72
|
For a text-only provider turn, the runtime emits:
|
|
70
73
|
|
|
@@ -178,17 +181,17 @@ await agent.createSession().run("Hi", { model: overrideModel });
|
|
|
178
181
|
- Compaction context contains branch entries and explicit compaction options only; it does not include provider objects, provider requests, credential resolvers, resolved credentials, settings, or hidden metadata.
|
|
179
182
|
- Store entries contain explicit session data only; Prism does not store provider objects, credential resolvers, resolved credentials, full provider requests, settings, or hidden metadata.
|
|
180
183
|
- Runtime events contain messages/content only; do not put secrets in prompts, metadata, provider events, session entries, or docs examples.
|
|
181
|
-
- The event broadcaster is in-memory, live-only, and bounded per subscriber by `SubscribeOptions`. It adds no dependency, timer, filesystem/network discovery, worker, or durable queue.
|
|
184
|
+
- The event broadcaster is in-memory, live-only, and bounded per subscriber by `SubscribeOptions`. It adds no dependency, timer, filesystem/network discovery, worker, or durable queue. An `acrossRuns: true` subscriber holds that bounded queue (default `maxQueuedEvents` 1024) for the session's lifetime instead of one run, and it subscribes to no other session: the broadcaster stays session-scoped, so no subscriber can observe another session's or ownership scope's events.
|
|
182
185
|
|
|
183
186
|
## Durable interruption
|
|
184
187
|
|
|
185
|
-
Set `runState` with a host-owned `CheckpointStore`, stable `definitionRevision`, and `interruptBeforeTool: true` to suspend at a persisted pre-side-effect boundary. A suspended result has `status: "suspended"`, a redacted `interruption`, and `runState.version`; it releases session resources before returning. When a provider turn requests several tools, the round is collected into **one** suspension whose `interruption.pendingDecisions` holds one redacted `PendingDecision` per gated call (`approvalId`, kind, scope with tool name/effect kind/identity/arguments hash — never raw arguments); ungated calls still dispatch.
|
|
188
|
+
Set `runState` with a host-owned `CheckpointStore`, stable `definitionRevision`, and `interruptBeforeTool: true` to suspend at a persisted pre-side-effect boundary. A compiled pack rule with `action: "ask"` gates exactly the calls it matches the same way, without the all-tools switch (see [Guardrails § ask rules](guardrails.md#asking-for-approval-ask-rules)). A suspended result has `status: "suspended"`, a redacted `interruption`, and `runState.version`; it releases session resources before returning. When a provider turn requests several tools, the round is collected into **one** suspension whose `interruption.pendingDecisions` holds one redacted `PendingDecision` per gated call (`approvalId`, kind, scope with tool name/effect kind/identity/arguments hash — never raw arguments); ungated calls still dispatch.
|
|
186
189
|
|
|
187
190
|
`resumeAgentRun` accepts exactly one of:
|
|
188
191
|
|
|
189
192
|
- `decision: "approve" | "deny"` — legacy single-approval path. `approve` allows every pending decision once; `deny` terminates the run as `denied`.
|
|
190
193
|
- `decision: "continue"` — crash recovery for a running-state checkpoint written by [`checkpointPolicy: "every-turn"`](durable-runs.md): resumes from the last provider-turn boundary without re-dispatching tools. It requires a running state and never bypasses a gate — a suspended run still needs `approve`/`deny` or a decision batch.
|
|
191
|
-
- `decisions: readonly RunDecision[]` — one atomic batch. Every entry validates against the recorded pending set (unknown/foreign `approvalId`, duplicates, stale `expectedVersion`, invalid outcomes fail the whole batch closed with `AgentDecisionError` and leave state and version untouched). Outcomes: `allow_once`, `allow_for_run`, `reject_once`, `reject_for_run`. `reject_*` continues the run with a blocked tool result carrying the bounded (2 KB) `reason`. `modifiedArguments` are revalidated (schema, then input guardrails; permission/trust re-run at dispatch) and produce a new arguments hash. `elicitation` payloads are validated against the pending decision's `elicitationSchema` (required keys plus the configured host validator) and resolve the suspended call without executing it. A batch deciding a strict subset persists the decided entries and re-suspends with the remainder pending at the bumped version.
|
|
194
|
+
- `decisions: readonly RunDecision[]` — one atomic batch. Every entry validates against the recorded pending set (unknown/foreign `approvalId`, duplicates, stale `expectedVersion`, invalid outcomes fail the whole batch closed with `AgentDecisionError` and leave state and version untouched). Outcomes: `allow_once`, `allow_for_run`, `reject_once`, `reject_for_run`. `reject_*` continues the run with a blocked tool result carrying the bounded (2 KB) `reason`. `modifiedArguments` are revalidated (schema, then input guardrails — including the session's restored guardrail pack rules, so an edit into a pack-violating state, a `deny` or an `ask` rule alike, is refused here with `ERR_PRISM_DECISION_INVALID` naming the rule instead of being accepted and only stopped at dispatch; permission/trust re-run at dispatch) and produce a new arguments hash. `elicitation` payloads are validated against the pending decision's `elicitationSchema` (required keys plus the configured host validator) and resolve the suspended call without executing it. A batch deciding a strict subset persists the decided entries and re-suspends with the remainder pending at the bumped version.
|
|
192
195
|
|
|
193
196
|
`*_for_run` outcomes append a `StickyDecision` to the durable run state: later calls in the same run matching the scope exactly (all recorded fields) proceed or are blocked without a new suspension, policy still enforced at dispatch. Sticky decisions expire when the run reaches any terminal status. Caps: 32 pending decisions per run (hard 128), 64 sticky decisions (hard 256), 2 KB decision reasons, 16 KB elicitation payloads. Frontend adapters (such as AG-UI with `capabilities.humanInTheLoop.approveWithEdits`) and the server resume endpoint (`POST .../resume` with `modifiedArguments`) map human edits directly to `RunDecision` entries with `modifiedArguments` under single atomic CAS, revalidating tool parameter schemas and invalidating stale draft approvals.
|
|
194
197
|
|
|
@@ -205,7 +208,7 @@ if (result.status === "suspended") {
|
|
|
205
208
|
}
|
|
206
209
|
```
|
|
207
210
|
|
|
208
|
-
Resume requires exact checkpoint ownership, version, agent fingerprint, and revision. A checkpoint load or delete under a non-matching ownership scope reads as absent (`null`), and a save against a foreign-owned record fails as a generic `ERR_PRISM_CHECKPOINT_CONFLICT` (plan 080 Task 3) — a tenant cannot distinguish “another tenant owns this key” from “missing”, and callers that relied on the old `Checkpoint ownership mismatch` throw now see the same miss they would for an unknown key. The fingerprint hashes the agent id/name, `definitionRevision`, model, instructions, system-prompt contributions, skills (name/instructions/tool names), tool definitions (name/parameters/exclusive), guardrail definitions (name/stage/revision), and loop strategy — changing any of them without bumping `definitionRevision` fails resume closed instead of silently continuing with different agent semantics. Prism CAS-claims approval before work, rechecks normal guardrail/permission/validation/limit paths, and marks a pending tool dispatched before its side effect. `createAgentRunLifecycle()` wraps the same core path for server/MCP hosts: adapters pass only authorized ownership, status returns only `{ state, version }`, and `resolveAgent()` supplies current agent/revision. `resumeStream()` uses that same claim path and bounded subscriber, so adapters do not poll or duplicate resume logic. Remote restart requires both checkpoint and session stores to be durable. A crash after that mark is ambiguous and is never replayed automatically; use host tool idempotency keyed by `runId`/`toolCallId` or resolve it manually. Checkpoints contain bounded redacted state plus session/leaf references, never provider objects, callbacks, signals, credentials, or raw secrets. State is bounded at save by `runState.maxStateBytes` (default 256 KB, at most the 1 MB hard cap); load bounds against the 1 MB hard cap only, so state saved with a raised limit stays resumable while oversized records are still rejected. Since 0.1.3 (plan 015 Task 4), durable runs may opt in to session-state persistence with `persistSessionState: true` on both the run and resume options: the loaded-skill **name catalog** (≤64 names, ≤256 chars each) rides the checkpoint and is restored into the resumed session's `LoadedSkillSet`; skill **bodies are never persisted** and re-resolve from the live registry via `load_skill`. Since 0.1.6 (plan 018 closeout `checkpoint-bodies`), `includeSkillBodies: true` on BOTH the run and resume options additionally persists the exact loaded-skill **instructions** (`{name, instructions}` pairs, redacted at the checkpoint boundary like all state, ≤64 bodies / ≤256-char names / ≤262144-byte bodies / ≤1 MiB total) so resume re-renders them registry-independently — no `load_skill` round-trip and no dependence on the registry still serving the same text; `maxStateBytes` (default 256 KB) refuses oversize bodies with a recorded error, never silently truncates. Default off keeps the checkpoint shape byte-identical to 0.1.3. Since 0.7.0 (plan 074 P3), `persistSessionState: true` also carries the opt-in [attention compiler](attention-compiler.md)'s sticky frontier (`sessionState.attentionSticky`: 32-hex thinking keys plus tool-call ids, newest 256 of each, redacted like all state) so a resumed run keeps its thinking strips and tool stubs instead of re-deciding its first turn from the ratio; a malformed frontier is dropped entry by entry and never blocks a resume. Since 0.7.0, `onSession` hands the reconstructed session to a caller-supplied observer before the resumed run starts, so an observer (the supervisor's child-event pump) can subscribe while the run is still live; it is called for every resume outcome, a throw fails closed before any event or tool work, and the session is valid only for the duration of that resume. Built-in loop options are durable; custom `AgentLoopStrategy` instances are durable when they declare `snapshot`/`restore` hooks (see [Agent loops § Durable runs](agent-loops.md#durable-runs)) and reject before provider work otherwise. For mid-run crash recovery (`checkpointPolicy: "every-turn"` plus `decision: "continue"`), see [Durable runs](durable-runs.md).
|
|
211
|
+
Resume requires exact checkpoint ownership, version, agent fingerprint, and revision. A checkpoint load or delete under a non-matching ownership scope reads as absent (`null`), and a save against a foreign-owned record fails as a generic `ERR_PRISM_CHECKPOINT_CONFLICT` (plan 080 Task 3) — a tenant cannot distinguish “another tenant owns this key” from “missing”, and callers that relied on the old `Checkpoint ownership mismatch` throw now see the same miss they would for an unknown key. The fingerprint hashes the agent id/name, `definitionRevision`, model, instructions, system-prompt contributions, skills (name/instructions/tool names), tool definitions (name/parameters/exclusive), guardrail definitions (name/stage/revision), and loop strategy — changing any of them without bumping `definitionRevision` fails resume closed instead of silently continuing with different agent semantics. Prism CAS-claims approval before work, rechecks normal guardrail/permission/validation/limit paths, and marks a pending tool dispatched before its side effect. `createAgentRunLifecycle()` wraps the same core path for server/MCP hosts: adapters pass only authorized ownership, status returns only `{ state, version }`, and `resolveAgent()` supplies current agent/revision. `resumeStream()` uses that same claim path and bounded subscriber, so adapters do not poll or duplicate resume logic. Remote restart requires both checkpoint and session stores to be durable. A crash after that mark is ambiguous and is never replayed automatically; use host tool idempotency keyed by `runId`/`toolCallId` or resolve it manually. Checkpoints contain bounded redacted state plus session/leaf references, never provider objects, callbacks, signals, credentials, or raw secrets. State is bounded at save by `runState.maxStateBytes` (default 256 KB, at most the 1 MB hard cap); load bounds against the 1 MB hard cap only, so state saved with a raised limit stays resumable while oversized records are still rejected. Since 0.1.3 (plan 015 Task 4), durable runs may opt in to session-state persistence with `persistSessionState: true` on both the run and resume options: the loaded-skill **name catalog** (≤64 names, ≤256 chars each) rides the checkpoint and is restored into the resumed session's `LoadedSkillSet`; skill **bodies are never persisted** and re-resolve from the live registry via `load_skill`. Since 0.1.6 (plan 018 closeout `checkpoint-bodies`), `includeSkillBodies: true` on BOTH the run and resume options additionally persists the exact loaded-skill **instructions** (`{name, instructions}` pairs, redacted at the checkpoint boundary like all state, ≤64 bodies / ≤256-char names / ≤262144-byte bodies / ≤1 MiB total) so resume re-renders them registry-independently — no `load_skill` round-trip and no dependence on the registry still serving the same text; `maxStateBytes` (default 256 KB) refuses oversize bodies with a recorded error, never silently truncates. Default off keeps the checkpoint shape byte-identical to 0.1.3. Since 0.7.0 (plan 074 P3), `persistSessionState: true` also carries the opt-in [attention compiler](attention-compiler.md)'s sticky frontier (`sessionState.attentionSticky`: 32-hex thinking keys plus tool-call ids, newest 256 of each, redacted like all state) so a resumed run keeps its thinking strips and tool stubs instead of re-deciding its first turn from the ratio; a malformed frontier is dropped entry by entry and never blocks a resume. Since 0.9.0 (plan 104 Task 2/3), `persistSessionState: true` also carries the session's guardrail pack refs (`sessionState.guardrailPacks`: `id`, `version`, bounded host `options` — or, for an inline pack, its pattern `rules`, which Task 3 allows to ride the checkpoint while a `deny` predicate or `RegExp` pattern refuses the save) plus each pack's own state-codec snapshot, so a resumed run recompiles and reinstates exactly the packs the suspended run enforced — including the `ask` rules that gate its later calls. The key's presence is the opt-in on resume (the run that wrote it had already opted in), and an unresolvable block — unknown pack id, version mismatch against the installed definition, a pack with persisted state but no codec, more than 8 packs, or malformed/oversized state — fails closed with `AgentRunStateError` before any provider or tool turn rather than resuming unenforced; `session.guardrailPackRefs` then feeds `snapshotRunBundle({ packs })` so recorded identity matches enforcement. Since 0.7.0, `onSession` hands the reconstructed session to a caller-supplied observer before the resumed run starts, so an observer (the supervisor's child-event pump) can subscribe while the run is still live; it is called for every resume outcome, a throw fails closed before any event or tool work, and the session is valid only for the duration of that resume. Built-in loop options are durable; custom `AgentLoopStrategy` instances are durable when they declare `snapshot`/`restore` hooks (see [Agent loops § Durable runs](agent-loops.md#durable-runs)) and reject before provider work otherwise. For mid-run crash recovery (`checkpointPolicy: "every-turn"` plus `decision: "continue"`), see [Durable runs](durable-runs.md).
|
|
209
212
|
|
|
210
213
|
## Secure composition
|
|
211
214
|
|
|
@@ -614,5 +614,5 @@ Every configurable value is a positive safe integer (context may be zero); Prism
|
|
|
614
614
|
- [Public contracts](public-contracts.md): `ToolDefinition`, `ToolResult`, `ToolExecutionContext`, `ContentBlock`, and `JsonObject` shapes.
|
|
615
615
|
- [Host security guide](host-security.md): fail-closed checklist for permission policies, tool validation, and trust boundaries that must gate these tools.
|
|
616
616
|
- [Tool conformance](tool-conformance.md): assertions for the tool-dispatch blocked-reason matrix these tools participate in.
|
|
617
|
-
- [ACP coding-host interop](acp.md): host editors drive these tools through stable ACP v1 — client fs/terminal adapters, `CodingLifecycleEvent` emission (`file_changed` etc. via the `onEvent` options; `plan_changed` also fires from `writeCodingPlanFile`'s `onEvent`, F5), redacted supervisor `subagent_started` / `subagent_stopped` via `observeSupervisorLifecycle
|
|
617
|
+
- [ACP coding-host interop](acp.md): host editors drive these tools through stable ACP v1 — client fs/terminal adapters, `CodingLifecycleEvent` emission (`file_changed` etc. via the `onEvent` options; `plan_changed` also fires from `writeCodingPlanFile`'s `onEvent`, F5), redacted supervisor `subagent_started` / `subagent_stopped` via `observeSupervisorLifecycle` (the opt-in `includeFailure` / `includeRecovery` options add the redacted reason/limit/stop reason and the child's `summary()` counters to stopped events), and permission/elicitation through the shared four-outcome decision model.
|
|
618
618
|
- [LLM compaction package](compaction-llm.md): optional `createCodingCompactionStrategy()` retains bounded paths, patch intent, checks, plan/todo state, blockers, and next verification—not complete diffs or raw command output.
|
package/docs/coding-tools.md
CHANGED
|
@@ -8,14 +8,11 @@ The `@arnilo/prism-coding-tools` family package unifies Prism's coding agent too
|
|
|
8
8
|
npm install @arnilo/prism @arnilo/prism-coding-tools
|
|
9
9
|
```
|
|
10
10
|
|
|
11
|
-
For document reading or specialized
|
|
11
|
+
For document reading or specialized integrations, install the optional peer dependencies as needed:
|
|
12
12
|
|
|
13
13
|
```bash
|
|
14
14
|
# PDF and DOCX document extraction
|
|
15
15
|
npm install pdf-parse mammoth
|
|
16
|
-
|
|
17
|
-
# Ponytail upstream integration
|
|
18
|
-
npm install @dietrichgebert/ponytail
|
|
19
16
|
```
|
|
20
17
|
|
|
21
18
|
## Subpaths Map
|
|
@@ -28,8 +25,6 @@ npm install @dietrichgebert/ponytail
|
|
|
28
25
|
| `@arnilo/prism-coding-tools/computer-use-linux` | Linux desktop observation and targeting tool bridge | — |
|
|
29
26
|
| `@arnilo/prism-coding-tools/dev` | Loopback-only developer inspector, event timeline visualizer, and local replay server | — |
|
|
30
27
|
| `@arnilo/prism-coding-tools/dev/cli` | Command-line entrypoint for `prism dev` | — |
|
|
31
|
-
| `@arnilo/prism-coding-tools/caveman` | Caveman ultra-terse engineering persona extension | — |
|
|
32
|
-
| `@arnilo/prism-coding-tools/ponytail` | Ponytail multi-agent planning and delegation persona extension | `@dietrichgebert/ponytail` |
|
|
33
28
|
| `@arnilo/prism-coding-tools/impeccable` | Impeccable high-precision frontend engineering persona extension | — |
|
|
34
29
|
|
|
35
30
|
## CLI
|
|
@@ -65,17 +60,18 @@ const composition = createSandboxCodingComposition({
|
|
|
65
60
|
|
|
66
61
|
### Persona Extensions
|
|
67
62
|
```ts
|
|
68
|
-
import { createCavemanExtension } from "@arnilo/prism-coding-tools/caveman";
|
|
69
|
-
import { createPonytailExtension } from "@arnilo/prism-coding-tools/ponytail";
|
|
70
63
|
import { createImpeccableExtension } from "@arnilo/prism-coding-tools/impeccable";
|
|
71
64
|
|
|
72
|
-
const caveman = createCavemanExtension();
|
|
73
|
-
const ponytail = createPonytailExtension();
|
|
74
65
|
const impeccable = createImpeccableExtension();
|
|
75
66
|
```
|
|
76
67
|
|
|
68
|
+
Host-owned personas (any upstream `SKILL.md` tree) need no package subpath: load it with
|
|
69
|
+
`loadSkillDirectory` from `@arnilo/prism/node/contribution-discovery`, register the skills and an
|
|
70
|
+
instruction injector from a host extension, and persist the active mode in session entries — see
|
|
71
|
+
[`examples/caveman-ponytail.ts`](../examples/caveman-ponytail.ts).
|
|
72
|
+
|
|
77
73
|
## Security & Import Isolation
|
|
78
74
|
|
|
79
75
|
- Importing `@arnilo/prism-coding-tools/agent` never loads Docker sandbox adapters, desktop MCP bridges, document parser peers, or Dev inspector modules.
|
|
80
|
-
- Document parser peers (`pdf-parse`, `mammoth`)
|
|
76
|
+
- Document parser peers (`pdf-parse`, `mammoth`) fail closed when absent.
|
|
81
77
|
- Persona extensions are pure prompt and behavior modifiers and never gain implicit host privileges.
|
package/docs/compaction-llm.md
CHANGED
|
@@ -122,6 +122,8 @@ const agent = createAgent({ model, provider, compaction: { strategy, thresholdEn
|
|
|
122
122
|
|
|
123
123
|
Registration only contributes an inert strategy. The host must resolve and pass it to runtime config.
|
|
124
124
|
|
|
125
|
+
Both compaction routes (`session.compact()` and auto-compaction) hand this strategy its `CompactionContext` through the pre-strategy `compaction_request` middleware hook, so a host can rewrite the entries the strategy summarizes; the strategy itself needs no change — see [Middleware hooks](middleware-hooks.md).
|
|
126
|
+
|
|
125
127
|
## Security and performance notes
|
|
126
128
|
Preparation is O(n) over branch entries and uses only arrays, strings, and JSON serialization. Limit options must be positive safe integers at or below their hard caps and reject during strategy creation. Missing output options use a 16,384-token summary ceiling; reserve ratio/model metadata may narrow the provider request, never remove its finite `maxTokens`. A request policy that replaces `maxTokens` with NaN, Infinity, zero, an unsafe integer, or above-hard-cap input fails before provider generation.
|
|
127
129
|
|