@arnilo/prism 0.6.0 → 0.7.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (129) hide show
  1. package/CHANGELOG.md +44 -5
  2. package/README.md +10 -10
  3. package/dist/agent-definitions.js +1 -0
  4. package/dist/agent-run-lifecycle.js +11 -0
  5. package/dist/agent-run-state.d.ts +6 -0
  6. package/dist/agent-run-state.js +29 -9
  7. package/dist/agent-session/session/assemble.js +33 -2
  8. package/dist/agent-session/session/persist.js +6 -2
  9. package/dist/agent-session/session/tool-round.js +1 -0
  10. package/dist/agent-session/session/types.d.ts +10 -0
  11. package/dist/agent-session/session.d.ts +14 -0
  12. package/dist/agent-session/session.js +40 -3
  13. package/dist/artifacts.d.ts +39 -1
  14. package/dist/artifacts.js +73 -0
  15. package/dist/attention-compiler.d.ts +121 -0
  16. package/dist/attention-compiler.js +479 -0
  17. package/dist/cli-init.js +20 -6
  18. package/dist/context-budget.d.ts +20 -1
  19. package/dist/context-budget.js +10 -1
  20. package/dist/contracts-core/agent.d.ts +7 -0
  21. package/dist/contracts-core/attention.d.ts +66 -0
  22. package/dist/contracts-core/attention.js +2 -0
  23. package/dist/contracts-core/compaction.d.ts +59 -0
  24. package/dist/contracts-core/compaction.js +77 -1
  25. package/dist/contracts-core/provider.d.ts +4 -0
  26. package/dist/contracts-core.d.ts +1 -0
  27. package/dist/contracts-core.js +1 -0
  28. package/dist/contracts-protocol.d.ts +29 -0
  29. package/dist/contracts-run-state.d.ts +6 -0
  30. package/dist/host-composition.d.ts +78 -0
  31. package/dist/host-composition.js +248 -0
  32. package/dist/index.d.ts +9 -6
  33. package/dist/index.js +5 -4
  34. package/dist/input.d.ts +13 -1
  35. package/dist/input.js +40 -1
  36. package/dist/secure-agent.d.ts +2 -0
  37. package/dist/secure-agent.js +6 -1
  38. package/dist/tool-result-fold.d.ts +12 -0
  39. package/dist/tool-result-fold.js +13 -6
  40. package/dist/tools.d.ts +10 -0
  41. package/dist/tools.js +41 -0
  42. package/docs/acp-agent.md +42 -11
  43. package/docs/acp.md +2 -1
  44. package/docs/ag-ui.md +5 -3
  45. package/docs/agent-definitions.md +9 -1
  46. package/docs/agent-events.md +4 -1
  47. package/docs/agent-session-runtime.md +6 -6
  48. package/docs/attention-compiler.md +272 -0
  49. package/docs/cli-rpc.md +4 -2
  50. package/docs/coding-agent-tools.md +1 -1
  51. package/docs/coding-security.md +5 -3
  52. package/docs/coding-tools.md +1 -1
  53. package/docs/coding-workspaces.md +22 -0
  54. package/docs/compaction-and-retry.md +36 -4
  55. package/docs/compaction-observational-memory.md +62 -9
  56. package/docs/context-and-skills.md +4 -2
  57. package/docs/conversations.md +1 -1
  58. package/docs/dev-inspector.md +4 -0
  59. package/docs/device-adapters.md +1 -0
  60. package/docs/document-reader.md +11 -3
  61. package/docs/documents.md +10 -2
  62. package/docs/enterprise-postgres-state.md +2 -2
  63. package/docs/evaluations.md +168 -4
  64. package/docs/execution-timeline.md +180 -0
  65. package/docs/history/0.7.0-primitive-review.md +254 -0
  66. package/docs/history/migration-0.0.md +2 -2
  67. package/docs/history/release-handoffs.md +37 -1
  68. package/docs/host-compositions.md +147 -0
  69. package/docs/hosted-sandboxes.md +94 -0
  70. package/docs/index.md +58 -39
  71. package/docs/input-and-prompt-assembly.md +1 -0
  72. package/docs/knowledge-sync.md +84 -0
  73. package/docs/language-intelligence.md +1 -1
  74. package/docs/live-testing.md +4 -1
  75. package/docs/mcp-tools.md +2 -1
  76. package/docs/memory-fabric.md +416 -0
  77. package/docs/migrate-to-0.5.md +1 -1
  78. package/docs/migrate-to-0.6.md +1 -0
  79. package/docs/migrate-to-0.7.md +345 -0
  80. package/docs/migration.md +13 -1
  81. package/docs/model-routing.md +79 -4
  82. package/docs/multi-agent-patterns.md +20 -6
  83. package/docs/observability.md +52 -1
  84. package/docs/operations.md +13 -1
  85. package/docs/options-index.md +13 -1
  86. package/docs/peer-dependencies.md +6 -4
  87. package/docs/process-sessions.md +3 -1
  88. package/docs/prompt-registry.md +1 -1
  89. package/docs/provider-caching.md +4 -2
  90. package/docs/provider-conformance.md +1 -1
  91. package/docs/provider-packages.md +22 -22
  92. package/docs/providers/bedrock.md +71 -7
  93. package/docs/providers/openai.md +1 -1
  94. package/docs/rag.md +24 -8
  95. package/docs/realtime-voice.md +87 -0
  96. package/docs/release-and-install.md +36 -34
  97. package/docs/runs-and-usage.md +3 -2
  98. package/docs/server.md +5 -3
  99. package/docs/speech.md +2 -0
  100. package/docs/supervisors.md +33 -5
  101. package/docs/testing.md +1 -1
  102. package/docs/thinking-and-reasoning.md +3 -1
  103. package/docs/tools.md +6 -5
  104. package/docs/web-tools.md +2 -1
  105. package/docs/work-artifacts-and-review.md +14 -4
  106. package/docs/work-connectors.md +3 -1
  107. package/docs/work-tools.md +14 -4
  108. package/docs/workflows.md +69 -1
  109. package/docs/working-and-semantic-memory.md +25 -14
  110. package/package.json +1 -1
  111. package/templates/README.md +2 -0
  112. package/templates/business-worker/README.md.tmpl +19 -0
  113. package/templates/business-worker/env.example.tmpl +1 -0
  114. package/templates/business-worker/gitignore.tmpl +11 -0
  115. package/templates/business-worker/manifest.json +11 -0
  116. package/templates/business-worker/package.json.tmpl +23 -0
  117. package/templates/business-worker/src/agent.ts.tmpl +92 -0
  118. package/templates/business-worker/src/index.ts.tmpl +13 -0
  119. package/templates/business-worker/src/tests/agent.test.ts.tmpl +77 -0
  120. package/templates/business-worker/tsconfig.json.tmpl +15 -0
  121. package/templates/personal-assistant/README.md.tmpl +18 -0
  122. package/templates/personal-assistant/env.example.tmpl +1 -0
  123. package/templates/personal-assistant/gitignore.tmpl +11 -0
  124. package/templates/personal-assistant/manifest.json +11 -0
  125. package/templates/personal-assistant/package.json.tmpl +23 -0
  126. package/templates/personal-assistant/src/agent.ts.tmpl +65 -0
  127. package/templates/personal-assistant/src/index.ts.tmpl +13 -0
  128. package/templates/personal-assistant/src/tests/agent.test.ts.tmpl +28 -0
  129. package/templates/personal-assistant/tsconfig.json.tmpl +15 -0
