@arnilo/prism 0.10.0 → 0.11.1
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 +32 -1
- package/README.md +18 -16
- package/dist/agent-run-lifecycle.d.ts +2 -1
- package/dist/agent-run-lifecycle.js +1 -1
- package/dist/agent-session/session/assemble.js +9 -7
- package/dist/agent-session/session/tool-round.js +30 -20
- package/dist/agent-session/session/types.d.ts +1 -0
- package/dist/agent-session/session.d.ts +1 -0
- package/dist/agent-session/session.js +3 -2
- package/dist/checkpoint-restore.d.ts +50 -14
- package/dist/checkpoint-restore.js +104 -28
- package/dist/contracts-core/session.d.ts +2 -1
- package/dist/contracts-run-state.d.ts +12 -4
- package/dist/index.d.ts +3 -3
- package/dist/index.js +1 -1
- package/dist/leases.js +32 -6
- package/dist/node/contribution-discovery.d.ts +16 -1
- package/dist/node/contribution-discovery.js +47 -0
- package/dist/node/session-store-jsonl.js +67 -17
- package/dist/run-limits.d.ts +11 -5
- package/dist/session-stores.js +61 -12
- package/dist/testing/prefix-stability-conformance.d.ts +44 -1
- package/dist/testing/prefix-stability-conformance.js +92 -29
- package/dist/usage-estimation.d.ts +7 -1
- package/dist/usage-estimation.js +16 -10
- package/docs/acp.md +2 -2
- package/docs/agent-events.md +7 -6
- package/docs/agent-session-runtime.md +1 -1
- package/docs/coding-agent-tools.md +1 -1
- package/docs/coding-tools.md +7 -11
- package/docs/context-and-skills.md +6 -7
- package/docs/contribution-discovery.md +13 -0
- package/docs/durable-runs.md +10 -3
- package/docs/embeddings.md +3 -1
- package/docs/execution-timeline.md +6 -0
- package/docs/extensions.md +1 -2
- package/docs/impeccable.md +1 -2
- package/docs/index.md +26 -21
- package/docs/live-testing.md +1 -2
- package/docs/memory-fabric.md +3 -2
- package/docs/migrate-to-0.11.md +65 -0
- package/docs/migration.md +12 -1
- package/docs/node-jsonl-session-store.md +4 -3
- package/docs/operations.md +1 -1
- package/docs/peer-dependencies.md +3 -5
- package/docs/policy-and-audit.md +1 -1
- package/docs/prefix-stability-conformance.md +30 -7
- package/docs/provider-packages.md +23 -21
- package/docs/providers/laya.md +113 -0
- package/docs/providers/typesafe.md +145 -0
- package/docs/public-contracts.md +1 -1
- package/docs/rag.md +2 -2
- package/docs/release-and-install.md +60 -58
- package/docs/runs-and-usage.md +6 -4
- package/docs/session-stores.md +2 -2
- package/docs/supervisors.md +14 -6
- package/docs/testing.md +17 -9
- package/docs/workflows.md +2 -2
- package/package.json +5 -4
- package/docs/caveman.md +0 -130
- package/docs/graft.md +0 -149
- package/docs/ponytail.md +0 -129
package/CHANGELOG.md
CHANGED
|
@@ -1,6 +1,33 @@
|
|
|
1
|
+
## [0.11.1] - 2026-09-24 (System One decision models)
|
|
2
|
+
|
|
3
|
+
### Added
|
|
4
|
+
- **`@arnilo/prism-providers/typesafe` and `@arnilo/prism-providers/laya`.** Structured-output decision models on `POST /v1/systemone`. Jev is hosted (`https://api.typesafe.ai`, env `TYPESAFE_API_KEY`). Laya is self-hosted `laya-serve` (default `http://localhost:8000`, optional `LAYA_API_KEY`). A request without `options.structuredOutput`, or with tools, fails before any network call. See [docs/providers/typesafe.md](docs/providers/typesafe.md) and [docs/providers/laya.md](docs/providers/laya.md).
|
|
5
|
+
|
|
6
|
+
## [0.11.0] - 2026-09-24 (store bounds, lease fence reset, subpath removals)
|
|
7
|
+
|
|
8
|
+
> **Twelve publishable packages.** Lockstep minor. No new public exports from the review remediation. Two host-visible store contracts change, and three subpaths that the published 0.10.0 tarball still shipped are removed. Predecessor published line is **0.10.0**. Registry/tag writes stay operator-authorized.
|
|
9
|
+
|
|
10
|
+
### Changed
|
|
11
|
+
- **Lockstep `0.10.0` → `0.11.0`.** All twelve publishable manifests move together with `^0.11.0` internal ranges. The lockfile, the `src/index.ts` version constant, the docs index banner, the release-workflow tag list, and the generated package-truth artifact agree (`scripts/version-literal-gate.test.mjs`).
|
|
12
|
+
- **Compat baseline regenerated.** The only declaration this cut changed is `version` (`"0.10.0"` → `"0.11.0"`). The same regen records the already-landed persona/graft removals (58 `@arnilo/prism-memory`, 48 `@arnilo/prism-coding-tools`) and the signature widenings already named in [docs/migration.md](docs/migration.md). No plan-120 public name was added.
|
|
13
|
+
- **Memory session store implements `readBranchPath`.** A snapshot walks the branch once and clones each kept entry once. JSONL and the durable adapters already had the method.
|
|
14
|
+
- **JSONL parse cache.** Reads after an in-process append reuse the parsed file when `(size, mtimeMs)` match. A same-size write inside one filesystem timestamp tick can still look unchanged; the upgrade path is hashing the file.
|
|
15
|
+
- **Idempotency window is 4,096 keys** on the memory store and the JSONL store. A replay of a key still inside the window is rejected. A replay of an evicted key appends a new entry. See [docs/migrate-to-0.11.md](docs/migrate-to-0.11.md).
|
|
16
|
+
- **In-memory lease sweep.** Expired rows are deleted once the map reaches 1,024. A released key that is still in the map inherits `fencingToken + 1`. A key that was swept starts its next fence at 1 (`docs/operations.md`). SQLite and Postgres adapters still keep the counter on the expired row.
|
|
17
|
+
- **Plain-text token estimates share one helper** (`ceil(length/4)`). Message and entry estimates are unchanged and are not billing numbers. Golden vectors live in `scripts/token-estimate-fixtures.json` (not shipped).
|
|
18
|
+
- **`npm test` gains two stages:** examples execution (spawns examples not already executed; `docker-process-session.ts` is `known-broken:placeholder-image`) and a Node branch-coverage audit (floor 83.49, `dist/**` only). Bun's coverage gate still records `branches: null`. Offline test budget pin is `< 110s`.
|
|
19
|
+
- **Freeze-test package presence** comes from `workspacePackageCounts()` in `scripts/package-truth.mjs`. Frozen count deltas stay in each suite.
|
|
20
|
+
|
|
21
|
+
### Removed
|
|
22
|
+
- **Behavior persona subpaths (`@arnilo/prism-coding-tools/caveman`, `/ponytail`).** Both subpaths, their vendored upstream fixtures, and the `@dietrichgebert/ponytail` optional peer are gone. Load the upstream tree with `loadSkillDirectory` and register skills from a host extension. `@arnilo/prism-coding-tools/impeccable` is unchanged. See [docs/migrate-to-0.11.md](docs/migrate-to-0.11.md).
|
|
23
|
+
- **Graft bridge subpath (`@arnilo/prism-memory/graft`).** The subpath, its `@nanonets/graft` optional peer, the fixture CLI, and the graft commands are gone. Integrate graft as a host MCP server, or author tools against `registerTool` / `registerCommand`. `/rag`, `/compaction/*`, `/fabric`, `/wiki`, and `/scoped` stay.
|
|
24
|
+
|
|
25
|
+
### Security
|
|
26
|
+
- No new credential surface. The lease sweep does not delete a live lease. Idempotency eviction cannot be used to skip the in-window duplicate check.
|
|
27
|
+
|
|
1
28
|
## [0.10.0] - 2026-09-21 (hook lifecycle completion, scoped agent memory)
|
|
2
29
|
|
|
3
|
-
> **Twelve publishable packages.** All 105–106 features ship behind their documented options and defaults: `@arnilo/prism-hooks` is the new twelfth package, and scoped memory is an opt-in subpath that does nothing until a host constructs it. Predecessor published line is **0.9.0**.
|
|
30
|
+
> **Twelve publishable packages.** All 105–106 features ship behind their documented options and defaults: `@arnilo/prism-hooks` is the new twelfth package, and scoped memory is an opt-in subpath that does nothing until a host constructs it. Predecessor published line is **0.9.0**. Plan 097 (trajectory export) was not implemented for this line and is deferred rather than held unstarted against the cut. Registry/tag writes stay operator-authorized.
|
|
4
31
|
|
|
5
32
|
### Added
|
|
6
33
|
- **Scoped persistent memory (`@arnilo/prism-memory/scoped`).** New opt-in subpath: `createScopedMemoryPolicy({ memory, fabric, scopeRoot, policy })` is a policy and lifecycle layer over stores the host already configured (no fifth store). Create is inert and fails closed unless `scope.resourceId` equals the resolved `scopeRoot` and a `threadId` is set; the read path oversamples `fabric.recall`, applies an abstain floor (`activation.minSimilarity` 0.35) and activation budget (`topK` 3), ranks by `fabricScore × exp(−ageDays/tauDays) × (1 + ln(1 + uses))` (`tauDays` 30), and returns `{ hits: [], abstained: true }` rather than a weak hit. Writes go through a conservative post-run reviewer (`reviewSession`, strict JSON, garbage → zero writes), land as `candidate` records, and flip to `verified` after `promotion.reuseThreshold` (2) successful recalls — ledger status only, no fabric rewrite. `gcPass()` proposes archives onto `pending()` and never deletes (`legal_hold` skipped), `rememberFact` keeps a bounded working block (`maxChars` 2200, overflow throws `MemoryLimitError` with `consolidate first`), an injection scan plus an optional staged approval gate (`approval.default: "off"`) guard content, and a deterministic git audit mirror renders notes. State lives in one JSON ledger at `<scopeRoot>/.memory/state.json` (ids, counters, timestamps — never note content). Sizing: one reviewer call per run, one ledger read plus one write per recall, no model calls on the read path. See [docs/scoped-memory.md](docs/scoped-memory.md).
|
|
@@ -10,6 +37,7 @@
|
|
|
10
37
|
- **Pre-compaction rewrite seam.** The `compaction_request` middleware hook runs after `compaction_started` and before the strategy, so a handler returns the `CompactionContext` the strategy actually compacts (entries and budget) and the following `compaction` hook plus `compaction_finished` event report that result. See [docs/middleware-hooks.md](docs/middleware-hooks.md).
|
|
11
38
|
- **`@arnilo/prism-hooks`, the hooks-file adapter.** New twelfth publishable package: `parseHooksConfig(text | object)` accepts the Claude flat and Codex `{ "hooks": { … } }` shapes, and `createHooksExtension(config, options)` compiles them onto public seams — `SessionStart` / `UserPromptSubmit` `additionalContext` through an instruction injector, `UserPromptSubmit` / `PreToolUse` / `PostToolUse` guardrails for denies, `tool_call` / `tool_result` middleware for `updatedInput` and post-tool context, and `Stop` onto stop hooks. Handlers are `command` (spawned shell-free, hash-pinned with `hookCommandHash()`) or `mcp_tool` (host-provided client, `${field.path}` templates), with seconds-based `timeout`, per-handler `additionalContextLimit` (default 2500 tokens, spilling to `<tempdir>/hook_outputs/`), and Codex's `continue: false` read as the stop signal. See [docs/hooks.md](docs/hooks.md).
|
|
12
39
|
- **Unified hook map.** [docs/hooks.md](docs/hooks.md) is the owning page for the hook model: the five families (transform, decide, inject, run-end control, observe), one row per Claude Code / Codex event mapped to its Prism surface or a documented non-goal with rationale (`PermissionRequest` stays host policy), the adapter reference, and migration steps from an existing `hooks.json` config.
|
|
40
|
+
- **Host skill-directory loader.** `loadSkillDirectory(directory, { maxSkillBytes? })` on `@arnilo/prism/node/contribution-discovery` loads a host-supplied `<dir>/<name>/SKILL.md` tree into inert `Skill[]`: one level, sorted, symlink-contained, per-file cap 262 144 bytes by default. It replaces the bounded reader the deleted persona subpaths carried. See [docs/contribution-discovery.md](docs/contribution-discovery.md).
|
|
13
41
|
- **Examples.** `examples/scoped-memory.ts` (scope guard → facts overflow → reviewer → abstain/hit recall → promotion → GC approval → mirror), `examples/hooks-json.ts` (a declarative `hooks.json` compiled onto guardrails, injectors, and stop hooks), and `examples/hooks-audit-command.mjs` (an audit-trail command handler).
|
|
14
42
|
|
|
15
43
|
### Changed
|
|
@@ -19,6 +47,9 @@
|
|
|
19
47
|
- **Coverage thresholds gained the new package's row** (`@arnilo/prism-hooks` 84.7 / 78.67 / 90), so the coverage summary fails closed on a new package instead of passing silently.
|
|
20
48
|
- **Migration notes for 0.9.0 hosts**: [docs/migration.md](docs/migration.md) — the `AgentSession.close()` addition, `session_start` / `session_shutdown` now being emitted, the new `hook_limit` finish reason, the `maxStopContinuations` default, and the opt-in scoped-memory subpath.
|
|
21
49
|
|
|
50
|
+
### Removed
|
|
51
|
+
- None in the published 0.10.0 tarball. A post-publish amendment recorded the persona and graft subpath removals here; they ship in 0.11.0 (see the section above).
|
|
52
|
+
|
|
22
53
|
### Security
|
|
23
54
|
- **Stop hooks and the hooks-file adapter fail closed.** `StopHook` entries are validated at run start (`name` plus `decide`), a malformed decision or a throwing handler is a non-blocking error that never silently allows a stop, `maxStopContinuations` (default 3) bounds continuation loops, and the adapter's `command` handlers spawn shell-free with tokenized argv and a `hookCommandHash()` trust pin. Codex's `continue: false` reads as the stop signal, not a continuation.
|
|
24
55
|
- **Scoped memory never widens access and never deletes.** Create fails closed on a scope mismatch, unknown policy fields are ignored and invalid values fail closed, recall abstains below the floor, GC only proposes, `legal_hold` is skipped, the ledger stores no note content, and the mirror is deterministic.
|
package/README.md
CHANGED
|
@@ -162,28 +162,30 @@ printf '{"id":"1","command":"prompt","params":{"input":"Hi"}}\n' \
|
|
|
162
162
|
|
|
163
163
|
| package | version | notes |
|
|
164
164
|
| --- | --- | --- |
|
|
165
|
-
| `@arnilo/prism` | 0.
|
|
166
|
-
| `@arnilo/prism-channels` | 0.
|
|
167
|
-
| `@arnilo/prism-coding-tools` | 0.
|
|
168
|
-
| `@arnilo/prism-core` | 0.
|
|
169
|
-
| `@arnilo/prism-providers` | 0.
|
|
170
|
-
| `@arnilo/prism-acp-agent` | 0.
|
|
171
|
-
| `@arnilo/prism-ag-ui` | 0.
|
|
172
|
-
| `@arnilo/prism-hooks` | 0.
|
|
173
|
-
| `@arnilo/prism-mcp` | 0.
|
|
174
|
-
| `@arnilo/prism-memory` | 0.
|
|
175
|
-
| `@arnilo/prism-web-tools` | 0.
|
|
176
|
-
| `@arnilo/prism-work` | 0.
|
|
165
|
+
| `@arnilo/prism` | 0.11.1 | core — runtime, CLI/RPC, templates, docs |
|
|
166
|
+
| `@arnilo/prism-channels` | 0.11.1 | 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.11.1 | family — /agent, /security, /openapi, /computer-use-linux, /dev, /impeccable subpaths |
|
|
168
|
+
| `@arnilo/prism-core` | 0.11.1 | family — /runtime, /sessions, /governance, /credentials, /enterprise, /validation subpaths |
|
|
169
|
+
| `@arnilo/prism-providers` | 0.11.1 | family — all provider adapters as `/<adapter>` subpaths |
|
|
170
|
+
| `@arnilo/prism-acp-agent` | 0.11.1 | capability — ACP adapter |
|
|
171
|
+
| `@arnilo/prism-ag-ui` | 0.11.1 | capability — AG-UI/A2A/A2UI adapter |
|
|
172
|
+
| `@arnilo/prism-hooks` | 0.11.1 | capability — Claude/Codex-compatible hooks.json adapter compiled onto middleware, guardrail, injector, and stop-hook seams |
|
|
173
|
+
| `@arnilo/prism-mcp` | 0.11.1 | capability — MCP client/server/OAuth interop |
|
|
174
|
+
| `@arnilo/prism-memory` | 0.11.1 | capability — memory plus /rag, /compaction/*, /fabric, /wiki subpaths |
|
|
175
|
+
| `@arnilo/prism-web-tools` | 0.11.1 | capability — Brave/Exa/Firecrawl plus peer-gated /browser and /obscura subpaths |
|
|
176
|
+
| `@arnilo/prism-work` | 0.11.1 | capability — /connectors, /documents, /sheets, /diagrams, /document-reader, /sandbox, /skills, /tools subpaths |
|
|
177
177
|
<!-- generated:package-truth:inventory end -->
|
|
178
178
|
|
|
179
179
|
## Scripts
|
|
180
180
|
|
|
181
181
|
| command | action |
|
|
182
182
|
|---------|--------|
|
|
183
|
-
| `
|
|
184
|
-
| `
|
|
185
|
-
| `
|
|
186
|
-
| `
|
|
183
|
+
| `bun ci` | Install dependencies from the committed `bun.lock` (frozen) |
|
|
184
|
+
| `bun run build` | Compile TypeScript to `dist/` (core + workspaces) |
|
|
185
|
+
| `bun run typecheck` | Type-check without emitting |
|
|
186
|
+
| `bun run test` | Build + run network-free tests (`npm test` still works) |
|
|
187
|
+
| `bun run test:coverage` | Coverage gate: `bun test --coverage` over the core suite and every gated workspace, with Bun-measured floors (`scripts/coverage-thresholds.json`) |
|
|
188
|
+
| `bun run test:live` | Run live suites whose credentials are present (skip the rest) |
|
|
187
189
|
| `prism --help` | CLI help |
|
|
188
190
|
|
|
189
191
|
## Non-goals (v1)
|
|
@@ -15,7 +15,8 @@ export interface AgentRunLifecycleOptions {
|
|
|
15
15
|
/**
|
|
16
16
|
* Plan 094 Task 3: external-state restore hooks, run on every claiming resume before the
|
|
17
17
|
* checkpoint is claimed. Registered once here because a resume builds its session from the
|
|
18
|
-
* stored state (there is no live session to register against beforehand).
|
|
18
|
+
* stored state (there is no live session to register against beforehand). Plan 109 Task 2: an
|
|
19
|
+
* entry may be `{ id?, restore, compensate? }` so a failed resume rolls the applied layers back.
|
|
19
20
|
*/
|
|
20
21
|
readonly restoreHooks?: readonly AgentCheckpointRestoreHook[];
|
|
21
22
|
/** Per-hook restore ceiling in ms; defaults to `DEFAULT_CHECKPOINT_RESTORE_TIMEOUT_MS`. */
|
|
@@ -317,7 +317,7 @@ async function prepareAgentRunResume(agent, ref, resume, options, signal) {
|
|
|
317
317
|
status: state.status,
|
|
318
318
|
...(recordMetadata ? { metadata: recordMetadata } : {}),
|
|
319
319
|
checkpoint: record,
|
|
320
|
-
}, { timeoutMs: options.restoreHookTimeoutMs, signal })
|
|
320
|
+
}, { timeoutMs: options.restoreHookTimeoutMs, signal, redactor: agent.config.redactor })
|
|
321
321
|
: undefined;
|
|
322
322
|
throwIfAbortedSignal(signal);
|
|
323
323
|
const claimed = await saveAgentRunState({
|
|
@@ -753,15 +753,12 @@ export async function executeRun(session, input, options, runId, resumed) {
|
|
|
753
753
|
}
|
|
754
754
|
runError = errorToErrorInfo(error);
|
|
755
755
|
const breach = error instanceof RunLimitError ? error.breach : limits.breach;
|
|
756
|
+
// Built once: the event and the terminal result carry the same attribution payload (plan 108 T5).
|
|
757
|
+
const exhaustion = breach ? describeBudgetExhaustion(limits, breach, session.activeRecentToolCalls ?? []) : undefined;
|
|
756
758
|
// Terminal attribution before the terminal `error`/finish records, so a subscriber that stops
|
|
757
759
|
// at the first terminal event still sees why the run died (plan 087 T2).
|
|
758
|
-
if (
|
|
759
|
-
session.emit({
|
|
760
|
-
type: "budget_exhausted",
|
|
761
|
-
sessionId: session.id,
|
|
762
|
-
runId,
|
|
763
|
-
...describeBudgetExhaustion(limits, breach, session.activeRecentToolCalls ?? []),
|
|
764
|
-
});
|
|
760
|
+
if (exhaustion) {
|
|
761
|
+
session.emit({ type: "budget_exhausted", sessionId: session.id, runId, ...exhaustion });
|
|
765
762
|
}
|
|
766
763
|
session.emit({ type: "error", sessionId: session.id, runId, error: runError });
|
|
767
764
|
runStatus = breach ? "failed" : controller.signal.aborted ? "aborted" : "failed";
|
|
@@ -781,6 +778,11 @@ export async function executeRun(session, input, options, runId, resumed) {
|
|
|
781
778
|
status: runStatus,
|
|
782
779
|
usage: runUsage.value() ?? usage,
|
|
783
780
|
limit: breach,
|
|
781
|
+
attribution: exhaustion && {
|
|
782
|
+
consumed: exhaustion.consumed,
|
|
783
|
+
closestOtherAxes: exhaustion.closestOtherAxes,
|
|
784
|
+
recentToolCalls: exhaustion.recentToolCalls,
|
|
785
|
+
},
|
|
784
786
|
error: runError,
|
|
785
787
|
abortReason: !breach && controller.signal.aborted ? String(controller.signal.reason) : undefined,
|
|
786
788
|
runState,
|
|
@@ -2,8 +2,8 @@
|
|
|
2
2
|
import { AgentRunSuspended, decisionIdentityRef, decisionScopesEqual, nestedApprovalId, nestedOutcomeToolResult, pathsEqual, } from "../../agent-approval.js";
|
|
3
3
|
import { toolElicitationRequest } from "../../agent-tool-dispatch.js";
|
|
4
4
|
import { AgentDecisionError, AgentDelegationSuspendedError, AgentRunStateError, DEFAULT_MAX_PENDING_DECISIONS, HARD_MAX_PENDING_DECISIONS, MAX_ATTRIBUTION_DEPTH, } from "../../contracts.js";
|
|
5
|
-
import { toToolResultMessage } from "../../input.js";
|
|
6
5
|
import { runGuardrails } from "../../guardrails.js";
|
|
6
|
+
import { toToolResultMessage } from "../../input.js";
|
|
7
7
|
import { canonicalToolEffectJson, toolEffectArgumentsHash } from "../../tool-effects.js";
|
|
8
8
|
import { dispatchToolCall, resolveToolEffectDeclaration } from "../../tools.js";
|
|
9
9
|
import { randomId } from "../helpers.js";
|
|
@@ -148,6 +148,16 @@ export async function applyNestedRun(session, input) {
|
|
|
148
148
|
return { toolResult: nestedOutcomeToolResult(outcome, input.toolCall.id, input.toolCall.name) };
|
|
149
149
|
}
|
|
150
150
|
}
|
|
151
|
+
function buildRunInterruption(input) {
|
|
152
|
+
return {
|
|
153
|
+
kind: input.kind,
|
|
154
|
+
reason: input.reason,
|
|
155
|
+
...(input.toolCallId ? { toolCallId: input.toolCallId } : {}),
|
|
156
|
+
...(input.toolName ? { toolName: input.toolName } : {}),
|
|
157
|
+
...(input.guardrail ? { guardrail: input.guardrail } : {}),
|
|
158
|
+
pendingDecisions: input.decisions,
|
|
159
|
+
};
|
|
160
|
+
}
|
|
151
161
|
export async function suspendGatedRound(ctx) {
|
|
152
162
|
const gated = ctx.session.activeGatedRound;
|
|
153
163
|
if (!gated?.size)
|
|
@@ -155,14 +165,14 @@ export async function suspendGatedRound(ctx) {
|
|
|
155
165
|
const entries = [...gated.values()];
|
|
156
166
|
const decisions = entries.map((gatedCall) => gatedCall.decision);
|
|
157
167
|
const single = decisions.length === 1 ? decisions[0] : undefined;
|
|
158
|
-
const interruption = {
|
|
168
|
+
const interruption = buildRunInterruption({
|
|
169
|
+
decisions,
|
|
159
170
|
kind: single?.kind === "elicitation" ? "elicitation" : "tool_approval",
|
|
160
171
|
reason: single ? single.reason : `${decisions.length} tool side effects require approval`,
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
};
|
|
172
|
+
toolCallId: single?.toolCallId,
|
|
173
|
+
toolName: single?.scope.toolName,
|
|
174
|
+
guardrail: single?.guardrail,
|
|
175
|
+
});
|
|
166
176
|
throw new AgentRunSuspended(await suspendDurable(ctx.session, {
|
|
167
177
|
runId: ctx.runId,
|
|
168
178
|
model: ctx.model,
|
|
@@ -195,13 +205,13 @@ export async function suspendNested(ctx, nested) {
|
|
|
195
205
|
throw new AgentDecisionError("ERR_PRISM_DECISION_LIMIT", `Pending decisions exceed ${HARD_MAX_PENDING_DECISIONS} per run`);
|
|
196
206
|
}
|
|
197
207
|
const single = decisions.length === 1 ? decisions[0] : undefined;
|
|
198
|
-
const interruption = {
|
|
208
|
+
const interruption = buildRunInterruption({
|
|
209
|
+
decisions,
|
|
199
210
|
kind: single?.kind ?? "tool_approval",
|
|
200
211
|
reason: single ? single.reason : `${decisions.length} approval request(s) need a decision`,
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
};
|
|
212
|
+
toolCallId: single?.toolCallId,
|
|
213
|
+
toolName: single?.scope.toolName,
|
|
214
|
+
});
|
|
205
215
|
throw new AgentRunSuspended(await suspendDurable(ctx.session, {
|
|
206
216
|
runId: ctx.runId,
|
|
207
217
|
model: ctx.model,
|
|
@@ -384,13 +394,13 @@ export function bindDispatchToolCall(ctx) {
|
|
|
384
394
|
return;
|
|
385
395
|
const approvalId = randomId("approval");
|
|
386
396
|
const decision = buildPendingDecision(ctx.session, mediatedCall, approvalId, ctx.registry, ctx.runId, ctx.metadata, ctx.controller.signal);
|
|
387
|
-
const interruption = {
|
|
397
|
+
const interruption = buildRunInterruption({
|
|
398
|
+
decisions: [decision],
|
|
388
399
|
kind: "tool_approval",
|
|
389
400
|
reason: decision.reason,
|
|
390
401
|
toolCallId: mediatedCall.id,
|
|
391
402
|
toolName: mediatedCall.name,
|
|
392
|
-
|
|
393
|
-
};
|
|
403
|
+
});
|
|
394
404
|
throw new AgentRunSuspended(await suspendDurable(ctx.session, {
|
|
395
405
|
runId: ctx.runId,
|
|
396
406
|
model: ctx.model,
|
|
@@ -477,13 +487,13 @@ export async function replayDurableNestedAndPending(ctx) {
|
|
|
477
487
|
if (remainingOwn.length > 0 || surfacedPending.length > 0) {
|
|
478
488
|
const pendingDecisions = [...remainingOwn, ...surfacedPending];
|
|
479
489
|
const single = pendingDecisions.length === 1 ? pendingDecisions[0] : undefined;
|
|
480
|
-
const interruption = {
|
|
490
|
+
const interruption = buildRunInterruption({
|
|
491
|
+
decisions: pendingDecisions,
|
|
481
492
|
kind: single?.kind ?? "tool_approval",
|
|
482
493
|
reason: `${pendingDecisions.length} approval request(s) remain`,
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
};
|
|
494
|
+
toolCallId: single?.toolCallId,
|
|
495
|
+
toolName: single?.scope.toolName,
|
|
496
|
+
});
|
|
487
497
|
throw new AgentRunSuspended(await suspendDurable(session, {
|
|
488
498
|
runId: ctx.runId,
|
|
489
499
|
model: ctx.model,
|
|
@@ -107,6 +107,7 @@ export type SessionHost = {
|
|
|
107
107
|
readonly status: AgentRunResult["status"];
|
|
108
108
|
readonly usage?: Usage;
|
|
109
109
|
readonly limit?: import("../../contracts.js").RunLimitBreach;
|
|
110
|
+
readonly attribution?: import("../../run-limits.js").BudgetExhaustionAttribution;
|
|
110
111
|
readonly error?: ErrorInfo;
|
|
111
112
|
readonly abortReason?: string;
|
|
112
113
|
readonly runState?: import("../../contracts.js").AgentRunState;
|
|
@@ -183,6 +183,7 @@ export declare class RuntimeAgentSession implements AgentSession {
|
|
|
183
183
|
readonly status: AgentRunResult["status"];
|
|
184
184
|
readonly usage?: Usage;
|
|
185
185
|
readonly limit?: import("../contracts.js").RunLimitBreach;
|
|
186
|
+
readonly attribution?: import("../run-limits.js").BudgetExhaustionAttribution;
|
|
186
187
|
readonly error?: ErrorInfo;
|
|
187
188
|
readonly abortReason?: string;
|
|
188
189
|
readonly stopReason?: import("../contracts.js").AgentFinishReason;
|
|
@@ -422,6 +422,7 @@ export class RuntimeAgentSession {
|
|
|
422
422
|
message: final.message,
|
|
423
423
|
usage: input.usage,
|
|
424
424
|
limit: input.limit,
|
|
425
|
+
attribution: input.attribution,
|
|
425
426
|
error: input.error,
|
|
426
427
|
abortReason: input.abortReason,
|
|
427
428
|
stopReason: input.stopReason,
|
|
@@ -486,8 +487,8 @@ export class RuntimeAgentSession {
|
|
|
486
487
|
});
|
|
487
488
|
}
|
|
488
489
|
branchReader() {
|
|
489
|
-
// ponytail: prefer
|
|
490
|
-
//
|
|
490
|
+
// ponytail: prefer readBranchPath when present (memory, SQLite, Postgres) so snapshot does
|
|
491
|
+
// not list() the whole session. JSONL and other omitters fall back to list() + in-memory walk.
|
|
491
492
|
const read = this.store.readBranchPath;
|
|
492
493
|
return read ? (query) => read.call(this.store, query) : undefined;
|
|
493
494
|
}
|
|
@@ -1,12 +1,17 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Checkpoint restore hooks (plan 094 Task 3). A hook restores
|
|
3
|
-
* document version, workspace fingerprint) recorded in a
|
|
2
|
+
* Checkpoint restore hooks (plan 094 Task 3; reverse compensation plan 109 Task 2). A hook restores
|
|
3
|
+
* one external layer (git commit, document version, workspace fingerprint) recorded in a
|
|
4
|
+
* checkpoint's sidecar metadata.
|
|
4
5
|
*
|
|
5
|
-
* All-or-nothing: hooks run before the resume claims the checkpoint. The first hook that fails
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
6
|
+
* All-or-nothing: hooks run before the resume claims the checkpoint. The first hook that fails or
|
|
7
|
+
* times out aborts the resume with `CheckpointRestoreError` naming that hook, so the conversation
|
|
8
|
+
* restore never applies on top of a half-restored external world. A handler may also declare a
|
|
9
|
+
* `compensate` direction: on failure the applied layers — including the failing one, which may be
|
|
10
|
+
* half-applied — are undone in reverse order and the error carries a best-effort
|
|
11
|
+
* `CheckpointRestoreCompensation` report. Compensation never runs after the caller aborted, never
|
|
12
|
+
* masks the original failure, and the checkpoint stays resumable either way.
|
|
9
13
|
*/
|
|
14
|
+
import type { SecretRedactor } from "./redaction.js";
|
|
10
15
|
/** Per-hook ceiling for a restore (plan 094 Task 3 default). */
|
|
11
16
|
export declare const DEFAULT_CHECKPOINT_RESTORE_TIMEOUT_MS = 10000;
|
|
12
17
|
/** One hook that completed during a restore. */
|
|
@@ -20,26 +25,57 @@ export interface CheckpointRestoreAudit {
|
|
|
20
25
|
readonly durationMs: number;
|
|
21
26
|
}
|
|
22
27
|
/**
|
|
23
|
-
* Host code restoring one external layer. `signal` aborts on the per-hook timeout
|
|
24
|
-
* caller's abort, so a hook that talks to a remote system can cancel instead of dangling.
|
|
28
|
+
* Host code restoring or compensating one external layer. `signal` aborts on the per-hook timeout
|
|
29
|
+
* and on the caller's abort, so a hook that talks to a remote system can cancel instead of dangling.
|
|
25
30
|
*/
|
|
26
31
|
export type CheckpointRestoreHook<Context> = (checkpoint: Context, signal: AbortSignal) => void | Promise<void>;
|
|
32
|
+
/** Object form of a restore handler: an identity plus the layer's undo direction (plan 109 Task 2). */
|
|
33
|
+
interface CheckpointRestoreHandlerObject<Context> {
|
|
34
|
+
/** Stable name reported in the audit and in `CheckpointRestoreError.compensation`; defaults to the restore hook's name, then `hook[i]`. */
|
|
35
|
+
readonly id?: string;
|
|
36
|
+
readonly restore: CheckpointRestoreHook<Context>;
|
|
37
|
+
/** Undo for this layer, run when a later hook — or this one — fails. Absent = the layer has no undo. */
|
|
38
|
+
readonly compensate?: CheckpointRestoreHook<Context>;
|
|
39
|
+
}
|
|
40
|
+
/**
|
|
41
|
+
* A restore handler: today's bare function, or `{ id?, restore, compensate? }` to declare an undo
|
|
42
|
+
* direction. Bare functions behave exactly as in plan 094 and are never compensated.
|
|
43
|
+
*/
|
|
44
|
+
export type CheckpointRestoreHandler<Context> = CheckpointRestoreHook<Context> | CheckpointRestoreHandlerObject<Context>;
|
|
45
|
+
/** Best-effort record of the reverse compensation pass after a failed restore (plan 109 Task 2). */
|
|
46
|
+
export interface CheckpointRestoreCompensation {
|
|
47
|
+
/** Names of the hooks whose `compensate` ran, most recently applied layer first. */
|
|
48
|
+
readonly ran: readonly string[];
|
|
49
|
+
/** First compensation that failed; the pass still continues with the remaining layers. */
|
|
50
|
+
readonly failed?: {
|
|
51
|
+
readonly hook: string;
|
|
52
|
+
readonly reason: string;
|
|
53
|
+
};
|
|
54
|
+
}
|
|
27
55
|
/** Thrown when a restore hook fails or times out; the checkpoint and conversation are untouched. */
|
|
28
56
|
export declare class CheckpointRestoreError extends Error {
|
|
29
57
|
readonly code = "ERR_PRISM_CHECKPOINT_RESTORE";
|
|
30
|
-
/** Name of the failing hook (`
|
|
58
|
+
/** Name of the failing hook (`id`, `hook.name`, or `hook[i]`). */
|
|
31
59
|
readonly hook: string;
|
|
32
|
-
|
|
60
|
+
/** Present only when at least one `compensate` ran; omitted otherwise, as in plan 094. */
|
|
61
|
+
readonly compensation?: CheckpointRestoreCompensation;
|
|
62
|
+
constructor(hook: string, cause: unknown, compensation?: CheckpointRestoreCompensation);
|
|
33
63
|
}
|
|
34
64
|
export interface RunCheckpointRestoreHooksOptions {
|
|
35
65
|
/** Per-hook timeout; defaults to `DEFAULT_CHECKPOINT_RESTORE_TIMEOUT_MS`. */
|
|
36
66
|
readonly timeoutMs?: number;
|
|
37
67
|
/** Caller abort: checked between hooks and combined into each hook's signal. */
|
|
38
68
|
readonly signal?: AbortSignal;
|
|
69
|
+
/** Applied to the bounded compensation reason; absent = the message is reported as-is (still capped). */
|
|
70
|
+
readonly redactor?: SecretRedactor;
|
|
39
71
|
}
|
|
40
72
|
/**
|
|
41
|
-
* Run restore
|
|
42
|
-
* `CheckpointRestoreError` immediately (later
|
|
43
|
-
*
|
|
73
|
+
* Run restore handlers sequentially and report the audit. A handler failure throws
|
|
74
|
+
* `CheckpointRestoreError` immediately (later handlers do not run) after a best-effort reverse
|
|
75
|
+
* compensation pass over the applied layers; with no `compensate` declared anywhere, the failure
|
|
76
|
+
* shape is exactly plan 094's. An already-aborted caller signal throws its own abort reason so the
|
|
77
|
+
* resume reads as cancelled rather than as a restore failure, and a caller abort during the pass
|
|
78
|
+
* stops it before the next handler runs.
|
|
44
79
|
*/
|
|
45
|
-
export declare function runCheckpointRestoreHooks<Context>(hooks: readonly
|
|
80
|
+
export declare function runCheckpointRestoreHooks<Context>(hooks: readonly CheckpointRestoreHandler<Context>[], context: Context, options?: RunCheckpointRestoreHooksOptions): Promise<CheckpointRestoreAudit>;
|
|
81
|
+
export {};
|
|
@@ -1,53 +1,129 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Checkpoint restore hooks (plan 094 Task 3). A hook restores
|
|
3
|
-
* document version, workspace fingerprint) recorded in a
|
|
2
|
+
* Checkpoint restore hooks (plan 094 Task 3; reverse compensation plan 109 Task 2). A hook restores
|
|
3
|
+
* one external layer (git commit, document version, workspace fingerprint) recorded in a
|
|
4
|
+
* checkpoint's sidecar metadata.
|
|
4
5
|
*
|
|
5
|
-
* All-or-nothing: hooks run before the resume claims the checkpoint. The first hook that fails
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
6
|
+
* All-or-nothing: hooks run before the resume claims the checkpoint. The first hook that fails or
|
|
7
|
+
* times out aborts the resume with `CheckpointRestoreError` naming that hook, so the conversation
|
|
8
|
+
* restore never applies on top of a half-restored external world. A handler may also declare a
|
|
9
|
+
* `compensate` direction: on failure the applied layers — including the failing one, which may be
|
|
10
|
+
* half-applied — are undone in reverse order and the error carries a best-effort
|
|
11
|
+
* `CheckpointRestoreCompensation` report. Compensation never runs after the caller aborted, never
|
|
12
|
+
* masks the original failure, and the checkpoint stays resumable either way.
|
|
9
13
|
*/
|
|
10
14
|
/** Per-hook ceiling for a restore (plan 094 Task 3 default). */
|
|
11
15
|
export const DEFAULT_CHECKPOINT_RESTORE_TIMEOUT_MS = 10_000;
|
|
12
16
|
/** Thrown when a restore hook fails or times out; the checkpoint and conversation are untouched. */
|
|
13
17
|
export class CheckpointRestoreError extends Error {
|
|
14
18
|
code = "ERR_PRISM_CHECKPOINT_RESTORE";
|
|
15
|
-
/** Name of the failing hook (`
|
|
19
|
+
/** Name of the failing hook (`id`, `hook.name`, or `hook[i]`). */
|
|
16
20
|
hook;
|
|
17
|
-
constructor(hook, cause) {
|
|
21
|
+
constructor(hook, cause, compensation) {
|
|
18
22
|
super(`Checkpoint restore hook ${hook} failed: ${cause instanceof Error ? cause.message : String(cause)}`, { cause });
|
|
19
23
|
this.name = "CheckpointRestoreError";
|
|
20
24
|
this.hook = hook;
|
|
25
|
+
if (compensation)
|
|
26
|
+
this.compensation = compensation;
|
|
21
27
|
}
|
|
22
28
|
}
|
|
29
|
+
/** Compensation reasons are operator hints, not log lines: capped at this many UTF-8 bytes. */
|
|
30
|
+
const MAX_COMPENSATION_REASON_BYTES = 1_024;
|
|
31
|
+
/** One handler identity per restore, resolved once at the executor boundary (plan 109 Task 2). */
|
|
32
|
+
function normalizeRestoreHandler(handler, index) {
|
|
33
|
+
if (typeof handler === "function")
|
|
34
|
+
return { id: handler.name || `hook[${index}]`, restore: handler };
|
|
35
|
+
return {
|
|
36
|
+
id: handler.id ?? handler.restore.name ?? `hook[${index}]`,
|
|
37
|
+
restore: handler.restore,
|
|
38
|
+
...(handler.compensate ? { compensate: handler.compensate } : {}),
|
|
39
|
+
};
|
|
40
|
+
}
|
|
41
|
+
/** Run one direction of one handler under the per-hook timeout, reporting failure instead of throwing. */
|
|
42
|
+
async function invokeRestoreHandler(handler, context, timeoutMs, callerSignal) {
|
|
43
|
+
const controller = new AbortController();
|
|
44
|
+
const timer = setTimeout(() => controller.abort(new Error(`timed out after ${timeoutMs}ms`)), timeoutMs);
|
|
45
|
+
const signal = callerSignal ? AbortSignal.any([callerSignal, controller.signal]) : controller.signal;
|
|
46
|
+
const started = Date.now();
|
|
47
|
+
try {
|
|
48
|
+
await handler(context, signal);
|
|
49
|
+
return { ok: true, durationMs: Date.now() - started };
|
|
50
|
+
}
|
|
51
|
+
catch (error) {
|
|
52
|
+
// Our own abort means the hook either ignored the signal or lost the race; name the timeout.
|
|
53
|
+
const timedOut = controller.signal.aborted && !callerSignal?.aborted;
|
|
54
|
+
return { ok: false, cause: timedOut ? controller.signal.reason : error };
|
|
55
|
+
}
|
|
56
|
+
finally {
|
|
57
|
+
clearTimeout(timer);
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
/** UTF-8-safe truncation; no suffix, so the cap is exact. */
|
|
61
|
+
function truncateUtf8Bytes(text, maxBytes) {
|
|
62
|
+
if (Buffer.byteLength(text, "utf8") <= maxBytes)
|
|
63
|
+
return text;
|
|
64
|
+
let bytes = 0;
|
|
65
|
+
let end = 0;
|
|
66
|
+
for (const char of text) {
|
|
67
|
+
const size = Buffer.byteLength(char, "utf8");
|
|
68
|
+
if (bytes + size > maxBytes)
|
|
69
|
+
break;
|
|
70
|
+
bytes += size;
|
|
71
|
+
end += char.length;
|
|
72
|
+
}
|
|
73
|
+
return text.slice(0, end);
|
|
74
|
+
}
|
|
75
|
+
/** Redacted, byte-bounded operator hint; hook arguments are never copied into it. */
|
|
76
|
+
function compensationReason(error, redactor) {
|
|
77
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
78
|
+
return truncateUtf8Bytes(redactor ? redactor.redact(message) : message, MAX_COMPENSATION_REASON_BYTES);
|
|
79
|
+
}
|
|
23
80
|
/**
|
|
24
|
-
*
|
|
25
|
-
*
|
|
26
|
-
*
|
|
81
|
+
* Reverse pass over the applied layers (`failedIndex`…0, failing hook first). Best-effort: a
|
|
82
|
+
* failing compensation is recorded once and the pass continues; a caller abort stops the pass and
|
|
83
|
+
* rethrows the abort reason unchanged, so a cancel still reads as cancelled.
|
|
84
|
+
*/
|
|
85
|
+
async function compensateRestoreHandlers(handlers, failedIndex, context, timeoutMs, options) {
|
|
86
|
+
const ran = [];
|
|
87
|
+
let failed;
|
|
88
|
+
for (let index = failedIndex; index >= 0; index -= 1) {
|
|
89
|
+
const handler = handlers[index];
|
|
90
|
+
if (!handler?.compensate)
|
|
91
|
+
continue;
|
|
92
|
+
options.signal?.throwIfAborted();
|
|
93
|
+
const outcome = await invokeRestoreHandler(handler.compensate, context, timeoutMs, options.signal);
|
|
94
|
+
options.signal?.throwIfAborted();
|
|
95
|
+
ran.push(handler.id);
|
|
96
|
+
if (!outcome.ok && failed === undefined)
|
|
97
|
+
failed = { hook: handler.id, reason: compensationReason(outcome.cause, options.redactor) };
|
|
98
|
+
}
|
|
99
|
+
if (ran.length === 0)
|
|
100
|
+
return undefined;
|
|
101
|
+
return { ran, ...(failed ? { failed } : {}) };
|
|
102
|
+
}
|
|
103
|
+
/**
|
|
104
|
+
* Run restore handlers sequentially and report the audit. A handler failure throws
|
|
105
|
+
* `CheckpointRestoreError` immediately (later handlers do not run) after a best-effort reverse
|
|
106
|
+
* compensation pass over the applied layers; with no `compensate` declared anywhere, the failure
|
|
107
|
+
* shape is exactly plan 094's. An already-aborted caller signal throws its own abort reason so the
|
|
108
|
+
* resume reads as cancelled rather than as a restore failure, and a caller abort during the pass
|
|
109
|
+
* stops it before the next handler runs.
|
|
27
110
|
*/
|
|
28
111
|
export async function runCheckpointRestoreHooks(hooks, context, options = {}) {
|
|
29
112
|
const timeoutMs = options.timeoutMs ?? DEFAULT_CHECKPOINT_RESTORE_TIMEOUT_MS;
|
|
30
113
|
const started = Date.now();
|
|
31
114
|
const entries = [];
|
|
32
|
-
|
|
115
|
+
const handlers = hooks.map((handler, index) => normalizeRestoreHandler(handler, index));
|
|
116
|
+
for (const [index, handler] of handlers.entries()) {
|
|
33
117
|
options.signal?.throwIfAborted();
|
|
34
|
-
const
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
}
|
|
42
|
-
catch (error) {
|
|
43
|
-
// Our own abort means the hook either ignored the signal or lost the race; name the timeout.
|
|
44
|
-
const timedOut = controller.signal.aborted && !options.signal?.aborted;
|
|
45
|
-
throw new CheckpointRestoreError(name, timedOut ? controller.signal.reason : error);
|
|
46
|
-
}
|
|
47
|
-
finally {
|
|
48
|
-
clearTimeout(timer);
|
|
118
|
+
const outcome = await invokeRestoreHandler(handler.restore, context, timeoutMs, options.signal);
|
|
119
|
+
if (!outcome.ok) {
|
|
120
|
+
// Security: never run host compensation code once the caller has aborted.
|
|
121
|
+
const compensation = options.signal?.aborted
|
|
122
|
+
? undefined
|
|
123
|
+
: await compensateRestoreHandlers(handlers, index, context, timeoutMs, options);
|
|
124
|
+
throw new CheckpointRestoreError(handler.id, outcome.cause, compensation);
|
|
49
125
|
}
|
|
50
|
-
entries.push({ hook:
|
|
126
|
+
entries.push({ hook: handler.id, durationMs: outcome.durationMs });
|
|
51
127
|
}
|
|
52
128
|
return { hooks: entries, durationMs: Date.now() - started };
|
|
53
129
|
}
|
|
@@ -30,7 +30,8 @@ export interface SessionStore {
|
|
|
30
30
|
get?(id: string): Promise<SessionEntry | undefined>;
|
|
31
31
|
/** DB-friendly branch read: return one branch's ancestor chain as a page so adapters
|
|
32
32
|
* avoid `list(sessionId)` (full-session scan) + in-memory rebuild. Optional — the
|
|
33
|
-
* built-in
|
|
33
|
+
* built-in JSONL store omits it and the runtime falls back to `list()`. The memory
|
|
34
|
+
* store implements it. */
|
|
34
35
|
readBranchPath?(query: SessionBranchRead): Promise<PersistencePage<SessionEntry>>;
|
|
35
36
|
/**
|
|
36
37
|
* Optional bounded session search. Prefer implementing this **or** returning a companion
|
|
@@ -1,6 +1,7 @@
|
|
|
1
|
-
import type {
|
|
1
|
+
import type { CheckpointRestoreHandler } from "./checkpoint-restore.js";
|
|
2
2
|
import type { AgentSessionCloneOptions, AgentSessionForkOptions, CheckpointRecord, CheckpointStore, CompactionOptions, CompactionResult, ContentBlock, ContextMeter, ErrorInfo, GuardrailPackRef, JsonObject, JsonValue, Message, ModelConfig, OwnershipScope, RunLimitBreach, SessionEntry, SubscribeOptions, ToolCallContent, Usage } from "./contracts-core.js";
|
|
3
3
|
import type { AgentEvent, AgentFinishReason, RunOptions, ToolEffectKind } from "./contracts-protocol.js";
|
|
4
|
+
import type { BudgetExhaustionAttribution } from "./run-limits.js";
|
|
4
5
|
export type AgentRunStatus = "succeeded" | "failed" | "aborted" | "suspended" | "denied";
|
|
5
6
|
export type AgentRunInterruptionKind = "input_guardrail" | "tool_approval" | "elicitation";
|
|
6
7
|
export type ApprovalOutcome = "allow_once" | "allow_for_run" | "reject_once" | "reject_for_run";
|
|
@@ -247,8 +248,8 @@ export interface AgentCheckpointRestoreContext {
|
|
|
247
248
|
readonly metadata?: AgentRunCheckpointMetadata;
|
|
248
249
|
readonly checkpoint: CheckpointRecord;
|
|
249
250
|
}
|
|
250
|
-
/** Host code restoring one external layer before a durable resume applies. */
|
|
251
|
-
export type AgentCheckpointRestoreHook =
|
|
251
|
+
/** Host code restoring or compensating one external layer before a durable resume applies. */
|
|
252
|
+
export type AgentCheckpointRestoreHook = CheckpointRestoreHandler<AgentCheckpointRestoreContext>;
|
|
252
253
|
export interface AgentRunResumeOptions {
|
|
253
254
|
readonly checkpoints: CheckpointStore;
|
|
254
255
|
/** Current host-authored revision; must exactly match the checkpoint. */
|
|
@@ -278,7 +279,9 @@ export interface AgentRunResumeOptions {
|
|
|
278
279
|
* Plan 094 Task 3: external-state restore hooks. Every hook must succeed (sequentially, each
|
|
279
280
|
* within `restoreHookTimeoutMs`) before the claim write and the conversation restore apply;
|
|
280
281
|
* the first failure throws `CheckpointRestoreError` naming the hook and leaves the checkpoint
|
|
281
|
-
* suspended.
|
|
282
|
+
* suspended. Plan 109 Task 2: an entry may be `{ id?, restore, compensate? }`, and on failure the
|
|
283
|
+
* applied layers are compensated in reverse order (best-effort) before the throw. Hosts that
|
|
284
|
+
* register hooks on the lifecycle instead pass them once there.
|
|
282
285
|
*/
|
|
283
286
|
readonly restoreHooks?: readonly AgentCheckpointRestoreHook[];
|
|
284
287
|
/** Per-hook restore ceiling in ms; defaults to `DEFAULT_CHECKPOINT_RESTORE_TIMEOUT_MS`. */
|
|
@@ -325,6 +328,11 @@ export interface AgentRunResult {
|
|
|
325
328
|
readonly usage?: Usage;
|
|
326
329
|
/** Present when the run hit a configured resource ceiling. */
|
|
327
330
|
readonly limit?: RunLimitBreach;
|
|
331
|
+
/**
|
|
332
|
+
* Present with `limit` when the run died on a ceiling: the same plan-087 attribution payload the
|
|
333
|
+
* `budget_exhausted` event carries (`limit` stays on {@link AgentRunResult.limit}).
|
|
334
|
+
*/
|
|
335
|
+
readonly attribution?: BudgetExhaustionAttribution;
|
|
328
336
|
/** Present when `status` is `"failed"` or when a failed attempt still produced partial output. */
|
|
329
337
|
readonly error?: ErrorInfo;
|
|
330
338
|
/** String form of the abort reason when `status` is `"aborted"`. */
|