@copilotkit/vue 1.78.0 → 1.79.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 (28) hide show
  1. package/dist/index.cjs +1 -1
  2. package/dist/index.mjs +2 -2
  3. package/dist/use-render-activity-message-DxYudLWg.cjs +70 -0
  4. package/dist/use-render-activity-message-DxYudLWg.cjs.map +1 -0
  5. package/dist/{use-render-activity-message-B9MOLfR_.js → use-render-activity-message-sBoXH1Ew.js} +2359 -2373
  6. package/dist/use-render-activity-message-sBoXH1Ew.js.map +1 -0
  7. package/dist/v2/hooks/use-agent.d.ts.map +1 -1
  8. package/dist/v2/hooks/use-threads.d.ts.map +1 -1
  9. package/dist/v2/index.cjs +1 -1
  10. package/dist/v2/index.mjs +1 -1
  11. package/dist/v2/providers/CopilotKitProvider.types.d.ts +7 -2
  12. package/dist/v2/providers/CopilotKitProvider.types.d.ts.map +1 -1
  13. package/dist/v2/providers/CopilotKitProvider.vue.d.ts +2 -1
  14. package/dist/v2/providers/CopilotKitProvider.vue.d.ts.map +1 -1
  15. package/package.json +6 -6
  16. package/src/v2/hooks/__tests__/use-agent-stability.test.ts +11 -0
  17. package/src/v2/hooks/__tests__/use-threads.test.ts +27 -6
  18. package/src/v2/hooks/use-agent.ts +28 -22
  19. package/src/v2/hooks/use-threads.ts +9 -8
  20. package/src/v2/lib/__tests__/transcription-client.test.ts +2 -0
  21. package/src/v2/lib/transcription-client.ts +1 -1
  22. package/src/v2/providers/CopilotKitProvider.types.ts +7 -1
  23. package/src/v2/providers/CopilotKitProvider.vue +62 -12
  24. package/src/v2/providers/__tests__/CopilotKitProvider.headersAtSendTime.test.ts +598 -0
  25. package/src/v2/providers/__tests__/CopilotKitProvider.test.ts +8 -3
  26. package/dist/use-render-activity-message-B9MOLfR_.js.map +0 -1
  27. package/dist/use-render-activity-message-eN0gdzOa.cjs +0 -70
  28. package/dist/use-render-activity-message-eN0gdzOa.cjs.map +0 -1
