@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
@@ -5,6 +5,8 @@ const __filename = __eveFileURLToPath(import.meta.url);
5
5
  __eveDirname(__filename);
6
6
  __eveCreateRequire(import.meta.url);
7
7
  const MAXIMUM_PROXY_BODY_BYTES = 2097152;
8
+ const MAXIMUM_DOWNLOAD_BYTES = 20971520;
9
+ const PROXY_STREAM_MEDIA_TYPE = "application/vnd.segmentstream.proxy-stream.v1+ndjson";
8
10
  const CALL_TIMEOUT_MS = 4e4;
9
11
  var ConnectionProxyError = class extends Error {
10
12
  name = "ConnectionProxyError";
@@ -55,59 +57,153 @@ function connectionProxy(input) {
55
57
  const { environment, connection } = input;
56
58
  const fetchImpl = input.fetch ?? fetch;
57
59
  const url = `${`${environment.segmentstreamUrl}/api/internal/v1/workspaces/${environment.workspaceId}`}/ontology/connections/${encodeURIComponent(connection)}/request`;
60
+ async function send(call, accept) {
61
+ if (call.body.byteLength > 2097152) throw new ConnectionProxyError({
62
+ connection,
63
+ code: "invalid_request",
64
+ message: "The request body exceeds the proxy's 2 MiB limit."
65
+ });
66
+ const token = await input.token();
67
+ try {
68
+ return await fetchImpl(url, {
69
+ method: "POST",
70
+ headers: {
71
+ "x-segmentstream-runtime-identity": token,
72
+ "x-segmentstream-organization-id": environment.organizationId,
73
+ "content-type": "application/json",
74
+ accept
75
+ },
76
+ body: JSON.stringify({
77
+ ...call.api === void 0 ? {} : { api: call.api },
78
+ method: call.method,
79
+ path: call.path,
80
+ headers: call.headers,
81
+ body: Buffer.from(call.body).toString("base64")
82
+ }),
83
+ redirect: "error",
84
+ signal: AbortSignal.timeout(CALL_TIMEOUT_MS)
85
+ });
86
+ } catch (error) {
87
+ throw new ConnectionProxyError({
88
+ connection,
89
+ code: error instanceof DOMException && error.name === "TimeoutError" ? "timeout" : "unavailable",
90
+ message: "SegmentStream's connection proxy could not be reached.",
91
+ cause: error
92
+ });
93
+ }
94
+ }
95
+ async function buffered(response) {
96
+ const text = await response.text();
97
+ let value;
98
+ try {
99
+ value = JSON.parse(text);
100
+ } catch (error) {
101
+ if (response.ok) throw new ConnectionProxyError({
102
+ connection,
103
+ code: "invalid_response",
104
+ message: "The connection proxy answered with an invalid response.",
105
+ cause: error
106
+ });
107
+ throw refusal(connection, response.status, void 0, error);
108
+ }
109
+ if (!response.ok) throw refusal(connection, response.status, value);
110
+ return decodeReply(connection, value);
111
+ }
58
112
  return {
59
113
  connection,
60
114
  async request(call) {
61
- if (call.body.byteLength > 2097152) throw new ConnectionProxyError({
62
- connection,
63
- code: "invalid_request",
64
- message: "The request body exceeds the proxy's 2 MiB limit."
65
- });
66
- const token = await input.token();
67
- let response;
115
+ return buffered(await send(call, "application/json"));
116
+ },
117
+ async download(call, maxBytes = MAXIMUM_DOWNLOAD_BYTES) {
118
+ const response = await send(call, PROXY_STREAM_MEDIA_TYPE);
119
+ const type = response.headers.get("content-type") ?? "";
120
+ if (!response.ok || !type.startsWith("application/vnd.segmentstream.proxy-stream.v1+ndjson") || response.body === null) return buffered(response);
68
121
  try {
69
- response = await fetchImpl(url, {
70
- method: "POST",
71
- headers: {
72
- "x-segmentstream-runtime-identity": token,
73
- "x-segmentstream-organization-id": environment.organizationId,
74
- "content-type": "application/json",
75
- accept: "application/json"
76
- },
77
- body: JSON.stringify({
78
- ...call.api === void 0 ? {} : { api: call.api },
79
- method: call.method,
80
- path: call.path,
81
- headers: call.headers,
82
- body: Buffer.from(call.body).toString("base64")
83
- }),
84
- redirect: "error",
85
- signal: AbortSignal.timeout(CALL_TIMEOUT_MS)
86
- });
122
+ return await framedReply(connection, response.body, maxBytes);
87
123
  } catch (error) {
124
+ if (error instanceof ConnectionProxyError) throw error;
88
125
  throw new ConnectionProxyError({
89
126
  connection,
90
127
  code: error instanceof DOMException && error.name === "TimeoutError" ? "timeout" : "unavailable",
91
- message: "SegmentStream's connection proxy could not be reached.",
128
+ message: "SegmentStream's connection proxy stopped relaying the response.",
92
129
  cause: error
93
130
  });
94
131
  }
95
- const text = await response.text();
96
- let value;
97
- try {
98
- value = JSON.parse(text);
99
- } catch (error) {
100
- if (response.ok) throw new ConnectionProxyError({
132
+ }
133
+ };
134
+ }
135
+ async function framedReply(connection, body, maxBytes) {
136
+ const invalid = () => new ConnectionProxyError({
137
+ connection,
138
+ code: "invalid_response",
139
+ message: "The connection proxy answered with an invalid stream."
140
+ });
141
+ const reader = body.getReader();
142
+ const decoder = new TextDecoder();
143
+ const chunks = [];
144
+ let head;
145
+ let size = 0;
146
+ let pending = "";
147
+ let ended = false;
148
+ try {
149
+ while (!ended) {
150
+ const next = await reader.read();
151
+ if (next.done) break;
152
+ pending += decoder.decode(next.value, { stream: true });
153
+ for (let newline = pending.indexOf("\n"); newline >= 0; newline = pending.indexOf("\n")) {
154
+ const line = pending.slice(0, newline);
155
+ pending = pending.slice(newline + 1);
156
+ if (line.trim() === "") continue;
157
+ let frame;
158
+ try {
159
+ frame = JSON.parse(line);
160
+ } catch (error) {
161
+ throw new ConnectionProxyError({
162
+ connection,
163
+ code: "invalid_response",
164
+ message: "The connection proxy answered with an invalid stream.",
165
+ cause: error
166
+ });
167
+ }
168
+ if (!isRecord(frame)) throw invalid();
169
+ if (head === void 0) {
170
+ if (typeof frame.statusCode !== "number" || !isHeaderPairs(frame.headers)) throw invalid();
171
+ head = {
172
+ status: frame.statusCode,
173
+ headers: frame.headers
174
+ };
175
+ } else if (typeof frame.data === "string") {
176
+ const chunk = Buffer.from(frame.data, "base64");
177
+ size += chunk.byteLength;
178
+ if (size > maxBytes) throw new ConnectionProxyError({
179
+ connection,
180
+ code: "too_large",
181
+ message: `The response exceeds the ${Math.floor(maxBytes / 1024 / 1024)} MiB download limit.`
182
+ });
183
+ chunks.push(chunk);
184
+ } else if (frame.end === true) {
185
+ ended = true;
186
+ break;
187
+ } else if (typeof frame.error === "string") throw new ConnectionProxyError({
101
188
  connection,
102
- code: "invalid_response",
103
- message: "The connection proxy answered with an invalid response.",
104
- cause: error
189
+ code: frame.error,
190
+ message: "SegmentStream's connection proxy failed while relaying the response."
105
191
  });
106
- throw refusal(connection, response.status, void 0, error);
192
+ else throw invalid();
107
193
  }
108
- if (!response.ok) throw refusal(connection, response.status, value);
109
- return decodeReply(connection, value);
110
194
  }
195
+ } finally {
196
+ await reader.cancel().catch(() => void 0);
197
+ }
198
+ if (head === void 0 || !ended) throw new ConnectionProxyError({
199
+ connection,
200
+ code: "unavailable",
201
+ message: "The connection proxy's stream ended before the response did."
202
+ });
203
+ return {
204
+ status: head.status,
205
+ headers: head.headers,
206
+ body: new Uint8Array(Buffer.concat(chunks))
111
207
  };
112
208
  }
113
- export { ConnectionProxyError, MAXIMUM_PROXY_BODY_BYTES, connectionProxy };
209
+ export { ConnectionProxyError, MAXIMUM_DOWNLOAD_BYTES, MAXIMUM_PROXY_BODY_BYTES, PROXY_STREAM_MEDIA_TYPE, connectionProxy };
@@ -20,6 +20,19 @@ export interface ProxyEnvironment {
20
20
  readonly organizationId: string;
21
21
  }
22
22
  export declare function proxyEnvironment(environment?: Environment): ProxyEnvironment;
23
+ /**
24
+ * Whom the agent trusts as a caller of its own routes: Vercel OIDC tokens of the functions of this
25
+ * deployment's project, such as the ontology service's. The platform sets the issuer and the audience
26
+ * on the project for the ontology service; Vercel sets `VERCEL_PROJECT_ID`.
27
+ */
28
+ export interface RuntimeIdentityEnvironment {
29
+ /** The team's issuer, `https://oidc.vercel.com/<team slug>`, without a trailing slash. */
30
+ readonly issuer: string;
31
+ /** The audience Vercel issues function tokens for: `https://vercel.com/<team slug>`. */
32
+ readonly audience: string;
33
+ readonly projectId: string;
34
+ }
35
+ export declare function runtimeIdentityEnvironment(environment?: Environment): RuntimeIdentityEnvironment;
23
36
  /** The secret Telegram echoes in `X-Telegram-Bot-Api-Secret-Token`; `undefined` when unset. */
24
37
  export declare function webhookSecret(environment?: Environment): string | undefined;
25
38
  /**
@@ -33,6 +33,13 @@ function proxyEnvironment(environment = process.env) {
33
33
  organizationId: required(environment, "SEGMENTSTREAM_ORGANIZATION_ID")
34
34
  };
35
35
  }
36
+ function runtimeIdentityEnvironment(environment = process.env) {
37
+ return {
38
+ issuer: withoutTrailingSlashes(required(environment, "SEGMENTSTREAM_RUNTIME_OIDC_ISSUER")),
39
+ audience: required(environment, "SEGMENTSTREAM_RUNTIME_OIDC_AUDIENCE"),
40
+ projectId: required(environment, "VERCEL_PROJECT_ID")
41
+ };
42
+ }
36
43
  function webhookSecret(environment = process.env) {
37
44
  const secret = environment.SEGMENTSTREAM_AGENT_WEBHOOK_SECRET;
38
45
  return secret === void 0 || secret === "" ? void 0 : secret;
@@ -43,4 +50,4 @@ function channelsEnabled(environment = process.env) {
43
50
  function isPreview(environment = process.env) {
44
51
  return environment.VERCEL_ENV === "preview";
45
52
  }
46
- export { AgentEnvironmentError, appOrigin, channelsEnabled, isPreview, proxyEnvironment, webhookSecret };
53
+ export { AgentEnvironmentError, appOrigin, channelsEnabled, isPreview, proxyEnvironment, runtimeIdentityEnvironment, webhookSecret };
@@ -3,7 +3,8 @@ import type { RuntimeToken } from "./runtime-token.js";
3
3
  * The ontology service's pairing routes (`agent-design.md`, section 13, "Identity"). The agent
4
4
  * calls them as the deployment, with its OIDC token: it looks up which member a chat account is
5
5
  * paired with, and asks for a pairing code when it is paired with nobody. The member approves the
6
- * code on the app's `/agent/connect` page.
6
+ * code on the app's `/agent/connect` page. It also reports the group chats it is in to the
7
+ * ontology's `agent_chat` registry.
7
8
  */
8
9
  /** A member as the ontology resolves a paired chat account, with their current roles. */
9
10
  export interface PairedMember {
@@ -33,12 +34,32 @@ export interface ChatAccount {
33
34
  readonly provider: string;
34
35
  readonly externalId: string;
35
36
  }
37
+ /** A group chat in the ontology's `agent_chat` registry, with what members said it is for. */
38
+ export interface AgentChat {
39
+ readonly id: string;
40
+ readonly title: string;
41
+ readonly purpose: string | null;
42
+ readonly whenToUse: string | null;
43
+ }
36
44
  export interface AgentIdentities {
37
45
  lookup(account: ChatAccount): Promise<IdentityLookup>;
38
46
  /** Issues a pairing code, or returns the account's pending one while it has time left. */
39
47
  createPairing(account: ChatAccount & {
40
48
  readonly display?: string;
41
49
  }): Promise<Pairing>;
50
+ /**
51
+ * Records that the agent saw the group chat, and the paired member who wrote, if any, or the
52
+ * paired member who left it, whom the registry unlinks from the chat, or that the agent itself
53
+ * left it (`agentLeft`), which unlinks every member, or that the chat was the group
54
+ * `migratedFrom` before it became a supergroup, whose record the registry moves to the new id.
55
+ */
56
+ seenChat(chat: ChatAccount & {
57
+ readonly title: string;
58
+ readonly memberId?: string;
59
+ readonly leftMemberId?: string;
60
+ readonly agentLeft?: true;
61
+ readonly migratedFrom?: string;
62
+ }): Promise<AgentChat>;
42
63
  }
43
64
  /** The ontology service refused or failed a pairing call. Carries its error code, not its body. */
44
65
  export declare class AgentIdentityError extends Error {
@@ -54,6 +54,21 @@ function pairing(value) {
54
54
  expiresAt: value.expires_at
55
55
  };
56
56
  }
57
+ function nullableString(value) {
58
+ return value === null || typeof value === "string";
59
+ }
60
+ function agentChat(value) {
61
+ if (!isRecord(value) || typeof value.id !== "string" || typeof value.title !== "string" || !nullableString(value.purpose) || !nullableString(value.whenToUse)) throw new AgentIdentityError({
62
+ code: "invalid_response",
63
+ message: "The ontology service answered the chat report with an invalid body."
64
+ });
65
+ return {
66
+ id: value.id,
67
+ title: value.title,
68
+ purpose: value.purpose,
69
+ whenToUse: value.whenToUse
70
+ };
71
+ }
57
72
  async function refusal(response, call) {
58
73
  const status = response.status;
59
74
  let value;
@@ -127,6 +142,11 @@ function agentIdentities(input) {
127
142
  const created = pairing(await response.json());
128
143
  remember(pending, key, created);
129
144
  return created;
145
+ },
146
+ async seenChat(chat) {
147
+ const response = await call("/chats/seen", { body: chat });
148
+ if (!response.ok) throw await refusal(response, "chat report");
149
+ return agentChat(await response.json());
130
150
  }
131
151
  };
132
152
  }
@@ -0,0 +1,22 @@
1
+ import { type JWTVerifyGetKey } from "jose";
2
+ import type { RuntimeIdentityEnvironment } from "./environment.js";
3
+ export type RuntimeVerification = {
4
+ readonly status: "valid";
5
+ }
6
+ /** `reason` names what failed, a jose code or a claim name, never a value. */
7
+ | {
8
+ readonly status: "invalid";
9
+ readonly reason: string;
10
+ }
11
+ /**
12
+ * The key set could not be read: not the token's fault. `reason` is the jose code, or the error's
13
+ * name and message from fetching the key set, never the token.
14
+ */
15
+ | {
16
+ readonly status: "unavailable";
17
+ readonly reason: string;
18
+ };
19
+ export type RuntimeTokenVerifier = (token: string) => Promise<RuntimeVerification>;
20
+ /** The issuer's key set, at `<issuer>/.well-known/jwks` as Vercel publishes it; jose caches it. */
21
+ export declare function runtimeKeySet(issuer: string): JWTVerifyGetKey;
22
+ export declare function runtimeTokenVerifier(config: RuntimeIdentityEnvironment, keySet?: JWTVerifyGetKey): RuntimeTokenVerifier;
@@ -0,0 +1,62 @@
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 { createRemoteJWKSet, errors, jwtVerify } from "jose";
8
+ const CLOCK_TOLERANCE_SECONDS = 5;
9
+ const JWKS_TIMEOUT_MS = 5e3;
10
+ function runtimeKeySet(issuer) {
11
+ return createRemoteJWKSet(new URL(`${issuer}/.well-known/jwks`), { timeoutDuration: JWKS_TIMEOUT_MS });
12
+ }
13
+ function keySetUnavailable(error) {
14
+ if (!(error instanceof errors.JOSEError)) return true;
15
+ return error instanceof errors.JWKSTimeout || error instanceof errors.JWKSInvalid || error.code === errors.JOSEError.code;
16
+ }
17
+ function unavailableReason(error) {
18
+ if (error instanceof errors.JOSEError) return error.code;
19
+ return error instanceof Error ? `${error.name}: ${error.message}` : "unknown";
20
+ }
21
+ function failureReason(error) {
22
+ if (error instanceof errors.JWTClaimValidationFailed || error instanceof errors.JWTExpired) return `claim:${error.claim}`;
23
+ return error instanceof errors.JOSEError ? error.code : "invalid";
24
+ }
25
+ function runtimeTokenVerifier(config, keySet = runtimeKeySet(config.issuer)) {
26
+ return async (token) => {
27
+ try {
28
+ const { payload } = await jwtVerify(token, keySet, {
29
+ algorithms: ["RS256"],
30
+ issuer: config.issuer,
31
+ audience: config.audience,
32
+ clockTolerance: CLOCK_TOLERANCE_SECONDS,
33
+ requiredClaims: [
34
+ "exp",
35
+ "iat",
36
+ "iss",
37
+ "aud",
38
+ "project_id",
39
+ "environment"
40
+ ]
41
+ });
42
+ if (payload.project_id !== config.projectId) return {
43
+ status: "invalid",
44
+ reason: "claim:project_id"
45
+ };
46
+ if (payload.environment !== "production") return {
47
+ status: "invalid",
48
+ reason: "claim:environment"
49
+ };
50
+ return { status: "valid" };
51
+ } catch (error) {
52
+ return keySetUnavailable(error) ? {
53
+ status: "unavailable",
54
+ reason: unavailableReason(error)
55
+ } : {
56
+ status: "invalid",
57
+ reason: failureReason(error)
58
+ };
59
+ }
60
+ };
61
+ }
62
+ export { runtimeKeySet, runtimeTokenVerifier };
@@ -1,6 +1,8 @@
1
1
  import { type TelegramChannel } from "eve/channels/telegram";
2
2
  import type { SessionSettings, TelegramSettings } from "../../config.js";
3
+ import { type AgentAttachments } from "../attachments.js";
3
4
  import { type Environment } from "../environment.js";
5
+ import { type RuntimeTokenVerifier } from "../runtime-identity.js";
4
6
  import { type RuntimeToken } from "../runtime-token.js";
5
7
  import { type ObservedMessages } from "./observed.js";
6
8
  import { type DecideFunction } from "./router.js";
@@ -27,8 +29,12 @@ export interface TelegramAdapterOptions {
27
29
  readonly fetch?: typeof fetch;
28
30
  readonly transcribe?: Transcriber;
29
31
  readonly observed?: ObservedMessages;
32
+ /** Verifies callers of the messages route; tests pass one on a local key set. */
33
+ readonly verify?: RuntimeTokenVerifier;
30
34
  /** Reads the tail of a session's durable stream; tests pass a fake. */
31
35
  readonly history?: HistoryReader;
36
+ /** The attachment store; tests pass a fake. */
37
+ readonly attachments?: AgentAttachments;
32
38
  }
33
39
  /** The Telegram channel of the extension. */
34
40
  export declare function segmentstreamTelegram(options: TelegramAdapterOptions): TelegramChannel;
@@ -4,11 +4,14 @@ import { createRequire as __eveCreateRequire } from "node:module";
4
4
  const __filename = __eveFileURLToPath(import.meta.url);
5
5
  __eveDirname(__filename);
6
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";
7
+ import { appOrigin, channelsEnabled, proxyEnvironment, runtimeIdentityEnvironment, webhookSecret } from "../environment.mjs";
8
+ import { MAXIMUM_DOWNLOAD_BYTES, connectionProxy } from "../connection-proxy.mjs";
10
9
  import { runtimeToken } from "../runtime-token.mjs";
10
+ import { agentAttachments, parseAttachmentUrl } from "../attachments.mjs";
11
+ import { agentIdentities } from "../identities.mjs";
12
+ import { runtimeTokenVerifier } from "../runtime-identity.mjs";
11
13
  import { botIdentityCache } from "./bot.mjs";
14
+ import { agentChats } from "./chats.mjs";
12
15
  import { observedMessages } from "./observed.mjs";
13
16
  import { decisionRouter } from "./router.mjs";
14
17
  import { scopedAddress, sessionPolicy } from "./sessions.mjs";
@@ -16,6 +19,7 @@ import { downloadTelegramFile, gatewayTranscriber } from "./voice.mjs";
16
19
  import { handleInboundMessage } from "./inbound.mjs";
17
20
  import { InboundRequest, currentInboundRequest, runInboundRequest, withReplacedText } from "./inbound-request.mjs";
18
21
  import { TELEGRAM_PLACEHOLDER_TOKEN, TELEGRAM_PROXY_ORIGIN, telegramProxyFetch } from "./proxy-fetch.mjs";
22
+ import { messagesRoute } from "./requests.mjs";
19
23
  import { deliverUnlessSilent } from "./silent.mjs";
20
24
  import { telegramWebhookVerifier } from "./webhook.mjs";
21
25
  import { telegramChannel } from "eve/channels/telegram";
@@ -42,6 +46,7 @@ function segmentstreamTelegram(options) {
42
46
  const bot = botIdentityCache();
43
47
  const proxies = new Map();
44
48
  let identities;
49
+ let verify = options.verify;
45
50
  function settings() {
46
51
  const configured = options.settings();
47
52
  if (configured === null) throw new Error("The Telegram channel is not configured in agent/segmentstream.json.");
@@ -69,6 +74,16 @@ function segmentstreamTelegram(options) {
69
74
  });
70
75
  return identities;
71
76
  }
77
+ const chats = agentChats({ identities: pairing });
78
+ let store = options.attachments;
79
+ function attachments() {
80
+ store ??= agentAttachments({
81
+ origin: appOrigin(environment),
82
+ token,
83
+ ...options.fetch === void 0 ? {} : { fetch: options.fetch }
84
+ });
85
+ return store;
86
+ }
72
87
  const channel = telegramChannel({
73
88
  api: {
74
89
  apiBaseUrl: TELEGRAM_PROXY_ORIGIN,
@@ -79,7 +94,7 @@ function segmentstreamTelegram(options) {
79
94
  botToken: TELEGRAM_PLACEHOLDER_TOKEN,
80
95
  webhookVerifier: telegramWebhookVerifier(() => webhookSecret(environment))
81
96
  },
82
- uploadPolicy: { maxBytes: MAXIMUM_PROXY_BODY_BYTES },
97
+ uploadPolicy: { maxBytes: MAXIMUM_DOWNLOAD_BYTES },
83
98
  onMessage: (context, message) => handleInboundMessage({
84
99
  context,
85
100
  message,
@@ -100,21 +115,55 @@ function segmentstreamTelegram(options) {
100
115
  proxy: proxy(),
101
116
  fileId
102
117
  }),
118
+ attachments: attachments(),
103
119
  observed,
120
+ chats,
104
121
  connectUrl: (code) => `${appOrigin(environment)}/agent/connect?code=${encodeURIComponent(code)}`
105
122
  }
106
123
  }),
107
124
  events: { "message.completed": deliverUnlessSilent }
108
125
  });
126
+ const adapter = channel.adapter;
127
+ const fetchTelegramFile = adapter?.fetchFile;
128
+ async function fetchFile(url, context) {
129
+ const reference = parseAttachmentUrl(url);
130
+ if (reference === void 0) return fetchTelegramFile === void 0 ? null : fetchTelegramFile(url, context);
131
+ const sessionId = sessionIdOf(context);
132
+ const { attachment, bytes } = await attachments().read({
133
+ memberId: reference.memberId,
134
+ conversation: reference.conversation,
135
+ ...sessionId === void 0 ? {} : { sessionId }
136
+ }, reference.id);
137
+ return {
138
+ bytes: Buffer.from(bytes),
139
+ mediaType: attachment.contentType,
140
+ filename: attachment.filename
141
+ };
142
+ }
109
143
  return {
110
144
  ...channel,
111
- routes: channel.routes.map((route) => gatedRoute({
145
+ ...adapter === void 0 ? {} : { adapter: {
146
+ ...adapter,
147
+ fetchFile
148
+ } },
149
+ routes: [...channel.routes.map((route) => gatedRoute({
112
150
  route,
113
151
  options,
114
152
  environment
115
- }))
153
+ })), messagesRoute({
154
+ channel,
155
+ enabled: () => channelsEnabled(environment),
156
+ configured: () => options.settings() !== null,
157
+ verify: () => verify ??= runtimeTokenVerifier(runtimeIdentityEnvironment(environment)),
158
+ sessionPolicy: policy,
159
+ scope: () => options.sessions().scope
160
+ })]
116
161
  };
117
162
  }
163
+ function sessionIdOf(context) {
164
+ const session = typeof context === "object" && context !== null ? context.session : void 0;
165
+ return typeof session?.id === "string" && session.id !== "" ? session.id : void 0;
166
+ }
118
167
  function gatedRoute(input) {
119
168
  const { route, options, environment } = input;
120
169
  if (route.transport === "websocket") return route;
@@ -129,6 +178,7 @@ function gatedRoute(input) {
129
178
  resolveSession: args.resolveSession,
130
179
  from: args.from
131
180
  };
181
+ inbound.waitUntil = args.waitUntil;
132
182
  return runInboundRequest(inbound, () => route.handler(request, {
133
183
  ...args,
134
184
  from: (address) => withReplacedText(args.from(scoped(address)), inbound)
@@ -0,0 +1,46 @@
1
+ import type { AgentChat, AgentIdentities } from "../identities.js";
2
+ /** How long a chat's report stands before the next message reports it again. */
3
+ export declare const REPORT_INTERVAL_MS: number;
4
+ export interface ChatReport {
5
+ readonly chatId: string;
6
+ readonly title: string;
7
+ readonly memberId?: string;
8
+ }
9
+ export interface AgentChats {
10
+ /** Reports the chat when due. Resolves when done; never rejects. */
11
+ seen(report: ChatReport): Promise<void>;
12
+ /**
13
+ * Reports that the paired member left the chat, so the registry unlinks them, and forgets them
14
+ * here, so that a later message from them links them again. Resolves when done; never rejects.
15
+ */
16
+ left(report: ChatReport & {
17
+ readonly memberId: string;
18
+ }): Promise<void>;
19
+ /**
20
+ * Reports that the agent itself left the chat, so the registry unlinks every member, and forgets
21
+ * the members reported here, so that each links again when they write after the agent is back.
22
+ * Resolves when done; never rejects.
23
+ */
24
+ agentLeft(report: {
25
+ readonly chatId: string;
26
+ readonly title: string;
27
+ }): Promise<void>;
28
+ /**
29
+ * Reports that the group `migratedFrom` became the supergroup `chatId`, so the registry moves its
30
+ * record, and forgets the old chat here. Resolves when done; never rejects.
31
+ */
32
+ migrated(report: {
33
+ readonly chatId: string;
34
+ readonly title: string;
35
+ readonly migratedFrom: string;
36
+ }): Promise<void>;
37
+ /** The registry's latest answer for the chat, if this process has one. */
38
+ chat(chatId: string): AgentChat | undefined;
39
+ }
40
+ export declare function agentChats(input: {
41
+ readonly identities: () => AgentIdentities;
42
+ readonly now?: () => number;
43
+ readonly log?: (message: string, error: unknown) => void;
44
+ }): AgentChats;
45
+ /** The registry's facts as turn context: only the fields it holds. */
46
+ export declare function chatContext(chat: AgentChat | undefined): ReadonlyArray<string>;