@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
@@ -87,6 +87,7 @@ Key exports:
87
87
  | `createFoldedMemoryDetails()` | Create JSON details for compaction `data.memory`. |
88
88
  | `renderObservationalMemory()` | Render reflections and observations into a prepared memory summary. |
89
89
  | `recallObservationalMemory()` | Recover source evidence for a known observation/reflection id from supplied current-branch entries. `invalidatedIds` withholds content (`reason: "revoked"`) without injecting derived text. |
90
+ | `listInvalidatedIds()` (`@arnilo/prism-memory`) | Read the ids one exact scope currently withholds (`corrected` stays) and pass them as `invalidatedIds`, so blocks that rest on a source revoked mid-turn go stale on the next build. Empty for stores without lineage invalidation. |
90
91
  | `recallObservationalMemoryBranchPage()` | Page eligible user/assistant/tool messages around a cursor entry id (`forward`/`backward`, optional `detail: summary|full`). |
91
92
  | `createMemoryId()` / `isMemoryId()` | Create/check 12-character ids. |
92
93
  | `resolveObservationalMemorySettings()` | Merge `observational-memory` settings with defaults and overrides. |
@@ -94,7 +95,7 @@ Key exports:
94
95
  | `createObservationalMemoryRuntime()` | Low-level explicit flush for advanced hosts or tests. |
95
96
  | `createObservationalMemoryCompactionStrategy()` | Render existing folded memory as a standard Prism compaction summary with `data.memory`. |
96
97
  | `createObservationalMemoryExtension()` | Inert extension helper that registers the strategy contribution unless disabled. |
97
- | `createRecallMemoryTool()` | Optional `recall` tool factory: exact id lookup or current-branch message paging via host-supplied entries. |
98
+ | `createRecallMemoryTool()` | Optional `recall` tool factory: exact id lookup (optionally merged with granted shared scopes) or current-branch message paging via host-supplied entries. |
98
99
  | `createMemoryStatusCommand()` / `createMemoryViewCommand()` | Optional `om:status` and `om:view` command factories. |
99
100
  | `createObservationalMemoryCommands()` | Convenience factory returning status and view commands. |
100
101
 
@@ -102,14 +103,40 @@ Pure utilities create no events, workers, tools, commands, credentials, or provi
102
103
 
103
104
  ### Work-scope index (opt-in)
104
105
 
105
- `WorkScope` is a host-named, append-only index over one observational-memory ledger. Without `om.scope.*` entries, the map has only its implicit `session` root, context renders the existing active pool, and the dropper keeps its existing behavior.
106
+ `WorkScope` is a host-named, append-only index over one observational-memory ledger. Without `om.scope.*` entries, the map has only its implicit `session` root, context renders the existing active pool, and the dropper keeps its existing behavior. Shared work scopes extend that index across sessions under explicit grants — see "Shared work scopes (opt-in)" below.
106
107
 
107
- Use `createWorkScopeController({ session, appendEntry, secrets? })` to `open`, `close`, `enter`, `leave`, `bind`, or `unbind` scopes. Scope ids are host-defined (`[A-Za-z0-9._:/-]{1,128}`, no `..`); there are caps of 256 scopes, depth/stack 8, 4,096 binds per scope, and 512 characters for labels or kinds. Invalid ids, missing/closed parents, duplicate scopes, unknown record ids, and ownership mismatch fail closed. Labels and kinds receive the same secret redaction as observational-memory text.
108
+ Use `createWorkScopeController({ session, appendEntry, secrets? })` to `open`, `close`, `enter`, `leave`, `bind`, `unbind`, `grant`, or `revoke` scopes. Scope ids are host-defined (`[A-Za-z0-9._:/-]{1,128}`, no `..`); there are caps of 256 scopes, depth/stack 8, 4,096 binds and 1,024 principals per scope, and 512 characters for labels or kinds. Invalid ids, missing/closed parents, duplicate scopes, unknown record ids, reserved/closed grant targets, and ownership mismatch fail closed. Labels and kinds receive the same secret redaction as observational-memory text.
108
109
 
109
110
  `projectWorkMemory(ledger, map, { from, include, closed?, kinds? })` returns a filtered observation/reflection view plus outline. `include` is `self`, `self+ancestors`, `self+descendants`, or `lineage`; `closed: "hide"` is the default, except closed ancestors of `from` remain available. Default attached context uses the current leaf with `self+ancestors`, rendering Scope Outline, Reflections, then Observations. The compaction summary — the layer the next run's pack starts from — renders the same projection, so the full ledger never rides into the prefix; the folded payload keeps every observation, so entering another scope can still surface what that summary hid. `recallObservationalMemory()` still reads the complete current branch by exact id.
110
111
 
111
112
  After a flush records new observations or reflections, it binds those ids once to the current leaf scope only. A host promotes relevant memory explicitly by binding it to an ancestor; a reflection whose bind sits on a **closed** scope can also graduate into durable semantic memory through the fabric's `remember({ kind: "fact" | "procedure", reflectionId })`. While any host scope exists, the runtime skips the observation dropper; the folded-payload byte cap remains a storage safety cap, not working-set garbage collection. `withWorkScope(controller, spec, fn)` opens `spec` if needed, enters it, runs `fn`, and leaves in `finally`; it never closes a scope. This index does not provide resource-scoped observational memory or budget-based dropping as a working-set mechanism.
112
113
 
114
+ ### Shared work scopes (opt-in)
115
+
116
+ A shared work scope lets several sessions contribute to and read one scope under explicit owner grants. Declare it per participant in `attach()`; see `examples/shared-work-scope.ts` for a runnable grant → contribute → recall → revoke demo:
117
+
118
+ ```ts
119
+ const attached = om.attach(session, {
120
+ appendEntry: (entry, options) => store.append(entry, options),
121
+ sharedScopes: { "build-42": { ownerSessionId: "session-...", entries: (sessionId) => store.list(sessionId) } },
122
+ onScopeAccess: (event) => audit.info("om.scope.access", event),
123
+ });
124
+ ```
125
+
126
+ Requirements, all fail-closed:
127
+
128
+ - The participant opens the scope in its own branch (`open`/`enter`) and binds its own observations/reflections to it. Only ids bound to that exact scope id are shared; memory bound to an ancestor, descendant, or other scope stays private.
129
+ - The owner branch carries `om.scope.granted` / `om.scope.revoked` records (`controller.grant(scopeId, principalIds)` / `revoke`) and is the only grant authority; grants in any other branch are inert. A grant is symmetric read+write — use separate scopes for asymmetric visibility.
130
+ - The host `entries(sessionId)` callback is the store/tenant boundary: the package checks grants, it cannot verify another branch's tenant. Keep the callback inside one `OwnershipScope`.
131
+ - Absent, unknown, revoked, unreachable, or not-opened-locally scope state denies the read and reports `onScopeAccess({ granted: false, reason })`. `onScopeAccess` fires for every decision, granted or denied.
132
+ - Revocation lands on the next read: each resolve re-reads the owner branch and re-folds the grant map. The local folded payload never contains foreign observations, so revocation also holds across local compaction.
133
+
134
+ `resolveSharedScopes({ scopes, principalId, map, onAccess? })` reads the owner branch for grants, then the owner and every granted branch, folds each branch separately, and unions the id-keyed results (`mergeObservationalMemoryLedgers`). Raw entry lists are never concatenated across branches — coverage cursors and projection boundaries are positional per branch. Bound memory is merged into the context blocks, `recallObservationalMemory`, the `recall` tool (exact-id only; branch paging stays current-branch), and `om:view`; `om:status` counts stay session-local.
135
+
136
+ Rendering still follows the work-scope projection: the reader needs the shared scope in its current leaf lineage (`enter`, or a host that keeps it entered) for the context block to include it. Recall by exact id does not depend on the leaf. The compaction strategy and its folded payload stay local, so a shared observation re-enters context from the provider rather than from the summary.
137
+
138
+ Cost: one branch read and fold per participating branch per context resolve (and per `recall` call that resolves shared scopes) — not per observation. Cache per flush only if profiling demands it.
139
+
113
140
  ### Compact-when override
