@arnilo/prism 0.9.0 → 0.10.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +24 -1
- package/README.md +13 -12
- package/dist/agent-approval.d.ts +7 -1
- package/dist/agent-approval.js +15 -6
- package/dist/agent-run-lifecycle.js +19 -5
- package/dist/agent-run-state.d.ts +26 -5
- package/dist/agent-run-state.js +97 -1
- package/dist/agent-session/event-subscriber.d.ts +2 -0
- package/dist/agent-session/event-subscriber.js +3 -0
- package/dist/agent-session/session/assemble.js +156 -9
- package/dist/agent-session/session/persist.js +11 -5
- package/dist/agent-session/session/provider-round.js +54 -13
- package/dist/agent-session/session/tool-round.d.ts +2 -2
- package/dist/agent-session/session/tool-round.js +58 -5
- package/dist/agent-session/session/types.d.ts +20 -2
- package/dist/agent-session/session.d.ts +65 -4
- package/dist/agent-session/session.js +156 -16
- package/dist/context-budget.d.ts +11 -0
- package/dist/context-budget.js +33 -2
- package/dist/contracts-core/agent.d.ts +26 -5
- package/dist/contracts-core/extensions.d.ts +3 -0
- package/dist/contracts-core/guardrail-packs.d.ts +8 -3
- package/dist/contracts-core/loop.d.ts +36 -0
- package/dist/contracts-core/provider.d.ts +6 -1
- package/dist/contracts-core/run-limits.d.ts +10 -1
- package/dist/contracts-protocol.d.ts +6 -4
- package/dist/contracts-run-state.d.ts +37 -3
- package/dist/contributions.d.ts +2 -1
- package/dist/contributions.js +1 -0
- package/dist/extensions.d.ts +15 -1
- package/dist/extensions.js +68 -0
- package/dist/guardrail-packs/types.d.ts +10 -0
- package/dist/guardrail-packs/validation-respect.js +16 -0
- package/dist/guardrails.d.ts +42 -1
- package/dist/guardrails.js +124 -15
- package/dist/index.d.ts +6 -6
- package/dist/index.js +4 -4
- package/dist/middleware.d.ts +1 -1
- package/dist/run-bundle.d.ts +6 -1
- package/dist/run-bundle.js +4 -1
- package/dist/run-limits.js +13 -0
- package/dist/testing/prefix-stability-conformance.d.ts +29 -0
- package/dist/testing/prefix-stability-conformance.js +91 -23
- package/dist/tools.js +10 -3
- package/docs/agent-events.md +12 -8
- package/docs/agent-session-runtime.md +9 -6
- package/docs/caveman.md +1 -1
- package/docs/compaction-llm.md +2 -0
- package/docs/compaction-observational-memory.md +21 -1
- package/docs/durable-runs.md +4 -3
- package/docs/embeddings.md +5 -1
- package/docs/execution-timeline.md +3 -2
- package/docs/extensions.md +20 -3
- package/docs/guardrails.md +16 -6
- package/docs/hooks.md +282 -0
- package/docs/index.md +18 -15
- package/docs/input-and-prompt-assembly.md +1 -1
- package/docs/instruction-injection.md +1 -0
- package/docs/live-testing.md +3 -1
- package/docs/memory-fabric.md +28 -0
- package/docs/middleware-hooks.md +54 -4
- package/docs/migration.md +13 -0
- package/docs/options-index.md +3 -1
- package/docs/policy-and-audit.md +14 -1
- package/docs/prefix-stability-conformance.md +57 -7
- package/docs/provider-packages.md +20 -20
- package/docs/public-contracts.md +1 -0
- package/docs/rag.md +93 -6
- package/docs/release-and-install.md +42 -39
- package/docs/runs-and-usage.md +17 -8
- package/docs/scoped-agent-memory.md +17 -9
- package/docs/scoped-memory.md +138 -0
- package/docs/tools.md +1 -1
- package/docs/wiki.md +4 -2
- package/package.json +4 -2
package/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,7 +2,7 @@
|
|
|
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.
|
|
5
|
+
## Current line (0.10.0)
|
|
6
6
|
|
|
7
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
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`.
|
|
@@ -17,7 +17,7 @@ Prism is a TypeScript/Node.js agent harness. Hosts own providers, tools, credent
|
|
|
17
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
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
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
|
-
- **
|
|
20
|
+
- **12 publishable packages** at current **0.10.0** lockstep, with the migration guide reachable from the release section below — inventory below.
|
|
21
21
|
|
|
22
22
|
### Carried from the 0.8.0 line
|
|
23
23
|
|
|
@@ -76,6 +76,7 @@ Prism is a TypeScript/Node.js agent harness. Hosts own providers, tools, credent
|
|
|
76
76
|
- [Durable runs](durable-runs.md): turn-boundary `checkpointPolicy: "every-turn"` checkpoints and `decision: "continue"` crash recovery for long runs.
|
|
77
77
|
- [Agent definitions](agent-definitions.md): declarative `AgentDefinition` resolution and `AGENT.md` bundle discovery, fail-closed activation.
|
|
78
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.
|
|
79
80
|
- [Guardrails](guardrails.md): typed fail-closed input/output/tool checks with redacted decision records.
|
|
80
81
|
- [Agent events](agent-events.md): `turn_started`/`tool_call_delta` stream plus durable page/resume sources for reconnect.
|
|
81
82
|
- [Observability](observability.md): OTel GenAI span hierarchy, workflow spans, cockpit aggregations, RAG span tree, bounded trace linkage, exporter isolation.
|
|
@@ -95,6 +96,7 @@ Prism is a TypeScript/Node.js agent harness. Hosts own providers, tools, credent
|
|
|
95
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.
|
|
96
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.
|
|
97
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`).
|
|
98
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.
|
|
99
101
|
- [Session stores](session-stores.md): `SessionStore` contract, append options, branches, bounded search — start here for persistence.
|
|
100
102
|
- [Conversations](conversations.md): durable user-scoped threads with versioned metadata and legal-hold-aware deletion.
|
|
@@ -233,7 +235,7 @@ Prism is a TypeScript/Node.js agent harness. Hosts own providers, tools, credent
|
|
|
233
235
|
- [Prefix stability conformance](prefix-stability-conformance.md): assert progressive disclosure keeps the provider cache prefix stable.
|
|
234
236
|
- [Tool conformance](tool-conformance.md): assert blocked-reason matrix and success-path dispatch behavior.
|
|
235
237
|
- [Extension conformance](extension-conformance.md): assert inert contributions and redacted setup errors.
|
|
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.
|
|
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.
|
|
237
239
|
|
|
238
240
|
## Third-party integrations
|
|
239
241
|
|
|
@@ -262,19 +264,20 @@ Prism is a TypeScript/Node.js agent harness. Hosts own providers, tools, credent
|
|
|
262
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.
|
|
263
265
|
|
|
264
266
|
<!-- generated:package-truth:inventory begin -->
|
|
265
|
-
**
|
|
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.
|
|
266
268
|
|
|
267
269
|
| package | version | notes |
|
|
268
270
|
| --- | --- | --- |
|
|
269
|
-
| `@arnilo/prism` | 0.
|
|
270
|
-
| `@arnilo/prism-channels` | 0.
|
|
271
|
-
| `@arnilo/prism-coding-tools` | 0.
|
|
272
|
-
| `@arnilo/prism-core` | 0.
|
|
273
|
-
| `@arnilo/prism-providers` | 0.
|
|
274
|
-
| `@arnilo/prism-acp-agent` | 0.
|
|
275
|
-
| `@arnilo/prism-ag-ui` | 0.
|
|
276
|
-
| `@arnilo/prism-
|
|
277
|
-
| `@arnilo/prism-
|
|
278
|
-
| `@arnilo/prism-
|
|
279
|
-
| `@arnilo/prism-
|
|
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 |
|
|
280
283
|
<!-- generated:package-truth:inventory end -->
|
|
@@ -91,7 +91,7 @@ In cache-aware mode, leading system instructions form the stable boundary before
|
|
|
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
|
|
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.
|
package/docs/live-testing.md
CHANGED
|
@@ -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. |
|
package/docs/memory-fabric.md
CHANGED
|
@@ -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
|
package/docs/middleware-hooks.md
CHANGED
|
@@ -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. Per-turn tool menus use `AgentConfig.toolNarrowing` / `RunOptions.toolNarrowing`, not a middleware hook — see [Tools](tools.md).
|
|
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). Run-end decisions use stop hooks (`AgentConfig.stopHooks` / `RunOptions.stopHooks`) — see [Hooks](hooks.md) — not a middleware hook.
|
|
18
18
|
|
|
19
19
|
## Inputs / request
|
|
20
20
|
|
|
@@ -32,6 +32,7 @@ Built-in hook names:
|
|
|
32
32
|
- `tool_call`
|
|
33
33
|
- `tool_result`
|
|
34
34
|
- `retry`
|
|
35
|
+
- `compaction_request`
|
|
35
36
|
- `compaction`
|
|
36
37
|
- `session_start`
|
|
37
38
|
- `session_shutdown`
|
|
@@ -48,7 +49,16 @@ Built-in hook names:
|
|
|
48
49
|
|
|
49
50
|
## Outputs / response / events
|
|
50
51
|
|
|
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
|
|
52
|
+
`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_request` with the strategy's `CompactionContext` before the strategy runs — only when compaction triggers (manual `session.compact()` and auto-compaction both route through it), so a handler rewrites `entries`, `keepRecentEntries`, `metadata`, or `secrets` for the strategy — then invokes `compaction` with `{ context, result }` after the 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.
|
|
53
|
+
|
|
54
|
+
Session lifecycle hooks are dispatched by the agent/session runtime, once each:
|
|
55
|
+
|
|
56
|
+
| Hook | When | Payload |
|
|
57
|
+
| --- | --- | --- |
|
|
58
|
+
| `session_start` | First run start of a session, after `agent_started`/`agent_resumed` and before the first provider turn. A session rebuilt from a durable checkpoint is a new runtime session, so it opens again. | `{ sessionId, runId }` |
|
|
59
|
+
| `session_shutdown` | `session.close()`, before every subscriber is closed. Idempotent — calling `close()` twice dispatches once. | `{ sessionId }` |
|
|
60
|
+
|
|
61
|
+
Both are one dispatch per session, never per turn, and both honor the registry `errorPolicy` exactly like every other hook: with `"event"` a throw becomes an `extension_error` event and the run continues, with `"throw"` it surfaces (for `session_start`, `session.run()` rejects; for `session_shutdown`, `close()` rejects after closing subscribers).
|
|
52
62
|
|
|
53
63
|
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.
|
|
54
64
|
|
|
@@ -88,11 +98,48 @@ import type { Extension } from "@arnilo/prism";
|
|
|
88
98
|
export const extension: Extension = {
|
|
89
99
|
name: "demo-middleware",
|
|
90
100
|
setup(api) {
|
|
91
|
-
api.use("session_start", (event) =>
|
|
101
|
+
api.use("session_start", (event) => {
|
|
102
|
+
// Once per session: provision session-scoped state here.
|
|
103
|
+
return event;
|
|
104
|
+
});
|
|
92
105
|
},
|
|
93
106
|
};
|
|
94
107
|
```
|
|
95
108
|
|
|
109
|
+
`session_shutdown` runs on `await session.close()`, which then closes every subscriber:
|
|
110
|
+
|
|
111
|
+
```ts
|
|
112
|
+
const session = createAgent({ model, provider, middleware }).createSession();
|
|
113
|
+
await session.run("hello");
|
|
114
|
+
await session.close(); // session_shutdown middleware once, then every subscriber closes
|
|
115
|
+
```
|
|
116
|
+
|
|
117
|
+
## Pre-compaction rewrite (`compaction_request`)
|
|
118
|
+
|
|
119
|
+
`compaction_request` is the input side of the compaction pair: it runs once per compaction event, right
|
|
120
|
+
after `compaction_started` and before the strategy's `compact()`, and its return value **is** the
|
|
121
|
+
strategy's input. The payload is the same `CompactionContext` the strategy would have received
|
|
122
|
+
(`sessionId`, `entries`, `keepRecentEntries`, `trigger`, `secrets`, `metadata`, `signal`), and the
|
|
123
|
+
post-strategy `compaction` hook then observes that rewritten context — so a subscriber always sees
|
|
124
|
+
what actually compacted.
|
|
125
|
+
|
|
126
|
+
```ts
|
|
127
|
+
import { createMiddlewareRegistry, type CompactionContext } from "@arnilo/prism";
|
|
128
|
+
|
|
129
|
+
const middleware = createMiddlewareRegistry();
|
|
130
|
+
middleware.use<CompactionContext>("compaction_request", (context, next) =>
|
|
131
|
+
next({ ...context, entries: pinCriticalFacts(context.entries) }),
|
|
132
|
+
);
|
|
133
|
+
```
|
|
134
|
+
|
|
135
|
+
Contract:
|
|
136
|
+
|
|
137
|
+
- Only compaction events dispatch it: ordinary turns no-op, even with auto-compaction configured but not triggered.
|
|
138
|
+
- Returning `undefined` without `next()` leaves the original context in place; the registry's `next()`-shape rules apply as everywhere else.
|
|
139
|
+
- A throw follows the registry `errorPolicy`: with `"event"` the error is reported and compaction proceeds on the last committed context; with `"throw"` `session.compact()` (or the run, on the auto path) rejects and no compaction entry is appended.
|
|
140
|
+
- The seam rewrites input, it cannot skip compaction: an empty or invalid entry set is the strategy's own error, not `"do nothing"`.
|
|
141
|
+
- Validation stays where it already was. Entries are redacted on append and the strategy owns its input expectations; the hook is host code inside the same trust boundary as `AgentConfig.compaction`, not a remote endpoint.
|
|
142
|
+
|
|
96
143
|
## No-model turns (`beforeProviderTurn`)
|
|
97
144
|
|
|
98
145
|
`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:
|
|
@@ -131,8 +178,10 @@ Contract:
|
|
|
131
178
|
- Middleware registration is explicit through `createMiddlewareRegistry()` or `ExtensionAPI.use()`.
|
|
132
179
|
- `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.
|
|
133
180
|
- Middleware runs only when the host/runtime calls `run()` or passes the registry to a helper that documents a call site.
|
|
181
|
+
- `session_start`/`session_shutdown` dispatch only when the host passes its registry to `AgentConfig.middleware`; a session that never runs never starts, and one the host never closes never shuts down. Closing an idle session is fine — the hook still does not fire twice. There is no `session_start` for a session that only calls `compact()` or `contextMeter()`.
|
|
134
182
|
- `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.
|
|
135
183
|
- `compaction` middleware may adjust the compaction result summary/data, but runtime still owns session store append ordering and branch parent ids.
|
|
184
|
+
- `compaction_request` runs once per compaction event with the strategy's context as payload; the runtime still redacts the summary, appends the compaction entry, and rebuilds history. Neither hook can skip compaction (an empty entry set is the strategy's error), and neither runs on ordinary turns.
|
|
136
185
|
- `retry` middleware may stop retrying or adjust delay, but runtime still owns retry event emission, abort-aware waiting, and provider-turn boundaries.
|
|
137
186
|
- The registry does not discover packages, read manifests, load config, call providers, execute tools, read resources, or start sessions.
|
|
138
187
|
- Hosts may pass a middleware registry into `createExtensionKernel({ middleware })` to share it with direct host code.
|
|
@@ -152,8 +201,9 @@ Contract:
|
|
|
152
201
|
- [Contribution registries](contribution-registries.md): direct contribution registration separate from middleware.
|
|
153
202
|
- [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`.
|
|
154
203
|
- [Tools](tools.md): tool dispatch behavior that runs `tool_call` and `tool_result` hooks.
|
|
204
|
+
- [Hooks](hooks.md): the hook model, the Claude Code / Codex event map, the `hooks.json` adapter, and the run-end stop hooks that are separate from payload-transforming middleware.
|
|
155
205
|
- [Input and prompt assembly](input-and-prompt-assembly.md): `input_assembly` and `prompt_build` helper call sites.
|
|
156
|
-
- [Compaction and retry policies](compaction-and-retry.md): compaction/retry middleware payloads and runtime timing.
|
|
206
|
+
- [Compaction and retry policies](compaction-and-retry.md): compaction/retry middleware payloads and runtime timing, including the pre-strategy `compaction_request` seam.
|
|
157
207
|
- [Context and skills](context-and-skills.md): `context` helper call site.
|
|
158
208
|
- [Observability](observability.md): optional OpenTelemetry adapter over `AgentEvent` streams.
|
|
159
209
|
- [Public contracts](public-contracts.md): provider, tool, context, session, and extension contracts that runtimes can pass through hooks.
|
package/docs/migration.md
CHANGED
|
@@ -1,5 +1,18 @@
|
|
|
1
1
|
# Migration guide
|
|
2
2
|
|
|
3
|
+
## 0.9.0 → 0.10.0 (hook lifecycle completion, scoped agent memory)
|
|
4
|
+
|
|
5
|
+
**Prism 0.10.0 is a lockstep minor for all twelve publishable packages** — `@arnilo/prism-hooks` is new. Node `>=22` stays the floor. Nothing was removed: no import path moved and no export was dropped (compat baseline: +47 names, zero removals, zero renames). Scoped memory is a new opt-in subpath that stays inert until a host constructs a policy.
|
|
6
|
+
|
|
7
|
+
What a 0.9.0 host must check before upgrading:
|
|
8
|
+
|
|
9
|
+
- **One type-level addition: `AgentSession.close(): Promise<void>`.** Hosts that implement or proxy the interface (not only consume it) must add a `close()`; `async close() {}` satisfies the type, and a proxy should forward to the wrapped session so `session_shutdown` fires once. The in-repo precedent is the observational-memory proxy in `@arnilo/prism-memory`, which forwards it.
|
|
10
|
+
- **`session_start` and `session_shutdown` are now emitted.** Both names were declared in 0.9.0 but had no call site, so extension handlers registered against them never ran. `session_start` fires once per session at the first turn of the first run (durable resumes included); `session_shutdown` fires from `session.close()` and is idempotent — a host that never calls `close()` never sees it.
|
|
11
|
+
- **`hook_limit` is a new `AgentFinishReason`** (and a `StoredAgentRunState.stopReason`). Exhaustive switches over finish reasons must handle it; a `hook_limit` stop stays resumable only under `checkpointPolicy: "every-turn"`.
|
|
12
|
+
- **Stop hooks are bounded by default.** `RunLimits.maxStopContinuations` defaults to 3 (`0` disables continuation, `null` uncaps), and only a run that registers stop hooks can reach the limit.
|
|
13
|
+
- **`compaction_request` runs before the compaction strategy** when a handler is registered (entries and budget are rewritable); with no handler the compaction path is unchanged.
|
|
14
|
+
- **Additive, inert by default:** `AgentConfig.stopHooks` / `RunOptions.stopHooks` and `ExtensionAPI.registerStopHook()`, `forwardAgentEvents()`, the `@arnilo/prism-hooks` adapter, and the whole `@arnilo/prism-memory/scoped` surface (reviewer, promotion ladder, GC proposals, bounded facts block, approval gate, audit mirror, eval harness).
|
|
15
|
+
|
|
3
16
|
## 0.8.0 → 0.9.0 (attention budget axes, turn traces, tool narrowing, guardrail packs, background agents, session search, deterministic turns, shared scopes)
|
|
4
17
|
|
|
5
18
|
**Prism 0.9.0 is a lockstep minor for all eleven publishable packages.** Node `>=22` stays the floor. Nothing was removed: no import path moved, no export was dropped, and every new surface defaults to 0.8 behavior. The full guide — the four deltas inside existing surfaces, every new option with its sizing line, upgrade steps, and rollback — is [migrate-to-0.9.md](migrate-to-0.9.md).
|
package/docs/options-index.md
CHANGED
|
@@ -42,10 +42,12 @@ Field-level detail (defaults, bounds, failure modes) lives on the owning page
|
|
|
42
42
|
| --- | --- | --- |
|
|
43
43
|
| `RunOptions.turnPolicy` (`TurnPolicyOptions`) | Synchronous host stop at a turn boundary; a stop lands as `stopReason: "host_policy"` and stays resumable | [Agent loops](agent-loops.md) |
|
|
44
44
|
| `CreateAgUiHandlerOptions.inputPolicy` (`AgUiInputPolicyOptions`) | `clientState: "honor"` \| `"ignore"` — whether the server honors client-supplied AG-UI state and tools | [Frontend interoperability](ag-ui.md) |
|
|
45
|
+
| `SubscribeOptions.acrossRuns` | `true` keeps one live subscriber open across runs of the same session until the host ends it, session teardown, or an overflow (bounded queue, default 1024, no background work); default `false` closes it at run end | [Agent events](agent-events.md) |
|
|
45
46
|
| `snapshotRunBundle(...)` → `RunBundleSnapshot` | Inspectable digest projection of the effective run bundle (prompt/skill/tool/guardrail digests, limits, storage kinds) | [Run bundle](run-bundle.md) |
|
|
46
47
|
| `createClaimGroundingGuardrail` (`ClaimGroundingGuardrailOptions`) | `"output"`-stage guardrail that blocks or flags numeric claims no tool result or host evidence supports | [Guardrails](guardrails.md) |
|
|
47
48
|
| `ErrorInfo.failureClass` (`ProviderFailureClass`) | Typed provider failure on run outcomes, ledger rows, and tool results (`quota`, `rate_limited`, `auth`, `transient`, `permanent`) | [Runs and usage](runs-and-usage.md) |
|
|
48
|
-
| `AgentConfig.usageEstimation` | `"fallback"` (default) records a labeled estimate when a provider reports no usage; `"off"` leaves usage absent; estimates are never priced | [Runs and usage](runs-and-usage.md#automatic-fallback-agentconfigusageestimation) |
|
|
49
|
+
| `AgentConfig.usageEstimation` | `"fallback"` (default) records a labeled estimate when a provider reports no usage; `"off"` leaves usage absent; `"strict"` refuses the turn instead (`code: "usage_missing"`); estimates are never priced | [Runs and usage](runs-and-usage.md#automatic-fallback-agentconfigusageestimation) |
|
|
50
|
+
| `AgentConfig.contextBudget` | Session-turn eviction budget forwarded to every assembly (`maxInputTokens`/`maxInputBytes`, `reportOmissions`, `tokenEstimator`); the usage fallback prefers its measurement | [Input and prompt assembly](input-and-prompt-assembly.md) |
|
|
49
51
|
| `ModelConfig.capabilities.toolCallStrictness` | Advisory tool-call reliability (`"strict"` \| `"lenient"` \| `"legacy"`); catalog conformance, not a promise | [Model registry](model-registry.md) |
|
|
50
52
|
|
|
51
53
|
## Agent/session runtime
|