@backendkit-labs/agent-web 0.6.5 → 0.6.6

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 -613
  2. package/package.json +51 -51
package/README.md CHANGED
@@ -1,613 +1,613 @@
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
- ```
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
+ ```
package/package.json CHANGED
@@ -1,51 +1,51 @@
1
- {
2
- "name": "@backendkit-labs/agent-web",
3
- "version": "0.6.5",
4
- "description": "HTTP/WebSocket server layer for agent-core — browser-ready streaming agent sessions",
5
- "main": "dist/index.js",
6
- "types": "dist/index.d.ts",
7
- "files": [
8
- "dist/",
9
- "client/",
10
- "README.md"
11
- ],
12
- "scripts": {
13
- "build": "tsc",
14
- "dev": "tsc --watch",
15
- "typecheck": "tsc --noEmit",
16
- "test": "jest",
17
- "prepublishOnly": "npm run build"
18
- },
19
- "peerDependencies": {
20
- "@backendkit-labs/agent-core": ">=0.24.0 <0.40.0"
21
- },
22
- "dependencies": {
23
- "@fastify/websocket": "^11.0.0",
24
- "fastify": "^5.0.0",
25
- "jsonwebtoken": "^9.0.0",
26
- "prom-client": "^15.0.0"
27
- },
28
- "optionalDependencies": {
29
- "@fastify/rate-limit": "^9.0.0",
30
- "bullmq": "^5.0.0",
31
- "node-cron": "^4.2.1"
32
- },
33
- "devDependencies": {
34
- "@backendkit-labs/agent-core": "^0.38.0",
35
- "@types/jest": "^29.5.14",
36
- "@types/jsonwebtoken": "^9.0.0",
37
- "@types/node": "^20.0.0",
38
- "@types/ws": "^8.0.0",
39
- "bullmq": "^5.0.0",
40
- "jest": "^29.7.0",
41
- "ts-jest": "^29.4.11",
42
- "typescript": "^5.4.0",
43
- "ws": "^8.21.0"
44
- },
45
- "engines": {
46
- "node": ">=20.0.0"
47
- },
48
- "publishConfig": {
49
- "access": "public"
50
- }
51
- }
1
+ {
2
+ "name": "@backendkit-labs/agent-web",
3
+ "version": "0.6.6",
4
+ "description": "HTTP/WebSocket server layer for agent-core — browser-ready streaming agent sessions",
5
+ "main": "dist/index.js",
6
+ "types": "dist/index.d.ts",
7
+ "files": [
8
+ "dist/",
9
+ "client/",
10
+ "README.md"
11
+ ],
12
+ "scripts": {
13
+ "build": "tsc",
14
+ "dev": "tsc --watch",
15
+ "typecheck": "tsc --noEmit",
16
+ "test": "jest",
17
+ "prepublishOnly": "npm run build"
18
+ },
19
+ "peerDependencies": {
20
+ "@backendkit-labs/agent-core": ">=0.24.0 <0.41.0"
21
+ },
22
+ "dependencies": {
23
+ "@fastify/websocket": "^11.0.0",
24
+ "fastify": "^5.0.0",
25
+ "jsonwebtoken": "^9.0.0",
26
+ "prom-client": "^15.0.0"
27
+ },
28
+ "optionalDependencies": {
29
+ "@fastify/rate-limit": "^9.0.0",
30
+ "bullmq": "^5.0.0",
31
+ "node-cron": "^4.2.1"
32
+ },
33
+ "devDependencies": {
34
+ "@backendkit-labs/agent-core": "^0.39.0",
35
+ "@types/jest": "^29.5.14",
36
+ "@types/jsonwebtoken": "^9.0.0",
37
+ "@types/node": "^20.0.0",
38
+ "@types/ws": "^8.0.0",
39
+ "bullmq": "^5.0.0",
40
+ "jest": "^29.7.0",
41
+ "ts-jest": "^29.4.11",
42
+ "typescript": "^5.4.0",
43
+ "ws": "^8.21.0"
44
+ },
45
+ "engines": {
46
+ "node": ">=20.0.0"
47
+ },
48
+ "publishConfig": {
49
+ "access": "public"
50
+ }
51
+ }