@segmentstream/agent 0.3.0 → 0.4.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 +18 -3
- package/dist/extension/_manifest.json +4 -1
- 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 +21 -6
- package/dist/extension/lib/confirmations.d.ts +26 -0
- package/dist/extension/lib/confirmations.mjs +96 -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/ontology-connection.d.ts +1 -0
- package/dist/extension/lib/ontology-connection.mjs +3 -0
- package/dist/extension/lib/ontology-tools.d.ts +11 -0
- package/dist/extension/lib/ontology-tools.mjs +41 -0
- package/dist/extension/lib/questions.d.ts +14 -0
- package/dist/extension/lib/questions.mjs +30 -0
- package/dist/extension/lib/telegram/adapter.d.ts +3 -1
- package/dist/extension/lib/telegram/adapter.mjs +63 -12
- package/dist/extension/lib/telegram/confirmations.d.ts +18 -0
- package/dist/extension/lib/telegram/confirmations.mjs +280 -0
- package/dist/extension/lib/telegram/conversations.d.ts +30 -0
- package/dist/extension/lib/telegram/conversations.mjs +287 -0
- package/dist/extension/lib/telegram/inbound-request.d.ts +4 -0
- package/dist/extension/lib/telegram/inbound-request.mjs +2 -0
- package/dist/extension/lib/telegram/inbound.d.ts +3 -0
- package/dist/extension/lib/telegram/inbound.mjs +63 -13
- package/dist/extension/lib/telegram/principal.mjs +1 -0
- package/dist/extension/lib/telegram/requests.mjs +3 -1
- package/dist/extension/lib/telegram/router.d.ts +1 -1
- package/dist/extension/lib/telegram/routing-context.mjs +2 -2
- package/dist/extension/lib/telegram/sessions.d.ts +4 -7
- package/dist/extension/tools/attachments_promote.mjs +3 -0
- package/dist/extension/tools/questions_ask.d.ts +12 -0
- package/dist/extension/tools/questions_ask.mjs +50 -0
- package/dist/tools/index.d.ts +1 -0
- package/dist/tools/index.mjs +2 -1
- package/package.json +1 -1
|
@@ -0,0 +1,287 @@
|
|
|
1
|
+
import { fileURLToPath as __eveFileURLToPath } from "node:url";
|
|
2
|
+
import { dirname as __eveDirname } from "node:path";
|
|
3
|
+
import { createRequire as __eveCreateRequire } from "node:module";
|
|
4
|
+
const __filename = __eveFileURLToPath(import.meta.url);
|
|
5
|
+
__eveDirname(__filename);
|
|
6
|
+
__eveCreateRequire(import.meta.url);
|
|
7
|
+
import { conversationEvent } from "../conversations.mjs";
|
|
8
|
+
import { observedChatKey } from "./observed.mjs";
|
|
9
|
+
import { currentInboundRequest } from "./inbound-request.mjs";
|
|
10
|
+
import { parseTelegramUpdate } from "eve/channels/telegram";
|
|
11
|
+
import { randomUUID } from "node:crypto";
|
|
12
|
+
const captures = new WeakMap();
|
|
13
|
+
const record = (value) => value !== null && typeof value === "object" && !Array.isArray(value) ? value : {};
|
|
14
|
+
const id = (value) => typeof value === "number" || typeof value === "string" ? String(value) : null;
|
|
15
|
+
function attachmentRefs(message) {
|
|
16
|
+
const files = message.attachments.map((file) => ({
|
|
17
|
+
id: file.fileId,
|
|
18
|
+
kind: file.kind,
|
|
19
|
+
...file.fileName === void 0 ? {} : { filename: file.fileName },
|
|
20
|
+
...file.mediaType === void 0 ? {} : { contentType: file.mediaType },
|
|
21
|
+
...file.size === void 0 ? {} : { size: file.size },
|
|
22
|
+
...file.fileUniqueId === void 0 ? {} : { uniqueId: file.fileUniqueId }
|
|
23
|
+
}));
|
|
24
|
+
for (const kind of [
|
|
25
|
+
"voice",
|
|
26
|
+
"audio",
|
|
27
|
+
"video",
|
|
28
|
+
"video_note",
|
|
29
|
+
"sticker",
|
|
30
|
+
"animation"
|
|
31
|
+
]) {
|
|
32
|
+
const file = record(message.raw[kind]);
|
|
33
|
+
const fileId = id(file.file_id);
|
|
34
|
+
if (fileId !== null) files.push({
|
|
35
|
+
id: fileId,
|
|
36
|
+
kind,
|
|
37
|
+
...id(file.file_unique_id) ? { uniqueId: id(file.file_unique_id) } : {},
|
|
38
|
+
...typeof file.file_size === "number" ? { size: file.file_size } : {},
|
|
39
|
+
...typeof file.mime_type === "string" ? { contentType: file.mime_type } : {},
|
|
40
|
+
...typeof file.file_name === "string" ? { filename: file.file_name } : {}
|
|
41
|
+
});
|
|
42
|
+
}
|
|
43
|
+
return files;
|
|
44
|
+
}
|
|
45
|
+
async function participants(message, identities, resolved) {
|
|
46
|
+
if (message.chat.type !== "private" || message.from === void 0 || message.from.isBot) return [];
|
|
47
|
+
try {
|
|
48
|
+
const lookup = await identities.lookup({
|
|
49
|
+
provider: "telegram",
|
|
50
|
+
externalId: message.from.id
|
|
51
|
+
});
|
|
52
|
+
resolved(lookup);
|
|
53
|
+
return lookup.status === "paired" ? [lookup.member.id] : [];
|
|
54
|
+
} catch (error) {
|
|
55
|
+
console.error("[segmentstream] conversation participant lookup failed", { error });
|
|
56
|
+
return [];
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
async function captureTelegramWebhook(input) {
|
|
60
|
+
let raw;
|
|
61
|
+
try {
|
|
62
|
+
raw = await input.request.clone().json();
|
|
63
|
+
} catch {
|
|
64
|
+
return input.next();
|
|
65
|
+
}
|
|
66
|
+
const update = record(raw);
|
|
67
|
+
const edited = update.edited_message ?? update.edited_channel_post;
|
|
68
|
+
const messagePayload = edited ?? update.channel_post ?? update.message;
|
|
69
|
+
const parsed = parseTelegramUpdate({ message: messagePayload });
|
|
70
|
+
if (parsed?.kind !== "message") return input.next();
|
|
71
|
+
const message = parsed.message;
|
|
72
|
+
const sentAt = typeof message.raw.date === "number" ? message.raw.date * 1e3 : Date.now();
|
|
73
|
+
const senderChat = record(message.raw.sender_chat);
|
|
74
|
+
let lookup;
|
|
75
|
+
const event = conversationEvent({
|
|
76
|
+
eventId: [
|
|
77
|
+
"telegram",
|
|
78
|
+
message.chat.id,
|
|
79
|
+
message.messageId,
|
|
80
|
+
edited === void 0 ? "message" : `edit:${id(update.update_id) ?? id(message.raw.edit_date)}`
|
|
81
|
+
].join(":"),
|
|
82
|
+
provider: "telegram",
|
|
83
|
+
chatId: message.chat.id,
|
|
84
|
+
chatType: message.chat.type === "supergroup" ? "group" : message.chat.type,
|
|
85
|
+
threadId: message.messageThreadId === void 0 ? null : String(message.messageThreadId),
|
|
86
|
+
kind: "inbound",
|
|
87
|
+
text: message.text || message.caption || null,
|
|
88
|
+
messageId: message.messageId,
|
|
89
|
+
replyToMessageId: message.replyToMessage?.messageId ?? null,
|
|
90
|
+
senderId: id(senderChat.id) ?? message.from?.id ?? null,
|
|
91
|
+
senderName: typeof senderChat.title === "string" ? senderChat.title : message.from?.username ?? message.from?.firstName ?? null,
|
|
92
|
+
senderIsBot: message.from?.isBot ?? false,
|
|
93
|
+
participantIds: await participants(message, input.identities, (result) => {
|
|
94
|
+
lookup = result;
|
|
95
|
+
}),
|
|
96
|
+
attachments: attachmentRefs(message),
|
|
97
|
+
at: new Date(sentAt).toISOString()
|
|
98
|
+
});
|
|
99
|
+
await input.archive.append(event);
|
|
100
|
+
const capture = {
|
|
101
|
+
event,
|
|
102
|
+
message,
|
|
103
|
+
...lookup === void 0 ? {} : { lookup }
|
|
104
|
+
};
|
|
105
|
+
const inbound = currentInboundRequest();
|
|
106
|
+
if (inbound !== void 0) captures.set(inbound, capture);
|
|
107
|
+
const chat = observedChatKey({
|
|
108
|
+
chatId: message.chat.id,
|
|
109
|
+
...message.messageThreadId === void 0 ? {} : { messageThreadId: message.messageThreadId }
|
|
110
|
+
});
|
|
111
|
+
if (!(message.from === void 0 || message.from.isBot || message.chat.type === "channel") && edited === void 0) {
|
|
112
|
+
const history = await input.archive.context({
|
|
113
|
+
provider: "telegram",
|
|
114
|
+
chatId: message.chat.id,
|
|
115
|
+
...event.threadId === null ? {} : { threadId: event.threadId },
|
|
116
|
+
limit: 100
|
|
117
|
+
});
|
|
118
|
+
for (const earlier of (input.observed.recent(chat).length === 0 ? history.events : []).filter((item) => item.kind === "inbound" && item.eventId !== event.eventId).slice(0, 30).reverse()) input.observed.add(chat, {
|
|
119
|
+
author: earlier.senderName ?? earlier.senderId ?? "automated post",
|
|
120
|
+
text: earlier.text ?? "[attachment or service message]",
|
|
121
|
+
at: earlier.at,
|
|
122
|
+
...earlier.senderId === null ? {} : { authorId: earlier.senderId }
|
|
123
|
+
});
|
|
124
|
+
} else {
|
|
125
|
+
if (message.chat.type === "group" || message.chat.type === "supergroup") await input.chats?.seen({
|
|
126
|
+
chatId: message.chat.id,
|
|
127
|
+
title: message.chat.title ?? ""
|
|
128
|
+
});
|
|
129
|
+
input.observed.add(chat, {
|
|
130
|
+
author: event.senderName ?? event.senderId ?? "automated post",
|
|
131
|
+
text: event.text ?? "[attachment or service message]",
|
|
132
|
+
at: event.at
|
|
133
|
+
});
|
|
134
|
+
capture.decision = "silent";
|
|
135
|
+
capture.reason = edited !== void 0 ? "message_edit" : "automated_or_channel_message";
|
|
136
|
+
}
|
|
137
|
+
let response;
|
|
138
|
+
try {
|
|
139
|
+
response = await input.next();
|
|
140
|
+
} catch (error) {
|
|
141
|
+
try {
|
|
142
|
+
await input.archive.append({
|
|
143
|
+
...event,
|
|
144
|
+
eventId: `${event.eventId}:decision`,
|
|
145
|
+
kind: "decision",
|
|
146
|
+
text: null,
|
|
147
|
+
decision: "silent",
|
|
148
|
+
reason: "inbound_handler_failed",
|
|
149
|
+
attachments: []
|
|
150
|
+
});
|
|
151
|
+
} catch (storageError) {
|
|
152
|
+
console.error("[segmentstream] recording inbound failure failed", {
|
|
153
|
+
storageError,
|
|
154
|
+
cause: error
|
|
155
|
+
});
|
|
156
|
+
}
|
|
157
|
+
throw error;
|
|
158
|
+
}
|
|
159
|
+
await input.archive.append({
|
|
160
|
+
...event,
|
|
161
|
+
eventId: `${event.eventId}:decision`,
|
|
162
|
+
kind: "decision",
|
|
163
|
+
text: null,
|
|
164
|
+
decision: capture.decision ?? "silent",
|
|
165
|
+
reason: capture.reason ?? (message.raw.left_chat_member ? "member_departure" : message.raw.migrate_to_chat_id || message.raw.migrate_from_chat_id ? "chat_migration" : "unpaired_refused_or_empty_message"),
|
|
166
|
+
attachments: []
|
|
167
|
+
});
|
|
168
|
+
return response;
|
|
169
|
+
}
|
|
170
|
+
function recordGateDecision(reason, decision = "silent") {
|
|
171
|
+
const inbound = currentInboundRequest();
|
|
172
|
+
const capture = inbound === void 0 ? void 0 : captures.get(inbound);
|
|
173
|
+
if (capture !== void 0) {
|
|
174
|
+
capture.reason = reason;
|
|
175
|
+
capture.decision = decision;
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
function recordRouteDecision(route, addressed, policy) {
|
|
179
|
+
const inbound = currentInboundRequest();
|
|
180
|
+
const capture = inbound === void 0 ? void 0 : captures.get(inbound);
|
|
181
|
+
if (capture !== void 0) {
|
|
182
|
+
capture.decision = route === "reply" ? "reply" : "silent";
|
|
183
|
+
capture.reason ??= typeof addressed === "string" ? addressed : addressed ? "addressed_message" : policy === "mention" ? "mention_policy" : `decision_model_${route}`;
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
function captureRouteDecider(decide) {
|
|
187
|
+
return async (question) => {
|
|
188
|
+
try {
|
|
189
|
+
return await decide(question);
|
|
190
|
+
} catch (error) {
|
|
191
|
+
const inbound = currentInboundRequest();
|
|
192
|
+
const capture = inbound === void 0 ? void 0 : captures.get(inbound);
|
|
193
|
+
if (capture !== void 0) capture.reason = error instanceof Error && error.name === "TimeoutError" ? "decision_model_timeout" : "decision_model_failed";
|
|
194
|
+
throw error;
|
|
195
|
+
}
|
|
196
|
+
};
|
|
197
|
+
}
|
|
198
|
+
function captureTelegramFetch(fetchImpl, archive, identities) {
|
|
199
|
+
return async (input, init) => {
|
|
200
|
+
if (!(typeof input === "string" ? input : input instanceof URL ? input.href : input.url).endsWith("/sendMessage") || typeof init?.body !== "string") return fetchImpl(input, init);
|
|
201
|
+
const body = record(JSON.parse(init.body));
|
|
202
|
+
const chatId = id(body.chat_id);
|
|
203
|
+
if (chatId === null) return fetchImpl(input, init);
|
|
204
|
+
const inbound = currentInboundRequest();
|
|
205
|
+
const capture = inbound === void 0 ? void 0 : captures.get(inbound);
|
|
206
|
+
let participantIds = capture?.event.participantIds ?? [];
|
|
207
|
+
const chatType = capture?.event.chatType ?? (chatId.startsWith("-") ? "group" : "private");
|
|
208
|
+
if (chatType === "private" && participantIds.length === 0 && capture === void 0) try {
|
|
209
|
+
const lookup = await identities().lookup({
|
|
210
|
+
provider: "telegram",
|
|
211
|
+
externalId: chatId
|
|
212
|
+
});
|
|
213
|
+
if (lookup.status === "paired") participantIds = [lookup.member.id];
|
|
214
|
+
} catch (error) {
|
|
215
|
+
console.error("[segmentstream] outbound participant lookup failed", { error });
|
|
216
|
+
}
|
|
217
|
+
const event = conversationEvent({
|
|
218
|
+
eventId: `telegram:send:${randomUUID()}`,
|
|
219
|
+
provider: "telegram",
|
|
220
|
+
chatId,
|
|
221
|
+
chatType,
|
|
222
|
+
kind: "outbound",
|
|
223
|
+
text: typeof body.text === "string" ? body.text : null,
|
|
224
|
+
senderIsBot: true,
|
|
225
|
+
senderId: "agent",
|
|
226
|
+
senderName: "agent",
|
|
227
|
+
participantIds,
|
|
228
|
+
threadId: id(body.message_thread_id),
|
|
229
|
+
replyToMessageId: id(body.reply_to_message_id) ?? id(record(body.reply_parameters).message_id),
|
|
230
|
+
status: "requested"
|
|
231
|
+
});
|
|
232
|
+
await archive.append(event);
|
|
233
|
+
let response;
|
|
234
|
+
try {
|
|
235
|
+
response = await fetchImpl(input, init);
|
|
236
|
+
} catch (error) {
|
|
237
|
+
try {
|
|
238
|
+
await archive.append({
|
|
239
|
+
...event,
|
|
240
|
+
eventId: `${event.eventId}:result`,
|
|
241
|
+
status: "unknown"
|
|
242
|
+
});
|
|
243
|
+
} catch (storageError) {
|
|
244
|
+
console.error("[segmentstream] recording uncertain delivery failed", {
|
|
245
|
+
storageError,
|
|
246
|
+
cause: error
|
|
247
|
+
});
|
|
248
|
+
}
|
|
249
|
+
throw error;
|
|
250
|
+
}
|
|
251
|
+
const result = record(await response.clone().json());
|
|
252
|
+
await archive.append({
|
|
253
|
+
...event,
|
|
254
|
+
eventId: `${event.eventId}:result`,
|
|
255
|
+
status: response.ok && result.ok === true ? "completed" : "failed",
|
|
256
|
+
messageId: id(record(result.result).message_id)
|
|
257
|
+
});
|
|
258
|
+
return response;
|
|
259
|
+
};
|
|
260
|
+
}
|
|
261
|
+
function conversationIdentities(identities) {
|
|
262
|
+
return {
|
|
263
|
+
...identities,
|
|
264
|
+
lookup: async (account) => {
|
|
265
|
+
const inbound = currentInboundRequest();
|
|
266
|
+
const capture = inbound === void 0 ? void 0 : captures.get(inbound);
|
|
267
|
+
return account.provider === "telegram" && capture?.message.from?.id === account.externalId && capture.lookup !== void 0 ? capture.lookup : identities.lookup(account);
|
|
268
|
+
}
|
|
269
|
+
};
|
|
270
|
+
}
|
|
271
|
+
function telegramConversationCapture(input) {
|
|
272
|
+
return async (request, next) => {
|
|
273
|
+
if (!input.verify(request)) return next();
|
|
274
|
+
try {
|
|
275
|
+
return await captureTelegramWebhook({
|
|
276
|
+
...input,
|
|
277
|
+
request,
|
|
278
|
+
identities: input.identities(),
|
|
279
|
+
next
|
|
280
|
+
});
|
|
281
|
+
} catch (error) {
|
|
282
|
+
console.error("[segmentstream] conversation capture failed", { error });
|
|
283
|
+
return new Response("conversation storage unavailable", { status: 503 });
|
|
284
|
+
}
|
|
285
|
+
};
|
|
286
|
+
}
|
|
287
|
+
export { captureRouteDecider, captureTelegramFetch, captureTelegramWebhook, conversationIdentities, recordGateDecision, recordRouteDecision, telegramConversationCapture };
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { ChannelSource } from "eve/channels";
|
|
2
|
+
import type { SessionAuthContext } from "eve/context";
|
|
2
3
|
import type { Attachment } from "../attachments.js";
|
|
3
4
|
import type { SessionOperations } from "./sessions.js";
|
|
4
5
|
/** What became of one Telegram file of the message: stored as an attachment, or why not. */
|
|
@@ -32,6 +33,9 @@ export declare class InboundRequest {
|
|
|
32
33
|
sessions: SessionOperations | undefined;
|
|
33
34
|
/** Keeps work the request started, such as a chat report, alive after it answers. */
|
|
34
35
|
waitUntil: ((task: Promise<unknown>) => void) | undefined;
|
|
36
|
+
/** Continues the exact pending question's session, including under thread scope. */
|
|
37
|
+
address: string | undefined;
|
|
38
|
+
respond: ((address: string, requestId: string, decision: "approve" | "cancel", principal: SessionAuthContext) => Promise<unknown>) | undefined;
|
|
35
39
|
constructor(
|
|
36
40
|
/** The agent's description from the compiled manifest, for the routing decision. */
|
|
37
41
|
agentDescription: () => Promise<string>);
|
|
@@ -38,11 +38,14 @@ export interface InboundDependencies {
|
|
|
38
38
|
readonly identities: AgentIdentities;
|
|
39
39
|
readonly bot: BotIdentityLookup;
|
|
40
40
|
readonly decide: RouteDecider;
|
|
41
|
+
/** Full session history for open questions, which can survive compaction. */
|
|
42
|
+
readonly history?: HistoryReader;
|
|
41
43
|
readonly transcribe: Transcriber;
|
|
42
44
|
readonly download: (fileId: string) => Promise<Uint8Array>;
|
|
43
45
|
/** The attachment store; without it, files reach the turn as eve's Telegram file parts. */
|
|
44
46
|
readonly attachments?: AgentAttachments;
|
|
45
47
|
readonly observed: ObservedMessages;
|
|
48
|
+
/** Recent session events for smart routing. */
|
|
46
49
|
readonly readHistory: HistoryReader;
|
|
47
50
|
readonly chats: AgentChats;
|
|
48
51
|
/** The app's `/agent/connect` link for a pairing code. */
|
|
@@ -5,12 +5,15 @@ const __filename = __eveFileURLToPath(import.meta.url);
|
|
|
5
5
|
__eveDirname(__filename);
|
|
6
6
|
__eveCreateRequire(import.meta.url);
|
|
7
7
|
import { telegramPrincipal } from "./principal.mjs";
|
|
8
|
+
import { observedChatKey, observedContext } from "./observed.mjs";
|
|
9
|
+
import { recordGateDecision, recordRouteDecision } from "./conversations.mjs";
|
|
10
|
+
import { answerMarker } from "../questions.mjs";
|
|
11
|
+
import { formatAddress, messageAddress, scopedAddress } from "./sessions.mjs";
|
|
12
|
+
import { questionAddress, questionFromHistory } from "./confirmations.mjs";
|
|
8
13
|
import { chatContext } from "./chats.mjs";
|
|
9
14
|
import { attachmentsContext, stageFiles } from "./files.mjs";
|
|
10
|
-
import { observedChatKey, observedContext } from "./observed.mjs";
|
|
11
15
|
import { addressingReason, routeMessage } from "./router.mjs";
|
|
12
16
|
import { routingContext } from "./routing-context.mjs";
|
|
13
|
-
import { messageAddress, scopedAddress } from "./sessions.mjs";
|
|
14
17
|
import { voiceNote, voiceOf, voiceText } from "./voice.mjs";
|
|
15
18
|
const UNKNOWN_PRIVATE = "Hi, I don't know you yet. Connect your Telegram account to the workspace and I'll act as you. The button works for ten minutes.";
|
|
16
19
|
const UNKNOWN_IN_GROUP = "I don't know you yet. Message me privately to connect your account.";
|
|
@@ -141,6 +144,7 @@ async function handleInboundMessage(input) {
|
|
|
141
144
|
const left = message.raw.left_chat_member;
|
|
142
145
|
const migration = isGroupChat(message) ? migrationOf(message) : void 0;
|
|
143
146
|
if (isGroupChat(message) && isRecord(left)) {
|
|
147
|
+
recordGateDecision("member_departure");
|
|
144
148
|
await reportDeparture({
|
|
145
149
|
context,
|
|
146
150
|
message,
|
|
@@ -150,6 +154,7 @@ async function handleInboundMessage(input) {
|
|
|
150
154
|
return null;
|
|
151
155
|
}
|
|
152
156
|
if (migration !== void 0) {
|
|
157
|
+
recordGateDecision("chat_migration");
|
|
153
158
|
await dependencies.chats.migrated({
|
|
154
159
|
...migration,
|
|
155
160
|
title: message.chat.title ?? ""
|
|
@@ -175,6 +180,7 @@ async function handleInboundMessage(input) {
|
|
|
175
180
|
}) : void 0;
|
|
176
181
|
if (report !== void 0) input.request?.waitUntil?.(report);
|
|
177
182
|
if (lookup.status !== "paired") {
|
|
183
|
+
recordGateDecision(`identity_${lookup.status}`, addressed !== void 0 ? "reply" : "silent");
|
|
178
184
|
if (addressed !== void 0) await answerUnpaired({
|
|
179
185
|
context,
|
|
180
186
|
message: sent,
|
|
@@ -184,13 +190,32 @@ async function handleInboundMessage(input) {
|
|
|
184
190
|
});
|
|
185
191
|
return null;
|
|
186
192
|
}
|
|
187
|
-
|
|
193
|
+
let address = scopedAddress(messageAddress(sent), dependencies.sessions().scope);
|
|
188
194
|
const operations = input.request?.sessions;
|
|
195
|
+
const topicAddress = formatAddress({
|
|
196
|
+
chatId: sent.chat.id,
|
|
197
|
+
messageThreadId: String(sent.messageThreadId ?? ""),
|
|
198
|
+
conversationId: ""
|
|
199
|
+
});
|
|
200
|
+
const questionOwner = {
|
|
201
|
+
memberId: lookup.member.id,
|
|
202
|
+
chatId: sent.chat.id,
|
|
203
|
+
messageThreadId: String(sent.messageThreadId ?? "")
|
|
204
|
+
};
|
|
205
|
+
const memberQuestionAddress = questionAddress(questionOwner);
|
|
206
|
+
const questionSession = await operations?.resolveSession(memberQuestionAddress);
|
|
207
|
+
const owner = questionSession ?? await operations?.resolveSession(address) ?? await operations?.resolveSession(topicAddress);
|
|
208
|
+
const pending = owner === void 0 || dependencies.history === void 0 ? void 0 : questionFromHistory(await dependencies.history(owner.id), questionOwner);
|
|
209
|
+
let answering = pending !== void 0 && pending.memberId === lookup.member.id && pending.chatId === sent.chat.id && pending.messageThreadId === String(sent.messageThreadId ?? "");
|
|
210
|
+
if (answering && pending !== void 0 && input.request !== void 0) {
|
|
211
|
+
address = pending.address || (questionSession === void 0 ? topicAddress : memberQuestionAddress);
|
|
212
|
+
input.request.address = address;
|
|
213
|
+
}
|
|
189
214
|
const voice = voiceOf(sent.raw);
|
|
190
215
|
const caption = sent.text || sent.caption;
|
|
191
216
|
const notes = [];
|
|
192
217
|
let text = caption;
|
|
193
|
-
if (voice !== void 0 && (addressed !== void 0 || dependencies.policy === "smart")) {
|
|
218
|
+
if (voice !== void 0 && (addressed !== void 0 || answering || dependencies.policy === "smart")) {
|
|
194
219
|
const spoken = await voiceText({
|
|
195
220
|
voice,
|
|
196
221
|
model: dependencies.sttModel,
|
|
@@ -200,16 +225,19 @@ async function handleInboundMessage(input) {
|
|
|
200
225
|
text = [caption, spoken.text].filter((part) => part.trim() !== "").join("\n\n");
|
|
201
226
|
notes.push(spoken.note);
|
|
202
227
|
} else if (voice !== void 0) text = caption || voiceNote(voice);
|
|
203
|
-
if (text.trim() === "" && sent.attachments.length === 0)
|
|
228
|
+
if (text.trim() === "" && sent.attachments.length === 0) {
|
|
229
|
+
recordGateDecision("empty_message");
|
|
230
|
+
return null;
|
|
231
|
+
}
|
|
204
232
|
const chat = observedChatKey({
|
|
205
233
|
chatId: sent.chat.id,
|
|
206
234
|
...sent.messageThreadId === void 0 ? {} : { messageThreadId: sent.messageThreadId }
|
|
207
235
|
});
|
|
208
|
-
|
|
236
|
+
const route = await routeMessage({
|
|
209
237
|
chatId: sent.chat.id,
|
|
210
238
|
memberId: lookup.member.id,
|
|
211
239
|
policy: dependencies.policy,
|
|
212
|
-
addressed,
|
|
240
|
+
addressed: addressed ?? (answering ? "pending-question" : void 0),
|
|
213
241
|
decide: dependencies.decide,
|
|
214
242
|
question: async () => {
|
|
215
243
|
const recent = await routingContext({
|
|
@@ -225,19 +253,34 @@ async function handleInboundMessage(input) {
|
|
|
225
253
|
message: observedMessage(sent, text, lookup.member)
|
|
226
254
|
};
|
|
227
255
|
}
|
|
228
|
-
})
|
|
256
|
+
});
|
|
257
|
+
recordRouteDecision(route, addressed ?? (answering ? "pending-question" : void 0), dependencies.policy);
|
|
258
|
+
if (route === "observe") {
|
|
229
259
|
dependencies.observed.add(chat, observedMessage(sent, text === "" ? "[attachment]" : text, lookup.member));
|
|
230
260
|
return null;
|
|
231
261
|
}
|
|
232
262
|
if (voice !== void 0 && input.request !== void 0) input.request.text = text;
|
|
233
263
|
else if (voice !== void 0) notes.push(text);
|
|
264
|
+
if (answering && pending !== void 0 && input.request !== void 0) {
|
|
265
|
+
input.request.text = `${answerMarker(pending.id)}\n${text}`;
|
|
266
|
+
notes.push(`<pending_question>\nTask: ${pending.task}\nQuestion: ${pending.question}\nUse this member's answer to continue the pending task with their current permissions.
|
|
267
|
+
</pending_question>`);
|
|
268
|
+
}
|
|
234
269
|
if (operations !== void 0) {
|
|
235
270
|
const newest = dependencies.observed.list(chat).at(-1);
|
|
236
271
|
if (await dependencies.sessionPolicy.apply({
|
|
237
272
|
address,
|
|
238
273
|
operations,
|
|
239
274
|
...newest === void 0 ? {} : { activeAt: Date.parse(newest.at) || 0 }
|
|
240
|
-
}) === "cleared")
|
|
275
|
+
}) === "cleared") {
|
|
276
|
+
dependencies.observed.clear(chat);
|
|
277
|
+
if (answering && input.request !== void 0) {
|
|
278
|
+
answering = false;
|
|
279
|
+
input.request.text = text;
|
|
280
|
+
const questionNote = notes.findIndex((note) => note.startsWith("<pending_question>"));
|
|
281
|
+
if (questionNote !== -1) notes.splice(questionNote, 1);
|
|
282
|
+
}
|
|
283
|
+
}
|
|
241
284
|
dependencies.sessionPolicy.delivered(address);
|
|
242
285
|
}
|
|
243
286
|
await context.telegram.startTyping();
|
|
@@ -250,11 +293,18 @@ async function handleInboundMessage(input) {
|
|
|
250
293
|
if (input.request !== void 0 && files.size > 0) input.request.files = files;
|
|
251
294
|
await report;
|
|
252
295
|
dependencies.observed.remember(chat, observedMessage(sent, text, lookup.member));
|
|
296
|
+
const principal = telegramPrincipal({
|
|
297
|
+
message: sent,
|
|
298
|
+
member: lookup.member
|
|
299
|
+
});
|
|
253
300
|
return {
|
|
254
|
-
auth:
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
301
|
+
auth: answering && pending !== void 0 ? {
|
|
302
|
+
...principal,
|
|
303
|
+
attributes: {
|
|
304
|
+
...principal.attributes,
|
|
305
|
+
pendingQuestionId: pending.id
|
|
306
|
+
}
|
|
307
|
+
} : principal,
|
|
258
308
|
context: [
|
|
259
309
|
...observedContext(dependencies.observed.take(chat)),
|
|
260
310
|
...group ? chatContext(dependencies.chats.chat(sent.chat.id)) : [],
|
|
@@ -18,6 +18,7 @@ function telegramPrincipal(input) {
|
|
|
18
18
|
chatType: message.chat.type
|
|
19
19
|
};
|
|
20
20
|
if (member.name !== null) attributes.memberName = member.name;
|
|
21
|
+
if (message.messageThreadId !== void 0) attributes.messageThreadId = String(message.messageThreadId);
|
|
21
22
|
if (message.from.username !== void 0) attributes.username = message.from.username;
|
|
22
23
|
return {
|
|
23
24
|
authenticator: TELEGRAM_AUTHENTICATOR,
|
|
@@ -87,7 +87,9 @@ function requestContext(request) {
|
|
|
87
87
|
const lines = [
|
|
88
88
|
"<agent_request>",
|
|
89
89
|
`A request from ${requester.name} (${requester.kind}, user_id ${requester.id}) arrived through the ontology action agent_chat.send_message; the message is the request.`,
|
|
90
|
-
|
|
90
|
+
"This turn already runs in the target chat. Your reply here is the message delivered to this chat. Do not look for or call agent_chat.send_message to deliver it, even if the ontology catalog is empty or that action is unavailable.",
|
|
91
|
+
`Write in this chat${to}, carry out the clear request, ask only for missing or ambiguous information, and handle the replies here.`,
|
|
92
|
+
"If the addressed person has a linked Telegram username, mention them with @username; otherwise address them by name without a tag. Never invent a username."
|
|
91
93
|
];
|
|
92
94
|
if (requester.kind === "service" || !requester.actAs) lines.push(`${requester.kind === "service" ? "The requester is a service, not a member" : "The requester has not paired a Telegram account with the agent"}, so this turn runs without a member and the ontology is not available in it. Each member who replies acts with their own permissions.`);
|
|
93
95
|
lines.push("</agent_request>");
|
|
@@ -8,7 +8,7 @@ import type { ObservedMessage, RecentMessage } from "./observed.js";
|
|
|
8
8
|
* policy, put to one decision on the decision model.
|
|
9
9
|
*/
|
|
10
10
|
export type Route = "observe" | "reply";
|
|
11
|
-
export type AddressingReason = "private" | "mention" | "reply-to-bot" | "command";
|
|
11
|
+
export type AddressingReason = "private" | "mention" | "reply-to-bot" | "command" | "pending-question";
|
|
12
12
|
/** The bot, as Telegram's `getMe` describes it. Unknown when `getMe` failed. */
|
|
13
13
|
export interface BotIdentity {
|
|
14
14
|
readonly id: string;
|
|
@@ -23,8 +23,8 @@ async function routingContext(input) {
|
|
|
23
23
|
resetAt = Date.parse(event.meta.at);
|
|
24
24
|
}
|
|
25
25
|
const age = sentAt - Date.parse(event.meta.at);
|
|
26
|
-
const data = event.data;
|
|
27
|
-
if (event.type === "message.completed" && data !== void 0 && data.finishReason !== "tool-calls" && data.message.trim() !== "" && !isSilentReply(data.message) && age >= 0 && age <= 18e5) recovered.push(agentMessage({
|
|
26
|
+
const data = typeof event.data === "object" && event.data !== null ? event.data : void 0;
|
|
27
|
+
if (event.type === "message.completed" && data !== void 0 && typeof data.message === "string" && typeof data.finishReason === "string" && data.finishReason !== "tool-calls" && data.message.trim() !== "" && !isSilentReply(data.message) && age >= 0 && age <= 18e5) recovered.push(agentMessage({
|
|
28
28
|
text: data.message,
|
|
29
29
|
at: event.meta.at
|
|
30
30
|
}));
|
|
@@ -37,19 +37,16 @@ export interface SessionHistory {
|
|
|
37
37
|
/** Turns completed since `resetAt`; a compaction is worth nothing before the first. */
|
|
38
38
|
readonly turnsSinceReset: number;
|
|
39
39
|
}
|
|
40
|
-
/** One recorded event
|
|
40
|
+
/** One recorded event used by session policy, routing context and interaction projections. */
|
|
41
41
|
export interface RecordedEvent {
|
|
42
42
|
readonly type: string;
|
|
43
43
|
readonly meta: {
|
|
44
44
|
readonly at: string;
|
|
45
45
|
};
|
|
46
|
-
/**
|
|
47
|
-
readonly data?:
|
|
48
|
-
readonly message: string;
|
|
49
|
-
readonly finishReason: string;
|
|
50
|
-
};
|
|
46
|
+
/** Consumers validate the payload for the event type they project. */
|
|
47
|
+
readonly data?: unknown;
|
|
51
48
|
}
|
|
52
|
-
/**
|
|
49
|
+
/** Session events, oldest first; readers choose a tail or full stream for their consumers. */
|
|
53
50
|
export type HistoryReader = (sessionId: string) => Promise<ReadonlyArray<RecordedEvent>>;
|
|
54
51
|
/** How many events of the tail the reader fetches: enough for a day of a busy group. */
|
|
55
52
|
export declare const HISTORY_TAIL = 400;
|
|
@@ -4,9 +4,12 @@ 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 { actionApproval } from "../lib/confirmations.mjs";
|
|
8
|
+
import { actionToolName, ontologyToolLabels } from "../lib/ontology-tools.mjs";
|
|
7
9
|
import { attachmentForModel, attachmentOwner, optionalObject, optionalString, stringArgument, toolAttachments } from "../lib/attachment-tools.mjs";
|
|
8
10
|
import { defineTool } from "eve/tools";
|
|
9
11
|
var attachments_promote_default = defineTool({
|
|
12
|
+
approval: actionApproval({ labels: (memberId, _tool, input) => ontologyToolLabels()(memberId, actionToolName(stringArgument(input, "entity"), stringArgument(input, "action"))) }),
|
|
10
13
|
description: "Keep a file the member sent by storing it in a record through an ontology action that takes a file, such as document.upload. Give the entity, the action and the action's input without the file field: the platform fills it with the attachment, and fills sha256 when the action takes one. Runs with the member's permissions. The same file promoted again through the same action returns the earlier result instead of a duplicate.",
|
|
11
14
|
inputSchema: {
|
|
12
15
|
type: "object",
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
declare const _default: import("eve/tools").ToolDefinition<Record<string, unknown>, {
|
|
2
|
+
pendingQuestion: {
|
|
3
|
+
id: string;
|
|
4
|
+
question: string;
|
|
5
|
+
task: string;
|
|
6
|
+
memberId: string;
|
|
7
|
+
chatId: string;
|
|
8
|
+
messageThreadId: string;
|
|
9
|
+
address: string;
|
|
10
|
+
};
|
|
11
|
+
}>;
|
|
12
|
+
export default _default;
|
|
@@ -0,0 +1,50 @@
|
|
|
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 { memberIdOf } from "../lib/telegram/principal.mjs";
|
|
8
|
+
import { pendingQuestions, questionKey } from "../lib/questions.mjs";
|
|
9
|
+
import { stringArgument } from "../lib/attachment-tools.mjs";
|
|
10
|
+
import { defineTool } from "eve/tools";
|
|
11
|
+
var questions_ask_default = defineTool({
|
|
12
|
+
availableInSubagents: false,
|
|
13
|
+
description: "Ask for missing or ambiguous information needed to continue the member's task. Saves the task and question in the session, sends the question, and waits for their answer. Never use this for approval: destructive or irreversible actions have Confirm / Cancel buttons.",
|
|
14
|
+
inputSchema: {
|
|
15
|
+
type: "object",
|
|
16
|
+
properties: {
|
|
17
|
+
question: {
|
|
18
|
+
type: "string",
|
|
19
|
+
description: "The one question to send to the member."
|
|
20
|
+
},
|
|
21
|
+
task: {
|
|
22
|
+
type: "string",
|
|
23
|
+
description: "The pending task, including known targets and supplied values."
|
|
24
|
+
}
|
|
25
|
+
},
|
|
26
|
+
required: ["question", "task"],
|
|
27
|
+
additionalProperties: false
|
|
28
|
+
},
|
|
29
|
+
endsTurn: true,
|
|
30
|
+
execute(input, ctx) {
|
|
31
|
+
const memberId = memberIdOf(ctx.session.auth.current);
|
|
32
|
+
const chatId = ctx.session.auth.current?.attributes.chatId;
|
|
33
|
+
if (memberId === void 0 || typeof chatId !== "string") throw new Error("A member in a chat is required.");
|
|
34
|
+
const question = {
|
|
35
|
+
id: ctx.callId,
|
|
36
|
+
question: stringArgument(input, "question"),
|
|
37
|
+
task: stringArgument(input, "task"),
|
|
38
|
+
memberId,
|
|
39
|
+
chatId,
|
|
40
|
+
messageThreadId: String(ctx.session.auth.current?.attributes.messageThreadId ?? ""),
|
|
41
|
+
address: ""
|
|
42
|
+
};
|
|
43
|
+
pendingQuestions.update((all) => ({
|
|
44
|
+
...all,
|
|
45
|
+
[questionKey(question)]: question
|
|
46
|
+
}));
|
|
47
|
+
return { pendingQuestion: question };
|
|
48
|
+
}
|
|
49
|
+
});
|
|
50
|
+
export { questions_ask_default as default };
|
package/dist/tools/index.d.ts
CHANGED
|
@@ -5,3 +5,4 @@ export { default as attachments_list } from "../extension/tools/attachments_list
|
|
|
5
5
|
export { default as attachments_promote } from "../extension/tools/attachments_promote.js";
|
|
6
6
|
export { default as attachments_read } from "../extension/tools/attachments_read.js";
|
|
7
7
|
export { default as attachments_table } from "../extension/tools/attachments_table.js";
|
|
8
|
+
export { default as questions_ask } from "../extension/tools/questions_ask.js";
|
package/dist/tools/index.mjs
CHANGED
|
@@ -9,4 +9,5 @@ import attachments_list_default from "../extension/tools/attachments_list.mjs";
|
|
|
9
9
|
import attachments_promote_default from "../extension/tools/attachments_promote.mjs";
|
|
10
10
|
import attachments_read_default from "../extension/tools/attachments_read.mjs";
|
|
11
11
|
import attachments_table_default from "../extension/tools/attachments_table.mjs";
|
|
12
|
-
|
|
12
|
+
import questions_ask_default from "../extension/tools/questions_ask.mjs";
|
|
13
|
+
export { attachments_discard_default as attachments_discard, attachments_list_default as attachments_list, attachments_promote_default as attachments_promote, attachments_read_default as attachments_read, attachments_table_default as attachments_table, questions_ask_default as questions_ask };
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@segmentstream/agent",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.4.0",
|
|
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",
|