@fitchmultz/pi-subagents 0.50.0 → 0.51.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 (169) hide show
  1. package/CHANGELOG.md +51 -2
  2. package/README.md +36 -5
  3. package/dist/agent/contracts.js +6 -24
  4. package/dist/agent/host.js +3 -16
  5. package/dist/agent/index.js +207 -173
  6. package/dist/agent/journal-tail.js +115 -156
  7. package/dist/agent/launch.js +20 -8
  8. package/dist/agent/legacy.js +65 -23
  9. package/dist/agent/notifications.js +138 -47
  10. package/dist/agent/observation-state.js +246 -0
  11. package/dist/agent/observation-view.js +258 -0
  12. package/dist/agent/observation.js +346 -0
  13. package/dist/agent/observe-card.js +243 -0
  14. package/dist/agent/observe-chrome.js +85 -0
  15. package/dist/agent/observe-client.js +285 -0
  16. package/dist/agent/observe-layout.js +65 -0
  17. package/dist/agent/observe-reading.js +84 -0
  18. package/dist/agent/observe-screen.js +400 -96
  19. package/dist/agent/observe-transcript.js +382 -0
  20. package/dist/agent/observe.js +250 -43
  21. package/dist/agent/process.js +198 -25
  22. package/dist/agent/receipts.js +92 -20
  23. package/dist/agent/runner.js +125 -20
  24. package/dist/agent/shell.js +2 -1
  25. package/dist/agent/storage.js +29 -12
  26. package/dist/agent/tool-schemas.js +20 -0
  27. package/dist/agent/typebox.js +10 -5
  28. package/dist/agent/usage.js +0 -115
  29. package/dist/agent/view.js +46 -16
  30. package/dist/agent/wake.js +99 -0
  31. package/dist/build-info.js +1 -1
  32. package/dist/intercom-client.d.ts +26 -0
  33. package/dist/intercom-client.js +45 -0
  34. package/dist/pi-intercom/agent-dir.d.ts +1 -0
  35. package/dist/pi-intercom/bridge-session.js +2 -1
  36. package/dist/pi-intercom/broker/broker.js +27 -21
  37. package/dist/pi-intercom/broker/client-last.d.ts +13 -0
  38. package/dist/pi-intercom/broker/client-last.js +17 -5
  39. package/dist/pi-intercom/broker/client.d.ts +60 -0
  40. package/dist/pi-intercom/broker/client.js +47 -16
  41. package/dist/pi-intercom/broker/connection.d.ts +29 -0
  42. package/dist/pi-intercom/broker/framing.d.ts +17 -0
  43. package/dist/pi-intercom/broker/framing.js +10 -7
  44. package/dist/pi-intercom/broker/guard.js +23 -0
  45. package/dist/pi-intercom/broker/last.d.ts +26 -0
  46. package/dist/pi-intercom/broker/last.js +57 -32
  47. package/dist/pi-intercom/broker/paths.d.ts +13 -0
  48. package/dist/pi-intercom/broker/paths.js +35 -12
  49. package/dist/pi-intercom/broker/pid.js +5 -1
  50. package/dist/pi-intercom/broker/server-connection.js +19 -0
  51. package/dist/pi-intercom/broker/snapshot-reader.d.ts +9 -0
  52. package/dist/pi-intercom/broker/snapshots.d.ts +29 -0
  53. package/dist/pi-intercom/broker/targets.d.ts +9 -0
  54. package/dist/pi-intercom/connection.js +10 -2
  55. package/dist/pi-intercom/identity.js +6 -5
  56. package/dist/pi-intercom/inbound-delivery.js +16 -60
  57. package/dist/pi-intercom/inbound-journal.js +83 -6
  58. package/dist/pi-intercom/inbound-record.js +18 -1
  59. package/dist/pi-intercom/inbound.js +12 -38
  60. package/dist/pi-intercom/index.js +3 -0
  61. package/dist/pi-intercom/last-message.js +4 -16
  62. package/dist/pi-intercom/lifecycle.js +88 -3
  63. package/dist/pi-intercom/outbound.js +5 -15
  64. package/dist/pi-intercom/reply-tracker.js +7 -6
  65. package/dist/pi-intercom/reply-wait.js +2 -2
  66. package/dist/pi-intercom/session-targets.d.ts +24 -0
  67. package/dist/pi-intercom/tool-registration.js +4 -1
  68. package/dist/pi-intercom/tool-results.js +5 -7
  69. package/dist/pi-intercom/tool-schemas.js +12 -26
  70. package/dist/pi-intercom/types.d.ts +212 -0
  71. package/dist/pi-intercom/types.js +54 -3
  72. package/dist/pi-intercom/validation.d.ts +12 -0
  73. package/dist/pi-intercom/validation.js +3 -0
  74. package/dist/shared/bounded-json.js +151 -0
  75. package/dist/shared/compact-cards.js +66 -0
  76. package/dist/shared/journal-frames.js +3 -6
  77. package/dist/shared/json-projection.js +267 -129
  78. package/dist/shared/native-journal.js +123 -56
  79. package/dist/shared/post-exit-stdio-guard.js +120 -7
  80. package/dist/shared/tui-action.js +29 -0
  81. package/dist/shared/unknown.d.ts +9 -0
  82. package/docs/code-quality.md +2 -0
  83. package/docs/development.md +40 -1
  84. package/docs/intercom-bridge.md +9 -4
  85. package/docs/intercom.md +226 -6
  86. package/docs/performance.md +87 -11
  87. package/docs/reference.md +157 -20
  88. package/docs/retired-history-maintenance.md +252 -0
  89. package/package.json +28 -15
  90. package/scripts/archive-retired-history.mjs +464 -0
  91. package/scripts/build.mjs +48 -28
  92. package/scripts/measure-size.mjs +1 -1
  93. package/scripts/native-package-smoke.mjs +153 -12
  94. package/scripts/package-smoke.mjs +80 -11
  95. package/scripts/prepare.mjs +2 -2
  96. package/scripts/real-pi-live.mjs +377 -0
  97. package/scripts/real-pi-processes.mjs +6 -1
  98. package/scripts/real-pi-smoke.mjs +6 -282
  99. package/scripts/run-tests.mjs +4 -2
  100. package/scripts/typescript.mjs +35 -0
  101. package/skills/pi-intercom/SKILL.md +2 -1
  102. package/skills/pi-subagents/SKILL.md +23 -5
  103. package/src/agent/contracts.ts +8 -34
  104. package/src/agent/host.ts +19 -20
  105. package/src/agent/index.ts +224 -190
  106. package/src/agent/journal-tail.ts +141 -153
  107. package/src/agent/launch.ts +33 -9
  108. package/src/agent/legacy.ts +85 -31
  109. package/src/agent/notifications.ts +192 -67
  110. package/src/agent/observation-state.ts +277 -0
  111. package/src/agent/observation-view.ts +267 -0
  112. package/src/agent/observation.ts +405 -0
  113. package/src/agent/observe-card.ts +304 -0
  114. package/src/agent/observe-chrome.ts +170 -0
  115. package/src/agent/observe-client.ts +318 -0
  116. package/src/agent/observe-layout.ts +79 -0
  117. package/src/agent/observe-reading.ts +97 -0
  118. package/src/agent/observe-screen.ts +443 -129
  119. package/src/agent/observe-transcript.ts +434 -0
  120. package/src/agent/observe.ts +304 -71
  121. package/src/agent/process.ts +240 -34
  122. package/src/agent/receipts.ts +114 -24
  123. package/src/agent/runner.ts +125 -27
  124. package/src/agent/shell.ts +2 -1
  125. package/src/agent/storage.ts +27 -11
  126. package/src/agent/tool-schemas.ts +33 -0
  127. package/src/agent/typebox.ts +12 -7
  128. package/src/agent/usage.ts +0 -130
  129. package/src/agent/view.ts +70 -20
  130. package/src/agent/wake.ts +107 -0
  131. package/src/intercom-client.ts +108 -0
  132. package/src/pi-intercom/bridge-session.ts +2 -2
  133. package/src/pi-intercom/broker/broker.ts +27 -23
  134. package/src/pi-intercom/broker/client-last.ts +25 -12
  135. package/src/pi-intercom/broker/client.ts +55 -33
  136. package/src/pi-intercom/broker/framing.ts +11 -8
  137. package/src/pi-intercom/broker/guard.ts +33 -0
  138. package/src/pi-intercom/broker/last.ts +86 -33
  139. package/src/pi-intercom/broker/paths.ts +39 -12
  140. package/src/pi-intercom/broker/pid.ts +6 -1
  141. package/src/pi-intercom/broker/server-connection.ts +25 -0
  142. package/src/pi-intercom/connection.ts +21 -10
  143. package/src/pi-intercom/identity.ts +8 -5
  144. package/src/pi-intercom/inbound-delivery.ts +20 -66
  145. package/src/pi-intercom/inbound-journal.ts +103 -7
  146. package/src/pi-intercom/inbound-record.ts +21 -4
  147. package/src/pi-intercom/inbound.ts +18 -49
  148. package/src/pi-intercom/index.ts +3 -0
  149. package/src/pi-intercom/last-message.ts +10 -19
  150. package/src/pi-intercom/lifecycle.ts +117 -14
  151. package/src/pi-intercom/outbound.ts +7 -16
  152. package/src/pi-intercom/reply-tracker.ts +8 -6
  153. package/src/pi-intercom/reply-wait.ts +3 -2
  154. package/src/pi-intercom/runtime-types.ts +0 -1
  155. package/src/pi-intercom/tool-registration.ts +4 -1
  156. package/src/pi-intercom/tool-results.ts +10 -9
  157. package/src/pi-intercom/tool-schemas.ts +12 -44
  158. package/src/pi-intercom/types.ts +115 -4
  159. package/src/pi-intercom/validation.ts +4 -0
  160. package/src/shared/bounded-json.ts +162 -0
  161. package/src/shared/compact-cards.ts +118 -0
  162. package/src/shared/journal-frames.ts +3 -19
  163. package/src/shared/json-projection.ts +267 -147
  164. package/src/shared/native-journal.ts +157 -75
  165. package/src/shared/post-exit-stdio-guard.ts +165 -7
  166. package/src/shared/tui-action.ts +39 -0
  167. package/tsconfig.client.json +8 -0
  168. package/tsconfig.json +1 -0
  169. package/src/shared/stream-json.d.ts +0 -10
