agents 0.16.1 → 0.17.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (127) hide show
  1. package/README.md +11 -8
  2. package/dist/{agent-tool-types-NofdbL9X.d.ts → agent-tool-types-Cd1TZPfB.d.ts} +595 -137
  3. package/dist/agent-tool-types.d.ts +34 -18
  4. package/dist/agent-tool-types.js +20 -1
  5. package/dist/agent-tool-types.js.map +1 -0
  6. package/dist/agent-tools-BXlsuX0d.js +304 -0
  7. package/dist/agent-tools-BXlsuX0d.js.map +1 -0
  8. package/dist/agent-tools-_E8wxUIK.d.ts +119 -0
  9. package/dist/agent-tools.d.ts +24 -18
  10. package/dist/agent-tools.js.map +1 -1
  11. package/dist/ai-chat-agent.d.ts +1 -1
  12. package/dist/ai-chat-agent.js +1 -2
  13. package/dist/ai-chat-agent.js.map +1 -1
  14. package/dist/ai-chat-v5-migration.d.ts +1 -1
  15. package/dist/ai-chat-v5-migration.js +1 -2
  16. package/dist/ai-chat-v5-migration.js.map +1 -1
  17. package/dist/ai-react.d.ts +1 -1
  18. package/dist/ai-react.js +1 -2
  19. package/dist/ai-react.js.map +1 -1
  20. package/dist/ai-types.d.ts +1 -7
  21. package/dist/ai-types.js +2 -8
  22. package/dist/ai-types.js.map +1 -1
  23. package/dist/browser/ai.js +1 -1
  24. package/dist/browser/index.js +1 -1
  25. package/dist/chat/index.d.ts +1923 -76
  26. package/dist/chat/index.js +1784 -278
  27. package/dist/chat/index.js.map +1 -1
  28. package/dist/chat/react.d.ts +602 -0
  29. package/dist/chat/react.js +1506 -0
  30. package/dist/chat/react.js.map +1 -0
  31. package/dist/chat-sdk/index.d.ts +4 -4
  32. package/dist/{classPrivateFieldGet2-CZ7QjTXN.js → classPrivateFieldGet2-DZBYAB34.js} +5 -5
  33. package/dist/{classPrivateMethodInitSpec-D-0__zd9.js → classPrivateMethodInitSpec-qMjJ6sHQ.js} +2 -2
  34. package/dist/{client-FUizKzj2.js → client-BZ-B3NhC.js} +19 -3
  35. package/dist/client-BZ-B3NhC.js.map +1 -0
  36. package/dist/client-tools-aIBO0Fk7.d.ts +53 -0
  37. package/dist/client.d.ts +76 -57
  38. package/dist/client.js +33 -5
  39. package/dist/client.js.map +1 -1
  40. package/dist/{compaction-helpers-DVcu5lPN.d.ts → compaction-helpers-wUz6M3us.d.ts} +20 -1
  41. package/dist/{connector-CrKhowfD.js → connector-CdldGF3h.js} +5 -5
  42. package/dist/{connector-CrKhowfD.js.map → connector-CdldGF3h.js.map} +1 -1
  43. package/dist/email.js +1 -1
  44. package/dist/email.js.map +1 -1
  45. package/dist/experimental/memory/session/index.d.ts +1 -1
  46. package/dist/experimental/memory/session/index.js +20 -2
  47. package/dist/experimental/memory/session/index.js.map +1 -1
  48. package/dist/experimental/memory/utils/index.d.ts +1 -1
  49. package/dist/{index-B7IbEeze.d.ts → index-CcbnKkNh.d.ts} +188 -14
  50. package/dist/index.d.ts +91 -71
  51. package/dist/index.js +562 -24
  52. package/dist/index.js.map +1 -1
  53. package/dist/mcp/client.d.ts +18 -14
  54. package/dist/mcp/client.js +1 -1
  55. package/dist/mcp/index.d.ts +30 -30
  56. package/dist/mcp/index.js +7 -7
  57. package/dist/mcp/index.js.map +1 -1
  58. package/dist/{agent-tools-3zLG7MgA.js → message-builder-BymO4N_D.js} +45 -126
  59. package/dist/message-builder-BymO4N_D.js.map +1 -0
  60. package/dist/observability/index.d.ts +1 -1
  61. package/dist/observability/index.js +4 -2
  62. package/dist/observability/index.js.map +1 -1
  63. package/dist/react.d.ts +123 -110
  64. package/dist/react.js +44 -11
  65. package/dist/react.js.map +1 -1
  66. package/dist/serializable.d.ts +1 -1
  67. package/dist/skills/compile.js +1 -1
  68. package/dist/skills/compile.js.map +1 -1
  69. package/dist/skills/index.js +5 -5
  70. package/dist/skills/index.js.map +1 -1
  71. package/dist/sub-routing.d.ts +6 -6
  72. package/dist/utils.js +1 -1
  73. package/dist/utils.js.map +1 -1
  74. package/dist/vite.js +5 -5
  75. package/dist/vite.js.map +1 -1
  76. package/dist/wire-types-nflOzNuU.js +240 -0
  77. package/dist/wire-types-nflOzNuU.js.map +1 -0
  78. package/dist/{workflow-types-SrZK_o9p.d.ts → workflow-types-Baz_PO5v.d.ts} +42 -22
  79. package/dist/workflow-types.d.ts +25 -21
  80. package/dist/workflow-types.js.map +1 -1
  81. package/dist/workflows.d.ts +22 -21
  82. package/dist/workflows.js +31 -7
  83. package/dist/workflows.js.map +1 -1
  84. package/docs/adding-to-existing-project.md +450 -0
  85. package/docs/agent-class.md +503 -0
  86. package/docs/agent-tools.md +552 -0
  87. package/docs/browse-the-web.md +430 -0
  88. package/docs/callable-methods.md +627 -0
  89. package/docs/chat-agents.md +1687 -0
  90. package/docs/chat-sdk.md +181 -0
  91. package/docs/client-sdk.md +520 -0
  92. package/docs/client-tools-continuation.md +177 -0
  93. package/docs/codemode.md +440 -0
  94. package/docs/configuration.md +775 -0
  95. package/docs/cross-domain-authentication.md +171 -0
  96. package/docs/durable-execution.md +537 -0
  97. package/docs/email.md +663 -0
  98. package/docs/get-current-agent.md +204 -0
  99. package/docs/getting-started.md +305 -0
  100. package/docs/http-websockets.md +668 -0
  101. package/docs/human-in-the-loop.md +661 -0
  102. package/docs/index.md +151 -0
  103. package/docs/long-running-agents.md +730 -0
  104. package/docs/mcp-client.md +620 -0
  105. package/docs/mcp-servers.md +526 -0
  106. package/docs/mcp-transports.md +308 -0
  107. package/docs/migration-to-ai-sdk-v5.md +96 -0
  108. package/docs/migration-to-ai-sdk-v6.md +163 -0
  109. package/docs/observability.md +261 -0
  110. package/docs/push-notifications.md +367 -0
  111. package/docs/queue.md +329 -0
  112. package/docs/readonly-connections.md +278 -0
  113. package/docs/resumable-streaming.md +127 -0
  114. package/docs/retries.md +444 -0
  115. package/docs/routing.md +749 -0
  116. package/docs/scheduling.md +898 -0
  117. package/docs/securing-mcp-servers.md +359 -0
  118. package/docs/server-driven-messages.md +477 -0
  119. package/docs/sessions.md +1024 -0
  120. package/docs/state.md +512 -0
  121. package/docs/sub-agents.md +389 -0
  122. package/docs/webhooks.md +604 -0
  123. package/docs/workflows.md +877 -0
  124. package/package.json +47 -15
  125. package/dist/agent-tools-3zLG7MgA.js.map +0 -1
  126. package/dist/agent-tools-DLquv-dp.d.ts +0 -14
  127. package/dist/client-FUizKzj2.js.map +0 -1
