@arnilo/prism 0.9.0 → 0.11.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (103) hide show
  1. package/CHANGELOG.md +50 -1
  2. package/README.md +19 -16
  3. package/dist/agent-approval.d.ts +7 -1
  4. package/dist/agent-approval.js +15 -6
  5. package/dist/agent-run-lifecycle.d.ts +2 -1
  6. package/dist/agent-run-lifecycle.js +20 -6
  7. package/dist/agent-run-state.d.ts +26 -5
  8. package/dist/agent-run-state.js +97 -1
  9. package/dist/agent-session/event-subscriber.d.ts +2 -0
  10. package/dist/agent-session/event-subscriber.js +3 -0
  11. package/dist/agent-session/session/assemble.js +165 -16
  12. package/dist/agent-session/session/persist.js +11 -5
  13. package/dist/agent-session/session/provider-round.js +54 -13
  14. package/dist/agent-session/session/tool-round.d.ts +2 -2
  15. package/dist/agent-session/session/tool-round.js +86 -23
  16. package/dist/agent-session/session/types.d.ts +21 -2
  17. package/dist/agent-session/session.d.ts +66 -4
  18. package/dist/agent-session/session.js +159 -18
  19. package/dist/checkpoint-restore.d.ts +50 -14
  20. package/dist/checkpoint-restore.js +104 -28
  21. package/dist/context-budget.d.ts +11 -0
  22. package/dist/context-budget.js +33 -2
  23. package/dist/contracts-core/agent.d.ts +26 -5
  24. package/dist/contracts-core/extensions.d.ts +3 -0
  25. package/dist/contracts-core/guardrail-packs.d.ts +8 -3
  26. package/dist/contracts-core/loop.d.ts +36 -0
  27. package/dist/contracts-core/provider.d.ts +6 -1
  28. package/dist/contracts-core/run-limits.d.ts +10 -1
  29. package/dist/contracts-core/session.d.ts +2 -1
  30. package/dist/contracts-protocol.d.ts +6 -4
  31. package/dist/contracts-run-state.d.ts +48 -6
  32. package/dist/contributions.d.ts +2 -1
  33. package/dist/contributions.js +1 -0
  34. package/dist/extensions.d.ts +15 -1
  35. package/dist/extensions.js +68 -0
  36. package/dist/guardrail-packs/types.d.ts +10 -0
  37. package/dist/guardrail-packs/validation-respect.js +16 -0
  38. package/dist/guardrails.d.ts +42 -1
  39. package/dist/guardrails.js +124 -15
  40. package/dist/index.d.ts +7 -7
  41. package/dist/index.js +4 -4
  42. package/dist/leases.js +32 -6
  43. package/dist/middleware.d.ts +1 -1
  44. package/dist/node/contribution-discovery.d.ts +16 -1
  45. package/dist/node/contribution-discovery.js +47 -0
  46. package/dist/node/session-store-jsonl.js +67 -17
  47. package/dist/run-bundle.d.ts +6 -1
  48. package/dist/run-bundle.js +4 -1
  49. package/dist/run-limits.d.ts +11 -5
  50. package/dist/run-limits.js +13 -0
  51. package/dist/session-stores.js +61 -12
  52. package/dist/testing/prefix-stability-conformance.d.ts +73 -1
  53. package/dist/testing/prefix-stability-conformance.js +158 -27
  54. package/dist/tools.js +10 -3
  55. package/dist/usage-estimation.d.ts +7 -1
  56. package/dist/usage-estimation.js +16 -10
  57. package/docs/acp.md +2 -2
  58. package/docs/agent-events.md +15 -10
  59. package/docs/agent-session-runtime.md +10 -7
  60. package/docs/coding-agent-tools.md +1 -1
  61. package/docs/coding-tools.md +7 -11
  62. package/docs/compaction-llm.md +2 -0
  63. package/docs/compaction-observational-memory.md +21 -1
  64. package/docs/context-and-skills.md +6 -7
  65. package/docs/contribution-discovery.md +13 -0
  66. package/docs/durable-runs.md +14 -6
  67. package/docs/embeddings.md +7 -1
  68. package/docs/execution-timeline.md +9 -2
  69. package/docs/extensions.md +21 -5
  70. package/docs/guardrails.md +16 -6
  71. package/docs/hooks.md +282 -0
  72. package/docs/impeccable.md +1 -2
  73. package/docs/index.md +28 -21
  74. package/docs/input-and-prompt-assembly.md +1 -1
  75. package/docs/instruction-injection.md +1 -0
  76. package/docs/live-testing.md +3 -2
  77. package/docs/memory-fabric.md +29 -0
  78. package/docs/middleware-hooks.md +54 -4
  79. package/docs/migrate-to-0.11.md +65 -0
  80. package/docs/migration.md +24 -0
  81. package/docs/node-jsonl-session-store.md +4 -3
  82. package/docs/operations.md +1 -1
  83. package/docs/options-index.md +3 -1
  84. package/docs/peer-dependencies.md +3 -5
  85. package/docs/policy-and-audit.md +15 -2
  86. package/docs/prefix-stability-conformance.md +82 -9
  87. package/docs/provider-packages.md +20 -20
  88. package/docs/public-contracts.md +2 -1
  89. package/docs/rag.md +94 -7
  90. package/docs/release-and-install.md +62 -59
  91. package/docs/runs-and-usage.md +21 -10
  92. package/docs/scoped-agent-memory.md +17 -9
  93. package/docs/scoped-memory.md +138 -0
  94. package/docs/session-stores.md +2 -2
  95. package/docs/supervisors.md +14 -6
  96. package/docs/testing.md +17 -9
  97. package/docs/tools.md +1 -1
  98. package/docs/wiki.md +4 -2
  99. package/docs/workflows.md +2 -2
  100. package/package.json +8 -5
  101. package/docs/caveman.md +0 -130
  102. package/docs/graft.md +0 -149
  103. package/docs/ponytail.md +0 -129
