@copilotkit/vue 1.67.1 → 1.68.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.
Files changed (27) hide show
  1. package/PARITY.md +2 -2
  2. package/dist/index.cjs +1 -1
  3. package/dist/index.mjs +2 -2
  4. package/dist/{use-render-activity-message-CaArNmtw.js → use-render-activity-message-DljRBrXN.js} +3401 -3370
  5. package/dist/use-render-activity-message-DljRBrXN.js.map +1 -0
  6. package/dist/use-render-activity-message-bkrPa-92.cjs +85 -0
  7. package/dist/use-render-activity-message-bkrPa-92.cjs.map +1 -0
  8. package/dist/v2/components/chat/CopilotChat.vue.d.ts.map +1 -1
  9. package/dist/v2/components/chat/CopilotChatMessageView.vue.d.ts.map +1 -1
  10. package/dist/v2/hooks/use-agent.d.ts +66 -5
  11. package/dist/v2/hooks/use-agent.d.ts.map +1 -1
  12. package/dist/v2/index.cjs +1 -1
  13. package/dist/v2/index.mjs +1 -1
  14. package/package.json +5 -5
  15. package/src/v2/components/chat/CopilotChat.vue +14 -1
  16. package/src/v2/components/chat/CopilotChatMessageView.vue +5 -7
  17. package/src/v2/components/chat/__tests__/CopilotChat.clearOnFresh.test.ts +14 -25
  18. package/src/v2/components/chat/__tests__/CopilotChat.slots.e2e.test.ts +1 -2
  19. package/src/v2/components/chat/__tests__/CopilotChat.test.ts +2 -3
  20. package/src/v2/components/chat/__tests__/CopilotChatActivityRendering.e2e.test.ts +6 -6
  21. package/src/v2/components/chat/__tests__/MCPAppsUiMessage.e2e.test.ts +14 -0
  22. package/src/v2/hooks/__tests__/use-agent-thread-pinning.test.ts +179 -0
  23. package/src/v2/hooks/use-agent.ts +205 -78
  24. package/dist/use-render-activity-message-CNbp9Fob.cjs +0 -85
  25. package/dist/use-render-activity-message-CNbp9Fob.cjs.map +0 -1
  26. package/dist/use-render-activity-message-CaArNmtw.js.map +0 -1
  27. package/src/v2/hooks/__tests__/use-agent-thread-isolation.test.ts +0 -433
@@ -26,9 +26,7 @@ const ALL_UPDATES: UseAgentUpdate[] = [
26
26
  UseAgentUpdate.OnRunStatusChanged,
27
27
  ];
28
28
 
