@arnilo/prism 0.8.0 → 0.10.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (141) hide show
  1. package/CHANGELOG.md +62 -1
  2. package/README.md +13 -12
  3. package/dist/agent-approval.d.ts +17 -2
  4. package/dist/agent-approval.js +15 -6
  5. package/dist/agent-event-source.d.ts +9 -1
  6. package/dist/agent-event-source.js +10 -3
  7. package/dist/agent-loops.js +7 -4
  8. package/dist/agent-run-lifecycle.d.ts +15 -1
  9. package/dist/agent-run-lifecycle.js +82 -11
  10. package/dist/agent-run-state.d.ts +47 -6
  11. package/dist/agent-run-state.js +154 -6
  12. package/dist/agent-session/event-subscriber.d.ts +2 -0
  13. package/dist/agent-session/event-subscriber.js +3 -0
  14. package/dist/agent-session/helpers.js +14 -0
  15. package/dist/agent-session/session/assemble.js +281 -32
  16. package/dist/agent-session/session/persist.d.ts +11 -0
  17. package/dist/agent-session/session/persist.js +48 -16
  18. package/dist/agent-session/session/provider-round.d.ts +14 -4
  19. package/dist/agent-session/session/provider-round.js +226 -19
  20. package/dist/agent-session/session/tool-round.d.ts +2 -2
  21. package/dist/agent-session/session/tool-round.js +78 -6
  22. package/dist/agent-session/session/types.d.ts +44 -3
  23. package/dist/agent-session/session.d.ts +100 -5
  24. package/dist/agent-session/session.js +224 -13
  25. package/dist/attention-compiler.d.ts +51 -2
  26. package/dist/attention-compiler.js +282 -21
  27. package/dist/cache-helpers.d.ts +4 -2
  28. package/dist/cache-helpers.js +8 -6
  29. package/dist/checkpoint-restore.d.ts +45 -0
  30. package/dist/checkpoint-restore.js +54 -0
  31. package/dist/context-budget.d.ts +13 -1
  32. package/dist/context-budget.js +57 -4
  33. package/dist/contracts-core/agent.d.ts +52 -1
  34. package/dist/contracts-core/attention.d.ts +95 -0
  35. package/dist/contracts-core/content.d.ts +10 -0
  36. package/dist/contracts-core/extensions.d.ts +3 -0
  37. package/dist/contracts-core/guardrail-packs.d.ts +46 -0
  38. package/dist/contracts-core/guardrail-packs.js +2 -0
  39. package/dist/contracts-core/loop.d.ts +36 -0
  40. package/dist/contracts-core/provider.d.ts +30 -0
  41. package/dist/contracts-core/run-limits.d.ts +29 -1
  42. package/dist/contracts-core/session.d.ts +23 -5
  43. package/dist/contracts-core/session.js +21 -2
  44. package/dist/contracts-core/usage.d.ts +40 -0
  45. package/dist/contracts-core/usage.js +8 -0
  46. package/dist/contracts-core.d.ts +2 -0
  47. package/dist/contracts-core.js +2 -0
  48. package/dist/contracts-protocol.d.ts +81 -5
  49. package/dist/contracts-run-state.d.ts +91 -2
  50. package/dist/contributions.d.ts +2 -1
  51. package/dist/contributions.js +1 -0
  52. package/dist/extensions.d.ts +15 -1
  53. package/dist/extensions.js +68 -0
  54. package/dist/guardrail-packs/coding-standard.d.ts +3 -0
  55. package/dist/guardrail-packs/coding-standard.js +63 -0
  56. package/dist/guardrail-packs/destructive-commands.d.ts +3 -0
  57. package/dist/guardrail-packs/destructive-commands.js +46 -0
  58. package/dist/guardrail-packs/errors.d.ts +7 -0
  59. package/dist/guardrail-packs/errors.js +9 -0
  60. package/dist/guardrail-packs/index.d.ts +4 -0
  61. package/dist/guardrail-packs/index.js +15 -0
  62. package/dist/guardrail-packs/secrets-hygiene.d.ts +3 -0
  63. package/dist/guardrail-packs/secrets-hygiene.js +23 -0
  64. package/dist/guardrail-packs/types.d.ts +26 -0
  65. package/dist/guardrail-packs/types.js +2 -0
  66. package/dist/guardrail-packs/validation-respect.d.ts +3 -0
  67. package/dist/guardrail-packs/validation-respect.js +69 -0
  68. package/dist/guardrails.d.ts +61 -1
  69. package/dist/guardrails.js +377 -0
  70. package/dist/index.d.ts +16 -11
  71. package/dist/index.js +10 -7
  72. package/dist/input.d.ts +8 -1
  73. package/dist/input.js +68 -6
  74. package/dist/middleware.d.ts +37 -2
  75. package/dist/middleware.js +41 -0
  76. package/dist/node/session-store-jsonl.js +18 -3
  77. package/dist/observability.js +6 -0
  78. package/dist/provider-events.d.ts +8 -2
  79. package/dist/provider-events.js +60 -2
  80. package/dist/providers/openai-compatible.js +6 -3
  81. package/dist/run-bundle.d.ts +6 -1
  82. package/dist/run-bundle.js +5 -1
  83. package/dist/run-limits.d.ts +11 -1
  84. package/dist/run-limits.js +59 -0
  85. package/dist/session-stores.d.ts +12 -1
  86. package/dist/session-stores.js +21 -4
  87. package/dist/testing/agent-event-source-conformance.js +41 -2
  88. package/dist/testing/prefix-stability-conformance.d.ts +59 -0
  89. package/dist/testing/prefix-stability-conformance.js +172 -0
  90. package/dist/testing/session-store-conformance.d.ts +3 -2
  91. package/dist/testing/session-store-conformance.js +48 -0
  92. package/dist/tools.d.ts +5 -0
  93. package/dist/tools.js +21 -6
  94. package/dist/usage-estimation.d.ts +29 -0
  95. package/dist/usage-estimation.js +79 -0
  96. package/docs/agent-events.md +75 -4
  97. package/docs/agent-session-runtime.md +10 -6
  98. package/docs/attention-compiler.md +89 -8
  99. package/docs/caveman.md +1 -1
  100. package/docs/coding-agent-tools.md +1 -1
  101. package/docs/compaction-and-retry.md +1 -1
  102. package/docs/compaction-llm.md +2 -0
  103. package/docs/compaction-observational-memory.md +54 -7
  104. package/docs/durable-runs.md +46 -3
  105. package/docs/embeddings.md +9 -0
  106. package/docs/evaluations.md +5 -0
  107. package/docs/execution-timeline.md +79 -1
  108. package/docs/extensions.md +20 -3
  109. package/docs/guardrails.md +50 -4
  110. package/docs/hooks.md +282 -0
  111. package/docs/index.md +37 -15
  112. package/docs/input-and-prompt-assembly.md +4 -4
  113. package/docs/instruction-injection.md +1 -0
  114. package/docs/knowledge-sync.md +4 -0
  115. package/docs/live-testing.md +3 -1
  116. package/docs/memory-fabric.md +28 -0
  117. package/docs/middleware-hooks.md +90 -4
  118. package/docs/migrate-to-0.9.md +210 -0
  119. package/docs/migration.md +26 -0
  120. package/docs/multi-agent-patterns.md +25 -2
  121. package/docs/node-jsonl-session-store.md +7 -1
  122. package/docs/observability.md +7 -3
  123. package/docs/options-index.md +4 -1
  124. package/docs/policy-and-audit.md +26 -1
  125. package/docs/prefix-stability-conformance.md +143 -0
  126. package/docs/provider-caching.md +4 -4
  127. package/docs/provider-conformance.md +16 -0
  128. package/docs/provider-packages.md +20 -20
  129. package/docs/public-contracts.md +3 -2
  130. package/docs/rag.md +188 -3
  131. package/docs/release-and-install.md +45 -40
  132. package/docs/runs-and-usage.md +56 -10
  133. package/docs/scoped-agent-memory.md +270 -0
  134. package/docs/scoped-memory.md +138 -0
  135. package/docs/session-store-conformance.md +1 -2
  136. package/docs/session-stores.md +17 -17
  137. package/docs/supervisors.md +32 -12
  138. package/docs/tools.md +18 -1
  139. package/docs/wiki.md +4 -2
  140. package/docs/workflows.md +5 -0
  141. package/package.json +8 -2