114
141
 
115
142
  `createObservationalMemory()` accepts a compact-when gate beside the settings: `trigger` (the same union `CompactionOptions.trigger` uses) or the `shouldCompact(context)` shorthand. When either is set it **replaces** `context.compactAfterTokens`; omitted, the token gate is unchanged.
@@ -220,7 +247,7 @@ The runtime requires host-supplied `session`, an `appendEntry` callback bound to
220
247
 
221
248
  ## Cross-session / delegation-tree recall (opt-in pattern)
222
249
 
223
- Default is per-session: `attach()` + `appendEntry` bind one store/branch, and `recallObservationalMemory(entries, id)` / `createRecallMemoryTool({ getEntries })` see only the entries the host passes for that session. Supervisor children therefore produce observations the parent cannot recall. That is acceptable for v1 — the parent transcript already contains `delegate()` results, so parent OM covers milestones. There is no package primitive for a shared workspace scope (a namespaced multi-tenant store key is out of scope).
250
+ Default is per-session: `attach()` + `appendEntry` bind one store/branch, and `recallObservationalMemory(entries, id)` / `createRecallMemoryTool({ getEntries })` see only the entries the host passes for that session. Supervisor children therefore produce observations the parent cannot recall. That is acceptable for v1 — the parent transcript already contains `delegate()` results, so parent OM covers milestones. When the host can read the participating branches, use a shared work scope instead (above); the funnel below remains the option when it cannot (a namespaced multi-tenant store key is still out of scope).
224
251
 
225
252
  Hosts that need parent recall of child *source* work compose it themselves: wrap the shared `SessionStore.append` so eligible child messages (`isEligibleObservationSourceEntry`) are copied onto a workspace (or parent) session with a **new entry id** and that session's `sessionId`/`parentId`. Parent OM then observes those copies and mints **new** observation ids. Child OM, if attached, stays on the child session with its own ids.
226
253
 
@@ -254,7 +281,7 @@ Wire the wrapped store into both the parent session and each supervisor child fa
254
281
 
255
282
  Rules that keep exact-id recall unambiguous:
256
283
 
257
- - Recall always takes **one** branch (`session.entries()` / `getEntries(sessionId)`). Never concatenate parent + child lists into one `recallObservationalMemory()` call.
284
+ - Recall always takes **one** branch (`session.entries()` / `getEntries(sessionId)`). Never concatenate parent + child lists into one `recallObservationalMemory()` call. Shared work scopes are the supported exception: they union per-branch folded ledgers (id-keyed), never raw entry lists.
258
285
  - Copies mint a new `entry.id`. `createMemorySessionStore` rejects duplicate ids globally; JSONL/DB adapters do too.
259
286
  - Do **not** rewrite the child's OM `appendEntry` onto the workspace session. After each memory append the runtime checks the entry is visible at the **child** leaf and fails closed on a session/store mismatch. Funnel messages; let parent OM observe them.
260
287
  - Do **not** copy `om.*` custom entries across. Their `sourceEntryIds` point at the origin session and would dangle on the workspace branch.
@@ -262,7 +289,7 @@ Rules that keep exact-id recall unambiguous:
262
289
 
263
290
  Cost: the workspace branch grows with every funneled child message; parent `compactAfterTokens` / observation-pool caps still apply but fire sooner. Keep the per-session default unless parent recall of child sources is required.
264
291
 
265
- Ownership: funnel only within the `OwnershipScope` already on the parent agent/store. Child factories receive that ownership from the supervisor; do not share a store across tenants or identities. Observations never leave the store the host scoped.
292
+ Ownership: funnel only within the `OwnershipScope` already on the parent agent/store. Child factories receive that ownership from the supervisor; do not share a store across tenants or identities. Observations never leave the store the host scoped — the same rule applies to shared work-scope grants.
266
293
 
267
294
  ## Security and performance notes
268
295
 
