@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.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
@@ -2308,8 +2298,21 @@ let CopilotKitCoreErrorCode = /* @__PURE__ */ function(CopilotKitCoreErrorCode)
2308
2298
  CopilotKitCoreErrorCode["TRANSCRIPTION_RATE_LIMITED"] = "transcription_rate_limited";
2309
2299
  CopilotKitCoreErrorCode["TRANSCRIPTION_AUTH_FAILED"] = "transcription_auth_failed";
2310
2300
  CopilotKitCoreErrorCode["TRANSCRIPTION_NETWORK_ERROR"] = "transcription_network_error";
2301
+ CopilotKitCoreErrorCode["SUBSCRIBER_CALLBACK_FAILED"] = "subscriber_callback_failed";
2311
2302
  return CopilotKitCoreErrorCode;
2312
2303
  }({});
2304
+ /**
2305
+ * Runtime allowlist derived from {@link SUBSCRIBE_TO_AGENT_KEYS}. Hoisted
2306
+ * to module scope so the Set is allocated once, not per-subscription.
2307
+ */
2308
+ const ALLOWED_KEYS = new Set([
2309
+ "onMessagesChanged",
2310
+ "onStateChanged",
2311
+ "onRunInitialized",
2312
+ "onRunFinalized",
2313
+ "onRunFailed",
2314
+ "onRunErrorEvent"
2315
+ ]);
2313
2316
  let CopilotKitCoreRuntimeConnectionStatus = /* @__PURE__ */ function(CopilotKitCoreRuntimeConnectionStatus) {
2314
2317
  CopilotKitCoreRuntimeConnectionStatus["Disconnected"] = "disconnected";
2315
2318
  CopilotKitCoreRuntimeConnectionStatus["Connected"] = "connected";
@@ -2375,6 +2378,16 @@ var CopilotKitCore = class {
2375
2378
  }), "Subscriber onError error:");
2376
2379
  }
2377
2380
  /**
2381
+ * Log a message to the console and emit an error to subscribers.
2382
+ * Catches failures from `emitError` itself to prevent unhandled rejections.
2383
+ */
2384
+ logAndEmitError(message, params, logLevel = "error") {
2385
+ console[logLevel](message, params.error);
2386
+ this.emitError(params).catch((emitErr) => {
2387
+ console.error(message + " — emitError itself failed:", emitErr);
2388
+ });
2389
+ }
2390
+ /**
2378
2391
  * Snapshot accessors
2379
2392
  */
2380
2393
  get context() {
@@ -2411,17 +2424,30 @@ var CopilotKitCore = class {
2411
2424
  return this._properties;
2412
2425
  }
2413
2426
  /**
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.
2427
+ * Default throttle interval (ms) used by `subscribeToAgentWithOptions()`
2428
+ * when the caller does not specify an explicit `throttleMs`.
2429
+ * `undefined` means no default is configured; `0` means no throttling.
2418
2430
  */
2419
2431
  get defaultThrottleMs() {
2420
2432
  return this._defaultThrottleMs;
2421
2433
  }
2434
+ /**
2435
+ * Set the default throttle interval (ms) for `subscribeToAgentWithOptions()`.
2436
+ *
2437
+ * Accepts a non-negative finite number or `undefined` (to clear the
2438
+ * default). Invalid values (NaN, Infinity, negative) are logged as
2439
+ * errors and ignored — the previous valid value is preserved.
2440
+ */
2422
2441
  setDefaultThrottleMs(value) {
2423
2442
  if (value !== void 0 && (!Number.isFinite(value) || value < 0)) {
2424
- this._defaultThrottleMs = void 0;
2443
+ this.logAndEmitError(`CopilotKitCore.setDefaultThrottleMs: value must be a non-negative finite number or undefined, got ${value}. Keeping current value (${this._defaultThrottleMs}).`, {
2444
+ error: /* @__PURE__ */ new Error(`setDefaultThrottleMs: invalid value (${value}), keeping current value (${this._defaultThrottleMs})`),
2445
+ code: CopilotKitCoreErrorCode.SUBSCRIBER_CALLBACK_FAILED,
2446
+ context: {
2447
+ value,
2448
+ currentValue: this._defaultThrottleMs
2449
+ }
2450
+ });
2425
2451
  return;
2426
2452
  }
2427
2453
  this._defaultThrottleMs = value;
@@ -2542,6 +2568,137 @@ var CopilotKitCore = class {
2542
2568
  } };
2543
2569
  }
