@segmentstream/agent 0.1.4 → 0.2.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 +64 -21
  2. package/dist/extension/instructions.md +11 -0
  3. package/dist/extension/lib/attachment-tools.d.ts +68 -0
  4. package/dist/extension/lib/attachment-tools.mjs +110 -0
  5. package/dist/extension/lib/attachments.d.ts +96 -0
  6. package/dist/extension/lib/attachments.mjs +233 -0
  7. package/dist/extension/lib/connection-proxy.d.ts +14 -1
  8. package/dist/extension/lib/connection-proxy.mjs +135 -39
  9. package/dist/extension/lib/environment.d.ts +13 -0
  10. package/dist/extension/lib/environment.mjs +8 -1
  11. package/dist/extension/lib/identities.d.ts +22 -1
  12. package/dist/extension/lib/identities.mjs +20 -0
  13. package/dist/extension/lib/runtime-identity.d.ts +22 -0
  14. package/dist/extension/lib/runtime-identity.mjs +62 -0
  15. package/dist/extension/lib/telegram/adapter.d.ts +6 -0
  16. package/dist/extension/lib/telegram/adapter.mjs +56 -6
  17. package/dist/extension/lib/telegram/chats.d.ts +46 -0
  18. package/dist/extension/lib/telegram/chats.mjs +123 -0
  19. package/dist/extension/lib/telegram/files.d.ts +25 -0
  20. package/dist/extension/lib/telegram/files.mjs +77 -0
  21. package/dist/extension/lib/telegram/inbound-request.d.ts +22 -1
  22. package/dist/extension/lib/telegram/inbound-request.mjs +30 -1
  23. package/dist/extension/lib/telegram/inbound.d.ts +17 -3
  24. package/dist/extension/lib/telegram/inbound.mjs +80 -0
  25. package/dist/extension/lib/telegram/proxy-fetch.mjs +3 -2
  26. package/dist/extension/lib/telegram/requests.d.ts +71 -0
  27. package/dist/extension/lib/telegram/requests.mjs +173 -0
  28. package/dist/extension/lib/telegram/voice.d.ts +2 -2
  29. package/dist/extension/lib/telegram/voice.mjs +2 -2
  30. package/dist/extension/tools/attachments_discard.d.ts +30 -0
  31. package/dist/extension/tools/attachments_discard.mjs +22 -0
  32. package/dist/extension/tools/attachments_list.d.ts +30 -0
  33. package/dist/extension/tools/attachments_list.mjs +25 -0
  34. package/dist/extension/tools/attachments_promote.d.ts +36 -0
  35. package/dist/extension/tools/attachments_promote.mjs +69 -0
  36. package/dist/extension/tools/attachments_read.d.ts +9 -0
  37. package/dist/extension/tools/attachments_read.mjs +43 -0
  38. package/dist/tools/index.d.ts +4 -0
  39. package/dist/tools/index.mjs +5 -1
  40. package/package.json +3 -2
