@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.
Files changed (40) hide show
  1. package/README.md +18 -3
  2. package/dist/extension/_manifest.json +4 -1
  3. package/dist/extension/hooks/conversations.d.ts +2 -0
  4. package/dist/extension/hooks/conversations.mjs +10 -0
  5. package/dist/extension/instructions/conversations.d.ts +3 -0
  6. package/dist/extension/instructions/conversations.mjs +20 -0
  7. package/dist/extension/instructions.md +21 -6
  8. package/dist/extension/lib/confirmations.d.ts +26 -0
  9. package/dist/extension/lib/confirmations.mjs +96 -0
  10. package/dist/extension/lib/conversation-runtime.d.ts +19 -0
  11. package/dist/extension/lib/conversation-runtime.mjs +104 -0
  12. package/dist/extension/lib/conversations.d.ts +65 -0
  13. package/dist/extension/lib/conversations.mjs +107 -0
  14. package/dist/extension/lib/ontology-connection.d.ts +1 -0
  15. package/dist/extension/lib/ontology-connection.mjs +3 -0
  16. package/dist/extension/lib/ontology-tools.d.ts +11 -0
  17. package/dist/extension/lib/ontology-tools.mjs +41 -0
  18. package/dist/extension/lib/questions.d.ts +14 -0
  19. package/dist/extension/lib/questions.mjs +30 -0
  20. package/dist/extension/lib/telegram/adapter.d.ts +3 -1
  21. package/dist/extension/lib/telegram/adapter.mjs +63 -12
  22. package/dist/extension/lib/telegram/confirmations.d.ts +18 -0
  23. package/dist/extension/lib/telegram/confirmations.mjs +280 -0
  24. package/dist/extension/lib/telegram/conversations.d.ts +30 -0
  25. package/dist/extension/lib/telegram/conversations.mjs +287 -0
  26. package/dist/extension/lib/telegram/inbound-request.d.ts +4 -0
  27. package/dist/extension/lib/telegram/inbound-request.mjs +2 -0
  28. package/dist/extension/lib/telegram/inbound.d.ts +3 -0
  29. package/dist/extension/lib/telegram/inbound.mjs +63 -13
  30. package/dist/extension/lib/telegram/principal.mjs +1 -0
  31. package/dist/extension/lib/telegram/requests.mjs +3 -1
  32. package/dist/extension/lib/telegram/router.d.ts +1 -1
  33. package/dist/extension/lib/telegram/routing-context.mjs +2 -2
  34. package/dist/extension/lib/telegram/sessions.d.ts +4 -7
  35. package/dist/extension/tools/attachments_promote.mjs +3 -0
  36. package/dist/extension/tools/questions_ask.d.ts +12 -0
  37. package/dist/extension/tools/questions_ask.mjs +50 -0
  38. package/dist/tools/index.d.ts +1 -0
  39. package/dist/tools/index.mjs +2 -1
  40. package/package.json +1 -1
@@ -0,0 +1,30 @@
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 { defineState } from "eve/context";
8
+ const pendingQuestions = defineState("segmentstream.pending-questions", () => ({}));
9
+ function questionKey(question) {
10
+ return JSON.stringify([
11
+ question.memberId,
12
+ question.chatId,
13
+ question.messageThreadId
14
+ ]);
15
+ }
16
+ function isPendingQuestion(value) {
17
+ return typeof value === "object" && value !== null && [
18
+ "id",
19
+ "question",
20
+ "task",
21
+ "memberId",
22
+ "chatId",
23
+ "messageThreadId",
24
+ "address"
25
+ ].every((key) => typeof value[key] === "string");
26
+ }
27
+ function answerMarker(id) {
28
+ return `[Answer to pending question ${id}]`;
29
+ }
30
+ export { answerMarker, isPendingQuestion, pendingQuestions, questionKey };
@@ -2,6 +2,7 @@ import { type TelegramChannel } from "eve/channels/telegram";
2
2
  import type { SessionSettings, TelegramSettings } from "../../config.js";
3
3
  import { type AgentAttachments } from "../attachments.js";
4
4
  import { type Environment } from "../environment.js";