2544
2570
  /**
2571
+ * Subscribe to an agent's notification and lifecycle events with
2572
+ * optional configuration (e.g. throttling).
2573
+ *
2574
+ * Wraps every callback with error protection (`safeCall`) and applies
2575
+ * the options before delegating to `agent.subscribe()`.
2576
+ *
2577
+ * See {@link SubscribeToAgentSubscriber} for the accepted callback subset
2578
+ * and the rationale for excluding AG-UI event handlers.
2579
+ */
2580
+ subscribeToAgentWithOptions(agent, subscriber, options) {
2581
+ const resolved = options?.throttleMs ?? this._defaultThrottleMs ?? 0;
2582
+ let effectiveMs = 0;
2583
+ if (!Number.isFinite(resolved) || resolved < 0) {
2584
+ const source = options?.throttleMs !== void 0 ? "throttleMs" : "defaultThrottleMs";
2585
+ this.logAndEmitError(`CopilotKitCore.subscribeToAgentWithOptions: ${source} must be a non-negative finite number, got ${resolved}. Falling back to unthrottled.`, {
2586
+ error: /* @__PURE__ */ new Error(`subscribeToAgentWithOptions: invalid ${source} (${resolved}), falling back to unthrottled`),
2587
+ code: CopilotKitCoreErrorCode.SUBSCRIBER_CALLBACK_FAILED,
2588
+ context: {
2589
+ agentId: agent.agentId,
2590
+ source,
2591
+ value: resolved
2592
+ }
2593
+ });
2594
+ } else effectiveMs = resolved;
2595
+ const agentLabel = agent.agentId || "(unknown agent)";
2596
+ const safeCall = (label, fn, ...args) => {
2597
+ const reportError = (err, verb) => {
2598
+ this.logAndEmitError(`CopilotKitCore.subscribeToAgentWithOptions[${agentLabel}]: ${label} callback ${verb}:`, {
2599
+ error: err instanceof Error ? err : new Error(String(err)),
2600
+ code: CopilotKitCoreErrorCode.SUBSCRIBER_CALLBACK_FAILED,
2601
+ context: {
2602
+ agentId: agent.agentId,
2603
+ callback: label
2604
+ }
2605
+ });
2606
+ };
2607
+ try {
2608
+ const result = fn(...args);
2609
+ if (result instanceof Promise) return result.catch((err) => {
2610
+ reportError(err, "rejected");
2611
+ });
2612
+ return result;
2613
+ } catch (err) {
2614
+ reportError(err, "threw");
2615
+ }
2616
+ };
2617
+ const guardAll = (sub) => {
2618
+ const guarded = {};
2619
+ if (sub.onMessagesChanged) {
2620
+ const fn = sub.onMessagesChanged;
2621
+ guarded.onMessagesChanged = (params) => safeCall("onMessagesChanged", fn, params);
2622
+ }
2623
+ if (sub.onStateChanged) {
2624
+ const fn = sub.onStateChanged;
2625
+ guarded.onStateChanged = (params) => safeCall("onStateChanged", fn, params);
2626
+ }
2627
+ if (sub.onRunInitialized) {
2628
+ const fn = sub.onRunInitialized;
2629
+ guarded.onRunInitialized = (params) => safeCall("onRunInitialized", fn, params);
2630
+ }
2631
+ if (sub.onRunFinalized) {
2632
+ const fn = sub.onRunFinalized;
2633
+ guarded.onRunFinalized = (params) => safeCall("onRunFinalized", fn, params);
2634
+ }
2635
+ if (sub.onRunFailed) {
2636
+ const fn = sub.onRunFailed;
2637
+ guarded.onRunFailed = (params) => safeCall("onRunFailed", fn, params);
2638
+ }
2639
+ if (sub.onRunErrorEvent) {
2640
+ const fn = sub.onRunErrorEvent;
2641
+ guarded.onRunErrorEvent = (params) => safeCall("onRunErrorEvent", fn, params);
2642
+ }
2643
+ return guarded;
2644
+ };
2645
+ for (const key of Object.keys(subscriber)) if (typeof subscriber[key] === "function" && !ALLOWED_KEYS.has(key)) {
2646
+ 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.`;
2647
+ this.logAndEmitError(message, {
2648
+ error: new Error(message),
2649
+ code: CopilotKitCoreErrorCode.SUBSCRIBER_CALLBACK_FAILED,
2650
+ context: {
2651
+ agentId: agent.agentId,
2652
+ droppedCallback: key
2653
+ }
2654
+ }, "warn");
2655
+ }
2656
+ if (effectiveMs <= 0) {
2657
+ const subscription = agent.subscribe(guardAll(subscriber));
2658
+ return { unsubscribe: () => subscription.unsubscribe() };
2659
+ }
2660
+ let active = true;
2661
+ let latestMessagesParams = null;
2662
+ let latestStateParams = null;
2663
+ const flushPending = () => {
2664
+ if (active && subscriber.onMessagesChanged && latestMessagesParams) {
2665
+ const params = latestMessagesParams;
2666
+ latestMessagesParams = null;
2667
+ safeCall("onMessagesChanged", subscriber.onMessagesChanged, params);
2668
+ }
2669
+ if (active && subscriber.onStateChanged && latestStateParams) {
2670
+ const params = latestStateParams;
2671
+ latestStateParams = null;
2672
+ safeCall("onStateChanged", subscriber.onStateChanged, params);
2673
+ }
2674
+ };
2675
+ const throttler = new Throttler(flushPending, {
2676
+ wait: effectiveMs,
2677
+ leading: true,
2678
+ trailing: true
2679
+ });
2680
+ const lifecycleOnly = {};
2681
+ if (subscriber.onRunInitialized) lifecycleOnly.onRunInitialized = subscriber.onRunInitialized;
2682
+ if (subscriber.onRunFinalized) lifecycleOnly.onRunFinalized = subscriber.onRunFinalized;
2683
+ if (subscriber.onRunFailed) lifecycleOnly.onRunFailed = subscriber.onRunFailed;
2684
+ if (subscriber.onRunErrorEvent) lifecycleOnly.onRunErrorEvent = subscriber.onRunErrorEvent;
2685
+ const wrappedSubscriber = guardAll(lifecycleOnly);
2686
+ if (subscriber.onMessagesChanged) wrappedSubscriber.onMessagesChanged = (params) => {
2687
+ latestMessagesParams = params;
2688
+ throttler.maybeExecute();
2689
+ };
2690
+ if (subscriber.onStateChanged) wrappedSubscriber.onStateChanged = (params) => {
2691
+ latestStateParams = params;
2692
+ throttler.maybeExecute();
2693
+ };
2694
+ const subscription = agent.subscribe(wrappedSubscriber);
2695
+ return { unsubscribe: () => {
2696
+ active = false;
2697
+ throttler.cancel();
2698
+ subscription.unsubscribe();
2699
+ } };
2700
+ }
2701
+ /**
2545
2702
  * Agent connectivity (delegated to RunHandler)
2546
2703
  */
2547
2704
  async connectAgent(params) {
@@ -3088,15 +3245,18 @@ const threadDomainEvents = createActionGroup("Thread Domain", {
3088
3245
  threadUpserted: props(),
3089
3246
  threadDeleted: props()
3090
3247
  });
3091
- function sortThreadsByUpdatedAt(threads) {
3092
- return [...threads].sort((left, right) => right.updatedAt.localeCompare(left.updatedAt));
3248
+ function sortThreadsByRecency(threads) {
3249
+ return [...threads].sort((left, right) => {
3250
+ const leftKey = left.lastRunAt ?? left.updatedAt ?? left.createdAt;
3251
+ return (right.lastRunAt ?? right.updatedAt ?? right.createdAt).localeCompare(leftKey);
3252
+ });
3093
3253
  }
3094
3254
  function upsertThread(threads, thread) {
3095
3255
  const existingIndex = threads.findIndex((item) => item.id === thread.id);
3096
- if (existingIndex === -1) return sortThreadsByUpdatedAt([...threads, thread]);
3256
+ if (existingIndex === -1) return sortThreadsByRecency([...threads, thread]);
3097
3257
  const next = [...threads];
3098
3258
  next[existingIndex] = thread;
3099
- return sortThreadsByUpdatedAt(next);
3259
+ return sortThreadsByRecency(next);
3100
3260
  }
3101
3261
  const threadReducer = createReducer(initialThreadState, on(threadAdapterEvents.contextChanged, (state, { context }) => ({
3102
3262
  ...state,
@@ -3129,7 +3289,7 @@ const threadReducer = createReducer(initialThreadState, on(threadAdapterEvents.c
3129
3289
  if (sessionId !== state.sessionId) return state;
3130
3290
  return {
3131
3291
  ...state,
3132
- threads: sortThreadsByUpdatedAt(threads),
3292
+ threads: sortThreadsByRecency(threads),
3133
3293
  isLoading: false,
3134
3294
  error: null,
3135
3295
  metadataJoinCode: joinCode,