@arnilo/prism 0.8.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 +38 -0
- package/README.md +11 -11
- package/dist/agent-approval.d.ts +11 -2
- 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 +63 -6
- package/dist/agent-run-state.d.ts +22 -2
- package/dist/agent-run-state.js +57 -5
- package/dist/agent-session/helpers.js +14 -0
- package/dist/agent-session/session/assemble.js +126 -24
- package/dist/agent-session/session/persist.d.ts +11 -0
- package/dist/agent-session/session/persist.js +37 -11
- package/dist/agent-session/session/provider-round.d.ts +14 -4
- package/dist/agent-session/session/provider-round.js +185 -19
- package/dist/agent-session/session/tool-round.js +20 -1
- package/dist/agent-session/session/types.d.ts +25 -2
- package/dist/agent-session/session.d.ts +38 -4
- package/dist/agent-session/session.js +76 -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/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 +10 -0
- package/dist/contracts-core/guardrail-packs.d.ts +41 -0
- package/dist/contracts-core/guardrail-packs.js +2 -0
- package/dist/contracts-core/provider.d.ts +25 -0
- package/dist/contracts-core/run-limits.d.ts +19 -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 +76 -2
- package/dist/contracts-run-state.d.ts +56 -1
- 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/index.d.ts +14 -9
- package/dist/index.js +9 -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 +8 -2
- package/dist/provider-events.js +60 -2
- package/dist/providers/openai-compatible.js +6 -3
- package/dist/run-bundle.js +2 -1
- 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/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/agent-events.md +68 -1
- package/docs/agent-session-runtime.md +1 -0
- package/docs/attention-compiler.md +89 -8
- package/docs/coding-agent-tools.md +1 -1
- package/docs/compaction-and-retry.md +1 -1
- package/docs/compaction-observational-memory.md +33 -6
- package/docs/durable-runs.md +42 -0
- package/docs/embeddings.md +5 -0
- package/docs/evaluations.md +5 -0
- package/docs/execution-timeline.md +78 -1
- package/docs/guardrails.md +38 -2
- package/docs/index.md +32 -13
- package/docs/input-and-prompt-assembly.md +3 -3
- package/docs/knowledge-sync.md +4 -0
- package/docs/middleware-hooks.md +38 -2
- package/docs/migrate-to-0.9.md +210 -0
- package/docs/migration.md +13 -0
- 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/options-index.md +2 -1
- package/docs/policy-and-audit.md +13 -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-packages.md +20 -20
- package/docs/public-contracts.md +2 -2
- package/docs/rag.md +101 -3
- package/docs/release-and-install.md +39 -37
- package/docs/runs-and-usage.md +43 -6
- package/docs/scoped-agent-memory.md +262 -0
- package/docs/session-store-conformance.md +1 -2
- package/docs/session-stores.md +17 -17
- package/docs/supervisors.md +32 -12
- package/docs/tools.md +17 -0
- package/docs/workflows.md +5 -0
- package/package.json +5 -1
|
@@ -25,26 +25,26 @@ Do not use provider packages as a package manager, credential store, env loader,
|
|
|
25
25
|
|
|
26
26
|
| adapter package | version |
|
|
27
27
|
| --- | --- |
|
|
28
|
-
| `@arnilo/prism-providers/ai-sdk` | 0.
|
|
29
|
-
| `@arnilo/prism-providers/alibaba` | 0.
|
|
30
|
-
| `@arnilo/prism-providers/anthropic` | 0.
|
|
31
|
-
| `@arnilo/prism-providers/azure` | 0.
|
|
32
|
-
| `@arnilo/prism-providers/bedrock` | 0.
|
|
33
|
-
| `@arnilo/prism-providers/clinepass` | 0.
|
|
34
|
-
| `@arnilo/prism-providers/commandcode` | 0.
|
|
35
|
-
| `@arnilo/prism-providers/deepseek` | 0.
|
|
36
|
-
| `@arnilo/prism-providers/google` | 0.
|
|
37
|
-
| `@arnilo/prism-providers/hyper` | 0.
|
|
38
|
-
| `@arnilo/prism-providers/kimi` | 0.
|
|
39
|
-
| `@arnilo/prism-providers/model-discovery` | 0.
|
|
40
|
-
| `@arnilo/prism-providers/neuralwatt` | 0.
|
|
41
|
-
| `@arnilo/prism-providers/ollama` | 0.
|
|
42
|
-
| `@arnilo/prism-providers/openai` | 0.
|
|
43
|
-
| `@arnilo/prism-providers/opencode-go` | 0.
|
|
44
|
-
| `@arnilo/prism-providers/openrouter` | 0.
|
|
45
|
-
| `@arnilo/prism-providers/vertex` | 0.
|
|
46
|
-
| `@arnilo/prism-providers/xai` | 0.
|
|
47
|
-
| `@arnilo/prism-providers/zai` | 0.
|
|
28
|
+
| `@arnilo/prism-providers/ai-sdk` | 0.9.0 |
|
|
29
|
+
| `@arnilo/prism-providers/alibaba` | 0.9.0 |
|
|
30
|
+
| `@arnilo/prism-providers/anthropic` | 0.9.0 |
|
|
31
|
+
| `@arnilo/prism-providers/azure` | 0.9.0 |
|
|
32
|
+
| `@arnilo/prism-providers/bedrock` | 0.9.0 |
|
|
33
|
+
| `@arnilo/prism-providers/clinepass` | 0.9.0 |
|
|
34
|
+
| `@arnilo/prism-providers/commandcode` | 0.9.0 |
|
|
35
|
+
| `@arnilo/prism-providers/deepseek` | 0.9.0 |
|
|
36
|
+
| `@arnilo/prism-providers/google` | 0.9.0 |
|
|
37
|
+
| `@arnilo/prism-providers/hyper` | 0.9.0 |
|
|
38
|
+
| `@arnilo/prism-providers/kimi` | 0.9.0 |
|
|
39
|
+
| `@arnilo/prism-providers/model-discovery` | 0.9.0 |
|
|
40
|
+
| `@arnilo/prism-providers/neuralwatt` | 0.9.0 |
|
|
41
|
+
| `@arnilo/prism-providers/ollama` | 0.9.0 |
|
|
42
|
+
| `@arnilo/prism-providers/openai` | 0.9.0 |
|
|
43
|
+
| `@arnilo/prism-providers/opencode-go` | 0.9.0 |
|
|
44
|
+
| `@arnilo/prism-providers/openrouter` | 0.9.0 |
|
|
45
|
+
| `@arnilo/prism-providers/vertex` | 0.9.0 |
|
|
46
|
+
| `@arnilo/prism-providers/xai` | 0.9.0 |
|
|
47
|
+
| `@arnilo/prism-providers/zai` | 0.9.0 |
|
|
48
48
|
<!-- generated:package-truth:providers end -->
|
|
49
49
|
|
|
50
50
|
|
package/docs/public-contracts.md
CHANGED
|
@@ -154,10 +154,10 @@ Important request shapes:
|
|
|
154
154
|
| `PersistenceQuery` | Common pagination controls: `cursor?`, `limit?`, `order?: "asc" \| "desc"`. |
|
|
155
155
|
| `OwnershipScope` | Multi-tenant scope: `tenantId?`, `accountId?`, `userId?`. Included in records and queries. |
|
|
156
156
|
| `SessionRecord` / `SessionQuery` | Stored session and query filters (parent, agent definition, retention policy, timestamps, ownership). `SessionRecord.version` (with `appendSession` `expectedVersion`) is the optimistic metadata CAS: 0 = create-only, N = exact-version update; mismatch throws `SessionMetadataConflictError` (`metadata_conflict`). |
|
|
157
|
-
| `SessionIndex` / `SessionSearchQuery` / `SessionSearchHit` | Bounded optional session search seam (`search` / `SessionStore.searchSessions?`). Filters: workspace (`metadata.workspaceRoot`), time, provider/model, label/summary, optional FTS `query`, ownership. Hits return `sessionId` + optional `leafId` for resume
|
|
157
|
+
| `SessionIndex` / `SessionSearchQuery` / `SessionSearchHit` | Bounded optional session search seam (`search` / `SessionStore.searchSessions?`). Filters: workspace (`metadata.workspaceRoot`), time, provider/model, label/summary, entry-kind (`kind`, e.g. annotation search), optional FTS `query`, ownership. Hits return `sessionId` + optional `leafId` for resume, and on a text match the matched entry pointer (`entryId`, `runId`, 1-based `turn`, store `score`) with a bounded matched-text `snippet`; never credentials. Caps via `resolveSessionSearchQuery` / `DEFAULT_*` / `HARD_MAX_*` session-search constants. SQLite/Postgres index (FTS5 / `tsvector`); memory and JSONL scan linearly. |
|
|
158
158
|
| `contextBudget` / `getContextBudgetReport` / `ContextBudgetError` | Opt-in assembler budget on `AssembleProviderInputOptions`; deterministic eviction; omission report in `ProviderRequest.metadata` (kinds/ids/sizes only). |
|
|
159
159
|
| `AgentSession.steer` / `SteerOptions` / pending-steer caps | Mid-run enqueue into active run; optional `softInterrupt`; default 8 msgs / 64 KiB UTF-8. |
|
|
160
|
-
| `SessionSearchUnsupportedError` / `sessionSearchMode` | Memory opt-out
|
|
160
|
+
| `SessionSearchUnsupportedError` / `sessionSearchMode` | Memory opt-out (`sessionSearchMode: "unsupported"`); typed throw (not empty success). Memory linear caps are host-overridable via `CreateMemorySessionStoreOptions.search`; the JSONL store searches linearly with the contract default caps. |
|
|
161
161
|
| `BranchRecord` / `BranchQuery` | Branch handle/leaf pointer and query filters (session, name, parent branch, leaf presence). |
|
|
162
162
|
| `SessionEntryQuery` | Paginated entry filters: `sessionId`, `runId`, `parentId`, `leafId`, `kind`, timestamp range, ownership. |
|
|
163
163
|
| `RunRecord` / `RunQuery` | Stored run and filters: session, branch, status, timestamps, ownership. |
|
package/docs/rag.md
CHANGED
|
@@ -28,6 +28,10 @@ Document lifecycle:
|
|
|
28
28
|
| `deleteSource({ sourceId, store, scope })` | Deletes only matching IDs under exact tenant/resource/corpus scope. |
|
|
29
29
|
| `replaceDocument({ uri, loader, parser, store, scope, ... })` | Loads through a host seam, parses, chunks, and atomically replaces. `sourceId` is required unless loader supplies one. |
|
|
30
30
|
| `syncKnowledge({ connector, checkpoints, checkpoint, store, embedder, scope })` | Paged connector import; cursor CAS only after each committed page. See [Knowledge synchronization](knowledge-sync.md). |
|
|
31
|
+
| `createDeletionPropagator({ scope, vectorStore, authorization })` | Privileged deletion orchestration: lineage-closed tombstone set + registered handlers. See [Deletion propagation](#deletion-propagation). |
|
|
32
|
+
| `createRagDeletionHandler({ store, scope, statusStore? })` | The RAG layer's propagation handler: removes a deleted source's chunk rows and ingestion status. |
|
|
33
|
+
| `createLocalReranker({ model?, runtime?, … })` | Zero-service default reranker: in-process cross-encoder behind the `LocalRerankRuntime` seam. See [Local reranker](#local-reranker). |
|
|
34
|
+
| `resolveReranker(config)` | Declarative reranker config (`kind: "local" \| "tei" \| "openai-compatible" \| "voyage" \| "fake" \| "none"`) → `Reranker`. |
|
|
31
35
|
| `createGoogleDriveConnector({ tokenProvider, resolveAccess })` | Drive `files.list` + `changes.list` connector. Host maps permissions; watch payloads are not authorization. |
|
|
32
36
|
| `DocumentLoader` / `Parser` | Small host-replaceable seams. `@arnilo/prism-memory/rag/loaders` and `/rag/parsers` export reference adapters. |
|
|
33
37
|
| `textParser` / `markdownParser` / `htmlParser` / `pdfParser` | UTF-8 text, Markdown, script/style-stripping HTML, and uncompressed-text PDF parsers. |
|
|
@@ -83,6 +87,98 @@ Default/hard ceilings include 1,000/16,384 chunk characters, 100/4,096 overlap,
|
|
|
83
87
|
}
|
|
84
88
|
```
|
|
85
89
|
|
|
90
|
+
## Deletion propagation
|
|
91
|
+
|
|
92
|
+
`deleteSource()` removes one source's chunk rows. Derived artifacts (summaries, observational-memory entries, compiled wiki pages, host projections) are not chunk rows, so they need an explicit, privileged propagation pass:
|
|
93
|
+
|
|
94
|
+
```ts
|
|
95
|
+
import { createDeletionPropagator, createMemoryVectorStore } from "@arnilo/prism-memory";
|
|
96
|
+
import { createRagDeletionHandler } from "@arnilo/prism-memory/rag";
|
|
97
|
+
import { createWikiDeletionHandler } from "@arnilo/prism-memory/wiki";
|
|
98
|
+
|
|
99
|
+
const store = createMemoryVectorStore();
|
|
100
|
+
const propagator = createDeletionPropagator({
|
|
101
|
+
scope: { tenantId: "t1", resourceId: "docs", threadId: "handbook" },
|
|
102
|
+
vectorStore: store,
|
|
103
|
+
authorization: { tenantId: "t1", principalId: "p1", groupIds: ["eng"] }, // host-verified; ACL-store grants are enforced here
|
|
104
|
+
});
|
|
105
|
+
propagator.register(createRagDeletionHandler({ store, scope: ragScope }));
|
|
106
|
+
propagator.register(createWikiDeletionHandler({ workspaceRoot }));
|
|
107
|
+
|
|
108
|
+
const result = await propagator.propagate("doc:erp-lead");
|
|
109
|
+
// { sourceId, ids, tombstoned, layers: { rag: 4, wiki: 1 }, batched: true }
|
|
110
|
+
```
|
|
111
|
+
|
|
112
|
+
- `propagate(sourceId)` expands the source through `_lineage.sourceIds` (`collectInvalidationIds`, depth 8) into a closed id set, tombstones **all** of it with reason `forgotten` inside one store transaction, then runs every registered handler with `{ sourceId, ids, scope, signal }`. Handlers return how many artifacts they removed (reported per `kind` in `layers`).
|
|
113
|
+
- Tombstones, not deletions, for derived rows: rows stay for explainability (`recall({ explain: true })` reports the invalidation), and lineage links never dangle. Handlers own physical removal (chunk rows, files, ledger entries).
|
|
114
|
+
- Retrieval is belt-and-suspenders: `retrieveContext()` reads per-scope invalidations before assembly and drops any candidate whose record id, `_lineage.sourceIds`, or `_rag.sourceId` is tombstoned — so a delete that lands after the query legs read rows still returns zero hits.
|
|
115
|
+
- `HARD_PROPAGATION_EDGES` (4,096) is the one-pass privileged ceiling; over it the whole delete rejects (fail-closed), never a half-tombstoned document. Each store `invalidate` call carries at most `HARD_INVALIDATION_BATCH` (64) entries.
|
|
116
|
+
- Deletion is privileged: `authorization` is required, tenant-checked, and enforced through the store's existing `checkSourceAccess` ACL when the store declares `authorization: "acl"` (missing grant → `MemoryScopeError` before anything is written). Retrieval paths never construct a propagator.
|
|
117
|
+
|
|
118
|
+
## Grant recheck and re-pointing
|
|
119
|
+
|
|
120
|
+
Retrieval never trusts a grant snapshot. `retrieveContext()` re-asks the store for **each distinct source** it is about to inject, on both sides of the reranker:
|
|
121
|
+
|
|
122
|
+
```ts
|
|
123
|
+
const result = await retrieveContext("approval policy", {
|
|
124
|
+
embedder,
|
|
125
|
+
store,
|
|
126
|
+
scope,
|
|
127
|
+
authorization: hostVerifiedPrincipal, // every query re-reads the live grant
|
|
128
|
+
onAccessDenied: (denial) => audit.write({ kind: "rag.acl_denied", ...denial }),
|
|
129
|
+
});
|
|
130
|
+
// mid-turn revoke → the source is gone from this and every later result
|
|
131
|
+
await store.setSourceAccess(thread, [{ sourceId: "doc:payroll", principalIds: [], accessVersion: 2 }]);
|
|
132
|
+
```
|
|
133
|
+
|
|
134
|
+
- Candidate pre-filter and, when a reranker ran, a fresh post-rerank gate (`createAccessRecheck()`, one instance per query). The post-rerank gate re-reads on purpose: a grant revoked *while the reranker was running* must not leak its text into the prompt.
|
|
135
|
+
- Cost is per source, not per hit: 200 candidates over 50 sources cost 50 lookups per gate, not 200. The in-memory hook does that inside the 5ms budget for 50 sources; a PostgreSQL store pays one indexed `checkSourceAccess` per source per gate.
|
|
136
|
+
- Fail closed, never silently: an absent/revoked/version-mismatched grant and a **thrown** store error both withhold the hits, and every withheld source is reported once through `onAccessDenied` as `{ sourceId, scope, reason: "no_grant" | "check_failed", hits, error? }` (`error` is redacted and capped at 256 chars). The query still completes with the remaining hits. Abort still aborts — it is not reclassified as a denial.
|
|
137
|
+
- There is no per-request off switch: passing `authorization` is what turns the gate on, and the only knob is the audit sink. A store that declares `authorization: "acl"` without `checkSourceAccess` fails closed before ranking.
|
|
138
|
+
- The store's own query/lexical predicate remains the first line of defense (unauthorized text never leaves the store); the boundary recheck also covers stores whose query leg ignores grants, and revokes that land after the query legs have read.
|
|
139
|
+
|
|
140
|
+
When a source's grant identity moves (`doc:a` → `doc:b`, a document re-filed under a new source id), `repointSource()` makes the derived artifacts follow **without re-embedding**:
|
|
141
|
+
|
|
142
|
+
```ts
|
|
143
|
+
import { repointSource } from "@arnilo/prism-memory";
|
|
144
|
+
import { createWikiRepointHandler } from "@arnilo/prism-memory/wiki";
|
|
145
|
+
|
|
146
|
+
const moved = await repointSource({
|
|
147
|
+
scope: { tenantId: "t1", resourceId: "docs", threadId: "handbook" },
|
|
148
|
+
vectorStore: store,
|
|
149
|
+
from: "doc:a",
|
|
150
|
+
to: "doc:b",
|
|
151
|
+
authorization: hostVerifiedPrincipal, // must admit BOTH ids on an ACL store
|
|
152
|
+
handlers: [createWikiRepointHandler({ workspaceRoot })],
|
|
153
|
+
});
|
|
154
|
+
// { from, to, movedChunks, rewrittenEdges, layers: { wiki: 1 }, batched: true }
|
|
155
|
+
```
|
|
156
|
+
|
|
157
|
+
- Chunk rows keep their text, embeddings, offsets, and generation: the row id (`doc:a#0001` → `doc:b#0001`), `_rag.sourceId`, and `_rag.citationId` are rewritten, old ids are deleted, and the whole move lands in one store transaction (`batched: true`) or not at all.
|
|
158
|
+
- Lineage edges (`_lineage.sourceIds`) on derived rows move from `from` to `to` in the same pass, so `createDeletionPropagator()` stays correct afterwards: deleting `doc:b` still tombstones the derived rows, deleting `doc:a` no longer touches them.
|
|
159
|
+
- Privileged like deletion propagation: on a store that declares `authorization: "acl"` the caller must pass an `authorization` that admits **both** the source and the destination, and re-point never creates or copies grants — grant the destination first or the move fails closed. `HARD_REPOINT_RECORDS` (4,096) bounds one pass; over the cap nothing moves.
|
|
160
|
+
- Row ids that already exist at the destination (other than rows of the moved source) abort the move instead of overwriting (`MemoryValidationError`).
|
|
161
|
+
- `createWikiRepointHandler()` moves the wiki projection: manifest `rawSources`/`anchors`, the `sourceFileHashes` entry, every page that names the old path, the index pages, and a `Repointed` log line — no recompilation. `pathsFor(sourceId → paths)` maps ids to paths when they differ.
|
|
162
|
+
- Observational memory: `listInvalidatedIds(vectorStore, scope)` returns the ids a scope currently withholds (`corrected` sources stay) — pass them as `invalidatedIds` to `buildObservationalMemoryProjection()` / recall so already-emitted blocks that rest on a revoked source go stale on the next build instead of being re-injected.
|
|
163
|
+
|
|
164
|
+
## Local reranker
|
|
165
|
+
|
|
166
|
+
The default reranker runs in-process — no service, no credential, no per-query egress:
|
|
167
|
+
|
|
168
|
+
```ts
|
|
169
|
+
import { createHashEmbedder, createMemoryVectorStore } from "@arnilo/prism-memory";
|
|
170
|
+
import { resolveReranker, retrieveContext } from "@arnilo/prism-memory/rag";
|
|
171
|
+
|
|
172
|
+
const reranker = resolveReranker({ kind: "local" }); // Xenova/bge-reranker-base via transformers.js
|
|
173
|
+
const result = await retrieveContext("How do approvals work?", { embedder, store, scope, reranker });
|
|
174
|
+
```
|
|
175
|
+
|
|
176
|
+
- `resolveReranker({ kind: "local" })` is the zero-config path. The model runtime is a host seam exactly like `Embedder`: `createLocalReranker({ model?, runtime?, onLoad?, cacheDir?, dtype?, device?, allowRemoteModels? })`. Pass `runtime: { load(model) → { id, score({ query, documents, signal }) } }` to inject a runtime the host already owns (transformers.js, onnxruntime-node, llama.cpp). With no `runtime`, the built-in loader resolves `@huggingface/transformers` at first use — the package declares no inference dependency (no new dependency name in any manifest) and nothing resolves it at build/install time.
|
|
177
|
+
- Sizing trade-off: model download is one-time and host-cached, per-query latency is CPU-bound and grows with candidates × tokens. A bge-reranker-base class model (≈1.1 GB fp32 / ≈280 MB int8, `dtype: "q8"`) reranks top-50 in tens to low hundreds of ms on CPU dev hardware — measure it with your own runtime and weight cache, then keep `topK`/`queryCandidates` near what recall actually needs; the package guarantees the plumbing (one lazy load, one batched score call per rerank), not the model's speed. The hosted/TEI adapters stay for scale (higher throughput, no local RAM, no download).
|
|
178
|
+
- Cheap by construction: the model loads lazily once per reranker instance, `score` is called once per rerank with every candidate (never one call per document), and `onLoad({ model, loadMs })` is the only opt-in observability — no document text is ever logged. Zero network after load; the built-in loader only touches the model registry at load time, and `allowRemoteModels: false` pins it to local files.
|
|
179
|
+
- Failure is loud: a missing runtime, an unreachable model, or a runtime that returns no per-document scores throws a redacted `RagValidationError` naming the model and the install path (`npm i @huggingface/transformers` or pass `{ runtime }`). There is deliberately **no** silent lexical fallback.
|
|
180
|
+
- `rerankHits` is unchanged and still owns the caps and the trust boundary: local scores reorder the same `RagHit` references (provenance/trust untouched), byte/ms/concurrency limits apply, and abort/timeout/malformed-score cases fail closed.
|
|
181
|
+
|
|
86
182
|
## Implementation example
|
|
87
183
|
|
|
88
184
|
```ts
|
|
@@ -159,11 +255,11 @@ const found = await retrieveContext("leave balance", {
|
|
|
159
255
|
|
|
160
256
|
- Supply any Phase 7-conforming embedder/vector store, including the in-memory reference or PostgreSQL/pgvector adapter.
|
|
161
257
|
- Metadata filtering is package-local after a bounded candidate query so existing vector contracts/adapters remain unchanged. Increase `queryCandidates` only when selective filters measurably need it. `filter` never grants document access.
|
|
162
|
-
- Document ACL is opt-in via `authorization` on `retrieveContext` / `store.query` / `store.lexicalQuery`. Reference memory and PostgreSQL adapters declare `authorization: "acl"` and apply principal/group predicates **before** top-K. `setSourceAccess` replaces grants per source (empty principal+group lists revoke). Access version is independent of embedding generation; an unresolved `accessVersion` denies. Missing grants deny. Stores that omit the capability throw rather than claim protection. Group lists cap at 32.
|
|
258
|
+
- Document ACL is opt-in via `authorization` on `retrieveContext` / `store.query` / `store.lexicalQuery`. Reference memory and PostgreSQL adapters declare `authorization: "acl"` and apply principal/group predicates **before** top-K. `setSourceAccess` replaces grants per source (empty principal+group lists revoke). Access version is independent of embedding generation; an unresolved `accessVersion` denies. Missing grants deny. Stores that omit the capability throw rather than claim protection. Group lists cap at 32. `onAccessDenied` observes the boundary recheck; it never disables it.
|
|
163
259
|
- `Reranker` is a host seam, not a provider integration. Return each redacted candidate ID exactly once; Prism retains canonical hit/provenance/trust fields and exposes `retrievalRank` for diagnostics. Add a hosted reranker only when a host owns its credentials, quota, and retry policy.
|
|
164
260
|
- `createTeiReranker({ baseUrl, model?, timeoutMs?, maxResponseBytes?, ssrf?, allowLoopback?, fetch? })` (`CreateTeiRerankerOptions`) adapts a Hugging Face TEI `POST <baseUrl>/rerank` endpoint (`{query, texts, raw_scores:false}` → `{results:[{index,score}]}`) into the `Reranker` seam. It returns a permutation-only reorder of the same hit objects, so provenance/trust move untouched. Response parsing is strict — short/duplicate/out-of-range indices, non-finite scores, HTTP errors, timeouts, and oversized bodies all fail closed; the `rerankHits` caps (`maxRerankBytes`, `maxRerankMs`, `rerankConcurrency`) still apply around it. The default transport is the core DNS-pinned `pinnedFetch` (redirect-free, byte-bounded to 65,536 by default); HTTPS is required unless `allowLoopback: true` (loopback dev/test) or the host supplies `ssrf`/`fetch` for cluster networking. The adapter validates URL shape only — SSRF policy enforcement stays host-side. No credentials are ever sent; there is no SaaS default URL.
|
|
165
261
|
- Hosted rerank adapters over the same seam (plan 062): `createOpenAiCompatibleReranker({ baseUrl, model?, apiKey?, timeoutMs?, maxResponseBytes?, ssrf?, allowLoopback?, fetch? })` speaks the OpenAI-compatible `POST <baseUrl>/rerank` route (`{model, query, documents}` → `{results:[{index,relevance_score}]}`; pass the version segment in `baseUrl`, e.g. `https://api.jina.ai/v1`), and `createVoyageReranker({ baseUrl, model?, apiKey, … })` adapts Voyage AI (`…/v1/rerank` → `{data:[{index,relevance_score}]}`; `apiKey` required). Both send one request per rerank — no adapter-side batching — never send `top_k` (the retrieval seam owns top-K), return the same permutation-only reorder, and fail closed on the same malformed-response/HTTP/timeout/byte-bound cases. `apiKey` rides as `Authorization: Bearer …` and is never logged; errors carry status/host only. No SaaS default URL — hosts own credentials, quota, and retry policy.
|
|
166
|
-
- `createFakeReranker()` is a network-free deterministic reranker (query-term-overlap scoring, stable ties) and `runRerankerConformance(createReranker)` is the shared network-free conformance for any `Reranker` implementation: empty input → `[]`, output is a permutation of the exact input references (provenance/trust untouched), repeated calls are deterministic.
|
|
262
|
+
- `createFakeReranker()` is a network-free deterministic reranker (query-term-overlap scoring, stable ties) and `runRerankerConformance(createReranker)` is the shared network-free conformance for any `Reranker` implementation: empty input → `[]`, output is a permutation of the exact input references (provenance/trust untouched), repeated calls are deterministic. `createLocalReranker()` passes the same conformance; `resolveReranker({ kind: "local" })` is the zero-service default, and no reranker ever constructs itself from retrieval options (host config only).
|
|
167
263
|
- Hybrid retrieval: pass `lexical: "fts"` (or `"bm25"` when the store supports it) to `retrieveContext()`; the two legs are fused with reciprocal-rank fusion (`fusion: "rrf"`, `rrfK` 60 default; the pure helper `fuseReciprocalRank()` returns `FusedCandidate[]` for custom orchestration). Stores advertise support via `lexicalModes?: readonly LexicalMode[]` and `tokenizeLexical()` is the shared tokenizer. Each hit's provenance `retrieval` field reports `vector`/`lexical`/`hybrid`; fusion internals expose `RetrievalLeg`.
|
|
168
264
|
- Multi-scope retrieve: `scopes: RagScope[]` searches each exact scope against that scope's current generation, then runs **one** RRF over the union and **one** rerank. The query is embedded once. `queryCandidates` is per scope. Duplicate scopes are dropped. `HARD_RETRIEVE_SCOPE_CAP` is 8.
|
|
169
265
|
- Embedder identity/drift guard: `Embedder.id` (memory contract) is stamped onto every vector record as `embedderId`. `retrieveContext()` fails closed with `ERR_PRISM_RAG_EMBEDDER_MISMATCH` when a stored record's `embedderId` or dimensions differ from the active embedder (for example after a model change) — re-index the source before retrieving. Legacy records without an `embedderId` also fail closed, naming the re-index path.
|
|
@@ -178,7 +274,8 @@ const found = await retrieveContext("leave balance", {
|
|
|
178
274
|
## Security and performance notes
|
|
179
275
|
|
|
180
276
|
- Every index/query includes exact tenant/resource/corpus scope; returned records are rechecked and malformed/foreign records fail closed. `retrieveContext` accepts `scope` or `scopes` (never both, never neither). Empty `scopes` is the host “no allowed corpora” path — no embed, no search, no rerank. A hit whose stored scope is not in the requested list fails closed. Generation filters stay per scope.
|
|
181
|
-
- When `authorization` is set, unauthorized text, titles, citations, counts, and reranker payloads never leave the store. Recheck runs after fusion (before rerank) and again after rerank before injection, so
|
|
277
|
+
- When `authorization` is set, unauthorized text, titles, citations, counts, and reranker payloads never leave the store. Recheck runs after fusion (before rerank) and again after rerank before injection, so a revoke that lands between those steps drops the candidate; the post-rerank gate deliberately re-reads the live grant instead of reusing the pre-filter decision, and both gates dedupe to one lookup per distinct source. A thrown grant lookup withholds the source and reports `reason: "check_failed"` rather than failing open or aborting the query. `authorization.tenantId` must match every retrieve scope.
|
|
278
|
+
- Re-pointing is the only path that can re-key a source's row ids; it is ACL-gated on both ends, transactional, capped, and refuses destination collisions. It changes identity metadata only — content, embeddings, provenance, and trust are copied verbatim, and no text is ever re-embedded or re-injected because of a move.
|
|
182
279
|
- Embedding identity is a privacy/consistency boundary: records from a different embedder (or dimension) never silently mingle with new ones — retrieval fails closed and names the re-index path. Generation pointers are scope-scoped: a pointer row belongs to exactly one scope, and visibility is computed inside the store (SQL), never by post-filtering in JS.
|
|
183
280
|
- Source IDs become citation/storage IDs and must be stable non-secret identifiers. Text and user metadata can be redacted before external embedding and persistence.
|
|
184
281
|
- Heading metadata is document text only — it passes through the existing `maxMetadataBytes` cap as chunk metadata; no new content path is introduced.
|
|
@@ -187,6 +284,7 @@ const found = await retrieveContext("leave balance", {
|
|
|
187
284
|
- Remote sources must pass existing resource/media trust, SSRF, MIME, and byte policies before their decoded text reaches this package.
|
|
188
285
|
- `replaceSource()` stages every bounded embedding before opening the store transaction. It requires a source-aware transactional store and fails closed rather than pretending generic upserts are atomic. `createMemoryVectorStore()` supplies the reference `getBySource()` / transaction capability; durable stores must implement equivalent exact-scope behavior.
|
|
189
286
|
- `deleteSource()` rechecks every returned record's tenant/resource/corpus and source metadata before delete. Same source IDs in another corpus remain untouched.
|
|
287
|
+
- Deletion propagation reuses the existing memory ACL (`checkSourceAccess` plus the caller's host-verified `authorization`) and rejects unprivileged callers before writing any tombstone; it is only reachable from the explicit `createDeletionPropagator()` seam, never from `retrieveContext()` or any `filter`/query option. The retrieval-side tombstone guard is an exclusion only — it grants nothing.
|
|
190
288
|
- Parsers enforce byte/page/time caps, abort before and after parsing, decode UTF-8 strictly, and strip HTML script/style content. Parsed and retrieved text remains untrusted inert context; it never gains tool authority.
|
|
191
289
|
- Rerankers receive redacted input under byte/time/concurrency caps. Timeout, abort, unknown/duplicate/missing IDs, oversized input, and reranker failures fail closed; returned objects cannot overwrite Prism provenance/trust fields. The TEI adapter adds fail-closed response parsing (permutation completeness, finite scores) and honors the 65,536-byte response ceiling; SSRF/URL policy is host-side (see Extension notes). The hosted OpenAI-compatible and Voyage adapters carry the same guarantees and add Bearer credentials that are never logged and error messages that never contain document text or the API key.
|
|
192
290
|
- Telemetry is a host-owned seam: `RagTelemetry` adapter (`createRagTelemetry()`) drops anything outside a fixed span-name set and `rag.*`-shaped attribute keys, so raw chunk text never reaches the tracer unless the host's own `attributeFilter` opts it in; when the seam is absent, instrumentation costs nothing.
|
|
@@ -5,28 +5,28 @@
|
|
|
5
5
|
## What it does
|
|
6
6
|
|
|
7
7
|
|
|
8
|
-
Prism's current **0.
|
|
8
|
+
Prism's current **0.9.0** line has **11 publishable manifests**: the root `@arnilo/prism` core package plus **10 workspace packages** — **19 provider adapters** (19 provider adapter subpaths inside the `@arnilo/prism-providers` family), 4 `prism-*` family packages, and 6 capability packages. (Generated by `node scripts/package-truth.mjs` → `scripts/package-truth.json` — the manifest-derived single source for counts, provider membership, umbrella closures, and profile closures.) The last lockstep cut was 0.3.0; Decision B now publishes changed packages independently inside `^0.3.0` — the plan 039 changed-package cut moved root `@arnilo/prism` and every plan-035+ changed package to **0.3.1**, and the plan 050 changed-package cut moved root plus four changed packages to **0.3.2**; the plan 041-044 changed-package cut moves root to **0.3.3** with `@arnilo/prism-memory@0.3.2` (composite recall scoring), `@arnilo/prism-evals@0.3.1` (trace-to-dataset curation), the three session-store packages at **0.3.1** (run-ledger `promptVersion` provenance), and the initial `@arnilo/prism-prompts@0.0.1` (independent opt-in, outside `prism-all`); plan 054 consolidation then folded `@arnilo/prism-browser` and `@arnilo/prism-obscura` into the `@arnilo/prism-web-tools` family as `/browser` and `/obscura` subpaths, folded `@arnilo/prism-rag`, both compaction strategies, `@arnilo/prism-graft`, and `@arnilo/prism-wiki` into the `@arnilo/prism-memory` family as `/rag`, `/compaction/llm`, `/compaction/observational-memory`, `/graft`, and `/wiki` subpaths (deleting the `@arnilo/prism-compaction` profile), and folded all 17 `@arnilo/prism-provider-*` packages into the `@arnilo/prism-providers` family as `/<adapter>` subpaths (Azure/Bedrock/Vertex stop being special all-only manifests); independent publication continues inside `^0.3.0` ranges (which satisfy 0.3.1, 0.3.2, and 0.3.3). This page describes how they are packed, what each tarball contains, how to install them, the required non-optional **caret** `@arnilo/prism@^0.9.0` peer range, the release workflow, and the offline test budget. The measurable 1.0 readiness gates (command-per-gate) live in [`0.1.0-readiness.md`](history/./0.1.0-readiness.md).
|
|
9
9
|
|
|
10
10
|
Core `@arnilo/prism` ships runtime, CLI, templates, and docs. Every code package has a required `@arnilo/prism` peer inside the Decision B window — the caret current spec is `@arnilo/prism@^0.3.3` and every declared window peer satisfies it: packages republishing in the plan 050 cut carry `^0.3.2`; the plan 039 set keeps `^0.3.1`; unchanged packages keep their `^0.3.0` peer; profiles are pure manifests. The plan 050 republished set declares the required `@arnilo/prism@^0.3.2` peer; the plan 041-044 republished set keeps its existing `^0.3.0` window peer; unchanged packages keep their prior window. Installation activates no provider, listener, database, browser, credential, or tool capability.
|
|
11
11
|
|
|
12
|
-
The **0.
|
|
12
|
+
The **0.9.0 lockstep cut** moved all **eleven** manifests together: the current declared peer is `@arnilo/prism@^0.9.0` on every package, and `release.mjs` lockstep mode fails closed on any internal range that merely satisfies the cut version instead of matching it. The **0.6.0, 0.7.0, and 0.8.0 lockstep cuts** each moved the then-current manifest set together. The independent-publication history above (0.3.x, 0.4.x, 0.5.x) describes how the line grew when packages moved separately.
|
|
13
13
|
|
|
14
14
|
<!-- generated:package-truth:inventory begin -->
|
|
15
15
|
**11 publishable manifests** — root `@arnilo/prism` plus 10 workspace packages (4 `prism-*` family packages, 6 capability packages). Generated by `node scripts/package-truth.mjs --emit-docs` — do not hand-edit.
|
|
16
16
|
|
|
17
17
|
| package | version | notes |
|
|
18
18
|
| --- | --- | --- |
|
|
19
|
-
| `@arnilo/prism` | 0.
|
|
20
|
-
| `@arnilo/prism-channels` | 0.
|
|
21
|
-
| `@arnilo/prism-coding-tools` | 0.
|
|
22
|
-
| `@arnilo/prism-core` | 0.
|
|
23
|
-
| `@arnilo/prism-providers` | 0.
|
|
24
|
-
| `@arnilo/prism-acp-agent` | 0.
|
|
25
|
-
| `@arnilo/prism-ag-ui` | 0.
|
|
26
|
-
| `@arnilo/prism-mcp` | 0.
|
|
27
|
-
| `@arnilo/prism-memory` | 0.
|
|
28
|
-
| `@arnilo/prism-web-tools` | 0.
|
|
29
|
-
| `@arnilo/prism-work` | 0.
|
|
19
|
+
| `@arnilo/prism` | 0.9.0 | core — runtime, CLI/RPC, templates, docs |
|
|
20
|
+
| `@arnilo/prism-channels` | 0.9.0 | family — transport-neutral messaging runtime, durable journal, pairing and one-use approvals; official /telegram (private DMs, opt-in granted groups/topics) and experimental pinned signal-cli /signal |
|
|
21
|
+
| `@arnilo/prism-coding-tools` | 0.9.0 | family — /agent, /security, /openapi, /computer-use-linux, /dev, /caveman, /ponytail, /impeccable subpaths |
|
|
22
|
+
| `@arnilo/prism-core` | 0.9.0 | family — /runtime, /sessions, /governance, /credentials, /enterprise, /validation subpaths |
|
|
23
|
+
| `@arnilo/prism-providers` | 0.9.0 | family — all provider adapters as `/<adapter>` subpaths |
|
|
24
|
+
| `@arnilo/prism-acp-agent` | 0.9.0 | capability — ACP adapter |
|
|
25
|
+
| `@arnilo/prism-ag-ui` | 0.9.0 | capability — AG-UI/A2A/A2UI adapter |
|
|
26
|
+
| `@arnilo/prism-mcp` | 0.9.0 | capability — MCP client/server/OAuth interop |
|
|
27
|
+
| `@arnilo/prism-memory` | 0.9.0 | capability — memory plus /rag, /compaction/*, /fabric, /graft, /wiki subpaths |
|
|
28
|
+
| `@arnilo/prism-web-tools` | 0.9.0 | capability — Brave/Exa/Firecrawl plus peer-gated /browser and /obscura subpaths |
|
|
29
|
+
| `@arnilo/prism-work` | 0.9.0 | capability — /connectors, /documents, /sheets, /diagrams, /document-reader, /sandbox, /skills, /tools subpaths |
|
|
30
30
|
<!-- generated:package-truth:inventory end -->
|
|
31
31
|
|
|
32
32
|
|
|
@@ -35,26 +35,26 @@ The **0.8.0 lockstep cut** moved all **eleven** manifests together: the current
|
|
|
35
35
|
|
|
36
36
|
| adapter package | version |
|
|
37
37
|
| --- | --- |
|
|
38
|
-
| `@arnilo/prism-providers/ai-sdk` | 0.
|
|
39
|
-
| `@arnilo/prism-providers/alibaba` | 0.
|
|
40
|
-
| `@arnilo/prism-providers/anthropic` | 0.
|
|
41
|
-
| `@arnilo/prism-providers/azure` | 0.
|
|
42
|
-
| `@arnilo/prism-providers/bedrock` | 0.
|
|
43
|
-
| `@arnilo/prism-providers/clinepass` | 0.
|
|
44
|
-
| `@arnilo/prism-providers/commandcode` | 0.
|
|
45
|
-
| `@arnilo/prism-providers/deepseek` | 0.
|
|
46
|
-
| `@arnilo/prism-providers/google` | 0.
|
|
47
|
-
| `@arnilo/prism-providers/hyper` | 0.
|
|
48
|
-
| `@arnilo/prism-providers/kimi` | 0.
|
|
49
|
-
| `@arnilo/prism-providers/model-discovery` | 0.
|
|
50
|
-
| `@arnilo/prism-providers/neuralwatt` | 0.
|
|
51
|
-
| `@arnilo/prism-providers/ollama` | 0.
|
|
52
|
-
| `@arnilo/prism-providers/openai` | 0.
|
|
53
|
-
| `@arnilo/prism-providers/opencode-go` | 0.
|
|
54
|
-
| `@arnilo/prism-providers/openrouter` | 0.
|
|
55
|
-
| `@arnilo/prism-providers/vertex` | 0.
|
|
56
|
-
| `@arnilo/prism-providers/xai` | 0.
|
|
57
|
-
| `@arnilo/prism-providers/zai` | 0.
|
|
38
|
+
| `@arnilo/prism-providers/ai-sdk` | 0.9.0 |
|
|
39
|
+
| `@arnilo/prism-providers/alibaba` | 0.9.0 |
|
|
40
|
+
| `@arnilo/prism-providers/anthropic` | 0.9.0 |
|
|
41
|
+
| `@arnilo/prism-providers/azure` | 0.9.0 |
|
|
42
|
+
| `@arnilo/prism-providers/bedrock` | 0.9.0 |
|
|
43
|
+
| `@arnilo/prism-providers/clinepass` | 0.9.0 |
|
|
44
|
+
| `@arnilo/prism-providers/commandcode` | 0.9.0 |
|
|
45
|
+
| `@arnilo/prism-providers/deepseek` | 0.9.0 |
|
|
46
|
+
| `@arnilo/prism-providers/google` | 0.9.0 |
|
|
47
|
+
| `@arnilo/prism-providers/hyper` | 0.9.0 |
|
|
48
|
+
| `@arnilo/prism-providers/kimi` | 0.9.0 |
|
|
49
|
+
| `@arnilo/prism-providers/model-discovery` | 0.9.0 |
|
|
50
|
+
| `@arnilo/prism-providers/neuralwatt` | 0.9.0 |
|
|
51
|
+
| `@arnilo/prism-providers/ollama` | 0.9.0 |
|
|
52
|
+
| `@arnilo/prism-providers/openai` | 0.9.0 |
|
|
53
|
+
| `@arnilo/prism-providers/opencode-go` | 0.9.0 |
|
|
54
|
+
| `@arnilo/prism-providers/openrouter` | 0.9.0 |
|
|
55
|
+
| `@arnilo/prism-providers/vertex` | 0.9.0 |
|
|
56
|
+
| `@arnilo/prism-providers/xai` | 0.9.0 |
|
|
57
|
+
| `@arnilo/prism-providers/zai` | 0.9.0 |
|
|
58
58
|
<!-- generated:package-truth:providers end -->
|
|
59
59
|
|
|
60
60
|
|
|
@@ -125,6 +125,7 @@ Run `npm run clean` explicitly after deleting source files or switching branches
|
|
|
125
125
|
| `@arnilo/prism/testing/state-concurrency-conformance` | `dist/testing/state-concurrency-conformance.{js,d.ts}` |
|
|
126
126
|
| `@arnilo/prism/testing/session-store-conformance` | `dist/testing/session-store-conformance.{js,d.ts}` |
|
|
127
127
|
| `@arnilo/prism/testing/compaction-conformance` | `dist/testing/compaction-conformance.{js,d.ts}` |
|
|
128
|
+
| `@arnilo/prism/testing/prefix-stability-conformance` | `dist/testing/prefix-stability-conformance.{js,d.ts}` |
|
|
128
129
|
| `@arnilo/prism/testing/tool-conformance` | `dist/testing/tool-conformance.{js,d.ts}` |
|
|
129
130
|
| `@arnilo/prism/testing/tool-effect-store-conformance` | `dist/testing/tool-effect-store-conformance.{js,d.ts}` |
|
|
130
131
|
| `@arnilo/prism/testing/extension-conformance` | `dist/testing/extension-conformance.{js,d.ts}` |
|
|
@@ -149,7 +150,7 @@ A packed tarball contains only public compiled output and release files:
|
|
|
149
150
|
- Code packages ship `README.md`, `LICENSE`, and `CHANGELOG.md`; family/profile packages ship `README.md` and `CHANGELOG.md`.
|
|
150
151
|
- The core tarball additionally ships the full `docs/` directory (the docs hub), `templates/init/`, and the `templates/` gallery (e.g. `deep-research`) used by `prism init`.
|
|
151
152
|
- `dist/cli.js` and the `bin` link in core.
|
|
152
|
-
- **Tarball filenames.** npm strips the `@scope/` prefix, so the core package `@arnilo/prism` produces a tarball named `arnilo-prism-0.
|
|
153
|
+
- **Tarball filenames.** npm strips the `@scope/` prefix, so the core package `@arnilo/prism` produces a tarball named `arnilo-prism-0.9.0.tgz`; family packages produce `arnilo-prism-core-0.9.0.tgz`, `arnilo-prism-coding-tools-0.9.0.tgz`, `arnilo-prism-providers-0.9.0.tgz` (all 19 adapters inside), `arnilo-prism-channels-0.9.0.tgz`, `arnilo-prism-memory-0.9.0.tgz`, and `arnilo-prism-web-tools-0.9.0.tgz`; capability packages like `arnilo-prism-mcp-0.9.0.tgz` and `arnilo-prism-work-0.9.0.tgz` carry their own package version. Independent-package tags carry their own version. The CLI bin name `prism` is unaffected by the package name (`npx prism` still works; npm allows the bin field to differ from the package name).
|
|
153
154
|
|
|
154
155
|
Excluded from every tarball by `files` negation:
|
|
155
156
|
|
|
@@ -250,13 +251,13 @@ Frozen by Phase 12 Task 0 in `scripts/phase12-freeze-manifest.json` (schema gate
|
|
|
250
251
|
|
|
251
252
|
| Runtime | Supported | Measured in CI |
|
|
252
253
|
| --- | --- | --- |
|
|
253
|
-
| Node | 22, 24 (`engines.node >=22`) | `verify` runs the full `sdk:ready` gate on Node 24; `node22-compat` builds and imports every public root `exports` target on Node 22. Node 20 support was dropped in 0.6.0 (`dev-006`; Node 20 reached upstream end-of-life 2026-04-30); 0.
|
|
254
|
+
| Node | 22, 24 (`engines.node >=22`) | `verify` runs the full `sdk:ready` gate on Node 24; `node22-compat` builds and imports every public root `exports` target on Node 22. Node 20 support was dropped in 0.6.0 (`dev-006`; Node 20 reached upstream end-of-life 2026-04-30); 0.9.0 keeps the same floor. |
|
|
254
255
|
| PostgreSQL | 16 (`pgvector/pgvector:pg16`) | `postgres-integration` service container |
|
|
255
256
|
|
|
256
257
|
## Extension and configuration notes
|
|
257
258
|
|
|
258
259
|
|
|
259
|
-
- **Required `@arnilo/prism` peer.** Every first-party code package declares a non-optional **caret** `@arnilo/prism@^0.
|
|
260
|
+
- **Required `@arnilo/prism` peer.** Every first-party code package declares a non-optional **caret** `@arnilo/prism@^0.9.0` peer (the lockstep 0.9.0 cut rewrote every internal range; the version-literal gate rejects a declared range that only satisfies the cut version) (`peerDependenciesMeta` must not mark `@arnilo/prism` optional; other peers such as `playwright-core` may be optional). **Peer-version policy (plan 030, Decision B — independent packages):** internal ranges stay inside the caret window of the cut they shipped in, so a package may patch independently while consumers remain on a compatible 0.x line. A package outside that window is refused by the release gate until the next coordinated peer bump. Inside the workspace each package also declares `"@arnilo/prism": "file:../.."` in `devDependencies` so `npm install` resolves the peer locally; that devDependency is stripped from consumer installs and is not a runtime dependency.
|
|
260
261
|
- **Public access.** All 56 manifests (root + 55 workspace packages: 49 code packages + 6 pure-manifest family/profile packages — the 10 `prism-*` family/profile set is the 6 pure-manifest profiles plus the 4 code packages `prism-caveman`, `prism-impeccable`, `prism-openapi-tools`, `prism-ponytail`) declare `"publishConfig": { "access": "public" }`; the publisher also passes `--access public` explicitly because scoped packages otherwise default to restricted on first publish.
|
|
261
262
|
- **Shipped vs repository docs.** The npm tarball ships `docs/` pages linked from `docs/index.md` (public API, security, migration, providers, install). It excludes `docs/_evidence/` (per-phase evidence freezes, including `release-0.2.7-evidence.md`), `docs/release-*-evidence.md`, and `docs/api-page-template.md`. Those files remain in git for audit. `dist/__tests__` and `*.map` stay excluded.
|
|
262
263
|
- **Map retention knob.** Source maps are emitted locally but stripped from tarballs by `!dist/**/*.map`. Removing that `files` negation ships maps in releases (larger tarballs, better consumer stack traces).
|
|
@@ -281,7 +282,8 @@ Frozen by Phase 12 Task 0 in `scripts/phase12-freeze-manifest.json` (schema gate
|
|
|
281
282
|
## Security and performance notes
|
|
282
283
|
|
|
283
284
|
|
|
284
|
-
- **Export-count budget.** `scripts/budget-gate.test.mjs` counts each publishable package's public exports (same name classes as `scripts/dead-exports.mjs`) and fails CI when any exceed the `exportCounts` ceilings in `scripts/budgets.json
|
|
285
|
+
- **Export-count budget.** `scripts/budget-gate.test.mjs` counts each publishable package's public exports (same name classes as `scripts/dead-exports.mjs`) and fails CI when any exceed the `exportCounts` ceilings in `scripts/budgets.json`; the failure names the package and the exact delta. Ceilings are the 0.9.0 pre-release baselines (plan 099 Task 0, measured 2026-09-19): `@arnilo/prism` 1445 and `@arnilo/prism-memory` 892, every other ceiling unchanged since its recorded rebaseline. Each raise carries its measured value and the plans that caused it, and `docs/_evidence/phase54-package-map.md` records the same per-package count in its Budget-Gated Exports column. Growth requires removing exports or rebaselining with a recorded reason.
|
|
286
|
+
- **Artifact diet.** The same gate re-packs the root tarball and fails if packed bytes, unpacked bytes, or file count exceed `scripts/budgets.json#root` + 5%; the 0.9.0 pre-release baselines are 1414295 packed / 4647338 unpacked / 533 files (measured 2026-09-19, plan 099 Task 0). Tests, fixtures, plans, scripts, `src/`, and `docs/_evidence/**` stay out of the pack (plan 026 rule), and every page linked from shipped `docs/index.md` must be present.
|
|
285
287
|
- **No secrets or fixtures in tarballs.** Tests, fixtures, `src/`, `plans/`, `.agents/`, `roadmap.md`, and `tsconfig` files are excluded. The `docs avoid real-looking secret examples` docs check and the packaging guard's deny list prevent secret-bearing fixtures from shipping.
|
|
286
288
|
- **Live tests stay opt-in.** The default `npm test` is network-free by construction and never sets these vars. Provider/compaction live gates stay credential-gated and are not set by default or during `sdk:ready`. The PostgreSQL adapter live matrix is the exception that runs in CI via the dedicated `postgres-integration` job (still skipped in the default suite).
|
|
287
289
|
- `PRISM_LIVE_PROVIDER_TESTS=1` — gates the eight provider packages' `src/__tests__/live.test.ts` (`@arnilo/prism-providers/anthropic`, `provider-google`, `provider-openai`, `provider-opencode-go`, `provider-openrouter`, `provider-zai`, `provider-kimi`, `provider-neuralwatt`). Each provider live test also requires its own API key env var and skips safely when it is missing:
|
package/docs/runs-and-usage.md
CHANGED
|
@@ -59,12 +59,49 @@ 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
|
+
|
|
68
105
|
## Clean stops and stop reasons
|
|
69
106
|
|
|
70
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).
|
|
@@ -136,7 +173,7 @@ The adapter receives these record shapes:
|
|
|
136
173
|
| `runId` / `sessionId` / `entryId` | Correlation ids. |
|
|
137
174
|
| `scope` | `provider_turn` for billable source rows; `run_total` for the aggregate. Never sum both scopes. |
|
|
138
175
|
| `turn` / `attempt` | Provider-turn attribution; absent on `run_total`. |
|
|
139
|
-
| `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`. |
|
|
140
177
|
| `recordedAt` | ISO timestamp. |
|
|
141
178
|
|
|
142
179
|
## Cost/catalog freshness (host adapter)
|
|
@@ -272,7 +309,7 @@ const ledger: RunLedger = {
|
|
|
272
309
|
|
|
273
310
|
const agent = createAgent({
|
|
274
311
|
model: { provider: "mock", model: "demo" },
|
|
275
|
-
provider: createMockProvider([providerTextDelta("Hello"), providerDone()]),
|
|
312
|
+
provider: createMockProvider([providerTextDelta("Hello"), providerDone({ inputTokens: 1_000, cacheReadTokens: 800 })]),
|
|
276
313
|
runLedger: ledger,
|
|
277
314
|
ownership: { tenantId: "tenant_a", accountId: "account_a" },
|
|
278
315
|
idempotencyKey: "agent-key",
|
|
@@ -290,7 +327,7 @@ console.log(runs.at(-1)?.status); // succeeded
|
|
|
290
327
|
const billable = usageRows.filter((row) => row.scope === "provider_turn");
|
|
291
328
|
const aggregate = usageRows.find((row) => row.scope === "run_total");
|
|
292
329
|
console.log(cacheUsageReport(aggregate?.usage));
|
|
293
|
-
// { cacheReadTokens:
|
|
330
|
+
// { cacheReadTokens: 800, hitRate: 0.8 } — cacheWriteTokens stays absent when unreported
|
|
294
331
|
```
|
|
295
332
|
|
|
296
333
|
## Extension and configuration notes
|
|
@@ -310,7 +347,7 @@ console.log(cacheUsageReport(aggregate?.usage));
|
|
|
310
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.
|
|
311
348
|
- Billing queries must filter `scope = "provider_turn"`; presentation queries normally read the single `run_total`. `UsageQuery.scope`, `turn`, and `attempt` are explicit filters.
|
|
312
349
|
- Adapters that need upsert semantics can use `RunRecord.id` (== `runId`) as the stable key.
|
|
313
|
-
- 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.
|
|
314
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.
|
|
315
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).
|
|
316
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).
|