@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
@@ -2,7 +2,7 @@ import { randomUUID } from "node:crypto";
2
2
  import { execFileSync } from "node:child_process";
3
3
  import * as path from "node:path";
4
4
  import * as fs from "node:fs";
5
- import { readChildProcessIdentity } from "../shared/post-exit-stdio-guard.js";
5
+ import { readChildProcessIdentity, readProcessClock } from "../shared/post-exit-stdio-guard.js";
6
6
  import { read, write } from "./storage.js";
7
7
  import { processSchema } from "./contracts.js";
8
8
  import { verifyProcess } from "./process.js";
@@ -39,5 +39,6 @@ write(path.join(directory, "shells", token, `${randomUUID()}.json`), {
39
39
  pid,
40
40
  identity,
41
41
  token,
42
+ clock: readProcessClock(),
42
43
  }, true);
43
44
  verifyOwner(directory, token);
@@ -4,6 +4,7 @@ import { randomUUID } from "node:crypto";
4
4
  import { readJsonProjection } from "../shared/journal-frames.js";
5
5
  import { Assert } from "./typebox.js";
6
6
  import { launchSchema, successorSchema } from "./contracts.js";
7
+ import { hasErrorCode } from "../shared/unknown.js";
7
8
  export function missing(error) {
8
9
  return error instanceof Error && "code" in error && error.code === "ENOENT";
9
10
  }
@@ -19,31 +20,47 @@ export function read(file, schema, select = () => true) {
19
20
  throw error;
20
21
  }
21
22
  Assert(schema, value);
22
- // TypeBox's generic Static<T> is intentionally the asserted schema's type.
23
- // oxlint-disable-next-line typescript/no-unsafe-return
24
23
  return value;
25
24
  }
