roboport 0.0.1 → 0.1.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 CHANGED
@@ -8,8 +8,9 @@ Minimal TypeScript framework for building LLM agents.
8
8
  bun add roboport zod
9
9
  ```
10
10
 
11
- `zod` is a peer dependency, declare it in your own dependencies when you define
12
- Zod-backed tools.
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 };
package/harness/index.js CHANGED
@@ -210,7 +210,7 @@ class Agent {
210
210
  this.unsubs = [];
211
211
  await Promise.all(unsubs.map((u) => u()));
212
212
  }
213
- buildSystem(allTools) {
213
+ buildSystem(allTools, systemExtension) {
214
214
  let system = this.system;
215
215
  if (this.skills.length > 0) {
216
216
  const skillsList = this.skills.map((skill) => `- ${skill.name}: ${skill.description}`).join(`
@@ -231,6 +231,11 @@ ${skillsList}`;
231
231
  # Deferred tools
232
232
  These tools are available but their schemas are not loaded. Use ToolSearch to load them before calling.
233
233
  ${list}`;
234
+ }
235
+ if (systemExtension) {
236
+ system = `${system}
237
+
238
+ ${systemExtension}`;
234
239
  }
235
240
  return system;
236
241
  }
@@ -257,6 +262,7 @@ ${found.content}
257
262
  session(init) {
258
263
  const initialMessages = init?.messages ? [...init.messages] : [];
259
264
  const sessionCwd = init?.cwd ?? this.cwd ?? process.cwd();
265
+ const systemExtension = init?.systemExtension;
260
266
  const state = {
261
267
  messages: initialMessages,
262
268
  store: new Map
@@ -289,11 +295,14 @@ ${found.content}
289
295
  tools: registry,
290
296
  cwd: sessionCwd
291
297
  };
292
- if (state.messages.length === 0 || state.messages[0]?.role !== "system") {
293
- state.messages.unshift({
294
- role: "system",
295
- content: this.buildSystem(allTools)
296
- });
298
+ const systemMessage = {
299
+ role: "system",
300
+ content: this.buildSystem(allTools, systemExtension)
301
+ };
302
+ if (state.messages[0]?.role === "system") {
303
+ state.messages[0] = systemMessage;
304
+ } else {
305
+ state.messages.unshift(systemMessage);
297
306
  }
298
307
  }
299
308
  return { tools: allTools, registry, ctx };
package/index.js CHANGED
@@ -205,7 +205,7 @@ class Agent {
205
205
  this.unsubs = [];
206
206
  await Promise.all(unsubs.map((u) => u()));
207
207
  }
208
- buildSystem(allTools) {
208
+ buildSystem(allTools, systemExtension) {
209
209
  let system = this.system;
210
210
  if (this.skills.length > 0) {
211
211
  const skillsList = this.skills.map((skill) => `- ${skill.name}: ${skill.description}`).join(`
@@ -226,6 +226,11 @@ ${skillsList}`;
226
226
  # Deferred tools
227
227
  These tools are available but their schemas are not loaded. Use ToolSearch to load them before calling.
228
228
  ${list}`;
229
+ }
230
+ if (systemExtension) {
231
+ system = `${system}
232
+
233
+ ${systemExtension}`;
229
234
  }
230
235
  return system;
231
236
  }
@@ -252,6 +257,7 @@ ${found.content}
252
257
  session(init) {
253
258
  const initialMessages = init?.messages ? [...init.messages] : [];
254
259
  const sessionCwd = init?.cwd ?? this.cwd ?? process.cwd();
260
+ const systemExtension = init?.systemExtension;
255
261
  const state = {
256
262
  messages: initialMessages,
257
263
  store: new Map
@@ -284,11 +290,14 @@ ${found.content}
284
290
  tools: registry,
285
291
  cwd: sessionCwd
286
292
  };
287
- if (state.messages.length === 0 || state.messages[0]?.role !== "system") {
288
- state.messages.unshift({
289
- role: "system",
290
- content: this.buildSystem(allTools)
291
- });
293
+ const systemMessage = {
294
+ role: "system",
295
+ content: this.buildSystem(allTools, systemExtension)
296
+ };
297
+ if (state.messages[0]?.role === "system") {
298
+ state.messages[0] = systemMessage;
299
+ } else {
300
+ state.messages.unshift(systemMessage);
292
301
  }
293
302
  }
294
303
  return { tools: allTools, registry, ctx };
package/mcp/index.js CHANGED
@@ -205,7 +205,7 @@ class Agent {
205
205
  this.unsubs = [];
206
206
  await Promise.all(unsubs.map((u) => u()));
207
207
  }
208
- buildSystem(allTools) {
208
+ buildSystem(allTools, systemExtension) {
209
209
  let system = this.system;
210
210
  if (this.skills.length > 0) {
211
211
  const skillsList = this.skills.map((skill) => `- ${skill.name}: ${skill.description}`).join(`
@@ -226,6 +226,11 @@ ${skillsList}`;
226
226
  # Deferred tools
227
227
  These tools are available but their schemas are not loaded. Use ToolSearch to load them before calling.
228
228
  ${list}`;
229
+ }
230
+ if (systemExtension) {
231
+ system = `${system}
232
+
233
+ ${systemExtension}`;
229
234
  }
230
235
  return system;
231
236
  }
@@ -252,6 +257,7 @@ ${found.content}
252
257
  session(init) {
253
258
  const initialMessages = init?.messages ? [...init.messages] : [];
254
259
  const sessionCwd = init?.cwd ?? this.cwd ?? process.cwd();
260
+ const systemExtension = init?.systemExtension;
255
261
  const state = {
256
262
  messages: initialMessages,
257
263
  store: new Map
@@ -284,11 +290,14 @@ ${found.content}
284
290
  tools: registry,
285
291
  cwd: sessionCwd
286
292
  };
287
- if (state.messages.length === 0 || state.messages[0]?.role !== "system") {
288
- state.messages.unshift({
289
- role: "system",
290
- content: this.buildSystem(allTools)
291
- });
293
+ const systemMessage = {
294
+ role: "system",
295
+ content: this.buildSystem(allTools, systemExtension)
296
+ };
297
+ if (state.messages[0]?.role === "system") {
298
+ state.messages[0] = systemMessage;
299
+ } else {
300
+ state.messages.unshift(systemMessage);
292
301
  }
293
302
  }
294
303
  return { tools: allTools, registry, ctx };
package/models/index.js CHANGED
@@ -205,7 +205,7 @@ class Agent {
205
205
  this.unsubs = [];
206
206
  await Promise.all(unsubs.map((u) => u()));
207
207
  }
208
- buildSystem(allTools) {
208
+ buildSystem(allTools, systemExtension) {
209
209
  let system = this.system;
210
210
  if (this.skills.length > 0) {
211
211
  const skillsList = this.skills.map((skill) => `- ${skill.name}: ${skill.description}`).join(`
@@ -226,6 +226,11 @@ ${skillsList}`;
226
226
  # Deferred tools
227
227
  These tools are available but their schemas are not loaded. Use ToolSearch to load them before calling.
228
228
  ${list}`;
229
+ }
230
+ if (systemExtension) {
231
+ system = `${system}
232
+
233
+ ${systemExtension}`;
229
234
  }
230
235
  return system;
231
236
  }
@@ -252,6 +257,7 @@ ${found.content}
252
257
  session(init) {
253
258
  const initialMessages = init?.messages ? [...init.messages] : [];
254
259
  const sessionCwd = init?.cwd ?? this.cwd ?? process.cwd();
260
+ const systemExtension = init?.systemExtension;
255
261
  const state = {
256
262
  messages: initialMessages,
257
263
  store: new Map
@@ -284,11 +290,14 @@ ${found.content}
284
290
  tools: registry,
285
291
  cwd: sessionCwd
286
292
  };
287
- if (state.messages.length === 0 || state.messages[0]?.role !== "system") {
288
- state.messages.unshift({
289
- role: "system",
290
- content: this.buildSystem(allTools)
291
- });
293
+ const systemMessage = {
294
+ role: "system",
295
+ content: this.buildSystem(allTools, systemExtension)
296
+ };
297
+ if (state.messages[0]?.role === "system") {
298
+ state.messages[0] = systemMessage;
299
+ } else {
300
+ state.messages.unshift(systemMessage);
292
301
  }
293
302
  }
294
303
  return { tools: allTools, registry, ctx };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "roboport",
3
- "version": "0.0.1",
3
+ "version": "0.1.0",
4
4
  "description": "Minimal TypeScript framework for building LLM agents.",
5
5
  "author": {
6
6
  "name": "Timur Badretdinov",
@@ -20,11 +20,11 @@
20
20
  "llm",
21
21
  "agent",
22
22
  "ai",
23
- "mcp",
24
- "anthropic",
25
- "openai",
26
- "typescript"
23
+ "mcp"
27
24
  ],
25
+ "engines": {
26
+ "bun": ">=1.0.0"
27
+ },
28
28
  "type": "module",
29
29
  "main": "./index.js",
30
30
  "types": "./index.d.ts",
@@ -33,6 +33,10 @@
33
33
  "types": "./index.d.ts",
34
34
  "import": "./index.js"
35
35
  },
36
+ "./gateways": {
37
+ "types": "./gateways/index.d.ts",
38
+ "import": "./gateways/index.js"
39
+ },
36
40
  "./harness": {
37
41
  "types": "./harness/index.d.ts",
38
42
  "import": "./harness/index.js"
package/skills/index.js CHANGED
@@ -205,7 +205,7 @@ class Agent {
205
205
  this.unsubs = [];
206
206
  await Promise.all(unsubs.map((u) => u()));
207
207
  }
208
- buildSystem(allTools) {
208
+ buildSystem(allTools, systemExtension) {
209
209
  let system = this.system;
210
210
  if (this.skills.length > 0) {
211
211
  const skillsList = this.skills.map((skill) => `- ${skill.name}: ${skill.description}`).join(`
@@ -226,6 +226,11 @@ ${skillsList}`;
226
226
  # Deferred tools
227
227
  These tools are available but their schemas are not loaded. Use ToolSearch to load them before calling.
228
228
  ${list}`;
229
+ }
230
+ if (systemExtension) {
231
+ system = `${system}
232
+
233
+ ${systemExtension}`;
229
234
  }
230
235
  return system;
231
236
  }
@@ -252,6 +257,7 @@ ${found.content}
252
257
  session(init) {
253
258
  const initialMessages = init?.messages ? [...init.messages] : [];
254
259
  const sessionCwd = init?.cwd ?? this.cwd ?? process.cwd();
260
+ const systemExtension = init?.systemExtension;
255
261
  const state = {
256
262
  messages: initialMessages,
257
263
  store: new Map
@@ -284,11 +290,14 @@ ${found.content}
284
290
  tools: registry,
285
291
  cwd: sessionCwd
286
292
  };
287
- if (state.messages.length === 0 || state.messages[0]?.role !== "system") {
288
- state.messages.unshift({
289
- role: "system",
290
- content: this.buildSystem(allTools)
291
- });
293
+ const systemMessage = {
294
+ role: "system",
295
+ content: this.buildSystem(allTools, systemExtension)
296
+ };
297
+ if (state.messages[0]?.role === "system") {
298
+ state.messages[0] = systemMessage;
299
+ } else {
300
+ state.messages.unshift(systemMessage);
292
301
  }
293
302
  }
294
303
  return { tools: allTools, registry, ctx };
@@ -11,4 +11,4 @@ interface CustomTriggerInit<T> {
11
11
  start: (emit: Emit<T>) => MaybePromise<Unsub>;
12
12
  }
13
13
  declare function trigger<T = unknown>(init: CustomTriggerInit<T>): Trigger<T>;
14
- export { trigger, type Emit, type Trigger, type TriggerHandler, type Unsub };
14
+ export { trigger, type Emit, type MaybePromise, type Trigger, type TriggerHandler, type Unsub, };
package/triggers/index.js CHANGED
@@ -497,11 +497,12 @@ class TelegramClient {
497
497
  this.token = token;
498
498
  this.baseUrl = (opts?.baseUrl ?? TELEGRAM_API_BASE).replace(/\/+$/, "");
499
499
  }
500
- async call(method, params) {
500
+ async call(method, params, signal) {
501
501
  const response = await fetch(`${this.baseUrl}/bot${this.token}/${method}`, {
502
502
  method: "POST",
503
503
  headers: { "content-type": "application/json" },
504
- body: JSON.stringify(params)
504
+ body: JSON.stringify(params),
505
+ ...signal ? { signal } : {}
505
506
  });
506
507
  const data = await response.json();
507
508
  if (!data.ok) {
@@ -568,6 +569,13 @@ class TelegramClient {
568
569
  ...opts?.dropPendingUpdates ? { drop_pending_updates: true } : {}
569
570
  });
570
571
  }
572
+ getUpdates(opts) {
573
+ return this.call("getUpdates", {
574
+ ...opts?.offset !== undefined ? { offset: opts.offset } : {},
575
+ ...opts?.timeout !== undefined ? { timeout: opts.timeout } : {},
576
+ ...opts?.allowedUpdates ? { allowed_updates: opts.allowedUpdates } : {}
577
+ }, opts?.signal);
578
+ }
571
579
  }
572
580
  function telegram(options) {
573
581
  return new TelegramReceiver(options);
@@ -17,6 +17,7 @@ interface TelegramChat {
17
17
  }
18
18
  interface TelegramMessage {
19
19
  message_id: number;
20
+ message_thread_id?: number;
20
21
  from?: TelegramUser;
21
22
  chat: TelegramChat;
22
23
  date: number;
@@ -45,6 +46,7 @@ interface TelegramReceiverOptions {
45
46
  secretToken: string;
46
47
  updateCacheSize?: number;
47
48
  }
49
+ declare function matchesCommand(message: TelegramMessage, commands: string[], botUsername?: string): boolean;
48
50
  declare class TelegramReceiver {
49
51
  private messageBus;
50
52
  private editedMessageBus;
@@ -80,6 +82,12 @@ declare class TelegramClient {
80
82
  deleteWebhook(opts?: {
81
83
  dropPendingUpdates?: boolean;
82
84
  }): Promise<boolean>;
85
+ getUpdates(opts?: {
86
+ offset?: number;
87
+ timeout?: number;
88
+ allowedUpdates?: string[];
89
+ signal?: AbortSignal;
90
+ }): Promise<TelegramUpdate[]>;
83
91
  }
84
92
  declare function telegram(options: TelegramReceiverOptions): TelegramReceiver;
85
- export { telegram, TelegramClient, TelegramReceiver, splitMessage, type SendMessageDraftOptions, type SendMessageOptions, type TelegramChat, type TelegramChatAction, type TelegramMessage, type TelegramReceiverOptions, type TelegramUpdate, type TelegramUser, };
93
+ export { matchesCommand, telegram, TelegramClient, TelegramReceiver, splitMessage, type SendMessageDraftOptions, type SendMessageOptions, type TelegramChat, type TelegramChatAction, type TelegramMessage, type TelegramReceiverOptions, type TelegramUpdate, type TelegramUser, };