@arnilo/prism 0.7.0 → 0.8.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 +35 -0
- package/README.md +12 -11
- package/dist/agent-approval.d.ts +4 -0
- package/dist/agent-approval.js +5 -1
- package/dist/agent-run-lifecycle.js +29 -5
- package/dist/agent-run-state.d.ts +13 -1
- package/dist/agent-run-state.js +12 -2
- package/dist/agent-session/helpers.js +6 -1
- package/dist/agent-session/session/assemble.js +126 -5
- package/dist/agent-session/session/persist.d.ts +16 -0
- package/dist/agent-session/session/persist.js +58 -2
- package/dist/agent-session/session/provider-round.d.ts +3 -3
- package/dist/agent-session/session/provider-round.js +12 -6
- package/dist/agent-session/session/tool-round.js +4 -1
- package/dist/agent-session/session/types.d.ts +12 -1
- package/dist/agent-session/session.d.ts +2 -0
- package/dist/agent-session/session.js +2 -0
- package/dist/checkpoints.js +7 -11
- package/dist/contracts-core/content.d.ts +5 -0
- package/dist/contracts-core/loop.d.ts +42 -0
- package/dist/contracts-core/run-limits.d.ts +2 -0
- package/dist/contracts-protocol.d.ts +15 -3
- package/dist/contracts-run-state.d.ts +26 -5
- package/dist/evidence-grounding.d.ts +29 -0
- package/dist/evidence-grounding.js +162 -0
- package/dist/host-composition.d.ts +13 -0
- package/dist/host-composition.js +33 -2
- package/dist/index.d.ts +6 -2
- package/dist/index.js +3 -1
- package/dist/provider-events.d.ts +3 -1
- package/dist/provider-events.js +2 -2
- 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 +149 -0
- package/dist/testing/state-concurrency-conformance.js +5 -12
- package/docs/ag-ui.md +5 -0
- package/docs/agent-loops.md +33 -0
- package/docs/agent-session-runtime.md +4 -3
- package/docs/coding-security.md +1 -0
- package/docs/coding-tools.md +0 -1
- package/docs/compaction-observational-memory.md +1 -1
- 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 +87 -0
- package/docs/enterprise-postgres-state.md +4 -0
- package/docs/execution-timeline.md +6 -0
- package/docs/guardrails.md +33 -0
- 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 +47 -29
- 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/migrate-to-0.8.md +124 -0
- package/docs/migration.md +30 -0
- package/docs/model-registry.md +12 -2
- package/docs/openapi-tools.md +1 -1
- package/docs/operations.md +1 -3
- package/docs/options-index.md +34 -2
- package/docs/peer-dependencies.md +6 -6
- package/docs/postgres-persistence.md +1 -1
- package/docs/provider-layer.md +2 -2
- package/docs/provider-packages.md +20 -20
- package/docs/providers/neuralwatt.md +5 -1
- package/docs/rag.md +1 -1
- package/docs/release-and-install.md +52 -46
- package/docs/run-bundle.md +92 -0
- package/docs/runs-and-usage.md +14 -0
- package/docs/server.md +2 -0
- 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/telegram-channel.md +157 -0
- package/docs/testing.md +2 -2
- 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/package.json +5 -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/CHANGELOG.md
CHANGED
|
@@ -1,3 +1,38 @@
|
|
|
1
|
+
## [Unreleased]
|
|
2
|
+
|
|
3
|
+
## [0.8.0] - 2026-09-18 (messaging channels, connected apps, work family, durable runs, honesty gates)
|
|
4
|
+
|
|
5
|
+
> **Eleven publishable packages.** `@arnilo/prism-channels` is new; `@arnilo/prism-work` replaces `@arnilo/prism-office`. Predecessor published line is **0.7.0**. Registry/tag writes stay operator-authorized.
|
|
6
|
+
|
|
7
|
+
### Added
|
|
8
|
+
- **Messaging channels.** `@arnilo/prism-channels` ships the transport-neutral runtime (deny-by-default sender authorization, owned session binding, serialized turns, current-run replies, one-use durable approvals, bounded attachment refs), official Telegram (private DMs, opt-in granted groups/topics, private-chat drafts, bounded media, optional voice transcription/synthesis, opt-in notices to one already-bound pair), and experimental pinned signal-cli Signal. See [docs/messaging-channels.md](docs/messaging-channels.md), [docs/telegram-channel.md](docs/telegram-channel.md), [docs/signal-channel.md](docs/signal-channel.md), [docs/messaging-channel-operations.md](docs/messaging-channel-operations.md).
|
|
9
|
+
- **Connected apps and work HTTP.** Identity-bound MCP connected-app sessions admit host-selected transports and register prefixed tools. Google Workspace and Microsoft 365 HTTP adapters live under `@arnilo/prism-work/connectors`. Slack MCP wrap and Open Connector sidecar stay examples. See [docs/connected-apps.md](docs/connected-apps.md), [docs/work-connectors.md](docs/work-connectors.md).
|
|
10
|
+
- **Durable runs, turn-boundary stops, and run-bundle snapshots.** `AgentRunStateOptions.checkpointPolicy: "every-turn"` checkpoints a run at the provider-turn boundary so a crashed worker resumes with the host-only `decision: "continue"` action (never reachable from AG-UI or the server boundary, and rejected while any approval or ready tool call is pending). `RunOptions.turnPolicy` (`TurnPolicyOptions`) stops a run synchronously at a turn boundary and reports `stopReason: "host_policy"` with a redacted, bounded `stopDetail` on the result, the ledger row, the `agent_finished` event, and the execution timeline; a host-policy stop stays resumable. `snapshotRunBundle` returns a `RunBundleSnapshot` — a frozen, redacted digest projection of the effective run bundle with zero store or network reads. `createClaimGroundingGuardrail` (stage `"output"`) blocks or flags numeric claims that no tool result or host evidence supports. `ErrorInfo.failureClass` (`ProviderFailureClass`) types provider failures as `quota` / `rate_limited` / `auth` / `transient` / `permanent` / `unknown`; `ModelCapabilities.toolCallStrictness` adds advisory tool-call reliability. See [docs/durable-runs.md](docs/durable-runs.md), [docs/run-bundle.md](docs/run-bundle.md), [docs/guardrails.md](docs/guardrails.md).
|
|
11
|
+
- **Work sandbox and vendored skills.** `@arnilo/prism-work/sandbox` plus `createWorkComposition` run office/exec in an injected Docker sandbox; connectors stay on the host. The package ships `docx`, `xlsx`, `powerpoint`, `pdf` skills.
|
|
12
|
+
|
|
13
|
+
### Changed
|
|
14
|
+
- **Lockstep `0.7.0` → `0.8.0`.** All eleven publishable manifests move together with `^0.8.0` internal ranges; the lockfile, the `src/index.ts` version constant, the docs index banner, the release-workflow tag lists, and the generated package-truth artifact agree (enforced by `scripts/version-literal-gate.test.mjs`).
|
|
15
|
+
- **Work family rename.** `@arnilo/prism-office` is replaced by `@arnilo/prism-work` (connectors, documents, sheets, diagrams, document-reader, sandbox, skills, tools). No pre-1.0 shim. See [docs/migrate-to-0.8.md](docs/migrate-to-0.8.md).
|
|
16
|
+
- **AG-UI input authority is opt-in server-side.** `CreateAgUiHandlerOptions.inputPolicy.clientState: "ignore"` validates then discards client-supplied AG-UI state and tools before projection, and stops advertising client-provided tools; the default `"honor"` path is byte-identical to 0.7.0.
|
|
17
|
+
- **Observational-memory workers are tool-only.** Text/thinking/done-only turns are successful no-ops. Limit and unknown-tool failures throw `MemoryError` / `MemoryLimitError` rather than matching an English message prefix.
|
|
18
|
+
- **Channel lease release is fail-closed.** In-memory `route.lease` clears only after the store acknowledges; a failed release retries on idle/`stop`. TTL remains the cross-process backstop.
|
|
19
|
+
- **This-tree Postgres evidence.** `release:gate` reports `test:postgres` as pass only when `scripts/postgres-evidence.json` matches current `git rev-parse HEAD`. A stale phase baseline is blocked.
|
|
20
|
+
- **Compat baselines regenerated** at 0.8.0. Inherited 083 `@arnilo/prism-office` → `@arnilo/prism-work` removals (and `prism-core` / `prism-coding-tools` moves) are listed separately from 085 additions. This cut's own Tasks 1–6 add no public removals.
|
|
21
|
+
- **Migration guide for 0.7.0 hosts**: [docs/migrate-to-0.8.md](docs/migrate-to-0.8.md), indexed from [docs/migration.md](docs/migration.md) and [docs/index.md](docs/index.md).
|
|
22
|
+
|
|
23
|
+
### Fixed
|
|
24
|
+
- **Portable work-idempotency error codes survived the work-family move.** The enterprise PostgreSQL `IdempotencyStore` adapter keeps `ERR_PRISM_WORK_IDEMPOTENCY` / `ERR_PRISM_WORK_IDEMPOTENCY_CONFLICT`; only the error class changed (`EnterprisePostgresError`).
|
|
25
|
+
- **Stale ownership assertions in the protected PostgreSQL leg:** a foreign checkpoint scope is a miss plus a generic CAS conflict, and a foreign agent-run status read is indistinguishable from a missing run (`ERR_PRISM_AGENT_RUN_STATE`).
|
|
26
|
+
- **Wiki isolation nested-runner flake.** The wiki scratch gate spawns `node --test --test-isolation=none` (still strips `NODE_TEST_CONTEXT` / `NODE_TEST_WORKER_ID`) so worker IPC deserialization cannot fail the gate under `npm test` load.
|
|
27
|
+
- **Coverage artifact names.** Package keys in `scripts/coverage-summary.json` must equal live workspace manifests (`@arnilo/prism-work`, not `@arnilo/prism-office`).
|
|
28
|
+
- **Alibaba video `fetchUrl`.** Declared `fetchUrl` now downloads generated video bytes; unused OpenAI speech `_bearerHeaders` deleted.
|
|
29
|
+
|
|
30
|
+
### Security
|
|
31
|
+
- **Numeric claims must be grounded or they fail closed.** The claim-grounding guardrail blocks by default, bounds every reported span, caps evidence collection (4,096 figures, 16 levels, 128 KiB), and treats a missing evidence set as ungrounded rather than passing silently.
|
|
32
|
+
- **Crash recovery cannot bypass approval gates.** `"continue"` resumes only a running checkpoint with no pending decision or ready call, and keeps the recorded fingerprint, revision, ownership/fencing, and CAS-version gates.
|
|
33
|
+
- **Failed channel lease release is not success.** This process does not treat the binding as free until the store acknowledges; TTL is the other-process backstop.
|
|
34
|
+
- **Postgres release pass cannot be inherited.** Missing or stale this-tree evidence is blocked, never a pass from a previous commit's counts.
|
|
35
|
+
|
|
1
36
|
## [0.7.0] - 2026-09-15 (extended line: plans 072, 073, 074, 075, 077, 078)
|
|
2
37
|
|
|
3
38
|
> **Channels are not in this cut.** Plan 079 (Telegram/Signal adapters) was reassigned to **0.8.0** so the 0.7.0 cut stops waiting on it; nothing in this release mentions or ships a channel adapter.
|
package/README.md
CHANGED
|
@@ -158,20 +158,21 @@ printf '{"id":"1","command":"prompt","params":{"input":"Hi"}}\n' \
|
|
|
158
158
|
## Packages
|
|
159
159
|
|
|
160
160
|
<!-- generated:package-truth:inventory begin -->
|
|
161
|
-
**
|
|
161
|
+
**11 publishable manifests** — root `@arnilo/prism` plus 10 workspace packages (4 `prism-*` family packages, 6 capability packages). Generated by `node scripts/package-truth.mjs --emit-docs` — do not hand-edit.
|
|
162
162
|
|
|
163
163
|
| package | version | notes |
|
|
164
164
|
| --- | --- | --- |
|
|
165
|
-
| `@arnilo/prism` | 0.
|
|
166
|
-
| `@arnilo/prism-
|
|
167
|
-
| `@arnilo/prism-
|
|
168
|
-
| `@arnilo/prism-
|
|
169
|
-
| `@arnilo/prism-
|
|
170
|
-
| `@arnilo/prism-
|
|
171
|
-
| `@arnilo/prism-
|
|
172
|
-
| `@arnilo/prism-
|
|
173
|
-
| `@arnilo/prism-
|
|
174
|
-
| `@arnilo/prism-web-tools` | 0.
|
|
165
|
+
| `@arnilo/prism` | 0.8.0 | core — runtime, CLI/RPC, templates, docs |
|
|
166
|
+
| `@arnilo/prism-channels` | 0.8.0 | family — transport-neutral messaging runtime, durable journal, pairing and one-use approvals; official /telegram (private DMs, opt-in granted groups/topics) and experimental pinned signal-cli /signal |
|
|
167
|
+
| `@arnilo/prism-coding-tools` | 0.8.0 | family — /agent, /security, /openapi, /computer-use-linux, /dev, /caveman, /ponytail, /impeccable subpaths |
|
|
168
|
+
| `@arnilo/prism-core` | 0.8.0 | family — /runtime, /sessions, /governance, /credentials, /enterprise, /validation subpaths |
|
|
169
|
+
| `@arnilo/prism-providers` | 0.8.0 | family — all provider adapters as `/<adapter>` subpaths |
|
|
170
|
+
| `@arnilo/prism-acp-agent` | 0.8.0 | capability — ACP adapter |
|
|
171
|
+
| `@arnilo/prism-ag-ui` | 0.8.0 | capability — AG-UI/A2A/A2UI adapter |
|
|
172
|
+
| `@arnilo/prism-mcp` | 0.8.0 | capability — MCP client/server/OAuth interop |
|
|
173
|
+
| `@arnilo/prism-memory` | 0.8.0 | capability — memory plus /rag, /compaction/*, /fabric, /graft, /wiki subpaths |
|
|
174
|
+
| `@arnilo/prism-web-tools` | 0.8.0 | capability — Brave/Exa/Firecrawl plus peer-gated /browser and /obscura subpaths |
|
|
175
|
+
| `@arnilo/prism-work` | 0.8.0 | capability — /connectors, /documents, /sheets, /diagrams, /document-reader, /sandbox, /skills, /tools subpaths |
|
|
175
176
|
<!-- generated:package-truth:inventory end -->
|
|
176
177
|
|
|
177
178
|
## Scripts
|
package/dist/agent-approval.d.ts
CHANGED
|
@@ -10,6 +10,10 @@ export declare function pendingDecisionsOf(state: StoredAgentRunState): readonly
|
|
|
10
10
|
* crash with raw TypeErrors. State-dependent checks (foreign/stale/duplicate ids, scope,
|
|
11
11
|
* schema, policy) stay in {@link resolveRunDecisions}. Errors never include tool arguments,
|
|
12
12
|
* elicitation payloads, credentials, or foreign approval details.
|
|
13
|
+
*
|
|
14
|
+
* The legacy `decision` accepts `continue` (plan 084 Task 1) in addition to `approve`/`deny`;
|
|
15
|
+
* it is a crash-recovery action for running-state checkpoints and is resolved in
|
|
16
|
+
* `prepareAgentRunResume`, never as an approval outcome.
|
|
13
17
|
*/
|
|
14
18
|
export declare function assertValidAgentRunResume(resume: AgentRunResume): void;
|
|
15
19
|
interface ResolvedRunDecisions {
|
package/dist/agent-approval.js
CHANGED
|
@@ -28,6 +28,10 @@ export function pendingDecisionsOf(state) {
|
|
|
28
28
|
* crash with raw TypeErrors. State-dependent checks (foreign/stale/duplicate ids, scope,
|
|
29
29
|
* schema, policy) stay in {@link resolveRunDecisions}. Errors never include tool arguments,
|
|
30
30
|
* elicitation payloads, credentials, or foreign approval details.
|
|
31
|
+
*
|
|
32
|
+
* The legacy `decision` accepts `continue` (plan 084 Task 1) in addition to `approve`/`deny`;
|
|
33
|
+
* it is a crash-recovery action for running-state checkpoints and is resolved in
|
|
34
|
+
* `prepareAgentRunResume`, never as an approval outcome.
|
|
31
35
|
*/
|
|
32
36
|
export function assertValidAgentRunResume(resume) {
|
|
33
37
|
const invalid = (message) => new AgentDecisionError("ERR_PRISM_DECISION_INVALID", message);
|
|
@@ -45,7 +49,7 @@ export function assertValidAgentRunResume(resume) {
|
|
|
45
49
|
if (!hasDecision && !hasDecisions)
|
|
46
50
|
throw invalid("Resume requires a decision or decisions");
|
|
47
51
|
if (hasDecision) {
|
|
48
|
-
if (resume.decision !== "approve" && resume.decision !== "deny") {
|
|
52
|
+
if (resume.decision !== "approve" && resume.decision !== "deny" && resume.decision !== "continue") {
|
|
49
53
|
throw invalid("Unknown legacy decision");
|
|
50
54
|
}
|
|
51
55
|
return;
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { assertValidAgentRunResume, pendingDecisionsOf, resolveRunDecisions } from "./agent-approval.js";
|
|
2
|
-
import { parseAttentionStickyFrontier } from "./attention-compiler.js";
|
|
3
2
|
import { agentFingerprint, loadAgentRunState, publicState, saveAgentRunState } from "./agent-run-state.js";
|
|
4
3
|
import { RuntimeAgentSession, throwIfAbortedSignal } from "./agent-session.js";
|
|
4
|
+
import { parseAttentionStickyFrontier } from "./attention-compiler.js";
|
|
5
5
|
import { AgentRunStateError } from "./contracts.js";
|
|
6
6
|
function assertAgentId(actual, expected) {
|
|
7
7
|
if (expected !== undefined && actual !== expected)
|
|
@@ -28,6 +28,7 @@ export function createAgentRunLifecycle(options) {
|
|
|
28
28
|
ownership: request.ownership,
|
|
29
29
|
fencingToken: options.fencingToken,
|
|
30
30
|
definitionRevision: resolved.definitionRevision,
|
|
31
|
+
signal: request.signal,
|
|
31
32
|
persistSessionState: request.persistSessionState,
|
|
32
33
|
includeSkillBodies: request.includeSkillBodies,
|
|
33
34
|
});
|
|
@@ -55,7 +56,8 @@ export function createAgentRunLifecycle(options) {
|
|
|
55
56
|
// Resume free functions moved from agents.ts at 0.1.4 (verbatim; the barrel re-exports the two public ones).
|
|
56
57
|
/** Resume a persisted built-in run. A claimed/dispatched tool is never replayed automatically. */
|
|
57
58
|
export async function resumeAgentRun(agent, ref, resume, options) {
|
|
58
|
-
|
|
59
|
+
throwIfAbortedSignal(options.signal);
|
|
60
|
+
return executePreparedAgentRunResume(await prepareAgentRunResume(agent, ref, resume, options, options.signal), options.signal);
|
|
59
61
|
}
|
|
60
62
|
/** Subscribe before resuming one durable run. Early consumer return aborts that resumed execution. */
|
|
61
63
|
export async function* resumeAgentRunStream(agent, ref, resume, options) {
|
|
@@ -81,20 +83,39 @@ export async function* resumeAgentRunStream(agent, ref, resume, options) {
|
|
|
81
83
|
}
|
|
82
84
|
}
|
|
83
85
|
}
|
|
86
|
+
/**
|
|
87
|
+
* A `continue` resume needs a run whose frontier is intact: a crash-recovery checkpoint
|
|
88
|
+
* (`status: "running"`) or a turn-policy stop, which writes a terminal state that still carries
|
|
89
|
+
* the frontier (plan 084 Task 2). Every other terminal state is final — a naturally finished run
|
|
90
|
+
* must never be resurrected.
|
|
91
|
+
*/
|
|
92
|
+
function isContinuableState(state) {
|
|
93
|
+
return state.status === "running" || (state.status === "succeeded" && state.stopReason === "host_policy");
|
|
94
|
+
}
|
|
84
95
|
async function prepareAgentRunResume(agent, ref, resume, options, signal) {
|
|
85
96
|
throwIfAbortedSignal(signal);
|
|
86
97
|
// Plan 020 Task 2: one shared shape assertion before any checkpoint read/write, agent
|
|
87
98
|
// resolution, subscription, or tool execution. Unknown legacy decisions (e.g. "sideways")
|
|
88
99
|
// and malformed untyped batches fail closed here instead of falling through to approval.
|
|
89
100
|
assertValidAgentRunResume(resume);
|
|
101
|
+
const continuing = resume.decision === "continue";
|
|
90
102
|
const { record, state } = await loadAgentRunState(options.checkpoints, ref, options.ownership);
|
|
91
103
|
if (state.definitionRevision !== options.definitionRevision ||
|
|
92
104
|
state.agentId !== (agent.config.id ?? agent.config.name) ||
|
|
93
105
|
state.fingerprint !== agentFingerprint(agent, options.definitionRevision)) {
|
|
94
106
|
throw new AgentRunStateError("Agent definition revision or fingerprint mismatch on resume");
|
|
95
107
|
}
|
|
96
|
-
if (record.version !== resume.expectedVersion || state.status
|
|
97
|
-
throw new AgentRunStateError("Stale or non-suspended agent run resume");
|
|
108
|
+
if (record.version !== resume.expectedVersion || !(continuing ? isContinuableState(state) : state.status === "suspended")) {
|
|
109
|
+
throw new AgentRunStateError(continuing ? "Stale or non-running agent run resume" : "Stale or non-suspended agent run resume");
|
|
110
|
+
}
|
|
111
|
+
// Crash recovery never bypasses a gate: only a running checkpoint with no unresolved work may
|
|
112
|
+
// continue. A suspended state (tool approval, elicitation, input guardrail) requires a decision.
|
|
113
|
+
if (continuing) {
|
|
114
|
+
const pending = pendingDecisionsOf(state);
|
|
115
|
+
const awaitingDispatch = state.pending?.status === "ready" || state.pendingCalls?.some((entry) => entry.status === "ready") === true;
|
|
116
|
+
if (state.interruption !== undefined || (pending?.length ?? 0) > 0 || awaitingDispatch) {
|
|
117
|
+
throw new AgentRunStateError("Continue resume requires a running checkpoint with no pending decisions");
|
|
118
|
+
}
|
|
98
119
|
}
|
|
99
120
|
const session = new RuntimeAgentSession({ agent, id: state.sessionId, leafId: state.leafId });
|
|
100
121
|
// Plan 078 Task 7: hand the reconstructed session to an observer (supervisor child-event pump)
|
|
@@ -250,7 +271,7 @@ async function prepareAgentRunResume(agent, ref, resume, options, signal) {
|
|
|
250
271
|
fencingToken: options.fencingToken,
|
|
251
272
|
});
|
|
252
273
|
return {
|
|
253
|
-
kind: "
|
|
274
|
+
kind: "claim",
|
|
254
275
|
session,
|
|
255
276
|
state: claimed.state,
|
|
256
277
|
decisions: resolved?.decisionsById,
|
|
@@ -261,6 +282,9 @@ async function prepareAgentRunResume(agent, ref, resume, options, signal) {
|
|
|
261
282
|
interruptBeforeTool: state.interruptBeforeTool,
|
|
262
283
|
fencingToken: options.fencingToken,
|
|
263
284
|
resumeNestedRun: options.resumeNestedRun,
|
|
285
|
+
// The checkpoint records its own cadence (plan 084 Task 1), so a continued run keeps writing
|
|
286
|
+
// turn checkpoints without the host repeating the option on resume.
|
|
287
|
+
...(state.checkpointPolicy ? { checkpointPolicy: state.checkpointPolicy } : {}),
|
|
264
288
|
},
|
|
265
289
|
};
|
|
266
290
|
}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import type { Agent, AgentRunInterruption, AgentRunRef, AgentRunState, AgentRunStateOptions, CheckpointRecord, CheckpointStore, JsonValue, Message, ModelConfig, NestedRunRef, OwnershipScope, RunDecision, RunLimitCounters, StickyDecision, ToolCallContent } from "./contracts.js";
|
|
2
1
|
import { type PersistedAttentionStickyFrontier } from "./attention-compiler.js";
|
|
2
|
+
import type { Agent, AgentRunInterruption, AgentRunRef, AgentRunState, AgentRunStateOptions, CheckpointRecord, CheckpointStore, JsonValue, Message, ModelConfig, NestedRunRef, OwnershipScope, RunDecision, RunLimitCounters, StickyDecision, ToolCallContent } from "./contracts.js";
|
|
3
3
|
import type { SecretRedactor } from "./redaction.js";
|
|
4
4
|
import { type LoadedSkillBodiesEntry } from "./skill-load.js";
|
|
5
5
|
export declare const AGENT_RUN_STATE_NAMESPACE = "prism.agent-run";
|
|
@@ -53,6 +53,18 @@ export interface StoredAgentRunState extends AgentRunState {
|
|
|
53
53
|
};
|
|
54
54
|
/** Per-run allow-list (Task 21). Absent = full registered set (legacy checkpoints). */
|
|
55
55
|
readonly toolNames?: readonly string[];
|
|
56
|
+
/**
|
|
57
|
+
* Recorded checkpoint cadence (plan 084 Task 1). Present only for `"every-turn"` runs, so
|
|
58
|
+
* default checkpoints stay byte-identical. A resume of such a state keeps checkpointing each
|
|
59
|
+
* turn without the host repeating the option.
|
|
60
|
+
*/
|
|
61
|
+
readonly checkpointPolicy?: "every-turn";
|
|
62
|
+
/**
|
|
63
|
+
* Set when a terminal state was written by a `RunOptions.turnPolicy` stop (plan 084 Task 2):
|
|
64
|
+
* the run succeeded cleanly but its frontier is intact, so `decision: "continue"` may resume
|
|
65
|
+
* it. Absent on every other state — a naturally finished run is never continuable.
|
|
66
|
+
*/
|
|
67
|
+
readonly stopReason?: "host_policy";
|
|
56
68
|
}
|
|
57
69
|
/** Session-state caps (plan 015 Task 4): bounded names charged against the run-state byte budget. */
|
|
58
70
|
export declare const MAX_PERSISTED_SKILL_NAMES = 64;
|
package/dist/agent-run-state.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { createHash } from "node:crypto";
|
|
2
|
-
import { AgentLoopStateError, AgentRunStateError } from "./contracts.js";
|
|
3
2
|
import { parseAttentionStickyFrontier } from "./attention-compiler.js";
|
|
3
|
+
import { AgentLoopStateError, AgentRunStateError } from "./contracts.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";
|
|
@@ -102,6 +102,9 @@ export function validateRunStateOptions(options) {
|
|
|
102
102
|
if (!Number.isSafeInteger(bytes) || bytes < 1 || bytes > HARD_MAX_AGENT_RUN_STATE_BYTES) {
|
|
103
103
|
throw new AgentRunStateError(`maxStateBytes must be a positive safe integer at most ${HARD_MAX_AGENT_RUN_STATE_BYTES}`);
|
|
104
104
|
}
|
|
105
|
+
if (options.checkpointPolicy !== undefined && options.checkpointPolicy !== "decision" && options.checkpointPolicy !== "every-turn") {
|
|
106
|
+
throw new AgentRunStateError('checkpointPolicy must be "decision" or "every-turn"');
|
|
107
|
+
}
|
|
105
108
|
}
|
|
106
109
|
export async function loadAgentRunState(checkpoints, ref, ownership) {
|
|
107
110
|
const record = await checkpoints.loadCheckpoint({ namespace: AGENT_RUN_STATE_NAMESPACE, key: ref.runId, ...ownership });
|
|
@@ -130,7 +133,7 @@ export async function saveAgentRunState(input) {
|
|
|
130
133
|
return { record, state: { ...bounded, version: record.version } };
|
|
131
134
|
}
|
|
132
135
|
export function publicState(state) {
|
|
133
|
-
const { input: _input, pending: _pending, pendingCalls: _pendingCalls, nestedRuns: _nestedRuns, interruptBeforeTool: _interruptBeforeTool, counters: _counters, deadlineAt: _deadlineAt, toolNames: _toolNames, ...publicValue } = state;
|
|
136
|
+
const { input: _input, pending: _pending, pendingCalls: _pendingCalls, nestedRuns: _nestedRuns, interruptBeforeTool: _interruptBeforeTool, counters: _counters, deadlineAt: _deadlineAt, toolNames: _toolNames, checkpointPolicy: _checkpointPolicy, stopReason: _stopReason, ...publicValue } = state;
|
|
134
137
|
return publicValue;
|
|
135
138
|
}
|
|
136
139
|
export function initialAgentRunState(input) {
|
|
@@ -152,6 +155,7 @@ export function initialAgentRunState(input) {
|
|
|
152
155
|
interruptBeforeTool: input.interruptBeforeTool,
|
|
153
156
|
counters: input.counters,
|
|
154
157
|
deadlineAt: input.deadlineAt,
|
|
158
|
+
...(input.options.checkpointPolicy === "every-turn" ? { checkpointPolicy: "every-turn" } : {}),
|
|
155
159
|
};
|
|
156
160
|
}
|
|
157
161
|
export function parseAgentRunState(value, version) {
|
|
@@ -215,6 +219,12 @@ export function parseAgentRunState(value, version) {
|
|
|
215
219
|
}
|
|
216
220
|
}
|
|
217
221
|
}
|
|
222
|
+
if (state.checkpointPolicy !== undefined && state.checkpointPolicy !== "every-turn") {
|
|
223
|
+
throw new AgentRunStateError("Malformed agent run checkpoint policy");
|
|
224
|
+
}
|
|
225
|
+
if (state.stopReason !== undefined && state.stopReason !== "host_policy") {
|
|
226
|
+
throw new AgentRunStateError("Malformed agent run stop reason");
|
|
227
|
+
}
|
|
218
228
|
// Load bounds against the hard cap, not the default: the configured maxStateBytes is a
|
|
219
229
|
// save-side policy knob, while the load-side bound is only a DoS ceiling. States saved
|
|
220
230
|
// with a raised maxStateBytes must remain resumable.
|
|
@@ -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;
|
|
@@ -19,11 +19,104 @@ import { resolveToolResultFold } from "../../tool-result-fold.js";
|
|
|
19
19
|
import { createSearchToolsTool, createToolSearchState, resolveToolsDisclosure } from "../../tool-search.js";
|
|
20
20
|
import { 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";
|
|
22
|
+
import { checkpointDurableTurn, cleanupRun, persistDurable, persistSucceeded, suspendDurable } from "./persist.js";
|
|
23
23
|
import { generateWithRetry, recordProviderUsage } 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
|
+
/**
|
|
30
|
+
* `RunOptions.turnPolicy` stopped the run at a turn boundary (plan 084 Task 2). Internal control
|
|
31
|
+
* signal: it unwinds any loop shape and `executeRun` turns it into a clean terminal success with
|
|
32
|
+
* `stopReason: "host_policy"` — never a run error.
|
|
33
|
+
*/
|
|
34
|
+
class AgentRunStopped extends Error {
|
|
35
|
+
constructor() {
|
|
36
|
+
super("Agent run stopped by host turn policy");
|
|
37
|
+
this.name = "AgentRunStopped";
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
/** Host turn-policy misuse: a throwing or malformed callback fails the run closed. */
|
|
41
|
+
class TurnPolicyError extends Error {
|
|
42
|
+
code = "ERR_PRISM_TURN_POLICY";
|
|
43
|
+
constructor(message, options) {
|
|
44
|
+
super(message, options);
|
|
45
|
+
this.name = "TurnPolicyError";
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
/** Validate `RunOptions.turnPolicy` once, before any provider turn (plan 084 Task 2). */
|
|
49
|
+
function assertTurnPolicy(policy, resolvedLimits) {
|
|
50
|
+
if (policy === undefined)
|
|
51
|
+
return;
|
|
52
|
+
if (typeof policy !== "object" || policy === null)
|
|
53
|
+
throw new TypeError("RunOptions.turnPolicy must be an object");
|
|
54
|
+
if (policy.stop !== undefined && typeof policy.stop !== "function") {
|
|
55
|
+
throw new TypeError("RunOptions.turnPolicy.stop must be a function");
|
|
56
|
+
}
|
|
57
|
+
const maxTurns = policy.maxTurns;
|
|
58
|
+
if (maxTurns === undefined)
|
|
59
|
+
return;
|
|
60
|
+
if (!Number.isSafeInteger(maxTurns) || maxTurns < 1) {
|
|
61
|
+
throw new TypeError("RunOptions.turnPolicy.maxTurns must be a positive safe integer");
|
|
62
|
+
}
|
|
63
|
+
// Same narrowing law as `limits`: a run overlay may tighten the agent's cap, never widen it.
|
|
64
|
+
const configured = resolvedLimits.maxTurns;
|
|
65
|
+
if (configured !== null && maxTurns > configured) {
|
|
66
|
+
throw new TypeError(`RunOptions.turnPolicy.maxTurns (${maxTurns}) cannot widen limits.maxTurns (${configured})`);
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
/** Redact and bound a host stop reason; anything unusable fails the run closed. */
|
|
70
|
+
function boundedStopDetail(session, reason) {
|
|
71
|
+
if (typeof reason !== "string" || reason.length === 0) {
|
|
72
|
+
throw new TurnPolicyError("RunOptions.turnPolicy.stop must return a non-empty reason string");
|
|
73
|
+
}
|
|
74
|
+
const redacted = session.redact(reason);
|
|
75
|
+
if (Buffer.byteLength(redacted, "utf8") > TURN_STOP_DETAIL_MAX_BYTES) {
|
|
76
|
+
throw new TurnPolicyError(`RunOptions.turnPolicy.stop reason must be at most ${TURN_STOP_DETAIL_MAX_BYTES} UTF-8 bytes`);
|
|
77
|
+
}
|
|
78
|
+
return redacted;
|
|
79
|
+
}
|
|
80
|
+
/**
|
|
81
|
+
* Evaluate the host turn policy at the current provider-turn boundary (plan 084 Task 2). Returns
|
|
82
|
+
* the stop to record, or `undefined` to run the turn. Omitted policy → nothing is read or called.
|
|
83
|
+
*/
|
|
84
|
+
function evaluateTurnStop(ctx) {
|
|
85
|
+
const policy = ctx.options.turnPolicy;
|
|
86
|
+
if (!policy)
|
|
87
|
+
return undefined;
|
|
88
|
+
const turn = Math.max(1, ctx.session.activeLoopTurn);
|
|
89
|
+
const turns = turn - 1;
|
|
90
|
+
if (policy.maxTurns !== undefined && turns >= policy.maxTurns)
|
|
91
|
+
return { reason: "turn_limit", detail: "maxTurns" };
|
|
92
|
+
if (!policy.stop)
|
|
93
|
+
return undefined;
|
|
94
|
+
const context = {
|
|
95
|
+
sessionId: ctx.session.id,
|
|
96
|
+
runId: ctx.runId,
|
|
97
|
+
turn,
|
|
98
|
+
turns,
|
|
99
|
+
toolCalls: ctx.toolCalls,
|
|
100
|
+
...(ctx.runUsage.value() ? { usage: ctx.runUsage.value() } : {}),
|
|
101
|
+
metadata: ctx.metadata,
|
|
102
|
+
};
|
|
103
|
+
let decision;
|
|
104
|
+
try {
|
|
105
|
+
decision = policy.stop(context);
|
|
106
|
+
}
|
|
107
|
+
catch (error) {
|
|
108
|
+
throw new TurnPolicyError("RunOptions.turnPolicy.stop threw", { cause: error });
|
|
109
|
+
}
|
|
110
|
+
if (decision === null || typeof decision !== "object" || typeof decision.then === "function") {
|
|
111
|
+
throw new TurnPolicyError("RunOptions.turnPolicy.stop must synchronously return a TurnStopDecision");
|
|
112
|
+
}
|
|
113
|
+
const action = decision.action;
|
|
114
|
+
if (action === "continue")
|
|
115
|
+
return undefined;
|
|
116
|
+
if (action !== "stop")
|
|
117
|
+
throw new TurnPolicyError('RunOptions.turnPolicy.stop decision action must be "continue" or "stop"');
|
|
118
|
+
return { reason: "host_policy", detail: boundedStopDetail(ctx.session, decision.reason) };
|
|
119
|
+
}
|
|
27
120
|
function assertPromptVersionRef(ref) {
|
|
28
121
|
if (ref === undefined)
|
|
29
122
|
return undefined;
|
|
@@ -174,6 +267,8 @@ async function assembleRoundContext(params) {
|
|
|
174
267
|
assembledTurn: false,
|
|
175
268
|
artifactFinished: false,
|
|
176
269
|
artifactFailedInfo: undefined,
|
|
270
|
+
toolCalls: 0,
|
|
271
|
+
toolResults: [],
|
|
177
272
|
runUsage,
|
|
178
273
|
loopCtx: undefined,
|
|
179
274
|
};
|
|
@@ -236,10 +331,24 @@ async function assembleRoundContext(params) {
|
|
|
236
331
|
if (!ctx.assembledTurn)
|
|
237
332
|
limits.charge("maxTurns");
|
|
238
333
|
ctx.assembledTurn = false;
|
|
334
|
+
// Host turn policy (plan 084 Task 2): evaluated at the same turn boundary as the
|
|
335
|
+
// crash-recovery checkpoint below, before any provider work. Throwing unwinds any loop
|
|
336
|
+
// shape; `executeRun` converts it into a clean terminal success with `stopReason`.
|
|
337
|
+
const stop = evaluateTurnStop(ctx);
|
|
338
|
+
if (stop) {
|
|
339
|
+
ctx.runStop = stop;
|
|
340
|
+
ctx.loopCtx.finishReason = stop.reason;
|
|
341
|
+
throw new AgentRunStopped();
|
|
342
|
+
}
|
|
343
|
+
// Crash-recovery boundary (plan 084 Task 1): after the previous turn's tool results are in
|
|
344
|
+
// the store and before this provider request. No-op unless `checkpointPolicy: "every-turn"`.
|
|
345
|
+
if (session.activeDurable?.options.checkpointPolicy === "every-turn") {
|
|
346
|
+
await checkpointDurableTurn(session, { runId, model, limits });
|
|
347
|
+
}
|
|
239
348
|
const policyResult = await session.applyProviderRequestPolicies(request, runId, options, metadata, controller.signal);
|
|
240
349
|
const middlewareRequest = (await session.agent.config.middleware?.run("provider_request", policyResult.request)) ?? policyResult.request;
|
|
241
350
|
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));
|
|
351
|
+
return await generateWithRetry(session, session.redactProviderRequest(middlewareRequest), runId, options, controller.signal, policyResult.secrets, session.activeLoopTurn, (turnUsage, turn, attempt) => recordProviderUsage(ctx, turnUsage, turn, attempt), ctx.toolResults);
|
|
243
352
|
}
|
|
244
353
|
catch (error) {
|
|
245
354
|
if (isSteerSoftInterrupt(error)) {
|
|
@@ -288,6 +397,7 @@ export async function executeRun(session, input, options, runId, resumed) {
|
|
|
288
397
|
}
|
|
289
398
|
const requestedLimits = options.limits;
|
|
290
399
|
const resolvedLimits = resolveRunLimits(session.agent.config.limits, requestedLimits);
|
|
400
|
+
assertTurnPolicy(options.turnPolicy, resolvedLimits);
|
|
291
401
|
const durableOptions = options.runState ?? session.agent.config.runState;
|
|
292
402
|
if (session.agent.config.runState && options.runState && session.agent.config.runState !== options.runState) {
|
|
293
403
|
throw new AgentRunStateError("RunOptions cannot replace agent durable run-state configuration");
|
|
@@ -330,6 +440,9 @@ export async function executeRun(session, input, options, runId, resumed) {
|
|
|
330
440
|
const startedAt = new Date().toISOString();
|
|
331
441
|
let runError;
|
|
332
442
|
let runStatus = "succeeded";
|
|
443
|
+
// Set only on the clean-success path; the finish ledger record carries them (plan 084 Task 2).
|
|
444
|
+
let stopReason;
|
|
445
|
+
let stopDetail;
|
|
333
446
|
const runUsage = createUsageAccumulator();
|
|
334
447
|
let usage;
|
|
335
448
|
const metadata = {
|
|
@@ -374,8 +487,16 @@ export async function executeRun(session, input, options, runId, resumed) {
|
|
|
374
487
|
}
|
|
375
488
|
ctx.loop.restore?.(resumedLoopState.snapshot);
|
|
376
489
|
}
|
|
377
|
-
const loopUsage = await runLoopUntilSettled(ctx)
|
|
378
|
-
|
|
490
|
+
const loopUsage = await runLoopUntilSettled(ctx).catch((error) => {
|
|
491
|
+
// Host turn-policy stop (plan 084 Task 2): the loop was unwound on purpose at a turn
|
|
492
|
+
// boundary. Not an error — the run settles cleanly and stays resumable.
|
|
493
|
+
if (error instanceof AgentRunStopped)
|
|
494
|
+
return undefined;
|
|
495
|
+
throw error;
|
|
496
|
+
});
|
|
497
|
+
stopReason = ctx.runStop?.reason ?? ctx.loopCtx.finishReason;
|
|
498
|
+
stopDetail = ctx.runStop?.detail;
|
|
499
|
+
if (!ctx.runStop && ctx.loop.name === "generate-validate-revise" && !ctx.artifactFinished) {
|
|
379
500
|
throw Object.assign(new Error(ctx.artifactFailedInfo?.message ?? "artifact loop ended without a validated artifact"), {
|
|
380
501
|
name: "ArtifactFailed",
|
|
381
502
|
code: ctx.artifactFailedInfo?.code ?? "artifact_failed",
|
|
@@ -418,7 +539,7 @@ export async function executeRun(session, input, options, runId, resumed) {
|
|
|
418
539
|
throw new AgentRunError(result, { cause: error });
|
|
419
540
|
}
|
|
420
541
|
finally {
|
|
421
|
-
await cleanupRun({ session, controller, cleanupSignal, runId, model, startedAt, runStatus, runError });
|
|
542
|
+
await cleanupRun({ session, controller, cleanupSignal, runId, model, startedAt, runStatus, runError, stopReason, stopDetail });
|
|
422
543
|
}
|
|
423
544
|
}
|
|
424
545
|
//# sourceMappingURL=assemble.js.map
|
|
@@ -15,6 +15,19 @@ 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>;
|
|
18
31
|
export declare function persistSucceeded(ctx: RoundContext, loopUsage: Usage | undefined): Promise<AgentRunResult>;
|
|
19
32
|
export declare function cleanupRun(input: {
|
|
20
33
|
session: SessionHost;
|
|
@@ -25,4 +38,7 @@ export declare function cleanupRun(input: {
|
|
|
25
38
|
startedAt: string;
|
|
26
39
|
runStatus: AgentRunResult["status"];
|
|
27
40
|
runError: ErrorInfo | undefined;
|
|
41
|
+
/** Clean stop taxonomy for the finish record; only written for a succeeded run (plan 084 Task 2). */
|
|
42
|
+
stopReason?: import("../../contracts.js").AgentFinishReason;
|
|
43
|
+
stopDetail?: string;
|
|
28
44
|
}): Promise<void>;
|
|
@@ -77,8 +77,50 @@ export async function suspendDurable(session, input) {
|
|
|
77
77
|
counters: input.limits.snapshot(),
|
|
78
78
|
});
|
|
79
79
|
}
|
|
80
|
+
/**
|
|
81
|
+
* Turn-boundary crash-recovery checkpoint (plan 084 Task 1). Called before each provider
|
|
82
|
+
* request when `checkpointPolicy: "every-turn"`; a no-op otherwise, so default-policy runs keep
|
|
83
|
+
* the 0.8.x checkpoint shape and write count unchanged. Pending-decision markers are dropped:
|
|
84
|
+
* at a turn boundary every gated call has been resolved or the run already suspended, and a
|
|
85
|
+
* stale marker must never replay. The recorded `checkpointPolicy` makes the cadence survive
|
|
86
|
+
* into a resumed run, and loop-local state rides along exactly as it does at suspension.
|
|
87
|
+
*/
|
|
88
|
+
export async function checkpointDurableTurn(session, input) {
|
|
89
|
+
const durable = session.activeDurable;
|
|
90
|
+
if (durable?.options.checkpointPolicy !== "every-turn")
|
|
91
|
+
return;
|
|
92
|
+
const loop = session.activeLoop;
|
|
93
|
+
const loopState = loop?.snapshot ? boundedLoopSnapshot(loop.name, loop.revision ?? "1", loop.snapshot()) : undefined;
|
|
94
|
+
const state = durable.state ??
|
|
95
|
+
initialAgentRunState({
|
|
96
|
+
agent: session.agent,
|
|
97
|
+
options: durable.options,
|
|
98
|
+
runId: input.runId,
|
|
99
|
+
sessionId: session.id,
|
|
100
|
+
leafId: session.currentLeafId,
|
|
101
|
+
model: input.model,
|
|
102
|
+
counters: input.limits.snapshot(),
|
|
103
|
+
deadlineAt: input.limits.deadlineAt,
|
|
104
|
+
status: "running",
|
|
105
|
+
interruptBeforeTool: durable.options.interruptBeforeTool,
|
|
106
|
+
});
|
|
107
|
+
await persistDurable(session, {
|
|
108
|
+
...state,
|
|
109
|
+
leafId: session.currentLeafId,
|
|
110
|
+
status: "running",
|
|
111
|
+
interruption: undefined,
|
|
112
|
+
// The input messages are already in the session store by the time a turn boundary is
|
|
113
|
+
// reached; keeping them would re-append them on a later resume.
|
|
114
|
+
input: undefined,
|
|
115
|
+
pending: undefined,
|
|
116
|
+
pendingCalls: undefined,
|
|
117
|
+
...(loopState ? { loopState } : {}),
|
|
118
|
+
counters: input.limits.snapshot(),
|
|
119
|
+
});
|
|
120
|
+
}
|
|
80
121
|
export async function persistSucceeded(ctx, loopUsage) {
|
|
81
122
|
const { session, runId, runUsage } = ctx;
|
|
123
|
+
const stop = ctx.runStop;
|
|
82
124
|
const usage = runUsage.value() ?? loopUsage;
|
|
83
125
|
if (usage && session.activeLedger) {
|
|
84
126
|
const usageRecord = {
|
|
@@ -97,12 +139,15 @@ export async function persistSucceeded(ctx, loopUsage) {
|
|
|
97
139
|
? await persistDurable(session, {
|
|
98
140
|
...session.activeDurable.state,
|
|
99
141
|
status: "succeeded",
|
|
142
|
+
// Plan 084 Task 2: a host-policy stop is terminal for the run but leaves the frontier
|
|
143
|
+
// intact — the loop state is kept and the state is marked continuable.
|
|
144
|
+
...(stop ? { stopReason: "host_policy", leafId: session.currentLeafId } : {}),
|
|
100
145
|
pending: undefined,
|
|
101
146
|
pendingCalls: undefined,
|
|
102
147
|
nestedRuns: undefined,
|
|
103
148
|
stickyDecisions: undefined,
|
|
104
149
|
interruption: undefined,
|
|
105
|
-
loopState: undefined,
|
|
150
|
+
...(stop ? {} : { loopState: undefined }),
|
|
106
151
|
})
|
|
107
152
|
: undefined;
|
|
108
153
|
session.emit({
|
|
@@ -111,8 +156,17 @@ export async function persistSucceeded(ctx, loopUsage) {
|
|
|
111
156
|
runId,
|
|
112
157
|
usage,
|
|
113
158
|
...(ctx.loopCtx.finishReason ? { finishReason: ctx.loopCtx.finishReason } : {}),
|
|
159
|
+
...(stop?.detail ? { stopDetail: stop.detail } : {}),
|
|
160
|
+
});
|
|
161
|
+
const stopReason = ctx.runStop?.reason ?? ctx.loopCtx.finishReason;
|
|
162
|
+
return session.buildRunResult({
|
|
163
|
+
runId,
|
|
164
|
+
status: "succeeded",
|
|
165
|
+
usage,
|
|
166
|
+
runState,
|
|
167
|
+
...(stopReason ? { stopReason } : {}),
|
|
168
|
+
...(stop?.detail ? { stopDetail: stop.detail } : {}),
|
|
114
169
|
});
|
|
115
|
-
return session.buildRunResult({ runId, status: "succeeded", usage, runState });
|
|
116
170
|
}
|
|
117
171
|
export async function cleanupRun(input) {
|
|
118
172
|
const { session, controller, cleanupSignal, runId, model, startedAt, runStatus, runError } = input;
|
|
@@ -138,6 +192,8 @@ export async function cleanupRun(input) {
|
|
|
138
192
|
status: runStatus,
|
|
139
193
|
startedAt,
|
|
140
194
|
finishedAt: new Date().toISOString(),
|
|
195
|
+
...(input.runStatus === "succeeded" && input.stopReason ? { stopReason: input.stopReason } : {}),
|
|
196
|
+
...(input.runStatus === "succeeded" && input.stopDetail ? { stopDetail: input.stopDetail } : {}),
|
|
141
197
|
abortReason: controller.signal.aborted ? String(controller.signal.reason) : undefined,
|
|
142
198
|
error: runError,
|
|
143
199
|
...(session.activePromptVersion ? { promptVersion: session.activePromptVersion } : {}),
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
/** Provider-round phase of runInternal (plan 059). */
|
|
2
|
-
import type { ProviderRequest, ProviderTurnResult, RunOptions, Usage } from "../../contracts.js";
|
|
2
|
+
import type { ProviderRequest, ProviderTurnResult, RunOptions, ToolResult, Usage } from "../../contracts.js";
|
|
3
3
|
import type { RoundContext, SessionHost } from "./types.js";
|
|
4
4
|
export declare function recordProviderUsage(ctx: RoundContext, turnUsage: Usage | undefined, turn: number, attempt: number): Promise<void>;
|
|
5
|
-
export declare function generateWithRetry(session: SessionHost, request: ProviderRequest, runId: string, options: RunOptions, signal: AbortSignal, requestSecrets?: readonly (string | undefined)[], turn?: number, recordUsage?: (usage: Usage | undefined, turn: number, attempt: number) => Promise<void
|
|
6
|
-
export declare function generateProviderTurn(session: SessionHost, request: ProviderRequest, runId: string, signal: AbortSignal, secrets?: readonly (string | undefined)[], turn?: number, attempt?: number, recordUsage?: (usage: Usage | undefined, turn: number, attempt: number) => Promise<void
|
|
5
|
+
export declare function generateWithRetry(session: SessionHost, request: ProviderRequest, runId: string, options: RunOptions, signal: AbortSignal, requestSecrets?: readonly (string | undefined)[], turn?: number, recordUsage?: (usage: Usage | undefined, turn: number, attempt: number) => Promise<void>, toolResults?: readonly ToolResult[]): Promise<ProviderTurnResult>;
|
|
6
|
+
export declare function generateProviderTurn(session: SessionHost, request: ProviderRequest, runId: string, signal: AbortSignal, secrets?: readonly (string | undefined)[], turn?: number, attempt?: number, recordUsage?: (usage: Usage | undefined, turn: number, attempt: number) => Promise<void>, toolResults?: readonly ToolResult[]): Promise<ProviderTurnResult>;
|