package/CHANGELOG.md CHANGED
@@ -1,4 +1,53 @@
1
- ## [Unreleased]
1
+ ## [0.11.0] - 2026-09-24 (store bounds, lease fence reset, subpath removals)
2
+
3
+ > **Twelve publishable packages.** Lockstep minor. No new public exports from the review remediation. Two host-visible store contracts change, and three subpaths that the published 0.10.0 tarball still shipped are removed. Predecessor published line is **0.10.0**. Registry/tag writes stay operator-authorized.
4
+
5
+ ### Changed
6
+ - **Lockstep `0.10.0` → `0.11.0`.** All twelve publishable manifests move together with `^0.11.0` internal ranges. The lockfile, the `src/index.ts` version constant, the docs index banner, the release-workflow tag list, and the generated package-truth artifact agree (`scripts/version-literal-gate.test.mjs`).
7
+ - **Compat baseline regenerated.** The only declaration this cut changed is `version` (`"0.10.0"` → `"0.11.0"`). The same regen records the already-landed persona/graft removals (58 `@arnilo/prism-memory`, 48 `@arnilo/prism-coding-tools`) and the signature widenings already named in [docs/migration.md](docs/migration.md). No plan-120 public name was added.
8
+ - **Memory session store implements `readBranchPath`.** A snapshot walks the branch once and clones each kept entry once. JSONL and the durable adapters already had the method.
9
+ - **JSONL parse cache.** Reads after an in-process append reuse the parsed file when `(size, mtimeMs)` match. A same-size write inside one filesystem timestamp tick can still look unchanged; the upgrade path is hashing the file.
10
+ - **Idempotency window is 4,096 keys** on the memory store and the JSONL store. A replay of a key still inside the window is rejected. A replay of an evicted key appends a new entry. See [docs/migrate-to-0.11.md](docs/migrate-to-0.11.md).
11
+ - **In-memory lease sweep.** Expired rows are deleted once the map reaches 1,024. A released key that is still in the map inherits `fencingToken + 1`. A key that was swept starts its next fence at 1 (`docs/operations.md`). SQLite and Postgres adapters still keep the counter on the expired row.
12
+ - **Plain-text token estimates share one helper** (`ceil(length/4)`). Message and entry estimates are unchanged and are not billing numbers. Golden vectors live in `scripts/token-estimate-fixtures.json` (not shipped).
13
+ - **`npm test` gains two stages:** examples execution (spawns examples not already executed; `docker-process-session.ts` is `known-broken:placeholder-image`) and a Node branch-coverage audit (floor 83.49, `dist/**` only). Bun's coverage gate still records `branches: null`. Offline test budget pin is `< 110s`.
14
+ - **Freeze-test package presence** comes from `workspacePackageCounts()` in `scripts/package-truth.mjs`. Frozen count deltas stay in each suite.
15
+
16
+ ### Removed
17
+ - **Behavior persona subpaths (`@arnilo/prism-coding-tools/caveman`, `/ponytail`).** Both subpaths, their vendored upstream fixtures, and the `@dietrichgebert/ponytail` optional peer are gone. Load the upstream tree with `loadSkillDirectory` and register skills from a host extension. `@arnilo/prism-coding-tools/impeccable` is unchanged. See [docs/migrate-to-0.11.md](docs/migrate-to-0.11.md).
18
+ - **Graft bridge subpath (`@arnilo/prism-memory/graft`).** The subpath, its `@nanonets/graft` optional peer, the fixture CLI, and the graft commands are gone. Integrate graft as a host MCP server, or author tools against `registerTool` / `registerCommand`. `/rag`, `/compaction/*`, `/fabric`, `/wiki`, and `/scoped` stay.
19
+
20
+ ### Security
21
+ - No new credential surface. The lease sweep does not delete a live lease. Idempotency eviction cannot be used to skip the in-window duplicate check.
22
+
23
+ ## [0.10.0] - 2026-09-21 (hook lifecycle completion, scoped agent memory)
24
+
25
+ > **Twelve publishable packages.** All 105–106 features ship behind their documented options and defaults: `@arnilo/prism-hooks` is the new twelfth package, and scoped memory is an opt-in subpath that does nothing until a host constructs it. Predecessor published line is **0.9.0**. Plan 097 (trajectory export) was not implemented for this line and is deferred rather than held unstarted against the cut. Registry/tag writes stay operator-authorized.
26
+
27
+ ### Added
28
+ - **Scoped persistent memory (`@arnilo/prism-memory/scoped`).** New opt-in subpath: `createScopedMemoryPolicy({ memory, fabric, scopeRoot, policy })` is a policy and lifecycle layer over stores the host already configured (no fifth store). Create is inert and fails closed unless `scope.resourceId` equals the resolved `scopeRoot` and a `threadId` is set; the read path oversamples `fabric.recall`, applies an abstain floor (`activation.minSimilarity` 0.35) and activation budget (`topK` 3), ranks by `fabricScore × exp(−ageDays/tauDays) × (1 + ln(1 + uses))` (`tauDays` 30), and returns `{ hits: [], abstained: true }` rather than a weak hit. Writes go through a conservative post-run reviewer (`reviewSession`, strict JSON, garbage → zero writes), land as `candidate` records, and flip to `verified` after `promotion.reuseThreshold` (2) successful recalls — ledger status only, no fabric rewrite. `gcPass()` proposes archives onto `pending()` and never deletes (`legal_hold` skipped), `rememberFact` keeps a bounded working block (`maxChars` 2200, overflow throws `MemoryLimitError` with `consolidate first`), an injection scan plus an optional staged approval gate (`approval.default: "off"`) guard content, and a deterministic git audit mirror renders notes. State lives in one JSON ledger at `<scopeRoot>/.memory/state.json` (ids, counters, timestamps — never note content). Sizing: one reviewer call per run, one ledger read plus one write per recall, no model calls on the read path. See [docs/scoped-memory.md](docs/scoped-memory.md).
29
+ - **Scoped memory evaluation and health.** `runScopedMemoryEval` runs a win-rate A/B (with/without scoped memory) over a host-provided task set, `probePrecisionAt3` and `probeLocomoRecall` score the read path, `scopedMemoryHealth` reports counts plus conversion/activation/duplication rates, and `createScopedMemoryHealthCommand` exposes the same numbers as a command. See [docs/scoped-memory.md](docs/scoped-memory.md).
30
+ - **Stop hooks with bounded continuation.** `AgentConfig.stopHooks` / `RunOptions.stopHooks` run at the natural end of an agent loop with a `StopHookContext` (session and run ids, turn, redacted history, `stopHookActive`) and answer `{ action: "stop" }` or `{ action: "continue", reason }`; a continuation re-enters the loop with the reason queued as the next turn's steer — run-start input is never replayed — and `RunLimits.maxStopContinuations` (default 3, `0` disables, `null` uncaps) ends the run cleanly with the new `hook_limit` finish reason, which `checkpointPolicy: "every-turn"` leaves resumable. Extensions register hooks with `ExtensionAPI.registerStopHook()`. See [docs/hooks.md](docs/hooks.md).
31
+ - **Agent-event bridge and session lifecycle hooks.** `forwardAgentEvents(source, events, options?)` keeps the extension bus in step with a run (`agent_started` → `before_agent_start`, `turn_started` / `turn_finished` → `turn`, `tool_execution_started` → `tool_call`, `tool_execution_finished` → `tool_result`), delivering in event order with the original event as the read-only payload and never rethrowing into the run; core emits `session_start` once per session (durable resumes included) and `session_shutdown` on the new idempotent `AgentSession.close()`. See [docs/extensions.md](docs/extensions.md), [docs/middleware-hooks.md](docs/middleware-hooks.md).
32
+ - **Pre-compaction rewrite seam.** The `compaction_request` middleware hook runs after `compaction_started` and before the strategy, so a handler returns the `CompactionContext` the strategy actually compacts (entries and budget) and the following `compaction` hook plus `compaction_finished` event report that result. See [docs/middleware-hooks.md](docs/middleware-hooks.md).
33
+ - **`@arnilo/prism-hooks`, the hooks-file adapter.** New twelfth publishable package: `parseHooksConfig(text | object)` accepts the Claude flat and Codex `{ "hooks": { … } }` shapes, and `createHooksExtension(config, options)` compiles them onto public seams — `SessionStart` / `UserPromptSubmit` `additionalContext` through an instruction injector, `UserPromptSubmit` / `PreToolUse` / `PostToolUse` guardrails for denies, `tool_call` / `tool_result` middleware for `updatedInput` and post-tool context, and `Stop` onto stop hooks. Handlers are `command` (spawned shell-free, hash-pinned with `hookCommandHash()`) or `mcp_tool` (host-provided client, `${field.path}` templates), with seconds-based `timeout`, per-handler `additionalContextLimit` (default 2500 tokens, spilling to `<tempdir>/hook_outputs/`), and Codex's `continue: false` read as the stop signal. See [docs/hooks.md](docs/hooks.md).
34
+ - **Unified hook map.** [docs/hooks.md](docs/hooks.md) is the owning page for the hook model: the five families (transform, decide, inject, run-end control, observe), one row per Claude Code / Codex event mapped to its Prism surface or a documented non-goal with rationale (`PermissionRequest` stays host policy), the adapter reference, and migration steps from an existing `hooks.json` config.
35
+ - **Host skill-directory loader.** `loadSkillDirectory(directory, { maxSkillBytes? })` on `@arnilo/prism/node/contribution-discovery` loads a host-supplied `<dir>/<name>/SKILL.md` tree into inert `Skill[]`: one level, sorted, symlink-contained, per-file cap 262 144 bytes by default. It replaces the bounded reader the deleted persona subpaths carried. See [docs/contribution-discovery.md](docs/contribution-discovery.md).
36
+ - **Examples.** `examples/scoped-memory.ts` (scope guard → facts overflow → reviewer → abstain/hit recall → promotion → GC approval → mirror), `examples/hooks-json.ts` (a declarative `hooks.json` compiled onto guardrails, injectors, and stop hooks), and `examples/hooks-audit-command.mjs` (an audit-trail command handler).
37
+
38
+ ### Changed
39
+ - **Lockstep `0.9.0` → `0.10.0`.** All twelve publishable manifests move together with `^0.10.0` internal ranges; the lockfile, the `src/index.ts` version constant, the docs index banner, the release-workflow tag lists, and the generated package-truth artifact agree (enforced by `scripts/version-literal-gate.test.mjs`).
40
+ - **Compat baselines regenerated at 0.10.0**: `+47` public names with **zero removals**, zero renames, and zero signature breaks (`@arnilo/prism` +6, `@arnilo/prism-memory` +41, `@arnilo/prism-hooks` new at 34). The only consumer-visible type addition is `AgentSession.close()`; see [docs/migration.md](docs/migration.md).
41
+ - **Budgets rebaselined with recorded reasons**: export ceilings `@arnilo/prism` 1456, `@arnilo/prism-memory` 934, `@arnilo/prism-hooks` 34, plus the non-null assertion ratchet; the root artifact diet is unchanged this cut.
42
+ - **Coverage thresholds gained the new package's row** (`@arnilo/prism-hooks` 84.7 / 78.67 / 90), so the coverage summary fails closed on a new package instead of passing silently.
43
+ - **Migration notes for 0.9.0 hosts**: [docs/migration.md](docs/migration.md) — the `AgentSession.close()` addition, `session_start` / `session_shutdown` now being emitted, the new `hook_limit` finish reason, the `maxStopContinuations` default, and the opt-in scoped-memory subpath.
44
+
45
+ ### Removed
46
+ - None in the published 0.10.0 tarball. A post-publish amendment recorded the persona and graft subpath removals here; they ship in 0.11.0 (see the section above).
47
+
48
+ ### Security
49
+ - **Stop hooks and the hooks-file adapter fail closed.** `StopHook` entries are validated at run start (`name` plus `decide`), a malformed decision or a throwing handler is a non-blocking error that never silently allows a stop, `maxStopContinuations` (default 3) bounds continuation loops, and the adapter's `command` handlers spawn shell-free with tokenized argv and a `hookCommandHash()` trust pin. Codex's `continue: false` reads as the stop signal, not a continuation.
50
+ - **Scoped memory never widens access and never deletes.** Create fails closed on a scope mismatch, unknown policy fields are ignored and invalid values fail closed, recall abstains below the floor, GC only proposes, `legal_hold` is skipped, the ledger stores no note content, and the mirror is deterministic.
2
51
 
