@arnilo/prism 0.1.6 → 0.1.7

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/docs/index.md CHANGED
@@ -8,7 +8,7 @@ Prism is a TypeScript/Node.js agent harness. Host apps and extension packages ow
8
8
  ## Identity and governance
9
9
  - [Agent identity](agent-identity.md): host-verified `Principal` / `AgentIdentity`, delegation narrowing, ownership projection, and redacted telemetry refs for enterprise runs/tools/MCP/A2A/workflows; optional OIDC/JWKS verifier adapter (`@arnilo/prism-credentials-node/oidc` — pinned issuer/audience/JWKS, bounded claims, fail closed).
10
10
  - [Policy and audit](policy-and-audit.md): optional `@arnilo/prism-policy` decision ledger (allow/deny/modify/approval), evidence refs only, cursor-paginated WORM export, and durable PostgreSQL composition; 0.0.28 adds the OPA REST evaluator (`@arnilo/prism-policy/opa` — pinned SSRF-checked endpoint, redacted input, fail-closed deny, optional bundle-revision pin).
11
- - [Model routing](model-routing.md): optional `@arnilo/prism-model-router` allow-list/residency/budget/rate/circuit/fallback governance with redacted diagnostics; durable state requires awaited identity-scoped calls.
11
+ - [Model routing](model-routing.md): optional `@arnilo/prism-model-router` allow-list/residency/budget/rate/circuit/fallback governance with redacted diagnostics; durable state requires awaited identity-scoped calls; host-configurable selection policies (reference cost/latency policy ranks by `ModelCost` then in-memory latency EMA fed from `recordOutcome`).
12
12
 
13
13
  ## Agent/session runtime
14
14
  - [Agent/session runtime](agent-session-runtime.md): create explicit or opt-in secure agents/sessions, get direct `AgentRunResult` values from `run`/`prompt`, mid-run `steer` (turn-boundary or softInterrupt), use integrated `stream()`/`resumeAgentRunStream()`, shared batch pending-decisions / sticky run-scope approvals, subscribe to normalized events, and expose opted-in durable lifecycle capabilities (0.1.6 plan 018 closeout `checkpoint-bodies`: optional `includeSkillBodies` persists the exact loaded-skill instructions with the names-only `persistSessionState`, so resume re-renders bodies registry-independently; ≤64 bodies, `maxStateBytes` refuses oversize).
@@ -43,7 +43,7 @@ Prism is a TypeScript/Node.js agent harness. Host apps and extension packages ow
43
43
  - [Provider primitives](provider-primitives.md): shared bounded transport and OpenAI serialization helpers — migrated across first-party providers; native structured-output and observability contracts.
44
44
  - [Provider layer](provider-layer.md): register and resolve host-owned providers/models, choose replace-or-error duplicate policy, create provider events, stream/reconstruct tool-call deltas, use generic provider request options, and test with the mock provider; deprecated provider-level timeout/retry hints point to runtime abort/retry.
45
45
  - [Model registry](model-registry.md): register and resolve `ModelConfig` records with capabilities, limits, cost, cache support metadata, compat data, and duplicate policy.
46
- - [Provider caching](provider-caching.md): use `PromptCacheHints`, `PromptCacheBreakpoint`, `ModelCacheCapabilities`, cache-aware stable-prefix guidance, and shared cache diagnostics helpers; includes the complete per-provider explicit/implicit cache matrix plus no-Prism-cache entries (including Anthropic, Google, Alibaba, Ollama, cloud adapters, and host-owned AI SDK); cache hints are best-effort and cache keys are never secrets.
46
+ - [Provider caching](provider-caching.md): use `PromptCacheHints`, `PromptCacheBreakpoint`, `ModelCacheCapabilities`, cache-aware stable-prefix guidance, and shared cache diagnostics helpers; includes the complete per-provider explicit/implicit cache matrix plus no-Prism-cache entries (including Anthropic, Google, Alibaba, Ollama, cloud adapters, and host-owned AI SDK); cache hints are best-effort and cache keys are never secrets; `createCacheTelemetry()` aggregates per-provider/model hit rate and cache-token totals from the `usage` event stream for tuning the `cache_aware` layout.
47
47
  - [Thinking and reasoning](thinking-and-reasoning.md): portable `ThinkingLevel` helpers (`applyThinkingLevel` / `thinkingCompatFor`) map per-turn effort into provider `compat` fields; model defaults stay on `ModelConfig.compat`; no second options tree.
48
48
  - [Use-case model selection](use-case-model-selection.md): bind `{ model?, provider?, thinkingLevel? }` for observational memory, LLM compaction, and other non-session LLM jobs with explicit session-model fallback via `resolveUseCaseModel`.
49
49
  - [Provider request policies](provider-request-policies.md): chain `ProviderRequestPolicy` hooks, use `createSessionCachePolicy`, and merge legacy/structured cache options safely.
@@ -103,7 +103,7 @@ Prism is a TypeScript/Node.js agent harness. Host apps and extension packages ow
103
103
  - [AG-UI adoption evaluation](ag-ui-adoption.md): official 0.0.57 input/event/capability matrix and shipped hardened MCP/MCP Apps/A2A handshake boundaries.
104
104
 
105
105
  ## CLI/RPC
106
- - [CLI/RPC](cli-rpc.md): Run print/json modes and LF-delimited RPC over the public AgentSession runtime, including mid-run `steer`, branch-handle results, fixed `forkSession`, and `checkout`. `prism init` scaffolds a tiny TypeScript project with one selected provider and an offline mock test.
106
+ - [CLI/RPC](cli-rpc.md): Run print/json modes and LF-delimited RPC over the public AgentSession runtime, including mid-run `steer`, branch-handle results, fixed `forkSession`, and `checkout`. `prism init` scaffolds a tiny TypeScript project with one selected provider and an offline mock test; `prism providers add <name>` scaffolds an OpenAI-compatible provider package (manifest, provider, models, cache helpers, conformance test, docs stub).
107
107
  - [Workflows](workflows.md): optional `@arnilo/prism-workflows` typed bounded DAG orchestration — explicit recursive definition revisions, exact-owner cancellation/active identity, finite hard limits, durable human suspend/resume, schedules/background execution, revocable proactive schedule capability tokens, nested workflows, replay, coordination, events, and optional RPC/Web bindings. Compose coding plans/checkpoints via workspace Markdown + `state.coding` without a second runtime. Interactive TUI (C-012) deferred.
