@arnilo/prism 0.9.0 → 0.10.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 +24 -1
- package/README.md +13 -12
- package/dist/agent-approval.d.ts +7 -1
- package/dist/agent-approval.js +15 -6
- package/dist/agent-run-lifecycle.js +19 -5
- 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 +156 -9
- 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 +58 -5
- package/dist/agent-session/session/types.d.ts +20 -2
- package/dist/agent-session/session.d.ts +65 -4
- package/dist/agent-session/session.js +156 -16
- 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-protocol.d.ts +6 -4
- package/dist/contracts-run-state.d.ts +37 -3
- 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 +6 -6
- package/dist/index.js +4 -4
- package/dist/middleware.d.ts +1 -1
- package/dist/run-bundle.d.ts +6 -1
- package/dist/run-bundle.js +4 -1
- package/dist/run-limits.js +13 -0
- package/dist/testing/prefix-stability-conformance.d.ts +29 -0
- package/dist/testing/prefix-stability-conformance.js +91 -23
- package/dist/tools.js +10 -3
- package/docs/agent-events.md +12 -8
- package/docs/agent-session-runtime.md +9 -6
- package/docs/caveman.md +1 -1
- package/docs/compaction-llm.md +2 -0
- package/docs/compaction-observational-memory.md +21 -1
- package/docs/durable-runs.md +4 -3
- package/docs/embeddings.md +5 -1
- package/docs/execution-timeline.md +3 -2
- package/docs/extensions.md +20 -3
- package/docs/guardrails.md +16 -6
- package/docs/hooks.md +282 -0
- package/docs/index.md +18 -15
- package/docs/input-and-prompt-assembly.md +1 -1
- package/docs/instruction-injection.md +1 -0
- package/docs/live-testing.md +3 -1
- package/docs/memory-fabric.md +28 -0
- package/docs/middleware-hooks.md +54 -4
- package/docs/migration.md +13 -0
- package/docs/options-index.md +3 -1
- package/docs/policy-and-audit.md +14 -1
- package/docs/prefix-stability-conformance.md +57 -7
- package/docs/provider-packages.md +20 -20
- package/docs/public-contracts.md +1 -0
- package/docs/rag.md +93 -6
- package/docs/release-and-install.md +42 -39
- package/docs/runs-and-usage.md +17 -8
- package/docs/scoped-agent-memory.md +17 -9
- package/docs/scoped-memory.md +138 -0
- package/docs/tools.md +1 -1
- package/docs/wiki.md +4 -2
- package/package.json +4 -2
package/dist/guardrails.js
CHANGED
|
@@ -17,6 +17,28 @@ const MAX_ARG_SCAN_STRINGS = 64;
|
|
|
17
17
|
const MAX_ARG_STRING_BYTES = 16 * 1024;
|
|
18
18
|
const MAX_PACK_NAME_BYTES = 128;
|
|
19
19
|
const OBSERVE_RULE_ID = "observe";
|
|
20
|
+
/** Bytes allowed for a rule-naming refusal line, so no rule text can grow a model or host message. */
|
|
21
|
+
const MAX_GUARDRAIL_REFUSAL_BYTES = 200;
|
|
22
|
+
/**
|
|
23
|
+
* Plan 104 T4/T6: the bounded, redacted refusal line for a terminal record that came from a compiled
|
|
24
|
+
* pack rule — `<prefix> by guardrail rule pack:<pack>/<rule>`, plus the pack's own reason when it set
|
|
25
|
+
* one — or `undefined` for any other guardrail, so the caller keeps its own neutral text. Only the
|
|
26
|
+
* compiler writes the `pack`/`rule` metadata, so a host-written guardrail named `pack:…` is never
|
|
27
|
+
* presented as a pack rule. Reasons are redacted where the record is built, pack names are
|
|
28
|
+
* compiler-bounded to 128 bytes (the identity always survives the cap), and a long reason is
|
|
29
|
+
* truncated, so the same derivation serves the tool refusal and decision-time revalidation.
|
|
30
|
+
*/
|
|
31
|
+
export function guardrailRefusalText(record, prefix = "Blocked") {
|
|
32
|
+
const pack = record.metadata?.pack;
|
|
33
|
+
const rule = record.metadata?.rule;
|
|
34
|
+
if (typeof pack !== "string" || typeof rule !== "string")
|
|
35
|
+
return undefined;
|
|
36
|
+
const line = `${prefix} by guardrail rule ${record.guardrail}`;
|
|
37
|
+
// The compiler synthesizes `guardrail pack rule <pack>/<rule>` when the rule set no reason; the
|
|
38
|
+
// identity already implies it, so only a real reason is appended.
|
|
39
|
+
const text = record.reason && record.reason !== `guardrail pack rule ${pack}/${rule}` ? `${line}: ${record.reason}` : line;
|
|
40
|
+
return boundText(text, MAX_GUARDRAIL_REFUSAL_BYTES);
|
|
41
|
+
}
|
|
20
42
|
export class GuardrailError extends Error {
|
|
21
43
|
code;
|
|
22
44
|
record;
|
|
@@ -76,6 +98,7 @@ export function assertGuardrailsAllowed(result) {
|
|
|
76
98
|
if (result.terminal)
|
|
77
99
|
throw new GuardrailError(result.terminal);
|
|
78
100
|
}
|
|
101
|
+
const EMPTY_COMPILED_PACKS = { guardrails: undefined, packs: [], snapshotState: () => undefined };
|
|
79
102
|
/**
|
|
80
103
|
* Compiles `guardrailPacks` config onto the existing tool interception seams: one `tool_input`
|
|
81
104
|
* guardrail per rule (`name = pack:<pack>/<rule>`), plus one `tool_output` recorder for packs that
|
|
@@ -83,19 +106,60 @@ export function assertGuardrailsAllowed(result) {
|
|
|
83
106
|
* Throws `GuardrailPackError` on malformed config (fail closed); returns `undefined` when unset.
|
|
84
107
|
*/
|
|
85
108
|
export function compileGuardrailPacks(refs, registry = BUILT_IN_GUARDRAIL_PACKS) {
|
|
86
|
-
|
|
109
|
+
return compileGuardrailPacksWithState(refs, registry).guardrails;
|
|
110
|
+
}
|
|
111
|
+
/**
|
|
112
|
+
* Plan 104 Task 2: the internal compile entry behind `compileGuardrailPacks`. Passing `initial`
|
|
113
|
+
* marks a durable restore — rows must then come from the installed registry, match its version, and
|
|
114
|
+
* parse through the pack's own state codec, so a mismatch fails closed instead of restoring a
|
|
115
|
+
* weaker policy. `snapshotState` is the checkpoint-side counterpart.
|
|
116
|
+
*/
|
|
117
|
+
export function compileGuardrailPacksWithState(refs, registry = BUILT_IN_GUARDRAIL_PACKS, initial) {
|
|
118
|
+
const packs = resolveGuardrailPacks(refs, registry, initial);
|
|
87
119
|
if (packs.length === 0)
|
|
88
|
-
return
|
|
120
|
+
return EMPTY_COMPILED_PACKS;
|
|
89
121
|
const toolInput = [];
|
|
90
122
|
const toolOutput = [];
|
|
123
|
+
const askGate = [];
|
|
124
|
+
const askBlocks = [];
|
|
91
125
|
for (const pack of packs) {
|
|
92
|
-
const state = {};
|
|
93
126
|
if (pack.observe)
|
|
94
|
-
toolOutput.push(observeGuardrail(pack, pack.observe, state));
|
|
95
|
-
for (const resolved of pack.rules)
|
|
96
|
-
|
|
127
|
+
toolOutput.push(observeGuardrail(pack, pack.observe, pack.state));
|
|
128
|
+
for (const resolved of pack.rules) {
|
|
129
|
+
if (resolved.action === "ask") {
|
|
130
|
+
// Plan 104 T3: `ask` is not an ordinary stage decision. A run that can suspend gates the
|
|
131
|
+
// call at charge time (`interrupt` is the record meaning "awaiting a decision"), while a run
|
|
132
|
+
// that cannot suspend evaluates the same rule as a plain block through `activeGuardrails`.
|
|
133
|
+
askGate.push(ruleGuardrail(pack, resolved, pack.state, "interrupt"));
|
|
134
|
+
askBlocks.push(ruleGuardrail(pack, resolved, pack.state, "block"));
|
|
135
|
+
continue;
|
|
136
|
+
}
|
|
137
|
+
toolInput.push(ruleGuardrail(pack, resolved, pack.state));
|
|
138
|
+
}
|
|
97
139
|
}
|
|
98
|
-
return
|
|
140
|
+
return {
|
|
141
|
+
guardrails: toolOutput.length > 0 ? { toolInput, toolOutput } : { toolInput },
|
|
142
|
+
packs: packs.map((pack) => pack.row),
|
|
143
|
+
...(askGate.length > 0 ? { askGate: { toolInput: askGate }, askBlocks: { toolInput: askBlocks } } : {}),
|
|
144
|
+
snapshotState: () => {
|
|
145
|
+
const state = {};
|
|
146
|
+
for (const pack of packs) {
|
|
147
|
+
// Plan 104 T3: an inline pack now rides the checkpoint as rules, so only the rule shapes
|
|
148
|
+
// that cannot round-trip matter: a closure has no JSON form, and a `RegExp` serializes to
|
|
149
|
+
// `{}`, which would restore as an invalid (or, worse, absent) pattern.
|
|
150
|
+
const unpersistable = pack.inline
|
|
151
|
+
? pack.rules.find((resolved) => resolved.rule.deny !== undefined || resolved.rule.pattern instanceof RegExp)
|
|
152
|
+
: undefined;
|
|
153
|
+
if (unpersistable) {
|
|
154
|
+
throw new GuardrailPackError(`guardrail pack "${pack.id}" rule "${unpersistable.rule.id}" cannot be persisted (deny predicate or RegExp pattern); use a pattern string or a registered pack id when \`persistSessionState\` is on`);
|
|
155
|
+
}
|
|
156
|
+
const snapshot = pack.stateCodec?.snapshot(pack.state);
|
|
157
|
+
if (snapshot !== undefined)
|
|
158
|
+
state[pack.id] = snapshot;
|
|
159
|
+
}
|
|
160
|
+
return Object.keys(state).length > 0 ? state : undefined;
|
|
161
|
+
},
|
|
162
|
+
};
|
|
99
163
|
}
|
|
100
164
|
/** Stable identity rows for the same config `compileGuardrailPacks` accepts (no state, no guardrails built). */
|
|
101
165
|
export function describeGuardrailPacks(refs, registry = BUILT_IN_GUARDRAIL_PACKS) {
|
|
@@ -114,13 +178,14 @@ function packRevision(pack) {
|
|
|
114
178
|
function packRuleName(packId, ruleId) {
|
|
115
179
|
return `pack:${packId}/${ruleId}`;
|
|
116
180
|
}
|
|
117
|
-
function resolveGuardrailPacks(refs, registry) {
|
|
181
|
+
function resolveGuardrailPacks(refs, registry, initial) {
|
|
118
182
|
if (refs === undefined)
|
|
119
183
|
return [];
|
|
120
184
|
if (!Array.isArray(refs))
|
|
121
185
|
throw new GuardrailPackError("guardrailPacks must be an array of pack ids or pack input objects");
|
|
122
186
|
if (refs.length > MAX_GUARDRAIL_PACKS)
|
|
123
187
|
throw new GuardrailPackError(`guardrailPacks accepts at most ${MAX_GUARDRAIL_PACKS} packs`);
|
|
188
|
+
const restoring = initial !== undefined;
|
|
124
189
|
const seenPacks = new Set();
|
|
125
190
|
return refs.map((ref) => {
|
|
126
191
|
const input = (typeof ref === "string" ? { id: ref } : ref) ?? {};
|
|
@@ -135,7 +200,22 @@ function resolveGuardrailPacks(refs, registry) {
|
|
|
135
200
|
}
|
|
136
201
|
const definition = registry.get(input.id);
|
|
137
202
|
if (definition === undefined && input.rules === undefined) {
|
|
138
|
-
throw
|
|
203
|
+
throw unknownGuardrailPack(input.id, registry);
|
|
204
|
+
}
|
|
205
|
+
// A restored checkpoint replays rows, so a registered id must still match its installed version
|
|
206
|
+
// while an inline pack needs its pattern rules back (a closure cannot ride a checkpoint, and a
|
|
207
|
+
// pack replaying without it would enforce less than it did).
|
|
208
|
+
if (restoring) {
|
|
209
|
+
if (input.rules === undefined) {
|
|
210
|
+
if (definition === undefined)
|
|
211
|
+
throw unknownGuardrailPack(input.id, registry);
|
|
212
|
+
if (input.version !== definition.version) {
|
|
213
|
+
throw new GuardrailPackError(`guardrail pack "${input.id}" was persisted at version ${input.version} but the installed version is ${definition.version}`);
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
else if (input.rules.some((rule) => rule?.deny !== undefined)) {
|
|
217
|
+
throw new GuardrailPackError(`persisted guardrail pack "${input.id}" carries a deny predicate; only pattern rules can be restored`);
|
|
218
|
+
}
|
|
139
219
|
}
|
|
140
220
|
let built;
|
|
141
221
|
if (input.rules !== undefined) {
|
|
@@ -144,6 +224,8 @@ function resolveGuardrailPacks(refs, registry) {
|
|
|
144
224
|
built = { rules: input.rules };
|
|
145
225
|
}
|
|
146
226
|
else {
|
|
227
|
+
if (definition === undefined)
|
|
228
|
+
throw unknownGuardrailPack(input.id, registry);
|
|
147
229
|
built = definition.build(Object.freeze({ ...input.options }));
|
|
148
230
|
}
|
|
149
231
|
if (!built || !Array.isArray(built.rules) || built.rules.length === 0) {
|
|
@@ -155,19 +237,43 @@ function resolveGuardrailPacks(refs, registry) {
|
|
|
155
237
|
if (built.observe !== undefined && typeof built.observe !== "function") {
|
|
156
238
|
throw new GuardrailPackError(`guardrail pack "${input.id}" observe must be a function`);
|
|
157
239
|
}
|
|
240
|
+
if (built.state !== undefined && (typeof built.state.snapshot !== "function" || typeof built.state.parse !== "function")) {
|
|
241
|
+
throw new GuardrailPackError(`guardrail pack "${input.id}" state codec must declare snapshot and parse`);
|
|
242
|
+
}
|
|
158
243
|
const seenRules = new Set();
|
|
159
244
|
const rules = built.rules.map((rule) => resolveRule(input.id, rule, seenRules));
|
|
160
245
|
if (built.observe && byteLength(packRuleName(input.id, OBSERVE_RULE_ID)) > MAX_PACK_NAME_BYTES) {
|
|
161
246
|
throw new GuardrailPackError(`guardrail pack "${input.id}" name exceeds ${MAX_PACK_NAME_BYTES} bytes`);
|
|
162
247
|
}
|
|
248
|
+
const version = input.version ?? definition?.version ?? 1;
|
|
249
|
+
const state = {};
|
|
250
|
+
const persisted = initial === undefined ? undefined : initial[input.id];
|
|
251
|
+
if (persisted !== undefined) {
|
|
252
|
+
if (built.state === undefined) {
|
|
253
|
+
throw new GuardrailPackError(`guardrail pack "${input.id}" has persisted state but declares no state codec`);
|
|
254
|
+
}
|
|
255
|
+
Object.assign(state, built.state.parse(persisted));
|
|
256
|
+
}
|
|
163
257
|
return {
|
|
164
258
|
id: input.id,
|
|
165
|
-
version
|
|
259
|
+
version,
|
|
166
260
|
rules,
|
|
261
|
+
state,
|
|
262
|
+
inline: input.rules !== undefined,
|
|
263
|
+
row: {
|
|
264
|
+
id: input.id,
|
|
265
|
+
version,
|
|
266
|
+
...(input.options !== undefined ? { options: input.options } : {}),
|
|
267
|
+
...(input.rules !== undefined ? { rules: input.rules } : {}),
|
|
268
|
+
},
|
|
167
269
|
...(built.observe ? { observe: built.observe } : {}),
|
|
270
|
+
...(built.state !== undefined ? { stateCodec: built.state } : {}),
|
|
168
271
|
};
|
|
169
272
|
});
|
|
170
273
|
}
|
|
274
|
+
function unknownGuardrailPack(id, registry) {
|
|
275
|
+
return new GuardrailPackError(`unknown guardrail pack "${id}"; known packs: ${[...registry.keys()].join(", ") || "none"}`);
|
|
276
|
+
}
|
|
171
277
|
function resolveRule(packId, rule, seenRules) {
|
|
172
278
|
const where = `guardrail pack "${packId}"`;
|
|
173
279
|
if (!rule || typeof rule.id !== "string" || !rule.id.trim() || byteLength(rule.id) > MAX_RULE_ID_BYTES) {
|
|
@@ -177,8 +283,11 @@ function resolveRule(packId, rule, seenRules) {
|
|
|
177
283
|
throw new GuardrailPackError(`${where} has a duplicate rule id "${rule.id}"`);
|
|
178
284
|
seenRules.add(rule.id);
|
|
179
285
|
const action = rule.action ?? "deny";
|
|
180
|
-
if (action !== "deny" && action !== "tripwire") {
|
|
181
|
-
throw new GuardrailPackError(`${where} rule "${rule.id}" action must be "deny"
|
|
286
|
+
if (action !== "deny" && action !== "tripwire" && action !== "ask") {
|
|
287
|
+
throw new GuardrailPackError(`${where} rule "${rule.id}" action must be "deny", "tripwire", or "ask"`);
|
|
288
|
+
}
|
|
289
|
+
if (action === "ask" && rule.deny !== undefined) {
|
|
290
|
+
throw new GuardrailPackError(`${where} rule "${rule.id}" action "ask" requires "pattern": an opaque deny predicate cannot raise an approval`);
|
|
182
291
|
}
|
|
183
292
|
const hasPattern = rule.pattern !== undefined;
|
|
184
293
|
const hasDeny = rule.deny !== undefined;
|
|
@@ -229,7 +338,7 @@ function compileRulePattern(packId, ruleId, pattern) {
|
|
|
229
338
|
throw new GuardrailPackError(`${where} has an invalid pattern`, { cause: error });
|
|
230
339
|
}
|
|
231
340
|
}
|
|
232
|
-
function ruleGuardrail(pack, resolved, state) {
|
|
341
|
+
function ruleGuardrail(pack, resolved, state, askAction = "interrupt") {
|
|
233
342
|
return {
|
|
234
343
|
name: packRuleName(pack.id, resolved.rule.id),
|
|
235
344
|
revision: packRevision(pack),
|
|
@@ -244,8 +353,8 @@ function ruleGuardrail(pack, resolved, state) {
|
|
|
244
353
|
if (!matched)
|
|
245
354
|
return { action: "allow" };
|
|
246
355
|
return {
|
|
247
|
-
// Pack vocabulary is `deny`; the core guardrail
|
|
248
|
-
action: resolved.action === "deny" ? "block" : "tripwire",
|
|
356
|
+
// Pack vocabulary is `deny`/`ask`; the core guardrail actions are `block`/`interrupt`.
|
|
357
|
+
action: resolved.action === "deny" ? "block" : resolved.action === "ask" ? askAction : "tripwire",
|
|
249
358
|
reason: resolved.reason,
|
|
250
359
|
metadata: { pack: pack.id, rule: resolved.rule.id, version: pack.version },
|
|
251
360
|
};
|
package/dist/index.d.ts
CHANGED
|
@@ -4,8 +4,6 @@ export { AgentEventSourceError, createMemoryAgentEventSource, isTerminalAgentEve
|
|
|
4
4
|
export { dispatchToolCallsInOrder, generateValidateReviseLoop, isAgentLoopOptions, resolveLoop, resolveToolConcurrency, singleShotLoop, } from "./agent-loops.js";
|
|
5
5
|
export type { AgentRunLifecycle, AgentRunLifecycleAgent, AgentRunLifecycleOptions, AgentRunLifecycleRequest, AgentRunLifecycleStreamRequest, } from "./agent-run-lifecycle.js";
|
|
6
6
|
export { createAgentRunLifecycle } from "./agent-run-lifecycle.js";
|
|
7
|
-
export type { CheckpointRestoreAudit, CheckpointRestoreAuditEntry, CheckpointRestoreHook, RunCheckpointRestoreHooksOptions, } from "./checkpoint-restore.js";
|
|
8
|
-
export { CheckpointRestoreError, DEFAULT_CHECKPOINT_RESTORE_TIMEOUT_MS, runCheckpointRestoreHooks } from "./checkpoint-restore.js";
|
|
9
7
|
export type { PendingToolCall, StoredAgentRunState } from "./agent-run-state.js";
|
|
10
8
|
export { AGENT_RUN_STATE_NAMESPACE, AGENT_RUN_STATE_SCHEMA_VERSION, agentFingerprint, boundCheckpointMetadata, DEFAULT_MAX_AGENT_RUN_STATE_BYTES, HARD_MAX_AGENT_RUN_STATE_BYTES, loadAgentRunState, MAX_AGENT_RUN_METADATA_BYTES, readCheckpointMetadata, resolveCheckpointMetadata, } from "./agent-run-state.js";
|
|
11
9
|
export { createAgent, createAgentSession, resumeAgentRun, resumeAgentRunStream } from "./agents.js";
|
|
@@ -19,6 +17,8 @@ export type { CacheTelemetry, CacheTelemetryOptions, CacheTelemetryReport, Cache
|
|
|
19
17
|
export { CACHE_TELEMETRY_OVERFLOW_KEY, CacheTelemetryError, createCacheTelemetry, DEFAULT_CACHE_TELEMETRY_CAP, } from "./cache-telemetry.js";
|
|
20
18
|
export type { ProviderCapture, ProviderCaptureEntry, ProviderCaptureOptions, ProviderCapturePolicy } from "./capture.js";
|
|
21
19
|
export { createProviderCapture } from "./capture.js";
|
|
20
|
+
export type { CheckpointRestoreAudit, CheckpointRestoreAuditEntry, CheckpointRestoreHook, RunCheckpointRestoreHooksOptions, } from "./checkpoint-restore.js";
|
|
21
|
+
export { CheckpointRestoreError, DEFAULT_CHECKPOINT_RESTORE_TIMEOUT_MS, runCheckpointRestoreHooks } from "./checkpoint-restore.js";
|
|
22
22
|
export type { MemoryCheckpointStoreOptions } from "./checkpoints.js";
|
|
23
23
|
export { CHECKPOINT_CONFLICT_CODE, CheckpointConflictError, createMemoryCheckpointStore } from "./checkpoints.js";
|
|
24
24
|
export type { DefaultCompactionStrategyOptions } from "./compaction.js";
|
|
@@ -49,15 +49,15 @@ export type { ClaimGroundingEvidence, ClaimGroundingEvidenceExtractor, ClaimGrou
|
|
|
49
49
|
export { createClaimGroundingGuardrail } from "./evidence-grounding.js";
|
|
50
50
|
export type { ExecutionAction, ExecutionDecision, ExecutionPolicy, ExecutionRisk } from "./execution-policy.js";
|
|
51
51
|
export { applyExecutionDecision, assertExecutionAllowed, checkExecution, ExecutionDeniedError } from "./execution-policy.js";
|
|
52
|
-
export type { ActivatedKernelConfig, ExtensionErrorPolicy, ExtensionEventBus, ExtensionEventHandler, ExtensionKernel, ExtensionKernelOptions, ExtensionLoadPolicy, LoadedExtension, } from "./extensions.js";
|
|
53
|
-
export { activateKernel, createExtensionEventBus, createExtensionKernel } from "./extensions.js";
|
|
52
|
+
export type { ActivatedKernelConfig, AgentEventBridgeOptions, ExtensionErrorPolicy, ExtensionEventBus, ExtensionEventHandler, ExtensionKernel, ExtensionKernelOptions, ExtensionLoadPolicy, LoadedExtension, } from "./extensions.js";
|
|
53
|
+
export { activateKernel, createExtensionEventBus, createExtensionKernel, forwardAgentEvents } from "./extensions.js";
|
|
54
54
|
export type { MemoryRunFeedbackStoreOptions, PrepareRunFeedbackOptions, RunFeedbackLimits, RunFeedbackRun, RunFeedbackRunResolver, } from "./feedback.js";
|
|
55
55
|
export { createMemoryRunFeedbackStore, prepareRunFeedback, RunFeedbackError, requireRunFeedbackOwnership, runFeedbackPageLimit, } from "./feedback.js";
|
|
56
56
|
export type { ApplyFieldPolicyOptions, AuditFieldRedaction, AuditFieldRedactorLike, AuditFieldRedactorOptions, FieldPolicy, FieldPolicyAction, FieldPolicyDecision, FieldPolicyInput, ProtectedFieldPolicyOptions, } from "./field-policy.js";
|
|
57
57
|
export { ALLOW_FIELD_POLICY, applyFieldPolicy, createAuditFieldRedactor, createProtectedFieldPolicy, FIELD_POLICY_LIMITS, FieldPolicyError, } from "./field-policy.js";
|
|
58
|
+
export { BUILT_IN_GUARDRAIL_PACK_IDS } from "./guardrail-packs/index.js";
|
|
58
59
|
export type { GuardrailPackRow, GuardrailRunResult, RunGuardrailsOptions, } from "./guardrails.js";
|
|
59
60
|
export { assertGuardrailsAllowed, compileGuardrailPacks, describeGuardrailPacks, GuardrailError, GuardrailPackError, MAX_GUARDRAIL_CONCURRENCY, MAX_GUARDRAIL_PACK_RULES, MAX_GUARDRAIL_PACKS, runGuardrails, } from "./guardrails.js";
|
|
60
|
-
export { BUILT_IN_GUARDRAIL_PACK_IDS } from "./guardrail-packs/index.js";
|
|
61
61
|
export type { AgentIdentity, AssertIdentityActiveOptions, IdentityLimits, IdentityVerifier, NarrowIdentityOptions, Principal, ResolvedIdentityLimits, } from "./identity.js";
|
|
62
62
|
export { assertIdentityActive, assertIdentityMatchesOwnership, assertIdentityPropagation, DEFAULT_IDENTITY_LIMITS, HARD_IDENTITY_LIMITS, IdentityError, identityTelemetryAttributes, narrowIdentity, ownershipFromIdentity, resolveIdentityLimits, resolveRunIdentity, } from "./identity.js";
|
|
63
63
|
export type { AgentInput, AssembleProviderInputOptions, DefaultInputBuildContext, DefaultInputBuilder, DefaultPromptBuilder, InputAttachment, PromptInstruction, PromptTemplateOptions, ResolveContextOptions, } from "./input.js";
|
|
@@ -131,5 +131,5 @@ export { MODEL_FAMILY_TOKENS, resolveModelFamily } from "./usage-estimation.js";
|
|
|
131
131
|
export type { ResolvedUseCaseModel, ResolveUseCaseModelInput, UseCaseModelBinding, } from "./use-case-model.js";
|
|
132
132
|
export { resolveUseCaseModel, resolveUseCaseModelBinding, useCaseCredentialProviderId, } from "./use-case-model.js";
|
|
133
133
|
export declare const name = "prism";
|
|
134
|
-
export declare const version = "0.
|
|
134
|
+
export declare const version = "0.10.0";
|
|
135
135
|
export declare const description = "Agent harness for AI providers, agents, sessions, and tools.";
|
package/dist/index.js
CHANGED
|
@@ -2,7 +2,6 @@ export { resolveAgentDefinition } from "./agent-definitions.js";
|
|
|
2
2
|
export { AgentEventSourceError, createMemoryAgentEventSource, isTerminalAgentEventType } from "./agent-event-source.js";
|
|
3
3
|
export { dispatchToolCallsInOrder, generateValidateReviseLoop, isAgentLoopOptions, resolveLoop, resolveToolConcurrency, singleShotLoop, } from "./agent-loops.js";
|
|
4
4
|
export { createAgentRunLifecycle } from "./agent-run-lifecycle.js";
|
|
5
|
-
export { CheckpointRestoreError, DEFAULT_CHECKPOINT_RESTORE_TIMEOUT_MS, runCheckpointRestoreHooks } from "./checkpoint-restore.js";
|
|
6
5
|
export { AGENT_RUN_STATE_NAMESPACE, AGENT_RUN_STATE_SCHEMA_VERSION, agentFingerprint, boundCheckpointMetadata, DEFAULT_MAX_AGENT_RUN_STATE_BYTES, HARD_MAX_AGENT_RUN_STATE_BYTES, loadAgentRunState, MAX_AGENT_RUN_METADATA_BYTES, readCheckpointMetadata, resolveCheckpointMetadata, } from "./agent-run-state.js";
|
|
7
6
|
export { createAgent, createAgentSession, resumeAgentRun, resumeAgentRunStream } from "./agents.js";
|
|
8
7
|
export { ARTIFACT_BODY_ERROR_CODES, ARTIFACT_CHECKPOINT_NAMESPACE, ArtifactBodyStoreError, ArtifactError, approvalEvidenceIntact, artifactApprovalState, artifactCheckpointKey, checkCitationIntegrity, citationBindingDigest, HARD_CITATION_EXCERPT_BYTES, } from "./artifacts.js";
|
|
@@ -10,6 +9,7 @@ export { ATTENTION_BUDGET_ERROR_CODE, AttentionBudgetError, createAttentionCompi
|
|
|
10
9
|
export { applyCacheControl, cacheHitRate, cacheSavings, cacheUsageReport, mapCacheRetention, resolveBreakpoint, sanitizeCacheKey, systemCacheControlField, } from "./cache-helpers.js";
|
|
11
10
|
export { CACHE_TELEMETRY_OVERFLOW_KEY, CacheTelemetryError, createCacheTelemetry, DEFAULT_CACHE_TELEMETRY_CAP, } from "./cache-telemetry.js";
|
|
12
11
|
export { createProviderCapture } from "./capture.js";
|
|
12
|
+
export { CheckpointRestoreError, DEFAULT_CHECKPOINT_RESTORE_TIMEOUT_MS, runCheckpointRestoreHooks } from "./checkpoint-restore.js";
|
|
13
13
|
export { CHECKPOINT_CONFLICT_CODE, CheckpointConflictError, createMemoryCheckpointStore } from "./checkpoints.js";
|
|
14
14
|
export { createDefaultCompactionStrategy, isCompactionEntryData } from "./compaction.js";
|
|
15
15
|
export { assertJsonObject, isJsonObject, loadConfigLayers, mergeConfigLayers } from "./config.js";
|
|
@@ -25,11 +25,11 @@ export { acceptDeviceChunk, assertDeviceAdmit, DEFAULT_DEVICE_MAX_CHUNK_BYTES, D
|
|
|
25
25
|
export { createEventMultiplexer, EVENT_MULTIPLEXER_SINGLE_CONSUMER_CODE, EventMultiplexerError } from "./event-multiplexer.js";
|
|
26
26
|
export { createClaimGroundingGuardrail } from "./evidence-grounding.js";
|
|
27
27
|
export { applyExecutionDecision, assertExecutionAllowed, checkExecution, ExecutionDeniedError } from "./execution-policy.js";
|
|
28
|
-
export { activateKernel, createExtensionEventBus, createExtensionKernel } from "./extensions.js";
|
|
28
|
+
export { activateKernel, createExtensionEventBus, createExtensionKernel, forwardAgentEvents } from "./extensions.js";
|
|
29
29
|
export { createMemoryRunFeedbackStore, prepareRunFeedback, RunFeedbackError, requireRunFeedbackOwnership, runFeedbackPageLimit, } from "./feedback.js";
|
|
30
30
|
export { ALLOW_FIELD_POLICY, applyFieldPolicy, createAuditFieldRedactor, createProtectedFieldPolicy, FIELD_POLICY_LIMITS, FieldPolicyError, } from "./field-policy.js";
|
|
31
|
-
export { assertGuardrailsAllowed, compileGuardrailPacks, describeGuardrailPacks, GuardrailError, GuardrailPackError, MAX_GUARDRAIL_CONCURRENCY, MAX_GUARDRAIL_PACK_RULES, MAX_GUARDRAIL_PACKS, runGuardrails, } from "./guardrails.js";
|
|
32
31
|
export { BUILT_IN_GUARDRAIL_PACK_IDS } from "./guardrail-packs/index.js";
|
|
32
|
+
export { assertGuardrailsAllowed, compileGuardrailPacks, describeGuardrailPacks, GuardrailError, GuardrailPackError, MAX_GUARDRAIL_CONCURRENCY, MAX_GUARDRAIL_PACK_RULES, MAX_GUARDRAIL_PACKS, runGuardrails, } from "./guardrails.js";
|
|
33
33
|
export { assertIdentityActive, assertIdentityMatchesOwnership, assertIdentityPropagation, DEFAULT_IDENTITY_LIMITS, HARD_IDENTITY_LIMITS, IdentityError, identityTelemetryAttributes, narrowIdentity, ownershipFromIdentity, resolveIdentityLimits, resolveRunIdentity, } from "./identity.js";
|
|
34
34
|
export { assembleProviderInput, createDefaultInputBuilder, createDefaultPromptBuilder, EMPTY_TOOL_RESULT_TEXT, renderPromptTemplate, resolveContextProviders, } from "./input.js";
|
|
35
35
|
export { resolveInstructionInjectors, runInstructionInjectors } from "./instruction-injection.js";
|
|
@@ -72,6 +72,6 @@ export { trimTrailingSlashes } from "./trim-trailing-slashes.js";
|
|
|
72
72
|
export { MODEL_FAMILY_TOKENS, resolveModelFamily } from "./usage-estimation.js";
|
|
73
73
|
export { resolveUseCaseModel, resolveUseCaseModelBinding, useCaseCredentialProviderId, } from "./use-case-model.js";
|
|
74
74
|
export const name = "prism";
|
|
75
|
-
export const version = "0.
|
|
75
|
+
export const version = "0.10.0";
|
|
76
76
|
export const description = "Agent harness for AI providers, agents, sessions, and tools.";
|
|
77
77
|
//# sourceMappingURL=index.js.map
|
package/dist/middleware.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { ContentBlock, ExtensionEvent } from "./contracts.js";
|
|
2
|
-
export type MiddlewareHookName = "beforeProviderTurn" | "provider_request" | "input_assembly" | "prompt_build" | "context" | "tool_call" | "tool_result" | "retry" | "compaction" | "session_start" | "session_shutdown";
|
|
2
|
+
export type MiddlewareHookName = "beforeProviderTurn" | "provider_request" | "input_assembly" | "prompt_build" | "context" | "tool_call" | "tool_result" | "retry" | "compaction_request" | "compaction" | "session_start" | "session_shutdown";
|
|
3
3
|
/** Provenance of a host-answered turn (plan 096). An id, never free host code. */
|
|
4
4
|
export interface DeterministicTurnProvenance {
|
|
5
5
|
/** Answering middleware id; bounded, replay-stable, and auditable. */
|
package/dist/run-bundle.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { Agent, AgentSessionConfig, GuardrailStage, RunOptions } from "./contracts.js";
|
|
1
|
+
import type { Agent, AgentSessionConfig, GuardrailPackRef, GuardrailStage, RunOptions } from "./contracts.js";
|
|
2
2
|
/** Report format revision. Any shape change bumps this so pinned digests cannot compare across formats. */
|
|
3
3
|
export declare const RUN_BUNDLE_SCHEMA_VERSION = 1;
|
|
4
4
|
/** Inspectable projection of the inputs a run actually resolves to. Frozen JSON, safe to persist and diff. */
|
|
@@ -81,6 +81,11 @@ export interface RunBundleSnapshotInput {
|
|
|
81
81
|
readonly run?: RunOptions;
|
|
82
82
|
/** Optional memory store instance; only its kind/durability label is read, never its contents. */
|
|
83
83
|
readonly memory?: unknown;
|
|
84
|
+
/**
|
|
85
|
+
* Plan 104 Task 2: effective pack refs (`session.guardrailPackRefs`) to report instead of the
|
|
86
|
+
* caller-supplied `config.guardrailPacks` — a resumed session's enforced rows.
|
|
87
|
+
*/
|
|
88
|
+
readonly packs?: readonly GuardrailPackRef[];
|
|
84
89
|
}
|
|
85
90
|
/**
|
|
86
91
|
* Snapshots the effective run bundle: synchronous, in-memory, zero network and zero store reads.
|
package/dist/run-bundle.js
CHANGED
|
@@ -52,7 +52,10 @@ export function snapshotRunBundle(input) {
|
|
|
52
52
|
effect: tool.effect === undefined ? null : typeof tool.effect === "function" ? "classifier" : tool.effect.kind,
|
|
53
53
|
})),
|
|
54
54
|
activeSkills: run?.activeSkills ?? null,
|
|
55
|
-
guardrails: [
|
|
55
|
+
guardrails: [
|
|
56
|
+
...guardrailRows(config.guardrails, run?.guardrails),
|
|
57
|
+
...describeGuardrailPacks(input.packs ?? input.config?.guardrailPacks),
|
|
58
|
+
],
|
|
56
59
|
loop: loopIdentity(effectiveLoop),
|
|
57
60
|
thinkingLevel: run?.thinkingLevel ?? config.thinkingLevel ?? null,
|
|
58
61
|
limits: resolveRunLimits(config.limits, run?.limits),
|
package/dist/run-limits.js
CHANGED
|
@@ -10,6 +10,8 @@ export const DEFAULT_RUN_LIMITS = Object.freeze({
|
|
|
10
10
|
maxOutputTokens: 10_000,
|
|
11
11
|
maxTotalTokens: 50_000,
|
|
12
12
|
});
|
|
13
|
+
/** Stop-hook continuation cap when no layer configures one (plan 106 R1). Not a counter axis. */
|
|
14
|
+
const DEFAULT_MAX_STOP_CONTINUATIONS = 3;
|
|
13
15
|
/**
|
|
14
16
|
* Process-safety ceilings that exist so a bug cannot OOM the host via JSON.parse of giant
|
|
15
17
|
* provider frames. Product axes (turns, wall time, tokens, …) have no hard cap: hosts set
|
|
@@ -83,8 +85,13 @@ export function resolveRunLimits(agent, run) {
|
|
|
83
85
|
resolved.maxProviderAttempts = turns;
|
|
84
86
|
}
|
|
85
87
|
const maxCost = override?.maxCost ?? base?.maxCost;
|
|
88
|
+
// Stop-hook continuation cap (plan 106 R1): no counters-table row — the wrapper turns it into a
|
|
89
|
+
// clean `hook_limit` stop, not a breach — so it resolves outside the counter-backed axes and
|
|
90
|
+
// keeps the same narrowing-only law (min, `null` = uncapped).
|
|
91
|
+
const stopContinuations = minCap(base?.maxStopContinuations, override?.maxStopContinuations);
|
|
86
92
|
return Object.freeze({
|
|
87
93
|
...resolved,
|
|
94
|
+
maxStopContinuations: stopContinuations !== undefined ? stopContinuations : DEFAULT_MAX_STOP_CONTINUATIONS,
|
|
88
95
|
...(maxCost
|
|
89
96
|
? {
|
|
90
97
|
maxCost: base?.maxCost && override?.maxCost
|
|
@@ -121,6 +128,12 @@ function validateLimits(input) {
|
|
|
121
128
|
if (!Number.isFinite(amount) || amount < 0 || !currency.trim())
|
|
122
129
|
throw new TypeError("maxCost requires a finite non-negative amount and currency");
|
|
123
130
|
}
|
|
131
|
+
const stopContinuations = input.maxStopContinuations;
|
|
132
|
+
if (stopContinuations !== undefined &&
|
|
133
|
+
stopContinuations !== null &&
|
|
134
|
+
(!Number.isSafeInteger(stopContinuations) || stopContinuations < 0)) {
|
|
135
|
+
throw new TypeError("maxStopContinuations must be a non-negative safe integer or null to disable the cap");
|
|
136
|
+
}
|
|
124
137
|
return input;
|
|
125
138
|
}
|
|
126
139
|
export class RunLimitTracker {
|
|
@@ -10,14 +10,39 @@ export interface PrefixStabilityConformanceOptions {
|
|
|
10
10
|
readonly skills: readonly [Skill, Skill];
|
|
11
11
|
/** Minimum shared byte-prefix fraction between consecutive requests. Default `0.95`. */
|
|
12
12
|
readonly minContinuity?: number;
|
|
13
|
+
/**
|
|
14
|
+
* Which fraction gates the run: `"providerPrefix"` (default, today's behavior) asserts the
|
|
15
|
+
* provider-visible prefix; `"cacheablePrefix"` asserts the same measurement with tail segments
|
|
16
|
+
* removed, so a body-heavy or eager host is not failed for the tail it deliberately re-sends.
|
|
17
|
+
*/
|
|
18
|
+
readonly assertOn?: "providerPrefix" | "cacheablePrefix";
|
|
13
19
|
/** Turn inputs; defaults are fixed strings so runs are comparable across hosts. */
|
|
14
20
|
readonly inputs?: readonly [string, string];
|
|
21
|
+
/**
|
|
22
|
+
* How many request pairs may break below `minContinuity` (default `0`, today's behavior). Use
|
|
23
|
+
* `1` for an assembly that folds or evicts exactly one boundary — an attention-compiler fold,
|
|
24
|
+
* a compaction, a budget eviction. More resets than declared fail, and fewer fail too: the
|
|
25
|
+
* fixture was supposed to invalidate the prefix, so a run that never did cannot pass vacuously.
|
|
26
|
+
*/
|
|
27
|
+
readonly allowedResets?: number;
|
|
15
28
|
}
|
|
16
29
|
export interface PrefixStabilityConformanceResult {
|
|
17
30
|
/** Provider requests captured by the fixture (two per turn: skill load, then completion). */
|
|
18
31
|
readonly requests: number;
|
|
19
32
|
/** Lowest shared-prefix fraction observed across consecutive captured requests. */
|
|
20
33
|
readonly minContinuity: number;
|
|
34
|
+
/**
|
|
35
|
+
* The same lowest fraction with the session's tail segments removed from both requests of each
|
|
36
|
+
* pair — the provider-visible prefix the cache can actually keep paying for. Equals
|
|
37
|
+
* `minContinuity` when no captured request carried a tail segment.
|
|
38
|
+
*/
|
|
39
|
+
readonly cacheableContinuity: number;
|
|
40
|
+
/**
|
|
41
|
+
* 1-based indexes of the captured requests whose asserted prefix broke below `minContinuity`
|
|
42
|
+
* (the later request of each pair), in ascending order — where the assembly invalidated the
|
|
43
|
+
* prefix instead of appending. Empty when every gap stayed above the minimum.
|
|
44
|
+
*/
|
|
45
|
+
readonly resets: readonly number[];
|
|
21
46
|
}
|
|
22
47
|
/**
|
|
23
48
|
* Drive a real session through two staggered skill loads and assert that each
|
|
@@ -26,5 +51,9 @@ export interface PrefixStabilityConformanceResult {
|
|
|
26
51
|
* after the stable prefix, so the shared prefix stays intact; a host that
|
|
27
52
|
* rewrites the context block, the skill catalog, or any leading message per
|
|
28
53
|
* request fails with the offending request pair and the measured fraction.
|
|
54
|
+
* Reports both the provider-visible fraction and the same fraction with the
|
|
55
|
+
* session's tail segments removed; `assertOn` picks which one gates the run.
|
|
56
|
+
* A gap below the minimum is collected as a reset instead of failing in the
|
|
57
|
+
* loop, so `allowedResets` can permit the one boundary an assembly folds at.
|
|
29
58
|
*/
|
|
30
59
|
export declare function runPrefixStabilityConformance(options: PrefixStabilityConformanceOptions): Promise<PrefixStabilityConformanceResult>;
|
|
@@ -5,7 +5,7 @@
|
|
|
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
11
|
/**
|
|
@@ -15,6 +15,10 @@ import { createSkillRegistry } from "../skills.js";
|
|
|
15
15
|
* after the stable prefix, so the shared prefix stays intact; a host that
|
|
16
16
|
* rewrites the context block, the skill catalog, or any leading message per
|
|
17
17
|
* request fails with the offending request pair and the measured fraction.
|
|
18
|
+
* Reports both the provider-visible fraction and the same fraction with the
|
|
19
|
+
* session's tail segments removed; `assertOn` picks which one gates the run.
|
|
20
|
+
* A gap below the minimum is collected as a reset instead of failing in the
|
|
21
|
+
* loop, so `allowedResets` can permit the one boundary an assembly folds at.
|
|
18
22
|
*/
|
|
19
23
|
export async function runPrefixStabilityConformance(options) {
|
|
20
24
|
const { host, skills } = options;
|
|
@@ -34,7 +38,7 @@ export async function runPrefixStabilityConformance(options) {
|
|
|
34
38
|
...host,
|
|
35
39
|
skills: registry,
|
|
36
40
|
tools: [...hostTools, createLoadSkillTool({ registry })],
|
|
37
|
-
provider: fixtureProvider(requests, [first.name, second.name]),
|
|
41
|
+
provider: fixtureProvider(requests, [first.name, second.name], host.attentionCompiler === true || typeof host.attentionCompiler === "object"),
|
|
38
42
|
});
|
|
39
43
|
const session = agent.createSession();
|
|
40
44
|
const [firstInput, secondInput] = options.inputs ?? ["Prefix stability turn one", "Prefix stability turn two"];
|
|
@@ -42,27 +46,67 @@ export async function runPrefixStabilityConformance(options) {
|
|
|
42
46
|
await session.run(firstInput, runOptions);
|
|
43
47
|
await session.run(secondInput, runOptions);
|
|
44
48
|
assert.equal(requests.length, 4, `prefix stability conformance expected 4 provider requests (2 per staggered turn), captured ${requests.length}`);
|
|
45
|
-
|
|
49
|
+
// The session's own map holds the exact `Message` objects `appendTailSegment` allocated, so the
|
|
50
|
+
// classification is exact rather than a heuristic over host-authored content. (`tailSegments` is
|
|
51
|
+
// runtime-session state, not part of the public `AgentSession` contract, hence the narrow above.)
|
|
52
|
+
const isTail = tailClassifier(session.tailSegments);
|
|
53
|
+
const captured = requests.map((request) => measureRequest(request, isTail));
|
|
46
54
|
// Guard against a vacuous pass: both bodies must have been disclosed by the end.
|
|
47
|
-
const last =
|
|
55
|
+
const last = captured.at(-1)?.providerPrefix ?? "";
|
|
48
56
|
for (const [index, skill] of skills.entries()) {
|
|
49
57
|
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
58
|
}
|
|
59
|
+
const assertOn = options.assertOn ?? "providerPrefix";
|
|
60
|
+
const allowedResets = options.allowedResets ?? 0;
|
|
61
|
+
assert.ok(Number.isSafeInteger(allowedResets) && allowedResets >= 0, "prefix stability conformance allowedResets must be a non-negative safe integer");
|
|
51
62
|
let observed = 1;
|
|
52
|
-
let
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
63
|
+
let cacheableObserved = 1;
|
|
64
|
+
let previous = captured.at(0) ?? { providerPrefix: "", cacheablePrefix: "" };
|
|
65
|
+
// Collect every gap first: an allowed reset must not be hidden by a later assert, and the
|
|
66
|
+
// vacuity check needs the whole list to prove the fixture folded exactly as declared.
|
|
67
|
+
const gaps = [];
|
|
68
|
+
for (let index = 1; index < captured.length; index += 1) {
|
|
69
|
+
const next = captured[index] ?? previous;
|
|
70
|
+
const fraction = sharedPrefixFraction(previous.providerPrefix, next.providerPrefix);
|
|
71
|
+
const cacheableFraction = sharedPrefixFraction(previous.cacheablePrefix, next.cacheablePrefix);
|
|
56
72
|
observed = Math.min(observed, fraction);
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
73
|
+
cacheableObserved = Math.min(cacheableObserved, cacheableFraction);
|
|
74
|
+
const measured = assertOn === "cacheablePrefix" ? cacheableFraction : fraction;
|
|
75
|
+
if (measured < minContinuity)
|
|
76
|
+
gaps.push({ request: index + 1, fraction, cacheableFraction });
|
|
60
77
|
previous = next;
|
|
61
78
|
}
|
|
62
|
-
|
|
79
|
+
const resets = gaps.map((gap) => gap.request);
|
|
80
|
+
const measuredLabel = assertOn === "cacheablePrefix" ? "previous cacheable prefix (tail segments excluded)" : "previous provider prefix";
|
|
81
|
+
const minimum = (minContinuity * 100).toFixed(1);
|
|
82
|
+
const observedResets = `resets ${formatResets(resets)} of ${captured.length - 1} request pairs`;
|
|
83
|
+
const firstGap = gaps[0];
|
|
84
|
+
if (firstGap !== undefined && gaps.length > allowedResets) {
|
|
85
|
+
const measured = assertOn === "cacheablePrefix" ? firstGap.cacheableFraction : firstGap.fraction;
|
|
86
|
+
assert.fail(`prefix stability conformance: request ${firstGap.request - 1} → ${firstGap.request} kept ${(measured * 100).toFixed(1)}% of the ${measuredLabel} ` +
|
|
87
|
+
`(minimum ${minimum}%), and ${gaps.length} pair(s) broke below it (${observedResets}, allowedResets ${allowedResets}). ` +
|
|
88
|
+
"Late skill bodies must append after the stable prefix; recomposed context, an in-place skill-catalog rewrite, or any leading-message mutation invalidates it. " +
|
|
89
|
+
"Pass allowedResets for the fold, compaction, or eviction the assembly performs per run, or fix the assembly so every other gap stays byte-stable.");
|
|
90
|
+
}
|
|
91
|
+
if (gaps.length < allowedResets) {
|
|
92
|
+
assert.fail(`prefix stability conformance: allowedResets is ${allowedResets} but only ${gaps.length} pair(s) broke below the minimum (${minimum}% of the ${measuredLabel}); ${observedResets}. ` +
|
|
93
|
+
"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.");
|
|
94
|
+
}
|
|
95
|
+
return { requests: captured.length, minContinuity: observed, cacheableContinuity: cacheableObserved, resets };
|
|
63
96
|
}
|
|
64
|
-
/**
|
|
65
|
-
|
|
97
|
+
/**
|
|
98
|
+
* Deterministic reasoning block the fixture provider emits before each skill load when the host
|
|
99
|
+
* runs an attention compiler. Sized to be a real fraction of the request so the compiler's
|
|
100
|
+
* thinking stage (`thinkingKeepTurns`) has something to strip and the resulting fold is visible
|
|
101
|
+
* in the measured prefix.
|
|
102
|
+
*/
|
|
103
|
+
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);
|
|
104
|
+
/**
|
|
105
|
+
* Fixture provider: turn 1 loads `skillNames[0]`, turn 2 loads `skillNames[1]`, everything else
|
|
106
|
+
* completes. With `reasoning` (the host runs an attention compiler) each skill-load round also
|
|
107
|
+
* carries a thinking block, so the compiler's thinking stage has real content to fold.
|
|
108
|
+
*/
|
|
109
|
+
function fixtureProvider(requests, skillNames, reasoning) {
|
|
66
110
|
let call = 0;
|
|
67
111
|
return {
|
|
68
112
|
id: "prefix-stability-fixture",
|
|
@@ -72,6 +116,8 @@ function fixtureProvider(requests, skillNames) {
|
|
|
72
116
|
call += 1;
|
|
73
117
|
const skillName = skillNames[index >> 1];
|
|
74
118
|
if (index % 2 === 0 && skillName !== undefined) {
|
|
119
|
+
if (reasoning)
|
|
120
|
+
yield providerThinkingDelta(FIXTURE_THINKING);
|
|
75
121
|
yield { type: "tool_call", call: toolCallContent(`prefix-stability-${index}`, "load_skill", { name: skillName }) };
|
|
76
122
|
return;
|
|
77
123
|
}
|
|
@@ -79,15 +125,37 @@ function fixtureProvider(requests, skillNames) {
|
|
|
79
125
|
},
|
|
80
126
|
};
|
|
81
127
|
}
|
|
82
|
-
/**
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
];
|
|
90
|
-
return
|
|
128
|
+
/**
|
|
129
|
+
* Classifies a captured message as a tail segment: by object identity first (the default builder
|
|
130
|
+
* passes the session's own `Message` objects through), then by serialized equality for builders
|
|
131
|
+
* that clone messages. Takes the pre-serialized fragment so each message is serialized once.
|
|
132
|
+
*/
|
|
133
|
+
function tailClassifier(tailSegments) {
|
|
134
|
+
const identities = new Set(tailSegments.values());
|
|
135
|
+
const values = new Set([...identities].map((message) => JSON.stringify(message)));
|
|
136
|
+
return (message, fragment) => identities.has(message) || values.has(fragment);
|
|
137
|
+
}
|
|
138
|
+
/**
|
|
139
|
+
* Provider-visible payload only — messages plus the tool schema fields sent on the wire — measured
|
|
140
|
+
* twice: whole, and with tail segments dropped. One JSON fragment per message/tool so a structural
|
|
141
|
+
* array boundary never reads as a byte divergence: an appended message list stays an exact prefix
|
|
142
|
+
* of the next request.
|
|
143
|
+
*/
|
|
144
|
+
function measureRequest(request, isTail) {
|
|
145
|
+
const toolParts = (request.tools ?? []).map((tool) => JSON.stringify({ name: tool.name, description: tool.description, parameters: tool.parameters }));
|
|
146
|
+
const providerParts = [...toolParts];
|
|
147
|
+
const cacheableParts = [...toolParts];
|
|
148
|
+
for (const message of request.messages) {
|
|
149
|
+
const fragment = JSON.stringify(message);
|
|
150
|
+
providerParts.push(fragment);
|
|
151
|
+
if (!isTail(message, fragment))
|
|
152
|
+
cacheableParts.push(fragment);
|
|
153
|
+
}
|
|
154
|
+
return { providerPrefix: providerParts.join("\n"), cacheablePrefix: cacheableParts.join("\n") };
|
|
155
|
+
}
|
|
156
|
+
/** Bracket form for reset lists, e.g. `[3]` or `[3, 4]`. */
|
|
157
|
+
function formatResets(resets) {
|
|
158
|
+
return `[${resets.join(", ")}]`;
|
|
91
159
|
}
|
|
92
160
|
/** Byte-shared prefix as a fraction of the previous request, so a shrink is a cache miss. */
|
|
93
161
|
function sharedPrefixFraction(previous, next) {
|