@arnilo/prism 0.8.0 → 0.9.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 (121) hide show
  1. package/CHANGELOG.md +38 -0
  2. package/README.md +11 -11
  3. package/dist/agent-approval.d.ts +11 -2
  4. package/dist/agent-event-source.d.ts +9 -1
  5. package/dist/agent-event-source.js +10 -3
  6. package/dist/agent-loops.js +7 -4
  7. package/dist/agent-run-lifecycle.d.ts +15 -1
  8. package/dist/agent-run-lifecycle.js +63 -6
  9. package/dist/agent-run-state.d.ts +22 -2
  10. package/dist/agent-run-state.js +57 -5
  11. package/dist/agent-session/helpers.js +14 -0
  12. package/dist/agent-session/session/assemble.js +126 -24
  13. package/dist/agent-session/session/persist.d.ts +11 -0
  14. package/dist/agent-session/session/persist.js +37 -11
  15. package/dist/agent-session/session/provider-round.d.ts +14 -4
  16. package/dist/agent-session/session/provider-round.js +185 -19
  17. package/dist/agent-session/session/tool-round.js +20 -1
  18. package/dist/agent-session/session/types.d.ts +25 -2
  19. package/dist/agent-session/session.d.ts +38 -4
  20. package/dist/agent-session/session.js +76 -5
  21. package/dist/attention-compiler.d.ts +51 -2
  22. package/dist/attention-compiler.js +282 -21
  23. package/dist/cache-helpers.d.ts +4 -2
  24. package/dist/cache-helpers.js +8 -6
  25. package/dist/checkpoint-restore.d.ts +45 -0
  26. package/dist/checkpoint-restore.js +54 -0
  27. package/dist/context-budget.d.ts +2 -1
  28. package/dist/context-budget.js +24 -2
  29. package/dist/contracts-core/agent.d.ts +30 -0
  30. package/dist/contracts-core/attention.d.ts +95 -0
  31. package/dist/contracts-core/content.d.ts +10 -0
  32. package/dist/contracts-core/guardrail-packs.d.ts +41 -0
  33. package/dist/contracts-core/guardrail-packs.js +2 -0
  34. package/dist/contracts-core/provider.d.ts +25 -0
  35. package/dist/contracts-core/run-limits.d.ts +19 -0
  36. package/dist/contracts-core/session.d.ts +23 -5
  37. package/dist/contracts-core/session.js +21 -2
  38. package/dist/contracts-core/usage.d.ts +40 -0
  39. package/dist/contracts-core/usage.js +8 -0
  40. package/dist/contracts-core.d.ts +2 -0
  41. package/dist/contracts-core.js +2 -0
  42. package/dist/contracts-protocol.d.ts +76 -2
  43. package/dist/contracts-run-state.d.ts +56 -1
  44. package/dist/guardrail-packs/coding-standard.d.ts +3 -0
  45. package/dist/guardrail-packs/coding-standard.js +63 -0
  46. package/dist/guardrail-packs/destructive-commands.d.ts +3 -0
  47. package/dist/guardrail-packs/destructive-commands.js +46 -0
  48. package/dist/guardrail-packs/errors.d.ts +7 -0
  49. package/dist/guardrail-packs/errors.js +9 -0
  50. package/dist/guardrail-packs/index.d.ts +4 -0
  51. package/dist/guardrail-packs/index.js +15 -0
  52. package/dist/guardrail-packs/secrets-hygiene.d.ts +3 -0
  53. package/dist/guardrail-packs/secrets-hygiene.js +23 -0
  54. package/dist/guardrail-packs/types.d.ts +16 -0
  55. package/dist/guardrail-packs/types.js +2 -0
  56. package/dist/guardrail-packs/validation-respect.d.ts +3 -0
  57. package/dist/guardrail-packs/validation-respect.js +53 -0
  58. package/dist/guardrails.d.ts +20 -1
  59. package/dist/guardrails.js +268 -0
  60. package/dist/index.d.ts +14 -9
  61. package/dist/index.js +9 -6
  62. package/dist/input.d.ts +8 -1
  63. package/dist/input.js +68 -6
  64. package/dist/middleware.d.ts +37 -2
  65. package/dist/middleware.js +41 -0
  66. package/dist/node/session-store-jsonl.js +18 -3
  67. package/dist/observability.js +6 -0
  68. package/dist/provider-events.d.ts +8 -2
  69. package/dist/provider-events.js +60 -2
  70. package/dist/providers/openai-compatible.js +6 -3
  71. package/dist/run-bundle.js +2 -1
  72. package/dist/run-limits.d.ts +11 -1
  73. package/dist/run-limits.js +46 -0
  74. package/dist/session-stores.d.ts +12 -1
  75. package/dist/session-stores.js +21 -4
  76. package/dist/testing/agent-event-source-conformance.js +41 -2
  77. package/dist/testing/prefix-stability-conformance.d.ts +30 -0
  78. package/dist/testing/prefix-stability-conformance.js +104 -0
  79. package/dist/testing/session-store-conformance.d.ts +3 -2
  80. package/dist/testing/session-store-conformance.js +48 -0
  81. package/dist/tools.d.ts +5 -0
  82. package/dist/tools.js +11 -3
  83. package/dist/usage-estimation.d.ts +29 -0
  84. package/dist/usage-estimation.js +79 -0
  85. package/docs/agent-events.md +68 -1
  86. package/docs/agent-session-runtime.md +1 -0
  87. package/docs/attention-compiler.md +89 -8
  88. package/docs/coding-agent-tools.md +1 -1
  89. package/docs/compaction-and-retry.md +1 -1
  90. package/docs/compaction-observational-memory.md +33 -6
  91. package/docs/durable-runs.md +42 -0
  92. package/docs/embeddings.md +5 -0
  93. package/docs/evaluations.md +5 -0
  94. package/docs/execution-timeline.md +78 -1
  95. package/docs/guardrails.md +38 -2
  96. package/docs/index.md +32 -13
  97. package/docs/input-and-prompt-assembly.md +3 -3
  98. package/docs/knowledge-sync.md +4 -0
  99. package/docs/middleware-hooks.md +38 -2
  100. package/docs/migrate-to-0.9.md +210 -0
  101. package/docs/migration.md +13 -0
  102. package/docs/multi-agent-patterns.md +25 -2
  103. package/docs/node-jsonl-session-store.md +7 -1
  104. package/docs/observability.md +7 -3
  105. package/docs/options-index.md +2 -1
  106. package/docs/policy-and-audit.md +13 -1
  107. package/docs/prefix-stability-conformance.md +93 -0
  108. package/docs/provider-caching.md +4 -4
  109. package/docs/provider-conformance.md +16 -0
  110. package/docs/provider-packages.md +20 -20
  111. package/docs/public-contracts.md +2 -2
  112. package/docs/rag.md +101 -3
  113. package/docs/release-and-install.md +39 -37
  114. package/docs/runs-and-usage.md +43 -6
  115. package/docs/scoped-agent-memory.md +262 -0
  116. package/docs/session-store-conformance.md +1 -2
  117. package/docs/session-stores.md +17 -17
  118. package/docs/supervisors.md +32 -12
  119. package/docs/tools.md +17 -0
  120. package/docs/workflows.md +5 -0
  121. package/package.json +5 -1