29
- export interface UseAgentProps {
30
- agentId?: MaybeRefOrGetter<string | undefined>;
31
- threadId?: MaybeRefOrGetter<string | undefined>;
29
+ interface UseAgentPropsBase {
32
30
  updates?: UseAgentUpdate[];
33
31
  /**
34
32
  * Throttle interval (in milliseconds) for re-renders triggered by
@@ -55,65 +53,73 @@ export interface UseAgentProps {
55
53
  throttleMs?: MaybeRefOrGetter<number | undefined>;
56
54
  }
57
55
 
58
- function cloneForThread(
59
- source: AbstractAgent,
60
- threadId: string,
61
- headers: Record<string, string>,
62
- ): AbstractAgent {
63
- const clone = source.clone();
64
- if (clone === source) {
65
- throw new Error(
66
- `useAgent: ${source.constructor.name}.clone() returned the same instance. ` +
67
- "clone() must return a new, independent object.",
68
- );
69
- }
70
-
71
- clone.threadId = threadId;
72
- clone.setMessages([]);
73
- clone.setState({});
74
- if (clone instanceof HttpAgent) {
75
- clone.headers = { ...headers };
76
- }
77
- return clone;
56
+ /**
57
+ * Thread-scoped variant. `agentId`, `runtimeAgentId` and `threadId` are a matched
58
+ * set: together they give this hook a private proxied agent — registered under the
59
+ * local `agentId`, routing outbound to `runtimeAgentId` — that is safe to pin a
60
+ * thread onto. Mirrors React's `UseAgentThreadScopedProps`.
61
+ */
62
+ interface UseAgentThreadScopedProps {
63
+ /**
64
+ * The *local* registry id to register this hook's proxied agent under. Required
65
+ * here: the usual fallbacks (chat configuration, then `DEFAULT_AGENT_ID`) name
66
+ * agents that already exist, and registering over one of those throws.
67
+ */
68
+ agentId: MaybeRefOrGetter<string>;
69
+ /**
70
+ * The runtime agent to route outbound requests to while this hook exposes a
71
+ * distinct local `agentId`. Registers a proxied agent via
72
+ * `CopilotKitCore.registerProxiedAgent`, so several frontend agents (e.g. one
73
+ * per open thread) can mount against one runtime agent.
74
+ *
75
+ * REQUIRES `threadId` — a private agent with no thread to scope behaves like
76
+ * the shared one, minus a registration and a local id to keep unique.
77
+ */
78
+ runtimeAgentId: MaybeRefOrGetter<string>;
79
+ /**
80
+ * Thread to scope the agent's run to. Written onto the underlying agent, so
81
+ * `/agent/run`, `/agent/connect` and `/agent/stop` address this thread.
82
+ *
83
+ * REQUIRES `runtimeAgentId` — an agent resolved by `agentId` alone is shared,
84
+ * so a per-hook thread written onto it would clobber every other holder.
85
+ */
86
+ threadId: MaybeRefOrGetter<string>;
78
87
  }
79
88
 
80
- export const globalThreadCloneMap = new WeakMap<
81
- AbstractAgent,
82
- Map<string, AbstractAgent>
83
- >();
84
-
85
- export function getThreadClone(
86
- registryAgent: AbstractAgent | undefined | null,
87
- threadId: string | undefined | null,
88
- ): AbstractAgent | undefined {
89
- if (!registryAgent || !threadId) return undefined;
90
- return globalThreadCloneMap.get(registryAgent)?.get(threadId);
89
+ /**
90
+ * Default variant: no thread scoping. Binds to the shared agent registered under
91
+ * `agentId` and takes its thread from the surrounding chat configuration, gated
92
+ * on `hasExplicitThreadId`. Mirrors React's `UseAgentUnscopedProps`.
93
+ *
94
+ * `threadId` and `runtimeAgentId` are typed `undefined` rather than omitted so
95
+ * that supplying either alone matches *neither* branch — that is the type-level
96
+ * enforcement of the all-or-nothing rule.
97
+ */
98
+ interface UseAgentUnscopedProps {
99
+ /**
100
+ * Agent to bind to. Resolution precedence: this property, then the surrounding
101
+ * chat configuration's agentId, then the global default.
102
+ */
103
+ agentId?: MaybeRefOrGetter<string | undefined>;
104
+ /** Requires `runtimeAgentId`. See {@link UseAgentThreadScopedProps.threadId}. */
105
+ threadId?: undefined;
106
+ /** Requires `threadId`. See {@link UseAgentThreadScopedProps.runtimeAgentId}. */
107
+ runtimeAgentId?: undefined;
91
108
  }
92
109
 
93
- function getOrCreateThreadClone(
94
- source: AbstractAgent,
95
- threadId: string,
96
- headers: Record<string, string>,
97
- ): AbstractAgent {
98
- let byThread = globalThreadCloneMap.get(source);
99
- if (!byThread) {
100
- byThread = new Map();
101
- globalThreadCloneMap.set(source, byThread);
102
- }
103
-
104
- const existing = byThread.get(threadId);
105
- if (existing) {
106
- existing.threadId = threadId;
107
- if (existing instanceof HttpAgent) {
108
- existing.headers = { ...headers };
109
- }
110
- return existing;
111
- }
112
-
113
- const clone = cloneForThread(source, threadId, headers);
114
- byThread.set(threadId, clone);
115
- return clone;
116
- }
110
+ /**
111
+ * Props for {@link useAgent}. Two valid shapes, nothing in between:
112
+ *
113
+ * - **Bind to an agent** — `useAgent()`, `useAgent({ agentId })`. The shared
114
+ * registry instance; the thread comes from the chat configuration.
115
+ * - **Bind a private agent to a thread** —
116
+ * `useAgent({ agentId, runtimeAgentId, threadId })`. All three required.
117
+ *
118
+ * So `{ agentId, threadId }`, `{ agentId, runtimeAgentId }` and
119
+ * `{ runtimeAgentId, threadId }` are all compile errors, matching React.
120
+ */
121
+ export type UseAgentProps = UseAgentPropsBase &
122
+ (UseAgentThreadScopedProps | UseAgentUnscopedProps);
117
123
 
118
124
  /**
119
125
  * Resolves and subscribes to a CopilotKit agent for the current Vue scope.
@@ -128,13 +134,64 @@ function getOrCreateThreadClone(
128
134
  * ```
129
135
  */
130
136
  export function useAgent(props: UseAgentProps = {}) {
137
+ // `threadId`, `runtimeAgentId` and an explicit `agentId` are all-or-nothing.
138
+ // UseAgentProps rejects a partial set at compile time; these are the runtime
139
+ // backstop for callers TypeScript doesn't reach (plain JS, `as any`). Same
140
+ // three checks, same messages, as React's useAgent.
141
+ const hasThreadId = props.threadId !== undefined;
142
+ const hasRuntimeAgentId = props.runtimeAgentId !== undefined;
143
+ const hasAgentId = props.agentId !== undefined;
144
+
145
+ if (hasThreadId && !hasRuntimeAgentId) {
146
+ throw new Error(
147
+ "useAgent: `threadId` requires `runtimeAgentId`. A threadId is written onto a " +
148
+ "single agent, but an agent resolved by agentId alone is shared, so scoping a " +
149
+ "thread to it would clobber other useAgent callers. Pass a distinct local `agentId` " +
150
+ 'and the runtime agent to route to, e.g. useAgent({ agentId: "chat-1", ' +
151
+ 'runtimeAgentId: "assistant", threadId }).',
152
+ );
153
+ }
154
+
155
+ if (hasRuntimeAgentId && !hasThreadId) {
156
+ throw new Error(
157
+ "useAgent: `runtimeAgentId` requires `threadId`. A proxied agent exists to scope a " +
158
+ "thread to a private instance; without a threadId it behaves like the shared agent " +
159
+ "while adding a registration and a local agentId to keep unique. Either pass the " +
160
+ "thread, or bind to the agent directly with useAgent({ agentId }).",
161
+ );
162
+ }
163
+
164
+ if (hasRuntimeAgentId && !hasAgentId) {
165
+ throw new Error(
166
+ "useAgent: `runtimeAgentId` requires an explicit `agentId`. The proxied agent is " +
167
+ "registered under `agentId`, and the usual fallbacks (chat configuration, then " +
168
+ `"${DEFAULT_AGENT_ID}") name agents that already exist — registering over one throws ` +
169
+ "or shadows it. Pick a local id for this hook, e.g. " +
170
+ 'useAgent({ agentId: "chat-1", runtimeAgentId: "assistant", threadId }).',
171
+ );
172
+ }
173
+
174
+ // After the guards, all three are present or none are.
175
+ const isThreadScoped = hasRuntimeAgentId;
176
+
131
177
  const chatConfig = useCopilotChatConfiguration();
132
178
  const agentId = computed(
133
179
  () =>
134
180
  toValue(props.agentId) ?? chatConfig.value?.agentId ?? DEFAULT_AGENT_ID,
135
181
  );
136
- const threadId = computed(
137
- () => toValue(props.threadId) ?? chatConfig.value?.threadId,
182
+ const runtimeAgentId = computed(() =>
183
+ isThreadScoped ? toValue(props.runtimeAgentId) : undefined,
184
+ );
185
+ // Mirrors React: an explicit `threadId` prop wins, otherwise the chat
186
+ // configuration's thread, gated on `hasExplicitThreadId` so a
187
+ // ThreadsProvider-minted placeholder UUID doesn't overwrite the agent's own
188
+ // auto-minted one (both are random and useless to the backend).
189
+ const resolvedThreadId = computed(
190
+ () =>
191
+ toValue(props.threadId) ??
192
+ (chatConfig.value?.hasExplicitThreadId
193
+ ? chatConfig.value.threadId
194
+ : undefined),
138
195
  );
139
196
  const { copilotkit } = useCopilotKit();
140
197
  const updateFlags = computed(() => props.updates ?? ALL_UPDATES);
@@ -165,22 +222,79 @@ export function useAgent(props: UseAgentProps = {}) {
165
222
  return provisional;
166
223
  };
167
224
 
225
+ // On the thread-scoped path this hook owns an agent registered under
226
+ // `agentId` that routes to `runtimeAgentId`. Register/unregister as one
227
+ // balanced watcher — the cleanup runs both when the ids change and when the
228
+ // scope is disposed. Mirrors React's registration effect, including its dep
229
+ // set (core identity plus the two ids).
230
+ const registeredProxy = shallowRef<AbstractAgent | null>(null);
231
+ if (isThreadScoped) {
232
+ watch(
233
+ [agentId, runtimeAgentId, () => copilotkit.value],
234
+ ([id, rtId], _old, onCleanup) => {
235
+ const { agent: proxy, unregister } =
236
+ copilotkit.value.registerProxiedAgent({
237
+ agentId: id as string,
238
+ runtimeAgentId: rtId as string,
239
+ });
240
+ provisionalAgentCache.delete(id as string);
241
+ registeredProxy.value = proxy;
242
+ onCleanup(() => {
243
+ unregister();
244
+ registeredProxy.value = null;
245
+ });
246
+ },
247
+ { immediate: true },
248
+ );
249
+ }
250
+
168
251
  const resolveAgent = () => {
169
252
  const id = agentId.value;
170
- const resolvedThreadId = threadId.value;
171
- const cacheKey = resolvedThreadId ? `${id}:${resolvedThreadId}` : id;
172
253
  const core = copilotkit.value;
254
+
255
+ // Proxied path: this hook registered its own agent, so bypass the shared
256
+ // registry lookup. Until registration lands, hand back a provisional proxy
257
+ // so `agent` is never null and its identity stays stable.
258
+ if (isThreadScoped) {
259
+ const proxy = registeredProxy.value;
260
+ if (proxy) {
261
+ provisionalAgentCache.delete(id);
262
+ const shouldForceUpdate = agent.value === proxy;
263
+ agent.value = proxy;
264
+ subscriptionAgent.value = proxy;
265
+ if (shouldForceUpdate) triggerRef(agent);
266
+ return;
267
+ }
268
+
269
+ const cachedProxy = provisionalAgentCache.get(id);
270
+ if (cachedProxy) {
271
+ cachedProxy.headers = { ...core.headers };
272
+ agent.value = cachedProxy;
273
+ subscriptionAgent.value = cachedProxy;
274
+ return;
275
+ }
276
+
277
+ const provisionalProxy = new ProxiedCopilotRuntimeAgent({
278
+ runtimeUrl: core.runtimeUrl,
279
+ agentId: id,
280
+ runtimeAgentId: runtimeAgentId.value as string,
281
+ transport: core.runtimeTransport,
282
+ runtimeMode: "pending",
283
+ });
284
+ provisionalProxy.headers = { ...core.headers };
285
+ provisionalAgentCache.set(id, provisionalProxy);
286
+ agent.value = provisionalProxy;
287
+ subscriptionAgent.value = provisionalProxy;
288
+ return;
289
+ }
290
+
173
291
  const existing = core.getAgent(id);
174
292
  if (existing) {
175
- provisionalAgentCache.delete(cacheKey);
176
293
  provisionalAgentCache.delete(id);
177
294
 
178
- const resolvedAgent = resolvedThreadId
179
- ? getOrCreateThreadClone(existing, resolvedThreadId, core.headers)
180
- : existing;
181
- const shouldForceUpdate = agent.value === resolvedAgent;
182
- agent.value = resolvedAgent;
183
- subscriptionAgent.value = resolvedAgent;
295
+ const shouldForceUpdate = agent.value === existing;
296
+ agent.value = existing;
297
+ subscriptionAgent.value = existing;
184
298
  if (shouldForceUpdate) {
185
299
  triggerRef(agent);
186
300
  }
@@ -196,12 +310,9 @@ export function useAgent(props: UseAgentProps = {}) {
196
310
  status === CopilotKitCoreRuntimeConnectionStatus.Connecting ||
197
311
  status === CopilotKitCoreRuntimeConnectionStatus.Error)
198
312
  ) {
199
- const cached = provisionalAgentCache.get(cacheKey);
313
+ const cached = provisionalAgentCache.get(id);
200
314
  if (cached) {
201
315
  cached.headers = { ...core.headers };
202
- if (resolvedThreadId) {
203
- cached.threadId = resolvedThreadId;
204
- }
205
316
  agent.value = cached;
206
317
  subscriptionAgent.value = cached;
207
318
  return;
@@ -213,10 +324,7 @@ export function useAgent(props: UseAgentProps = {}) {
213
324
  core.runtimeTransport,
214
325
  core.headers,
215
326
  );
216
- if (resolvedThreadId) {
217
- provisional.threadId = resolvedThreadId;
218
- }
219
- provisionalAgentCache.set(cacheKey, provisional);
327
+ provisionalAgentCache.set(id, provisional);
220
328
  agent.value = provisional;
221
329
  subscriptionAgent.value = provisional;
222
330
  return;
@@ -238,6 +346,7 @@ export function useAgent(props: UseAgentProps = {}) {
238
346
  watch(
239
347
  [
240
348
  agentId,
349
+ registeredProxy,
241
350
  () => copilotkit.value.agents,
242
351
  () => copilotkit.value.runtimeConnectionStatus,
243
352
  () => copilotkit.value.runtimeUrl,
@@ -248,12 +357,30 @@ export function useAgent(props: UseAgentProps = {}) {
248
357
  a.localeCompare(b),
249
358
  ),
250
359
  ),
251
- threadId,
252
360
  ],
253
361
  resolveAgent,
254
362
  { immediate: true },
255
363
  );
256
364
 
365
+ // Pin the resolved thread onto the current agent. AbstractAgent auto-mints a
366
+ // UUID when none is given, so without this the agent would ship its own random
367
+ // id in /agent/run, /agent/connect and /agent/stop. Deps are deliberately just
368
+ // the agent and the thread — matching React — so status/header churn cannot
369
+ // re-fire this and overwrite a thread `CopilotChat` pinned for its own chat.
370
+ // `() => agent.value`, not `agent`: Vue sets `forceTrigger` when any array
371
+ // watch source is a shallow ref, which would re-run this on every
372
+ // `triggerRef(agent)` — i.e. every streamed message — and re-pin a thread that
373
+ // `CopilotChat` may have deliberately overridden for the chat it renders. A
374
+ // getter fires only when the identity actually changes.
375
+ watch(
376
+ [() => agent.value, resolvedThreadId],
377
+ ([currentAgent, threadId]) => {
378
+ if (!currentAgent || !threadId) return;
379
+ currentAgent.threadId = threadId as string;
380
+ },
381
+ { immediate: true },
382
+ );
383
+
257
384
  watch(
258
385
  [
259
386
  subscriptionAgent,