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