@@ -26,6 +26,47 @@ For approval suspension and batch decisions, see [Agent/session runtime § Durab
26
26
  | `persistSessionState` | Also carries loaded-skill names and the attention sticky frontier into each turn checkpoint. |
27
27
  | `includeSkillBodies` | Alongside `persistSessionState`, carries exact skill instructions. |
28
28
  | `maxStateBytes` | Save-side byte ceiling (default 256 KB, hard 1 MB). Applies to every turn checkpoint identically. |
29
+ | `checkpointMetadata` | Sidecar map (`Record<string, string>`, ≤ 4 KB, redacted) written with every checkpoint record — never inside the state value, so it costs no `maxStateBytes` budget. A function is resolved at each write, so a host closure can pin state that moves mid-run (git commit, document version). |
30
+
31
+ ```ts
32
+ let head = "commit-1";
33
+ await session.run("investigate", {
34
+ runState: {
35
+ checkpoints,
36
+ definitionRevision: "2026-09-19.1",
37
+ checkpointMetadata: () => ({ gitCommit: head, docVersion: "v12" }),
38
+ },
39
+ });
40
+ head = "commit-2"; // the next checkpoint records the new commit
41
+ ```
42
+
43
+ `AgentRunLifecycle.status()` and `loadAgentRunState()` return the record's `metadata`; `resume` accepts `checkpointMetadata` to annotate the claim write, and without it the recorded map is preserved byte-for-byte across the claim and every later write. Legacy records without metadata read as `undefined` — an oversize or non-string map reads as absent rather than failing the resume.
44
+
45
+ ### Restore hooks (all-or-nothing)
46
+
47
+ `resume` also accepts `restoreHooks`: host code that puts each external layer recorded in `checkpointMetadata` back where the checkpoint says it was. Hooks run sequentially before the claim write, each receiving the checkpoint context (`runId`, `version`, `status`, the redacted `metadata` map, and the raw `checkpoint` record) plus an `AbortSignal` that fires on host abort or the per-hook timeout.
48
+
49
+ ```ts
50
+ await lifecycle.resume(ref, { decision: "approve", expectedVersion }, {
51
+ restoreHooks: [
52
+ async function restoreGit(cp) {
53
+ await git.reset(cp.metadata?.gitCommit);
54
+ },
55
+ async function restoreDocs(cp) {
56
+ await docs.restoreVersion(cp.metadata?.docVersion);
57
+ },
58
+ ],
59
+ restoreHookTimeoutMs: 10_000, // default, per hook
60
+ });
61
+ ```
62
+
63
+ All-or-nothing:
64
+
65
+ - The first hook that throws or overruns `restoreHookTimeoutMs` (default 10 s, `DEFAULT_CHECKPOINT_RESTORE_TIMEOUT_MS`) aborts the resume with `CheckpointRestoreError` — `code: "ERR_PRISM_CHECKPOINT_RESTORE"`, `hook` naming the layer, `cause` the original error. Later hooks do not run.
66
+ - The claim write and the conversation replay happen only after every hook succeeds, so a failed restore leaves the checkpoint byte-for-byte as it was — still resumable — instead of claiming a half-restored world. The server maps the failure to `409`/`ERR_PRISM_CHECKPOINT_RESTORE`.
67
+ - Hooks run on claiming resumes only; `deny` and resuspend paths never call them.
68
+ - The claim's `agent_resumed` event carries the audit: `restore: { hooks: [{ hook, durationMs }], durationMs }`.
69
+ - Register once on the lifecycle (`createAgentRunLifecycle({ restoreHooks })`) or per resume; lifecycle-registered hooks run first. No hooks ⇒ no call, no overhead, no `restore` field.
29
70
 
30
71
  Resume uses `resumeAgentRun` / `resumeAgentRunStream` with `{ expectedVersion, decision: "continue" }`. The checkpoint records its own cadence, so a continued run keeps writing turn checkpoints without the host repeating `checkpointPolicy`.
31
72
 
@@ -82,6 +123,7 @@ The complete network-free demo — one tool execution across the crash, resumed
82
123
  ## Security and performance notes
83
124
 
84
125
  - `"continue"` is a host-API action only. Prism's AG-UI interrupt resolution accepts `approve`/`deny` only, channel adapters resume with `deny`, and there is no server route that forwards an untrusted `continue`; adding one would create an approval-bypass path.
126
+ - Restore hooks are trusted host code running outside the sandbox: they see the checkpoint's (already redacted) sidecar map and are bounded only by their timeout. Because they run before the claim write, a timeout cannot leave a claimed checkpoint pointing at un-restored external state.
85
127
  - Every gate that protects a suspension protects a continue resume: exact ownership, fencing token, fingerprint, revision, CAS version, and the absence of unresolved work. A running checkpoint is a recovery point, never an authorization.
86
128
  - Cost is one bounded checkpoint write per provider turn (same redaction and `maxStateBytes` ceiling as suspension writes). A 40-turn investigation under `"every-turn"` therefore writes 40 checkpoint rows plus the terminal save, while the default `"decision"` policy writes at most one row per approval or suspension. Each row carries the run frontier, counters, run limits, and loop snapshot — not the message history, which stays in the session store and is pointed at by `leafId` — so the store grows with turns, not with turns × transcript; a state that would exceed `maxStateBytes` (default 256 KiB, `DEFAULT_MAX_AGENT_RUN_STATE_BYTES`) fails closed rather than truncating. Pick `"every-turn"` when a worker restart must cost at most one turn of thinking, and leave the default for runs with many cheap turns.
87
129
  - Checkpoints never contain provider objects, callbacks, signals, credentials, or raw secrets; the payload is bounded and redacted like any other durable state.
@@ -87,6 +87,11 @@ await runEmbeddingsConformance({
87
87
  bridge structurally (`createAlibabaEmbedder` remains assignable to `Embedder`
88
88
  without importing it). The contract is a superset: it adds usage and per-item
89
89
  error mapping.
90
+ - The same host-seam posture covers local inference in retrieval: the RAG local
91
+ reranker (`resolveReranker({ kind: "local" })` / `createLocalReranker`) runs a
92
+ cross-encoder in the host process through a `LocalRerankRuntime`, so no
93
+ inference dependency name enters any manifest — see
94
+ [RAG local reranker](rag.md#local-reranker).
90
95
  - Adapters never auto-chunk: a batch over the provider cap rejects with
91
96
  `batch_too_large`, so `embedBatched`-style callers own batching and preserve
92
97
  per-item error attribution.
@@ -31,6 +31,7 @@ Use this package when a host needs offline quality checks or sampled live scorin
31
31
  | `createSchemaScorer` | Validates final result or named step output against JSON schema |
32
32
  | `createErrorClassScorer` | Fails closed if denied error codes or blocked executions appear on timeline |
33
33
  | `createApprovalBeforeEffectScorer` | Verifies explicit approval occurred on timeline prior to sensitive tool effect |
34
+ | `createDeterministicTurnScorer` | Requires host-answered (no-model) turns with intact provenance: `minTurns` deterministic steps, optionally from one `middleware`, and no provider request inside those turns |
34
35
  | `createCitationIntegrityScorer` | Invariant 0 on missing source, hash/span mismatch, or revoked ACL. Reads `environment.citations[]`. Ignores semantic `support`. |
35
36
  | `runComparison` | immutable dataset, 2–8 named candidates by default, pairwise scorers |
36
37
  | `assertEvaluationThreshold` / `serializeEvaluationReport` | mean/failure/per-scorer gates, hard invariant enforcement, and bounded redacted JSON |
@@ -267,6 +268,10 @@ The spawn pack (`@arnilo/prism-core/governance/evals` `spawn-pack.test.ts`) grad
267
268
 
268
269
  Negative controls wire deliberately vulnerable host compositions — uncatalogued spawn, skipped reservation, model-supplied scope escalation, leaky child tool list, non-aborting cancel, ungated ship — and assert the matching grader reports `0` naming the violation.
269
270
 
271
+ ## Guardrail-pack trajectory scenarios (plan 092)
272
+
273
+ `guardrail-pack-scenarios.test.ts` gives every built-in [guardrail pack](guardrails.md#guardrail-packs) a violating and a compliant trajectory, graded by `createGuardrailPackScorer()` on the projected timeline: a denying guardrail step scores `0` and names `metadata.guardrail` (`pack:<pack>/<rule>`), a compliant trajectory scores `1` with no pack denial, and a `forbidTools` call that executed fails as an enforcement escape instead of passing vacuously. Each scenario runs `runScenario({ agent, turns, sessionConfig: { guardrailPacks: [...] }, timeline: "metadata" })` against a scripted mock provider; a pack-absent control re-runs the destructive script with no packs to prove the blocked calls were blocked by the pack.
274
+
270
275
  ## PostgreSQL enterprise state (0.0.23)
271
276
 
272
277
  `createPostgresEnterpriseState({ pool, schema }).evaluations` implements this package's existing `EvaluationStore`. The host creates an `EvaluationRecord` from verified ownership before append; every PostgreSQL query requires tenant scope, uses exact normalized account/user matching, and returns owner-bound opaque cursor pages. It is durable across reopen and supports the existing id/scorer/session/run/trace/dataset/item/experiment/status filters.
@@ -68,6 +68,8 @@ interface ExecutionTimeline {
68
68
  readonly sessionId?: string;
69
69
  readonly workflowId?: string;
70
70
  readonly workflowRevision?: string;
71
+ /** Workflow checkpoint sidecar metadata (`WorkflowCheckpointValue.metadata`); present only when projected with a checkpoint. */
72
+ readonly workflowMetadata?: Readonly<Record<string, unknown>>;
71
73
  readonly traceId?: string;
72
74
  readonly status: string;
73
75
  readonly stopReason?: AgentFinishReason;
@@ -77,7 +79,10 @@ interface ExecutionTimeline {
77
79
  readonly input?: unknown;
78
80
  readonly result?: unknown;
79
81
  readonly usage?: Usage;
82
+ readonly cacheHitRate?: number;
80
83
  readonly steps: readonly ExecutionStep[];
84
+ readonly turns?: readonly TimelineTurn[];
85
+ readonly exhaustion?: TimelineExhaustion;
81
86
  readonly redacted: boolean;
82
87
  readonly content: TimelineContentPolicy;
83
88
  }
@@ -104,10 +109,54 @@ interface ExecutionStep {
104
109
  }
105
110
  ```
106
111
 
107
- Step kinds: `"run"`, `"turn"`, `"provider"`, `"tool"`, `"guardrail"`, `"delegation"`, `"compaction"`, `"attention"`, `"retry"`, `"hitl"`, `"artifact"`, `"workflow_node"`, `"loop_iteration"`, `"nested_workflow"`.
112
+ Step kinds: `"run"`, `"turn"`, `"deterministic"`, `"provider"`, `"tool"`, `"guardrail"`, `"delegation"`, `"compaction"`, `"attention"`, `"retry"`, `"hitl"`, `"artifact"`, `"workflow_node"`, `"loop_iteration"`, `"nested_workflow"`.
113
+
114
+ ### `TimelineTurn` and `TimelineExhaustion`
115
+
116
+ ```ts
117
+ interface TimelineTurn {
118
+ readonly turn: number;
119
+ readonly status: ExecutionStepStatus;
120
+ readonly startedAt: string;
121
+ readonly finishedAt?: string;
122
+ readonly durationMs?: number;
123
+ readonly providerAttempts: number;
124
+ readonly cacheHitRate?: number;
125
+ readonly budgets?: TurnBudgets;
126
+ readonly stopReason?: ProviderStopReason;
127
+ }
128
+
129
+ interface TimelineExhaustion {
130
+ readonly limit: RunLimitName;
131
+ readonly maximum?: number;
132
+ readonly observed?: number;
133
+ readonly currency?: string;
134
+ readonly consumed?: BudgetConsumedCounters;
135
+ readonly closestOtherAxes: readonly BudgetAxisUsage[];
136
+ readonly recentToolCalls: readonly ToolCallSummary[];
137
+ }
138
+ ```
139
+
140
+ `turns` is the per-turn trace, derived in one pass over folded provider steps: turn number, status,
141
+ timing, attempts (retries included), input-token-weighted `cacheHitRate`, the last provider
142
+ attempt's recorded `budgets`, and its stop reason. Cache rate is absent when cache usage is unknown;
143
+ `budgets` is copied verbatim from `provider_turn_finished.metadata.budgets` and is absent on legacy
144
+ events. `ExecutionTimeline.cacheHitRate` is the same input-token-weighted calculation across all
145
+ provider attempts. The stop reason also rides the `provider` step's metadata (`metadata.stopReason`),
146
+ so a flat renderer can badge attempts without walking `turns`. `turns` is absent on timelines with
147
+ no turn steps (workflow timelines).
148
+
149
+ `exhaustion` is the terminal limit attribution, present only when the run died on a run limit. It
150
+ joins `run_limit_exceeded` (`limit`, `maximum`, `observed`, `currency`) with `budget_exhausted`
151
+ (`consumed`, `closestOtherAxes`, `recentToolCalls`); a trace that recorded only the breach carries the
152
+ first group and empty axes. Argument hashes only — `recentToolCalls` never contains raw arguments.
108
153
 
109
154
  `attention_compiled` folds into a one-step `"attention"` entry (status `succeeded`) whose metadata carries the measured counts (`used`, `usedAfter`, `inputCap`, `triggerRatio`, `droppedThinkingTurns`, `stubbedToolResults`, `stubbedBytes`, `truncated`); under-ratio turns emit no event, so they add no step.
110
155
 
156
+ `deterministic_turn` folds into a `"deterministic"` step whose `name` is the answering middleware id and whose metadata carries `{ turn, middleware }`. A deterministic turn has no provider step, no `usage`, and no `stopReason`, so a host-answered turn can never be read as model output; its `turns` entry carries `providerAttempts: 0`, and `summarizeTimeline()`/`summarizeSession()` split the turn count into `turns: { model, deterministic }`. The same provenance is copied onto the assistant message as `message.metadata.deterministic = { middleware }`, so the persisted transcript alone proves the turn had no model behind it.
157
+
158
+ `guardrail_decision` folds into a `"guardrail"` step whose `name` is the stage (`input`/`output`/`tool_input`/`tool_output`) and whose metadata carries `action`, the rule identity `metadata.guardrail` (compiled packs name it `pack:<pack>/<rule>`, other guardrails their configured name), and `toolName`/`toolCallId` when the decision is tool-scoped. A denying action (`deny`, `block`, `tripwire`) sets status `denied`; the free-text guardrail reason stays on the event, not the step.
159
+
111
160
  Step statuses: `"running"`, `"succeeded"`, `"failed"`, `"blocked"`, `"skipped"`, `"suspended"`, `"denied"`, `"aborted"`.
112
161
 
113
162
  Tree structure: steps are a flat ordered array. Tree via `parentId` (run → turn → provider/tool). Scorers iterate the flat array; UIs that need nesting walk `parentId`.
@@ -138,6 +187,7 @@ const timeline = projectTraceTimeline(trace, {
138
187
  redactor: createSecretRedactor(secrets),
139
188
  });
140
189
  // timeline.steps.map(s => [s.order, s.kind, s.name, s.status])
190
+ // timeline.turns.map(t => [t.turn, t.cacheHitRate, t.budgets, t.stopReason])
141
191
  ```
142
192
 
143
193
  ### Workflow fold with checkpoint outputs
@@ -158,6 +208,33 @@ See runnable host demo in `examples/execution-timeline.ts` for offline workflow
158
208
 
159
209
  Run-level `stopReason` mirrors `agent_finished.finishReason` when the loop stopped on a ceiling or a host turn policy (`"host_policy"`); `status` reads `finished:<stopReason>` for those runs and `succeeded` for a natural end. `stopDetail` carries the host's `turnPolicy.stop` reason, bounded to 256 bytes and redacted at the runtime boundary. See [Runs and usage ledger § Clean stops and stop reasons](runs-and-usage.md#clean-stops-and-stop-reasons).
160
210
 
211
+ Per-turn stop reasons are a separate, closed taxonomy (`ProviderStopReason`: `end_turn`, `tool_calls`,
212
+ `max_output_tokens`, `content_filter`, `abort`, `provider_error`, `unknown`) because they answer a
213
+ different question — why the *provider* returned, not why the loop ended. Each `provider_turn_finished`
214
+ badges its turn (`timeline.turns[i].stopReason`) and its provider step (`metadata.stopReason`). See
215
+ [Agent events](agent-events.md) § Provider turn events.
216
+
217
+ A run that died on a run limit packs its attribution into the timeline and the summary line:
218
+
219
+ ```ts
220
+ import { projectTraceTimeline, summarizeTimeline } from "@arnilo/prism-core/governance/observability";
221
+
222
+ const timeline = projectTraceTimeline(trace);
223
+ // timeline.turns.map(t => [t.turn, t.stopReason]);
224
+ // [[1, "tool_calls"], [2, "end_turn"]]
225
+ // timeline.exhaustion;
226
+ // { limit: "maxTurns", maximum: 12, observed: 13,
227
+ // consumed: { turns: 13, inputTokens: 41_200, providerAttempts: 13, requestBytes: 1_048_576 },
228
+ // closestOtherAxes: [{ axis: "maxToolCalls", usedRatio: 0.625 }],
229
+ // recentToolCalls: [{ id: "tc_91", name: "searchCodebase", argHash: "sha256:9f.." }] }
230
+
231
+ summarizeTimeline(timeline).exhaustion;
232
+ // "maxTurns exhausted (13/12); closest: maxToolCalls 0.625"
233
+ ```
234
+
235
+ `summarizeTimeline().exhaustion` is one renderable dashboard line; runs that ended any other way omit
236
+ it, and `summarizeSession()` keeps each run's line in its `runs` array.
237
+
161
238
  ## Bounds
162
239
 
163
240
  | Dimension | Default | Hard cap |
@@ -26,6 +26,8 @@ const guardrails: Guardrails = { input: [pii], maxConcurrency: 1 };
26
26
 
27
27
  Set `AgentConfig.guardrails` for every session run or `RunOptions.guardrails` to append checks for one run. `DispatchToolCallOptions.guardrails`, workflow `RunWorkflowOptions.guardrails`, and MCP server `CreatePrismMcpServerOptions.guardrails` apply tool stages to direct calls. A stage has `Guardrail<"input" | "output" | "tool_input" | "tool_output">`, a name, optional revision, and `evaluate(context)` result.
28
28
 
29
+ `AgentSessionConfig.guardrailPacks` compiles declarative, restrictive-only rule sets onto the tool stages once per session (see [Guardrail packs](#guardrail-packs)). Session packs merge after `AgentConfig.guardrails` and before `RunOptions.guardrails`. Hosts that dispatch tools directly can compile the same config with `compileGuardrailPacks(refs)` and pass the result as `DispatchToolCallOptions.guardrails`.
30
+
29
31
  Decisions are `allow`, `block`, `tripwire`, or `interrupt`. Evaluation defaults to declaration-order sequential. `maxConcurrency` may be 1–16; records are emitted in declaration order. Thrown or malformed decisions become a fail-closed tripwire. A throwing guardrail produces a `guardrail_failed` record whose `metadata.error` carries the underlying error message — redacted and bounded to 4 KiB — so failures stay diagnosable without leaking internals. Decision reasons are capped at 4 KiB and metadata at 16 KiB after JSON normalization and optional redaction.
30
32
 
31
33
  ## Outputs / response / events
@@ -70,6 +72,40 @@ const agent = createAgent({ model, provider, guardrails: { input: [pii], output:
70
72
  await agent.createSession().run("Draft reply", { guardrails: { toolInput: [commandGuard] } });
71
73
  ```
72
74
 
75
+ ## Guardrail packs
76
+
77
+ A pack is configuration, not code: rules compile once per session onto the existing `tool_input` / `tool_output` seams. Packs can only deny or tripwire — they never grant permissions, widen arguments, or add a stage. A rule that matches produces the standard refusal-shaped `ToolResult`; `tripwire` additionally rejects the enclosing run.
78
+
79
+ ```ts
80
+ const session = agent.createSession({
81
+ guardrailPacks: ["secrets-hygiene"],
82
+ // or, with options / inline rules:
83
+ guardrailPacks: [
84
+ { id: "coding-standard", options: { cwd: "/repo", roots: ["/repo"] } },
85
+ {
86
+ id: "my-pack",
87
+ version: 1,
88
+ rules: [{ id: "no-etc", tool: "write", pattern: "^/etc/", reason: "system path" }],
89
+ },
90
+ ],
91
+ });
92
+ ```
93
+
94
+ Built-in pack ids are public surface and versioned:
95
+
96
+ | Pack | Rules | Notes |
97
+ | --- | --- | --- |
98
+ | `coding-standard` | `no-unrelated-file-edits`, `no-test-rewrites` | Applies to `write`/`edit`/`delete`/`move`. `options.roots` defaults to `[process.cwd()]`; `options.cwd` is the resolution base. Containment is lexical — symlinks are not resolved, so an `ExecutionPolicy` remains the hard boundary. |
99
+ | `destructive-commands` | `no-recursive-force-delete`, `no-long-flag-force-delete`, `no-force-push` (includes `--force-with-lease`), `no-destructive-sql`, `no-device-overwrite` | Matched against the `shell` tool's `command` argument. |
100
+ | `validation-respect` | `no-mutation-after-failed-validation` | Observes `options.validationTools` (default `test`, `run_tests`, `validate`, `validation`, `lint`, `typecheck`, `check`). A result carrying an error or a non-zero `exitCode` marks validation failed; the next successful validation clears it. Opt `shell` in explicitly when validations run through the shell tool. |
101
+ | `secrets-hygiene` | `no-secret-material-in-arguments` | Scans argument strings (bounded depth and count) for credential shapes: `sk-`, `gh[pousr]_`, `AKIA…`, PEM private-key headers, JWTs, `xox[baprs]-`. Prism redaction replaces exact known values only, so these patterns ship with the pack. |
102
+
103
+ Inline rule shape: exactly one of `pattern` (string or `RegExp`, compiled once) or `deny(args, context)` (typed predicate, host-trusted like all host code); optional `tool` (name or names; omitted matches every tool), `argPath` (dot path or paths such as `command` or `["from", "to"]`; omitted scans every argument string), `action` (`deny` default, or `tripwire`), and `reason`. Predicates receive `{ toolName, toolCallId, sessionId, runId, metadata, state }`, where `state` is pack-local and read-only. `action: "ask"` is rejected: the tool stage has no deterministic approval seam.
104
+
105
+ Every evaluated rule emits a `guardrail_decision` event; the denying record's `guardrail` is `pack:<pack>/<rule>` and its `metadata` is `{ pack, rule, version }` — never tool arguments. `describeGuardrailPacks(refs)` returns the same identity rows (`pack:<pack>/<rule>`, stage, `pack@version`) that `snapshotRunBundle()` reports for the session config. Malformed config (unknown id, duplicate pack or rule id, both `pattern` and `deny`, invalid regex, `ask`) throws `GuardrailPackError` at session creation instead of silently dropping a rule.
106
+
107
+ On the observability timeline each guardrail step carries that identity in `metadata.guardrail` (with `status: "denied"` when it denied — the free-text reason stays off the step to keep metadata low-cardinality), so evals can grade enforcement without reading tool arguments: `createGuardrailPackScorer()` from `@arnilo/prism-core/governance/evals` scores a denied `pack:` rule as a failed trajectory and names it. The built-in packs are covered by violating/compliant scenario pairs in `packages/prism-core/src/governance/evals/__tests__/guardrail-pack-scenarios.test.ts` (see [Evaluations](evaluations.md#guardrail-pack-trajectory-scenarios-plan-092)).
108
+
73
109
  ## Claim grounding
74
110
 
75
111
  `createClaimGroundingGuardrail(options: ClaimGroundingGuardrailOptions)` is a deterministic output guardrail for quantitative claims. It scans assistant text once, then attributes each number to a completed host tool result from **this run** or to a host-governed figure. It never calls a model, store, or network service.
@@ -105,13 +141,13 @@ With `onViolation: "block"`, the standard `GuardrailError` has `reason: "claim_u
105
141
 
106
142
  ## Extension and configuration notes
107
143
 
108
- Guardrails are callbacks supplied by the host. Prism does not discover, load, retry, or persist callback code. `createSecureAgent()` keeps configured guardrails and only appends run-level checks; it never lets a run remove secure defaults. Custom loops receive guarded `LoopContext.generate()` and `LoopContext.dispatchToolCall()`; host code that directly calls a provider or `ToolDefinition.execute()` is outside the runtime boundary.
144
+ Guardrails are callbacks supplied by the host. Prism does not discover, load, retry, or persist callback code. `createSecureAgent()` keeps configured guardrails and only appends run-level checks; it never lets a run remove secure defaults. Custom loops receive guarded `LoopContext.generate()` and `LoopContext.dispatchToolCall()`; host code that directly calls a provider or `ToolDefinition.execute()` is outside the runtime boundary. Guardrail packs follow the same rule: they are host-supplied config, compiled in memory per session, never discovered from disk or persisted by Prism.
109
145
 
110
146
  ## Security and performance notes
111
147
 
112
148
  Optional `@arnilo/prism-core/governance/policy` can record guardrail outcomes via `recordGuardrailDecision` (evidence refs only; see [Policy and audit](policy-and-audit.md)).
113
149
 
114
- Output buffering prevents blocked provider content from reaching subscribers, session entries, ledgers, parsers, delegation, or tools. Tool-output checks receive raw results but Prism discards blocked raw output before event, ledger, transcript, or MCP exposure. Redaction replaces exact known values only; it is not general secret detection. Parallel checks receive an abort signal, but callback code must honor it to stop in-flight work. Browser snapshots and page text from the `browser` subpath are untrusted external content: never allow them to modify tools, permissions, credentials, or policy. Browser mutations still require host `ExecutionPolicy`/approval; prompt-injection text in a page cannot grant upload/download release.
150
+ Output buffering prevents blocked provider content from reaching subscribers, session entries, ledgers, parsers, delegation, or tools. Tool-output checks receive raw results but Prism discards blocked raw output before event, ledger, transcript, or MCP exposure. Redaction replaces exact known values only; it is not general secret detection. Guardrail-pack patterns compile once at session creation and argument scans are bounded (depth 8, 64 strings, 16 KiB per string), so rule cost stays off the provider path. Parallel checks receive an abort signal, but callback code must honor it to stop in-flight work. Browser snapshots and page text from the `browser` subpath are untrusted external content: never allow them to modify tools, permissions, credentials, or policy. Browser mutations still require host `ExecutionPolicy`/approval; prompt-injection text in a page cannot grant upload/download release.
115
151
 
116
152
  ## Related APIs
117
153
 
package/docs/index.md CHANGED
@@ -2,14 +2,30 @@
2
2
 
3
3
  Prism is a TypeScript/Node.js agent harness. Hosts own providers, tools, credentials, storage, and behavior; Prism supplies contracts, registries, events, and replaceable runtime primitives.
4
4
 
5
- ## Current line (0.8.0)
5
+ ## Current line (0.9.0)
6
+
7
+ - **Attention budget axes**: `attentionCompiler.trigger` accepts one axis, a predicate, or an any-of array (`input_ratio`, `run_input_ratio`, `token_floor`), and `durable: true` keeps the fold ledger and sticky frontier in the checkpoint across a resume.
8
+ - **Turn traces and exhaustion attribution**: `provider_turn_finished` carries a closed `stopReason`, a `budgets` snapshot, the effective tool menu (`count` / `idsHash`), and provider cache counts; `agent_finished` carries the run outcome, and the execution timeline adds per-turn stop reasons plus `exhaustion`.
9
+ - **Cache-stable disclosure**: late-expanding context (skill bodies, deferred schemas, loaded references) lands at the request tail instead of rewriting the prefix, cache read/write telemetry rides usage records, and `runPrefixStabilityConformance` asserts a shared-prefix floor against a host's own assembly.
10
+ - **Per-turn tool narrowing**: a host callback receives the turn and the run grant and returns the effective subset; out-of-grant names are dropped and reported as `tool_narrowing_clamped`.
11
+ - **Usage estimation and the context meter**: labeled token estimates for providers that report no usage (reported usage always wins, and `usageEstimation: "off"` restores zero-for-no-usage), plus `session.contextMeter()` for cap and spend ratios.
12
+ - **Guardrail packs**: four built-in restrictive packs (`coding-standard`, `destructive-commands`, `validation-respect`, `secrets-hygiene`) compiled onto existing tool stages, each with a trajectory scorer.
13
+ - **Background child agents**: session-lifetime children with milestone or streamed reports, narrowed budget shares, and rate-coalesced child events.
14
+ - **Checkpoint sidecar metadata**: a redacted ≤4 KiB map attached to every checkpoint record without charging `maxStateBytes`, plus restore hooks that revert external layers before a resume claims the run.
15
+ - **Bounded session search**: `store.searchSessions(query)` over workspace/time/provider/label/kind/ownership filters, indexed at append time (SQLite FTS5, Postgres `tsvector`) with a bounded linear matcher for JSONL and memory stores.
16
+ - **Deterministic turns**: the `beforeProviderTurn` middleware hook answers a turn from host data with no provider request, recorded as `deterministic` on the timeline and in usage.
17
+ - **Shared work scopes**: explicitly granted observational-memory scopes shared across sessions, deny-by-default, audited, and revocable at the next read; session-private scopes stay the default.
18
+ - **Retrieval revocation and local reranking**: deletion and revocation propagate through derived vector/wiki artifacts under bounded walks, and an in-process cross-encoder reranker ships with no declared inference dependency.
19
+ - **Live-stream terminal semantics**: one `isTerminalAgentEventType` predicate (`agent_finished` / `agent_denied` / `error`) shared by every source, so a limit death delivers `run_limit_exceeded` → `budget_exhausted` → `error` before a stream or replay ends.
20
+ - **11 publishable packages** at current **0.9.0** lockstep, with the migration guide reachable from the release section below — inventory below.
21
+
22
+ ### Carried from the 0.8.0 line
6
23
 
7
24
  - **Messaging channels**: `@arnilo/prism-channels` transport-neutral runtime with deny-by-default authorization, owned bindings, one-use durable approvals, official Telegram (private DMs, opt-in granted groups/topics, drafts, bounded media/voice, opt-in notices) and experimental pinned signal-cli Signal.
8
25
  - **Connected apps**: identity-bound MCP server sessions admit host-selected transports and register prefixed tools; Google Workspace and Microsoft 365 HTTP adapters live under `@arnilo/prism-work/connectors`.
9
26
  - **Work family**: `@arnilo/prism-work` replaces `@arnilo/prism-office` — connectors, documents, sheets, diagrams, document-reader, sandbox, and vendored office skills. No pre-1.0 shim.
10
27
  - **Durable long runs**: turn-boundary checkpoints with host-only `decision: "continue"`, turn-stop policy, frozen run-bundle snapshots, claim-grounding guardrail, and typed provider failure classes.
11
28
  - **Honesty surfaces**: Postgres release evidence is this-commit, channel lease release stays held until the store acknowledges, and observational-memory workers ignore non-tool events on purpose.
12
- - **11 publishable packages** at current **0.8.0** lockstep, with the migration guide reachable from the release section below — inventory below.
13
29
 
14
30
  ### Carried from the 0.7.0 line
15
31
 
@@ -79,6 +95,7 @@ Prism is a TypeScript/Node.js agent harness. Hosts own providers, tools, credent
79
95
  - [Observational memory compaction subpath](compaction-observational-memory.md): source-backed observations/reflections, an optional work-scope index for the current working set, and exact-id recall; `invalidatedIds` withhold derived injection.
80
96
  - [Working and semantic memory](working-and-semantic-memory.md): working-memory store, semantic recall, pgvector path, consent lifecycle, lineage invalidation, parent-child share grants.
81
97
  - [Memory fabric](memory-fabric.md): opt-in typed notes (fact/procedure/file/working/episode) with validity windows over the existing vector and working stores.
98
+ - [Scoped agent memory design concept](scoped-agent-memory.md): workspace-scoped persistent memory — gated writes, promotion ladder, decay-based reads; case study and research basis.
82
99
  - [Session stores](session-stores.md): `SessionStore` contract, append options, branches, bounded search — start here for persistence.
83
100
  - [Conversations](conversations.md): durable user-scoped threads with versioned metadata and legal-hold-aware deletion.
84
101
  - [Work artifacts and review](work-artifacts-and-review.md): artifact attach, revision compare, evidence-bound citations, approve/reject, expiring delivery links.
@@ -213,6 +230,7 @@ Prism is a TypeScript/Node.js agent harness. Hosts own providers, tools, credent
213
230
  - [Session store conformance](session-store-conformance.md): assert append/idempotency/conflict/branch invariants for any store.
214
231
  - [Run ledger conformance](run-ledger-conformance.md): assert durable run/usage writes and reopen survival.
215
232
  - [Compaction conformance](compaction-conformance.md): assert redacted non-empty summaries and abort observation.
233
+ - [Prefix stability conformance](prefix-stability-conformance.md): assert progressive disclosure keeps the provider cache prefix stable.
216
234
  - [Tool conformance](tool-conformance.md): assert blocked-reason matrix and success-path dispatch behavior.
217
235
  - [Extension conformance](extension-conformance.md): assert inert contributions and redacted setup errors.
218
236
  - `examples/`: compile-checked typed examples ([`conversation-durable-replay.ts`](../examples/conversation-durable-replay.ts), [`artifact-review-delivery.ts`](../examples/artifact-review-delivery.ts), [`enterprise-identity.ts`](../examples/enterprise-identity.ts), [`enterprise-policy-audit.ts`](../examples/enterprise-policy-audit.ts), [`enterprise-work-connectors.ts`](../examples/enterprise-work-connectors.ts), [`connected-slack-mcp.ts`](../examples/connected-slack-mcp.ts), [`server-deployment-seams.ts`](../examples/server-deployment-seams.ts), [`neuralwatt-agent-run.ts`](../examples/neuralwatt-agent-run.ts), [`cache-aware-prompt-assembly.ts`](../examples/cache-aware-prompt-assembly.ts), [`ag-ui-server.ts`](../examples/ag-ui-server.ts), [`acp-coding-host.ts`](../examples/acp-coding-host.ts), [`telegram-agent.ts`](../examples/telegram-agent.ts), [`signal-agent.ts`](../examples/signal-agent.ts), [`messaging-agent.ts`](../examples/messaging-agent.ts), and more), plus runnable mock demos.
@@ -231,6 +249,7 @@ Prism is a TypeScript/Node.js agent harness. Hosts own providers, tools, credent
231
249
  ## Release and install
232
250
 
233
251
  - [Release and install](release-and-install.md): install rules, package graph, and deterministic resumable publication.
252
+ - [Migrate 0.8 → 0.9](migrate-to-0.9.md): the four behavior deltas inside existing surfaces (limit-death stream order, turn-trace metadata, cache-stable disclosure, labeled usage estimates), every new option with its sizing line, and 0.9.0 host migration steps.
234
253
  - [Migrate 0.7 → 0.8](migrate-to-0.8.md): work-family import map, messaging channels, connected apps, durable runs, and 0.8.0 host migration steps.
235
254
  - [Migrate 0.6 → 0.7](migrate-to-0.7.md): ACP MCP allow-list URL normalization, model router facade fail-closed governance, and 0.7.0 host migration steps.
236
255
  - [Migrate 0.5 → 0.6](migrate-to-0.6.md): Node 22 floor, folded 0.5.7 host delta, third-party floors, and upgrade/rollback steps.
@@ -247,15 +266,15 @@ The generated inventory below derives from [`scripts/package-truth.json`](../scr
247
266
 
248
267
  | package | version | notes |
249
268
  | --- | --- | --- |
250
- | `@arnilo/prism` | 0.8.0 | core — runtime, CLI/RPC, templates, docs |
251
- | `@arnilo/prism-channels` | 0.8.0 | family — transport-neutral messaging runtime, durable journal, pairing and one-use approvals; official /telegram (private DMs, opt-in granted groups/topics) and experimental pinned signal-cli /signal |
252
- | `@arnilo/prism-coding-tools` | 0.8.0 | family — /agent, /security, /openapi, /computer-use-linux, /dev, /caveman, /ponytail, /impeccable subpaths |
253
- | `@arnilo/prism-core` | 0.8.0 | family — /runtime, /sessions, /governance, /credentials, /enterprise, /validation subpaths |
254
- | `@arnilo/prism-providers` | 0.8.0 | family — all provider adapters as `/<adapter>` subpaths |
255
- | `@arnilo/prism-acp-agent` | 0.8.0 | capability — ACP adapter |
256
- | `@arnilo/prism-ag-ui` | 0.8.0 | capability — AG-UI/A2A/A2UI adapter |
257
- | `@arnilo/prism-mcp` | 0.8.0 | capability — MCP client/server/OAuth interop |
258
- | `@arnilo/prism-memory` | 0.8.0 | capability — memory plus /rag, /compaction/*, /fabric, /graft, /wiki subpaths |
259
- | `@arnilo/prism-web-tools` | 0.8.0 | capability — Brave/Exa/Firecrawl plus peer-gated /browser and /obscura subpaths |
260
- | `@arnilo/prism-work` | 0.8.0 | capability — /connectors, /documents, /sheets, /diagrams, /document-reader, /sandbox, /skills, /tools subpaths |
269
+ | `@arnilo/prism` | 0.9.0 | core — runtime, CLI/RPC, templates, docs |
270
+ | `@arnilo/prism-channels` | 0.9.0 | family — transport-neutral messaging runtime, durable journal, pairing and one-use approvals; official /telegram (private DMs, opt-in granted groups/topics) and experimental pinned signal-cli /signal |
271
+ | `@arnilo/prism-coding-tools` | 0.9.0 | family — /agent, /security, /openapi, /computer-use-linux, /dev, /caveman, /ponytail, /impeccable subpaths |
272
+ | `@arnilo/prism-core` | 0.9.0 | family — /runtime, /sessions, /governance, /credentials, /enterprise, /validation subpaths |
273
+ | `@arnilo/prism-providers` | 0.9.0 | family — all provider adapters as `/<adapter>` subpaths |
274
+ | `@arnilo/prism-acp-agent` | 0.9.0 | capability — ACP adapter |
275
+ | `@arnilo/prism-ag-ui` | 0.9.0 | capability — AG-UI/A2A/A2UI adapter |
276
+ | `@arnilo/prism-mcp` | 0.9.0 | capability — MCP client/server/OAuth interop |
277
+ | `@arnilo/prism-memory` | 0.9.0 | capability — memory plus /rag, /compaction/*, /fabric, /graft, /wiki subpaths |
278
+ | `@arnilo/prism-web-tools` | 0.9.0 | capability — Brave/Exa/Firecrawl plus peer-gated /browser and /obscura subpaths |
279
+ | `@arnilo/prism-work` | 0.9.0 | capability — /connectors, /documents, /sheets, /diagrams, /document-reader, /sandbox, /skills, /tools subpaths |
261
280
  <!-- generated:package-truth:inventory end -->
@@ -62,7 +62,7 @@ Useful exported types:
62
62
  - `InputAttachment`: already-loaded text/content blocks (including `audio`, `file`, and `document`) or an explicit URI loaded through a caller-provided `ResourceLoader`.
63
63
  - `PromptInstruction`: labeled system instruction text.
64
64
  - `DefaultPromptBuilder`: the default `PromptBuilder`; cache-aware by default and legacy-preserving when `inputLayout: "legacy"` is passed in its request.
65
- - `AssembleProviderInputOptions`: model, input, optional builders, context providers, selected skills, active tools, generic provider options, metadata, signal, and optional `contextBudget` (`maxInputTokens` / `maxInputBytes` / `reportOmissions` / `tokenEstimator`).
65
+ - `AssembleProviderInputOptions`: model, input, optional builders, context providers, selected skills, active tools, generic provider options, metadata, signal, optional session-owned `tailSegments`, and optional `contextBudget` (`maxInputTokens` / `maxInputBytes` / `reportOmissions` / `tokenEstimator`).
66
66
  - `applyContextBudget` / `getContextBudgetReport` / `resolveContextBudget`: deterministic eviction + omission report helpers (estimate = UTF-16 code units ÷ 4, or the host's `tokenEstimator`).
67
67
  - `PromptTemplateOptions`: missing-variable behavior for `renderPromptTemplate()`.
68
68
 
@@ -81,10 +81,10 @@ The builder returns `readonly Message[]`.
81
81
 
82
82
  The default prompt builder preserves one composition path while honoring layout:
83
83
 
84
- - `cache_aware` (default): leading system messages from input assembly → resolved context blocks → selected/progressively disclosed skills → text tool declarations for text-only/unknown models → remaining input-builder messages (attachments/resources → summaries → history → tool results → current input).
84
+ - `cache_aware` (default): leading system messages from input assembly → resolved context blocks → selected/progressively disclosed skill catalogs → text tool declarations for text-only/unknown models → remaining input-builder messages (attachments/resources → summaries → history → tool results → current input) → optional session tail.
85
85
  - `legacy`: context blocks → skills → text tool declarations → all input-builder messages (instructions → summaries → history → current input → attachments/resources → tool results).
86
86
 
87
- In cache-aware mode, leading system instructions form the stable boundary before dynamic context and skills. The provider `tools` field remains the host-supplied schema list; text declarations are only a fallback for models without declared tool support. Changing only current input changes the final suffix; changing context, loaded skills, resources, summaries, history, attachments, or tools changes that boundary or a later suffix. A stable prefix persists only while those stable inputs stay byte-stable; provider cache hits remain best-effort.
87
+ In cache-aware mode, leading system instructions form the stable boundary before dynamic context and skill catalogs. The provider `tools` field remains the host-supplied schema list; text declarations are only a fallback for models without declared tool support. `RuntimeAgentSession` supplies a run-owned `tailSegments` map: URI resources and loaded skill bodies move to the final tail, while their catalog rows remain in place. First insertion fixes tail order (`resource:<uri>` / `skill:<name>`); re-derivation of an id replaces only that segment's bytes, making changed source content an explicit cache-invalidation boundary. Context-budget eviction can omit a tail segment. Custom prompt builders receive the tail in `messages` plus `tailSkillBodies`; a builder that independently renders `skills` must honor that flag. A stable prefix persists only while those stable inputs stay byte-stable; provider cache hits remain best-effort.
88
88
  - History is prepended before current input.
89
89
  - Instructions and summaries are system messages; compacted branch summaries from `rebuildSessionContext()` use the same path.
90
90
  - Text attachments and explicit text resources are user messages; inline `audio`/`file`/`document` blocks pass through unchanged on attachments with `content`.
@@ -29,6 +29,10 @@ Use it when a host must keep a RAG corpus aligned with an enterprise file source
29
29
 
30
30
  No tools, no watch-channel authorization, no events.
31
31
 
32
+ ## Delete contract
33
+
34
+ A connector `delete` change calls `deleteSource()` for that source only: the connector's own chunk rows and ingestion status go away, under exact tenant/resource/corpus scope. Connector payloads are never authorization, so sync cannot reach beyond the corpus it owns — derived artifacts (summaries, observational-memory entries, compiled wiki pages, host projections) survive sync deletes by design. Removing those is a separate privileged pass through `createDeletionPropagator().propagate(sourceId)` (see [RAG deletion propagation](rag.md#deletion-propagation)), driven by the host, not by the connector.
35
+
32
36
  ## Request/response example
33
37
 
34
38
  ```json
@@ -14,7 +14,7 @@ APIs:
14
14
 
15
15
  Use middleware hooks when a host wants extension/package code to observe or transform a value at a named runtime boundary.
16
16
 
17
- Do not use middleware hooks as a provider adapter, prompt builder, retry policy, compaction strategy, tool dispatcher, permission system, or agent/session runtime.
17
+ Do not use middleware hooks as a provider adapter, prompt builder, retry policy, compaction strategy, tool dispatcher, permission system, or agent/session runtime. Per-turn tool menus use `AgentConfig.toolNarrowing` / `RunOptions.toolNarrowing`, not a middleware hook — see [Tools](tools.md).
18
18
 
19
19
  ## Inputs / request
20
20
 
@@ -24,6 +24,7 @@ createMiddlewareRegistry(options?: MiddlewareRegistryOptions): MiddlewareRegistr
24
24
 
25
25
  Built-in hook names:
26
26
 
27
+ - `beforeProviderTurn`
27
28
  - `provider_request`
28
29
  - `input_assembly`
29
30
  - `prompt_build`
@@ -47,7 +48,7 @@ Built-in hook names:
47
48
 
48
49
  ## Outputs / response / events
49
50
 
50
- `run()` returns the transformed value. If no middleware is registered for a hook, `run()` returns the original value. `assembleProviderInput()` calls Phase 5 hooks in this order when middleware is supplied: `input_assembly`, then `context`, then `prompt_build`. The `input_assembly` call is unconditional — it runs after whatever `InputBuilder` produced the messages, so host middleware at that hook cannot be skipped by a custom builder. The agent/session runtime applies configured provider request policies, then invokes `provider_request` once with the `ProviderRequest` before `AIProvider.generate()`, invokes `tool_call` and `tool_result` through `dispatchToolCall()` for complete provider tool calls, invokes `compaction` with `{ context, result }` after a compaction strategy returns and before the runtime appends its standard compaction entry, and invokes `retry` with `{ context, decision }` before scheduling a provider-turn retry. There is no `provider_response` hook; observing provider output belongs to the provider adapter or subscriber events.
51
+ `run()` returns the transformed value. If no middleware is registered for a hook, `run()` returns the original value. `assembleProviderInput()` calls Phase 5 hooks in this order when middleware is supplied: `input_assembly`, then `context`, then `prompt_build`. The `input_assembly` call is unconditional — it runs after whatever `InputBuilder` produced the messages, so host middleware at that hook cannot be skipped by a custom builder. The agent/session runtime runs `beforeProviderTurn` once per turn after the request is assembled and before any provider-round work, then applies configured provider request policies, then invokes `provider_request` once with the `ProviderRequest` before `AIProvider.generate()`, invokes `tool_call` and `tool_result` through `dispatchToolCall()` for complete provider tool calls, invokes `compaction` with `{ context, result }` after a compaction strategy returns and before the runtime appends its standard compaction entry, and invokes `retry` with `{ context, decision }` before scheduling a provider-turn retry. There is no `provider_response` hook; observing provider output belongs to the provider adapter or subscriber events.
51
52
 
52
53
  With default `errorPolicy: "event"`, middleware errors become `extension_error` events when `onError` is provided, and later middleware still runs with the current value. With `errorPolicy: "throw"`, `run()` rejects on the first middleware error.
53
54
 
@@ -92,11 +93,45 @@ export const extension: Extension = {
92
93
  };
93
94
  ```
94
95
 
96
+ ## No-model turns (`beforeProviderTurn`)
97
+
98
+ `beforeProviderTurn` lets the host answer a turn from data it already has — teaching empty states, canned flows, deterministic lookups — without any provider request. The payload is `BeforeProviderTurnPayload` (`sessionId`, `runId`, `turn`, `userText`) and middleware returns it unchanged or with `answer: DeterministicTurnAnswer` set:
99
+
100
+ ```ts
101
+ export interface DeterministicTurnAnswer {
102
+ readonly content: readonly ContentBlock[];
103
+ readonly provenance: { readonly middleware: string };
104
+ }
105
+ ```
106
+
107
+ ```ts
108
+ import { createAgent, createMiddlewareRegistry, type BeforeProviderTurnPayload } from "@arnilo/prism";
109
+
110
+ const DESK_ANSWERS = new Map([["what can you do?", "I answer from local records; ask about an order id."]]);
111
+ const middleware = createMiddlewareRegistry();
112
+ middleware.use<BeforeProviderTurnPayload>("beforeProviderTurn", (payload, next) => {
113
+ const text = DESK_ANSWERS.get(payload.userText);
114
+ return text ? { ...payload, answer: { content: [{ type: "text", text }], provenance: { middleware: "desk" } } } : next(payload);
115
+ });
116
+
117
+ const session = createAgent({ model, provider, middleware }).createSession();
118
+ await session.run("what can you do?"); // no provider call; assistant message recorded
119
+ ```
120
+
121
+ Contract:
122
+
123
+ - Returning the payload without `answer` (or returning `undefined`) sends the turn to the provider exactly as if the hook were absent.
124
+ - `answer.provenance.middleware` is mandatory and validated as a bounded id (1–64 chars: letters, digits, `.` `_` `:` `-`); a deterministic turn can never masquerade as model output.
125
+ - `answer.content` accepts assistant-visible content blocks (`text`, `image`, `audio`, `file`, `document`, `video`, `thinking`). Tool-call blocks are rejected — no provider ran to authorize a call — and an empty block array throws `DeterministicTurnError` (`ERR_PRISM_DETERMINISTIC_TURN`), failing the run closed instead of falling through to the provider.
126
+ - Content passes the same output guardrails as provider output and is charged against `maxResponseBytes`, but the turn records no usage: usage is absent, never zero, and the run timeline shows a `deterministic` step named after the answering middleware.
127
+ - Provenance persists: the assistant message carries `metadata.deterministic = { middleware }`, so a transcript loaded back from any session store still proves the turn had no model behind it. `summarizeTimeline()`/`summarizeSession()` report `turns: { model, deterministic }`, and `createDeterministicTurnScorer()` (from `@arnilo/prism-core/governance/evals`) grades a trajectory for no-model coverage — failing a turn that both answered deterministically and still issued a provider request.
128
+
95
129
  ## Extension and configuration notes
96
130
 
97
131
  - Middleware registration is explicit through `createMiddlewareRegistry()` or `ExtensionAPI.use()`.
98
132
  - `provider_request` middleware sees generic `ProviderRequest.options` after request policies have run; do not add secrets unless a redactor/policy secret list covers that boundary.
99
133
  - Middleware runs only when the host/runtime calls `run()` or passes the registry to a helper that documents a call site.
134
+ - `beforeProviderTurn` runs only for turns that reach the provider boundary; a turn already ended by a run limit, host turn policy, or durable suspension never reaches it, and host middleware is trusted code — it must not use the hook to bypass `RunLimits` or guardrails.
100
135
  - `compaction` middleware may adjust the compaction result summary/data, but runtime still owns session store append ordering and branch parent ids.
101
136
  - `retry` middleware may stop retrying or adjust delay, but runtime still owns retry event emission, abort-aware waiting, and provider-turn boundaries.
102
137
  - The registry does not discover packages, read manifests, load config, call providers, execute tools, read resources, or start sessions.
@@ -112,6 +147,7 @@ export const extension: Extension = {
112
147
 
113
148
  ## Related APIs
114
149
 
150
+ - [Middlewares vs restore hooks](durable-runs.md#restore-hooks-all-or-nothing): middleware transforms payloads at named boundaries; `restoreHooks` restore external state before a durable resume and are not middleware.
115
151
  - [Extension kernel and event bus](extensions.md): `ExtensionAPI.use()` and shared error policy.
116
152
  - [Contribution registries](contribution-registries.md): direct contribution registration separate from middleware.
117
153
  - [Agent/session runtime](agent-session-runtime.md): provider request policy/middleware timing, bounded tool loop call site for `tool_call`/`tool_result` hooks, and runtime call sites for `compaction` and `retry`.