@@ -1,27 +1,15 @@
1
1
  #!/usr/bin/env node
2
- import {
3
- copyFileSync,
4
- existsSync,
5
- mkdirSync,
6
- mkdtempSync,
7
- readFileSync,
8
- rmSync,
9
- writeFileSync,
10
- } from "node:fs";
11
- import { dirname, join, resolve, relative, isAbsolute } from "node:path";
2
+ import { mkdirSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from "node:fs";
3
+ import { join, resolve } from "node:path";
12
4
  import process from "node:process";
13
- import { setTimeout as delay } from "node:timers/promises";
14
- import { isChildTreeAlive } from "../src/shared/post-exit-stdio-guard.ts";
15
- import { SmokeProcesses, filesIn, processRef } from "./real-pi-processes.mjs";
5
+ import { runLiveSmoke } from "./real-pi-live.mjs";
6
+ import { SmokeProcesses } from "./real-pi-processes.mjs";
16
7
 
17
8
  const DEFAULT_TIMEOUT_MS = 120_000;
18
- const authAgentDir =
19
- process.env.PI_REAL_SMOKE_AUTH_AGENT_DIR ??
20
- (process.env.HOME ? join(process.env.HOME, ".pi", "agent") : undefined);
21
9
 
22
10
  function usage() {
23
11
  console.log(
24
- `Usage: node scripts/real-pi-smoke.mjs [--llm] [--llm-full] [--keep-temp] [--timeout-ms <ms>]\n\nRuns an opt-in real Pi package smoke for this checkout. It uses an isolated\ntemporary Pi home, installs the single pi-subagents package with bundled intercom, verifies pi list,\nand loads the single compiled entry. It does not install pi-fitch-kit, publish anything,\nor create GitHub Actions.\n\nOptions:\n --llm Also run live model-backed list, foreground, and async-completion smoke prompts\n --llm-full Also verify forked tool-result context and saved-conversation continuation\n --keep-temp Keep the isolated temporary home for debugging\n --timeout-ms <ms> Per-command timeout in milliseconds (default: ${DEFAULT_TIMEOUT_MS})\n -h, --help Show this help\n\nEnvironment:\n PI_REAL_SMOKE_AUTH_AGENT_DIR Source Pi agent dir for auth.json/models.json during --llm (default: ~/.pi/agent)\n PI_REAL_SMOKE_MODEL Model passed to live --llm smoke prompts, e.g. openai/gpt-6-astra\n PI_REAL_SMOKE_PROVIDER Provider passed to live --llm smoke prompts\n\nExit codes:\n 0 real Pi smoke passed\n 1 install/list/live smoke failed\n 2 invalid arguments`,
12
+ `Usage: node scripts/real-pi-smoke.mjs [--llm] [--llm-full] [--keep-temp] [--timeout-ms <ms>]\n\nRuns an opt-in real Pi package smoke for this checkout. It uses an isolated\ntemporary Pi home, installs the single pi-subagents package with bundled intercom, verifies pi list,\nand loads the single compiled entry. It does not install pi-fitch-kit, publish anything,\nor create GitHub Actions.\n\nOptions:\n --llm Also run live model-backed intercom, list, and delegate prompts in a persistent RPC main\n that stays open until each launch receipt's completion notification resumes it\n --llm-full Also verify forked completion context and saved-conversation continuation\n --keep-temp Keep the isolated temporary home for debugging\n --timeout-ms <ms> Per-command timeout in milliseconds (default: ${DEFAULT_TIMEOUT_MS})\n -h, --help Show this help\n\nEnvironment:\n PI_REAL_SMOKE_AUTH_AGENT_DIR Source Pi agent dir for auth.json/models.json during --llm (default: ~/.pi/agent)\n PI_REAL_SMOKE_MODEL Model passed to live --llm smoke prompts, e.g. openai/gpt-6-astra\n PI_REAL_SMOKE_PROVIDER Provider passed to live --llm smoke prompts\n\nExit codes:\n 0 real Pi smoke passed\n 1 install/list/live smoke failed\n 2 invalid arguments`,
25
13
  );
26
14
  }
27
15
 
@@ -75,23 +63,6 @@ function parseArgs(argv) {
75
63
  return options;
76
64
  }
77
65
 
78
- function copyLiveAuth(agentDir) {
79
- if (!authAgentDir || !existsSync(authAgentDir)) {
80
- return [];
81
- }
82
- mkdirSync(agentDir, { recursive: true });
83
- const copied = [];
84
- for (const filename of ["auth.json", "models.json"]) {
85
- const source = join(authAgentDir, filename);
86
- if (!existsSync(source)) {
87
- continue;
88
- }
89
- copyFileSync(source, join(agentDir, filename));
90
- copied.push(filename);
91
- }
92
- return copied;
93
- }
94
-
95
66
  function isolatedEnv(root, agentDir) {
96
67
  const home = join(root, "home");
97
68
  const env = { ...process.env };
@@ -176,253 +147,6 @@ function registeredCommands(response) {
176
147
  : [];
177
148
  }
178
149
 
179
- async function runLivePrompt(label, prompt, options, expectedTool) {
180
- const args = [
181
- "--print",
182
- "--mode",
183
- "json",
184
- "--session-dir",
185
- join(options.root, "sessions"),
186
- "--approve",
187
- ];
188
- if (process.env.PI_REAL_SMOKE_PROVIDER) {
189
- args.push("--provider", process.env.PI_REAL_SMOKE_PROVIDER);
190
- }
191
- if (process.env.PI_REAL_SMOKE_MODEL) {
192
- args.push(
193
- "--model",
194
- process.env.PI_REAL_SMOKE_MODEL,
195
- "--models",
196
- process.env.PI_REAL_SMOKE_MODEL,
197
- );
198
- }
199
- args.push(prompt);
200
- const output = await runPi(label, args, options);
201
- writeFileSync(join(options.root, `${label.replace(/ /g, "-")}.jsonl`), output);
202
- const events = output.split("\n").flatMap((line) => {
203
- try {
204
- return [JSON.parse(line)];
205
- } catch {
206
- return [];
207
- }
208
- });
209
- if (
210
- !events.some(
211
- (event) => event.type === "tool_execution_start" && event.toolName === expectedTool,
212
- )
213
- ) {
214
- throw new Error(
215
- `${label} never invoked ${expectedTool}; a matching final sentence is not execution evidence.`,
216
- );
217
- }
218
- const errors = events.filter((event) => event.type === "tool_execution_end" && event.isError);
219
- if (errors.length) {
220
- throw new Error(
221
- `${label} had ${errors.length} failed tool call(s); inspect its saved wire log.`,
222
- );
223
- }
224
- if (!events.some((event) => event.type === "agent_settled")) {
225
- throw new Error(`${label} did not reach agent_settled.`);
226
- }
227
- return {
228
- events,
229
- text: events
230
- .filter((event) => event.type === "message_end" && event.message?.role === "assistant")
231
- .flatMap((event) =>
232
- event.message.content.filter((part) => part.type === "text").map((part) => part.text),
233
- )
234
- .join("\n"),
235
- };
236
- }
237
-
238
- function* savedAssistantModels(file) {
239
- for (const line of readFileSync(file, "utf8").split("\n")) {
240
- if (!line.trim()) {
241
- continue;
242
- }
243
- const entry = JSON.parse(line);
244
- if (entry.type === "message" && entry.message?.role === "assistant") {
245
- yield `${entry.message.provider}/${entry.message.model}`;
246
- }
247
- }
248
- }
249
-
250
- function auditSavedModels(directories, expectedModel) {
251
- const models = new Set();
252
- let responses = 0;
253
- for (const file of directories.flatMap((dir) => [...filesIn(dir)])) {
254
- if (!file.endsWith(".jsonl")) {
255
- continue;
256
- }
257
- for (const model of savedAssistantModels(file)) {
258
- responses++;
259
- models.add(model);
260
- }
261
- }
262
- if (!responses) {
263
- throw new Error("No saved assistant messages found for model audit.");
264
- }
265
- if (expectedModel?.includes("/") && [...models].some((model) => model !== expectedModel)) {
266
- throw new Error(`Unexpected model in saved parent/child sessions: ${[...models].join(", ")}`);
267
- }
268
- console.log(
269
- `[real-pi-smoke] audited ${responses} saved parent/child responses: ${[...models].join(", ")}`,
270
- );
271
- }
272
-
273
- function requireOutput(label, output, pattern) {
274
- if (!pattern.test(output)) {
275
- throw new Error(`${label} did not include expected evidence ${pattern}.\nOutput:\n${output}`);
276
- }
277
- const compact = output.trim().split(/\r?\n/).slice(-8).join("\n");
278
- console.log(`[real-pi-smoke] ${label} output evidence:\n${compact}`);
279
- }
280
-
281
- function asyncCompletion(dir, runId) {
282
- const resultPath = join(dir, "result.json");
283
- if (!existsSync(resultPath)) {
284
- return { status: "missing" };
285
- }
286
- const result = JSON.parse(readFileSync(resultPath, "utf8"));
287
- if (result.id !== runId) {
288
- throw new Error("Async completion belongs to another saved run");
289
- }
290
- if (result.status !== "completed") {
291
- throw new Error(`async run ${runId} ended ${result.status}: ${result.error ?? ""}`);
292
- }
293
- const processPath = join(dir, "actor.lock", "process.json");
294
- if (!existsSync(processPath)) {
295
- return { status: "completed; actor exit unconfirmed" };
296
- }
297
- const actor = JSON.parse(readFileSync(processPath, "utf8"));
298
- return {
299
- status: result.status,
300
- output: isChildTreeAlive(processRef(actor.pid))
301
- ? undefined
302
- : readFileSync(join(dir, "output.txt"), "utf8"),
303
- };
304
- }
305
-
306
- async function waitForAsyncCompletion(run, pattern, { env, timeoutMs, signal }) {
307
- const dir = dirname(run.sessionFile);
308
- const within = relative(join(env.PI_CODING_AGENT_DIR, "sessions", "agent-runs"), dir);
309
- if (isAbsolute(within) || within.startsWith("..") || !/^[a-zA-Z0-9_-]{1,128}$/.test(run.id)) {
310
- throw new Error("Async receipt did not identify this smoke's private saved run");
311
- }
312
- const deadline = Date.now() + timeoutMs;
313
- let lastState = "missing";
314
- while (Date.now() < deadline) {
315
- signal.throwIfAborted();
316
- const completion = asyncCompletion(dir, run.id);
317
- lastState = completion.status;
318
- if (completion.output !== undefined) {
319
- requireOutput(`async run ${run.id} completed and actor exited`, completion.output, pattern);
320
- return;
321
- }
322
- // Result publication is not process exit; retain resources until final actor writes finish.
323
- // oxlint-disable-next-line no-await-in-loop
324
- await delay(100, undefined, { signal });
325
- }
326
- throw new Error(
327
- `async run ${run.id} did not complete and exit after ${timeoutMs}ms (last state: ${lastState}).`,
328
- );
329
- }
330
-
331
- async function runLiveSmoke(options, runOptions, agentDir) {
332
- const copiedAuthFiles = copyLiveAuth(agentDir);
333
- if (copiedAuthFiles.length > 0) {
334
- console.log(
335
- `[real-pi-smoke] copied ${copiedAuthFiles.join(" and ")} into isolated Pi agent dir for live provider auth`,
336
- );
337
- }
338
- const childModelInstruction = process.env.PI_REAL_SMOKE_MODEL
339
- ? ` Pass model '${process.env.PI_REAL_SMOKE_MODEL}' to every delegate call.`
340
- : "";
341
- await runBasicLiveSmoke(runOptions, childModelInstruction);
342
- if (options.llmFull) {
343
- await runFullLiveSmoke(runOptions, childModelInstruction);
344
- }
345
- auditSavedModels(
346
- [join(runOptions.root, "sessions"), join(agentDir, "sessions", "agent-runs")],
347
- process.env.PI_REAL_SMOKE_MODEL,
348
- );
349
- }
350
-
351
- function toolDetails(events, name) {
352
- return events
353
- .filter((event) => event.type === "tool_execution_end" && event.toolName === name)
354
- .map((event) => event.result?.details);
355
- }
356
-
357
- function requireCompleted(run, expected) {
358
- if (run?.status !== "completed" || run.output?.text !== expected) {
359
- throw new Error(`Native child did not complete with expected output: ${expected}`);
360
- }
361
- }
362
-
363
- async function runBasicLiveSmoke(runOptions, childModelInstruction) {
364
- const prompts = [
365
- [
366
- "intercom",
367
- "Call the intercom tool with action status. Reply exactly 'real-pi-smoke intercom ok' only if it reports 'Connected: Yes'.",
368
- "intercom",
369
- ],
370
- [
371
- "list",
372
- "Call agent_runs with action list. Reply exactly 'real-pi-smoke list ok' only if it successfully returns a saved-run list. There are no profiles to discover.",
373
- "agent_runs",
374
- ],
375
- [
376
- "foreground",
377
- `Call delegate with prompt 'Reply exactly: real-pi-smoke foreground ok', name 'smoke-foreground', and async false.${childModelInstruction} Reply exactly 'real-pi-smoke foreground ok' only if the actual child result contains it.`,
378
- "delegate",
379
- ],
380
- ];
381
- for (const [kind, prompt, tool] of prompts) {
382
- // Keep separate native parent sessions so each smoke owns its saved runs.
383
- // oxlint-disable-next-line no-await-in-loop
384
- const result = await runLivePrompt(`real Pi ${kind} prompt`, prompt, runOptions, tool);
385
- if (kind === "foreground") {
386
- requireCompleted(toolDetails(result.events, "delegate")[0], "real-pi-smoke foreground ok");
387
- }
388
- requireOutput(`real Pi ${kind} prompt`, result.text, new RegExp(`real-pi-smoke ${kind} ok`));
389
- }
390
- const result = await runLivePrompt(
391
- "real Pi async prompt",
392
- `Call delegate with prompt 'Reply exactly: real-pi-smoke async ok', name 'smoke-async', and async true.${childModelInstruction} Do not wait or call agent_runs. Reply exactly 'real-pi-smoke async launched ok' after receiving a launch receipt, not claiming completion.`,
393
- runOptions,
394
- "delegate",
395
- );
396
- requireOutput("real Pi async prompt", result.text, /real-pi-smoke async launched ok/);
397
- const receipt = toolDetails(result.events, "delegate")[0];
398
- if (receipt?.receipt !== "launch" || !receipt.id || !receipt.sessionFile) {
399
- throw new Error("Native delegate did not return a saved asynchronous launch receipt");
400
- }
401
- await waitForAsyncCompletion(receipt, /^real-pi-smoke async ok$/m, runOptions);
402
- }
403
-
404
- async function runFullLiveSmoke(runOptions, childModelInstruction) {
405
- const prompt = `First call delegate with prompt 'Reply exactly CALIBRATION_FACT=packed-otter-7319', name 'fact', context fresh and async false.${childModelInstruction} Then call delegate with context fork, async false, and prompt 'Read the preceding delegate tool result in inherited conversation. Without tools, filesystem reads, or guessing, return the CALIBRATION_FACT line exactly; return FACT_NOT_VISIBLE if absent.' Finally call agent_runs with action continue, the first saved run id, async false, and prompt 'Without tools, repeat the CALIBRATION_FACT line from your own saved conversation exactly.' Reply exactly 'real-pi-smoke fork ok' only if both the forked child and continued child actually return CALIBRATION_FACT=packed-otter-7319. There are no profiles, task prefixes, output settings, or canned role instructions.`;
406
- const result = await runLivePrompt("real Pi fork prompt", prompt, runOptions, "delegate");
407
- const delegates = toolDetails(result.events, "delegate");
408
- const continuation = toolDetails(result.events, "agent_runs")[0];
409
- for (const run of [...delegates, continuation]) {
410
- requireCompleted(run, "CALIBRATION_FACT=packed-otter-7319");
411
- }
412
- if (
413
- delegates.length !== 2 ||
414
- delegates[0].context !== "fresh" ||
415
- delegates[1].context !== "fork" ||
416
- continuation.predecessor !== delegates[0].id ||
417
- continuation.sessionFile !== delegates[0].sessionFile
418
- ) {
419
- throw new Error(
420
- "Fork/continuation smoke did not execute the two contexts and reuse the first saved conversation",
421
- );
422
- }
423
- requireOutput("real Pi fork prompt", result.text, /real-pi-smoke fork ok/);
424
- }
425
-
426
150
  function createProject(root) {
427
151
  const projectRoot = join(root, "project");
428
152
  mkdirSync(projectRoot, { recursive: true });
@@ -479,7 +203,7 @@ async function main() {
479
203
  );
480
204
  if (!options.llm) {
481
205
  console.log(
482
- "[real-pi-smoke] live model subagent prompts skipped; pass --llm to exercise foreground/async paths.",
206
+ "[real-pi-smoke] live model subagent prompts skipped; pass --llm to exercise launch receipts and completion notifications.",
483
207
  );
484
208
  }
485
209
  } finally {
@@ -1,7 +1,7 @@
1
1
  #!/usr/bin/env node
2
2
  import { spawnSync } from "node:child_process";
3
3
  import { mkdtempSync, readdirSync, rmSync } from "node:fs";
4
- import { availableParallelism } from "node:os";
4
+ import { availableParallelism, tmpdir } from "node:os";
5
5
  import { join } from "node:path";
6
6
 
7
7
  // ponytail: integration is sized for the 3-vCPU macOS hosted runner, where the suite takes
@@ -91,7 +91,9 @@ function sanitizedEnv() {
91
91
  function runNodeTest(label, files, timeoutMs, concurrency) {
92
92
  const args = [...(concurrency ? [`--test-concurrency=${concurrency}`] : []), "--test", ...files];
93
93
  const startedAt = Date.now();
94
- const tempRoot = mkdtempSync(join("/tmp", "pi-subagents-test-"));
94
+ const tempRoot = mkdtempSync(
95
+ join(process.platform === "android" ? tmpdir() : "/tmp", "pi-subagents-"),
96
+ );
95
97
  const env = sanitizedEnv();
96
98
  env.PI_SUBAGENT_TEMP_ROOT = tempRoot;
97
99
  env.PI_INTERCOM_RUNTIME_DIR = join(tempRoot, "ic");
@@ -0,0 +1,35 @@
1
+ #!/usr/bin/env node
2
+ import { spawnSync } from "node:child_process";
3
+ import { existsSync } from "node:fs";
4
+ import { join } from "node:path";
5
+
6
+ /**
7
+ * Android's npm compiler package is not published; use its installed native compiler.
8
+ * @param {string} [script]
9
+ * @returns {{ command: string, args: string[] }}
10
+ */
11
+ export function compilerInvocation(script) {
12
+ if (script === undefined && process.platform === "android") {
13
+ return { command: "tsgo", args: [] };
14
+ }
15
+ const compiler = script ?? join(process.cwd(), "node_modules", "typescript", "bin", "tsc");
16
+ if (!existsSync(compiler)) {
17
+ throw new Error(`typescript is not installed at ${compiler}; run npm install first.`);
18
+ }
19
+ return { command: process.execPath, args: [compiler] };
20
+ }
21
+
22
+ if (import.meta.main) {
23
+ const compiler = compilerInvocation();
24
+ const result = spawnSync(compiler.command, [...compiler.args, ...process.argv.slice(2)], {
25
+ stdio: "inherit",
26
+ });
27
+ if (result.error) {
28
+ console.error(
29
+ process.platform === "android"
30
+ ? `Cannot run native TypeScript: install an Android build of tsgo on PATH. ${result.error.message}`
31
+ : result.error.message,
32
+ );
33
+ }
34
+ process.exitCode = result.status ?? 1;
35
+ }
@@ -56,4 +56,5 @@ persistent supervisor question/approval workflow.
56
56
  Old agent-directory brokers may coexist during migration; updated sessions use
57
57
  the same-user broker. Do not kill another broker or change runtime configuration
58
58
  to force discovery. Only intercom-connected eligible sessions appear, not all
59
- open Pi processes. See `docs/intercom.md` for broker and persistence contracts.
59
+ open Pi processes. See [local messaging](../../docs/intercom.md) for broker and
60
+ persistence contracts.
@@ -1,6 +1,6 @@
1
1
  ---
2
2
  name: pi-subagents
3
- description: "Delegate one complete assignment to a native Pi child; inspect, continue, wait for, or stop saved child runs. Use for Pi delegation, not profile administration or non-Pi agents."
3
+ description: "Delegate one complete assignment to a native Pi child; inspect, continue, or stop saved child runs. Use for Pi delegation, not profile administration or non-Pi agents."
4
4
  ---
5
5
 
6
6
  # Pi delegation
@@ -25,19 +25,26 @@ Fresh context is default; `context:"fork"` preserves the native conversation,
25
25
  including prior tool results. A forked child still cannot delegate. Optional
26
26
  `cwd` and `model` are explicit launch overrides; do not change global auth/model
27
27
  settings to implement an override.
28
+ `model:"provider/model:thinking"` uses Pi's native suffix resolver when the host
29
+ exports it; a literal model ID wins before suffix parsing. Without that host API,
30
+ use a literal ID with inherited thinking or upgrade Pi. Never retry another model
31
+ silently.
28
32
 
29
- Runs are asynchronous unless `async:false`. Continue useful independent work;
30
- when child evidence gates the next step, end the turn and await completion rather
31
- than polling. A launch receipt is not completion. Review actual outputs and
33
+ Every launch, continuation and guidance call returns a receipt; no tool waits for
34
+ child completion. Continue useful independent work; when child evidence gates the
35
+ next step, end the turn and let the completion notice resume you rather than
36
+ polling `inspect`. A launch receipt is not completion. Review actual outputs and
32
37
  validation evidence before accepting the result.
33
38
 
39
+ Keep the main session running for completion delivery. A one-shot print
40
+ invocation exits after its final turn and stops any remaining children.
41
+
34
42
  ## Controls
35
43
 
36
44
  - `agent_runs({action:"list"})` discovers this native main's saved runs.
37
45
  - `inspect` reads a run by ID; follow returned offsets for complete output.
38
46
  - `continue` takes a complete follow-up `prompt` and keeps the saved child chat.
39
47
  Active work receives guidance; completed work gets an immutable successor.
40
- - `wait` observes existing work; cancelling that wait does not stop the producer.
41
48
  - `stop` requests termination. Inspect actual process evidence; request acceptance
42
49
  does not prove actor or command exit.
43
50
 
@@ -45,6 +52,15 @@ Use IDs returned by the tools. Runs belong to their native main session, not a
45
52
  cwd, project, name, or forked copy of its transcript. Resume the same main for
46
53
  ownership. Do not delete ambiguous launch state to force another attempt.
47
54
 
55
+ Escape aborts the parent's reply, not already-launched children; `stop` ends one
56
+ run. Children terminate when their parent exits. Resume the same parent and explicitly
57
+ continue a saved child conversation; the child inspects the interrupted work
58
+ rather than automatically replaying tool calls. Ordinary `/reload` is not exit.
59
+ Under a fork launcher that provides `ctx.managedHost`, `pi restart` or `/restart`
60
+ is not exit either: children keep running and report to the ready replacement.
61
+ That needs one full CLI relaunch after upgrading; a worker under an older loaded
62
+ launcher still stops children when it exits.
63
+
48
64
  ## Coordination and delivery
49
65
 
50
66
  All pairs inside a main/children group can use `intercom` list/send/last. Across
@@ -60,3 +76,5 @@ proof; a child-reported pass is evidence to inspect, not automatic sign-off.
60
76
  Normal child completion returns through the run, not a redundant intercom
61
77
  message. Use direct `send` for guidance, corrections, or blockers that affect live
62
78
  work. Ordinary `ask` is bounded request/reply, not durable supervisor revival.
79
+ The [pi-intercom skill](../pi-intercom/SKILL.md) covers messaging operations.
80
+ See [tools and lifecycle](../../docs/reference.md) for the owning contracts.
@@ -36,11 +36,17 @@ export const processSchema = Type.Object({
36
36
  pid: Type.Integer({ minimum: 1 }),
37
37
  identity: Type.String(),
38
38
  token: Type.String(),
39
+ clock: Type.Optional(
40
+ Type.Object({
41
+ boot: Type.String(),
42
+ namespace: Type.String(),
43
+ at: Type.String({ pattern: "^[1-9][0-9]*$", maxLength: 20 }),
44
+ }),
45
+ ),
39
46
  });
40
47
  export type ProcessIdentity = Static<typeof processSchema>;
41
48
  export const attachmentSchema = Type.Object({
42
- pid: Type.Integer({ minimum: 1 }),
43
- identity: Type.String(),
49
+ ...Type.Pick(processSchema, ["pid", "identity", "clock"]).properties,
44
50
  generation: Type.String(),
45
51
  attached: Type.Boolean(),
46
52
  });
@@ -110,35 +116,3 @@ export const admissionSchema = Type.Object({
110
116
  state: Type.Optional(Type.Union([Type.Literal("accepted"), Type.Literal("stale")])),
111
117
  at: Type.Number(),
112
118
  });
113
- export const delegateSchema = Type.Object(
114
- {
115
- prompt: Type.String({ minLength: 1 }),
116
- name: Type.Optional(Type.String({ minLength: 1 })),
117
- cwd: Type.Optional(Type.String({ minLength: 1 })),
118
- model: Type.Optional(
119
- Type.String({ minLength: 1, description: "Exact provider/model ID; no fallback." }),
120
- ),
121
- context: Type.Optional(Type.Union([Type.Literal("fresh"), Type.Literal("fork")])),
122
- async: Type.Optional(Type.Boolean({ default: true })),
123
- },
124
- { additionalProperties: false },
125
- );
126
- export type DelegateInput = Static<typeof delegateSchema>;
127
- export const runsSchema = Type.Object(
128
- {
129
- action: Type.Union([
130
- Type.Literal("list"),
131
- Type.Literal("inspect"),
132
- Type.Literal("continue"),
133
- Type.Literal("stop"),
134
- Type.Literal("wait"),
135
- ]),
136
- id: Type.Optional(Type.String()),
137
- prompt: Type.Optional(Type.String({ minLength: 1 })),
138
- async: Type.Optional(Type.Boolean({ default: true })),
139
- offset: Type.Optional(Type.Integer({ minimum: 0 })),
140
- length: Type.Optional(Type.Integer({ minimum: 4, maximum: 262144 })),
141
- },
142
- { additionalProperties: false },
143
- );
144
- export type RunsInput = Static<typeof runsSchema>;
package/src/agent/host.ts CHANGED
@@ -3,6 +3,22 @@ import * as path from "node:path";
3
3
  import { findPackageJSON } from "node:module";
4
4
  import { fileURLToPath, pathToFileURL } from "node:url";
5
5
  import type * as SDK from "@earendil-works/pi-coding-agent";
6
+ import type { Api, Model } from "@earendil-works/pi-ai";
7
+ import type { ThinkingLevel } from "@earendil-works/pi-agent-core";
8
+ import { readIntercomLineage } from "../pi-intercom/identity.ts";
9
+
10
+ export { errorMessage as errorText } from "../shared/unknown.ts";
11
+
12
+ // The native resolver is optional until official Pi exports it at the package root.
13
+ type HostSDK = typeof SDK & {
14
+ resolveModelReference?: (
15
+ request: { text: string; provider: string; thinkingSuffix: "strict"; fuzzy: false },
16
+ models: readonly Model<Api>[],
17
+ usable: (provider: string) => boolean,
18
+ ) =>
19
+ | { status: "resolved"; model: Model<Api>; thinkingLevel?: ThinkingLevel }
20
+ | { status: "ambiguous" | "not-found" };
21
+ };
6
22
 
7
23
  function isPackage(root: string): boolean {
8
24
  const file = path.join(root, "package.json");
@@ -49,10 +65,10 @@ export function packageRoot(): string {
49
65
  }
50
66
  return path.dirname(manifest);
51
67
  }
52
- export async function loadSDK(root: string): Promise<typeof SDK> {
68
+ export async function loadSDK(root: string): Promise<HostSDK> {
53
69
  // Selected host SDK, not a second package-local runtime hidden behind Jiti aliases.
54
70
  // oxlint-disable-next-line typescript/no-unsafe-assignment
55
- const sdk: typeof SDK = await import(pathToFileURL(path.join(root, "dist/index.js")).href);
71
+ const sdk: HostSDK = await import(pathToFileURL(path.join(root, "dist/index.js")).href);
56
72
  return sdk;
57
73
  }
58
74
  export function runtimeEntry(): string {
@@ -61,22 +77,5 @@ export function runtimeEntry(): string {
61
77
  );
62
78
  }
63
79
  export function isChild(): boolean {
64
- const root = process.env.PI_INTERCOM_ROOT_SESSION_ID;
65
- const parent = process.env.PI_INTERCOM_PARENT_SESSION_ID;
66
- if (root === undefined && parent === undefined) {
67
- return false;
68
- }
69
- if (
70
- root === undefined ||
71
- parent === undefined ||
72
- root.length === 0 ||
73
- parent.length === 0 ||
74
- root !== parent
75
- ) {
76
- throw new Error("Malformed child identity; delegation is disabled.");
77
- }
78
- return true;
79
- }
80
- export function errorText(error: unknown): string {
81
- return error instanceof Error ? error.message : JSON.stringify(error);
80
+ return readIntercomLineage() !== undefined;
82
81
  }