@arnilo/prism 0.1.2 → 0.1.4

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.
Files changed (42) hide show
  1. package/CHANGELOG.md +10 -0
  2. package/dist/agent-approval.d.ts +49 -0
  3. package/dist/agent-approval.js +178 -0
  4. package/dist/agent-run-lifecycle.d.ts +7 -1
  5. package/dist/agent-run-lifecycle.js +213 -3
  6. package/dist/agent-run-state.d.ts +11 -0
  7. package/dist/agent-run-state.js +23 -0
  8. package/dist/agent-session.d.ts +98 -0
  9. package/dist/agent-session.js +1806 -0
  10. package/dist/agent-tool-dispatch.d.ts +13 -0
  11. package/dist/agent-tool-dispatch.js +92 -0
  12. package/dist/agents.d.ts +16 -9
  13. package/dist/agents.js +14 -2254
  14. package/dist/contracts-core.d.ts +1402 -0
  15. package/dist/contracts-core.js +119 -0
  16. package/dist/contracts-protocol.d.ts +596 -0
  17. package/dist/contracts-protocol.js +2 -0
  18. package/dist/contracts-run-state.d.ts +285 -0
  19. package/dist/contracts-run-state.js +77 -0
  20. package/dist/contracts.d.ts +7 -2261
  21. package/dist/contracts.js +7 -192
  22. package/dist/index.d.ts +1 -1
  23. package/dist/index.js +1 -1
  24. package/docs/0.1.0-readiness.md +3 -1
  25. package/docs/agent-identity.md +1 -1
  26. package/docs/agent-session-runtime.md +3 -1
  27. package/docs/browser-automation.md +13 -9
  28. package/docs/coding-agent-tools.md +13 -0
  29. package/docs/context-and-skills.md +2 -2
  30. package/docs/index.md +3 -14
  31. package/docs/migration.md +11 -5
  32. package/docs/performance.md +14 -4
  33. package/docs/persistence-credentials-multimodality-primitives.md +1 -1
  34. package/docs/provider-primitives.md +1 -1
  35. package/docs/public-contracts.md +2 -0
  36. package/docs/release-and-install.md +65 -0
  37. package/docs/session-stores.md +2 -2
  38. package/docs/thinking-and-reasoning.md +1 -1
  39. package/docs/tool-execution-primitives.md +2 -2
  40. package/docs/use-case-model-selection.md +1 -1
  41. package/docs/workflow-orchestration-primitives.md +1 -1
  42. package/package.json +4 -3
package/CHANGELOG.md CHANGED
@@ -1,5 +1,15 @@
1
1
  # Changelog
2
2
 
