@backendkit-labs/agent-web 0.6.4 → 0.6.5

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