@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 +10 -0
- package/README.md +1 -1
- package/dist/supervisor.js +121 -1
- package/dist/types.d.ts +15 -1
- package/package.json +2 -2
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).
|
package/dist/supervisor.js
CHANGED
|
@@ -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.
|
|
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.
|
|
28
|
+
"@arnilo/prism": "0.0.25"
|
|
29
29
|
},
|
|
30
30
|
"devDependencies": {
|
|
31
31
|
"@arnilo/prism": "file:../.."
|