@copilotkit/core 1.56.2 → 1.56.4-canary.1777529757

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