108
108
  - [Workflow orchestration primitives](workflow-orchestration-primitives.md): architecture inventory — workflow adapters consume core `CheckpointStore`, `LeaseStore`, and bounded `EventMultiplexer`; run control and optional RPC commands stay package-local.
109
109
  - [Workflow/TUI scope](workflow-tui-primitives.md): records why 0.0.5 ships workflow APIs/RPC control but no interactive terminal UI.
@@ -129,7 +129,7 @@ Prism is a TypeScript/Node.js agent harness. Host apps and extension packages ow
129
129
  - [Ponytail behavior integration](ponytail.md): optional `@arnilo/prism-ponytail` — upstream Ponytail skills/commands, `ponytail-mode` injector, session `ponytail-mode` persistence; resolves peer `@dietrichgebert/ponytail` or `upstreamPath`; opt-in (not in code/sdk profiles).
130
130
 
131
131
  ## Release and install
132
- - [Release and install](release-and-install.md): current **0.1.6** 50-package graph (root + 49 workspace packages, including the plan 018 optional `@arnilo/prism-document-reader` — the graph grew 49 → 50 because its doc-reader closeout was demanded; plan 017 the documented breaking cut — deprecated-option removal with `docs/migration.md` `0.1.4 → 0.1.5` section and reviewed compat-baseline regeneration via `--allow-break` then `--update-baseline`: the inert provider request knobs, `RunOptions.maxToolRounds`, observational-memory flat settings keys + top-level worker aliases, `ReadToolOptions.autoResizeImages`, `INIT_PROVIDERS`; all removals fail closed naming their replacement; plan 016 internal god-module split — `agents.ts`/`contracts.ts` reorganized behind barrel re-exports with a byte-identical public entry surface, measured tree-shaking improvement in `scripts/phase16-baseline.json`, and additive `@arnilo/prism-browser` Chrome DevTools Protocol capabilities — `browser_evaluate`/`browser_observe` and `block_urls`/`unblock_urls`/`throttle`/`emulate` act actions; plan 015 dead-code and deprecation hygiene on the frozen 0.1.x line — parameterized benchmark runner `scripts/benchmark.mjs` absorbing the per-version runners, archived review-coverage evidence in `docs/_evidence/`, non-blocking unused-code sweep `npm run sweep:unused`, opt-in checkpoint persistence for loaded-skill names and read-path sets; plan 014 Alibaba provider enrichment — embeddings, video input, verified compatible-mode surface decision table; plan 013 post-release hardening — build single-flight, MCP SSE relay test, combined coverage summary, canonical manifest-count narrative, ACP modes/config persistence guidance; Phase 12 release-candidate hardening; plan 012 — freeze manifest, compatibility matrix, upgrade matrix, packed-install e2e journeys, restart-recovery evidence, capacity envelopes, security policy), exact-peer/install/tarball rules, deterministic resumable publication and publish dry-run, frozen 0.1.x compatibility and support matrix (Node/PostgreSQL/platform/provider/protocol pins and unsupported combinations, machine-checked against `scripts/phase12-freeze-manifest.json`), protected PostgreSQL gate, pinned supply-chain gates, offline tests, the 0.0.15 provider/AI-SDK/RAG/memory protected live-canary matrix, and sandbox-browser Docker/Playwright gates.
132
+ - [Release and install](release-and-install.md): current **0.1.7** 50-package graph (root + 49 workspace packages, including the plan 018 optional `@arnilo/prism-document-reader` — the graph grew 49 → 50 because its doc-reader closeout was demanded; plan 019 the performance-and-DX patch — dependency-free `createCacheTelemetry()` per-provider/model cache hit/miss aggregator (bounded cardinality with `__overflow__`, token counters/rates only, host-activated), host-configurable `ModelRouterSelectionPolicy` on `createModelRouter` with the reference `createCostLatencySelection` (ModelCost rank then in-memory latency EMA, default ordered behavior byte-identical), `prism providers add <name>` OpenAI-compatible provider scaffold (manifest/provider/models/cache/conformance test/docs stub, npm-name + traversal + symlink-escape validation, placeholders only), and the async `AgUiProjection` verification closeout (plan 009 Task 15 evidence recorded, no new code); plan 017 the documented breaking cut — deprecated-option removal with `docs/migration.md` `0.1.4 → 0.1.5` section and reviewed compat-baseline regeneration via `--allow-break` then `--update-baseline`: the inert provider request knobs, `RunOptions.maxToolRounds`, observational-memory flat settings keys + top-level worker aliases, `ReadToolOptions.autoResizeImages`, `INIT_PROVIDERS`; all removals fail closed naming their replacement; plan 016 internal god-module split — `agents.ts`/`contracts.ts` reorganized behind barrel re-exports with a byte-identical public entry surface, measured tree-shaking improvement in `scripts/phase16-baseline.json`, and additive `@arnilo/prism-browser` Chrome DevTools Protocol capabilities — `browser_evaluate`/`browser_observe` and `block_urls`/`unblock_urls`/`throttle`/`emulate` act actions; plan 015 dead-code and deprecation hygiene on the frozen 0.1.x line — parameterized benchmark runner `scripts/benchmark.mjs` absorbing the per-version runners, archived review-coverage evidence in `docs/_evidence/`, non-blocking unused-code sweep `npm run sweep:unused`, opt-in checkpoint persistence for loaded-skill names and read-path sets; plan 014 Alibaba provider enrichment — embeddings, video input, verified compatible-mode surface decision table; plan 013 post-release hardening — build single-flight, MCP SSE relay test, combined coverage summary, canonical manifest-count narrative, ACP modes/config persistence guidance; Phase 12 release-candidate hardening; plan 012 — freeze manifest, compatibility matrix, upgrade matrix, packed-install e2e journeys, restart-recovery evidence, capacity envelopes, security policy), exact-peer/install/tarball rules, deterministic resumable publication and publish dry-run, frozen 0.1.x compatibility and support matrix (Node/PostgreSQL/platform/provider/protocol pins and unsupported combinations, machine-checked against `scripts/phase12-freeze-manifest.json`), protected PostgreSQL gate, pinned supply-chain gates, offline tests, the 0.0.15 provider/AI-SDK/RAG/memory protected live-canary matrix, and sandbox-browser Docker/Playwright gates.
133
133
  - [0.1.0 / 1.0 readiness gates](0.1.0-readiness.md): command-per-gate 1.0 readiness table — frozen API surface + compat gate, migration/docs tripwires, budget table, live-suite matrix, security matrix, current-line status (**0.0.23** published target), signed-publication/live-canary prerequisites for 1.0, and Phase 12 demand-evidence entry criteria.
