@copilotkit/core 1.76.0 → 1.77.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
@@ -5,6 +5,7 @@ import { BehaviorSubject, EMPTY, NEVER, Notification, Observable, Subject, Subsc
5
5
  import { catchError, concatMap, concatWith, delay, distinctUntilChanged, endWith, filter, finalize, ignoreElements, map, mergeMap, observeOn as observeOn$1, scan, share, shareReplay, startWith, switchMap as switchMap$1, take, takeUntil, tap, timeout, withLatestFrom } from "rxjs/operators";
6
6
  import { Socket } from "phoenix";
7
7
  import { zodToJsonSchema } from "zod-to-json-schema";
8
+ import { createCollector, createTrajectoryCollector } from "@copilotkit/learning";
8
9
 
9
10
  //#region src/utils/http-agent.ts
10
11
  /**
@@ -194,10 +195,17 @@ function ɵcreatePhoenixJoinOutcome$(channel) {
194
195
  *
195
196
  * The socket is constructed and connected on subscription, and disconnected on
196
197
  * teardown. Each subscription creates an isolated socket instance.
198
+ *
199
+ * Heartbeats default to every 15s: Phoenix's own 30s default matches common
200
+ * reverse-proxy WebSocket idle timeouts (e.g. Azure Application Gateway), which
201
+ * would drop the socket during a quiet run. Callers may override it.
197
202
  */
198
203
  function ɵphoenixSocket$(options) {
199
204
  return defer(() => {
200
- const socket = new Socket(options.url, options.options);
205
+ const socket = new Socket(options.url, {
206
+ heartbeatIntervalMs: 15e3,
207
+ ...options.options
208
+ });
201
209
  const signals$ = ɵcreatePhoenixSocketSignals$(socket).pipe(shareReplay({
202
210
  bufferSize: 1,
203
211
  refCount: true
@@ -281,6 +289,11 @@ const STOP_RUN_EVENT = "stop_run";
281
289
  const CONNECT_STREAM_IDLE_REPLAY_FALLBACK_MS = 100;
282
290
  const CONNECT_IDLE_WARNING_MS = 3e4;
283
291
  const MAX_UNOPENED_CREDENTIAL_REFRESHES = 2;
292
+ function isMcpResourceRead(input) {
293
+ const forwardedProps = input.forwardedProps;
294
+ const request = forwardedProps?.__proxiedMCPRequest;
295
+ return forwardedProps?.__copilotkitMcpResourceReadOnly === true && typeof request === "object" && request !== null && request.method === "resources/read";
296
+ }
284
297
  var AgentThreadLockedError = class extends Error {
285
298
  constructor(threadId) {
286
299
  super(threadId ? `Thread ${threadId} is locked` : "Thread is locked");
@@ -388,10 +401,22 @@ var IntelligenceAgent = class IntelligenceAgent extends AbstractAgent {
388
401
  * server-pushed AG-UI events to the Observable subscriber.
389
402
  */
390
403
  run(input) {
391
- this.threadId = input.threadId;
392
- this.canonicalRunId = input.runId;
404
+ if (!isMcpResourceRead(input)) {
405
+ this.threadId = input.threadId;
406
+ this.canonicalRunId = input.runId;
407
+ }
393
408
  return defer(() => this.requestJoinCredentials$("run", input)).pipe(switchMap((credentials) => {
394
409
  if (credentials === null) return throwError(() => /* @__PURE__ */ new Error("REST run request returned no credentials"));
410
+ if ("kind" in credentials) return of({
411
+ type: EventType.RUN_STARTED,
412
+ threadId: credentials.threadId,
413
+ runId: credentials.runId
414
+ }, {
415
+ type: EventType.RUN_FINISHED,
416
+ threadId: credentials.threadId,
417
+ runId: credentials.runId,
418
+ result: credentials.result
419
+ });
395
420
  const canonicalInput = this.applyCanonicalRunIdentity(input, credentials, { fallbackToInputRunId: true });
396
421
  return this.observeThread$(canonicalInput, credentials, {
397
422
  completeOnRunError: false,
@@ -483,7 +508,18 @@ var IntelligenceAgent = class IntelligenceAgent extends AbstractAgent {
483
508
  const text = await response.text().catch(() => "");
484
509
  throw new Error(text || response.statusText || String(response.status));
485
510
  }
486
- return this.normalizeJoinCredentials(await response.json(), input);
511
+ const payload = await response.json();
512
+ if (mode === "run" && isMcpResourceRead(input) && typeof payload === "object" && payload !== null && payload.kind === "mcp-resource-read") {
513
+ const resource = payload;
514
+ if (typeof resource.threadId !== "string" || !resource.threadId || typeof resource.runId !== "string" || !resource.runId) throw new Error("invalid MCP resource response identity");
515
+ return {
516
+ kind: "mcp-resource-read",
517
+ threadId: resource.threadId,
518
+ runId: resource.runId,
519
+ result: resource.result
520
+ };
521
+ }
522
+ return this.normalizeJoinCredentials(payload, input);
487
523
  } catch (error) {
488
524
  if (error instanceof AgentThreadLockedError) throw error;
489
525
  throw new Error(`REST ${mode} request failed: ${error instanceof Error ? error.message : String(error)}`, { cause: error });
@@ -1266,6 +1302,21 @@ var ProxiedCopilotRuntimeAgent = class ProxiedCopilotRuntimeAgent extends HttpAg
1266
1302
  }
1267
1303
  return cloned;
1268
1304
  }
1305
+ /** Run presentation-only MCP reads on a separate client agent so their
1306
+ * lifecycle cannot replace an in-flight approval's detach/completion state. */
1307
+ async ɵrunMcpResourceRead(parameters) {
1308
+ const request = parameters.forwardedProps?.__proxiedMCPRequest;
1309
+ if (typeof request !== "object" || request === null || request.method !== "resources/read") throw new Error("Only MCP resources/read can use the read-only path");
1310
+ await this.ensureRuntimeConfiguration();
1311
+ const resourceParameters = {
1312
+ ...parameters,
1313
+ forwardedProps: {
1314
+ ...parameters.forwardedProps,
1315
+ __copilotkitMcpResourceReadOnly: true
1316
+ }
1317
+ };
1318
+ return this.runtimeMode === RUNTIME_MODE_INTELLIGENCE ? this.clone().runAgent(resourceParameters) : this.runAgent(resourceParameters);
1319
+ }
1269
1320
  /**
1270
1321
  * Drop the delegate's cached `lastSeenEventId` for this thread so
1271
1322
  * the next connect requests a full historical replay from the
@@ -1906,6 +1957,10 @@ var AgentRegistry = class {
1906
1957
  this.applyMessageFilterToAgent(agent);
1907
1958
  });
1908
1959
  }
1960
+ /** Preserve the exact configured URL when posting to a single endpoint. */
1961
+ get runtimeEndpointUrl() {
1962
+ return this._runtimeEndpointUrl;
1963
+ }
1909
1964
  createRuntimeFetch() {
1910
1965
  if (!this.runtimeFetch) this.runtimeFetch = (async (input, init) => {
1911
1966
  const meta = () => runtimeRequestMeta(init);
@@ -2930,6 +2985,68 @@ function normalizeToolResultContent(content) {
2930
2985
  function isForwardedToClientPlaceholder(content) {
2931
2986
  return normalizeToolResultContent(content) === FORWARDED_TO_CLIENT;
2932
2987
  }
2988
+ const MEDIA_PART_TYPES = new Set([
2989
+ "image",
2990
+ "audio",
2991
+ "video",
2992
+ "document"
2993
+ ]);
2994
+ function isOptionalString(value) {
2995
+ return value === void 0 || typeof value === "string";
2996
+ }
2997
+ function isPartSource(value) {
2998
+ if (!value || typeof value !== "object") return false;
2999
+ const source = value;
3000
+ if (typeof source.value !== "string") return false;
3001
+ switch (source.type) {
3002
+ case "data": return typeof source.mimeType === "string";
3003
+ case "url": return isOptionalString(source.mimeType);
3004
+ case "file": return isOptionalString(source.provider) && isOptionalString(source.mimeType);
3005
+ default: return false;
3006
+ }
3007
+ }
3008
+ /**
3009
+ * Mirrors `ContentPartSchema` from `@ag-ui/core/schemas`. The runtime parses
3010
+ * the request with that schema, and one invalid part there fails the whole run.
3011
+ * Core does not import the schema because the UMD build has no global for it.
3012
+ * A test checks this function against the schema.
3013
+ */
3014
+ function isContentPart(value) {
3015
+ if (!value || typeof value !== "object" || Array.isArray(value)) return false;
3016
+ const part = value;
3017
+ if (!isOptionalString(part.id) || part.metadata === null) return false;
3018
+ if (part.type === "text") return typeof part.text === "string";
3019
+ return typeof part.type === "string" && MEDIA_PART_TYPES.has(part.type) && isPartSource(part.source);
3020
+ }
3021
+ /**
3022
+ * Converts what a frontend tool handler returned into tool message content.
3023
+ * A non-empty array of AG-UI content parts passes through unchanged, so an
3024
+ * image reaches the model as media. Everything else becomes a string, as before.
3025
+ *
3026
+ * `text` is the string form, for surfaces typed as `result: string`
3027
+ * (`runTool`, `onToolExecutionEnd`). For content parts it is the JSON text they
3028
+ * received before. It is computed here, so a part that cannot be serialized
3029
+ * throws inside the caller's handler error boundary.
3030
+ */
3031
+ function toToolResultContent(result) {
3032
+ if (result === void 0 || result === null) return {
3033
+ content: "",
3034
+ text: ""
3035
+ };
3036
+ if (typeof result === "string") return {
3037
+ content: result,
3038
+ text: result
3039
+ };
3040
+ const text = JSON.stringify(result);
3041
+ if (Array.isArray(result) && result.length > 0 && result.every(isContentPart)) return {
3042
+ content: result,
3043
+ text
3044
+ };
3045
+ return {
3046
+ content: text,
3047
+ text
3048
+ };
3049
+ }
2933
3050
 
2934
3051
  //#endregion
2935
3052
  //#region src/core/tool-schema.ts
@@ -3421,7 +3538,7 @@ var RunHandler = class {
3421
3538
  this.processAgentResult({
3422
3539
  runAgentResult,
3423
3540
  agent,
3424
- toolExecutionMode: "human-in-the-loop",
3541
+ toolExecutionMode: "restore",
3425
3542
  signal: controller.signal
3426
3543
  }).catch(async (error) => {
3427
3544
  await this._internal.emitError({
@@ -3552,8 +3669,8 @@ var RunHandler = class {
3552
3669
  const threadId = agent.threadId;
3553
3670
  const executing = this._executingToolCalls.get(agent) ?? /* @__PURE__ */ new Set();
3554
3671
  this._executingToolCalls.set(agent, executing);
3555
- 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;
3556
- const messagesToProcess = toolExecutionMode === "human-in-the-loop" ? [...agent.messages] : newMessages;
3672
+ if (toolExecutionMode === "restore" && 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;
3673
+ const messagesToProcess = toolExecutionMode === "restore" ? [...agent.messages] : newMessages;
3557
3674
  for (const message of messagesToProcess) if (message.role === "assistant") for (const toolCall of message.toolCalls || []) {
3558
3675
  const tool = this.getTool({
3559
3676
  toolName: toolCall.function.name,
@@ -3569,7 +3686,7 @@ var RunHandler = class {
3569
3686
  return wildcardTool;
3570
3687
  };
3571
3688
  const executableTool = tool ?? getWildcardTool();
3572
- if (toolExecutionMode === "human-in-the-loop" && (executableTool?.type !== "human-in-the-loop" || !executableTool.handler)) continue;
3689
+ if (toolExecutionMode === "restore" && (!executableTool?.handler || executableTool.type !== "human-in-the-loop" && executableTool.reconnectBehavior !== "resume-pending")) continue;
3573
3690
  const executionKey = JSON.stringify([threadId, toolCall.id]);
3574
3691
  if (signal?.aborted || agent.threadId !== threadId || executing.has(executionKey)) continue;
3575
3692
  const existingResultIndex = newMessages.findIndex((m) => m.role === "tool" && m.toolCallId === toolCall.id);
@@ -3600,14 +3717,14 @@ var RunHandler = class {
3600
3717
  if (signal?.aborted) abortInteraction();
3601
3718
  }
3602
3719
  const executionSignal = interactionController?.signal ?? signal;
3603
- const discardOnAbort = toolExecutionMode === "human-in-the-loop" || !!interactionController;
3720
+ const discardOnAbort = toolExecutionMode === "restore" || !!interactionController;
3604
3721
  try {
3605
3722
  if (tool) {
3606
- if (await this.executeSpecificTool(tool, toolCall, message, agent, agentId, executionSignal, discardOnAbort)) needsFollowUp = true;
3723
+ if (await this.executeSpecificTool(tool, toolCall, message, agent, agentId, executionSignal, discardOnAbort, toolExecutionMode === "restore")) needsFollowUp = true;
3607
3724
  } else {
3608
3725
  const fallbackTool = getWildcardTool();
3609
3726
  if (fallbackTool) {
3610
- if (await this.executeWildcardTool(fallbackTool, toolCall, message, agent, agentId, executionSignal, discardOnAbort)) needsFollowUp = true;
3727
+ if (await this.executeWildcardTool(fallbackTool, toolCall, message, agent, agentId, executionSignal, discardOnAbort, toolExecutionMode === "restore")) needsFollowUp = true;
3611
3728
  }
3612
3729
  }
3613
3730
  } finally {
@@ -3641,8 +3758,9 @@ var RunHandler = class {
3641
3758
  * Handles arg parsing, subscriber notifications, handler invocation, result stringification,
3642
3759
  * and error handling.
3643
3760
  */
3644
- async executeToolHandler({ tool, toolCall, agent, agentId, handlerArgs, toolType, messageId, signal = this._runAbortController?.signal, discardOnAbort = false }) {
3761
+ async executeToolHandler({ tool, toolCall, agent, agentId, handlerArgs, toolType, messageId, signal = this._runAbortController?.signal, discardOnAbort = false, isReplay = false }) {
3645
3762
  let toolCallResult = "";
3763
+ let toolCallText = "";
3646
3764
  let errorMessage;
3647
3765
  let isArgumentError = false;
3648
3766
  let parsedArgs;
@@ -3676,11 +3794,10 @@ var RunHandler = class {
3676
3794
  const result = await tool.handler(parsedArgs, {
3677
3795
  toolCall,
3678
3796
  agent,
3679
- signal
3797
+ signal,
3798
+ ...isReplay ? { isReplay } : {}
3680
3799
  });
3681
- if (result === void 0 || result === null) toolCallResult = "";
3682
- else if (typeof result === "string") toolCallResult = result;
3683
- else toolCallResult = JSON.stringify(result);
3800
+ ({content: toolCallResult, text: toolCallText} = toToolResultContent(result));
3684
3801
  } catch (error) {
3685
3802
  const handlerError = error instanceof Error ? error : new Error(String(error));
3686
3803
  errorMessage = handlerError.message;
@@ -3697,17 +3814,18 @@ var RunHandler = class {
3697
3814
  }
3698
3815
  });
3699
3816
  }
3700
- if (errorMessage) toolCallResult = `Error: ${errorMessage}`;
3817
+ if (errorMessage) toolCallResult = toolCallText = `Error: ${errorMessage}`;
3701
3818
  await this._internal.notifySubscribers((subscriber) => subscriber.onToolExecutionEnd?.({
3702
3819
  copilotkit: this.core,
3703
3820
  toolCallId: toolCall.id,
3704
3821
  agentId,
3705
3822
  toolName: toolCall.function.name,
3706
- result: errorMessage ? "" : toolCallResult,
3823
+ result: errorMessage ? "" : toolCallText,
3707
3824
  error: errorMessage
3708
3825
  }), "Subscriber onToolExecutionEnd error:");
3709
3826
  return {
3710
- result: toolCallResult,
3827
+ content: toolCallResult,
3828
+ result: toolCallText,
3711
3829
  error: errorMessage,
3712
3830
  isArgumentError
3713
3831
  };
@@ -3715,10 +3833,11 @@ var RunHandler = class {
3715
3833
  /**
3716
3834
  * Execute a specific tool
3717
3835
  */
3718
- async executeSpecificTool(tool, toolCall, message, agent, agentId, signal, discardOnAbort = false) {
3836
+ async executeSpecificTool(tool, toolCall, message, agent, agentId, signal, discardOnAbort = false, isReplay = false) {
3719
3837
  const threadId = agent.threadId;
3720
3838
  if (tool?.agentId && tool.agentId !== agent.agentId) return false;
3721
3839
  let handlerResult = {
3840
+ content: "",
3722
3841
  result: "",
3723
3842
  error: void 0,
3724
3843
  isArgumentError: false
@@ -3732,7 +3851,8 @@ var RunHandler = class {
3732
3851
  toolType: "specific",
3733
3852
  messageId: message.id,
3734
3853
  signal,
3735
- discardOnAbort
3854
+ discardOnAbort,
3855
+ isReplay
3736
3856
  });
3737
3857
  {
3738
3858
  const messageIndex = agent.messages.findIndex((m) => m.id === message.id);
@@ -3743,7 +3863,7 @@ var RunHandler = class {
3743
3863
  id: randomUUID$1(),
3744
3864
  role: "tool",
3745
3865
  toolCallId: toolCall.id,
3746
- content: handlerResult.result
3866
+ content: handlerResult.content
3747
3867
  };
3748
3868
  agent.messages.splice(insertAt, 0, toolMessage);
3749
3869
  if (!handlerResult.error && tool?.followUp !== false) return true;
@@ -3756,10 +3876,11 @@ var RunHandler = class {
3756
3876
  * specific tools, so this method keeps its own arg-wrapping logic rather than
3757
3877
  * delegating to `executeToolHandler`.
3758
3878
  */
3759
- async executeWildcardTool(wildcardTool, toolCall, message, agent, agentId, signal, discardOnAbort = false) {
3879
+ async executeWildcardTool(wildcardTool, toolCall, message, agent, agentId, signal, discardOnAbort = false, isReplay = false) {
3760
3880
  const threadId = agent.threadId;
3761
3881
  if (wildcardTool?.agentId && wildcardTool.agentId !== agent.agentId) return false;
3762
3882
  let toolCallResult = "";
3883
+ let toolCallText = "";
3763
3884
  let errorMessage;
3764
3885
  if (wildcardTool?.handler) {
3765
3886
  let parsedArgs;
@@ -3796,11 +3917,10 @@ var RunHandler = class {
3796
3917
  const result = await wildcardTool.handler(wildcardArgs, {
3797
3918
  toolCall,
3798
3919
  agent,
3799
- signal
3920
+ signal,
3921
+ ...isReplay ? { isReplay } : {}
3800
3922
  });
3801
- if (result === void 0 || result === null) toolCallResult = "";
3802
- else if (typeof result === "string") toolCallResult = result;
3803
- else toolCallResult = JSON.stringify(result);
3923
+ ({content: toolCallResult, text: toolCallText} = toToolResultContent(result));
3804
3924
  } catch (error) {
3805
3925
  const handlerError = error instanceof Error ? error : new Error(String(error));
3806
3926
  errorMessage = handlerError.message;
@@ -3817,13 +3937,13 @@ var RunHandler = class {
3817
3937
  }
3818
3938
  });
3819
3939
  }
3820
- if (errorMessage) toolCallResult = `Error: ${errorMessage}`;
3940
+ if (errorMessage) toolCallResult = toolCallText = `Error: ${errorMessage}`;
3821
3941
  await this._internal.notifySubscribers((subscriber) => subscriber.onToolExecutionEnd?.({
3822
3942
  copilotkit: this.core,
3823
3943
  toolCallId: toolCall.id,
3824
3944
  agentId,
3825
3945
  toolName: toolCall.function.name,
3826
- result: errorMessage ? "" : toolCallResult,
3946
+ result: errorMessage ? "" : toolCallText,
3827
3947
  error: errorMessage
3828
3948
  }), "Subscriber onToolExecutionEnd error:");
3829
3949
  }
@@ -3894,6 +4014,7 @@ var RunHandler = class {
3894
4014
  };
3895
4015
  agent.messages.push(assistantMessage);
3896
4016
  let handlerResult = {
4017
+ content: "",
3897
4018
  result: "",
3898
4019
  error: void 0,
3899
4020
  isArgumentError: false
@@ -3910,7 +4031,7 @@ var RunHandler = class {
3910
4031
  id: randomUUID$1(),
3911
4032
  role: "tool",
3912
4033
  toolCallId,
3913
- content: handlerResult.result
4034
+ content: handlerResult.content
3914
4035
  };
3915
4036
  const assistantIndex = agent.messages.findIndex((m) => m.id === assistantMessage.id);
3916
4037
  if (assistantIndex !== -1) agent.messages.splice(assistantIndex + 1, 0, toolResultMessage);
@@ -4511,6 +4632,750 @@ var ThreadStoreRegistry = class {
4511
4632
  }
4512
4633
  };
4513
4634
 
4635
+ //#endregion
4636
+ //#region src/core/trajectory-connection.ts
4637
+ const TIMEOUT_MS = 1e4;
4638
+ const BATCH_INTERVAL_MS = 2e3;
4639
+ const MAX_BATCH_EVENTS = 50;
4640
+ const MAX_BATCH_BYTES = 64 * 1024;
4641
+ const MAX_EVENT_BYTES = 16 * 1024;
4642
+ const byteLength = (value) => new TextEncoder().encode(JSON.stringify(value)).byteLength;
4643
+ const reconnectDelay = phoenixExponentialBackoff(1e3, 1e4);
4644
+ function isObject(value) {
4645
+ return typeof value === "object" && value !== null;
4646
+ }
4647
+ function isNonNegativeInteger(value) {
4648
+ return typeof value === "number" && Number.isSafeInteger(value) && value >= 0;
4649
+ }
4650
+ function isBatchAcknowledgement(value, eventCount) {
4651
+ return isObject(value) && isNonNegativeInteger(value.accepted) && isNonNegativeInteger(value.rejected) && value.accepted + value.rejected === eventCount && (value.highestSeq === null || isNonNegativeInteger(value.highestSeq)) && (value.accepted === 0 || value.highestSeq !== null);
4652
+ }
4653
+ function isGrant(value) {
4654
+ return isObject(value) && typeof value.joinToken === "string" && value.joinToken.length > 0 && isObject(value.realtime) && typeof value.realtime.clientUrl === "string" && value.realtime.clientUrl.length > 0 && typeof value.realtime.topic === "string" && value.realtime.topic.length > 0;
4655
+ }
4656
+ function errorCode(value, fallback) {
4657
+ return isObject(value) && typeof value.code === "string" && /^[A-Z][A-Z0-9_]{0,63}$/.test(value.code) ? value.code : fallback;
4658
+ }
4659
+ /** Owns bounded, connected-only capture batches. Failed batches are never resent. */
4660
+ var TrajectoryConnection = class {
4661
+ session;
4662
+ nextSeq = Math.floor(Date.now()) * 1e3;
4663
+ constructor(core, onChange) {
4664
+ this.core = core;
4665
+ this.onChange = onChange;
4666
+ }
4667
+ get trajectoryId() {
4668
+ return this.session?.ready ? this.session.trajectoryId : null;
4669
+ }
4670
+ start(trajectoryId, config) {
4671
+ if (this.session && (trajectoryId === void 0 || trajectoryId === this.session.trajectoryId)) return this.session.promise;
4672
+ this.stop();
4673
+ const id = trajectoryId ?? crypto.randomUUID();
4674
+ let resolveStart;
4675
+ const promise = new Promise((resolve) => {
4676
+ resolveStart = resolve;
4677
+ });
4678
+ const session = {
4679
+ trajectoryId: id,
4680
+ config,
4681
+ promise,
4682
+ resolve: resolveStart,
4683
+ retries: 0,
4684
+ queue: [],
4685
+ dropped: 0,
4686
+ ready: false,
4687
+ started: false,
4688
+ offline: typeof navigator !== "undefined" && navigator.onLine === false,
4689
+ onOffline: () => {
4690
+ if (this.session !== session) return;
4691
+ session.offline = true;
4692
+ if (session.connection) this.fail(session, session.connection, "CONNECTION_LOST");
4693
+ clearTimeout(session.retry);
4694
+ session.retry = void 0;
4695
+ },
4696
+ onOnline: () => {
4697
+ if (this.session !== session) return;
4698
+ session.offline = false;
4699
+ if (!session.connection) {
4700
+ clearTimeout(session.retry);
4701
+ session.retry = void 0;
4702
+ this.connect(session);
4703
+ }
4704
+ },
4705
+ linkedThreads: /* @__PURE__ */ new Set(),
4706
+ agentWatches: /* @__PURE__ */ new Map()
4707
+ };
4708
+ this.session = session;
4709
+ session.runWatch = this.core.subscribe({ onAgentRunStarted: ({ agent }) => this.watchAgent(session, agent) });
4710
+ if (typeof window !== "undefined" && typeof window.addEventListener === "function") {
4711
+ window.addEventListener("offline", session.onOffline);
4712
+ window.addEventListener("online", session.onOnline);
4713
+ }
4714
+ if (session.offline) {
4715
+ this.report(session, "CONNECTION_LOST");
4716
+ this.end(session, {
4717
+ status: "error",
4718
+ code: "CONNECTION_LOST"
4719
+ });
4720
+ } else this.connect(session);
4721
+ return promise;
4722
+ }
4723
+ stop() {
4724
+ const session = this.session;
4725
+ if (!session) return;
4726
+ if (session.ready && session.connection) this.flush(session, session.connection);
4727
+ this.end(session, {
4728
+ status: "error",
4729
+ code: "CANCELLED"
4730
+ });
4731
+ }
4732
+ emit(name, value) {
4733
+ const session = this.session;
4734
+ if (session?.ready) session.collector?.emit(name, value);
4735
+ else if (session?.started) this.addDropped(session, 1, false);
4736
+ }
4737
+ watchAgent(session, agent) {
4738
+ if (this.session !== session || session.agentWatches.has(agent)) return;
4739
+ session.agentWatches.set(agent, agent.subscribe({ onRunStartedEvent: ({ event }) => this.linkThread(session, event) }));
4740
+ }
4741
+ /** Sends one `thread.linked` per Thread. A link that cannot be sent waits for the next run. */
4742
+ linkThread(session, { threadId }) {
4743
+ const connection = session.connection;
4744
+ if (!session.ready || !connection || !threadId || session.linkedThreads.has(threadId)) return;
4745
+ session.linkedThreads.add(threadId);
4746
+ const event = {
4747
+ type: "CUSTOM",
4748
+ name: "thread.linked",
4749
+ timestamp: Date.now(),
4750
+ value: { threadId }
4751
+ };
4752
+ try {
4753
+ const { beforeSend } = session.config;
4754
+ const kept = beforeSend === void 0 ? event : beforeSend(event);
4755
+ if (kept !== null) this.send(session, connection, kept);
4756
+ } catch {
4757
+ this.report(session, "INVALID_EVENT");
4758
+ }
4759
+ }
4760
+ report(session, code) {
4761
+ try {
4762
+ session.config.onError?.({
4763
+ code,
4764
+ message: `Trajectory capture: ${code}.`
4765
+ });
4766
+ } catch {}
4767
+ }
4768
+ current(session, connection) {
4769
+ return this.session === session && session.connection === connection;
4770
+ }
4771
+ end(session, result) {
4772
+ if (this.session !== session) return;
4773
+ this.session = void 0;
4774
+ clearTimeout(session.retry);
4775
+ session.runWatch?.unsubscribe();
4776
+ for (const watch of session.agentWatches.values()) watch.unsubscribe();
4777
+ session.agentWatches.clear();
4778
+ session.collector?.stop();
4779
+ session.ready = false;
4780
+ if (session.queue.length > 0) this.report(session, "EVENTS_DROPPED");
4781
+ this.discardQueue(session);
4782
+ if (session.connection) {
4783
+ for (const pending of session.connection.pending) {
4784
+ pending.finish("PERSISTENCE_UNKNOWN");
4785
+ this.report(session, "PERSISTENCE_UNKNOWN");
4786
+ }
4787
+ this.cleanup(session, session.connection);
4788
+ }
4789
+ if (typeof window !== "undefined" && typeof window.removeEventListener === "function") {
4790
+ window.removeEventListener("offline", session.onOffline);
4791
+ window.removeEventListener("online", session.onOnline);
4792
+ }
4793
+ session.resolve(result);
4794
+ this.onChange();
4795
+ }
4796
+ cleanup(session, connection) {
4797
+ session.connection = void 0;
4798
+ clearTimeout(connection.timer);
4799
+ connection.abort.abort();
4800
+ for (const pending of connection.pending) clearTimeout(pending.timer);
4801
+ connection.pending.clear();
4802
+ const { socket, channel } = connection;
4803
+ try {
4804
+ if (channel) {
4805
+ for (const [event, ref] of connection.channelRefs) channel.off(event, ref);
4806
+ channel.leave();
4807
+ }
4808
+ } finally {
4809
+ if (socket) {
4810
+ socket.off(connection.socketRefs);
4811
+ socket.disconnect();
4812
+ }
4813
+ }
4814
+ }
4815
+ fail(session, connection, code, retryable = true) {
4816
+ if (!this.current(session, connection)) return;
4817
+ session.collector?.stop();
4818
+ session.collector = void 0;
4819
+ if (session.ready && session.started) session.promise = new Promise((resolve) => {
4820
+ session.resolve = resolve;
4821
+ });
4822
+ session.ready = false;
4823
+ this.discardQueue(session);
4824
+ session.linkedThreads.clear();
4825
+ for (const pending of connection.pending) pending.finish("PERSISTENCE_UNKNOWN");
4826
+ if (!this.current(session, connection)) return;
4827
+ this.cleanup(session, connection);
4828
+ this.onChange();
4829
+ this.report(session, code);
4830
+ if (this.session !== session) return;
4831
+ if (!session.started || !retryable) {
4832
+ this.end(session, {
4833
+ status: "error",
4834
+ code
4835
+ });
4836
+ return;
4837
+ }
4838
+ if (session.offline || this.session !== session) return;
4839
+ session.retry = setTimeout(() => {
4840
+ session.retry = void 0;
4841
+ if (this.session === session && !session.offline) this.connect(session);
4842
+ }, reconnectDelay(++session.retries));
4843
+ }
4844
+ async connect(session) {
4845
+ if (this.session !== session || session.offline || session.connection) return;
4846
+ const connection = {
4847
+ abort: new AbortController(),
4848
+ socketRefs: [],
4849
+ channelRefs: [],
4850
+ pending: /* @__PURE__ */ new Set()
4851
+ };
4852
+ session.connection = connection;
4853
+ connection.timer = setTimeout(() => this.fail(session, connection, "CONNECTION_TIMEOUT"), TIMEOUT_MS);
4854
+ try {
4855
+ const runtimeUrl = this.core.runtimeUrl;
4856
+ if (!runtimeUrl) {
4857
+ this.fail(session, connection, "RUNTIME_REQUIRED", false);
4858
+ return;
4859
+ }
4860
+ let transport = this.core.runtimeTransport;
4861
+ if (transport === "auto") {
4862
+ transport = await this.detectedTransport(connection.abort.signal);
4863
+ if (!this.current(session, connection)) return;
4864
+ }
4865
+ const rest = transport === "rest";
4866
+ const response = await fetch(rest ? `${runtimeUrl}/trajectory/${encodeURIComponent(session.trajectoryId)}/connect` : this.core.ɵruntimeEndpointUrl ?? runtimeUrl, {
4867
+ method: "POST",
4868
+ redirect: "error",
4869
+ signal: connection.abort.signal,
4870
+ headers: {
4871
+ ...this.core.headers,
4872
+ "Content-Type": "application/json"
4873
+ },
4874
+ credentials: this.core.credentials,
4875
+ body: JSON.stringify(rest ? {} : {
4876
+ method: "trajectory/connect",
4877
+ params: { trajectoryId: session.trajectoryId },
4878
+ body: {}
4879
+ })
4880
+ });
4881
+ if (!this.current(session, connection)) return;
4882
+ const payload = await response.json().catch(() => void 0);
4883
+ if (!this.current(session, connection)) return;
4884
+ if (!response.ok) {
4885
+ this.fail(session, connection, errorCode(payload, "CONNECTION_FAILED"), response.status >= 500 || response.status === 408 || response.status === 429);
4886
+ return;
4887
+ }
4888
+ if (!isGrant(payload)) {
4889
+ this.fail(session, connection, "INVALID_GRANT", false);
4890
+ return;
4891
+ }
4892
+ const socket = new Socket(payload.realtime.clientUrl, {
4893
+ params: { join_token: payload.joinToken },
4894
+ timeout: TIMEOUT_MS
4895
+ });
4896
+ connection.socket = socket;
4897
+ const lost = () => this.fail(session, connection, "CONNECTION_LOST");
4898
+ connection.socketRefs.push(socket.onError(lost), socket.onClose(lost));
4899
+ const channel = socket.channel(payload.realtime.topic, {});
4900
+ connection.channel = channel;
4901
+ connection.channelRefs.push(["phx_error", channel.onError(lost)], ["phx_close", channel.onClose(lost)]);
4902
+ socket.connect();
4903
+ if (!this.current(session, connection)) return;
4904
+ channel.join(TIMEOUT_MS).receive("ok", () => {
4905
+ if (!this.current(session, connection)) return;
4906
+ try {
4907
+ clearTimeout(connection.timer);
4908
+ session.ready = true;
4909
+ session.retries = 0;
4910
+ session.collector = createTrajectoryCollector({
4911
+ ...session.config,
4912
+ ignoreUrls: [
4913
+ ...session.config.ignoreUrls ?? [],
4914
+ ...this.core.runtimeUrl ? [this.core.runtimeUrl] : [],
4915
+ ...this.core.ɵruntimeEndpointUrl ? [this.core.ɵruntimeEndpointUrl] : [],
4916
+ "https://telemetry.copilotkit.ai/",
4917
+ "https://cdn.copilotkit.ai/"
4918
+ ],
4919
+ onError: (error) => {
4920
+ if (error.code === "CAPTURE_FAILED") this.fail(session, connection, error.code, false);
4921
+ else {
4922
+ if (error.code === "INVALID_EVENT" || error.code === "EVENT_TOO_LARGE") this.addDropped(session, 1);
4923
+ session.config.onError?.(error);
4924
+ }
4925
+ },
4926
+ send: (event) => this.send(session, connection, event)
4927
+ });
4928
+ session.collector.start();
4929
+ if (!this.current(session, connection)) return;
4930
+ if (session.dropped > 0) this.scheduleFlush(session, connection);
4931
+ session.started = true;
4932
+ session.resolve({
4933
+ status: "started",
4934
+ trajectoryId: session.trajectoryId
4935
+ });
4936
+ this.onChange();
4937
+ } catch {
4938
+ this.fail(session, connection, "CAPTURE_FAILED", false);
4939
+ }
4940
+ }).receive("error", (joinError) => this.fail(session, connection, errorCode(joinError, "JOIN_FAILED"))).receive("timeout", () => this.fail(session, connection, "CONNECTION_TIMEOUT"));
4941
+ } catch {
4942
+ this.fail(session, connection, "CONNECTION_FAILED");
4943
+ }
4944
+ }
4945
+ /** Resolves once auto-detection picks a transport, or with "auto" when the attempt is aborted. */
4946
+ detectedTransport(signal) {
4947
+ return new Promise((resolve) => {
4948
+ const finish = () => {
4949
+ subscription.unsubscribe();
4950
+ signal.removeEventListener("abort", finish);
4951
+ resolve(this.core.runtimeTransport);
4952
+ };
4953
+ const subscription = this.core.subscribe({ onRuntimeConnectionStatusChanged: () => {
4954
+ if (this.core.runtimeTransport !== "auto") finish();
4955
+ } });
4956
+ signal.addEventListener("abort", finish, { once: true });
4957
+ });
4958
+ }
4959
+ addDropped(session, count, schedule = true) {
4960
+ session.dropped = Math.min(Number.MAX_SAFE_INTEGER, session.dropped + count);
4961
+ if (schedule && count > 0 && session.ready && session.connection) this.scheduleFlush(session, session.connection);
4962
+ }
4963
+ scheduleFlush(session, connection) {
4964
+ if (session.flushTimer !== void 0 || !this.current(session, connection)) return;
4965
+ session.flushTimer = setTimeout(() => {
4966
+ session.flushTimer = void 0;
4967
+ this.flush(session, connection);
4968
+ }, BATCH_INTERVAL_MS);
4969
+ }
4970
+ discardQueue(session) {
4971
+ clearTimeout(session.flushTimer);
4972
+ session.flushTimer = void 0;
4973
+ this.addDropped(session, session.queue.length, false);
4974
+ session.queue = [];
4975
+ }
4976
+ connected(connection) {
4977
+ return connection.socket?.isConnected() === true && connection.channel?.state === "joined";
4978
+ }
4979
+ send(session, connection, event) {
4980
+ if (!this.current(session, connection) || !session.ready) return;
4981
+ if (!this.connected(connection)) {
4982
+ this.addDropped(session, 1);
4983
+ this.fail(session, connection, "CONNECTION_LOST");
4984
+ return;
4985
+ }
4986
+ if (!Number.isSafeInteger(this.nextSeq)) {
4987
+ this.addDropped(session, 1);
4988
+ this.report(session, "SEQUENCE_EXHAUSTED");
4989
+ return;
4990
+ }
4991
+ const original = event.value;
4992
+ const fields = isObject(original) && !Array.isArray(original) && !Object.hasOwn(original, "seq") ? original : { data: original };
4993
+ const wire = {
4994
+ ...event,
4995
+ value: {
4996
+ ...fields,
4997
+ seq: this.nextSeq++
4998
+ }
4999
+ };
5000
+ if (byteLength(wire) > MAX_EVENT_BYTES) {
5001
+ this.addDropped(session, 1);
5002
+ this.report(session, "EVENT_TOO_LARGE");
5003
+ return;
5004
+ }
5005
+ const fits = () => session.queue.length < MAX_BATCH_EVENTS && byteLength({
5006
+ events: [...session.queue, wire],
5007
+ dropped: Number.MAX_SAFE_INTEGER
5008
+ }) <= MAX_BATCH_BYTES;
5009
+ if (!fits()) this.flush(session, connection);
5010
+ if (!this.current(session, connection) || !session.ready) return;
5011
+ if (!fits()) {
5012
+ this.addDropped(session, 1);
5013
+ this.report(session, "EVENTS_DROPPED");
5014
+ return;
5015
+ }
5016
+ session.queue.push(wire);
5017
+ if (session.queue.length === MAX_BATCH_EVENTS) this.flush(session, connection);
5018
+ else this.scheduleFlush(session, connection);
5019
+ }
5020
+ flush(session, connection) {
5021
+ if (!this.current(session, connection) || !session.ready || session.queue.length === 0 && session.dropped === 0) return;
5022
+ if (!this.connected(connection)) {
5023
+ this.fail(session, connection, "CONNECTION_LOST");
5024
+ return;
5025
+ }
5026
+ if (connection.pending.size > 0) return;
5027
+ clearTimeout(session.flushTimer);
5028
+ session.flushTimer = void 0;
5029
+ const batch = {
5030
+ events: session.queue,
5031
+ dropped: session.dropped
5032
+ };
5033
+ session.queue = [];
5034
+ session.dropped = 0;
5035
+ let finished = false;
5036
+ const pending = {
5037
+ timer: setTimeout(() => pending.finish("PERSISTENCE_UNKNOWN"), TIMEOUT_MS),
5038
+ finish: (code, knownRollback = false, flushNext = true) => {
5039
+ if (finished) return;
5040
+ finished = true;
5041
+ clearTimeout(pending.timer);
5042
+ connection.pending.delete(pending);
5043
+ if (knownRollback) this.addDropped(session, batch.events.length + batch.dropped, false);
5044
+ if (code && this.current(session, connection)) this.report(session, code);
5045
+ if (flushNext && this.current(session, connection) && session.ready && (session.queue.length > 0 || !knownRollback && session.dropped > 0)) this.flush(session, connection);
5046
+ }
5047
+ };
5048
+ connection.pending.add(pending);
5049
+ try {
5050
+ connection.channel.push("events", batch, TIMEOUT_MS).receive("ok", (payload) => {
5051
+ pending.finish(!isBatchAcknowledgement(payload, batch.events.length) ? "PERSISTENCE_UNKNOWN" : payload.rejected > 0 ? "EVENTS_REJECTED" : void 0);
5052
+ }).receive("error", (payload) => {
5053
+ if (!this.current(session, connection)) return;
5054
+ const reason = isObject(payload) ? payload.reason : void 0;
5055
+ if (reason === "unauthorized" || reason === "trajectory_mismatch") {
5056
+ pending.finish(void 0, true, false);
5057
+ this.fail(session, connection, reason.toUpperCase(), false);
5058
+ return;
5059
+ }
5060
+ const rollback = typeof reason === "string" && [
5061
+ "invalid_batch",
5062
+ "batch_too_large",
5063
+ "batch_too_many_events",
5064
+ "trajectory_over_share",
5065
+ "trajectory_outbox_full",
5066
+ "unsupported_event"
5067
+ ].includes(reason);
5068
+ pending.finish(rollback ? reason.toUpperCase() : "PERSISTENCE_UNKNOWN", rollback);
5069
+ }).receive("timeout", () => pending.finish("PERSISTENCE_UNKNOWN"));
5070
+ } catch {
5071
+ pending.finish("PERSISTENCE_UNKNOWN");
5072
+ }
5073
+ }
5074
+ };
5075
+
5076
+ //#endregion
5077
+ //#region src/core/learning-bridge.ts
5078
+ const COPILOTKIT_OWN_URLS = ["https://telemetry.copilotkit.ai/", "https://cdn.copilotkit.ai/"];
5079
+ /**
5080
+ * Wires `@copilotkit/learning` into Core. Default capture uses Runtime and Phoenix;
5081
+ * explicit legacy sinks also use agent events and open-Thread attribution.
5082
+ */
5083
+ var LearningBridge = class {
5084
+ collector = null;
5085
+ agents = /* @__PURE__ */ new Map();
5086
+ openThreads = /* @__PURE__ */ new Set();
5087
+ toolCalls = /* @__PURE__ */ new Map();
5088
+ executingTools = /* @__PURE__ */ new Map();
5089
+ lastRunIds = /* @__PURE__ */ new WeakMap();
5090
+ lastLinkedThreadId = null;
5091
+ coreSubscription = null;
5092
+ seq = 0;
5093
+ activeConfig;
5094
+ connection;
5095
+ constructor(core, config, onChange = () => {}) {
5096
+ this.core = core;
5097
+ this.config = config;
5098
+ this.connection = new TrajectoryConnection(core, onChange);
5099
+ this.setConfig(config);
5100
+ }
5101
+ setConfig(config) {
5102
+ this.config = config;
5103
+ if (config === void 0) this.stop();
5104
+ }
5105
+ get isConfigured() {
5106
+ return this.config !== void 0;
5107
+ }
5108
+ get isLegacyCaptureActive() {
5109
+ return this.collector !== null && this.collector.trajectoryId !== null;
5110
+ }
5111
+ startTracking() {
5112
+ if (this.coreSubscription !== null || !this.isLegacyCaptureActive) return;
5113
+ this.coreSubscription = this.core.subscribe({
5114
+ onAgentsChanged: ({ agents }) => {
5115
+ for (const agent of Object.values(agents)) this.track(agent);
5116
+ },
5117
+ onAgentRunStarted: ({ agent }) => this.track(agent),
5118
+ onToolExecutionStart: ({ toolCallId, agentId, toolName }) => {
5119
+ if (!this.isLegacyCaptureActive) return;
5120
+ this.executingTools.set(toolCallId, Date.now());
5121
+ this.emitToolCall("executing", toolCallId, agentId, toolName, {});
5122
+ },
5123
+ onToolExecutionEnd: ({ toolCallId, agentId, toolName, error }) => {
5124
+ if (!this.isLegacyCaptureActive) return;
5125
+ const startedAt = this.executingTools.get(toolCallId);
5126
+ this.executingTools.delete(toolCallId);
5127
+ this.emitToolCall("completed", toolCallId, agentId, toolName, {
5128
+ durationMs: startedAt === void 0 ? void 0 : Date.now() - startedAt,
5129
+ outcome: error === void 0 ? "ok" : "error"
5130
+ });
5131
+ this.toolCalls.delete(toolCallId);
5132
+ }
5133
+ });
5134
+ for (const agent of Object.values(this.core.agents)) this.track(agent);
5135
+ }
5136
+ get trajectoryId() {
5137
+ return this.connection.trajectoryId ?? this.collector?.trajectoryId ?? null;
5138
+ }
5139
+ start(options = {}) {
5140
+ const config = this.config;
5141
+ if (config === void 0) {
5142
+ console.warn("[CopilotKit] startTrajectory() needs the `learning` option on CopilotKitCore (or the CopilotKitProvider `learning` prop).");
5143
+ return Promise.resolve({
5144
+ status: "error",
5145
+ code: "LEARNING_DISABLED"
5146
+ });
5147
+ }
5148
+ if (config.sink === void 0) {
5149
+ if (options.learningContainerIds !== void 0) console.warn("[CopilotKit] learningContainerIds is supported only with a custom sink. Authenticated Trajectory capture does not assign Learning Containers; remove learningContainerIds from the capture options.");
5150
+ this.stopLegacy();
5151
+ return this.connection.start(options.trajectoryId, config);
5152
+ }
5153
+ this.connection.stop();
5154
+ const trajectoryId = options.trajectoryId ?? this.collector?.trajectoryId ?? crypto.randomUUID();
5155
+ if (this.collector === null) {
5156
+ this.activeConfig = config;
5157
+ const runtimeUrl = this.core.runtimeUrl;
5158
+ this.collector = createCollector({
5159
+ ...config,
5160
+ beforeSend: (event) => {
5161
+ const sequenced = {
5162
+ ...event,
5163
+ value: {
5164
+ ...event.value,
5165
+ seq: ++this.seq
5166
+ }
5167
+ };
5168
+ return config.beforeSend === void 0 ? sequenced : config.beforeSend(sequenced);
5169
+ },
5170
+ ignoreUrls: [
5171
+ ...config.ignoreUrls ?? [],
5172
+ ...COPILOTKIT_OWN_URLS,
5173
+ ...runtimeUrl === void 0 ? [] : [runtimeUrl]
5174
+ ],
5175
+ enrich: (target) => this.enrich(target)
5176
+ });
5177
+ }
5178
+ const wasActive = this.collector.trajectoryId !== null;
5179
+ this.collector.start({
5180
+ ...options,
5181
+ trajectoryId
5182
+ });
5183
+ if (wasActive) return Promise.resolve(this.collector.trajectoryId === trajectoryId ? {
5184
+ status: "started",
5185
+ trajectoryId
5186
+ } : {
5187
+ status: "error",
5188
+ code: "TRAJECTORY_ACTIVE"
5189
+ });
5190
+ this.startTracking();
5191
+ this.lastLinkedThreadId = null;
5192
+ for (const threadId of this.distinctOpenThreadIds()) this.emitThreadLinked(threadId, "start");
5193
+ return Promise.resolve({
5194
+ status: "started",
5195
+ trajectoryId
5196
+ });
5197
+ }
5198
+ stop() {
5199
+ this.connection.stop();
5200
+ this.stopLegacy();
5201
+ }
5202
+ stopLegacy() {
5203
+ this.collector?.stop();
5204
+ this.collector = null;
5205
+ this.activeConfig = void 0;
5206
+ this.coreSubscription?.unsubscribe();
5207
+ this.coreSubscription = null;
5208
+ for (const subscription of this.agents.values()) subscription.unsubscribe();
5209
+ this.agents.clear();
5210
+ this.toolCalls.clear();
5211
+ this.executingTools.clear();
5212
+ }
5213
+ /** A developer event. Only an explicit legacy sink adds open-Thread context. */
5214
+ emit(name, value) {
5215
+ this.connection.emit(name, value);
5216
+ if (value !== null && typeof value === "object" && !Array.isArray(value)) this.collector?.emit(name, {
5217
+ ...this.openThreadContext(),
5218
+ ...value
5219
+ });
5220
+ }
5221
+ registerOpenThread(params) {
5222
+ const entry = { ...params };
5223
+ const isNew = !this.distinctOpenThreadIds().includes(entry.threadId);
5224
+ this.openThreads.add(entry);
5225
+ if (isNew) this.emitThreadLinked(entry.threadId, "open");
5226
+ return {
5227
+ update: (threadId) => {
5228
+ if (threadId === entry.threadId) return;
5229
+ entry.threadId = threadId;
5230
+ if (![...this.openThreads].filter((other) => other !== entry).some((other) => other.threadId === threadId)) this.emitThreadLinked(threadId, "switch");
5231
+ },
5232
+ unregister: () => {
5233
+ this.openThreads.delete(entry);
5234
+ }
5235
+ };
5236
+ }
5237
+ distinctOpenThreadIds() {
5238
+ return [...new Set([...this.openThreads].map((open) => open.threadId))];
5239
+ }
5240
+ emitThreadLinked(threadId, reason) {
5241
+ if (!(this.collector !== null && this.collector.trajectoryId !== null) || threadId === this.lastLinkedThreadId) return;
5242
+ this.lastLinkedThreadId = threadId;
5243
+ const owner = [...this.openThreads].find((open) => open.threadId === threadId);
5244
+ const agent = this.findAgentForThread(threadId);
5245
+ this.collector?.ɵemit("thread.linked", {
5246
+ threadId,
5247
+ agentId: owner?.agentId ?? agent?.agentId ?? null,
5248
+ hasMessages: (agent?.messages.length ?? 0) > 0,
5249
+ reason
5250
+ });
5251
+ }
5252
+ findAgentForThread(threadId) {
5253
+ return [...this.agents.keys()].find((agent) => agent.threadId === threadId);
5254
+ }
5255
+ track(agent) {
5256
+ const agentId = agent.agentId;
5257
+ if (!this.isLegacyCaptureActive || this.agents.has(agent) || agentId === void 0) return;
5258
+ this.agents.set(agent, agent.subscribe(this.agentSubscriber(agent, agentId)));
5259
+ }
5260
+ agentSubscriber(agent, agentId) {
5261
+ const runIdFor = () => this.lastRunIds.get(agent);
5262
+ return {
5263
+ onRunStartedEvent: ({ event }) => {
5264
+ this.lastRunIds.set(agent, event.runId);
5265
+ this.emitRun("started", agent, event.threadId, event.runId);
5266
+ },
5267
+ onRunFinishedEvent: ({ event }) => {
5268
+ this.emitRun("finished", agent, event.threadId, event.runId);
5269
+ },
5270
+ onRunErrorEvent: () => {
5271
+ this.emitRun("error", agent, agent.threadId, runIdFor() ?? null);
5272
+ },
5273
+ onToolCallStartEvent: ({ event }) => {
5274
+ if (!this.isLegacyCaptureActive) return;
5275
+ const origin = {
5276
+ agentId,
5277
+ threadId: agent.threadId,
5278
+ messageId: event.parentMessageId,
5279
+ runId: runIdFor()
5280
+ };
5281
+ this.toolCalls.set(event.toolCallId, origin);
5282
+ this.emitToolCall("started", event.toolCallId, origin.agentId, event.toolCallName, {});
5283
+ },
5284
+ onToolCallResultEvent: ({ event }) => {
5285
+ if (!this.executingTools.has(event.toolCallId)) this.toolCalls.delete(event.toolCallId);
5286
+ },
5287
+ onNewMessage: ({ message }) => this.emitMessage(agent, message)
5288
+ };
5289
+ }
5290
+ emitRun(phase, agent, threadId, runId) {
5291
+ this.collector?.ɵemit("agent.run", {
5292
+ phase,
5293
+ runId,
5294
+ threadId,
5295
+ agentId: agent.agentId
5296
+ });
5297
+ }
5298
+ emitMessage(agent, message) {
5299
+ const content = typeof message.content === "string" ? message.content : "";
5300
+ const includeText = this.activeConfig?.capture?.agentText !== false && typeof message.content === "string";
5301
+ this.collector?.ɵemit("agent.message", {
5302
+ messageId: message.id,
5303
+ runId: this.runIdForMessage(agent, message.id),
5304
+ threadId: agent.threadId,
5305
+ agentId: agent.agentId,
5306
+ role: message.role,
5307
+ textLength: content.length,
5308
+ ...includeText ? { text: content } : {}
5309
+ });
5310
+ }
5311
+ emitToolCall(phase, toolCallId, agentId, toolName, extra) {
5312
+ const origin = this.toolCalls.get(toolCallId);
5313
+ this.collector?.ɵemit("tool.call", {
5314
+ phase,
5315
+ toolCallId,
5316
+ toolName,
5317
+ agentId,
5318
+ threadId: origin?.threadId ?? null,
5319
+ messageId: origin?.messageId,
5320
+ runId: origin?.runId,
5321
+ ...extra
5322
+ });
5323
+ }
5324
+ runIdForMessage(agent, messageId) {
5325
+ const agentId = agent.agentId;
5326
+ if (agentId === void 0) return null;
5327
+ return this.core.getRunIdForMessage(agentId, agent.threadId, messageId) ?? this.lastRunIds.get(agent) ?? null;
5328
+ }
5329
+ findMessage(messageId) {
5330
+ for (const agent of this.agents.keys()) {
5331
+ const message = agent.messages.find((candidate) => candidate.id === messageId);
5332
+ if (message !== void 0) return {
5333
+ agent,
5334
+ message
5335
+ };
5336
+ }
5337
+ return null;
5338
+ }
5339
+ toolStatus(agent, toolCallId) {
5340
+ if (this.executingTools.has(toolCallId)) return "executing";
5341
+ return agent.messages.some((message) => message.role === "tool" && message.toolCallId === toolCallId) ? "complete" : "inProgress";
5342
+ }
5343
+ /** Adds agent context to a click: exact message first, then the single open Thread. */
5344
+ enrich(target) {
5345
+ const messageId = target.closest("[data-message-id]")?.getAttribute("data-message-id") ?? null;
5346
+ const toolCallId = target.closest("[data-tool-call-id]")?.getAttribute("data-tool-call-id") ?? null;
5347
+ const found = messageId === null ? null : this.findMessage(messageId);
5348
+ if (found !== null) {
5349
+ const { agent, message } = found;
5350
+ const toolCall = toolCallId !== null && message.role === "assistant" ? message.toolCalls?.find((call) => call.id === toolCallId) : void 0;
5351
+ return {
5352
+ threadId: agent.threadId,
5353
+ agentId: agent.agentId,
5354
+ messageId: message.id,
5355
+ runId: this.runIdForMessage(agent, message.id),
5356
+ ...toolCall === void 0 ? {} : {
5357
+ toolCallId: toolCall.id,
5358
+ toolName: toolCall.function.name,
5359
+ toolStatus: this.toolStatus(agent, toolCall.id)
5360
+ }
5361
+ };
5362
+ }
5363
+ return this.openThreadContext();
5364
+ }
5365
+ /** The single open Thread, or `null` with the candidates when several are open. */
5366
+ openThreadContext() {
5367
+ const openIds = this.distinctOpenThreadIds();
5368
+ if (openIds.length > 1) return {
5369
+ threadId: null,
5370
+ threadAmbiguity: {
5371
+ reason: "multiple-open",
5372
+ candidates: openIds
5373
+ }
5374
+ };
5375
+ return { threadId: openIds[0] ?? null };
5376
+ }
5377
+ };
5378
+
4514
5379
  //#endregion
4515
5380
  //#region src/utils/micro-redux.ts
4516
5381
  const INTERNAL_ACTION_TYPES = {
@@ -5663,6 +6528,9 @@ var CopilotKitCore = class {
5663
6528
  runHandler;
5664
6529
  stateManager;
5665
6530
  threadStoreRegistry;
6531
+ learningBridge;
6532
+ notifiedTrajectoryId = null;
6533
+ learningConfiguredListeners = /* @__PURE__ */ new Set();
5666
6534
  /**
5667
6535
  * The single core-owned memory store, created lazily on first
5668
6536
  * `getMemoryStore()` and kept user-scoped for the lifetime of the core.
@@ -5677,7 +6545,7 @@ var CopilotKitCore = class {
5677
6545
  * store that was registered prior to that initial notification.
5678
6546
  */
5679
6547
  previousAgentIds = /* @__PURE__ */ new Set();
5680
- constructor({ runtimeUrl, runtimeTransport = "auto", deferInitialConnection = false, headers = {}, credentials, messageFilter, properties = {}, agents__unsafe_dev_only = {}, tools = [], suggestionsConfig = [], debug }) {
6548
+ constructor({ runtimeUrl, runtimeTransport = "auto", deferInitialConnection = false, headers = {}, credentials, messageFilter, properties = {}, agents__unsafe_dev_only = {}, tools = [], suggestionsConfig = [], debug, learning }) {
5681
6549
  this._headers = normalizeHeaders(headers);
5682
6550
  this._credentials = credentials;
5683
6551
  this._messageFilter = messageFilter;
@@ -5693,6 +6561,7 @@ var CopilotKitCore = class {
5693
6561
  this.runHandler.initialize(tools);
5694
6562
  this.suggestionEngine.initialize(suggestionsConfig);
5695
6563
  this.stateManager.initialize();
6564
+ this.learningBridge = new LearningBridge(this, learning, () => this.notifyTrajectoryChanged());
5696
6565
  this.agentRegistry.setRuntimeTransport(runtimeTransport);
5697
6566
  this.agentRegistry.setRuntimeUrl(runtimeUrl, { deferConnection: deferInitialConnection });
5698
6567
  this.previousAgentIds = new Set(Object.keys(agents__unsafe_dev_only));
@@ -5844,6 +6713,10 @@ var CopilotKitCore = class {
5844
6713
  get runtimeConnectionStatus() {
5845
6714
  return this.agentRegistry.runtimeConnectionStatus;
5846
6715
  }
6716
+ /** @internal The verbatim single-endpoint URL, including any trailing slash. */
6717
+ get ɵruntimeEndpointUrl() {
6718
+ return this.agentRegistry.runtimeEndpointUrl;
6719
+ }
5847
6720
  get ɵruntimeFetch() {
5848
6721
  return this.agentRegistry.createRuntimeFetch();
5849
6722
  }
@@ -6003,6 +6876,83 @@ var CopilotKitCore = class {
6003
6876
  getAgent(id) {
6004
6877
  return this.agentRegistry.getAgent(id);
6005
6878
  }
6879
+ /** Updates settings for the next capture. Removing the config stops capture. */
6880
+ setLearningConfig(config) {
6881
+ const wasConfigured = this.ɵlearningConfigured;
6882
+ this.learningBridge.setConfig(config);
6883
+ this.notifyTrajectoryChanged();
6884
+ if (this.ɵlearningConfigured === wasConfigured) return;
6885
+ for (const listener of this.learningConfiguredListeners) try {
6886
+ listener();
6887
+ } catch (error) {
6888
+ console.error("Learning configured listener error:", error);
6889
+ }
6890
+ }
6891
+ /**
6892
+ * @internal Whether a `learning` config is set. Unlike {@link trajectoryId},
6893
+ * it does not change with capture start, connection failures or reconnects.
6894
+ */
6895
+ get ɵlearningConfigured() {
6896
+ return this.learningBridge.isConfigured;
6897
+ }
6898
+ /** @internal Calls `listener` when {@link ɵlearningConfigured} changes. */
6899
+ ɵsubscribeToLearningConfigured(listener) {
6900
+ this.learningConfiguredListeners.add(listener);
6901
+ return () => {
6902
+ this.learningConfiguredListeners.delete(listener);
6903
+ };
6904
+ }
6905
+ /** The active capture's Trajectory ID, or null while capture is stopped. */
6906
+ get trajectoryId() {
6907
+ return this.learningBridge.trajectoryId;
6908
+ }
6909
+ notifyTrajectoryChanged() {
6910
+ const trajectoryId = this.trajectoryId;
6911
+ if (trajectoryId === this.notifiedTrajectoryId) return;
6912
+ this.notifiedTrajectoryId = trajectoryId;
6913
+ this.notifySubscribers((subscriber) => subscriber.onTrajectoryChanged?.({
6914
+ copilotkit: this,
6915
+ trajectoryId
6916
+ }), "Subscriber onTrajectoryChanged error:");
6917
+ }
6918
+ /**
6919
+ * Starts one Trajectory after Runtime authorization and the Gateway join.
6920
+ * Omit the ID to create one. Repeated starts await the active connection;
6921
+ * a different ID stops the previous capture. Explicit sinks retain the
6922
+ * prototype's synchronous capture and require a stop before changing IDs.
6923
+ *
6924
+ * @example copilotkit.startTrajectory({ trajectoryId: crypto.randomUUID() })
6925
+ */
6926
+ startTrajectory(options = {}) {
6927
+ const result = this.learningBridge.start(options);
6928
+ this.notifyTrajectoryChanged();
6929
+ return result;
6930
+ }
6931
+ /** Cancels pending work and stops capture, attempting one final batch without waiting for its ACK. */
6932
+ stopTrajectory() {
6933
+ this.learningBridge.stop();
6934
+ this.notifyTrajectoryChanged();
6935
+ }
6936
+ /**
6937
+ * Records an outcome that clicks cannot show, such as a saved report or an
6938
+ * approved deal. The wire adds `value.seq`; arrays, primitives, and objects
6939
+ * already containing `seq` are preserved under `value.data`. Explicit legacy
6940
+ * sinks retain object-only events with open-Thread enrichment.
6941
+ * No-op while no Trajectory runs. Events emitted during connection recovery
6942
+ * count as dropped. Built-in names such as `click` are rejected.
6943
+ *
6944
+ * @example copilotkit.emitTrajectoryEvent("deal.approved", { dealId: "deal-1" })
6945
+ */
6946
+ emitTrajectoryEvent(name, value = {}) {
6947
+ this.learningBridge.emit(name, value);
6948
+ }
6949
+ /**
6950
+ * Tells Core that a view shows this Thread, so captured interactions can link to it.
6951
+ * Framework bindings call this; call it yourself only without a CopilotKit provider.
6952
+ */
6953
+ registerOpenThread(params) {
6954
+ return this.learningBridge.registerOpenThread(params);
6955
+ }
6006
6956
  /**
6007
6957
  * Re-apply the current headers to a single agent (delegated to
6008
6958
  * AgentRegistry). Core headers are merged on top of the agent's own