@arnilo/prism 0.7.0 → 0.9.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 +73 -0
- package/README.md +12 -11
- package/dist/agent-approval.d.ts +15 -2
- package/dist/agent-approval.js +5 -1
- 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 +91 -10
- package/dist/agent-run-state.d.ts +34 -2
- package/dist/agent-run-state.js +68 -6
- package/dist/agent-session/helpers.js +20 -1
- package/dist/agent-session/session/assemble.js +250 -27
- package/dist/agent-session/session/persist.d.ts +27 -0
- package/dist/agent-session/session/persist.js +94 -12
- package/dist/agent-session/session/provider-round.d.ts +14 -4
- package/dist/agent-session/session/provider-round.js +197 -25
- package/dist/agent-session/session/tool-round.js +24 -2
- package/dist/agent-session/session/types.d.ts +36 -2
- package/dist/agent-session/session.d.ts +40 -4
- package/dist/agent-session/session.js +78 -5
- 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/checkpoints.js +7 -11
- package/dist/context-budget.d.ts +2 -1
- package/dist/context-budget.js +24 -2
- package/dist/contracts-core/agent.d.ts +30 -0
- package/dist/contracts-core/attention.d.ts +95 -0
- package/dist/contracts-core/content.d.ts +15 -0
- package/dist/contracts-core/guardrail-packs.d.ts +41 -0
- package/dist/contracts-core/guardrail-packs.js +2 -0
- package/dist/contracts-core/loop.d.ts +42 -0
- package/dist/contracts-core/provider.d.ts +25 -0
- package/dist/contracts-core/run-limits.d.ts +21 -0
- 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 +90 -4
- package/dist/contracts-run-state.d.ts +82 -6
- package/dist/evidence-grounding.d.ts +29 -0
- package/dist/evidence-grounding.js +162 -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 +16 -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 +53 -0
- package/dist/guardrails.d.ts +20 -1
- package/dist/guardrails.js +268 -0
- package/dist/host-composition.d.ts +13 -0
- package/dist/host-composition.js +33 -2
- package/dist/index.d.ts +19 -10
- package/dist/index.js +11 -6
- 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 +11 -3
- package/dist/provider-events.js +62 -4
- package/dist/providers/openai-compatible.js +6 -3
- package/dist/providers/transport.d.ts +3 -1
- package/dist/providers/transport.js +36 -0
- package/dist/redaction.js +18 -2
- package/dist/run-bundle.d.ts +89 -0
- package/dist/run-bundle.js +150 -0
- package/dist/run-limits.d.ts +11 -1
- package/dist/run-limits.js +46 -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 +30 -0
- package/dist/testing/prefix-stability-conformance.js +104 -0
- package/dist/testing/session-store-conformance.d.ts +3 -2
- package/dist/testing/session-store-conformance.js +48 -0
- package/dist/testing/state-concurrency-conformance.js +5 -12
- package/dist/tools.d.ts +5 -0
- package/dist/tools.js +11 -3
- package/dist/usage-estimation.d.ts +29 -0
- package/dist/usage-estimation.js +79 -0
- package/docs/ag-ui.md +5 -0
- package/docs/agent-events.md +68 -1
- package/docs/agent-loops.md +33 -0
- package/docs/agent-session-runtime.md +5 -3
- package/docs/attention-compiler.md +89 -8
- package/docs/coding-agent-tools.md +1 -1
- package/docs/coding-security.md +1 -0
- package/docs/coding-tools.md +0 -1
- package/docs/compaction-and-retry.md +1 -1
- package/docs/compaction-observational-memory.md +34 -7
- package/docs/connected-apps.md +116 -0
- package/docs/context-and-skills.md +13 -0
- package/docs/core.md +1 -1
- package/docs/diagrams.md +6 -6
- package/docs/document-reader.md +9 -9
- package/docs/documents.md +32 -11
- package/docs/durable-runs.md +129 -0
- package/docs/embeddings.md +5 -0
- package/docs/enterprise-postgres-state.md +4 -0
- package/docs/evaluations.md +5 -0
- package/docs/execution-timeline.md +84 -1
- package/docs/guardrails.md +71 -2
- package/docs/history/079-messaging-primitive-review.md +391 -0
- package/docs/history/080-messaging-followon-primitive-review.md +234 -0
- package/docs/history/081-connected-apps-primitive-review.md +74 -0
- package/docs/history/083-prism-work-primitive-review.md +84 -0
- package/docs/history/084-primitive-review.md +96 -0
- package/docs/history/085-honesty-and-cut-primitive-review.md +91 -0
- package/docs/history/README.md +5 -0
- package/docs/history/release-handoffs.md +38 -0
- package/docs/host-compositions.md +8 -6
- package/docs/host-security.md +2 -2
- package/docs/index.md +66 -29
- package/docs/input-and-prompt-assembly.md +3 -3
- package/docs/knowledge-sync.md +4 -0
- package/docs/live-testing.md +5 -3
- package/docs/mcp-tools.md +1 -0
- package/docs/messaging-channel-operations.md +166 -0
- package/docs/messaging-channels.md +150 -0
- package/docs/middleware-hooks.md +38 -2
- package/docs/migrate-to-0.8.md +124 -0
- package/docs/migrate-to-0.9.md +210 -0
- package/docs/migration.md +43 -0
- package/docs/model-registry.md +12 -2
- 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/openapi-tools.md +1 -1
- package/docs/operations.md +1 -3
- package/docs/options-index.md +36 -3
- package/docs/peer-dependencies.md +6 -6
- package/docs/policy-and-audit.md +13 -1
- package/docs/postgres-persistence.md +1 -1
- package/docs/prefix-stability-conformance.md +93 -0
- package/docs/provider-caching.md +4 -4
- package/docs/provider-conformance.md +16 -0
- package/docs/provider-layer.md +2 -2
- package/docs/provider-packages.md +20 -20
- package/docs/providers/neuralwatt.md +5 -1
- package/docs/public-contracts.md +2 -2
- package/docs/rag.md +102 -4
- package/docs/release-and-install.md +55 -47
- package/docs/run-bundle.md +92 -0
- package/docs/runs-and-usage.md +57 -6
- package/docs/scoped-agent-memory.md +262 -0
- package/docs/server.md +2 -0
- package/docs/session-store-conformance.md +1 -2
- package/docs/session-stores.md +17 -17
- package/docs/sheets.md +9 -9
- package/docs/signal-channel.md +112 -0
- package/docs/speech.md +5 -1
- package/docs/sqlite-persistence.md +1 -1
- package/docs/supervisors.md +32 -12
- package/docs/telegram-channel.md +157 -0
- package/docs/testing.md +2 -2
- package/docs/tools.md +17 -0
- package/docs/wiki.md +1 -1
- package/docs/work-artifacts-and-review.md +1 -1
- package/docs/work-connectors.md +9 -9
- package/docs/work-sandbox.md +115 -0
- package/docs/work-tools.md +38 -16
- package/docs/workflows.md +5 -0
- package/package.json +9 -3
- package/templates/business-worker/manifest.json +2 -1
- package/templates/business-worker/src/agent.ts.tmpl +1 -1
- package/templates/business-worker/src/tests/agent.test.ts.tmpl +1 -1
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
import { createHash } from "node:crypto";
|
|
8
8
|
import { estimateMessageBytes, estimateMessageTokens, estimateTextBytes, measureInputCost, } from "./context-budget.js";
|
|
9
9
|
import { assertCompactionTrigger } from "./contracts-core/compaction.js";
|
|
10
|
-
import { capToolResultSummary, foldedToolResultHeader, inferToolResultTurns, toolResultFoldText, } from "./tool-result-fold.js";
|
|
10
|
+
import { capToolResultSummary, foldedToolResultHeader, HARD_TOOL_RESULT_FOLD_MAX_SUMMARY_BYTES, inferToolResultTurns, toolResultFoldText, } from "./tool-result-fold.js";
|
|
11
11
|
export const ATTENTION_BUDGET_ERROR_CODE = "attention_budget_exceeded";
|
|
12
12
|
/** C9: still over `triggerRatio` after every eligible stage — host should compact, not delete. */
|
|
13
13
|
export class AttentionBudgetError extends Error {
|
|
@@ -44,6 +44,19 @@ function resolveCount(value, fallback, name) {
|
|
|
44
44
|
}
|
|
45
45
|
return value;
|
|
46
46
|
}
|
|
47
|
+
/** `resolveRatio` for a value with no default: an axis ratio is required. */
|
|
48
|
+
function requiredRatio(value, name) {
|
|
49
|
+
if (value === undefined)
|
|
50
|
+
throw new TypeError(`${name} must be a number in (0, 1)`);
|
|
51
|
+
return resolveRatio(value, DEFAULT_ATTENTION_TRIGGER_RATIO, name);
|
|
52
|
+
}
|
|
53
|
+
function resolveBoolean(value, name) {
|
|
54
|
+
if (value === undefined)
|
|
55
|
+
return false;
|
|
56
|
+
if (typeof value !== "boolean")
|
|
57
|
+
throw new TypeError(`${name} must be a boolean`);
|
|
58
|
+
return value;
|
|
59
|
+
}
|
|
47
60
|
export function resolveAttentionReserveTokens(value) {
|
|
48
61
|
return resolveCount(value, DEFAULT_ATTENTION_RESERVE_TOKENS, "attentionCompiler.reserveTokens");
|
|
49
62
|
}
|
|
@@ -92,29 +105,170 @@ function resolveExcludeTools(value) {
|
|
|
92
105
|
}
|
|
93
106
|
return Object.freeze(out);
|
|
94
107
|
}
|
|
108
|
+
/** Normalize one configured axis, freezing it. Unnamed predicates are wrapped so the evaluator
|
|
109
|
+
* sees one shape; every failure is a config-time `TypeError` naming the option. */
|
|
110
|
+
function normalizeAttentionTrigger(value, name) {
|
|
111
|
+
if (typeof value === "function")
|
|
112
|
+
return Object.freeze({ kind: "predicate", shouldFold: value });
|
|
113
|
+
if (typeof value !== "object" || value === null || Array.isArray(value)) {
|
|
114
|
+
throw new TypeError(`${name} must be a trigger object, a predicate function, or an array of them`);
|
|
115
|
+
}
|
|
116
|
+
const kind = value.kind;
|
|
117
|
+
switch (kind) {
|
|
118
|
+
case "input_ratio":
|
|
119
|
+
case "run_input_ratio":
|
|
120
|
+
return Object.freeze({ kind, ratio: requiredRatio(value.ratio, `${name}.ratio`) });
|
|
121
|
+
case "token_floor": {
|
|
122
|
+
const tokens = value.tokens;
|
|
123
|
+
if (!Number.isSafeInteger(tokens) || tokens < 1) {
|
|
124
|
+
throw new TypeError(`${name}.tokens must be a positive safe integer`);
|
|
125
|
+
}
|
|
126
|
+
return Object.freeze({ kind, tokens: tokens });
|
|
127
|
+
}
|
|
128
|
+
case "predicate": {
|
|
129
|
+
const shouldFold = value.shouldFold;
|
|
130
|
+
if (typeof shouldFold !== "function")
|
|
131
|
+
throw new TypeError(`${name}.shouldFold must be a function`);
|
|
132
|
+
return Object.freeze({ kind, shouldFold: shouldFold });
|
|
133
|
+
}
|
|
134
|
+
default:
|
|
135
|
+
throw new TypeError(`unknown ${name} kind: ${String(kind)} (expected input_ratio, run_input_ratio, token_floor, or predicate)`);
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
/** Fold axes for a compiler: exactly the configured `trigger`, or the `triggerRatio` axis alone
|
|
139
|
+
* (plan 074 behavior) when none is. Frozen, in evaluation order. */
|
|
140
|
+
function resolveAttentionTriggerAxes(triggerRatio, value) {
|
|
141
|
+
if (value === undefined)
|
|
142
|
+
return Object.freeze([Object.freeze({ kind: "input_ratio", ratio: triggerRatio })]);
|
|
143
|
+
const entries = Array.isArray(value) ? value : [value];
|
|
144
|
+
if (entries.length === 0) {
|
|
145
|
+
throw new TypeError("attentionCompiler.trigger must be a trigger object, a predicate function, or a non-empty array of them");
|
|
146
|
+
}
|
|
147
|
+
const single = entries.length === 1;
|
|
148
|
+
return Object.freeze(entries.map((entry, index) => normalizeAttentionTrigger(entry, single ? "attentionCompiler.trigger" : `attentionCompiler.trigger[${index}]`)));
|
|
149
|
+
}
|
|
150
|
+
/** Resolved `RunLimits.maxInputTokens`; `null` disables the cap, so it is no budget either. */
|
|
151
|
+
function resolveAttentionRunInputBudget(value) {
|
|
152
|
+
if (value === undefined || value === null)
|
|
153
|
+
return undefined;
|
|
154
|
+
if (!Number.isSafeInteger(value) || value < 1) {
|
|
155
|
+
throw new TypeError("attentionCompiler.runInputBudget must be a positive safe integer");
|
|
156
|
+
}
|
|
157
|
+
return value;
|
|
158
|
+
}
|
|
159
|
+
/**
|
|
160
|
+
* Build the frozen per-turn state the axes read: the compiler supplies the cap and the run budget
|
|
161
|
+
* (so a caller cannot silently drop the budget and fall back to the input cap), the caller
|
|
162
|
+
* supplies what only the turn knows.
|
|
163
|
+
*/
|
|
164
|
+
export function attentionTriggerState(compiler, turn) {
|
|
165
|
+
return Object.freeze({
|
|
166
|
+
estimatedInputTokens: turn.estimatedInputTokens,
|
|
167
|
+
inputCapTokens: compiler.inputCap,
|
|
168
|
+
...(compiler.runInputBudget === undefined ? {} : { runInputBudgetTokens: compiler.runInputBudget }),
|
|
169
|
+
runInputTokens: turn.runInputTokens ?? 0,
|
|
170
|
+
turn: turn.turn ?? 1,
|
|
171
|
+
});
|
|
172
|
+
}
|
|
173
|
+
/**
|
|
174
|
+
* Evaluate the fold axes in order against one turn's state; the first axis that fires wins, and
|
|
175
|
+
* its `targetTokens` (when it has one) is what the stages fold to. Called at most twice per turn —
|
|
176
|
+
* once at turn start, once after the stages — so a host predicate never runs per row.
|
|
177
|
+
*/
|
|
178
|
+
export function evaluateAttentionTrigger(axes, state) {
|
|
179
|
+
for (const axis of axes) {
|
|
180
|
+
switch (axis.kind) {
|
|
181
|
+
case "input_ratio": {
|
|
182
|
+
const targetTokens = axis.ratio * state.inputCapTokens;
|
|
183
|
+
if (state.estimatedInputTokens >= targetTokens) {
|
|
184
|
+
return { shouldFold: true, firedAxis: "input_ratio", targetTokens, failsClosed: true };
|
|
185
|
+
}
|
|
186
|
+
continue;
|
|
187
|
+
}
|
|
188
|
+
case "token_floor":
|
|
189
|
+
if (state.estimatedInputTokens >= axis.tokens) {
|
|
190
|
+
return { shouldFold: true, firedAxis: "token_floor", targetTokens: axis.tokens, failsClosed: true };
|
|
191
|
+
}
|
|
192
|
+
continue;
|
|
193
|
+
case "run_input_ratio": {
|
|
194
|
+
const budget = state.runInputBudgetTokens;
|
|
195
|
+
if (budget === undefined) {
|
|
196
|
+
// No run budget declared: the axis is the per-request ratio comparison.
|
|
197
|
+
const targetTokens = axis.ratio * state.inputCapTokens;
|
|
198
|
+
if (state.estimatedInputTokens >= targetTokens) {
|
|
199
|
+
return { shouldFold: true, firedAxis: "run_input_ratio", targetTokens, failsClosed: true };
|
|
200
|
+
}
|
|
201
|
+
continue;
|
|
202
|
+
}
|
|
203
|
+
if (state.runInputTokens + state.estimatedInputTokens < axis.ratio * budget)
|
|
204
|
+
continue;
|
|
205
|
+
// Cumulative: the spend is already booked, so folding cannot settle the axis. No
|
|
206
|
+
// per-request target (fold every eligible row) and no fail-closed throw — the run limit
|
|
207
|
+
// owns the cap, and folding is what keeps the run under it.
|
|
208
|
+
return { shouldFold: true, firedAxis: "run_input_ratio", failsClosed: false };
|
|
209
|
+
}
|
|
210
|
+
case "predicate": {
|
|
211
|
+
const fired = axis.shouldFold(state);
|
|
212
|
+
if (typeof fired !== "boolean") {
|
|
213
|
+
throw new TypeError("attentionCompiler.trigger predicate must return a boolean (use a synchronous function)");
|
|
214
|
+
}
|
|
215
|
+
if (fired)
|
|
216
|
+
return { shouldFold: true, firedAxis: "predicate", failsClosed: true };
|
|
217
|
+
continue;
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
return { shouldFold: false, failsClosed: false };
|
|
222
|
+
}
|
|
95
223
|
/** Validate compiler options + resolve the input cap. No provider I/O; unknown or
|
|
96
224
|
* unresolvable input throws here rather than on the first turn. */
|
|
97
225
|
export function createAttentionCompiler(options = {}, context = {}) {
|
|
98
226
|
if (typeof options !== "object" || options === null)
|
|
99
227
|
throw new TypeError("attentionCompiler options must be an object");
|
|
100
228
|
const triggerRatio = resolveRatio(options.triggerRatio, DEFAULT_ATTENTION_TRIGGER_RATIO, "attentionCompiler.triggerRatio");
|
|
229
|
+
const triggerAxes = resolveAttentionTriggerAxes(triggerRatio, options.trigger);
|
|
230
|
+
// Reference ratio the handle reports and `compactRatio` / the compaction trigger are checked
|
|
231
|
+
// against: the explicit `triggerRatio`, else the first `input_ratio` axis, so a `trigger` that
|
|
232
|
+
// replaces the gate still reports the ratio that gate actually folds at.
|
|
233
|
+
let referenceRatio = triggerRatio;
|
|
234
|
+
if (options.triggerRatio === undefined) {
|
|
235
|
+
for (const axis of triggerAxes) {
|
|
236
|
+
if (axis.kind === "input_ratio") {
|
|
237
|
+
referenceRatio = axis.ratio;
|
|
238
|
+
break;
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
}
|
|
101
242
|
const compactRatio = resolveRatio(options.compactRatio, DEFAULT_ATTENTION_COMPACT_RATIO, "attentionCompiler.compactRatio");
|
|
102
|
-
if (compactRatio <=
|
|
103
|
-
throw new TypeError(`attentionCompiler.compactRatio (${compactRatio}) must exceed triggerRatio (${
|
|
243
|
+
if (compactRatio <= referenceRatio) {
|
|
244
|
+
throw new TypeError(`attentionCompiler.compactRatio (${compactRatio}) must exceed triggerRatio (${referenceRatio})`);
|
|
104
245
|
}
|
|
105
246
|
const thinkingKeepTurns = resolveCount(options.thinkingKeepTurns, DEFAULT_ATTENTION_THINKING_KEEP_TURNS, "attentionCompiler.thinkingKeepTurns");
|
|
106
247
|
const keepLast = resolveCount(options.keepLast, DEFAULT_ATTENTION_KEEP_LAST, "attentionCompiler.keepLast");
|
|
107
248
|
const inputCap = resolveInputCap(options, context.model);
|
|
108
249
|
const reserveTokens = resolveAttentionReserveTokens(options.reserveTokens);
|
|
109
250
|
const excludeTools = resolveExcludeTools(options.excludeTools);
|
|
251
|
+
const runInputBudget = resolveAttentionRunInputBudget(context.runInputBudget);
|
|
252
|
+
const durable = resolveBoolean(options.durable, "attentionCompiler.durable");
|
|
110
253
|
const trigger = context.compactionTrigger;
|
|
111
254
|
if (trigger !== undefined) {
|
|
112
255
|
assertCompactionTrigger(trigger);
|
|
113
|
-
if (trigger.type === "input_ratio" && trigger.ratio <=
|
|
114
|
-
throw new TypeError(`compaction input_ratio (${trigger.ratio}) must exceed attentionCompiler.triggerRatio (${
|
|
256
|
+
if (trigger.type === "input_ratio" && trigger.ratio <= referenceRatio) {
|
|
257
|
+
throw new TypeError(`compaction input_ratio (${trigger.ratio}) must exceed attentionCompiler.triggerRatio (${referenceRatio})`);
|
|
115
258
|
}
|
|
116
259
|
}
|
|
117
|
-
return Object.freeze({
|
|
260
|
+
return Object.freeze({
|
|
261
|
+
inputCap,
|
|
262
|
+
reserveTokens,
|
|
263
|
+
triggerRatio: referenceRatio,
|
|
264
|
+
compactRatio,
|
|
265
|
+
thinkingKeepTurns,
|
|
266
|
+
keepLast,
|
|
267
|
+
excludeTools,
|
|
268
|
+
trigger: triggerAxes,
|
|
269
|
+
durable,
|
|
270
|
+
...(runInputBudget === undefined ? {} : { runInputBudget }),
|
|
271
|
+
});
|
|
118
272
|
}
|
|
119
273
|
/* ------------------------------------------------------------------------------------------------
|
|
120
274
|
* Opt-in wiring (Task 5)
|
|
@@ -154,8 +308,11 @@ function mergeAttentionRunOverlay(base, overlay) {
|
|
|
154
308
|
}
|
|
155
309
|
return value;
|
|
156
310
|
};
|
|
157
|
-
if (overlay.maxInputTokens !== undefined ||
|
|
158
|
-
|
|
311
|
+
if (overlay.maxInputTokens !== undefined ||
|
|
312
|
+
overlay.reserveTokens !== undefined ||
|
|
313
|
+
overlay.trigger !== undefined ||
|
|
314
|
+
overlay.durable !== undefined) {
|
|
315
|
+
throw new TypeError("RunOptions.attentionCompiler must not set maxInputTokens, reserveTokens, trigger, or durable: the input cap, the gate, and fold durability are agent-config only");
|
|
159
316
|
}
|
|
160
317
|
const triggerRatio = gate("triggerRatio", DEFAULT_ATTENTION_TRIGGER_RATIO);
|
|
161
318
|
const compactRatio = gate("compactRatio", DEFAULT_ATTENTION_COMPACT_RATIO);
|
|
@@ -174,7 +331,11 @@ function mergeAttentionRunOverlay(base, overlay) {
|
|
|
174
331
|
/** Resolve the run's compiler from the agent setting plus an optional run overlay, validating
|
|
175
332
|
* both eagerly (no provider I/O) so a typo fails at run start, not on some later turn (C12).
|
|
176
333
|
* Returns `undefined` when the compiler is off — the assembly path then allocates nothing. */
|
|
177
|
-
export function resolveRunAttentionCompiler(agent, run, model) {
|
|
334
|
+
export function resolveRunAttentionCompiler(agent, run, model, runInputBudget) {
|
|
335
|
+
const context = {
|
|
336
|
+
model,
|
|
337
|
+
...(runInputBudget === undefined || runInputBudget === null ? {} : { runInputBudget }),
|
|
338
|
+
};
|
|
178
339
|
const enabled = enabledAttentionSetting(agent);
|
|
179
340
|
if (enabled === undefined) {
|
|
180
341
|
if (run !== undefined && run !== false) {
|
|
@@ -185,11 +346,11 @@ export function resolveRunAttentionCompiler(agent, run, model) {
|
|
|
185
346
|
if (run === false)
|
|
186
347
|
return undefined;
|
|
187
348
|
if (run === undefined || run === true)
|
|
188
|
-
return createAttentionCompiler(enabled,
|
|
349
|
+
return createAttentionCompiler(enabled, context);
|
|
189
350
|
if (typeof run !== "object" || run === null || Array.isArray(run)) {
|
|
190
351
|
throw new TypeError("RunOptions.attentionCompiler must be false or an options object");
|
|
191
352
|
}
|
|
192
|
-
return createAttentionCompiler(mergeAttentionRunOverlay(enabled, run),
|
|
353
|
+
return createAttentionCompiler(mergeAttentionRunOverlay(enabled, run), context);
|
|
193
354
|
}
|
|
194
355
|
export function createAttentionStickyFrontier() {
|
|
195
356
|
return { thinking: new Set(), toolCallIds: new Set() };
|
|
@@ -215,15 +376,30 @@ export async function compileAttention(options) {
|
|
|
215
376
|
const { compiler, groups } = options;
|
|
216
377
|
const turn = options.turn ?? 1;
|
|
217
378
|
const frontier = options.frontier;
|
|
218
|
-
const
|
|
379
|
+
const runInputTokens = options.runInputTokens ?? 0;
|
|
380
|
+
// One frozen state per evaluation: predicates never see a live object they could mutate, and
|
|
381
|
+
// the axes are evaluated at most twice a turn (turn start, then after the stages).
|
|
382
|
+
const triggerState = (estimatedInputTokens) => attentionTriggerState(compiler, { estimatedInputTokens, runInputTokens, turn });
|
|
219
383
|
const usedAtStart = measureInputCost({ groups, context: options.context, skills: options.skills, tools: options.tools }).tokens;
|
|
384
|
+
const decision = evaluateAttentionTrigger(compiler.trigger, triggerState(usedAtStart));
|
|
385
|
+
const targetTokens = decision.targetTokens;
|
|
386
|
+
const gateOpen = decision.shouldFold;
|
|
220
387
|
let used = usedAtStart;
|
|
221
|
-
|
|
388
|
+
// The gate is decided once per turn against the assembled estimate; the per-row loop then only
|
|
389
|
+
// compares numbers, so a predicate axis costs one call, not one per eligible row. A target-less
|
|
390
|
+
// axis (cumulative run budget, predicate) folds every eligible row.
|
|
391
|
+
const needsMore = () => gateOpen && (targetTokens === undefined || used >= targetTokens);
|
|
392
|
+
/** Post-stage verdict: a per-request axis still over its target, or a target-less axis whose
|
|
393
|
+
* predicate still fires, fails closed. A cumulative run-budget axis never does. */
|
|
394
|
+
const stillFired = () => targetTokens !== undefined
|
|
395
|
+
? used >= targetTokens
|
|
396
|
+
: decision.failsClosed && evaluateAttentionTrigger(compiler.trigger, triggerState(Math.max(0, used))).shouldFold;
|
|
222
397
|
const history = [...groups.history];
|
|
223
398
|
const toolResults = [...groups.toolResults];
|
|
224
399
|
let droppedThinkingTurns = 0;
|
|
225
400
|
let stubbedToolResults = 0;
|
|
226
401
|
let stubbedBytes = 0;
|
|
402
|
+
let newFoldedBodies = 0;
|
|
227
403
|
let leftEligible = 0;
|
|
228
404
|
// Stage 1 — strip thinking from every assistant turn except the newest `thinkingKeepTurns`.
|
|
229
405
|
const rowTurns = inferToolResultTurns(history);
|
|
@@ -233,7 +409,7 @@ export async function compileAttention(options) {
|
|
|
233
409
|
continue;
|
|
234
410
|
const key = thinkingKey(message, options.redactor);
|
|
235
411
|
const sticky = frontier?.thinking.has(key) === true;
|
|
236
|
-
if (!sticky && !
|
|
412
|
+
if (!sticky && !needsMore()) {
|
|
237
413
|
leftEligible += 1;
|
|
238
414
|
continue;
|
|
239
415
|
}
|
|
@@ -247,11 +423,12 @@ export async function compileAttention(options) {
|
|
|
247
423
|
// and the in-flight group (which is always newer than history).
|
|
248
424
|
for (const target of toolResultTargets({ history, toolResults, rowTurns, turn, compiler, fold: options.fold })) {
|
|
249
425
|
const sticky = frontier?.toolCallIds.has(target.toolCallId) === true;
|
|
250
|
-
if (!sticky && !
|
|
426
|
+
if (!sticky && !needsMore()) {
|
|
251
427
|
leftEligible += 1;
|
|
252
428
|
continue;
|
|
253
429
|
}
|
|
254
|
-
const
|
|
430
|
+
const folded = await foldedBody(target, options);
|
|
431
|
+
const stubbed = stubToolResultMessage(target, folded.body);
|
|
255
432
|
const before = estimateMessageTokens(target.message);
|
|
256
433
|
const after = estimateMessageTokens(stubbed);
|
|
257
434
|
if (after >= before && !sticky) {
|
|
@@ -259,6 +436,8 @@ export async function compileAttention(options) {
|
|
|
259
436
|
leftEligible += 1;
|
|
260
437
|
continue;
|
|
261
438
|
}
|
|
439
|
+
if (!folded.cached && rememberFold(options.attentionFold, target.toolCallId, folded.body))
|
|
440
|
+
newFoldedBodies += 1;
|
|
262
441
|
const beforeBytes = estimateMessageBytes(target.message);
|
|
263
442
|
target.update(stubbed);
|
|
264
443
|
used -= before - after;
|
|
@@ -266,8 +445,9 @@ export async function compileAttention(options) {
|
|
|
266
445
|
stubbedBytes += Math.max(0, beforeBytes - estimateMessageBytes(stubbed));
|
|
267
446
|
rememberToolCall(frontier, target.toolCallId);
|
|
268
447
|
}
|
|
269
|
-
if (
|
|
270
|
-
|
|
448
|
+
if (gateOpen && stillFired()) {
|
|
449
|
+
const threshold = targetTokens === undefined ? String(decision.firedAxis) : Math.ceil(targetTokens);
|
|
450
|
+
throw new AttentionBudgetError(`attention budget exceeded: estimated ${used} tokens >= ${threshold} (${gateDetail(compiler, decision)}) after dropping ${droppedThinkingTurns} thinking turns and stubbing ${stubbedToolResults} tool results`);
|
|
271
451
|
}
|
|
272
452
|
const mutated = droppedThinkingTurns > 0 || stubbedToolResults > 0;
|
|
273
453
|
return {
|
|
@@ -278,15 +458,26 @@ export async function compileAttention(options) {
|
|
|
278
458
|
usedAfter: Math.max(0, used),
|
|
279
459
|
inputCap: compiler.inputCap,
|
|
280
460
|
triggerRatio: compiler.triggerRatio,
|
|
461
|
+
...(decision.firedAxis === undefined ? {} : { firedAxis: decision.firedAxis }),
|
|
281
462
|
droppedThinkingTurns,
|
|
282
463
|
stubbedToolResults,
|
|
283
464
|
stubbedBytes,
|
|
465
|
+
newFoldedBodies,
|
|
284
466
|
truncated: leftEligible > 0,
|
|
285
467
|
...(options.runId === undefined ? {} : { runId: options.runId }),
|
|
286
468
|
...(options.sessionId === undefined ? {} : { sessionId: options.sessionId }),
|
|
287
469
|
},
|
|
288
470
|
};
|
|
289
471
|
}
|
|
472
|
+
/** Error detail for the fail-closed throw: the legacy wording for the plain `triggerRatio` axis,
|
|
473
|
+
* the axis name (and run budget, when one is in play) for every other axis. */
|
|
474
|
+
function gateDetail(compiler, decision) {
|
|
475
|
+
if (decision.firedAxis === "input_ratio" && decision.targetTokens === compiler.triggerRatio * compiler.inputCap) {
|
|
476
|
+
return `triggerRatio ${compiler.triggerRatio} of inputCap ${compiler.inputCap}`;
|
|
477
|
+
}
|
|
478
|
+
const budget = compiler.runInputBudget === undefined ? "" : ` with run input budget ${compiler.runInputBudget}`;
|
|
479
|
+
return `${String(decision.firedAxis)} gate of inputCap ${compiler.inputCap}${budget}`;
|
|
480
|
+
}
|
|
290
481
|
/** Thinking-bearing assistant-message indexes, oldest first, excluding the newest `keepTurns`.
|
|
291
482
|
* Counts thinking turns, not assistant messages, so a thinking-free reply does not push an
|
|
292
483
|
* older reasoning block out of the keep window. */
|
|
@@ -338,11 +529,17 @@ function toolResultTargets(options) {
|
|
|
338
529
|
return estimateTextBytes(toolResultFoldText(block.result, block.error, message.content)) >= fold.minBytes;
|
|
339
530
|
});
|
|
340
531
|
}
|
|
341
|
-
|
|
532
|
+
/** The stub body for one row: the ledger's copy when it has one, the host `summarize`
|
|
533
|
+
* otherwise. `cached` is the turn's summarize saving — a miss is the only case that costs a
|
|
534
|
+
* host call, and the only case a durable run has new fold state to checkpoint. */
|
|
535
|
+
async function foldedBody(target, options) {
|
|
536
|
+
const cached = options.attentionFold?.bodies.get(target.toolCallId);
|
|
537
|
+
if (cached !== undefined)
|
|
538
|
+
return { body: cached, cached: true };
|
|
342
539
|
const { block, message } = target;
|
|
343
540
|
const text = toolResultFoldText(block.result, block.error, message.content);
|
|
344
541
|
const fold = options.fold;
|
|
345
|
-
const
|
|
542
|
+
const body = fold
|
|
346
543
|
? capToolResultSummary(String(await fold.summarize({
|
|
347
544
|
sessionId: options.sessionId ?? "",
|
|
348
545
|
runId: options.runId ?? "",
|
|
@@ -352,9 +549,13 @@ async function stubToolResultMessage(target, options) {
|
|
|
352
549
|
text,
|
|
353
550
|
})), fold.maxSummaryBytes)
|
|
354
551
|
: attentionStubText(options.redactor?.redact(text) ?? text);
|
|
552
|
+
return { body, cached: false };
|
|
553
|
+
}
|
|
554
|
+
function stubToolResultMessage(target, body) {
|
|
555
|
+
const { message } = target;
|
|
355
556
|
return {
|
|
356
557
|
...message,
|
|
357
|
-
content: message.content.map((part) => part.type === "tool_result" ? { ...part, result: foldedToolResultHeader(target.toolName, target.toolCallId,
|
|
558
|
+
content: message.content.map((part) => part.type === "tool_result" ? { ...part, result: foldedToolResultHeader(target.toolName, target.toolCallId, body) } : part),
|
|
358
559
|
metadata: { ...message.metadata, prismFolded: true },
|
|
359
560
|
};
|
|
360
561
|
}
|
|
@@ -448,6 +649,66 @@ export function restoreAttentionStickyFrontier(persisted) {
|
|
|
448
649
|
frontier.toolCallIds.add(id);
|
|
449
650
|
return frontier;
|
|
450
651
|
}
|
|
652
|
+
export function createAttentionFoldLedger() {
|
|
653
|
+
return { bodies: new Map() };
|
|
654
|
+
}
|
|
655
|
+
/** Live and persisted ledger caps. One body is bounded by the fold's own `maxSummaryBytes`
|
|
656
|
+
* (hard cap 4 KiB), so the entry count is what bounds a durable checkpoint. */
|
|
657
|
+
const MAX_ATTENTION_FOLD_BODIES = 64;
|
|
658
|
+
const ATTENTION_FOLD_SCHEMA_VERSION = 1;
|
|
659
|
+
/** Keep the newest `max` entries: a session that folds more rows than the cap keeps the recent
|
|
660
|
+
* bodies and re-summarizes the oldest if their rows are ever folded again. */
|
|
661
|
+
function trimOldest(bodies, max) {
|
|
662
|
+
while (bodies.size > max) {
|
|
663
|
+
const oldest = bodies.keys().next().value;
|
|
664
|
+
if (oldest === undefined)
|
|
665
|
+
break;
|
|
666
|
+
bodies.delete(oldest);
|
|
667
|
+
}
|
|
668
|
+
}
|
|
669
|
+
/** Record one folded body, keeping the live ledger inside its caps. Returns `false` for a body
|
|
670
|
+
* too large to be worth a checkpoint byte (the fold's own cap makes this host-only in practice). */
|
|
671
|
+
function rememberFold(ledger, toolCallId, body) {
|
|
672
|
+
if (!ledger || estimateTextBytes(body) > HARD_TOOL_RESULT_FOLD_MAX_SUMMARY_BYTES)
|
|
673
|
+
return false;
|
|
674
|
+
ledger.bodies.delete(toolCallId);
|
|
675
|
+
ledger.bodies.set(toolCallId, body);
|
|
676
|
+
trimOldest(ledger.bodies, MAX_ATTENTION_FOLD_BODIES);
|
|
677
|
+
return true;
|
|
678
|
+
}
|
|
679
|
+
/** Bounded snapshot of a live ledger; caller-owned (the runtime persists it, the compiler never
|
|
680
|
+
* writes anywhere). */
|
|
681
|
+
export function serializeAttentionFoldLedger(ledger) {
|
|
682
|
+
return {
|
|
683
|
+
v: ATTENTION_FOLD_SCHEMA_VERSION,
|
|
684
|
+
bodies: [...ledger.bodies].slice(-MAX_ATTENTION_FOLD_BODIES).map(([id, body]) => ({ id, body })),
|
|
685
|
+
};
|
|
686
|
+
}
|
|
687
|
+
/** Validate a persisted ledger from an untrusted store (plan 086 T3). Malformed *entries* are
|
|
688
|
+
* dropped one by one — a body the compiler cannot trust simply re-summarizes on the next turn —
|
|
689
|
+
* while a malformed *shape* yields `undefined` so the caller starts from an empty ledger.
|
|
690
|
+
* Never throws: a resume must not fail because a checkpoint was hand-edited. */
|
|
691
|
+
export function restoreAttentionFoldLedger(value) {
|
|
692
|
+
if (typeof value !== "object" || value === null || Array.isArray(value))
|
|
693
|
+
return undefined;
|
|
694
|
+
const bodies = value.bodies;
|
|
695
|
+
if (!Array.isArray(bodies))
|
|
696
|
+
return undefined;
|
|
697
|
+
const ledger = createAttentionFoldLedger();
|
|
698
|
+
for (const entry of bodies) {
|
|
699
|
+
if (typeof entry !== "object" || entry === null)
|
|
700
|
+
continue;
|
|
701
|
+
const { id, body } = entry;
|
|
702
|
+
if (typeof id !== "string" || id.length === 0 || id.length > MAX_ATTENTION_TOOL_CALL_ID_CHARS || id.includes("\u0000"))
|
|
703
|
+
continue;
|
|
704
|
+
if (typeof body !== "string" || body.length === 0 || estimateTextBytes(body) > HARD_TOOL_RESULT_FOLD_MAX_SUMMARY_BYTES)
|
|
705
|
+
continue;
|
|
706
|
+
ledger.bodies.delete(id);
|
|
707
|
+
ledger.bodies.set(id, body);
|
|
708
|
+
}
|
|
709
|
+
trimOldest(ledger.bodies, MAX_ATTENTION_FOLD_BODIES);
|
|
710
|
+
return ledger;
|
|
711
|
+
}
|
|
451
712
|
/** Consecutive `truncated` turns that arm compaction by default. */
|
|
452
713
|
export const DEFAULT_ATTENTION_TRUNCATION_THRESHOLD = 2;
|
|
453
714
|
export function createAttentionTruncationTrigger(options = {}) {
|
package/dist/cache-helpers.d.ts
CHANGED
|
@@ -14,8 +14,10 @@ export type CacheControlledMessage = Omit<Message, "content"> & {
|
|
|
14
14
|
readonly content: readonly CacheControlledContentBlock[];
|
|
15
15
|
};
|
|
16
16
|
export interface CacheUsageReport {
|
|
17
|
-
|
|
18
|
-
readonly
|
|
17
|
+
/** Present only when the provider reported cache-read usage; never fabricated as zero. */
|
|
18
|
+
readonly cacheReadTokens?: number;
|
|
19
|
+
/** Present only when the provider reported cache-write usage; never fabricated as zero. */
|
|
20
|
+
readonly cacheWriteTokens?: number;
|
|
19
21
|
readonly hitRate?: number;
|
|
20
22
|
readonly estimatedSavings?: number;
|
|
21
23
|
readonly currency?: string;
|
package/dist/cache-helpers.js
CHANGED
|
@@ -67,15 +67,17 @@ export function cacheSavings(usage, model) {
|
|
|
67
67
|
return (read * Math.max(0, input - cacheRead)) / costUnitDivisor(model.cost?.unit);
|
|
68
68
|
}
|
|
69
69
|
export function cacheUsageReport(usage, model) {
|
|
70
|
-
if (!usage)
|
|
70
|
+
if (!usage || (usage.cacheReadTokens === undefined && usage.cacheWriteTokens === undefined))
|
|
71
71
|
return undefined;
|
|
72
72
|
const estimatedSavings = model ? cacheSavings(usage, model) : undefined;
|
|
73
|
+
const currency = estimatedSavings === undefined ? usage.currency : (model?.cost?.currency ?? usage.currency);
|
|
74
|
+
const hitRate = cacheHitRate(usage);
|
|
73
75
|
return {
|
|
74
|
-
cacheReadTokens: usage.cacheReadTokens
|
|
75
|
-
cacheWriteTokens: usage.cacheWriteTokens
|
|
76
|
-
hitRate:
|
|
77
|
-
estimatedSavings,
|
|
78
|
-
currency
|
|
76
|
+
...(usage.cacheReadTokens === undefined ? {} : { cacheReadTokens: usage.cacheReadTokens }),
|
|
77
|
+
...(usage.cacheWriteTokens === undefined ? {} : { cacheWriteTokens: usage.cacheWriteTokens }),
|
|
78
|
+
...(hitRate === undefined ? {} : { hitRate }),
|
|
79
|
+
...(estimatedSavings === undefined ? {} : { estimatedSavings }),
|
|
80
|
+
...(currency === undefined ? {} : { currency }),
|
|
79
81
|
};
|
|
80
82
|
}
|
|
81
83
|
export function resolveBreakpoint(messages, breakpoint) {
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Checkpoint restore hooks (plan 094 Task 3). A hook restores one external layer (git commit,
|
|
3
|
+
* document version, workspace fingerprint) recorded in a checkpoint's sidecar metadata.
|
|
4
|
+
*
|
|
5
|
+
* All-or-nothing: hooks run before the resume claims the checkpoint. The first hook that fails
|
|
6
|
+
* or times out aborts the resume with `CheckpointRestoreError` naming that hook, so the
|
|
7
|
+
* conversation restore never applies on top of a half-restored external world. Hosts that need
|
|
8
|
+
* every layer back where they were re-run the whole restore after fixing the failing layer.
|
|
9
|
+
*/
|
|
10
|
+
/** Per-hook ceiling for a restore (plan 094 Task 3 default). */
|
|
11
|
+
export declare const DEFAULT_CHECKPOINT_RESTORE_TIMEOUT_MS = 10000;
|
|
12
|
+
/** One hook that completed during a restore. */
|
|
13
|
+
export interface CheckpointRestoreAuditEntry {
|
|
14
|
+
readonly hook: string;
|
|
15
|
+
readonly durationMs: number;
|
|
16
|
+
}
|
|
17
|
+
/** Audit of a completed restore: every hook that ran, in order. */
|
|
18
|
+
export interface CheckpointRestoreAudit {
|
|
19
|
+
readonly hooks: readonly CheckpointRestoreAuditEntry[];
|
|
20
|
+
readonly durationMs: number;
|
|
21
|
+
}
|
|
22
|
+
/**
|
|
23
|
+
* Host code restoring one external layer. `signal` aborts on the per-hook timeout and on the
|
|
24
|
+
* caller's abort, so a hook that talks to a remote system can cancel instead of dangling.
|
|
25
|
+
*/
|
|
26
|
+
export type CheckpointRestoreHook<Context> = (checkpoint: Context, signal: AbortSignal) => void | Promise<void>;
|
|
27
|
+
/** Thrown when a restore hook fails or times out; the checkpoint and conversation are untouched. */
|
|
28
|
+
export declare class CheckpointRestoreError extends Error {
|
|
29
|
+
readonly code = "ERR_PRISM_CHECKPOINT_RESTORE";
|
|
30
|
+
/** Name of the failing hook (`fn.name` or `hook[i]`). */
|
|
31
|
+
readonly hook: string;
|
|
32
|
+
constructor(hook: string, cause: unknown);
|
|
33
|
+
}
|
|
34
|
+
export interface RunCheckpointRestoreHooksOptions {
|
|
35
|
+
/** Per-hook timeout; defaults to `DEFAULT_CHECKPOINT_RESTORE_TIMEOUT_MS`. */
|
|
36
|
+
readonly timeoutMs?: number;
|
|
37
|
+
/** Caller abort: checked between hooks and combined into each hook's signal. */
|
|
38
|
+
readonly signal?: AbortSignal;
|
|
39
|
+
}
|
|
40
|
+
/**
|
|
41
|
+
* Run restore hooks sequentially and report the audit. A hook failure throws
|
|
42
|
+
* `CheckpointRestoreError` immediately (later hooks do not run); an already-aborted caller signal
|
|
43
|
+
* throws its own abort reason so the resume reads as cancelled rather than as a restore failure.
|
|
44
|
+
*/
|
|
45
|
+
export declare function runCheckpointRestoreHooks<Context>(hooks: readonly CheckpointRestoreHook<Context>[], context: Context, options?: RunCheckpointRestoreHooksOptions): Promise<CheckpointRestoreAudit>;
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Checkpoint restore hooks (plan 094 Task 3). A hook restores one external layer (git commit,
|
|
3
|
+
* document version, workspace fingerprint) recorded in a checkpoint's sidecar metadata.
|
|
4
|
+
*
|
|
5
|
+
* All-or-nothing: hooks run before the resume claims the checkpoint. The first hook that fails
|
|
6
|
+
* or times out aborts the resume with `CheckpointRestoreError` naming that hook, so the
|
|
7
|
+
* conversation restore never applies on top of a half-restored external world. Hosts that need
|
|
8
|
+
* every layer back where they were re-run the whole restore after fixing the failing layer.
|
|
9
|
+
*/
|
|
10
|
+
/** Per-hook ceiling for a restore (plan 094 Task 3 default). */
|
|
11
|
+
export const DEFAULT_CHECKPOINT_RESTORE_TIMEOUT_MS = 10_000;
|
|
12
|
+
/** Thrown when a restore hook fails or times out; the checkpoint and conversation are untouched. */
|
|
13
|
+
export class CheckpointRestoreError extends Error {
|
|
14
|
+
code = "ERR_PRISM_CHECKPOINT_RESTORE";
|
|
15
|
+
/** Name of the failing hook (`fn.name` or `hook[i]`). */
|
|
16
|
+
hook;
|
|
17
|
+
constructor(hook, cause) {
|
|
18
|
+
super(`Checkpoint restore hook ${hook} failed: ${cause instanceof Error ? cause.message : String(cause)}`, { cause });
|
|
19
|
+
this.name = "CheckpointRestoreError";
|
|
20
|
+
this.hook = hook;
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
/**
|
|
24
|
+
* Run restore hooks sequentially and report the audit. A hook failure throws
|
|
25
|
+
* `CheckpointRestoreError` immediately (later hooks do not run); an already-aborted caller signal
|
|
26
|
+
* throws its own abort reason so the resume reads as cancelled rather than as a restore failure.
|
|
27
|
+
*/
|
|
28
|
+
export async function runCheckpointRestoreHooks(hooks, context, options = {}) {
|
|
29
|
+
const timeoutMs = options.timeoutMs ?? DEFAULT_CHECKPOINT_RESTORE_TIMEOUT_MS;
|
|
30
|
+
const started = Date.now();
|
|
31
|
+
const entries = [];
|
|
32
|
+
for (const [index, hook] of hooks.entries()) {
|
|
33
|
+
options.signal?.throwIfAborted();
|
|
34
|
+
const name = hook.name || `hook[${index}]`;
|
|
35
|
+
const controller = new AbortController();
|
|
36
|
+
const timer = setTimeout(() => controller.abort(new Error(`timed out after ${timeoutMs}ms`)), timeoutMs);
|
|
37
|
+
const signal = options.signal ? AbortSignal.any([options.signal, controller.signal]) : controller.signal;
|
|
38
|
+
const hookStarted = Date.now();
|
|
39
|
+
try {
|
|
40
|
+
await hook(context, signal);
|
|
41
|
+
}
|
|
42
|
+
catch (error) {
|
|
43
|
+
// Our own abort means the hook either ignored the signal or lost the race; name the timeout.
|
|
44
|
+
const timedOut = controller.signal.aborted && !options.signal?.aborted;
|
|
45
|
+
throw new CheckpointRestoreError(name, timedOut ? controller.signal.reason : error);
|
|
46
|
+
}
|
|
47
|
+
finally {
|
|
48
|
+
clearTimeout(timer);
|
|
49
|
+
}
|
|
50
|
+
entries.push({ hook: name, durationMs: Date.now() - hookStarted });
|
|
51
|
+
}
|
|
52
|
+
return { hooks: entries, durationMs: Date.now() - started };
|
|
53
|
+
}
|
|
54
|
+
//# sourceMappingURL=checkpoint-restore.js.map
|
package/dist/checkpoints.js
CHANGED
|
@@ -23,8 +23,11 @@ export function createMemoryCheckpointStore(options = {}) {
|
|
|
23
23
|
}
|
|
24
24
|
const id = recordKey(input);
|
|
25
25
|
const existing = records.get(id);
|
|
26
|
-
|
|
27
|
-
|
|
26
|
+
// A foreign-owned record is indistinguishable from a CAS miss: never an
|
|
27
|
+
// ownership-shaped error (plan 080 Task 3).
|
|
28
|
+
if (existing && !ownershipMatches(input, existing)) {
|
|
29
|
+
throw new CheckpointConflictError(`Checkpoint compare-and-swap failed (expected ${input.expectedVersion ?? 0}, current ${existing.version})`);
|
|
30
|
+
}
|
|
28
31
|
if (input.expectedVersion !== undefined && input.expectedVersion !== (existing?.version ?? 0)) {
|
|
29
32
|
throw new CheckpointConflictError(`Checkpoint compare-and-swap failed (expected ${input.expectedVersion}, current ${existing?.version ?? 0})`);
|
|
30
33
|
}
|
|
@@ -66,8 +69,7 @@ export function createMemoryCheckpointStore(options = {}) {
|
|
|
66
69
|
const record = records.get(recordKey(input));
|
|
67
70
|
if (!record)
|
|
68
71
|
return null;
|
|
69
|
-
|
|
70
|
-
return record;
|
|
72
|
+
return ownershipMatches(input, record) ? record : null;
|
|
71
73
|
},
|
|
72
74
|
async listCheckpoints(query = {}) {
|
|
73
75
|
throwIfAborted(query.signal);
|
|
@@ -94,8 +96,7 @@ export function createMemoryCheckpointStore(options = {}) {
|
|
|
94
96
|
const record = records.get(id);
|
|
95
97
|
if (!record)
|
|
96
98
|
return false;
|
|
97
|
-
|
|
98
|
-
return records.delete(id);
|
|
99
|
+
return ownershipMatches(input, record) ? records.delete(id) : false;
|
|
99
100
|
},
|
|
100
101
|
};
|
|
101
102
|
}
|
|
@@ -121,11 +122,6 @@ function ownershipFilterMatches(expected, actual) {
|
|
|
121
122
|
(expected.accountId === undefined || expected.accountId === actual.accountId) &&
|
|
122
123
|
(expected.userId === undefined || expected.userId === actual.userId));
|
|
123
124
|
}
|
|
124
|
-
function assertOwnership(expected, actual) {
|
|
125
|
-
if (!ownershipMatches(expected, actual)) {
|
|
126
|
-
throw new CheckpointConflictError("Checkpoint ownership mismatch");
|
|
127
|
-
}
|
|
128
|
-
}
|
|
129
125
|
function cloneJson(value, label) {
|
|
130
126
|
try {
|
|
131
127
|
const encoded = JSON.stringify(value);
|
package/dist/context-budget.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { ContextBlock, InputAssemblyLayout, Message, ProviderRequest, Skill, ToolDefinition } from "./contracts.js";
|
|
1
|
+
import type { ContextBlock, InputAssemblyLayout, Message, ProviderRequest, Skill, TokenEstimate, ToolDefinition } from "./contracts.js";
|
|
2
2
|
import { type LoadedSkillSet, type SkillRenderContext, type SkillsDisclosure } from "./skill-disclosure.js";
|
|
3
3
|
/**
|
|
4
4
|
* Host-supplied token estimator. Budget-only: it never reaches billing, provider
|
|
@@ -55,6 +55,7 @@ export declare function isContextBudgetError(error: unknown): error is ContextBu
|
|
|
55
55
|
export declare function estimateTextTokens(text: string): number;
|
|
56
56
|
export declare function estimateTextBytes(text: string): number;
|
|
57
57
|
export declare function estimateMessageTokens(message: Message, estimateTokens?: TokenEstimator): number;
|
|
58
|
+
export declare function estimateMessageTokens(messages: readonly Message[], modelFamily?: string): TokenEstimate;
|
|
58
59
|
export declare function estimateMessageBytes(message: Message): number;
|
|
59
60
|
export declare function estimateAssemblyTokens(messages: readonly Message[]): number;
|
|
60
61
|
export declare function resolveContextBudget(budget: ContextBudget): Required<Pick<ContextBudget, "reportOmissions">> & ContextBudget;
|