@llblab/pi-telegram 0.24.11 → 0.25.1
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/AGENTS.md +3 -2
- package/BACKLOG.md +15 -1
- package/CHANGELOG.md +9 -1
- package/README.md +5 -4
- package/docs/activity.md +9 -7
- package/docs/architecture.md +3 -1
- package/docs/multi-instance-bus.md +11 -8
- package/docs/outbound.md +23 -1
- package/docs/public-api.md +4 -2
- package/docs/sections.md +4 -3
- package/docs/ui-style.md +2 -0
- package/index.ts +35 -1
- package/lib/activity-verbosity.ts +494 -0
- package/lib/bindings.ts +13 -2
- package/lib/config.ts +65 -17
- package/lib/menu-settings.ts +111 -45
- package/lib/queue.ts +4 -0
- package/lib/telegram-api.ts +32 -1
- package/package.json +1 -1
|
@@ -0,0 +1,494 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Bridge-owned Telegram activity verbosity projection
|
|
3
|
+
* Zones: telegram activity, rich rendering, operational delivery
|
|
4
|
+
* Owns ephemeral reasoning drafts and bounded durable tool disclosures; excludes activity normalization, assistant answer rendering, and transport authority policy
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import type { TelegramActivityEvent } from "./activity.ts";
|
|
8
|
+
import { escapeHtml } from "./rendering.ts";
|
|
9
|
+
import type {
|
|
10
|
+
TelegramEditMessageTextBody,
|
|
11
|
+
TelegramRichText,
|
|
12
|
+
TelegramSendMessageBody,
|
|
13
|
+
TelegramSendRichMessageDraftBody,
|
|
14
|
+
TelegramSentMessage,
|
|
15
|
+
} from "./telegram-api.ts";
|
|
16
|
+
import type { TelegramTarget } from "./target.ts";
|
|
17
|
+
|
|
18
|
+
export const TELEGRAM_TOOL_ACTIVITY_ICON = "🛠";
|
|
19
|
+
export const TELEGRAM_ACTIVITY_DETAIL_MAX_CHARS = 1_200;
|
|
20
|
+
export const TELEGRAM_ACTIVITY_MESSAGE_MAX_CHARS = 3_900;
|
|
21
|
+
export const TELEGRAM_ACTIVITY_MESSAGE_MAX_TOOLS = 6;
|
|
22
|
+
export const TELEGRAM_REASONING_DRAFT_MAX_FRAMES = 24;
|
|
23
|
+
export const TELEGRAM_REASONING_BUFFER_MAX_CHARS = 1_200;
|
|
24
|
+
export const TELEGRAM_TOOL_UPDATE_MAX_ENTRIES = 4;
|
|
25
|
+
|
|
26
|
+
interface ToolActivity {
|
|
27
|
+
id: string;
|
|
28
|
+
name: string;
|
|
29
|
+
args: string;
|
|
30
|
+
updates: string[];
|
|
31
|
+
droppedUpdates: number;
|
|
32
|
+
result?: string;
|
|
33
|
+
isError?: boolean;
|
|
34
|
+
complete: boolean;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
interface ToolMessage {
|
|
38
|
+
messageId: number;
|
|
39
|
+
tools: ToolActivity[];
|
|
40
|
+
target: TelegramTarget;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function targetEquals(left: TelegramTarget, right: TelegramTarget): boolean {
|
|
44
|
+
return left.chatId === right.chatId && left.threadId === right.threadId;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function redactActivityText(text: string): string {
|
|
48
|
+
return text
|
|
49
|
+
.replace(/\b\d{8,12}:[A-Za-z0-9_-]{30,}\b/g, "[REDACTED_BOT_TOKEN]")
|
|
50
|
+
.replace(
|
|
51
|
+
/\b(Bearer\s+)[A-Za-z0-9._~+/=-]{16,}\b/gi,
|
|
52
|
+
"$1[REDACTED]",
|
|
53
|
+
)
|
|
54
|
+
.replace(
|
|
55
|
+
/(["']?(?:api[_-]?key|token|password|secret)["']?\s*[:=]\s*["']?)[^"',\s}]+/gi,
|
|
56
|
+
"$1[REDACTED]",
|
|
57
|
+
);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function renderReasoningRichText(text: string): TelegramRichText {
|
|
61
|
+
const parts: TelegramRichText[] = [];
|
|
62
|
+
let cursor = 0;
|
|
63
|
+
while (cursor < text.length) {
|
|
64
|
+
const codeStart = text.indexOf("`", cursor);
|
|
65
|
+
const boldStart = text.indexOf("**", cursor);
|
|
66
|
+
const starts = [codeStart, boldStart].filter((index) => index >= 0);
|
|
67
|
+
if (starts.length === 0) {
|
|
68
|
+
parts.push(text.slice(cursor));
|
|
69
|
+
break;
|
|
70
|
+
}
|
|
71
|
+
const start = Math.min(...starts);
|
|
72
|
+
if (start > cursor) parts.push(text.slice(cursor, start));
|
|
73
|
+
const marker = start === codeStart ? "`" : "**";
|
|
74
|
+
const end = text.indexOf(marker, start + marker.length);
|
|
75
|
+
if (end < 0) {
|
|
76
|
+
parts.push(text.slice(start));
|
|
77
|
+
break;
|
|
78
|
+
}
|
|
79
|
+
parts.push({
|
|
80
|
+
type: marker === "`" ? "code" : "bold",
|
|
81
|
+
text: text.slice(start + marker.length, end),
|
|
82
|
+
});
|
|
83
|
+
cursor = end + marker.length;
|
|
84
|
+
}
|
|
85
|
+
if (parts.length === 0) return "";
|
|
86
|
+
return parts.length === 1 ? parts[0]! : parts;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
function formatActivityJson(value: unknown, depth = 0): string[] {
|
|
90
|
+
const indent = " ".repeat(depth);
|
|
91
|
+
if (Array.isArray(value)) {
|
|
92
|
+
if (value.length === 0) return [`${indent}[]`];
|
|
93
|
+
if (
|
|
94
|
+
value.every(
|
|
95
|
+
(entry) =>
|
|
96
|
+
entry !== null && typeof entry === "object" && !Array.isArray(entry),
|
|
97
|
+
)
|
|
98
|
+
) {
|
|
99
|
+
const lines = [`${indent}[{`];
|
|
100
|
+
value.forEach((entry, index) => {
|
|
101
|
+
const fields = Object.entries(entry as Record<string, unknown>);
|
|
102
|
+
fields.forEach(([key, nested], fieldIndex) => {
|
|
103
|
+
const nestedLines = formatActivityJson(nested, depth + 1);
|
|
104
|
+
const nestedIndent = " ".repeat(depth + 1);
|
|
105
|
+
lines.push(
|
|
106
|
+
`${nestedIndent}${JSON.stringify(key)}: ${nestedLines[0]!.slice(nestedIndent.length)}`,
|
|
107
|
+
...nestedLines.slice(1),
|
|
108
|
+
);
|
|
109
|
+
if (fieldIndex < fields.length - 1) {
|
|
110
|
+
lines[lines.length - 1] += ",";
|
|
111
|
+
}
|
|
112
|
+
});
|
|
113
|
+
lines.push(
|
|
114
|
+
index < value.length - 1 ? `${indent}}, {` : `${indent}}]`,
|
|
115
|
+
);
|
|
116
|
+
});
|
|
117
|
+
return lines;
|
|
118
|
+
}
|
|
119
|
+
const lines = [`${indent}[`];
|
|
120
|
+
value.forEach((entry, index) => {
|
|
121
|
+
const nestedLines = formatActivityJson(entry, depth + 1);
|
|
122
|
+
if (index < value.length - 1) {
|
|
123
|
+
nestedLines[nestedLines.length - 1] += ",";
|
|
124
|
+
}
|
|
125
|
+
lines.push(...nestedLines);
|
|
126
|
+
});
|
|
127
|
+
lines.push(`${indent}]`);
|
|
128
|
+
return lines;
|
|
129
|
+
}
|
|
130
|
+
if (value !== null && typeof value === "object") {
|
|
131
|
+
const entries = Object.entries(value as Record<string, unknown>);
|
|
132
|
+
if (entries.length === 0) return [`${indent}{}`];
|
|
133
|
+
const lines = [`${indent}{`];
|
|
134
|
+
entries.forEach(([key, nested], index) => {
|
|
135
|
+
const nestedLines = formatActivityJson(nested, depth + 1);
|
|
136
|
+
const nestedIndent = " ".repeat(depth + 1);
|
|
137
|
+
lines.push(
|
|
138
|
+
`${nestedIndent}${JSON.stringify(key)}: ${nestedLines[0]!.slice(nestedIndent.length)}`,
|
|
139
|
+
...nestedLines.slice(1),
|
|
140
|
+
);
|
|
141
|
+
if (index < entries.length - 1) lines[lines.length - 1] += ",";
|
|
142
|
+
});
|
|
143
|
+
lines.push(`${indent}}`);
|
|
144
|
+
return lines;
|
|
145
|
+
}
|
|
146
|
+
return [`${indent}${JSON.stringify(value)}`];
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
function serializeActivityValue(value: unknown): string {
|
|
150
|
+
const seen = new WeakSet<object>();
|
|
151
|
+
let text: string;
|
|
152
|
+
try {
|
|
153
|
+
const normalized =
|
|
154
|
+
JSON.stringify(
|
|
155
|
+
value,
|
|
156
|
+
(_key, nested) => {
|
|
157
|
+
if (typeof nested === "bigint") return nested.toString();
|
|
158
|
+
if (nested && typeof nested === "object") {
|
|
159
|
+
if (seen.has(nested)) return "[Circular]";
|
|
160
|
+
seen.add(nested);
|
|
161
|
+
}
|
|
162
|
+
return nested;
|
|
163
|
+
},
|
|
164
|
+
) ?? JSON.stringify(String(value));
|
|
165
|
+
text = formatActivityJson(JSON.parse(normalized)).join("\n");
|
|
166
|
+
} catch {
|
|
167
|
+
text = JSON.stringify(String(value));
|
|
168
|
+
}
|
|
169
|
+
const redacted = redactActivityText(text);
|
|
170
|
+
if (redacted.length <= TELEGRAM_ACTIVITY_DETAIL_MAX_CHARS) return redacted;
|
|
171
|
+
const omitted = redacted.length - TELEGRAM_ACTIVITY_DETAIL_MAX_CHARS;
|
|
172
|
+
return `${redacted.slice(0, TELEGRAM_ACTIVITY_DETAIL_MAX_CHARS)}\n… [${omitted} chars truncated]`;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
function renderToolActivityHtml(tool: ToolActivity): string {
|
|
176
|
+
const evidence = [`"arguments": ${tool.args}`];
|
|
177
|
+
if (tool.droppedUpdates > 0) {
|
|
178
|
+
evidence.push(`… [${tool.droppedUpdates} earlier updates omitted]`);
|
|
179
|
+
}
|
|
180
|
+
tool.updates.forEach((update, index) => {
|
|
181
|
+
evidence.push(
|
|
182
|
+
`"update ${tool.droppedUpdates + index + 1}": ${update}`,
|
|
183
|
+
);
|
|
184
|
+
});
|
|
185
|
+
if (tool.complete && tool.result !== undefined) {
|
|
186
|
+
evidence.push(`"${tool.isError ? "error" : "result"}": ${tool.result}`);
|
|
187
|
+
}
|
|
188
|
+
const status = tool.complete
|
|
189
|
+
? tool.isError
|
|
190
|
+
? "failed"
|
|
191
|
+
: "done"
|
|
192
|
+
: "running";
|
|
193
|
+
return [
|
|
194
|
+
`<b>${TELEGRAM_TOOL_ACTIVITY_ICON}  ${escapeHtml(tool.name)}:</b> <code>${status}</code>`,
|
|
195
|
+
`<blockquote expandable>${escapeHtml(evidence.join("\n\n"))}</blockquote>`,
|
|
196
|
+
].join("\n");
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
export function renderTelegramToolActivityHtml(
|
|
200
|
+
tools: readonly ToolActivity[],
|
|
201
|
+
): string {
|
|
202
|
+
return tools.map(renderToolActivityHtml).join("\n\n");
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
function toolMessageSize(tools: readonly ToolActivity[]): number {
|
|
206
|
+
return renderTelegramToolActivityHtml(tools).length;
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
function draftIdForActivity(activityId: string): number {
|
|
210
|
+
let hash = 2166136261;
|
|
211
|
+
for (const character of activityId) {
|
|
212
|
+
hash ^= character.charCodeAt(0);
|
|
213
|
+
hash = Math.imul(hash, 16777619);
|
|
214
|
+
}
|
|
215
|
+
return (hash >>> 0) || 1;
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
export interface TelegramActivityVerbosityRuntime {
|
|
219
|
+
accept: (event: TelegramActivityEvent) => void;
|
|
220
|
+
reset: () => void;
|
|
221
|
+
stop: () => void;
|
|
222
|
+
waitForIdle: () => Promise<void>;
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
export function createTelegramActivityVerbosityRuntime<TAuthority>(deps: {
|
|
226
|
+
isVerbose: () => boolean;
|
|
227
|
+
resolveTarget: (event: TelegramActivityEvent) => TelegramTarget | undefined;
|
|
228
|
+
captureAuthority: () => TAuthority;
|
|
229
|
+
isAuthorityActive: (authority: TAuthority) => boolean;
|
|
230
|
+
sendMessage: (body: TelegramSendMessageBody) => Promise<TelegramSentMessage>;
|
|
231
|
+
sendRichMessageDraft: (
|
|
232
|
+
body: TelegramSendRichMessageDraftBody,
|
|
233
|
+
) => Promise<boolean>;
|
|
234
|
+
editMessageText: (
|
|
235
|
+
body: TelegramEditMessageTextBody,
|
|
236
|
+
) => Promise<"edited" | "unchanged">;
|
|
237
|
+
recordFailure?: (
|
|
238
|
+
operation: "reasoning-draft" | "tool-send" | "tool-edit",
|
|
239
|
+
event: TelegramActivityEvent,
|
|
240
|
+
error: unknown,
|
|
241
|
+
) => void;
|
|
242
|
+
}): TelegramActivityVerbosityRuntime {
|
|
243
|
+
let active = true;
|
|
244
|
+
let generation = 0;
|
|
245
|
+
let tail = Promise.resolve();
|
|
246
|
+
let activityId: string | undefined;
|
|
247
|
+
let authority: TAuthority | undefined;
|
|
248
|
+
let target: TelegramTarget | undefined;
|
|
249
|
+
let reasoningBuffer = "";
|
|
250
|
+
let reasoningChars = 0;
|
|
251
|
+
let reasoningDraftFrames = 0;
|
|
252
|
+
let lastReasoningDraftChars = 0;
|
|
253
|
+
let toolMessage: ToolMessage | undefined;
|
|
254
|
+
const tools = new Map<string, ToolActivity>();
|
|
255
|
+
const toolOrder: string[] = [];
|
|
256
|
+
|
|
257
|
+
const clearActivity = () => {
|
|
258
|
+
activityId = undefined;
|
|
259
|
+
authority = undefined;
|
|
260
|
+
target = undefined;
|
|
261
|
+
reasoningBuffer = "";
|
|
262
|
+
reasoningChars = 0;
|
|
263
|
+
reasoningDraftFrames = 0;
|
|
264
|
+
lastReasoningDraftChars = 0;
|
|
265
|
+
toolMessage = undefined;
|
|
266
|
+
tools.clear();
|
|
267
|
+
toolOrder.length = 0;
|
|
268
|
+
};
|
|
269
|
+
const hasAuthority = (): boolean =>
|
|
270
|
+
authority !== undefined && deps.isAuthorityActive(authority);
|
|
271
|
+
const ensureActivity = (event: TelegramActivityEvent): boolean => {
|
|
272
|
+
if (!deps.isVerbose()) return false;
|
|
273
|
+
if (activityId === event.activityId) return hasAuthority();
|
|
274
|
+
clearActivity();
|
|
275
|
+
const resolvedTarget = deps.resolveTarget(event);
|
|
276
|
+
if (!resolvedTarget) return false;
|
|
277
|
+
activityId = event.activityId;
|
|
278
|
+
target = { ...resolvedTarget };
|
|
279
|
+
authority = deps.captureAuthority();
|
|
280
|
+
return hasAuthority();
|
|
281
|
+
};
|
|
282
|
+
const closeToolBatch = () => {
|
|
283
|
+
toolMessage = undefined;
|
|
284
|
+
};
|
|
285
|
+
const sendReasoningDraft = async (
|
|
286
|
+
event: TelegramActivityEvent,
|
|
287
|
+
acceptedGeneration: number,
|
|
288
|
+
) => {
|
|
289
|
+
if (
|
|
290
|
+
generation !== acceptedGeneration ||
|
|
291
|
+
!target ||
|
|
292
|
+
!hasAuthority()
|
|
293
|
+
) {
|
|
294
|
+
return;
|
|
295
|
+
}
|
|
296
|
+
const omitted = reasoningChars - reasoningBuffer.length;
|
|
297
|
+
const text =
|
|
298
|
+
omitted > 0
|
|
299
|
+
? `… [${omitted} earlier chars omitted]\n${reasoningBuffer}`
|
|
300
|
+
: reasoningBuffer;
|
|
301
|
+
try {
|
|
302
|
+
await deps.sendRichMessageDraft({
|
|
303
|
+
chat_id: target.chatId,
|
|
304
|
+
...(target.threadId === undefined
|
|
305
|
+
? {}
|
|
306
|
+
: { message_thread_id: target.threadId }),
|
|
307
|
+
draft_id: draftIdForActivity(event.activityId),
|
|
308
|
+
rich_message: {
|
|
309
|
+
blocks: [
|
|
310
|
+
{
|
|
311
|
+
type: "thinking",
|
|
312
|
+
text: renderReasoningRichText(redactActivityText(text)),
|
|
313
|
+
},
|
|
314
|
+
],
|
|
315
|
+
skip_entity_detection: true,
|
|
316
|
+
},
|
|
317
|
+
});
|
|
318
|
+
if (generation !== acceptedGeneration) return;
|
|
319
|
+
reasoningDraftFrames += 1;
|
|
320
|
+
lastReasoningDraftChars = reasoningChars;
|
|
321
|
+
} catch (error) {
|
|
322
|
+
deps.recordFailure?.("reasoning-draft", event, error);
|
|
323
|
+
}
|
|
324
|
+
};
|
|
325
|
+
const publishTool = async (
|
|
326
|
+
event: TelegramActivityEvent,
|
|
327
|
+
tool: ToolActivity,
|
|
328
|
+
acceptedGeneration: number,
|
|
329
|
+
) => {
|
|
330
|
+
if (
|
|
331
|
+
generation !== acceptedGeneration ||
|
|
332
|
+
!target ||
|
|
333
|
+
!hasAuthority()
|
|
334
|
+
) {
|
|
335
|
+
return;
|
|
336
|
+
}
|
|
337
|
+
const canAppend =
|
|
338
|
+
toolMessage &&
|
|
339
|
+
targetEquals(toolMessage.target, target) &&
|
|
340
|
+
toolMessage.tools.length < TELEGRAM_ACTIVITY_MESSAGE_MAX_TOOLS &&
|
|
341
|
+
toolMessageSize([...toolMessage.tools, tool]) <=
|
|
342
|
+
TELEGRAM_ACTIVITY_MESSAGE_MAX_CHARS;
|
|
343
|
+
try {
|
|
344
|
+
if (canAppend && toolMessage) {
|
|
345
|
+
const nextTools = [...toolMessage.tools, tool];
|
|
346
|
+
await deps.editMessageText({
|
|
347
|
+
chat_id: target.chatId,
|
|
348
|
+
message_id: toolMessage.messageId,
|
|
349
|
+
text: renderTelegramToolActivityHtml(nextTools),
|
|
350
|
+
parse_mode: "HTML",
|
|
351
|
+
});
|
|
352
|
+
if (generation !== acceptedGeneration) return;
|
|
353
|
+
toolMessage.tools = nextTools;
|
|
354
|
+
return;
|
|
355
|
+
}
|
|
356
|
+
const sent = await deps.sendMessage({
|
|
357
|
+
chat_id: target.chatId,
|
|
358
|
+
...(target.threadId === undefined
|
|
359
|
+
? {}
|
|
360
|
+
: { message_thread_id: target.threadId }),
|
|
361
|
+
text: renderTelegramToolActivityHtml([tool]),
|
|
362
|
+
parse_mode: "HTML",
|
|
363
|
+
});
|
|
364
|
+
if (generation !== acceptedGeneration) return;
|
|
365
|
+
toolMessage = {
|
|
366
|
+
messageId: sent.message_id,
|
|
367
|
+
tools: [tool],
|
|
368
|
+
target: { ...target },
|
|
369
|
+
};
|
|
370
|
+
} catch (error) {
|
|
371
|
+
deps.recordFailure?.(canAppend ? "tool-edit" : "tool-send", event, error);
|
|
372
|
+
closeToolBatch();
|
|
373
|
+
}
|
|
374
|
+
};
|
|
375
|
+
const process = async (
|
|
376
|
+
event: TelegramActivityEvent,
|
|
377
|
+
acceptedGeneration: number,
|
|
378
|
+
) => {
|
|
379
|
+
if (!ensureActivity(event)) {
|
|
380
|
+
if (activityId === event.activityId && !deps.isVerbose()) clearActivity();
|
|
381
|
+
return;
|
|
382
|
+
}
|
|
383
|
+
if (
|
|
384
|
+
event.type === "assistant-text-delta" ||
|
|
385
|
+
event.type === "assistant-segment" ||
|
|
386
|
+
event.type === "reasoning-delta" ||
|
|
387
|
+
event.type === "reasoning-end"
|
|
388
|
+
) {
|
|
389
|
+
closeToolBatch();
|
|
390
|
+
}
|
|
391
|
+
if (event.type === "reasoning-delta") {
|
|
392
|
+
reasoningChars += event.delta.length;
|
|
393
|
+
reasoningBuffer = `${reasoningBuffer}${event.delta}`.slice(
|
|
394
|
+
-TELEGRAM_REASONING_BUFFER_MAX_CHARS,
|
|
395
|
+
);
|
|
396
|
+
if (
|
|
397
|
+
reasoningDraftFrames < TELEGRAM_REASONING_DRAFT_MAX_FRAMES &&
|
|
398
|
+
(reasoningDraftFrames === 0 ||
|
|
399
|
+
reasoningChars - lastReasoningDraftChars >= 160)
|
|
400
|
+
) {
|
|
401
|
+
await sendReasoningDraft(event, acceptedGeneration);
|
|
402
|
+
}
|
|
403
|
+
return;
|
|
404
|
+
}
|
|
405
|
+
if (event.type === "reasoning-end") {
|
|
406
|
+
if (
|
|
407
|
+
reasoningChars > lastReasoningDraftChars &&
|
|
408
|
+
reasoningDraftFrames < TELEGRAM_REASONING_DRAFT_MAX_FRAMES
|
|
409
|
+
) {
|
|
410
|
+
await sendReasoningDraft(event, acceptedGeneration);
|
|
411
|
+
}
|
|
412
|
+
reasoningBuffer = "";
|
|
413
|
+
reasoningChars = 0;
|
|
414
|
+
return;
|
|
415
|
+
}
|
|
416
|
+
if (event.type === "tool-start") {
|
|
417
|
+
tools.set(event.toolCallId, {
|
|
418
|
+
id: event.toolCallId,
|
|
419
|
+
name: event.toolName,
|
|
420
|
+
args: serializeActivityValue(event.args),
|
|
421
|
+
updates: [],
|
|
422
|
+
droppedUpdates: 0,
|
|
423
|
+
complete: false,
|
|
424
|
+
});
|
|
425
|
+
toolOrder.push(event.toolCallId);
|
|
426
|
+
return;
|
|
427
|
+
}
|
|
428
|
+
if (event.type === "tool-update") {
|
|
429
|
+
const tool = tools.get(event.toolCallId);
|
|
430
|
+
if (!tool) return;
|
|
431
|
+
tool.updates.push(serializeActivityValue(event.update));
|
|
432
|
+
if (tool.updates.length > TELEGRAM_TOOL_UPDATE_MAX_ENTRIES) {
|
|
433
|
+
tool.updates.shift();
|
|
434
|
+
tool.droppedUpdates += 1;
|
|
435
|
+
}
|
|
436
|
+
return;
|
|
437
|
+
}
|
|
438
|
+
if (event.type === "tool-end") {
|
|
439
|
+
const tool = tools.get(event.toolCallId) ?? {
|
|
440
|
+
id: event.toolCallId,
|
|
441
|
+
name: event.toolName,
|
|
442
|
+
args: serializeActivityValue(undefined),
|
|
443
|
+
updates: [],
|
|
444
|
+
droppedUpdates: 0,
|
|
445
|
+
complete: false,
|
|
446
|
+
};
|
|
447
|
+
if (!tools.has(event.toolCallId)) toolOrder.push(event.toolCallId);
|
|
448
|
+
tool.result = serializeActivityValue(event.result);
|
|
449
|
+
tool.isError = event.isError;
|
|
450
|
+
tool.complete = true;
|
|
451
|
+
tools.set(event.toolCallId, tool);
|
|
452
|
+
while (toolOrder.length > 0) {
|
|
453
|
+
const next = tools.get(toolOrder[0]!);
|
|
454
|
+
if (!next?.complete) break;
|
|
455
|
+
toolOrder.shift();
|
|
456
|
+
tools.delete(next.id);
|
|
457
|
+
await publishTool(event, next, acceptedGeneration);
|
|
458
|
+
if (generation !== acceptedGeneration) return;
|
|
459
|
+
}
|
|
460
|
+
return;
|
|
461
|
+
}
|
|
462
|
+
if (event.type === "agent-end" || event.type === "agent-settled") {
|
|
463
|
+
clearActivity();
|
|
464
|
+
}
|
|
465
|
+
};
|
|
466
|
+
return {
|
|
467
|
+
accept(event) {
|
|
468
|
+
if (!active) return;
|
|
469
|
+
const acceptedGeneration = generation;
|
|
470
|
+
tail = tail
|
|
471
|
+
.then(() => {
|
|
472
|
+
if (!active || generation !== acceptedGeneration) return;
|
|
473
|
+
return process(event, acceptedGeneration);
|
|
474
|
+
})
|
|
475
|
+
.catch((error) => {
|
|
476
|
+
deps.recordFailure?.("tool-send", event, error);
|
|
477
|
+
});
|
|
478
|
+
},
|
|
479
|
+
reset() {
|
|
480
|
+
generation += 1;
|
|
481
|
+
clearActivity();
|
|
482
|
+
tail = Promise.resolve();
|
|
483
|
+
},
|
|
484
|
+
stop() {
|
|
485
|
+
active = false;
|
|
486
|
+
generation += 1;
|
|
487
|
+
clearActivity();
|
|
488
|
+
tail = Promise.resolve();
|
|
489
|
+
},
|
|
490
|
+
waitForIdle() {
|
|
491
|
+
return tail;
|
|
492
|
+
},
|
|
493
|
+
};
|
|
494
|
+
}
|
package/lib/bindings.ts
CHANGED
|
@@ -5,6 +5,7 @@
|
|
|
5
5
|
*/
|
|
6
6
|
|
|
7
7
|
import * as Activity from "./activity.ts";
|
|
8
|
+
import type { TelegramActivityVerbosityRuntime } from "./activity-verbosity.ts";
|
|
8
9
|
import * as CommandTemplates from "./command-templates.ts";
|
|
9
10
|
import * as Commands from "./commands.ts";
|
|
10
11
|
import * as Config from "./config.ts";
|
|
@@ -61,19 +62,24 @@ export function createTelegramAssistantOutputBindingRuntime<
|
|
|
61
62
|
sender: Parameters<
|
|
62
63
|
typeof OutboundHandlers.createTelegramAssistantOutputSender<TTransportStamp>
|
|
63
64
|
>[0];
|
|
65
|
+
waitForActivityIdle?: () => Promise<void>;
|
|
64
66
|
recordRuntimeEvent: TelegramRuntimeEventRecorder;
|
|
65
67
|
}): TelegramAssistantOutputBindingRuntime<TTransportStamp> {
|
|
66
68
|
const authority = Routing.createTelegramAssistantOutputAuthorityRuntime(
|
|
67
69
|
deps.authority,
|
|
68
70
|
);
|
|
69
|
-
const
|
|
71
|
+
const sendOutput =
|
|
70
72
|
OutboundHandlers.createTelegramAssistantOutputSender<TTransportStamp>(
|
|
71
73
|
deps.sender,
|
|
72
74
|
);
|
|
73
75
|
const runtime = Activity.createTelegramAssistantOutputRuntime({
|
|
74
76
|
isEnabled: deps.isEnabled,
|
|
75
77
|
...authority,
|
|
76
|
-
send,
|
|
78
|
+
async send(event, authority, isAuthorityActive) {
|
|
79
|
+
await deps.waitForActivityIdle?.();
|
|
80
|
+
if (!isAuthorityActive()) return;
|
|
81
|
+
await sendOutput(event, authority, isAuthorityActive);
|
|
82
|
+
},
|
|
77
83
|
recordFailure(event, error) {
|
|
78
84
|
deps.recordRuntimeEvent("proactive-push", error, {
|
|
79
85
|
activityId: event.activityId,
|
|
@@ -278,6 +284,7 @@ export function registerTelegramCommandsAndTools({
|
|
|
278
284
|
interface TelegramLifecycleBindingDeps {
|
|
279
285
|
pi: Pi.ExtensionAPI;
|
|
280
286
|
activityRuntime: Activity.TelegramActivityRuntime;
|
|
287
|
+
activityVerbosityRuntime?: TelegramActivityVerbosityRuntime;
|
|
281
288
|
assistantOutputRuntime: Pick<
|
|
282
289
|
Activity.TelegramAssistantOutputRuntime,
|
|
283
290
|
"start" | "stop"
|
|
@@ -363,6 +370,7 @@ interface TelegramLifecycleBindingDeps {
|
|
|
363
370
|
export function registerTelegramLifecycleRuntimeHooks({
|
|
364
371
|
pi,
|
|
365
372
|
activityRuntime,
|
|
373
|
+
activityVerbosityRuntime,
|
|
366
374
|
assistantOutputRuntime,
|
|
367
375
|
sessionLifecycleRuntime,
|
|
368
376
|
configStore,
|
|
@@ -556,6 +564,7 @@ export function registerTelegramLifecycleRuntimeHooks({
|
|
|
556
564
|
isSessionActive: isSessionContextActive,
|
|
557
565
|
isTurnTransportActive,
|
|
558
566
|
waitForTypingIdle: typing.waitForIdle,
|
|
567
|
+
waitForActivityIdle: activityVerbosityRuntime?.waitForIdle,
|
|
559
568
|
dispatchNextQueuedTelegramTurn,
|
|
560
569
|
requestDeferredDispatchNextQueuedTelegramTurn:
|
|
561
570
|
deferredQueueDispatchRuntime.request,
|
|
@@ -638,6 +647,7 @@ export function registerTelegramLifecycleRuntimeHooks({
|
|
|
638
647
|
previewRuntime.invalidate();
|
|
639
648
|
assistantOutputRuntime.start();
|
|
640
649
|
activityRuntime.onSessionStart?.();
|
|
650
|
+
activityVerbosityRuntime?.reset();
|
|
641
651
|
modelContextAvailabilityRuntime.reconcile();
|
|
642
652
|
await sessionLifecycleRuntime.onSessionStart(event, ctx);
|
|
643
653
|
},
|
|
@@ -645,6 +655,7 @@ export function registerTelegramLifecycleRuntimeHooks({
|
|
|
645
655
|
if (!isSessionContextActive(ctx)) return;
|
|
646
656
|
agentLifecycleHooks.clearRetainedAgentEnd();
|
|
647
657
|
activityRuntime.onSessionShutdown();
|
|
658
|
+
activityVerbosityRuntime?.reset();
|
|
648
659
|
assistantOutputRuntime.stop();
|
|
649
660
|
compactionObserver.onSessionShutdown();
|
|
650
661
|
if (event.reason === "quit" && disconnectOnQuit) {
|