134
134
  - [Review coverage archive](_evidence/): per-phase evidence freezes (plans 067–079, releases 0.0.4–0.0.16) — traceability matrices, provider validation, capability/primitive/limit matrices, benchmark budgets, and artifact-diet findings; tarball-excluded, kept in-repo for audit.
135
135
 
@@ -91,6 +91,51 @@ await enterprise.close();
91
91
 
92
92
  Router is optional. Chain returned `providerRequestPolicy` with other `ProviderRequestPolicy` values. Wire `onDiagnostics` to `@arnilo/prism-policy` when audit export is required. OpenRouter package behavior is unchanged; routing metadata participates only when this gate allows it.
93
93
 
94
+ ### Selection policies (0.1.7)
95
+
96
+ By default the router tries candidates in input order: the primary model, then
97
+ `fallbacks` in order. A host can instead supply a `selection` policy on
98
+ `createModelRouter` to rank the candidates before the governance checks run:
99
+
100
+ ```ts
101
+ import { createCostLatencySelection, createModelRouter } from "@arnilo/prism-model-router";
102
+
103
+ const router = createModelRouter({
104
+ resolver,
105
+ selection: createCostLatencySelection({ latencyWeight: 0.5 }),
106
+ fallbacks: [cheaperModel],
107
+ });
108
+
109
+ // host-measured provider-call latency feeds the policy's EMA:
110
+ await router.recordOutcome({ identity, provider, model, success: true, latencyMs: 412 });
111
+ ```
112
+
113
+ `ModelRouterSelectionPolicy` is `{ name, rank(candidates, request), observe? }`:
114
+
115
+ - `rank` must return a **permutation** of the input candidates. Any other
116
+ result (added, dropped, or duplicated candidates) fails closed with
117
+ `ERR_PRISM_MODEL_ROUTER_POLICY` — a policy can never widen the allow-list,
118
+ residency, or budget decisions, because those checks still run per candidate
119
+ after ranking.
120
+ - `observe` receives outcome feedback from `router.recordOutcome`, including
121
+ the host-supplied `latencyMs` (validated finite non-negative).
122
+ - The policy name is recorded in selection diagnostics (the redaction cap
123
+ still applies). Absent `selection`, behavior is identical to 0.1.6.
124
+
125
+ `createCostLatencySelection()` is the reference policy:
126
+
127
+ - Ranks by unit price first: `ModelCost.input` + `output` + `cacheRead`,
128
+ normalized by the cost unit (`per_million_tokens` vs per-token). Models
129
+ without valid cost metadata rank after all priced models, preserving their
130
+ relative input order.
131
+ - Breaks cost ties by recent measured latency — an in-memory per-
132
+ provider/model EMA fed from `recordOutcome` `latencyMs`. Cold start (no
133
+ samples) is pure cost order.
134
+ - `latencyWeight` (default 0.5) is the EMA smoothing factor: 0 keeps the
135
+ first sample, 1 tracks only the latest. `ponytail:` the EMA is in-memory and
136
+ process-local; durable latency statistics would require a
137
+ `ModelRouterStateStore` contract change and are demand-gated.
138
+
94
139
  ## Security and performance notes
95
140
 
96
141
  - Allow-list and residency denies never call the underlying resolver.
@@ -220,6 +220,69 @@ Static featured catalogs remain offline bootstrap and must **not** invent pricin
220
220
  - Cache usage reports contain only usage counts and optional pricing/currency; they do not include prompt text, cache keys, headers, credentials, or provider payloads.
221
221
  - `applyCacheControl()` returns new message objects for stamped anchors and does not mutate input messages.
222
222
 
223
+ ## Cache telemetry
224
+
225
+ ### What it does
226
+
227
+ `createCacheTelemetry()` is a dependency-free aggregator that turns the per-call
228
+ `Usage.cacheReadTokens`/`cacheWriteTokens` counters into per-provider/model
229
+ statistics hosts can use to tune the `cache_aware` input layout: request count,
230
+ cache-read/write token totals, hit rate, and an estimated read-token savings
231
+ when the model carries cost metadata.
232
+
233
+ ### When to use it
234
+
235
+ Use it when you want to observe cache effectiveness per provider/model over a
236
+ session, a day, or a run ledger. It is opt-in by construction: importing the
237
+ module never collects anything — the host explicitly wires `record()` to its
238
+ `usage` `ProviderEvent` stream or to run-ledger usage records.
239
+
240
+ ### Inputs / request
241
+
242
+ | Input | Meaning |
243
+ | --- | --- |
244
+ | `usage` (`Usage`) | One usage record: `cacheReadTokens`, `cacheWriteTokens`, `inputTokens` are validated (non-negative safe integers; a violation throws `CacheTelemetryError` and mutates nothing). |
245
+ | `model` (`ModelConfig?`) | Attribution key (`provider` + `model`). Omit it for provider-only aggregation into the `unknown` bucket. Cost metadata (`ModelCost.input`/`cacheRead`) enables `estimatedSavings`. |
246
+ | `options.maxKeys` | Distinct provider/model keys before excess keys collapse into the `__overflow__` bucket (default `DEFAULT_CACHE_TELEMETRY_CAP` = 256). |
247
+
248
+ ### Outputs / response / events
249
+
250
+ `report()` returns `{ samples, overflowed, totalRequests, totalCacheReadTokens,
251
+ totalCacheWriteTokens }`. Each sample carries `provider`, `model`, `requests`,
252
+ `cacheReadTokens`, `cacheWriteTokens`, `inputTokens`, `hitRate` (total reads /
253
+ total input — the same math as `cacheHitRate()`), and `estimatedSavings` with
254
+ `currency` only when the model has cost metadata. Samples are sorted by
255
+ provider then model. `reset()` clears all samples; `size` reports the current
256
+ distinct-key count.
257
+
258
+ ### Request/response example
259
+
260
+ ```ts
261
+ import { createCacheTelemetry } from "@arnilo/prism";
262
+
263
+ const telemetry = createCacheTelemetry();
264
+ for await (const event of provider.generate(request)) {
265
+ if (event.type === "usage") telemetry.record(event.usage, request.model);
266
+ }
267
+
268
+ const report = telemetry.report();
269
+ for (const sample of report.samples) {
270
+ console.log(sample.provider, sample.model, sample.hitRate, sample.cacheReadTokens);
271
+ }
272
+ ```
273
+
274
+ ### Security and performance notes
275
+
276
+ - Reports carry token counters, rates, currency, and provider/model names only
277
+ — never prompt content, cache keys, headers, credentials, or identity fields
278
+ (redaction-safe by construction).
279
+ - Cardinality is bounded: beyond `maxKeys` distinct provider/model keys, excess
280
+ keys accumulate in a single `__overflow__` bucket; memory cannot grow with
281
+ hostile model names (`ponytail:` ceiling — upgrade to host-configurable caps
282
+ or LRU eviction only if a real deployment exceeds it).
283
+ - `record()` is O(1) per usage event; `report()` is O(keys). No secrets or
284
+ cache keys are accepted or stored.
285
+
223
286
  ## Related APIs