@@ -0,0 +1,123 @@
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
+ const MAXIMUM_WHEN_TO_USE_LENGTH = 1e3;
8
+ const REPORT_INTERVAL_MS = 6e5;
9
+ const MAXIMUM_CHATS = 500;
10
+ function agentChats(input) {
11
+ const chats = new Map();
12
+ const now = input.now ?? Date.now;
13
+ const log = input.log ?? ((message, error) => console.warn(message, error));
14
+ function remember(chatId, value) {
15
+ chats.delete(chatId);
16
+ chats.set(chatId, value);
17
+ if (chats.size > MAXIMUM_CHATS) {
18
+ const oldest = chats.keys().next();
19
+ if (oldest.done !== true) chats.delete(oldest.value);
20
+ }
21
+ }
22
+ function restore(chatId, written, previous) {
23
+ if (chats.get(chatId) !== written) return;
24
+ if (previous === void 0) chats.delete(chatId);
25
+ else chats.set(chatId, previous);
26
+ }
27
+ async function report(chatId, fallback, body) {
28
+ try {
29
+ const chat = await input.identities().seenChat({
30
+ provider: "telegram",
31
+ externalId: chatId,
32
+ ...body
33
+ });
34
+ remember(chatId, {
35
+ ...chats.get(chatId) ?? fallback,
36
+ chat
37
+ });
38
+ return true;
39
+ } catch (error) {
40
+ log("The agent could not report the chat to the ontology's registry.", error);
41
+ return false;
42
+ }
43
+ }
44
+ return {
45
+ async seen({ chatId, title, memberId }) {
46
+ const known = chats.get(chatId);
47
+ const at = now();
48
+ if (!(known === void 0 || at - known.reportedAt >= 6e5 || memberId !== void 0 && !known.members.has(memberId))) return;
49
+ const members = new Set(known?.members);
50
+ if (memberId !== void 0) members.add(memberId);
51
+ const written = {
52
+ reportedAt: at,
53
+ members,
54
+ chat: known?.chat
55
+ };
56
+ remember(chatId, written);
57
+ if (!await report(chatId, {
58
+ reportedAt: at,
59
+ members,
60
+ chat: void 0
61
+ }, {
62
+ title,
63
+ ...memberId === void 0 ? {} : { memberId }
64
+ })) restore(chatId, written, known);
65
+ },
66
+ async left({ chatId, title, memberId }) {
67
+ const known = chats.get(chatId);
68
+ const remembered = {
69
+ reportedAt: known?.reportedAt ?? now(),
70
+ members: new Set([...known?.members ?? []].filter((member) => member !== memberId)),
71
+ chat: known?.chat
72
+ };
73
+ remember(chatId, remembered);
74
+ if (!await report(chatId, remembered, {
75
+ title,
76
+ leftMemberId: memberId
77
+ })) restore(chatId, remembered, known);
78
+ },
79
+ async agentLeft({ chatId, title }) {
80
+ const known = chats.get(chatId);
81
+ const remembered = {
82
+ reportedAt: known?.reportedAt ?? now(),
83
+ members: new Set(),
84
+ chat: known?.chat
85
+ };
86
+ remember(chatId, remembered);
87
+ if (!await report(chatId, remembered, {
88
+ title,
89
+ agentLeft: true
90
+ })) restore(chatId, remembered, known);
91
+ },
92
+ async migrated({ chatId, title, migratedFrom }) {
93
+ const old = chats.get(migratedFrom);
94
+ const known = chats.get(chatId);
95
+ const remembered = {
96
+ reportedAt: now(),
97
+ members: new Set([...old?.members ?? [], ...known?.members ?? []]),
98
+ chat: known?.chat
99
+ };
100
+ chats.delete(migratedFrom);
101
+ remember(chatId, remembered);
102
+ if (!await report(chatId, remembered, {
103
+ title,
104
+ migratedFrom
105
+ })) {
106
+ restore(chatId, remembered, known);
107
+ if (old !== void 0 && !chats.has(migratedFrom)) remember(migratedFrom, old);
108
+ }
109
+ },
110
+ chat(chatId) {
111
+ return chats.get(chatId)?.chat;
112
+ }
113
+ };
114
+ }
115
+ function chatContext(chat) {
116
+ if (chat === void 0) return [];
117
+ const lines = ["<chat>", `title: ${chat.title}`];
118
+ if (chat.purpose !== null && chat.purpose !== "") lines.push(`purpose: ${chat.purpose}`);
119
+ if (chat.whenToUse !== null && chat.whenToUse !== "") lines.push(`when_to_use: ${chat.whenToUse.slice(0, MAXIMUM_WHEN_TO_USE_LENGTH)}`);
120
+ lines.push("</chat>");
121
+ return [lines.join("\n")];
122
+ }
123
+ export { REPORT_INTERVAL_MS, agentChats, chatContext };
@@ -0,0 +1,25 @@
1
+ import type { TelegramMessage } from "eve/channels/telegram";
2
+ import { type AgentAttachments } from "../attachments.js";
3
+ import type { StagedFile } from "./inbound-request.js";
4
+ /**
5
+ * Photos and documents a member sends the agent (`docs/agent.md`, "Attachments"). Before the turn
6
+ * starts, each file is downloaded through the connection and stored once as a temporary attachment
7
+ * of the member, in this chat, for 24 hours. The turn reads the stored copy, and the model learns
8
+ * each attachment's id from an `<attachments>` block, so that it can read, discard or promote it
9
+ * with the attachment tools. A file that cannot be stored is never passed on silently: the turn
10
+ * gets a note saying why.
11
+ */
12
+ /** The chat an attachment belongs to: `telegram:<chat id>`. */
13
+ export declare function telegramConversation(chatId: string): string;
14
+ /** Stores the message's files one after another, so that a large album holds one file at a time. */
15
+ export declare function stageFiles(input: {
16
+ readonly message: TelegramMessage;
17
+ readonly memberId: string;
18
+ readonly attachments: AgentAttachments;
19
+ readonly download: (fileId: string) => Promise<Uint8Array>;
20
+ }): Promise<ReadonlyMap<string, StagedFile>>;
21
+ /** The `<attachments>` block that tells the model which attachments the message carries. */
22
+ export declare function attachmentsContext(input: {
23
+ readonly files: ReadonlyMap<string, StagedFile>;
24
+ readonly group: boolean;
25
+ }): ReadonlyArray<string>;
@@ -0,0 +1,77 @@
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 { MAXIMUM_ATTACHMENT_BYTES, attachmentUrl } from "../attachments.mjs";
8
+ function telegramConversation(chatId) {
9
+ return `telegram:${chatId}`;
10
+ }
11
+ function described(file, index) {
12
+ return {
13
+ filename: file.fileName ?? (file.kind === "photo" ? `photo-${index}.jpg` : `file-${index}`),
14
+ contentType: file.mediaType ?? (file.kind === "photo" ? "image/jpeg" : "application/octet-stream")
15
+ };
16
+ }
17
+ function megabytes(bytes) {
18
+ return `${(bytes / 1024 / 1024).toFixed(1)} MB`;
19
+ }
20
+ async function stageFiles(input) {
21
+ const { message } = input;
22
+ const conversation = telegramConversation(message.chat.id);
23
+ const staged = new Map();
24
+ for (const [index, file] of message.attachments.entries()) {
25
+ const { filename, contentType } = described(file, index);
26
+ if (file.size !== void 0 && file.size > MAXIMUM_ATTACHMENT_BYTES) {
27
+ staged.set(file.fileId, {
28
+ status: "refused",
29
+ filename,
30
+ reason: `it is ${megabytes(file.size)}, over the ${megabytes(MAXIMUM_ATTACHMENT_BYTES)} a bot may download`
31
+ });
32
+ continue;
33
+ }
34
+ try {
35
+ const bytes = await input.download(file.fileId);
36
+ const attachment = await input.attachments.receive({
37
+ memberId: input.memberId,
38
+ conversation
39
+ }, {
40
+ bytes,
41
+ filename,
42
+ contentType,
43
+ sourceRef: `${message.chat.id}:${message.messageId}:${index}`
44
+ });
45
+ staged.set(file.fileId, {
46
+ status: "stored",
47
+ attachment,
48
+ url: attachmentUrl({
49
+ id: attachment.id,
50
+ memberId: input.memberId,
51
+ conversation
52
+ })
53
+ });
54
+ } catch (error) {
55
+ console.error("[segmentstream] attachment could not be stored", {
56
+ kind: file.kind,
57
+ size: file.size,
58
+ error
59
+ });
60
+ staged.set(file.fileId, {
61
+ status: "refused",
62
+ filename,
63
+ reason: error instanceof Error ? error.message : "it could not be stored"
64
+ });
65
+ }
66
+ }
67
+ return staged;
68
+ }
69
+ function attachmentsContext(input) {
70
+ if (input.files.size === 0) return [];
71
+ const lines = ["<attachments>", `The member's files from this message, kept for 24 hours for this member in this chat only. Read, discard or promote them with the segmentstream__attachments_* tools and these ids; promote only what should be kept.`];
72
+ for (const staged of input.files.values()) lines.push(staged.status === "stored" ? `- id: ${staged.attachment.id}, filename: ${JSON.stringify(staged.attachment.filename)}, type: ${staged.attachment.contentType}, size: ${staged.attachment.size} bytes, expires_at: ${staged.attachment.expiresAt}` : `- ${JSON.stringify(staged.filename)}: not received, ${staged.reason}`);
73
+ if (input.group) lines.push("This is a group chat: everyone in it shares this conversation's history, so do not repeat a private file's contents here. Suggest sending private files in a direct message.");
74
+ lines.push("</attachments>");
75
+ return [lines.join("\n")];
76
+ }
77
+ export { attachmentsContext, stageFiles, telegramConversation };
@@ -1,5 +1,16 @@
1
1
  import type { ChannelSource } from "eve/channels";
