@copilotkit/core 1.56.2 → 1.56.4

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.
package/dist/index.cjs CHANGED
@@ -1,4 +1,5 @@
1
1
  Object.defineProperty(exports, Symbol.toStringTag, { value: 'Module' });
2
+ let _tanstack_pacer = require("@tanstack/pacer");
2
3
  let _ag_ui_client = require("@ag-ui/client");
3
4
  let _copilotkit_shared = require("@copilotkit/shared");
4
5
  let rxjs = require("rxjs");
@@ -127,6 +128,8 @@ function ɵobservePhoenixSocketHealth$(socketSignals$, maxConsecutiveErrors) {
127
128
  //#endregion
128
129
  //#region src/intelligence-agent.ts
129
130
  const CLIENT_AG_UI_EVENT = "ag_ui_event";
131
+ const REPLAY_COMPLETE_EVENT = "replay_complete";
132
+ const STREAM_IDLE_EVENT = "stream_idle";
130
133
  const STOP_RUN_EVENT = "stop_run";
131
134
  var AgentThreadLockedError = class extends Error {
132
135
  constructor(threadId) {
@@ -138,7 +141,7 @@ var IntelligenceAgent = class IntelligenceAgent extends _ag_ui_client.AbstractAg
138
141
  config;
139
142
  socket = null;
140
143
  activeChannel = null;
141
- runId = null;
144
+ canonicalRunId = null;
142
145
  sharedState;
143
146
  constructor(config, sharedState = { lastSeenEventIds: /* @__PURE__ */ new Map() }) {
144
147
  super();
@@ -176,7 +179,11 @@ var IntelligenceAgent = class IntelligenceAgent extends _ag_ui_client.AbstractAg
176
179
  try {
177
180
  this.isRunning = true;
178
181
  this.agentId = this.agentId ?? (0, _ag_ui_client.randomUUID)();
179
- const input = this.prepareRunAgentInput(parameters);
182
+ const effectiveParameters = parameters?.runId || !this.canonicalRunId ? parameters : {
183
+ ...parameters,
184
+ runId: this.canonicalRunId
185
+ };
186
+ const input = this.prepareRunAgentInput(effectiveParameters);
180
187
  let result;
181
188
  const previousMessageIds = new Set(this.messages.map((m) => m.id));
182
189
  const subscribers = [
@@ -215,14 +222,14 @@ var IntelligenceAgent = class IntelligenceAgent extends _ag_ui_client.AbstractAg
215
222
  }
216
223
  }
217
224
  abortRun() {
218
- if (this.activeChannel && this.runId) {
225
+ if (this.activeChannel && this.canonicalRunId) {
219
226
  const fallback = setTimeout(() => clear(), 5e3);
220
227
  const clear = () => {
221
228
  clearTimeout(fallback);
222
229
  this.detachActiveRun();
223
230
  this.cleanup();
224
231
  };
225
- this.activeChannel.push(STOP_RUN_EVENT, { run_id: this.runId }).receive("ok", clear).receive("error", clear).receive("timeout", clear);
232
+ this.activeChannel.push(STOP_RUN_EVENT, { run_id: this.canonicalRunId }).receive("ok", clear).receive("error", clear).receive("timeout", clear);
226
233
  } else {
227
234
  this.detachActiveRun();
228
235
  this.cleanup();
@@ -234,11 +241,15 @@ var IntelligenceAgent = class IntelligenceAgent extends _ag_ui_client.AbstractAg
234
241
  */
235
242
  run(input) {
236
243
  this.threadId = input.threadId;
237
- this.runId = input.runId;
238
- return (0, rxjs.defer)(() => this.requestJoinCredentials$("run", input)).pipe((0, rxjs.switchMap)((credentials) => this.observeThread$(input, credentials, {
239
- completeOnRunError: false,
240
- streamMode: "run"
241
- })));
244
+ this.canonicalRunId = input.runId;
245
+ return (0, rxjs.defer)(() => this.requestJoinCredentials$("run", input)).pipe((0, rxjs.switchMap)((credentials) => {
246
+ if (credentials === null) return (0, rxjs.throwError)(() => /* @__PURE__ */ new Error("REST run request returned no credentials"));
247
+ const canonicalInput = this.applyCanonicalRunIdentity(input, credentials, { fallbackToInputRunId: true });
248
+ return this.observeThread$(canonicalInput, credentials, {
249
+ completeOnRunError: false,
250
+ streamMode: "run"
251
+ });
252
+ }));
242
253
  }
243
254
  /**
244
255
  * Reconnect to an existing thread by fetching websocket credentials and
@@ -246,21 +257,15 @@ var IntelligenceAgent = class IntelligenceAgent extends _ag_ui_client.AbstractAg
246
257
  */
247
258
  connect(input) {
248
259
  this.threadId = input.threadId;
249
- this.runId = input.runId;
250
- return (0, rxjs.defer)(() => this.requestConnectPlan$(input)).pipe((0, rxjs.switchMap)((plan) => {
251
- if (plan === null) return rxjs.EMPTY;
252
- if (plan.mode === "bootstrap") {
253
- this.setLastSeenEventId(input.threadId, plan.latestEventId);
254
- for (const event of plan.events) this.updateRunIdFromEvent(event);
255
- return (0, rxjs.from)(plan.events);
256
- }
257
- this.setLastSeenEventId(input.threadId, plan.joinFromEventId);
258
- for (const event of plan.events) this.updateRunIdFromEvent(event);
259
- return (0, rxjs.concat)((0, rxjs.from)(plan.events), this.observeThread$(input, { joinToken: plan.joinToken }, {
260
- completeOnRunError: true,
261
- streamMode: "connect",
262
- replayCursor: plan.joinFromEventId
263
- }));
260
+ this.canonicalRunId = null;
261
+ this.clearReconnectCursor(input.threadId);
262
+ return (0, rxjs.defer)(() => this.requestJoinCredentials$("connect", input)).pipe((0, rxjs.switchMap)((credentials) => {
263
+ if (credentials === null) return rxjs.EMPTY;
264
+ const canonicalInput = this.applyCanonicalRunIdentity(input, credentials, { fallbackToInputRunId: false });
265
+ return this.observeThread$(canonicalInput, credentials, {
266
+ completeOnRunError: false,
267
+ streamMode: "connect"
268
+ });
264
269
  }));
265
270
  }
266
271
  /**
@@ -277,8 +282,7 @@ var IntelligenceAgent = class IntelligenceAgent extends _ag_ui_client.AbstractAg
277
282
  ownSocket.disconnect();
278
283
  if (this.socket === ownSocket) this.socket = null;
279
284
  }
280
- if (this.threadId) this.sharedState.lastSeenEventIds.delete(this.threadId);
281
- this.runId = null;
285
+ this.canonicalRunId = null;
282
286
  }
283
287
  cleanup() {
284
288
  this.cleanupOwned(this.activeChannel, this.socket);
@@ -299,80 +303,56 @@ var IntelligenceAgent = class IntelligenceAgent extends _ag_ui_client.AbstractAg
299
303
  tools: input.tools,
300
304
  context: input.context,
301
305
  state: input.state,
302
- forwardedProps: input.forwardedProps
306
+ forwardedProps: input.forwardedProps,
307
+ ...mode === "connect" ? { lastSeenEventId: this.getReconnectCursor(input) } : {}
303
308
  }),
304
309
  ...this.config.credentials ? { credentials: this.config.credentials } : {}
305
310
  });
306
- if (response.status === 409) throw new AgentThreadLockedError(input.threadId);
311
+ if (response.status === 204 && mode === "connect") return null;
312
+ if (response.status === 409 && mode === "run") throw new AgentThreadLockedError(input.threadId);
307
313
  if (!response.ok) {
308
314
  const text = await response.text().catch(() => "");
309
315
  throw new Error(text || response.statusText || String(response.status));
310
316
  }
311
- const payload = await response.json();
312
- if (!payload.joinToken) throw new Error("missing joinToken");
313
- return { joinToken: payload.joinToken };
317
+ return this.normalizeJoinCredentials(await response.json(), input);
314
318
  } catch (error) {
315
319
  if (error instanceof AgentThreadLockedError) throw error;
316
320
  throw new Error(`REST ${mode} request failed: ${error instanceof Error ? error.message : String(error)}`, { cause: error });
317
321
  }
318
322
  });
319
323
  }
320
- requestConnectPlan$(input) {
321
- return (0, rxjs.defer)(async () => {
322
- try {
323
- const response = await fetch(this.buildRuntimeUrl("connect"), {
324
- method: "POST",
325
- headers: {
326
- "Content-Type": "application/json",
327
- ...this.config.headers
328
- },
329
- body: JSON.stringify({
330
- threadId: input.threadId,
331
- runId: input.runId,
332
- messages: input.messages,
333
- tools: input.tools,
334
- context: input.context,
335
- state: input.state,
336
- forwardedProps: input.forwardedProps,
337
- lastSeenEventId: this.getReconnectCursor(input)
338
- }),
339
- ...this.config.credentials ? { credentials: this.config.credentials } : {}
340
- });
341
- if (response.status === 204) return null;
342
- if (!response.ok) {
343
- const text = await response.text().catch(() => "");
344
- throw new Error(text || response.statusText || String(response.status));
345
- }
346
- return this.normalizeConnectPlan(await response.json());
347
- } catch (error) {
348
- throw new Error(`REST connect request failed: ${error instanceof Error ? error.message : String(error)}`, { cause: error });
349
- }
350
- });
351
- }
352
- normalizeConnectPlan(payload) {
324
+ normalizeJoinCredentials(payload, input) {
353
325
  const envelope = payload && typeof payload === "object" ? payload : null;
354
- if (envelope?.mode === "bootstrap") return {
355
- mode: "bootstrap",
356
- latestEventId: typeof envelope.latestEventId === "string" ? envelope.latestEventId : null,
357
- events: Array.isArray(envelope.events) ? envelope.events : []
326
+ const realtime = envelope?.realtime && typeof envelope.realtime === "object" ? envelope.realtime : null;
327
+ if (typeof envelope?.joinToken !== "string" || !envelope.joinToken) throw new Error("missing joinToken");
328
+ if (typeof realtime?.clientUrl !== "string" || !realtime.clientUrl) throw new Error("missing realtime.clientUrl");
329
+ if (typeof realtime.topic !== "string" || !realtime.topic) throw new Error("missing realtime.topic");
330
+ return {
331
+ threadId: typeof envelope.threadId === "string" && envelope.threadId ? envelope.threadId : input.threadId,
332
+ runId: typeof envelope.runId === "string" && envelope.runId ? envelope.runId : null,
333
+ joinToken: envelope.joinToken,
334
+ realtime: {
335
+ clientUrl: realtime.clientUrl,
336
+ topic: realtime.topic
337
+ }
358
338
  };
359
- if (envelope?.mode === "live") {
360
- if (typeof envelope.joinToken !== "string" || envelope.joinToken.length === 0) throw new Error("missing joinToken");
361
- return {
362
- mode: "live",
363
- joinToken: envelope.joinToken,
364
- joinFromEventId: typeof envelope.joinFromEventId === "string" ? envelope.joinFromEventId : null,
365
- events: Array.isArray(envelope.events) ? envelope.events : []
366
- };
367
- }
368
- throw new Error("invalid connect plan");
369
339
  }
370
340
  observeThread$(input, credentials, options) {
341
+ return this.observeThreadSession$(input, credentials, options).pipe((0, rxjs_operators.catchError)((error) => {
342
+ if (!this.isSocketReconnectExhaustedError(error)) return (0, rxjs.throwError)(() => error);
343
+ return this.requestJoinCredentials$("connect", input).pipe((0, rxjs.switchMap)((refreshedCredentials) => refreshedCredentials === null ? rxjs.EMPTY : this.observeThread$(this.applyCanonicalRunIdentity(input, refreshedCredentials, { fallbackToInputRunId: options.streamMode === "run" }), refreshedCredentials, {
344
+ ...options,
345
+ channelMode: "connect",
346
+ replayCursor: this.getReconnectCursor(input)
347
+ })));
348
+ }));
349
+ }
350
+ observeThreadSession$(input, credentials, options) {
371
351
  return (0, rxjs.defer)(() => {
372
352
  let ownSocket = null;
373
353
  let ownChannel = null;
374
354
  const socket$ = ɵphoenixSocket$({
375
- url: this.config.url,
355
+ url: credentials.realtime.clientUrl,
376
356
  options: {
377
357
  params: {
378
358
  ...this.config.socketParams,
@@ -388,10 +368,10 @@ var IntelligenceAgent = class IntelligenceAgent extends _ag_ui_client.AbstractAg
388
368
  bufferSize: 1,
389
369
  refCount: true
390
370
  }));
391
- const { topic, params } = this.createThreadChannelDescriptor(input, options.streamMode, options.replayCursor);
371
+ const params = this.createThreadChannelParams(input, options.channelMode ?? options.streamMode, options.replayCursor);
392
372
  const channel$ = ɵphoenixChannel$({
393
373
  socket$,
394
- topic,
374
+ topic: credentials.realtime.topic,
395
375
  params
396
376
  }).pipe((0, rxjs_operators.tap)(({ channel }) => {
397
377
  ownChannel = channel;
@@ -401,8 +381,14 @@ var IntelligenceAgent = class IntelligenceAgent extends _ag_ui_client.AbstractAg
401
381
  refCount: true
402
382
  }));
403
383
  const threadEvents$ = this.observeThreadEvents$(input.threadId, channel$, options).pipe((0, rxjs_operators.share)());
404
- const threadCompleted$ = threadEvents$.pipe((0, rxjs_operators.ignoreElements)(), (0, rxjs_operators.endWith)(null), (0, rxjs_operators.take)(1));
405
- return (0, rxjs.merge)(this.joinThreadChannel$(channel$), this.observeSocketHealth$(socket$).pipe((0, rxjs_operators.takeUntil)(threadCompleted$)), threadEvents$).pipe((0, rxjs_operators.finalize)(() => this.cleanupOwned(ownChannel, ownSocket)));
384
+ const replayComplete$ = this.observeControlEvent$(input.threadId, channel$, REPLAY_COMPLETE_EVENT).pipe((0, rxjs_operators.ignoreElements)(), (0, rxjs_operators.share)());
385
+ const streamIdle$ = this.observeControlEvent$(input.threadId, channel$, STREAM_IDLE_EVENT).pipe((0, rxjs_operators.shareReplay)({
386
+ bufferSize: 1,
387
+ refCount: true
388
+ }));
389
+ const streamIdleCompletion$ = options.streamMode === "connect" ? streamIdle$.pipe((0, rxjs_operators.take)(1)) : rxjs.EMPTY;
390
+ const terminal$ = (0, rxjs.merge)(threadEvents$.pipe((0, rxjs_operators.ignoreElements)(), (0, rxjs_operators.endWith)(null), (0, rxjs_operators.take)(1)), streamIdleCompletion$);
391
+ return (0, rxjs.merge)(this.joinThreadChannel$(channel$), this.observeSocketHealth$(socket$).pipe((0, rxjs_operators.takeUntil)(terminal$)), threadEvents$.pipe((0, rxjs_operators.takeUntil)(streamIdleCompletion$)), replayComplete$.pipe((0, rxjs_operators.takeUntil)(terminal$)), streamIdleCompletion$.pipe((0, rxjs_operators.ignoreElements)())).pipe((0, rxjs_operators.finalize)(() => this.cleanupOwned(ownChannel, ownSocket)));
406
392
  });
407
393
  }
408
394
  joinThreadChannel$(channel$) {
@@ -414,17 +400,23 @@ var IntelligenceAgent = class IntelligenceAgent extends _ag_ui_client.AbstractAg
414
400
  observeThreadEvents$(threadId, channel$, options) {
415
401
  return channel$.pipe((0, rxjs_operators.switchMap)(({ channel }) => this.observeChannelEvent$(channel, CLIENT_AG_UI_EVENT)), (0, rxjs_operators.tap)((payload) => {
416
402
  this.updateLastSeenEventId(threadId, payload);
417
- this.updateRunIdFromEvent(payload);
418
- }), (0, rxjs_operators.mergeMap)((payload) => (0, rxjs.from)(this.createThreadNotifications(payload, options.completeOnRunError))), (0, rxjs.dematerialize)());
403
+ }), (0, rxjs_operators.mergeMap)((payload) => this.createThreadNotifications(payload, {
404
+ completeOnRunError: options.completeOnRunError,
405
+ completeOnRunFinished: options.streamMode === "run",
406
+ errorOnRunError: options.streamMode === "run"
407
+ })), (0, rxjs.dematerialize)());
408
+ }
409
+ observeControlEvent$(threadId, channel$, eventName) {
410
+ return channel$.pipe((0, rxjs_operators.switchMap)(({ channel }) => this.observeChannelEvent$(channel, eventName)), (0, rxjs_operators.tap)((payload) => this.updateLastSeenEventIdFromControl(threadId, payload)));
419
411
  }
420
412
  observeChannelEvent$(channel, eventName) {
421
413
  return ɵobservePhoenixEvent$(channel, eventName);
422
414
  }
423
- createThreadNotifications(payload, completeOnRunError) {
424
- if (payload.type === _ag_ui_client.EventType.RUN_FINISHED) return [rxjs.Notification.createNext(payload), rxjs.Notification.createComplete()];
415
+ createThreadNotifications(payload, options) {
416
+ if (payload.type === _ag_ui_client.EventType.RUN_FINISHED) return options.completeOnRunFinished ? [rxjs.Notification.createNext(payload), rxjs.Notification.createComplete()] : [rxjs.Notification.createNext(payload)];
425
417
  if (payload.type === _ag_ui_client.EventType.RUN_ERROR) {
426
418
  const errorMessage = payload.message ?? "Run error";
427
- return completeOnRunError ? [rxjs.Notification.createNext(payload), rxjs.Notification.createComplete()] : [rxjs.Notification.createNext(payload), rxjs.Notification.createError(new Error(errorMessage))];
419
+ return options.completeOnRunError ? [rxjs.Notification.createNext(payload), rxjs.Notification.createComplete()] : options.errorOnRunError ? [rxjs.Notification.createNext(payload), rxjs.Notification.createError(new Error(errorMessage))] : [rxjs.Notification.createNext(payload)];
428
420
  }
429
421
  return [rxjs.Notification.createNext(payload)];
430
422
  }
@@ -433,60 +425,58 @@ var IntelligenceAgent = class IntelligenceAgent extends _ag_ui_client.AbstractAg
433
425
  const origin = typeof window !== "undefined" && window.location ? window.location.origin : "http://localhost";
434
426
  return new URL(path, new URL(this.config.runtimeUrl, origin)).toString();
435
427
  }
436
- createThreadChannelDescriptor(input, streamMode, replayCursor) {
437
- const params = streamMode === "run" ? {
428
+ createThreadChannelParams(input, streamMode, replayCursor) {
429
+ return streamMode === "run" ? {
438
430
  stream_mode: "run",
439
431
  run_id: input.runId
440
432
  } : {
441
433
  stream_mode: "connect",
442
434
  last_seen_event_id: replayCursor === void 0 ? this.getReconnectCursor(input) : replayCursor
443
435
  };
444
- return {
445
- topic: `thread:${input.threadId}`,
446
- params
447
- };
448
436
  }
449
437
  getLastSeenEventId(threadId) {
450
438
  return this.sharedState.lastSeenEventIds.get(threadId) ?? null;
451
439
  }
452
440
  getReconnectCursor(input) {
453
- return this.hasLocalThreadMessages(input) ? this.getLastSeenEventId(input.threadId) : null;
441
+ return this.getLastSeenEventId(input.threadId);
454
442
  }
455
- hasLocalThreadMessages(input) {
456
- return Array.isArray(input.messages) && input.messages.length > 0;
443
+ clearReconnectCursor(threadId) {
444
+ this.sharedState.lastSeenEventIds.delete(threadId);
457
445
  }
458
446
  updateLastSeenEventId(threadId, payload) {
459
447
  const eventId = this.readEventId(payload);
460
448
  if (!eventId) return;
461
449
  this.sharedState.lastSeenEventIds.set(threadId, eventId);
462
450
  }
463
- setLastSeenEventId(threadId, eventId) {
451
+ updateLastSeenEventIdFromControl(threadId, payload) {
452
+ const eventId = this.readControlEventId(payload);
464
453
  if (!eventId) return;
465
454
  this.sharedState.lastSeenEventIds.set(threadId, eventId);
466
455
  }
467
- /**
468
- * Keep `this.runId` in sync with the backend's actual run ID.
469
- *
470
- * During a `connect` (resume) flow the client generates a fresh `runId`
471
- * via `prepareRunAgentInput`, but the backend is running under its own
472
- * run ID. If the client later sends `STOP_RUN_EVENT` with the wrong
473
- * `runId`, the gateway's runner channel will not match it and the agent
474
- * keeps running. Extracting the run ID from live events fixes this.
475
- *
476
- * The runner normalises events to `run_id` (snake_case) before pushing
477
- * to the gateway, so we check both `runId` and `run_id`.
478
- */
479
- updateRunIdFromEvent(payload) {
480
- const record = payload;
481
- const eventRunId = record.runId ?? record.run_id;
482
- if (typeof eventRunId === "string" && eventRunId.length > 0) this.runId = eventRunId;
483
- }
484
456
  readEventId(payload) {
485
457
  const metadata = payload.metadata;
486
458
  if (!metadata || typeof metadata !== "object") return null;
487
459
  const runnerEventId = metadata.cpki_event_id;
488
460
  return typeof runnerEventId === "string" ? runnerEventId : null;
489
461
  }
462
+ readControlEventId(payload) {
463
+ if (!payload || typeof payload !== "object") return null;
464
+ const latestEventId = payload.latestEventId;
465
+ return typeof latestEventId === "string" ? latestEventId : null;
466
+ }
467
+ applyCanonicalRunIdentity(input, credentials, options) {
468
+ this.threadId = credentials.threadId;
469
+ const runId = credentials.runId ?? (options.fallbackToInputRunId ? input.runId : null);
470
+ this.canonicalRunId = runId;
471
+ return {
472
+ ...input,
473
+ threadId: credentials.threadId,
474
+ ...runId === null ? {} : { runId }
475
+ };
476
+ }
477
+ isSocketReconnectExhaustedError(error) {
478
+ return error instanceof Error && error.message.includes("WebSocket connection failed after");
479
+ }
490
480
  };
491
481
 
492
482
  //#endregion
@@ -2309,8 +2299,21 @@ let CopilotKitCoreErrorCode = /* @__PURE__ */ function(CopilotKitCoreErrorCode)
2309
2299
  CopilotKitCoreErrorCode["TRANSCRIPTION_RATE_LIMITED"] = "transcription_rate_limited";
2310
2300
  CopilotKitCoreErrorCode["TRANSCRIPTION_AUTH_FAILED"] = "transcription_auth_failed";
2311
2301
  CopilotKitCoreErrorCode["TRANSCRIPTION_NETWORK_ERROR"] = "transcription_network_error";
2302
+ CopilotKitCoreErrorCode["SUBSCRIBER_CALLBACK_FAILED"] = "subscriber_callback_failed";
2312
2303
  return CopilotKitCoreErrorCode;
2313
2304
  }({});
2305
+ /**
2306
+ * Runtime allowlist derived from {@link SUBSCRIBE_TO_AGENT_KEYS}. Hoisted
2307
+ * to module scope so the Set is allocated once, not per-subscription.
2308
+ */
2309
+ const ALLOWED_KEYS = new Set([
2310
+ "onMessagesChanged",
2311
+ "onStateChanged",
2312
+ "onRunInitialized",
2313
+ "onRunFinalized",
2314
+ "onRunFailed",
2315
+ "onRunErrorEvent"
2316
+ ]);
2314
2317
  let CopilotKitCoreRuntimeConnectionStatus = /* @__PURE__ */ function(CopilotKitCoreRuntimeConnectionStatus) {
2315
2318
  CopilotKitCoreRuntimeConnectionStatus["Disconnected"] = "disconnected";
2316
2319
  CopilotKitCoreRuntimeConnectionStatus["Connected"] = "connected";
@@ -2376,6 +2379,16 @@ var CopilotKitCore = class {
2376
2379
  }), "Subscriber onError error:");
2377
2380
  }
2378
2381
  /**
2382
+ * Log a message to the console and emit an error to subscribers.
2383
+ * Catches failures from `emitError` itself to prevent unhandled rejections.
2384
+ */
2385
+ logAndEmitError(message, params, logLevel = "error") {
2386
+ console[logLevel](message, params.error);
2387
+ this.emitError(params).catch((emitErr) => {
2388
+ console.error(message + " — emitError itself failed:", emitErr);
2389
+ });
2390
+ }
2391
+ /**
2379
2392
  * Snapshot accessors
2380
2393
  */
2381
2394
  get context() {
@@ -2412,17 +2425,30 @@ var CopilotKitCore = class {
2412
2425
  return this._properties;
2413
2426
  }
2414
2427
  /**
2415
- * Default throttle interval (ms) applied by framework hooks (e.g.
2416
- * `useAgent()`) when the hook/component does not specify an explicit
2417
- * `throttleMs`. An explicit `0` passed as `throttleMs` to `useAgent()`
2418
- * or `<CopilotChat>` overrides this default and disables throttling.
2428
+ * Default throttle interval (ms) used by `subscribeToAgentWithOptions()`
2429
+ * when the caller does not specify an explicit `throttleMs`.
2430
+ * `undefined` means no default is configured; `0` means no throttling.
2419
2431
  */
2420
2432
  get defaultThrottleMs() {
2421
2433
  return this._defaultThrottleMs;
2422
2434
  }
2435
+ /**
2436
+ * Set the default throttle interval (ms) for `subscribeToAgentWithOptions()`.
2437
+ *
2438
+ * Accepts a non-negative finite number or `undefined` (to clear the
2439
+ * default). Invalid values (NaN, Infinity, negative) are logged as
2440
+ * errors and ignored — the previous valid value is preserved.
2441
+ */
2423
2442
  setDefaultThrottleMs(value) {
2424
2443
  if (value !== void 0 && (!Number.isFinite(value) || value < 0)) {
2425
- this._defaultThrottleMs = void 0;
2444
+ this.logAndEmitError(`CopilotKitCore.setDefaultThrottleMs: value must be a non-negative finite number or undefined, got ${value}. Keeping current value (${this._defaultThrottleMs}).`, {
2445
+ error: /* @__PURE__ */ new Error(`setDefaultThrottleMs: invalid value (${value}), keeping current value (${this._defaultThrottleMs})`),
2446
+ code: CopilotKitCoreErrorCode.SUBSCRIBER_CALLBACK_FAILED,
2447
+ context: {
2448
+ value,
2449
+ currentValue: this._defaultThrottleMs
2450
+ }
2451
+ });
2426
2452
  return;
2427
2453
  }
2428
2454
  this._defaultThrottleMs = value;
@@ -2543,6 +2569,137 @@ var CopilotKitCore = class {
2543
2569
  } };
2544
2570
  }
2545
2571
  /**
2572
+ * Subscribe to an agent's notification and lifecycle events with
2573
+ * optional configuration (e.g. throttling).
2574
+ *
2575
+ * Wraps every callback with error protection (`safeCall`) and applies
2576
+ * the options before delegating to `agent.subscribe()`.
2577
+ *
2578
+ * See {@link SubscribeToAgentSubscriber} for the accepted callback subset
2579
+ * and the rationale for excluding AG-UI event handlers.
2580
+ */
2581
+ subscribeToAgentWithOptions(agent, subscriber, options) {
2582
+ const resolved = options?.throttleMs ?? this._defaultThrottleMs ?? 0;
2583
+ let effectiveMs = 0;
2584
+ if (!Number.isFinite(resolved) || resolved < 0) {
2585
+ const source = options?.throttleMs !== void 0 ? "throttleMs" : "defaultThrottleMs";
2586
+ this.logAndEmitError(`CopilotKitCore.subscribeToAgentWithOptions: ${source} must be a non-negative finite number, got ${resolved}. Falling back to unthrottled.`, {
2587
+ error: /* @__PURE__ */ new Error(`subscribeToAgentWithOptions: invalid ${source} (${resolved}), falling back to unthrottled`),
2588
+ code: CopilotKitCoreErrorCode.SUBSCRIBER_CALLBACK_FAILED,
2589
+ context: {
2590
+ agentId: agent.agentId,
2591
+ source,
2592
+ value: resolved
2593
+ }
2594
+ });
2595
+ } else effectiveMs = resolved;
2596
+ const agentLabel = agent.agentId || "(unknown agent)";
2597
+ const safeCall = (label, fn, ...args) => {
2598
+ const reportError = (err, verb) => {
2599
+ this.logAndEmitError(`CopilotKitCore.subscribeToAgentWithOptions[${agentLabel}]: ${label} callback ${verb}:`, {
2600
+ error: err instanceof Error ? err : new Error(String(err)),
2601
+ code: CopilotKitCoreErrorCode.SUBSCRIBER_CALLBACK_FAILED,
2602
+ context: {
2603
+ agentId: agent.agentId,
2604
+ callback: label
2605
+ }
2606
+ });
2607
+ };
2608
+ try {
2609
+ const result = fn(...args);
2610
+ if (result instanceof Promise) return result.catch((err) => {
2611
+ reportError(err, "rejected");
2612
+ });
2613
+ return result;
2614
+ } catch (err) {
2615
+ reportError(err, "threw");
2616
+ }
2617
+ };
2618
+ const guardAll = (sub) => {
2619
+ const guarded = {};
2620
+ if (sub.onMessagesChanged) {
2621
+ const fn = sub.onMessagesChanged;
2622
+ guarded.onMessagesChanged = (params) => safeCall("onMessagesChanged", fn, params);
2623
+ }
2624
+ if (sub.onStateChanged) {
2625
+ const fn = sub.onStateChanged;
2626
+ guarded.onStateChanged = (params) => safeCall("onStateChanged", fn, params);
2627
+ }
2628
+ if (sub.onRunInitialized) {
2629
+ const fn = sub.onRunInitialized;
2630
+ guarded.onRunInitialized = (params) => safeCall("onRunInitialized", fn, params);
2631
+ }
2632
+ if (sub.onRunFinalized) {
2633
+ const fn = sub.onRunFinalized;
2634
+ guarded.onRunFinalized = (params) => safeCall("onRunFinalized", fn, params);
2635
+ }
2636
+ if (sub.onRunFailed) {
2637
+ const fn = sub.onRunFailed;
2638
+ guarded.onRunFailed = (params) => safeCall("onRunFailed", fn, params);
2639
+ }
2640
+ if (sub.onRunErrorEvent) {
2641
+ const fn = sub.onRunErrorEvent;
2642
+ guarded.onRunErrorEvent = (params) => safeCall("onRunErrorEvent", fn, params);
2643
+ }
2644
+ return guarded;
2645
+ };
2646
+ for (const key of Object.keys(subscriber)) if (typeof subscriber[key] === "function" && !ALLOWED_KEYS.has(key)) {
2647
+ const message = `CopilotKitCore.subscribeToAgentWithOptions[${agentLabel}]: callback "${key}" is not supported and was dropped. Supported callbacks: ${Array.from(ALLOWED_KEYS).join(", ")}. Use agent.subscribe() directly for event handlers and per-item notifications.`;
2648
+ this.logAndEmitError(message, {
2649
+ error: new Error(message),
2650
+ code: CopilotKitCoreErrorCode.SUBSCRIBER_CALLBACK_FAILED,
2651
+ context: {
2652
+ agentId: agent.agentId,
2653
+ droppedCallback: key
2654
+ }
2655
+ }, "warn");
2656
+ }
2657
+ if (effectiveMs <= 0) {
2658
+ const subscription = agent.subscribe(guardAll(subscriber));
2659
+ return { unsubscribe: () => subscription.unsubscribe() };
2660
+ }
2661
+ let active = true;
2662
+ let latestMessagesParams = null;
2663
+ let latestStateParams = null;
2664
+ const flushPending = () => {
2665
+ if (active && subscriber.onMessagesChanged && latestMessagesParams) {
2666
+ const params = latestMessagesParams;
2667
+ latestMessagesParams = null;
2668
+ safeCall("onMessagesChanged", subscriber.onMessagesChanged, params);
2669
+ }
2670
+ if (active && subscriber.onStateChanged && latestStateParams) {
2671
+ const params = latestStateParams;
2672
+ latestStateParams = null;
2673
+ safeCall("onStateChanged", subscriber.onStateChanged, params);
2674
+ }
2675
+ };
2676
+ const throttler = new _tanstack_pacer.Throttler(flushPending, {
2677
+ wait: effectiveMs,
2678
+ leading: true,
2679
+ trailing: true
2680
+ });
2681
+ const lifecycleOnly = {};
2682
+ if (subscriber.onRunInitialized) lifecycleOnly.onRunInitialized = subscriber.onRunInitialized;
2683
+ if (subscriber.onRunFinalized) lifecycleOnly.onRunFinalized = subscriber.onRunFinalized;
2684
+ if (subscriber.onRunFailed) lifecycleOnly.onRunFailed = subscriber.onRunFailed;
2685
+ if (subscriber.onRunErrorEvent) lifecycleOnly.onRunErrorEvent = subscriber.onRunErrorEvent;
2686
+ const wrappedSubscriber = guardAll(lifecycleOnly);
2687
+ if (subscriber.onMessagesChanged) wrappedSubscriber.onMessagesChanged = (params) => {
2688
+ latestMessagesParams = params;
2689
+ throttler.maybeExecute();
2690
+ };
2691
+ if (subscriber.onStateChanged) wrappedSubscriber.onStateChanged = (params) => {
2692
+ latestStateParams = params;
2693
+ throttler.maybeExecute();
2694
+ };
2695
+ const subscription = agent.subscribe(wrappedSubscriber);
2696
+ return { unsubscribe: () => {
2697
+ active = false;
2698
+ throttler.cancel();
2699
+ subscription.unsubscribe();
2700
+ } };
2701
+ }
2702
+ /**
2546
2703
  * Agent connectivity (delegated to RunHandler)
2547
2704
  */
2548
2705
  async connectAgent(params) {
@@ -3089,15 +3246,18 @@ const threadDomainEvents = createActionGroup("Thread Domain", {
3089
3246
  threadUpserted: props(),
3090
3247
  threadDeleted: props()
3091
3248
  });
3092
- function sortThreadsByUpdatedAt(threads) {
3093
- return [...threads].sort((left, right) => right.updatedAt.localeCompare(left.updatedAt));
3249
+ function sortThreadsByRecency(threads) {
3250
+ return [...threads].sort((left, right) => {
3251
+ const leftKey = left.lastRunAt ?? left.updatedAt ?? left.createdAt;
3252
+ return (right.lastRunAt ?? right.updatedAt ?? right.createdAt).localeCompare(leftKey);
3253
+ });
3094
3254
  }
3095
3255
  function upsertThread(threads, thread) {
3096
3256
  const existingIndex = threads.findIndex((item) => item.id === thread.id);
3097
- if (existingIndex === -1) return sortThreadsByUpdatedAt([...threads, thread]);
3257
+ if (existingIndex === -1) return sortThreadsByRecency([...threads, thread]);
3098
3258
  const next = [...threads];
3099
3259
  next[existingIndex] = thread;
3100
- return sortThreadsByUpdatedAt(next);
3260
+ return sortThreadsByRecency(next);
3101
3261
  }
3102
3262
  const threadReducer = createReducer(initialThreadState, on(threadAdapterEvents.contextChanged, (state, { context }) => ({
3103
3263
  ...state,
@@ -3130,7 +3290,7 @@ const threadReducer = createReducer(initialThreadState, on(threadAdapterEvents.c
3130
3290
  if (sessionId !== state.sessionId) return state;
3131
3291
  return {
3132
3292
  ...state,
3133
- threads: sortThreadsByUpdatedAt(threads),
3293
+ threads: sortThreadsByRecency(threads),
3134
3294
  isLoading: false,
3135
3295
  error: null,
3136
3296
  metadataJoinCode: joinCode,