@segmentstream/agent 0.5.4 → 0.6.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +56 -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 +15 -26
- 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/customer-text.mjs +2 -1
- 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/runtime-request.d.ts +8 -0
- package/dist/extension/lib/telegram/runtime-request.mjs +36 -0
- package/package.json +1 -1
- package/dist/extension/lib/telegram/requests.d.ts +0 -80
- package/dist/extension/lib/telegram/requests.mjs +0 -186
|
@@ -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,
|
|
@@ -4,6 +4,7 @@ import { createRequire as __eveCreateRequire } from "node:module";
|
|
|
4
4
|
const __filename = __eveFileURLToPath(import.meta.url);
|
|
5
5
|
__eveDirname(__filename);
|
|
6
6
|
__eveCreateRequire(import.meta.url);
|
|
7
|
+
import { isExactChatPostCard } from "./chat-post-card.mjs";
|
|
7
8
|
import { redactDiagnostic } from "../diagnostics.mjs";
|
|
8
9
|
import { currentInboundRequest } from "./inbound-request.mjs";
|
|
9
10
|
import { randomBytes } from "node:crypto";
|
|
@@ -97,7 +98,7 @@ function safeTelegramFetch(fetchImpl) {
|
|
|
97
98
|
const url = typeof input === "string" ? input : input instanceof URL ? input.href : input.url;
|
|
98
99
|
if (!methods.has(url.split("/").at(-1) ?? "") || typeof init?.body !== "string") return fetchImpl(input, init);
|
|
99
100
|
const body = JSON.parse(init.body);
|
|
100
|
-
for (const field of ["text", "caption"]) if (typeof body[field] === "string") body[field] = customerText(body[field]);
|
|
101
|
+
for (const field of ["text", "caption"]) if (typeof body[field] === "string" && !isExactChatPostCard(body[field])) body[field] = customerText(body[field]);
|
|
101
102
|
if (Array.isArray(body.media)) body.media = body.media.map((item) => ({
|
|
102
103
|
...item,
|
|
103
104
|
...typeof item.caption === "string" ? { caption: customerText(item.caption) } : {}
|
|
@@ -4,7 +4,7 @@ import { createRequire as __eveCreateRequire } from "node:module";
|
|
|
4
4
|
const __filename = __eveFileURLToPath(import.meta.url);
|
|
5
5
|
__eveDirname(__filename);
|
|
6
6
|
__eveCreateRequire(import.meta.url);
|
|
7
|
-
import { runtimeRequestFailure } from "./
|
|
7
|
+
import { runtimeRequestFailure } from "./runtime-request.mjs";
|
|
8
8
|
import { GET } from "eve/channels";
|
|
9
9
|
const HEALTH_ROUTE = "/eve/v1/telegram/health";
|
|
10
10
|
function healthRoute(input) {
|
|
@@ -42,7 +42,7 @@ export declare class InboundRequest {
|
|
|
42
42
|
waitUntil: ((task: Promise<unknown>) => void) | undefined;
|
|
43
43
|
/** Continues the exact pending question's session, including under thread scope. */
|
|
44
44
|
address: string | undefined;
|
|
45
|
-
respond: ((address: string, requestId: string, decision: "approve" | "cancel", principal: SessionAuthContext) => Promise<unknown>) | undefined;
|
|
45
|
+
respond: ((address: string, requestId: string, decision: "approve" | "cancel", principal: SessionAuthContext, text?: string) => Promise<unknown>) | undefined;
|
|
46
46
|
constructor(
|
|
47
47
|
/** The agent's description from the compiled manifest, for the routing decision. */
|
|
48
48
|
agentDescription: () => Promise<string>);
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import type { RuntimeTokenVerifier } from "../runtime-identity.js";
|
|
2
|
+
/** Authenticate internal channel routes with the production deployment's runtime identity. */
|
|
3
|
+
export declare function runtimeRequestFailure(request: Request, options: {
|
|
4
|
+
readonly enabled: () => boolean;
|
|
5
|
+
readonly configured: () => boolean;
|
|
6
|
+
readonly verify: () => RuntimeTokenVerifier;
|
|
7
|
+
readonly log?: (message: string, detail: unknown) => void;
|
|
8
|
+
}): Promise<Response | undefined>;
|
|
@@ -0,0 +1,36 @@
|
|
|
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 bearer(header) {
|
|
8
|
+
return (header === null ? null : /^Bearer\s+(\S+)$/i.exec(header.trim()))?.[1];
|
|
9
|
+
}
|
|
10
|
+
function error(code, status) {
|
|
11
|
+
return Response.json({ error: code }, { status });
|
|
12
|
+
}
|
|
13
|
+
async function runtimeRequestFailure(request, options) {
|
|
14
|
+
const log = options.log ?? ((message, detail) => console.warn(message, detail));
|
|
15
|
+
if (!options.enabled()) return error("channels_disabled", 503);
|
|
16
|
+
if (!options.configured()) return error("not_found", 404);
|
|
17
|
+
const token = bearer(request.headers.get("authorization"));
|
|
18
|
+
if (token === void 0) return error("unauthorized", 401);
|
|
19
|
+
let verifier;
|
|
20
|
+
try {
|
|
21
|
+
verifier = options.verify();
|
|
22
|
+
} catch (cause) {
|
|
23
|
+
log("The agent cannot verify requests: the runtime identity is not configured.", cause);
|
|
24
|
+
return error("unavailable", 503);
|
|
25
|
+
}
|
|
26
|
+
const verification = await verifier(token);
|
|
27
|
+
if (verification.status === "unavailable") {
|
|
28
|
+
log("The agent cannot verify requests: the runtime key set is unavailable.", verification.reason);
|
|
29
|
+
return error("unavailable", 503);
|
|
30
|
+
}
|
|
31
|
+
if (verification.status !== "valid") {
|
|
32
|
+
log("The agent refused a request with an invalid runtime token.", verification.reason);
|
|
33
|
+
return error("unauthorized", 401);
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
export { runtimeRequestFailure };
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@segmentstream/agent",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.6.3",
|
|
4
4
|
"description": "Mount a SegmentStream workspace's agent on eve: the ontology as tools, acting as the member who writes, and Telegram through the workspace's connection.",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"author": "SegmentStream",
|
|
@@ -1,80 +0,0 @@
|
|
|
1
|
-
import { type RouteDefinition } from "eve/channels";
|
|
2
|
-
import type { TelegramChannel, TelegramChannelState } from "eve/channels/telegram";
|
|
3
|
-
import type { SessionAuthContext } from "eve/context";
|
|
4
|
-
import type { SessionSettings } from "../../config.js";
|
|
5
|
-
import type { RuntimeTokenVerifier } from "../runtime-identity.js";
|
|
6
|
-
import { type SessionPolicy } from "./sessions.js";
|
|
7
|
-
/**
|
|
8
|
-
* Requests from the ontology's `agent_chat.send_message` action: an external agent or a member asks
|
|
9
|
-
* the workspace agent to contact someone in a group or a private chat. The ontology service resolves
|
|
10
|
-
* the chat and posts here with its own Vercel OIDC token; the request is delivered into the chat's
|
|
11
|
-
* session through eve's proactive `receive`, without a seed message, so it continues the session
|
|
12
|
-
* the chat's own messages reach (`chatId::`, the chat-scope address) and the replies land there too.
|
|
13
|
-
* The session policy runs on that session first, as it does for an inbound message.
|
|
14
|
-
*/
|
|
15
|
-
export declare const MESSAGES_ROUTE = "/eve/v1/telegram/messages";
|
|
16
|
-
export interface AgentRequest {
|
|
17
|
-
readonly target: {
|
|
18
|
-
readonly kind: "group" | "private";
|
|
19
|
-
readonly provider: "telegram";
|
|
20
|
-
/** The group's chat id, or the Telegram user id of a private chat. */
|
|
21
|
-
readonly chatId: string;
|
|
22
|
-
};
|
|
23
|
-
readonly message: string;
|
|
24
|
-
/** Document record ids from newer ontologies; omitted by older deployments. */
|
|
25
|
-
readonly documentIds?: ReadonlyArray<string>;
|
|
26
|
-
/** Whom to contact, when the requester named someone. */
|
|
27
|
-
readonly person: {
|
|
28
|
-
readonly id: string;
|
|
29
|
-
readonly name: string;
|
|
30
|
-
} | null;
|
|
31
|
-
readonly requester: {
|
|
32
|
-
readonly id: string;
|
|
33
|
-
readonly name: string;
|
|
34
|
-
readonly kind: "member" | "service";
|
|
35
|
-
/** The roles the requester holds; empty from an ontology that does not send them. */
|
|
36
|
-
readonly roles: ReadonlyArray<string>;
|
|
37
|
-
/**
|
|
38
|
-
* Whether the turn can act as the requester: a member with a live paired identity. An ontology
|
|
39
|
-
* that does not send it means a member can.
|
|
40
|
-
*/
|
|
41
|
-
readonly actAs: boolean;
|
|
42
|
-
};
|
|
43
|
-
}
|
|
44
|
-
/** The request body, or `undefined` when it does not match the contract. */
|
|
45
|
-
export declare function agentRequest(value: unknown): AgentRequest | undefined;
|
|
46
|
-
/**
|
|
47
|
-
* Whom the request's turn runs as. A member requester the ontology says can be acted as is the
|
|
48
|
-
* turn's member, so the ontology connection acts as them and the ontology service checks, as for any
|
|
49
|
-
* turn, that they are active and paired. Their roles, as the action's principal held them, choose the
|
|
50
|
-
* model of a session the request starts; the service resolves them afresh on every call. A service
|
|
51
|
-
* user, or a member who has not paired, cannot be acted as, so the request runs without a member and
|
|
52
|
-
* the turn has no ontology; their roles still choose the model, and grant nothing, since the
|
|
53
|
-
* ontology connection acts only for a `memberId`.
|
|
54
|
-
*/
|
|
55
|
-
export declare function requestPrincipal(request: AgentRequest): SessionAuthContext;
|
|
56
|
-
/** What the turn is about, for the model, ahead of the request itself. */
|
|
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>;
|
|
65
|
-
/**
|
|
66
|
-
* `POST /eve/v1/telegram/messages`. Gated like the webhook: outside production it answers 503 and a
|
|
67
|
-
* workspace without a Telegram channel 404. The caller must be a function of this deployment's
|
|
68
|
-
* project, verified from its Vercel OIDC token.
|
|
69
|
-
*/
|
|
70
|
-
export declare function messagesRoute(input: {
|
|
71
|
-
readonly channel: TelegramChannel;
|
|
72
|
-
readonly enabled: () => boolean;
|
|
73
|
-
readonly configured: () => boolean;
|
|
74
|
-
readonly verify: () => RuntimeTokenVerifier;
|
|
75
|
-
/** The adapter's session policy, which the webhook route applies too. */
|
|
76
|
-
readonly sessionPolicy: SessionPolicy;
|
|
77
|
-
/** The mounted `sessions.scope`. */
|
|
78
|
-
readonly scope: () => SessionSettings["scope"];
|
|
79
|
-
readonly log?: (message: string, detail: unknown) => void;
|
|
80
|
-
}): RouteDefinition<TelegramChannelState>;
|