@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.
- package/README.md +66 -3
- package/dist/extension/_manifest.json +2 -0
- package/dist/extension/hooks/conversations.d.ts +2 -0
- package/dist/extension/hooks/conversations.mjs +10 -0
- package/dist/extension/instructions/conversations.d.ts +3 -0
- package/dist/extension/instructions/conversations.mjs +20 -0
- package/dist/extension/instructions.md +11 -0
- package/dist/extension/lib/conversation-runtime.d.ts +19 -0
- package/dist/extension/lib/conversation-runtime.mjs +104 -0
- package/dist/extension/lib/conversations.d.ts +65 -0
- package/dist/extension/lib/conversations.mjs +107 -0
- package/dist/extension/lib/diagnostics.d.ts +2 -0
- package/dist/extension/lib/diagnostics.mjs +24 -0
- package/dist/extension/lib/identities.d.ts +2 -0
- package/dist/extension/lib/identities.mjs +27 -6
- package/dist/extension/lib/telegram/adapter.d.ts +2 -0
- package/dist/extension/lib/telegram/adapter.mjs +61 -22
- package/dist/extension/lib/telegram/attachments-send.d.ts +40 -0
- package/dist/extension/lib/telegram/attachments-send.mjs +269 -0
- package/dist/extension/lib/telegram/chats.d.ts +21 -1
- package/dist/extension/lib/telegram/chats.mjs +75 -11
- package/dist/extension/lib/telegram/confirmations.mjs +1 -1
- package/dist/extension/lib/telegram/conversations.d.ts +30 -0
- package/dist/extension/lib/telegram/conversations.mjs +298 -0
- package/dist/extension/lib/telegram/health.d.ts +12 -0
- package/dist/extension/lib/telegram/health.mjs +24 -0
- package/dist/extension/lib/telegram/inbound.mjs +13 -4
- package/dist/extension/lib/telegram/membership.d.ts +8 -0
- package/dist/extension/lib/telegram/membership.mjs +48 -0
- package/dist/extension/lib/telegram/proxy-fetch.mjs +4 -7
- package/dist/extension/lib/telegram/requests.d.ts +9 -0
- package/dist/extension/lib/telegram/requests.mjs +30 -23
- package/dist/extension/tools/attachments_send.d.ts +7 -0
- package/dist/extension/tools/attachments_send.mjs +59 -0
- package/dist/tools/index.d.ts +1 -0
- package/dist/tools/index.mjs +2 -1
- 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)
|
|
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
|
-
|
|
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
|
-
})
|
|
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(
|
|
55
|
+
method: proxyMethod(request.method),
|
|
59
56
|
path: route.path,
|
|
60
|
-
headers: headerPairs(
|
|
61
|
-
body: await
|
|
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
|
-
|
|
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
|
-
|
|
119
|
-
if (
|
|
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 };
|