@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.
- package/CHANGELOG.md +63 -2
- package/README.md +36 -5
- package/dist/agent/child-runtime.js +80 -0
- package/dist/agent/contracts.js +15 -24
- package/dist/agent/host.js +3 -16
- package/dist/agent/index.js +212 -192
- 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 +350 -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/parking.js +253 -0
- package/dist/agent/process.js +228 -28
- package/dist/agent/receipts.js +92 -20
- package/dist/agent/runner.js +229 -126
- package/dist/agent/shell.js +2 -1
- package/dist/agent/storage.js +47 -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 +62 -43
- package/dist/pi-intercom/broker/client-last.d.ts +24 -0
- package/dist/pi-intercom/broker/client-last.js +42 -5
- package/dist/pi-intercom/broker/client-park.d.ts +9 -0
- package/dist/pi-intercom/broker/client-park.js +21 -0
- package/dist/pi-intercom/broker/client.d.ts +59 -0
- package/dist/pi-intercom/broker/client.js +55 -29
- 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 +30 -0
- package/dist/pi-intercom/broker/last.js +87 -40
- package/dist/pi-intercom/broker/parked.js +107 -0
- 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 +20 -1
- 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 +71 -20
- 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 +22 -37
- package/dist/pi-intercom/index.js +41 -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/parking.js +12 -0
- 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 +5 -3
- package/docs/development.md +42 -3
- package/docs/intercom-bridge.md +9 -4
- package/docs/intercom.md +228 -7
- package/docs/performance.md +118 -14
- package/docs/reference.md +168 -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/child-runtime.ts +105 -0
- package/src/agent/contracts.ts +18 -34
- package/src/agent/host.ts +39 -20
- package/src/agent/index.ts +234 -207
- 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 +409 -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/parking.ts +341 -0
- package/src/agent/process.ts +280 -37
- package/src/agent/receipts.ts +114 -24
- package/src/agent/runner.ts +227 -149
- package/src/agent/shell.ts +2 -1
- package/src/agent/storage.ts +44 -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 +64 -53
- package/src/pi-intercom/broker/client-last.ts +65 -12
- package/src/pi-intercom/broker/client-park.ts +23 -0
- package/src/pi-intercom/broker/client.ts +63 -45
- 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 +131 -43
- package/src/pi-intercom/broker/parked.ts +130 -0
- 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 +27 -2
- package/src/pi-intercom/connection.ts +103 -39
- 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 +32 -49
- package/src/pi-intercom/index.ts +44 -0
- package/src/pi-intercom/last-message.ts +11 -20
- 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/parking.ts +22 -0
- 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
|
@@ -2,6 +2,7 @@ import type { ExtensionContext } from "@earendil-works/pi-coding-agent";
|
|
|
2
2
|
import type { IntercomConfig } from "./config.ts";
|
|
3
3
|
import type { IntercomLifecycle } from "./lifecycle.ts";
|
|
4
4
|
import type { InboundDeliveryOwner } from "./inbound-delivery.ts";
|
|
5
|
+
import type { InboundJournal } from "./inbound-journal.ts";
|
|
5
6
|
import type { ReplyTracker } from "./reply-tracker.ts";
|
|
6
7
|
import type { ReplyWait } from "./reply-wait.ts";
|
|
7
8
|
import type { IntercomConnection } from "./connection.ts";
|
|
@@ -11,17 +12,18 @@ import { requestedDelivery } from "./inbound-record.ts";
|
|
|
11
12
|
import { formatAttachments } from "./message-format.ts";
|
|
12
13
|
|
|
13
14
|
type Lifecycle = Readonly<
|
|
14
|
-
Pick<IntercomLifecycle, "generation" | "isIdle" | "live" | "markActivity">
|
|
15
|
+
Pick<IntercomLifecycle, "accepts" | "generation" | "isIdle" | "live" | "markActivity">
|
|
15
16
|
>;
|
|
16
17
|
type InboundDeliveryHandle = Readonly<
|
|
17
|
-
Pick<InboundDeliveryOwner, "batching" | "
|
|
18
|
+
Pick<InboundDeliveryOwner, "batching" | "queue" | "restoring" | "send">
|
|
18
19
|
>;
|
|
19
20
|
type ReplyWaitHandle = Readonly<Pick<ReplyWait, "receive">>;
|
|
20
|
-
type Connection = Readonly<Pick<IntercomConnection, "
|
|
21
|
-
type Replies = Readonly<Pick<ReplyTracker, "
|
|
21
|
+
type Connection = Readonly<Pick<IntercomConnection, "syncStatus">>;
|
|
22
|
+
type Replies = Readonly<Pick<ReplyTracker, "recordIncomingMessage">>;
|
|
22
23
|
|
|
23
24
|
interface InboundOwners {
|
|
24
25
|
readonly delivery: InboundDeliveryHandle;
|
|
26
|
+
readonly journal: Readonly<Pick<InboundJournal, "consume" | "owns">>;
|
|
25
27
|
readonly replies: Replies;
|
|
26
28
|
readonly wait: ReplyWaitHandle;
|
|
27
29
|
readonly connection: Connection;
|
|
@@ -71,33 +73,6 @@ export class IntercomInbound {
|
|
|
71
73
|
}
|
|
72
74
|
return true;
|
|
73
75
|
}
|
|
74
|
-
private async busyNonInteractive(
|
|
75
|
-
entry: InboundMessageEntry,
|
|
76
|
-
ctx: ExtensionContext,
|
|
77
|
-
generation: number,
|
|
78
|
-
): Promise<void> {
|
|
79
|
-
if (entry.message.expectsReply !== true) {
|
|
80
|
-
this.owners.delivery.send(entry, "steer", generation);
|
|
81
|
-
return;
|
|
82
|
-
}
|
|
83
|
-
const active = this.owners.connection.active;
|
|
84
|
-
if (entry.message.replyTo !== undefined || active?.isConnected() !== true) {
|
|
85
|
-
return;
|
|
86
|
-
}
|
|
87
|
-
try {
|
|
88
|
-
const result = await active.send(entry.from.id, {
|
|
89
|
-
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.",
|
|
90
|
-
replyTo: entry.message.id,
|
|
91
|
-
});
|
|
92
|
-
if (result.delivered && this.lifecycle.live(ctx, generation)) {
|
|
93
|
-
this.owners.replies.markReplied(entry.message.id);
|
|
94
|
-
this.lifecycle.markActivity();
|
|
95
|
-
this.owners.connection.syncStatus();
|
|
96
|
-
}
|
|
97
|
-
} catch {
|
|
98
|
-
/* Best-effort busy reply; the non-interactive task must continue either way. */
|
|
99
|
-
}
|
|
100
|
-
}
|
|
101
76
|
private async admit(
|
|
102
77
|
entry: InboundMessageEntry,
|
|
103
78
|
live: ExtensionContext,
|
|
@@ -108,7 +83,7 @@ export class IntercomInbound {
|
|
|
108
83
|
return;
|
|
109
84
|
}
|
|
110
85
|
if (!this.lifecycle.isIdle(active)) {
|
|
111
|
-
|
|
86
|
+
this.busy(entry, generation);
|
|
112
87
|
return;
|
|
113
88
|
}
|
|
114
89
|
if (this.lifecycle.live(live, generation)) {
|
|
@@ -119,32 +94,33 @@ export class IntercomInbound {
|
|
|
119
94
|
);
|
|
120
95
|
}
|
|
121
96
|
}
|
|
122
|
-
private
|
|
123
|
-
entry: InboundMessageEntry,
|
|
124
|
-
ctx: ExtensionContext,
|
|
125
|
-
generation: number,
|
|
126
|
-
): Promise<void> {
|
|
97
|
+
private busy(entry: InboundMessageEntry, generation: number): void {
|
|
127
98
|
const delivery = requestedDelivery(entry.message);
|
|
128
99
|
if (delivery === "steer") {
|
|
129
100
|
this.owners.delivery.send(entry, "steer", generation);
|
|
130
|
-
await this.owners.delivery.detach(entry, entry.message.expectsReply !== true);
|
|
131
101
|
return;
|
|
132
102
|
}
|
|
133
|
-
if (delivery === "queue"
|
|
103
|
+
if (delivery === "queue") {
|
|
134
104
|
this.owners.delivery.send(entry, "followUp", generation);
|
|
135
105
|
return;
|
|
136
106
|
}
|
|
137
|
-
|
|
138
|
-
this.owners.delivery.queue(entry, "passive");
|
|
139
|
-
return;
|
|
140
|
-
}
|
|
141
|
-
if (!ctx.hasUI) {
|
|
142
|
-
await this.busyNonInteractive(entry, ctx, generation);
|
|
143
|
-
return;
|
|
144
|
-
}
|
|
145
|
-
this.owners.delivery.queue(entry, "auto");
|
|
107
|
+
this.owners.delivery.queue(entry, "passive");
|
|
146
108
|
}
|
|
109
|
+
/** Counted synchronously so a parking fence observes frames received before its ack. */
|
|
110
|
+
receiving = 0;
|
|
147
111
|
async receive(ctx: ExtensionContext, from: SessionInfo, input: Message): Promise<void> {
|
|
112
|
+
this.receiving++;
|
|
113
|
+
try {
|
|
114
|
+
await this.receiveCounted(ctx, from, input);
|
|
115
|
+
} finally {
|
|
116
|
+
this.receiving--;
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
private async receiveCounted(
|
|
120
|
+
ctx: ExtensionContext,
|
|
121
|
+
from: SessionInfo,
|
|
122
|
+
input: Message,
|
|
123
|
+
): Promise<void> {
|
|
148
124
|
const generation = this.lifecycle.generation;
|
|
149
125
|
const restore = this.owners.delivery.restoring;
|
|
150
126
|
if (restore) {
|
|
@@ -154,10 +130,18 @@ export class IntercomInbound {
|
|
|
154
130
|
if (!live) {
|
|
155
131
|
return;
|
|
156
132
|
}
|
|
133
|
+
if (input.guard && !this.lifecycle.accepts(input.guard)) {
|
|
134
|
+
return;
|
|
135
|
+
}
|
|
157
136
|
// Peer metadata never grants user authority.
|
|
158
137
|
const message = input.human ? { ...input, human: undefined } : input;
|
|
138
|
+
const entry = this.entry(from, message);
|
|
139
|
+
if (!this.owners.journal.owns(entry)) {
|
|
140
|
+
return;
|
|
141
|
+
}
|
|
159
142
|
if (
|
|
160
143
|
this.owners.wait.receive(from, message, () => {
|
|
144
|
+
this.owners.journal.consume(message.id);
|
|
161
145
|
this.lifecycle.markActivity();
|
|
162
146
|
this.owners.connection.syncStatus();
|
|
163
147
|
})
|
|
@@ -167,7 +151,6 @@ export class IntercomInbound {
|
|
|
167
151
|
this.owners.replies.recordIncomingMessage(from, message);
|
|
168
152
|
this.lifecycle.markActivity();
|
|
169
153
|
this.owners.connection.syncStatus();
|
|
170
|
-
const entry = this.entry(from, message);
|
|
171
154
|
await this.admit(entry, live, generation);
|
|
172
155
|
}
|
|
173
156
|
}
|
package/src/pi-intercom/index.ts
CHANGED
|
@@ -13,6 +13,20 @@ import { IntercomInspection } from "./inspection.ts";
|
|
|
13
13
|
import { IntercomTools } from "./tools.ts";
|
|
14
14
|
import { IntercomSessionEvents } from "./session-events.ts";
|
|
15
15
|
import { registerIntercomTools } from "./tool-registration.ts";
|
|
16
|
+
import { isParkRequest, PARK, PARKED_HISTORY, WAKE } from "../shared/parking.ts";
|
|
17
|
+
import type { NativeHistory } from "./last-message.ts";
|
|
18
|
+
|
|
19
|
+
/** The agent runtime answers for its exited parked child from the captured native session. */
|
|
20
|
+
function parkedHistory(pi: ExtensionAPI, peer: string): Promise<NativeHistory | undefined> {
|
|
21
|
+
let history: Promise<NativeHistory | undefined> | undefined;
|
|
22
|
+
pi.events.emit(PARKED_HISTORY, {
|
|
23
|
+
peer,
|
|
24
|
+
reply: (found: Promise<NativeHistory | undefined>) => {
|
|
25
|
+
history = found;
|
|
26
|
+
},
|
|
27
|
+
});
|
|
28
|
+
return history ?? Promise.reject(new Error("No agent runtime owns parked history."));
|
|
29
|
+
}
|
|
16
30
|
|
|
17
31
|
export default function piIntercomExtension(pi: ExtensionAPI): void {
|
|
18
32
|
const config = loadConfig();
|
|
@@ -23,6 +37,7 @@ export default function piIntercomExtension(pi: ExtensionAPI): void {
|
|
|
23
37
|
const connection = new IntercomConnection(lifecycle, config, {
|
|
24
38
|
restoring: () => delivery.restoring,
|
|
25
39
|
pendingAsks: () => replies.listPending().length,
|
|
40
|
+
receipt: (query) => journal.receipt(query),
|
|
26
41
|
incoming: (ctx, from, message) => inbound.receive(ctx, from, message),
|
|
27
42
|
peerLeft: (id) => {
|
|
28
43
|
wait.peerDisconnected(id);
|
|
@@ -33,13 +48,19 @@ export default function piIntercomExtension(pi: ExtensionAPI): void {
|
|
|
33
48
|
new Error(`Disconnected while waiting for reply: ${error.message}`, { cause: error }),
|
|
34
49
|
);
|
|
35
50
|
},
|
|
51
|
+
parkedHistory: (peer) => parkedHistory(pi, peer.id),
|
|
52
|
+
wake: (peer) => {
|
|
53
|
+
pi.events.emit(WAKE, peer);
|
|
54
|
+
},
|
|
36
55
|
});
|
|
37
56
|
const delivery = new InboundDeliveryOwner(pi, lifecycle, journal, {
|
|
38
57
|
replies,
|
|
39
58
|
syncStatus: () => connection.syncStatus(),
|
|
40
59
|
});
|
|
60
|
+
lifecycle.registerContextEvents(() => connection.syncStatus());
|
|
41
61
|
const inbound = new IntercomInbound(lifecycle, config, {
|
|
42
62
|
delivery,
|
|
63
|
+
journal,
|
|
43
64
|
replies,
|
|
44
65
|
wait,
|
|
45
66
|
connection,
|
|
@@ -56,4 +77,27 @@ export default function piIntercomExtension(pi: ExtensionAPI): void {
|
|
|
56
77
|
wait,
|
|
57
78
|
}).register();
|
|
58
79
|
registerIntercomTools(pi, tools);
|
|
80
|
+
pi.events.on(PARK, (request) => {
|
|
81
|
+
if (isParkRequest(request)) {
|
|
82
|
+
request.reply(request.park ? connection.park() : connection.unpark());
|
|
83
|
+
}
|
|
84
|
+
});
|
|
85
|
+
// Native fork sleep readiness; it vetoes only this package's own parking, never other saves.
|
|
86
|
+
Reflect.apply(pi.on.bind(pi), undefined, [
|
|
87
|
+
"working_session_save",
|
|
88
|
+
() =>
|
|
89
|
+
!connection.parking
|
|
90
|
+
? undefined
|
|
91
|
+
: {
|
|
92
|
+
blockers: [
|
|
93
|
+
inbound.receiving > 0 && "intercom is receiving a message",
|
|
94
|
+
journal.size > 0 && "intercom inbound messages await delivery",
|
|
95
|
+
replies.listPending().length > 0 && "intercom asks await replies",
|
|
96
|
+
wait.waiting && "intercom is waiting for a reply",
|
|
97
|
+
(delivery.restoring !== undefined || delivery.batching) &&
|
|
98
|
+
"intercom delivery is restoring",
|
|
99
|
+
connection.reconnected && "intercom reconnected while parking",
|
|
100
|
+
].filter((blocker) => typeof blocker === "string"),
|
|
101
|
+
},
|
|
102
|
+
]);
|
|
59
103
|
}
|
|
@@ -1,26 +1,18 @@
|
|
|
1
1
|
import type { ExtensionContext } from "@earendil-works/pi-coding-agent";
|
|
2
|
-
import
|
|
2
|
+
import {
|
|
3
|
+
isLastMessageQuery,
|
|
4
|
+
LAST_MESSAGE_PAGE_LIMIT,
|
|
5
|
+
type LastMessageQuery,
|
|
6
|
+
type LastMessageResult,
|
|
7
|
+
type SessionInfo,
|
|
8
|
+
} from "./types.ts";
|
|
3
9
|
|
|
4
|
-
type NativeHistory = ExtensionContext["sessionManager"]
|
|
5
|
-
function validPage(offset: number, limit: number): boolean {
|
|
6
|
-
return (
|
|
7
|
-
Number.isSafeInteger(offset) &&
|
|
8
|
-
offset >= 0 &&
|
|
9
|
-
Number.isSafeInteger(limit) &&
|
|
10
|
-
limit >= 1 &&
|
|
11
|
-
limit <= 16384
|
|
12
|
-
);
|
|
13
|
-
}
|
|
10
|
+
export type NativeHistory = Pick<ExtensionContext["sessionManager"], "getBranch">;
|
|
14
11
|
function completedResponse(
|
|
15
12
|
manager: NativeHistory,
|
|
16
13
|
entryId?: string,
|
|
17
14
|
): { readonly id: string; readonly text: string } | undefined {
|
|
18
|
-
|
|
19
|
-
while (id !== null) {
|
|
20
|
-
const entry = manager.getEntry(id);
|
|
21
|
-
if (!entry) {
|
|
22
|
-
throw new Error("Native session history is unavailable.");
|
|
23
|
-
}
|
|
15
|
+
for (const entry of manager.getBranch().reverse()) {
|
|
24
16
|
if (
|
|
25
17
|
entry.type === "message" &&
|
|
26
18
|
entry.message.role === "assistant" &&
|
|
@@ -35,7 +27,6 @@ function completedResponse(
|
|
|
35
27
|
.join("\n"),
|
|
36
28
|
};
|
|
37
29
|
}
|
|
38
|
-
id = entry.parentId;
|
|
39
30
|
}
|
|
40
31
|
return;
|
|
41
32
|
}
|
|
@@ -46,8 +37,8 @@ export function readLastMessage(
|
|
|
46
37
|
query: LastMessageQuery,
|
|
47
38
|
): LastMessageResult {
|
|
48
39
|
const offset = query.offset ?? 0;
|
|
49
|
-
const limit = query.limit ??
|
|
50
|
-
if (!
|
|
40
|
+
const limit = query.limit ?? LAST_MESSAGE_PAGE_LIMIT;
|
|
41
|
+
if (!isLastMessageQuery({ offset, limit })) {
|
|
51
42
|
return {
|
|
52
43
|
error: "Invalid last response page: offset must be non-negative and limit must be 1–16384.",
|
|
53
44
|
};
|
|
@@ -1,8 +1,28 @@
|
|
|
1
1
|
import type { ExtensionAPI, ExtensionContext } from "@earendil-works/pi-coding-agent";
|
|
2
|
+
import { randomUUID } from "node:crypto";
|
|
2
3
|
import type { IntercomConfig } from "./config.ts";
|
|
3
4
|
import { buildPresenceIdentity, type IntercomLineage } from "./identity.ts";
|
|
4
5
|
import { resolveSessionProjectId } from "./session-targets.ts";
|
|
5
|
-
import
|
|
6
|
+
import {
|
|
7
|
+
isContextTarget,
|
|
8
|
+
type ContextTarget,
|
|
9
|
+
type DeliveryGuard,
|
|
10
|
+
type SessionInfo,
|
|
11
|
+
} from "./types.ts";
|
|
12
|
+
import { isRecord, safeLabel } from "./validation.ts";
|
|
13
|
+
|
|
14
|
+
export function isContextRequest(value: unknown): value is Readonly<Record<string, unknown>> & {
|
|
15
|
+
readonly respond: (response: unknown) => void;
|
|
16
|
+
} {
|
|
17
|
+
return isRecord(value) && typeof value.respond === "function";
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
type ContextSnapshot = Readonly<Record<keyof ContextTarget, string | null>> &
|
|
21
|
+
Pick<SessionInfo, "parentSessionId" | "applicationGuard"> & {
|
|
22
|
+
readonly version: 1;
|
|
23
|
+
readonly connected: boolean;
|
|
24
|
+
readonly guardedDelivery: 1 | null;
|
|
25
|
+
};
|
|
6
26
|
|
|
7
27
|
/** Owns native context validity and occupancy, never journal or socket state. */
|
|
8
28
|
export class IntercomLifecycle {
|
|
@@ -11,6 +31,10 @@ export class IntercomLifecycle {
|
|
|
11
31
|
private model = "unknown";
|
|
12
32
|
private disposed = true;
|
|
13
33
|
private epoch = 0;
|
|
34
|
+
private incarnation: string | null = null;
|
|
35
|
+
private peerId: string | null = null;
|
|
36
|
+
private guardedDelivery = false;
|
|
37
|
+
private applicationGuard: string | undefined;
|
|
14
38
|
private running = false;
|
|
15
39
|
private activity = 0;
|
|
16
40
|
private readonly tools = new Map<string, string>();
|
|
@@ -31,15 +55,97 @@ export class IntercomLifecycle {
|
|
|
31
55
|
get toolCount(): number {
|
|
32
56
|
return this.tools.size;
|
|
33
57
|
}
|
|
58
|
+
registerContextEvents(sync: () => void): void {
|
|
59
|
+
this.pi.events.on("pi-intercom:context-query", (request) => this.contextRequest(request));
|
|
60
|
+
this.pi.events.on("pi-intercom:application-guard", (request) =>
|
|
61
|
+
this.contextRequest(request, sync),
|
|
62
|
+
);
|
|
63
|
+
}
|
|
64
|
+
private contextRequest(request: unknown, sync?: () => void): void {
|
|
65
|
+
if (!isContextRequest(request)) {
|
|
66
|
+
return;
|
|
67
|
+
}
|
|
68
|
+
if (request.version !== 1) {
|
|
69
|
+
request.respond({ error: "unsupported_version" });
|
|
70
|
+
return;
|
|
71
|
+
}
|
|
72
|
+
if (sync) {
|
|
73
|
+
if (
|
|
74
|
+
!isContextTarget(request.target) ||
|
|
75
|
+
!this.matches(request.target) ||
|
|
76
|
+
!(
|
|
77
|
+
request.revision === null ||
|
|
78
|
+
(typeof request.revision === "string" && safeLabel(request.revision))
|
|
79
|
+
)
|
|
80
|
+
) {
|
|
81
|
+
request.respond({ error: "context_changed" });
|
|
82
|
+
return;
|
|
83
|
+
}
|
|
84
|
+
this.applicationGuard = request.revision === null ? undefined : request.revision;
|
|
85
|
+
sync();
|
|
86
|
+
this.publishContext();
|
|
87
|
+
}
|
|
88
|
+
request.respond(this.snapshot());
|
|
89
|
+
}
|
|
90
|
+
snapshot(): ContextSnapshot {
|
|
91
|
+
if (!this.live()) {
|
|
92
|
+
return {
|
|
93
|
+
version: 1,
|
|
94
|
+
rootSessionId: null,
|
|
95
|
+
peerId: null,
|
|
96
|
+
incarnation: null,
|
|
97
|
+
connected: false,
|
|
98
|
+
guardedDelivery: null,
|
|
99
|
+
} as const;
|
|
100
|
+
}
|
|
101
|
+
return {
|
|
102
|
+
version: 1,
|
|
103
|
+
rootSessionId: this.lineage?.rootSessionId ?? this.sessionId,
|
|
104
|
+
parentSessionId: this.lineage?.parentSessionId,
|
|
105
|
+
peerId: this.peerId,
|
|
106
|
+
incarnation: this.incarnation,
|
|
107
|
+
connected: this.peerId !== null,
|
|
108
|
+
guardedDelivery: this.guardedDelivery ? 1 : null,
|
|
109
|
+
applicationGuard: this.applicationGuard,
|
|
110
|
+
} as const;
|
|
111
|
+
}
|
|
112
|
+
private publishContext(): void {
|
|
113
|
+
this.pi.events.emit("pi-intercom:context-state", this.snapshot());
|
|
114
|
+
}
|
|
115
|
+
transport(peerId: string | null, guardedDelivery = false): void {
|
|
116
|
+
this.peerId = peerId;
|
|
117
|
+
this.guardedDelivery = guardedDelivery;
|
|
118
|
+
this.publishContext();
|
|
119
|
+
}
|
|
120
|
+
private matches(target: ContextTarget): boolean {
|
|
121
|
+
const current = this.snapshot();
|
|
122
|
+
return (
|
|
123
|
+
current.connected &&
|
|
124
|
+
current.rootSessionId === target.rootSessionId &&
|
|
125
|
+
current.peerId === target.peerId &&
|
|
126
|
+
current.incarnation === target.incarnation
|
|
127
|
+
);
|
|
128
|
+
}
|
|
129
|
+
accepts(guard: DeliveryGuard): boolean {
|
|
130
|
+
return (
|
|
131
|
+
this.guardedDelivery &&
|
|
132
|
+
this.lineage?.parentSessionId === undefined &&
|
|
133
|
+
this.matches(guard) &&
|
|
134
|
+
this.applicationGuard === guard.applicationGuard
|
|
135
|
+
);
|
|
136
|
+
}
|
|
34
137
|
start(ctx: ExtensionContext): number {
|
|
35
138
|
this.disposed = false;
|
|
36
139
|
this.epoch += 1;
|
|
37
140
|
this.context = ctx;
|
|
38
141
|
this.sessionId = ctx.sessionManager.getSessionId();
|
|
142
|
+
this.incarnation = randomUUID();
|
|
143
|
+
this.applicationGuard = undefined;
|
|
39
144
|
this.model = ctx.model?.id ?? "unknown";
|
|
40
145
|
this.running = false;
|
|
41
146
|
this.activity = 0;
|
|
42
147
|
this.tools.clear();
|
|
148
|
+
this.transport(null);
|
|
43
149
|
return this.epoch;
|
|
44
150
|
}
|
|
45
151
|
invalidate(): void {
|
|
@@ -47,6 +153,7 @@ export class IntercomLifecycle {
|
|
|
47
153
|
this.epoch += 1;
|
|
48
154
|
this.running = false;
|
|
49
155
|
this.tools.clear();
|
|
156
|
+
this.publishContext();
|
|
50
157
|
}
|
|
51
158
|
clear(): void {
|
|
52
159
|
this.context = null;
|
|
@@ -104,11 +211,7 @@ export class IntercomLifecycle {
|
|
|
104
211
|
markActivity(): void {
|
|
105
212
|
this.activity = Date.now();
|
|
106
213
|
}
|
|
107
|
-
identity(sessionId: string): {
|
|
108
|
-
readonly name: string;
|
|
109
|
-
readonly rootSessionId: string;
|
|
110
|
-
readonly parentSessionId?: string;
|
|
111
|
-
} {
|
|
214
|
+
identity(sessionId: string): Pick<SessionInfo, "name" | "rootSessionId" | "parentSessionId"> {
|
|
112
215
|
return {
|
|
113
216
|
...buildPresenceIdentity(this.pi, sessionId),
|
|
114
217
|
rootSessionId: this.lineage?.rootSessionId ?? sessionId,
|
|
@@ -125,11 +228,9 @@ export class IntercomLifecycle {
|
|
|
125
228
|
? `${status} · ${this.config.status}`
|
|
126
229
|
: status;
|
|
127
230
|
}
|
|
128
|
-
health(
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
readonly lastIntercomActivity: number;
|
|
132
|
-
} {
|
|
231
|
+
health(
|
|
232
|
+
pendingAsks: number,
|
|
233
|
+
): Pick<SessionInfo, "pendingAsks" | "acceptsAsks" | "lastIntercomActivity"> {
|
|
133
234
|
const live = this.live();
|
|
134
235
|
return {
|
|
135
236
|
pendingAsks,
|
|
@@ -142,12 +243,14 @@ export class IntercomLifecycle {
|
|
|
142
243
|
if (!live || this.sessionId === null) {
|
|
143
244
|
throw new Error("Intercom runtime not initialized");
|
|
144
245
|
}
|
|
145
|
-
const cwd = live.cwd;
|
|
146
246
|
return {
|
|
147
247
|
...this.identity(this.sessionId),
|
|
148
|
-
|
|
248
|
+
incarnation: this.incarnation ?? undefined,
|
|
249
|
+
guardedDelivery: 1,
|
|
250
|
+
applicationGuard: this.applicationGuard,
|
|
251
|
+
cwd: live.cwd,
|
|
149
252
|
model: this.model,
|
|
150
|
-
projectId: await resolveSessionProjectId(cwd),
|
|
253
|
+
projectId: await resolveSessionProjectId(live.cwd),
|
|
151
254
|
lastSeen: Date.now(),
|
|
152
255
|
status: this.status(),
|
|
153
256
|
...this.health(pendingAsks),
|
|
@@ -22,7 +22,7 @@ import { formatAttachments } from "./message-format.ts";
|
|
|
22
22
|
import {
|
|
23
23
|
AskDeliveryError,
|
|
24
24
|
failureDetails,
|
|
25
|
-
|
|
25
|
+
RecoveryError,
|
|
26
26
|
type RecoveryAction,
|
|
27
27
|
} from "./tool-results.ts";
|
|
28
28
|
import { sendOptions, toolError, toolText } from "./tool-arguments.ts";
|
|
@@ -155,11 +155,10 @@ export class IntercomOutbound {
|
|
|
155
155
|
}
|
|
156
156
|
return this.sent(to, params, result);
|
|
157
157
|
} catch (error) {
|
|
158
|
-
const text = errorMessage(error);
|
|
159
158
|
return toolError(
|
|
160
|
-
`Failed to send: ${
|
|
159
|
+
`Failed to send: ${errorMessage(error)}`,
|
|
161
160
|
failureDetails(
|
|
162
|
-
|
|
161
|
+
error instanceof RecoveryError ? error.code : "send_failed",
|
|
163
162
|
[
|
|
164
163
|
{ action: "list" },
|
|
165
164
|
{ action: "send", guidance: "Retry with an exact active recipient target." },
|
|
@@ -225,17 +224,10 @@ export class IntercomOutbound {
|
|
|
225
224
|
const result = this.deliveryFailure(to, error.result);
|
|
226
225
|
return { ...result, details: { ...result.details, error: true } };
|
|
227
226
|
}
|
|
228
|
-
const text = errorMessage(error);
|
|
229
|
-
let reason = "ask_failed";
|
|
230
|
-
if (text.startsWith("No reply from")) {
|
|
231
|
-
reason = "reply_timeout";
|
|
232
|
-
} else if (text.startsWith("Target ")) {
|
|
233
|
-
reason = "ambiguous_target";
|
|
234
|
-
}
|
|
235
227
|
return toolError(
|
|
236
|
-
`Failed: ${
|
|
228
|
+
`Failed: ${errorMessage(error)}`,
|
|
237
229
|
failureDetails(
|
|
238
|
-
|
|
230
|
+
error instanceof RecoveryError ? error.code : "ask_failed",
|
|
239
231
|
[
|
|
240
232
|
{ action: "status" },
|
|
241
233
|
{ action: "list" },
|
|
@@ -409,8 +401,7 @@ export class IntercomOutbound {
|
|
|
409
401
|
}
|
|
410
402
|
}
|
|
411
403
|
private replyFailure(error: unknown, replyTo: string | undefined): ToolResultLike {
|
|
412
|
-
const
|
|
413
|
-
const reason = replyFailureReason(text);
|
|
404
|
+
const reason = error instanceof RecoveryError ? error.code : "reply_failed";
|
|
414
405
|
const actions: readonly RecoveryAction[] =
|
|
415
406
|
reason === "no_pending_reply"
|
|
416
407
|
? [
|
|
@@ -423,7 +414,7 @@ export class IntercomOutbound {
|
|
|
423
414
|
]
|
|
424
415
|
: [{ action: "pending" }, { action: "list" }];
|
|
425
416
|
return toolError(
|
|
426
|
-
`Failed to reply: ${
|
|
417
|
+
`Failed to reply: ${errorMessage(error)}`,
|
|
427
418
|
failureDetails(reason, actions, {
|
|
428
419
|
error: true,
|
|
429
420
|
...((replyTo ?? "") !== "" ? { replyTo } : {}),
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import type { Message, SessionInfo } from "./types.ts";
|
|
2
2
|
import { formatSessionTarget, resolveSessionTarget, shortSessionId } from "./session-targets.ts";
|
|
3
|
+
import { RecoveryError } from "./tool-results.ts";
|
|
3
4
|
|
|
4
5
|
export interface IntercomContext {
|
|
5
6
|
readonly from: SessionInfo;
|
|
@@ -55,7 +56,7 @@ function matchingSender(
|
|
|
55
56
|
): readonly IntercomContext[] {
|
|
56
57
|
const tooShort = tooShortSenderTargetMessage(contexts, to);
|
|
57
58
|
if (tooShort !== null && tooShort !== "") {
|
|
58
|
-
throw new
|
|
59
|
+
throw new RecoveryError("ambiguous_reply_target", tooShort);
|
|
59
60
|
}
|
|
60
61
|
return resolveBySenderTarget(contexts, to);
|
|
61
62
|
}
|
|
@@ -66,7 +67,7 @@ function explicitReply(
|
|
|
66
67
|
): IntercomContext {
|
|
67
68
|
const target = contexts.find((context) => context.message.id === replyTo);
|
|
68
69
|
if (!target) {
|
|
69
|
-
throw new
|
|
70
|
+
throw new RecoveryError("no_pending_reply", `No pending ask with replyTo "${replyTo}"`);
|
|
70
71
|
}
|
|
71
72
|
if (
|
|
72
73
|
to !== "" &&
|
|
@@ -82,11 +83,12 @@ function replyBySender(contexts: readonly IntercomContext[], to: string): Interc
|
|
|
82
83
|
return matches[0];
|
|
83
84
|
}
|
|
84
85
|
if (matches.length > 1) {
|
|
85
|
-
throw new
|
|
86
|
+
throw new RecoveryError(
|
|
87
|
+
"ambiguous_reply_target",
|
|
86
88
|
`Multiple pending asks from "${to}" — use one of: ${pendingSenderOptions(matches, contexts)}.`,
|
|
87
89
|
);
|
|
88
90
|
}
|
|
89
|
-
throw new
|
|
91
|
+
throw new RecoveryError("no_pending_reply", `No pending ask from "${to}"`);
|
|
90
92
|
}
|
|
91
93
|
export class ReplyTracker {
|
|
92
94
|
private readonly pendingAsks = new Map<string, IntercomContext>();
|
|
@@ -209,9 +211,9 @@ export class ReplyTracker {
|
|
|
209
211
|
return pending[0];
|
|
210
212
|
}
|
|
211
213
|
if (pending.length === 0) {
|
|
212
|
-
throw new
|
|
214
|
+
throw new RecoveryError("no_pending_reply", "No active intercom context to reply to");
|
|
213
215
|
}
|
|
214
|
-
throw new
|
|
216
|
+
throw new RecoveryError("ambiguous_reply_target", "Multiple pending asks — specify `to`");
|
|
215
217
|
}
|
|
216
218
|
|
|
217
219
|
markReplied(replyTo: string): void {
|
|
@@ -3,7 +3,7 @@ import type { IntercomClient, SendResult } from "./broker/client.ts";
|
|
|
3
3
|
import type { IntercomTransport } from "./transport.ts";
|
|
4
4
|
import type { IntercomLifecycle } from "./lifecycle.ts";
|
|
5
5
|
import { asError } from "./validation.ts";
|
|
6
|
-
import { AskDeliveryError } from "./tool-results.ts";
|
|
6
|
+
import { AskDeliveryError, RecoveryError } from "./tool-results.ts";
|
|
7
7
|
|
|
8
8
|
type Lifecycle = Readonly<Pick<IntercomLifecycle, "markActivity">>;
|
|
9
9
|
interface ReplyWaiter {
|
|
@@ -71,7 +71,8 @@ export class ReplyWait {
|
|
|
71
71
|
const { from, replyTo, signal } = options;
|
|
72
72
|
const timeout = setTimeout(() => {
|
|
73
73
|
this.reject(
|
|
74
|
-
new
|
|
74
|
+
new RecoveryError(
|
|
75
|
+
"reply_timeout",
|
|
75
76
|
`No reply from "${from}" within ${Math.max(1, Math.round(this.timeoutMs / 60000))} minute(s)`,
|
|
76
77
|
),
|
|
77
78
|
);
|
|
@@ -17,7 +17,6 @@ export type InboundCheckpoint =
|
|
|
17
17
|
readonly messageId: string;
|
|
18
18
|
readonly stage: PendingInboundMessage["stage"] | "discarded" | "reply-retired";
|
|
19
19
|
};
|
|
20
|
-
export type RequestedDelivery = MessageDelivery | "auto";
|
|
21
20
|
export type InboundDelivery = "trigger" | "followUp" | "steer" | "passive";
|
|
22
21
|
export type IntercomSessionScope = "project" | "all";
|
|
23
22
|
export interface ToolResultLike {
|
|
@@ -2,17 +2,20 @@ import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
|
2
2
|
import type { IntercomTools } from "./tools.ts";
|
|
3
3
|
import { throwIfToolError } from "./tool-results.ts";
|
|
4
4
|
import { intercomSchema } from "./tool-schemas.ts";
|
|
5
|
+
import { registerCompactMessages, renderCompactResult } from "../shared/compact-cards.ts";
|
|
5
6
|
|
|
6
7
|
export function registerIntercomTools(
|
|
7
8
|
pi: ExtensionAPI,
|
|
8
9
|
tools: Readonly<Pick<IntercomTools, "execute">>,
|
|
9
10
|
): void {
|
|
11
|
+
registerCompactMessages(pi, ["intercom_message", "subagent-human-message"]);
|
|
10
12
|
pi.registerTool({
|
|
11
13
|
name: "intercom",
|
|
12
14
|
label: "Intercom",
|
|
13
15
|
description:
|
|
14
|
-
"
|
|
16
|
+
"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.",
|
|
15
17
|
parameters: intercomSchema,
|
|
18
|
+
renderResult: renderCompactResult,
|
|
16
19
|
async execute(_id, params, signal, _update, ctx) {
|
|
17
20
|
const result = throwIfToolError(await tools.execute(params, signal, ctx));
|
|
18
21
|
return { content: [...result.content], details: result.details };
|
|
@@ -1,5 +1,10 @@
|
|
|
1
1
|
import type { SendResult } from "./types.ts";
|
|
2
2
|
import type { ToolResultLike } from "./runtime-types.ts";
|
|
3
|
+
type RecoveryCode =
|
|
4
|
+
| "ambiguous_target"
|
|
5
|
+
| "reply_timeout"
|
|
6
|
+
| "no_pending_reply"
|
|
7
|
+
| "ambiguous_reply_target";
|
|
3
8
|
export interface RecoveryAction {
|
|
4
9
|
readonly action: "list" | "pending" | "send" | "status";
|
|
5
10
|
readonly guidance?: string;
|
|
@@ -11,16 +16,12 @@ export function failureDetails(
|
|
|
11
16
|
): Readonly<Record<string, unknown>> {
|
|
12
17
|
return { ...details, reasonCode, nextActions };
|
|
13
18
|
}
|
|
14
|
-
export
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
}
|
|
20
|
-
if (message.startsWith("Multiple pending asks") || message.includes("too short")) {
|
|
21
|
-
return "ambiguous_reply_target";
|
|
19
|
+
export class RecoveryError extends Error {
|
|
20
|
+
readonly code: RecoveryCode;
|
|
21
|
+
constructor(code: RecoveryCode, message: string) {
|
|
22
|
+
super(message);
|
|
23
|
+
this.code = code;
|
|
22
24
|
}
|
|
23
|
-
return "reply_failed";
|
|
24
25
|
}
|
|
25
26
|
export function firstTextContent(result: {
|
|
26
27
|
readonly content?: readonly { readonly type: string; readonly text?: string }[];
|