@@ -0,0 +1,668 @@
1
+ # HTTP & WebSockets
2
+
3
+ Agents handle both HTTP requests and WebSocket connections, giving you flexibility to build REST APIs, real-time applications, or hybrid architectures.
4
+
5
+ ## Overview
6
+
7
+ Every agent can respond to:
8
+
9
+ - **HTTP requests** via `onRequest()` - REST APIs, webhooks, file uploads
10
+ - **WebSocket connections** via `onConnect()`, `onMessage()`, `onClose()` - Real-time bidirectional communication
11
+
12
+ ```typescript
13
+ import { Agent } from "agents";
14
+
15
+ export class MyAgent extends Agent {
16
+ // Handle HTTP requests
17
+ onRequest(request: Request): Response {
18
+ return new Response("Hello from HTTP!");
19
+ }
20
+
21
+ // Handle WebSocket connections
22
+ onConnect(connection: Connection, ctx: ConnectionContext) {
23
+ connection.send("Welcome!");
24
+ }
25
+
26
+ onMessage(connection: Connection, message: WSMessage) {
27
+ // Echo back
28
+ connection.send(message);
29
+ }
30
+ }
31
+ ```
32
+
33
+ ## Lifecycle Hooks
34
+
35
+ Agents have several lifecycle hooks that are called at different points:
36
+
37
+ | Hook | When Called |
38
+ | --------------------------------------------- | --------------------------------------------------------- |
39
+ | `onStart(props?)` | Once when the agent first starts (before any connections) |
40
+ | `onRequest(request)` | When an HTTP request is received (non-WebSocket) |
41
+ | `onConnect(connection, ctx)` | When a new WebSocket connection is established |
42
+ | `onMessage(connection, message)` | When a WebSocket message is received |
43
+ | `onClose(connection, code, reason, wasClean)` | When a WebSocket connection closes |
44
+ | `onError(connection, error)` | When a WebSocket error occurs |
45
+ | `onError(error)` | When a server error occurs (overloaded) |
46
+
47
+ ### Lifecycle Flow
48
+
49
+ ```
50
+ Agent Created
51
+
52
+ onStart() ←── Called once, before any connections
53
+
54
+ ┌─────────────────────────────────────┐
55
+ │ For each request: │
56
+ │ │
57
+ │ HTTP Request ──→ onRequest() │
58
+ │ │
59
+ │ WebSocket ──→ onConnect() │
60
+ │ ↓ │
61
+ │ Messages ──→ onMessage() (repeat) │
62
+ │ ↓ │
63
+ │ Disconnect ──→ onClose() │
64
+ └─────────────────────────────────────┘
65
+ ```
66
+
67
+ ## HTTP Requests
68
+
69
+ Handle HTTP requests with `onRequest()`. This is called for any non-WebSocket request to your agent.
70
+
71
+ ```typescript
72
+ export class ApiAgent extends Agent {
73
+ async onRequest(request: Request): Promise<Response> {
74
+ const url = new URL(request.url);
75
+
76
+ // Route by path
77
+ if (url.pathname.endsWith("/status")) {
78
+ return Response.json({
79
+ status: "ok",
80
+ connections: this.getConnections().length
81
+ });
82
+ }
83
+
84
+ if (url.pathname.endsWith("/data") && request.method === "POST") {
85
+ const data = await request.json();
86
+ // Process data...
87
+ return Response.json({ received: true });
88
+ }
89
+
90
+ return new Response("Not found", { status: 404 });
91
+ }
92
+ }
93
+ ```
94
+
95
+ ### Common HTTP Patterns
96
+
97
+ **REST API with CORS:**
98
+
99
+ ```typescript
100
+ onRequest(request: Request): Response {
101
+ // Handle preflight
102
+ if (request.method === "OPTIONS") {
103
+ return new Response(null, {
104
+ headers: {
105
+ "Access-Control-Allow-Origin": "*",
106
+ "Access-Control-Allow-Methods": "GET, POST, OPTIONS",
107
+ "Access-Control-Allow-Headers": "Content-Type"
108
+ }
109
+ });
110
+ }
111
+
112
+ return Response.json(
113
+ { data: "..." },
114
+ { headers: { "Access-Control-Allow-Origin": "*" } }
115
+ );
116
+ }
117
+ ```
118
+
119
+ **File Upload:**
120
+
121
+ ```typescript
122
+ async onRequest(request: Request): Promise<Response> {
123
+ if (request.method === "POST") {
124
+ const formData = await request.formData();
125
+ const file = formData.get("file") as File;
126
+
127
+ // Process file...
128
+ const content = await file.text();
129
+
130
+ return Response.json({ filename: file.name, size: file.size });
131
+ }
132
+ return new Response("Method not allowed", { status: 405 });
133
+ }
134
+ ```
135
+
136
+ ## WebSocket Connections
137
+
138
+ WebSockets enable real-time bidirectional communication between clients and your agent.
139
+
140
+ ### Connection Lifecycle
141
+
142
+ ```typescript
143
+ export class ChatAgent extends Agent {
144
+ // Called when a client connects
145
+ onConnect(connection: Connection, ctx: ConnectionContext) {
146
+ // ctx.request contains the original HTTP request (for auth, headers, etc.)
147
+ const url = new URL(ctx.request.url);
148
+ const token = url.searchParams.get("token");
149
+
150
+ console.log(`Client ${connection.id} connected`);
151
+ connection.send(JSON.stringify({ type: "welcome", id: connection.id }));
152
+ }
153
+
154
+ // Called for each message from this connection
155
+ onMessage(connection: Connection, message: WSMessage) {
156
+ if (typeof message === "string") {
157
+ const data = JSON.parse(message);
158
+ // Handle message...
159
+ } else {
160
+ // Binary message (ArrayBuffer)
161
+ }
162
+ }
163
+
164
+ // Called when connection closes
165
+ onClose(
166
+ connection: Connection,
167
+ code: number,
168
+ reason: string,
169
+ wasClean: boolean
170
+ ) {
171
+ console.log(`Client ${connection.id} disconnected: ${code} ${reason}`);
172
+ }
173
+
174
+ // Called on WebSocket errors
175
+ onError(connection: Connection, error: unknown) {
176
+ console.error(`Error on connection ${connection.id}:`, error);
177
+ }
178
+ }
179
+ ```
180
+
181
+ ### The Connection Object
182
+
183
+ Each WebSocket connection is represented by a `Connection` object:
184
+
185
+ ```typescript
186
+ interface Connection<TState = unknown> {
187
+ /** Unique connection identifier */
188
+ id: string;
189
+
190
+ /** The agent instance name this connection belongs to */
191
+ server: string;
192
+
193
+ /** Per-connection state (read-only, use setState to update) */
194
+ state: TState | null;
195
+
196
+ /** Update connection state */
197
+ setState(state: TState | ((prev: TState | null) => TState)): void;
198
+
199
+ /** Send a message to this connection */
200
+ send(message: string | ArrayBuffer): void;
201
+
202
+ /** Close this connection */
203
+ close(code?: number, reason?: string): void;
204
+ }
205
+ ```
206
+
207
+ ### Message Types
208
+
209
+ Messages can be strings or binary:
210
+
211
+ ```typescript
212
+ onMessage(connection: Connection, message: WSMessage) {
213
+ if (typeof message === "string") {
214
+ // Text message - usually JSON
215
+ const data = JSON.parse(message);
216
+ this.handleTextMessage(connection, data);
217
+ } else {
218
+ // Binary message - ArrayBuffer or ArrayBufferView
219
+ this.handleBinaryMessage(connection, message);
220
+ }
221
+ }
222
+ ```
223
+
224
+ ## Connection Management
225
+
226
+ ### Getting Connections
227
+
228
+ ```typescript
229
+ // Get all connections
230
+ const connections = this.getConnections();
231
+
232
+ // Get a specific connection by ID
233
+ const connection = this.getConnection("abc123");
234
+
235
+ // Get connections with a specific tag
236
+ const adminConnections = this.getConnections("admin");
237
+ ```
238
+
239
+ ### Broadcasting
240
+
241
+ Send a message to all connected clients:
242
+
243
+ ```typescript
244
+ // Broadcast to everyone
245
+ this.broadcast(JSON.stringify({ type: "update", data: "..." }));
246
+
247
+ // Broadcast to everyone except specific connections
248
+ this.broadcast(
249
+ JSON.stringify({ type: "user-typing", userId: "123" }),
250
+ ["connection-id-to-exclude"] // Don't send to the originator
251
+ );
252
+ ```
253
+
254
+ ### Connection Tags
255
+
256
+ Tag connections for easy filtering. Override `getConnectionTags()` to assign tags:
257
+
258
+ ```typescript
259
+ export class ChatAgent extends Agent {
260
+ // Called when a connection is established
261
+ getConnectionTags(connection: Connection, ctx: ConnectionContext): string[] {
262
+ const url = new URL(ctx.request.url);
263
+ const role = url.searchParams.get("role");
264
+
265
+ const tags: string[] = [];
266
+ if (role === "admin") tags.push("admin");
267
+ if (role === "moderator") tags.push("moderator");
268
+
269
+ return tags; // Up to 9 tags, max 256 chars each
270
+ }
271
+
272
+ // Later, broadcast only to admins
273
+ notifyAdmins(message: string) {
274
+ for (const conn of this.getConnections("admin")) {
275
+ conn.send(message);
276
+ }
277
+ }
278
+ }
279
+ ```
280
+
281
+ ## Per-Connection State
282
+
283
+ Store data specific to each connection using `connection.state` and `connection.setState()`:
284
+
285
+ ```typescript
286
+ type ConnectionState = {
287
+ username: string;
288
+ joinedAt: number;
289
+ messageCount: number;
290
+ };
291
+
292
+ export class ChatAgent extends Agent {
293
+ onConnect(connection: Connection<ConnectionState>, ctx: ConnectionContext) {
294
+ const url = new URL(ctx.request.url);
295
+
296
+ // Initialize connection state
297
+ connection.setState({
298
+ username: url.searchParams.get("username") || "Anonymous",
299
+ joinedAt: Date.now(),
300
+ messageCount: 0
301
+ });
302
+ }
303
+
304
+ onMessage(connection: Connection<ConnectionState>, message: WSMessage) {
305
+ // Update message count using functional update
306
+ connection.setState((prev) => ({
307
+ ...prev!,
308
+ messageCount: (prev?.messageCount || 0) + 1
309
+ }));
310
+
311
+ // Access current state
312
+ const { username, messageCount } = connection.state!;
313
+ console.log(`${username} sent message #${messageCount}`);
314
+ }
315
+ }
316
+ ```
317
+
318
+ **Important:** Connection state is:
319
+
320
+ - **Immutable** - Read via `connection.state`, update via `connection.setState()`
321
+ - **Per-connection** - Each connection has its own state
322
+ - **Persisted across hibernation** - Survives agent sleep/wake cycles
323
+
324
+ ## The `onStart` Hook
325
+
326
+ `onStart()` is called once when the agent first starts, before any connections are established:
327
+
328
+ ```typescript
329
+ export class MyAgent extends Agent {
330
+ private cache: Map<string, unknown> = new Map();
331
+
332
+ async onStart() {
333
+ // Initialize resources
334
+ console.log(`Agent ${this.name} starting...`);
335
+
336
+ // Load data from storage
337
+ const savedData = this.sql`SELECT * FROM cache`;
338
+ for (const row of savedData) {
339
+ this.cache.set(row.key, row.value);
340
+ }
341
+
342
+ // Restore MCP connections, check workflows, etc.
343
+ // (Agent does this automatically, but you can add custom logic)
344
+ }
345
+
346
+ onConnect(connection: Connection) {
347
+ // By the time connections arrive, onStart has completed
348
+ }
349
+ }
350
+ ```
351
+
352
+ ## Protocol Message Control
353
+
354
+ By default, when a WebSocket client connects, the agent sends protocol text frames (`CF_AGENT_IDENTITY`, `CF_AGENT_STATE`, `CF_AGENT_MCP_SERVERS`) to keep the client in sync. You can suppress these on a per-connection basis by overriding `shouldSendProtocolMessages`:
355
+
356
+ ```typescript
357
+ export class MyAgent extends Agent {
358
+ shouldSendProtocolMessages(
359
+ connection: Connection,
360
+ ctx: ConnectionContext
361
+ ): boolean {
362
+ // Suppress protocol frames for binary-only clients
363
+ const url = new URL(ctx.request.url);
364
+ return url.searchParams.get("protocol") !== "false";
365
+ }
366
+ }
367
+ ```
368
+
369
+ When `shouldSendProtocolMessages` returns `false` for a connection:
370
+
371
+ - No `CF_AGENT_IDENTITY`, `CF_AGENT_STATE`, or `CF_AGENT_MCP_SERVERS` frames are sent on connect
372
+ - The connection is excluded from protocol broadcasts (state updates, MCP server changes)
373
+ - Regular messages via `connection.send()` and `this.broadcast()` still work normally
374
+
375
+ This is useful for IoT devices, binary-only clients, or lightweight consumers that only need raw messages.
376
+
377
+ ### Checking Protocol Status
378
+
379
+ Use `isConnectionProtocolEnabled` to check whether a connection receives protocol messages:
380
+
381
+ ```typescript
382
+ const enabled = this.isConnectionProtocolEnabled(connection);
383
+ ```
384
+
385
+ This status persists across hibernation — a connection that was marked as no-protocol before hibernation remains no-protocol after waking up.
386
+
387
+ ## Error Handling
388
+
389
+ Handle errors gracefully with `onError`:
390
+
391
+ ```typescript
392
+ export class MyAgent extends Agent {
393
+ // WebSocket connection error
394
+ onError(connection: Connection, error: unknown): void {
395
+ console.error(`WebSocket error on ${connection.id}:`, error);
396
+ connection.send(
397
+ JSON.stringify({ type: "error", message: "Connection error" })
398
+ );
399
+ }
400
+
401
+ // Server error (overloaded signature - no connection parameter)
402
+ onError(error: unknown): void {
403
+ console.error("Server error:", error);
404
+ // Log to external service, etc.
405
+ }
406
+ }
407
+ ```
408
+
409
+ ## Hibernation
410
+
411
+ Agents support hibernation - they can sleep when inactive and wake when needed. This saves resources while maintaining WebSocket connections.
412
+
413
+ ### Enabling Hibernation
414
+
415
+ Hibernation is enabled by default. To disable:
416
+
417
+ ```typescript
418
+ export class AlwaysOnAgent extends Agent {
419
+ static options = { hibernate: false };
420
+ }
421
+ ```
422
+
423
+ ### How Hibernation Works
424
+
425
+ 1. Agent is active, handling connections
426
+ 2. After ~10 seconds of no messages, agent hibernates (sleeps)
427
+ 3. WebSocket connections remain open (handled by Cloudflare)
428
+ 4. When a message arrives, agent wakes up
429
+ 5. `onMessage` is called as normal
430
+
431
+ ### What Persists Across Hibernation
432
+
433
+ | Persists | Does Not Persist |
434
+ | -------------------------- | ------------------- |
435
+ | `this.state` (agent state) | In-memory variables |
436
+ | `connection.state` | Timers/intervals |
437
+ | SQLite data (`this.sql`) | Promises in flight |
438
+ | Connection metadata | Local caches |
439
+
440
+ **Best Practice:** Store important data in `this.state` or SQLite, not in class properties:
441
+
442
+ ```typescript
443
+ export class MyAgent extends Agent<Env, { counter: number }> {
444
+ initialState = { counter: 0 };
445
+
446
+ // ❌ Don't do this - lost on hibernation
447
+ private localCounter = 0;
448
+
449
+ onMessage(connection: Connection, message: WSMessage) {
450
+ // ✅ Do this - persists
451
+ this.setState({ counter: this.state.counter + 1 });
452
+
453
+ // ❌ Lost after hibernation
454
+ this.localCounter++;
455
+ }
456
+ }
457
+ ```
458
+
459
+ ## Common Patterns
460
+
461
+ ### Authentication on Connect
462
+
463
+ Validate users when they connect:
464
+
465
+ ```typescript
466
+ export class SecureAgent extends Agent {
467
+ async onConnect(connection: Connection, ctx: ConnectionContext) {
468
+ const url = new URL(ctx.request.url);
469
+ const token = url.searchParams.get("token");
470
+
471
+ if (!token || !(await this.validateToken(token))) {
472
+ connection.close(4001, "Unauthorized");
473
+ return;
474
+ }
475
+
476
+ const user = await this.getUserFromToken(token);
477
+ connection.setState({ userId: user.id, role: user.role });
478
+
479
+ connection.send(JSON.stringify({ type: "authenticated", user }));
480
+ }
481
+
482
+ private async validateToken(token: string): Promise<boolean> {
483
+ // Validate JWT, check database, etc.
484
+ return true;
485
+ }
486
+ }
487
+ ```
488
+
489
+ ### Chat Room with Broadcast
490
+
491
+ ```typescript
492
+ type Message = {
493
+ type: "message" | "join" | "leave";
494
+ user: string;
495
+ text?: string;
496
+ timestamp: number;
497
+ };
498
+
499
+ export class ChatRoom extends Agent {
500
+ onConnect(connection: Connection, ctx: ConnectionContext) {
501
+ const url = new URL(ctx.request.url);
502
+ const username = url.searchParams.get("username") || "Anonymous";
503
+
504
+ connection.setState({ username });
505
+
506
+ // Notify others
507
+ this.broadcast(
508
+ JSON.stringify({
509
+ type: "join",
510
+ user: username,
511
+ timestamp: Date.now()
512
+ } satisfies Message),
513
+ [connection.id] // Don't send to the joining user
514
+ );
515
+ }
516
+
517
+ onMessage(connection: Connection, message: WSMessage) {
518
+ if (typeof message !== "string") return;
519
+
520
+ const { username } = connection.state as { username: string };
521
+
522
+ // Broadcast to everyone
523
+ this.broadcast(
524
+ JSON.stringify({
525
+ type: "message",
526
+ user: username,
527
+ text: message,
528
+ timestamp: Date.now()
529
+ } satisfies Message)
530
+ );
531
+ }
532
+
533
+ onClose(connection: Connection) {
534
+ const { username } = (connection.state as { username: string }) || {};
535
+ if (username) {
536
+ this.broadcast(
537
+ JSON.stringify({
538
+ type: "leave",
539
+ user: username,
540
+ timestamp: Date.now()
541
+ } satisfies Message)
542
+ );
543
+ }
544
+ }
545
+ }
546
+ ```
547
+
548
+ ### Presence Tracking
549
+
550
+ Track who's online using per-connection state. This pattern is clean because connection state is automatically cleaned up when users disconnect:
551
+
552
+ ```typescript
553
+ type UserState = {
554
+ name: string;
555
+ joinedAt: number;
556
+ lastSeen: number;
557
+ };
558
+
559
+ export class PresenceAgent extends Agent {
560
+ onConnect(connection: Connection<UserState>, ctx: ConnectionContext) {
561
+ const url = new URL(ctx.request.url);
562
+ const name = url.searchParams.get("name") || "Anonymous";
563
+
564
+ // Store user data on the connection itself
565
+ connection.setState({
566
+ name,
567
+ joinedAt: Date.now(),
568
+ lastSeen: Date.now()
569
+ });
570
+
571
+ // Send current presence to new user
572
+ connection.send(
573
+ JSON.stringify({
574
+ type: "presence",
575
+ users: this.getPresence()
576
+ })
577
+ );
578
+
579
+ // Notify others that someone joined
580
+ this.broadcastPresence();
581
+ }
582
+
583
+ onClose(connection: Connection) {
584
+ // No manual cleanup needed - connection state is automatically gone
585
+ // Just broadcast updated presence to remaining users
586
+ this.broadcastPresence();
587
+ }
588
+
589
+ // Heartbeat to update lastSeen
590
+ onMessage(connection: Connection<UserState>, message: WSMessage) {
591
+ if (message === "ping") {
592
+ connection.setState((prev) => ({
593
+ ...prev!,
594
+ lastSeen: Date.now()
595
+ }));
596
+ connection.send("pong");
597
+ }
598
+ }
599
+
600
+ // Build presence from all connections
601
+ private getPresence() {
602
+ const users: Record<string, { name: string; lastSeen: number }> = {};
603
+ for (const conn of this.getConnections<UserState>()) {
604
+ if (conn.state) {
605
+ users[conn.id] = {
606
+ name: conn.state.name,
607
+ lastSeen: conn.state.lastSeen
608
+ };
609
+ }
610
+ }
611
+ return users;
612
+ }
613
+
614
+ private broadcastPresence() {
615
+ this.broadcast(
616
+ JSON.stringify({
617
+ type: "presence",
618
+ users: this.getPresence()
619
+ })
620
+ );
621
+ }
622
+ }
623
+ ```
624
+
625
+ ## API Reference
626
+
627
+ ### Agent Lifecycle Methods
628
+
629
+ | Method | Signature | Description |
630
+ | ---------------------------- | --------------------------------------------------------------- | ------------------------------------------------------ |
631
+ | `onStart` | `(props?) => void \| Promise<void>` | Called once when agent starts |
632
+ | `onRequest` | `(request: Request) => Response \| Promise<Response>` | Handle HTTP requests |
633
+ | `onConnect` | `(connection, ctx) => void \| Promise<void>` | WebSocket connected |
634
+ | `onMessage` | `(connection, message) => void \| Promise<void>` | Message received |
635
+ | `onClose` | `(connection, code, reason, wasClean) => void \| Promise<void>` | Connection closed |
636
+ | `onError` | `(connection, error) => void \| Promise<void>` | WebSocket error |
637
+ | `onError` | `(error) => void \| Promise<void>` | Server error (overload) |
638
+ | `shouldSendProtocolMessages` | `(connection, ctx) => boolean` | Control per-connection protocol frames (default: true) |
639
+
640
+ ### Connection Management Methods
641
+
642
+ | Method | Signature | Description |
643
+ | ----------------------------- | ----------------------------------------- | ---------------------------------------------- |
644
+ | `getConnections` | `(tag?: string) => Iterable<Connection>` | Get all connections, optionally by tag |
645
+ | `getConnection` | `(id: string) => Connection \| undefined` | Get connection by ID |
646
+ | `getConnectionTags` | `(connection, ctx) => string[]` | Override to tag connections |
647
+ | `broadcast` | `(message, without?: string[]) => void` | Send to all connections |
648
+ | `isConnectionProtocolEnabled` | `(connection) => boolean` | Check if connection receives protocol messages |
649
+
650
+ ### Connection Object
651
+
652
+ | Property/Method | Type | Description |
653
+ | --------------- | ------------------------------------------ | -------------------------------- |
654
+ | `id` | `string` | Unique connection identifier |
655
+ | `server` | `string` | Agent instance name |
656
+ | `state` | `T \| null` | Per-connection state (read-only) |
657
+ | `setState` | `(state \| (prev) => state) => void` | Update connection state |
658
+ | `send` | `(message: string \| ArrayBuffer) => void` | Send message |
659
+ | `close` | `(code?, reason?) => void` | Close connection |
660
+
661
+ ### Agent Properties
662
+
663
+ | Property | Type | Description |
664
+ | ------------ | -------------------- | ----------------------------------- |
665
+ | `this.name` | `string` | Agent instance name |
666
+ | `this.state` | `State` | Agent state (use with `setState()`) |
667
+ | `this.env` | `Env` | Environment bindings |
668
+ | `this.ctx` | `DurableObjectState` | Durable Object context |