@arnilo/prism 0.0.16 → 0.0.17

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (54) hide show
  1. package/CHANGELOG.md +20 -0
  2. package/dist/agent-run-state.js +13 -1
  3. package/dist/agents.js +42 -10
  4. package/dist/checkpoints.d.ts +4 -0
  5. package/dist/checkpoints.js +12 -0
  6. package/dist/cli-runner.d.ts +1 -5
  7. package/dist/cli-runner.js +5 -28
  8. package/dist/context-budget.js +6 -3
  9. package/dist/contracts.d.ts +13 -0
  10. package/dist/contributions.d.ts +2 -0
  11. package/dist/contributions.js +3 -0
  12. package/dist/credentials.d.ts +7 -1
  13. package/dist/credentials.js +6 -2
  14. package/dist/event-multiplexer.js +17 -1
  15. package/dist/extensions.d.ts +7 -1
  16. package/dist/extensions.js +64 -6
  17. package/dist/feedback.js +1 -1
  18. package/dist/guardrails.js +9 -3
  19. package/dist/index.d.ts +2 -2
  20. package/dist/index.js +1 -1
  21. package/dist/input.js +11 -4
  22. package/dist/middleware.js +9 -1
  23. package/dist/models.d.ts +2 -0
  24. package/dist/models.js +3 -0
  25. package/dist/providers/openai-compatible.d.ts +42 -1
  26. package/dist/providers/openai-compatible.js +109 -47
  27. package/dist/providers/transport.d.ts +6 -0
  28. package/dist/providers/transport.js +21 -0
  29. package/dist/providers.d.ts +2 -0
  30. package/dist/providers.js +3 -0
  31. package/dist/redaction.js +21 -7
  32. package/dist/retry.d.ts +5 -0
  33. package/dist/retry.js +8 -1
  34. package/dist/run-ledger.d.ts +6 -0
  35. package/dist/run-ledger.js +3 -9
  36. package/dist/session-stores.js +15 -11
  37. package/docs/agent-events.md +2 -1
  38. package/docs/agent-session-runtime.md +2 -2
  39. package/docs/cli-rpc.md +1 -5
  40. package/docs/coding-agent-tools.md +2 -0
  41. package/docs/compaction-and-retry.md +3 -1
  42. package/docs/contribution-registries.md +1 -0
  43. package/docs/credentials-and-redaction.md +1 -1
  44. package/docs/extensions.md +1 -1
  45. package/docs/guardrails.md +13 -2
  46. package/docs/index.md +2 -2
  47. package/docs/input-and-prompt-assembly.md +3 -3
  48. package/docs/middleware-hooks.md +2 -2
  49. package/docs/migration.md +11 -0
  50. package/docs/providers/openai-compatible.md +28 -1
  51. package/docs/public-contracts.md +1 -1
  52. package/docs/release-and-install.md +36 -15
  53. package/docs/session-stores.md +1 -1
  54. package/package.json +1 -1
@@ -31,8 +31,7 @@ async function readBranchFromReader(reader, query) {
31
31
  // indexEntries + the parentId walk order it and still reject missing parents / dupes.
32
32
  return getSessionBranchEntriesCore(items, { leafId: query.leafId });
33
33
  }
