@segmentstream/agent 0.5.3 → 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.
Files changed (55) hide show
  1. package/README.md +140 -30
  2. package/dist/extension/config.d.ts +4 -0
  3. package/dist/extension/config.mjs +12 -3
  4. package/dist/extension/connections/ontology.mjs +2 -2
  5. package/dist/extension/instructions.md +10 -9
  6. package/dist/extension/lib/attachment-tools.mjs +1 -3
  7. package/dist/extension/lib/chat-post-confirmation.d.ts +23 -0
  8. package/dist/extension/lib/chat-post-confirmation.mjs +61 -0
  9. package/dist/extension/lib/confirmation-description.d.ts +2 -0
  10. package/dist/extension/lib/confirmations.d.ts +5 -0
  11. package/dist/extension/lib/confirmations.mjs +25 -5
  12. package/dist/extension/lib/conversation-runtime.mjs +18 -6
  13. package/dist/extension/lib/conversations.d.ts +12 -1
  14. package/dist/extension/lib/conversations.mjs +4 -3
  15. package/dist/extension/lib/ontology-connection.d.ts +4 -0
  16. package/dist/extension/lib/ontology-connection.mjs +46 -3
  17. package/dist/extension/lib/ontology-tools.d.ts +1 -0
  18. package/dist/extension/lib/ontology-tools.mjs +27 -2
  19. package/dist/extension/lib/telegram/adapter.d.ts +8 -2
  20. package/dist/extension/lib/telegram/adapter.mjs +52 -28
  21. package/dist/extension/lib/telegram/audience.d.ts +26 -0
  22. package/dist/extension/lib/telegram/audience.mjs +80 -0
  23. package/dist/extension/lib/telegram/chat-post-card.d.ts +15 -0
  24. package/dist/extension/lib/telegram/chat-post-card.mjs +65 -0
  25. package/dist/extension/lib/telegram/confirmations.d.ts +3 -2
  26. package/dist/extension/lib/telegram/confirmations.mjs +140 -21
  27. package/dist/extension/lib/telegram/content.d.ts +29 -0
  28. package/dist/extension/lib/telegram/content.mjs +161 -0
  29. package/dist/extension/lib/telegram/conversations.d.ts +2 -2
  30. package/dist/extension/lib/telegram/conversations.mjs +16 -8
  31. package/dist/extension/lib/telegram/customer-text.mjs +4 -3
  32. package/dist/extension/lib/telegram/files.d.ts +4 -0
  33. package/dist/extension/lib/telegram/files.mjs +33 -2
  34. package/dist/extension/lib/telegram/health.mjs +1 -1
  35. package/dist/extension/lib/telegram/images.d.ts +21 -0
  36. package/dist/extension/lib/telegram/images.mjs +47 -0
  37. package/dist/extension/lib/telegram/inbound-request.d.ts +6 -1
  38. package/dist/extension/lib/telegram/inbound-request.mjs +11 -1
  39. package/dist/extension/lib/telegram/inbound.d.ts +10 -5
  40. package/dist/extension/lib/telegram/inbound.mjs +82 -22
  41. package/dist/extension/lib/telegram/media-policy.d.ts +15 -0
  42. package/dist/extension/lib/telegram/media-policy.mjs +72 -0
  43. package/dist/extension/lib/telegram/observed.d.ts +8 -0
  44. package/dist/extension/lib/telegram/observed.mjs +10 -5
  45. package/dist/extension/lib/telegram/router.d.ts +10 -0
  46. package/dist/extension/lib/telegram/router.mjs +39 -32
  47. package/dist/extension/lib/telegram/runtime-request.d.ts +8 -0
  48. package/dist/extension/lib/telegram/runtime-request.mjs +36 -0
  49. package/dist/extension/lib/telegram/silent.d.ts +2 -0
  50. package/dist/extension/lib/telegram/silent.mjs +8 -2
  51. package/dist/extension/lib/telegram/voice.d.ts +1 -0
  52. package/dist/extension/lib/telegram/voice.mjs +12 -6
  53. package/package.json +1 -1
  54. package/dist/extension/lib/telegram/requests.d.ts +0 -80
  55. package/dist/extension/lib/telegram/requests.mjs +0 -186
