@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.
Files changed (75) hide show
  1. package/CHANGELOG.md +24 -1
  2. package/README.md +13 -12
  3. package/dist/agent-approval.d.ts +7 -1
  4. package/dist/agent-approval.js +15 -6
  5. package/dist/agent-run-lifecycle.js +19 -5
  6. package/dist/agent-run-state.d.ts +26 -5
  7. package/dist/agent-run-state.js +97 -1
  8. package/dist/agent-session/event-subscriber.d.ts +2 -0
  9. package/dist/agent-session/event-subscriber.js +3 -0
  10. package/dist/agent-session/session/assemble.js +156 -9
  11. package/dist/agent-session/session/persist.js +11 -5
  12. package/dist/agent-session/session/provider-round.js +54 -13
  13. package/dist/agent-session/session/tool-round.d.ts +2 -2
  14. package/dist/agent-session/session/tool-round.js +58 -5
  15. package/dist/agent-session/session/types.d.ts +20 -2
  16. package/dist/agent-session/session.d.ts +65 -4
  17. package/dist/agent-session/session.js +156 -16
  18. package/dist/context-budget.d.ts +11 -0
  19. package/dist/context-budget.js +33 -2
  20. package/dist/contracts-core/agent.d.ts +26 -5
  21. package/dist/contracts-core/extensions.d.ts +3 -0
  22. package/dist/contracts-core/guardrail-packs.d.ts +8 -3
  23. package/dist/contracts-core/loop.d.ts +36 -0
  24. package/dist/contracts-core/provider.d.ts +6 -1
  25. package/dist/contracts-core/run-limits.d.ts +10 -1
  26. package/dist/contracts-protocol.d.ts +6 -4
  27. package/dist/contracts-run-state.d.ts +37 -3
  28. package/dist/contributions.d.ts +2 -1
  29. package/dist/contributions.js +1 -0
  30. package/dist/extensions.d.ts +15 -1
  31. package/dist/extensions.js +68 -0
  32. package/dist/guardrail-packs/types.d.ts +10 -0
  33. package/dist/guardrail-packs/validation-respect.js +16 -0
  34. package/dist/guardrails.d.ts +42 -1
  35. package/dist/guardrails.js +124 -15
  36. package/dist/index.d.ts +6 -6
  37. package/dist/index.js +4 -4
  38. package/dist/middleware.d.ts +1 -1
  39. package/dist/run-bundle.d.ts +6 -1
  40. package/dist/run-bundle.js +4 -1
  41. package/dist/run-limits.js +13 -0
  42. package/dist/testing/prefix-stability-conformance.d.ts +29 -0
  43. package/dist/testing/prefix-stability-conformance.js +91 -23
  44. package/dist/tools.js +10 -3
  45. package/docs/agent-events.md +12 -8
  46. package/docs/agent-session-runtime.md +9 -6
  47. package/docs/caveman.md +1 -1
  48. package/docs/compaction-llm.md +2 -0
  49. package/docs/compaction-observational-memory.md +21 -1
  50. package/docs/durable-runs.md +4 -3
  51. package/docs/embeddings.md +5 -1
  52. package/docs/execution-timeline.md +3 -2
  53. package/docs/extensions.md +20 -3
  54. package/docs/guardrails.md +16 -6
  55. package/docs/hooks.md +282 -0
  56. package/docs/index.md +18 -15
  57. package/docs/input-and-prompt-assembly.md +1 -1
  58. package/docs/instruction-injection.md +1 -0
  59. package/docs/live-testing.md +3 -1
  60. package/docs/memory-fabric.md +28 -0
  61. package/docs/middleware-hooks.md +54 -4
  62. package/docs/migration.md +13 -0
  63. package/docs/options-index.md +3 -1
  64. package/docs/policy-and-audit.md +14 -1
  65. package/docs/prefix-stability-conformance.md +57 -7
  66. package/docs/provider-packages.md +20 -20
  67. package/docs/public-contracts.md +1 -0
  68. package/docs/rag.md +93 -6
  69. package/docs/release-and-install.md +42 -39
  70. package/docs/runs-and-usage.md +17 -8
  71. package/docs/scoped-agent-memory.md +17 -9
  72. package/docs/scoped-memory.md +138 -0
  73. package/docs/tools.md +1 -1
  74. package/docs/wiki.md +4 -2
  75. package/package.json +4 -2
@@ -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
- const packs = resolveGuardrailPacks(refs, registry);
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 undefined;
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
- toolInput.push(ruleGuardrail(pack, resolved, state));
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 toolOutput.length > 0 ? { toolInput, toolOutput } : { toolInput };
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 new GuardrailPackError(`unknown guardrail pack "${input.id}"; known packs: ${[...registry.keys()].join(", ") || "none"}`);
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: input.version ?? definition?.version ?? 1,
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" or "tripwire" ("ask" has no deterministic seam)`);
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 action for a denial is `block`.
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.9.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.9.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
@@ -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. */
@@ -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.
@@ -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: [...guardrailRows(config.guardrails, run?.guardrails), ...describeGuardrailPacks(input.config?.guardrailPacks)],
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),
@@ -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
- const serialized = requests.map(serializeRequest);
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 = serialized.at(-1) ?? "";
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 previous = serialized.at(0) ?? "";
53
- for (let index = 1; index < serialized.length; index += 1) {
54
- const next = serialized[index] ?? "";
55
- const fraction = sharedPrefixFraction(previous, next);
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
- assert.ok(fraction >= minContinuity, `prefix stability conformance: request ${index} → ${index + 1} kept ${(fraction * 100).toFixed(1)}% of the previous provider prefix ` +
58
- `(minimum ${(minContinuity * 100).toFixed(1)}%). Late skill bodies must append after the stable prefix; ` +
59
- "recomposed context, an in-place skill-catalog rewrite, or any leading-message mutation invalidates it.");
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
- return { requests: serialized.length, minContinuity: observed };
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
- /** Fixture provider: turn 1 loads `skillNames[0]`, turn 2 loads `skillNames[1]`, everything else completes. */
65
- function fixtureProvider(requests, skillNames) {
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
- /** Provider-visible payload only: messages plus the tool schema fields sent on the wire. */
83
- function serializeRequest(request) {
84
- // One JSON fragment per message/tool so a structural array boundary never reads as a byte
85
- // divergence: an appended message list stays an exact prefix of the next request.
86
- const parts = [
87
- ...(request.tools ?? []).map((tool) => JSON.stringify({ name: tool.name, description: tool.description, parameters: tool.parameters })),
88
- ...request.messages.map((message) => JSON.stringify(message)),
89
- ];
90
- return parts.join("\n");
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) {