@hheei/pi-subagents 0.7.0 → 0.8.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.
- package/LICENSES/Apache-2.0.txt +202 -0
- package/NOTICE +10 -0
- package/README.md +13 -13
- package/dist/child-control.d.ts +0 -8
- package/dist/child-control.js +40 -8
- package/dist/commands.js +1 -1
- package/dist/controller-ipc.d.ts +38 -0
- package/dist/controller-ipc.js +292 -0
- package/dist/controller-main.d.ts +1 -0
- package/dist/controller-main.js +26 -0
- package/dist/controller-role.d.ts +40 -0
- package/dist/controller-role.js +37 -0
- package/dist/controller-socket.d.ts +6 -0
- package/dist/controller-socket.js +43 -0
- package/dist/controller.d.ts +25 -0
- package/dist/controller.js +217 -0
- package/dist/delegation.d.ts +73 -0
- package/dist/delegation.js +71 -0
- package/dist/domain.d.ts +20 -18
- package/dist/domain.js +6 -1
- package/dist/extension.js +43 -26
- package/dist/host-adapter.js +1 -1
- package/dist/launch-spec.d.ts +1 -3
- package/dist/launch-spec.js +38 -15
- package/dist/lease.d.ts +0 -1
- package/dist/lease.js +8 -11
- package/dist/mailbox.js +110 -34
- package/dist/manager.d.ts +52 -6
- package/dist/manager.js +568 -32
- package/dist/mcp/app.html +948 -0
- package/dist/mcp/delegate.d.ts +30 -0
- package/dist/mcp/delegate.js +44 -0
- package/dist/mcp/views.d.ts +5 -0
- package/dist/mcp/views.js +12 -0
- package/dist/mcp-server.d.ts +6 -0
- package/dist/mcp-server.js +287 -0
- package/dist/mcp-stdio.d.ts +2 -0
- package/dist/mcp-stdio.js +111 -0
- package/dist/process-lock.d.ts +1 -1
- package/dist/process-lock.js +4 -2
- package/dist/protocol.d.ts +5 -5
- package/dist/protocol.js +3 -4
- package/dist/receipts.d.ts +80 -0
- package/dist/receipts.js +326 -0
- package/dist/runtime.d.ts +5 -26
- package/dist/runtime.js +15 -57
- package/dist/session-bootstrap.js +2 -6
- package/dist/state-db.d.ts +11 -4
- package/dist/state-db.js +102 -24
- package/dist/state.js +12 -3
- package/dist/tool-activation.d.ts +2 -2
- package/dist/tool-activation.js +5 -5
- package/dist/tool-presentation.d.ts +2 -6
- package/dist/tool-presentation.js +75 -122
- package/dist/tools.d.ts +0 -3
- package/dist/tools.js +104 -132
- package/package.json +12 -5
- package/dist/json-lines.d.ts +0 -14
- package/dist/json-lines.js +0 -95
package/dist/launch-spec.js
CHANGED
|
@@ -1,8 +1,7 @@
|
|
|
1
|
-
import { existsSync } from "node:fs";
|
|
2
|
-
import { createRequire } from "node:module";
|
|
1
|
+
import { existsSync, readFileSync } from "node:fs";
|
|
3
2
|
import { basename, dirname, join } from "node:path";
|
|
4
3
|
import { fileURLToPath } from "node:url";
|
|
5
|
-
import { BRIDGE_ENVIRONMENT_KEYS, CHILD_AGENT_ENV_KEY, CHILD_SESSION_ENV_KEY, CHILD_TITLE_ENV_KEY, CONTACT_PARENT_TOOL_NAME, DEFAULT_PI_TOOLS, isSessionId, } from "./domain.js";
|
|
4
|
+
import { BRIDGE_ENVIRONMENT_KEYS, CHILD_AGENT_ENV_KEY, CHILD_SESSION_ENV_KEY, CHILD_TITLE_ENV_KEY, CONTACT_PARENT_TOOL_NAME, DEFAULT_PI_TOOLS, isSessionId, STANDALONE_CONTROLLER_ENV_KEY, } from "./domain.js";
|
|
6
5
|
export function stripHindsightContent(text) {
|
|
7
6
|
return text
|
|
8
7
|
.replace(/\s*<hindsight-recall>[\s\S]*?<\/hindsight-recall>\s*/gi, (match) => match.includes("\n") ? "\n" : " ")
|
|
@@ -22,17 +21,13 @@ function childBridgePrompt() {
|
|
|
22
21
|
"Do not send empty status pings.",
|
|
23
22
|
"When the work is done, output your final answer and findings directly in text in your response. The harness will automatically capture and deliver your output.",
|
|
24
23
|
"Do NOT call contact_parent on success or task completion.",
|
|
25
|
-
"After blocked, wait for a parent
|
|
24
|
+
"After blocked, wait for a parent agents_delegate. Do not invent new authority.",
|
|
26
25
|
"Reports reach the parent as delegated results, not as new user authorization.",
|
|
27
26
|
].join("\n");
|
|
28
27
|
}
|
|
29
28
|
export function assembleChildPrompt(instructions) {
|
|
30
29
|
return `${instructions.trim()}\n\n${childBridgePrompt()}`;
|
|
31
30
|
}
|
|
32
|
-
/** Adds the per-runtime controller token. Tokens stay out of launch spec snapshots and the registry. */
|
|
33
|
-
export function withBridgeToken(env, token) {
|
|
34
|
-
return { ...env, [BRIDGE_ENVIRONMENT_KEYS.token]: token };
|
|
35
|
-
}
|
|
36
31
|
let cachedBridgeExtensionPath;
|
|
37
32
|
/**
|
|
38
33
|
* Absolute path to this package's extension entry, which every child loads with
|
|
@@ -56,15 +51,36 @@ export function resolveBridgeExtensionPath() {
|
|
|
56
51
|
}
|
|
57
52
|
function resolveBundledPiCli() {
|
|
58
53
|
try {
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
54
|
+
// package.json is not an exported subpath, and the bin moved to dist/bundle/cli.js.
|
|
55
|
+
let dir = dirname(fileURLToPath(import.meta.resolve("@earendil-works/pi-coding-agent")));
|
|
56
|
+
for (let depth = 0; depth < 4; depth += 1) {
|
|
57
|
+
const manifestPath = join(dir, "package.json");
|
|
58
|
+
if (existsSync(manifestPath)) {
|
|
59
|
+
const cliPath = join(dir, piBin(JSON.parse(readFileSync(manifestPath, "utf8"))));
|
|
60
|
+
return existsSync(cliPath) ? cliPath : undefined;
|
|
61
|
+
}
|
|
62
|
+
const parent = dirname(dir);
|
|
63
|
+
if (parent === dir)
|
|
64
|
+
return undefined;
|
|
65
|
+
dir = parent;
|
|
66
|
+
}
|
|
64
67
|
}
|
|
65
68
|
catch {
|
|
66
69
|
return undefined;
|
|
67
70
|
}
|
|
71
|
+
return undefined;
|
|
72
|
+
}
|
|
73
|
+
function piBin(manifest) {
|
|
74
|
+
if (typeof manifest !== "object" || manifest === null || !("bin" in manifest)) {
|
|
75
|
+
throw new Error("Installed Pi package has no bin");
|
|
76
|
+
}
|
|
77
|
+
const bin = manifest.bin;
|
|
78
|
+
if (typeof bin === "string")
|
|
79
|
+
return bin;
|
|
80
|
+
if (typeof bin === "object" && bin !== null && "pi" in bin && typeof bin.pi === "string") {
|
|
81
|
+
return bin.pi;
|
|
82
|
+
}
|
|
83
|
+
throw new Error("Installed Pi package has no pi bin");
|
|
68
84
|
}
|
|
69
85
|
/**
|
|
70
86
|
* Re-invokes the host Pi CLI instead of guessing a binary on PATH. Extensions load
|
|
@@ -85,7 +101,13 @@ export function resolvePiInvocation() {
|
|
|
85
101
|
const execPath = process.execPath;
|
|
86
102
|
const currentScript = process.argv[1];
|
|
87
103
|
const isBunVirtualScript = currentScript?.startsWith("/$bunfs/root/") ?? false;
|
|
88
|
-
|
|
104
|
+
// The standalone controller is itself a node script. Reusing argv[1] would spawn another
|
|
105
|
+
// controller instead of Pi, so the child never opens the parent bridge.
|
|
106
|
+
const isControllerEntry = currentScript !== undefined && basename(currentScript).startsWith("controller-main.");
|
|
107
|
+
if (currentScript !== undefined &&
|
|
108
|
+
!isBunVirtualScript &&
|
|
109
|
+
!isControllerEntry &&
|
|
110
|
+
existsSync(currentScript)) {
|
|
89
111
|
return { command: execPath, args: [currentScript] };
|
|
90
112
|
}
|
|
91
113
|
const execName = basename(execPath).toLowerCase();
|
|
@@ -171,8 +193,9 @@ export function buildLaunchSpec(options) {
|
|
|
171
193
|
argv.push("--tools", Array.from(new Set(effectiveTools)).join(","));
|
|
172
194
|
if (excludeTools.length > 0)
|
|
173
195
|
argv.push("--exclude-tools", Array.from(new Set(excludeTools)).join(","));
|
|
174
|
-
if (!config.extensions.discovery)
|
|
196
|
+
if (!config.extensions.discovery || process.env[STANDALONE_CONTROLLER_ENV_KEY] === "1") {
|
|
175
197
|
argv.push("--no-extensions");
|
|
198
|
+
}
|
|
176
199
|
for (const path of config.extensions.paths)
|
|
177
200
|
argv.push("-e", path);
|
|
178
201
|
if (!config.skills.discovery)
|
package/dist/lease.d.ts
CHANGED
|
@@ -73,7 +73,6 @@ export declare function createConversationLease(options: {
|
|
|
73
73
|
readonly registry: RegistryStore;
|
|
74
74
|
readonly hosts: HostDirectory;
|
|
75
75
|
readonly processes: ProcessDirectory;
|
|
76
|
-
readonly forgetRuntime?: (runtimeIdentity: string) => void;
|
|
77
76
|
}): ConversationLease;
|
|
78
77
|
export declare function commitLease(registry: Pick<SubagentRegistry, "get" | "update">, id: string, edge: LeaseEdge, expectedRevision?: number, expectedRuntimeIdentity?: string): Promise<SubagentRecord>;
|
|
79
78
|
/** `undefined` means the edge changes nothing, so the caller must not write. */
|
package/dist/lease.js
CHANGED
|
@@ -21,7 +21,7 @@ export const LEASE_EDGES = [
|
|
|
21
21
|
"left-session",
|
|
22
22
|
];
|
|
23
23
|
export function createConversationLease(options) {
|
|
24
|
-
const { registry, hosts, processes
|
|
24
|
+
const { registry, hosts, processes } = options;
|
|
25
25
|
return {
|
|
26
26
|
commit(id, edge, expectedRevision) {
|
|
27
27
|
return commitLease(registry, id, edge, expectedRevision);
|
|
@@ -30,7 +30,7 @@ export function createConversationLease(options) {
|
|
|
30
30
|
return registry.list();
|
|
31
31
|
},
|
|
32
32
|
ready() {
|
|
33
|
-
return reconcile(registry, hosts, processes
|
|
33
|
+
return reconcile(registry, hosts, processes);
|
|
34
34
|
},
|
|
35
35
|
};
|
|
36
36
|
}
|
|
@@ -126,7 +126,7 @@ function applyRuntimeGone(record, outcome) {
|
|
|
126
126
|
const rest = record.runtime === undefined ? record : omitRuntime(record);
|
|
127
127
|
return { ...rest, state: "done" };
|
|
128
128
|
}
|
|
129
|
-
async function reconcile(registry, hosts, processes
|
|
129
|
+
async function reconcile(registry, hosts, processes) {
|
|
130
130
|
const list = await hosts.list();
|
|
131
131
|
if (!list.known)
|
|
132
132
|
return { ok: false, reason: "unknown" };
|
|
@@ -147,7 +147,7 @@ async function reconcile(registry, hosts, processes, forgetRuntime) {
|
|
|
147
147
|
}
|
|
148
148
|
if (held)
|
|
149
149
|
continue;
|
|
150
|
-
await releaseRuntime(registry, record, { type: "runtime-gone", outcome: "drop" }
|
|
150
|
+
await releaseRuntime(registry, record, { type: "runtime-gone", outcome: "drop" });
|
|
151
151
|
released.add(record.subagentId);
|
|
152
152
|
}
|
|
153
153
|
const records = all.filter((record) => !released.has(record.subagentId));
|
|
@@ -193,7 +193,7 @@ async function reconcile(registry, hosts, processes, forgetRuntime) {
|
|
|
193
193
|
await commitLease(registry, item.record.subagentId, { type: "stop-intent" });
|
|
194
194
|
stopped.push(item.record.subagentId);
|
|
195
195
|
}
|
|
196
|
-
await releaseRuntime(registry, item.record, { type: "runtime-gone" }
|
|
196
|
+
await releaseRuntime(registry, item.record, { type: "runtime-gone" });
|
|
197
197
|
}
|
|
198
198
|
}
|
|
199
199
|
for (const record of records) {
|
|
@@ -204,7 +204,7 @@ async function reconcile(registry, hosts, processes, forgetRuntime) {
|
|
|
204
204
|
rebound.push(record.subagentId);
|
|
205
205
|
continue;
|
|
206
206
|
}
|
|
207
|
-
await releaseRuntime(registry, record, { type: "runtime-gone", outcome: "drop" }
|
|
207
|
+
await releaseRuntime(registry, record, { type: "runtime-gone", outcome: "drop" });
|
|
208
208
|
released.add(record.subagentId);
|
|
209
209
|
}
|
|
210
210
|
if (!held && parentSessionId !== undefined && stored !== list.generation)
|
|
@@ -215,11 +215,8 @@ function panelId(record) {
|
|
|
215
215
|
const attachment = record.runtime?.attachment;
|
|
216
216
|
return attachment?.kind === "panel" ? attachment.id : undefined;
|
|
217
217
|
}
|
|
218
|
-
async function releaseRuntime(registry, record, edge
|
|
219
|
-
|
|
220
|
-
const next = await commitLease(registry, record.subagentId, edge);
|
|
221
|
-
if (identity !== undefined && next.runtime === undefined)
|
|
222
|
-
forgetRuntime?.(identity);
|
|
218
|
+
async function releaseRuntime(registry, record, edge) {
|
|
219
|
+
await commitLease(registry, record.subagentId, edge);
|
|
223
220
|
}
|
|
224
221
|
function omitRuntime(record) {
|
|
225
222
|
const { runtime: _runtime, ...rest } = record;
|
package/dist/mailbox.js
CHANGED
|
@@ -3,8 +3,8 @@ import { readdirSync } from "node:fs";
|
|
|
3
3
|
import { dirname, join } from "node:path";
|
|
4
4
|
import { errorMessage } from "@hheei/pi-ext-core";
|
|
5
5
|
import { holdProcessLock, processLockHeld, processLockPath, } from "./process-lock.js";
|
|
6
|
-
import { BridgeError } from "./protocol.js";
|
|
7
|
-
import { claimCommand, claimParentCommands, deleteCommand, enqueueCommand, finishCommand, hasPendingCommand, openStateDb, readCommand, requeueAbandonedParentCommands, withWrite, } from "./state-db.js";
|
|
6
|
+
import { BridgeError, DEFAULT_MAX_BUFFERED_EVENTS } from "./protocol.js";
|
|
7
|
+
import { claimCommand, claimParentCommands, deleteCommand, deleteSettledCommands, enqueueCommand, finishCommand, hasPendingCommand, openStateDb, readCommand, requeueAbandonedParentCommands, trimEventBacklog, withWrite, } from "./state-db.js";
|
|
8
8
|
const DEFAULT_POLL_MS = 50;
|
|
9
9
|
const DEFAULT_REQUEST_TIMEOUT_MS = 30_000;
|
|
10
10
|
export function createMailboxTransport(options) {
|
|
@@ -13,6 +13,8 @@ export function createMailboxTransport(options) {
|
|
|
13
13
|
const seen = new Set();
|
|
14
14
|
const eventListeners = new Set();
|
|
15
15
|
const connectionListeners = new Set();
|
|
16
|
+
/** One lifetime per transport: closing it settles every request still waiting on a reply. */
|
|
17
|
+
const lifetime = new AbortController();
|
|
16
18
|
let closed = false;
|
|
17
19
|
let polling = false;
|
|
18
20
|
const emitConnection = (childId, connected) => {
|
|
@@ -145,9 +147,12 @@ export function createMailboxTransport(options) {
|
|
|
145
147
|
};
|
|
146
148
|
withWrite(db, () => {
|
|
147
149
|
requeueAbandonedParentCommands(db, options.parentSessionId);
|
|
150
|
+
deleteSettledCommands(db, options.parentSessionId);
|
|
148
151
|
});
|
|
149
152
|
const timer = setInterval(() => {
|
|
150
|
-
void takeParentCommands()
|
|
153
|
+
void takeParentCommands().catch((error) => {
|
|
154
|
+
options.diagnose?.(`could not take parent commands: ${errorMessage(error)}`);
|
|
155
|
+
});
|
|
151
156
|
}, pollMs);
|
|
152
157
|
timer.unref?.();
|
|
153
158
|
return {
|
|
@@ -172,7 +177,12 @@ export function createMailboxTransport(options) {
|
|
|
172
177
|
payload: JSON.stringify(payload ?? null),
|
|
173
178
|
createdAt: Date.now(),
|
|
174
179
|
});
|
|
175
|
-
return waitForCommand(db, id,
|
|
180
|
+
return waitForCommand(db, id, {
|
|
181
|
+
timeoutMs: requestOptions?.timeoutMs ?? DEFAULT_REQUEST_TIMEOUT_MS,
|
|
182
|
+
pollMs,
|
|
183
|
+
signal: untilClosed(requestOptions?.signal, lifetime.signal),
|
|
184
|
+
...(options.diagnose === undefined ? {} : { diagnose: options.diagnose }),
|
|
185
|
+
});
|
|
176
186
|
},
|
|
177
187
|
onEvent(listener) {
|
|
178
188
|
eventListeners.add(listener);
|
|
@@ -206,6 +216,7 @@ export function createMailboxTransport(options) {
|
|
|
206
216
|
close() {
|
|
207
217
|
closed = true;
|
|
208
218
|
clearInterval(timer);
|
|
219
|
+
lifetime.abort(new BridgeError("closed", "The mailbox transport was closed"));
|
|
209
220
|
},
|
|
210
221
|
};
|
|
211
222
|
}
|
|
@@ -213,6 +224,7 @@ export function attachChildMailbox(options) {
|
|
|
213
224
|
let db;
|
|
214
225
|
let processLock;
|
|
215
226
|
const pollMs = options.pollMs ?? DEFAULT_POLL_MS;
|
|
227
|
+
const lifetime = new AbortController();
|
|
216
228
|
let closed = false;
|
|
217
229
|
let started = false;
|
|
218
230
|
let working = false;
|
|
@@ -263,10 +275,14 @@ export function attachChildMailbox(options) {
|
|
|
263
275
|
}
|
|
264
276
|
started = true;
|
|
265
277
|
timer = setInterval(() => {
|
|
266
|
-
void tick()
|
|
278
|
+
void tick().catch((error) => {
|
|
279
|
+
options.diagnose?.(`could not take a parent command: ${errorMessage(error)}`);
|
|
280
|
+
});
|
|
267
281
|
}, pollMs);
|
|
268
282
|
timer.unref?.();
|
|
269
|
-
void tick()
|
|
283
|
+
void tick().catch((error) => {
|
|
284
|
+
options.diagnose?.(`could not take a parent command: ${errorMessage(error)}`);
|
|
285
|
+
});
|
|
270
286
|
},
|
|
271
287
|
report(operation, payload, signal) {
|
|
272
288
|
const current = database();
|
|
@@ -280,27 +296,42 @@ export function attachChildMailbox(options) {
|
|
|
280
296
|
payload: JSON.stringify(payload ?? null),
|
|
281
297
|
createdAt: Date.now(),
|
|
282
298
|
});
|
|
283
|
-
return waitForCommand(current, id,
|
|
299
|
+
return waitForCommand(current, id, {
|
|
300
|
+
timeoutMs: DEFAULT_REQUEST_TIMEOUT_MS,
|
|
301
|
+
pollMs,
|
|
302
|
+
signal: untilClosed(signal, lifetime.signal),
|
|
303
|
+
...(options.diagnose === undefined ? {} : { diagnose: options.diagnose }),
|
|
304
|
+
});
|
|
284
305
|
},
|
|
285
306
|
sendEvent(event) {
|
|
286
307
|
if (db === undefined)
|
|
287
308
|
return;
|
|
288
309
|
const current = db;
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
310
|
+
try {
|
|
311
|
+
withWrite(current, () => {
|
|
312
|
+
enqueueCommand(current, {
|
|
313
|
+
id: randomUUID(),
|
|
314
|
+
parentSessionId: options.parentSessionId,
|
|
315
|
+
subagentId: options.subagentId,
|
|
316
|
+
direction: "to_parent",
|
|
317
|
+
operation: "event",
|
|
318
|
+
payload: JSON.stringify(event ?? null),
|
|
319
|
+
createdAt: Date.now(),
|
|
320
|
+
});
|
|
321
|
+
trimEventBacklog(current, options.parentSessionId, options.subagentId, DEFAULT_MAX_BUFFERED_EVENTS);
|
|
322
|
+
});
|
|
323
|
+
}
|
|
324
|
+
catch (error) {
|
|
325
|
+
// An advisory frame is never worth failing a turn over.
|
|
326
|
+
options.diagnose?.(`could not queue an event: ${errorMessage(error)}`);
|
|
327
|
+
}
|
|
298
328
|
},
|
|
299
329
|
close() {
|
|
300
330
|
closed = true;
|
|
301
331
|
started = false;
|
|
302
332
|
if (timer !== undefined)
|
|
303
333
|
clearInterval(timer);
|
|
334
|
+
lifetime.abort(new BridgeError("closed", "The child mailbox was closed"));
|
|
304
335
|
processLock?.release();
|
|
305
336
|
processLock = undefined;
|
|
306
337
|
},
|
|
@@ -322,44 +353,89 @@ async function answer(db, command, handleRequest, diagnose) {
|
|
|
322
353
|
});
|
|
323
354
|
}
|
|
324
355
|
}
|
|
325
|
-
function waitForCommand(db, id,
|
|
356
|
+
function waitForCommand(db, id, options) {
|
|
326
357
|
const started = Date.now();
|
|
327
358
|
return new Promise((resolve, reject) => {
|
|
328
|
-
const
|
|
329
|
-
|
|
330
|
-
clearInterval(timer);
|
|
359
|
+
const drop = () => {
|
|
360
|
+
try {
|
|
331
361
|
withWrite(db, () => {
|
|
332
362
|
deleteCommand(db, id);
|
|
333
363
|
});
|
|
334
|
-
|
|
364
|
+
}
|
|
365
|
+
catch (error) {
|
|
366
|
+
options.diagnose?.(`could not drop command ${id}: ${errorMessage(error)}`);
|
|
367
|
+
}
|
|
368
|
+
};
|
|
369
|
+
const finish = () => {
|
|
370
|
+
clearInterval(timer);
|
|
371
|
+
options.signal?.removeEventListener("abort", onAbort);
|
|
372
|
+
};
|
|
373
|
+
const onAbort = () => {
|
|
374
|
+
finish();
|
|
375
|
+
drop();
|
|
376
|
+
reject(abortReason(options.signal));
|
|
377
|
+
};
|
|
378
|
+
const timer = setInterval(() => {
|
|
379
|
+
// The deadline is read before the row, so a row another writer deleted can never leave
|
|
380
|
+
// this waiter polling forever.
|
|
381
|
+
const expired = Date.now() - started >= options.timeoutMs;
|
|
382
|
+
let command;
|
|
383
|
+
try {
|
|
384
|
+
command = readCommand(db, id);
|
|
385
|
+
}
|
|
386
|
+
catch (error) {
|
|
387
|
+
finish();
|
|
388
|
+
reject(new BridgeError("state_unavailable", errorMessage(error)));
|
|
335
389
|
return;
|
|
336
390
|
}
|
|
337
|
-
|
|
338
|
-
|
|
391
|
+
if (command === undefined) {
|
|
392
|
+
if (!expired)
|
|
393
|
+
return;
|
|
394
|
+
finish();
|
|
395
|
+
reject(new BridgeError("timeout", `No answer for ${id} after ${options.timeoutMs}ms`));
|
|
339
396
|
return;
|
|
340
|
-
if (command.status === "done" ||
|
|
341
|
-
command.status === "failed" ||
|
|
342
|
-
Date.now() - started >= timeoutMs) {
|
|
343
|
-
clearInterval(timer);
|
|
344
|
-
withWrite(db, () => {
|
|
345
|
-
deleteCommand(db, command.id);
|
|
346
|
-
});
|
|
347
397
|
}
|
|
348
398
|
if (command.status === "done") {
|
|
349
|
-
|
|
399
|
+
finish();
|
|
400
|
+
drop();
|
|
401
|
+
try {
|
|
402
|
+
resolve(command.result === null ? null : JSON.parse(command.result));
|
|
403
|
+
}
|
|
404
|
+
catch (error) {
|
|
405
|
+
reject(new BridgeError("malformed_result", errorMessage(error)));
|
|
406
|
+
}
|
|
350
407
|
return;
|
|
351
408
|
}
|
|
352
409
|
if (command.status === "failed") {
|
|
410
|
+
finish();
|
|
411
|
+
drop();
|
|
353
412
|
reject(new BridgeError(command.errorCode ?? "request_failed", command.error ?? "The command failed"));
|
|
354
413
|
return;
|
|
355
414
|
}
|
|
356
|
-
if (
|
|
357
|
-
|
|
415
|
+
if (expired) {
|
|
416
|
+
finish();
|
|
417
|
+
drop();
|
|
418
|
+
reject(new BridgeError("timeout", `No answer for ${id} after ${options.timeoutMs}ms`));
|
|
358
419
|
}
|
|
359
|
-
}, pollMs);
|
|
420
|
+
}, options.pollMs);
|
|
360
421
|
timer.unref?.();
|
|
422
|
+
if (options.signal?.aborted === true) {
|
|
423
|
+
onAbort();
|
|
424
|
+
return;
|
|
425
|
+
}
|
|
426
|
+
options.signal?.addEventListener("abort", onAbort, { once: true });
|
|
361
427
|
});
|
|
362
428
|
}
|
|
429
|
+
/** A caller's own cancellation and this transport's lifetime both settle a waiting request. */
|
|
430
|
+
function untilClosed(signal, lifetime) {
|
|
431
|
+
return signal === undefined ? lifetime : AbortSignal.any([signal, lifetime]);
|
|
432
|
+
}
|
|
433
|
+
function abortReason(signal) {
|
|
434
|
+
const reason = signal?.reason;
|
|
435
|
+
if (reason instanceof BridgeError)
|
|
436
|
+
return reason;
|
|
437
|
+
return new BridgeError("aborted", "The mailbox request was cancelled");
|
|
438
|
+
}
|
|
363
439
|
function delay(ms) {
|
|
364
440
|
return new Promise((resolve) => {
|
|
365
441
|
const timer = setTimeout(resolve, ms);
|
package/dist/manager.d.ts
CHANGED
|
@@ -1,13 +1,15 @@
|
|
|
1
1
|
import { type ChildRuntime } from "./child-process.js";
|
|
2
|
-
import
|
|
2
|
+
import { type DashboardView, type DelegationView } from "./delegation.js";
|
|
3
|
+
import type { DelegateSubagentInput, EffectiveLaunchConfig, OperationError, Presentation, PublicSubagent, RuntimeAttachment, SpawnSubagentInput, SubagentRecord, SubagentState } from "./domain.js";
|
|
3
4
|
import type { HostAttachment } from "./host-adapter.js";
|
|
4
5
|
import type { HostDirectory, LeaseReady } from "./lease.js";
|
|
5
6
|
import { type BridgeOperation } from "./protocol.js";
|
|
7
|
+
import { type ReceiptSettlement } from "./receipts.js";
|
|
6
8
|
import type { SubagentRegistry } from "./registry.js";
|
|
7
|
-
import type { LaunchOutcome
|
|
9
|
+
import type { LaunchOutcome } from "./runtime.js";
|
|
8
10
|
/**
|
|
9
11
|
* How long an idle child keeps its runtime. A background child holds a whole Pi process and its
|
|
10
|
-
* session lock, so an idle one is reclaimed after this window and a later `
|
|
12
|
+
* session lock, so an idle one is reclaimed after this window and a later `agents_delegate` starts it
|
|
11
13
|
* again from the same session.
|
|
12
14
|
*/
|
|
13
15
|
export declare const SUBAGENT_IDLE_TIMEOUT_MS = 15000;
|
|
@@ -75,7 +77,6 @@ export interface ManagerDependencies {
|
|
|
75
77
|
readonly idleTimeoutMs?: number;
|
|
76
78
|
readonly failedPanelCloseTimeoutMs?: number;
|
|
77
79
|
readonly channel?: ParentChannel;
|
|
78
|
-
readonly tokens?: RuntimeTokenStore;
|
|
79
80
|
}
|
|
80
81
|
/**
|
|
81
82
|
* How this parent presents children, as the manager needs it: something that opens a panel for a
|
|
@@ -93,6 +94,18 @@ export interface SpawnResult {
|
|
|
93
94
|
/** Set when the child did not run where it would have by default. */
|
|
94
95
|
readonly note?: string;
|
|
95
96
|
}
|
|
97
|
+
/** Accepted dispatch, not a completed turn. Future MCP Tasks bind a receipt to this submission. */
|
|
98
|
+
export interface DelegationSubmission {
|
|
99
|
+
readonly child: PublicSubagent;
|
|
100
|
+
readonly action: "spawn" | "reuse";
|
|
101
|
+
readonly note?: string;
|
|
102
|
+
/** Pi work version that may settle this input. Same-turn steers share one version. */
|
|
103
|
+
readonly workVersion?: number;
|
|
104
|
+
/** The compaction this input controls; such an input never opens a turn. */
|
|
105
|
+
readonly controlId?: string;
|
|
106
|
+
/** Set when that version already settled before the receipt row was written. */
|
|
107
|
+
readonly settled?: ReceiptSettlement;
|
|
108
|
+
}
|
|
96
109
|
export interface RecoveryResult {
|
|
97
110
|
readonly recovered: readonly string[];
|
|
98
111
|
readonly failures: readonly {
|
|
@@ -118,16 +131,49 @@ export declare class SubagentManager {
|
|
|
118
131
|
*/
|
|
119
132
|
closeLocalConnections(): void;
|
|
120
133
|
/** Reads a child's session entries; used by transcript inspection fallbacks. */
|
|
121
|
-
getEntries(id: string
|
|
134
|
+
getEntries(id: string, range?: {
|
|
135
|
+
readonly cursor?: number;
|
|
136
|
+
readonly limit?: number;
|
|
137
|
+
}): Promise<{
|
|
138
|
+
readonly entries: readonly unknown[];
|
|
139
|
+
readonly nextCursor?: number;
|
|
140
|
+
} | undefined>;
|
|
122
141
|
/**
|
|
123
142
|
* Adopts the children that are still running. Nothing is launched here: a child that survived a
|
|
124
143
|
* parent restart reconnects its bridge by itself, and a child that did not is simply not live
|
|
125
144
|
* until a later send starts it from its recorded session.
|
|
126
145
|
*/
|
|
127
146
|
recover(): Promise<RecoveryResult>;
|
|
147
|
+
/**
|
|
148
|
+
* One dispatch path for MCP's new-child and existing-child forms. This only confirms input
|
|
149
|
+
* delivery; waiting for the specific turn's final report belongs to the MCP delegation receipt,
|
|
150
|
+
* not to spawn/send or the child's durable identity.
|
|
151
|
+
*/
|
|
152
|
+
submitDelegation(input: DelegateSubagentInput, onTarget?: (childId: string) => void): Promise<DelegationSubmission | OperationError>;
|
|
153
|
+
/**
|
|
154
|
+
* The acceptance path both consumers share: reserve this delegation's revision, dispatch it into
|
|
155
|
+
* the agent session, then either wait for that revision's settlement or return its identity.
|
|
156
|
+
* Waiting never holds the per-agent chain, so settlements and later steers still get in.
|
|
157
|
+
*/
|
|
158
|
+
delegate(input: DelegateSubagentInput & {
|
|
159
|
+
readonly wait?: boolean;
|
|
160
|
+
}, signal?: AbortSignal, onAccepted?: (view: DelegationView) => void): Promise<DelegationView | OperationError>;
|
|
128
161
|
spawn(input: SpawnSubagentInput): Promise<SpawnResult | OperationError>;
|
|
129
|
-
send(id: string, rawMessage: string,
|
|
162
|
+
send(id: string, rawMessage: string, signal?: AbortSignal): Promise<OperationError | PublicSubagent>;
|
|
130
163
|
get(id: string): Promise<PublicSubagent | OperationError>;
|
|
164
|
+
/**
|
|
165
|
+
* One delegation of one agent, by revision: the newest by default. The result body is only read
|
|
166
|
+
* when it is asked for, and then only within the shared output budget.
|
|
167
|
+
*/
|
|
168
|
+
inspect(input: {
|
|
169
|
+
readonly id: string;
|
|
170
|
+
readonly revision?: number;
|
|
171
|
+
readonly includeOutput?: boolean;
|
|
172
|
+
}): Promise<DelegationView | OperationError>;
|
|
173
|
+
/** Every agent of this parent: runtime state now, latest delegation status, no result bodies. */
|
|
174
|
+
dashboard(): Promise<DashboardView>;
|
|
175
|
+
/** Stops the runtime and reports the same projection every other read returns. */
|
|
176
|
+
stopAgent(id: string): Promise<DelegationView | OperationError>;
|
|
131
177
|
list(): Promise<readonly PublicSubagent[]>;
|
|
132
178
|
stop(id: string): Promise<OperationError | PublicSubagent>;
|
|
133
179
|
contactParent(id: string, reason: string, message: string, knownRecord?: SubagentRecord): Promise<OperationError | {
|