3
+ ## [0.1.4] - 2026-08-10
4
+
5
+ ### Changed
6
+ - **Release 0.1.4 (plan 016)** is the internal god-module split, compat-preserving on the frozen 0.1.x line. (1) **Contracts split** (Task 1): `src/contracts.ts` (2,549 lines) split by concern into `src/contracts-core.ts`, `src/contracts-run-state.ts`, and `src/contracts-protocol.ts` behind a pure `export *` barrel — the 295-name public surface is unchanged (0 added/0 removed/0 changed vs the 0.1.3 baseline, 702 = 702 at the entry). (2) **Agents split** (Task 2): `src/agents.ts` (2,576 lines) split into `src/agent-session.ts` (`RuntimeAgentSession` + factories + shared helpers), `src/agent-run-lifecycle.ts` (resume lifecycle), `src/agent-approval.ts`, `src/agent-tool-dispatch.ts`, with `agent-run-state.ts`/`agent-loops.ts`/`compaction.ts` reused — `agents.ts` is now a barrel of the four public functions; 14 internal cross-module helper exports joined the union `.d.ts` surface but are not consumer-importable (documented deviation #1, `scripts/phase16-freeze-manifest.json`). (3) **Tree-shake verification** (Task 3): measured in `scripts/phase16-baseline.json` — `dist/agents.js` 111,049 → 982 B, `dist/contracts.js` 9,420 → 418 B, `dist/contracts.d.ts` 98,825 → 1,029 B, dist module count 64 → 70 (static-import reachability reported, not gated). (4) **`@arnilo/prism-browser` CDP capabilities** (Tasks 4-5): additive Chrome DevTools Protocol surface riding playwright-core's existing transport — `browser_evaluate` (bounded `Runtime.evaluate`, policy-gated), `browser_observe` (console/network rings, bodies never captured), `block_urls`/`unblock_urls`/`throttle`/`emulate` act actions, and raw `{ css }`/`{ xpath }` targets; capability-gated via `BrowserCdpOptions.mode` with `ERR_PRISM_BROWSER_CDP_UNAVAILABLE`; documented additive deltas vs the 0.1.3 prism-browser baseline (41 added / 0 removed / 18 changed — 15 statement-text artifacts + 3 optional-member/signature-widening, deviation #2), root `@arnilo/prism` zero deltas. Zero new dependencies across the milestone; exit gate green (npm test core 1,425/1,425 + 151 script gates, `sdk:ready`, audit 0, pack dry-run 49/49 twice byte-identical, tree-shake + benchmark evidence in `scripts/phase16-baseline.json`). Store compatibility with 0.1.3: **compatible, no migration** (rollback = restore the 0.1.3 manifests/tag). **Publication remains the operator handoff** (`docs/release-and-install.md` `0.1.4 publish handoff` — signed `v0.1.4` tag + npm OIDC).
7
+
8
+ ## [0.1.3] - 2026-08-10
9
+
10
+ ### Changed
11
+ - **Release 0.1.3 (plan 015)** is the dead-code and deprecation hygiene patch on the frozen 0.1.x line, additive-only vs 0.1.2 (freeze manifest `scripts/phase15-freeze-manifest.json`). (1) **Benchmark-runner consolidation** (Task 1): one parameterized runner `scripts/benchmark.mjs --scenario <name>` replaces the per-version runners; the six live legs moved to `scripts/benchmark-scenarios/` as named scenarios (`phase6-postgres`, `phase7-postgres`, `phase8-loops-hitl`, `phase9-coding`, `phase10-acp`, `phase11-auth`) and the 0.1.0 envelope orchestrator composes them through the runner; **removed files**: `scripts/benchmark-0.0.{8,9,10,11,12,13,14,15,16}.mjs` and `scripts/benchmark-0.0.{9,10,11,12,13,14,15}.test.mjs` (orphaned, unreferenced by `npm test`); all `benchmark-*.json` evidence files kept byte-identical; the CI benchmark-schema leg now runs `scripts/benchmark.test.mjs`. (2) **Review-coverage archive** (Task 2): the 12 `docs/review-coverage-2026-07-*.md` per-phase evidence files moved to `docs/_evidence/` (tarball-excluded via the `files` field; index/migration/performance links updated; archived evidence is not part of the shipped docs surface). (3) **Non-blocking unused-code sweep** (Task 3): `npm run sweep:unused` runs tsc `--noUnusedLocals`/`--noUnusedParameters` over core + every workspace tsconfig plus a zero-dep dead-export scan (`scripts/dead-exports.mjs`), writes the combined report to `scripts/unused-sweep-report.txt`, and always exits 0; CI runs it as a `continue-on-error` step with a retained artifact; 43 internal unused diagnostics (22 test files + 13 source files) removed in-tree, public-but-unused exports are report-only (removal is the 0.1.5 breaking cut). (4) **Opt-in checkpoint persistence** (Task 4): durable runs may set `persistSessionState: true` on the run and resume options — the loaded-skill **name catalog** (≤64 names, ≤256 chars each, validated fail-closed on every save and load) rides the run-state checkpoint and is restored into the resumed session's `LoadedSkillSet`; skill **bodies are never persisted** and re-resolve from the live registry; flag off keeps the checkpoint shape byte-identical to 0.1.2. `@arnilo/prism-coding-agent` adds `createReadPathSetPersistence({ checkpoints, key, ownership })` for the read-before-write path set (≤1024 paths / ≤1024 chars each, CAS read-modify-write, cross-ownership restore fails closed). Store compatibility with 0.1.2: **compatible, no migration**; declaration surface additive-only vs the frozen 0.1.x contract.
12
+
3
13
  ## [0.1.2] - 2026-08-10
4
14
 
5
15
  ### Changed
@@ -0,0 +1,49 @@
1
+ import type { StoredAgentRunState } from "./agent-run-state.js";
2
+ import { Agent, AgentRunState, AgentRunStateOptions, DecisionScope, NestedRunOutcome, PendingDecision, RunDecision, StickyDecision, ToolResult } from "./contracts.js";
3
+ import type { AgentIdentity } from "./identity.js";
4
+ /** Pending decisions of a suspended state, synthesizing the legacy single-approval shape. */
5
+ export declare function pendingDecisionsOf(state: StoredAgentRunState): readonly PendingDecision[] | undefined;
6
+ interface ResolvedRunDecisions {
7
+ readonly decisionsById: ReadonlyMap<string, RunDecision>;
8
+ readonly stickyDecisions: readonly StickyDecision[];
9
+ readonly remaining: readonly PendingDecision[];
10
+ }
11
+ /**
12
+ * Validate one decision batch against the suspended state. Fail-closed and atomic: any
13
+ * invalid entry rejects the whole batch before any CAS, leaving state and version untouched.
14
+ * Unknown and foreign approval ids share one non-enumerating error.
15
+ */
16
+ export declare function resolveRunDecisions(input: {
17
+ readonly agent: Agent;
18
+ readonly state: StoredAgentRunState;
19
+ readonly decisions: readonly RunDecision[];
20
+ readonly signal?: AbortSignal;
21
+ }): Promise<ResolvedRunDecisions>;
22
+ /**
23
+ * Resolve a tool's declared elicitation contract for a gated call. A throwing hook falls back to
24
+ * plain tool approval: malformed model args then surface as a tool error after approval, never
25
+ * as a run failure at the gate. Output is bounded before it enters the pending-decision record.
26
+ */
27
+ export declare class AgentRunSuspended extends Error {
28
+ readonly state: AgentRunState;
29
+ readonly interruption: import("./contracts.js").AgentRunInterruption;
30
+ readonly code = "ERR_PRISM_AGENT_RUN_SUSPENDED";
31
+ constructor(state: AgentRunState, interruption: import("./contracts.js").AgentRunInterruption);
32
+ }
33
+ /** Root-visible nested approval id: hashed so it stays bounded and non-enumerating at any depth. */
34
+ export declare function nestedApprovalId(runId: string, childApprovalId: string): string;
35
+ export declare function pathsEqual(a: readonly string[] | undefined, b: readonly string[] | undefined): boolean;
36
+ export declare function decisionScopesEqual(a: DecisionScope, b: DecisionScope): boolean;
37
+ export declare function nestedOutcomeToolResult(outcome: Exclude<NestedRunOutcome, {
38
+ status: "suspended";
39
+ }>, toolCallId: string, name: string): ToolResult;
40
+ export interface ActiveDurableRun {
41
+ readonly options: AgentRunStateOptions;
42
+ state?: StoredAgentRunState;
43
+ version: number;
44
+ /** Validated decisions driving the pending-call replay on a resumed run. */
45
+ readonly decisions?: ReadonlyMap<string, RunDecision>;
46
+ }
47
+ /** Compact redacted principal reference used in decision scopes; never a credential. */
48
+ export declare function decisionIdentityRef(identity: AgentIdentity | undefined): string | undefined;
49
+ export {};
@@ -0,0 +1,178 @@
1
+ // Approval / pending-decision helpers split from agents.ts at 0.1.4 (verbatim move; internal, not public API).
2
+ import { createHash } from "node:crypto";
3
+ import { activeTools, validateElicitationPayload } from "./agent-tool-dispatch.js";
4
+ import { AgentDecisionError, DEFAULT_MAX_STICKY_DECISIONS, HARD_MAX_PENDING_DECISIONS, MAX_DECISION_REASON_BYTES, MAX_ELICITATION_BYTES, } from "./contracts.js";
5
+ import { runGuardrails } from "./guardrails.js";
6
+ import { canonicalToolEffectJson } from "./tool-effects.js";
7
+ /** Pending decisions of a suspended state, synthesizing the legacy single-approval shape. */
8
+ export function pendingDecisionsOf(state) {
9
+ if (state.interruption?.pendingDecisions)
10
+ return state.interruption.pendingDecisions;
11
+ if (state.pending) {
12
+ return [
13
+ {
14
+ approvalId: state.pending.call.id,
15
+ kind: "tool_approval",
16
+ toolCallId: state.pending.call.id,
17
+ scope: { toolName: state.pending.call.name },
18
+ reason: state.interruption?.reason ?? "Tool side effect requires approval",
19
+ },
20
+ ];
21
+ }
22
+ return undefined;
23
+ }
24
+ /**
25
+ * Validate one decision batch against the suspended state. Fail-closed and atomic: any
26
+ * invalid entry rejects the whole batch before any CAS, leaving state and version untouched.
27
+ * Unknown and foreign approval ids share one non-enumerating error.
28
+ */
29
+ export async function resolveRunDecisions(input) {
30
+ const { agent, state, decisions } = input;
31
+ if (decisions.length === 0)
32
+ throw new AgentDecisionError("ERR_PRISM_DECISION_INVALID", "Decision batch must not be empty");
33
+ if (decisions.length > HARD_MAX_PENDING_DECISIONS) {
34
+ throw new AgentDecisionError("ERR_PRISM_DECISION_LIMIT", `Decision batch exceeds ${HARD_MAX_PENDING_DECISIONS} entries`);
35
+ }
36
+ const pending = pendingDecisionsOf(state);
37
+ if (!pending?.length)
38
+ throw new AgentDecisionError("ERR_PRISM_DECISION_UNKNOWN", "No pending approval decisions for this run");
39
+ const byId = new Map(pending.map((entry) => [entry.approvalId, entry]));
40
+ const seen = new Set();
41
+ const decisionsById = new Map();
42
+ const stickies = [];
43
+ const decidedAt = new Date().toISOString();
44
+ const { registry } = activeTools(agent.config.tools);
45
+ for (const decision of decisions) {
46
+ if (seen.has(decision.approvalId)) {
47
+ throw new AgentDecisionError("ERR_PRISM_DECISION_DUPLICATE", "Duplicate approval decision in batch");
48
+ }
49
+ seen.add(decision.approvalId);
50
+ const target = byId.get(decision.approvalId);
51
+ if (!target)
52
+ throw new AgentDecisionError("ERR_PRISM_DECISION_UNKNOWN", "Unknown approval decision");
53
+ if (decision.reason !== undefined && Buffer.byteLength(decision.reason, "utf8") > MAX_DECISION_REASON_BYTES) {
54
+ throw new AgentDecisionError("ERR_PRISM_DECISION_LIMIT", `Decision reason exceeds ${MAX_DECISION_REASON_BYTES} bytes`);
55
+ }
56
+ if (decision.outcome !== "allow_once" &&
57
+ decision.outcome !== "allow_for_run" &&
58
+ decision.outcome !== "reject_once" &&
59
+ decision.outcome !== "reject_for_run") {
60
+ throw new AgentDecisionError("ERR_PRISM_DECISION_INVALID", "Unknown approval outcome");
61
+ }
62
+ if (decision.modifiedArguments !== undefined) {
63
+ if (target.kind !== "tool_approval" || !target.toolCallId) {
64
+ throw new AgentDecisionError("ERR_PRISM_DECISION_SCOPE", "Modified arguments apply only to tool approvals");
65
+ }
66
+ await validateModifiedArguments(agent, registry, state, target, decision.modifiedArguments, input.signal);
67
+ }
68
+ if (decision.elicitation !== undefined) {
69
+ if (target.kind !== "elicitation") {
70
+ throw new AgentDecisionError("ERR_PRISM_DECISION_SCOPE", "Elicitation payload applies only to elicitation decisions");
71
+ }
72
+ await validateElicitationPayload(agent, state, target, decision.elicitation, input.signal);
73
+ }
74
+ decisionsById.set(decision.approvalId, decision);
75
+ if (decision.outcome === "allow_for_run" || decision.outcome === "reject_for_run") {
76
+ stickies.push({
77
+ // A decision with modified arguments must not stick to the original arguments hash:
78
+ // the modification is one-off, so the sticky scope matches by name/effect/identity only.
79
+ scope: decision.modifiedArguments !== undefined ? { ...target.scope, argumentsHash: undefined } : target.scope,
80
+ outcome: decision.outcome,
81
+ ...(decision.reason !== undefined ? { reason: decision.reason } : {}),
82
+ // Root-owned sticky scope includes the delegation path for nested decisions.
83
+ ...(target.attribution ? { attribution: target.attribution } : {}),
84
+ decidedAt,
85
+ });
86
+ }
87
+ }
88
+ const stickyDecisions = [...(state.stickyDecisions ?? []), ...stickies];
89
+ if (stickyDecisions.length > DEFAULT_MAX_STICKY_DECISIONS) {
90
+ throw new AgentDecisionError("ERR_PRISM_DECISION_LIMIT", `Sticky decisions exceed ${DEFAULT_MAX_STICKY_DECISIONS} per run`);
91
+ }
92
+ return {
93
+ decisionsById,
94
+ stickyDecisions,
95
+ remaining: pending.filter((entry) => !seen.has(entry.approvalId)),
96
+ };
97
+ }
98
+ /** Decision-time revalidation of modified arguments: schema, then input guardrails. Policy and trust re-run at dispatch. */
99
+ async function validateModifiedArguments(agent, registry, state, target, modified, signal) {
100
+ const invalid = (message, cause) => new AgentDecisionError("ERR_PRISM_DECISION_INVALID", message, { cause });
101
+ if (JSON.stringify(modified) === undefined || Buffer.byteLength(JSON.stringify(modified), "utf8") > MAX_ELICITATION_BYTES) {
102
+ throw invalid("Modified arguments must be a bounded JSON object");
103
+ }
104
+ const call = state.pendingCalls?.find((entry) => entry.approvalId === target.approvalId)?.call ??
105
+ (state.pending && state.pending.call.id === target.toolCallId ? state.pending.call : undefined);
106
+ const toolName = target.scope.toolName ?? call?.name ?? "";
107
+ const tool = registry.get(toolName);
108
+ const context = { sessionId: state.sessionId, runId: state.runId, toolCallId: target.toolCallId ?? "", signal };
109
+ if (agent.config.validator && tool) {
110
+ const validation = await agent.config.validator(tool, modified, context);
111
+ if (validation)
112
+ throw invalid("Modified arguments failed schema validation");
113
+ }
114
+ const value = call
115
+ ? { ...call, arguments: modified }
116
+ : { type: "tool_call", id: target.toolCallId ?? "", name: toolName, arguments: modified };
117
+ const guarded = await runGuardrails({
118
+ stage: "tool_input",
119
+ guardrails: agent.config.guardrails,
120
+ value,
121
+ context: {
122
+ sessionId: state.sessionId,
123
+ runId: state.runId,
124
+ toolCallId: target.toolCallId,
125
+ toolName,
126
+ metadata: {},
127
+ signal,
128
+ },
129
+ redactor: agent.config.redactor,
130
+ });
131
+ if (guarded.terminal)
132
+ throw invalid("Modified arguments blocked by guardrail");
133
+ }
134
+ /**
135
+ * Resolve a tool's declared elicitation contract for a gated call. A throwing hook falls back to
136
+ * plain tool approval: malformed model args then surface as a tool error after approval, never
137
+ * as a run failure at the gate. Output is bounded before it enters the pending-decision record.
138
+ */
139
+ export class AgentRunSuspended extends Error {
140
+ state;
141
+ interruption;
142
+ code = "ERR_PRISM_AGENT_RUN_SUSPENDED";
143
+ constructor(state, interruption) {
144
+ super("Agent run suspended");
145
+ this.state = state;
146
+ this.interruption = interruption;
147
+ this.name = "AgentRunSuspended";
148
+ }
149
+ }
150
+ /** Root-visible nested approval id: hashed so it stays bounded and non-enumerating at any depth. */
151
+ export function nestedApprovalId(runId, childApprovalId) {
152
+ return `sub_${createHash("sha256").update(`${runId}:${childApprovalId}`).digest("hex")}`;
153
+ }
154
+ export function pathsEqual(a, b) {
155
+ if (a === undefined || b === undefined)
156
+ return a === b;
157
+ return a.length === b.length && a.every((value, index) => value === b[index]);
158
+ }
159
+ export function decisionScopesEqual(a, b) {
160
+ if (a.toolName !== b.toolName || a.argumentsHash !== b.argumentsHash || a.effectKind !== b.effectKind || a.identity !== b.identity)
161
+ return false;
162
+ if (a.actionConstraints === undefined || b.actionConstraints === undefined)
163
+ return a.actionConstraints === b.actionConstraints;
164
+ const keys = Object.keys(a.actionConstraints);
165
+ return (keys.length === Object.keys(b.actionConstraints).length &&
166
+ keys.every((key) => key in b.actionConstraints &&
167
+ canonicalToolEffectJson(a.actionConstraints[key]) === canonicalToolEffectJson(b.actionConstraints[key])));
168
+ }
169
+ export function nestedOutcomeToolResult(outcome, toolCallId, name) {
170
+ return outcome.status === "completed"
171
+ ? { toolCallId, name, ...(outcome.value !== undefined ? { value: outcome.value } : {}) }
172
+ : { toolCallId, name, error: { code: outcome.code, message: outcome.message } };
173
+ }
174
+ /** Compact redacted principal reference used in decision scopes; never a credential. */
175
+ export function decisionIdentityRef(identity) {
176
+ return identity ? `${identity.tenantId}:${identity.principal.kind}:${identity.principal.id}` : undefined;
177
+ }
178
+ //# sourceMappingURL=agent-approval.js.map
@@ -1,4 +1,4 @@
1
- import type { Agent, AgentEvent, AgentRunRef, AgentRunResult, AgentRunResume, AgentRunStatusResult, CheckpointStore, OwnershipScope, SubscribeOptions } from "./contracts.js";
1
+ import type { Agent, AgentEvent, AgentRunRef, AgentRunResult, AgentRunResume, AgentRunResumeOptions, AgentRunResumeStreamOptions, AgentRunStatusResult, CheckpointStore, OwnershipScope, SubscribeOptions } from "./contracts.js";
2
2
  export interface AgentRunLifecycleAgent {
3
3
  readonly agent: Agent;
4
4
  /** Current host-authored revision; it must match the stored revision. */
@@ -18,6 +18,8 @@ export interface AgentRunLifecycleRequest {
18
18
  readonly signal?: AbortSignal;
19
19
  /** Adapter-selected capability; stored runs for another agent are non-enumerable. */
20
20
  readonly agentId?: string;
21
+ /** Opt-in (plan 015 Task 4): restore persisted loaded-skill names on resume. */
22
+ readonly persistSessionState?: boolean;
21
23
  }
22
24
  /** Bounded live-event options for a durable lifecycle resume. */
23
25
  export interface AgentRunLifecycleStreamRequest extends AgentRunLifecycleRequest, SubscribeOptions {
@@ -29,3 +31,7 @@ export interface AgentRunLifecycle {
29
31
  }
30
32
  /** Host capability for durable agent status/resume. Adapters supply authorized ownership only. */
31
33
  export declare function createAgentRunLifecycle(options: AgentRunLifecycleOptions): AgentRunLifecycle;
34
+ /** Resume a persisted built-in run. A claimed/dispatched tool is never replayed automatically. */
35
+ export declare function resumeAgentRun(agent: Agent, ref: AgentRunRef, resume: AgentRunResume, options: AgentRunResumeOptions): Promise<AgentRunResult>;
36
+ /** Subscribe before resuming one durable run. Early consumer return aborts that resumed execution. */
37
+ export declare function resumeAgentRunStream(agent: Agent, ref: AgentRunRef, resume: AgentRunResume, options: AgentRunResumeStreamOptions): AsyncGenerator<AgentEvent>;
@@ -1,6 +1,7 @@
1
- import { loadAgentRunState, publicState } from "./agent-run-state.js";
2
- import { resumeAgentRun, resumeAgentRunStream } from "./agents.js";
3
- import { AgentRunStateError } from "./contracts.js";
1
+ import { agentFingerprint, loadAgentRunState, publicState, saveAgentRunState } from "./agent-run-state.js";
2
+ import { AgentDecisionError, AgentRunStateError } from "./contracts.js";
3
+ import { pendingDecisionsOf, resolveRunDecisions } from "./agent-approval.js";
4
+ import { RuntimeAgentSession, throwIfAbortedSignal } from "./agent-session.js";
4
5
  function assertAgentId(actual, expected) {
5
6
  if (expected !== undefined && actual !== expected)
6
7
  throw new AgentRunStateError("Agent run capability mismatch");
@@ -26,6 +27,7 @@ export function createAgentRunLifecycle(options) {
26
27
  ownership: request.ownership,
27
28
  fencingToken: options.fencingToken,
28
29
  definitionRevision: resolved.definitionRevision,
30
+ persistSessionState: request.persistSessionState,
29
31
  });
30
32
  },
31
33
  async *resumeStream(ref, resume, request = {}) {
@@ -42,8 +44,216 @@ export function createAgentRunLifecycle(options) {
42
44
  signal: request.signal,
43
45
  maxQueuedEvents: request.maxQueuedEvents,
44
46
  overflow: request.overflow,
47
+ persistSessionState: request.persistSessionState,
45
48
  });
46
49
  },
47
50
  };
