@cloudflare/ai-chat 0.0.4 → 0.0.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,3 @@
1
+ // properly set up the act environment for react-tests
2
+ // @ts-expect-error - react specific API
3
+ globalThis.IS_REACT_ACT_ENVIRONMENT = true;
@@ -10,6 +10,10 @@ import {
10
10
  } from "../react";
11
11
  import type { useAgent } from "agents/react";
12
12
 
13
+ function sleep(ms: number) {
14
+ return new Promise((resolve) => setTimeout(resolve, ms));
15
+ }
16
+
13
17
  function createAgent({ name, url }: { name: string; url: string }) {
14
18
  const target = new EventTarget();
15
19
  const baseAgent = {
@@ -63,15 +67,17 @@ describe("useAgentChat", () => {
63
67
  return "Suspended";
64
68
  };
65
69
 
66
- const screen = await act(() =>
67
- render(<TestComponent />, {
70
+ const screen = await act(async () => {
71
+ const screen = render(<TestComponent />, {
68
72
  wrapper: ({ children }) => (
69
73
  <StrictMode>
70
74
  <Suspense fallback={<SuspenseObserver />}>{children}</Suspense>
71
75
  </StrictMode>
72
76
  )
73
- })
74
- );
77
+ });
78
+ await sleep(10);
79
+ return screen;
80
+ });
75
81
 
76
82
  await expect
77
83
  .element(screen.getByTestId("messages"))
@@ -123,15 +129,18 @@ describe("useAgentChat", () => {
123
129
  return "Suspended";
124
130
  };
125
131
 
126
- const screen = await act(() =>
127
- render(<TestComponent agent={agentA} />, {
132
+ const screen = await act(async () => {
133
+ const screen = render(<TestComponent agent={agentA} />, {
128
134
  wrapper: ({ children }) => (
129
135
  <StrictMode>
130
136
  <Suspense fallback={<SuspenseObserver />}>{children}</Suspense>
131
137
  </StrictMode>
132
138
  )
133
- })
134
- );
139
+ });
140
+
141
+ await sleep(10);
142
+ return screen;
143
+ });
135
144
 
136
145
  await expect
137
146
  .element(screen.getByTestId("messages"))
@@ -145,7 +154,10 @@ describe("useAgentChat", () => {
145
154
 
146
155
  suspenseRendered.mockClear();
147
156
 
148
- await act(() => screen.rerender(<TestComponent agent={agentB} />));
157
+ await act(async () => {
158
+ screen.rerender(<TestComponent agent={agentB} />);
159
+ await sleep(10);
160
+ });
149
161
 
150
162
  await expect
151
163
  .element(screen.getByTestId("messages"))
@@ -219,7 +231,7 @@ describe("useAgentChat", () => {
219
231
  _options: PrepareSendMessagesRequestOptions<UIMessage>
220
232
  ): Promise<PrepareSendMessagesRequestResult> => {
221
233
  // Simulate async operation like fetching tool definitions
222
- await new Promise((resolve) => setTimeout(resolve, 10));
234
+ await sleep(10);
223
235
  return {
224
236
  body: {
225
237
  clientTools: [
@@ -475,26 +487,24 @@ describe("useAgentChat client-side tool execution (issue #728)", () => {
475
487
  );
476
488
  };
477
489
 
478
- const screen = await act(() =>
479
- render(<TestComponent />, {
490
+ const screen = await act(async () => {
491
+ const screen = render(<TestComponent />, {
480
492
  wrapper: ({ children }) => (
481
493
  <StrictMode>
482
494
  <Suspense fallback="Loading...">{children}</Suspense>
483
495
  </StrictMode>
484
496
  )
485
- })
486
- );
497
+ });
498
+ // The tool should have been automatically executed
499
+ await sleep(10);
500
+ return screen;
501
+ });
487
502
 
488
503
  // Wait for initial messages to load
489
504
  await expect
490
505
  .element(screen.getByTestId("messages-count"))
491
506
  .toHaveTextContent("2");
492
507
 
493
- // The tool should have been automatically executed
494
- await act(async () => {
495
- await new Promise((resolve) => setTimeout(resolve, 100));
496
- });
497
-
498
508
  // Verify the tool execute was called
499
509
  expect(mockExecute).toHaveBeenCalled();
500
510
 
@@ -574,15 +584,17 @@ describe("useAgentChat client-side tool execution (issue #728)", () => {
574
584
  );
575
585
  };
576
586
 
577
- const screen = await act(() =>
578
- render(<TestComponent />, {
587
+ const screen = await act(async () => {
588
+ const screen = render(<TestComponent />, {
579
589
  wrapper: ({ children }) => (
580
590
  <StrictMode>
581
591
  <Suspense fallback="Loading...">{children}</Suspense>
582
592
  </StrictMode>
583
593
  )
584
- })
585
- );
594
+ });
595
+ await sleep(10);
596
+ return screen;
597
+ });
586
598
 
587
599
  await expect
588
600
  .element(screen.getByTestId("messages-count"))
@@ -12,6 +12,7 @@ export default defineConfig({
12
12
  ],
13
13
  provider: "playwright"
14
14
  },
15
- clearMocks: true
15
+ clearMocks: true,
16
+ setupFiles: ["./setup.ts"]
16
17
  }
17
18
  });
@@ -3,6 +3,7 @@ import { describe, it, expect } from "vitest";
3
3
  import { MessageType } from "../types";
4
4
  import type { UIMessage as ChatMessage } from "ai";
5
5
  import { connectChatWS } from "./test-utils";
6
+ import { getAgentByName } from "agents";
6
7
 
7
8
  describe("AIChatAgent Connection Context - Issue #711", () => {
8
9
  it("getCurrentAgent() should return connection in onChatMessage and nested async functions (tool execute)", async () => {
@@ -10,7 +11,7 @@ describe("AIChatAgent Connection Context - Issue #711", () => {
10
11
  const { ws } = await connectChatWS(`/agents/test-chat-agent/${room}`);
11
12
 
12
13
  // Get the agent stub to access captured context
13
- const agentStub = env.TestChatAgent.get(env.TestChatAgent.idFromName(room));
14
+ const agentStub = await getAgentByName(env.TestChatAgent, room);
14
15
 
15
16
  // Clear any previous captured context
16
17
  await agentStub.clearCapturedContext();
@@ -4,6 +4,7 @@ import worker from "./worker";
4
4
  import { MessageType } from "../types";
5
5
  import type { UIMessage as ChatMessage } from "ai";
6
6
  import { connectChatWS } from "./test-utils";
7
+ import { getAgentByName } from "agents";
7
8
 
8
9
  // Type helper for tool call parts - extracts ToolUIPart from ChatMessage parts
9
10
  type TestToolCallPart = Extract<
@@ -198,7 +199,7 @@ describe("Chat Agent Persistence", () => {
198
199
 
199
200
  await ctx.waitUntil(Promise.resolve());
200
201
 
201
- const agentStub = env.TestChatAgent.get(env.TestChatAgent.idFromName(room));
202
+ const agentStub = await getAgentByName(env.TestChatAgent, room);
202
203
 
203
204
  await agentStub.testPersistToolCall("msg-tool-1", "getLocalTime");
204
205
 
@@ -256,7 +257,7 @@ describe("Chat Agent Persistence", () => {
256
257
 
257
258
  await ctx.waitUntil(Promise.resolve());
258
259
 
259
- const agentStub = env.TestChatAgent.get(env.TestChatAgent.idFromName(room));
260
+ const agentStub = await getAgentByName(env.TestChatAgent, room);
260
261
 
261
262
  const userMessage: ChatMessage = {
262
263
  id: "user-1",
@@ -362,7 +363,7 @@ describe("Chat Agent Persistence", () => {
362
363
 
363
364
  await ctx.waitUntil(Promise.resolve());
364
365
 
365
- const agentStub = env.TestChatAgent.get(env.TestChatAgent.idFromName(room));
366
+ const agentStub = await getAgentByName(env.TestChatAgent, room);
366
367
 
367
368
  const userMessage: ChatMessage = {
368
369
  id: "user-1",
@@ -1,4 +1,5 @@
1
1
  import { createExecutionContext, env } from "cloudflare:test";
2
+ import { getAgentByName } from "agents";
2
3
  import { describe, it, expect } from "vitest";
3
4
  import worker from "./worker";
4
5
  import type { UIMessage as ChatMessage } from "ai";
@@ -17,7 +18,7 @@ describe("Client-side tool duplicate message prevention", () => {
17
18
  ws.accept();
18
19
  await ctx.waitUntil(Promise.resolve());
19
20
 
20
- const agentStub = env.TestChatAgent.get(env.TestChatAgent.idFromName(room));
21
+ const agentStub = await getAgentByName(env.TestChatAgent, room);
21
22
  const toolCallId = "call_merge_test";
22
23
 
23
24
  // Persist assistant message with tool in input-available state
@@ -91,7 +92,7 @@ describe("Client-side tool duplicate message prevention", () => {
91
92
  ws.accept();
92
93
  await ctx.waitUntil(Promise.resolve());
93
94
 
94
- const agentStub = env.TestChatAgent.get(env.TestChatAgent.idFromName(room));
95
+ const agentStub = await getAgentByName(env.TestChatAgent, room);
95
96
  const toolCallId = "call_tool_result_test";
96
97
 
97
98
  // Persist assistant message with tool in input-available state
@@ -164,7 +165,7 @@ describe("Client-side tool duplicate message prevention", () => {
164
165
  ws.accept();
165
166
  await ctx.waitUntil(Promise.resolve());
166
167
 
167
- const agentStub = env.TestChatAgent.get(env.TestChatAgent.idFromName(room));
168
+ const agentStub = await getAgentByName(env.TestChatAgent, room);
168
169
  const toolCallId = "call_tool_result_auto_continue";
169
170
 
170
171
  // Persist assistant message with tool in input-available state
@@ -239,7 +240,7 @@ describe("Client-side tool duplicate message prevention", () => {
239
240
  ws.accept();
240
241
  await ctx.waitUntil(Promise.resolve());
241
242
 
242
- const agentStub = env.TestChatAgent.get(env.TestChatAgent.idFromName(room));
243
+ const agentStub = await getAgentByName(env.TestChatAgent, room);
243
244
 
244
245
  // Persist message with OpenAI itemId in providerMetadata (simulates OpenAI Responses API)
245
246
  await agentStub.persistMessages([
@@ -299,7 +300,7 @@ describe("Client-side tool duplicate message prevention", () => {
299
300
  ws.accept();
300
301
  await ctx.waitUntil(Promise.resolve());
301
302
 
302
- const agentStub = env.TestChatAgent.get(env.TestChatAgent.idFromName(room));
303
+ const agentStub = await getAgentByName(env.TestChatAgent, room);
303
304
  const toolCallId = "call_openai_strip_test";
304
305
 
305
306
  // Persist message with tool that has OpenAI itemId in callProviderMetadata
@@ -362,7 +363,7 @@ describe("Client-side tool duplicate message prevention", () => {
362
363
  ws.accept();
363
364
  await ctx.waitUntil(Promise.resolve());
364
365
 
365
- const agentStub = env.TestChatAgent.get(env.TestChatAgent.idFromName(room));
366
+ const agentStub = await getAgentByName(env.TestChatAgent, room);
366
367
 
367
368
  // Persist message with other metadata alongside itemId
368
369
  await agentStub.persistMessages([
@@ -433,7 +434,7 @@ describe("Client-side tool duplicate message prevention", () => {
433
434
  ws.accept();
434
435
  await ctx.waitUntil(Promise.resolve());
435
436
 
436
- const agentStub = env.TestChatAgent.get(env.TestChatAgent.idFromName(room));
437
+ const agentStub = await getAgentByName(env.TestChatAgent, room);
437
438
 
438
439
  // Persist message with empty reasoning part (simulates OpenAI Responses API)
439
440
  await agentStub.persistMessages([
@@ -487,7 +488,7 @@ describe("Client-side tool duplicate message prevention", () => {
487
488
  ws.accept();
488
489
  await ctx.waitUntil(Promise.resolve());
489
490
 
490
- const agentStub = env.TestChatAgent.get(env.TestChatAgent.idFromName(room));
491
+ const agentStub = await getAgentByName(env.TestChatAgent, room);
491
492
 
492
493
  // Persist message with non-empty reasoning part
493
494
  await agentStub.persistMessages([
@@ -2,6 +2,7 @@ import { env } from "cloudflare:test";
2
2
  import { describe, it, expect } from "vitest";
3
3
  import { MessageType, type OutgoingMessage } from "../types";
4
4
  import { connectChatWS, isUseChatResponseMessage } from "./test-utils";
5
+ import { getAgentByName } from "agents";
5
6
 
6
7
  function isStreamResumingMessage(
7
8
  m: unknown
@@ -37,9 +38,7 @@ describe("Resumable Streaming", () => {
37
38
 
38
39
  await new Promise((r) => setTimeout(r, 50));
39
40
 
40
- const agentStub = env.TestChatAgent.get(
41
- env.TestChatAgent.idFromName(room)
42
- );
41
+ const agentStub = await getAgentByName(env.TestChatAgent, room);
43
42
 
44
43
  const streamId = await agentStub.testStartStream("req-123");
45
44
  expect(streamId).toBeDefined();
@@ -59,9 +58,7 @@ describe("Resumable Streaming", () => {
59
58
 
60
59
  await new Promise((r) => setTimeout(r, 50));
61
60
 
62
- const agentStub = env.TestChatAgent.get(
63
- env.TestChatAgent.idFromName(room)
64
- );
61
+ const agentStub = await getAgentByName(env.TestChatAgent, room);
65
62
 
66
63
  const streamId = await agentStub.testStartStream("req-456");
67
64
 
@@ -98,9 +95,7 @@ describe("Resumable Streaming", () => {
98
95
 
99
96
  await new Promise((r) => setTimeout(r, 50));
100
97
 
101
- const agentStub = env.TestChatAgent.get(
102
- env.TestChatAgent.idFromName(room)
103
- );
98
+ const agentStub = await getAgentByName(env.TestChatAgent, room);
104
99
 
105
100
  const streamId = await agentStub.testStartStream("req-789");
106
101
 
@@ -127,9 +122,7 @@ describe("Resumable Streaming", () => {
127
122
 
128
123
  await new Promise((r) => setTimeout(r, 50));
129
124
 
130
- const agentStub = env.TestChatAgent.get(
131
- env.TestChatAgent.idFromName(room)
132
- );
125
+ const agentStub = await getAgentByName(env.TestChatAgent, room);
133
126
 
134
127
  const streamId = await agentStub.testStartStream("req-error");
135
128
 
@@ -156,9 +149,7 @@ describe("Resumable Streaming", () => {
156
149
  );
157
150
  await new Promise((r) => setTimeout(r, 50));
158
151
 
159
- const agentStub = env.TestChatAgent.get(
160
- env.TestChatAgent.idFromName(room)
161
- );
152
+ const agentStub = await getAgentByName(env.TestChatAgent, room);
162
153
  const streamId = await agentStub.testStartStream("req-resume");
163
154
  await agentStub.testStoreStreamChunk(
164
155
  streamId,
@@ -193,9 +184,7 @@ describe("Resumable Streaming", () => {
193
184
  );
194
185
  await new Promise((r) => setTimeout(r, 50));
195
186
 
196
- const agentStub = env.TestChatAgent.get(
197
- env.TestChatAgent.idFromName(room)
198
- );
187
+ const agentStub = await getAgentByName(env.TestChatAgent, room);
199
188
  const streamId = await agentStub.testStartStream("req-ack");
200
189
  await agentStub.testStoreStreamChunk(
201
190
  streamId,
@@ -237,6 +226,82 @@ describe("Resumable Streaming", () => {
237
226
  ws2.close();
238
227
  });
239
228
 
229
+ it("does not deliver live chunks before ACK to resuming connections", async () => {
230
+ const room = crypto.randomUUID();
231
+
232
+ // First connection - start a stream
233
+ const { ws: ws1 } = await connectChatWS(
234
+ `/agents/test-chat-agent/${room}`
235
+ );
236
+ const messages1 = collectMessages(ws1);
237
+ await new Promise((r) => setTimeout(r, 50));
238
+
239
+ const agentStub = await getAgentByName(env.TestChatAgent, room);
240
+ const streamId = await agentStub.testStartStream("req-live");
241
+
242
+ // Second connection - will be notified to resume
243
+ const { ws: ws2 } = await connectChatWS(
244
+ `/agents/test-chat-agent/${room}`
245
+ );
246
+ const messages2 = collectMessages(ws2);
247
+
248
+ await new Promise((r) => setTimeout(r, 100));
249
+
250
+ // Broadcast a live chunk while ws2 is pending resume (no ACK yet)
251
+ await agentStub.testBroadcastLiveChunk(
252
+ "req-live",
253
+ streamId,
254
+ '{"type":"text-delta","id":"0","delta":"A"}'
255
+ );
256
+
257
+ await new Promise((r) => setTimeout(r, 100));
258
+
259
+ // ws2 should NOT receive live chunks before ACK
260
+ const preAckChunks = messages2.filter(isUseChatResponseMessage);
261
+ expect(preAckChunks.length).toBe(0);
262
+
263
+ // ws1 should receive the live chunk
264
+ const ws1Chunks = messages1.filter(isUseChatResponseMessage);
265
+ expect(ws1Chunks.length).toBe(1);
266
+ expect(ws1Chunks[0].body).toBe(
267
+ '{"type":"text-delta","id":"0","delta":"A"}'
268
+ );
269
+
270
+ // Send ACK to resume
271
+ ws2.send(
272
+ JSON.stringify({
273
+ type: MessageType.CF_AGENT_STREAM_RESUME_ACK,
274
+ id: "req-live"
275
+ })
276
+ );
277
+
278
+ await new Promise((r) => setTimeout(r, 100));
279
+
280
+ // After ACK, ws2 should receive the replayed chunk
281
+ const postAckChunks = messages2.filter(isUseChatResponseMessage);
282
+ expect(postAckChunks.length).toBeGreaterThanOrEqual(1);
283
+ expect(postAckChunks[0].body).toBe(
284
+ '{"type":"text-delta","id":"0","delta":"A"}'
285
+ );
286
+
287
+ // Live chunks after ACK should be delivered
288
+ await agentStub.testBroadcastLiveChunk(
289
+ "req-live",
290
+ streamId,
291
+ '{"type":"text-delta","id":"0","delta":"B"}'
292
+ );
293
+
294
+ await new Promise((r) => setTimeout(r, 100));
295
+
296
+ const finalChunks = messages2.filter(isUseChatResponseMessage);
297
+ expect(finalChunks.some((m) => m.body?.includes('"delta":"B"'))).toBe(
298
+ true
299
+ );
300
+
301
+ ws1.close();
302
+ ws2.close();
303
+ });
304
+
240
305
  it("ignores ACK with wrong request ID", async () => {
241
306
  const room = crypto.randomUUID();
242
307
 
@@ -246,9 +311,7 @@ describe("Resumable Streaming", () => {
246
311
  );
247
312
  await new Promise((r) => setTimeout(r, 50));
248
313
 
249
- const agentStub = env.TestChatAgent.get(
250
- env.TestChatAgent.idFromName(room)
251
- );
314
+ const agentStub = await getAgentByName(env.TestChatAgent, room);
252
315
  const streamId = await agentStub.testStartStream("req-correct");
253
316
  await agentStub.testStoreStreamChunk(
254
317
  streamId,
@@ -292,9 +355,7 @@ describe("Resumable Streaming", () => {
292
355
 
293
356
  await new Promise((r) => setTimeout(r, 50));
294
357
 
295
- const agentStub = env.TestChatAgent.get(
296
- env.TestChatAgent.idFromName(room)
297
- );
358
+ const agentStub = await getAgentByName(env.TestChatAgent, room);
298
359
 
299
360
  // Insert a stale stream (6 minutes old)
300
361
  const staleStreamId = "stale-stream-123";
@@ -327,9 +388,7 @@ describe("Resumable Streaming", () => {
327
388
 
328
389
  await new Promise((r) => setTimeout(r, 50));
329
390
 
330
- const agentStub = env.TestChatAgent.get(
331
- env.TestChatAgent.idFromName(room)
332
- );
391
+ const agentStub = await getAgentByName(env.TestChatAgent, room);
333
392
 
334
393
  // Insert a fresh stream (1 minute old)
335
394
  const freshStreamId = "fresh-stream-456";
@@ -363,9 +422,7 @@ describe("Resumable Streaming", () => {
363
422
 
364
423
  await new Promise((r) => setTimeout(r, 50));
365
424
 
366
- const agentStub = env.TestChatAgent.get(
367
- env.TestChatAgent.idFromName(room)
368
- );
425
+ const agentStub = await getAgentByName(env.TestChatAgent, room);
369
426
 
370
427
  // Create a stream with chunks
371
428
  const streamId = await agentStub.testStartStream("req-clear");
@@ -405,9 +462,7 @@ describe("Resumable Streaming", () => {
405
462
 
406
463
  await new Promise((r) => setTimeout(r, 50));
407
464
 
408
- const agentStub = env.TestChatAgent.get(
409
- env.TestChatAgent.idFromName(room)
410
- );
465
+ const agentStub = await getAgentByName(env.TestChatAgent, room);
411
466
 
412
467
  // Start first stream and add chunks without explicit flush
413
468
  const stream1 = await agentStub.testStartStream("req-1");
@@ -439,9 +494,7 @@ describe("Resumable Streaming", () => {
439
494
 
440
495
  await new Promise((r) => setTimeout(r, 50));
441
496
 
442
- const agentStub = env.TestChatAgent.get(
443
- env.TestChatAgent.idFromName(room)
444
- );
497
+ const agentStub = await getAgentByName(env.TestChatAgent, room);
445
498
 
446
499
  const streamId = await agentStub.testStartStream("req-flush");
447
500
  await agentStub.testStoreStreamChunk(
@@ -470,9 +523,7 @@ describe("Resumable Streaming", () => {
470
523
  );
471
524
  await new Promise((r) => setTimeout(r, 50));
472
525
 
473
- const agentStub = env.TestChatAgent.get(
474
- env.TestChatAgent.idFromName(room)
475
- );
526
+ const agentStub = await getAgentByName(env.TestChatAgent, room);
476
527
  const streamId = await agentStub.testStartStream("req-done");
477
528
  await agentStub.testStoreStreamChunk(
478
529
  streamId,
@@ -1,6 +1,7 @@
1
1
  import { AIChatAgent } from "../";
2
2
  import type { UIMessage as ChatMessage } from "ai";
3
3
  import { callable, getCurrentAgent, routeAgentRequest } from "agents";
4
+ import { MessageType, type OutgoingMessage } from "../types";
4
5
 
5
6
  // Type helper for tool call parts - extracts from ChatMessage parts
6
7
  type TestToolCallPart = Extract<
@@ -148,6 +149,29 @@ export class TestChatAgent extends AIChatAgent<Env> {
148
149
  this._storeStreamChunk(streamId, body);
149
150
  }
150
151
 
152
+ @callable()
153
+ testBroadcastLiveChunk(
154
+ requestId: string,
155
+ streamId: string,
156
+ body: string
157
+ ): void {
158
+ this._storeStreamChunk(streamId, body);
159
+ const message: OutgoingMessage = {
160
+ body,
161
+ done: false,
162
+ id: requestId,
163
+ type: MessageType.CF_AGENT_USE_CHAT_RESPONSE
164
+ };
165
+ (
166
+ this as unknown as {
167
+ _broadcastChatMessage: (
168
+ msg: OutgoingMessage,
169
+ exclude?: string[]
170
+ ) => void;
171
+ }
172
+ )._broadcastChatMessage(message);
173
+ }
174
+
151
175
  @callable()
152
176
  testFlushChunkBuffer(): void {
153
177
  this._flushChunkBuffer();