@fitchmultz/pi-subagents 0.50.0 → 0.52.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 (180) hide show
  1. package/CHANGELOG.md +63 -2
  2. package/README.md +36 -5
  3. package/dist/agent/child-runtime.js +80 -0
  4. package/dist/agent/contracts.js +15 -24
  5. package/dist/agent/host.js +3 -16
  6. package/dist/agent/index.js +212 -192
  7. package/dist/agent/journal-tail.js +115 -156
  8. package/dist/agent/launch.js +20 -8
  9. package/dist/agent/legacy.js +65 -23
  10. package/dist/agent/notifications.js +138 -47
  11. package/dist/agent/observation-state.js +246 -0
  12. package/dist/agent/observation-view.js +258 -0
  13. package/dist/agent/observation.js +350 -0
  14. package/dist/agent/observe-card.js +243 -0
  15. package/dist/agent/observe-chrome.js +85 -0
  16. package/dist/agent/observe-client.js +285 -0
  17. package/dist/agent/observe-layout.js +65 -0
  18. package/dist/agent/observe-reading.js +84 -0
  19. package/dist/agent/observe-screen.js +400 -96
  20. package/dist/agent/observe-transcript.js +382 -0
  21. package/dist/agent/observe.js +250 -43
  22. package/dist/agent/parking.js +253 -0
  23. package/dist/agent/process.js +228 -28
  24. package/dist/agent/receipts.js +92 -20
  25. package/dist/agent/runner.js +229 -126
  26. package/dist/agent/shell.js +2 -1
  27. package/dist/agent/storage.js +47 -12
  28. package/dist/agent/tool-schemas.js +20 -0
  29. package/dist/agent/typebox.js +10 -5
  30. package/dist/agent/usage.js +0 -115
  31. package/dist/agent/view.js +46 -16
  32. package/dist/agent/wake.js +99 -0
  33. package/dist/build-info.js +1 -1
  34. package/dist/intercom-client.d.ts +26 -0
  35. package/dist/intercom-client.js +45 -0
  36. package/dist/pi-intercom/agent-dir.d.ts +1 -0
  37. package/dist/pi-intercom/bridge-session.js +2 -1
  38. package/dist/pi-intercom/broker/broker.js +62 -43
  39. package/dist/pi-intercom/broker/client-last.d.ts +24 -0
  40. package/dist/pi-intercom/broker/client-last.js +42 -5
  41. package/dist/pi-intercom/broker/client-park.d.ts +9 -0
  42. package/dist/pi-intercom/broker/client-park.js +21 -0
  43. package/dist/pi-intercom/broker/client.d.ts +59 -0
  44. package/dist/pi-intercom/broker/client.js +55 -29
  45. package/dist/pi-intercom/broker/connection.d.ts +29 -0
  46. package/dist/pi-intercom/broker/framing.d.ts +17 -0
  47. package/dist/pi-intercom/broker/framing.js +10 -7
  48. package/dist/pi-intercom/broker/guard.js +23 -0
  49. package/dist/pi-intercom/broker/last.d.ts +30 -0
  50. package/dist/pi-intercom/broker/last.js +87 -40
  51. package/dist/pi-intercom/broker/parked.js +107 -0
  52. package/dist/pi-intercom/broker/paths.d.ts +13 -0
  53. package/dist/pi-intercom/broker/paths.js +35 -12
  54. package/dist/pi-intercom/broker/pid.js +5 -1
  55. package/dist/pi-intercom/broker/server-connection.js +20 -1
  56. package/dist/pi-intercom/broker/snapshot-reader.d.ts +9 -0
  57. package/dist/pi-intercom/broker/snapshots.d.ts +29 -0
  58. package/dist/pi-intercom/broker/targets.d.ts +9 -0
  59. package/dist/pi-intercom/connection.js +71 -20
  60. package/dist/pi-intercom/identity.js +6 -5
  61. package/dist/pi-intercom/inbound-delivery.js +16 -60
  62. package/dist/pi-intercom/inbound-journal.js +83 -6
  63. package/dist/pi-intercom/inbound-record.js +18 -1
  64. package/dist/pi-intercom/inbound.js +22 -37
  65. package/dist/pi-intercom/index.js +41 -0
  66. package/dist/pi-intercom/last-message.js +4 -16
  67. package/dist/pi-intercom/lifecycle.js +88 -3
  68. package/dist/pi-intercom/outbound.js +5 -15
  69. package/dist/pi-intercom/reply-tracker.js +7 -6
  70. package/dist/pi-intercom/reply-wait.js +2 -2
  71. package/dist/pi-intercom/session-targets.d.ts +24 -0
  72. package/dist/pi-intercom/tool-registration.js +4 -1
  73. package/dist/pi-intercom/tool-results.js +5 -7
  74. package/dist/pi-intercom/tool-schemas.js +12 -26
  75. package/dist/pi-intercom/types.d.ts +212 -0
  76. package/dist/pi-intercom/types.js +54 -3
  77. package/dist/pi-intercom/validation.d.ts +12 -0
  78. package/dist/pi-intercom/validation.js +3 -0
  79. package/dist/shared/bounded-json.js +151 -0
  80. package/dist/shared/compact-cards.js +66 -0
  81. package/dist/shared/journal-frames.js +3 -6
  82. package/dist/shared/json-projection.js +267 -129
  83. package/dist/shared/native-journal.js +123 -56
  84. package/dist/shared/parking.js +12 -0
  85. package/dist/shared/post-exit-stdio-guard.js +120 -7
  86. package/dist/shared/tui-action.js +29 -0
  87. package/dist/shared/unknown.d.ts +9 -0
  88. package/docs/code-quality.md +5 -3
  89. package/docs/development.md +42 -3
  90. package/docs/intercom-bridge.md +9 -4
  91. package/docs/intercom.md +228 -7
  92. package/docs/performance.md +118 -14
  93. package/docs/reference.md +168 -20
  94. package/docs/retired-history-maintenance.md +252 -0
  95. package/package.json +28 -15
  96. package/scripts/archive-retired-history.mjs +464 -0
  97. package/scripts/build.mjs +48 -28
  98. package/scripts/measure-size.mjs +1 -1
  99. package/scripts/native-package-smoke.mjs +153 -12
  100. package/scripts/package-smoke.mjs +80 -11
  101. package/scripts/prepare.mjs +2 -2
  102. package/scripts/real-pi-live.mjs +377 -0
  103. package/scripts/real-pi-processes.mjs +6 -1
  104. package/scripts/real-pi-smoke.mjs +6 -282
  105. package/scripts/run-tests.mjs +4 -2
  106. package/scripts/typescript.mjs +35 -0
  107. package/skills/pi-intercom/SKILL.md +2 -1
  108. package/skills/pi-subagents/SKILL.md +23 -5
  109. package/src/agent/child-runtime.ts +105 -0
  110. package/src/agent/contracts.ts +18 -34
  111. package/src/agent/host.ts +39 -20
  112. package/src/agent/index.ts +234 -207
  113. package/src/agent/journal-tail.ts +141 -153
  114. package/src/agent/launch.ts +33 -9
  115. package/src/agent/legacy.ts +85 -31
  116. package/src/agent/notifications.ts +192 -67
  117. package/src/agent/observation-state.ts +277 -0
  118. package/src/agent/observation-view.ts +267 -0
  119. package/src/agent/observation.ts +409 -0
  120. package/src/agent/observe-card.ts +304 -0
  121. package/src/agent/observe-chrome.ts +170 -0
  122. package/src/agent/observe-client.ts +318 -0
  123. package/src/agent/observe-layout.ts +79 -0
  124. package/src/agent/observe-reading.ts +97 -0
  125. package/src/agent/observe-screen.ts +443 -129
  126. package/src/agent/observe-transcript.ts +434 -0
  127. package/src/agent/observe.ts +304 -71
  128. package/src/agent/parking.ts +341 -0
  129. package/src/agent/process.ts +280 -37
  130. package/src/agent/receipts.ts +114 -24
  131. package/src/agent/runner.ts +227 -149
  132. package/src/agent/shell.ts +2 -1
  133. package/src/agent/storage.ts +44 -11
  134. package/src/agent/tool-schemas.ts +33 -0
  135. package/src/agent/typebox.ts +12 -7
  136. package/src/agent/usage.ts +0 -130
  137. package/src/agent/view.ts +70 -20
  138. package/src/agent/wake.ts +107 -0
  139. package/src/intercom-client.ts +108 -0
  140. package/src/pi-intercom/bridge-session.ts +2 -2
  141. package/src/pi-intercom/broker/broker.ts +64 -53
  142. package/src/pi-intercom/broker/client-last.ts +65 -12
  143. package/src/pi-intercom/broker/client-park.ts +23 -0
  144. package/src/pi-intercom/broker/client.ts +63 -45
  145. package/src/pi-intercom/broker/framing.ts +11 -8
  146. package/src/pi-intercom/broker/guard.ts +33 -0
  147. package/src/pi-intercom/broker/last.ts +131 -43
  148. package/src/pi-intercom/broker/parked.ts +130 -0
  149. package/src/pi-intercom/broker/paths.ts +39 -12
  150. package/src/pi-intercom/broker/pid.ts +6 -1
  151. package/src/pi-intercom/broker/server-connection.ts +27 -2
  152. package/src/pi-intercom/connection.ts +103 -39
  153. package/src/pi-intercom/identity.ts +8 -5
  154. package/src/pi-intercom/inbound-delivery.ts +20 -66
  155. package/src/pi-intercom/inbound-journal.ts +103 -7
  156. package/src/pi-intercom/inbound-record.ts +21 -4
  157. package/src/pi-intercom/inbound.ts +32 -49
  158. package/src/pi-intercom/index.ts +44 -0
  159. package/src/pi-intercom/last-message.ts +11 -20
  160. package/src/pi-intercom/lifecycle.ts +117 -14
  161. package/src/pi-intercom/outbound.ts +7 -16
  162. package/src/pi-intercom/reply-tracker.ts +8 -6
  163. package/src/pi-intercom/reply-wait.ts +3 -2
  164. package/src/pi-intercom/runtime-types.ts +0 -1
  165. package/src/pi-intercom/tool-registration.ts +4 -1
  166. package/src/pi-intercom/tool-results.ts +10 -9
  167. package/src/pi-intercom/tool-schemas.ts +12 -44
  168. package/src/pi-intercom/types.ts +115 -4
  169. package/src/pi-intercom/validation.ts +4 -0
  170. package/src/shared/bounded-json.ts +162 -0
  171. package/src/shared/compact-cards.ts +118 -0
  172. package/src/shared/journal-frames.ts +3 -19
  173. package/src/shared/json-projection.ts +267 -147
  174. package/src/shared/native-journal.ts +157 -75
  175. package/src/shared/parking.ts +22 -0
  176. package/src/shared/post-exit-stdio-guard.ts +165 -7
  177. package/src/shared/tui-action.ts +39 -0
  178. package/tsconfig.client.json +8 -0
  179. package/tsconfig.json +1 -0
  180. package/src/shared/stream-json.d.ts +0 -10
