@segmentstream/agent 0.3.1 → 0.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (37) hide show
  1. package/README.md +66 -3
  2. package/dist/extension/_manifest.json +2 -0
  3. package/dist/extension/hooks/conversations.d.ts +2 -0
  4. package/dist/extension/hooks/conversations.mjs +10 -0
  5. package/dist/extension/instructions/conversations.d.ts +3 -0
  6. package/dist/extension/instructions/conversations.mjs +20 -0
  7. package/dist/extension/instructions.md +11 -0
  8. package/dist/extension/lib/conversation-runtime.d.ts +19 -0
  9. package/dist/extension/lib/conversation-runtime.mjs +104 -0
  10. package/dist/extension/lib/conversations.d.ts +65 -0
  11. package/dist/extension/lib/conversations.mjs +107 -0
  12. package/dist/extension/lib/diagnostics.d.ts +2 -0
  13. package/dist/extension/lib/diagnostics.mjs +24 -0
  14. package/dist/extension/lib/identities.d.ts +2 -0
  15. package/dist/extension/lib/identities.mjs +27 -6
  16. package/dist/extension/lib/telegram/adapter.d.ts +2 -0
  17. package/dist/extension/lib/telegram/adapter.mjs +61 -22
  18. package/dist/extension/lib/telegram/attachments-send.d.ts +40 -0
  19. package/dist/extension/lib/telegram/attachments-send.mjs +269 -0
  20. package/dist/extension/lib/telegram/chats.d.ts +21 -1
  21. package/dist/extension/lib/telegram/chats.mjs +75 -11
  22. package/dist/extension/lib/telegram/confirmations.mjs +1 -1
  23. package/dist/extension/lib/telegram/conversations.d.ts +30 -0
  24. package/dist/extension/lib/telegram/conversations.mjs +298 -0
  25. package/dist/extension/lib/telegram/health.d.ts +12 -0
  26. package/dist/extension/lib/telegram/health.mjs +24 -0
  27. package/dist/extension/lib/telegram/inbound.mjs +13 -4
  28. package/dist/extension/lib/telegram/membership.d.ts +8 -0
  29. package/dist/extension/lib/telegram/membership.mjs +48 -0
  30. package/dist/extension/lib/telegram/proxy-fetch.mjs +4 -7
  31. package/dist/extension/lib/telegram/requests.d.ts +9 -0
  32. package/dist/extension/lib/telegram/requests.mjs +30 -23
  33. package/dist/extension/tools/attachments_send.d.ts +7 -0
  34. package/dist/extension/tools/attachments_send.mjs +59 -0
  35. package/dist/tools/index.d.ts +1 -0
  36. package/dist/tools/index.mjs +2 -1
  37. package/package.json +1 -1
