@arnilo/prism-supervisor 0.0.5
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 +13 -0
- package/LICENSE +9 -0
- package/README.md +31 -0
- package/dist/a2a-card.d.ts +17 -0
- package/dist/a2a-card.js +129 -0
- package/dist/a2a-client.d.ts +2 -0
- package/dist/a2a-client.js +312 -0
- package/dist/a2a-server.d.ts +2 -0
- package/dist/a2a-server.js +230 -0
- package/dist/a2a-types.d.ts +135 -0
- package/dist/a2a-types.js +2 -0
- package/dist/errors.d.ts +17 -0
- package/dist/errors.js +26 -0
- package/dist/index.d.ts +9 -0
- package/dist/index.js +8 -0
- package/dist/limits.d.ts +38 -0
- package/dist/limits.js +42 -0
- package/dist/supervisor.d.ts +2 -0
- package/dist/supervisor.js +218 -0
- package/dist/types.d.ts +99 -0
- package/dist/types.js +2 -0
- package/package.json +57 -0
|
@@ -0,0 +1,218 @@
|
|
|
1
|
+
import { AgentRunError, createAgent, createEventMultiplexer, } from "@arnilo/prism";
|
|
2
|
+
import { SupervisorDeniedError, SupervisorError, SupervisorLimitError, SupervisorValidationError } from "./errors.js";
|
|
3
|
+
import { narrowSupervisorLimits, resolveSupervisorLimits } from "./limits.js";
|
|
4
|
+
const ID = /^[A-Za-z0-9][A-Za-z0-9._:-]{0,127}$/;
|
|
5
|
+
export function createSupervisor(options) {
|
|
6
|
+
requireOwnership(options.ownership);
|
|
7
|
+
const id = options.id ?? "supervisor";
|
|
8
|
+
if (!ID.test(id))
|
|
9
|
+
throw new SupervisorValidationError("Supervisor id is invalid");
|
|
10
|
+
const children = Object.entries(options.children);
|
|
11
|
+
if (children.length === 0)
|
|
12
|
+
throw new SupervisorValidationError("At least one child is required");
|
|
13
|
+
for (const [childId] of children)
|
|
14
|
+
if (!ID.test(childId))
|
|
15
|
+
throw new SupervisorValidationError(`Invalid child id: ${childId}`);
|
|
16
|
+
const baseLimits = resolveSupervisorLimits(options.limits);
|
|
17
|
+
const events = createEventMultiplexer({ maxQueuedEvents: baseLimits.maxQueuedEvents, overflow: "drop_oldest" });
|
|
18
|
+
let activeChildren = 0;
|
|
19
|
+
let sequence = 0;
|
|
20
|
+
async function delegate(request, chain = { path: [] }) {
|
|
21
|
+
const child = options.children[request.childId];
|
|
22
|
+
if (!child)
|
|
23
|
+
throw new SupervisorDeniedError("Child is not allow-listed");
|
|
24
|
+
if (chain.path.includes(request.childId))
|
|
25
|
+
throw new SupervisorLimitError("Delegation cycle detected");
|
|
26
|
+
const depth = chain.path.length + 1;
|
|
27
|
+
let limits = narrowSupervisorLimits(narrowSupervisorLimits(baseLimits, child.limits), request.limits);
|
|
28
|
+
if (depth > limits.maxDepth)
|
|
29
|
+
throw new SupervisorLimitError("Delegation depth exceeded");
|
|
30
|
+
let input = options.redactor?.redact(request.input) ?? request.input;
|
|
31
|
+
assertBytes(input, limits.maxMessageBytes, "Delegation input");
|
|
32
|
+
if (activeChildren >= limits.maxActiveChildren)
|
|
33
|
+
throw new SupervisorLimitError("Active child limit exceeded");
|
|
34
|
+
activeChildren += 1;
|
|
35
|
+
const delegationId = `${id}-${++sequence}`;
|
|
36
|
+
const path = Object.freeze([...chain.path, request.childId]);
|
|
37
|
+
const controller = new AbortController();
|
|
38
|
+
const disposeSignals = linkSignals(controller, request.signal, chain.signal);
|
|
39
|
+
let timer = setTimeout(() => controller.abort(new SupervisorLimitError("Delegation timeout exceeded")), limits.timeoutMs);
|
|
40
|
+
let completionSent = false;
|
|
41
|
+
try {
|
|
42
|
+
let hookPermission;
|
|
43
|
+
if (options.hooks?.before) {
|
|
44
|
+
const decision = await abortable(Promise.resolve(options.hooks.before(Object.freeze({
|
|
45
|
+
childId: request.childId,
|
|
46
|
+
delegationId,
|
|
47
|
+
depth,
|
|
48
|
+
path,
|
|
49
|
+
input,
|
|
50
|
+
limits,
|
|
51
|
+
metadata: options.redactor?.redact(request.metadata) ?? request.metadata,
|
|
52
|
+
signal: controller.signal,
|
|
53
|
+
}))), controller.signal);
|
|
54
|
+
if (decision.allowed === false) {
|
|
55
|
+
const reason = safeError(decision.reason ?? "Delegation denied", options);
|
|
56
|
+
events.publish({ type: "delegation_rejected", childId: request.childId, delegationId, depth, reason });
|
|
57
|
+
await complete({ childId: request.childId, delegationId, depth, status: "rejected", text: "", error: reason });
|
|
58
|
+
completionSent = true;
|
|
59
|
+
throw new SupervisorDeniedError(reason);
|
|
60
|
+
}
|
|
61
|
+
limits = narrowSupervisorLimits(limits, decision.limits);
|
|
62
|
+
if (depth > limits.maxDepth)
|
|
63
|
+
throw new SupervisorLimitError("Delegation depth exceeded");
|
|
64
|
+
if (activeChildren > limits.maxActiveChildren)
|
|
65
|
+
throw new SupervisorLimitError("Active child limit exceeded");
|
|
66
|
+
clearTimeout(timer);
|
|
67
|
+
timer = setTimeout(() => controller.abort(new SupervisorLimitError("Delegation timeout exceeded")), limits.timeoutMs);
|
|
68
|
+
hookPermission = decision.permission;
|
|
69
|
+
if (decision.input !== undefined)
|
|
70
|
+
input = options.redactor?.redact(decision.input) ?? decision.input;
|
|
71
|
+
assertBytes(input, limits.maxMessageBytes, "Delegation input");
|
|
72
|
+
}
|
|
73
|
+
const resourceId = `${id}/${delegationId}/${request.childId}`;
|
|
74
|
+
const threadId = `${resourceId}/${encodeURIComponent(request.threadId ?? "default")}`;
|
|
75
|
+
const preliminaryPermission = intersectPolicies(options.permission, child.permission, hookPermission, toolBudgetPolicy(limits.maxToolCalls));
|
|
76
|
+
events.publish({ type: "delegation_started", childId: request.childId, delegationId, depth, resourceId, threadId });
|
|
77
|
+
const childAgent = await abortable(Promise.resolve(child.createAgent(Object.freeze({
|
|
78
|
+
childId: request.childId,
|
|
79
|
+
delegationId,
|
|
80
|
+
depth,
|
|
81
|
+
path,
|
|
82
|
+
ownership: options.ownership,
|
|
83
|
+
resourceId,
|
|
84
|
+
threadId,
|
|
85
|
+
permission: preliminaryPermission,
|
|
86
|
+
signal: controller.signal,
|
|
87
|
+
delegate: (nested) => delegate(nested, { path, signal: controller.signal }),
|
|
88
|
+
}))), controller.signal);
|
|
89
|
+
const agent = createAgent({
|
|
90
|
+
...childAgent.config,
|
|
91
|
+
permission: intersectPolicies(preliminaryPermission, childAgent.config.permission),
|
|
92
|
+
ownership: options.ownership,
|
|
93
|
+
redactor: options.redactor ?? childAgent.config.redactor,
|
|
94
|
+
});
|
|
95
|
+
const session = agent.createSession({ id: `${delegationId}-session`, metadata: { supervisorId: id, delegationId, resourceId, threadId } });
|
|
96
|
+
const result = await abortable(session.run(input, {
|
|
97
|
+
signal: controller.signal,
|
|
98
|
+
maxToolRounds: limits.maxSteps,
|
|
99
|
+
ownership: options.ownership,
|
|
100
|
+
redactor: options.redactor,
|
|
101
|
+
metadata: { ...request.metadata, supervisorId: id, delegationId, resourceId, threadId, depth },
|
|
102
|
+
}), controller.signal);
|
|
103
|
+
const totalTokens = result.usage?.totalTokens ?? ((result.usage?.inputTokens ?? 0) + (result.usage?.outputTokens ?? 0));
|
|
104
|
+
if (totalTokens > limits.maxTokens)
|
|
105
|
+
throw new SupervisorLimitError("Delegation token limit exceeded");
|
|
106
|
+
events.publish({ type: "delegation_finished", childId: request.childId, delegationId, depth, status: result.status, totalTokens });
|
|
107
|
+
await complete(toCompletion(result, request.childId, delegationId, depth, options));
|
|
108
|
+
completionSent = true;
|
|
109
|
+
return result;
|
|
110
|
+
}
|
|
111
|
+
catch (error) {
|
|
112
|
+
if (!(error instanceof SupervisorDeniedError && completionSent)) {
|
|
113
|
+
const result = error instanceof AgentRunError ? error.result : undefined;
|
|
114
|
+
const message = safeError(error, options);
|
|
115
|
+
events.publish({ type: "delegation_error", childId: request.childId, delegationId, depth, error: message });
|
|
116
|
+
await complete(result ? toCompletion(result, request.childId, delegationId, depth, options) : {
|
|
117
|
+
childId: request.childId,
|
|
118
|
+
delegationId,
|
|
119
|
+
depth,
|
|
120
|
+
status: controller.signal.aborted ? "aborted" : "rejected",
|
|
121
|
+
text: "",
|
|
122
|
+
error: message,
|
|
123
|
+
});
|
|
124
|
+
}
|
|
125
|
+
if (error instanceof AgentRunError || error instanceof SupervisorError)
|
|
126
|
+
throw error;
|
|
127
|
+
throw new SupervisorError(safeError(error, options));
|
|
128
|
+
}
|
|
129
|
+
finally {
|
|
130
|
+
clearTimeout(timer);
|
|
131
|
+
disposeSignals();
|
|
132
|
+
activeChildren -= 1;
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
async function complete(value) {
|
|
136
|
+
if (!options.hooks?.after)
|
|
137
|
+
return;
|
|
138
|
+
try {
|
|
139
|
+
await options.hooks.after(Object.freeze(value));
|
|
140
|
+
}
|
|
141
|
+
catch (error) {
|
|
142
|
+
events.publish({ type: "delegation_error", childId: value.childId, delegationId: value.delegationId, depth: value.depth, error: safeError(error, options) });
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
return { delegate: (request) => delegate(request), subscribe: () => events.subscribe(), get activeChildren() { return activeChildren; } };
|
|
146
|
+
}
|
|
147
|
+
function toCompletion(result, childId, delegationId, depth, options) {
|
|
148
|
+
return Object.freeze({
|
|
149
|
+
childId,
|
|
150
|
+
delegationId,
|
|
151
|
+
depth,
|
|
152
|
+
status: result.status,
|
|
153
|
+
text: options.redactor?.redact(result.text) ?? result.text,
|
|
154
|
+
usage: result.usage,
|
|
155
|
+
error: result.error ? safeError(result.error.message, options) : undefined,
|
|
156
|
+
});
|
|
157
|
+
}
|
|
158
|
+
function intersectPolicies(...policies) {
|
|
159
|
+
const active = policies.filter((policy) => policy !== undefined);
|
|
160
|
+
return { async check(request) {
|
|
161
|
+
for (const policy of active) {
|
|
162
|
+
const decision = await policy.check(request);
|
|
163
|
+
if (!decision.allowed)
|
|
164
|
+
return decision;
|
|
165
|
+
}
|
|
166
|
+
return { allowed: true };
|
|
167
|
+
} };
|
|
168
|
+
}
|
|
169
|
+
function toolBudgetPolicy(max) {
|
|
170
|
+
let count = 0;
|
|
171
|
+
return { check(request) {
|
|
172
|
+
if (request.kind !== "tool" || request.action !== "execute")
|
|
173
|
+
return { allowed: true };
|
|
174
|
+
count += 1;
|
|
175
|
+
return count <= max ? { allowed: true } : { allowed: false, reason: "Delegation tool-call limit exceeded" };
|
|
176
|
+
} };
|
|
177
|
+
}
|
|
178
|
+
function linkSignals(controller, ...signals) {
|
|
179
|
+
const removers = [];
|
|
180
|
+
for (const signal of signals) {
|
|
181
|
+
if (!signal)
|
|
182
|
+
continue;
|
|
183
|
+
if (signal.aborted)
|
|
184
|
+
controller.abort(signal.reason);
|
|
185
|
+
else {
|
|
186
|
+
const abort = () => controller.abort(signal.reason);
|
|
187
|
+
signal.addEventListener("abort", abort, { once: true });
|
|
188
|
+
removers.push(() => signal.removeEventListener("abort", abort));
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
return () => { for (const remove of removers)
|
|
192
|
+
remove(); };
|
|
193
|
+
}
|
|
194
|
+
function abortable(promise, signal) {
|
|
195
|
+
if (signal.aborted)
|
|
196
|
+
return Promise.reject(signal.reason);
|
|
197
|
+
return new Promise((resolve, reject) => {
|
|
198
|
+
const abort = () => reject(signal.reason);
|
|
199
|
+
signal.addEventListener("abort", abort, { once: true });
|
|
200
|
+
promise.then(resolve, reject).finally(() => signal.removeEventListener("abort", abort));
|
|
201
|
+
});
|
|
202
|
+
}
|
|
203
|
+
function assertBytes(value, max, label) {
|
|
204
|
+
if (new TextEncoder().encode(value).byteLength > max)
|
|
205
|
+
throw new SupervisorLimitError(`${label} exceeds max bytes`);
|
|
206
|
+
}
|
|
207
|
+
function requireOwnership(ownership) {
|
|
208
|
+
if (!ownership.tenantId?.trim()
|
|
209
|
+
|| (ownership.accountId !== undefined && !ownership.accountId.trim())
|
|
210
|
+
|| (ownership.userId !== undefined && !ownership.userId.trim())
|
|
211
|
+
|| (!ownership.accountId && !ownership.userId))
|
|
212
|
+
throw new SupervisorValidationError("tenantId and non-empty accountId or userId are required");
|
|
213
|
+
}
|
|
214
|
+
function safeError(error, options) {
|
|
215
|
+
const message = error instanceof Error ? error.message : typeof error === "string" ? error : "Delegation failed";
|
|
216
|
+
return options.redactor?.redact(message) ?? message;
|
|
217
|
+
}
|
|
218
|
+
//# sourceMappingURL=supervisor.js.map
|
package/dist/types.d.ts
ADDED
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
import type { Agent, AgentRunResult, OwnershipScope, PermissionPolicy, SecretRedactor } from "@arnilo/prism";
|
|
2
|
+
import type { ResolvedSupervisorLimits, SupervisorLimits } from "./limits.js";
|
|
3
|
+
export interface DelegationRequest {
|
|
4
|
+
readonly childId: string;
|
|
5
|
+
readonly input: string;
|
|
6
|
+
readonly threadId?: string;
|
|
7
|
+
readonly metadata?: Readonly<Record<string, unknown>>;
|
|
8
|
+
readonly limits?: SupervisorLimits;
|
|
9
|
+
readonly signal?: AbortSignal;
|
|
10
|
+
}
|
|
11
|
+
export interface DelegationChildContext {
|
|
12
|
+
readonly childId: string;
|
|
13
|
+
readonly delegationId: string;
|
|
14
|
+
readonly depth: number;
|
|
15
|
+
readonly path: readonly string[];
|
|
16
|
+
readonly ownership: OwnershipScope;
|
|
17
|
+
readonly resourceId: string;
|
|
18
|
+
readonly threadId: string;
|
|
19
|
+
readonly permission: PermissionPolicy;
|
|
20
|
+
readonly signal: AbortSignal;
|
|
21
|
+
delegate(request: DelegationRequest): Promise<AgentRunResult>;
|
|
22
|
+
}
|
|
23
|
+
export interface SupervisorChild {
|
|
24
|
+
readonly description?: string;
|
|
25
|
+
readonly permission?: PermissionPolicy;
|
|
26
|
+
readonly limits?: SupervisorLimits;
|
|
27
|
+
createAgent(context: DelegationChildContext): Agent | Promise<Agent>;
|
|
28
|
+
}
|
|
29
|
+
export interface DelegationHookDecision {
|
|
30
|
+
readonly allowed?: boolean;
|
|
31
|
+
readonly reason?: string;
|
|
32
|
+
readonly input?: string;
|
|
33
|
+
readonly limits?: SupervisorLimits;
|
|
34
|
+
readonly permission?: PermissionPolicy;
|
|
35
|
+
}
|
|
36
|
+
export interface DelegationHookInput {
|
|
37
|
+
readonly childId: string;
|
|
38
|
+
readonly delegationId: string;
|
|
39
|
+
readonly depth: number;
|
|
40
|
+
readonly path: readonly string[];
|
|
41
|
+
readonly input: string;
|
|
42
|
+
readonly limits: ResolvedSupervisorLimits;
|
|
43
|
+
readonly metadata?: Readonly<Record<string, unknown>>;
|
|
44
|
+
readonly signal: AbortSignal;
|
|
45
|
+
}
|
|
46
|
+
export interface DelegationCompletion {
|
|
47
|
+
readonly childId: string;
|
|
48
|
+
readonly delegationId: string;
|
|
49
|
+
readonly depth: number;
|
|
50
|
+
readonly status: AgentRunResult["status"] | "rejected";
|
|
51
|
+
readonly text: string;
|
|
52
|
+
readonly usage?: AgentRunResult["usage"];
|
|
53
|
+
readonly error?: string;
|
|
54
|
+
}
|
|
55
|
+
export interface SupervisorHooks {
|
|
56
|
+
before?(input: DelegationHookInput): DelegationHookDecision | Promise<DelegationHookDecision>;
|
|
57
|
+
after?(completion: DelegationCompletion): void | Promise<void>;
|
|
58
|
+
}
|
|
59
|
+
export type SupervisorEvent = {
|
|
60
|
+
readonly type: "delegation_started";
|
|
61
|
+
readonly childId: string;
|
|
62
|
+
readonly delegationId: string;
|
|
63
|
+
readonly depth: number;
|
|
64
|
+
readonly resourceId: string;
|
|
65
|
+
readonly threadId: string;
|
|
66
|
+
} | {
|
|
67
|
+
readonly type: "delegation_finished";
|
|
68
|
+
readonly childId: string;
|
|
69
|
+
readonly delegationId: string;
|
|
70
|
+
readonly depth: number;
|
|
71
|
+
readonly status: AgentRunResult["status"];
|
|
72
|
+
readonly totalTokens: number;
|
|
73
|
+
} | {
|
|
74
|
+
readonly type: "delegation_rejected";
|
|
75
|
+
readonly childId: string;
|
|
76
|
+
readonly delegationId: string;
|
|
77
|
+
readonly depth: number;
|
|
78
|
+
readonly reason: string;
|
|
79
|
+
} | {
|
|
80
|
+
readonly type: "delegation_error";
|
|
81
|
+
readonly childId: string;
|
|
82
|
+
readonly delegationId: string;
|
|
83
|
+
readonly depth: number;
|
|
84
|
+
readonly error: string;
|
|
85
|
+
};
|
|
86
|
+
export interface CreateSupervisorOptions {
|
|
87
|
+
readonly id?: string;
|
|
88
|
+
readonly ownership: OwnershipScope;
|
|
89
|
+
readonly children: Readonly<Record<string, SupervisorChild>>;
|
|
90
|
+
readonly permission?: PermissionPolicy;
|
|
91
|
+
readonly limits?: SupervisorLimits;
|
|
92
|
+
readonly hooks?: SupervisorHooks;
|
|
93
|
+
readonly redactor?: SecretRedactor;
|
|
94
|
+
}
|
|
95
|
+
export interface Supervisor {
|
|
96
|
+
delegate(request: DelegationRequest): Promise<AgentRunResult>;
|
|
97
|
+
subscribe(): AsyncIterable<SupervisorEvent>;
|
|
98
|
+
readonly activeChildren: number;
|
|
99
|
+
}
|
package/dist/types.js
ADDED
package/package.json
ADDED
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@arnilo/prism-supervisor",
|
|
3
|
+
"version": "0.0.5",
|
|
4
|
+
"description": "Optional bounded local supervisor delegation and A2A 1.0 interoperability.",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "./dist/index.js",
|
|
7
|
+
"types": "./dist/index.d.ts",
|
|
8
|
+
"exports": {
|
|
9
|
+
".": {
|
|
10
|
+
"types": "./dist/index.d.ts",
|
|
11
|
+
"default": "./dist/index.js"
|
|
12
|
+
}
|
|
13
|
+
},
|
|
14
|
+
"files": [
|
|
15
|
+
"dist",
|
|
16
|
+
"!dist/__tests__",
|
|
17
|
+
"!dist/**/*.map",
|
|
18
|
+
"README.md",
|
|
19
|
+
"CHANGELOG.md"
|
|
20
|
+
],
|
|
21
|
+
"scripts": {
|
|
22
|
+
"build": "tsc -p tsconfig.json",
|
|
23
|
+
"typecheck": "tsc -p tsconfig.json --noEmit",
|
|
24
|
+
"test": "node --test dist/__tests__/*.test.js",
|
|
25
|
+
"pack:dry-run": "npm pack --dry-run"
|
|
26
|
+
},
|
|
27
|
+
"peerDependencies": {
|
|
28
|
+
"@arnilo/prism": "0.0.5"
|
|
29
|
+
},
|
|
30
|
+
"devDependencies": {
|
|
31
|
+
"@arnilo/prism": "file:../.."
|
|
32
|
+
},
|
|
33
|
+
"engines": {
|
|
34
|
+
"node": ">=20"
|
|
35
|
+
},
|
|
36
|
+
"license": "MIT",
|
|
37
|
+
"repository": {
|
|
38
|
+
"type": "git",
|
|
39
|
+
"url": "git+https://github.com/ashiqrniloy/prism.git",
|
|
40
|
+
"directory": "packages/supervisor"
|
|
41
|
+
},
|
|
42
|
+
"bugs": {
|
|
43
|
+
"url": "https://github.com/ashiqrniloy/prism/issues"
|
|
44
|
+
},
|
|
45
|
+
"homepage": "https://github.com/ashiqrniloy/prism/tree/main/packages/supervisor#readme",
|
|
46
|
+
"keywords": [
|
|
47
|
+
"prism",
|
|
48
|
+
"supervisor",
|
|
49
|
+
"delegation",
|
|
50
|
+
"a2a",
|
|
51
|
+
"agent"
|
|
52
|
+
],
|
|
53
|
+
"sideEffects": false,
|
|
54
|
+
"publishConfig": {
|
|
55
|
+
"access": "public"
|
|
56
|
+
}
|
|
57
|
+
}
|