package/docs/hooks.md ADDED
@@ -0,0 +1,282 @@
1
+ # Hooks
2
+
3
+ ## What it does
4
+
5
+ Hooks are the seams where host- or package-owned code reacts to a run. Prism splits them by what the
6
+ code is allowed to do, so a hook never gains authority it was not granted:
7
+
8
+ | Family | Seam | May decide | Owning page |
9
+ | --- | --- | --- | --- |
10
+ | Middleware hooks | Transform a payload at a named boundary: `session_start`, `session_shutdown`, `beforeProviderTurn`, `provider_request`, `input_assembly`, `prompt_build`, `context`, `tool_call`, `tool_result`, `retry`, `compaction_request`, `compaction` | Returns the payload; cannot end the run | [Middleware hooks](middleware-hooks.md) |
11
+ | Guardrails | `input`, `output`, `tool_input`, `tool_output` stages | `allow` / `block` / `tripwire` / `interrupt` — the only seams that can reject | [Guardrails](guardrails.md) |
12
+ | Instruction injectors | Prompt and context injection on the first turn, every turn, or on matching input | Returns `instructions` and `contextBlocks`; cannot mutate the provider request | [Instruction injection](instruction-injection.md) |
13
+ | Stop hooks | A natural loop end, after the model produced an answer | `continue` (bounded) or `stop` | This page |
14
+ | Extension bus | Every emitted `AgentEvent`, optionally bridged read-only | Observes only | [Extensions](extensions.md), [Agent events](agent-events.md) |
15
+
16
+ This page owns stop hooks and the map from Claude Code / Codex hook events onto Prism seams.
17
+
18
+ APIs:
19
+
20
+ - `AgentConfig.stopHooks` / `RunOptions.stopHooks`
21
+ - `StopHook`, `StopHookContext`, `StopHookDecision`
22
+ - `RunLimits.maxStopContinuations`
23
+ - `ExtensionAPI.registerStopHook()` and `activateKernel().stopHooks`
24
+ - `session_start` / `session_shutdown` middleware and `session.close()`
25
+ - `compaction_request` / `compaction` middleware
26
+ - `forwardAgentEvents()` for hosts that already listen on the extension bus
27
+ - `@arnilo/prism-hooks`: `parseHooksConfig()` / `createHooksExtension()` for a declarative `hooks.json`
28
+
29
+ ## When to use it
30
+
31
+ Use stop hooks for end-of-run policy: nudge the model to address deferred items, verify a checklist
32
+ before finishing, or ask for a final answer shape. Stop hooks run *after* the loop finished naturally,
33
+ when the model produced its answer.
34
+
35
+ Do not use them as middleware (payload transformation), as guardrails (policy denials), or as turn
36
+ control. `RunOptions.turnPolicy.stop` and the loop ceilings end a run *before* the next provider
37
+ request — see [Agent loops](agent-loops.md).
38
+
39
+ ## Inputs / request
40
+
41
+ ```ts
42
+ interface StopHookContext {
43
+ readonly sessionId: string;
44
+ readonly runId: string;
45
+ /** Provider turns assembled in this run (resumption continues the run's counter). */
46
+ readonly turn: number;
47
+ /** Read-only live transcript at loop end. */
48
+ readonly history: readonly Message[];
49
+ readonly metadata: Readonly<Record<string, unknown>>;
50
+ readonly signal: AbortSignal;
51
+ /** True on every invocation after the first continuation in this run. */
52
+ readonly stopHookActive: boolean;
53
+ }
54
+
55
+ type StopHookDecision =
56
+ | { readonly action: "stop" }
57
+ | { readonly action: "continue"; readonly reason: string; readonly steer?: string | Message };
58
+
59
+ interface StopHook {
60
+ readonly name: string;
61
+ decide(context: StopHookContext): StopHookDecision | Promise<StopHookDecision>;
62
+ }
63
+ ```
64
+
65
+ Configuration: `AgentConfig.stopHooks` is the agent default; `RunOptions.stopHooks` appends to it for
66
+ one run (agent hooks first). Hooks run serially in order, the first `continue` wins, and every hook
67
+ is consulted before the run settles. `RunLimits.maxStopContinuations` caps continuations — default
68
+ 3, `0` observes hooks but never continues, `null` disables the cap — and a run overlay may only
69
+ narrow the agent value (resolution takes the minimum across layers).
70
+
71
+ ## Outputs / response / events
72
+
73
+ - `continue` queues `reason` (and `steer` after it) as steered input. The next turn sees them in
74
+ history; input guardrails re-check them exactly like `session.steer()`, so a `block`/`tripwire`
75
+ decision drops the message (emitting `steer_rejected`) while the continuation still runs, and an
76
+ `interrupt` decision fails the run.
77
+ - The continuation turn is a normal provider turn: it charges `maxTurns`/token limits, emits
78
+ `turn_started`/`turn_finished`, and its output joins the transcript.
79
+ - Exceeding the cap ends the run cleanly with `AgentRunResult.stopReason: "hook_limit"`, riding
80
+ `agent_finished.finishReason` and the finish `RunRecord`; no `error` is set. With
81
+ `runState: { checkpointPolicy: "every-turn" }` the checkpoint keeps its frontier and
82
+ `resumeAgentRun(..., { decision: "continue" })` resumes it.
83
+ - A throwing or malformed hook fails the run closed with `ERR_PRISM_STOP_HOOK`. Hooks never run
84
+ after a loop ceiling (`turn_limit`/`token_limit`/`refusal`), a `turnPolicy` stop (`host_policy`),
85
+ or a `generate-validate-revise` artifact failure.
86
+
87
+ ## Request/response example
88
+
89
+ ```text
90
+ turn 1: provider answers "draft"
91
+ stop hook: { action: "continue", reason: "address the deferred items" }
92
+ turn 2: provider sees "address the deferred items" in history and answers again
93
+ stop hook: ctx.stopHookActive === true → { action: "stop" }
94
+ result: natural end (no stopReason)
95
+ ```
96
+
97
+ ## Implementation example
98
+
99
+ ```ts
100
+ import { createAgent } from "@arnilo/prism";
101
+
102
+ const agent = createAgent({
103
+ model: { provider: "mock", model: "demo" },
104
+ provider,
105
+ stopHooks: [
106
+ {
107
+ name: "checklist",
108
+ decide: (ctx) =>
109
+ ctx.stopHookActive
110
+ ? { action: "stop" }
111
+ : { action: "continue", reason: "Verify the checklist before finishing." },
112
+ },
113
+ ],
114
+ limits: { maxStopContinuations: 3 },
115
+ });
116
+
117
+ const result = await agent.createSession().run("Write the report");
118
+ ```
119
+
120
+ ## Session lifecycle and boundary hooks
121
+
122
+ The session and compaction boundaries are middleware, dispatched once each by the agent/session
123
+ runtime (never per turn):
124
+
125
+ | Hook | Fires | Payload |
126
+ | --- | --- | --- |
127
+ | `session_start` | First run start of a session — after `agent_started`/`agent_resumed`, before the first provider turn. A session rebuilt from a durable checkpoint is a new runtime session, so it opens again. | `{ sessionId, runId }` |
128
+ | `session_shutdown` | `await session.close()`, before every subscriber is closed. Idempotent. | `{ sessionId }` |
129
+ | `compaction_request` | Before a compaction strategy runs — `session.compact()` and auto-compaction both route through it, ordinary turns do not. The payload is the strategy's input, and the return value is what it compacts. | `CompactionContext` (`sessionId`, `entries`, `keepRecentEntries`, `trigger`, `secrets`, `metadata`, `signal`) |
130
+ | `compaction` | After the strategy returns, with the final compacted result. | `{ context, result }` |
131
+
132
+ They are middleware, not stop hooks: they observe a boundary and must return the payload. A throw
133
+ follows the registry's `errorPolicy` — an `extension_error` event with the run continuing (default),
134
+ or a rejection from `run()`/`close()` with `errorPolicy: "throw"`. Hosts register them through
135
+ `api.use("session_start", ...)`, `api.use("session_shutdown", ...)`, `api.use("compaction_request", ...)`,
136
+ or `api.use("compaction", ...)` and pass the registry as `AgentConfig.middleware`; without it none of
137
+ them fire. `compaction_request` cannot skip compaction (an empty entry set is the strategy's own
138
+ error) and the post-strategy `compaction` hook still sees the result — see
139
+ [Middleware hooks](middleware-hooks.md). Use [Agent events](agent-events.md) or
140
+ `forwardAgentEvents(source, kernel.events)` for finer-grained per-turn observation — the bridge maps
141
+ `agent_started` → `before_agent_start`, `turn_started`/`turn_finished` → `turn`, and
142
+ `tool_execution_started`/`tool_execution_finished` → `tool_call`/`tool_result` as read-only
143
+ notifications.
144
+
145
+ ## Claude Code and Codex event map
146
+
147
+ Every Claude Code / Codex hook event maps onto one of the seams above — or is a documented non-goal:
148
+
149
+ | Claude Code / Codex event | Prism surface | `hooks.json` adapter | Notes |
150
+ | --- | --- | --- | --- |
151
+ | `SessionStart` | `session_start` middleware (+ instruction-injector queue) | compiled | `additionalContext` lands in the next assembly. Claude/Codex `source` values `startup` and `resume` both select — the seam cannot tell a fresh session from a resumed one — while `clear` and `compact` do not: compaction never reopens the session here (use the `compaction_request`/`compaction` seams). |
152
+ | `SessionEnd` | `session_shutdown` middleware | not compiled | Teardown cannot inject context or block a run, so cleanup belongs in the middleware or `LoadedExtension.dispose()`. A `hooks.json` file that needs it uses `api.use("session_shutdown", ...)` alongside the adapter. |
153
+ | `UserPromptSubmit` | `input` guardrail (deny) + injector queue | compiled | Exit `2`, `decision: "block"`, or `continue: false` rejects the run with `GuardrailError`; `additionalContext` is injected. |
154
+ | `PreToolUse` | `tool_call` middleware (`updatedInput`, `additionalContext`) + `tool_input` guardrail (deny) | compiled | `permissionDecision: "deny"` and exit `2` return a blocked `ToolResult`; `updatedInput` rewrites the arguments before dispatch; `additionalContext` is queued for the next assembly and dropped when the call is denied. Approvals are host policy — see `PermissionRequest`. |
155
+ | `PermissionRequest` | non-goal — host permission policy | not compiled | A hook that can grant a permission is a privilege-escalation vector: config-authored or third-party code would gain approval authority over the host's tools. Prism keeps grants in host-owned policy — `ExecutionPolicy`, `requiresApproval`, `interruptBeforeTool` — and guardrail packs' [`ask` rules](guardrails.md#asking-for-approval-ask-rules), which suspend a durable run for an explicit host decision. Hook seams (and `@arnilo/prism-hooks`) only ever deny. |
156
+ | `PostToolUse` | `tool_result` middleware (`additionalContext`, rewrite) + `tool_output` guardrail (deny) | compiled | `decision: "block"` and exit `2` replace the result with a refusal-shaped `ToolResult`; `additionalContext` is queued for the next assembly. |
157
+ | `Stop` | stop hooks (`AgentConfig.stopHooks` / `RunOptions.stopHooks`) | compiled | Both harnesses continue with `decision: "block"` + `reason`, or with `additionalContext` (exit `2` also continues, using stderr as the reason): that becomes `{ action: "continue" }`, with the reason as the continuation prompt and `ctx.stopHookActive` true on re-entry. The common-field `continue: false` is the *stop* signal and wins over continuation decisions in the same event. `RunLimits.maxStopContinuations` (default 3) caps how often the loop re-enters before a clean `hook_limit` stop. |
158
+ | `PreCompact` | `compaction_request` middleware | not compiled | Claude/Codex `PreCompact` is decision-only (block the compaction, no replaceable input). Prism's seam adds input rewrite: the returned `CompactionContext` is exactly what the strategy compacts. |
159
+ | `PostCompact` | `compaction` middleware + `compaction_finished` event | not compiled | Observes or rewrites the compacted result after the strategy returned. |
160
+ | `Interrupt` (Codex) | non-goal — hooks run at boundaries | not compiled | Hooks run before and after a turn, not inside one, so they cannot interrupt an in-flight turn. Interruption is the host's abort path: the run's `AbortSignal`, `session.close()`, or a guardrail `interrupt` at the input stage of a durable run (which suspends for approval). |
161
+ | `SubagentStart` / `SubagentStop` | non-goal — no subagent primitive | not compiled | Prism has no implicit child-agent lifecycle: delegation is a host-authored tool or a separate agent/run, so the parent loop is unaffected. Observe child runs with `agent_started`/`agent_finished` and `forwardAgentEvents()`. |
162
+
163
+ Claude Code's remaining harness-specific events (`Setup`, `Notification`, `StopFailure`,
164
+ `PostToolUseFailure`, `PostToolBatch`, `TeammateIdle`, `TaskCreated`, `TaskCompleted`,
165
+ `Elicitation`, `ElicitationResult`, `PreToolBatch`) have no Prism surface: they belong to a
166
+ terminal/CI harness (init, notifications, multi-teammate gating, MCP elicitation) rather than to the
167
+ agent loop. `StopFailure` and `PostToolUseFailure` are covered as outcomes — `agent_finished.error`
168
+ and a failed `ToolResult` — and the rest are observed through `AgentEvent`s.
169
+
170
+ ## `hooks.json` adapter (`@arnilo/prism-hooks`)
171
+
172
+ Hosts that already keep a Claude Code or Codex hooks file do not have to rewrite it as SDK
173
+ callbacks. The opt-in capability package `@arnilo/prism-hooks` parses the file and compiles each
174
+ event onto the seams above — no Prism internals, `@arnilo/prism` as its only peer, and no new
175
+ runtime dependency. It compiles the five events marked *compiled* in the map.
176
+
177
+ ```bash
178
+ npm install @arnilo/prism @arnilo/prism-hooks
179
+ ```
180
+
181
+ ```ts
182
+ import { activateKernel, createAgent, createExtensionKernel } from "@arnilo/prism";
183
+ import { createHooksExtension, hookCommandHash, parseHooksConfig } from "@arnilo/prism-hooks";
184
+
185
+ const config = parseHooksConfig(await readFile("hooks.json", "utf8"));
186
+ const audit = { type: "command", command: "node audit-tool.js" } as const;
187
+ const hooks = createHooksExtension(config, { trusted: { [audit.command]: hookCommandHash(audit) } });
188
+
189
+ const kernel = createExtensionKernel();
190
+ await kernel.load([hooks]);
191
+ const activated = activateKernel(kernel);
192
+ const agent = createAgent({
193
+ model,
194
+ provider,
195
+ guardrails: hooks.guardrails, // required for UserPromptSubmit / PreToolUse / PostToolUse
196
+ instructionInjectors: activated.instructionInjectors,
197
+ stopHooks: activated.stopHooks,
198
+ middleware: activated.middleware,
199
+ });
200
+ ```
201
+
202
+ Schema and semantics:
203
+
204
+ - Both authoring shapes parse: the flat Claude map (`{ "PreToolUse": [{ "matcher": "Bash", "hooks": [...] }] }`) and Codex's nested `{ "hooks": { "PreToolUse": [...] } }` with bare handler objects. An unknown event name, unknown handler `type`, empty command, non-positive `timeout`, or `"shell": true` throws `ERR_PRISM_HOOKS_CONFIG` — never a silent no-op.
205
+ - Handlers are `{ type: "command", command, args?, timeout?, async?, asyncRewake?, statusMessage? }` or `{ type: "mcp_tool", server, tool, input? }`. HTTP hooks are not implemented.
206
+ - Matchers follow Claude's hybrid rule: letters/digits/`_`/`-`/space/`,`/`|` select literally (with `|`/`,` alternatives), anything else is an unanchored regex, and no `$1` capture substitution exists. `UserPromptSubmit` and `Stop` have no matcher subject, so a matcher there warns and is ignored; `SessionStart` cannot distinguish a fresh session from a checkpoint resume at that seam, so `startup` and `resume` both select.
207
+ - Exit codes are Claude/Codex's: `0` success (JSON stdout parsed), `2` block with stderr as the reason, any other code a non-blocking error reported as a `hooks:warning` extension event. There is no exit code `64`.
208
+ - The common-field `continue: false` is read as the stop signal: on `Stop` it returns `{ action: "stop" }` and wins over continuation decisions from the same event (Codex precedence); on a tool or prompt event it reads as a refusal, the conservative reading, since Claude halts processing there and Codex marks the field unsupported.
209
+ - `timeout` is in **seconds** (default 600). Expiry warns and never blocks: pre-events allow with a warning, additive events simply drop their context.
210
+ - `additionalContextLimit` is a token threshold measured at ~4 code units per token — Codex's unit (default 2500, `0` = unlimited). It is read per handler, and a handler without the field falls back to the config-level value beside `hooks`. Claude's fixed 10,000-character `persistHookOutput` is the same spill behavior at a different unit. Over the limit that handler's text is written to `<temp_dir>/hook_outputs/` and a pointer line is injected instead — the only filesystem write this package performs.
211
+ - `async: true` handlers run detached (`asyncRewake` is accepted for config parity). Their context lands at the next assembly, never on the event path.
212
+ - Trust is a hash allowlist: `options.trusted` maps a command string to a digest from `hookCommandHash()` (default SHA-256 over the tokenized argv, so appending an argument invalidates the entry). A missing or mismatched entry skips the handler with a `hooks:warning`. `trusted: "all"` is the documented escape hatch and logs loudly. `mcp_tool` handlers use the host-supplied client and are trusted by construction.
213
+ - Commands are tokenized locally (quotes and escapes only — no expansion, pipes, or substitutions) and spawned with an argv array; there is no shell. Guardrails and stop hooks stay inert until the host activates them, like every other kernel contribution.
214
+
215
+ ## Migrating from Claude Code or Codex configs
216
+
217
+ 1. Keep the file where it is and parse it: `parseHooksConfig(text)` accepts both the flat
218
+ `.claude/settings.json` map and Codex's `{ "hooks": { ... } }` shape, including Claude's extra
219
+ settings keys next to `hooks`.
220
+ 2. Drop the events the adapter does not compile — an unknown event name is a loud error, not a
221
+ silent skip, so filter them explicitly instead of stripping the file by hand:
222
+
223
+ ```ts
224
+ // SessionEnd, PermissionRequest, Interrupt, PreCompact/PostCompact, SubagentStart/Stop are not compiled.
225
+ const { SessionEnd, PermissionRequest, Interrupt, PreCompact, PostCompact, SubagentStart, SubagentStop, ...compiled } = raw;
226
+ const config = parseHooksConfig(compiled);
227
+ ```
228
+
229
+ For the non-goals, use the Prism surface named in the map (host permission policy, the
230
+ `session_shutdown` / `compaction_request` / `compaction` middleware) rather than a handler file.
231
+ 3. Decide trust before the first run: `trusted: { [command]: hookCommandHash(handler) }` for each
232
+ command you have reviewed, or `"all"` only for a config you own end to end. Handlers run
233
+ out-of-process but with your privileges, so the allowlist is the boundary.
234
+ 4. Activate the two halves together: `guardrails: hooks.guardrails` on the agent, plus
235
+ `middleware` / `instructionInjectors` / `stopHooks` from `activateKernel()` — activating one
236
+ without the other logs a `hooks:warning` and the blocking decisions do not apply.
237
+ 5. Re-check the differences: `timeout` is seconds (default 600), only exit code `2` blocks,
238
+ `decision: "block"` is what continues a `Stop` (never `continue: false`, which stops), no `$1`
239
+ capture substitution in matchers, `additionalContext` over `additionalContextLimit` spills to a
240
+ file pointer, and matchers on `UserPromptSubmit`/`Stop` are ignored with a warning.
241
+
242
+ ## Extension and configuration notes
243
+
244
+ - `api.registerStopHook({ name, decide })` contributes an inert hook to `kernel.registries.stopHooks`;
245
+ pass `activateKernel(kernel).stopHooks` into `createAgent({ stopHooks })`. Disposing the loaded
246
+ extension unwinds the registration.
247
+ - Registrations are keyed by `name` (last write wins, like other contribution registries).
248
+ - `RunOptions.stopHooks` cannot replace agent hooks; it appends. `maxStopContinuations` narrows only.
249
+ - No dedicated `AgentEvent` is added: observe decisions through your own callback and the run outcome.
250
+
251
+ ## Security and performance notes
252
+
253
+ - In-process seams — middleware, guardrails, injectors, and stop hooks — are host code with the
254
+ host's full privileges. Registering one is trusting it; Prism validates payload shapes, bounds
255
+ what reaches the provider, and redacts what reaches events, but it cannot sandbox a callback.
256
+ - Hooks never receive credentials and cannot mutate history: stop hooks get metadata plus the
257
+ read-only transcript, and continuation text enters through the steer queue, so the 8-message /
258
+ 64 KiB caps bound it and the host redactor applies before it reaches the provider.
259
+ - No hook seam grants authority, by design. Permission and approval decisions stay with host policy
260
+ (`ExecutionPolicy`, `requiresApproval`, `interruptBeforeTool`, guardrail-pack `ask` rules), so a
261
+ hook can only deny or add context — the `PermissionRequest` non-goal above.
262
+ - Out-of-process hooks (`@arnilo/prism-hooks`) are the opposite trade: they run in a separate
263
+ process, but with your user's privileges and an inherited environment, so the hash allowlist plus
264
+ the per-handler `timeout` are the trust boundary. `mcp_tool` handlers add the MCP client's own
265
+ transport and auth.
266
+ - Hooks run serially, awaited, once per natural loop end. Each continuation costs full provider
267
+ turns until the model stops naturally; size `maxStopContinuations` accordingly (`0` = observe
268
+ only). With no stop hooks configured, the wrapper is skipped entirely. `session_start` /
269
+ `session_shutdown` fire once per session, `compaction_request` / `compaction` once per compaction,
270
+ and tool seams once per tool call in their stage.
271
+
272
+ ## Related APIs
273
+
274
+ - [Agent loops](agent-loops.md): loop strategies, turn policy, and `finishReason` ceilings.
275
+ - [Guardrails](guardrails.md): input/output/tool decisions, steer re-checking, and the `ask` rules that own approvals.
276
+ - [Middleware hooks](middleware-hooks.md): payload transforms at named boundaries, including the session and compaction seams.
277
+ - [Instruction injection](instruction-injection.md): where `additionalContext` from session and prompt hooks lands.
278
+ - [Extensions](extensions.md): `ExtensionAPI.registerStopHook()`, the contribution registries, and the `forwardAgentEvents()` bridge.
279
+ - [Runs and usage ledger](runs-and-usage.md): `maxStopContinuations` and clean stop reasons.
280
+ - [Agent/session runtime](agent-session-runtime.md): `session.close()`, steer queue semantics, and durable resume.
281
+ - [Agent events](agent-events.md): the events the extension bus observes, including `hook_limit`.
282
+ - [Public contracts](public-contracts.md): `RunOptions`, `RunLimits`, and `AgentConfig` surfaces.
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.10.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
+ - **12 publishable packages** at current **0.10.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
 
