@copilotkit/vue 1.61.0 → 1.61.2

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.
@@ -1,17 +1,39 @@
1
1
  import { computed, onScopeDispose, shallowRef, watch } from "vue";
2
2
  import type { ComputedRef, Ref } from "vue";
3
+ import { buildResumeArray, isInterruptExpired } from "@ag-ui/client";
4
+ import type { Interrupt, RunAgentResult } from "@ag-ui/client";
3
5
  import { useCopilotKit } from "../providers/useCopilotKit";
4
6
  import { useAgent } from "./use-agent";
5
7
  import type {
6
8
  InterruptEvent,
7
9
  InterruptHandlerProps,
8
10
  InterruptRenderProps,
11
+ InterruptResolveFn,
12
+ InterruptCancelFn,
9
13
  } from "../types/interrupt";
10
14
 
11
- export type { InterruptEvent, InterruptHandlerProps, InterruptRenderProps };
15
+ export type {
16
+ InterruptEvent,
17
+ InterruptHandlerProps,
18
+ InterruptRenderProps,
19
+ Interrupt,
20
+ };
12
21
 
13
22
  const INTERRUPT_EVENT_NAME = "on_interrupt";
14
23
 
24
+ /** Internal accumulator response shape consumed by buildResumeArray. */
25
+ type ResumeResponse =
26
+ | { status: "resolved"; payload?: unknown }
27
+ | { status: "cancelled" };
28
+
29
+ /**
30
+ * Normalized pending interrupt. `legacy` carries the custom-event payload;
31
+ * `standard` carries the AG-UI `outcome:"interrupt"` interrupts array.
32
+ */
33
+ type PendingInterrupt =
34
+ | { kind: "legacy"; event: InterruptEvent }
35
+ | { kind: "standard"; interrupts: Interrupt[] };
36
+
15
37
  type InterruptHandlerFn<TValue, TResult> = (
16
38
  props: InterruptHandlerProps<TValue>,
17
39
  ) => TResult | PromiseLike<TResult>;
@@ -39,7 +61,14 @@ export interface UseInterruptResult<TValue = unknown, TResult = never> {
39
61
  interrupt: Ref<InterruptEvent<TValue> | null>;
40
62
  result: Ref<InterruptResult<TValue, TResult>>;
41
63
  hasInterrupt: ComputedRef<boolean>;
42
- resolveInterrupt: (response: unknown) => void;
64
+ /** Resolve the pending interrupt. Standard: records resolved response and submits when all addressed. Legacy: resumes via forwardedProps.command. */
65
+ resolve: InterruptResolveFn;
66
+ /** Alias of resolve for back-compat. */
67
+ resolveInterrupt: InterruptResolveFn;
68
+ /** Cancel the pending interrupt. Standard: records cancelled response and submits when all addressed. */
69
+ cancel: InterruptCancelFn;
70
+ /** Alias of cancel for back-compat. */
71
+ cancelInterrupt: InterruptCancelFn;
43
72
  slotProps: ComputedRef<InterruptRenderProps<
44
73
  TValue,
45
74
  InterruptResult<TValue, TResult>
@@ -72,16 +101,27 @@ function normalizeAsyncResult<TValue>(
72
101
  });
73
102
  }
74
103
 
