@arnilo/prism 0.7.0 → 0.9.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +73 -0
- package/README.md +12 -11
- package/dist/agent-approval.d.ts +15 -2
- package/dist/agent-approval.js +5 -1
- package/dist/agent-event-source.d.ts +9 -1
- package/dist/agent-event-source.js +10 -3
- package/dist/agent-loops.js +7 -4
- package/dist/agent-run-lifecycle.d.ts +15 -1
- package/dist/agent-run-lifecycle.js +91 -10
- package/dist/agent-run-state.d.ts +34 -2
- package/dist/agent-run-state.js +68 -6
- package/dist/agent-session/helpers.js +20 -1
- package/dist/agent-session/session/assemble.js +250 -27
- package/dist/agent-session/session/persist.d.ts +27 -0
- package/dist/agent-session/session/persist.js +94 -12
- package/dist/agent-session/session/provider-round.d.ts +14 -4
- package/dist/agent-session/session/provider-round.js +197 -25
- package/dist/agent-session/session/tool-round.js +24 -2
- package/dist/agent-session/session/types.d.ts +36 -2
- package/dist/agent-session/session.d.ts +40 -4
- package/dist/agent-session/session.js +78 -5
- package/dist/attention-compiler.d.ts +51 -2
- package/dist/attention-compiler.js +282 -21
- package/dist/cache-helpers.d.ts +4 -2
- package/dist/cache-helpers.js +8 -6
- package/dist/checkpoint-restore.d.ts +45 -0
- package/dist/checkpoint-restore.js +54 -0
- package/dist/checkpoints.js +7 -11
- package/dist/context-budget.d.ts +2 -1
- package/dist/context-budget.js +24 -2
- package/dist/contracts-core/agent.d.ts +30 -0
- package/dist/contracts-core/attention.d.ts +95 -0
- package/dist/contracts-core/content.d.ts +15 -0
- package/dist/contracts-core/guardrail-packs.d.ts +41 -0
- package/dist/contracts-core/guardrail-packs.js +2 -0
- package/dist/contracts-core/loop.d.ts +42 -0
- package/dist/contracts-core/provider.d.ts +25 -0
- package/dist/contracts-core/run-limits.d.ts +21 -0
- package/dist/contracts-core/session.d.ts +23 -5
- package/dist/contracts-core/session.js +21 -2
- package/dist/contracts-core/usage.d.ts +40 -0
- package/dist/contracts-core/usage.js +8 -0
- package/dist/contracts-core.d.ts +2 -0
- package/dist/contracts-core.js +2 -0
- package/dist/contracts-protocol.d.ts +90 -4
- package/dist/contracts-run-state.d.ts +82 -6
- package/dist/evidence-grounding.d.ts +29 -0
- package/dist/evidence-grounding.js +162 -0
- package/dist/guardrail-packs/coding-standard.d.ts +3 -0
- package/dist/guardrail-packs/coding-standard.js +63 -0
- package/dist/guardrail-packs/destructive-commands.d.ts +3 -0
- package/dist/guardrail-packs/destructive-commands.js +46 -0
- package/dist/guardrail-packs/errors.d.ts +7 -0
- package/dist/guardrail-packs/errors.js +9 -0
- package/dist/guardrail-packs/index.d.ts +4 -0
- package/dist/guardrail-packs/index.js +15 -0
- package/dist/guardrail-packs/secrets-hygiene.d.ts +3 -0
- package/dist/guardrail-packs/secrets-hygiene.js +23 -0
- package/dist/guardrail-packs/types.d.ts +16 -0
- package/dist/guardrail-packs/types.js +2 -0
- package/dist/guardrail-packs/validation-respect.d.ts +3 -0
- package/dist/guardrail-packs/validation-respect.js +53 -0
- package/dist/guardrails.d.ts +20 -1
- package/dist/guardrails.js +268 -0
- package/dist/host-composition.d.ts +13 -0
- package/dist/host-composition.js +33 -2
- package/dist/index.d.ts +19 -10
- package/dist/index.js +11 -6
- package/dist/input.d.ts +8 -1
- package/dist/input.js +68 -6
- package/dist/middleware.d.ts +37 -2
- package/dist/middleware.js +41 -0
- package/dist/node/session-store-jsonl.js +18 -3
- package/dist/observability.js +6 -0
- package/dist/provider-events.d.ts +11 -3
- package/dist/provider-events.js +62 -4
- package/dist/providers/openai-compatible.js +6 -3
- package/dist/providers/transport.d.ts +3 -1
- package/dist/providers/transport.js +36 -0
- package/dist/redaction.js +18 -2
- package/dist/run-bundle.d.ts +89 -0
- package/dist/run-bundle.js +150 -0
- package/dist/run-limits.d.ts +11 -1
- package/dist/run-limits.js +46 -0
- package/dist/session-stores.d.ts +12 -1
- package/dist/session-stores.js +21 -4
- package/dist/testing/agent-event-source-conformance.js +41 -2
- package/dist/testing/prefix-stability-conformance.d.ts +30 -0
- package/dist/testing/prefix-stability-conformance.js +104 -0
- package/dist/testing/session-store-conformance.d.ts +3 -2
- package/dist/testing/session-store-conformance.js +48 -0
- package/dist/testing/state-concurrency-conformance.js +5 -12
- package/dist/tools.d.ts +5 -0
- package/dist/tools.js +11 -3
- package/dist/usage-estimation.d.ts +29 -0
- package/dist/usage-estimation.js +79 -0
- package/docs/ag-ui.md +5 -0
- package/docs/agent-events.md +68 -1
- package/docs/agent-loops.md +33 -0
- package/docs/agent-session-runtime.md +5 -3
- package/docs/attention-compiler.md +89 -8
- package/docs/coding-agent-tools.md +1 -1
- package/docs/coding-security.md +1 -0
- package/docs/coding-tools.md +0 -1
- package/docs/compaction-and-retry.md +1 -1
- package/docs/compaction-observational-memory.md +34 -7
- package/docs/connected-apps.md +116 -0
- package/docs/context-and-skills.md +13 -0
- package/docs/core.md +1 -1
- package/docs/diagrams.md +6 -6
- package/docs/document-reader.md +9 -9
- package/docs/documents.md +32 -11
- package/docs/durable-runs.md +129 -0
- package/docs/embeddings.md +5 -0
- package/docs/enterprise-postgres-state.md +4 -0
- package/docs/evaluations.md +5 -0
- package/docs/execution-timeline.md +84 -1
- package/docs/guardrails.md +71 -2
- package/docs/history/079-messaging-primitive-review.md +391 -0
- package/docs/history/080-messaging-followon-primitive-review.md +234 -0
- package/docs/history/081-connected-apps-primitive-review.md +74 -0
- package/docs/history/083-prism-work-primitive-review.md +84 -0
- package/docs/history/084-primitive-review.md +96 -0
- package/docs/history/085-honesty-and-cut-primitive-review.md +91 -0
- package/docs/history/README.md +5 -0
- package/docs/history/release-handoffs.md +38 -0
- package/docs/host-compositions.md +8 -6
- package/docs/host-security.md +2 -2
- package/docs/index.md +66 -29
- package/docs/input-and-prompt-assembly.md +3 -3
- package/docs/knowledge-sync.md +4 -0
- package/docs/live-testing.md +5 -3
- package/docs/mcp-tools.md +1 -0
- package/docs/messaging-channel-operations.md +166 -0
- package/docs/messaging-channels.md +150 -0
- package/docs/middleware-hooks.md +38 -2
- package/docs/migrate-to-0.8.md +124 -0
- package/docs/migrate-to-0.9.md +210 -0
- package/docs/migration.md +43 -0
- package/docs/model-registry.md +12 -2
- package/docs/multi-agent-patterns.md +25 -2
- package/docs/node-jsonl-session-store.md +7 -1
- package/docs/observability.md +7 -3
- package/docs/openapi-tools.md +1 -1
- package/docs/operations.md +1 -3
- package/docs/options-index.md +36 -3
- package/docs/peer-dependencies.md +6 -6
- package/docs/policy-and-audit.md +13 -1
- package/docs/postgres-persistence.md +1 -1
- package/docs/prefix-stability-conformance.md +93 -0
- package/docs/provider-caching.md +4 -4
- package/docs/provider-conformance.md +16 -0
- package/docs/provider-layer.md +2 -2
- package/docs/provider-packages.md +20 -20
- package/docs/providers/neuralwatt.md +5 -1
- package/docs/public-contracts.md +2 -2
- package/docs/rag.md +102 -4
- package/docs/release-and-install.md +55 -47
- package/docs/run-bundle.md +92 -0
- package/docs/runs-and-usage.md +57 -6
- package/docs/scoped-agent-memory.md +262 -0
- package/docs/server.md +2 -0
- package/docs/session-store-conformance.md +1 -2
- package/docs/session-stores.md +17 -17
- package/docs/sheets.md +9 -9
- package/docs/signal-channel.md +112 -0
- package/docs/speech.md +5 -1
- package/docs/sqlite-persistence.md +1 -1
- package/docs/supervisors.md +32 -12
- package/docs/telegram-channel.md +157 -0
- package/docs/testing.md +2 -2
- package/docs/tools.md +17 -0
- package/docs/wiki.md +1 -1
- package/docs/work-artifacts-and-review.md +1 -1
- package/docs/work-connectors.md +9 -9
- package/docs/work-sandbox.md +115 -0
- package/docs/work-tools.md +38 -16
- package/docs/workflows.md +5 -0
- package/package.json +9 -3
- package/templates/business-worker/manifest.json +2 -1
- package/templates/business-worker/src/agent.ts.tmpl +1 -1
- package/templates/business-worker/src/tests/agent.test.ts.tmpl +1 -1
package/docs/documents.md
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
|
-
# Documents, spreadsheets, and presentations (`@arnilo/prism-
|
|
1
|
+
# Documents, spreadsheets, and presentations (`@arnilo/prism-work/documents`)
|
|
2
2
|
|
|
3
3
|
## What it does
|
|
4
4
|
|
|
5
|
-
The `@arnilo/prism-
|
|
5
|
+
The `@arnilo/prism-work/documents` package provides specification-compliant, AI-native OpenXML document generation, parsing, patching, and bounded preview rendering for Microsoft Word (`.docx`), Microsoft Excel (`.xlsx`), and Microsoft PowerPoint (`.pptx`) artifacts.
|
|
6
6
|
|
|
7
7
|
It operates on a canonical, typed abstract syntax tree (AST) called the **Prism Document Model** (`DocModel`, `SheetModel`, `DeckModel`):
|
|
8
8
|
- **Pure in-memory doctrine**: Functions accept `Uint8Array` container buffers or typed model objects and emit `Uint8Array` buffers or JSON models. Zero filesystem reads, zero network I/O, zero `process.env` lookups, and zero child process spawns.
|
|
@@ -15,13 +15,13 @@ It operates on a canonical, typed abstract syntax tree (AST) called the **Prism
|
|
|
15
15
|
|
|
16
16
|
## When to use it
|
|
17
17
|
|
|
18
|
-
Use `@arnilo/prism-
|
|
18
|
+
Use `@arnilo/prism-work/documents` whenever autonomous agents, coding assistants, workflow orchestrators, or enterprise applications need to:
|
|
19
19
|
1. Synthesize professional DOCX reports, financial XLSX spreadsheets, or PPTX presentation decks from structured LLM outputs.
|
|
20
20
|
2. Ingest existing OOXML artifacts into a structured, validated document model for analysis or automated summarization.
|
|
21
21
|
3. Perform atomic, validated updates or localized edits to documents using typed patch operations.
|
|
22
22
|
4. Render safe, bounded HTML previews or framework-neutral outline and grid snapshots in web and desktop hosts.
|
|
23
23
|
|
|
24
|
-
Do **not** use this package for collaborative real-time editing (OT/CRDT), macro execution,
|
|
24
|
+
Do **not** use this package for collaborative real-time editing (OT/CRDT), macro execution, in-memory spreadsheet formula evaluation (formulas are preserved verbatim as `{ formula, cachedValue? }` read-only pairs), or OLE binaries (`.doc` / `.xls` / `.ppt`). Recalculation, slide PNG QA, and legacy convert run out of process in the [work sandbox](work-sandbox.md) (`xlsx_recalc.py`, `soffice --convert-to pdf|docx|xlsx|pptx`, `pdftoppm`).
|
|
25
25
|
|
|
26
26
|
## Inputs / request
|
|
27
27
|
|
|
@@ -40,6 +40,24 @@ Do **not** use this package for collaborative real-time editing (OT/CRDT), macro
|
|
|
40
40
|
| `documentModelSchema` | `(kind: DocumentKind, slice?: string \| readonly string[]) => JsonSchema` | Retrieves the Draft-07 JSON Schema for a document kind, or a self-contained sliced sub-schema with resolved `$defs` (`docModelSchema` / `sheetModelSchema` / `deckModelSchema` expose the unsliced schemas). |
|
|
41
41
|
| `validateDocumentModel`| `(model: unknown) => asserts model is DocumentModel` | Validates arbitrary JSON objects against Draft-07 document schemas and structural invariants. |
|
|
42
42
|
|
|
43
|
+
### Agent tool facade
|
|
44
|
+
|
|
45
|
+
`@arnilo/prism-work/tools` exports `createOfficeTools({ caps?, redactor?, filesystem?, artifacts? })`: six thin wrappers over this package — `office_parse`, `office_import`, `office_generate`, `office_patch`, `office_diff`, and `office_preview`.
|
|
46
|
+
|
|
47
|
+
- Parse/import accept a document `kind` plus exactly one bounded `bytesBase64` or sandbox-relative `path`; results are marked `untrusted_external`.
|
|
48
|
+
- Generate returns only `{ contentHash, byteLength, format }`; set `outputPath` for a contained sandbox write or `artifact: true` for the host-provided `ArtifactBodyStore`. Raw OOXML never enters tool content.
|
|
49
|
+
- Patch is pure unless it also writes generated bytes. Parse/import/diff/preview declare no effect; generate/patch declare an external mutation only when a sandbox/artifact target is requested.
|
|
50
|
+
- Filesystem paths must be relative to the configured root and pass the host/sandbox realpath containment hook when supplied. OOXML is still PK-gated, cap-limited, redacted at parse, and never executes macros.
|
|
51
|
+
|
|
52
|
+
```ts
|
|
53
|
+
import { createOfficeTools } from "@arnilo/prism-work/tools";
|
|
54
|
+
|
|
55
|
+
const tools = createOfficeTools({
|
|
56
|
+
filesystem: { root: "/workspace", readFile, writeFile },
|
|
57
|
+
artifacts: { bodies, createRef }, // host-owned ArtifactBodyStore and ownership-aware ref factory
|
|
58
|
+
});
|
|
59
|
+
```
|
|
60
|
+
|
|
43
61
|
### Capacity Limits and Defaults
|
|
44
62
|
|
|
45
63
|
Caps are strictly enforced in memory before compute-intensive translation or parsing:
|
|
@@ -107,7 +125,7 @@ import {
|
|
|
107
125
|
renderPreviewBlocks,
|
|
108
126
|
renderPreviewHtml,
|
|
109
127
|
type DocModel,
|
|
110
|
-
} from "@arnilo/prism-
|
|
128
|
+
} from "@arnilo/prism-work/documents";
|
|
111
129
|
|
|
112
130
|
// 1. Define typed document model
|
|
113
131
|
const doc: DocModel = {
|
|
@@ -156,7 +174,7 @@ history.apply([{ op: "set", target: { title: true }, value: "Updated Review" }])
|
|
|
156
174
|
console.log(history.canUndo()); // true
|
|
157
175
|
const restored = history.undo(); // restored to "Executive Summary" state
|
|
158
176
|
|
|
159
|
-
import { diffDocument } from "@arnilo/prism-
|
|
177
|
+
import { diffDocument } from "@arnilo/prism-work/documents";
|
|
160
178
|
const diff = diffDocument(parsed, restored, { maxOps: 4096 });
|
|
161
179
|
|
|
162
180
|
// 6. Generate structured preview blocks & safe HTML
|
|
@@ -167,7 +185,7 @@ const htmlSnippet = renderPreviewHtml(restored, { maxHtmlBytes: 256 * 1024 });
|
|
|
167
185
|
## Extension and configuration notes
|
|
168
186
|
|
|
169
187
|
### Sub-package Pinning
|
|
170
|
-
To avoid pulling in CLI frameworks or extraneous dependencies, `@arnilo/prism-
|
|
188
|
+
To avoid pulling in CLI frameworks or extraneous dependencies, `@arnilo/prism-work/documents` directly pins the exact underlying modular packages:
|
|
171
189
|
- `@office-open/docx@0.12.3`
|
|
172
190
|
- `@office-open/xlsx@0.12.3`
|
|
173
191
|
- `@office-open/pptx@0.12.3`
|
|
@@ -203,20 +221,23 @@ const telemetry: DocumentsTelemetry = {
|
|
|
203
221
|
```
|
|
204
222
|
|
|
205
223
|
### Decimal Fidelity Ceiling
|
|
206
|
-
Financial worksheets often require exact decimal representations that JavaScript 64-bit binary floating-point numbers cannot represent without precision loss. `@arnilo/prism-
|
|
224
|
+
Financial worksheets often require exact decimal representations that JavaScript 64-bit binary floating-point numbers cannot represent without precision loss. `@arnilo/prism-work/documents` supports canonical string decimals (`{ type: "decimal", value: "1500000.00" }`). Generate emits that string; it does not coerce through `Number()`. Parse round-trips `{ type: "decimal", value: "1500000.00" }`.
|
|
225
|
+
|
|
226
|
+
`list` blocks generate as OOXML numbering (`w:numPr`), not a `• ` prefix in `w:t`. Charts, pivots, comments, and headers remain fidelity-reported drops.
|
|
207
227
|
|
|
208
228
|
## Security and performance notes
|
|
209
229
|
|
|
210
230
|
- **Pure In-Memory Operation**: No temporary files, no shell execution, no binary spawning, and zero network sockets.
|
|
211
|
-
- **ZIP Signature Gating**: Buffers must begin with PK zip container signatures (`0x50, 0x4B, 0x03, 0x04`). Extension-based type inference is strictly prohibited.
|
|
231
|
+
- **ZIP Signature Gating**: Buffers must begin with PK zip container signatures (`0x50, 0x4B, 0x03, 0x04`). Extension-based type inference is strictly prohibited. OLE compound files (`.doc` / `.xls` / `.ppt`, magic `D0 CF 11 E0`) fail this gate. Convert them in the [work sandbox](work-sandbox.md) then `office_parse`. Encrypted OOXML is still dropped on import.
|
|
212
232
|
- **Fail-Closed Caps**: Input size and element count caps are evaluated before entering XML translation passes, preventing zip-bomb and decompression amplification attacks.
|
|
213
233
|
- **Sanitize-by-Construction HTML**: `renderPreviewHtml` strictly entity-encodes all text fields, neutralizes dangerous protocols (`javascript:`, `http://`, `https://`), strips raw script/image tags, and caps output size to prevent DOM-based XSS and memory exhaustion.
|
|
214
234
|
- **Performance Budget**: Warm generation of 200-block documents completes in under 15 ms; parse and round-trip equality checks complete in under 100 ms.
|
|
215
235
|
|
|
216
236
|
## Related APIs
|
|
217
237
|
|
|
218
|
-
- [`@arnilo/prism-
|
|
219
|
-
- [`@arnilo/prism-
|
|
238
|
+
- [`@arnilo/prism-work/document-reader`](./document-reader.md): Bounded literal text extraction from PDF and DOCX documents for coding agent tools; optional host-selected Mistral OCR parser.
|
|
239
|
+
- [`@arnilo/prism-work/connectors`](./work-tools.md): Microsoft 365 and Google Workspace identity-scoped connectors.
|
|
220
240
|
- [`@arnilo/prism-coding-tools/agent`](./coding-agent-tools.md): Coding tools and file operations.
|
|
221
241
|
- [`@arnilo/prism-core/governance/observability`](./observability.md): OpenTelemetry instrumentation and trace adapters.
|
|
222
242
|
- [Work artifacts and review](work-artifacts-and-review.md): evidence-bound artifact citations and `evidenceDigest` approvals.
|
|
243
|
+
- [Work sandbox](work-sandbox.md): LibreOffice formula recalc, slide rasterization, and OLE convert; this package still does not spawn `soffice`.
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
# Durable runs
|
|
2
|
+
|
|
3
|
+
## What it does
|
|
4
|
+
|
|
5
|
+
Long investigations survive a host process that dies mid-run. With `checkpointPolicy: "every-turn"`, the durable store holds a running-state checkpoint at each provider-turn boundary — after the previous turn's tool results are in the session store, before the next provider request. A restarted worker resumes the *same* run with `decision: "continue"`: no tool is re-dispatched, and the session history is rebuilt from the checkpoint's session/leaf reference instead of being re-run from turn zero.
|
|
6
|
+
|
|
7
|
+
This is crash recovery for the in-run state, not an orchestrator. The host workflow engine (Temporal, a queue, a supervisor) still owns scheduling, retries, and completion; Prism owns only the run's turns, counters, loop-local state, and sticky attention frontier.
|
|
8
|
+
|
|
9
|
+
## When to use it
|
|
10
|
+
|
|
11
|
+
- A `Do`-style investigation can outlive its worker process (deploys, evictions, OOM kills, spot reclamation) and re-running the paid turns is unacceptable.
|
|
12
|
+
- The host wants a bounded, explicit recovery point rather than "restart the whole run".
|
|
13
|
+
- An external orchestrator needs to resume a single run without replaying its tools.
|
|
14
|
+
|
|
15
|
+
For approval suspension and batch decisions, 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
|
+
|
|
17
|
+
## Inputs / request
|
|
18
|
+
|
|
19
|
+
`AgentRunStateOptions` (per-run `RunOptions.runState` or `AgentConfig.runState`):
|
|
20
|
+
|
|
21
|
+
| Field | Meaning |
|
|
22
|
+
| --- | --- |
|
|
23
|
+
| `checkpointPolicy` | `"decision"` (default) persists only on suspension/terminal status. `"every-turn"` adds one running-state checkpoint per provider turn. |
|
|
24
|
+
| `checkpoints` | The host's `CheckpointStore`; the same store serves suspension, crash recovery, and status. |
|
|
25
|
+
| `definitionRevision` | Host-authored revision participating in the fingerprint; a change without a revision bump refuses resume. |
|
|
26
|
+
| `persistSessionState` | Also carries loaded-skill names and the attention sticky frontier into each turn checkpoint. |
|
|
27
|
+
| `includeSkillBodies` | Alongside `persistSessionState`, carries exact skill instructions. |
|
|
28
|
+
| `maxStateBytes` | Save-side byte ceiling (default 256 KB, hard 1 MB). Applies to every turn checkpoint identically. |
|
|
29
|
+
| `checkpointMetadata` | Sidecar map (`Record<string, string>`, ≤ 4 KB, redacted) written with every checkpoint record — never inside the state value, so it costs no `maxStateBytes` budget. A function is resolved at each write, so a host closure can pin state that moves mid-run (git commit, document version). |
|
|
30
|
+
|
|
31
|
+
```ts
|
|
32
|
+
let head = "commit-1";
|
|
33
|
+
await session.run("investigate", {
|
|
34
|
+
runState: {
|
|
35
|
+
checkpoints,
|
|
36
|
+
definitionRevision: "2026-09-19.1",
|
|
37
|
+
checkpointMetadata: () => ({ gitCommit: head, docVersion: "v12" }),
|
|
38
|
+
},
|
|
39
|
+
});
|
|
40
|
+
head = "commit-2"; // the next checkpoint records the new commit
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
`AgentRunLifecycle.status()` and `loadAgentRunState()` return the record's `metadata`; `resume` accepts `checkpointMetadata` to annotate the claim write, and without it the recorded map is preserved byte-for-byte across the claim and every later write. Legacy records without metadata read as `undefined` — an oversize or non-string map reads as absent rather than failing the resume.
|
|
44
|
+
|
|
45
|
+
### Restore hooks (all-or-nothing)
|
|
46
|
+
|
|
47
|
+
`resume` also accepts `restoreHooks`: host code that puts each external layer recorded in `checkpointMetadata` back where the checkpoint says it was. Hooks run sequentially before the claim write, each receiving the checkpoint context (`runId`, `version`, `status`, the redacted `metadata` map, and the raw `checkpoint` record) plus an `AbortSignal` that fires on host abort or the per-hook timeout.
|
|
48
|
+
|
|
49
|
+
```ts
|
|
50
|
+
await lifecycle.resume(ref, { decision: "approve", expectedVersion }, {
|
|
51
|
+
restoreHooks: [
|
|
52
|
+
async function restoreGit(cp) {
|
|
53
|
+
await git.reset(cp.metadata?.gitCommit);
|
|
54
|
+
},
|
|
55
|
+
async function restoreDocs(cp) {
|
|
56
|
+
await docs.restoreVersion(cp.metadata?.docVersion);
|
|
57
|
+
},
|
|
58
|
+
],
|
|
59
|
+
restoreHookTimeoutMs: 10_000, // default, per hook
|
|
60
|
+
});
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
All-or-nothing:
|
|
64
|
+
|
|
65
|
+
- The first hook that throws or overruns `restoreHookTimeoutMs` (default 10 s, `DEFAULT_CHECKPOINT_RESTORE_TIMEOUT_MS`) aborts the resume with `CheckpointRestoreError` — `code: "ERR_PRISM_CHECKPOINT_RESTORE"`, `hook` naming the layer, `cause` the original error. Later hooks do not run.
|
|
66
|
+
- The claim write and the conversation replay happen only after every hook succeeds, so a failed restore leaves the checkpoint byte-for-byte as it was — still resumable — instead of claiming a half-restored world. The server maps the failure to `409`/`ERR_PRISM_CHECKPOINT_RESTORE`.
|
|
67
|
+
- Hooks run on claiming resumes only; `deny` and resuspend paths never call them.
|
|
68
|
+
- The claim's `agent_resumed` event carries the audit: `restore: { hooks: [{ hook, durationMs }], durationMs }`.
|
|
69
|
+
- Register once on the lifecycle (`createAgentRunLifecycle({ restoreHooks })`) or per resume; lifecycle-registered hooks run first. No hooks ⇒ no call, no overhead, no `restore` field.
|
|
70
|
+
|
|
71
|
+
Resume uses `resumeAgentRun` / `resumeAgentRunStream` with `{ expectedVersion, decision: "continue" }`. The checkpoint records its own cadence, so a continued run keeps writing turn checkpoints without the host repeating `checkpointPolicy`.
|
|
72
|
+
|
|
73
|
+
## Outputs / response / events
|
|
74
|
+
|
|
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 plus sticky attention frontier. 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
|
+
|
|
77
|
+
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
|
+
|
|
79
|
+
- `expectedVersion` mismatch, ownership/fencing mismatch, revision or fingerprint mismatch (`Stale or non-running agent run resume`, `Agent revision or fingerprint mismatch on resume`).
|
|
80
|
+
- Status `suspended` — approvals, elicitations, and input guardrails still require `approve`/`deny` or a `RunDecision` batch; `continue` never bypasses a gate.
|
|
81
|
+
- Any interruption, pending decision, or ready-to-dispatch pending call recorded in the state.
|
|
82
|
+
|
|
83
|
+
The resumed run emits `agent_resumed` with the claimed version, reuses the recorded run counter snapshot, restores loop-local state, and dispatches nothing that was already persisted. Terminal saves then drop the pending markers as before.
|
|
84
|
+
|
|
85
|
+
**Ambiguity window.** A turn checkpoint is taken between turns, so a crash can lose at most the one provider turn that was in flight; that turn is re-requested on resume. A crash *inside* a tool's side effect is still the pre-existing ambiguous case — an already-marked `dispatched` call is never replayed automatically; resolve it manually or key host effects on `runId`/`toolCallId` idempotency. Counter caveat: the interrupted turn's `maxTurns` charge was already recorded at assembly, and the resumed turn charges once more, so a crash costs one extra turn against a finite `maxTurns` budget.
|
|
86
|
+
|
|
87
|
+
## Request/response example
|
|
88
|
+
|
|
89
|
+
```ts
|
|
90
|
+
import { createAgent, createMemoryCheckpointStore, providerDone, providerTextDelta, resumeAgentRun } from "@arnilo/prism";
|
|
91
|
+
|
|
92
|
+
const checkpoints = createMemoryCheckpointStore();
|
|
93
|
+
const agent = createAgent({
|
|
94
|
+
id: "investigation",
|
|
95
|
+
model: { provider: "mock", model: "demo" },
|
|
96
|
+
provider: { id: "mock", async *generate() { yield providerTextDelta("done"); yield providerDone(); } },
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
// Worker 1: long run, crash-recoverable between turns.
|
|
100
|
+
const session = agent.createSession({ id: "investigation-session" });
|
|
101
|
+
const first = await session
|
|
102
|
+
.run("Investigate", { runState: { checkpoints, definitionRevision: "2026-09-20.1", checkpointPolicy: "every-turn" } })
|
|
103
|
+
.catch(() => undefined); // worker died
|
|
104
|
+
|
|
105
|
+
// Worker 2: same stores, same run, continue from the last turn boundary.
|
|
106
|
+
const resumed = await resumeAgentRun(
|
|
107
|
+
agent,
|
|
108
|
+
{ runId, sessionId: session.id },
|
|
109
|
+
{ decision: "continue", expectedVersion: checkpointVersion },
|
|
110
|
+
{ checkpoints, definitionRevision: "2026-09-20.1" },
|
|
111
|
+
);
|
|
112
|
+
```
|
|
113
|
+
|
|
114
|
+
The complete network-free demo — one tool execution across the crash, resumed from the turn checkpoint — is [`examples/durable-investigation.ts`](../examples/durable-investigation.ts).
|
|
115
|
+
|
|
116
|
+
## Extension and configuration notes
|
|
117
|
+
|
|
118
|
+
- `checkpointPolicy: "decision"` is byte-identical to the pre-0.8 behavior: no turn writes, no extra events, no state fields. Turn checkpoints appear only when the option is set.
|
|
119
|
+
- The policy is recorded on the state (when non-default) and restored on resume, so hosts do not thread the option through `AgentRunResumeOptions`.
|
|
120
|
+
- Custom loops declared durable via `snapshot`/`restore` hooks keep their loop-local state across a crash resume exactly as they do across a suspension.
|
|
121
|
+
- Per-run `toolNames` grants, run counters, and wall deadlines all ride the turn checkpoint; the resumed run cannot widen any of them.
|
|
122
|
+
|
|
123
|
+
## Security and performance notes
|
|
124
|
+
|
|
125
|
+
- `"continue"` is a host-API action only. Prism's AG-UI interrupt resolution accepts `approve`/`deny` only, channel adapters resume with `deny`, and there is no server route that forwards an untrusted `continue`; adding one would create an approval-bypass path.
|
|
126
|
+
- Restore hooks are trusted host code running outside the sandbox: they see the checkpoint's (already redacted) sidecar map and are bounded only by their timeout. Because they run before the claim write, a timeout cannot leave a claimed checkpoint pointing at un-restored external state.
|
|
127
|
+
- Every gate that protects a suspension protects a continue resume: exact ownership, fencing token, fingerprint, revision, CAS version, and the absence of unresolved work. A running checkpoint is a recovery point, never an authorization.
|
|
128
|
+
- Cost is one bounded checkpoint write per provider turn (same redaction and `maxStateBytes` ceiling as suspension writes). A 40-turn investigation under `"every-turn"` therefore writes 40 checkpoint rows plus the terminal save, while the default `"decision"` policy writes at most one row per approval or suspension. Each row carries the run frontier, counters, run limits, and loop snapshot — not the message history, which stays in the session store and is pointed at by `leafId` — so the store grows with turns, not with turns × transcript; a state that would exceed `maxStateBytes` (default 256 KiB, `DEFAULT_MAX_AGENT_RUN_STATE_BYTES`) fails closed rather than truncating. Pick `"every-turn"` when a worker restart must cost at most one turn of thinking, and leave the default for runs with many cheap turns.
|
|
129
|
+
- 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
|
@@ -87,6 +87,11 @@ await runEmbeddingsConformance({
|
|
|
87
87
|
bridge structurally (`createAlibabaEmbedder` remains assignable to `Embedder`
|
|
88
88
|
without importing it). The contract is a superset: it adds usage and per-item
|
|
89
89
|
error mapping.
|
|
90
|
+
- The same host-seam posture covers local inference in retrieval: the RAG local
|
|
91
|
+
reranker (`resolveReranker({ kind: "local" })` / `createLocalReranker`) runs a
|
|
92
|
+
cross-encoder in the host process through a `LocalRerankRuntime`, so no
|
|
93
|
+
inference dependency name enters any manifest — see
|
|
94
|
+
[RAG local reranker](rag.md#local-reranker).
|
|
90
95
|
- Adapters never auto-chunk: a batch over the provider cap rejects with
|
|
91
96
|
`batch_too_large`, so `embedBatched`-style callers own batching and preserve
|
|
92
97
|
per-item error attribution.
|
|
@@ -200,6 +200,10 @@ export async function recordEnterpriseState(state: PostgresEnterpriseState) {
|
|
|
200
200
|
// `state` comes from `await createPostgresEnterpriseState({ pool, schema: "prism" })`.
|
|
201
201
|
```
|
|
202
202
|
|
|
203
|
+
## Composing with messaging channels
|
|
204
|
+
|
|
205
|
+
An outbox row is a natural fit for the `deliver` seam of `createMessagingRuntime` when the host wants each reply recorded alongside its own business state: append in the host transaction, commit, then hand the reply to the transport adapter. The channel runtime never imports this package and `@arnilo/prism-channels` has no `pg` peer — the composition is host code, because only the host knows which local mutation the row belongs with and which transport it owns. The appended payload should carry correlation ids (`connectionId`, the answered event id, reply kind) rather than message text, and the tenant id should come from the resolved identity's ownership scope rather than from the inbound event. `examples/messaging-outbox.ts` runs this seam offline against a fake pool.
|
|
206
|
+
|
|
203
207
|
## Extension and configuration notes
|
|
204
208
|
|
|
205
209
|
- `createModelRouter({ resolver, stateStore: state.modelRouter })` keeps allow-list, residency, fallback, and diagnostics behavior in `@arnilo/prism-core/governance/model-router`; this package only supplies durable state. Router admission reservations (`reserveBudget`/`commitBudget`/`releaseBudget` on `state.modelRouter`) live in the `reservations` JSONB column of `prism_model_router_budgets`: one atomic UPSERT per admission, fencing-token-guarded commit/release in a SERIALIZABLE transaction, and TTL reconciliation as unknown usage; see [Model routing](model-routing.md).
|
package/docs/evaluations.md
CHANGED
|
@@ -31,6 +31,7 @@ Use this package when a host needs offline quality checks or sampled live scorin
|
|
|
31
31
|
| `createSchemaScorer` | Validates final result or named step output against JSON schema |
|
|
32
32
|
| `createErrorClassScorer` | Fails closed if denied error codes or blocked executions appear on timeline |
|
|
33
33
|
| `createApprovalBeforeEffectScorer` | Verifies explicit approval occurred on timeline prior to sensitive tool effect |
|
|
34
|
+
| `createDeterministicTurnScorer` | Requires host-answered (no-model) turns with intact provenance: `minTurns` deterministic steps, optionally from one `middleware`, and no provider request inside those turns |
|
|
34
35
|
| `createCitationIntegrityScorer` | Invariant 0 on missing source, hash/span mismatch, or revoked ACL. Reads `environment.citations[]`. Ignores semantic `support`. |
|
|
35
36
|
| `runComparison` | immutable dataset, 2–8 named candidates by default, pairwise scorers |
|
|
36
37
|
| `assertEvaluationThreshold` / `serializeEvaluationReport` | mean/failure/per-scorer gates, hard invariant enforcement, and bounded redacted JSON |
|
|
@@ -267,6 +268,10 @@ The spawn pack (`@arnilo/prism-core/governance/evals` `spawn-pack.test.ts`) grad
|
|
|
267
268
|
|
|
268
269
|
Negative controls wire deliberately vulnerable host compositions — uncatalogued spawn, skipped reservation, model-supplied scope escalation, leaky child tool list, non-aborting cancel, ungated ship — and assert the matching grader reports `0` naming the violation.
|
|
269
270
|
|
|
271
|
+
## Guardrail-pack trajectory scenarios (plan 092)
|
|
272
|
+
|
|
273
|
+
`guardrail-pack-scenarios.test.ts` gives every built-in [guardrail pack](guardrails.md#guardrail-packs) a violating and a compliant trajectory, graded by `createGuardrailPackScorer()` on the projected timeline: a denying guardrail step scores `0` and names `metadata.guardrail` (`pack:<pack>/<rule>`), a compliant trajectory scores `1` with no pack denial, and a `forbidTools` call that executed fails as an enforcement escape instead of passing vacuously. Each scenario runs `runScenario({ agent, turns, sessionConfig: { guardrailPacks: [...] }, timeline: "metadata" })` against a scripted mock provider; a pack-absent control re-runs the destructive script with no packs to prove the blocked calls were blocked by the pack.
|
|
274
|
+
|
|
270
275
|
## PostgreSQL enterprise state (0.0.23)
|
|
271
276
|
|
|
272
277
|
`createPostgresEnterpriseState({ pool, schema }).evaluations` implements this package's existing `EvaluationStore`. The host creates an `EvaluationRecord` from verified ownership before append; every PostgreSQL query requires tenant scope, uses exact normalized account/user matching, and returns owner-bound opaque cursor pages. It is durable across reopen and supports the existing id/scorer/session/run/trace/dataset/item/experiment/status filters.
|
|
@@ -68,14 +68,21 @@ interface ExecutionTimeline {
|
|
|
68
68
|
readonly sessionId?: string;
|
|
69
69
|
readonly workflowId?: string;
|
|
70
70
|
readonly workflowRevision?: string;
|
|
71
|
+
/** Workflow checkpoint sidecar metadata (`WorkflowCheckpointValue.metadata`); present only when projected with a checkpoint. */
|
|
72
|
+
readonly workflowMetadata?: Readonly<Record<string, unknown>>;
|
|
71
73
|
readonly traceId?: string;
|
|
72
74
|
readonly status: string;
|
|
75
|
+
readonly stopReason?: AgentFinishReason;
|
|
76
|
+
readonly stopDetail?: string;
|
|
73
77
|
readonly startedAt: string;
|
|
74
78
|
readonly finishedAt?: string;
|
|
75
79
|
readonly input?: unknown;
|
|
76
80
|
readonly result?: unknown;
|
|
77
81
|
readonly usage?: Usage;
|
|
82
|
+
readonly cacheHitRate?: number;
|
|
78
83
|
readonly steps: readonly ExecutionStep[];
|
|
84
|
+
readonly turns?: readonly TimelineTurn[];
|
|
85
|
+
readonly exhaustion?: TimelineExhaustion;
|
|
79
86
|
readonly redacted: boolean;
|
|
80
87
|
readonly content: TimelineContentPolicy;
|
|
81
88
|
}
|
|
@@ -102,10 +109,54 @@ interface ExecutionStep {
|
|
|
102
109
|
}
|
|
103
110
|
```
|
|
104
111
|
|
|
105
|
-
Step kinds: `"run"`, `"turn"`, `"provider"`, `"tool"`, `"guardrail"`, `"delegation"`, `"compaction"`, `"attention"`, `"retry"`, `"hitl"`, `"artifact"`, `"workflow_node"`, `"loop_iteration"`, `"nested_workflow"`.
|
|
112
|
+
Step kinds: `"run"`, `"turn"`, `"deterministic"`, `"provider"`, `"tool"`, `"guardrail"`, `"delegation"`, `"compaction"`, `"attention"`, `"retry"`, `"hitl"`, `"artifact"`, `"workflow_node"`, `"loop_iteration"`, `"nested_workflow"`.
|
|
113
|
+
|
|
114
|
+
### `TimelineTurn` and `TimelineExhaustion`
|
|
115
|
+
|
|
116
|
+
```ts
|
|
117
|
+
interface TimelineTurn {
|
|
118
|
+
readonly turn: number;
|
|
119
|
+
readonly status: ExecutionStepStatus;
|
|
120
|
+
readonly startedAt: string;
|
|
121
|
+
readonly finishedAt?: string;
|
|
122
|
+
readonly durationMs?: number;
|
|
123
|
+
readonly providerAttempts: number;
|
|
124
|
+
readonly cacheHitRate?: number;
|
|
125
|
+
readonly budgets?: TurnBudgets;
|
|
126
|
+
readonly stopReason?: ProviderStopReason;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
interface TimelineExhaustion {
|
|
130
|
+
readonly limit: RunLimitName;
|
|
131
|
+
readonly maximum?: number;
|
|
132
|
+
readonly observed?: number;
|
|
133
|
+
readonly currency?: string;
|
|
134
|
+
readonly consumed?: BudgetConsumedCounters;
|
|
135
|
+
readonly closestOtherAxes: readonly BudgetAxisUsage[];
|
|
136
|
+
readonly recentToolCalls: readonly ToolCallSummary[];
|
|
137
|
+
}
|
|
138
|
+
```
|
|
139
|
+
|
|
140
|
+
`turns` is the per-turn trace, derived in one pass over folded provider steps: turn number, status,
|
|
141
|
+
timing, attempts (retries included), input-token-weighted `cacheHitRate`, the last provider
|
|
142
|
+
attempt's recorded `budgets`, and its stop reason. Cache rate is absent when cache usage is unknown;
|
|
143
|
+
`budgets` is copied verbatim from `provider_turn_finished.metadata.budgets` and is absent on legacy
|
|
144
|
+
events. `ExecutionTimeline.cacheHitRate` is the same input-token-weighted calculation across all
|
|
145
|
+
provider attempts. The stop reason also rides the `provider` step's metadata (`metadata.stopReason`),
|
|
146
|
+
so a flat renderer can badge attempts without walking `turns`. `turns` is absent on timelines with
|
|
147
|
+
no turn steps (workflow timelines).
|
|
148
|
+
|
|
149
|
+
`exhaustion` is the terminal limit attribution, present only when the run died on a run limit. It
|
|
150
|
+
joins `run_limit_exceeded` (`limit`, `maximum`, `observed`, `currency`) with `budget_exhausted`
|
|
151
|
+
(`consumed`, `closestOtherAxes`, `recentToolCalls`); a trace that recorded only the breach carries the
|
|
152
|
+
first group and empty axes. Argument hashes only — `recentToolCalls` never contains raw arguments.
|
|
106
153
|
|
|
107
154
|
`attention_compiled` folds into a one-step `"attention"` entry (status `succeeded`) whose metadata carries the measured counts (`used`, `usedAfter`, `inputCap`, `triggerRatio`, `droppedThinkingTurns`, `stubbedToolResults`, `stubbedBytes`, `truncated`); under-ratio turns emit no event, so they add no step.
|
|
108
155
|
|
|
156
|
+
`deterministic_turn` folds into a `"deterministic"` step whose `name` is the answering middleware id and whose metadata carries `{ turn, middleware }`. A deterministic turn has no provider step, no `usage`, and no `stopReason`, so a host-answered turn can never be read as model output; its `turns` entry carries `providerAttempts: 0`, and `summarizeTimeline()`/`summarizeSession()` split the turn count into `turns: { model, deterministic }`. The same provenance is copied onto the assistant message as `message.metadata.deterministic = { middleware }`, so the persisted transcript alone proves the turn had no model behind it.
|
|
157
|
+
|
|
158
|
+
`guardrail_decision` folds into a `"guardrail"` step whose `name` is the stage (`input`/`output`/`tool_input`/`tool_output`) and whose metadata carries `action`, the rule identity `metadata.guardrail` (compiled packs name it `pack:<pack>/<rule>`, other guardrails their configured name), and `toolName`/`toolCallId` when the decision is tool-scoped. A denying action (`deny`, `block`, `tripwire`) sets status `denied`; the free-text guardrail reason stays on the event, not the step.
|
|
159
|
+
|
|
109
160
|
Step statuses: `"running"`, `"succeeded"`, `"failed"`, `"blocked"`, `"skipped"`, `"suspended"`, `"denied"`, `"aborted"`.
|
|
110
161
|
|
|
111
162
|
Tree structure: steps are a flat ordered array. Tree via `parentId` (run → turn → provider/tool). Scorers iterate the flat array; UIs that need nesting walk `parentId`.
|
|
@@ -136,6 +187,7 @@ const timeline = projectTraceTimeline(trace, {
|
|
|
136
187
|
redactor: createSecretRedactor(secrets),
|
|
137
188
|
});
|
|
138
189
|
// timeline.steps.map(s => [s.order, s.kind, s.name, s.status])
|
|
190
|
+
// timeline.turns.map(t => [t.turn, t.cacheHitRate, t.budgets, t.stopReason])
|
|
139
191
|
```
|
|
140
192
|
|
|
141
193
|
### Workflow fold with checkpoint outputs
|
|
@@ -152,6 +204,37 @@ const timeline = projectWorkflowTimeline(workflowEvents, {
|
|
|
152
204
|
|
|
153
205
|
See runnable host demo in `examples/execution-timeline.ts` for offline workflow timeline projection, cockpit summary, and Mermaid diagram export.
|
|
154
206
|
|
|
207
|
+
### Stop reasons
|
|
208
|
+
|
|
209
|
+
Run-level `stopReason` mirrors `agent_finished.finishReason` when the loop stopped on a ceiling or a host turn policy (`"host_policy"`); `status` reads `finished:<stopReason>` for those runs and `succeeded` for a natural end. `stopDetail` carries the host's `turnPolicy.stop` reason, bounded to 256 bytes and redacted at the runtime boundary. See [Runs and usage ledger § Clean stops and stop reasons](runs-and-usage.md#clean-stops-and-stop-reasons).
|
|
210
|
+
|
|
211
|
+
Per-turn stop reasons are a separate, closed taxonomy (`ProviderStopReason`: `end_turn`, `tool_calls`,
|
|
212
|
+
`max_output_tokens`, `content_filter`, `abort`, `provider_error`, `unknown`) because they answer a
|
|
213
|
+
different question — why the *provider* returned, not why the loop ended. Each `provider_turn_finished`
|
|
214
|
+
badges its turn (`timeline.turns[i].stopReason`) and its provider step (`metadata.stopReason`). See
|
|
215
|
+
[Agent events](agent-events.md) § Provider turn events.
|
|
216
|
+
|
|
217
|
+
A run that died on a run limit packs its attribution into the timeline and the summary line:
|
|
218
|
+
|
|
219
|
+
```ts
|
|
220
|
+
import { projectTraceTimeline, summarizeTimeline } from "@arnilo/prism-core/governance/observability";
|
|
221
|
+
|
|
222
|
+
const timeline = projectTraceTimeline(trace);
|
|
223
|
+
// timeline.turns.map(t => [t.turn, t.stopReason]);
|
|
224
|
+
// [[1, "tool_calls"], [2, "end_turn"]]
|
|
225
|
+
// timeline.exhaustion;
|
|
226
|
+
// { limit: "maxTurns", maximum: 12, observed: 13,
|
|
227
|
+
// consumed: { turns: 13, inputTokens: 41_200, providerAttempts: 13, requestBytes: 1_048_576 },
|
|
228
|
+
// closestOtherAxes: [{ axis: "maxToolCalls", usedRatio: 0.625 }],
|
|
229
|
+
// recentToolCalls: [{ id: "tc_91", name: "searchCodebase", argHash: "sha256:9f.." }] }
|
|
230
|
+
|
|
231
|
+
summarizeTimeline(timeline).exhaustion;
|
|
232
|
+
// "maxTurns exhausted (13/12); closest: maxToolCalls 0.625"
|
|
233
|
+
```
|
|
234
|
+
|
|
235
|
+
`summarizeTimeline().exhaustion` is one renderable dashboard line; runs that ended any other way omit
|
|
236
|
+
it, and `summarizeSession()` keeps each run's line in its `runs` array.
|
|
237
|
+
|
|
155
238
|
## Bounds
|
|
156
239
|
|
|
157
240
|
| Dimension | Default | Hard cap |
|
package/docs/guardrails.md
CHANGED
|
@@ -26,6 +26,8 @@ const guardrails: Guardrails = { input: [pii], maxConcurrency: 1 };
|
|
|
26
26
|
|
|
27
27
|
Set `AgentConfig.guardrails` for every session run or `RunOptions.guardrails` to append checks for one run. `DispatchToolCallOptions.guardrails`, workflow `RunWorkflowOptions.guardrails`, and MCP server `CreatePrismMcpServerOptions.guardrails` apply tool stages to direct calls. A stage has `Guardrail<"input" | "output" | "tool_input" | "tool_output">`, a name, optional revision, and `evaluate(context)` result.
|
|
28
28
|
|
|
29
|
+
`AgentSessionConfig.guardrailPacks` compiles declarative, restrictive-only rule sets onto the tool stages once per session (see [Guardrail packs](#guardrail-packs)). Session packs merge after `AgentConfig.guardrails` and before `RunOptions.guardrails`. Hosts that dispatch tools directly can compile the same config with `compileGuardrailPacks(refs)` and pass the result as `DispatchToolCallOptions.guardrails`.
|
|
30
|
+
|
|
29
31
|
Decisions are `allow`, `block`, `tripwire`, or `interrupt`. Evaluation defaults to declaration-order sequential. `maxConcurrency` may be 1–16; records are emitted in declaration order. Thrown or malformed decisions become a fail-closed tripwire. A throwing guardrail produces a `guardrail_failed` record whose `metadata.error` carries the underlying error message — redacted and bounded to 4 KiB — so failures stay diagnosable without leaking internals. Decision reasons are capped at 4 KiB and metadata at 16 KiB after JSON normalization and optional redaction.
|
|
30
32
|
|
|
31
33
|
## Outputs / response / events
|
|
@@ -70,15 +72,82 @@ const agent = createAgent({ model, provider, guardrails: { input: [pii], output:
|
|
|
70
72
|
await agent.createSession().run("Draft reply", { guardrails: { toolInput: [commandGuard] } });
|
|
71
73
|
```
|
|
72
74
|
|
|
75
|
+
## Guardrail packs
|
|
76
|
+
|
|
77
|
+
A pack is configuration, not code: rules compile once per session onto the existing `tool_input` / `tool_output` seams. Packs can only deny or tripwire — they never grant permissions, widen arguments, or add a stage. A rule that matches produces the standard refusal-shaped `ToolResult`; `tripwire` additionally rejects the enclosing run.
|
|
78
|
+
|
|
79
|
+
```ts
|
|
80
|
+
const session = agent.createSession({
|
|
81
|
+
guardrailPacks: ["secrets-hygiene"],
|
|
82
|
+
// or, with options / inline rules:
|
|
83
|
+
guardrailPacks: [
|
|
84
|
+
{ id: "coding-standard", options: { cwd: "/repo", roots: ["/repo"] } },
|
|
85
|
+
{
|
|
86
|
+
id: "my-pack",
|
|
87
|
+
version: 1,
|
|
88
|
+
rules: [{ id: "no-etc", tool: "write", pattern: "^/etc/", reason: "system path" }],
|
|
89
|
+
},
|
|
90
|
+
],
|
|
91
|
+
});
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
Built-in pack ids are public surface and versioned:
|
|
95
|
+
|
|
96
|
+
| Pack | Rules | Notes |
|
|
97
|
+
| --- | --- | --- |
|
|
98
|
+
| `coding-standard` | `no-unrelated-file-edits`, `no-test-rewrites` | Applies to `write`/`edit`/`delete`/`move`. `options.roots` defaults to `[process.cwd()]`; `options.cwd` is the resolution base. Containment is lexical — symlinks are not resolved, so an `ExecutionPolicy` remains the hard boundary. |
|
|
99
|
+
| `destructive-commands` | `no-recursive-force-delete`, `no-long-flag-force-delete`, `no-force-push` (includes `--force-with-lease`), `no-destructive-sql`, `no-device-overwrite` | Matched against the `shell` tool's `command` argument. |
|
|
100
|
+
| `validation-respect` | `no-mutation-after-failed-validation` | Observes `options.validationTools` (default `test`, `run_tests`, `validate`, `validation`, `lint`, `typecheck`, `check`). A result carrying an error or a non-zero `exitCode` marks validation failed; the next successful validation clears it. Opt `shell` in explicitly when validations run through the shell tool. |
|
|
101
|
+
| `secrets-hygiene` | `no-secret-material-in-arguments` | Scans argument strings (bounded depth and count) for credential shapes: `sk-`, `gh[pousr]_`, `AKIA…`, PEM private-key headers, JWTs, `xox[baprs]-`. Prism redaction replaces exact known values only, so these patterns ship with the pack. |
|
|
102
|
+
|
|
103
|
+
Inline rule shape: exactly one of `pattern` (string or `RegExp`, compiled once) or `deny(args, context)` (typed predicate, host-trusted like all host code); optional `tool` (name or names; omitted matches every tool), `argPath` (dot path or paths such as `command` or `["from", "to"]`; omitted scans every argument string), `action` (`deny` default, or `tripwire`), and `reason`. Predicates receive `{ toolName, toolCallId, sessionId, runId, metadata, state }`, where `state` is pack-local and read-only. `action: "ask"` is rejected: the tool stage has no deterministic approval seam.
|
|
104
|
+
|
|
105
|
+
Every evaluated rule emits a `guardrail_decision` event; the denying record's `guardrail` is `pack:<pack>/<rule>` and its `metadata` is `{ pack, rule, version }` — never tool arguments. `describeGuardrailPacks(refs)` returns the same identity rows (`pack:<pack>/<rule>`, stage, `pack@version`) that `snapshotRunBundle()` reports for the session config. Malformed config (unknown id, duplicate pack or rule id, both `pattern` and `deny`, invalid regex, `ask`) throws `GuardrailPackError` at session creation instead of silently dropping a rule.
|
|
106
|
+
|
|
107
|
+
On the observability timeline each guardrail step carries that identity in `metadata.guardrail` (with `status: "denied"` when it denied — the free-text reason stays off the step to keep metadata low-cardinality), so evals can grade enforcement without reading tool arguments: `createGuardrailPackScorer()` from `@arnilo/prism-core/governance/evals` scores a denied `pack:` rule as a failed trajectory and names it. The built-in packs are covered by violating/compliant scenario pairs in `packages/prism-core/src/governance/evals/__tests__/guardrail-pack-scenarios.test.ts` (see [Evaluations](evaluations.md#guardrail-pack-trajectory-scenarios-plan-092)).
|
|
108
|
+
|
|
109
|
+
## Claim grounding
|
|
110
|
+
|
|
111
|
+
`createClaimGroundingGuardrail(options: ClaimGroundingGuardrailOptions)` is a deterministic output guardrail for quantitative claims. It scans assistant text once, then attributes each number to a completed host tool result from **this run** or to a host-governed figure. It never calls a model, store, or network service.
|
|
112
|
+
|
|
113
|
+
```ts
|
|
114
|
+
import { createClaimGroundingGuardrail } from "@arnilo/prism";
|
|
115
|
+
|
|
116
|
+
const grounding = createClaimGroundingGuardrail({
|
|
117
|
+
requireEvidenceForNumbers: true,
|
|
118
|
+
evidenceSources: "tool_results", // default
|
|
119
|
+
onViolation: "block", // default; "flag" records but permits output
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
const agent = createAgent({ model, provider, guardrails: { output: [grounding] } });
|
|
123
|
+
```
|
|
124
|
+
|
|
125
|
+
Numbers in an assistant text block pass when their exact numeric value occurs in a same-run successful tool result. The default is strict: `4,320.50` matches `4320.5`; `~4.3k` does not. Set `tolerance: "rounded"` to accept half the final printed unit, so `~4.3k` can match `4320.5`.
|
|
126
|
+
|
|
127
|
+
A host can supply governed figures without giving this package a storage dependency:
|
|
128
|
+
|
|
129
|
+
```ts
|
|
130
|
+
const grounding = createClaimGroundingGuardrail({
|
|
131
|
+
requireEvidenceForNumbers: true,
|
|
132
|
+
evidenceSources: ({ metadata }) =>
|
|
133
|
+
metadata.metric === "revenue-q2" ? [{ value: 4320.5, ref: "metric:revenue-q2" }] : [],
|
|
134
|
+
});
|
|
135
|
+
// `Revenue is 999 [evidence:metric:revenue-q2]` cites that governed source.
|
|
136
|
+
```
|
|
137
|
+
|
|
138
|
+
An explicit citation is `[evidence:<ref>]`, immediately after its claim (within 96 characters). Tool-result refs are `tool:<toolCallId>`; extractor refs may contain only letters, digits, `.`, `_`, `:`, and `-` (1–128 chars). A citation must name an evidence ref the guardrail received; arbitrary labels do not pass.
|
|
139
|
+
|
|
140
|
+
With `onViolation: "block"`, the standard `GuardrailError` has `reason: "claim_ungrounded"` and bounded metadata `{ claim, contentIndex, start, end }` — never a full response body. `"flag"` returns `action: "allow"` plus that same metadata and `violation: true`, so the response stays visible while the normal `guardrail_decision` event and run ledger preserve the flag. Strict mode treats every standalone number (including dates, percentages, and versions) as a claim; use the option only where that law is wanted.
|
|
141
|
+
|
|
73
142
|
## Extension and configuration notes
|
|
74
143
|
|
|
75
|
-
Guardrails are callbacks supplied by the host. Prism does not discover, load, retry, or persist callback code. `createSecureAgent()` keeps configured guardrails and only appends run-level checks; it never lets a run remove secure defaults. Custom loops receive guarded `LoopContext.generate()` and `LoopContext.dispatchToolCall()`; host code that directly calls a provider or `ToolDefinition.execute()` is outside the runtime boundary.
|
|
144
|
+
Guardrails are callbacks supplied by the host. Prism does not discover, load, retry, or persist callback code. `createSecureAgent()` keeps configured guardrails and only appends run-level checks; it never lets a run remove secure defaults. Custom loops receive guarded `LoopContext.generate()` and `LoopContext.dispatchToolCall()`; host code that directly calls a provider or `ToolDefinition.execute()` is outside the runtime boundary. Guardrail packs follow the same rule: they are host-supplied config, compiled in memory per session, never discovered from disk or persisted by Prism.
|
|
76
145
|
|
|
77
146
|
## Security and performance notes
|
|
78
147
|
|
|
79
148
|
Optional `@arnilo/prism-core/governance/policy` can record guardrail outcomes via `recordGuardrailDecision` (evidence refs only; see [Policy and audit](policy-and-audit.md)).
|
|
80
149
|
|
|
81
|
-
Output buffering prevents blocked provider content from reaching subscribers, session entries, ledgers, parsers, delegation, or tools. Tool-output checks receive raw results but Prism discards blocked raw output before event, ledger, transcript, or MCP exposure. Redaction replaces exact known values only; it is not general secret detection. Parallel checks receive an abort signal, but callback code must honor it to stop in-flight work. Browser snapshots and page text from the `browser` subpath are untrusted external content: never allow them to modify tools, permissions, credentials, or policy. Browser mutations still require host `ExecutionPolicy`/approval; prompt-injection text in a page cannot grant upload/download release.
|
|
150
|
+
Output buffering prevents blocked provider content from reaching subscribers, session entries, ledgers, parsers, delegation, or tools. Tool-output checks receive raw results but Prism discards blocked raw output before event, ledger, transcript, or MCP exposure. Redaction replaces exact known values only; it is not general secret detection. Guardrail-pack patterns compile once at session creation and argument scans are bounded (depth 8, 64 strings, 16 KiB per string), so rule cost stays off the provider path. Parallel checks receive an abort signal, but callback code must honor it to stop in-flight work. Browser snapshots and page text from the `browser` subpath are untrusted external content: never allow them to modify tools, permissions, credentials, or policy. Browser mutations still require host `ExecutionPolicy`/approval; prompt-injection text in a page cannot grant upload/download release.
|
|
82
151
|
|
|
83
152
|
## Related APIs
|
|
84
153
|
|