@arnilo/prism 0.8.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 +62 -1
- package/README.md +13 -12
- package/dist/agent-approval.d.ts +17 -2
- package/dist/agent-approval.js +15 -6
- package/dist/agent-event-source.d.ts +9 -1
- package/dist/agent-event-source.js +10 -3
- package/dist/agent-loops.js +7 -4
- package/dist/agent-run-lifecycle.d.ts +15 -1
- package/dist/agent-run-lifecycle.js +82 -11
- package/dist/agent-run-state.d.ts +47 -6
- package/dist/agent-run-state.js +154 -6
- package/dist/agent-session/event-subscriber.d.ts +2 -0
- package/dist/agent-session/event-subscriber.js +3 -0
- package/dist/agent-session/helpers.js +14 -0
- package/dist/agent-session/session/assemble.js +281 -32
- package/dist/agent-session/session/persist.d.ts +11 -0
- package/dist/agent-session/session/persist.js +48 -16
- package/dist/agent-session/session/provider-round.d.ts +14 -4
- package/dist/agent-session/session/provider-round.js +226 -19
- package/dist/agent-session/session/tool-round.d.ts +2 -2
- package/dist/agent-session/session/tool-round.js +78 -6
- package/dist/agent-session/session/types.d.ts +44 -3
- package/dist/agent-session/session.d.ts +100 -5
- package/dist/agent-session/session.js +224 -13
- package/dist/attention-compiler.d.ts +51 -2
- package/dist/attention-compiler.js +282 -21
- package/dist/cache-helpers.d.ts +4 -2
- package/dist/cache-helpers.js +8 -6
- package/dist/checkpoint-restore.d.ts +45 -0
- package/dist/checkpoint-restore.js +54 -0
- package/dist/context-budget.d.ts +13 -1
- package/dist/context-budget.js +57 -4
- package/dist/contracts-core/agent.d.ts +52 -1
- package/dist/contracts-core/attention.d.ts +95 -0
- package/dist/contracts-core/content.d.ts +10 -0
- package/dist/contracts-core/extensions.d.ts +3 -0
- package/dist/contracts-core/guardrail-packs.d.ts +46 -0
- package/dist/contracts-core/guardrail-packs.js +2 -0
- package/dist/contracts-core/loop.d.ts +36 -0
- package/dist/contracts-core/provider.d.ts +30 -0
- package/dist/contracts-core/run-limits.d.ts +29 -1
- package/dist/contracts-core/session.d.ts +23 -5
- package/dist/contracts-core/session.js +21 -2
- package/dist/contracts-core/usage.d.ts +40 -0
- package/dist/contracts-core/usage.js +8 -0
- package/dist/contracts-core.d.ts +2 -0
- package/dist/contracts-core.js +2 -0
- package/dist/contracts-protocol.d.ts +81 -5
- package/dist/contracts-run-state.d.ts +91 -2
- 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/coding-standard.d.ts +3 -0
- package/dist/guardrail-packs/coding-standard.js +63 -0
- package/dist/guardrail-packs/destructive-commands.d.ts +3 -0
- package/dist/guardrail-packs/destructive-commands.js +46 -0
- package/dist/guardrail-packs/errors.d.ts +7 -0
- package/dist/guardrail-packs/errors.js +9 -0
- package/dist/guardrail-packs/index.d.ts +4 -0
- package/dist/guardrail-packs/index.js +15 -0
- package/dist/guardrail-packs/secrets-hygiene.d.ts +3 -0
- package/dist/guardrail-packs/secrets-hygiene.js +23 -0
- package/dist/guardrail-packs/types.d.ts +26 -0
- package/dist/guardrail-packs/types.js +2 -0
- package/dist/guardrail-packs/validation-respect.d.ts +3 -0
- package/dist/guardrail-packs/validation-respect.js +69 -0
- package/dist/guardrails.d.ts +61 -1
- package/dist/guardrails.js +377 -0
- package/dist/index.d.ts +16 -11
- package/dist/index.js +10 -7
- package/dist/input.d.ts +8 -1
- package/dist/input.js +68 -6
- package/dist/middleware.d.ts +37 -2
- package/dist/middleware.js +41 -0
- package/dist/node/session-store-jsonl.js +18 -3
- package/dist/observability.js +6 -0
- package/dist/provider-events.d.ts +8 -2
- package/dist/provider-events.js +60 -2
- package/dist/providers/openai-compatible.js +6 -3
- package/dist/run-bundle.d.ts +6 -1
- package/dist/run-bundle.js +5 -1
- package/dist/run-limits.d.ts +11 -1
- package/dist/run-limits.js +59 -0
- package/dist/session-stores.d.ts +12 -1
- package/dist/session-stores.js +21 -4
- package/dist/testing/agent-event-source-conformance.js +41 -2
- package/dist/testing/prefix-stability-conformance.d.ts +59 -0
- package/dist/testing/prefix-stability-conformance.js +172 -0
- package/dist/testing/session-store-conformance.d.ts +3 -2
- package/dist/testing/session-store-conformance.js +48 -0
- package/dist/tools.d.ts +5 -0
- package/dist/tools.js +21 -6
- package/dist/usage-estimation.d.ts +29 -0
- package/dist/usage-estimation.js +79 -0
- package/docs/agent-events.md +75 -4
- package/docs/agent-session-runtime.md +10 -6
- package/docs/attention-compiler.md +89 -8
- package/docs/caveman.md +1 -1
- package/docs/coding-agent-tools.md +1 -1
- package/docs/compaction-and-retry.md +1 -1
- package/docs/compaction-llm.md +2 -0
- package/docs/compaction-observational-memory.md +54 -7
- package/docs/durable-runs.md +46 -3
- package/docs/embeddings.md +9 -0
- package/docs/evaluations.md +5 -0
- package/docs/execution-timeline.md +79 -1
- package/docs/extensions.md +20 -3
- package/docs/guardrails.md +50 -4
- package/docs/hooks.md +282 -0
- package/docs/index.md +37 -15
- package/docs/input-and-prompt-assembly.md +4 -4
- package/docs/instruction-injection.md +1 -0
- package/docs/knowledge-sync.md +4 -0
- package/docs/live-testing.md +3 -1
- package/docs/memory-fabric.md +28 -0
- package/docs/middleware-hooks.md +90 -4
- package/docs/migrate-to-0.9.md +210 -0
- package/docs/migration.md +26 -0
- package/docs/multi-agent-patterns.md +25 -2
- package/docs/node-jsonl-session-store.md +7 -1
- package/docs/observability.md +7 -3
- package/docs/options-index.md +4 -1
- package/docs/policy-and-audit.md +26 -1
- package/docs/prefix-stability-conformance.md +143 -0
- package/docs/provider-caching.md +4 -4
- package/docs/provider-conformance.md +16 -0
- package/docs/provider-packages.md +20 -20
- package/docs/public-contracts.md +3 -2
- package/docs/rag.md +188 -3
- package/docs/release-and-install.md +45 -40
- package/docs/runs-and-usage.md +56 -10
- package/docs/scoped-agent-memory.md +270 -0
- package/docs/scoped-memory.md +138 -0
- package/docs/session-store-conformance.md +1 -2
- package/docs/session-stores.md +17 -17
- package/docs/supervisors.md +32 -12
- package/docs/tools.md +18 -1
- package/docs/wiki.md +4 -2
- package/docs/workflows.md +5 -0
- package/package.json +8 -2
package/CHANGELOG.md
CHANGED
|
@@ -1,4 +1,65 @@
|
|
|
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.
|
|
25
|
+
|
|
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)
|
|
27
|
+
|
|
28
|
+
> **Eleven publishable packages.** All 086–098 features ship behind their documented options, joined by the live-stream terminal fix (plan 100). Predecessor published line is **0.8.0**. Registry/tag writes stay operator-authorized.
|
|
29
|
+
|
|
30
|
+
### Added
|
|
31
|
+
- **Attention budget axes and durable folding.** `attentionCompiler.trigger` accepts `input_ratio` (the legacy axis), `run_input_ratio` (fires against `RunLimits.maxInputTokens` instead of the model window), `token_floor`, and a host predicate, alone or as an any-of array; `attentionCompiler.durable: true` writes the fold ledger and sticky frontier into the checkpoint so a resumed run keeps its stubs. See [docs/attention-compiler.md](docs/attention-compiler.md).
|
|
32
|
+
- **Turn traces and exhaustion attribution.** `provider_turn_finished` carries a closed `stopReason` taxonomy, a `budgets` snapshot, the effective tool menu (`tools.count` / `tools.idsHash`), and provider-reported cache counts with a per-turn `hitRate`; `agent_finished` carries `finishReason` / `stopDetail`, `AgentRunResult.stopReason` names host-policy and loop-ceiling stops, and the execution timeline adds `turns[i].stopReason` plus `timeline.exhaustion`. See [docs/agent-events.md](docs/agent-events.md), [docs/execution-timeline.md](docs/execution-timeline.md).
|
|
33
|
+
- **Cache-stable progressive disclosure and cache metrics.** Skill bodies, deferred tool schemas, and loaded references land at cache-stable positions (append-only tail or explicit invalidation); cache read/write tokens and per-turn hit rate are recorded on usage records and turn events. See [docs/provider-caching.md](docs/provider-caching.md), [docs/prefix-stability-conformance.md](docs/prefix-stability-conformance.md).
|
|
34
|
+
- **Per-turn tool narrowing.** `AgentConfig.toolNarrowing` / `RunOptions.toolNarrowing` returns the effective tool subset before each provider turn; out-of-grant names are dropped and reported on `tool_narrowing_clamped`. See [docs/tools.md](docs/tools.md).
|
|
35
|
+
- **Usage estimation and the context meter.** `estimateMessageTokens` / `MODEL_FAMILY_TOKENS` / `resolveModelFamily` produce labeled `TokenEstimate`s, `AgentConfig.usageEstimation: "fallback"` (default) charges one labeled estimate for usage-less providers, and `session.contextMeter()` exposes `{ inputTokens, source, inputCap?, runInputBudget?, usedRatio? }`. See [docs/runs-and-usage.md](docs/runs-and-usage.md).
|
|
36
|
+
- **Execution guardrail packs.** `guardrailPacks` compiles four built-in restrictive rule sets (`coding-standard`, `destructive-commands`, `validation-respect`, `secrets-hygiene`) onto existing tool stages, each with a trajectory scorer (`createGuardrailPackScorer`). See [docs/guardrails.md](docs/guardrails.md).
|
|
37
|
+
- **Background child agents and child-event passthrough.** `lifetime: "session"`, `report: "on-complete" | "milestones" | "stream"`, `milestone`, and `budgetShare` on `delegate` / `delegateAsync` / `spawn_agent`, with `child_milestone`, `child_failed`, and rate-coalesced child events. See [docs/supervisors.md](docs/supervisors.md), [docs/multi-agent-patterns.md](docs/multi-agent-patterns.md).
|
|
38
|
+
- **Checkpoint sidecar metadata and restore hooks.** A redacted ≤4 KB `checkpointMetadata` map rides every checkpoint record without charging `maxStateBytes`, and `restoreHooks` revert external layers before a resume claims the run. See [docs/durable-runs.md](docs/durable-runs.md).
|
|
39
|
+
- **Bounded workspace session search.** `SessionStore.searchSessions?(query)` over workspace/time/provider/label/kind/ownership filters with an indexed full-text path (SQLite FTS5, Postgres `tsvector`, migration 004) and a shared linear matcher for memory and JSONL stores. See [docs/session-stores.md](docs/session-stores.md).
|
|
40
|
+
- **Deterministic no-model turns.** The `beforeProviderTurn` middleware hook answers a turn from host data (`BeforeProviderTurnPayload` → `DeterministicTurnAnswer`) with no provider request, recorded as `deterministic` on the timeline and graded by `createDeterministicTurnScorer`. See [docs/middleware-hooks.md](docs/middleware-hooks.md).
|
|
41
|
+
- **Shared work scopes for observational memory.** `sharedScopes` at attach time plus owner-branch grants (`grant` / `revoke`) let granted sessions contribute to and read one scope, with deny-by-default rechecks, `onScopeAccess` auditing, and revocation at the next read. See [docs/compaction-observational-memory.md](docs/compaction-observational-memory.md).
|
|
42
|
+
- **Retrieval revocation and a zero-service default reranker.** `createDeletionPropagator` (+ `collectInvalidationIds` / `listInvalidatedIds`, `repointSource`, `retireWikiSources`, RAG/wiki handlers) propagates deletion and revocation through derived artifacts, `createAccessRecheck` rechecks governed sources per query, and `resolveReranker({ kind: "local" })` / `createLocalReranker()` run an in-process cross-encoder behind the `LocalRerankRuntime` seam with no declared inference dependency. See [docs/rag.md](docs/rag.md), [docs/embeddings.md](docs/embeddings.md).
|
|
43
|
+
- **Examples.** `examples/attention-budget-axes.ts`, `examples/tool-narrowing-planes.ts`, `examples/session-search.ts`.
|
|
44
|
+
|
|
45
|
+
### Changed
|
|
46
|
+
- **Lockstep `0.8.0` → `0.9.0`.** All eleven publishable manifests move together with `^0.9.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`).
|
|
47
|
+
- **A limit death no longer ends a stream early.** The terminal set is exactly `agent_finished` / `agent_denied` / `error`, decided by one exported `isTerminalAgentEventType` predicate shared by the memory, NATS, and Postgres sources, AG-UI replay, the A2A stream break, AG-UI `filterRun`, and conversation replay; `run_limit_exceeded` → `budget_exhausted` → `error` now reaches page, subscribe, and replay consumers in order. See [docs/migrate-to-0.9.md](docs/migrate-to-0.9.md).
|
|
48
|
+
- **Usage-less providers are charged a labeled estimate by default.** `AgentConfig.usageEstimation: "fallback"` records one `TokenEstimate` (`estimated: true` + `confidence`) at the existing usage seam; reported usage always wins, and `"off"` keeps the previous zero-for-no-usage behavior.
|
|
49
|
+
- **Compat baselines regenerated at 0.9.0**: `+119` public names with **zero removals** and zero renames (`@arnilo/prism` +49, `@arnilo/prism-memory` +60, `@arnilo/prism-core` +10). One type-level consumer change: the `recordUsage` callback of `generateProviderTurn` / `generateWithRetry` returns `Promise<Usage | undefined>` instead of `Promise<void>`.
|
|
50
|
+
- **Budgets rebaselined with recorded reasons**: root packed/unpacked/file count, per-package export ceilings, and the non-null assertion ratchet carry the measured 0.9.0 values and the plans that moved them.
|
|
51
|
+
- **Migration guide for 0.8.0 hosts**: [docs/migrate-to-0.9.md](docs/migrate-to-0.9.md) — four behavior changes inside existing surfaces, every new option with defaults and sizing lines, upgrade steps, and rollback — indexed from [docs/migration.md](docs/migration.md) and [docs/index.md](docs/index.md).
|
|
52
|
+
|
|
53
|
+
### Fixed
|
|
54
|
+
- **Streams ended one record early on a run-limit death.** In-memory, NATS, Postgres, and AG-UI replay consumers treated `run_limit_exceeded` as terminal, so a subscriber that stopped at the first breach record never saw the `budget_exhausted` attribution or the terminal `error` (`assertAgentEventSourceConforms` now asserts the delivery order against every source implementation).
|
|
55
|
+
- **Attention folding was inert when the run input cap sat below the model window.** The new `run_input_ratio` axis evaluates cumulative run spend plus the assembled request, so a 500k run cap under a 1M-window model folds instead of dying at the cap.
|
|
56
|
+
- **Unindexed session search could scan an unbounded store.** Memory and JSONL stores share `searchLinearSessions` with contract linear caps, overridable only within their hard bounds.
|
|
57
|
+
|
|
58
|
+
### Security
|
|
59
|
+
- **Guardrail packs fail closed on secrets and destructive commands.** `secrets-hygiene` scans tool arguments for credential shapes, `destructive-commands` matches shell commands, and `coding-standard` keeps edits inside `options.roots`; every pack is restrictive-only, so a pack can never widen a host policy. Cap: 8 packs, 64 rules per pack.
|
|
60
|
+
- **Shared-scope access is deny-by-default and audited.** Absent, unknown, revoked, unreachable, or not-opened-locally scope state denies the read, every decision reaches `onScopeAccess`, revocation lands on the next read, and the host `entries(sessionId)` callback stays inside one `OwnershipScope`.
|
|
61
|
+
- **Checkpoint metadata and restore hooks are redacted and bounded.** The sidecar map is redacted unconditionally (no public-key exemption) and capped at 4 KiB; a failing or timed-out restore hook aborts the restore rather than claiming the run.
|
|
62
|
+
- **Estimates can never masquerade as provider truth.** Every estimated token count carries `estimated: true` (and `confidence`), reported usage is never overwritten, and estimates never produce a price — a configured `maxCost` stays the fail-closed envelope.
|
|
2
63
|
|
|
3
64
|
## [0.8.0] - 2026-09-18 (messaging channels, connected apps, work family, durable runs, honesty gates)
|
|
4
65
|
|
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,5 +1,6 @@
|
|
|
1
|
+
import type { CheckpointRestoreAudit } from "./checkpoint-restore.js";
|
|
1
2
|
import type { StoredAgentRunState } from "./agent-run-state.js";
|
|
2
|
-
import { Agent, 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";
|
|
3
4
|
import type { AgentIdentity } from "./identity.js";
|
|
4
5
|
/** Pending decisions of a suspended state, synthesizing the legacy single-approval shape. */
|
|
5
6
|
export declare function pendingDecisionsOf(state: StoredAgentRunState): readonly PendingDecision[] | undefined;
|
|
@@ -31,6 +32,12 @@ export declare function resolveRunDecisions(input: {
|
|
|
31
32
|
readonly state: StoredAgentRunState;
|
|
32
33
|
readonly decisions: readonly RunDecision[];
|
|
33
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;
|
|
34
41
|
}): Promise<ResolvedRunDecisions>;
|
|
35
42
|
/**
|
|
36
43
|
* Resolve a tool's declared elicitation contract for a gated call. A throwing hook falls back to
|
|
@@ -50,7 +57,15 @@ export declare function decisionScopesEqual(a: DecisionScope, b: DecisionScope):
|
|
|
50
57
|
export declare function nestedOutcomeToolResult(outcome: Exclude<NestedRunOutcome, {
|
|
51
58
|
status: "suspended";
|
|
52
59
|
}>, toolCallId: string, name: string): ToolResult;
|
|
53
|
-
|
|
60
|
+
/**
|
|
61
|
+
* Non-state extras carried into a resumed run: the sidecar metadata seed (so later checkpoint
|
|
62
|
+
* writes preserve the record's map) and the restore audit emitted on `agent_resumed`.
|
|
63
|
+
*/
|
|
64
|
+
export interface ActiveDurableRunExtras {
|
|
65
|
+
readonly checkpointMetadata?: AgentRunCheckpointMetadata;
|
|
66
|
+
readonly restore?: CheckpointRestoreAudit;
|
|
67
|
+
}
|
|
68
|
+
export interface ActiveDurableRun extends ActiveDurableRunExtras {
|
|
54
69
|
readonly options: AgentRunStateOptions;
|
|
55
70
|
state?: StoredAgentRunState;
|
|
56
71
|
version: number;
|
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
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { AgentEventSource, AgentEventSourceOptions } from "./contracts.js";
|
|
1
|
+
import type { AgentEvent, AgentEventSource, AgentEventSourceOptions } from "./contracts.js";
|
|
2
2
|
export type AgentEventSourceErrorCode = "ERR_PRISM_AGENT_EVENT_SOURCE_INPUT" | "ERR_PRISM_AGENT_EVENT_SOURCE_CURSOR" | "ERR_PRISM_AGENT_EVENT_SOURCE_RETENTION" | "ERR_PRISM_AGENT_EVENT_SOURCE_OVERFLOW" | "ERR_PRISM_AGENT_EVENT_SOURCE_CLOSED";
|
|
3
3
|
export declare class AgentEventSourceError extends Error {
|
|
4
4
|
readonly code: AgentEventSourceErrorCode;
|
|
@@ -9,3 +9,11 @@ export declare class AgentEventSourceError extends Error {
|
|
|
9
9
|
* wakeup or persistence; use a database-backed source for distributed delivery.
|
|
10
10
|
*/
|
|
11
11
|
export declare function createMemoryAgentEventSource(options?: AgentEventSourceOptions): AgentEventSource;
|
|
12
|
+
/**
|
|
13
|
+
* Terminal-set membership for agent event streams: a run's stream ends on its outcome record.
|
|
14
|
+
*
|
|
15
|
+
* Attribution records — `run_limit_exceeded` and `budget_exhausted` — are delivered *before* that
|
|
16
|
+
* outcome (the `error` that follows a run-limit death), so a page, subscription, or replay must
|
|
17
|
+
* keep reading past them. Every stream-ending site routes through this one predicate.
|
|
18
|
+
*/
|
|
19
|
+
export declare function isTerminalAgentEventType(type: AgentEvent["type"]): boolean;
|
|
@@ -439,12 +439,19 @@ function positionAfter(records, cursor, stream) {
|
|
|
439
439
|
}
|
|
440
440
|
function terminalAt(records, after) {
|
|
441
441
|
const last = records.at(-1);
|
|
442
|
-
if (!last || !
|
|
442
|
+
if (!last || !isTerminalAgentEventType(last.type))
|
|
443
443
|
return false;
|
|
444
444
|
return after === undefined || after.sequence <= last.sequence;
|
|
445
445
|
}
|
|
446
|
-
|
|
447
|
-
|
|
446
|
+
/**
|
|
447
|
+
* Terminal-set membership for agent event streams: a run's stream ends on its outcome record.
|
|
448
|
+
*
|
|
449
|
+
* Attribution records — `run_limit_exceeded` and `budget_exhausted` — are delivered *before* that
|
|
450
|
+
* outcome (the `error` that follows a run-limit death), so a page, subscription, or replay must
|
|
451
|
+
* keep reading past them. Every stream-ending site routes through this one predicate.
|
|
452
|
+
*/
|
|
453
|
+
export function isTerminalAgentEventType(type) {
|
|
454
|
+
return type === "agent_finished" || type === "agent_denied" || type === "error";
|
|
448
455
|
}
|
|
449
456
|
function sameRecord(existing, input) {
|
|
450
457
|
const { sequence: _existingSequence, ...existingWithoutSequence } = existing;
|
package/dist/agent-loops.js
CHANGED
|
@@ -25,12 +25,14 @@ export const singleShotLoop = {
|
|
|
25
25
|
ctx.emit({ type: "turn_started", sessionId: ctx.sessionId, runId: ctx.runId, turn });
|
|
26
26
|
const request = await ctx.assemble(nextInput, undefined, turn);
|
|
27
27
|
throwIfAborted(ctx.signal);
|
|
28
|
-
const { content, calls, messageId, started, usage: turnUsage } = await ctx.generate(request);
|
|
28
|
+
const { content, calls, messageId, started, usage: turnUsage, metadata: turnMetadata } = await ctx.generate(request);
|
|
29
29
|
usage = turnUsage ?? usage;
|
|
30
30
|
if (turn === 1)
|
|
31
31
|
ctx.history.push(...ctx.inputMessages);
|
|
32
32
|
if (started) {
|
|
33
|
-
|
|
33
|
+
// Turn provenance (plan 096): a deterministic answer carries `{ deterministic: { middleware } }`
|
|
34
|
+
// so the persisted transcript proves the turn had no model behind it.
|
|
35
|
+
const message = { id: messageId, role: "assistant", content, ...(turnMetadata ? { metadata: turnMetadata } : {}) };
|
|
34
36
|
ctx.history.push(message);
|
|
35
37
|
await ctx.appendMessage(message);
|
|
36
38
|
ctx.emit({ type: "message_finished", sessionId: ctx.sessionId, runId: ctx.runId, message });
|
|
@@ -140,7 +142,7 @@ export function generateValidateReviseLoop(opts) {
|
|
|
140
142
|
}
|
|
141
143
|
}
|
|
142
144
|
throwIfAborted(ctx.signal);
|
|
143
|
-
const { content, calls, messageId, started, usage: turnUsage } = await ctx.generate(request);
|
|
145
|
+
const { content, calls, messageId, started, usage: turnUsage, metadata: turnMetadata } = await ctx.generate(request);
|
|
144
146
|
usage = turnUsage ?? usage;
|
|
145
147
|
if (pendingHistory.length > 0) {
|
|
146
148
|
ctx.history.push(...pendingHistory);
|
|
@@ -149,7 +151,8 @@ export function generateValidateReviseLoop(opts) {
|
|
|
149
151
|
if (turn === 1)
|
|
150
152
|
ctx.history.push(...ctx.inputMessages);
|
|
151
153
|
if (started) {
|
|
152
|
-
|
|
154
|
+
// Same provenance contract as singleShotLoop (plan 096).
|
|
155
|
+
const message = { id: messageId, role: "assistant", content, ...(turnMetadata ? { metadata: turnMetadata } : {}) };
|
|
153
156
|
ctx.history.push(message);
|
|
154
157
|
await ctx.appendMessage(message);
|
|
155
158
|
ctx.emit({ type: "message_finished", sessionId: ctx.sessionId, runId: ctx.runId, message });
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { Agent, AgentEvent, AgentRunRef, AgentRunResult, AgentRunResume, AgentRunResumeOptions, AgentRunResumeStreamOptions, AgentRunStatusResult, CheckpointStore, OwnershipScope, SubscribeOptions } from "./contracts.js";
|
|
1
|
+
import type { Agent, AgentCheckpointRestoreHook, AgentEvent, AgentRunCheckpointMetadataSource, AgentRunRef, AgentRunResult, AgentRunResume, AgentRunResumeOptions, AgentRunResumeStreamOptions, AgentRunStatusResult, CheckpointStore, OwnershipScope, SubscribeOptions } from "./contracts.js";
|
|
2
2
|
export interface AgentRunLifecycleAgent {
|
|
3
3
|
readonly agent: Agent;
|
|
4
4
|
/** Current host-authored revision; it must match the stored revision. */
|
|
@@ -12,6 +12,14 @@ export interface AgentRunLifecycleOptions {
|
|
|
12
12
|
readonly signal?: AbortSignal;
|
|
13
13
|
}) => AgentRunLifecycleAgent | Promise<AgentRunLifecycleAgent>;
|
|
14
14
|
readonly fencingToken?: number;
|
|
15
|
+
/**
|
|
16
|
+
* Plan 094 Task 3: external-state restore hooks, run on every claiming resume before the
|
|
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).
|
|
19
|
+
*/
|
|
20
|
+
readonly restoreHooks?: readonly AgentCheckpointRestoreHook[];
|
|
21
|
+
/** Per-hook restore ceiling in ms; defaults to `DEFAULT_CHECKPOINT_RESTORE_TIMEOUT_MS`. */
|
|
22
|
+
readonly restoreHookTimeoutMs?: number;
|
|
15
23
|
}
|
|
16
24
|
export interface AgentRunLifecycleRequest {
|
|
17
25
|
readonly ownership?: OwnershipScope;
|
|
@@ -22,6 +30,12 @@ export interface AgentRunLifecycleRequest {
|
|
|
22
30
|
readonly persistSessionState?: boolean;
|
|
23
31
|
/** Opt-in (plan 018 Task 6): restore persisted loaded-skill bodies on resume (requires `persistSessionState` too). */
|
|
24
32
|
readonly includeSkillBodies?: boolean;
|
|
33
|
+
/** Checkpoint sidecar metadata applied on resume (and to the resumed run's later checkpoints). */
|
|
34
|
+
readonly checkpointMetadata?: AgentRunCheckpointMetadataSource;
|
|
35
|
+
/** Plan 094 Task 3: restore hooks for this resume; the lifecycle's own hooks are used when omitted. */
|
|
36
|
+
readonly restoreHooks?: readonly AgentCheckpointRestoreHook[];
|
|
37
|
+
/** Per-hook restore ceiling in ms; defaults to `DEFAULT_CHECKPOINT_RESTORE_TIMEOUT_MS`. */
|
|
38
|
+
readonly restoreHookTimeoutMs?: number;
|
|
25
39
|
}
|
|
26
40
|
/** Bounded live-event options for a durable lifecycle resume. */
|
|
27
41
|
export interface AgentRunLifecycleStreamRequest extends AgentRunLifecycleRequest, SubscribeOptions {
|
|
@@ -1,21 +1,31 @@
|
|
|
1
1
|
import { assertValidAgentRunResume, pendingDecisionsOf, resolveRunDecisions } from "./agent-approval.js";
|
|
2
|
-
import { agentFingerprint, loadAgentRunState, publicState, saveAgentRunState } from "./agent-run-state.js";
|
|
2
|
+
import { agentFingerprint, loadAgentRunState, publicState, resolveCheckpointMetadata, saveAgentRunState } from "./agent-run-state.js";
|
|
3
3
|
import { RuntimeAgentSession, throwIfAbortedSignal } from "./agent-session.js";
|
|
4
|
-
import { parseAttentionStickyFrontier } from "./attention-compiler.js";
|
|
4
|
+
import { parseAttentionStickyFrontier, restoreAttentionFoldLedger } from "./attention-compiler.js";
|
|
5
|
+
import { runCheckpointRestoreHooks } from "./checkpoint-restore.js";
|
|
5
6
|
import { AgentRunStateError } from "./contracts.js";
|
|
6
7
|
function assertAgentId(actual, expected) {
|
|
7
8
|
if (expected !== undefined && actual !== expected)
|
|
8
9
|
throw new AgentRunStateError("Agent run capability mismatch");
|
|
9
10
|
}
|
|
11
|
+
/** Lifecycle-registered hooks run first, then per-request ones; either timeout setting wins for both. */
|
|
12
|
+
function restoreHookOptions(lifecycle, request) {
|
|
13
|
+
const hooks = [...(lifecycle.restoreHooks ?? []), ...(request.restoreHooks ?? [])];
|
|
14
|
+
const timeoutMs = request.restoreHookTimeoutMs ?? lifecycle.restoreHookTimeoutMs;
|
|
15
|
+
return {
|
|
16
|
+
...(hooks.length > 0 ? { restoreHooks: hooks } : {}),
|
|
17
|
+
...(timeoutMs === undefined ? {} : { restoreHookTimeoutMs: timeoutMs }),
|
|
18
|
+
};
|
|
19
|
+
}
|
|
10
20
|
/** Host capability for durable agent status/resume. Adapters supply authorized ownership only. */
|
|
11
21
|
export function createAgentRunLifecycle(options) {
|
|
12
22
|
return {
|
|
13
23
|
async status(ref, request = {}) {
|
|
14
24
|
request.signal?.throwIfAborted();
|
|
15
|
-
const { state, record } = await loadAgentRunState(options.checkpoints, ref, request.ownership);
|
|
25
|
+
const { state, record, metadata } = await loadAgentRunState(options.checkpoints, ref, request.ownership);
|
|
16
26
|
assertAgentId(state.agentId, request.agentId);
|
|
17
27
|
request.signal?.throwIfAborted();
|
|
18
|
-
return { state: publicState({ ...state, version: record.version }), version: record.version };
|
|
28
|
+
return { state: publicState({ ...state, version: record.version }), version: record.version, ...(metadata ? { metadata } : {}) };
|
|
19
29
|
},
|
|
20
30
|
async resume(ref, resume, request = {}) {
|
|
21
31
|
request.signal?.throwIfAborted();
|
|
@@ -31,6 +41,8 @@ export function createAgentRunLifecycle(options) {
|
|
|
31
41
|
signal: request.signal,
|
|
32
42
|
persistSessionState: request.persistSessionState,
|
|
33
43
|
includeSkillBodies: request.includeSkillBodies,
|
|
44
|
+
...(request.checkpointMetadata === undefined ? {} : { checkpointMetadata: request.checkpointMetadata }),
|
|
45
|
+
...restoreHookOptions(options, request),
|
|
34
46
|
});
|
|
35
47
|
},
|
|
36
48
|
async *resumeStream(ref, resume, request = {}) {
|
|
@@ -49,6 +61,8 @@ export function createAgentRunLifecycle(options) {
|
|
|
49
61
|
overflow: request.overflow,
|
|
50
62
|
persistSessionState: request.persistSessionState,
|
|
51
63
|
includeSkillBodies: request.includeSkillBodies,
|
|
64
|
+
...(request.checkpointMetadata === undefined ? {} : { checkpointMetadata: request.checkpointMetadata }),
|
|
65
|
+
...restoreHookOptions(options, request),
|
|
52
66
|
});
|
|
53
67
|
},
|
|
54
68
|
};
|
|
@@ -85,12 +99,13 @@ export async function* resumeAgentRunStream(agent, ref, resume, options) {
|
|
|
85
99
|
}
|
|
86
100
|
/**
|
|
87
101
|
* A `continue` resume needs a run whose frontier is intact: a crash-recovery checkpoint
|
|
88
|
-
* (`status: "running"`) or a
|
|
89
|
-
*
|
|
90
|
-
* 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.
|
|
91
105
|
*/
|
|
92
106
|
function isContinuableState(state) {
|
|
93
|
-
return state.status === "running" ||
|
|
107
|
+
return (state.status === "running" ||
|
|
108
|
+
(state.status === "succeeded" && (state.stopReason === "host_policy" || state.stopReason === "hook_limit")));
|
|
94
109
|
}
|
|
95
110
|
async function prepareAgentRunResume(agent, ref, resume, options, signal) {
|
|
96
111
|
throwIfAbortedSignal(signal);
|
|
@@ -99,7 +114,10 @@ async function prepareAgentRunResume(agent, ref, resume, options, signal) {
|
|
|
99
114
|
// and malformed untyped batches fail closed here instead of falling through to approval.
|
|
100
115
|
assertValidAgentRunResume(resume);
|
|
101
116
|
const continuing = resume.decision === "continue";
|
|
102
|
-
const { record, state } = await loadAgentRunState(options.checkpoints, ref, options.ownership);
|
|
117
|
+
const { record, state, metadata: recordMetadata } = await loadAgentRunState(options.checkpoints, ref, options.ownership);
|
|
118
|
+
// Sidecar metadata: a resume-time source wins; otherwise the record's existing map is
|
|
119
|
+
// preserved on every write below, so a non-durable resume cannot wipe it.
|
|
120
|
+
const checkpointMetadata = resolveCheckpointMetadata(options.checkpointMetadata) ?? recordMetadata;
|
|
103
121
|
if (state.definitionRevision !== options.definitionRevision ||
|
|
104
122
|
state.agentId !== (agent.config.id ?? agent.config.name) ||
|
|
105
123
|
state.fingerprint !== agentFingerprint(agent, options.definitionRevision)) {
|
|
@@ -121,6 +139,12 @@ async function prepareAgentRunResume(agent, ref, resume, options, signal) {
|
|
|
121
139
|
// Plan 078 Task 7: hand the reconstructed session to an observer (supervisor child-event pump)
|
|
122
140
|
// before any event flows. Called for every resume outcome; a throw fails closed.
|
|
123
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
|
+
}
|
|
124
148
|
// Opt-in session-state restore (plan 015 Task 4): names only; bodies re-resolve from
|
|
125
149
|
// the live registry the next time the model (re)loads them via load_skill.
|
|
126
150
|
if (options.persistSessionState && state.sessionState?.loadedSkillNames) {
|
|
@@ -137,22 +161,44 @@ async function prepareAgentRunResume(agent, ref, resume, options, signal) {
|
|
|
137
161
|
if (frontier)
|
|
138
162
|
session.restoreAttentionSticky(frontier);
|
|
139
163
|
}
|
|
164
|
+
// Plan 086 T3: durable folding writes its own ledger (with the frontier it belongs to), so it
|
|
165
|
+
// is restored whenever the checkpoint carries one — the `durable` opt-in was the host's
|
|
166
|
+
// consent, and a run without it never has this key. Without the frontier the ledger's rows
|
|
167
|
+
// would not be re-applied on an under-ratio turn, so the two ride together.
|
|
168
|
+
if (state.sessionState?.attentionFold) {
|
|
169
|
+
const ledger = restoreAttentionFoldLedger(state.sessionState.attentionFold);
|
|
170
|
+
if (ledger) {
|
|
171
|
+
session.restoreAttentionFold(ledger);
|
|
172
|
+
if (!options.persistSessionState && state.sessionState.attentionSticky) {
|
|
173
|
+
const frontier = parseAttentionStickyFrontier(state.sessionState.attentionSticky);
|
|
174
|
+
if (frontier)
|
|
175
|
+
session.restoreAttentionSticky(frontier);
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
}
|
|
140
179
|
// Plan 018 Task 6 (closeout `checkpoint-bodies`): restore exact instructions so the
|
|
141
180
|
// resumed session renders them registry-independently (no load_skill round-trip).
|
|
142
181
|
if (options.persistSessionState && options.includeSkillBodies && state.sessionState?.loadedSkillBodies) {
|
|
143
182
|
session.restoreLoadedSkillBodies(state.sessionState.loadedSkillBodies);
|
|
144
183
|
}
|
|
145
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;
|
|
146
191
|
// Legacy approve maps to allow-once on every pending decision; legacy deny keeps its
|
|
147
192
|
// terminal-denied behavior. Batch decisions are validated and applied atomically below.
|
|
148
193
|
const resolved = resume.decisions !== undefined
|
|
149
|
-
? await resolveRunDecisions({ agent, state, decisions: resume.decisions, signal })
|
|
194
|
+
? await resolveRunDecisions({ agent, state, decisions: resume.decisions, signal, guardrails: decisionGuardrails })
|
|
150
195
|
: resume.decision === "approve" && pendingDecisions
|
|
151
196
|
? await resolveRunDecisions({
|
|
152
197
|
agent,
|
|
153
198
|
state,
|
|
154
199
|
decisions: pendingDecisions.map((pending) => ({ approvalId: pending.approvalId, outcome: "allow_once" })),
|
|
155
200
|
signal,
|
|
201
|
+
guardrails: decisionGuardrails,
|
|
156
202
|
})
|
|
157
203
|
: undefined;
|
|
158
204
|
if (resolved && resolved.remaining.length > 0) {
|
|
@@ -231,6 +277,7 @@ async function prepareAgentRunResume(agent, ref, resume, options, signal) {
|
|
|
231
277
|
expectedVersion: record.version,
|
|
232
278
|
ownership: options.ownership,
|
|
233
279
|
fencingToken: options.fencingToken,
|
|
280
|
+
...(checkpointMetadata ? { metadata: checkpointMetadata } : {}),
|
|
234
281
|
});
|
|
235
282
|
return {
|
|
236
283
|
kind: "deny",
|
|
@@ -258,6 +305,21 @@ async function prepareAgentRunResume(agent, ref, resume, options, signal) {
|
|
|
258
305
|
throw new AgentRunStateError("Agent durable run-state configuration mismatch on resume");
|
|
259
306
|
}
|
|
260
307
|
throwIfAbortedSignal(signal);
|
|
308
|
+
// Plan 094 Task 3: restore external state (git commit, document versions) before the claim
|
|
309
|
+
// write. Every hook must succeed — a throw here leaves the checkpoint exactly as it was, and
|
|
310
|
+
// the conversation restore below never runs, so no half-restored world is claimed as resumed.
|
|
311
|
+
const restoreHooks = options.restoreHooks ?? [];
|
|
312
|
+
const restore = restoreHooks.length
|
|
313
|
+
? await runCheckpointRestoreHooks(restoreHooks, {
|
|
314
|
+
runId: state.runId,
|
|
315
|
+
sessionId: state.sessionId,
|
|
316
|
+
version: record.version,
|
|
317
|
+
status: state.status,
|
|
318
|
+
...(recordMetadata ? { metadata: recordMetadata } : {}),
|
|
319
|
+
checkpoint: record,
|
|
320
|
+
}, { timeoutMs: options.restoreHookTimeoutMs, signal })
|
|
321
|
+
: undefined;
|
|
322
|
+
throwIfAbortedSignal(signal);
|
|
261
323
|
const claimed = await saveAgentRunState({
|
|
262
324
|
checkpoints: options.checkpoints,
|
|
263
325
|
state: {
|
|
@@ -269,6 +331,7 @@ async function prepareAgentRunResume(agent, ref, resume, options, signal) {
|
|
|
269
331
|
expectedVersion: record.version,
|
|
270
332
|
ownership: options.ownership,
|
|
271
333
|
fencingToken: options.fencingToken,
|
|
334
|
+
...(checkpointMetadata ? { metadata: checkpointMetadata } : {}),
|
|
272
335
|
});
|
|
273
336
|
return {
|
|
274
337
|
kind: "claim",
|
|
@@ -276,6 +339,9 @@ async function prepareAgentRunResume(agent, ref, resume, options, signal) {
|
|
|
276
339
|
state: claimed.state,
|
|
277
340
|
decisions: resolved?.decisionsById,
|
|
278
341
|
ownership: options.ownership,
|
|
342
|
+
// The configured object must be passed by identity (agent-session assemble rejects a
|
|
343
|
+
// replaced config); its own `checkpointMetadata` provider wins, and the resolved map rides
|
|
344
|
+
// the session as a seed so later writes preserve a record's existing sidecar.
|
|
279
345
|
runState: configured ?? {
|
|
280
346
|
checkpoints: options.checkpoints,
|
|
281
347
|
definitionRevision: options.definitionRevision,
|
|
@@ -286,6 +352,8 @@ async function prepareAgentRunResume(agent, ref, resume, options, signal) {
|
|
|
286
352
|
// turn checkpoints without the host repeating the option on resume.
|
|
287
353
|
...(state.checkpointPolicy ? { checkpointPolicy: state.checkpointPolicy } : {}),
|
|
288
354
|
},
|
|
355
|
+
...(checkpointMetadata ? { checkpointMetadata } : {}),
|
|
356
|
+
...(restore ? { restore } : {}),
|
|
289
357
|
};
|
|
290
358
|
}
|
|
291
359
|
/** Pending decisions of a suspended state, synthesizing the legacy single-approval shape. */
|
|
@@ -298,6 +366,9 @@ async function executePreparedAgentRunResume(prepared, signal) {
|
|
|
298
366
|
await prepared.session.recordDurableResumption(prepared.result.runId, prepared.interruption, prepared.version, prepared.ownership);
|
|
299
367
|
return prepared.result;
|
|
300
368
|
}
|
|
301
|
-
return prepared.session.resumeDurable(prepared.state, prepared.runState, prepared.ownership, signal, prepared.decisions
|
|
369
|
+
return prepared.session.resumeDurable(prepared.state, prepared.runState, prepared.ownership, signal, prepared.decisions, {
|
|
370
|
+
...(prepared.checkpointMetadata ? { checkpointMetadata: prepared.checkpointMetadata } : {}),
|
|
371
|
+
...(prepared.restore ? { restore: prepared.restore } : {}),
|
|
372
|
+
});
|
|
302
373
|
}
|
|
303
374
|
//# sourceMappingURL=agent-run-lifecycle.js.map
|