package/CHANGELOG.md CHANGED
@@ -1,3 +1,42 @@
1
+ ## [0.7.0] - 2026-09-15 (extended line: plans 072, 073, 074, 075, 077, 078)
2
+
3
+ > **Channels are not in this cut.** Plan 079 (Telegram/Signal adapters) was reassigned to **0.8.0** so the 0.7.0 cut stops waiting on it; nothing in this release mentions or ships a channel adapter.
4
+
5
+ ### Added
6
+ - **Execution timeline (plan 072).** `@arnilo/prism-core/governance/observability` ships one frozen, JSON-serializable view-model for what a run did: `projectAgentTimeline` (live `AgentEvent[]`), `projectTraceTimeline` (persisted `EvaluationTrace`), `projectWorkflowTimeline` (workflow events plus optional checkpoint), incremental `createTimelineFolder` / `createWorkflowTimelineFolder` folders for SSE/cockpit updates, and `summarizeTimeline` / `summarizeSession` rollups (tool counts capped at 64, no double counting across runs). See [docs/execution-timeline.md](docs/execution-timeline.md).
7
+ - **Workflow graph view-model (plan 072).** `serializeWorkflowGraph`, `workflowGraphToMermaid`, `workflowGraphToDot`, `projectWorkflowGraphRun`, and `createWorkflowGraphRunFolder` render a workflow DAG and overlay live or checkpoint run state (`WorkflowGraphRunView`) without executing it.
8
+ - **Trajectory and outcome evals (plan 072).** Scorers (`defineScorer`, pairwise preferences, model-judge budgets), `runScenario`, `runExperiment`, `runWorkflowExperiment`, dataset items with expected trajectories, trials, manifests, comparisons, and thresholds — with `runComparison` / `datasetFromRuns` curation over recorded runs.
9
+ - **Eval primitives match their contracts (plan 073 Tasks 30–31).** Injection/timeline holes closed, deterministic `mulberry32` sampling, `collectWhileRunning`, and host-activity eval packs (coding, browser, memory, voice invariants) over the true primitives.
10
+ - **Attention compiler (plan 074, opt-in).** `createAttentionCompiler` / `resolveInputCap` / `compileAttention` / `createAttentionTruncationTrigger`: a per-turn gate that measures the assembled input against a host ratio of the model input cap and, only past the ratio, mutates a **history clone** oldest thinking blocks first, then fold-eligible tool results — keeping cache prefixes, the session store, and the observational-memory ledger untouched, and raising `AttentionBudgetError` rather than silently dropping context. Wired through `AgentConfig`, `AgentDefinition`, and `RunOptions` (run overlay narrows), plus `attention_compiled` telemetry and an `attention` timeline step; sticky frontier persists through `persistSessionState`. See [docs/attention-compiler.md](docs/attention-compiler.md).
11
+ - **Memory fabric subpath (plan 075, opt-in).** `@arnilo/prism-memory/fabric` adds typed notes (`fact`, `procedure`, `file`, `working`, `episode`) with links, validity windows, time/tool recall, conversation search, and opt-in consolidation/linker/evolution workers over the stores a host already configured. Inert until `fabric.attach(session)`; no new package, provider, database, or mandatory dependency. See [docs/memory-fabric.md](docs/memory-fabric.md).
12
+ - **Work-scope memory index (plan 077, opt-in).** `createWorkScopeController` appends `om.scope.*` entries to one observational-memory ledger; `foldWorkScopeMap`, `projectWorkMemory`, and `withWorkScope` project the outline to a host-selected working set (leaf `self+ancestors` by default), auto-bind new observations to the leaf, and skip the observation dropper while any host scope exists. Caps fail closed (256 scopes, depth 8, 4,096 binds, 512-char labels). See [docs/compaction-observational-memory.md](docs/compaction-observational-memory.md).
13
+ - **Host-owned subagent spawn (plan 078, opt-in).** `createSpawnAgentTool` turns the supervisor's host-owned child catalog into a non-exclusive `spawn_agent` whose closed schema exposes only allow-listed child ids, input, an optional thread id, and `mode: "sync" | "async"`; `createWaitAgentTool` / `createCancelAgentTool` join or abort async handles from `delegateAsync()`. Child identity narrows from the parent, results and errors are redacted, child slots are reserved atomically (after a before-hook narrows limits), and parent-run abort cancels running children. `createWorktreeChildFactory` gives each child its own linked git worktree and cleans it up on every terminal outcome (including a suspended child that later resumes), while `observeSupervisorLifecycle` bridges `delegation_*` events to redacted coding `subagent_started` / `subagent_stopped` lifecycle events. See [docs/supervisors.md](docs/supervisors.md).
14
+ - **Governed host completeness (plan 073).** Governed provider invocation with aggregate task/tenant accounting across every paid work kind (enterprise migration `006_aggregate_budgets`); durable business-action drafts with editable approvals; Docker process sessions and coherent workspace recovery; incremental Drive knowledge synchronization; snapshot/reconnect lifecycle with a hosted E2B sandbox; fair worker admission and operator routes; cross-layer memory lineage with correction and revocation; semantic artifact review with evidence-backed citations; import-fidelity reports with optional OCR; monotonic per-run tool narrowing with remote invalidation; native Bedrock `Converse`/`ConverseStream`; and governed realtime voice orchestration. New pages: [docs/execution-timeline.md](docs/execution-timeline.md), [docs/host-compositions.md](docs/host-compositions.md), [docs/hosted-sandboxes.md](docs/hosted-sandboxes.md), [docs/knowledge-sync.md](docs/knowledge-sync.md), [docs/realtime-voice.md](docs/realtime-voice.md), [docs/attention-compiler.md](docs/attention-compiler.md), [docs/memory-fabric.md](docs/memory-fabric.md).
15
+ - **Examples.** Runnable demos for the new surfaces: `examples/execution-timeline.ts`, `examples/behavior-evaluation.ts`, `examples/coding-browser-evaluation.ts`, `examples/attention-compiler.ts`, `examples/memory-fabric.ts`, `examples/work-scopes-coding-loop.ts`, `examples/spawn-agent-tool.ts`, `examples/governed-provider.ts`, `examples/docker-process-session.ts`, `examples/drive-rag-sync.ts`, `examples/hosted-sandbox.ts`, `examples/scanned-document-rag.ts`, and `examples/realtime-voice-host.ts`.
16
+
17
+ ### Changed
18
+ - **Lockstep `0.6.0` → `0.7.0`.** All ten publishable manifests move together with `^0.7.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`).
19
+ - **Compat baselines regenerated** (`--update-baseline`): **469 added declarations, zero removals**. The additions are the new subpath APIs above plus members added to existing declaration groups; no export was renamed or dropped.
20
+ - **Release budgets rebaselined with recorded reasons**: root packed/unpacked/fileCount moved for the new dist modules, templates, and docs pages, and per-package export ceilings carry the 0.7.0 addition list. Startup and timing ceilings are unchanged.
21
+ - **Migration guide for 0.6.0 hosts**: [docs/migrate-to-0.7.md](docs/migrate-to-0.7.md) (per-item actions for the ACP/model-router refusals, every tightening, the opt-in activation steps, and rollback), indexed from [docs/migration.md](docs/migration.md) and [docs/index.md](docs/index.md).
22
+ - **Options index and peer matrix** cover the new surfaces: [docs/options-index.md](docs/options-index.md) routes `AttentionCompilerOptions`/`AttentionInputCapOptions`/`AttentionCompileOptions`/`AttentionTruncationTriggerOptions`, the fabric and work-scope option objects, and the supervisor/spawn/worktree/lifecycle options to their owning pages (gated by `scripts/live-doc-check.test.mjs`).
23
+
24
+ ### Fixed
25
+ - **Task-scoped enterprise budgets failed on their first insert.** Migration `006_aggregate_budgets`' insert bound one JavaScript `Date` to both a `timestamptz` column and interval arithmetic, so PostgreSQL refused the statement with `42P08 inconsistent types deduced for parameter $8`; the parameter is now explicitly `::timestamptz` (found by the protected PostgreSQL leg, not by hermetic doubles).
26
+ - **A serialization failure inside the budget upsert was swallowed.** The read-then-insert path caught *every* error from the `SELECT … FOR UPDATE` probe and then issued SQL against an aborted transaction (`25P02`), which defeated the retry loop; only a genuinely missing row is recoverable by inserting now.
27
+ - **Serializable retry policy was too small for concurrent writers.** Budget/rate rewrites on one row now retry up to 12 times with full-jitter exponential backoff (capped at 250 ms), so a 16-client burst converges instead of exhausting three near-instant attempts with `ERR_PRISM_ENTERPRISE_POSTGRES_RETRYABLE`.
28
+ - **Integration tests were stale against migration 006** (expected five migrations) and `scripts/phase27-release.test.mjs` still asserted that no `006_` migration existed; both now check the append-only list including `006_aggregate_budgets`.
29
+
30
+ ### Security
31
+ - **ACP MCP destination matching (Trap A).** `mcp.allow` entries now match by WHATWG origin plus path-segment subtree: origin lookalikes (`mcp.example.com.attacker.invalid`) and sibling path prefixes (`/mcp-other`) no longer match, and allow entries carrying userinfo, query, fragment, or ambiguous encoded path forms fail `ConfigError` at parse time.
32
+ - **Model-router facade fails closed (Trap B).** `router.providerSource(model)` throws `ERR_PRISM_MODEL_ROUTER_ASYNC_REQUIRED` / `ERR_PRISM_MODEL_ROUTER_ASYNC_STATE` instead of handing back a provider that bypasses budgets, rate limits, circuits, fallbacks, selection policies, or durable state; `isProviderSourceEligible` lets a host check first.
33
+ - **ACP launcher requires a real provider (Trap C/R05).** No silent `createMockProvider()` fallback; mock mode is an explicit opt-in.
34
+ - **Per-run tool narrowing is monotonic (R11)** and remote invalidation is honored; child agents cannot widen the tenant, account, user, or scopes of the parent identity (`narrowIdentity` + `assertIdentityPropagation`).
35
+
36
+ ### Notes
37
+ - **Protected PostgreSQL leg now green on 0.7.0 code**: `PRISM_TEST_POSTGRES_URL=… npm run test:postgres` passes core (72), memory (457), and the phase conformance legs (11) against `pgvector/pgvector:pg16`, the same image the release workflow uses. The three fixes above are what that leg caught.
38
+ - **Node floor is unchanged** at `>=22` (Node 22/24 supported).
39
+
1
40
  ## [0.6.0] - 2026-09-12 (plans 070, 071)
