jeopi-cli 16.6.12 → 16.6.13
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 +10 -0
- package/README.md +1 -1
- package/dist/cli.js +4059 -4054
- package/dist/types/cli/update-cli.d.ts +9 -0
- package/dist/types/messenger/activity.d.ts +7 -0
- package/dist/types/messenger/gateway.d.ts +5 -1
- package/dist/types/messenger/types.d.ts +8 -1
- package/package.json +12 -12
- package/src/cli/update-cli.ts +13 -3
- package/src/extensibility/plugins/legacy-pi-bundled-keys.ts +2 -0
- package/src/extensibility/plugins/legacy-pi-bundled-registry.ts +8 -0
- package/src/internal-urls/docs-index.generated.txt +1 -1
- package/src/messenger/activity.ts +24 -0
- package/src/messenger/discord.ts +58 -0
- package/src/messenger/gateway.ts +173 -2
- package/src/messenger/live-session.ts +16 -2
- package/src/messenger/session.ts +6 -1
- package/src/messenger/types.ts +13 -1
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import type { AgentSessionEvent } from "../session/agent-session";
|
|
2
|
+
import type { MessengerActivityReporter } from "./types";
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Derive one-line progress from session events: the model's declared tool intent (falling back to the
|
|
6
|
+
* tool name) and long-running lifecycle notices. Raw tool arguments, results and errors never surface.
|
|
7
|
+
*/
|
|
8
|
+
export function createMessengerActivityTracker(report: MessengerActivityReporter): (event: AgentSessionEvent) => void {
|
|
9
|
+
let step = 0;
|
|
10
|
+
return event => {
|
|
11
|
+
switch (event.type) {
|
|
12
|
+
case "tool_execution_start":
|
|
13
|
+
step++;
|
|
14
|
+
report(`Step ${step} · ${event.intent ?? `Using ${event.toolName}`}`);
|
|
15
|
+
return;
|
|
16
|
+
case "auto_compaction_start":
|
|
17
|
+
report("Compacting context");
|
|
18
|
+
return;
|
|
19
|
+
case "auto_retry_start":
|
|
20
|
+
report(`Retrying after a provider error (${event.attempt}/${event.maxAttempts})`);
|
|
21
|
+
return;
|
|
22
|
+
}
|
|
23
|
+
};
|
|
24
|
+
}
|
package/src/messenger/discord.ts
CHANGED
|
@@ -14,6 +14,8 @@ const PERMANENT_CLOSE_CODES = new Set([4004, 4010, 4011, 4012, 4013, 4014]);
|
|
|
14
14
|
// they are part of the delivered text, and consume one UTF-16 unit each.
|
|
15
15
|
const DISCORD_WHITESPACE = /^[\s\u0085\u001c-\u001f]$/u;
|
|
16
16
|
const DISCORD_BOUNDARY_GUARD = "\u200b";
|
|
17
|
+
// SUPPRESS_EMBEDS | SUPPRESS_NOTIFICATIONS: a status line never unfurls links or pings on creation.
|
|
18
|
+
const DISCORD_ACTIVITY_FLAGS = (1 << 2) | (1 << 12);
|
|
17
19
|
|
|
18
20
|
function splitDiscordText(text: string): { chunks: string[]; needsAttachment: boolean } {
|
|
19
21
|
const chunks: string[] = [];
|
|
@@ -333,6 +335,7 @@ export function createDiscordAdapter(
|
|
|
333
335
|
if (oldest !== undefined) createdThreads.delete(oldest);
|
|
334
336
|
}
|
|
335
337
|
};
|
|
338
|
+
const activityMessages = new Map<string, string>();
|
|
336
339
|
const headers = (): Record<string, string> => {
|
|
337
340
|
if (!token) throw new DiscordConfigurationError("Discord bot token is missing");
|
|
338
341
|
return { Authorization: `Bot ${token}`, "Content-Type": "application/json" };
|
|
@@ -504,6 +507,61 @@ export function createDiscordAdapter(
|
|
|
504
507
|
deps.fetch,
|
|
505
508
|
);
|
|
506
509
|
},
|
|
510
|
+
async activity(message, text, signal) {
|
|
511
|
+
const channelId = message.threadId ?? message.channelId;
|
|
512
|
+
const key = JSON.stringify([channelId, message.messageId]);
|
|
513
|
+
const messages = `${API}/channels/${encodeURIComponent(channelId)}/messages`;
|
|
514
|
+
const statusId = activityMessages.get(key);
|
|
515
|
+
if (text === undefined) {
|
|
516
|
+
if (!statusId) return;
|
|
517
|
+
try {
|
|
518
|
+
await messengerFetchJson(
|
|
519
|
+
`${messages}/${encodeURIComponent(statusId)}`,
|
|
520
|
+
{ method: "DELETE", headers: headers() },
|
|
521
|
+
signal,
|
|
522
|
+
deps.fetch,
|
|
523
|
+
);
|
|
524
|
+
} catch (error) {
|
|
525
|
+
// Keep the ID so the caller can retry; a missing message is already removed.
|
|
526
|
+
if (!(error instanceof MessengerHttpError && error.status === 404)) throw error;
|
|
527
|
+
}
|
|
528
|
+
activityMessages.delete(key);
|
|
529
|
+
return;
|
|
530
|
+
}
|
|
531
|
+
const content = `⏳ ${text}`;
|
|
532
|
+
if (statusId) {
|
|
533
|
+
try {
|
|
534
|
+
// Edits re-parse mentions with default allowances unless allowed_mentions is repeated.
|
|
535
|
+
await messengerFetchJson(
|
|
536
|
+
`${messages}/${encodeURIComponent(statusId)}`,
|
|
537
|
+
{
|
|
538
|
+
method: "PATCH",
|
|
539
|
+
headers: headers(),
|
|
540
|
+
body: JSON.stringify({ content, allowed_mentions: { parse: [] } }),
|
|
541
|
+
},
|
|
542
|
+
signal,
|
|
543
|
+
deps.fetch,
|
|
544
|
+
);
|
|
545
|
+
return;
|
|
546
|
+
} catch (error) {
|
|
547
|
+
if (!(error instanceof MessengerHttpError && error.status === 404)) throw error;
|
|
548
|
+
activityMessages.delete(key);
|
|
549
|
+
}
|
|
550
|
+
}
|
|
551
|
+
const created = await messengerFetchJson(
|
|
552
|
+
messages,
|
|
553
|
+
{
|
|
554
|
+
method: "POST",
|
|
555
|
+
headers: headers(),
|
|
556
|
+
body: JSON.stringify({ content, allowed_mentions: { parse: [] }, flags: DISCORD_ACTIVITY_FLAGS }),
|
|
557
|
+
},
|
|
558
|
+
signal,
|
|
559
|
+
deps.fetch,
|
|
560
|
+
);
|
|
561
|
+
if (!isRecord(created) || typeof created.id !== "string" || !created.id)
|
|
562
|
+
throw new Error("Discord activity message returned no ID");
|
|
563
|
+
activityMessages.set(key, created.id);
|
|
564
|
+
},
|
|
507
565
|
async send(message, text, signal) {
|
|
508
566
|
let files: { name: string; data: Uint8Array }[] = [];
|
|
509
567
|
if (config.artifactDir) {
|
package/src/messenger/gateway.ts
CHANGED
|
@@ -1,5 +1,12 @@
|
|
|
1
1
|
import { logger } from "jeopi-utils";
|
|
2
|
-
import
|
|
2
|
+
import { MessengerHttpError } from "./http";
|
|
3
|
+
import type {
|
|
4
|
+
MessengerActivityReporter,
|
|
5
|
+
MessengerAdapter,
|
|
6
|
+
MessengerConfig,
|
|
7
|
+
MessengerMessage,
|
|
8
|
+
MessengerReply,
|
|
9
|
+
} from "./types";
|
|
3
10
|
import { MessengerFatalError } from "./types";
|
|
4
11
|
|
|
5
12
|
const MAX_MESSAGE_LENGTH = 32_000;
|
|
@@ -7,6 +14,157 @@ const MAX_QUEUED_MESSAGES = 100;
|
|
|
7
14
|
const MAX_SEEN_MESSAGES = 10_000;
|
|
8
15
|
const STATUS_TIMEOUT_MS = 1000;
|
|
9
16
|
const MAX_STATUS_IN_FLIGHT = 8;
|
|
17
|
+
const MAX_ACTIVITY_LENGTH = 120;
|
|
18
|
+
// Status edits are cosmetic: keep them sparse so they never compete with reply delivery for rate limits.
|
|
19
|
+
const ACTIVITY_INTERVAL_MS = 2_000;
|
|
20
|
+
const ACTIVITY_TIMEOUT_MS = 3_000;
|
|
21
|
+
|
|
22
|
+
/** Collapse to one bounded line: drop invisible format characters, fold control and whitespace runs. */
|
|
23
|
+
function activityLine(text: string): string {
|
|
24
|
+
const line = text
|
|
25
|
+
.replace(/\p{Cf}/gu, "")
|
|
26
|
+
.replace(/[\p{Cc}\p{Z}\s]+/gu, " ")
|
|
27
|
+
.trim();
|
|
28
|
+
if (line.length <= MAX_ACTIVITY_LENGTH) return line;
|
|
29
|
+
let end = MAX_ACTIVITY_LENGTH - 1;
|
|
30
|
+
const last = line.charCodeAt(end - 1);
|
|
31
|
+
if (last >= 0xd800 && last <= 0xdbff) end--;
|
|
32
|
+
return `${line.slice(0, end).trimEnd()}…`;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
interface ActivityReporter {
|
|
36
|
+
report: MessengerActivityReporter;
|
|
37
|
+
/** Stop updates and remove any shown line; idempotent and never rejects. */
|
|
38
|
+
close(): Promise<void>;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
const NO_ACTIVITY: ActivityReporter = { report() {}, close: () => Promise.resolve() };
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Bound a cosmetic adapter call: a stalled provider request must not hold up the request queue. A timeout
|
|
45
|
+
* rejects so callers can retry; cancellation through `signal` resolves because the request is ending.
|
|
46
|
+
*/
|
|
47
|
+
async function boundedActivity(run: (signal: AbortSignal) => Promise<void>, signal?: AbortSignal): Promise<void> {
|
|
48
|
+
const timeout = new AbortController();
|
|
49
|
+
const callSignal = signal ? AbortSignal.any([signal, timeout.signal]) : timeout.signal;
|
|
50
|
+
const stopped = Promise.withResolvers<void>();
|
|
51
|
+
const onStop = (): void => {
|
|
52
|
+
if (timeout.signal.aborted) stopped.reject(new Error("Messenger activity update timed out"));
|
|
53
|
+
else stopped.resolve();
|
|
54
|
+
};
|
|
55
|
+
callSignal.addEventListener("abort", onStop, { once: true });
|
|
56
|
+
const timer = setTimeout(() => timeout.abort(), ACTIVITY_TIMEOUT_MS);
|
|
57
|
+
try {
|
|
58
|
+
await Promise.race([Promise.resolve().then(() => run(callSignal)), stopped.promise]);
|
|
59
|
+
} finally {
|
|
60
|
+
clearTimeout(timer);
|
|
61
|
+
callSignal.removeEventListener("abort", onStop);
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Coalesce a request's activity into one status line: show the first report at once, then at most the
|
|
67
|
+
* latest line per interval. Removal skips the request signal so an aborted request still retires its line.
|
|
68
|
+
*/
|
|
69
|
+
function createActivityReporter(
|
|
70
|
+
adapter: MessengerAdapter,
|
|
71
|
+
destination: MessengerMessage,
|
|
72
|
+
signal: AbortSignal,
|
|
73
|
+
intervalMs: number,
|
|
74
|
+
): ActivityReporter {
|
|
75
|
+
const activity = adapter.activity;
|
|
76
|
+
if (!activity) return NO_ACTIVITY;
|
|
77
|
+
let latest: string | undefined;
|
|
78
|
+
let sent: string | undefined;
|
|
79
|
+
let shown = false;
|
|
80
|
+
let closed = false;
|
|
81
|
+
let pumping = false;
|
|
82
|
+
let pump = Promise.resolve();
|
|
83
|
+
let nextAt = 0;
|
|
84
|
+
let wake: (() => void) | undefined;
|
|
85
|
+
let closing: Promise<void> | undefined;
|
|
86
|
+
const update = (text: string | undefined, callSignal?: AbortSignal): Promise<void> =>
|
|
87
|
+
boundedActivity(bounded => activity.call(adapter, destination, text, bounded), callSignal);
|
|
88
|
+
const pause = async (ms: number): Promise<void> => {
|
|
89
|
+
const { promise, resolve } = Promise.withResolvers<void>();
|
|
90
|
+
const timer = setTimeout(resolve, ms);
|
|
91
|
+
wake = () => {
|
|
92
|
+
clearTimeout(timer);
|
|
93
|
+
resolve();
|
|
94
|
+
};
|
|
95
|
+
try {
|
|
96
|
+
await promise;
|
|
97
|
+
} finally {
|
|
98
|
+
wake = undefined;
|
|
99
|
+
}
|
|
100
|
+
};
|
|
101
|
+
const run = async (): Promise<void> => {
|
|
102
|
+
try {
|
|
103
|
+
while (!closed && !signal.aborted && latest !== sent) {
|
|
104
|
+
const delay = nextAt - Date.now();
|
|
105
|
+
if (delay > 0) {
|
|
106
|
+
await pause(delay);
|
|
107
|
+
continue;
|
|
108
|
+
}
|
|
109
|
+
const text = latest;
|
|
110
|
+
sent = text;
|
|
111
|
+
shown = true;
|
|
112
|
+
let spacing = intervalMs;
|
|
113
|
+
try {
|
|
114
|
+
await update(text, signal);
|
|
115
|
+
} catch (error) {
|
|
116
|
+
if (error instanceof MessengerHttpError && error.status === 429) {
|
|
117
|
+
// Keep the line pending: a rate-limited update is retried unless a newer line replaces it.
|
|
118
|
+
sent = undefined;
|
|
119
|
+
spacing = Math.max(spacing, error.retryAfterMs ?? 1_000);
|
|
120
|
+
} else if (!signal.aborted) {
|
|
121
|
+
logger.warn("Messenger activity update failed", { platform: destination.platform });
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
nextAt = Date.now() + spacing;
|
|
125
|
+
}
|
|
126
|
+
} finally {
|
|
127
|
+
pumping = false;
|
|
128
|
+
}
|
|
129
|
+
};
|
|
130
|
+
return {
|
|
131
|
+
report(text) {
|
|
132
|
+
if (closed || signal.aborted) return;
|
|
133
|
+
const line = activityLine(text);
|
|
134
|
+
if (!line) return;
|
|
135
|
+
latest = line;
|
|
136
|
+
if (pumping) return;
|
|
137
|
+
pumping = true;
|
|
138
|
+
pump = run();
|
|
139
|
+
},
|
|
140
|
+
close() {
|
|
141
|
+
closing ??= (async () => {
|
|
142
|
+
closed = true;
|
|
143
|
+
wake?.();
|
|
144
|
+
await pump;
|
|
145
|
+
if (!shown) return;
|
|
146
|
+
for (let attempt = 0; ; attempt++) {
|
|
147
|
+
try {
|
|
148
|
+
await update(undefined);
|
|
149
|
+
return;
|
|
150
|
+
} catch (error) {
|
|
151
|
+
const wait =
|
|
152
|
+
error instanceof MessengerHttpError && error.status === 429
|
|
153
|
+
? (error.retryAfterMs ?? 1_000)
|
|
154
|
+
: 1_000;
|
|
155
|
+
// One bounded retry: removal holds the request queue.
|
|
156
|
+
if (attempt > 0 || wait > ACTIVITY_TIMEOUT_MS) {
|
|
157
|
+
logger.warn("Messenger activity removal failed", { platform: destination.platform });
|
|
158
|
+
return;
|
|
159
|
+
}
|
|
160
|
+
await Bun.sleep(wait);
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
})();
|
|
164
|
+
return closing;
|
|
165
|
+
},
|
|
166
|
+
};
|
|
167
|
+
}
|
|
10
168
|
|
|
11
169
|
/** Include the sender even in shared threads: remote histories must not cross users. */
|
|
12
170
|
export function messengerConversationKey(message: MessengerMessage): string {
|
|
@@ -50,11 +208,18 @@ export interface MessengerGateway {
|
|
|
50
208
|
handle(message: MessengerMessage, signal?: AbortSignal): Promise<void>;
|
|
51
209
|
}
|
|
52
210
|
|
|
211
|
+
export interface MessengerGatewayOptions {
|
|
212
|
+
/** Minimum spacing between in-place activity edits for one request. */
|
|
213
|
+
activityIntervalMs?: number;
|
|
214
|
+
}
|
|
215
|
+
|
|
53
216
|
export function createMessengerGateway(
|
|
54
217
|
config: MessengerConfig,
|
|
55
218
|
adapters: MessengerAdapter[],
|
|
56
219
|
reply: MessengerReply,
|
|
220
|
+
options: MessengerGatewayOptions = {},
|
|
57
221
|
): MessengerGateway {
|
|
222
|
+
const activityIntervalMs = Math.max(0, options.activityIntervalMs ?? ACTIVITY_INTERVAL_MS);
|
|
58
223
|
const byPlatform = new Map(adapters.map(adapter => [adapter.platform, adapter]));
|
|
59
224
|
if (byPlatform.size !== adapters.length) throw new Error("Duplicate messenger adapter");
|
|
60
225
|
for (const platform of Object.keys(config.platforms)) {
|
|
@@ -76,6 +241,7 @@ export function createMessengerGateway(
|
|
|
76
241
|
if (!pending) break;
|
|
77
242
|
const { message, adapter, signal, resolve } = pending;
|
|
78
243
|
let failed = false;
|
|
244
|
+
let activity = NO_ACTIVITY;
|
|
79
245
|
let statusChain = Promise.resolve();
|
|
80
246
|
const status = (state: "processing" | "success" | "failure"): void => {
|
|
81
247
|
if (!adapter.status) return;
|
|
@@ -111,9 +277,10 @@ export function createMessengerGateway(
|
|
|
111
277
|
if (signal.aborted) continue;
|
|
112
278
|
status("processing");
|
|
113
279
|
const destination = (await adapter.prepare?.(message, signal)) ?? message;
|
|
280
|
+
activity = createActivityReporter(adapter, destination, signal, activityIntervalMs);
|
|
114
281
|
let text: string;
|
|
115
282
|
try {
|
|
116
|
-
text = await reply(destination, signal);
|
|
283
|
+
text = await reply(destination, signal, activity.report);
|
|
117
284
|
} catch (error) {
|
|
118
285
|
failed = true;
|
|
119
286
|
if (error instanceof MessengerFatalError) {
|
|
@@ -125,6 +292,9 @@ export function createMessengerGateway(
|
|
|
125
292
|
if (signal.aborted) continue;
|
|
126
293
|
logger.warn("Messenger agent turn failed", { platform: message.platform });
|
|
127
294
|
text = "The request could not be completed. Please try again.";
|
|
295
|
+
} finally {
|
|
296
|
+
// Retire the status line alongside reply delivery rather than delaying the reply on it.
|
|
297
|
+
void activity.close();
|
|
128
298
|
}
|
|
129
299
|
if (!signal.aborted && text.trim()) {
|
|
130
300
|
// A failed send may already have delivered a chunk: never retry the entire reply.
|
|
@@ -137,6 +307,7 @@ export function createMessengerGateway(
|
|
|
137
307
|
status("failure");
|
|
138
308
|
}
|
|
139
309
|
} finally {
|
|
310
|
+
await activity.close();
|
|
140
311
|
resolve();
|
|
141
312
|
}
|
|
142
313
|
}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import type { AssistantMessage, UserMessage } from "jeopi-ai";
|
|
2
2
|
import type { InteractiveModeContext } from "../modes/types";
|
|
3
|
+
import { createMessengerActivityTracker } from "./activity";
|
|
3
4
|
import { messengerArtifactPrompt } from "./artifacts";
|
|
4
5
|
import { loadMessengerConfig, validateMessengerEnvironment } from "./config";
|
|
5
6
|
import { createDiscordAdapter, type DiscordTransportDeps } from "./discord";
|
|
@@ -156,7 +157,7 @@ export function createLiveDiscordBridge(
|
|
|
156
157
|
let localCapture: object | undefined;
|
|
157
158
|
let close: () => Promise<void>;
|
|
158
159
|
let revoke: () => Promise<void>;
|
|
159
|
-
const reply: MessengerReply = async (message, signal) => {
|
|
160
|
+
const reply: MessengerReply = async (message, signal, onActivity) => {
|
|
160
161
|
if (closed || signal?.aborted) throw new Error(LIVE_UNAVAILABLE_MESSAGE);
|
|
161
162
|
if (!sameSession(ctx, attachedSession, sessionId, sessionFile)) throw new Error(LIVE_UNAVAILABLE_MESSAGE);
|
|
162
163
|
if (!isMessengerMessageAllowed(message, scopedConfig)) throw new Error(LIVE_UNAVAILABLE_MESSAGE);
|
|
@@ -190,8 +191,13 @@ export function createLiveDiscordBridge(
|
|
|
190
191
|
void revoke();
|
|
191
192
|
};
|
|
192
193
|
operationSignal.addEventListener("abort", onAbort, { once: true });
|
|
194
|
+
const track = onActivity && createMessengerActivityTracker(onActivity);
|
|
193
195
|
const unsubscribe = attachedSession.subscribe(event => {
|
|
194
|
-
if (event.type !== "message_end")
|
|
196
|
+
if (event.type !== "message_end") {
|
|
197
|
+
// Report only this Discord turn: its prompt event precedes the turn's tool and retry events.
|
|
198
|
+
if (capture.promptSeen && !capture.conflicted) track?.(event);
|
|
199
|
+
return;
|
|
200
|
+
}
|
|
195
201
|
if (event.message.role === "user") {
|
|
196
202
|
const text = userMessageText(event.message);
|
|
197
203
|
if (!capture.promptSeen && text === capture.promptText) {
|
|
@@ -265,6 +271,7 @@ export function createLiveDiscordBridge(
|
|
|
265
271
|
const adapterFactory = dependencies.createAdapter ?? createDiscordAdapter;
|
|
266
272
|
const gatewayFactory = dependencies.createGateway ?? createMessengerGateway;
|
|
267
273
|
const adapter = adapterFactory(discordConfig, dependencies.env, dependencies.transport);
|
|
274
|
+
const activity = adapter.activity;
|
|
268
275
|
const guardedAdapter: MessengerAdapter = {
|
|
269
276
|
...adapter,
|
|
270
277
|
send: async (message, text, signal) => {
|
|
@@ -277,6 +284,13 @@ export function createLiveDiscordBridge(
|
|
|
277
284
|
}
|
|
278
285
|
await adapter.send(message, text, sendSignal);
|
|
279
286
|
},
|
|
287
|
+
// New status text requires the attached session; removal must still retire a shown line after close.
|
|
288
|
+
activity:
|
|
289
|
+
activity &&
|
|
290
|
+
(async (message, text, signal) => {
|
|
291
|
+
if (text !== undefined && (closed || !sameSession(ctx, attachedSession, sessionId, sessionFile))) return;
|
|
292
|
+
await activity.call(adapter, message, text, signal);
|
|
293
|
+
}),
|
|
280
294
|
};
|
|
281
295
|
const setRelayDestination = (channelId: string | undefined): void => {
|
|
282
296
|
if (closed || !sameSession(ctx, attachedSession, sessionId, sessionFile))
|
package/src/messenger/session.ts
CHANGED
|
@@ -10,6 +10,7 @@ import type { AgentSession } from "../session/agent-session";
|
|
|
10
10
|
import type { AuthStorage } from "../session/auth-storage";
|
|
11
11
|
import { USER_INTERRUPT_LABEL } from "../session/messages";
|
|
12
12
|
import { SessionManager } from "../session/session-manager";
|
|
13
|
+
import { createMessengerActivityTracker } from "./activity";
|
|
13
14
|
import { messengerArtifactPrompt } from "./artifacts";
|
|
14
15
|
import { messengerConversationKey } from "./gateway";
|
|
15
16
|
import { type MessengerConfig, MessengerFatalError, type MessengerReply } from "./types";
|
|
@@ -123,12 +124,13 @@ export function createMessengerSessionBridge(
|
|
|
123
124
|
activeRoute = route;
|
|
124
125
|
}
|
|
125
126
|
|
|
126
|
-
const reply: MessengerReply = async (message, signal) => {
|
|
127
|
+
const reply: MessengerReply = async (message, signal, onActivity) => {
|
|
127
128
|
if (closed || signal.aborted) throw new Error("Messenger session is closed");
|
|
128
129
|
if (activeTurn) throw new Error("Messenger turns must be serialized");
|
|
129
130
|
const finished = Promise.withResolvers<void>();
|
|
130
131
|
activeTurn = finished.promise;
|
|
131
132
|
let aborting: Promise<void> | undefined;
|
|
133
|
+
let unsubscribe: (() => void) | undefined;
|
|
132
134
|
const onAbort = (): void => {
|
|
133
135
|
if (session && !aborting) {
|
|
134
136
|
aborting = session.abort({ reason: USER_INTERRUPT_LABEL });
|
|
@@ -182,6 +184,8 @@ export function createMessengerSessionBridge(
|
|
|
182
184
|
const guidance = await messengerArtifactPrompt(artifactRoot, message, signal);
|
|
183
185
|
if (guidance) prompt += `\n\n${guidance}`;
|
|
184
186
|
}
|
|
187
|
+
// Turns are serialized, so every event until idle belongs to this request.
|
|
188
|
+
if (onActivity) unsubscribe = current.subscribe(createMessengerActivityTracker(onActivity));
|
|
185
189
|
const dispatched = await current.prompt(prompt, { expandPromptTemplates: false });
|
|
186
190
|
await current.waitForIdle();
|
|
187
191
|
await current.sessionManager.flush();
|
|
@@ -197,6 +201,7 @@ export function createMessengerSessionBridge(
|
|
|
197
201
|
if (!text.trim()) throw new Error("Messenger response is empty");
|
|
198
202
|
return text;
|
|
199
203
|
} finally {
|
|
204
|
+
unsubscribe?.();
|
|
200
205
|
signal.removeEventListener("abort", onAbort);
|
|
201
206
|
await aborting?.catch(() => {});
|
|
202
207
|
activeTurn = undefined;
|
package/src/messenger/types.ts
CHANGED
|
@@ -14,6 +14,9 @@ export interface MessengerMessage {
|
|
|
14
14
|
attachments?: { url: string; filename: string; size: number }[];
|
|
15
15
|
}
|
|
16
16
|
|
|
17
|
+
/** Report a short, human-readable description of the work in progress. */
|
|
18
|
+
export type MessengerActivityReporter = (text: string) => void;
|
|
19
|
+
|
|
17
20
|
export interface MessengerAdapter {
|
|
18
21
|
platform: MessengerPlatform;
|
|
19
22
|
start(onMessage: (message: MessengerMessage) => Promise<void>, signal: AbortSignal): Promise<void>;
|
|
@@ -24,6 +27,11 @@ export interface MessengerAdapter {
|
|
|
24
27
|
status: "processing" | "success" | "failure",
|
|
25
28
|
signal?: AbortSignal,
|
|
26
29
|
): Promise<void>;
|
|
30
|
+
/**
|
|
31
|
+
* Show `text` (one sanitized line) as the request's single in-progress status, editing it in place on
|
|
32
|
+
* later calls; `undefined` removes it. The gateway serializes and throttles calls per request.
|
|
33
|
+
*/
|
|
34
|
+
activity?(message: MessengerMessage, text: string | undefined, signal?: AbortSignal): Promise<void>;
|
|
27
35
|
}
|
|
28
36
|
|
|
29
37
|
export interface MessengerPlatformConfig {
|
|
@@ -47,7 +55,11 @@ export interface MessengerTransportDeps {
|
|
|
47
55
|
WebSocket?: typeof WebSocket;
|
|
48
56
|
}
|
|
49
57
|
|
|
50
|
-
export type MessengerReply = (
|
|
58
|
+
export type MessengerReply = (
|
|
59
|
+
message: MessengerMessage,
|
|
60
|
+
signal: AbortSignal,
|
|
61
|
+
onActivity?: MessengerActivityReporter,
|
|
62
|
+
) => Promise<string>;
|
|
51
63
|
/** A partially initialized SDK must not be retried inside a live gateway. */
|
|
52
64
|
export class MessengerFatalError extends Error {
|
|
53
65
|
constructor() {
|