@segmentstream/agent 0.0.0-stage → 0.1.1

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 (52) hide show
  1. package/LICENSE +201 -0
  2. package/NOTICE +2 -0
  3. package/README.md +178 -2
  4. package/dist/extension/_manifest.json +13 -0
  5. package/dist/extension/channels/telegram.d.ts +6 -0
  6. package/dist/extension/channels/telegram.mjs +15 -0
  7. package/dist/extension/config.d.ts +99 -0
  8. package/dist/extension/config.mjs +144 -0
  9. package/dist/extension/connections/ontology.d.ts +7 -0
  10. package/dist/extension/connections/ontology.mjs +11 -0
  11. package/dist/extension/extension.d.ts +19 -0
  12. package/dist/extension/extension.mjs +15 -0
  13. package/dist/extension/instructions.md +13 -0
  14. package/dist/extension/lib/connection-proxy.d.ts +57 -0
  15. package/dist/extension/lib/connection-proxy.mjs +113 -0
  16. package/dist/extension/lib/environment.d.ts +30 -0
  17. package/dist/extension/lib/environment.mjs +46 -0
  18. package/dist/extension/lib/identities.d.ts +65 -0
  19. package/dist/extension/lib/identities.mjs +133 -0
  20. package/dist/extension/lib/ontology-connection.d.ts +36 -0
  21. package/dist/extension/lib/ontology-connection.mjs +48 -0
  22. package/dist/extension/lib/runtime-token.d.ts +17 -0
  23. package/dist/extension/lib/runtime-token.mjs +31 -0
  24. package/dist/extension/lib/telegram/adapter.d.ts +29 -0
  25. package/dist/extension/lib/telegram/adapter.mjs +115 -0
  26. package/dist/extension/lib/telegram/bot.d.ts +4 -0
  27. package/dist/extension/lib/telegram/bot.mjs +45 -0
  28. package/dist/extension/lib/telegram/inbound-request.d.ts +22 -0
  29. package/dist/extension/lib/telegram/inbound-request.mjs +37 -0
  30. package/dist/extension/lib/telegram/inbound.d.ts +35 -0
  31. package/dist/extension/lib/telegram/inbound.mjs +117 -0
  32. package/dist/extension/lib/telegram/observed.d.ts +32 -0
  33. package/dist/extension/lib/telegram/observed.mjs +47 -0
  34. package/dist/extension/lib/telegram/principal.d.ts +20 -0
  35. package/dist/extension/lib/telegram/principal.mjs +40 -0
  36. package/dist/extension/lib/telegram/proxy-fetch.d.ts +28 -0
  37. package/dist/extension/lib/telegram/proxy-fetch.mjs +69 -0
  38. package/dist/extension/lib/telegram/router.d.ts +69 -0
  39. package/dist/extension/lib/telegram/router.mjs +64 -0
  40. package/dist/extension/lib/telegram/silent.d.ts +9 -0
  41. package/dist/extension/lib/telegram/silent.mjs +15 -0
  42. package/dist/extension/lib/telegram/voice.d.ts +52 -0
  43. package/dist/extension/lib/telegram/voice.mjs +115 -0
  44. package/dist/extension/lib/telegram/webhook.d.ts +13 -0
  45. package/dist/extension/lib/telegram/webhook.mjs +23 -0
  46. package/dist/extension/model.d.ts +29 -0
  47. package/dist/extension/model.mjs +23 -0
  48. package/dist/index.d.ts +4 -0
  49. package/dist/index.mjs +8 -0
  50. package/dist/tools/index.d.ts +2 -0
  51. package/dist/tools/index.mjs +7 -0
  52. package/package.json +76 -4