2
41
 
3
42
  > **0.5.7 was never published.** This release folds that cut's content (durable concurrent tool rounds, strict-provider tool results, host-tunable knobs, peer/options truth, the dependency refresh, and the module splits) together with the 0.6.0 changes below, so a host on 0.5.6 upgrades once. See [docs/migrate-to-0.6.md](docs/migrate-to-0.6.md).
@@ -129,27 +168,27 @@
129
168
  ### Added
130
169
  - **OKF adoption (`@arnilo/prism-wiki`)**: wiki-init/refresh/lint emit and validate
131
170
  OKF v0.2 bundles (Karpathy prompt retained). See `docs/wiki.md`.
132
- - **DOCS-1 (Clay integration findings)**: three integrator contracts, in place on
171
+ - **DOCS-1 (integration findings)**: three integrator contracts, in place on
133
172
  the pages that own them — resume-aware workflow nodes (`ctx.resume` or silent
134
173
  re-suspend) in `docs/workflows.md`; supervisor child factories return `Agent`
135
174
  not `AgentSession` (`SupervisorError: child "<id>" factory must return an
136
175
  Agent, got <type>`) plus durable-store nested approvals in `docs/supervisors.md`;
137
176
  task-boundary `session.compact()` fails closed during an active run in
138
177
  `docs/compaction-and-retry.md`. Each block links `examples/autonomous-coding-loop.ts`.
139
- - **FEATURE-2 (Clay integration findings)**: documented bounded iterate-until-done
178
+ - **FEATURE-2 (integration findings)**: documented bounded iterate-until-done
140
179
  host-loop pattern in `docs/workflows.md` — one `runWorkflow` per iteration,
141
180
  iteration state in workflow inputs, explicit termination predicate and budgets,
142
181
  typed `BudgetExhaustedError` (fail-closed, never a hang), `replayWorkflow` per
143
182
  iteration run id. Seeded by `examples/autonomous-coding-loop.ts`. Plan 045 `loop`
144
183
  node remains the future in-graph primitive; this intake ships the docs+example
145
184
  minimum only.
146
- - **FEATURE-6 (Clay integration findings)**: composite `examples/autonomous-coding-loop.ts`
185
+ - **FEATURE-6 (integration findings)**: composite `examples/autonomous-coding-loop.ts`
147
186
  conformance reference — goal → roadmap → per-task supervisor children (per-child
148
187
  models) → `runCodingGoalVerify`-style validation → observational-memory attach +
149
188
  task-boundary compact + recall → human gate with simulated restart → host-side
150
189
  bounded iterate-until-done with deterministic budget exhaustion. Mock providers
151
190
  only; no credentials or network.
152
- - **FEATURE-3 (Clay integration findings)**: host-opt-in command driver hooks.
191
+ - **FEATURE-3 (integration findings)**: host-opt-in command driver hooks.
153
192
  `CommandExecutionContext` gains an optional `drivers?: CommandDrivers`
154
193
  (`startRun` / `startWorkflow` / `steer` — typed minimal handles returning
155
194
  `AgentRunResult`-shaped results / workflow run id + status) so a contributed
@@ -159,7 +198,7 @@
159
198
  the context shape unchanged (no key, no allocation). Drivers are
160
199
  host-injected capabilities, never package-supplied.
161
200
  ### Fixed
162
- - **FEATURE-1 (Clay integration findings)**: `resolveAgentDefinition` no longer
201
+ - **FEATURE-1 (integration findings)**: `resolveAgentDefinition` no longer
163
202
  throws `Agent "<name>" has no model` when the declarative definition omits
164
203
  `model` but `context.overrides.model` supplies one — the fallback is a
165
204
  single `??` at `buildBaseConfig`, the `create()` path is unchanged, and a
package/README.md CHANGED
@@ -162,16 +162,16 @@ printf '{"id":"1","command":"prompt","params":{"input":"Hi"}}\n' \
162
162
 
163
163
  | package | version | notes |
164
164
  | --- | --- | --- |