@@ -0,0 +1,598 @@
1
+ /**
2
+ * #1937: the headers builder is evaluated when a request is SENT, not during
3
+ * render/setup. Mirrors
4
+ * `react-core/src/v2/providers/__tests__/CopilotKitProvider.headersAtSendTime.test.tsx`.
5
+ */
6
+ import { mount } from "@vue/test-utils";
7
+ import { defineComponent, h, nextTick } from "vue";
8
+ import { afterEach, describe, expect, it, vi } from "vitest";
9
+ import { HttpAgent } from "@ag-ui/client";
10
+ import type { CopilotKitHeadersSource } from "@copilotkit/core";
11
+ import CopilotKitProvider from "../CopilotKitProvider.vue";
12
+ import { useCopilotKit } from "../useCopilotKit";
13
+ import { useAgent } from "../../hooks/use-agent";
14
+ import { useThreads } from "../../hooks/use-threads";
15
+ import { transcribeAudio } from "../../lib/transcription-client";
16
+ import type { CopilotKitCoreVue } from "../../lib/vue-core";
17
+
18
+ type Call = { url: string; auth: string | null; publicApiKey: string | null };
19
+
20
+ function stubFetch(calls: Call[]) {
21
+ return vi.fn(async (input: RequestInfo | URL, init?: RequestInit) => {
22
+ const url = typeof input === "string" ? input : input.toString();
23
+ const headers = new Headers(init?.headers ?? (input as Request).headers);
24
+ calls.push({
25
+ url,
26
+ auth: headers.get("authorization"),
27
+ publicApiKey: headers.get("x-copilotcloud-public-api-key"),
28
+ });
29
+ let body: unknown = {};
30
+ try {
31
+ body = init?.body ? JSON.parse(String(init.body)) : {};
32
+ } catch {
33
+ // Non-JSON body (e.g. connect/transcribe form data) — irrelevant here.
34
+ }
35
+ const method = (body as { method?: string }).method;
36
+ if (url.endsWith("/info") || method === "info") {
37
+ return new Response(
38
+ JSON.stringify({
39
+ version: "1.0.0",
40
+ agents: { default: { name: "default", description: "" } },
41
+ }),
42
+ { status: 200, headers: { "content-type": "application/json" } },
43
+ );
44
+ }
45
+ if (url.includes("/threads?")) {
46
+ return new Response(JSON.stringify({ threads: [], nextCursor: null }), {
47
+ status: 200,
48
+ headers: { "content-type": "application/json" },
49
+ });
50
+ }
51
+ // A non-2xx here would read as a runtime health signal (core re-probes
52
+ // /info on a failed request while Connected). The run/connect body
53
+ // itself is irrelevant to every assertion here (only request headers are
54
+ // inspected), so keep every other request a plain 200.
55
+ return new Response("{}", {
56
+ status: 200,
57
+ headers: { "content-type": "application/json" },
58
+ });
59
+ });
60
+ }
61
+
62
+ let token = "tok-1";
63
+
64
+ afterEach(() => {
65
+ vi.unstubAllGlobals();
66
+ token = "tok-1";
67
+ });
68
+
69
+ /** Poll a predicate for about `ms`, flushing Vue's reactivity each tick. */
70
+ async function pollFor(predicate: () => boolean, ms = 50): Promise<void> {
71
+ const start = Date.now();
72
+ while (Date.now() - start < ms) {
73
+ if (predicate()) return;
74
+ await nextTick();
75
+ await new Promise((resolve) => setTimeout(resolve, 5));
76
+ }
77
+ }
78
+
79
+ function mountCore(props: Record<string, unknown>): {
80
+ getCore: () => CopilotKitCoreVue;
81
+ wrapper: ReturnType<typeof mount>;
82
+ } {
83
+ let core: CopilotKitCoreVue | undefined;
84
+ const Probe = defineComponent({
85
+ setup() {
86
+ const { copilotkit } = useCopilotKit();
87
+ core = copilotkit.value;
88
+ return () => null;
89
+ },
90
+ });
91
+
92
+ const wrapper = mount(CopilotKitProvider, {
93
+ props,
94
+ slots: { default: () => h(Probe) },
95
+ });
96
+
97
+ return {
98
+ wrapper,
99
+ getCore: () => {
100
+ if (!core) throw new Error("CopilotKit core not available");
101
+ return core;
102
+ },
103
+ };
104
+ }
105
+
106
+ describe("CopilotKitProvider — headers builder evaluated at send time (#1937)", () => {
107
+ it("a run carries the builder's current token when nothing about the provider changed", async () => {
108
+ const calls: Call[] = [];
109
+ vi.stubGlobal("fetch", stubFetch(calls));
110
+
111
+ const { getCore } = mountCore({
112
+ runtimeUrl: "http://rt.test/api/copilotkit",
113
+ headers: (() => ({
114
+ Authorization: `Bearer ${token}`,
115
+ })) as CopilotKitHeadersSource,
116
+ });
117
+
118
+ await vi.waitFor(() => expect(getCore().getAgent("default")).toBeDefined());
119
+ const callsBeforeRun = calls.length;
120
+
121
+ // Token rotates (e.g. a Clerk re-mint). Nothing about the provider's
122
+ // props changes — no re-render/re-setup exists to eagerly re-resolve.
123
+ token = "tok-2";
124
+
125
+ const agent = getCore().getAgent("default")!;
126
+ await getCore()
127
+ .runAgent({ agent })
128
+ .catch(() => {});
129
+
130
+ const runCalls = calls.slice(callsBeforeRun);
131
+ expect(runCalls.length).toBeGreaterThan(0);
132
+ expect(runCalls.every((c) => c.auth === "Bearer tok-2")).toBe(true);
133
+ });
134
+
135
+ it("an async builder is awaited", async () => {
136
+ const calls: Call[] = [];
137
+ vi.stubGlobal("fetch", stubFetch(calls));
138
+
139
+ const { getCore } = mountCore({
140
+ runtimeUrl: "http://rt.test/api/copilotkit",
141
+ headers: (async () => {
142
+ await new Promise((resolve) => setTimeout(resolve, 5));
143
+ return { Authorization: `Bearer ${token}` };
144
+ }) as CopilotKitHeadersSource,
145
+ });
146
+
147
+ await vi.waitFor(() => expect(getCore().getAgent("default")).toBeDefined());
148
+ const callsBeforeRun = calls.length;
149
+
150
+ token = "tok-2";
151
+
152
+ const agent = getCore().getAgent("default")!;
153
+ await getCore()
154
+ .runAgent({ agent })
155
+ .catch(() => {});
156
+
157
+ const runCalls = calls.slice(callsBeforeRun);
158
+ expect(runCalls.length).toBeGreaterThan(0);
159
+ expect(runCalls.every((c) => c.auth === "Bearer tok-2")).toBe(true);
160
+ });
161
+
162
+ it("a provisional proxy's first connect carries the fresh token", async () => {
163
+ const calls: Call[] = [];
164
+ let releaseInfo!: (value: Response) => void;
165
+ vi.stubGlobal(
166
+ "fetch",
167
+ vi.fn(async (input: RequestInfo | URL, init?: RequestInit) => {
168
+ const url = typeof input === "string" ? input : input.toString();
169
+ const headers = new Headers(
170
+ init?.headers ?? (input as Request).headers,
171
+ );
172
+ calls.push({
173
+ url,
174
+ auth: headers.get("authorization"),
175
+ publicApiKey: headers.get("x-copilotcloud-public-api-key"),
176
+ });
177
+ if (url.endsWith("/info")) {
178
+ // Held open: the runtime stays Connecting, so `useAgent` hands
179
+ // back a provisional `ProxiedCopilotRuntimeAgent` instead of the
180
+ // real synced one.
181
+ return new Promise<Response>((resolve) => {
182
+ releaseInfo = resolve;
183
+ });
184
+ }
185
+ return new Response("", { status: 200 });
186
+ }),
187
+ );
188
+
189
+ let agentRef!: {
190
+ headers?: Record<string, string>;
191
+ fetch: typeof fetch;
192
+ };
193
+ const Child = defineComponent({
194
+ setup() {
195
+ const { agent } = useAgent({ agentId: "default" });
196
+ agentRef = agent.value as unknown as typeof agentRef;
197
+ return () => null;
198
+ },
199
+ });
200
+
201
+ let core!: CopilotKitCoreVue;
202
+ const Probe = defineComponent({
203
+ setup() {
204
+ const { copilotkit } = useCopilotKit();
205
+ core = copilotkit.value;
206
+ return () => null;
207
+ },
208
+ });
209
+
210
+ mount(CopilotKitProvider, {
211
+ props: {
212
+ runtimeUrl: "http://rt.test/api/copilotkit",
213
+ headers: (() => ({
214
+ Authorization: `Bearer ${token}`,
215
+ })) as CopilotKitHeadersSource,
216
+ },
217
+ slots: { default: () => [h(Child), h(Probe)] },
218
+ });
219
+
220
+ await nextTick();
221
+ expect(core.getAgent("default")).toBeUndefined();
222
+
223
+ token = "tok-2";
224
+ const callsBeforeConnect = calls.length;
225
+
226
+ // Call the proxy's OWN installed fetch directly — this is exactly what a
227
+ // real `/agent/default/connect` send does, without pulling in the
228
+ // AG-UI stream-parsing machinery a fabricated empty response can't
229
+ // satisfy. `applyHeadersToAgent` installed this as `ɵruntimeFetch` when
230
+ // the provisional was constructed, so it resolves headers fresh on
231
+ // every call, never a baked-in snapshot from construction time.
232
+ await agentRef.fetch(
233
+ "http://rt.test/api/copilotkit/agent/default/connect",
234
+ {
235
+ method: "POST",
236
+ },
237
+ );
238
+
239
+ const connectCalls = calls.slice(callsBeforeConnect);
240
+ expect(connectCalls.length).toBeGreaterThan(0);
241
+ expect(connectCalls.every((c) => c.auth === "Bearer tok-2")).toBe(true);
242
+ // `releaseInfo` is intentionally never called: this test only needs the
243
+ // runtime held in "Connecting" long enough to observe the provisional
244
+ // proxy's own fetch.
245
+ void releaseInfo;
246
+ });
247
+
248
+ it.each([
249
+ { label: "no publicApiKey", publicApiKey: undefined },
250
+ // With a publicApiKey, `ɵwithHeaderDefaults` is no longer a passthrough:
251
+ // it wraps the builder in a NEW closure on every call (non-empty
252
+ // defaults), so this row is the one that actually exercises the memo —
253
+ // the plain-builder row above still passes even without it, because
254
+ // `ɵwithHeaderDefaults` returns the source unchanged when there is
255
+ // nothing to fill in.
256
+ { label: "with publicApiKey (Cloud)", publicApiKey: "pk_test_123" },
257
+ ])(
258
+ "inline builder across 20 prop updates: 0 extra setHeaders ($label)",
259
+ async ({ publicApiKey }) => {
260
+ const calls: Call[] = [];
261
+ vi.stubGlobal("fetch", stubFetch(calls));
262
+
263
+ const { getCore, wrapper } = mountCore({
264
+ runtimeUrl: "http://rt.test/api/copilotkit",
265
+ publicApiKey,
266
+ headers: (() => ({
267
+ Authorization: `Bearer ${token}`,
268
+ })) as CopilotKitHeadersSource,
269
+ });
270
+
271
+ await vi.waitFor(() =>
272
+ expect(getCore().getAgent("default")).toBeDefined(),
273
+ );
274
+
275
+ const setHeadersSpy = vi.spyOn(getCore(), "setHeaders");
276
+
277
+ for (let i = 0; i < 20; i++) {
278
+ // A NEW inline arrow every update: identity churns even though the
279
+ // builder's behavior never does.
280
+ await wrapper.setProps({
281
+ headers: (() => ({
282
+ Authorization: `Bearer ${token}`,
283
+ })) as CopilotKitHeadersSource,
284
+ });
285
+ }
286
+
287
+ await pollFor(() => false, 30);
288
+
289
+ expect(setHeadersSpy).toHaveBeenCalledTimes(0);
290
+ },
291
+ );
292
+
293
+ it("the public API key header is added when headers omit it", async () => {
294
+ const calls: Call[] = [];
295
+ vi.stubGlobal("fetch", stubFetch(calls));
296
+
297
+ mountCore({
298
+ runtimeUrl: "http://rt.test/api/copilotkit",
299
+ publicApiKey: "pk_test_123",
300
+ headers: (() => ({})) as CopilotKitHeadersSource,
301
+ });
302
+
303
+ await vi.waitFor(() => {
304
+ const infoCall = calls.find((c) => c.url.endsWith("/info"));
305
+ expect(infoCall).toBeDefined();
306
+ expect(infoCall?.publicApiKey).toBe("pk_test_123");
307
+ });
308
+ });
309
+
310
+ it("a token change does not re-dispatch the thread context", async () => {
311
+ const calls: Call[] = [];
312
+ vi.stubGlobal("fetch", stubFetch(calls));
313
+
314
+ const Threads = defineComponent({
315
+ setup() {
316
+ useThreads({ agentId: "default" });
317
+ return () => null;
318
+ },
319
+ });
320
+
321
+ let core!: CopilotKitCoreVue;
322
+ const Probe = defineComponent({
323
+ setup() {
324
+ const { copilotkit } = useCopilotKit();
325
+ core = copilotkit.value;
326
+ return () => null;
327
+ },
328
+ });
329
+
330
+ mount(CopilotKitProvider, {
331
+ props: {
332
+ runtimeUrl: "http://rt.test/api/copilotkit",
333
+ headers: (() => ({
334
+ Authorization: `Bearer ${token}`,
335
+ })) as CopilotKitHeadersSource,
336
+ },
337
+ slots: { default: () => [h(Threads), h(Probe)] },
338
+ });
339
+
340
+ await vi.waitFor(() => expect(core.getAgent("default")).toBeDefined());
341
+
342
+ const store = await vi.waitFor(() => {
343
+ const s = core.getThreadStore("default");
344
+ expect(s).toBeDefined();
345
+ return s!;
346
+ });
347
+
348
+ // Wait for the first (mount) context dispatch before spying, so the spy
349
+ // only observes dispatches caused by the token rotation below.
350
+ await vi.waitFor(() => {
351
+ expect(calls.some((c) => c.url.includes("/threads?"))).toBe(true);
352
+ });
353
+
354
+ const setContextSpy = vi.spyOn(store, "setContext");
355
+
356
+ token = "tok-2";
357
+ // A run actually changes the resolved snapshot (routes through
358
+ // `ɵruntimeFetch`, which resolves the builder's now-current token) — a
359
+ // rotation nobody ever reads is not a real test of "the snapshot changed
360
+ // but nothing re-dispatched".
361
+ const agent = core.getAgent("default")!;
362
+ await core.runAgent({ agent }).catch(() => {});
363
+
364
+ // Force a Vue-level trigger entirely unrelated to headers. The OLD
365
+ // values-keyed `headersKey` would have re-evaluated (and seen the
366
+ // now-changed snapshot) the next time ANYTHING pokes Vue's reactivity
367
+ // around `copilotkit` — this is that poke.
368
+ core.setRenderToolCalls([]);
369
+ await pollFor(() => false, 50);
370
+
371
+ expect(setContextSpy).toHaveBeenCalledTimes(0);
372
+ });
373
+
374
+ it("transcribeAudio never re-sends a stale header snapshot key (#1937)", async () => {
375
+ const calls: Call[] = [];
376
+ let signedIn = true;
377
+ vi.stubGlobal(
378
+ "fetch",
379
+ vi.fn(async (input: RequestInfo | URL, init?: RequestInit) => {
380
+ const url = typeof input === "string" ? input : input.toString();
381
+ const headers = new Headers(
382
+ init?.headers ?? (input as Request).headers,
383
+ );
384
+ calls.push({
385
+ url,
386
+ auth: headers.get("authorization"),
387
+ publicApiKey: null,
388
+ });
389
+ if (url.endsWith("/info")) {
390
+ return new Response(
391
+ JSON.stringify({
392
+ version: "1.0.0",
393
+ agents: { default: { name: "default", description: "" } },
394
+ }),
395
+ { status: 200, headers: { "content-type": "application/json" } },
396
+ );
397
+ }
398
+ if (url.endsWith("/transcribe")) {
399
+ return new Response(
400
+ JSON.stringify({ text: "hi", size: 1, type: "audio/webm" }),
401
+ { status: 200, headers: { "content-type": "application/json" } },
402
+ );
403
+ }
404
+ return new Response("{}", { status: 200 });
405
+ }),
406
+ );
407
+
408
+ const { getCore } = mountCore({
409
+ runtimeUrl: "http://rt.test/api/copilotkit",
410
+ headers: (() =>
411
+ signedIn
412
+ ? { Authorization: "Bearer x" }
413
+ : {}) as CopilotKitHeadersSource,
414
+ });
415
+
416
+ await vi.waitFor(() => expect(getCore().getAgent("default")).toBeDefined());
417
+
418
+ // Force an actual resolution while signed in, so the header source's
419
+ // last snapshot is non-empty — otherwise a stale-merge bug has nothing
420
+ // stale to leak, and the assertion below would pass vacuously.
421
+ await transcribeAudio(getCore(), new Blob(["a"], { type: "audio/webm" }));
422
+ const firstCall = calls.find((c) => c.url.endsWith("/transcribe"));
423
+ expect(firstCall?.auth).toBe("Bearer x");
424
+
425
+ calls.length = 0;
426
+ signedIn = false;
427
+
428
+ await transcribeAudio(getCore(), new Blob(["a"], { type: "audio/webm" }));
429
+ const secondCall = calls.find((c) => c.url.endsWith("/transcribe"));
430
+ expect(secondCall).toBeDefined();
431
+ expect(secondCall!.auth).toBeNull();
432
+ });
433
+
434
+ it("a plain HttpAgent's headers refresh via applyHeadersToAgent when ɵheadersGeneration changes, not on header VALUES alone", async () => {
435
+ const directAgent = new HttpAgent({ url: "http://direct.test/agent" });
436
+
437
+ let agentRef!: { headers?: Record<string, string> };
438
+ const Child = defineComponent({
439
+ setup() {
440
+ const { agent } = useAgent({ agentId: "direct" });
441
+ agentRef = agent.value as unknown as typeof agentRef;
442
+ return () => null;
443
+ },
444
+ });
445
+
446
+ const { getCore } = (() => {
447
+ let core!: CopilotKitCoreVue;
448
+ const Probe = defineComponent({
449
+ setup() {
450
+ const { copilotkit } = useCopilotKit();
451
+ core = copilotkit.value;
452
+ return () => null;
453
+ },
454
+ });
455
+ mount(CopilotKitProvider, {
456
+ props: {
457
+ agents__unsafe_dev_only: { direct: directAgent },
458
+ headers: {
459
+ Authorization: "Bearer static-1",
460
+ } as CopilotKitHeadersSource,
461
+ },
462
+ slots: { default: () => [h(Child), h(Probe)] },
463
+ });
464
+ return { getCore: () => core };
465
+ })();
466
+
467
+ await nextTick();
468
+ expect(agentRef.headers?.Authorization).toBe("Bearer static-1");
469
+
470
+ // Mutate the resolved snapshot IN PLACE — no `setHeaders()`, so
471
+ // `ɵheadersGeneration` never bumps. The watcher must not react to this:
472
+ // it is keyed on the generation, never on header VALUES.
473
+ getCore().headers.Authorization = "Bearer mutated-in-place";
474
+ await nextTick();
475
+ expect(agentRef.headers?.Authorization).toBe("Bearer static-1");
476
+
477
+ // A real `setHeaders()` bumps `ɵheadersGeneration` — THAT is what the
478
+ // watcher is keyed on, and it re-applies via `applyHeadersToAgent`.
479
+ getCore().setHeaders({ Authorization: "Bearer static-2" });
480
+ await nextTick();
481
+
482
+ expect(agentRef.headers?.Authorization).toBe("Bearer static-2");
483
+ });
484
+
485
+ it("changing the headers prop re-dispatches the thread context exactly once (onHeadersChanged -> triggerRef)", async () => {
486
+ // The provider subscribes to core's `onHeadersChanged` and calls
487
+ // `triggerRef(copilotkit)` so Vue's reactivity actually re-reads
488
+ // `ɵheadersGeneration`-keyed dependents (`headersKey` in use-threads.ts,
489
+ // the HttpAgent watcher in use-agent.ts) when a real source change
490
+ // lands. Without it, nothing pokes Vue: `ɵheadersGeneration` is a plain
491
+ // property read on a non-reactive core instance, not a ref.
492
+ const calls: Call[] = [];
493
+ vi.stubGlobal("fetch", stubFetch(calls));
494
+
495
+ const Threads = defineComponent({
496
+ setup() {
497
+ useThreads({ agentId: "default" });
498
+ return () => null;
499
+ },
500
+ });
501
+
502
+ let core!: CopilotKitCoreVue;
503
+ const Probe = defineComponent({
504
+ setup() {
505
+ const { copilotkit } = useCopilotKit();
506
+ core = copilotkit.value;
507
+ return () => null;
508
+ },
509
+ });
510
+
511
+ const wrapper = mount(CopilotKitProvider, {
512
+ props: {
513
+ runtimeUrl: "http://rt.test/api/copilotkit",
514
+ headers: {
515
+ Authorization: "Bearer static-1",
516
+ } as CopilotKitHeadersSource,
517
+ },
518
+ slots: { default: () => [h(Threads), h(Probe)] },
519
+ });
520
+
521
+ await vi.waitFor(() => expect(core.getAgent("default")).toBeDefined());
522
+ const store = await vi.waitFor(() => {
523
+ const s = core.getThreadStore("default");
524
+ expect(s).toBeDefined();
525
+ return s!;
526
+ });
527
+ // Wait for the first (mount) context dispatch before spying, so the spy
528
+ // only observes dispatches caused by the prop change below.
529
+ await vi.waitFor(() => {
530
+ expect(calls.some((c) => c.url.includes("/threads?"))).toBe(true);
531
+ });
532
+
533
+ const setContextSpy = vi.spyOn(store, "setContext");
534
+
535
+ // A record-to-record change (the case a strict-reference `setSource`
536
+ // dedup can't collapse) — one real source change, one `onHeadersChanged`.
537
+ await wrapper.setProps({
538
+ headers: { Authorization: "Bearer static-2" } as CopilotKitHeadersSource,
539
+ });
540
+ await pollFor(() => setContextSpy.mock.calls.length > 0, 100);
541
+
542
+ expect(setContextSpy).toHaveBeenCalledTimes(1);
543
+ });
544
+
545
+ it("resolveAgent does not re-run after a builder token rotation + a run + a forced copilotkit trigger", async () => {
546
+ // `resolveAgent`'s watch used to key one of its sources on
547
+ // `JSON.stringify(...headers...)`. A run resolves the builder's CURRENT
548
+ // token and changes that snapshot; the next time ANYTHING pokes Vue's
549
+ // reactivity around `copilotkit` (unrelated to headers), the stale key
550
+ // would differ and (combined with `registeredProxy` forcing the whole
551
+ // watch to skip its diff — see the comment on the watch itself)
552
+ // re-run `resolveAgent`, calling `core.getAgent` and force-triggering
553
+ // `agent` even though nothing about the bound agent itself changed.
554
+ const calls: Call[] = [];
555
+ vi.stubGlobal("fetch", stubFetch(calls));
556
+
557
+ const Child = defineComponent({
558
+ setup() {
559
+ useAgent({ agentId: "default" });
560
+ return () => null;
561
+ },
562
+ });
563
+
564
+ let core!: CopilotKitCoreVue;
565
+ const Probe = defineComponent({
566
+ setup() {
567
+ const { copilotkit } = useCopilotKit();
568
+ core = copilotkit.value;
569
+ return () => null;
570
+ },
571
+ });
572
+
573
+ mount(CopilotKitProvider, {
574
+ props: {
575
+ runtimeUrl: "http://rt.test/api/copilotkit",
576
+ headers: (() => ({
577
+ Authorization: `Bearer ${token}`,
578
+ })) as CopilotKitHeadersSource,
579
+ },
580
+ slots: { default: () => [h(Child), h(Probe)] },
581
+ });
582
+
583
+ await vi.waitFor(() => expect(core.getAgent("default")).toBeDefined());
584
+ await nextTick();
585
+
586
+ token = "tok-2";
587
+ const agent = core.getAgent("default")!;
588
+ await core.runAgent({ agent }).catch(() => {});
589
+
590
+ const getAgentSpy = vi.spyOn(core, "getAgent");
591
+ // Force an unrelated Vue-level trigger on `copilotkit` — same technique
592
+ // as the thread-context test above.
593
+ core.setRenderToolCalls([]);
594
+ await pollFor(() => false, 50);
595
+
596
+ expect(getAgentSpy).toHaveBeenCalledTimes(0);
597
+ });
598
+ });
@@ -106,7 +106,12 @@ describe("CopilotKitProvider", () => {
106
106
  );
107
107
  });
