@arnilo/prism-supervisor 0.0.23 → 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,11 +1,30 @@
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
+
13
+ ## [0.0.24] - 2026-08-04
14
+
15
+ ### Added
16
+ - Durable `AgentEventSource` (memory + PostgreSQL LISTEN/NOTIFY), recoverable `ToolEffectStore`, and AG-UI MCP/MCP Apps/A2A fronting for Phase 7.
17
+
18
+ ### Changed
19
+ - Publishable graph remains **47** manifests at **0.0.24**; peers and lockfile move together.
20
+
21
+ See [migration guide](../../docs/migration.md) for the 0.0.23 → 0.0.24 notes.
22
+
3
23
  ## [0.0.23] - 2026-08-03
4
24
 
5
25
  ### Changed
6
26
  - Released with exact 0.0.23 graph.
7
27
 
8
-
9
28
  ## [0.0.22] - 2026-07-31
10
29
 
11
30
  ### Changed
@@ -16,14 +35,11 @@
16
35
  ### Changed
17
36
  - Released with exact 0.0.21 graph.
18
37
 
19
-
20
-
21
38
  ## [0.0.20] - 2026-07-31
22
39
 
23
40
  ### Changed
24
41
  - Released with exact 0.0.20 graph.
25
42
 
26
-
27
43
  ## [0.0.19] - 2026-07-30
28
44
 
29
45
  ### Changed