@@ -9,11 +9,12 @@ import { runtimeToken } from "./runtime-token.mjs";
9
9
  function collapseConversationEvents(events) {
10
10
  const latest = new Map();
11
11
  for (const event of events) {
12
- const key = event.kind === "outbound" && /^telegram:(send|edit):/u.test(event.eventId) ? event.eventId.replace(/:result$/u, "") : event.eventId;
12
+ const key = event.kind === "outbound" && event.operationId != null ? `chat-delivery:${event.operationId}` : event.kind === "inbound" && event.provider === "telegram" && event.messageId !== null ? `telegram:${event.chatId}:${event.messageId}:message` : event.kind === "outbound" && /^telegram:(send|edit):/u.test(event.eventId) ? event.eventId.replace(/:result$/u, "") : event.eventId;
13
13
  const previous = latest.get(key);
14
14
  if (previous === void 0 || (/^\d+$/u.test(event.sequence) && /^\d+$/u.test(previous.sequence) ? BigInt(event.sequence) > BigInt(previous.sequence) : event.storedAt > previous.storedAt)) latest.set(key, {
15
15
  ...event,
16
- eventId: key
16
+ eventId: key,
17
+ ...event.kind === "outbound" && event.operationId != null && event.status === "requested" ? { status: "uncertain" } : {}
17
18
  });
18
19
  }
19
20
  return [...latest.values()];
@@ -95,7 +96,7 @@ async function storedConversationContext(archive, coordinates) {
95
96
  chatId: coordinates.chatId,
96
97
  ...coordinates.threadId === null ? {} : { threadId: coordinates.threadId },
97
98
  limit: 100
98
- })).events).filter((event) => event.kind === "inbound" || event.kind === "outbound" && event.status === "completed");
99
+ })).events).filter((event) => event.kind === "inbound" || event.kind === "outbound" && ["completed", "sent"].includes(event.status ?? ""));
99
100
  const selected = [];
100
101
  let characters = 0;
