@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.
Files changed (65) hide show
  1. package/README.md +42 -137
  2. package/dist/agent-resolver.d.ts +20 -0
  3. package/dist/agent-resolver.js +155 -21
  4. package/dist/bridge-client.d.ts +55 -0
  5. package/dist/bridge-client.js +453 -0
  6. package/dist/bridge-server.d.ts +72 -0
  7. package/dist/bridge-server.js +490 -0
  8. package/dist/builtin-agents.d.ts +24 -0
  9. package/dist/builtin-agents.js +152 -0
  10. package/dist/child-bridge.d.ts +31 -15
  11. package/dist/child-bridge.js +95 -117
  12. package/dist/child-control.d.ts +58 -0
  13. package/dist/child-control.js +176 -0
  14. package/dist/child-process.d.ts +21 -0
  15. package/dist/child-process.js +93 -0
  16. package/dist/commands.d.ts +2 -2
  17. package/dist/commands.js +52 -52
  18. package/dist/diagnostics.d.ts +0 -2
  19. package/dist/diagnostics.js +0 -8
  20. package/dist/domain.d.ts +57 -27
  21. package/dist/domain.js +60 -11
  22. package/dist/extension.d.ts +26 -1
  23. package/dist/extension.js +243 -46
  24. package/dist/host-adapter.d.ts +37 -12
  25. package/dist/host-adapter.js +214 -63
  26. package/dist/json-lines.js +2 -1
  27. package/dist/launch-spec.d.ts +8 -6
  28. package/dist/launch-spec.js +61 -18
  29. package/dist/manager.d.ts +93 -49
  30. package/dist/manager.js +1175 -528
  31. package/dist/markdown.d.ts +8 -0
  32. package/dist/markdown.js +21 -0
  33. package/dist/protocol.d.ts +33 -52
  34. package/dist/protocol.js +41 -52
  35. package/dist/registry.d.ts +3 -8
  36. package/dist/registry.js +41 -163
  37. package/dist/rpc-events.d.ts +3 -5
  38. package/dist/rpc-events.js +5 -9
  39. package/dist/runtime.d.ts +66 -11
  40. package/dist/runtime.js +182 -208
  41. package/dist/session-bootstrap.d.ts +23 -7
  42. package/dist/session-bootstrap.js +79 -15
  43. package/dist/session-fork.d.ts +16 -0
  44. package/dist/session-fork.js +67 -0
  45. package/dist/state.d.ts +7 -0
  46. package/dist/state.js +60 -25
  47. package/dist/tool-activation.d.ts +18 -0
  48. package/dist/tool-activation.js +227 -0
  49. package/dist/tools.d.ts +6 -3
  50. package/dist/tools.js +213 -72
  51. package/dist/widget.d.ts +4 -8
  52. package/dist/widget.js +55 -64
  53. package/package.json +51 -52
  54. package/dist/child-nudge.d.ts +0 -24
  55. package/dist/child-nudge.js +0 -102
  56. package/dist/connector.d.ts +0 -53
  57. package/dist/connector.js +0 -283
  58. package/dist/pause-gate.d.ts +0 -14
  59. package/dist/pause-gate.js +0 -64
  60. package/dist/rpc-adapter.d.ts +0 -40
  61. package/dist/rpc-adapter.js +0 -194
  62. package/dist/runner-entry.d.ts +0 -24
  63. package/dist/runner-entry.js +0 -166
  64. package/dist/runner.d.ts +0 -52
  65. package/dist/runner.js +0 -930
package/dist/registry.js CHANGED
@@ -1,7 +1,7 @@
1
1
  import { join } from "node:path";
2
2
  import { getAgentDir } from "@earendil-works/pi-coding-agent";