48
51
  }
52
+ // Resume free functions moved from agents.ts at 0.1.4 (verbatim; the barrel re-exports the two public ones).
53
+ /** Resume a persisted built-in run. A claimed/dispatched tool is never replayed automatically. */
54
+ export async function resumeAgentRun(agent, ref, resume, options) {
55
+ return executePreparedAgentRunResume(await prepareAgentRunResume(agent, ref, resume, options));
56
+ }
57
+ /** Subscribe before resuming one durable run. Early consumer return aborts that resumed execution. */
58
+ export async function* resumeAgentRunStream(agent, ref, resume, options) {
59
+ throwIfAbortedSignal(options.signal);
60
+ const prepared = await prepareAgentRunResume(agent, ref, resume, options, options.signal);
61
+ const subscription = prepared.session.subscribe(options);
62
+ let settled = false;
63
+ const runPromise = executePreparedAgentRunResume(prepared, options.signal).finally(() => {
64
+ settled = true;
65
+ });
66
+ try {
67
+ for await (const event of subscription) {
68
+ if ("runId" in event && event.runId !== ref.runId)
69
+ continue;
70
+ yield event;
71
+ }
72
+ await runPromise;
73
+ }
74
+ finally {
75
+ if (!settled) {
76
+ prepared.session.abort(new Error("resume stream consumer closed"));
77
+ await runPromise.catch(() => undefined);
78
+ }
79
+ }
80
+ }
81
+ async function prepareAgentRunResume(agent, ref, resume, options, signal) {
82
+ throwIfAbortedSignal(signal);
83
+ const { record, state } = await loadAgentRunState(options.checkpoints, ref, options.ownership);
84
+ if (state.definitionRevision !== options.definitionRevision ||
85
+ state.agentId !== (agent.config.id ?? agent.config.name) ||
86
+ state.fingerprint !== agentFingerprint(agent, options.definitionRevision)) {
87
+ throw new AgentRunStateError("Agent definition revision or fingerprint mismatch on resume");
88
+ }
89
+ if (record.version !== resume.expectedVersion || state.status !== "suspended") {
90
+ throw new AgentRunStateError("Stale or non-suspended agent run resume");
91
+ }
92
+ const session = new RuntimeAgentSession({ agent, id: state.sessionId, leafId: state.leafId });
93
+ // Opt-in session-state restore (plan 015 Task 4): names only; bodies re-resolve from
94
+ // the live registry the next time the model (re)loads them via load_skill.
95
+ if (options.persistSessionState && state.sessionState?.loadedSkillNames) {
96
+ session.restoreLoadedSkills(state.sessionState.loadedSkillNames);
97
+ }
98
+ if (resume.decision !== undefined && resume.decisions !== undefined) {
99
+ throw new AgentDecisionError("ERR_PRISM_DECISION_INVALID", "Resume accepts exactly one of decision or decisions");
100
+ }
101
+ const pendingDecisions = pendingDecisionsOf(state);
102
+ // Legacy approve maps to allow-once on every pending decision; legacy deny keeps its
103
+ // terminal-denied behavior. Batch decisions are validated and applied atomically below.
104
+ const resolved = resume.decisions !== undefined
105
+ ? await resolveRunDecisions({ agent, state, decisions: resume.decisions, signal })
106
+ : resume.decision === "approve" && pendingDecisions
107
+ ? await resolveRunDecisions({
108
+ agent,
109
+ state,
110
+ decisions: pendingDecisions.map((pending) => ({ approvalId: pending.approvalId, outcome: "allow_once" })),
111
+ signal,
112
+ })
113
+ : undefined;
114
+ if (resume.decision === undefined && resume.decisions === undefined) {
115
+ throw new AgentDecisionError("ERR_PRISM_DECISION_INVALID", "Resume requires a decision or decisions");
116
+ }
117
+ if (resolved && resolved.remaining.length > 0) {
118
+ throwIfAbortedSignal(signal);
119
+ const single = resolved.remaining.length === 1 ? resolved.remaining[0] : undefined;
120
+ const interruption = {
121
+ kind: state.interruption?.kind ?? "tool_approval",
122
+ reason: `${resolved.remaining.length} approval request(s) remain`,
123
+ ...(single?.toolCallId ? { toolCallId: single.toolCallId } : {}),
124
+ ...(single?.scope.toolName ? { toolName: single.scope.toolName } : {}),
125
+ pendingDecisions: resolved.remaining,
126
+ };
127
+ const resuspended = await saveAgentRunState({
128
+ checkpoints: options.checkpoints,
129
+ state: {
130
+ ...state,
131
+ status: "suspended",
132
+ interruption,
133
+ pending: undefined,
134
+ // Decided approvals persist on their entries so a partial batch never loses them;
135
+ // they dispatch (or synthesize their result) when the run finally resumes.
136
+ pendingCalls: state.pendingCalls?.map((entry) => {
137
+ const decision = resolved.decisionsById.get(entry.approvalId);
138
+ return decision ? { ...entry, decision } : entry;
139
+ }),
140
+ // Decided nested approvals persist on their nested-run entries, keyed by
141
+ // root-visible approval id, so a partial batch never loses them either.
142
+ nestedRuns: state.nestedRuns?.map((entry) => {
143
+ const decided = entry.approvals.filter((approval) => resolved.decisionsById.has(approval.id));
144
+ if (decided.length === 0)
145
+ return entry;
146
+ return {
147
+ ...entry,
148
+ decisions: {
149
+ ...entry.decisions,
150
+ ...Object.fromEntries(decided.map((approval) => [approval.id, resolved.decisionsById.get(approval.id)])),
151
+ },
152
+ };
153
+ }),
154
+ stickyDecisions: resolved.stickyDecisions,
155
+ },
156
+ expectedVersion: record.version,
157
+ ownership: options.ownership,
158
+ fencingToken: options.fencingToken,
159
+ });
160
+ return {
161
+ kind: "resuspend",
162
+ session,
163
+ interruption,
164
+ version: resuspended.record.version,
165
+ ownership: options.ownership,
166
+ result: {
167
+ sessionId: state.sessionId,
168
+ runId: state.runId,
169
+ status: "suspended",
170
+ leafId: state.leafId,
171
+ text: "",
172
+ content: [],
173
+ runState: publicState(resuspended.state),
174
+ interruption,
175
+ },
176
+ };
177
+ }
178
+ if (resume.decision === "deny") {
179
+ throwIfAbortedSignal(signal);
180
+ const denied = await saveAgentRunState({
181
+ checkpoints: options.checkpoints,
182
+ state: {
183
+ ...state,
184
+ status: "denied",
185
+ loopState: undefined,
186
+ pendingCalls: undefined,
187
+ nestedRuns: undefined,
188
+ stickyDecisions: undefined,
189
+ },
190
+ expectedVersion: record.version,
191
+ ownership: options.ownership,
192
+ fencingToken: options.fencingToken,
193
+ });
194
+ return {
195
+ kind: "deny",
196
+ session,
197
+ interruption: state.interruption,
198
+ version: denied.record.version,
199
+ ownership: options.ownership,
200
+ result: {
201
+ sessionId: state.sessionId,
202
+ runId: state.runId,
203
+ status: "denied",
204
+ leafId: state.leafId,
205
+ text: "",
206
+ content: [],
207
+ runState: publicState(denied.state),
208
+ interruption: state.interruption,
209
+ },
210
+ };
211
+ }
212
+ if (state.pending?.status === "dispatched" || state.pendingCalls?.some((entry) => entry.status === "dispatched")) {
213
+ throw new AgentRunStateError("Ambiguous dispatched tool requires operator resolution");
214
+ }
215
+ const configured = agent.config.runState;
216
+ if (configured && (configured.checkpoints !== options.checkpoints || configured.definitionRevision !== options.definitionRevision)) {
217
+ throw new AgentRunStateError("Agent durable run-state configuration mismatch on resume");
218
+ }
219
+ throwIfAbortedSignal(signal);
220
+ const claimed = await saveAgentRunState({
221
+ checkpoints: options.checkpoints,
222
+ state: {
223
+ ...state,
224
+ status: "running",
225
+ interruption: undefined,
226
+ stickyDecisions: resolved?.stickyDecisions ?? state.stickyDecisions,
227
+ },
228
+ expectedVersion: record.version,
229
+ ownership: options.ownership,
230
+ fencingToken: options.fencingToken,
231
+ });
232
+ return {
233
+ kind: "approve",
234
+ session,
235
+ state: claimed.state,
236
+ decisions: resolved?.decisionsById,
237
+ ownership: options.ownership,
238
+ runState: configured ?? {
239
+ checkpoints: options.checkpoints,
240
+ definitionRevision: options.definitionRevision,
241
+ interruptBeforeTool: state.interruptBeforeTool,
242
+ fencingToken: options.fencingToken,
243
+ resumeNestedRun: options.resumeNestedRun,
244
+ },
245
+ };
246
+ }
247
+ /** Pending decisions of a suspended state, synthesizing the legacy single-approval shape. */
248
+ async function executePreparedAgentRunResume(prepared, signal) {
249
+ if (prepared.kind === "deny") {
250
+ await prepared.session.recordDurableDenial(prepared.result.runId, prepared.interruption, prepared.version, prepared.ownership);
251
+ return prepared.result;
252
+ }
253
+ if (prepared.kind === "resuspend") {
254
+ await prepared.session.recordDurableResumption(prepared.result.runId, prepared.interruption, prepared.version, prepared.ownership);
255
+ return prepared.result;
256
+ }
257
+ return prepared.session.resumeDurable(prepared.state, prepared.runState, prepared.ownership, signal, prepared.decisions);
258
+ }
49
259
  //# sourceMappingURL=agent-run-lifecycle.js.map