3
52
  ## [0.9.0] - 2026-09-20 (attention budget axes, turn traces, cache-stable disclosure, tool narrowing, guardrail packs, background agents, session search, deterministic turns, shared scopes)
4
53
 
package/README.md CHANGED
@@ -158,31 +158,34 @@ printf '{"id":"1","command":"prompt","params":{"input":"Hi"}}\n' \
158
158
  ## Packages
159
159
 
160
160
  <!-- generated:package-truth:inventory begin -->
161
- **11 publishable manifests** — root `@arnilo/prism` plus 10 workspace packages (4 `prism-*` family packages, 6 capability packages). Generated by `node scripts/package-truth.mjs --emit-docs` — do not hand-edit.
161
+ **12 publishable manifests** — root `@arnilo/prism` plus 11 workspace packages (4 `prism-*` family packages, 7 capability packages). Generated by `node scripts/package-truth.mjs --emit-docs` — do not hand-edit.
162
162
 
163
163
  | package | version | notes |
164
164
  | --- | --- | --- |
165
- | `@arnilo/prism` | 0.9.0 | core — runtime, CLI/RPC, templates, docs |
166
- | `@arnilo/prism-channels` | 0.9.0 | family — transport-neutral messaging runtime, durable journal, pairing and one-use approvals; official /telegram (private DMs, opt-in granted groups/topics) and experimental pinned signal-cli /signal |
167
- | `@arnilo/prism-coding-tools` | 0.9.0 | family — /agent, /security, /openapi, /computer-use-linux, /dev, /caveman, /ponytail, /impeccable subpaths |
168
- | `@arnilo/prism-core` | 0.9.0 | family — /runtime, /sessions, /governance, /credentials, /enterprise, /validation subpaths |
169
- | `@arnilo/prism-providers` | 0.9.0 | family — all provider adapters as `/<adapter>` subpaths |
170
- | `@arnilo/prism-acp-agent` | 0.9.0 | capability — ACP adapter |
171
- | `@arnilo/prism-ag-ui` | 0.9.0 | capability — AG-UI/A2A/A2UI adapter |
172
- | `@arnilo/prism-mcp` | 0.9.0 | capability — MCP client/server/OAuth interop |
173
- | `@arnilo/prism-memory` | 0.9.0 | capability — memory plus /rag, /compaction/*, /fabric, /graft, /wiki subpaths |
174
- | `@arnilo/prism-web-tools` | 0.9.0 | capability — Brave/Exa/Firecrawl plus peer-gated /browser and /obscura subpaths |
175
- | `@arnilo/prism-work` | 0.9.0 | capability — /connectors, /documents, /sheets, /diagrams, /document-reader, /sandbox, /skills, /tools subpaths |
165
+ | `@arnilo/prism` | 0.11.0 | core — runtime, CLI/RPC, templates, docs |
166
+ | `@arnilo/prism-channels` | 0.11.0 | family — transport-neutral messaging runtime, durable journal, pairing and one-use approvals; official /telegram (private DMs, opt-in granted groups/topics) and experimental pinned signal-cli /signal |
167
+ | `@arnilo/prism-coding-tools` | 0.11.0 | family — /agent, /security, /openapi, /computer-use-linux, /dev, /impeccable subpaths |
168
+ | `@arnilo/prism-core` | 0.11.0 | family — /runtime, /sessions, /governance, /credentials, /enterprise, /validation subpaths |
169
+ | `@arnilo/prism-providers` | 0.11.0 | family — all provider adapters as `/<adapter>` subpaths |
170
+ | `@arnilo/prism-acp-agent` | 0.11.0 | capability — ACP adapter |
171
+ | `@arnilo/prism-ag-ui` | 0.11.0 | capability — AG-UI/A2A/A2UI adapter |
172
+ | `@arnilo/prism-hooks` | 0.11.0 | capability — Claude/Codex-compatible hooks.json adapter compiled onto middleware, guardrail, injector, and stop-hook seams |
173
+ | `@arnilo/prism-mcp` | 0.11.0 | capability — MCP client/server/OAuth interop |
174
+ | `@arnilo/prism-memory` | 0.11.0 | capability — memory plus /rag, /compaction/*, /fabric, /wiki subpaths |
175
+ | `@arnilo/prism-web-tools` | 0.11.0 | capability — Brave/Exa/Firecrawl plus peer-gated /browser and /obscura subpaths |
176
+ | `@arnilo/prism-work` | 0.11.0 | capability — /connectors, /documents, /sheets, /diagrams, /document-reader, /sandbox, /skills, /tools subpaths |
176
177
  <!-- generated:package-truth:inventory end -->
177
178
 
178
179
  ## Scripts
179
180
 
180
181
  | command | action |
181
182
  |---------|--------|
182
- | `npm run build` | Compile TypeScript to `dist/` (core + workspaces) |
183
- | `npm run typecheck` | Type-check without emitting |
184
- | `npm test` | Build + run network-free tests |
185
- | `npm run test:live` | Run live suites whose credentials are present (skip the rest) |
183
+ | `bun ci` | Install dependencies from the committed `bun.lock` (frozen) |
184
+ | `bun run build` | Compile TypeScript to `dist/` (core + workspaces) |
185
+ | `bun run typecheck` | Type-check without emitting |
186
+ | `bun run test` | Build + run network-free tests (`npm test` still works) |
187
+ | `bun run test:coverage` | Coverage gate: `bun test --coverage` over the core suite and every gated workspace, with Bun-measured floors (`scripts/coverage-thresholds.json`) |
188
+ | `bun run test:live` | Run live suites whose credentials are present (skip the rest) |
186
189
  | `prism --help` | CLI help |
187
190
 
188
191
  ## Non-goals (v1)
@@ -1,6 +1,6 @@
1
1
  import type { CheckpointRestoreAudit } from "./checkpoint-restore.js";
2
2
  import type { StoredAgentRunState } from "./agent-run-state.js";
3
- import { Agent, AgentRunCheckpointMetadata, AgentRunResume, AgentRunState, AgentRunStateOptions, DecisionScope, NestedRunOutcome, PendingDecision, RunDecision, StickyDecision, ToolResult } from "./contracts.js";
3
+ import { Agent, AgentRunCheckpointMetadata, AgentRunResume, AgentRunState, AgentRunStateOptions, DecisionScope, Guardrails, NestedRunOutcome, PendingDecision, RunDecision, StickyDecision, ToolResult } from "./contracts.js";
4
4
  import type { AgentIdentity } from "./identity.js";
5
5
  /** Pending decisions of a suspended state, synthesizing the legacy single-approval shape. */
