@arnilo/prism-supervisor 0.0.24 → 0.0.25

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 CHANGED
@@ -1,5 +1,15 @@
1
1
  # Changelog
2
2
 
3
+ ## [0.0.25] - 2026-08-06
4
+
5
+ ### Added
6
+ - Durable child approvals: `checkpoints` + `definitionRevision` on supervisor; `resumeNestedRun` routes hashed attributed decisions without widening child permission.
7
+
8
+ ### Changed
9
+ - Released with exact 0.0.25 graph.
10
+
11
+ See [migration guide](../../docs/migration.md) for the 0.0.24 → 0.0.25 notes.
12
+
3
13
  ## [0.0.24] - 2026-08-04
4
14
 
5
15
  ### Added
package/README.md CHANGED
@@ -28,4 +28,4 @@ console.log((await supervisor.delegate({ childId: "research", input: "Check sour
28
28
 
29
29
  Also exports bounded A2A 1.0 cards, handler/client, rich one-of parts, `client.streamMessage()` for verified task/message stream records, host-owned `A2ATaskLifecycle`, `createA2AAgentEventSource()` for shared durable run→task subscriptions, reconnect subscriptions, and push-config CRUD. Direct text invocation remains compatible; durable get/list/cancel/subscribe and rich raw/data/URL parts require explicit adapters/policy. URL parts are validated but never fetched. Push persistence/network/credentials and exact-owner checks remain host-owned; explicit `deliverA2APushEvent()` only bounds attempts/time and forwards stable event IDs for host idempotency. Returned configs omit secrets. JSON-RPC/HTTPS is the only binding.
30
30
 
31
- See [Supervisors](../../docs/supervisors.md) and [A2A interoperability](../../docs/a2a.md).
31
+ Pass `checkpoints` + `definitionRevision` for durable child approvals (`resumeNestedRun` routes hashed attributed decisions without widening permission). See [Supervisors](../../docs/supervisors.md) and [A2A interoperability](../../docs/a2a.md).
@@ -1,9 +1,13 @@
1
- import { AgentRunError, assertIdentityActive, assertIdentityMatchesOwnership, createAgent, createEventMultiplexer, } from "@arnilo/prism";
1
+ import { AgentDelegationSuspendedError, AgentRunError, assertIdentityActive, assertIdentityMatchesOwnership, createAgent, createEventMultiplexer, resumeAgentRun, } from "@arnilo/prism";
2
2
  import { SupervisorDeniedError, SupervisorError, SupervisorLimitError, SupervisorValidationError } from "./errors.js";
3
3
  import { narrowSupervisorLimits, resolveSupervisorLimits } from "./limits.js";
4
4
  const ID = /^[A-Za-z0-9][A-Za-z0-9._:-]{0,127}$/;
5
+ const DELEGATION_NAMESPACE = "prism.supervisor-delegation";
5
6
  export function createSupervisor(options) {
6
7
  requireOwnership(options.ownership);
8
+ if (options.checkpoints && !options.definitionRevision?.trim()) {
9
+ throw new SupervisorValidationError("definitionRevision is required when checkpoints are configured");
10
+ }
7
11
  if (options.identity) {
8
12
  assertIdentityActive(options.identity);
9
13
  assertIdentityMatchesOwnership(options.identity, options.ownership);
@@ -117,6 +121,16 @@ export function createSupervisor(options) {
117
121
  ownership: options.ownership,
118
122
  redactor: options.redactor,
119
123
  metadata: { ...request.metadata, supervisorId: id, delegationId, resourceId, threadId, depth },
124
+ ...(options.checkpoints
125
+ ? {
126
+ runState: {
127
+ checkpoints: options.checkpoints,
128
+ definitionRevision: options.definitionRevision,
129
+ interruptBeforeTool: true,
130
+ resumeNestedRun,
131
+ },
132
+ }
133
+ : {}),
120
134
  }), controller.signal);
121
135
  }
122
136
  catch (error) {
@@ -132,6 +146,24 @@ export function createSupervisor(options) {
132
146
  }
133
147
  throw error;
134
148
  }
149
+ if (result.status === "suspended") {
150
+ // Child approvals surface on the hosting root run: persist the rebuild mapping, then
151
+ // signal core with the child's pending decisions (core hashes/attributes the ids).
152
+ const pending = result.interruption?.pendingDecisions;
153
+ const version = result.runState?.version;
154
+ if (!pending?.length || version === undefined) {
155
+ throw new SupervisorError("Child run suspended without a pending-decision set");
156
+ }
157
+ await saveMapping(result.runId, {
158
+ childId: request.childId,
159
+ delegationId,
160
+ threadId,
161
+ path,
162
+ version,
163
+ input,
164
+ });
165
+ throw new AgentDelegationSuspendedError({ runId: result.runId, sessionId: result.sessionId }, pending, path);
166
+ }
135
167
  const totalTokens = result.usage?.totalTokens ?? (result.usage?.inputTokens ?? 0) + (result.usage?.outputTokens ?? 0);
136
168
  events.publish({ type: "delegation_finished", childId: request.childId, delegationId, depth, status: result.status, totalTokens });
137
169
  await complete(toCompletion(result, request.childId, delegationId, depth, options));
@@ -139,6 +171,8 @@ export function createSupervisor(options) {
139
171
  return result;
140
172
  }
141
173
  catch (error) {
174
+ if (error instanceof AgentDelegationSuspendedError)
175
+ throw error;
142
176
  if (!(error instanceof SupervisorDeniedError && completionSent)) {
143
177
  const result = error instanceof AgentRunError ? error.result : undefined;
144
178
  const message = safeError(error, options);
@@ -164,6 +198,91 @@ export function createSupervisor(options) {
164
198
  activeChildren -= 1;
165
199
  }
166
200
  }
201
+ async function saveMapping(runId, mapping) {
202
+ const existing = await options.checkpoints.loadCheckpoint({ namespace: DELEGATION_NAMESPACE, key: runId });
203
+ await options.checkpoints.saveCheckpoint({
204
+ namespace: DELEGATION_NAMESPACE,
205
+ key: runId,
206
+ version: (existing?.version ?? 0) + 1,
207
+ expectedVersion: existing?.version,
208
+ value: mapping,
209
+ });
210
+ }
211
+ const resumeNestedRun = async (nested, decisions) => {
212
+ const checkpoints = options.checkpoints;
213
+ if (!checkpoints || !options.definitionRevision) {
214
+ throw new SupervisorValidationError("Nested-run resume requires supervisor checkpoints and definitionRevision");
215
+ }
216
+ const record = await checkpoints.loadCheckpoint({ namespace: DELEGATION_NAMESPACE, key: nested.ref.runId });
217
+ const mapping = record?.value;
218
+ // Non-enumerating: unknown and foreign run ids share one error.
219
+ if (!mapping || typeof mapping.childId !== "string" || typeof mapping.version !== "number") {
220
+ throw new SupervisorDeniedError("Unknown delegated run");
221
+ }
222
+ const child = options.children[mapping.childId];
223
+ if (!child)
224
+ throw new SupervisorDeniedError("Unknown delegated run");
225
+ const depth = mapping.path.length;
226
+ const controller = new AbortController();
227
+ let limits = narrowSupervisorLimits(baseLimits, child.limits);
228
+ let hookPermission;
229
+ // The before-hook re-runs at resume so its narrowing applies exactly as it did to the
230
+ // original run; hooks must be idempotent (same contract as core resume guardrails).
231
+ if (options.hooks?.before) {
232
+ const decision = await options.hooks.before(Object.freeze({
233
+ childId: mapping.childId,
234
+ delegationId: mapping.delegationId,
235
+ depth,
236
+ path: mapping.path,
237
+ input: mapping.input,
238
+ limits,
239
+ metadata: undefined,
240
+ signal: controller.signal,
241
+ }));
242
+ if (decision.allowed === false) {
243
+ return { status: "failed", code: "delegation_denied", message: safeError(decision.reason ?? "Delegation denied", options) };
244
+ }
245
+ limits = narrowSupervisorLimits(limits, decision.limits);
246
+ hookPermission = decision.permission;
247
+ }
248
+ const resourceId = `${id}/${mapping.delegationId}/${mapping.childId}`;
249
+ const permission = intersectPolicies(options.permission, child.permission, hookPermission, toolBudgetPolicy(limits.maxToolCalls));
250
+ const childAgent = await child.createAgent(Object.freeze({
251
+ childId: mapping.childId,
252
+ delegationId: mapping.delegationId,
253
+ depth,
254
+ path: mapping.path,
255
+ ownership: options.ownership,
256
+ identity: options.identity,
257
+ effectStore: options.effectStore,
258
+ resourceId,
259
+ threadId: mapping.threadId,
260
+ permission,
261
+ signal: controller.signal,
262
+ delegate: (nestedRequest) => delegate(nestedRequest, { path: mapping.path, signal: controller.signal }),
263
+ }));
264
+ const agent = createAgent({
265
+ ...childAgent.config,
266
+ permission: intersectPolicies(permission, childAgent.config.permission),
267
+ ownership: options.ownership,
268
+ identity: options.identity ?? childAgent.config.identity,
269
+ effectStore: options.effectStore ?? childAgent.config.effectStore,
270
+ redactor: options.redactor ?? childAgent.config.redactor,
271
+ });
272
+ const result = await resumeAgentRun(agent, { runId: nested.ref.runId, ...(nested.ref.sessionId ? { sessionId: nested.ref.sessionId } : {}) }, { decisions, expectedVersion: mapping.version }, { checkpoints, definitionRevision: options.definitionRevision, ownership: options.ownership, resumeNestedRun });
273
+ if (result.status === "suspended") {
274
+ await saveMapping(nested.ref.runId, { ...mapping, version: result.runState?.version ?? mapping.version });
275
+ return { status: "suspended", pendingDecisions: result.interruption?.pendingDecisions ?? [] };
276
+ }
277
+ if (result.status === "succeeded") {
278
+ return { status: "completed", value: options.redactor?.redact(result.text) ?? result.text };
279
+ }
280
+ return {
281
+ status: "failed",
282
+ code: result.status === "denied" ? "delegation_denied" : "delegation_failed",
283
+ message: safeError(result.error?.message ?? `Delegated run ${result.status}`, options),
284
+ };
285
+ };
167
286
  async function complete(value) {
168
287
  if (!options.hooks?.after)
169
288
  return;
@@ -182,6 +301,7 @@ export function createSupervisor(options) {
182
301
  }
183
302
  return {
184
303
  delegate: (request) => delegate(request),
304
+ resumeNestedRun,
185
305
  subscribe: () => events.subscribe(),
186
306
  get activeChildren() {
187
307
  return activeChildren;
package/dist/types.d.ts CHANGED
@@ -1,4 +1,4 @@
1
- import type { Agent, AgentIdentity, AgentRunResult, OwnershipScope, PermissionPolicy, SecretRedactor, ToolEffectStore } from "@arnilo/prism";
1
+ import type { Agent, AgentIdentity, AgentRunResult, CheckpointStore, OwnershipScope, PermissionPolicy, ResumeNestedRun, SecretRedactor, ToolEffectStore } from "@arnilo/prism";
2
2
  import type { ResolvedSupervisorLimits, SupervisorLimits } from "./limits.js";
3
3
  export interface DelegationRequest {
4
4
  readonly childId: string;
@@ -99,9 +99,23 @@ export interface CreateSupervisorOptions {
99
99
  readonly limits?: SupervisorLimits;
100
100
  readonly hooks?: SupervisorHooks;
101
101
  readonly redactor?: SecretRedactor;
102
+ /**
103
+ * Durable child runs: with `checkpoints` + `definitionRevision`, every child runs with
104
+ * `interruptBeforeTool`; a child that suspends on pending decisions throws
105
+ * `AgentDelegationSuspendedError` so the hosting root run can surface them.
106
+ */
107
+ readonly checkpoints?: CheckpointStore;
108
+ /** Host-authored revision shared by child durable runs; bump on policy/definition change. */
109
+ readonly definitionRevision?: string;
102
110
  }
103
111
  export interface Supervisor {
104
112
  delegate(request: DelegationRequest): Promise<AgentRunResult>;
113
+ /**
114
+ * Routes root-run decisions back to the suspended child. Pass as `resumeNestedRun` in the
115
+ * root run's `runState` (sticky auto-apply) and every `resumeAgentRun` options object.
116
+ * Throws when `checkpoints`/`definitionRevision` are not configured.
117
+ */
118
+ readonly resumeNestedRun: ResumeNestedRun;
105
119
  subscribe(): AsyncIterable<SupervisorEvent>;
106
120
  readonly activeChildren: number;
107
121
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@arnilo/prism-supervisor",
3
- "version": "0.0.24",
3
+ "version": "0.0.25",
4
4
  "description": "Bounded supervisor delegation and A2A 1.0 durable task, rich-part, reconnect, and push interoperability.",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
@@ -25,7 +25,7 @@
25
25
  "pack:dry-run": "npm pack --dry-run"
26
26
  },
27
27
  "peerDependencies": {
28
- "@arnilo/prism": "0.0.24"
28
+ "@arnilo/prism": "0.0.25"
29
29
  },
30
30
  "devDependencies": {
31
31
  "@arnilo/prism": "file:../.."