@hheei/pi-subagents 0.1.2 → 0.2.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 +42 -137
- package/dist/agent-resolver.d.ts +20 -0
- package/dist/agent-resolver.js +155 -21
- package/dist/bridge-client.d.ts +55 -0
- package/dist/bridge-client.js +453 -0
- package/dist/bridge-server.d.ts +72 -0
- package/dist/bridge-server.js +490 -0
- package/dist/builtin-agents.d.ts +24 -0
- package/dist/builtin-agents.js +152 -0
- package/dist/child-bridge.d.ts +31 -15
- package/dist/child-bridge.js +95 -117
- package/dist/child-control.d.ts +58 -0
- package/dist/child-control.js +176 -0
- package/dist/child-process.d.ts +21 -0
- package/dist/child-process.js +93 -0
- package/dist/commands.d.ts +2 -2
- package/dist/commands.js +52 -52
- package/dist/diagnostics.d.ts +0 -2
- package/dist/diagnostics.js +0 -8
- package/dist/domain.d.ts +57 -27
- package/dist/domain.js +60 -11
- package/dist/extension.d.ts +26 -1
- package/dist/extension.js +243 -46
- package/dist/host-adapter.d.ts +37 -12
- package/dist/host-adapter.js +214 -63
- package/dist/json-lines.js +2 -1
- package/dist/launch-spec.d.ts +8 -6
- package/dist/launch-spec.js +61 -18
- package/dist/manager.d.ts +93 -49
- package/dist/manager.js +1175 -528
- package/dist/markdown.d.ts +8 -0
- package/dist/markdown.js +21 -0
- package/dist/protocol.d.ts +33 -52
- package/dist/protocol.js +41 -52
- package/dist/registry.d.ts +3 -8
- package/dist/registry.js +41 -163
- package/dist/rpc-events.d.ts +3 -5
- package/dist/rpc-events.js +5 -9
- package/dist/runtime.d.ts +66 -11
- package/dist/runtime.js +182 -208
- package/dist/session-bootstrap.d.ts +23 -7
- package/dist/session-bootstrap.js +79 -15
- package/dist/session-fork.d.ts +16 -0
- package/dist/session-fork.js +67 -0
- package/dist/state.d.ts +7 -0
- package/dist/state.js +60 -25
- package/dist/tool-activation.d.ts +18 -0
- package/dist/tool-activation.js +227 -0
- package/dist/tools.d.ts +6 -3
- package/dist/tools.js +213 -72
- package/dist/widget.d.ts +4 -8
- package/dist/widget.js +55 -64
- package/package.json +51 -52
- package/dist/child-nudge.d.ts +0 -24
- package/dist/child-nudge.js +0 -102
- package/dist/connector.d.ts +0 -53
- package/dist/connector.js +0 -283
- package/dist/pause-gate.d.ts +0 -14
- package/dist/pause-gate.js +0 -64
- package/dist/rpc-adapter.d.ts +0 -40
- package/dist/rpc-adapter.js +0 -194
- package/dist/runner-entry.d.ts +0 -24
- package/dist/runner-entry.js +0 -166
- package/dist/runner.d.ts +0 -52
- package/dist/runner.js +0 -930
package/dist/rpc-adapter.d.ts
DELETED
|
@@ -1,40 +0,0 @@
|
|
|
1
|
-
import type { ChildProcessWithoutNullStreams } from "node:child_process";
|
|
2
|
-
import { type RunnerOperation } from "./protocol.js";
|
|
3
|
-
/**
|
|
4
|
-
* Runner operations that map onto a real Pi `--mode rpc` command. Runner-local
|
|
5
|
-
* shutdown, writer handoff, and contact_parent operations are never forwarded to Pi.
|
|
6
|
-
*/
|
|
7
|
-
export type PiRpcOperation = Exclude<RunnerOperation, "shutdown" | "contact_parent" | "report_lifecycle" | "report_paused" | "pause" | "cancel_pause" | "close_writer" | "start_rpc">;
|
|
8
|
-
/** A forwarded Pi RPC command did not answer within its deadline. */
|
|
9
|
-
export declare class PiRpcTimeoutError extends Error {
|
|
10
|
-
constructor(message: string);
|
|
11
|
-
}
|
|
12
|
-
export interface PiRpcAdapterOptions {
|
|
13
|
-
readonly process: ChildProcessWithoutNullStreams;
|
|
14
|
-
readonly maxFrameBytes?: number;
|
|
15
|
-
readonly maxPendingRequests?: number;
|
|
16
|
-
readonly requestTimeoutMs?: number;
|
|
17
|
-
/**
|
|
18
|
-
* Receives exceptions thrown by event listeners. A listener that throws must
|
|
19
|
-
* not be mistaken for a malformed Pi frame, so the error is reported here.
|
|
20
|
-
*/
|
|
21
|
-
readonly onListenerError?: (error: unknown) => void;
|
|
22
|
-
}
|
|
23
|
-
/**
|
|
24
|
-
* Thin request/response transport over one Pi `--mode rpc` child process.
|
|
25
|
-
*
|
|
26
|
-
* Pi stdout is RPC JSON lines only. Responses are matched to in-flight requests
|
|
27
|
-
* by request ID; everything else is delivered to event listeners unchanged.
|
|
28
|
-
*/
|
|
29
|
-
export declare class PiRpcAdapter {
|
|
30
|
-
#private;
|
|
31
|
-
constructor(options: PiRpcAdapterOptions);
|
|
32
|
-
onEvent(listener: (event: unknown) => void): () => void;
|
|
33
|
-
/** Confirms readiness with a real `get_state` round trip. */
|
|
34
|
-
ready(signal?: AbortSignal): Promise<unknown>;
|
|
35
|
-
request(operation: PiRpcOperation, payload?: unknown, options?: {
|
|
36
|
-
readonly signal?: AbortSignal;
|
|
37
|
-
readonly timeoutMs?: number;
|
|
38
|
-
}): Promise<unknown>;
|
|
39
|
-
close(reason?: Error): void;
|
|
40
|
-
}
|
package/dist/rpc-adapter.js
DELETED
|
@@ -1,194 +0,0 @@
|
|
|
1
|
-
import { Value } from "typebox/value";
|
|
2
|
-
import { abortError, toError } from "./diagnostics.js";
|
|
3
|
-
import { isRecord } from "./domain.js";
|
|
4
|
-
import { attachJsonLineReader, serializeJsonLine } from "./json-lines.js";
|
|
5
|
-
import { DEFAULT_MAX_FRAME_BYTES, DEFAULT_MAX_PENDING_REQUESTS, GetEntriesPayloadSchema, PromptPayloadSchema, } from "./protocol.js";
|
|
6
|
-
/** A forwarded Pi RPC command did not answer within its deadline. */
|
|
7
|
-
export class PiRpcTimeoutError extends Error {
|
|
8
|
-
constructor(message) {
|
|
9
|
-
super(message);
|
|
10
|
-
this.name = "PiRpcTimeoutError";
|
|
11
|
-
}
|
|
12
|
-
}
|
|
13
|
-
/**
|
|
14
|
-
* Thin request/response transport over one Pi `--mode rpc` child process.
|
|
15
|
-
*
|
|
16
|
-
* Pi stdout is RPC JSON lines only. Responses are matched to in-flight requests
|
|
17
|
-
* by request ID; everything else is delivered to event listeners unchanged.
|
|
18
|
-
*/
|
|
19
|
-
export class PiRpcAdapter {
|
|
20
|
-
#process;
|
|
21
|
-
#maxFrameBytes;
|
|
22
|
-
#maxPendingRequests;
|
|
23
|
-
#requestTimeoutMs;
|
|
24
|
-
#onListenerError;
|
|
25
|
-
#pending = new Map();
|
|
26
|
-
#listeners = new Set();
|
|
27
|
-
#nextRequestId = 0;
|
|
28
|
-
#closedError;
|
|
29
|
-
#detachReader;
|
|
30
|
-
constructor(options) {
|
|
31
|
-
this.#process = options.process;
|
|
32
|
-
this.#maxFrameBytes = options.maxFrameBytes ?? DEFAULT_MAX_FRAME_BYTES;
|
|
33
|
-
this.#maxPendingRequests = options.maxPendingRequests ?? DEFAULT_MAX_PENDING_REQUESTS;
|
|
34
|
-
this.#requestTimeoutMs = options.requestTimeoutMs ?? 30_000;
|
|
35
|
-
this.#onListenerError =
|
|
36
|
-
options.onListenerError ??
|
|
37
|
-
((error) => {
|
|
38
|
-
process.stderr.write(`pi-subagents rpc listener error: ${error instanceof Error ? (error.stack ?? error.message) : String(error)}\n`);
|
|
39
|
-
});
|
|
40
|
-
const onStreamError = (error) => this.#fail(error);
|
|
41
|
-
this.#process.stdin.on("error", onStreamError);
|
|
42
|
-
this.#process.stdout.on("error", onStreamError);
|
|
43
|
-
this.#process.once("error", onStreamError);
|
|
44
|
-
this.#process.once("exit", (code, signal) => {
|
|
45
|
-
this.#fail(new Error(`Pi RPC exited (code=${String(code)}, signal=${String(signal)})`));
|
|
46
|
-
});
|
|
47
|
-
this.#detachReader = attachJsonLineReader(this.#process.stdout, {
|
|
48
|
-
maxFrameBytes: this.#maxFrameBytes,
|
|
49
|
-
onValue: (value) => this.#handleValue(value),
|
|
50
|
-
onError: (error) => this.#fail(error),
|
|
51
|
-
});
|
|
52
|
-
}
|
|
53
|
-
onEvent(listener) {
|
|
54
|
-
this.#listeners.add(listener);
|
|
55
|
-
return () => {
|
|
56
|
-
this.#listeners.delete(listener);
|
|
57
|
-
};
|
|
58
|
-
}
|
|
59
|
-
/** Confirms readiness with a real `get_state` round trip. */
|
|
60
|
-
async ready(signal) {
|
|
61
|
-
return this.request("get_state", undefined, signal === undefined ? {} : { signal });
|
|
62
|
-
}
|
|
63
|
-
request(operation, payload, options = {}) {
|
|
64
|
-
let command;
|
|
65
|
-
try {
|
|
66
|
-
command = buildPiCommand(operation, payload);
|
|
67
|
-
}
|
|
68
|
-
catch (error) {
|
|
69
|
-
return Promise.reject(toError(error));
|
|
70
|
-
}
|
|
71
|
-
return this.#send(command, options);
|
|
72
|
-
}
|
|
73
|
-
close(reason = new Error("Pi RPC adapter closed")) {
|
|
74
|
-
this.#fail(reason);
|
|
75
|
-
}
|
|
76
|
-
#send(command, options) {
|
|
77
|
-
if (this.#closedError)
|
|
78
|
-
return Promise.reject(this.#closedError);
|
|
79
|
-
if (this.#process.exitCode !== null) {
|
|
80
|
-
return Promise.reject(new Error(`Pi RPC process already exited (code=${this.#process.exitCode})`));
|
|
81
|
-
}
|
|
82
|
-
if (options.signal?.aborted)
|
|
83
|
-
return Promise.reject(abortError());
|
|
84
|
-
if (this.#pending.size >= this.#maxPendingRequests) {
|
|
85
|
-
return Promise.reject(new Error("Pi RPC pending request limit exceeded"));
|
|
86
|
-
}
|
|
87
|
-
const id = `pi-${++this.#nextRequestId}`;
|
|
88
|
-
let encoded;
|
|
89
|
-
try {
|
|
90
|
-
encoded = serializeJsonLine({ ...command, id }, this.#maxFrameBytes);
|
|
91
|
-
}
|
|
92
|
-
catch (error) {
|
|
93
|
-
return Promise.reject(toError(error));
|
|
94
|
-
}
|
|
95
|
-
return new Promise((resolve, reject) => {
|
|
96
|
-
const settle = (error, value) => {
|
|
97
|
-
const pending = this.#pending.get(id);
|
|
98
|
-
if (!pending)
|
|
99
|
-
return;
|
|
100
|
-
this.#pending.delete(id);
|
|
101
|
-
pending.cleanup();
|
|
102
|
-
if (error)
|
|
103
|
-
pending.reject(error);
|
|
104
|
-
else
|
|
105
|
-
pending.resolve(value);
|
|
106
|
-
};
|
|
107
|
-
const timeout = setTimeout(() => settle(new PiRpcTimeoutError(`Pi RPC ${command.type} timed out`)), options.timeoutMs ?? this.#requestTimeoutMs);
|
|
108
|
-
const onAbort = () => settle(abortError());
|
|
109
|
-
const cleanup = () => {
|
|
110
|
-
clearTimeout(timeout);
|
|
111
|
-
options.signal?.removeEventListener("abort", onAbort);
|
|
112
|
-
};
|
|
113
|
-
this.#pending.set(id, { command: command.type, resolve, reject, cleanup });
|
|
114
|
-
options.signal?.addEventListener("abort", onAbort, { once: true });
|
|
115
|
-
this.#process.stdin.write(encoded, (error) => {
|
|
116
|
-
if (error)
|
|
117
|
-
settle(error);
|
|
118
|
-
});
|
|
119
|
-
});
|
|
120
|
-
}
|
|
121
|
-
#handleValue(value) {
|
|
122
|
-
if (!isRecord(value) || value.type !== "response") {
|
|
123
|
-
for (const listener of this.#listeners) {
|
|
124
|
-
try {
|
|
125
|
-
listener(value);
|
|
126
|
-
}
|
|
127
|
-
catch (error) {
|
|
128
|
-
this.#onListenerError(error);
|
|
129
|
-
}
|
|
130
|
-
}
|
|
131
|
-
return;
|
|
132
|
-
}
|
|
133
|
-
if (typeof value.id !== "string" ||
|
|
134
|
-
typeof value.command !== "string" ||
|
|
135
|
-
typeof value.success !== "boolean" ||
|
|
136
|
-
(value.success === false && typeof value.error !== "string")) {
|
|
137
|
-
this.#fail(new Error("Malformed Pi RPC response"));
|
|
138
|
-
return;
|
|
139
|
-
}
|
|
140
|
-
const pending = this.#pending.get(value.id);
|
|
141
|
-
if (!pending)
|
|
142
|
-
return;
|
|
143
|
-
this.#pending.delete(value.id);
|
|
144
|
-
pending.cleanup();
|
|
145
|
-
if (value.command !== pending.command) {
|
|
146
|
-
pending.reject(new Error(`Pi RPC response command mismatch: expected ${pending.command}`));
|
|
147
|
-
return;
|
|
148
|
-
}
|
|
149
|
-
if (!value.success) {
|
|
150
|
-
pending.reject(new Error(value.error));
|
|
151
|
-
return;
|
|
152
|
-
}
|
|
153
|
-
pending.resolve(value.data);
|
|
154
|
-
}
|
|
155
|
-
#fail(error) {
|
|
156
|
-
if (this.#closedError)
|
|
157
|
-
return;
|
|
158
|
-
this.#closedError = toError(error);
|
|
159
|
-
this.#detachReader?.();
|
|
160
|
-
this.#detachReader = undefined;
|
|
161
|
-
const closed = this.#closedError;
|
|
162
|
-
for (const pending of this.#pending.values()) {
|
|
163
|
-
pending.cleanup();
|
|
164
|
-
pending.reject(closed);
|
|
165
|
-
}
|
|
166
|
-
this.#pending.clear();
|
|
167
|
-
}
|
|
168
|
-
}
|
|
169
|
-
function buildPiCommand(operation, payload) {
|
|
170
|
-
switch (operation) {
|
|
171
|
-
case "prompt":
|
|
172
|
-
case "steer":
|
|
173
|
-
case "follow_up": {
|
|
174
|
-
if (!Value.Check(PromptPayloadSchema, payload)) {
|
|
175
|
-
throw new Error(`${operation} requires a payload with only a non-empty message`);
|
|
176
|
-
}
|
|
177
|
-
return { type: operation, message: payload.message };
|
|
178
|
-
}
|
|
179
|
-
case "get_entries": {
|
|
180
|
-
if (payload === undefined)
|
|
181
|
-
return { type: operation };
|
|
182
|
-
if (!Value.Check(GetEntriesPayloadSchema, payload)) {
|
|
183
|
-
throw new Error("get_entries payload must be undefined or contain only a non-empty since string");
|
|
184
|
-
}
|
|
185
|
-
return { type: operation, since: payload.since };
|
|
186
|
-
}
|
|
187
|
-
case "abort":
|
|
188
|
-
case "get_state":
|
|
189
|
-
case "get_session_stats":
|
|
190
|
-
if (payload !== undefined)
|
|
191
|
-
throw new Error(`${operation} does not accept a payload`);
|
|
192
|
-
return { type: operation };
|
|
193
|
-
}
|
|
194
|
-
}
|
package/dist/runner-entry.d.ts
DELETED
|
@@ -1,24 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
import { type Static, Type } from "typebox";
|
|
3
|
-
import type { ChildIdentity } from "./domain.js";
|
|
4
|
-
export declare const RUNNER_JOB_FLAG = "--job";
|
|
5
|
-
export declare const JOB_FROM_STDIN = "-";
|
|
6
|
-
/** Bytes of Pi child stderr kept for failure diagnostics. */
|
|
7
|
-
export declare const STDERR_TAIL_BYTES: number;
|
|
8
|
-
export declare const RunnerJobSchema: Type.TObject<{
|
|
9
|
-
invocation: Type.TObject<{
|
|
10
|
-
command: Type.TString;
|
|
11
|
-
args: Type.TArray<Type.TString>;
|
|
12
|
-
}>;
|
|
13
|
-
cwd: Type.TString;
|
|
14
|
-
sessionId: Type.TString;
|
|
15
|
-
sessionPath: Type.TOptional<Type.TString>;
|
|
16
|
-
claimId: Type.TOptional<Type.TString>;
|
|
17
|
-
env: Type.TOptional<Type.TRecord<"^.*$", Type.TString>>;
|
|
18
|
-
}>;
|
|
19
|
-
export type RunnerJob = Static<typeof RunnerJobSchema>;
|
|
20
|
-
/** Reads the runner identity from the shared environment contract. */
|
|
21
|
-
export declare function readIdentityFromEnv(env: NodeJS.ProcessEnv): ChildIdentity;
|
|
22
|
-
/** Resolves the job source: `--job <path>`, a positional path, or stdin. */
|
|
23
|
-
export declare function resolveJobSource(argv: readonly string[]): string;
|
|
24
|
-
export declare function readRunnerJob(source: string, stdin?: AsyncIterable<Buffer | string>): Promise<RunnerJob>;
|
package/dist/runner-entry.js
DELETED
|
@@ -1,166 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
/**
|
|
3
|
-
* Executable entry for one independently living pi-subagents runner.
|
|
4
|
-
*
|
|
5
|
-
* Usage:
|
|
6
|
-
* node runner-entry.js --job <job.json> # or: node runner-entry.js <job.json>
|
|
7
|
-
* node runner-entry.js - # job JSON on stdin
|
|
8
|
-
*
|
|
9
|
-
* The job file carries the pre-resolved Pi `--mode rpc` invocation (command and
|
|
10
|
-
* complete argv), cwd, session identity and extra environment variables. This
|
|
11
|
-
* process never guesses the Pi CLI: it spawns exactly what the launch-spec
|
|
12
|
-
* builder resolved in the parent.
|
|
13
|
-
*
|
|
14
|
-
* Identity comes from the `PI_SUBAGENTS_*` environment contract, which is also
|
|
15
|
-
* inherited by the Pi child so its child branch can identify itself and reach
|
|
16
|
-
* this runner's endpoint.
|
|
17
|
-
*/
|
|
18
|
-
import { spawn } from "node:child_process";
|
|
19
|
-
import { createHash } from "node:crypto";
|
|
20
|
-
import { readFile, stat } from "node:fs/promises";
|
|
21
|
-
import { Type } from "typebox";
|
|
22
|
-
import { Value } from "typebox/value";
|
|
23
|
-
import { errorMessage, writeDiagnostic } from "./diagnostics.js";
|
|
24
|
-
import { createSubagentRegistry } from "./registry.js";
|
|
25
|
-
import { startRunner } from "./runner.js";
|
|
26
|
-
export const RUNNER_JOB_FLAG = "--job";
|
|
27
|
-
export const JOB_FROM_STDIN = "-";
|
|
28
|
-
/** Bytes of Pi child stderr kept for failure diagnostics. */
|
|
29
|
-
export const STDERR_TAIL_BYTES = 16 * 1024;
|
|
30
|
-
export const RunnerJobSchema = Type.Object({
|
|
31
|
-
invocation: Type.Object({ command: Type.String({ minLength: 1 }), args: Type.Array(Type.String()) }, { additionalProperties: false }),
|
|
32
|
-
cwd: Type.String({ minLength: 1 }),
|
|
33
|
-
sessionId: Type.String({ minLength: 1 }),
|
|
34
|
-
sessionPath: Type.Optional(Type.String({ minLength: 1 })),
|
|
35
|
-
claimId: Type.Optional(Type.String({ minLength: 1 })),
|
|
36
|
-
env: Type.Optional(Type.Record(Type.String(), Type.String())),
|
|
37
|
-
}, { additionalProperties: false });
|
|
38
|
-
const IDENTITY_ENV_KEYS = {
|
|
39
|
-
parentSessionId: "PI_SUBAGENTS_PARENT_SESSION_ID",
|
|
40
|
-
subagentId: "PI_SUBAGENTS_CHILD_ID",
|
|
41
|
-
runtimeIdentity: "PI_SUBAGENTS_RUNTIME_ID",
|
|
42
|
-
endpoint: "PI_SUBAGENTS_ENDPOINT",
|
|
43
|
-
token: "PI_SUBAGENTS_TOKEN",
|
|
44
|
-
};
|
|
45
|
-
/** Reads the runner identity from the shared environment contract. */
|
|
46
|
-
export function readIdentityFromEnv(env) {
|
|
47
|
-
const read = (key) => {
|
|
48
|
-
const value = env[key];
|
|
49
|
-
if (value === undefined || value === "") {
|
|
50
|
-
throw new Error(`runner requires ${key} in its environment`);
|
|
51
|
-
}
|
|
52
|
-
return value;
|
|
53
|
-
};
|
|
54
|
-
return {
|
|
55
|
-
parentSessionId: read(IDENTITY_ENV_KEYS.parentSessionId),
|
|
56
|
-
subagentId: read(IDENTITY_ENV_KEYS.subagentId),
|
|
57
|
-
runtimeIdentity: read(IDENTITY_ENV_KEYS.runtimeIdentity),
|
|
58
|
-
endpoint: read(IDENTITY_ENV_KEYS.endpoint),
|
|
59
|
-
token: read(IDENTITY_ENV_KEYS.token),
|
|
60
|
-
};
|
|
61
|
-
}
|
|
62
|
-
/** Resolves the job source: `--job <path>`, a positional path, or stdin. */
|
|
63
|
-
export function resolveJobSource(argv) {
|
|
64
|
-
const flagIndex = argv.indexOf(RUNNER_JOB_FLAG);
|
|
65
|
-
if (flagIndex >= 0) {
|
|
66
|
-
const value = argv[flagIndex + 1];
|
|
67
|
-
if (value === undefined || value === "")
|
|
68
|
-
throw new Error(`${RUNNER_JOB_FLAG} requires a path`);
|
|
69
|
-
return value;
|
|
70
|
-
}
|
|
71
|
-
const positional = argv.find((argument) => !argument.startsWith("--"));
|
|
72
|
-
if (positional === undefined) {
|
|
73
|
-
throw new Error(`runner requires a job: ${RUNNER_JOB_FLAG} <path> or ${JOB_FROM_STDIN}`);
|
|
74
|
-
}
|
|
75
|
-
return positional;
|
|
76
|
-
}
|
|
77
|
-
export async function readRunnerJob(source, stdin = process.stdin) {
|
|
78
|
-
const raw = source === JOB_FROM_STDIN ? await readStream(stdin) : await readFile(source, "utf8");
|
|
79
|
-
let parsed;
|
|
80
|
-
try {
|
|
81
|
-
parsed = JSON.parse(raw);
|
|
82
|
-
}
|
|
83
|
-
catch (error) {
|
|
84
|
-
throw new Error(`runner job is not valid JSON: ${errorMessage(error)}`);
|
|
85
|
-
}
|
|
86
|
-
if (!Value.Check(RunnerJobSchema, parsed))
|
|
87
|
-
throw new Error("runner job does not match the runner job schema");
|
|
88
|
-
return parsed;
|
|
89
|
-
}
|
|
90
|
-
async function readStream(stdin) {
|
|
91
|
-
const chunks = [];
|
|
92
|
-
for await (const chunk of stdin) {
|
|
93
|
-
chunks.push(Buffer.isBuffer(chunk) ? chunk.toString("utf8") : chunk);
|
|
94
|
-
}
|
|
95
|
-
return chunks.join("");
|
|
96
|
-
}
|
|
97
|
-
async function main() {
|
|
98
|
-
const job = await readRunnerJob(resolveJobSource(process.argv.slice(2)));
|
|
99
|
-
const identity = readIdentityFromEnv(process.env);
|
|
100
|
-
const cwd = await stat(job.cwd).catch(() => undefined);
|
|
101
|
-
if (cwd === undefined || !cwd.isDirectory()) {
|
|
102
|
-
throw new Error(`runner job cwd is not a directory: ${job.cwd}`);
|
|
103
|
-
}
|
|
104
|
-
const registry = createSubagentRegistry({ parentSessionId: identity.parentSessionId });
|
|
105
|
-
if (job.claimId !== undefined) {
|
|
106
|
-
await registry.markClaimRunner(identity.subagentId, job.claimId, process.pid);
|
|
107
|
-
await registry.activateClaim(identity.subagentId, job.claimId, process.pid);
|
|
108
|
-
}
|
|
109
|
-
let stderrTail = "";
|
|
110
|
-
const spawnWriter = () => {
|
|
111
|
-
const next = spawn(job.invocation.command, [...job.invocation.args], {
|
|
112
|
-
cwd: job.cwd,
|
|
113
|
-
env: {
|
|
114
|
-
...process.env,
|
|
115
|
-
...(job.env ?? {}),
|
|
116
|
-
[IDENTITY_ENV_KEYS.parentSessionId]: identity.parentSessionId,
|
|
117
|
-
[IDENTITY_ENV_KEYS.subagentId]: identity.subagentId,
|
|
118
|
-
[IDENTITY_ENV_KEYS.runtimeIdentity]: identity.runtimeIdentity,
|
|
119
|
-
[IDENTITY_ENV_KEYS.endpoint]: identity.endpoint,
|
|
120
|
-
[IDENTITY_ENV_KEYS.token]: identity.token,
|
|
121
|
-
},
|
|
122
|
-
stdio: ["pipe", "pipe", "pipe"],
|
|
123
|
-
});
|
|
124
|
-
next.stderr.on("data", (chunk) => {
|
|
125
|
-
process.stderr.write(chunk);
|
|
126
|
-
stderrTail = `${stderrTail}${chunk.toString("utf8")}`.slice(-STDERR_TAIL_BYTES);
|
|
127
|
-
});
|
|
128
|
-
return next;
|
|
129
|
-
};
|
|
130
|
-
const child = spawnWriter();
|
|
131
|
-
try {
|
|
132
|
-
const runner = await startRunner({
|
|
133
|
-
identity,
|
|
134
|
-
process: child,
|
|
135
|
-
spawnWriter,
|
|
136
|
-
sessionId: job.sessionId,
|
|
137
|
-
authorizeRecovery: async (claimId, token) => {
|
|
138
|
-
const tokenHash = createHash("sha256").update(token).digest("hex");
|
|
139
|
-
await registry.consumeReconnectClaim(identity.subagentId, claimId, tokenHash);
|
|
140
|
-
return true;
|
|
141
|
-
},
|
|
142
|
-
onDiagnostic: (line) => writeDiagnostic("runner", line),
|
|
143
|
-
});
|
|
144
|
-
writeDiagnostic("runner", `ready endpoint=${identity.endpoint} child=${identity.subagentId} session=${job.sessionId}`);
|
|
145
|
-
const stop = (signal) => {
|
|
146
|
-
void runner.shutdown(new Error(`runner received ${signal}`));
|
|
147
|
-
};
|
|
148
|
-
process.once("SIGTERM", () => stop("SIGTERM"));
|
|
149
|
-
process.once("SIGINT", () => stop("SIGINT"));
|
|
150
|
-
const exit = await runner.closed();
|
|
151
|
-
if (stderrTail !== "" && !exit.requested) {
|
|
152
|
-
writeDiagnostic("runner", `Pi child stderr tail:\n${stderrTail}`);
|
|
153
|
-
}
|
|
154
|
-
// An explicit shutdown is a clean end; a child that died on its own is not.
|
|
155
|
-
process.exitCode = exit.requested ? 0 : (exit.code ?? (exit.signal === null ? 0 : 1));
|
|
156
|
-
}
|
|
157
|
-
catch (error) {
|
|
158
|
-
if (stderrTail !== "")
|
|
159
|
-
writeDiagnostic("runner", `Pi child stderr tail:\n${stderrTail}`);
|
|
160
|
-
throw error;
|
|
161
|
-
}
|
|
162
|
-
}
|
|
163
|
-
void main().catch((error) => {
|
|
164
|
-
writeDiagnostic("runner-entry", errorMessage(error));
|
|
165
|
-
process.exitCode = 1;
|
|
166
|
-
});
|
package/dist/runner.d.ts
DELETED
|
@@ -1,52 +0,0 @@
|
|
|
1
|
-
import type { ChildProcessWithoutNullStreams } from "node:child_process";
|
|
2
|
-
import { type ChildIdentity } from "./domain.js";
|
|
3
|
-
export interface SubagentRunnerOptions {
|
|
4
|
-
readonly identity: ChildIdentity;
|
|
5
|
-
/** The Pi `--mode rpc` child owned by this runner. */
|
|
6
|
-
readonly process: ChildProcessWithoutNullStreams;
|
|
7
|
-
/**
|
|
8
|
-
* Recreates a Pi RPC writer with the same job. Required for attach recovery
|
|
9
|
-
* (`start_rpc`) so the runner endpoint survives writer replacement.
|
|
10
|
-
*/
|
|
11
|
-
readonly spawnWriter?: () => ChildProcessWithoutNullStreams;
|
|
12
|
-
readonly maxFrameBytes?: number;
|
|
13
|
-
readonly maxPendingRequests?: number;
|
|
14
|
-
readonly maxBufferedEvents?: number;
|
|
15
|
-
/** Queued bytes tolerated on a controller socket before it is dropped. */
|
|
16
|
-
readonly maxControllerBufferBytes?: number;
|
|
17
|
-
readonly requestTimeoutMs?: number;
|
|
18
|
-
readonly readyTimeoutMs?: number;
|
|
19
|
-
readonly handshakeTimeoutMs?: number;
|
|
20
|
-
readonly shutdownGraceMs?: number;
|
|
21
|
-
readonly killGraceMs?: number;
|
|
22
|
-
readonly onDiagnostic?: (line: string) => void;
|
|
23
|
-
/** Atomically consumes a durable recovery claim for a one-time controller token. */
|
|
24
|
-
readonly authorizeRecovery?: (claimId: string, token: string) => Promise<boolean>;
|
|
25
|
-
/** Bound child session; reporters for a different session are rejected after a leave. */
|
|
26
|
-
readonly sessionId?: string;
|
|
27
|
-
}
|
|
28
|
-
/** Observed end of the Pi child owned by the runner. */
|
|
29
|
-
export interface RunnerExit {
|
|
30
|
-
readonly code: number | null;
|
|
31
|
-
readonly signal: string | null;
|
|
32
|
-
readonly reason: Error;
|
|
33
|
-
/** True when an explicit shutdown ended the runtime, not the child itself. */
|
|
34
|
-
readonly requested: boolean;
|
|
35
|
-
}
|
|
36
|
-
export interface SubagentRunner {
|
|
37
|
-
readonly endpoint: string;
|
|
38
|
-
readonly identity: ChildIdentity;
|
|
39
|
-
/** Real `get_state` data captured when the Pi child became ready. */
|
|
40
|
-
readonly readiness: unknown;
|
|
41
|
-
/** Resolves once the Pi child exited and every runner resource is released. */
|
|
42
|
-
closed(): Promise<RunnerExit>;
|
|
43
|
-
shutdown(reason?: Error): Promise<RunnerExit>;
|
|
44
|
-
}
|
|
45
|
-
/**
|
|
46
|
-
* Starts the reconnectable runner endpoint for one Pi RPC child.
|
|
47
|
-
*
|
|
48
|
-
* The runner owns the child stdio, accepts exactly one authenticated controller
|
|
49
|
-
* at a time, and stays alive across controller disconnects: only an explicit
|
|
50
|
-
* `shutdown` request, the end of the Pi child, or process termination stops it.
|
|
51
|
-
*/
|
|
52
|
-
export declare function startRunner(options: SubagentRunnerOptions): Promise<SubagentRunner>;
|