@arnilo/prism 0.7.0 → 0.9.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +73 -0
- package/README.md +12 -11
- package/dist/agent-approval.d.ts +15 -2
- package/dist/agent-approval.js +5 -1
- package/dist/agent-event-source.d.ts +9 -1
- package/dist/agent-event-source.js +10 -3
- package/dist/agent-loops.js +7 -4
- package/dist/agent-run-lifecycle.d.ts +15 -1
- package/dist/agent-run-lifecycle.js +91 -10
- package/dist/agent-run-state.d.ts +34 -2
- package/dist/agent-run-state.js +68 -6
- package/dist/agent-session/helpers.js +20 -1
- package/dist/agent-session/session/assemble.js +250 -27
- package/dist/agent-session/session/persist.d.ts +27 -0
- package/dist/agent-session/session/persist.js +94 -12
- package/dist/agent-session/session/provider-round.d.ts +14 -4
- package/dist/agent-session/session/provider-round.js +197 -25
- package/dist/agent-session/session/tool-round.js +24 -2
- package/dist/agent-session/session/types.d.ts +36 -2
- package/dist/agent-session/session.d.ts +40 -4
- package/dist/agent-session/session.js +78 -5
- package/dist/attention-compiler.d.ts +51 -2
- package/dist/attention-compiler.js +282 -21
- package/dist/cache-helpers.d.ts +4 -2
- package/dist/cache-helpers.js +8 -6
- package/dist/checkpoint-restore.d.ts +45 -0
- package/dist/checkpoint-restore.js +54 -0
- package/dist/checkpoints.js +7 -11
- package/dist/context-budget.d.ts +2 -1
- package/dist/context-budget.js +24 -2
- package/dist/contracts-core/agent.d.ts +30 -0
- package/dist/contracts-core/attention.d.ts +95 -0
- package/dist/contracts-core/content.d.ts +15 -0
- package/dist/contracts-core/guardrail-packs.d.ts +41 -0
- package/dist/contracts-core/guardrail-packs.js +2 -0
- package/dist/contracts-core/loop.d.ts +42 -0
- package/dist/contracts-core/provider.d.ts +25 -0
- package/dist/contracts-core/run-limits.d.ts +21 -0
- package/dist/contracts-core/session.d.ts +23 -5
- package/dist/contracts-core/session.js +21 -2
- package/dist/contracts-core/usage.d.ts +40 -0
- package/dist/contracts-core/usage.js +8 -0
- package/dist/contracts-core.d.ts +2 -0
- package/dist/contracts-core.js +2 -0
- package/dist/contracts-protocol.d.ts +90 -4
- package/dist/contracts-run-state.d.ts +82 -6
- package/dist/evidence-grounding.d.ts +29 -0
- package/dist/evidence-grounding.js +162 -0
- package/dist/guardrail-packs/coding-standard.d.ts +3 -0
- package/dist/guardrail-packs/coding-standard.js +63 -0
- package/dist/guardrail-packs/destructive-commands.d.ts +3 -0
- package/dist/guardrail-packs/destructive-commands.js +46 -0
- package/dist/guardrail-packs/errors.d.ts +7 -0
- package/dist/guardrail-packs/errors.js +9 -0
- package/dist/guardrail-packs/index.d.ts +4 -0
- package/dist/guardrail-packs/index.js +15 -0
- package/dist/guardrail-packs/secrets-hygiene.d.ts +3 -0
- package/dist/guardrail-packs/secrets-hygiene.js +23 -0
- package/dist/guardrail-packs/types.d.ts +16 -0
- package/dist/guardrail-packs/types.js +2 -0
- package/dist/guardrail-packs/validation-respect.d.ts +3 -0
- package/dist/guardrail-packs/validation-respect.js +53 -0
- package/dist/guardrails.d.ts +20 -1
- package/dist/guardrails.js +268 -0
- package/dist/host-composition.d.ts +13 -0
- package/dist/host-composition.js +33 -2
- package/dist/index.d.ts +19 -10
- package/dist/index.js +11 -6
- package/dist/input.d.ts +8 -1
- package/dist/input.js +68 -6
- package/dist/middleware.d.ts +37 -2
- package/dist/middleware.js +41 -0
- package/dist/node/session-store-jsonl.js +18 -3
- package/dist/observability.js +6 -0
- package/dist/provider-events.d.ts +11 -3
- package/dist/provider-events.js +62 -4
- package/dist/providers/openai-compatible.js +6 -3
- package/dist/providers/transport.d.ts +3 -1
- package/dist/providers/transport.js +36 -0
- package/dist/redaction.js +18 -2
- package/dist/run-bundle.d.ts +89 -0
- package/dist/run-bundle.js +150 -0
- package/dist/run-limits.d.ts +11 -1
- package/dist/run-limits.js +46 -0
- package/dist/session-stores.d.ts +12 -1
- package/dist/session-stores.js +21 -4
- package/dist/testing/agent-event-source-conformance.js +41 -2
- package/dist/testing/prefix-stability-conformance.d.ts +30 -0
- package/dist/testing/prefix-stability-conformance.js +104 -0
- package/dist/testing/session-store-conformance.d.ts +3 -2
- package/dist/testing/session-store-conformance.js +48 -0
- package/dist/testing/state-concurrency-conformance.js +5 -12
- package/dist/tools.d.ts +5 -0
- package/dist/tools.js +11 -3
- package/dist/usage-estimation.d.ts +29 -0
- package/dist/usage-estimation.js +79 -0
- package/docs/ag-ui.md +5 -0
- package/docs/agent-events.md +68 -1
- package/docs/agent-loops.md +33 -0
- package/docs/agent-session-runtime.md +5 -3
- package/docs/attention-compiler.md +89 -8
- package/docs/coding-agent-tools.md +1 -1
- package/docs/coding-security.md +1 -0
- package/docs/coding-tools.md +0 -1
- package/docs/compaction-and-retry.md +1 -1
- package/docs/compaction-observational-memory.md +34 -7
- package/docs/connected-apps.md +116 -0
- package/docs/context-and-skills.md +13 -0
- package/docs/core.md +1 -1
- package/docs/diagrams.md +6 -6
- package/docs/document-reader.md +9 -9
- package/docs/documents.md +32 -11
- package/docs/durable-runs.md +129 -0
- package/docs/embeddings.md +5 -0
- package/docs/enterprise-postgres-state.md +4 -0
- package/docs/evaluations.md +5 -0
- package/docs/execution-timeline.md +84 -1
- package/docs/guardrails.md +71 -2
- package/docs/history/079-messaging-primitive-review.md +391 -0
- package/docs/history/080-messaging-followon-primitive-review.md +234 -0
- package/docs/history/081-connected-apps-primitive-review.md +74 -0
- package/docs/history/083-prism-work-primitive-review.md +84 -0
- package/docs/history/084-primitive-review.md +96 -0
- package/docs/history/085-honesty-and-cut-primitive-review.md +91 -0
- package/docs/history/README.md +5 -0
- package/docs/history/release-handoffs.md +38 -0
- package/docs/host-compositions.md +8 -6
- package/docs/host-security.md +2 -2
- package/docs/index.md +66 -29
- package/docs/input-and-prompt-assembly.md +3 -3
- package/docs/knowledge-sync.md +4 -0
- package/docs/live-testing.md +5 -3
- package/docs/mcp-tools.md +1 -0
- package/docs/messaging-channel-operations.md +166 -0
- package/docs/messaging-channels.md +150 -0
- package/docs/middleware-hooks.md +38 -2
- package/docs/migrate-to-0.8.md +124 -0
- package/docs/migrate-to-0.9.md +210 -0
- package/docs/migration.md +43 -0
- package/docs/model-registry.md +12 -2
- package/docs/multi-agent-patterns.md +25 -2
- package/docs/node-jsonl-session-store.md +7 -1
- package/docs/observability.md +7 -3
- package/docs/openapi-tools.md +1 -1
- package/docs/operations.md +1 -3
- package/docs/options-index.md +36 -3
- package/docs/peer-dependencies.md +6 -6
- package/docs/policy-and-audit.md +13 -1
- package/docs/postgres-persistence.md +1 -1
- package/docs/prefix-stability-conformance.md +93 -0
- package/docs/provider-caching.md +4 -4
- package/docs/provider-conformance.md +16 -0
- package/docs/provider-layer.md +2 -2
- package/docs/provider-packages.md +20 -20
- package/docs/providers/neuralwatt.md +5 -1
- package/docs/public-contracts.md +2 -2
- package/docs/rag.md +102 -4
- package/docs/release-and-install.md +55 -47
- package/docs/run-bundle.md +92 -0
- package/docs/runs-and-usage.md +57 -6
- package/docs/scoped-agent-memory.md +262 -0
- package/docs/server.md +2 -0
- package/docs/session-store-conformance.md +1 -2
- package/docs/session-stores.md +17 -17
- package/docs/sheets.md +9 -9
- package/docs/signal-channel.md +112 -0
- package/docs/speech.md +5 -1
- package/docs/sqlite-persistence.md +1 -1
- package/docs/supervisors.md +32 -12
- package/docs/telegram-channel.md +157 -0
- package/docs/testing.md +2 -2
- package/docs/tools.md +17 -0
- package/docs/wiki.md +1 -1
- package/docs/work-artifacts-and-review.md +1 -1
- package/docs/work-connectors.md +9 -9
- package/docs/work-sandbox.md +115 -0
- package/docs/work-tools.md +38 -16
- package/docs/workflows.md +5 -0
- package/package.json +9 -3
- package/templates/business-worker/manifest.json +2 -1
- package/templates/business-worker/src/agent.ts.tmpl +1 -1
- package/templates/business-worker/src/tests/agent.test.ts.tmpl +1 -1
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
# Effective run bundle snapshots
|
|
2
|
+
|
|
3
|
+
`snapshotRunBundle()` answers one question for a host harness: *what exactly ran?* It projects the inputs a
|
|
4
|
+
run resolves to — prompt contributions, skills, tools, guardrails, loop, limits, model, storage kinds — into
|
|
5
|
+
frozen JSON with one stable digest, so a harness registry can pin the bundle it evaluated and diff a later run
|
|
6
|
+
against it. It is the inspectable half of the durable-run fingerprint: same inputs, named fields, one hash.
|
|
7
|
+
|
|
8
|
+
## What it does
|
|
9
|
+
|
|
10
|
+
- Reads only in-process configuration: `agent` (plus optional `AgentSessionConfig` and `RunOptions` overrides).
|
|
11
|
+
- Returns frozen JSON with `schemaVersion`, a `sha256:` `digest`, and the durable `fingerprint` it corresponds to.
|
|
12
|
+
- Never opens a socket, never reads a store, never resolves a credential, and never emits a store connection
|
|
13
|
+
string — only its kind and durability.
|
|
14
|
+
- Never emits bodies: prompt and skill instructions are digests, tool parameters are digests.
|
|
15
|
+
- Is synchronous, in-memory, and O(contributions): a 100-tool agent snapshots in well under a millisecond.
|
|
16
|
+
|
|
17
|
+
`digest` is SHA-256 over the canonicalized, redacted snapshot (the `digest` field itself excluded), prefixed
|
|
18
|
+
`sha256:`. Tool parameter schemas go through `canonicalizeJsonSchema()` first, so key order and `required`
|
|
19
|
+
ordering cannot fake a change. Identical configuration produces an identical digest; any listed contribution
|
|
20
|
+
change — a tool, a schema, a skill body, a guardrail revision, a limit, `thinkingLevel`, the loop revision, a
|
|
21
|
+
request policy, a store kind — produces a different one.
|
|
22
|
+
|
|
23
|
+
## When to use it
|
|
24
|
+
|
|
25
|
+
- Pin the bundle in a harness/eval registry next to the run or timeline id, then diff digests across releases.
|
|
26
|
+
- Explain a durable-resume failure: `fingerprint` is the value compare-and-set against stored run state, and
|
|
27
|
+
the snapshot shows *which* field moved.
|
|
28
|
+
- Feed a release manifest or a support bundle: it is JSON, bounded (512 KiB), and secret-free by construction.
|
|
29
|
+
|
|
30
|
+
Do not use it as a substitute for `inspectHostComposition()` (that inspects a whole composition's readiness,
|
|
31
|
+
sandbox isolation, and credential references) or as a policy decision — it is a report, not a guard.
|
|
32
|
+
|
|
33
|
+
## Inputs
|
|
34
|
+
|
|
35
|
+
| Input | Purpose |
|
|
36
|
+
| --- | --- |
|
|
37
|
+
| `agent` | The agent to inspect; `agent.config` supplies tools, skills, guardrails, prompt, loop, limits, model. |
|
|
38
|
+
| `config` | Optional `AgentSessionConfig`: its `store` wins over `agent.config.store` for the reported session-store kind. |
|
|
39
|
+
| `run` | Optional `RunOptions`: run-level overrides (`limit`s, `thinkingLevel`, `systemPrompt`, `guardrails`, `loop`, `toolNames`, `attentionCompiler`, `providerRequestPolicies`, `runState.definitionRevision`, `effectStore`). |
|
|
40
|
+
| `memory` | Optional memory store instance. Only its kind and durability are read — never its contents. |
|
|
41
|
+
|
|
42
|
+
## Output
|
|
43
|
+
|
|
44
|
+
```ts
|
|
45
|
+
const bundle = snapshotRunBundle({ agent, run: { limits: { maxTurns: 12 }, toolNames: ["search"] } });
|
|
46
|
+
|
|
47
|
+
bundle.schemaVersion; // 1
|
|
48
|
+
bundle.digest; // "sha256:1ddd…" — pin this
|
|
49
|
+
bundle.fingerprint; // agentFingerprint() for durable resume
|
|
50
|
+
bundle.agent; // { id, definitionRevision }
|
|
51
|
+
bundle.systemPrompt; // { disabled, instructionsDigest, contributions: [{ id, mode, source, digest }] }
|
|
52
|
+
bundle.skills; // [{ name, instructionsDigest, toolNames }]
|
|
53
|
+
bundle.tools; // [{ name, schemaDigest, exclusive, effect }] — run.toolNames already applied
|
|
54
|
+
bundle.guardrails; // [{ name, stage, revision }]
|
|
55
|
+
bundle.loop; // { strategy, revision }
|
|
56
|
+
bundle.limits; // resolved ResolvedRunLimits
|
|
57
|
+
bundle.model; // { provider, model }
|
|
58
|
+
bundle.storage; // { sessionStore, checkpoints, effectStore, memory } → { kind, durable }
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
`tools` is the *effective* set: `RunOptions.toolNames` narrowing is applied, and an unknown name fails closed
|
|
62
|
+
exactly as it would during the run. `storage.*.kind` comes from the store's declared `kind` or constructor name
|
|
63
|
+
and is reduced to a plain token (`[a-z0-9_.-]`, ≤64 chars); anything URL-shaped is reported as `custom`, so a
|
|
64
|
+
connection string can never reach a pinned artifact.
|
|
65
|
+
|
|
66
|
+
## Example
|
|
67
|
+
|
|
68
|
+
```ts
|
|
69
|
+
import { createAgent, snapshotRunBundle } from "@arnilo/prism";
|
|
70
|
+
|
|
71
|
+
const agent = createAgent({ model: { provider: "anthropic", model: "claude-sonnet-4-5" }, /* … */ });
|
|
72
|
+
const bundle = snapshotRunBundle({ agent, run: { thinkingLevel: "high" } });
|
|
73
|
+
|
|
74
|
+
const pinned = bundle.digest; // store with the harness artifact
|
|
75
|
+
const next = snapshotRunBundle({ agent: changedAgent });
|
|
76
|
+
if (next.digest !== pinned) reportFields(next, pinned); // hosts diff by field, not by digest alone
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
## Redaction and limits
|
|
80
|
+
|
|
81
|
+
Every string field is passed through the host `SecretRedactor` (`RunOptions.redactor` ?? `AgentConfig.redactor`)
|
|
82
|
+
before hashing and before returning, so redaction is part of the pinned digest. The snapshot refuses to exceed
|
|
83
|
+
512 KiB — a bundle that large is a host wiring bug, not something to retain — and throws `TypeError` rather than
|
|
84
|
+
truncating. There is no network path, no store read, and no credential resolution in this function; a store that
|
|
85
|
+
throws on every method still snapshots fine.
|
|
86
|
+
|
|
87
|
+
## Related APIs
|
|
88
|
+
|
|
89
|
+
- [`agentFingerprint()`](durable-runs.md): the durable-resume identity this snapshot projects.
|
|
90
|
+
- [`inspectHostComposition()`](host-compositions.md): composition readiness, storage durability, sandbox isolation.
|
|
91
|
+
- [`ExecutionTimeline`](execution-timeline.md): what a run *did*; the snapshot is what it was *configured* with.
|
|
92
|
+
- [`RunRecord`](runs-and-usage.md): the ledger row a snapshotted run leaves behind.
|
package/docs/runs-and-usage.md
CHANGED
|
@@ -59,12 +59,61 @@ await session.run("Summarize", {
|
|
|
59
59
|
});
|
|
60
60
|
```
|
|
61
61
|
|
|
62
|
-
Defaults are the unconfigured fence (OWASP LLM10): turns 16, provider attempts 24, tool rounds 8, tool calls 32, wall time 120 seconds, request and response bytes 8 MiB each, input tokens 40,000, output tokens 10,000, total tokens 50,000. Hard process ceilings exist only for request/response bytes (64 MiB each), so a bug cannot OOM the host through a giant provider frame; those two axes reject `null` and are charged **per frame** (request payload, provider event), not as a run-lifetime sum — a 2 MiB prompt sent forty times is 2 MiB frames, not an 80 MiB parse. Snapshots still report the cumulative `requestBytes`/`responseBytes` counters for telemetry. Every other axis is host policy (0.5.4): omit a key for the default, set a positive safe integer sized to the workload, or set `null` to disable the axis — overnight sessions raise turns/wall/tokens, and a disabled wall still honors `RunOptions.signal`. Resolution stays narrowing-only: `RunOptions.limits` may lower `AgentConfig.limits`, `null` acts as +Infinity (agent 16 + run `null` → 16), and a raised/disabled `maxTurns` lifts an omitted `maxProviderAttempts` (default 24) to at least `maxTurns` so attempts cannot undercut turns; explicitly set attempts values are lifted only when both are finite. Cumulative token counters are billed usage across the whole run, not the context window (`contextBudget` governs window compaction). For production, prefer an explicit `maxCost`: cost needs a finite non-negative amount plus one currency, and when cost is limited, absent, non-finite, or mixed-currency provider cost fails closed. Vendors that omit usage charge
|
|
62
|
+
Defaults are the unconfigured fence (OWASP LLM10): turns 16, provider attempts 24, tool rounds 8, tool calls 32, wall time 120 seconds, request and response bytes 8 MiB each, input tokens 40,000, output tokens 10,000, total tokens 50,000. Hard process ceilings exist only for request/response bytes (64 MiB each), so a bug cannot OOM the host through a giant provider frame; those two axes reject `null` and are charged **per frame** (request payload, provider event), not as a run-lifetime sum — a 2 MiB prompt sent forty times is 2 MiB frames, not an 80 MiB parse. Snapshots still report the cumulative `requestBytes`/`responseBytes` counters for telemetry. Every other axis is host policy (0.5.4): omit a key for the default, set a positive safe integer sized to the workload, or set `null` to disable the axis — overnight sessions raise turns/wall/tokens, and a disabled wall still honors `RunOptions.signal`. Resolution stays narrowing-only: `RunOptions.limits` may lower `AgentConfig.limits`, `null` acts as +Infinity (agent 16 + run `null` → 16), and a raised/disabled `maxTurns` lifts an omitted `maxProviderAttempts` (default 24) to at least `maxTurns` so attempts cannot undercut turns; explicitly set attempts values are lifted only when both are finite. Cumulative token counters are billed usage across the whole run, not the context window (`contextBudget` governs window compaction). For production, prefer an explicit `maxCost`: cost needs a finite non-negative amount plus one currency, and when cost is limited, absent, non-finite, or mixed-currency provider cost fails closed. Vendors that omit usage charge their
|
|
63
|
+
labeled estimate (or zero with `usageEstimation: "off"`) to the token counters and never a
|
|
64
|
+
price, so a configured `maxCost` stays the fail-closed envelope for usage-less vendors.
|
|
63
65
|
|
|
64
|
-
Prism charges turns before assembly, provider attempts before generation, request bytes per request payload, response bytes per provider event (each frame must fit the byte cap on its own), tool rounds before a batch, tool calls before dispatch, and usage before another turn. A breach stops new work, aborts active work through the run signal, emits exactly one redacted `run_limit_exceeded` event/ledger row, and throws `AgentRunError` with `result.limit` (`limit`, `maximum`, `observed`, optional `currency`). Provider-reported token/cost totals arrive after generation, so that completed provider turn can be the unavoidable overshoot boundary.
|
|
66
|
+
Prism charges turns before assembly, provider attempts before generation, request bytes per request payload, response bytes per provider event (each frame must fit the byte cap on its own), tool rounds before a batch, tool calls before dispatch, and usage before another turn. A breach stops new work, aborts active work through the run signal, emits exactly one redacted `run_limit_exceeded` event/ledger row, and throws `AgentRunError` with `result.limit` (`limit`, `maximum`, `observed`, optional `currency`). Just before the terminal `error`, the run also emits one `budget_exhausted` attribution event — the axis that fired, run counters at exhaustion, the three closest other axes, and hashes of the last ten dispatched tool calls ([Agent events § Run limit events](agent-events.md#run-limit-events)). Provider-reported token/cost totals arrive after generation, so that completed provider turn can be the unavoidable overshoot boundary.
|
|
65
67
|
|
|
66
68
|
`createRunLimitTracker()` and `resolveRunLimits()` are public for adapters that need the same validation and accounting semantics. Workflow agent nodes forward `RunWorkflowOptions.limits`; supervisor delegation narrows its step/tool/token/timeout budget into core limits; MCP tool calls use a per-call tracker.
|
|
67
69
|
|
|
70
|
+
## Token estimation (provider reports no usage)
|
|
71
|
+
|
|
72
|
+
When a provider reports no usage, `estimateMessageTokens(messages, modelFamily)` returns a labeled `TokenEstimate` instead of a silent zero. An estimate is never provider truth: reported usage always wins and is never overwritten. The array form reuses the same message flattening as budget accounting and adds the family's per-message chat-template overhead; the single-message form `estimateMessageTokens(message)` remains the numeric budget heuristic used by `contextBudget`.
|
|
73
|
+
|
|
74
|
+
```ts
|
|
75
|
+
import { estimateMessageTokens, MODEL_FAMILY_TOKENS, resolveModelFamily } from "@arnilo/prism";
|
|
76
|
+
|
|
77
|
+
const estimate = estimateMessageTokens(messages, "claude-sonnet-4.5"); // model id, provider id, or family name
|
|
78
|
+
// { tokens: 41_200, confidence: "medium", lowConfidence: false }
|
|
79
|
+
```
|
|
80
|
+
|
|
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
|
+
|
|
83
|
+
### Automatic fallback (`AgentConfig.usageEstimation`)
|
|
84
|
+
|
|
85
|
+
`usageEstimation` is `"fallback"` (default) or `"off"`. With the default, a provider turn that reports no usage records one labeled estimate at the existing usage seam — no adapter changes:
|
|
86
|
+
|
|
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;
|
|
88
|
+
- 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
|
+
- 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
|
+
- `"off"` leaves absent usage absent — no ledger row, no run total, never a zero.
|
|
91
|
+
|
|
92
|
+
The estimate covers the turn's own request: messages plus tool declarations and context blocks, using the model id's family table.
|
|
93
|
+
|
|
94
|
+
### `session.contextMeter()`
|
|
95
|
+
|
|
96
|
+
One state read for host UIs (Clay's token meter, Synapta's model-router budgets):
|
|
97
|
+
|
|
98
|
+
```ts
|
|
99
|
+
const meter = session.contextMeter();
|
|
100
|
+
// { inputTokens: 43_000, source: "estimated", inputCap: 200_000, runInputBudget: 500_000, usedRatio: 0.215 }
|
|
101
|
+
```
|
|
102
|
+
|
|
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.
|
|
104
|
+
|
|
105
|
+
## Clean stops and stop reasons
|
|
106
|
+
|
|
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, or a loop ceiling. `AgentRunResult.stopReason` names that outcome — `"host_policy"` for a host policy stop, `"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
|
+
|
|
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).
|
|
110
|
+
|
|
111
|
+
## Provider failure classes
|
|
112
|
+
|
|
113
|
+
Provider-originated failures carry advisory `ErrorInfo.failureClass` on the failed `AgentRunResult`, terminal `RunRecord`, error events, and any `ToolResult.error` that already carries that `ErrorInfo`. Values are `"quota"`, `"auth"`, `"rate_limited"`, `"transient"`, `"permanent"`, and `"unknown"`. The classifier uses an already-captured HTTP status plus bounded error body: quota-shaped `429` responses (for example `GoUsageLimitError`) are `"quota"`; other `429` values are `"rate_limited"`; `401`/`403` are `"auth"`; `5xx` and known network codes such as `ECONNRESET` are `"transient"`; other `4xx` values are `"permanent"`; anything else is `"unknown"`.
|
|
114
|
+
|
|
115
|
+
This field is outcome metadata, not a retry control. Existing retry policy, attempt limits, and fail-closed behavior continue to use `ErrorInfo.code` exactly as before. Prism records no provider headers or response bodies beyond the existing redacted error message.
|
|
116
|
+
|
|
68
117
|
## Durable run state
|
|
69
118
|
|
|
70
119
|
`RunOptions.runState` writes a bounded, versioned checkpoint only at a safe interruption boundary. Its counters and absolute deadline resume with the run, while transcript history stays in `SessionStore` by session/leaf reference. `AgentRunResult.runState` exposes only redacted identity/status/version data; `interruption` excludes tool arguments. See [Agent/session runtime](agent-session-runtime.md#durable-interruption).
|
|
@@ -86,6 +135,8 @@ The adapter receives these record shapes:
|
|
|
86
135
|
| `status` | `queued` \| `running` \| `suspended` \| `denied` \| `succeeded` \| `failed` \| `aborted`. |
|
|
87
136
|
| `startedAt` / `finishedAt` | ISO timestamps. |
|
|
88
137
|
| `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. |
|
|
139
|
+
| `stopDetail` | Host stop detail from `turnPolicy.stop` (≤256 bytes, redacted). |
|
|
89
140
|
| `error` | `ErrorInfo` when status is `failed`. |
|
|
90
141
|
| `tenantId` / `accountId` / `userId` | From active ownership scope. |
|
|
91
142
|
|
|
@@ -122,7 +173,7 @@ The adapter receives these record shapes:
|
|
|
122
173
|
| `runId` / `sessionId` / `entryId` | Correlation ids. |
|
|
123
174
|
| `scope` | `provider_turn` for billable source rows; `run_total` for the aggregate. Never sum both scopes. |
|
|
124
175
|
| `turn` / `attempt` | Provider-turn attribution; absent on `run_total`. |
|
|
125
|
-
| `usage` | `Usage` shape: input/output/total/cache tokens, cost, currency. |
|
|
176
|
+
| `usage` | `Usage` shape: input/output/total/cache tokens, cost, currency. Cache fields stay absent when provider does not report them; an explicit provider zero remains `0`. |
|
|
126
177
|
| `recordedAt` | ISO timestamp. |
|
|
127
178
|
|
|
128
179
|
## Cost/catalog freshness (host adapter)
|
|
@@ -258,7 +309,7 @@ const ledger: RunLedger = {
|
|
|
258
309
|
|
|
259
310
|
const agent = createAgent({
|
|
260
311
|
model: { provider: "mock", model: "demo" },
|
|
261
|
-
provider: createMockProvider([providerTextDelta("Hello"), providerDone()]),
|
|
312
|
+
provider: createMockProvider([providerTextDelta("Hello"), providerDone({ inputTokens: 1_000, cacheReadTokens: 800 })]),
|
|
262
313
|
runLedger: ledger,
|
|
263
314
|
ownership: { tenantId: "tenant_a", accountId: "account_a" },
|
|
264
315
|
idempotencyKey: "agent-key",
|
|
@@ -276,7 +327,7 @@ console.log(runs.at(-1)?.status); // succeeded
|
|
|
276
327
|
const billable = usageRows.filter((row) => row.scope === "provider_turn");
|
|
277
328
|
const aggregate = usageRows.find((row) => row.scope === "run_total");
|
|
278
329
|
console.log(cacheUsageReport(aggregate?.usage));
|
|
279
|
-
// { cacheReadTokens:
|
|
330
|
+
// { cacheReadTokens: 800, hitRate: 0.8 } — cacheWriteTokens stays absent when unreported
|
|
280
331
|
```
|
|
281
332
|
|
|
282
333
|
## Extension and configuration notes
|
|
@@ -296,7 +347,7 @@ console.log(cacheUsageReport(aggregate?.usage));
|
|
|
296
347
|
- Adapters should treat appends as ordered within a `runId`: event and tool-call rows preserve emission order because the runtime serializes event ledger appends through one promise chain (concurrency 1), drains pending appends before writing the final `RunRecord`, and propagates append failures by rejecting run completion.
|
|
297
348
|
- Billing queries must filter `scope = "provider_turn"`; presentation queries normally read the single `run_total`. `UsageQuery.scope`, `turn`, and `attempt` are explicit filters.
|
|
298
349
|
- Adapters that need upsert semantics can use `RunRecord.id` (== `runId`) as the stable key.
|
|
299
|
-
- Use `cacheUsageReport(record.usage, model)` for cache diagnostics from normalized usage. It
|
|
350
|
+
- Use `cacheUsageReport(record.usage, model)` for cache diagnostics from normalized usage. It reports `cacheReadTokens` without `cacheWriteTokens` when that is all a provider supplies; neither token field nor hit rate is fabricated as zero. `provider_turn_finished.metadata.cache` carries that same per-attempt report, while `ExecutionTimeline.cacheHitRate` is the input-token-weighted run aggregate.
|
|
300
351
|
- **Provider-specific telemetry is package-owned.** Core `Usage` carries token counts and `cost`/`currency`; it has no energy or detailed cost-breakdown fields. Providers that surface extra telemetry (e.g. `@arnilo/prism-providers/neuralwatt` exposes `neuralWattEventsWithTelemetry()`, `parseNeuralWattComment()`, and `mapNeuralWattTelemetry()` for `: energy`/`: cost` SSE comments and non-streaming top-level fields) keep that data in package-specific helpers/types. Telemetry never enters `RunLedger` usage rows unless the host explicitly copies it in; it carries usage/cost numbers only — never prompts, API keys, or headers. Account-level quota is likewise package-owned: `@arnilo/prism-providers/neuralwatt` exports an explicit `getNeuralWattQuota()` helper that the host calls on demand (never during generation); NeuralWatt rate-limits that endpoint to 1 request per second per customer, so the caller owns throttling.
|
|
301
352
|
- **Governed provider lifecycle and reservation reconciliation.** For invocation-level accounting outside of or in addition to `RunLedger`, wrap providers with `createGovernedProvider` or `router.createGovernedProvider` from `@arnilo/prism-core/governance/model-router`. The adapter handles atomic admission reservations, bounds streaming, and guarantees explicit settlement: missing actual usage on an interrupted or EOF stream is committed as reserved liability (`unknownUsage: true`) rather than zero, avoiding budget leakages or unmetered oversubscriptions. See [Model routing](model-routing.md).
|
|
302
353
|
- **Aggregate task/tenant accounting across all paid work.** Complex agent tasks often span retries, model fallbacks, delegated children, background compactions, embedding jobs, and paid tools. Passing `taskId` and `kind` (`"generation" | "embedding" | "compaction" | "tool"`) coordinates all related calls under a single atomic task-level reservation and budget scope. Committed usage decomposes into separate `byModel` and `byKind` attributions (`router.readBudget({ identity, taskId })`) while preventing double-charging across parent/child boundaries or replayed events. Long-running holds can be safely renewed via `router.renewBudget({ ... })` before expiry without prematurely releasing live liability. See [Model routing](model-routing.md) and [Enterprise PostgreSQL state](enterprise-postgres-state.md).
|
|
@@ -0,0 +1,262 @@
|
|
|
1
|
+
# Scoped persistent agent memory — design concept
|
|
2
|
+
|
|
3
|
+
Status: **concept/proposal**. This page describes a system design for workspace-scoped persistent agent memory — durable facts and procedures that are recorded, updated, and used automatically during agentic work. Nothing here is implemented as a package yet; it is the reference description for the approach. 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
|
+
|
|
5
|
+
## Problem and goals
|
|
6
|
+
|
|
7
|
+
A host running agents over a workspace — a codebase, a professional practice, a research corpus — wants the agent to accumulate durable knowledge across sessions without manual curation: conventions, environment quirks, proven procedures, corrections, user preferences. The system must:
|
|
8
|
+
|
|
9
|
+
- **Persist** durable facts and procedures per *scope of work* (workspace, codebase, content collection).
|
|
10
|
+
- **Record automatically** — post-task reflection, not user-issued "remember this" commands only.
|
|
11
|
+
- **Stay accurate at scale** — hundreds of accumulated records must not degrade routing, cost, or behavior.
|
|
12
|
+
- **Be auditable** — professional work requires provenance, review, and human override.
|
|
13
|
+
|
|
14
|
+
The canonical live experiment is the Hermes agent; its documented failure modes at scale define the requirements here.
|
|
15
|
+
|
|
16
|
+
## Case study: Hermes agent
|
|
17
|
+
|
|
18
|
+
Hermes (NousResearch/hermes-agent) is a self-improving personal agent. Its memory architecture:
|
|
19
|
+
|
|
20
|
+
| Layer | Mechanism | Design constraint |
|
|
21
|
+
| --- | --- | --- |
|
|
22
|
+
| Declarative memory | `MEMORY.md` (2,200 chars) + `USER.md` (1,375 chars), injected as a frozen snapshot at session start | Hard capacity: an overflowing write returns an **error that forces consolidation** — never silent growth |
|
|
23
|
+
| Episodic | SQLite FTS5 session search (`session_search`), ~20 ms, no LLM calls | Unlimited, never injected into prompts |
|
|
24
|
+
| Procedural | Skills: markdown `SKILL.md` under `~/.hermes/skills/` (agentskills.io format) | Name + one-line description in the system prompt; full body loaded on demand (`skill_view`) — progressive disclosure |
|
|
25
|
+
| Write loop | Background review agent after each turn, prompt biased toward action: *"most sessions produce at least one skill update"*; bar ≈ 5 tool calls / error recovery / user correction | Patch > edit > create; optional `write_approval` staging gate |
|
|
26
|
+
|
|
27
|
+
What Hermes got right: bounded always-on memory with capacity-forced consolidation, episodic search off the prompt, progressive disclosure for skill bodies, and a background reflection loop instead of user-driven memory commands.
|
|
28
|
+
|
|
29
|
+
### Documented failure modes at scale
|
|
30
|
+
|
|
31
|
+
- **Catalog inflation (issue [#22620](https://github.com/NousResearch/hermes-agent/issues/22620)):** every skill's name + category + description is injected into the system prompt on *every turn*. 243 skills ≈ 10–15K tokens per API call; a 130-skill setup ≈ 4K tokens/turn. Routing is done by making the model attend over the whole catalog, so prompt cost and selection confusion scale O(N) with library size.
|
|
32
|
+
- **Lazy-loading demand (issue [#2045](https://github.com/NousResearch/hermes-agent/issues/2045)):** 87 bundled skills ≈ 1.5–2K tokens before any user content; users manually prune (73 → 26 in one report).
|
|
33
|
+
- **No metabolism (issue [#12877](https://github.com/NousResearch/hermes-agent/issues/12877)):** skills are add-only. No decay, no invalidation, no usage-based cleanup. The library grows monotonically and useful skills are buried under throwaways.
|
|
34
|
+
- **No write-quality gate (#12877 §1):** ~5 tool calls is enough to mint a permanent "skill." A one-off debugging session becomes procedural knowledge with no consolidation or validation — working memory promoted directly to long-term memory.
|
|
35
|
+
- **Skill islands (#12877 §3):** no composition between skills; the same logic (e.g. a TDD workflow) is duplicated across many skills; association fields are decorative.
|
|
36
|
+
- **No conflict detection (#12877 §4):** only identical *names* are blocked; near-duplicate *functionality* competes for execution and produces chaotic output.
|
|
37
|
+
- **Community-validated fix inside #22620:** usage-decay scoring (`score = uses × exp(−Δdays/30)`) plus a hard character budget for the skill list cut catalog tokens ~70% at zero LLM cost — evidence that decay-weighted routing is the right primitive.
|
|
38
|
+
|
|
39
|
+
**Root cause synthesis:** Hermes solved read-path token economics (progressive disclosure) but left the *routing surface* O(N) in the prompt and gave the *write path* no quality gate, no garbage collection, no deduplication, and no conflict resolution. "Misfiring" is the routing problem: as semantic overlap across hundreds of descriptions grows, selection degrades, and stale or duplicate entries win over correct ones.
|
|
40
|
+
|
|
41
|
+
## Research basis
|
|
42
|
+
|
|
43
|
+
| System | Contribution taken |
|
|
44
|
+
| --- | --- |
|
|
45
|
+
| **Mem0** ([arXiv 2504.19413](https://doi.org/10.48550/arxiv.2504.19413)) | Write path as extraction + adjudication: an LLM decides ADD / UPDATE / DELETE / NOOP against existing memories. Solves add-only bloat; strong LoCoMo results against MemGPT/A-MEM baselines. |
|
|
46
|
+
| **Zep / Graphiti** ([arXiv 2501.13956](https://arxiv.org/html/2501.13956)) | Temporal validity as a first-class citizen: every fact carries `valid_at`/`invalid_at`; a new fact *closes* the old one rather than duplicating it. Solves fact staleness and contradiction. |
|
|
47
|
+
| **A-MEM** ([arXiv 2502.12110](https://arxiv.org/abs/2502.12110), NeurIPS'25) | Zettelkasten-style memory: notes carry structured attributes (keywords, tags, contextual descriptions), **link generation** to related notes, and **memory evolution** — new notes update the representations of old ones. Solves island isolation. |
|
|
48
|
+
| **Agent Workflow Memory** ([arXiv 2409.07429](https://arxiv.org/abs/2409.07429), ICML'25) | Procedural induction by mining *repeated* sub-routines across trajectories and abstracting out instance-specific context before storage. Fixes "one successful session becomes a skill." |
|
|
49
|
+
| **Generative Agents** ([arXiv 2304.03442](https://arxiv.org/abs/2304.03442)) | Memory-stream retrieval scored by **relevance × recency × importance**, plus periodic **reflection** that synthesizes higher-level insights once accumulated importance crosses a threshold. |
|
|
50
|
+
| **Voyager** ([arXiv 2305.16291](https://arxiv.org/html/2305.16291v2)) | Skill-library precedent: skills retrieved by embedding top-k, and a skill enters the library only after **execution verification** — promotion gates are not new. |
|
|
51
|
+
| **HippoRAG** ([NeurIPS'24](https://proceedings.neurips.cc/paper_files/paper/2024/file/6ddc001d07ca4f319af96a3024f6dbd1-Paper-Conference.pdf)) | Associative multi-hop recall via knowledge graph + personalized PageRank, for "everything connected to this" queries beyond nearest-neighbor retrieval. |
|
|
52
|
+
| **Letta sleep-time compute** ([arXiv 2504.13171](https://arxiv.org/abs/2504.13171)) | Memory consolidation moved off the interaction path: a background pass reorganizes memory between sessions, improving accuracy while cutting per-turn cost. |
|
|
53
|
+
| **Anthropic Agent Skills** ([engineering post](https://www.anthropic.com/engineering/equipping-agents-for-the-real-world-with-agent-skills)) | Progressive disclosure as a three-level spec; the always-loaded layer (name + description) is the routing surface and must stay tiny. |
|
|
54
|
+
|
|
55
|
+
## Design principles
|
|
56
|
+
|
|
57
|
+
1. **Memory is a first-class artifact of the workspace.** Versioned with the workspace, reviewable like code, and *scoped*: a record created in scope X is invisible outside X. One silo per workspace; a separate tiny global layer for user preferences (profile ≠ project).
|
|
58
|
+
2. **Typed memory, one store per type, different physics per type.** Facts are bounded and always-on; episodes are append-only and searchable; notes/insights are linked and evolving; procedures earn permanence through reuse.
|
|
59
|
+
3. **Write-heavy is the failure mode.** Writes are gated by repetition, adjudication, and conflict detection. The default write action is *no-op*.
|
|
60
|
+
4. **Reads are query-driven, never catalog-driven.** Routing happens through a retrieval tool, not attention over an injected list. Prompt cost is O(1) in library size.
|
|
61
|
+
5. **Metabolism, not accumulation.** Usage-decay garbage collection, validity windows, capacity-forced consolidation. Every record can die.
|
|
62
|
+
6. **Abstention beats misfiring.** Below a relevance floor, the memory returns "nothing relevant." An agent confidently following a *wrong* retrieved record is worse than one starting fresh.
|
|
63
|
+
|
|
64
|
+
## System description
|
|
65
|
+
|
|
66
|
+
### Scope model
|
|
67
|
+
|
|
68
|
+
The unit of memory is the **scope**: a workspace root, codebase, project directory, or content collection. Records carry a `scope` binding and are retrieved only within it — scope itself is the largest single precision filter (a query inside `~/work/api-server` never competes with records from other projects). A distinct, optional global scope holds user-profile facts; nothing crosses scope boundaries by default.
|
|
69
|
+
|
|
70
|
+
### Storage — typed records, not one bucket
|
|
71
|
+
|
|
72
|
+
```
|
|
73
|
+
<workspace>/.memory/
|
|
74
|
+
facts.md # always-on, hard character budget
|
|
75
|
+
records/*.md # searchable semantic memory: notes, procedures, insights
|
|
76
|
+
episodes.db # SQLite FTS5, append-only session logs
|
|
77
|
+
index.db # FTS5 (+ optional embeddings) over records — derived, rebuildable
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
Record format — markdown with frontmatter, human-editable, git-diffable, PR-reviewable:
|
|
81
|
+
|
|
82
|
+
```markdown
|
|
83
|
+
---
|
|
84
|
+
id: rec_7f3a91c2d0b4
|
|
85
|
+
type: procedure # fact | note | procedure | insight
|
|
86
|
+
scope: ~/work/api-server
|
|
87
|
+
status: candidate # candidate → verified → archived (promotion ladder)
|
|
88
|
+
created: 2026-06-14
|
|
89
|
+
valid: [2026-06-14, ] # validity window; a superseding write closes it
|
|
90
|
+
provenance: session 8a2f, turn 41 # every claim traceable to its source
|
|
91
|
+
uses: 0 # retrieval activations
|
|
92
|
+
last_used: null
|
|
93
|
+
links: [rec_deploy_rollback, rec_pg_pool]
|
|
94
|
+
---
|
|
95
|
+
## Deploy without downtime
|
|
96
|
+
1. ... (steps abstracted from instance specifics)
|
|
97
|
+
```
|
|
98
|
+
|
|
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
|
+
|
|
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/` becomes a **git audit mirror** — a rendered export of fabric notes for diff/review — not a second storage engine. One write path, two views.
|
|
102
|
+
|
|
103
|
+
### Write path — reflect, adjudicate, gate
|
|
104
|
+
|
|
105
|
+
A background reflection pass runs post-task on the session digest (Hermes's background review; Letta's sleep-time compute), on a cheaper model. Four hard differences from Hermes:
|
|
106
|
+
|
|
107
|
+
1. **Conservative bias.** The review prompt's prior is *"most sessions update nothing"* — the explicit inversion of Hermes's action-biased prompt. Write triggers: user correction, error→recovery, a technique reused *within* the session, or an explicit "remember this."
|
|
108
|
+
2. **Adjudication before write** (Mem0). Retrieve the top-k nearest existing records; the writer decides ADD / UPDATE / MERGE / NOOP against them. Duplication is killed at write time, not by later cleanup.
|
|
109
|
+
3. **Temporal supersession** (Zep/Graphiti). A contradicting fact closes the old record's validity window — never two live records claiming opposite things. Conflict is detected *at write*, not at misfire time.
|
|
110
|
+
4. **Promotion ladder** (Voyager; #12877 recommendation). Post-task output is a `candidate` note, never a procedure. A candidate becomes `verified` after **N successful reuses** (N ≈ 2–3) logged from actual retrieval→outcome feedback; insights promoted from reflections follow the same gate. Real-but-unproven knowledge lives as a note; only proven repetition earns procedure status. This single gate eliminates most of Hermes's throwaway-skill flood, because throwaways are never reused.
|
|
111
|
+
|
|
112
|
+
After the gate: **link generation** against retrieved neighbors (A-MEM) so records compose instead of islanding, and every record carries provenance back to the session/turn that produced it.
|
|
113
|
+
|
|
114
|
+
### Read path — where misfiring is prevented
|
|
115
|
+
|
|
116
|
+
- **In the system prompt:** `facts.md` only (bounded, ~1–2K tokens) plus one line — "memory available, N records in this scope." Never the record list. Routing is a tool call, not attention over a catalog: prompt cost stays flat regardless of library size.
|
|
117
|
+
- **`memory_search(query)`** — hybrid retrieval: lexical (FTS5/BM25, milliseconds, $0) plus embeddings when available, reranked by a Generative-Agents-style score extended with usage feedback:
|
|
118
|
+
`score = relevance × exp(−Δdays/τ) × importance × (1 + log uses)`
|
|
119
|
+
The decay term is exactly the mechanism community-measured at ~70% catalog reduction inside Hermes issue #22620.
|
|
120
|
+
- **Activation budget:** top-3 results per query, and an **abstain floor** — below a similarity threshold the tool returns "no relevant memory."
|
|
121
|
+
- **Link traversal:** follow `links:` one hop for associative recall (HippoRAG-lite). No graph database — frontmatter adjacency only.
|
|
122
|
+
|
|
123
|
+
### Lifecycle — metabolism
|
|
124
|
+
|
|
125
|
+
An idle/nightly consolidation pass (cheap model, off the interaction path):
|
|
126
|
+
|
|
127
|
+
- **Garbage collection by usage decay.** Archive `candidate` records unused for ~30 days; archive `verified` records below a usage-decay threshold. Recommend-then-delete, never silent deletion (git keeps history regardless).
|
|
128
|
+
- **Near-duplicate merge.** Records flagged by embedding similarity above threshold are merged or invalidated.
|
|
129
|
+
- **Promotion/demotion** per the ladder, from logged retrieval→outcome feedback.
|
|
130
|
+
- **Capacity-forced consolidation of `facts.md`.** When a facts write would exceed the budget, the write *fails* with "consolidate first" (the Hermes overflow-error pattern) — the writer must merge or remove entries in the same action. The always-on layer can therefore never rot.
|
|
131
|
+
- **Digest.** A weekly human-readable diff of memory changes (git already provides the bookkeeping).
|
|
132
|
+
|
|
133
|
+
### Trust boundary
|
|
134
|
+
|
|
135
|
+
- **Injection/exfiltration scanning** of record content before any prompt injection (patterns, invisible Unicode) — Hermes does this for `MEMORY.md`; extend to all records.
|
|
136
|
+
- **Staged approval.** Writes may be staged for human review (`write_approval`-style). Default: off for personal scopes, on for team/professional scopes.
|
|
137
|
+
- **Scope isolation.** Records never leak across workspace roots; the global user layer is opt-in per record.
|
|
138
|
+
- **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).
|
|
139
|
+
|
|
140
|
+
## Failure-mode → mechanism map
|
|
141
|
+
|
|
142
|
+
| Hermes failure mode | Mechanism here | Backing |
|
|
143
|
+
| --- | --- | --- |
|
|
144
|
+
| Catalog tokens O(N)/turn | Routing via retrieval tool; never list injection | #22620, #2045; Anthropic three-level disclosure |
|
|
145
|
+
| Wrong entry wins selection | Abstain floor + top-3 budget + decay-weighted scoring | Generative Agents; measured decay fix in #22620 |
|
|
146
|
+
| Throwaway-skill flood | candidate→verified promotion after N reuses | Voyager verification; #12877 §1; AWM repetition mining |
|
|
147
|
+
| Duplicates | Write-time ADD/UPDATE/MERGE/NOOP adjudication | Mem0 |
|
|
148
|
+
| Contradictory live facts | Validity windows; supersede, don't duplicate | Zep/Graphiti |
|
|
149
|
+
| Record islands | Link generation + memory evolution on write | A-MEM; HippoRAG traversal |
|
|
150
|
+
| Unreviewable autonomous writes | Markdown + git + provenance + staged approval | Hermes `journey`/`write_approval` generalized |
|
|
151
|
+
| No cleanup | Usage-decay GC + consolidation pass | Letta sleep-time; #12877 §2 |
|
|
152
|
+
|
|
153
|
+
## Evaluation
|
|
154
|
+
|
|
155
|
+
Non-negotiable for professional use; memory must earn its complexity:
|
|
156
|
+
|
|
157
|
+
1. **Task win-rate A/B** — a fixed task suite per scope, run with memory on / off / never-consolidated. If memory does not lift win rate or reduce turns, it ships off by default.
|
|
158
|
+
2. **Retrieval precision@3** against a hand-labeled query set per workspace; alert on drops — the leading indicator of misfiring.
|
|
159
|
+
3. **Health metrics** — duplication rate, candidate→verified conversion rate, activation rate (retrieved-and-used / retrieved), and prompt token cost per turn vs. library size (target: flat).
|
|
160
|
+
4. **LoCoMo-style recall probes** for the episodic layer, the standard benchmark in the Mem0/Zep line.
|
|
161
|
+
|
|
162
|
+
## Relationship to existing Prism memory surfaces
|
|
163
|
+
|
|
164
|
+
The placement principle: **the scoped layer is a policy and lifecycle layer, not a fifth store.** Mechanism lives in the engines and the fabric; policy lives in the scoped layer. This section concretizes how the layers compose into one memory-management system.
|
|
165
|
+
|
|
166
|
+
### Layered architecture and ownership
|
|
167
|
+
|
|
168
|
+
| Layer | Surface | Owns | Never does |
|
|
169
|
+
| --- | --- | --- | --- |
|
|
170
|
+
| Raw transcript | [Session stores](session-stores.md) | append-only entries, branches, bounded lexical search | — |
|
|
171
|
+
| 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
|
+
| 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
|
+
| 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 | **Scoped memory (this concept)** | conservative post-run writer, promotion ladder, usage-decay GC, abstain floor + activation budget, workspace-root scope identity, git audit mirror | no store, no engine, no context-block type, no second write path |
|
|
175
|
+
|
|
176
|
+
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
|
+
|
|
178
|
+
### Ideal composition for a persistent-memory agent
|
|
179
|
+
|
|
180
|
+
```ts
|
|
181
|
+
// 1. Engines — workspace root becomes the silo identity
|
|
182
|
+
const memory = createMemory({ tenantId: host, resourceId: workspaceRoot, embedder, stores });
|
|
183
|
+
// 2. Durable records — folding, links, evolution on by policy
|
|
184
|
+
const fabric = createMemoryFabric({ memory, observational, consolidate: { threshold: 0.85 },
|
|
185
|
+
linker: { enabled: true }, evolution: { enabled: true } });
|
|
186
|
+
// 3. Episodic ledger per session; work-scope index bound to the workspace
|
|
187
|
+
om.attach(session);
|
|
188
|
+
// 4. Gate fabric tools + workers to this session
|
|
189
|
+
fabric.attach(session);
|
|
190
|
+
// 5. Injection: ONLY the bounded working facts block reaches the prompt
|
|
191
|
+
registries.contextProviders.register("memory-fabric", fabric.createContextProvider());
|
|
192
|
+
const agent = await resolveAgentDefinition(
|
|
193
|
+
{ name: "assistant", model, context: ["memory-fabric"], tools: ["memory.recall"] },
|
|
194
|
+
{ registries, providerSource });
|
|
195
|
+
// 6. Scoped policy module (host-side, the new part): post-run review,
|
|
196
|
+
// usage logging on recall hits, idle promotion/GC jobs, .memory/ git mirror
|
|
197
|
+
```
|
|
198
|
+
|
|
199
|
+
End-to-end flow:
|
|
200
|
+
|
|
201
|
+
1. **During the session** — observational workers record source-backed observations/reflections; compaction renders prepared memory; the agent may call `memory.recall`/`memory.insert` directly; the prompt carries only the bounded facts block.
|
|
202
|
+
2. **After the run** — the scoped policy reviews the session digest on a cheap model, biased to no-op, and promotes the *generalizable* part through `fabric.remember` as `candidate` notes carrying `sourceEntryIds` provenance (or `reflectionId` for notes derived from a closed-scope reflection). Folding adjudicates duplicates; linker/evolution connect and refine.
|
|
203
|
+
3. **Next session** — facts block (working notes) always on; everything else via `memory.recall` (`kinds: ["procedure"]` stays opt-in, `asOf` honors validity, `budget` caps tokens). Recall hits log `uses`.
|
|
204
|
+
4. **Idle/nightly** — promotion job converts candidates with N logged successful reuses to `verified`; usage-decay GC archives the rest; the facts block is consolidated under its budget; the git mirror renders the diff for human review.
|
|
205
|
+
|
|
206
|
+
### Read/write paths across layers
|
|
207
|
+
|
|
208
|
+
| Write-path step | Owner |
|
|
209
|
+
| --- | --- |
|
|
210
|
+
| Trigger (post-run, noop-biased) | scoped policy |
|
|
211
|
+
| Extraction with provenance | observational reflection → `reflectionId`/`sourceEntryIds` |
|
|
212
|
+
| Adjudicate ADD/UPDATE/supersede | fabric consolidation folding (cosine threshold 0.85) |
|
|
213
|
+
| Close contradicted facts | fabric `validTo` + `supersedes` |
|
|
214
|
+
| Link + evolve neighbors | fabric linker/evolution workers |
|
|
215
|
+
| Status `candidate`, staging/approval | scoped policy |
|
|
216
|
+
|
|
217
|
+
| Read-path step | Owner |
|
|
218
|
+
| --- | --- |
|
|
219
|
+
| Always-on bounded facts | working block via the fabric context provider — the only injected surface |
|
|
220
|
+
| Query-driven records | `memory.recall` (kinds/asOf/budget) + scoped abstain floor, top-3 budget, decay-weighted scoring |
|
|
221
|
+
| Associative recall | `links` traversal (recall hits with `explain.link: true`) |
|
|
222
|
+
| Episodic specifics | observational exact-id recall / `searchConversation` lexical pages |
|
|
223
|
+
|
|
224
|
+
### Research leverage map
|
|
225
|
+
|
|
226
|
+
How the research findings land on shipped surfaces versus policy added by this concept:
|
|
227
|
+
|
|
228
|
+
| Finding | Source | Shipped in Prism | Added by this concept |
|
|
229
|
+
| --- | --- | --- | --- |
|
|
230
|
+
| Write adjudication (ADD/UPDATE/DELETE/NOOP) | Mem0 | fabric consolidation folding (insert / rewrite / supersede) | MERGE of near-duplicates in the GC pass |
|
|
231
|
+
| Importance weighting | Mem0 | `importance` / `importanceFrom` hook | — |
|
|
232
|
+
| Temporal validity windows | Zep/Graphiti | fabric `validFrom`/`validTo`, recall `asOf` | — |
|
|
233
|
+
| Invalidate, don't duplicate | Zep/Graphiti | supersede fold at write | — |
|
|
234
|
+
| Bi-temporal time (event vs ingestion) | Zep/Graphiti | `tRef` vs `ingestedAt` | — |
|
|
235
|
+
| Link generation | A-MEM | fabric linker worker | — |
|
|
236
|
+
| Memory evolution | A-MEM | fabric evolution worker | — |
|
|
237
|
+
| Relevance × recency × importance scoring | Generative Agents | fabric recall scoring (similarity/recency/importance) | usage feedback `(1 + log uses)` + exp decay |
|
|
238
|
+
| Verify before permanence | Voyager | — | promotion ladder (N reuses) |
|
|
239
|
+
| Mine repeated routines, abstract instance specifics | AWM | — | conservative review writes abstracted candidates |
|
|
240
|
+
| Off-path consolidation | Letta sleep-time | — (idle-job placement) | GC/promotion/facts consolidation on a cheap model |
|
|
241
|
+
| Tiny always-loaded layer, disclosure on demand | Anthropic skills | recall tools + working block only | abstain floor + top-3 activation budget |
|
|
242
|
+
| Hybrid BM25 + cosine + graph walk | Zep/Graphiti | partial: embedding score + link traversal; lexical on branch search | fusing lexical into one recall — open question |
|
|
243
|
+
|
|
244
|
+
### Deliberate deviations
|
|
245
|
+
|
|
246
|
+
- **Against Mem0's accumulate-and-rank-at-query.** Mem0's current algorithm appends dated variants and ranks at query time. This design sides with Zep: close the validity window at write. Rationale: the scoped layer injects few records, so each must be *the* trusted record; ranking dated variants at retrieval reintroduces the Hermes catalog/misfire problem one layer down.
|
|
247
|
+
- **Beyond both Mem0 and Zep.** Neither gates *procedures* by reuse — that is exactly the Hermes failure (#12877: ~5 tool calls = permanent skill). The promotion ladder is the addition, from Voyager/AWM.
|
|
248
|
+
- **Not adopted (deferred):** graph engines and community detection (frontmatter `links` + one-hop traversal suffice inside a scoped silo); Zep-style episodic subgraph summarization (the observational reflector already does hierarchical episodic summarization with provenance — adopting it would duplicate a shipped layer).
|
|
249
|
+
|
|
250
|
+
## Non-goals / deferred
|
|
251
|
+
|
|
252
|
+
- No vector database, graph database, or external memory provider in the core design — embeddings only where lexical retrieval measurably fails (inside a scoped silo it mostly will not).
|
|
253
|
+
- No second write path — the scoped policy writes only through the fabric, so folding, consent, and lineage rules apply to every write.
|
|
254
|
+
- No cross-scope federation or sharing; add only when a multi-workspace pattern measurably needs it.
|
|
255
|
+
- No autonomous deletion of human-authored records; GC proposes, humans dispose.
|
|
256
|
+
|
|
257
|
+
## Open questions
|
|
258
|
+
|
|
259
|
+
- Exact promotion thresholds (N reuses, decay τ, similarity floors) — must be empirically tuned per workload class (coding vs. research vs. professional ops).
|
|
260
|
+
- 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
|
+
- Team-scope semantics: per-user silos sharing one workspace root, or one shared silo with author-attributed records.
|
package/docs/server.md
CHANGED
|
@@ -264,6 +264,8 @@ A2A routes are not added to `createPrismHandler()`. Install `@arnilo/prism-core/
|
|
|
264
264
|
- [MCP client and server exposure](mcp-tools.md): selected MCP capabilities and web-standard MCP transport.
|
|
265
265
|
- [Host security guide](host-security.md): remote-boundary checklist.
|
|
266
266
|
- [A2A interoperability](a2a.md): separately mounted A2A 1.0 handler/client.
|
|
267
|
+
- [Telegram channel](telegram-channel.md): separately mounted Telegram Web `Request`/`Response` webhook handler; host owns fixed HTTPS route and TLS.
|
|
268
|
+
- [Signal channel (experimental)](signal-channel.md): private Unix-socket manual receive for an externally supervised signal-cli daemon; host owns socket, account, accepted-use policy and supervision.
|
|
267
269
|
- [Obscura browser engine](obscura.md): optional binary-backed generic tools for hosted agents.
|
|
268
270
|
- [Conversations](conversations.md): durable user-scoped conversation service, replay, branches, export, deletion.
|
|
269
271
|
- [Work artifacts and review](work-artifacts-and-review.md): durable artifact review service, revisions, approvals, authorized expiring delivery links.
|
|
@@ -25,8 +25,7 @@ Use this helper when implementing a DB-backed `SessionStore` (for example, the r
|
|
|
25
25
|
- session ids remain isolated (`assertSessionStoreConforms` always probes a secondary session)
|
|
26
26
|
- optional concurrent fork children of the same parent succeed when `exerciseConcurrentParentAppend: true`
|
|
27
27
|
- optional durable reopen/idempotency survival when `runSessionStoreConformance(..., { exerciseReopen: true })`
|
|
28
|
-
- optional `searchSessions`
|
|
29
|
-
- optional `searchSessions` bounds/ownership/empty-page checks when `exerciseSearchSessions: true` (`assertSessionStoreSearchSessions`)
|
|
28
|
+
- optional `searchSessions` case when `exerciseSearchSessions: true` (`assertSessionStoreSearchSessions`): invalid limit/query/kind rejection, empty-page bounds, limit cap, a written-message round-trip asserting `entryId`/`runId`/`turn`/`snippet` point at the match, one hit per session when a second entry also matches, the `kind` filter excluding non-matching entries, and ownership bounds
|
|
30
29
|
|
|
31
30
|
## Inputs / request
|
|
32
31
|
|
package/docs/session-stores.md
CHANGED
|
@@ -24,7 +24,7 @@ import type { SessionStore, SessionEntry } from "@arnilo/prism";
|
|
|
24
24
|
| `list(sessionId)` | Return all entries for one session in stored order. Development fallback for branch reads. |
|
|
25
25
|
| `get?(id)` | Return one entry by id, if present. Optional. |
|
|
26
26
|
| `readBranchPath?(query)` | Optional DB-friendly branch read. Return one branch's ancestor chain as a `PersistencePage<SessionEntry>` so the runtime can avoid `list(sessionId)`. |
|
|
27
|
-
| `searchSessions?(query)` | Optional bounded session search (`SessionSearchQuery` → `PersistencePage<SessionSearchHit>`). SQLite/Postgres implement FTS + metadata filters; memory
|
|
27
|
+
| `searchSessions?(query)` | Optional bounded session search (`SessionSearchQuery` → `PersistencePage<SessionSearchHit>`). SQLite/Postgres implement FTS + metadata filters; memory and JSONL scan linearly (JSONL re-reads its file per query). |
|
|
28
28
|
|
|
29
29
|
Public helpers:
|
|
30
30
|
|
|
@@ -110,9 +110,13 @@ Recognize it with `isSessionAppendConflict(error)`, not message text. Built-in s
|
|
|
110
110
|
- Branch semantics are parent links plus a leaf id. External UIs should keep branch handles as `(sessionId, leafId)`; RPC exposes an additional `handleId` for active handles.
|
|
111
111
|
- Development stores can omit `readBranchPath`; the runtime falls back to `list(sessionId)` and the pure in-memory branch walk. Database-backed stores should implement `readBranchPath` so `entries()`, `clone()`, and context rebuild read only the selected ancestor chain.
|
|
112
112
|
|
|
113
|
-
## Session search
|
|
113
|
+
## Session search
|
|
114
114
|
|
|
115
|
-
Bounded `SessionIndex` / `searchSessions` lists sessions by optional `workspaceRoot` (`metadata.workspaceRoot`), provider/model, label/summary, time range, ownership, and optional text `query
|
|
115
|
+
Bounded `SessionIndex` / `searchSessions` lists sessions by optional `workspaceRoot` (`metadata.workspaceRoot`), provider/model, label/summary, time range, ownership, and optional text `query`. SQLite/Postgres run indexed full-text search; the memory and JSONL stores scan linearly (case-sensitive substring, capped by the contract linear caps; JSONL re-reads and parses its file per query, see [Node JSONL session store](node-jsonl-session-store.md)). Hits require `sessionId` and may include `leafId` for `checkout`; never credentials or whole transcripts.
|
|
116
|
+
|
|
117
|
+
When a text `query` matches, the hit points at one matched entry per session (the store's best-ranked match on the indexed SQLite/Postgres paths, the first match in transcript order on the linear memory/JSONL paths): `entryId`, `runId`, and a 1-based `turn` (transcript position, `(timestamp, id)` order) locate it, `score` is the store relevance where the store has an index (higher is better; SQLite bm25 negated, Postgres `ts_rank_cd`; absent on linear stores; a non-discriminative term can legitimately score 0, so test for presence, not `> 0`), and `snippet` is bounded context around the match in that entry. Hits stay ordered by session `updatedAt` with cursor pagination, so hosts rank by `score` client-side when they want relevance order.
|
|
118
|
+
|
|
119
|
+
`SessionSearchQuery.kind` restricts which entry kinds the query may match (one kind or a list; omitted or `"any"` = all). Annotation search is `kind: ["label", "summary", "metadata", "custom"]`; `kind: "label"` without a `query` lists sessions that carry an annotation entry. Unknown kinds fail closed with `TypeError`. Indexed text is transcript message text plus label/summary - tool arguments and tool results are never indexed, so they cannot leak through search.
|
|
116
120
|
|
|
117
121
|
```ts
|
|
118
122
|
import { createMemorySessionStore, resolveSessionSearchQuery } from "@arnilo/prism";
|
|
@@ -121,32 +125,28 @@ const store = createMemorySessionStore([], { sessionSearchMode: "linear" });
|
|
|
121
125
|
const page = await store.searchSessions!({
|
|
122
126
|
workspaceRoot: "/repo",
|
|
123
127
|
query: "flake",
|
|
128
|
+
kind: "any",
|
|
124
129
|
limit: 20,
|
|
125
130
|
});
|
|
131
|
+
// [{ sessionId, leafId, entryId, runId, turn, score, snippet, ... }] // score is absent on linear stores
|
|
126
132
|
// Opt out: createMemorySessionStore([], { sessionSearchMode: "unsupported" })
|
|
127
133
|
// Raise the in-process scan caps for a small but large-query session set (defaults are the contract caps):
|
|
128
134
|
const wide = createMemorySessionStore([], { search: { maxLinearSessions: 5_000, maxLinearEntries: 50_000 } });
|
|
129
135
|
```
|
|
130
136
|
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
## Session search (0.0.11)
|
|
134
|
-
|
|
135
|
-
Bounded `SessionIndex` / `searchSessions` lists sessions by optional `workspaceRoot` (`metadata.workspaceRoot`), provider/model, label/summary, time range, ownership, and optional text `query` (FTS on SQLite/Postgres; case-sensitive substring on memory linear). Hits require `sessionId` and may include `leafId` for `checkout`; never credentials or whole transcripts.
|
|
137
|
+
The JSONL store exposes the same `searchSessions` contract through the same matcher, with no index:
|
|
136
138
|
|
|
137
139
|
```ts
|
|
138
|
-
import {
|
|
140
|
+
import { createJsonlSessionStore } from "@arnilo/prism/node/session-store-jsonl";
|
|
139
141
|
|
|
140
|
-
const store =
|
|
141
|
-
const page = await store.searchSessions!({
|
|
142
|
-
|
|
143
|
-
query: "flake",
|
|
144
|
-
limit: 20,
|
|
145
|
-
});
|
|
146
|
-
// Opt out: createMemorySessionStore([], { sessionSearchMode: "unsupported" })
|
|
142
|
+
const store = createJsonlSessionStore("./sessions.jsonl");
|
|
143
|
+
const page = await store.searchSessions!({ workspaceRoot: "/repo", query: "flake", limit: 20 });
|
|
144
|
+
// Every query reads and parses the file: O(corpus) time and memory, caps default to the linear caps.
|
|
147
145
|
```
|
|
148
146
|
|
|
149
|
-
Finite caps (defaults / hard): page 20/100; query string 4 KiB/16 KiB; snippet 512 B/4 KiB; cursor 1 KiB/4 KiB; memory linear sessions 1000/5000, entries 10000/50000, bytes 8 MiB/64 MiB; DB FTS candidates 1000/5000. Overflow fails closed via `resolveSessionSearchQuery`.
|
|
147
|
+
Finite caps (defaults / hard): page 20/100; query string 4 KiB/16 KiB; snippet 512 B/4 KiB; cursor 1 KiB/4 KiB; memory linear sessions 1000/5000, entries 10000/50000, bytes 8 MiB/64 MiB (also the JSONL scan caps); DB FTS candidates 1000/5000. Overflow fails closed via `resolveSessionSearchQuery`.
|
|
148
|
+
|
|
149
|
+
Sizing (plan 095): SQLite FTS5 and the Postgres `tsvector` column are maintained additively at append time (no background job). On the 100k-turn fixture in `scripts/benchmark-scenarios/session-search.mjs` (stored tool output, which is never indexed), the index is 18.8% of transcript page bytes and query p95 is 38 ms (`node scripts/benchmark.mjs --scenario session-search`; ceiling 100 ms). Stores receive already-redacted entries, so the index inherits the same redaction as session reads. Unindexed stores (memory, JSONL) trade that cost for O(corpus) per query — see `examples/session-search.ts` for both paths side by side.
|
|
150
150
|
|
|
151
151
|
## Security and performance notes
|
|
152
152
|
|