roboport 0.0.1 → 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.
- package/README.md +3 -2
- package/core/agent.d.ts +2 -1
- package/gateways/core.d.ts +27 -0
- package/gateways/index.d.ts +5 -0
- package/gateways/index.js +575 -0
- package/gateways/serve.d.ts +19 -0
- package/gateways/sources/telegram.d.ts +28 -0
- package/gateways/store.d.ts +9 -0
- package/harness/index.js +15 -6
- package/index.js +15 -6
- package/mcp/auth.d.ts +2 -0
- package/mcp/index.d.ts +4 -1
- package/mcp/index.js +814 -795
- package/models/index.js +15 -6
- package/package.json +9 -5
- package/skills/index.js +15 -6
- package/triggers/core.d.ts +1 -1
- package/triggers/index.js +10 -2
- package/triggers/sources/telegram.d.ts +9 -1
package/README.md
CHANGED
|
@@ -8,8 +8,9 @@ Minimal TypeScript framework for building LLM agents.
|
|
|
8
8
|
bun add roboport zod
|
|
9
9
|
```
|
|
10
10
|
|
|
11
|
-
`
|
|
12
|
-
|
|
11
|
+
`roboport` is built for and tested on [Bun](https://bun.sh) and uses Bun-native
|
|
12
|
+
APIs. `zod` is a required peer dependency — it is imported at runtime, so
|
|
13
|
+
install it alongside the package.
|
|
13
14
|
|
|
14
15
|
## Exports
|
|
15
16
|
|
package/core/agent.d.ts
CHANGED
|
@@ -25,11 +25,12 @@ declare class Agent {
|
|
|
25
25
|
on<T>(trigger: Trigger<T>, handler: TriggerHandler<T>): void;
|
|
26
26
|
start(): Promise<void>;
|
|
27
27
|
stop(): Promise<void>;
|
|
28
|
-
buildSystem(allTools: Tool[]): string;
|
|
28
|
+
buildSystem(allTools: Tool[], systemExtension?: string): string;
|
|
29
29
|
private buildSkillTool;
|
|
30
30
|
session(init?: {
|
|
31
31
|
messages?: Message[];
|
|
32
32
|
cwd?: string;
|
|
33
|
+
systemExtension?: string;
|
|
33
34
|
}): Session;
|
|
34
35
|
}
|
|
35
36
|
export { Agent };
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import type { Turn } from '../core';
|
|
2
|
+
import type { MaybePromise, Unsub } from '../triggers/core';
|
|
3
|
+
interface InboundMessage {
|
|
4
|
+
id: string;
|
|
5
|
+
conversationId: string;
|
|
6
|
+
text: string;
|
|
7
|
+
user?: {
|
|
8
|
+
id: string;
|
|
9
|
+
name?: string;
|
|
10
|
+
};
|
|
11
|
+
replyToId?: string;
|
|
12
|
+
raw?: unknown;
|
|
13
|
+
}
|
|
14
|
+
interface Channel {
|
|
15
|
+
conversationId: string;
|
|
16
|
+
send(text: string): Promise<void>;
|
|
17
|
+
thinking?(): () => void;
|
|
18
|
+
}
|
|
19
|
+
type GatewayHandler<In extends InboundMessage, Ch extends Channel> = (message: In, channel: Ch) => MaybePromise<void>;
|
|
20
|
+
type Relay<In extends InboundMessage, Ch extends Channel> = (turn: Turn, channel: Ch, message: In) => Promise<void>;
|
|
21
|
+
interface Gateway<In extends InboundMessage = InboundMessage, Ch extends Channel = Channel> {
|
|
22
|
+
name: string;
|
|
23
|
+
open(handler: GatewayHandler<In, Ch>): MaybePromise<Unsub>;
|
|
24
|
+
handle?(req: Request): Promise<Response>;
|
|
25
|
+
relay?: Relay<In, Ch>;
|
|
26
|
+
}
|
|
27
|
+
export type { Channel, Gateway, GatewayHandler, InboundMessage, Relay };
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
import { type Channel, type Gateway, type GatewayHandler, type InboundMessage, type Relay } from './core';
|
|
2
|
+
import { serve, type GatewayRuntime, type ServeOptions } from './serve';
|
|
3
|
+
import { stream, telegramGateway, type TelegramChannel, type TelegramGateway, type TelegramGatewayOptions, type TelegramTransport } from './sources/telegram';
|
|
4
|
+
import { fileStore, memoryStore, type ConversationStore } from './store';
|
|
5
|
+
export { fileStore, memoryStore, serve, stream, telegramGateway, type Channel, type ConversationStore, type Gateway, type GatewayHandler, type GatewayRuntime, type InboundMessage, type Relay, type ServeOptions, type TelegramChannel, type TelegramGateway, type TelegramGatewayOptions, type TelegramTransport, };
|
|
@@ -0,0 +1,575 @@
|
|
|
1
|
+
// @bun
|
|
2
|
+
// src/gateways/store.ts
|
|
3
|
+
import { createHash } from "crypto";
|
|
4
|
+
import { appendFile, mkdir, readFile } from "fs/promises";
|
|
5
|
+
import { join } from "path";
|
|
6
|
+
function memoryStore() {
|
|
7
|
+
const byId = new Map;
|
|
8
|
+
return {
|
|
9
|
+
load(id) {
|
|
10
|
+
const existing = byId.get(id);
|
|
11
|
+
return existing ? [...existing] : null;
|
|
12
|
+
},
|
|
13
|
+
append(id, ...messages) {
|
|
14
|
+
const existing = byId.get(id);
|
|
15
|
+
if (existing)
|
|
16
|
+
existing.push(...messages);
|
|
17
|
+
else
|
|
18
|
+
byId.set(id, [...messages]);
|
|
19
|
+
}
|
|
20
|
+
};
|
|
21
|
+
}
|
|
22
|
+
function fileNameFor(id) {
|
|
23
|
+
const prefix = id.replace(/[^a-zA-Z0-9_-]/g, "_").slice(0, 64);
|
|
24
|
+
const hash = createHash("sha256").update(id).digest("hex").slice(0, 16);
|
|
25
|
+
return `${prefix}-${hash}.jsonl`;
|
|
26
|
+
}
|
|
27
|
+
function fileStore(dir) {
|
|
28
|
+
function fileFor(id) {
|
|
29
|
+
return join(dir, fileNameFor(id));
|
|
30
|
+
}
|
|
31
|
+
return {
|
|
32
|
+
async load(id) {
|
|
33
|
+
let raw;
|
|
34
|
+
try {
|
|
35
|
+
raw = await readFile(fileFor(id), "utf8");
|
|
36
|
+
} catch {
|
|
37
|
+
return null;
|
|
38
|
+
}
|
|
39
|
+
const messages = [];
|
|
40
|
+
for (const line of raw.split(`
|
|
41
|
+
`)) {
|
|
42
|
+
if (!line.trim())
|
|
43
|
+
continue;
|
|
44
|
+
try {
|
|
45
|
+
messages.push(JSON.parse(line));
|
|
46
|
+
} catch {}
|
|
47
|
+
}
|
|
48
|
+
return messages;
|
|
49
|
+
},
|
|
50
|
+
async append(id, ...messages) {
|
|
51
|
+
if (messages.length === 0)
|
|
52
|
+
return;
|
|
53
|
+
await mkdir(dir, { recursive: true });
|
|
54
|
+
const lines = messages.map((message) => JSON.stringify(message)).join(`
|
|
55
|
+
`);
|
|
56
|
+
await appendFile(fileFor(id), `${lines}
|
|
57
|
+
`, "utf8");
|
|
58
|
+
}
|
|
59
|
+
};
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
// src/gateways/serve.ts
|
|
63
|
+
function toUserMessage(prompt) {
|
|
64
|
+
return { role: "user", content: prompt };
|
|
65
|
+
}
|
|
66
|
+
function newMessages(session, seedLength) {
|
|
67
|
+
const all = session.messages;
|
|
68
|
+
const head = all[0]?.role === "system" ? 1 : 0;
|
|
69
|
+
return [...all.slice(head + seedLength + 1)];
|
|
70
|
+
}
|
|
71
|
+
async function bufferReplies(turn, channel) {
|
|
72
|
+
const blocks = [];
|
|
73
|
+
let failure = null;
|
|
74
|
+
for await (const event of turn) {
|
|
75
|
+
if (event.type === "text")
|
|
76
|
+
blocks.push(event.text);
|
|
77
|
+
else if (event.type === "error")
|
|
78
|
+
failure = event.error;
|
|
79
|
+
}
|
|
80
|
+
const reply = blocks.join(`
|
|
81
|
+
|
|
82
|
+
`).trim();
|
|
83
|
+
if (failure && !reply)
|
|
84
|
+
throw failure;
|
|
85
|
+
await channel.send(reply || "(no response)");
|
|
86
|
+
}
|
|
87
|
+
async function defaultError(error, channel) {
|
|
88
|
+
console.error("[gateways] turn failed:", error);
|
|
89
|
+
await channel.send("Sorry \u2014 something went wrong.").catch(() => {});
|
|
90
|
+
}
|
|
91
|
+
function serve(agent, gateway, options = {}) {
|
|
92
|
+
const store = options.store ?? memoryStore();
|
|
93
|
+
const keyOf = options.conversation ?? ((message) => message.conversationId);
|
|
94
|
+
const relay = options.relay ?? gateway.relay ?? bufferReplies;
|
|
95
|
+
const queues = new Map;
|
|
96
|
+
async function runTurn(message, channel, id) {
|
|
97
|
+
let stopThinking;
|
|
98
|
+
try {
|
|
99
|
+
if (options.authorize && !await options.authorize(message, channel)) {
|
|
100
|
+
return;
|
|
101
|
+
}
|
|
102
|
+
const promptValue = options.prompt ? options.prompt(message) : message.text;
|
|
103
|
+
if (promptValue === null)
|
|
104
|
+
return;
|
|
105
|
+
const stored = await store.load(id) ?? [];
|
|
106
|
+
const seed = options.context ? await options.context(stored, message) : stored;
|
|
107
|
+
const seedLength = seed.length;
|
|
108
|
+
const systemExtension = options.systemExtension ? await options.systemExtension(message) : undefined;
|
|
109
|
+
const session = agent.session({ messages: seed, systemExtension });
|
|
110
|
+
await store.append(id, toUserMessage(promptValue));
|
|
111
|
+
stopThinking = channel.thinking?.();
|
|
112
|
+
try {
|
|
113
|
+
const turn = session.send(promptValue);
|
|
114
|
+
await relay(turn, channel, message);
|
|
115
|
+
stopThinking?.();
|
|
116
|
+
stopThinking = undefined;
|
|
117
|
+
await store.append(id, ...newMessages(session, seedLength));
|
|
118
|
+
} finally {
|
|
119
|
+
await session.close();
|
|
120
|
+
}
|
|
121
|
+
} catch (error) {
|
|
122
|
+
stopThinking?.();
|
|
123
|
+
const err = error instanceof Error ? error : new Error(String(error));
|
|
124
|
+
if (options.onError)
|
|
125
|
+
await options.onError(err, channel, message);
|
|
126
|
+
else
|
|
127
|
+
await defaultError(err, channel);
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
function handler(message, channel) {
|
|
131
|
+
const id = keyOf(message);
|
|
132
|
+
const prior = queues.get(id) ?? Promise.resolve();
|
|
133
|
+
const next = prior.then(() => runTurn(message, channel, id)).catch((error) => {
|
|
134
|
+
console.error(`[gateways] ${gateway.name} ${id}:`, error);
|
|
135
|
+
});
|
|
136
|
+
queues.set(id, next);
|
|
137
|
+
next.finally(() => {
|
|
138
|
+
if (queues.get(id) === next)
|
|
139
|
+
queues.delete(id);
|
|
140
|
+
});
|
|
141
|
+
}
|
|
142
|
+
const opened = Promise.resolve(gateway.open(handler));
|
|
143
|
+
opened.catch((error) => {
|
|
144
|
+
console.error(`[gateways] ${gateway.name} failed to open:`, error);
|
|
145
|
+
});
|
|
146
|
+
function notWebhook() {
|
|
147
|
+
return Promise.resolve(new Response("gateway is not in webhook mode", { status: 404 }));
|
|
148
|
+
}
|
|
149
|
+
return {
|
|
150
|
+
async stop() {
|
|
151
|
+
const unsub = await opened.catch(() => {
|
|
152
|
+
return;
|
|
153
|
+
});
|
|
154
|
+
if (unsub)
|
|
155
|
+
await unsub();
|
|
156
|
+
},
|
|
157
|
+
handle: gateway.handle ? (req) => gateway.handle(req) : notWebhook
|
|
158
|
+
};
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
// src/triggers/bus.ts
|
|
162
|
+
function makeBus() {
|
|
163
|
+
return { subs: new Set };
|
|
164
|
+
}
|
|
165
|
+
function subscribe(bus, emit, filter) {
|
|
166
|
+
const wrapped = filter ? (event) => {
|
|
167
|
+
if (filter(event))
|
|
168
|
+
emit(event);
|
|
169
|
+
} : emit;
|
|
170
|
+
bus.subs.add(wrapped);
|
|
171
|
+
return () => {
|
|
172
|
+
bus.subs.delete(wrapped);
|
|
173
|
+
};
|
|
174
|
+
}
|
|
175
|
+
function dispatch(bus, event) {
|
|
176
|
+
for (const sub of bus.subs)
|
|
177
|
+
sub(event);
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
// src/triggers/sources/telegram.ts
|
|
181
|
+
var DEFAULT_UPDATE_CACHE_SIZE = 1024;
|
|
182
|
+
var TELEGRAM_API_BASE = "https://api.telegram.org";
|
|
183
|
+
var MAX_MESSAGE_LENGTH = 4096;
|
|
184
|
+
function timingSafeEqual(a, b) {
|
|
185
|
+
if (a.length !== b.length)
|
|
186
|
+
return false;
|
|
187
|
+
let diff = 0;
|
|
188
|
+
for (let i = 0;i < a.length; i++) {
|
|
189
|
+
diff |= a.charCodeAt(i) ^ b.charCodeAt(i);
|
|
190
|
+
}
|
|
191
|
+
return diff === 0;
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
class UpdateCache {
|
|
195
|
+
seen = new Set;
|
|
196
|
+
order = [];
|
|
197
|
+
maxSize;
|
|
198
|
+
constructor(maxSize) {
|
|
199
|
+
this.maxSize = maxSize;
|
|
200
|
+
}
|
|
201
|
+
has(id) {
|
|
202
|
+
return this.seen.has(id);
|
|
203
|
+
}
|
|
204
|
+
add(id) {
|
|
205
|
+
if (this.seen.has(id))
|
|
206
|
+
return;
|
|
207
|
+
this.seen.add(id);
|
|
208
|
+
this.order.push(id);
|
|
209
|
+
while (this.order.length > this.maxSize) {
|
|
210
|
+
const dropped = this.order.shift();
|
|
211
|
+
if (dropped !== undefined)
|
|
212
|
+
this.seen.delete(dropped);
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
function matchesCommand(message, commands, botUsername) {
|
|
217
|
+
const text = message.text;
|
|
218
|
+
if (!text || !text.startsWith("/"))
|
|
219
|
+
return false;
|
|
220
|
+
const token = text.slice(1).split(/\s/, 1)[0] ?? "";
|
|
221
|
+
const [name, target] = token.split("@");
|
|
222
|
+
if (target && botUsername) {
|
|
223
|
+
const normalized = botUsername.replace(/^@/, "").toLowerCase();
|
|
224
|
+
if (target.toLowerCase() !== normalized)
|
|
225
|
+
return false;
|
|
226
|
+
}
|
|
227
|
+
return commands.some((command) => (command.startsWith("/") ? command.slice(1) : command) === name);
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
class TelegramReceiver {
|
|
231
|
+
messageBus = makeBus();
|
|
232
|
+
editedMessageBus = makeBus();
|
|
233
|
+
secretToken;
|
|
234
|
+
updates;
|
|
235
|
+
constructor(options) {
|
|
236
|
+
if (!options.secretToken) {
|
|
237
|
+
throw new Error("TelegramReceiver requires a non-empty secretToken");
|
|
238
|
+
}
|
|
239
|
+
this.secretToken = options.secretToken;
|
|
240
|
+
this.updates = new UpdateCache(options.updateCacheSize ?? DEFAULT_UPDATE_CACHE_SIZE);
|
|
241
|
+
}
|
|
242
|
+
message(opts) {
|
|
243
|
+
const bus = this.messageBus;
|
|
244
|
+
const commands = opts?.commands;
|
|
245
|
+
const botUsername = opts?.botUsername;
|
|
246
|
+
return {
|
|
247
|
+
name: "telegram:message",
|
|
248
|
+
start: (emit) => subscribe(bus, emit, commands ? (m) => matchesCommand(m, commands, botUsername) : undefined)
|
|
249
|
+
};
|
|
250
|
+
}
|
|
251
|
+
editedMessage() {
|
|
252
|
+
const bus = this.editedMessageBus;
|
|
253
|
+
return {
|
|
254
|
+
name: "telegram:edited_message",
|
|
255
|
+
start: (emit) => subscribe(bus, emit)
|
|
256
|
+
};
|
|
257
|
+
}
|
|
258
|
+
handle = async (req) => {
|
|
259
|
+
const provided = req.headers.get("x-telegram-bot-api-secret-token");
|
|
260
|
+
if (!provided || !timingSafeEqual(provided, this.secretToken)) {
|
|
261
|
+
return new Response("invalid secret token", { status: 401 });
|
|
262
|
+
}
|
|
263
|
+
let update;
|
|
264
|
+
try {
|
|
265
|
+
update = await req.json();
|
|
266
|
+
} catch {
|
|
267
|
+
return new Response("invalid json", { status: 400 });
|
|
268
|
+
}
|
|
269
|
+
if (typeof update.update_id !== "number") {
|
|
270
|
+
return new Response("missing update_id", { status: 400 });
|
|
271
|
+
}
|
|
272
|
+
if (this.updates.has(update.update_id)) {
|
|
273
|
+
return new Response("duplicate", { status: 200 });
|
|
274
|
+
}
|
|
275
|
+
if (update.message) {
|
|
276
|
+
dispatch(this.messageBus, update.message);
|
|
277
|
+
} else if (update.edited_message) {
|
|
278
|
+
dispatch(this.editedMessageBus, update.edited_message);
|
|
279
|
+
}
|
|
280
|
+
this.updates.add(update.update_id);
|
|
281
|
+
return new Response("ok", { status: 200 });
|
|
282
|
+
};
|
|
283
|
+
}
|
|
284
|
+
function splitMessage(text, max = MAX_MESSAGE_LENGTH) {
|
|
285
|
+
if (max < 1)
|
|
286
|
+
throw new Error("splitMessage requires max >= 1");
|
|
287
|
+
if (text.length <= max)
|
|
288
|
+
return [text];
|
|
289
|
+
const chunks = [];
|
|
290
|
+
let remaining = text;
|
|
291
|
+
while (remaining.length > max) {
|
|
292
|
+
let cut = remaining.lastIndexOf(`
|
|
293
|
+
`, max);
|
|
294
|
+
if (cut <= 0) {
|
|
295
|
+
cut = max;
|
|
296
|
+
const code = remaining.charCodeAt(cut - 1);
|
|
297
|
+
if (code >= 55296 && code <= 56319)
|
|
298
|
+
cut -= 1;
|
|
299
|
+
}
|
|
300
|
+
chunks.push(remaining.slice(0, cut));
|
|
301
|
+
remaining = remaining.slice(cut).replace(/^\n/, "");
|
|
302
|
+
}
|
|
303
|
+
if (remaining.length > 0)
|
|
304
|
+
chunks.push(remaining);
|
|
305
|
+
return chunks;
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
class TelegramClient {
|
|
309
|
+
token;
|
|
310
|
+
baseUrl;
|
|
311
|
+
constructor(token, opts) {
|
|
312
|
+
if (!token)
|
|
313
|
+
throw new Error("TelegramClient requires a bot token");
|
|
314
|
+
this.token = token;
|
|
315
|
+
this.baseUrl = (opts?.baseUrl ?? TELEGRAM_API_BASE).replace(/\/+$/, "");
|
|
316
|
+
}
|
|
317
|
+
async call(method, params, signal) {
|
|
318
|
+
const response = await fetch(`${this.baseUrl}/bot${this.token}/${method}`, {
|
|
319
|
+
method: "POST",
|
|
320
|
+
headers: { "content-type": "application/json" },
|
|
321
|
+
body: JSON.stringify(params),
|
|
322
|
+
...signal ? { signal } : {}
|
|
323
|
+
});
|
|
324
|
+
const data = await response.json();
|
|
325
|
+
if (!data.ok) {
|
|
326
|
+
throw new Error(`Telegram ${method} failed (${response.status}): ${data.description ?? "unknown error"}`);
|
|
327
|
+
}
|
|
328
|
+
return data.result;
|
|
329
|
+
}
|
|
330
|
+
getMe() {
|
|
331
|
+
return this.call("getMe", {});
|
|
332
|
+
}
|
|
333
|
+
sendChatAction(chatId, action = "typing") {
|
|
334
|
+
return this.call("sendChatAction", { chat_id: chatId, action });
|
|
335
|
+
}
|
|
336
|
+
async sendMessage(chatId, text, opts) {
|
|
337
|
+
if (opts?.parseMode && text.length > MAX_MESSAGE_LENGTH) {
|
|
338
|
+
throw new Error(`sendMessage: text exceeds ${MAX_MESSAGE_LENGTH} chars with parse_mode ${opts.parseMode}; auto-splitting could break entities. Split it yourself.`);
|
|
339
|
+
}
|
|
340
|
+
const sent = [];
|
|
341
|
+
for (const chunk of splitMessage(text)) {
|
|
342
|
+
sent.push(await this.call("sendMessage", {
|
|
343
|
+
chat_id: chatId,
|
|
344
|
+
text: chunk,
|
|
345
|
+
...opts?.parseMode ? { parse_mode: opts.parseMode } : {},
|
|
346
|
+
...opts?.messageThreadId !== undefined ? { message_thread_id: opts.messageThreadId } : {},
|
|
347
|
+
...opts?.replyToMessageId ? { reply_parameters: { message_id: opts.replyToMessageId } } : {},
|
|
348
|
+
...opts?.disableNotification ? { disable_notification: true } : {},
|
|
349
|
+
...opts?.linkPreview === false ? { link_preview_options: { is_disabled: true } } : {}
|
|
350
|
+
}));
|
|
351
|
+
}
|
|
352
|
+
return sent;
|
|
353
|
+
}
|
|
354
|
+
editMessageText(chatId, messageId, text, opts) {
|
|
355
|
+
return this.call("editMessageText", {
|
|
356
|
+
chat_id: chatId,
|
|
357
|
+
message_id: messageId,
|
|
358
|
+
text,
|
|
359
|
+
...opts?.parseMode ? { parse_mode: opts.parseMode } : {}
|
|
360
|
+
});
|
|
361
|
+
}
|
|
362
|
+
async sendMessageDraft(chatId, draftId, text, opts) {
|
|
363
|
+
if (!Number.isInteger(draftId) || draftId === 0) {
|
|
364
|
+
throw new Error("sendMessageDraft: draftId must be a non-zero integer");
|
|
365
|
+
}
|
|
366
|
+
if (text.length > MAX_MESSAGE_LENGTH) {
|
|
367
|
+
throw new Error(`sendMessageDraft: text exceeds ${MAX_MESSAGE_LENGTH} chars; a draft is a single bubble and can't be split. Truncate it and persist the full reply via sendMessage.`);
|
|
368
|
+
}
|
|
369
|
+
return this.call("sendMessageDraft", {
|
|
370
|
+
chat_id: chatId,
|
|
371
|
+
draft_id: draftId,
|
|
372
|
+
text,
|
|
373
|
+
...opts?.parseMode ? { parse_mode: opts.parseMode } : {},
|
|
374
|
+
...opts?.messageThreadId !== undefined ? { message_thread_id: opts.messageThreadId } : {}
|
|
375
|
+
});
|
|
376
|
+
}
|
|
377
|
+
setWebhook(url, opts) {
|
|
378
|
+
return this.call("setWebhook", {
|
|
379
|
+
url,
|
|
380
|
+
...opts?.secretToken ? { secret_token: opts.secretToken } : {},
|
|
381
|
+
...opts?.allowedUpdates ? { allowed_updates: opts.allowedUpdates } : {}
|
|
382
|
+
});
|
|
383
|
+
}
|
|
384
|
+
deleteWebhook(opts) {
|
|
385
|
+
return this.call("deleteWebhook", {
|
|
386
|
+
...opts?.dropPendingUpdates ? { drop_pending_updates: true } : {}
|
|
387
|
+
});
|
|
388
|
+
}
|
|
389
|
+
getUpdates(opts) {
|
|
390
|
+
return this.call("getUpdates", {
|
|
391
|
+
...opts?.offset !== undefined ? { offset: opts.offset } : {},
|
|
392
|
+
...opts?.timeout !== undefined ? { timeout: opts.timeout } : {},
|
|
393
|
+
...opts?.allowedUpdates ? { allowed_updates: opts.allowedUpdates } : {}
|
|
394
|
+
}, opts?.signal);
|
|
395
|
+
}
|
|
396
|
+
}
|
|
397
|
+
function telegram(options) {
|
|
398
|
+
return new TelegramReceiver(options);
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
// src/gateways/sources/telegram.ts
|
|
402
|
+
var TYPING_INTERVAL_MS = 4000;
|
|
403
|
+
var POLL_TIMEOUT_SECONDS = 25;
|
|
404
|
+
var POLL_BACKOFF_MS = 1000;
|
|
405
|
+
var DRAFT_THROTTLE_MS = 500;
|
|
406
|
+
var DRAFT_MAX_LENGTH = 4096;
|
|
407
|
+
function conversationKey(message) {
|
|
408
|
+
return message.message_thread_id !== undefined ? `${message.chat.id}:${message.message_thread_id}` : String(message.chat.id);
|
|
409
|
+
}
|
|
410
|
+
function toInbound(message) {
|
|
411
|
+
return {
|
|
412
|
+
id: String(message.message_id),
|
|
413
|
+
conversationId: conversationKey(message),
|
|
414
|
+
text: message.text ?? message.caption ?? "",
|
|
415
|
+
user: message.from ? {
|
|
416
|
+
id: String(message.from.id),
|
|
417
|
+
name: message.from.username ?? message.from.first_name
|
|
418
|
+
} : undefined,
|
|
419
|
+
replyToId: message.reply_to_message ? String(message.reply_to_message.message_id) : undefined,
|
|
420
|
+
raw: message
|
|
421
|
+
};
|
|
422
|
+
}
|
|
423
|
+
function startTyping(client, chatId) {
|
|
424
|
+
let stopped = false;
|
|
425
|
+
function tick() {
|
|
426
|
+
if (!stopped)
|
|
427
|
+
client.sendChatAction(chatId, "typing").catch(() => {});
|
|
428
|
+
}
|
|
429
|
+
tick();
|
|
430
|
+
const interval = setInterval(tick, TYPING_INTERVAL_MS);
|
|
431
|
+
return () => {
|
|
432
|
+
stopped = true;
|
|
433
|
+
clearInterval(interval);
|
|
434
|
+
};
|
|
435
|
+
}
|
|
436
|
+
function sleep(ms) {
|
|
437
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
438
|
+
}
|
|
439
|
+
function telegramGateway(options) {
|
|
440
|
+
const client = new TelegramClient(options.token);
|
|
441
|
+
const transport = options.transport ?? { mode: "polling" };
|
|
442
|
+
function channelFor(message) {
|
|
443
|
+
const chatId = message.chat.id;
|
|
444
|
+
const threadId = message.message_thread_id;
|
|
445
|
+
return {
|
|
446
|
+
conversationId: conversationKey(message),
|
|
447
|
+
chatId,
|
|
448
|
+
client,
|
|
449
|
+
send: async (text, opts) => {
|
|
450
|
+
await client.sendMessage(chatId, text, {
|
|
451
|
+
messageThreadId: threadId,
|
|
452
|
+
...opts
|
|
453
|
+
});
|
|
454
|
+
},
|
|
455
|
+
draft: async (text) => {
|
|
456
|
+
await client.sendMessageDraft(chatId, message.message_id, text, {
|
|
457
|
+
messageThreadId: threadId
|
|
458
|
+
});
|
|
459
|
+
},
|
|
460
|
+
thinking: () => startTyping(client, chatId)
|
|
461
|
+
};
|
|
462
|
+
}
|
|
463
|
+
function forwards(message) {
|
|
464
|
+
return !options.commands || matchesCommand(message, options.commands, options.botUsername);
|
|
465
|
+
}
|
|
466
|
+
function deliver(handler, message) {
|
|
467
|
+
Promise.resolve(handler(toInbound(message), channelFor(message))).catch((error) => {
|
|
468
|
+
console.error("[gateways] telegram handler error:", error);
|
|
469
|
+
});
|
|
470
|
+
}
|
|
471
|
+
if (transport.mode === "webhook") {
|
|
472
|
+
const receiver = new TelegramReceiver({
|
|
473
|
+
secretToken: transport.secretToken
|
|
474
|
+
});
|
|
475
|
+
return {
|
|
476
|
+
name: "telegram",
|
|
477
|
+
client,
|
|
478
|
+
handle: receiver.handle,
|
|
479
|
+
open(handler) {
|
|
480
|
+
const trigger = receiver.message(options.commands ? { commands: options.commands, botUsername: options.botUsername } : undefined);
|
|
481
|
+
return trigger.start((message) => deliver(handler, message));
|
|
482
|
+
}
|
|
483
|
+
};
|
|
484
|
+
}
|
|
485
|
+
return {
|
|
486
|
+
name: "telegram",
|
|
487
|
+
client,
|
|
488
|
+
open(handler) {
|
|
489
|
+
const controller = new AbortController;
|
|
490
|
+
(async () => {
|
|
491
|
+
await client.deleteWebhook().catch(() => {});
|
|
492
|
+
let offset;
|
|
493
|
+
while (!controller.signal.aborted) {
|
|
494
|
+
try {
|
|
495
|
+
const updates = await client.getUpdates({
|
|
496
|
+
offset,
|
|
497
|
+
timeout: POLL_TIMEOUT_SECONDS,
|
|
498
|
+
allowedUpdates: ["message"],
|
|
499
|
+
signal: controller.signal
|
|
500
|
+
});
|
|
501
|
+
for (const update of updates) {
|
|
502
|
+
offset = update.update_id + 1;
|
|
503
|
+
const message = update.message;
|
|
504
|
+
if (message && forwards(message))
|
|
505
|
+
deliver(handler, message);
|
|
506
|
+
}
|
|
507
|
+
} catch (error) {
|
|
508
|
+
if (controller.signal.aborted)
|
|
509
|
+
break;
|
|
510
|
+
console.error("[gateways] telegram polling error:", error);
|
|
511
|
+
await sleep(POLL_BACKOFF_MS);
|
|
512
|
+
}
|
|
513
|
+
}
|
|
514
|
+
})();
|
|
515
|
+
return () => controller.abort();
|
|
516
|
+
}
|
|
517
|
+
};
|
|
518
|
+
}
|
|
519
|
+
function stream(options = {}) {
|
|
520
|
+
const throttleMs = options.throttleMs ?? DRAFT_THROTTLE_MS;
|
|
521
|
+
return async (turn, channel) => {
|
|
522
|
+
const blocks = [];
|
|
523
|
+
let current = "";
|
|
524
|
+
let failure = null;
|
|
525
|
+
let inFlight = false;
|
|
526
|
+
let lastSentAt = 0;
|
|
527
|
+
let lastSentText = "";
|
|
528
|
+
function preview() {
|
|
529
|
+
return [blocks.join(`
|
|
530
|
+
|
|
531
|
+
`), current].filter((part) => part.length > 0).join(`
|
|
532
|
+
|
|
533
|
+
`);
|
|
534
|
+
}
|
|
535
|
+
function refresh() {
|
|
536
|
+
const body = preview();
|
|
537
|
+
if (!body || body === lastSentText || body.length > DRAFT_MAX_LENGTH) {
|
|
538
|
+
return;
|
|
539
|
+
}
|
|
540
|
+
if (inFlight || Date.now() - lastSentAt < throttleMs)
|
|
541
|
+
return;
|
|
542
|
+
inFlight = true;
|
|
543
|
+
lastSentAt = Date.now();
|
|
544
|
+
lastSentText = body;
|
|
545
|
+
channel.draft(body).catch(() => {}).finally(() => {
|
|
546
|
+
inFlight = false;
|
|
547
|
+
});
|
|
548
|
+
}
|
|
549
|
+
for await (const event of turn) {
|
|
550
|
+
if (event.type === "text-delta") {
|
|
551
|
+
current += event.text;
|
|
552
|
+
refresh();
|
|
553
|
+
} else if (event.type === "text") {
|
|
554
|
+
blocks.push(event.text);
|
|
555
|
+
current = "";
|
|
556
|
+
refresh();
|
|
557
|
+
} else if (event.type === "error") {
|
|
558
|
+
failure = event.error;
|
|
559
|
+
}
|
|
560
|
+
}
|
|
561
|
+
const reply = blocks.join(`
|
|
562
|
+
|
|
563
|
+
`).trim();
|
|
564
|
+
if (failure && !reply)
|
|
565
|
+
throw failure;
|
|
566
|
+
await channel.send(reply || "(no response)");
|
|
567
|
+
};
|
|
568
|
+
}
|
|
569
|
+
export {
|
|
570
|
+
telegramGateway,
|
|
571
|
+
stream,
|
|
572
|
+
serve,
|
|
573
|
+
memoryStore,
|
|
574
|
+
fileStore
|
|
575
|
+
};
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { Agent, type Message, type TextPart } from '../core';
|
|
2
|
+
import type { Channel, Gateway, InboundMessage, Relay } from './core';
|
|
3
|
+
import { type ConversationStore } from './store';
|
|
4
|
+
interface ServeOptions<In extends InboundMessage, Ch extends Channel> {
|
|
5
|
+
conversation?: (message: In) => string;
|
|
6
|
+
authorize?: (message: In, channel: Ch) => boolean | Promise<boolean>;
|
|
7
|
+
systemExtension?: (message: In) => string | Promise<string>;
|
|
8
|
+
prompt?: (message: In) => string | TextPart[] | null;
|
|
9
|
+
context?: (stored: Message[], message: In) => Message[] | Promise<Message[]>;
|
|
10
|
+
relay?: Relay<In, Ch>;
|
|
11
|
+
store?: ConversationStore;
|
|
12
|
+
onError?: (error: Error, channel: Ch, message: In) => void | Promise<void>;
|
|
13
|
+
}
|
|
14
|
+
interface GatewayRuntime {
|
|
15
|
+
stop(): Promise<void>;
|
|
16
|
+
handle(req: Request): Promise<Response>;
|
|
17
|
+
}
|
|
18
|
+
declare function serve<In extends InboundMessage, Ch extends Channel>(agent: Agent, gateway: Gateway<In, Ch>, options?: ServeOptions<In, Ch>): GatewayRuntime;
|
|
19
|
+
export { serve, type GatewayRuntime, type ServeOptions };
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { TelegramClient, type SendMessageOptions } from '../../triggers/sources/telegram';
|
|
2
|
+
import type { Channel, Gateway, InboundMessage, Relay } from '../core';
|
|
3
|
+
interface TelegramChannel extends Channel {
|
|
4
|
+
chatId: number;
|
|
5
|
+
client: TelegramClient;
|
|
6
|
+
send(text: string, opts?: SendMessageOptions): Promise<void>;
|
|
7
|
+
draft(text: string): Promise<void>;
|
|
8
|
+
}
|
|
9
|
+
type TelegramTransport = {
|
|
10
|
+
mode: 'polling';
|
|
11
|
+
} | {
|
|
12
|
+
mode: 'webhook';
|
|
13
|
+
secretToken: string;
|
|
14
|
+
};
|
|
15
|
+
interface TelegramGatewayOptions {
|
|
16
|
+
token: string;
|
|
17
|
+
transport?: TelegramTransport;
|
|
18
|
+
commands?: string[];
|
|
19
|
+
botUsername?: string;
|
|
20
|
+
}
|
|
21
|
+
interface TelegramGateway extends Gateway<InboundMessage, TelegramChannel> {
|
|
22
|
+
client: TelegramClient;
|
|
23
|
+
}
|
|
24
|
+
declare function telegramGateway(options: TelegramGatewayOptions): TelegramGateway;
|
|
25
|
+
declare function stream(options?: {
|
|
26
|
+
throttleMs?: number;
|
|
27
|
+
}): Relay<InboundMessage, TelegramChannel>;
|
|
28
|
+
export { stream, telegramGateway, type TelegramChannel, type TelegramGateway, type TelegramGatewayOptions, type TelegramTransport, };
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import type { Message } from '../core';
|
|
2
|
+
import type { MaybePromise } from '../triggers/core';
|
|
3
|
+
interface ConversationStore {
|
|
4
|
+
load(id: string): MaybePromise<Message[] | null>;
|
|
5
|
+
append(id: string, ...messages: Message[]): MaybePromise<void>;
|
|
6
|
+
}
|
|
7
|
+
declare function memoryStore(): ConversationStore;
|
|
8
|
+
declare function fileStore(dir: string): ConversationStore;
|
|
9
|
+
export { fileStore, memoryStore, type ConversationStore };
|