@@ -0,0 +1,298 @@
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
+ import { conversationEvent } from "../conversations.mjs";
8
+ import { observedChatKey } from "./observed.mjs";
9
+ import { currentInboundRequest } from "./inbound-request.mjs";
10
+ import { reportMembership } from "./membership.mjs";
11
+ import { parseTelegramUpdate } from "eve/channels/telegram";
12
+ import { randomUUID } from "node:crypto";
13
+ const captures = new WeakMap();
14
+ const record = (value) => value !== null && typeof value === "object" && !Array.isArray(value) ? value : {};
15
+ const id = (value) => typeof value === "number" || typeof value === "string" ? String(value) : null;
16
+ function attachmentRefs(message) {
17
+ const files = message.attachments.map((file) => ({
18
+ id: file.fileId,
19
+ kind: file.kind,
20
+ ...file.fileName === void 0 ? {} : { filename: file.fileName },
21
+ ...file.mediaType === void 0 ? {} : { contentType: file.mediaType },
22
+ ...file.size === void 0 ? {} : { size: file.size },
23
+ ...file.fileUniqueId === void 0 ? {} : { uniqueId: file.fileUniqueId }
24
+ }));
25
+ for (const kind of [
26
+ "voice",
27
+ "audio",
28
+ "video",
29
+ "video_note",
30
+ "sticker",
31
+ "animation"
32
+ ]) {
33
+ const file = record(message.raw[kind]);
34
+ const fileId = id(file.file_id);
35
+ if (fileId !== null) files.push({
36
+ id: fileId,
37
+ kind,
38
+ ...id(file.file_unique_id) ? { uniqueId: id(file.file_unique_id) } : {},
39
+ ...typeof file.file_size === "number" ? { size: file.file_size } : {},
40
+ ...typeof file.mime_type === "string" ? { contentType: file.mime_type } : {},
41
+ ...typeof file.file_name === "string" ? { filename: file.file_name } : {}
42
+ });
43
+ }
44
+ return files;
45
+ }
46
+ async function participants(message, identities, resolved) {
47
+ if (message.chat.type !== "private" || message.from === void 0 || message.from.isBot) return [];
48
+ try {
49
+ const lookup = await identities.lookup({
50
+ provider: "telegram",
51
+ externalId: message.from.id
52
+ });
53
+ resolved(lookup);
54
+ return lookup.status === "paired" ? [lookup.member.id] : [];
55
+ } catch (error) {
56
+ console.error("[segmentstream] conversation participant lookup failed", { error });
57
+ return [];
58
+ }
59
+ }
60
+ async function captureTelegramWebhook(input) {
61
+ let raw;
62
+ try {
63
+ raw = await input.request.clone().json();
64
+ } catch {
65
+ return input.next();
66
+ }
67
+ const update = record(raw);
68
+ if (input.chats !== void 0 && (update.my_chat_member !== void 0 || update.chat_member !== void 0)) {
69
+ const task = reportMembership({
70
+ update,
71
+ chats: input.chats,
72
+ identities: input.identities
73
+ });
74
+ currentInboundRequest()?.waitUntil?.(task);
75
+ await task;
76
+ return input.next();
77
+ }
78
+ const edited = update.edited_message ?? update.edited_channel_post;
79
+ const messagePayload = edited ?? update.channel_post ?? update.message;
80
+ const parsed = parseTelegramUpdate({ message: messagePayload });
81
+ if (parsed?.kind !== "message") return input.next();
82
+ const message = parsed.message;
83
+ const sentAt = typeof message.raw.date === "number" ? message.raw.date * 1e3 : Date.now();
84
+ const senderChat = record(message.raw.sender_chat);
85
+ let lookup;
86
+ const event = conversationEvent({
87
+ eventId: [
88
+ "telegram",
89
+ message.chat.id,
90
+ message.messageId,
91
+ edited === void 0 ? "message" : `edit:${id(update.update_id) ?? id(message.raw.edit_date)}`
92
+ ].join(":"),
93
+ provider: "telegram",
94
+ chatId: message.chat.id,
95
+ chatType: message.chat.type === "supergroup" ? "group" : message.chat.type,
96
+ threadId: message.messageThreadId === void 0 ? null : String(message.messageThreadId),
97
+ kind: "inbound",
98
+ text: message.text || message.caption || null,
99
+ messageId: message.messageId,
100
+ replyToMessageId: message.replyToMessage?.messageId ?? null,
101
+ senderId: id(senderChat.id) ?? message.from?.id ?? null,
102
+ senderName: typeof senderChat.title === "string" ? senderChat.title : message.from?.username ?? message.from?.firstName ?? null,
103
+ senderIsBot: message.from?.isBot ?? false,
104
+ participantIds: await participants(message, input.identities, (result) => {
105
+ lookup = result;
106
+ }),
107
+ attachments: attachmentRefs(message),
108
+ at: new Date(sentAt).toISOString()
109
+ });
110
+ await input.archive.append(event);
111
+ const capture = {
112
+ event,
113
+ message,
114
+ ...lookup === void 0 ? {} : { lookup }
115
+ };
116
+ const inbound = currentInboundRequest();
117
+ if (inbound !== void 0) captures.set(inbound, capture);
118
+ const chat = observedChatKey({
119
+ chatId: message.chat.id,
120
+ ...message.messageThreadId === void 0 ? {} : { messageThreadId: message.messageThreadId }
121
+ });
122
+ if (!(message.from === void 0 || message.from.isBot || message.chat.type === "channel") && edited === void 0) {
123
+ const history = await input.archive.context({
124
+ provider: "telegram",
125
+ chatId: message.chat.id,
126
+ ...event.threadId === null ? {} : { threadId: event.threadId },
127
+ limit: 100
128
+ });
129
+ 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, {
130
+ author: earlier.senderName ?? earlier.senderId ?? "automated post",
131
+ text: earlier.text ?? "[attachment or service message]",
132
+ at: earlier.at,
133
+ ...earlier.senderId === null ? {} : { authorId: earlier.senderId }
134
+ });
135
+ } else {
136
+ if (message.chat.type === "group" || message.chat.type === "supergroup") await input.chats?.seen({
137
+ chatId: message.chat.id,
138
+ title: message.chat.title ?? ""
139
+ });
140
+ input.observed.add(chat, {
141
+ author: event.senderName ?? event.senderId ?? "automated post",
142
+ text: event.text ?? "[attachment or service message]",
143
+ at: event.at
144
+ });
145
+ capture.decision = "silent";
146
+ capture.reason = edited !== void 0 ? "message_edit" : "automated_or_channel_message";
147
+ }
148
+ let response;
149
+ try {
150
+ response = await input.next();
151
+ } catch (error) {
152
+ try {
153
+ await input.archive.append({
154
+ ...event,
155
+ eventId: `${event.eventId}:decision`,
156
+ kind: "decision",
157
+ text: null,
158
+ decision: "silent",
159
+ reason: "inbound_handler_failed",
160
+ attachments: []
161
+ });
162
+ } catch (storageError) {
163
+ console.error("[segmentstream] recording inbound failure failed", {
164
+ storageError,
165
+ cause: error
166
+ });
167
+ }
168
+ throw error;
169
+ }
170
+ await input.archive.append({
171
+ ...event,
172
+ eventId: `${event.eventId}:decision`,
173
+ kind: "decision",
174
+ text: null,
175
+ decision: capture.decision ?? "silent",
176
+ 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"),
177
+ attachments: []
178
+ });
179
+ return response;
180
+ }
181
+ function recordGateDecision(reason, decision = "silent") {
182
+ const inbound = currentInboundRequest();
183
+ const capture = inbound === void 0 ? void 0 : captures.get(inbound);
184
+ if (capture !== void 0) {
185
+ capture.reason = reason;
186
+ capture.decision = decision;
187
+ }
188
+ }
189
+ function recordRouteDecision(route, addressed, policy) {
190
+ const inbound = currentInboundRequest();
191
+ const capture = inbound === void 0 ? void 0 : captures.get(inbound);
192
+ if (capture !== void 0) {
193
+ capture.decision = route === "reply" ? "reply" : "silent";
194
+ capture.reason ??= typeof addressed === "string" ? addressed : addressed ? "addressed_message" : policy === "mention" ? "mention_policy" : `decision_model_${route}`;
195
+ }
196
+ }
197
+ function captureRouteDecider(decide) {
198
+ return async (question) => {
199
+ try {
200
+ return await decide(question);
201
+ } catch (error) {
202
+ const inbound = currentInboundRequest();
203
+ const capture = inbound === void 0 ? void 0 : captures.get(inbound);
204
+ if (capture !== void 0) capture.reason = error instanceof Error && error.name === "TimeoutError" ? "decision_model_timeout" : "decision_model_failed";
205
+ throw error;
206
+ }
207
+ };
208
+ }
209
+ function captureTelegramFetch(fetchImpl, archive, identities) {
210
+ return async (input, init) => {
211
+ if (!(typeof input === "string" ? input : input instanceof URL ? input.href : input.url).endsWith("/sendMessage") || typeof init?.body !== "string") return fetchImpl(input, init);
212
+ const body = record(JSON.parse(init.body));
213
+ const chatId = id(body.chat_id);
214
+ if (chatId === null) return fetchImpl(input, init);
215
+ const inbound = currentInboundRequest();
216
+ const capture = inbound === void 0 ? void 0 : captures.get(inbound);
217
+ let participantIds = capture?.event.participantIds ?? [];
218
+ const chatType = capture?.event.chatType ?? (chatId.startsWith("-") ? "group" : "private");
219
+ if (chatType === "private" && participantIds.length === 0 && capture === void 0) try {
220
+ const lookup = await identities().lookup({
221
+ provider: "telegram",
222
+ externalId: chatId
223
+ });
224
+ if (lookup.status === "paired") participantIds = [lookup.member.id];
225
+ } catch (error) {
226
+ console.error("[segmentstream] outbound participant lookup failed", { error });
227
+ }
228
+ const event = conversationEvent({
229
+ eventId: `telegram:send:${randomUUID()}`,
230
+ provider: "telegram",
231
+ chatId,
232
+ chatType,
233
+ kind: "outbound",
234
+ text: typeof body.text === "string" ? body.text : null,
235
+ senderIsBot: true,
236
+ senderId: "agent",
237
+ senderName: "agent",
238
+ participantIds,
239
+ threadId: id(body.message_thread_id),
240
+ replyToMessageId: id(body.reply_to_message_id) ?? id(record(body.reply_parameters).message_id),
241
+ status: "requested"
242
+ });
243
+ await archive.append(event);
244
+ let response;
245
+ try {
246
+ response = await fetchImpl(input, init);
247
+ } catch (error) {
248
+ try {
249
+ await archive.append({
250
+ ...event,
251
+ eventId: `${event.eventId}:result`,
252
+ status: "unknown"
253
+ });
254
+ } catch (storageError) {
255
+ console.error("[segmentstream] recording uncertain delivery failed", {
256
+ storageError,
257
+ cause: error
258
+ });
259
+ }
260
+ throw error;
261
+ }
262
+ const result = record(await response.clone().json());
263
+ await archive.append({
264
+ ...event,
265
+ eventId: `${event.eventId}:result`,
266
+ status: response.ok && result.ok === true ? "completed" : "failed",
267
+ messageId: id(record(result.result).message_id)
268
+ });
269
+ return response;
270
+ };
271
+ }
272
+ function conversationIdentities(identities) {
273
+ return {
274
+ ...identities,
275
+ lookup: async (account) => {
276
+ const inbound = currentInboundRequest();
277
+ const capture = inbound === void 0 ? void 0 : captures.get(inbound);
278
+ return account.provider === "telegram" && capture?.message.from?.id === account.externalId && capture.lookup !== void 0 ? capture.lookup : identities.lookup(account);
279
+ }
280
+ };
281
+ }
282
+ function telegramConversationCapture(input) {
283
+ return async (request, next) => {
284
+ if (!input.verify(request)) return next();
285
+ try {
286
+ return await captureTelegramWebhook({
287
+ ...input,
288
+ request,
289
+ identities: input.identities(),
290
+ next
291
+ });
292
+ } catch (error) {
293
+ console.error("[segmentstream] conversation capture failed", { error });
294
+ return new Response("conversation storage unavailable", { status: 503 });
295
+ }
296
+ };
297
+ }
298
+ export { captureRouteDecider, captureTelegramFetch, captureTelegramWebhook, conversationIdentities, recordGateDecision, recordRouteDecision, telegramConversationCapture };
@@ -0,0 +1,12 @@
1
+ import { type RouteDefinition } from "eve/channels";
2
+ import type { TelegramChannelState } from "eve/channels/telegram";
3
+ import type { RuntimeTokenVerifier } from "../runtime-identity.js";
4
+ import type { AgentChats } from "./chats.js";
5
+ export declare const HEALTH_ROUTE = "/eve/v1/telegram/health";
6
+ /** Runtime-only, per-instance health. It never exposes credentials or private provider bodies. */
7
+ export declare function healthRoute(input: {
8
+ readonly chats: AgentChats;
9
+ readonly enabled: () => boolean;
10
+ readonly configured: () => boolean;
11
+ readonly verify: () => RuntimeTokenVerifier;
12
+ }): RouteDefinition<TelegramChannelState>;
@@ -0,0 +1,24 @@
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
+ import { runtimeRequestFailure } from "./requests.mjs";
8
+ import { GET } from "eve/channels";
9
+ const HEALTH_ROUTE = "/eve/v1/telegram/health";
10
+ function healthRoute(input) {
11
+ return GET(HEALTH_ROUTE, async (request) => {
12
+ const failure = await runtimeRequestFailure(request, input);
13
+ if (failure !== void 0) return failure;
14
+ const registry = input.chats.health();
15
+ return Response.json({
16
+ status: registry.degraded ? "degraded" : "ok",
17
+ registry
18
+ }, {
19
+ status: registry.degraded ? 503 : 200,
20
+ headers: { "cache-control": "no-store" }
21
+ });
22
+ });
23
+ }
24
+ export { HEALTH_ROUTE, healthRoute };
@@ -5,12 +5,13 @@ const __filename = __eveFileURLToPath(import.meta.url);
5
5
  __eveDirname(__filename);
