@copilotkit/core 1.56.2 → 1.56.3

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";
@@ -137,7 +138,7 @@ var IntelligenceAgent = class IntelligenceAgent extends AbstractAgent {
137
138
  config;
138
139
  socket = null;
139
140
  activeChannel = null;
140
- runId = null;
141
+ canonicalRunId = null;
141
142
  sharedState;
142
143
  constructor(config, sharedState = { lastSeenEventIds: /* @__PURE__ */ new Map() }) {
143
144
  super();
@@ -175,7 +176,11 @@ var IntelligenceAgent = class IntelligenceAgent extends AbstractAgent {
175
176
  try {
176
177
  this.isRunning = true;
177
178
  this.agentId = this.agentId ?? randomUUID();
178
- const input = this.prepareRunAgentInput(parameters);
179
+ const effectiveParameters = parameters?.runId || !this.canonicalRunId ? parameters : {
180
+ ...parameters,
181
+ runId: this.canonicalRunId
182
+ };
183
+ const input = this.prepareRunAgentInput(effectiveParameters);
179
184
  let result;
180
185
  const previousMessageIds = new Set(this.messages.map((m) => m.id));
181
186
  const subscribers = [
@@ -214,14 +219,14 @@ var IntelligenceAgent = class IntelligenceAgent extends AbstractAgent {
214
219
  }
215
220
  }
216
221
  abortRun() {
217
- if (this.activeChannel && this.runId) {
222
+ if (this.activeChannel && this.canonicalRunId) {
218
223
  const fallback = setTimeout(() => clear(), 5e3);
219
224
  const clear = () => {
220
225
  clearTimeout(fallback);
221
226
  this.detachActiveRun();
222
227
  this.cleanup();
223
228
  };
224
- this.activeChannel.push(STOP_RUN_EVENT, { run_id: this.runId }).receive("ok", clear).receive("error", clear).receive("timeout", clear);
229
+ this.activeChannel.push(STOP_RUN_EVENT, { run_id: this.canonicalRunId }).receive("ok", clear).receive("error", clear).receive("timeout", clear);
225
230
  } else {
226
231
  this.detachActiveRun();
227
232
  this.cleanup();
@@ -233,7 +238,7 @@ var IntelligenceAgent = class IntelligenceAgent extends AbstractAgent {
233
238
  */
234
239
  run(input) {
235
240
  this.threadId = input.threadId;
236
- this.runId = input.runId;
241
+ this.canonicalRunId = input.runId;
237
242
  return defer(() => this.requestJoinCredentials$("run", input)).pipe(switchMap((credentials) => this.observeThread$(input, credentials, {
238
243
  completeOnRunError: false,
239
244
  streamMode: "run"
@@ -245,16 +250,14 @@ var IntelligenceAgent = class IntelligenceAgent extends AbstractAgent {
245
250
  */
246
251
  connect(input) {
247
252
  this.threadId = input.threadId;
248
- this.runId = input.runId;
253
+ this.canonicalRunId = input.runId;
249
254
  return defer(() => this.requestConnectPlan$(input)).pipe(switchMap((plan) => {
250
255
  if (plan === null) return EMPTY;
251
256
  if (plan.mode === "bootstrap") {
252
257
  this.setLastSeenEventId(input.threadId, plan.latestEventId);
253
- for (const event of plan.events) this.updateRunIdFromEvent(event);
254
258
  return from(plan.events);
255
259
  }
256
260
  this.setLastSeenEventId(input.threadId, plan.joinFromEventId);
257
- for (const event of plan.events) this.updateRunIdFromEvent(event);
258
261
  return concat(from(plan.events), this.observeThread$(input, { joinToken: plan.joinToken }, {
259
262
  completeOnRunError: true,
260
263
  streamMode: "connect",
@@ -277,7 +280,7 @@ var IntelligenceAgent = class IntelligenceAgent extends AbstractAgent {
277
280
  if (this.socket === ownSocket) this.socket = null;
278
281
  }
279
282
  if (this.threadId) this.sharedState.lastSeenEventIds.delete(this.threadId);
280
- this.runId = null;
283
+ this.canonicalRunId = null;
281
284
  }
282
285
  cleanup() {
283
286
  this.cleanupOwned(this.activeChannel, this.socket);
@@ -413,7 +416,6 @@ var IntelligenceAgent = class IntelligenceAgent extends AbstractAgent {
413
416
  observeThreadEvents$(threadId, channel$, options) {
414
417
  return channel$.pipe(switchMap$1(({ channel }) => this.observeChannelEvent$(channel, CLIENT_AG_UI_EVENT)), tap((payload) => {
415
418
  this.updateLastSeenEventId(threadId, payload);
416
- this.updateRunIdFromEvent(payload);
417
419
  }), mergeMap((payload) => from(this.createThreadNotifications(payload, options.completeOnRunError))), dematerialize());
418
420
  }
419
421
  observeChannelEvent$(channel, eventName) {
@@ -463,23 +465,6 @@ var IntelligenceAgent = class IntelligenceAgent extends AbstractAgent {
463
465
  if (!eventId) return;
464
466
  this.sharedState.lastSeenEventIds.set(threadId, eventId);
465
467
  }
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
468
  readEventId(payload) {
484
469
  const metadata = payload.metadata;
485
470
  if (!metadata || typeof metadata !== "object") return null;
@@ -2308,8 +2293,21 @@ let CopilotKitCoreErrorCode = /* @__PURE__ */ function(CopilotKitCoreErrorCode)
2308
2293
  CopilotKitCoreErrorCode["TRANSCRIPTION_RATE_LIMITED"] = "transcription_rate_limited";
2309
2294
  CopilotKitCoreErrorCode["TRANSCRIPTION_AUTH_FAILED"] = "transcription_auth_failed";
2310
2295
  CopilotKitCoreErrorCode["TRANSCRIPTION_NETWORK_ERROR"] = "transcription_network_error";
2296
+ CopilotKitCoreErrorCode["SUBSCRIBER_CALLBACK_FAILED"] = "subscriber_callback_failed";
2311
2297
  return CopilotKitCoreErrorCode;
2312
2298
  }({});
2299
+ /**
2300
+ * Runtime allowlist derived from {@link SUBSCRIBE_TO_AGENT_KEYS}. Hoisted
2301
+ * to module scope so the Set is allocated once, not per-subscription.
2302
+ */
2303
+ const ALLOWED_KEYS = new Set([
2304
+ "onMessagesChanged",
2305
+ "onStateChanged",
2306
+ "onRunInitialized",
2307
+ "onRunFinalized",
2308
+ "onRunFailed",
2309
+ "onRunErrorEvent"
2310
+ ]);
2313
2311
  let CopilotKitCoreRuntimeConnectionStatus = /* @__PURE__ */ function(CopilotKitCoreRuntimeConnectionStatus) {
2314
2312
  CopilotKitCoreRuntimeConnectionStatus["Disconnected"] = "disconnected";
2315
2313
  CopilotKitCoreRuntimeConnectionStatus["Connected"] = "connected";
@@ -2375,6 +2373,16 @@ var CopilotKitCore = class {
2375
2373
  }), "Subscriber onError error:");
2376
2374
  }
2377
2375
  /**
2376
+ * Log a message to the console and emit an error to subscribers.
2377
+ * Catches failures from `emitError` itself to prevent unhandled rejections.
2378
+ */
2379
+ logAndEmitError(message, params, logLevel = "error") {
2380
+ console[logLevel](message, params.error);
2381
+ this.emitError(params).catch((emitErr) => {
2382
+ console.error(message + " — emitError itself failed:", emitErr);
2383
+ });
2384
+ }
2385
+ /**
2378
2386
  * Snapshot accessors
2379
2387
  */
2380
2388
  get context() {
@@ -2411,17 +2419,30 @@ var CopilotKitCore = class {
2411
2419
  return this._properties;
2412
2420
  }
2413
2421
  /**
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.
2422
+ * Default throttle interval (ms) used by `subscribeToAgentWithOptions()`
2423
+ * when the caller does not specify an explicit `throttleMs`.
2424
+ * `undefined` means no default is configured; `0` means no throttling.
2418
2425
  */
2419
2426
  get defaultThrottleMs() {
2420
2427
  return this._defaultThrottleMs;
2421
2428
  }
2429
+ /**
2430
+ * Set the default throttle interval (ms) for `subscribeToAgentWithOptions()`.
2431
+ *
2432
+ * Accepts a non-negative finite number or `undefined` (to clear the
2433
+ * default). Invalid values (NaN, Infinity, negative) are logged as
2434
+ * errors and ignored — the previous valid value is preserved.
2435
+ */
2422
2436
  setDefaultThrottleMs(value) {
2423
2437
  if (value !== void 0 && (!Number.isFinite(value) || value < 0)) {
2424
- this._defaultThrottleMs = void 0;
2438
+ this.logAndEmitError(`CopilotKitCore.setDefaultThrottleMs: value must be a non-negative finite number or undefined, got ${value}. Keeping current value (${this._defaultThrottleMs}).`, {
2439
+ error: /* @__PURE__ */ new Error(`setDefaultThrottleMs: invalid value (${value}), keeping current value (${this._defaultThrottleMs})`),
2440
+ code: CopilotKitCoreErrorCode.SUBSCRIBER_CALLBACK_FAILED,
2441
+ context: {
2442
+ value,
2443
+ currentValue: this._defaultThrottleMs
2444
+ }
2445
+ });
2425
2446
  return;
2426
2447
  }
2427
2448
  this._defaultThrottleMs = value;
@@ -2542,6 +2563,137 @@ var CopilotKitCore = class {
2542
2563
  } };
2543
2564
  }
2544
2565
  /**
2566
+ * Subscribe to an agent's notification and lifecycle events with
2567
+ * optional configuration (e.g. throttling).
2568
+ *
2569
+ * Wraps every callback with error protection (`safeCall`) and applies
2570
+ * the options before delegating to `agent.subscribe()`.
2571
+ *
2572
+ * See {@link SubscribeToAgentSubscriber} for the accepted callback subset
2573
+ * and the rationale for excluding AG-UI event handlers.
2574
+ */
2575
+ subscribeToAgentWithOptions(agent, subscriber, options) {
2576
+ const resolved = options?.throttleMs ?? this._defaultThrottleMs ?? 0;
2577
+ let effectiveMs = 0;
2578
+ if (!Number.isFinite(resolved) || resolved < 0) {
2579
+ const source = options?.throttleMs !== void 0 ? "throttleMs" : "defaultThrottleMs";
2580
+ this.logAndEmitError(`CopilotKitCore.subscribeToAgentWithOptions: ${source} must be a non-negative finite number, got ${resolved}. Falling back to unthrottled.`, {
2581
+ error: /* @__PURE__ */ new Error(`subscribeToAgentWithOptions: invalid ${source} (${resolved}), falling back to unthrottled`),
2582
+ code: CopilotKitCoreErrorCode.SUBSCRIBER_CALLBACK_FAILED,
2583
+ context: {
2584
+ agentId: agent.agentId,
2585
+ source,
2586
+ value: resolved
2587
+ }
2588
+ });
2589
+ } else effectiveMs = resolved;
2590
+ const agentLabel = agent.agentId || "(unknown agent)";
2591
+ const safeCall = (label, fn, ...args) => {
2592
+ const reportError = (err, verb) => {
2593
+ this.logAndEmitError(`CopilotKitCore.subscribeToAgentWithOptions[${agentLabel}]: ${label} callback ${verb}:`, {
2594
+ error: err instanceof Error ? err : new Error(String(err)),
2595
+ code: CopilotKitCoreErrorCode.SUBSCRIBER_CALLBACK_FAILED,
2596
+ context: {
2597
+ agentId: agent.agentId,
2598
+ callback: label
2599
+ }
2600
+ });
2601
+ };
2602
+ try {
2603
+ const result = fn(...args);
2604
+ if (result instanceof Promise) return result.catch((err) => {
2605
+ reportError(err, "rejected");
2606
+ });
2607
+ return result;
2608
+ } catch (err) {
2609
+ reportError(err, "threw");
2610
+ }
2611
+ };
2612
+ const guardAll = (sub) => {
2613
+ const guarded = {};
2614
+ if (sub.onMessagesChanged) {
2615
+ const fn = sub.onMessagesChanged;
2616
+ guarded.onMessagesChanged = (params) => safeCall("onMessagesChanged", fn, params);
2617
+ }
2618
+ if (sub.onStateChanged) {
2619
+ const fn = sub.onStateChanged;
2620
+ guarded.onStateChanged = (params) => safeCall("onStateChanged", fn, params);
2621
+ }
2622
+ if (sub.onRunInitialized) {
2623
+ const fn = sub.onRunInitialized;
2624
+ guarded.onRunInitialized = (params) => safeCall("onRunInitialized", fn, params);
2625
+ }
2626
+ if (sub.onRunFinalized) {
2627
+ const fn = sub.onRunFinalized;
2628
+ guarded.onRunFinalized = (params) => safeCall("onRunFinalized", fn, params);
2629
+ }
2630
+ if (sub.onRunFailed) {
2631
+ const fn = sub.onRunFailed;
2632
+ guarded.onRunFailed = (params) => safeCall("onRunFailed", fn, params);
2633
+ }
2634
+ if (sub.onRunErrorEvent) {
2635
+ const fn = sub.onRunErrorEvent;
2636
+ guarded.onRunErrorEvent = (params) => safeCall("onRunErrorEvent", fn, params);
2637
+ }
2638
+ return guarded;
2639
+ };
2640
+ for (const key of Object.keys(subscriber)) if (typeof subscriber[key] === "function" && !ALLOWED_KEYS.has(key)) {
2641
+ 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.`;
2642
+ this.logAndEmitError(message, {
2643
+ error: new Error(message),
2644
+ code: CopilotKitCoreErrorCode.SUBSCRIBER_CALLBACK_FAILED,
2645
+ context: {
2646
+ agentId: agent.agentId,
2647
+ droppedCallback: key
2648
+ }
2649
+ }, "warn");
2650
+ }
2651
+ if (effectiveMs <= 0) {
2652
+ const subscription = agent.subscribe(guardAll(subscriber));
2653
+ return { unsubscribe: () => subscription.unsubscribe() };
2654
+ }
2655
+ let active = true;
2656
+ let latestMessagesParams = null;
2657
+ let latestStateParams = null;
2658
+ const flushPending = () => {
2659
+ if (active && subscriber.onMessagesChanged && latestMessagesParams) {
2660
+ const params = latestMessagesParams;
2661
+ latestMessagesParams = null;
2662
+ safeCall("onMessagesChanged", subscriber.onMessagesChanged, params);
2663
+ }
2664
+ if (active && subscriber.onStateChanged && latestStateParams) {
2665
+ const params = latestStateParams;
2666
+ latestStateParams = null;
2667
+ safeCall("onStateChanged", subscriber.onStateChanged, params);
2668
+ }
2669
+ };
2670
+ const throttler = new Throttler(flushPending, {
2671
+ wait: effectiveMs,
2672
+ leading: true,
2673
+ trailing: true
2674
+ });
2675
+ const lifecycleOnly = {};
2676
+ if (subscriber.onRunInitialized) lifecycleOnly.onRunInitialized = subscriber.onRunInitialized;
2677
+ if (subscriber.onRunFinalized) lifecycleOnly.onRunFinalized = subscriber.onRunFinalized;
2678
+ if (subscriber.onRunFailed) lifecycleOnly.onRunFailed = subscriber.onRunFailed;
2679
+ if (subscriber.onRunErrorEvent) lifecycleOnly.onRunErrorEvent = subscriber.onRunErrorEvent;
2680
+ const wrappedSubscriber = guardAll(lifecycleOnly);
2681
+ if (subscriber.onMessagesChanged) wrappedSubscriber.onMessagesChanged = (params) => {
2682
+ latestMessagesParams = params;
2683
+ throttler.maybeExecute();
2684
+ };
2685
+ if (subscriber.onStateChanged) wrappedSubscriber.onStateChanged = (params) => {
2686
+ latestStateParams = params;
2687
+ throttler.maybeExecute();
2688
+ };
2689
+ const subscription = agent.subscribe(wrappedSubscriber);
2690
+ return { unsubscribe: () => {
2691
+ active = false;
2692
+ throttler.cancel();
2693
+ subscription.unsubscribe();
2694
+ } };
2695
+ }
2696
+ /**
2545
2697
  * Agent connectivity (delegated to RunHandler)
2546
2698
  */
2547
2699
  async connectAgent(params) {
@@ -3088,15 +3240,18 @@ const threadDomainEvents = createActionGroup("Thread Domain", {
3088
3240
  threadUpserted: props(),
3089
3241
  threadDeleted: props()
3090
3242
  });
3091
- function sortThreadsByUpdatedAt(threads) {
3092
- return [...threads].sort((left, right) => right.updatedAt.localeCompare(left.updatedAt));
3243
+ function sortThreadsByRecency(threads) {
3244
+ return [...threads].sort((left, right) => {
3245
+ const leftKey = left.lastRunAt ?? left.updatedAt ?? left.createdAt;
3246
+ return (right.lastRunAt ?? right.updatedAt ?? right.createdAt).localeCompare(leftKey);
3247
+ });
3093
3248
  }
3094
3249
  function upsertThread(threads, thread) {
3095
3250
  const existingIndex = threads.findIndex((item) => item.id === thread.id);
3096
- if (existingIndex === -1) return sortThreadsByUpdatedAt([...threads, thread]);
3251
+ if (existingIndex === -1) return sortThreadsByRecency([...threads, thread]);
3097
3252
  const next = [...threads];
3098
3253
  next[existingIndex] = thread;
3099
- return sortThreadsByUpdatedAt(next);
3254
+ return sortThreadsByRecency(next);
3100
3255
  }
3101
3256
  const threadReducer = createReducer(initialThreadState, on(threadAdapterEvents.contextChanged, (state, { context }) => ({
3102
3257
  ...state,
@@ -3129,7 +3284,7 @@ const threadReducer = createReducer(initialThreadState, on(threadAdapterEvents.c
3129
3284
  if (sessionId !== state.sessionId) return state;
3130
3285
  return {
3131
3286
  ...state,
3132
- threads: sortThreadsByUpdatedAt(threads),
3287
+ threads: sortThreadsByRecency(threads),
3133
3288
  isLoading: false,
3134
3289
  error: null,
3135
3290
  metadataJoinCode: joinCode,