@arnilo/prism 0.8.0 → 0.10.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +62 -1
- package/README.md +13 -12
- package/dist/agent-approval.d.ts +17 -2
- package/dist/agent-approval.js +15 -6
- package/dist/agent-event-source.d.ts +9 -1
- package/dist/agent-event-source.js +10 -3
- package/dist/agent-loops.js +7 -4
- package/dist/agent-run-lifecycle.d.ts +15 -1
- package/dist/agent-run-lifecycle.js +82 -11
- package/dist/agent-run-state.d.ts +47 -6
- package/dist/agent-run-state.js +154 -6
- package/dist/agent-session/event-subscriber.d.ts +2 -0
- package/dist/agent-session/event-subscriber.js +3 -0
- package/dist/agent-session/helpers.js +14 -0
- package/dist/agent-session/session/assemble.js +281 -32
- package/dist/agent-session/session/persist.d.ts +11 -0
- package/dist/agent-session/session/persist.js +48 -16
- package/dist/agent-session/session/provider-round.d.ts +14 -4
- package/dist/agent-session/session/provider-round.js +226 -19
- package/dist/agent-session/session/tool-round.d.ts +2 -2
- package/dist/agent-session/session/tool-round.js +78 -6
- package/dist/agent-session/session/types.d.ts +44 -3
- package/dist/agent-session/session.d.ts +100 -5
- package/dist/agent-session/session.js +224 -13
- package/dist/attention-compiler.d.ts +51 -2
- package/dist/attention-compiler.js +282 -21
- package/dist/cache-helpers.d.ts +4 -2
- package/dist/cache-helpers.js +8 -6
- package/dist/checkpoint-restore.d.ts +45 -0
- package/dist/checkpoint-restore.js +54 -0
- package/dist/context-budget.d.ts +13 -1
- package/dist/context-budget.js +57 -4
- package/dist/contracts-core/agent.d.ts +52 -1
- package/dist/contracts-core/attention.d.ts +95 -0
- package/dist/contracts-core/content.d.ts +10 -0
- package/dist/contracts-core/extensions.d.ts +3 -0
- package/dist/contracts-core/guardrail-packs.d.ts +46 -0
- package/dist/contracts-core/guardrail-packs.js +2 -0
- package/dist/contracts-core/loop.d.ts +36 -0
- package/dist/contracts-core/provider.d.ts +30 -0
- package/dist/contracts-core/run-limits.d.ts +29 -1
- package/dist/contracts-core/session.d.ts +23 -5
- package/dist/contracts-core/session.js +21 -2
- package/dist/contracts-core/usage.d.ts +40 -0
- package/dist/contracts-core/usage.js +8 -0
- package/dist/contracts-core.d.ts +2 -0
- package/dist/contracts-core.js +2 -0
- package/dist/contracts-protocol.d.ts +81 -5
- package/dist/contracts-run-state.d.ts +91 -2
- package/dist/contributions.d.ts +2 -1
- package/dist/contributions.js +1 -0
- package/dist/extensions.d.ts +15 -1
- package/dist/extensions.js +68 -0
- package/dist/guardrail-packs/coding-standard.d.ts +3 -0
- package/dist/guardrail-packs/coding-standard.js +63 -0
- package/dist/guardrail-packs/destructive-commands.d.ts +3 -0
- package/dist/guardrail-packs/destructive-commands.js +46 -0
- package/dist/guardrail-packs/errors.d.ts +7 -0
- package/dist/guardrail-packs/errors.js +9 -0
- package/dist/guardrail-packs/index.d.ts +4 -0
- package/dist/guardrail-packs/index.js +15 -0
- package/dist/guardrail-packs/secrets-hygiene.d.ts +3 -0
- package/dist/guardrail-packs/secrets-hygiene.js +23 -0
- package/dist/guardrail-packs/types.d.ts +26 -0
- package/dist/guardrail-packs/types.js +2 -0
- package/dist/guardrail-packs/validation-respect.d.ts +3 -0
- package/dist/guardrail-packs/validation-respect.js +69 -0
- package/dist/guardrails.d.ts +61 -1
- package/dist/guardrails.js +377 -0
- package/dist/index.d.ts +16 -11
- package/dist/index.js +10 -7
- package/dist/input.d.ts +8 -1
- package/dist/input.js +68 -6
- package/dist/middleware.d.ts +37 -2
- package/dist/middleware.js +41 -0
- package/dist/node/session-store-jsonl.js +18 -3
- package/dist/observability.js +6 -0
- package/dist/provider-events.d.ts +8 -2
- package/dist/provider-events.js +60 -2
- package/dist/providers/openai-compatible.js +6 -3
- package/dist/run-bundle.d.ts +6 -1
- package/dist/run-bundle.js +5 -1
- package/dist/run-limits.d.ts +11 -1
- package/dist/run-limits.js +59 -0
- package/dist/session-stores.d.ts +12 -1
- package/dist/session-stores.js +21 -4
- package/dist/testing/agent-event-source-conformance.js +41 -2
- package/dist/testing/prefix-stability-conformance.d.ts +59 -0
- package/dist/testing/prefix-stability-conformance.js +172 -0
- package/dist/testing/session-store-conformance.d.ts +3 -2
- package/dist/testing/session-store-conformance.js +48 -0
- package/dist/tools.d.ts +5 -0
- package/dist/tools.js +21 -6
- package/dist/usage-estimation.d.ts +29 -0
- package/dist/usage-estimation.js +79 -0
- package/docs/agent-events.md +75 -4
- package/docs/agent-session-runtime.md +10 -6
- package/docs/attention-compiler.md +89 -8
- package/docs/caveman.md +1 -1
- package/docs/coding-agent-tools.md +1 -1
- package/docs/compaction-and-retry.md +1 -1
- package/docs/compaction-llm.md +2 -0
- package/docs/compaction-observational-memory.md +54 -7
- package/docs/durable-runs.md +46 -3
- package/docs/embeddings.md +9 -0
- package/docs/evaluations.md +5 -0
- package/docs/execution-timeline.md +79 -1
- package/docs/extensions.md +20 -3
- package/docs/guardrails.md +50 -4
- package/docs/hooks.md +282 -0
- package/docs/index.md +37 -15
- package/docs/input-and-prompt-assembly.md +4 -4
- package/docs/instruction-injection.md +1 -0
- package/docs/knowledge-sync.md +4 -0
- package/docs/live-testing.md +3 -1
- package/docs/memory-fabric.md +28 -0
- package/docs/middleware-hooks.md +90 -4
- package/docs/migrate-to-0.9.md +210 -0
- package/docs/migration.md +26 -0
- package/docs/multi-agent-patterns.md +25 -2
- package/docs/node-jsonl-session-store.md +7 -1
- package/docs/observability.md +7 -3
- package/docs/options-index.md +4 -1
- package/docs/policy-and-audit.md +26 -1
- package/docs/prefix-stability-conformance.md +143 -0
- package/docs/provider-caching.md +4 -4
- package/docs/provider-conformance.md +16 -0
- package/docs/provider-packages.md +20 -20
- package/docs/public-contracts.md +3 -2
- package/docs/rag.md +188 -3
- package/docs/release-and-install.md +45 -40
- package/docs/runs-and-usage.md +56 -10
- package/docs/scoped-agent-memory.md +270 -0
- package/docs/scoped-memory.md +138 -0
- package/docs/session-store-conformance.md +1 -2
- package/docs/session-stores.md +17 -17
- package/docs/supervisors.md +32 -12
- package/docs/tools.md +18 -1
- package/docs/wiki.md +4 -2
- package/docs/workflows.md +5 -0
- package/package.json +8 -2
package/docs/middleware-hooks.md
CHANGED
|
@@ -14,7 +14,7 @@ APIs:
|
|
|
14
14
|
|
|
15
15
|
Use middleware hooks when a host wants extension/package code to observe or transform a value at a named runtime boundary.
|
|
16
16
|
|
|
17
|
-
Do not use middleware hooks as a provider adapter, prompt builder, retry policy, compaction strategy, tool dispatcher, permission system, or agent/session runtime.
|
|
17
|
+
Do not use middleware hooks as a provider adapter, prompt builder, retry policy, compaction strategy, tool dispatcher, permission system, or agent/session runtime. Per-turn tool menus use `AgentConfig.toolNarrowing` / `RunOptions.toolNarrowing`, not a middleware hook — see [Tools](tools.md). Run-end decisions use stop hooks (`AgentConfig.stopHooks` / `RunOptions.stopHooks`) — see [Hooks](hooks.md) — not a middleware hook.
|
|
18
18
|
|
|
19
19
|
## Inputs / request
|
|
20
20
|
|
|
@@ -24,6 +24,7 @@ createMiddlewareRegistry(options?: MiddlewareRegistryOptions): MiddlewareRegistr
|
|
|
24
24
|
|
|
25
25
|
Built-in hook names:
|
|
26
26
|
|
|
27
|
+
- `beforeProviderTurn`
|
|
27
28
|
- `provider_request`
|
|
28
29
|
- `input_assembly`
|
|
29
30
|
- `prompt_build`
|
|
@@ -31,6 +32,7 @@ Built-in hook names:
|
|
|
31
32
|
- `tool_call`
|
|
32
33
|
- `tool_result`
|
|
33
34
|
- `retry`
|
|
35
|
+
- `compaction_request`
|
|
34
36
|
- `compaction`
|
|
35
37
|
- `session_start`
|
|
36
38
|
- `session_shutdown`
|
|
@@ -47,7 +49,16 @@ Built-in hook names:
|
|
|
47
49
|
|
|
48
50
|
## Outputs / response / events
|
|
49
51
|
|
|
50
|
-
`run()` returns the transformed value. If no middleware is registered for a hook, `run()` returns the original value. `assembleProviderInput()` calls Phase 5 hooks in this order when middleware is supplied: `input_assembly`, then `context`, then `prompt_build`. The `input_assembly` call is unconditional — it runs after whatever `InputBuilder` produced the messages, so host middleware at that hook cannot be skipped by a custom builder. The agent/session runtime applies configured provider request policies, then invokes `provider_request` once with the `ProviderRequest` before `AIProvider.generate()`, invokes `tool_call` and `tool_result` through `dispatchToolCall()` for complete provider tool calls, invokes `compaction` with `{ context, result }` after
|
|
52
|
+
`run()` returns the transformed value. If no middleware is registered for a hook, `run()` returns the original value. `assembleProviderInput()` calls Phase 5 hooks in this order when middleware is supplied: `input_assembly`, then `context`, then `prompt_build`. The `input_assembly` call is unconditional — it runs after whatever `InputBuilder` produced the messages, so host middleware at that hook cannot be skipped by a custom builder. The agent/session runtime runs `beforeProviderTurn` once per turn after the request is assembled and before any provider-round work, then applies configured provider request policies, then invokes `provider_request` once with the `ProviderRequest` before `AIProvider.generate()`, invokes `tool_call` and `tool_result` through `dispatchToolCall()` for complete provider tool calls, invokes `compaction_request` with the strategy's `CompactionContext` before the strategy runs — only when compaction triggers (manual `session.compact()` and auto-compaction both route through it), so a handler rewrites `entries`, `keepRecentEntries`, `metadata`, or `secrets` for the strategy — then invokes `compaction` with `{ context, result }` after the strategy returns and before the runtime appends its standard compaction entry, and invokes `retry` with `{ context, decision }` before scheduling a provider-turn retry. There is no `provider_response` hook; observing provider output belongs to the provider adapter or subscriber events.
|
|
53
|
+
|
|
54
|
+
Session lifecycle hooks are dispatched by the agent/session runtime, once each:
|
|
55
|
+
|
|
56
|
+
| Hook | When | Payload |
|
|
57
|
+
| --- | --- | --- |
|
|
58
|
+
| `session_start` | First run start of a session, after `agent_started`/`agent_resumed` and before the first provider turn. A session rebuilt from a durable checkpoint is a new runtime session, so it opens again. | `{ sessionId, runId }` |
|
|
59
|
+
| `session_shutdown` | `session.close()`, before every subscriber is closed. Idempotent — calling `close()` twice dispatches once. | `{ sessionId }` |
|
|
60
|
+
|
|
61
|
+
Both are one dispatch per session, never per turn, and both honor the registry `errorPolicy` exactly like every other hook: with `"event"` a throw becomes an `extension_error` event and the run continues, with `"throw"` it surfaces (for `session_start`, `session.run()` rejects; for `session_shutdown`, `close()` rejects after closing subscribers).
|
|
51
62
|
|
|
52
63
|
With default `errorPolicy: "event"`, middleware errors become `extension_error` events when `onError` is provided, and later middleware still runs with the current value. With `errorPolicy: "throw"`, `run()` rejects on the first middleware error.
|
|
53
64
|
|
|
@@ -87,17 +98,90 @@ import type { Extension } from "@arnilo/prism";
|
|
|
87
98
|
export const extension: Extension = {
|
|
88
99
|
name: "demo-middleware",
|
|
89
100
|
setup(api) {
|
|
90
|
-
api.use("session_start", (event) =>
|
|
101
|
+
api.use("session_start", (event) => {
|
|
102
|
+
// Once per session: provision session-scoped state here.
|
|
103
|
+
return event;
|
|
104
|
+
});
|
|
91
105
|
},
|
|
92
106
|
};
|
|
93
107
|
```
|
|
94
108
|
|
|
109
|
+
`session_shutdown` runs on `await session.close()`, which then closes every subscriber:
|
|
110
|
+
|
|
111
|
+
```ts
|
|
112
|
+
const session = createAgent({ model, provider, middleware }).createSession();
|
|
113
|
+
await session.run("hello");
|
|
114
|
+
await session.close(); // session_shutdown middleware once, then every subscriber closes
|
|
115
|
+
```
|
|
116
|
+
|
|
117
|
+
## Pre-compaction rewrite (`compaction_request`)
|
|
118
|
+
|
|
119
|
+
`compaction_request` is the input side of the compaction pair: it runs once per compaction event, right
|
|
120
|
+
after `compaction_started` and before the strategy's `compact()`, and its return value **is** the
|
|
121
|
+
strategy's input. The payload is the same `CompactionContext` the strategy would have received
|
|
122
|
+
(`sessionId`, `entries`, `keepRecentEntries`, `trigger`, `secrets`, `metadata`, `signal`), and the
|
|
123
|
+
post-strategy `compaction` hook then observes that rewritten context — so a subscriber always sees
|
|
124
|
+
what actually compacted.
|
|
125
|
+
|
|
126
|
+
```ts
|
|
127
|
+
import { createMiddlewareRegistry, type CompactionContext } from "@arnilo/prism";
|
|
128
|
+
|
|
129
|
+
const middleware = createMiddlewareRegistry();
|
|
130
|
+
middleware.use<CompactionContext>("compaction_request", (context, next) =>
|
|
131
|
+
next({ ...context, entries: pinCriticalFacts(context.entries) }),
|
|
132
|
+
);
|
|
133
|
+
```
|
|
134
|
+
|
|
135
|
+
Contract:
|
|
136
|
+
|
|
137
|
+
- Only compaction events dispatch it: ordinary turns no-op, even with auto-compaction configured but not triggered.
|
|
138
|
+
- Returning `undefined` without `next()` leaves the original context in place; the registry's `next()`-shape rules apply as everywhere else.
|
|
139
|
+
- A throw follows the registry `errorPolicy`: with `"event"` the error is reported and compaction proceeds on the last committed context; with `"throw"` `session.compact()` (or the run, on the auto path) rejects and no compaction entry is appended.
|
|
140
|
+
- The seam rewrites input, it cannot skip compaction: an empty or invalid entry set is the strategy's own error, not `"do nothing"`.
|
|
141
|
+
- Validation stays where it already was. Entries are redacted on append and the strategy owns its input expectations; the hook is host code inside the same trust boundary as `AgentConfig.compaction`, not a remote endpoint.
|
|
142
|
+
|
|
143
|
+
## No-model turns (`beforeProviderTurn`)
|
|
144
|
+
|
|
145
|
+
`beforeProviderTurn` lets the host answer a turn from data it already has — teaching empty states, canned flows, deterministic lookups — without any provider request. The payload is `BeforeProviderTurnPayload` (`sessionId`, `runId`, `turn`, `userText`) and middleware returns it unchanged or with `answer: DeterministicTurnAnswer` set:
|
|
146
|
+
|
|
147
|
+
```ts
|
|
148
|
+
export interface DeterministicTurnAnswer {
|
|
149
|
+
readonly content: readonly ContentBlock[];
|
|
150
|
+
readonly provenance: { readonly middleware: string };
|
|
151
|
+
}
|
|
152
|
+
```
|
|
153
|
+
|
|
154
|
+
```ts
|
|
155
|
+
import { createAgent, createMiddlewareRegistry, type BeforeProviderTurnPayload } from "@arnilo/prism";
|
|
156
|
+
|
|
157
|
+
const DESK_ANSWERS = new Map([["what can you do?", "I answer from local records; ask about an order id."]]);
|
|
158
|
+
const middleware = createMiddlewareRegistry();
|
|
159
|
+
middleware.use<BeforeProviderTurnPayload>("beforeProviderTurn", (payload, next) => {
|
|
160
|
+
const text = DESK_ANSWERS.get(payload.userText);
|
|
161
|
+
return text ? { ...payload, answer: { content: [{ type: "text", text }], provenance: { middleware: "desk" } } } : next(payload);
|
|
162
|
+
});
|
|
163
|
+
|
|
164
|
+
const session = createAgent({ model, provider, middleware }).createSession();
|
|
165
|
+
await session.run("what can you do?"); // no provider call; assistant message recorded
|
|
166
|
+
```
|
|
167
|
+
|
|
168
|
+
Contract:
|
|
169
|
+
|
|
170
|
+
- Returning the payload without `answer` (or returning `undefined`) sends the turn to the provider exactly as if the hook were absent.
|
|
171
|
+
- `answer.provenance.middleware` is mandatory and validated as a bounded id (1–64 chars: letters, digits, `.` `_` `:` `-`); a deterministic turn can never masquerade as model output.
|
|
172
|
+
- `answer.content` accepts assistant-visible content blocks (`text`, `image`, `audio`, `file`, `document`, `video`, `thinking`). Tool-call blocks are rejected — no provider ran to authorize a call — and an empty block array throws `DeterministicTurnError` (`ERR_PRISM_DETERMINISTIC_TURN`), failing the run closed instead of falling through to the provider.
|
|
173
|
+
- Content passes the same output guardrails as provider output and is charged against `maxResponseBytes`, but the turn records no usage: usage is absent, never zero, and the run timeline shows a `deterministic` step named after the answering middleware.
|
|
174
|
+
- Provenance persists: the assistant message carries `metadata.deterministic = { middleware }`, so a transcript loaded back from any session store still proves the turn had no model behind it. `summarizeTimeline()`/`summarizeSession()` report `turns: { model, deterministic }`, and `createDeterministicTurnScorer()` (from `@arnilo/prism-core/governance/evals`) grades a trajectory for no-model coverage — failing a turn that both answered deterministically and still issued a provider request.
|
|
175
|
+
|
|
95
176
|
## Extension and configuration notes
|
|
96
177
|
|
|
97
178
|
- Middleware registration is explicit through `createMiddlewareRegistry()` or `ExtensionAPI.use()`.
|
|
98
179
|
- `provider_request` middleware sees generic `ProviderRequest.options` after request policies have run; do not add secrets unless a redactor/policy secret list covers that boundary.
|
|
99
180
|
- Middleware runs only when the host/runtime calls `run()` or passes the registry to a helper that documents a call site.
|
|
181
|
+
- `session_start`/`session_shutdown` dispatch only when the host passes its registry to `AgentConfig.middleware`; a session that never runs never starts, and one the host never closes never shuts down. Closing an idle session is fine — the hook still does not fire twice. There is no `session_start` for a session that only calls `compact()` or `contextMeter()`.
|
|
182
|
+
- `beforeProviderTurn` runs only for turns that reach the provider boundary; a turn already ended by a run limit, host turn policy, or durable suspension never reaches it, and host middleware is trusted code — it must not use the hook to bypass `RunLimits` or guardrails.
|
|
100
183
|
- `compaction` middleware may adjust the compaction result summary/data, but runtime still owns session store append ordering and branch parent ids.
|
|
184
|
+
- `compaction_request` runs once per compaction event with the strategy's context as payload; the runtime still redacts the summary, appends the compaction entry, and rebuilds history. Neither hook can skip compaction (an empty entry set is the strategy's error), and neither runs on ordinary turns.
|
|
101
185
|
- `retry` middleware may stop retrying or adjust delay, but runtime still owns retry event emission, abort-aware waiting, and provider-turn boundaries.
|
|
102
186
|
- The registry does not discover packages, read manifests, load config, call providers, execute tools, read resources, or start sessions.
|
|
103
187
|
- Hosts may pass a middleware registry into `createExtensionKernel({ middleware })` to share it with direct host code.
|
|
@@ -112,12 +196,14 @@ export const extension: Extension = {
|
|
|
112
196
|
|
|
113
197
|
## Related APIs
|
|
114
198
|
|
|
199
|
+
- [Middlewares vs restore hooks](durable-runs.md#restore-hooks-all-or-nothing): middleware transforms payloads at named boundaries; `restoreHooks` restore external state before a durable resume and are not middleware.
|
|
115
200
|
- [Extension kernel and event bus](extensions.md): `ExtensionAPI.use()` and shared error policy.
|
|
116
201
|
- [Contribution registries](contribution-registries.md): direct contribution registration separate from middleware.
|
|
117
202
|
- [Agent/session runtime](agent-session-runtime.md): provider request policy/middleware timing, bounded tool loop call site for `tool_call`/`tool_result` hooks, and runtime call sites for `compaction` and `retry`.
|
|
118
203
|
- [Tools](tools.md): tool dispatch behavior that runs `tool_call` and `tool_result` hooks.
|
|
204
|
+
- [Hooks](hooks.md): the hook model, the Claude Code / Codex event map, the `hooks.json` adapter, and the run-end stop hooks that are separate from payload-transforming middleware.
|
|
119
205
|
- [Input and prompt assembly](input-and-prompt-assembly.md): `input_assembly` and `prompt_build` helper call sites.
|
|
120
|
-
- [Compaction and retry policies](compaction-and-retry.md): compaction/retry middleware payloads and runtime timing.
|
|
206
|
+
- [Compaction and retry policies](compaction-and-retry.md): compaction/retry middleware payloads and runtime timing, including the pre-strategy `compaction_request` seam.
|
|
121
207
|
- [Context and skills](context-and-skills.md): `context` helper call site.
|
|
122
208
|
- [Observability](observability.md): optional OpenTelemetry adapter over `AgentEvent` streams.
|
|
123
209
|
- [Public contracts](public-contracts.md): provider, tool, context, session, and extension contracts that runtimes can pass through hooks.
|
|
@@ -0,0 +1,210 @@
|
|
|
1
|
+
# Migrate Prism 0.8 to 0.9
|
|
2
|
+
|
|
3
|
+
> **Status: 0.9.0** (attention budget axes, turn traces, cache-stable disclosure, per-turn tool narrowing, guardrail packs, background agents, checkpoint metadata, session search, deterministic turns, shared work scopes).
|
|
4
|
+
|
|
5
|
+
This document details migration steps, behavioral changes, and compatibility notes for upgrading from Prism 0.8.0 to 0.9.0.
|
|
6
|
+
|
|
7
|
+
0.9.0 is a lockstep minor for all **eleven** publishable packages. Node `>=22` stays the floor. **Nothing was removed**: no import path moved, no export was dropped, and every new surface defaults to 0.8 behavior — a host that only moves its dependency ranges keeps 0.8 request bytes, stores, and tool lists. The four deltas below sit inside existing surfaces, so they are readable without opting into anything.
|
|
8
|
+
|
|
9
|
+
---
|
|
10
|
+
|
|
11
|
+
## Behavior changes inside existing surfaces
|
|
12
|
+
|
|
13
|
+
### 1. A limit death delivers three records, and only the last one is terminal
|
|
14
|
+
|
|
15
|
+
`run_limit_exceeded` was treated as terminal by the in-memory, NATS, and Postgres event sources and by AG-UI replay, so a consumer that stopped at the first breach record ended one record early — before the `budget_exhausted` attribution and before the run's terminal `error`. The terminal set is now exactly `agent_finished`, `agent_denied`, and `error`, decided by one exported predicate that every stream-ending site shares.
|
|
16
|
+
|
|
17
|
+
```ts
|
|
18
|
+
// before — the stream could end on the breach record
|
|
19
|
+
for await (const item of source.subscribe({ ... })) {
|
|
20
|
+
if (item.record.type === "run_limit_exceeded") break; // missed budget_exhausted and the error
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
// after — the breach and its attribution are not terminal; the error is
|
|
24
|
+
for await (const item of source.subscribe({ ... })) {
|
|
25
|
+
if (isTerminalAgentEventType(item.record.type)) break; // ends on the run's error
|
|
26
|
+
}
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
**Migration actions**
|
|
30
|
+
- Keep reading past `run_limit_exceeded` and `budget_exhausted`; the stream ends on `error`. A consumer that wants the attribution reads until `isTerminalAgentEventType(type)` is `true` (or the iterator ends).
|
|
31
|
+
- No configuration, no flag: this is the shipped delivery contract for pages, subscriptions, and replays. See [Agent events § Durable AgentEventSource](agent-events.md#durable-agenteventsource).
|
|
32
|
+
|
|
33
|
+
### 2. `provider_turn_finished` carries stop reason, budgets, tools, and cache metrics
|
|
34
|
+
|
|
35
|
+
The turn event gains attributed metadata: `stopReason` from one closed taxonomy (`end_turn`, `tool_calls`, `max_output_tokens`, `content_filter`, `abort`, `provider_error`, `unknown`), a `budgets` snapshot (`inputTokens?`, `inputCap?`, `runInputBudget?`, `runInputUsed`, `turns`, `maxTurns`), the effective tool menu as counts plus `tools.idsHash`, and provider-reported `cache` counts (`cacheReadTokens?`, `cacheWriteTokens?`, `hitRate?`). `agent_finished` carries the run-level `finishReason`/`stopDetail`, `AgentRunResult.stopReason` names host-policy and loop-ceiling stops, and the execution timeline adds `turns[i].stopReason` plus `timeline.exhaustion`.
|
|
36
|
+
|
|
37
|
+
```ts
|
|
38
|
+
source.subscribe({ ... }); // each provider_turn_finished.metadata:
|
|
39
|
+
// { latencyMs, stopReason: "tool_calls", budgets: { runInputUsed: 43_000, turns: 3, maxTurns: 16 }, tools: { count: 7, idsHash: "sha256:…" } }
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
**Migration actions**
|
|
43
|
+
- Consumers that deep-equal `metadata` (or reject unknown keys) must allow the new fields; consumers that read specific keys are unaffected.
|
|
44
|
+
- Read `metadata.cache` only when present — unknown cache usage stays absent rather than zero-filled.
|
|
45
|
+
|
|
46
|
+
### 3. Progressive disclosure is cache-stable
|
|
47
|
+
|
|
48
|
+
Late-expanding context (skill bodies, deferred tool schemas, loaded references) now lands at cache-stable positions: the request tail, or an explicit documented invalidation of the segment that changed. The default group order and the catalogs' slot are unchanged, so a host that never loads late context sends the same bytes as 0.8; hosts that do get append-only growth instead of a rewritten prefix.
|
|
49
|
+
|
|
50
|
+
```ts
|
|
51
|
+
// assert it against your own assembly (fixture provider, network-free, no keys)
|
|
52
|
+
await runPrefixStabilityConformance({ agent, minContinuity: 0.95 }); // ≥95% shared serialized prefix per turn
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
Sizing: measured 100% / 95.7% / 95.8% shared prefix on the padded fixture for three consecutive requests; `minContinuity` defaults to `0.95` and is checked over messages **and** tool schemas. Cache reads/writes and per-turn hit rate are now recorded on usage records and `provider_turn_finished.metadata.cache`. See [Prefix stability conformance](prefix-stability-conformance.md) and [Provider caching](provider-caching.md).
|
|
56
|
+
|
|
57
|
+
### 4. A provider that reports no usage is charged a labeled estimate
|
|
58
|
+
|
|
59
|
+
A usage-less provider used to contribute zero tokens. `AgentConfig.usageEstimation` now defaults to `"fallback"`: one labeled `TokenEstimate` is recorded at the existing usage seam, and the label survives everywhere the number goes.
|
|
60
|
+
|
|
61
|
+
```ts
|
|
62
|
+
const meter = session.contextMeter();
|
|
63
|
+
// { inputTokens: 43_000, source: "estimated", inputCap: 200_000, runInputBudget: 500_000, usedRatio: 0.215 }
|
|
64
|
+
const estimate = estimateMessageTokens(messages, "claude-sonnet-4.5"); // { tokens, confidence: "medium" | "low", … }
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
**Migration actions**
|
|
68
|
+
- Billing or reporting code must read the `estimated` flag (and `confidence`) rather than treating every usage row as provider truth; reported usage always wins and is never overwritten.
|
|
69
|
+
- Set `usageEstimation: "off"` to keep the 0.8 zero-for-no-usage behavior. Estimates charge the token counters for usage-less vendors but never a price, so a configured `maxCost` stays fail-closed.
|
|
70
|
+
|
|
71
|
+
---
|
|
72
|
+
|
|
73
|
+
## Additive surfaces (inert unless wired)
|
|
74
|
+
|
|
75
|
+
### 5. Attention budget axes and durable folding
|
|
76
|
+
|
|
77
|
+
`attentionCompiler.trigger` replaces the single `triggerRatio` gate with one axis, a predicate, or an any-of array: `{ kind: "input_ratio", ratio }` (the legacy axis), `{ kind: "run_input_ratio", ratio }` (fires against `RunLimits.maxInputTokens` — the case that used to be inert when the run cap sat below the model window), `{ kind: "token_floor", tokens }`, and a predicate function. Omitted, `triggerRatio` (default `0.75`) is the only axis and behavior is byte-identical to 0.8.
|
|
78
|
+
|
|
79
|
+
```ts
|
|
80
|
+
const attention = createAttentionCompiler(
|
|
81
|
+
{ trigger: [{ kind: "run_input_ratio", ratio: 0.75 }, { kind: "token_floor", tokens: 120_000 }], durable: true },
|
|
82
|
+
{ model, runInputBudget: limits.maxInputTokens },
|
|
83
|
+
);
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
Sizing: one session-store write per fold (not per turn); folding stays default-off, and `durable: true` requires a checkpoint store (`runState`) or the run throws `AgentRunStateError` before its first provider turn. See [Attention compiler](attention-compiler.md).
|
|
87
|
+
|
|
88
|
+
### 6. Per-turn tool narrowing
|
|
89
|
+
|
|
90
|
+
`AgentConfig.toolNarrowing` / `RunOptions.toolNarrowing` (run wins) is a host callback invoked before each provider turn: it receives `{ turn, lastAssistantText?, toolIds }` and must return a subset of the run grant. Extra or unknown names are dropped — the runtime emits `tool_narrowing_clamped` with the dropped names — and a throw fails the turn instead of sending a partial schema.
|
|
91
|
+
|
|
92
|
+
```ts
|
|
93
|
+
await session.run("fix the failing test", {
|
|
94
|
+
toolNarrowing: async ({ turn, toolIds }) => (turn > 2 ? toolIds.filter((id) => id === "read" || id === "edit") : toolIds),
|
|
95
|
+
});
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
Sizing and cache cost: changing the toolset rewrites provider schemas, so pair narrowing with tool search or deferred disclosure (where the tail contract above applies), and read menu identity from `provider_turn_finished.metadata.tools.idsHash` — identical consecutive subsets keep the same hash. Absent callback: 0.8 menu, byte-identical. See [Tools](tools.md).
|
|
99
|
+
|
|
100
|
+
### 7. Guardrail packs
|
|
101
|
+
|
|
102
|
+
`AgentSessionConfig.guardrailPacks` (or `compileGuardrailPacks(refs)` for hosts that dispatch tools directly) compiles declarative, restrictive-only rule sets onto the tool stages once per session. Four built-ins ship: `coding-standard` (`no-unrelated-file-edits`, `no-test-rewrites`), `destructive-commands`, `validation-respect`, and `secrets-hygiene`. Every pack ships a trajectory scorer (`createGuardrailPackScorer`) so enforcement can be graded, and pack denials are attributed as `pack:<pack>/<rule>`.
|
|
103
|
+
|
|
104
|
+
```ts
|
|
105
|
+
const agent = createAgent({ /* … */, session: { guardrailPacks: ["secrets-hygiene", "destructive-commands"] } });
|
|
106
|
+
// compiled from existing seams: interruptBeforeTool, the extension kernel, enforceExecutionPolicy
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
Sizing: `guardrailPacks` accepts at most 8 packs and 64 rules per pack; containment in `coding-standard` is lexical (`options.roots` defaults to `[process.cwd()]`, symlinks are not resolved), so an `ExecutionPolicy` stays the hard boundary. See [Guardrails](guardrails.md#guardrail-packs).
|
|
110
|
+
|
|
111
|
+
### 8. Background (session-lifetime) child agents and child-event passthrough
|
|
112
|
+
|
|
113
|
+
`delegate` / `delegateAsync` / `spawn_agent` accept `lifetime: "session"`, `report: "on-complete" | "milestones" | "stream"`, `milestone`, and `budgetShare`; a host `SupervisorChild.policy` sets the ceiling and a model request can only narrow it (report is clamped, `everyTurns` can only be raised, share takes the lower value, session lifetime must be host-enabled). Session-lifetime children survive caller turns until `cancel_agent` / `cancel(delegationId)`. New events: `child_milestone`, `child_failed` (with the plan-087 `RunLimitBreach` attribution), `delegation_child_events_capped`, `delegation_child_events_coalesced`.
|
|
114
|
+
|
|
115
|
+
```ts
|
|
116
|
+
const { delegationId } = await supervisor.delegateAsync({ childId: "researcher", input: "survey the repo", lifetime: "session", report: "milestones", milestone: { everyTurns: 3 }, budgetShare: 0.25 });
|
|
117
|
+
supervisor.subscribe(); // … child_milestone … child_failed (on a limit or error)
|
|
118
|
+
```
|
|
119
|
+
|
|
120
|
+
Sizing: child events per delegation 256/4096, child-event bytes 32 KiB/256 KiB, child events per second 10/1000 (default/hard, per child) — 10/s is trivial for a UI, raise it only for a child whose tool events are the UI. Exceeding rate coalesces into one `delegation_child_events_coalesced` marker with the dropped count (never throws). Defaults are exactly 0.8: `lifetime: "task"`, `report: "on-complete"`, no milestone, no share, no subscription. See [Supervisors](supervisors.md) and [Multi-agent patterns](multi-agent-patterns.md).
|
|
121
|
+
|
|
122
|
+
### 9. Checkpoint sidecar metadata and cross-layer restore hooks
|
|
123
|
+
|
|
124
|
+
Hosts attach an opaque, redacted metadata map (≤4 KiB) to every checkpoint record — git commit, document version, workspace fingerprint — without charging `maxStateBytes`, and register restore hooks that put each recorded layer back before a resume claims the run.
|
|
125
|
+
|
|
126
|
+
```ts
|
|
127
|
+
createAgentRunLifecycle({ runState: { checkpointMetadata: () => ({ gitCommit: head, docVersion: "v12" }) },
|
|
128
|
+
restoreHooks: [async ({ metadata, signal }) => { await checkout(metadata.gitCommit, { signal }); }] });
|
|
129
|
+
```
|
|
130
|
+
|
|
131
|
+
Sizing: `MAX_AGENT_RUN_METADATA_BYTES` is 4 KiB (fixed, no override), the whole map is redacted unconditionally (no public-key exemption), and each hook has a 10-second default timeout (`DEFAULT_CHECKPOINT_RESTORE_TIMEOUT_MS`) with sequential execution; any hook failure aborts the restore with `{ hook, error }`. Legacy records without metadata read as `undefined`, and an oversize or non-string map reads as absent rather than failing a resume — unused, behavior is unchanged. See [Durable runs](durable-runs.md).
|
|
132
|
+
|
|
133
|
+
### 10. Bounded workspace session search
|
|
134
|
+
|
|
135
|
+
`SessionStore.searchSessions?(query)` is part of the store contract: filters by workspace root (`metadata.workspaceRoot`), time, provider/model, label/summary, entry kind, ownership, and an optional full-text `query`; hits carry `sessionId`, optional `leafId`, and the matched entry pointer (`entryId`, `runId`, 1-based `turn`, store `score`, bounded `snippet`) — never credentials or whole transcripts. SQLite FTS5 and the Postgres `tsvector` column are maintained additively at append time (migration 004, no background job); memory and JSONL stores scan linearly through the shared `searchLinearSessions` matcher.
|
|
136
|
+
|
|
137
|
+
```ts
|
|
138
|
+
const page = await store.searchSessions!({ workspaceRoot: "/repo", query: "flake", kind: "any", limit: 20 });
|
|
139
|
+
const { page } = await searchSessions({ bySession, leafBySession, query: "flake" }); // linear caps apply
|
|
140
|
+
```
|
|
141
|
+
|
|
142
|
+
Sizing: on the 100k-turn fixture the index is 18.8% of transcript page bytes (stored tool output is never indexed) and query p95 is 38 ms against the 100 ms ceiling; unindexed stores are O(corpus) per query and accept `maxLinearSessions` / `maxLinearEntries` / `maxLinearBytes` overrides bounded by their hard caps. See [Session stores](session-stores.md) and `examples/session-search.ts`.
|
|
143
|
+
|
|
144
|
+
### 11. Deterministic no-model turns
|
|
145
|
+
|
|
146
|
+
The `beforeProviderTurn` middleware hook receives `BeforeProviderTurnPayload` (`sessionId`, `runId`, `turn`, `userText`) and may answer the turn from host data by returning a `DeterministicTurnAnswer` — no provider request, zero model cost, no hallucination surface. Answers are validated (`validateDeterministicTurnAnswer` / `resolveDeterministicTurn`), the turn is recorded as `deterministic` on the timeline and in usage, and `DeterministicTurnProvenance` names the middleware that produced it. `createDeterministicTurnScorer` grades the behavior in evals.
|
|
147
|
+
|
|
148
|
+
```ts
|
|
149
|
+
middleware.use<BeforeProviderTurnPayload>("beforeProviderTurn", async (payload, next) =>
|
|
150
|
+
payload.userText.startsWith("status:")
|
|
151
|
+
? { ...payload, answer: { text: await hostStatus(payload.userText), provenance: { middleware: "status" } } }
|
|
152
|
+
: next(payload));
|
|
153
|
+
```
|
|
154
|
+
|
|
155
|
+
Sizing: no provider turn, no usage beyond a zero-cost record; the hook runs only for turns that reach the provider boundary (a turn already ended by a run limit, host turn policy, or suspension never reaches it), and host middleware is trusted code — it must not use the hook to bypass `RunLimits` or guardrails. See [Middleware hooks](middleware-hooks.md#no-model-turns-beforeproviderturn).
|
|
156
|
+
|
|
157
|
+
### 12. Shared work scopes for observational memory
|
|
158
|
+
|
|
159
|
+
`om.attach(session, { sharedScopes })` lets several sessions contribute to and read one observational-memory scope under explicit owner grants (`controller.grant(scopeId, principalIds)` / `revoke`). Only ids bound to that exact scope are shared; the owner branch is the only grant authority; every resolve re-reads it, so revocation lands on the next read, and `onScopeAccess` audits each grant/denial.
|
|
160
|
+
|
|
161
|
+
```ts
|
|
162
|
+
om.attach(session, {
|
|
163
|
+
appendEntry: (entry, options) => store.append(entry, options),
|
|
164
|
+
sharedScopes: { "build-42": { ownerSessionId, entries: (id) => store.list(id) } },
|
|
165
|
+
onScopeAccess: (event) => audit.info("om.scope.access", event),
|
|
166
|
+
});
|
|
167
|
+
```
|
|
168
|
+
|
|
169
|
+
Sizing: one local write per append (flush stays local — no second writer on a branch); one branch read plus fold per participating branch per context resolve and per shared-scope recall, not per observation; 1,024 principals per scope and 256-character principal ids, on top of the existing scope caps (256 scopes, depth 8, 4,096 binds, 512-character labels). Session-private scopes stay the default: with no `sharedScopes` configured, behavior is byte-identical to 0.8. See [Compaction and observational memory](compaction-observational-memory.md#shared-work-scopes-opt-in) and `examples/shared-work-scope.ts`.
|
|
170
|
+
|
|
171
|
+
### 13. Retrieval revocation and a zero-service reranker
|
|
172
|
+
|
|
173
|
+
Deletion and revocation propagate through derived artifacts: `createDeletionPropagator` deletes vector rows and then hands the invalidation set (`collectInvalidationIds` / `listInvalidatedIds`) to host handlers, and `repointSource` / `retireWikiSources` (plus the `createWikiDeletionHandler` / `createWikiRepointHandler` helpers) keep wiki pages and summaries consistent. `createAccessRecheck` rechecks governed sources per query and reports denials through an audit sink. Reranking no longer needs a research project: `resolveReranker({ kind: "local" })` / `createLocalReranker()` runs an in-process cross-encoder behind the `LocalRerankRuntime` seam, with the same `runRerankerConformance` contract as every other reranker; TEI and hosted adapters are unchanged.
|
|
174
|
+
|
|
175
|
+
```ts
|
|
176
|
+
const reranker = resolveReranker({ kind: "local" }); // Xenova/bge-reranker-base via a host-owned runtime
|
|
177
|
+
const access = createAccessRecheck({ store, onDenied: audit.warn });
|
|
178
|
+
const propagator = createDeletionPropagator({ store, handlers: [createRagDeletionHandler(vectors), createWikiDeletionHandler(wiki)] });
|
|
179
|
+
```
|
|
180
|
+
|
|
181
|
+
Sizing: the local reranker declares no inference dependency — the built-in loader resolves `@huggingface/transformers` at first use (pass `runtime` to inject your own, or `allowRemoteModels: false` for a no-network posture after the model is cached); propagation and repoint walks are bounded by `HARD_PROPAGATION_EDGES` / `HARD_REPOINT_RECORDS` and fail closed past them. See [RAG](rag.md#local-reranker), [Embeddings](embeddings.md), and [Knowledge sync](knowledge-sync.md).
|
|
182
|
+
|
|
183
|
+
---
|
|
184
|
+
|
|
185
|
+
## Operator / release honesty (not a host API break)
|
|
186
|
+
|
|
187
|
+
- **Compatibility baseline regenerated**: `+119` public names across `@arnilo/prism` (+49), `@arnilo/prism-memory` (+60), and `@arnilo/prism-core` (+10); **zero removals** and zero renames. One declaration change is consumer-visible at the type level: the `recordUsage` callback accepted by `generateProviderTurn` / `generateWithRetry` now returns `Promise<Usage | undefined>` instead of `Promise<void>`, so a hand-written callback that returned nothing must return the usage (or `undefined`).
|
|
188
|
+
- **Budgets rebaselined with recorded reasons**: root packed/unpacked/file count, per-package export ceilings, and the non-null assertion ratchet carry the measured 0.9.0 values and the plans that moved them.
|
|
189
|
+
- **This-tree Postgres evidence**: `release:gate` reports the durable Postgres surface as pass only when `scripts/postgres-evidence.json` matches the current `git rev-parse HEAD`; a stale phase baseline is blocked rather than inherited.
|
|
190
|
+
- **Version literals agree** across all eleven manifests, the lockfile, `src/index.ts`, the docs banner, the release workflow tag lists, and the generated package-truth artifact (`scripts/version-literal-gate.test.mjs`).
|
|
191
|
+
|
|
192
|
+
## Upgrade steps
|
|
193
|
+
|
|
194
|
+
1. Bump every `@arnilo/*` dependency and peer to `^0.9.0` (all **eleven** manifests cut together; a range that only *satisfies* 0.9.0 is refused by the release gate). The published predecessor is 0.8.0.
|
|
195
|
+
2. Build and run the host suite. No import path moved, so compile errors should be limited to the `recordUsage` callback return type above and to code that deep-equals `provider_turn_finished.metadata`.
|
|
196
|
+
3. Re-read §1–§4 if the host tails durable agent events, parses provider-turn metadata, uses progressive disclosure or prompt caching, or bills usage for vendors that report no usage.
|
|
197
|
+
4. Adopt §5–§13 only where the host wants the new surfaces. Omitted, request bytes, stores, and tool lists stay 0.8.
|
|
198
|
+
5. Run the new migration 004 on SQLite/Postgres stores if the host wants indexed session search; existing tables and columns are untouched, and 0.8 stores open unchanged.
|
|
199
|
+
6. Optional: `PRISM_TEST_POSTGRES_URL=… npm run test:postgres` then `npm run release:gate` to reproduce this-tree Postgres evidence.
|
|
200
|
+
|
|
201
|
+
## Rollback
|
|
202
|
+
|
|
203
|
+
Pin the previous published line: `@arnilo/prism@0.8.0` and its siblings, exact pins per package. Session, checkpoint, and ledger schema are unchanged across 0.8.0 → 0.9.0 apart from the additive session-search index (migration 004), which a 0.8.0 process never reads; observability rows written under 0.9.0 carry extra metadata fields that 0.8.0 ignores. Revert host config to 0.8.0 semantics by dropping `attentionCompiler.trigger` / `durable`, `toolNarrowing`, `guardrailPacks`, `usageEstimation`, `checkpointMetadata` / `restoreHooks`, `sharedScopes`, and the session-lifetime child options — every default already matches 0.8.0.
|
|
204
|
+
|
|
205
|
+
## Related APIs
|
|
206
|
+
|
|
207
|
+
- [Migration guide](migration.md): the era index of migration cuts with replacement tables and rollback notes.
|
|
208
|
+
- [Migrate Prism 0.7 to 0.8](migrate-to-0.8.md): work-family import map, messaging channels, connected apps, durable runs.
|
|
209
|
+
- [Release and install](release-and-install.md): packed surfaces, install rules, support matrix, and the offline test budget.
|
|
210
|
+
- [Agent events](agent-events.md), [Runs and usage](runs-and-usage.md), [Observability](observability.md), [Tools](tools.md), [Guardrails](guardrails.md), [Supervisors](supervisors.md), [Session stores](session-stores.md): owning pages for the 0.9.0 additions.
|
package/docs/migration.md
CHANGED
|
@@ -1,5 +1,31 @@
|
|
|
1
1
|
# Migration guide
|
|
2
2
|
|
|
3
|
+
## 0.9.0 → 0.10.0 (hook lifecycle completion, scoped agent memory)
|
|
4
|
+
|
|
5
|
+
**Prism 0.10.0 is a lockstep minor for all twelve publishable packages** — `@arnilo/prism-hooks` is new. Node `>=22` stays the floor. Nothing was removed: no import path moved and no export was dropped (compat baseline: +47 names, zero removals, zero renames). Scoped memory is a new opt-in subpath that stays inert until a host constructs a policy.
|
|
6
|
+
|
|
7
|
+
What a 0.9.0 host must check before upgrading:
|
|
8
|
+
|
|
9
|
+
- **One type-level addition: `AgentSession.close(): Promise<void>`.** Hosts that implement or proxy the interface (not only consume it) must add a `close()`; `async close() {}` satisfies the type, and a proxy should forward to the wrapped session so `session_shutdown` fires once. The in-repo precedent is the observational-memory proxy in `@arnilo/prism-memory`, which forwards it.
|
|
10
|
+
- **`session_start` and `session_shutdown` are now emitted.** Both names were declared in 0.9.0 but had no call site, so extension handlers registered against them never ran. `session_start` fires once per session at the first turn of the first run (durable resumes included); `session_shutdown` fires from `session.close()` and is idempotent — a host that never calls `close()` never sees it.
|
|
11
|
+
- **`hook_limit` is a new `AgentFinishReason`** (and a `StoredAgentRunState.stopReason`). Exhaustive switches over finish reasons must handle it; a `hook_limit` stop stays resumable only under `checkpointPolicy: "every-turn"`.
|
|
12
|
+
- **Stop hooks are bounded by default.** `RunLimits.maxStopContinuations` defaults to 3 (`0` disables continuation, `null` uncaps), and only a run that registers stop hooks can reach the limit.
|
|
13
|
+
- **`compaction_request` runs before the compaction strategy** when a handler is registered (entries and budget are rewritable); with no handler the compaction path is unchanged.
|
|
14
|
+
- **Additive, inert by default:** `AgentConfig.stopHooks` / `RunOptions.stopHooks` and `ExtensionAPI.registerStopHook()`, `forwardAgentEvents()`, the `@arnilo/prism-hooks` adapter, and the whole `@arnilo/prism-memory/scoped` surface (reviewer, promotion ladder, GC proposals, bounded facts block, approval gate, audit mirror, eval harness).
|
|
15
|
+
|
|
16
|
+
## 0.8.0 → 0.9.0 (attention budget axes, turn traces, tool narrowing, guardrail packs, background agents, session search, deterministic turns, shared scopes)
|
|
17
|
+
|
|
18
|
+
**Prism 0.9.0 is a lockstep minor for all eleven publishable packages.** Node `>=22` stays the floor. Nothing was removed: no import path moved, no export was dropped, and every new surface defaults to 0.8 behavior. The full guide — the four deltas inside existing surfaces, every new option with its sizing line, upgrade steps, and rollback — is [migrate-to-0.9.md](migrate-to-0.9.md).
|
|
19
|
+
|
|
20
|
+
What a 0.8.0 host must check before upgrading:
|
|
21
|
+
|
|
22
|
+
- **A limit death no longer ends a stream early.** `run_limit_exceeded` and `budget_exhausted` are not terminal; keep reading until `error` (or `isTerminalAgentEventType(type)` is true) to see the breach, its attribution, and the run's outcome in order.
|
|
23
|
+
- **`provider_turn_finished` metadata grew** (`stopReason`, `budgets`, `tools`, `cache`), and `agent_finished` now carries `finishReason` / `stopDetail`; consumers that deep-equal `metadata` must allow the new fields.
|
|
24
|
+
- **`AgentConfig.usageEstimation` defaults to `"fallback"`**: a provider that reports no usage is charged one labeled estimate (`estimated: true` + `confidence`) instead of zero. Set `"off"` for the old behavior; billing code must read the label.
|
|
25
|
+
- **Progressive disclosure is cache-stable**: skill bodies and deferred schemas append at the tail instead of rewriting the prefix. Defaults keep 0.8 bytes for hosts that never load late context.
|
|
26
|
+
- **One type-level change**: the `recordUsage` callback of `generateProviderTurn` / `generateWithRetry` returns `Promise<Usage | undefined>` instead of `Promise<void>`.
|
|
27
|
+
- **Additive, inert by default**: attention `trigger` axes and `durable` folding, per-turn `toolNarrowing`, `guardrailPacks`, session-lifetime child agents with child-event passthrough, `checkpointMetadata` / `restoreHooks`, `searchSessions`, `beforeProviderTurn` deterministic turns, observability shared work scopes, deletion propagation, and the local reranker. One additive migration (004) adds the session-search index; no existing table or column changes.
|
|
28
|
+
|
|
3
29
|
## 0.7.0 → 0.8.0 (messaging channels, connected apps, work family, durable runs)
|
|
4
30
|
|
|
5
31
|
**Prism 0.8.0 is a lockstep minor for all eleven publishable packages.** Node `>=22` stays the floor. The only import-map break is `@arnilo/prism-office` → `@arnilo/prism-work` (plus the work/document-reader subpath moves). A host that never imported those paths upgrades by moving every `@arnilo/*` dependency and peer to `^0.8.0`. The full guide — per-item actions, opt-in activation, and rollback — is [migrate-to-0.8.md](migrate-to-0.8.md).
|
|
@@ -17,7 +17,7 @@ Maps five Prism answers for "more than one agent" onto one decision table. All f
|
|
|
17
17
|
| In-session handoff | One host, one ongoing conversation; the model decides **when** to transfer; specialists are alternate definitions of the same app | One continuous transcript chain (same store, session id, `leafId`) | Same session scope; give the specialist its own identity via its definition (`AgentConfig.identity` / `RunOptions.identity`) | Attribution is per-run: each `session.run()`'s events/result belong to the active definition — record the swap in host bookkeeping; no `delegated_agent_step` event exists for in-process swaps |
|
|
18
18
|
| Hierarchical crew | A goal requires dynamic decomposition by a manager LLM, parallel execution by role specialists, host aggregation, and conditional validation/revision loop | Workflow DAG execution — each specialist executes a bounded child task session; final deliverable returns to host | Workflow tenant/ownership scopes propagate; specialists activate only their own narrowed `tools` | Workflow node events (`node_started`/`node_finished`/`agent_event`); task attribution per role in the aggregated deliverable |
|
|
19
19
|
| Supervisor delegation | Host code dynamically selects a bounded child run | Separate runs; child result returns to the host | Parent identity/effectStore propagate; child factories receive derived resource/thread ids and AND-composed permission | Dedicated `delegation_started/finished/rejected/error` events, projectable through observability `handleDelegation()`; opt-in `delegation_child_event` passthrough |
|
|
20
|
-
| In-process spawn tool | Parent model needs an allow-listed child as a non-exclusive tool call | Separate runs; sync result returns through `spawn_agent`, async handle joins through `wait_agent` | Host owns catalog, tools, scopes, limits, and local handles; schema accepts only child ID/input/thread ID/mode | Same supervisor `delegation_*` events |
|
|
20
|
+
| In-process spawn tool | Parent model needs an allow-listed child as a non-exclusive tool call | Separate runs; sync result returns through `spawn_agent`, async handle joins through `wait_agent` | Host owns catalog, tools, scopes, limits, and local handles; schema accepts only child ID/input/thread ID/mode plus policy args the host ceiling allows | Same supervisor `delegation_*` events; with host opt-in, `child_milestone` / `delegation_child_event` (redacted, capped, rate-coalesced) |
|
|
21
21
|
| A2A 1.0 | The other agent is owned by a **different service/deployment**; cross-org or cross-cluster; needs durable task lifecycle, push configs, streaming | Protocol boundary (JSON-RPC/HTTPS agent card); replay/reconnect via host-owned task adapter | Exact-origin verified client, `A2AAuthorization` per operation, principal-scoped push configs | Host-owned task adapter records the remote lifecycle; Prism creates no worker/store |
|
|
22
22
|
|
|
23
23
|
Rule of thumb: same conversation → handoff; dynamic task decomposition + parallel execution → hierarchical crew; host-selected same-process subtask → supervisor delegation; model-requested allow-listed subtask → in-process spawn tool; different deployment/trust boundary → A2A.
|
|
@@ -151,6 +151,29 @@ Live demo: [`examples/crew-hierarchy.ts`](../examples/crew-hierarchy.ts) — man
|
|
|
151
151
|
|
|
152
152
|
Live demo: [`examples/spawn-agent-tool.ts`](../examples/spawn-agent-tool.ts) — a narrowed read-only explore child spawned twice in parallel, an uncatalogued child refused, and both handles joined.
|
|
153
153
|
|
|
154
|
+
### Background agents (session lifetime)
|
|
155
|
+
|
|
156
|
+
A host can start a background child at session open that reports without occupying the conversation:
|
|
157
|
+
|
|
158
|
+
```ts
|
|
159
|
+
const supervisor = createSupervisor({
|
|
160
|
+
ownership,
|
|
161
|
+
signal: sessionAbort.signal, // host session end stops every child and closes the stream
|
|
162
|
+
children: {
|
|
163
|
+
researcher: {
|
|
164
|
+
policy: { lifetime: "session", report: "milestones", milestone: { everyTurns: 5 }, budgetShare: 0.2 },
|
|
165
|
+
createAgent: ({ resourceId, threadId, permission, signal, delegate }) => createResearchAgent(/* ... */),
|
|
166
|
+
},
|
|
167
|
+
},
|
|
168
|
+
});
|
|
169
|
+
|
|
170
|
+
// Host code or the parent model (spawn_agent routes session lifetime to the async path):
|
|
171
|
+
const handle = await supervisor.delegateAsync({ childId: "researcher", input: "watch the build", lifetime: "session" });
|
|
172
|
+
await supervisor.wait(handle.delegationId); // join later; cancel(handle.delegationId) ends it explicitly
|
|
173
|
+
```
|
|
174
|
+
|
|
175
|
+
Session-lifetime children detach from the caller and ancestor-child abort signals, hold one `activeChildren` slot until they end, and stop on `cancel_agent` / `cancel(delegationId)` or the supervisor `signal`. `budgetShare` scales the inherited steps/tool-calls/tokens/timeout limits — never above the parent or host ceiling. Reporting stays host-opt-in and redacted: `milestones` emits `child_milestone` every N turns or on a host predicate, `stream` forwards every per-turn provider/tool/turn event (never token deltas), and both are rate-coalesced at `limits.maxChildEventsPerSecond` (10/s per child default) with a `delegation_child_events_coalesced` marker. To surface them on a parent session stream, pass `childEventSink` — it receives the same redacted payload tagged `child: { childId, delegationId, depth }`, so the parent subscriber only reads `event.child`.
|
|
176
|
+
|
|
154
177
|
## Where Prism is stronger
|
|
155
178
|
|
|
156
179
|
- **Durable Human-in-the-Loop (HITL)**: Prism workflows support durable pause and resume via [`suspend()`](workflows.md#durable-suspension-and-resumption) and [`resumeWorkflow()`](workflows.md) across worker restarts or approval gates ([Agent durable approval](agent-session-runtime.md)).
|
|
@@ -166,7 +189,7 @@ Live demo: [`examples/spawn-agent-tool.ts`](../examples/spawn-agent-tool.ts) —
|
|
|
166
189
|
- **Narrowing on transfer, never widening.** If the specialist needs the caller's verified identity, project it through `narrowIdentity` / `assertIdentityPropagation` ([Agent identity](agent-identity.md)) so scopes and tenant cannot widen across the swap. For delegation the same discipline is built in (`narrowIdentity`, AND-composed policies); for A2A the exact-origin client plus per-operation authorization is the boundary.
|
|
167
190
|
- **Manager-generated task plans are untrusted model output.** Manager plan outputs are validated against the typed schema via `ArtifactValidator` before being persisted to workflow state or dispatched to `fan_out`. Malformed or invalid plans trigger the artifact repair loop or fail closed before any specialist is invoked.
|
|
168
191
|
- **Redaction of carried context.** Handoff carries the raw transcript by design — same rows a human replay would read. Apply the session egress seams on the way out: `redactSessionEntry` / `redactMessage` with a host field policy (see [Data classification](data-classification.md)) and `AgentConfig.redactor`; for durable replay across tenants reuse the redacted transcript seam discipline used by ACP `sessions.transcript` ([ACP interop](acp.md)).
|
|
169
|
-
- **Telemetry attribution.** Which agent produced which turn is not stored on message entries; the host knows (it performed the swap or aggregated fan-out results) and should pin it per run via `RunOptions.identity` (principal kind `agent`) so `identityTelemetryAttributes` (`prism.identity.*`) carries redacted attribution on telemetry, or via observability metadata. Supervisor runs emit dedicated `delegation_*` events
|
|
192
|
+
- **Telemetry attribution.** Which agent produced which turn is not stored on message entries; the host knows (it performed the swap or aggregated fan-out results) and should pin it per run via `RunOptions.identity` (principal kind `agent`) so `identityTelemetryAttributes` (`prism.identity.*`) carries redacted attribution on telemetry, or via observability metadata. Supervisor runs emit dedicated `delegation_*` events plus `child_failed` attribution (terminal `status`/`stopReason`, plan-086/087 `RunLimitBreach` when a ceiling fired), and `supervisor.summary()` reports per-child `attempts`/`retries`/`failures`/`failureRadius`/`outcome` — the recovery and cascade-radius counters a host cannot reconstruct from totals alone. An in-process definition swap has no session seam to emit one, so the host records attribution.
|
|
170
193
|
- **Performance.** The swap performs zero provider calls; it costs one registry resolution plus one session open (~sub-millisecond in the example fixture). The transferred turn costs what any tool round costs.
|
|
171
194
|
|
|
172
195
|
## Extension and configuration notes
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
## What it does
|
|
4
4
|
|
|
5
|
-
The optional `@arnilo/prism/node/session-store-jsonl` subpath stores `SessionEntry` records in a caller-named JSONL file: one JSON object per line. `searchSessions` is
|
|
5
|
+
The optional `@arnilo/prism/node/session-store-jsonl` subpath stores `SessionEntry` records in a caller-named JSONL file: one JSON object per line. `searchSessions` is supported as an unindexed linear scan of the file: the memory-store matcher (workspace/label/summary/`kind` filters, text `query`, cursor pagination, hit pointers) with the contract linear scan caps. Every query reads and parses the whole file (O(corpus) time and memory), so indexed SQLite/Postgres adapters remain the recommended path for search over large corpora.
|
|
6
6
|
|
|
7
7
|
APIs:
|
|
8
8
|
|
|
@@ -35,6 +35,7 @@ import { createJsonlSessionStore } from "@arnilo/prism/node/session-store-jsonl"
|
|
|
35
35
|
- `append(entry, options?)` appends one JSON line, rejects duplicate entry ids, honors `expectedParentId` existence checks, and deduplicates exact idempotency retries within this store instance. Append **fails closed** when the file already contains any corrupt or shape-invalid line (`Invalid JSONL at line N: …`) so writers cannot extend a damaged log.
|
|
36
36
|
- `list(sessionId)` reads the file and returns valid entries for that session id. Corrupt or shape-invalid lines are skipped; they do not poison the whole file.
|
|
37
37
|
- `get(id)` reads the file and returns the matching valid entry, if any.
|
|
38
|
+
- `searchSessions(query)` reads the file and runs the shared linear session matcher. Corrupt or shape-invalid lines are quarantined exactly as in `list()`/`get()`, the contract linear caps bound sessions/entries/bytes scanned, and hits carry the same shape as the indexed adapters (`sessionId`, `leafId`, `entryId`, `runId`, `turn`, `snippet`) — without `score`, since a linear scan has no index relevance.
|
|
38
39
|
- `readJsonlSessionEntries(path)` returns `{ entries: SessionEntry[]; errors: SessionEntryParseError[] }` so hosts/tests can inspect per-line parse errors.
|
|
39
40
|
|
|
40
41
|
Missing files read as empty stores (typed Node `ENOENT`). Invalid JSON, missing required fields, unsupported `schemaVersion`, unknown `kind`, or wrong per-kind shapes (`message`, `summary`, `model_change`, `custom`, `compaction`, `label`, `event`, `metadata`, or non-string `parentId`) are quarantined per line with line number and reason; the raw line is included in `SessionEntryParseError.raw`. Unknown entry kinds and future schema versions fail closed for reads: the line is skipped and never returned by `list()` or `get()`. For writes, any parse error blocks `append()` until the host repairs or replaces the file.
|
|
@@ -56,6 +57,10 @@ import { createJsonlSessionStore, readJsonlSessionEntries } from "@arnilo/prism/
|
|
|
56
57
|
const store = createJsonlSessionStore("./sessions.jsonl");
|
|
57
58
|
const { entries, errors } = await readJsonlSessionEntries("./sessions.jsonl");
|
|
58
59
|
if (errors.length) console.warn("quarantined lines", errors);
|
|
60
|
+
|
|
61
|
+
// Linear search (unindexed): the same query API as the SQLite/Postgres adapters.
|
|
62
|
+
const page = await store.searchSessions!({ workspaceRoot: "/repo", query: "flake", kind: "any", limit: 20 });
|
|
63
|
+
// [{ sessionId, leafId, entryId, runId, turn, snippet, ... }]
|
|
59
64
|
```
|
|
60
65
|
|
|
61
66
|
Use `createMemorySessionStore()` for tests or throwaway sessions; use the JSONL store when entries should survive a process restart.
|
|
@@ -73,6 +78,7 @@ Use `createMemorySessionStore()` for tests or throwaway sessions; use the JSONL
|
|
|
73
78
|
- Errors include path/reason or line number, not file contents.
|
|
74
79
|
- Do not put secrets in messages, metadata, summaries, labels, or custom entries.
|
|
75
80
|
- Reads are linear in file size. Appends also re-read and re-parse the whole file for duplicate/parent/corruption checks before writing one line, and are serialized per store instance. A rejected append does not poison later appends; the rejected line is not written.
|
|
81
|
+
- `searchSessions` is linear in file size too (there is no index): every query reads and parses the whole file before the capped scan, so latency and peak memory grow with the corpus. Use a SQLite/Postgres `SessionStore` when search latency matters, and treat search here as resume/filter tooling on small stores.
|
|
76
82
|
- There is no cross-process lock or durable idempotency table; two processes writing the same file can race. Add a database or external lock if multiple processes write the same file.
|
|
77
83
|
- Treat this adapter as development/single-process storage. Production multi-writer hosts should use an indexed database `SessionStore` adapter.
|
|
78
84
|
|
package/docs/observability.md
CHANGED
|
@@ -41,7 +41,7 @@ New agent event variants (metadata only):
|
|
|
41
41
|
| Variant | When | Key fields |
|
|
42
42
|
| --- | --- | --- |
|
|
43
43
|
| `provider_turn_started` | Before each provider `generate()` attempt | `turn`, `metadata: ProviderTurnMetadata` |
|
|
44
|
-
| `provider_turn_finished` | After success or failure of that attempt | `metadata` (includes `latencyMs`, optional `httpStatus`), `usage?`, `error?` |
|
|
44
|
+
| `provider_turn_finished` | After success or failure of that attempt | `metadata` (includes `latencyMs`, optional `httpStatus`, `stopReason`, `budgets`, `cache`), `usage?`, `error?` |
|
|
45
45
|
|
|
46
46
|
`ToolExecutionMetadata` on terminal tool events:
|
|
47
47
|
|
|
@@ -91,6 +91,7 @@ Provider turn metadata fields:
|
|
|
91
91
|
| `latencyMs` | Set on `provider_turn_finished` |
|
|
92
92
|
| `httpStatus` | Numeric `ErrorInfo.code` when present |
|
|
93
93
|
| `rateLimitRemaining` / `rateLimitResetMs` | Reserved for provider adapters (optional) |
|
|
94
|
+
| `cache` | Provider-reported `{ cacheReadTokens?, cacheWriteTokens?, hitRate? }`; absent when cache usage is unknown. |
|
|
94
95
|
|
|
95
96
|
OpenTelemetry mapping (when enabled):
|
|
96
97
|
|
|
@@ -190,8 +191,8 @@ const found = await retrieveContext("policy", { embedder, store, scope, telemetr
|
|
|
190
191
|
|
|
191
192
|
Host cockpits and dashboard cards need fast aggregate summaries of an execution without re-walking every raw event or risking prompt/secret leaks:
|
|
192
193
|
|
|
193
|
-
- `summarizeTimeline(timeline)`: rolls up an `ExecutionTimeline` into a `TimelineSummary` containing duration, turn count, tool call counts, provider attempts, total tokens, cost, error counts, and
|
|
194
|
-
- `summarizeSession(timelines)`: rolls up an array of `ExecutionTimeline`s for a session/conversation into a `SessionSummary` with aggregated tokens, costs, run counts, and
|
|
194
|
+
- `summarizeTimeline(timeline)`: rolls up an `ExecutionTimeline` into a `TimelineSummary` containing duration, turn count (split into model vs deterministic turns), tool call counts, provider attempts, total tokens, cost, error counts, suspension state, and — for a run that died on a run limit — an `exhaustion` line (`"maxTurns exhausted (13/12); closest: maxToolCalls 0.625"`).
|
|
195
|
+
- `summarizeSession(timelines)`: rolls up an array of `ExecutionTimeline`s for a session/conversation into a `SessionSummary` with aggregated tokens, costs, run counts, duration, and the same model/deterministic turn split.
|
|
195
196
|
|
|
196
197
|
```ts
|
|
197
198
|
import { summarizeTimeline, summarizeSession } from "@arnilo/prism-core/governance/observability";
|
|
@@ -201,6 +202,7 @@ const summary = summarizeTimeline(timeline);
|
|
|
201
202
|
// {
|
|
202
203
|
// durationMs: 1250,
|
|
203
204
|
// turnCount: 2,
|
|
205
|
+
// turns: { model: 1, deterministic: 1 },
|
|
204
206
|
// toolCallCount: 3,
|
|
205
207
|
// toolCounts: { search: 2, lookup: 1 },
|
|
206
208
|
// providerAttempts: 2,
|
|
@@ -210,6 +212,7 @@ const summary = summarizeTimeline(timeline);
|
|
|
210
212
|
// blockedToolCount: 0,
|
|
211
213
|
// suspended: false,
|
|
212
214
|
// status: "succeeded",
|
|
215
|
+
// exhaustion: "maxTurns exhausted (13/12); closest: maxToolCalls 0.625", // only when a limit fired
|
|
213
216
|
// }
|
|
214
217
|
|
|
215
218
|
const sessionSummary = summarizeSession([run1Timeline, run2Timeline]);
|
|
@@ -218,6 +221,7 @@ const sessionSummary = summarizeSession([run1Timeline, run2Timeline]);
|
|
|
218
221
|
|
|
219
222
|
Cardinality and correctness guarantees:
|
|
220
223
|
- **Bounded cardinality**: `toolCounts` is capped to `MAX_SUMMARY_DISTINCT_TOOLS = 64` distinct tool names. If more tools are invoked, lowest-frequency tool names overflow into an `"other"` bucket.
|
|
224
|
+
- **Honest turn attribution**: `turns.model` counts turns with a provider step; `turns.deterministic` counts turns answered by host middleware (plan 096, `deterministic` step kind). A no-model turn is never rolled into model counts, and its usage stays absent rather than zero.
|
|
221
225
|
- **No double counting**: Token usage is derived from the root run's `run_total` (or aggregated across `turn` / `provider` steps if no run-level total exists), avoiding double counting between provider turn steps and run totals. Costs are rounded to 6 decimal places to prevent floating-point drift.
|
|
222
226
|
- **Payload-free**: Summaries contain counts, durations, status codes, and usage metrics only — zero prompt text, tool arguments, or credentials.
|
|
223
227
|
|
package/docs/options-index.md
CHANGED
|
@@ -18,7 +18,7 @@ Field-level detail (defaults, bounds, failure modes) lives on the owning page
|
|
|
18
18
|
| --- | --- | --- |
|
|
19
19
|
| `AgentConfig` | The reusable agent: provider, model, tools, skills, stores, retry, compaction, prompts, limits | [Agent/session runtime](agent-session-runtime.md) |
|
|
20
20
|
| `RunOptions` | One run's overrides: model, limits, thinking level, skills, middleware, metadata, signal | [Agent/session runtime](agent-session-runtime.md) |
|
|
21
|
-
| `AgentSessionConfig` | Session creation: id, agent, store, branch leaf, snapshot cache TTL | [Agent/session runtime](agent-session-runtime.md) |
|
|
21
|
+
| `AgentSessionConfig` | Session creation: id, agent, store, branch leaf, snapshot cache TTL, guardrail packs | [Agent/session runtime](agent-session-runtime.md) |
|
|
22
22
|
| `ModelConfig` | A registered model record: capabilities, limits, cost, cache and thinking metadata | [Model registry](model-registry.md) |
|
|
23
23
|
| `ProviderRequestOptions` | Per-request provider hints: session/cache/header/compat/extra, applied after host policies | [Provider layer](provider-layer.md) |
|
|
24
24
|
|
|
@@ -42,9 +42,12 @@ Field-level detail (defaults, bounds, failure modes) lives on the owning page
|
|
|
42
42
|
| --- | --- | --- |
|
|
43
43
|
| `RunOptions.turnPolicy` (`TurnPolicyOptions`) | Synchronous host stop at a turn boundary; a stop lands as `stopReason: "host_policy"` and stays resumable | [Agent loops](agent-loops.md) |
|
|
44
44
|
| `CreateAgUiHandlerOptions.inputPolicy` (`AgUiInputPolicyOptions`) | `clientState: "honor"` \| `"ignore"` — whether the server honors client-supplied AG-UI state and tools | [Frontend interoperability](ag-ui.md) |
|
|
45
|
+
| `SubscribeOptions.acrossRuns` | `true` keeps one live subscriber open across runs of the same session until the host ends it, session teardown, or an overflow (bounded queue, default 1024, no background work); default `false` closes it at run end | [Agent events](agent-events.md) |
|
|
45
46
|
| `snapshotRunBundle(...)` → `RunBundleSnapshot` | Inspectable digest projection of the effective run bundle (prompt/skill/tool/guardrail digests, limits, storage kinds) | [Run bundle](run-bundle.md) |
|
|
46
47
|
| `createClaimGroundingGuardrail` (`ClaimGroundingGuardrailOptions`) | `"output"`-stage guardrail that blocks or flags numeric claims no tool result or host evidence supports | [Guardrails](guardrails.md) |
|
|
47
48
|
| `ErrorInfo.failureClass` (`ProviderFailureClass`) | Typed provider failure on run outcomes, ledger rows, and tool results (`quota`, `rate_limited`, `auth`, `transient`, `permanent`) | [Runs and usage](runs-and-usage.md) |
|
|
49
|
+
| `AgentConfig.usageEstimation` | `"fallback"` (default) records a labeled estimate when a provider reports no usage; `"off"` leaves usage absent; `"strict"` refuses the turn instead (`code: "usage_missing"`); estimates are never priced | [Runs and usage](runs-and-usage.md#automatic-fallback-agentconfigusageestimation) |
|
|
50
|
+
| `AgentConfig.contextBudget` | Session-turn eviction budget forwarded to every assembly (`maxInputTokens`/`maxInputBytes`, `reportOmissions`, `tokenEstimator`); the usage fallback prefers its measurement | [Input and prompt assembly](input-and-prompt-assembly.md) |
|
|
48
51
|
| `ModelConfig.capabilities.toolCallStrictness` | Advisory tool-call reliability (`"strict"` \| `"lenient"` \| `"legacy"`); catalog conformance, not a promise | [Model registry](model-registry.md) |
|
|
49
52
|
|
|
50
53
|
## Agent/session runtime
|