@segmentstream/agent 0.5.3 → 0.6.3
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/README.md +140 -30
- package/dist/extension/config.d.ts +4 -0
- package/dist/extension/config.mjs +12 -3
- package/dist/extension/connections/ontology.mjs +2 -2
- package/dist/extension/instructions.md +10 -9
- package/dist/extension/lib/attachment-tools.mjs +1 -3
- package/dist/extension/lib/chat-post-confirmation.d.ts +23 -0
- package/dist/extension/lib/chat-post-confirmation.mjs +61 -0
- package/dist/extension/lib/confirmation-description.d.ts +2 -0
- package/dist/extension/lib/confirmations.d.ts +5 -0
- package/dist/extension/lib/confirmations.mjs +25 -5
- package/dist/extension/lib/conversation-runtime.mjs +18 -6
- package/dist/extension/lib/conversations.d.ts +12 -1
- package/dist/extension/lib/conversations.mjs +4 -3
- package/dist/extension/lib/ontology-connection.d.ts +4 -0
- package/dist/extension/lib/ontology-connection.mjs +46 -3
- package/dist/extension/lib/ontology-tools.d.ts +1 -0
- package/dist/extension/lib/ontology-tools.mjs +27 -2
- package/dist/extension/lib/telegram/adapter.d.ts +8 -2
- package/dist/extension/lib/telegram/adapter.mjs +52 -28
- package/dist/extension/lib/telegram/audience.d.ts +26 -0
- package/dist/extension/lib/telegram/audience.mjs +80 -0
- package/dist/extension/lib/telegram/chat-post-card.d.ts +15 -0
- package/dist/extension/lib/telegram/chat-post-card.mjs +65 -0
- package/dist/extension/lib/telegram/confirmations.d.ts +3 -2
- package/dist/extension/lib/telegram/confirmations.mjs +140 -21
- package/dist/extension/lib/telegram/content.d.ts +29 -0
- package/dist/extension/lib/telegram/content.mjs +161 -0
- package/dist/extension/lib/telegram/conversations.d.ts +2 -2
- package/dist/extension/lib/telegram/conversations.mjs +16 -8
- package/dist/extension/lib/telegram/customer-text.mjs +4 -3
- package/dist/extension/lib/telegram/files.d.ts +4 -0
- package/dist/extension/lib/telegram/files.mjs +33 -2
- package/dist/extension/lib/telegram/health.mjs +1 -1
- package/dist/extension/lib/telegram/images.d.ts +21 -0
- package/dist/extension/lib/telegram/images.mjs +47 -0
- package/dist/extension/lib/telegram/inbound-request.d.ts +6 -1
- package/dist/extension/lib/telegram/inbound-request.mjs +11 -1
- package/dist/extension/lib/telegram/inbound.d.ts +10 -5
- package/dist/extension/lib/telegram/inbound.mjs +82 -22
- package/dist/extension/lib/telegram/media-policy.d.ts +15 -0
- package/dist/extension/lib/telegram/media-policy.mjs +72 -0
- package/dist/extension/lib/telegram/observed.d.ts +8 -0
- package/dist/extension/lib/telegram/observed.mjs +10 -5
- package/dist/extension/lib/telegram/router.d.ts +10 -0
- package/dist/extension/lib/telegram/router.mjs +39 -32
- package/dist/extension/lib/telegram/runtime-request.d.ts +8 -0
- package/dist/extension/lib/telegram/runtime-request.mjs +36 -0
- package/dist/extension/lib/telegram/silent.d.ts +2 -0
- package/dist/extension/lib/telegram/silent.mjs +8 -2
- package/dist/extension/lib/telegram/voice.d.ts +1 -0
- package/dist/extension/lib/telegram/voice.mjs +12 -6
- package/package.json +1 -1
- package/dist/extension/lib/telegram/requests.d.ts +0 -80
- package/dist/extension/lib/telegram/requests.mjs +0 -186
|
@@ -5,7 +5,10 @@ const __filename = __eveFileURLToPath(import.meta.url);
|
|
|
5
5
|
__eveDirname(__filename);
|
|
6
6
|
__eveCreateRequire(import.meta.url);
|
|
7
7
|
import { conversationLanguage, memberIdOf, telegramPrincipal } from "./principal.mjs";
|
|
8
|
+
import { chatPostOperationId, isChatPost } from "../chat-post-confirmation.mjs";
|
|
9
|
+
import { chatPostCard, chatPostCopy, chatPostReceipt, withChatPostCard } from "./chat-post-card.mjs";
|
|
8
10
|
import { confirmations } from "../confirmations.mjs";
|
|
11
|
+
import { conversationEvent } from "../conversations.mjs";
|
|
9
12
|
import { currentInboundRequest } from "./inbound-request.mjs";
|
|
10
13
|
import { currentLocale, failureText, reportTelegramError } from "./customer-text.mjs";
|
|
11
14
|
import { answerMarker, isPendingQuestion, pendingQuestions, questionKey } from "../questions.mjs";
|
|
@@ -18,7 +21,8 @@ function shortHash(value) {
|
|
|
18
21
|
return createHash("sha256").update(value).digest("hex").slice(0, 20);
|
|
19
22
|
}
|
|
20
23
|
function confirmationCallback(requestId, memberId, decision) {
|
|
21
|
-
|
|
24
|
+
const choice = decision === "approve" ? "y" : decision === "edit" ? "e" : "n";
|
|
25
|
+
return `${PREFIX}${shortHash(requestId)}:${shortHash(memberId)}:${choice}`;
|
|
22
26
|
}
|
|
23
27
|
function object(value) {
|
|
24
28
|
return typeof value === "object" && value !== null ? value : {};
|
|
@@ -34,7 +38,9 @@ function openConfirmations(events) {
|
|
|
34
38
|
if (request.kind === "tool-approval" && typeof request.requestId === "string" && typeof action.callId === "string") open.set(request.requestId, {
|
|
35
39
|
requestId: request.requestId,
|
|
36
40
|
callId: action.callId,
|
|
37
|
-
at: Date.parse(event.meta.at)
|
|
41
|
+
at: Date.parse(event.meta.at),
|
|
42
|
+
toolName: String(action.toolName ?? ""),
|
|
43
|
+
input: object(action.input)
|
|
38
44
|
});
|
|
39
45
|
}
|
|
40
46
|
if (event.type === "input.resolved" && Array.isArray(data.resolutions)) for (const item of data.resolutions) {
|
|
@@ -49,6 +55,10 @@ function questionFromHistory(events, owner) {
|
|
|
49
55
|
for (const event of events) {
|
|
50
56
|
const data = object(event.data);
|
|
51
57
|
if (event.type === "context.cleared") pending.clear();
|
|
58
|
+
if (event.type === "input.resolved" && Array.isArray(data.resolutions)) for (const resolution of data.resolutions) {
|
|
59
|
+
const question = chatPostEditQuestion(object(resolution));
|
|
60
|
+
if (question !== void 0) pending.set(questionKey(question), question);
|
|
61
|
+
}
|
|
52
62
|
if (event.type === "action.result") {
|
|
53
63
|
const result = object(data.result);
|
|
54
64
|
const candidate = object(result.output).pendingQuestion;
|
|
@@ -60,6 +70,16 @@ function questionFromHistory(events, owner) {
|
|
|
60
70
|
}
|
|
61
71
|
return owner === void 0 ? [...pending.values()].at(-1) : pending.get(questionKey(owner));
|
|
62
72
|
}
|
|
73
|
+
function chatPostEditQuestion(resolution) {
|
|
74
|
+
const text = object(resolution.response).text;
|
|
75
|
+
if (resolution.outcome !== "cancelled" || typeof text !== "string") return;
|
|
76
|
+
try {
|
|
77
|
+
const question = object(JSON.parse(text)).pendingChatPostEdit;
|
|
78
|
+
return isPendingQuestion(question) ? question : void 0;
|
|
79
|
+
} catch {
|
|
80
|
+
return;
|
|
81
|
+
}
|
|
82
|
+
}
|
|
63
83
|
function questionAddress(owner) {
|
|
64
84
|
return formatAddress({
|
|
65
85
|
...owner,
|
|
@@ -111,12 +131,17 @@ async function handleConfirmationCallback(input) {
|
|
|
111
131
|
}), input.scope);
|
|
112
132
|
const session = await (currentInboundRequest()?.sessions)?.resolveSession(address);
|
|
113
133
|
const prompt = session === void 0 ? void 0 : openConfirmations(await input.read(session.id)).find((candidate) => shortHash(candidate.requestId) === requestHash);
|
|
114
|
-
if (prompt === void 0 ||
|
|
134
|
+
if (prompt === void 0 || ![
|
|
135
|
+
"y",
|
|
136
|
+
"n",
|
|
137
|
+
"e"
|
|
138
|
+
].includes(choice ?? "") || choice === "e" && !isChatPost(prompt.toolName)) {
|
|
115
139
|
await notice("This confirmation is no longer pending.");
|
|
116
140
|
return;
|
|
117
141
|
}
|
|
118
142
|
const expired = !Number.isFinite(prompt.at) || prompt.at + 6e5 <= (input.now ?? Date.now());
|
|
119
|
-
const decision = expired || choice
|
|
143
|
+
const decision = expired || choice !== "y" ? "cancel" : "approve";
|
|
144
|
+
const edit = !expired && choice === "e";
|
|
120
145
|
const principal = telegramPrincipal({
|
|
121
146
|
member: lookup.member,
|
|
122
147
|
message: {
|
|
@@ -130,17 +155,58 @@ async function handleConfirmationCallback(input) {
|
|
|
130
155
|
});
|
|
131
156
|
const source = currentInboundRequest()?.respond;
|
|
132
157
|
if (source === void 0) throw new Error("Confirmation callback has no session continuation.");
|
|
158
|
+
const editQuestion = edit ? {
|
|
159
|
+
id: `edit-${prompt.callId}`,
|
|
160
|
+
memberId: lookup.member.id,
|
|
161
|
+
chatId: query.message.chat.id,
|
|
162
|
+
messageThreadId: String(query.message.messageThreadId ?? ""),
|
|
163
|
+
address,
|
|
164
|
+
question: chatPostCopy(language).changes,
|
|
165
|
+
task: `The previous chat post was cancelled for editing. Apply the member's changes to this draft, then make a NEW chat_post_message call and confirmation. Never reuse the cancelled call. Previous input: ${JSON.stringify(prompt.input)}`
|
|
166
|
+
} : void 0;
|
|
133
167
|
await source(address, prompt.requestId, decision, {
|
|
134
168
|
...principal,
|
|
135
169
|
attributes: {
|
|
136
170
|
...principal.attributes,
|
|
137
171
|
confirmationRequestId: prompt.requestId,
|
|
138
|
-
confirmationDecision: decision
|
|
172
|
+
confirmationDecision: decision,
|
|
173
|
+
...expired ? { confirmationReason: "expired" } : edit ? { confirmationReason: "edit" } : {}
|
|
139
174
|
}
|
|
140
|
-
});
|
|
175
|
+
}, editQuestion === void 0 ? void 0 : JSON.stringify({ pendingChatPostEdit: editQuestion }));
|
|
141
176
|
await notice(expired ? "This confirmation expired; nothing executed." : decision === "cancel" ? "Cancellation received." : "Confirmation received.");
|
|
142
177
|
}
|
|
143
|
-
function telegramConfirmationEvents(render = async (description, locale) => fallbackConfirmation(description, locale)) {
|
|
178
|
+
function telegramConfirmationEvents(render = async (description, locale) => fallbackConfirmation(description, locale), archive) {
|
|
179
|
+
const record = async (callId, phase, text, status, ctx) => {
|
|
180
|
+
const saved = confirmations.get()[callId];
|
|
181
|
+
if (archive === void 0 || saved?.description?.chatPost === void 0 || saved.source === void 0) return;
|
|
182
|
+
const operationId = chatPostOperationId(ctx.session.id, callId);
|
|
183
|
+
const source = saved.source;
|
|
184
|
+
const append = () => archive.append(conversationEvent({
|
|
185
|
+
eventId: `chat-post:${operationId}:${phase}`,
|
|
186
|
+
provider: "telegram",
|
|
187
|
+
chatId: source.chatId,
|
|
188
|
+
chatType: source.chatId.startsWith("-") ? "group" : "private",
|
|
189
|
+
threadId: source.threadId || null,
|
|
190
|
+
participantIds: [saved.memberId],
|
|
191
|
+
kind: "tool_result",
|
|
192
|
+
operationId,
|
|
193
|
+
toolCallId: operationId,
|
|
194
|
+
toolName: saved.action,
|
|
195
|
+
sessionId: ctx.session.id,
|
|
196
|
+
text,
|
|
197
|
+
status,
|
|
198
|
+
reason: phase
|
|
199
|
+
}));
|
|
200
|
+
if (phase === "preview") await append();
|
|
201
|
+
else try {
|
|
202
|
+
await append();
|
|
203
|
+
} catch (error) {
|
|
204
|
+
reportTelegramError("chat_post_source_archive_failed", error, {
|
|
205
|
+
operationId,
|
|
206
|
+
phase
|
|
207
|
+
});
|
|
208
|
+
}
|
|
209
|
+
};
|
|
144
210
|
return {
|
|
145
211
|
"context.cleared"(_data, channel) {
|
|
146
212
|
confirmations.update(() => ({}));
|
|
@@ -176,18 +242,30 @@ function telegramConfirmationEvents(render = async (description, locale) => fall
|
|
|
176
242
|
}
|
|
177
243
|
const memberId = confirmations.get()[request.action.callId]?.memberId ?? memberIdOf(ctx.session.auth.current);
|
|
178
244
|
if (memberId === void 0) throw new Error("Confirmation requires a member.");
|
|
179
|
-
const
|
|
245
|
+
const description = confirmations.get()[request.action.callId]?.description;
|
|
246
|
+
const exactPost = isChatPost(request.action.toolName);
|
|
247
|
+
if (exactPost && description?.chatPost === void 0) throw new Error("An exact chat post cannot show a generic confirmation.");
|
|
248
|
+
const copy = description?.chatPost === void 0 ? await render(description, confirmationLocale(ctx.session.auth.current)) : chatPostCard(description.chatPost, confirmationLocale(ctx.session.auth.current));
|
|
180
249
|
const text = copy.text;
|
|
181
|
-
|
|
250
|
+
await record(request.action.callId, "preview", text, "requested", ctx);
|
|
251
|
+
const send = () => channel.telegram.post({
|
|
182
252
|
text,
|
|
183
|
-
reply_markup: { inline_keyboard: [[
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
253
|
+
reply_markup: { inline_keyboard: [[
|
|
254
|
+
{
|
|
255
|
+
text: copy.confirm,
|
|
256
|
+
callback_data: confirmationCallback(request.requestId, memberId, "approve")
|
|
257
|
+
},
|
|
258
|
+
...exactPost ? [{
|
|
259
|
+
text: chatPostCopy(confirmationLocale(ctx.session.auth.current)).edit,
|
|
260
|
+
callback_data: confirmationCallback(request.requestId, memberId, "edit")
|
|
261
|
+
}] : [],
|
|
262
|
+
{
|
|
263
|
+
text: copy.cancel,
|
|
264
|
+
callback_data: confirmationCallback(request.requestId, memberId, "cancel")
|
|
265
|
+
}
|
|
266
|
+
]] }
|
|
190
267
|
});
|
|
268
|
+
const sent = await (exactPost ? withChatPostCard(text, send) : send());
|
|
191
269
|
const state = channel.state;
|
|
192
270
|
state.confirmationMessages = {
|
|
193
271
|
...state.confirmationMessages,
|
|
@@ -196,7 +274,8 @@ function telegramConfirmationEvents(render = async (description, locale) => fall
|
|
|
196
274
|
messageId: sent.id,
|
|
197
275
|
text,
|
|
198
276
|
copy,
|
|
199
|
-
locale: confirmationLocale(ctx.session.auth.current)
|
|
277
|
+
locale: confirmationLocale(ctx.session.auth.current),
|
|
278
|
+
exactPost
|
|
200
279
|
}
|
|
201
280
|
};
|
|
202
281
|
}
|
|
@@ -221,12 +300,39 @@ function telegramConfirmationEvents(render = async (description, locale) => fall
|
|
|
221
300
|
}
|
|
222
301
|
const [callId, message] = entry;
|
|
223
302
|
const copy = message.copy ?? confirmationCopy(message.locale ?? confirmationLocale(ctx.session.auth.current, currentLocale()));
|
|
224
|
-
const
|
|
225
|
-
|
|
303
|
+
const reason = confirmations.get()[callId]?.cancellationReason;
|
|
304
|
+
const status = resolution.outcome === "approved" ? copy.executing : reason === "expired" ? chatPostCopy(message.locale ?? "en").expired : copy.cancelled;
|
|
305
|
+
const text = message.exactPost ? `${message.text}\n\n${status}` : status;
|
|
306
|
+
const edit = () => channel.telegram.editMessageText({
|
|
226
307
|
messageId: message.messageId,
|
|
227
308
|
replyMarkup: { inline_keyboard: [] },
|
|
228
309
|
text
|
|
229
310
|
});
|
|
311
|
+
await (message.exactPost ? withChatPostCard(text, edit) : edit());
|
|
312
|
+
await record(callId, reason ?? resolution.outcome, text, resolution.outcome === "approved" ? "completed" : "cancelled", ctx);
|
|
313
|
+
const question = chatPostEditQuestion(resolution);
|
|
314
|
+
if (message.exactPost && reason === "edit" && question !== void 0) {
|
|
315
|
+
pendingQuestions.update((all) => ({
|
|
316
|
+
...all,
|
|
317
|
+
[questionKey(question)]: question
|
|
318
|
+
}));
|
|
319
|
+
state.pendingQuestions = {
|
|
320
|
+
...state.pendingQuestions,
|
|
321
|
+
[questionKey(question)]: question
|
|
322
|
+
};
|
|
323
|
+
channel.continuation?.alias(questionAddress(question));
|
|
324
|
+
channel.continuation?.alias(formatAddress({
|
|
325
|
+
...question,
|
|
326
|
+
conversationId: ""
|
|
327
|
+
}));
|
|
328
|
+
await channel.telegram.post({
|
|
329
|
+
text: question.question,
|
|
330
|
+
reply_markup: {
|
|
331
|
+
force_reply: true,
|
|
332
|
+
selective: true
|
|
333
|
+
}
|
|
334
|
+
});
|
|
335
|
+
}
|
|
230
336
|
if (resolution.outcome !== "approved") {
|
|
231
337
|
delete state.confirmationMessages?.[callId];
|
|
232
338
|
confirmations.update((all) => ({
|
|
@@ -239,7 +345,7 @@ function telegramConfirmationEvents(render = async (description, locale) => fall
|
|
|
239
345
|
}
|
|
240
346
|
}
|
|
241
347
|
},
|
|
242
|
-
async "action.result"(data, channel) {
|
|
348
|
+
async "action.result"(data, channel, ctx) {
|
|
243
349
|
const state = channel.state;
|
|
244
350
|
if (data.result.kind === "tool-result") {
|
|
245
351
|
const question = object(data.result.output).pendingQuestion;
|
|
@@ -278,7 +384,20 @@ function telegramConfirmationEvents(render = async (description, locale) => fall
|
|
|
278
384
|
}
|
|
279
385
|
const message = state.confirmationMessages?.[data.result.callId];
|
|
280
386
|
if (message === void 0) return;
|
|
281
|
-
|
|
387
|
+
const failed = data.status !== "completed" || data.result.kind === "tool-result" && (data.result.isError === true || object(data.result.output).isError === true || object(object(data.result.output).validation).ok === false);
|
|
388
|
+
if (message.exactPost) {
|
|
389
|
+
const locale = message.locale ?? currentLocale();
|
|
390
|
+
const receipt = !failed && data.result.kind === "tool-result" ? chatPostReceipt(data.result.output, locale) : void 0;
|
|
391
|
+
const errorId = failed ? reportTelegramError("confirmed_chat_post_failed", data, { callId: data.result.callId }) : void 0;
|
|
392
|
+
const status = receipt ?? (failed ? `${message.copy?.failed ?? "Chat posting failed."} ${errorId}\n${chatPostCopy(locale).uncertain}` : chatPostCopy(locale).uncertain);
|
|
393
|
+
const text = `${message.text}\n\n${status}`;
|
|
394
|
+
await withChatPostCard(text, () => channel.telegram.editMessageText({
|
|
395
|
+
messageId: message.messageId,
|
|
396
|
+
replyMarkup: { inline_keyboard: [] },
|
|
397
|
+
text
|
|
398
|
+
}));
|
|
399
|
+
await record(data.result.callId, "receipt", text, failed ? "failed" : object(data.result.kind === "tool-result" ? data.result.output : void 0).status === "sent" ? "completed" : "unknown", ctx);
|
|
400
|
+
} else if (failed) {
|
|
282
401
|
const errorId = reportTelegramError("confirmed_action_failed", data, { callId: data.result.callId });
|
|
283
402
|
await channel.telegram.editMessageText({
|
|
284
403
|
messageId: message.messageId,
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
/** Telegram entities annotate UTF-16 text ranges; they never contain the missing message body. */
|
|
2
|
+
export interface ContentEntity {
|
|
3
|
+
readonly type: string;
|
|
4
|
+
readonly offset: number;
|
|
5
|
+
readonly length: number;
|
|
6
|
+
readonly user?: Readonly<Record<string, unknown>>;
|
|
7
|
+
readonly username?: string;
|
|
8
|
+
}
|
|
9
|
+
export interface MessageContent {
|
|
10
|
+
readonly text: string;
|
|
11
|
+
readonly entities: ReadonlyArray<ContentEntity>;
|
|
12
|
+
readonly source: "text" | "caption" | "rich_message" | "none";
|
|
13
|
+
readonly truncated: boolean;
|
|
14
|
+
}
|
|
15
|
+
export declare function telegramRecord(value: unknown): Readonly<Record<string, unknown>>;
|
|
16
|
+
/** Text/caption stay verbatim, including forwarded messages; rich bodies need flattening. */
|
|
17
|
+
export declare function messageContent(raw: Readonly<Record<string, unknown>>): MessageContent;
|
|
18
|
+
/** Only shape/size diagnostics: do not retain/log raw webhook bodies, URLs or credential values. */
|
|
19
|
+
export declare function contentShape(raw: Readonly<Record<string, unknown>>, content?: MessageContent): {
|
|
20
|
+
contentSource: "text" | "caption" | "rich_message" | "none";
|
|
21
|
+
textType: "string" | "number" | "bigint" | "boolean" | "symbol" | "undefined" | "object" | "function";
|
|
22
|
+
captionType: "string" | "number" | "bigint" | "boolean" | "symbol" | "undefined" | "object" | "function";
|
|
23
|
+
hasRichMessage: boolean;
|
|
24
|
+
entityCount: number;
|
|
25
|
+
captionEntityCount: number;
|
|
26
|
+
textLength: number;
|
|
27
|
+
truncated: boolean;
|
|
28
|
+
forwarded: boolean;
|
|
29
|
+
};
|
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
import { fileURLToPath as __eveFileURLToPath } from "node:url";
|
|
2
|
+
import { dirname as __eveDirname } from "node:path";
|
|
3
|
+
import { createRequire as __eveCreateRequire } from "node:module";
|
|
4
|
+
const __filename = __eveFileURLToPath(import.meta.url);
|
|
5
|
+
__eveDirname(__filename);
|
|
6
|
+
__eveCreateRequire(import.meta.url);
|
|
7
|
+
function telegramRecord(value) {
|
|
8
|
+
return typeof value === "object" && value !== null && !Array.isArray(value) ? value : {};
|
|
9
|
+
}
|
|
10
|
+
function entities(value, text) {
|
|
11
|
+
return (Array.isArray(value) ? value : []).flatMap((item) => {
|
|
12
|
+
const entity = telegramRecord(item);
|
|
13
|
+
const { offset, length, type } = entity;
|
|
14
|
+
return typeof type === "string" && typeof offset === "number" && Number.isInteger(offset) && offset >= 0 && typeof length === "number" && Number.isInteger(length) && length > 0 && offset + length <= text.length ? [{
|
|
15
|
+
type,
|
|
16
|
+
offset,
|
|
17
|
+
length,
|
|
18
|
+
...entity.user ? { user: telegramRecord(entity.user) } : {}
|
|
19
|
+
}] : [];
|
|
20
|
+
});
|
|
21
|
+
}
|
|
22
|
+
function richContent(value) {
|
|
23
|
+
let text = "";
|
|
24
|
+
let nodes = 0;
|
|
25
|
+
let truncated = false;
|
|
26
|
+
const mentions = [];
|
|
27
|
+
const append = (part) => {
|
|
28
|
+
const remaining = 16384 - text.length;
|
|
29
|
+
truncated ||= part.length > remaining;
|
|
30
|
+
text += part.slice(0, remaining);
|
|
31
|
+
};
|
|
32
|
+
const enter = (depth) => {
|
|
33
|
+
nodes += 1;
|
|
34
|
+
if (nodes > 2048 || depth > 24 || text.length >= 16384) {
|
|
35
|
+
truncated = true;
|
|
36
|
+
return false;
|
|
37
|
+
}
|
|
38
|
+
return true;
|
|
39
|
+
};
|
|
40
|
+
function inline(value, depth) {
|
|
41
|
+
if (value === void 0 || value === null) return;
|
|
42
|
+
if (!enter(depth)) return;
|
|
43
|
+
if (typeof value === "string") {
|
|
44
|
+
append(value);
|
|
45
|
+
return;
|
|
46
|
+
}
|
|
47
|
+
if (Array.isArray(value)) {
|
|
48
|
+
for (const item of value) {
|
|
49
|
+
if (!enter(depth + 1)) break;
|
|
50
|
+
inline(item, depth + 1);
|
|
51
|
+
}
|
|
52
|
+
return;
|
|
53
|
+
}
|
|
54
|
+
const item = telegramRecord(value);
|
|
55
|
+
const offset = text.length;
|
|
56
|
+
if (item.type === "custom_emoji" && typeof item.alternative_text === "string") append(item.alternative_text);
|
|
57
|
+
else if (item.type === "mathematical_expression" && typeof item.expression === "string") append(item.expression);
|
|
58
|
+
else if (item.type === "button") inline(telegramRecord(item.button).text, depth + 1);
|
|
59
|
+
else inline(item.text, depth + 1);
|
|
60
|
+
if ([
|
|
61
|
+
"mention",
|
|
62
|
+
"text_mention",
|
|
63
|
+
"code"
|
|
64
|
+
].includes(String(item.type)) && text.length > offset) mentions.push({
|
|
65
|
+
type: String(item.type),
|
|
66
|
+
offset,
|
|
67
|
+
length: text.length - offset,
|
|
68
|
+
...item.type === "text_mention" ? { user: telegramRecord(item.user) } : {},
|
|
69
|
+
...typeof item.username === "string" ? { username: item.username } : {}
|
|
70
|
+
});
|
|
71
|
+
}
|
|
72
|
+
function blocks(value, depth) {
|
|
73
|
+
if (!Array.isArray(value) || !enter(depth)) return;
|
|
74
|
+
for (const entry of value) {
|
|
75
|
+
if (!enter(depth + 1)) break;
|
|
76
|
+
const item = telegramRecord(entry);
|
|
77
|
+
const offset = text.length;
|
|
78
|
+
if (text !== "" && !text.endsWith("\n")) append("\n");
|
|
79
|
+
if (item.type === "divider") append("---");
|
|
80
|
+
else if (item.type === "mathematical_expression" && typeof item.expression === "string") append(item.expression);
|
|
81
|
+
else {
|
|
82
|
+
inline(item.text, depth + 1);
|
|
83
|
+
inline(item.title, depth + 1);
|
|
84
|
+
blocks(item.blocks, depth + 1);
|
|
85
|
+
if (Array.isArray(item.items)) for (const entry of item.items) {
|
|
86
|
+
if (!enter(depth + 1)) break;
|
|
87
|
+
const child = telegramRecord(entry);
|
|
88
|
+
if (text !== "" && !text.endsWith("\n")) append("\n");
|
|
89
|
+
if (typeof child.label === "string") append(`${child.label} `);
|
|
90
|
+
blocks(child.blocks, depth + 1);
|
|
91
|
+
}
|
|
92
|
+
if (Array.isArray(item.cells)) for (const row of item.cells) {
|
|
93
|
+
if (!enter(depth + 1)) break;
|
|
94
|
+
if (!Array.isArray(row)) continue;
|
|
95
|
+
if (text !== "" && !text.endsWith("\n")) append("\n");
|
|
96
|
+
for (const [index, cell] of row.entries()) {
|
|
97
|
+
if (!enter(depth + 1)) break;
|
|
98
|
+
if (index > 0) append(" | ");
|
|
99
|
+
inline(telegramRecord(cell).text, depth + 1);
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
if (item.caption !== void 0) {
|
|
103
|
+
if (text !== "" && !text.endsWith("\n")) append("\n");
|
|
104
|
+
const caption = telegramRecord(item.caption);
|
|
105
|
+
inline(caption.text ?? item.caption, depth + 1);
|
|
106
|
+
inline(caption.credit, depth + 1);
|
|
107
|
+
}
|
|
108
|
+
inline(item.credit, depth + 1);
|
|
109
|
+
}
|
|
110
|
+
if ([
|
|
111
|
+
"blockquote",
|
|
112
|
+
"expandable_blockquote",
|
|
113
|
+
"pullquote",
|
|
114
|
+
"pre"
|
|
115
|
+
].includes(String(item.type)) && text.length > offset) mentions.push({
|
|
116
|
+
type: "blockquote",
|
|
117
|
+
offset,
|
|
118
|
+
length: text.length - offset
|
|
119
|
+
});
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
blocks(telegramRecord(value).blocks, 0);
|
|
123
|
+
return {
|
|
124
|
+
text,
|
|
125
|
+
entities: mentions,
|
|
126
|
+
source: "rich_message",
|
|
127
|
+
truncated
|
|
128
|
+
};
|
|
129
|
+
}
|
|
130
|
+
function messageContent(raw) {
|
|
131
|
+
for (const source of ["text", "caption"]) {
|
|
132
|
+
const text = raw[source];
|
|
133
|
+
if (typeof text === "string" && text !== "") return {
|
|
134
|
+
text,
|
|
135
|
+
entities: entities(raw[source === "text" ? "entities" : "caption_entities"], text),
|
|
136
|
+
source,
|
|
137
|
+
truncated: false
|
|
138
|
+
};
|
|
139
|
+
}
|
|
140
|
+
if (raw.rich_message !== void 0) return richContent(raw.rich_message);
|
|
141
|
+
return {
|
|
142
|
+
text: "",
|
|
143
|
+
entities: [],
|
|
144
|
+
source: "none",
|
|
145
|
+
truncated: false
|
|
146
|
+
};
|
|
147
|
+
}
|
|
148
|
+
function contentShape(raw, content = messageContent(raw)) {
|
|
149
|
+
return {
|
|
150
|
+
contentSource: content.source,
|
|
151
|
+
textType: typeof raw.text,
|
|
152
|
+
captionType: typeof raw.caption,
|
|
153
|
+
hasRichMessage: raw.rich_message !== void 0,
|
|
154
|
+
entityCount: Array.isArray(raw.entities) ? raw.entities.length : 0,
|
|
155
|
+
captionEntityCount: Array.isArray(raw.caption_entities) ? raw.caption_entities.length : 0,
|
|
156
|
+
textLength: content.text.length,
|
|
157
|
+
truncated: content.truncated,
|
|
158
|
+
forwarded: raw.forward_origin !== void 0
|
|
159
|
+
};
|
|
160
|
+
}
|
|
161
|
+
export { contentShape, messageContent, telegramRecord };
|
|
@@ -2,7 +2,7 @@ import type { AgentIdentities } from "../identities.js";
|
|
|
2
2
|
import { type Conversations } from "../conversations.js";
|
|
3
3
|
import type { AgentChats } from "./chats.js";
|
|
4
4
|
import type { ObservedMessages } from "./observed.js";
|
|
5
|
-
import type { AddressingReason, RouteDecider } from "./router.js";
|
|
5
|
+
import type { AddressingReason, RouteDecider, RouteDecision } from "./router.js";
|
|
6
6
|
/** Runs after webhook verification, before eve drops bot/channel/service updates. */
|
|
7
7
|
export declare function captureTelegramWebhook(input: {
|
|
8
8
|
readonly request: Request;
|
|
@@ -14,7 +14,7 @@ export declare function captureTelegramWebhook(input: {
|
|
|
14
14
|
}): Promise<Response>;
|
|
15
15
|
/** Additive inbound hook: the router keeps ownership of the reply policy. */
|
|
16
16
|
export declare function recordGateDecision(reason: string, decision?: "reply" | "silent"): void;
|
|
17
|
-
export declare function recordRouteDecision(route: "reply" | "observe", addressed: boolean | AddressingReason | undefined, policy: string): void;
|
|
17
|
+
export declare function recordRouteDecision(route: "reply" | "observe", addressed: boolean | AddressingReason | undefined, policy: string, audit?: Pick<RouteDecision, "reason" | "note">): void;
|
|
18
18
|
export declare function captureRouteDecider(decide: RouteDecider): RouteDecider;
|
|
19
19
|
/** Captures message sends and button/status edits, including pairing and proactive replies. */
|
|
20
20
|
export declare function captureTelegramFetch(fetchImpl: typeof fetch, archive: Conversations, identities: () => AgentIdentities): typeof fetch;
|
|
@@ -4,11 +4,12 @@ import { createRequire as __eveCreateRequire } from "node:module";
|
|
|
4
4
|
const __filename = __eveFileURLToPath(import.meta.url);
|
|
5
5
|
__eveDirname(__filename);
|
|
6
6
|
__eveCreateRequire(import.meta.url);
|
|
7
|
-
import { conversationEvent } from "../conversations.mjs";
|
|
7
|
+
import { collapseConversationEvents, conversationEvent } from "../conversations.mjs";
|
|
8
8
|
import { observedChatKey } from "./observed.mjs";
|
|
9
9
|
import { currentInboundRequest } from "./inbound-request.mjs";
|
|
10
10
|
import { reportMembership } from "./membership.mjs";
|
|
11
11
|
import { currentThreadId } from "./customer-text.mjs";
|
|
12
|
+
import { contentShape, messageContent } from "./content.mjs";
|
|
12
13
|
import { parseTelegramUpdate } from "eve/channels/telegram";
|
|
13
14
|
import { randomUUID } from "node:crypto";
|
|
14
15
|
const captures = new WeakMap();
|
|
@@ -81,7 +82,9 @@ async function captureTelegramWebhook(input) {
|
|
|
81
82
|
const parsed = parseTelegramUpdate({ message: messagePayload });
|
|
82
83
|
if (parsed?.kind !== "message") return input.next();
|
|
83
84
|
const message = parsed.message;
|
|
84
|
-
const
|
|
85
|
+
const content = messageContent(message.raw);
|
|
86
|
+
const time = edited === void 0 ? message.raw.date : message.raw.edit_date ?? message.raw.date;
|
|
87
|
+
const sentAt = typeof time === "number" ? time * 1e3 : Date.now();
|
|
85
88
|
const senderChat = record(message.raw.sender_chat);
|
|
86
89
|
let lookup;
|
|
87
90
|
const event = conversationEvent({
|
|
@@ -96,7 +99,8 @@ async function captureTelegramWebhook(input) {
|
|
|
96
99
|
chatType: message.chat.type === "supergroup" ? "group" : message.chat.type,
|
|
97
100
|
threadId: message.messageThreadId === void 0 ? null : String(message.messageThreadId),
|
|
98
101
|
kind: "inbound",
|
|
99
|
-
text:
|
|
102
|
+
text: content.text || null,
|
|
103
|
+
argsSummary: JSON.stringify(contentShape(message.raw, content)),
|
|
100
104
|
messageId: message.messageId,
|
|
101
105
|
replyToMessageId: message.replyToMessage?.messageId ?? null,
|
|
102
106
|
senderId: id(senderChat.id) ?? message.from?.id ?? null,
|
|
@@ -127,10 +131,11 @@ async function captureTelegramWebhook(input) {
|
|
|
127
131
|
...event.threadId === null ? {} : { threadId: event.threadId },
|
|
128
132
|
limit: 100
|
|
129
133
|
});
|
|
130
|
-
for (const earlier of (input.observed.recent(chat).length === 0 ? history.events : []).filter((item) => item.kind === "inbound" && item.eventId !== event.eventId).slice(0, 30).reverse()) input.observed.add(chat, {
|
|
134
|
+
for (const earlier of (input.observed.recent(chat).length === 0 ? collapseConversationEvents(history.events) : []).filter((item) => item.kind === "inbound" && item.eventId !== event.eventId).slice(0, 30).reverse()) input.observed.add(chat, {
|
|
131
135
|
author: earlier.senderName ?? earlier.senderId ?? "automated post",
|
|
132
136
|
text: earlier.text ?? "[attachment or service message]",
|
|
133
137
|
at: earlier.at,
|
|
138
|
+
...earlier.messageId === null ? {} : { messageId: earlier.messageId },
|
|
134
139
|
...earlier.senderId === null ? {} : { authorId: earlier.senderId }
|
|
135
140
|
});
|
|
136
141
|
} else {
|
|
@@ -141,7 +146,8 @@ async function captureTelegramWebhook(input) {
|
|
|
141
146
|
input.observed.add(chat, {
|
|
142
147
|
author: event.senderName ?? event.senderId ?? "automated post",
|
|
143
148
|
text: event.text ?? "[attachment or service message]",
|
|
144
|
-
at: event.at
|
|
149
|
+
at: event.at,
|
|
150
|
+
messageId: message.messageId
|
|
145
151
|
});
|
|
146
152
|
capture.decision = "silent";
|
|
147
153
|
capture.reason = edited !== void 0 ? "message_edit" : "automated_or_channel_message";
|
|
@@ -172,7 +178,8 @@ async function captureTelegramWebhook(input) {
|
|
|
172
178
|
...event,
|
|
173
179
|
eventId: `${event.eventId}:decision`,
|
|
174
180
|
kind: "decision",
|
|
175
|
-
|
|
181
|
+
argsSummary: null,
|
|
182
|
+
text: capture.note ?? (edited !== void 0 ? "Message edit recorded as context; no agent turn started." : null),
|
|
176
183
|
decision: capture.decision ?? "silent",
|
|
177
184
|
reason: capture.reason ?? (message.raw.left_chat_member ? "member_departure" : message.raw.migrate_to_chat_id || message.raw.migrate_from_chat_id ? "chat_migration" : "unpaired_refused_or_empty_message"),
|
|
178
185
|
attachments: []
|
|
@@ -187,12 +194,13 @@ function recordGateDecision(reason, decision = "silent") {
|
|
|
187
194
|
capture.decision = decision;
|
|
188
195
|
}
|
|
189
196
|
}
|
|
190
|
-
function recordRouteDecision(route, addressed, policy) {
|
|
197
|
+
function recordRouteDecision(route, addressed, policy, audit) {
|
|
191
198
|
const inbound = currentInboundRequest();
|
|
192
199
|
const capture = inbound === void 0 ? void 0 : captures.get(inbound);
|
|
193
200
|
if (capture !== void 0) {
|
|
194
201
|
capture.decision = route === "reply" ? "reply" : "silent";
|
|
195
|
-
capture.reason ??= typeof addressed === "string" ? addressed : addressed ? "addressed_message" : policy === "mention" ? "mention_policy" : `decision_model_${route}
|
|
202
|
+
capture.reason ??= audit?.reason ?? (typeof addressed === "string" ? addressed : addressed ? "addressed_message" : policy === "mention" ? "mention_policy" : `decision_model_${route}`);
|
|
203
|
+
capture.note = audit?.note.slice(0, 500);
|
|
196
204
|
}
|
|
197
205
|
}
|
|
198
206
|
function captureRouteDecider(decide) {
|
|
@@ -4,6 +4,7 @@ import { createRequire as __eveCreateRequire } from "node:module";
|
|
|
4
4
|
const __filename = __eveFileURLToPath(import.meta.url);
|
|
5
5
|
__eveDirname(__filename);
|
|
6
6
|
__eveCreateRequire(import.meta.url);
|
|
7
|
+
import { isExactChatPostCard } from "./chat-post-card.mjs";
|
|
7
8
|
import { redactDiagnostic } from "../diagnostics.mjs";
|
|
8
9
|
import { currentInboundRequest } from "./inbound-request.mjs";
|
|
9
10
|
import { randomBytes } from "node:crypto";
|
|
@@ -47,7 +48,6 @@ const TECHNICAL_PATTERNS = [
|
|
|
47
48
|
/\b[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}\b/iu,
|
|
48
49
|
/\b[0-9a-hjkmnp-tv-z]{26}\b/iu,
|
|
49
50
|
/\b\w+__\w+/u,
|
|
50
|
-
/\b[a-z][a-z0-9]*_[a-z][a-z0-9_]*\b(?!\.[a-z0-9])/iu,
|
|
51
51
|
/\b(?:Serialization|Type|Reference|Syntax|Tool|Provider|API|Workflow|InternalServer)Error\b/u,
|
|
52
52
|
/\bError:\s|(?:^|\n)\s*at\s+\S+|\b(?:stack trace|traceback|SQLSTATE|node_modules)\b/imu,
|
|
53
53
|
/\b(?:duplicate key|ECONN\w+|HTTP [45]\d\d)\b/iu,
|
|
@@ -56,7 +56,8 @@ const TECHNICAL_PATTERNS = [
|
|
|
56
56
|
function isTechnicalText(text) {
|
|
57
57
|
if (/https?:\/\/[^\s/@]+:[^\s/@]+@|[?&](?:token|access_token|api_key|secret|password)=/iu.test(text)) return true;
|
|
58
58
|
const visible = text.replace(/https?:\/\/[^\s)<>]+/giu, "link");
|
|
59
|
-
|
|
59
|
+
const prose = visible.replace(/(?<![\w@])@[a-z][a-z0-9_]{4,31}(?!\w)/giu, "recipient");
|
|
60
|
+
return TECHNICAL_PATTERNS.some((pattern) => pattern.test(visible)) || /\b[a-z][a-z0-9]*_[a-z][a-z0-9_]*\b(?!\.[a-z0-9])/iu.test(prose);
|
|
60
61
|
}
|
|
61
62
|
function redactCredentials(value) {
|
|
62
63
|
if (typeof value === "string") return value.replace(/\bBearer [A-Za-z0-9_.~+/-]+=*/giu, "Bearer [redacted]").replace(/(https?:\/\/)[^/@\s]+:[^/@\s]+@/gu, "$1[redacted]@").replace(/([?&](?:token|access_token|api_key|secret|password)=)[^&#\s]*/giu, "$1[redacted]").replace(/("(?:token|api_key|password|secret)"\s*:\s*)"(?:\\.|[^"\\])*"/giu, "$1\"[redacted]\"");
|
|
@@ -97,7 +98,7 @@ function safeTelegramFetch(fetchImpl) {
|
|
|
97
98
|
const url = typeof input === "string" ? input : input instanceof URL ? input.href : input.url;
|
|
98
99
|
if (!methods.has(url.split("/").at(-1) ?? "") || typeof init?.body !== "string") return fetchImpl(input, init);
|
|
99
100
|
const body = JSON.parse(init.body);
|
|
100
|
-
for (const field of ["text", "caption"]) if (typeof body[field] === "string") body[field] = customerText(body[field]);
|
|
101
|
+
for (const field of ["text", "caption"]) if (typeof body[field] === "string" && !isExactChatPostCard(body[field])) body[field] = customerText(body[field]);
|
|
101
102
|
if (Array.isArray(body.media)) body.media = body.media.map((item) => ({
|
|
102
103
|
...item,
|
|
103
104
|
...typeof item.caption === "string" ? { caption: customerText(item.caption) } : {}
|
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import type { TelegramMessage } from "eve/channels/telegram";
|
|
2
2
|
import { type AgentAttachments } from "../attachments.js";
|
|
3
3
|
import type { StagedFile } from "./inbound-request.js";
|
|
4
|
+
import { type ImageDescriber } from "./images.js";
|
|
5
|
+
import type { MediaPlan } from "./media-policy.js";
|
|
4
6
|
/**
|
|
5
7
|
* Photos and documents a member sends the agent (`docs/agent.md`, "Attachments"). Before the turn
|
|
6
8
|
* starts, each file is downloaded through the connection and stored once as a temporary attachment
|
|
@@ -17,6 +19,8 @@ export declare function stageFiles(input: {
|
|
|
17
19
|
readonly memberId: string;
|
|
18
20
|
readonly attachments: AgentAttachments;
|
|
19
21
|
readonly download: (fileId: string) => Promise<Uint8Array>;
|
|
22
|
+
readonly imagePlan?: MediaPlan;
|
|
23
|
+
readonly describeImage?: ImageDescriber;
|
|
20
24
|
}): Promise<ReadonlyMap<string, StagedFile>>;
|
|
21
25
|
/** The `<attachments>` block that tells the model which attachments the message carries. */
|
|
22
26
|
export declare function attachmentsContext(input: {
|