2
+ import type { Attachment } from "../attachments.js";
2
3
  import type { SessionOperations } from "./sessions.js";
4
+ /** What became of one Telegram file of the message: stored as an attachment, or why not. */
5
+ export type StagedFile = {
6
+ readonly status: "stored";
7
+ readonly attachment: Attachment;
8
+ readonly url: URL;
9
+ } | {
10
+ readonly status: "refused";
11
+ readonly filename: string;
12
+ readonly reason: string;
13
+ };
3
14
  /**
4
15
  * What one webhook request shares between eve's `onMessage` hook and the route wrapper around it.
5
16
  * `onMessage` may only return `{ auth, context, title }`; it cannot change the message eve sends.
@@ -12,8 +23,15 @@ export declare class InboundRequest {
12
23
  readonly agentDescription: () => Promise<string>;
13
24
  /** The text the turn should carry instead of the parsed message, once a transcript exists. */
14
25
  text: string | undefined;
26
+ /**
27
+ * The message's Telegram files by file id, once stored as attachments: the turn reads each from
28
+ * the attachment store instead of Telegram, and one that could not be stored becomes a note.
29
+ */
30
+ files: ReadonlyMap<string, StagedFile> | undefined;
15
31
  /** The route's session operations, on addresses already mapped to the configured scope. */