@@ -60,6 +76,7 @@ Prism is a TypeScript/Node.js agent harness. Hosts own providers, tools, credent
60
76
  - [Durable runs](durable-runs.md): turn-boundary `checkpointPolicy: "every-turn"` checkpoints and `decision: "continue"` crash recovery for long runs.
61
77
  - [Agent definitions](agent-definitions.md): declarative `AgentDefinition` resolution and `AGENT.md` bundle discovery, fail-closed activation.
62
78
  - [Agent loops](agent-loops.md): replaceable loops with `limits.maxToolRounds` budgets and durable revision/restore hooks.
79
+ - [Hooks](hooks.md): the hook model — stop hooks with bounded continuation, session/compaction boundary seams, and the Claude Code / Codex event map plus the `hooks.json` adapter.
63
80
  - [Guardrails](guardrails.md): typed fail-closed input/output/tool checks with redacted decision records.
64
81
  - [Agent events](agent-events.md): `turn_started`/`tool_call_delta` stream plus durable page/resume sources for reconnect.
65
82
  - [Observability](observability.md): OTel GenAI span hierarchy, workflow spans, cockpit aggregations, RAG span tree, bounded trace linkage, exporter isolation.
@@ -79,6 +96,8 @@ Prism is a TypeScript/Node.js agent harness. Hosts own providers, tools, credent
79
96
  - [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
97
  - [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
98
  - [Memory fabric](memory-fabric.md): opt-in typed notes (fact/procedure/file/working/episode) with validity windows over the existing vector and working stores.
99
+ - [Scoped memory](scoped-memory.md): workspace-scope guard, gated writes, promotion ladder, decay reads, audit mirror (`@arnilo/prism-memory/scoped`).
100
+ - [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
101
  - [Session stores](session-stores.md): `SessionStore` contract, append options, branches, bounded search — start here for persistence.
83
102
  - [Conversations](conversations.md): durable user-scoped threads with versioned metadata and legal-hold-aware deletion.
84
103
  - [Work artifacts and review](work-artifacts-and-review.md): artifact attach, revision compare, evidence-bound citations, approve/reject, expiring delivery links.
@@ -213,9 +232,10 @@ Prism is a TypeScript/Node.js agent harness. Hosts own providers, tools, credent
213
232
  - [Session store conformance](session-store-conformance.md): assert append/idempotency/conflict/branch invariants for any store.
214
233
  - [Run ledger conformance](run-ledger-conformance.md): assert durable run/usage writes and reopen survival.
215
234
  - [Compaction conformance](compaction-conformance.md): assert redacted non-empty summaries and abort observation.
235
+ - [Prefix stability conformance](prefix-stability-conformance.md): assert progressive disclosure keeps the provider cache prefix stable.
216
236
  - [Tool conformance](tool-conformance.md): assert blocked-reason matrix and success-path dispatch behavior.
217
237
  - [Extension conformance](extension-conformance.md): assert inert contributions and redacted setup errors.
218
- - `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.
238
+ - `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), [`guardrail-packs.ts`](../examples/guardrail-packs.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.
219
239
 
220
240
  ## Third-party integrations
221
241
 
@@ -231,6 +251,7 @@ Prism is a TypeScript/Node.js agent harness. Hosts own providers, tools, credent
231
251
  ## Release and install
232
252
 
233
253
  - [Release and install](release-and-install.md): install rules, package graph, and deterministic resumable publication.
254
+ - [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
255
  - [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
256
  - [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
257
  - [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.
@@ -243,19 +264,20 @@ Prism is a TypeScript/Node.js agent harness. Hosts own providers, tools, credent
243
264
  The generated inventory below derives from [`scripts/package-truth.json`](../scripts/package-truth.json) — regenerate with `node scripts/package-truth.mjs --emit-docs`, never hand-edit.
244
265
 
245
266
  <!-- generated:package-truth:inventory begin -->
246
- **11 publishable manifests** — root `@arnilo/prism` plus 10 workspace packages (4 `prism-*` family packages, 6 capability packages). Generated by `node scripts/package-truth.mjs --emit-docs` — do not hand-edit.
267
+ **12 publishable manifests** — root `@arnilo/prism` plus 11 workspace packages (4 `prism-*` family packages, 7 capability packages). Generated by `node scripts/package-truth.mjs --emit-docs` — do not hand-edit.
247
268
 
248
269
  | package | version | notes |
249
270
  | --- | --- | --- |
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 |
271
+ | `@arnilo/prism` | 0.10.0 | core — runtime, CLI/RPC, templates, docs |
272
+ | `@arnilo/prism-channels` | 0.10.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 |
273
+ | `@arnilo/prism-coding-tools` | 0.10.0 | family — /agent, /security, /openapi, /computer-use-linux, /dev, /caveman, /ponytail, /impeccable subpaths |
274
+ | `@arnilo/prism-core` | 0.10.0 | family — /runtime, /sessions, /governance, /credentials, /enterprise, /validation subpaths |
275
+ | `@arnilo/prism-providers` | 0.10.0 | family — all provider adapters as `/<adapter>` subpaths |
276
+ | `@arnilo/prism-acp-agent` | 0.10.0 | capability — ACP adapter |
277
+ | `@arnilo/prism-ag-ui` | 0.10.0 | capability — AG-UI/A2A/A2UI adapter |
278
+ | `@arnilo/prism-hooks` | 0.10.0 | capability — Claude/Codex-compatible hooks.json adapter compiled onto middleware, guardrail, injector, and stop-hook seams |
279
+ | `@arnilo/prism-mcp` | 0.10.0 | capability — MCP client/server/OAuth interop |
280
+ | `@arnilo/prism-memory` | 0.10.0 | capability — memory plus /rag, /compaction/*, /fabric, /graft, /wiki subpaths |
281
+ | `@arnilo/prism-web-tools` | 0.10.0 | capability — Brave/Exa/Firecrawl plus peer-gated /browser and /obscura subpaths |
282
+ | `@arnilo/prism-work` | 0.10.0 | capability — /connectors, /documents, /sheets, /diagrams, /document-reader, /sandbox, /skills, /tools subpaths |
261
283
  <!-- 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,17 +81,17 @@ 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`.
91
91
  - Tool results are tool messages containing `tool_result` content; the agent/session runtime uses this to feed dispatched tool results into the next provider turn, placing the assistant `tool_call` and the matching role `tool` `tool_result` before any final assistant content. Cache-aware layout keeps tool results before the current user suffix so it does not split tool transcripts. A result with no `value`, no `type:text` content, and no error carries the constant `EMPTY_TOOL_RESULT_TEXT` (`"(tool completed with no output)"`) as its `result`, so no provider route serializes an empty or absent tool payload.
92
92
  - Middleware runs only when `middleware` is supplied in the context.
93
93
  - `assembleProviderInput()` returns a `ProviderRequest` with the caller's model/tools/provider options/metadata/signal and composed messages/context. It stamps missing `sessionId`/`cacheKey` via `applyDefaultProviderRequestOptions` when `sessionId` is passed (agent sessions always pass `session.id`). It also calls `assertMessagesSupportModelCapabilities()` so unsupported `audio`/`file`/`document`/`image` blocks fail with `UnsupportedModalityError` when the model declares `capabilities.input`.
94
- - Optional `contextBudget` (at least one of `maxInputTokens` / `maxInputBytes`) runs after default message groups are built and before final flatten. `tokenEstimator` replaces the built-in ÷4 heuristic for **eviction accounting only** — it never reaches billing, provider usage, or the wire, byte caps (`maxInputBytes`) stay estimator-independent and are always enforced, and an estimator that returns a non-finite or negative count (or is not a function) fails the assembly closed with `TypeError` instead of making eviction decisions unsound. Eviction drops droppable sections first (toolResults → history → summaries → context → skills → attachments; layout-aware). Within `history`, oldest messages drop first. Protected instructions + current user `input` (+ tools catalog) fail closed with `ContextBudgetError` if they alone exceed the budget. When `reportOmissions: true`, attach `ProviderRequest.metadata[CONTEXT_BUDGET_REPORT_METADATA_KEY]` and read via `getContextBudgetReport(request)` (kinds/ids/sizes only — no secrets). Raw session store entries are never deleted.
94
+ - Optional `contextBudget` (at least one of `maxInputTokens` / `maxInputBytes`) runs after default message groups are built and before final flatten. Sessions forward `AgentConfig.contextBudget` here, so an agent-level budget gets the same semantics. `tokenEstimator` replaces the built-in ÷4 heuristic for **eviction accounting** — and, with `usageEstimation: "fallback"`, also feeds the missing-usage estimate ([Runs and usage](runs-and-usage.md#automatic-fallback-agentconfigusageestimation)); it never reaches billing, provider usage reports, or the wire, byte caps (`maxInputBytes`) stay estimator-independent and are always enforced, and an estimator that returns a non-finite or negative count (or is not a function) fails the assembly closed with `TypeError` instead of making eviction decisions unsound. Eviction drops droppable sections first (toolResults → history → summaries → context → skills → attachments; layout-aware). Within `history`, oldest messages drop first. Protected instructions + current user `input` (+ tools catalog) fail closed with `ContextBudgetError` if they alone exceed the budget. When `reportOmissions: true`, attach `ProviderRequest.metadata[CONTEXT_BUDGET_REPORT_METADATA_KEY]` and read via `getContextBudgetReport(request)` (kinds/ids/sizes only — no secrets). Raw session store entries are never deleted.
95
95
  - `renderPromptTemplate()` replaces top-level `{{name}}` variables with caller-supplied JSON-compatible values. Strings are inserted directly; numbers, booleans, `null`, arrays, and objects are stringified deterministically with sorted object keys. Missing variables throw by default or stay unchanged with `{ missing: "preserve" }`.
96
96
 
97
97
  ## Request/response example
@@ -181,3 +181,4 @@ RPC: `prompt`/`followUp` params accept an optional `instructionInjectors: readon
181
181
  - [Extensions](extensions.md): `registerInstructionInjector` in the contribution-kinds list.
182
182
  - [CLI and RPC](cli-rpc.md): `--instruction`/`--injector-file` flags and the RPC `instructionInjectors` field.
183
183
  - [Credentials and redaction](credentials-and-redaction.md): `createSecretRedactor`, `redactProviderRequest`, `redactAgentEvent`.
184
+ - [Hooks](hooks.md): the Claude Code / Codex event map, where hook `additionalContext` is queued for the next assembly.
@@ -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
@@ -87,8 +87,10 @@ Set only the rows you want to run; everything else skips. Least-privilege scope
87
87
  | `providers/ollama` | active | `PRISM_LIVE_PROVIDER_TESTS` + `OLLAMA_BASE_URL` | `PRISM_LIVE_OLLAMA_MODEL` (default `(first model served by OLLAMA_BASE_URL)`) | No credential for local ollama serve; Ollama Cloud key optional via provider options. | 3-4 requests on the first locally pulled model. |
88
88
  | `providers/ai-sdk` | active | `PRISM_LIVE_PROVIDER_TESTS` + `OPENAI_API_KEY` | `PRISM_LIVE_AISDK_MODEL` (default `gpt-5.1`) | Reuses OPENAI_API_KEY through the real @ai-sdk/openai provider. | 3 requests on gpt-5.1. |
89
89
  | `providers/model-discovery` | active | `PRISM_LIVE_PROVIDER_TESTS`; any of: `OPENAI_API_KEY` / `GEMINI_API_KEY` | — | Reuses OPENAI_API_KEY or GEMINI_API_KEY for a real listing request. | 2 GET /models requests (second cached in TTL). |
90
+ | `calibration/vendor-count-tokens` | active | `PRISM_LIVE_PROVIDER_TESTS`; any of: `ANTHROPIC_API_KEY` / `GEMINI_API_KEY` / `GOOGLE_API_KEY` | `PRISM_LIVE_ANTHROPIC_MODEL` (default `claude-haiku-4-5`), `PRISM_LIVE_GOOGLE_MODEL` (default `gemini-2.5-flash-lite`) | Count-only token requests (no generation) on non-sensitive fixture text, using the chat key each vendor already needs. | 3 token-count requests per credentialed vendor (prose, CJK, chat transcript); no generation, no streaming. |
90
91
  | `cli/live-journey` | planned | `PRISM_LIVE_PROVIDER_TESTS` + `OPENAI_API_KEY` | — | Packed CLI: init/provider-add/print/json/rpc over a real provider; transcript secret-scanned. | 1 pack + install, offline scaffold tests, <=3 wire prompts on the selected provider model (wire legs skip on 401/403). |
91
92
  | `memory/rag-rerankers-live` | active | any of: `PRISM_TEST_TEI_RERANKER_URL` / `PRISM_TEST_HOSTED_RERANK_URL`; optional: `PRISM_TEST_HOSTED_RERANK_URL` | `PRISM_LIVE_TEI_RERANKER_MODEL` (default `(endpoint default model)`), `PRISM_LIVE_HOSTED_RERANK_MODEL` (default `(endpoint default model)`) | Real TEI / OpenAI-compatible rerank endpoints; each leg self-skips when its endpoint env is unset. | 1 rerank request per configured endpoint (≤2 total). |
93
+ | `memory/local-rerank-live` | active | `PRISM_TEST_LOCAL_RERANK`; optional: `PRISM_TEST_LOCAL_RERANK_CACHE_DIR` | `PRISM_LIVE_LOCAL_RERANK_MODEL` (default `Xenova/bge-reranker-base`) | In-process cross-encoder (transformers.js, q8/cpu) on this machine; no service and no credential. Weights come from the documented model id into a host cache dir; a cold run downloads a ≈280 MB int8 model. | One ≈280 MB model download on a cold cache; zero network after load, no API spend. ≈30 s wall clock after the cache is warm (the recall leg scores 24 × 96 query/document pairs). |
92
94
  | `memory/drive-sync-live` | active | `PRISM_TEST_DRIVE_ACCESS_TOKEN`; optional: `PRISM_TEST_DRIVE_FOLDER_ID` `PRISM_TEST_DRIVE_SHARED_DRIVE_ID` | — | Delegated Drive readonly token; least privilege: one throwaway folder. Folder/shared-drive ids optional. | <=2 Drive list/changes pages plus file media for that page; replay must not re-embed. |
93
95
  | `coding-tools/openapi-live` | active | `PRISM_LIVE_OPENAPI_TOOLS` | — | Real public OpenAPI 3.1 spec (warnely.com) + real GET tool calls; no credential. | 3 HTTP requests against example.com-class public hosts (plan budget ≤5). |
94
96
  | `coding-tools/computer-use-live` | active | `PRISM_TEST_COMPUTER_USE` + `PRISM_COMPUTER_USE_BIN` | — | Real host computer-use-linux MCP binary over stdio; real tool inventory + one bounded read-only screenshot. | Local desktop only; ≤30s ceiling. |
@@ -98,7 +100,7 @@ Set only the rows you want to run; everything else skips. Least-privilege scope
98
100
  | `core/webhooks-live` | active | `PRISM_TEST_WEBHOOK_URL` | — | Operator-controlled signed webhook receiver. | <=3 webhook deliveries (1 signed target + 1 loopback retry receiver). |
99
101
  | `core/artifact-bodies-s3-live` | active | `PRISM_TEST_S3_ENDPOINT` + `PRISM_TEST_S3_KEY` + `PRISM_TEST_S3_SECRET` + `PRISM_TEST_S3_BUCKET` | — | Dedicated throwaway S3-compatible bucket. | <=5 S3 requests (put/get/presign/delete x2). |
100
102
  | `cli/journey` | active | `PRISM_LIVE_PROVIDER_TESTS`; any of: `OPENAI_API_KEY` / `OPENROUTER_API_KEY` / `KIMI_API_KEY` / `ZAI_API_KEY` / `OPENCODE_API_KEY` / `NEURALWATT_API_KEY` / `DASHSCOPE_API_KEY` / `OLLAMA_API_KEY` | — | First init-catalog provider credential present in the environment. | 4-5 one-shot requests on the default catalog model. |
101
- | `memory/postgres` | active | `PRISM_TEST_POSTGRES_URL` | — | Postgres memory store + pgvector index round-trips on the operator database. | Bounded insert/query cycles against the configured Postgres. |
103
+ | `memory/postgres` | active | `PRISM_TEST_POSTGRES_URL` | — | Postgres memory store + pgvector index round-trips on the operator database, including durable deletion propagation (1,000 derived rows tombstoned in one transaction) and grant-change re-pointing. | Bounded insert/query cycles against the configured Postgres. |
102
104
  | `memory/graft` | active | — (hermetic leg) | — | Graft upstream CLI child-process protocol (real binary spawns against the in-repo fixture graft bin). | Hermetic; no network. |
103
105
  | `memory/wiki` | active | — (hermetic leg) | — | Wiki lifecycle over real fs trees (init/refresh/lint/search fallback; qmd child-process client degrades without the binary). | Hermetic; no network. |
104
106
  | `coding-tools/lsp-forge` | active | — (hermetic leg) | — | LSP/language-intelligence + forge suites: real child-process spawns over the real LSP/forge wire protocols against fixture binaries. | Hermetic; no network. |
@@ -57,6 +57,34 @@ keyed by block label, and `episode` views write nothing. A rewrite keeps the row
57
57
  consent, importance, and every non-fabric metadata key; an explicit `id` or `supersedes` from the
58
58
  caller disables auto-folding for that write.
59
59
 
60
+ ### Following the file (path moves and deletes)
61
+
62
+ A `file` note names its document by `metadata.path`, and that path — not a lineage edge — is the only
63
+ link between note and file. Notes are store-backed metadata rather than derived chunk rows, so the
64
+ `_lineage` walk that revokes derived records can never find them: this handler is the only path.
65
+
66
+ ```ts
67
+ import { createFabricRepointHandler } from "@arnilo/prism-memory/fabric";
68
+
69
+ // One handler, both seams: a move rewrites `metadata.path`, a delete tombstones.
70
+ const notes = createFabricRepointHandler({ scope, vectorStore: store });
71
+ await repointSource({ scope, vectorStore: store, from: "docs/a.md", to: "docs/b.md", authorization: principal, handlers: [notes] });
72
+ await createDeletionPropagator({ scope, vectorStore: store, authorization: principal, handlers: [notes] }).propagate("docs/a.md");
73
+ ```
74
+
75
+ - Move: only `kind: "file"` notes whose `path` is the moved id are touched. Id, text, embedding,
76
+ `sourceEntryIds`, and every other metadata field are reused verbatim — no re-embed, no re-score, and
77
+ no `_lineage` field invented, so the walk stays for records that really are derived. Non-file notes
78
+ and notes for other paths are untouched, and the write joins one store transaction when the store
79
+ has one (a plain `upsert` otherwise).
80
+ - Delete: the notes recorded against the deleted path are tombstoned through the store's own
81
+ invalidation path (`invalidate`, batched at `HARD_INVALIDATION_BATCH`; reason `forgotten` by default,
82
+ `legal_hold` when both the handler and the propagator are given it), so recall stops serving them
83
+ with no second revocation plane and no background cleanup to wait for.
84
+ - One scope read per leg, selecting on `metadata.fabric.path` — never on content. A note in another
85
+ scope is never visible, and a composition whose scope differs from the handler's is refused
86
+ (`MemoryScopeError`) instead of writing across threads.
87
+
60
88
  ### Workers (opt-in)
61
89
 
62
90
  Both workers run inline (awaited) after the write, see redacted text only, call no model and no