@hheei/pi-subagents 0.1.2 → 0.2.3

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 +45 -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 -60
  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/runtime.js CHANGED
@@ -1,253 +1,227 @@
1
- import { spawn } from "node:child_process";
2
- import { createHash, randomBytes, randomUUID } from "node:crypto";
3
- import { chmod, mkdir, readFile, unlink, writeFile } from "node:fs/promises";
1
+ /**
2
+ * Runtime plumbing for a child Pi process that this parent owns.
3
+ *
4
+ * There is no supervisor: a background child is a child process of this Pi, and a child that runs in
5
+ * a host panel is owned by that host. Either way the parent's bridge socket is what the child dials,
6
+ * so "is this child alive?" is answered by a bridge connection, and the runtime metadata in the
7
+ * registry only records which runtime and endpoint a child was last launched with.
8
+ */
9
+ import { randomBytes, randomUUID } from "node:crypto";
10
+ import { mkdirSync, readFileSync } from "node:fs";
11
+ import { chmod, mkdir, rename, writeFile } from "node:fs/promises";
4
12
  import { tmpdir } from "node:os";
5
13
  import { join } from "node:path";
6
- import { fileURLToPath } from "node:url";
7
- import { connectWithRetry, RunnerConnection } from "./connector.js";
14
+ import { errorMessage } from "@hheei/pi-ext-core";
15
+ import { spawnChildRuntime } from "./child-process.js";
8
16
  import { buildLaunchSpec, withBridgeToken } from "./launch-spec.js";
9
- import { planSessionPlacement } from "./session-bootstrap.js";
10
17
  function runtimeDirectory() {
11
18
  const user = typeof process.getuid === "function" ? process.getuid() : "user";
12
19
  return join(tmpdir(), `pi-subagents-${user}`);
13
20
  }
