@backendkit-labs/agent-web 0.5.1 → 0.6.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
@@ -1,708 +1,708 @@
1
- # @backendkit-labs/agent-web
2
-
3
- > **Status: `stable`** — Maintained and production-ready. `TriggerBus` (cron/webhook/BullMQ) is `experimental` and frozen during the pilot phase.
4
-
5
- HTTP/WebSocket server layer for [@backendkit-labs/agent-core](https://www.npmjs.com/package/@backendkit-labs/agent-core).
6
-
7
- Wraps `AgentEngine` in a Fastify 5 server with streaming WebSocket sessions, JWT authentication, Prometheus metrics, rate limiting, a real-time monitor bus, and a flexible trigger system (cron jobs, webhooks, BullMQ queues).
8
-
9
- [![npm](https://img.shields.io/npm/v/@backendkit-labs/agent-web)](https://www.npmjs.com/package/@backendkit-labs/agent-web)
10
-
11
- ## Table of contents
12
-
13
- - [Installation](#installation)
14
- - [Quick start](#quick-start)
15
- - [AgentServer options](#agentserver-options)
16
- - [HTTP and WebSocket routes](#http-and-websocket-routes)
17
- - [WebSocket protocol](#websocket-protocol)
18
- - [EngineFactory pattern](#enginefactory-pattern)
19
- - [SessionManager](#sessionmanager)
20
- - [JWT authentication](#jwt-authentication)
21
- - [Prometheus metrics](#prometheus-metrics)
22
- - [Rate limiting](#rate-limiting)
23
- - [Session metadata store](#session-metadata-store)
24
- - [MonitorBus](#monitorbus)
25
- - [TriggerBus — cron, webhooks, queues](#triggerbus--cron-webhooks-queues)
26
- - [HeadlessTransport](#headlesstransport)
27
- - [Full production example](#full-production-example)
28
-
29
- ---
30
-
31
- ## Installation
32
-
33
- ```bash
34
- npm install @backendkit-labs/agent-web @backendkit-labs/agent-core
35
- ```
36
-
37
- Optional integrations (install only what you need):
38
-
39
- ```bash
40
- npm install jsonwebtoken # JWT auth
41
- npm install prom-client # Prometheus metrics
42
- npm install @fastify/rate-limit # rate limiting
43
- npm install node-cron # cron triggers
44
- npm install bullmq # queue triggers
45
- npm install ioredis # Redis session store
46
- ```
47
-
48
- ---
49
-
50
- ## Quick start
51
-
52
- ```typescript
53
- import { AgentServer } from '@backendkit-labs/agent-web';
54
- import type { EngineFactory } from '@backendkit-labs/agent-web';
55
- import { AgentEngine, AgentRegistry, ToolRegistry, ProviderRegistry } from '@backendkit-labs/agent-core';
56
-
57
- const engineFactory: EngineFactory = (sessionId, transport) =>
58
- new AgentEngine({
59
- transport,
60
- sessionId,
61
- // ...your engine config
62
- });
63
-
64
- const server = new AgentServer({
65
- port: 3000,
66
- engineFactory,
67
- cors: '*',
68
- });
69
-
70
- await server.start();
71
- // WS /chat — streaming chat sessions
72
- // WS /monitor — real-time read-only observer
73
- // GET /health — { status, sessions, monitors }
74
- // GET /sessions — active session list
75
- // DELETE /sessions/:id — close a session
76
- ```
77
-
78
- ---
79
-
80
- ## AgentServer options
81
-
82
- ```typescript
83
- interface AgentServerOptions {
84
- // Required
85
- engineFactory: EngineFactory; // creates a new AgentEngine per session
86
-
87
- // Network
88
- port?: number; // default: 3000
89
- host?: string; // default: '0.0.0.0'
90
- cors?: string | string[]; // default: '*'
91
- logger?: boolean | object; // Fastify logger, default: false
92
-
93
- // Auth
94
- auth?: AuthHook; // called on every request before handlers
95
-
96
- // Rate limiting (requires @fastify/rate-limit)
97
- rateLimiting?: {
98
- max?: number; // max requests per IP per window, default: 100
99
- timeWindow?: string; // e.g. '1 minute', '30 seconds'
100
- };
101
-
102
- // Prometheus metrics (requires prom-client)
103
- metrics?: {
104
- enabled: boolean; // must be true to activate
105
- path?: string; // default: '/metrics'
106
- prefix?: string; // metric name prefix
107
- collectNodeMetrics?: boolean; // include Node.js process metrics
108
- };
109
-
110
- // Session management
111
- maxIdleMs?: number; // idle timeout before session is closed
112
- maxSessions?: number; // max concurrent sessions
113
- store?: SessionMetaStore; // durable metadata store (default: in-memory)
114
-
115
- // Triggers
116
- triggers?: TriggerBus; // wires webhook routes and starts cron jobs
117
- }
118
- ```
119
-
120
- ---
121
-
122
- ## HTTP and WebSocket routes
123
-
124
- | Method | Path | Description |
125
- |--------|------|-------------|
126
- | `GET` | `/health` | Server health: `{ status, sessions, monitors, uptime }` |
127
- | `GET` | `/sessions` | Array of active session metadata |
128
- | `DELETE` | `/sessions/:id` | Force-close a session |
129
- | `GET` | `/metrics` | Prometheus metrics (if enabled) |
130
- | `WS` | `/chat` | Streaming chat — one session per connection |
131
- | `WS` | `/monitor` | Read-only observer of all sessions |
132
- | `POST` | `/webhooks/:name` | Webhook trigger (registered via TriggerBus) |
133
-
134
- ---
135
-
136
- ## WebSocket protocol
137
-
138
- ### `/chat` — chat sessions
139
-
140
- **Client → server:**
141
-
142
- ```json
143
- { "type": "input", "content": "Your message here" }
144
- { "type": "abort" }
145
- ```
146
-
147
- **Server → client** (streamed `AgentEvent` frames):
148
-
149
- ```json
150
- { "type": "run_start", "sessionId": "abc-123" }
151
- { "type": "token", "content": "Hello" }
152
- { "type": "tool_call", "name": "search", "args": { "query": "..." } }
153
- { "type": "tool_result", "name": "search", "result": "..." }
154
- { "type": "agent_switch", "from": "general", "to": "security-reviewer" }
155
- { "type": "done", "final": "Full assistant response" }
156
- { "type": "error", "message": "Something went wrong" }
157
- ```
158
-
159
- ### `/monitor` — real-time observer
160
-
161
- Connect to observe all sessions without sending any messages:
162
-
163
- ```json
164
- { "type": "monitor_hello", "sessionIds": ["abc-123", "def-456"] }
165
- { "type": "monitor_session_open", "sessionId": "ghi-789" }
166
- { "type": "monitor_session_close", "sessionId": "ghi-789" }
167
- { "sessionId": "abc-123", "type": "run_input", "content": "..." }
168
- { "sessionId": "abc-123", "type": "token", "content": "..." }
169
- { "sessionId": "abc-123", "type": "done", "final": "..." }
170
- ```
171
-
172
- ---
173
-
174
- ## EngineFactory pattern
175
-
176
- `EngineFactory` is a function `(sessionId: string, transport: Transport) => AgentEngine`. Each incoming WebSocket connection gets its own isolated engine instance.
177
-
178
- ### Minimal factory
179
-
180
- ```typescript
181
- import type { EngineFactory } from '@backendkit-labs/agent-web';
182
- import { AgentEngine } from '@backendkit-labs/agent-core';
183
-
184
- const engineFactory: EngineFactory = (sessionId, transport) =>
185
- new AgentEngine({
186
- transport,
187
- sessionId,
188
- model: { provider: 'openai', id: 'gpt-4o' },
189
- agents,
190
- tools,
191
- providers,
192
- defaultProvider: 'openai',
193
- defaultAgentId: 'general',
194
- maxIterations: 50,
195
- iterationMode: 'auto',
196
- });
197
- ```
198
-
199
- ### Factory with per-session memory
200
-
201
- ```typescript
202
- import { createMemorySystem } from '@backendkit-labs/agent-core';
203
- import * as path from 'path';
204
- import * as os from 'os';
205
-
206
- const engineFactory: EngineFactory = (sessionId, transport) => {
207
- // Isolated memory per session
208
- const sessDir = path.join(os.homedir(), '.bk-agent', 'sessions', sessionId);
209
- const memory = createMemorySystem({
210
- episodic: { provider: 'json', path: path.join(sessDir, 'episodes.json') },
211
- semantic: { provider: 'json', path: path.join(sessDir, 'knowledge.json') },
212
- procedural:{ provider: 'json', path: path.join(sessDir, 'patterns.json') },
213
- });
214
-
215
- return new AgentEngine({
216
- transport,
217
- sessionId,
218
- memorySystem: memory,
219
- ...baseConfig,
220
- });
221
- };
222
- ```
223
-
224
- ### Factory with userId from JWT
225
-
226
- When JWT auth is enabled, the `sessionId` carries the authenticated user's context. You can also inject user-specific data:
227
-
228
- ```typescript
229
- const engineFactory: EngineFactory = (sessionId, transport) => {
230
- // sessionId is set to the JWT 'sub' claim when JwtAuth is used
231
- const userConfig = getUserConfig(sessionId); // look up per-user settings
232
-
233
- return new AgentEngine({
234
- transport,
235
- sessionId,
236
- projectContext: `User: ${userConfig.name}\nRole: ${userConfig.role}`,
237
- ...baseConfig,
238
- });
239
- };
240
- ```
241
-
242
- ---
243
-
244
- ## SessionManager
245
-
246
- `SessionManager` creates, tracks, and cleans up sessions. It is created internally by `AgentServer`, but can also be used standalone.
247
-
248
- ```typescript
249
- import { SessionManager } from '@backendkit-labs/agent-web';
250
-
251
- const sessions = new SessionManager({
252
- engineFactory,
253
- maxIdleMs: 30 * 60 * 1000, // close after 30 min of inactivity
254
- maxSessions: 100,
255
- monitorBus: myMonitorBus,
256
- metrics: myMetricsCollector,
257
- store: myMetaStore,
258
- });
259
-
260
- // Lifecycle
261
- const session = await sessions.getOrCreate('session-id', myTransport);
262
- await sessions.close('session-id');
263
- const all = sessions.list(); // SessionMeta[]
264
- ```
265
-
266
- ---
267
-
268
- ## JWT authentication
269
-
270
- `JwtAuth` validates Bearer tokens in the `Authorization` header or `?token=` query parameter (useful for browser WebSocket connections that can't set custom headers).
271
-
272
- ### Setup
273
-
274
- ```typescript
275
- import { AgentServer, JwtAuth } from '@backendkit-labs/agent-web';
276
-
277
- const jwt = new JwtAuth({
278
- secret: process.env.JWT_SECRET!, // or use publicKey for RS256
279
- algorithm: 'HS256', // default
280
- sessionIdClaim:'sub', // JWT claim to use as sessionId
281
- expiresIn: '24h',
282
- });
283
-
284
- const server = new AgentServer({
285
- port: 3000,
286
- engineFactory,
287
- auth: jwt.hook(), // AuthHook applied to all routes
288
- });
289
- ```
290
-
291
- ### Issuing tokens (your login endpoint)
292
-
293
- ```typescript
294
- import { JwtAuth } from '@backendkit-labs/agent-web';
295
-
296
- const jwt = new JwtAuth({ secret: process.env.JWT_SECRET! });
297
-
298
- // Issue a token for an authenticated user
299
- const token = jwt.sign({
300
- sub: 'user-123',
301
- role: 'admin',
302
- name: 'Alice',
303
- });
304
-
305
- // Return to client — they send it as:
306
- // Authorization: Bearer <token>
307
- // or ws://host/chat?token=<token>
308
- ```
309
-
310
- ### Per-user session isolation
311
-
312
- When `sessionIdClaim` is `'sub'`, every user gets their own session automatically. Two requests with the same token always hit the same session:
313
-
314
- ```typescript
315
- const jwt = new JwtAuth({
316
- secret: process.env.JWT_SECRET!,
317
- sessionIdClaim: 'sub', // user ID as session key
318
- });
319
- ```
320
-
321
- ### Custom auth hook
322
-
323
- If you use a different auth system (API keys, OAuth tokens), implement `AuthHook` directly:
324
-
325
- ```typescript
326
- import type { AuthHook } from '@backendkit-labs/agent-web';
327
-
328
- const apiKeyAuth: AuthHook = async (req, reply) => {
329
- const key = req.headers['x-api-key'];
330
- if (!key || !validApiKeys.has(key as string)) {
331
- reply.code(401).send({ error: 'Unauthorized' });
332
- }
333
- };
334
-
335
- const server = new AgentServer({ auth: apiKeyAuth, ...opts });
336
- ```
337
-
338
- ---
339
-
340
- ## Prometheus metrics
341
-
342
- Requires `prom-client` to be installed. Exposes a `/metrics` endpoint with standard Prometheus counters and histograms.
343
-
344
- ```typescript
345
- const server = new AgentServer({
346
- ...opts,
347
- metrics: {
348
- enabled: true,
349
- path: '/metrics', // default
350
- prefix: 'bk_agent_', // prepended to all metric names
351
- collectNodeMetrics: true, // include Node.js process metrics
352
- },
353
- });
354
- ```
355
-
356
- ### Exposed metrics
357
-
358
- | Metric | Type | Description |
359
- |--------|------|-------------|
360
- | `bk_agent_sessions_active` | Gauge | Currently open sessions |
361
- | `bk_agent_runs_total{status}` | Counter | Total runs by status (`success`, `error`) |
362
- | `bk_agent_run_duration_seconds` | Histogram | Run latency distribution |
363
- | Node.js metrics | Various | Memory, CPU, GC (when `collectNodeMetrics: true`) |
364
-
365
- ### Using MetricsCollector directly
366
-
367
- ```typescript
368
- import { MetricsCollector } from '@backendkit-labs/agent-web';
369
-
370
- const collector = new MetricsCollector({ prefix: 'my_agent_' });
371
-
372
- // Record a completed run
373
- const endRun = collector.startRun('session-abc');
374
- // ... after run completes:
375
- endRun('success'); // or endRun('error')
376
-
377
- // Get Prometheus text format
378
- const text = await collector.getMetrics();
379
- // # HELP my_agent_runs_total ...
380
- // my_agent_runs_total{status="success"} 42
381
- ```
382
-
383
- ---
384
-
385
- ## Rate limiting
386
-
387
- Requires `@fastify/rate-limit`. Applied globally to all routes:
388
-
389
- ```typescript
390
- const server = new AgentServer({
391
- ...opts,
392
- rateLimiting: {
393
- max: 60, // max 60 requests per IP
394
- timeWindow: '1 minute', // sliding window
395
- },
396
- });
397
- ```
398
-
399
- When the limit is exceeded, the server responds with:
400
- ```json
401
- { "statusCode": 429, "error": "Too Many Requests", "message": "Rate limit exceeded" }
402
- ```
403
-
404
- ---
405
-
406
- ## Session metadata store
407
-
408
- By default, session metadata lives in memory and is lost on restart. Use `RedisSessionMetaStore` for durable metadata across restarts and horizontal scaling.
409
-
410
- ### In-memory (default)
411
-
412
- ```typescript
413
- import { InMemorySessionMetaStore } from '@backendkit-labs/agent-web';
414
-
415
- const server = new AgentServer({
416
- ...opts,
417
- store: new InMemorySessionMetaStore(),
418
- });
419
- ```
420
-
421
- ### Redis store (requires ioredis or node-redis)
422
-
423
- `RedisSessionMetaStore` is duck-typed — it accepts any Redis client that implements `.get()`, `.set()`, and `.del()`:
424
-
425
- ```typescript
426
- import { RedisSessionMetaStore } from '@backendkit-labs/agent-web';
427
- import Redis from 'ioredis';
428
-
429
- const redis = new Redis(process.env.REDIS_URL!);
430
-
431
- const server = new AgentServer({
432
- ...opts,
433
- store: new RedisSessionMetaStore(redis, {
434
- ttlSeconds: 86400, // keep session metadata for 24h
435
- prefix: 'bk:sess:', // key prefix
436
- }),
437
- });
438
- ```
439
-
440
- ---
441
-
442
- ## MonitorBus
443
-
444
- `MonitorBus` broadcasts all session events to connected `/monitor` WebSocket observers. Used by the Kanban board in `@backendkit-labs/agent-web-ui`.
445
-
446
- ### Standalone usage
447
-
448
- ```typescript
449
- import { MonitorBus } from '@backendkit-labs/agent-web';
450
-
451
- const monitor = new MonitorBus();
452
-
453
- // Subscribe to all events (e.g. for logging or alerting)
454
- const unsub = monitor.subscribe((frame) => {
455
- if (frame.type === 'done') {
456
- console.log(`[${frame.sessionId}] run completed`);
457
- }
458
- if (frame.type === 'error') {
459
- alerting.notify(`Agent error in session ${frame.sessionId}: ${frame.message}`);
460
- }
461
- });
462
-
463
- // Clean up
464
- unsub();
465
- ```
466
-
467
- ---
468
-
469
- ## TriggerBus — cron, webhooks, queues
470
-
471
- `TriggerBus` lets autonomous agent runs happen without a WebSocket client. Runs are visible in the Kanban monitor.
472
-
473
- ### Cron trigger (requires `node-cron`)
474
-
475
- ```typescript
476
- import { TriggerBus } from '@backendkit-labs/agent-web';
477
-
478
- const triggers = new TriggerBus(engineFactory)
479
- .addCron({
480
- name: 'daily-report',
481
- schedule: '0 8 * * 1-5', // weekdays at 8 AM
482
- buildPrompt: () => 'Generate the daily engineering standup summary from recent commits.',
483
- runOnStart: false, // set true to fire immediately on server start
484
- onResult: (out) => slack.send('#daily', out),
485
- onError: (err) => logger.error('[daily-report]', err),
486
- })
487
- .addCron({
488
- name: 'health-check',
489
- schedule: '*/15 * * * *', // every 15 minutes
490
- buildPrompt: () => 'Check all API endpoints and report any that are down.',
491
- onResult: (out) => { if (out.includes('DOWN')) pagerduty.alert(out); },
492
- });
493
-
494
- const server = new AgentServer({ triggers, ...opts });
495
- await server.start();
496
- // Cron jobs start automatically with the server
497
- ```
498
-
499
- ### Webhook trigger
500
-
501
- ```typescript
502
- const triggers = new TriggerBus(engineFactory)
503
- .addWebhook({
504
- name: 'github-pr',
505
- path: '/webhooks/github', // registered as POST /webhooks/github
506
- secret: process.env.GITHUB_SECRET, // HMAC X-Hub-Signature-256 validation (optional)
507
- buildPrompt: (body) => {
508
- const pr = body.pull_request;
509
- return `Review PR #${pr.number}: "${pr.title}"\n${pr.diff_url}`;
510
- },
511
- onResult: (out, body) => github.postReview(body.pull_request.number, out),
512
- onError: (err) => logger.error('[github-pr]', err),
513
- })
514
- .addWebhook({
515
- name: 'jira-issue',
516
- path: '/webhooks/jira',
517
- buildPrompt: (body) => `Analyze Jira issue ${body.issue.key}: ${body.issue.fields.summary}`,
518
- onResult: (out) => jira.addComment(body.issue.key, out),
519
- });
520
- ```
521
-
522
- **Testing a webhook:**
523
-
524
- ```bash
525
- curl -X POST http://localhost:3000/webhooks/github \
526
- -H "Content-Type: application/json" \
527
- -H "X-Hub-Signature-256: sha256=<hmac>" \
528
- -d '{ "pull_request": { "number": 42, "title": "Add auth module" } }'
529
- ```
530
-
531
- ### Queue trigger (BullMQ — requires `bullmq`)
532
-
533
- ```typescript
534
- import { TriggerBus } from '@backendkit-labs/agent-web';
535
-
536
- const triggers = new TriggerBus(engineFactory)
537
- .addQueue({
538
- name: 'document-processor',
539
- queueName: 'documents', // BullMQ queue name
540
- connection: process.env.REDIS_URL!, // Redis URL or connection object
541
- concurrency: 3, // process 3 jobs in parallel
542
- buildPrompt: (job) => {
543
- return `Summarize and extract key points from this document:\n\n${job.data.content}`;
544
- },
545
- onResult: (out, job) => {
546
- db.documents.update(job.data.id, { summary: out });
547
- },
548
- onError: (err, job) => {
549
- logger.error(`[queue] Failed to process document ${job?.data?.id}:`, err);
550
- // The error is re-thrown so BullMQ marks the job as failed and applies retry policy
551
- },
552
- });
553
- ```
554
-
555
- **Publishing a job (from anywhere in your system):**
556
-
557
- ```typescript
558
- import { Queue } from 'bullmq';
559
-
560
- const queue = new Queue('documents', { connection: process.env.REDIS_URL! });
561
-
562
- await queue.add('process-doc', {
563
- id: 'doc-456',
564
- content: 'Full document text...',
565
- }, {
566
- attempts: 3,
567
- backoff: { type: 'exponential', delay: 2000 },
568
- removeOnComplete: 100,
569
- removeOnFail: 50,
570
- });
571
- ```
572
-
573
- ### Queue connection options
574
-
575
- ```typescript
576
- // URL string
577
- connection: 'redis://localhost:6379'
578
- connection: 'redis://:password@redis-host:6380/1'
579
-
580
- // Connection object
581
- connection: {
582
- host: 'redis-host',
583
- port: 6380,
584
- password: 'mypassword',
585
- username: 'default',
586
- db: 1,
587
- tls: {}, // enable TLS
588
- }
589
- ```
590
-
591
- ### CronTriggerDef with all options
592
-
593
- ```typescript
594
- interface CronTriggerDef {
595
- name: string; // unique name for the trigger
596
- schedule: string; // standard cron expression
597
- buildPrompt: (ts: Date) => string; // returns the prompt to run
598
- agentId?: string; // override default agent
599
- runOnStart?: boolean; // fire immediately on server start
600
- onResult?: (output: string) => void;
601
- onError?: (err: Error) => void;
602
- }
603
- ```
604
-
605
- ---
606
-
607
- ## HeadlessTransport
608
-
609
- `HeadlessTransport` is a `Transport` implementation that buffers all `AgentEvent` objects in memory instead of streaming them over a socket. Used internally by `TriggerBus` for autonomous runs.
610
-
611
- ```typescript
612
- import { HeadlessTransport } from '@backendkit-labs/agent-web';
613
-
614
- const transport = new HeadlessTransport();
615
-
616
- const engine = new AgentEngine({ ...opts, transport });
617
- await engine.run('Generate a daily report');
618
-
619
- // All events are buffered
620
- const events = transport.getEvents();
621
- const tokens = events
622
- .filter(e => e.type === 'chunk')
623
- .map(e => e.content)
624
- .join('');
625
-
626
- console.log('Final output:', tokens);
627
- ```
628
-
629
- ---
630
-
631
- ## Full production example
632
-
633
- A complete production server with JWT auth, Redis sessions, Prometheus metrics, cron jobs, webhook triggers, and BullMQ queue processing:
634
-
635
- ```typescript
636
- import { AgentServer, JwtAuth, TriggerBus, RedisSessionMetaStore } from '@backendkit-labs/agent-web';
637
- import { createEnterpriseSetup } from '@backendkit-labs/agent-enterprise';
638
- import { ProviderRegistry, DeepSeekProvider } from '@backendkit-labs/agent-core';
639
- import Redis from 'ioredis';
640
-
641
- // LLM providers
642
- const providers = new ProviderRegistry();
643
- providers.register('deepseek', new DeepSeekProvider({
644
- apiKey: process.env.DEEPSEEK_API_KEY!,
645
- model: 'deepseek-chat',
646
- }));
647
-
648
- // Enterprise setup (RAG + 6 agent profiles)
649
- const enterprise = createEnterpriseSetup({
650
- vaultPath: process.env.VAULT_PATH!,
651
- providers,
652
- defaultProvider: 'deepseek',
653
- });
654
- await enterprise.indexAll();
655
-
656
- // Redis
657
- const redis = new Redis(process.env.REDIS_URL!);
658
-
659
- // JWT auth
660
- const jwt = new JwtAuth({
661
- secret: process.env.JWT_SECRET!,
662
- sessionIdClaim: 'sub',
663
- });
664
-
665
- // Autonomous triggers
666
- const triggers = new TriggerBus(enterprise.engineFactory)
667
- .addCron({
668
- name: 'weekly-digest',
669
- schedule: '0 9 * * 1', // Mondays at 9 AM
670
- buildPrompt: () => 'Generate a weekly digest of all knowledge base changes.',
671
- onResult: (out) => slack.send('#general', out),
672
- })
673
- .addWebhook({
674
- name: 'support-ticket',
675
- path: '/webhooks/support',
676
- secret: process.env.WEBHOOK_SECRET,
677
- buildPrompt: (body) => `Handle support ticket #${body.id}: ${body.subject}\n${body.description}`,
678
- onResult: (out, body) => helpdesk.reply(body.id, out),
679
- })
680
- .addQueue({
681
- name: 'email-processor',
682
- queueName: 'emails',
683
- connection: process.env.REDIS_URL!,
684
- concurrency: 5,
685
- buildPrompt: (job) => `Draft a professional reply to this email:\n\n${job.data.body}`,
686
- onResult: (out, job) => emailService.send({ to: job.data.from, body: out }),
687
- });
688
-
689
- // Server
690
- const server = new AgentServer({
691
- port: 3000,
692
- engineFactory: enterprise.engineFactory,
693
- auth: jwt.hook(),
694
- store: new RedisSessionMetaStore(redis),
695
- cors: ['https://app.company.com'],
696
- logger: { level: 'info' },
697
- rateLimiting: { max: 60, timeWindow: '1 minute' },
698
- metrics: { enabled: true, prefix: 'bk_' },
699
- maxIdleMs: 30 * 60 * 1000,
700
- triggers,
701
- });
702
-
703
- await server.start();
704
- console.log('Agent server running on :3000');
705
-
706
- // Graceful shutdown
707
- process.on('SIGTERM', () => server.stop());
708
- ```
1
+ # @backendkit-labs/agent-web
2
+
3
+ > **Status: `stable`** — Maintained and production-ready. `TriggerBus` (cron/webhook/BullMQ) is `experimental` and frozen during the pilot phase.
4
+
5
+ HTTP/WebSocket server layer for [@backendkit-labs/agent-core](https://www.npmjs.com/package/@backendkit-labs/agent-core).
6
+
7
+ Wraps `AgentEngine` in a Fastify 5 server with streaming WebSocket sessions, JWT authentication, Prometheus metrics, rate limiting, a real-time monitor bus, and a flexible trigger system (cron jobs, webhooks, BullMQ queues).
8
+
9
+ [![npm](https://img.shields.io/npm/v/@backendkit-labs/agent-web)](https://www.npmjs.com/package/@backendkit-labs/agent-web)
10
+
11
+ ## Table of contents
12
+
13
+ - [Installation](#installation)
14
+ - [Quick start](#quick-start)
15
+ - [AgentServer options](#agentserver-options)
16
+ - [HTTP and WebSocket routes](#http-and-websocket-routes)
17
+ - [WebSocket protocol](#websocket-protocol)
18
+ - [EngineFactory pattern](#enginefactory-pattern)
19
+ - [SessionManager](#sessionmanager)
20
+ - [JWT authentication](#jwt-authentication)
21
+ - [Prometheus metrics](#prometheus-metrics)
22
+ - [Rate limiting](#rate-limiting)
23
+ - [Session metadata store](#session-metadata-store)
24
+ - [MonitorBus](#monitorbus)
25
+ - [TriggerBus — cron, webhooks, queues](#triggerbus--cron-webhooks-queues)
26
+ - [HeadlessTransport](#headlesstransport)
27
+ - [Full production example](#full-production-example)
28
+
29
+ ---
30
+
31
+ ## Installation
32
+
33
+ ```bash
34
+ npm install @backendkit-labs/agent-web @backendkit-labs/agent-core
35
+ ```
36
+
37
+ Optional integrations (install only what you need):
38
+
39
+ ```bash
40
+ npm install jsonwebtoken # JWT auth
41
+ npm install prom-client # Prometheus metrics
42
+ npm install @fastify/rate-limit # rate limiting
43
+ npm install node-cron # cron triggers
44
+ npm install bullmq # queue triggers
45
+ npm install ioredis # Redis session store
46
+ ```
47
+
48
+ ---
49
+
50
+ ## Quick start
51
+
52
+ ```typescript
53
+ import { AgentServer } from '@backendkit-labs/agent-web';
54
+ import type { EngineFactory } from '@backendkit-labs/agent-web';
55
+ import { AgentEngine, AgentRegistry, ToolRegistry, ProviderRegistry } from '@backendkit-labs/agent-core';
56
+
57
+ const engineFactory: EngineFactory = (sessionId, transport) =>
58
+ new AgentEngine({
59
+ transport,
60
+ sessionId,
61
+ // ...your engine config
62
+ });
63
+
64
+ const server = new AgentServer({
65
+ port: 3000,
66
+ engineFactory,
67
+ cors: '*',
68
+ });
69
+
70
+ await server.start();
71
+ // WS /chat — streaming chat sessions
72
+ // WS /monitor — real-time read-only observer
73
+ // GET /health — { status, sessions, monitors }
74
+ // GET /sessions — active session list
75
+ // DELETE /sessions/:id — close a session
76
+ ```
77
+
78
+ ---
79
+
80
+ ## AgentServer options
81
+
82
+ ```typescript
83
+ interface AgentServerOptions {
84
+ // Required
85
+ engineFactory: EngineFactory; // creates a new AgentEngine per session
86
+
87
+ // Network
88
+ port?: number; // default: 3000
89
+ host?: string; // default: '127.0.0.1' (use '0.0.0.0', with auth, for remote callers)
90
+ cors?: string | string[]; // default: '*'
91
+ logger?: boolean | object; // Fastify logger, default: false
92
+
93
+ // Auth
94
+ auth?: AuthHook; // called on every request before handlers
95
+
96
+ // Rate limiting (requires @fastify/rate-limit)
97
+ rateLimiting?: {
98
+ max?: number; // max requests per IP per window, default: 100
99
+ timeWindow?: string; // e.g. '1 minute', '30 seconds'
100
+ };
101
+
102
+ // Prometheus metrics (requires prom-client)
103
+ metrics?: {
104
+ enabled: boolean; // must be true to activate
105
+ path?: string; // default: '/metrics'
106
+ prefix?: string; // metric name prefix
107
+ collectNodeMetrics?: boolean; // include Node.js process metrics
108
+ };
109
+
110
+ // Session management
111
+ maxIdleMs?: number; // idle timeout before session is closed
112
+ maxSessions?: number; // max concurrent sessions
113
+ store?: SessionMetaStore; // durable metadata store (default: in-memory)
114
+
115
+ // Triggers
116
+ triggers?: TriggerBus; // wires webhook routes and starts cron jobs
117
+ }
118
+ ```
119
+
120
+ ---
121
+
122
+ ## HTTP and WebSocket routes
123
+
124
+ | Method | Path | Description |
125
+ |--------|------|-------------|
126
+ | `GET` | `/health` | Server health: `{ status, sessions, monitors, uptime }` |
127
+ | `GET` | `/sessions` | Array of active session metadata |
128
+ | `DELETE` | `/sessions/:id` | Force-close a session |
129
+ | `GET` | `/metrics` | Prometheus metrics (if enabled) |
130
+ | `WS` | `/chat` | Streaming chat — one session per connection |
131
+ | `WS` | `/monitor` | Read-only observer of all sessions |
132
+ | `POST` | `/webhooks/:name` | Webhook trigger (registered via TriggerBus) |
133
+
134
+ ---
135
+
136
+ ## WebSocket protocol
137
+
138
+ ### `/chat` — chat sessions
139
+
140
+ **Client → server:**
141
+
142
+ ```json
143
+ { "type": "input", "content": "Your message here" }
144
+ { "type": "abort" }
145
+ ```
146
+
147
+ **Server → client** (streamed `AgentEvent` frames):
148
+
149
+ ```json
150
+ { "type": "run_start", "sessionId": "abc-123" }
151
+ { "type": "token", "content": "Hello" }
152
+ { "type": "tool_call", "name": "search", "args": { "query": "..." } }
153
+ { "type": "tool_result", "name": "search", "result": "..." }
154
+ { "type": "agent_switch", "from": "general", "to": "security-reviewer" }
155
+ { "type": "done", "final": "Full assistant response" }
156
+ { "type": "error", "message": "Something went wrong" }
157
+ ```
158
+
159
+ ### `/monitor` — real-time observer
160
+
161
+ Connect to observe all sessions without sending any messages:
162
+
163
+ ```json
164
+ { "type": "monitor_hello", "sessionIds": ["abc-123", "def-456"] }
165
+ { "type": "monitor_session_open", "sessionId": "ghi-789" }
166
+ { "type": "monitor_session_close", "sessionId": "ghi-789" }
167
+ { "sessionId": "abc-123", "type": "run_input", "content": "..." }
168
+ { "sessionId": "abc-123", "type": "token", "content": "..." }
169
+ { "sessionId": "abc-123", "type": "done", "final": "..." }
170
+ ```
171
+
172
+ ---
173
+
174
+ ## EngineFactory pattern
175
+
176
+ `EngineFactory` is a function `(sessionId: string, transport: Transport) => AgentEngine`. Each incoming WebSocket connection gets its own isolated engine instance.
177
+
178
+ ### Minimal factory
179
+
180
+ ```typescript
181
+ import type { EngineFactory } from '@backendkit-labs/agent-web';
182
+ import { AgentEngine } from '@backendkit-labs/agent-core';
183
+
184
+ const engineFactory: EngineFactory = (sessionId, transport) =>
185
+ new AgentEngine({
186
+ transport,
187
+ sessionId,
188
+ model: { provider: 'openai', id: 'gpt-4o' },
189
+ agents,
190
+ tools,
191
+ providers,
192
+ defaultProvider: 'openai',
193
+ defaultAgentId: 'general',
194
+ maxIterations: 50,
195
+ iterationMode: 'auto',
196
+ });
197
+ ```
198
+
199
+ ### Factory with per-session memory
200
+
201
+ ```typescript
202
+ import { createMemorySystem } from '@backendkit-labs/agent-core';
203
+ import * as path from 'path';
204
+ import * as os from 'os';
205
+
206
+ const engineFactory: EngineFactory = (sessionId, transport) => {
207
+ // Isolated memory per session
208
+ const sessDir = path.join(os.homedir(), '.bk-agent', 'sessions', sessionId);
209
+ const memory = createMemorySystem({
210
+ episodic: { provider: 'json', path: path.join(sessDir, 'episodes.json') },
211
+ semantic: { provider: 'json', path: path.join(sessDir, 'knowledge.json') },
212
+ procedural:{ provider: 'json', path: path.join(sessDir, 'patterns.json') },
213
+ });
214
+
215
+ return new AgentEngine({
216
+ transport,
217
+ sessionId,
218
+ memorySystem: memory,
219
+ ...baseConfig,
220
+ });
221
+ };
222
+ ```
223
+
224
+ ### Factory with userId from JWT
225
+
226
+ When JWT auth is enabled, the `sessionId` carries the authenticated user's context. You can also inject user-specific data:
227
+
228
+ ```typescript
229
+ const engineFactory: EngineFactory = (sessionId, transport) => {
230
+ // sessionId is set to the JWT 'sub' claim when JwtAuth is used
231
+ const userConfig = getUserConfig(sessionId); // look up per-user settings
232
+
233
+ return new AgentEngine({
234
+ transport,
235
+ sessionId,
236
+ projectContext: `User: ${userConfig.name}\nRole: ${userConfig.role}`,
237
+ ...baseConfig,
238
+ });
239
+ };
240
+ ```
241
+
242
+ ---
243
+
244
+ ## SessionManager
245
+
246
+ `SessionManager` creates, tracks, and cleans up sessions. It is created internally by `AgentServer`, but can also be used standalone.
247
+
248
+ ```typescript
249
+ import { SessionManager } from '@backendkit-labs/agent-web';
250
+
251
+ const sessions = new SessionManager({
252
+ engineFactory,
253
+ maxIdleMs: 30 * 60 * 1000, // close after 30 min of inactivity
254
+ maxSessions: 100,
255
+ monitorBus: myMonitorBus,
256
+ metrics: myMetricsCollector,
257
+ store: myMetaStore,
258
+ });
259
+
260
+ // Lifecycle
261
+ const session = await sessions.getOrCreate('session-id', myTransport);
262
+ await sessions.close('session-id');
263
+ const all = sessions.list(); // SessionMeta[]
264
+ ```
265
+
266
+ ---
267
+
268
+ ## JWT authentication
269
+
270
+ `JwtAuth` validates Bearer tokens in the `Authorization` header or `?token=` query parameter (useful for browser WebSocket connections that can't set custom headers).
271
+
272
+ ### Setup
273
+
274
+ ```typescript
275
+ import { AgentServer, JwtAuth } from '@backendkit-labs/agent-web';
276
+
277
+ const jwt = new JwtAuth({
278
+ secret: process.env.JWT_SECRET!, // or use publicKey for RS256
279
+ algorithm: 'HS256', // default
280
+ sessionIdClaim:'sub', // JWT claim to use as sessionId
281
+ expiresIn: '24h',
282
+ });
283
+
284
+ const server = new AgentServer({
285
+ port: 3000,
286
+ engineFactory,
287
+ auth: jwt.hook(), // AuthHook applied to all routes
288
+ });
289
+ ```
290
+
291
+ ### Issuing tokens (your login endpoint)
292
+
293
+ ```typescript
294
+ import { JwtAuth } from '@backendkit-labs/agent-web';
295
+
296
+ const jwt = new JwtAuth({ secret: process.env.JWT_SECRET! });
297
+
298
+ // Issue a token for an authenticated user
299
+ const token = jwt.sign({
300
+ sub: 'user-123',
301
+ role: 'admin',
302
+ name: 'Alice',
303
+ });
304
+
305
+ // Return to client — they send it as:
306
+ // Authorization: Bearer <token>
307
+ // or ws://host/chat?token=<token>
308
+ ```
309
+
310
+ ### Per-user session isolation
311
+
312
+ When `sessionIdClaim` is `'sub'`, every user gets their own session automatically. Two requests with the same token always hit the same session:
313
+
314
+ ```typescript
315
+ const jwt = new JwtAuth({
316
+ secret: process.env.JWT_SECRET!,
317
+ sessionIdClaim: 'sub', // user ID as session key
318
+ });
319
+ ```
320
+
321
+ ### Custom auth hook
322
+
323
+ If you use a different auth system (API keys, OAuth tokens), implement `AuthHook` directly:
324
+
325
+ ```typescript
326
+ import type { AuthHook } from '@backendkit-labs/agent-web';
327
+
328
+ const apiKeyAuth: AuthHook = async (req, reply) => {
329
+ const key = req.headers['x-api-key'];
330
+ if (!key || !validApiKeys.has(key as string)) {
331
+ reply.code(401).send({ error: 'Unauthorized' });
332
+ }
333
+ };
334
+
335
+ const server = new AgentServer({ auth: apiKeyAuth, ...opts });
336
+ ```
337
+
338
+ ---
339
+
340
+ ## Prometheus metrics
341
+
342
+ Requires `prom-client` to be installed. Exposes a `/metrics` endpoint with standard Prometheus counters and histograms.
343
+
344
+ ```typescript
345
+ const server = new AgentServer({
346
+ ...opts,
347
+ metrics: {
348
+ enabled: true,
349
+ path: '/metrics', // default
350
+ prefix: 'bk_agent_', // prepended to all metric names
351
+ collectNodeMetrics: true, // include Node.js process metrics
352
+ },
353
+ });
354
+ ```
355
+
356
+ ### Exposed metrics
357
+
358
+ | Metric | Type | Description |
359
+ |--------|------|-------------|
360
+ | `bk_agent_sessions_active` | Gauge | Currently open sessions |
361
+ | `bk_agent_runs_total{status}` | Counter | Total runs by status (`success`, `error`) |
362
+ | `bk_agent_run_duration_seconds` | Histogram | Run latency distribution |
363
+ | Node.js metrics | Various | Memory, CPU, GC (when `collectNodeMetrics: true`) |
364
+
365
+ ### Using MetricsCollector directly
366
+
367
+ ```typescript
368
+ import { MetricsCollector } from '@backendkit-labs/agent-web';
369
+
370
+ const collector = new MetricsCollector({ prefix: 'my_agent_' });
371
+
372
+ // Record a completed run
373
+ const endRun = collector.startRun('session-abc');
374
+ // ... after run completes:
375
+ endRun('success'); // or endRun('error')
376
+
377
+ // Get Prometheus text format
378
+ const text = await collector.getMetrics();
379
+ // # HELP my_agent_runs_total ...
380
+ // my_agent_runs_total{status="success"} 42
381
+ ```
382
+
383
+ ---
384
+
385
+ ## Rate limiting
386
+
387
+ Requires `@fastify/rate-limit`. Applied globally to all routes:
388
+
389
+ ```typescript
390
+ const server = new AgentServer({
391
+ ...opts,
392
+ rateLimiting: {
393
+ max: 60, // max 60 requests per IP
394
+ timeWindow: '1 minute', // sliding window
395
+ },
396
+ });
397
+ ```
398
+
399
+ When the limit is exceeded, the server responds with:
400
+ ```json
401
+ { "statusCode": 429, "error": "Too Many Requests", "message": "Rate limit exceeded" }
402
+ ```
403
+
404
+ ---
405
+
406
+ ## Session metadata store
407
+
408
+ By default, session metadata lives in memory and is lost on restart. Use `RedisSessionMetaStore` for durable metadata across restarts and horizontal scaling.
409
+
410
+ ### In-memory (default)
411
+
412
+ ```typescript
413
+ import { InMemorySessionMetaStore } from '@backendkit-labs/agent-web';
414
+
415
+ const server = new AgentServer({
416
+ ...opts,
417
+ store: new InMemorySessionMetaStore(),
418
+ });
419
+ ```
420
+
421
+ ### Redis store (requires ioredis or node-redis)
422
+
423
+ `RedisSessionMetaStore` is duck-typed — it accepts any Redis client that implements `.get()`, `.set()`, and `.del()`:
424
+
425
+ ```typescript
426
+ import { RedisSessionMetaStore } from '@backendkit-labs/agent-web';
427
+ import Redis from 'ioredis';
428
+
429
+ const redis = new Redis(process.env.REDIS_URL!);
430
+
431
+ const server = new AgentServer({
432
+ ...opts,
433
+ store: new RedisSessionMetaStore(redis, {
434
+ ttlSeconds: 86400, // keep session metadata for 24h
435
+ prefix: 'bk:sess:', // key prefix
436
+ }),
437
+ });
438
+ ```
439
+
440
+ ---
441
+
442
+ ## MonitorBus
443
+
444
+ `MonitorBus` broadcasts all session events to connected `/monitor` WebSocket observers. Used by the Kanban board in `@backendkit-labs/agent-web-ui`.
445
+
446
+ ### Standalone usage
447
+
448
+ ```typescript
449
+ import { MonitorBus } from '@backendkit-labs/agent-web';
450
+
451
+ const monitor = new MonitorBus();
452
+
453
+ // Subscribe to all events (e.g. for logging or alerting)
454
+ const unsub = monitor.subscribe((frame) => {
455
+ if (frame.type === 'done') {
456
+ console.log(`[${frame.sessionId}] run completed`);
457
+ }
458
+ if (frame.type === 'error') {
459
+ alerting.notify(`Agent error in session ${frame.sessionId}: ${frame.message}`);
460
+ }
461
+ });
462
+
463
+ // Clean up
464
+ unsub();
465
+ ```
466
+
467
+ ---
468
+
469
+ ## TriggerBus — cron, webhooks, queues
470
+
471
+ `TriggerBus` lets autonomous agent runs happen without a WebSocket client. Runs are visible in the Kanban monitor.
472
+
473
+ ### Cron trigger (requires `node-cron`)
474
+
475
+ ```typescript
476
+ import { TriggerBus } from '@backendkit-labs/agent-web';
477
+
478
+ const triggers = new TriggerBus(engineFactory)
479
+ .addCron({
480
+ name: 'daily-report',
481
+ schedule: '0 8 * * 1-5', // weekdays at 8 AM
482
+ buildPrompt: () => 'Generate the daily engineering standup summary from recent commits.',
483
+ runOnStart: false, // set true to fire immediately on server start
484
+ onResult: (out) => slack.send('#daily', out),
485
+ onError: (err) => logger.error('[daily-report]', err),
486
+ })
487
+ .addCron({
488
+ name: 'health-check',
489
+ schedule: '*/15 * * * *', // every 15 minutes
490
+ buildPrompt: () => 'Check all API endpoints and report any that are down.',
491
+ onResult: (out) => { if (out.includes('DOWN')) pagerduty.alert(out); },
492
+ });
493
+
494
+ const server = new AgentServer({ triggers, ...opts });
495
+ await server.start();
496
+ // Cron jobs start automatically with the server
497
+ ```
498
+
499
+ ### Webhook trigger
500
+
501
+ ```typescript
502
+ const triggers = new TriggerBus(engineFactory)
503
+ .addWebhook({
504
+ name: 'github-pr',
505
+ path: '/webhooks/github', // registered as POST /webhooks/github
506
+ secret: process.env.GITHUB_SECRET, // HMAC X-Hub-Signature-256 validation (optional)
507
+ buildPrompt: (body) => {
508
+ const pr = body.pull_request;
509
+ return `Review PR #${pr.number}: "${pr.title}"\n${pr.diff_url}`;
510
+ },
511
+ onResult: (out, body) => github.postReview(body.pull_request.number, out),
512
+ onError: (err) => logger.error('[github-pr]', err),
513
+ })
514
+ .addWebhook({
515
+ name: 'jira-issue',
516
+ path: '/webhooks/jira',
517
+ buildPrompt: (body) => `Analyze Jira issue ${body.issue.key}: ${body.issue.fields.summary}`,
518
+ onResult: (out) => jira.addComment(body.issue.key, out),
519
+ });
520
+ ```
521
+
522
+ **Testing a webhook:**
523
+
524
+ ```bash
525
+ curl -X POST http://localhost:3000/webhooks/github \
526
+ -H "Content-Type: application/json" \
527
+ -H "X-Hub-Signature-256: sha256=<hmac>" \
528
+ -d '{ "pull_request": { "number": 42, "title": "Add auth module" } }'
529
+ ```
530
+
531
+ ### Queue trigger (BullMQ — requires `bullmq`)
532
+
533
+ ```typescript
534
+ import { TriggerBus } from '@backendkit-labs/agent-web';
535
+
536
+ const triggers = new TriggerBus(engineFactory)
537
+ .addQueue({
538
+ name: 'document-processor',
539
+ queueName: 'documents', // BullMQ queue name
540
+ connection: process.env.REDIS_URL!, // Redis URL or connection object
541
+ concurrency: 3, // process 3 jobs in parallel
542
+ buildPrompt: (job) => {
543
+ return `Summarize and extract key points from this document:\n\n${job.data.content}`;
544
+ },
545
+ onResult: (out, job) => {
546
+ db.documents.update(job.data.id, { summary: out });
547
+ },
548
+ onError: (err, job) => {
549
+ logger.error(`[queue] Failed to process document ${job?.data?.id}:`, err);
550
+ // The error is re-thrown so BullMQ marks the job as failed and applies retry policy
551
+ },
552
+ });
553
+ ```
554
+
555
+ **Publishing a job (from anywhere in your system):**
556
+
557
+ ```typescript
558
+ import { Queue } from 'bullmq';
559
+
560
+ const queue = new Queue('documents', { connection: process.env.REDIS_URL! });
561
+
562
+ await queue.add('process-doc', {
563
+ id: 'doc-456',
564
+ content: 'Full document text...',
565
+ }, {
566
+ attempts: 3,
567
+ backoff: { type: 'exponential', delay: 2000 },
568
+ removeOnComplete: 100,
569
+ removeOnFail: 50,
570
+ });
571
+ ```
572
+
573
+ ### Queue connection options
574
+
575
+ ```typescript
576
+ // URL string
577
+ connection: 'redis://localhost:6379'
578
+ connection: 'redis://:password@redis-host:6380/1'
579
+
580
+ // Connection object
581
+ connection: {
582
+ host: 'redis-host',
583
+ port: 6380,
584
+ password: 'mypassword',
585
+ username: 'default',
586
+ db: 1,
587
+ tls: {}, // enable TLS
588
+ }
589
+ ```
590
+
591
+ ### CronTriggerDef with all options
592
+
593
+ ```typescript
594
+ interface CronTriggerDef {
595
+ name: string; // unique name for the trigger
596
+ schedule: string; // standard cron expression
597
+ buildPrompt: (ts: Date) => string; // returns the prompt to run
598
+ agentId?: string; // override default agent
599
+ runOnStart?: boolean; // fire immediately on server start
600
+ onResult?: (output: string) => void;
601
+ onError?: (err: Error) => void;
602
+ }
603
+ ```
604
+
605
+ ---
606
+
607
+ ## HeadlessTransport
608
+
609
+ `HeadlessTransport` is a `Transport` implementation that buffers all `AgentEvent` objects in memory instead of streaming them over a socket. Used internally by `TriggerBus` for autonomous runs.
610
+
611
+ ```typescript
612
+ import { HeadlessTransport } from '@backendkit-labs/agent-web';
613
+
614
+ const transport = new HeadlessTransport();
615
+
616
+ const engine = new AgentEngine({ ...opts, transport });
617
+ await engine.run('Generate a daily report');
618
+
619
+ // All events are buffered
620
+ const events = transport.getEvents();
621
+ const tokens = events
622
+ .filter(e => e.type === 'chunk')
623
+ .map(e => e.content)
624
+ .join('');
625
+
626
+ console.log('Final output:', tokens);
627
+ ```
628
+
629
+ ---
630
+
631
+ ## Full production example
632
+
633
+ A complete production server with JWT auth, Redis sessions, Prometheus metrics, cron jobs, webhook triggers, and BullMQ queue processing:
634
+
635
+ ```typescript
636
+ import { AgentServer, JwtAuth, TriggerBus, RedisSessionMetaStore } from '@backendkit-labs/agent-web';
637
+ import { createEnterpriseSetup } from '@backendkit-labs/agent-enterprise';
638
+ import { ProviderRegistry, DeepSeekProvider } from '@backendkit-labs/agent-core';
639
+ import Redis from 'ioredis';
640
+
641
+ // LLM providers
642
+ const providers = new ProviderRegistry();
643
+ providers.register('deepseek', new DeepSeekProvider({
644
+ apiKey: process.env.DEEPSEEK_API_KEY!,
645
+ model: 'deepseek-chat',
646
+ }));
647
+
648
+ // Enterprise setup (RAG + 6 agent profiles)
649
+ const enterprise = createEnterpriseSetup({
650
+ vaultPath: process.env.VAULT_PATH!,
651
+ providers,
652
+ defaultProvider: 'deepseek',
653
+ });
654
+ await enterprise.indexAll();
655
+
656
+ // Redis
657
+ const redis = new Redis(process.env.REDIS_URL!);
658
+
659
+ // JWT auth
660
+ const jwt = new JwtAuth({
661
+ secret: process.env.JWT_SECRET!,
662
+ sessionIdClaim: 'sub',
663
+ });
664
+
665
+ // Autonomous triggers
666
+ const triggers = new TriggerBus(enterprise.engineFactory)
667
+ .addCron({
668
+ name: 'weekly-digest',
669
+ schedule: '0 9 * * 1', // Mondays at 9 AM
670
+ buildPrompt: () => 'Generate a weekly digest of all knowledge base changes.',
671
+ onResult: (out) => slack.send('#general', out),
672
+ })
673
+ .addWebhook({
674
+ name: 'support-ticket',
675
+ path: '/webhooks/support',
676
+ secret: process.env.WEBHOOK_SECRET,
677
+ buildPrompt: (body) => `Handle support ticket #${body.id}: ${body.subject}\n${body.description}`,
678
+ onResult: (out, body) => helpdesk.reply(body.id, out),
679
+ })
680
+ .addQueue({
681
+ name: 'email-processor',
682
+ queueName: 'emails',
683
+ connection: process.env.REDIS_URL!,
684
+ concurrency: 5,
685
+ buildPrompt: (job) => `Draft a professional reply to this email:\n\n${job.data.body}`,
686
+ onResult: (out, job) => emailService.send({ to: job.data.from, body: out }),
687
+ });
688
+
689
+ // Server
690
+ const server = new AgentServer({
691
+ port: 3000,
692
+ engineFactory: enterprise.engineFactory,
693
+ auth: jwt.hook(),
694
+ store: new RedisSessionMetaStore(redis),
695
+ cors: ['https://app.company.com'],
696
+ logger: { level: 'info' },
697
+ rateLimiting: { max: 60, timeWindow: '1 minute' },
698
+ metrics: { enabled: true, prefix: 'bk_' },
699
+ maxIdleMs: 30 * 60 * 1000,
700
+ triggers,
701
+ });
702
+
703
+ await server.start();
704
+ console.log('Agent server running on :3000');
705
+
706
+ // Graceful shutdown
707
+ process.on('SIGTERM', () => server.stop());
708
+ ```