@@ -34,7 +34,18 @@ export interface StoredAgentRunState extends AgentRunState {
34
34
  readonly revision: string;
35
35
  readonly snapshot: JsonValue;
36
36
  };
37
+ /**
38
+ * Opt-in session-level state (plan 015 Task 4): loaded-skill names only; bodies are
39
+ * never persisted and reload on demand from the live registry via `load_skill`.
40
+ * Absent by default (0.1.x checkpoints parse unchanged).
41
+ */
42
+ readonly sessionState?: {
43
+ readonly loadedSkillNames?: readonly string[];
44
+ };
37
45
  }
46
+ /** Session-state caps (plan 015 Task 4): bounded names charged against the run-state byte budget. */
47
+ export declare const MAX_PERSISTED_SKILL_NAMES = 64;
48
+ export declare const MAX_PERSISTED_SKILL_NAME_CHARS = 256;
38
49
  /** Revision stamps of the built-in loops; custom strategies declare their own `revision`. */
39
50
  export declare const BUILT_IN_LOOP_REVISIONS: Readonly<Record<string, string>>;
40
51
  /** Validate a strategy snapshot as JSON-compatible and package it for the durable envelope. */
@@ -6,6 +6,9 @@ export const DEFAULT_MAX_AGENT_RUN_STATE_BYTES = 256 * 1024;
6
6
  export const HARD_MAX_AGENT_RUN_STATE_BYTES = 1024 * 1024;