package/README.md CHANGED
@@ -26,6 +26,6 @@ const supervisor = createSupervisor({
26
26
  console.log((await supervisor.delegate({ childId: "research", input: "Check sources" })).text);
27
27
  ```
28
28
 
29
- Also exports bounded A2A 1.0 cards, handler/client, rich one-of parts, host-owned `A2ATaskLifecycle`, 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.
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).
@@ -70,17 +70,17 @@ export function createA2AClient(options) {
70
70
  return taskResult(parseTaskResult(rpc.result), options);
71
71
  });
72
72
  }
73
- async function* stream(input, call = {}) {
73
+ async function* streamMessage(message, call = {}) {
74
74
  if (active >= limits.maxConcurrentRequests)
75
75
  throw new A2AError("A2A client concurrency exceeded", 429, "ERR_PRISM_A2A_CONCURRENCY");
76
76
  active += 1;
77
77
  const owned = ownedSignal(call.signal, limits.timeoutMs);
78
78
  let reader;
79
79
  try {
80
- assertInput(input, limits.maxRequestBytes);
80
+ assertMessage(message, limits.maxRequestBytes);
81
81
  await getCardWithin(owned.signal);
82
82
  const id = ++requestId;
83
- const body = JSON.stringify(requestBody(id, "SendStreamingMessage", input));
83
+ const body = JSON.stringify({ jsonrpc: "2.0", id, method: "SendStreamingMessage", params: { message } });
84
84
  if (new TextEncoder().encode(body).byteLength > limits.maxRequestBytes)
85
85
  throw new A2AError("A2A request exceeds max bytes", 413, "ERR_PRISM_A2A_REQUEST_LIMIT");
86
86
  const authHeaders = await abortable(Promise.resolve(options.authorize?.({ endpoint: endpoint.href, signal: owned.signal }) ?? {}), owned.signal);
@@ -100,6 +100,7 @@ export function createA2AClient(options) {
100
100
  throw new A2AError("A2A stream request failed", response.status, "ERR_PRISM_A2A_REMOTE");
101
101
  reader = response.body.getReader();
102
102
  let terminal = false;
103
+ let count = 0;
103
104
  for await (const data of readA2AStreamData(reader, limits, owned.signal)) {
104
105
  if (terminal)
105
106
  throw new A2AError("A2A stream continued after terminal task state", 502, "ERR_PRISM_A2A_REMOTE");
@@ -115,19 +116,17 @@ export function createA2AClient(options) {
115
116
  const rpc = parseRpcResponse(parsed, id);
116
117
  if (rpc.error)
117
118
  throw new A2AError(safeRemote(rpc.error.message, options), 502, "ERR_PRISM_A2A_REMOTE");
118
- const task = parseTaskResult(rpc.result);
119
- if (task.status.state === "TASK_STATE_FAILED" ||
120
- task.status.state === "TASK_STATE_CANCELED" ||
121
- task.status.state === "TASK_STATE_REJECTED")
122
- throw new A2AError("Remote A2A stream task failed", 502, "ERR_PRISM_A2A_REMOTE");
123
- if (task.status.state === "TASK_STATE_INPUT_REQUIRED" || task.status.state === "TASK_STATE_AUTH_REQUIRED")
124
- throw new A2AError(`Remote A2A task interrupted: ${task.status.state}`, 409, "ERR_PRISM_A2A_INTERRUPTED");
125
- if (task.status.state === "TASK_STATE_COMPLETED")
119
+ const event = parseA2AStreamEvent(rpc.result, `stream-${++count}`);
120
+ const status = eventStatus(event);
121
+ if (status === "TASK_STATE_FAILED" ||
122
+ status === "TASK_STATE_CANCELED" ||
123
+ status === "TASK_STATE_REJECTED" ||
124
+ status === "TASK_STATE_INPUT_REQUIRED" ||
125
+ status === "TASK_STATE_AUTH_REQUIRED")
126
+ terminal = true;
127
+ if (status === "TASK_STATE_COMPLETED")
126
128
  terminal = true;
127
- for (const artifact of task.artifacts ?? [])
128
- for (const part of artifact.parts)
129
- if (typeof part.text === "string")
130
- yield options.redactor?.redact(part.text) ?? part.text;
129
+ yield event;
131
130
  }
132
131
  if (!terminal)
133
132
  throw new A2AError("A2A stream ended before terminal task state", 502, "ERR_PRISM_A2A_REMOTE");
@@ -138,6 +137,18 @@ export function createA2AClient(options) {
138
137
  active -= 1;
139
138
  }
140
139
  }
140
+ async function* stream(input, call = {}) {
141
+ assertInput(input, limits.maxRequestBytes);
142
+ for await (const event of streamMessage({ role: "user", messageId: "stream-input", parts: [{ text: input }] }, call)) {
143
+ const status = eventStatus(event);
144
+ if (status === "TASK_STATE_FAILED" || status === "TASK_STATE_CANCELED" || status === "TASK_STATE_REJECTED")
145
+ throw new A2AError("Remote A2A stream task failed", 502, "ERR_PRISM_A2A_REMOTE");
146
+ if (status === "TASK_STATE_INPUT_REQUIRED" || status === "TASK_STATE_AUTH_REQUIRED")
147
+ throw new A2AError(`Remote A2A task interrupted: ${status}`, 409, "ERR_PRISM_A2A_INTERRUPTED");
148
+ for (const text of streamEventText(event))
149
+ yield options.redactor?.redact(text) ?? text;
150
+ }
151
+ }
141
152
  async function getCardWithin(signal) {
142
153
  const response = await fetcher(cardUrl, {
143
154
  method: "GET",
@@ -226,15 +237,16 @@ export function createA2AClient(options) {
226
237
  if (!response.ok || !response.body || !response.headers.get("content-type")?.startsWith("text/event-stream"))
227
238
  throw new A2AError("A2A subscribe request failed", response.status, "ERR_PRISM_A2A_REMOTE");
228
239
  reader = response.body.getReader();
229
- let previous = "", count = 0;
240
+ const seen = new Set();
241
+ let count = 0;
230
242
  for await (const data of readA2AStreamData(reader, limits, owned.signal)) {
231
243
  const rpc = parseRpcResponse(JSON.parse(data), request);
232
244
  if (rpc.error)
233
245
  throw remoteProtocolError(rpc.error.code, rpc.error.message, options);
234
246
  const event = parseTaskEvent(rpc.result);
235
- if (event.eventId === previous)
247
+ if (seen.has(event.eventId))
236
248
  continue;
237
- previous = event.eventId;
249
+ seen.add(event.eventId);
238
250
  if (++count > limits.maxReplayEvents)
239
251
  throw new A2AError("A2A replay exceeds event limit", 507, "ERR_PRISM_A2A_STREAM_LIMIT");
240
252
  yield event;
@@ -269,6 +281,7 @@ export function createA2AClient(options) {
269
281
  send,
270
282
  sendMessage,
271
283
  stream,
284
+ streamMessage,
272
285
  getTask,
273
286
  listTasks,
274
287
  cancelTask,
@@ -433,6 +446,7 @@ function parseTaskResult(value) {
433
446
  status: {
434
447
  state: task.status.state,
435
448
  timestamp: typeof task.status.timestamp === "string" ? task.status.timestamp : new Date(0).toISOString(),
449
+ ...(task.status.message === undefined ? {} : { message: parseRemoteMessage(task.status.message) }),
436
450
  },
437
451
  artifacts,
438
452
  history,
@@ -487,6 +501,31 @@ function parseRemoteMessage(value) {
487
501
  taskId: typeof value.taskId === "string" ? value.taskId : undefined,
488
502
  };
489
503
  }
504
+ function parseA2AStreamEvent(value, fallbackEventId) {
505
+ if (isRecord(value) && isRecord(value.message))
506
+ return { eventId: fallbackEventId, message: parseRemoteMessage(value.message) };
507
+ if (isRecord(value) && !Object.hasOwn(value, "eventId") && (isRecord(value.task) || typeof value.id === "string")) {
508
+ return { eventId: fallbackEventId, task: parseTaskResult(value) };
509
+ }
510
+ return parseTaskEvent(value);
511
+ }
512
+ function eventStatus(event) {
513
+ if ("task" in event)
514
+ return event.task.status.state;
515
+ if ("statusUpdate" in event)
516
+ return event.statusUpdate.status.state;
517
+ return undefined;
518
+ }
519
+ function streamEventText(event) {
520
+ const parts = "message" in event
521
+ ? event.message.parts
522
+ : "task" in event
523
+ ? (event.task.artifacts?.flatMap((artifact) => artifact.parts) ?? [])
524
+ : "artifactUpdate" in event
525
+ ? event.artifactUpdate.artifact.parts
526
+ : [];
527
+ return parts.flatMap((part) => (typeof part.text === "string" ? [part.text] : []));
528
+ }
490
529
  function parseTaskEvent(value) {
491
530
  if (!isRecord(value) || typeof value.eventId !== "string" || !value.eventId)
492
531
  throw new A2AError("Malformed A2A task event", 502, "ERR_PRISM_A2A_REMOTE");
@@ -688,6 +727,19 @@ function assertInput(input, maxBytes) {
688
727
  if (new TextEncoder().encode(input).byteLength > maxBytes)
689
728
  throw new A2AError("A2A input exceeds max bytes", 413, "ERR_PRISM_A2A_REQUEST_LIMIT");
690
729
  }
730
+ function assertMessage(message, maxBytes) {
731
+ if (!message.messageId || !Array.isArray(message.parts) || message.parts.length === 0)
732
+ throw new A2AError("Invalid A2A outbound message", 400, "ERR_PRISM_A2A_MESSAGE");
733
+ let encoded;
734
+ try {
735
+ encoded = JSON.stringify(message);
736
+ }
737
+ catch {
738
+ throw new A2AError("Invalid A2A outbound message", 400, "ERR_PRISM_A2A_MESSAGE");
739
+ }
740
+ if (Buffer.byteLength(encoded, "utf8") > maxBytes)
741
+ throw new A2AError("A2A input exceeds max bytes", 413, "ERR_PRISM_A2A_REQUEST_LIMIT");
742
+ }
691
743
  function headersObject(headers) {
692
744
  return Object.fromEntries(new Headers(headers).entries());
693
745
  }
@@ -0,0 +1,42 @@
1
+ import type { AgentEventSource, AgentRunRef, DurableAgentEventRecord } from "@arnilo/prism";
2
+ import type { A2AAuthorization, A2ATask, A2ATaskEvent } from "./a2a-types.js";
3
+ export type A2ATaskEventPayload = {
4
+ readonly task: A2ATask;
5
+ } | {
6
+ readonly statusUpdate: Extract<A2ATaskEvent, {
7
+ readonly statusUpdate: unknown;
8
+ }>["statusUpdate"];
9
+ } | {
10
+ readonly artifactUpdate: Extract<A2ATaskEvent, {
11
+ readonly artifactUpdate: unknown;
12
+ }>["artifactUpdate"];
13
+ };
14
+ export interface A2AAgentEventTask {
15
+ readonly task: A2ATask;
16
+ readonly run: AgentRunRef;
17
+ }
18
+ export interface A2AAgentEventSourceOptions {
19
+ readonly source: AgentEventSource;
20
+ /** Resolves only host-owned task state and its exact Prism run. */
21
+ readonly resolveTask: (input: {
22
+ readonly id: string;
23
+ readonly authorization: A2AAuthorization;
24
+ readonly signal: AbortSignal;
25
+ }) => A2AAgentEventTask | undefined | Promise<A2AAgentEventTask | undefined>;
26
+ /** Maps at most one durable Prism record to one A2A update. Event IDs stay source-owned cursors. */
27
+ readonly map: (input: {
28
+ readonly record: DurableAgentEventRecord;
29
+ readonly task: A2ATask;
30
+ readonly authorization: A2AAuthorization;
31
+ }) => A2ATaskEventPayload | undefined | Promise<A2ATaskEventPayload | undefined>;
32
+ }
33
+ export interface A2AAgentEventSource {
34
+ subscribe(input: {
35
+ readonly id: string;
36
+ readonly afterEventId?: string;
37
+ readonly authorization: A2AAuthorization;
38
+ readonly signal: AbortSignal;
39
+ }): AsyncIterable<A2ATaskEvent>;
40
+ }
41
+ /** Host-selected durable task stream over AgentEventSource; owns no task database or worker. */
42
+ export declare function createA2AAgentEventSource(options: A2AAgentEventSourceOptions): A2AAgentEventSource;
@@ -0,0 +1,40 @@
1
+ import { A2AError } from "./errors.js";
2
+ /** Host-selected durable task stream over AgentEventSource; owns no task database or worker. */
3
+ export function createA2AAgentEventSource(options) {
4
+ return {
5
+ subscribe(input) {
6
+ return {
7
+ async *[Symbol.asyncIterator]() {
8
+ input.signal.throwIfAborted();
9
+ const resolved = await options.resolveTask(input);
10
+ if (!resolved?.run.sessionId || resolved.task.id !== input.id) {
11
+ throw new A2AError("Task unavailable", 404, "ERR_PRISM_A2A_TASK");
12
+ }
13
+ let first = true;
14
+ for await (const item of options.source.subscribe({
15
+ ownership: input.authorization.ownership,
16
+ sessionId: resolved.run.sessionId,
17
+ runId: resolved.run.runId,
18
+ after: input.afterEventId,
19
+ signal: input.signal,
20
+ })) {
21
+ if (!item.record.redacted)
22
+ throw new A2AError("Task event unavailable", 500, "ERR_PRISM_A2A_TASK");
23
+ const payload = await options.map({ record: item.record, task: resolved.task, authorization: input.authorization });
24
+ if (!payload)
25
+ continue;
26
+ if (first && input.afterEventId === undefined && !("task" in payload)) {
27
+ throw new A2AError("Initial task event required", 500, "ERR_PRISM_A2A_TASK");
28
+ }
29
+ first = false;
30
+ yield { eventId: item.cursor, ...payload };
31
+ }
32
+ if (first && input.afterEventId === undefined) {
33
+ throw new A2AError("Initial task event required", 500, "ERR_PRISM_A2A_TASK");
34
+ }
35
+ },
36
+ };
37
+ },
38
+ };
39
+ }
40
+ //# sourceMappingURL=a2a-event-source.js.map
@@ -113,6 +113,11 @@ export type A2ATaskEvent = {
113
113
  readonly lastChunk?: boolean;
114
114
  };
115
115
  };
116
+ /** A streaming response may be a task lifecycle or one direct message. */
117
+ export type A2AStreamEvent = A2ATaskEvent | {
118
+ readonly eventId: string;
119
+ readonly message: A2AMessage;
120
+ };
116
121
  export type A2ARequestId = string | number | null;
117
122
  export interface A2AJsonRpcRequest {
118
123
  readonly jsonrpc: "2.0";
@@ -286,6 +291,10 @@ export interface A2AClient {
286
291
  stream(input: string, options?: {
287
292
  readonly signal?: AbortSignal;
288
293
  }): AsyncIterable<string>;
294
+ /** Rich `SendStreamingMessage` events. Host-supplied messages remain subject to client bounds/card verification. */
295
+ streamMessage(message: A2AMessage, options?: {
296
+ readonly signal?: AbortSignal;
297
+ }): AsyncIterable<A2AStreamEvent>;
289
298
  getTask(id: string, options?: {
290
299
  readonly signal?: AbortSignal;
291
300
  readonly historyLength?: number;
package/dist/index.d.ts CHANGED
@@ -1,4 +1,5 @@
1
1
  export * from "./a2a-card.js";
2
+ export * from "./a2a-event-source.js";
2
3
  export * from "./a2a-client.js";
3
4
  export * from "./a2a-parts.js";
4
5
  export * from "./a2a-push.js";
package/dist/index.js CHANGED
@@ -1,4 +1,5 @@
1
1
  export * from "./a2a-card.js";
2
+ export * from "./a2a-event-source.js";
2
3
  export * from "./a2a-client.js";
3
4
  export * from "./a2a-parts.js";
4
5
  export * from "./a2a-push.js";
@@ -1,9 +1,17 @@
1
- import { AgentRunError, 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
+ }
11
+ if (options.identity) {
12
+ assertIdentityActive(options.identity);
13
+ assertIdentityMatchesOwnership(options.identity, options.ownership);
14
+ }
7
15
  const id = options.id ?? "supervisor";
8
16
  if (!ID.test(id))
9
17
  throw new SupervisorValidationError("Supervisor id is invalid");
@@ -80,6 +88,8 @@ export function createSupervisor(options) {
80
88
  depth,
81
89
  path,
82
90
  ownership: options.ownership,
91
+ identity: options.identity,
92
+ effectStore: options.effectStore,
83
93
  resourceId,
84
94
  threadId,
85
95
  permission: preliminaryPermission,
@@ -90,6 +100,8 @@ export function createSupervisor(options) {
90
100
  ...childAgent.config,
91
101
  permission: intersectPolicies(preliminaryPermission, childAgent.config.permission),
92
102
  ownership: options.ownership,
103
+ identity: options.identity ?? childAgent.config.identity,
104
+ effectStore: options.effectStore ?? childAgent.config.effectStore,
93
105
  redactor: options.redactor ?? childAgent.config.redactor,
94
106
  });
95
107
  const session = agent.createSession({
@@ -109,6 +121,16 @@ export function createSupervisor(options) {
109
121
  ownership: options.ownership,
110
122
  redactor: options.redactor,
111
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
+ : {}),
112
134
  }), controller.signal);
113
135
  }
114
136
  catch (error) {
@@ -124,6 +146,24 @@ export function createSupervisor(options) {
124
146
  }
125
147
  throw error;
126
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
+ }
127
167
  const totalTokens = result.usage?.totalTokens ?? (result.usage?.inputTokens ?? 0) + (result.usage?.outputTokens ?? 0);
128
168
  events.publish({ type: "delegation_finished", childId: request.childId, delegationId, depth, status: result.status, totalTokens });
129
169
  await complete(toCompletion(result, request.childId, delegationId, depth, options));
@@ -131,6 +171,8 @@ export function createSupervisor(options) {
131
171
  return result;
132
172
  }
133
173
  catch (error) {
174
+ if (error instanceof AgentDelegationSuspendedError)
175
+ throw error;
134
176
  if (!(error instanceof SupervisorDeniedError && completionSent)) {
135
177
  const result = error instanceof AgentRunError ? error.result : undefined;
136
178
  const message = safeError(error, options);
@@ -156,6 +198,91 @@ export function createSupervisor(options) {
156
198
  activeChildren -= 1;
157
199
  }
158
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
+ };
159
286
  async function complete(value) {
160
287
  if (!options.hooks?.after)
161
288
  return;
@@ -174,6 +301,7 @@ export function createSupervisor(options) {
174
301
  }
175
302
  return {
176
303
  delegate: (request) => delegate(request),
304
+ resumeNestedRun,
177
305
  subscribe: () => events.subscribe(),
178
306
  get activeChildren() {
179
307
  return activeChildren;
package/dist/types.d.ts CHANGED
@@ -1,4 +1,4 @@
1
- import type { Agent, AgentRunResult, OwnershipScope, PermissionPolicy, SecretRedactor } 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;
@@ -14,6 +14,10 @@ export interface DelegationChildContext {
14
14
  readonly depth: number;
15
15
  readonly path: readonly string[];
16
16
  readonly ownership: OwnershipScope;
17
+ /** Parent-verified identity; child factories cannot widen it. */
18
+ readonly identity?: AgentIdentity;
19
+ /** One shared durable effect store for every child run. */
20
+ readonly effectStore?: ToolEffectStore;
17
21
  readonly resourceId: string;
18
22
  readonly threadId: string;
19
23
  readonly permission: PermissionPolicy;
@@ -86,14 +90,32 @@ export type SupervisorEvent = {
86
90
  export interface CreateSupervisorOptions {
87
91
  readonly id?: string;
88
92
  readonly ownership: OwnershipScope;
93
+ /** Optional parent-verified identity, propagated unchanged to every child. */
94
+ readonly identity?: AgentIdentity;
95
+ /** Optional parent effect store, propagated unchanged to every child. */
96
+ readonly effectStore?: ToolEffectStore;
89
97
  readonly children: Readonly<Record<string, SupervisorChild>>;
90
98
  readonly permission?: PermissionPolicy;
91
99
  readonly limits?: SupervisorLimits;
92
100
  readonly hooks?: SupervisorHooks;
93
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;
94
110
  }
95
111
  export interface Supervisor {
96
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;
97
119
  subscribe(): AsyncIterable<SupervisorEvent>;
98
120
  readonly activeChildren: number;
99
121
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@arnilo/prism-supervisor",
3
- "version": "0.0.23",
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.23"
28
+ "@arnilo/prism": "0.0.25"
29
29
  },
30
30
  "devDependencies": {
31
31
  "@arnilo/prism": "file:../.."