26
- export function write(file, value, exclusive = false) {
27
- fs.mkdirSync(path.dirname(file), { recursive: true, mode: 0o700 });
28
- const temp = `${file}.${randomUUID()}.tmp`;
29
- const fd = fs.openSync(temp, "wx", 0o600);
25
+ /** Publish complete bytes without replacing a winner, even where Android denies hard links. */
26
+ function publishExclusive(temp, file) {
30
27
  try {
31
- fs.writeFileSync(fd, `${JSON.stringify(value)}\n`);
32
- fs.fsyncSync(fd);
28
+ fs.linkSync(temp, file);
29
+ return false;
33
30
  }
34
- finally {
35
- fs.closeSync(fd);
31
+ catch (error) {
32
+ if (!["EACCES", "EPERM", "ENOTSUP"].some((code) => hasErrorCode(error, code))) {
33
+ throw error;
34
+ }
35
+ // The immutable, private backing file must outlive its atomic no-clobber symlink.
36
+ fs.symlinkSync(path.basename(temp), file);
37
+ return true;
36
38
  }
39
+ }
40
+ export function write(file, value, exclusive = false) {
41
+ fs.mkdirSync(path.dirname(file), { recursive: true, mode: 0o700 });
42
+ const temp = `${file}.${randomUUID()}.${exclusive ? "data" : "tmp"}`;
43
+ const fd = fs.openSync(temp, "wx", 0o600);
44
+ let retained = false;
37
45
  try {
46
+ try {
47
+ fs.writeFileSync(fd, `${JSON.stringify(value)}\n`);
48
+ fs.fsyncSync(fd);
49
+ }
50
+ finally {
51
+ fs.closeSync(fd);
52
+ }
38
53
  if (exclusive) {
39
- fs.linkSync(temp, file);
54
+ retained = publishExclusive(temp, file);
40
55
  }
41
56
  else {
42
57
  fs.renameSync(temp, file);
43
58
  }
44
59
  }
45
60
  finally {
46
- fs.rmSync(temp, { force: true });
61
+ if (!retained) {
62
+ fs.rmSync(temp, { force: true });
63
+ }
47
64
  }
48
65
  }
49
66
  export function safeId(id) {
@@ -0,0 +1,20 @@
1
+ import { StringEnum } from "@earendil-works/pi-ai";
2
+ import { Type } from "./typebox.js";
3
+ // Main-session tool parameters only; the detached runner must not load pi-ai values.
4
+ export const delegateSchema = Type.Object({
5
+ prompt: Type.String({ minLength: 1 }),
6
+ name: Type.Optional(Type.String({ minLength: 1 })),
7
+ cwd: Type.Optional(Type.String({ minLength: 1 })),
8
+ model: Type.Optional(Type.String({
9
+ minLength: 1,
10
+ description: "Exact provider/model ID (:thinking and any case need host support); no fuzzy match or fallback.",
11
+ })),
12
+ context: Type.Optional(StringEnum(["fresh", "fork"])),
13
+ }, { additionalProperties: false });
14
+ export const runsSchema = Type.Object({
15
+ action: StringEnum(["list", "inspect", "continue", "stop"]),
16
+ id: Type.Optional(Type.String()),
17
+ prompt: Type.Optional(Type.String({ minLength: 1 })),
18
+ offset: Type.Optional(Type.Integer({ minimum: 0 })),
19
+ length: Type.Optional(Type.Integer({ minimum: 4, maximum: 262144 })),
20
+ }, { additionalProperties: false });
@@ -9,12 +9,17 @@ var __rewriteRelativeImportExtension = (this && this.__rewriteRelativeImportExte
9
9
  import { createRequire } from "node:module";
10
10
  import * as path from "node:path";
11
11
  import { pathToFileURL } from "node:url";
12
- import { packageRoot } from "./host.js";
13
- const require = createRequire(path.join(packageRoot(), "dist/index.js"));
14
- // The detached runner needs the selected host's dependencies, not Jiti aliases.
12
+ import { isChild, packageRoot } from "./host.js";
13
+ // Pi aliases these imports to its own instance (including bundled virtual modules).
14
+ // Only the detached runner needs absolute imports from the selected host package.
15
+ const require = isChild() ? createRequire(path.join(packageRoot(), "dist/index.js")) : undefined;
15
16
  // oxlint-disable-next-line typescript/no-unsafe-assignment
16
- const typebox = await import(__rewriteRelativeImportExtension(pathToFileURL(require.resolve("typebox")).href));
17
+ const typebox = require === undefined
18
+ ? await import("typebox")
19
+ : await import(__rewriteRelativeImportExtension(pathToFileURL(require.resolve("typebox")).href));
17
20
  // oxlint-disable-next-line typescript/no-unsafe-assignment
18
- const value = await import(__rewriteRelativeImportExtension(pathToFileURL(require.resolve("typebox/value")).href));
21
+ const value = require === undefined
22
+ ? await import("typebox/value")
23
+ : await import(__rewriteRelativeImportExtension(pathToFileURL(require.resolve("typebox/value")).href));
19
24
  export const { Type } = typebox;
20
25
  export const Assert = value.Assert;
@@ -1,127 +1,12 @@
1
1
  import * as path from "node:path";
2
2
  import * as fs from "node:fs";
3
3
  import { journalStamp } from "../shared/native-journal.js";
4
- import { isDeepStrictEqual } from "node:util";
5
4
  import { nativeProjection } from "../shared/journal-projections.js";
6
5
  import { scanJournal } from "../shared/journal-frames.js";
7
6
  import { isRecord } from "../shared/unknown.js";
8
7
  import { Assert } from "./typebox.js";
9
8
  import { contributionsSchema, resultSchema, } from "./contracts.js";
10
9
  import { read, runDirectory, write } from "./storage.js";
11
- export function sumUsage(values) {
12
- const total = {
13
- input: 0,
14
- output: 0,
15
- cacheRead: 0,
16
- cacheWrite: 0,
17
- totalTokens: 0,
18
- cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, total: 0 },
19
- };
20
- for (const { usage } of values) {
21
- for (const key of ["input", "output", "cacheRead", "cacheWrite", "totalTokens"]) {
22
- total[key] += usage[key];
23
- }
24
- for (const key of ["input", "output", "cacheRead", "cacheWrite", "total"]) {
25
- total.cost[key] += usage.cost[key];
26
- }
27
- for (const key of ["reasoning", "cacheWrite1h"]) {
28
- if (usage[key] !== undefined) {
29
- total[key] = (total[key] ?? 0) + usage[key];
30
- }
31
- }
32
- }
33
- return total;
34
- }
35
- function intents(details) {
36
- if (!isRecord(details) || details.agentUsage === undefined) {
37
- return;
38
- }
39
- const values = details.agentUsage;
40
- Assert(contributionsSchema, values);
41
- return values;
42
- }
43
- function publishedContributions(entry) {
44
- if (!isRecord(entry) || entry.type !== "message" || !isRecord(entry.message)) {
45
- return [];
46
- }
47
- const message = entry.message;
48
- if (message.role !== "toolResult" ||
49
- (message.toolName !== "delegate" && message.toolName !== "agent_runs")) {
50
- return [];
51
- }
52
- const values = intents(message.details);
53
- if (values === undefined || !isDeepStrictEqual(sumUsage(values), message.usage)) {
54
- return [];
55
- }
56
- return values;
57
- }
58
- const receiptProjection = (keys) => {
59
- if (keys.length === 0 || keys[0] === "type") {
60
- return true;
61
- }
62
- if (keys[0] !== "message") {
63
- return false;
64
- }
65
- if (keys.length === 1 || ["role", "toolName", "usage"].includes(String(keys[1]))) {
66
- return true;
67
- }
68
- return keys[1] === "details" && (keys.length === 2 || keys[2] === "agentUsage");
69
- };
70
- function priorUsage(ctx) {
71
- const values = new Map();
72
- const file = ctx.sessionManager.getSessionFile();
73
- if (file !== undefined) {
74
- scanJournal(file, receiptProjection, ({ value: entry }) => {
75
- for (const value of publishedContributions(entry)) {
76
- values.set(value.id, value);
77
- }
78
- }, { policy: "live", requireNewline: true });
79
- }
80
- for (const entry of ctx.sessionManager.getEntries()) {
81
- for (const value of publishedContributions(entry)) {
82
- values.set(value.id, value);
83
- }
84
- }
85
- return values;
86
- }
87
- function uncharged(values, prior) {
88
- const pending = [];
89
- for (const value of values) {
90
- const earlier = prior.get(value.id);
91
- if (earlier && !isDeepStrictEqual(earlier, value)) {
92
- throw new Error(`Conflicting native usage contribution ${value.id}`);
93
- }
94
- if (!earlier) {
95
- pending.push(value);
96
- prior.set(value.id, value);
97
- }
98
- }
99
- return pending;
100
- }
101
- export function registerUsage(pi) {
102
- pi.on("message_end", (event, ctx) => {
103
- const message = event.message;
104
- if (message.role !== "toolResult" ||
105
- (message.toolName !== "delegate" && message.toolName !== "agent_runs")) {
106
- return;
107
- }
108
- const values = intents(message.details);
109
- if (values === undefined || !isRecord(message.details)) {
110
- return;
111
- }
112
- const pending = uncharged(values, priorUsage(ctx));
113
- const { usage: _usage, ...rest } = message;
114
- // Finalized native tool messages are serialized even for concurrent tools.
115
- // Preparing or discarding a result cannot reserve a charge.
116
- return {
117
- message: {
118
- ...rest,
119
- details: { ...message.details, agentUsage: pending },
120
- ...(pending.length > 0 ? { usage: sumUsage(pending) } : {}),
121
- },
122
- };
123
- });
124
- }
125
10
  function billing(entry) {
126
11
  if (entry.checkpoint === true) {
127
12
  return;
@@ -1,23 +1,38 @@
1
1
  import * as fs from "node:fs";
2
2
  import * as path from "node:path";
3
3
  import { readOutputPage } from "../shared/output-page.js";
4
- import { actor, alive } from "./process.js";
4
+ import { actor, alive, observeAlive } from "./process.js";
5
5
  import { admissionSchema, commandSchema, exitSchema, successorSchema, } from "./contracts.js";
6
6
  import { conversation, read, runDirectory } from "./storage.js";
7
7
  import { published } from "./notifications.js";
8
8
  import { readCanonicalResult, readResult } from "./usage.js";
9
9
  import { Type } from "./typebox.js";
10
10
  const commandMetadataSchema = Type.Pick(commandSchema, ["id", "runId", "action", "createdAt"]);
11
- export function toolResult(value, text = JSON.stringify(value, null, 2)) {
11
+ function toolResult(value, text) {
12
12
  return { content: [{ type: "text", text }], details: value };
13
13
  }
14
- function processView(base, launch) {
14
+ function receiptResult(result) {
15
+ return result === undefined
16
+ ? undefined
17
+ : {
18
+ id: result.id,
19
+ owner: result.owner,
20
+ status: result.status,
21
+ endedAt: result.endedAt,
22
+ terminalEntryId: result.terminalEntryId,
23
+ error: result.error,
24
+ accountingError: result.accountingError,
25
+ accountingPending: result.accountingPending,
26
+ };
27
+ }
28
+ /** `observed` holds batched liveness by actor token; any other actor gets a fresh check. */
29
+ function processView(base, launch, observed) {
15
30
  const identity = actor(base, launch);
16
31
  const exit = identity
17
32
  ? read(path.join(conversation(base, launch), `exit-${identity.token}.json`), exitSchema)
18
33
  : undefined;
19
34
  return {
20
- live: identity !== undefined && alive(identity),
35
+ live: identity !== undefined && (observed?.get(identity.token) ?? alive(identity)),
21
36
  exit: exit ?? null,
22
37
  exitKnown: exit !== undefined,
23
38
  };
@@ -60,7 +75,7 @@ function guidanceView(base, launches) {
60
75
  }
61
76
  function metadata(base, launch, result, options) {
62
77
  const directory = runDirectory(base, launch.id);
63
- const process = processView(base, launch);
78
+ const process = processView(base, launch, options.observed);
64
79
  const outputFile = path.join(directory, "output.txt");
65
80
  const admission = read(path.join(conversation(base, launch), "admissions", `${launch.id}.json`), admissionSchema);
66
81
  const status = result?.status ??
@@ -89,12 +104,18 @@ function metadata(base, launch, result, options) {
89
104
  guidance: options.guidance,
90
105
  };
91
106
  }
92
- export function summaries(base, launches, ctx) {
107
+ async function summaries(base, launches, ctx) {
93
108
  const guidance = guidanceView(base, launches);
109
+ const identities = [
110
+ ...new Map(launches.map((launch) => [launch.rootId, actor(base, launch)])).values(),
111
+ ].filter((identity) => identity !== undefined);
112
+ const verdicts = await observeAlive(identities);
113
+ const observed = new Map(identities.map((identity, index) => [identity.token, verdicts[index]]));
94
114
  return launches.map((launch) => {
95
115
  const view = metadata(base, launch, readCanonicalResult(base, launch), {
96
116
  ctx,
97
117
  guidance: guidance.get(launch.id) ?? [],
118
+ observed,
98
119
  });
99
120
  return {
100
121
  ...view,
@@ -109,7 +130,7 @@ export function summaries(base, launches, ctx) {
109
130
  };
110
131
  });
111
132
  }
112
- export function projection(base, launch, ctx, input = {}) {
133
+ function projection(base, launch, ctx, input = {}) {
113
134
  const view = metadata(base, launch, readResult(base, launch), {
114
135
  ctx,
115
136
  guidance: guidanceView(base, [launch]).get(launch.id) ?? [],
@@ -119,6 +140,22 @@ export function projection(base, launch, ctx, input = {}) {
119
140
  output: outputView(view.outputFile, input),
120
141
  };
121
142
  }
143
+ export function renderReceipt(base, launch, ctx, receipt) {
144
+ const details = { ...projection(base, launch, ctx), ...receipt };
145
+ const model = { ...details, result: receiptResult(details.result) };
146
+ return toolResult(details, JSON.stringify(model, null, 2));
147
+ }
148
+ export async function renderInventory(base, launches, ctx, input) {
149
+ const offset = input.offset ?? 0;
150
+ const runs = await summaries(base, launches.slice(offset, offset + 50), ctx);
151
+ const details = {
152
+ runs,
153
+ offset,
154
+ nextOffset: offset + runs.length < launches.length ? offset + runs.length : undefined,
155
+ total: launches.length,
156
+ };
157
+ return toolResult(details, JSON.stringify(details, null, 2));
158
+ }
122
159
  function receiptText(details) {
123
160
  return details.nativeReceipt !== null
124
161
  ? `Native completion receipt: published (${details.nativeReceipt}); publication does not prove model consumption.`
@@ -148,15 +185,8 @@ function resultText(details) {
148
185
  `Native journal: ${details.sessionFile}`,
149
186
  ].join("\n");
150
187
  }
151
- export function render(base, launch, ctx, input = {}) {
152
- const view = projection(base, launch, ctx, input);
153
- const details = {
154
- ...view,
155
- receivedResult: input.receivedResult === true,
156
- ...(input.receivedResult === true && view.result?.contributions !== undefined
157
- ? { agentUsage: view.result.contributions }
158
- : {}),
159
- };
188
+ export function renderInspection(base, launch, ctx, input = {}) {
189
+ const details = projection(base, launch, ctx, input);
160
190
  return {
161
191
  ...toolResult(details, resultText(details)),
162
192
  isError: details.result?.status === "failed",
@@ -0,0 +1,99 @@
1
+ import * as fs from "node:fs";
2
+ import { journalStamp } from "../shared/native-journal.js";
3
+ import { missing } from "./storage.js";
4
+ /** Identity and change stamp of a path; "" when absent. */
5
+ export function pathStamp(file) {
6
+ try {
7
+ const stat = fs.statSync(file, { bigint: true, throwIfNoEntry: false });
8
+ return stat === undefined ? "" : journalStamp(stat);
9
+ }
10
+ catch {
11
+ return "unreadable";
12
+ }
13
+ }
14
+ /** Native change hints for one directory identity; owners still reconcile canonical files. */
15
+ export class DirectoryWatch {
16
+ watcher;
17
+ armed = "";
18
+ checked = "";
19
+ failed = false;
20
+ directory;
21
+ relevant;
22
+ wake;
23
+ constructor(directory, relevant, wake) {
24
+ this.directory = directory;
25
+ this.relevant = relevant;
26
+ this.wake = wake;
27
+ }
28
+ ensure() {
29
+ const stat = fs.statSync(this.directory, { bigint: true, throwIfNoEntry: false });
30
+ const identity = stat?.isDirectory() === true ? `${stat.dev}:${stat.ino}` : "";
31
+ const checked = this.checked;
32
+ this.checked = stat === undefined ? "" : `${identity}:${stat.mtimeNs}:${stat.ctimeNs}`;
33
+ if (this.watcher !== undefined && identity === this.armed) {
34
+ return checked === this.checked ? "watching" : "changed";
35
+ }
36
+ // A replaced directory leaves an inode watch silent on Linux.
37
+ this.close();
38
+ return identity === "" || this.failed ? "unwatched" : this.arm(identity);
39
+ }
40
+ arm(identity) {
41
+ try {
42
+ const watcher = fs.watch(this.directory, { persistent: false }, (_event, file) => {
43
+ if (watcher === this.watcher && this.relevant(file)) {
44
+ this.wake();
45
+ }
46
+ });
47
+ watcher.on("error", () => {
48
+ if (watcher === this.watcher) {
49
+ this.failed = true;
50
+ this.close();
51
+ this.wake();
52
+ }
53
+ });
54
+ this.watcher = watcher;
55
+ this.armed = identity;
56
+ return "armed";
57
+ }
58
+ catch (error) {
59
+ // Unsupported platforms and watch limits keep their owner's polling fallback.
60
+ this.failed = !missing(error);
61
+ return "unwatched";
62
+ }
63
+ }
64
+ close() {
65
+ this.watcher?.close();
66
+ this.watcher = undefined;
67
+ this.armed = "";
68
+ }
69
+ }
70
+ /** A coalesced wake for one owner loop; raised until that loop takes it. */
71
+ export class Wake {
72
+ raised = true;
73
+ resolve;
74
+ raise = () => {
75
+ this.raised = true;
76
+ this.resolve?.();
77
+ };
78
+ take() {
79
+ const raised = this.raised;
80
+ this.raised = false;
81
+ return raised;
82
+ }
83
+ /** Returns at once for a wake raised since the last take. */
84
+ async sleep(ms) {
85
+ if (this.raised) {
86
+ return;
87
+ }
88
+ const { promise, resolve } = Promise.withResolvers();
89
+ const timer = setTimeout(() => {
90
+ resolve("elapsed");
91
+ }, ms);
92
+ this.resolve = () => {
93
+ resolve("woken");
94
+ };
95
+ await promise;
96
+ clearTimeout(timer);
97
+ this.resolve = undefined;
98
+ }
99
+ }
@@ -1 +1 @@
1
- export const EXTENSION_BUILD = Object.freeze({"version":"0.50.0","sha256":"030b516d81cb57e1faa44c99a09eb3eba9eca4a3b9d14e53836d7b54b900ced2"});
1
+ export const EXTENSION_BUILD = Object.freeze({"version":"0.51.0","sha256":"fc084c36d416e6ea9947600b7d1fdef98c84a82f3408dea2ea028a9c485ac31e"});
@@ -0,0 +1,26 @@
1
+ import { type IntercomClientOptions as BrokerOptions } from "./pi-intercom/broker/client.ts";
2
+ import { type DeliveryGuard, type LastMessageQuery, type LastMessageResult, type ReceiptQuery, type ReceiptResult, type SendOptions, type SendResult, type SessionInfo } from "./pi-intercom/types.ts";
3
+ export type { ContextTarget, DeliveryGuard, LastMessageQuery, LastMessageResult, MessageDelivery, NativeReceipt, PublicationReplyHint, ReceiptQuery, ReceiptResult, SendResult, SessionInfo, } from "./pi-intercom/types.ts";
4
+ export type GuardedSendOptions = Omit<SendOptions, "messageId" | "guard" | "expectsReply" | "replyTo" | "human" | "topic" | "passive" | "delivery"> & {
5
+ readonly messageId: string;
6
+ readonly guard: DeliveryGuard;
7
+ readonly expectsReply: false;
8
+ readonly delivery: "queue" | "steer" | "passive";
9
+ };
10
+ export interface IntercomClientOptions extends BrokerOptions {
11
+ readonly spawn: false;
12
+ readonly session: Omit<SessionInfo, "id" | "guardedDelivery" | "applicationGuard" | "topics" | "subscriptions">;
13
+ readonly requestedId?: string;
14
+ readonly onError?: (error: Readonly<Error>) => void;
15
+ }
16
+ export interface IntercomClient {
17
+ readonly sessionId: string | null;
18
+ readonly isConnected: () => boolean;
19
+ readonly listSessions: () => Promise<readonly SessionInfo[]>;
20
+ readonly send: (to: string, options: GuardedSendOptions) => Promise<SendResult>;
21
+ readonly last: (to: string, query?: LastMessageQuery) => Promise<LastMessageResult>;
22
+ readonly receipt: (to: string, query: ReceiptQuery) => Promise<ReceiptResult>;
23
+ readonly disconnect: () => Promise<void>;
24
+ }
25
+ /** Connect to an existing native broker. No Pi runtime, broker spawn or automatic retry. */
26
+ export declare function connectIntercom(options: IntercomClientOptions): Promise<IntercomClient>;
@@ -0,0 +1,45 @@
1
+ import { IntercomClient as BrokerClient, } from "./pi-intercom/broker/client.js";
2
+ import { isDeliveryGuard, } from "./pi-intercom/types.js";
3
+ import { isRecord } from "./shared/unknown.js";
4
+ function requireNoSpawn(value) {
5
+ if (value !== false) {
6
+ throw new Error("The supported Intercom client requires spawn:false.");
7
+ }
8
+ }
9
+ function guardedSend(to, input) {
10
+ return (isRecord(input) &&
11
+ isDeliveryGuard(input.guard) &&
12
+ to === input.guard.peerId &&
13
+ typeof input.messageId === "string" &&
14
+ input.expectsReply === false &&
15
+ input.delivery !== undefined);
16
+ }
17
+ /** Connect to an existing native broker. No Pi runtime, broker spawn or automatic retry. */
18
+ export async function connectIntercom(options) {
19
+ requireNoSpawn(options.spawn);
20
+ const client = new BrokerClient(options);
21
+ client.on("error", (error) => options.onError?.(error));
22
+ try {
23
+ await client.connect(options.session, options.requestedId);
24
+ }
25
+ catch (error) {
26
+ await client.disconnect();
27
+ throw error;
28
+ }
29
+ return Object.freeze({
30
+ get sessionId() {
31
+ return client.sessionId;
32
+ },
33
+ isConnected: () => client.isConnected(),
34
+ listSessions: () => client.listSessions(),
35
+ async send(to, input) {
36
+ if (!guardedSend(to, input)) {
37
+ throw new Error("An exact guarded nonblocking send with an original message ID is required.");
38
+ }
39
+ return await client.send(to, input);
40
+ },
41
+ last: (to, query) => client.last(to, query),
42
+ receipt: (to, query) => client.receipt(to, query),
43
+ disconnect: () => client.disconnect(),
44
+ });
45
+ }
@@ -0,0 +1 @@
1
+ export declare function getPiAgentDir(): string;
@@ -4,6 +4,7 @@ import { IntercomClient } from "./broker/client.js";
4
4
  import { resolveSessionProjectId } from "./session-targets.js";
5
5
  import { Fault, cancelled, log, wait } from "./bridge-protocol.js";
6
6
  import { Mailbox } from "./bridge-mailbox.js";
7
+ import { hasErrorCode } from "../shared/unknown.js";
7
8
  import { resolveBridgeTarget, bridgeSessionList } from "./bridge-target.js";
8
9
  const MAX_REQUESTS = 8;
9
10
  const IDLE_TIMEOUT = 5 * 60 * 1000;
@@ -222,7 +223,7 @@ export class Session {
222
223
  }
223
224
  catch (error) {
224
225
  cancelled(signal);
225
- if (error instanceof Error && error.message === "List sessions timeout") {
226
+ if (hasErrorCode(error, "list_timeout")) {
226
227
  throw new Fault(504, "list_timeout", "Broker session list deadline exceeded.");
227
228
  }
228
229
  throw new Fault(503, "broker_unavailable", "Broker session list failed.");