165
- | `@arnilo/prism` | 0.6.0 | core — runtime, CLI/RPC, templates, docs |
166
- | `@arnilo/prism-coding-tools` | 0.6.0 | family — /agent, /security, /document-reader, /openapi, /computer-use-linux, /dev, /caveman, /ponytail, /impeccable subpaths |
167
- | `@arnilo/prism-core` | 0.6.0 | family — /runtime, /sessions, /governance, /credentials, /enterprise, /work, /validation subpaths |
168
- | `@arnilo/prism-providers` | 0.6.0 | family — all provider adapters as `/<adapter>` subpaths |
169
- | `@arnilo/prism-acp-agent` | 0.6.0 | capability — ACP adapter |
170
- | `@arnilo/prism-ag-ui` | 0.6.0 | capability — AG-UI/A2A/A2UI adapter |
171
- | `@arnilo/prism-mcp` | 0.6.0 | capability — MCP client/server/OAuth interop |
172
- | `@arnilo/prism-memory` | 0.6.0 | capability — memory plus /rag, /compaction/*, /graft, /wiki subpaths |
173
- | `@arnilo/prism-office` | 0.6.0 | capability — /documents, /sheets, /diagrams subpaths |
174
- | `@arnilo/prism-web-tools` | 0.6.0 | capability — Brave/Exa/Firecrawl plus peer-gated /browser and /obscura subpaths |
165
+ | `@arnilo/prism` | 0.7.0 | core — runtime, CLI/RPC, templates, docs |
166
+ | `@arnilo/prism-coding-tools` | 0.7.0 | family — /agent, /security, /document-reader, /openapi, /computer-use-linux, /dev, /caveman, /ponytail, /impeccable subpaths |
167
+ | `@arnilo/prism-core` | 0.7.0 | family — /runtime, /sessions, /governance, /credentials, /enterprise, /work, /validation subpaths |
168
+ | `@arnilo/prism-providers` | 0.7.0 | family — all provider adapters as `/<adapter>` subpaths |
169
+ | `@arnilo/prism-acp-agent` | 0.7.0 | capability — ACP adapter |
170
+ | `@arnilo/prism-ag-ui` | 0.7.0 | capability — AG-UI/A2A/A2UI adapter |
171
+ | `@arnilo/prism-mcp` | 0.7.0 | capability — MCP client/server/OAuth interop |
172
+ | `@arnilo/prism-memory` | 0.7.0 | capability — memory plus /rag, /compaction/*, /fabric, /graft, /wiki subpaths |
173
+ | `@arnilo/prism-office` | 0.7.0 | capability — /documents, /sheets, /diagrams subpaths |
174
+ | `@arnilo/prism-web-tools` | 0.7.0 | capability — Brave/Exa/Firecrawl plus peer-gated /browser and /obscura subpaths |
175
175
  <!-- generated:package-truth:inventory end -->
176
176
 
177
177
  ## Scripts
@@ -34,6 +34,7 @@ function buildBaseConfig(def, context) {
34
34
  ...(def.context !== undefined && { context: resolveContextProviders(def.name, def.context, context) }),
35
35
  ...(def.systemPrompt !== undefined && { systemPrompt: def.systemPrompt }),
36
36
  ...(def.instructions !== undefined && { instructions: def.instructions }),
37
+ ...(def.attentionCompiler !== undefined && { attentionCompiler: def.attentionCompiler }),
37
38
  ...(def.loop !== undefined && { loop: def.loop }),
38
39
  ...(def.metadata !== undefined && { metadata: def.metadata }),
39
40
  };
@@ -1,4 +1,5 @@
1
1
  import { assertValidAgentRunResume, pendingDecisionsOf, resolveRunDecisions } from "./agent-approval.js";
2
+ import { parseAttentionStickyFrontier } from "./attention-compiler.js";
2
3
  import { agentFingerprint, loadAgentRunState, publicState, saveAgentRunState } from "./agent-run-state.js";
3
4
  import { RuntimeAgentSession, throwIfAbortedSignal } from "./agent-session.js";
4
5
  import { AgentRunStateError } from "./contracts.js";
@@ -96,6 +97,9 @@ async function prepareAgentRunResume(agent, ref, resume, options, signal) {
96
97
  throw new AgentRunStateError("Stale or non-suspended agent run resume");
97
98
  }
98
99
  const session = new RuntimeAgentSession({ agent, id: state.sessionId, leafId: state.leafId });
100
+ // Plan 078 Task 7: hand the reconstructed session to an observer (supervisor child-event pump)
101
+ // before any event flows. Called for every resume outcome; a throw fails closed.
102
+ options.onSession?.(session);
99
103
  // Opt-in session-state restore (plan 015 Task 4): names only; bodies re-resolve from
100
104
  // the live registry the next time the model (re)loads them via load_skill.
101
105
  if (options.persistSessionState && state.sessionState?.loadedSkillNames) {
@@ -105,6 +109,13 @@ async function prepareAgentRunResume(agent, ref, resume, options, signal) {
105
109
  if (options.persistSessionState && state.sessionState?.activatedToolNames) {
106
110
  session.restoreActivatedTools(state.sessionState.activatedToolNames);
107
111
  }
112
+ // Plan 074 P3: restore sticky attention mutations (already validated at load) so the first
113
+ // turn after a resume keeps its stubs instead of re-deciding them from the ratio.
114
+ if (options.persistSessionState && state.sessionState?.attentionSticky) {
115
+ const frontier = parseAttentionStickyFrontier(state.sessionState.attentionSticky);
116
+ if (frontier)
117
+ session.restoreAttentionSticky(frontier);
118
+ }
108
119
  // Plan 018 Task 6 (closeout `checkpoint-bodies`): restore exact instructions so the
109
120
  // resumed session renders them registry-independently (no load_skill round-trip).
110
121
  if (options.persistSessionState && options.includeSkillBodies && state.sessionState?.loadedSkillBodies) {
@@ -1,4 +1,5 @@
1
1
  import type { Agent, AgentRunInterruption, AgentRunRef, AgentRunState, AgentRunStateOptions, CheckpointRecord, CheckpointStore, JsonValue, Message, ModelConfig, NestedRunRef, OwnershipScope, RunDecision, RunLimitCounters, StickyDecision, ToolCallContent } from "./contracts.js";
2
+ import { type PersistedAttentionStickyFrontier } from "./attention-compiler.js";
2
3
  import type { SecretRedactor } from "./redaction.js";
3
4
  import { type LoadedSkillBodiesEntry } from "./skill-load.js";
4
5
  export declare const AGENT_RUN_STATE_NAMESPACE = "prism.agent-run";
@@ -46,7 +47,12 @@ export interface StoredAgentRunState extends AgentRunState {
46
47
  readonly loadedSkillBodies?: readonly LoadedSkillBodiesEntry[];
47
48
  /** Plan 041: tools activated via `search_tools` (names only; inert for absent tools on restore). */
48
49
  readonly activatedToolNames?: readonly string[];
50
+ /** Plan 074 P3: sticky attention mutations (thinking hashes + tool-call ids), so a durable
51
+ * resume keeps its stubs instead of re-deciding on the first turn. Validated on load. */
52
+ readonly attentionSticky?: PersistedAttentionStickyFrontier;
49
53
  };
54
+ /** Per-run allow-list (Task 21). Absent = full registered set (legacy checkpoints). */
55
+ readonly toolNames?: readonly string[];
50
56
  }
51
57
  /** Session-state caps (plan 015 Task 4): bounded names charged against the run-state byte budget. */
52
58
  export declare const MAX_PERSISTED_SKILL_NAMES = 64;
@@ -1,6 +1,8 @@
1
1
  import { createHash } from "node:crypto";
2
2
  import { AgentLoopStateError, AgentRunStateError } from "./contracts.js";
3
+ import { parseAttentionStickyFrontier } from "./attention-compiler.js";
3
4
  import { validateLoadedSkillBodies } from "./skill-load.js";
5
+ import { HARD_RUN_TOOL_NAMES } from "./tools.js";
4
6
  export const AGENT_RUN_STATE_NAMESPACE = "prism.agent-run";
5
7
  export const AGENT_RUN_STATE_SCHEMA_VERSION = 1;
6
8
  export const DEFAULT_MAX_AGENT_RUN_STATE_BYTES = 256 * 1024;