@@ -1,5 +1,11 @@
1
+ import { randomUUID } from "node:crypto";
1
2
  import { buildPresenceIdentity } from "./identity.js";
2
3
  import { resolveSessionProjectId } from "./session-targets.js";
4
+ import { isContextTarget, } from "./types.js";
5
+ import { isRecord, safeLabel } from "./validation.js";
6
+ export function isContextRequest(value) {
7
+ return isRecord(value) && typeof value.respond === "function";
8
+ }
3
9
  /** Owns native context validity and occupancy, never journal or socket state. */
4
10
  export class IntercomLifecycle {
5
11
  context = null;
@@ -7,6 +13,10 @@ export class IntercomLifecycle {
7
13
  model = "unknown";
8
14
  disposed = true;
9
15
  epoch = 0;
16
+ incarnation = null;
17
+ peerId = null;
18
+ guardedDelivery = false;
19
+ applicationGuard;
10
20
  running = false;
11
21
  activity = 0;
12
22
  tools = new Map();
@@ -27,15 +37,87 @@ export class IntercomLifecycle {
27
37
  get toolCount() {
28
38
  return this.tools.size;
29
39
  }
40
+ registerContextEvents(sync) {
41
+ this.pi.events.on("pi-intercom:context-query", (request) => this.contextRequest(request));
42
+ this.pi.events.on("pi-intercom:application-guard", (request) => this.contextRequest(request, sync));
43
+ }
44
+ contextRequest(request, sync) {
45
+ if (!isContextRequest(request)) {
46
+ return;
47
+ }
48
+ if (request.version !== 1) {
49
+ request.respond({ error: "unsupported_version" });
50
+ return;
51
+ }
52
+ if (sync) {
53
+ if (!isContextTarget(request.target) ||
54
+ !this.matches(request.target) ||
55
+ !(request.revision === null ||
56
+ (typeof request.revision === "string" && safeLabel(request.revision)))) {
57
+ request.respond({ error: "context_changed" });
58
+ return;
59
+ }
60
+ this.applicationGuard = request.revision === null ? undefined : request.revision;
61
+ sync();
62
+ this.publishContext();
63
+ }
64
+ request.respond(this.snapshot());
65
+ }
66
+ snapshot() {
67
+ if (!this.live()) {
68
+ return {
69
+ version: 1,
70
+ rootSessionId: null,
71
+ peerId: null,
72
+ incarnation: null,
73
+ connected: false,
74
+ guardedDelivery: null,
75
+ };
76
+ }
77
+ return {
78
+ version: 1,
79
+ rootSessionId: this.lineage?.rootSessionId ?? this.sessionId,
80
+ parentSessionId: this.lineage?.parentSessionId,
81
+ peerId: this.peerId,
82
+ incarnation: this.incarnation,
83
+ connected: this.peerId !== null,
84
+ guardedDelivery: this.guardedDelivery ? 1 : null,
85
+ applicationGuard: this.applicationGuard,
86
+ };
87
+ }
88
+ publishContext() {
89
+ this.pi.events.emit("pi-intercom:context-state", this.snapshot());
90
+ }
91
+ transport(peerId, guardedDelivery = false) {
92
+ this.peerId = peerId;
93
+ this.guardedDelivery = guardedDelivery;
94
+ this.publishContext();
95
+ }
96
+ matches(target) {
97
+ const current = this.snapshot();
98
+ return (current.connected &&
99
+ current.rootSessionId === target.rootSessionId &&
100
+ current.peerId === target.peerId &&
101
+ current.incarnation === target.incarnation);
102
+ }
103
+ accepts(guard) {
104
+ return (this.guardedDelivery &&
105
+ this.lineage?.parentSessionId === undefined &&
106
+ this.matches(guard) &&
107
+ this.applicationGuard === guard.applicationGuard);
108
+ }
30
109
  start(ctx) {
31
110
  this.disposed = false;
32
111
  this.epoch += 1;
33
112
  this.context = ctx;
34
113
  this.sessionId = ctx.sessionManager.getSessionId();
114
+ this.incarnation = randomUUID();
115
+ this.applicationGuard = undefined;
35
116
  this.model = ctx.model?.id ?? "unknown";
36
117
  this.running = false;
37
118
  this.activity = 0;
38
119
  this.tools.clear();
120
+ this.transport(null);
39
121
  return this.epoch;
40
122
  }
41
123
  invalidate() {
@@ -43,6 +125,7 @@ export class IntercomLifecycle {
43
125
  this.epoch += 1;
44
126
  this.running = false;
45
127
  this.tools.clear();
128
+ this.publishContext();
46
129
  }
47
130
  clear() {
48
131
  this.context = null;
@@ -129,12 +212,14 @@ export class IntercomLifecycle {
129
212
  if (!live || this.sessionId === null) {
130
213
  throw new Error("Intercom runtime not initialized");
131
214
  }
132
- const cwd = live.cwd;
133
215
  return {
134
216
  ...this.identity(this.sessionId),
135
- cwd,
217
+ incarnation: this.incarnation ?? undefined,
218
+ guardedDelivery: 1,
219
+ applicationGuard: this.applicationGuard,
220
+ cwd: live.cwd,
136
221
  model: this.model,
137
- projectId: await resolveSessionProjectId(cwd),
222
+ projectId: await resolveSessionProjectId(live.cwd),
138
223
  lastSeen: Date.now(),
139
224
  status: this.status(),
140
225
  ...this.health(pendingAsks),
@@ -3,7 +3,7 @@ import { resolveConnectedTarget, resolvePeerHealth, } from "./connection.js";
3
3
  import { RECIPIENT_TURN_FAILED_ATTACHMENT, } from "./runtime-types.js";
4
4
  import { errorMessage } from "./validation.js";
5
5
  import { formatAttachments } from "./message-format.js";
6
- import { AskDeliveryError, failureDetails, replyFailureReason, } from "./tool-results.js";
6
+ import { AskDeliveryError, failureDetails, RecoveryError, } from "./tool-results.js";
7
7
  import { sendOptions, toolError, toolText } from "./tool-arguments.js";
8
8
  export class IntercomOutbound {
9
9
  owners;
@@ -101,8 +101,7 @@ export class IntercomOutbound {
101
101
  return this.sent(to, params, result);
102
102
  }
103
103
  catch (error) {
104
- const text = errorMessage(error);
105
- return toolError(`Failed to send: ${text}`, failureDetails(text.startsWith("Target ") ? "ambiguous_target" : "send_failed", [
104
+ return toolError(`Failed to send: ${errorMessage(error)}`, failureDetails(error instanceof RecoveryError ? error.code : "send_failed", [
106
105
  { action: "list" },
107
106
  { action: "send", guidance: "Retry with an exact active recipient target." },
108
107
  ], { error: true }));
@@ -147,15 +146,7 @@ export class IntercomOutbound {
147
146
  const result = this.deliveryFailure(to, error.result);
148
147
  return { ...result, details: { ...result.details, error: true } };
149
148
  }
150
- const text = errorMessage(error);
151
- let reason = "ask_failed";
152
- if (text.startsWith("No reply from")) {
153
- reason = "reply_timeout";
154
- }
155
- else if (text.startsWith("Target ")) {
156
- reason = "ambiguous_target";
157
- }
158
- return toolError(`Failed: ${text}`, failureDetails(reason, [
149
+ return toolError(`Failed: ${errorMessage(error)}`, failureDetails(error instanceof RecoveryError ? error.code : "ask_failed", [
159
150
  { action: "status" },
160
151
  { action: "list" },
161
152
  {
@@ -293,8 +284,7 @@ export class IntercomOutbound {
293
284
  }
294
285
  }
295
286
  replyFailure(error, replyTo) {
296
- const text = errorMessage(error);
297
- const reason = replyFailureReason(text);
287
+ const reason = error instanceof RecoveryError ? error.code : "reply_failed";
298
288
  const actions = reason === "no_pending_reply"
299
289
  ? [
300
290
  { action: "pending" },
@@ -304,7 +294,7 @@ export class IntercomOutbound {
304
294
  },
305
295
  ]
306
296
  : [{ action: "pending" }, { action: "list" }];
307
- return toolError(`Failed to reply: ${text}`, failureDetails(reason, actions, {
297
+ return toolError(`Failed to reply: ${errorMessage(error)}`, failureDetails(reason, actions, {
308
298
  error: true,
309
299
  ...((replyTo ?? "") !== "" ? { replyTo } : {}),
310
300
  }));
@@ -1,4 +1,5 @@
1
1
  import { formatSessionTarget, resolveSessionTarget, shortSessionId } from "./session-targets.js";
2
+ import { RecoveryError } from "./tool-results.js";
2
3
  function resolveBySenderTarget(contexts, to) {
3
4
  const sessions = contexts.map((context) => context.from);
4
5
  const resolution = resolveSessionTarget(sessions, to);
@@ -26,14 +27,14 @@ function pendingSenderOptions(contexts, allContexts = contexts) {
26
27
  function matchingSender(contexts, to) {
27
28
  const tooShort = tooShortSenderTargetMessage(contexts, to);
28
29
  if (tooShort !== null && tooShort !== "") {
29
- throw new Error(tooShort);
30
+ throw new RecoveryError("ambiguous_reply_target", tooShort);
30
31
  }
31
32
  return resolveBySenderTarget(contexts, to);
32
33
  }
33
34
  function explicitReply(contexts, replyTo, to) {
34
35
  const target = contexts.find((context) => context.message.id === replyTo);
35
36
  if (!target) {
36
- throw new Error(`No pending ask with replyTo "${replyTo}"`);
37
+ throw new RecoveryError("no_pending_reply", `No pending ask with replyTo "${replyTo}"`);
37
38
  }
38
39
  if (to !== "" &&
39
40
  !matchingSender(contexts, to).some((context) => context.message.id === target.message.id)) {
@@ -47,9 +48,9 @@ function replyBySender(contexts, to) {
47
48
  return matches[0];
48
49
  }
49
50
  if (matches.length > 1) {
50
- throw new Error(`Multiple pending asks from "${to}" — use one of: ${pendingSenderOptions(matches, contexts)}.`);
51
+ throw new RecoveryError("ambiguous_reply_target", `Multiple pending asks from "${to}" — use one of: ${pendingSenderOptions(matches, contexts)}.`);
51
52
  }
52
- throw new Error(`No pending ask from "${to}"`);
53
+ throw new RecoveryError("no_pending_reply", `No pending ask from "${to}"`);
53
54
  }
54
55
  export class ReplyTracker {
55
56
  pendingAsks = new Map();
@@ -151,9 +152,9 @@ export class ReplyTracker {
151
152
  return pending[0];
152
153
  }
153
154
  if (pending.length === 0) {
154
- throw new Error("No active intercom context to reply to");
155
+ throw new RecoveryError("no_pending_reply", "No active intercom context to reply to");
155
156
  }
156
- throw new Error("Multiple pending asks — specify `to`");
157
+ throw new RecoveryError("ambiguous_reply_target", "Multiple pending asks — specify `to`");
157
158
  }
158
159
  markReplied(replyTo) {
159
160
  this.removeContext(replyTo);
@@ -1,5 +1,5 @@
1
1
  import { asError } from "./validation.js";
2
- import { AskDeliveryError } from "./tool-results.js";
2
+ import { AskDeliveryError, RecoveryError } from "./tool-results.js";
3
3
  /** One correlated blocking wait; admitted before send so a fast reply cannot race it. */
4
4
  export class ReplyWait {
5
5
  waiter = null;
@@ -42,7 +42,7 @@ export class ReplyWait {
42
42
  return new Promise((resolve, reject) => {
43
43
  const { from, replyTo, signal } = options;
44
44
  const timeout = setTimeout(() => {
45
- this.reject(new Error(`No reply from "${from}" within ${Math.max(1, Math.round(this.timeoutMs / 60000))} minute(s)`));
45
+ this.reject(new RecoveryError("reply_timeout", `No reply from "${from}" within ${Math.max(1, Math.round(this.timeoutMs / 60000))} minute(s)`));
46
46
  }, this.timeoutMs);
47
47
  timeout.unref();
48
48
  const cleanup = () => {
@@ -0,0 +1,24 @@
1
+ export interface TargetIdentity {
2
+ readonly id: string;
3
+ readonly name?: string;
4
+ }
5
+ export interface ProjectSessionIdentity extends TargetIdentity {
6
+ readonly cwd: string;
7
+ readonly projectId?: string;
8
+ }
9
+ export declare const MIN_SESSION_TARGET_PREFIX_LENGTH = 8;
10
+ export declare function nativePeerId(sessionId: string): string;
11
+ export interface TargetResolution<T extends TargetIdentity> {
12
+ readonly status: "none" | "found" | "ambiguous" | "prefix_too_short";
13
+ readonly target?: T;
14
+ readonly matches: readonly T[];
15
+ }
16
+ export declare function shortSessionId(sessionId: string): string;
17
+ export declare function resolveSessionProjectId(cwd: string): Promise<string>;
18
+ export declare function formatSessionTarget(session: TargetIdentity, allSessions?: readonly TargetIdentity[]): string;
19
+ export declare function formatTargetOptions(sessions: readonly TargetIdentity[], allSessions?: readonly TargetIdentity[]): string;
20
+ export declare function targetDisplayName(session: TargetIdentity, allSessions?: readonly TargetIdentity[]): string;
21
+ export declare const PEER_AWARENESS_HINT = "Other Pi sessions may be connected to this project. Use load_intercom({}) if needed, then intercom({ action: \"list\" }) before changing shared state or coordinating known overlapping work. Routine standalone read-only tasks do not need a peer check. Coordinate only when work overlaps; use subagent controls for managed child runs.";
22
+ export declare function filterProjectSessions<T extends ProjectSessionIdentity>(sessions: readonly T[], currentSessionId: string): T[];
23
+ export declare function formatPeerAwarenessHint(sessions: readonly ProjectSessionIdentity[], currentSessionId: string): string | undefined;
24
+ export declare function resolveSessionTarget<T extends TargetIdentity>(sessions: readonly T[], rawTarget: string): TargetResolution<T>;
@@ -1,11 +1,14 @@
1
1
  import { throwIfToolError } from "./tool-results.js";
2
2
  import { intercomSchema } from "./tool-schemas.js";
3
+ import { registerCompactMessages, renderCompactResult } from "../shared/compact-cards.js";
3
4
  export function registerIntercomTools(pi, tools) {
5
+ registerCompactMessages(pi, ["intercom_message", "subagent-human-message"]);
4
6
  pi.registerTool({
5
7
  name: "intercom",
6
8
  label: "Intercom",
7
- description: "Discover and message connected Pi sessions in your collaboration group; main sessions can also reach other mains, across projects. Use list for unambiguous targets. Send defaults to steer at the next tool boundary; queue defers; passive never wakes the model. Ask waits for a correlated reply; reply answers the active or single pending ask (specify replyTo when ambiguous). Pending lists asks; status shows connection and inbound queue state. Last reads a peer's original completed assistant response without waking it; page using entryId and nextOffset. Broker acceptance is not recipient persistence, consumption, or task completion.",
9
+ description: "Message connected Pi sessions in your group; mains also reach other mains across projects. List shows target IDs. Ask waits for a correlated reply; reply answers the active or sole pending ask. Pending lists asks; status shows connection and inbound queue. Last pages a peer's last completed response without waking it. Broker acceptance is not recipient persistence, consumption or completion.",
8
10
  parameters: intercomSchema,
11
+ renderResult: renderCompactResult,
9
12
  async execute(_id, params, signal, _update, ctx) {
10
13
  const result = throwIfToolError(await tools.execute(params, signal, ctx));
11
14
  return { content: [...result.content], details: result.details };
@@ -1,14 +1,12 @@
1
1
  export function failureDetails(reasonCode, nextActions, details = {}) {
2
2
  return { ...details, reasonCode, nextActions };
3
3
  }
4
- export function replyFailureReason(message) {
5
- if (message.startsWith("No active intercom context") || message.startsWith("No pending ask")) {
6
- return "no_pending_reply";
7
- }
8
- if (message.startsWith("Multiple pending asks") || message.includes("too short")) {
9
- return "ambiguous_reply_target";
4
+ export class RecoveryError extends Error {
5
+ code;
6
+ constructor(code, message) {
7
+ super(message);
8
+ this.code = code;
10
9
  }
11
- return "reply_failed";
12
10
  }
13
11
  export function firstTextContent(result) {
14
12
  return (result.content
@@ -1,42 +1,28 @@
1
1
  import { StringEnum } from "@earendil-works/pi-ai";
2
2
  import { Type } from "../agent/typebox.js";
3
3
  export const intercomSchema = Type.Object({
4
- action: StringEnum(["list", "send", "ask", "reply", "pending", "status", "last"], {
5
- description: "List, send, ask, reply, pending asks, connection status, or last completed assistant response.",
6
- }),
4
+ action: StringEnum(["list", "send", "ask", "reply", "pending", "status", "last"]),
7
5
  scope: Type.Optional(StringEnum(["project", "all"], {
8
- description: "For list/status: 'all' (default) includes every reachable session; 'project' further filters this repository and its worktrees.",
9
- })),
10
- to: Type.Optional(Type.String({
11
- description: "Target session name or ID (for 'send', 'ask', or disambiguating 'reply')",
12
- })),
13
- message: Type.Optional(Type.String({
14
- description: "Message for send/ask/reply",
6
+ description: "all (default) or project (repo and worktrees)",
15
7
  })),
8
+ to: Type.Optional(Type.String({ description: "Session name or ID" })),
9
+ message: Type.Optional(Type.String()),
16
10
  attachments: Type.Optional(Type.Array(Type.Object({
17
11
  type: StringEnum(["file", "snippet", "context"]),
18
12
  name: Type.String(),
19
13
  content: Type.String(),
20
14
  language: Type.Optional(Type.String()),
21
15
  }))),
22
- replyTo: Type.Optional(Type.String({
23
- description: "Message ID to reply to (for threading or responding to an 'ask')",
24
- })),
16
+ replyTo: Type.Optional(Type.String({ description: "Message ID to answer or thread" })),
25
17
  delivery: Type.Optional(StringEnum(["queue", "steer", "passive"], {
26
- description: "Delivery mode. Omitted send delivery defaults to 'steer', which injects after the current tool call. Use 'queue' only to intentionally wait behind active work; 'passive' does not wake the recipient model.",
18
+ description: "steer (default) at next tool boundary; queue after active work; passive never wakes the model",
27
19
  })),
28
20
  queueMode: Type.Optional(StringEnum(["stack", "replace"], {
29
- description: "For delivery='queue': 'stack' keeps all messages; 'replace' keeps only the latest undelivered message for the same threadId.",
30
- })),
31
- threadId: Type.Optional(Type.String({ description: "Stable topic key for queueMode='replace'." })),
32
- entryId: Type.Optional(Type.String({ description: "Last response entry ID for stable paging." })),
33
- offset: Type.Optional(Type.Integer({ minimum: 0, description: "Last response character offset (UTF-16)." })),
34
- limit: Type.Optional(Type.Integer({
35
- minimum: 1,
36
- maximum: 16384,
37
- description: "Last response page size; default/max 16384 characters.",
38
- })),
39
- passive: Type.Optional(Type.Boolean({
40
- description: "For action='send' only: legacy alias for delivery='passive'. Discouraged for agent-to-agent messages.",
21
+ description: "stack keeps all; replace keeps only the latest per threadId",
41
22
  })),
23
+ threadId: Type.Optional(Type.String()),
24
+ entryId: Type.Optional(Type.String()),
25
+ offset: Type.Optional(Type.Integer({ minimum: 0, description: "UTF-16 characters" })),
26
+ limit: Type.Optional(Type.Integer({ minimum: 1, maximum: 16384 })),
27
+ passive: Type.Optional(Type.Boolean({ description: "Legacy alias for delivery=passive" })),
42
28
  });
@@ -0,0 +1,212 @@
1
+ export interface SessionInfo {
2
+ readonly id: string;
3
+ readonly name?: string;
4
+ readonly cwd: string;
5
+ readonly model: string;
6
+ /** Opaque same-repository/worktree identity used for ambient peer awareness. */
7
+ readonly projectId?: string;
8
+ readonly rootSessionId?: string;
9
+ readonly parentSessionId?: string;
10
+ readonly incarnation?: string;
11
+ readonly guardedDelivery?: 1;
12
+ readonly applicationGuard?: string;
13
+ readonly status?: string;
14
+ readonly lastSeen?: number;
15
+ readonly lastIntercomActivity?: number;
16
+ readonly pendingAsks?: number;
17
+ readonly acceptsAsks?: boolean;
18
+ readonly subscriptions?: readonly TopicSubscription[];
19
+ readonly topics?: readonly TopicUpdate[];
20
+ }
21
+ export interface TopicSubscription {
22
+ readonly topic: string;
23
+ readonly awaitRelease?: boolean;
24
+ }
25
+ export interface TopicUpdate {
26
+ readonly topic: string;
27
+ readonly text: string;
28
+ readonly event: "update" | "blocker" | "decision" | "release";
29
+ readonly resource?: string;
30
+ readonly ownership?: "held" | "released";
31
+ readonly revision: number;
32
+ readonly updatedAt: number;
33
+ }
34
+ export declare function isTopicSubscription(value: unknown): value is TopicSubscription;
35
+ export declare function isTopicUpdate(value: unknown): value is TopicUpdate;
36
+ export type MessageDelivery = "queue" | "steer" | "passive";
37
+ export type QueueMode = "stack" | "replace";
38
+ export interface ContextTarget {
39
+ readonly rootSessionId: string;
40
+ readonly peerId: string;
41
+ readonly incarnation: string;
42
+ }
43
+ export interface DeliveryGuard extends ContextTarget {
44
+ readonly applicationGuard: string;
45
+ }
46
+ export interface PublicationReplyHint {
47
+ readonly tool: string;
48
+ readonly commandId: string;
49
+ readonly commandFingerprint?: string;
50
+ }
51
+ export type SendOptions = Omit<Message, "id" | "timestamp" | "content"> & Message["content"] & {
52
+ readonly messageId?: string;
53
+ };
54
+ export declare function isContextTarget(value: unknown): value is ContextTarget;
55
+ export declare function isDeliveryGuard(value: unknown): value is DeliveryGuard;
56
+ export declare function isPublicationReplyHint(value: unknown): value is PublicationReplyHint;
57
+ export interface HumanMessageOrigin {
58
+ readonly ownerSessionId: string;
59
+ readonly runId: string;
60
+ readonly index: number;
61
+ }
62
+ export declare function isHumanMessageOrigin(value: unknown): value is HumanMessageOrigin;
63
+ export interface Message {
64
+ readonly id: string;
65
+ readonly timestamp: number;
66
+ readonly replyTo?: string;
67
+ readonly expectsReply?: boolean;
68
+ /** Recipients verify parent ownership before elevating human origin. */
69
+ readonly human?: HumanMessageOrigin;
70
+ readonly topic?: TopicUpdate;
71
+ readonly delivery?: MessageDelivery;
72
+ readonly queueMode?: QueueMode;
73
+ readonly threadId?: string;
74
+ readonly passive?: boolean;
75
+ readonly guard?: DeliveryGuard;
76
+ readonly replyHint?: PublicationReplyHint;
77
+ readonly content: {
78
+ readonly text: string;
79
+ readonly attachments?: readonly Attachment[];
80
+ };
81
+ }
82
+ export interface Attachment {
83
+ readonly type: "file" | "snippet" | "context";
84
+ readonly name: string;
85
+ readonly content: string;
86
+ readonly language?: string;
87
+ }
88
+ export declare function isAttachment(value: unknown): value is Attachment;
89
+ export declare function isMessage(value: unknown): value is Message;
90
+ export declare function isSessionRegistration(value: unknown): value is Omit<SessionInfo, "id">;
91
+ export declare function normalizeSessionInfo(value: unknown): SessionInfo | null;
92
+ export interface SendResult {
93
+ readonly id: string;
94
+ readonly accepted: boolean;
95
+ readonly delivered: boolean;
96
+ readonly queued?: boolean;
97
+ readonly reason?: string;
98
+ }
99
+ export type TopicChange = {
100
+ readonly action: "publish" | "restore";
101
+ readonly topic: TopicUpdate;
102
+ } | {
103
+ readonly action: "subscribe" | "restore";
104
+ readonly subscription: TopicSubscription;
105
+ } | {
106
+ readonly action: "unsubscribe";
107
+ readonly topic: string;
108
+ };
109
+ export interface SessionSnapshot {
110
+ readonly sessions: readonly SessionInfo[];
111
+ readonly receipts: readonly (SendResult & {
112
+ readonly to: string;
113
+ })[];
114
+ }
115
+ type CompactTopicMessage = Omit<Message, "topic"> & {
116
+ readonly topic?: Omit<TopicUpdate, "text"> & {
117
+ readonly text?: string;
118
+ };
119
+ };
120
+ /** Topic text and the standard coalescing key need only one copy on the wire. */
121
+ export declare function compactTopicMessage(message: Message): CompactTopicMessage;
122
+ export declare function normalizeMessage(value: unknown): Message | null;
123
+ export interface LastMessageQuery {
124
+ readonly entryId?: string;
125
+ readonly offset?: number;
126
+ readonly limit?: number;
127
+ }
128
+ export declare const LAST_MESSAGE_PAGE_LIMIT = 16384;
129
+ export declare function isLastMessageQuery(value: unknown): value is LastMessageQuery;
130
+ export type LastMessageResult = {
131
+ readonly session: SessionInfo;
132
+ readonly entryId: string | null;
133
+ readonly text: string;
134
+ readonly offset: number;
135
+ readonly totalLength: number;
136
+ readonly nextOffset?: number;
137
+ } | {
138
+ readonly error: string;
139
+ };
140
+ export interface ReceiptQuery {
141
+ readonly rootSessionId: string;
142
+ readonly messageId: string;
143
+ }
144
+ export declare function isReceiptQuery(value: unknown): value is ReceiptQuery;
145
+ export interface NativeReceipt extends ReceiptQuery {
146
+ readonly version: 1;
147
+ readonly sender: {
148
+ readonly peerId: string;
149
+ readonly rootSessionId?: string;
150
+ readonly incarnation?: string;
151
+ };
152
+ readonly textSha256: string;
153
+ readonly delivery: MessageDelivery;
154
+ readonly guard?: DeliveryGuard;
155
+ readonly replyHint?: PublicationReplyHint;
156
+ readonly checkpointEntryId: string;
157
+ readonly receivedAt: number;
158
+ /** Historical observations, not a promise that an input is still queued. */
159
+ readonly queued: boolean;
160
+ readonly nativeHandoff: boolean;
161
+ readonly placementEntryId: string | null;
162
+ readonly discarded: boolean;
163
+ }
164
+ export type ReceiptResult = {
165
+ readonly receipt: NativeReceipt;
166
+ } | {
167
+ readonly error: string;
168
+ };
169
+ export type BrokerMessage = {
170
+ readonly type: "registered";
171
+ readonly sessionId: string;
172
+ readonly topicsSupported?: true;
173
+ readonly topicFrames?: true;
174
+ readonly lastMessages?: true;
175
+ readonly receiptReads?: 1;
176
+ readonly guardedDelivery?: 1;
177
+ } | {
178
+ readonly type: "sessions";
179
+ readonly requestId: string;
180
+ readonly sessions: readonly (Partial<SessionInfo> & Pick<SessionInfo, "id">)[];
181
+ readonly more?: boolean;
182
+ readonly receipts?: SessionSnapshot["receipts"];
183
+ readonly error?: string;
184
+ } | {
185
+ readonly type: "message";
186
+ readonly from: SessionInfo;
187
+ readonly message: Message | CompactTopicMessage;
188
+ } | {
189
+ readonly type: "last_request";
190
+ readonly requestId: string;
191
+ readonly from: SessionInfo;
192
+ readonly query: LastMessageQuery | ReceiptQuery;
193
+ } | {
194
+ readonly type: "last_response";
195
+ readonly requestId: string;
196
+ readonly result: LastMessageResult | ReceiptResult;
197
+ } | {
198
+ readonly type: "session_left";
199
+ readonly sessionId: string;
200
+ } | {
201
+ readonly type: "delivered";
202
+ readonly messageId: string;
203
+ } | {
204
+ readonly type: "delivery_queued";
205
+ readonly messageId: string;
206
+ readonly reason: string;
207
+ } | {
208
+ readonly type: "delivery_failed";
209
+ readonly messageId: string;
210
+ readonly reason: string;
211
+ };
212
+ export {};