5
+ import { type Conversations } from "../conversations.js";
5
6
  import { type RuntimeTokenVerifier } from "../runtime-identity.js";
6
7
  import { type RuntimeToken } from "../runtime-token.js";
7
8
  import { type ObservedMessages } from "./observed.js";
@@ -31,10 +32,11 @@ export interface TelegramAdapterOptions {
31
32
  readonly observed?: ObservedMessages;
32
33
  /** Verifies callers of the messages route; tests pass one on a local key set. */
33
34
  readonly verify?: RuntimeTokenVerifier;
34
- /** Reads the tail of a session's durable stream; tests pass a fake. */
35
+ /** Reads session events for routing, policy and interactions; tests pass a fake. */
35
36
  readonly history?: HistoryReader;
36
37
  /** The attachment store; tests pass a fake. */
37
38
  readonly attachments?: AgentAttachments;
39
+ readonly conversations?: Conversations;
38
40
  }
39
41
  /** The Telegram channel of the extension. */
40
42
  export declare function segmentstreamTelegram(options: TelegramAdapterOptions): TelegramChannel;
@@ -9,16 +9,19 @@ import { MAXIMUM_DOWNLOAD_BYTES, connectionProxy } from "../connection-proxy.mjs
9
9
  import { runtimeToken } from "../runtime-token.mjs";
10
10
  import { agentAttachments, parseAttachmentUrl } from "../attachments.mjs";
11
11
  import { agentIdentities } from "../identities.mjs";
12
+ import { conversations } from "../conversations.mjs";
13
+ import { observedMessages } from "./observed.mjs";
14
+ import { InboundRequest, currentInboundRequest, runInboundRequest, withReplacedText } from "./inbound-request.mjs";
15
+ import { captureRouteDecider, captureTelegramFetch, conversationIdentities, telegramConversationCapture } from "./conversations.mjs";
12
16
  import { runtimeTokenVerifier } from "../runtime-identity.mjs";
13
17
  import { botIdentityCache } from "./bot.mjs";
18
+ import { scopedAddress, sessionPolicy } from "./sessions.mjs";
19
+ import { confirmationEvents, handleConfirmationCallback } from "./confirmations.mjs";
14
20
  import { agentChats } from "./chats.mjs";
15
- import { observedMessages } from "./observed.mjs";
16
21
  import { decisionRouter } from "./router.mjs";
17
22
  import { replyDelivery } from "./silent.mjs";
18
- import { scopedAddress, sessionPolicy } from "./sessions.mjs";
19
23
  import { downloadTelegramFile, gatewayTranscriber } from "./voice.mjs";
20
24
  import { handleInboundMessage } from "./inbound.mjs";
21
- import { InboundRequest, currentInboundRequest, runInboundRequest, withReplacedText } from "./inbound-request.mjs";
22
25
  import { TELEGRAM_PLACEHOLDER_TOKEN, TELEGRAM_PROXY_ORIGIN, telegramProxyFetch } from "./proxy-fetch.mjs";
23
26
  import { messagesRoute } from "./requests.mjs";
24
27
  import { withTurnErrorLogging } from "./turn-errors.mjs";
@@ -40,11 +43,27 @@ const eveHistory = async (sessionId) => {
40
43
  });
41
44
  return events;
42
45
  };
