@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
|
@@ -9,19 +9,22 @@ import { MAXIMUM_DOWNLOAD_BYTES, connectionProxy } from "../connection-proxy.mjs
|
|
|
9
9
|
import { runtimeToken } from "../runtime-token.mjs";
|
|
10
10
|
import { agentAttachments, parseAttachmentUrl } from "../attachments.mjs";
|
|
11
11
|
import { agentIdentities } from "../identities.mjs";
|
|
12
|
+
import { conversations } from "../conversations.mjs";
|
|
13
|
+
import { observedMessages } from "./observed.mjs";
|
|
14
|
+
import { InboundRequest, currentInboundRequest, runInboundRequest, withReplacedText } from "./inbound-request.mjs";
|
|
15
|
+
import { captureRouteDecider, captureTelegramFetch, conversationIdentities, telegramConversationCapture } from "./conversations.mjs";
|
|
12
16
|
import { runtimeTokenVerifier } from "../runtime-identity.mjs";
|
|
13
17
|
import { botIdentityCache } from "./bot.mjs";
|
|
14
|
-
import { InboundRequest, currentInboundRequest, runInboundRequest, withReplacedText } from "./inbound-request.mjs";
|
|
15
18
|
import { scopedAddress, sessionPolicy } from "./sessions.mjs";
|
|
16
19
|
import { confirmationEvents, handleConfirmationCallback } from "./confirmations.mjs";
|
|
17
20
|
import { agentChats } from "./chats.mjs";
|
|
18
|
-
import { observedMessages } from "./observed.mjs";
|
|
19
21
|
import { decisionRouter } from "./router.mjs";
|
|
20
22
|
import { replyDelivery } from "./silent.mjs";
|
|
21
23
|
import { downloadTelegramFile, gatewayTranscriber } from "./voice.mjs";
|
|
22
24
|
import { handleInboundMessage } from "./inbound.mjs";
|
|
23
25
|
import { TELEGRAM_PLACEHOLDER_TOKEN, TELEGRAM_PROXY_ORIGIN, telegramProxyFetch } from "./proxy-fetch.mjs";
|
|
24
26
|
import { messagesRoute } from "./requests.mjs";
|
|
27
|
+
import { healthRoute } from "./health.mjs";
|
|
25
28
|
import { withTurnErrorLogging } from "./turn-errors.mjs";
|
|
26
29
|
import { telegramWebhookVerifier } from "./webhook.mjs";
|
|
27
30
|
import { telegramChannel } from "eve/channels/telegram";
|
|
@@ -57,6 +60,11 @@ function segmentstreamTelegram(options) {
|
|
|
57
60
|
const observed = options.observed ?? observedMessages();
|
|
58
61
|
const readHistory = options.history ?? eveHistory;
|
|
59
62
|
const readInteractions = options.history ?? eveInteractions;
|
|
63
|
+
const archive = options.conversations ?? conversations({
|
|
64
|
+
environment,
|
|
65
|
+
token,
|
|
66
|
+
fetch: options.fetch
|
|
67
|
+
});
|
|
60
68
|
const policy = sessionPolicy({
|
|
61
69
|
settings: options.sessions,
|
|
62
70
|
read: readHistory
|
|
@@ -93,6 +101,13 @@ function segmentstreamTelegram(options) {
|
|
|
93
101
|
return identities;
|
|
94
102
|
}
|
|
95
103
|
const chats = agentChats({ identities: pairing });
|
|
104
|
+
const capture = telegramConversationCapture({
|
|
105
|
+
archive,
|
|
106
|
+
identities: pairing,
|
|
107
|
+
observed,
|
|
108
|
+
chats,
|
|
109
|
+
verify: telegramWebhookVerifier(() => webhookSecret(environment))
|
|
110
|
+
});
|
|
96
111
|
let store = options.attachments;
|
|
97
112
|
function attachments() {
|
|
98
113
|
store ??= agentAttachments({
|
|
@@ -106,7 +121,7 @@ function segmentstreamTelegram(options) {
|
|
|
106
121
|
api: {
|
|
107
122
|
apiBaseUrl: TELEGRAM_PROXY_ORIGIN,
|
|
108
123
|
fileBaseUrl: TELEGRAM_PROXY_ORIGIN,
|
|
109
|
-
fetch: (input, init) => telegramProxyFetch(proxy())(input, init)
|
|
124
|
+
fetch: (input, init) => captureTelegramFetch(telegramProxyFetch(proxy()), archive, pairing)(input, init)
|
|
110
125
|
},
|
|
111
126
|
credentials: {
|
|
112
127
|
botToken: TELEGRAM_PLACEHOLDER_TOKEN,
|
|
@@ -122,12 +137,12 @@ function segmentstreamTelegram(options) {
|
|
|
122
137
|
sessions: options.sessions,
|
|
123
138
|
sessionPolicy: policy,
|
|
124
139
|
sttModel: options.models().stt,
|
|
125
|
-
identities: pairing(),
|
|
140
|
+
identities: conversationIdentities(pairing()),
|
|
126
141
|
bot,
|
|
127
|
-
decide: decisionRouter({
|
|
142
|
+
decide: captureRouteDecider(decisionRouter({
|
|
128
143
|
decide: options.decide,
|
|
129
144
|
model: options.models().decision
|
|
130
|
-
}),
|
|
145
|
+
})),
|
|
131
146
|
transcribe: options.transcribe ?? gatewayTranscriber,
|
|
132
147
|
download: (fileId) => downloadTelegramFile({
|
|
133
148
|
proxy: proxy(),
|
|
@@ -178,18 +193,28 @@ function segmentstreamTelegram(options) {
|
|
|
178
193
|
fetchFile,
|
|
179
194
|
...turnFailed === void 0 ? {} : { "turn.failed": withTurnErrorLogging(turnFailed) }
|
|
180
195
|
} },
|
|
181
|
-
routes: [
|
|
182
|
-
route
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
196
|
+
routes: [
|
|
197
|
+
...channel.routes.map((route) => gatedRoute({
|
|
198
|
+
route,
|
|
199
|
+
options,
|
|
200
|
+
environment,
|
|
201
|
+
capture
|
|
202
|
+
})),
|
|
203
|
+
messagesRoute({
|
|
204
|
+
channel,
|
|
205
|
+
enabled: () => channelsEnabled(environment),
|
|
206
|
+
configured: () => options.settings() !== null,
|
|
207
|
+
verify: () => verify ??= runtimeTokenVerifier(runtimeIdentityEnvironment(environment)),
|
|
208
|
+
sessionPolicy: policy,
|
|
209
|
+
scope: () => options.sessions().scope
|
|
210
|
+
}),
|
|
211
|
+
healthRoute({
|
|
212
|
+
chats,
|
|
213
|
+
enabled: () => channelsEnabled(environment),
|
|
214
|
+
configured: () => options.settings() !== null,
|
|
215
|
+
verify: () => verify ??= runtimeTokenVerifier(runtimeIdentityEnvironment(environment))
|
|
216
|
+
})
|
|
217
|
+
]
|
|
193
218
|
};
|
|
194
219
|
}
|
|
195
220
|
function sessionIdOf(context) {
|
|
@@ -210,14 +235,28 @@ function gatedRoute(input) {
|
|
|
210
235
|
resolveSession: args.resolveSession,
|
|
211
236
|
from: args.from
|
|
212
237
|
};
|
|
213
|
-
inbound.waitUntil = args.waitUntil;
|
|
214
238
|
inbound.respond = (address, requestId, decision, auth) => args.from(scoped(address)).respond([{
|
|
215
239
|
requestId,
|
|
216
240
|
optionId: decision
|
|
217
241
|
}], { auth });
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
242
|
+
const tasks = [];
|
|
243
|
+
const waitUntil = (task) => {
|
|
244
|
+
tasks.push(task);
|
|
245
|
+
args.waitUntil(task);
|
|
246
|
+
};
|
|
247
|
+
inbound.waitUntil = waitUntil;
|
|
248
|
+
return runInboundRequest(inbound, () => input.capture(request, async () => {
|
|
249
|
+
const response = await route.handler(request, {
|
|
250
|
+
...args,
|
|
251
|
+
waitUntil,
|
|
252
|
+
from: (address) => withReplacedText(args.from(inbound.address ?? scoped(address)), inbound)
|
|
253
|
+
});
|
|
254
|
+
for (let completed = 0; completed < tasks.length;) {
|
|
255
|
+
const pending = tasks.slice(completed);
|
|
256
|
+
completed = tasks.length;
|
|
257
|
+
await Promise.all(pending);
|
|
258
|
+
}
|
|
259
|
+
return response;
|
|
221
260
|
}));
|
|
222
261
|
}
|
|
223
262
|
};
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import type { SessionAuthContext } from "eve/context";
|
|
2
|
+
import { type Environment } from "../environment.js";
|
|
3
|
+
import { type RuntimeToken } from "../runtime-token.js";
|
|
4
|
+
export declare const MAXIMUM_PHOTO_BYTES: number;
|
|
5
|
+
export declare const MAXIMUM_DOCUMENT_BYTES: number;
|
|
6
|
+
export interface DocumentReference {
|
|
7
|
+
readonly entity: string;
|
|
8
|
+
readonly id: string;
|
|
9
|
+
readonly property?: string;
|
|
10
|
+
}
|
|
11
|
+
export interface SendAttachmentsInput {
|
|
12
|
+
readonly documentIds: ReadonlyArray<DocumentReference>;
|
|
13
|
+
readonly caption?: string;
|
|
14
|
+
readonly replyToMessageId?: number;
|
|
15
|
+
}
|
|
16
|
+
export interface SentFile {
|
|
17
|
+
readonly id: string;
|
|
18
|
+
readonly filename: string;
|
|
19
|
+
readonly telegramMessageId?: number;
|
|
20
|
+
readonly error?: string;
|
|
21
|
+
readonly detail?: string;
|
|
22
|
+
readonly reference?: string;
|
|
23
|
+
}
|
|
24
|
+
/** Bare ids are document records; explicit references select other entities and file properties. */
|
|
25
|
+
export declare function sendAttachmentsInput(value: Record<string, unknown>): SendAttachmentsInput;
|
|
26
|
+
/** All destinations and credentials come from the channel's turn and mounted configuration. */
|
|
27
|
+
export declare function sendAttachments(options: {
|
|
28
|
+
readonly input: SendAttachmentsInput;
|
|
29
|
+
readonly principal: SessionAuthContext | null;
|
|
30
|
+
readonly sessionId: string;
|
|
31
|
+
readonly connection: string | undefined;
|
|
32
|
+
readonly environment?: Environment;
|
|
33
|
+
readonly token?: RuntimeToken;
|
|
34
|
+
readonly fetch?: typeof fetch;
|
|
35
|
+
}): Promise<{
|
|
36
|
+
readonly files: ReadonlyArray<SentFile>;
|
|
37
|
+
} | {
|
|
38
|
+
readonly error: string;
|
|
39
|
+
readonly detail: string;
|
|
40
|
+
}>;
|
|
@@ -0,0 +1,269 @@
|
|
|
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 { appOrigin, channelsEnabled, proxyEnvironment } from "../environment.mjs";
|
|
8
|
+
import "./principal.mjs";
|
|
9
|
+
import { MAXIMUM_PROXY_BODY_BYTES, connectionProxy } from "../connection-proxy.mjs";
|
|
10
|
+
import { runtimeToken } from "../runtime-token.mjs";
|
|
11
|
+
import { ACT_AS_HEADER } from "../ontology-connection.mjs";
|
|
12
|
+
import { AttachmentError, CONVERSATION_HEADER, SESSION_HEADER } from "../attachments.mjs";
|
|
13
|
+
import { redactDiagnostic } from "../diagnostics.mjs";
|
|
14
|
+
import { TELEGRAM_PLACEHOLDER_TOKEN, TELEGRAM_PROXY_ORIGIN, telegramProxyFetch } from "./proxy-fetch.mjs";
|
|
15
|
+
import { attachmentOwner } from "../attachment-tools.mjs";
|
|
16
|
+
const MAXIMUM_PHOTO_BYTES = 10485760;
|
|
17
|
+
const MAXIMUM_DOCUMENT_BYTES = 52428800;
|
|
18
|
+
const PHOTOS = new Set([
|
|
19
|
+
"image/jpeg",
|
|
20
|
+
"image/png",
|
|
21
|
+
"image/webp"
|
|
22
|
+
]);
|
|
23
|
+
const RECORD_ID = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
|
|
24
|
+
const NAME = /^[a-z][a-z0-9_]{0,63}$/;
|
|
25
|
+
function isRecord(value) {
|
|
26
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
27
|
+
}
|
|
28
|
+
function sendAttachmentsInput(value) {
|
|
29
|
+
if (!Array.isArray(value.documentIds) || value.documentIds.length < 1 || value.documentIds.length > 10) throw new TypeError("documentIds must contain 1 to 10 file record ids or references.");
|
|
30
|
+
const documentIds = value.documentIds.map((item) => {
|
|
31
|
+
const reference = typeof item === "string" ? {
|
|
32
|
+
entity: "document",
|
|
33
|
+
id: item
|
|
34
|
+
} : item;
|
|
35
|
+
if (!isRecord(reference) || typeof reference.id !== "string" || !RECORD_ID.test(reference.id) || typeof reference.entity !== "string" || !NAME.test(reference.entity) || reference.property !== void 0 && (typeof reference.property !== "string" || !NAME.test(reference.property))) throw new TypeError("Each file reference needs an entity, a record UUID and optionally a property.");
|
|
36
|
+
return {
|
|
37
|
+
entity: reference.entity,
|
|
38
|
+
id: reference.id,
|
|
39
|
+
...reference.property === void 0 ? {} : { property: reference.property }
|
|
40
|
+
};
|
|
41
|
+
});
|
|
42
|
+
if (value.caption !== void 0 && (typeof value.caption !== "string" || value.caption.length > 1024)) throw new TypeError("caption must be a string of at most 1024 characters.");
|
|
43
|
+
if (value.replyToMessageId !== void 0 && (typeof value.replyToMessageId !== "number" || !Number.isSafeInteger(value.replyToMessageId) || value.replyToMessageId < 1)) throw new TypeError("replyToMessageId must be a positive integer.");
|
|
44
|
+
return {
|
|
45
|
+
documentIds,
|
|
46
|
+
...value.caption === void 0 ? {} : { caption: value.caption },
|
|
47
|
+
...value.replyToMessageId === void 0 ? {} : { replyToMessageId: value.replyToMessageId }
|
|
48
|
+
};
|
|
49
|
+
}
|
|
50
|
+
function refused(code, message) {
|
|
51
|
+
return new AttachmentError({
|
|
52
|
+
code,
|
|
53
|
+
message
|
|
54
|
+
});
|
|
55
|
+
}
|
|
56
|
+
const PROXY_LIMIT = "The multipart upload exceeds the connection proxy's 2 MiB request limit (including overhead).";
|
|
57
|
+
async function boundedDownload(response, maximum) {
|
|
58
|
+
if (response.body === null) throw refused("unsupported", "The stored file is empty.");
|
|
59
|
+
const reader = response.body.getReader();
|
|
60
|
+
const chunks = [];
|
|
61
|
+
let size = 0;
|
|
62
|
+
try {
|
|
63
|
+
while (true) {
|
|
64
|
+
const next = await reader.read();
|
|
65
|
+
if (next.done) break;
|
|
66
|
+
size += next.value.byteLength;
|
|
67
|
+
if (size > maximum) throw refused("too_large", PROXY_LIMIT);
|
|
68
|
+
chunks.push(next.value);
|
|
69
|
+
}
|
|
70
|
+
} finally {
|
|
71
|
+
await reader.cancel().catch(() => void 0);
|
|
72
|
+
}
|
|
73
|
+
return new Uint8Array(Buffer.concat(chunks));
|
|
74
|
+
}
|
|
75
|
+
async function sendAttachments(options) {
|
|
76
|
+
const environment = options.environment ?? process.env;
|
|
77
|
+
const principal = options.principal;
|
|
78
|
+
if (!channelsEnabled(environment)) return {
|
|
79
|
+
error: "channels_disabled",
|
|
80
|
+
detail: "Attachments can be sent only in production."
|
|
81
|
+
};
|
|
82
|
+
if (options.connection === void 0) return {
|
|
83
|
+
error: "unsupported",
|
|
84
|
+
detail: "The Telegram channel is not configured."
|
|
85
|
+
};
|
|
86
|
+
if (principal?.authenticator !== "segmentstream-telegram" || typeof principal.attributes.chatId !== "string" || !/^-?\d{1,20}$/.test(principal.attributes.chatId)) return {
|
|
87
|
+
error: "no_telegram_chat",
|
|
88
|
+
detail: "This turn has no Telegram chat."
|
|
89
|
+
};
|
|
90
|
+
let owner;
|
|
91
|
+
try {
|
|
92
|
+
owner = attachmentOwner({
|
|
93
|
+
principal,
|
|
94
|
+
sessionId: options.sessionId
|
|
95
|
+
});
|
|
96
|
+
} catch {
|
|
97
|
+
return {
|
|
98
|
+
error: "forbidden",
|
|
99
|
+
detail: "A paired member is required to send stored files."
|
|
100
|
+
};
|
|
101
|
+
}
|
|
102
|
+
const runtimeToken$1 = options.token ?? runtimeToken;
|
|
103
|
+
const secrets = new Set();
|
|
104
|
+
const token = async () => {
|
|
105
|
+
const value = await runtimeToken$1();
|
|
106
|
+
secrets.add(value);
|
|
107
|
+
return value;
|
|
108
|
+
};
|
|
109
|
+
const fetchImpl = options.fetch ?? fetch;
|
|
110
|
+
const origin = appOrigin(environment);
|
|
111
|
+
const telegram = telegramProxyFetch(connectionProxy({
|
|
112
|
+
environment: proxyEnvironment(environment),
|
|
113
|
+
connection: options.connection,
|
|
114
|
+
token,
|
|
115
|
+
fetch: fetchImpl
|
|
116
|
+
}));
|
|
117
|
+
const results = options.input.documentIds.map(({ id }) => ({
|
|
118
|
+
id,
|
|
119
|
+
filename: ""
|
|
120
|
+
}));
|
|
121
|
+
const uploads = [];
|
|
122
|
+
for (const [index, reference] of options.input.documentIds.entries()) {
|
|
123
|
+
let filename = "";
|
|
124
|
+
try {
|
|
125
|
+
const response = await fetchImpl(`${origin}/api/ontology/agent/files/${[
|
|
126
|
+
reference.entity,
|
|
127
|
+
reference.id,
|
|
128
|
+
...reference.property === void 0 ? [] : [reference.property]
|
|
129
|
+
].map(encodeURIComponent).join("/")}`, {
|
|
130
|
+
headers: {
|
|
131
|
+
authorization: `Bearer ${await token()}`,
|
|
132
|
+
[ACT_AS_HEADER]: owner.memberId,
|
|
133
|
+
[CONVERSATION_HEADER]: owner.conversation,
|
|
134
|
+
...owner.sessionId === void 0 ? {} : { [SESSION_HEADER]: owner.sessionId }
|
|
135
|
+
},
|
|
136
|
+
redirect: "error",
|
|
137
|
+
signal: AbortSignal.timeout(3e4)
|
|
138
|
+
});
|
|
139
|
+
if (!response.ok) {
|
|
140
|
+
const code = response.status === 404 ? "not_found" : response.status === 403 ? "forbidden" : response.status === 422 ? "unsupported" : "unavailable";
|
|
141
|
+
throw refused(code, `The ontology refused the file (${code}).`);
|
|
142
|
+
}
|
|
143
|
+
const answer = await response.json();
|
|
144
|
+
const file = isRecord(answer) ? answer.file : void 0;
|
|
145
|
+
const grant = isRecord(answer) ? answer.download : void 0;
|
|
146
|
+
if (!isRecord(file) || typeof file.filename !== "string" || typeof file.content_type !== "string" || typeof file.size !== "number" || !Number.isSafeInteger(file.size) || file.size < 0 || !isRecord(grant) || typeof grant.url !== "string" || !/^https?:\/\//.test(grant.url) || grant.method !== "GET" || !isRecord(grant.headers) || !Object.values(grant.headers).every((header) => typeof header === "string")) throw refused("invalid_response", "The ontology returned invalid file metadata.");
|
|
147
|
+
filename = file.filename;
|
|
148
|
+
secrets.add(grant.url);
|
|
149
|
+
Object.values(grant.headers).forEach((header) => secrets.add(header));
|
|
150
|
+
new URL(grant.url).searchParams.forEach((value, name) => {
|
|
151
|
+
if (/signature|credential|token|key/i.test(name)) secrets.add(value);
|
|
152
|
+
});
|
|
153
|
+
const contentType = file.content_type.toLowerCase();
|
|
154
|
+
const type = PHOTOS.has(contentType) ? "photo" : "document";
|
|
155
|
+
const maximum = type === "photo" ? MAXIMUM_PHOTO_BYTES : MAXIMUM_DOCUMENT_BYTES;
|
|
156
|
+
if (file.size > maximum) throw refused("too_large", `Telegram's ${type} limit is ${maximum / 1024 / 1024} MiB.`);
|
|
157
|
+
if (file.size > 2097152) throw refused("too_large", PROXY_LIMIT);
|
|
158
|
+
if (file.size === 0) throw refused("unsupported", "Telegram cannot send an empty file.");
|
|
159
|
+
const download = await fetchImpl(grant.url, {
|
|
160
|
+
method: "GET",
|
|
161
|
+
headers: grant.headers,
|
|
162
|
+
redirect: "error",
|
|
163
|
+
signal: AbortSignal.timeout(12e4)
|
|
164
|
+
});
|
|
165
|
+
if (!download.ok) throw refused(download.status === 404 ? "not_found" : "unavailable", "The stored file could not be downloaded.");
|
|
166
|
+
const bytes = await boundedDownload(download, Math.min(maximum, MAXIMUM_PROXY_BODY_BYTES));
|
|
167
|
+
if (bytes.byteLength !== file.size) throw refused("invalid_response", "The downloaded size differs from the stored file metadata.");
|
|
168
|
+
uploads.push({
|
|
169
|
+
index,
|
|
170
|
+
id: reference.id,
|
|
171
|
+
filename,
|
|
172
|
+
type,
|
|
173
|
+
blob: new Blob([Buffer.from(bytes)], { type: contentType })
|
|
174
|
+
});
|
|
175
|
+
} catch (error) {
|
|
176
|
+
const errorId = crypto.randomUUID();
|
|
177
|
+
console.error("[segmentstream] stored attachment fetch failed", {
|
|
178
|
+
errorId,
|
|
179
|
+
id: reference.id,
|
|
180
|
+
error: redactDiagnostic(error, secrets)
|
|
181
|
+
});
|
|
182
|
+
results[index] = {
|
|
183
|
+
id: reference.id,
|
|
184
|
+
filename,
|
|
185
|
+
error: error instanceof AttachmentError ? error.code : "unavailable",
|
|
186
|
+
detail: error instanceof AttachmentError ? error.message : "The file could not be fetched.",
|
|
187
|
+
reference: errorId
|
|
188
|
+
};
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
function multipart(files, caption) {
|
|
192
|
+
const form = new FormData();
|
|
193
|
+
form.set("chat_id", principal.attributes.chatId);
|
|
194
|
+
const thread = principal.attributes.messageThreadId;
|
|
195
|
+
if (typeof thread === "string" && /^\d+$/.test(thread)) form.set("message_thread_id", thread);
|
|
196
|
+
if (options.input.replyToMessageId !== void 0) form.set("reply_parameters", JSON.stringify({ message_id: options.input.replyToMessageId }));
|
|
197
|
+
if (files.length === 1) {
|
|
198
|
+
const file = files[0];
|
|
199
|
+
form.set(file.type, file.blob, file.filename);
|
|
200
|
+
if (caption !== void 0) form.set("caption", caption);
|
|
201
|
+
} else {
|
|
202
|
+
form.set("media", JSON.stringify(files.map((file, index) => ({
|
|
203
|
+
type: file.type,
|
|
204
|
+
media: `attach://file${index}`,
|
|
205
|
+
...index === 0 && caption !== void 0 ? { caption } : {}
|
|
206
|
+
}))));
|
|
207
|
+
files.forEach((file, index) => form.set(`file${index}`, file.blob, file.filename));
|
|
208
|
+
}
|
|
209
|
+
const method = files.length > 1 ? "sendMediaGroup" : files[0].type === "photo" ? "sendPhoto" : "sendDocument";
|
|
210
|
+
return new Request(`${TELEGRAM_PROXY_ORIGIN}/bot${TELEGRAM_PLACEHOLDER_TOKEN}/${method}`, {
|
|
211
|
+
method: "POST",
|
|
212
|
+
body: form
|
|
213
|
+
});
|
|
214
|
+
}
|
|
215
|
+
let caption = options.input.caption;
|
|
216
|
+
let cursor = 0;
|
|
217
|
+
while (cursor < uploads.length) {
|
|
218
|
+
const batch = [uploads[cursor++]];
|
|
219
|
+
let request = multipart(batch, caption);
|
|
220
|
+
let size = (await request.clone().arrayBuffer()).byteLength;
|
|
221
|
+
while (cursor < uploads.length && batch.length < 10 && uploads[cursor].type === batch[0].type) {
|
|
222
|
+
const candidate = multipart([...batch, uploads[cursor]], caption);
|
|
223
|
+
const candidateSize = (await candidate.clone().arrayBuffer()).byteLength;
|
|
224
|
+
if (candidateSize > 2097152) break;
|
|
225
|
+
batch.push(uploads[cursor++]);
|
|
226
|
+
request = candidate;
|
|
227
|
+
size = candidateSize;
|
|
228
|
+
}
|
|
229
|
+
try {
|
|
230
|
+
if (size > 2097152) throw refused("too_large", PROXY_LIMIT);
|
|
231
|
+
const response = await telegram(request);
|
|
232
|
+
const answer = await response.json();
|
|
233
|
+
if (!response.ok || !isRecord(answer) || answer.ok !== true) {
|
|
234
|
+
console.error("[segmentstream] Telegram attachment upload refused", {
|
|
235
|
+
status: response.status,
|
|
236
|
+
answer: redactDiagnostic(answer, secrets)
|
|
237
|
+
});
|
|
238
|
+
throw refused(response.status === 413 ? "too_large" : response.status === 403 ? "forbidden" : "send_failed", "Telegram refused the attachment upload.");
|
|
239
|
+
}
|
|
240
|
+
const messages = batch.length === 1 ? [answer.result] : answer.result;
|
|
241
|
+
if (!Array.isArray(messages) || messages.length !== batch.length || !messages.every((message) => isRecord(message) && Number.isSafeInteger(message.message_id))) throw refused("invalid_response", "Telegram did not return the uploaded files' message ids.");
|
|
242
|
+
batch.forEach((file, index) => {
|
|
243
|
+
results[file.index] = {
|
|
244
|
+
id: file.id,
|
|
245
|
+
filename: file.filename,
|
|
246
|
+
telegramMessageId: messages[index].message_id
|
|
247
|
+
};
|
|
248
|
+
});
|
|
249
|
+
caption = void 0;
|
|
250
|
+
} catch (error) {
|
|
251
|
+
const errorId = crypto.randomUUID();
|
|
252
|
+
console.error("[segmentstream] attachment upload failed", {
|
|
253
|
+
errorId,
|
|
254
|
+
error: redactDiagnostic(error, secrets)
|
|
255
|
+
});
|
|
256
|
+
batch.forEach((file) => {
|
|
257
|
+
results[file.index] = {
|
|
258
|
+
id: file.id,
|
|
259
|
+
filename: file.filename,
|
|
260
|
+
error: error instanceof AttachmentError ? error.code : "send_failed",
|
|
261
|
+
detail: error instanceof AttachmentError ? error.message : "The attachment upload failed.",
|
|
262
|
+
reference: errorId
|
|
263
|
+
};
|
|
264
|
+
});
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
return { files: results };
|
|
268
|
+
}
|
|
269
|
+
export { MAXIMUM_DOCUMENT_BYTES, MAXIMUM_PHOTO_BYTES, sendAttachments, sendAttachmentsInput };
|
|
@@ -1,12 +1,22 @@
|
|
|
1
|
-
import type
|
|
1
|
+
import { type AgentChat, type AgentIdentities } from "../identities.js";
|
|
2
2
|
/** How long a chat's report stands before the next message reports it again. */
|
|
3
3
|
export declare const REPORT_INTERVAL_MS: number;
|
|
4
|
+
export declare const REPORT_FAILURE_THRESHOLD = 3;
|
|
4
5
|
export interface ChatReport {
|
|
5
6
|
readonly chatId: string;
|
|
6
7
|
readonly title: string;
|
|
7
8
|
readonly memberId?: string;
|
|
8
9
|
}
|
|
9
10
|
export interface AgentChats {
|
|
11
|
+
/** The bot joined/rejoined: report immediately even if this chat was recently remembered. */
|
|
12
|
+
joined(report: ChatReport): Promise<void>;
|
|
13
|
+
/** Process-local report diagnostics for the authenticated channel health route. */
|
|
14
|
+
health(): {
|
|
15
|
+
readonly failureThreshold: number;
|
|
16
|
+
readonly totalFailures: number;
|
|
17
|
+
readonly degraded: boolean;
|
|
18
|
+
readonly chats: ReadonlyArray<ChatReportFailure>;
|
|
19
|
+
};
|
|
10
20
|
/** Reports the chat when due. Resolves when done; never rejects. */
|
|
11
21
|
seen(report: ChatReport): Promise<void>;
|
|
12
22
|
/**
|
|
@@ -37,10 +47,20 @@ export interface AgentChats {
|
|
|
37
47
|
/** The registry's latest answer for the chat, if this process has one. */
|
|
38
48
|
chat(chatId: string): AgentChat | undefined;
|
|
39
49
|
}
|
|
50
|
+
export interface ChatReportFailure {
|
|
51
|
+
readonly chatId: string;
|
|
52
|
+
readonly consecutiveFailures: number;
|
|
53
|
+
readonly lastFailedAt: string;
|
|
54
|
+
readonly status: number | null;
|
|
55
|
+
readonly code: string;
|
|
56
|
+
readonly requestId: string | null;
|
|
57
|
+
}
|
|
40
58
|
export declare function agentChats(input: {
|
|
41
59
|
readonly identities: () => AgentIdentities;
|
|
42
60
|
readonly now?: () => number;
|
|
43
61
|
readonly log?: (message: string, error: unknown) => void;
|
|
62
|
+
readonly logError?: (message: string, error: unknown) => void;
|
|
63
|
+
readonly logRecovery?: (message: string, detail: unknown) => void;
|
|
44
64
|
}): AgentChats;
|
|
45
65
|
/** The registry's facts as turn context: only the fields it holds. */
|
|
46
66
|
export declare function chatContext(chat: AgentChat | undefined): ReadonlyArray<string>;
|
|
@@ -4,13 +4,21 @@ 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 { redactDiagnostic } from "../diagnostics.mjs";
|
|
8
|
+
import { AgentIdentityError } from "../identities.mjs";
|
|
7
9
|
const MAXIMUM_WHEN_TO_USE_LENGTH = 1e3;
|
|
8
10
|
const REPORT_INTERVAL_MS = 6e5;
|
|
11
|
+
const REPORT_FAILURE_THRESHOLD = 3;
|
|
9
12
|
const MAXIMUM_CHATS = 500;
|
|
10
13
|
function agentChats(input) {
|
|
11
14
|
const chats = new Map();
|
|
15
|
+
const inFlight = new Map();
|
|
16
|
+
const failures = new Map();
|
|
17
|
+
let totalFailures = 0;
|
|
12
18
|
const now = input.now ?? Date.now;
|
|
13
19
|
const log = input.log ?? ((message, error) => console.warn(message, error));
|
|
20
|
+
const logError = input.logError ?? ((message, error) => console.error(message, error));
|
|
21
|
+
const logRecovery = input.logRecovery ?? ((message, detail) => console.info(message, detail));
|
|
14
22
|
function remember(chatId, value) {
|
|
15
23
|
chats.delete(chatId);
|
|
16
24
|
chats.set(chatId, value);
|
|
@@ -35,17 +43,65 @@ function agentChats(input) {
|
|
|
35
43
|
...chats.get(chatId) ?? fallback,
|
|
36
44
|
chat
|
|
37
45
|
});
|
|
46
|
+
const previousFailure = failures.get(chatId);
|
|
47
|
+
failures.delete(chatId);
|
|
48
|
+
if ((previousFailure?.consecutiveFailures ?? 0) >= 3) logRecovery("The agent's chat registry report recovered.", {
|
|
49
|
+
event: "segmentstream.telegram.registry_recovered",
|
|
50
|
+
chatId,
|
|
51
|
+
consecutiveFailures: previousFailure.consecutiveFailures
|
|
52
|
+
});
|
|
38
53
|
return true;
|
|
39
54
|
} catch (error) {
|
|
40
|
-
|
|
55
|
+
totalFailures += 1;
|
|
56
|
+
const failure = {
|
|
57
|
+
chatId,
|
|
58
|
+
consecutiveFailures: (failures.get(chatId)?.consecutiveFailures ?? 0) + 1,
|
|
59
|
+
lastFailedAt: new Date(now()).toISOString(),
|
|
60
|
+
status: error instanceof AgentIdentityError ? error.status ?? null : null,
|
|
61
|
+
code: error instanceof AgentIdentityError ? error.code : "unavailable",
|
|
62
|
+
requestId: error instanceof AgentIdentityError ? error.requestId ?? null : null
|
|
63
|
+
};
|
|
64
|
+
failures.delete(chatId);
|
|
65
|
+
failures.set(chatId, failure);
|
|
66
|
+
if (failures.size > MAXIMUM_CHATS) failures.delete(failures.keys().next().value);
|
|
67
|
+
const detail = {
|
|
68
|
+
event: "segmentstream.telegram.registry_report_failed",
|
|
69
|
+
...failure,
|
|
70
|
+
totalFailures,
|
|
71
|
+
error: redactDiagnostic(error, new Set())
|
|
72
|
+
};
|
|
73
|
+
if (failure.consecutiveFailures < 3) log("The agent could not report the chat to the ontology's registry.", detail);
|
|
74
|
+
else if (failure.consecutiveFailures === 3) logError("The agent's chat registry reports repeatedly failed.", detail);
|
|
41
75
|
return false;
|
|
42
76
|
}
|
|
43
77
|
}
|
|
44
78
|
return {
|
|
79
|
+
async joined({ chatId, title }) {
|
|
80
|
+
const known = chats.get(chatId);
|
|
81
|
+
const remembered = {
|
|
82
|
+
reportedAt: now(),
|
|
83
|
+
members: known?.members ?? new Set(),
|
|
84
|
+
chat: known?.chat
|
|
85
|
+
};
|
|
86
|
+
remember(chatId, remembered);
|
|
87
|
+
if (!await report(chatId, remembered, { title })) restore(chatId, remembered, known);
|
|
88
|
+
},
|
|
89
|
+
health() {
|
|
90
|
+
const failedChats = [...failures.values()];
|
|
91
|
+
return {
|
|
92
|
+
failureThreshold: 3,
|
|
93
|
+
totalFailures,
|
|
94
|
+
degraded: failedChats.some((failure) => failure.consecutiveFailures >= 3),
|
|
95
|
+
chats: failedChats
|
|
96
|
+
};
|
|
97
|
+
},
|
|
45
98
|
async seen({ chatId, title, memberId }) {
|
|
46
99
|
const known = chats.get(chatId);
|
|
47
100
|
const at = now();
|
|
48
|
-
if (!(known === void 0 || at - known.reportedAt >= 6e5 || memberId !== void 0 && !known.members.has(memberId)))
|
|
101
|
+
if (!(known === void 0 || at - known.reportedAt >= 6e5 || memberId !== void 0 && !known.members.has(memberId))) {
|
|
102
|
+
await inFlight.get(chatId);
|
|
103
|
+
return;
|
|
104
|
+
}
|
|
49
105
|
const members = new Set(known?.members);
|
|
50
106
|
if (memberId !== void 0) members.add(memberId);
|
|
51
107
|
const written = {
|
|
@@ -54,14 +110,22 @@ function agentChats(input) {
|
|
|
54
110
|
chat: known?.chat
|
|
55
111
|
};
|
|
56
112
|
remember(chatId, written);
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
113
|
+
const task = (async () => {
|
|
114
|
+
if (!await report(chatId, {
|
|
115
|
+
reportedAt: at,
|
|
116
|
+
members,
|
|
117
|
+
chat: void 0
|
|
118
|
+
}, {
|
|
119
|
+
title,
|
|
120
|
+
...memberId === void 0 ? {} : { memberId }
|
|
121
|
+
})) restore(chatId, written, known);
|
|
122
|
+
})();
|
|
123
|
+
inFlight.set(chatId, task);
|
|
124
|
+
try {
|
|
125
|
+
await task;
|
|
126
|
+
} finally {
|
|
127
|
+
if (inFlight.get(chatId) === task) inFlight.delete(chatId);
|
|
128
|
+
}
|
|
65
129
|
},
|
|
66
130
|
async left({ chatId, title, memberId }) {
|
|
67
131
|
const known = chats.get(chatId);
|
|
@@ -120,4 +184,4 @@ function chatContext(chat) {
|
|
|
120
184
|
lines.push("</chat>");
|
|
121
185
|
return [lines.join("\n")];
|
|
122
186
|
}
|
|
123
|
-
export { REPORT_INTERVAL_MS, agentChats, chatContext };
|
|
187
|
+
export { REPORT_FAILURE_THRESHOLD, REPORT_INTERVAL_MS, agentChats, chatContext };
|
|
@@ -6,8 +6,8 @@ __eveDirname(__filename);
|
|
|
6
6
|
__eveCreateRequire(import.meta.url);
|
|
7
7
|
import { memberIdOf, telegramPrincipal } from "./principal.mjs";
|
|
8
8
|
import { confirmations } from "../confirmations.mjs";
|
|
9
|
-
import { answerMarker, isPendingQuestion, pendingQuestions, questionKey } from "../questions.mjs";
|
|
10
9
|
import { currentInboundRequest } from "./inbound-request.mjs";
|
|
10
|
+
import { answerMarker, isPendingQuestion, pendingQuestions, questionKey } from "../questions.mjs";
|
|
11
11
|
import { formatAddress, scopedAddress } from "./sessions.mjs";
|
|
12
12
|
import { registerTelegramFreeformPrompt, renderTelegramInputRequest } from "eve/channels/telegram";
|
|
13
13
|
import { createHash } from "node:crypto";
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import type { AgentIdentities } from "../identities.js";
|
|
2
|
+
import { type Conversations } from "../conversations.js";
|
|
3
|
+
import type { AgentChats } from "./chats.js";
|
|
4
|
+
import type { ObservedMessages } from "./observed.js";
|
|
5
|
+
import type { AddressingReason, RouteDecider } from "./router.js";
|
|
6
|
+
/** Runs after webhook verification, before eve drops bot/channel/service updates. */
|
|
7
|
+
export declare function captureTelegramWebhook(input: {
|
|
8
|
+
readonly request: Request;
|
|
9
|
+
readonly archive: Conversations;
|
|
10
|
+
readonly identities: AgentIdentities;
|
|
11
|
+
readonly observed: ObservedMessages;
|
|
12
|
+
readonly chats?: AgentChats;
|
|
13
|
+
readonly next: () => Promise<Response>;
|
|
14
|
+
}): Promise<Response>;
|
|
15
|
+
/** Additive inbound hook: the router keeps ownership of the reply policy. */
|
|
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;
|
|
18
|
+
export declare function captureRouteDecider(decide: RouteDecider): RouteDecider;
|
|
19
|
+
/** Captures every Telegram sendMessage chunk, including pairing/HITL/proactive replies. */
|
|
20
|
+
export declare function captureTelegramFetch(fetchImpl: typeof fetch, archive: Conversations, identities: () => AgentIdentities): typeof fetch;
|
|
21
|
+
/** Reuses the request's verified lookup; it never survives into another inbound request. */
|
|
22
|
+
export declare function conversationIdentities(identities: AgentIdentities): AgentIdentities;
|
|
23
|
+
/** Verification and failure acknowledgement stay outside eve's swallowed onMessage failures. */
|
|
24
|
+
export declare function telegramConversationCapture(input: {
|
|
25
|
+
readonly archive: Conversations;
|
|
26
|
+
readonly identities: () => AgentIdentities;
|
|
27
|
+
readonly observed: ObservedMessages;
|
|
28
|
+
readonly chats: AgentChats;
|
|
29
|
+
readonly verify: (request: Request) => boolean;
|
|
30
|
+
}): (request: Request, next: () => Promise<Response>) => Promise<Response>;
|