@alexeiled/pi-fusion 0.1.0

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.
@@ -0,0 +1,407 @@
1
+ import { randomUUID } from "node:crypto";
2
+
3
+ export const SUBAGENTS_RPC_VERSION = 1;
4
+ export const SUBAGENTS_RPC_REQUEST_CHANNEL = "subagents:rpc:v1:request";
5
+ export const SUBAGENTS_RPC_REPLY_CHANNEL_PREFIX = "subagents:rpc:v1:reply:";
6
+ export const DEFAULT_SUBAGENTS_RPC_TIMEOUT_MS = 15_000;
7
+
8
+ export const SUBAGENTS_RPC_METHODS = [
9
+ "ping",
10
+ "spawn",
11
+ "status",
12
+ "stop",
13
+ "interrupt",
14
+ ] as const;
15
+
16
+ export type SubagentsRpcMethod = (typeof SUBAGENTS_RPC_METHODS)[number];
17
+
18
+ export interface SubagentsEventBus {
19
+ on(event: string, handler: (payload: unknown) => void): (() => void) | void;
20
+ emit(event: string, payload: unknown): void;
21
+ }
22
+
23
+ export interface SubagentsRpcSource {
24
+ extension?: string;
25
+ }
26
+
27
+ export interface SubagentsRpcClientOptions {
28
+ events: SubagentsEventBus;
29
+ timeoutMs?: number;
30
+ requestId?: () => string;
31
+ source?: SubagentsRpcSource;
32
+ }
33
+
34
+ export interface SubagentsRpcRequestOptions {
35
+ timeoutMs?: number;
36
+ }
37
+
38
+ export interface SubagentsTargetParams {
39
+ id?: string;
40
+ runId?: string;
41
+ dir?: string;
42
+ index?: number;
43
+ }
44
+
45
+ export type SubagentsSpawnParams = object;
46
+
47
+ export interface SubagentsRpcRequestEnvelope {
48
+ version: typeof SUBAGENTS_RPC_VERSION;
49
+ requestId: string;
50
+ method: SubagentsRpcMethod;
51
+ params?: unknown;
52
+ source?: SubagentsRpcSource;
53
+ }
54
+
55
+ export type SubagentsRpcReplyEnvelope =
56
+ | {
57
+ version: typeof SUBAGENTS_RPC_VERSION;
58
+ requestId: string;
59
+ method?: SubagentsRpcMethod;
60
+ success: true;
61
+ data: unknown;
62
+ }
63
+ | {
64
+ version: typeof SUBAGENTS_RPC_VERSION;
65
+ requestId: string;
66
+ method?: SubagentsRpcMethod;
67
+ success: false;
68
+ error: {
69
+ code: string;
70
+ message: string;
71
+ };
72
+ };
73
+
74
+ export class SubagentsRpcRemoteError extends Error {
75
+ readonly code: string;
76
+ readonly requestId: string;
77
+ readonly method: SubagentsRpcMethod;
78
+
79
+ constructor(input: {
80
+ code: string;
81
+ message: string;
82
+ requestId: string;
83
+ method: SubagentsRpcMethod;
84
+ }) {
85
+ super(input.message);
86
+ this.name = "SubagentsRpcRemoteError";
87
+ this.code = input.code;
88
+ this.requestId = input.requestId;
89
+ this.method = input.method;
90
+ }
91
+ }
92
+
93
+ export class SubagentsRpcProtocolError extends Error {
94
+ readonly requestId: string;
95
+ readonly method: SubagentsRpcMethod;
96
+
97
+ constructor(input: {
98
+ message: string;
99
+ requestId: string;
100
+ method: SubagentsRpcMethod;
101
+ }) {
102
+ super(input.message);
103
+ this.name = "SubagentsRpcProtocolError";
104
+ this.requestId = input.requestId;
105
+ this.method = input.method;
106
+ }
107
+ }
108
+
109
+ export class SubagentsRpcTimeoutError extends Error {
110
+ readonly requestId: string;
111
+ readonly method: SubagentsRpcMethod;
112
+ readonly timeoutMs: number;
113
+
114
+ constructor(input: {
115
+ requestId: string;
116
+ method: SubagentsRpcMethod;
117
+ timeoutMs: number;
118
+ }) {
119
+ super(
120
+ `Subagents RPC ${input.method} request ${input.requestId} timed out after ${input.timeoutMs}ms.`,
121
+ );
122
+ this.name = "SubagentsRpcTimeoutError";
123
+ this.requestId = input.requestId;
124
+ this.method = input.method;
125
+ this.timeoutMs = input.timeoutMs;
126
+ }
127
+ }
128
+
129
+ export function subagentsRpcReplyChannel(requestId: string): string {
130
+ return `${SUBAGENTS_RPC_REPLY_CHANNEL_PREFIX}${requestId}`;
131
+ }
132
+
133
+ export class SubagentsRpcClient {
134
+ private readonly events: SubagentsEventBus;
135
+ private readonly timeoutMs: number;
136
+ private readonly createRequestId: () => string;
137
+ private readonly source: SubagentsRpcSource;
138
+
139
+ constructor(options: SubagentsRpcClientOptions) {
140
+ this.events = options.events;
141
+ this.timeoutMs = normalizeTimeoutMs(
142
+ options.timeoutMs ?? DEFAULT_SUBAGENTS_RPC_TIMEOUT_MS,
143
+ );
144
+ this.createRequestId = options.requestId ?? randomUUID;
145
+ this.source = options.source ?? { extension: "pi-fusion" };
146
+ }
147
+
148
+ request<T = unknown>(
149
+ method: SubagentsRpcMethod,
150
+ params?: unknown,
151
+ options: SubagentsRpcRequestOptions = {},
152
+ ): Promise<T> {
153
+ const requestId = this.createRequestId();
154
+ const timeoutMs = normalizeTimeoutMs(options.timeoutMs ?? this.timeoutMs);
155
+ const replyChannel = subagentsRpcReplyChannel(requestId);
156
+ const envelope = createRequestEnvelope({
157
+ requestId,
158
+ method,
159
+ params,
160
+ source: this.source,
161
+ });
162
+
163
+ return new Promise<T>((resolve, reject) => {
164
+ let settled = false;
165
+ let unsubscribe: (() => void) | undefined;
166
+
167
+ const timer = setTimeout(() => {
168
+ rejectOnce(
169
+ new SubagentsRpcTimeoutError({ requestId, method, timeoutMs }),
170
+ );
171
+ }, timeoutMs);
172
+
173
+ const cleanup = (): void => {
174
+ clearTimeout(timer);
175
+ if (unsubscribe) unsubscribe();
176
+ };
177
+
178
+ const resolveOnce = (value: T): void => {
179
+ if (settled) return;
180
+ settled = true;
181
+ cleanup();
182
+ resolve(value);
183
+ };
184
+
185
+ const rejectOnce = (error: Error): void => {
186
+ if (settled) return;
187
+ settled = true;
188
+ cleanup();
189
+ reject(error);
190
+ };
191
+
192
+ const maybeUnsubscribe = this.events.on(
193
+ replyChannel,
194
+ (payload: unknown) => {
195
+ if (!isRecord(payload) || payload.requestId !== requestId) return;
196
+
197
+ let reply: SubagentsRpcReplyEnvelope;
198
+ try {
199
+ reply = parseReplyEnvelope(payload, method, requestId);
200
+ } catch (error: unknown) {
201
+ rejectOnce(
202
+ error instanceof Error
203
+ ? error
204
+ : new SubagentsRpcProtocolError({
205
+ requestId,
206
+ method,
207
+ message: String(error),
208
+ }),
209
+ );
210
+ return;
211
+ }
212
+
213
+ if (reply.success) {
214
+ resolveOnce(reply.data as T);
215
+ return;
216
+ }
217
+
218
+ rejectOnce(
219
+ new SubagentsRpcRemoteError({
220
+ requestId,
221
+ method,
222
+ code: reply.error.code,
223
+ message: reply.error.message,
224
+ }),
225
+ );
226
+ },
227
+ );
228
+ if (typeof maybeUnsubscribe === "function")
229
+ unsubscribe = maybeUnsubscribe;
230
+
231
+ try {
232
+ this.events.emit(SUBAGENTS_RPC_REQUEST_CHANNEL, envelope);
233
+ } catch (error: unknown) {
234
+ rejectOnce(
235
+ error instanceof Error
236
+ ? error
237
+ : new SubagentsRpcProtocolError({
238
+ requestId,
239
+ method,
240
+ message: String(error),
241
+ }),
242
+ );
243
+ }
244
+ });
245
+ }
246
+
247
+ ping(options?: SubagentsRpcRequestOptions): Promise<unknown> {
248
+ return this.request("ping", undefined, options);
249
+ }
250
+
251
+ spawn(
252
+ params: SubagentsSpawnParams,
253
+ options?: SubagentsRpcRequestOptions,
254
+ ): Promise<unknown> {
255
+ return this.request("spawn", params, options);
256
+ }
257
+
258
+ status(
259
+ params: SubagentsTargetParams = {},
260
+ options?: SubagentsRpcRequestOptions,
261
+ ): Promise<unknown> {
262
+ return this.request("status", params, options);
263
+ }
264
+
265
+ stop(
266
+ params: SubagentsTargetParams,
267
+ options?: SubagentsRpcRequestOptions,
268
+ ): Promise<unknown> {
269
+ return this.request("stop", params, options);
270
+ }
271
+
272
+ interrupt(
273
+ params: SubagentsTargetParams,
274
+ options?: SubagentsRpcRequestOptions,
275
+ ): Promise<unknown> {
276
+ return this.request("interrupt", params, options);
277
+ }
278
+ }
279
+
280
+ function createRequestEnvelope(input: {
281
+ requestId: string;
282
+ method: SubagentsRpcMethod;
283
+ params?: unknown;
284
+ source: SubagentsRpcSource;
285
+ }): SubagentsRpcRequestEnvelope {
286
+ return {
287
+ version: SUBAGENTS_RPC_VERSION,
288
+ requestId: input.requestId,
289
+ method: input.method,
290
+ ...(input.params !== undefined ? { params: input.params } : {}),
291
+ ...(Object.keys(input.source).length > 0 ? { source: input.source } : {}),
292
+ };
293
+ }
294
+
295
+ function parseReplyEnvelope(
296
+ payload: Record<string, unknown>,
297
+ expectedMethod: SubagentsRpcMethod,
298
+ requestId: string,
299
+ ): SubagentsRpcReplyEnvelope {
300
+ if (payload.version !== SUBAGENTS_RPC_VERSION) {
301
+ throw new SubagentsRpcProtocolError({
302
+ requestId,
303
+ method: expectedMethod,
304
+ message: `Unsupported subagents RPC reply version: ${String(payload.version)}.`,
305
+ });
306
+ }
307
+
308
+ const method = parseOptionalMethod(payload.method, expectedMethod, requestId);
309
+ if (method && method !== expectedMethod) {
310
+ throw new SubagentsRpcProtocolError({
311
+ requestId,
312
+ method: expectedMethod,
313
+ message: `Subagents RPC reply method ${method} did not match ${expectedMethod}.`,
314
+ });
315
+ }
316
+
317
+ if (payload.success === true) {
318
+ return {
319
+ version: SUBAGENTS_RPC_VERSION,
320
+ requestId,
321
+ ...(method ? { method } : {}),
322
+ success: true,
323
+ data: payload.data,
324
+ };
325
+ }
326
+
327
+ if (payload.success === false) {
328
+ if (!isRecord(payload.error)) {
329
+ throw new SubagentsRpcProtocolError({
330
+ requestId,
331
+ method: expectedMethod,
332
+ message: "Subagents RPC failure reply did not include an error object.",
333
+ });
334
+ }
335
+ if (
336
+ typeof payload.error.code !== "string" ||
337
+ typeof payload.error.message !== "string"
338
+ ) {
339
+ throw new SubagentsRpcProtocolError({
340
+ requestId,
341
+ method: expectedMethod,
342
+ message:
343
+ "Subagents RPC failure reply error must include code and message.",
344
+ });
345
+ }
346
+ return {
347
+ version: SUBAGENTS_RPC_VERSION,
348
+ requestId,
349
+ ...(method ? { method } : {}),
350
+ success: false,
351
+ error: {
352
+ code: payload.error.code,
353
+ message: payload.error.message,
354
+ },
355
+ };
356
+ }
357
+
358
+ throw new SubagentsRpcProtocolError({
359
+ requestId,
360
+ method: expectedMethod,
361
+ message: "Subagents RPC reply success flag must be true or false.",
362
+ });
363
+ }
364
+
365
+ function parseOptionalMethod(
366
+ value: unknown,
367
+ expectedMethod: SubagentsRpcMethod,
368
+ requestId: string,
369
+ ): SubagentsRpcMethod | undefined {
370
+ if (value === undefined) return undefined;
371
+ if (
372
+ typeof value === "string" &&
373
+ (SUBAGENTS_RPC_METHODS as readonly string[]).includes(value)
374
+ ) {
375
+ return value as SubagentsRpcMethod;
376
+ }
377
+ throw new SubagentsRpcProtocolError({
378
+ requestId,
379
+ method: expectedMethod,
380
+ message: `Unsupported subagents RPC reply method: ${formatUnknown(value)}.`,
381
+ });
382
+ }
383
+
384
+ function formatUnknown(value: unknown): string {
385
+ if (typeof value === "string") return value;
386
+ if (typeof value === "number" || typeof value === "boolean") {
387
+ return String(value);
388
+ }
389
+ if (value === null) return "null";
390
+ if (value === undefined) return "undefined";
391
+ try {
392
+ return JSON.stringify(value) ?? "unknown";
393
+ } catch {
394
+ return "unknown";
395
+ }
396
+ }
397
+
398
+ function normalizeTimeoutMs(value: number): number {
399
+ if (!Number.isInteger(value) || value <= 0) {
400
+ throw new RangeError("Subagents RPC timeoutMs must be a positive integer.");
401
+ }
402
+ return value;
403
+ }
404
+
405
+ function isRecord(value: unknown): value is Record<string, unknown> {
406
+ return typeof value === "object" && value !== null && !Array.isArray(value);
407
+ }
package/src/types.ts ADDED
@@ -0,0 +1,54 @@
1
+ export const THINKING_LEVELS = [
2
+ "off",
3
+ "minimal",
4
+ "low",
5
+ "medium",
6
+ "high",
7
+ "xhigh",
8
+ ] as const;
9
+
10
+ export type ThinkingLevel = (typeof THINKING_LEVELS)[number];
11
+ export type FusionContextMode = "fresh" | "fork";
12
+
13
+ export interface PanelMemberConfig {
14
+ id: string;
15
+ label: string;
16
+ agent: string;
17
+ model?: string;
18
+ thinking?: ThinkingLevel;
19
+ role?: string;
20
+ }
21
+
22
+ export interface JudgeConfig {
23
+ agent: string;
24
+ model?: string;
25
+ thinking?: ThinkingLevel;
26
+ }
27
+
28
+ export interface FusionProfile {
29
+ panel: PanelMemberConfig[];
30
+ judge: JudgeConfig;
31
+ concurrency?: number;
32
+ timeoutMs?: number;
33
+ context?: FusionContextMode;
34
+ }
35
+
36
+ export interface FusionConfig {
37
+ defaultProfile: string;
38
+ profiles: Record<string, FusionProfile>;
39
+ }
40
+
41
+ export type FusionPhase = "panel" | "judge" | "done" | "failed" | "cancelled";
42
+
43
+ export interface FusionRun {
44
+ id: string;
45
+ prompt: string;
46
+ profileName: string;
47
+ phase: FusionPhase;
48
+ createdAt: number;
49
+ updatedAt: number;
50
+ panelRunId?: string;
51
+ judgeRunId?: string;
52
+ report?: string;
53
+ error?: string;
54
+ }
package/src/utils.ts ADDED
File without changes
package/tsconfig.json ADDED
@@ -0,0 +1,19 @@
1
+ {
2
+ "compilerOptions": {
3
+ "target": "ES2023",
4
+ "module": "NodeNext",
5
+ "moduleResolution": "NodeNext",
6
+ "strict": true,
7
+ "noUncheckedIndexedAccess": true,
8
+ "exactOptionalPropertyTypes": true,
9
+ "useUnknownInCatchVariables": true,
10
+ "noImplicitOverride": true,
11
+ "noImplicitReturns": true,
12
+ "noFallthroughCasesInSwitch": true,
13
+ "skipLibCheck": true
14
+ },
15
+ "include": [
16
+ "src/**/*.ts",
17
+ "test/**/*.ts"
18
+ ]
19
+ }