@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
package/dist/agent-run-state.js
CHANGED
|
@@ -1,12 +1,14 @@
|
|
|
1
1
|
import { createHash } from "node:crypto";
|
|
2
|
+
import { parseAttentionStickyFrontier, restoreAttentionFoldLedger, } from "./attention-compiler.js";
|
|
2
3
|
import { AgentLoopStateError, AgentRunStateError } from "./contracts.js";
|
|
3
|
-
import { parseAttentionStickyFrontier } from "./attention-compiler.js";
|
|
4
4
|
import { validateLoadedSkillBodies } from "./skill-load.js";
|
|
5
5
|
import { HARD_RUN_TOOL_NAMES } from "./tools.js";
|
|
6
6
|
export const AGENT_RUN_STATE_NAMESPACE = "prism.agent-run";
|
|
7
7
|
export const AGENT_RUN_STATE_SCHEMA_VERSION = 1;
|
|
8
8
|
export const DEFAULT_MAX_AGENT_RUN_STATE_BYTES = 256 * 1024;
|
|
9
9
|
export const HARD_MAX_AGENT_RUN_STATE_BYTES = 1024 * 1024;
|
|
10
|
+
/** Sidecar metadata ceiling per checkpoint record (not the run-state value). */
|
|
11
|
+
export const MAX_AGENT_RUN_METADATA_BYTES = 4 * 1024;
|
|
10
12
|
const MAX_DEPTH = 32;
|
|
11
13
|
const MAX_PROPERTIES = 256;
|
|
12
14
|
/** Session-state caps (plan 015 Task 4): bounded names charged against the run-state byte budget. */
|
|
@@ -102,6 +104,9 @@ export function validateRunStateOptions(options) {
|
|
|
102
104
|
if (!Number.isSafeInteger(bytes) || bytes < 1 || bytes > HARD_MAX_AGENT_RUN_STATE_BYTES) {
|
|
103
105
|
throw new AgentRunStateError(`maxStateBytes must be a positive safe integer at most ${HARD_MAX_AGENT_RUN_STATE_BYTES}`);
|
|
104
106
|
}
|
|
107
|
+
if (options.checkpointPolicy !== undefined && options.checkpointPolicy !== "decision" && options.checkpointPolicy !== "every-turn") {
|
|
108
|
+
throw new AgentRunStateError('checkpointPolicy must be "decision" or "every-turn"');
|
|
109
|
+
}
|
|
105
110
|
}
|
|
106
111
|
export async function loadAgentRunState(checkpoints, ref, ownership) {
|
|
107
112
|
const record = await checkpoints.loadCheckpoint({ namespace: AGENT_RUN_STATE_NAMESPACE, key: ref.runId, ...ownership });
|
|
@@ -113,10 +118,55 @@ export async function loadAgentRunState(checkpoints, ref, ownership) {
|
|
|
113
118
|
record.value.sessionId !== ref.sessionId) {
|
|
114
119
|
throw new AgentRunStateError("Agent run session mismatch");
|
|
115
120
|
}
|
|
116
|
-
|
|
121
|
+
const metadata = readCheckpointMetadata(record.metadata);
|
|
122
|
+
return { record, state: parseAgentRunState(record.value, record.version), ...(metadata ? { metadata } : {}) };
|
|
123
|
+
}
|
|
124
|
+
/** Resolve a host metadata source. A throwing provider fails the checkpoint write (fail closed). */
|
|
125
|
+
export function resolveCheckpointMetadata(source) {
|
|
126
|
+
return typeof source === "function" ? source() : source;
|
|
127
|
+
}
|
|
128
|
+
function checkpointMetadataBytes(metadata) {
|
|
129
|
+
return Buffer.byteLength(JSON.stringify(metadata), "utf8");
|
|
130
|
+
}
|
|
131
|
+
/**
|
|
132
|
+
* Redact + bound a sidecar metadata map for a checkpoint write. Values must be strings;
|
|
133
|
+
* redaction runs first so a replacement marker is still charged against the 4 KiB ceiling.
|
|
134
|
+
*/
|
|
135
|
+
export function boundCheckpointMetadata(metadata, redactor) {
|
|
136
|
+
const redacted = redactor?.redact(metadata) ?? metadata;
|
|
137
|
+
if (!redacted || typeof redacted !== "object" || Array.isArray(redacted)) {
|
|
138
|
+
throw new AgentRunStateError("Checkpoint metadata must be an object");
|
|
139
|
+
}
|
|
140
|
+
const bounded = {};
|
|
141
|
+
for (const [key, value] of Object.entries(redacted)) {
|
|
142
|
+
if (typeof value !== "string")
|
|
143
|
+
throw new AgentRunStateError(`Checkpoint metadata value for ${key} must be a string`);
|
|
144
|
+
bounded[key] = value;
|
|
145
|
+
}
|
|
146
|
+
if (checkpointMetadataBytes(bounded) > MAX_AGENT_RUN_METADATA_BYTES) {
|
|
147
|
+
throw new AgentRunStateError(`Checkpoint metadata exceeds ${MAX_AGENT_RUN_METADATA_BYTES} bytes`);
|
|
148
|
+
}
|
|
149
|
+
return Object.freeze(bounded);
|
|
150
|
+
}
|
|
151
|
+
/**
|
|
152
|
+
* Read-side normalization (legacy tolerance): absent, oversize, or non-string entries are
|
|
153
|
+
* dropped, never thrown — a malformed sidecar must not block a resume.
|
|
154
|
+
*/
|
|
155
|
+
export function readCheckpointMetadata(metadata) {
|
|
156
|
+
if (!metadata || typeof metadata !== "object" || Array.isArray(metadata))
|
|
157
|
+
return undefined;
|
|
158
|
+
const bounded = {};
|
|
159
|
+
for (const [key, value] of Object.entries(metadata)) {
|
|
160
|
+
if (typeof value === "string")
|
|
161
|
+
bounded[key] = value;
|
|
162
|
+
}
|
|
163
|
+
if (Object.keys(bounded).length === 0 || checkpointMetadataBytes(bounded) > MAX_AGENT_RUN_METADATA_BYTES)
|
|
164
|
+
return undefined;
|
|
165
|
+
return Object.freeze(bounded);
|
|
117
166
|
}
|
|
118
167
|
export async function saveAgentRunState(input) {
|
|
119
168
|
const bounded = boundState(input.redactor?.redact(input.state) ?? input.state, input.maxStateBytes ?? DEFAULT_MAX_AGENT_RUN_STATE_BYTES);
|
|
169
|
+
const metadata = input.metadata === undefined ? undefined : boundCheckpointMetadata(input.metadata, input.redactor);
|
|
120
170
|
const record = await input.checkpoints.saveCheckpoint({
|
|
121
171
|
namespace: AGENT_RUN_STATE_NAMESPACE,
|
|
122
172
|
key: bounded.runId,
|
|
@@ -125,12 +175,13 @@ export async function saveAgentRunState(input) {
|
|
|
125
175
|
fencingToken: input.fencingToken,
|
|
126
176
|
value: bounded,
|
|
127
177
|
category: "agent-run",
|
|
178
|
+
...(metadata ? { metadata } : {}),
|
|
128
179
|
...input.ownership,
|
|
129
180
|
});
|
|
130
181
|
return { record, state: { ...bounded, version: record.version } };
|
|
131
182
|
}
|
|
132
183
|
export function publicState(state) {
|
|
133
|
-
const { input: _input, pending: _pending, pendingCalls: _pendingCalls, nestedRuns: _nestedRuns, interruptBeforeTool: _interruptBeforeTool, counters: _counters, deadlineAt: _deadlineAt, toolNames: _toolNames, ...publicValue } = state;
|
|
184
|
+
const { input: _input, pending: _pending, pendingCalls: _pendingCalls, nestedRuns: _nestedRuns, interruptBeforeTool: _interruptBeforeTool, counters: _counters, deadlineAt: _deadlineAt, toolNames: _toolNames, checkpointPolicy: _checkpointPolicy, stopReason: _stopReason, ...publicValue } = state;
|
|
134
185
|
return publicValue;
|
|
135
186
|
}
|
|
136
187
|
export function initialAgentRunState(input) {
|
|
@@ -152,6 +203,7 @@ export function initialAgentRunState(input) {
|
|
|
152
203
|
interruptBeforeTool: input.interruptBeforeTool,
|
|
153
204
|
counters: input.counters,
|
|
154
205
|
deadlineAt: input.deadlineAt,
|
|
206
|
+
...(input.options.checkpointPolicy === "every-turn" ? { checkpointPolicy: "every-turn" } : {}),
|
|
155
207
|
};
|
|
156
208
|
}
|
|
157
209
|
export function parseAgentRunState(value, version) {
|
|
@@ -215,6 +267,12 @@ export function parseAgentRunState(value, version) {
|
|
|
215
267
|
}
|
|
216
268
|
}
|
|
217
269
|
}
|
|
270
|
+
if (state.checkpointPolicy !== undefined && state.checkpointPolicy !== "every-turn") {
|
|
271
|
+
throw new AgentRunStateError("Malformed agent run checkpoint policy");
|
|
272
|
+
}
|
|
273
|
+
if (state.stopReason !== undefined && state.stopReason !== "host_policy") {
|
|
274
|
+
throw new AgentRunStateError("Malformed agent run stop reason");
|
|
275
|
+
}
|
|
218
276
|
// Load bounds against the hard cap, not the default: the configured maxStateBytes is a
|
|
219
277
|
// save-side policy knob, while the load-side bound is only a DoS ceiling. States saved
|
|
220
278
|
// with a raised maxStateBytes must remain resumable.
|
|
@@ -284,12 +342,16 @@ function validateSessionState(sessionState) {
|
|
|
284
342
|
}
|
|
285
343
|
}
|
|
286
344
|
const attention = sessionState.attentionSticky;
|
|
287
|
-
if (attention === undefined)
|
|
288
|
-
return;
|
|
289
345
|
// Both arrays are capped by the parser, and a malformed frontier is dropped rather than
|
|
290
346
|
// failing the resume: re-deciding a mutation is safe, refusing to resume is not.
|
|
291
|
-
if (parseAttentionStickyFrontier(attention) === undefined) {
|
|
347
|
+
if (attention !== undefined && parseAttentionStickyFrontier(attention) === undefined) {
|
|
292
348
|
throw new AgentRunStateError("Malformed agent run attention frontier");
|
|
293
349
|
}
|
|
350
|
+
// Plan 086 T3: the fold ledger gets the same treatment — malformed entries are dropped by the
|
|
351
|
+
// parser, a malformed shape fails the load rather than the first provider turn.
|
|
352
|
+
const fold = sessionState.attentionFold;
|
|
353
|
+
if (fold !== undefined && restoreAttentionFoldLedger(fold) === undefined) {
|
|
354
|
+
throw new AgentRunStateError("Malformed agent run attention fold ledger");
|
|
355
|
+
}
|
|
294
356
|
}
|
|
295
357
|
//# sourceMappingURL=agent-run-state.js.map
|
|
@@ -53,7 +53,12 @@ export function finalAssistantMessage(history) {
|
|
|
53
53
|
return { content: [], text: "" };
|
|
54
54
|
}
|
|
55
55
|
export function errorFromInfo(error) {
|
|
56
|
-
return Object.assign(new Error(error.message), {
|
|
56
|
+
return Object.assign(new Error(error.message), {
|
|
57
|
+
name: error.name ?? "Error",
|
|
58
|
+
cause: error.cause,
|
|
59
|
+
code: error.code,
|
|
60
|
+
failureClass: error.failureClass,
|
|
61
|
+
});
|
|
57
62
|
}
|
|
58
63
|
export class ProviderTurnFailure extends Error {
|
|
59
64
|
info;
|
|
@@ -155,6 +160,8 @@ export function createUsageAccumulator() {
|
|
|
155
160
|
const sums = new Map();
|
|
156
161
|
let costCurrency;
|
|
157
162
|
let costCompatible = true;
|
|
163
|
+
let estimated = false;
|
|
164
|
+
let confidence;
|
|
158
165
|
return {
|
|
159
166
|
add(usage) {
|
|
160
167
|
for (const key of ["inputTokens", "outputTokens", "cacheReadTokens", "cacheWriteTokens"]) {
|
|
@@ -176,6 +183,13 @@ export function createUsageAccumulator() {
|
|
|
176
183
|
if (costCompatible)
|
|
177
184
|
sums.set("cost", (sums.get("cost") ?? 0) + usage.cost);
|
|
178
185
|
}
|
|
186
|
+
// Run totals keep estimate provenance (plan 091 T2): one estimated turn
|
|
187
|
+
// labels the total, and the weakest confidence wins.
|
|
188
|
+
if (usage.estimated === true) {
|
|
189
|
+
estimated = true;
|
|
190
|
+
if (usage.confidence !== undefined && (confidence === undefined || usage.confidence === "low"))
|
|
191
|
+
confidence = usage.confidence;
|
|
192
|
+
}
|
|
179
193
|
},
|
|
180
194
|
value() {
|
|
181
195
|
if (sums.size === 0)
|
|
@@ -187,6 +201,11 @@ export function createUsageAccumulator() {
|
|
|
187
201
|
}
|
|
188
202
|
if (costCompatible && sums.has("cost") && costCurrency !== undefined)
|
|
189
203
|
usage.currency = costCurrency;
|
|
204
|
+
if (estimated) {
|
|
205
|
+
usage.estimated = true;
|
|
206
|
+
if (confidence !== undefined)
|
|
207
|
+
usage.confidence = confidence;
|
|
208
|
+
}
|
|
190
209
|
return Object.keys(usage).length > 0 ? usage : undefined;
|
|
191
210
|
},
|
|
192
211
|
};
|
|
@@ -9,7 +9,7 @@ import { assertGuardrailsAllowed, runGuardrails } from "../../guardrails.js";
|
|
|
9
9
|
import { identityTelemetryAttributes, ownershipFromIdentity, resolveRunIdentity } from "../../identity.js";
|
|
10
10
|
import { assembleProviderInput } from "../../input.js";
|
|
11
11
|
import { errorToErrorInfo, redactRunLedgerRecord } from "../../redaction.js";
|
|
12
|
-
import { RunLimitError, RunLimitTracker, resolveRunLimits } from "../../run-limits.js";
|
|
12
|
+
import { describeBudgetExhaustion, RunLimitError, RunLimitTracker, resolveRunLimits } from "../../run-limits.js";
|
|
13
13
|
import { createSessionEntry } from "../../session-stores.js";
|
|
14
14
|
import { resolveSkillsDisclosure } from "../../skill-disclosure.js";
|
|
15
15
|
import { applyRestoredSkillBodies } from "../../skill-load.js";
|
|
@@ -17,13 +17,117 @@ import { assertStructuredOutputRequestSupported, resolveRunProviderOptions } fro
|
|
|
17
17
|
import { composeSystemPrompt, mergeSystemPromptConfig } from "../../system-prompts.js";
|
|
18
18
|
import { resolveToolResultFold } from "../../tool-result-fold.js";
|
|
19
19
|
import { createSearchToolsTool, createToolSearchState, resolveToolsDisclosure } from "../../tool-search.js";
|
|
20
|
-
import { createToolRegistry, selectRunTools } from "../../tools.js";
|
|
20
|
+
import { clampTurnToolNames, createToolRegistry, selectRunTools } from "../../tools.js";
|
|
21
21
|
import { bridgeAbort, createUsageAccumulator, inputToMessages, isDurableLoop, isSteerSoftInterrupt, mergeGuardrails, throwIfAborted, } from "../helpers.js";
|
|
22
|
-
import { cleanupRun, persistDurable, persistSucceeded, suspendDurable } from "./persist.js";
|
|
23
|
-
import { generateWithRetry, recordProviderUsage } from "./provider-round.js";
|
|
22
|
+
import { checkpointDurableFold, checkpointDurableTurn, cleanupRun, persistDurable, persistSucceeded, suspendDurable } from "./persist.js";
|
|
23
|
+
import { generateWithRetry, recordProviderUsage, resolveDeterministicTurn } from "./provider-round.js";
|
|
24
24
|
import { bindChargeToolRound, bindDispatchToolCall, replayDurableNestedAndPending, runLoopUntilSettled, suspendGatedRound, } from "./tool-round.js";
|
|
25
25
|
const PROMPT_VERSION_MAX_NAME_BYTES = 256;
|
|
26
26
|
const PROMPT_VERSION_HASH_PATTERN = /^sha256:[0-9a-f]{64}$/;
|
|
27
|
+
/** Cap on the host stop detail that reaches the result, ledger, and timeline (plan 084 Task 2). */
|
|
28
|
+
const TURN_STOP_DETAIL_MAX_BYTES = 256;
|
|
29
|
+
function lastAssistantText(history) {
|
|
30
|
+
for (let i = history.length - 1; i >= 0; i -= 1) {
|
|
31
|
+
const message = history[i];
|
|
32
|
+
if (message?.role !== "assistant")
|
|
33
|
+
continue;
|
|
34
|
+
const text = message.content.map((block) => (block.type === "text" ? block.text : "")).join("");
|
|
35
|
+
if (text)
|
|
36
|
+
return text;
|
|
37
|
+
}
|
|
38
|
+
return undefined;
|
|
39
|
+
}
|
|
40
|
+
/**
|
|
41
|
+
* `RunOptions.turnPolicy` stopped the run at a turn boundary (plan 084 Task 2). Internal control
|
|
42
|
+
* signal: it unwinds any loop shape and `executeRun` turns it into a clean terminal success with
|
|
43
|
+
* `stopReason: "host_policy"` — never a run error.
|
|
44
|
+
*/
|
|
45
|
+
class AgentRunStopped extends Error {
|
|
46
|
+
constructor() {
|
|
47
|
+
super("Agent run stopped by host turn policy");
|
|
48
|
+
this.name = "AgentRunStopped";
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
/** Host turn-policy misuse: a throwing or malformed callback fails the run closed. */
|
|
52
|
+
class TurnPolicyError extends Error {
|
|
53
|
+
code = "ERR_PRISM_TURN_POLICY";
|
|
54
|
+
constructor(message, options) {
|
|
55
|
+
super(message, options);
|
|
56
|
+
this.name = "TurnPolicyError";
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
/** Validate `RunOptions.turnPolicy` once, before any provider turn (plan 084 Task 2). */
|
|
60
|
+
function assertTurnPolicy(policy, resolvedLimits) {
|
|
61
|
+
if (policy === undefined)
|
|
62
|
+
return;
|
|
63
|
+
if (typeof policy !== "object" || policy === null)
|
|
64
|
+
throw new TypeError("RunOptions.turnPolicy must be an object");
|
|
65
|
+
if (policy.stop !== undefined && typeof policy.stop !== "function") {
|
|
66
|
+
throw new TypeError("RunOptions.turnPolicy.stop must be a function");
|
|
67
|
+
}
|
|
68
|
+
const maxTurns = policy.maxTurns;
|
|
69
|
+
if (maxTurns === undefined)
|
|
70
|
+
return;
|
|
71
|
+
if (!Number.isSafeInteger(maxTurns) || maxTurns < 1) {
|
|
72
|
+
throw new TypeError("RunOptions.turnPolicy.maxTurns must be a positive safe integer");
|
|
73
|
+
}
|
|
74
|
+
// Same narrowing law as `limits`: a run overlay may tighten the agent's cap, never widen it.
|
|
75
|
+
const configured = resolvedLimits.maxTurns;
|
|
76
|
+
if (configured !== null && maxTurns > configured) {
|
|
77
|
+
throw new TypeError(`RunOptions.turnPolicy.maxTurns (${maxTurns}) cannot widen limits.maxTurns (${configured})`);
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
/** Redact and bound a host stop reason; anything unusable fails the run closed. */
|
|
81
|
+
function boundedStopDetail(session, reason) {
|
|
82
|
+
if (typeof reason !== "string" || reason.length === 0) {
|
|
83
|
+
throw new TurnPolicyError("RunOptions.turnPolicy.stop must return a non-empty reason string");
|
|
84
|
+
}
|
|
85
|
+
const redacted = session.redact(reason);
|
|
86
|
+
if (Buffer.byteLength(redacted, "utf8") > TURN_STOP_DETAIL_MAX_BYTES) {
|
|
87
|
+
throw new TurnPolicyError(`RunOptions.turnPolicy.stop reason must be at most ${TURN_STOP_DETAIL_MAX_BYTES} UTF-8 bytes`);
|
|
88
|
+
}
|
|
89
|
+
return redacted;
|
|
90
|
+
}
|
|
91
|
+
/**
|
|
92
|
+
* Evaluate the host turn policy at the current provider-turn boundary (plan 084 Task 2). Returns
|
|
93
|
+
* the stop to record, or `undefined` to run the turn. Omitted policy → nothing is read or called.
|
|
94
|
+
*/
|
|
95
|
+
function evaluateTurnStop(ctx) {
|
|
96
|
+
const policy = ctx.options.turnPolicy;
|
|
97
|
+
if (!policy)
|
|
98
|
+
return undefined;
|
|
99
|
+
const turn = Math.max(1, ctx.session.activeLoopTurn);
|
|
100
|
+
const turns = turn - 1;
|
|
101
|
+
if (policy.maxTurns !== undefined && turns >= policy.maxTurns)
|
|
102
|
+
return { reason: "turn_limit", detail: "maxTurns" };
|
|
103
|
+
if (!policy.stop)
|
|
104
|
+
return undefined;
|
|
105
|
+
const context = {
|
|
106
|
+
sessionId: ctx.session.id,
|
|
107
|
+
runId: ctx.runId,
|
|
108
|
+
turn,
|
|
109
|
+
turns,
|
|
110
|
+
toolCalls: ctx.toolCalls,
|
|
111
|
+
...(ctx.runUsage.value() ? { usage: ctx.runUsage.value() } : {}),
|
|
112
|
+
metadata: ctx.metadata,
|
|
113
|
+
};
|
|
114
|
+
let decision;
|
|
115
|
+
try {
|
|
116
|
+
decision = policy.stop(context);
|
|
117
|
+
}
|
|
118
|
+
catch (error) {
|
|
119
|
+
throw new TurnPolicyError("RunOptions.turnPolicy.stop threw", { cause: error });
|
|
120
|
+
}
|
|
121
|
+
if (decision === null || typeof decision !== "object" || typeof decision.then === "function") {
|
|
122
|
+
throw new TurnPolicyError("RunOptions.turnPolicy.stop must synchronously return a TurnStopDecision");
|
|
123
|
+
}
|
|
124
|
+
const action = decision.action;
|
|
125
|
+
if (action === "continue")
|
|
126
|
+
return undefined;
|
|
127
|
+
if (action !== "stop")
|
|
128
|
+
throw new TurnPolicyError('RunOptions.turnPolicy.stop decision action must be "continue" or "stop"');
|
|
129
|
+
return { reason: "host_policy", detail: boundedStopDetail(ctx.session, decision.reason) };
|
|
130
|
+
}
|
|
27
131
|
function assertPromptVersionRef(ref) {
|
|
28
132
|
if (ref === undefined)
|
|
29
133
|
return undefined;
|
|
@@ -47,7 +151,13 @@ async function assembleRoundContext(params) {
|
|
|
47
151
|
throwIfAborted(controller.signal);
|
|
48
152
|
session.emit({ type: "agent_started", sessionId: session.id, runId });
|
|
49
153
|
if (resumed)
|
|
50
|
-
session.emit({
|
|
154
|
+
session.emit({
|
|
155
|
+
type: "agent_resumed",
|
|
156
|
+
sessionId: session.id,
|
|
157
|
+
runId,
|
|
158
|
+
version: resumed.version,
|
|
159
|
+
...(resumed.restore ? { restore: resumed.restore } : {}),
|
|
160
|
+
});
|
|
51
161
|
const startRecord = {
|
|
52
162
|
id: runId,
|
|
53
163
|
sessionId: session.id,
|
|
@@ -83,6 +193,7 @@ async function assembleRoundContext(params) {
|
|
|
83
193
|
const tools = searchTool ? [...activeToolList, searchTool] : activeToolList;
|
|
84
194
|
const activeSkills = session.resolveRunSkills(options, tools);
|
|
85
195
|
session.activeRunSkills = activeSkills;
|
|
196
|
+
session.tailSegments.clear();
|
|
86
197
|
if (options.model && JSON.stringify(options.model) !== JSON.stringify(session.agent.config.model)) {
|
|
87
198
|
await session.appendEntry(createSessionEntry({
|
|
88
199
|
sessionId: session.id,
|
|
@@ -122,24 +233,41 @@ async function assembleRoundContext(params) {
|
|
|
122
233
|
assertStructuredOutputRequestSupported(options.model ?? session.agent.config.model, providerOptions);
|
|
123
234
|
const validate = options.validate ?? session.agent.config.validator;
|
|
124
235
|
// Resolved once per run, before any provider turn: a bad setting or a widening run overlay
|
|
125
|
-
// fails here rather than on the turn that happens to cross the ratio (plan 074 C12).
|
|
126
|
-
|
|
236
|
+
// fails here rather than on the turn that happens to cross the ratio (plan 074 C12). The
|
|
237
|
+
// resolved run input budget rides the handle so `run_input_ratio` folds against the same cap
|
|
238
|
+
// the run limit enforces (plan 086 T2); `null` (disabled) leaves that axis on the input cap.
|
|
239
|
+
const attentionCompiler = resolveRunAttentionCompiler(session.agent.config.attentionCompiler, options.attentionCompiler, options.model ?? session.agent.config.model, limits.limits.maxInputTokens);
|
|
240
|
+
// Plan 086 T3: durable folding writes the fold ledger to the run checkpoint, so it needs a
|
|
241
|
+
// durable run (the session's durable state is set before this call). Fail at run start, before
|
|
242
|
+
// any provider turn, rather than folding into memory only. The fold state rides that
|
|
243
|
+
// checkpoint independently of `persistSessionState`.
|
|
244
|
+
if (attentionCompiler?.durable && !session.activeDurable) {
|
|
245
|
+
throw new AgentRunStateError("attentionCompiler.durable requires a durable run: set AgentConfig or RunOptions runState with a checkpoint store");
|
|
246
|
+
}
|
|
247
|
+
session.attentionDurable = attentionCompiler?.durable === true;
|
|
127
248
|
// Telemetry seam (plan 074 T6): one `attention_compiled` per mutated turn, counts and the
|
|
128
249
|
// measured ratio inputs only. Under-ratio turns and compiler-off runs emit nothing.
|
|
250
|
+
// Plan 086 T3: a turn that folded new bodies is the fold-boundary durability signal, so it is
|
|
251
|
+
// remembered here (the callback is synchronous) and checkpointed by the assembler below.
|
|
252
|
+
let foldCheckpointPending = false;
|
|
129
253
|
const onAttentionReport = attentionCompiler
|
|
130
|
-
? (report) =>
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
254
|
+
? (report) => {
|
|
255
|
+
if (report.newFoldedBodies > 0)
|
|
256
|
+
foldCheckpointPending = true;
|
|
257
|
+
session.emit({
|
|
258
|
+
type: "attention_compiled",
|
|
259
|
+
sessionId: session.id,
|
|
260
|
+
runId,
|
|
261
|
+
used: report.used,
|
|
262
|
+
usedAfter: report.usedAfter,
|
|
263
|
+
inputCap: report.inputCap,
|
|
264
|
+
triggerRatio: report.triggerRatio,
|
|
265
|
+
droppedThinkingTurns: report.droppedThinkingTurns,
|
|
266
|
+
stubbedToolResults: report.stubbedToolResults,
|
|
267
|
+
stubbedBytes: report.stubbedBytes,
|
|
268
|
+
truncated: report.truncated,
|
|
269
|
+
});
|
|
270
|
+
}
|
|
143
271
|
: undefined;
|
|
144
272
|
const instructionInjectors = options.instructionInjectors ?? session.agent.config.instructionInjectors ?? [];
|
|
145
273
|
const inputLayout = options.inputLayout ?? session.agent.config.inputLayout;
|
|
@@ -174,9 +302,13 @@ async function assembleRoundContext(params) {
|
|
|
174
302
|
assembledTurn: false,
|
|
175
303
|
artifactFinished: false,
|
|
176
304
|
artifactFailedInfo: undefined,
|
|
305
|
+
toolCalls: 0,
|
|
306
|
+
toolResults: [],
|
|
177
307
|
runUsage,
|
|
178
308
|
loopCtx: undefined,
|
|
179
309
|
};
|
|
310
|
+
const toolNarrowing = options.toolNarrowing ?? session.agent.config.toolNarrowing;
|
|
311
|
+
let narrowedForTurn;
|
|
180
312
|
const loopCtx = {
|
|
181
313
|
sessionId: session.id,
|
|
182
314
|
runId,
|
|
@@ -190,6 +322,38 @@ async function assembleRoundContext(params) {
|
|
|
190
322
|
restoredLoopState: resumed?.state?.loopState?.snapshot,
|
|
191
323
|
assemble: async (nextInput, toolResults, turn) => {
|
|
192
324
|
limits.charge("maxTurns");
|
|
325
|
+
const turnIndex = turn ?? 1;
|
|
326
|
+
let turnTools = tools;
|
|
327
|
+
if (toolNarrowing) {
|
|
328
|
+
if (typeof toolNarrowing !== "function")
|
|
329
|
+
throw new TypeError("toolNarrowing must be a function");
|
|
330
|
+
if (narrowedForTurn?.turn === turnIndex) {
|
|
331
|
+
turnTools = narrowedForTurn.tools;
|
|
332
|
+
}
|
|
333
|
+
else {
|
|
334
|
+
const assistant = lastAssistantText(session.history);
|
|
335
|
+
const requested = await toolNarrowing({
|
|
336
|
+
turn: turnIndex,
|
|
337
|
+
toolIds: tools.map((tool) => tool.name),
|
|
338
|
+
...(assistant !== undefined ? { lastAssistantText: assistant } : {}),
|
|
339
|
+
});
|
|
340
|
+
if (!Array.isArray(requested))
|
|
341
|
+
throw new TypeError("toolNarrowing must return a string array");
|
|
342
|
+
const clamped = clampTurnToolNames(tools, requested);
|
|
343
|
+
if (clamped.dropped.length > 0) {
|
|
344
|
+
session.emit({
|
|
345
|
+
type: "tool_narrowing_clamped",
|
|
346
|
+
sessionId: session.id,
|
|
347
|
+
runId,
|
|
348
|
+
turn: turnIndex,
|
|
349
|
+
dropped: clamped.dropped,
|
|
350
|
+
});
|
|
351
|
+
}
|
|
352
|
+
turnTools = clamped.tools;
|
|
353
|
+
narrowedForTurn = { turn: turnIndex, tools: turnTools };
|
|
354
|
+
}
|
|
355
|
+
ctx.turnAllow = turnTools.map((tool) => tool.name);
|
|
356
|
+
}
|
|
193
357
|
const request = await assembleProviderInput({
|
|
194
358
|
model: options.model ?? session.agent.config.model,
|
|
195
359
|
input: nextInput,
|
|
@@ -213,9 +377,16 @@ async function assembleRoundContext(params) {
|
|
|
213
377
|
// Session-owned: a stub made earlier stays applied even on a later under-ratio turn, so
|
|
214
378
|
// the prompt-cache prefix is not rewritten (C10). Undefined when the compiler is off.
|
|
215
379
|
attentionSticky: attentionCompiler ? session.attentionStickyFor() : undefined,
|
|
380
|
+
// Folded bodies (plan 086 T3): a row summarized once is re-applied, never re-summarized,
|
|
381
|
+
// so sticky rows stay byte-identical and a resumed run reuses the persisted bodies.
|
|
382
|
+
attentionFold: attentionCompiler ? session.attentionFoldFor() : undefined,
|
|
383
|
+
// Charge-so-far for the `run_input_ratio` axis: the counter only holds completed turns,
|
|
384
|
+
// so the axis projects this turn's estimate onto it.
|
|
385
|
+
runInputTokens: limits.snapshot().inputTokens,
|
|
216
386
|
onAttentionReport,
|
|
217
387
|
loadedSkills: session.loadedSkills,
|
|
218
|
-
|
|
388
|
+
tailSegments: session.tailSegments,
|
|
389
|
+
tools: turnTools,
|
|
219
390
|
resourceLoader: session.agent.config.resourceLoader,
|
|
220
391
|
permission: session.agent.config.permission,
|
|
221
392
|
trust: session.agent.config.trust,
|
|
@@ -228,6 +399,13 @@ async function assembleRoundContext(params) {
|
|
|
228
399
|
signal: controller.signal,
|
|
229
400
|
});
|
|
230
401
|
ctx.assembledTurn = true;
|
|
402
|
+
if (foldCheckpointPending) {
|
|
403
|
+
foldCheckpointPending = false;
|
|
404
|
+
// Fold-boundary durability (plan 086 T3): one write per turn that added folded bodies,
|
|
405
|
+
// after the request is assembled and before the provider sees it, so a crash during this
|
|
406
|
+
// turn resumes with the same ledger. No-op unless the compiler is durable.
|
|
407
|
+
await checkpointDurableFold(session, { runId, model, limits });
|
|
408
|
+
}
|
|
231
409
|
return request;
|
|
232
410
|
},
|
|
233
411
|
chargeToolRound: bindChargeToolRound(ctx),
|
|
@@ -236,10 +414,29 @@ async function assembleRoundContext(params) {
|
|
|
236
414
|
if (!ctx.assembledTurn)
|
|
237
415
|
limits.charge("maxTurns");
|
|
238
416
|
ctx.assembledTurn = false;
|
|
417
|
+
// Host turn policy (plan 084 Task 2): evaluated at the same turn boundary as the
|
|
418
|
+
// crash-recovery checkpoint below, before any provider work. Throwing unwinds any loop
|
|
419
|
+
// shape; `executeRun` converts it into a clean terminal success with `stopReason`.
|
|
420
|
+
const stop = evaluateTurnStop(ctx);
|
|
421
|
+
if (stop) {
|
|
422
|
+
ctx.runStop = stop;
|
|
423
|
+
ctx.loopCtx.finishReason = stop.reason;
|
|
424
|
+
throw new AgentRunStopped();
|
|
425
|
+
}
|
|
426
|
+
// Crash-recovery boundary (plan 084 Task 1): after the previous turn's tool results are in
|
|
427
|
+
// the store and before this provider request. No-op unless `checkpointPolicy: "every-turn"`.
|
|
428
|
+
if (session.activeDurable?.options.checkpointPolicy === "every-turn") {
|
|
429
|
+
await checkpointDurableTurn(session, { runId, model, limits });
|
|
430
|
+
}
|
|
431
|
+
// Deterministic no-model turn (plan 096): host middleware answers at the provider boundary,
|
|
432
|
+
// before any provider-round work. No answer → provider path unchanged.
|
|
433
|
+
const deterministic = await resolveDeterministicTurn(session, request, runId, session.activeLoopTurn, controller.signal, ctx.toolResults);
|
|
434
|
+
if (deterministic)
|
|
435
|
+
return deterministic;
|
|
239
436
|
const policyResult = await session.applyProviderRequestPolicies(request, runId, options, metadata, controller.signal);
|
|
240
437
|
const middlewareRequest = (await session.agent.config.middleware?.run("provider_request", policyResult.request)) ?? policyResult.request;
|
|
241
438
|
try {
|
|
242
|
-
return await generateWithRetry(session, session.redactProviderRequest(middlewareRequest), runId, options, controller.signal, policyResult.secrets, session.activeLoopTurn, (turnUsage, turn, attempt) => recordProviderUsage(ctx, turnUsage, turn, attempt));
|
|
439
|
+
return await generateWithRetry(session, session.redactProviderRequest(middlewareRequest), runId, options, controller.signal, policyResult.secrets, session.activeLoopTurn, (turnUsage, turn, attempt) => recordProviderUsage(ctx, turnUsage, turn, attempt, middlewareRequest), ctx.toolResults);
|
|
243
440
|
}
|
|
244
441
|
catch (error) {
|
|
245
442
|
if (isSteerSoftInterrupt(error)) {
|
|
@@ -288,6 +485,7 @@ export async function executeRun(session, input, options, runId, resumed) {
|
|
|
288
485
|
}
|
|
289
486
|
const requestedLimits = options.limits;
|
|
290
487
|
const resolvedLimits = resolveRunLimits(session.agent.config.limits, requestedLimits);
|
|
488
|
+
assertTurnPolicy(options.turnPolicy, resolvedLimits);
|
|
291
489
|
const durableOptions = options.runState ?? session.agent.config.runState;
|
|
292
490
|
if (session.agent.config.runState && options.runState && session.agent.config.runState !== options.runState) {
|
|
293
491
|
throw new AgentRunStateError("RunOptions cannot replace agent durable run-state configuration");
|
|
@@ -321,8 +519,11 @@ export async function executeRun(session, input, options, runId, resumed) {
|
|
|
321
519
|
if (session.activeIdentity && !session.activeOwnership)
|
|
322
520
|
session.activeOwnership = ownershipFromIdentity(session.activeIdentity);
|
|
323
521
|
session.activeIdempotencyKey = options.idempotencyKey ?? session.agent.config.idempotencyKey;
|
|
324
|
-
session.activeGuardrails = mergeGuardrails(session.agent.config.guardrails, options.guardrails);
|
|
522
|
+
session.activeGuardrails = mergeGuardrails(mergeGuardrails(session.agent.config.guardrails, session.packGuardrails), options.guardrails);
|
|
325
523
|
session.activeDurable = resumed ?? (durableOptions ? { options: durableOptions, version: 0 } : undefined);
|
|
524
|
+
// Plan 086 T3: reset here, so a suspension before the compiler is resolved (input guardrail)
|
|
525
|
+
// cannot inherit the previous run's durable-folding flag. `assembleRoundContext` sets it true.
|
|
526
|
+
session.attentionDurable = false;
|
|
326
527
|
session.activeGatedRound = undefined;
|
|
327
528
|
if (resumed)
|
|
328
529
|
session.invalidateSnapshot();
|
|
@@ -330,6 +531,9 @@ export async function executeRun(session, input, options, runId, resumed) {
|
|
|
330
531
|
const startedAt = new Date().toISOString();
|
|
331
532
|
let runError;
|
|
332
533
|
let runStatus = "succeeded";
|
|
534
|
+
// Set only on the clean-success path; the finish ledger record carries them (plan 084 Task 2).
|
|
535
|
+
let stopReason;
|
|
536
|
+
let stopDetail;
|
|
333
537
|
const runUsage = createUsageAccumulator();
|
|
334
538
|
let usage;
|
|
335
539
|
const metadata = {
|
|
@@ -349,6 +553,7 @@ export async function executeRun(session, input, options, runId, resumed) {
|
|
|
349
553
|
deadlineAt: resumed?.state?.deadlineAt,
|
|
350
554
|
});
|
|
351
555
|
session.activeLimits = limits;
|
|
556
|
+
session.activeRecentToolCalls = [];
|
|
352
557
|
const hasFiniteTokenCap = (value) => typeof value === "number" && Number.isFinite(value);
|
|
353
558
|
session.activeLimitOutputBuffer = [session.agent.config.limits, requestedLimits].some((value) => hasFiniteTokenCap(value?.maxOutputTokens) || hasFiniteTokenCap(value?.maxTotalTokens) || value?.maxCost !== undefined);
|
|
354
559
|
try {
|
|
@@ -374,8 +579,16 @@ export async function executeRun(session, input, options, runId, resumed) {
|
|
|
374
579
|
}
|
|
375
580
|
ctx.loop.restore?.(resumedLoopState.snapshot);
|
|
376
581
|
}
|
|
377
|
-
const loopUsage = await runLoopUntilSettled(ctx)
|
|
378
|
-
|
|
582
|
+
const loopUsage = await runLoopUntilSettled(ctx).catch((error) => {
|
|
583
|
+
// Host turn-policy stop (plan 084 Task 2): the loop was unwound on purpose at a turn
|
|
584
|
+
// boundary. Not an error — the run settles cleanly and stays resumable.
|
|
585
|
+
if (error instanceof AgentRunStopped)
|
|
586
|
+
return undefined;
|
|
587
|
+
throw error;
|
|
588
|
+
});
|
|
589
|
+
stopReason = ctx.runStop?.reason ?? ctx.loopCtx.finishReason;
|
|
590
|
+
stopDetail = ctx.runStop?.detail;
|
|
591
|
+
if (!ctx.runStop && ctx.loop.name === "generate-validate-revise" && !ctx.artifactFinished) {
|
|
379
592
|
throw Object.assign(new Error(ctx.artifactFailedInfo?.message ?? "artifact loop ended without a validated artifact"), {
|
|
380
593
|
name: "ArtifactFailed",
|
|
381
594
|
code: ctx.artifactFailedInfo?.code ?? "artifact_failed",
|
|
@@ -392,8 +605,18 @@ export async function executeRun(session, input, options, runId, resumed) {
|
|
|
392
605
|
return session.buildRunResult({ runId, status: "suspended", runState: error.state, interruption: error.interruption });
|
|
393
606
|
}
|
|
394
607
|
runError = errorToErrorInfo(error);
|
|
395
|
-
session.emit({ type: "error", sessionId: session.id, runId, error: runError });
|
|
396
608
|
const breach = error instanceof RunLimitError ? error.breach : limits.breach;
|
|
609
|
+
// Terminal attribution before the terminal `error`/finish records, so a subscriber that stops
|
|
610
|
+
// at the first terminal event still sees why the run died (plan 087 T2).
|
|
611
|
+
if (breach) {
|
|
612
|
+
session.emit({
|
|
613
|
+
type: "budget_exhausted",
|
|
614
|
+
sessionId: session.id,
|
|
615
|
+
runId,
|
|
616
|
+
...describeBudgetExhaustion(limits, breach, session.activeRecentToolCalls ?? []),
|
|
617
|
+
});
|
|
618
|
+
}
|
|
619
|
+
session.emit({ type: "error", sessionId: session.id, runId, error: runError });
|
|
397
620
|
runStatus = breach ? "failed" : controller.signal.aborted ? "aborted" : "failed";
|
|
398
621
|
const runState = session.activeDurable?.state
|
|
399
622
|
? await persistDurable(session, {
|
|
@@ -418,7 +641,7 @@ export async function executeRun(session, input, options, runId, resumed) {
|
|
|
418
641
|
throw new AgentRunError(result, { cause: error });
|
|
419
642
|
}
|
|
420
643
|
finally {
|
|
421
|
-
await cleanupRun({ session, controller, cleanupSignal, runId, model, startedAt, runStatus, runError });
|
|
644
|
+
await cleanupRun({ session, controller, cleanupSignal, runId, model, startedAt, runStatus, runError, stopReason, stopDetail });
|
|
422
645
|
}
|
|
423
646
|
}
|
|
424
647
|
//# sourceMappingURL=assemble.js.map
|
|
@@ -15,6 +15,30 @@ export declare function suspendDurable(session: SessionHost, input: {
|
|
|
15
15
|
/** Full replacement when provided; otherwise the recorded nested runs are preserved. */
|
|
16
16
|
readonly nestedRuns?: readonly NestedRunRef[];
|
|
17
17
|
}): Promise<AgentRunState>;
|
|
18
|
+
/**
|
|
19
|
+
* Turn-boundary crash-recovery checkpoint (plan 084 Task 1). Called before each provider
|
|
20
|
+
* request when `checkpointPolicy: "every-turn"`; a no-op otherwise, so default-policy runs keep
|
|
21
|
+
* the 0.8.x checkpoint shape and write count unchanged. Pending-decision markers are dropped:
|
|
22
|
+
* at a turn boundary every gated call has been resolved or the run already suspended, and a
|
|
23
|
+
* stale marker must never replay. The recorded `checkpointPolicy` makes the cadence survive
|
|
24
|
+
* into a resumed run, and loop-local state rides along exactly as it does at suspension.
|
|
25
|
+
*/
|
|
26
|
+
export declare function checkpointDurableTurn(session: SessionHost, input: {
|
|
27
|
+
readonly runId: string;
|
|
28
|
+
readonly model: ModelConfig;
|
|
29
|
+
readonly limits: RunLimitTracker;
|
|
30
|
+
}): Promise<void>;
|
|
31
|
+
/**
|
|
32
|
+
* Fold-boundary checkpoint (plan 086 T3). Called once per turn that added folded bodies — never
|
|
33
|
+
* per turn — when the resolved compiler is durable, so a crash after a fold resumes with the
|
|
34
|
+
* ledger and frontier already on disk. Independent of `checkpointPolicy`: the fold is the
|
|
35
|
+
* durability point that matters for a long single run, not the turn boundary.
|
|
36
|
+
*/
|
|
37
|
+
export declare function checkpointDurableFold(session: SessionHost, input: {
|
|
38
|
+
readonly runId: string;
|
|
39
|
+
readonly model: ModelConfig;
|
|
40
|
+
readonly limits: RunLimitTracker;
|
|
41
|
+
}): Promise<void>;
|
|
18
42
|
export declare function persistSucceeded(ctx: RoundContext, loopUsage: Usage | undefined): Promise<AgentRunResult>;
|
|
19
43
|
export declare function cleanupRun(input: {
|
|
20
44
|
session: SessionHost;
|
|
@@ -25,4 +49,7 @@ export declare function cleanupRun(input: {
|
|
|
25
49
|
startedAt: string;
|
|
26
50
|
runStatus: AgentRunResult["status"];
|
|
27
51
|
runError: ErrorInfo | undefined;
|
|
52
|
+
/** Clean stop taxonomy for the finish record; only written for a succeeded run (plan 084 Task 2). */
|
|
53
|
+
stopReason?: import("../../contracts.js").AgentFinishReason;
|
|
54
|
+
stopDetail?: string;
|
|
28
55
|
}): Promise<void>;
|