@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,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
10
|
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
|
-
}
|
|
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 }[];
|
|
@@ -1,26 +1,14 @@
|
|
|
1
1
|
import { StringEnum } from "@earendil-works/pi-ai";
|
|
2
2
|
import { Type } from "../agent/typebox.ts";
|
|
3
3
|
export const intercomSchema = Type.Object({
|
|
4
|
-
action: StringEnum(["list", "send", "ask", "reply", "pending", "status", "last"] as const,
|
|
5
|
-
description:
|
|
6
|
-
"List, send, ask, reply, pending asks, connection status, or last completed assistant response.",
|
|
7
|
-
}),
|
|
4
|
+
action: StringEnum(["list", "send", "ask", "reply", "pending", "status", "last"] as const),
|
|
8
5
|
scope: Type.Optional(
|
|
9
6
|
StringEnum(["project", "all"] as const, {
|
|
10
|
-
description:
|
|
11
|
-
"For list/status: 'all' (default) includes every reachable session; 'project' further filters this repository and its worktrees.",
|
|
12
|
-
}),
|
|
13
|
-
),
|
|
14
|
-
to: Type.Optional(
|
|
15
|
-
Type.String({
|
|
16
|
-
description: "Target session name or ID (for 'send', 'ask', or disambiguating 'reply')",
|
|
17
|
-
}),
|
|
18
|
-
),
|
|
19
|
-
message: Type.Optional(
|
|
20
|
-
Type.String({
|
|
21
|
-
description: "Message for send/ask/reply",
|
|
7
|
+
description: "all (default) or project (repo and worktrees)",
|
|
22
8
|
}),
|
|
23
9
|
),
|
|
10
|
+
to: Type.Optional(Type.String({ description: "Session name or ID" })),
|
|
11
|
+
message: Type.Optional(Type.String()),
|
|
24
12
|
attachments: Type.Optional(
|
|
25
13
|
Type.Array(
|
|
26
14
|
Type.Object({
|
|
@@ -31,41 +19,21 @@ export const intercomSchema = Type.Object({
|
|
|
31
19
|
}),
|
|
32
20
|
),
|
|
33
21
|
),
|
|
34
|
-
replyTo: Type.Optional(
|
|
35
|
-
Type.String({
|
|
36
|
-
description: "Message ID to reply to (for threading or responding to an 'ask')",
|
|
37
|
-
}),
|
|
38
|
-
),
|
|
22
|
+
replyTo: Type.Optional(Type.String({ description: "Message ID to answer or thread" })),
|
|
39
23
|
delivery: Type.Optional(
|
|
40
24
|
StringEnum(["queue", "steer", "passive"] as const, {
|
|
41
25
|
description:
|
|
42
|
-
"
|
|
26
|
+
"steer (default) at next tool boundary; queue after active work; passive never wakes the model",
|
|
43
27
|
}),
|
|
44
28
|
),
|
|
45
29
|
queueMode: Type.Optional(
|
|
46
30
|
StringEnum(["stack", "replace"] as const, {
|
|
47
|
-
description:
|
|
48
|
-
"For delivery='queue': 'stack' keeps all messages; 'replace' keeps only the latest undelivered message for the same threadId.",
|
|
49
|
-
}),
|
|
50
|
-
),
|
|
51
|
-
threadId: Type.Optional(
|
|
52
|
-
Type.String({ description: "Stable topic key for queueMode='replace'." }),
|
|
53
|
-
),
|
|
54
|
-
entryId: Type.Optional(Type.String({ description: "Last response entry ID for stable paging." })),
|
|
55
|
-
offset: Type.Optional(
|
|
56
|
-
Type.Integer({ minimum: 0, description: "Last response character offset (UTF-16)." }),
|
|
57
|
-
),
|
|
58
|
-
limit: Type.Optional(
|
|
59
|
-
Type.Integer({
|
|
60
|
-
minimum: 1,
|
|
61
|
-
maximum: 16384,
|
|
62
|
-
description: "Last response page size; default/max 16384 characters.",
|
|
63
|
-
}),
|
|
64
|
-
),
|
|
65
|
-
passive: Type.Optional(
|
|
66
|
-
Type.Boolean({
|
|
67
|
-
description:
|
|
68
|
-
"For action='send' only: legacy alias for delivery='passive'. Discouraged for agent-to-agent messages.",
|
|
31
|
+
description: "stack keeps all; replace keeps only the latest per threadId",
|
|
69
32
|
}),
|
|
70
33
|
),
|
|
34
|
+
threadId: Type.Optional(Type.String()),
|
|
35
|
+
entryId: Type.Optional(Type.String()),
|
|
36
|
+
offset: Type.Optional(Type.Integer({ minimum: 0, description: "UTF-16 characters" })),
|
|
37
|
+
limit: Type.Optional(Type.Integer({ minimum: 1, maximum: 16384 })),
|
|
38
|
+
passive: Type.Optional(Type.Boolean({ description: "Legacy alias for delivery=passive" })),
|
|
71
39
|
});
|