@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.
- package/CHANGELOG.md +44 -5
- package/README.md +10 -10
- package/dist/agent-definitions.js +1 -0
- package/dist/agent-run-lifecycle.js +11 -0
- package/dist/agent-run-state.d.ts +6 -0
- package/dist/agent-run-state.js +29 -9
- package/dist/agent-session/session/assemble.js +33 -2
- package/dist/agent-session/session/persist.js +6 -2
- package/dist/agent-session/session/tool-round.js +1 -0
- package/dist/agent-session/session/types.d.ts +10 -0
- package/dist/agent-session/session.d.ts +14 -0
- package/dist/agent-session/session.js +40 -3
- package/dist/artifacts.d.ts +39 -1
- package/dist/artifacts.js +73 -0
- package/dist/attention-compiler.d.ts +121 -0
- package/dist/attention-compiler.js +479 -0
- package/dist/cli-init.js +20 -6
- package/dist/context-budget.d.ts +20 -1
- package/dist/context-budget.js +10 -1
- package/dist/contracts-core/agent.d.ts +7 -0
- package/dist/contracts-core/attention.d.ts +66 -0
- package/dist/contracts-core/attention.js +2 -0
- package/dist/contracts-core/compaction.d.ts +59 -0
- package/dist/contracts-core/compaction.js +77 -1
- package/dist/contracts-core/provider.d.ts +4 -0
- package/dist/contracts-core.d.ts +1 -0
- package/dist/contracts-core.js +1 -0
- package/dist/contracts-protocol.d.ts +29 -0
- package/dist/contracts-run-state.d.ts +6 -0
- package/dist/host-composition.d.ts +78 -0
- package/dist/host-composition.js +248 -0
- package/dist/index.d.ts +9 -6
- package/dist/index.js +5 -4
- package/dist/input.d.ts +13 -1
- package/dist/input.js +40 -1
- package/dist/secure-agent.d.ts +2 -0
- package/dist/secure-agent.js +6 -1
- package/dist/tool-result-fold.d.ts +12 -0
- package/dist/tool-result-fold.js +13 -6
- package/dist/tools.d.ts +10 -0
- package/dist/tools.js +41 -0
- package/docs/acp-agent.md +42 -11
- package/docs/acp.md +2 -1
- package/docs/ag-ui.md +5 -3
- package/docs/agent-definitions.md +9 -1
- package/docs/agent-events.md +4 -1
- package/docs/agent-session-runtime.md +6 -6
- package/docs/attention-compiler.md +272 -0
- package/docs/cli-rpc.md +4 -2
- package/docs/coding-agent-tools.md +1 -1
- package/docs/coding-security.md +5 -3
- package/docs/coding-tools.md +1 -1
- package/docs/coding-workspaces.md +22 -0
- package/docs/compaction-and-retry.md +36 -4
- package/docs/compaction-observational-memory.md +62 -9
- package/docs/context-and-skills.md +4 -2
- package/docs/conversations.md +1 -1
- package/docs/dev-inspector.md +4 -0
- package/docs/device-adapters.md +1 -0
- package/docs/document-reader.md +11 -3
- package/docs/documents.md +10 -2
- package/docs/enterprise-postgres-state.md +2 -2
- package/docs/evaluations.md +168 -4
- package/docs/execution-timeline.md +180 -0
- package/docs/history/0.7.0-primitive-review.md +254 -0
- package/docs/history/migration-0.0.md +2 -2
- package/docs/history/release-handoffs.md +37 -1
- package/docs/host-compositions.md +147 -0
- package/docs/hosted-sandboxes.md +94 -0
- package/docs/index.md +58 -39
- package/docs/input-and-prompt-assembly.md +1 -0
- package/docs/knowledge-sync.md +84 -0
- package/docs/language-intelligence.md +1 -1
- package/docs/live-testing.md +4 -1
- package/docs/mcp-tools.md +2 -1
- package/docs/memory-fabric.md +416 -0
- package/docs/migrate-to-0.5.md +1 -1
- package/docs/migrate-to-0.6.md +1 -0
- package/docs/migrate-to-0.7.md +345 -0
- package/docs/migration.md +13 -1
- package/docs/model-routing.md +79 -4
- package/docs/multi-agent-patterns.md +20 -6
- package/docs/observability.md +52 -1
- package/docs/operations.md +13 -1
- package/docs/options-index.md +13 -1
- package/docs/peer-dependencies.md +6 -4
- package/docs/process-sessions.md +3 -1
- package/docs/prompt-registry.md +1 -1
- package/docs/provider-caching.md +4 -2
- package/docs/provider-conformance.md +1 -1
- package/docs/provider-packages.md +22 -22
- package/docs/providers/bedrock.md +71 -7
- package/docs/providers/openai.md +1 -1
- package/docs/rag.md +24 -8
- package/docs/realtime-voice.md +87 -0
- package/docs/release-and-install.md +36 -34
- package/docs/runs-and-usage.md +3 -2
- package/docs/server.md +5 -3
- package/docs/speech.md +2 -0
- package/docs/supervisors.md +33 -5
- package/docs/testing.md +1 -1
- package/docs/thinking-and-reasoning.md +3 -1
- package/docs/tools.md +6 -5
- package/docs/web-tools.md +2 -1
- package/docs/work-artifacts-and-review.md +14 -4
- package/docs/work-connectors.md +3 -1
- package/docs/work-tools.md +14 -4
- package/docs/workflows.md +69 -1
- package/docs/working-and-semantic-memory.md +25 -14
- package/package.json +1 -1
- package/templates/README.md +2 -0
- package/templates/business-worker/README.md.tmpl +19 -0
- package/templates/business-worker/env.example.tmpl +1 -0
- package/templates/business-worker/gitignore.tmpl +11 -0
- package/templates/business-worker/manifest.json +11 -0
- package/templates/business-worker/package.json.tmpl +23 -0
- package/templates/business-worker/src/agent.ts.tmpl +92 -0
- package/templates/business-worker/src/index.ts.tmpl +13 -0
- package/templates/business-worker/src/tests/agent.test.ts.tmpl +77 -0
- package/templates/business-worker/tsconfig.json.tmpl +15 -0
- package/templates/personal-assistant/README.md.tmpl +18 -0
- package/templates/personal-assistant/env.example.tmpl +1 -0
- package/templates/personal-assistant/gitignore.tmpl +11 -0
- package/templates/personal-assistant/manifest.json +11 -0
- package/templates/personal-assistant/package.json.tmpl +23 -0
- package/templates/personal-assistant/src/agent.ts.tmpl +65 -0
- package/templates/personal-assistant/src/index.ts.tmpl +13 -0
- package/templates/personal-assistant/src/tests/agent.test.ts.tmpl +28 -0
- package/templates/personal-assistant/tsconfig.json.tmpl +15 -0
package/docs/acp-agent.md
CHANGED
|
@@ -24,22 +24,36 @@ The config file is the trust boundary: unknown keys are rejected (a typo cannot
|
|
|
24
24
|
| --- | --- | --- |
|
|
25
25
|
| `userId` | yes | Ownership user id for every session (single-local-user `authorize`). |
|
|
26
26
|
| `cwd` | yes | Workspace root the coding tools are bound to (must be an existing directory). Sessions always operate on this root — a client-supplied `cwd` never moves the tools. |
|
|
27
|
-
| `
|
|
28
|
-
| `
|
|
27
|
+
| `model` | yes* | Model selection `{ "provider": "<name>", "model": "<id>" }`. Supported providers: `openai`, `anthropic`, `google`, `deepseek`, `openrouter`, `ollama`, `xai`, `zai`, `alibaba`, `kimi`, `clinepass`, `commandcode`, `neuralwatt`, `opencode-go`, `hyper`, and `mock`. *Required unless `provider` is passed programmatically. |
|
|
28
|
+
| `credentialRef` | yes* | Reference (env var name or host secret identifier) used to resolve API credentials. *Required when using a non-mock provider without an injected provider instance. |
|
|
29
|
+
| `sessionStore` | no | `{ "type": "sqlite", "path": ".prism/sessions.db" }` or `{ "type": "memory" }` (default). SQLite persists sessions, runs, checkpoints, and leases (`createSqlitePersistence`), and supports session agent reconstruction across restarts. |
|
|
30
|
+
| `mcp.allow` | no | MCP allow-list. http/sse servers must match an allow origin or path-segment subtree; stdio servers require the marker `"stdio"`. The UNSTABLE `acp` transport is never approved. |
|
|
29
31
|
| `modes` | no | Mode table `{ "modes": [{ "id", "name", "description?" }], "defaultModeId"? }`; ids unique, `defaultModeId` must name a mode. |
|
|
30
32
|
| `configOptions` | no | `{ "options": [{ "type": "boolean" \| "select", "id", "name", "defaultValue", ... }] }`; ids unique. Select options are advertised/settable per the B3 gate (see [acp.md](acp.md)). |
|
|
31
33
|
| `limits` | no | AG-UI/ACP caps passthrough (`AgUiLimitOptions`). |
|
|
32
34
|
|
|
33
|
-
|
|
35
|
+
Real-provider example:
|
|
34
36
|
|
|
35
37
|
```json
|
|
36
38
|
{
|
|
37
39
|
"userId": "local",
|
|
38
40
|
"cwd": ".",
|
|
41
|
+
"model": { "provider": "openai", "model": "gpt-4o" },
|
|
42
|
+
"credentialRef": "OPENAI_API_KEY",
|
|
39
43
|
"sessionStore": { "type": "sqlite", "path": ".prism/sessions.db" },
|
|
40
44
|
"mcp": { "allow": ["https://mcp.example.com"] },
|
|
41
45
|
"modes": { "modes": [{ "id": "edit", "name": "Edit" }], "defaultModeId": "edit" },
|
|
42
|
-
"configOptions": [{ "type": "boolean", "id": "verbose", "name": "Verbose", "defaultValue": false }]
|
|
46
|
+
"configOptions": { "options": [{ "type": "boolean", "id": "verbose", "name": "Verbose", "defaultValue": false }] }
|
|
47
|
+
}
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
Explicit offline mock mode example:
|
|
51
|
+
|
|
52
|
+
```json
|
|
53
|
+
{
|
|
54
|
+
"userId": "local",
|
|
55
|
+
"cwd": ".",
|
|
56
|
+
"model": { "provider": "mock", "model": "mock" }
|
|
43
57
|
}
|
|
44
58
|
```
|
|
45
59
|
|
|
@@ -48,31 +62,48 @@ Example:
|
|
|
48
62
|
The binary is pure wiring (~200 lines) — no protocol code lives here. It builds:
|
|
49
63
|
|
|
50
64
|
- `authorize` — single local user; every inbound call is scoped by session id.
|
|
51
|
-
- `sessionFactory` — real Prism sessions over `createAgent` with the nine coding tools (`createCodingTools(config.cwd)`), durable `runState` (`interruptBeforeTool`, checkpoints), ownership-scoped to `userId`.
|
|
52
|
-
- `lifecycle` — `createAgentRunLifecycle` over the same checkpoint store
|
|
53
|
-
- `mcp` — allow-list `select` gate with http/sse transports.
|
|
65
|
+
- `sessionFactory` — real Prism sessions over `createAgent` with the nine coding tools (`createCodingTools(config.cwd)`), durable `runState` (`interruptBeforeTool`, checkpoints), ownership-scoped to `userId`. When the client advertises filesystem capabilities, a per-session agent with client-backed buffer tools is constructed and bound to the session id.
|
|
66
|
+
- `lifecycle` — `createAgentRunLifecycle` over the same checkpoint store. Durable SQLite checkpoints enable interrupted runs and approval state to be reconstructed across restarts with the selected model and provider intact.
|
|
67
|
+
- `mcp` — allow-list `select` gate with origin- and path-segment subtree checking for http/sse transports.
|
|
54
68
|
- `modes` / `configOptions` — from config.
|
|
55
|
-
- Provider — **mock
|
|
69
|
+
- Provider — **fail closed before startup**. Unlike earlier releases where missing configuration silently defaulted to mock mode (Trap C), Prism 0.7.0 requires either an explicit `model` in config or an injected provider. For real providers, credentials are resolved lazily on demand via dynamic import of `@arnilo/prism-providers/<adapter>`. Offline mock mode must be explicitly specified (`model: { provider: "mock", model: "mock" }`).
|
|
70
|
+
|
|
71
|
+
Programmatic usage:
|
|
56
72
|
|
|
57
73
|
```ts
|
|
58
74
|
import { createSpawnableAgent, loadConfig } from "@arnilo/prism-acp-agent";
|
|
59
|
-
import { createOpenAIResponsesProvider } from "@arnilo/prism-providers/openai";
|
|
60
75
|
|
|
76
|
+
// Driven by config with optional custom credential resolver
|
|
61
77
|
const agent = createSpawnableAgent({
|
|
62
78
|
config: loadConfig("prism-acp-agent.json"),
|
|
63
|
-
|
|
79
|
+
credentialResolver: (ref) => process.env[ref],
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
// Or programmatic provider override (must match config.model.provider)
|
|
83
|
+
const customAgent = createSpawnableAgent({
|
|
84
|
+
config: loadConfig("prism-acp-agent.json"),
|
|
85
|
+
provider: customProvider,
|
|
64
86
|
});
|
|
65
87
|
```
|
|
66
88
|
|
|
67
89
|
## Library surface
|
|
68
90
|
|
|
69
91
|
- `loadConfig(path)` / `parseConfig(text, baseDir)` — read + validate; throw `ConfigError` (code `PRISM_ACP_AGENT_CONFIG`) with a clear message.
|
|
70
|
-
- `createSpawnableAgent({ config, provider? })` — build the ACP `AgentApp`.
|
|
92
|
+
- `createSpawnableAgent({ config, provider?, model?, credentialResolver? })` — build the ACP `AgentApp`.
|
|
71
93
|
- `selectMcpServers(allow, servers)` — the allow-list gate, exported for reuse in custom hosts.
|
|
94
|
+
- `SUPPORTED_PROVIDERS` — list of supported provider adapter identifiers.
|
|
72
95
|
|
|
73
96
|
## Security posture
|
|
74
97
|
|
|
75
98
|
- Config file = trust boundary: validated shape, no arbitrary code execution.
|
|
76
99
|
- MCP servers only from the allow-list; the UNSTABLE `acp` transport is never bridged.
|
|
100
|
+
- **Origin and path-segment destination matching**:
|
|
101
|
+
- Exact origin matching normalizes scheme, hostname (punycode IDN), and effective port (e.g. 443 on https). Lookalike hosts (`https://mcp.example.com.attacker.invalid`) are strictly rejected.
|
|
102
|
+
- Path matching enforces exact path or path-segment subtree: `https://mcp.example.com/mcp` admits `/mcp`, `/mcp/`, and `/mcp/sub`, but rejects `/mcp-other` and `/other`.
|
|
103
|
+
- Config entries with credentials (`user:pass@`), query parameters, fragment identifiers, or ambiguous path forms (`%2e%2e`, `%2f`, `%5c`, `..`, backslashes) fail validation.
|
|
104
|
+
- Candidate URLs embedding credentials or ambiguous encoded path forms fail closed at selection.
|
|
105
|
+
- Stdio servers require the explicit `"stdio"` marker; URL entries cannot authorize `stdio` processes, and `"stdio"` cannot authorize remote servers.
|
|
77
106
|
- Coding tools are bound to `config.cwd` only; session ownership is fixed to `userId`.
|
|
78
107
|
- Session store paths are resolved against the config directory and fail closed on invalid config.
|
|
108
|
+
- **Credential isolation**: Secret values never enter config persistence, argv flags, stdout protocol streams, events, or model context. Identity carries only the non-secret `credentialRef` name.
|
|
109
|
+
- **Trust model**: The ACP agent config is designed as a single-local-user trust boundary (workstation / editor agent), not a multi-tenant business boundary. Cross-tenant credential sharing or multi-user elevation must not be multiplexed through a single spawnable ACP agent process.
|
package/docs/acp.md
CHANGED
|
@@ -57,6 +57,7 @@ In-stream `SessionUpdate`s:
|
|
|
57
57
|
| `permission_denied` lifecycle | `tool_call_update` status `failed` (never raw args; synthesized id `prism:denied:<approvalId>` when no `toolCallId`) |
|
|
58
58
|
| `configuration_changed` lifecycle | `config_option_update` with the full current set, per streaming session |
|
|
59
59
|
| Plan lifecycle (F5, UNSTABLE-gated) | `plan_changed` → `plan_update` with `plan: { type: "items", planId = planPath, entries: [{ content, priority: "medium", status }] }` — the complete entry list per update (client replaces its plan wholesale); `plan_removed` → `plan_removed` with `planId = planPath`. Emitted only when the client advertised `ClientCapabilities.plan`; mapper stays capability-agnostic (gate in the agent wiring). Entries come from `writeCodingPlanFile`'s `onEvent` (parsed via `parseCodingPlanTodos`) or host-emitted through their `CodingLifecycleEmitter`; text passes the shared redactor and byte caps. |
|
|
60
|
+
| Subagent lifecycle | `subagent_started` / `subagent_stopped` → `agent_message_chunk` with only redacted child/delegation ids and terminal status. Wire `observeSupervisorLifecycle()` to emit them; child inputs, outputs, paths, and error text stay absent. |
|
|
60
61
|
| Session title (F6) | `sessions.title({ sessionId, prompt, signal })` resolves on `session/prompt`; a defined value differing from the last emitted title produces `session_info_update` with `{ sessionUpdate: "session_info_update", title }`. Best-effort: `undefined` or a throw means no title and no update (requests never fail on titles); the host owns title storage. Titles pass the shared redactor and are truncated at `maxTextBytes`/`maxEventBytes`. |
|
|
61
62
|
| Slash commands (F9) | `commands.list({ sessionId, signal })` on `session/new`/`load`/`resume` produces `available_commands_update` with `{ name, description, input?: { hint } }` (SDK `AvailableCommand`; description is required). Names/descriptions/hints pass the shared redactor and `maxTextBytes`; the list is sliced at `acpCommandsPerUpdate`. Best-effort: a throw or non-array omits the update (session start never fails on commands). |
|
|
62
63
|
| Session mode/config switch | `current_mode_update` / `config_option_update` |
|
|
@@ -115,7 +116,7 @@ const agent = createPrismAcpAgent({
|
|
|
115
116
|
- **Client fs/terminal are adapters, not a second implementation.** `AcpClientFilesystem` / `AcpClientTerminals` wrap the client's `fs/*` and `terminal/*` methods behind the Phase 9 `ProcessSession`-flavored interfaces; the agent pre-generates the session id so terminal requests can carry it. `createAcpFilesystemOperations` from `@arnilo/prism-coding-tools/agent` maps that filesystem seam onto the coding tools' `read`/`write`/`edit` operations. This editor-buffer mode is intentionally hybrid: `repo_list`, `repo_search`, `glob`, `delete`, and `move` remain disk-backed unless the host supplies separate operations; binary/image/document handling never falls back to local disk. Host repo operations remain default when the client fs is absent.
|
|
116
117
|
- **Spawnable ACP coding registry (Task 6).** `@arnilo/prism-acp-agent` wires `createAcpClientFilesystem` and creates a separate coding tool registry per ACP session when the client advertises `fs/read_text_file` or `fs/write_text_file`. That session's `read`/`write`/`edit` operations use editor buffers; without fs advertisement, the existing disk registry is used. `shell`, repository search/list/glob, `delete`, and `move` remain disk-backed in this hybrid mode. Durable approvals resolve the same per-session agent, so one session cannot resume through another session's buffer adapter.
|
|
117
118
|
- **Modes and config options are a pure host overlay.** The agent stores only a thin per-session registry; `apply`/`onChange` hooks narrow the host's own behavior. Mode switches can narrow or host-authorized widen — never a parallel policy evaluator, never a client-enabled tool.
|
|
118
|
-
- **Lifecycle wiring.** Pass your `createCodingLifecycleEmitter()` as `coding.lifecycle`; `file_changed` etc. then flow to streaming sessions. `configuration_changed` broadcasts `config_option_update` (agent-message fallback if the SDK rejects the kind).
|
|
119
|
+
- **Lifecycle wiring.** Pass your `createCodingLifecycleEmitter()` as `coding.lifecycle`; `file_changed` etc. then flow to streaming sessions. Call `observeSupervisorLifecycle(supervisor, { onEvent: lifecycle.emit })` for redacted child start/stop milestones. `configuration_changed` broadcasts `config_option_update` (agent-message fallback if the SDK rejects the kind).
|
|
119
120
|
- **Stream budgets.** Every lifecycle update counts against the same per-run stream event/byte budget as prompt updates; overflowing closes the update, never the run.
|
|
120
121
|
|
|
121
122
|
### Persistence and ownership
|
package/docs/ag-ui.md
CHANGED
|
@@ -48,9 +48,9 @@ The handler accepts only `POST` JSON validated with official AG-UI `RunAgentInpu
|
|
|
48
48
|
|
|
49
49
|
The handler returns `text/event-stream`, one `data: <AG-UI event>\n\n` frame per output. Mapper lifecycle is ordered: `RUN_*`, `STEP_*`, `TEXT_MESSAGE_*`, and `TOOL_CALL_*` are deterministic Prism mappings. Host projectors may additionally prove and emit `STATE_SNAPSHOT`/`STATE_DELTA`, `MESSAGES_SNAPSHOT`, `ACTIVITY_*`, current `REASONING_*`, `RAW`, and named `CUSTOM` values.
|
|
50
50
|
|
|
51
|
-
`delegated_agent_step` maps by default to bounded `ACTIVITY_SNAPSHOT` metadata with activity type `prism.delegated_agent_step`; `includeCustomEvents: true` also emits `CUSTOM prism.delegated_agent_step`. The safe payload contains adapter/conversation identifiers, step index/state/kind, duration, token counts, tool/subagent names, and opaque detail references only. Normal assistant text remains `TEXT_MESSAGE_*`; delegated events never duplicate transcript text. Raw event bodies, tool arguments/results, paths, URIs, logs, and hidden thought text remain absent unless a host explicitly supplies a projection. All values revalidate against official `EventSchemas`; deprecated `THINKING_*` and convenience chunk events are not produced. Active message/tool/reasoning/step sequences close before error, interruption, or finish.
|
|
51
|
+
`delegated_agent_step` maps by default to bounded `ACTIVITY_SNAPSHOT` metadata with activity type `prism.delegated_agent_step`; `includeCustomEvents: true` also emits `CUSTOM prism.delegated_agent_step`. The safe payload contains adapter/conversation identifiers, step index/state/kind, duration, token counts, tool/subagent names, and opaque detail references only. Coding hosts can supply `observeSupervisorLifecycle(..., { delegatedAgentStep })` output here for supervisor child activity. Normal assistant text remains `TEXT_MESSAGE_*`; delegated events never duplicate transcript text. Raw event bodies, tool arguments/results, paths, URIs, logs, and hidden thought text remain absent unless a host explicitly supplies a projection. All values revalidate against official `EventSchemas`; deprecated `THINKING_*` and convenience chunk events are not produced. Active message/tool/reasoning/step sequences close before error, interruption, or finish.
|
|
52
52
|
|
|
53
|
-
A Prism durable `agent_suspended` returns `RUN_FINISHED` with core interrupt id `${runId}:${version}` and a strict `{ decision: "approve" | "deny" }` schema. `projection.interrupt` may attach bounded expiry/metadata or additional host policy interrupts but must retain that core id. Without `interrupts.resume`, one exact entry is required; `cancelled` means deny. An aggregate policy may validate bounded multiple entries, then returns one current-version core decision.
|
|
53
|
+
A Prism durable `agent_suspended` returns `RUN_FINISHED` with core interrupt id `${runId}:${version}` and a strict `{ decision: "approve" | "deny" }` schema (extended with `editedArgs`/`modifiedArguments`, `approvalId`, and `reason` when `capabilities.humanInTheLoop.approveWithEdits` is enabled). `projection.interrupt` may attach bounded expiry/metadata or additional host policy interrupts but must retain that core id. Without `interrupts.resume`, one exact entry is required; `cancelled` means deny. An aggregate policy may validate bounded multiple entries, then returns one current-version core decision. When `approveWithEdits` is configured, validated client edits map directly to core `RunDecision` with `outcome: "allow_once"` and `modifiedArguments`, executing revalidation against tool schemas under single atomic CAS. When `approveWithEdits` is omitted or false (default), payloads containing `editedArgs`/`args`/`modifiedArguments` safely fail closed and deny. The adapter checks host authorization, selected run, suspended status, and checkpoint version, then calls `AgentRunLifecycle.resumeStream()` once. Claimed/dispatched tools are never replayed.
|
|
54
54
|
|
|
55
55
|
`createPersistenceAgUiReplay()` remains a compatible page adapter. `createAgentEventSourceAgUiReplay()` resolves exact ownership/run once per open, then consumes the shared durable source through terminal or live follow; it never attaches replica-local `session.subscribe()`. Every record must already be redacted. Mapped events carry stable `prismEventId` and bounded opaque `prismCursor`; records with no standard mapping emit `CUSTOM prism.replay_cursor`, so clients can persist progress. Terminal replay never creates a session or reruns a provider/tool.
|
|
56
56
|
|
|
@@ -84,7 +84,9 @@ Co-work uses bounded, redacted `CUSTOM prism.cowork.*` events through `mapCoWork
|
|
|
84
84
|
|
|
85
85
|
## Request/response example
|
|
86
86
|
|
|
87
|
-
Resume a default single interrupt with `resume: [{ "interruptId": "run-1:4", "status": "resolved", "payload": { "decision": "approve" } }]`.
|
|
87
|
+
Resume a default single interrupt with `resume: [{ "interruptId": "run-1:4", "status": "resolved", "payload": { "decision": "approve" } }]`.
|
|
88
|
+
When `approveWithEdits` is advertised and configured, clients can approve with edited arguments: `resume: [{ "interruptId": "run-1:4", "status": "resolved", "payload": { "decision": "approve", "editedArgs": { "path": "approved.txt" } } }]` (or `modifiedArguments`). The edits map directly to core `RunDecision` (`outcome: "allow_once"`, `modifiedArguments`), validated against the tool schema under CAS `expectedVersion`.
|
|
89
|
+
Full history, client tool results, and mutable state need authorized `input.project` selection. This adapter is not a conversation database.
|
|
88
90
|
|
|
89
91
|
## Implementation example
|
|
90
92
|
|
|
@@ -30,11 +30,12 @@ Do not use the bundle loader to discover providers — provider/model packages s
|
|
|
30
30
|
| `model?` | `ModelConfig` object, or a `"<provider>/<model>"` string resolved through `registries.models`. Optional at authoring time: when omitted, resolution falls back to `context.overrides.model` (host-injected selection); an explicit definition `model` drives registry resolution, and neither present fails closed with `Agent "<name>" has no model`. |
|
|
31
31
|
| `tools?` | Tool names to activate from the active tool registry / `registries.tools`. Omitted means no active tools unless `activateAllCapabilities: true` is passed for migration. |
|
|
32
32
|
| `skills?` | Skill names resolved via `resolveActiveSkills()`; omitted means no active skills unless `activateAllCapabilities: true` is passed for migration. `toolNames` enforcement applies at activation. |
|
|
33
|
-
| `context?` | Context provider names from `registries.contextProviders`. |
|
|
33
|
+
| `context?` | Context provider names from `registries.contextProviders`. A host registers whichever providers it wants under its own names — for example a memory fabric's provider as `"memory-fabric"`; the definition contract itself carries no provider-specific field (see [Memory fabric](memory-fabric.md)). |
|
|
34
34
|
| `systemPrompt?` | `SystemPromptConfig` layer (see [System prompts](system-prompts.md)). |
|
|
35
35
|
| `instructions?` | Base prompt text. |
|
|
36
36
|
| `loop?` | `AgentLoopStrategy` or `AgentLoopOptions` (see [Agent loops](agent-loops.md)). |
|
|
37
37
|
| `metadata?` | Free-form metadata. |
|
|
38
|
+
| `attentionCompiler?` | Opt-in attention compiler for the resolved config: `true` for defaults, an `AttentionCompilerOptions` object to tune ratios/depth (see [Attention compiler](attention-compiler.md)). Copied verbatim onto `AgentConfig.attentionCompiler` and nothing else; `RunOptions.attentionCompiler` may later disable or relax it. |
|
|
38
39
|
| `create?(config?)` | Optional escape hatch. When present, overrides declarative resolution: the helper builds a base `AgentConfig` from the declarative fields, calls `create(config)`, then merges `context.overrides`. |
|
|
39
40
|
|
|
40
41
|
### `AgentDefinitionResolutionContext` (contract, `@arnilo/prism`)
|
|
@@ -116,6 +117,12 @@ instructions: You are a careful coding agent.
|
|
|
116
117
|
Prefer minimal diffs. Cite the file you changed.
|
|
117
118
|
```
|
|
118
119
|
|
|
120
|
+
`context` names are whatever the host registered in `registries.contextProviders` — for example
|
|
121
|
+
`registries.contextProviders.register("memory-fabric", fabric.createContextProvider())` makes
|
|
122
|
+
`context: [memory-fabric]` work, while an unregistered name fails closed at resolution. Nothing about
|
|
123
|
+
the fabric is required by `AgentDefinition`; a host that never registers a provider resolves the same
|
|
124
|
+
definition unchanged.
|
|
125
|
+
|
|
119
126
|
`discoverAgentBundles({ configRoot })` returns (paths only):
|
|
120
127
|
|
|
121
128
|
```json
|
|
@@ -244,6 +251,7 @@ Use `activateAllCapabilities: true` only while migrating old configs. It intenti
|
|
|
244
251
|
- [System prompts](system-prompts.md): `composeSystemPrompt` source ranks and the `AGENT.md` body / `SYSTEM.md` / `AGENTS.md` prompt layering reused by `resolveAgentBundle`.
|
|
245
252
|
- [Contribution discovery (workspace)](contribution-discovery.md): `discoverContributions` for repo `.agents/` contributions passed as `repoContributions`.
|
|
246
253
|
- [Context and skills](context-and-skills.md): `resolveActiveSkills` and `RunOptions.activeSkills` activation that consumes discovered skills.
|
|
254
|
+
- [Memory fabric](memory-fabric.md): optional `fabric.createContextProvider()` registered as a `context` name, and `fabric.attach(session)` gating its tools and workers.
|
|
247
255
|
- [Tools](tools.md): `ToolDefinition` / `(toolNames)` enforcement and host-owned tool execution.
|
|
248
256
|
- [Agent loops](agent-loops.md): `resolveLoop` and loop strategies passed via `loop` / `context.overrides`.
|
|
249
257
|
- [Extensions](extensions.md): `registerAgent()` programmatic registration of inert `AgentDefinition` values.
|
package/docs/agent-events.md
CHANGED
|
@@ -105,6 +105,8 @@ Agent / turn / message events:
|
|
|
105
105
|
|
|
106
106
|
Adapters should call `createDelegatedAgentStep({ sessionId, runId, adapterId, externalConversationId, stepIndex, state, kind, usage })` rather than forwarding external JSON. The constructor allow-lists fields and fails closed on malformed or oversized identifiers/counters.
|
|
107
107
|
|
|
108
|
+
Coding hosts call `observeSupervisorLifecycle(supervisor, { onEvent, delegatedAgentStep })` to turn supervisor milestones into `subagent_started` / `subagent_stopped` coding lifecycle events. Both carry only redacted `childId`, `delegationId`, and `depth`; stopped events add terminal `AgentRunStatus`. Supplying `delegatedAgentStep` emits the bounded `delegated_agent_step` records AG-UI already maps. Child inputs, outputs, paths, and delegation error text never cross either bridge.
|
|
109
|
+
|
|
108
110
|
`message_delta.content.type === "tool_call_delta"` carries `{ index, id?, name?, argumentsText? }`. Treat it as a streaming fragment. The runtime reconstructs and persists a final `tool_call` before executing tools. Deltas missing `id`/`name` at stream end fail the provider turn with `ErrorInfo.code: "incomplete_delta"` (typed `ProviderTransportError`); they never throw a bare `Error`. Malformed JSON with id+name present recovers as a blocked tool result (`invalid_json_arguments`) instead.
|
|
109
111
|
|
|
110
112
|
Tool execution events:
|
|
@@ -134,6 +136,7 @@ Queue / subscriber / compaction / retry / provider events:
|
|
|
134
136
|
| `event_subscriber_overflow` | `sessionId`, `droppedEvents: number`, `maxQueuedEvents: number`, `overflow: "close" \| "drop_oldest" \| "drop_newest"` |
|
|
135
137
|
| `compaction_started` | `sessionId`, `runId?` |
|
|
136
138
|
| `compaction_finished` | `sessionId`, `runId?`, `summary: string` |
|
|
139
|
+
| `attention_compiled` | `sessionId`, `runId?`, `used: number`, `usedAfter: number`, `inputCap: number`, `triggerRatio: number`, `droppedThinkingTurns: number`, `stubbedToolResults: number`, `stubbedBytes: number`, `truncated: boolean` — one per mutated turn of the opt-in [attention compiler](attention-compiler.md); counts only, never message text |
|
|
137
140
|
| `retry_scheduled` | `sessionId`, `runId`, `attempt: number`, `delayMs: number`, `error: ErrorInfo` |
|
|
138
141
|
|
|
139
142
|
Provider turn events (metadata only — see [Observability](observability.md)):
|
|
@@ -251,4 +254,4 @@ for await (const event of session.stream("draft", { loop: { strategy: "generate-
|
|
|
251
254
|
- [Observability](observability.md): `ProviderTurnMetadata`; optional adapter builds one parented GenAI span tree from metadata-only lifecycle events and ignores message/progress deltas.
|
|
252
255
|
- [Tools](tools.md): `tool_execution_*` variants.
|
|
253
256
|
- [Compaction and retry policies](compaction-and-retry.md): `compaction_*` and `retry_scheduled` variants.
|
|
254
|
-
- [Frontend interoperability (AG-UI and ACP)](ag-ui.md): optional redacted mapping of this stream; durable replay is ledger-backed and at-least-once, never a live-subscriber substitute. [ACP coding-host interop](acp.md) additionally maps `CodingLifecycleEvent`s from `@arnilo/prism-coding-tools/agent` (`file_changed`, `worktree_changed`, `permission_denied`, `configuration_changed`, `plan_changed`, `plan_removed`; process events reuse `CodingProcessEvent`) into ACP session updates — locations/diff blocks only through projection allow-lists, terminal chunks under `process.outputChunkBytes`, plan updates only to clients that advertised the UNSTABLE `plan` capability.
|
|
257
|
+
- [Frontend interoperability (AG-UI and ACP)](ag-ui.md): optional redacted mapping of this stream; durable replay is ledger-backed and at-least-once, never a live-subscriber substitute. [ACP coding-host interop](acp.md) additionally maps `CodingLifecycleEvent`s from `@arnilo/prism-coding-tools/agent` (`file_changed`, `worktree_changed`, `permission_denied`, `configuration_changed`, `plan_changed`, `plan_removed`, `subagent_started`, `subagent_stopped`; process events reuse `CodingProcessEvent`) into ACP session updates — locations/diff blocks only through projection allow-lists, terminal chunks under `process.outputChunkBytes`, plan updates only to clients that advertised the UNSTABLE `plan` capability.
|
|
@@ -39,7 +39,7 @@ createAgentSession(config: AgentSessionConfig & { agent: Agent }): AgentSession
|
|
|
39
39
|
|
|
40
40
|
`AgentConfig.provider` must contain the host-selected provider. Prism does not resolve providers from hidden globals. Alternatively, set `AgentConfig.providerSource: ProviderResolver` (or override per run with `RunOptions.providerSource`, which wins) to resolve the provider from `model.provider` each run; when `AgentConfig.provider` is set it takes first precedence and the resolver is bypassed. See [Provider layer § Provider resolver](provider-layer.md#provider-resolver).
|
|
41
41
|
|
|
42
|
-
`session.run(input, options)` accepts the existing Prism input shape
|
|
42
|
+
`session.run(input, options)` accepts the existing Prism input shape. `RunOptions.toolNames` optionally allow-lists registered tool names for that run (omit = full registry; empty = none). See [Tools](tools.md#per-run-tool-scoping).
|
|
43
43
|
|
|
44
44
|
```ts
|
|
45
45
|
string | Message | readonly Message[]
|
|
@@ -51,7 +51,7 @@ string | Message | readonly Message[]
|
|
|
51
51
|
|
|
52
52
|
`RunOptions.model` can override the request model for a run. Model overrides append a `model_change` entry. `AgentConfig.inputLayout` selects the default input assembly layout (`"cache_aware"` by default, or opt-in `"legacy"`); `RunOptions.inputLayout` wins for one run. `AgentConfig.thinkingLevel` / `RunOptions.thinkingLevel` (run wins) is the session thinking intent — Prism snaps it onto the request after host `providerOptions`. `AgentConfig.providerOptions`/`RunOptions.providerOptions` supply generic provider request options (session/cache/header/compat/extra hints only — provider-level timeout/retry hints were removed in 0.1.5). Kernel construction always stamps `options.sessionId`/`cacheKey` from `session.id` when missing; `createSessionCachePolicy` is an overlay, not required. Use `RunOptions.signal`/host abort controllers for timeouts and `AgentConfig.retry`/`RunOptions.retry` for retry. `AgentConfig.providerRequestPolicies`/`RunOptions.providerRequestPolicies` run before `AIProvider.generate()` and before `provider_request` middleware. `AgentConfig.systemPrompt` and `RunOptions.systemPrompt` add explicit layered system prompt contributions; `RunOptions.systemPrompt: false` disables configured prompt layers for that run while keeping `AgentConfig.instructions` as the base path. `RunOptions.compaction` can enable auto-compaction for that run or use `false` to disable configured auto-compaction. `RunOptions.retry` can enable provider-turn retry for that run or use `false` to disable configured retry. `RunOptions.metadata` is merged with agent/session metadata for assembly, provider requests, and tool contexts. Run tool-round limits via `RunOptions.limits.maxToolRounds`. `RunOptions.signal` is bridged into the per-run abort signal passed to assembly, providers, tools, auto-compaction, and retry backoff.
|
|
53
53
|
|
|
54
|
-
`RunOptions.activeSkills` selects named skills from a configured `SkillRegistry`; `RunOptions.skills` replaces a plain `Skill[]` config for one run. When `AgentConfig.skills` is a registry and neither is set, **no skills activate** unless `activateAllSkills: true` (run or agent). `skillsDisclosure` (`"progressive"` default, `"eager"` opt-in; run wins) controls catalog vs full instruction bodies; the session-owned `LoadedSkillSet` is populated by `load_skill` when the host registers `createLoadSkillTool`. `toolResultFold` (off unless the host supplies `summarize`) optionally folds aged large tool results in provider input only. See [Context and skills](context-and-skills.md).
|
|
54
|
+
`RunOptions.activeSkills` selects named skills from a configured `SkillRegistry`; `RunOptions.skills` replaces a plain `Skill[]` config for one run. When `AgentConfig.skills` is a registry and neither is set, **no skills activate** unless `activateAllSkills: true` (run or agent). `skillsDisclosure` (`"progressive"` default, `"eager"` opt-in; run wins) controls catalog vs full instruction bodies; the session-owned `LoadedSkillSet` is populated by `load_skill` when the host registers `createLoadSkillTool`. `toolResultFold` (off unless the host supplies `summarize`) optionally folds aged large tool results in provider input only. `AgentConfig.attentionCompiler` (or `true` for defaults) opts into the per-turn attention compiler; `RunOptions.attentionCompiler: false` disables it for one run and an options object may only relax the agent setting — the session resolves it with the run's model before the first provider turn, and keeps one sticky frontier per session so a stub made once stays applied. See [Context and skills](context-and-skills.md) and [Attention compiler](attention-compiler.md).
|
|
55
55
|
|
|
56
56
|
## Outputs / response / events
|
|
57
57
|
|
|
@@ -61,7 +61,7 @@ string | Message | readonly Message[]
|
|
|
61
61
|
|
|
62
62
|
`session.stream(input, options?)` subscribes first, starts exactly one run, yields only that run's events, and terminates when the run succeeds, fails, or aborts. Early consumer return aborts the owned run and releases the session. `SubscribeOptions.maxQueuedEvents` / `overflow` may be passed alongside `RunOptions`.
|
|
63
63
|
|
|
64
|
-
`resumeAgentRunStream(agent, ref, resume, options?)` does the same for one existing suspended durable run. It validates checkpoint ownership, revision/fingerprint, and `expectedVersion`, then subscribes before emitting `agent_started` / `agent_resumed` and resumed message/tool/terminal events. `AgentRunResumeStreamOptions` combines
|
|
64
|
+
`resumeAgentRunStream(agent, ref, resume, options?)` does the same for one existing suspended durable run. It validates checkpoint ownership, revision/fingerprint, and `expectedVersion`, then subscribes before emitting `agent_started` / `agent_resumed` and resumed message/tool/terminal events. `AgentRunResumeStreamOptions` combines `AgentRunResumeOptions` (including the optional `onSession` observer seam a supervisor uses to attach a child event pump to the rebuilt session) with `signal`, `maxQueuedEvents`, and `overflow`; early return aborts only resumed execution. It does not replay a claimed/dispatched tool, poll a ledger, or retain a worker. `createAgentRunLifecycle().resumeStream(ref, resume, request?)` adds the same behavior after host agent-capability resolution.
|
|
65
65
|
|
|
66
66
|
`session.subscribe(options?)` remains available for hosts that want a long-lived subscriber across runs. Subscribe before `run()` to observe that run's events. The consumer loop and `session.run()` must run concurrently (e.g. start the `for await` consumer, then `await Promise.all([consumer, session.run("Hi")])`): events are only emitted during a live run, so awaiting the subscribe loop before calling `run()` deadlocks. Prefer `session.stream()` when you only need one run's events. `SubscribeOptions.maxQueuedEvents` defaults to `1024` (minimum `1`) and caps events queued while the consumer is not awaiting `next()`. `SubscribeOptions.overflow` defaults to `"close"`; it clears queued payload events, delivers one `event_subscriber_overflow` notice to that subscriber, then closes it. `"drop_oldest"` keeps newest events; `"drop_newest"` ignores new events while full.
|
|
67
67
|
|
|
@@ -79,7 +79,7 @@ For tool calls, the runtime streams provider `tool_call_delta` fragments as `mes
|
|
|
79
79
|
|
|
80
80
|
Provider `thinking`/`reasoning` content emitted during a turn is preserved as `thinking` content blocks on the assistant message in session history. On the next turn, provider packages decide how to carry prior reasoning forward. For example, the NeuralWatt provider serializes prior `thinking` blocks under a `reasoning_content` field for reasoning-capable models (gated on `capabilities.reasoning` / `compat.preserve_thinking`, droppable via `compat.clear_thinking`); see [NeuralWatt provider](providers/neuralwatt.md). Non-reasoning providers/models receive no reasoning field, so prior thinking does not leak into providers that do not support it.
|
|
81
81
|
|
|
82
|
-
`session.compact(options?)` runs the selected compaction strategy, appends one `kind: "compaction"` entry under the current leaf, updates the leaf, emits `compaction_started` and `compaction_finished`, and returns the appended `CompactionResult`. If `AgentConfig.compaction` or `RunOptions.compaction` includes `thresholdEntries
|
|
82
|
+
`session.compact(options?)` runs the selected compaction strategy, appends one `kind: "compaction"` entry under the current leaf, updates the leaf, emits `compaction_started` and `compaction_finished`, and returns the appended `CompactionResult`. If `AgentConfig.compaction` or `RunOptions.compaction` includes `thresholdEntries` (entry count) or `trigger` (entry count, input ratio against the compiler's cap, or a host callback), auto-compaction checks once after input/model-change entries are appended and before provider input assembly; `RunOptions.compaction: false` skips that run's auto-compaction. A branch that already ends with a `kind: "compaction"` entry is left alone.
|
|
83
83
|
|
|
84
84
|
`entries()` returns the current branch entries. `checkout(leafId?)` moves the session to an existing leaf and rebuilds history. `fork()` returns a session on the same store/session id at the selected leaf without copying entries. `clone({ id })` copies the current branch to a new session id with new entry ids.
|
|
85
85
|
|
|
@@ -188,7 +188,7 @@ Set `runState` with a host-owned `CheckpointStore`, stable `definitionRevision`,
|
|
|
188
188
|
- `decision: "approve" | "deny"` — legacy single-approval path. `approve` allows every pending decision once; `deny` terminates the run as `denied`.
|
|
189
189
|
- `decisions: readonly RunDecision[]` — one atomic batch. Every entry validates against the recorded pending set (unknown/foreign `approvalId`, duplicates, stale `expectedVersion`, invalid outcomes fail the whole batch closed with `AgentDecisionError` and leave state and version untouched). Outcomes: `allow_once`, `allow_for_run`, `reject_once`, `reject_for_run`. `reject_*` continues the run with a blocked tool result carrying the bounded (2 KB) `reason`. `modifiedArguments` are revalidated (schema, then input guardrails; permission/trust re-run at dispatch) and produce a new arguments hash. `elicitation` payloads are validated against the pending decision's `elicitationSchema` (required keys plus the configured host validator) and resolve the suspended call without executing it. A batch deciding a strict subset persists the decided entries and re-suspends with the remainder pending at the bumped version.
|
|
190
190
|
|
|
191
|
-
`*_for_run` outcomes append a `StickyDecision` to the durable run state: later calls in the same run matching the scope exactly (all recorded fields) proceed or are blocked without a new suspension, policy still enforced at dispatch. Sticky decisions expire when the run reaches any terminal status. Caps: 32 pending decisions per run (hard 128), 64 sticky decisions (hard 256), 2 KB decision reasons, 16 KB elicitation payloads.
|
|
191
|
+
`*_for_run` outcomes append a `StickyDecision` to the durable run state: later calls in the same run matching the scope exactly (all recorded fields) proceed or are blocked without a new suspension, policy still enforced at dispatch. Sticky decisions expire when the run reaches any terminal status. Caps: 32 pending decisions per run (hard 128), 64 sticky decisions (hard 256), 2 KB decision reasons, 16 KB elicitation payloads. Frontend adapters (such as AG-UI with `capabilities.humanInTheLoop.approveWithEdits`) and the server resume endpoint (`POST .../resume` with `modifiedArguments`) map human edits directly to `RunDecision` entries with `modifiedArguments` under single atomic CAS, revalidating tool parameter schemas and invalidating stale draft approvals.
|
|
192
192
|
|
|
193
193
|
**Runtime input validation (0.2.0, plan 020 Task 2).** Every public resume entrypoint (`resumeAgentRun`, `resumeAgentRunStream`, `AgentRunLifecycle.resume()`/`resumeStream()`) validates the complete resume input in core before any checkpoint read/write, agent resolution, subscription, or tool execution: a non-null object, positive safe-integer `expectedVersion`, exactly one of `decision`/`decisions`, legacy `decision` exactly `approve`/`deny`, and a non-empty batch ≤ 128 entries whose entries are objects with a bounded non-empty `approvalId`, a whitelisted outcome, an optional string `reason` within the 2 KB limit, and JSON-object `modifiedArguments`/`elicitation` within the 16 KB limit. Unknown legacy decisions (e.g. `"sideways"`) and malformed untyped batches fail closed with `AgentDecisionError` (`ERR_PRISM_DECISION_INVALID`/`..._LIMIT`/`..._DUPLICATE`) under a **no-side-effect guarantee**: zero checkpoint writes/CAS changes, zero tool calls, zero resumed events. This holds for plain-JavaScript and `as any` callers; the server's transport parser is defense in depth, not the security boundary. State-dependent checks (foreign/stale approval ids, scope, schema, policy) still run in the atomic batch resolver.
|
|
194
194
|
|
|
@@ -203,7 +203,7 @@ if (result.status === "suspended") {
|
|
|
203
203
|
}
|
|
204
204
|
```
|
|
205
205
|
|
|
206
|
-
Resume requires exact checkpoint ownership, version, agent fingerprint, and revision. The fingerprint hashes the agent id/name, `definitionRevision`, model, instructions, system-prompt contributions, skills (name/instructions/tool names), tool definitions (name/parameters/exclusive), guardrail definitions (name/stage/revision), and loop strategy — changing any of them without bumping `definitionRevision` fails resume closed instead of silently continuing with different agent semantics. Prism CAS-claims approval before work, rechecks normal guardrail/permission/validation/limit paths, and marks a pending tool dispatched before its side effect. `createAgentRunLifecycle()` wraps the same core path for server/MCP hosts: adapters pass only authorized ownership, status returns only `{ state, version }`, and `resolveAgent()` supplies current agent/revision. `resumeStream()` uses that same claim path and bounded subscriber, so adapters do not poll or duplicate resume logic. Remote restart requires both checkpoint and session stores to be durable. A crash after that mark is ambiguous and is never replayed automatically; use host tool idempotency keyed by `runId`/`toolCallId` or resolve it manually. Checkpoints contain bounded redacted state plus session/leaf references, never provider objects, callbacks, signals, credentials, or raw secrets. State is bounded at save by `runState.maxStateBytes` (default 256 KB, at most the 1 MB hard cap); load bounds against the 1 MB hard cap only, so state saved with a raised limit stays resumable while oversized records are still rejected. Since 0.1.3 (plan 015 Task 4), durable runs may opt in to session-state persistence with `persistSessionState: true` on both the run and resume options: the loaded-skill **name catalog** (≤64 names, ≤256 chars each) rides the checkpoint and is restored into the resumed session's `LoadedSkillSet`; skill **bodies are never persisted** and re-resolve from the live registry via `load_skill`. Since 0.1.6 (plan 018 closeout `checkpoint-bodies`), `includeSkillBodies: true` on BOTH the run and resume options additionally persists the exact loaded-skill **instructions** (`{name, instructions}` pairs, redacted at the checkpoint boundary like all state, ≤64 bodies / ≤256-char names / ≤262144-byte bodies / ≤1 MiB total) so resume re-renders them registry-independently — no `load_skill` round-trip and no dependence on the registry still serving the same text; `maxStateBytes` (default 256 KB) refuses oversize bodies with a recorded error, never silently truncates. Default off keeps the checkpoint shape byte-identical to 0.1.3. Built-in loop options are durable; custom `AgentLoopStrategy` instances are durable when they declare `snapshot`/`restore` hooks (see [Agent loops § Durable runs](agent-loops.md#durable-runs)) and reject before provider work otherwise.
|
|
206
|
+
Resume requires exact checkpoint ownership, version, agent fingerprint, and revision. The fingerprint hashes the agent id/name, `definitionRevision`, model, instructions, system-prompt contributions, skills (name/instructions/tool names), tool definitions (name/parameters/exclusive), guardrail definitions (name/stage/revision), and loop strategy — changing any of them without bumping `definitionRevision` fails resume closed instead of silently continuing with different agent semantics. Prism CAS-claims approval before work, rechecks normal guardrail/permission/validation/limit paths, and marks a pending tool dispatched before its side effect. `createAgentRunLifecycle()` wraps the same core path for server/MCP hosts: adapters pass only authorized ownership, status returns only `{ state, version }`, and `resolveAgent()` supplies current agent/revision. `resumeStream()` uses that same claim path and bounded subscriber, so adapters do not poll or duplicate resume logic. Remote restart requires both checkpoint and session stores to be durable. A crash after that mark is ambiguous and is never replayed automatically; use host tool idempotency keyed by `runId`/`toolCallId` or resolve it manually. Checkpoints contain bounded redacted state plus session/leaf references, never provider objects, callbacks, signals, credentials, or raw secrets. State is bounded at save by `runState.maxStateBytes` (default 256 KB, at most the 1 MB hard cap); load bounds against the 1 MB hard cap only, so state saved with a raised limit stays resumable while oversized records are still rejected. Since 0.1.3 (plan 015 Task 4), durable runs may opt in to session-state persistence with `persistSessionState: true` on both the run and resume options: the loaded-skill **name catalog** (≤64 names, ≤256 chars each) rides the checkpoint and is restored into the resumed session's `LoadedSkillSet`; skill **bodies are never persisted** and re-resolve from the live registry via `load_skill`. Since 0.1.6 (plan 018 closeout `checkpoint-bodies`), `includeSkillBodies: true` on BOTH the run and resume options additionally persists the exact loaded-skill **instructions** (`{name, instructions}` pairs, redacted at the checkpoint boundary like all state, ≤64 bodies / ≤256-char names / ≤262144-byte bodies / ≤1 MiB total) so resume re-renders them registry-independently — no `load_skill` round-trip and no dependence on the registry still serving the same text; `maxStateBytes` (default 256 KB) refuses oversize bodies with a recorded error, never silently truncates. Default off keeps the checkpoint shape byte-identical to 0.1.3. Since 0.7.0 (plan 074 P3), `persistSessionState: true` also carries the opt-in [attention compiler](attention-compiler.md)'s sticky frontier (`sessionState.attentionSticky`: 32-hex thinking keys plus tool-call ids, newest 256 of each, redacted like all state) so a resumed run keeps its thinking strips and tool stubs instead of re-deciding its first turn from the ratio; a malformed frontier is dropped entry by entry and never blocks a resume. Since 0.7.0, `onSession` hands the reconstructed session to a caller-supplied observer before the resumed run starts, so an observer (the supervisor's child-event pump) can subscribe while the run is still live; it is called for every resume outcome, a throw fails closed before any event or tool work, and the session is valid only for the duration of that resume. Built-in loop options are durable; custom `AgentLoopStrategy` instances are durable when they declare `snapshot`/`restore` hooks (see [Agent loops § Durable runs](agent-loops.md#durable-runs)) and reject before provider work otherwise.
|
|
207
207
|
|
|
208
208
|
## Secure composition
|
|
209
209
|
|
|
@@ -0,0 +1,272 @@
|
|
|
1
|
+
# Attention compiler
|
|
2
|
+
|
|
3
|
+
## What it does
|
|
4
|
+
|
|
5
|
+
`createAttentionCompiler(options?, context?)` returns a validated, frozen configuration for the opt-in attention compiler: a per-turn gate that **measures** the assembled input against a host ratio of the model input cap and **rewrites nothing** until that ratio is reached.
|
|
6
|
+
|
|
7
|
+
The compiler is a gate, not a mixer. Once over the ratio it mutates a **history clone** monotonically — oldest `thinking` blocks first, then oldest fold-eligible tool results — so prompt-cache prefixes survive and the session store, observational-memory ledger, and input history array are never touched. Still over after every eligible row → `AttentionBudgetError` instead of silently dropping constitution.
|
|
8
|
+
|
|
9
|
+
The whole request (instruction groups, summaries, history, input, attachments, in-flight tool results, context blocks, skill catalog, tool declarations) is measured **once per turn**; each mutation then subtracts its own delta instead of re-measuring, so a turn costs one extra pass over the assembled input.
|
|
10
|
+
|
|
11
|
+
Current status: `createAttentionCompiler` and the two stages ship today, wired through `AgentConfig` / `AgentDefinition` and overlayed per run by `RunOptions`, and also available directly on `assembleProviderInput`. The field is opt-in everywhere: omitted, the request bytes are unchanged and no frontier is allocated.
|
|
12
|
+
|
|
13
|
+
Context blocks are measured, never repacked: the compiler sees whatever providers returned, tagged
|
|
14
|
+
however they tagged it. A memory fabric's provider is one such source — it contributes the same
|
|
15
|
+
`working-memory` / `semantic-memory` blocks `createMemory` resolves (see
|
|
16
|
+
[Memory fabric](memory-fabric.md)), and turning the compiler on or off changes their cost, never
|
|
17
|
+
their identity. The observational-memory ledger it protects stays the session's episodic record,
|
|
18
|
+
written (if at all) by that subpath's own workers; the compiler only ever mutates its history clone,
|
|
19
|
+
so neither OM nor a typed-notes layer is rewritten here. There is no layer id, handle, or per-source
|
|
20
|
+
quota in this seam.
|
|
21
|
+
|
|
22
|
+
## When to use it
|
|
23
|
+
|
|
24
|
+
Use it when a host runs long sessions with prompt caching and wants a deterministic, dependency-free gate that keeps a request inside the model input cap **without** reordering or deleting history:
|
|
25
|
+
|
|
26
|
+
- thinking-heavy agent loops where old reasoning blocks are pure attention waste;
|
|
27
|
+
- long tool loops where old grep/dump results crowd out recent context;
|
|
28
|
+
- hosts that want an explicit `AttentionBudgetError` signal (compact now) rather than silent eviction.
|
|
29
|
+
|
|
30
|
+
Do not use it as a replacement for compaction or for `applyContextBudget` eviction: compaction is the boundary operation that writes a summary, the compiler only rewrites what this turn sends.
|
|
31
|
+
|
|
32
|
+
## Inputs / request
|
|
33
|
+
|
|
34
|
+
### Enabling it
|
|
35
|
+
|
|
36
|
+
| Surface | Value | Meaning |
|
|
37
|
+
| --- | --- | --- |
|
|
38
|
+
| `AgentConfig.attentionCompiler` | `true \| AttentionCompilerOptions` | `true` uses the defaults below. Omitted (or `false`) keeps today's request bytes. |
|
|
39
|
+
| `AgentDefinition.attentionCompiler` | same | Copied onto the resolved config by `resolveAgentDefinition`; it changes nothing else about the definition. |
|
|
40
|
+
| `RunOptions.attentionCompiler` | `false \| true \| AttentionCompilerOptions` | `false` disables the compiler for that run, `true` is a no-op, and an object is a **narrowing overlay** on the agent setting. |
|
|
41
|
+
|
|
42
|
+
```ts
|
|
43
|
+
const agent = createAgent({ model, provider, attentionCompiler: true });
|
|
44
|
+
await agent.createSession({ id: "s" }).run("long task");
|
|
45
|
+
await session.run("cheap run", { attentionCompiler: { triggerRatio: 0.95, compactRatio: 0.99 } });
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
The agent setting is resolved with the run's model at run start, before any provider turn, so a malformed setting or a widening overlay fails the run immediately instead of on the turn that crosses the ratio:
|
|
49
|
+
|
|
50
|
+
- **Allowed in the overlay:** `triggerRatio` / `compactRatio` at or above the agent setting, `keepLast` / `thinkingKeepTurns` at or below it, and extra `excludeTools` (unioned with the agent list, never removed).
|
|
51
|
+
- **Rejected:** a lower gate ratio, more protected rows, and `maxInputTokens` / `reserveTokens` — cap inputs are agent-config only, because moving the cap moves the gate itself. Raising `triggerRatio` at or above the agent's `compactRatio` needs `compactRatio` raised in the same overlay.
|
|
52
|
+
- **Enabling from a run is rejected:** a run may disable or relax the compiler, never switch it on where the agent config left it off.
|
|
53
|
+
|
|
54
|
+
The **sticky frontier is session-owned and created lazily** the first time an enabled run assembles a request: one `{ thinking, toolCallIds }` set pair per session, shared across runs, provider rounds, and branches, so a stub or strip made once stays applied even on a later under-ratio turn. It lives in memory only — a resumed process simply re-decides from the ratio it sees.
|
|
55
|
+
|
|
56
|
+
`AttentionCompilerOptions` (all optional):
|
|
57
|
+
|
|
58
|
+
| Field | Type | Default | Meaning |
|
|
59
|
+
| --- | --- | --- | --- |
|
|
60
|
+
| `triggerRatio` | `number` | `0.75` | Fraction of `inputCap` that enables mutation; must be in `(0, 1)` (exclusive). |
|
|
61
|
+
| `compactRatio` | `number` | `0.9` | Where compaction should fire relative to the compiler; must exceed `triggerRatio`. |
|
|
62
|
+
| `thinkingKeepTurns` | `number` | `1` | Newest thinking-bearing assistant turns kept intact. |
|
|
63
|
+
| `keepLast` | `number` | `3` | Newest tool results kept full. |
|
|
64
|
+
| `excludeTools` | `readonly string[]` | `[]` | Tool names whose results are never stubbed, whatever the ratio. |
|
|
65
|
+
| `maxInputTokens` | `number` | — | Host cap; when set it wins over `model.limits.contextWindow`. |
|
|
66
|
+
| `reserveTokens` | `number` | `1024` | Output + next-turn headroom subtracted from the window. |
|
|
67
|
+
|
|
68
|
+
`AttentionCompilerContext`:
|
|
69
|
+
|
|
70
|
+
| Field | Type | Meaning |
|
|
71
|
+
| --- | --- | --- |
|
|
72
|
+
| `model` | `{ limits?: ModelLimits }` | Source of `contextWindow` / `maxOutputTokens` when `maxInputTokens` is absent. |
|
|
73
|
+
| `compactionTrigger` | `CompactionTrigger` | Optional: validated here so an unknown trigger `type` fails at create time, not on the first turn. An `input_ratio` trigger must exceed `triggerRatio`. |
|
|
74
|
+
|
|
75
|
+
**Public surface.** `createAttentionCompiler(options?: AttentionCompilerOptions, context?)` is the factory; `AttentionCompilerOptions` carries the gate ratios, sticky-stage tuning (`thinkingKeepTurns`, `keepLast`), `excludeTools`, and `reserveTokens`. `resolveInputCap(options?: AttentionInputCapOptions, model?)` is the cap resolver, `compileAttention(options: AttentionCompileOptions)` is the per-turn call `assembleProviderInput` makes (`AttentionCompileOptions` also carries `fold`, `frontier`, `redactor`, `signal`, and the `turn`/`sessionId`/`runId` telemetry ids), and `createAttentionTruncationTrigger(options?: AttentionTruncationTriggerOptions)` builds the host-programmable compaction trigger.
|
|
76
|
+
|
|
77
|
+
Input cap resolution: `maxInputTokens` when set, otherwise `contextWindow - (maxOutputTokens ?? 0) - reserveTokens`. Both `resolveInputCap(options?, model?)` and the compiler fail closed with a `TypeError` when neither source is present, when a declared limit is malformed, or when the computed cap is not positive.
|
|
78
|
+
|
|
79
|
+
Turn options, passed to `assembleProviderInput`:
|
|
80
|
+
|
|
81
|
+
| Field | Type | Meaning |
|
|
82
|
+
| --- | --- | --- |
|
|
83
|
+
| `attentionCompiler` | `AttentionCompilerOptions \| AttentionCompiler` | Raw options are validated for that call; a resolved handle reuses one validation. The session passes the run's resolved handle so a tuning typo fails before the first provider turn. |
|
|
84
|
+
| `attentionSticky` | `AttentionStickyFrontier` | `{ thinking, toolCallIds }` sets from `createAttentionStickyFrontier()`. The session supplies its own; a direct `assembleProviderInput` caller owns it, and omitting it makes each call mutate for its turn only. |
|
|
85
|
+
| `onAttentionReport` | `(report: AttentionReport) => void` | Called once per **mutated** turn, before `input_assembly` middleware; silent under the ratio. The session uses it to emit `attention_compiled`. |
|
|
86
|
+
|
|
87
|
+
`attentionCompiler` and `contextBudget` are **mutually exclusive** — a compiler-on turn that is still over throws `AttentionBudgetError` rather than evicting through the budget, so passing both fails closed with a `TypeError`.
|
|
88
|
+
|
|
89
|
+
## Outputs / response / events
|
|
90
|
+
|
|
91
|
+
`createAttentionCompiler` returns an `AttentionCompiler`: `inputCap`, `reserveTokens`, `triggerRatio`, `compactRatio`, `thinkingKeepTurns`, `keepLast`, and a frozen, de-duplicated `excludeTools`. It performs no I/O and calls no provider.
|
|
92
|
+
|
|
93
|
+
`compileAttention` always returns an `AttentionReport` beside `mutated`; under the ratio `mutated` is `false`, the same groups object comes back, and nothing is emitted:
|
|
94
|
+
|
|
95
|
+
| Field | Type | Meaning |
|
|
96
|
+
| --- | --- | --- |
|
|
97
|
+
| `used` | `number` | Estimated tokens measured before this turn's mutation. |
|
|
98
|
+
| `usedAfter` | `number` | Estimated tokens of the same request after the mutation, so `used` → `usedAfter` is the per-turn cost curve. |
|
|
99
|
+
| `inputCap` | `number` | Resolved cap the ratio was compared against. |
|
|
100
|
+
| `triggerRatio` | `number` | Configured ratio. |
|
|
101
|
+
| `droppedThinkingTurns` | `number` | Thinking turns absent from this request — rows re-applied from the sticky frontier count again. |
|
|
102
|
+
| `stubbedToolResults` | `number` | Tool results stubbed in this request — re-applied rows count again. |
|
|
103
|
+
| `stubbedBytes` | `number` | Payload bytes those stubs took out of the request (message bytes minus the stub header). |
|
|
104
|
+
| `truncated` | `boolean` | `true` when the gate stopped with eligible rows left, so the sticky frontier is partial. |
|
|
105
|
+
| `runId` / `sessionId` | `string?` | Owning run/session when known. |
|
|
106
|
+
|
|
107
|
+
Telemetry: a session emits **one `attention_compiled` per mutated turn** and nothing on an under-ratio turn or when the compiler is off. The payload is counts only — `sessionId`, `runId`, `used`, `usedAfter`, `inputCap`, `triggerRatio`, `droppedThinkingTurns`, `stubbedToolResults`, `stubbedBytes`, `truncated` — never message text or stub bodies, so it is safe in a redacted ledger and folds into an `attention` step of the [execution timeline](execution-timeline.md). A run that fails closed raises `AttentionBudgetError` instead, which surfaces as the run's `error` event. Direct `assembleProviderInput` callers get the same data through `onAttentionReport`.
|
|
108
|
+
|
|
109
|
+
Stage order once the gate opens (C4):
|
|
110
|
+
|
|
111
|
+
1. **Thinking** — strip every `thinking` block from assistant turns except the newest `thinkingKeepTurns`, oldest turn first.
|
|
112
|
+
2. **Tool results** — stub the oldest tool-result rows beyond the newest `keepLast`, oldest first, across history and the in-flight results of the current turn.
|
|
113
|
+
|
|
114
|
+
Each stage stops as soon as the estimate is back under `triggerRatio`; rows left eligible make `AttentionReport.truncated` `true`. A stub keeps the call identity and drops the payload:
|
|
115
|
+
|
|
116
|
+
```text
|
|
117
|
+
Tool result read_file [call_1]: omitted 41_982 bytes (sha256 3f9a1c2b4d5e6f70a1b2c3d4e5f6a7b8)
|
|
118
|
+
```
|
|
119
|
+
|
|
120
|
+
Never stubbed: rows named in `excludeTools`, tool **errors**, results stamped as a decision/approval payload (`approval`, `approvalId`, `prismApproval`, `decision`, `decisions`, `pendingDecisions`, `elicitation` metadata), rows the host fold's own age/byte gates exclude, and any row whose stub would cost more than the payload it replaces. When `toolResultFold.summarize` is configured, that function produces the stub body for the rows the compiler picked (capped by its `maxSummaryBytes`); otherwise the deterministic digest above is used.
|
|
121
|
+
|
|
122
|
+
`compileAttention({ compiler, groups, context?, skills?, tools?, fold?, frontier?, redactor?, signal?, turn?, sessionId?, runId? })` is what `assembleProviderInput` calls; it returns `{ groups, mutated, report }`. Under the ratio it returns the **same groups object** it was given; when it mutates it returns new `history` / `toolResults` arrays and never writes into the caller's arrays.
|
|
123
|
+
|
|
124
|
+
Errors:
|
|
125
|
+
|
|
126
|
+
| Error | Code | Raised when |
|
|
127
|
+
| --- | --- | --- |
|
|
128
|
+
| `AttentionBudgetError` | `attention_budget_exceeded` | Still over `triggerRatio` after every eligible stage — host should compact, not delete. |
|
|
129
|
+
| `TypeError` | — | Invalid option, unknown compaction trigger `type`, or an unresolvable input cap. Use `isAttentionBudgetError` to narrow. |
|
|
130
|
+
|
|
131
|
+
## Request/response example
|
|
132
|
+
|
|
133
|
+
```json
|
|
134
|
+
{
|
|
135
|
+
"triggerRatio": 0.75,
|
|
136
|
+
"thinkingKeepTurns": 1,
|
|
137
|
+
"keepLast": 3,
|
|
138
|
+
"excludeTools": ["submit_payment"],
|
|
139
|
+
"reserveTokens": 1024,
|
|
140
|
+
"compaction": {
|
|
141
|
+
"trigger": {
|
|
142
|
+
"type": "custom",
|
|
143
|
+
"shouldCompact": "host function — sees sessionId, entryCount, estimatedInputTokens, inputCapTokens"
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
```
|
|
148
|
+
|
|
149
|
+
## Implementation example
|
|
150
|
+
|
|
151
|
+
```ts
|
|
152
|
+
import { createAttentionCompiler, resolveInputCap } from "@arnilo/prism";
|
|
153
|
+
|
|
154
|
+
const compiler = createAttentionCompiler(
|
|
155
|
+
{
|
|
156
|
+
triggerRatio: 0.75,
|
|
157
|
+
thinkingKeepTurns: 1,
|
|
158
|
+
keepLast: 3,
|
|
159
|
+
excludeTools: ["submit_payment"],
|
|
160
|
+
reserveTokens: 1024,
|
|
161
|
+
},
|
|
162
|
+
{ model: { limits: { contextWindow: 200_000, maxOutputTokens: 8_192 } } },
|
|
163
|
+
);
|
|
164
|
+
|
|
165
|
+
compiler.inputCap; // 200000 - 8192 - 1024
|
|
166
|
+
|
|
167
|
+
// Host-programmable compact-when, validated at create:
|
|
168
|
+
createAttentionCompiler(
|
|
169
|
+
{ triggerRatio: 0.75 },
|
|
170
|
+
{ model: { limits: { contextWindow: 200_000 } }, compactionTrigger: { type: "input_ratio", ratio: 0.9 } },
|
|
171
|
+
);
|
|
172
|
+
|
|
173
|
+
// Shared cap helper for hosts that only need the number:
|
|
174
|
+
resolveInputCap({ reserveTokens: 1024 }, { limits: { contextWindow: 200_000, maxOutputTokens: 8_192 } });
|
|
175
|
+
```
|
|
176
|
+
|
|
177
|
+
Run it through assembly — this is the raw seam; sessions do it for you. Pass the same frontier on every turn so an over-ratio turn stays shrunk afterwards (the session keeps one per session).
|
|
178
|
+
|
|
179
|
+
```ts
|
|
180
|
+
import { assembleProviderInput, createAttentionStickyFrontier } from "@arnilo/prism";
|
|
181
|
+
|
|
182
|
+
const attentionSticky = createAttentionStickyFrontier();
|
|
183
|
+
|
|
184
|
+
const request = await assembleProviderInput({
|
|
185
|
+
model,
|
|
186
|
+
input: "continue",
|
|
187
|
+
history, // session history snapshot; never mutated
|
|
188
|
+
tools,
|
|
189
|
+
turn,
|
|
190
|
+
attentionSticky,
|
|
191
|
+
attentionCompiler: {
|
|
192
|
+
triggerRatio: 0.75,
|
|
193
|
+
thinkingKeepTurns: 1,
|
|
194
|
+
keepLast: 3,
|
|
195
|
+
excludeTools: ["submit_payment"],
|
|
196
|
+
},
|
|
197
|
+
});
|
|
198
|
+
|
|
199
|
+
// Under the ratio: identical to the same call without `attentionCompiler`.
|
|
200
|
+
// Over the ratio: old `thinking` blocks are gone, old tool bodies are stubs,
|
|
201
|
+
// and the session store still holds every original payload.
|
|
202
|
+
```
|
|
203
|
+
|
|
204
|
+
## Compact-when: where the compiler hands off
|
|
205
|
+
|
|
206
|
+
The compiler never compacts. It mutates a clone of the request for one turn, and `session.compact()` keeps its task-boundary rule (it throws while a run is in flight). Compaction stays where it was: `CompactionOptions.trigger`, `session.compact()`, or the observational-memory attach loop.
|
|
207
|
+
|
|
208
|
+
What the compiler does contribute is the *number*. `session.autoCompact()` decides an `input_ratio` trigger with `resolveInputCap` — the same helper that resolves the compiler's `inputCap` — and `AttentionCompilerOptions.compactRatio` / `createAttentionCompiler(options, { compactionTrigger })` exist so a host can express "shrink at 0.75, compact at 0.9" with one validated pair: a compaction `input_ratio` at or below the compiler's `triggerRatio` is rejected, because the compiler has already tried the cheap stages at that point.
|
|
209
|
+
|
|
210
|
+
```ts
|
|
211
|
+
const agent = createAgent({
|
|
212
|
+
model,
|
|
213
|
+
provider,
|
|
214
|
+
attentionCompiler: { triggerRatio: 0.75, keepLast: 3 },
|
|
215
|
+
compaction: { trigger: { type: "input_ratio", ratio: 0.9 } },
|
|
216
|
+
});
|
|
217
|
+
```
|
|
218
|
+
|
|
219
|
+
Ordering per run: auto-compaction is evaluated once, after the run input is appended and before provider input assembly; the compiler then runs inside assembly on whatever survived. After a compaction the next request is the frozen prefix plus the fresh summary plus the recent tail, and the compiler treats that summary and prefix as untouchable — it may still strip thinking or stub tool results in the **kept tail** when the estimate is over the ratio again. When the observational-memory strategy wrote that summary while work scopes were open, the summary is already the **projected** working set (leaf scope + ancestors), not the full ledger.
|
|
220
|
+
|
|
221
|
+
### Acting on `truncated`
|
|
222
|
+
|
|
223
|
+
`truncated: true` says the gate ran out of *eligible* rows: stubs cannot hold the request under the ratio, so the honest answer is a new prefix at the next task boundary rather than a silent eviction. `createAttentionTruncationTrigger` turns that signal into a drop-in trigger — feed it every `attention_compiled` event and hand `trigger` to the same `CompactionOptions.trigger` seam:
|
|
224
|
+
|
|
225
|
+
```ts
|
|
226
|
+
const truncation = createAttentionTruncationTrigger({ threshold: 2 }); // consecutive truncated turns
|
|
227
|
+
const agent = createAgent({
|
|
228
|
+
model,
|
|
229
|
+
provider,
|
|
230
|
+
attentionCompiler: true,
|
|
231
|
+
compaction: { trigger: truncation.trigger },
|
|
232
|
+
});
|
|
233
|
+
session.subscribe((event) => {
|
|
234
|
+
if (event.type === "attention_compiled") truncation.observe(event);
|
|
235
|
+
});
|
|
236
|
+
```
|
|
237
|
+
|
|
238
|
+
It fires **once per armed streak** at the next compaction decision (auto-compact before the next `run()`, or a host attach loop's post-run gate), and a mutated turn that was *not* truncated clears the streak because the pressure was relieved. `streak()` reads the current count and `reset()` clears an armed streak (for instance after a host-initiated `session.compact()`). The threshold is validated at create, so a typo fails at config time.
|
|
239
|
+
|
|
240
|
+
See [Compaction and retry policies](compaction-and-retry.md) for the trigger union and its fail-closed rules.
|
|
241
|
+
|
|
242
|
+
## Extension and configuration notes
|
|
243
|
+
|
|
244
|
+
- `excludeTools` is fail closed: entries are validated as non-empty bounded strings, de-duplicated, and frozen; a named tool is never stubbed even when the request stays over the ratio.
|
|
245
|
+
- The compiler never orchestrates other levers: `toolResultFold.summarize` still wins for fold-eligible rows when a host supplies it, `applyContextBudget` keeps working unchanged for compiler-off agents, and compaction stays a task-boundary operation (`session.compact()` still throws while a run is in flight).
|
|
246
|
+
- Sticky means sticky: a stripped thinking turn is never restored and a stubbed call id is never un-stubbed, even on a later under-ratio turn — restoring either would rewrite the cached prefix. Pass no `attentionSticky` for one-shot assemblies.
|
|
247
|
+
- The frontier is bounded (256 thinking keys, 256 tool-call ids, newest kept) and lives on the session, so it survives turns and runs. A durable run with `persistSessionState: true` also writes it into the checkpoint (`sessionState.attentionSticky`) and restores it on resume, so a resumed run keeps its stubs instead of re-deciding its first turn from the ratio; a malformed or hand-edited frontier is dropped entry by entry, never fatal to a resume.
|
|
248
|
+
- A compiler-on turn assembles from the default message groups (instructions, summaries, history, input, attachments, tool results) exactly like a `contextBudget` turn, so a custom `inputBuilder` is not consulted while the compiler is on.
|
|
249
|
+
- Compaction timing is programmable per agent through `CompactionOptions.trigger` (`threshold_entries` | `input_ratio` | `custom`); omitting it keeps today's `thresholdEntries` gate. `assertCompactionTrigger(trigger)` validates a trigger independently of the compiler.
|
|
250
|
+
- The gate is opt-in per agent/run; omit the option for current assembly bytes.
|
|
251
|
+
|
|
252
|
+
## Security and performance notes
|
|
253
|
+
|
|
254
|
+
- Validation is synchronous with no provider I/O, and the returned handle plus `excludeTools` are frozen.
|
|
255
|
+
- Reports and trigger contexts carry ids, counts, and token estimates only — never message text, tool payloads, or secrets.
|
|
256
|
+
- Stub text is `name` + `toolCallId` + byte count + a SHA-256 digest of the already-redacted payload: deterministic, model-free, and impossible to invert back into the payload. The digest is taken over redactor output when a redactor is configured, and the assembled request is redacted again at the provider edge.
|
|
257
|
+
- Projection-only, and **a stub is not a delete**: the session store, observational-memory ledger, and semantic stores are never rewritten, so recall, branching, and post-hoc audit still see every original payload. Observational-memory context blocks are never dropped, stubbed, or reordered.
|
|
258
|
+
- Compaction stays a task boundary: the compiler never writes the store and never triggers compaction mid-run (`session.compact()` still refuses while a run is active), and it never rewrites observational memory.
|
|
259
|
+
- Telemetry stays payload-free: the `attention_compiled` event carries `used`, `usedAfter`, `inputCap`, `triggerRatio`, `droppedThinkingTurns`, `stubbedToolResults`, `stubbedBytes`, and `truncated` only.
|
|
260
|
+
- `applyContextBudget` is not the compiler's last resort — overflowing after all eligible stages throws `AttentionBudgetError` so the frozen prefix (system instructions, `AGENTS.md`, skill catalog, tool declarations) cannot be silently evicted.
|
|
261
|
+
- Measured on the hermetic fixture in [`docs/_evidence/phase74-attention-measurements.md`](_evidence/phase74-attention-measurements.md): 63.7 % fewer input tokens, one cache bust on the turn the gate trips (compiler-off is append-only), and a volatile provider block that re-sends everything behind it every turn — which is why pinning a block stays a host recipe (`resolve` once per session) rather than compiler behavior. Regenerate with `node scripts/benchmark.mjs --scenario attention-compiler`.
|
|
262
|
+
|
|
263
|
+
## Related APIs
|
|
264
|
+
|
|
265
|
+
- [`assembleProviderInput`](input-and-prompt-assembly.md): the compose path the compiler pre-passes when enabled.
|
|
266
|
+
- [`toolResultFold`](input-and-prompt-assembly.md): host summarizer that wins over the deterministic stub for eligible rows.
|
|
267
|
+
- [`CompactionOptions`](compaction-and-retry.md): `trigger` is the host compact-when seam; `thresholdEntries` remains the default gate.
|
|
268
|
+
- [`observational-memory`](compaction-observational-memory.md): host `shouldCompact` / trigger overrides `compactAfterTokens` for post-run compaction.
|
|
269
|
+
- [`provider caching`](provider-caching.md): why mutations are monotonic and in-place.
|
|
270
|
+
- [`AttentionReport` measurements](_evidence/phase74-attention-measurements.md): the hermetic fixture behind the savings, cache, resume, and truncation numbers.
|
|
271
|
+
- [Memory fabric](memory-fabric.md): a context source whose blocks are measured like any other (`working-memory` / `semantic-memory` tags, no layer id).
|
|
272
|
+
- [`thinking and reasoning`](thinking-and-reasoning.md): the `thinking` blocks the first stage strips.
|