@arnilo/prism 0.9.0 → 0.10.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +24 -1
- package/README.md +13 -12
- package/dist/agent-approval.d.ts +7 -1
- package/dist/agent-approval.js +15 -6
- package/dist/agent-run-lifecycle.js +19 -5
- package/dist/agent-run-state.d.ts +26 -5
- package/dist/agent-run-state.js +97 -1
- package/dist/agent-session/event-subscriber.d.ts +2 -0
- package/dist/agent-session/event-subscriber.js +3 -0
- package/dist/agent-session/session/assemble.js +156 -9
- package/dist/agent-session/session/persist.js +11 -5
- package/dist/agent-session/session/provider-round.js +54 -13
- package/dist/agent-session/session/tool-round.d.ts +2 -2
- package/dist/agent-session/session/tool-round.js +58 -5
- package/dist/agent-session/session/types.d.ts +20 -2
- package/dist/agent-session/session.d.ts +65 -4
- package/dist/agent-session/session.js +156 -16
- package/dist/context-budget.d.ts +11 -0
- package/dist/context-budget.js +33 -2
- package/dist/contracts-core/agent.d.ts +26 -5
- package/dist/contracts-core/extensions.d.ts +3 -0
- package/dist/contracts-core/guardrail-packs.d.ts +8 -3
- package/dist/contracts-core/loop.d.ts +36 -0
- package/dist/contracts-core/provider.d.ts +6 -1
- package/dist/contracts-core/run-limits.d.ts +10 -1
- package/dist/contracts-protocol.d.ts +6 -4
- package/dist/contracts-run-state.d.ts +37 -3
- package/dist/contributions.d.ts +2 -1
- package/dist/contributions.js +1 -0
- package/dist/extensions.d.ts +15 -1
- package/dist/extensions.js +68 -0
- package/dist/guardrail-packs/types.d.ts +10 -0
- package/dist/guardrail-packs/validation-respect.js +16 -0
- package/dist/guardrails.d.ts +42 -1
- package/dist/guardrails.js +124 -15
- package/dist/index.d.ts +6 -6
- package/dist/index.js +4 -4
- package/dist/middleware.d.ts +1 -1
- package/dist/run-bundle.d.ts +6 -1
- package/dist/run-bundle.js +4 -1
- package/dist/run-limits.js +13 -0
- package/dist/testing/prefix-stability-conformance.d.ts +29 -0
- package/dist/testing/prefix-stability-conformance.js +91 -23
- package/dist/tools.js +10 -3
- package/docs/agent-events.md +12 -8
- package/docs/agent-session-runtime.md +9 -6
- package/docs/caveman.md +1 -1
- package/docs/compaction-llm.md +2 -0
- package/docs/compaction-observational-memory.md +21 -1
- package/docs/durable-runs.md +4 -3
- package/docs/embeddings.md +5 -1
- package/docs/execution-timeline.md +3 -2
- package/docs/extensions.md +20 -3
- package/docs/guardrails.md +16 -6
- package/docs/hooks.md +282 -0
- package/docs/index.md +18 -15
- package/docs/input-and-prompt-assembly.md +1 -1
- package/docs/instruction-injection.md +1 -0
- package/docs/live-testing.md +3 -1
- package/docs/memory-fabric.md +28 -0
- package/docs/middleware-hooks.md +54 -4
- package/docs/migration.md +13 -0
- package/docs/options-index.md +3 -1
- package/docs/policy-and-audit.md +14 -1
- package/docs/prefix-stability-conformance.md +57 -7
- package/docs/provider-packages.md +20 -20
- package/docs/public-contracts.md +1 -0
- package/docs/rag.md +93 -6
- package/docs/release-and-install.md +42 -39
- package/docs/runs-and-usage.md +17 -8
- package/docs/scoped-agent-memory.md +17 -9
- package/docs/scoped-memory.md +138 -0
- package/docs/tools.md +1 -1
- package/docs/wiki.md +4 -2
- package/package.json +4 -2
package/CHANGELOG.md
CHANGED
|
@@ -1,4 +1,27 @@
|
|
|
1
|
-
## [
|
|
1
|
+
## [0.10.0] - 2026-09-21 (hook lifecycle completion, scoped agent memory)
|
|
2
|
+
|
|
3
|
+
> **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**. Plans 097 (trajectory export) and 107 (behavior/graft removals) were not implemented for this line and are deferred rather than held unstarted against the cut. Registry/tag writes stay operator-authorized.
|
|
4
|
+
|
|
5
|
+
### Added
|
|
6
|
+
- **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).
|
|
7
|
+
- **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).
|
|
8
|
+
- **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).
|
|
9
|
+
- **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).
|
|
10
|
+
- **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).
|
|
11
|
+
- **`@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).
|
|
12
|
+
- **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.
|
|
13
|
+
- **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).
|
|
14
|
+
|
|
15
|
+
### Changed
|
|
16
|
+
- **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`).
|
|
17
|
+
- **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).
|
|
18
|
+
- **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.
|
|
19
|
+
- **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.
|
|
20
|
+
- **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.
|
|
21
|
+
|
|
22
|
+
### Security
|
|
23
|
+
- **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.
|
|
24
|
+
- **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
25
|
|
|
3
26
|
## [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
27
|
|
package/README.md
CHANGED
|
@@ -158,21 +158,22 @@ printf '{"id":"1","command":"prompt","params":{"input":"Hi"}}\n' \
|
|
|
158
158
|
## Packages
|
|
159
159
|
|
|
160
160
|
<!-- generated:package-truth:inventory begin -->
|
|
161
|
-
**
|
|
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.
|
|
166
|
-
| `@arnilo/prism-channels` | 0.
|
|
167
|
-
| `@arnilo/prism-coding-tools` | 0.
|
|
168
|
-
| `@arnilo/prism-core` | 0.
|
|
169
|
-
| `@arnilo/prism-providers` | 0.
|
|
170
|
-
| `@arnilo/prism-acp-agent` | 0.
|
|
171
|
-
| `@arnilo/prism-ag-ui` | 0.
|
|
172
|
-
| `@arnilo/prism-
|
|
173
|
-
| `@arnilo/prism-
|
|
174
|
-
| `@arnilo/prism-
|
|
175
|
-
| `@arnilo/prism-
|
|
165
|
+
| `@arnilo/prism` | 0.10.0 | core — runtime, CLI/RPC, templates, docs |
|
|
166
|
+
| `@arnilo/prism-channels` | 0.10.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.10.0 | family — /agent, /security, /openapi, /computer-use-linux, /dev, /caveman, /ponytail, /impeccable subpaths |
|
|
168
|
+
| `@arnilo/prism-core` | 0.10.0 | family — /runtime, /sessions, /governance, /credentials, /enterprise, /validation subpaths |
|
|
169
|
+
| `@arnilo/prism-providers` | 0.10.0 | family — all provider adapters as `/<adapter>` subpaths |
|
|
170
|
+
| `@arnilo/prism-acp-agent` | 0.10.0 | capability — ACP adapter |
|
|
171
|
+
| `@arnilo/prism-ag-ui` | 0.10.0 | capability — AG-UI/A2A/A2UI adapter |
|
|
172
|
+
| `@arnilo/prism-hooks` | 0.10.0 | capability — Claude/Codex-compatible hooks.json adapter compiled onto middleware, guardrail, injector, and stop-hook seams |
|
|
173
|
+
| `@arnilo/prism-mcp` | 0.10.0 | capability — MCP client/server/OAuth interop |
|
|
174
|
+
| `@arnilo/prism-memory` | 0.10.0 | capability — memory plus /rag, /compaction/*, /fabric, /graft, /wiki subpaths |
|
|
175
|
+
| `@arnilo/prism-web-tools` | 0.10.0 | capability — Brave/Exa/Firecrawl plus peer-gated /browser and /obscura subpaths |
|
|
176
|
+
| `@arnilo/prism-work` | 0.10.0 | capability — /connectors, /documents, /sheets, /diagrams, /document-reader, /sandbox, /skills, /tools subpaths |
|
|
176
177
|
<!-- generated:package-truth:inventory end -->
|
|
177
178
|
|
|
178
179
|
## Scripts
|
package/dist/agent-approval.d.ts
CHANGED
|
@@ -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
|
package/dist/agent-approval.js
CHANGED
|
@@ -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
|
|
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
|
-
|
|
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
|
|
@@ -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
|
|
103
|
-
*
|
|
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" ||
|
|
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) {
|
|
@@ -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
|
|
70
|
-
*
|
|
71
|
-
*
|
|
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 {};
|
package/dist/agent-run-state.js
CHANGED
|
@@ -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;
|