104
+ /** Derive the legacy-compatible `event` for any pending interrupt. */
105
+ function toLegacyEvent(pending: PendingInterrupt): InterruptEvent {
106
+ if (pending.kind === "legacy") return pending.event;
107
+ return { name: INTERRUPT_EVENT_NAME, value: pending.interrupts[0] };
108
+ }
109
+
75
110
  /**
76
- * Vue composable for handling `on_interrupt` custom events from an agent.
111
+ * Vue composable for handling agent interrupts with optional filtering,
112
+ * preprocessing, and resume behavior.
77
113
  *
78
- * It tracks the latest pending interrupt, optionally derives UI data via
79
- * `handler`, and can publish slot state into `CopilotChat` so consumers render
80
- * interrupts through the `#interrupt` slot instead of render functions/TSX.
114
+ * Supports both the AG-UI standard interrupt flow (`RUN_FINISHED` with
115
+ * `outcome:"interrupt"`) and the legacy custom-event flow (`on_interrupt`).
116
+ * For standard interrupts, `slotProps` receives `interrupt` (the primary one)
117
+ * and `interrupts` (the full open set); call `resolve(payload)` to resume or
118
+ * `cancel()` to cancel. Resuming addresses the targeted interrupt and, once
119
+ * every open interrupt is addressed, submits a single spec `resume` array via
120
+ * `copilotkit.runAgent`.
81
121
  *
82
122
  * @example
83
123
  * ```ts
84
- * const { interrupt, hasInterrupt, resolveInterrupt } = useInterrupt({
124
+ * const { interrupt, hasInterrupt, resolve, cancel } = useInterrupt({
85
125
  * handler: ({ event }) => ({ label: String(event.value) }),
86
126
  * });
87
127
  * ```
@@ -91,40 +131,64 @@ export function useInterrupt<TValue = unknown, TResult = never>(
91
131
  ): UseInterruptResult<TValue, TResult> {
92
132
  const { copilotkit } = useCopilotKit();
93
133
  const { agent } = useAgent({ agentId: config.agentId });
94
- const interrupt = shallowRef<InterruptEvent<TValue> | null>(null);
134
+ const pending = shallowRef<PendingInterrupt | null>(null);
95
135
  const result = shallowRef<InterruptResult<TValue, TResult>>(null);
96
136
 
137
+ // Accumulated per-interrupt responses for the current standard interrupt set.
138
+ const responses: Record<string, ResumeResponse> = {};
139
+
97
140
  watch(
98
141
  agent,
99
142
  (resolvedAgent, _previousAgent, onCleanup) => {
100
143
  if (!resolvedAgent) {
101
- interrupt.value = null;
144
+ pending.value = null;
102
145
  result.value = null;
103
146
  return;
104
147
  }
105
148
 
106
- let localInterrupt: InterruptEvent<TValue> | null = null;
149
+ let localLegacy: InterruptEvent<TValue> | null = null;
150
+ let localStandard: Interrupt[] | null = null;
151
+
107
152
  const subscription = resolvedAgent.subscribe({
108
153
  onCustomEvent: ({ event }) => {
109
154
  if (event.name === INTERRUPT_EVENT_NAME) {
110
- localInterrupt = {
155
+ localLegacy = {
111
156
  name: event.name,
112
157
  value: event.value as TValue,
113
158
  };
114
159
  }
115
160
  },
161
+ onRunFinishedEvent: (params) => {
162
+ if (params.outcome === "interrupt") {
163
+ localStandard = params.interrupts;
164
+ }
165
+ },
116
166
  onRunStartedEvent: () => {
117
- localInterrupt = null;
118
- interrupt.value = null;
167
+ localLegacy = null;
168
+ localStandard = null;
169
+ // Reset accumulated responses for the new run.
170
+ for (const k of Object.keys(responses)) {
171
+ delete responses[k];
172
+ }
173
+ pending.value = null;
119
174
  },
120
175
  onRunFinalized: () => {
121
- if (localInterrupt) {
122
- interrupt.value = localInterrupt;
123
- localInterrupt = null;
176
+ // Standard wins if both somehow appear for one run.
177
+ if (localStandard && localStandard.length > 0) {
178
+ pending.value = { kind: "standard", interrupts: localStandard };
179
+ } else if (localLegacy) {
180
+ pending.value = { kind: "legacy", event: localLegacy };
124
181
  }
182
+ localLegacy = null;
183
+ localStandard = null;
125
184
  },
126
185
  onRunFailed: () => {
127
- localInterrupt = null;
186
+ localLegacy = null;
187
+ localStandard = null;
188
+ for (const k of Object.keys(responses)) {
189
+ delete responses[k];
190
+ }
191
+ pending.value = null;
128
192
  },
129
193
  });
130
194
 
@@ -133,38 +197,122 @@ export function useInterrupt<TValue = unknown, TResult = never>(
133
197
  { immediate: true },
134
198
  );
135
199
 
136
- const resolveInterrupt = (response: unknown) => {
200
+ /** Submit the accumulated standard responses once all open interrupts are addressed. */
201
+ const submitStandardIfComplete = async (
202
+ interrupts: Interrupt[],
203
+ ): Promise<RunAgentResult | void> => {
204
+ const allAddressed = interrupts.every((i) => responses[i.id]);
205
+ if (!allAddressed) return;
206
+
207
+ const expired = interrupts.find((i) => isInterruptExpired(i));
208
+ if (expired) {
209
+ console.error(
210
+ `[CopilotKit] useInterrupt: interrupt ${expired.id} expired at ${expired.expiresAt}; not resuming.`,
211
+ );
212
+ for (const k of Object.keys(responses)) {
213
+ delete responses[k];
214
+ }
215
+ pending.value = null;
216
+ return;
217
+ }
218
+
219
+ const resume = buildResumeArray(interrupts, responses);
220
+ for (const k of Object.keys(responses)) {
221
+ delete responses[k];
222
+ }
223
+ const resolvedAgent = agent.value;
224
+ if (!resolvedAgent) return;
225
+ try {
226
+ return await copilotkit.value.runAgent({ agent: resolvedAgent, resume });
227
+ } catch (err) {
228
+ console.error(
229
+ "[CopilotKit] useInterrupt resolve: runAgent rejected; clearing pending + rethrowing",
230
+ err,
231
+ );
232
+ pending.value = null;
233
+ throw err;
234
+ }
235
+ };
236
+
237
+ const resolve: InterruptResolveFn = async (payload?, interruptId?) => {
238
+ const current = pending.value;
239
+ if (!current) return;
240
+
137
241
  const resolvedAgent = agent.value;
138
242
  if (!resolvedAgent) return;
139
243
 
140
- const interruptEventValue = interrupt.value?.value;
141
- interrupt.value = null;
142
- void copilotkit.value
143
- .runAgent({
144
- agent: resolvedAgent,
145
- forwardedProps: {
146
- command: {
147
- resume: response,
148
- interruptEvent: interruptEventValue,
244
+ if (current.kind === "legacy") {
245
+ const interruptEventValue = current.event.value;
246
+ try {
247
+ return await copilotkit.value.runAgent({
248
+ agent: resolvedAgent,
249
+ forwardedProps: {
250
+ command: {
251
+ resume: payload,
252
+ interruptEvent: interruptEventValue,
253
+ },
149
254
  },
150
- },
151
- })
152
- .catch((error) => {
255
+ });
256
+ } catch (err) {
153
257
  console.error(
154
- "[CopilotKit] useInterrupt: failed to resume agent:",
155
- error,
258
+ "[CopilotKit] useInterrupt resolve: runAgent rejected; clearing pending + rethrowing",
259
+ err,
156
260
  );
157
- });
261
+ pending.value = null;
262
+ throw err;
263
+ }
264
+ }
265
+
266
+ if (current.interrupts.length > 1 && interruptId === undefined) {
267
+ console.warn(
268
+ `[CopilotKit] useInterrupt: resolve()/cancel() called without an interruptId while ${current.interrupts.length} interrupts are open; defaulting to the first. Pass an interruptId to address a specific interrupt.`,
269
+ );
270
+ }
271
+ const id = interruptId ?? current.interrupts[0]?.id;
272
+ if (!id) return;
273
+ responses[id] = { status: "resolved", payload };
274
+ return submitStandardIfComplete(current.interrupts);
158
275
  };
159
276
 
277
+ const cancel: InterruptCancelFn = async (interruptId?) => {
278
+ const current = pending.value;
279
+ if (!current) return;
280
+
281
+ if (current.kind === "legacy") {
282
+ // Legacy interrupts have no cancel semantics; dismiss without resuming.
283
+ console.warn(
284
+ "[CopilotKit] useInterrupt: cancel() is not supported for legacy on_interrupt interrupts; dismissing.",
285
+ );
286
+ pending.value = null;
287
+ return;
288
+ }
289
+
290
+ if (current.interrupts.length > 1 && interruptId === undefined) {
291
+ console.warn(
292
+ `[CopilotKit] useInterrupt: resolve()/cancel() called without an interruptId while ${current.interrupts.length} interrupts are open; defaulting to the first. Pass an interruptId to address a specific interrupt.`,
293
+ );
294
+ }
295
+ const id = interruptId ?? current.interrupts[0]?.id;
296
+ if (!id) return;
297
+ responses[id] = { status: "cancelled" };
298
+ return submitStandardIfComplete(current.interrupts);
299
+ };
300
+
301
+ // Keep resolveInterrupt and cancelInterrupt as aliases for back-compat.
302
+ const resolveInterrupt: InterruptResolveFn = resolve;
303
+ const cancelInterrupt: InterruptCancelFn = cancel;
304
+
160
305
  watch(
161
- interrupt,
162
- (pendingEvent, _previous, onCleanup) => {
163
- if (!pendingEvent) {
306
+ pending,
307
+ (currentPending, _previous, onCleanup) => {
308
+ if (!currentPending) {
164
309
  result.value = null;
165
310
  return;
166
311
  }
167
- if (config.enabled && !config.enabled(pendingEvent)) {
312
+ const legacyEvent = toLegacyEvent(
313
+ currentPending,
314
+ ) as InterruptEvent<TValue>;
315
+ if (config.enabled && !config.enabled(legacyEvent)) {
168
316
  result.value = null;
169
317
  return;
170
318
  }
@@ -176,8 +324,15 @@ export function useInterrupt<TValue = unknown, TResult = never>(
176
324
 
177
325
  let cancelled = false;
178
326
  const maybePromise = handler({
179
- event: pendingEvent,
180
- resolve: resolveInterrupt,
327
+ event: legacyEvent,
328
+ interrupt:
329
+ currentPending.kind === "standard"
330
+ ? (currentPending.interrupts[0] ?? null)
331
+ : null,
332
+ interrupts:
333
+ currentPending.kind === "standard" ? currentPending.interrupts : [],
334
+ resolve,
335
+ cancel,
181
336
  });
182
337
 
183
338
  if (isPromiseLike(maybePromise)) {
@@ -204,17 +359,32 @@ export function useInterrupt<TValue = unknown, TResult = never>(
204
359
  { immediate: true },
205
360
  );
206
361
 
362
+ // Compute the legacy-compat "interrupt" ref for existing consumers (InterruptEvent shape).
363
+ const interrupt = computed<InterruptEvent<TValue> | null>(() => {
364
+ if (!pending.value) return null;
365
+ return toLegacyEvent(pending.value) as InterruptEvent<TValue>;
366
+ });
367
+
207
368
  const slotProps = computed<InterruptRenderProps<
208
369
  TValue,
209
370
  InterruptResult<TValue, TResult>
210
371
  > | null>(() => {
211
- if (!interrupt.value) return null;
212
- if (config.enabled && !config.enabled(interrupt.value)) return null;
372
+ const currentPending = pending.value;
373
+ if (!currentPending) return null;
374
+ const legacyEvent = toLegacyEvent(currentPending) as InterruptEvent<TValue>;
375
+ if (config.enabled && !config.enabled(legacyEvent)) return null;
213
376
 
214
377
  return {
215
- event: interrupt.value,
378
+ event: legacyEvent,
379
+ interrupt:
380
+ currentPending.kind === "standard"
381
+ ? (currentPending.interrupts[0] ?? null)
382
+ : null,
383
+ interrupts:
384
+ currentPending.kind === "standard" ? currentPending.interrupts : [],
216
385
  result: result.value,
217
- resolve: resolveInterrupt,
386
+ resolve,
387
+ cancel,
218
388
  };
219
389
  });
220
390
 
@@ -226,6 +396,7 @@ export function useInterrupt<TValue = unknown, TResult = never>(
226
396
  }
227
397
 
228
398
  core.setInterruptState(
399
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
229
400
  nextSlotProps as InterruptRenderProps<any, any> | null,
230
401
  );
231
402
  const publishedState = nextSlotProps;
@@ -250,10 +421,13 @@ export function useInterrupt<TValue = unknown, TResult = never>(
250
421
  });
251
422
 
252
423
  return {
253
- interrupt: interrupt as Ref<InterruptEvent<TValue> | null>,
424
+ interrupt: interrupt as unknown as Ref<InterruptEvent<TValue> | null>,
254
425
  result: result as Ref<InterruptResult<TValue, TResult>>,
255
426
  hasInterrupt: computed(() => slotProps.value !== null),
427
+ resolve,
256
428
  resolveInterrupt,
429
+ cancel,
430
+ cancelInterrupt,
257
431
  slotProps: slotProps as ComputedRef<InterruptRenderProps<
258
432
  TValue,
259
433
  InterruptResult<TValue, TResult>
@@ -16,6 +16,7 @@ import type {
16
16
  FrontendTool,
17
17
  } from "@copilotkit/core";
18
18
  import { schemaToJsonSchema } from "@copilotkit/shared";
19
+ import type { RuntimeLicenseStatus } from "@copilotkit/shared";
19
20
  import { zodToJsonSchema } from "zod-to-json-schema";
20
21
  import { CopilotKitCoreVue } from "../lib/vue-core";
21
22
  import { createA2UIMessageRenderer } from "../components/A2UIMessageRenderer";
@@ -37,8 +38,8 @@ import { CopilotKitKey, SandboxFunctionsKey } from "./keys";
37
38
  import {
38
39
  LicenseContextKey,
39
40
  createLicenseContextValue,
40
- type LicenseContextValue,
41
41
  } from "./license-context";
42
+ import type { LicenseContextValue } from "./license-context";
42
43
  import CopilotKitInspector from "../components/CopilotKitInspector.vue";
43
44
  import LicenseWarningBanner from "../components/LicenseWarningBanner.vue";
44
45
  import type { CopilotKitProviderProps } from "./CopilotKitProvider.types";
@@ -290,7 +291,7 @@ const allRenderCustomMessages = computed(
290
291
  );
291
292
  const runtimeA2UIEnabled = ref(false);
292
293
  const runtimeOpenGenerativeUIEnabled = ref(false);
293
- const runtimeLicenseStatus = ref<string | undefined>(undefined);
294
+ const runtimeLicenseStatus = ref<RuntimeLicenseStatus | undefined>(undefined);
294
295
  const openGenerativeUIActive = computed(
295
296
  () => runtimeOpenGenerativeUIEnabled.value || !!props.openGenerativeUI,
296
297
  );
@@ -616,11 +617,8 @@ provide(CopilotKitKey, {
616
617
  provide(SandboxFunctionsKey, sandboxFunctions);
617
618
 
618
619
  // License context — driven by server-reported `/info` license status.
619
- // Stays at the permissive default (`createLicenseContextValue(null)`)
620
- // to mirror React's current provider behavior; banner rendering below
621
- // is the sole consumer of `runtimeLicenseStatus`.
622
620
  const licenseContextValue = computed<LicenseContextValue>(() =>
623
- createLicenseContextValue(null),
621
+ createLicenseContextValue(runtimeLicenseStatus.value),
624
622
  );
625
623
  provide(LicenseContextKey, licenseContextValue);
626
624
 
@@ -1,15 +1,31 @@
1
+ import type { Interrupt, RunAgentResult } from "@ag-ui/client";
2
+
1
3
  export interface InterruptEvent<TValue = unknown> {
2
4
  name: string;
3
5
  value: TValue;
4
6
  }
5
7
 
8
+ export type InterruptResolveFn = (
9
+ payload?: unknown,
10
+ interruptId?: string,
11
+ ) => Promise<RunAgentResult | void>;
12
+ export type InterruptCancelFn = (
13
+ interruptId?: string,
14
+ ) => Promise<RunAgentResult | void>;
15
+
6
16
  export interface InterruptHandlerProps<TValue = unknown> {
7
17
  event: InterruptEvent<TValue>;
8
- resolve: (response: unknown) => void;
18
+ interrupt: Interrupt | null;
19
+ interrupts: Interrupt[];
20
+ resolve: InterruptResolveFn;
21
+ cancel: InterruptCancelFn;
9
22
  }
10
23
 
11
24
  export interface InterruptRenderProps<TValue = unknown, TResult = unknown> {
12
25
  event: InterruptEvent<TValue>;
26
+ interrupt: Interrupt | null;
27
+ interrupts: Interrupt[];
13
28
  result: TResult;
14
- resolve: (response: unknown) => void;
29
+ resolve: InterruptResolveFn;
30
+ cancel: InterruptCancelFn;
15
31
  }