@arnilo/prism-supervisor 0.0.96 → 0.1.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.
@@ -1,4 +1,4 @@
1
- import type { AgentRunResult, AgentSession, OwnershipScope, SecretRedactor } from "@arnilo/prism";
1
+ import type { AgentIdentity, AgentRunResult, AgentSession, OwnershipScope, SecretRedactor } from "@arnilo/prism";
2
2
  export declare const A2A_PROTOCOL_VERSION = "1.0";
3
3
  export interface A2AAgentInterface {
4
4
  readonly url: string;
@@ -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";
@@ -132,6 +137,8 @@ export interface A2AJsonRpcResponse {
132
137
  }
133
138
  export interface A2AAuthorization {
134
139
  readonly ownership: OwnershipScope;
140
+ /** Host-verified identity; when set must project onto ownership without widening. */
141
+ readonly identity?: AgentIdentity;
135
142
  readonly metadata?: Readonly<Record<string, unknown>>;
136
143
  }
137
144
  export type A2AAuthorizer = (input: {
@@ -284,6 +291,10 @@ export interface A2AClient {
284
291
  stream(input: string, options?: {
285
292
  readonly signal?: AbortSignal;
286
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>;
287
298
  getTask(id: string, options?: {
288
299
  readonly signal?: AbortSignal;
289
300
  readonly historyLength?: number;
package/dist/errors.js CHANGED
@@ -7,13 +7,22 @@ export class SupervisorError extends Error {
7
7
  }
8
8
  }
9
9
  export class SupervisorValidationError extends SupervisorError {
10
- constructor(message) { super(message, "ERR_PRISM_SUPERVISOR_VALIDATION"); this.name = "SupervisorValidationError"; }
10
+ constructor(message) {
11
+ super(message, "ERR_PRISM_SUPERVISOR_VALIDATION");
12
+ this.name = "SupervisorValidationError";
13
+ }
11
14
  }
12
15
  export class SupervisorLimitError extends SupervisorError {
13
- constructor(message) { super(message, "ERR_PRISM_SUPERVISOR_LIMIT"); this.name = "SupervisorLimitError"; }
16
+ constructor(message) {
17
+ super(message, "ERR_PRISM_SUPERVISOR_LIMIT");
18
+ this.name = "SupervisorLimitError";
19
+ }
14
20
  }
15
21
  export class SupervisorDeniedError extends SupervisorError {
16
- constructor(message = "Delegation denied") { super(message, "ERR_PRISM_SUPERVISOR_DENIED"); this.name = "SupervisorDeniedError"; }
22
+ constructor(message = "Delegation denied") {
23
+ super(message, "ERR_PRISM_SUPERVISOR_DENIED");
24
+ this.name = "SupervisorDeniedError";
25
+ }
17
26
  }
18
27
  export class A2AError extends SupervisorError {
19
28
  status;
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";
package/dist/limits.js CHANGED
@@ -37,6 +37,9 @@ export function narrowSupervisorLimits(parent, input) {
37
37
  if (!input)
38
38
  return parent;
39
39
  const requested = resolveSupervisorLimits({ ...parent, ...input });
40
- return Object.fromEntries(Object.keys(SPECS).map((key) => [key, Math.min(parent[key], requested[key])]));
40
+ return Object.fromEntries(Object.keys(SPECS).map((key) => [
41
+ key,
42
+ Math.min(parent[key], requested[key]),
43
+ ]));
41
44
  }
42
45
  //# sourceMappingURL=limits.js.map
@@ -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,9 +100,14 @@ 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
- const session = agent.createSession({ id: `${delegationId}-session`, metadata: { supervisorId: id, delegationId, resourceId, threadId } });
107
+ const session = agent.createSession({
108
+ id: `${delegationId}-session`,
109
+ metadata: { supervisorId: id, delegationId, resourceId, threadId },
110
+ });
96
111
  let result;
97
112
  try {
98
113
  result = await abortable(session.run(input, {
@@ -106,37 +121,72 @@ export function createSupervisor(options) {
106
121
  ownership: options.ownership,
107
122
  redactor: options.redactor,
108
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
+ : {}),
109
134
  }), controller.signal);
110
135
  }
111
136
  catch (error) {
112
137
  if (error instanceof AgentRunError && error.result.limit) {
113
- const label = error.result.limit.limit === "maxTotalTokens" ? "token"
114
- : error.result.limit.limit === "maxToolCalls" ? "tool-call"
115
- : error.result.limit.limit === "maxWallTimeMs" ? "timeout"
138
+ const label = error.result.limit.limit === "maxTotalTokens"
139
+ ? "token"
140
+ : error.result.limit.limit === "maxToolCalls"
141
+ ? "tool-call"
142
+ : error.result.limit.limit === "maxWallTimeMs"
143
+ ? "timeout"
116
144
  : "run";
117
145
  throw new SupervisorLimitError(`Delegation ${label} limit exceeded`);
118
146
  }
119
147
  throw error;
120
148
  }
121
- const totalTokens = result.usage?.totalTokens ?? ((result.usage?.inputTokens ?? 0) + (result.usage?.outputTokens ?? 0));
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
+ }
167
+ const totalTokens = result.usage?.totalTokens ?? (result.usage?.inputTokens ?? 0) + (result.usage?.outputTokens ?? 0);
122
168
  events.publish({ type: "delegation_finished", childId: request.childId, delegationId, depth, status: result.status, totalTokens });
123
169
  await complete(toCompletion(result, request.childId, delegationId, depth, options));
124
170
  completionSent = true;
125
171
  return result;
126
172
  }
127
173
  catch (error) {
174
+ if (error instanceof AgentDelegationSuspendedError)
175
+ throw error;
128
176
  if (!(error instanceof SupervisorDeniedError && completionSent)) {
129
177
  const result = error instanceof AgentRunError ? error.result : undefined;
130
178
  const message = safeError(error, options);
131
179
  events.publish({ type: "delegation_error", childId: request.childId, delegationId, depth, error: message });
132
- await complete(result ? toCompletion(result, request.childId, delegationId, depth, options) : {
133
- childId: request.childId,
134
- delegationId,
135
- depth,
136
- status: controller.signal.aborted ? "aborted" : "rejected",
137
- text: "",
138
- error: message,
139
- });
180
+ await complete(result
181
+ ? toCompletion(result, request.childId, delegationId, depth, options)
182
+ : {
183
+ childId: request.childId,
184
+ delegationId,
185
+ depth,
186
+ status: controller.signal.aborted ? "aborted" : "rejected",
187
+ text: "",
188
+ error: message,
189
+ });
140
190
  }
141
191
  if (error instanceof AgentRunError || error instanceof SupervisorError)
142
192
  throw error;
@@ -148,6 +198,91 @@ export function createSupervisor(options) {
148
198
  activeChildren -= 1;
149
199
  }
150
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
+ };
151
286
  async function complete(value) {
152
287
  if (!options.hooks?.after)
153
288
  return;
@@ -155,10 +290,23 @@ export function createSupervisor(options) {
155
290
  await options.hooks.after(Object.freeze(value));
156
291
  }
157
292
  catch (error) {
158
- events.publish({ type: "delegation_error", childId: value.childId, delegationId: value.delegationId, depth: value.depth, error: safeError(error, options) });
293
+ events.publish({
294
+ type: "delegation_error",
295
+ childId: value.childId,
296
+ delegationId: value.delegationId,
297
+ depth: value.depth,
298
+ error: safeError(error, options),
299
+ });
159
300
  }
160
301
  }
161
- return { delegate: (request) => delegate(request), subscribe: () => events.subscribe(), get activeChildren() { return activeChildren; } };
302
+ return {
303
+ delegate: (request) => delegate(request),
304
+ resumeNestedRun,
305
+ subscribe: () => events.subscribe(),
306
+ get activeChildren() {
307
+ return activeChildren;
308
+ },
309
+ };
162
310
  }
163
311
  function toCompletion(result, childId, delegationId, depth, options) {
164
312
  return Object.freeze({
@@ -173,23 +321,27 @@ function toCompletion(result, childId, delegationId, depth, options) {
173
321
  }
174
322
  function intersectPolicies(...policies) {
175
323
  const active = policies.filter((policy) => policy !== undefined);
176
- return { async check(request) {
324
+ return {
325
+ async check(request) {
177
326
  for (const policy of active) {
178
327
  const decision = await policy.check(request);
179
328
  if (!decision.allowed)
180
329
  return decision;
181
330
  }
182
331
  return { allowed: true };
183
- } };
332
+ },
333
+ };
184
334
  }
185
335
  function toolBudgetPolicy(max) {
186
336
  let count = 0;
187
- return { check(request) {
337
+ return {
338
+ check(request) {
188
339
  if (request.kind !== "tool" || request.action !== "execute")
189
340
  return { allowed: true };
190
341
  count += 1;
191
342
  return count <= max ? { allowed: true } : { allowed: false, reason: "Delegation tool-call limit exceeded" };
192
- } };
343
+ },
344
+ };
193
345
  }
194
346
  function linkSignals(controller, ...signals) {
195
347
  const removers = [];
@@ -204,8 +356,10 @@ function linkSignals(controller, ...signals) {
204
356
  removers.push(() => signal.removeEventListener("abort", abort));
205
357
  }
206
358
  }
207
- return () => { for (const remove of removers)
208
- remove(); };
359
+ return () => {
360
+ for (const remove of removers)
361
+ remove();
362
+ };
209
363
  }
210
364
  function abortable(promise, signal) {
211
365
  if (signal.aborted)
@@ -221,10 +375,10 @@ function assertBytes(value, max, label) {
221
375
  throw new SupervisorLimitError(`${label} exceeds max bytes`);
222
376
  }
223
377
  function requireOwnership(ownership) {
224
- if (!ownership.tenantId?.trim()
225
- || (ownership.accountId !== undefined && !ownership.accountId.trim())
226
- || (ownership.userId !== undefined && !ownership.userId.trim())
227
- || (!ownership.accountId && !ownership.userId))
378
+ if (!ownership.tenantId?.trim() ||
379
+ (ownership.accountId !== undefined && !ownership.accountId.trim()) ||
380
+ (ownership.userId !== undefined && !ownership.userId.trim()) ||
381
+ (!ownership.accountId && !ownership.userId))
228
382
  throw new SupervisorValidationError("tenantId and non-empty accountId or userId are required");
229
383
  }
230
384
  function safeError(error, options) {
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.96",
3
+ "version": "0.1.1",
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.96"
28
+ "@arnilo/prism": "0.1.1"
29
29
  },
30
30
  "devDependencies": {
31
31
  "@arnilo/prism": "file:../.."