6
6
  export declare function pendingDecisionsOf(state: StoredAgentRunState): readonly PendingDecision[] | undefined;
@@ -32,6 +32,12 @@ export declare function resolveRunDecisions(input: {
32
32
  readonly state: StoredAgentRunState;
33
33
  readonly decisions: readonly RunDecision[];
34
34
  readonly signal?: AbortSignal;
35
+ /**
36
+ * Plan 104 T6: extra `tool_input` guardrails for decision-time revalidation of modified arguments —
37
+ * the resumed session's restored pack rules. Session-scoped on purpose: `agent.config.guardrails` is
38
+ * never mutated, so no other session of that agent inherits the packs.
39
+ */
40
+ readonly guardrails?: Guardrails;
35
41
  }): Promise<ResolvedRunDecisions>;
36
42
  /**
37
43
  * Resolve a tool's declared elicitation contract for a gated call. A throwing hook falls back to
@@ -2,7 +2,7 @@
2
2
  import { createHash } from "node:crypto";
3
3
  import { activeTools, validateElicitationPayload } from "./agent-tool-dispatch.js";
4
4
  import { AgentDecisionError, DEFAULT_MAX_STICKY_DECISIONS, HARD_MAX_PENDING_DECISIONS, MAX_DECISION_REASON_BYTES, MAX_ELICITATION_BYTES, } from "./contracts.js";
5
- import { runGuardrails } from "./guardrails.js";
5
+ import { guardrailRefusalText, runGuardrails } from "./guardrails.js";
6
6
  import { canonicalToolEffectJson } from "./tool-effects.js";
7
7
  /** Pending decisions of a suspended state, synthesizing the legacy single-approval shape. */
