@narumitw/pi-subagents 3.0.1 → 3.0.2

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/package.json CHANGED
@@ -1,56 +1,55 @@
1
1
  {
2
- "name": "@narumitw/pi-subagents",
3
- "version": "3.0.1",
4
- "description": "Subagent jobs with asynchronous main-agent messaging for Pi.",
5
- "type": "module",
6
- "license": "MIT",
7
- "private": false,
8
- "keywords": [
9
- "pi-package",
10
- "pi-extension",
11
- "pi",
12
- "subagents",
13
- "agents",
14
- "delegation"
15
- ],
16
- "files": [
17
- "src",
18
- "dist",
19
- "docs",
20
- "README.md",
21
- "LICENSE"
22
- ],
23
- "pi": {
24
- "extensions": [
25
- "./dist/index.ts"
26
- ]
27
- },
28
- "scripts": {
29
- "build": "node scripts/build-runtime.mjs",
30
- "check": "npm run build && biome check . && npm run typecheck",
31
- "format": "biome check --write .",
32
- "typecheck": "tsc --noEmit",
33
- "prepack": "npm run build"
34
- },
35
- "peerDependencies": {
36
- "@earendil-works/pi-ai": "*",
37
- "@earendil-works/pi-coding-agent": "*",
38
- "@earendil-works/pi-tui": "*",
39
- "typebox": "*"
40
- },
41
- "devDependencies": {
42
- "@biomejs/biome": "2.5.11",
43
- "@earendil-works/pi-ai": "0.84.4",
44
- "@earendil-works/pi-coding-agent": "0.84.4",
45
- "@earendil-works/pi-tui": "0.84.4",
46
- "@types/node": "26.4.0",
47
- "esbuild": "0.28.2",
48
- "typebox": "1.3.20",
49
- "typescript": "7.0.2"
50
- },
51
- "repository": {
52
- "type": "git",
53
- "url": "https://github.com/narumiruna/pi-extensions",
54
- "directory": "packages/pi-subagents"
55
- }
2
+ "name": "@narumitw/pi-subagents",
3
+ "version": "3.0.2",
4
+ "description": "Subagent jobs with asynchronous main-agent messaging for Pi.",
5
+ "type": "module",
6
+ "license": "MIT",
7
+ "private": false,
8
+ "keywords": [
9
+ "pi-package",
10
+ "pi-extension",
11
+ "pi",
12
+ "subagents",
13
+ "agents",
14
+ "delegation"
15
+ ],
16
+ "files": [
17
+ "src",
18
+ "dist",
19
+ "docs",
20
+ "README.md",
21
+ "LICENSE"
22
+ ],
23
+ "pi": {
24
+ "extensions": [
25
+ "./dist/index.ts"
26
+ ]
27
+ },
28
+ "scripts": {
29
+ "build": "node scripts/build-runtime.mjs",
30
+ "check": "npm run build && biome check . && npm run typecheck",
31
+ "format": "biome check --write .",
32
+ "typecheck": "tsc --noEmit",
33
+ "prepack": "npm run build"
34
+ },
35
+ "peerDependencies": {
36
+ "@earendil-works/pi-ai": "*",
37
+ "@earendil-works/pi-coding-agent": "*",
38
+ "@earendil-works/pi-tui": "*",
39
+ "typebox": "*"
40
+ },
41
+ "devDependencies": {
42
+ "@earendil-works/pi-ai": "0.86.0",
43
+ "@earendil-works/pi-coding-agent": "0.86.0",
44
+ "@earendil-works/pi-tui": "0.86.0",
45
+ "@types/node": "26.6.2",
46
+ "esbuild": "0.28.2",
47
+ "typebox": "1.3.34",
48
+ "typescript": "7.0.2"
49
+ },
50
+ "repository": {
51
+ "type": "git",
52
+ "url": "https://github.com/narumiruna/pi-extensions",
53
+ "directory": "packages/pi-subagents"
54
+ }
56
55
  }
