@hheei/pi-subagents 0.1.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.
Files changed (53) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +161 -0
  3. package/dist/agent-resolver.d.ts +37 -0
  4. package/dist/agent-resolver.js +232 -0
  5. package/dist/child-bridge.d.ts +41 -0
  6. package/dist/child-bridge.js +214 -0
  7. package/dist/child-nudge.d.ts +24 -0
  8. package/dist/child-nudge.js +102 -0
  9. package/dist/commands.d.ts +6 -0
  10. package/dist/commands.js +155 -0
  11. package/dist/connector.d.ts +53 -0
  12. package/dist/connector.js +283 -0
  13. package/dist/diagnostics.d.ts +10 -0
  14. package/dist/diagnostics.js +20 -0
  15. package/dist/domain.d.ts +192 -0
  16. package/dist/domain.js +63 -0
  17. package/dist/extension.d.ts +2 -0
  18. package/dist/extension.js +145 -0
  19. package/dist/host-adapter.d.ts +76 -0
  20. package/dist/host-adapter.js +323 -0
  21. package/dist/index.d.ts +1 -0
  22. package/dist/index.js +1 -0
  23. package/dist/json-lines.d.ts +14 -0
  24. package/dist/json-lines.js +94 -0
  25. package/dist/launch-spec.d.ts +47 -0
  26. package/dist/launch-spec.js +149 -0
  27. package/dist/manager.d.ts +101 -0
  28. package/dist/manager.js +802 -0
  29. package/dist/pause-gate.d.ts +14 -0
  30. package/dist/pause-gate.js +64 -0
  31. package/dist/protocol.d.ts +141 -0
  32. package/dist/protocol.js +168 -0
  33. package/dist/registry.d.ts +41 -0
  34. package/dist/registry.js +575 -0
  35. package/dist/rpc-adapter.d.ts +40 -0
  36. package/dist/rpc-adapter.js +194 -0
  37. package/dist/rpc-events.d.ts +8 -0
  38. package/dist/rpc-events.js +29 -0
  39. package/dist/runner-entry.d.ts +24 -0
  40. package/dist/runner-entry.js +166 -0
  41. package/dist/runner.d.ts +52 -0
  42. package/dist/runner.js +930 -0
  43. package/dist/runtime.d.ts +22 -0
  44. package/dist/runtime.js +253 -0
  45. package/dist/session-bootstrap.d.ts +69 -0
  46. package/dist/session-bootstrap.js +195 -0
  47. package/dist/state.d.ts +15 -0
  48. package/dist/state.js +168 -0
  49. package/dist/tools.d.ts +9 -0
  50. package/dist/tools.js +158 -0
  51. package/dist/widget.d.ts +16 -0
  52. package/dist/widget.js +158 -0
  53. package/package.json +53 -0