@@ -0,0 +1,210 @@
1
+ # Migrate Prism 0.8 to 0.9
2
+
3
+ > **Status: 0.9.0** (attention budget axes, turn traces, cache-stable disclosure, per-turn tool narrowing, guardrail packs, background agents, checkpoint metadata, session search, deterministic turns, shared work scopes).
4
+
5
+ This document details migration steps, behavioral changes, and compatibility notes for upgrading from Prism 0.8.0 to 0.9.0.
6
+
7
+ 0.9.0 is a lockstep minor for all **eleven** publishable packages. Node `>=22` stays the floor. **Nothing was removed**: no import path moved, no export was dropped, and every new surface defaults to 0.8 behavior — a host that only moves its dependency ranges keeps 0.8 request bytes, stores, and tool lists. The four deltas below sit inside existing surfaces, so they are readable without opting into anything.
8
+
9
+ ---
10
+
11
+ ## Behavior changes inside existing surfaces
12
+
13
+ ### 1. A limit death delivers three records, and only the last one is terminal
14
+
15
+ `run_limit_exceeded` was treated as terminal by the in-memory, NATS, and Postgres event sources and by AG-UI replay, so a consumer that stopped at the first breach record ended one record early — before the `budget_exhausted` attribution and before the run's terminal `error`. The terminal set is now exactly `agent_finished`, `agent_denied`, and `error`, decided by one exported predicate that every stream-ending site shares.
16
+
17
+ ```ts
18
+ // before — the stream could end on the breach record
19
+ for await (const item of source.subscribe({ ... })) {
20
+ if (item.record.type === "run_limit_exceeded") break; // missed budget_exhausted and the error
21
+ }
22
+
23
+ // after — the breach and its attribution are not terminal; the error is
24
+ for await (const item of source.subscribe({ ... })) {
25
+ if (isTerminalAgentEventType(item.record.type)) break; // ends on the run's error
26
+ }
27
+ ```
28
+
29
+ **Migration actions**
30
+ - Keep reading past `run_limit_exceeded` and `budget_exhausted`; the stream ends on `error`. A consumer that wants the attribution reads until `isTerminalAgentEventType(type)` is `true` (or the iterator ends).
31
+ - No configuration, no flag: this is the shipped delivery contract for pages, subscriptions, and replays. See [Agent events § Durable AgentEventSource](agent-events.md#durable-agenteventsource).
32
+
33
+ ### 2. `provider_turn_finished` carries stop reason, budgets, tools, and cache metrics
34
+
35
+ The turn event gains attributed metadata: `stopReason` from one closed taxonomy (`end_turn`, `tool_calls`, `max_output_tokens`, `content_filter`, `abort`, `provider_error`, `unknown`), a `budgets` snapshot (`inputTokens?`, `inputCap?`, `runInputBudget?`, `runInputUsed`, `turns`, `maxTurns`), the effective tool menu as counts plus `tools.idsHash`, and provider-reported `cache` counts (`cacheReadTokens?`, `cacheWriteTokens?`, `hitRate?`). `agent_finished` carries the run-level `finishReason`/`stopDetail`, `AgentRunResult.stopReason` names host-policy and loop-ceiling stops, and the execution timeline adds `turns[i].stopReason` plus `timeline.exhaustion`.
36
+
37
+ ```ts
38
+ source.subscribe({ ... }); // each provider_turn_finished.metadata:
39
+ // { latencyMs, stopReason: "tool_calls", budgets: { runInputUsed: 43_000, turns: 3, maxTurns: 16 }, tools: { count: 7, idsHash: "sha256:…" } }
40
+ ```
41
+
42
+ **Migration actions**
43
+ - Consumers that deep-equal `metadata` (or reject unknown keys) must allow the new fields; consumers that read specific keys are unaffected.
44
+ - Read `metadata.cache` only when present — unknown cache usage stays absent rather than zero-filled.
45
+
46
+ ### 3. Progressive disclosure is cache-stable
47
+
48
+ Late-expanding context (skill bodies, deferred tool schemas, loaded references) now lands at cache-stable positions: the request tail, or an explicit documented invalidation of the segment that changed. The default group order and the catalogs' slot are unchanged, so a host that never loads late context sends the same bytes as 0.8; hosts that do get append-only growth instead of a rewritten prefix.
49
+
50
+ ```ts
51
+ // assert it against your own assembly (fixture provider, network-free, no keys)
52
+ await runPrefixStabilityConformance({ agent, minContinuity: 0.95 }); // ≥95% shared serialized prefix per turn
53
+ ```
54
+
55
+ Sizing: measured 100% / 95.7% / 95.8% shared prefix on the padded fixture for three consecutive requests; `minContinuity` defaults to `0.95` and is checked over messages **and** tool schemas. Cache reads/writes and per-turn hit rate are now recorded on usage records and `provider_turn_finished.metadata.cache`. See [Prefix stability conformance](prefix-stability-conformance.md) and [Provider caching](provider-caching.md).
56
+
57
+ ### 4. A provider that reports no usage is charged a labeled estimate
58
+
59
+ A usage-less provider used to contribute zero tokens. `AgentConfig.usageEstimation` now defaults to `"fallback"`: one labeled `TokenEstimate` is recorded at the existing usage seam, and the label survives everywhere the number goes.
60
+
61
+ ```ts
62
+ const meter = session.contextMeter();
63
+ // { inputTokens: 43_000, source: "estimated", inputCap: 200_000, runInputBudget: 500_000, usedRatio: 0.215 }
64
+ const estimate = estimateMessageTokens(messages, "claude-sonnet-4.5"); // { tokens, confidence: "medium" | "low", … }
65
+ ```
66
+
67
+ **Migration actions**
68
+ - Billing or reporting code must read the `estimated` flag (and `confidence`) rather than treating every usage row as provider truth; reported usage always wins and is never overwritten.
69
+ - Set `usageEstimation: "off"` to keep the 0.8 zero-for-no-usage behavior. Estimates charge the token counters for usage-less vendors but never a price, so a configured `maxCost` stays fail-closed.
70
+
71
+ ---
72
+
73
+ ## Additive surfaces (inert unless wired)
74
+
75
+ ### 5. Attention budget axes and durable folding
76
+
77
+ `attentionCompiler.trigger` replaces the single `triggerRatio` gate with one axis, a predicate, or an any-of array: `{ kind: "input_ratio", ratio }` (the legacy axis), `{ kind: "run_input_ratio", ratio }` (fires against `RunLimits.maxInputTokens` — the case that used to be inert when the run cap sat below the model window), `{ kind: "token_floor", tokens }`, and a predicate function. Omitted, `triggerRatio` (default `0.75`) is the only axis and behavior is byte-identical to 0.8.
78
+
79
+ ```ts
80
+ const attention = createAttentionCompiler(
81
+ { trigger: [{ kind: "run_input_ratio", ratio: 0.75 }, { kind: "token_floor", tokens: 120_000 }], durable: true },
82
+ { model, runInputBudget: limits.maxInputTokens },
83
+ );
84
+ ```
85
+
86
+ Sizing: one session-store write per fold (not per turn); folding stays default-off, and `durable: true` requires a checkpoint store (`runState`) or the run throws `AgentRunStateError` before its first provider turn. See [Attention compiler](attention-compiler.md).
87
+
88
+ ### 6. Per-turn tool narrowing
89
+
90
+ `AgentConfig.toolNarrowing` / `RunOptions.toolNarrowing` (run wins) is a host callback invoked before each provider turn: it receives `{ turn, lastAssistantText?, toolIds }` and must return a subset of the run grant. Extra or unknown names are dropped — the runtime emits `tool_narrowing_clamped` with the dropped names — and a throw fails the turn instead of sending a partial schema.
91
+
92
+ ```ts
93
+ await session.run("fix the failing test", {
94
+ toolNarrowing: async ({ turn, toolIds }) => (turn > 2 ? toolIds.filter((id) => id === "read" || id === "edit") : toolIds),
95
+ });
96
+ ```
97
+
98
+ Sizing and cache cost: changing the toolset rewrites provider schemas, so pair narrowing with tool search or deferred disclosure (where the tail contract above applies), and read menu identity from `provider_turn_finished.metadata.tools.idsHash` — identical consecutive subsets keep the same hash. Absent callback: 0.8 menu, byte-identical. See [Tools](tools.md).
99
+
100
+ ### 7. Guardrail packs
101
+
102
+ `AgentSessionConfig.guardrailPacks` (or `compileGuardrailPacks(refs)` for hosts that dispatch tools directly) compiles declarative, restrictive-only rule sets onto the tool stages once per session. Four built-ins ship: `coding-standard` (`no-unrelated-file-edits`, `no-test-rewrites`), `destructive-commands`, `validation-respect`, and `secrets-hygiene`. Every pack ships a trajectory scorer (`createGuardrailPackScorer`) so enforcement can be graded, and pack denials are attributed as `pack:<pack>/<rule>`.
103
+
104
+ ```ts
105
+ const agent = createAgent({ /* … */, session: { guardrailPacks: ["secrets-hygiene", "destructive-commands"] } });
106
+ // compiled from existing seams: interruptBeforeTool, the extension kernel, enforceExecutionPolicy
107
+ ```
108
+
109
+ Sizing: `guardrailPacks` accepts at most 8 packs and 64 rules per pack; containment in `coding-standard` is lexical (`options.roots` defaults to `[process.cwd()]`, symlinks are not resolved), so an `ExecutionPolicy` stays the hard boundary. See [Guardrails](guardrails.md#guardrail-packs).
110
+
111
+ ### 8. Background (session-lifetime) child agents and child-event passthrough
112
+
113
+ `delegate` / `delegateAsync` / `spawn_agent` accept `lifetime: "session"`, `report: "on-complete" | "milestones" | "stream"`, `milestone`, and `budgetShare`; a host `SupervisorChild.policy` sets the ceiling and a model request can only narrow it (report is clamped, `everyTurns` can only be raised, share takes the lower value, session lifetime must be host-enabled). Session-lifetime children survive caller turns until `cancel_agent` / `cancel(delegationId)`. New events: `child_milestone`, `child_failed` (with the plan-087 `RunLimitBreach` attribution), `delegation_child_events_capped`, `delegation_child_events_coalesced`.
114
+
115
+ ```ts
116
+ const { delegationId } = await supervisor.delegateAsync({ childId: "researcher", input: "survey the repo", lifetime: "session", report: "milestones", milestone: { everyTurns: 3 }, budgetShare: 0.25 });
117
+ supervisor.subscribe(); // … child_milestone … child_failed (on a limit or error)
118
+ ```
119
+
120
+ Sizing: child events per delegation 256/4096, child-event bytes 32 KiB/256 KiB, child events per second 10/1000 (default/hard, per child) — 10/s is trivial for a UI, raise it only for a child whose tool events are the UI. Exceeding rate coalesces into one `delegation_child_events_coalesced` marker with the dropped count (never throws). Defaults are exactly 0.8: `lifetime: "task"`, `report: "on-complete"`, no milestone, no share, no subscription. See [Supervisors](supervisors.md) and [Multi-agent patterns](multi-agent-patterns.md).
121
+
122
+ ### 9. Checkpoint sidecar metadata and cross-layer restore hooks
123
+
124
+ Hosts attach an opaque, redacted metadata map (≤4 KiB) to every checkpoint record — git commit, document version, workspace fingerprint — without charging `maxStateBytes`, and register restore hooks that put each recorded layer back before a resume claims the run.
125
+
126
+ ```ts
127
+ createAgentRunLifecycle({ runState: { checkpointMetadata: () => ({ gitCommit: head, docVersion: "v12" }) },
128
+ restoreHooks: [async ({ metadata, signal }) => { await checkout(metadata.gitCommit, { signal }); }] });
129
+ ```
130
+
131
+ Sizing: `MAX_AGENT_RUN_METADATA_BYTES` is 4 KiB (fixed, no override), the whole map is redacted unconditionally (no public-key exemption), and each hook has a 10-second default timeout (`DEFAULT_CHECKPOINT_RESTORE_TIMEOUT_MS`) with sequential execution; any hook failure aborts the restore with `{ hook, error }`. Legacy records without metadata read as `undefined`, and an oversize or non-string map reads as absent rather than failing a resume — unused, behavior is unchanged. See [Durable runs](durable-runs.md).
132
+
133
+ ### 10. Bounded workspace session search
134
+
135
+ `SessionStore.searchSessions?(query)` is part of the store contract: filters by workspace root (`metadata.workspaceRoot`), time, provider/model, label/summary, entry kind, ownership, and an optional full-text `query`; hits carry `sessionId`, optional `leafId`, and the matched entry pointer (`entryId`, `runId`, 1-based `turn`, store `score`, bounded `snippet`) — never credentials or whole transcripts. SQLite FTS5 and the Postgres `tsvector` column are maintained additively at append time (migration 004, no background job); memory and JSONL stores scan linearly through the shared `searchLinearSessions` matcher.
136
+
137
+ ```ts
138
+ const page = await store.searchSessions!({ workspaceRoot: "/repo", query: "flake", kind: "any", limit: 20 });
139
+ const { page } = await searchSessions({ bySession, leafBySession, query: "flake" }); // linear caps apply
140
+ ```
141
+
142
+ Sizing: on the 100k-turn fixture the index is 18.8% of transcript page bytes (stored tool output is never indexed) and query p95 is 38 ms against the 100 ms ceiling; unindexed stores are O(corpus) per query and accept `maxLinearSessions` / `maxLinearEntries` / `maxLinearBytes` overrides bounded by their hard caps. See [Session stores](session-stores.md) and `examples/session-search.ts`.
143
+
144
+ ### 11. Deterministic no-model turns
145
+
146
+ The `beforeProviderTurn` middleware hook receives `BeforeProviderTurnPayload` (`sessionId`, `runId`, `turn`, `userText`) and may answer the turn from host data by returning a `DeterministicTurnAnswer` — no provider request, zero model cost, no hallucination surface. Answers are validated (`validateDeterministicTurnAnswer` / `resolveDeterministicTurn`), the turn is recorded as `deterministic` on the timeline and in usage, and `DeterministicTurnProvenance` names the middleware that produced it. `createDeterministicTurnScorer` grades the behavior in evals.
147
+
148
+ ```ts
149
+ middleware.use<BeforeProviderTurnPayload>("beforeProviderTurn", async (payload, next) =>
150
+ payload.userText.startsWith("status:")
151
+ ? { ...payload, answer: { text: await hostStatus(payload.userText), provenance: { middleware: "status" } } }
152
+ : next(payload));
153
+ ```
154
+
155
+ Sizing: no provider turn, no usage beyond a zero-cost record; the hook runs only for turns that reach the provider boundary (a turn already ended by a run limit, host turn policy, or suspension never reaches it), and host middleware is trusted code — it must not use the hook to bypass `RunLimits` or guardrails. See [Middleware hooks](middleware-hooks.md#no-model-turns-beforeproviderturn).
156
+
157
+ ### 12. Shared work scopes for observational memory
158
+
159
+ `om.attach(session, { sharedScopes })` lets several sessions contribute to and read one observational-memory scope under explicit owner grants (`controller.grant(scopeId, principalIds)` / `revoke`). Only ids bound to that exact scope are shared; the owner branch is the only grant authority; every resolve re-reads it, so revocation lands on the next read, and `onScopeAccess` audits each grant/denial.
160
+
161
+ ```ts
162
+ om.attach(session, {
163
+ appendEntry: (entry, options) => store.append(entry, options),
164
+ sharedScopes: { "build-42": { ownerSessionId, entries: (id) => store.list(id) } },
165
+ onScopeAccess: (event) => audit.info("om.scope.access", event),
166
+ });
167
+ ```
168
+
169
+ Sizing: one local write per append (flush stays local — no second writer on a branch); one branch read plus fold per participating branch per context resolve and per shared-scope recall, not per observation; 1,024 principals per scope and 256-character principal ids, on top of the existing scope caps (256 scopes, depth 8, 4,096 binds, 512-character labels). Session-private scopes stay the default: with no `sharedScopes` configured, behavior is byte-identical to 0.8. See [Compaction and observational memory](compaction-observational-memory.md#shared-work-scopes-opt-in) and `examples/shared-work-scope.ts`.
170
+
171
+ ### 13. Retrieval revocation and a zero-service reranker
172
+
173
+ Deletion and revocation propagate through derived artifacts: `createDeletionPropagator` deletes vector rows and then hands the invalidation set (`collectInvalidationIds` / `listInvalidatedIds`) to host handlers, and `repointSource` / `retireWikiSources` (plus the `createWikiDeletionHandler` / `createWikiRepointHandler` helpers) keep wiki pages and summaries consistent. `createAccessRecheck` rechecks governed sources per query and reports denials through an audit sink. Reranking no longer needs a research project: `resolveReranker({ kind: "local" })` / `createLocalReranker()` runs an in-process cross-encoder behind the `LocalRerankRuntime` seam, with the same `runRerankerConformance` contract as every other reranker; TEI and hosted adapters are unchanged.
174
+
175
+ ```ts
176
+ const reranker = resolveReranker({ kind: "local" }); // Xenova/bge-reranker-base via a host-owned runtime
177
+ const access = createAccessRecheck({ store, onDenied: audit.warn });
178
+ const propagator = createDeletionPropagator({ store, handlers: [createRagDeletionHandler(vectors), createWikiDeletionHandler(wiki)] });
179
+ ```
180
+
181
+ Sizing: the local reranker declares no inference dependency — the built-in loader resolves `@huggingface/transformers` at first use (pass `runtime` to inject your own, or `allowRemoteModels: false` for a no-network posture after the model is cached); propagation and repoint walks are bounded by `HARD_PROPAGATION_EDGES` / `HARD_REPOINT_RECORDS` and fail closed past them. See [RAG](rag.md#local-reranker), [Embeddings](embeddings.md), and [Knowledge sync](knowledge-sync.md).
182
+
183
+ ---
184
+
185
+ ## Operator / release honesty (not a host API break)
186
+
187
+ - **Compatibility baseline regenerated**: `+119` public names across `@arnilo/prism` (+49), `@arnilo/prism-memory` (+60), and `@arnilo/prism-core` (+10); **zero removals** and zero renames. One declaration change is consumer-visible at the type level: the `recordUsage` callback accepted by `generateProviderTurn` / `generateWithRetry` now returns `Promise<Usage | undefined>` instead of `Promise<void>`, so a hand-written callback that returned nothing must return the usage (or `undefined`).
188
+ - **Budgets rebaselined with recorded reasons**: root packed/unpacked/file count, per-package export ceilings, and the non-null assertion ratchet carry the measured 0.9.0 values and the plans that moved them.
189
+ - **This-tree Postgres evidence**: `release:gate` reports the durable Postgres surface as pass only when `scripts/postgres-evidence.json` matches the current `git rev-parse HEAD`; a stale phase baseline is blocked rather than inherited.
190
+ - **Version literals agree** across all eleven manifests, the lockfile, `src/index.ts`, the docs banner, the release workflow tag lists, and the generated package-truth artifact (`scripts/version-literal-gate.test.mjs`).
191
+
192
+ ## Upgrade steps
193
+
194
+ 1. Bump every `@arnilo/*` dependency and peer to `^0.9.0` (all **eleven** manifests cut together; a range that only *satisfies* 0.9.0 is refused by the release gate). The published predecessor is 0.8.0.
195
+ 2. Build and run the host suite. No import path moved, so compile errors should be limited to the `recordUsage` callback return type above and to code that deep-equals `provider_turn_finished.metadata`.
196
+ 3. Re-read §1–§4 if the host tails durable agent events, parses provider-turn metadata, uses progressive disclosure or prompt caching, or bills usage for vendors that report no usage.
197
+ 4. Adopt §5–§13 only where the host wants the new surfaces. Omitted, request bytes, stores, and tool lists stay 0.8.
198
+ 5. Run the new migration 004 on SQLite/Postgres stores if the host wants indexed session search; existing tables and columns are untouched, and 0.8 stores open unchanged.
199
+ 6. Optional: `PRISM_TEST_POSTGRES_URL=… npm run test:postgres` then `npm run release:gate` to reproduce this-tree Postgres evidence.
200
+
201
+ ## Rollback
202
+
203
+ Pin the previous published line: `@arnilo/prism@0.8.0` and its siblings, exact pins per package. Session, checkpoint, and ledger schema are unchanged across 0.8.0 → 0.9.0 apart from the additive session-search index (migration 004), which a 0.8.0 process never reads; observability rows written under 0.9.0 carry extra metadata fields that 0.8.0 ignores. Revert host config to 0.8.0 semantics by dropping `attentionCompiler.trigger` / `durable`, `toolNarrowing`, `guardrailPacks`, `usageEstimation`, `checkpointMetadata` / `restoreHooks`, `sharedScopes`, and the session-lifetime child options — every default already matches 0.8.0.
204
+
205
+ ## Related APIs
206
+
207
+ - [Migration guide](migration.md): the era index of migration cuts with replacement tables and rollback notes.
208
+ - [Migrate Prism 0.7 to 0.8](migrate-to-0.8.md): work-family import map, messaging channels, connected apps, durable runs.
209
+ - [Release and install](release-and-install.md): packed surfaces, install rules, support matrix, and the offline test budget.
210
+ - [Agent events](agent-events.md), [Runs and usage](runs-and-usage.md), [Observability](observability.md), [Tools](tools.md), [Guardrails](guardrails.md), [Supervisors](supervisors.md), [Session stores](session-stores.md): owning pages for the 0.9.0 additions.
package/docs/migration.md CHANGED
@@ -1,5 +1,18 @@
1
1
  # Migration guide
2
2
 
3
+ ## 0.8.0 → 0.9.0 (attention budget axes, turn traces, tool narrowing, guardrail packs, background agents, session search, deterministic turns, shared scopes)
4
+
5
+ **Prism 0.9.0 is a lockstep minor for all eleven publishable packages.** Node `>=22` stays the floor. Nothing was removed: no import path moved, no export was dropped, and every new surface defaults to 0.8 behavior. The full guide — the four deltas inside existing surfaces, every new option with its sizing line, upgrade steps, and rollback — is [migrate-to-0.9.md](migrate-to-0.9.md).
6
+
7
+ What a 0.8.0 host must check before upgrading:
8
+
9
+ - **A limit death no longer ends a stream early.** `run_limit_exceeded` and `budget_exhausted` are not terminal; keep reading until `error` (or `isTerminalAgentEventType(type)` is true) to see the breach, its attribution, and the run's outcome in order.
10
+ - **`provider_turn_finished` metadata grew** (`stopReason`, `budgets`, `tools`, `cache`), and `agent_finished` now carries `finishReason` / `stopDetail`; consumers that deep-equal `metadata` must allow the new fields.
11
+ - **`AgentConfig.usageEstimation` defaults to `"fallback"`**: a provider that reports no usage is charged one labeled estimate (`estimated: true` + `confidence`) instead of zero. Set `"off"` for the old behavior; billing code must read the label.
12
+ - **Progressive disclosure is cache-stable**: skill bodies and deferred schemas append at the tail instead of rewriting the prefix. Defaults keep 0.8 bytes for hosts that never load late context.
13
+ - **One type-level change**: the `recordUsage` callback of `generateProviderTurn` / `generateWithRetry` returns `Promise<Usage | undefined>` instead of `Promise<void>`.
14
+ - **Additive, inert by default**: attention `trigger` axes and `durable` folding, per-turn `toolNarrowing`, `guardrailPacks`, session-lifetime child agents with child-event passthrough, `checkpointMetadata` / `restoreHooks`, `searchSessions`, `beforeProviderTurn` deterministic turns, observability shared work scopes, deletion propagation, and the local reranker. One additive migration (004) adds the session-search index; no existing table or column changes.
15
+
3
16
  ## 0.7.0 → 0.8.0 (messaging channels, connected apps, work family, durable runs)
4
17
 
5
18
  **Prism 0.8.0 is a lockstep minor for all eleven publishable packages.** Node `>=22` stays the floor. The only import-map break is `@arnilo/prism-office` → `@arnilo/prism-work` (plus the work/document-reader subpath moves). A host that never imported those paths upgrades by moving every `@arnilo/*` dependency and peer to `^0.8.0`. The full guide — per-item actions, opt-in activation, and rollback — is [migrate-to-0.8.md](migrate-to-0.8.md).
@@ -17,7 +17,7 @@ Maps five Prism answers for "more than one agent" onto one decision table. All f
17
17
  | In-session handoff | One host, one ongoing conversation; the model decides **when** to transfer; specialists are alternate definitions of the same app | One continuous transcript chain (same store, session id, `leafId`) | Same session scope; give the specialist its own identity via its definition (`AgentConfig.identity` / `RunOptions.identity`) | Attribution is per-run: each `session.run()`'s events/result belong to the active definition — record the swap in host bookkeeping; no `delegated_agent_step` event exists for in-process swaps |
18
18
  | Hierarchical crew | A goal requires dynamic decomposition by a manager LLM, parallel execution by role specialists, host aggregation, and conditional validation/revision loop | Workflow DAG execution — each specialist executes a bounded child task session; final deliverable returns to host | Workflow tenant/ownership scopes propagate; specialists activate only their own narrowed `tools` | Workflow node events (`node_started`/`node_finished`/`agent_event`); task attribution per role in the aggregated deliverable |
19
19
  | Supervisor delegation | Host code dynamically selects a bounded child run | Separate runs; child result returns to the host | Parent identity/effectStore propagate; child factories receive derived resource/thread ids and AND-composed permission | Dedicated `delegation_started/finished/rejected/error` events, projectable through observability `handleDelegation()`; opt-in `delegation_child_event` passthrough |
20
- | In-process spawn tool | Parent model needs an allow-listed child as a non-exclusive tool call | Separate runs; sync result returns through `spawn_agent`, async handle joins through `wait_agent` | Host owns catalog, tools, scopes, limits, and local handles; schema accepts only child ID/input/thread ID/mode | Same supervisor `delegation_*` events |
20
+ | In-process spawn tool | Parent model needs an allow-listed child as a non-exclusive tool call | Separate runs; sync result returns through `spawn_agent`, async handle joins through `wait_agent` | Host owns catalog, tools, scopes, limits, and local handles; schema accepts only child ID/input/thread ID/mode plus policy args the host ceiling allows | Same supervisor `delegation_*` events; with host opt-in, `child_milestone` / `delegation_child_event` (redacted, capped, rate-coalesced) |
21
21
  | A2A 1.0 | The other agent is owned by a **different service/deployment**; cross-org or cross-cluster; needs durable task lifecycle, push configs, streaming | Protocol boundary (JSON-RPC/HTTPS agent card); replay/reconnect via host-owned task adapter | Exact-origin verified client, `A2AAuthorization` per operation, principal-scoped push configs | Host-owned task adapter records the remote lifecycle; Prism creates no worker/store |
22
22
 
23
23
  Rule of thumb: same conversation → handoff; dynamic task decomposition + parallel execution → hierarchical crew; host-selected same-process subtask → supervisor delegation; model-requested allow-listed subtask → in-process spawn tool; different deployment/trust boundary → A2A.
@@ -151,6 +151,29 @@ Live demo: [`examples/crew-hierarchy.ts`](../examples/crew-hierarchy.ts) — man
151
151
 
152
152
  Live demo: [`examples/spawn-agent-tool.ts`](../examples/spawn-agent-tool.ts) — a narrowed read-only explore child spawned twice in parallel, an uncatalogued child refused, and both handles joined.
153
153
 
154
+ ### Background agents (session lifetime)
155
+
156
+ A host can start a background child at session open that reports without occupying the conversation:
157
+
158
+ ```ts
159
+ const supervisor = createSupervisor({
160
+ ownership,
161
+ signal: sessionAbort.signal, // host session end stops every child and closes the stream
162
+ children: {
163
+ researcher: {
164
+ policy: { lifetime: "session", report: "milestones", milestone: { everyTurns: 5 }, budgetShare: 0.2 },
165
+ createAgent: ({ resourceId, threadId, permission, signal, delegate }) => createResearchAgent(/* ... */),
166
+ },
167
+ },
168
+ });
169
+
170
+ // Host code or the parent model (spawn_agent routes session lifetime to the async path):
171
+ const handle = await supervisor.delegateAsync({ childId: "researcher", input: "watch the build", lifetime: "session" });
172
+ await supervisor.wait(handle.delegationId); // join later; cancel(handle.delegationId) ends it explicitly
173
+ ```
174
+
175
+ Session-lifetime children detach from the caller and ancestor-child abort signals, hold one `activeChildren` slot until they end, and stop on `cancel_agent` / `cancel(delegationId)` or the supervisor `signal`. `budgetShare` scales the inherited steps/tool-calls/tokens/timeout limits — never above the parent or host ceiling. Reporting stays host-opt-in and redacted: `milestones` emits `child_milestone` every N turns or on a host predicate, `stream` forwards every per-turn provider/tool/turn event (never token deltas), and both are rate-coalesced at `limits.maxChildEventsPerSecond` (10/s per child default) with a `delegation_child_events_coalesced` marker. To surface them on a parent session stream, pass `childEventSink` — it receives the same redacted payload tagged `child: { childId, delegationId, depth }`, so the parent subscriber only reads `event.child`.
176
+
154
177
  ## Where Prism is stronger
155
178
 
156
179
  - **Durable Human-in-the-Loop (HITL)**: Prism workflows support durable pause and resume via [`suspend()`](workflows.md#durable-suspension-and-resumption) and [`resumeWorkflow()`](workflows.md) across worker restarts or approval gates ([Agent durable approval](agent-session-runtime.md)).
@@ -166,7 +189,7 @@ Live demo: [`examples/spawn-agent-tool.ts`](../examples/spawn-agent-tool.ts) —
166
189
  - **Narrowing on transfer, never widening.** If the specialist needs the caller's verified identity, project it through `narrowIdentity` / `assertIdentityPropagation` ([Agent identity](agent-identity.md)) so scopes and tenant cannot widen across the swap. For delegation the same discipline is built in (`narrowIdentity`, AND-composed policies); for A2A the exact-origin client plus per-operation authorization is the boundary.
167
190
  - **Manager-generated task plans are untrusted model output.** Manager plan outputs are validated against the typed schema via `ArtifactValidator` before being persisted to workflow state or dispatched to `fan_out`. Malformed or invalid plans trigger the artifact repair loop or fail closed before any specialist is invoked.
168
191
  - **Redaction of carried context.** Handoff carries the raw transcript by design — same rows a human replay would read. Apply the session egress seams on the way out: `redactSessionEntry` / `redactMessage` with a host field policy (see [Data classification](data-classification.md)) and `AgentConfig.redactor`; for durable replay across tenants reuse the redacted transcript seam discipline used by ACP `sessions.transcript` ([ACP interop](acp.md)).
169
- - **Telemetry attribution.** Which agent produced which turn is not stored on message entries; the host knows (it performed the swap or aggregated fan-out results) and should pin it per run via `RunOptions.identity` (principal kind `agent`) so `identityTelemetryAttributes` (`prism.identity.*`) carries redacted attribution on telemetry, or via observability metadata. Supervisor runs emit dedicated `delegation_*` events; an in-process definition swap has no session seam to emit one, so the host records attribution.
192
+ - **Telemetry attribution.** Which agent produced which turn is not stored on message entries; the host knows (it performed the swap or aggregated fan-out results) and should pin it per run via `RunOptions.identity` (principal kind `agent`) so `identityTelemetryAttributes` (`prism.identity.*`) carries redacted attribution on telemetry, or via observability metadata. Supervisor runs emit dedicated `delegation_*` events plus `child_failed` attribution (terminal `status`/`stopReason`, plan-086/087 `RunLimitBreach` when a ceiling fired), and `supervisor.summary()` reports per-child `attempts`/`retries`/`failures`/`failureRadius`/`outcome` — the recovery and cascade-radius counters a host cannot reconstruct from totals alone. An in-process definition swap has no session seam to emit one, so the host records attribution.
170
193
  - **Performance.** The swap performs zero provider calls; it costs one registry resolution plus one session open (~sub-millisecond in the example fixture). The transferred turn costs what any tool round costs.
171
194
 
172
195
  ## Extension and configuration notes
@@ -2,7 +2,7 @@
2
2
 
3
3
  ## What it does
4
4
 
5
- The optional `@arnilo/prism/node/session-store-jsonl` subpath stores `SessionEntry` records in a caller-named JSONL file: one JSON object per line. `searchSessions` is unsupported and throws `SessionSearchUnsupportedError` (use memory linear mode or a DB adapter for search).
5
+ The optional `@arnilo/prism/node/session-store-jsonl` subpath stores `SessionEntry` records in a caller-named JSONL file: one JSON object per line. `searchSessions` is supported as an unindexed linear scan of the file: the memory-store matcher (workspace/label/summary/`kind` filters, text `query`, cursor pagination, hit pointers) with the contract linear scan caps. Every query reads and parses the whole file (O(corpus) time and memory), so indexed SQLite/Postgres adapters remain the recommended path for search over large corpora.
6
6
 
7
7
  APIs:
8
8
 
@@ -35,6 +35,7 @@ import { createJsonlSessionStore } from "@arnilo/prism/node/session-store-jsonl"
35
35
  - `append(entry, options?)` appends one JSON line, rejects duplicate entry ids, honors `expectedParentId` existence checks, and deduplicates exact idempotency retries within this store instance. Append **fails closed** when the file already contains any corrupt or shape-invalid line (`Invalid JSONL at line N: …`) so writers cannot extend a damaged log.
36
36
  - `list(sessionId)` reads the file and returns valid entries for that session id. Corrupt or shape-invalid lines are skipped; they do not poison the whole file.
37
37
  - `get(id)` reads the file and returns the matching valid entry, if any.
38
+ - `searchSessions(query)` reads the file and runs the shared linear session matcher. Corrupt or shape-invalid lines are quarantined exactly as in `list()`/`get()`, the contract linear caps bound sessions/entries/bytes scanned, and hits carry the same shape as the indexed adapters (`sessionId`, `leafId`, `entryId`, `runId`, `turn`, `snippet`) — without `score`, since a linear scan has no index relevance.
38
39
  - `readJsonlSessionEntries(path)` returns `{ entries: SessionEntry[]; errors: SessionEntryParseError[] }` so hosts/tests can inspect per-line parse errors.
39
40
 
40
41
  Missing files read as empty stores (typed Node `ENOENT`). Invalid JSON, missing required fields, unsupported `schemaVersion`, unknown `kind`, or wrong per-kind shapes (`message`, `summary`, `model_change`, `custom`, `compaction`, `label`, `event`, `metadata`, or non-string `parentId`) are quarantined per line with line number and reason; the raw line is included in `SessionEntryParseError.raw`. Unknown entry kinds and future schema versions fail closed for reads: the line is skipped and never returned by `list()` or `get()`. For writes, any parse error blocks `append()` until the host repairs or replaces the file.
@@ -56,6 +57,10 @@ import { createJsonlSessionStore, readJsonlSessionEntries } from "@arnilo/prism/
56
57
  const store = createJsonlSessionStore("./sessions.jsonl");
57
58
  const { entries, errors } = await readJsonlSessionEntries("./sessions.jsonl");
58
59
  if (errors.length) console.warn("quarantined lines", errors);
60
+
61
+ // Linear search (unindexed): the same query API as the SQLite/Postgres adapters.
62
+ const page = await store.searchSessions!({ workspaceRoot: "/repo", query: "flake", kind: "any", limit: 20 });
63
+ // [{ sessionId, leafId, entryId, runId, turn, snippet, ... }]
59
64
  ```
60
65
 
61
66
  Use `createMemorySessionStore()` for tests or throwaway sessions; use the JSONL store when entries should survive a process restart.
@@ -73,6 +78,7 @@ Use `createMemorySessionStore()` for tests or throwaway sessions; use the JSONL
73
78
  - Errors include path/reason or line number, not file contents.
74
79
  - Do not put secrets in messages, metadata, summaries, labels, or custom entries.
75
80
  - Reads are linear in file size. Appends also re-read and re-parse the whole file for duplicate/parent/corruption checks before writing one line, and are serialized per store instance. A rejected append does not poison later appends; the rejected line is not written.
81
+ - `searchSessions` is linear in file size too (there is no index): every query reads and parses the whole file before the capped scan, so latency and peak memory grow with the corpus. Use a SQLite/Postgres `SessionStore` when search latency matters, and treat search here as resume/filter tooling on small stores.
76
82
  - There is no cross-process lock or durable idempotency table; two processes writing the same file can race. Add a database or external lock if multiple processes write the same file.
77
83
  - Treat this adapter as development/single-process storage. Production multi-writer hosts should use an indexed database `SessionStore` adapter.
78
84
 
@@ -41,7 +41,7 @@ New agent event variants (metadata only):
41
41
  | Variant | When | Key fields |
42
42
  | --- | --- | --- |
43
43
  | `provider_turn_started` | Before each provider `generate()` attempt | `turn`, `metadata: ProviderTurnMetadata` |
44
- | `provider_turn_finished` | After success or failure of that attempt | `metadata` (includes `latencyMs`, optional `httpStatus`), `usage?`, `error?` |
44
+ | `provider_turn_finished` | After success or failure of that attempt | `metadata` (includes `latencyMs`, optional `httpStatus`, `stopReason`, `budgets`, `cache`), `usage?`, `error?` |
45
45
 
46
46
  `ToolExecutionMetadata` on terminal tool events:
47
47
 
@@ -91,6 +91,7 @@ Provider turn metadata fields:
91
91
  | `latencyMs` | Set on `provider_turn_finished` |
92
92
  | `httpStatus` | Numeric `ErrorInfo.code` when present |
93
93
  | `rateLimitRemaining` / `rateLimitResetMs` | Reserved for provider adapters (optional) |
94
+ | `cache` | Provider-reported `{ cacheReadTokens?, cacheWriteTokens?, hitRate? }`; absent when cache usage is unknown. |
94
95
 
95
96
  OpenTelemetry mapping (when enabled):
96
97
 
@@ -190,8 +191,8 @@ const found = await retrieveContext("policy", { embedder, store, scope, telemetr
190
191
 
191
192
  Host cockpits and dashboard cards need fast aggregate summaries of an execution without re-walking every raw event or risking prompt/secret leaks:
192
193
 
193
- - `summarizeTimeline(timeline)`: rolls up an `ExecutionTimeline` into a `TimelineSummary` containing duration, turn count, tool call counts, provider attempts, total tokens, cost, error counts, and suspension state.
194
- - `summarizeSession(timelines)`: rolls up an array of `ExecutionTimeline`s for a session/conversation into a `SessionSummary` with aggregated tokens, costs, run counts, and duration.
194
+ - `summarizeTimeline(timeline)`: rolls up an `ExecutionTimeline` into a `TimelineSummary` containing duration, turn count (split into model vs deterministic turns), tool call counts, provider attempts, total tokens, cost, error counts, suspension state, and — for a run that died on a run limit — an `exhaustion` line (`"maxTurns exhausted (13/12); closest: maxToolCalls 0.625"`).
195
+ - `summarizeSession(timelines)`: rolls up an array of `ExecutionTimeline`s for a session/conversation into a `SessionSummary` with aggregated tokens, costs, run counts, duration, and the same model/deterministic turn split.
195
196
 
196
197
  ```ts
197
198
  import { summarizeTimeline, summarizeSession } from "@arnilo/prism-core/governance/observability";
@@ -201,6 +202,7 @@ const summary = summarizeTimeline(timeline);
201
202
  // {
202
203
  // durationMs: 1250,
203
204
  // turnCount: 2,
205
+ // turns: { model: 1, deterministic: 1 },
204
206
  // toolCallCount: 3,
205
207
  // toolCounts: { search: 2, lookup: 1 },
206
208
  // providerAttempts: 2,
@@ -210,6 +212,7 @@ const summary = summarizeTimeline(timeline);
210
212
  // blockedToolCount: 0,
211
213
  // suspended: false,
212
214
  // status: "succeeded",
215
+ // exhaustion: "maxTurns exhausted (13/12); closest: maxToolCalls 0.625", // only when a limit fired
213
216
  // }
214
217
 
215
218
  const sessionSummary = summarizeSession([run1Timeline, run2Timeline]);
@@ -218,6 +221,7 @@ const sessionSummary = summarizeSession([run1Timeline, run2Timeline]);
218
221
 
219
222
  Cardinality and correctness guarantees:
220
223
  - **Bounded cardinality**: `toolCounts` is capped to `MAX_SUMMARY_DISTINCT_TOOLS = 64` distinct tool names. If more tools are invoked, lowest-frequency tool names overflow into an `"other"` bucket.
224
+ - **Honest turn attribution**: `turns.model` counts turns with a provider step; `turns.deterministic` counts turns answered by host middleware (plan 096, `deterministic` step kind). A no-model turn is never rolled into model counts, and its usage stays absent rather than zero.
221
225
  - **No double counting**: Token usage is derived from the root run's `run_total` (or aggregated across `turn` / `provider` steps if no run-level total exists), avoiding double counting between provider turn steps and run totals. Costs are rounded to 6 decimal places to prevent floating-point drift.
222
226
  - **Payload-free**: Summaries contain counts, durations, status codes, and usage metrics only — zero prompt text, tool arguments, or credentials.
223
227
 
@@ -18,7 +18,7 @@ Field-level detail (defaults, bounds, failure modes) lives on the owning page
18
18
  | --- | --- | --- |
19
19
  | `AgentConfig` | The reusable agent: provider, model, tools, skills, stores, retry, compaction, prompts, limits | [Agent/session runtime](agent-session-runtime.md) |
20
20
  | `RunOptions` | One run's overrides: model, limits, thinking level, skills, middleware, metadata, signal | [Agent/session runtime](agent-session-runtime.md) |
21
- | `AgentSessionConfig` | Session creation: id, agent, store, branch leaf, snapshot cache TTL | [Agent/session runtime](agent-session-runtime.md) |
21
+ | `AgentSessionConfig` | Session creation: id, agent, store, branch leaf, snapshot cache TTL, guardrail packs | [Agent/session runtime](agent-session-runtime.md) |
22
22
  | `ModelConfig` | A registered model record: capabilities, limits, cost, cache and thinking metadata | [Model registry](model-registry.md) |
23
23
  | `ProviderRequestOptions` | Per-request provider hints: session/cache/header/compat/extra, applied after host policies | [Provider layer](provider-layer.md) |
24
24
 
@@ -45,6 +45,7 @@ Field-level detail (defaults, bounds, failure modes) lives on the owning page
45
45
  | `snapshotRunBundle(...)` → `RunBundleSnapshot` | Inspectable digest projection of the effective run bundle (prompt/skill/tool/guardrail digests, limits, storage kinds) | [Run bundle](run-bundle.md) |
46
46
  | `createClaimGroundingGuardrail` (`ClaimGroundingGuardrailOptions`) | `"output"`-stage guardrail that blocks or flags numeric claims no tool result or host evidence supports | [Guardrails](guardrails.md) |
47
47
  | `ErrorInfo.failureClass` (`ProviderFailureClass`) | Typed provider failure on run outcomes, ledger rows, and tool results (`quota`, `rate_limited`, `auth`, `transient`, `permanent`) | [Runs and usage](runs-and-usage.md) |
48
+ | `AgentConfig.usageEstimation` | `"fallback"` (default) records a labeled estimate when a provider reports no usage; `"off"` leaves usage absent; estimates are never priced | [Runs and usage](runs-and-usage.md#automatic-fallback-agentconfigusageestimation) |
48
49
  | `ModelConfig.capabilities.toolCallStrictness` | Advisory tool-call reliability (`"strict"` \| `"lenient"` \| `"legacy"`); catalog conformance, not a promise | [Model registry](model-registry.md) |
49
50
 
50
51
  ## Agent/session runtime
@@ -107,7 +107,19 @@ for await (const page of exportPolicyDecisions({
107
107
 
108
108
  ## Extension and configuration notes
109
109
 
110
- Policy is optional. Hosts wire `record*` helpers or `evaluateAndAppend` at permission/guardrail/tool-approval/router/connector boundaries. Model-router and work-connector packages (later Phase 8 tasks) may call the same store when configured. Replace file/memory adapters with host WORM/KMS without changing record shape.
110
+ Policy is optional. Hosts wire `record*` helpers or `evaluateAndAppend` at permission/guardrail/tool-approval/router/connector boundaries. Model-router and work-connector packages (later Phase 8 tasks) may call the same store when configured. Replace file/memory adapters with host WORM/KMS without changing record shape. Guardrail-pack denials record through `recordGuardrailDecision` like any other guardrail: the target id is the rule identity `pack:<pack>/<rule>`, `block`/`tripwire` map to outcome `deny`, and the evidence ref is `guardrail:pack:<pack>/<rule>:<stage>`. Pack config appears in run bundles as identity rows (`pack:<pack>/<rule>`, stage, `pack@version`) — never inline predicate code or tool arguments.
111
+
112
+ ## Memory retrieval ACL denials and re-pointing (plan 089)
113
+
114
+ Memory retrieval keeps its own audit events next to policy decisions; hosts forward them to the same append-only sink:
115
+
116
+ | Event | Shape | When |
117
+ | --- | --- | --- |
118
+ | `rag.acl_denied` | `{ sourceId, scope: { tenantId, resourceId, threadId }, reason: "no_grant" \| "check_failed", hits, error? }` via `retrieveContext({ onAccessDenied })` | A source was withheld at the retrieval boundary: revoked/absent/version-mismatched grant, or the grant lookup threw (`error` is redacted, capped at 256 chars) |
119
+ | `Repointed` log line + result | `repointSource()` → `{ from, to, movedChunks, rewrittenEdges, layers, batched }` | A source's grant identity moved and derived artifacts followed |
120
+ | Invalidation rows | `store.invalidate()` rows (`{ id, reason: "corrected" \| "revoked" \| "forgotten" \| "legal_hold", at }`) read back by `listInvalidatedIds()` | A source was revoked/forgotten/held; tombstones stay for explainability |
121
+
122
+ Events are per *source*, not per hit, and are emitted once per query. They never contain document text, grant contents, or credentials; `check_failed` messages pass through the same redactor as retrieved content. Denials are fail-closed: a source is excluded whether the grant is absent, revoked, or the lookup failed, and the query returns the remaining hits. Aborts are not denials and are never recorded as such.
111
123
 
112
124
  ## Security and performance notes
113
125
 
@@ -0,0 +1,93 @@
1
+ # Prefix stability conformance
2
+
3
+ ## What it does
4
+
5
+ Prefix stability conformance drives a real agent session through two staggered skill loads and asserts that each provider request keeps a byte-identical leading prefix with its predecessor — messages **and** tool schemas. It is the host-runnable form of the golden check behind [provider caching](provider-caching.md) and progressive skill disclosure: a late `load_skill` must append a body after the stable prefix instead of rewriting it.
6
+
7
+ Exported from `@arnilo/prism/testing/prefix-stability-conformance`:
8
+
9
+ - `runPrefixStabilityConformance(options)`
10
+ - `PrefixStabilityConformanceOptions`
11
+ - `PrefixStabilityConformanceResult`
12
+
13
+ ## When to use it
14
+
15
+ Use it when a host owns any part of prompt assembly — custom `inputBuilder`, `promptBuilder`, context providers, instruction injectors, input/prompt middleware, or an explicit `inputLayout` — and wants to prove that progressive disclosure still holds the cache prefix. The runner:
16
+
17
+ - installs a fixture provider (no network) that loads `skills[0]` on the first turn and `skills[1]` on the second, two provider requests per turn;
18
+ - keeps everything else in `host` exactly as production: system prompt, context providers, builders, middleware, disclosure settings;
19
+ - measures, for each consecutive captured request, the byte-shared prefix as a fraction of the previous request and fails below `minContinuity` (default `0.95`);
20
+ - fails when a loaded body never reaches a provider request, so a builder that drops the tail cannot pass vacuously.
21
+
22
+ ## Inputs / request
23
+
24
+ ```ts
25
+ import { runPrefixStabilityConformance } from "@arnilo/prism/testing/prefix-stability-conformance";
26
+
27
+ const result = await runPrefixStabilityConformance({
28
+ host: {
29
+ model: { provider: "anthropic", model: "claude-sonnet-4-6" },
30
+ systemPrompt: { text: "..." },
31
+ context: [projectContextProvider],
32
+ },
33
+ skills: [skillA, skillB],
34
+ });
35
+ ```
36
+
37
+ `PrefixStabilityConformanceOptions`:
38
+ - `host` — the host's `AgentConfig` minus `provider`, `providerSource`, and `skills`; the runner supplies the fixture provider and fixture skill registry
39
+ - `skills` — exactly two distinct `Skill` values with non-empty `instructions`, loaded in turn order
40
+ - `minContinuity?` — minimum shared-prefix fraction between consecutive requests (default `0.95`)
41
+ - `inputs?` — the two turn inputs (default fixed strings, so runs stay comparable across hosts)
42
+
43
+ ## Outputs / response / events
44
+
45
+ Returns `Promise<{ requests: number; minContinuity: number }>`: the captured request count (four) and the lowest shared-prefix fraction observed. Throws a plain `Error` naming the offending request pair and the measured percentage on the first violation. No events, no test runner, no network.
46
+
47
+ ## Request/response example
48
+
49
+ ```ts
50
+ import { runPrefixStabilityConformance } from "@arnilo/prism/testing/prefix-stability-conformance";
51
+
52
+ const { minContinuity } = await runPrefixStabilityConformance({
53
+ host: myAgentAssembly,
54
+ skills: [alphaSkill, betaSkill],
55
+ });
56
+ // throws: "request 2 → 3 kept 41.2% of the previous provider prefix (minimum 95.0%)"
57
+ // when a context block or the skill catalog is recomposed in place.
58
+ ```
59
+
60
+ ## Implementation example
61
+
62
+ ```ts
63
+ import { runPrefixStabilityConformance } from "@arnilo/prism/testing/prefix-stability-conformance";
64
+
65
+ // The runner owns the provider and skills, so the same helper is the negative control too:
66
+ // add a deliberately volatile context provider to prove the assertion can fail.
67
+ await runPrefixStabilityConformance({
68
+ host: {
69
+ model: myModel,
70
+ context: [{ name: "volatile", resolve: () => [{ title: "Now", content: `${Date.now()}` }] }],
71
+ },
72
+ skills: [alphaSkill, betaSkill],
73
+ });
74
+ ```
75
+
76
+ ## Extension and configuration notes
77
+
78
+ - Loaded skill bodies and URI resources are re-sent after new transcript content by design (the tail is append-only, not immutable); a body larger than `1 - minContinuity` of the whole prompt lowers the fraction without indicating a prefix regression. Raise the fixture's stable prefix or lower `minContinuity` for body-heavy hosts.
79
+ - Prompt builders that render skill bodies outside the tail are welcome — the check measures the provider-visible prefix, not where the body sits.
80
+ - Attention/tool-result folding and context-budget eviction are explicit invalidation boundaries: run this check on an assembly path that neither folds nor evicts, or expect the fold to reset the measured prefix at that turn.
81
+
82
+ ## Security and performance notes
83
+
84
+ - No credentials, no network, no real skills required; the fixture provider is a local generator.
85
+ - Four small provider requests per run, in-memory session store (unless `host.store` says otherwise); cheap enough for a conformance suite.
86
+
87
+ ## Related APIs
88
+
89
+ - [Provider caching](provider-caching.md)
90
+ - [Input and prompt assembly](input-and-prompt-assembly.md)
91
+ - [Context and skills](context-and-skills.md)
92
+ - [Provider conformance](provider-conformance.md)
93
+ - [Compaction conformance](compaction-conformance.md)
@@ -66,11 +66,11 @@ Cache helpers return plain data:
66
66
  | `canonicalizeJsonSchema(value)` | Clone with sorted object keys and `required` names; semantic arrays stay ordered. Used by first-party tool serializers. |
67
67
  | `cacheHitRate(usage)` | Cached input ratio or `undefined`. |
68
68
  | `cacheSavings(usage, model)` | Estimated read-token savings or `undefined` without pricing. |
69
- | `cacheUsageReport(usage, model?)` | Normalized read/write tokens, hit rate, estimated savings, and currency when available; `undefined` when no usage is supplied. |
69
+ | `cacheUsageReport(usage, model?)` | Normalized reported read/write tokens, hit rate, estimated savings, and currency when available; `undefined` when no cache token field is reported. Missing fields stay absent, never become `0`. |
70
70
 
71
- Provider events do not change. Cache accounting stays in normalized `Usage.cacheReadTokens` and `Usage.cacheWriteTokens`.
71
+ Cache accounting stays in normalized `Usage.cacheReadTokens` and `Usage.cacheWriteTokens`. Terminal `provider_turn_finished.metadata.cache` carries the same numeric report for a reporting provider; unavailable cache usage stays absent.
72
72
 
73
- For stable-prefix payloads, `inputLayout: "cache_aware"` is the default on the default input builder, `assembleProviderInput()`, `AgentConfig`, and `RunOptions`; set `inputLayout: "legacy"` to restore the prior order. The default prompt builder's cache-aware order is leading system instructions → resolved context blocks → selected/progressively disclosed skills → fallback text tool declarations → attachments/resources → summaries → prior history → pending tool results → current input. Declared tool schemas remain in `ProviderRequest.tools` and are never granted by prompt middleware. First-party tool serializers run `canonicalizeJsonSchema` so property insertion order cannot break that prefix. Changing only current input preserves the serialized message prefix before the final user suffix; changing dynamic context or loaded skills changes only from its own boundary onward, while tool schemas remain independently stable. The prefix is byte-stable only when those stable inputs are unchanged; Prism still does not guarantee provider cache hits.
73
+ For stable-prefix payloads, `inputLayout: "cache_aware"` is the default on the default input builder, `assembleProviderInput()`, `AgentConfig`, and `RunOptions`; set `inputLayout: "legacy"` to restore the prior order. The default prompt builder's cache-aware order is leading system instructions → resolved context blocks → selected/progressively disclosed skill catalogs → fallback text tool declarations → attachments/resources → summaries → prior history → pending tool results → current input → optional session tail. `RuntimeAgentSession` uses that tail for URI resources and loaded skill bodies: first insertion fixes `resource:<uri>` / `skill:<name>` order, so a later skill load appends instead of rewriting its catalog slot. Re-deriving the same id keeps its position; changed bytes explicitly invalidate from that tail segment. Declared tool schemas remain in `ProviderRequest.tools` and are never granted by prompt middleware. First-party tool serializers run `canonicalizeJsonSchema` so property insertion order cannot break that prefix. Context-budget eviction, custom builders/middleware, `toolResultFold`, and attention compilation are explicit invalidation boundaries; folding cannot move to an append-only tail without retaining the raw payload it exists to remove. The prefix is byte-stable only when those stable inputs are unchanged; Prism still does not guarantee provider cache hits.
74
74
 
75
75
  ## Request/response example
76
76
 
@@ -128,7 +128,7 @@ const retention = mapCacheRetention(hints.retention, model);
128
128
  const stamped = applyCacheControl(messages, hints.breakpoints ?? [], { maxBreakpoints: model.cache?.maxBreakpoints });
129
129
  const hitRate = cacheHitRate({ inputTokens: 1000, cacheReadTokens: 800 });
130
130
  const report = cacheUsageReport({ inputTokens: 1000, cacheReadTokens: 800 }, model);
131
- // { cacheReadTokens: 800, cacheWriteTokens: 0, hitRate: 0.8, ... }
131
+ // { cacheReadTokens: 800, hitRate: 0.8, ... }
132
132
 
133
133
  await session.run("Explain this", { inputLayout: "cache_aware" });
134
134
  ```
@@ -223,6 +223,22 @@ Canonical contract: [Thinking and reasoning](thinking-and-reasoning.md).
223
223
 
224
224
  Canonical contract: [AI SDK provider adapter](providers/ai-sdk.md).
225
225
 
226
+ ## Stop-reason checklist
227
+
228
+ Every adapter that parses a native completion reason must map it through the shared
229
+ `mapProviderStopReason` table and emit it on the normalized `done` event
230
+ (`providerDone(usage, mapped)`); `provider_turn_finished.metadata.stopReason` then carries it to
231
+ hosts (see [Agent events](agent-events.md#outputs--response--events)). Cover:
232
+
233
+ 1. **One mapped native reason per protocol** — a fake stream whose wire reason means truncation
234
+ (`finish_reason: "length"`, `stop_reason: "max_tokens"`, `finishReason: "MAX_TOKENS"`,
235
+ Converse `stopReason: "max_tokens"`) reaches `done.stopReason === "max_output_tokens"`.
236
+ 2. **Tool-call turns** — a native tool reason (`tool_calls` / `tool_use` / `tool-calls`) maps to
237
+ `tool_calls`; a generic completion reason on a turn that produced tool calls is normalized to
238
+ `tool_calls` by the session, not the adapter.
239
+ 3. **Unknown degrades** — a new or unmapped wire value yields `unknown` and never fails the stream.
240
+ 4. **No extra fields** — the adapter adds nothing else to `done`; redaction and bounds are unchanged.
241
+
226
242
  ## Extension and configuration notes
227
243
 
228
244
  The helpers are a testing subpath only. Provider packages can use them with their own mocked fetch/transport or `createMockProvider()`. Live provider tests should stay opt-in and env-gated outside Prism's default test suite.