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