14
- export function createRuntimeTokenStore() {
15
- const tokens = new Map();
21
+ /**
22
+ * The one socket a parent session listens on. A child dials this path, so a parent that restarts
23
+ * re-listens in the same place and a surviving child reconnects instead of being rebuilt.
24
+ */
25
+ export function parentBridgeEndpoint(parentSessionId) {
26
+ return join(runtimeDirectory(), `parent-${parentSessionId}.sock`);
27
+ }
28
+ /**
29
+ * Every parent session keeps its own token file. One shared file is rewritten wholesale by whichever
30
+ * parent saved last, so two parent sessions in the same runtime directory would erase each other's
31
+ * credentials and a child that survived a restart could no longer authorize itself.
32
+ */
33
+ export function parentTokenFile(parentSessionId) {
34
+ return join(runtimeDirectory(), `tokens-${parentSessionId}.json`);
35
+ }
36
+ /**
37
+ * Tokens outlive the parent process: a child that survived (a panel child, or one whose parent was
38
+ * killed) reconnects with the token it was launched with, and a restarted parent must be able to
39
+ * authorize it without holding the old process's memory. The store is a 0600 file in the 0700
40
+ * runtime directory; the registry never sees a token.
41
+ */
42
+ export function createRuntimeTokenStore(options) {
43
+ const file = options.file ?? parentTokenFile(options.parentSessionId);
44
+ const diagnose = options.diagnose ?? (() => { });
45
+ const tokens = new Map(readStoredTokens(file, diagnose));
46
+ let writing = Promise.resolve();
47
+ const persist = () => {
48
+ const payload = `${JSON.stringify(Object.fromEntries(tokens))}\n`;
49
+ writing = writing
50
+ .then(async () => {
51
+ mkdirSync(runtimeDirectory(), { recursive: true, mode: 0o700 });
52
+ // Writing in place truncates the file first, and a parent killed inside that window would
53
+ // leave every surviving child locked out of the bridge. The swap is atomic instead.
54
+ const temporary = `${file}.${process.pid}.tmp`;
55
+ await writeFile(temporary, payload, { mode: 0o600 });
56
+ await rename(temporary, file);
57
+ })
58
+ .catch((error) => {
59
+ diagnose(`could not persist bridge tokens: ${errorMessage(error)}`);
60
+ });
61
+ };
16
62
  return {
17
63
  remember(runtimeIdentity, token) {
18
64
  tokens.set(runtimeIdentity, token);
65
+ persist();
19
66
  },
20
67
  get(runtimeIdentity) {
21
68
  return tokens.get(runtimeIdentity);
22
69
  },
23
70
  forget(runtimeIdentity) {
24
- tokens.delete(runtimeIdentity);
25
- },
26
- clear() {
27
- tokens.clear();
71
+ if (tokens.delete(runtimeIdentity))
72
+ persist();
28
73
  },
29
74
  };
30
75
  }
31
- /** Starts the package runner as a detached process and returns its authenticated controller. */
32
- export async function launchDetachedRunner(options) {
33
- const record = await requireCurrentRecord(options.registry, options.record.subagentId, options.signal);
34
- const prepared = await claimRuntime(options.registry, record, "replacement", undefined, undefined, options.signal);
35
- return startClaimedRunner(options.registry, prepared.record, prepared.identity, prepared.token, prepared.claim.claimId, options.signal, options.tokens);
36
- }
37
- /** Reconnects to a surviving writer, launching a replacement only after its PID is confirmed dead. */
38
- export async function recoverDetachedRunner(options) {
39
- let record = await requireCurrentRecord(options.registry, options.record.subagentId, options.signal);
40
- if (record.intent === "stopped")
41
- throw new Error(`Child ${record.subagentId} is stopped`);
42
- record = await clearDeadHolderClaim(options.registry, record, options.signal);
43
- if (record.runtime !== undefined) {
44
- const prepared = await claimRuntime(options.registry, record, "reconnect", record.runtime.runtimeIdentity, record.claim?.claimId, options.signal);
45
- const connection = new RunnerConnection({
46
- endpoint: prepared.identity.endpoint,
47
- identity: prepared.identity,
48
- token: prepared.token,
49
- role: "recovery",
50
- claimId: prepared.claim.claimId,
51
- });
52
- try {
53
- await connectWithRetry(connection, {
54
- attempts: 20,
55
- delayMs: 50,
56
- ...(options.signal === undefined ? {} : { signal: options.signal }),
57
- });
58
- rememberClaimedToken(options.tokens, record, prepared.identity.runtimeIdentity, prepared.token);
59
- return connection;
60
- }
61
- catch (error) {
62
- connection.close();
63
- const afterFailure = await requireCurrentRecord(options.registry, record.subagentId, options.signal);
64
- if (afterFailure.claim?.claimId !== prepared.claim.claimId) {
65
- throw new Error(`Recovery authorization for ${record.subagentId} was consumed before the connection completed`, { cause: error });
66
- }
67
- if (!(await isRecordedRunnerConfirmedDead(afterFailure))) {
68
- await options.registry.releaseClaim(record.subagentId, prepared.claim.claimId, options.signal);
69
- throw new Error(`Runner ${record.subagentId} could not be reconnected and is not confirmed dead; replacement refused`, { cause: error });
70
- }
71
- record = afterFailure;
72
- }
76
+ function readStoredTokens(file, diagnose) {
77
+ let raw;
78
+ try {
79
+ raw = readFileSync(file, "utf8");
80
+ }
81
+ catch (error) {
82
+ const code = error.code;
83
+ if (code !== "ENOENT")
84
+ diagnose(`could not read bridge tokens: ${errorMessage(error)}`);
85
+ return [];
86
+ }
87
+ if (raw.trim() === "")
88
+ return [];
89
+ let parsed;
90
+ try {
91
+ parsed = JSON.parse(raw);
92
+ }
93
+ catch (error) {
94
+ diagnose(`bridge token file is not valid JSON: ${errorMessage(error)}`);
95
+ return [];
73
96
  }
74
- const expectedRuntimeIdentity = record.runtime?.runtimeIdentity;
75
- record = await refreshSessionPlacement(options.registry, record, options.signal);
76
- const prepared = await claimRuntime(options.registry, record, "replacement", expectedRuntimeIdentity, record.claim?.claimId, options.signal);
77
- return startClaimedRunner(options.registry, prepared.record, prepared.identity, prepared.token, prepared.claim.claimId, options.signal, options.tokens);
97
+ if (typeof parsed !== "object" || parsed === null)
98
+ return [];
99
+ return Object.entries(parsed).filter((entry) => typeof entry[1] === "string");
78
100
  }
79
- async function claimRuntime(registry, record, kind, expectedRuntimeIdentity, expectedClaimId, signal) {
80
- const runtimeIdentity = kind === "reconnect" ? expectedRuntimeIdentity : randomUUID();
81
- if (runtimeIdentity === undefined)
82
- throw new Error("Reconnect requires a runtime identity");
101
+ /**
102
+ * Mints the runtime identity and its bridge token, remembers the token so the child's very first
103
+ * hello can be authorized, and builds the one LaunchSpec for this presentation. The token travels
104
+ * in the spec's environment, because who starts the child differs: the parent starts a background
105
+ * child and the host starts a panel one.
106
+ */
107
+ function planChildRuntime(record, tokens, presentation) {
108
+ const runtimeIdentity = randomUUID();
83
109
  const token = randomBytes(32).toString("hex");
84
- const directory = runtimeDirectory();
85
- await mkdir(directory, { recursive: true, mode: 0o700 });
86
- await chmod(directory, 0o700);
87
- const endpoint = kind === "reconnect" && record.runtime !== undefined
88
- ? record.runtime.endpoint
89
- : join(directory, `${record.subagentId}-${runtimeIdentity.slice(0, 12)}.sock`);
90
- const claim = {
91
- claimId: randomUUID(),
92
- kind,
93
- holderPid: process.pid,
94
- runtimeIdentity,
95
- endpoint,
96
- controllerTokenHash: createHash("sha256").update(token).digest("hex"),
97
- };
98
- const claimed = await registry.claim(record.subagentId, record.revision, claim, expectedRuntimeIdentity, expectedClaimId, signal);
99
- return {
100
- record: claimed,
101
- identity: {
102
- parentSessionId: claimed.parentSessionId,
103
- subagentId: claimed.subagentId,
104
- runtimeIdentity,
105
- endpoint,
106
- token,
107
- },
108
- token,
109
- claim,
110
- };
111
- }
112
- async function startClaimedRunner(registry, record, identity, token, claimId, signal, tokens) {
113
- const launch = buildLaunchSpec({
110
+ const endpoint = parentBridgeEndpoint(record.parentSessionId);
111
+ const spec = buildLaunchSpec({
114
112
  config: record.launchConfig,
115
113
  invocation: record.launchConfig.invocation,
116
- mode: "rpc",
114
+ presentation,
117
115
  persistence: record.persistence,
118
116
  bridge: {
119
- parentSessionId: identity.parentSessionId,
120
- subagentId: identity.subagentId,
121
- runtimeIdentity: identity.runtimeIdentity,
122
- endpoint: identity.endpoint,
117
+ parentSessionId: record.parentSessionId,
118
+ subagentId: record.subagentId,
119
+ runtimeIdentity,
120
+ endpoint,
123
121
  },
124
122
  });
125
- const directory = runtimeDirectory();
126
- const jobPath = join(directory, `${record.subagentId}-${identity.runtimeIdentity}.json`);
127
- const job = {
128
- invocation: { command: launch.command, args: launch.argv },
129
- cwd: launch.cwd,
130
- sessionId: record.sessionId,
131
- ...(record.sessionPath === undefined ? {} : { sessionPath: record.sessionPath }),
132
- claimId,
133
- env: launch.env,
134
- };
135
- let child;
136
- const connection = new RunnerConnection({ endpoint: identity.endpoint, identity, token });
137
- try {
138
- await writeFile(jobPath, `${JSON.stringify(job)}\n`, { mode: 0o600, flag: "wx" });
139
- const runnerEntry = fileURLToPath(new URL("./runner-entry.js", import.meta.url));
140
- child = spawn(process.execPath, [runnerEntry, "--job", jobPath], {
141
- cwd: launch.cwd,
142
- detached: true,
143
- env: { ...process.env, ...withBridgeToken(launch.env, token) },
144
- stdio: "ignore",
145
- });
146
- if (child.pid === undefined)
147
- throw new Error(`Runner process for ${record.subagentId} has no PID`);
148
- await registry.markClaimRunner(record.subagentId, claimId, child.pid, signal);
149
- child.unref();
150
- await connectWithRetry(connection, {
151
- attempts: 120,
152
- delayMs: 50,
153
- ...(signal === undefined ? {} : { signal }),
154
- });
155
- await unlink(jobPath).catch(() => undefined);
156
- rememberClaimedToken(tokens, record, identity.runtimeIdentity, token);
157
- return connection;
158
- }
159
- catch (error) {
160
- connection.close();
161
- child?.kill("SIGTERM");
162
- await unlink(jobPath).catch(() => undefined);
163
- const current = await registry.get(record.subagentId).catch(() => undefined);
164
- if (current?.claim?.claimId === claimId) {
165
- await registry.releaseClaim(record.subagentId, claimId).catch(() => undefined);
166
- }
167
- throw error;
168
- }
169
- }
170
- function rememberClaimedToken(tokens, record, runtimeIdentity, token) {
171
- const previous = record.runtime?.runtimeIdentity;
172
123
  tokens.remember(runtimeIdentity, token);
173
- if (previous !== undefined && previous !== runtimeIdentity)
174
- tokens.forget(previous);
124
+ return { runtimeIdentity, endpoint, spec: { ...spec, env: withBridgeToken(spec.env, token) } };
175
125
  }
176
- async function requireCurrentRecord(registry, id, signal) {
177
- const record = await registry.get(id, signal);
178
- if (record === undefined)
179
- throw new Error(`Child ${id} disappeared from the registry`);
180
- return record;
126
+ /** Evidence first: the record names the runtime that was asked for even if starting it fails. */
127
+ async function recordRuntimeEvidence(registry, record, plan) {
128
+ await registry.update(record.subagentId, undefined, (current) => ({
129
+ ...current,
130
+ runtime: { runtimeIdentity: plan.runtimeIdentity, endpoint: plan.endpoint },
131
+ }));
181
132
  }
182
- async function refreshSessionPlacement(registry, record, signal) {
183
- const placement = await planSessionPlacement({
184
- sessionId: record.sessionId,
185
- cwd: record.cwd,
186
- sessionDir: record.launchConfig.sessionDir,
187
- persistence: record.persistence,
188
- ...(record.sessionPath === undefined ? {} : { sessionPath: record.sessionPath }),
133
+ /**
134
+ * Starts one headless child Pi and waits for it to connect. Nothing is driven over its stdio: the
135
+ * process gets the parent's socket path and its own runtime token in the environment, and the
136
+ * bridge is the only control channel. A child that never connects is terminated here rather than
137
+ * left running with no way to reach it.
138
+ */
139
+ export async function launchChild(options) {
140
+ const record = options.record;
141
+ const plan = planChildRuntime(record, options.tokens, "background");
142
+ await recordRuntimeEvidence(options.registry, record, plan);
143
+ const runtime = spawnChildRuntime({
144
+ command: plan.spec.command,
145
+ args: plan.spec.argv,
146
+ cwd: plan.spec.cwd,
147
+ env: { ...process.env, ...plan.spec.env },
148
+ ...(options.diagnose === undefined ? {} : { diagnose: options.diagnose }),
189
149
  });
190
- if (placement.persistence === record.persistence &&
191
- placement.sessionPath === record.sessionPath) {
192
- return record;
150
+ let connected;
151
+ try {
152
+ connected = await options.waitForBridge(record.subagentId, options.signal);
193
153
  }
194
- return registry.update(record.subagentId, record.revision, (current) => ({
195
- ...current,
196
- persistence: placement.persistence,
197
- ...(placement.sessionPath === undefined
198
- ? {}
199
- : {
200
- sessionPath: placement.sessionPath,
201
- launchConfig: { ...current.launchConfig, sessionPath: placement.sessionPath },
202
- }),
203
- }), record.runtime?.runtimeIdentity, signal);
204
- }
205
- async function clearDeadHolderClaim(registry, record, signal) {
206
- const claim = record.claim;
207
- if (claim === undefined)
208
- return record;
209
- if (!isPidConfirmedDead(claim.holderPid)) {
210
- throw new Error(`Child ${record.subagentId} is already claimed by a live or unknown owner`);
154
+ catch (error) {
155
+ options.diagnose?.(`bridge wait failed: ${errorMessage(error)}`);
156
+ connected = false;
211
157
  }
212
- if (claim.runnerPid !== undefined && !isPidConfirmedDead(claim.runnerPid)) {
213
- throw new Error(`Child ${record.subagentId} has a live or unknown claimed runner`);
158
+ if (connected)
159
+ return { handle: runtime };
160
+ await runtime.terminate();
161
+ if (runtime.alive) {
162
+ // The process outlived termination, so it is not gone and the caller keeps the handle: it is
163
+ // the only thing that can try again.
164
+ return {
165
+ handle: runtime,
166
+ failure: `Child ${record.subagentId} never connected to the parent bridge and did not stop (it may still be running)`,
167
+ };
214
168
  }
215
- return registry.releaseClaim(record.subagentId, claim.claimId, signal);
169
+ options.tokens.forget(plan.runtimeIdentity);
170
+ return { failure: `Child ${record.subagentId} never connected to the parent bridge` };
216
171
  }
217
- export async function isRecordedRunnerConfirmedDead(record) {
218
- const runtime = record.runtime;
219
- if (runtime?.pid === undefined)
220
- return false;
221
- if (process.platform !== "linux")
222
- return isPidConfirmedDead(runtime.pid);
172
+ /**
173
+ * Opens one child in a host panel and waits for it to connect. The host owns this process, so the
174
+ * parent holds no runtime handle for it; a panel that never yields a bridge is closed again instead
175
+ * of being left as a process nobody can control.
176
+ */
177
+ export async function openChildPanel(options) {
178
+ const record = options.record;
179
+ const plan = planChildRuntime(record, options.tokens, "panel");
180
+ await recordRuntimeEvidence(options.registry, record, plan);
181
+ let attachment;
223
182
  try {
224
- const environment = await readFile(`/proc/${runtime.pid}/environ`, "utf8");
225
- const identity = environment
226
- .split("\0")
227
- .filter(Boolean)
228
- .map((entry) => {
229
- const separator = entry.indexOf("=");
230
- return [entry.slice(0, separator), entry.slice(separator + 1)];
231
- })
232
- .find(([key]) => key === "PI_SUBAGENTS_RUNTIME_ID")?.[1];
233
- return identity !== runtime.runtimeIdentity;
183
+ attachment = await options.host.open(plan.spec);
234
184
  }
235
185
  catch (error) {
236
- return isErrno(error, "ENOENT");
186
+ // Opening is not atomic: the host may have created the panel and started the process before
187
+ // failing to report it. The token is kept so a child that does come up can still be adopted,
188
+ // and the caller is told the runtime is unconfirmed instead of that nothing was started.
189
+ return {
190
+ unconfirmed: true,
191
+ failure: `Child ${record.subagentId} could not be opened in a ${options.host.kind} panel (${errorMessage(error)}); a panel may have been created, so stop the child before sending again`,
192
+ };
237
193
  }
238
- }
239
- export function isPidConfirmedDead(pid) {
194
+ let connected;
240
195
  try {
241
- process.kill(pid, 0);
242
- return false;
196
+ connected = await options.waitForBridge(record.subagentId, options.signal);
243
197
  }
244
198
  catch (error) {
245
- return isErrno(error, "ESRCH");
199
+ options.diagnose?.(`bridge wait failed: ${errorMessage(error)}`);
200
+ connected = false;
201
+ }
202
+ if (connected)
203
+ return { handle: attachment };
204
+ // The child never dialed in, so the panel is closed again — and only a host that confirms it is
205
+ // gone counts. While it may still run, the caller keeps the handle and the token: the handle is
206
+ // what stops a second runtime from being started for the same session, and the token is what lets
207
+ // a child that does come up be adopted instead of lingering as a process nobody owns.
208
+ const cleanup = await attachment.cleanup();
209
+ const observed = await attachment.observe();
210
+ if (!(observed.known && !observed.alive)) {
211
+ return {
212
+ handle: attachment,
213
+ failure: `Child ${record.subagentId} never connected to the parent bridge and its panel could not be closed (${cleanup.stderr || "its state could not be observed"}); close it manually`,
214
+ };
246
215
  }
216
+ options.tokens.forget(plan.runtimeIdentity);
217
+ return { failure: `Child ${record.subagentId} never connected to the parent bridge` };
247
218
  }
248
- function isErrno(value, code) {
249
- return (typeof value === "object" &&
250
- value !== null &&
251
- "code" in value &&
252
- value.code === code);
219
+ /** Creates the runtime directory if needed; the socket itself is 0600 in a 0700 directory. */
220
+ export async function prepareRuntimeDirectory(diagnose) {
221
+ const directory = runtimeDirectory();
222
+ await mkdir(directory, { recursive: true, mode: 0o700 });
223
+ await chmod(directory, 0o700).catch((error) => {
224
+ diagnose?.(`could not restrict the runtime directory: ${errorMessage(error)}`);
225
+ });
226
+ return directory;
253
227
  }
@@ -1,5 +1,5 @@
1
- import type { ModelRegistryLike, ParentAgentDefaults } from "./agent-resolver.js";
2
- import type { EffectiveLaunchConfig, ExecutionMode, PersistenceState, PiInvocation, SpawnSubagentInput, SubagentRecord } from "./domain.js";
1
+ import type { ModelRegistryLike, ParentAgentDefaults, SkillCatalogEntry } from "./agent-resolver.js";
2
+ import type { EffectiveLaunchConfig, PersistenceState, PiInvocation, Presentation, SpawnSubagentInput, SubagentRecord } from "./domain.js";
3
3
  import type { SubagentRegistry } from "./registry.js";
4
4
  export interface SessionPlacement {
5
5
  readonly sessionId: string;
@@ -20,11 +20,18 @@ export interface SessionFileIdentity {
20
20
  readonly cwd: string;
21
21
  }
22
22
  /**
23
- * The Pi session directory a child would use by default for its cwd. It is asked
24
- * of Pi itself instead of re-deriving the layout, and is always passed to the child
25
- * as an explicit `--session-dir` so recovery can find the file again.
23
+ * The session directory Pi itself would use for this cwd. At the top level it holds the
24
+ * human's own sessions, which is why children never write there directly.
26
25
  */
27
26
  export declare function resolveDefaultSessionDir(cwd: string): string;
27
+ /**
28
+ * The session directory a delegated child writes to.
29
+ *
30
+ * Pi discovers sessions with a non-recursive `readdir` of the session directory, so a
31
+ * subdirectory keeps delegated sessions out of the human's session list while the child still
32
+ * receives that directory as an explicit `--session-dir` recovery can find again.
33
+ */
34
+ export declare function resolveSubagentSessionDir(cwd: string): string;
28
35
  /** Verifies that a durable session file exists and still carries the expected identity. */
29
36
  export declare function inspectSessionFile(path: string, sessionId: string): Promise<SessionFileIdentity>;
30
37
  /** Finds an already-flushed session file for an id. Empty files are not sessions yet. */
@@ -35,18 +42,27 @@ export declare function findSessionFile(sessionDir: string, sessionId: string):
35
42
  * random id; a flushed session must still prove its identity on disk.
36
43
  */
37
44
  export declare function planSessionPlacement(options: PlanSessionPlacementOptions): Promise<SessionPlacement>;
38
- export declare function createSubagentId(): string;
45
+ export declare function resetSubagentIdCounter(value?: number): void;
46
+ export declare function createSubagentId(existingIds?: readonly string[]): string;
39
47
  export interface ResolveSubagentLaunchOptions {
40
48
  readonly input: SpawnSubagentInput;
41
49
  /** Parent agent cwd, used when the tool call does not ask for another one. */
42
50
  readonly cwd: string;
43
51
  readonly parent: ParentAgentDefaults;
44
52
  readonly modelRegistry: ModelRegistryLike;
53
+ /** Parent's loaded skills, the catalog this launch's skill names resolve against. */
54
+ readonly skillCatalog?: readonly SkillCatalogEntry[];
55
+ /** Diagnostics for the caller to surface; omitted means they are dropped. */
56
+ readonly onWarning?: (message: string) => void;
45
57
  readonly invocation?: PiInvocation;
46
58
  readonly bridgeExtensionPath?: string;
47
59
  readonly homeDirectory?: string;
48
60
  readonly subagentId?: string;
49
61
  readonly sessionId?: string;
62
+ readonly existingSubagentIds?: readonly string[];
63
+ readonly enforceEnabled?: boolean;
64
+ readonly sessionPath?: string;
65
+ readonly referencedSessionPath?: string;
50
66
  }
51
67
  /**
52
68
  * One resolution chain for a spawn: agent definition + explicit parent defaults
@@ -59,7 +75,7 @@ export interface PersistSubagentIntentOptions {
59
75
  readonly parentSessionId: string;
60
76
  readonly task: string;
61
77
  readonly launchConfig: EffectiveLaunchConfig;
62
- readonly mode?: ExecutionMode;
78
+ readonly presentation?: Presentation;
63
79
  readonly now?: () => Date;
64
80
  }
65
81
  /**