@toolpack-sdk/agents 2.0.0-alpha.1 → 2.0.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.
@@ -0,0 +1,927 @@
1
+ import { A as AgentInput, e as AgentOutput } from './types-TB6yypig.js';
2
+ import { Participant } from 'toolpack-sdk';
3
+
4
+ /**
5
+ * Abstract base class for all agent channels.
6
+ * Channels handle the two-way communication between the external world and agents.
7
+ */
8
+ declare abstract class BaseChannel {
9
+ /** Optional name for the channel - required for sendTo() routing */
10
+ name?: string;
11
+ /**
12
+ * Whether this is a trigger channel (no human recipient).
13
+ * Trigger channels like ScheduledChannel cannot use this.ask() since there's no human to answer.
14
+ * Conversation channels (Slack, Telegram, Webhook) can use this.ask().
15
+ */
16
+ abstract readonly isTriggerChannel: boolean;
17
+ /** Message handler set by AgentRegistry */
18
+ protected _handler?: (input: AgentInput) => Promise<void>;
19
+ /**
20
+ * Start listening for incoming messages.
21
+ * Called by AgentRegistry when the SDK initializes.
22
+ */
23
+ abstract listen(): void;
24
+ /**
25
+ * Send output back to the external world.
26
+ * @param output The agent's output to deliver
27
+ */
28
+ abstract send(output: AgentOutput): Promise<void>;
29
+ /**
30
+ * Normalize an incoming event into AgentInput.
31
+ * Each channel implementation maps its specific event format.
32
+ * @param incoming Raw event from the external source
33
+ * @returns Normalized AgentInput
34
+ */
35
+ abstract normalize(incoming: unknown): AgentInput;
36
+ /**
37
+ * Set the message handler. Called by AgentRegistry.
38
+ * @param handler Function to call when a message arrives
39
+ */
40
+ onMessage(handler: (input: AgentInput) => Promise<void>): void;
41
+ /**
42
+ * Helper to call the handler if set.
43
+ * @param input The normalized agent input
44
+ */
45
+ protected handleMessage(input: AgentInput): Promise<void>;
46
+ }
47
+
48
+ /**
49
+ * Configuration options for SlackChannel.
50
+ */
51
+ interface SlackChannelConfig {
52
+ /** Optional name for the channel - required for sendTo() routing */
53
+ name?: string;
54
+ /**
55
+ * Which Slack channel(s) this instance listens to and replies into.
56
+ *
57
+ * - `string` (e.g. `'#support'` or `'C12345'`) — single channel (back-compat).
58
+ * - `string[]` — multiple channels; inbound events outside this list are dropped.
59
+ * - `null` / omitted — listen to every channel the bot is invited to.
60
+ *
61
+ * **Matching:** compared verbatim against `event.channel` from the Slack payload.
62
+ * Slack events carry channel IDs (`C...`), so pass IDs here for deterministic
63
+ * filtering. If you pass a display name like `'#general'`, it must match the
64
+ * raw string Slack sends — usually an ID, not a name. DMs (`im`/`mpim`) are
65
+ * always accepted regardless of this list.
66
+ *
67
+ * **Outbound:** when sending, `metadata.channelId` (set by `normalize()`) wins.
68
+ * If absent, the fallback is: `string` → itself; `string[]` → first element;
69
+ * `null` → error (must provide `metadata.channelId`).
70
+ */
71
+ channel?: string | string[] | null;
72
+ /** Slack bot token (starts with 'xoxb-') */
73
+ token: string;
74
+ /** Slack app signing secret for request verification */
75
+ signingSecret: string;
76
+ /** Optional port for the HTTP server (default: 3000) */
77
+ port?: number;
78
+ /**
79
+ * Allowlist of bot identities whose Slack messages should be processed when
80
+ * strict mode is desired.
81
+ *
82
+ * Behavior:
83
+ * - Omitted: non-self bot messages are accepted by default (Option B).
84
+ * - Provided (including empty array): only listed bots are accepted.
85
+ *
86
+ * Each entry is matched against **both** `event.bot_id` (a `B...` integration
87
+ * id) and `event.user` (a `U...` user id), since Slack events carry both and
88
+ * developers frequently know one but not the other. Pass whichever you have —
89
+ * typically the peer agent's `SlackChannel.botUserId` (a `U...` value).
90
+ *
91
+ * Note: for normal multi-agent teams you do **not** need to list peers here —
92
+ * non-self bot messages are allowed by default. Use this field only when you
93
+ * want strict, allowlist-only acceptance. To suppress specific noisy bots
94
+ * (e.g. GitHub, CI) while keeping the default-allow behavior, prefer
95
+ * {@link SlackChannelConfig.blockedBotIds}.
96
+ *
97
+ * Example (strict mode): `allowedBotIds: [ramDevAgent.slackChannel.botUserId, 'B_YALINA_BOT']`
98
+ */
99
+ allowedBotIds?: string[];
100
+ /**
101
+ * Blocklist of bot identities that should always be ignored.
102
+ *
103
+ * Matched against both `event.bot_id` (B...) and `event.user` (U...).
104
+ * Takes precedence over `allowedBotIds` and the default allow behavior.
105
+ */
106
+ blockedBotIds?: string[];
107
+ }
108
+ /**
109
+ * Slack channel for two-way Slack integration.
110
+ * Receives messages from users and replies in-thread.
111
+ */
112
+ declare class SlackChannel extends BaseChannel {
113
+ readonly isTriggerChannel = false;
114
+ private config;
115
+ private server?;
116
+ /**
117
+ * Per-process cache of resolved participants keyed by Slack user id.
118
+ * Populated lazily by `resolveParticipant()`. Invalidated on `user_change`
119
+ * events via `invalidateParticipant()`.
120
+ */
121
+ private participantCache;
122
+ /**
123
+ * The bot's Slack user id (e.g. `'U_BOT123'`), populated by the startup
124
+ * self-check (`auth.test`) when `listen()` is called.
125
+ *
126
+ * Pass this to `AssemblerOptions.agentAliases` so the assembler's
127
+ * addressed-only mode can match `<@U_BOT123>` mentions against this agent:
128
+ * ```ts
129
+ * assemblePrompt(store, conversationId, agent.name, agent.name, {
130
+ * agentAliases: [slackChannel.botUserId].filter(Boolean) as string[],
131
+ * });
132
+ * ```
133
+ */
134
+ botUserId?: string;
135
+ /**
136
+ * Normalized allowlist of channel identifiers, or `null` to accept any channel.
137
+ * Derived from `config.channel` at construction time.
138
+ */
139
+ private allowedChannels;
140
+ constructor(config: SlackChannelConfig);
141
+ /**
142
+ * Start listening for Slack events via HTTP webhook.
143
+ *
144
+ * Performs a startup self-check (`auth.test`) after the server is ready.
145
+ * The bot user id is stored on `this.botUserId` for use in `agentAliases`.
146
+ */
147
+ listen(): void;
148
+ /**
149
+ * Calls Slack's `auth.test` API to verify credentials and log the bot's
150
+ * identity. Stores `botUserId` for use in `AssemblerOptions.agentAliases`.
151
+ * Non-fatal — a failed check logs a warning but does not stop the server.
152
+ */
153
+ private runStartupCheck;
154
+ /**
155
+ * Verify a Slack request signature using HMAC-SHA256.
156
+ *
157
+ * Implements Slack's signing secret verification spec:
158
+ * https://api.slack.com/authentication/verifying-requests-from-slack
159
+ *
160
+ * - Rejects requests with a timestamp older than 5 minutes (replay protection).
161
+ * - Uses `timingSafeEqual` to prevent timing-oracle attacks.
162
+ *
163
+ * Returns `false` for any missing, malformed, or invalid input so that the
164
+ * caller can respond with 401 without leaking which check failed.
165
+ */
166
+ verifySignature(headers: Record<string, string | string[] | undefined>, rawBody: string): boolean;
167
+ /**
168
+ * Send a message back to Slack.
169
+ * @param output The agent output to send
170
+ */
171
+ send(output: AgentOutput): Promise<void>;
172
+ /**
173
+ * Normalize a Slack event into AgentInput.
174
+ * @param incoming Slack event payload
175
+ * @returns Normalized AgentInput
176
+ */
177
+ normalize(incoming: unknown): AgentInput;
178
+ /**
179
+ * Resolve a richer `Participant` (with `displayName`) for a normalized input.
180
+ *
181
+ * Uses Slack's `users.info` API and an in-process cache. Returns `undefined`
182
+ * if the input has no user id or if the lookup fails; callers fall back to
183
+ * the bare id. Never throws.
184
+ *
185
+ * Cache invalidation is handled externally via `invalidateParticipant()`,
186
+ * typically wired to the Slack `user_change` event.
187
+ */
188
+ resolveParticipant(input: AgentInput): Promise<Participant | undefined>;
189
+ /**
190
+ * Invalidate a cached participant. Call this from a `user_change`
191
+ * Slack event handler to force a refresh on the next lookup.
192
+ */
193
+ invalidateParticipant(userId: string): void;
194
+ /**
195
+ * Decide whether an incoming Slack event should be normalised and dispatched
196
+ * to the agent.
197
+ *
198
+ * Rules, applied in order:
199
+ * 1. Only `message` and `app_mention` events are processed; others are dropped.
200
+ * 2. Channel allowlist (from `config.channel`): events outside the allowlist
201
+ * are dropped. DMs (`im`/`mpim`) always pass because they are per-user, not
202
+ * per-channel. Skipped entirely when `config.channel` is null/omitted.
203
+ * 3. **Self-suppression (automatic):** events where `event.user` matches this
204
+ * channel's own `botUserId` are dropped. This prevents agents from looping
205
+ * on their own posts and requires no configuration — `botUserId` is
206
+ * discovered via `auth.test` at startup.
207
+ * 4. Events without `bot_id` (human messages) pass.
208
+ * 5. Explicit blocklist check (`blockedBotIds`) runs first for bot messages.
209
+ * 6. If `allowedBotIds` is provided, strict mode applies: only listed bots
210
+ * pass (matched against both `bot_id` and `user`).
211
+ * 7. If `allowedBotIds` is omitted, other bot messages pass by default.
212
+ *
213
+ * Exposed for direct unit testing; not intended as a public API.
214
+ */
215
+ shouldProcessEvent(event: Record<string, unknown>): boolean;
216
+ /**
217
+ * Handle incoming HTTP requests from Slack.
218
+ */
219
+ private handleRequest;
220
+ /**
221
+ * Stop the HTTP server.
222
+ */
223
+ stop(): Promise<void>;
224
+ }
225
+
226
+ /**
227
+ * Configuration options for WebhookChannel.
228
+ */
229
+ interface WebhookChannelConfig {
230
+ /** Optional name for the channel - required for sendTo() routing */
231
+ name?: string;
232
+ /** HTTP path to listen on (e.g., '/agent/support') */
233
+ path: string;
234
+ /** Optional port for the HTTP server (default: 3000) */
235
+ port?: number;
236
+ }
237
+ /**
238
+ * Webhook channel that exposes an HTTP endpoint.
239
+ * Receives HTTP requests and responds with agent output.
240
+ */
241
+ declare class WebhookChannel extends BaseChannel {
242
+ readonly isTriggerChannel = false;
243
+ private config;
244
+ private server?;
245
+ private pendingResponses;
246
+ constructor(config: WebhookChannelConfig);
247
+ /**
248
+ * Start listening for HTTP requests.
249
+ */
250
+ listen(): void;
251
+ /**
252
+ * Send the agent output as an HTTP response.
253
+ * @param output The agent output to send
254
+ */
255
+ send(output: AgentOutput): Promise<void>;
256
+ /**
257
+ * Normalize an HTTP request into AgentInput.
258
+ * @param incoming HTTP request body with headers
259
+ * @returns Normalized AgentInput
260
+ */
261
+ normalize(incoming: unknown): AgentInput;
262
+ /**
263
+ * Handle incoming HTTP requests.
264
+ */
265
+ private handleRequest;
266
+ /**
267
+ * Generate a unique session ID.
268
+ */
269
+ private generateSessionId;
270
+ /**
271
+ * Stop the HTTP server.
272
+ */
273
+ stop(): Promise<void>;
274
+ }
275
+
276
+ /**
277
+ * Scheduler types — shared across SchedulerStore, scheduler tools, and ScheduledChannel.
278
+ */
279
+ type JobStatus = 'pending' | 'running' | 'completed' | 'failed' | 'cancelled';
280
+ /**
281
+ * A single scheduled job record.
282
+ */
283
+ interface ScheduledJob {
284
+ /** Unique identifier (UUIDv4). */
285
+ id: string;
286
+ /** Name of the channel this job belongs to (for multi-channel routing). */
287
+ channelName?: string;
288
+ /** Next execution time in epoch ms. */
289
+ nextRunAt: number;
290
+ /**
291
+ * Cron expression for recurring jobs.
292
+ * Undefined for one-shot jobs.
293
+ */
294
+ cron?: string;
295
+ /** Intent hint forwarded to AgentInput. */
296
+ intent?: string;
297
+ /** Message forwarded to AgentInput. */
298
+ message?: string;
299
+ /** Extra data merged into AgentInput.data. */
300
+ payload?: Record<string, unknown>;
301
+ /** Current lifecycle status. */
302
+ status: JobStatus;
303
+ /** Epoch ms of the last execution. */
304
+ lastRunAt?: number;
305
+ /** Error message from the last failed execution. */
306
+ lastError?: string;
307
+ /** Epoch ms when the job was created. */
308
+ createdAt: number;
309
+ }
310
+ /**
311
+ * Options for creating a new scheduled job.
312
+ * Exactly one of `cron` (recurring) or `runAt` (one-shot) must be provided.
313
+ */
314
+ interface CreateJobOptions {
315
+ /** Channel name to scope this job to. */
316
+ channelName?: string;
317
+ /** Intent hint for the agent. */
318
+ intent?: string;
319
+ /** Message for the agent. */
320
+ message?: string;
321
+ /** Extra data merged into AgentInput.data. */
322
+ payload?: Record<string, unknown>;
323
+ /**
324
+ * Cron expression for recurring jobs.
325
+ * Supports 5-field (min hour dom month dow) and 6-field (sec min hour dom month dow).
326
+ */
327
+ cron?: string;
328
+ /**
329
+ * Exact time for one-shot jobs.
330
+ * Accepts a Date object or epoch ms.
331
+ */
332
+ runAt?: Date | number;
333
+ }
334
+ /**
335
+ * Result of a create operation — includes a duplicate flag if dedup matched.
336
+ */
337
+ interface CreateJobResult {
338
+ job: ScheduledJob;
339
+ /** True if an existing pending job matched the dedup key (job was not re-created). */
340
+ duplicate: boolean;
341
+ }
342
+
343
+ /**
344
+ * SQLite-backed persistent store for scheduled jobs.
345
+ *
346
+ * @example
347
+ * ```ts
348
+ * const store = new SchedulerStore({ dbPath: './scheduler.db' });
349
+ *
350
+ * // Create a recurring job
351
+ * const { job } = store.create({
352
+ * intent: 'weekly_report',
353
+ * cron: '0 9 * * 1',
354
+ * message: 'Generate weekly summary',
355
+ * });
356
+ *
357
+ * // Create a one-shot job
358
+ * store.create({
359
+ * intent: 'onboarding_followup',
360
+ * runAt: new Date('2026-06-01T10:00:00Z'),
361
+ * });
362
+ * ```
363
+ */
364
+ declare class SchedulerStore {
365
+ private db;
366
+ constructor({ dbPath }?: {
367
+ dbPath?: string;
368
+ });
369
+ private _migrate;
370
+ /**
371
+ * Create a new scheduled job.
372
+ *
373
+ * **Deduplication** — before inserting, checks for an existing `pending` job
374
+ * with the same key:
375
+ * - Recurring (`cron`): matched on `(intent, cron, channel_name)`
376
+ * - One-shot (`runAt`): matched on `(intent, next_run_at, channel_name)`
377
+ *
378
+ * Returns the existing job with `duplicate: true` instead of creating a duplicate.
379
+ */
380
+ create(opts: CreateJobOptions): CreateJobResult;
381
+ /**
382
+ * Get a single job by ID.
383
+ */
384
+ get(id: string): ScheduledJob | undefined;
385
+ /**
386
+ * List jobs, optionally filtered by status and channel.
387
+ */
388
+ list(filter?: {
389
+ status?: JobStatus | 'all';
390
+ channelName?: string;
391
+ limit?: number;
392
+ }): ScheduledJob[];
393
+ /**
394
+ * Return all pending jobs whose nextRunAt <= now (due or overdue).
395
+ * Used by ScheduledChannel for missed-run recovery and normal polling.
396
+ * Pass `channelName` to scope results to a specific channel.
397
+ */
398
+ getDue(now?: number, channelName?: string): ScheduledJob[];
399
+ /**
400
+ * Return the single next pending job (earliest nextRunAt).
401
+ * Used by ScheduledChannel to calculate optimal sleep duration.
402
+ */
403
+ getNextPending(channelName?: string): ScheduledJob | undefined;
404
+ /**
405
+ * Update an existing pending job.
406
+ */
407
+ update(id: string, updates: {
408
+ cron?: string;
409
+ message?: string;
410
+ intent?: string;
411
+ runAt?: Date | number;
412
+ payload?: Record<string, unknown>;
413
+ }): ScheduledJob | undefined;
414
+ /**
415
+ * Cancel a pending job. Returns true if cancelled, false if not found or not pending.
416
+ */
417
+ cancel(id: string): boolean;
418
+ /**
419
+ * Reset any jobs stuck in `running` state back to `pending`.
420
+ *
421
+ * Should be called once on ScheduledChannel startup. A job can get stuck as
422
+ * `running` if the process crashes after `markRunning()` but before
423
+ * `markCompleted()` or `markFailed()`. Without this call those jobs would
424
+ * never be retried since `getDue()` only returns `pending` jobs.
425
+ *
426
+ * Pass `channelName` to scope the reset to a specific channel.
427
+ *
428
+ * @returns The number of jobs reset.
429
+ */
430
+ resetStuck(channelName?: string): number;
431
+ /** Mark a job as running (called before execution). */
432
+ markRunning(id: string): void;
433
+ /** Mark a completed job. For recurring jobs, calculates and sets the next run time. */
434
+ markCompleted(id: string): void;
435
+ /** Mark a job as failed and store the error. Recurring jobs are rescheduled. */
436
+ markFailed(id: string, error: string): void;
437
+ private _getById;
438
+ private _findDuplicate;
439
+ /** Close the underlying database connection. */
440
+ close(): void;
441
+ }
442
+
443
+ /**
444
+ * Configuration for ScheduledChannel.
445
+ *
446
+ * Provide `cron` for a simple static schedule, `store` for dynamic agent-driven
447
+ * scheduling, or both — the store takes precedence when both are supplied, and
448
+ * the `cron` is seeded into the store as an initial job on `listen()`.
449
+ *
450
+ * Output delivery is the agent's responsibility. Use `sendTo()` inside
451
+ * `invokeAgent()` to route results to a named channel (Slack, webhook, etc.).
452
+ */
453
+ interface ScheduledChannelConfig {
454
+ /** Optional name — required for `sendTo()` routing in multi-channel setups. */
455
+ name?: string;
456
+ /**
457
+ * Static cron expression.
458
+ * Used directly when no `store` is provided.
459
+ * When a `store` is also provided, this is seeded into the store as an
460
+ * initial recurring job (subject to deduplication).
461
+ *
462
+ * Supports 5-field (min hour dom month dow) and 6-field (sec min hour dom month dow).
463
+ * Example: `'0 9 * * 1-5'` for 9am on weekdays.
464
+ */
465
+ cron?: string;
466
+ /**
467
+ * Dynamic scheduler store.
468
+ * When provided, the channel polls the store for due jobs instead of (or in
469
+ * addition to) the static cron. The store drives all scheduling decisions,
470
+ * enabling agents to schedule their own future invocations via scheduler tools.
471
+ */
472
+ store?: SchedulerStore;
473
+ /** Default intent forwarded to AgentInput when no job-level intent is set. */
474
+ intent?: string;
475
+ /** Default message forwarded to AgentInput when no job-level message is set. */
476
+ message?: string;
477
+ /**
478
+ * How often (in ms) to poll the store for new jobs when it is currently empty.
479
+ * Agents that create new jobs via `scheduler.create` while the store is idle
480
+ * will be picked up within this interval.
481
+ *
482
+ * Default: `30_000` (30 seconds).
483
+ */
484
+ idlePollMs?: number;
485
+ }
486
+ /**
487
+ * A trigger-only channel that runs agents on a cron schedule or via a
488
+ * dynamic SchedulerStore.
489
+ *
490
+ * - No `notify` option — output delivery is the agent's responsibility.
491
+ * - `isTriggerChannel = true` — `this.ask()` is not available inside triggered runs.
492
+ * - When a `store` is provided, supports missed-run recovery on startup and
493
+ * lets agents schedule their own future invocations using `scheduler.*` tools.
494
+ *
495
+ * @example Static cron
496
+ * ```ts
497
+ * const channel = new ScheduledChannel({
498
+ * name: 'daily',
499
+ * cron: '0 9 * * 1-5',
500
+ * intent: 'daily_report',
501
+ * });
502
+ * ```
503
+ *
504
+ * @example Dynamic store (agent-driven scheduling)
505
+ * ```ts
506
+ * const store = new SchedulerStore({ dbPath: './scheduler.db' });
507
+ *
508
+ * const channel = new ScheduledChannel({
509
+ * name: 'dynamic',
510
+ * store,
511
+ * });
512
+ *
513
+ * // Agent tools: scheduler.create / scheduler.list / scheduler.cancel / scheduler.update
514
+ * ```
515
+ *
516
+ * @example Both — static seed + dynamic store
517
+ * ```ts
518
+ * const store = new SchedulerStore({ dbPath: './scheduler.db' });
519
+ *
520
+ * const channel = new ScheduledChannel({
521
+ * name: 'hybrid',
522
+ * cron: '0 9 * * 1-5', // seeded into store on listen()
523
+ * store,
524
+ * intent: 'morning_check',
525
+ * idlePollMs: 10_000, // check for agent-created jobs every 10s when idle
526
+ * });
527
+ * ```
528
+ */
529
+ declare class ScheduledChannel extends BaseChannel {
530
+ readonly isTriggerChannel = true;
531
+ private config;
532
+ private timer?;
533
+ private _stopped;
534
+ /**
535
+ * Incremented each time `listen()` is called. Callbacks capture the value at
536
+ * the moment they are created; if it no longer matches `_generation` by the
537
+ * time they resume, they know a new listen-cycle has started and self-cancel.
538
+ * This prevents a stop()+listen() during an in-flight async callback from
539
+ * spawning a second independent timer loop.
540
+ */
541
+ private _generation;
542
+ constructor(config: ScheduledChannelConfig);
543
+ /**
544
+ * Start the scheduler.
545
+ *
546
+ * - Static-only mode: sets up a recurring setTimeout loop.
547
+ * - Store mode: seeds the static cron (if any), recovers missed runs, then
548
+ * sets up a smart-sleep loop driven by the store's next pending job.
549
+ */
550
+ listen(): void;
551
+ /**
552
+ * Stop the scheduler and clear any pending timer.
553
+ */
554
+ stop(): Promise<void>;
555
+ /**
556
+ * ScheduledChannel is a pure trigger — output delivery is handled by the
557
+ * agent via `sendTo()`. This method is intentionally a no-op.
558
+ */
559
+ send(_output: AgentOutput): Promise<void>;
560
+ /**
561
+ * Normalize a scheduled trigger (or store job) into AgentInput.
562
+ *
563
+ * @param incoming A ScheduledJob from the store, or null for static cron triggers.
564
+ */
565
+ normalize(incoming: unknown): AgentInput;
566
+ private _listenStatic;
567
+ private _scheduleNextStatic;
568
+ private _triggerStatic;
569
+ private _listenWithStore;
570
+ private _scheduleNextFromStore;
571
+ private _triggerJob;
572
+ private _nextRunFromCron;
573
+ }
574
+
575
+ /**
576
+ * Configuration options for TelegramChannel.
577
+ */
578
+ interface TelegramChannelConfig {
579
+ /** Optional name for the channel - required for sendTo() routing */
580
+ name?: string;
581
+ /** Telegram bot token (from @BotFather) */
582
+ token: string;
583
+ /** Optional webhook URL for receiving updates (if not using polling) */
584
+ webhookUrl?: string;
585
+ }
586
+ /**
587
+ * Telegram channel for two-way Telegram bot integration.
588
+ * Receives messages from users and sends replies.
589
+ */
590
+ declare class TelegramChannel extends BaseChannel {
591
+ readonly isTriggerChannel = false;
592
+ private config;
593
+ private offset;
594
+ private pollingInterval?;
595
+ private server?;
596
+ /**
597
+ * The bot's Telegram user id (numeric, as a string), populated by the
598
+ * startup self-check (`getMe`) when `listen()` is called.
599
+ *
600
+ * Pass this to `AssemblerOptions.agentAliases` so the assembler's
601
+ * addressed-only mode can match `text_mention` entities whose user id
602
+ * equals this value.
603
+ */
604
+ botUserId?: string;
605
+ /** The bot's @username (without the @), populated by the `getMe` check. */
606
+ botUsername?: string;
607
+ constructor(config: TelegramChannelConfig);
608
+ /**
609
+ * Start listening for Telegram updates.
610
+ * Uses either webhook or polling mode depending on configuration.
611
+ */
612
+ listen(): void;
613
+ /**
614
+ * Calls Telegram's `getMe` API to verify the token and log the bot's
615
+ * identity. Stores `botUserId` and `botUsername` for use in
616
+ * `AssemblerOptions.agentAliases`. Non-fatal — a failed check logs a
617
+ * warning but does not stop the channel.
618
+ */
619
+ private runStartupCheck;
620
+ /**
621
+ * Send a message back to Telegram.
622
+ * @param output The agent output to send
623
+ */
624
+ send(output: AgentOutput): Promise<void>;
625
+ /**
626
+ * Normalize a Telegram update into AgentInput.
627
+ * @param incoming Telegram update object
628
+ * @returns Normalized AgentInput
629
+ */
630
+ normalize(incoming: unknown): AgentInput;
631
+ /**
632
+ * Start polling for updates.
633
+ */
634
+ private startPolling;
635
+ /**
636
+ * Poll for updates from Telegram.
637
+ */
638
+ private pollUpdates;
639
+ /**
640
+ * Start webhook server for receiving updates.
641
+ */
642
+ private startWebhook;
643
+ /**
644
+ * Set webhook URL with Telegram.
645
+ */
646
+ private setWebhook;
647
+ /**
648
+ * Handle incoming webhook requests from Telegram.
649
+ */
650
+ private handleWebhookRequest;
651
+ /**
652
+ * Stop the channel (polling or webhook).
653
+ */
654
+ stop(): Promise<void>;
655
+ }
656
+
657
+ interface DiscordAuthor {
658
+ id: string;
659
+ username: string;
660
+ globalName?: string | null;
661
+ bot?: boolean;
662
+ system?: boolean;
663
+ }
664
+ interface DiscordChannelInfo {
665
+ type?: number;
666
+ name?: string;
667
+ }
668
+ interface DiscordThread {
669
+ id: string;
670
+ }
671
+ /** Minimal shape of a discord.js Message used by this channel. */
672
+ interface DiscordMessage {
673
+ id?: string;
674
+ content?: string;
675
+ channelId?: string;
676
+ guildId?: string;
677
+ /** Present on webhook-originated messages; absent on normal user messages. */
678
+ webhookId?: string;
679
+ author?: DiscordAuthor;
680
+ channel?: DiscordChannelInfo;
681
+ thread?: DiscordThread;
682
+ }
683
+ /**
684
+ * Configuration options for DiscordChannel.
685
+ */
686
+ interface DiscordChannelConfig {
687
+ /** Optional name for the channel — required for sendTo() routing. */
688
+ name?: string;
689
+ /** Discord bot token. */
690
+ token: string;
691
+ /**
692
+ * Target guild (server) ID filter.
693
+ * When set, only messages from this guild pass through.
694
+ * DMs (which carry no guildId) are dropped when this is configured.
695
+ * Omit to allow messages from all guilds and DMs.
696
+ */
697
+ guildId?: string;
698
+ /**
699
+ * Target channel ID filter.
700
+ * - `string` — only messages in this channel pass through.
701
+ * - `string[]` — only messages in one of these channels pass through.
702
+ * - `null` / `undefined` — no channel filter; all channels in the guild pass.
703
+ *
704
+ * Also used as the default destination when send() has no metadata.channelId.
705
+ */
706
+ channelId?: string | string[] | null;
707
+ /**
708
+ * Bot user IDs that are explicitly allowed to pass through.
709
+ * When set, only bots in this list are processed; all other bots are dropped.
710
+ * blockedBotIds takes precedence — a bot in both lists is always dropped.
711
+ * When omitted, ALL bots are dropped by default.
712
+ */
713
+ allowedBotIds?: string[];
714
+ /**
715
+ * Bot user IDs that are explicitly blocked.
716
+ * Checked before allowedBotIds; matching bots are always dropped.
717
+ */
718
+ blockedBotIds?: string[];
719
+ }
720
+ /**
721
+ * DiscordChannel — full-featured Discord bot channel.
722
+ *
723
+ * Capabilities beyond the basic implementation:
724
+ *
725
+ * 1. shouldProcessEvent() — ordered, deterministic filtering:
726
+ * system/webhook messages → self-suppression → guild filter → channel allowlist
727
+ * → bot blocklist → bot allowlist → default-drop bots → pass.
728
+ * All filtering happens in code, not in the LLM prompt.
729
+ *
730
+ * 2. normalize() — extracts @mention user IDs (<@id> and <@!id>) from content
731
+ * and exposes them as context.mentions (the key the capture interceptor reads
732
+ * for addressed-only mode). Also sets context.isMentioned when the bot is tagged.
733
+ *
734
+ * 3. resolveParticipant() — fetches richer user info (globalName / avatar) from
735
+ * the Discord REST API with an in-process per-channel cache.
736
+ * Call invalidateParticipant(userId) when a `userUpdate` event fires.
737
+ *
738
+ * 4. send() — priority chain: threadId metadata → channelId metadata → first
739
+ * configured channel → throws if nothing is resolvable.
740
+ *
741
+ * 5. Multi-channel support — channelId accepts string, string[], or null/undefined.
742
+ *
743
+ * 6. Bot self-suppression — botUserId is captured from the `ready` event so our
744
+ * own echoes are dropped without a separate lookup.
745
+ *
746
+ * 7. userUpdate wiring — participant cache entries are invalidated automatically
747
+ * when Discord fires a userUpdate event.
748
+ */
749
+ declare class DiscordChannel extends BaseChannel {
750
+ readonly isTriggerChannel = false;
751
+ protected readonly config: DiscordChannelConfig;
752
+ /** Normalised set of allowed channel IDs (empty = no filter). */
753
+ private readonly allowedChannelIds;
754
+ /**
755
+ * The bot's own Discord user ID — populated after the `ready` event.
756
+ *
757
+ * Expose this to agent code for use in `agentAliases` (addressed-only mode):
758
+ * ```ts
759
+ * agentAliases: [discordChannel.botUserId].filter(Boolean) as string[]
760
+ * ```
761
+ */
762
+ botUserId?: string;
763
+ /** In-process participant cache keyed by Discord user ID. */
764
+ private readonly participantCache;
765
+ /** discord.js Client instance (populated by listen()). */
766
+ private client?;
767
+ constructor(config: DiscordChannelConfig);
768
+ /**
769
+ * Deterministic gate applied before any message reaches the LLM.
770
+ *
771
+ * Rules (applied in order; first match wins):
772
+ * 1. No author OR webhook message → drop (system embeds, webhooks).
773
+ * 2. Author is the bot itself → drop (echo suppression).
774
+ * 3. guildId filter configured and message doesn't match → drop (incl. DMs).
775
+ * 4. channelId allowlist configured and message not in it → drop.
776
+ * 5. Author is a bot / system:
777
+ * a. blockedBotIds matches → drop.
778
+ * b. allowedBotIds set and not in list → drop.
779
+ * c. No allowedBotIds → drop all bots by default.
780
+ * 6. All checks passed → process.
781
+ */
782
+ shouldProcessEvent(message: DiscordMessage): boolean;
783
+ normalize(incoming: unknown): AgentInput;
784
+ /**
785
+ * Resolve a richer Participant (with displayName) from the Discord REST API.
786
+ *
787
+ * Successful results are cached in-process for the lifetime of the channel.
788
+ * Call invalidateParticipant(userId) when a `userUpdate` event fires.
789
+ *
790
+ * On API error or network failure, returns a bare `{ kind, id }` participant
791
+ * without caching so the next invocation will retry — matching the behavior
792
+ * of SlackChannel. Never throws.
793
+ */
794
+ resolveParticipant(input: AgentInput): Promise<Participant | undefined>;
795
+ /**
796
+ * Remove a cached participant entry.
797
+ * Call this when Discord fires a `userUpdate` event so the next interaction
798
+ * fetches fresh data from the API.
799
+ */
800
+ invalidateParticipant(userId: string): void;
801
+ send(output: AgentOutput): Promise<void>;
802
+ listen(): void;
803
+ stop(): Promise<void>;
804
+ }
805
+
806
+ /**
807
+ * Configuration options for EmailChannel.
808
+ */
809
+ interface EmailChannelConfig {
810
+ /** Optional name for the channel - required for sendTo() routing */
811
+ name?: string;
812
+ /** Sender email address */
813
+ from: string;
814
+ /** Recipient email address(es) - for scheduled/outbound emails */
815
+ to: string | string[];
816
+ /** SMTP configuration */
817
+ smtp: {
818
+ host: string;
819
+ port: number;
820
+ auth: {
821
+ user: string;
822
+ pass: string;
823
+ };
824
+ secure?: boolean;
825
+ };
826
+ /** Optional subject line template */
827
+ subject?: string;
828
+ }
829
+ /**
830
+ * Email channel for sending outbound emails via SMTP.
831
+ * This is an outbound-only channel - for inbound email handling,
832
+ * use a custom email-reader tool + WebhookChannel.
833
+ */
834
+ declare class EmailChannel extends BaseChannel {
835
+ readonly isTriggerChannel = true;
836
+ private config;
837
+ private transporter?;
838
+ constructor(config: EmailChannelConfig);
839
+ /**
840
+ * Initialize the email transporter.
841
+ * EmailChannel is outbound-only, so listen() just sets up the transporter.
842
+ */
843
+ listen(): void;
844
+ /**
845
+ * Send an email with the agent's output.
846
+ * @param output The agent output to send
847
+ */
848
+ send(output: AgentOutput): Promise<void>;
849
+ /**
850
+ * EmailChannel is outbound-only and doesn't receive messages.
851
+ * This method should not be called.
852
+ */
853
+ normalize(_incoming: unknown): never;
854
+ /**
855
+ * Format plain text as HTML for better email rendering.
856
+ */
857
+ private formatAsHtml;
858
+ /**
859
+ * Close the email transporter.
860
+ */
861
+ stop(): Promise<void>;
862
+ }
863
+
864
+ /**
865
+ * Configuration options for SMSChannel (Twilio).
866
+ */
867
+ interface SMSChannelConfig {
868
+ /** Optional name for the channel - required for sendTo() routing */
869
+ name?: string;
870
+ /** Twilio Account SID */
871
+ accountSid: string;
872
+ /** Twilio Auth Token */
873
+ authToken: string;
874
+ /** Twilio phone number (sender) */
875
+ from: string;
876
+ /** Recipient phone number - for outbound/scheduled SMS */
877
+ to?: string;
878
+ /** Optional webhook path for inbound SMS (e.g., '/sms/webhook') */
879
+ webhookPath?: string;
880
+ /** Optional port for the HTTP server (default: 3000) */
881
+ port?: number;
882
+ }
883
+ /**
884
+ * SMS channel for Twilio integration.
885
+ * Can be configured as:
886
+ * - Two-way: Set webhookPath to receive inbound SMS and reply
887
+ * - Outbound-only: Set 'to' without webhookPath for scheduled/triggered SMS
888
+ */
889
+ declare class SMSChannel extends BaseChannel {
890
+ private config;
891
+ private twilioClient?;
892
+ private server?;
893
+ constructor(config: SMSChannelConfig);
894
+ /**
895
+ * Two-way when webhookPath is set, outbound-only otherwise.
896
+ */
897
+ get isTriggerChannel(): boolean;
898
+ /**
899
+ * Start listening for inbound SMS via Twilio webhook (if webhookPath is set).
900
+ */
901
+ listen(): void;
902
+ /**
903
+ * Send an SMS message.
904
+ * @param output The agent output to send
905
+ */
906
+ send(output: AgentOutput): Promise<void>;
907
+ /**
908
+ * Normalize a Twilio webhook payload into AgentInput.
909
+ * @param incoming Twilio webhook payload
910
+ * @returns Normalized AgentInput
911
+ */
912
+ normalize(incoming: unknown): AgentInput;
913
+ /**
914
+ * Start HTTP server to receive Twilio webhooks.
915
+ */
916
+ private startWebhookServer;
917
+ /**
918
+ * Handle incoming webhook requests from Twilio.
919
+ */
920
+ private handleWebhookRequest;
921
+ /**
922
+ * Stop the webhook server.
923
+ */
924
+ stop(): Promise<void>;
925
+ }
926
+
927
+ export { BaseChannel as B, type CreateJobOptions as C, DiscordChannel as D, EmailChannel as E, type JobStatus as J, SchedulerStore as S, TelegramChannel as T, WebhookChannel as W, type CreateJobResult as a, type DiscordChannelConfig as b, type EmailChannelConfig as c, SMSChannel as d, type SMSChannelConfig as e, ScheduledChannel as f, type ScheduledChannelConfig as g, type ScheduledJob as h, SlackChannel as i, type SlackChannelConfig as j, type TelegramChannelConfig as k, type WebhookChannelConfig as l };