224
287
 
225
288
  - [Input and prompt assembly](input-and-prompt-assembly.md): opt-in cache-aware ordering for stable provider payload prefixes.
@@ -74,6 +74,8 @@ First-party providers map generic `ModelConfig.parameters.maxTokens` to real out
74
74
 
75
75
  ## First-party provider package skeletons
76
76
 
77
+ Scaffold new OpenAI-compatible provider packages with `prism providers add <name>` (see [CLI/RPC](cli-rpc.md#prism-providers-add-017)): it generates the manifest, provider (`createOpenAICompatibleProvider`), starter models, cache-hint helpers, an offline conformance test, and a docs stub — mirroring the first-party skeleton conventions below. Scaffold output is host-chosen and never auto-registered.
78
+
77
79
  Phase 12 adds explicit npm workspaces for [`@arnilo/prism-provider-openai`](providers/openai.md), [`@arnilo/prism-provider-opencode-go`](providers/opencode-go.md), [`@arnilo/prism-provider-openrouter`](providers/openrouter.md), [`@arnilo/prism-provider-zai`](providers/zai.md), [`@arnilo/prism-provider-kimi`](providers/kimi.md), and [`@arnilo/prism-provider-neuralwatt`](providers/neuralwatt.md). Each package starts with a side-effect-free `create*ProviderPackage()` export, README, TypeScript build, network-free default tests, and real opt-in live smoke tests.
78
80
 
79
81
  Phase 6 also adds optional [`@arnilo/prism-provider-ai-sdk`](providers/ai-sdk.md), which adapts a host-owned AI SDK `LanguageModelV4` to Prism's `AIProvider`. It joins `@arnilo/prism-providers` as the seventh adapter while remaining independent from the six HTTP implementations.
@@ -274,6 +274,30 @@ git push origin v0.1.2 # tag push triggers release.yml publish job (prove
274
274
 
275
275
  **Rollback notes.** `release:publish --version 0.1.2 --resume --report release-artifacts/publish-report.json` resumes an interrupted publication and skips only registry versions whose internal dependency fingerprint matches the local manifest. A failed package aborts the run with its status written to the report; re-run after fixing the cause. npm cannot unpublish the `0.1.2` line after 72 hours — a post-publication defect ships as a `0.1.x` patch (additive-only compat promise, `release:gate` enforced), or as a documented break in the next line with a `docs/migration.md` entry. `0.1.2` is store-compatible with `0.1.1` in **both directions** (no migration ran — same checksum-protected contract), so an operator may defer or roll back the patch without a database rollback.
276
276
 
277
+ ### 0.1.7 publish handoff (plan 019 Task 6)
278
+
279
+ **Decision: GO when the operator prerequisites below are recorded.** Release **0.1.7** (plan 019) is the performance-and-DX patch on the frozen 0.1.x line — **additive-only** vs 0.1.6 (plain compat gate at 0.1.7 passed with 0 breaking declaration deltas; the baseline text was regenerated with `--update-baseline` for the version literal only, no `--allow-break` anywhere; freeze manifest `scripts/phase19-freeze-manifest.json` machine-checks each task's diff stayed inside its allowed files). Shipped: (1) **prompt-cache telemetry surface** — dependency-free `createCacheTelemetry()` aggregator in core, host-activated, per-provider/model request counts + aggregate hit rate + cache-read/write token totals + estimated savings, bounded cardinality (cap 256 distinct keys, `__overflow__` bucket), token counters/rates only (never prompt content, cache keys, or identity), O(1) `record()`; (2) **model-router selection policies** — additive `ModelRouterSelectionPolicy` on `createModelRouter` (default ordered behavior byte-identical) with the reference `createCostLatencySelection` ranking by `ModelCost` then in-memory latency EMA fed from `recordOutcome({ latencyMs })`, permutation-only reorder of already-allowed candidates, misbehavior fails closed `ERR_PRISM_MODEL_ROUTER_POLICY`; (3) **async AgUiProjection closeout** — plan 009 Task 15 surface verified with evidence (`asyncHooks: {verified: true, gapFound: false}` in `scripts/phase19-baseline.json`), no new code; (4) **`prism providers add <name>` scaffold** — new CLI subcommand generating an OpenAI-compatible provider package (manifest, provider via `createOpenAICompatibleProvider`, starter models, cache helpers, offline conformance test, docs stub) with npm-name/traversal/symlink-escape validation and placeholders only — never secrets; scaffold output is host-chosen and never auto-registered. Store compatibility with 0.1.6: **compatible, no migration** (additive-only; no persisted-shape change; `docs/migration.md` gains no entries). Exit gate green: npm test core + script gates (incl. phase19-freeze done-phase), `sdk:ready` exit 0, audit 0 moderate, pack dry-run 50/50 twice byte-identical, budget/benchmark gates green; evidence in `scripts/phase19-baseline.json` `exitGate`. Rollback = restore the 0.1.6 manifests/tag.
280
+
281
+ ```bash
282
+ # Operator prerequisites recorded: clean tree at the v0.1.7 tag candidate, GPG key, npm OIDC publisher.
283
+ npm test # core + workspace suites + all script gates
284
+ npm run sdk:ready # typecheck, lint, format, test, pack, release:gate
285
+ node scripts/release.mjs gate --version 0.1.7 # plain additive gate, 0 breaking deltas
286
+ npm run pack:dry-run # twice; diff reports — deterministic
287
+ npm audit --audit-level=moderate
288
+ npm run release:check -- --version 0.1.7 --report /tmp/prism-0.1.7-preflight.json
289
+ npm run release:publish -- --version 0.1.7 --dry-run --allow-dirty --allow-untagged --report /tmp/prism-0.1.7-dry-run.json
290
+ # run the dry-run twice and diff the reports: deterministic, byte-identical
291
+
292
+ # Sign the release on the clean tagged tree (operator GPG key):
293
+ git tag -s v0.1.7 -m "Prism 0.1.7 — performance and DX (additive)"
294
+ git verify-tag v0.1.7
295
+ git push origin v0.1.7 # tag push triggers release.yml publish job (provenance, attestations)
296
+
297
+ # Real publication never bypasses the gates: release.mjs refuses
298
+ # --allow-dirty/--allow-untagged without --dry-run.
299
+ ```
300
+
277
301
  ### 0.1.6 publish handoff (plan 018 Task 7)
278
302
 
279
303
  **Decision: GO when the operator prerequisites below are recorded.** Release **0.1.6** (plan 018) is the coding-agent capability-closeouts patch on the frozen 0.1.x line — **additive-only** vs 0.1.5 (plain compat gate at 0.1.6 passed with 0 breaking declaration deltas; the baseline text was regenerated with `--update-baseline` for the version literal only, no `--allow-break` anywhere). Five demand-gated closeouts shipped, each flipped to `demanded` by named demand evidence (operator `arn` for native-sandbox/doc-reader/delete-glob/checkpoint-bodies, user `Clay` for acp-session-store) before its task landed; the demand-gate registry (`scripts/phase18-freeze-manifest.json`) machine-checks demanded ⇒ implemented, deferred ⇒ untouched. Shipped: (1) **durable ACP session store** — `@arnilo/prism-ag-ui` `AcpSessionStore` host seam (`save`/`loadAll`/`evict`), persisted `{sessionId, ownership, modeId, configValues, cwd, additionalDirectories, updatedAt}`, lazy ownership-scoped restore, fail-closed drops, absent seam = 0.1.5 behavior; (2) **network-free native sandbox** — `createNativeSandbox` in `@arnilo/prism-coding-security` (fresh netns per command via the OS `unshare` binary, chained ulimits with `|| exit 126`, argv-only exec, cwd containment, process-group kill, env allow-list, Linux-only fail-closed); (3) **bounded PDF/Office document reader** — new optional package `@arnilo/prism-document-reader` (the 50th manifest, graph 49 → 50) with optional `pdf-parse`/`mammoth` peers fail-closed at creation, magic-byte gating, null fall-through, caps + redaction at the adapter boundary; (4) **recursive delete + brace-expanding glob** — per-call `recursive: true` with fan-out cap and symlink-unlink-not-follow, host-selected/per-call `braceExpansion` bounded to 128 alternatives / 4096 expanded bytes, fail-closed on overflow/malformed braces; (5) **checkpoint persistence for loaded-skill bodies** — opt-in `includeSkillBodies` on run + resume options (names-only stays default, 0.1.3 shapes byte-identical), ≤64 bodies / ≤256-char names / ≤262144-byte bodies / ≤1 MiB total, `maxStateBytes` refusal, redacted at rest, registry-independent resume render. Store compatibility with 0.1.5: **compatible, no migration** (additive-only; no persisted-shape change; `docs/migration.md` gains no entries). Exit gate green: npm test core 1,433/1,433 + 190 script gates (incl. phase18-freeze done-phase), `sdk:ready` exit 0, audit 0 moderate, pack dry-run 50/50 twice byte-identical, budget/benchmark gates green; evidence in `scripts/phase18-baseline.json` `exitGate`. Rollback = restore the 0.1.5 manifests/tag.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@arnilo/prism",
3
- "version": "0.1.6",
3
+ "version": "0.1.7",
4
4
  "description": "Agent harness for AI providers, agents, sessions, and tools.",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
@@ -143,7 +143,7 @@
143
143
  "build": "npm run build:core && npm run build --workspaces --if-present",
144
144
  "typecheck": "npm run build && npm run typecheck --workspaces --if-present && tsc -p examples --noEmit",
145
145
  "sweep:unused": "node scripts/sweep-unused.mjs",
146
- "test": "npm run build && node --test dist/__tests__/*.test.js && node --test scripts/release-gate.test.mjs scripts/tooling-gate.test.mjs scripts/budget-gate.test.mjs scripts/phase8-conformance.test.mjs scripts/phase9-conformance.test.mjs scripts/phase10-conformance.test.mjs scripts/phase11-conformance.test.mjs scripts/phase11-freeze.test.mjs scripts/phase12-freeze.test.mjs scripts/phase13-freeze.test.mjs scripts/phase14-freeze.test.mjs scripts/phase15-freeze.test.mjs scripts/phase16-freeze.test.mjs scripts/phase17-freeze.test.mjs scripts/phase18-freeze.test.mjs scripts/benchmark-0.1.0.test.mjs scripts/sweep-unused.test.mjs scripts/e2e-enterprise-journey.test.mjs scripts/e2e-coding-journey.test.mjs && npm run test --workspaces --if-present",
146
+ "test": "npm run build && node --test dist/__tests__/*.test.js && node --test scripts/release-gate.test.mjs scripts/tooling-gate.test.mjs scripts/budget-gate.test.mjs scripts/phase8-conformance.test.mjs scripts/phase9-conformance.test.mjs scripts/phase10-conformance.test.mjs scripts/phase11-conformance.test.mjs scripts/phase11-freeze.test.mjs scripts/phase12-freeze.test.mjs scripts/phase13-freeze.test.mjs scripts/phase14-freeze.test.mjs scripts/phase15-freeze.test.mjs scripts/phase16-freeze.test.mjs scripts/phase17-freeze.test.mjs scripts/phase18-freeze.test.mjs scripts/phase19-freeze.test.mjs scripts/benchmark-0.1.0.test.mjs scripts/sweep-unused.test.mjs scripts/e2e-enterprise-journey.test.mjs scripts/e2e-coding-journey.test.mjs && npm run test --workspaces --if-present",
147
147
  "test:coverage": "node --test --experimental-test-coverage --test-coverage-lines=60 --test-coverage-functions=70 --test-coverage-branches=75 --test-coverage-exclude='**/__tests__/**' --test-coverage-exclude='**/node_modules/**' --test-coverage-exclude='**/scripts/**' --test-coverage-exclude='**/packages/**' --test-coverage-exclude='**/examples/**' dist/__tests__/*.test.js && node scripts/coverage-summary.mjs",
148
148
  "coverage:summary": "node scripts/coverage-summary.mjs",
149
149
  "lint": "biome lint .",
@@ -0,0 +1,5 @@
1
+ # Changelog
2
+
3
+ ## __PRISM_VERSION__ — initial scaffold
4
+
5
+ - Generated by `prism providers add __PROVIDER_ID__` (OpenAI-compatible provider package).
@@ -0,0 +1,41 @@
1
+ # __PACKAGE_NAME__
2
+
3
+ __PROVIDER_ID__ provider package for Prism (OpenAI-compatible Chat Completions).
4
+
5
+ ## Quick start
6
+
7
+ ```bash
8
+ npm install __PACKAGE_NAME__ @arnilo/prism
9
+ ```
10
+
11
+ Wire the provider and models into your Prism host. The package registers an
12
+ `api_key` auth method; hosts resolve the credential value — typically from the
13
+ `__ENV_KEY__` environment variable:
14
+
15
+ ```ts
16
+ import { createResolver } from "@arnilo/prism";
17
+ import { create__PROVIDER_PASCAL__ProviderPackage } from "__PACKAGE_NAME__";
18
+
19
+ const resolver = createResolver();
20
+ resolver.registerProviderPackage(
21
+ create__PROVIDER_PASCAL__ProviderPackage({
22
+ apiKey: () => process.env.__ENV_KEY__,
23
+ }),
24
+ );
25
+ ```
26
+
27
+ ## Models
28
+
29
+ Starter catalog in `src/models.ts` (`__MODEL_ID__`). Replace with
30
+ docs-verified model metadata (limits, costs, cache behavior) before publishing.
31
+
32
+ ## Conformance
33
+
34
+ `npm test` builds the package and runs the offline conformance suite wired to
35
+ `@arnilo/prism/testing/provider-conformance` (stream shape, tool-call delta
36
+ reconstruction, header ownership, secret-leak redaction, serialized content
37
+ coverage).
38
+
39
+ ## Docs
40
+
41
+ See `docs/providers/__PROVIDER_ID__.md`.
@@ -0,0 +1,61 @@
1
+ # __PROVIDER_ID__ provider
2
+
3
+ > Scaffold stub — replace with docs-verified provider documentation before publishing.
4
+
5
+ ## What it does
6
+
7
+ `__PACKAGE_NAME__` is an OpenAI-compatible provider package for Prism
8
+ (chat-completions style). It builds on `createOpenAICompatibleProvider` from
9
+ `@arnilo/prism/providers/openai-compatible`.
10
+
11
+ ## When to use it
12
+
13
+ Use it for OpenAI-compatible endpoints that follow the Chat Completions
14
+ convention. Skip it for providers with bespoke serialization or auth.
15
+
16
+ ## Inputs / request
17
+
18
+ - Base URL: `__BASE_URL__` (`--base-url` at scaffold time).
19
+ - Auth: `api_key` auth method; hosts resolve the credential value (e.g. from
20
+ `__ENV_KEY__`).
21
+
22
+ ## Outputs / response / events
23
+
24
+ Standard `ProviderEvent` stream: `content_delta`, `done` (with usage), or
25
+ `error`. Tool calls arrive as deltas and are reconstructed by the host.
26
+
27
+ ## Request/response example
28
+
29
+ ```ts
30
+ import { createResolver } from "@arnilo/prism";
31
+ import { create__PROVIDER_PASCAL__ProviderPackage } from "__PACKAGE_NAME__";
32
+
33
+ const resolver = createResolver();
34
+ resolver.registerProviderPackage(
35
+ create__PROVIDER_PASCAL__ProviderPackage({ apiKey: () => process.env.__ENV_KEY__ }),
36
+ );
37
+ ```
38
+
39
+ ## Implementation example
40
+
41
+ `src/provider.ts` calls `createOpenAICompatibleProvider` with the scaffolded
42
+ base URL, api key, and `doneUsage: true`. Model metadata lives in `src/models.ts`;
43
+ cache-hint mapping helpers live in `src/cache.ts`.
44
+
45
+ ## Extension and configuration notes
46
+
47
+ - Override `baseUrl`, `id`, `models`, `apiKey`, and `fetch` per provider
48
+ package/instance.
49
+ - Cache behavior is scaffolded as `cache: { kind: "implicit" }` — confirm
50
+ against the provider's actual caching before relying on it.
51
+
52
+ ## Security and performance notes
53
+
54
+ - API keys are host-resolved credentials; generated code stores no secrets.
55
+ - Cache keys are identifiers only, sanitized via shared core helpers.
56
+
57
+ ## Related APIs
58
+
59
+ - [OpenAI-compatible provider base](../../providers/openai-compatible.md)
60
+ - [Provider conformance](../../provider-conformance.md)
61
+ - [Provider layer](../../provider-layer.md)
@@ -0,0 +1,49 @@
1
+ {
2
+ "name": "__PACKAGE_NAME__",
3
+ "version": "__PRISM_VERSION__",
4
+ "description": "__PROVIDER_ID__ provider package for Prism.",
5
+ "type": "module",
6
+ "main": "./dist/index.js",
7
+ "types": "./dist/index.d.ts",
8
+ "exports": {
9
+ ".": {
10
+ "types": "./dist/index.d.ts",
11
+ "default": "./dist/index.js"
12
+ }
13
+ },
14
+ "files": [
15
+ "dist",
16
+ "!dist/__tests__",
17
+ "!dist/**/*.map",
18
+ "README.md",
19
+ "CHANGELOG.md"
20
+ ],
21
+ "scripts": {
22
+ "build": "tsc -p tsconfig.json",
23
+ "typecheck": "tsc -p tsconfig.json --noEmit",
24
+ "test": "npm run build && node --test dist/__tests__/*.test.js",
25
+ "pack:dry-run": "npm pack --dry-run"
26
+ },
27
+ "peerDependencies": {
28
+ "@arnilo/prism": "__PRISM_VERSION__"
29
+ },
30
+ "devDependencies": {
31
+ "@types/node": "^22.0.0",
32
+ "typescript": "^5.7.0"
33
+ },
34
+ "engines": {
35
+ "node": ">=20"
36
+ },
37
+ "license": "MIT",
38
+ "keywords": [
39
+ "prism",
40
+ "provider",
41
+ "__PROVIDER_ID__",
42
+ "agent",
43
+ "llm"
44
+ ],
45
+ "sideEffects": false,
46
+ "publishConfig": {
47
+ "access": "public"
48
+ }
49
+ }
@@ -0,0 +1,20 @@
1
+ import type { ModelConfig, ProviderRequestOptions } from "@arnilo/prism";
2
+ import { mapCacheRetention, sanitizeCacheKey } from "@arnilo/prism";
3
+
4
+ /** OpenAI-compatible `prompt_cache_key` accepted length cap. */
5
+ export const __PROVIDER_UPPER___PROMPT_CACHE_KEY_MAX_LENGTH = 64;
6
+
7
+ export function __PROVIDER_ID__PromptCacheKey(options: ProviderRequestOptions | undefined): string | undefined {
8
+ // Sanitize + clamp via the shared core helper so cache keys cannot carry
9
+ // disallowed characters or exceed the provider limit. Cache keys are
10
+ // session/customer identifiers only, never credentials.
11
+ return sanitizeCacheKey(options?.cacheKey ?? options?.sessionId, __PROVIDER_UPPER___PROMPT_CACHE_KEY_MAX_LENGTH);
12
+ }
13
+
14
+ /** Retention mapping via the shared core helper; `"short"`/`"long"` only when the model supports it. */
15
+ export function __PROVIDER_ID__PromptCacheRetention(
16
+ retention: ProviderRequestOptions["cacheRetention"] | undefined,
17
+ model: ModelConfig,
18
+ ): "short" | "long" | undefined {
19
+ return mapCacheRetention(retention, model);
20
+ }
@@ -0,0 +1,33 @@
1
+ import { type CredentialValueSource, defineProviderPackage, type ModelConfig, type ProviderPackage } from "@arnilo/prism";
2
+ import { __PROVIDER_ID__Models } from "./models.js";
3
+ import { create__PROVIDER_PASCAL__Provider } from "./provider.js";
4
+
5
+ export interface __PROVIDER_PASCAL__ProviderPackageOptions {
6
+ readonly apiKey?: CredentialValueSource;
7
+ readonly fetch?: typeof fetch;
8
+ readonly baseUrl?: string;
9
+ readonly id?: string;
10
+ readonly models?: readonly ModelConfig[];
11
+ }
12
+
13
+ export function create__PROVIDER_PASCAL__ProviderPackage(options: __PROVIDER_PASCAL__ProviderPackageOptions = {}): ProviderPackage {
14
+ const providerId = options.id ?? "__PROVIDER_ID__";
15
+ return defineProviderPackage({
16
+ name: "__PACKAGE_NAME__",
17
+ description: "__PROVIDER_ID__ provider package for Prism.",
18
+ docs: { links: ["docs/providers/__PROVIDER_ID__.md"] },
19
+ setup(api) {
20
+ api.registerProvider(create__PROVIDER_PASCAL__Provider(options));
21
+ for (const model of options.models ?? __PROVIDER_ID__Models) api.registerModel({ ...model, provider: providerId });
22
+ api.registerAuthMethod({ kind: "api_key", provider: providerId, credentialName: "apiKey" });
23
+ },
24
+ });
25
+ }
26
+
27
+ export { __PROVIDER_ID__Models, type __PROVIDER_PASCAL__ModelConfig } from "./models.js";
28
+ export { create__PROVIDER_PASCAL__Provider, __PROVIDER_UPPER___DEFAULT_BASE_URL, type __PROVIDER_PASCAL__ProviderOptions } from "./provider.js";
29
+ export {
30
+ __PROVIDER_ID__PromptCacheKey,
31
+ __PROVIDER_ID__PromptCacheRetention,
32
+ __PROVIDER_UPPER___PROMPT_CACHE_KEY_MAX_LENGTH,
33
+ } from "./cache.js";
@@ -0,0 +1,16 @@
1
+ import type { ModelConfig } from "@arnilo/prism";
2
+
3
+ /** Starter catalog for __PROVIDER_ID__: replace with docs-verified model metadata. */
4
+ export interface __PROVIDER_PASCAL__ModelConfig extends Omit<ModelConfig, "provider"> {
5
+ readonly provider: "__PROVIDER_ID__";
6
+ }
7
+
8
+ export const __PROVIDER_ID__Models: readonly __PROVIDER_PASCAL__ModelConfig[] = [
9
+ {
10
+ provider: "__PROVIDER_ID__",
11
+ model: "__MODEL_ID__",
12
+ limits: { contextWindow: 128_000 },
13
+ cost: { input: 1, output: 2, cacheRead: 0.5, unit: "per_million_tokens" },
14
+ cache: { kind: "implicit" },
15
+ },
16
+ ];
@@ -0,0 +1,23 @@
1
+ import type { AIProvider, CredentialValueSource } from "@arnilo/prism";
2
+ import { createOpenAICompatibleProvider } from "@arnilo/prism/providers/openai-compatible";
3
+
4
+ /** Default Chat Completions base URL for __PROVIDER_ID__ (override per provider docs). */
5
+ export const __PROVIDER_UPPER___DEFAULT_BASE_URL = "__BASE_URL__";
6
+
7
+ export interface __PROVIDER_PASCAL__ProviderOptions {
8
+ readonly id?: string;
9
+ readonly baseUrl?: string;
10
+ readonly apiKey?: CredentialValueSource;
11
+ readonly fetch?: typeof fetch;
12
+ }
13
+
14
+ export function create__PROVIDER_PASCAL__Provider(options: __PROVIDER_PASCAL__ProviderOptions = {}): AIProvider {
15
+ return createOpenAICompatibleProvider({
16
+ id: options.id ?? "__PROVIDER_ID__",
17
+ baseUrl: (options.baseUrl ?? __PROVIDER_UPPER___DEFAULT_BASE_URL).replace(/\/+$/, ""),
18
+ apiKey: options.apiKey,
19
+ fetch: options.fetch,
20
+ doneUsage: true,
21
+ requestFailedPrefix: "__PROVIDER_PASCAL__ request failed",
22
+ });
23
+ }
@@ -0,0 +1,104 @@
1
+ import assert from "node:assert/strict";
2
+ import { describe, it } from "node:test";
3
+ import type { ProviderRequest } from "@arnilo/prism";
4
+ import {
5
+ assertNoSecretLeak,
6
+ assertProviderOwnedHeadersWin,
7
+ assertProviderStreamConforms,
8
+ assertSerializedRequestCoversContent,
9
+ assertToolCallDeltasReconstruct,
10
+ } from "@arnilo/prism/testing/provider-conformance";
11
+ import { create__PROVIDER_PASCAL__Provider } from "../index.js";
12
+ import { __PROVIDER_ID__Models } from "../models.js";
13
+
14
+ const request: ProviderRequest = {
15
+ model: __PROVIDER_ID__Models[0],
16
+ messages: [
17
+ { role: "system", content: [{ type: "text", text: "developer instructions" }] },
18
+ { role: "user", content: [{ type: "text", text: "hi" }] },
19
+ ],
20
+ tools: [{ name: "lookup", parameters: { type: "object" }, execute: () => ({ toolCallId: "call_1", name: "lookup", content: [] }) }],
21
+ };
22
+
23
+ const API_KEY = "fake-__PROVIDER_ID__-key";
24
+
25
+ describe("__PROVIDER_ID__ provider scaffold", () => {
26
+ it("streams text, usage, and done; owns its headers; leaks no secrets", async () => {
27
+ let captured: RequestInit | undefined;
28
+ const provider = create__PROVIDER_PASCAL__Provider({
29
+ apiKey: API_KEY,
30
+ fetch: (async (_input, init) => {
31
+ captured = init;
32
+ return ok(
33
+ sse([
34
+ { id: "chatcmpl-1", object: "chat.completion.chunk", choices: [{ index: 0, delta: { role: "assistant", content: "hi" } }] },
35
+ {
36
+ id: "chatcmpl-1",
37
+ object: "chat.completion.chunk",
38
+ choices: [{ index: 0, delta: {} }],
39
+ usage: { prompt_tokens: 5, completion_tokens: 2, total_tokens: 7 },
40
+ },
41
+ ]),
42
+ );
43
+ }) as typeof fetch,
44
+ });
45
+ const events = await assertProviderStreamConforms({
46
+ provider,
47
+ request: {
48
+ ...request,
49
+ options: {
50
+ ...request.options,
51
+ headers: { authorization: "Bearer caller-key", "content-type": "text/plain", "x-caller": "kept" },
52
+ },
53
+ },
54
+ expect: { text: "hi", usage: { inputTokens: 5, outputTokens: 2, totalTokens: 7 } },
55
+ });
56
+ assertNoSecretLeak(events, [API_KEY]);
57
+ const headers = new Headers(captured?.headers);
58
+ assertProviderOwnedHeadersWin(headers, {
59
+ owned: { authorization: `Bearer ${API_KEY}`, "content-type": "application/json" },
60
+ caller: { authorization: "Bearer caller-key", "content-type": "text/plain", "x-caller": "kept" },
61
+ });
62
+ });
63
+
64
+ it("serializes request content and reconstructs tool-call deltas", async () => {
65
+ let body: unknown;
66
+ const provider = create__PROVIDER_PASCAL__Provider({
67
+ apiKey: API_KEY,
68
+ fetch: (async (_input, init) => {
69
+ body = JSON.parse(String(init?.body));
70
+ return ok(
71
+ sse([
72
+ {
73
+ id: "c1",
74
+ object: "chat.completion.chunk",
75
+ choices: [{ index: 0, delta: { tool_calls: [{ index: 0, id: "call_1", function: { name: "lookup", arguments: "" } }] } }],
76
+ },
77
+ {
78
+ id: "c1",
79
+ object: "chat.completion.chunk",
80
+ choices: [{ index: 0, delta: { tool_calls: [{ index: 0, function: { arguments: '{"q":"x"}' } }] } }],
81
+ },
82
+ ]),
83
+ );
84
+ }) as typeof fetch,
85
+ });
86
+ const events = await assertProviderStreamConforms({ provider, request });
87
+ assertToolCallDeltasReconstruct(events, [{ index: 0, id: "call_1", name: "lookup", arguments: { q: "x" } }]);
88
+ assertSerializedRequestCoversContent(request, body);
89
+ });
90
+ });
91
+
92
+ function ok(body: ReadableStream<Uint8Array>): Response {
93
+ return new Response(body, { status: 200 });
94
+ }
95
+
96
+ function sse(events: readonly object[]): ReadableStream<Uint8Array> {
97
+ const text = `${events.map((event) => `data: ${JSON.stringify(event)}\n\n`).join("")}data: [DONE]\n\n`;
98
+ return new ReadableStream({
99
+ start(controller) {
100
+ controller.enqueue(new TextEncoder().encode(text));
101
+ controller.close();
102
+ },
103
+ });
104
+ }
@@ -0,0 +1,16 @@
1
+ {
2
+ "compilerOptions": {
3
+ "target": "ES2022",
4
+ "module": "NodeNext",
5
+ "moduleResolution": "NodeNext",
6
+ "strict": true,
7
+ "outDir": "dist",
8
+ "rootDir": "src",
9
+ "declaration": true,
10
+ "skipLibCheck": true,
11
+ "esModuleInterop": true,
12
+ "forceConsistentCasingInFileNames": true,
13
+ "types": ["node"]
14
+ },
15
+ "include": ["src"]
16
+ }