@segmentstream/agent 0.5.4 → 0.6.4
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 +78 -18
- package/dist/extension/connections/ontology.mjs +2 -2
- package/dist/extension/instructions.md +10 -9
- package/dist/extension/lib/attachment-tools.mjs +1 -3
- package/dist/extension/lib/chat-post-confirmation.d.ts +23 -0
- package/dist/extension/lib/chat-post-confirmation.mjs +61 -0
- package/dist/extension/lib/confirmation-description.d.ts +2 -0
- package/dist/extension/lib/confirmations.d.ts +5 -0
- package/dist/extension/lib/confirmations.mjs +25 -5
- package/dist/extension/lib/conversations.d.ts +12 -1
- package/dist/extension/lib/conversations.mjs +4 -3
- package/dist/extension/lib/ontology-connection.d.ts +4 -0
- package/dist/extension/lib/ontology-connection.mjs +46 -3
- package/dist/extension/lib/ontology-tools.d.ts +1 -0
- package/dist/extension/lib/ontology-tools.mjs +27 -2
- package/dist/extension/lib/telegram/adapter.d.ts +1 -1
- package/dist/extension/lib/telegram/adapter.mjs +18 -28
- package/dist/extension/lib/telegram/attachments-send.d.ts +7 -3
- package/dist/extension/lib/telegram/attachments-send.mjs +25 -7
- package/dist/extension/lib/telegram/chat-post-card.d.ts +15 -0
- package/dist/extension/lib/telegram/chat-post-card.mjs +65 -0
- package/dist/extension/lib/telegram/confirmations.d.ts +3 -2
- package/dist/extension/lib/telegram/confirmations.mjs +140 -21
- package/dist/extension/lib/telegram/conversations.mjs +5 -3
- package/dist/extension/lib/telegram/customer-text.d.ts +13 -1
- package/dist/extension/lib/telegram/customer-text.mjs +108 -7
- package/dist/extension/lib/telegram/format.d.ts +17 -0
- package/dist/extension/lib/telegram/format.mjs +171 -0
- package/dist/extension/lib/telegram/health.mjs +1 -1
- package/dist/extension/lib/telegram/inbound-request.d.ts +1 -1
- package/dist/extension/lib/telegram/proxy-fetch.d.ts +1 -1
- package/dist/extension/lib/telegram/proxy-fetch.mjs +14 -6
- package/dist/extension/lib/telegram/runtime-request.d.ts +8 -0
- package/dist/extension/lib/telegram/runtime-request.mjs +36 -0
- package/dist/extension/lib/telegram/send-queue.d.ts +4 -0
- package/dist/extension/lib/telegram/send-queue.mjs +35 -0
- package/dist/extension/lib/telegram/silent.d.ts +3 -3
- package/dist/extension/lib/telegram/silent.mjs +19 -4
- package/package.json +2 -1
- package/dist/extension/lib/telegram/requests.d.ts +0 -80
- package/dist/extension/lib/telegram/requests.mjs +0 -186
|
@@ -14,6 +14,7 @@ import { agentIdentities } from "../identities.mjs";
|
|
|
14
14
|
import { conversations } from "../conversations.mjs";
|
|
15
15
|
import { observedMessages } from "./observed.mjs";
|
|
16
16
|
import { InboundRequest, currentInboundRequest, runInboundRequest, withReplacedText } from "./inbound-request.mjs";
|
|
17
|
+
import { telegramSendScope } from "./send-queue.mjs";
|
|
17
18
|
import { failureText, reportTelegramError, safeTelegramFetch, withDeliveryLocale } from "./customer-text.mjs";
|
|
18
19
|
import { captureRouteDecider, captureTelegramFetch, conversationIdentities, telegramConversationCapture } from "./conversations.mjs";
|
|
19
20
|
import { runtimeTokenVerifier } from "../runtime-identity.mjs";
|
|
@@ -29,7 +30,6 @@ import { replyDelivery } from "./silent.mjs";
|
|
|
29
30
|
import { downloadTelegramFile, gatewayTranscriber } from "./voice.mjs";
|
|
30
31
|
import { handleInboundMessage } from "./inbound.mjs";
|
|
31
32
|
import { TELEGRAM_PLACEHOLDER_TOKEN, TELEGRAM_PROXY_ORIGIN, telegramProxyFetch } from "./proxy-fetch.mjs";
|
|
32
|
-
import { messagesRoute } from "./requests.mjs";
|
|
33
33
|
import { healthRoute } from "./health.mjs";
|
|
34
34
|
import { telegramFailureEvents } from "./turn-errors.mjs";
|
|
35
35
|
import { telegramWebhookVerifier } from "./webhook.mjs";
|
|
@@ -128,7 +128,7 @@ function segmentstreamTelegram(options) {
|
|
|
128
128
|
api: {
|
|
129
129
|
apiBaseUrl: TELEGRAM_PROXY_ORIGIN,
|
|
130
130
|
fileBaseUrl: TELEGRAM_PROXY_ORIGIN,
|
|
131
|
-
fetch: (input, init) => safeTelegramFetch(captureTelegramFetch(telegramProxyFetch(proxy()), archive, pairing))(input, init)
|
|
131
|
+
fetch: (input, init) => safeTelegramFetch(captureTelegramFetch(telegramProxyFetch(proxy(), telegramSendScope(environment, settings().connection)), archive, pairing), telegramSendScope(environment, settings().connection))(input, init)
|
|
132
132
|
},
|
|
133
133
|
credentials: {
|
|
134
134
|
botToken: TELEGRAM_PLACEHOLDER_TOKEN,
|
|
@@ -228,8 +228,8 @@ function segmentstreamTelegram(options) {
|
|
|
228
228
|
},
|
|
229
229
|
events: {
|
|
230
230
|
...telegramFailureEvents,
|
|
231
|
-
...telegramConfirmationEvents(confirmationRenderer({ model: () => options.models().decision })),
|
|
232
|
-
"message.completed": replyDelivery(observed)
|
|
231
|
+
...telegramConfirmationEvents(confirmationRenderer({ model: () => options.models().decision }), archive),
|
|
232
|
+
"message.completed": replyDelivery(observed, () => telegramSendScope(environment, settings().connection))
|
|
233
233
|
}
|
|
234
234
|
});
|
|
235
235
|
const adapter = channel.adapter;
|
|
@@ -263,28 +263,17 @@ function segmentstreamTelegram(options) {
|
|
|
263
263
|
}, context.state.messageThreadId === null ? void 0 : String(context.state.messageThreadId));
|
|
264
264
|
}]))
|
|
265
265
|
} },
|
|
266
|
-
routes: [
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
verify: () => verify ??= runtimeTokenVerifier(runtimeIdentityEnvironment(environment)),
|
|
278
|
-
sessionPolicy: policy,
|
|
279
|
-
scope: () => options.sessions().scope
|
|
280
|
-
}),
|
|
281
|
-
healthRoute({
|
|
282
|
-
chats,
|
|
283
|
-
enabled: () => channelsEnabled(environment),
|
|
284
|
-
configured: () => options.settings() !== null,
|
|
285
|
-
verify: () => verify ??= runtimeTokenVerifier(runtimeIdentityEnvironment(environment))
|
|
286
|
-
})
|
|
287
|
-
]
|
|
266
|
+
routes: [...channel.routes.map((route) => gatedRoute({
|
|
267
|
+
route,
|
|
268
|
+
options,
|
|
269
|
+
environment,
|
|
270
|
+
capture
|
|
271
|
+
})), healthRoute({
|
|
272
|
+
chats,
|
|
273
|
+
enabled: () => channelsEnabled(environment),
|
|
274
|
+
configured: () => options.settings() !== null,
|
|
275
|
+
verify: () => verify ??= runtimeTokenVerifier(runtimeIdentityEnvironment(environment))
|
|
276
|
+
})]
|
|
288
277
|
};
|
|
289
278
|
}
|
|
290
279
|
function sessionIdOf(context) {
|
|
@@ -305,9 +294,10 @@ function gatedRoute(input) {
|
|
|
305
294
|
resolveSession: args.resolveSession,
|
|
306
295
|
from: args.from
|
|
307
296
|
};
|
|
308
|
-
inbound.respond = (address, requestId, decision, auth) => args.from(scoped(address)).respond([{
|
|
297
|
+
inbound.respond = (address, requestId, decision, auth, text) => args.from(scoped(address)).respond([{
|
|
309
298
|
requestId,
|
|
310
|
-
optionId: decision
|
|
299
|
+
optionId: decision,
|
|
300
|
+
...text === void 0 ? {} : { text }
|
|
311
301
|
}], { auth });
|
|
312
302
|
const tasks = [];
|
|
313
303
|
const waitUntil = (task) => {
|
|
@@ -24,7 +24,7 @@ export interface SentFile {
|
|
|
24
24
|
/** Bare ids are document records; explicit references select other entities and file properties. */
|
|
25
25
|
export declare function sendAttachmentsInput(value: Record<string, unknown>): SendAttachmentsInput;
|
|
26
26
|
/** All destinations and credentials come from the channel's turn and mounted configuration. */
|
|
27
|
-
|
|
27
|
+
interface SendOptions {
|
|
28
28
|
readonly input: SendAttachmentsInput;
|
|
29
29
|
readonly principal: SessionAuthContext | null;
|
|
30
30
|
readonly sessionId: string;
|
|
@@ -32,10 +32,14 @@ export declare function sendAttachments(options: {
|
|
|
32
32
|
readonly environment?: Environment;
|
|
33
33
|
readonly token?: RuntimeToken;
|
|
34
34
|
readonly fetch?: typeof fetch;
|
|
35
|
-
}
|
|
35
|
+
}
|
|
36
|
+
type SendResult = {
|
|
36
37
|
readonly files: ReadonlyArray<SentFile>;
|
|
37
38
|
} | {
|
|
38
39
|
readonly error: string;
|
|
39
40
|
readonly detail: string;
|
|
40
41
|
readonly reference: string;
|
|
41
|
-
}
|
|
42
|
+
};
|
|
43
|
+
/** Reserve the chat before downloading, and keep every album/batch in the same queue slot. */
|
|
44
|
+
export declare function sendAttachments(options: SendOptions): Promise<SendResult>;
|
|
45
|
+
export {};
|
|
@@ -11,7 +11,8 @@ import { redactDiagnostic } from "../diagnostics.mjs";
|
|
|
11
11
|
import { runtimeToken } from "../runtime-token.mjs";
|
|
12
12
|
import { ACT_AS_HEADER } from "../ontology-connection.mjs";
|
|
13
13
|
import { AttachmentError, CONVERSATION_HEADER, SESSION_HEADER } from "../attachments.mjs";
|
|
14
|
-
import {
|
|
14
|
+
import { inTelegramSendQueue, telegramSendScope } from "./send-queue.mjs";
|
|
15
|
+
import { customerCopy, customerMessage, formattingRefused, reportTelegramError } from "./customer-text.mjs";
|
|
15
16
|
import { confirmationLocale } from "./confirmation-text.mjs";
|
|
16
17
|
import { TELEGRAM_PLACEHOLDER_TOKEN, TELEGRAM_PROXY_ORIGIN, telegramProxyFetch } from "./proxy-fetch.mjs";
|
|
17
18
|
import { attachmentOwner } from "../attachment-tools.mjs";
|
|
@@ -74,7 +75,11 @@ async function boundedDownload(response, maximum) {
|
|
|
74
75
|
}
|
|
75
76
|
return new Uint8Array(Buffer.concat(chunks));
|
|
76
77
|
}
|
|
77
|
-
|
|
78
|
+
function sendAttachments(options) {
|
|
79
|
+
const chatId = options.principal?.attributes.chatId;
|
|
80
|
+
return typeof chatId === "string" && options.connection !== void 0 ? inTelegramSendQueue(telegramSendScope(options.environment ?? process.env, options.connection), chatId, () => sendAttachmentsInOrder(options)) : sendAttachmentsInOrder(options);
|
|
81
|
+
}
|
|
82
|
+
async function sendAttachmentsInOrder(options) {
|
|
78
83
|
const environment = options.environment ?? process.env;
|
|
79
84
|
const principal = options.principal;
|
|
80
85
|
const locale = confirmationLocale(principal);
|
|
@@ -117,7 +122,7 @@ async function sendAttachments(options) {
|
|
|
117
122
|
connection: options.connection,
|
|
118
123
|
token,
|
|
119
124
|
fetch: fetchImpl
|
|
120
|
-
}));
|
|
125
|
+
}), telegramSendScope(environment, options.connection));
|
|
121
126
|
const results = options.input.documentIds.map(({ id }) => ({
|
|
122
127
|
id,
|
|
123
128
|
filename: ""
|
|
@@ -196,12 +201,18 @@ async function sendAttachments(options) {
|
|
|
196
201
|
if (files.length === 1) {
|
|
197
202
|
const file = files[0];
|
|
198
203
|
form.set(file.type, file.blob, file.filename);
|
|
199
|
-
if (caption !== void 0)
|
|
204
|
+
if (caption !== void 0) {
|
|
205
|
+
form.set("caption", caption.text);
|
|
206
|
+
if (caption.entities.length > 0) form.set("caption_entities", JSON.stringify(caption.entities));
|
|
207
|
+
}
|
|
200
208
|
} else {
|
|
201
209
|
form.set("media", JSON.stringify(files.map((file, index) => ({
|
|
202
210
|
type: file.type,
|
|
203
211
|
media: `attach://file${index}`,
|
|
204
|
-
...index === 0 && caption !== void 0 ? {
|
|
212
|
+
...index === 0 && caption !== void 0 ? {
|
|
213
|
+
caption: caption.text,
|
|
214
|
+
...caption.entities.length > 0 ? { caption_entities: caption.entities } : {}
|
|
215
|
+
} : {}
|
|
205
216
|
}))));
|
|
206
217
|
files.forEach((file, index) => form.set(`file${index}`, file.blob, file.filename));
|
|
207
218
|
}
|
|
@@ -211,7 +222,7 @@ async function sendAttachments(options) {
|
|
|
211
222
|
body: form
|
|
212
223
|
});
|
|
213
224
|
}
|
|
214
|
-
let caption = options.input.caption === void 0 ? void 0 :
|
|
225
|
+
let caption = options.input.caption === void 0 ? void 0 : customerMessage(options.input.caption, locale);
|
|
215
226
|
let cursor = 0;
|
|
216
227
|
while (cursor < uploads.length) {
|
|
217
228
|
const batch = [uploads[cursor++]];
|
|
@@ -227,7 +238,14 @@ async function sendAttachments(options) {
|
|
|
227
238
|
}
|
|
228
239
|
try {
|
|
229
240
|
if (size > 2097152) throw refused("too_large", PROXY_LIMIT);
|
|
230
|
-
|
|
241
|
+
let response = await telegram(request);
|
|
242
|
+
if (caption !== void 0 && caption.entities.length > 0 && await formattingRefused(response)) {
|
|
243
|
+
caption = {
|
|
244
|
+
text: caption.text,
|
|
245
|
+
entities: []
|
|
246
|
+
};
|
|
247
|
+
response = await telegram(multipart(batch, caption));
|
|
248
|
+
}
|
|
231
249
|
const answer = await response.json();
|
|
232
250
|
if (!response.ok || !isRecord(answer) || answer.ok !== true) {
|
|
233
251
|
console.error("[segmentstream] Telegram attachment upload refused", {
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import type { ChatPostPreview } from "../chat-post-confirmation.js";
|
|
2
|
+
import type { ConfirmationText } from "./confirmation-text.js";
|
|
3
|
+
/** Only the frozen deterministic card may bypass the generic technical-prose filter. */
|
|
4
|
+
export declare function withChatPostCard<T>(text: string, run: () => T): T;
|
|
5
|
+
export declare function isExactChatPostCard(text: string): boolean;
|
|
6
|
+
export declare function chatPostCopy(locale: string): {
|
|
7
|
+
edit: string;
|
|
8
|
+
changes: string;
|
|
9
|
+
uncertain: string;
|
|
10
|
+
sent: string;
|
|
11
|
+
history: string;
|
|
12
|
+
expired: string;
|
|
13
|
+
};
|
|
14
|
+
export declare function chatPostCard(preview: ChatPostPreview, locale: string): ConfirmationText;
|
|
15
|
+
export declare function chatPostReceipt(output: unknown, locale: string): string | undefined;
|
|
@@ -0,0 +1,65 @@
|
|
|
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 { AsyncLocalStorage } from "node:async_hooks";
|
|
8
|
+
const MAX_PREVIEW_LENGTH = 3584;
|
|
9
|
+
const exactText = new AsyncLocalStorage();
|
|
10
|
+
function withChatPostCard(text, run) {
|
|
11
|
+
if (text.length > 4096) throw new Error("The complete chat confirmation cannot fit in one Telegram message.");
|
|
12
|
+
return exactText.run(text, run);
|
|
13
|
+
}
|
|
14
|
+
function isExactChatPostCard(text) {
|
|
15
|
+
return exactText.getStore() === text;
|
|
16
|
+
}
|
|
17
|
+
function chatPostCopy(locale) {
|
|
18
|
+
return locale.startsWith("ru") ? {
|
|
19
|
+
edit: "Изменить",
|
|
20
|
+
changes: "Что изменить в сообщении?",
|
|
21
|
+
uncertain: "Неизвестно, отправлено ли сообщение. Не отправляйте его повторно автоматически.",
|
|
22
|
+
sent: "Отправлено.",
|
|
23
|
+
history: "Не удалось записать квитанцию в историю.",
|
|
24
|
+
expired: "Срок подтверждения истёк. Ничего не отправлено."
|
|
25
|
+
} : {
|
|
26
|
+
edit: "Edit",
|
|
27
|
+
changes: "What would you like to change in the message?",
|
|
28
|
+
uncertain: "Delivery is uncertain. Do not resend automatically.",
|
|
29
|
+
sent: "Sent.",
|
|
30
|
+
history: "The receipt could not be recorded in history.",
|
|
31
|
+
expired: "This confirmation expired. Nothing was sent."
|
|
32
|
+
};
|
|
33
|
+
}
|
|
34
|
+
function chatPostCard(preview, locale) {
|
|
35
|
+
const russian = locale.startsWith("ru");
|
|
36
|
+
const text = [
|
|
37
|
+
russian ? "Отправить сообщение?" : "Send message?",
|
|
38
|
+
`${russian ? "Чат" : "Chat"}: ${preview.title} (${preview.provider})`,
|
|
39
|
+
...preview.replyTo === null ? [] : [`${russian ? "В ответ на" : "Reply to"}: ${preview.replyTo}`],
|
|
40
|
+
"",
|
|
41
|
+
preview.finalText
|
|
42
|
+
].join("\n");
|
|
43
|
+
if (text.length > MAX_PREVIEW_LENGTH) throw new Error("The full message and receipt cannot fit in one Telegram confirmation. Make a shorter draft.");
|
|
44
|
+
return {
|
|
45
|
+
text,
|
|
46
|
+
confirm: russian ? "Отправить" : "Confirm",
|
|
47
|
+
cancel: russian ? "Отмена" : "Cancel",
|
|
48
|
+
executing: russian ? "Подтверждено." : "Confirmed.",
|
|
49
|
+
cancelled: russian ? "Отменено." : "Cancelled.",
|
|
50
|
+
completed: chatPostCopy(locale).sent,
|
|
51
|
+
failed: russian ? "Ошибка отправки сообщения." : "Chat posting failed."
|
|
52
|
+
};
|
|
53
|
+
}
|
|
54
|
+
function chatPostReceipt(output, locale) {
|
|
55
|
+
const value = typeof output === "object" && output !== null ? output : {};
|
|
56
|
+
if (value.status !== "sent" && value.status !== "uncertain" || typeof value.history_recorded !== "boolean" || typeof value.duplicate !== "boolean" || value.message_id !== null && typeof value.message_id !== "string" || value.url !== null && typeof value.url !== "string") return;
|
|
57
|
+
const copy = chatPostCopy(locale);
|
|
58
|
+
const url = typeof value.url === "string" && value.url.length <= 256 && /^https:\/\/t\.me\/[A-Za-z0-9_/-]+$/u.test(value.url) ? value.url : null;
|
|
59
|
+
return [
|
|
60
|
+
value.status === "sent" ? copy.sent : copy.uncertain,
|
|
61
|
+
...url === null ? [] : [url],
|
|
62
|
+
...value.history_recorded ? [] : [copy.history]
|
|
63
|
+
].join("\n");
|
|
64
|
+
}
|
|
65
|
+
export { chatPostCard, chatPostCopy, chatPostReceipt, isExactChatPostCard, withChatPostCard };
|
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
import type { ChannelEvents } from "eve/channels";
|
|
2
2
|
import type { TelegramCallbackQuery, TelegramChannelEvents, TelegramContext, TelegramEventContext } from "eve/channels/telegram";
|
|
3
|
+
import { type Conversations } from "../conversations.js";
|
|
3
4
|
import type { AgentIdentities } from "../identities.js";
|
|
4
5
|
import { type PendingQuestion } from "../questions.js";
|
|
5
6
|
import { type ConfirmationRenderer } from "./confirmation-text.js";
|
|
6
7
|
import { type HistoryReader, type RecordedEvent } from "./sessions.js";
|
|
7
|
-
export declare function confirmationCallback(requestId: string, memberId: string, decision: "approve" | "cancel"): string;
|
|
8
|
+
export declare function confirmationCallback(requestId: string, memberId: string, decision: "approve" | "cancel" | "edit"): string;
|
|
8
9
|
export declare function questionFromHistory(events: readonly RecordedEvent[], owner?: Pick<PendingQuestion, "memberId" | "chatId" | "messageThreadId">): PendingQuestion | undefined;
|
|
9
10
|
export declare function questionAddress(owner: Pick<PendingQuestion, "memberId" | "chatId" | "messageThreadId">): string;
|
|
10
11
|
/** Custom callback data avoids Eve's default, unpaired Telegram callback principal. */
|
|
@@ -16,5 +17,5 @@ export declare function handleConfirmationCallback(input: {
|
|
|
16
17
|
readonly scope: "chat" | "thread";
|
|
17
18
|
readonly now?: number;
|
|
18
19
|
}): Promise<void>;
|
|
19
|
-
export declare function telegramConfirmationEvents(render?: ConfirmationRenderer): TelegramChannelEvents & Pick<ChannelEvents<TelegramEventContext>, "context.cleared">;
|
|
20
|
+
export declare function telegramConfirmationEvents(render?: ConfirmationRenderer, archive?: Conversations): TelegramChannelEvents & Pick<ChannelEvents<TelegramEventContext>, "context.cleared">;
|
|
20
21
|
export declare const confirmationEvents: TelegramChannelEvents & Pick<ChannelEvents<TelegramEventContext>, "context.cleared">;
|
|
@@ -5,7 +5,10 @@ const __filename = __eveFileURLToPath(import.meta.url);
|
|
|
5
5
|
__eveDirname(__filename);
|
|
6
6
|
__eveCreateRequire(import.meta.url);
|
|
7
7
|
import { conversationLanguage, memberIdOf, telegramPrincipal } from "./principal.mjs";
|
|
8
|
+
import { chatPostOperationId, isChatPost } from "../chat-post-confirmation.mjs";
|
|
9
|
+
import { chatPostCard, chatPostCopy, chatPostReceipt, withChatPostCard } from "./chat-post-card.mjs";
|
|
8
10
|
import { confirmations } from "../confirmations.mjs";
|
|
11
|
+
import { conversationEvent } from "../conversations.mjs";
|
|
9
12
|
import { currentInboundRequest } from "./inbound-request.mjs";
|
|
10
13
|
import { currentLocale, failureText, reportTelegramError } from "./customer-text.mjs";
|
|
11
14
|
import { answerMarker, isPendingQuestion, pendingQuestions, questionKey } from "../questions.mjs";
|
|
@@ -18,7 +21,8 @@ function shortHash(value) {
|
|
|
18
21
|
return createHash("sha256").update(value).digest("hex").slice(0, 20);
|
|
19
22
|
}
|
|
20
23
|
function confirmationCallback(requestId, memberId, decision) {
|
|
21
|
-
|
|
24
|
+
const choice = decision === "approve" ? "y" : decision === "edit" ? "e" : "n";
|
|
25
|
+
return `${PREFIX}${shortHash(requestId)}:${shortHash(memberId)}:${choice}`;
|
|
22
26
|
}
|
|
23
27
|
function object(value) {
|
|
24
28
|
return typeof value === "object" && value !== null ? value : {};
|
|
@@ -34,7 +38,9 @@ function openConfirmations(events) {
|
|
|
34
38
|
if (request.kind === "tool-approval" && typeof request.requestId === "string" && typeof action.callId === "string") open.set(request.requestId, {
|
|
35
39
|
requestId: request.requestId,
|
|
36
40
|
callId: action.callId,
|
|
37
|
-
at: Date.parse(event.meta.at)
|
|
41
|
+
at: Date.parse(event.meta.at),
|
|
42
|
+
toolName: String(action.toolName ?? ""),
|
|
43
|
+
input: object(action.input)
|
|
38
44
|
});
|
|
39
45
|
}
|
|
40
46
|
if (event.type === "input.resolved" && Array.isArray(data.resolutions)) for (const item of data.resolutions) {
|
|
@@ -49,6 +55,10 @@ function questionFromHistory(events, owner) {
|
|
|
49
55
|
for (const event of events) {
|
|
50
56
|
const data = object(event.data);
|
|
51
57
|
if (event.type === "context.cleared") pending.clear();
|
|
58
|
+
if (event.type === "input.resolved" && Array.isArray(data.resolutions)) for (const resolution of data.resolutions) {
|
|
59
|
+
const question = chatPostEditQuestion(object(resolution));
|
|
60
|
+
if (question !== void 0) pending.set(questionKey(question), question);
|
|
61
|
+
}
|
|
52
62
|
if (event.type === "action.result") {
|
|
53
63
|
const result = object(data.result);
|
|
54
64
|
const candidate = object(result.output).pendingQuestion;
|
|
@@ -60,6 +70,16 @@ function questionFromHistory(events, owner) {
|
|
|
60
70
|
}
|
|
61
71
|
return owner === void 0 ? [...pending.values()].at(-1) : pending.get(questionKey(owner));
|
|
62
72
|
}
|
|
73
|
+
function chatPostEditQuestion(resolution) {
|
|
74
|
+
const text = object(resolution.response).text;
|
|
75
|
+
if (resolution.outcome !== "cancelled" || typeof text !== "string") return;
|
|
76
|
+
try {
|
|
77
|
+
const question = object(JSON.parse(text)).pendingChatPostEdit;
|
|
78
|
+
return isPendingQuestion(question) ? question : void 0;
|
|
79
|
+
} catch {
|
|
80
|
+
return;
|
|
81
|
+
}
|
|
82
|
+
}
|
|
63
83
|
function questionAddress(owner) {
|
|
64
84
|
return formatAddress({
|
|
65
85
|
...owner,
|
|
@@ -111,12 +131,17 @@ async function handleConfirmationCallback(input) {
|
|
|
111
131
|
}), input.scope);
|
|
112
132
|
const session = await (currentInboundRequest()?.sessions)?.resolveSession(address);
|
|
113
133
|
const prompt = session === void 0 ? void 0 : openConfirmations(await input.read(session.id)).find((candidate) => shortHash(candidate.requestId) === requestHash);
|
|
114
|
-
if (prompt === void 0 ||
|
|
134
|
+
if (prompt === void 0 || ![
|
|
135
|
+
"y",
|
|
136
|
+
"n",
|
|
137
|
+
"e"
|
|
138
|
+
].includes(choice ?? "") || choice === "e" && !isChatPost(prompt.toolName)) {
|
|
115
139
|
await notice("This confirmation is no longer pending.");
|
|
116
140
|
return;
|
|
117
141
|
}
|
|
118
142
|
const expired = !Number.isFinite(prompt.at) || prompt.at + 6e5 <= (input.now ?? Date.now());
|
|
119
|
-
const decision = expired || choice
|
|
143
|
+
const decision = expired || choice !== "y" ? "cancel" : "approve";
|
|
144
|
+
const edit = !expired && choice === "e";
|
|
120
145
|
const principal = telegramPrincipal({
|
|
121
146
|
member: lookup.member,
|
|
122
147
|
message: {
|
|
@@ -130,17 +155,58 @@ async function handleConfirmationCallback(input) {
|
|
|
130
155
|
});
|
|
131
156
|
const source = currentInboundRequest()?.respond;
|
|
132
157
|
if (source === void 0) throw new Error("Confirmation callback has no session continuation.");
|
|
158
|
+
const editQuestion = edit ? {
|
|
159
|
+
id: `edit-${prompt.callId}`,
|
|
160
|
+
memberId: lookup.member.id,
|
|
161
|
+
chatId: query.message.chat.id,
|
|
162
|
+
messageThreadId: String(query.message.messageThreadId ?? ""),
|
|
163
|
+
address,
|
|
164
|
+
question: chatPostCopy(language).changes,
|
|
165
|
+
task: `The previous chat post was cancelled for editing. Apply the member's changes to this draft, then make a NEW chat_post_message call and confirmation. Never reuse the cancelled call. Previous input: ${JSON.stringify(prompt.input)}`
|
|
166
|
+
} : void 0;
|
|
133
167
|
await source(address, prompt.requestId, decision, {
|
|
134
168
|
...principal,
|
|
135
169
|
attributes: {
|
|
136
170
|
...principal.attributes,
|
|
137
171
|
confirmationRequestId: prompt.requestId,
|
|
138
|
-
confirmationDecision: decision
|
|
172
|
+
confirmationDecision: decision,
|
|
173
|
+
...expired ? { confirmationReason: "expired" } : edit ? { confirmationReason: "edit" } : {}
|
|
139
174
|
}
|
|
140
|
-
});
|
|
175
|
+
}, editQuestion === void 0 ? void 0 : JSON.stringify({ pendingChatPostEdit: editQuestion }));
|
|
141
176
|
await notice(expired ? "This confirmation expired; nothing executed." : decision === "cancel" ? "Cancellation received." : "Confirmation received.");
|
|
142
177
|
}
|
|
143
|
-
function telegramConfirmationEvents(render = async (description, locale) => fallbackConfirmation(description, locale)) {
|
|
178
|
+
function telegramConfirmationEvents(render = async (description, locale) => fallbackConfirmation(description, locale), archive) {
|
|
179
|
+
const record = async (callId, phase, text, status, ctx) => {
|
|
180
|
+
const saved = confirmations.get()[callId];
|
|
181
|
+
if (archive === void 0 || saved?.description?.chatPost === void 0 || saved.source === void 0) return;
|
|
182
|
+
const operationId = chatPostOperationId(ctx.session.id, callId);
|
|
183
|
+
const source = saved.source;
|
|
184
|
+
const append = () => archive.append(conversationEvent({
|
|
185
|
+
eventId: `chat-post:${operationId}:${phase}`,
|
|
186
|
+
provider: "telegram",
|
|
187
|
+
chatId: source.chatId,
|
|
188
|
+
chatType: source.chatId.startsWith("-") ? "group" : "private",
|
|
189
|
+
threadId: source.threadId || null,
|
|
190
|
+
participantIds: [saved.memberId],
|
|
191
|
+
kind: "tool_result",
|
|
192
|
+
operationId,
|
|
193
|
+
toolCallId: operationId,
|
|
194
|
+
toolName: saved.action,
|
|
195
|
+
sessionId: ctx.session.id,
|
|
196
|
+
text,
|
|
197
|
+
status,
|
|
198
|
+
reason: phase
|
|
199
|
+
}));
|
|
200
|
+
if (phase === "preview") await append();
|
|
201
|
+
else try {
|
|
202
|
+
await append();
|
|
203
|
+
} catch (error) {
|
|
204
|
+
reportTelegramError("chat_post_source_archive_failed", error, {
|
|
205
|
+
operationId,
|
|
206
|
+
phase
|
|
207
|
+
});
|
|
208
|
+
}
|
|
209
|
+
};
|
|
144
210
|
return {
|
|
145
211
|
"context.cleared"(_data, channel) {
|
|
146
212
|
confirmations.update(() => ({}));
|
|
@@ -176,18 +242,30 @@ function telegramConfirmationEvents(render = async (description, locale) => fall
|
|
|
176
242
|
}
|
|
177
243
|
const memberId = confirmations.get()[request.action.callId]?.memberId ?? memberIdOf(ctx.session.auth.current);
|
|
178
244
|
if (memberId === void 0) throw new Error("Confirmation requires a member.");
|
|
179
|
-
const
|
|
245
|
+
const description = confirmations.get()[request.action.callId]?.description;
|
|
246
|
+
const exactPost = isChatPost(request.action.toolName);
|
|
247
|
+
if (exactPost && description?.chatPost === void 0) throw new Error("An exact chat post cannot show a generic confirmation.");
|
|
248
|
+
const copy = description?.chatPost === void 0 ? await render(description, confirmationLocale(ctx.session.auth.current)) : chatPostCard(description.chatPost, confirmationLocale(ctx.session.auth.current));
|
|
180
249
|
const text = copy.text;
|
|
181
|
-
|
|
250
|
+
await record(request.action.callId, "preview", text, "requested", ctx);
|
|
251
|
+
const send = () => channel.telegram.post({
|
|
182
252
|
text,
|
|
183
|
-
reply_markup: { inline_keyboard: [[
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
253
|
+
reply_markup: { inline_keyboard: [[
|
|
254
|
+
{
|
|
255
|
+
text: copy.confirm,
|
|
256
|
+
callback_data: confirmationCallback(request.requestId, memberId, "approve")
|
|
257
|
+
},
|
|
258
|
+
...exactPost ? [{
|
|
259
|
+
text: chatPostCopy(confirmationLocale(ctx.session.auth.current)).edit,
|
|
260
|
+
callback_data: confirmationCallback(request.requestId, memberId, "edit")
|
|
261
|
+
}] : [],
|
|
262
|
+
{
|
|
263
|
+
text: copy.cancel,
|
|
264
|
+
callback_data: confirmationCallback(request.requestId, memberId, "cancel")
|
|
265
|
+
}
|
|
266
|
+
]] }
|
|
190
267
|
});
|
|
268
|
+
const sent = await (exactPost ? withChatPostCard(text, send) : send());
|
|
191
269
|
const state = channel.state;
|
|
192
270
|
state.confirmationMessages = {
|
|
193
271
|
...state.confirmationMessages,
|
|
@@ -196,7 +274,8 @@ function telegramConfirmationEvents(render = async (description, locale) => fall
|
|
|
196
274
|
messageId: sent.id,
|
|
197
275
|
text,
|
|
198
276
|
copy,
|
|
199
|
-
locale: confirmationLocale(ctx.session.auth.current)
|
|
277
|
+
locale: confirmationLocale(ctx.session.auth.current),
|
|
278
|
+
exactPost
|
|
200
279
|
}
|
|
201
280
|
};
|
|
202
281
|
}
|
|
@@ -221,12 +300,39 @@ function telegramConfirmationEvents(render = async (description, locale) => fall
|
|
|
221
300
|
}
|
|
222
301
|
const [callId, message] = entry;
|
|
223
302
|
const copy = message.copy ?? confirmationCopy(message.locale ?? confirmationLocale(ctx.session.auth.current, currentLocale()));
|
|
224
|
-
const
|
|
225
|
-
|
|
303
|
+
const reason = confirmations.get()[callId]?.cancellationReason;
|
|
304
|
+
const status = resolution.outcome === "approved" ? copy.executing : reason === "expired" ? chatPostCopy(message.locale ?? "en").expired : copy.cancelled;
|
|
305
|
+
const text = message.exactPost ? `${message.text}\n\n${status}` : status;
|
|
306
|
+
const edit = () => channel.telegram.editMessageText({
|
|
226
307
|
messageId: message.messageId,
|
|
227
308
|
replyMarkup: { inline_keyboard: [] },
|
|
228
309
|
text
|
|
229
310
|
});
|
|
311
|
+
await (message.exactPost ? withChatPostCard(text, edit) : edit());
|
|
312
|
+
await record(callId, reason ?? resolution.outcome, text, resolution.outcome === "approved" ? "completed" : "cancelled", ctx);
|
|
313
|
+
const question = chatPostEditQuestion(resolution);
|
|
314
|
+
if (message.exactPost && reason === "edit" && question !== void 0) {
|
|
315
|
+
pendingQuestions.update((all) => ({
|
|
316
|
+
...all,
|
|
317
|
+
[questionKey(question)]: question
|
|
318
|
+
}));
|
|
319
|
+
state.pendingQuestions = {
|
|
320
|
+
...state.pendingQuestions,
|
|
321
|
+
[questionKey(question)]: question
|
|
322
|
+
};
|
|
323
|
+
channel.continuation?.alias(questionAddress(question));
|
|
324
|
+
channel.continuation?.alias(formatAddress({
|
|
325
|
+
...question,
|
|
326
|
+
conversationId: ""
|
|
327
|
+
}));
|
|
328
|
+
await channel.telegram.post({
|
|
329
|
+
text: question.question,
|
|
330
|
+
reply_markup: {
|
|
331
|
+
force_reply: true,
|
|
332
|
+
selective: true
|
|
333
|
+
}
|
|
334
|
+
});
|
|
335
|
+
}
|
|
230
336
|
if (resolution.outcome !== "approved") {
|
|
231
337
|
delete state.confirmationMessages?.[callId];
|
|
232
338
|
confirmations.update((all) => ({
|
|
@@ -239,7 +345,7 @@ function telegramConfirmationEvents(render = async (description, locale) => fall
|
|
|
239
345
|
}
|
|
240
346
|
}
|
|
241
347
|
},
|
|
242
|
-
async "action.result"(data, channel) {
|
|
348
|
+
async "action.result"(data, channel, ctx) {
|
|
243
349
|
const state = channel.state;
|
|
244
350
|
if (data.result.kind === "tool-result") {
|
|
245
351
|
const question = object(data.result.output).pendingQuestion;
|
|
@@ -278,7 +384,20 @@ function telegramConfirmationEvents(render = async (description, locale) => fall
|
|
|
278
384
|
}
|
|
279
385
|
const message = state.confirmationMessages?.[data.result.callId];
|
|
280
386
|
if (message === void 0) return;
|
|
281
|
-
|
|
387
|
+
const failed = data.status !== "completed" || data.result.kind === "tool-result" && (data.result.isError === true || object(data.result.output).isError === true || object(object(data.result.output).validation).ok === false);
|
|
388
|
+
if (message.exactPost) {
|
|
389
|
+
const locale = message.locale ?? currentLocale();
|
|
390
|
+
const receipt = !failed && data.result.kind === "tool-result" ? chatPostReceipt(data.result.output, locale) : void 0;
|
|
391
|
+
const errorId = failed ? reportTelegramError("confirmed_chat_post_failed", data, { callId: data.result.callId }) : void 0;
|
|
392
|
+
const status = receipt ?? (failed ? `${message.copy?.failed ?? "Chat posting failed."} ${errorId}\n${chatPostCopy(locale).uncertain}` : chatPostCopy(locale).uncertain);
|
|
393
|
+
const text = `${message.text}\n\n${status}`;
|
|
394
|
+
await withChatPostCard(text, () => channel.telegram.editMessageText({
|
|
395
|
+
messageId: message.messageId,
|
|
396
|
+
replyMarkup: { inline_keyboard: [] },
|
|
397
|
+
text
|
|
398
|
+
}));
|
|
399
|
+
await record(data.result.callId, "receipt", text, failed ? "failed" : object(data.result.kind === "tool-result" ? data.result.output : void 0).status === "sent" ? "completed" : "unknown", ctx);
|
|
400
|
+
} else if (failed) {
|
|
282
401
|
const errorId = reportTelegramError("confirmed_action_failed", data, { callId: data.result.callId });
|
|
283
402
|
await channel.telegram.editMessageText({
|
|
284
403
|
messageId: message.messageId,
|
|
@@ -272,12 +272,14 @@ function captureTelegramFetch(fetchImpl, archive, identities) {
|
|
|
272
272
|
}
|
|
273
273
|
throw error;
|
|
274
274
|
}
|
|
275
|
-
const result = record(await response.clone().json());
|
|
275
|
+
const result = record(await response.clone().json().catch(() => null));
|
|
276
|
+
const messageId = id(record(result.result).message_id);
|
|
277
|
+
const acknowledged = edit || messageId !== null && /^[1-9]\d*$/u.test(messageId);
|
|
276
278
|
await archive.append({
|
|
277
279
|
...event,
|
|
278
280
|
eventId: `${event.eventId}:result`,
|
|
279
|
-
status: response.ok && result.ok === true ? "completed" : "failed",
|
|
280
|
-
messageId:
|
|
281
|
+
status: response.ok && result.ok === true && acknowledged ? "completed" : result.ok === false ? "failed" : "unknown",
|
|
282
|
+
messageId: messageId ?? event.messageId
|
|
281
283
|
});
|
|
282
284
|
return response;
|
|
283
285
|
};
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { type TelegramText } from "./format.js";
|
|
1
2
|
export declare function withDeliveryLocale<T>(locale: string, run: () => T, threadId?: string): T;
|
|
2
3
|
export declare function currentLocale(): string;
|
|
3
4
|
export declare function currentThreadId(): string | undefined;
|
|
@@ -16,5 +17,16 @@ export declare function isTechnicalText(text: string): boolean;
|
|
|
16
17
|
export declare function reportTelegramError(operation: string, error: unknown, metadata?: Record<string, unknown>, secrets?: ReadonlySet<string>): string;
|
|
17
18
|
export declare function failureText(errorId: string, kind?: "failed" | "recovery" | "attachment" | "authorization" | "response", locale?: string): string;
|
|
18
19
|
export declare function customerText(text: string, locale?: string): string;
|
|
20
|
+
/** Check what Telegram will display, including hidden link destinations, after removing markup. */
|
|
21
|
+
export declare function customerMessage(text: string, locale?: string): TelegramText;
|
|
22
|
+
/** Only a definite formatting refusal permits resending; network/unknown failures never do. */
|
|
23
|
+
export declare function formattingRefused(response: Response): Promise<boolean>;
|
|
24
|
+
export declare class TelegramDeliveryError extends Error {
|
|
25
|
+
readonly reference: string;
|
|
26
|
+
readonly delivery: "failed" | "unknown";
|
|
27
|
+
readonly messageIds: readonly number[];
|
|
28
|
+
readonly name = "TelegramDeliveryError";
|
|
29
|
+
constructor(reference: string, delivery: "failed" | "unknown", messageIds: readonly number[]);
|
|
30
|
+
}
|
|
19
31
|
/** Covers framework defaults, edits and callback notices as well as ordinary agent replies. */
|
|
20
|
-
export declare function safeTelegramFetch(fetchImpl: typeof fetch): typeof fetch;
|
|
32
|
+
export declare function safeTelegramFetch(fetchImpl: typeof fetch, scope?: string): typeof fetch;
|