@copilotkit/core 1.73.0 → 1.73.2-canary.1790068297

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
@@ -379,16 +379,24 @@ var IntelligenceAgent = class IntelligenceAgent extends AbstractAgent {
379
379
  requestJoinCredentials$(mode, input, replayCursor) {
380
380
  return defer(async () => {
381
381
  try {
382
- const response = await (this.config.fetch ?? globalFetch)(this.buildRuntimeUrl(mode), {
382
+ const requestFetch = this.config.fetch ?? globalFetch;
383
+ const body = {
384
+ ...input,
385
+ ...mode === "connect" ? { lastSeenEventId: replayCursor === void 0 ? this.getReconnectCursor(input) : replayCursor } : {}
386
+ };
387
+ const single = this.config.transport === "single";
388
+ const response = await requestFetch(single ? this.config.runtimeUrl : this.buildRuntimeUrl(mode), {
383
389
  method: "POST",
390
+ redirect: "error",
384
391
  headers: {
385
392
  "Content-Type": "application/json",
386
393
  ...this.headers
387
394
  },
388
- body: JSON.stringify({
389
- ...input,
390
- ...mode === "connect" ? { lastSeenEventId: replayCursor === void 0 ? this.getReconnectCursor(input) : replayCursor } : {}
391
- }),
395
+ body: JSON.stringify(single ? {
396
+ method: `agent/${mode}`,
397
+ params: { agentId: this.config.agentId },
398
+ body
399
+ } : body),
392
400
  ...this.credentials ? { credentials: this.credentials } : {}
393
401
  });
394
402
  if (response.status === 204 && mode === "connect") return null;
@@ -622,6 +630,90 @@ function isRuntimeInfoRequestError(error) {
622
630
  return typeof error === "object" && error !== null && typeof error.runtimeInfoStatus === "number";
623
631
  }
624
632
 
633
+ //#endregion
634
+ //#region src/core/message-filter.ts
635
+ function toolCallIdsOf(message) {
636
+ const calls = message.toolCalls;
637
+ if (!Array.isArray(calls)) return [];
638
+ return calls.map((call) => call.id).filter((id) => Boolean(id));
639
+ }
640
+ function resultCallIdOf(message) {
641
+ if (message.role !== "tool") return void 0;
642
+ return message.toolCallId;
643
+ }
644
+ /**
645
+ * Repair the tool-call pairs a filter broke.
646
+ *
647
+ * Every provider rejects a half pair. A tool result whose call is gone reads as
648
+ * a reply to nothing, and an assistant tool call whose result is gone leaves
649
+ * the turn unanswered — the failure reporters hit on #1482 when a trimmed
650
+ * history landed mid-HITL and Anthropic answered "a tool result is expected".
651
+ * A filter written as `messages.slice(-1)` cannot know this, so the repair runs
652
+ * for every filter rather than being something each caller opts into.
653
+ *
654
+ * Both repairs restore, never discard, and both are anchored on the untrimmed
655
+ * thread:
656
+ *
657
+ * - A kept tool result whose assistant call was trimmed away gets that call put
658
+ * back in front of it. Dropping the result instead would be protocol-safe and
659
+ * semantically wrong: on a human-in-the-loop turn the result IS the new
660
+ * information, and a backend that already holds the call would be asked to
661
+ * resume with nothing.
662
+ * - A kept assistant call whose result was trimmed gets that result restored,
663
+ * placed directly after the call.
664
+ *
665
+ * Restoring a call pulls in every result for that call, not only the one the
666
+ * filter kept. Providers reject an assistant turn with a parallel tool call
667
+ * left unanswered, so a half-answered set is as invalid as a missing one.
668
+ *
669
+ * A call with no result anywhere in `full` is left alone. That is an open call
670
+ * (a pending frontend tool, an unanswered interrupt), not a broken pair. A
671
+ * result whose call is nowhere in `full` is left alone too: it was already
672
+ * unpaired before the filter ran, so it is not this function's to fix.
673
+ *
674
+ * The filter's own ordering is preserved. Restorations are the only insertions,
675
+ * and messages the filter synthesized (absent from `full`) pass through
676
+ * untouched.
677
+ */
678
+ function ɵrepairToolCallPairs(kept, full) {
679
+ const callIdToIssuer = /* @__PURE__ */ new Map();
680
+ const callIdToResult = /* @__PURE__ */ new Map();
681
+ for (const message of full) {
682
+ for (const callId of toolCallIdsOf(message)) callIdToIssuer.set(callId, message);
683
+ const resultCallId = resultCallIdOf(message);
684
+ if (resultCallId !== void 0) callIdToResult.set(resultCallId, message);
685
+ }
686
+ const repaired = [];
687
+ const emitted = /* @__PURE__ */ new Set();
688
+ const emit = (message) => {
689
+ if (emitted.has(message.id)) return;
690
+ emitted.add(message.id);
691
+ repaired.push(message);
692
+ };
693
+ /** Emit an assistant turn together with every result it is owed. */
694
+ const emitWithResults = (message) => {
695
+ emit(message);
696
+ for (const callId of toolCallIdsOf(message)) {
697
+ const result = callIdToResult.get(callId);
698
+ if (result) emit(result);
699
+ }
700
+ };
701
+ for (const message of kept) {
702
+ const resultCallId = resultCallIdOf(message);
703
+ if (resultCallId === void 0) {
704
+ emitWithResults(message);
705
+ continue;
706
+ }
707
+ const issuer = callIdToIssuer.get(resultCallId);
708
+ if (issuer) {
709
+ emitWithResults(issuer);
710
+ continue;
711
+ }
712
+ emit(message);
713
+ }
714
+ return repaired;
715
+ }
716
+
625
717
  //#endregion
626
718
  //#region src/agent.ts
627
719
  function hasHeaders(agent) {
@@ -660,6 +752,18 @@ var ProxiedCopilotRuntimeAgent = class ProxiedCopilotRuntimeAgent extends HttpAg
660
752
  _capabilities;
661
753
  delegate;
662
754
  runtimeInfoPromise;
755
+ _messageFilter;
756
+ /**
757
+ * The HTTP `run` this agent started and has not yet seen finish, as the
758
+ * exact `{ threadId, runId }` it POSTed. `abortRun` narrows `/stop` to this
759
+ * run only while `threadId` still matches: `threadId` is a public field the
760
+ * host may reassign under a live run, and a runId from another thread must
761
+ * not be sent as if it were this thread's. Released when the run stream
762
+ * finalizes, cleared on `connect` (a reconnected thread may run under a
763
+ * runId this client never saw) and never copied by `clone`. Without a
764
+ * provable active run the stop stays thread-wide.
765
+ */
766
+ activeRun;
663
767
  constructor(config) {
664
768
  const normalizedRuntimeUrl = config.runtimeUrl ? config.runtimeUrl.replace(/\/$/, "") : void 0;
665
769
  const transport = config.transport ?? "auto";
@@ -678,10 +782,47 @@ var ProxiedCopilotRuntimeAgent = class ProxiedCopilotRuntimeAgent extends HttpAg
678
782
  this.runtimeMode = config.runtimeMode ?? RUNTIME_MODE_SSE;
679
783
  this.intelligence = config.intelligence;
680
784
  this._capabilities = config.capabilities;
785
+ this._messageFilter = config.messageFilter;
681
786
  if (config.debug) this.debug = config.debug;
682
787
  if (this.transport === "single") this.singleEndpointUrl = this.runtimeEndpointUrl;
683
788
  }
684
789
  /**
790
+ * Adopt the runtime mode a fresh `/info` just reported.
791
+ *
792
+ * A live proxy outlives the runtime configuration that minted it: a
793
+ * redeploy, an env-var change or a rollback can flip `intelligence` → `sse`
794
+ * under a page that is already open. The registry preserves the proxy across
795
+ * that re-sync on purpose — it is backing an open conversation — so the new
796
+ * mode is pushed onto the instance here instead of replacing it.
797
+ *
798
+ * Without this the mode was decided once, at construction, and `run()` kept
799
+ * taking the delegate path against a runtime serving plain SSE, where
800
+ * `response.json()` on a `text/event-stream` body throws for the rest of the
801
+ * page's life. See #7130.
802
+ *
803
+ * A delegate built for the old mode is torn down: it speaks a protocol the
804
+ * runtime no longer serves, and it may be holding a websocket open. The
805
+ * memoized `/info` promise goes with it — it answered for the old
806
+ * configuration, so a later `ensureRuntimeConfiguration()` must re-ask
807
+ * rather than treat that answer as current.
808
+ *
809
+ * Only `wsUrl` is compared on the Intelligence metadata, because that is the
810
+ * one field the delegate bakes in at construction.
811
+ */
812
+ adoptRuntimeMode(runtimeMode, intelligence) {
813
+ const unchanged = this.runtimeMode === runtimeMode && this.intelligence?.wsUrl === intelligence?.wsUrl;
814
+ this.runtimeMode = runtimeMode;
815
+ this.intelligence = intelligence;
816
+ if (unchanged) return;
817
+ this.runtimeInfoPromise = void 0;
818
+ const staleDelegate = this.delegate;
819
+ this.delegate = void 0;
820
+ if (staleDelegate) {
821
+ staleDelegate.abortRun();
822
+ this.detachActiveRun();
823
+ }
824
+ }
825
+ /**
685
826
  * The agent id used for outbound runtime requests — `runtimeAgentId` when
686
827
  * set (manually-registered proxy), otherwise `agentId` (registry id
687
828
  * matches runtime id). Subscriber bookkeeping keeps using `agentId`
@@ -699,6 +840,67 @@ var ProxiedCopilotRuntimeAgent = class ProxiedCopilotRuntimeAgent extends HttpAg
699
840
  get capabilities() {
700
841
  return this._capabilities;
701
842
  }
843
+ /**
844
+ * The filter applied to the outbound message list on every run.
845
+ *
846
+ * Registry-owned. `AgentRegistry` writes it whenever the core-level filter
847
+ * changes, so an agent discovered before the app configured one still picks
848
+ * it up — and so a value written here by hand is replaced on the registry's
849
+ * next sweep. Configure it through `CopilotKitCore` instead.
850
+ */
851
+ get messageFilter() {
852
+ return this._messageFilter;
853
+ }
854
+ set messageFilter(filter) {
855
+ this._messageFilter = filter;
856
+ }
857
+ /**
858
+ * Narrow the outbound payload with the configured message filter.
859
+ *
860
+ * **Called from the HTTP paths only, and that placement is the Intelligence
861
+ * exemption.** An earlier revision applied the filter in
862
+ * `prepareRunAgentInput`, which runs before the runtime mode is resolved:
863
+ * with the default `"auto"` transport, or on a proxy still `"pending"` its
864
+ * first `/info`, `run()` prepared the input, *then* resolved the mode, then
865
+ * handed that already-filtered input to `#runViaDelegate` — so a managed
866
+ * runtime received a truncated thread despite the exemption. Filtering here
867
+ * makes that unreachable by construction: `#runViaHttp` and
868
+ * `#connectViaHttp` are only entered once the mode is known not to be
869
+ * Intelligence. Do not move this back up the call chain.
870
+ *
871
+ * The managed runtime is the store of record for the thread — the threads
872
+ * drawer and the Slack transcript read from it — so a client-side truncation
873
+ * there has a blast radius nobody asked for. Every reporter on #1482 is
874
+ * self-hosted.
875
+ *
876
+ * `prepareRunAgentInput` has already deep-cloned the thread and stripped
877
+ * `activity` messages by the time the input reaches here, so the filter
878
+ * cannot reach the messages the UI renders no matter what it does with the
879
+ * array it is handed. The filter gets a *second* deep clone on top of that,
880
+ * because `input.messages` is also the repair baseline and the untrimmed
881
+ * fallback: sharing the message objects let a filter that mutates a kept
882
+ * message's `toolCallId` corrupt the baseline, and an orphaned tool result
883
+ * then went out on the wire — a payload the provider rejects, which is
884
+ * exactly the failure this whole mechanism exists to prevent.
885
+ */
886
+ #applyMessageFilter(input) {
887
+ const filter = this._messageFilter;
888
+ if (!filter) return input;
889
+ try {
890
+ const kept = filter(structuredClone_(input.messages), { agentId: this.agentId ?? "" });
891
+ if (!Array.isArray(kept)) {
892
+ console.warn("ProxiedCopilotRuntimeAgent: messageFilter returned a non-array value; sending the full message history instead.");
893
+ return input;
894
+ }
895
+ return {
896
+ ...input,
897
+ messages: ɵrepairToolCallPairs(kept, input.messages)
898
+ };
899
+ } catch (error) {
900
+ console.warn("ProxiedCopilotRuntimeAgent: messageFilter failed; sending the full message history instead.", error);
901
+ return input;
902
+ }
903
+ }
702
904
  requestInit(input) {
703
905
  return {
704
906
  ...super.requestInit(input),
@@ -722,6 +924,7 @@ var ProxiedCopilotRuntimeAgent = class ProxiedCopilotRuntimeAgent extends HttpAg
722
924
  if (!this.agentId || !this.threadId) return;
723
925
  if (typeof fetch === "undefined") return;
724
926
  const routedId = this.routedAgentId();
927
+ const runId = this.activeRun?.threadId === this.threadId ? this.activeRun.runId : void 0;
725
928
  if (this.transport === "single") {
726
929
  if (!this.singleEndpointUrl) return;
727
930
  const headers = new Headers({
@@ -736,7 +939,8 @@ var ProxiedCopilotRuntimeAgent = class ProxiedCopilotRuntimeAgent extends HttpAg
736
939
  params: {
737
940
  agentId: routedId,
738
941
  threadId: this.threadId
739
- }
942
+ },
943
+ ...runId === void 0 ? {} : { body: { runId } }
740
944
  }),
741
945
  ...this.credentials ? { credentials: this.credentials } : {}
742
946
  }).catch((error) => {
@@ -755,6 +959,7 @@ var ProxiedCopilotRuntimeAgent = class ProxiedCopilotRuntimeAgent extends HttpAg
755
959
  "Content-Type": "application/json",
756
960
  ...this.headers
757
961
  },
962
+ ...runId === void 0 ? {} : { body: JSON.stringify({ runId }) },
758
963
  ...this.credentials ? { credentials: this.credentials } : {}
759
964
  }).catch((error) => {
760
965
  console.error("ProxiedCopilotRuntimeAgent: stop request failed", error);
@@ -810,7 +1015,9 @@ var ProxiedCopilotRuntimeAgent = class ProxiedCopilotRuntimeAgent extends HttpAg
810
1015
  #connectViaDelegate(input) {
811
1016
  return defer(() => from(this.resolveDelegate())).pipe(switchMap$1((delegate) => withAbortErrorHandling(delegate.connect(input))));
812
1017
  }
813
- #connectViaHttp(input) {
1018
+ #connectViaHttp(unfiltered) {
1019
+ const input = this.#applyMessageFilter(unfiltered);
1020
+ this.activeRun = void 0;
814
1021
  const routedId = this.routedAgentId();
815
1022
  if (this.transport === "single") {
816
1023
  if (!this.singleEndpointUrl) throw new Error("Single endpoint transport requires a runtimeUrl");
@@ -824,13 +1031,24 @@ var ProxiedCopilotRuntimeAgent = class ProxiedCopilotRuntimeAgent extends HttpAg
824
1031
  #runViaDelegate(input) {
825
1032
  return defer(() => from(this.resolveDelegate())).pipe(switchMap$1((delegate) => withAbortErrorHandling(delegate.run(input))));
826
1033
  }
827
- #runViaHttp(input) {
1034
+ #runViaHttp(unfiltered) {
1035
+ const input = this.#applyMessageFilter(unfiltered);
1036
+ const activeRun = {
1037
+ threadId: input.threadId,
1038
+ runId: input.runId
1039
+ };
1040
+ const trackActiveRun = (source) => {
1041
+ this.activeRun = activeRun;
1042
+ return source.pipe(finalize(() => {
1043
+ if (this.activeRun === activeRun) this.activeRun = void 0;
1044
+ }));
1045
+ };
828
1046
  if (this.transport === "single") {
829
1047
  if (!this.singleEndpointUrl) throw new Error("Single endpoint transport requires a runtimeUrl");
830
1048
  const requestInit = this.createSingleRouteRequestInit(input, "agent/run", { agentId: this.routedAgentId() });
831
- return withAbortErrorHandling(transformHttpEventStream(runHttpRequest(() => this.fetch(this.singleEndpointUrl, requestInit))));
1049
+ return trackActiveRun(withAbortErrorHandling(transformHttpEventStream(runHttpRequest(() => this.fetch(this.singleEndpointUrl, requestInit)))));
832
1050
  }
833
- return withAbortErrorHandling(super.run(input));
1051
+ return trackActiveRun(withAbortErrorHandling(super.run(input)));
834
1052
  }
835
1053
  clone() {
836
1054
  const cloned = new ProxiedCopilotRuntimeAgent({
@@ -845,7 +1063,8 @@ var ProxiedCopilotRuntimeAgent = class ProxiedCopilotRuntimeAgent extends HttpAg
845
1063
  intelligence: this.intelligence,
846
1064
  capabilities: this._capabilities,
847
1065
  debug: this.debug,
848
- fetch: this.fetch
1066
+ fetch: this.fetch,
1067
+ messageFilter: this._messageFilter
849
1068
  });
850
1069
  cloned.threadId = this.threadId;
851
1070
  cloned.setState(this.state);
@@ -973,9 +1192,11 @@ var ProxiedCopilotRuntimeAgent = class ProxiedCopilotRuntimeAgent extends HttpAg
973
1192
  createIntelligenceDelegate() {
974
1193
  const routedId = this.routedAgentId();
975
1194
  if (!this.runtimeUrl || !routedId || !this.intelligence?.wsUrl) throw new Error("Intelligence mode requires runtimeUrl, agentId, and intelligence websocket metadata");
1195
+ const single = this.transport === "single";
976
1196
  return new IntelligenceAgent({
977
1197
  url: this.intelligence.wsUrl,
978
- runtimeUrl: this.runtimeUrl,
1198
+ runtimeUrl: single ? this.runtimeEndpointUrl ?? this.runtimeUrl : this.runtimeUrl,
1199
+ transport: single ? "single" : "rest",
979
1200
  agentId: routedId,
980
1201
  headers: { ...this.headers },
981
1202
  credentials: this.credentials,
@@ -1238,6 +1459,7 @@ var AgentRegistry = class {
1238
1459
  this.localAgents = this.assignAgentIds(agents);
1239
1460
  this.applyHeadersToAgents(this.localAgents);
1240
1461
  this.applyCredentialsToAgents(this.localAgents);
1462
+ this.applyMessageFilterToAgents(this.localAgents);
1241
1463
  this.applyRuntimeFetchToAgents(this.localAgents);
1242
1464
  this._agents = this.localAgents;
1243
1465
  }
@@ -1329,6 +1551,7 @@ var AgentRegistry = class {
1329
1551
  };
1330
1552
  this.applyHeadersToAgents(this._agents);
1331
1553
  this.applyCredentialsToAgents(this._agents);
1554
+ this.applyMessageFilterToAgents(this._agents);
1332
1555
  this.applyRuntimeFetchToAgents(this._agents);
1333
1556
  this.notifyAgentsChanged();
1334
1557
  }
@@ -1381,7 +1604,8 @@ var AgentRegistry = class {
1381
1604
  credentials: friends.credentials,
1382
1605
  runtimeMode: this._runtimeUrl ? this._runtimeConnectionStatus === CopilotKitCoreRuntimeConnectionStatus.Connected ? this._runtimeMode : "pending" : RUNTIME_MODE_SSE,
1383
1606
  intelligence: this._intelligence,
1384
- debug: debug ? resolveDebugConfig(debug) : void 0
1607
+ debug: debug ? resolveDebugConfig(debug) : void 0,
1608
+ messageFilter: friends.messageFilter
1385
1609
  });
1386
1610
  this.applyHeadersToAgent(agent);
1387
1611
  this.applyRuntimeFetchToAgent(agent);
@@ -1450,6 +1674,45 @@ var AgentRegistry = class {
1450
1674
  this.applyCredentialsToAgent(agent);
1451
1675
  });
1452
1676
  }
1677
+ /**
1678
+ * Carry the mode a fresh `/info` reported onto a proxy this re-sync kept.
1679
+ *
1680
+ * `canReuseRuntimeAgent` deliberately ignores the mode: a preserved proxy is
1681
+ * backing an open conversation, and replacing the instance to change one
1682
+ * field would drop it. The mode is therefore pushed onto the survivor here,
1683
+ * the same way headers and credentials are. Without it the mode was fixed at
1684
+ * construction, so a runtime that flipped `intelligence` → `sse` under an
1685
+ * open page left every later run on the delegate path. See #7130.
1686
+ */
1687
+ applyRuntimeModeToAgent(agent, runtimeInfo) {
1688
+ if (agent instanceof ProxiedCopilotRuntimeAgent) agent.adoptRuntimeMode(runtimeInfo.mode ?? RUNTIME_MODE_SSE, runtimeInfo.intelligence);
1689
+ }
1690
+ /**
1691
+ * Apply the core's message filter to an agent.
1692
+ *
1693
+ * The test is the class, not which bucket the agent is registered in: every
1694
+ * `ProxiedCopilotRuntimeAgent` gets the core filter, whether it came from
1695
+ * `/info` or from `registerProxiedAgent` (which files its proxy under
1696
+ * `localAgents`). Agents of any other class are left untouched — an
1697
+ * `AbstractAgent` the app built has no such hook, and its owner already has
1698
+ * the AG-UI middleware seam.
1699
+ *
1700
+ * This is the sole writer of `agent.messageFilter` in normal operation. The
1701
+ * public setter exists for the registry and for tests; a value written
1702
+ * directly onto an agent is replaced the next time the registry sweeps, the
1703
+ * same way per-agent credentials are.
1704
+ */
1705
+ applyMessageFilterToAgent(agent) {
1706
+ if (agent instanceof ProxiedCopilotRuntimeAgent) agent.messageFilter = this.core.messageFilter;
1707
+ }
1708
+ /**
1709
+ * Apply the core's message filter to all agents
1710
+ */
1711
+ applyMessageFilterToAgents(agents) {
1712
+ Object.values(agents).forEach((agent) => {
1713
+ this.applyMessageFilterToAgent(agent);
1714
+ });
1715
+ }
1453
1716
  createRuntimeFetch() {
1454
1717
  if (!this.runtimeFetch) this.runtimeFetch = (async (input, init) => {
1455
1718
  const meta = () => runtimeRequestMeta(init);
@@ -1837,7 +2100,9 @@ var AgentRegistry = class {
1837
2100
  if (existing instanceof ProxiedCopilotRuntimeAgent && this.canReuseRuntimeAgent(existing, runtimeUrl, this._runtimeTransport)) {
1838
2101
  this.applyHeadersToAgent(existing);
1839
2102
  this.applyCredentialsToAgent(existing);
2103
+ this.applyMessageFilterToAgent(existing);
1840
2104
  this.applyRuntimeFetchToAgent(existing);
2105
+ this.applyRuntimeModeToAgent(existing, runtimeInfoResponse);
1841
2106
  return [id, existing];
1842
2107
  }
1843
2108
  const agent = new ProxiedCopilotRuntimeAgent({
@@ -1849,7 +2114,8 @@ var AgentRegistry = class {
1849
2114
  runtimeMode: runtimeInfoResponse.mode ?? RUNTIME_MODE_SSE,
1850
2115
  intelligence: runtimeInfoResponse.intelligence,
1851
2116
  capabilities,
1852
- debug: rawDebug ? resolveDebugConfig(rawDebug) : void 0
2117
+ debug: rawDebug ? resolveDebugConfig(rawDebug) : void 0,
2118
+ messageFilter: this.core.messageFilter
1853
2119
  });
1854
2120
  this.applyHeadersToAgent(agent);
1855
2121
  this.applyRuntimeFetchToAgent(agent);
@@ -2241,7 +2507,10 @@ var SuggestionEngine = class {
2241
2507
  credentials
2242
2508
  }) } : {}
2243
2509
  });
2244
- } else suggestionAgent = suggestionsProviderAgent.clone();
2510
+ } else {
2511
+ suggestionAgent = suggestionsProviderAgent.clone();
2512
+ if (suggestionAgent instanceof ProxiedCopilotRuntimeAgent) suggestionAgent.messageFilter = void 0;
2513
+ }
2245
2514
  suggestionAgent.threadId = suggestionId;
2246
2515
  suggestionAgent.setMessages(seededMessages);
2247
2516
  suggestionAgent.setState(seededState);
@@ -2603,6 +2872,19 @@ var WebMCPRegistry = class {
2603
2872
  //#endregion
2604
2873
  //#region src/core/run-handler.ts
2605
2874
  /**
2875
+ * Live approvals cancelled by stopping their own run, keyed by the handler's
2876
+ * signal. The value reports whether that stop is still current: no reconnect
2877
+ * has started and the thread is unchanged, so nothing will restore the call.
2878
+ *
2879
+ * Kept here rather than in `AbortSignal.reason`: React Native's global
2880
+ * `AbortController` is the `abort-controller@3` polyfill, whose `abort()`
2881
+ * takes no reason and whose signals never expose one.
2882
+ */
2883
+ const liveHITLStops = /* @__PURE__ */ new WeakMap();
2884
+ function discardAbortedResult(signal, discardOnAbort, errorMessage) {
2885
+ return !!(discardOnAbort && signal?.aborted && !(liveHITLStops.get(signal)?.() && errorMessage));
2886
+ }
2887
+ /**
2606
2888
  * Absolute safety cap on the depth of recursive follow-up runs triggered by
2607
2889
  * `processAgentResult`. Without this, any scenario that keeps
2608
2890
  * `needsFollowUp = true` (e.g. the LLM repeatedly calling the same tool, a
@@ -2715,6 +2997,10 @@ var RunHandler = class {
2715
2997
  _lastConnectedThreadIdsByAgent = /* @__PURE__ */ new Map();
2716
2998
  _anonymousAgentIds = /* @__PURE__ */ new WeakMap();
2717
2999
  _nextAnonymousAgentId = 0;
3000
+ /** Prevent reconnects from starting an interaction already executing locally. */
3001
+ _executingToolCalls = /* @__PURE__ */ new WeakMap();
3002
+ _connectionGenerations = /* @__PURE__ */ new WeakMap();
3003
+ _interactionAbortControllers = /* @__PURE__ */ new WeakMap();
2718
3004
  constructor(core) {
2719
3005
  this.core = core;
2720
3006
  }
@@ -2723,8 +3009,14 @@ var RunHandler = class {
2723
3009
  * that in-flight tool handlers should stop and `processAgentResult` should
2724
3010
  * not start a follow-up run.
2725
3011
  */
2726
- abortCurrentRun() {
3012
+ abortCurrentRun(agent) {
2727
3013
  this._runAbortController?.abort();
3014
+ const replay = this._interactionAbortControllers.get(agent);
3015
+ if (replay) {
3016
+ for (const controller of replay.controllers) controller.abort();
3017
+ this._interactionAbortControllers.delete(agent);
3018
+ this._executingToolCalls.delete(agent);
3019
+ }
2728
3020
  }
2729
3021
  /**
2730
3022
  * Typed access to CopilotKitCore's internal ("friend") methods.
@@ -2880,7 +3172,14 @@ var RunHandler = class {
2880
3172
  * Connect an agent (establish initial connection)
2881
3173
  */
2882
3174
  async connectAgent({ agent }) {
3175
+ this._connectionGenerations.set(agent, {});
2883
3176
  const incomingThreadId = agent.threadId ?? null;
3177
+ const previousReplay = this._interactionAbortControllers.get(agent);
3178
+ if (previousReplay && previousReplay.threadId !== incomingThreadId) {
3179
+ for (const controller of previousReplay.controllers) controller.abort();
3180
+ this._interactionAbortControllers.delete(agent);
3181
+ this._executingToolCalls.delete(agent);
3182
+ }
2884
3183
  try {
2885
3184
  const restoreKey = this.getConnectRestoreKey(agent);
2886
3185
  const isFreshRestore = incomingThreadId !== (this._lastConnectedThreadIdsByAgent.get(restoreKey) ?? null);
@@ -2905,11 +3204,33 @@ var RunHandler = class {
2905
3204
  tools: this.buildFrontendTools(agent.agentId),
2906
3205
  context: this._internal.getContextForAgent(agent.agentId)
2907
3206
  }, this.createAgentErrorSubscriber(agent));
2908
- return this.processAgentResult({
3207
+ if ((agent.threadId ?? null) !== incomingThreadId) return runAgentResult;
3208
+ const controller = new AbortController();
3209
+ const replay = this._interactionAbortControllers.get(agent) ?? {
3210
+ threadId: incomingThreadId,
3211
+ controllers: /* @__PURE__ */ new Set()
3212
+ };
3213
+ replay.controllers.add(controller);
3214
+ this._interactionAbortControllers.set(agent, replay);
3215
+ this.processAgentResult({
2909
3216
  runAgentResult,
2910
3217
  agent,
2911
- executeFrontendTools: false
3218
+ toolExecutionMode: "human-in-the-loop",
3219
+ signal: controller.signal
3220
+ }).catch(async (error) => {
3221
+ await this._internal.emitError({
3222
+ error: error instanceof Error ? error : new Error(String(error)),
3223
+ code: CopilotKitCoreErrorCode.AGENT_CONNECT_FAILED,
3224
+ context: {
3225
+ agentId: agent.agentId,
3226
+ threadId: incomingThreadId
3227
+ }
3228
+ });
3229
+ }).finally(() => {
3230
+ replay.controllers.delete(controller);
3231
+ if (replay.controllers.size === 0 && this._interactionAbortControllers.get(agent) === replay) this._interactionAbortControllers.delete(agent);
2912
3232
  });
3233
+ return runAgentResult;
2913
3234
  } catch (error) {
2914
3235
  const connectError = error instanceof Error ? error : new Error(String(error));
2915
3236
  if (!isAbortError$1(connectError)) {
@@ -3011,45 +3332,83 @@ var RunHandler = class {
3011
3332
  /**
3012
3333
  * Process agent result and execute tools
3013
3334
  */
3014
- async processAgentResult({ runAgentResult, agent, runId, executeFrontendTools = true }) {
3335
+ async processAgentResult({ runAgentResult, agent, runId, toolExecutionMode = "all", signal = this._runAbortController?.signal }) {
3015
3336
  const { newMessages } = runAgentResult;
3016
3337
  const agentId = agent.agentId;
3017
3338
  let needsFollowUp = false;
3018
- if (executeFrontendTools) {
3019
- for (const message of newMessages) if (message.role === "assistant") for (const toolCall of message.toolCalls || []) {
3020
- const tool = this.getTool({
3021
- toolName: toolCall.function.name,
3339
+ const threadId = agent.threadId;
3340
+ const executing = this._executingToolCalls.get(agent) ?? /* @__PURE__ */ new Set();
3341
+ this._executingToolCalls.set(agent, executing);
3342
+ if (toolExecutionMode === "human-in-the-loop" && agent.messages.some((message) => message.role === "assistant" && message.toolCalls?.some((call) => executing.has(JSON.stringify([threadId, call.id])) && !agent.messages.some((result) => result.role === "tool" && result.toolCallId === call.id && !this.isFrontendPlaceholderResult(result))))) return runAgentResult;
3343
+ const messagesToProcess = toolExecutionMode === "human-in-the-loop" ? [...agent.messages] : newMessages;
3344
+ for (const message of messagesToProcess) if (message.role === "assistant") for (const toolCall of message.toolCalls || []) {
3345
+ const tool = this.getTool({
3346
+ toolName: toolCall.function.name,
3347
+ agentId: agent.agentId
3348
+ });
3349
+ let wildcardTool;
3350
+ const getWildcardTool = () => {
3351
+ if (tool || wildcardTool) return wildcardTool;
3352
+ wildcardTool = this.getTool({
3353
+ toolName: WILDCARD_TOOL_NAME,
3022
3354
  agentId: agent.agentId
3023
3355
  });
3024
- let wildcardTool;
3025
- const getWildcardTool = () => {
3026
- if (tool || wildcardTool) return wildcardTool;
3027
- wildcardTool = this.getTool({
3028
- toolName: WILDCARD_TOOL_NAME,
3029
- agentId: agent.agentId
3030
- });
3031
- return wildcardTool;
3356
+ return wildcardTool;
3357
+ };
3358
+ const executableTool = tool ?? getWildcardTool();
3359
+ if (toolExecutionMode === "human-in-the-loop" && (executableTool?.type !== "human-in-the-loop" || !executableTool.handler)) continue;
3360
+ const executionKey = JSON.stringify([threadId, toolCall.id]);
3361
+ if (signal?.aborted || agent.threadId !== threadId || executing.has(executionKey)) continue;
3362
+ const existingResultIndex = newMessages.findIndex((m) => m.role === "tool" && m.toolCallId === toolCall.id);
3363
+ let existingResult = agent.messages.find((m) => m.role === "tool" && m.toolCallId === toolCall.id) ?? newMessages[existingResultIndex];
3364
+ if (existingResult && executableTool?.handler && this.isFrontendPlaceholderResult(existingResult)) {
3365
+ if (existingResultIndex !== -1) newMessages.splice(existingResultIndex, 1);
3366
+ existingResult = void 0;
3367
+ const agentMsgIdx = agent.messages.findIndex((m) => m.role === "tool" && m.toolCallId === toolCall.id);
3368
+ if (agentMsgIdx !== -1) agent.messages.splice(agentMsgIdx, 1);
3369
+ }
3370
+ if (!existingResult) {
3371
+ executing.add(executionKey);
3372
+ const interactionController = toolExecutionMode === "all" && executableTool?.type === "human-in-the-loop" ? new AbortController() : void 0;
3373
+ const interactions = this._interactionAbortControllers.get(agent) ?? {
3374
+ threadId: threadId ?? null,
3375
+ controllers: /* @__PURE__ */ new Set()
3376
+ };
3377
+ const abortInteraction = () => {
3378
+ if (!interactionController) return;
3379
+ const connectionGeneration = this._connectionGenerations.get(agent);
3380
+ liveHITLStops.set(interactionController.signal, () => this._connectionGenerations.get(agent) === connectionGeneration && agent.threadId === threadId);
3381
+ interactionController.abort();
3032
3382
  };
3033
- let existingResultIndex = newMessages.findIndex((m) => m.role === "tool" && m.toolCallId === toolCall.id);
3034
- const existingResult = existingResultIndex === -1 ? void 0 : newMessages[existingResultIndex];
3035
- const executableTool = tool ?? getWildcardTool();
3036
- if (existingResult && executableTool?.handler && this.isFrontendPlaceholderResult(existingResult)) {
3037
- newMessages.splice(existingResultIndex, 1);
3038
- existingResultIndex = -1;
3039
- const agentMsgIdx = agent.messages.findIndex((m) => m.role === "tool" && m.toolCallId === toolCall.id);
3040
- if (agentMsgIdx !== -1) agent.messages.splice(agentMsgIdx, 1);
3383
+ if (interactionController) {
3384
+ interactions.controllers.add(interactionController);
3385
+ this._interactionAbortControllers.set(agent, interactions);
3386
+ signal?.addEventListener("abort", abortInteraction, { once: true });
3387
+ if (signal?.aborted) abortInteraction();
3041
3388
  }
3042
- if (existingResultIndex === -1) if (tool) {
3043
- if (await this.executeSpecificTool(tool, toolCall, message, agent, agentId)) needsFollowUp = true;
3044
- } else {
3045
- const fallbackTool = getWildcardTool();
3046
- if (fallbackTool) {
3047
- if (await this.executeWildcardTool(fallbackTool, toolCall, message, agent, agentId)) needsFollowUp = true;
3389
+ const executionSignal = interactionController?.signal ?? signal;
3390
+ const discardOnAbort = toolExecutionMode === "human-in-the-loop" || !!interactionController;
3391
+ try {
3392
+ if (tool) {
3393
+ if (await this.executeSpecificTool(tool, toolCall, message, agent, agentId, executionSignal, discardOnAbort)) needsFollowUp = true;
3394
+ } else {
3395
+ const fallbackTool = getWildcardTool();
3396
+ if (fallbackTool) {
3397
+ if (await this.executeWildcardTool(fallbackTool, toolCall, message, agent, agentId, executionSignal, discardOnAbort)) needsFollowUp = true;
3398
+ }
3399
+ }
3400
+ } finally {
3401
+ executing.delete(executionKey);
3402
+ if (interactionController) {
3403
+ signal?.removeEventListener("abort", abortInteraction);
3404
+ interactions.controllers.delete(interactionController);
3405
+ if (interactions.controllers.size === 0 && this._interactionAbortControllers.get(agent) === interactions) this._interactionAbortControllers.delete(agent);
3048
3406
  }
3049
3407
  }
3408
+ if (interactionController?.signal.aborted) return runAgentResult;
3050
3409
  }
3051
3410
  }
3052
- if (needsFollowUp && !this._runAbortController?.signal.aborted) if (this._runDepth >= MAX_FOLLOW_UP_DEPTH) logger.warn(`[CopilotKit] Follow-up depth limit (${MAX_FOLLOW_UP_DEPTH}) reached for agent "${agentId}". Stopping recursive follow-up runs to prevent an infinite loop. This usually indicates a tool that keeps requesting a follow-up (e.g. the LLM repeatedly calling the same tool). Consider setting "followUp: false" on the offending tool.`);
3411
+ if (needsFollowUp && agent.threadId === threadId && !signal?.aborted) if (this._runDepth >= MAX_FOLLOW_UP_DEPTH) logger.warn(`[CopilotKit] Follow-up depth limit (${MAX_FOLLOW_UP_DEPTH}) reached for agent "${agentId}". Stopping recursive follow-up runs to prevent an infinite loop. This usually indicates a tool that keeps requesting a follow-up (e.g. the LLM repeatedly calling the same tool). Consider setting "followUp: false" on the offending tool.`);
3053
3412
  else {
3054
3413
  await this._internal.waitForPendingFrameworkUpdates();
3055
3414
  const continuationHandoff = this._internal.stateManager.markNextRunAsContinuation(agent, runId);
@@ -3069,7 +3428,7 @@ var RunHandler = class {
3069
3428
  * Handles arg parsing, subscriber notifications, handler invocation, result stringification,
3070
3429
  * and error handling.
3071
3430
  */
3072
- async executeToolHandler({ tool, toolCall, agent, agentId, handlerArgs, toolType, messageId }) {
3431
+ async executeToolHandler({ tool, toolCall, agent, agentId, handlerArgs, toolType, messageId, signal = this._runAbortController?.signal, discardOnAbort = false }) {
3073
3432
  let toolCallResult = "";
3074
3433
  let errorMessage;
3075
3434
  let isArgumentError = false;
@@ -3104,7 +3463,7 @@ var RunHandler = class {
3104
3463
  const result = await tool.handler(parsedArgs, {
3105
3464
  toolCall,
3106
3465
  agent,
3107
- signal: this._runAbortController?.signal
3466
+ signal
3108
3467
  });
3109
3468
  if (result === void 0 || result === null) toolCallResult = "";
3110
3469
  else if (typeof result === "string") toolCallResult = result;
@@ -3112,7 +3471,7 @@ var RunHandler = class {
3112
3471
  } catch (error) {
3113
3472
  const handlerError = error instanceof Error ? error : new Error(String(error));
3114
3473
  errorMessage = handlerError.message;
3115
- await this._internal.emitError({
3474
+ if (!discardAbortedResult(signal, discardOnAbort, errorMessage)) await this._internal.emitError({
3116
3475
  error: handlerError,
3117
3476
  code: CopilotKitCoreErrorCode.TOOL_HANDLER_FAILED,
3118
3477
  context: {
@@ -3143,7 +3502,8 @@ var RunHandler = class {
3143
3502
  /**
3144
3503
  * Execute a specific tool
3145
3504
  */
3146
- async executeSpecificTool(tool, toolCall, message, agent, agentId) {
3505
+ async executeSpecificTool(tool, toolCall, message, agent, agentId, signal, discardOnAbort = false) {
3506
+ const threadId = agent.threadId;
3147
3507
  if (tool?.agentId && tool.agentId !== agent.agentId) return false;
3148
3508
  let handlerResult = {
3149
3509
  result: "",
@@ -3157,11 +3517,13 @@ var RunHandler = class {
3157
3517
  agentId,
3158
3518
  handlerArgs: toolCall.function.arguments,
3159
3519
  toolType: "specific",
3160
- messageId: message.id
3520
+ messageId: message.id,
3521
+ signal,
3522
+ discardOnAbort
3161
3523
  });
3162
3524
  {
3163
3525
  const messageIndex = agent.messages.findIndex((m) => m.id === message.id);
3164
- if (messageIndex === -1) return false;
3526
+ if (discardAbortedResult(signal, discardOnAbort, handlerResult.error) || agent.threadId !== threadId || messageIndex === -1 || agent.messages.some((m) => m.role === "tool" && m.toolCallId === toolCall.id && !this.isFrontendPlaceholderResult(m))) return false;
3165
3527
  let insertAt = messageIndex + 1;
3166
3528
  while (insertAt < agent.messages.length && agent.messages[insertAt]?.role === "tool") insertAt++;
3167
3529
  const toolMessage = {
@@ -3181,7 +3543,8 @@ var RunHandler = class {
3181
3543
  * specific tools, so this method keeps its own arg-wrapping logic rather than
3182
3544
  * delegating to `executeToolHandler`.
3183
3545
  */
3184
- async executeWildcardTool(wildcardTool, toolCall, message, agent, agentId) {
3546
+ async executeWildcardTool(wildcardTool, toolCall, message, agent, agentId, signal, discardOnAbort = false) {
3547
+ const threadId = agent.threadId;
3185
3548
  if (wildcardTool?.agentId && wildcardTool.agentId !== agent.agentId) return false;
3186
3549
  let toolCallResult = "";
3187
3550
  let errorMessage;
@@ -3219,7 +3582,8 @@ var RunHandler = class {
3219
3582
  if (!errorMessage) try {
3220
3583
  const result = await wildcardTool.handler(wildcardArgs, {
3221
3584
  toolCall,
3222
- agent
3585
+ agent,
3586
+ signal
3223
3587
  });
3224
3588
  if (result === void 0 || result === null) toolCallResult = "";
3225
3589
  else if (typeof result === "string") toolCallResult = result;
@@ -3227,7 +3591,7 @@ var RunHandler = class {
3227
3591
  } catch (error) {
3228
3592
  const handlerError = error instanceof Error ? error : new Error(String(error));
3229
3593
  errorMessage = handlerError.message;
3230
- await this._internal.emitError({
3594
+ if (!discardAbortedResult(signal, discardOnAbort, errorMessage)) await this._internal.emitError({
3231
3595
  error: handlerError,
3232
3596
  code: CopilotKitCoreErrorCode.TOOL_HANDLER_FAILED,
3233
3597
  context: {
@@ -3252,7 +3616,7 @@ var RunHandler = class {
3252
3616
  }
3253
3617
  {
3254
3618
  const messageIndex = agent.messages.findIndex((m) => m.id === message.id);
3255
- if (messageIndex === -1) return false;
3619
+ if (discardAbortedResult(signal, discardOnAbort, errorMessage) || agent.threadId !== threadId || messageIndex === -1 || agent.messages.some((m) => m.role === "tool" && m.toolCallId === toolCall.id && !this.isFrontendPlaceholderResult(m))) return false;
3256
3620
  let insertAt = messageIndex + 1;
3257
3621
  while (insertAt < agent.messages.length && agent.messages[insertAt]?.role === "tool") insertAt++;
3258
3622
  const toolMessage = {
@@ -5037,6 +5401,7 @@ function normalizeHeaders(headers) {
5037
5401
  var CopilotKitCore = class {
5038
5402
  _headers;
5039
5403
  _credentials;
5404
+ _messageFilter;
5040
5405
  _properties;
5041
5406
  _defaultThrottleMs;
5042
5407
  _debug;
@@ -5061,9 +5426,10 @@ var CopilotKitCore = class {
5061
5426
  * store that was registered prior to that initial notification.
5062
5427
  */
5063
5428
  previousAgentIds = /* @__PURE__ */ new Set();
5064
- constructor({ runtimeUrl, runtimeTransport = "auto", deferInitialConnection = false, headers = {}, credentials, properties = {}, agents__unsafe_dev_only = {}, tools = [], suggestionsConfig = [], debug }) {
5429
+ constructor({ runtimeUrl, runtimeTransport = "auto", deferInitialConnection = false, headers = {}, credentials, messageFilter, properties = {}, agents__unsafe_dev_only = {}, tools = [], suggestionsConfig = [], debug }) {
5065
5430
  this._headers = normalizeHeaders(headers);
5066
5431
  this._credentials = credentials;
5432
+ this._messageFilter = messageFilter;
5067
5433
  this._properties = properties;
5068
5434
  this._debug = debug;
5069
5435
  this.agentRegistry = new AgentRegistry(this);
@@ -5183,6 +5549,9 @@ var CopilotKitCore = class {
5183
5549
  get credentials() {
5184
5550
  return this._credentials;
5185
5551
  }
5552
+ get messageFilter() {
5553
+ return this._messageFilter;
5554
+ }
5186
5555
  get properties() {
5187
5556
  return this._properties;
5188
5557
  }
@@ -5328,6 +5697,17 @@ var CopilotKitCore = class {
5328
5697
  this.agentRegistry.applyCredentialsToAgents(this.agentRegistry.agents);
5329
5698
  this.agentRegistry.handleCredentialsChanged();
5330
5699
  }
5700
+ /**
5701
+ * Replace the message filter applied to every runtime agent.
5702
+ *
5703
+ * Applies to agents already discovered as well as ones discovered later, so
5704
+ * a filter set before `/info` lands is not lost. Pass `undefined` to go back
5705
+ * to sending the full thread.
5706
+ */
5707
+ setMessageFilter(messageFilter) {
5708
+ this._messageFilter = messageFilter;
5709
+ this.agentRegistry.applyMessageFilterToAgents(this.agentRegistry.agents);
5710
+ }
5331
5711
  setProperties(properties) {
5332
5712
  this._properties = properties;
5333
5713
  this.notifySubscribers((subscriber) => subscriber.onPropertiesChanged?.({
@@ -5676,7 +6056,7 @@ var CopilotKitCore = class {
5676
6056
  return this.runHandler.connectAgent(params);
5677
6057
  }
5678
6058
  stopAgent(params) {
5679
- this.runHandler.abortCurrentRun();
6059
+ this.runHandler.abortCurrentRun(params.agent);
5680
6060
  params.agent.abortRun();
5681
6061
  }
5682
6062
  async runAgent(params) {