@copilotkit/runtime 1.66.1 → 1.66.2-canary.1785878925

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/package.cjs CHANGED
@@ -5,7 +5,7 @@ const require_runtime = require('./_virtual/_rolldown/runtime.cjs');
5
5
  var require_package = /* @__PURE__ */ require_runtime.__commonJSMin(((exports, module) => {
6
6
  module.exports = {
7
7
  "name": "@copilotkit/runtime",
8
- "version": "1.66.1",
8
+ "version": "1.66.2-canary.1785878925",
9
9
  "private": false,
10
10
  "keywords": [
11
11
  "ai",
@@ -5,7 +5,7 @@ import { __commonJSMin } from "../_virtual/_rolldown/runtime.mjs";
5
5
  var require_package = /* @__PURE__ */ __commonJSMin(((exports, module) => {
6
6
  module.exports = {
7
7
  "name": "@copilotkit/runtime",
8
- "version": "1.66.1",
8
+ "version": "1.66.2-canary.1785878925",
9
9
  "private": false,
10
10
  "keywords": [
11
11
  "ai",
@@ -209,9 +209,21 @@ async function runCanonicalChannelAgent(runner, intelligence, lockTtlSeconds, lo
209
209
  next: (event) => {
210
210
  if (event.type !== _ag_ui_client.EventType.RUN_ERROR || terminalError) return;
211
211
  const message = "message" in event && typeof event.message === "string" ? event.message : "Canonical Channel agent run failed";
212
- terminalError = new Error(message);
212
+ const details = safeChannelRunErrorDetails(event);
213
+ terminalError = new Error(message, details ? { cause: details } : void 0);
213
214
  terminalError.name = "ChannelCanonicalRunError";
214
215
  if ("code" in event && typeof event.code === "string" && event.code.length > 0) terminalError.code = event.code;
216
+ if (details) {
217
+ terminalError.category = details.category;
218
+ terminalError.provider = details.provider;
219
+ terminalError.operation = details.operation;
220
+ terminalError.effectKind = details.effectKind;
221
+ terminalError.providerCode = details.providerCode;
222
+ terminalError.validationMessages = details.validationMessages;
223
+ terminalError.retryable = details.retryable;
224
+ terminalError.deliveryId = details.deliveryId;
225
+ terminalError.details = details;
226
+ }
215
227
  },
216
228
  error: reject,
217
229
  complete: () => {
@@ -238,6 +250,25 @@ async function runCanonicalChannelAgent(runner, intelligence, lockTtlSeconds, lo
238
250
  }
239
251
  return result;
240
252
  }
253
+ function safeChannelRunErrorDetails(event) {
254
+ if (!("details" in event) || typeof event.details !== "object" || event.details === null || Array.isArray(event.details)) return;
255
+ const details = event.details;
256
+ const allowed = new Set([
257
+ "category",
258
+ "provider",
259
+ "operation",
260
+ "effectKind",
261
+ "providerCode",
262
+ "validationMessages",
263
+ "retryable",
264
+ "deliveryId"
265
+ ]);
266
+ if (!Object.keys(details).every((field) => allowed.has(field)) || details.category !== "validation" || details.provider !== "slack" && details.provider !== "teams" || !boundedString(details.operation, 80) || !boundedString(details.effectKind, 80) || details.providerCode !== "invalid_arguments" && details.providerCode !== "invalid_blocks" || details.retryable !== false || !boundedString(details.deliveryId, 512) || !Array.isArray(details.validationMessages) || details.validationMessages.length > 5 || !details.validationMessages.every((validationMessage) => typeof validationMessage === "string" && validationMessage.length <= 256 && validationMessage.startsWith("invalid field at /"))) return;
267
+ return details;
268
+ }
269
+ function boundedString(value, maxLength) {
270
+ return typeof value === "string" && value.length > 0 && value.length <= maxLength;
271
+ }
241
272
  /** Convert canonical Intelligence history into AG-UI messages. */
242
273
  async function toAgentMessage(message, intelligence) {
243
274
  const content = await hydrateManagedContent(message.content, intelligence);
@@ -289,6 +320,12 @@ async function hydrateManagedContent(content, intelligence) {
289
320
  function isSetupRequired(err) {
290
321
  return err instanceof ChannelSetupRequiredError || typeof err === "object" && err !== null && err.code === "SETUP_REQUIRED";
291
322
  }
323
+ /** Whether a failed initial activation can recover without new configuration. */
324
+ function isRetryableActivationError(err) {
325
+ if (typeof err !== "object" || err === null) return false;
326
+ const value = err;
327
+ return (value.code === "GATEWAY_UNREACHABLE" || value.code === "GATEWAY_JOIN_FAILED") && value.retryable === true;
328
+ }
292
329
  /**
293
330
  * Whether `err` is a Node/runtime module-resolution failure — i.e. the error
294
331
  * a dynamic `import()` throws when the target package is not installed.
@@ -302,8 +339,14 @@ function isModuleNotFound(err) {
302
339
  }
303
340
  /** Default deadline (ms) for a single `handle.stop()` during teardown. */
304
341
  const DEFAULT_STOP_HANDLE_TIMEOUT_MS = 5e3;
305
- /** Default cadence (ms) for the "still down" log while a session is dropped. */
342
+ /** First delay (ms) before logging that a dropped session is still down. */
306
343
  const DEFAULT_RECONNECT_LOG_INTERVAL_MS = 3e4;
344
+ /** Longest delay (ms) between reminders during one continuous outage. */
345
+ const DEFAULT_RECONNECT_LOG_MAX_INTERVAL_MS = 15 * 6e4;
346
+ /** First delay (ms) before retrying a transient initial activation failure. */
347
+ const DEFAULT_ACTIVATION_RETRY_DELAY_MS = 1e3;
348
+ /** Longest delay (ms) between transient initial activation attempts. */
349
+ const DEFAULT_ACTIVATION_RETRY_MAX_DELAY_MS = 3e4;
307
350
  /**
308
351
  * Reject with `timeoutMessage` after `timeoutMs` if `inner` has not settled,
309
352
  * otherwise pass `inner` through. When `timeoutMs` is undefined, `inner` is
@@ -336,17 +379,18 @@ function withTimeout(inner, timeoutMs, timeoutMessage) {
336
379
  * {@link activate} starts it and a second call is a no-op. Activation throws
337
380
  * SYNCHRONOUSLY (a {@link ChannelConfigError}) only for a misconfiguration it
338
381
  * can detect up front — a duplicate or missing Channel name. Every OTHER
339
- * activation failure is recorded as the Channel's status (`error`, or
340
- * `setup_required` for a missing provider) and surfaced through {@link status}
341
- * and {@link ready} rather than thrown.
382
+ * permanent activation failure is recorded as the Channel's status (`error`,
383
+ * or `setup_required` for a missing provider) and surfaced through
384
+ * {@link status} and {@link ready} rather than thrown. A retryable initial
385
+ * gateway outage stays unsettled and retries until it connects or the manager
386
+ * stops.
342
387
  *
343
- * Reconnection is NOT handled here — it is delegated to the Phoenix connection
388
+ * Established-session reconnection is delegated to the Phoenix connection
344
389
  * layer that backs the launcher. When a managed control socket drops, Phoenix's
345
- * `Socket` reconnects and rejoins with the same Runtime declaration. Active
346
- * deliveries request fresh one-use join tokens through that control link. A
347
- * re-activation here would be both redundant AND broken: re-invoking the engine
348
- * on an already-started `Channel` throws in `channel.addAdapter` (started=true).
349
- * The manager therefore never re-activates on a drop.
390
+ * `Socket` reconnects and rejoins with the same Runtime declaration. The manager
391
+ * never re-activates an already-started Channel. It does retry a transient
392
+ * INITIAL gateway activation failure: that happens before the launcher adds or
393
+ * starts the managed adapter, so a later attempt is safe.
350
394
  *
351
395
  * It DOES, however, reflect real connection health through the session's
352
396
  * `onStateChange` observer so {@link ChannelManager.status} stays honest rather
@@ -381,8 +425,8 @@ var ChannelManager = class {
381
425
  /**
382
426
  * Start activation of every declared Channel (lazy + idempotent). Mints a
383
427
  * distinct runtime instance id per Channel, derives its activation config,
384
- * and calls the engine. Records each Channel as `connecting`, transitioning
385
- * to `online`/`setup_required`/`error` as its activation settles.
428
+ * and calls the engine. Transient gateway failures retry with exponential
429
+ * backoff; other outcomes transition to `online`/`setup_required`/`error`.
386
430
  */
387
431
  activate() {
388
432
  if (this.activated || this.stopped) return;
@@ -399,24 +443,23 @@ var ChannelManager = class {
399
443
  rejectSettled = reject;
400
444
  });
401
445
  settled.catch(() => {});
446
+ const entry = {
447
+ status: "connecting",
448
+ handle: void 0,
449
+ handleStopped: false,
450
+ settled
451
+ };
402
452
  let activation;
403
- let config;
404
453
  try {
405
- config = require_channel_activation_config.deriveChannelActivationConfig({
454
+ const config = require_channel_activation_config.deriveChannelActivationConfig({
406
455
  intelligence: this.intelligence,
407
456
  channel,
408
457
  runtimeInstanceId
409
458
  });
410
- activation = this.activateChannel(config, channel);
459
+ activation = this.activateWithRetry(config, channel, name, entry);
411
460
  } catch (err) {
412
461
  activation = Promise.reject(err);
413
462
  }
414
- const entry = {
415
- status: "connecting",
416
- handle: void 0,
417
- handleStopped: false,
418
- settled
419
- };
420
463
  activation.then(async (handle) => {
421
464
  entry.handle = handle;
422
465
  if (this.stopped) {
@@ -469,6 +512,54 @@ var ChannelManager = class {
469
512
  }
470
513
  }
471
514
  /**
515
+ * Retry only transient failures from the pre-adapter gateway connection.
516
+ * Permanent errors reject on the first attempt; teardown cancels a pending
517
+ * timer while preserving the existing late-settle handling for in-flight work.
518
+ */
519
+ activateWithRetry(config, channel, name, entry) {
520
+ return new Promise((resolve, reject) => {
521
+ const attempt = () => {
522
+ let activation;
523
+ try {
524
+ activation = this.activateChannel(config, channel);
525
+ } catch (err) {
526
+ activation = Promise.reject(err);
527
+ }
528
+ activation.then((handle) => {
529
+ this.clearActivationRetry(entry);
530
+ resolve(handle);
531
+ }, (err) => {
532
+ if (this.stopped || !isRetryableActivationError(err)) {
533
+ this.clearActivationRetry(entry);
534
+ reject(err);
535
+ return;
536
+ }
537
+ const delayMs = entry.activationRetryDelayMs ?? DEFAULT_ACTIVATION_RETRY_DELAY_MS;
538
+ entry.status = "reconnecting";
539
+ entry.activationRetryDelayMs = Math.min(delayMs * 2, DEFAULT_ACTIVATION_RETRY_MAX_DELAY_MS);
540
+ this.log?.(`channel "${name}" failed to activate; retrying in ${delayMs}ms`, err);
541
+ const timer = setTimeout(() => {
542
+ entry.activationRetryTimer = void 0;
543
+ entry.cancelActivationRetry = void 0;
544
+ if (this.stopped || entry.status === "stopped") {
545
+ reject(err);
546
+ return;
547
+ }
548
+ entry.status = "connecting";
549
+ attempt();
550
+ }, delayMs);
551
+ timer.unref?.();
552
+ entry.activationRetryTimer = timer;
553
+ entry.cancelActivationRetry = () => {
554
+ this.clearActivationRetry(entry);
555
+ reject(err);
556
+ };
557
+ });
558
+ };
559
+ attempt();
560
+ });
561
+ }
562
+ /**
472
563
  * Throw if two declared Channels share a `name`. `entries` is keyed by name,
473
564
  * so a duplicate would overwrite the first Channel's entry and leak its live
474
565
  * session. Called at the very start of {@link activate}, before any engine
@@ -611,29 +702,49 @@ var ChannelManager = class {
611
702
  return entry.downSince === void 0 ? "unknown" : `${Math.round((Date.now() - entry.downSince) / 1e3)}s`;
612
703
  }
613
704
  /**
614
- * Repeat a "still down" line for as long as this outage lasts. Runs THROUGH
615
- * `gave_up` on purpose: that transition is where the old behavior went quiet,
616
- * and an operator watching a silent process cannot tell a dead bot from an
617
- * idle one.
705
+ * Repeat a "still down" line for as long as this outage lasts, with an
706
+ * exponential delay capped at 15 minutes. Runs THROUGH `gave_up` on purpose:
707
+ * that transition is where the old behavior went quiet, and an operator
708
+ * watching a silent process cannot tell a dead bot from an idle one.
618
709
  */
619
710
  startReconnectLog(name, entry) {
620
711
  if (entry.reconnectLogTimer !== void 0) return;
621
- const timer = setInterval(() => {
712
+ const delayMs = entry.reconnectLogDelayMs ?? this.reconnectLogIntervalMs;
713
+ const timer = setTimeout(() => {
714
+ entry.reconnectLogTimer = void 0;
622
715
  if (this.stopped || entry.status === "stopped") {
623
716
  this.clearReconnectLog(entry);
624
717
  return;
625
718
  }
626
719
  this.log?.(`channel "${name}" managed session still down after ${this.downFor(entry)}; Phoenix is retrying`);
627
- }, this.reconnectLogIntervalMs);
720
+ entry.reconnectLogDelayMs = Math.min(delayMs * 2, Math.max(this.reconnectLogIntervalMs, DEFAULT_RECONNECT_LOG_MAX_INTERVAL_MS));
721
+ this.startReconnectLog(name, entry);
722
+ }, delayMs);
628
723
  timer.unref?.();
629
724
  entry.reconnectLogTimer = timer;
630
725
  }
631
726
  /** Stop this entry's "still down" repeat, if one is running. */
632
727
  clearReconnectLog(entry) {
633
728
  if (entry.reconnectLogTimer !== void 0) {
634
- clearInterval(entry.reconnectLogTimer);
729
+ clearTimeout(entry.reconnectLogTimer);
635
730
  entry.reconnectLogTimer = void 0;
636
731
  }
732
+ entry.reconnectLogDelayMs = void 0;
733
+ }
734
+ /** Cancel a pending transient activation retry and reset its backoff. */
735
+ clearActivationRetry(entry) {
736
+ if (entry.activationRetryTimer !== void 0) {
737
+ clearTimeout(entry.activationRetryTimer);
738
+ entry.activationRetryTimer = void 0;
739
+ }
740
+ entry.activationRetryDelayMs = void 0;
741
+ entry.cancelActivationRetry = void 0;
742
+ }
743
+ /** Cancel a scheduled activation retry and settle its wrapper. */
744
+ cancelActivationRetry(entry) {
745
+ const cancel = entry.cancelActivationRetry;
746
+ if (cancel) cancel();
747
+ else this.clearActivationRetry(entry);
637
748
  }
638
749
  /**
639
750
  * Drive a single entry to its terminal `stopped` state, tearing down its
@@ -670,6 +781,7 @@ var ChannelManager = class {
670
781
  async stopEntry(entry) {
671
782
  entry.status = "stopped";
672
783
  this.clearReconnectLog(entry);
784
+ this.cancelActivationRetry(entry);
673
785
  if (entry.handle && !entry.handleStopped) {
674
786
  entry.handleStopped = true;
675
787
  const handle = entry.handle;