@copilotkit/runtime 1.9.1 → 1.9.2-next.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.
Files changed (47) hide show
  1. package/CHANGELOG.md +16 -0
  2. package/dist/{chunk-2CND6WGV.mjs → chunk-C3SWOFLO.mjs} +2 -2
  3. package/dist/{chunk-EVF3MXEK.mjs → chunk-KPFOAXRX.mjs} +2 -2
  4. package/dist/{chunk-RVLXQ2V5.mjs → chunk-RIPQZJB5.mjs} +2 -2
  5. package/dist/{chunk-GOPTDPPB.mjs → chunk-XGBY45FP.mjs} +265 -61
  6. package/dist/chunk-XGBY45FP.mjs.map +1 -0
  7. package/dist/{chunk-BHNTR222.mjs → chunk-YV3YXRMR.mjs} +2 -2
  8. package/dist/index.d.ts +1 -1
  9. package/dist/index.js +310 -106
  10. package/dist/index.js.map +1 -1
  11. package/dist/index.mjs +5 -5
  12. package/dist/lib/index.d.ts +1 -1
  13. package/dist/lib/index.js +303 -99
  14. package/dist/lib/index.js.map +1 -1
  15. package/dist/lib/index.mjs +5 -5
  16. package/dist/lib/integrations/index.d.ts +2 -2
  17. package/dist/lib/integrations/index.js +16 -7
  18. package/dist/lib/integrations/index.js.map +1 -1
  19. package/dist/lib/integrations/index.mjs +4 -4
  20. package/dist/lib/integrations/nest/index.d.ts +1 -1
  21. package/dist/lib/integrations/nest/index.js +16 -7
  22. package/dist/lib/integrations/nest/index.js.map +1 -1
  23. package/dist/lib/integrations/nest/index.mjs +2 -2
  24. package/dist/lib/integrations/node-express/index.d.ts +1 -1
  25. package/dist/lib/integrations/node-express/index.js +16 -7
  26. package/dist/lib/integrations/node-express/index.js.map +1 -1
  27. package/dist/lib/integrations/node-express/index.mjs +2 -2
  28. package/dist/lib/integrations/node-http/index.d.ts +1 -1
  29. package/dist/lib/integrations/node-http/index.js +16 -7
  30. package/dist/lib/integrations/node-http/index.js.map +1 -1
  31. package/dist/lib/integrations/node-http/index.mjs +1 -1
  32. package/dist/{shared-0c31d7c5.d.ts → shared-e272b15a.d.ts} +1 -0
  33. package/package.json +2 -2
  34. package/src/agents/langgraph/event-source.ts +10 -1
  35. package/src/graphql/resolvers/copilot.resolver.ts +13 -0
  36. package/src/graphql/resolvers/state.resolver.ts +5 -7
  37. package/src/lib/runtime/copilot-runtime.ts +70 -6
  38. package/src/lib/runtime/remote-action-constructors.ts +37 -28
  39. package/src/lib/runtime/remote-lg-action.ts +45 -24
  40. package/src/lib/runtime/retry-utils.ts +96 -0
  41. package/src/lib/streaming.ts +54 -1
  42. package/src/service-adapters/events.ts +101 -6
  43. package/dist/chunk-GOPTDPPB.mjs.map +0 -1
  44. /package/dist/{chunk-2CND6WGV.mjs.map → chunk-C3SWOFLO.mjs.map} +0 -0
  45. /package/dist/{chunk-EVF3MXEK.mjs.map → chunk-KPFOAXRX.mjs.map} +0 -0
  46. /package/dist/{chunk-RVLXQ2V5.mjs.map → chunk-RIPQZJB5.mjs.map} +0 -0
  47. /package/dist/{chunk-BHNTR222.mjs.map → chunk-YV3YXRMR.mjs.map} +0 -0
@@ -1,6 +1,6 @@
1
1
  import {
2
2
  copilotRuntimeNodeHttpEndpoint
3
- } from "../../../chunk-GOPTDPPB.mjs";
3
+ } from "../../../chunk-XGBY45FP.mjs";
4
4
  import "../../../chunk-IIXJVVTV.mjs";
5
5
  import "../../../chunk-5BIEM2UU.mjs";