@@ -7,65 +7,65 @@ export const BROKER_CREDENTIAL_FD_ENV = "PI_SUBAGENT_BROKER_FD";
7
7
  const MAX_CREDENTIAL_BYTES = 1_024;
8
8
 
9
9
  export function brokerCredentialEnvironment(): NodeJS.ProcessEnv {
10
- return { [BROKER_CREDENTIAL_FD_ENV]: String(BROKER_CREDENTIAL_FD) };
10
+ return { [BROKER_CREDENTIAL_FD_ENV]: String(BROKER_CREDENTIAL_FD) };
11
11
  }
12
12
 
13
13
  export function serializeBrokerCredentials(credentials: BrokerCredentials): string {
14
- return JSON.stringify(credentials);
14
+ return JSON.stringify(credentials);
15
15
  }
16
16
 
17
17
  export function captureBrokerCredentials(
18
- readCredentials: () => string = readCredentialPipe,
18
+ readCredentials: () => string = readCredentialPipe,
19
19
  ): BrokerCredentials | undefined {
20
- const descriptor = process.env[BROKER_CREDENTIAL_FD_ENV];
21
- delete process.env[BROKER_CREDENTIAL_FD_ENV];
22
- if (descriptor === undefined) return undefined;
23
- if (descriptor !== String(BROKER_CREDENTIAL_FD)) {
24
- throw new Error("Invalid pi-subagents broker credential descriptor.");
25
- }
26
- let serialized: string;
27
- try {
28
- serialized = readCredentials();
29
- } catch {
30
- throw new Error("Unable to read pi-subagents broker credentials.");
31
- }
32
- if (Buffer.byteLength(serialized, "utf8") > MAX_CREDENTIAL_BYTES) {
33
- throw new Error("Invalid pi-subagents broker credentials.");
34
- }
35
- let value: unknown;
36
- try {
37
- value = JSON.parse(serialized);
38
- } catch {
39
- throw new Error("Invalid pi-subagents broker credentials.");
40
- }
41
- if (!isBrokerCredentials(value)) {
42
- throw new Error("Invalid pi-subagents broker credentials.");
43
- }
44
- return value;
20
+ const descriptor = process.env[BROKER_CREDENTIAL_FD_ENV];
21
+ delete process.env[BROKER_CREDENTIAL_FD_ENV];
22
+ if (descriptor === undefined) return undefined;
23
+ if (descriptor !== String(BROKER_CREDENTIAL_FD)) {
24
+ throw new Error("Invalid pi-subagents broker credential descriptor.");
25
+ }
26
+ let serialized: string;
27
+ try {
28
+ serialized = readCredentials();
29
+ } catch {
30
+ throw new Error("Unable to read pi-subagents broker credentials.");
31
+ }
32
+ if (Buffer.byteLength(serialized, "utf8") > MAX_CREDENTIAL_BYTES) {
33
+ throw new Error("Invalid pi-subagents broker credentials.");
34
+ }
35
+ let value: unknown;
36
+ try {
37
+ value = JSON.parse(serialized);
38
+ } catch {
39
+ throw new Error("Invalid pi-subagents broker credentials.");
40
+ }
41
+ if (!isBrokerCredentials(value)) {
42
+ throw new Error("Invalid pi-subagents broker credentials.");
43
+ }
44
+ return value;
45
45
  }
46
46
 
47
47
  function readCredentialPipe(): string {
48
- try {
49
- return fs.readFileSync(BROKER_CREDENTIAL_FD, "utf8");
50
- } finally {
51
- try {
52
- fs.closeSync(BROKER_CREDENTIAL_FD);
53
- } catch {
54
- // The descriptor may already be closed after a failed read.
55
- }
56
- }
48
+ try {
49
+ return fs.readFileSync(BROKER_CREDENTIAL_FD, "utf8");
50
+ } finally {
51
+ try {
52
+ fs.closeSync(BROKER_CREDENTIAL_FD);
53
+ } catch {
54
+ // The descriptor may already be closed after a failed read.
55
+ }
56
+ }
57
57
  }
58
58
 