8
8
  export function pendingDecisionsOf(state) {
@@ -148,7 +148,7 @@ export async function resolveRunDecisions(input) {
148
148
  if (target.kind !== "tool_approval" || !target.toolCallId) {
149
149
  throw new AgentDecisionError("ERR_PRISM_DECISION_SCOPE", "Modified arguments apply only to tool approvals");
150
150
  }
151
- await validateModifiedArguments(agent, registry, state, target, decision.modifiedArguments, input.signal);
151
+ await validateModifiedArguments(agent, registry, state, target, decision.modifiedArguments, input.signal, input.guardrails);
152
152
  }
153
153
  if (decision.elicitation !== undefined) {
154
154
  if (target.kind !== "elicitation") {
@@ -181,7 +181,7 @@ export async function resolveRunDecisions(input) {
181
181
  };
182
182
  }
183
183
  /** Decision-time revalidation of modified arguments: schema, then input guardrails. Policy and trust re-run at dispatch. */
184
- async function validateModifiedArguments(agent, registry, state, target, modified, signal) {
184
+ async function validateModifiedArguments(agent, registry, state, target, modified, signal, extraGuardrails) {
185
185
  const invalid = (message, cause) => new AgentDecisionError("ERR_PRISM_DECISION_INVALID", message, { cause });
186
186
  if (JSON.stringify(modified) === undefined || Buffer.byteLength(JSON.stringify(modified), "utf8") > MAX_ELICITATION_BYTES) {
187
187
  throw invalid("Modified arguments must be a bounded JSON object");
@@ -199,9 +199,15 @@ async function validateModifiedArguments(agent, registry, state, target, modifie
199
199
  const value = call
200
200
  ? { ...call, arguments: modified }
201
201
  : { type: "tool_call", id: target.toolCallId ?? "", name: toolName, arguments: modified };
202
+ // Plan 104 T6: the session's pack rules join the agent's own here, so an approval that edits
203
+ // arguments into a pack-violating state is refused at decision time instead of being accepted and
204
+ // stopped at dispatch. Only `tool_input` is evaluated by this function, so only it is merged.
205
+ const guardrails = extraGuardrails?.toolInput?.length
206
+ ? { ...agent.config.guardrails, toolInput: [...(agent.config.guardrails?.toolInput ?? []), ...extraGuardrails.toolInput] }
207
+ : agent.config.guardrails;
202
208
  const guarded = await runGuardrails({
203
209
  stage: "tool_input",
204
- guardrails: agent.config.guardrails,
210
+ guardrails,
205
211
  value,
206
212
  context: {
207
213
  sessionId: state.sessionId,
@@ -213,8 +219,11 @@ async function validateModifiedArguments(agent, registry, state, target, modifie
213
219
  },
214
220
  redactor: agent.config.redactor,
215
221
  });
216
- if (guarded.terminal)
217
- throw invalid("Modified arguments blocked by guardrail");
222
+ if (guarded.terminal) {
223
+ // A compiled pack rule is named (bounded, redacted) so the host sees which rule refused the edit;
224
+ // the arguments themselves are never echoed.
225
+ throw invalid(guardrailRefusalText(guarded.terminal, "Modified arguments blocked") ?? "Modified arguments blocked by guardrail");
226
+ }
218
227
  }
219
228
  /**
220
229
  * Resolve a tool's declared elicitation contract for a gated call. A throwing hook falls back to
@@ -15,7 +15,8 @@ export interface AgentRunLifecycleOptions {
15
15
  /**
16
16
  * Plan 094 Task 3: external-state restore hooks, run on every claiming resume before the
17
17
  * checkpoint is claimed. Registered once here because a resume builds its session from the
18
- * stored state (there is no live session to register against beforehand).
18
+ * stored state (there is no live session to register against beforehand). Plan 109 Task 2: an
19
+ * entry may be `{ id?, restore, compensate? }` so a failed resume rolls the applied layers back.
19
20
  */
20
21
  readonly restoreHooks?: readonly AgentCheckpointRestoreHook[];
21
22
  /** Per-hook restore ceiling in ms; defaults to `DEFAULT_CHECKPOINT_RESTORE_TIMEOUT_MS`. */
@@ -99,12 +99,13 @@ export async function* resumeAgentRunStream(agent, ref, resume, options) {
99
99
  }
100
100
  /**
101
101
  * A `continue` resume needs a run whose frontier is intact: a crash-recovery checkpoint
102
- * (`status: "running"`) or a turn-policy stop, which writes a terminal state that still carries
103
- * the frontier (plan 084 Task 2). Every other terminal state is final — a naturally finished run
104
- * must never be resurrected.
102
+ * (`status: "running"`) or a clean run-end stop — a turn-policy stop or a stop-hook continuation
103
+ * cap — which writes a terminal state that still carries the frontier (plan 084 Task 2, plan 106 R1).
104
+ * Every other terminal state is final — a naturally finished run must never be resurrected.
105
105
  */
106
106
  function isContinuableState(state) {
107
- return state.status === "running" || (state.status === "succeeded" && state.stopReason === "host_policy");
107
+ return (state.status === "running" ||
108
+ (state.status === "succeeded" && (state.stopReason === "host_policy" || state.stopReason === "hook_limit")));
108
109
  }
109
110
  async function prepareAgentRunResume(agent, ref, resume, options, signal) {
110
111
  throwIfAbortedSignal(signal);
@@ -138,6 +139,12 @@ async function prepareAgentRunResume(agent, ref, resume, options, signal) {
138
139
  // Plan 078 Task 7: hand the reconstructed session to an observer (supervisor child-event pump)
139
140
  // before any event flows. Called for every resume outcome; a throw fails closed.
140
141
  options.onSession?.(session);
142
+ // Plan 104 T2: pack enforcement rides the checkpoint. A run without `persistSessionState` never
143
+ // writes the key, so its presence is the host's opt-in — restore before any turn (and before the
144
+ // pending-decision block, which may re-run input guardrails) or fail closed on a pack mismatch.
145
+ if (state.sessionState?.guardrailPacks) {
146
+ session.restoreGuardrailPacks(state.sessionState.guardrailPacks.packs, state.sessionState.guardrailPacks.state);
147
+ }
141
148
  // Opt-in session-state restore (plan 015 Task 4): names only; bodies re-resolve from
142
149
  // the live registry the next time the model (re)loads them via load_skill.
143
150
  if (options.persistSessionState && state.sessionState?.loadedSkillNames) {
@@ -175,16 +182,23 @@ async function prepareAgentRunResume(agent, ref, resume, options, signal) {
175
182
  session.restoreLoadedSkillBodies(state.sessionState.loadedSkillBodies);
176
183
  }
177
184
  const pendingDecisions = pendingDecisionsOf(state);
185
+ // Plan 104 T6: pack rules revalidate modified arguments at decision time. The set is the session's
186
+ // restored deny/tripwire rules plus its `ask` rules compiled as blocks — an approval that edits
187
+ // arguments into *any* pack-violating state is refused instead of becoming a run-wide allowance.
188
+ // Passed explicitly (never read from agent config), so a checkpoint that carried packs fails closed.
189
+ const sessionGuardrails = { toolInput: [...(session.packGuardrails?.toolInput ?? []), ...(session.packAskBlocks?.toolInput ?? [])] };
190
+ const decisionGuardrails = sessionGuardrails.toolInput.length > 0 ? sessionGuardrails : undefined;
178
191
  // Legacy approve maps to allow-once on every pending decision; legacy deny keeps its
179
192
  // terminal-denied behavior. Batch decisions are validated and applied atomically below.
180
193
  const resolved = resume.decisions !== undefined
181
- ? await resolveRunDecisions({ agent, state, decisions: resume.decisions, signal })
194
+ ? await resolveRunDecisions({ agent, state, decisions: resume.decisions, signal, guardrails: decisionGuardrails })
182
195
  : resume.decision === "approve" && pendingDecisions
183
196
  ? await resolveRunDecisions({
184
197
  agent,
185
198
  state,
186
199
  decisions: pendingDecisions.map((pending) => ({ approvalId: pending.approvalId, outcome: "allow_once" })),
187
200
  signal,
201
+ guardrails: decisionGuardrails,
188
202
  })
189
203
  : undefined;
190
204
  if (resolved && resolved.remaining.length > 0) {
@@ -303,7 +317,7 @@ async function prepareAgentRunResume(agent, ref, resume, options, signal) {
303
317
  status: state.status,
304
318
  ...(recordMetadata ? { metadata: recordMetadata } : {}),
305
319
  checkpoint: record,
306
- }, { timeoutMs: options.restoreHookTimeoutMs, signal })
320
+ }, { timeoutMs: options.restoreHookTimeoutMs, signal, redactor: agent.config.redactor })
307
321
  : undefined;
308
322
  throwIfAbortedSignal(signal);
309
323
  const claimed = await saveAgentRunState({
@@ -1,5 +1,5 @@
1
1
  import { type PersistedAttentionFoldLedger, type PersistedAttentionStickyFrontier } from "./attention-compiler.js";
2
- import type { Agent, AgentRunCheckpointMetadata, AgentRunCheckpointMetadataSource, AgentRunInterruption, AgentRunRef, AgentRunState, AgentRunStateOptions, CheckpointRecord, CheckpointStore, JsonValue, Message, ModelConfig, NestedRunRef, OwnershipScope, RunDecision, RunLimitCounters, StickyDecision, ToolCallContent } from "./contracts.js";
2
+ import type { Agent, AgentRunCheckpointMetadata, AgentRunCheckpointMetadataSource, AgentRunInterruption, AgentRunRef, AgentRunState, AgentRunStateOptions, CheckpointRecord, CheckpointStore, GuardrailRule, JsonValue, Message, ModelConfig, NestedRunRef, OwnershipScope, RunDecision, RunLimitCounters, StickyDecision, ToolCallContent } from "./contracts.js";
3
3
  import type { SecretRedactor } from "./redaction.js";
4
4
  import { type LoadedSkillBodiesEntry } from "./skill-load.js";
5
5
  export declare const AGENT_RUN_STATE_NAMESPACE = "prism.agent-run";
@@ -56,6 +56,9 @@ export interface StoredAgentRunState extends AgentRunState {
56
56
  * the same stub bytes instead of re-summarizing. Written and restored independently of
57
57
  * `persistSessionState`. Validated on load. */
58
58
  readonly attentionFold?: PersistedAttentionFoldLedger;
59
+ /** Plan 104 T2: compiled pack refs plus pack-owned state, so a resume re-enforces exactly what
60
+ * the suspended run enforced. Written only with `persistSessionState`; validated on load. */
61
+ readonly guardrailPacks?: PersistedGuardrailPacks;
59
62
  };
60
63
  /** Per-run allow-list (Task 21). Absent = full registered set (legacy checkpoints). */
61
64
  readonly toolNames?: readonly string[];
@@ -66,17 +69,34 @@ export interface StoredAgentRunState extends AgentRunState {
66
69
  */
67
70
  readonly checkpointPolicy?: "every-turn";
68
71
  /**
69
- * Set when a terminal state was written by a `RunOptions.turnPolicy` stop (plan 084 Task 2):
70
- * the run succeeded cleanly but its frontier is intact, so `decision: "continue"` may resume
71
- * it. Absent on every other state — a naturally finished run is never continuable.
72
+ * Set when a terminal state was written by a clean run-end stop that leaves the frontier intact:
73
+ * a `RunOptions.turnPolicy` stop (`host_policy`, plan 084 Task 2) or a stop-hook continuation cap
74
+ * (`hook_limit`, plan 106 R1). The run succeeded but `decision: "continue"` may resume it. Absent
75
+ * on every other state — a naturally finished run is never continuable.
72
76
  */
73
- readonly stopReason?: "host_policy";
77
+ readonly stopReason?: "host_policy" | "hook_limit";
74
78
  }
75
79
  /** Session-state caps (plan 015 Task 4): bounded names charged against the run-state byte budget. */
76
80
  export declare const MAX_PERSISTED_SKILL_NAMES = 64;
77
81
  export declare const MAX_PERSISTED_SKILL_NAME_CHARS = 256;
78
82
  /** Plan 041: activated-tool names ride the same budget discipline (cap 128; multiple searches accumulate). */
79
83
  export declare const MAX_PERSISTED_ACTIVATED_TOOL_NAMES = 128;
84
+ /** Plan 104 T2: one replayable pack row — the id, the version it was compiled at, and host options. */
85
+ interface PersistedGuardrailPackRef {
86
+ readonly id: string;
87
+ readonly version: number;
88
+ readonly options?: Readonly<Record<string, unknown>>;
89
+ /** Inline pattern rules; closures and `RegExp` patterns never reach a checkpoint (refused at save). */
90
+ readonly rules?: readonly GuardrailRule[];
91
+ }
92
+ /**
93
+ * Plan 104 T2: the checkpoint-side pack block written with `persistSessionState`. Rows replay a
94
+ * registered pack by `id`/`version` or an inline pack by its pattern `rules` (plan 104 T3).
95
+ */
96
+ export interface PersistedGuardrailPacks {
97
+ readonly packs: readonly PersistedGuardrailPackRef[];
98
+ readonly state?: Readonly<Record<string, Readonly<Record<string, unknown>>>>;
99
+ }
80
100
  /** Revision stamps of the built-in loops; custom strategies declare their own `revision`. */
81
101
  export declare const BUILT_IN_LOOP_REVISIONS: Readonly<Record<string, string>>;
82
102
  /** Validate a strategy snapshot as JSON-compatible and package it for the durable envelope. */
@@ -132,3 +152,4 @@ export declare function initialAgentRunState(input: {
132
152
  readonly interruptBeforeTool?: boolean;
133
153
  }): StoredAgentRunState;
134
154
  export declare function parseAgentRunState(value: unknown, version?: number): StoredAgentRunState;
155
+ export {};
@@ -16,6 +16,12 @@ export const MAX_PERSISTED_SKILL_NAMES = 64;
16
16
  export const MAX_PERSISTED_SKILL_NAME_CHARS = 256;
17
17
  /** Plan 041: activated-tool names ride the same budget discipline (cap 128; multiple searches accumulate). */
18
18
  export const MAX_PERSISTED_ACTIVATED_TOOL_NAMES = 128;
19
+ /** Plan 104 T2: persisted pack refs and state (cap matches `MAX_GUARDRAIL_PACKS`; ids match the pack cap). */
20
+ const MAX_PERSISTED_GUARDRAIL_PACKS = 8;
21
+ const MAX_PERSISTED_GUARDRAIL_PACK_ID_CHARS = 96;
22
+ const MAX_PERSISTED_GUARDRAIL_PACK_RULES = 64;
23
+ /** Per-pack options/state byte ceiling; the whole session state is still bounded by `maxStateBytes`. */
24
+ const MAX_PERSISTED_GUARDRAIL_PACK_BYTES = 8 * 1024;
19
25
  /** Revision stamps of the built-in loops; custom strategies declare their own `revision`. */
20
26
  export const BUILT_IN_LOOP_REVISIONS = {
21
27
  "single-shot": "1",
@@ -270,7 +276,7 @@ export function parseAgentRunState(value, version) {
270
276
  if (state.checkpointPolicy !== undefined && state.checkpointPolicy !== "every-turn") {
271
277
  throw new AgentRunStateError("Malformed agent run checkpoint policy");
272
278
  }
273
- if (state.stopReason !== undefined && state.stopReason !== "host_policy") {
279
+ if (state.stopReason !== undefined && state.stopReason !== "host_policy" && state.stopReason !== "hook_limit") {
274
280
  throw new AgentRunStateError("Malformed agent run stop reason");
275
281
  }
276
282
  // Load bounds against the hard cap, not the default: the configured maxStateBytes is a
@@ -353,5 +359,95 @@ function validateSessionState(sessionState) {
353
359
  if (fold !== undefined && restoreAttentionFoldLedger(fold) === undefined) {
354
360
  throw new AgentRunStateError("Malformed agent run attention fold ledger");
355
361
  }
362
+ validateGuardrailPackState(sessionState.guardrailPacks);
363
+ }
364
+ /**
365
+ * Plan 104 T2/T3: bounds for the persisted pack block. Rows replay a registered pack by id/version or
366
+ * an inline pack by its pattern rules, and state may only name those rows — anything else fails the
367
+ * load, because a dropped pack silently re-allows what it existed to deny. Rule data is re-validated
368
+ * (pattern compile, id/reason caps) by the compiler that replays it; this checks the JSON envelope.
369
+ */
370
+ function validateGuardrailPackState(value) {
371
+ if (value === undefined)
372
+ return;
373
+ const raw = value;
374
+ if (!raw || typeof raw !== "object" || !Array.isArray(raw.packs)) {
375
+ throw new AgentRunStateError("Malformed agent run guardrail pack state");
376
+ }
377
+ if (raw.packs.length > MAX_PERSISTED_GUARDRAIL_PACKS) {
378
+ throw new AgentRunStateError(`Persisted guardrail packs exceed ${MAX_PERSISTED_GUARDRAIL_PACKS} entries`);
379
+ }
380
+ const ids = new Set();
381
+ for (const row of raw.packs) {
382
+ if (!isPlainObject(row))
383
+ throw new AgentRunStateError("Malformed agent run guardrail pack row");
384
+ const { id, version, options } = row;
385
+ if (typeof id !== "string" || !id.trim() || id.length > MAX_PERSISTED_GUARDRAIL_PACK_ID_CHARS) {
386
+ throw new AgentRunStateError(`Persisted guardrail pack ids must be non-empty strings of at most ${MAX_PERSISTED_GUARDRAIL_PACK_ID_CHARS} chars`);
387
+ }
388
+ if (ids.has(id))
389
+ throw new AgentRunStateError(`Duplicate persisted guardrail pack id "${id}"`);
390
+ ids.add(id);
391
+ if (!Number.isSafeInteger(version) || version < 1) {
392
+ throw new AgentRunStateError(`Persisted guardrail pack "${id}" version must be a positive integer`);
393
+ }
394
+ if (options !== undefined && !isPlainObject(options)) {
395
+ throw new AgentRunStateError(`Persisted guardrail pack "${id}" options must be an object`);
396
+ }
397
+ if (options !== undefined)
398
+ boundPackBytes(options, `Pack "${id}" options`);
399
+ validatePersistedPackRules(id, row);
400
+ }
401
+ if (raw.state === undefined)
402
+ return;
403
+ if (!isPlainObject(raw.state))
404
+ throw new AgentRunStateError("Malformed agent run guardrail pack state");
405
+ for (const [id, state] of Object.entries(raw.state)) {
406
+ if (!ids.has(id))
407
+ throw new AgentRunStateError(`Persisted guardrail pack state names unknown pack "${id}"`);
408
+ if (!isPlainObject(state))
409
+ throw new AgentRunStateError(`Persisted guardrail pack "${id}" state must be an object`);
410
+ boundPackBytes(state, `Pack "${id}" state`);
411
+ }
412
+ }
413
+ function isPlainObject(value) {
414
+ return typeof value === "object" && value !== null && !Array.isArray(value);
415
+ }
416
+ /** Plan 104 T3: a persisted inline pack must be pattern data — a closure or `RegExp` cannot restore. */
417
+ function validatePersistedPackRules(id, row) {
418
+ if (row.rules === undefined)
419
+ return;
420
+ if (!Array.isArray(row.rules))
421
+ throw new AgentRunStateError(`Persisted guardrail pack "${id}" rules must be an array`);
422
+ if (row.rules.length === 0 || row.rules.length > MAX_PERSISTED_GUARDRAIL_PACK_RULES) {
423
+ throw new AgentRunStateError(`Persisted guardrail pack "${id}" rules must number 1..${MAX_PERSISTED_GUARDRAIL_PACK_RULES}`);
424
+ }
425
+ for (const rule of row.rules) {
426
+ if (!isPlainObject(rule))
427
+ throw new AgentRunStateError(`Persisted guardrail pack "${id}" rule must be an object`);
428
+ const { id: ruleId, pattern, deny } = rule;
429
+ if (typeof ruleId !== "string" || !ruleId.trim() || ruleId.length > MAX_PERSISTED_GUARDRAIL_PACK_ID_CHARS) {
430
+ throw new AgentRunStateError(`Persisted guardrail pack "${id}" rule ids must be non-empty strings`);
431
+ }
432
+ if (deny !== undefined)
433
+ throw new AgentRunStateError(`Persisted guardrail pack "${id}" rule "${ruleId}" carries a deny predicate`);
434
+ if (pattern !== undefined && typeof pattern !== "string") {
435
+ throw new AgentRunStateError(`Persisted guardrail pack "${id}" rule "${ruleId}" pattern must be a string`);
436
+ }
437
+ boundPackBytes(rule, `Pack "${id}" rule "${ruleId}"`);
438
+ }
439
+ }
440
+ /** A pack's state must be JSON and under its per-pack ceiling: refuse, never truncate. */
441
+ function boundPackBytes(value, label) {
442
+ let text;
443
+ try {
444
+ text = JSON.stringify(value);
445
+ }
446
+ catch {
447
+ throw new AgentRunStateError(`${label} must be JSON serializable`);
448
+ }
449
+ if ((text ? Buffer.byteLength(text) : 0) > MAX_PERSISTED_GUARDRAIL_PACK_BYTES) {
450
+ throw new AgentRunStateError(`${label} exceeds ${MAX_PERSISTED_GUARDRAIL_PACK_BYTES} bytes`);
451
+ }
356
452
  }
357
453
  //# sourceMappingURL=agent-run-state.js.map
@@ -3,6 +3,8 @@ import type { AgentEvent, SubscribeOptions } from "../contracts.js";
3
3
  export declare class EventSubscriber implements AsyncIterable<AgentEvent>, AsyncIterator<AgentEvent> {
4
4
  private readonly sessionId;
5
5
  private readonly onClose;
6
+ /** Plan 104 T5: run-scoped unless the host opted into `acrossRuns`. */
7
+ readonly acrossRuns: boolean;
6
8
  private readonly queue;
7
9
  private readonly waiters;
8
10
  private readonly maxQueuedEvents;
@@ -1,6 +1,8 @@
1
1
  export class EventSubscriber {
2
2
  sessionId;
3
3
  onClose;
4
+ /** Plan 104 T5: run-scoped unless the host opted into `acrossRuns`. */
5
+ acrossRuns;
4
6
  queue = [];
5
7
  waiters = [];
6
8
  maxQueuedEvents;
@@ -12,6 +14,7 @@ export class EventSubscriber {
12
14
  const maxQueuedEvents = options.maxQueuedEvents ?? 1024;
13
15
  this.maxQueuedEvents = Number.isFinite(maxQueuedEvents) ? Math.max(1, Math.floor(maxQueuedEvents)) : 1024;
14
16
  this.overflow = options.overflow ?? "close";
17
+ this.acrossRuns = options.acrossRuns === true;
15
18
  }
16
19
  [Symbol.asyncIterator]() {
17
20
  return this;