6
6
  import "../../../chunk-SHBDMA63.mjs";
@@ -360,6 +360,7 @@ declare class CopilotRuntime<const T extends Parameter[] | [] = []> {
360
360
  private processAgentRequest;
361
361
  private getServerSideActions;
362
362
  private detectProvider;
363
+ private convertStreamingErrorToStructured;
363
364
  }
364
365
  declare function flattenToolCallsNoDuplicates(toolsByPriority: ActionInput[]): ActionInput[];
365
366
  declare function copilotKitEndpoint(config: Omit<CopilotKitEndpoint, "type">): CopilotKitEndpoint;
package/package.json CHANGED
@@ -9,7 +9,7 @@
9
9
  "publishConfig": {
10
10
  "access": "public"
11
11
  },
12
- "version": "1.9.1",
12
+ "version": "1.9.2-next.1",
13
13
  "sideEffects": false,
14
14
  "main": "./dist/index.js",
15
15
  "module": "./dist/index.mjs",
@@ -66,7 +66,7 @@
66
66
  "rxjs": "^7.8.1",
67
67
  "type-graphql": "2.0.0-rc.1",
68
68
  "zod": "^3.23.3",
69
- "@copilotkit/shared": "1.9.1"
69
+ "@copilotkit/shared": "1.9.2-next.1"
70
70
  },