@@ -0,0 +1,29 @@
1
+ import { type TelegramChannel } from "eve/channels/telegram";
2
+ import type { TelegramSettings } from "../../config.js";
3
+ import { type Environment } from "../environment.js";
4
+ import { type RuntimeToken } from "../runtime-token.js";
5
+ import { type ObservedMessages } from "./observed.js";
6
+ import { type DecideFunction } from "./router.js";
7
+ import { type Transcriber } from "./voice.js";
8
+ /**
9
+ * The SegmentStream Telegram adapter: eve's `telegramChannel`, with its credentials, identity,
10
+ * voice and routing replaced (`agent-design.md`, section 4). Everything it reads from the mount's
11
+ * configuration is read per request, because eve binds the mount after it loads the channel.
12
+ */
13
+ export interface TelegramAdapterOptions {
14
+ /** The mounted `channels.telegram`, or `null` when the workspace has no Telegram channel. */
15
+ readonly settings: () => TelegramSettings | null;
16
+ readonly models: () => {
17
+ readonly stt: string | false;
18
+ readonly decision: string;
19
+ };
20
+ readonly decide: DecideFunction;
21
+ readonly environment?: Environment;
22
+ readonly token?: RuntimeToken;
23
+ /** For the proxy and the ontology service; tests pass a fake. */
24
+ readonly fetch?: typeof fetch;
25
+ readonly transcribe?: Transcriber;
26
+ readonly observed?: ObservedMessages;
27
+ }
28
+ /** The Telegram channel of the extension. */
29
+ export declare function segmentstreamTelegram(options: TelegramAdapterOptions): TelegramChannel;
@@ -0,0 +1,115 @@
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 { appOrigin, channelsEnabled, proxyEnvironment, webhookSecret } from "../environment.mjs";
8
+ import { MAXIMUM_PROXY_BODY_BYTES, connectionProxy } from "../connection-proxy.mjs";
9
+ import { agentIdentities } from "../identities.mjs";
10
+ import { runtimeToken } from "../runtime-token.mjs";
11
+ import { botIdentityCache } from "./bot.mjs";
12
+ import { observedMessages } from "./observed.mjs";
13
+ import { decisionRouter } from "./router.mjs";
14
+ import { downloadTelegramFile, gatewayTranscriber } from "./voice.mjs";
15
+ import { handleInboundMessage } from "./inbound.mjs";
16
+ import { InboundRequest, currentInboundRequest, runInboundRequest, withReplacedText } from "./inbound-request.mjs";
17
+ import { TELEGRAM_PLACEHOLDER_TOKEN, TELEGRAM_PROXY_ORIGIN, telegramProxyFetch } from "./proxy-fetch.mjs";
18
+ import { deliverUnlessSilent } from "./silent.mjs";
19
+ import { telegramWebhookVerifier } from "./webhook.mjs";
20
+ import { telegramChannel } from "eve/channels/telegram";
21
+ function segmentstreamTelegram(options) {
22
+ const environment = options.environment ?? process.env;
23
+ const token = options.token ?? runtimeToken;
24
+ const observed = options.observed ?? observedMessages();
25
+ const bot = botIdentityCache();
26
+ const proxies = new Map();
27
+ let identities;
28
+ function settings() {
29
+ const configured = options.settings();
30
+ if (configured === null) throw new Error("The Telegram channel is not configured in agent/segmentstream.json.");
31
+ return configured;
32
+ }
33
+ function proxy() {
34
+ const connection = settings().connection;
35
+ let existing = proxies.get(connection);
36
+ if (existing === void 0) {
37
+ existing = connectionProxy({
38
+ environment: proxyEnvironment(environment),
39
+ connection,
40
+ token,
41
+ ...options.fetch === void 0 ? {} : { fetch: options.fetch }
42
+ });
43
+ proxies.set(connection, existing);
44
+ }
45
+ return existing;
46
+ }
47
+ function pairing() {
48
+ identities ??= agentIdentities({
49
+ origin: appOrigin(environment),
50
+ token,
51
+ ...options.fetch === void 0 ? {} : { fetch: options.fetch }
52
+ });
53
+ return identities;
54
+ }
55
+ const channel = telegramChannel({
56
+ api: {
57
+ apiBaseUrl: TELEGRAM_PROXY_ORIGIN,
58
+ fileBaseUrl: TELEGRAM_PROXY_ORIGIN,
59
+ fetch: (input, init) => telegramProxyFetch(proxy())(input, init)
60
+ },
61
+ credentials: {
62
+ botToken: TELEGRAM_PLACEHOLDER_TOKEN,
63
+ webhookVerifier: telegramWebhookVerifier(() => webhookSecret(environment))
64
+ },
65
+ uploadPolicy: { maxBytes: MAXIMUM_PROXY_BODY_BYTES },
66
+ onMessage: (context, message) => handleInboundMessage({
67
+ context,
68
+ message,
69
+ request: currentInboundRequest(),
70
+ dependencies: {
71
+ policy: settings().policy,
72
+ sttModel: options.models().stt,
73
+ identities: pairing(),
74
+ bot,
75
+ decide: decisionRouter({
76
+ decide: options.decide,
77
+ model: options.models().decision
78
+ }),
79
+ transcribe: options.transcribe ?? gatewayTranscriber,
80
+ download: (fileId) => downloadTelegramFile({
81
+ proxy: proxy(),
82
+ fileId
83
+ }),
84
+ observed,
85
+ connectUrl: (code) => `${appOrigin(environment)}/agent/connect?code=${encodeURIComponent(code)}`
86
+ }
87
+ }),
88
+ events: { "message.completed": deliverUnlessSilent }
89
+ });
90
+ return {
91
+ ...channel,
92
+ routes: channel.routes.map((route) => gatedRoute({
93
+ route,
94
+ options,
95
+ environment
96
+ }))
97
+ };
98
+ }
99
+ function gatedRoute(input) {
100
+ const { route, options, environment } = input;
101
+ if (route.transport === "websocket") return route;
102
+ return {
103
+ ...route,
104
+ handler: async (request, args) => {
105
+ if (!channelsEnabled(environment)) return new Response("ok");
106
+ if (options.settings() === null) return new Response("not found", { status: 404 });
107
+ const inbound = new InboundRequest(async () => (await args.describe()).description ?? "");
108
+ return runInboundRequest(inbound, () => route.handler(request, {
109
+ ...args,
110
+ from: (address) => withReplacedText(args.from(address), inbound)
111
+ }));
112
+ }
113
+ };
114
+ }
115
+ export { segmentstreamTelegram };
@@ -0,0 +1,4 @@
1
+ import type { TelegramHandle } from "eve/channels/telegram";
2
+ import type { BotIdentity } from "./router.js";
3
+ export type BotIdentityLookup = (telegram: Pick<TelegramHandle, "request">) => Promise<BotIdentity | undefined>;
4
+ export declare function botIdentityCache(now?: () => number): BotIdentityLookup;
@@ -0,0 +1,45 @@
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 IDENTITY_TTL_MS = 36e5;
8
+ const FAILURE_TTL_MS = 6e4;
9
+ function isRecord(value) {
10
+ return typeof value === "object" && value !== null && !Array.isArray(value);
11
+ }
12
+ function identityFrom(body) {
13
+ const result = isRecord(body) ? body.result : void 0;
14
+ if (!isRecord(result) || typeof result.id !== "number" && typeof result.id !== "string") return;
15
+ return {
16
+ id: String(result.id),
17
+ username: typeof result.username === "string" ? result.username : void 0
18
+ };
19
+ }
20
+ function botIdentityCache(now = Date.now) {
21
+ let cached;
22
+ return async (telegram) => {
23
+ if (cached !== void 0 && cached.until > now()) return cached.identity;
24
+ let identity;
25
+ try {
26
+ const response = await telegram.request("getMe");
27
+ identity = response.ok ? identityFrom(response.body) : void 0;
28
+ if (identity === void 0) {
29
+ const body = isRecord(response.body) ? response.body : {};
30
+ console.error("[segmentstream] Telegram refused getMe", {
31
+ ok: response.ok,
32
+ description: typeof body.description === "string" ? body.description.slice(0, 200) : ""
33
+ });
34
+ }
35
+ } catch (error) {
36
+ console.error("[segmentstream] Telegram getMe failed", { error });
37
+ }
38
+ cached = {
39
+ identity,
40
+ until: now() + (identity === void 0 ? FAILURE_TTL_MS : IDENTITY_TTL_MS)
41
+ };
42
+ return identity;
43
+ };
44
+ }
45
+ export { botIdentityCache };
@@ -0,0 +1,22 @@
1
+ import type { ChannelSource } from "eve/channels";
2
+ /**
3
+ * What one webhook request shares between eve's `onMessage` hook and the route wrapper around it.
4
+ * `onMessage` may only return `{ auth, context, title }`; it cannot change the message eve sends.
5
+ * A voice message reaches it with no text, so it records the transcript here, and the wrapper's
6
+ * `send` delivers that text in place of the empty message. Async local storage scopes the record
7
+ * to the request: eve calls `onMessage` and `send` inside the route handler's async context.
8
+ */
9
+ export declare class InboundRequest {
10
+ /** The agent's description from the compiled manifest, for the routing decision. */
11
+ readonly agentDescription: () => Promise<string>;
12
+ /** The text the turn should carry instead of the parsed message, once a transcript exists. */
13
+ text: string | undefined;
14
+ constructor(
15
+ /** The agent's description from the compiled manifest, for the routing decision. */
16
+ agentDescription: () => Promise<string>);
17
+ }
18
+ export declare function runInboundRequest<Result>(request: InboundRequest, run: () => Result): Result;
19
+ /** The current webhook request, or `undefined` outside the wrapped route. */
20
+ export declare function currentInboundRequest(): InboundRequest | undefined;
21
+ /** A channel source whose `send` delivers the request's replacement text, when one was recorded. */
22
+ export declare function withReplacedText<State>(source: ChannelSource<State>, request: InboundRequest): ChannelSource<State>;
@@ -0,0 +1,37 @@
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
+ var InboundRequest = class {
9
+ agentDescription;
10
+ text;
11
+ constructor(agentDescription) {
12
+ this.agentDescription = agentDescription;
13
+ }
14
+ };
15
+ const storage = new AsyncLocalStorage();
16
+ function runInboundRequest(request, run) {
17
+ return storage.run(request, run);
18
+ }
19
+ function currentInboundRequest() {
20
+ return storage.getStore();
21
+ }
22
+ function replaceText(message, text) {
23
+ if (typeof message === "string") return text;
24
+ return [{
25
+ type: "text",
26
+ text
27
+ }, ...message.filter((part) => part.type !== "text")];
28
+ }
29
+ function withReplacedText(source, request) {
30
+ return {
31
+ ...source,
32
+ send(message, options) {
33
+ return source.send(request.text === void 0 ? message : replaceText(message, request.text), options);
34
+ }
35
+ };
36
+ }
37
+ export { InboundRequest, currentInboundRequest, runInboundRequest, withReplacedText };
@@ -0,0 +1,35 @@
1
+ import type { TelegramContext, TelegramInboundResult, TelegramMessage } from "eve/channels/telegram";
2
+ import type { TelegramPolicy } from "../../config.js";
3
+ import type { AgentIdentities } from "../identities.js";
4
+ import type { BotIdentityLookup } from "./bot.js";
5
+ import type { InboundRequest } from "./inbound-request.js";
6
+ import { type ObservedMessages } from "./observed.js";
7
+ import { type RouteDecider } from "./router.js";
8
+ import { type Transcriber } from "./voice.js";
9
+ /**
10
+ * eve's Telegram `onMessage` hook, in the order `agent-design.md` gives:
11
+ *
12
+ * 1. The hard gate, with no model: bots, channel posts and senders without a live pairing never
13
+ * start a turn. An unknown sender who writes to the bot privately gets a pairing link.
14
+ * 2. Voice becomes text, so the router and the agent read what was said.
15
+ * 3. The router answers or observes. An observed message goes to the chat's ring and stops here.
16
+ * 4. An answered message dispatches as the paired member, with the ring ahead of it as context.
17
+ */
18
+ export interface InboundDependencies {
19
+ readonly policy: TelegramPolicy;
20
+ readonly sttModel: string | false;
21
+ readonly identities: AgentIdentities;
22
+ readonly bot: BotIdentityLookup;
23
+ readonly decide: RouteDecider;
24
+ readonly transcribe: Transcriber;
25
+ readonly download: (fileId: string) => Promise<Uint8Array>;
26
+ readonly observed: ObservedMessages;
27
+ /** The app's `/agent/connect` link for a pairing code. */
28
+ readonly connectUrl: (code: string) => string;
29
+ }
30
+ export declare function handleInboundMessage(input: {
31
+ readonly context: TelegramContext;
32
+ readonly message: TelegramMessage;
33
+ readonly request: InboundRequest | undefined;
34
+ readonly dependencies: InboundDependencies;
35
+ }): Promise<TelegramInboundResult>;
@@ -0,0 +1,117 @@
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 { telegramPrincipal } from "./principal.mjs";
8
+ import { observedChatKey, observedContext } from "./observed.mjs";
9
+ import { isAddressed, routeMessage } from "./router.mjs";
10
+ import { voiceNote, voiceOf, voiceText } from "./voice.mjs";
11
+ const UNKNOWN_PRIVATE = "I don't know who you are yet. Open this link and sign in to connect your Telegram account; it works for ten minutes:";
12
+ const REFUSED = "Your access to this agent was revoked, or your membership is inactive. Ask a workspace administrator.";
13
+ function display(user) {
14
+ if (user.username !== void 0) return `@${user.username}`;
15
+ const name = [user.firstName, user.lastName].filter((part) => part !== void 0 && part !== "").join(" ");
16
+ return name === "" ? void 0 : name;
17
+ }
18
+ function sentAt(message) {
19
+ const date = message.raw.date;
20
+ return new Date(typeof date === "number" ? date * 1e3 : Date.now()).toISOString();
21
+ }
22
+ function observedMessage(message, text) {
23
+ return {
24
+ author: display(message.from) ?? message.from.id,
25
+ text,
26
+ at: sentAt(message)
27
+ };
28
+ }
29
+ function unknownInGroup(bot) {
30
+ return `I don't know who you are yet. Message me privately to connect your account.${bot?.username === void 0 ? "" : ` https://t.me/${bot.username}`}`;
31
+ }
32
+ async function answerUnpaired(input) {
33
+ const { context, message, lookup, dependencies } = input;
34
+ if (lookup.status === "refused") {
35
+ await context.telegram.post(REFUSED);
36
+ return;
37
+ }
38
+ if (message.chat.type !== "private") {
39
+ await context.telegram.post(unknownInGroup(input.bot));
40
+ return;
41
+ }
42
+ const sender = display(message.from);
43
+ const pairing = await dependencies.identities.createPairing({
44
+ provider: "telegram",
45
+ externalId: message.from.id,
46
+ ...sender === void 0 ? {} : { display: sender }
47
+ });
48
+ await context.telegram.post(`${UNKNOWN_PRIVATE}\n${dependencies.connectUrl(pairing.code)}`);
49
+ }
50
+ async function handleInboundMessage(input) {
51
+ const { context, dependencies } = input;
52
+ const message = input.message;
53
+ if (message.from === void 0 || message.from.isBot || message.chat.type === "channel") return null;
54
+ const sent = {
55
+ ...message,
56
+ from: message.from
57
+ };
58
+ const bot = await dependencies.bot(context.telegram);
59
+ const addressed = isAddressed(sent, bot);
60
+ const lookup = await dependencies.identities.lookup({
61
+ provider: "telegram",
62
+ externalId: sent.from.id
63
+ });
64
+ if (lookup.status !== "paired") {
65
+ if (addressed) await answerUnpaired({
66
+ context,
67
+ message: sent,
68
+ lookup,
69
+ bot,
70
+ dependencies
71
+ });
72
+ return null;
73
+ }
74
+ const voice = voiceOf(sent.raw);
75
+ const caption = sent.text || sent.caption;
76
+ const notes = [];
77
+ let text = caption;
78
+ if (voice !== void 0 && (addressed || dependencies.policy === "smart")) {
79
+ const spoken = await voiceText({
80
+ voice,
81
+ model: dependencies.sttModel,
82
+ download: dependencies.download,
83
+ transcribe: dependencies.transcribe
84
+ });
85
+ text = [caption, spoken.text].filter((part) => part.trim() !== "").join("\n\n");
86
+ notes.push(spoken.note);
87
+ } else if (voice !== void 0) text = caption || voiceNote(voice);
88
+ if (text.trim() === "" && sent.attachments.length === 0) return null;
89
+ const chat = observedChatKey({
90
+ chatId: sent.chat.id,
91
+ ...sent.messageThreadId === void 0 ? {} : { messageThreadId: sent.messageThreadId }
92
+ });
93
+ if (await routeMessage({
94
+ policy: dependencies.policy,
95
+ addressed,
96
+ decide: dependencies.decide,
97
+ question: async () => ({
98
+ agent: await input.request?.agentDescription() ?? "",
99
+ observed: dependencies.observed.list(chat),
100
+ message: observedMessage(sent, text)
101
+ })
102
+ }) === "observe") {
103
+ dependencies.observed.add(chat, observedMessage(sent, text === "" ? "[attachment]" : text));
104
+ return null;
105
+ }
106
+ if (voice !== void 0 && input.request !== void 0) input.request.text = text;
107
+ else if (voice !== void 0) notes.push(text);
108
+ await context.telegram.startTyping();
109
+ return {
110
+ auth: telegramPrincipal({
111
+ message: sent,
112
+ member: lookup.member
113
+ }),
114
+ context: [...observedContext(dependencies.observed.take(chat)), ...notes]
115
+ };
116
+ }
117
+ export { handleInboundMessage };
@@ -0,0 +1,32 @@
1
+ /**
2
+ * The bounded ring of group messages the router observed without answering. The router shows it to
3
+ * the decision model, and a reply carries it into the turn as context, so the agent knows what the
4
+ * chat was talking about.
5
+ *
6
+ * eve's `defineState` would be the durable home, but it is session state and needs an active eve
7
+ * context; an observed message has neither, because observing must not start a session. The ring
8
+ * therefore lives in process memory, per chat and forum topic, and a cold start begins empty.
9
+ */
10
+ /** How many observed messages a chat keeps; the oldest goes first. */
11
+ export declare const OBSERVED_MESSAGES = 30;
12
+ export interface ObservedMessage {
13
+ /** How the chat names the author: their username, else their first name. */
14
+ readonly author: string;
15
+ readonly text: string;
16
+ /** ISO time the message was sent. */
17
+ readonly at: string;
18
+ }
19
+ export interface ObservedMessages {
20
+ add(chat: string, message: ObservedMessage): void;
21
+ list(chat: string): ReadonlyArray<ObservedMessage>;
22
+ /** Returns and forgets the chat's ring, once a reply has carried it into a turn. */
23
+ take(chat: string): ReadonlyArray<ObservedMessage>;
24
+ }
25
+ export declare function observedMessages(capacity?: number): ObservedMessages;
26
+ /** The key of a chat's ring: the chat and, in a forum, the topic. */
27
+ export declare function observedChatKey(input: {
28
+ readonly chatId: string;
29
+ readonly messageThreadId?: number;
30
+ }): string;
31
+ /** The ring as turn context: one block the model reads before the message it answers. */
32
+ export declare function observedContext(messages: ReadonlyArray<ObservedMessage>): ReadonlyArray<string>;
@@ -0,0 +1,47 @@
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 OBSERVED_MESSAGES = 30;
8
+ const MAXIMUM_CHATS = 500;
9
+ const MAXIMUM_TEXT_LENGTH = 1e3;
10
+ function observedMessages(capacity = 30) {
11
+ const chats = new Map();
12
+ return {
13
+ add(chat, message) {
14
+ const ring = [...chats.get(chat) ?? [], {
15
+ ...message,
16
+ text: message.text.slice(0, MAXIMUM_TEXT_LENGTH)
17
+ }];
18
+ chats.delete(chat);
19
+ chats.set(chat, ring.slice(-capacity));
20
+ if (chats.size > MAXIMUM_CHATS) {
21
+ const oldest = chats.keys().next();
22
+ if (oldest.done !== true) chats.delete(oldest.value);
23
+ }
24
+ },
25
+ list(chat) {
26
+ return chats.get(chat) ?? [];
27
+ },
28
+ take(chat) {
29
+ const ring = chats.get(chat) ?? [];
30
+ chats.delete(chat);
31
+ return ring;
32
+ }
33
+ };
34
+ }
35
+ function observedChatKey(input) {
36
+ return input.messageThreadId === void 0 ? input.chatId : `${input.chatId}:${input.messageThreadId}`;
37
+ }
38
+ function observedContext(messages) {
39
+ if (messages.length === 0) return [];
40
+ return [[
41
+ "<observed_messages>",
42
+ "Earlier messages in this chat that were not addressed to you, oldest first:",
43
+ ...messages.map((message) => `[${message.at}] ${message.author}: ${message.text}`),
44
+ "</observed_messages>"
45
+ ].join("\n")];
46
+ }
47
+ export { OBSERVED_MESSAGES, observedChatKey, observedContext, observedMessages };
@@ -0,0 +1,20 @@
1
+ import type { TelegramMessage } from "eve/channels/telegram";
2
+ import type { SessionAuthContext } from "eve/context";
3
+ import type { PairedMember } from "../identities.js";
4
+ /** The route authenticator the ontology connection and the model resolver recognize. */
5
+ export declare const TELEGRAM_AUTHENTICATOR = "segmentstream-telegram";
6
+ /**
7
+ * The session principal of a paired sender. The member comes from the pairing, never from the
8
+ * message, so the model cannot choose whom it acts for. Pairing is by user: a member keeps their
9
+ * roles in every chat, and a group chat is recorded in the principal id and attributes for audit.
10
+ */
11
+ export declare function telegramPrincipal(input: {
12
+ readonly message: TelegramMessage & {
13
+ readonly from: NonNullable<TelegramMessage["from"]>;
14
+ };
15
+ readonly member: PairedMember;
16
+ }): SessionAuthContext;
17
+ /** The member a session principal acts for: `memberId` stamped by a SegmentStream channel. */
18
+ export declare function memberIdOf(principal: SessionAuthContext | null | undefined): string | undefined;
19
+ /** The member's roles on a session principal; empty when it carries none. */
20
+ export declare function rolesOf(principal: SessionAuthContext | null | undefined): ReadonlyArray<string>;
@@ -0,0 +1,40 @@
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 TELEGRAM_AUTHENTICATOR = "segmentstream-telegram";
8
+ function isGroup(message) {
9
+ return message.chat.type === "group" || message.chat.type === "supergroup";
10
+ }
11
+ function telegramPrincipal(input) {
12
+ const { message, member } = input;
13
+ const sender = message.from.id;
14
+ const attributes = {
15
+ memberId: member.id,
16
+ roles: member.roles,
17
+ chatId: message.chat.id,
18
+ chatType: message.chat.type
19
+ };
20
+ if (member.name !== null) attributes.memberName = member.name;
21
+ if (message.from.username !== void 0) attributes.username = message.from.username;
22
+ return {
23
+ authenticator: TELEGRAM_AUTHENTICATOR,
24
+ issuer: "telegram",
25
+ principalId: isGroup(message) ? `telegram:${message.chat.id}:${sender}` : `telegram:${sender}`,
26
+ principalType: "user",
27
+ subject: sender,
28
+ attributes
29
+ };
30
+ }
31
+ function memberIdOf(principal) {
32
+ const memberId = principal?.attributes.memberId;
33
+ return typeof memberId === "string" && memberId !== "" ? memberId : void 0;
34
+ }
35
+ function rolesOf(principal) {
36
+ const roles = principal?.attributes.roles;
37
+ if (typeof roles === "string") return [roles];
38
+ return roles ?? [];
39
+ }
40
+ export { TELEGRAM_AUTHENTICATOR, memberIdOf, rolesOf, telegramPrincipal };
@@ -0,0 +1,28 @@
1
+ import type { ConnectionProxy } from "../connection-proxy.js";
2
+ /**
3
+ * Telegram puts the bot token in the URL path: eve's Telegram API layer calls
4
+ * `${apiBaseUrl}/bot<token>/<method>` for methods and `${fileBaseUrl}/file/bot<token>/<path>` for
5
+ * downloads, and it refuses to run without a token. The token lives in backend-next behind the
6
+ * workspace's Telegram connection, whose `bot` API is `https://api.telegram.org/bot<token>/` and
7
+ * whose `file` API is `https://api.telegram.org/file/bot<token>/`.
8
+ *
9
+ * So the channel gets a placeholder token and an origin that does not resolve, and its `fetch`
10
+ * is this function: it accepts only URLs under that origin with that placeholder, strips both, and
11
+ * sends the method or file path through the connection proxy. A request that somehow bypassed it
12
+ * would fail to resolve rather than reach Telegram, and the real token never enters this process.
13
+ */
14
+ /** The origin eve's Telegram API layer is pointed at. `.invalid` never resolves (RFC 6761). */
15
+ export declare const TELEGRAM_PROXY_ORIGIN = "https://telegram.segmentstream.invalid";
16
+ /** Stands in for the bot token, which only backend-next holds. Not a secret. */
17
+ export declare const TELEGRAM_PLACEHOLDER_TOKEN = "segmentstream-connection";
18
+ /** The Telegram channel tried to send a request this adapter does not route through the proxy. */
19
+ export declare class TelegramProxyRouteError extends Error {
20
+ readonly name = "TelegramProxyRouteError";
21
+ }
22
+ /** Where a Telegram URL goes on the connection: an API and a path relative to its base URL. */
23
+ export declare function telegramProxyRoute(url: string): {
24
+ readonly api: "bot" | "file";
25
+ readonly path: string;
26
+ };
27
+ /** The `fetch` eve's Telegram channel calls; see the module comment. */
28
+ export declare function telegramProxyFetch(proxy: ConnectionProxy): typeof fetch;
@@ -0,0 +1,69 @@
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 TELEGRAM_PROXY_ORIGIN = "https://telegram.segmentstream.invalid";
8
+ const TELEGRAM_PLACEHOLDER_TOKEN = "segmentstream-connection";
9
+ const METHOD_PREFIX = `${TELEGRAM_PROXY_ORIGIN}/bot${TELEGRAM_PLACEHOLDER_TOKEN}/`;
10
+ const FILE_PREFIX = `${TELEGRAM_PROXY_ORIGIN}/file/bot${TELEGRAM_PLACEHOLDER_TOKEN}/`;
11
+ const METHODS = new Set([
12
+ "GET",
13
+ "POST",
14
+ "PUT",
15
+ "PATCH",
16
+ "DELETE",
17
+ "HEAD",
18
+ "OPTIONS"
19
+ ]);
20
+ var TelegramProxyRouteError = class extends Error {
21
+ name = "TelegramProxyRouteError";
22
+ };
23
+ function requestUrl(input) {
24
+ if (typeof input === "string") return input;
25
+ return input instanceof URL ? input.href : input.url;
26
+ }
27
+ function telegramProxyRoute(url) {
28
+ if (url.startsWith(FILE_PREFIX)) return {
29
+ api: "file",
30
+ path: `/${url.slice(FILE_PREFIX.length)}`
31
+ };
32
+ if (url.startsWith(METHOD_PREFIX)) return {
33
+ api: "bot",
34
+ path: `/${url.slice(METHOD_PREFIX.length)}`
35
+ };
36
+ throw new TelegramProxyRouteError("The Telegram channel sends requests only through the connection proxy.");
37
+ }
38
+ function headerPairs(headers) {
39
+ return [...new Headers(headers).entries()];
40
+ }
41
+ async function bodyBytes(body) {
42
+ if (body === void 0 || body === null) return new Uint8Array();
43
+ return new Uint8Array(await new Response(body).arrayBuffer());
44
+ }
45
+ function proxyMethod(method) {
46
+ const upper = (method ?? "GET").toUpperCase();
47
+ for (const candidate of METHODS) if (candidate === upper) return candidate;
48
+ throw new TelegramProxyRouteError(`The connection proxy does not forward ${upper} requests.`);
49
+ }
50
+ function hasNullBody(status) {
51
+ return status === 101 || status === 204 || status === 205 || status === 304;
52
+ }
53
+ function telegramProxyFetch(proxy) {
54
+ return async (input, init) => {
55
+ const route = telegramProxyRoute(requestUrl(input));
56
+ const reply = await proxy.request({
57
+ api: route.api,
58
+ method: proxyMethod(init?.method),
59
+ path: route.path,
60
+ headers: headerPairs(init?.headers),
61
+ body: await bodyBytes(init?.body)
62
+ });
63
+ return new Response(hasNullBody(reply.status) ? null : Buffer.from(reply.body), {
64
+ status: reply.status,
65
+ headers: reply.headers.map(([name, value]) => [name, value])
66
+ });
67
+ };
68
+ }
69
+ export { TELEGRAM_PLACEHOLDER_TOKEN, TELEGRAM_PROXY_ORIGIN, TelegramProxyRouteError, telegramProxyFetch, telegramProxyRoute };