46
+ const eveInteractions = async (sessionId) => {
47
+ const { sessions } = await import("eve/server");
48
+ const events = [];
49
+ for await (const event of sessions.attach(sessionId).stream({ follow: false })) events.push({
50
+ type: event.type,
51
+ meta: { at: event.meta.at },
52
+ data: event.data
53
+ });
54
+ return events;
55
+ };
43
56
  function segmentstreamTelegram(options) {
44
57
  const environment = options.environment ?? process.env;
45
58
  const token = options.token ?? runtimeToken;
46
59
  const observed = options.observed ?? observedMessages();
47
60
  const readHistory = options.history ?? eveHistory;
61
+ const readInteractions = options.history ?? eveInteractions;
62
+ const archive = options.conversations ?? conversations({
63
+ environment,
64
+ token,
65
+ fetch: options.fetch
66
+ });
48
67
  const policy = sessionPolicy({
49
68
  settings: options.sessions,
50
69
  read: readHistory
@@ -81,6 +100,13 @@ function segmentstreamTelegram(options) {
81
100
  return identities;
82
101
  }
83
102
  const chats = agentChats({ identities: pairing });
103
+ const capture = telegramConversationCapture({
104
+ archive,
105
+ identities: pairing,
106
+ observed,
107
+ chats,
108
+ verify: telegramWebhookVerifier(() => webhookSecret(environment))
109
+ });
84
110
  let store = options.attachments;
85
111
  function attachments() {
86
112
  store ??= agentAttachments({
@@ -94,7 +120,7 @@ function segmentstreamTelegram(options) {
94
120
  api: {
95
121
  apiBaseUrl: TELEGRAM_PROXY_ORIGIN,
96
122
  fileBaseUrl: TELEGRAM_PROXY_ORIGIN,
97
- fetch: (input, init) => telegramProxyFetch(proxy())(input, init)
123
+ fetch: (input, init) => captureTelegramFetch(telegramProxyFetch(proxy()), archive, pairing)(input, init)
98
124
  },
99
125
  credentials: {
100
126
  botToken: TELEGRAM_PLACEHOLDER_TOKEN,
@@ -110,12 +136,12 @@ function segmentstreamTelegram(options) {
110
136
  sessions: options.sessions,
111
137
  sessionPolicy: policy,
112
138
  sttModel: options.models().stt,
113
- identities: pairing(),
139
+ identities: conversationIdentities(pairing()),
114
140
  bot,
115
- decide: decisionRouter({
141
+ decide: captureRouteDecider(decisionRouter({
116
142
  decide: options.decide,
117
143
  model: options.models().decision
118
- }),
144
+ })),
119
145
  transcribe: options.transcribe ?? gatewayTranscriber,
120
146
  download: (fileId) => downloadTelegramFile({
121
147
  proxy: proxy(),
@@ -124,11 +150,22 @@ function segmentstreamTelegram(options) {
124
150
  attachments: attachments(),
125
151
  observed,
126
152
  readHistory,
153
+ history: readInteractions,
127
154
  chats,
128
155
  connectUrl: (code) => `${appOrigin(environment)}/agent/connect?code=${encodeURIComponent(code)}`
129
156
  }
130
157
  }),
131
- events: { "message.completed": replyDelivery(observed) }
158
+ onCallbackQuery: (context, query) => handleConfirmationCallback({
159
+ context,
160
+ query,
161
+ identities: pairing(),
162
+ read: readInteractions,
163
+ scope: options.sessions().scope
164
+ }),
165
+ events: {
166
+ ...confirmationEvents,
167
+ "message.completed": replyDelivery(observed)
168
+ }
132
169
  });
133
170
  const adapter = channel.adapter;
134
171
  const fetchTelegramFile = adapter?.fetchFile;
@@ -158,7 +195,8 @@ function segmentstreamTelegram(options) {
158
195
  routes: [...channel.routes.map((route) => gatedRoute({
159
196
  route,
160
197
  options,
161
- environment
198
+ environment,
199
+ capture
162
200
  })), messagesRoute({
163
201
  channel,
164
202
  enabled: () => channelsEnabled(environment),
@@ -188,9 +226,22 @@ function gatedRoute(input) {
188
226
  from: args.from
189
227
  };
190
228
  inbound.waitUntil = args.waitUntil;
191
- return runInboundRequest(inbound, () => route.handler(request, {
192
- ...args,
193
- from: (address) => withReplacedText(args.from(scoped(address)), inbound)
229
+ inbound.respond = (address, requestId, decision, auth) => args.from(scoped(address)).respond([{
230
+ requestId,
231
+ optionId: decision
232
+ }], { auth });
233
+ return runInboundRequest(inbound, () => input.capture(request, async () => {
234
+ const tasks = [];
235
+ const response = await route.handler(request, {
236
+ ...args,
237
+ waitUntil: (task) => {
238
+ tasks.push(task);
239
+ args.waitUntil(task);
240
+ },
241
+ from: (address) => withReplacedText(args.from(inbound.address ?? scoped(address)), inbound)
242
+ });
243
+ await Promise.all(tasks);
244
+ return response;
194
245
  }));
195
246
  }
196
247
  };
@@ -0,0 +1,18 @@
1
+ import type { ChannelEvents } from "eve/channels";
2
+ import type { TelegramCallbackQuery, TelegramChannelEvents, TelegramContext, TelegramEventContext } from "eve/channels/telegram";
3
+ import type { AgentIdentities } from "../identities.js";
4
+ import { type PendingQuestion } from "../questions.js";
5
+ import { type HistoryReader, type RecordedEvent } from "./sessions.js";
6
+ export declare function confirmationCallback(requestId: string, memberId: string, decision: "approve" | "cancel"): string;
7
+ export declare function questionFromHistory(events: readonly RecordedEvent[], owner?: Pick<PendingQuestion, "memberId" | "chatId" | "messageThreadId">): PendingQuestion | undefined;
8
+ export declare function questionAddress(owner: Pick<PendingQuestion, "memberId" | "chatId" | "messageThreadId">): string;
9
+ /** Custom callback data avoids Eve's default, unpaired Telegram callback principal. */
10
+ export declare function handleConfirmationCallback(input: {
11
+ readonly context: TelegramContext;
12
+ readonly query: TelegramCallbackQuery;
13
+ readonly identities: AgentIdentities;
14
+ readonly read: HistoryReader;
15
+ readonly scope: "chat" | "thread";
16
+ readonly now?: number;
17
+ }): Promise<void>;
18
+ export declare const confirmationEvents: TelegramChannelEvents & Pick<ChannelEvents<TelegramEventContext>, "context.cleared">;
@@ -0,0 +1,280 @@
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, telegramPrincipal } from "./principal.mjs";
8
+ import { confirmations } from "../confirmations.mjs";
9
+ import { currentInboundRequest } from "./inbound-request.mjs";
10
+ import { answerMarker, isPendingQuestion, pendingQuestions, questionKey } from "../questions.mjs";
11
+ import { formatAddress, scopedAddress } from "./sessions.mjs";
12
+ import { registerTelegramFreeformPrompt, renderTelegramInputRequest } from "eve/channels/telegram";
13
+ import { createHash } from "node:crypto";
14
+ const PREFIX = "ss-confirm:";
15
+ function shortHash(value) {
16
+ return createHash("sha256").update(value).digest("hex").slice(0, 20);
17
+ }
18
+ function confirmationCallback(requestId, memberId, decision) {
19
+ return `${PREFIX}${shortHash(requestId)}:${shortHash(memberId)}:${decision === "approve" ? "y" : "n"}`;
20
+ }
21
+ function object(value) {
22
+ return typeof value === "object" && value !== null ? value : {};
23
+ }
24
+ function openConfirmations(events) {
25
+ const open = new Map();
26
+ for (const event of events) {
27
+ const data = object(event.data);
28
+ if (event.type === "context.cleared") open.clear();
29
+ if (event.type === "input.requested" && Array.isArray(data.requests)) for (const item of data.requests) {
30
+ const request = object(item);
31
+ const action = object(request.action);
32
+ if (request.kind === "tool-approval" && typeof request.requestId === "string" && typeof action.callId === "string") open.set(request.requestId, {
33
+ requestId: request.requestId,
34
+ callId: action.callId,
35
+ at: Date.parse(event.meta.at)
36
+ });
37
+ }
38
+ if (event.type === "input.resolved" && Array.isArray(data.resolutions)) for (const item of data.resolutions) {
39
+ const id = object(item).requestId;
40
+ if (typeof id === "string") open.delete(id);
41
+ }
42
+ }
43
+ return [...open.values()];
44
+ }
45
+ function questionFromHistory(events, owner) {
46
+ const pending = new Map();
47
+ for (const event of events) {
48
+ const data = object(event.data);
49
+ if (event.type === "context.cleared") pending.clear();
50
+ if (event.type === "action.result") {
51
+ const result = object(data.result);
52
+ const candidate = object(result.output).pendingQuestion;
53
+ if (typeof result.toolName === "string" && result.toolName.endsWith("__questions_ask") && isPendingQuestion(candidate)) pending.set(questionKey(candidate), candidate);
54
+ }
55
+ if (event.type === "message.received" && typeof data.message === "string" && data.message.startsWith("[Answer to pending question ")) {
56
+ for (const [key, question] of pending) if (data.message.startsWith(answerMarker(question.id))) pending.delete(key);
57
+ }
58
+ }
59
+ return owner === void 0 ? [...pending.values()].at(-1) : pending.get(questionKey(owner));
60
+ }
61
+ function questionAddress(owner) {
62
+ return formatAddress({
63
+ ...owner,
64
+ conversationId: `question-${shortHash(owner.memberId)}`
65
+ });
66
+ }
67
+ async function handleConfirmationCallback(input) {
68
+ const { context, query } = input;
69
+ const notice = (text) => context.telegram.answerCallbackQuery({
70
+ callbackQueryId: query.id,
71
+ text
72
+ });
73
+ if (!query.data?.startsWith(PREFIX) || query.message === void 0 || query.from.isBot) {
74
+ await notice("Unsupported action.");
75
+ return;
76
+ }
77
+ const [requestHash, memberHash, choice] = query.data.slice(11).split(":");
78
+ const lookup = await input.identities.lookup({
79
+ provider: "telegram",
80
+ externalId: query.from.id
81
+ });
82
+ if (lookup.status !== "paired" || shortHash(lookup.member.id) !== memberHash) {
83
+ await notice("Only the requesting member can confirm or cancel this action.");
84
+ return;
85
+ }
86
+ const address = scopedAddress(formatAddress({
87
+ chatId: query.message.chat.id,
88
+ messageThreadId: String(query.message.messageThreadId ?? ""),
89
+ conversationId: query.message.chat.type === "private" ? "" : query.message.messageId
90
+ }), input.scope);
91
+ const session = await (currentInboundRequest()?.sessions)?.resolveSession(address);
92
+ const prompt = session === void 0 ? void 0 : openConfirmations(await input.read(session.id)).find((candidate) => shortHash(candidate.requestId) === requestHash);
93
+ if (prompt === void 0 || choice !== "y" && choice !== "n") {
94
+ await notice("This confirmation is no longer pending.");
95
+ return;
96
+ }
97
+ const expired = !Number.isFinite(prompt.at) || prompt.at + 6e5 <= (input.now ?? Date.now());
98
+ const decision = expired || choice === "n" ? "cancel" : "approve";
99
+ const principal = telegramPrincipal({
100
+ member: lookup.member,
101
+ message: {
102
+ ...query.message,
103
+ from: query.from,
104
+ text: "",
105
+ caption: "",
106
+ raw: {},
107
+ attachments: []
108
+ }
109
+ });
110
+ const source = currentInboundRequest()?.respond;
111
+ if (source === void 0) throw new Error("Confirmation callback has no session continuation.");
112
+ await source(address, prompt.requestId, decision, {
113
+ ...principal,
114
+ attributes: {
115
+ ...principal.attributes,
116
+ confirmationRequestId: prompt.requestId,
117
+ confirmationDecision: decision
118
+ }
119
+ });
120
+ await notice(expired ? "This confirmation expired; nothing executed." : decision === "cancel" ? "Cancellation received." : "Confirmation received.");
121
+ }
122
+ const confirmationEvents = {
123
+ "context.cleared"(_data, channel) {
124
+ confirmations.update(() => ({}));
125
+ pendingQuestions.update(() => ({}));
126
+ const state = channel.state;
127
+ state.confirmationMessages = {};
128
+ state.pendingQuestions = {};
129
+ },
130
+ async "input.requested"(data, channel, ctx) {
131
+ for (const request of data.requests) {
132
+ if (request.kind !== "tool-approval") {
133
+ const rendered = renderTelegramInputRequest(request, channel.state);
134
+ const sent = await channel.telegram.post({
135
+ text: rendered.text,
136
+ reply_markup: rendered.replyMarkup
137
+ });
138
+ if (rendered.freeformRequestId !== void 0) registerTelegramFreeformPrompt(channel.state, {
139
+ messageId: sent.id,
140
+ requestId: request.requestId
141
+ });
142
+ else channel.state.hitlPrompts = {
143
+ ...channel.state.hitlPrompts,
144
+ [request.requestId]: {
145
+ messageId: sent.id,
146
+ text: rendered.text,
147
+ options: (request.options ?? []).map(({ id, label }) => ({
148
+ id,
149
+ label
150
+ }))
151
+ }
152
+ };
153
+ continue;
154
+ }
155
+ const memberId = confirmations.get()[request.action.callId]?.memberId ?? memberIdOf(ctx.session.auth.current);
156
+ if (memberId === void 0) throw new Error("Confirmation requires a member.");
157
+ const text = `Confirm ${request.action.toolName}?\n${JSON.stringify(request.action.input, null, 2)}`.slice(0, 3900);
158
+ const sent = await channel.telegram.post({
159
+ text,
160
+ reply_markup: { inline_keyboard: [[{
161
+ text: "Confirm",
162
+ callback_data: confirmationCallback(request.requestId, memberId, "approve")
163
+ }, {
164
+ text: "Cancel",
165
+ callback_data: confirmationCallback(request.requestId, memberId, "cancel")
166
+ }]] }
167
+ });
168
+ const state = channel.state;
169
+ state.confirmationMessages = {
170
+ ...state.confirmationMessages,
171
+ [request.action.callId]: {
172
+ requestId: request.requestId,
173
+ messageId: sent.id,
174
+ text
175
+ }
176
+ };
177
+ }
178
+ },
179
+ async "input.resolved"(data, channel) {
180
+ const state = channel.state;
181
+ for (const resolution of data.resolutions) {
182
+ const entry = Object.entries(state.confirmationMessages ?? {}).find(([, message]) => message.requestId === resolution.requestId);
183
+ if (entry === void 0) {
184
+ const prompt = channel.state.hitlPrompts?.[resolution.requestId];
185
+ if (prompt !== void 0) {
186
+ const response = resolution.response;
187
+ const label = response?.text ?? prompt.options.find((option) => option.id === response?.optionId)?.label;
188
+ await channel.telegram.editMessageText({
189
+ messageId: prompt.messageId,
190
+ replyMarkup: { inline_keyboard: [] },
191
+ text: `${prompt.text}\n\n${label ?? resolution.outcome}`
192
+ });
193
+ delete channel.state.hitlPrompts?.[resolution.requestId];
194
+ }
195
+ continue;
196
+ }
197
+ const [callId, message] = entry;
198
+ const text = resolution.outcome === "approved" ? "Confirmed. Executing…" : "Cancelled. Nothing executed.";
199
+ await channel.telegram.editMessageText({
200
+ messageId: message.messageId,
201
+ replyMarkup: { inline_keyboard: [] },
202
+ text: `${message.text}\n\n${text}`
203
+ });
204
+ if (resolution.outcome !== "approved") {
205
+ delete state.confirmationMessages?.[callId];
206
+ confirmations.update((all) => ({
207
+ ...all,
208
+ ...all[callId] === void 0 ? {} : { [callId]: {
209
+ ...all[callId],
210
+ status: "cancelled"
211
+ } }
212
+ }));
213
+ }
214
+ }
215
+ },
216
+ async "action.result"(data, channel) {
217
+ const state = channel.state;
218
+ if (data.result.kind === "tool-result") {
219
+ const question = object(data.result.output).pendingQuestion;
220
+ if (data.result.toolName.endsWith("__questions_ask") && isPendingQuestion(question)) {
221
+ const address = channel.continuation?.token ?? formatAddress({
222
+ chatId: channel.telegram.chatId,
223
+ messageThreadId: String(channel.telegram.messageThreadId ?? ""),
224
+ conversationId: channel.telegram.conversationId ?? ""
225
+ });
226
+ const saved = {
227
+ ...question,
228
+ address
229
+ };
230
+ pendingQuestions.update((all) => ({
231
+ ...all,
232
+ [questionKey(saved)]: saved
233
+ }));
234
+ state.pendingQuestions = {
235
+ ...state.pendingQuestions,
236
+ [questionKey(saved)]: saved
237
+ };
238
+ channel.continuation?.alias(formatAddress({
239
+ chatId: question.chatId,
240
+ messageThreadId: question.messageThreadId,
241
+ conversationId: ""
242
+ }));
243
+ channel.continuation?.alias(questionAddress(saved));
244
+ await channel.telegram.post({
245
+ text: question.question,
246
+ reply_markup: {
247
+ force_reply: true,
248
+ selective: true
249
+ }
250
+ });
251
+ }
252
+ }
253
+ const message = state.confirmationMessages?.[data.result.callId];
254
+ if (message === void 0) return;
255
+ 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);
256
+ const result = data.result.kind === "tool-result" ? JSON.stringify(data.result.output) : "";
257
+ await channel.telegram.editMessageText({
258
+ messageId: message.messageId,
259
+ replyMarkup: { inline_keyboard: [] },
260
+ text: `${message.text}\n\n${failed ? "Failed" : "Completed"}: ${result}`.slice(0, 4096)
261
+ });
262
+ delete state.confirmationMessages?.[data.result.callId];
263
+ },
264
+ "turn.started"(_data, channel, ctx) {
265
+ const memberId = memberIdOf(ctx.session.auth.current);
266
+ const answeredId = ctx.session.auth.current?.attributes.pendingQuestionId;
267
+ if (memberId !== void 0 && typeof answeredId === "string") {
268
+ const key = questionKey({
269
+ memberId,
270
+ chatId: channel.telegram.chatId,
271
+ messageThreadId: String(channel.telegram.messageThreadId ?? "")
272
+ });
273
+ if (pendingQuestions.get()[key]?.id === answeredId) {
274
+ pendingQuestions.update((all) => Object.fromEntries(Object.entries(all).filter(([name]) => name !== key)));
275
+ delete channel.state.pendingQuestions?.[key];
276
+ }
277
+ }
278
+ }
279
+ };
280
+ export { confirmationCallback, confirmationEvents, handleConfirmationCallback, questionAddress, questionFromHistory };
@@ -0,0 +1,30 @@
1
+ import type { AgentIdentities } from "../identities.js";
2
+ import { type Conversations } from "../conversations.js";
3
+ import type { AgentChats } from "./chats.js";
4
+ import type { ObservedMessages } from "./observed.js";
5
+ import type { AddressingReason, RouteDecider } from "./router.js";
6
+ /** Runs after webhook verification, before eve drops bot/channel/service updates. */
7
+ export declare function captureTelegramWebhook(input: {
8
+ readonly request: Request;
9
+ readonly archive: Conversations;
10
+ readonly identities: AgentIdentities;
11
+ readonly observed: ObservedMessages;
12
+ readonly chats?: AgentChats;
13
+ readonly next: () => Promise<Response>;
14
+ }): Promise<Response>;
15
+ /** Additive inbound hook: the router keeps ownership of the reply policy. */
16
+ export declare function recordGateDecision(reason: string, decision?: "reply" | "silent"): void;
17
+ export declare function recordRouteDecision(route: "reply" | "observe", addressed: boolean | AddressingReason | undefined, policy: string): void;
18
+ export declare function captureRouteDecider(decide: RouteDecider): RouteDecider;
19
+ /** Captures every Telegram sendMessage chunk, including pairing/HITL/proactive replies. */
20
+ export declare function captureTelegramFetch(fetchImpl: typeof fetch, archive: Conversations, identities: () => AgentIdentities): typeof fetch;
21
+ /** Reuses the request's verified lookup; it never survives into another inbound request. */
22
+ export declare function conversationIdentities(identities: AgentIdentities): AgentIdentities;
23
+ /** Verification and failure acknowledgement stay outside eve's swallowed onMessage failures. */
24
+ export declare function telegramConversationCapture(input: {
25
+ readonly archive: Conversations;
26
+ readonly identities: () => AgentIdentities;
27
+ readonly observed: ObservedMessages;
28
+ readonly chats: AgentChats;
29
+ readonly verify: (request: Request) => boolean;
30
+ }): (request: Request, next: () => Promise<Response>) => Promise<Response>;