@@ -128,7 +130,7 @@ export async function saveAgentRunState(input) {
128
130
  return { record, state: { ...bounded, version: record.version } };
129
131
  }
130
132
  export function publicState(state) {
131
- const { input: _input, pending: _pending, pendingCalls: _pendingCalls, nestedRuns: _nestedRuns, interruptBeforeTool: _interruptBeforeTool, counters: _counters, deadlineAt: _deadlineAt, ...publicValue } = state;
133
+ const { input: _input, pending: _pending, pendingCalls: _pendingCalls, nestedRuns: _nestedRuns, interruptBeforeTool: _interruptBeforeTool, counters: _counters, deadlineAt: _deadlineAt, toolNames: _toolNames, ...publicValue } = state;
132
134
  return publicValue;
133
135
  }
134
136
  export function initialAgentRunState(input) {
@@ -203,6 +205,16 @@ export function parseAgentRunState(value, version) {
203
205
  !("snapshot" in state.loopState))) {
204
206
  throw new AgentRunStateError("Malformed agent run loop state");
205
207
  }
208
+ if (state.toolNames !== undefined) {
209
+ if (!Array.isArray(state.toolNames) || state.toolNames.length > HARD_RUN_TOOL_NAMES) {
210
+ throw new AgentRunStateError(`Run toolNames exceed ${HARD_RUN_TOOL_NAMES} entries`);
211
+ }
212
+ for (const name of state.toolNames) {
213
+ if (typeof name !== "string" || name.length === 0 || name.length > MAX_PERSISTED_SKILL_NAME_CHARS) {
214
+ throw new AgentRunStateError("Malformed agent run toolNames");
215
+ }
216
+ }
217
+ }
206
218
  // Load bounds against the hard cap, not the default: the configured maxStateBytes is a
207
219
  // save-side policy knob, while the load-side bound is only a DoS ceiling. States saved
208
220
  // with a raised maxStateBytes must remain resumable.
@@ -261,15 +273,23 @@ function validateSessionState(sessionState) {
261
273
  }
262
274
  }
263
275
  const activated = sessionState.activatedToolNames;