6
6
  __eveCreateRequire(import.meta.url);
7
7
  import { telegramPrincipal } from "./principal.mjs";
8
+ import { observedChatKey, observedContext } from "./observed.mjs";
9
+ import { recordGateDecision, recordRouteDecision } from "./conversations.mjs";
8
10
  import { answerMarker } from "../questions.mjs";
9
11
  import { formatAddress, messageAddress, scopedAddress } from "./sessions.mjs";
10
12
  import { questionAddress, questionFromHistory } from "./confirmations.mjs";
11
13
  import { chatContext } from "./chats.mjs";
12
14
  import { attachmentsContext, stageFiles } from "./files.mjs";
13
- import { observedChatKey, observedContext } from "./observed.mjs";
14
15
  import { addressingReason, routeMessage } from "./router.mjs";
15
16
  import { routingContext } from "./routing-context.mjs";
16
17
  import { voiceNote, voiceOf, voiceText } from "./voice.mjs";
@@ -143,6 +144,7 @@ async function handleInboundMessage(input) {
143
144
  const left = message.raw.left_chat_member;
144
145
  const migration = isGroupChat(message) ? migrationOf(message) : void 0;
145
146
  if (isGroupChat(message) && isRecord(left)) {
147
+ recordGateDecision("member_departure");
146
148
  await reportDeparture({
147
149
  context,
148
150
  message,
@@ -152,6 +154,7 @@ async function handleInboundMessage(input) {
152
154
  return null;
153
155
  }
154
156
  if (migration !== void 0) {
157
+ recordGateDecision("chat_migration");
155
158
  await dependencies.chats.migrated({
156
159
  ...migration,
157
160
  title: message.chat.title ?? ""
@@ -177,6 +180,7 @@ async function handleInboundMessage(input) {
177
180
  }) : void 0;
178
181
  if (report !== void 0) input.request?.waitUntil?.(report);
179
182
  if (lookup.status !== "paired") {
183
+ recordGateDecision(`identity_${lookup.status}`, addressed !== void 0 ? "reply" : "silent");
180
184
  if (addressed !== void 0) await answerUnpaired({
181
185
  context,
182
186
  message: sent,
@@ -221,12 +225,15 @@ async function handleInboundMessage(input) {
221
225
  text = [caption, spoken.text].filter((part) => part.trim() !== "").join("\n\n");
222
226
  notes.push(spoken.note);
223
227
  } else if (voice !== void 0) text = caption || voiceNote(voice);
224
- if (text.trim() === "" && sent.attachments.length === 0) return null;
228
+ if (text.trim() === "" && sent.attachments.length === 0) {
229
+ recordGateDecision("empty_message");
230
+ return null;
231
+ }
225
232
  const chat = observedChatKey({
226
233
  chatId: sent.chat.id,
227
234
  ...sent.messageThreadId === void 0 ? {} : { messageThreadId: sent.messageThreadId }
228
235
  });
229
- if (await routeMessage({
236
+ const route = await routeMessage({
230
237
  chatId: sent.chat.id,
231
238
  memberId: lookup.member.id,
232
239
  policy: dependencies.policy,
@@ -246,7 +253,9 @@ async function handleInboundMessage(input) {
246
253
  message: observedMessage(sent, text, lookup.member)
247
254
  };
248
255
  }
249
- }) === "observe") {
256
+ });
257
+ recordRouteDecision(route, addressed ?? (answering ? "pending-question" : void 0), dependencies.policy);
258
+ if (route === "observe") {
250
259
  dependencies.observed.add(chat, observedMessage(sent, text === "" ? "[attachment]" : text, lookup.member));
251
260
  return null;
252
261
  }
@@ -0,0 +1,8 @@
1
+ import type { AgentIdentities } from "../identities.js";
2
+ import type { AgentChats } from "./chats.js";
3
+ /** Membership updates have no message for Eve to route. Report them at the verified webhook boundary. */
4
+ export declare function reportMembership(input: {
5
+ readonly update: Record<string, unknown>;
6
+ readonly chats: AgentChats;
7
+ readonly identities: AgentIdentities;
8
+ }): Promise<void>;
@@ -0,0 +1,48 @@
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 record(value) {
8
+ return typeof value === "object" && value !== null && !Array.isArray(value) ? value : {};
9
+ }
10
+ function isMember(value) {
11
+ return value.status === "member" || value.status === "administrator" || value.status === "creator" || value.status === "restricted" && value.is_member === true;
12
+ }
13
+ async function reportMembership(input) {
14
+ const mine = input.update.my_chat_member !== void 0;
15
+ const update = record(input.update.my_chat_member ?? input.update.chat_member);
16
+ const chat = record(update.chat);
17
+ const next = record(update.new_chat_member);
18
+ const previous = record(update.old_chat_member);
19
+ if (chat.type !== "group" && chat.type !== "supergroup" || typeof chat.id !== "number" && typeof chat.id !== "string" || !/^-?\d{1,20}$/.test(String(chat.id))) return;
20
+ const report = {
21
+ chatId: String(chat.id),
22
+ title: typeof chat.title === "string" ? chat.title : ""
23
+ };
24
+ if (mine) {
25
+ if (isMember(next)) await input.chats.joined(report);
26
+ else if (isMember(previous) && (next.status === "left" || next.status === "kicked" || next.status === "restricted")) await input.chats.agentLeft(report);
27
+ return;
28
+ }
29
+ const user = record(next.user);
30
+ if (user.is_bot === true || typeof user.id !== "number" && typeof user.id !== "string") return;
31
+ const lookup = await input.identities.lookup({
32
+ provider: "telegram",
33
+ externalId: String(user.id)
34
+ });
35
+ if (lookup.status !== "paired") {
36
+ await input.chats.seen(report);
37
+ return;
38
+ }
39
+ if (isMember(next)) await input.chats.seen({
40
+ ...report,
41
+ memberId: lookup.member.id
42
+ });
43
+ else if (isMember(previous)) await input.chats.left({
44
+ ...report,
45
+ memberId: lookup.member.id
46
+ });
47
+ }
48
+ export { reportMembership };
@@ -38,10 +38,6 @@ function telegramProxyRoute(url) {
38
38
  function headerPairs(headers) {
39
39
  return [...new Headers(headers).entries()];
40
40
  }
41
- async function bodyBytes(body) {
42
- if (body === void 0 || body === null) return new Uint8Array();
43
- return new Uint8Array(await new Response(body).arrayBuffer());
44
- }
45
41
  function proxyMethod(method) {
46
42
  const upper = (method ?? "GET").toUpperCase();
47
43
  for (const candidate of METHODS) if (candidate === upper) return candidate;
@@ -53,12 +49,13 @@ function hasNullBody(status) {
53
49
  function telegramProxyFetch(proxy) {
54
50
  return async (input, init) => {
55
51
  const route = telegramProxyRoute(requestUrl(input));
52
+ const request = new Request(input, init);
56
53
  const call = {
57
54
  api: route.api,
58
- method: proxyMethod(init?.method),
55
+ method: proxyMethod(request.method),
59
56
  path: route.path,
60
- headers: headerPairs(init?.headers),
61
- body: await bodyBytes(init?.body)
57
+ headers: headerPairs(request.headers),
58
+ body: new Uint8Array(await request.arrayBuffer())
62
59
  };
63
60
  const reply = route.api === "file" ? await proxy.download(call) : await proxy.request(call);
64
61
  return new Response(hasNullBody(reply.status) ? null : Buffer.from(reply.body), {
@@ -21,6 +21,8 @@ export interface AgentRequest {
21
21
  readonly chatId: string;
22
22
  };
23
23
  readonly message: string;
24
+ /** Document record ids from newer ontologies; omitted by older deployments. */
25
+ readonly documentIds?: ReadonlyArray<string>;
24
26
  /** Whom to contact, when the requester named someone. */
25
27
  readonly person: {
26
28
  readonly id: string;
@@ -53,6 +55,13 @@ export declare function agentRequest(value: unknown): AgentRequest | undefined;
53
55
  export declare function requestPrincipal(request: AgentRequest): SessionAuthContext;
54
56
  /** What the turn is about, for the model, ahead of the request itself. */
55
57
  export declare function requestContext(request: AgentRequest): string;
58
+ /** Authenticate internal channel routes with the production deployment's runtime identity. */
59
+ export declare function runtimeRequestFailure(request: Request, options: {
60
+ readonly enabled: () => boolean;
61
+ readonly configured: () => boolean;
62
+ readonly verify: () => RuntimeTokenVerifier;
63
+ readonly log?: (message: string, detail: unknown) => void;
64
+ }): Promise<Response | undefined>;
56
65
  /**
57
66
  * `POST /eve/v1/telegram/messages`. Gated like the webhook: outside production it answers 503 and a
58
67
  * workspace without a Telegram channel 404. The caller must be a function of this deployment's
@@ -33,7 +33,8 @@ function agentRequest(value) {
33
33
  const from = user(requester);
34
34
  const held = isRecord(requester) ? roles(requester.roles) : void 0;
35
35
  const actAs = isRecord(requester) ? requester.actAs : void 0;
36
- if (!isRecord(target) || target.kind !== "group" && target.kind !== "private" || target.provider !== "telegram" || typeof target.chatId !== "string" || !CHAT_ID.test(target.chatId) || !isText(message) || message.length > MAX_REQUEST_MESSAGE_LENGTH || person === void 0 || from === void 0 || !isRecord(requester) || requester.kind !== "member" && requester.kind !== "service" || held === void 0 || actAs !== void 0 && typeof actAs !== "boolean") return;
36
+ const documentIds = value.documentIds;
37
+ if (!isRecord(target) || target.kind !== "group" && target.kind !== "private" || target.provider !== "telegram" || typeof target.chatId !== "string" || !CHAT_ID.test(target.chatId) || !isText(message) || message.length > MAX_REQUEST_MESSAGE_LENGTH || person === void 0 || from === void 0 || !isRecord(requester) || requester.kind !== "member" && requester.kind !== "service" || held === void 0 || actAs !== void 0 && typeof actAs !== "boolean" || documentIds !== void 0 && (!Array.isArray(documentIds) || documentIds.length > 10 || !documentIds.every((id) => typeof id === "string" && /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i.test(id)))) return;
37
38
  return {
38
39
  target: {
39
40
  kind: target.kind,
@@ -41,6 +42,7 @@ function agentRequest(value) {
41
42
  chatId: target.chatId
42
43
  },
43
44
  message,
45
+ ...documentIds === void 0 ? {} : { documentIds },
44
46
  person,
45
47
  requester: {
46
48
  ...from,
@@ -91,6 +93,7 @@ function requestContext(request) {
91
93
  `Write in this chat${to}, carry out the clear request, ask only for missing or ambiguous information, and handle the replies here.`,
92
94
  "If the addressed person has a linked Telegram username, mention them with @username; otherwise address them by name without a tag. Never invent a username."
93
95
  ];
96
+ if (request.documentIds !== void 0 && request.documentIds.length > 0) lines.push(`documentIds: ${JSON.stringify(request.documentIds)}`, "Send these documents with the question using segmentstream__attachments_send in this chat. Use the question as the caption (at most 1024 characters), one question per message. In groups send documents only when the chat's purpose allows; never send restricted documents there.");
94
97
  if (requester.kind === "service" || !requester.actAs) lines.push(`${requester.kind === "service" ? "The requester is a service, not a member" : "The requester has not paired a Telegram account with the agent"}, so this turn runs without a member and the ontology is not available in it. Each member who replies acts with their own permissions.`);
95
98
  lines.push("</agent_request>");
96
99
  return lines.join("\n");
@@ -112,29 +115,33 @@ function bearer(header) {
112
115
  function error(code, status) {
113
116
  return Response.json({ error: code }, { status });
114
117
  }
118
+ async function runtimeRequestFailure(request, options) {
119
+ const log = options.log ?? ((message, detail) => console.warn(message, detail));
120
+ if (!options.enabled()) return error("channels_disabled", 503);
121
+ if (!options.configured()) return error("not_found", 404);
122
+ const token = bearer(request.headers.get("authorization"));
123
+ if (token === void 0) return error("unauthorized", 401);
124
+ let verifier;
125
+ try {
126
+ verifier = options.verify();
127
+ } catch (cause) {
128
+ log("The agent cannot verify requests: the runtime identity is not configured.", cause);
129
+ return error("unavailable", 503);
130
+ }
131
+ const verification = await verifier(token);
132
+ if (verification.status === "unavailable") {
133
+ log("The agent cannot verify requests: the runtime key set is unavailable.", verification.reason);
134
+ return error("unavailable", 503);
135
+ }
136
+ if (verification.status !== "valid") {
137
+ log("The agent refused a request with an invalid runtime token.", verification.reason);
138
+ return error("unauthorized", 401);
139
+ }
140
+ }
115
141
  function messagesRoute(input) {
116
- const log = input.log ?? ((message, detail) => console.warn(message, detail));
117
142
  return POST(MESSAGES_ROUTE, async (request, args) => {
118
- if (!input.enabled()) return error("channels_disabled", 503);
119
- if (!input.configured()) return error("not_found", 404);
120
- const token = bearer(request.headers.get("authorization"));
121
- if (token === void 0) return error("unauthorized", 401);
122
- let verifier;
123
- try {
124
- verifier = input.verify();
125
- } catch (cause) {
126
- log("The agent cannot verify requests: the runtime identity is not configured.", cause);
127
- return error("unavailable", 503);
128
- }
129
- const verification = await verifier(token);
130
- if (verification.status === "unavailable") {
131
- log("The agent cannot verify requests: the runtime key set is unavailable.", verification.reason);
132
- return error("unavailable", 503);
133
- }
134
- if (verification.status !== "valid") {
135
- log("The agent refused a request with an invalid runtime token.", verification.reason);
136
- return error("unauthorized", 401);
137
- }
143
+ const failure = await runtimeRequestFailure(request, input);
144
+ if (failure !== void 0) return failure;
138
145
  let body;
139
146
  try {
140
147
  body = await request.json();
@@ -172,4 +179,4 @@ function messagesRoute(input) {
172
179
  }, { status: 202 });
173
180
  });
174
181
  }
175
- export { MESSAGES_ROUTE, agentRequest, messagesRoute, requestContext, requestPrincipal };
182
+ export { MESSAGES_ROUTE, agentRequest, messagesRoute, requestContext, requestPrincipal, runtimeRequestFailure };
@@ -0,0 +1,7 @@
1
+ declare const _default: import("eve/tools").ToolDefinition<Record<string, unknown>, {
2
+ readonly files: ReadonlyArray<import("../lib/telegram/attachments-send.js").SentFile>;
3
+ } | {
4
+ readonly error: string;
5
+ readonly detail: string;
6
+ }>;
7
+ export default _default;
@@ -0,0 +1,59 @@
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
+ import segmentstream from "../extension.mjs";
8
+ import { sendAttachments, sendAttachmentsInput } from "../lib/telegram/attachments-send.mjs";
9
+ import { defineTool } from "eve/tools";
10
+ var attachments_send_default = defineTool({
11
+ availableInSubagents: false,
12
+ description: "Send 1 to 10 stored ontology files to this turn's Telegram chat, as the paired member. Bare ids are document record ids; use entity/id/property references for other file records. Images use sendPhoto, other files sendDocument, with compatible albums when possible. Uploads are limited to 2 MiB including multipart overhead by the connection proxy. Use the question as caption when sending documents with a question; one question per message. In groups follow the chat's purpose and never send restricted documents.",
13
+ inputSchema: {
14
+ type: "object",
15
+ properties: {
16
+ documentIds: {
17
+ type: "array",
18
+ minItems: 1,
19
+ maxItems: 10,
20
+ items: { anyOf: [{
21
+ type: "string",
22
+ description: "A document record id (UUID)."
23
+ }, {
24
+ type: "object",
25
+ properties: {
26
+ entity: { type: "string" },
27
+ id: { type: "string" },
28
+ property: {
29
+ type: "string",
30
+ description: "Required when there are multiple file properties."
31
+ }
32
+ },
33
+ required: ["entity", "id"],
34
+ additionalProperties: false
35
+ }] }
36
+ },
37
+ caption: {
38
+ type: "string",
39
+ maxLength: 1024
40
+ },
41
+ replyToMessageId: {
42
+ type: "integer",
43
+ minimum: 1
44
+ }
45
+ },
46
+ required: ["documentIds"],
47
+ additionalProperties: false
48
+ },
49
+ label: { start: () => "Send stored attachments to Telegram" },
50
+ execute(input, ctx) {
51
+ return sendAttachments({
52
+ input: sendAttachmentsInput(input),
53
+ principal: ctx.session.auth.current,
54
+ sessionId: ctx.session.id,
55
+ connection: segmentstream.config.channels.telegram?.connection
56
+ });
57
+ }
58
+ });
59
+ export { attachments_send_default as default };