16
32
  sessions: SessionOperations | undefined;
33
+ /** Keeps work the request started, such as a chat report, alive after it answers. */
34
+ waitUntil: ((task: Promise<unknown>) => void) | undefined;
17
35
  constructor(
18
36
  /** The agent's description from the compiled manifest, for the routing decision. */
19
37
  agentDescription: () => Promise<string>);
@@ -21,5 +39,8 @@ export declare class InboundRequest {
21
39
  export declare function runInboundRequest<Result>(request: InboundRequest, run: () => Result): Result;
22
40
  /** The current webhook request, or `undefined` outside the wrapped route. */
23
41
  export declare function currentInboundRequest(): InboundRequest | undefined;
24
- /** A channel source whose `send` delivers the request's replacement text, when one was recorded. */
42
+ /**
43
+ * A channel source whose `send` delivers the request's replacement text, when one was recorded,
44
+ * and its stored attachments in place of the Telegram files.
45
+ */
25
46
  export declare function withReplacedText<State>(source: ChannelSource<State>, request: InboundRequest): ChannelSource<State>;
@@ -8,7 +8,9 @@ import { AsyncLocalStorage } from "node:async_hooks";
8
8
  var InboundRequest = class {
9
9
  agentDescription;
10
10
  text;
11
+ files;
11
12
  sessions;
13
+ waitUntil;
12
14
  constructor(agentDescription) {
13
15
  this.agentDescription = agentDescription;
14
16
  }
@@ -27,11 +29,38 @@ function replaceText(message, text) {
27
29
  text
28
30
  }, ...message.filter((part) => part.type !== "text")];
29
31
  }
32
+ function telegramFileId(data) {
33
+ const href = data instanceof URL ? data.href : typeof data === "string" ? data : void 0;
34
+ if (href === void 0 || !href.startsWith("telegram-file:")) return;
35
+ try {
36
+ return decodeURIComponent(new URL(href).pathname);
37
+ } catch {
38
+ return;
39
+ }
40
+ }
41
+ function replaceFiles(message, files) {
42
+ if (typeof message === "string") return message;
43
+ return message.map((part) => {
44
+ if (part.type !== "file") return part;
45
+ const fileId = telegramFileId(part.data);
46
+ const staged = fileId === void 0 ? void 0 : files.get(fileId);
47
+ if (staged === void 0) return part;
48
+ if (staged.status === "refused") return {
49
+ type: "text",
50
+ text: `[file ${staged.filename} was not received: ${staged.reason}]`
51
+ };
52
+ return {
53
+ ...part,
54
+ data: staged.url
55
+ };
56
+ });
57
+ }
30
58
  function withReplacedText(source, request) {
31
59
  return {
32
60
  ...source,
33
61
  send(message, options) {
34
- return source.send(request.text === void 0 ? message : replaceText(message, request.text), options);
62
+ const text = request.text === void 0 ? message : replaceText(message, request.text);
63
+ return source.send(request.files === void 0 ? text : replaceFiles(text, request.files), options);
35
64
  }
36
65
  };
37
66
  }