101
102
  for (const event of messages) {
@@ -34,4 +34,8 @@ export declare function ontologyConnection(input: {
34
34
  readonly environment?: Environment;
35
35
  readonly token?: RuntimeToken;
36
36
  readonly fetch?: typeof fetch;
37
+ /** Set only by the authenticated catalog handshake, never by model input. */
38
+ readonly chatPosting?: boolean;
37
39
  }): McpClientConnectionDefinition | null;
40
+ /** Negotiate before opening Eve's client: old strict schemas never receive the new envelope. */
41
+ export declare function ontologyConnectionForTurn(input: Parameters<typeof ontologyConnection>[0]): Promise<McpClientConnectionDefinition | null>;
@@ -6,7 +6,8 @@ __eveDirname(__filename);
6
6
  __eveCreateRequire(import.meta.url);
7
7
  import { appOrigin } from "./environment.mjs";
8
8
  import { memberIdOf } from "./telegram/principal.mjs";
9
- import { actionApproval } from "./confirmations.mjs";
9
+ import { CHAT_POST_ENVELOPE, chatPostDigest, chatPostOperationId, isChatPost } from "./chat-post-confirmation.mjs";
10
+ import { actionApproval, confirmations } from "./confirmations.mjs";
10
11
  import { runtimeToken, tokenExpiry } from "./runtime-token.mjs";
11
12
  import { ontologyTools } from "./ontology-tools.mjs";
12
13
  import { defineMcpClientConnection } from "eve/connections";
@@ -29,8 +30,36 @@ function ontologyConnection(input) {
29
30
  return defineMcpClientConnection({
30
31
  url: `${origin}/mcp`,
31
32
  description: DESCRIPTION,
32
- instanceKey: `${origin}/mcp#member:${memberId}`,
33
+ instanceKey: `${origin}/mcp#member:${memberId}${input.chatPosting ? ":chat-post-v1" : ""}`,
33
34
  approval: actionApproval(ontologyTools(input)),
35
+ ...input.chatPosting ? { toolCall: { providedArguments: { [CHAT_POST_ENVELOPE]: (ctx) => {
36
+ actAsHeaders({
37
+ principal: ctx.session.auth.current,
38
+ expectedMemberId: memberId
39
+ });
40
+ const base = {
41
+ version: 1,
42
+ session_id: ctx.session.id,
43
+ call_id: ctx.callId
44
+ };
45
+ if (!isChatPost(ctx.toolName)) return base;
46
+ const saved = confirmations.get()[ctx.callId];
47
+ const preview = saved?.description?.chatPost;
48
+ if (saved?.status !== "consumed" || saved.memberId !== memberId || saved.action.split("__").at(-1) !== ctx.toolName || saved.expiresAt <= Date.now() || preview === void 0 || saved.source === void 0) throw new Error("Exact chat posting requires the original unexpired confirmed call.");
49
+ return {
50
+ ...base,
51
+ operation_id: chatPostOperationId(ctx.session.id, ctx.callId),
52
+ source: {
53
+ provider: "telegram",
54
+ chat_id: saved.source.chatId,
55
+ thread_id: saved.source.threadId
56
+ },
57
+ expected: {
58
+ chat_id: preview.chatId,
59
+ digest: chatPostDigest(preview)
60
+ }
61
+ };
62
+ } } } } : {},
34
63
  auth: {
35
64
  credentialOwner: "app",
36
65
  getToken: async () => {
@@ -48,4 +77,18 @@ function ontologyConnection(input) {
48
77
  })
49
78
  });
50
79
  }
51
- export { ACT_AS_HEADER, MemberRequiredError, actAsHeaders, ontologyConnection };
80
+ async function ontologyConnectionForTurn(input) {
81
+ const memberId = memberIdOf(input.principal);
82
+ if (memberId === void 0) return null;
83
+ let chatPosting = false;
84
+ try {
85
+ chatPosting = await ontologyTools(input).supportsChatPosting(memberId);
86
+ } catch (error) {
87
+ console.warn("Chat posting capability unavailable; keeping the compatible ontology connection.", error);
88
+ }
89
+ return ontologyConnection({
90
+ ...input,
91
+ chatPosting
92
+ });
93
+ }
94
+ export { ACT_AS_HEADER, MemberRequiredError, actAsHeaders, ontologyConnection, ontologyConnectionForTurn };
@@ -7,6 +7,7 @@ export declare function ontologyTools(options?: {
7
7
  readonly token?: RuntimeToken;
8
8
  readonly fetch?: typeof fetch;
9
9
  }): {
10
+ supportsChatPosting(memberId: string): Promise<boolean>;
10
11
  labels(memberId: string, action: string): Promise<ActionAnnotations>;
11
12
  describe(memberId: string, action: string, input: Readonly<Record<string, unknown>>): Promise<import("./confirmation-description.js").ConfirmationDescription>;
12
13
  };
@@ -6,6 +6,7 @@ __eveDirname(__filename);
6
6
  __eveCreateRequire(import.meta.url);
7
7
  import { appOrigin } from "./environment.mjs";
8
8
  import { describeConfirmation, object } from "./confirmation-description.mjs";
9
+ import { describeChatPost, isChatPost, supportsChatPost } from "./chat-post-confirmation.mjs";
9
10
  import { redactDiagnostic } from "./diagnostics.mjs";
10
11
  import { runtimeToken } from "./runtime-token.mjs";
11
12
  function ontologyTools(options = {}) {
@@ -31,10 +32,12 @@ function ontologyTools(options = {}) {
31
32
  signal: signal ?? AbortSignal.timeout(15e3),
32
33
  redirect: "error"
33
34
  });
34
- if (!response.ok) throw new Error(`Cannot check action permission (HTTP ${response.status}).`);
35
+ if (!response.ok) throw new Error(`Cannot check action permission (HTTP ${response.status}).`, { cause: await response.text() });
35
36
  const body = object(await response.json());
36
37
  if (body.error !== void 0) throw new Error("Ontology metadata unavailable.", { cause: body.error });
37
- return object(body.result);
38
+ const result = object(body.result);
39
+ if (result.isError === true) throw new Error("The ontology preview read failed.", { cause: result });
40
+ return result;
38
41
  } catch (error) {
39
42
  throw new Error("The ontology metadata request failed.", { cause: redactDiagnostic(error, secrets) });
40
43
  }
@@ -51,12 +54,34 @@ function ontologyTools(options = {}) {
51
54
  };
52
55
  }