3
- import { readJsonSettingsRoot, updateJsonSettingsRoot } from "@hheei/pi-ext-core";
4
- import { isExecutionMode, isLaunchValueSource, isPersistenceState, isRecord, isSessionId, isSubagentIntent, isSubagentState, isThinkingLevel, REGISTRY_VERSION, } from "./domain.js";
3
+ import { isRecord, readJsonSettingsRoot, updateJsonSettingsRoot } from "@hheei/pi-ext-core";
4
+ import { isLaunchValueSource, isPersistenceState, isPresentation, isSessionId, isSubagentIntent, isSubagentState, isThinkingLevel, normalizeSubagentState, REGISTRY_VERSION, } from "./domain.js";
5
5
  export class SubagentRegistryError extends Error {
6
6
  code;
7
7
  constructor(code, message) {
@@ -31,15 +31,15 @@ const RECORD_FIELDS = {
31
31
  initialTask: true,
32
32
  intent: true,
33
33
  state: true,
34
- mode: true,
34
+ presentation: true,
35
35
  persistence: true,
36
36
  launchConfig: true,
37
37
  runtime: true,
38
38
  latestSummary: true,
39
+ activeTool: true,
39
40
  usage: true,
40
41
  interrupted: true,
41
42
  unacknowledgedInput: true,
42
- claim: true,
43
43
  };
44
44
  const LAUNCH_CONFIG_FIELDS = {
45
45
  subagentId: true,
@@ -58,6 +58,10 @@ const LAUNCH_CONFIG_FIELDS = {
58
58
  prompt: true,
59
59
  bridgeExtensionPath: true,
60
60
  interactive: true,
61
+ title: true,
62
+ task: true,
63
+ referencedSessionPath: true,
64
+ codemodeOnly: true,
61
65
  };
62
66
  const AGENT_FIELDS = {
63
67
  name: true,
@@ -71,16 +75,7 @@ const INVOCATION_FIELDS = { command: true, args: true };
71
75
  const MODEL_FIELDS = { provider: true, id: true, source: true };
72
76
  const THINKING_FIELDS = { level: true, source: true };
73
77
  const SELECTION_FIELDS = { discovery: true, paths: true };
74
- const RUNTIME_FIELDS = { runtimeIdentity: true, endpoint: true, pid: true };
75
- const CLAIM_FIELDS = {
76
- claimId: true,
77
- kind: true,
78
- holderPid: true,
79
- runtimeIdentity: true,
80
- endpoint: true,
81
- controllerTokenHash: true,
82
- runnerPid: true,
83
- };
78
+ const RUNTIME_FIELDS = { runtimeIdentity: true, endpoint: true };
84
79
  const USAGE_FIELDS = {
85
80
  inputTokens: true,
86
81
  outputTokens: true,
@@ -188,26 +183,15 @@ function parseThinking(value, path) {
188
183
  throw invalid(path, "launchConfig.thinking.source is invalid");
189
184
  return Object.freeze({ level, source });
190
185
  }
191
- function parseExtensions(value, path) {
192
- const raw = expectObject(value, "launchConfig.extensions", path);
193
- expectKeys(raw, SELECTION_FIELDS, "launchConfig.extensions", path);
194
- if (typeof raw.discovery !== "boolean") {
195
- throw invalid(path, "launchConfig.extensions.discovery must be boolean");
196
- }
197
- return Object.freeze({
198
- discovery: raw.discovery,
199
- paths: Object.freeze(expectStringArray(raw.paths, "launchConfig.extensions.paths", path)),
200
- });
201
- }
202
- function parseSkills(value, path) {
203
- const raw = expectObject(value, "launchConfig.skills", path);
204
- expectKeys(raw, SELECTION_FIELDS, "launchConfig.skills", path);
186
+ function parseSelection(value, path, field) {
187
+ const raw = expectObject(value, `launchConfig.${field}`, path);
188
+ expectKeys(raw, SELECTION_FIELDS, `launchConfig.${field}`, path);
205
189
  if (typeof raw.discovery !== "boolean") {
206
- throw invalid(path, "launchConfig.skills.discovery must be boolean");
190
+ throw invalid(path, `launchConfig.${field}.discovery must be boolean`);
207
191
  }
208
192
  return Object.freeze({
209
193
  discovery: raw.discovery,
210
- paths: Object.freeze(expectStringArray(raw.paths, "launchConfig.skills.paths", path)),
194
+ paths: Object.freeze(expectStringArray(raw.paths, `launchConfig.${field}.paths`, path)),
211
195
  });
212
196
  }
213
197
  function parseUsage(value, path) {
@@ -235,42 +219,7 @@ function parseRuntime(value, path) {
235
219
  expectKeys(raw, RUNTIME_FIELDS, "runtime", path);
236
220
  const runtimeIdentity = expectString(raw.runtimeIdentity, "runtime.runtimeIdentity", path);
237
221
  const endpoint = expectString(raw.endpoint, "runtime.endpoint", path);
238
- const pid = raw.pid;
239
- if (pid === undefined)
240
- return Object.freeze({ runtimeIdentity, endpoint });
241
- if (typeof pid !== "number" || !Number.isInteger(pid) || pid <= 0) {
242
- throw invalid(path, "runtime.pid must be a positive integer");
243
- }
244
- return Object.freeze({ runtimeIdentity, endpoint, pid });
245
- }
246
- function parseClaim(value, path) {
247
- const raw = expectObject(value, "claim", path);
248
- expectKeys(raw, CLAIM_FIELDS, "claim", path);
249
- if (raw.kind !== "reconnect" && raw.kind !== "replacement") {
250
- throw invalid(path, "claim.kind must be reconnect or replacement");
251
- }
252
- const holderPid = raw.holderPid;
253
- if (typeof holderPid !== "number" || !Number.isInteger(holderPid) || holderPid <= 0) {
254
- throw invalid(path, "claim.holderPid must be a positive integer");
255
- }
256
- const tokenHash = expectString(raw.controllerTokenHash, "claim.controllerTokenHash", path);
257
- if (!/^[0-9a-f]{64}$/u.test(tokenHash)) {
258
- throw invalid(path, "claim.controllerTokenHash must be a SHA-256 hex digest");
259
- }
260
- const runnerPid = raw.runnerPid;
261
- if (runnerPid !== undefined &&
262
- (typeof runnerPid !== "number" || !Number.isInteger(runnerPid) || runnerPid <= 0)) {
263
- throw invalid(path, "claim.runnerPid must be a positive integer");
264
- }
265
- return Object.freeze({
266
- claimId: expectString(raw.claimId, "claim.claimId", path),
267
- kind: raw.kind,
268
- holderPid,
269
- runtimeIdentity: expectString(raw.runtimeIdentity, "claim.runtimeIdentity", path),
270
- endpoint: expectString(raw.endpoint, "claim.endpoint", path),
271
- controllerTokenHash: tokenHash,
272
- ...(runnerPid === undefined ? {} : { runnerPid }),
273
- });
222
+ return Object.freeze({ runtimeIdentity, endpoint });
274
223
  }
275
224
  function parseLaunchConfig(value, path) {
276
225
  const raw = expectObject(value, "launchConfig", path);
@@ -279,7 +228,8 @@ function parseLaunchConfig(value, path) {
279
228
  if (!isSessionId(sessionId))
280
229
  throw invalid(path, "launchConfig.sessionId is not a valid session id");
281
230
  const sessionPath = expectOptionalString(raw.sessionPath, "launchConfig.sessionPath", path);
282
- const extensions = parseExtensions(raw.extensions, path);
231
+ const referencedSessionPath = expectOptionalString(raw.referencedSessionPath, "launchConfig.referencedSessionPath", path);
232
+ const extensions = parseSelection(raw.extensions, path, "extensions");
283
233
  const bridgeExtensionPath = expectString(raw.bridgeExtensionPath, "launchConfig.bridgeExtensionPath", path);
284
234
  if (!extensions.paths.includes(bridgeExtensionPath)) {
285
235
  throw invalid(path, "launchConfig.bridgeExtensionPath is missing from the extension selection");
@@ -287,6 +237,7 @@ function parseLaunchConfig(value, path) {
287
237
  const subagentId = expectString(raw.subagentId, "launchConfig.subagentId", path);
288
238
  if (!isSessionId(subagentId))
289
239
  throw invalid(path, "launchConfig.subagentId is not a valid child id");
240
+ const title = expectOptionalString(raw.title, "launchConfig.title", path);
290
241
  return Object.freeze({
291
242
  subagentId,
292
243
  invocation: parseInvocation(raw.invocation, path),
@@ -294,21 +245,33 @@ function parseLaunchConfig(value, path) {
294
245
  sessionId,
295
246
  sessionDir: expectString(raw.sessionDir, "launchConfig.sessionDir", path),
296
247
  ...(sessionPath === undefined ? {} : { sessionPath }),
248
+ ...(referencedSessionPath === undefined ? {} : { referencedSessionPath }),
297
249
  agent: parseAgent(raw.agent, path),
298
250
  model: parseModel(raw.model, path),
299
251
  thinking: parseThinking(raw.thinking, path),
300
252
  tools: Object.freeze(expectStringArray(raw.tools, "launchConfig.tools", path)),
301
253
  excludeTools: Object.freeze(expectStringArray(raw.excludeTools, "launchConfig.excludeTools", path)),
302
254
  extensions,
303
- skills: parseSkills(raw.skills, path),
255
+ skills: parseSelection(raw.skills, path, "skills"),
304
256
  prompt: expectString(raw.prompt, "launchConfig.prompt", path),
305
257
  bridgeExtensionPath,
306
258
  interactive: expectOptionalBoolean(raw.interactive, "launchConfig.interactive", path, false),
259
+ ...(expectOptionalBoolean(raw.codemodeOnly, "launchConfig.codemodeOnly", path, false)
260
+ ? { codemodeOnly: true }
261
+ : {}),
262
+ ...(title === undefined ? {} : { title }),
307
263
  });
308
264
  }
309
265
  /** Strictly revalidates one record read from disk; partial or unknown shapes never become state. */
310
266
  export function parseSubagentRecord(value, key, parentSessionId, path) {
311
267
  const raw = expectObject(value, `record ${key}`, path);
268
+ if (raw.presentation === undefined && raw.mode !== undefined) {
269
+ // A record from the attach era names its presentation `mode`, and that field means something
270
+ // else now (who holds the child's process), so it is never guessed at: the user deletes the
271
+ // record and this version starts the child fresh. Checked before the field list, because the
272
+ // old field is otherwise just "unsupported".
273
+ throw invalid(path, `record ${key} was written by an older version (mode ${JSON.stringify(raw.mode)}); delete this record to use presentation "panel" | "background"`);
274
+ }
312
275
  expectKeys(raw, RECORD_FIELDS, `record ${key}`, path);
313
276
  const subagentId = expectString(raw.subagentId, "subagentId", path);
314
277
  if (subagentId !== key)
@@ -322,12 +285,13 @@ export function parseSubagentRecord(value, key, parentSessionId, path) {
322
285
  if (!Number.isInteger(revision) || revision < 1) {
323
286
  throw invalid(path, `record ${key} revision must be a positive integer`);
324
287
  }
325
- const state = raw.state;
326
- if (!isSubagentState(state))
288
+ const rawState = raw.state;
289
+ if (!isSubagentState(rawState))
327
290
  throw invalid(path, `record ${key} state is invalid`);
328
- const mode = raw.mode;
329
- if (!isExecutionMode(mode))
330
- throw invalid(path, `record ${key} mode is invalid`);
291
+ const state = normalizeSubagentState(rawState) ?? rawState;
292
+ const presentation = raw.presentation;
293
+ if (!isPresentation(presentation))
294
+ throw invalid(path, `record ${key} presentation is invalid`);
331
295
  const intent = raw.intent;
332
296
  if (!isSubagentIntent(intent))
333
297
  throw invalid(path, `record ${key} intent is invalid`);
@@ -357,9 +321,9 @@ export function parseSubagentRecord(value, key, parentSessionId, path) {
357
321
  throw invalid(path, `record ${key} claims a never-flushed session with a session path`);
358
322
  }
359
323
  const runtime = raw.runtime === undefined ? undefined : parseRuntime(raw.runtime, path);
360
- const claim = raw.claim === undefined ? undefined : parseClaim(raw.claim, path);
361
324
  const usage = raw.usage === undefined ? undefined : parseUsage(raw.usage, path);
362
325
  const latestSummary = expectOptionalString(raw.latestSummary, "latestSummary", path);
326
+ const activeTool = expectOptionalString(raw.activeTool, "activeTool", path);
363
327
  const interrupted = expectOptionalString(raw.interrupted, "interrupted", path);
364
328
  const unacknowledgedInput = expectOptionalString(raw.unacknowledgedInput, "unacknowledgedInput", path);
365
329
  return Object.freeze({
@@ -374,12 +338,12 @@ export function parseSubagentRecord(value, key, parentSessionId, path) {
374
338
  initialTask: expectString(raw.initialTask, "initialTask", path),
375
339
  intent,
376
340
  state,
377
- mode,
341
+ presentation,
378
342
  persistence,
379
343
  launchConfig,
380
344
  ...(runtime === undefined ? {} : { runtime }),
381
- ...(claim === undefined ? {} : { claim }),
382
345
  ...(latestSummary === undefined ? {} : { latestSummary }),
346
+ ...(activeTool === undefined ? {} : { activeTool }),
383
347
  ...(usage === undefined ? {} : { usage }),
384
348
  ...(interrupted === undefined ? {} : { interrupted }),
385
349
  ...(unacknowledgedInput === undefined ? {} : { unacknowledgedInput }),
@@ -461,7 +425,7 @@ export function createSubagentRegistry(options) {
461
425
  if (current === undefined) {
462
426
  throw new SubagentRegistryError("unknown_child", `Unknown child ${id} in ${path}`);
463
427
  }
464
- if (current.revision !== expectedRevision) {
428
+ if (expectedRevision !== undefined && current.revision !== expectedRevision) {
465
429
  throw new SubagentRegistryError("stale_revision", `Child ${id} is at revision ${current.revision}, not ${expectedRevision}`);
466
430
  }
467
431
  if (expectedRuntimeIdentity !== undefined &&
@@ -484,92 +448,6 @@ export function createSubagentRegistry(options) {
484
448
  throw corrupt(path, `update of ${id} did not persist a record`);
485
449
  return stored;
486
450
  },
487
- async claim(id, expectedRevision, candidate, expectedRuntimeIdentity, expectedClaimId, signal) {
488
- return store.update(id, expectedRevision, (current) => {
489
- if (current.intent === "stopped") {
490
- throw new SubagentRegistryError("stopped_child", `Child ${id} is stopped`);
491
- }
492
- if ((expectedClaimId === undefined && current.claim !== undefined) ||
493
- (expectedClaimId !== undefined && current.claim?.claimId !== expectedClaimId)) {
494
- throw new SubagentRegistryError("claim_conflict", `Child ${id} has another claim`);
495
- }
496
- if (candidate.kind === "reconnect") {
497
- if (current.runtime?.runtimeIdentity !== candidate.runtimeIdentity ||
498
- current.runtime.endpoint !== candidate.endpoint) {
499
- throw new SubagentRegistryError("runtime_mismatch", `Child ${id} runtime changed before reconnect claim`);
500
- }
501
- }
502
- else if (expectedRuntimeIdentity === undefined
503
- ? current.runtime !== undefined
504
- : current.runtime?.runtimeIdentity !== expectedRuntimeIdentity) {
505
- throw new SubagentRegistryError("runtime_mismatch", `Child ${id} runtime changed before replacement claim`);
506
- }
507
- return { ...current, claim: candidate };
508
- }, undefined, signal);
509
- },
510
- async markClaimRunner(id, claimId, runnerPid, signal) {
511
- const current = await store.get(id, signal);
512
- if (current === undefined)
513
- throw new SubagentRegistryError("unknown_child", `Unknown child ${id} in ${path}`);
514
- return store.update(id, current.revision, (value) => {
515
- if (value.claim?.claimId !== claimId || value.claim.kind !== "replacement") {
516
- throw new SubagentRegistryError("claim_mismatch", `Child ${id} claim changed`);
517
- }
518
- return { ...value, claim: { ...value.claim, runnerPid } };
519
- }, undefined, signal);
520
- },
521
- async activateClaim(id, claimId, runnerPid, signal) {
522
- const current = await store.get(id, signal);
523
- if (current === undefined)
524
- throw new SubagentRegistryError("unknown_child", `Unknown child ${id} in ${path}`);
525
- return store.update(id, current.revision, (value) => {
526
- const claim = value.claim;
527
- if (claim?.claimId !== claimId ||
528
- claim.kind !== "replacement" ||
529
- (claim.runnerPid !== undefined && claim.runnerPid !== runnerPid) ||
530
- value.intent === "stopped") {
531
- throw new SubagentRegistryError("claim_mismatch", `Child ${id} replacement claim is no longer valid`);
532
- }
533
- const { claim: _claim, ...withoutClaim } = value;
534
- return {
535
- ...withoutClaim,
536
- runtime: {
537
- runtimeIdentity: claim.runtimeIdentity,
538
- endpoint: claim.endpoint,
539
- pid: runnerPid,
540
- },
541
- };
542
- }, undefined, signal);
543
- },
544
- async consumeReconnectClaim(id, claimId, controllerTokenHash, signal) {
545
- const current = await store.get(id, signal);
546
- if (current === undefined)
547
- throw new SubagentRegistryError("unknown_child", `Unknown child ${id} in ${path}`);
548
- return store.update(id, current.revision, (value) => {
549
- const claim = value.claim;
550
- if (claim?.claimId !== claimId ||
551
- claim.kind !== "reconnect" ||
552
- claim.controllerTokenHash !== controllerTokenHash ||
553
- value.runtime?.runtimeIdentity !== claim.runtimeIdentity ||
554
- value.intent === "stopped") {
555
- throw new SubagentRegistryError("claim_mismatch", `Child ${id} reconnect claim is no longer valid`);
556
- }
557
- const { claim: _claim, ...withoutClaim } = value;
558
- return withoutClaim;
559
- }, current.runtime?.runtimeIdentity, signal);
560
- },
561
- async releaseClaim(id, claimId, signal) {
562
- const current = await store.get(id, signal);
563
- if (current === undefined)
564
- throw new SubagentRegistryError("unknown_child", `Unknown child ${id} in ${path}`);
565
- return store.update(id, current.revision, (value) => {
566
- if (value.claim?.claimId !== claimId) {
567
- throw new SubagentRegistryError("claim_mismatch", `Child ${id} claim changed`);
568
- }
569
- const { claim: _claim, ...withoutClaim } = value;
570
- return withoutClaim;
571
- }, undefined, signal);
572
- },
573
451
  };
574
452
  return store;
575
453
  }
@@ -1,8 +1,6 @@
1
1
  /**
2
- * Runner-local and child-lifecycle events are produced by the runner itself.
3
- * Only these Pi RPC event types are forwarded to a controller; everything
4
- * else (streaming token noise, undocumented internals) is dropped.
2
+ * The Pi events a child forwards to its parent. Everything else — streaming token noise,
3
+ * undocumented internals — is dropped, so the parent's projection only sees turn-level facts.
5
4
  */
6
- export declare const FORWARDED_PI_EVENT_TYPES: readonly ["agent_start", "agent_end", "turn_start", "turn_end", "tool_execution_start", "tool_execution_update", "tool_execution_end", "auto_retry", "message_update", "agent_settled"];
5
+ export declare const FORWARDED_PI_EVENT_TYPES: readonly ["agent_start", "agent_end", "turn_start", "turn_end", "tool_execution_start", "tool_execution_update", "tool_execution_end", "auto_retry", "auto_retry_start", "auto_retry_end", "message_update", "agent_settled"];
7
6
  export declare function shouldForwardPiEvent(event: unknown): boolean;
8
- export declare function isIdlePiState(value: unknown): boolean;
@@ -1,8 +1,7 @@
1
- import { isRecord } from "./domain.js";
1
+ import { isRecord } from "@hheei/pi-ext-core";
2
2
  /**
3
- * Runner-local and child-lifecycle events are produced by the runner itself.
4
- * Only these Pi RPC event types are forwarded to a controller; everything
5
- * else (streaming token noise, undocumented internals) is dropped.
3
+ * The Pi events a child forwards to its parent. Everything else — streaming token noise,
4
+ * undocumented internals — is dropped, so the parent's projection only sees turn-level facts.
6
5
  */
7
6
  export const FORWARDED_PI_EVENT_TYPES = [
8
7
  "agent_start",
@@ -13,6 +12,8 @@ export const FORWARDED_PI_EVENT_TYPES = [
13
12
  "tool_execution_update",
14
13
  "tool_execution_end",
15
14
  "auto_retry",
15
+ "auto_retry_start",
16
+ "auto_retry_end",
16
17
  "message_update",
17
18
  "agent_settled",
18
19
  ];
@@ -22,8 +23,3 @@ export function shouldForwardPiEvent(event) {
22
23
  return (typeof event.type === "string" &&
23
24
  FORWARDED_PI_EVENT_TYPES.includes(event.type));
24
25
  }
25
- export function isIdlePiState(value) {
26
- if (!isRecord(value))
27
- return false;
28
- return (value.isStreaming === false && value.isCompacting === false && value.pendingMessageCount === 0);
29
- }
package/dist/runtime.d.ts CHANGED
@@ -1,22 +1,77 @@
1
+ import { type ChildRuntime } from "./child-process.js";
1
2
  import type { SubagentRecord } from "./domain.js";
2
- import type { RunnerLike } from "./manager.js";
3
+ import type { HostAdapter, HostAttachment } from "./host-adapter.js";
3
4
  import type { SubagentRegistry } from "./registry.js";
5
+ /** Per-runtime bridge tokens, kept out of the registry because they are credentials. */
4
6
  export interface RuntimeTokenStore {
5
7
  remember(runtimeIdentity: string, token: string): void;
6
8
  get(runtimeIdentity: string): string | undefined;
7
9
  forget(runtimeIdentity: string): void;
8
- clear(): void;
9
10
  }
10
- export interface LaunchDetachedRunnerOptions {
11
- readonly registry: SubagentRegistry;
11
+ export interface RuntimeTokenStoreOptions {
12
+ /** The parent session these tokens belong to. */
13
+ readonly parentSessionId: string;
14
+ /** Override for tests. Defaults to this parent's file in the shared runtime directory. */
15
+ readonly file?: string;
16
+ readonly diagnose?: (message: string) => void;
17
+ }
18
+ /**
19
+ * The one socket a parent session listens on. A child dials this path, so a parent that restarts
20
+ * re-listens in the same place and a surviving child reconnects instead of being rebuilt.
21
+ */
22
+ export declare function parentBridgeEndpoint(parentSessionId: string): string;
23
+ /**
24
+ * Every parent session keeps its own token file. One shared file is rewritten wholesale by whichever
25
+ * parent saved last, so two parent sessions in the same runtime directory would erase each other's
26
+ * credentials and a child that survived a restart could no longer authorize itself.
27
+ */
28
+ export declare function parentTokenFile(parentSessionId: string): string;
29
+ /**
30
+ * Tokens outlive the parent process: a child that survived (a panel child, or one whose parent was
31
+ * killed) reconnects with the token it was launched with, and a restarted parent must be able to
32
+ * authorize it without holding the old process's memory. The store is a 0600 file in the 0700
33
+ * runtime directory; the registry never sees a token.
34
+ */
35
+ export declare function createRuntimeTokenStore(options: RuntimeTokenStoreOptions): RuntimeTokenStore;
36
+ export interface LaunchChildOptions {
37
+ readonly registry: Pick<SubagentRegistry, "update">;
12
38
  readonly record: SubagentRecord;
13
39
  readonly tokens: RuntimeTokenStore;
40
+ /** Waits for this child's bridge connection; false when it did not connect in time. */
41
+ readonly waitForBridge: (childId: string, signal?: AbortSignal) => Promise<boolean>;
14
42
  readonly signal?: AbortSignal;
43
+ readonly diagnose?: (message: string) => void;
44
+ }
45
+ /**
46
+ * Starts one headless child Pi and waits for it to connect. Nothing is driven over its stdio: the
47
+ * process gets the parent's socket path and its own runtime token in the environment, and the
48
+ * bridge is the only control channel. A child that never connects is terminated here rather than
49
+ * left running with no way to reach it.
50
+ */
51
+ export declare function launchChild(options: LaunchChildOptions): Promise<LaunchOutcome<ChildRuntime>>;
52
+ export interface OpenChildPanelOptions extends LaunchChildOptions {
53
+ readonly host: HostAdapter;
54
+ }
55
+ /**
56
+ * What a launch left behind. A launch that failed can still have started something — a child that
57
+ * never dialed in, or a process that ignores SIGTERM — and then the caller must keep the handle:
58
+ * dropping it would let the next send start a second runtime for a session the first one may still
59
+ * own. `failure` is what the caller reports.
60
+ */
61
+ export interface LaunchOutcome<T> {
62
+ readonly handle?: T;
63
+ readonly failure?: string;
64
+ /**
65
+ * Set when something may still own the child's session and this process cannot observe it, so
66
+ * the caller refuses to start a second runtime until an explicit stop.
67
+ */
68
+ readonly unconfirmed?: boolean;
15
69
  }
16
- export declare function createRuntimeTokenStore(): RuntimeTokenStore;
17
- /** Starts the package runner as a detached process and returns its authenticated controller. */
18
- export declare function launchDetachedRunner(options: LaunchDetachedRunnerOptions): Promise<RunnerLike>;
19
- /** Reconnects to a surviving writer, launching a replacement only after its PID is confirmed dead. */
20
- export declare function recoverDetachedRunner(options: LaunchDetachedRunnerOptions): Promise<RunnerLike>;
21
- export declare function isRecordedRunnerConfirmedDead(record: SubagentRecord): Promise<boolean>;
22
- export declare function isPidConfirmedDead(pid: number): boolean;
70
+ /**
71
+ * Opens one child in a host panel and waits for it to connect. The host owns this process, so the
72
+ * parent holds no runtime handle for it; a panel that never yields a bridge is closed again instead
73
+ * of being left as a process nobody can control.
74
+ */
75
+ export declare function openChildPanel(options: OpenChildPanelOptions): Promise<LaunchOutcome<HostAttachment>>;
76
+ /** Creates the runtime directory if needed; the socket itself is 0600 in a 0700 directory. */
77
+ export declare function prepareRuntimeDirectory(diagnose?: (message: string) => void): Promise<string>;