7
7
  const MAX_DEPTH = 32;
8
8
  const MAX_PROPERTIES = 256;
9
+ /** Session-state caps (plan 015 Task 4): bounded names charged against the run-state byte budget. */
10
+ export const MAX_PERSISTED_SKILL_NAMES = 64;
11
+ export const MAX_PERSISTED_SKILL_NAME_CHARS = 256;
9
12
  /** Revision stamps of the built-in loops; custom strategies declare their own `revision`. */
10
13
  export const BUILT_IN_LOOP_REVISIONS = {
11
14
  "single-shot": "1",
@@ -207,6 +210,7 @@ export function parseAgentRunState(value, version) {
207
210
  return boundState({ ...state, version }, HARD_MAX_AGENT_RUN_STATE_BYTES);
208
211
  }
209
212
  function boundState(state, maxBytes) {
213
+ validateSessionState(state.sessionState);
210
214
  checkShape(state, 0);
211
215
  let text;
212
216
  try {
@@ -230,4 +234,23 @@ function checkShape(value, depth) {
230
234
  for (const item of entries)
231
235
  checkShape(item, depth + 1);
232
236
  }
237
+ /** Fail-closed validation of the opt-in session-state block (load and save sides). */
238
+ function validateSessionState(sessionState) {
239
+ if (sessionState === undefined)
240
+ return;
241
+ if (!sessionState || typeof sessionState !== "object") {
242
+ throw new AgentRunStateError("Malformed agent run session state");
243
+ }
244
+ const names = sessionState.loadedSkillNames;
245
+ if (names === undefined)
246
+ return;
247
+ if (!Array.isArray(names) || names.length > MAX_PERSISTED_SKILL_NAMES) {
248
+ throw new AgentRunStateError(`Loaded-skill names exceed ${MAX_PERSISTED_SKILL_NAMES} entries`);
249
+ }
250
+ for (const name of names) {
251
+ if (typeof name !== "string" || name.length > MAX_PERSISTED_SKILL_NAME_CHARS) {
252
+ throw new AgentRunStateError(`Loaded-skill name exceeds ${MAX_PERSISTED_SKILL_NAME_CHARS} chars`);
253
+ }
254
+ }
255
+ }
233
256
  //# sourceMappingURL=agent-run-state.js.map