@arnilo/prism 0.6.0 → 0.7.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (129) hide show
  1. package/CHANGELOG.md +44 -5
  2. package/README.md +10 -10
  3. package/dist/agent-definitions.js +1 -0
  4. package/dist/agent-run-lifecycle.js +11 -0
  5. package/dist/agent-run-state.d.ts +6 -0
  6. package/dist/agent-run-state.js +29 -9
  7. package/dist/agent-session/session/assemble.js +33 -2
  8. package/dist/agent-session/session/persist.js +6 -2
  9. package/dist/agent-session/session/tool-round.js +1 -0
  10. package/dist/agent-session/session/types.d.ts +10 -0
  11. package/dist/agent-session/session.d.ts +14 -0
  12. package/dist/agent-session/session.js +40 -3
  13. package/dist/artifacts.d.ts +39 -1
  14. package/dist/artifacts.js +73 -0
  15. package/dist/attention-compiler.d.ts +121 -0
  16. package/dist/attention-compiler.js +479 -0
  17. package/dist/cli-init.js +20 -6
  18. package/dist/context-budget.d.ts +20 -1
  19. package/dist/context-budget.js +10 -1
  20. package/dist/contracts-core/agent.d.ts +7 -0
  21. package/dist/contracts-core/attention.d.ts +66 -0
  22. package/dist/contracts-core/attention.js +2 -0
  23. package/dist/contracts-core/compaction.d.ts +59 -0
  24. package/dist/contracts-core/compaction.js +77 -1
  25. package/dist/contracts-core/provider.d.ts +4 -0
  26. package/dist/contracts-core.d.ts +1 -0
  27. package/dist/contracts-core.js +1 -0
  28. package/dist/contracts-protocol.d.ts +29 -0
  29. package/dist/contracts-run-state.d.ts +6 -0
  30. package/dist/host-composition.d.ts +78 -0
  31. package/dist/host-composition.js +248 -0
  32. package/dist/index.d.ts +9 -6
  33. package/dist/index.js +5 -4
  34. package/dist/input.d.ts +13 -1
  35. package/dist/input.js +40 -1
  36. package/dist/secure-agent.d.ts +2 -0
  37. package/dist/secure-agent.js +6 -1
  38. package/dist/tool-result-fold.d.ts +12 -0
  39. package/dist/tool-result-fold.js +13 -6
  40. package/dist/tools.d.ts +10 -0
  41. package/dist/tools.js +41 -0
  42. package/docs/acp-agent.md +42 -11
  43. package/docs/acp.md +2 -1
  44. package/docs/ag-ui.md +5 -3
  45. package/docs/agent-definitions.md +9 -1
  46. package/docs/agent-events.md +4 -1
  47. package/docs/agent-session-runtime.md +6 -6
  48. package/docs/attention-compiler.md +272 -0
  49. package/docs/cli-rpc.md +4 -2
  50. package/docs/coding-agent-tools.md +1 -1
  51. package/docs/coding-security.md +5 -3
  52. package/docs/coding-tools.md +1 -1
  53. package/docs/coding-workspaces.md +22 -0
  54. package/docs/compaction-and-retry.md +36 -4
  55. package/docs/compaction-observational-memory.md +62 -9
  56. package/docs/context-and-skills.md +4 -2
  57. package/docs/conversations.md +1 -1
  58. package/docs/dev-inspector.md +4 -0
  59. package/docs/device-adapters.md +1 -0
  60. package/docs/document-reader.md +11 -3
  61. package/docs/documents.md +10 -2
  62. package/docs/enterprise-postgres-state.md +2 -2
  63. package/docs/evaluations.md +168 -4
  64. package/docs/execution-timeline.md +180 -0
  65. package/docs/history/0.7.0-primitive-review.md +254 -0
  66. package/docs/history/migration-0.0.md +2 -2
  67. package/docs/history/release-handoffs.md +37 -1
  68. package/docs/host-compositions.md +147 -0
  69. package/docs/hosted-sandboxes.md +94 -0
  70. package/docs/index.md +58 -39
  71. package/docs/input-and-prompt-assembly.md +1 -0
  72. package/docs/knowledge-sync.md +84 -0
  73. package/docs/language-intelligence.md +1 -1
  74. package/docs/live-testing.md +4 -1
  75. package/docs/mcp-tools.md +2 -1
  76. package/docs/memory-fabric.md +416 -0
  77. package/docs/migrate-to-0.5.md +1 -1
  78. package/docs/migrate-to-0.6.md +1 -0
  79. package/docs/migrate-to-0.7.md +345 -0
  80. package/docs/migration.md +13 -1
  81. package/docs/model-routing.md +79 -4
  82. package/docs/multi-agent-patterns.md +20 -6
  83. package/docs/observability.md +52 -1
  84. package/docs/operations.md +13 -1
  85. package/docs/options-index.md +13 -1
  86. package/docs/peer-dependencies.md +6 -4
  87. package/docs/process-sessions.md +3 -1
  88. package/docs/prompt-registry.md +1 -1
  89. package/docs/provider-caching.md +4 -2
  90. package/docs/provider-conformance.md +1 -1
  91. package/docs/provider-packages.md +22 -22
  92. package/docs/providers/bedrock.md +71 -7
  93. package/docs/providers/openai.md +1 -1
  94. package/docs/rag.md +24 -8
  95. package/docs/realtime-voice.md +87 -0
  96. package/docs/release-and-install.md +36 -34
  97. package/docs/runs-and-usage.md +3 -2
  98. package/docs/server.md +5 -3
  99. package/docs/speech.md +2 -0
  100. package/docs/supervisors.md +33 -5
  101. package/docs/testing.md +1 -1
  102. package/docs/thinking-and-reasoning.md +3 -1
  103. package/docs/tools.md +6 -5
  104. package/docs/web-tools.md +2 -1
  105. package/docs/work-artifacts-and-review.md +14 -4
  106. package/docs/work-connectors.md +3 -1
  107. package/docs/work-tools.md +14 -4
  108. package/docs/workflows.md +69 -1
  109. package/docs/working-and-semantic-memory.md +25 -14
  110. package/package.json +1 -1
  111. package/templates/README.md +2 -0
  112. package/templates/business-worker/README.md.tmpl +19 -0
  113. package/templates/business-worker/env.example.tmpl +1 -0
  114. package/templates/business-worker/gitignore.tmpl +11 -0
  115. package/templates/business-worker/manifest.json +11 -0
  116. package/templates/business-worker/package.json.tmpl +23 -0
  117. package/templates/business-worker/src/agent.ts.tmpl +92 -0
  118. package/templates/business-worker/src/index.ts.tmpl +13 -0
  119. package/templates/business-worker/src/tests/agent.test.ts.tmpl +77 -0
  120. package/templates/business-worker/tsconfig.json.tmpl +15 -0
  121. package/templates/personal-assistant/README.md.tmpl +18 -0
  122. package/templates/personal-assistant/env.example.tmpl +1 -0
  123. package/templates/personal-assistant/gitignore.tmpl +11 -0
  124. package/templates/personal-assistant/manifest.json +11 -0
  125. package/templates/personal-assistant/package.json.tmpl +23 -0
  126. package/templates/personal-assistant/src/agent.ts.tmpl +65 -0
  127. package/templates/personal-assistant/src/index.ts.tmpl +13 -0
  128. package/templates/personal-assistant/src/tests/agent.test.ts.tmpl +28 -0
  129. package/templates/personal-assistant/tsconfig.json.tmpl +15 -0