34
- function getSessionBranchEntriesCore(entries, options = {}) {
35
- const index = indexEntries(entries);
34
+ function getSessionBranchEntriesCore(entries, options = {}, index = indexEntries(entries)) {
36
35
  const leafId = options.leafId ?? entries.at(-1)?.id;
37
36
  if (!leafId)
38
37
  return [];
@@ -52,7 +51,7 @@ export function listSessionBranches(entries) {
52
51
  const index = indexEntries(entries);
53
52
  return entries
54
53
  .filter((entry) => !index.parentIds.has(entry.id))
55
- .map((entry) => ({ leafId: entry.id, entries: getSessionBranchEntries(entries, { leafId: entry.id }) }));
54
+ .map((entry) => ({ leafId: entry.id, entries: getSessionBranchEntriesCore(entries, { leafId: entry.id }, index) }));
56
55
  }
57
56
  export function rebuildSessionContext(input, options = {}) {
58
57
  if (typeof input === "function") {
@@ -133,17 +132,22 @@ export function createMemorySessionStore(initialEntries = [], options = {}) {
133
132
  if (dedupKey !== undefined && idempotencySeen.has(dedupKey)) {
134
133
  throw new SessionAppendConflictError({ code: SESSION_APPEND_CONFLICT_CODE, idempotencyDuplicate: true });
135
134
  }
136
- // expectedParentId is existence validation (the parent must already be in the
137
- // store or be undefined for a root). Tip-CAS is intentionally NOT used: prism
135
+ // expectedParentId is same-session existence validation (the parent must already
136
+ // be in the store for THIS session, or be undefined for a root). A parent from
137
+ // another session is rejected: the write would be unreadable, since every branch
138
+ // walk is per-session. Tip-CAS is intentionally NOT used: prism
138
139
  // allows branching from any existing leaf (checkout + append), so a stale-but-
139
140
  // existing parent is a valid branch, not a conflict. DB adapters may layer
140
141
  // stricter tip-CAS via unique constraints for linear-only sessions.
141
- if (options?.expectedParentId !== undefined && !byId.has(options.expectedParentId)) {
142
- throw new SessionAppendConflictError({
143
- code: SESSION_APPEND_CONFLICT_CODE,
144
- expectedParentId: options.expectedParentId,
145
- currentLeafId: leafBySession.get(entry.sessionId),
146
- });
142
+ if (options?.expectedParentId !== undefined) {
143
+ const parent = byId.get(options.expectedParentId);
144
+ if (!parent || parent.sessionId !== entry.sessionId) {
145
+ throw new SessionAppendConflictError({
146
+ code: SESSION_APPEND_CONFLICT_CODE,
147
+ expectedParentId: options.expectedParentId,
148
+ currentLeafId: leafBySession.get(entry.sessionId),
149
+ });
150
+ }
147
151
  }
148
152
  if (byId.has(entry.id))
149
153
  throw new Error(`Duplicate session entry id: ${entry.id}`);
@@ -44,7 +44,7 @@ The `AgentEvent` union (grouped by concern):
44
44
  | Assistant messages | `message_started`, `message_delta`, `message_finished` |
45
45
  | Tool execution | `tool_execution_started`, `tool_execution_progress`, `tool_execution_finished`, `tool_execution_error`, `tool_execution_blocked` |
46
46
  | Guardrails | `guardrail_decision` |
47
- | Queue/subscribers | `queue_updated`, `event_subscriber_overflow` |
47
+ | Queue/subscribers | `queue_updated`, `event_subscriber_overflow`, `steer_rejected` |
48
48
  | Compaction | `compaction_started`, `compaction_finished` |
49
49
  | Retry | `retry_scheduled` |
50
50
  | Artifacts | `artifact_validation_started`, `artifact_validation_finished`, `artifact_revision_started`, `artifact_finished`, `artifact_failed` |
@@ -92,6 +92,7 @@ Queue / subscriber / compaction / retry / provider events:
92
92
  | Variant | Fields |
93
93
  | --- | --- |
94
94
  | `queue_updated` | `sessionId`, `runId`, `size: number` |
95
+ | `steer_rejected` | `sessionId`, `runId`, `message: Message` (redacted), `record: GuardrailRecord` — a steered message dropped by a terminal input guardrail; the run continues without it |
95
96
  | `event_subscriber_overflow` | `sessionId`, `droppedEvents: number`, `maxQueuedEvents: number`, `overflow: "close" \| "drop_oldest" \| "drop_newest"` |
96
97
  | `compaction_started` | `sessionId`, `runId?` |
97
98
  | `compaction_finished` | `sessionId`, `runId?`, `summary: string` |
@@ -85,7 +85,7 @@ Only one `run()` may be active per session. Concurrent `run()` / `prompt` / `fol
85
85
 
86
86
  ### Mid-run steer (0.0.11)
87
87
 
88
- `session.steer(input, options?)` enqueues user text into the **same** active run (fail closed when no run). Default: inject at the next turn boundary (after tool rounds / before next provider assemble). `options.softInterrupt: true` aborts only the current provider stream, then continues the same `runId` with steered text. Pending queue caps: **8** messages / **64 KiB** UTF-8 total (`DEFAULT_MAX_PENDING_STEERS` / `DEFAULT_MAX_PENDING_STEER_BYTES`); overflow throws. Steered messages pass input guardrails + normal session append/redaction. Loops drain via optional `LoopContext.hasPendingSteers` / `applyPendingSteers`.
88
+ `session.steer(input, options?)` enqueues user text into the **same** active run (fail closed when no run). Default: inject at the next turn boundary (after tool rounds / before next provider assemble). `options.softInterrupt: true` aborts only the current provider stream, then continues the same `runId` with steered text. Pending queue caps: **8** messages / **64 KiB** UTF-8 total (`DEFAULT_MAX_PENDING_STEERS` / `DEFAULT_MAX_PENDING_STEER_BYTES`); overflow throws. Steered messages pass input guardrails + normal session append/redaction. A `block`/`tripwire` on a steered message drops just that message: Prism emits `guardrail_decision` plus a `steer_rejected` event (redacted message + `GuardrailRecord`) and the run continues; the message never enters history or the session store. Run-start input blocking still fails the run. `interrupt` on a steered message fails closed (durable suspension is only for run-start input). Loops drain via optional `LoopContext.hasPendingSteers` / `applyPendingSteers`.
89
89
 
90
90
  `session.abort(reason)` aborts the active run. The abort signal is passed to input assembly, provider requests, and tool execution; if a tool/provider path aborts after a tool call, Prism does not start another provider turn.
91
91
 
@@ -187,7 +187,7 @@ if (result.status === "suspended") {
187
187
  }
188
188
  ```
189
189
 
190
- Resume requires exact checkpoint ownership, version, agent fingerprint, and revision. Prism CAS-claims approval before work, rechecks normal guardrail/permission/validation/limit paths, and marks a pending tool dispatched before its side effect. `createAgentRunLifecycle()` wraps the same core path for server/MCP hosts: adapters pass only authorized ownership, status returns only `{ state, version }`, and `resolveAgent()` supplies current agent/revision. `resumeStream()` uses that same claim path and bounded subscriber, so adapters do not poll or duplicate resume logic. Remote restart requires both checkpoint and session stores to be durable. A crash after that mark is ambiguous and is never replayed automatically; use host tool idempotency keyed by `runId`/`toolCallId` or resolve it manually. Checkpoints contain bounded redacted state plus session/leaf references, never provider objects, callbacks, signals, credentials, or raw secrets. Only built-in loop options are durable; custom `AgentLoopStrategy` rejects before provider work.
190
+ Resume requires exact checkpoint ownership, version, agent fingerprint, and revision. The fingerprint hashes the agent id/name, `definitionRevision`, model, instructions, system-prompt contributions, skills (name/instructions/tool names), tool definitions (name/parameters/exclusive), guardrail definitions (name/stage/revision), and loop strategy — changing any of them without bumping `definitionRevision` fails resume closed instead of silently continuing with different agent semantics. Prism CAS-claims approval before work, rechecks normal guardrail/permission/validation/limit paths, and marks a pending tool dispatched before its side effect. `createAgentRunLifecycle()` wraps the same core path for server/MCP hosts: adapters pass only authorized ownership, status returns only `{ state, version }`, and `resolveAgent()` supplies current agent/revision. `resumeStream()` uses that same claim path and bounded subscriber, so adapters do not poll or duplicate resume logic. Remote restart requires both checkpoint and session stores to be durable. A crash after that mark is ambiguous and is never replayed automatically; use host tool idempotency keyed by `runId`/`toolCallId` or resolve it manually. Checkpoints contain bounded redacted state plus session/leaf references, never provider objects, callbacks, signals, credentials, or raw secrets. State is bounded at save by `runState.maxStateBytes` (default 256 KB, at most the 1 MB hard cap); load bounds against the 1 MB hard cap only, so state saved with a raised limit stays resumable while oversized records are still rejected. Only built-in loop options are durable; custom `AgentLoopStrategy` rejects before provider work.
191
191
 
192
192
  ## Secure composition
193
193
 
package/docs/cli-rpc.md CHANGED
@@ -45,10 +45,6 @@ Default generation installs only `@arnilo/prism` (mock provider). Selecting a re
45
45
  | `--provider <name>` | Explicit provider id. The built-in `mock` id is only a smoke-test provider. |
46
46
  | `--model <name>` | Explicit model name. |
47
47
  | `--session <id>` | Session id. |
48
- | `--config <path>` | Explicit config path recorded by the adapter; not auto-loaded. |
49
- | `--resource <uri>` | Explicit resource URI recorded by the adapter; not auto-loaded. |
50
- | `--extension <name>` | Explicit extension name recorded by the adapter; not auto-loaded/imported. |
51
- | `--tool <name>` | Explicit tool name recorded by the adapter; not auto-enabled. |
52
48
  | `--system <text>` | System instructions. |
53
49
  | `--context <text>` | Context text reserved for host adapters. |
54
50
  | `--compact <entries>` | Auto-compaction threshold for the run. |
@@ -202,6 +198,6 @@ Suspended workflow resume parameters are `{ workflowId, runId, decision: "approv
202
198
  - [Observational memory compaction package](compaction-observational-memory.md): optional `om:status` and `om:view` command factories for explicitly wired hosts.
203
199
  - [Workflows](workflows.md): optional `createWorkflowCommands()` for direct/background/replay/status/cancel/resume and selected schedule control over the same RPC `command` seam.
204
200
 
205
- The CLI records flags but does not auto-load project-local resources, extensions, tools, or config. The two system/project prompt files are the exception: in print/json modes the CLI auto-loads `<workspaceRoot>/AGENTS.md` (trust-gated) and an app-supplied `SYSTEM.md` layer as `AgentConfig.systemPrompt` layers composed with `--system` (base); `--no-agents-md` / `--no-system-md` skip them and `--agents-md-file` / `--system-md-file` override the paths. The CLI does not default `globalRoot` to the user's home directory — pass it from a host adapter or use `--agents-config <path>` for the app-config bundle layout. RPC mode does not auto-read these files (the host owns the session factory). Hosts must make explicit trust and permission decisions before wiring any other local loading.
201
+ The CLI records flags but does not auto-load project-local resources, extensions, tools, or config. `--config`, `--resource`, `--extension`, and `--tool` were parsed-and-recorded in earlier builds without any effect; they are now rejected loudly (`<flag> is not supported in this build`) until a CLI-harness plan wires them. The two system/project prompt files are the exception: in print/json modes the CLI auto-loads `<workspaceRoot>/AGENTS.md` (trust-gated) and an app-supplied `SYSTEM.md` layer as `AgentConfig.systemPrompt` layers composed with `--system` (base); `--no-agents-md` / `--no-system-md` skip them and `--agents-md-file` / `--system-md-file` override the paths. The CLI does not default `globalRoot` to the user's home directory — pass it from a host adapter or use `--agents-config <path>` for the app-config bundle layout. RPC mode does not auto-read these files (the host owns the session factory). Hosts must make explicit trust and permission decisions before wiring any other local loading.
206
202
 
207
203
  For app-controlled agent bundles under `<configRoot>/agents/<name>/AGENT.md` (including the three-layer `SYSTEM.md` → `AGENT.md` body → repo `AGENTS.md` prompt append and the union skill/tool scopes), see [Agent definitions](agent-definitions.md).
@@ -367,6 +367,8 @@ const shell = createShellTool("/repo", {
367
367
  maxLines: 500,
368
368
  timeout: 600,
369
369
  maxTotalOutputBytes: 64 * 1024 * 1024,
370
+ // Optional: scrub the environment the spawn hook and child process see (default: full process.env clone).
371
+ envAllowlist: ["PATH", "HOME", "LANG"],
370
372
  });
371
373
 
372
374
  const remoteWrite = createWriteTool("/repo", {
@@ -68,10 +68,12 @@ createDefaultRetryPolicy(options?: DefaultRetryPolicyOptions): RetryPolicy
68
68
  | `maxAttempts` | Total provider-turn attempts; defaults to `3`. |
69
69
  | `baseDelayMs` | First retry delay; defaults to `100`. |
70
70
  | `maxDelayMs` | Backoff cap; defaults to `1000`. |
71
+ | `jitter` | Symmetric jitter fraction on computed delays; defaults to `0.25` (±25%). Set `0` for exact delays. |
72
+ | `random` | Random source for jitter (tests); defaults to `Math.random`. |
71
73
  | `secrets` | Exact known secret strings to redact from retry errors/events. |
72
74
  | `metadata` | Explicit host metadata for retry policy context. |
73
75
 
74
- `RunOptions.retry: false` disables configured retry for that run. Default classification retries generic transient codes/messages such as `ETIMEDOUT`, `ECONNRESET`, `429`, `500`, `502`, `503`, `504`, `timeout`, `rate_limit`, and `temporarily_unavailable`; aborts and non-transient errors fail closed.
76
+ `RunOptions.retry: false` disables configured retry for that run. Default classification retries generic transient codes/messages such as `ETIMEDOUT`, `ECONNRESET`, `429`, `500`, `502`, `503`, `504`, `timeout`, `rate_limit`, and `temporarily_unavailable`; aborts and non-transient errors fail closed. Delays are exponential (`baseDelayMs * 2^(attempt-1)`, capped at `maxDelayMs`) with symmetric jitter, so concurrent sessions do not retry in lockstep during a shared outage. When `ErrorInfo.retryAfterMs` is set — first-party HTTP providers populate it from the `Retry-After` response header via `httpStatusError()` — the hint wins over computed backoff, jitter still applies, and the result is always capped at `maxDelayMs` so a hostile or huge hint cannot pin a run.
75
77
 
76
78
  `CompactionEntryData` is stored in `SessionEntry.data` for compaction entries:
77
79
 
@@ -27,6 +27,7 @@ createContributionRegistries(options?: { duplicate?: "replace" | "error" }): Con
27
27
  | Method | Input | Result |
28
28
  | --- | --- | --- |
29
29
  | `register(key, contribution)` | string key and contribution | Stores/replaces the contribution for that key; throws `Duplicate <label>: <key>` when `duplicate: "error"`. |
30
+ | `unregister(key)` | string key | Removes the contribution; returns `false` when the key was not registered. `providers.unregister(id)` and `models.unregister(provider, model)` mirror this on the specialized registries. |
30
31
  | `get(key)` | string key | Returns the contribution or `undefined`. |
31
32
  | `resolve(key)` | string key | Returns the contribution or throws `Unknown <label>: <key>`. |
32
33
  | `list()` | none | Returns contributions in insertion order. |
@@ -132,4 +132,4 @@ A future provider-local OAuth adapter needs published permission for third-party
132
132
  - [LLM compaction package](compaction-llm.md): resolves optional summary-provider credentials per compaction call and redacts exact known values.
133
133
  - [OpenAI-compatible provider](providers/openai-compatible.md): resolves API keys per request and redacts known values from adapter errors.
134
134
 
135
- Phase 10 added `createMemoryCredentialStore()`, `createChainedCredentialResolver()`, and `createSecretRedactor()` for opt-in in-memory auth and runtime redaction. Phase 11 adds OAuth/API-key contracts plus explicit resolver order helpers. Core still has no persistent secret store and does not read environment variables or files for credentials. For durable storage, use [`@arnilo/prism-credentials-node`](credential-storage.md) encrypted-file or keychain backends. See [Security/auth/trust](settings-auth-trust-security.md).
135
+ Phase 10 added `createMemoryCredentialStore()`, `createChainedCredentialResolver()`, and `createSecretRedactor()` for opt-in in-memory auth and runtime redaction. By default the memory store serves a providerless record for a provider-scoped request of the same name — that record is then shared across every provider; pass `{ allowProviderFallback: false }` for exact-match-only resolution (strict provider scoping). Phase 11 adds OAuth/API-key contracts plus explicit resolver order helpers. Core still has no persistent secret store and does not read environment variables or files for credentials. For durable storage, use [`@arnilo/prism-credentials-node`](credential-storage.md) encrypted-file or keychain backends. See [Security/auth/trust](settings-auth-trust-security.md).
@@ -37,7 +37,7 @@ createExtensionEventBus(options?: { errorPolicy?: "event" | "throw"; secrets?: r
37
37
 
38
38
  ## Outputs / response / events
39
39
 
40
- - `kernel.load(extensions)` calls each extension's `setup(api)` in host-provided order.
40
+ - `kernel.load(extensions)` calls each extension's `setup(api)` in host-provided order and resolves to `LoadedExtension[]` (`{ name, dispose() }`). A failed `setup` unwinds that extension's partial registrations (no orphaned half-loads). `dispose()` removes the extension's registry contributions and middleware/event subscriptions via each registry's `unregister(key)` — best-effort, idempotent, and limited to registries/subscriptions: side effects outside the registries (files, network, spawned work) are not unwound, and load-order/dependency graphs between extensions are out of scope.
41
41
  - `kernel.registries` exposes the explicit contribution registry bundle.
42
42
  - `kernel.events.on(type, handler)` registers ordered event handlers and returns an unsubscribe function.
43
43
  - `kernel.events.emit(event)` calls matching handlers in registration order.
@@ -26,11 +26,22 @@ const guardrails: Guardrails = { input: [pii], maxConcurrency: 1 };
26
26
 
27
27
  Set `AgentConfig.guardrails` for every session run or `RunOptions.guardrails` to append checks for one run. `DispatchToolCallOptions.guardrails`, workflow `RunWorkflowOptions.guardrails`, and MCP server `CreatePrismMcpServerOptions.guardrails` apply tool stages to direct calls. A stage has `Guardrail<"input" | "output" | "tool_input" | "tool_output">`, a name, optional revision, and `evaluate(context)` result.
28
28
 
29
- Decisions are `allow`, `block`, `tripwire`, or `interrupt`. Evaluation defaults to declaration-order sequential. `maxConcurrency` may be 1–16; records are emitted in declaration order. Thrown or malformed decisions become a fail-closed tripwire. Decision reasons are capped at 4 KiB and metadata at 16 KiB after JSON normalization and optional redaction.
29
+ Decisions are `allow`, `block`, `tripwire`, or `interrupt`. Evaluation defaults to declaration-order sequential. `maxConcurrency` may be 1–16; records are emitted in declaration order. Thrown or malformed decisions become a fail-closed tripwire. A throwing guardrail produces a `guardrail_failed` record whose `metadata.error` carries the underlying error message — redacted and bounded to 4 KiB — so failures stay diagnosable without leaking internals. Decision reasons are capped at 4 KiB and metadata at 16 KiB after JSON normalization and optional redaction.
30
30
 
31
31
  ## Outputs / response / events
32
32
 
33
- Every evaluated guard produces a redacted `guardrail_decision` `AgentEvent` with a bounded `GuardrailRecord`. Optional OpenTelemetry instrumentation records only controlled stage/action on a short run-child span; guardrail name, reason, and metadata are excluded. An input or output terminal decision rejects the run with `GuardrailError`; `tripwire` stops remaining evaluation. A tool-input or tool-output `block` returns a redacted blocked `ToolResult`; a `tripwire` rejects the enclosing run. `interrupt` is reserved for durable runs and currently fails closed with `ERR_PRISM_GUARDRAIL_INTERRUPT_UNAVAILABLE`.
33
+ Every evaluated guard produces a redacted `guardrail_decision` `AgentEvent` with a bounded `GuardrailRecord`. Optional OpenTelemetry instrumentation records only controlled stage/action on a short run-child span; guardrail name, reason, and metadata are excluded. An input or output terminal decision rejects the run with `GuardrailError`; `tripwire` stops remaining evaluation. A tool-input or tool-output `block` returns a redacted blocked `ToolResult`; a `tripwire` rejects the enclosing run. `interrupt` is reserved for durable runs: at the input stage of a fresh durable run it suspends the run for approval (persisted `input_guardrail` interruption); anywhere else it currently fails closed with `ERR_PRISM_GUARDRAIL_INTERRUPT_UNAVAILABLE`. Resuming a suspended durable run re-evaluates input guardrails on the stored input, and the resume decision itself counts as the approval: a repeated input-stage `interrupt` does not re-suspend or fail the resumed run, while `block`/`tripwire` still reject it.
34
+
35
+ Action outcome by stage:
36
+
37
+ | Stage | `block` | `tripwire` | `interrupt` |
38
+ | --- | --- | --- | --- |
39
+ | `input` | run rejected (`GuardrailError`); steered message: dropped + `steer_rejected`, run continues | run rejected; steered message: dropped + `steer_rejected`, run continues | fresh durable run: suspends for approval; otherwise fails closed |
40
+ | `output` | run rejected | run rejected | fails closed (`ERR_PRISM_GUARDRAIL_INTERRUPT_UNAVAILABLE`) |
41
+ | `tool_input` | blocked `ToolResult`, run continues | run rejected | fails closed |
42
+ | `tool_output` | blocked `ToolResult`, run continues | run rejected | fails closed |
43
+
44
+ The `GuardrailError` message names the stage so unsupported `interrupt` placements are diagnosable without reading core source.
34
45
 
35
46
  Ordering is fixed:
36
47
 
package/docs/index.md CHANGED
@@ -50,7 +50,7 @@ Prism is a TypeScript/Node.js agent harness. Host apps and extension packages ow
50
50
  - Phase 12 package workspaces: [`@arnilo/prism-provider-openai`](providers/openai.md) (Responses hosted-tool attribution, bounded continuation, Realtime session seam), [`@arnilo/prism-provider-anthropic`](providers/anthropic.md) (native Messages, `cache_control`, thinking, caller-gated `listAnthropicModels`), [`@arnilo/prism-provider-google`](providers/google.md) (native Gemini `generateContent` SSE, caller-gated `listGoogleModels`), [`@arnilo/prism-provider-opencode-go`](providers/opencode-go.md) (official Go open models, dual-route Anthropic/OpenAI, caller-gated `listOpenCodeGoModels`, `reasoning_content`/thinking preserve), [`@arnilo/prism-provider-openrouter`](providers/openrouter.md) (app-controlled catalog, caller-gated `listOpenRouterModels`, `reasoning` merge/preserve, `cache_control` + sticky `session_id`), [`@arnilo/prism-provider-zai`](providers/zai.md) (official `thinking`/`reasoning_effort`/`tool_stream`, implicit cache, caller-gated `listZaiModels`), [`@arnilo/prism-provider-kimi`](providers/kimi.md), [`@arnilo/prism-provider-alibaba`](providers/alibaba.md) (Alibaba Cloud Model Studio / DashScope + Coding Plan, OpenAI-compatible, caller-gated `listAlibabaModels`, implicit + explicit `cache_control` caching, Qwen `enable_thinking`), [`@arnilo/prism-provider-ollama`](providers/ollama.md) (Ollama Cloud + local, OpenAI-compatible, caller-gated `listOllamaModels`, implicit-only caching, `reasoning_effort`), and [`@arnilo/prism-provider-neuralwatt`](providers/neuralwatt.md) with implicit vLLM prefix caching, reasoning controls (`reasoning_effort`/`thinking_token_budget`/`enable_thinking`/`preserve_thinking`/`clear_thinking`), reasoning preservation, OpenAI-style tool-call loop, quota, telemetry, and retry classification helpers.
51
51
  - Phase 8 enterprise cloud (workload identity; separate from consumer Anthropic/Google): [`@arnilo/prism-provider-azure`](providers/azure.md) (Entra / Foundry), [`@arnilo/prism-provider-bedrock`](providers/bedrock.md) (IAM/IRSA + region/PrivateLink), [`@arnilo/prism-provider-vertex`](providers/vertex.md) (ADC / Vertex OpenAPI).
52
52
  - Optional AI SDK adapter: [`@arnilo/prism-provider-ai-sdk`](providers/ai-sdk.md) maps host-owned pinned `LanguageModelV4` models onto Prism `AIProvider` streams (offline-tested `@ai-sdk/provider` version matrix; no Prism catalog; maps metadata/tool authority/`finish.usage` cache tokens; reasoning is host-model-owned).
53
- - [OpenAI-compatible provider](providers/openai-compatible.md): optional provider subpath using native or injected `fetch` for Chat Completions streaming (`chatCompletionsUrl` / `authStyle` overrides for enterprise adapters).
53
+ - [OpenAI-compatible provider](providers/openai-compatible.md): optional provider subpath using native or injected `fetch` for Chat Completions streaming (`chatCompletionsUrl` / `authStyle` overrides for enterprise adapters; `buildBodyExtra` / `mapMessages` / `mapUsage` / `extraHeaders` hooks for vendor variants).
54
54
 
55
55
  ## Input, prompt, and context assembly
56
56
  - [SDK customization guide](customization.md): map provider resolution, middleware, context, builders, injectors, loops, compaction, retry, stores, and skills to explicit host-wired APIs.
@@ -117,7 +117,7 @@ Prism is a TypeScript/Node.js agent harness. Host apps and extension packages ow
117
117
  - `examples/`: compile-checked typed examples and runnable mock demos (SDK basics, provider registration, auth, tools, [`examples/ag-ui-server.ts`](../examples/ag-ui-server.ts), [`examples/enterprise-identity.ts`](../examples/enterprise-identity.ts), [`examples/enterprise-policy-audit.ts`](../examples/enterprise-policy-audit.ts), [`examples/enterprise-work-connectors.ts`](../examples/enterprise-work-connectors.ts), [`examples/conversation-durable-replay.ts`](../examples/conversation-durable-replay.ts), [`examples/artifact-review-delivery.ts`](../examples/artifact-review-delivery.ts), [`examples/server-deployment-seams.ts`](../examples/server-deployment-seams.ts), cache-aware prompt assembly, NeuralWatt agent run ([`examples/neuralwatt-agent-run.ts`](../examples/neuralwatt-agent-run.ts)), [`examples/coding-compaction.ts`](../examples/coding-compaction.ts), stores/branching, structured-output/artifact-loop, CLI, RPC, workflow orchestration).
118
118
 
119
119
  ## Release and install
120
- - [Release and install](release-and-install.md): current **0.0.16** 44-package graph (Phase 11 simplification/readiness; one internal codec package, no retirement), exact-peer/install/tarball rules, deterministic resumable publication and publish dry-run, pinned supply-chain gates, offline tests, the 0.0.15 provider/AI-SDK/RAG/memory protected live-canary matrix (still standing for 0.0.16), and sandbox-browser Docker/Playwright gates.
120
+ - [Release and install](release-and-install.md): current **0.0.17** 44-package graph (code-review hardening release; plan 081), exact-peer/install/tarball rules, deterministic resumable publication and publish dry-run, pinned supply-chain gates, offline tests, the 0.0.15 provider/AI-SDK/RAG/memory protected live-canary matrix (still standing for 0.0.16), and sandbox-browser Docker/Playwright gates.
121
121
  - [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 coverage 0.0.5→0.0.16, budget table, live-suite matrix, security matrix, the signed-publication/live-canary prerequisites remaining for 1.0, and the Phase 12 demand-evidence entry criteria.
122
122
  - [Review coverage (2026-07-26 Phase 11)](review-coverage-2026-07-26-phase-11.md): Plan 079 evidence freeze — baseline size/startup/benchmark budgets, hotspot domain extraction table, confirmed duplication survivors (redactor/cleanJson/row-codecs/checkpoints/exec-runner/approval/ownership), profile adoption recommendations, and tarball artifact-diet findings for 0.0.16.
123
123
  - [Review coverage (2026-07-26 Phase 10)](review-coverage-2026-07-26-phase-10.md): Plan 078 evidence freeze — OpenAI hosted tools/continuation/realtime, AI SDK version matrix, remaining provider metadata parity, RAG replaceSource/loaders/parsers/reranker/provenance/ingestion-status, memory export/rebuild/conformance, and 0.0.15 (43 → 43 manifests; no new package) release gates.
@@ -2,9 +2,9 @@
2
2
 
3
3
  ## What it does
4
4
 
5
- `createDefaultInputBuilder()` turns common host input into Prism `Message[]` without starting an agent loop or calling a provider. It accepts strings, `Message`, or `Message[]`, and can add host-supplied instructions, history, summaries, attachments, explicit text resources, tool results, metadata, and optional `input_assembly` middleware.
5
+ `createDefaultInputBuilder()` turns common host input into Prism `Message[]` without starting an agent loop or calling a provider. It accepts strings, `Message`, or `Message[]`, and can add host-supplied instructions, history, summaries, attachments, explicit text resources, tool results, and metadata. It never applies `input_assembly` middleware itself: `assembleProviderInput()` owns that hook and runs it exactly once after whichever `InputBuilder` is installed returns, so a custom builder cannot bypass it.
6
6
 
7
- `createDefaultPromptBuilder()` composes messages, context blocks, selected skills, and host-supplied active tools into provider-ready messages. `assembleProviderInput()` wires input assembly, ordered context resolution, prompt middleware, and prompt composition into a `ProviderRequest` without calling a provider. Layered system prompts are composed before this helper and passed as `systemInstructions`. `renderPromptTemplate()` expands tiny `{{name}}` variables for CLI/RPC prompt strings before input assembly.
7
+ `createDefaultPromptBuilder()` composes messages, context blocks, selected skills, and host-supplied active tools into provider-ready messages. The `Available tools:` text listing is emitted only for models without declared tool support (`model.capabilities.tools !== true` — unknown capability keeps it, fail-safe for text-only providers); tool-capable models receive schemas via `request.tools` and skip the duplicated text. `assembleProviderInput()` wires input assembly, ordered context resolution, prompt middleware, and prompt composition into a `ProviderRequest` without calling a provider. Layered system prompts are composed before this helper and passed as `systemInstructions`. `renderPromptTemplate()` expands tiny `{{name}}` variables for CLI/RPC prompt strings before input assembly.
8
8
 
9
9
  ## When to use it
10
10
 
@@ -161,7 +161,7 @@ const request = await assembleProviderInput({
161
161
  });
162
162
  ```
163
163
 
164
- `input_assembly`, `context`, and `prompt_build` middleware are not global. They run only for helper calls that receive a `MiddlewareRegistry`, in that assembly order. `assembleProviderInput()` keeps provider `tools` equal to the host-supplied active tool list after prompt middleware.
164
+ `input_assembly`, `context`, and `prompt_build` middleware are not global. They run only for helper calls that receive a `MiddlewareRegistry`, in that assembly order. Inside `assembleProviderInput()`, `input_assembly` always runs — for both the default and any custom `InputBuilder`, and on both the plain and context-budget paths. `assembleProviderInput()` keeps provider `tools` equal to the host-supplied active tool list after prompt middleware.
165
165
 
166
166
  ## Security and performance notes
167
167
 
@@ -43,11 +43,11 @@ Built-in hook names:
43
43
  | `run(hook, value)` | hook name and payload | Runs registered middleware and returns the final payload. |
44
44
  | `list(hook)` | hook name | Returns registered middleware for inspection. |
45
45
 
46
- `Middleware<T>` receives `(value, next)` and returns a value or promise. Calling `next(updatedValue)` passes an updated value to later middleware.
46
+ `Middleware<T>` receives `(value, next)` and returns a value or promise. Calling `next(updatedValue)` passes an updated value to later middleware. Two rules are enforced: call `next()` **at most once** — a second call throws (routed through the registry `errorPolicy`) naming hook and index; and **either** `return next(v)` **or** return a new value, never both — when `next(v)` was already called, a conflicting return is discarded and diagnosed via `onError` (the `next()` value wins).
47
47
 
48
48
  ## Outputs / response / events
49
49
 
50
- `run()` returns the transformed value. If no middleware is registered for a hook, `run()` returns the original value. `assembleProviderInput()` calls Phase 5 hooks in this order when middleware is supplied: `input_assembly`, then `context`, then `prompt_build`. The agent/session runtime applies configured provider request policies, then invokes `provider_request` once with the `ProviderRequest` before `AIProvider.generate()`, invokes `tool_call` and `tool_result` through `dispatchToolCall()` for complete provider tool calls, invokes `compaction` with `{ context, result }` after a compaction strategy returns and before the runtime appends its standard compaction entry, and invokes `retry` with `{ context, decision }` before scheduling a provider-turn retry. There is no `provider_response` hook; observing provider output belongs to the provider adapter or subscriber events.
50
+ `run()` returns the transformed value. If no middleware is registered for a hook, `run()` returns the original value. `assembleProviderInput()` calls Phase 5 hooks in this order when middleware is supplied: `input_assembly`, then `context`, then `prompt_build`. The `input_assembly` call is unconditional — it runs after whatever `InputBuilder` produced the messages, so host middleware at that hook cannot be skipped by a custom builder. The agent/session runtime applies configured provider request policies, then invokes `provider_request` once with the `ProviderRequest` before `AIProvider.generate()`, invokes `tool_call` and `tool_result` through `dispatchToolCall()` for complete provider tool calls, invokes `compaction` with `{ context, result }` after a compaction strategy returns and before the runtime appends its standard compaction entry, and invokes `retry` with `{ context, decision }` before scheduling a provider-turn retry. There is no `provider_response` hook; observing provider output belongs to the provider adapter or subscriber events.
51
51
 
52
52
  With default `errorPolicy: "event"`, middleware errors become `extension_error` events when `onError` is provided, and later middleware still runs with the current value. With `errorPolicy: "throw"`, `run()` rejects on the first middleware error.
53
53
 
package/docs/migration.md CHANGED
@@ -1,5 +1,16 @@
1
1
  # Migration guide
2
2
 
3
+ ## 0.0.16 → 0.0.17 code-review hardening (small intentional breaks)
4
+
5
+ Release **0.0.17** implements the 2026-07-29 full implementation review (plan 081): twenty fixes across durable runs, guardrails, retry, extension lifecycle, CLI, and provider plumbing. Most changes are additive or internal; four intentionally change existing behavior:
6
+
7
+ 1. **CLI: inert flags now rejected.** `--config`, `--resource`, `--extension`, and `--tool` were parsed-and-recorded without effect; `parseCliArgs` now throws `CliUsageError("<flag> is not supported in this build")`. The dead `config` / `resources` / `extensions` / `tools` fields were removed from `CliOptions`. Hosts passing those flags must drop them until a CLI-harness plan wires them.
8
+ 2. **`ExtensionKernel.load()` returns handles.** `load(extensions)` now resolves to `LoadedExtension[]` (`{ name, dispose() }`) instead of `void`; callers ignoring the return value are unaffected. A failed `setup` now unwinds that extension's partial registrations. Contribution registries, `ProviderRegistry`, and `ModelRegistry` gain `unregister(...)` (additive).
9
+ 3. **Default prompt builder omits the tool text list for tool-capable models.** When `model.capabilities.tools === true`, the `Available tools:` system message is no longer emitted (schemas already travel via `request.tools`); unknown/`false` capability keeps it. Saves duplicated tokens per turn; observable only in prompt text.
10
+ 4. **Default retry policy applies jitter and honors Retry-After.** `createDefaultRetryPolicy` now applies ±25% jitter (`jitter`/`random` options) and honors `error.retryAfterMs` (populated from provider `Retry-After` headers), capped by `maxDelayMs`. Delays are no longer deterministic unless `random` is injected.
11
+
12
+ Additive-only highlights: `MemoryCredentialStoreOptions.allowProviderFallback` (strict provider scoping opt-in), `createMemoryCheckpointStore` `maxRecords`/`maxValueBytes` bounds, `ShellToolOptions.envAllowlist`, guardrail `steer_rejected` event, `ErrorInfo.retryAfterMs`, agent fingerprint now covers instructions/system prompt/skills (existing durable runs resume or fail fingerprint exactly as before — the fingerprint only got stricter).
13
+
3
14
  ## What it does
4
15
 
5
16
  Prism 0.0.6 preserves documented 0.0.3 agent construction except for two intentional Phase 3 public-API cleanups:
@@ -32,6 +32,22 @@ Options:
32
32
  | `fetch` | `typeof fetch` | Optional fetch implementation for tests or custom hosts. |
33
33
  | `chatCompletionsUrl` | `string \| ((request) => string)` | Optional full chat-completions URL override (Azure deployment paths). |
34
34
  | `authStyle` | `"bearer" \| "api-key" \| "none"` | Auth header style. Default `bearer`. |
35
+ | `buildBodyExtra` | `(request) => JsonObject \| undefined` | Optional provider-specific body fields (thinking/reasoning/cache); merged over the base body. |
36
+ | `mapMessages` | `(request) => readonly Message[]` | Optional message transform before serialization (e.g. cache-control markers). Defaults to `request.messages`. |
37
+ | `mapUsage` | `(usage: unknown) => Usage \| undefined` | Optional usage mapping override (e.g. OpenRouter cost fields). Defaults to `mapOpenAIChatUsage`. |
38
+ | `serializeMessage` | `(message, request) => JsonObject` | Optional custom message serializer (e.g. Z.AI `reasoning_content` replay). Defaults to assert + `serializeOpenAIChatMessage`. |
39
+ | `doneUsage` | `boolean` | Emit the final stream usage on the `done` event (without strict completion checks). |
40
+ | `mapHttpError` | `(response, bodyText, secrets) => Error` | Custom HTTP error mapping (e.g. NeuralWatt retry classification). Receives the response and redacted body text. |
41
+ | `onComment` | `(text) => ProviderEvent \| undefined` | Handle SSE comment lines (text after `:`), e.g. NeuralWatt `: energy` / `: cost` telemetry. Returned events are yielded in stream order. |
42
+ | `extraHeaders` | `(request) => Record<string, string>` | Optional extra request headers; provider auth and `content-type` still win. |
43
+ | `transformBody` | `(body, request) => JsonObject` | Optional final body transform, applied last (token limits, compat stripping); wins over everything. |
44
+ | `strictCompletion` | `boolean` | Require `[DONE]` and a `finish_reason`; truncated streams yield an `error` and `done` carries the final usage. |
45
+ | `requestFailedPrefix` | `string` | Prefix for HTTP error messages. Default `OpenAI-compatible request failed`. |
46
+
47
+ The subpath also exports the building blocks for provider packages that keep public body/stream helpers:
48
+
49
+ - `openAIChatEvents(body, { signal, strictCompletion, doneUsage, mapUsage, onComment })`: the shared SSE stream loop as an `AsyncIterable<ProviderEvent>`.
50
+ - `buildOpenAIChatBody(request, { mapMessages, serializeMessage, buildBodyExtra, transformBody })`: the base Chat Completions request body builder.
35
51
 
36
52
  Provider requests use the standard `ProviderRequest` shape: `model`, `messages`, optional `tools`, `metadata`, and `signal`.
37
53
 
@@ -49,7 +65,7 @@ The returned provider emits normalized `ProviderEvent` values:
49
65
  | `[DONE]` or stream end | `done` event. |
50
66
  | HTTP/stream/parsing error | `error` event with redacted `ErrorInfo`. |
51
67
 
52
- The adapter passes `request.signal` to `fetch` for abort propagation.
68
+ The adapter passes `request.signal` to `fetch` for abort propagation; an already-aborted signal throws before fetch.
53
69
 
54
70
  ## Request/response example
55
71
 
@@ -110,6 +126,17 @@ const provider = createOpenAICompatibleProvider({
110
126
  - The adapter resolves `apiKey` per request through `resolveCredentialValue()`.
111
127
  - This adapter currently targets Chat Completions streaming only.
112
128
  - The serializer preserves text, thinking (downgraded to text), assistant `tool_call` blocks as `tool_calls`, `tool_result` blocks as role `tool` messages, and image blocks when the model declares `capabilities.input` includes `"image"`. Unsupported block placements or unclaimed images fail before fetch.
129
+ - Vendor-specific OpenAI-compatible endpoints (cache markers, thinking bodies, reasoning fields, custom usage) plug in through `buildBodyExtra`/`mapMessages`/`mapUsage`/`extraHeaders` instead of duplicating the stream loop:
130
+
131
+ ```ts
132
+ const provider = createOpenAICompatibleProvider({
133
+ baseUrl: "https://vendor.example/v1",
134
+ apiKey: () => process.env.VENDOR_API_KEY,
135
+ buildBodyExtra: (request) => ({ thinking: { type: "enabled" } }),
136
+ extraHeaders: () => ({ "x-vendor-app": "my-app" }),
137
+ });
138
+ ```
139
+
113
140
  - Cache behavior is intentionally minimal: this Chat Completions adapter sends no `prompt_cache_key`, `prompt_cache_retention`, or `cache_control` fields. Endpoints that cache implicitly do so automatically; hosts needing OpenAI `prompt_cache_key`/`prompt_cache_retention` should use the [`@arnilo/prism-provider-openai`](openai.md) Responses package. The adapter still normalizes cache usage from `prompt_tokens_details.cached_tokens` (and `prompt_cache_hit_tokens`) into `Usage.cacheReadTokens`.
114
141
 
115
142
  ## Security and performance notes
@@ -145,7 +145,7 @@ Important request shapes:
145
145
  | `ConfigLayer` | Named JSON config layer consumed by `mergeConfigLayers()`. |
146
146
  | `PrismManifest` | Data-only package manifest with config defaults, contribution declarations, and resource declarations. |
147
147
  | `ProductionPersistenceStore` | Adapter-facing interface for durable, paginated, multi-tenant storage plus optional `checkpoints?: CheckpointStore`, `leases?: LeaseStore`, and `feedback?: RunFeedbackStore`. No SQL/ORM/host file storage/network dependency. |
148
- | `CheckpointStore` | Generic versioned checkpoint capability: save/load/bounded-list/delete by namespace and key, with ownership, exact-version CAS, and lease fencing. `createMemoryCheckpointStore()` is the reference implementation. |
148
+ | `CheckpointStore` | Generic versioned checkpoint capability: save/load/bounded-list/delete by namespace and key, with ownership, exact-version CAS, and lease fencing. `createMemoryCheckpointStore()` is the reference implementation; it is bounded — `maxRecords` (default 10,000, evicts least-recently-saved) and `maxValueBytes` (default 1 MiB per JSON value). |
149
149
  | `LeaseStore` | Atomic acquire/renew/release/get by namespace and key, with opaque claim tokens, expiry, ownership scope, and monotonically increasing takeover fences. `createMemoryLeaseStore()` is the reference implementation. |
150
150
  | `RunFeedbackStore` | Immutable append, bounded owned query, and owned deletion for ratings/comments/tags linked to existing run/trace/evaluation IDs. `createMemoryRunFeedbackStore()` is the reference implementation. |
151
151
  | `EventMultiplexer<T>` | Generic bounded fan-in from async sources. `createEventMultiplexer()` owns queue limits, overflow policy, abort, source teardown, and close behavior. |
@@ -8,7 +8,7 @@ Core package:
8
8
 
9
9
  - `@arnilo/prism` — the runtime, contracts, registries, streaming events, CLI (including `prism init`), and the `/docs` hub. `files`: `dist` (with `!dist/__tests__` and `!dist/**/*.map` negations), `docs`, `templates`, `CHANGELOG.md`. `bin`: `prism` -> `dist/cli.js`. `sideEffects`: `["dist/cli.js"]`.
10
10
 
11
- First-party workspace packages (each has non-optional `@arnilo/prism@0.0.16` peer and `sideEffects: false`; RAG also peers on memory, and server also peers on workflows):
11
+ First-party workspace packages (each has non-optional `@arnilo/prism@0.0.17` peer and `sideEffects: false`; RAG also peers on memory, and server also peers on workflows):
12
12
 
13
13
  - `@arnilo/prism-provider-anthropic`, `@arnilo/prism-provider-google`, `@arnilo/prism-provider-openai`, `@arnilo/prism-provider-openrouter`, `@arnilo/prism-provider-kimi`, `@arnilo/prism-provider-zai`, `@arnilo/prism-provider-opencode-go`, `@arnilo/prism-provider-neuralwatt` — provider adapters.
14
14
  - `@arnilo/prism-provider-azure`, `@arnilo/prism-provider-bedrock`, `@arnilo/prism-provider-vertex` — optional enterprise-cloud adapters (Entra/IAM/ADC; separate from consumer Anthropic/Google).
@@ -36,7 +36,7 @@ First-party workspace packages (each has non-optional `@arnilo/prism@0.0.16` pee
36
36
 
37
37
  ### 0.0.12 AG-UI package boundary
38
38
 
39
- `@arnilo/prism-ag-ui` is a publishable optional code package with root AG-UI exports and stable `./acp` sibling, peer `@arnilo/prism@0.0.16`, pinned `@ag-ui/core@0.0.57` / `@agentclientprotocol/sdk@1.3.0`, and no import-time network/listener/run. It is included by `@arnilo/prism-all` only—not `@arnilo/prism-code` or `@arnilo/prism-sdk`—so coding and SDK profiles stay free of UI protocol dependencies.
39
+ `@arnilo/prism-ag-ui` is a publishable optional code package with root AG-UI exports and stable `./acp` sibling, peer `@arnilo/prism@0.0.17`, pinned `@ag-ui/core@0.0.57` / `@agentclientprotocol/sdk@1.3.0`, and no import-time network/listener/run. It is included by `@arnilo/prism-all` only—not `@arnilo/prism-code` or `@arnilo/prism-sdk`—so coding and SDK profiles stay free of UI protocol dependencies.
40
40
 
41
41
  Family/profile packages (pure manifests, no code or `dist`; ship `README.md` and `CHANGELOG.md`; use exact hard `dependencies`):
42
42
 
@@ -78,9 +78,9 @@ Consumers install the core package for the runtime and add first-party packages
78
78
  | Run the default (network-free) test suite | `npm test` |
79
79
  | Dry-run pack core + every package | `npm run pack:dry-run` |
80
80
  | Local mirror of the release verify gate | `npm run release:dry-run` |
81
- | Validate clean tag/version/ranges and reject registry collisions | `npm run release:check -- --version 0.0.16` |
82
- | Preview deterministic publish order | `npm run release:publish -- --version 0.0.16 --dry-run --allow-dirty --allow-untagged` |
83
- | Resume interrupted tagged publication | `npm run release:publish -- --version 0.0.16 --resume --report release-artifacts/publish-report.json` |
81
+ | Validate clean tag/version/ranges and reject registry collisions | `npm run release:check -- --version 0.0.17` |
82
+ | Preview deterministic publish order | `npm run release:publish -- --version 0.0.17 --dry-run --allow-dirty --allow-untagged` |
83
+ | Resume interrupted tagged publication | `npm run release:publish -- --version 0.0.17 --resume --report release-artifacts/publish-report.json` |
84
84
  | Full SDK readiness gate (typecheck + offline tests + pack) | `npm run sdk:ready` |
85
85
 
86
86
  Public core import specifiers (from the root `exports` map):
@@ -117,7 +117,7 @@ A packed tarball contains only public compiled output and release files:
117
117
  - Code packages ship `README.md`, `LICENSE`, and `CHANGELOG.md`; family/profile packages ship `README.md` and `CHANGELOG.md`.
118
118
  - The core tarball additionally ships the full `docs/` directory (the docs hub) and `templates/init/` used by `prism init`.
119
119
  - `dist/cli.js` and the `bin` link in core.
120
- - **Tarball filenames.** npm strips the `@scope/` prefix, so the core package `@arnilo/prism` produces a tarball named `arnilo-prism-0.0.16.tgz`; first-party packages produce `arnilo-prism-provider-<name>-0.0.16.tgz` / `arnilo-prism-compaction-<name>-0.0.16.tgz` / `arnilo-prism-coding-agent-0.0.16.tgz`; family/profile packages produce `arnilo-prism-{providers,compaction,base,code,sdk,all}-0.0.16.tgz`. 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).
120
+ - **Tarball filenames.** npm strips the `@scope/` prefix, so the core package `@arnilo/prism` produces a tarball named `arnilo-prism-0.0.17.tgz`; first-party packages produce `arnilo-prism-provider-<name>-0.0.17.tgz` / `arnilo-prism-compaction-<name>-0.0.17.tgz` / `arnilo-prism-coding-agent-0.0.17.tgz`; family/profile packages produce `arnilo-prism-{providers,compaction,base,code,sdk,all}-0.0.17.tgz`. 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).
121
121
 
122
122
  Excluded from every tarball by `files` negation:
123
123
 
@@ -136,9 +136,9 @@ Excluded from every tarball by `files` negation:
136
136
  "name": "host-app",
137
137
  "type": "module",
138
138
  "dependencies": {
139
- "@arnilo/prism": "0.0.16",
140
- "@arnilo/prism-provider-openai": "0.0.16",
141
- "@arnilo/prism-compaction-observational-memory": "0.0.16"
139
+ "@arnilo/prism": "0.0.17",
140
+ "@arnilo/prism-provider-openai": "0.0.17",
141
+ "@arnilo/prism-compaction-observational-memory": "0.0.17"
142
142
  }
143
143
  }
144
144
  ```
@@ -148,7 +148,7 @@ Installing the provider/compaction packages without `@arnilo/prism` present prod
148
148
  ```text
149
149
  npm error code ERESOLVE
150
150
  npm error Could not resolve dependency:
151
- npm error peer @arnilo/prism@"0.0.16" from @arnilo/prism-provider-openai@0.0.16
151
+ npm error peer @arnilo/prism@"0.0.17" from @arnilo/prism-provider-openai@0.0.17
152
152
  ```
153
153
 
154
154
  ## Implementation example
@@ -184,8 +184,8 @@ npm run sdk:ready
184
184
  Release publication derives all **44** manifests from the workspace once, validates exact `0.0.16` manifest/lockfile/internal ranges, then uses deterministic dependency order. `release:check` requires a clean commit tagged `v0.0.16` and rejects any existing registry version. `release:publish --resume` skips only registry versions whose internal dependency fingerprint matches the local manifest; conflicting versions fail closed. Each attempted package is written immediately to the JSON report, so a failed job can rerun safely. `--dry-run` performs registry availability checks and invokes `npm publish --dry-run` with explicit public access, provenance, and `latest` tag, but does not publish.
185
185
 
186
186
  ```bash
187
- npm run release:check -- --version 0.0.16
188
- npm run release:publish -- --version 0.0.16 --dry-run --allow-dirty --allow-untagged
187
+ npm run release:check -- --version 0.0.17
188
+ npm run release:publish -- --version 0.0.17 --dry-run --allow-dirty --allow-untagged
189
189
  ```
190
190
 
191
191
  `--allow-dirty` and `--allow-untagged` exist only for local preview; real publication and CI never pass them. npm registry calls occur only in these release preflight/publication commands, never build/test/package discovery.
@@ -196,6 +196,27 @@ Optional live smoke tests stay separate from SDK readiness because they require
196
196
  PRISM_LIVE_PROVIDER_TESTS=1 npm run test --workspaces --if-present
197
197
  ```
198
198
 
199
+ ### 0.0.17 publish handoff
200
+
201
+ **Decision: GO after protected operator prerequisites below.** Release 0.0.17 implements the 2026-07-29 full implementation review (plan 081, twenty fixes): durable run-state load bound, explicit resume-as-approval, unconditional `input_assembly` middleware, same-session parent enforcement, jitter + `Retry-After`-aware retries with provider error wiring, O(n) context-budget eviction, fingerprint coverage of instructions/system prompt/skills, stage-named guardrail interrupts with `metadata.error`, `steer_rejected`, middleware double-`next()` detection, parked-consumer sorted multiplexer delivery, checkpoint-store bounds, strict credential opt-in, extension `unregister`/dispose handles with failed-setup unwind, loud CLI rejection of inert flags, capability-conditional tool listing, and the C8 nit bundle. The exact graph stays **44 publishable manifests**; no package added or retired. Intentional pre-1.0 breaks are documented in [migration](migration.md): inert CLI flags rejected (`CliOptions` dead fields removed) and `ExtensionKernel.load()` now resolves to `LoadedExtension[]`. The compat baseline was refreshed with `--allow-break` + migration note. Provider HTTP errors now carry numeric codes and `Retry-After` hints across anthropic/google/kimi/openai/opencode-go and the shared OpenAI-compatible transport — wire behavior is additive (more retries of genuinely transient failures), so the 0.0.15 protected live-canary matrix below still applies and no new live row is introduced.
202
+
203
+ ```bash
204
+ git diff --check
205
+ npm ci
206
+ npm run sdk:ready
207
+ node --test scripts/budget-gate.test.mjs
208
+ node scripts/scan-secrets.mjs && node scripts/verify-sbom.mjs
209
+ npm audit --audit-level=high
210
+ npm run release:gate
211
+ npm run release:check -- --version 0.0.17 --allow-dirty --allow-untagged --report /tmp/prism-0.0.17-preflight.json
212
+ npm run release:publish -- --version 0.0.17 --dry-run --allow-dirty --allow-untagged --report /tmp/prism-0.0.17-dry-run.json
213
+ git tag -s v0.0.17 -m "Prism 0.0.17"
214
+ git verify-tag v0.0.17
215
+ git push origin v0.0.17
216
+ ```
217
+
218
+ The dry-run checks every registry collision and executes npm's non-publishing tarball validation for each dependency-ordered manifest. The protected tag workflow alone publishes through `npm run release:publish -- --version "${GITHUB_REF_NAME#v}" --resume --report release-artifacts/publish-report.json`; re-run a failed job for the same tag. `npm audit signatures --json --include-attestations` and artifact checksums remain post-publish checks.
219
+
199
220
  ### 0.0.16 publish handoff
200
221
 
201
222
  **Decision: GO after protected operator prerequisites below.** Phase 11 (plan 079) is a simplification/readiness release: no runtime behavior changes and no package retired. The exact graph is **44 publishable manifests** — Phase 11 Task 3 added one internal implementation package, `@arnilo/prism-session-store-codecs` (shared SQLite/Postgres row codecs, not enrolled in any profile family). The only public-surface change is the additive `resolveRedactor` export from `@arnilo/prism`; provider `cleanJson` was deliberately left per-package (wire-shape variants). All six profiles (`prism-all`, `prism-base`, `prism-code`, `prism-compaction`, `prism-providers`, `prism-sdk`) are retained on adoption evidence (zero retirements). The root tarball dropped the historical `docs/review-coverage-*.md` (659,478 → ≈575,680 packed bytes, 281 → 270 files). New offline release gates (`npm run release:gate`: API-surface `.d.ts` diff, tarball deny-list, exact ranges) run inside `sdk:ready`, and performance budgets (`scripts/budgets.json`) are enforced by `scripts/budget-gate.test.mjs` + `scripts/benchmark-0.0.16.mjs`. No Studio, Office, remote-browser vendor, additional vector-store, Slack/Teams, voice/desktop-control, internal-auth, or queue package ships. Protected CI, signed tag, npm authentication, OIDC attestation, and protected live-canary evidence remain operator/workflow prerequisites; no package is published by this handoff.
@@ -209,8 +230,8 @@ node --test scripts/budget-gate.test.mjs
209
230
  node scripts/scan-secrets.mjs && node scripts/verify-sbom.mjs
210
231
  npm audit --audit-level=high
211
232
  npm run release:gate
212
- npm run release:check -- --version 0.0.16 --allow-dirty --allow-untagged --report /tmp/prism-0.0.16-preflight.json
213
- npm run release:publish -- --version 0.0.16 --dry-run --allow-dirty --allow-untagged --report /tmp/prism-0.0.16-dry-run.json
233
+ npm run release:check -- --version 0.0.17 --allow-dirty --allow-untagged --report /tmp/prism-0.0.16-preflight.json
234
+ npm run release:publish -- --version 0.0.17 --dry-run --allow-dirty --allow-untagged --report /tmp/prism-0.0.16-dry-run.json
214
235
  git tag -s v0.0.16 -m "Prism 0.0.16"
215
236
  git verify-tag v0.0.16
216
237
  git push origin v0.0.16
@@ -751,7 +772,7 @@ npm publication is not transactional and published versions are immutable. Parti
751
772
 
752
773
  ## Extension and configuration notes
753
774
 
754
- - **Required `@arnilo/prism` peer.** Every first-party code package declares a non-optional `@arnilo/prism@0.0.16` peer (`peerDependenciesMeta` must not mark `@arnilo/prism` optional; other peers such as `playwright-core` may be optional). The range stays pinned to `0.0.16` for the current 0.x release and will widen to `^1.0.0` at the 1.x stable release. 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.
775
+ - **Required `@arnilo/prism` peer.** Every first-party code package declares a non-optional `@arnilo/prism@0.0.17` peer (`peerDependenciesMeta` must not mark `@arnilo/prism` optional; other peers such as `playwright-core` may be optional). The range stays pinned to `0.0.17` for the current 0.x release and will widen to `^1.0.0` at the 1.x stable release. 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.
755
776
  - **Public access.** All 43 manifests (37 code packages + 6 family/profile packages) declare `"publishConfig": { "access": "public" }`; the publisher also passes `--access public` explicitly because scoped packages otherwise default to restricted on first publish.
756
777
  - **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).
757
778
  - **Release workflow.** `.github/workflows/release.yml` has six jobs. `verify` runs network-free SDK readiness on Node 24; `node20-compat` builds/imports every public root `exports` default target on Node 20 for declared `engines.node >=20` (docs examples need Node >=22.6 native TypeScript stripping); `postgres-integration` uses `pgvector/pgvector:pg16`; `supply-chain` runs high-severity audit, SPDX/license policy, and tracked-source secret scanning; and tag-only `codeql-release` runs SAST. Tag-only `publish` needs all five gates, preserves clean exact-tag/version/topological publication, and alone receives `NPM_TOKEN`, `id-token: write`, and `attestations: write`. Before npm publish it packs all current tarballs, generates checksums plus SPDX, scans unpacked public artifacts, creates GitHub attestations for tarballs and SBOM, then retains artifacts for 30 days. Registry state remains the resumable journal. Local `npm run release:dry-run` remains network-free SDK readiness; local PostgreSQL coverage is `PRISM_TEST_POSTGRES_URL=... npm run test:postgres`.
@@ -100,7 +100,7 @@ await store.append(entry, options);
100
100
  }
101
101
  ```
102
102
 
103
- Recognize it with `isSessionAppendConflict(error)`, not message text. Built-in stores reject duplicate entry ids, dangling `expectedParentId` values, and exact idempotency retries. They allow two distinct children of the same existing parent because that is a branch/fork, not parent-order corruption. Production stores may add a stricter branch-tip compare-and-swap when a host wants one-writer linear branches.
103
+ Recognize it with `isSessionAppendConflict(error)`, not message text. Built-in stores reject duplicate entry ids, dangling `expectedParentId` values, and `expectedParentId` pointing at another session's entry (the parent must exist in the same session — a cross-session parent would be a write no per-session branch walk could read back), and exact idempotency retries. They allow two distinct children of the same existing parent because that is a branch/fork, not parent-order corruption. Production stores may add a stricter branch-tip compare-and-swap when a host wants one-writer linear branches.
104
104
 
105
105
  ## Extension and configuration notes
106
106
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@arnilo/prism",
3
- "version": "0.0.16",
3
+ "version": "0.0.17",
4
4
  "description": "Agent harness for AI providers, agents, sessions, and tools.",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",