@@ -0,0 +1,283 @@
1
+ import { createConnection } from "node:net";
2
+ import { abortError, errorMessage, toError, writeDiagnostic } from "./diagnostics.js";
3
+ import { PROTOCOL_VERSION } from "./domain.js";
4
+ import { attachJsonLineReader, writeJsonLine } from "./json-lines.js";
5
+ import { DEFAULT_MAX_FRAME_BYTES, DEFAULT_MAX_PENDING_REQUESTS, isEventFrame, isHelloAckFrame, isResponseFrame, } from "./protocol.js";
6
+ /** Bounded memory of locally finished request IDs, used to ignore late responses. */
7
+ const MAX_RETIRED_REQUEST_IDS = 256;
8
+ /**
9
+ * Parent-side client of the runner IPC endpoint.
10
+ *
11
+ * The connection is re-connectable by design: `close()` only releases this
12
+ * parent's local socket and in-flight requests. It never terminates the runner
13
+ * or the Pi child that the runner owns.
14
+ */
15
+ export class RunnerConnection {
16
+ #options;
17
+ #pending = new Map();
18
+ #retired = new Set();
19
+ #eventListeners = new Set();
20
+ #socket;
21
+ #detachReader;
22
+ #nextId = 0;
23
+ #connectPromise;
24
+ constructor(options) {
25
+ this.#options = options;
26
+ }
27
+ connect(signal) {
28
+ if (this.#socket && !this.#socket.destroyed)
29
+ return Promise.resolve();
30
+ if (this.#connectPromise !== undefined)
31
+ return this.#connectPromise;
32
+ const promise = this.#open(signal);
33
+ this.#connectPromise = promise;
34
+ void promise
35
+ .finally(() => {
36
+ if (this.#connectPromise === promise)
37
+ this.#connectPromise = undefined;
38
+ })
39
+ .catch(() => undefined);
40
+ return promise;
41
+ }
42
+ get connected() {
43
+ return this.#socket !== undefined && !this.#socket.destroyed;
44
+ }
45
+ request(operation, payload, options = {}) {
46
+ const socket = this.#socket;
47
+ if (!socket || socket.destroyed)
48
+ return Promise.reject(new Error("Runner is not connected"));
49
+ if (options.signal?.aborted)
50
+ return Promise.reject(abortError());
51
+ const limit = this.#options.maxPendingRequests ?? DEFAULT_MAX_PENDING_REQUESTS;
52
+ if (this.#pending.size >= limit) {
53
+ return Promise.reject(new Error("Runner pending request limit exceeded"));
54
+ }
55
+ const id = `controller-${++this.#nextId}`;
56
+ // `Promise.withResolvers` needs lib ES2024; this workspace targets ES2022.
57
+ return new Promise((resolve, reject) => {
58
+ const settle = (error, value) => {
59
+ const pending = this.#pending.get(id);
60
+ if (!pending)
61
+ return;
62
+ this.#pending.delete(id);
63
+ this.#retire(id);
64
+ pending.cleanup();
65
+ if (error)
66
+ pending.reject(error);
67
+ else
68
+ pending.resolve(value);
69
+ };
70
+ const timeout = setTimeout(() => settle(new Error(`Runner ${operation} request timed out`)), options.timeoutMs ?? this.#options.requestTimeoutMs ?? 30_000);
71
+ const onAbort = () => settle(abortError());
72
+ const cleanup = () => {
73
+ clearTimeout(timeout);
74
+ options.signal?.removeEventListener("abort", onAbort);
75
+ };
76
+ this.#pending.set(id, { resolve, reject, cleanup });
77
+ options.signal?.addEventListener("abort", onAbort, { once: true });
78
+ void writeJsonLine(socket, { version: PROTOCOL_VERSION, type: "request", id, operation, payload }, this.#options.maxFrameBytes ?? DEFAULT_MAX_FRAME_BYTES).catch((error) => {
79
+ settle(toError(error));
80
+ });
81
+ });
82
+ }
83
+ onEvent(listener) {
84
+ this.#eventListeners.add(listener);
85
+ return () => {
86
+ this.#eventListeners.delete(listener);
87
+ };
88
+ }
89
+ /**
90
+ * Releases the local socket, reader, listeners and in-flight requests. The
91
+ * runner keeps running and can be reconnected from the same identity.
92
+ */
93
+ close() {
94
+ const socket = this.#socket;
95
+ this.#disconnectFor(socket, new Error("Runner connection closed"));
96
+ }
97
+ async #open(signal) {
98
+ if (signal?.aborted)
99
+ throw abortError();
100
+ const socket = createConnection(this.#options.endpoint);
101
+ this.#socket = socket;
102
+ return new Promise((resolve, reject) => {
103
+ let handshakeSettled = false;
104
+ const settleHandshake = (error) => {
105
+ if (handshakeSettled)
106
+ return;
107
+ handshakeSettled = true;
108
+ clearTimeout(timeout);
109
+ signal?.removeEventListener("abort", onAbort);
110
+ if (error === undefined)
111
+ resolve();
112
+ else {
113
+ this.#disconnectFor(socket, error);
114
+ reject(error);
115
+ }
116
+ };
117
+ const streamError = (error) => {
118
+ if (!handshakeSettled)
119
+ settleHandshake(error);
120
+ else if (this.#socket === socket)
121
+ this.#disconnectFor(socket, error);
122
+ };
123
+ const timeout = setTimeout(() => settleHandshake(new Error("Runner connection timed out")), this.#options.connectTimeoutMs ?? 10_000);
124
+ const onAbort = () => settleHandshake(abortError());
125
+ signal?.addEventListener("abort", onAbort, { once: true });
126
+ socket.on("error", streamError);
127
+ socket.on("close", () => {
128
+ streamError(new Error("Runner disconnected"));
129
+ });
130
+ const detach = attachJsonLineReader(socket, {
131
+ maxFrameBytes: this.#options.maxFrameBytes ?? DEFAULT_MAX_FRAME_BYTES,
132
+ onError: streamError,
133
+ onValue: (value) => {
134
+ if (!handshakeSettled) {
135
+ if (!isHelloAckFrame(value)) {
136
+ settleHandshake(new Error("Runner sent an invalid handshake response"));
137
+ return;
138
+ }
139
+ settleHandshake();
140
+ return;
141
+ }
142
+ if (this.#socket !== socket)
143
+ return;
144
+ this.#handleFrame(value);
145
+ },
146
+ });
147
+ this.#detachReader = detach;
148
+ socket.once("connect", () => {
149
+ void writeJsonLine(socket, {
150
+ version: PROTOCOL_VERSION,
151
+ type: "hello",
152
+ ...(this.#options.role === undefined ? {} : { role: this.#options.role }),
153
+ ...(this.#options.claimId === undefined ? {} : { claimId: this.#options.claimId }),
154
+ ...this.#options.identity,
155
+ token: this.#options.token,
156
+ }, this.#options.maxFrameBytes ?? DEFAULT_MAX_FRAME_BYTES).catch((error) => settleHandshake(toError(error)));
157
+ });
158
+ });
159
+ }
160
+ #handleFrame(value) {
161
+ if (isEventFrame(value)) {
162
+ for (const listener of this.#eventListeners) {
163
+ try {
164
+ listener(value.event);
165
+ }
166
+ catch (error) {
167
+ writeDiagnostic("controller", `event listener failed: ${errorMessage(error)}`);
168
+ }
169
+ }
170
+ return;
171
+ }
172
+ const socket = this.#socket;
173
+ if (!isResponseFrame(value)) {
174
+ this.#disconnectFor(socket, new Error("Runner sent a malformed frame"));
175
+ return;
176
+ }
177
+ const pending = this.#pending.get(value.id);
178
+ if (pending === undefined) {
179
+ if (this.#retired.has(value.id))
180
+ return;
181
+ this.#disconnectFor(socket, new Error(`Runner sent unknown response ID: ${value.id}`));
182
+ return;
183
+ }
184
+ this.#pending.delete(value.id);
185
+ this.#retire(value.id);
186
+ pending.cleanup();
187
+ if (value.ok)
188
+ pending.resolve(value.data);
189
+ else
190
+ pending.reject(new Error(`${value.error.code}: ${value.error.message}`));
191
+ }
192
+ /** Remembers an ID that finished locally so a late response is ignored. */
193
+ #retire(id) {
194
+ this.#retired.add(id);
195
+ if (this.#retired.size <= MAX_RETIRED_REQUEST_IDS)
196
+ return;
197
+ const oldest = this.#retired.values().next();
198
+ if (!oldest.done)
199
+ this.#retired.delete(oldest.value);
200
+ }
201
+ #disconnectFor(socket, error) {
202
+ if (socket !== undefined && this.#socket !== socket) {
203
+ socket.destroy();
204
+ return;
205
+ }
206
+ this.#socket = undefined;
207
+ this.#detachReader?.();
208
+ this.#detachReader = undefined;
209
+ if (socket !== undefined && !socket.destroyed)
210
+ socket.destroy();
211
+ const pending = [...this.#pending.values()];
212
+ this.#pending.clear();
213
+ for (const entry of pending) {
214
+ entry.cleanup();
215
+ entry.reject(error);
216
+ }
217
+ }
218
+ }
219
+ /** Sends one child report through a short-lived authenticated reporter connection. */
220
+ export async function sendReportToRunner(identity, report, signal) {
221
+ await sendReporterRequest(identity, "contact_parent", report, signal);
222
+ }
223
+ /** Sends one child lifecycle event through a short-lived authenticated reporter connection. */
224
+ export async function sendLifecycleToRunner(identity, payload, signal) {
225
+ await sendReporterRequest(identity, "report_lifecycle", payload, signal);
226
+ }
227
+ async function sendReporterRequest(identity, operation, payload, signal) {
228
+ const connection = new RunnerConnection({
229
+ endpoint: identity.endpoint,
230
+ identity,
231
+ token: identity.token,
232
+ role: "reporter",
233
+ });
234
+ try {
235
+ await connection.connect(signal);
236
+ await connection.request(operation, payload, signal === undefined ? {} : { signal });
237
+ }
238
+ finally {
239
+ connection.close();
240
+ }
241
+ }
242
+ /**
243
+ * Connects to a runner endpoint that may still be starting up.
244
+ *
245
+ * Readiness is never a fixed sleep: each attempt is a real connect plus
246
+ * identity handshake, so a resolved promise means the runner accepted this
247
+ * controller. Only connection-level failures inside the deadline are retried.
248
+ */
249
+ export async function connectWithRetry(connection, options = {}) {
250
+ const attempts = options.attempts ?? 40;
251
+ const delayMs = options.delayMs ?? 50;
252
+ let lastError = new Error("Runner connection was not attempted");
253
+ for (let attempt = 0; attempt < attempts; attempt += 1) {
254
+ if (options.signal?.aborted)
255
+ throw abortError();
256
+ try {
257
+ await connection.connect(options.signal);
258
+ return;
259
+ }
260
+ catch (error) {
261
+ lastError = toError(error);
262
+ if (lastError.name === "AbortError")
263
+ throw lastError;
264
+ if (attempt === attempts - 1)
265
+ break;
266
+ await sleep(delayMs, options.signal);
267
+ }
268
+ }
269
+ throw lastError;
270
+ }
271
+ function sleep(ms, signal) {
272
+ return new Promise((resolve, reject) => {
273
+ const timer = setTimeout(() => {
274
+ signal?.removeEventListener("abort", onAbort);
275
+ resolve();
276
+ }, ms);
277
+ const onAbort = () => {
278
+ clearTimeout(timer);
279
+ reject(abortError());
280
+ };
281
+ signal?.addEventListener("abort", onAbort, { once: true });
282
+ });
283
+ }
@@ -0,0 +1,10 @@
1
+ /**
2
+ * Runner-side shared diagnostics.
3
+ *
4
+ * The runner process owns the Pi child stdio: Pi stdout carries RPC JSON lines
5
+ * only, so every runner diagnostic goes to the runner process stderr.
6
+ */
7
+ export declare function writeDiagnostic(scope: string, message: string): void;
8
+ export declare function toError(value: unknown): Error;
9
+ export declare function errorMessage(value: unknown): string;
10
+ export declare function abortError(): Error;
@@ -0,0 +1,20 @@
1
+ /**
2
+ * Runner-side shared diagnostics.
3
+ *
4
+ * The runner process owns the Pi child stdio: Pi stdout carries RPC JSON lines
5
+ * only, so every runner diagnostic goes to the runner process stderr.
6
+ */
7
+ export function writeDiagnostic(scope, message) {
8
+ process.stderr.write(`pi-subagents ${scope}: ${message}\n`);
9
+ }
10
+ export function toError(value) {
11
+ return value instanceof Error ? value : new Error(String(value));
12
+ }
13
+ export function errorMessage(value) {
14
+ return value instanceof Error ? value.message : String(value);
15
+ }
16
+ export function abortError() {
17
+ const error = new Error("Operation aborted");
18
+ error.name = "AbortError";
19
+ return error;
20
+ }
@@ -0,0 +1,192 @@
1
+ export declare const PROTOCOL_VERSION: 1;
2
+ export declare const REGISTRY_VERSION: 1;
3
+ export type SubagentState = "starting" | "running" | "idle" | "done" | "stopped" | "failed";
4
+ export type ExecutionMode = "rpc" | "tui";
5
+ export type SendMode = "steer" | "follow_up" | "auto";
6
+ export type ThinkingLevel = "off" | "minimal" | "low" | "medium" | "high" | "xhigh" | "max";
7
+ export type LaunchValueSource = "agent" | "parent";
8
+ export type SubagentIntent = "active" | "stopped";
9
+ export type PersistenceState = "never_flushed" | "flushed";
10
+ export interface ChildIdentity {
11
+ readonly parentSessionId: string;
12
+ readonly subagentId: string;
13
+ readonly runtimeIdentity: string;
14
+ readonly endpoint: string;
15
+ readonly token: string;
16
+ }
17
+ /**
18
+ * The non-secret half of {@link ChildIdentity}: what a launch spec may contain and
19
+ * what may be persisted. The controller token is added by the launcher at spawn
20
+ * time and never written to a registry snapshot.
21
+ */
22
+ export interface ChildBridgeEnvironment {
23
+ readonly parentSessionId: string;
24
+ readonly subagentId: string;
25
+ readonly runtimeIdentity: string;
26
+ readonly endpoint: string;
27
+ }
28
+ export interface PiInvocation {
29
+ /** Executable or runtime binary, never resolved through a shell. */
30
+ readonly command: string;
31
+ /** Host-resolved leading argv (for example an absolute `cli.js`). Feature flags are appended by the launch builder. */
32
+ readonly args: readonly string[];
33
+ }
34
+ export interface ResolvedAgentIdentity {
35
+ readonly name: string;
36
+ readonly displayName?: string;
37
+ readonly description?: string;
38
+ readonly hidden: boolean;
39
+ readonly sourcePath: string;
40
+ readonly instructions: string;
41
+ }
42
+ export interface ResolvedModel {
43
+ readonly provider: string;
44
+ readonly id: string;
45
+ readonly source: LaunchValueSource;
46
+ }
47
+ export interface ResolvedThinking {
48
+ readonly level: ThinkingLevel;
49
+ readonly source: LaunchValueSource;
50
+ }
51
+ export interface ExtensionSelection {
52
+ readonly discovery: boolean;
53
+ readonly paths: readonly string[];
54
+ }
55
+ export interface SkillSelection {
56
+ readonly discovery: boolean;
57
+ readonly paths: readonly string[];
58
+ }
59
+ export interface EffectiveLaunchConfig {
60
+ /** Logical child id; the same value the child bridge reports back. */
61
+ readonly subagentId: string;
62
+ readonly invocation: PiInvocation;
63
+ readonly cwd: string;
64
+ readonly sessionId: string;
65
+ /** Directory Pi uses for this child's session file; always passed explicitly so recovery can find the file. */
66
+ readonly sessionDir: string;
67
+ /** Known only once the session has been flushed to disk. */
68
+ readonly sessionPath?: string;
69
+ readonly agent: ResolvedAgentIdentity;
70
+ readonly model: ResolvedModel;
71
+ readonly thinking: ResolvedThinking;
72
+ readonly tools: readonly string[];
73
+ readonly excludeTools: readonly string[];
74
+ readonly extensions: ExtensionSelection;
75
+ readonly skills: SkillSelection;
76
+ readonly prompt: string;
77
+ readonly bridgeExtensionPath: string;
78
+ /** Frozen at spawn. Interactive children do not auto-wake the parent except via contact_parent. */
79
+ readonly interactive: boolean;
80
+ }
81
+ export interface ResolvedAgentPolicy {
82
+ readonly agent: ResolvedAgentIdentity;
83
+ readonly model: ResolvedModel;
84
+ readonly thinking: ResolvedThinking;
85
+ readonly tools: readonly string[];
86
+ readonly excludeTools: readonly string[];
87
+ readonly extensions: ExtensionSelection;
88
+ readonly skills: SkillSelection;
89
+ readonly interactive: boolean;
90
+ }
91
+ export interface RuntimeMetadata {
92
+ readonly runtimeIdentity: string;
93
+ readonly endpoint: string;
94
+ readonly pid?: number;
95
+ }
96
+ export type RuntimeClaimKind = "reconnect" | "replacement";
97
+ /** Non-secret, durable ownership claim used to serialize reconnect and replacement. */
98
+ export interface RuntimeClaim {
99
+ readonly claimId: string;
100
+ readonly kind: RuntimeClaimKind;
101
+ readonly holderPid: number;
102
+ readonly runtimeIdentity: string;
103
+ readonly endpoint: string;
104
+ /** SHA-256 of the one-time controller token; the token itself is never persisted. */
105
+ readonly controllerTokenHash: string;
106
+ /** Set by the runner before it starts the Pi writer. */
107
+ readonly runnerPid?: number;
108
+ }
109
+ export interface UsageSummary {
110
+ readonly inputTokens: number;
111
+ readonly outputTokens: number;
112
+ readonly cacheReadTokens: number;
113
+ readonly cacheWriteTokens: number;
114
+ readonly costUsd: number | null;
115
+ readonly turns: number;
116
+ }
117
+ export interface SubagentRecord {
118
+ readonly subagentId: string;
119
+ readonly parentSessionId: string;
120
+ readonly revision: number;
121
+ readonly createdAt: string;
122
+ readonly updatedAt: string;
123
+ readonly sessionId: string;
124
+ readonly sessionPath?: string;
125
+ readonly cwd: string;
126
+ readonly initialTask: string;
127
+ readonly intent: SubagentIntent;
128
+ readonly state: SubagentState;
129
+ readonly mode: ExecutionMode;
130
+ readonly persistence: PersistenceState;
131
+ readonly launchConfig: EffectiveLaunchConfig;
132
+ readonly runtime?: RuntimeMetadata;
133
+ readonly claim?: RuntimeClaim;
134
+ readonly latestSummary?: string;
135
+ readonly usage?: UsageSummary;
136
+ readonly interrupted?: string;
137
+ readonly unacknowledgedInput?: string;
138
+ }
139
+ export interface PublicSubagent {
140
+ readonly id: string;
141
+ readonly agent: string;
142
+ readonly displayName?: string;
143
+ readonly state: SubagentState;
144
+ readonly mode: ExecutionMode;
145
+ readonly cwd: string;
146
+ readonly sessionId: string;
147
+ readonly summary?: string;
148
+ readonly usage?: UsageSummary;
149
+ readonly interrupted?: string;
150
+ readonly freshness: "live" | "last_known";
151
+ readonly interactive: boolean;
152
+ readonly model: ResolvedModel;
153
+ readonly thinking: ResolvedThinking;
154
+ readonly createdAt: string;
155
+ readonly updatedAt: string;
156
+ }
157
+ export interface OperationError {
158
+ readonly operation: string;
159
+ readonly childId?: string;
160
+ readonly reason: string;
161
+ readonly sideEffects: readonly string[];
162
+ readonly state?: SubagentState;
163
+ readonly safeToRetry: boolean;
164
+ }
165
+ export interface SpawnSubagentInput {
166
+ readonly task: string;
167
+ readonly agent: string;
168
+ readonly cwd?: string;
169
+ }
170
+ /** Child bridge tool name; agent tool policy must never remove it. */
171
+ export declare const CONTACT_PARENT_TOOL_NAME: "contact_parent";
172
+ /** Environment contract between a parent launch and the child branch of this extension. */
173
+ export declare const BRIDGE_ENVIRONMENT_KEYS: {
174
+ readonly parentSessionId: "PI_SUBAGENTS_PARENT_SESSION_ID";
175
+ readonly childId: "PI_SUBAGENTS_CHILD_ID";
176
+ readonly runtimeId: "PI_SUBAGENTS_RUNTIME_ID";
177
+ readonly endpoint: "PI_SUBAGENTS_ENDPOINT";
178
+ readonly token: "PI_SUBAGENTS_TOKEN";
179
+ };
180
+ /** Non-secret agent label for the child TUI identity line. Not part of handshake. */
181
+ export declare const CHILD_AGENT_ENV_KEY: "PI_SUBAGENTS_AGENT";
182
+ /** Bound Pi session id for the child branch. Not part of the runner handshake. */
183
+ export declare const CHILD_SESSION_ENV_KEY: "PI_SUBAGENTS_SESSION_ID";
184
+ /** Pi session ids reach argv and file names, so they must stay in Pi's accepted alphabet. */
185
+ export declare function isSessionId(value: unknown): value is string;
186
+ export declare function isThinkingLevel(value: unknown): value is ThinkingLevel;
187
+ export declare function isSubagentState(value: unknown): value is SubagentState;
188
+ export declare function isExecutionMode(value: unknown): value is ExecutionMode;
189
+ export declare function isSubagentIntent(value: unknown): value is SubagentIntent;
190
+ export declare function isPersistenceState(value: unknown): value is PersistenceState;
191
+ export declare function isLaunchValueSource(value: unknown): value is LaunchValueSource;
192
+ export declare function isRecord(value: unknown): value is Record<string, unknown>;
package/dist/domain.js ADDED
@@ -0,0 +1,63 @@
1
+ export const PROTOCOL_VERSION = 1;
2
+ export const REGISTRY_VERSION = 1;
3
+ /** Child bridge tool name; agent tool policy must never remove it. */
4
+ export const CONTACT_PARENT_TOOL_NAME = "contact_parent";
5
+ /** Environment contract between a parent launch and the child branch of this extension. */
6
+ export const BRIDGE_ENVIRONMENT_KEYS = {
7
+ parentSessionId: "PI_SUBAGENTS_PARENT_SESSION_ID",
8
+ childId: "PI_SUBAGENTS_CHILD_ID",
9
+ runtimeId: "PI_SUBAGENTS_RUNTIME_ID",
10
+ endpoint: "PI_SUBAGENTS_ENDPOINT",
11
+ token: "PI_SUBAGENTS_TOKEN",
12
+ };
13
+ /** Non-secret agent label for the child TUI identity line. Not part of handshake. */
14
+ export const CHILD_AGENT_ENV_KEY = "PI_SUBAGENTS_AGENT";
15
+ /** Bound Pi session id for the child branch. Not part of the runner handshake. */
16
+ export const CHILD_SESSION_ENV_KEY = "PI_SUBAGENTS_SESSION_ID";
17
+ const SESSION_ID_PATTERN = /^[A-Za-z0-9](?:[A-Za-z0-9._-]*[A-Za-z0-9])?$/;
18
+ /** Pi session ids reach argv and file names, so they must stay in Pi's accepted alphabet. */
19
+ export function isSessionId(value) {
20
+ return typeof value === "string" && SESSION_ID_PATTERN.test(value);
21
+ }
22
+ const THINKING_LEVELS = {
23
+ off: true,
24
+ minimal: true,
25
+ low: true,
26
+ medium: true,
27
+ high: true,
28
+ xhigh: true,
29
+ max: true,
30
+ };
31
+ export function isThinkingLevel(value) {
32
+ return typeof value === "string" && THINKING_LEVELS[value] === true;
33
+ }
34
+ const SUBAGENT_STATES = {
35
+ starting: true,
36
+ running: true,
37
+ idle: true,
38
+ done: true,
39
+ stopped: true,
40
+ failed: true,
41
+ };
42
+ export function isSubagentState(value) {
43
+ return typeof value === "string" && SUBAGENT_STATES[value] === true;
44
+ }
45
+ const EXECUTION_MODES = { rpc: true, tui: true };
46
+ export function isExecutionMode(value) {
47
+ return typeof value === "string" && EXECUTION_MODES[value] === true;
48
+ }
49
+ const SUBAGENT_INTENTS = { active: true, stopped: true };
50
+ export function isSubagentIntent(value) {
51
+ return typeof value === "string" && SUBAGENT_INTENTS[value] === true;
52
+ }
53
+ const PERSISTENCE_STATES = { never_flushed: true, flushed: true };
54
+ export function isPersistenceState(value) {
55
+ return typeof value === "string" && PERSISTENCE_STATES[value] === true;
56
+ }
57
+ const LAUNCH_VALUE_SOURCES = { agent: true, parent: true };
58
+ export function isLaunchValueSource(value) {
59
+ return typeof value === "string" && LAUNCH_VALUE_SOURCES[value] === true;
60
+ }
61
+ export function isRecord(value) {
62
+ return typeof value === "object" && value !== null && !Array.isArray(value);
63
+ }
@@ -0,0 +1,2 @@
1
+ import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
2
+ export default function piSubagentsExtension(pi: ExtensionAPI): void;