108
108
 
109
- it("supports function-valued headers and refreshes runtime headers", async () => {
109
+ it("supports function-valued headers and resolves the builder's current token at send time (#1937)", async () => {
110
+ // The builder is evaluated when a request is actually sent, never
111
+ // eagerly during render/setup — so this asserts through
112
+ // `resolveHeaders()` (the resolving call a run/connect makes) rather
113
+ // than the passive `headers` snapshot, which stays empty until
114
+ // something has actually resolved it.
110
115
  const authToken = ref("initial");
111
116
  const { getCore } = mountWithProvider(() => h("div"), {
112
117
  runtimeUrl: "/api/copilotkit",
@@ -115,7 +120,7 @@ describe("CopilotKitProvider", () => {
115
120
  }),
116
121
  });
117
122
 
118
- expect(getCore().headers).toMatchObject({
123
+ expect(await getCore().resolveHeaders()).toMatchObject({
119
124
  Authorization: "Bearer initial",
120
125
  });
121
126
 
@@ -123,7 +128,7 @@ describe("CopilotKitProvider", () => {
123
128
  await nextTick();
124
129
  await nextTick();
125
130
 
126
- expect(getCore().headers).toMatchObject({
131
+ expect(await getCore().resolveHeaders()).toMatchObject({
127
132
  Authorization: "Bearer updated",
128
133
  });
129
134
  });