53
56
  return {
57
+ async supportsChatPosting(memberId) {
58
+ const result = await rpc(memberId, "tools/list", {});
59
+ return Array.isArray(result.tools) && result.tools.map(object).some(supportsChatPost);
60
+ },
54
61
  async labels(memberId, action) {
55
62
  return object((await toolFor(memberId, action)).tool.annotations);
56
63
  },
57
64
  async describe(memberId, action, input) {
58
65
  const signal = AbortSignal.timeout(15e3);
59
66
  const { tool, tools } = await toolFor(memberId, action, signal);
67
+ if (isChatPost(action)) {
68
+ if (!supportsChatPost(tool)) throw new Error("The ontology does not support exact chat confirmations.");
69
+ const read = async (name, args) => {
70
+ if (!tools.some((candidate) => candidate.name === name && object(candidate.annotations).readOnlyHint === true)) throw new Error("The chat preview reads are unavailable.");
71
+ const result = await rpc(memberId, "tools/call", {
72
+ name,
73
+ arguments: args
74
+ }, signal);
75
+ if (result.isError === true) throw new Error("The chat preview could not be resolved.");
76
+ return object(result.structuredContent);
77
+ };
78
+ const [caller, destinations] = await Promise.all([read("me", {}), read("chat_list", { chat: input.chat })]);
79
+ return describeChatPost({
80
+ values: input,
81
+ caller,
82
+ destinations
83
+ });
84
+ }
60
85
  let entities = [];
61
86
  try {
62
87
  const catalog = await rpc(memberId, "resources/read", { uri: "ontology://catalog" }, signal);
@@ -1,10 +1,12 @@
1
1
  import { type TelegramChannel } from "eve/channels/telegram";
2
- import type { SessionSettings, TelegramSettings } from "../../config.js";
2
+ import type { ChatModelChoice, SessionSettings, TelegramSettings } from "../../config.js";
3
3
  import { type AgentAttachments } from "../attachments.js";
4
4
  import { type Environment } from "../environment.js";
5
5
  import { type Conversations } from "../conversations.js";
6
6
  import { type RuntimeTokenVerifier } from "../runtime-identity.js";
7
7
  import { type RuntimeToken } from "../runtime-token.js";
8
+ import { type ImageDescriber } from "./images.js";
9
+ import { type CapabilityLookup } from "./media-policy.js";
8
10
  import { type ObservedMessages } from "./observed.js";
9
11
  import { type DecideFunction } from "./router.js";
10
12
  import { type HistoryReader } from "./sessions.js";
@@ -20,6 +22,8 @@ export interface TelegramAdapterOptions {
20
22
  readonly models: () => {
21
23
  readonly stt: string | false;
22
24
  readonly decision: string;
25
+ readonly chat?: ChatModelChoice | null;
26
+ readonly imageDescription?: string | false | null;
23
27
  };
24
28
  /** The mounted `sessions`. */
25
29
  readonly sessions: () => SessionSettings;
@@ -29,8 +33,10 @@ export interface TelegramAdapterOptions {
29
33
  /** For the proxy and the ontology service; tests pass a fake. */
30
34
  readonly fetch?: typeof fetch;
31
35
  readonly transcribe?: Transcriber;
36
+ readonly describeImage?: ImageDescriber;
37
+ readonly modelCapabilities?: CapabilityLookup;
32
38
  readonly observed?: ObservedMessages;
33
- /** Verifies callers of the messages route; tests pass one on a local key set. */
39
+ /** Verifies callers of the health route; tests pass one on a local key set. */
34
40
  readonly verify?: RuntimeTokenVerifier;
35
41
  /** Reads session events for routing, policy and interactions; tests pass a fake. */
36
42
  readonly history?: HistoryReader;
@@ -5,7 +5,8 @@ const __filename = __eveFileURLToPath(import.meta.url);
5
5
  __eveDirname(__filename);
6
6
  __eveCreateRequire(import.meta.url);
7
7
  import { appOrigin, channelsEnabled, proxyEnvironment, runtimeIdentityEnvironment, webhookSecret } from "../environment.mjs";
8
- import { conversationLanguage } from "./principal.mjs";
8
+ import { conversationLanguage, telegramPrincipal } from "./principal.mjs";
9
+ import { chooseModel } from "../../model.mjs";
9
10
  import { MAXIMUM_DOWNLOAD_BYTES, connectionProxy } from "../connection-proxy.mjs";
10
11
  import { runtimeToken } from "../runtime-token.mjs";
11
12
  import { agentAttachments, parseAttachmentUrl } from "../attachments.mjs";
@@ -20,13 +21,14 @@ import { botIdentityCache } from "./bot.mjs";
20
21
  import { confirmationLocale, confirmationRenderer } from "./confirmation-text.mjs";
21
22
  import { scopedAddress, sessionPolicy } from "./sessions.mjs";
22
23
  import { handleConfirmationCallback, telegramConfirmationEvents } from "./confirmations.mjs";
24
+ import { gatewayImageDescriber } from "./images.mjs";
25
+ import { gatewayMediaCapabilities, planMedia } from "./media-policy.mjs";
23
26
  import { agentChats } from "./chats.mjs";
24
27
  import { decisionRouter } from "./router.mjs";
25
28
  import { replyDelivery } from "./silent.mjs";
26
29
  import { downloadTelegramFile, gatewayTranscriber } from "./voice.mjs";
27
30
  import { handleInboundMessage } from "./inbound.mjs";
28
31
  import { TELEGRAM_PLACEHOLDER_TOKEN, TELEGRAM_PROXY_ORIGIN, telegramProxyFetch } from "./proxy-fetch.mjs";
29
- import { messagesRoute } from "./requests.mjs";
30
32
  import { healthRoute } from "./health.mjs";
31
33
  import { telegramFailureEvents } from "./turn-errors.mjs";
32
34
  import { telegramWebhookVerifier } from "./webhook.mjs";
@@ -40,7 +42,7 @@ const eveHistory = async (sessionId) => {
40
42
  })) events.push({
41
43
  type: event.type,
42
44
  meta: { at: event.meta.at },
43
- ...event.type === "message.completed" ? { data: {
45
+ ...event.type === "step.started" ? { data: { modelId: event.data.modelId } } : event.type === "message.completed" ? { data: {
44
46
  message: event.data.message,
45
47
  finishReason: event.data.finishReason
46
48
  } } : {}
@@ -63,6 +65,7 @@ function segmentstreamTelegram(options) {
63
65
  const observed = options.observed ?? observedMessages();
64
66
  const readHistory = options.history ?? eveHistory;
65
67
  const readInteractions = options.history ?? eveInteractions;
68
+ const capabilities = options.modelCapabilities ?? gatewayMediaCapabilities(options.fetch);
66
69
  const archive = options.conversations ?? conversations({
67
70
  environment,
68
71
  token,
@@ -147,6 +150,37 @@ function segmentstreamTelegram(options) {
147
150
  sessions: options.sessions,
148
151
  sessionPolicy: policy,
149
152
  sttModel: options.models().stt,
153
+ describeImage: options.describeImage ?? gatewayImageDescriber,
154
+ mediaPlan: async (member, sessionId) => {
155
+ let selected;
156
+ if (sessionId !== void 0) try {
157
+ for (const event of await readHistory(sessionId)) {
158
+ const data = event.data;
159
+ if (event.type === "step.started" && typeof data?.modelId === "string") selected = data.modelId;
160
+ }
161
+ } catch (error) {
162
+ reportTelegramError("media_session_model_lookup_failed", error, { sessionId });
163
+ }
164
+ else {
165
+ const choice = options.models().chat;
166
+ if (choice) selected = chooseModel({
167
+ choice,
168
+ principal: telegramPrincipal({
169
+ message: {
170
+ ...message,
171
+ from: message.from
172
+ },
173
+ member
174
+ }),
175
+ environment
176
+ });
177
+ }
178
+ return planMedia(selected === void 0 ? {
179
+ known: false,
180
+ images: false,
181
+ audio: false
182
+ } : await capabilities(selected), options.models().imageDescription ?? null);
183
+ },
150
184
  identities: conversationIdentities(pairing()),
151
185
  bot,
152
186
  decide: captureRouteDecider(decisionRouter({
@@ -193,7 +227,7 @@ function segmentstreamTelegram(options) {
193
227
  },
194
228
  events: {
195
229
  ...telegramFailureEvents,
196
- ...telegramConfirmationEvents(confirmationRenderer({ model: () => options.models().decision })),
230
+ ...telegramConfirmationEvents(confirmationRenderer({ model: () => options.models().decision }), archive),
197
231
  "message.completed": replyDelivery(observed)
198
232
  }
199
233
  });
@@ -228,28 +262,17 @@ function segmentstreamTelegram(options) {
228
262
  }, context.state.messageThreadId === null ? void 0 : String(context.state.messageThreadId));
229
263
  }]))
230
264
  } },
231
- routes: [
232
- ...channel.routes.map((route) => gatedRoute({
233
- route,
234
- options,
235
- environment,
236
- capture
237
- })),
238
- messagesRoute({
239
- channel,
240
- enabled: () => channelsEnabled(environment),
241
- configured: () => options.settings() !== null,
242
- verify: () => verify ??= runtimeTokenVerifier(runtimeIdentityEnvironment(environment)),
243
- sessionPolicy: policy,
244
- scope: () => options.sessions().scope
245
- }),
246
- healthRoute({
247
- chats,
248
- enabled: () => channelsEnabled(environment),
249
- configured: () => options.settings() !== null,
250
- verify: () => verify ??= runtimeTokenVerifier(runtimeIdentityEnvironment(environment))
251
- })
252
- ]
265
+ routes: [...channel.routes.map((route) => gatedRoute({
266
+ route,
267
+ options,
268
+ environment,
269
+ capture
270
+ })), healthRoute({
271
+ chats,
272
+ enabled: () => channelsEnabled(environment),
273
+ configured: () => options.settings() !== null,
274
+ verify: () => verify ??= runtimeTokenVerifier(runtimeIdentityEnvironment(environment))
275
+ })]
253
276
  };
254
277
  }
255
278
  function sessionIdOf(context) {
@@ -270,9 +293,10 @@ function gatedRoute(input) {
270
293
  resolveSession: args.resolveSession,
271
294
  from: args.from
272
295
  };
273
- inbound.respond = (address, requestId, decision, auth) => args.from(scoped(address)).respond([{
296
+ inbound.respond = (address, requestId, decision, auth, text) => args.from(scoped(address)).respond([{
274
297
  requestId,
275
- optionId: decision
298
+ optionId: decision,
299
+ ...text === void 0 ? {} : { text }
276
300
  }], { auth });
277
301
  const tasks = [];
278
302
  const waitUntil = (task) => {
@@ -0,0 +1,26 @@
1
+ import type { TelegramMessage } from "eve/channels/telegram";
2
+ import type { BotIdentity } from "./router.js";
3
+ type TargetKind = "bot" | "human" | "other_bot" | "unknown";
4
+ interface Recipient {
5
+ readonly kind: TargetKind;
6
+ readonly id?: string;
7
+ readonly username?: string;
8
+ readonly name?: string;
9
+ }
10
+ export interface MessageAudience {
11
+ readonly originalText: string;
12
+ readonly botTagged: boolean;
13
+ readonly mentions: ReadonlyArray<Recipient & {
14
+ readonly type: "mention" | "text_mention";
15
+ readonly text: string;
16
+ }>;
17
+ readonly replyTo: (Recipient & {
18
+ readonly messageId: string;
19
+ readonly text: string;
20
+ }) | null;
21
+ }
22
+ /** Current-message entities only; mentions inside a quoted/code range are not addressees. */
23
+ export declare function messageAudience(message: TelegramMessage, bot: BotIdentity | undefined): MessageAudience;
24
+ /** A known human reply or an explicit other recipient takes precedence over smart follow-ups. */
25
+ export declare function otherRecipient(audience: MessageAudience | undefined): string | undefined;
26
+ export {};
@@ -0,0 +1,80 @@
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 { messageContent, telegramRecord } from "./content.mjs";
8
+ function recipient(user, bot) {
9
+ const id = typeof user.id === "string" || typeof user.id === "number" ? String(user.id) : void 0;
10
+ const username = typeof user.username === "string" ? user.username : void 0;
11
+ return {
12
+ kind: bot !== void 0 && (id === bot.id || username !== void 0 && bot.username !== void 0 && username.toLowerCase() === bot.username.toLowerCase()) ? "bot" : user.is_bot === false ? "human" : user.is_bot === true && bot !== void 0 ? "other_bot" : "unknown",
13
+ ...id === void 0 ? {} : { id },
14
+ ...username === void 0 ? {} : { username },
15
+ ...typeof user.first_name === "string" ? { name: user.first_name } : {}
16
+ };
17
+ }
18
+ function messageAudience(message, bot) {
19
+ const content = messageContent(message.raw);
20
+ const text = content.source === "none" ? message.text || message.caption : content.text;
21
+ const mentions = [];
22
+ const quoted = (offset) => content.entities.some((entity) => [
23
+ "code",
24
+ "pre",
25
+ "blockquote",
26
+ "expandable_blockquote"
27
+ ].includes(entity.type) && offset >= entity.offset && offset < entity.offset + entity.length);
28
+ for (const entity of content.entities) {
29
+ if (entity.type !== "mention" && entity.type !== "text_mention" || quoted(entity.offset)) continue;
30
+ const label = text.slice(entity.offset, entity.offset + entity.length);
31
+ if (entity.type === "text_mention") mentions.push({
32
+ ...recipient(entity.user ?? {}, bot),
33
+ type: "text_mention",
34
+ text: label
35
+ });
36
+ else {
37
+ const username = entity.username ?? /^@([A-Za-z0-9_]+)$/u.exec(label)?.[1];
38
+ if (username !== void 0) mentions.push({
39
+ ...recipient({ username }, bot),
40
+ type: "mention",
41
+ text: label
42
+ });
43
+ }
44
+ }
45
+ for (const match of text.matchAll(/(?<![A-Za-z0-9_@])@([A-Za-z0-9_]{1,32})(?![A-Za-z0-9_])/gu)) {
46
+ if (quoted(match.index)) continue;
47
+ const username = match[1];
48
+ if (mentions.some((item) => item.username?.toLowerCase() === username.toLowerCase())) continue;
49
+ mentions.push({
50
+ ...recipient({ username }, bot),
51
+ type: "mention",
52
+ text: match[0]
53
+ });
54
+ }
55
+ const rawReply = telegramRecord(message.raw.reply_to_message);
56
+ const author = message.replyToMessage?.from;
57
+ const replyId = message.replyToMessage?.messageId ?? rawReply.message_id;
58
+ const replyTo = replyId === void 0 ? null : {
59
+ ...recipient(Object.keys(telegramRecord(rawReply.from)).length > 0 ? telegramRecord(rawReply.from) : {
60
+ id: author?.id,
61
+ username: author?.username,
62
+ is_bot: author?.isBot,
63
+ first_name: author?.firstName
64
+ }, bot),
65
+ messageId: String(replyId),
66
+ text: messageContent(rawReply).text.slice(0, 1e3)
67
+ };
68
+ return {
69
+ originalText: text,
70
+ mentions: mentions.slice(0, 50),
71
+ botTagged: mentions.some((item) => item.kind === "bot"),
72
+ replyTo
73
+ };
74
+ }
75
+ function otherRecipient(audience) {
76
+ if (audience === void 0 || audience.botTagged || audience.replyTo?.kind === "bot") return;
77
+ if (audience.replyTo?.kind === "human" || audience.replyTo?.kind === "other_bot") return "Reply targets another person or bot; this bot was not addressed.";
78
+ if (audience.mentions.some((item) => item.kind !== "bot")) return "Message tags another recipient; this bot was not addressed.";
79
+ }
80
+ export { messageAudience, otherRecipient };
@@ -0,0 +1,15 @@
1
+ import type { ChatPostPreview } from "../chat-post-confirmation.js";
2
+ import type { ConfirmationText } from "./confirmation-text.js";
3
+ /** Only the frozen deterministic card may bypass the generic technical-prose filter. */
4
+ export declare function withChatPostCard<T>(text: string, run: () => T): T;
5
+ export declare function isExactChatPostCard(text: string): boolean;
6
+ export declare function chatPostCopy(locale: string): {
7
+ edit: string;
8
+ changes: string;
9
+ uncertain: string;
10
+ sent: string;
11
+ history: string;
12
+ expired: string;
13
+ };
14
+ export declare function chatPostCard(preview: ChatPostPreview, locale: string): ConfirmationText;
15
+ export declare function chatPostReceipt(output: unknown, locale: string): string | undefined;
@@ -0,0 +1,65 @@
1
+ import { fileURLToPath as __eveFileURLToPath } from "node:url";
2
+ import { dirname as __eveDirname } from "node:path";
3
+ import { createRequire as __eveCreateRequire } from "node:module";
4
+ const __filename = __eveFileURLToPath(import.meta.url);
5
+ __eveDirname(__filename);
6
+ __eveCreateRequire(import.meta.url);
7
+ import { AsyncLocalStorage } from "node:async_hooks";
8
+ const MAX_PREVIEW_LENGTH = 3584;
9
+ const exactText = new AsyncLocalStorage();
10
+ function withChatPostCard(text, run) {
11
+ if (text.length > 4096) throw new Error("The complete chat confirmation cannot fit in one Telegram message.");
12
+ return exactText.run(text, run);
13
+ }
14
+ function isExactChatPostCard(text) {
15
+ return exactText.getStore() === text;
16
+ }
17
+ function chatPostCopy(locale) {
18
+ return locale.startsWith("ru") ? {
19
+ edit: "Изменить",
20
+ changes: "Что изменить в сообщении?",
21
+ uncertain: "Неизвестно, отправлено ли сообщение. Не отправляйте его повторно автоматически.",
22
+ sent: "Отправлено.",
23
+ history: "Не удалось записать квитанцию в историю.",
24
+ expired: "Срок подтверждения истёк. Ничего не отправлено."
25
+ } : {
26
+ edit: "Edit",
27
+ changes: "What would you like to change in the message?",
28
+ uncertain: "Delivery is uncertain. Do not resend automatically.",
29
+ sent: "Sent.",
30
+ history: "The receipt could not be recorded in history.",
31
+ expired: "This confirmation expired. Nothing was sent."
32
+ };
33
+ }
34
+ function chatPostCard(preview, locale) {
35
+ const russian = locale.startsWith("ru");
36
+ const text = [
37
+ russian ? "Отправить сообщение?" : "Send message?",
38
+ `${russian ? "Чат" : "Chat"}: ${preview.title} (${preview.provider})`,
39
+ ...preview.replyTo === null ? [] : [`${russian ? "В ответ на" : "Reply to"}: ${preview.replyTo}`],
40
+ "",
41
+ preview.finalText
42
+ ].join("\n");
43
+ if (text.length > MAX_PREVIEW_LENGTH) throw new Error("The full message and receipt cannot fit in one Telegram confirmation. Make a shorter draft.");
44
+ return {
45
+ text,
46
+ confirm: russian ? "Отправить" : "Confirm",
47
+ cancel: russian ? "Отмена" : "Cancel",
48
+ executing: russian ? "Подтверждено." : "Confirmed.",
49
+ cancelled: russian ? "Отменено." : "Cancelled.",
50
+ completed: chatPostCopy(locale).sent,
51
+ failed: russian ? "Ошибка отправки сообщения." : "Chat posting failed."
52
+ };
53
+ }
54
+ function chatPostReceipt(output, locale) {
55
+ const value = typeof output === "object" && output !== null ? output : {};
56
+ if (value.status !== "sent" && value.status !== "uncertain" || typeof value.history_recorded !== "boolean" || typeof value.duplicate !== "boolean" || value.message_id !== null && typeof value.message_id !== "string" || value.url !== null && typeof value.url !== "string") return;
57
+ const copy = chatPostCopy(locale);
58
+ const url = typeof value.url === "string" && value.url.length <= 256 && /^https:\/\/t\.me\/[A-Za-z0-9_/-]+$/u.test(value.url) ? value.url : null;
59
+ return [
60
+ value.status === "sent" ? copy.sent : copy.uncertain,
61
+ ...url === null ? [] : [url],
62
+ ...value.history_recorded ? [] : [copy.history]
63
+ ].join("\n");
64
+ }
65
+ export { chatPostCard, chatPostCopy, chatPostReceipt, isExactChatPostCard, withChatPostCard };
@@ -1,10 +1,11 @@
1
1
  import type { ChannelEvents } from "eve/channels";
2
2
  import type { TelegramCallbackQuery, TelegramChannelEvents, TelegramContext, TelegramEventContext } from "eve/channels/telegram";
3
+ import { type Conversations } from "../conversations.js";
3
4
  import type { AgentIdentities } from "../identities.js";
4
5
  import { type PendingQuestion } from "../questions.js";
5
6
  import { type ConfirmationRenderer } from "./confirmation-text.js";
6
7
  import { type HistoryReader, type RecordedEvent } from "./sessions.js";
7
- export declare function confirmationCallback(requestId: string, memberId: string, decision: "approve" | "cancel"): string;
8
+ export declare function confirmationCallback(requestId: string, memberId: string, decision: "approve" | "cancel" | "edit"): string;
8
9
  export declare function questionFromHistory(events: readonly RecordedEvent[], owner?: Pick<PendingQuestion, "memberId" | "chatId" | "messageThreadId">): PendingQuestion | undefined;
9
10
  export declare function questionAddress(owner: Pick<PendingQuestion, "memberId" | "chatId" | "messageThreadId">): string;
10
11
  /** Custom callback data avoids Eve's default, unpaired Telegram callback principal. */
@@ -16,5 +17,5 @@ export declare function handleConfirmationCallback(input: {
16
17
  readonly scope: "chat" | "thread";
17
18
  readonly now?: number;
18
19
  }): Promise<void>;
19
- export declare function telegramConfirmationEvents(render?: ConfirmationRenderer): TelegramChannelEvents & Pick<ChannelEvents<TelegramEventContext>, "context.cleared">;
20
+ export declare function telegramConfirmationEvents(render?: ConfirmationRenderer, archive?: Conversations): TelegramChannelEvents & Pick<ChannelEvents<TelegramEventContext>, "context.cleared">;
20
21
  export declare const confirmationEvents: TelegramChannelEvents & Pick<ChannelEvents<TelegramEventContext>, "context.cleared">;