@segmentstream/agent 0.1.3 → 0.1.5

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.
@@ -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 };
@@ -1,4 +1,5 @@
1
1
  import type { ChannelSource } from "eve/channels";
2
+ import type { SessionOperations } from "./sessions.js";
2
3
  /**
3
4
  * What one webhook request shares between eve's `onMessage` hook and the route wrapper around it.
4
5
  * `onMessage` may only return `{ auth, context, title }`; it cannot change the message eve sends.
@@ -11,6 +12,10 @@ export declare class InboundRequest {
11
12
  readonly agentDescription: () => Promise<string>;
12
13
  /** The text the turn should carry instead of the parsed message, once a transcript exists. */
13
14
  text: string | undefined;
15
+ /** The route's session operations, on addresses already mapped to the configured scope. */
16
+ sessions: SessionOperations | undefined;
17
+ /** Keeps work the request started, such as a chat report, alive after it answers. */
18
+ waitUntil: ((task: Promise<unknown>) => void) | undefined;
14
19
  constructor(
15
20
  /** The agent's description from the compiled manifest, for the routing decision. */
16
21
  agentDescription: () => Promise<string>);
@@ -8,6 +8,8 @@ import { AsyncLocalStorage } from "node:async_hooks";
8
8
  var InboundRequest = class {
9
9
  agentDescription;
10
10
  text;
11
+ sessions;
12
+ waitUntil;
11
13
  constructor(agentDescription) {
12
14
  this.agentDescription = agentDescription;
13
15
  }
@@ -1,22 +1,35 @@
1
1
  import type { TelegramContext, TelegramInboundResult, TelegramMessage } from "eve/channels/telegram";
2
- import type { TelegramPolicy } from "../../config.js";
2
+ import type { SessionSettings, TelegramPolicy } from "../../config.js";
3
3
  import type { AgentIdentities } from "../identities.js";
4
4
  import type { BotIdentityLookup } from "./bot.js";
5
+ import { type AgentChats } from "./chats.js";
5
6
  import type { InboundRequest } from "./inbound-request.js";
6
7
  import { type ObservedMessages } from "./observed.js";
7
8
  import { type RouteDecider } from "./router.js";
9
+ import { type SessionPolicy } from "./sessions.js";
8
10
  import { type Transcriber } from "./voice.js";
9
11
  /**
10
12
  * eve's Telegram `onMessage` hook, in the order `agent-design.md` gives:
11
13
  *
12
14
  * 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 button.
15
+ * start a turn. An unknown sender who writes to the bot privately gets a pairing button. A group
16
+ * message reports its chat to the ontology's registry in the background, whatever follows. A
17
+ * group's notice that a member left is never a turn: a paired member's departure is reported,
18
+ * and so is the agent's own, which unlinks every member. Neither is the notice, in the old chat
19
+ * or the new one, that a group became a supergroup: the migration is reported.
14
20
  * 2. Voice becomes text, so the router and the agent read what was said.
15
21
  * 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.
22
+ * 4. The session policy runs on the session the message continues: an idle one is cleared, the
23
+ * first message after the day's compaction time compacts it.
24
+ * 5. An answered message dispatches as the paired member, with the ring, the registry's `<chat>`
25
+ * block and a `<speaker>` block naming the member, their roles and the message they replied to
26
+ * ahead of it as context. An answered group message waits for its chat's report first, so that
27
+ * a chat new to the process already carries its `<chat>` block.
17
28
  */
18
29
  export interface InboundDependencies {
19
30
  readonly policy: TelegramPolicy;
31
+ readonly sessions: () => SessionSettings;
32
+ readonly sessionPolicy: SessionPolicy;
20
33
  readonly sttModel: string | false;
21
34
  readonly identities: AgentIdentities;
22
35
  readonly bot: BotIdentityLookup;
@@ -24,6 +37,7 @@ export interface InboundDependencies {
24
37
  readonly transcribe: Transcriber;
25
38
  readonly download: (fileId: string) => Promise<Uint8Array>;
26
39
  readonly observed: ObservedMessages;
40
+ readonly chats: AgentChats;
27
41
  /** The app's `/agent/connect` link for a pairing code. */
28
42
  readonly connectUrl: (code: string) => string;
29
43
  }
@@ -5,12 +5,15 @@ const __filename = __eveFileURLToPath(import.meta.url);
5
5
  __eveDirname(__filename);
6
6
  __eveCreateRequire(import.meta.url);
7
7
  import { telegramPrincipal } from "./principal.mjs";
8
+ import { chatContext } from "./chats.mjs";
8
9
  import { observedChatKey, observedContext } from "./observed.mjs";
9
10
  import { isAddressed, routeMessage } from "./router.mjs";
11
+ import { messageAddress, scopedAddress } from "./sessions.mjs";
10
12
  import { voiceNote, voiceOf, voiceText } from "./voice.mjs";
11
13
  const UNKNOWN_PRIVATE = "Hi, I don't know you yet. Connect your Telegram account to the workspace and I'll act as you. The button works for ten minutes.";
12
14
  const UNKNOWN_IN_GROUP = "I don't know you yet. Message me privately to connect your account.";
13
15
  const REFUSED = "Your access to this agent was revoked, or your membership is inactive. Ask a workspace administrator.";
16
+ const MAXIMUM_QUOTE_LENGTH = 500;
14
17
  function buttonTo(text, url) {
15
18
  return { inline_keyboard: [[{
16
19
  text,
@@ -26,13 +29,41 @@ function sentAt(message) {
26
29
  const date = message.raw.date;
27
30
  return new Date(typeof date === "number" ? date * 1e3 : Date.now()).toISOString();
28
31
  }
29
- function observedMessage(message, text) {
32
+ function authorOf(user, member) {
33
+ const name = display(user) ?? user.id;
34
+ if (member === void 0) return name;
35
+ const roles = member.roles.length === 0 ? "" : ` (${member.roles.join(", ")})`;
36
+ return member.name === null ? `${name}${roles}` : `${member.name}${roles}, ${name}`;
37
+ }
38
+ function observedMessage(message, text, member) {
30
39
  return {
31
- author: display(message.from) ?? message.from.id,
40
+ author: authorOf(message.from, member),
32
41
  text,
33
42
  at: sentAt(message)
34
43
  };
35
44
  }
45
+ function isRecord(value) {
46
+ return typeof value === "object" && value !== null && !Array.isArray(value);
47
+ }
48
+ function speakerBlock(input) {
49
+ const { message, member, bot } = input;
50
+ const roles = member.roles.length === 0 ? "no roles" : member.roles.join(", ");
51
+ const lines = [
52
+ "<speaker>",
53
+ `member: ${member.name ?? "unnamed member"} (${roles})`,
54
+ `telegram: ${display(message.from) ?? "no name"}, user_id ${message.from.id}`
55
+ ];
56
+ const raw = message.raw.reply_to_message;
57
+ if (isRecord(raw)) {
58
+ const from = isRecord(raw.from) ? raw.from : {};
59
+ const author = from.is_bot === true && bot !== void 0 && String(from.id) === bot.id ? "the agent" : typeof from.username === "string" ? `@${from.username}` : typeof from.first_name === "string" ? from.first_name : "unknown sender";
60
+ const text = typeof raw.text === "string" ? raw.text : typeof raw.caption === "string" ? raw.caption : "";
61
+ const quoted = text.length > MAXIMUM_QUOTE_LENGTH ? `${text.slice(0, MAXIMUM_QUOTE_LENGTH)}…` : text;
62
+ lines.push(`reply_to: ${author}: ${JSON.stringify(quoted)}`);
63
+ }
64
+ lines.push("</speaker>");
65
+ return lines.join("\n");
66
+ }
36
67
  function unknownInGroup(bot) {
37
68
  return bot?.username === void 0 ? { text: UNKNOWN_IN_GROUP } : {
38
69
  text: UNKNOWN_IN_GROUP,
@@ -60,9 +91,67 @@ async function answerUnpaired(input) {
60
91
  reply_markup: buttonTo("Connect account", dependencies.connectUrl(pairing.code))
61
92
  });
62
93
  }
94
+ function isGroupChat(message) {
95
+ return message.chat.type === "group" || message.chat.type === "supergroup";
96
+ }
97
+ async function reportDeparture(input) {
98
+ const { context, message, left, dependencies } = input;
99
+ const id = left.id;
100
+ if (typeof id !== "number" && typeof id !== "string") return;
101
+ if (left.is_bot === true) {
102
+ const bot = await dependencies.bot(context.telegram);
103
+ if (bot !== void 0 && String(id) === bot.id) await dependencies.chats.agentLeft({
104
+ chatId: message.chat.id,
105
+ title: message.chat.title ?? ""
106
+ });
107
+ return;
108
+ }
109
+ const lookup = await dependencies.identities.lookup({
110
+ provider: "telegram",
111
+ externalId: String(id)
112
+ });
113
+ if (lookup.status === "paired") await dependencies.chats.left({
114
+ chatId: message.chat.id,
115
+ title: message.chat.title ?? "",
116
+ memberId: lookup.member.id
117
+ });
118
+ }
119
+ function rawChatId(value) {
120
+ return typeof value === "number" || typeof value === "string" && value !== "" ? String(value) : void 0;
121
+ }
122
+ function migrationOf(message) {
123
+ const to = rawChatId(message.raw.migrate_to_chat_id);
124
+ const from = rawChatId(message.raw.migrate_from_chat_id);
125
+ if (to !== void 0) return {
126
+ chatId: to,
127
+ migratedFrom: message.chat.id
128
+ };
129
+ return from === void 0 ? void 0 : {
130
+ chatId: message.chat.id,
131
+ migratedFrom: from
132
+ };
133
+ }
63
134
  async function handleInboundMessage(input) {
64
135
  const { context, dependencies } = input;
65
136
  const message = input.message;
137
+ const left = message.raw.left_chat_member;
138
+ const migration = isGroupChat(message) ? migrationOf(message) : void 0;
139
+ if (isGroupChat(message) && isRecord(left)) {
140
+ await reportDeparture({
141
+ context,
142
+ message,
143
+ left,
144
+ dependencies
145
+ });
146
+ return null;
147
+ }
148
+ if (migration !== void 0) {
149
+ await dependencies.chats.migrated({
150
+ ...migration,
151
+ title: message.chat.title ?? ""
152
+ });
153
+ return null;
154
+ }
66
155
  if (message.from === void 0 || message.from.isBot || message.chat.type === "channel") return null;
67
156
  const sent = {
68
157
  ...message,
@@ -74,6 +163,13 @@ async function handleInboundMessage(input) {
74
163
  provider: "telegram",
75
164
  externalId: sent.from.id
76
165
  });
166
+ const group = isGroupChat(sent);
167
+ const report = group ? dependencies.chats.seen({
168
+ chatId: sent.chat.id,
169
+ title: sent.chat.title ?? "",
170
+ ...lookup.status === "paired" ? { memberId: lookup.member.id } : {}
171
+ }) : void 0;
172
+ if (report !== void 0) input.request?.waitUntil?.(report);
77
173
  if (lookup.status !== "paired") {
78
174
  if (addressed) await answerUnpaired({
79
175
  context,
@@ -84,6 +180,8 @@ async function handleInboundMessage(input) {
84
180
  });
85
181
  return null;
86
182
  }
183
+ const address = scopedAddress(messageAddress(sent), dependencies.sessions().scope);
184
+ const operations = input.request?.sessions;
87
185
  const voice = voiceOf(sent.raw);
88
186
  const caption = sent.text || sent.caption;
89
187
  const notes = [];
@@ -110,21 +208,40 @@ async function handleInboundMessage(input) {
110
208
  question: async () => ({
111
209
  agent: await input.request?.agentDescription() ?? "",
112
210
  observed: dependencies.observed.list(chat),
113
- message: observedMessage(sent, text)
211
+ message: observedMessage(sent, text, lookup.member)
114
212
  })
115
213
  }) === "observe") {
116
- dependencies.observed.add(chat, observedMessage(sent, text === "" ? "[attachment]" : text));
214
+ dependencies.observed.add(chat, observedMessage(sent, text === "" ? "[attachment]" : text, lookup.member));
117
215
  return null;
118
216
  }
119
217
  if (voice !== void 0 && input.request !== void 0) input.request.text = text;
120
218
  else if (voice !== void 0) notes.push(text);
219
+ if (operations !== void 0) {
220
+ const newest = dependencies.observed.list(chat).at(-1);
221
+ if (await dependencies.sessionPolicy.apply({
222
+ address,
223
+ operations,
224
+ ...newest === void 0 ? {} : { activeAt: Date.parse(newest.at) || 0 }
225
+ }) === "cleared") dependencies.observed.take(chat);
226
+ dependencies.sessionPolicy.delivered(address);
227
+ }
121
228
  await context.telegram.startTyping();
229
+ await report;
122
230
  return {
123
231
  auth: telegramPrincipal({
124
232
  message: sent,
125
233
  member: lookup.member
126
234
  }),
127
- context: [...observedContext(dependencies.observed.take(chat)), ...notes]
235
+ context: [
236
+ ...observedContext(dependencies.observed.take(chat)),
237
+ ...group ? chatContext(dependencies.chats.chat(sent.chat.id)) : [],
238
+ speakerBlock({
239
+ message: sent,
240
+ member: lookup.member,
241
+ bot
242
+ }),
243
+ ...notes
244
+ ]
128
245
  };
129
246
  }
130
247
  export { handleInboundMessage };
@@ -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>;
@@ -0,0 +1,173 @@
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 { TELEGRAM_AUTHENTICATOR } from "./principal.mjs";
8
+ import { formatAddress, scopedAddress } from "./sessions.mjs";
9
+ import { POST } from "eve/channels";
10
+ const MESSAGES_ROUTE = "/eve/v1/telegram/messages";
11
+ const MAX_REQUEST_MESSAGE_LENGTH = 4e3;
12
+ const CHAT_ID = /^-?\d{1,20}$/;
13
+ function isRecord(value) {
14
+ return typeof value === "object" && value !== null && !Array.isArray(value);
15
+ }
16
+ function isText(value) {
17
+ return typeof value === "string" && value.trim() !== "";
18
+ }
19
+ function user(value) {
20
+ return isRecord(value) && isText(value.id) && isText(value.name) ? {
21
+ id: value.id,
22
+ name: value.name
23
+ } : void 0;
24
+ }
25
+ function roles(value) {
26
+ if (value === void 0) return [];
27
+ return Array.isArray(value) && value.every((role) => typeof role === "string") ? value : void 0;
28
+ }
29
+ function agentRequest(value) {
30
+ if (!isRecord(value)) return;
31
+ const { target, message, requester } = value;
32
+ const person = value.person === null ? null : user(value.person);
33
+ const from = user(requester);
34
+ const held = isRecord(requester) ? roles(requester.roles) : void 0;
35
+ const actAs = isRecord(requester) ? requester.actAs : void 0;
36
+ if (!isRecord(target) || target.kind !== "group" && target.kind !== "private" || target.provider !== "telegram" || typeof target.chatId !== "string" || !CHAT_ID.test(target.chatId) || !isText(message) || message.length > MAX_REQUEST_MESSAGE_LENGTH || person === void 0 || from === void 0 || !isRecord(requester) || requester.kind !== "member" && requester.kind !== "service" || held === void 0 || actAs !== void 0 && typeof actAs !== "boolean") return;
37
+ return {
38
+ target: {
39
+ kind: target.kind,
40
+ provider: "telegram",
41
+ chatId: target.chatId
42
+ },
43
+ message,
44
+ person,
45
+ requester: {
46
+ ...from,
47
+ kind: requester.kind,
48
+ roles: held,
49
+ actAs: actAs ?? requester.kind === "member"
50
+ }
51
+ };
52
+ }
53
+ function requestPrincipal(request) {
54
+ const { requester, target } = request;
55
+ if (requester.kind === "member" && requester.actAs) return {
56
+ authenticator: TELEGRAM_AUTHENTICATOR,
57
+ issuer: "segmentstream",
58
+ principalId: `segmentstream:${requester.id}`,
59
+ principalType: "user",
60
+ subject: requester.id,
61
+ attributes: {
62
+ memberId: requester.id,
63
+ memberName: requester.name,
64
+ roles: requester.roles,
65
+ chatId: target.chatId,
66
+ chatType: target.kind
67
+ }
68
+ };
69
+ return {
70
+ authenticator: TELEGRAM_AUTHENTICATOR,
71
+ issuer: "segmentstream",
72
+ principalId: `segmentstream:${requester.kind}:${requester.id}`,
73
+ principalType: requester.kind === "member" ? "user" : "service",
74
+ subject: requester.id,
75
+ attributes: {
76
+ requesterId: requester.id,
77
+ requesterName: requester.name,
78
+ roles: requester.roles,
79
+ chatId: target.chatId,
80
+ chatType: target.kind
81
+ }
82
+ };
83
+ }
84
+ function requestContext(request) {
85
+ const { requester, person } = request;
86
+ const to = person === null ? "" : ` to ${person.name} (user_id ${person.id})`;
87
+ const lines = [
88
+ "<agent_request>",
89
+ `A request from ${requester.name} (${requester.kind}, user_id ${requester.id}) arrived through the ontology action agent_chat.send_message; the message is the request.`,
90
+ `Write in this chat${to}, ask what the request needs, and handle the replies here.`
91
+ ];
92
+ if (requester.kind === "service" || !requester.actAs) lines.push(`${requester.kind === "service" ? "The requester is a service, not a member" : "The requester has not paired a Telegram account with the agent"}, so this turn runs without a member and the ontology is not available in it. Each member who replies acts with their own permissions.`);
93
+ lines.push("</agent_request>");
94
+ return lines.join("\n");
95
+ }
96
+ function withContext(source, context) {
97
+ return {
98
+ ...source,
99
+ send(message, options) {
100
+ return source.send(message, {
101
+ ...options,
102
+ context: [...options.context ?? [], context]
103
+ });
104
+ }
105
+ };
106
+ }
107
+ function bearer(header) {
108
+ return (header === null ? null : /^Bearer\s+(\S+)$/i.exec(header.trim()))?.[1];
109
+ }
110
+ function error(code, status) {
111
+ return Response.json({ error: code }, { status });
112
+ }
113
+ function messagesRoute(input) {
114
+ const log = input.log ?? ((message, detail) => console.warn(message, detail));
115
+ return POST(MESSAGES_ROUTE, async (request, args) => {
116
+ if (!input.enabled()) return error("channels_disabled", 503);
117
+ if (!input.configured()) return error("not_found", 404);
118
+ const token = bearer(request.headers.get("authorization"));
119
+ if (token === void 0) return error("unauthorized", 401);
120
+ let verifier;
121
+ try {
122
+ verifier = input.verify();
123
+ } catch (cause) {
124
+ log("The agent cannot verify requests: the runtime identity is not configured.", cause);
125
+ return error("unavailable", 503);
126
+ }
127
+ const verification = await verifier(token);
128
+ if (verification.status === "unavailable") {
129
+ log("The agent cannot verify requests: the runtime key set is unavailable.", verification.reason);
130
+ return error("unavailable", 503);
131
+ }
132
+ if (verification.status !== "valid") {
133
+ log("The agent refused a request with an invalid runtime token.", verification.reason);
134
+ return error("unauthorized", 401);
135
+ }
136
+ let body;
137
+ try {
138
+ body = await request.json();
139
+ } catch {
140
+ return error("invalid_body", 400);
141
+ }
142
+ const parsed = agentRequest(body);
143
+ const receive = input.channel.receive;
144
+ if (parsed === void 0) return error("invalid_body", 400);
145
+ if (receive === void 0) throw new Error("eve's Telegram channel has no receive hook.");
146
+ const chatAddress = scopedAddress(formatAddress({
147
+ chatId: parsed.target.chatId,
148
+ messageThreadId: "",
149
+ conversationId: ""
150
+ }), input.scope());
151
+ await input.sessionPolicy.apply({
152
+ address: chatAddress,
153
+ operations: {
154
+ resolveSession: args.resolveSession,
155
+ from: args.from
156
+ }
157
+ });
158
+ const session = await receive({
159
+ target: { chatId: parsed.target.chatId },
160
+ message: parsed.message,
161
+ auth: requestPrincipal(parsed)
162
+ }, {
163
+ from: (address) => withContext(args.from(address), requestContext(parsed)),
164
+ resolveSession: args.resolveSession
165
+ });
166
+ input.sessionPolicy.delivered(chatAddress);
167
+ return Response.json({
168
+ delivered: true,
169
+ sessionId: session.id
170
+ }, { status: 202 });
171
+ });
172
+ }
173
+ export { MESSAGES_ROUTE, agentRequest, messagesRoute, requestContext, requestPrincipal };