59
59
  function isBrokerCredentials(value: unknown): value is BrokerCredentials {
60
- if (!value || typeof value !== "object" || Array.isArray(value)) return false;
61
- const candidate = value as Record<string, unknown>;
62
- return (
63
- Object.keys(candidate).length === 3 &&
64
- candidate.host === "127.0.0.1" &&
65
- Number.isSafeInteger(candidate.port) &&
66
- (candidate.port as number) >= 1 &&
67
- (candidate.port as number) <= 65_535 &&
68
- typeof candidate.token === "string" &&
69
- /^[a-f0-9]{64}$/u.test(candidate.token)
70
- );
60
+ if (!value || typeof value !== "object" || Array.isArray(value)) return false;
61
+ const candidate = value as Record<string, unknown>;
62
+ return (
63
+ Object.keys(candidate).length === 3 &&
64
+ candidate.host === "127.0.0.1" &&
65
+ Number.isSafeInteger(candidate.port) &&
66
+ (candidate.port as number) >= 1 &&
67
+ (candidate.port as number) <= 65_535 &&
68
+ typeof candidate.token === "string" &&
69
+ /^[a-f0-9]{64}$/u.test(candidate.token)
70
+ );
71
71
  }
@@ -1,10 +1,7 @@
1
1
  import net from "node:net";
2
2
  import type { ExtensionFactory } from "@earendil-works/pi-coding-agent";
3
3
  import { captureBrokerCredentials } from "./broker-credentials.js";
4
- import {
5
- type ChildCommunicationClient,
6
- createChildCommunicationExtension,
7
- } from "./child-communication-tools.js";
4
+ import { type ChildCommunicationClient, createChildCommunicationExtension } from "./child-communication-tools.js";
8
5
  import { MAX_FRAME_BYTES, MAX_IDENTIFIER_LENGTH } from "./message-broker.js";
9
6
  import type { BrokerCredentials } from "./types.js";
10
7
 
@@ -14,140 +11,135 @@ const SEND_RESPONSE_TIMEOUT_MS = 5_000;
14
11
  const captured = captureBrokerCredentials();
15
12
 
16
13
  const childCommunicationBridge: ExtensionFactory = captured
17
- ? createChildCommunicationExtension(createBrokerClient(captured))
18
- : () => undefined;
14
+ ? createChildCommunicationExtension(createBrokerClient(captured))
15
+ : () => undefined;
19
16
 
20
17
  export default childCommunicationBridge;
21
18
 
22
19
  export function createBrokerClient(credentials: BrokerCredentials): ChildCommunicationClient {
23
- return {
24
- async send(params, signal) {
25
- const response = await requestBroker(
26
- credentials,
27
- { type: "send", token: credentials.token, ...params },
28
- signal,
29
- SEND_RESPONSE_TIMEOUT_MS,
30
- );
31
- if (response.ok !== true) throw brokerError(response);
32
- if (
33
- typeof response.requestId !== "string" ||
34
- !response.requestId ||
35
- response.requestId.length > MAX_IDENTIFIER_LENGTH ||
36
- typeof response.accepted !== "boolean" ||
37
- typeof response.duplicate !== "boolean"
38
- ) {
39
- throw new Error("Subagent broker returned an invalid send acknowledgement.");
40
- }
41
- return {
42
- requestId: response.requestId,
43
- accepted: response.accepted,
44
- duplicate: response.duplicate,
45
- };
46
- },
47
- async wait(requestId, timeoutMs, signal) {
48
- const response = await requestBroker(
49
- credentials,
50
- {
51
- type: "wait",
52
- token: credentials.token,
53
- requestId,
54
- ...(timeoutMs !== undefined ? { timeoutMs } : {}),
55
- },
56
- signal,
57
- );
58
- if (response.ok !== true) throw brokerError(response);
59
- if (typeof response.response !== "string") {
60
- throw new Error("Subagent broker returned an invalid plain-text response.");
61
- }
62
- return response.response;
63
- },
64
- };
20
+ return {
21
+ async send(params, signal) {
22
+ const response = await requestBroker(
23
+ credentials,
24
+ { type: "send", token: credentials.token, ...params },
25
+ signal,
26
+ SEND_RESPONSE_TIMEOUT_MS,
27
+ );
28
+ if (response.ok !== true) throw brokerError(response);
29
+ if (
30
+ typeof response.requestId !== "string" ||
31
+ !response.requestId ||
32
+ response.requestId.length > MAX_IDENTIFIER_LENGTH ||
33
+ typeof response.accepted !== "boolean" ||
34
+ typeof response.duplicate !== "boolean"
35
+ ) {
36
+ throw new Error("Subagent broker returned an invalid send acknowledgement.");
37
+ }
38
+ return {
39
+ requestId: response.requestId,
40
+ accepted: response.accepted,
41
+ duplicate: response.duplicate,
42
+ };
43
+ },
44
+ async wait(requestId, timeoutMs, signal) {
45
+ const response = await requestBroker(
46
+ credentials,
47
+ {
48
+ type: "wait",
49
+ token: credentials.token,
50
+ requestId,
51
+ ...(timeoutMs !== undefined ? { timeoutMs } : {}),
52
+ },
53
+ signal,
54
+ );
55
+ if (response.ok !== true) throw brokerError(response);
56
+ if (typeof response.response !== "string") {
57
+ throw new Error("Subagent broker returned an invalid plain-text response.");
58
+ }
59
+ return response.response;
60
+ },
61
+ };
65
62
  }
