@arnilo/prism 0.9.0 → 0.11.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 +50 -1
- package/README.md +19 -16
- package/dist/agent-approval.d.ts +7 -1
- package/dist/agent-approval.js +15 -6
- package/dist/agent-run-lifecycle.d.ts +2 -1
- package/dist/agent-run-lifecycle.js +20 -6
- 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 +165 -16
- 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 +86 -23
- package/dist/agent-session/session/types.d.ts +21 -2
- package/dist/agent-session/session.d.ts +66 -4
- package/dist/agent-session/session.js +159 -18
- package/dist/checkpoint-restore.d.ts +50 -14
- package/dist/checkpoint-restore.js +104 -28
- 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-core/session.d.ts +2 -1
- package/dist/contracts-protocol.d.ts +6 -4
- package/dist/contracts-run-state.d.ts +48 -6
- 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 +7 -7
- package/dist/index.js +4 -4
- package/dist/leases.js +32 -6
- package/dist/middleware.d.ts +1 -1
- package/dist/node/contribution-discovery.d.ts +16 -1
- package/dist/node/contribution-discovery.js +47 -0
- package/dist/node/session-store-jsonl.js +67 -17
- package/dist/run-bundle.d.ts +6 -1
- package/dist/run-bundle.js +4 -1
- package/dist/run-limits.d.ts +11 -5
- package/dist/run-limits.js +13 -0
- package/dist/session-stores.js +61 -12
- package/dist/testing/prefix-stability-conformance.d.ts +73 -1
- package/dist/testing/prefix-stability-conformance.js +158 -27
- package/dist/tools.js +10 -3
- package/dist/usage-estimation.d.ts +7 -1
- package/dist/usage-estimation.js +16 -10
- package/docs/acp.md +2 -2
- package/docs/agent-events.md +15 -10
- package/docs/agent-session-runtime.md +10 -7
- package/docs/coding-agent-tools.md +1 -1
- package/docs/coding-tools.md +7 -11
- package/docs/compaction-llm.md +2 -0
- package/docs/compaction-observational-memory.md +21 -1
- package/docs/context-and-skills.md +6 -7
- package/docs/contribution-discovery.md +13 -0
- package/docs/durable-runs.md +14 -6
- package/docs/embeddings.md +7 -1
- package/docs/execution-timeline.md +9 -2
- package/docs/extensions.md +21 -5
- package/docs/guardrails.md +16 -6
- package/docs/hooks.md +282 -0
- package/docs/impeccable.md +1 -2
- package/docs/index.md +28 -21
- package/docs/input-and-prompt-assembly.md +1 -1
- package/docs/instruction-injection.md +1 -0
- package/docs/live-testing.md +3 -2
- package/docs/memory-fabric.md +29 -0
- package/docs/middleware-hooks.md +54 -4
- package/docs/migrate-to-0.11.md +65 -0
- package/docs/migration.md +24 -0
- package/docs/node-jsonl-session-store.md +4 -3
- package/docs/operations.md +1 -1
- package/docs/options-index.md +3 -1
- package/docs/peer-dependencies.md +3 -5
- package/docs/policy-and-audit.md +15 -2
- package/docs/prefix-stability-conformance.md +82 -9
- package/docs/provider-packages.md +20 -20
- package/docs/public-contracts.md +2 -1
- package/docs/rag.md +94 -7
- package/docs/release-and-install.md +62 -59
- package/docs/runs-and-usage.md +21 -10
- package/docs/scoped-agent-memory.md +17 -9
- package/docs/scoped-memory.md +138 -0
- package/docs/session-stores.md +2 -2
- package/docs/supervisors.md +14 -6
- package/docs/testing.md +17 -9
- package/docs/tools.md +1 -1
- package/docs/wiki.md +4 -2
- package/docs/workflows.md +2 -2
- package/package.json +8 -5
- package/docs/caveman.md +0 -130
- package/docs/graft.md +0 -149
- package/docs/ponytail.md +0 -129
|
@@ -88,6 +88,7 @@ Key exports:
|
|
|
88
88
|
| `renderObservationalMemory()` | Render reflections and observations into a prepared memory summary. |
|
|
89
89
|
| `recallObservationalMemory()` | Recover source evidence for a known observation/reflection id from supplied current-branch entries. `invalidatedIds` withholds content (`reason: "revoked"`) without injecting derived text. |
|
|
90
90
|
| `listInvalidatedIds()` (`@arnilo/prism-memory`) | Read the ids one exact scope currently withholds (`corrected` stays) and pass them as `invalidatedIds`, so blocks that rest on a source revoked mid-turn go stale on the next build. Empty for stores without lineage invalidation. |
|
|
91
|
+
| `createObservationalMemoryDropHandler()` | The OM leg of `createDeletionPropagator`: folds the session ledger once and appends one `om.observations.dropped` entry naming every active observation whose id or `sourceEntryIds` intersect the tombstone set (`coversUpToId` omitted — a tombstone set is not a coverage position). Register with `{ session, appendEntry }`; pair it with `listInvalidatedIds()` for the read path. |
|
|
91
92
|
| `recallObservationalMemoryBranchPage()` | Page eligible user/assistant/tool messages around a cursor entry id (`forward`/`backward`, optional `detail: summary|full`). |
|
|
92
93
|
| `createMemoryId()` / `isMemoryId()` | Create/check 12-character ids. |
|
|
93
94
|
| `resolveObservationalMemorySettings()` | Merge `observational-memory` settings with defaults and overrides. |
|
|
@@ -101,6 +102,25 @@ Key exports:
|
|
|
101
102
|
|
|
102
103
|
Pure utilities create no events, workers, tools, commands, credentials, or provider requests. `createObservationalMemoryExtension()` and import alone start nothing. `createObservationalMemory().attach()` runs workers only after proxied `run`/`prompt`/`stream`/`compact` complete (or after `wrapResumeRun` / `wrapResumeStream`). `createObservationalMemoryRuntime().flush()` remains for manual/advanced use. Attached `contextProvider` renders two blocks each turn: `observational-memory` (active reflections/observations aligned to the recent-message boundary) and `recent-messages` (last `keepRecentEntries` message entries in branch order, optionally trimmed by `recentMessageMaxTokens` using `estimateEntryTokens`; oldest dropped first). Compaction uses the same `keepRecentEntries` setting. Observer input includes only eligible `message` entries (`user`, `assistant`, `tool`); memory/compaction/bookkeeping entries advance `coversUpToId` scan coverage without entering the observer prompt. Successful observer/reflector runs append coverage markers even when they record zero facts. Reflection uses only active observations recorded after the last `om.reflections.recorded` entry unless `flush({ fullReflectionRebuild: true })`. Attached `flush()` skips with `run_active` while a proxied run is in flight. The compaction strategy is O(n) over supplied entries and makes no provider call.
|
|
103
104
|
|
|
105
|
+
### Revocation wiring (plan 102 Tasks 2/8)
|
|
106
|
+
|
|
107
|
+
A revoked source must stop feeding memory on both sides of the write. The two halves share one tombstone set:
|
|
108
|
+
|
|
109
|
+
```ts
|
|
110
|
+
import { listInvalidatedIds } from "@arnilo/prism-memory";
|
|
111
|
+
import { buildObservationalMemoryContextBlocks, createObservationalMemoryDropHandler } from "@arnilo/prism-memory/compaction/observational-memory";
|
|
112
|
+
|
|
113
|
+
// Write path: register the OM leg on the host's deletion propagator (see docs/rag.md for the full wiring).
|
|
114
|
+
const handlers = [createObservationalMemoryDropHandler({ session, appendEntry })];
|
|
115
|
+
|
|
116
|
+
// Read path: withhold at build time for a projection whose ledger has no drop entry yet.
|
|
117
|
+
const blocked = await listInvalidatedIds(store, scope); // one scope read; `corrected` ids stay
|
|
118
|
+
const blocks = buildObservationalMemoryContextBlocks(entries, { invalidatedIds: blocked });
|
|
119
|
+
```
|
|
120
|
+
|
|
121
|
+
- Both paths render the same memory for the same tombstones, so an id is withheld whether or not the physical drop ran — the drop entry answers "what did the revocation retire" for audit, `invalidatedIds` answers "what must not be injected right now".
|
|
122
|
+
- The drop entry carries observation ids only (no observation text), and the fold treats it as a drop rather than progress (`coversUpToId` omitted).
|
|
123
|
+
|
|
104
124
|
### Work-scope index (opt-in)
|
|
105
125
|
|
|
106
126
|
`WorkScope` is a host-named, append-only index over one observational-memory ledger. Without `om.scope.*` entries, the map has only its implicit `session` root, context renders the existing active pool, and the dropper keeps its existing behavior. Shared work scopes extend that index across sessions under explicit grants — see "Shared work scopes (opt-in)" below.
|
|
@@ -237,7 +257,7 @@ Worker `provider.generate` calls use a **derived** correlation id `om:{session.i
|
|
|
237
257
|
|
|
238
258
|
The runtime requires host-supplied `session`, an `appendEntry` callback bound to that session's owning store/branch, and at least one worker provider (`observation.provider` / `reflection.provider` / `dropper.provider`). Model selection uses [use-case model selection](use-case-model-selection.md): pass per-worker `model` (or settings `observation.model` / `reflection.model` / `dropper.model`) to override, and `sessionModel: agent.config.model` so workers fall back to the session model when no worker model is configured. `requireExplicitModel: true` restores the historical `missing_model` skip when no explicit worker model is set. It no longer accepts a separate `store` option because mismatched session/store pairs can append memory entries outside the active branch. After each memory append, the runtime checks the appended entry is visible at the session leaf and fails closed/restores the previous checkout if the callback points elsewhere. Optional credential resolution is explicit; missing requested credentials skip worker execution. Default credential requests use the **resolved** model's provider id.
|
|
239
259
|
|
|
240
|
-
`createObservationalMemoryCompactionStrategy()` keeps recent message entries like the default compaction strategy, renders existing observations/reflections as the summary, and returns a standard Prism compaction entry. Its `data` includes `throughEntryId`, `keepEntryIds`, `strategy`, `trigger`, and `memory: { type: "om.folded", version: 1, fullFold, observations, reflections, droppedObservationIds }`. When active observations exceed `context.observationsPoolMaxTokens`, it performs a full fold and synchronously trims lowest-relevance observations until the folded payload fits hard byte/token caps (or throws a typed error).
|
|
260
|
+
`createObservationalMemoryCompactionStrategy()` keeps recent message entries like the default compaction strategy, renders existing observations/reflections as the summary, and returns a standard Prism compaction entry. Its `data` includes `throughEntryId`, `keepEntryIds`, `strategy`, `trigger`, and `memory: { type: "om.folded", version: 1, fullFold, observations, reflections, droppedObservationIds }`. When active observations exceed `context.observationsPoolMaxTokens`, it performs a full fold and synchronously trims lowest-relevance observations until the folded payload fits hard byte/token caps (or throws a typed error). Because the runtime routes both compaction paths through the pre-strategy `compaction_request` middleware hook, a host can also rewrite the entries this strategy folds without forking it — see [Middleware hooks](middleware-hooks.md).
|
|
241
261
|
|
|
242
262
|
`createRecallMemoryTool()` accepts either `{ id }` for exact memory recall or `{ cursor, limit?, direction?, detail? }` for current-branch raw-message paging (default limit 20, hard cap 100). Reflection recall resolves supporting observations from the full ledger and reports `droppedSupportingObservationIds` / `missingSupportingObservationIds`; dropped supports still return available raw sources. Invalid ids, ambiguous requests, wrong `sessionId`, missing cursors, non-message cursors, and oversized pages fail closed. It does not search by topic.
|
|
243
263
|
|
|
@@ -198,16 +198,15 @@ await agent.createSession().run("…", { activeSkills: ["ponytail"] });
|
|
|
198
198
|
// Turn 1: catalog only. After load_skill({ name: "ponytail" }), later turns include instructions.
|
|
199
199
|
```
|
|
200
200
|
|
|
201
|
-
### Third-party behavior packages (
|
|
201
|
+
### Third-party behavior packages (Impeccable, host-owned personas)
|
|
202
202
|
|
|
203
|
-
`@arnilo/prism-coding-tools/
|
|
203
|
+
`@arnilo/prism-coding-tools/impeccable` registers its upstream skill into the extension kernel skill registry from a host-supplied `upstreamPath`. For any other upstream persona, the host keeps the same shape with public APIs only:
|
|
204
204
|
|
|
205
|
-
1. `
|
|
206
|
-
2.
|
|
207
|
-
3.
|
|
208
|
-
4. Select `instructionInjectors: ["caveman-mode", "ponytail-mode"]` (or subset) for mode/level slices **without** forcing `skillsDisclosure: "eager"`.
|
|
205
|
+
1. Load the `SKILL.md` tree with `loadSkillDirectory` from `@arnilo/prism/node/contribution-discovery` (bounded reads, symlink-contained), and register each skill from an extension `setup()`; log the skills registry with `createSkillRegistry(kernel.registries.skills.list())` and pass `activeSkills` / `resolveActiveSkills` names.
|
|
206
|
+
2. Keep `skillsDisclosure: "progressive"` and register `createLoadSkillTool` — full `SKILL.md` bodies stay catalog-only until `load_skill`.
|
|
207
|
+
3. Register an instruction injector (`api.registerInstructionInjector`) for the mode/level slice and a command that persists the mode as a session `custom` entry, restoring it on the next load — **without** forcing `skillsDisclosure: "eager"`.
|
|
209
208
|
|
|
210
|
-
Mode slices and skill bodies are independent: the injector can add `PONYTAIL MODE ACTIVE` while `ponytail-audit` remains catalog-only until loaded. See [
|
|
209
|
+
Mode slices and skill bodies are independent: the injector can add `PONYTAIL MODE ACTIVE` while `ponytail-audit` remains catalog-only until loaded. See [Impeccable](impeccable.md), [Contribution discovery](contribution-discovery.md), and `examples/caveman-ponytail.ts`.
|
|
211
210
|
|
|
212
211
|
Pure validation without the tool: `resolveSkillLoad({ registry, name, tools, loaded, activeSkillNames })`.
|
|
213
212
|
|
|
@@ -64,6 +64,18 @@ The markdown body below the front fence becomes `Skill.instructions`. Unknown fr
|
|
|
64
64
|
| `--discover-kinds <csv>` | Kinds to scan. Defaults to `skill`. Accepts `skill,tool,context,instructions`. |
|
|
65
65
|
| `--no-discovery` | Hard-disable discovery even if `--discover` is set. |
|
|
66
66
|
|
|
67
|
+
### `loadSkillDirectory` (third-party skill trees)
|
|
68
|
+
|
|
69
|
+
`discoverContributions` scans only `<workspaceRoot>/.agents/<kind>s/<name>/`. To load a tree you already own — an upstream persona checkout, a provider's compiled skills, a vendored `SKILL.md` set — use `loadSkillDirectory(directory, options?)` from `@arnilo/prism/node/contribution-discovery`:
|
|
70
|
+
|
|
71
|
+
```ts
|
|
72
|
+
import { loadSkillDirectory } from "@arnilo/prism/node/contribution-discovery";
|
|
73
|
+
|
|
74
|
+
const skills = await loadSkillDirectory("/opt/upstream/skills", { maxSkillBytes: 64 * 1024 });
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
It reads `<directory>/<name>/SKILL.md` and returns inert `Skill[]` sorted by directory name. Subdirectories without `SKILL.md`, plain files, and symlinks that escape `directory` are skipped; an unreadable `directory` or a file over `options.maxSkillBytes` (default `HARD_MAX_SKILL_INSTRUCTION_BYTES`, 262 144) throws. No trust policy, no permission callback, and no `import()`: the host naming the directory is the authority, exactly as with the deleted persona subpaths (see `examples/caveman-ponytail.ts`).
|
|
78
|
+
|
|
67
79
|
## Outputs / response / events
|
|
68
80
|
|
|
69
81
|
`discoverContributions()` returns `readonly DiscoveredContribution[]`. Each envelope has `kind`, `name`, `origin` (`"global"` | `"workspace"`), `path`, and either `skill` (for the `skill` kind) or `declaration` (a `ManifestContributionDeclaration` for other kinds), plus optional `metadata`. The envelope is inert: it contains no executable code, no credential, and no resolved provider/model/tool reference.
|
|
@@ -131,6 +143,7 @@ A complete runnable example lives at `examples/discover-skills.ts`.
|
|
|
131
143
|
- **Workspace gating**: workspace roots are checked through `createPathTrustPolicy` + `isPathInsideReal`, which resolve symlinks and fail closed (return false) if either root or target cannot be resolved. Untrusted workspace roots are skipped silently, never thrown over. Permission is asserted per directory read via `assertPermission`.
|
|
132
144
|
- **Symlink handling**: symlinked entries that escape the kind root after realpath resolution are excluded. `SKILL.md` and `manifest.json` are also realpath-checked against their contribution directory before read, so an entry-file symlink cannot escape to another path.
|
|
133
145
|
- **Opt-in**: discovery is opt-in — it runs only when the host passes `--discover` or calls `discoverContributions()` explicitly. Default runs perform no filesystem I/O.
|
|
146
|
+
- **Bounded third-party trees**: `loadSkillDirectory` reads one level (`<dir>/<name>/SKILL.md`), realpath-checks each directory and entry file against the supplied root, and fails closed on a missing directory or an over-cap file; the host names the root, so no workspace trust decision is implied.
|
|
134
147
|
- **No auto-execute**: discovery reads text. It does not `import()`, `require()`, or run contribution code. `registerDiscoveredContributions` registers descriptor stubs whose execution methods throw — the host lifts them into live tools/providers itself.
|
|
135
148
|
- **No auto-activate**: discovery registers skills; it does not select them. Activation requires explicit `RunOptions.activeSkills`, and `toolNames` is still validated against the resolved tool set. Discovery grants no tools, permissions, or provider slots.
|
|
136
149
|
- **No provider scanning**: provider/model discovery stays config/package-driven (Phase 24). See [Provider packages](provider-packages.md).
|
package/docs/durable-runs.md
CHANGED
|
@@ -12,7 +12,7 @@ This is crash recovery for the in-run state, not an orchestrator. The host workf
|
|
|
12
12
|
- The host wants a bounded, explicit recovery point rather than "restart the whole run".
|
|
13
13
|
- An external orchestrator needs to resume a single run without replaying its tools.
|
|
14
14
|
|
|
15
|
-
For approval suspension and batch decisions
|
|
15
|
+
For approval suspension and batch decisions — including approve-with-edits revalidation against the session's restored pack rules — see [Agent/session runtime § Durable interruption](agent-session-runtime.md#durable-interruption); `every-turn` is additive to that machinery and uses the same store, redaction, bounds, fingerprint, and CAS.
|
|
16
16
|
|
|
17
17
|
## Inputs / request
|
|
18
18
|
|
|
@@ -23,7 +23,7 @@ For approval suspension and batch decisions, see [Agent/session runtime § Durab
|
|
|
23
23
|
| `checkpointPolicy` | `"decision"` (default) persists only on suspension/terminal status. `"every-turn"` adds one running-state checkpoint per provider turn. |
|
|
24
24
|
| `checkpoints` | The host's `CheckpointStore`; the same store serves suspension, crash recovery, and status. |
|
|
25
25
|
| `definitionRevision` | Host-authored revision participating in the fingerprint; a change without a revision bump refuses resume. |
|
|
26
|
-
| `persistSessionState` | Also carries loaded-skill names
|
|
26
|
+
| `persistSessionState` | Also carries loaded-skill names, the attention sticky frontier, and (plan 104 Task 2/3) the session's guardrail pack refs (or an inline pack's pattern rules) plus each pack's own state snapshot into each turn checkpoint. |
|
|
27
27
|
| `includeSkillBodies` | Alongside `persistSessionState`, carries exact skill instructions. |
|
|
28
28
|
| `maxStateBytes` | Save-side byte ceiling (default 256 KB, hard 1 MB). Applies to every turn checkpoint identically. |
|
|
29
29
|
| `checkpointMetadata` | Sidecar map (`Record<string, string>`, ≤ 4 KB, redacted) written with every checkpoint record — never inside the state value, so it costs no `maxStateBytes` budget. A function is resolved at each write, so a host closure can pin state that moves mid-run (git commit, document version). |
|
|
@@ -49,20 +49,26 @@ head = "commit-2"; // the next checkpoint records the new commit
|
|
|
49
49
|
```ts
|
|
50
50
|
await lifecycle.resume(ref, { decision: "approve", expectedVersion }, {
|
|
51
51
|
restoreHooks: [
|
|
52
|
+
{
|
|
53
|
+
id: "docs",
|
|
54
|
+
restore: (cp) => docs.restoreVersion(cp.metadata?.docVersion),
|
|
55
|
+
compensate: () => docs.restoreVersion(previousVersion),
|
|
56
|
+
},
|
|
52
57
|
async function restoreGit(cp) {
|
|
53
58
|
await git.reset(cp.metadata?.gitCommit);
|
|
54
59
|
},
|
|
55
|
-
async function restoreDocs(cp) {
|
|
56
|
-
await docs.restoreVersion(cp.metadata?.docVersion);
|
|
57
|
-
},
|
|
58
60
|
],
|
|
59
61
|
restoreHookTimeoutMs: 10_000, // default, per hook
|
|
60
62
|
});
|
|
61
63
|
```
|
|
62
64
|
|
|
65
|
+
A bare function is the plan 094 form: the audit names it by `fn.name`, and it has no undo. The object
|
|
66
|
+
form `{ id?, restore, compensate? }` adds a stable audit name and the layer's own undo handler.
|
|
67
|
+
|
|
63
68
|
All-or-nothing:
|
|
64
69
|
|
|
65
70
|
- The first hook that throws or overruns `restoreHookTimeoutMs` (default 10 s, `DEFAULT_CHECKPOINT_RESTORE_TIMEOUT_MS`) aborts the resume with `CheckpointRestoreError` — `code: "ERR_PRISM_CHECKPOINT_RESTORE"`, `hook` naming the layer, `cause` the original error. Later hooks do not run.
|
|
71
|
+
- A failing restore compensates the applied layers in reverse order, starting with the failing hook itself — a half-applied layer is put back by its own `compensate` — each under the same `restoreHookTimeoutMs`. The error carries `compensation: { ran: [<hook names, most recent first>], failed?: { hook, reason } }`; the first compensation failure is recorded (reason redacted and capped at 1 KiB) and never replaces the original `cause`, and the pass continues with the remaining layers. A caller abort stops the pass and rethrows the abort. Compensation is best-effort: the checkpoint stays unclaimed and resumable either way, so a host fixes the failing layer and retries the whole resume.
|
|
66
72
|
- The claim write and the conversation replay happen only after every hook succeeds, so a failed restore leaves the checkpoint byte-for-byte as it was — still resumable — instead of claiming a half-restored world. The server maps the failure to `409`/`ERR_PRISM_CHECKPOINT_RESTORE`.
|
|
67
73
|
- Hooks run on claiming resumes only; `deny` and resuspend paths never call them.
|
|
68
74
|
- The claim's `agent_resumed` event carries the audit: `restore: { hooks: [{ hook, durationMs }], durationMs }`.
|
|
@@ -72,11 +78,12 @@ Resume uses `resumeAgentRun` / `resumeAgentRunStream` with `{ expectedVersion, d
|
|
|
72
78
|
|
|
73
79
|
## Outputs / response / events
|
|
74
80
|
|
|
75
|
-
Each turn checkpoint is a normal durable state (schema v1) carrying status `running`, the current `leafId`, run counters and wall deadline, loop-local state when the loop declares `snapshot`/`restore`, the run's `toolNames` grant, and — with `persistSessionState` — the loaded-skill catalog
|
|
81
|
+
Each turn checkpoint is a normal durable state (schema v1) carrying status `running`, the current `leafId`, run counters and wall deadline, loop-local state when the loop declares `snapshot`/`restore`, the run's `toolNames` grant, and — with `persistSessionState` — the loaded-skill catalog, sticky attention frontier, and the guardrail pack block (`sessionState.guardrailPacks`: `{ packs: [{ id, version, options?, rules? }], state?: { <packId>: <pack state> } }`, ≤ 8 packs and ≤ 64 rules each, ≤ 8 KiB per pack row/rule/options/state, ids ≤ 96 chars, redacted like all state). Measured cost: a 656-byte `persistSessionState` checkpoint grows by 185 bytes for all four built-in packs' rows (≈ 46 bytes/pack); a `validation-respect` row with non-default options and live `validationFailed` state adds ≈ 169 bytes in total. Hard gates are unchanged: CAS `expectedVersion`, ownership/fencing, redaction at the checkpoint boundary, `maxStateBytes`, and the agent fingerprint (`agentFingerprint`) over id, revision, model, instructions, system prompt, skills, tools, guardrails, and loop revision.
|
|
76
82
|
|
|
77
83
|
A crash leaves the last checkpoint at status `running`. `decision: "continue"` accepts exactly that: a running checkpoint with no interruption and no unresolved pending decisions. Everything else fails closed with `AgentRunStateError` and zero checkpoint writes:
|
|
78
84
|
|
|
79
85
|
- `expectedVersion` mismatch, ownership/fencing mismatch, revision or fingerprint mismatch (`Stale or non-running agent run resume`, `Agent revision or fingerprint mismatch on resume`).
|
|
86
|
+
- `sessionState.guardrailPacks` that cannot be replayed: an unknown pack id, an inline rule carrying a `deny` predicate or `RegExp` pattern, a row `version` that no longer matches the installed pack definition, a pack whose state has no codec, more than 8 rows (or 64 rules in one), or malformed/oversized pack state. The resume refuses with `AgentRunStateError` and dispatches nothing — a suspended run's enforcement never silently downgrades. A checkpoint written before plan 104 (no key) resumes with no packs and no error.
|
|
80
87
|
- Status `suspended` — approvals, elicitations, and input guardrails still require `approve`/`deny` or a `RunDecision` batch; `continue` never bypasses a gate.
|
|
81
88
|
- Any interruption, pending decision, or ready-to-dispatch pending call recorded in the state.
|
|
82
89
|
|
|
@@ -124,6 +131,7 @@ The complete network-free demo — one tool execution across the crash, resumed
|
|
|
124
131
|
|
|
125
132
|
- `"continue"` is a host-API action only. Prism's AG-UI interrupt resolution accepts `approve`/`deny` only, channel adapters resume with `deny`, and there is no server route that forwards an untrusted `continue`; adding one would create an approval-bypass path.
|
|
126
133
|
- Restore hooks are trusted host code running outside the sandbox: they see the checkpoint's (already redacted) sidecar map and are bounded only by their timeout. Because they run before the claim write, a timeout cannot leave a claimed checkpoint pointing at un-restored external state.
|
|
134
|
+
- Compensation reports only hook names and `compensation.failed.reason`: the failing handler's message, redacted by the configured agent/lifecycle redactor and capped at 1 KiB. Hook arguments and the sidecar map are never copied into the report; the original error stays in `cause` and crosses the server boundary as before.
|
|
127
135
|
- Every gate that protects a suspension protects a continue resume: exact ownership, fencing token, fingerprint, revision, CAS version, and the absence of unresolved work. A running checkpoint is a recovery point, never an authorization.
|
|
128
136
|
- Cost is one bounded checkpoint write per provider turn (same redaction and `maxStateBytes` ceiling as suspension writes). A 40-turn investigation under `"every-turn"` therefore writes 40 checkpoint rows plus the terminal save, while the default `"decision"` policy writes at most one row per approval or suspension. Each row carries the run frontier, counters, run limits, and loop snapshot — not the message history, which stays in the session store and is pointed at by `leafId` — so the store grows with turns, not with turns × transcript; a state that would exceed `maxStateBytes` (default 256 KiB, `DEFAULT_MAX_AGENT_RUN_STATE_BYTES`) fails closed rather than truncating. Pick `"every-turn"` when a worker restart must cost at most one turn of thinking, and leave the default for runs with many cheap turns.
|
|
129
137
|
- Checkpoints never contain provider objects, callbacks, signals, credentials, or raw secrets; the payload is bounded and redacted like any other durable state.
|
package/docs/embeddings.md
CHANGED
|
@@ -91,7 +91,13 @@ await runEmbeddingsConformance({
|
|
|
91
91
|
reranker (`resolveReranker({ kind: "local" })` / `createLocalReranker`) runs a
|
|
92
92
|
cross-encoder in the host process through a `LocalRerankRuntime`, so no
|
|
93
93
|
inference dependency name enters any manifest — see
|
|
94
|
-
[RAG local reranker](rag.md#local-reranker).
|
|
94
|
+
[RAG local reranker](rag.md#local-reranker). A host that runs both an embedder
|
|
95
|
+
and the reranker through the same runtime should point them at one weight cache:
|
|
96
|
+
one `cacheDir` per host (e.g. `~/.cache/prism/models`), one subdirectory per
|
|
97
|
+
model id, so each model is downloaded once and shared by every process on that
|
|
98
|
+
host; a cache miss downloads into that directory and later runs stay on disk. The live reranker
|
|
99
|
+
leg runs exactly that pair — a semantic embedder and the cross-encoder into one cache dir — and
|
|
100
|
+
records both subdirectories ([semantic reranker evidence](_evidence/phase111-reranker-semantic-recall.md)).
|
|
95
101
|
- Adapters never auto-chunk: a batch over the provider cap rejects with
|
|
96
102
|
`batch_too_large`, so `embedBatched`-style callers own batching and preserve
|
|
97
103
|
per-item error attribution.
|
|
@@ -70,6 +70,8 @@ interface ExecutionTimeline {
|
|
|
70
70
|
readonly workflowRevision?: string;
|
|
71
71
|
/** Workflow checkpoint sidecar metadata (`WorkflowCheckpointValue.metadata`); present only when projected with a checkpoint. */
|
|
72
72
|
readonly workflowMetadata?: Readonly<Record<string, unknown>>;
|
|
73
|
+
/** Restore-hook audit from the claiming `agent_resumed` / `workflow_resumed` event (plan 094 Task 3); hook names and durations only, absent when the run never resumed or resumed without hooks. */
|
|
74
|
+
readonly restore?: CheckpointRestoreAudit;
|
|
73
75
|
readonly traceId?: string;
|
|
74
76
|
readonly status: string;
|
|
75
77
|
readonly stopReason?: AgentFinishReason;
|
|
@@ -88,6 +90,10 @@ interface ExecutionTimeline {
|
|
|
88
90
|
}
|
|
89
91
|
```
|
|
90
92
|
|
|
93
|
+
`restore` is the same `{ hooks: [{ hook, durationMs }], durationMs }` the claiming resume event published, in
|
|
94
|
+
run order — a metadata-only audit (hook names are host-chosen identifiers), so it projects under every
|
|
95
|
+
content policy and never carries prompts, tool arguments, or node payloads.
|
|
96
|
+
|
|
91
97
|
### `ExecutionStep`
|
|
92
98
|
|
|
93
99
|
```ts
|
|
@@ -139,7 +145,8 @@ interface TimelineExhaustion {
|
|
|
139
145
|
|
|
140
146
|
`turns` is the per-turn trace, derived in one pass over folded provider steps: turn number, status,
|
|
141
147
|
timing, attempts (retries included), input-token-weighted `cacheHitRate`, the last provider
|
|
142
|
-
attempt's recorded `budgets
|
|
148
|
+
attempt's recorded `budgets` (with its `inputTokensSource` provenance label), and its stop reason.
|
|
149
|
+
Cache rate is absent when cache usage is unknown;
|
|
143
150
|
`budgets` is copied verbatim from `provider_turn_finished.metadata.budgets` and is absent on legacy
|
|
144
151
|
events. `ExecutionTimeline.cacheHitRate` is the same input-token-weighted calculation across all
|
|
145
152
|
provider attempts. The stop reason also rides the `provider` step's metadata (`metadata.stopReason`),
|
|
@@ -155,7 +162,7 @@ first group and empty axes. Argument hashes only — `recentToolCalls` never con
|
|
|
155
162
|
|
|
156
163
|
`deterministic_turn` folds into a `"deterministic"` step whose `name` is the answering middleware id and whose metadata carries `{ turn, middleware }`. A deterministic turn has no provider step, no `usage`, and no `stopReason`, so a host-answered turn can never be read as model output; its `turns` entry carries `providerAttempts: 0`, and `summarizeTimeline()`/`summarizeSession()` split the turn count into `turns: { model, deterministic }`. The same provenance is copied onto the assistant message as `message.metadata.deterministic = { middleware }`, so the persisted transcript alone proves the turn had no model behind it.
|
|
157
164
|
|
|
158
|
-
`guardrail_decision` folds into a `"guardrail"` step whose `name` is the stage (`input`/`output`/`tool_input`/`tool_output`) and whose metadata carries `action`, the rule identity `metadata.guardrail` (compiled packs name it `pack:<pack>/<rule>`, other guardrails their configured name), and `toolName`/`toolCallId` when the decision is tool-scoped. A denying action (`deny`, `block`, `tripwire`) sets status `denied`; the free-text guardrail reason stays on the event, not the step.
|
|
165
|
+
`guardrail_decision` folds into a `"guardrail"` step whose `name` is the stage (`input`/`output`/`tool_input`/`tool_output`) and whose metadata carries `action`, the rule identity `metadata.guardrail` (compiled packs name it `pack:<pack>/<rule>`, other guardrails their configured name), and `toolName`/`toolCallId` when the decision is tool-scoped. A denying action (`deny`, `block`, `tripwire`) sets status `denied`; `interrupt` (a pack `ask` rule in a durable run) sets status `succeeded` and leaves the run `suspended` awaiting a decision, while the same rule in a run that cannot suspend reports `block` and sets `denied`; the free-text guardrail reason stays on the event, not the step.
|
|
159
166
|
|
|
160
167
|
Step statuses: `"running"`, `"succeeded"`, `"failed"`, `"blocked"`, `"skipped"`, `"suspended"`, `"denied"`, `"aborted"`.
|
|
161
168
|
|
package/docs/extensions.md
CHANGED
|
@@ -8,6 +8,7 @@ APIs:
|
|
|
8
8
|
|
|
9
9
|
- `createExtensionKernel()` / `ExtensionKernel`
|
|
10
10
|
- `createExtensionEventBus()` / `ExtensionEventBus`
|
|
11
|
+
- `forwardAgentEvents()` / `AgentEventBridgeOptions`
|
|
11
12
|
- `ExtensionAPI`, `ExtensionEvent`, and `extension_error` events
|
|
12
13
|
- Shared `MiddlewareRegistry` access and `api.use()` registration
|
|
13
14
|
|
|
@@ -42,7 +43,8 @@ createExtensionEventBus(options?: { errorPolicy?: "event" | "throw"; secrets?: r
|
|
|
42
43
|
- `kernel.events.on(type, handler)` registers ordered event handlers and returns an unsubscribe function.
|
|
43
44
|
- `kernel.events.emit(event)` calls matching handlers in registration order.
|
|
44
45
|
- `kernel.middleware.run(hook, value)` runs matching middleware in registration order.
|
|
45
|
-
- `
|
|
46
|
+
- `forwardAgentEvents(source, events, options?)` is host-invoked wiring for a live `AgentEvent` stream: it maps `agent_started` → `before_agent_start`, `turn_started`/`turn_finished` → `turn`, `tool_execution_started` → `tool_call`, and `tool_execution_finished` → `tool_result`, carrying the original event as read-only `payload`. Other events are ignored. Handlers run in event order and never in the run's path, so a slow or throwing listener cannot stall or fail the observed run; the returned function stops forwarding and releases the source iterator. Bridge failures go to `options.onError` (or become `extension_error` under the bus's own policy) — never to the run.
|
|
47
|
+
- `activateKernel(kernel)` copies the `createAgent()` array slots into one config: `{ tools, skills, instructionInjectors, context, stopHooks, commands, middleware }`. Contributions stay inert until the host passes them into runtime config; single-slot builders, `compaction`/`retry`, provider/model selection, and skill activation remain host-owned decisions.
|
|
46
48
|
- With default `errorPolicy: "event"`, setup/listener/middleware errors become `extension_error` events with redacted `ErrorInfo`.
|
|
47
49
|
- With `errorPolicy: "throw"`, setup/listener/middleware errors reject/throw.
|
|
48
50
|
|
|
@@ -58,7 +60,7 @@ createExtensionEventBus(options?: { errorPolicy?: "event" | "throw"; secrets?: r
|
|
|
58
60
|
## Implementation example
|
|
59
61
|
|
|
60
62
|
```ts
|
|
61
|
-
import { activateKernel, createAgent, createExtensionKernel, type Extension } from "@arnilo/prism";
|
|
63
|
+
import { activateKernel, createAgent, createExtensionKernel, forwardAgentEvents, type Extension } from "@arnilo/prism";
|
|
62
64
|
|
|
63
65
|
const extension: Extension = {
|
|
64
66
|
name: "demo-extension",
|
|
@@ -76,9 +78,11 @@ const extension: Extension = {
|
|
|
76
78
|
api.registerAgent({ name: "demo", create: () => createAgent({ model, provider }) });
|
|
77
79
|
api.registerCompactionStrategy({ name: "compact", compact: () => ({ summary: "summary" }) });
|
|
78
80
|
api.registerRetryPolicy({ name: "retry", decide: () => ({ retry: false }) });
|
|
79
|
-
api.
|
|
81
|
+
api.registerStopHook({ name: "checklist", decide: (ctx) => (ctx.stopHookActive ? { action: "stop" } : { action: "continue", reason: "Verify the checklist." }) });
|
|
82
|
+
api.on("demo:ready", (event) => {
|
|
80
83
|
console.log(event.type);
|
|
81
84
|
});
|
|
85
|
+
api.use("session_start", (payload) => payload);
|
|
82
86
|
api.use("provider_request", (request) => request);
|
|
83
87
|
api.use("compaction", (payload) => payload);
|
|
84
88
|
api.use("retry", (payload) => payload);
|
|
@@ -105,9 +109,16 @@ const agent = createAgent({
|
|
|
105
109
|
tools: activated.tools,
|
|
106
110
|
skills: activated.skills,
|
|
107
111
|
instructionInjectors: activated.instructionInjectors,
|
|
112
|
+
stopHooks: activated.stopHooks,
|
|
108
113
|
context: activated.context,
|
|
109
114
|
middleware: activated.middleware,
|
|
110
115
|
});
|
|
116
|
+
|
|
117
|
+
// Forward live AgentEvents onto the bus; stop() ends forwarding and releases the subscription.
|
|
118
|
+
const session = agent.createSession();
|
|
119
|
+
const stop = forwardAgentEvents(session.subscribe(), kernel.events, { onError: (error) => console.warn(error) });
|
|
120
|
+
// const run = await session.run("Hi");
|
|
121
|
+
// stop();
|
|
111
122
|
```
|
|
112
123
|
|
|
113
124
|
## Extension and configuration notes
|
|
@@ -120,6 +131,9 @@ const agent = createAgent({
|
|
|
120
131
|
- `api.registerInputBuilder()`, `api.registerPromptBuilder()`, and `api.registerContextProvider()` contribute inert builders/providers; they do not replace defaults or run until the host passes selected entries to Phase 5 helpers.
|
|
121
132
|
- `api.registerSkill()` contributes an inert `Skill` to `registries.skills`; it does not disclose instructions, activate referenced tools, or grant permissions until the host selects it.
|
|
122
133
|
- `api.registerInstructionInjector()` (Phase 30) contributes an inert `InstructionInjector` to `registries.instructionInjectors`; it grants no tools, skills, or permissions and is only applied when the host selects it via `AgentConfig.instructionInjectors`/`RunOptions.instructionInjectors`. See [Instruction injection](instruction-injection.md).
|
|
134
|
+
- `api.registerStopHook()` contributes an inert run-end `StopHook` to `registries.stopHooks`; `activateKernel()` copies it into `stopHooks` for `createAgent({ stopHooks })`, and `LoadedExtension.dispose()` unwinds it. Hooks decide at a natural loop end only — see [Hooks](hooks.md).
|
|
135
|
+
- `forwardAgentEvents()` is host-invoked wiring, not a runtime default, and it observes only: the bus never transforms what the run sees. Prefer `session.subscribe()` directly when the host wants the raw stream; use the bridge when extension packages already listen on the bus.
|
|
136
|
+
- Session lifecycle middleware (`session_start`/`session_shutdown`) is dispatched by the agent/session runtime when the host passes its registry to `AgentConfig.middleware` — see [Middleware hooks](middleware-hooks.md).
|
|
123
137
|
- `api.registerProviderPackage()`, `api.registerAuthMethod()`, `api.registerProviderRequestPolicy()`, and `api.registerSystemPromptContribution()` contribute inert provider-package data; they do not load packages, resolve credentials, mutate provider payloads, or change prompts until selected by a host/runtime helper that documents that behavior.
|
|
124
138
|
- `api.registerAgent()` contributes an inert `AgentDefinition`; its `create()` can call `createAgent()`, but the runtime is not started until host code resolves the definition and creates/runs a session.
|
|
125
139
|
- The kernel registers middleware only into the explicit registry returned by `createMiddlewareRegistry()` or provided by the host.
|
|
@@ -144,7 +158,10 @@ const agent = createAgent({
|
|
|
144
158
|
- [Contribution registries](contribution-registries.md): registry bundle populated by `ExtensionAPI`.
|
|
145
159
|
- [Contribution discovery (workspace)](contribution-discovery.md): filesystem-driven complement to extension registration — opt-in scan without `import()` or activation.
|
|
146
160
|
- [Tools](tools.md): host activation, filtering, and dispatch for contributed tool definitions.
|
|
161
|
+
- [Middleware hooks](middleware-hooks.md): hook names, payloads, and the dispatched `session_start`/`session_shutdown` call sites.
|
|
162
|
+
- [Agent events](agent-events.md): the `AgentEvent` union the bridge forwards.
|
|
147
163
|
- [Instruction injection](instruction-injection.md): package injectors that layer instructions and context blocks for `first_turn`/`every_turn`/`on_input` without granting tools.
|
|
164
|
+
- [Hooks](hooks.md): the hook model and event map, plus run-end stop hooks contributed through `ExtensionAPI.registerStopHook()`.
|
|
148
165
|
- [Input and prompt assembly](input-and-prompt-assembly.md): host selection for contributed input/prompt builders.
|
|
149
166
|
- [System prompts](system-prompts.md): host selection for contributed system prompt layers.
|
|
150
167
|
- [Context and skills](context-and-skills.md): host selection and tool checks for contributed context providers and skills.
|
|
@@ -152,9 +169,8 @@ const agent = createAgent({
|
|
|
152
169
|
- [Compaction and retry policies](compaction-and-retry.md): compaction strategy/retry policy contributions and `compaction`/`retry` middleware runtime behavior.
|
|
153
170
|
- [LLM compaction package](compaction-llm.md): optional extension helper that registers a provider-backed compaction strategy.
|
|
154
171
|
- [Observational memory compaction package](compaction-observational-memory.md): optional extension helper that registers an inert fast memory compaction strategy.
|
|
155
|
-
- [Caveman behavior integration](caveman.md): optional `@arnilo/prism-coding-tools/caveman` upstream Caveman skills, commands, level injector, and session `caveman-level` persistence.
|
|
156
|
-
- [Ponytail behavior integration](ponytail.md): optional `@arnilo/prism-coding-tools/ponytail` upstream Ponytail skills, commands, mode injector, and session `ponytail-mode` persistence.
|
|
157
172
|
- [Impeccable behavior integration](impeccable.md): optional `@arnilo/prism-coding-tools/impeccable` upstream Impeccable skill and `load_skill` command.
|
|
173
|
+
- [Contribution discovery](contribution-discovery.md): `loadSkillDirectory` loads a host-supplied `<dir>/<name>/SKILL.md` tree for host-owned personas (`examples/caveman-ponytail.ts`).
|
|
158
174
|
- [Public contracts](public-contracts.md): `Extension`, `ExtensionAPI`, and contribution contract types.
|
|
159
175
|
- [Credentials and redaction](credentials-and-redaction.md): secret-redaction behavior used for extension errors.
|
|
160
176
|
|
package/docs/guardrails.md
CHANGED
|
@@ -6,7 +6,7 @@ Guardrails are typed, fail-closed checks at input, completed provider output, to
|
|
|
6
6
|
|
|
7
7
|
## When to use it
|
|
8
8
|
|
|
9
|
-
Use guardrails to block unsafe prompts, model responses, tool arguments, or tool results before their next boundary. Use a redactor for known secrets. Do not treat guardrails as a sandbox, secret detector, permission policy, or validation replacement.
|
|
9
|
+
Use guardrails to block unsafe prompts, model responses, tool arguments, or tool results before their next boundary. Use a redactor for known secrets. Do not treat guardrails as a sandbox, secret detector, permission policy, or validation replacement. For how guardrails combine with middleware, injectors, and stop hooks — and where each Claude Code / Codex hook event lands — see [Hooks](hooks.md).
|
|
10
10
|
|
|
11
11
|
## Inputs / request
|
|
12
12
|
|
|
@@ -40,7 +40,7 @@ Action outcome by stage:
|
|
|
40
40
|
| --- | --- | --- | --- |
|
|
41
41
|
| `input` | run rejected (`GuardrailError`); steered message: dropped + `steer_rejected`, run continues | run rejected; steered message: dropped + `steer_rejected`, run continues | fresh durable run: suspends for approval; otherwise fails closed |
|
|
42
42
|
| `output` | run rejected | run rejected | fails closed (`ERR_PRISM_GUARDRAIL_INTERRUPT_UNAVAILABLE`) |
|
|
43
|
-
| `tool_input` | blocked `ToolResult`, run continues | run rejected | fails closed |
|
|
43
|
+
| `tool_input` | blocked `ToolResult`, run continues | run rejected | fails closed for a hand-written guardrail; a compiled pack `ask` rule gates the call instead (see [Guardrail packs § `ask`](#asking-for-approval-ask-rules)); approve-with-edits decisions are revalidated at decision time, so an edit into a pack-violating state is refused with `ERR_PRISM_DECISION_INVALID` before the decision is recorded |
|
|
44
44
|
| `tool_output` | blocked `ToolResult`, run continues | run rejected | fails closed |
|
|
45
45
|
|
|
46
46
|
The `GuardrailError` message names the stage so unsupported `interrupt` placements are diagnosable without reading core source.
|
|
@@ -74,7 +74,9 @@ await agent.createSession().run("Draft reply", { guardrails: { toolInput: [comma
|
|
|
74
74
|
|
|
75
75
|
## Guardrail packs
|
|
76
76
|
|
|
77
|
-
A pack is configuration, not code: rules compile once per session onto the existing `tool_input` / `tool_output` seams. Packs can only deny or
|
|
77
|
+
A pack is configuration, not code: rules compile once per session onto the existing `tool_input` / `tool_output` seams. Packs can only deny, tripwire, or ask for approval — they never grant permissions, widen arguments, or add a stage. A `deny` rule that matches produces the standard refusal-shaped `ToolResult`; `tripwire` additionally rejects the enclosing run.
|
|
78
|
+
|
|
79
|
+
A blocked call tells the model which rule refused it, so it stops retrying the same call: `ToolResult.error.message` is `Blocked by guardrail rule <identity>` — with `: <reason>` appended when the pack configured one, where `<identity>` is `pack:<pack>/<rule>` — bounded to 200 bytes and redacted like every other guardrail record. Tool arguments never appear in the text. A guardrail the host wrote by hand keeps the neutral `Tool call blocked by guardrail` / `Tool result blocked by guardrail` line: a pack identity is taken from the compiled rule's metadata, never synthesized from a name. Compiler-synthesized default reasons are omitted rather than echoed twice, and a reason that would push the line past the cap is truncated, so the identity always survives. The same text lands on the `tool_execution_blocked` event's `error.message` while the event's `reason` stays the machine code (`guardrail_blocked`).
|
|
78
80
|
|
|
79
81
|
```ts
|
|
80
82
|
const session = agent.createSession({
|
|
@@ -100,9 +102,17 @@ Built-in pack ids are public surface and versioned:
|
|
|
100
102
|
| `validation-respect` | `no-mutation-after-failed-validation` | Observes `options.validationTools` (default `test`, `run_tests`, `validate`, `validation`, `lint`, `typecheck`, `check`). A result carrying an error or a non-zero `exitCode` marks validation failed; the next successful validation clears it. Opt `shell` in explicitly when validations run through the shell tool. |
|
|
101
103
|
| `secrets-hygiene` | `no-secret-material-in-arguments` | Scans argument strings (bounded depth and count) for credential shapes: `sk-`, `gh[pousr]_`, `AKIA…`, PEM private-key headers, JWTs, `xox[baprs]-`. Prism redaction replaces exact known values only, so these patterns ship with the pack. |
|
|
102
104
|
|
|
103
|
-
Inline rule shape: exactly one of `pattern` (string or `RegExp`, compiled once) or `deny(args, context)` (typed predicate, host-trusted like all host code); optional `tool` (name or names; omitted matches every tool), `argPath` (dot path or paths such as `command` or `["from", "to"]`; omitted scans every argument string), `action` (`deny` default, or `
|
|
105
|
+
Inline rule shape: exactly one of `pattern` (string or `RegExp`, compiled once) or `deny(args, context)` (typed predicate, host-trusted like all host code); optional `tool` (name or names; omitted matches every tool), `argPath` (dot path or paths such as `command` or `["from", "to"]`; omitted scans every argument string), `action` (`deny` default, `tripwire`, or `ask`), and `reason`. Predicates receive `{ toolName, toolCallId, sessionId, runId, metadata, state }`, where `state` is pack-local and read-only.
|
|
106
|
+
|
|
107
|
+
### Asking for approval (`ask` rules)
|
|
108
|
+
|
|
109
|
+
`action: "ask"` gates exactly the calls the rule matches, without the all-tools `interruptBeforeTool` gate. It requires `pattern` (an opaque `deny` predicate cannot raise an approval — it stays available for silent denials) and behaves by run shape:
|
|
110
|
+
|
|
111
|
+
- **Durable run** (`runState` set): the call suspends before dispatch, in a `tool_approval` interruption whose reason names `pack:<pack>/<rule>` and whose pending decision also carries `guardrail` and `guardrailRule: { pack, rule }`. `allow_once` dispatches once; `allow_for_run` sticks to the same decision scope (tool, argument hash, effect kind, identity); `reject_once`/`reject_for_run` continue the run with a refusal-shaped `ToolResult` and never execute the tool. The gate costs one rule evaluation per tool call at charge time, and the decision reaches the timeline as a `guardrail` step with `action: "interrupt"`.
|
|
112
|
+
- **Non-durable run** (no `runState`): nothing can resume a suspension, so the rule is an ordinary `block` — the run continues, the call never executes, and `ToolResult.error.message` names the rule (`Blocked by guardrail rule pack:<pack>/<rule>` plus the pack's `reason` when it configured one, bounded to 200 bytes).
|
|
113
|
+
An approval never widens a pack: after `allow_*` the call still runs the ordinary `tool_input` stage, so a `deny` rule that matches the same call blocks it. An approve-with-edits decision is revalidated against the packs the resumed session restored — the `deny`/`tripwire` rules plus the `ask` rules compiled as blocks — before the decision is recorded, so an edited argument set that still trips a rule is refused at decision time with `ERR_PRISM_DECISION_INVALID` naming the rule (bounded and redacted, never echoing the arguments). The rules come from the session's checkpoint-restored packs and are passed in explicitly, never merged into `agent.config.guardrails`, so no other session of that agent inherits them.
|
|
104
114
|
|
|
105
|
-
Every evaluated rule emits a `guardrail_decision` event; the denying record's `guardrail` is `pack:<pack>/<rule>` and its `metadata` is `{ pack, rule, version }` — never tool arguments. `describeGuardrailPacks(refs)` returns the same identity rows (`pack:<pack>/<rule>`, stage, `pack@version`) that `snapshotRunBundle()` reports for the session config. Malformed config (unknown id, duplicate pack or rule id, both `pattern` and `deny`, invalid regex, `ask`) throws `GuardrailPackError` at session creation instead of silently dropping a rule.
|
|
115
|
+
Every evaluated rule emits a `guardrail_decision` event; the denying record's `guardrail` is `pack:<pack>/<rule>` and its `metadata` is `{ pack, rule, version }` — never tool arguments. `describeGuardrailPacks(refs)` returns the same identity rows (`pack:<pack>/<rule>`, stage, `pack@version`) that `snapshotRunBundle()` reports for the session config. Malformed config (unknown id, duplicate pack or rule id, both `pattern` and `deny`, invalid regex, `ask` with a `deny` predicate, an unknown `action` value) throws `GuardrailPackError` at session creation instead of silently dropping a rule. Under `persistSessionState` (plan 104 Task 2/3), the checkpoint also carries each pack's row — a registered pack by `{ id, version, options? }`, an inline pack by its `pattern` rules (a `deny` predicate or `RegExp` pattern cannot round-trip and refuses the save) — plus its own state-codec output (≤ 8 packs, ≤ 8 KiB per pack, ids ≤ 96 chars); a resume recompiles the rows and refuses with `AgentRunStateError` on an unknown id, a version mismatch against the installed pack, or malformed/oversized state — a suspension never silently downgrades enforcement. `session.guardrailPackRefs` exposes the refs a session (including a resumed one) actually enforces.
|
|
106
116
|
|
|
107
117
|
On the observability timeline each guardrail step carries that identity in `metadata.guardrail` (with `status: "denied"` when it denied — the free-text reason stays off the step to keep metadata low-cardinality), so evals can grade enforcement without reading tool arguments: `createGuardrailPackScorer()` from `@arnilo/prism-core/governance/evals` scores a denied `pack:` rule as a failed trajectory and names it. The built-in packs are covered by violating/compliant scenario pairs in `packages/prism-core/src/governance/evals/__tests__/guardrail-pack-scenarios.test.ts` (see [Evaluations](evaluations.md#guardrail-pack-trajectory-scenarios-plan-092)).
|
|
108
118
|
|
|
@@ -141,7 +151,7 @@ With `onViolation: "block"`, the standard `GuardrailError` has `reason: "claim_u
|
|
|
141
151
|
|
|
142
152
|
## Extension and configuration notes
|
|
143
153
|
|
|
144
|
-
Guardrails are callbacks supplied by the host. Prism does not discover, load, retry, or persist callback code. `createSecureAgent()` keeps configured guardrails and only appends run-level checks; it never lets a run remove secure defaults. Custom loops receive guarded `LoopContext.generate()` and `LoopContext.dispatchToolCall()`; host code that directly calls a provider or `ToolDefinition.execute()` is outside the runtime boundary. Guardrail packs follow the same rule: they are host-supplied config, compiled in memory per session, never discovered from disk
|
|
154
|
+
Guardrails are callbacks supplied by the host. Prism does not discover, load, retry, or persist callback code. `createSecureAgent()` keeps configured guardrails and only appends run-level checks; it never lets a run remove secure defaults. Custom loops receive guarded `LoopContext.generate()` and `LoopContext.dispatchToolCall()`; host code that directly calls a provider or `ToolDefinition.execute()` is outside the runtime boundary. Guardrail packs follow the same rule: they are host-supplied config, compiled in memory per session, never discovered from disk. Their compiled identity and pack-owned state persist only inside an opt-in durable checkpoint (`persistSessionState`, see above) and nowhere else.
|
|
145
155
|
|
|
146
156
|
## Security and performance notes
|
|
147
157
|
|