264
- if (activated === undefined)
265
- return;
266
- if (!Array.isArray(activated) || activated.length > MAX_PERSISTED_ACTIVATED_TOOL_NAMES) {
267
- throw new AgentRunStateError(`Activated-tool names exceed ${MAX_PERSISTED_ACTIVATED_TOOL_NAMES} entries`);
268
- }
269
- for (const name of activated) {
270
- if (typeof name !== "string" || name.length > MAX_PERSISTED_SKILL_NAME_CHARS) {
271
- throw new AgentRunStateError(`Activated-tool name exceeds ${MAX_PERSISTED_SKILL_NAME_CHARS} chars`);
276
+ if (activated !== undefined) {
277
+ if (!Array.isArray(activated) || activated.length > MAX_PERSISTED_ACTIVATED_TOOL_NAMES) {
278
+ throw new AgentRunStateError(`Activated-tool names exceed ${MAX_PERSISTED_ACTIVATED_TOOL_NAMES} entries`);
272
279
  }
280
+ for (const name of activated) {
281
+ if (typeof name !== "string" || name.length > MAX_PERSISTED_SKILL_NAME_CHARS) {
282
+ throw new AgentRunStateError(`Activated-tool name exceeds ${MAX_PERSISTED_SKILL_NAME_CHARS} chars`);
283
+ }
284
+ }
285
+ }
286
+ const attention = sessionState.attentionSticky;
287
+ if (attention === undefined)
288
+ return;
289
+ // Both arrays are capped by the parser, and a malformed frontier is dropped rather than
290
+ // failing the resume: re-deciding a mutation is safe, refusing to resume is not.
291
+ if (parseAttentionStickyFrontier(attention) === undefined) {
292
+ throw new AgentRunStateError("Malformed agent run attention frontier");
273
293
  }
274
294
  }
275
295
  //# sourceMappingURL=agent-run-state.js.map
@@ -3,6 +3,7 @@ import { AgentRunSuspended } from "../../agent-approval.js";
3
3
  import { resolveLoop, resolveToolConcurrency } from "../../agent-loops.js";
4
4
  import { validateRunStateOptions } from "../../agent-run-state.js";
5
5
  import { activeTools } from "../../agent-tool-dispatch.js";
6
+ import { resolveRunAttentionCompiler } from "../../attention-compiler.js";
6
7
  import { AgentLoopStateError, AgentRunError, AgentRunStateError } from "../../contracts.js";
7
8
  import { assertGuardrailsAllowed, runGuardrails } from "../../guardrails.js";
8
9
  import { identityTelemetryAttributes, ownershipFromIdentity, resolveRunIdentity } from "../../identity.js";
@@ -16,7 +17,7 @@ import { assertStructuredOutputRequestSupported, resolveRunProviderOptions } fro
16
17
  import { composeSystemPrompt, mergeSystemPromptConfig } from "../../system-prompts.js";
17
18
  import { resolveToolResultFold } from "../../tool-result-fold.js";
18
19
  import { createSearchToolsTool, createToolSearchState, resolveToolsDisclosure } from "../../tool-search.js";
19
- import { createToolRegistry } from "../../tools.js";
20
+ import { createToolRegistry, selectRunTools } from "../../tools.js";
20
21
  import { bridgeAbort, createUsageAccumulator, inputToMessages, isDurableLoop, isSteerSoftInterrupt, mergeGuardrails, throwIfAborted, } from "../helpers.js";
21
22
  import { cleanupRun, persistDurable, persistSucceeded, suspendDurable } from "./persist.js";
22
23
  import { generateWithRetry, recordProviderUsage } from "./provider-round.js";
@@ -61,7 +62,12 @@ async function assembleRoundContext(params) {
61
62
  };
62
63
  await session.activeLedger?.appendRun(redactRunLedgerRecord(startRecord, session.activeRedactor));
63
64
  await session.rebuildHistory();
64
- const { registry: baseRegistry, tools: activeToolList } = activeTools(session.agent.config.tools);
65
+ const { tools: listed } = activeTools(session.agent.config.tools);
66
+ const selected = selectRunTools(listed, options.toolNames, resumed?.state?.toolNames);
67
+ session.activeToolNames = selected.grant;
68
+ // Run-local snapshot: concurrent runs and MCP refresh must not mutate this registry.
69
+ const activeToolList = selected.tools;
70
+ const baseRegistry = createToolRegistry(activeToolList);
65
71
  const toolsDisclosure = resolveToolsDisclosure(options.toolsDisclosure, session.agent.config.toolsDisclosure);
66
72
  const toolSearch = toolsDisclosure === "search" && activeToolList.length > 0
67
73
  ? {
@@ -115,6 +121,26 @@ async function assembleRoundContext(params) {
115
121
  const providerOptions = resolveRunProviderOptions(options, session.agent.config);
116
122
  assertStructuredOutputRequestSupported(options.model ?? session.agent.config.model, providerOptions);
117
123
  const validate = options.validate ?? session.agent.config.validator;
124
+ // Resolved once per run, before any provider turn: a bad setting or a widening run overlay
125
+ // fails here rather than on the turn that happens to cross the ratio (plan 074 C12).
126
+ const attentionCompiler = resolveRunAttentionCompiler(session.agent.config.attentionCompiler, options.attentionCompiler, options.model ?? session.agent.config.model);
127
+ // Telemetry seam (plan 074 T6): one `attention_compiled` per mutated turn, counts and the
128
+ // measured ratio inputs only. Under-ratio turns and compiler-off runs emit nothing.
129
+ const onAttentionReport = attentionCompiler
130
+ ? (report) => session.emit({
131
+ type: "attention_compiled",
132
+ sessionId: session.id,
133
+ runId,
134
+ used: report.used,
135
+ usedAfter: report.usedAfter,
136
+ inputCap: report.inputCap,
137
+ triggerRatio: report.triggerRatio,
138
+ droppedThinkingTurns: report.droppedThinkingTurns,
139
+ stubbedToolResults: report.stubbedToolResults,
140
+ stubbedBytes: report.stubbedBytes,
141
+ truncated: report.truncated,
142
+ })
143
+ : undefined;
118
144
  const instructionInjectors = options.instructionInjectors ?? session.agent.config.instructionInjectors ?? [];
119
145
  const inputLayout = options.inputLayout ?? session.agent.config.inputLayout;
120
146
  const loop = resolveLoop(options, session.agent.config);
@@ -183,6 +209,11 @@ async function assembleRoundContext(params) {
183
209
  toolsSearch: session.agent.config.toolsSearch,
184
210
  activatedTools: session.activatedTools,
185
211
  toolResultFold: resolveToolResultFold(options.toolResultFold, session.agent.config.toolResultFold),
212
+ attentionCompiler,
213
+ // Session-owned: a stub made earlier stays applied even on a later under-ratio turn, so
214
+ // the prompt-cache prefix is not rewritten (C10). Undefined when the compiler is off.
215
+ attentionSticky: attentionCompiler ? session.attentionStickyFor() : undefined,
216
+ onAttentionReport,
186
217
  loadedSkills: session.loadedSkills,
187
218
  tools,
188
219
  resourceLoader: session.agent.config.resourceLoader,
@@ -9,11 +9,14 @@ export async function persistDurable(session, state) {
9
9
  const durable = session.activeDurable;
10
10
  if (!durable)
11
11
  throw new AgentRunStateError("Durable run state is not configured");
12
+ const withGrant = session.activeToolNames !== undefined ? { ...state, toolNames: session.activeToolNames } : state;
13
+ const attentionSticky = session.serializedAttentionSticky();
12
14
  const persisted = durable.options.persistSessionState
13
15
  ? {
14
- ...state,
16
+ ...withGrant,
15
17
  sessionState: {
16
18
  loadedSkillNames: session.loadedSkills.list(),
19
+ ...(attentionSticky ? { attentionSticky } : {}),
17
20
  ...(session.activatedTools.list().length ? { activatedToolNames: session.activatedTools.list() } : {}),
18
21
  ...(durable.options.includeSkillBodies
19
22
  ? {
@@ -24,7 +27,7 @@ export async function persistDurable(session, state) {
24
27
  : {}),
25
28
  },
26
29
  }
27
- : state;
30
+ : withGrant;
28
31
  const saved = await saveAgentRunState({
29
32
  checkpoints: durable.options.checkpoints,
30
33
  state: persisted,
@@ -155,6 +158,7 @@ export async function cleanupRun(input) {
155
158
  session.activeMetadata = undefined;
156
159
  session.activePromptVersion = undefined;
157
160
  session.activeLimits?.dispose();
161
+ session.activeToolNames = undefined;
158
162
  session.activeLimits = undefined;
159
163
  session.activeLimitOutputBuffer = false;
160
164
  session.activeRedactor = undefined;
@@ -278,6 +278,7 @@ export function bindDispatchToolCall(ctx) {
278
278
  ownership: ctx.session.activeOwnership,
279
279
  identity: ctx.session.activeIdentity,
280
280
  guardrails: ctx.session.activeGuardrails,
281
+ ...(ctx.tools.length > 0 ? { filter: { allow: ctx.tools.map((tool) => tool.name) } } : {}),
281
282
  limitTracker: ctx.limits,
282
283
  beforeExecute: async (mediatedCall) => {
283
284
  const durable = ctx.session.activeDurable;
@@ -1,6 +1,7 @@
1
1
  /** Shared host/round types for runInternal phase split (plan 059). Internal only. */
2
2
  import type { ActiveDurableRun } from "../../agent-approval.js";
3
3
  import type { PendingToolCall } from "../../agent-run-state.js";
4
+ import type { AttentionStickyFrontier, PersistedAttentionStickyFrontier } from "../../attention-compiler.js";
4
5
  import type { Agent, AgentEvent, AgentLoopStrategy, AgentRunResult, AIProvider, ErrorInfo, Guardrails, LoopContext, Message, ModelConfig, OwnershipScope, PendingDecision, PromptVersionRef, ProviderRequest, RunLedger, RunOptions, SessionEntry, SessionStore, Skill, ToolDefinition, ToolEffectStore, ToolRegistry, Usage } from "../../contracts.js";
5
6
  import type { AgentIdentity } from "../../identity.js";
6
7
  import type { AgentInput } from "../../input.js";
@@ -47,6 +48,15 @@ export type SessionHost = {
47
48
  readonly activatedTools: ActiveToolSet;
48
49
  restoredSkillBodies: readonly LoadedSkillBodiesEntry[];
49
50
  activeRunSkills: readonly Skill[];
51
+ /** Names-only grant for this run; undefined means the full registered set. */
52
+ activeToolNames?: readonly string[];
53
+ /** Sticky mutation frontier for this session (plan 074 C10); session-owned so it survives
54
+ * across turns, runs, and provider rounds. Lazily created on first use. */
55
+ attentionStickyFor(): AttentionStickyFrontier;
56
+ /** Plan 074 P3: bounded frontier snapshot for durable checkpoints (undefined before any mutation). */
57
+ serializedAttentionSticky(): PersistedAttentionStickyFrontier | undefined;
58
+ /** Plan 074 P3: restore a frontier that was validated when the checkpoint was loaded. */
59
+ restoreAttentionSticky(persisted: PersistedAttentionStickyFrontier): void;
50
60
  invalidateSnapshot(): void;
51
61
  resolveRunProvider(options: RunOptions): void;
52
62
  emit(event: AgentEvent): void;
@@ -1,6 +1,7 @@
1
1
  /** session (0.2.5 plan 025 Task 1 split). Moved verbatim from agent-session.ts; public surface unchanged behind the barrel. */
2
2
  import { ActiveDurableRun } from "../agent-approval.js";
3
3
  import type { PendingToolCall, StoredAgentRunState } from "../agent-run-state.js";
4
+ import { type AttentionStickyFrontier, type PersistedAttentionStickyFrontier } from "../attention-compiler.js";
4
5
  import type { Agent, AgentEvent, AgentRunResult, AgentRunState, AgentRunStateOptions, AgentSession, AgentSessionConfig, AIProvider, CompactionOptions, CompactionResult, ErrorInfo, Message, OwnershipScope, PendingDecision, PromptVersionRef, ProviderRequest, RunDecision, RunOptions, SessionEntry, Skill, SteerOptions, SubscribeOptions, ToolDefinition, ToolEffectStore, Usage } from "../contracts.js";
5
6
  import type { AgentIdentity } from "../identity.js";
6
7
  import type { AgentInput } from "../input.js";
@@ -48,6 +49,19 @@ export declare class RuntimeAgentSession implements AgentSession {
48
49
  restoredSkillBodies: readonly LoadedSkillBodiesEntry[];
49
50
  /** Skills of the current run (for the bodies snapshot); replaced at each run start. */
50
51
  activeRunSkills: readonly import("../contracts.js").Skill[];
52
+ /** Per-run tool allow-list (Task 21); undefined means the full registered set. */
53
+ activeToolNames?: readonly string[];
54
+ /** Sticky frontier for this session (plan 074 C10); created on first use, so a session whose
55
+ * agents never enable the compiler allocates nothing. Mutations stay applied once made, so a
56
+ * later under-ratio turn re-applies them instead of rewriting the prompt-cache prefix. */
57
+ private attentionSticky?;
58
+ attentionStickyFor(): AttentionStickyFrontier;
59
+ /** Plan 074 P3: bounded snapshot for a durable checkpoint; `undefined` when the session never
60
+ * mutated anything, so a compiler-off (or never-over-ratio) session persists nothing extra. */
61
+ serializedAttentionSticky(): PersistedAttentionStickyFrontier | undefined;
62
+ /** Plan 074 P3: restore a frontier validated at checkpoint load, so a resumed run keeps its
63
+ * stubs instead of re-deciding its first turn from the ratio. */
64
+ restoreAttentionSticky(persisted: PersistedAttentionStickyFrontier): void;
51
65
  /** Plan 015 Task 4: re-add persisted loaded-skill names (names only; bodies re-resolve on demand). */
52
66
  restoreLoadedSkills(names: readonly string[]): void;
53
67
  /** Plan 041: re-add persisted activated-tool names (names only; inert for absent tools). */
@@ -1,7 +1,9 @@
1
1
  /** session (0.2.5 plan 025 Task 1 split). Moved verbatim from agent-session.ts; public surface unchanged behind the barrel. */
2
2
  import { policyList } from "../agent-tool-dispatch.js";
3
+ import { createAttentionStickyFrontier, resolveInputCap, restoreAttentionStickyFrontier, serializeAttentionStickyFrontier, } from "../attention-compiler.js";
3
4
  import { createDefaultCompactionStrategy, isCompactionEntryData } from "../compaction.js";
4
- import { DEFAULT_MAX_PENDING_STEER_BYTES, DEFAULT_MAX_PENDING_STEERS, DEFAULT_SNAPSHOT_CACHE_TTL_MS, HARD_MAX_SNAPSHOT_CACHE_TTL_MS, } from "../contracts.js";
5
+ import { estimateAssemblyTokens, estimateTextTokens } from "../context-budget.js";
6
+ import { DEFAULT_MAX_PENDING_STEER_BYTES, DEFAULT_MAX_PENDING_STEERS, DEFAULT_SNAPSHOT_CACHE_TTL_MS, HARD_MAX_SNAPSHOT_CACHE_TTL_MS, resolveShouldCompact, } from "../contracts.js";
5
7
  import { GuardrailError, runGuardrails } from "../guardrails.js";
6
8
  import { applyDefaultProviderRequestOptions, createProviderRequestPolicyChain, normalizeProviderRequestPolicyResult, } from "../provider-request-policy.js";
7
9
  import { redactAgentEvent, redactProviderRequest, redactRunLedgerRecord, redactSecrets, redactSessionEntry } from "../redaction.js";
@@ -53,6 +55,28 @@ export class RuntimeAgentSession {
53
55
  restoredSkillBodies = [];
54
56
  /** Skills of the current run (for the bodies snapshot); replaced at each run start. */
55
57
  activeRunSkills = [];
58
+ /** Per-run tool allow-list (Task 21); undefined means the full registered set. */
59
+ activeToolNames;
60
+ /** Sticky frontier for this session (plan 074 C10); created on first use, so a session whose
61
+ * agents never enable the compiler allocates nothing. Mutations stay applied once made, so a
62
+ * later under-ratio turn re-applies them instead of rewriting the prompt-cache prefix. */
63
+ attentionSticky;
64
+ attentionStickyFor() {
65
+ this.attentionSticky ??= createAttentionStickyFrontier();
66
+ return this.attentionSticky;
67
+ }
68
+ /** Plan 074 P3: bounded snapshot for a durable checkpoint; `undefined` when the session never
69
+ * mutated anything, so a compiler-off (or never-over-ratio) session persists nothing extra. */
70
+ serializedAttentionSticky() {
71
+ return this.attentionSticky && this.attentionSticky.thinking.size + this.attentionSticky.toolCallIds.size > 0
72
+ ? serializeAttentionStickyFrontier(this.attentionSticky)
73
+ : undefined;
74
+ }
75
+ /** Plan 074 P3: restore a frontier validated at checkpoint load, so a resumed run keeps its
76
+ * stubs instead of re-deciding its first turn from the ratio. */
77
+ restoreAttentionSticky(persisted) {
78
+ this.attentionSticky = restoreAttentionStickyFrontier(persisted);
79
+ }
56
80
  /** Plan 015 Task 4: re-add persisted loaded-skill names (names only; bodies re-resolve on demand). */
57
81
  restoreLoadedSkills(names) {
58
82
  for (const name of names)
@@ -393,10 +417,23 @@ export class RuntimeAgentSession {
393
417
  }
394
418
  async autoCompact(runId, options, signal, inputMessages) {
395
419
  const compaction = mergeCompaction(this.agent.config.compaction, options.compaction);
396
- if (!compaction || compaction.thresholdEntries === undefined)
420
+ if (!compaction || (compaction.trigger === undefined && compaction.thresholdEntries === undefined))
397
421
  return;
398
422
  const snapshot = await this.snapshot();
399
- if (snapshot.entries.length <= compaction.thresholdEntries || snapshot.entries.at(-1)?.kind === "compaction")
423
+ // A branch that just compacted keeps its fresh summary: no second pass over the same entries.
424
+ if (snapshot.entries.at(-1)?.kind === "compaction")
425
+ return;
426
+ const shouldCompact = await resolveShouldCompact({ trigger: compaction.trigger, thresholdEntries: compaction.thresholdEntries }, {
427
+ sessionId: this.id,
428
+ entryCount: snapshot.entries.length,
429
+ // Estimates of the branch the run is about to send: messages plus carried summaries.
430
+ estimateInputTokens: () => estimateAssemblyTokens(snapshot.messages) + snapshot.summaries.reduce((sum, summary) => sum + estimateTextTokens(summary), 0),
431
+ // Same cap helper the attention compiler resolves its `inputCap` with.
432
+ resolveInputCapTokens: () => resolveInputCap(undefined, options.model ?? this.agent.config.model),
433
+ metadata: compaction.metadata,
434
+ signal,
435
+ });
436
+ if (!shouldCompact)
400
437
  return;
401
438
  await this.compactBranch(compaction, runId, signal, "auto");
402
439
  const compacted = await this.snapshot();
@@ -8,12 +8,26 @@ import type { OwnershipScope } from "./contracts.js";
8
8
  export type ArtifactApprovalState = "pending" | "approved" | "rejected";
9
9
  /** A resolved decision on one revision (pending is the absence of a decision). */
10
10
  export type ArtifactDecisionState = Exclude<ArtifactApprovalState, "pending">;
11
+ /** Optional host semantic verdict. Never treated as citation integrity or proof. */
12
+ export type CitationSupport = "unverified" | "supported" | "unsupported" | "uncertain";
11
13
  /** Bounded citation / data-source reference. Host resolves the body; Prism stores the ref only. */
12
14
  export interface ArtifactCitation {
13
15
  readonly uri: string;
14
16
  readonly title?: string;
15
- /** Data-source kind (e.g. "web", "database", "upload"); host-defined, bounded. */
17
+ /** Data-source kind (e.g. "web", "database", "upload", "rag"); host-defined, bounded. */
16
18
  readonly kind?: string;
19
+ readonly sourceId?: string;
20
+ readonly revision?: string;
21
+ /** SHA-256 hex of the retrieved source snapshot (optional `sha256:` prefix). */
22
+ readonly contentHash?: string;
23
+ readonly retrievedAt?: string;
24
+ readonly excerpt?: string;
25
+ readonly span?: {
26
+ readonly start: number;
27
+ readonly end: number;
28
+ };
29
+ readonly tenantId?: string;
30
+ readonly support?: CitationSupport;
17
31
  }
18
32
  /** One immutable revision of an artifact. `uri`/`hash` reference host-owned content. */
19
33
  export interface ArtifactRevision {
@@ -43,6 +57,8 @@ export interface ArtifactApproval {
43
57
  /** Change-request / rejection note. */
44
58
  readonly note?: string;
45
59
  readonly decidedAt: string;
60
+ /** SHA-256 of bound citation sourceId/revision/contentHash tuples at decision time. */
61
+ readonly evidenceDigest?: string;
46
62
  }
47
63
  /**
48
64
  * Durable artifact record. Stored as a versioned checkpoint value; the checkpoint version
@@ -130,3 +146,25 @@ export declare class ArtifactError extends Error {
130
146
  export declare function artifactCheckpointKey(threadId: string, artifactId: string): string;
131
147
  /** Current review state: the decision on the latest revision, or pending when undecided. */
132
148
  export declare function artifactApprovalState(record: ArtifactRecord): ArtifactApprovalState;
149
+ export declare const HARD_CITATION_EXCERPT_BYTES = 8192;
150
+ export type CitationIntegrityReason = "ok" | "missing_source" | "hash_mismatch" | "span_mismatch" | "revoked_acl" | "revision_changed" | "excerpt_too_large" | "cross_tenant";
151
+ export interface CitationLiveSource {
152
+ readonly contentHash: string;
153
+ readonly revision: string;
154
+ readonly body?: string;
155
+ readonly tenantId?: string;
156
+ readonly authorized?: boolean;
157
+ }
158
+ export interface CitationIntegrityResult {
159
+ readonly ok: boolean;
160
+ readonly reason: CitationIntegrityReason;
161
+ }
162
+ /** Deterministic source existence / hash / span / ACL check. Ignores `support`. */
163
+ export declare function checkCitationIntegrity(citation: ArtifactCitation, live?: CitationLiveSource, options?: {
164
+ readonly boundRevision?: string;
165
+ readonly maxExcerptBytes?: number;
166
+ }): CitationIntegrityResult;
167
+ /** Stable digest of citation identity tuples. Source body changes after approval fail this digest only when citations themselves change; live hash is `checkCitationIntegrity`. */
168
+ export declare function citationBindingDigest(citations: readonly ArtifactCitation[] | undefined): string;
169
+ /** True when the approval digest still matches the revision and (if given) live sources pass integrity. */
170
+ export declare function approvalEvidenceIntact(approval: ArtifactApproval, revision: ArtifactRevision, liveSources?: Readonly<Record<string, CitationLiveSource>>): CitationIntegrityResult;
package/dist/artifacts.js CHANGED
@@ -1,3 +1,4 @@
1
+ import { createHash } from "node:crypto";
1
2
  /** Well-known error codes for ArtifactBodyStore failures. */
2
3
  export const ARTIFACT_BODY_ERROR_CODES = {
3
4
  OWNERSHIP: "ERR_PRISM_ARTIFACT_BODY_OWNERSHIP",
@@ -41,4 +42,76 @@ export function artifactApprovalState(record) {
41
42
  const decision = record.approvals.find((approval) => approval.version === latest.version);
42
43
  return decision?.state ?? "pending";
43
44
  }
45
+ export const HARD_CITATION_EXCERPT_BYTES = 8192;
46
+ function normalizeCitationHash(value) {
47
+ const raw = value.startsWith("sha256:") ? value.slice("sha256:".length) : value;
48
+ return raw.trim().toLowerCase();
49
+ }
50
+ /** Deterministic source existence / hash / span / ACL check. Ignores `support`. */
51
+ export function checkCitationIntegrity(citation, live, options) {
52
+ const maxExcerpt = options?.maxExcerptBytes ?? HARD_CITATION_EXCERPT_BYTES;
53
+ if (citation.excerpt !== undefined && Buffer.byteLength(citation.excerpt, "utf8") > maxExcerpt) {
54
+ return { ok: false, reason: "excerpt_too_large" };
55
+ }
56
+ if (!live)
57
+ return { ok: false, reason: "missing_source" };
58
+ if (live.authorized === false)
59
+ return { ok: false, reason: "revoked_acl" };
60
+ if (citation.tenantId && live.tenantId && citation.tenantId !== live.tenantId) {
61
+ return { ok: false, reason: "cross_tenant" };
62
+ }
63
+ if (!citation.contentHash)
64
+ return { ok: false, reason: "missing_source" };
65
+ if (normalizeCitationHash(citation.contentHash) !== normalizeCitationHash(live.contentHash)) {
66
+ return { ok: false, reason: "hash_mismatch" };
67
+ }
68
+ if (citation.revision !== undefined && citation.revision !== live.revision) {
69
+ return { ok: false, reason: "revision_changed" };
70
+ }
71
+ if (options?.boundRevision !== undefined && options.boundRevision !== live.revision) {
72
+ return { ok: false, reason: "revision_changed" };
73
+ }
74
+ if (citation.span) {
75
+ const { start, end } = citation.span;
76
+ if (!Number.isSafeInteger(start) || !Number.isSafeInteger(end) || start < 0 || end <= start) {
77
+ return { ok: false, reason: "span_mismatch" };
78
+ }
79
+ if (live.body !== undefined) {
80
+ const sliced = live.body.slice(start, end);
81
+ if (citation.excerpt !== undefined && sliced !== citation.excerpt) {
82
+ return { ok: false, reason: "span_mismatch" };
83
+ }
84
+ }
85
+ }
86
+ else if (citation.excerpt !== undefined && live.body !== undefined && citation.excerpt !== live.body) {
87
+ return { ok: false, reason: "span_mismatch" };
88
+ }
89
+ return { ok: true, reason: "ok" };
90
+ }
91
+ /** Stable digest of citation identity tuples. Source body changes after approval fail this digest only when citations themselves change; live hash is `checkCitationIntegrity`. */
92
+ export function citationBindingDigest(citations) {
93
+ const rows = (citations ?? [])
94
+ .map((citation) => `${citation.sourceId ?? ""}|${citation.revision ?? ""}|${citation.contentHash ? normalizeCitationHash(citation.contentHash) : ""}`)
95
+ .sort();
96
+ return createHash("sha256").update(JSON.stringify(rows)).digest("hex");
97
+ }
98
+ /** True when the approval digest still matches the revision and (if given) live sources pass integrity. */
99
+ export function approvalEvidenceIntact(approval, revision, liveSources) {
100
+ if (approval.evidenceDigest !== undefined && approval.evidenceDigest !== citationBindingDigest(revision.citations)) {
101
+ return { ok: false, reason: "revision_changed" };
102
+ }
103
+ if (liveSources === undefined)
104
+ return { ok: true, reason: "ok" };
105
+ for (const citation of revision.citations ?? []) {
106
+ if (!citation.sourceId && !citation.contentHash)
107
+ continue;
108
+ const live = citation.sourceId ? liveSources[citation.sourceId] : undefined;
109
+ const result = checkCitationIntegrity(citation, live, {
110
+ ...(citation.revision === undefined ? {} : { boundRevision: citation.revision }),
111
+ });
112
+ if (!result.ok)
113
+ return result;
114
+ }
115
+ return { ok: true, reason: "ok" };
116
+ }
44
117
  //# sourceMappingURL=artifacts.js.map