@arnilo/prism 0.9.0 → 0.10.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +24 -1
- package/README.md +13 -12
- package/dist/agent-approval.d.ts +7 -1
- package/dist/agent-approval.js +15 -6
- package/dist/agent-run-lifecycle.js +19 -5
- package/dist/agent-run-state.d.ts +26 -5
- package/dist/agent-run-state.js +97 -1
- package/dist/agent-session/event-subscriber.d.ts +2 -0
- package/dist/agent-session/event-subscriber.js +3 -0
- package/dist/agent-session/session/assemble.js +156 -9
- package/dist/agent-session/session/persist.js +11 -5
- package/dist/agent-session/session/provider-round.js +54 -13
- package/dist/agent-session/session/tool-round.d.ts +2 -2
- package/dist/agent-session/session/tool-round.js +58 -5
- package/dist/agent-session/session/types.d.ts +20 -2
- package/dist/agent-session/session.d.ts +65 -4
- package/dist/agent-session/session.js +156 -16
- package/dist/context-budget.d.ts +11 -0
- package/dist/context-budget.js +33 -2
- package/dist/contracts-core/agent.d.ts +26 -5
- package/dist/contracts-core/extensions.d.ts +3 -0
- package/dist/contracts-core/guardrail-packs.d.ts +8 -3
- package/dist/contracts-core/loop.d.ts +36 -0
- package/dist/contracts-core/provider.d.ts +6 -1
- package/dist/contracts-core/run-limits.d.ts +10 -1
- package/dist/contracts-protocol.d.ts +6 -4
- package/dist/contracts-run-state.d.ts +37 -3
- package/dist/contributions.d.ts +2 -1
- package/dist/contributions.js +1 -0
- package/dist/extensions.d.ts +15 -1
- package/dist/extensions.js +68 -0
- package/dist/guardrail-packs/types.d.ts +10 -0
- package/dist/guardrail-packs/validation-respect.js +16 -0
- package/dist/guardrails.d.ts +42 -1
- package/dist/guardrails.js +124 -15
- package/dist/index.d.ts +6 -6
- package/dist/index.js +4 -4
- package/dist/middleware.d.ts +1 -1
- package/dist/run-bundle.d.ts +6 -1
- package/dist/run-bundle.js +4 -1
- package/dist/run-limits.js +13 -0
- package/dist/testing/prefix-stability-conformance.d.ts +29 -0
- package/dist/testing/prefix-stability-conformance.js +91 -23
- package/dist/tools.js +10 -3
- package/docs/agent-events.md +12 -8
- package/docs/agent-session-runtime.md +9 -6
- package/docs/caveman.md +1 -1
- package/docs/compaction-llm.md +2 -0
- package/docs/compaction-observational-memory.md +21 -1
- package/docs/durable-runs.md +4 -3
- package/docs/embeddings.md +5 -1
- package/docs/execution-timeline.md +3 -2
- package/docs/extensions.md +20 -3
- package/docs/guardrails.md +16 -6
- package/docs/hooks.md +282 -0
- package/docs/index.md +18 -15
- package/docs/input-and-prompt-assembly.md +1 -1
- package/docs/instruction-injection.md +1 -0
- package/docs/live-testing.md +3 -1
- package/docs/memory-fabric.md +28 -0
- package/docs/middleware-hooks.md +54 -4
- package/docs/migration.md +13 -0
- package/docs/options-index.md +3 -1
- package/docs/policy-and-audit.md +14 -1
- package/docs/prefix-stability-conformance.md +57 -7
- package/docs/provider-packages.md +20 -20
- package/docs/public-contracts.md +1 -0
- package/docs/rag.md +93 -6
- package/docs/release-and-install.md +42 -39
- package/docs/runs-and-usage.md +17 -8
- package/docs/scoped-agent-memory.md +17 -9
- package/docs/scoped-memory.md +138 -0
- package/docs/tools.md +1 -1
- package/docs/wiki.md +4 -2
- package/package.json +4 -2
package/docs/runs-and-usage.md
CHANGED
|
@@ -44,7 +44,7 @@ All methods may be sync or async (`void | Promise<void>`). The runtime awaits th
|
|
|
44
44
|
|
|
45
45
|
## Run limits
|
|
46
46
|
|
|
47
|
-
`RunLimits` bounds one `session.run()` across turns, provider attempts, tool rounds/calls, elapsed wall time, request/response bytes, token usage, and
|
|
47
|
+
`RunLimits` bounds one `session.run()` across turns, provider attempts, tool rounds/calls, elapsed wall time, request/response bytes, token usage, optional cost, and stop-hook continuations (`maxStopContinuations`, default 3). Configure defaults on `AgentConfig.limits`; `RunOptions.limits` can only narrow an agent-configured value.
|
|
48
48
|
|
|
49
49
|
```ts
|
|
50
50
|
await session.run("Summarize", {
|
|
@@ -80,16 +80,25 @@ const estimate = estimateMessageTokens(messages, "claude-sonnet-4.5"); // model
|
|
|
80
80
|
|
|
81
81
|
`MODEL_FAMILY_TOKENS` holds the chars/token ratio, per-message overhead, and confidence label per family (`anthropic`, `openai`, `google`, `deepseek`, `openrouter-generic`, `mistral`, `unknown`). `resolveModelFamily(modelId)` maps a model id or provider id to a table key; unmatched input resolves to `unknown`, whose row is the most conservative (highest estimated token count) and carries `confidence: "low"` / `lowConfidence: true`. Estimates are heuristics, not tokenizers: prose, fenced code, and CJK content are weighted separately, and every calibrated family is `confidence: "medium"` because Prism ships no real tokenizer. The estimator is pure — no network, no I/O, and no content retention.
|
|
82
82
|
|
|
83
|
+
Row provenance and recalibration: reference counts per family are frozen in `src/__tests__/fixtures/usage-calibration.json` — `openai` measured against `o200k_base` (dev-time oracle; no tokenizer ships), `anthropic`/`google`/`mistral` as their published chars/token guidance, and `deepseek`/`openrouter-generic` as row-basis values (no public count endpoint). `src/__tests__/usage-calibration.test.ts` fails when a shipped row drifts outside the recorded bands (prose ±12%, CJK ±20%, per-message overhead ±1 token). To re-measure, run `PRISM_LIVE_PROVIDER_TESTS=1` with `ANTHROPIC_API_KEY` and/or `GEMINI_API_KEY`/`GOOGLE_API_KEY` and `node --test scripts/usage-calibration-live.test.mjs` (matrix suite `calibration/vendor-count-tokens`): it posts the fixed corpus to `POST /v1/messages/count_tokens` and `POST /v1beta/models/{model}:countTokens`, asserts each shipped row against the measured count, and refreshes `docs/_evidence/phase103-family-token-calibration.md`. A measurement outside its band means updating the row in `src/usage-estimation.ts` and the fixture counts in one change.
|
|
84
|
+
|
|
83
85
|
### Automatic fallback (`AgentConfig.usageEstimation`)
|
|
84
86
|
|
|
85
|
-
`usageEstimation` is `"fallback"` (default) or `"
|
|
87
|
+
`usageEstimation` is `"fallback"` (default), `"off"`, or `"strict"`. With the default, a provider turn that reports no usage records one labeled estimate at the existing usage seam — no adapter changes:
|
|
86
88
|
|
|
87
|
-
- the `provider_turn_finished.usage` carries `{ inputTokens, estimated: true, confidence }`, and its `budgets.inputTokens`/`runInputUsed` use that estimate, so the attention axes and run limits from plans 086/087 work on non-reporting models;
|
|
89
|
+
- the `provider_turn_finished.usage` carries `{ inputTokens, estimated: true, confidence }`, and its `budgets.inputTokens`/`runInputUsed` use that estimate, with `budgets.inputTokensSource: "estimated"` labeling the figure (`"reported"` when the provider did report it), so the attention axes and run limits from plans 086/087 work on non-reporting models;
|
|
88
90
|
- ledger `appendUsage` rows (`scope: "provider_turn"` and the `run_total` aggregate) and `AgentRunResult.usage` keep `estimated: true` (plus `confidence`) — a billing surface can always tell an estimate from a report;
|
|
89
91
|
- estimates are **never priced**: the cost catalog is not consulted, and estimated usage carries no `cost`/`currency`, so a `maxCost` limit still fails closed instead of blocking on invented numbers;
|
|
90
92
|
- `"off"` leaves absent usage absent — no ledger row, no run total, never a zero.
|
|
93
|
+
- `"strict"` (plan 103 T5) records nothing and refuses the turn instead. The run ends with `AgentRunResult.error` `{ name: "UsageMissingError", code: "usage_missing" }`, the terminal `error` event carries the same info, exactly one provider attempt is made — the refusal is an observable failure, so no retry policy retries it. No ledger usage row, no `run_total` aggregate, and no cost-catalog lookup happen: `usage` stays absent, never zero. The refusal is a harness decision, not a provider failure: no `failureClass` is stamped, and `metadata.stopReason: "provider_error"` on that turn is the shared error-path stop, not a claim about the provider. Because the refusal is decided before the existing fail-closed `maxCost` breach, a cost-limited host gets `usage_missing` instead of a confusing `budget_exhausted` attribution. A provider that *fails* a turn without reporting usage is not refused — its own error (and the retry policy) still applies, so strict never masks a provider failure.
|
|
94
|
+
|
|
95
|
+
The estimate covers the turn's own request — messages plus tool declarations and context blocks — and prefers the most exact measurement that already exists (plan 103 T6), in this order:
|
|
96
|
+
|
|
97
|
+
1. **The budget pass's own measurement.** When `AgentConfig.contextBudget` sets `reportOmissions: true`, the request carries a `ContextBudgetReport`, and the fallback reuses its `keptTokens` verbatim — the same whole-request figure (post-eviction messages, context, skills, tool declarations) that decided evictions, so usage accounting and budget decisions can never show two different numbers for the same request. It is measured at budget time, so content added afterwards (tail segments, middleware edits) is not included, and `confidence` names the basis: `"high"` when a host `tokenEstimator` made the measurement, `"low"` for the built-in ÷4 basis (uncalibrated).
|
|
98
|
+
2. **The host tokenizer.** With `contextBudget.tokenEstimator` and no report, the fallback projects the request through that tokenizer — per message plus tool/context portions — using the assembler's own text shapes (the `measureAll` tool-list line and context-block text, never `JSON.stringify` of the schemas), validated exactly like the budget pass validates it. `confidence: "high"`: a host tokenizer's count is still an estimate (`estimated: true`), never `"reported"`.
|
|
99
|
+
3. **The family heuristic** (plan 091): the model id's family table for messages (per-message overhead included), and those same assembler text shapes for the tool/context portions.
|
|
91
100
|
|
|
92
|
-
|
|
101
|
+
All three paths keep `estimated: true` and are never priced. `usageEstimation: "off"` and `"strict"` never consult the report or the tokenizer — their behavior is decided before any measurement.
|
|
93
102
|
|
|
94
103
|
### `session.contextMeter()`
|
|
95
104
|
|
|
@@ -100,13 +109,13 @@ const meter = session.contextMeter();
|
|
|
100
109
|
// { inputTokens: 43_000, source: "estimated", inputCap: 200_000, runInputBudget: 500_000, usedRatio: 0.215 }
|
|
101
110
|
```
|
|
102
111
|
|
|
103
|
-
`inputTokens` is the latest provider turn's input tokens — `source: "reported"` when the provider reported them, `"estimated"` when they are the labeled fallback (or, before any provider turn in the session, an estimate of stored history, so a fresh non-reporting model still shows a working meter). `inputCap` is resolved exactly like `provider_turn_finished.budgets.inputCap` (model window minus output reserve minus `attentionCompiler.reserveTokens`), `runInputBudget` is `RunLimits.maxInputTokens` while a run is active, and `usedRatio` is `inputTokens / inputCap`. Cap/budget/ratio are omitted when the model or run cannot derive them. The meter is never billing and never rewrites reported usage; `compact()` drops the pre-compaction reading so the next read re-estimates.
|
|
112
|
+
`inputTokens` is the latest provider turn's input tokens — `source: "reported"` when the provider reported them, `"estimated"` when they are the labeled fallback (or, before any provider turn in the session, an estimate of stored history, so a fresh non-reporting model still shows a working meter). `inputCap` is resolved exactly like `provider_turn_finished.budgets.inputCap` (model window minus output reserve minus `attentionCompiler.reserveTokens`), `runInputBudget` is `RunLimits.maxInputTokens` while a run is active, and `usedRatio` is `inputTokens / inputCap`. Cap/budget/ratio are omitted when the model or run cannot derive them. The meter is never billing and never rewrites reported usage; `compact()` drops the pre-compaction reading so the next read re-estimates. Reads are cached — the same frozen object is returned until the history changes (an appended entry, a steer push, a compaction), the branch leaf moves, or the active run's identity changes, so polling the meter per frame costs one estimate per mutation instead of one per read (measured: 1,000 reads over a 200k-character history in 0.11 ms, against ≈0.47 ms for one uncached estimate).
|
|
104
113
|
|
|
105
114
|
## Clean stops and stop reasons
|
|
106
115
|
|
|
107
|
-
A run can end without an error but also without the model finishing its thought: a host `RunOptions.turnPolicy.stop`, a `turnPolicy.maxTurns` cap,
|
|
116
|
+
A run can end without an error but also without the model finishing its thought: a host `RunOptions.turnPolicy.stop`, a `turnPolicy.maxTurns` cap, a loop ceiling, or the stop-hook continuation cap. `AgentRunResult.stopReason` names that outcome — `"host_policy"` for a host policy stop, `"hook_limit"` when `limits.maxStopContinuations` refused a continuation, `"turn_limit"`, `"token_limit"`, or `"refusal"` for loop ceilings — with `turnPolicy.stop`'s own string in `stopDetail`. A natural end carries neither field, so hosts that only care about "did it stop early?" check truthiness. The same values ride the emitted `agent_finished` event (as `finishReason`/`stopDetail`), the finish `RunRecord`, and the projected [Execution Timeline](execution-timeline.md).
|
|
108
117
|
|
|
109
|
-
A `host_policy` stop is terminal for the run yet resumable: with `runState: { checkpointPolicy: "every-turn" }` the stopped state keeps its frontier, and `resumeAgentRun(..., { decision: "continue" })` picks the loop up at the boundary. Every other terminal state is final. See [Agent loops § Turn policy](agent-loops.md#turn-policy).
|
|
118
|
+
A `host_policy` stop is terminal for the run yet resumable: with `runState: { checkpointPolicy: "every-turn" }` the stopped state keeps its frontier, and `resumeAgentRun(..., { decision: "continue" })` picks the loop up at the boundary. A `hook_limit` stop is resumable the same way. Every other terminal state is final. See [Agent loops § Turn policy](agent-loops.md#turn-policy) and [Hooks](hooks.md).
|
|
110
119
|
|
|
111
120
|
## Provider failure classes
|
|
112
121
|
|
|
@@ -135,7 +144,7 @@ The adapter receives these record shapes:
|
|
|
135
144
|
| `status` | `queued` \| `running` \| `suspended` \| `denied` \| `succeeded` \| `failed` \| `aborted`. |
|
|
136
145
|
| `startedAt` / `finishedAt` | ISO timestamps. |
|
|
137
146
|
| `abortReason` | Set when status is `aborted`. |
|
|
138
|
-
| `stopReason` | Why the loop stopped cleanly instead of reaching a natural end: `host_policy` (`RunOptions.turnPolicy.stop`), `turn_limit`, `token_limit`, or `refusal`. Absent on a natural end. |
|
|
147
|
+
| `stopReason` | Why the loop stopped cleanly instead of reaching a natural end: `host_policy` (`RunOptions.turnPolicy.stop`), `hook_limit` (stop-hook continuation cap), `turn_limit`, `token_limit`, or `refusal`. Absent on a natural end. |
|
|
139
148
|
| `stopDetail` | Host stop detail from `turnPolicy.stop` (≤256 bytes, redacted). |
|
|
140
149
|
| `error` | `ErrorInfo` when status is `failed`. |
|
|
141
150
|
| `tenantId` / `accountId` / `userId` | From active ownership scope. |
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# Scoped persistent agent memory — design concept
|
|
2
2
|
|
|
3
|
-
Status: **concept
|
|
3
|
+
Status: **concept**. The implemented contract is [Scoped memory](scoped-memory.md) (`@arnilo/prism-memory/scoped`). This page is the design rationale for workspace-scoped persistent agent memory — durable facts and procedures that are recorded, updated, and used automatically during agentic work. Terminology deliberately aligns with the existing Prism memory surfaces ([memory fabric](memory-fabric.md), [observational memory](compaction-observational-memory.md), [working and semantic memory](working-and-semantic-memory.md)) — see [Relationship to existing Prism memory surfaces](#relationship-to-existing-prism-memory-surfaces).
|
|
4
4
|
|
|
5
5
|
## Problem and goals
|
|
6
6
|
|
|
@@ -98,7 +98,7 @@ links: [rec_deploy_rollback, rec_pg_pool]
|
|
|
98
98
|
|
|
99
99
|
Rationale: human override and review are non-negotiable for professional work — diffs, blame, and PR review come free. This mirrors the Hermes `journey edit/delete` lesson (users *must* be able to prune) and generalizes it to full version control.
|
|
100
100
|
|
|
101
|
-
**Prism realization.** In the composed Prism stack (next section) the [memory fabric](memory-fabric.md) is the source of truth for records and the session store/observational ledger owns episodes; `<workspace>/.memory/`
|
|
101
|
+
**Prism realization.** In the composed Prism stack (next section) the [memory fabric](memory-fabric.md) is the source of truth for records and the session store/observational ledger owns episodes; usage/status/staging live in a gitignored JSON ledger (`<workspace>/.memory/state.json`); `<workspace>/.memory/` markdown is a **git audit mirror** — a rendered export of fabric notes + ledger counters for diff/review — not a second storage engine. One write path, two views.
|
|
102
102
|
|
|
103
103
|
### Write path — reflect, adjudicate, gate
|
|
104
104
|
|
|
@@ -132,7 +132,8 @@ An idle/nightly consolidation pass (cheap model, off the interaction path):
|
|
|
132
132
|
|
|
133
133
|
### Trust boundary
|
|
134
134
|
|
|
135
|
-
- **
|
|
135
|
+
- **Secret redaction** of record content via the shipped needle redactor (`createSecretRedactor` / observational `secrets` / working-memory `redactJson`).
|
|
136
|
+
- **Injection/exfiltration scanning** of record content before any prompt injection (patterns, invisible Unicode) — Hermes does this for `MEMORY.md`; extend to all records. This is a new scoped primitive (`scanScopedMemoryContent`); it is not the secret redactor.
|
|
136
137
|
- **Staged approval.** Writes may be staged for human review (`write_approval`-style). Default: off for personal scopes, on for team/professional scopes.
|
|
137
138
|
- **Scope isolation.** Records never leak across workspace roots; the global user layer is opt-in per record.
|
|
138
139
|
- **Provenance on every record.** Any memory-driven decision can be traced to the session and turn that produced the record (same philosophy as observational memory's source-backed ids and the recall path).
|
|
@@ -171,15 +172,18 @@ The placement principle: **the scoped layer is a policy and lifecycle layer, not
|
|
|
171
172
|
| Episodic ledger | [Observational memory](compaction-observational-memory.md) | source-backed observations/reflections (12-hex ids, `sourceEntryIds`), exact-id recall and branch pages, optional work-scope index; observer/reflector/dropper workers are the only writers | no semantic retrieval, no wholesale prompt injection, no downstream re-observation |
|
|
172
173
|
| Memory engines | [Working and semantic memory](working-and-semantic-memory.md) | `Embedder`/vector/working-store contracts, consent lifecycle, lineage invalidation, importance, recall scoring | no policy |
|
|
173
174
|
| Durable records | [Memory fabric](memory-fabric.md) | typed notes (`fact`/`procedure`/`file`/`working`/`episode`), validity windows, consolidation folding, linker/evolution workers, five governed tools, file jail, context provider, `forget`/legal hold | no autonomy — every write is an explicit caller decision |
|
|
174
|
-
| Policy + lifecycle |
|
|
175
|
+
| Policy + lifecycle | [Scoped memory](scoped-memory.md) (`@arnilo/prism-memory/scoped`) | conservative post-run writer, promotion ladder, usage-decay GC, abstain floor + activation budget, workspace-root scope identity, git audit mirror, usage/status/staging JSON ledger | no store, no engine, no context-block type, no second write path |
|
|
176
|
+
| Knowledge compiler | [LLM wiki](wiki.md) (`@arnilo/prism-memory/wiki`) | regenerable `.wiki/` pages with line-anchored citations over raw sources; `wiki_ingest` / `wiki_record_insight` | session-derived experience (that is scoped memory); it is not a memory store |
|
|
177
|
+
|
|
178
|
+
**Wiki boundary / routing.** Wiki compiles *source-cited knowledge* (files, docs, papers — regenerable, `file://…#Lxx-Lyy`). Scoped memory holds *session-derived experience* (primary fabric records, provenance `sourceEntryIds`). No storage overlap: wiki writes `.wiki/` + `raw/ingest/`; scoped writes fabric notes + `<scopeRoot>/.memory/state.json` + the git mirror. Route source-cited material to wiki; route session-derived experience to scoped policy. The post-run reviewer must not file a wiki-pageable insight as a scoped fact.
|
|
175
179
|
|
|
176
180
|
Two invariants carry over unchanged: observational memory stays **episodic** (promotion out of the ledger is an explicit host write — fabric's `promotedFrom` over a closed work scope), and the fabric never widens consent or visibility.
|
|
177
181
|
|
|
178
182
|
### Ideal composition for a persistent-memory agent
|
|
179
183
|
|
|
180
184
|
```ts
|
|
181
|
-
// 1. Engines — workspace root
|
|
182
|
-
const memory = createMemory({ tenantId: host, resourceId: workspaceRoot, embedder, stores });
|
|
185
|
+
// 1. Engines — workspace root is resourceId; threadId is a stable silo id (not the session id)
|
|
186
|
+
const memory = createMemory({ tenantId: host, resourceId: workspaceRoot, threadId: "scoped", embedder, stores });
|
|
183
187
|
// 2. Durable records — folding, links, evolution on by policy
|
|
184
188
|
const fabric = createMemoryFabric({ memory, observational, consolidate: { threshold: 0.85 },
|
|
185
189
|
linker: { enabled: true }, evolution: { enabled: true } });
|
|
@@ -188,7 +192,8 @@ om.attach(session);
|
|
|
188
192
|
// 4. Gate fabric tools + workers to this session
|
|
189
193
|
fabric.attach(session);
|
|
190
194
|
// 5. Injection: ONLY the bounded working facts block reaches the prompt
|
|
191
|
-
registries.contextProviders.register("memory-fabric",
|
|
195
|
+
registries.contextProviders.register("memory-fabric",
|
|
196
|
+
fabric.createContextProvider({ includeWorking: true, includeSemantic: false }));
|
|
192
197
|
const agent = await resolveAgentDefinition(
|
|
193
198
|
{ name: "assistant", model, context: ["memory-fabric"], tools: ["memory.recall"] },
|
|
194
199
|
{ registries, providerSource });
|
|
@@ -212,7 +217,7 @@ End-to-end flow:
|
|
|
212
217
|
| Adjudicate ADD/UPDATE/supersede | fabric consolidation folding (cosine threshold 0.85) |
|
|
213
218
|
| Close contradicted facts | fabric `validTo` + `supersedes` |
|
|
214
219
|
| Link + evolve neighbors | fabric linker/evolution workers |
|
|
215
|
-
| Status `candidate`, staging/approval | scoped
|
|
220
|
+
| Status `candidate`, usage counters, staging/approval | scoped JSON ledger (`<scopeRoot>/.memory/state.json`) — not fabric note metadata |
|
|
216
221
|
|
|
217
222
|
| Read-path step | Owner |
|
|
218
223
|
| --- | --- |
|
|
@@ -240,6 +245,8 @@ How the research findings land on shipped surfaces versus policy added by this c
|
|
|
240
245
|
| Off-path consolidation | Letta sleep-time | — (idle-job placement) | GC/promotion/facts consolidation on a cheap model |
|
|
241
246
|
| Tiny always-loaded layer, disclosure on demand | Anthropic skills | recall tools + working block only | abstain floor + top-3 activation budget |
|
|
242
247
|
| Hybrid BM25 + cosine + graph walk | Zep/Graphiti | partial: embedding score + link traversal; lexical on branch search | fusing lexical into one recall — open question |
|
|
248
|
+
| Known-secret redaction | runtime / OM | `createSecretRedactor`, observational `secrets`, working `redactJson` | — |
|
|
249
|
+
| Injection / exfil / invisible Unicode scan | Hermes MEMORY.md | — (redactor is needle-only) | `scanScopedMemoryContent` (pure patterns) |
|
|
243
250
|
|
|
244
251
|
### Deliberate deviations
|
|
245
252
|
|
|
@@ -258,5 +265,6 @@ How the research findings land on shipped surfaces versus policy added by this c
|
|
|
258
265
|
|
|
259
266
|
- Exact promotion thresholds (N reuses, decay τ, similarity floors) — must be empirically tuned per workload class (coding vs. research vs. professional ops).
|
|
260
267
|
- Whether the global user-profile layer reuses the working-memory store or a separate facts silo.
|
|
261
|
-
- Evaluation harness: reuse the existing evaluations/trajectory tooling vs. a purpose-built memory replay suite.
|
|
262
268
|
- Team-scope semantics: per-user silos sharing one workspace root, or one shared silo with author-attributed records.
|
|
269
|
+
|
|
270
|
+
Closed at primitive review: evaluation harness reuses `@arnilo/prism-core/governance/evals` scorer/dataset contracts and adds only scoped fixtures + `runScopedMemoryEval`.
|
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
# Scoped memory (`@arnilo/prism-memory/scoped`)
|
|
2
|
+
|
|
3
|
+
## What it does
|
|
4
|
+
|
|
5
|
+
`createScopedMemoryPolicy()` is a **policy and lifecycle layer** over stores a host already configured with `createMemory()` and `createMemoryFabric()`. It does not add a fifth store. It owns workspace-root identity, a conservative post-run writer, a candidate→verified promotion ladder, usage-decay GC proposals, an abstain floor plus activation budget on recall, a bounded working facts block, a git audit mirror, and a JSON usage/staging ledger at `<scopeRoot>/.memory/state.json`.
|
|
6
|
+
|
|
7
|
+
**Routing.** Source-cited knowledge belongs in [`@arnilo/prism-memory/wiki`](wiki.md) (regenerable, line-anchored). Session-derived experience belongs in scoped memory (primary fabric records with `sourceEntryIds` provenance). The post-run reviewer must not duplicate a wiki-pageable insight as a scoped fact.
|
|
8
|
+
|
|
9
|
+
## When to use it
|
|
10
|
+
|
|
11
|
+
Use it when a host wants durable facts and procedures to accumulate across sessions for one workspace without injecting the whole library into the prompt. Leave it off when an eval A/B shows no win-rate lift. Do not use it as a wiki, an observational-memory replacement, or a second vector store.
|
|
12
|
+
|
|
13
|
+
## Inputs / request
|
|
14
|
+
|
|
15
|
+
### `createScopedMemoryPolicy(options)`
|
|
16
|
+
|
|
17
|
+
| Field | Type | Required | Default | Description |
|
|
18
|
+
| :--- | :--- | :--- | :--- | :--- |
|
|
19
|
+
| `memory` | `Memory` | yes | — | `createMemory()` instance. `scope.resourceId` must equal `resolve(scopeRoot)`; `scope.threadId` is required (stable workspace silo, not a session id). |
|
|
20
|
+
| `fabric` | `MemoryFabric` | yes | — | `createMemoryFabric()` over that memory. |
|
|
21
|
+
| `scopeRoot` | `string` | yes | — | Workspace root. Create is inert: no attach, no files. |
|
|
22
|
+
| `policy` | `ScopedMemoryPolicyKnobs` | no | see knobs | Tuning. Unknown fields ignored; invalid values fail closed. |
|
|
23
|
+
|
|
24
|
+
### Knobs (`policy`)
|
|
25
|
+
|
|
26
|
+
| Knob | Default | Role |
|
|
27
|
+
| :--- | :--- | :--- |
|
|
28
|
+
| `promotion.reuseThreshold` | `2` | Flip ledger `candidate` → `verified` after this many successful recall uses. No fabric rewrite. |
|
|
29
|
+
| `decay.tauDays` | `30` | Time constant for `score = fabricScore × exp(−ageDays/tauDays) × (1 + ln(1 + uses))`. |
|
|
30
|
+
| `decay.candidateArchiveDays` | `30` | Unused candidates this old become GC archive proposals. |
|
|
31
|
+
| `activation.topK` | `3` | Recall budget after the floor. |
|
|
32
|
+
| `activation.minSimilarity` | `0.35` | Abstain floor (`hit.similarity ?? hit.score`). |
|
|
33
|
+
| `facts.block` | `"facts"` | Working-block label for `rememberFact`. |
|
|
34
|
+
| `facts.maxChars` | `2200` | Overflow throws `MemoryLimitError` (`consolidate first`). |
|
|
35
|
+
| `approval.default` | `"off"` | `"off"` writes reviewer proposals immediately; `"staged"` queues them on `pending()`. |
|
|
36
|
+
|
|
37
|
+
### Methods
|
|
38
|
+
|
|
39
|
+
| Method | Input | Notes |
|
|
40
|
+
| :--- | :--- | :--- |
|
|
41
|
+
| `recall(query, opts?)` | query string | Wraps `fabric.recall` with oversample, floor, topK, usage increment. |
|
|
42
|
+
| `reviewSession(digest, { reviewer, prompt? })` | string or entry array | One fake/real reviewer call. Strict JSON: `{kind, content, sourceEntryIds}` only; `kind` is `fact` or `procedure`; `sourceEntryIds` non-empty. Garbage → zero writes, no throw. |
|
|
43
|
+
| `promotionPass()` | — | Ledger status only. |
|
|
44
|
+
| `gcPass()` | — | Proposes archives onto `pending()`. Never deletes. Skips `legal_hold`. |
|
|
45
|
+
| `health()` | — | Counts + conversion/activation/duplication rates. |
|
|
46
|
+
| `rememberFact(text)` | non-empty string | Appends the facts block after `scanScopedMemoryContent`. |
|
|
47
|
+
| `pending()` | — | Reviewer stages and GC archives. |
|
|
48
|
+
| `approve(id)` / `reject(id)` | pending id | Approve writes/forgets; reject drops the proposal (archive restore uses `prevStatus`). |
|
|
49
|
+
| `renderMirror()` | — | Deterministic markdown under `<scopeRoot>/.memory/`; gitignores `state.json`. |
|
|
50
|
+
|
|
51
|
+
### Eval and scan
|
|
52
|
+
|
|
53
|
+
- `runScopedMemoryEval({ fixtures, fakeProvider? })` — fixture-only. Rejects `memory` / `policy` / `fabric` / `vectorStore`. Default fake answers from recall context or `"unknown"`. Reports A/B `winRate`, Precision@3 (mean `\|relevant ∩ top3\| / 3`, alert below floor 0.5), LoCoMo (`failedClosed` when `expectedId` was never seeded), and `health()`.
|
|
54
|
+
- `createScopedMemoryHealthCommand({ policy })` — `scoped-memory:health`.
|
|
55
|
+
- `scanScopedMemoryContent(text)` — `{ ok: true }` or `{ ok: false, class: "prompt-injection" \| "exfil" \| "invisible-unicode" }`. Writes fail closed on a match.
|
|
56
|
+
- `scoreScopedHit(score, uses, ageDays, tauDays)` — the read-policy formula.
|
|
57
|
+
|
|
58
|
+
## Outputs / response / events
|
|
59
|
+
|
|
60
|
+
Create returns a frozen `ScopedMemoryPolicy` (`scopeRoot`, `settings`, methods). No events; fabric/memory events are unchanged.
|
|
61
|
+
|
|
62
|
+
`recall` → `{ hits, abstained, explain }`. Empty hits + `abstained: true` when nothing clears the floor.
|
|
63
|
+
|
|
64
|
+
`reviewSession` → `{ proposed, written, staged, status: { candidate } }`.
|
|
65
|
+
|
|
66
|
+
`promotionPass` → `{ promoted }`. `gcPass` → `{ proposed, archived }` (`archived` stays 0 until the host `approve`s).
|
|
67
|
+
|
|
68
|
+
`health` → `{ notes: { candidate, verified, archived }, conversionRate, activationRate, duplicationRate }`.
|
|
69
|
+
|
|
70
|
+
`rememberFact` → `void` or `MemoryLimitError` / `MemoryValidationError`. `renderMirror` writes `notes/<id>.md`, `facts.md`, `.gitignore`.
|
|
71
|
+
|
|
72
|
+
## Request/response example
|
|
73
|
+
|
|
74
|
+
```json
|
|
75
|
+
{
|
|
76
|
+
"scopeRoot": "/tmp/workspace",
|
|
77
|
+
"policy": {
|
|
78
|
+
"promotion": { "reuseThreshold": 2 },
|
|
79
|
+
"decay": { "tauDays": 30, "candidateArchiveDays": 30 },
|
|
80
|
+
"activation": { "topK": 3, "minSimilarity": 0.35 },
|
|
81
|
+
"facts": { "block": "facts", "maxChars": 2200 },
|
|
82
|
+
"approval": { "default": "off" }
|
|
83
|
+
},
|
|
84
|
+
"recall": {
|
|
85
|
+
"hits": [{ "id": "aaaaaaaaaaaa", "kind": "fact", "content": "SSH listens on 2222", "score": 0.91 }],
|
|
86
|
+
"abstained": false
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
## Implementation example
|
|
92
|
+
|
|
93
|
+
```ts
|
|
94
|
+
import { createHashEmbedder, createMemory } from "@arnilo/prism-memory";
|
|
95
|
+
import { createMemoryFabric } from "@arnilo/prism-memory/fabric";
|
|
96
|
+
import { createScopedMemoryPolicy } from "@arnilo/prism-memory/scoped";
|
|
97
|
+
|
|
98
|
+
const memory = createMemory({
|
|
99
|
+
tenantId: "host",
|
|
100
|
+
resourceId: workspaceRoot,
|
|
101
|
+
threadId: "scoped",
|
|
102
|
+
embedder: createHashEmbedder({ dimensions: 8 }),
|
|
103
|
+
});
|
|
104
|
+
const fabric = createMemoryFabric({ memory, consolidate: false });
|
|
105
|
+
const policy = createScopedMemoryPolicy({ memory, fabric, scopeRoot: workspaceRoot });
|
|
106
|
+
|
|
107
|
+
await policy.rememberFact("SSH jump host listens on 2222");
|
|
108
|
+
await policy.reviewSession(digest, { reviewer });
|
|
109
|
+
const { hits, abstained } = await policy.recall("SSH 2222");
|
|
110
|
+
await policy.promotionPass();
|
|
111
|
+
await policy.gcPass();
|
|
112
|
+
await policy.renderMirror();
|
|
113
|
+
```
|
|
114
|
+
|
|
115
|
+
Runnable walk (scope guard → overflow → review → recall → promotion → GC approve → mirror): `examples/scoped-memory.ts`.
|
|
116
|
+
|
|
117
|
+
Inject only the facts block: `fabric.createContextProvider({ includeWorking: true, includeSemantic: false })`.
|
|
118
|
+
|
|
119
|
+
## Extension and configuration notes
|
|
120
|
+
|
|
121
|
+
The policy is off until the host constructs it. `reviewer` is host-supplied (LLM or fake). `approval.default: "staged"` makes reviewer writes host-gated. `createScopedMemoryHealthCommand` follows the observational `om:status` command factory. Eval fixtures live next to the runner; import eval APIs from `@arnilo/prism-memory/scoped` (no `./scoped/eval` subpath).
|
|
122
|
+
|
|
123
|
+
## Security and performance notes
|
|
124
|
+
|
|
125
|
+
- **Scope identity:** create throws if `resourceId !== resolve(scopeRoot)` or `threadId` is missing. Observational session mismatch stays `fabric.attach`.
|
|
126
|
+
- **Writes fail closed:** `scanScopedMemoryContent` then the memory redactor. Matches name a class; payloads are not logged.
|
|
127
|
+
- **GC never silently deletes.** `legal_hold` notes are not proposed. Host `approve` calls `fabric.forget`.
|
|
128
|
+
- **Sizing:** one reviewer call per run; ledger I/O per recall; off by default. Activation is top-3 after the floor, not the whole library.
|
|
129
|
+
- **Mirror** skips scan failures. `state.json` is gitignored; markdown is the audit copy, not a second engine.
|
|
130
|
+
|
|
131
|
+
## Related APIs
|
|
132
|
+
|
|
133
|
+
- [Memory fabric](memory-fabric.md): typed notes this policy writes and recalls through.
|
|
134
|
+
- [Working and semantic memory](working-and-semantic-memory.md): `createMemory`, consent, redaction, `exportMemory`.
|
|
135
|
+
- [Observational memory](compaction-observational-memory.md): episodic ledger; scoped facts keep `sourceEntryIds`.
|
|
136
|
+
- [LLM wiki](wiki.md): source-cited knowledge compiler — not session-derived experience.
|
|
137
|
+
- [Scoped agent memory design concept](scoped-agent-memory.md): rationale, Hermes case study, research basis.
|
|
138
|
+
- [Evaluations](evaluations.md): trajectory/outcome scorers; scoped eval is fixture-only on this subpath.
|
package/docs/tools.md
CHANGED
|
@@ -297,7 +297,7 @@ Limits (mirroring the skill-disclosure DEFAULT/HARD cap pattern):
|
|
|
297
297
|
|
|
298
298
|
## Guardrails
|
|
299
299
|
|
|
300
|
-
`DispatchToolCallOptions.guardrails` evaluates `tool_input` after `tool_call` middleware normalization and before lookup, permission, validation, execution policy, or side effect. `tool_output` evaluates raw completed results before redaction, event emission, ledger rows, and transcript append. A block returns a blocked result; tripwire fails the enclosing run. See [Guardrails](guardrails.md).
|
|
300
|
+
`DispatchToolCallOptions.guardrails` evaluates `tool_input` after `tool_call` middleware normalization and before lookup, permission, validation, execution policy, or side effect. `tool_output` evaluates raw completed results before redaction, event emission, ledger rows, and transcript append. A block returns a blocked result; tripwire fails the enclosing run. A blocked call's `error.message` names the refusing rule — `Blocked by guardrail rule pack:<pack>/<rule>`, plus the pack's reason when it set one, bounded to 200 bytes and redacted; a hand-written guardrail keeps `Tool call blocked by guardrail` / `Tool result blocked by guardrail`. The `tool_execution_blocked` event carries the same text in `error.message` while its `reason` stays the machine code. See [Guardrails](guardrails.md).
|
|
301
301
|
|
|
302
302
|
## Related APIs
|
|
303
303
|
|
package/docs/wiki.md
CHANGED
|
@@ -45,7 +45,7 @@ The Karpathy LLM Wiki pattern is structured into 3 distinct tiers:
|
|
|
45
45
|
|
|
46
46
|
- `/wiki-init`: Scaffolds `.wiki/`, instantiates `SCHEMA.md`, `index.md`, and `log.md`, deploys skills, and adds the `qmd` collection.
|
|
47
47
|
- `/wiki-refresh`: Detects modified source files via SHA-256 Merkle diffing, compiles updates to affected entity pages, reconciles contradictions in `log.md`, and runs `qmd update`.
|
|
48
|
-
- `/wiki-lint`: Checks OKF frontmatter (`type`, ISO `generated.at`), leftover `[[wikilinks]]`, unresolved relative markdown links, dead line anchors, and
|
|
48
|
+
- `/wiki-lint`: Checks OKF frontmatter (`type`, ISO `generated.at`), leftover `[[wikilinks]]`, unresolved relative markdown links, dead line anchors, orphan pages, and pruned sources (an entity page whose raw sources were retired or deleted — reported for re-filing, never a failure).
|
|
49
49
|
- `/wiki-ingest`: `{ text?, path?, url?, title? }` — stages one external source into `raw/ingest/<utc>-<slug>/` (`source.*` original + `extract.md`), then returns a brief (staged paths, extract preview, source URL when applicable, Karpathy filing checklist). When the host injects `drivers`, the command calls `drivers.startRun(brief, { activeSkills: ["wiki-maintainer"] })` so the maintainer skill files the source into the wiki; without drivers it stages only and reports `runStarted: false`. Results are labeled `metadata.trust: "untrusted_external"`.
|
|
50
50
|
|
|
51
51
|
### Standalone CLI Commands
|
|
@@ -57,7 +57,7 @@ npx prism-wiki init --profile codebase
|
|
|
57
57
|
# Refresh wiki after code edits
|
|
58
58
|
npx prism-wiki refresh
|
|
59
59
|
|
|
60
|
-
# Check wiki health
|
|
60
|
+
# Check wiki health: dead anchors, broken links, orphans, pruned sources
|
|
61
61
|
npx prism-wiki lint
|
|
62
62
|
|
|
63
63
|
# Search wiki from terminal
|
|
@@ -71,6 +71,8 @@ npx prism-wiki ingest --path notes/paper.pdf --title "Paper"
|
|
|
71
71
|
npx prism-wiki ingest --url https://example.com/rfc.pdf # → exit 1
|
|
72
72
|
```
|
|
73
73
|
|
|
74
|
+
Lint output is one summary line plus per-issue detail, and the report shape is `{ deadAnchors, brokenLinks, orphans, gaps, prunedSources, ok }`. **Pruned sources are not a failure**: a page whose raw sources were retired (`retireWikiSources`), re-pointed to a path that does not exist, or deleted out-of-band is maintainer work, so it is reported as `prunedSources` entries (`{ page, missing }`, both workspace-relative, capped to the first few paths in command/CLI text) while the health check stays `ok` and the CLI exits 0. The `wiki-maintainer` skill covers the response: re-read the surviving sources and re-file the page, or delete it when none remain.
|
|
75
|
+
|
|
74
76
|
## Outputs / response / events
|
|
75
77
|
|
|
76
78
|
- `wiki_search` returns a structured markdown payload containing section breadcrumbs, conceptual summaries, and clickable source line links (`file:///path#Lxx-Lyy`).
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@arnilo/prism",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.10.0",
|
|
4
4
|
"description": "Agent harness for AI providers, agents, sessions, and tools.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/index.js",
|
|
@@ -135,7 +135,8 @@
|
|
|
135
135
|
"packages/prism-coding-tools",
|
|
136
136
|
"packages/ag-ui",
|
|
137
137
|
"packages/web-tools",
|
|
138
|
-
"packages/acp-agent"
|
|
138
|
+
"packages/acp-agent",
|
|
139
|
+
"packages/hooks"
|
|
139
140
|
],
|
|
140
141
|
"scripts": {
|
|
141
142
|
"build:core": "node scripts/with-build-lock.mjs tsc",
|
|
@@ -155,6 +156,7 @@
|
|
|
155
156
|
"test:postgres:run": "node scripts/require-postgres-url.mjs && npm run test:postgres --workspace @arnilo/prism-core --if-present && npm run test:postgres --workspace @arnilo/prism-memory && npm run test:postgres --workspace @arnilo/prism-channels && node --test scripts/phase7-conformance.test.mjs scripts/phase12-restart-recovery.test.mjs scripts/phase22-conformance.test.mjs",
|
|
156
157
|
"test:nats": "node scripts/require-nats-url.mjs && npm run test:nats --workspace @arnilo/prism-core --if-present",
|
|
157
158
|
"release:dry-run": "npm run sdk:ready",
|
|
159
|
+
"post-publish:smoke": "node scripts/post-publish-smoke.mjs",
|
|
158
160
|
"release:check": "node scripts/release.mjs check",
|
|
159
161
|
"release:publish": "node scripts/release.mjs publish",
|
|
160
162
|
"release:evidence": "node scripts/release-skip-manifest.mjs",
|