@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.
- package/CHANGELOG.md +51 -2
- package/README.md +36 -5
- package/dist/agent/contracts.js +6 -24
- package/dist/agent/host.js +3 -16
- package/dist/agent/index.js +207 -173
- package/dist/agent/journal-tail.js +115 -156
- package/dist/agent/launch.js +20 -8
- package/dist/agent/legacy.js +65 -23
- package/dist/agent/notifications.js +138 -47
- package/dist/agent/observation-state.js +246 -0
- package/dist/agent/observation-view.js +258 -0
- package/dist/agent/observation.js +346 -0
- package/dist/agent/observe-card.js +243 -0
- package/dist/agent/observe-chrome.js +85 -0
- package/dist/agent/observe-client.js +285 -0
- package/dist/agent/observe-layout.js +65 -0
- package/dist/agent/observe-reading.js +84 -0
- package/dist/agent/observe-screen.js +400 -96
- package/dist/agent/observe-transcript.js +382 -0
- package/dist/agent/observe.js +250 -43
- package/dist/agent/process.js +198 -25
- package/dist/agent/receipts.js +92 -20
- package/dist/agent/runner.js +125 -20
- package/dist/agent/shell.js +2 -1
- package/dist/agent/storage.js +29 -12
- package/dist/agent/tool-schemas.js +20 -0
- package/dist/agent/typebox.js +10 -5
- package/dist/agent/usage.js +0 -115
- package/dist/agent/view.js +46 -16
- package/dist/agent/wake.js +99 -0
- package/dist/build-info.js +1 -1
- package/dist/intercom-client.d.ts +26 -0
- package/dist/intercom-client.js +45 -0
- package/dist/pi-intercom/agent-dir.d.ts +1 -0
- package/dist/pi-intercom/bridge-session.js +2 -1
- package/dist/pi-intercom/broker/broker.js +27 -21
- package/dist/pi-intercom/broker/client-last.d.ts +13 -0
- package/dist/pi-intercom/broker/client-last.js +17 -5
- package/dist/pi-intercom/broker/client.d.ts +60 -0
- package/dist/pi-intercom/broker/client.js +47 -16
- package/dist/pi-intercom/broker/connection.d.ts +29 -0
- package/dist/pi-intercom/broker/framing.d.ts +17 -0
- package/dist/pi-intercom/broker/framing.js +10 -7
- package/dist/pi-intercom/broker/guard.js +23 -0
- package/dist/pi-intercom/broker/last.d.ts +26 -0
- package/dist/pi-intercom/broker/last.js +57 -32
- package/dist/pi-intercom/broker/paths.d.ts +13 -0
- package/dist/pi-intercom/broker/paths.js +35 -12
- package/dist/pi-intercom/broker/pid.js +5 -1
- package/dist/pi-intercom/broker/server-connection.js +19 -0
- package/dist/pi-intercom/broker/snapshot-reader.d.ts +9 -0
- package/dist/pi-intercom/broker/snapshots.d.ts +29 -0
- package/dist/pi-intercom/broker/targets.d.ts +9 -0
- package/dist/pi-intercom/connection.js +10 -2
- package/dist/pi-intercom/identity.js +6 -5
- package/dist/pi-intercom/inbound-delivery.js +16 -60
- package/dist/pi-intercom/inbound-journal.js +83 -6
- package/dist/pi-intercom/inbound-record.js +18 -1
- package/dist/pi-intercom/inbound.js +12 -38
- package/dist/pi-intercom/index.js +3 -0
- package/dist/pi-intercom/last-message.js +4 -16
- package/dist/pi-intercom/lifecycle.js +88 -3
- package/dist/pi-intercom/outbound.js +5 -15
- package/dist/pi-intercom/reply-tracker.js +7 -6
- package/dist/pi-intercom/reply-wait.js +2 -2
- package/dist/pi-intercom/session-targets.d.ts +24 -0
- package/dist/pi-intercom/tool-registration.js +4 -1
- package/dist/pi-intercom/tool-results.js +5 -7
- package/dist/pi-intercom/tool-schemas.js +12 -26
- package/dist/pi-intercom/types.d.ts +212 -0
- package/dist/pi-intercom/types.js +54 -3
- package/dist/pi-intercom/validation.d.ts +12 -0
- package/dist/pi-intercom/validation.js +3 -0
- package/dist/shared/bounded-json.js +151 -0
- package/dist/shared/compact-cards.js +66 -0
- package/dist/shared/journal-frames.js +3 -6
- package/dist/shared/json-projection.js +267 -129
- package/dist/shared/native-journal.js +123 -56
- package/dist/shared/post-exit-stdio-guard.js +120 -7
- package/dist/shared/tui-action.js +29 -0
- package/dist/shared/unknown.d.ts +9 -0
- package/docs/code-quality.md +2 -0
- package/docs/development.md +40 -1
- package/docs/intercom-bridge.md +9 -4
- package/docs/intercom.md +226 -6
- package/docs/performance.md +87 -11
- package/docs/reference.md +157 -20
- package/docs/retired-history-maintenance.md +252 -0
- package/package.json +28 -15
- package/scripts/archive-retired-history.mjs +464 -0
- package/scripts/build.mjs +48 -28
- package/scripts/measure-size.mjs +1 -1
- package/scripts/native-package-smoke.mjs +153 -12
- package/scripts/package-smoke.mjs +80 -11
- package/scripts/prepare.mjs +2 -2
- package/scripts/real-pi-live.mjs +377 -0
- package/scripts/real-pi-processes.mjs +6 -1
- package/scripts/real-pi-smoke.mjs +6 -282
- package/scripts/run-tests.mjs +4 -2
- package/scripts/typescript.mjs +35 -0
- package/skills/pi-intercom/SKILL.md +2 -1
- package/skills/pi-subagents/SKILL.md +23 -5
- package/src/agent/contracts.ts +8 -34
- package/src/agent/host.ts +19 -20
- package/src/agent/index.ts +224 -190
- package/src/agent/journal-tail.ts +141 -153
- package/src/agent/launch.ts +33 -9
- package/src/agent/legacy.ts +85 -31
- package/src/agent/notifications.ts +192 -67
- package/src/agent/observation-state.ts +277 -0
- package/src/agent/observation-view.ts +267 -0
- package/src/agent/observation.ts +405 -0
- package/src/agent/observe-card.ts +304 -0
- package/src/agent/observe-chrome.ts +170 -0
- package/src/agent/observe-client.ts +318 -0
- package/src/agent/observe-layout.ts +79 -0
- package/src/agent/observe-reading.ts +97 -0
- package/src/agent/observe-screen.ts +443 -129
- package/src/agent/observe-transcript.ts +434 -0
- package/src/agent/observe.ts +304 -71
- package/src/agent/process.ts +240 -34
- package/src/agent/receipts.ts +114 -24
- package/src/agent/runner.ts +125 -27
- package/src/agent/shell.ts +2 -1
- package/src/agent/storage.ts +27 -11
- package/src/agent/tool-schemas.ts +33 -0
- package/src/agent/typebox.ts +12 -7
- package/src/agent/usage.ts +0 -130
- package/src/agent/view.ts +70 -20
- package/src/agent/wake.ts +107 -0
- package/src/intercom-client.ts +108 -0
- package/src/pi-intercom/bridge-session.ts +2 -2
- package/src/pi-intercom/broker/broker.ts +27 -23
- package/src/pi-intercom/broker/client-last.ts +25 -12
- package/src/pi-intercom/broker/client.ts +55 -33
- package/src/pi-intercom/broker/framing.ts +11 -8
- package/src/pi-intercom/broker/guard.ts +33 -0
- package/src/pi-intercom/broker/last.ts +86 -33
- package/src/pi-intercom/broker/paths.ts +39 -12
- package/src/pi-intercom/broker/pid.ts +6 -1
- package/src/pi-intercom/broker/server-connection.ts +25 -0
- package/src/pi-intercom/connection.ts +21 -10
- package/src/pi-intercom/identity.ts +8 -5
- package/src/pi-intercom/inbound-delivery.ts +20 -66
- package/src/pi-intercom/inbound-journal.ts +103 -7
- package/src/pi-intercom/inbound-record.ts +21 -4
- package/src/pi-intercom/inbound.ts +18 -49
- package/src/pi-intercom/index.ts +3 -0
- package/src/pi-intercom/last-message.ts +10 -19
- package/src/pi-intercom/lifecycle.ts +117 -14
- package/src/pi-intercom/outbound.ts +7 -16
- package/src/pi-intercom/reply-tracker.ts +8 -6
- package/src/pi-intercom/reply-wait.ts +3 -2
- package/src/pi-intercom/runtime-types.ts +0 -1
- package/src/pi-intercom/tool-registration.ts +4 -1
- package/src/pi-intercom/tool-results.ts +10 -9
- package/src/pi-intercom/tool-schemas.ts +12 -44
- package/src/pi-intercom/types.ts +115 -4
- package/src/pi-intercom/validation.ts +4 -0
- package/src/shared/bounded-json.ts +162 -0
- package/src/shared/compact-cards.ts +118 -0
- package/src/shared/journal-frames.ts +3 -19
- package/src/shared/json-projection.ts +267 -147
- package/src/shared/native-journal.ts +157 -75
- package/src/shared/post-exit-stdio-guard.ts +165 -7
- package/src/shared/tui-action.ts +39 -0
- package/tsconfig.client.json +8 -0
- package/tsconfig.json +1 -0
- package/src/shared/stream-json.d.ts +0 -10
|
@@ -1,5 +1,8 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
2
|
+
import { isContextRequest } from "./lifecycle.js";
|
|
1
3
|
import { INBOUND_CHECKPOINT_TYPE, } from "./runtime-types.js";
|
|
2
|
-
import { isInboundCheckpoint } from "./inbound-record.js";
|
|
4
|
+
import { isInboundCheckpoint, publicationMetadata, requestedDelivery } from "./inbound-record.js";
|
|
5
|
+
import { isReceiptQuery, } from "./types.js";
|
|
3
6
|
import { inboundIdFromCustomMessage } from "./message-format.js";
|
|
4
7
|
import { isRecord } from "./validation.js";
|
|
5
8
|
export class InboundJournal {
|
|
@@ -14,6 +17,72 @@ export class InboundJournal {
|
|
|
14
17
|
this.pi = pi;
|
|
15
18
|
this.lifecycle = lifecycle;
|
|
16
19
|
this.replies = replies;
|
|
20
|
+
pi.events.on("pi-intercom:provenance-query", (request) => {
|
|
21
|
+
if (!isContextRequest(request)) {
|
|
22
|
+
return;
|
|
23
|
+
}
|
|
24
|
+
request.respond(request.version === 1 && isReceiptQuery(request)
|
|
25
|
+
? this.receipt(request)
|
|
26
|
+
: { error: "publication_unavailable" });
|
|
27
|
+
});
|
|
28
|
+
}
|
|
29
|
+
receipt(query) {
|
|
30
|
+
const ctx = this.lifecycle.live();
|
|
31
|
+
if (!ctx || ctx.sessionManager.getSessionId() !== query.rootSessionId) {
|
|
32
|
+
return { error: "publication_unavailable" };
|
|
33
|
+
}
|
|
34
|
+
let receipt;
|
|
35
|
+
for (const item of ctx.sessionManager.getEntries()) {
|
|
36
|
+
receipt = this.receiptEntry(receipt, query, item);
|
|
37
|
+
}
|
|
38
|
+
return receipt ? { receipt } : { error: "receipt_not_found" };
|
|
39
|
+
}
|
|
40
|
+
receiptEntry(receipt, query, item) {
|
|
41
|
+
if (item.type === "custom" &&
|
|
42
|
+
item.customType === INBOUND_CHECKPOINT_TYPE &&
|
|
43
|
+
isInboundCheckpoint(item.data) &&
|
|
44
|
+
item.data.sessionId === query.rootSessionId) {
|
|
45
|
+
return this.receiptCheckpoint(receipt, query, item.id, item.data);
|
|
46
|
+
}
|
|
47
|
+
return receipt && inboundIdFromCustomMessage(item) === query.messageId
|
|
48
|
+
? { ...receipt, placementEntryId: item.id }
|
|
49
|
+
: receipt;
|
|
50
|
+
}
|
|
51
|
+
receiptCheckpoint(receipt, query, entryId, data) {
|
|
52
|
+
if ("entry" in data) {
|
|
53
|
+
if (receipt || data.entry.message.id !== query.messageId) {
|
|
54
|
+
return receipt;
|
|
55
|
+
}
|
|
56
|
+
const { from, message, stage, receivedAt } = data.entry;
|
|
57
|
+
return {
|
|
58
|
+
version: 1,
|
|
59
|
+
rootSessionId: query.rootSessionId,
|
|
60
|
+
messageId: query.messageId,
|
|
61
|
+
sender: {
|
|
62
|
+
peerId: from.id,
|
|
63
|
+
rootSessionId: from.rootSessionId,
|
|
64
|
+
incarnation: from.incarnation,
|
|
65
|
+
},
|
|
66
|
+
textSha256: createHash("sha256").update(message.content.text, "utf8").digest("hex"),
|
|
67
|
+
delivery: requestedDelivery(message),
|
|
68
|
+
...publicationMetadata(message),
|
|
69
|
+
checkpointEntryId: entryId,
|
|
70
|
+
receivedAt,
|
|
71
|
+
queued: stage === "queued",
|
|
72
|
+
nativeHandoff: stage === "native",
|
|
73
|
+
placementEntryId: null,
|
|
74
|
+
discarded: false,
|
|
75
|
+
};
|
|
76
|
+
}
|
|
77
|
+
if (!receipt || data.messageId !== query.messageId) {
|
|
78
|
+
return receipt;
|
|
79
|
+
}
|
|
80
|
+
return {
|
|
81
|
+
...receipt,
|
|
82
|
+
queued: receipt.queued || data.stage === "queued",
|
|
83
|
+
nativeHandoff: receipt.nativeHandoff || data.stage === "native",
|
|
84
|
+
discarded: receipt.discarded || data.stage === "discarded",
|
|
85
|
+
};
|
|
17
86
|
}
|
|
18
87
|
peerLeft(id) {
|
|
19
88
|
for (const pending of this.replies.listPending()) {
|
|
@@ -24,7 +93,7 @@ export class InboundJournal {
|
|
|
24
93
|
this.replies.expireSender(id);
|
|
25
94
|
for (const pending of this.queued()) {
|
|
26
95
|
if (pending.from.id === id && pending.message.expectsReply === true) {
|
|
27
|
-
this.
|
|
96
|
+
this.consume(pending.message.id);
|
|
28
97
|
this.checkpoint({ messageId: pending.message.id, stage: "discarded" });
|
|
29
98
|
}
|
|
30
99
|
}
|
|
@@ -42,7 +111,10 @@ export class InboundJournal {
|
|
|
42
111
|
return this.pending.get(id);
|
|
43
112
|
}
|
|
44
113
|
owns(entry) {
|
|
45
|
-
|
|
114
|
+
const id = entry.message.id;
|
|
115
|
+
return "stage" in entry
|
|
116
|
+
? this.pending.get(id) === entry
|
|
117
|
+
: !this.pending.has(id) && !this.consumed.has(id);
|
|
46
118
|
}
|
|
47
119
|
checkpoint(update) {
|
|
48
120
|
const sessionId = this.lifecycle.currentSessionId;
|
|
@@ -77,7 +149,7 @@ export class InboundJournal {
|
|
|
77
149
|
if (pending.stage === "queued" &&
|
|
78
150
|
pending.from.id === entry.from.id &&
|
|
79
151
|
pending.message.threadId === entry.message.threadId) {
|
|
80
|
-
this.
|
|
152
|
+
this.consume(pending.message.id);
|
|
81
153
|
this.replies.markReplied(pending.message.id);
|
|
82
154
|
}
|
|
83
155
|
}
|
|
@@ -135,7 +207,7 @@ export class InboundJournal {
|
|
|
135
207
|
return;
|
|
136
208
|
}
|
|
137
209
|
if (data.stage === "discarded") {
|
|
138
|
-
this.
|
|
210
|
+
this.consume(data.messageId);
|
|
139
211
|
this.replies.markReplied(data.messageId);
|
|
140
212
|
return;
|
|
141
213
|
}
|
|
@@ -163,6 +235,11 @@ export class InboundJournal {
|
|
|
163
235
|
if (item.customType === "intercom_sent") {
|
|
164
236
|
this.restoreSent(item.data);
|
|
165
237
|
}
|
|
238
|
+
if (item.customType === "intercom_received" &&
|
|
239
|
+
isRecord(item.data) &&
|
|
240
|
+
typeof item.data.messageId === "string") {
|
|
241
|
+
this.consume(item.data.messageId);
|
|
242
|
+
}
|
|
166
243
|
}
|
|
167
244
|
restoreSent(data) {
|
|
168
245
|
if (isRecord(data) &&
|
|
@@ -194,7 +271,7 @@ export class InboundJournal {
|
|
|
194
271
|
return ((entry.type === "custom_message" &&
|
|
195
272
|
["intercom_message", "subagent-human-message"].includes(entry.customType)) ||
|
|
196
273
|
(entry.type === "custom" &&
|
|
197
|
-
[INBOUND_CHECKPOINT_TYPE, "intercom_sent"].includes(entry.customType)));
|
|
274
|
+
[INBOUND_CHECKPOINT_TYPE, "intercom_sent", "intercom_received"].includes(entry.customType)));
|
|
198
275
|
}
|
|
199
276
|
recentEntries(ctx, fullScan) {
|
|
200
277
|
if (fullScan) {
|
|
@@ -1,10 +1,25 @@
|
|
|
1
1
|
import { isMessage, isSessionRegistration } from "./types.js";
|
|
2
2
|
import { finiteNumber, isRecord } from "./validation.js";
|
|
3
|
+
export function publicationMetadata(message) {
|
|
4
|
+
return {
|
|
5
|
+
guard: message.guard && {
|
|
6
|
+
rootSessionId: message.guard.rootSessionId,
|
|
7
|
+
peerId: message.guard.peerId,
|
|
8
|
+
incarnation: message.guard.incarnation,
|
|
9
|
+
applicationGuard: message.guard.applicationGuard,
|
|
10
|
+
},
|
|
11
|
+
replyHint: message.replyHint && {
|
|
12
|
+
tool: message.replyHint.tool,
|
|
13
|
+
commandId: message.replyHint.commandId,
|
|
14
|
+
commandFingerprint: message.replyHint.commandFingerprint,
|
|
15
|
+
},
|
|
16
|
+
};
|
|
17
|
+
}
|
|
3
18
|
export function requestedDelivery(message) {
|
|
4
19
|
if (message.passive === true || message.delivery === "passive") {
|
|
5
20
|
return "passive";
|
|
6
21
|
}
|
|
7
|
-
return message.delivery ??
|
|
22
|
+
return message.delivery ?? "steer";
|
|
8
23
|
}
|
|
9
24
|
function isLocalJournalMessage(value) {
|
|
10
25
|
if (!isRecord(value) || !isRecord(value.content)) {
|
|
@@ -25,6 +40,8 @@ function isLocalJournalMessage(value) {
|
|
|
25
40
|
"queueMode",
|
|
26
41
|
"threadId",
|
|
27
42
|
"passive",
|
|
43
|
+
"guard",
|
|
44
|
+
"replyHint",
|
|
28
45
|
].every((key) => value[key] === undefined));
|
|
29
46
|
}
|
|
30
47
|
function isJournalMessage(value, sender) {
|
|
@@ -45,63 +45,30 @@ export class IntercomInbound {
|
|
|
45
45
|
}
|
|
46
46
|
return true;
|
|
47
47
|
}
|
|
48
|
-
async busyNonInteractive(entry, ctx, generation) {
|
|
49
|
-
if (entry.message.expectsReply !== true) {
|
|
50
|
-
this.owners.delivery.send(entry, "steer", generation);
|
|
51
|
-
return;
|
|
52
|
-
}
|
|
53
|
-
const active = this.owners.connection.active;
|
|
54
|
-
if (entry.message.replyTo !== undefined || active?.isConnected() !== true) {
|
|
55
|
-
return;
|
|
56
|
-
}
|
|
57
|
-
try {
|
|
58
|
-
const result = await active.send(entry.from.id, {
|
|
59
|
-
text: "This agent is running in non-interactive mode and cannot respond to intercom messages while it is working. It will continue its current task and exit when done.",
|
|
60
|
-
replyTo: entry.message.id,
|
|
61
|
-
});
|
|
62
|
-
if (result.delivered && this.lifecycle.live(ctx, generation)) {
|
|
63
|
-
this.owners.replies.markReplied(entry.message.id);
|
|
64
|
-
this.lifecycle.markActivity();
|
|
65
|
-
this.owners.connection.syncStatus();
|
|
66
|
-
}
|
|
67
|
-
}
|
|
68
|
-
catch {
|
|
69
|
-
/* Best-effort busy reply; the non-interactive task must continue either way. */
|
|
70
|
-
}
|
|
71
|
-
}
|
|
72
48
|
async admit(entry, live, generation) {
|
|
73
49
|
const active = this.lifecycle.live(live, generation);
|
|
74
50
|
if (!active || this.queueAdmission(entry, active)) {
|
|
75
51
|
return;
|
|
76
52
|
}
|
|
77
53
|
if (!this.lifecycle.isIdle(active)) {
|
|
78
|
-
|
|
54
|
+
this.busy(entry, generation);
|
|
79
55
|
return;
|
|
80
56
|
}
|
|
81
57
|
if (this.lifecycle.live(live, generation)) {
|
|
82
58
|
this.owners.delivery.send(entry, requestedDelivery(entry.message) === "passive" ? "passive" : "trigger", generation);
|
|
83
59
|
}
|
|
84
60
|
}
|
|
85
|
-
|
|
61
|
+
busy(entry, generation) {
|
|
86
62
|
const delivery = requestedDelivery(entry.message);
|
|
87
63
|
if (delivery === "steer") {
|
|
88
64
|
this.owners.delivery.send(entry, "steer", generation);
|
|
89
|
-
await this.owners.delivery.detach(entry, entry.message.expectsReply !== true);
|
|
90
65
|
return;
|
|
91
66
|
}
|
|
92
|
-
if (delivery === "queue"
|
|
67
|
+
if (delivery === "queue") {
|
|
93
68
|
this.owners.delivery.send(entry, "followUp", generation);
|
|
94
69
|
return;
|
|
95
70
|
}
|
|
96
|
-
|
|
97
|
-
this.owners.delivery.queue(entry, "passive");
|
|
98
|
-
return;
|
|
99
|
-
}
|
|
100
|
-
if (!ctx.hasUI) {
|
|
101
|
-
await this.busyNonInteractive(entry, ctx, generation);
|
|
102
|
-
return;
|
|
103
|
-
}
|
|
104
|
-
this.owners.delivery.queue(entry, "auto");
|
|
71
|
+
this.owners.delivery.queue(entry, "passive");
|
|
105
72
|
}
|
|
106
73
|
async receive(ctx, from, input) {
|
|
107
74
|
const generation = this.lifecycle.generation;
|
|
@@ -113,9 +80,17 @@ export class IntercomInbound {
|
|
|
113
80
|
if (!live) {
|
|
114
81
|
return;
|
|
115
82
|
}
|
|
83
|
+
if (input.guard && !this.lifecycle.accepts(input.guard)) {
|
|
84
|
+
return;
|
|
85
|
+
}
|
|
116
86
|
// Peer metadata never grants user authority.
|
|
117
87
|
const message = input.human ? { ...input, human: undefined } : input;
|
|
88
|
+
const entry = this.entry(from, message);
|
|
89
|
+
if (!this.owners.journal.owns(entry)) {
|
|
90
|
+
return;
|
|
91
|
+
}
|
|
118
92
|
if (this.owners.wait.receive(from, message, () => {
|
|
93
|
+
this.owners.journal.consume(message.id);
|
|
119
94
|
this.lifecycle.markActivity();
|
|
120
95
|
this.owners.connection.syncStatus();
|
|
121
96
|
})) {
|
|
@@ -124,7 +99,6 @@ export class IntercomInbound {
|
|
|
124
99
|
this.owners.replies.recordIncomingMessage(from, message);
|
|
125
100
|
this.lifecycle.markActivity();
|
|
126
101
|
this.owners.connection.syncStatus();
|
|
127
|
-
const entry = this.entry(from, message);
|
|
128
102
|
await this.admit(entry, live, generation);
|
|
129
103
|
}
|
|
130
104
|
}
|
|
@@ -21,6 +21,7 @@ export default function piIntercomExtension(pi) {
|
|
|
21
21
|
const connection = new IntercomConnection(lifecycle, config, {
|
|
22
22
|
restoring: () => delivery.restoring,
|
|
23
23
|
pendingAsks: () => replies.listPending().length,
|
|
24
|
+
receipt: (query) => journal.receipt(query),
|
|
24
25
|
incoming: (ctx, from, message) => inbound.receive(ctx, from, message),
|
|
25
26
|
peerLeft: (id) => {
|
|
26
27
|
wait.peerDisconnected(id);
|
|
@@ -34,8 +35,10 @@ export default function piIntercomExtension(pi) {
|
|
|
34
35
|
replies,
|
|
35
36
|
syncStatus: () => connection.syncStatus(),
|
|
36
37
|
});
|
|
38
|
+
lifecycle.registerContextEvents(() => connection.syncStatus());
|
|
37
39
|
const inbound = new IntercomInbound(lifecycle, config, {
|
|
38
40
|
delivery,
|
|
41
|
+
journal,
|
|
39
42
|
replies,
|
|
40
43
|
wait,
|
|
41
44
|
connection,
|
|
@@ -1,17 +1,6 @@
|
|
|
1
|
-
|
|
2
|
-
return (Number.isSafeInteger(offset) &&
|
|
3
|
-
offset >= 0 &&
|
|
4
|
-
Number.isSafeInteger(limit) &&
|
|
5
|
-
limit >= 1 &&
|
|
6
|
-
limit <= 16384);
|
|
7
|
-
}
|
|
1
|
+
import { isLastMessageQuery, LAST_MESSAGE_PAGE_LIMIT, } from "./types.js";
|
|
8
2
|
function completedResponse(manager, entryId) {
|
|
9
|
-
|
|
10
|
-
while (id !== null) {
|
|
11
|
-
const entry = manager.getEntry(id);
|
|
12
|
-
if (!entry) {
|
|
13
|
-
throw new Error("Native session history is unavailable.");
|
|
14
|
-
}
|
|
3
|
+
for (const entry of manager.getBranch().reverse()) {
|
|
15
4
|
if (entry.type === "message" &&
|
|
16
5
|
entry.message.role === "assistant" &&
|
|
17
6
|
(entry.message.stopReason === "stop" || entry.message.stopReason === "length") &&
|
|
@@ -24,15 +13,14 @@ function completedResponse(manager, entryId) {
|
|
|
24
13
|
.join("\n"),
|
|
25
14
|
};
|
|
26
15
|
}
|
|
27
|
-
id = entry.parentId;
|
|
28
16
|
}
|
|
29
17
|
return;
|
|
30
18
|
}
|
|
31
19
|
/** Reads finalized native history only; no model invocation or synthesized response. */
|
|
32
20
|
export function readLastMessage(manager, session, query) {
|
|
33
21
|
const offset = query.offset ?? 0;
|
|
34
|
-
const limit = query.limit ??
|
|
35
|
-
if (!
|
|
22
|
+
const limit = query.limit ?? LAST_MESSAGE_PAGE_LIMIT;
|
|
23
|
+
if (!isLastMessageQuery({ offset, limit })) {
|
|
36
24
|
return {
|
|
37
25
|
error: "Invalid last response page: offset must be non-negative and limit must be 1–16384.",
|
|
38
26
|
};
|
|
@@ -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
|
-
|
|
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,
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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: ${
|
|
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
|
|
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
|
|
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
|
|
51
|
+
throw new RecoveryError("ambiguous_reply_target", `Multiple pending asks from "${to}" — use one of: ${pendingSenderOptions(matches, contexts)}.`);
|
|
51
52
|
}
|
|
52
|
-
throw new
|
|
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
|
|
155
|
+
throw new RecoveryError("no_pending_reply", "No active intercom context to reply to");
|
|
155
156
|
}
|
|
156
|
-
throw new
|
|
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
|
|
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: "
|
|
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
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
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
|