@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.
Files changed (55) hide show
  1. package/README.md +140 -30
  2. package/dist/extension/config.d.ts +4 -0
  3. package/dist/extension/config.mjs +12 -3
  4. package/dist/extension/connections/ontology.mjs +2 -2
  5. package/dist/extension/instructions.md +10 -9
  6. package/dist/extension/lib/attachment-tools.mjs +1 -3
  7. package/dist/extension/lib/chat-post-confirmation.d.ts +23 -0
  8. package/dist/extension/lib/chat-post-confirmation.mjs +61 -0
  9. package/dist/extension/lib/confirmation-description.d.ts +2 -0
  10. package/dist/extension/lib/confirmations.d.ts +5 -0
  11. package/dist/extension/lib/confirmations.mjs +25 -5
  12. package/dist/extension/lib/conversation-runtime.mjs +18 -6
  13. package/dist/extension/lib/conversations.d.ts +12 -1
  14. package/dist/extension/lib/conversations.mjs +4 -3
  15. package/dist/extension/lib/ontology-connection.d.ts +4 -0
  16. package/dist/extension/lib/ontology-connection.mjs +46 -3
  17. package/dist/extension/lib/ontology-tools.d.ts +1 -0
  18. package/dist/extension/lib/ontology-tools.mjs +27 -2
  19. package/dist/extension/lib/telegram/adapter.d.ts +8 -2
  20. package/dist/extension/lib/telegram/adapter.mjs +52 -28
  21. package/dist/extension/lib/telegram/audience.d.ts +26 -0
  22. package/dist/extension/lib/telegram/audience.mjs +80 -0
  23. package/dist/extension/lib/telegram/chat-post-card.d.ts +15 -0
  24. package/dist/extension/lib/telegram/chat-post-card.mjs +65 -0
  25. package/dist/extension/lib/telegram/confirmations.d.ts +3 -2
  26. package/dist/extension/lib/telegram/confirmations.mjs +140 -21
  27. package/dist/extension/lib/telegram/content.d.ts +29 -0
  28. package/dist/extension/lib/telegram/content.mjs +161 -0
  29. package/dist/extension/lib/telegram/conversations.d.ts +2 -2
  30. package/dist/extension/lib/telegram/conversations.mjs +16 -8
  31. package/dist/extension/lib/telegram/customer-text.mjs +4 -3
  32. package/dist/extension/lib/telegram/files.d.ts +4 -0
  33. package/dist/extension/lib/telegram/files.mjs +33 -2
  34. package/dist/extension/lib/telegram/health.mjs +1 -1
  35. package/dist/extension/lib/telegram/images.d.ts +21 -0
  36. package/dist/extension/lib/telegram/images.mjs +47 -0
  37. package/dist/extension/lib/telegram/inbound-request.d.ts +6 -1
  38. package/dist/extension/lib/telegram/inbound-request.mjs +11 -1
  39. package/dist/extension/lib/telegram/inbound.d.ts +10 -5
  40. package/dist/extension/lib/telegram/inbound.mjs +82 -22
  41. package/dist/extension/lib/telegram/media-policy.d.ts +15 -0
  42. package/dist/extension/lib/telegram/media-policy.mjs +72 -0
  43. package/dist/extension/lib/telegram/observed.d.ts +8 -0
  44. package/dist/extension/lib/telegram/observed.mjs +10 -5
  45. package/dist/extension/lib/telegram/router.d.ts +10 -0
  46. package/dist/extension/lib/telegram/router.mjs +39 -32
  47. package/dist/extension/lib/telegram/runtime-request.d.ts +8 -0
  48. package/dist/extension/lib/telegram/runtime-request.mjs +36 -0
  49. package/dist/extension/lib/telegram/silent.d.ts +2 -0
  50. package/dist/extension/lib/telegram/silent.mjs +8 -2
  51. package/dist/extension/lib/telegram/voice.d.ts +1 -0
  52. package/dist/extension/lib/telegram/voice.mjs +12 -6
  53. package/package.json +1 -1
  54. package/dist/extension/lib/telegram/requests.d.ts +0 -80
  55. 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
- return `${PREFIX}${shortHash(requestId)}:${shortHash(memberId)}:${decision === "approve" ? "y" : "n"}`;
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 || choice !== "y" && choice !== "n") {
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 === "n" ? "cancel" : "approve";
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 copy = await render(confirmations.get()[request.action.callId]?.description, confirmationLocale(ctx.session.auth.current));
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
- const sent = await channel.telegram.post({
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
- text: copy.confirm,
185
- callback_data: confirmationCallback(request.requestId, memberId, "approve")
186
- }, {
187
- text: copy.cancel,
188
- callback_data: confirmationCallback(request.requestId, memberId, "cancel")
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 text = resolution.outcome === "approved" ? copy.executing : copy.cancelled;
225
- await channel.telegram.editMessageText({
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
- if (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)) {
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 sentAt = typeof message.raw.date === "number" ? message.raw.date * 1e3 : Date.now();
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: message.text || message.caption || null,
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
- text: null,
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
- return TECHNICAL_PATTERNS.some((pattern) => pattern.test(visible));
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: {