@@ -1,7 +1,9 @@
1
1
  import type { TelegramContext, TelegramInboundResult, TelegramMessage } from "eve/channels/telegram";
2
2
  import type { SessionSettings, TelegramPolicy } from "../../config.js";
3
+ import type { AgentAttachments } from "../attachments.js";
3
4
  import type { AgentIdentities } from "../identities.js";
4
5
  import type { BotIdentityLookup } from "./bot.js";
6
+ import { type AgentChats } from "./chats.js";
5
7
  import type { InboundRequest } from "./inbound-request.js";
6
8
  import { type ObservedMessages } from "./observed.js";
7
9
  import { type RouteDecider } from "./router.js";
@@ -11,13 +13,22 @@ import { type Transcriber } from "./voice.js";
11
13
  * eve's Telegram `onMessage` hook, in the order `agent-design.md` gives:
12
14
  *
13
15
  * 1. The hard gate, with no model: bots, channel posts and senders without a live pairing never
14
- * start a turn. An unknown sender who writes to the bot privately gets a pairing button.
16
+ * start a turn. An unknown sender who writes to the bot privately gets a pairing button. A group
17
+ * message reports its chat to the ontology's registry in the background, whatever follows. A
18
+ * group's notice that a member left is never a turn: a paired member's departure is reported,
19
+ * and so is the agent's own, which unlinks every member. Neither is the notice, in the old chat
20
+ * or the new one, that a group became a supergroup: the migration is reported.
15
21
  * 2. Voice becomes text, so the router and the agent read what was said.
16
22
  * 3. The router answers or observes. An observed message goes to the chat's ring and stops here.
17
23
  * 4. The session policy runs on the session the message continues: an idle one is cleared, the
18
24
  * first message after the day's compaction time compacts it.
19
- * 5. An answered message dispatches as the paired member, with the ring and a `<speaker>` block
20
- * naming the member, their roles and the message they replied to ahead of it as context.
25
+ * 5. An answered message's photos and documents are stored as the member's temporary attachments,
26
+ * which the turn reads in place of the Telegram files.
27
+ * 6. An answered message dispatches as the paired member, with the ring, the registry's `<chat>`
28
+ * block, a `<speaker>` block naming the member, their roles and the message they replied to and
29
+ * an `<attachments>` block naming its stored files ahead of it as context. An answered group
30
+ * message waits for its chat's report first, so that a chat new to the process already carries
31
+ * its `<chat>` block.
21
32
  */
22
33
  export interface InboundDependencies {
23
34
  readonly policy: TelegramPolicy;
@@ -29,7 +40,10 @@ export interface InboundDependencies {
29
40
  readonly decide: RouteDecider;
30
41
  readonly transcribe: Transcriber;
31
42
  readonly download: (fileId: string) => Promise<Uint8Array>;
43
+ /** The attachment store; without it, files reach the turn as eve's Telegram file parts. */
44
+ readonly attachments?: AgentAttachments;
32
45
  readonly observed: ObservedMessages;
46
+ readonly chats: AgentChats;
33
47
  /** The app's `/agent/connect` link for a pairing code. */
34
48
  readonly connectUrl: (code: string) => string;
35
49
  }
@@ -5,6 +5,8 @@ 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 { chatContext } from "./chats.mjs";
9
+ import { attachmentsContext, stageFiles } from "./files.mjs";
8
10
  import { observedChatKey, observedContext } from "./observed.mjs";
9
11
  import { isAddressed, routeMessage } from "./router.mjs";
10
12
  import { messageAddress, scopedAddress } from "./sessions.mjs";
@@ -90,9 +92,67 @@ async function answerUnpaired(input) {
90
92
  reply_markup: buttonTo("Connect account", dependencies.connectUrl(pairing.code))
91
93
  });
92
94
  }
