@copilotkit/core 1.69.3 → 1.70.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.cjs CHANGED
@@ -127,6 +127,7 @@ function ɵobservePhoenixSocketHealth$(socketSignals$, maxConsecutiveErrors) {
127
127
 
128
128
  //#endregion
129
129
  //#region src/intelligence-agent.ts
130
+ const globalFetch = (...args) => fetch(...args);
130
131
  const CLIENT_AG_UI_EVENT = "ag_ui_event";
131
132
  const REPLAY_COMPLETE_EVENT = "replay_complete";
132
133
  const STREAM_IDLE_EVENT = "stream_idle";
@@ -353,7 +354,7 @@ var IntelligenceAgent = class IntelligenceAgent extends _ag_ui_client.AbstractAg
353
354
  requestJoinCredentials$(mode, input, replayCursor) {
354
355
  return (0, rxjs.defer)(async () => {
355
356
  try {
356
- const response = await fetch(this.buildRuntimeUrl(mode), {
357
+ const response = await (this.config.fetch ?? globalFetch)(this.buildRuntimeUrl(mode), {
357
358
  method: "POST",
358
359
  headers: {
359
360
  "Content-Type": "application/json",
@@ -594,7 +595,12 @@ async function runtimeInfoError(response) {
594
595
  const message = (await response.clone().json())?.message;
595
596
  if (typeof message === "string" && message.trim().length > 0) detail = message.trim();
596
597
  } catch {}
597
- return new Error(detail ? `${base}: ${detail}` : base);
598
+ const error = new Error(detail ? `${base}: ${detail}` : base);
599
+ error.runtimeInfoStatus = response.status;
600
+ return error;
601
+ }
602
+ function isRuntimeInfoRequestError(error) {
603
+ return typeof error === "object" && error !== null && typeof error.runtimeInfoStatus === "number";
598
604
  }
599
605
 
600
606
  //#endregion
@@ -608,12 +614,12 @@ function hasCredentials(agent) {
608
614
  function isZodError(error) {
609
615
  return error !== null && typeof error === "object" && "name" in error && error.name === "ZodError";
610
616
  }
611
- function isAbortError$1(error) {
617
+ function isAbortError$2(error) {
612
618
  return (error instanceof DOMException || error instanceof Error) && error.name === "AbortError";
613
619
  }
614
620
  function withAbortErrorHandling(observable) {
615
621
  return observable.pipe((0, rxjs_operators.catchError)((error) => {
616
- if (isZodError(error) || isAbortError$1(error)) return rxjs.EMPTY;
622
+ if (isZodError(error) || isAbortError$2(error)) return rxjs.EMPTY;
617
623
  throw error;
618
624
  }));
619
625
  }
@@ -811,7 +817,8 @@ var ProxiedCopilotRuntimeAgent = class ProxiedCopilotRuntimeAgent extends _ag_ui
811
817
  runtimeMode: this.runtimeMode,
812
818
  intelligence: this.intelligence,
813
819
  capabilities: this._capabilities,
814
- debug: this.debug
820
+ debug: this.debug,
821
+ fetch: this.fetch
815
822
  });
816
823
  cloned.threadId = this.threadId;
817
824
  cloned.setState(this.state);
@@ -881,7 +888,7 @@ var ProxiedCopilotRuntimeAgent = class ProxiedCopilotRuntimeAgent extends _ag_ui
881
888
  url = `${this.runtimeUrl}/info`;
882
889
  init = {};
883
890
  }
884
- const response = await fetch(url, {
891
+ const response = await this.fetch(url, {
885
892
  ...init,
886
893
  headers,
887
894
  ...this.credentials ? { credentials: this.credentials } : {}
@@ -891,7 +898,7 @@ var ProxiedCopilotRuntimeAgent = class ProxiedCopilotRuntimeAgent extends _ag_ui
891
898
  }
892
899
  async fetchRuntimeInfoAutoDetect(headers) {
893
900
  try {
894
- const response = await fetch(`${this.runtimeUrl}/info`, {
901
+ const response = await this.fetch(`${this.runtimeUrl}/info`, {
895
902
  headers: { ...headers },
896
903
  ...this.credentials ? { credentials: this.credentials } : {}
897
904
  });
@@ -902,7 +909,7 @@ var ProxiedCopilotRuntimeAgent = class ProxiedCopilotRuntimeAgent extends _ag_ui
902
909
  } catch {}
903
910
  const singleHeaders = { ...headers };
904
911
  if (!singleHeaders["Content-Type"]) singleHeaders["Content-Type"] = "application/json";
905
- const response = await fetch(this.runtimeUrl, {
912
+ const response = await this.fetch(this.runtimeUrl, {
906
913
  method: "POST",
907
914
  headers: singleHeaders,
908
915
  body: JSON.stringify({ method: "info" }),
@@ -943,7 +950,8 @@ var ProxiedCopilotRuntimeAgent = class ProxiedCopilotRuntimeAgent extends _ag_ui
943
950
  runtimeUrl: this.runtimeUrl,
944
951
  agentId: routedId,
945
952
  headers: { ...this.headers },
946
- credentials: this.credentials
953
+ credentials: this.credentials,
954
+ fetch: this.fetch
947
955
  });
948
956
  }
949
957
  syncDelegate(delegate) {
@@ -957,16 +965,44 @@ var ProxiedCopilotRuntimeAgent = class ProxiedCopilotRuntimeAgent extends _ag_ui
957
965
  }
958
966
  };
959
967
 
968
+ //#endregion
969
+ //#region src/utils/abort-error.ts
970
+ const ABORT_MESSAGES = new Set([
971
+ "Fetch is aborted",
972
+ "signal is aborted without reason",
973
+ "component unmounted"
974
+ ]);
975
+ function isAbortError$1(error) {
976
+ if (typeof error !== "object" || error === null) return false;
977
+ const { name, message } = error;
978
+ if (name === "AbortError") return true;
979
+ return typeof message === "string" && ABORT_MESSAGES.has(message);
980
+ }
981
+
982
+ //#endregion
983
+ //#region src/utils/runtime-request.ts
984
+ const RUNTIME_REQUEST_WATCHDOG_MS = 1e4;
985
+ function runtimeRequestMeta(init) {
986
+ return init?.ɵruntimeRequest;
987
+ }
988
+
960
989
  //#endregion
961
990
  //#region src/core/agent-registry.ts
962
991
  /** Maximum wait for optional Inspector metadata before degrading to absence. */
963
992
  const INSPECTOR_METADATA_REQUEST_TIMEOUT_MS = 5e3;
993
+ const ɵRUNTIME_PROBE_TIMEOUT_MS = 5e3;
994
+ function classifyRuntimeRequestFailure(error, meta) {
995
+ if (meta?.nonCritical) return "ignored";
996
+ if (isAbortError$1(error) && !meta?.timedOut) return "aborted";
997
+ return "failed";
998
+ }
964
999
  /** Build case-insensitive JSON headers without mutating the Core snapshot. */
965
1000
  function withJsonContentType(headers) {
966
1001
  const requestHeaders = new Headers(headers);
967
1002
  if (!requestHeaders.has("content-type")) requestHeaders.set("content-type", "application/json");
968
1003
  return requestHeaders;
969
1004
  }
1005
+ const RUNTIME_ENTITLEMENT_RETRY_DELAY_MS = 5e3;
970
1006
  /**
971
1007
  * Manages agent registration, lifecycle, and runtime connectivity for CopilotKitCore.
972
1008
  * Handles both local development agents and remote runtime agents.
@@ -975,8 +1011,14 @@ var AgentRegistry = class {
975
1011
  _agents = {};
976
1012
  localAgents = {};
977
1013
  remoteAgents = {};
1014
+ mintedThreadIds = /* @__PURE__ */ new WeakMap();
978
1015
  _runtimeUrl;
979
1016
  _connectionInFlight;
1017
+ _runtimeConnectionGeneration = 0;
1018
+ _activeRuntimeConnectionAttempt;
1019
+ _runtimeEntitlementRetryTimer;
1020
+ _runtimeEntitlementRetryAttemptedKey;
1021
+ _runtimeEntitlementRetryPendingKey;
980
1022
  _runtimeVersion;
981
1023
  _runtimeConnectionStatus = CopilotKitCoreRuntimeConnectionStatus.Disconnected;
982
1024
  _runtimeTransport = "auto";
@@ -999,7 +1041,16 @@ var AgentRegistry = class {
999
1041
  _a2uiAgents;
1000
1042
  _openGenerativeUIEnabled = false;
1001
1043
  _licenseStatus;
1044
+ _runtimeEntitlements;
1002
1045
  _telemetryDisabled = false;
1046
+ remoteAgentConnections = /* @__PURE__ */ new WeakMap();
1047
+ runtimeFetch;
1048
+ runtimeProbeInFlight = false;
1049
+ runtimeProbeToken = 0;
1050
+ runtimeProbeAbortController;
1051
+ runtimeRecoveryRunning = false;
1052
+ runtimeRecoveryPending = false;
1053
+ runtimeHealthGeneration = 0;
1003
1054
  /**
1004
1055
  * The headers each HttpAgent was constructed with, captured on the first
1005
1056
  * `applyHeadersToAgent` call for that agent (which, for agents the registry
@@ -1066,6 +1117,13 @@ var AgentRegistry = class {
1066
1117
  get licenseStatus() {
1067
1118
  return this._licenseStatus;
1068
1119
  }
1120
+ /** Structured Runtime entitlement authority advertised by `/info`. */
1121
+ get runtimeEntitlements() {
1122
+ return this._runtimeEntitlements;
1123
+ }
1124
+ get runtimeEntitlementRetryPending() {
1125
+ return this._runtimeEntitlementRetryPendingKey === this.runtimeConnectionKey();
1126
+ }
1069
1127
  get telemetryDisabled() {
1070
1128
  return this._telemetryDisabled;
1071
1129
  }
@@ -1076,6 +1134,7 @@ var AgentRegistry = class {
1076
1134
  this.localAgents = this.assignAgentIds(agents);
1077
1135
  this.applyHeadersToAgents(this.localAgents);
1078
1136
  this.applyCredentialsToAgents(this.localAgents);
1137
+ this.applyRuntimeFetchToAgents(this.localAgents);
1079
1138
  this._agents = this.localAgents;
1080
1139
  }
1081
1140
  /**
@@ -1085,9 +1144,13 @@ var AgentRegistry = class {
1085
1144
  const normalizedRuntimeUrl = runtimeUrl ? runtimeUrl.replace(/\/$/, "") : void 0;
1086
1145
  if (this._runtimeUrl === normalizedRuntimeUrl) return;
1087
1146
  this.invalidateInspectorMetadataConnection();
1147
+ this.abandonRuntimeHealthProbe();
1148
+ this.resetRuntimeEntitlementRetry();
1149
+ this._licenseStatus = void 0;
1150
+ this._runtimeEntitlements = void 0;
1088
1151
  this._runtimeUrl = normalizedRuntimeUrl;
1089
1152
  if (options?.deferConnection) return;
1090
- this.updateRuntimeConnection();
1153
+ this.updateRuntimeConnection({ preserveOnFailure: this.hasLiveRuntimeKnowledgeToProtect() });
1091
1154
  }
1092
1155
  /**
1093
1156
  * Start the initial runtime connection if it has not been started yet.
@@ -1109,9 +1172,40 @@ var AgentRegistry = class {
1109
1172
  setRuntimeTransport(runtimeTransport) {
1110
1173
  if (this._requestedTransport === runtimeTransport) return;
1111
1174
  this.invalidateInspectorMetadataConnection();
1175
+ this.abandonRuntimeHealthProbe();
1176
+ this.resetRuntimeEntitlementRetry();
1112
1177
  this._requestedTransport = runtimeTransport;
1113
1178
  this._runtimeTransport = runtimeTransport;
1114
- this.updateRuntimeConnection();
1179
+ this.updateRuntimeConnection({ preserveOnFailure: this.hasLiveRuntimeKnowledgeToProtect() });
1180
+ }
1181
+ hasLiveRuntimeKnowledgeToProtect() {
1182
+ if (Object.keys(this.remoteAgents).length === 0) return false;
1183
+ return this._runtimeConnectionStatus !== CopilotKitCoreRuntimeConnectionStatus.Connected;
1184
+ }
1185
+ reconcileRecoveredAgents(reported) {
1186
+ const merged = {
1187
+ ...this.remoteAgents,
1188
+ ...reported
1189
+ };
1190
+ if (Object.keys(reported).length === 0) return merged;
1191
+ for (const [id, agent] of Object.entries(merged)) {
1192
+ if (Object.prototype.hasOwnProperty.call(reported, id)) continue;
1193
+ if (this.carriesConversationState(agent)) continue;
1194
+ delete merged[id];
1195
+ }
1196
+ return merged;
1197
+ }
1198
+ carriesConversationState(agent) {
1199
+ if (agent.messages.length > 0) return true;
1200
+ const mintedThreadId = this.mintedThreadIds.get(agent);
1201
+ return mintedThreadId === void 0 || agent.threadId !== mintedThreadId;
1202
+ }
1203
+ abandonRuntimeHealthProbe() {
1204
+ this.runtimeProbeToken += 1;
1205
+ this.runtimeProbeInFlight = false;
1206
+ this.runtimeHealthGeneration += 1;
1207
+ this.runtimeProbeAbortController?.abort();
1208
+ this.runtimeProbeAbortController = void 0;
1115
1209
  }
1116
1210
  /**
1117
1211
  * Set all agents at once (for development use)
@@ -1127,6 +1221,7 @@ var AgentRegistry = class {
1127
1221
  };
1128
1222
  this.applyHeadersToAgents(this._agents);
1129
1223
  this.applyCredentialsToAgents(this._agents);
1224
+ this.applyRuntimeFetchToAgents(this._agents);
1130
1225
  this.notifyAgentsChanged();
1131
1226
  }
1132
1227
  /**
@@ -1137,6 +1232,7 @@ var AgentRegistry = class {
1137
1232
  this.localAgents[id] = agent;
1138
1233
  this.applyHeadersToAgent(agent);
1139
1234
  this.applyCredentialsToAgent(agent);
1235
+ this.applyRuntimeFetchToAgent(agent);
1140
1236
  this._agents = {
1141
1237
  ...this.localAgents,
1142
1238
  ...this.remoteAgents
@@ -1180,6 +1276,7 @@ var AgentRegistry = class {
1180
1276
  debug: debug ? (0, _copilotkit_shared.resolveDebugConfig)(debug) : void 0
1181
1277
  });
1182
1278
  this.applyHeadersToAgent(agent);
1279
+ this.applyRuntimeFetchToAgent(agent);
1183
1280
  this.localAgents[agentId] = agent;
1184
1281
  this._agents = {
1185
1282
  ...this.localAgents,
@@ -1245,6 +1342,45 @@ var AgentRegistry = class {
1245
1342
  this.applyCredentialsToAgent(agent);
1246
1343
  });
1247
1344
  }
1345
+ createRuntimeFetch() {
1346
+ if (!this.runtimeFetch) this.runtimeFetch = (async (input, init) => {
1347
+ const meta = () => runtimeRequestMeta(init);
1348
+ const watchdog = this.armRuntimeRequestWatchdog(meta());
1349
+ try {
1350
+ const response = await fetch(input, init);
1351
+ watchdog.clear();
1352
+ this.handleRuntimeRequestOutcome(response.ok ? "ok" : meta()?.nonCritical ? "ignored" : "failed", watchdog.checked);
1353
+ return response;
1354
+ } catch (error) {
1355
+ watchdog.clear();
1356
+ this.handleRuntimeRequestOutcome(classifyRuntimeRequestFailure(error, meta()), watchdog.checked);
1357
+ throw error;
1358
+ }
1359
+ });
1360
+ return this.runtimeFetch;
1361
+ }
1362
+ armRuntimeRequestWatchdog(meta) {
1363
+ if (typeof window === "undefined" || meta?.nonCritical || meta?.selfBounded) return {
1364
+ clear: () => {},
1365
+ checked: () => false
1366
+ };
1367
+ let checked = false;
1368
+ const timeoutId = setTimeout(() => {
1369
+ checked = this.handleRuntimeRequestOutcome("failed");
1370
+ }, RUNTIME_REQUEST_WATCHDOG_MS);
1371
+ return {
1372
+ clear: () => clearTimeout(timeoutId),
1373
+ checked: () => checked
1374
+ };
1375
+ }
1376
+ applyRuntimeFetchToAgent(agent) {
1377
+ if (agent instanceof ProxiedCopilotRuntimeAgent) agent.fetch = this.createRuntimeFetch();
1378
+ }
1379
+ applyRuntimeFetchToAgents(agents) {
1380
+ Object.values(agents).forEach((agent) => {
1381
+ this.applyRuntimeFetchToAgent(agent);
1382
+ });
1383
+ }
1248
1384
  /** Refresh metadata after the Core header snapshot changes. */
1249
1385
  handleHeadersChanged() {
1250
1386
  this.inspectorMetadataHeadersGeneration += 1;
@@ -1380,17 +1516,114 @@ var AgentRegistry = class {
1380
1516
  console.error("Subscriber onInspectorMetadataChanged queue error:", error);
1381
1517
  });
1382
1518
  }
1519
+ setRuntimeConnectionStatus(status) {
1520
+ this._runtimeConnectionStatus = status;
1521
+ this.runtimeHealthGeneration += 1;
1522
+ }
1523
+ handleRuntimeRequestOutcome(outcome, alreadyChecked = () => false) {
1524
+ if (outcome === "aborted" || outcome === "ignored") return false;
1525
+ if (outcome === "failed" && alreadyChecked()) return false;
1526
+ if (typeof window === "undefined") return false;
1527
+ if (!this._runtimeUrl) return false;
1528
+ if (outcome === "ok") {
1529
+ this.runtimeHealthGeneration += 1;
1530
+ if (this.runtimeRecoveryRunning) this.runtimeRecoveryPending = true;
1531
+ if (this._runtimeConnectionStatus === CopilotKitCoreRuntimeConnectionStatus.Error) this.recoverRuntimeConnection();
1532
+ return false;
1533
+ }
1534
+ if (this._runtimeConnectionStatus !== CopilotKitCoreRuntimeConnectionStatus.Connected) return false;
1535
+ if (this.runtimeProbeInFlight) return false;
1536
+ this.probeRuntimeReachability();
1537
+ return true;
1538
+ }
1539
+ async fetchRuntimeInfoWithTimeout(abortController, runtimeUrl = this.runtimeUrl, runtimeTransport = this._runtimeTransport) {
1540
+ if (!runtimeUrl) throw new Error("Runtime URL is not set");
1541
+ let timeoutId;
1542
+ const request = this.fetchRuntimeInfo(runtimeUrl, runtimeTransport, abortController.signal);
1543
+ request.catch(() => {});
1544
+ try {
1545
+ const timedOut = new Promise((_resolve, reject) => {
1546
+ timeoutId = setTimeout(() => {
1547
+ abortController.abort();
1548
+ reject(/* @__PURE__ */ new Error(`Runtime did not answer within ${ɵRUNTIME_PROBE_TIMEOUT_MS}ms`));
1549
+ }, ɵRUNTIME_PROBE_TIMEOUT_MS);
1550
+ });
1551
+ return await Promise.race([request, timedOut]);
1552
+ } finally {
1553
+ if (timeoutId !== void 0) clearTimeout(timeoutId);
1554
+ }
1555
+ }
1556
+ async probeRuntimeReachability() {
1557
+ const generation = this.runtimeHealthGeneration;
1558
+ const token = ++this.runtimeProbeToken;
1559
+ this.runtimeProbeInFlight = true;
1560
+ const abortController = new AbortController();
1561
+ this.runtimeProbeAbortController = abortController;
1562
+ try {
1563
+ await this.fetchRuntimeInfoWithTimeout(abortController);
1564
+ } catch (error) {
1565
+ if (generation !== this.runtimeHealthGeneration) return;
1566
+ await this.markRuntimeUnreachable(error instanceof Error ? error : new Error(String(error)));
1567
+ } finally {
1568
+ if (this.runtimeProbeAbortController === abortController) this.runtimeProbeAbortController = void 0;
1569
+ if (this.runtimeProbeToken === token) this.runtimeProbeInFlight = false;
1570
+ }
1571
+ }
1572
+ async markRuntimeUnreachable(error) {
1573
+ this.setRuntimeConnectionStatus(CopilotKitCoreRuntimeConnectionStatus.Error);
1574
+ await this.notifyRuntimeStatusChanged(CopilotKitCoreRuntimeConnectionStatus.Error);
1575
+ const runtimeStatus = isRuntimeInfoRequestError(error) ? error.runtimeInfoStatus : void 0;
1576
+ _copilotkit_shared.logger.warn(runtimeStatus === void 0 ? `Runtime did not answer the identification request (${this._runtimeUrl}/info): ${error.message}. The runtime appears to be unreachable.` : `Runtime answered the identification request with status ${runtimeStatus} (${this._runtimeUrl}/info): ${error.message}. The runtime is reachable but refused the request — check credentials and authorisation before addresses and ports.`);
1577
+ await this.core.emitError({
1578
+ error,
1579
+ code: CopilotKitCoreErrorCode.RUNTIME_INFO_FETCH_FAILED,
1580
+ context: {
1581
+ runtimeUrl: this._runtimeUrl,
1582
+ reason: runtimeStatus === void 0 ? "no-answer" : "answered",
1583
+ ...runtimeStatus === void 0 ? {} : { runtimeStatus }
1584
+ }
1585
+ });
1586
+ }
1587
+ async recoverRuntimeConnection() {
1588
+ if (this.runtimeRecoveryRunning) {
1589
+ this.runtimeRecoveryPending = true;
1590
+ return;
1591
+ }
1592
+ this.runtimeRecoveryRunning = true;
1593
+ try {
1594
+ do {
1595
+ this.runtimeRecoveryPending = false;
1596
+ await this.updateRuntimeConnection({
1597
+ preserveOnFailure: true,
1598
+ recovery: true
1599
+ });
1600
+ } while (this.runtimeRecoveryPending && this._runtimeConnectionStatus !== CopilotKitCoreRuntimeConnectionStatus.Connected);
1601
+ } finally {
1602
+ this.runtimeRecoveryRunning = false;
1603
+ this.runtimeRecoveryPending = false;
1604
+ }
1605
+ }
1383
1606
  /**
1384
1607
  * Update runtime connection and fetch remote agents
1385
1608
  */
1386
- async updateRuntimeConnection() {
1609
+ async updateRuntimeConnection(options) {
1387
1610
  if (typeof window === "undefined") return;
1388
- const key = `${this._runtimeUrl ?? ""}::${this._requestedTransport}`;
1611
+ const key = this.runtimeConnectionKey();
1389
1612
  const inFlight = this._connectionInFlight;
1390
- if (inFlight && inFlight.key === key) return inFlight.promise;
1391
- const promise = this.performRuntimeConnection();
1392
- this._connectionInFlight = {
1613
+ if (inFlight && inFlight.attempt.key === key && this.isCurrentRuntimeConnection(inFlight.attempt)) return inFlight.promise;
1614
+ const attempt = {
1393
1615
  key,
1616
+ generation: ++this._runtimeConnectionGeneration
1617
+ };
1618
+ this._activeRuntimeConnectionAttempt = attempt;
1619
+ const promise = this.performRuntimeConnection({
1620
+ attempt,
1621
+ runtimeUrl: this._runtimeUrl,
1622
+ transport: this._runtimeTransport,
1623
+ options
1624
+ });
1625
+ this._connectionInFlight = {
1626
+ attempt,
1394
1627
  promise
1395
1628
  };
1396
1629
  promise.finally(() => {
@@ -1398,10 +1631,64 @@ var AgentRegistry = class {
1398
1631
  });
1399
1632
  return promise;
1400
1633
  }
1401
- async performRuntimeConnection() {
1402
- if (!this.runtimeUrl) {
1634
+ /** Return the stable key that scopes connection and entitlement retries. */
1635
+ runtimeConnectionKey() {
1636
+ return `${this._runtimeUrl ?? ""}::${this._requestedTransport}`;
1637
+ }
1638
+ /** Return whether a proxy still targets this Runtime connection. */
1639
+ canReuseRuntimeAgent(agent, runtimeUrl, transport) {
1640
+ if (!runtimeUrl) return false;
1641
+ const connection = this.remoteAgentConnections.get(agent);
1642
+ return connection?.runtimeUrl === runtimeUrl.replace(/\/$/, "") && connection.transport === transport;
1643
+ }
1644
+ /** Return whether an async attempt still owns the active Runtime state. */
1645
+ isCurrentRuntimeConnection(attempt) {
1646
+ return this._activeRuntimeConnectionAttempt?.generation === attempt.generation && this.runtimeConnectionKey() === attempt.key;
1647
+ }
1648
+ /** Cancel a pending entitlement retry and allow a new target to retry. */
1649
+ resetRuntimeEntitlementRetry() {
1650
+ if (this._runtimeEntitlementRetryTimer !== void 0) {
1651
+ clearTimeout(this._runtimeEntitlementRetryTimer);
1652
+ this._runtimeEntitlementRetryTimer = void 0;
1653
+ }
1654
+ this._runtimeEntitlementRetryAttemptedKey = void 0;
1655
+ this._runtimeEntitlementRetryPendingKey = void 0;
1656
+ }
1657
+ /** Schedule one retry after Runtime's short failed-entitlement cache. */
1658
+ updateRuntimeEntitlementRetry(runtimeEntitlements, attempt) {
1659
+ if (!this.isCurrentRuntimeConnection(attempt)) return;
1660
+ const { key } = attempt;
1661
+ if (runtimeEntitlements?.status === "ready" || runtimeEntitlements?.error.retryable !== true) {
1662
+ this.resetRuntimeEntitlementRetry();
1663
+ return;
1664
+ }
1665
+ if (this._runtimeEntitlementRetryTimer !== void 0) return;
1666
+ if (this._runtimeEntitlementRetryAttemptedKey === key) {
1667
+ this._runtimeEntitlementRetryPendingKey = void 0;
1668
+ return;
1669
+ }
1670
+ this._runtimeEntitlementRetryAttemptedKey = key;
1671
+ this._runtimeEntitlementRetryPendingKey = key;
1672
+ this._runtimeEntitlementRetryTimer = setTimeout(() => {
1673
+ this._runtimeEntitlementRetryTimer = void 0;
1674
+ this.executeRuntimeEntitlementRetry(attempt).catch((error) => {
1675
+ const message = error instanceof Error ? error.message : JSON.stringify(error);
1676
+ _copilotkit_shared.logger.warn(`Failed to retry runtime info (${this.runtimeUrl}/info): ${message}`);
1677
+ });
1678
+ }, RUNTIME_ENTITLEMENT_RETRY_DELAY_MS);
1679
+ }
1680
+ /** Start the bounded retry after the original connection has settled. */
1681
+ async executeRuntimeEntitlementRetry(attempt) {
1682
+ const inFlight = this._connectionInFlight;
1683
+ if (inFlight?.attempt.generation === attempt.generation) await inFlight.promise;
1684
+ if (!this._runtimeUrl || !this.isCurrentRuntimeConnection(attempt)) return;
1685
+ await this.updateRuntimeConnection();
1686
+ }
1687
+ async performRuntimeConnection({ attempt, runtimeUrl, transport, options }) {
1688
+ const { key } = attempt;
1689
+ if (!runtimeUrl) {
1403
1690
  this.invalidateInspectorMetadataConnection();
1404
- this._runtimeConnectionStatus = CopilotKitCoreRuntimeConnectionStatus.Disconnected;
1691
+ this.setRuntimeConnectionStatus(CopilotKitCoreRuntimeConnectionStatus.Disconnected);
1405
1692
  this._runtimeVersion = void 0;
1406
1693
  this._audioFileTranscriptionEnabled = false;
1407
1694
  this._runtimeMode = _copilotkit_shared.RUNTIME_MODE_SSE;
@@ -1411,32 +1698,38 @@ var AgentRegistry = class {
1411
1698
  this._a2uiEnabled = false;
1412
1699
  this._a2uiAgents = void 0;
1413
1700
  this._openGenerativeUIEnabled = false;
1701
+ this._licenseStatus = void 0;
1702
+ this._runtimeEntitlements = void 0;
1414
1703
  this.remoteAgents = {};
1415
1704
  this._agents = this.localAgents;
1416
1705
  await this.notifyRuntimeStatusChanged(CopilotKitCoreRuntimeConnectionStatus.Disconnected);
1706
+ if (!this.isCurrentRuntimeConnection(attempt)) return;
1417
1707
  await this.notifyAgentsChanged();
1418
1708
  return;
1419
1709
  }
1420
1710
  const inspectorMetadataConnectionGeneration = this.inspectorMetadataConnectionGeneration;
1421
- this._runtimeConnectionStatus = CopilotKitCoreRuntimeConnectionStatus.Connecting;
1711
+ this.setRuntimeConnectionStatus(CopilotKitCoreRuntimeConnectionStatus.Connecting);
1712
+ const runtimeHealthGeneration = this.runtimeHealthGeneration;
1422
1713
  await this.notifyRuntimeStatusChanged(CopilotKitCoreRuntimeConnectionStatus.Connecting);
1714
+ if (!this.isCurrentRuntimeConnection(attempt)) return;
1423
1715
  try {
1424
1716
  if (inspectorMetadataConnectionGeneration !== this.inspectorMetadataConnectionGeneration) return;
1425
- const { runtimeInfo: runtimeInfoResponse, resolvedTransport } = await this.fetchRuntimeInfo();
1426
- if (inspectorMetadataConnectionGeneration !== this.inspectorMetadataConnectionGeneration) return;
1717
+ const { runtimeInfo: runtimeInfoResponse, resolvedTransport } = options?.recovery ? await this.fetchRuntimeInfoWithTimeout(new AbortController(), runtimeUrl, transport) : await this.fetchRuntimeInfo(runtimeUrl, transport);
1718
+ if (!this.isCurrentRuntimeConnection(attempt) || inspectorMetadataConnectionGeneration !== this.inspectorMetadataConnectionGeneration) return;
1427
1719
  this._runtimeTransport = resolvedTransport;
1428
1720
  const { version, ...runtimeInfo } = runtimeInfoResponse;
1429
1721
  const credentials = this.core.credentials;
1430
1722
  const rawDebug = this.core.debug;
1431
- this.remoteAgents = Object.fromEntries(Object.entries(runtimeInfo.agents).map(([id, { description, capabilities }]) => {
1723
+ const agents = Object.fromEntries(Object.entries(runtimeInfo.agents).map(([id, { description, capabilities }]) => {
1432
1724
  const existing = Object.prototype.hasOwnProperty.call(this.remoteAgents, id) ? this.remoteAgents[id] : void 0;
1433
- if (existing instanceof ProxiedCopilotRuntimeAgent) {
1725
+ if (existing instanceof ProxiedCopilotRuntimeAgent && this.canReuseRuntimeAgent(existing, runtimeUrl, this._runtimeTransport)) {
1434
1726
  this.applyHeadersToAgent(existing);
1435
1727
  this.applyCredentialsToAgent(existing);
1728
+ this.applyRuntimeFetchToAgent(existing);
1436
1729
  return [id, existing];
1437
1730
  }
1438
1731
  const agent = new ProxiedCopilotRuntimeAgent({
1439
- runtimeUrl: this.runtimeUrl,
1732
+ runtimeUrl,
1440
1733
  agentId: id,
1441
1734
  description,
1442
1735
  transport: this._runtimeTransport,
@@ -1447,13 +1740,25 @@ var AgentRegistry = class {
1447
1740
  debug: rawDebug ? (0, _copilotkit_shared.resolveDebugConfig)(rawDebug) : void 0
1448
1741
  });
1449
1742
  this.applyHeadersToAgent(agent);
1743
+ this.applyRuntimeFetchToAgent(agent);
1744
+ this.mintedThreadIds.set(agent, agent.threadId);
1745
+ if (options?.preserveOnFailure === true && existing) {
1746
+ agent.threadId = existing.threadId;
1747
+ agent.setState(existing.state);
1748
+ agent.setMessages([...existing.messages]);
1749
+ }
1750
+ this.remoteAgentConnections.set(agent, {
1751
+ runtimeUrl,
1752
+ transport: this._runtimeTransport
1753
+ });
1450
1754
  return [id, agent];
1451
1755
  }));
1756
+ this.remoteAgents = options?.recovery ? this.reconcileRecoveredAgents(agents) : agents;
1452
1757
  this._agents = {
1453
1758
  ...this.localAgents,
1454
1759
  ...this.remoteAgents
1455
1760
  };
1456
- this._runtimeConnectionStatus = CopilotKitCoreRuntimeConnectionStatus.Connected;
1761
+ this.setRuntimeConnectionStatus(CopilotKitCoreRuntimeConnectionStatus.Connected);
1457
1762
  this._runtimeVersion = version;
1458
1763
  this._audioFileTranscriptionEnabled = runtimeInfoResponse.audioFileTranscriptionEnabled ?? false;
1459
1764
  this._runtimeMode = runtimeInfoResponse.mode ?? _copilotkit_shared.RUNTIME_MODE_SSE;
@@ -1468,54 +1773,67 @@ var AgentRegistry = class {
1468
1773
  this._a2uiAgents = a2uiInfo?.enabled ? a2uiInfo.agents : void 0;
1469
1774
  this._openGenerativeUIEnabled = runtimeInfoResponse.openGenerativeUIEnabled ?? false;
1470
1775
  this._licenseStatus = runtimeInfoResponse.licenseStatus;
1776
+ this._runtimeEntitlements = runtimeInfoResponse.runtimeEntitlements;
1777
+ this.updateRuntimeEntitlementRetry(runtimeInfoResponse.runtimeEntitlements, attempt);
1471
1778
  this._telemetryDisabled = runtimeInfoResponse.telemetryDisabled ?? false;
1472
1779
  await this.notifyRuntimeStatusChanged(CopilotKitCoreRuntimeConnectionStatus.Connected);
1780
+ if (!this.isCurrentRuntimeConnection(attempt)) return;
1473
1781
  await this.notifyAgentsChanged();
1474
1782
  if (inspectorMetadataConnectionGeneration !== this.inspectorMetadataConnectionGeneration || this._runtimeConnectionStatus !== CopilotKitCoreRuntimeConnectionStatus.Connected) return;
1475
1783
  this.inspectorMetadataRefreshReady = true;
1476
1784
  if (this._inspectorMetadataSupported) this.refreshInspectorMetadata();
1477
1785
  } catch (error) {
1478
- if (inspectorMetadataConnectionGeneration !== this.inspectorMetadataConnectionGeneration) return;
1479
- this.invalidateInspectorMetadataConnection();
1480
- this._runtimeConnectionStatus = CopilotKitCoreRuntimeConnectionStatus.Error;
1481
- this._runtimeVersion = void 0;
1482
- this._audioFileTranscriptionEnabled = false;
1483
- this._runtimeMode = _copilotkit_shared.RUNTIME_MODE_SSE;
1484
- this._intelligence = void 0;
1485
- this._threadEndpoints = void 0;
1486
- this._suggestions = void 0;
1487
- this._a2uiEnabled = false;
1488
- this._a2uiAgents = void 0;
1489
- this._openGenerativeUIEnabled = false;
1490
- this.remoteAgents = {};
1491
- this._agents = this.localAgents;
1492
- await this.notifyRuntimeStatusChanged(CopilotKitCoreRuntimeConnectionStatus.Error);
1493
- await this.notifyAgentsChanged();
1786
+ if (!this.isCurrentRuntimeConnection(attempt) || inspectorMetadataConnectionGeneration !== this.inspectorMetadataConnectionGeneration) return;
1787
+ const boundedEntitlementRetryFailed = this._runtimeEntitlementRetryTimer === void 0 && this._runtimeEntitlementRetryAttemptedKey === key && this._runtimeEntitlementRetryPendingKey === key;
1788
+ if (boundedEntitlementRetryFailed) this._runtimeEntitlementRetryPendingKey = void 0;
1789
+ if (options?.preserveOnFailure) {
1790
+ if (options?.recovery && runtimeHealthGeneration !== this.runtimeHealthGeneration) return;
1791
+ this.setRuntimeConnectionStatus(CopilotKitCoreRuntimeConnectionStatus.Error);
1792
+ await this.notifyRuntimeStatusChanged(CopilotKitCoreRuntimeConnectionStatus.Error);
1793
+ } else {
1794
+ this.invalidateInspectorMetadataConnection();
1795
+ this.setRuntimeConnectionStatus(CopilotKitCoreRuntimeConnectionStatus.Error);
1796
+ this._runtimeVersion = void 0;
1797
+ this._audioFileTranscriptionEnabled = false;
1798
+ this._runtimeMode = _copilotkit_shared.RUNTIME_MODE_SSE;
1799
+ this._intelligence = void 0;
1800
+ this._threadEndpoints = void 0;
1801
+ this._suggestions = void 0;
1802
+ this._a2uiEnabled = false;
1803
+ this._a2uiAgents = void 0;
1804
+ this._openGenerativeUIEnabled = false;
1805
+ if (!boundedEntitlementRetryFailed) {
1806
+ this._licenseStatus = void 0;
1807
+ this._runtimeEntitlements = void 0;
1808
+ }
1809
+ this.remoteAgents = {};
1810
+ this._agents = this.localAgents;
1811
+ await this.notifyRuntimeStatusChanged(CopilotKitCoreRuntimeConnectionStatus.Error);
1812
+ await this.notifyAgentsChanged();
1813
+ }
1494
1814
  const message = error instanceof Error ? error.message : JSON.stringify(error);
1495
- _copilotkit_shared.logger.warn(`Failed to load runtime info (${this.runtimeUrl}/info): ${message}`);
1815
+ _copilotkit_shared.logger.warn(`Failed to load runtime info (${runtimeUrl}/info): ${message}`);
1496
1816
  const runtimeError = error instanceof Error ? error : new Error(String(error));
1497
1817
  await this.core.emitError({
1498
1818
  error: runtimeError,
1499
1819
  code: CopilotKitCoreErrorCode.RUNTIME_INFO_FETCH_FAILED,
1500
- context: { runtimeUrl: this.runtimeUrl }
1820
+ context: { runtimeUrl }
1501
1821
  });
1502
1822
  }
1503
1823
  }
1504
- async fetchRuntimeInfo() {
1505
- const runtimeUrl = this.runtimeUrl;
1506
- if (!runtimeUrl) throw new Error("Runtime URL is not set");
1824
+ async fetchRuntimeInfo(runtimeUrl, runtimeTransport, signal) {
1507
1825
  const baseHeaders = this.core.headers;
1508
1826
  const credentials = this.core.credentials;
1509
1827
  const headers = { ...baseHeaders };
1510
- const runtimeTransport = this._runtimeTransport;
1511
1828
  if (runtimeTransport === "single") return {
1512
- runtimeInfo: await this.fetchRuntimeInfoSingle(runtimeUrl, headers, credentials),
1829
+ runtimeInfo: await this.fetchRuntimeInfoSingle(runtimeUrl, headers, credentials, signal),
1513
1830
  resolvedTransport: "single"
1514
1831
  };
1515
- if (runtimeTransport === "auto") return this.fetchRuntimeInfoAutoDetect(runtimeUrl, headers, credentials);
1832
+ if (runtimeTransport === "auto") return this.fetchRuntimeInfoAutoDetect(runtimeUrl, headers, credentials, signal);
1516
1833
  const response = await fetch(`${runtimeUrl}/info`, {
1517
1834
  headers,
1518
- ...credentials ? { credentials } : {}
1835
+ ...credentials ? { credentials } : {},
1836
+ ...signal ? { signal } : {}
1519
1837
  });
1520
1838
  if (!response.ok) throw await runtimeInfoError(response);
1521
1839
  return {
@@ -1523,12 +1841,13 @@ var AgentRegistry = class {
1523
1841
  resolvedTransport: "rest"
1524
1842
  };
1525
1843
  }
1526
- async fetchRuntimeInfoSingle(runtimeUrl, headers, credentials) {
1844
+ async fetchRuntimeInfoSingle(runtimeUrl, headers, credentials, signal) {
1527
1845
  const response = await fetch(runtimeUrl, {
1528
1846
  method: "POST",
1529
1847
  headers: withJsonContentType(headers),
1530
1848
  body: JSON.stringify({ method: "info" }),
1531
- ...credentials ? { credentials } : {}
1849
+ ...credentials ? { credentials } : {},
1850
+ ...signal ? { signal } : {}
1532
1851
  });
1533
1852
  if (!response.ok) throw await runtimeInfoError(response);
1534
1853
  return await response.json();
@@ -1538,11 +1857,12 @@ var AgentRegistry = class {
1538
1857
  * The caller commits the detected transport only after confirming this
1539
1858
  * connection attempt is still current.
1540
1859
  */
1541
- async fetchRuntimeInfoAutoDetect(runtimeUrl, headers, credentials) {
1860
+ async fetchRuntimeInfoAutoDetect(runtimeUrl, headers, credentials, signal) {
1542
1861
  try {
1543
1862
  const response = await fetch(`${runtimeUrl}/info`, {
1544
1863
  headers: { ...headers },
1545
- ...credentials ? { credentials } : {}
1864
+ ...credentials ? { credentials } : {},
1865
+ ...signal ? { signal } : {}
1546
1866
  });
1547
1867
  if (response.status >= 200 && response.status < 300) return {
1548
1868
  runtimeInfo: await response.json(),
@@ -1550,7 +1870,7 @@ var AgentRegistry = class {
1550
1870
  };
1551
1871
  } catch {}
1552
1872
  return {
1553
- runtimeInfo: await this.fetchRuntimeInfoSingle(runtimeUrl, { ...headers }, credentials),
1873
+ runtimeInfo: await this.fetchRuntimeInfoSingle(runtimeUrl, { ...headers }, credentials, signal),
1554
1874
  resolvedTransport: "single"
1555
1875
  };
1556
1876
  }
@@ -2023,6 +2343,137 @@ function isForwardedToClientPlaceholder(content) {
2023
2343
  return normalizeToolResultContent(content) === FORWARDED_TO_CLIENT;
2024
2344
  }
2025
2345
 
2346
+ //#endregion
2347
+ //#region src/core/tool-schema.ts
2348
+ /**
2349
+ * Empty tool schema constant
2350
+ */
2351
+ const EMPTY_TOOL_SCHEMA = {
2352
+ type: "object",
2353
+ properties: {}
2354
+ };
2355
+ /**
2356
+ * Create a JSON schema from a tool's parameters
2357
+ */
2358
+ function createToolSchema(tool) {
2359
+ if (!tool.parameters) return { ...EMPTY_TOOL_SCHEMA };
2360
+ const rawSchema = (0, _copilotkit_shared.schemaToJsonSchema)(tool.parameters, { zodToJsonSchema: (schema, options) => (0, zod_to_json_schema.zodToJsonSchema)(schema, options) });
2361
+ if (!rawSchema || typeof rawSchema !== "object") return { ...EMPTY_TOOL_SCHEMA };
2362
+ const { $schema: _$schema, ...schema } = rawSchema;
2363
+ if (typeof schema.type !== "string") schema.type = "object";
2364
+ if (typeof schema.properties !== "object" || schema.properties === null) schema.properties = {};
2365
+ stripAdditionalProperties(schema);
2366
+ return schema;
2367
+ }
2368
+ /**
2369
+ * Remove every `additionalProperties` key from a JSON schema tree, in place.
2370
+ * Some LLM providers reject schemas that carry it.
2371
+ */
2372
+ function stripAdditionalProperties(schema) {
2373
+ if (!schema || typeof schema !== "object") return;
2374
+ if (Array.isArray(schema)) {
2375
+ schema.forEach(stripAdditionalProperties);
2376
+ return;
2377
+ }
2378
+ const record = schema;
2379
+ if (record.additionalProperties !== void 0) delete record.additionalProperties;
2380
+ for (const value of Object.values(record)) stripAdditionalProperties(value);
2381
+ }
2382
+
2383
+ //#endregion
2384
+ //#region src/core/webmcp.ts
2385
+ /**
2386
+ * Return the page's `document.modelContext`, or null when WebMCP is not
2387
+ * available (SSR, React Native, browsers without the API enabled).
2388
+ */
2389
+ function getWebMCPModelContext() {
2390
+ if (typeof document === "undefined") return null;
2391
+ return document.modelContext ?? null;
2392
+ }
2393
+ /**
2394
+ * Keeps a set of frontend tools registered on the page's WebMCP model context.
2395
+ *
2396
+ * `sync(desired)` reconciles the registrations with the desired set: tools that
2397
+ * are gone (or were re-registered with a new tool object) are unregistered by
2398
+ * aborting their registration signal, and new tools are registered with an
2399
+ * `execute` that delegates to the frontend tool's own handler.
2400
+ *
2401
+ * All WebMCP-API constraints live here so callers only supply the desired set:
2402
+ * a tool without a description is skipped with a warning (the spec rejects
2403
+ * empty descriptions), and failures from `registerTool` (duplicate name,
2404
+ * unsupported context, ...) are warned rather than thrown.
2405
+ */
2406
+ var WebMCPRegistry = class {
2407
+ entries = /* @__PURE__ */ new Map();
2408
+ warnedNames = /* @__PURE__ */ new Set();
2409
+ /** Names currently kept registered on the model context. */
2410
+ get registeredNames() {
2411
+ return [...this.entries.keys()];
2412
+ }
2413
+ /** Reconcile the registered tools with the desired set. */
2414
+ sync(desired) {
2415
+ const modelContext = getWebMCPModelContext();
2416
+ if (!modelContext) return;
2417
+ for (const [name, entry] of [...this.entries]) if (desired.get(name) !== entry.tool) {
2418
+ entry.controller.abort();
2419
+ this.entries.delete(name);
2420
+ }
2421
+ for (const [name, tool] of desired) {
2422
+ if (this.entries.has(name)) continue;
2423
+ if (!tool.description) {
2424
+ this.warnOnce(name, `Skipping WebMCP registration for tool '${name}': WebMCP requires a description.`);
2425
+ continue;
2426
+ }
2427
+ const controller = new AbortController();
2428
+ this.entries.set(name, {
2429
+ tool,
2430
+ controller
2431
+ });
2432
+ modelContext.registerTool(this.buildModelContextTool(tool), { signal: controller.signal }).catch((error) => {
2433
+ if (this.entries.get(name)?.controller !== controller) return;
2434
+ this.entries.delete(name);
2435
+ const message = error instanceof Error ? error.message : String(error);
2436
+ this.warnOnce(name, `WebMCP registration failed for tool '${name}': ${message}`);
2437
+ });
2438
+ }
2439
+ }
2440
+ /**
2441
+ * Build the WebMCP model-context tool for a frontend tool. The `execute`
2442
+ * callback delegates to the frontend tool's own handler, so a browser agent
2443
+ * call runs the same application code as an agent call.
2444
+ */
2445
+ buildModelContextTool(tool) {
2446
+ const annotations = typeof tool.webmcp === "object" ? tool.webmcp.annotations : void 0;
2447
+ return {
2448
+ name: tool.name,
2449
+ description: tool.description,
2450
+ inputSchema: createToolSchema(tool),
2451
+ execute: async (args, options) => {
2452
+ if (!tool.handler) return "";
2453
+ const toolCall = {
2454
+ id: (0, _copilotkit_shared.randomUUID)(),
2455
+ type: "function",
2456
+ function: {
2457
+ name: tool.name,
2458
+ arguments: JSON.stringify(args ?? {})
2459
+ }
2460
+ };
2461
+ return await tool.handler(args, {
2462
+ toolCall,
2463
+ signal: options?.signal
2464
+ });
2465
+ },
2466
+ ...annotations ? { annotations } : {}
2467
+ };
2468
+ }
2469
+ /** Log `message` once per tool name. Repeated syncs must not spam the log. */
2470
+ warnOnce(name, message) {
2471
+ if (this.warnedNames.has(name)) return;
2472
+ this.warnedNames.add(name);
2473
+ _copilotkit_shared.logger.warn(`[CopilotKit] ${message}`);
2474
+ }
2475
+ };
2476
+
2026
2477
  //#endregion
2027
2478
  //#region src/core/run-handler.ts
2028
2479
  /**
@@ -2039,11 +2490,40 @@ function isForwardedToClientPlaceholder(content) {
2039
2490
  */
2040
2491
  const MAX_FOLLOW_UP_DEPTH = 100;
2041
2492
  /**
2493
+ * Name of the catch-all frontend tool. A tool registered under this name
2494
+ * handles any tool call that has no exact match (see `executeWildcardTool`),
2495
+ * and is never advertised to the agent.
2496
+ */
2497
+ const WILDCARD_TOOL_NAME = "*";
2498
+ /**
2042
2499
  * Handles agent execution, tool calling, and agent connectivity for CopilotKitCore.
2043
2500
  * Manages the complete lifecycle of agent runs including tool execution and follow-ups.
2044
2501
  */
2045
2502
  var RunHandler = class {
2046
- _tools = [];
2503
+ /**
2504
+ * Tools owned by the framework provider, replaced wholesale by
2505
+ * {@link initialize} and {@link setTools} whenever the provider re-syncs its
2506
+ * props or runtime feature flags.
2507
+ */
2508
+ _propTools = [];
2509
+ /**
2510
+ * Tools registered imperatively via {@link addTool} — `useFrontendTool`,
2511
+ * `useHumanInTheLoop`, and direct `core.addTool()` callers. Held separately
2512
+ * from {@link _propTools} so a provider re-sync cannot wipe them: they shared
2513
+ * one array before, so the `/info` response flipping `openGenerativeUI` (or
2514
+ * any other provider-prop change) silently dropped every hook-registered
2515
+ * tool (#4952). Key = `capabilityKey(name, agentId)`.
2516
+ */
2517
+ _hookTools = /* @__PURE__ */ new Map();
2518
+ /**
2519
+ * Memoized merge of both buckets, invalidated on every registry write.
2520
+ *
2521
+ * This is about identity, not speed: `tools` is public and used to return the
2522
+ * same array instance until something mutated the registry, so consumers can
2523
+ * hold it or use it as an effect dependency. Rebuilding the merge on every
2524
+ * read would hand out a new array each time.
2525
+ */
2526
+ _cachedMergedTools = null;
2047
2527
  /**
2048
2528
  * The full list of A2UI catalog components registered by the provider.
2049
2529
  * Order is preserved for display in the inspector.
@@ -2076,6 +2556,12 @@ var RunHandler = class {
2076
2556
  */
2077
2557
  _runDepth = 0;
2078
2558
  /**
2559
+ * Keeps tools that opt in via `webmcp` registered on the page's WebMCP model
2560
+ * context (`document.modelContext`), in addition to the normal agent
2561
+ * registration. Reconciled on every tool registry mutation.
2562
+ */
2563
+ _webmcpRegistry = new WebMCPRegistry();
2564
+ /**
2079
2565
  * Tracks the threadId of the most recent `connectAgent` call so we
2080
2566
  * can distinguish a fresh thread restore (different threadId than
2081
2567
  * last time — chat is rebuilding state from scratch, must clear
@@ -2127,35 +2613,52 @@ var RunHandler = class {
2127
2613
  return anonymousId;
2128
2614
  }
2129
2615
  /**
2130
- * Get all tools as a readonly array
2616
+ * Get all tools as a readonly array: provider-owned tools in registration
2617
+ * order, with imperatively-added tools appended and taking precedence over a
2618
+ * provider tool of the same name + agentId.
2131
2619
  */
2132
2620
  get tools() {
2133
- return this._tools;
2621
+ if (this._hookTools.size === 0) return this._propTools;
2622
+ if (this._cachedMergedTools) return this._cachedMergedTools;
2623
+ const merged = /* @__PURE__ */ new Map();
2624
+ for (const tool of this._propTools) merged.set(this.capabilityKey(tool.name, tool.agentId), tool);
2625
+ for (const [key, tool] of this._hookTools) merged.set(key, tool);
2626
+ this._cachedMergedTools = Array.from(merged.values());
2627
+ return this._cachedMergedTools;
2134
2628
  }
2135
2629
  /**
2136
2630
  * Initialize with tools
2137
2631
  */
2138
2632
  initialize(tools) {
2139
- this._tools = tools;
2633
+ this._propTools = [...tools];
2634
+ this._cachedMergedTools = null;
2635
+ this.syncWebMCP();
2140
2636
  }
2141
2637
  /**
2142
- * Add a tool to the registry
2638
+ * Add a tool to the registry. Survives provider re-syncs ({@link setTools}),
2639
+ * and shadows a provider tool of the same name + agentId.
2143
2640
  */
2144
2641
  addTool(tool) {
2145
- if (this._tools.findIndex((t) => t.name === tool.name && t.agentId === tool.agentId) !== -1) {
2642
+ const key = this.capabilityKey(tool.name, tool.agentId);
2643
+ if (this._hookTools.has(key)) {
2146
2644
  _copilotkit_shared.logger.warn(`Tool already exists: '${tool.name}' for agent '${tool.agentId || "global"}', skipping.`);
2147
2645
  return;
2148
2646
  }
2149
- this._tools.push(tool);
2647
+ this._hookTools.set(key, tool);
2648
+ this._cachedMergedTools = null;
2649
+ this.syncWebMCP();
2150
2650
  }
2151
2651
  /**
2152
2652
  * Remove a tool by name and optionally by agentId
2153
2653
  */
2154
2654
  removeTool(id, agentId) {
2155
- this._tools = this._tools.filter((tool) => {
2655
+ this._hookTools.delete(this.capabilityKey(id, agentId));
2656
+ this._propTools = this._propTools.filter((tool) => {
2156
2657
  if (agentId !== void 0) return !(tool.name === id && tool.agentId === agentId);
2157
2658
  return !(tool.name === id && !tool.agentId);
2158
2659
  });
2660
+ this._cachedMergedTools = null;
2661
+ this.syncWebMCP();
2159
2662
  }
2160
2663
  /**
2161
2664
  * Get a tool by name and optionally by agentId.
@@ -2164,17 +2667,21 @@ var RunHandler = class {
2164
2667
  */
2165
2668
  getTool(params) {
2166
2669
  const { toolName, agentId } = params;
2670
+ const tools = this.tools;
2167
2671
  if (agentId) {
2168
- const agentTool = this._tools.find((tool) => tool.name === toolName && tool.agentId === agentId);
2672
+ const agentTool = tools.find((tool) => tool.name === toolName && tool.agentId === agentId);
2169
2673
  if (agentTool) return agentTool;
2170
2674
  }
2171
- return this._tools.find((tool) => tool.name === toolName && !tool.agentId);
2675
+ return tools.find((tool) => tool.name === toolName && !tool.agentId);
2172
2676
  }
2173
2677
  /**
2174
- * Set all tools at once. Replaces existing tools.
2678
+ * Set all provider-owned tools at once. Replaces the previous provider set;
2679
+ * tools registered via {@link addTool} are unaffected.
2175
2680
  */
2176
2681
  setTools(tools) {
2177
- this._tools = [...tools];
2682
+ this._propTools = [...tools];
2683
+ this._cachedMergedTools = null;
2684
+ this.syncWebMCP();
2178
2685
  }
2179
2686
  /**
2180
2687
  * Return the registered A2UI catalog components (readonly).
@@ -2238,7 +2745,7 @@ var RunHandler = class {
2238
2745
  });
2239
2746
  } catch (error) {
2240
2747
  const connectError = error instanceof Error ? error : new Error(String(error));
2241
- if (!(connectError.name === "AbortError" || connectError.message === "Fetch is aborted" || connectError.message === "signal is aborted without reason" || connectError.message === "component unmounted")) {
2748
+ if (!isAbortError$1(connectError)) {
2242
2749
  const context = {};
2243
2750
  if (agent.agentId) context.agentId = agent.agentId;
2244
2751
  if (incomingThreadId) context.threadId = incomingThreadId;
@@ -2351,7 +2858,7 @@ var RunHandler = class {
2351
2858
  const getWildcardTool = () => {
2352
2859
  if (tool || wildcardTool) return wildcardTool;
2353
2860
  wildcardTool = this.getTool({
2354
- toolName: "*",
2861
+ toolName: WILDCARD_TOOL_NAME,
2355
2862
  agentId: agent.agentId
2356
2863
  });
2357
2864
  return wildcardTool;
@@ -2696,6 +3203,7 @@ var RunHandler = class {
2696
3203
  const key = this.capabilityKey(name, agentId);
2697
3204
  if (enabled) this._disabledToolKeys.delete(key);
2698
3205
  else this._disabledToolKeys.add(key);
3206
+ this.syncWebMCP();
2699
3207
  }
2700
3208
  /** Whether a tool is currently enabled (not overridden off). Defaults true. */
2701
3209
  isToolEnabled(name, agentId) {
@@ -2705,13 +3213,39 @@ var RunHandler = class {
2705
3213
  * Build frontend tools for an agent
2706
3214
  */
2707
3215
  buildFrontendTools(agentId) {
2708
- return this._tools.filter((tool) => tool.available !== false && tool.available !== "disabled" && (!tool.agentId || tool.agentId === agentId) && this.isToolEnabled(tool.name, tool.agentId)).map((tool) => ({
3216
+ return this.tools.filter((tool) => tool.name !== WILDCARD_TOOL_NAME && tool.available !== false && tool.available !== "disabled" && (!tool.agentId || tool.agentId === agentId) && this.isToolEnabled(tool.name, tool.agentId)).map((tool) => ({
2709
3217
  name: tool.name,
2710
3218
  description: tool.description ?? "",
2711
3219
  parameters: createToolSchema(tool)
2712
3220
  }));
2713
3221
  }
2714
3222
  /**
3223
+ * Reconcile the WebMCP registrations with the current tool registry. Tools
3224
+ * opt in via `webmcp: true` or `webmcp: { annotations }`; the same
3225
+ * availability rules as `buildFrontendTools` apply. WebMCP tools are
3226
+ * page-level, so unlike the agent tool list they are not filtered by
3227
+ * agentId — a name collision across agentIds keeps the first registration.
3228
+ */
3229
+ syncWebMCP() {
3230
+ const desired = /* @__PURE__ */ new Map();
3231
+ const warnedCollisions = /* @__PURE__ */ new Set();
3232
+ for (const tool of this.tools) {
3233
+ if (!tool.webmcp) continue;
3234
+ if (tool.name === WILDCARD_TOOL_NAME) continue;
3235
+ if (tool.available === false || tool.available === "disabled") continue;
3236
+ if (!this.isToolEnabled(tool.name, tool.agentId)) continue;
3237
+ if (desired.has(tool.name)) {
3238
+ if (!warnedCollisions.has(tool.name)) {
3239
+ warnedCollisions.add(tool.name);
3240
+ _copilotkit_shared.logger.warn(`[CopilotKit] Multiple WebMCP tools share the name '${tool.name}'. Only the first registration is exposed to browser agents.`);
3241
+ }
3242
+ continue;
3243
+ }
3244
+ desired.set(tool.name, tool);
3245
+ }
3246
+ this._webmcpRegistry.sync(desired);
3247
+ }
3248
+ /**
2715
3249
  * Create an agent error subscriber
2716
3250
  */
2717
3251
  createAgentErrorSubscriber(agent) {
@@ -2744,36 +3278,6 @@ var RunHandler = class {
2744
3278
  }
2745
3279
  };
2746
3280
  /**
2747
- * Empty tool schema constant
2748
- */
2749
- const EMPTY_TOOL_SCHEMA = {
2750
- type: "object",
2751
- properties: {}
2752
- };
2753
- /**
2754
- * Create a JSON schema from a tool's parameters
2755
- */
2756
- function createToolSchema(tool) {
2757
- if (!tool.parameters) return { ...EMPTY_TOOL_SCHEMA };
2758
- const rawSchema = (0, _copilotkit_shared.schemaToJsonSchema)(tool.parameters, { zodToJsonSchema: (schema, options) => (0, zod_to_json_schema.zodToJsonSchema)(schema, options) });
2759
- if (!rawSchema || typeof rawSchema !== "object") return { ...EMPTY_TOOL_SCHEMA };
2760
- const { $schema: _$schema, ...schema } = rawSchema;
2761
- if (typeof schema.type !== "string") schema.type = "object";
2762
- if (typeof schema.properties !== "object" || schema.properties === null) schema.properties = {};
2763
- stripAdditionalProperties(schema);
2764
- return schema;
2765
- }
2766
- function stripAdditionalProperties(schema) {
2767
- if (!schema || typeof schema !== "object") return;
2768
- if (Array.isArray(schema)) {
2769
- schema.forEach(stripAdditionalProperties);
2770
- return;
2771
- }
2772
- const record = schema;
2773
- if (record.additionalProperties !== void 0) delete record.additionalProperties;
2774
- for (const value of Object.values(record)) stripAdditionalProperties(value);
2775
- }
2776
- /**
2777
3281
  * Ensures parsed tool arguments are a plain object.
2778
3282
  * Throws for non-object values so the caller's catch block can emit
2779
3283
  * a structured TOOL_ARGUMENT_PARSE_FAILED error.
@@ -2853,9 +3357,10 @@ var StateManager = class {
2853
3357
  subscribeToAgent(agent) {
2854
3358
  if (!agent.agentId) return;
2855
3359
  const agentId = agent.agentId;
2856
- const existingUnsubscribe = this.agentSubscriptions.get(agentId);
2857
- if (existingUnsubscribe) {
2858
- existingUnsubscribe();
3360
+ const existing = this.agentSubscriptions.get(agentId);
3361
+ if (existing) {
3362
+ if (existing.agent === agent) return;
3363
+ existing.unsubscribe();
2859
3364
  this.agentSubscriptions.delete(agentId);
2860
3365
  }
2861
3366
  let revoked = false;
@@ -2991,19 +3496,22 @@ var StateManager = class {
2991
3496
  if (!input) this.pruneRawEvents(agent.agentId, agent.threadId, messages);
2992
3497
  }
2993
3498
  });
2994
- this.agentSubscriptions.set(agentId, () => {
2995
- revoked = true;
2996
- this.pendingContinuations.delete(agent);
2997
- unsubscribe();
3499
+ this.agentSubscriptions.set(agentId, {
3500
+ agent,
3501
+ unsubscribe: () => {
3502
+ revoked = true;
3503
+ this.pendingContinuations.delete(agent);
3504
+ unsubscribe();
3505
+ }
2998
3506
  });
2999
3507
  }
3000
3508
  /**
3001
3509
  * Unsubscribe an agent's subscription.
3002
3510
  */
3003
3511
  unsubscribeFromAgent(agentId) {
3004
- const unsubscribe = this.agentSubscriptions.get(agentId);
3005
- if (unsubscribe) {
3006
- unsubscribe();
3512
+ const existing = this.agentSubscriptions.get(agentId);
3513
+ if (existing) {
3514
+ existing.unsubscribe();
3007
3515
  this.agentSubscriptions.delete(agentId);
3008
3516
  }
3009
3517
  this.rawEventByMessage.delete(agentId);
@@ -4533,6 +5041,9 @@ var CopilotKitCore = class {
4533
5041
  get runtimeConnectionStatus() {
4534
5042
  return this.agentRegistry.runtimeConnectionStatus;
4535
5043
  }
5044
+ get ɵruntimeFetch() {
5045
+ return this.agentRegistry.createRuntimeFetch();
5046
+ }
4536
5047
  get audioFileTranscriptionEnabled() {
4537
5048
  return this.agentRegistry.audioFileTranscriptionEnabled;
4538
5049
  }
@@ -4572,6 +5083,14 @@ var CopilotKitCore = class {
4572
5083
  get licenseStatus() {
4573
5084
  return this.agentRegistry.licenseStatus;
4574
5085
  }
5086
+ /** Structured Runtime entitlement authority advertised by `/info`. */
5087
+ get runtimeEntitlements() {
5088
+ return this.agentRegistry.runtimeEntitlements;
5089
+ }
5090
+ /** Whether Core still has a bounded Runtime entitlement retry to settle. */
5091
+ get runtimeEntitlementRetryPending() {
5092
+ return this.agentRegistry.runtimeEntitlementRetryPending;
5093
+ }
4575
5094
  get telemetryDisabled() {
4576
5095
  return this.agentRegistry.telemetryDisabled;
4577
5096
  }
@@ -5544,22 +6063,34 @@ function createThreadRequestId() {
5544
6063
  }
5545
6064
  function threadFromFetch(input, init) {
5546
6065
  return new rxjs.Observable((subscriber) => {
5547
- const { fetch: fetchImpl, selector, signal, ...requestInit } = init;
6066
+ const { fetch: fetchImpl, selector, signal, timeoutMs, nonCritical, ...requestInit } = init;
5548
6067
  const controller = new AbortController();
6068
+ const runtimeRequest = {
6069
+ ...nonCritical ? { nonCritical: true } : {},
6070
+ ...timeoutMs === void 0 ? {} : { selfBounded: true }
6071
+ };
6072
+ let timedOut = false;
5549
6073
  const abortRequest = () => controller.abort();
5550
6074
  if (signal?.aborted) abortRequest();
5551
6075
  else signal?.addEventListener("abort", abortRequest, { once: true });
6076
+ const timeoutId = timeoutMs === void 0 ? void 0 : setTimeout(() => {
6077
+ timedOut = true;
6078
+ runtimeRequest.timedOut = true;
6079
+ abortRequest();
6080
+ }, timeoutMs);
5552
6081
  fetchImpl(input, {
5553
6082
  ...requestInit,
5554
- signal: controller.signal
6083
+ signal: controller.signal,
6084
+ ɵruntimeRequest: runtimeRequest
5555
6085
  }).then((response) => selector(response)).then((value) => {
5556
6086
  if (subscriber.closed) return;
5557
6087
  subscriber.next(value);
5558
6088
  subscriber.complete();
5559
6089
  }).catch((error) => {
5560
- if (!subscriber.closed) subscriber.error(error);
6090
+ if (!subscriber.closed) subscriber.error(timedOut ? /* @__PURE__ */ new Error("Request timed out") : error);
5561
6091
  });
5562
6092
  return () => {
6093
+ if (timeoutId !== void 0) clearTimeout(timeoutId);
5563
6094
  signal?.removeEventListener("abort", abortRequest);
5564
6095
  abortRequest();
5565
6096
  };
@@ -5577,14 +6108,10 @@ function createThreadFetchObservable(environment, context, sessionId) {
5577
6108
  return response.json();
5578
6109
  },
5579
6110
  fetch: environment.fetch,
6111
+ timeoutMs: REQUEST_TIMEOUT_MS,
5580
6112
  method: "GET",
5581
6113
  headers: { ...context.headers }
5582
- }).pipe((0, rxjs_operators.timeout)({
5583
- first: REQUEST_TIMEOUT_MS,
5584
- with: () => {
5585
- throw new Error("Request timed out");
5586
- }
5587
- }), (0, rxjs_operators.map)((data) => threadRestEvents.listSucceeded({
6114
+ }).pipe((0, rxjs_operators.map)((data) => threadRestEvents.listSucceeded({
5588
6115
  sessionId,
5589
6116
  threads: data.threads,
5590
6117
  joinCode: typeof data.joinCode === "string" && data.joinCode.length > 0 ? data.joinCode : null,
@@ -5605,18 +6132,15 @@ function createThreadMetadataCredentialsObservable(environment, context, session
5605
6132
  return response.json();
5606
6133
  },
5607
6134
  fetch: environment.fetch,
6135
+ timeoutMs: REQUEST_TIMEOUT_MS,
6136
+ nonCritical: true,
5608
6137
  method: "POST",
5609
6138
  headers: {
5610
6139
  ...context.headers,
5611
6140
  "Content-Type": "application/json"
5612
6141
  },
5613
6142
  body: JSON.stringify({})
5614
- }).pipe((0, rxjs_operators.timeout)({
5615
- first: REQUEST_TIMEOUT_MS,
5616
- with: () => {
5617
- throw new Error("Request timed out");
5618
- }
5619
- }), (0, rxjs_operators.map)((data) => {
6143
+ }).pipe((0, rxjs_operators.map)((data) => {
5620
6144
  if (typeof data.joinToken !== "string" || data.joinToken.length === 0) throw new Error("missing joinToken");
5621
6145
  return threadRestEvents.metadataCredentialsSucceeded({
5622
6146
  sessionId,
@@ -5747,14 +6271,10 @@ function createThreadStore(environment) {
5747
6271
  return response.json();
5748
6272
  },
5749
6273
  fetch: environment.fetch,
6274
+ timeoutMs: REQUEST_TIMEOUT_MS,
5750
6275
  method: "GET",
5751
6276
  headers: { ...context.headers }
5752
- }).pipe((0, rxjs_operators.timeout)({
5753
- first: REQUEST_TIMEOUT_MS,
5754
- with: () => {
5755
- throw new Error("Request timed out");
5756
- }
5757
- }), (0, rxjs_operators.map)((data) => threadRestEvents.nextPageSucceeded({
6277
+ }).pipe((0, rxjs_operators.map)((data) => threadRestEvents.nextPageSucceeded({
5758
6278
  sessionId: state.sessionId,
5759
6279
  threads: data.threads,
5760
6280
  nextCursor: data.nextCursor ?? null
@@ -5918,6 +6438,12 @@ const ɵselectIsMutating = selectIsMutating;
5918
6438
  * hardcoding the threshold separately from production.
5919
6439
  */
5920
6440
  const ɵMAX_SOCKET_RETRIES = MAX_SOCKET_RETRIES;
6441
+ /**
6442
+ * How long a thread REST request is given before the store gives up on it.
6443
+ * Exposed for tests for the same reason as {@link ɵMAX_SOCKET_RETRIES}: a
6444
+ * separately hardcoded copy silently drifts away from this one.
6445
+ */
6446
+ const ɵTHREAD_REQUEST_TIMEOUT_MS = REQUEST_TIMEOUT_MS;
5921
6447
 
5922
6448
  //#endregion
5923
6449
  //#region src/features.ts
@@ -6180,6 +6706,7 @@ exports.RunHandler = RunHandler;
6180
6706
  exports.StateManager = StateManager;
6181
6707
  exports.SuggestionEngine = SuggestionEngine;
6182
6708
  exports.ToolCallStatus = ToolCallStatus;
6709
+ exports.WebMCPRegistry = WebMCPRegistry;
6183
6710
  exports.completePartialMarkdown = completePartialMarkdown;
6184
6711
  exports.createActionGroup = createActionGroup;
6185
6712
  exports.createEffect = createEffect;
@@ -6193,6 +6720,7 @@ exports.emitInspectorViewThread = emitInspectorViewThread;
6193
6720
  exports.emitInspectorViewThreadResult = emitInspectorViewThreadResult;
6194
6721
  exports.empty = empty;
6195
6722
  exports.ensureObjectArgs = ensureObjectArgs;
6723
+ exports.getWebMCPModelContext = getWebMCPModelContext;
6196
6724
  exports.isAbortError = isAbortError;
6197
6725
  exports.isInspectorThreadBridgeEnabled = isInspectorThreadBridgeEnabled;
6198
6726
  exports.isRunCompletionAware = isRunCompletionAware;
@@ -6208,6 +6736,8 @@ exports.select = select;
6208
6736
  exports.ɵCOPILOTKIT_FEATURES = ɵCOPILOTKIT_FEATURES;
6209
6737
  exports.ɵInterruptState = ɵInterruptState;
6210
6738
  exports.ɵMAX_SOCKET_RETRIES = ɵMAX_SOCKET_RETRIES;
6739
+ exports.ɵRUNTIME_PROBE_TIMEOUT_MS = ɵRUNTIME_PROBE_TIMEOUT_MS;
6740
+ exports.ɵTHREAD_REQUEST_TIMEOUT_MS = ɵTHREAD_REQUEST_TIMEOUT_MS;
6211
6741
  exports.ɵcreateMemoryStore = createMemoryStore;
6212
6742
  exports.ɵcreateThreadSelectors = ɵcreateThreadSelectors;
6213
6743
  exports.ɵcreateThreadStore = createThreadStore;