71
71
  "peerDependencies": {
72
72
  "@ag-ui/client": ">=0.0.28",
@@ -5,7 +5,7 @@ import {
5
5
  RuntimeEventTypes,
6
6
  RuntimeMetaEventName,
7
7
  } from "../../service-adapters/events";
8
- import { randomId } from "@copilotkit/shared";
8
+ import { randomId, CopilotKitError } from "@copilotkit/shared";
9
9
 
10
10
  interface LangGraphEventWithState {
11
11
  event: LangGraphEvent | null;
@@ -284,6 +284,15 @@ export class RemoteLangGraphEventSource {
284
284
  }),
285
285
  catchError((error) => {
286
286
  console.error(error);
287
+
288
+ // If it's a structured CopilotKitError, re-throw it to be handled by the frontend error system
289
+ if (
290
+ error instanceof CopilotKitError ||
291
+ (error?.name && error.name.includes("CopilotKit"))
292
+ ) {
293
+ throw error;
294
+ }
295
+
287
296
  const events: RuntimeEvent[] = [];
288
297
 
289
298
  if (lastEventWithState?.lastMessageId && !lastEventWithState.isToolCall) {
@@ -54,6 +54,7 @@ import telemetry from "../../lib/telemetry-client";
54
54
  import { randomId } from "@copilotkit/shared";
55
55
  import { AgentsResponse } from "../types/agents-response.type";
56
56
  import { LangGraphEventTypes } from "../../agents/langgraph/events";
57
+ import { CopilotKitError } from "@copilotkit/shared";
57
58
 
58
59
  const invokeGuardrails = async ({
59
60
  baseUrl,
@@ -646,6 +647,18 @@ export class CopilotResolver {
646
647
  },
647
648
  error: (err) => {
648
649
  logger.error({ err }, "Error in event stream");
650
+
651
+ // If it's a structured CopilotKitError, stop the repeater with the error so frontend can handle it
652
+ if (
653
+ err instanceof CopilotKitError ||
654
+ (err instanceof Error && err.name && err.name.includes("CopilotKit"))
655
+ ) {
656
+ eventStreamSubscription?.unsubscribe();
657
+ rejectOutputMessagesPromise(err);
658
+ stopStreamingMessages(err); // Pass the error to stop the GraphQL stream with this error
659
+ return;
660
+ }
661
+
649
662
  responseStatus$.next(
650
663
  new UnknownErrorResponse({
651
664
  description: `An unknown error has occurred in the event stream`,
@@ -4,6 +4,7 @@ import { Query } from "type-graphql";
4
4
  import { LoadAgentStateResponse } from "../types/load-agent-state-response.type";
5
5
  import type { GraphQLContext } from "../../lib/integrations";
6
6
  import { LoadAgentStateInput } from "../inputs/load-agent-state.input";
7
+ import { CopilotKitAgentDiscoveryError } from "@copilotkit/shared";
7
8
 
8
9
  @Resolver(() => LoadAgentStateResponse)
9
10
  export class StateResolver {
@@ -11,14 +12,11 @@ export class StateResolver {
11
12
  async loadAgentState(@Ctx() ctx: GraphQLContext, @Arg("data") data: LoadAgentStateInput) {
12
13
  const agents = await ctx._copilotkit.runtime.discoverAgentsFromEndpoints(ctx);
13
14
  const agent = agents.find((agent) => agent.name === data.agentName);
14
-
15
15
  if (!agent) {
16
- return {
17
- threadId: data.threadId || "",
18
- threadExists: false,
19
- state: JSON.stringify({}),
20
- messages: JSON.stringify([]),
21
- };
16
+ throw new CopilotKitAgentDiscoveryError({
17
+ agentName: data.agentName,
18
+ availableAgents: agents.map((a) => ({ name: a.name, id: a.name })),
19
+ });
22
20
  }
23
21
 
24
22
  const state = await ctx._copilotkit.runtime.loadAgentState(ctx, data.threadId, data.agentName);
@@ -20,9 +20,11 @@ import {
20
20
  CopilotKitApiDiscoveryError,
21
21
  randomId,
22
22
  CopilotKitError,
23
- CopilotKitLowLevelError,
23
+ CopilotKitRemoteEndpointDiscoveryError,
24
24
  CopilotKitAgentDiscoveryError,
25
25
  CopilotKitMisuseError,
26
+ CopilotKitErrorCode,
27
+ CopilotKitLowLevelError,
26
28
  } from "@copilotkit/shared";
27
29
  import {
28
30
  CopilotServiceAdapter,
@@ -54,6 +56,7 @@ import { from } from "rxjs";
54
56
  import { AgentStateInput } from "../../graphql/inputs/agent-state.input";
55
57
  import { ActionInputAvailability } from "../../graphql/types/enums";
56
58
  import { createHeaders } from "./remote-action-constructors";
59
+ import { fetchWithRetry } from "./retry-utils";
57
60
  import { Agent } from "../../graphql/types/agents-response.type";
58
61
  import { ExtensionsInput } from "../../graphql/inputs/extensions.input";
59
62
  import { ExtensionsResponse } from "../../graphql/types/extensions-response.type";
@@ -450,6 +453,14 @@ export class CopilotRuntime<const T extends Parameter[] | [] = []> {
450
453
  const streamedChunks: any[] = [];
451
454
 
452
455
  try {
456
+ if (
457
+ Object.keys(this.agents).length &&
458
+ agentSession?.agentName &&
459
+ !this.delegateAgentProcessingToServiceAdapter
460
+ ) {
461
+ this.agents = { [agentSession.agentName]: this.agents[agentSession.agentName] };
462
+ }
463
+
453
464
  if (agentSession && !this.delegateAgentProcessingToServiceAdapter) {
454
465
  return await this.processAgentRequest(request);
455
466
  }
@@ -667,9 +678,11 @@ please use an LLM adapter instead.`,
667
678
  if (error instanceof CopilotKitError) {
668
679
  throw error;
669
680
  }
681
+
682
+ // Convert non-CopilotKitErrors to structured errors
670
683
  console.error("Error getting response:", error);
671
- eventSource.sendErrorMessageToChat();
672
- throw error;
684
+ const structuredError = this.convertStreamingErrorToStructured(error);
685
+ throw structuredError;
673
686
  }
674
687
  }
675
688
 
@@ -721,7 +734,7 @@ please use an LLM adapter instead.`,
721
734
  const cpkEndpoint = endpoint as CopilotKitEndpoint;
722
735
  const fetchUrl = `${endpoint.url}/info`;
723
736
  try {
724
- const response = await fetch(fetchUrl, {
737
+ const response = await fetchWithRetry(fetchUrl, {
725
738
  method: "POST",
726
739
  headers: createHeaders(cpkEndpoint.onBeforeRequest, graphqlContext),
727
740
  body: JSON.stringify({ properties: graphqlContext.properties }),
@@ -810,7 +823,7 @@ please use an LLM adapter instead.`,
810
823
  const cpkEndpoint = agentWithEndpoint.endpoint as CopilotKitEndpoint;
811
824
  const fetchUrl = `${cpkEndpoint.url}/agents/state`;
812
825
  try {
813
- const response = await fetch(fetchUrl, {
826
+ const response = await fetchWithRetry(fetchUrl, {
814
827
  method: "POST",
815
828
  headers: createHeaders(cpkEndpoint.onBeforeRequest, graphqlContext),
816
829
  body: JSON.stringify({
@@ -1022,7 +1035,9 @@ please use an LLM adapter instead.`,
1022
1035
  }
1023
1036
  }
1024
1037
 
1025
- eventStream$.error(err);
1038
+ // Convert network termination errors to structured errors
1039
+ const structuredError = this.convertStreamingErrorToStructured(err);
1040
+ eventStream$.error(structuredError);
1026
1041
  eventStream$.complete();
1027
1042
  },
1028
1043
  complete: () => eventStream$.complete(),
@@ -1214,6 +1229,55 @@ please use an LLM adapter instead.`,
1214
1229
  if (adapterName.includes("LangChain")) return "langchain";
1215
1230
  return undefined;
1216
1231
  }
1232
+
1233
+ private convertStreamingErrorToStructured(error: any): CopilotKitError {
1234
+ // Handle network termination errors
1235
+ if (
1236
+ error?.message?.includes("terminated") ||
1237
+ error?.cause?.code === "UND_ERR_SOCKET" ||
1238
+ error?.message?.includes("other side closed") ||
1239
+ error?.code === "UND_ERR_SOCKET"
1240
+ ) {
1241
+ return new CopilotKitError({
1242
+ message:
1243
+ "Connection to agent was unexpectedly terminated. This may be due to the agent service being restarted or network issues. Please try again.",
1244
+ code: CopilotKitErrorCode.NETWORK_ERROR,
1245
+ });
1246
+ }
1247
+
1248
+ // Handle other network-related errors
1249
+ if (
1250
+ error?.message?.includes("fetch failed") ||
1251
+ error?.message?.includes("ECONNREFUSED") ||
1252
+ error?.message?.includes("ENOTFOUND") ||
1253
+ error?.message?.includes("ETIMEDOUT")
1254
+ ) {
1255
+ return new CopilotKitLowLevelError({
1256
+ error: error instanceof Error ? error : new Error(String(error)),
1257
+ url: "agent streaming connection",
1258
+ message:
1259
+ "Network error occurred during agent streaming. Please check your connection and try again.",
1260
+ });
1261
+ }
1262
+
1263
+ // Handle abort/cancellation errors (these are usually normal)
1264
+ if (
1265
+ error?.message?.includes("aborted") ||
1266
+ error?.message?.includes("canceled") ||
1267
+ error?.message?.includes("signal is aborted")
1268
+ ) {
1269
+ return new CopilotKitError({
1270
+ message: "Agent request was cancelled",
1271
+ code: CopilotKitErrorCode.UNKNOWN,
1272
+ });
1273
+ }
1274
+
1275
+ // Default: convert unknown streaming errors
1276
+ return new CopilotKitError({
1277
+ message: `Agent streaming error: ${error?.message || String(error)}`,
1278
+ code: CopilotKitErrorCode.UNKNOWN,
1279
+ });
1280
+ }
1217
1281
  }
1218
1282
 
1219
1283
  export function flattenToolCallsNoDuplicates(toolsByPriority: ActionInput[]): ActionInput[] {
@@ -20,6 +20,7 @@ import { writeJsonLineResponseToEventStream } from "../streaming";
20
20
  import { CopilotKitApiDiscoveryError, ResolvedCopilotKitError } from "@copilotkit/shared";
21
21
  import { parseJson, tryMap } from "@copilotkit/shared";
22
22
  import { ActionInput } from "../../graphql/inputs/action.input";
23
+ import { fetchWithRetry } from "./retry-utils";
23
24
 
24
25
  export function constructLGCRemoteAction({
25
26
  endpoint,
@@ -144,15 +145,19 @@ export function constructRemoteActions({
144
145
 
145
146
  const fetchUrl = `${url}/actions/execute`;
146
147
  try {
147
- const response = await fetch(fetchUrl, {
148
- method: "POST",
149
- headers,
150
- body: JSON.stringify({
151
- name: action.name,
152
- arguments: args,
153
- properties: graphqlContext.properties,
154
- }),
155
- });
148
+ const response = await fetchWithRetry(
149
+ fetchUrl,
150
+ {
151
+ method: "POST",
152
+ headers,
153
+ body: JSON.stringify({
154
+ name: action.name,
155
+ arguments: args,
156
+ properties: graphqlContext.properties,
157
+ }),
158
+ },
159
+ logger,
160
+ );
156
161
 
157
162
  if (!response.ok) {
158
163
  logger.error(
@@ -219,25 +224,29 @@ export function constructRemoteActions({
219
224
 
220
225
  const fetchUrl = `${url}/agents/execute`;
221
226
  try {
222
- const response = await fetch(fetchUrl, {
223
- method: "POST",
224
- headers,
225
- body: JSON.stringify({
226
- name,
227
- threadId,
228
- nodeName,
229
- messages: [...messages, ...additionalMessages],
230
- state,
231
- config,
232
- properties: graphqlContext.properties,
233
- actions: tryMap(actionInputsWithoutAgents, (action: ActionInput) => ({
234
- name: action.name,
235
- description: action.description,
236
- parameters: JSON.parse(action.jsonSchema),
237
- })),
238
- metaEvents,
239
- }),
240
- });
227
+ const response = await fetchWithRetry(
228
+ fetchUrl,
229
+ {
230
+ method: "POST",
231
+ headers,
232
+ body: JSON.stringify({
233
+ name,
234
+ threadId,
235
+ nodeName,
236
+ messages: [...messages, ...additionalMessages],
237
+ state,
238
+ config,
239
+ properties: graphqlContext.properties,
240
+ actions: tryMap(actionInputsWithoutAgents, (action: ActionInput) => ({
241
+ name: action.name,
242
+ description: action.description,
243
+ parameters: JSON.parse(action.jsonSchema),
244
+ })),
245
+ metaEvents,
246
+ }),
247
+ },
248
+ logger,
249
+ );
241
250
 
242
251
  if (!response.ok) {
243
252
  logger.error(
@@ -19,6 +19,7 @@ import { MetaEventInput } from "../../graphql/inputs/meta-event.input";
19
19
  import { MetaEventName } from "../../graphql/types/meta-events.type";
20
20
  import { parseJson, CopilotKitMisuseError } from "@copilotkit/shared";
21
21
  import { RemoveMessage } from "@langchain/core/messages";
22
+ import { RETRY_CONFIG, isRetryableError, sleep, calculateDelay } from "./retry-utils";
22
23
 
23
24
  type State = Record<string, any>;
24
25
 
@@ -88,32 +89,52 @@ let activeInterruptEvent = false;
88
89
  export async function execute(args: ExecutionArgs): Promise<ReadableStream<Uint8Array>> {
89
90
  return new ReadableStream({
90
91
  async start(controller) {
91
- try {
92
- await streamEvents(controller, args);
93
- controller.close();
94
- } catch (err) {
95
- // Unwrap the possible cause
96
- const cause = err?.cause;
97
-
98
- // Check code directly if it exists
99
- const errorCode = cause?.code || err?.code;
100
-
101
- if (errorCode === "ECONNREFUSED") {
102
- throw new CopilotKitMisuseError({
103
- message: `
104
- The LangGraph client could not connect to the graph. Please further check previous logs, which includes further details.
105
-
106
- See more: https://docs.copilotkit.ai/troubleshooting/common-issues`,
107
- });
108
- } else {
109
- throw new CopilotKitMisuseError({
110
- message: `
111
- The LangGraph client threw unhandled error ${err}.
112
-
113
- See more: https://docs.copilotkit.ai/troubleshooting/common-issues`,
114
- });
92
+ let lastError: any;
93
+
94
+ // Retry logic for transient connection errors
95
+ for (let attempt = 0; attempt <= RETRY_CONFIG.maxRetries; attempt++) {
96
+ try {
97
+ await streamEvents(controller, args);
98
+ controller.close();
99
+ return; // Success - exit retry loop
100
+ } catch (err) {
101
+ lastError = err;
102
+
103
+ // Check if this is a retryable error
104
+ if (isRetryableError(err) && attempt < RETRY_CONFIG.maxRetries) {
105
+ const delay = calculateDelay(attempt);
106
+ console.warn(
107
+ `LangGraph connection attempt ${attempt + 1}/${RETRY_CONFIG.maxRetries + 1} failed. ` +
108
+ `Retrying in ${delay}ms. Error: ${err?.message || String(err)}`,
109
+ );
110
+ await sleep(delay);
111
+ continue; // Retry
112
+ }
113
+
114
+ // Not retryable or max retries exceeded - handle error
115
+ break;
115
116
  }
116
117
  }
118
+
119
+ // Handle the final error after retries exhausted
120
+ const cause = lastError?.cause;
121
+ const errorCode = cause?.code || lastError?.code;
122
+
123
+ if (errorCode === "ECONNREFUSED") {
124
+ throw new CopilotKitMisuseError({
125
+ message: `
126
+ The LangGraph client could not connect to the graph after ${RETRY_CONFIG.maxRetries + 1} attempts. Please further check previous logs, which includes further details.
127
+
128
+ See more: https://docs.copilotkit.ai/troubleshooting/common-issues`,
129
+ });
130
+ } else {
131
+ throw new CopilotKitMisuseError({
132
+ message: `
133
+ The LangGraph client threw unhandled error ${lastError}.
134
+
135
+ See more: https://docs.copilotkit.ai/troubleshooting/common-issues`,
136
+ });
137
+ }
117
138
  },
118
139
  });
119
140
  }
@@ -0,0 +1,96 @@
1
+ import { Logger } from "pino";
2
+
3
+ // Retry configuration for network requests
4
+ export const RETRY_CONFIG = {
5
+ maxRetries: 3,
6
+ baseDelayMs: 1000,
7
+ maxDelayMs: 5000,
8
+ // HTTP status codes that should be retried
9
+ retryableStatusCodes: [502, 503, 504, 408, 429],
10
+ // Network error patterns that should be retried
11
+ retryableErrorMessages: [
12
+ "fetch failed",
13
+ "network error",
14
+ "connection timeout",
15
+ "ECONNREFUSED",
16
+ "ETIMEDOUT",
17
+ "ENOTFOUND",
18
+ "ECONNRESET",
19
+ ],
20
+ };
21
+
22
+ // Helper function to check if an error/response is retryable
23
+ export function isRetryableError(error: any, response?: Response): boolean {
24
+ // Check HTTP response status
25
+ if (response && RETRY_CONFIG.retryableStatusCodes.includes(response.status)) {
26
+ return true;
27
+ }
28
+
29
+ // Check error codes (for connection errors like ECONNREFUSED)
30
+ const errorCode = error?.cause?.code || error?.code;
31
+ if (errorCode && RETRY_CONFIG.retryableErrorMessages.includes(errorCode)) {
32
+ return true;
33
+ }
34
+
35
+ // Check error messages
36
+ const errorMessage = error?.message?.toLowerCase() || "";
37
+ return RETRY_CONFIG.retryableErrorMessages.some((msg) => errorMessage.includes(msg));
38
+ }
39
+
40
+ // Helper function to sleep for a given duration
41
+ export function sleep(ms: number): Promise<void> {
42
+ return new Promise((resolve) => setTimeout(resolve, ms));
43
+ }
44
+
45
+ // Calculate exponential backoff delay
46
+ export function calculateDelay(attempt: number): number {
47
+ const delay = RETRY_CONFIG.baseDelayMs * Math.pow(2, attempt);
48
+ return Math.min(delay, RETRY_CONFIG.maxDelayMs);
49
+ }
50
+
51
+ // Retry wrapper for fetch requests
52
+ export async function fetchWithRetry(
53
+ url: string,
54
+ options: RequestInit,
55
+ logger?: Logger,
56
+ ): Promise<Response> {
57
+ let lastError: any;
58
+
59
+ for (let attempt = 0; attempt <= RETRY_CONFIG.maxRetries; attempt++) {
60
+ try {
61
+ const response = await fetch(url, options);
62
+
63
+ // If response is retryable, treat as error and retry
64
+ if (isRetryableError(null, response) && attempt < RETRY_CONFIG.maxRetries) {
65
+ const delay = calculateDelay(attempt);
66
+ logger?.warn(
67
+ `Request to ${url} failed with status ${response.status}. ` +
68
+ `Retrying attempt ${attempt + 1}/${RETRY_CONFIG.maxRetries + 1} in ${delay}ms.`,
69
+ );
70
+ await sleep(delay);
71
+ continue;
72
+ }
73
+
74
+ return response; // Success or non-retryable error
75
+ } catch (error) {
76
+ lastError = error;
77
+
78
+ // Check if this is a retryable network error
79
+ if (isRetryableError(error) && attempt < RETRY_CONFIG.maxRetries) {
80
+ const delay = calculateDelay(attempt);
81
+ logger?.warn(
82
+ `Request to ${url} failed with network error. ` +
83
+ `Retrying attempt ${attempt + 1}/${RETRY_CONFIG.maxRetries + 1} in ${delay}ms. Error: ${error?.message || String(error)}`,
84
+ );
85
+ await sleep(delay);
86
+ continue;
87
+ }
88
+
89
+ // Not retryable or max retries exceeded
90
+ break;
91
+ }
92
+ }
93
+
94
+ // Re-throw the last error after retries exhausted
95
+ throw lastError;
96
+ }
@@ -1,4 +1,5 @@
1
1
  import { ReplaySubject } from "rxjs";
2
+ import { CopilotKitLowLevelError, CopilotKitError, CopilotKitErrorCode } from "@copilotkit/shared";
2
3
 
3
4
  export async function writeJsonLineResponseToEventStream<T>(
4
5
  response: ReadableStream<Uint8Array>,
@@ -52,8 +53,60 @@ export async function writeJsonLineResponseToEventStream<T>(
52
53
  }
53
54
  } catch (error) {
54
55
  console.error("Error in stream", error);
55
- eventStream$.error(error);
56
+
57
+ // Convert network termination errors to structured errors
58
+ const structuredError = convertStreamingErrorToStructured(error);
59
+ eventStream$.error(structuredError);
56
60
  return;
57
61
  }
58
62
  eventStream$.complete();
59
63
  }
64
+
65
+ function convertStreamingErrorToStructured(error: any): CopilotKitError {
66
+ // Handle network termination errors
67
+ if (
68
+ error?.message?.includes("terminated") ||
69
+ error?.cause?.code === "UND_ERR_SOCKET" ||
70
+ error?.message?.includes("other side closed") ||
71
+ error?.code === "UND_ERR_SOCKET"
72
+ ) {
73
+ return new CopilotKitError({
74
+ message:
75
+ "Connection to agent was unexpectedly terminated. This is likely due to the agent service being down or experiencing issues. Please check your agent logs and try again.",
76
+ code: CopilotKitErrorCode.NETWORK_ERROR,
77
+ });
78
+ }
79
+
80
+ // Handle other network-related errors
81
+ if (
82
+ error?.message?.includes("fetch failed") ||
83
+ error?.message?.includes("ECONNREFUSED") ||
84
+ error?.message?.includes("ENOTFOUND") ||
85
+ error?.message?.includes("ETIMEDOUT")
86
+ ) {
87
+ return new CopilotKitLowLevelError({
88
+ error: error instanceof Error ? error : new Error(String(error)),
89
+ url: "streaming connection",
90
+ message:
91
+ "Network error occurred during streaming. Please check your connection and try again.",
92
+ });
93
+ }
94
+
95
+ // Handle abort/cancellation errors (these are usually normal)
96
+ if (
97
+ error?.message?.includes("aborted") ||
98
+ error?.message?.includes("canceled") ||
99
+ error?.message?.includes("signal is aborted")
100
+ ) {
101
+ return new CopilotKitError({
102
+ message: "Request was cancelled",
103
+ code: CopilotKitErrorCode.UNKNOWN,
104
+ });
105
+ }
106
+
107
+ // Default: convert unknown streaming errors
108
+ return new CopilotKitError({
109
+ message: `Streaming error: ${error?.message || String(error)}`,
110
+ code: CopilotKitErrorCode.UNKNOWN,
111
+ });
112
+ }