@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/README.md +35 -100
- package/dist/child-communication-bridge.ts +3 -8
- package/dist/child-communication-bridge.ts.map +2 -2
- package/dist/chunks/{chunk-UKTWUQRM.js → chunk-7JQOB375.ts} +7 -22
- package/dist/chunks/chunk-7JQOB375.ts.map +7 -0
- package/dist/index.ts +13 -46
- package/dist/index.ts.map +2 -2
- package/package.json +53 -54
- package/src/broker-credentials.ts +48 -48
- package/src/child-communication-bridge.ts +115 -123
- package/src/child-communication-tools.ts +105 -111
- package/src/completion-renderer.ts +46 -48
- package/src/message-broker.ts +531 -561
- package/src/model-output.ts +25 -28
- package/src/process.ts +595 -616
- package/src/runtime.ts +454 -478
- package/src/subagents.ts +20 -23
- package/src/tools.ts +305 -319
- package/src/types.ts +41 -58
- package/src/widget.ts +85 -91
- package/dist/chunks/chunk-UKTWUQRM.js.map +0 -7
package/package.json
CHANGED
|
@@ -1,56 +1,55 @@
|
|
|
1
1
|
{
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
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
|
-
|
|
10
|
+
return { [BROKER_CREDENTIAL_FD_ENV]: String(BROKER_CREDENTIAL_FD) };
|
|
11
11
|
}
|
|
12
12
|
|
|
13
13
|
export function serializeBrokerCredentials(credentials: BrokerCredentials): string {
|
|
14
|
-
|
|
14
|
+
return JSON.stringify(credentials);
|
|
15
15
|
}
|
|
16
16
|
|
|
17
17
|
export function captureBrokerCredentials(
|
|
18
|
-
|
|
18
|
+
readCredentials: () => string = readCredentialPipe,
|
|
19
19
|
): BrokerCredentials | undefined {
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
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
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
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
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
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
|
-
|
|
18
|
-
|
|
14
|
+
? createChildCommunicationExtension(createBrokerClient(captured))
|
|
15
|
+
: () => undefined;
|
|
19
16
|
|
|
20
17
|
export default childCommunicationBridge;
|
|
21
18
|
|
|
22
19
|
export function createBrokerClient(credentials: BrokerCredentials): ChildCommunicationClient {
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
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
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
65
|
+
credentials: BrokerCredentials,
|
|
66
|
+
request: Record<string, unknown>,
|
|
67
|
+
signal?: AbortSignal,
|
|
68
|
+
responseTimeoutMs?: number,
|
|
72
69
|
): Promise<Record<string, unknown>> {
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
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
|
-
|
|
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
|
-
|
|
147
|
-
|
|
148
|
-
|
|
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
|
-
|
|
144
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
153
145
|
}
|