package/docs/cli-rpc.md CHANGED
@@ -40,7 +40,7 @@ prism init <dir> [--template <name>] [--list-templates] [--provider <name>] [--w
40
40
  | Flag / arg | Purpose |
41
41
  | --- | --- |
42
42
  | `<dir>` | Destination directory (created if missing). Required unless `--list-templates` is specified. |
43
- | `--template <name>` | Template starter name (`init` [default], `deep-research`). |
43
+ | `--template <name>` | Template starter name (`init` [default], `deep-research`, `personal-assistant`, `business-worker`). |
44
44
  | `--list-templates` | List available starter templates from the templates gallery. |
45
45
  | `--provider <name>` | `mock` (default), `openai`, `openrouter`, `kimi`, `zai`, `opencode-go`, or `neuralwatt`. |
46
46
  | `--with-workflows` | Add `@arnilo/prism-core/runtime/workflows` and `src/workflows-example.ts`. |
@@ -48,7 +48,7 @@ prism init <dir> [--template <name>] [--list-templates] [--provider <name>] [--w
48
48
  | `--force` | Overwrite generated files when the destination already exists. |
49
49
  | `-h`, `--help` | Print init usage. |
50
50
 
51
- Default generation (`init` template) installs only `@arnilo/prism` (mock provider). Selecting a real provider adds exactly one dependency: the `@arnilo/prism-providers` family package (the selected adapter imports from `@arnilo/prism-providers/<id>`). Specifying `--template deep-research` scaffolds a flagship deep research agent pipeline (`@arnilo/prism`, `@arnilo/prism-web-tools`, `@arnilo/prism-memory/rag`, `@arnilo/prism-core/runtime/workflows`) with planning, attributable citations, bounded refine loops, and HITL decision clarification. Rerunning without `--force` refuses non-empty destinations and existing generated files. `.env.example` contains placeholders only; `.gitignore` excludes `.env` and local stores.
51
+ Default generation (`init` template) installs only `@arnilo/prism` (mock provider). Selecting a real provider adds exactly one dependency: the `@arnilo/prism-providers` family package (the selected adapter imports from `@arnilo/prism-providers/<id>`). Specifying `--template deep-research` scaffolds a flagship deep research agent pipeline (`@arnilo/prism`, `@arnilo/prism-web-tools`, `@arnilo/prism-memory/rag`, `@arnilo/prism-core/runtime/workflows`) with planning, attributable citations, bounded refine loops, and HITL decision clarification. Specifying `--template personal-assistant` scaffolds a single-user personal assistant host composition with local status tools and secret redaction. Specifying `--template business-worker` scaffolds a multi-tenant enterprise worker host composition with verified tenant identity, durable storage contracts, and strict tenant boundaries. Rerunning without `--force` refuses non-empty destinations and existing generated files. `.env.example` contains placeholders only; `.gitignore` excludes `.env` and local stores.
52
52
 
53
53
 
54
54
  ### `prism providers add` (0.1.7)
@@ -224,6 +224,8 @@ prism init my-agent
224
224
  prism init my-agent --provider openai
225
225
  prism init my-agent --provider openrouter --with-workflows --with-evals
226
226
  prism init my-research --template deep-research
227
+ prism init my-assistant --template personal-assistant
228
+ prism init my-worker --template business-worker
227
229
  prism init --list-templates
228
230
  cd my-agent && npm install && npm test
229
231
 
@@ -614,5 +614,5 @@ Every configurable value is a positive safe integer (context may be zero); Prism
614
614
  - [Public contracts](public-contracts.md): `ToolDefinition`, `ToolResult`, `ToolExecutionContext`, `ContentBlock`, and `JsonObject` shapes.
615
615
  - [Host security guide](host-security.md): fail-closed checklist for permission policies, tool validation, and trust boundaries that must gate these tools.
616
616
  - [Tool conformance](tool-conformance.md): assertions for the tool-dispatch blocked-reason matrix these tools participate in.
617
- - [ACP coding-host interop](acp.md): host editors drive these tools through stable ACP v1 — client fs/terminal adapters, `CodingLifecycleEvent` emission (`file_changed` etc. via the `onEvent` options; `plan_changed` also fires from `writeCodingPlanFile`'s `onEvent`, F5), and permission/elicitation through the shared four-outcome decision model.
617
+ - [ACP coding-host interop](acp.md): host editors drive these tools through stable ACP v1 — client fs/terminal adapters, `CodingLifecycleEvent` emission (`file_changed` etc. via the `onEvent` options; `plan_changed` also fires from `writeCodingPlanFile`'s `onEvent`, F5), redacted supervisor `subagent_started` / `subagent_stopped` via `observeSupervisorLifecycle`, and permission/elicitation through the shared four-outcome decision model.
618
618
  - [LLM compaction package](compaction-llm.md): optional `createCodingCompactionStrategy()` retains bounded paths, patch intent, checks, plan/todo state, blockers, and next verification—not complete diffs or raw command output.
@@ -2,7 +2,7 @@
2
2
 
3
3
  ## What it does
4
4
 
5
- `@arnilo/prism-coding-tools/security` is an optional package that supplies structured execution policy for `@arnilo/prism-coding-tools/agent` tools and one disposable Docker/OCI sandbox reference. It complements name-based `PermissionPolicy` at dispatch time with path/command context checked **inside** each tool before side effects, and optionally contains untrusted coding work in a host-invoked container.
5
+ `@arnilo/prism-coding-tools/security` is an optional package that supplies structured execution policy for `@arnilo/prism-coding-tools/agent` tools, one disposable Docker/OCI sandbox reference, and an optional E2B hosted sandbox adapter. It complements name-based `PermissionPolicy` at dispatch time with path/command context checked **inside** each tool before side effects, and optionally contains untrusted coding work in a host-invoked container.
6
6
 
7
7
  | Export | Purpose |
8
8
  | --- | --- |
@@ -13,6 +13,7 @@
13
13
  | `createSandboxCodingTools` / `createSandboxReadOnlyTools` | Thin wrappers that return `tools` only (compat); still require `workspaceMode`. |
14
14
  | `createSandboxFilesystemOperations` / `createSandboxRepositoryOperations` | Optional execFile-backed FS/list/search backends for a disposable sandbox tree. |
15
15
  | `createDockerSandbox(options)` | Creates one disposable non-root Docker container with read-only root/source, bounded tmpfs workspace, typed `execFile`, import/export, and stop/kill/cleanup. |
16
+ | `createE2BSandbox(options)` / `connectE2BSandbox(options)` | Hosted E2B adapter on the same `DisposableSandbox` contract: `execFile`, `startProcess`/`attachProcess`, `pause`/`resume`, explicit `kill`. Optional peer `e2b@2.49.1` or a host `client`. Does not claim Docker network/egress parity. |
16
17
  | `createNativeSandbox(options)` | Linux-only network-free backend: every command runs in a fresh network namespace (`unshare`), POSIX `ulimit` hard caps, cwd-in-root containment; fails closed at creation on platforms/privileges that cannot deny egress. Reports truthful capability metadata (`networkIsolated`/`egressRestricted` true, `filesystemIsolated`/`processIsolated`/`privilegeIsolated` false). Docker remains the stronger, documented reference backend. |
17
18
  | `SandboxProcessHandle` | Optional long-running process handle (`write`/`signal`/`kill`/`release`/`wait`) returned by `DisposableSandbox.startProcess?`. |
18
19
  | `createEgressPolicy(options)` | Deny-all allow-list policy: exact host/port/protocol rules plus frozen `npm-registry` / `github` presets; SHA-256 fingerprint. |
@@ -32,7 +33,7 @@ import type { ExecutionAction, ExecutionPolicy, ExecutionDecision } from "@arnil
32
33
 
33
34
  Use this package when coding tools need path scoping, human approval, command rules, or a pluggable sandbox backend. Wire the returned policy through `createCodingTools(cwd, { executionPolicy })` or per-tool `executionPolicy` options.
34
35
 
35
- Use `createDockerSandbox()` when the host wants a production-reference containment boundary. Prism does **not** claim OS-level isolation unless the host constructs this adapter (or supplies an equivalent custom `DisposableSandbox`). Default policy denies shell/write/edit/delete/move without an `approve` callback and rejects paths outside configured roots. Coding shell definitions are marked `exclusive: true`, matching the approval policy's shell decision, so a single-shot turn containing shell work runs sequentially even when `toolConcurrency > 1`. Non-shell turns retain configured parallelism.
36
+ Use `createDockerSandbox()` when the host wants a production-reference containment boundary. Use `createE2BSandbox()` when the host wants a vendor-hosted VM; see [Hosted sandboxes](hosted-sandboxes.md). Prism does **not** claim OS-level isolation unless the host constructs this adapter (or supplies an equivalent custom `DisposableSandbox`). Default policy denies shell/write/edit/delete/move without an `approve` callback and rejects paths outside configured roots. Coding shell definitions are marked `exclusive: true`, matching the approval policy's shell decision, so a single-shot turn containing shell work runs sequentially even when `toolConcurrency > 1`. Non-shell turns retain configured parallelism.
36
37
 
37
38
  Use `createNativeSandbox()` when the host has no container runtime and needs network-free containment (0.1.6, plan 018 closeout `native-sandbox`). Linux only; creation fails closed with a documented error on other platforms or when the OS cannot create a network namespace (no root/CAP_SYS_ADMIN and no unprivileged user namespaces). Every command runs in a fresh netns — **loopback is down**, so even localhost connections fail; hosts that need loopback keep the Docker backend. Containment is egress denial + `ulimit` hard caps (address space from `memoryBytes`, CPU-time wall backstop, fd count from `maxFds`) + cwd-inside-root (`assertPathInsideRoots`, symlink-aware). The native backend does **not** isolate the filesystem: commands run as the invoking OS user with full host-tree access, so pair it with `createSandboxCodingComposition`/`createSandboxFilesystemOperations` (per-op `assertSandboxPath`) and the approval policy, exactly as with any custom `DisposableSandbox`. Its `capabilities` report `networkIsolated: true` and `egressRestricted: true` but `filesystemIsolated`/`processIsolated`/`privilegeIsolated: false` — the native backend is never a containment boundary for untrusted code (see [Sandbox capabilities](#sandbox-capabilities-020-plan-020-task-4)). Host env is never inherited; `env` is an exact allow-list (PATH only by default). No `startProcess` (ProcessSessions fails closed with `ERR_PRISM_PROCESS_UNSUPPORTED`), no CPU-rate/pids/fs-size caps (cgroup-only). Secrets passed as `secrets` are redacted from surfaced errors. See `docs/_evidence/phase18-primitive-review.md` for the full threat model.
38
39
 
@@ -126,7 +127,7 @@ Rules:
126
127
  - **`containmentClaim` is deprecated (0.2.0).** Retained for 0.1.7 compatibility as the conservative projection `workspaceCoherent && filesystemIsolated && networkIsolated && processIsolated` (privilege isolation excluded). It can only be `true` when every required capability is true — never authorize a security-sensitive action from this boolean alone; use `composition.capabilities`.
127
128
  - **Capability construction is O(1)** — one small frozen object per sandbox/composition; no command, filesystem, Docker, DNS, or network operation.
128
129
 
129
- `createDockerSandbox()` returns a `DisposableSandbox`: typed `execFile(file, args)`, shell-compatible `exec`, `status`, cooperative `stop`, forced `kill`, and idempotent `close`. Import may surface `importIdentity`; successful export updates `lastExportIdentity`. `close({ export })` can stream a bounded workspace tar plus SHA-256/entry/byte metadata through a host callback; checkpoints should retain only host artifact references/hashes, never whole workspaces. Optional `startProcess?(SandboxExecFileRequest)` returns a `SandboxProcessHandle` for long-running work consumed by coding-agent `createProcessSessions({ sandbox })`; absence means one-shot-only — ProcessSessions fails closed with `ERR_PRISM_PROCESS_UNSUPPORTED` (no native fallback). The Docker reference adapter does not implement `startProcess` yet; capability is detected, never assumed. See [Process sessions](process-sessions.md).
130
+ `createDockerSandbox()` returns a `DisposableSandbox`: typed `execFile(file, args)`, shell-compatible `exec`, `status`, cooperative `stop`, forced `kill`, and idempotent `close`. Import may surface `importIdentity`; successful export updates `lastExportIdentity`. `close({ export })` can stream a bounded workspace tar plus SHA-256/entry/byte metadata through a host callback; checkpoints should retain only host artifact references/hashes, never whole workspaces. `startProcess(SandboxExecFileRequest)` returns a `SandboxProcessHandle` for long-running work consumed by coding-agent `createProcessSessions({ sandbox })`; an opaque `ref` (`prism-docker-proc:<base64url>`) enables attested reconnect via `attachProcess(ref)` — container ID, workspace, and command fingerprint are validated fail-closed (no host fallback, no host PID probing). `sandbox.stop()` and `sandbox.kill()` terminate all active child processes before stopping the container. `createDockerProcessRecoveryBackend(sandbox, options?)` provides a ready-made `ProcessRecoveryBackend` with optional `expectedContainerId`, `expectedWorkspace`, and `expectedLabels` assertions; when a sandbox with `attachProcess` is passed to `createProcessSessions`, the recovery backend is wired automatically. See [Process sessions](process-sessions.md).
130
131
 
131
132
  ## Request/response example
132
133
 
@@ -219,6 +220,7 @@ A native Windows backend (Job objects / AppContainer) is tracked, not scheduled.
219
220
  ## Related APIs
220
221
 
221
222
  - [Coding agent tools](coding-agent-tools.md): durable plan/todo Markdown helpers and `state.coding` checkpoint metadata for restart/resume without a second runtime
223
+ - [Hosted sandboxes](hosted-sandboxes.md): E2B pause/resume adapter, filesystem-only snapshots, reconnect by sandbox id
222
224
  - [Workflows](workflows.md): `runWorkflow` / `resumeWorkflow` / `startWorkflowBackground` composition for coding tasks
223
225
  - [Host security guide](host-security.md)
224
226
  - [Performance limits](performance.md)
@@ -24,7 +24,7 @@ npm install @dietrichgebert/ponytail
24
24
  |---|---|---|
25
25
  | `@arnilo/prism-coding-tools/agent` | Core coding tools (read, write, edit, search, bash, git, diagnostics, check, ast-grep, lsp) | — |
26
26
  | `@arnilo/prism-coding-tools/security` | Sandbox execution adapters (Docker/OCI, native disposable sandbox, approval policies, egress proxy) | — |
27
- | `@arnilo/prism-coding-tools/document-reader` | Bounded PDF/DOCX literal-text extraction adapter with fail-closed loading | `pdf-parse`, `mammoth` |
27
+ | `@arnilo/prism-coding-tools/document-reader` | Bounded PDF/DOCX literal-text extraction; optional host-selected Mistral OCR (native fetch, no SDK peer) | `pdf-parse`, `mammoth` |
28
28
  | `@arnilo/prism-coding-tools/openapi` | OpenAPI 3.x tool generator and executor with SSRF protection and parameter validation | — |
29
29
  | `@arnilo/prism-coding-tools/computer-use-linux` | Linux desktop observation and targeting tool bridge | — |
30
30
  | `@arnilo/prism-coding-tools/dev` | Loopback-only developer inspector, event timeline visualizer, and local replay server | — |
@@ -56,6 +56,28 @@ Cleanup refuses, unless the host policy explicitly allows the documented action:
56
56
 
57
57
  Partial failure persists state `unknown` with per-repository `unknown`/`removed` legs and remains reconcilable: retrying cleanup converges to `closed`.
58
58
 
59
+ ## Spawn isolation (supervisor children)
60
+
61
+ Parallel model-requested children share the host cwd by default. Wrap the catalog factory that needs isolation with `createWorktreeChildFactory(factory, { workspaces, repositoryId, branch? })` from `@arnilo/prism-coding-tools/agent`:
62
+
63
+ ```ts
64
+ import { createWorktreeChildFactory } from "@arnilo/prism-coding-tools/agent";
65
+
66
+ const isolated = createWorktreeChildFactory((ctx) => createExploreAgent(ctx, ctx.cwd), {
67
+ workspaces,
68
+ repositoryId: "app",
69
+ });
70
+ createSupervisor({
71
+ children: { explore: { createAgent: isolated.createAgent } },
72
+ hooks: { after: isolated.after },
73
+ });
74
+ ```
75
+
76
+ - `createAgent` runs `workspaces.create({ taskId: delegationId, branch: branch ?? `agent/${delegationId}` })` before the child exists, so missing `worktreeRoots` or an unknown repository fails closed with no spawn, and the child context gains `cwd` = the record's `worktreePath`. Coding tools built from that `cwd` cannot reach the main checkout; keep `worktreeRoots` host-approved.
77
+ - `after` is the supervisor terminal hook: success, failure, abort, and pre-spawn rejection clean up once. A suspended child is deliberately **not** cleaned while it is non-terminal — the hook runs when the resume attempt reaches a terminal outcome instead — and a resumed child re-creates the identical workspace because the task id and default branch derive from `delegationId`.
78
+ - Unwrapped children keep the shared cwd: one `git worktree add` per isolated child, no clone, no second sandbox type. Write-heavy parallel children still need isolation or exclusive tools for the shared-cwd case.
79
+ - Dirty isolated worktrees refuse removal by default; extract artifacts first, allow `policy.allowDirtyCleanup` for forced removal, or reconcile with `list`/`cleanup` — a host restart loses in-process ownership of workspaces it created.
80
+
59
81
  ## Ownership and fencing
60
82
 
61
83
  Ownership scopes are part of the trust boundary: records are read and written under the configured `tenantId`/`accountId`/`userId`, and lease acquisition under another scope fails closed as `ERR_PRISM_WORKSPACE_OWNERSHIP`. Every mutation runs under a `LeaseStore` lease (`tryAcquireLease`/`releaseLease`, TTL 30 s default / 300 s hard); the lease fencing token is stored in the record and each `CheckpointStore` save is a version CAS plus a monotonic fencing-token check, so a worker whose lease lapsed or was fenced out cannot overwrite newer state. Stale workers reject deterministically with `ERR_PRISM_WORKSPACE_FENCE`.
@@ -21,7 +21,7 @@ Current APIs:
21
21
 
22
22
  ## When to use it
23
23
 
24
- Use compaction when a host wants provider input rebuilt from a summary plus recent messages while preserving the full branch in the session store. Use `session.compact()` for explicit compaction or `thresholdEntries` for opt-in auto-compaction before provider input.
24
+ Use compaction when a host wants provider input rebuilt from a summary plus recent messages while preserving the full branch in the session store. Use `session.compact()` for explicit compaction, `thresholdEntries` for an entry-count auto-compaction gate before provider input, or `trigger` when the decision should follow estimated input size or host code.
25
25
 
26
26
  Do not use it as vector memory, semantic search, provider-backed summarization, a store rewrite, a database migration, CLI/RPC command, provider-specific HTTP adapter, or whole-run retry loop.
27
27
 
@@ -45,11 +45,12 @@ createDefaultCompactionStrategy(options?: DefaultCompactionStrategyOptions): Com
45
45
  | Field | Purpose |
46
46
  | --- | --- |
47
47
  | `strategy` | Optional `CompactionStrategy`; defaults to `createDefaultCompactionStrategy()`. |
48
- | `thresholdEntries` | Enables auto-compaction when current branch entries exceed this count. Omit it for no auto-compaction. |
48
+ | `thresholdEntries` | Enables auto-compaction when current branch entries exceed this count. Omit it for no auto-compaction. Ignored when `trigger` is set. |
49
+ | `trigger` | Replaces `thresholdEntries`: `{ type: "threshold_entries", entries }`, `{ type: "input_ratio", ratio }` (compact when the estimated input is at least `ratio` of the compiler's resolved input cap), or `{ type: "custom", shouldCompact(context) }`. |
49
50
  | `keepRecentEntries` | Number of recent message entries kept in provider context. |
50
51
  | `maxSummaryChars` | Maximum default summary length. |
51
52
  | `secrets` | Exact known secret strings to redact from summaries/events/store text. |
52
- | `metadata` | Explicit host metadata passed to the compaction strategy only. |
53
+ | `metadata` | Explicit host metadata passed to the compaction strategy, and to a `custom` trigger context. |
53
54
  | `signal` | Optional manual compaction abort signal. |
54
55
 
55
56
  `RunOptions.compaction: false` disables configured auto-compaction for that run. `CompactionContext` also accepts optional `keepRecentEntries`, `trigger`, and `secrets`. Context values override or add to strategy defaults for that compaction call.
@@ -95,7 +96,37 @@ createDefaultRetryPolicy(options?: DefaultRetryPolicyOptions): RetryPolicy
95
96
 
96
97
  > **Contract — compact at the task boundary.** `session.compact()` throws `Error("Agent session already has an active run")` while `run()`/`stream()` is in flight. Intended model: one `run()` per task, then compact. Do not design mid-run compaction. Auto-compaction (when `thresholdEntries` is set) already runs **before** provider input, not during the turn. Live demo: [`examples/autonomous-coding-loop.ts`](../examples/autonomous-coding-loop.ts) (`compact` node after execute/validate/gate).
97
98
 
98
- Auto-compaction checks at most once per `run()`, after input/model-change entries are appended and before provider input assembly. It runs only when `AgentConfig.compaction` or `RunOptions.compaction` supplies `thresholdEntries`, and it is skipped by `RunOptions.compaction: false`.
99
+ Auto-compaction checks at most once per `run()`, after input/model-change entries are appended and before provider input assembly. It runs only when `AgentConfig.compaction` or `RunOptions.compaction` supplies `thresholdEntries` or `trigger`, and it is skipped by `RunOptions.compaction: false`.
100
+
101
+ `trigger` replaces the legacy gates and is asked once per run, with the would-be input already appended. All three forms are resolved by one helper (`resolveShouldCompact`), so `session`, observational memory, and host code share the same decision:
102
+
103
+ | Trigger | Decides with | Notes |
104
+ | --- | --- | --- |
105
+ | `threshold_entries` | `entryCount > entries` | Pure count, no token estimate. |
106
+ | `input_ratio` | `estimatedInputTokens >= ratio * inputCapTokens` | The cap comes from `resolveInputCap` — the same helper `attentionCompiler` uses, which needs `maxInputTokens` or `model.limits.contextWindow`. An unresolvable cap is a config error and fails the run loudly. |
107
+ | `custom` | `shouldCompact(context)` | Async-ok. `context` carries `sessionId`, `entryCount`, `estimatedInputTokens`, `inputCapTokens`, `metadata`, and `signal`; the two token numbers are resolved lazily, so a callback that only reads counts never needs a model cap. A callback that throws — including one that reads a cap that cannot resolve — decides **false** and never compacts on a guess. |
108
+
109
+ An unknown trigger `type` throws at first use (`assertCompactionTrigger`), so a typo never silently disables compaction. A branch whose last entry is already `kind: "compaction"` is skipped, so a fresh summary is never compacted again.
110
+
111
+ `custom` also has a ready-made builder for the [attention compiler](attention-compiler.md)'s `truncated` signal. `createAttentionTruncationTrigger({ threshold })` counts consecutive truncated turns from `attention_compiled` events and fires **once per armed streak** at the next compaction decision, which is exactly "compact at the next task boundary because stubs could no longer hold the request":
112
+
113
+ ```ts
114
+ const truncation = createAttentionTruncationTrigger();
115
+ session.subscribe((event) => {
116
+ if (event.type === "attention_compiled") truncation.observe(event);
117
+ });
118
+ const agent = createAgent({ model, provider, attentionCompiler: true, compaction: { trigger: truncation.trigger } });
119
+ ```
120
+
121
+ Example — compact when the assembled input passes 90% of the model window:
122
+
123
+ ```ts
124
+ const agent = createAgent({
125
+ model,
126
+ provider,
127
+ compaction: { trigger: { type: "input_ratio", ratio: 0.9 }, keepRecentEntries: 8 },
128
+ });
129
+ ```
99
130
 
100
131
  `rebuildSessionContext()` detects the latest compaction entry on a branch. Its returned `entries` still contains the raw full branch, while `messages` contains only messages after the compaction boundary plus `keepEntryIds`, and `summaries` contains the compaction summary plus later summary entries.
101
132
 
@@ -172,6 +203,7 @@ The default strategy does not call a provider. Hosts that need model-generated s
172
203
  - [Session stores and branching](session-stores-and-branching.md): branch entries, compaction entries, and `rebuildSessionContext()` behavior.
173
204
  - [Input and prompt assembly](input-and-prompt-assembly.md): compacted summaries become default summary messages for provider input.
174
205
  - [Agent/session runtime](agent-session-runtime.md): `session.compact()`, opt-in auto-compaction, `RunOptions.retry`, and `retry_scheduled` runtime behavior.
206
+ - [Attention compiler](attention-compiler.md): resolves the same input cap and shrinks an over-ratio request before compaction is considered.
175
207
  - Example: [`examples/autonomous-coding-loop.ts`](../examples/autonomous-coding-loop.ts) — task-boundary compact after each iteration.
176
208
  - [Middleware hooks](middleware-hooks.md): `compaction` and `retry` middleware payload timing.
177
209
  - [Contribution registries](contribution-registries.md): compaction strategy and retry policy contributions.
@@ -4,10 +4,12 @@
4
4
 
5
5
  `@arnilo/prism-memory/compaction/observational-memory` is an optional subpath for source-backed observational memory and fast compaction.
6
6
 
7
- Current status: ledger/projection/render/recall utilities, explicit worker runtime, fast compaction strategy, inert extension helper, recall tool, and status/view command factories are available.
7
+ Current status: ledger/projection/render/recall utilities, optional work-scope indexing, explicit worker runtime, fast compaction strategy, inert extension helper, recall tool, and status/view command factories are available.
8
8
 
9
9
  This package is distinct from `@arnilo/prism-memory` working/semantic memory: observational memory compresses and recalls source-backed observations/reflections; semantic memory retrieves embeddings; working memory stores the current structured profile/state. Hosts may compose both.
10
10
 
11
+ This page's memory stays **episodic**: the ledger records what happened in a session, its observer/reflector/dropper workers are the only writers of observations and reflections, and nothing downstream re-observes the transcript. Typed notes that want to outlive the session (facts, procedures, file references) are a separate layer — the [memory fabric](memory-fabric.md) — which views an observation by id as an `episode` note without copying it, never wraps or replaces these workers, and keeps its own recall path. Promotion out of the ledger is an explicit host write, not a side effect of compaction.
12
+
11
13
  ## Four-layer provider context
12
14
 
13
15
  Observational memory composes four independent layers for long sessions (Mastra-style):
@@ -19,7 +21,9 @@ Observational memory composes four independent layers for long sessions (Mastra-
19
21
  | **Reflections** | Higher-level summaries over observation ids | Reflector worker on observations after last reflection coverage when `reflection.observationTokens` met |
20
22
  | **Raw-source retrieval** | Exact branch messages behind a memory id or cursor page | `recallObservationalMemory()` / `recallObservationalMemoryBranchPage()` / `createRecallMemoryTool()` — exact-id or cursor paging only; no semantic search |
21
23
 
22
- Activation is explicit: `createObservationalMemory().attach()` coordinates post-run observe/reflect/drop and `context.compactAfterTokens` compaction. Import and extension `setup` start nothing. Recall, commands, and utilities fail closed on invalid ids, wrong `sessionId`, ambiguous tool input, or oversized pages. Pass `secrets` for exact-value redaction in render/recall/worker paths. Branch isolation: hosts supply current-branch `appendEntry` and `getEntries`; mismatched store/session pairs fail closed after append.
24
+ The opt-in work-scope index filters the observation and reflection layers for a host-selected working set. It is not a fifth context layer, retrieval system, or session scope.
25
+
26
+ Activation is explicit: `createObservationalMemory().attach()` coordinates post-run observe/reflect/drop and compaction — by default `context.compactAfterTokens`, or whatever host gate `trigger` / `shouldCompact` supplies. Import and extension `setup` start nothing. Recall, commands, and utilities fail closed on invalid ids, wrong `sessionId`, ambiguous tool input, or oversized pages. Pass `secrets` for exact-value redaction in render/recall/worker paths. Branch isolation: hosts supply current-branch `appendEntry` and `getEntries`; mismatched store/session pairs fail closed after append.
23
27
 
24
28
  See `examples/observational-memory-lifecycle.ts` for attach → turn → projection/recall/page without live credentials.
25
29
 
@@ -31,6 +35,8 @@ Use `createObservationalMemoryCompactionStrategy()` when compaction should rende
31
35
 
32
36
  ## Inputs / request
33
37
 
38
+ **Option surfaces** — `appendEntry` takes `ObservationalMemoryAppendOptions` (custom observation/reflection text, trust, and metadata); `createWorkScopeController` takes `WorkScopeControllerOptions` (session, `appendEntry`, optional `secrets`).
39
+
34
40
  Memory records use `SessionEntry.kind: "custom"` with `entry.data.type` markers:
35
41
 
36
42
  | Type | Payload |
@@ -38,6 +44,9 @@ Memory records use `SessionEntry.kind: "custom"` with `entry.data.type` markers:
38
44
  | `om.observations.recorded` | `{ observations, coversUpToId? }` — successful observer runs append coverage even when `observations` is empty. |
39
45
  | `om.reflections.recorded` | `{ reflections, coversUpToId? }` |
40
46
  | `om.observations.dropped` | `{ observationIds, coversUpToId? }` |
47
+ | `om.scope.opened` | `{ id, parentId?, kind?, label? }` — host-defined scope tree node. |
48
+ | `om.scope.closed` / `om.scope.entered` / `om.scope.left` | `{ scopeId }` for close/enter; `{}` for leave. |
49
+ | `om.scope.bound` / `om.scope.unbound` | `{ scopeId, refs }` — many-to-many `om:<12-hex>` or `reflection:<12-hex>` membership. |
41
50
  | `om.folded` | Compaction `data.memory` folded details. |
42
51
 
43
52
  Ids are known, source-backed 12-character lowercase hex strings matching `^[a-f0-9]{12}$`.
@@ -67,18 +76,21 @@ Key exports:
67
76
  | Export | Purpose |
68
77
  | --- | --- |
69
78
  | `foldObservationalMemoryLedger()` | Fold custom memory entries into observations, reflections, drops, and coverage markers. |
79
+ | `foldWorkScopeMap()` / `createWorkScopeController()` | Fold the opt-in scope index or append validated open/close/enter/leave/bind/unbind entries. |
80
+ | `projectWorkMemory()` | Filter the folded observations/reflections through a scope query; exact-id recall stays unfiltered. |
81
+ | `withWorkScope()` | Open a missing scope, enter it for an async callback, and always leave without closing it. |
70
82
  | `isEligibleObservationSourceEntry()` / `eligibleObservationSources()` | Select user/assistant/tool `message` entries for observer input. |
71
83
  | `unscannedEntries()` / `observationsUncoveredByReflection()` | Dual coverage helpers for observation scan and reflection windows. |
72
- | `buildObservationalMemoryProjection()` | Build active/full/folded projections from current branch entries. |
73
- | `buildObservationalMemoryContextBlocks()` | Render observational-memory + recent-messages context blocks for provider input. |
84
+ | `buildObservationalMemoryProjection()` | Build active/full/folded projections from current branch entries. Optional `invalidatedIds` drops observations whose id or `sourceEntryIds` match, and reflections that rest on them. Full ledger stays for audit. |
85
+ | `buildObservationalMemoryContextBlocks()` | Render observational-memory + recent-messages context blocks for provider input. Same `invalidatedIds` option. |
74
86
  | `selectRecentMessageEntries()` / `renderRecentMessageWindow()` | Bounded exact recent-message suffix; count via `keepRecentEntries`, optional token trim via `estimateEntryTokens`. |
75
87
  | `createFoldedMemoryDetails()` | Create JSON details for compaction `data.memory`. |
76
88
  | `renderObservationalMemory()` | Render reflections and observations into a prepared memory summary. |
77
- | `recallObservationalMemory()` | Recover source evidence for a known observation/reflection id from supplied current-branch entries. |
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. |
78
90
  | `recallObservationalMemoryBranchPage()` | Page eligible user/assistant/tool messages around a cursor entry id (`forward`/`backward`, optional `detail: summary|full`). |
79
91
  | `createMemoryId()` / `isMemoryId()` | Create/check 12-character ids. |
80
92
  | `resolveObservationalMemorySettings()` | Merge `observational-memory` settings with defaults and overrides. |
81
- | `createObservationalMemory()` / `attach()` | One activation wires post-run observe/reflect/drop and `compactAfterTokens` compaction; returns proxied session, runtime, context provider, and strategy. |
93
+ | `createObservationalMemory()` / `attach()` | One activation wires post-run observe/reflect/drop and compaction (`compactAfterTokens`, or a host `trigger` / `shouldCompact`); returns proxied session, runtime, context provider, and strategy. |
82
94
  | `createObservationalMemoryRuntime()` | Low-level explicit flush for advanced hosts or tests. |
83
95
  | `createObservationalMemoryCompactionStrategy()` | Render existing folded memory as a standard Prism compaction summary with `data.memory`. |
84
96
  | `createObservationalMemoryExtension()` | Inert extension helper that registers the strategy contribution unless disabled. |
@@ -86,7 +98,32 @@ Key exports:
86
98
  | `createMemoryStatusCommand()` / `createMemoryViewCommand()` | Optional `om:status` and `om:view` command factories. |
87
99
  | `createObservationalMemoryCommands()` | Convenience factory returning status and view commands. |
88
100
 
89
- 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. Tool and command factories are inert until a host registers/selects them.
101
+ 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.
102
+
103
+ ### Work-scope index (opt-in)
104
+
105
+ `WorkScope` is a host-named, append-only index over one observational-memory ledger. Without `om.scope.*` entries, the map has only its implicit `session` root, context renders the existing active pool, and the dropper keeps its existing behavior.
106
+
107
+ Use `createWorkScopeController({ session, appendEntry, secrets? })` to `open`, `close`, `enter`, `leave`, `bind`, or `unbind` scopes. Scope ids are host-defined (`[A-Za-z0-9._:/-]{1,128}`, no `..`); there are caps of 256 scopes, depth/stack 8, 4,096 binds per scope, and 512 characters for labels or kinds. Invalid ids, missing/closed parents, duplicate scopes, unknown record ids, and ownership mismatch fail closed. Labels and kinds receive the same secret redaction as observational-memory text.
108
+
109
+ `projectWorkMemory(ledger, map, { from, include, closed?, kinds? })` returns a filtered observation/reflection view plus outline. `include` is `self`, `self+ancestors`, `self+descendants`, or `lineage`; `closed: "hide"` is the default, except closed ancestors of `from` remain available. Default attached context uses the current leaf with `self+ancestors`, rendering Scope Outline, Reflections, then Observations. The compaction summary — the layer the next run's pack starts from — renders the same projection, so the full ledger never rides into the prefix; the folded payload keeps every observation, so entering another scope can still surface what that summary hid. `recallObservationalMemory()` still reads the complete current branch by exact id.
110
+
111
+ After a flush records new observations or reflections, it binds those ids once to the current leaf scope only. A host promotes relevant memory explicitly by binding it to an ancestor; a reflection whose bind sits on a **closed** scope can also graduate into durable semantic memory through the fabric's `remember({ kind: "fact" | "procedure", reflectionId })`. While any host scope exists, the runtime skips the observation dropper; the folded-payload byte cap remains a storage safety cap, not working-set garbage collection. `withWorkScope(controller, spec, fn)` opens `spec` if needed, enters it, runs `fn`, and leaves in `finally`; it never closes a scope. This index does not provide resource-scoped observational memory or budget-based dropping as a working-set mechanism.
112
+
113
+ ### Compact-when override
114
+
115
+ `createObservationalMemory()` accepts a compact-when gate beside the settings: `trigger` (the same union `CompactionOptions.trigger` uses) or the `shouldCompact(context)` shorthand. When either is set it **replaces** `context.compactAfterTokens`; omitted, the token gate is unchanged.
116
+
117
+ ```ts
118
+ const om = createObservationalMemory({
119
+ observation: { provider, model },
120
+ shouldCompact: (context) => context.entryCount > 40 || context.estimatedInputTokens / context.inputCapTokens >= 0.9,
121
+ });
122
+ ```
123
+
124
+ - `context.entryCount` counts current-branch entries; `context.estimatedInputTokens` is this package's own `estimateEntryTokens` sum; `context.inputCapTokens` comes from `resolveInputCap` on `attach({ sessionModel })`.
125
+ - Both token numbers resolve lazily, so a callback that only reads counts works with a `sessionModel` that declares no `contextWindow`. Reading the cap without one throws inside the callback, and the gate then decides **false** — the sync loop reports it through the `debug` sink (`observational-memory:compaction-trigger-error`) and never compacts on a guess. `input_ratio` needs a resolvable cap and throws instead.
126
+ - An unknown trigger `type` or a non-function `shouldCompact` throws at `createObservationalMemory()`, before any session work. Tool and command factories are inert until a host registers/selects them.
90
127
 
91
128
  ## Request/response example
92
129
 
@@ -105,8 +142,13 @@ import {
105
142
  createObservationalMemoryCommands,
106
143
  createObservationalMemoryRuntime,
107
144
  createRecallMemoryTool,
145
+ createWorkScopeController,
146
+ foldObservationalMemoryLedger,
147
+ foldWorkScopeMap,
148
+ projectWorkMemory,
108
149
  recallObservationalMemory,
109
150
  renderObservationalMemory,
151
+ withWorkScope,
110
152
  } from "@arnilo/prism-memory/compaction/observational-memory";
111
153
 
112
154
  const om = createObservationalMemory({
@@ -119,11 +161,19 @@ const attached = om.attach(session, {
119
161
  appendEntry: (entry, options) => store.append(entry, options),
120
162
  sessionModel: agent.config.model,
121
163
  });
122
- await attached.session.run("Continue from prior work");
164
+ const scopes = createWorkScopeController({ session: attached.session, appendEntry: (entry, options) => store.append(entry, options) });
165
+ await scopes.open({ id: "plan:memory", kind: "plan", label: "Memory work" });
166
+ await withWorkScope(scopes, { id: "task:cleanup", parentId: "plan:memory", kind: "task" }, () =>
167
+ attached.session.run("Continue from prior work"),
168
+ );
123
169
 
124
170
  const entries = await session.entries();
125
171
  const projection = buildObservationalMemoryProjection(entries);
126
- const summary = renderObservationalMemory(projection.reflections, projection.observations);
172
+ const scoped = projectWorkMemory(foldObservationalMemoryLedger(entries), foldWorkScopeMap(entries), {
173
+ from: "task:cleanup",
174
+ include: "self+ancestors",
175
+ });
176
+ const summary = renderObservationalMemory(scoped.reflections, scoped.observations, { outline: scoped.outline });
127
177
  const evidence = recallObservationalMemory(entries, "aaaaaaaaaaaa");
128
178
 
129
179
  const memory = createObservationalMemoryRuntime({
@@ -231,11 +281,14 @@ Ownership: funnel only within the `OwnershipScope` already on the parent agent/s
231
281
  ## Related APIs
232
282
 
233
283
  - [Use-case model selection](use-case-model-selection.md): session vs worker model binding and `resolveUseCaseModel`.
284
+ - [Attention compiler](attention-compiler.md): opt-in per-turn shrink that runs before compaction is considered and resolves the same input cap for `input_ratio` triggers.
234
285
  - [Thinking and reasoning](thinking-and-reasoning.md): `thinkingLevel` → provider `compat`.
235
286
  - [Provider request policies](provider-request-policies.md): derived `om:{session.id}` on worker generate.
236
287
  - [Compaction and retry policies](compaction-and-retry.md): replaceable compaction strategy boundary.
288
+ - [Workflows](workflows.md): a host may use a workflow `nodeId` as a scope id with `withWorkScope`; the workflow runner does not enter scopes itself.
237
289
  - [LLM compaction package](compaction-llm.md): existing optional compaction-package pattern.
238
290
  - [Session stores and branching](session-stores-and-branching.md): branch entries that observational memory reads and appends to.
291
+ - [Memory fabric](memory-fabric.md): optional typed notes over the same stores; `episode` notes are views of these observation ids, and the ledger stays episodic.
239
292
  - [Supervisor delegation](supervisors.md): child sessions whose messages this page's opt-in funnel can copy onto a workspace branch.
240
293
  - [Extensions](extensions.md): inert registration pattern for optional package contributions.
241
294
  - [Tools](tools.md): host activation and dispatch for optional recall tool contributions.
@@ -128,7 +128,7 @@ Each active skill contributes two things the runtime wires together:
128
128
  - `Skill` prompt text → rendered as system messages by `skillMessages()` / `skillPromptText()` (active set only). Default `skillsDisclosure: "progressive"` sends `Skill <name>: <description>`; full `instructions` appear only when the skill is in the session `LoadedSkillSet` or disclosure is `"eager"`.
129
129
  - `Skill.context: ContextProvider[]` → collected across active skills (`activeSkills.flatMap(s => s.context ?? [])`), resolved through the existing `resolveContextProviders(...)`, and merged into the request's `context` **after** host `AgentConfig.context` blocks. Inactive skills contribute neither instructions nor context.
130
130
 
131
- `toolNames` enforcement is live: because selection routes through `resolveActiveSkills()`, a skill demanding a host-inactive tool throws with `Skill ${name} requires inactive tool: ${missing}` **before the first provider turn** — no provider call, no store write, no partial side effect. This is the fail-fast contract the docs already claimed; the runtime now honors it.
131
+ `toolNames` enforcement is live: because selection routes through `resolveActiveSkills()`, a skill demanding a host-inactive tool throws with `Skill ${name} requires inactive tool: ${missing}` **before the first provider turn** — no provider call, no store write, no partial side effect. When `RunOptions.toolNames` narrows the run, that snapshot is the host-active list — a skill cannot require a registered tool the run did not grant.
132
132
 
133
133
  ```ts
134
134
  import { createAgent, createSkillRegistry, type ContextProvider } from "@arnilo/prism";
@@ -219,7 +219,7 @@ Under pressure on a skill with a loaded body, eviction may demote to catalog-onl
219
219
 
220
220
  ### Optional tool-result fold
221
221
 
222
- `toolResultFold` on `AgentConfig` / `RunOptions` (run wins) is **off** unless the host supplies a `summarize` callback. When enabled, aged large tool-result messages in the **provider view** become a one-line header plus bounded summary text; session store entries stay raw. Defaults: `minAgeTurns` **2**, `minBytes` **4096**, `maxSummaryBytes` **512** (hard **4096**). Summarizer failure keeps the raw tool result (fail closed). Not a second memory system — use observational memory / compaction for durable recall.
222
+ `toolResultFold` on `AgentConfig` / `RunOptions` (run wins) is **off** unless the host supplies a `summarize` callback. When enabled, aged large tool-result messages in the **provider view** become a one-line header plus bounded summary text; session store entries stay raw. Defaults: `minAgeTurns` **2**, `minBytes` **4096**, `maxSummaryBytes` **512** (hard **4096**). Summarizer failure keeps the raw tool result (fail closed). Not a second memory system — use observational memory / compaction for durable recall, and remember that observational memory stays the source-backed **episodic** ledger of the session: typed, time-bounded notes that outlive a session are a separate layer ([memory fabric](memory-fabric.md)) whose provider arrives through this same inert seam.
223
223
 
224
224
  ```ts
225
225
  await session.run("…", {
@@ -267,8 +267,10 @@ Use `activateAllCapabilities: true` only as a temporary all-skills/all-tools com
267
267
 
268
268
  - [Agent/session runtime](agent-session-runtime.md): consumes host-selected context providers and skills from explicit agent config.
269
269
  - [Input and prompt assembly](input-and-prompt-assembly.md): default prompt builder and provider-input assembly helper.
270
+ - [Attention compiler](attention-compiler.md): opt-in ratio gate that rewrites aged history and tool results for one request, leaving resolved context blocks and skills in place.
270
271
  - [Instruction injection](instruction-injection.md): package injectors contribute `contextBlocks` that merge after host+skill provider blocks.
271
272
  - [Retrieval-augmented generation](rag.md): optional retrieved citations contribute through the same explicit inert context seam.
273
+ - [Memory fabric](memory-fabric.md): optional typed notes whose provider contributes the same `working-memory` / `semantic-memory` blocks this seam already carries, under a host-registered name.
272
274
  - [Public contracts](public-contracts.md): `ContextProvider`, `ContextResolutionContext`, `ContextBlock`, `Skill`, `SkillRegistry`, `PromptBuilder`, and `PromptBuildRequest`.
273
275
  - [Middleware hooks](middleware-hooks.md): `context` and `prompt_build` hooks.
274
276
  - [Contribution registries](contribution-registries.md): inert context provider and skill contributions.
@@ -117,7 +117,7 @@ Behavior notes:
117
117
  - `create` with an explicit `id` writes with `expectedVersion: 0` (create-only): a duplicate create never overwrites the winner's metadata and returns the existing thread.
118
118
  - `branch()` enforces `maxActiveBranches` on its read snapshot; the version guard inside the same write makes the cap exact even under concurrency (a concurrent branch cannot slip past the cap), and the marker keeps branch refs append-only.
119
119
  - `archive()` on an already-archived thread is a no-op; a stale `branch`/`archive` racing a delete fails `not_found` (the row is gone) and the write path never re-creates a deleted thread — delete wins.
120
- - Deletion purges the whole session ledger (entries, runs, events, tool calls, usage, branches, search rows) through `lifecycle.applyRetention`; legal holds block deletion and report `held: true`.
120
+ - Deletion purges the whole session ledger (entries, runs, events, tool calls, usage, branches, search rows) through `lifecycle.applyRetention`; legal holds block deletion and report `held: true`. Semantic-memory `forget({ hold: true })` is a separate knowledge-store hold (see [Working and semantic memory](working-and-semantic-memory.md)); conversation delete does not walk embeddings.
121
121
 
122
122
  ## Security and performance notes
123
123
 
@@ -57,6 +57,9 @@ Data-defined route table over the server seam — each route either rewrites the
57
57
  | `GET /events?runId=<id>` | Durable SSE stream of normalized events. `Last-Event-ID` header reconnect and `?cursor=` are honored by the server seam; missing `runId` → `400 ERR_PRISM_DEV_ROUTE`. |
58
58
  | `GET /runs/:id/replay?cursor=…` | Paged replay of a stored run from the durable `AgentEventSource` — **no session, no provider, no re-execution** (`createPrismAgentEventReplay` page). Returns `{ items, nextCursor?, terminal }`; unknown/foreign run ids → `404`. |
59
59
  | `POST /runs/:runId/decisions/:decisionId` | Resumes/denies one suspended approval. Body `{ outcome: "allow_once" \| "allow_always" \| "deny", expectedVersion? }` → forwarded as a single-entry core decision batch; unknown discriminants and stale versions fail closed (`400`) at the core boundary **before any state write**. |
60
+ | `GET /inspect` | Returns the host composition report (`HostCompositionReport`) detailing profile, effective tools, redacted credentials, ownership, storage durability, sandbox capabilities, and governance coverage. | dev composition inspection (`inspectDevInspector`). |
61
+ | `GET /runs/:id/summary` | Projects durable replay events with `projectAgentTimeline({ content: "metadata" })` and returns `summarizeTimeline` (latency, cost, tool counts). No step I/O. Requires `eventSource`. Unknown run → `404`. |
62
+ | `POST /compare` | Body `{ left, right }` each `{ summary, aggregate?, manifest? }` — existing TimelineSummary / ExperimentAggregate fields only. Returns quality/cost/latency winners. `invariantsPassed: false` on either side sets `qualityWinner: "invariant_blocked"`. Bounded 64KiB. |
60
63
 
61
64
  Reconnect semantics: every SSE frame carries `id: <cursor>`; a reconnecting client sends `Last-Event-ID: <cursor>` and receives exactly the post-cursor events — no duplicates, no loss (server conformance-tested). Replay pages are bounded by the deployment limits (`maxReplayEvents`, `maxReplayCursorBytes`) and ownership-scoped by the source seam itself.
62
65
 
@@ -71,6 +74,7 @@ Panels:
71
74
  - **Usage** — per-run totals summed from `provider_turn_finished.usage` and the terminal `agent_finished.usage` (input/output/total tokens, cost when the model reports it).
72
75
  - **Decisions** — `agent_suspended` renders one card per pending decision (`PendingDecision.approvalId`, tool name, redacted reason, `expectedVersion` from the event's run version). Buttons post `POST /runs/:runId/decisions/:approvalId` ({ outcome: `allow_once` | `allow_always` | `deny`, expectedVersion }); rejections show the seam's fail-closed error verbatim, and a remaining-multi-decision suspension re-renders from the response's `runState.interruption`.
73
76
  - **Run selector** — session runs (live + loaded) with status; a durable view of any past run loads via `GET {basePath}/events?runId=…` over `EventSource` — the seam's own `Last-Event-ID` reconnect applies. Without a durable event source wired, loading by runId surfaces that fact instead of pretending to replay.
77
+ - **Compare** — `Compare last 2` loads `/runs/:id/summary` for the two most recent runs and `POST /compare`. Renders quality/cost/latency winners from those artifacts. A high mean score cannot beat a failed invariant. Without durable events the panel says so instead of inventing numbers.
74
78
 
75
79
  ## Request/response example
76
80
 
@@ -91,6 +91,7 @@ if (chunk.accepted) emit(redactDeviceTelemetry(createSecretRedactor([token]), fr
91
91
  ## Related APIs
92
92
 
93
93
  - [Browser automation](browser-automation.md): verified-state checkpoints + reload/verify-before-side-effect for browser composition.
94
+ - [Realtime voice](realtime-voice.md): governed OpenAI Realtime orchestration on this admission contract.
94
95
  - [Linux desktop control](computer-use-linux.md): first-party host-owned `computer-use-linux` MCP wrapper using this contract.
95
96
  - [Conversations](conversations.md): durable threads that own the runs device sessions bind to.
96
97
  - [Host security](host-security.md): approval, sandbox, and egress trust boundaries device adapters compose over.
@@ -1,10 +1,10 @@
1
1
  # Document reader (`@arnilo/prism-coding-tools/document-reader`)
2
2
 
3
- > **Optional peer install:** `pdf-parse` and/or `mammoth` — see [Optional peer dependencies](peer-dependencies.md).
3
+ > **Optional peer install:** `pdf-parse` and/or `mammoth` — see [Optional peer dependencies](peer-dependencies.md). OCR uses **native fetch**, not an SDK peer.
4
4
 
5
5
  ## What it does
6
6
 
7
- Optional bounded literal-text extraction for PDF and DOCX files, consumed by the coding `read` tool (plan 018 closeout `doc-reader`, 0.1.6). `createDocumentReader()` returns a `DocumentReader` that the host wires into `createReadTool(cwd, { documentReader })`; the read tool then extracts text from supported documents instead of falling back to the raw text page.
7
+ Optional bounded literal-text extraction for PDF and DOCX files, consumed by the coding `read` tool (plan 018 closeout `doc-reader`, 0.1.6). `createDocumentReader()` returns a `DocumentReader` that the host wires into `createReadTool(cwd, { documentReader })`; the read tool then extracts text from supported documents instead of falling back to the raw text page. Scanned PDFs/images need a **host-selected** `createMistralOcrParser({ apiKey })` passed in `parsers` — default wiring never calls an external OCR service.
8
8
 
9
9
  ## When to use it
10
10
 
@@ -63,18 +63,26 @@ const myPdfParser: DocumentParser = {
63
63
  },
64
64
  };
65
65
  const reader = await createDocumentReader({ parsers: [myPdfParser, await createPdfParser()] });
66
+
67
+ import { createMistralOcrParser } from "@arnilo/prism-coding-tools/document-reader";
68
+ const ocr = createMistralOcrParser({
69
+ apiKey: hostKey, // never read from process.env
70
+ recordUsage: (u) => router.recordUsage({ /* Task 7 */ tokens: 0, costUsd: hostPrice(u) }),
71
+ });
72
+ const scanned = await createDocumentReader({ parsers: [ocr] }); // not in the default parser list
66
73
  ```
67
74
 
68
75
  ## Extension and configuration notes
69
76
 
70
77
  - Default parser wiring uses the optional peer dependencies `pdf-parse` (PDF) and `mammoth` (DOCX raw text). Both are declared optional (`peerDependenciesMeta`); `createDocumentReader` fails closed with a documented error at creation when a selected format's peer is absent — never at read time. Hosts pin parser versions (their CVE surface is the host's responsibility; parser advisory is reviewed at ship time).
78
+ - `createMistralOcrParser` is **not** a default parser. It POSTs `https://api.mistral.ai/v1/ocr` (`mistral-ocr-latest`) with inline `data:` URLs (`include_image_base64: false`). No Files API upload, so no remote cleanup. Host `documentUrl` values pass `assertSsrfAllowedUrl`. Extracted markdown is untrusted. Caps: 8 MiB / 32 pages / 60 s / 1 in-flight by default (hard 50 MiB / 10 000 pages / 180 s / 4). Pass `recordUsage` to admit cost through Task 7 accounting. `baseUrl` selects residency.
71
79
  - DOCX has no page concept in raw text: `pages` is always `1` and the page cap applies to PDF only; the text cap governs DOCX output.
72
80
  - The read tool re-checks `maxTextBytes` on results (parity with its text-page bounds check) and refuses reader output beyond it.
73
81
  - The adapter truncates over-cap text at a UTF-8 byte boundary (never splits a code point).
74
82
 
75
83
  ## Security and performance notes
76
84
 
77
- - No embedded-script execution, no macro evaluation, no external resource fetching — the peer raw-text surfaces are pure extractors, and the no-fetch property is enforced by an egress tripwire test.
85
+ - No embedded-script execution, no macro evaluation, no external resource fetching on the **default** parsers — the peer raw-text surfaces are pure extractors, and the no-fetch property is enforced by an egress tripwire test on `document-reader/index.js`. OCR is a separate module and only runs when the host passes that parser.
78
86
  - Decompression/size-bomb protection: the read tool stats and refuses files above `maxBytes` before loading; output is capped at `maxTextBytes`.
79
87
  - Extraction envelope (recorded in `scripts/budgets.json` `docReader`, measured 2026-08-11): a max-cap 1000-page PDF (288 KB) extracts in ~162 ms with ~17 MB heap delta; the gate asserts completion within the ceiling or documented refusal.
80
88
  - Parser code never receives a buffer whose format gate failed; random binaries never reach a parser.
package/docs/documents.md CHANGED
@@ -7,7 +7,7 @@ The `@arnilo/prism-office/documents` package provides specification-compliant, A
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.
9
9
  - **Draft-07 JSON Schema validation & slicing**: Full runtime structural validation with transitive closure slicing (`getDocumentModelSchema`) allowing LLM tools and agent prompts to extract minimal, self-contained sub-schemas (e.g. `doc.paragraph`, `doc.table`).
10
- - **Bidirectional round-trip fidelity**: Prism-generated documents parse back into structurally equivalent models verified against a per-kind equality specification.
10
+ - **Bidirectional round-trip fidelity**: Prism-generated documents parse back into structurally equivalent models verified against a per-kind equality specification. `importDocument` also reports ZIP parts the model drops (macros, comments, media, charts, OLE, pivots) instead of silently omitting them.
11
11
  - **Typed model patch engine**: Immutably applies `set`, `insert`, `remove`, and `move` operations to document blocks, worksheet cells, and presentation slides with schema re-validation and an interactive `createPatchHistory` undo/redo stack.
12
12
  - **Framework-neutral preview blocks & bounded HTML**: Generates structured snapshots (`PreviewBlock[]`) for native desktop/web UI grids and outline trees, as well as safe, sanitize-by-construction HTML fragments (`renderPreviewHtml`) guaranteed to contain no executable scripts, no active pseudo-protocols, and no external hyperlinks.
13
13
  - **Boundary text redaction**: Pluggable `SecretRedactor` hook to sanitize extracted text content (paragraphs, cells, notes, tables) at the parse boundary before models are returned.
@@ -31,7 +31,9 @@ Do **not** use this package for collaborative real-time editing (OT/CRDT), macro
31
31
  | --- | --- | --- |
32
32
  | `generateDocument` | `(model: DocumentModel, options: GenerateDocumentOptions) => Promise<GenerateDocumentResult>` | Translates a typed model into spec-compliant OOXML binary bytes (PK zip container) with a SHA-256 content hash. |
33
33
  | `parseDocument` | `(bytes: Uint8Array, options: ParseDocumentOptions) => Promise<DocumentModel>` | Verifies PK zip signature, enforces caps, translates OOXML parts, applies optional redaction, and returns a validated model. |
34
+ | `importDocument` | `(bytes: Uint8Array, options: ParseDocumentOptions) => Promise<ImportDocumentResult>` | Same parse plus a ZIP-name **fidelity** report of structures the Document Model drops (macros, comments, media, charts, OLE, pivots). `parseDocument` returns `.model` only. |
34
35
  | `patchDocument` | `(model: DocumentModel, patches: readonly DocumentPatch[], options?: PatchDocumentOptions) => DocumentModel` | Clones the model, applies typed structural patch operations, and validates the resulting model against Draft-07 schemas. |
36
+ | `diffDocument` | `(from: DocumentModel, to: DocumentModel, options?: DiffDocumentOptions) => DocumentDiff` | Structural paragraph/table/cell/slide diff. Decimal cells compare as canonical strings. Caps report `truncated` instead of unbounded walk. |
35
37
  | `createPatchHistory` | `(initialModel: DocumentModel) => PatchHistory` | Creates an interactive undo/redo history manager for host editing workflows. |
36
38
  | `renderPreviewBlocks` | `(model: DocumentModel, options?: PreviewBlocksOptions) => PreviewBlock[]` | Emits framework-neutral structured blocks (document outlines, bounded sheet grid chunks, slide summaries). |
37
39
  | `renderPreviewHtml` | `(model: DocumentModel, options?: PreviewHtmlOptions) => string` | Emits safe, bounded HTML fragments with all entities escaped and external URLs neutralized. |
@@ -139,6 +141,8 @@ console.log(`Generated DOCX (${bytes.byteLength} bytes, SHA-256: ${contentHash})
139
141
 
140
142
  // 3. Parse OOXML bytes back to a validated model
141
143
  const parsed = await parseDocument(bytes, { kind: "doc" });
144
+ const { model, fidelity } = await importDocument(bytes, { kind: "doc" });
145
+ // fidelity.issues[].code: macros | comments | media | … lost: dropped | approximated
142
146
 
143
147
  // 4. Apply typed model patches
144
148
  const patched = patchDocument(parsed, [
@@ -152,6 +156,9 @@ history.apply([{ op: "set", target: { title: true }, value: "Updated Review" }])
152
156
  console.log(history.canUndo()); // true
153
157
  const restored = history.undo(); // restored to "Executive Summary" state
154
158
 
159
+ import { diffDocument } from "@arnilo/prism-office/documents";
160
+ const diff = diffDocument(parsed, restored, { maxOps: 4096 });
161
+
155
162
  // 6. Generate structured preview blocks & safe HTML
156
163
  const blocks = renderPreviewBlocks(restored);
157
164
  const htmlSnippet = renderPreviewHtml(restored, { maxHtmlBytes: 256 * 1024 });
@@ -208,7 +215,8 @@ Financial worksheets often require exact decimal representations that JavaScript
208
215
 
209
216
  ## Related APIs
210
217
 
211
- - [`@arnilo/prism-coding-tools/document-reader`](./document-reader.md): Bounded literal text extraction from PDF and DOCX documents for coding agent tools.
218
+ - [`@arnilo/prism-coding-tools/document-reader`](./document-reader.md): Bounded literal text extraction from PDF and DOCX documents for coding agent tools; optional host-selected Mistral OCR parser.
212
219
  - [`@arnilo/prism-core/integrations/work`](./work-tools.md): Microsoft 365 and Google Workspace identity-scoped connectors.
213
220
  - [`@arnilo/prism-coding-tools/agent`](./coding-agent-tools.md): Coding tools and file operations.
214
221
  - [`@arnilo/prism-core/governance/observability`](./observability.md): OpenTelemetry instrumentation and trace adapters.
222
+ - [Work artifacts and review](work-artifacts-and-review.md): evidence-bound artifact citations and `evidenceDigest` approvals.
@@ -14,7 +14,7 @@
14
14
  | ERP messaging | `erpMessaging` | Transactional outbox/inbox markers plus bounded, tenant-scoped at-least-once dispatch (migration 004). |
15
15
  | Multi-party approvals | `createPostgresApprovalStore({ pool, schema, authority })` | Immutable approval requests, role/quorum decisions, revocation, bounded delegation, and atomic grant consumption (migration 005). |
16
16
 
17
- `createPostgresEnterpriseState()` opens a host-supplied or adapter-owned `pg` pool, verifies/applies checksum-protected enterprise migrations (`001_enterprise_state`, `002_tool_effects`, `003_router_reservations`, `004_erp_messaging`, `005_erp_approvals`), and returns those stores plus explicit cleanup and close operations. Importing it performs no I/O. It is separate from session/run persistence in [`@arnilo/prism-core/sessions/postgres`](postgres-persistence.md).
17
+ `createPostgresEnterpriseState()` opens a host-supplied or adapter-owned `pg` pool, verifies/applies checksum-protected enterprise migrations (`001_enterprise_state`, `002_tool_effects`, `003_router_reservations`, `004_erp_messaging`, `005_erp_approvals`, `006_aggregate_budgets`), and returns those stores plus explicit cleanup and close operations. Importing it performs no I/O. It is separate from session/run persistence in [`@arnilo/prism-core/sessions/postgres`](postgres-persistence.md).
18
18
 
19
19
  ## When to use it
20
20
 
@@ -90,7 +90,7 @@ Model-router state is asynchronous and owner/principal/provider/model scoped. Su
90
90
  }
91
91
  ```
92
92
 
93
- A migration creates `prism_policy_decisions`, `prism_evaluations`, `prism_work_idempotency`, three `prism_model_router_*` tables, `prism_erp_outbox`, `prism_erp_inbox`, `prism_erp_approvals`, and its separate `prism_enterprise_migrations` history. Migration `003_router_reservations` adds the nullable-by-default `reservations` JSONB column to `prism_model_router_budgets` (atomic reservation slots for router admission; 0.2.1 readers ignore it). Migration `004_erp_messaging` adds tenant/message and tenant/consumer/message primary keys plus claim, lease, and inbox indexes. Migration `005_erp_approvals` adds the one-row-per-request approval table (PK `tenant_id + id`, status check, decisions JSONB, status/created indexes). Startup serializes per-schema setup with an advisory transaction lock and rejects checksum or catalog drift rather than silently repairing it.
93
+ A migration creates `prism_policy_decisions`, `prism_evaluations`, `prism_work_idempotency`, three `prism_model_router_*` tables, `prism_erp_outbox`, `prism_erp_inbox`, `prism_erp_approvals`, and its separate `prism_enterprise_migrations` history. Migration `003_router_reservations` adds the nullable-by-default `reservations` JSONB column to `prism_model_router_budgets` (atomic reservation slots for router admission; 0.2.1 readers ignore it). Migration `004_erp_messaging` adds tenant/message and tenant/consumer/message primary keys plus claim, lease, and inbox indexes. Migration `005_erp_approvals` adds the one-row-per-request approval table (PK `tenant_id + id`, status check, decisions JSONB, status/created indexes). Migration `006_aggregate_budgets` adds `task_id TEXT NOT NULL DEFAULT ''` and `attributions JSONB NOT NULL DEFAULT '{}'::jsonb` to `prism_model_router_budgets` and creates partial index `prism_model_router_budgets_task_idx` on `(tenant_id, account_key, user_key, principal_id, task_id, window_ms) WHERE task_id <> ''`. Task-scoped budgets route through `provider = ':task:'` and `model = taskId`, grouping retries, fallbacks, child workers, compactions, embeddings, and paid tools into a single atomic budget ceiling with granular attributions. Startup serializes per-schema setup with an advisory transaction lock and rejects checksum or catalog drift rather than silently repairing it.
94
94
 
95
95
  ## Implementation example
96
96