66
63
 
67
64
  function requestBroker(
68
- credentials: BrokerCredentials,
69
- request: Record<string, unknown>,
70
- signal?: AbortSignal,
71
- responseTimeoutMs?: number,
65
+ credentials: BrokerCredentials,
66
+ request: Record<string, unknown>,
67
+ signal?: AbortSignal,
68
+ responseTimeoutMs?: number,
72
69
  ): Promise<Record<string, unknown>> {
73
- if (signal?.aborted) return Promise.reject(abortError("Subagent broker request was cancelled."));
74
- return new Promise((resolve, reject) => {
75
- const socket = net.createConnection({ host: credentials.host, port: credentials.port });
76
- let response = Buffer.alloc(0);
77
- let settled = false;
78
- let responseTimer: NodeJS.Timeout | undefined;
79
- const connectTimer = setTimeout(
80
- () => finish(new Error("Subagent broker connection timed out.")),
81
- CONNECT_TIMEOUT_MS,
82
- );
83
- connectTimer.unref();
84
- const onAbort = () => finish(abortError("Subagent broker request was cancelled."));
85
- const finish = (error?: Error, value?: Record<string, unknown>) => {
86
- if (settled) return;
87
- settled = true;
88
- clearTimeout(connectTimer);
89
- if (responseTimer) clearTimeout(responseTimer);
90
- signal?.removeEventListener("abort", onAbort);
91
- socket.destroy();
92
- if (error) reject(error);
93
- else resolve(value ?? {});
94
- };
95
- signal?.addEventListener("abort", onAbort, { once: true });
96
- socket.once("connect", () => {
97
- clearTimeout(connectTimer);
98
- if (responseTimeoutMs !== undefined) {
99
- responseTimer = setTimeout(
100
- () => finish(new Error("Subagent broker response timed out.")),
101
- responseTimeoutMs,
102
- );
103
- responseTimer.unref();
104
- }
105
- socket.write(`${JSON.stringify(request)}\n`);
106
- });
107
- socket.on("data", (chunk: Buffer) => {
108
- if (settled) return;
109
- response = Buffer.concat([response, chunk]);
110
- if (response.byteLength > MAX_FRAME_BYTES) {
111
- finish(new Error("Subagent broker response exceeded its size limit."));
112
- return;
113
- }
114
- const newline = response.indexOf(0x0a);
115
- if (newline < 0) return;
116
- const trailing = response
117
- .subarray(newline + 1)
118
- .toString("utf8")
119
- .trim();
120
- if (trailing) {
121
- finish(new Error("Subagent broker returned more than one response frame."));
122
- return;
123
- }
124
- try {
125
- const parsed = JSON.parse(response.subarray(0, newline).toString("utf8")) as unknown;
126
- if (!isRecord(parsed)) throw new Error();
127
- finish(undefined, parsed);
128
- } catch {
129
- finish(new Error("Subagent broker returned malformed JSON."));
130
- }
131
- });
132
- socket.once("error", (error) => finish(error));
133
- socket.once("close", () => {
134
- if (!settled) finish(new Error("Subagent broker closed without a response."));
135
- });
136
- });
70
+ if (signal?.aborted) return Promise.reject(abortError("Subagent broker request was cancelled."));
71
+ return new Promise((resolve, reject) => {
72
+ const socket = net.createConnection({ host: credentials.host, port: credentials.port });
73
+ let response = Buffer.alloc(0);
74
+ let settled = false;
75
+ let responseTimer: NodeJS.Timeout | undefined;
76
+ const connectTimer = setTimeout(
77
+ () => finish(new Error("Subagent broker connection timed out.")),
78
+ CONNECT_TIMEOUT_MS,
79
+ );
80
+ connectTimer.unref();
81
+ const onAbort = () => finish(abortError("Subagent broker request was cancelled."));
82
+ const finish = (error?: Error, value?: Record<string, unknown>) => {
83
+ if (settled) return;
84
+ settled = true;
85
+ clearTimeout(connectTimer);
86
+ if (responseTimer) clearTimeout(responseTimer);
87
+ signal?.removeEventListener("abort", onAbort);
88
+ socket.destroy();
89
+ if (error) reject(error);
90
+ else resolve(value ?? {});
91
+ };
92
+ signal?.addEventListener("abort", onAbort, { once: true });
93
+ socket.once("connect", () => {
94
+ clearTimeout(connectTimer);
95
+ if (responseTimeoutMs !== undefined) {
96
+ responseTimer = setTimeout(() => finish(new Error("Subagent broker response timed out.")), responseTimeoutMs);
97
+ responseTimer.unref();
98
+ }
99
+ socket.write(`${JSON.stringify(request)}\n`);
100
+ });
101
+ socket.on("data", (chunk: Buffer) => {
102
+ if (settled) return;
103
+ response = Buffer.concat([response, chunk]);
104
+ if (response.byteLength > MAX_FRAME_BYTES) {
105
+ finish(new Error("Subagent broker response exceeded its size limit."));
106
+ return;
107
+ }
108
+ const newline = response.indexOf(0x0a);
109
+ if (newline < 0) return;
110
+ const trailing = response
111
+ .subarray(newline + 1)
112
+ .toString("utf8")
113
+ .trim();
114
+ if (trailing) {
115
+ finish(new Error("Subagent broker returned more than one response frame."));
116
+ return;
117
+ }
118
+ try {
119
+ const parsed = JSON.parse(response.subarray(0, newline).toString("utf8")) as unknown;
120
+ if (!isRecord(parsed)) throw new Error();
121
+ finish(undefined, parsed);
122
+ } catch {
123
+ finish(new Error("Subagent broker returned malformed JSON."));
124
+ }
125
+ });
126
+ socket.once("error", (error) => finish(error));
127
+ socket.once("close", () => {
128
+ if (!settled) finish(new Error("Subagent broker closed without a response."));
129
+ });
130
+ });
137
131
  }
138
132
 
139
133
  function brokerError(response: Record<string, unknown>): Error {
140
- return new Error(
141
- typeof response.error === "string" ? response.error : "Subagent broker rejected the request.",
142
- );
134
+ return new Error(typeof response.error === "string" ? response.error : "Subagent broker rejected the request.");
143
135
  }
144
136
 
145
137
  function abortError(message: string): Error {
146
- const error = new Error(message);
147
- error.name = "AbortError";
148
- return error;
138
+ const error = new Error(message);
139
+ error.name = "AbortError";
140
+ return error;
149
141
  }
150
142
 
151
143
  function isRecord(value: unknown): value is Record<string, unknown> {
152
- return typeof value === "object" && value !== null && !Array.isArray(value);
144
+ return typeof value === "object" && value !== null && !Array.isArray(value);
153
145
  }