95
+ function isGroupChat(message) {
96
+ return message.chat.type === "group" || message.chat.type === "supergroup";
97
+ }
98
+ async function reportDeparture(input) {
99
+ const { context, message, left, dependencies } = input;
100
+ const id = left.id;
101
+ if (typeof id !== "number" && typeof id !== "string") return;
102
+ if (left.is_bot === true) {
103
+ const bot = await dependencies.bot(context.telegram);
104
+ if (bot !== void 0 && String(id) === bot.id) await dependencies.chats.agentLeft({
105
+ chatId: message.chat.id,
106
+ title: message.chat.title ?? ""
107
+ });
108
+ return;
109
+ }
110
+ const lookup = await dependencies.identities.lookup({
111
+ provider: "telegram",
112
+ externalId: String(id)
113
+ });
114
+ if (lookup.status === "paired") await dependencies.chats.left({
115
+ chatId: message.chat.id,
116
+ title: message.chat.title ?? "",
117
+ memberId: lookup.member.id
118
+ });
119
+ }
120
+ function rawChatId(value) {
121
+ return typeof value === "number" || typeof value === "string" && value !== "" ? String(value) : void 0;
122
+ }
123
+ function migrationOf(message) {
124
+ const to = rawChatId(message.raw.migrate_to_chat_id);
125
+ const from = rawChatId(message.raw.migrate_from_chat_id);
126
+ if (to !== void 0) return {
127
+ chatId: to,
128
+ migratedFrom: message.chat.id
129
+ };
130
+ return from === void 0 ? void 0 : {
131
+ chatId: message.chat.id,
132
+ migratedFrom: from
133
+ };
134
+ }
93
135
  async function handleInboundMessage(input) {
94
136
  const { context, dependencies } = input;
95
137
  const message = input.message;
138
+ const left = message.raw.left_chat_member;
139
+ const migration = isGroupChat(message) ? migrationOf(message) : void 0;
140
+ if (isGroupChat(message) && isRecord(left)) {
141
+ await reportDeparture({
142
+ context,
143
+ message,
144
+ left,
145
+ dependencies
146
+ });
147
+ return null;
148
+ }
149
+ if (migration !== void 0) {
150
+ await dependencies.chats.migrated({
151
+ ...migration,
152
+ title: message.chat.title ?? ""
153
+ });
154
+ return null;
155
+ }
96
156
  if (message.from === void 0 || message.from.isBot || message.chat.type === "channel") return null;
97
157
  const sent = {
98
158
  ...message,
@@ -104,6 +164,13 @@ async function handleInboundMessage(input) {
104
164
  provider: "telegram",
105
165
  externalId: sent.from.id
106
166
  });
167
+ const group = isGroupChat(sent);
168
+ const report = group ? dependencies.chats.seen({
169
+ chatId: sent.chat.id,
170
+ title: sent.chat.title ?? "",
171
+ ...lookup.status === "paired" ? { memberId: lookup.member.id } : {}
172
+ }) : void 0;
173
+ if (report !== void 0) input.request?.waitUntil?.(report);
107
174
  if (lookup.status !== "paired") {
108
175
  if (addressed) await answerUnpaired({
109
176
  context,
@@ -160,6 +227,14 @@ async function handleInboundMessage(input) {
160
227
  dependencies.sessionPolicy.delivered(address);
161
228
  }
162
229
  await context.telegram.startTyping();
230
+ const files = sent.attachments.length === 0 || dependencies.attachments === void 0 || input.request === void 0 ? new Map() : await stageFiles({
231
+ message: sent,
232
+ memberId: lookup.member.id,
233
+ attachments: dependencies.attachments,
234
+ download: dependencies.download
235
+ });
236
+ if (input.request !== void 0 && files.size > 0) input.request.files = files;
237
+ await report;
163
238
  return {
164
239
  auth: telegramPrincipal({
165
240
  message: sent,
@@ -167,11 +242,16 @@ async function handleInboundMessage(input) {
167
242
  }),
168
243
  context: [
169
244
  ...observedContext(dependencies.observed.take(chat)),
245
+ ...group ? chatContext(dependencies.chats.chat(sent.chat.id)) : [],
170
246
  speakerBlock({
171
247
  message: sent,
172
248
  member: lookup.member,
173
249
  bot
174
250
  }),
251
+ ...attachmentsContext({
252
+ files,
253
+ group
254
+ }),
175
255
  ...notes
176
256
  ]
177
257
  };
@@ -53,13 +53,14 @@ function hasNullBody(status) {
53
53
  function telegramProxyFetch(proxy) {
54
54
  return async (input, init) => {
55
55
  const route = telegramProxyRoute(requestUrl(input));
56
- const reply = await proxy.request({
56
+ const call = {
57
57
  api: route.api,
58
58
  method: proxyMethod(init?.method),
59
59
  path: route.path,
60
60
  headers: headerPairs(init?.headers),
61
61
  body: await bodyBytes(init?.body)
62
- });
62
+ };
63
+ const reply = route.api === "file" ? await proxy.download(call) : await proxy.request(call);
63
64
  return new Response(hasNullBody(reply.status) ? null : Buffer.from(reply.body), {
64
65
  status: reply.status,
65
66
  headers: reply.headers.map(([name, value]) => [name, value])
@@ -0,0 +1,71 @@
1
+ import { type RouteDefinition } from "eve/channels";
2
+ import type { TelegramChannel, TelegramChannelState } from "eve/channels/telegram";
3
+ import type { SessionAuthContext } from "eve/context";
4
+ import type { SessionSettings } from "../../config.js";
5
+ import type { RuntimeTokenVerifier } from "../runtime-identity.js";
6
+ import { type SessionPolicy } from "./sessions.js";
7
+ /**
8
+ * Requests from the ontology's `agent_chat.send_message` action: an external agent or a member asks
9
+ * the workspace agent to contact someone in a group or a private chat. The ontology service resolves
10
+ * the chat and posts here with its own Vercel OIDC token; the request is delivered into the chat's
11
+ * session through eve's proactive `receive`, without a seed message, so it continues the session
12
+ * the chat's own messages reach (`chatId::`, the chat-scope address) and the replies land there too.
13
+ * The session policy runs on that session first, as it does for an inbound message.
14
+ */
15
+ export declare const MESSAGES_ROUTE = "/eve/v1/telegram/messages";
16
+ export interface AgentRequest {
17
+ readonly target: {
18
+ readonly kind: "group" | "private";
19
+ readonly provider: "telegram";
20
+ /** The group's chat id, or the Telegram user id of a private chat. */
21
+ readonly chatId: string;
22
+ };
23
+ readonly message: string;
24
+ /** Whom to contact, when the requester named someone. */
25
+ readonly person: {
26
+ readonly id: string;
27
+ readonly name: string;
28
+ } | null;
29
+ readonly requester: {
30
+ readonly id: string;
31
+ readonly name: string;
32
+ readonly kind: "member" | "service";
33
+ /** The roles the requester holds; empty from an ontology that does not send them. */
34
+ readonly roles: ReadonlyArray<string>;
35
+ /**
36
+ * Whether the turn can act as the requester: a member with a live paired identity. An ontology
37
+ * that does not send it means a member can.
38
+ */
39
+ readonly actAs: boolean;
40
+ };
41
+ }
42
+ /** The request body, or `undefined` when it does not match the contract. */
43
+ export declare function agentRequest(value: unknown): AgentRequest | undefined;
44
+ /**
45
+ * Whom the request's turn runs as. A member requester the ontology says can be acted as is the
46
+ * turn's member, so the ontology connection acts as them and the ontology service checks, as for any
47
+ * turn, that they are active and paired. Their roles, as the action's principal held them, choose the
48
+ * model of a session the request starts; the service resolves them afresh on every call. A service
49
+ * user, or a member who has not paired, cannot be acted as, so the request runs without a member and
50
+ * the turn has no ontology; their roles still choose the model, and grant nothing, since the
51
+ * ontology connection acts only for a `memberId`.
52
+ */
53
+ export declare function requestPrincipal(request: AgentRequest): SessionAuthContext;
54
+ /** What the turn is about, for the model, ahead of the request itself. */
55
+ export declare function requestContext(request: AgentRequest): string;
56
+ /**
57
+ * `POST /eve/v1/telegram/messages`. Gated like the webhook: outside production it answers 503 and a
58
+ * workspace without a Telegram channel 404. The caller must be a function of this deployment's
59
+ * project, verified from its Vercel OIDC token.
60
+ */
61
+ export declare function messagesRoute(input: {
62
+ readonly channel: TelegramChannel;
63
+ readonly enabled: () => boolean;
64
+ readonly configured: () => boolean;
65
+ readonly verify: () => RuntimeTokenVerifier;
66
+ /** The adapter's session policy, which the webhook route applies too. */
67
+ readonly sessionPolicy: SessionPolicy;
68
+ /** The mounted `sessions.scope`. */
69
+ readonly scope: () => SessionSettings["scope"];
70
+ readonly log?: (message: string, detail: unknown) => void;
71
+ }): RouteDefinition<TelegramChannelState>;