@hheei/pi-subagents 0.6.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.
Files changed (60) hide show
  1. package/LICENSES/Apache-2.0.txt +202 -0
  2. package/NOTICE +10 -0
  3. package/README.md +14 -12
  4. package/dist/child-control.d.ts +0 -8
  5. package/dist/child-control.js +66 -3
  6. package/dist/commands.js +1 -1
  7. package/dist/controller-ipc.d.ts +38 -0
  8. package/dist/controller-ipc.js +292 -0
  9. package/dist/controller-main.d.ts +1 -0
  10. package/dist/controller-main.js +26 -0
  11. package/dist/controller-role.d.ts +40 -0
  12. package/dist/controller-role.js +37 -0
  13. package/dist/controller-socket.d.ts +6 -0
  14. package/dist/controller-socket.js +43 -0
  15. package/dist/controller.d.ts +25 -0
  16. package/dist/controller.js +217 -0
  17. package/dist/delegation.d.ts +73 -0
  18. package/dist/delegation.js +71 -0
  19. package/dist/domain.d.ts +20 -18
  20. package/dist/domain.js +6 -1
  21. package/dist/extension.js +43 -26
  22. package/dist/host-adapter.js +1 -1
  23. package/dist/launch-spec.d.ts +1 -3
  24. package/dist/launch-spec.js +39 -15
  25. package/dist/lease.d.ts +0 -1
  26. package/dist/lease.js +8 -11
  27. package/dist/mailbox.js +110 -34
  28. package/dist/manager.d.ts +52 -6
  29. package/dist/manager.js +585 -48
  30. package/dist/mcp/app.html +948 -0
  31. package/dist/mcp/delegate.d.ts +30 -0
  32. package/dist/mcp/delegate.js +44 -0
  33. package/dist/mcp/views.d.ts +5 -0
  34. package/dist/mcp/views.js +12 -0
  35. package/dist/mcp-server.d.ts +6 -0
  36. package/dist/mcp-server.js +287 -0
  37. package/dist/mcp-stdio.d.ts +2 -0
  38. package/dist/mcp-stdio.js +111 -0
  39. package/dist/process-lock.d.ts +1 -1
  40. package/dist/process-lock.js +4 -2
  41. package/dist/protocol.d.ts +5 -5
  42. package/dist/protocol.js +3 -4
  43. package/dist/receipts.d.ts +80 -0
  44. package/dist/receipts.js +326 -0
  45. package/dist/runtime.d.ts +5 -26
  46. package/dist/runtime.js +15 -57
  47. package/dist/session-bootstrap.js +2 -6
  48. package/dist/state-db.d.ts +11 -4
  49. package/dist/state-db.js +102 -24
  50. package/dist/state.d.ts +1 -0
  51. package/dist/state.js +29 -3
  52. package/dist/tool-activation.d.ts +2 -2
  53. package/dist/tool-activation.js +5 -5
  54. package/dist/tool-presentation.d.ts +2 -6
  55. package/dist/tool-presentation.js +75 -122
  56. package/dist/tools.d.ts +0 -3
  57. package/dist/tools.js +104 -132
  58. package/package.json +12 -5
  59. package/dist/json-lines.d.ts +0 -14
  60. package/dist/json-lines.js +0 -95
@@ -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 agent_send. Do not invent new authority.",
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
- const requireFromHere = createRequire(import.meta.url);
60
- // The CLI ships at dist/cli.js of the installed Pi package.
61
- const packageJson = requireFromHere.resolve("@earendil-works/pi-coding-agent/package.json");
62
- const cliPath = join(dirname(packageJson), "dist", "cli.js");
63
- return existsSync(cliPath) ? cliPath : undefined;
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
- if (currentScript !== undefined && !isBunVirtualScript && existsSync(currentScript)) {
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)
@@ -192,6 +215,7 @@ export function buildLaunchSpec(options) {
192
215
  // Empty is the defined "derive the title in the child" case, mirroring the task channel below.
193
216
  [CHILD_TITLE_ENV_KEY]: config.title ?? "",
194
217
  PI_HINDSIGHT_DISABLE: "1",
218
+ PI_OBSERVATIONAL_MEMORY_DISABLE: "1",
195
219
  ...(config.codemodeOnly ? { PI_CODEMODE_MODE: "only" } : {}),
196
220
  ...(mcpExcluded && mcpExcluded.length > 0
197
221
  ? { PI_SUBAGENT_MCP_EXCLUDE: mcpExcluded.join(",") }
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, forgetRuntime } = options;
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, forgetRuntime);
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, forgetRuntime) {
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" }, forgetRuntime);
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" }, forgetRuntime);
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" }, forgetRuntime);
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, forgetRuntime) {
219
- const identity = record.runtime?.runtimeIdentity;
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, requestOptions?.timeoutMs ?? DEFAULT_REQUEST_TIMEOUT_MS, pollMs, requestOptions?.signal);
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, DEFAULT_REQUEST_TIMEOUT_MS, pollMs, signal);
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
- enqueueCommand(current, {
290
- id: randomUUID(),
291
- parentSessionId: options.parentSessionId,
292
- subagentId: options.subagentId,
293
- direction: "to_parent",
294
- operation: "event",
295
- payload: JSON.stringify(event ?? null),
296
- createdAt: Date.now(),
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, timeoutMs, pollMs, signal) {
356
+ function waitForCommand(db, id, options) {
326
357
  const started = Date.now();
327
358
  return new Promise((resolve, reject) => {
328
- const timer = setInterval(() => {
329
- if (signal?.aborted) {
330
- clearInterval(timer);
359
+ const drop = () => {
360
+ try {
331
361
  withWrite(db, () => {
332
362
  deleteCommand(db, id);
333
363
  });
334
- reject(new BridgeError("aborted", "The mailbox request was cancelled"));
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
- const command = readCommand(db, id);
338
- if (command === undefined)
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
- resolve(command.result === null ? null : JSON.parse(command.result));
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 (Date.now() - started >= timeoutMs) {
357
- reject(new BridgeError("timeout", `No answer for ${id} after ${timeoutMs}ms`));
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 type { EffectiveLaunchConfig, OperationError, Presentation, PublicSubagent, RuntimeAttachment, SendMode, SpawnSubagentInput, SubagentRecord, SubagentState } from "./domain.js";
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, RuntimeTokenStore } from "./runtime.js";
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 `agent_send` starts it
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): Promise<readonly unknown[] | undefined>;
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, mode?: SendMode, signal?: AbortSignal): Promise<OperationError | PublicSubagent>;
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 | {