@copilotkit/runtime 1.9.2-next.1 → 1.9.2-next.10

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 (84) hide show
  1. package/CHANGELOG.md +66 -0
  2. package/dist/{chunk-YV3YXRMR.mjs → chunk-5OK4GLKL.mjs} +19 -2
  3. package/dist/chunk-5OK4GLKL.mjs.map +1 -0
  4. package/dist/{chunk-RIPQZJB5.mjs → chunk-6RUTA76W.mjs} +2 -2
  5. package/dist/chunk-AMUJQ6IR.mjs +50 -0
  6. package/dist/chunk-AMUJQ6IR.mjs.map +1 -0
  7. package/dist/{chunk-IIXJVVTV.mjs → chunk-GS7DO47Q.mjs} +155 -78
  8. package/dist/chunk-GS7DO47Q.mjs.map +1 -0
  9. package/dist/{chunk-XGBY45FP.mjs → chunk-PMIAGZGS.mjs} +2371 -1857
  10. package/dist/chunk-PMIAGZGS.mjs.map +1 -0
  11. package/dist/{chunk-C3SWOFLO.mjs → chunk-TOBFVWZU.mjs} +2 -2
  12. package/dist/{chunk-KPFOAXRX.mjs → chunk-VBXBFZEL.mjs} +2 -2
  13. package/dist/{chunk-5BIEM2UU.mjs → chunk-XWBDEXDA.mjs} +4 -3
  14. package/dist/{chunk-5BIEM2UU.mjs.map → chunk-XWBDEXDA.mjs.map} +1 -1
  15. package/dist/{groq-adapter-25a2bd35.d.ts → groq-adapter-172a2ca4.d.ts} +1 -1
  16. package/dist/index.d.ts +4 -3
  17. package/dist/index.js +3748 -3096
  18. package/dist/index.js.map +1 -1
  19. package/dist/index.mjs +12 -8
  20. package/dist/index.mjs.map +1 -1
  21. package/dist/lib/index.d.ts +7 -133
  22. package/dist/lib/index.js +3357 -2748
  23. package/dist/lib/index.js.map +1 -1
  24. package/dist/lib/index.mjs +9 -8
  25. package/dist/lib/integrations/index.d.ts +3 -3
  26. package/dist/lib/integrations/index.js +162 -98
  27. package/dist/lib/integrations/index.js.map +1 -1
  28. package/dist/lib/integrations/index.mjs +7 -6
  29. package/dist/lib/integrations/nest/index.d.ts +2 -2
  30. package/dist/lib/integrations/nest/index.js +162 -98
  31. package/dist/lib/integrations/nest/index.js.map +1 -1
  32. package/dist/lib/integrations/nest/index.mjs +5 -4
  33. package/dist/lib/integrations/node-express/index.d.ts +2 -2
  34. package/dist/lib/integrations/node-express/index.js +162 -98
  35. package/dist/lib/integrations/node-express/index.js.map +1 -1
  36. package/dist/lib/integrations/node-express/index.mjs +5 -4
  37. package/dist/lib/integrations/node-http/index.d.ts +2 -2
  38. package/dist/lib/integrations/node-http/index.js +162 -98
  39. package/dist/lib/integrations/node-http/index.js.map +1 -1
  40. package/dist/lib/integrations/node-http/index.mjs +4 -3
  41. package/dist/service-adapters/index.d.ts +6 -4
  42. package/dist/service-adapters/index.js +225 -107
  43. package/dist/service-adapters/index.js.map +1 -1
  44. package/dist/service-adapters/index.mjs +6 -2
  45. package/dist/service-adapters/shared/index.d.ts +9 -0
  46. package/dist/service-adapters/shared/index.js +72 -0
  47. package/dist/service-adapters/shared/index.js.map +1 -0
  48. package/dist/service-adapters/shared/index.mjs +8 -0
  49. package/dist/service-adapters/shared/index.mjs.map +1 -0
  50. package/dist/{shared-e272b15a.d.ts → shared-bd953ebf.d.ts} +45 -5
  51. package/dist/utils/index.d.ts +17 -1
  52. package/dist/utils/index.js +3 -2
  53. package/dist/utils/index.js.map +1 -1
  54. package/dist/utils/index.mjs +1 -1
  55. package/package.json +4 -4
  56. package/src/agents/langgraph/event-source.ts +36 -38
  57. package/src/agents/langgraph/events.ts +19 -1
  58. package/src/graphql/resolvers/copilot.resolver.ts +108 -45
  59. package/src/graphql/resolvers/state.resolver.ts +3 -3
  60. package/src/lib/error-messages.ts +200 -0
  61. package/src/lib/integrations/shared.ts +43 -0
  62. package/src/lib/runtime/__tests__/copilot-runtime-trace.test.ts +169 -0
  63. package/src/lib/runtime/copilot-runtime.ts +412 -83
  64. package/src/lib/runtime/langgraph/langgraph-agent.ts +12 -0
  65. package/src/lib/runtime/remote-action-constructors.ts +28 -3
  66. package/src/lib/runtime/remote-lg-action.ts +130 -40
  67. package/src/lib/streaming.ts +125 -36
  68. package/src/service-adapters/anthropic/anthropic-adapter.ts +67 -8
  69. package/src/service-adapters/anthropic/utils.ts +3 -8
  70. package/src/service-adapters/events.ts +37 -81
  71. package/src/service-adapters/groq/groq-adapter.ts +66 -56
  72. package/src/service-adapters/index.ts +1 -0
  73. package/src/service-adapters/openai/openai-adapter.ts +18 -3
  74. package/src/service-adapters/shared/error-utils.ts +61 -0
  75. package/src/service-adapters/shared/index.ts +1 -0
  76. package/src/utils/failed-response-status-reasons.ts +23 -1
  77. package/tests/service-adapters/anthropic/anthropic-adapter.test.ts +172 -387
  78. package/dist/chunk-IIXJVVTV.mjs.map +0 -1
  79. package/dist/chunk-XGBY45FP.mjs.map +0 -1
  80. package/dist/chunk-YV3YXRMR.mjs.map +0 -1
  81. package/dist/{chunk-RIPQZJB5.mjs.map → chunk-6RUTA76W.mjs.map} +0 -0
  82. package/dist/{chunk-C3SWOFLO.mjs.map → chunk-TOBFVWZU.mjs.map} +0 -0
  83. package/dist/{chunk-KPFOAXRX.mjs.map → chunk-VBXBFZEL.mjs.map} +0 -0
  84. package/dist/{langserve-4a5c9217.d.ts → langserve-fc5cac89.d.ts} +7 -7
@@ -22,6 +22,9 @@ import { parseJson, tryMap } from "@copilotkit/shared";
22
22
  import { ActionInput } from "../../graphql/inputs/action.input";
23
23
  import { fetchWithRetry } from "./retry-utils";
24
24
 
25
+ // Import the utility function from remote-lg-action
26
+ import { isUserConfigurationError } from "./remote-lg-action";
27
+
25
28
  export function constructLGCRemoteAction({
26
29
  endpoint,
27
30
  graphqlContext,
@@ -93,11 +96,33 @@ export function constructLGCRemoteAction({
93
96
  writeJsonLineResponseToEventStream(response, eventSource.eventStream$);
94
97
  return eventSource.processLangGraphEvents();
95
98
  } catch (error) {
99
+ // Preserve structured CopilotKit errors with semantic information
100
+ if (error instanceof CopilotKitError || error instanceof CopilotKitLowLevelError) {
101
+ // Distinguish between user errors and system errors for logging
102
+ if (isUserConfigurationError(error)) {
103
+ logger.debug(
104
+ { url: endpoint.deploymentUrl, error: error.message, code: error.code },
105
+ "User configuration error in LangGraph Platform agent",
106
+ );
107
+ } else {
108
+ logger.error(
109
+ { url: endpoint.deploymentUrl, error: error.message, type: error.constructor.name },
110
+ "LangGraph Platform agent error",
111
+ );
112
+ }
113
+ throw error; // Re-throw the structured error to preserve semantic information
114
+ }
115
+
116
+ // For other errors, log and wrap them
96
117
  logger.error(
97
118
  { url: endpoint.deploymentUrl, status: 500, body: error.message },
98
119
  "Failed to execute LangGraph Platform agent",
99
120
  );
100
- throw new Error("Failed to execute LangGraph Platform agent");
121
+ throw new CopilotKitLowLevelError({
122
+ error: error instanceof Error ? error : new Error(String(error)),
123
+ url: endpoint.deploymentUrl,
124
+ message: "Failed to execute LangGraph Platform agent",
125
+ });
101
126
  }
102
127
  },
103
128
  }));
@@ -180,7 +205,7 @@ export function constructRemoteActions({
180
205
  logger.debug({ actionName: action.name, result }, "Executed remote action");
181
206
  return result;
182
207
  } catch (error) {
183
- if (error instanceof CopilotKitError) {
208
+ if (error instanceof CopilotKitError || error instanceof CopilotKitLowLevelError) {
184
209
  throw error;
185
210
  }
186
211
  throw new CopilotKitLowLevelError({ error, url: fetchUrl });
@@ -275,7 +300,7 @@ export function constructRemoteActions({
275
300
  throw new Error("Unsupported agent type");
276
301
  }
277
302
  } catch (error) {
278
- if (error instanceof CopilotKitError) {
303
+ if (error instanceof CopilotKitError || error instanceof CopilotKitLowLevelError) {
279
304
  throw error;
280
305
  }
281
306
  throw new CopilotKitLowLevelError({ error, url: fetchUrl });
@@ -2,7 +2,9 @@ import {
2
2
  Client as LangGraphClient,
3
3
  EventsStreamEvent,
4
4
  GraphSchema,
5
+ Interrupt,
5
6
  StreamMode,
7
+ ThreadState,
6
8
  } from "@langchain/langgraph-sdk";
7
9
  import { createHash } from "node:crypto";
8
10
  import { isValidUUID, randomUUID } from "@copilotkit/shared";
@@ -17,9 +19,28 @@ import { CustomEventNames, LangGraphEventTypes } from "../../agents/langgraph/ev
17
19
  import telemetry from "../telemetry-client";
18
20
  import { MetaEventInput } from "../../graphql/inputs/meta-event.input";
19
21
  import { MetaEventName } from "../../graphql/types/meta-events.type";
20
- import { parseJson, CopilotKitMisuseError } from "@copilotkit/shared";
22
+ import {
23
+ parseJson,
24
+ CopilotKitMisuseError,
25
+ CopilotKitLowLevelError,
26
+ CopilotKitError,
27
+ } from "@copilotkit/shared";
21
28
  import { RemoveMessage } from "@langchain/core/messages";
22
29
  import { RETRY_CONFIG, isRetryableError, sleep, calculateDelay } from "./retry-utils";
30
+ import { generateHelpfulErrorMessage } from "../streaming";
31
+
32
+ // Utility to determine if an error is a user configuration issue vs system error
33
+ export function isUserConfigurationError(error: any): boolean {
34
+ return (
35
+ (error instanceof CopilotKitError || error instanceof CopilotKitLowLevelError) &&
36
+ (error.code === "NETWORK_ERROR" ||
37
+ error.code === "AUTHENTICATION_ERROR" ||
38
+ error.statusCode === 401 ||
39
+ error.statusCode === 403 ||
40
+ error.message?.toLowerCase().includes("authentication") ||
41
+ error.message?.toLowerCase().includes("api key"))
42
+ );
43
+ }
23
44
 
24
45
  type State = Record<string, any>;
25
46
 
@@ -84,8 +105,6 @@ type SchemaKeys = {
84
105
  config: string[] | null;
85
106
  } | null;
86
107
 
87
- let activeInterruptEvent = false;
88
-
89
108
  export async function execute(args: ExecutionArgs): Promise<ReadableStream<Uint8Array>> {
90
109
  return new ReadableStream({
91
110
  async start(controller) {
@@ -128,6 +147,15 @@ export async function execute(args: ExecutionArgs): Promise<ReadableStream<Uint8
128
147
  See more: https://docs.copilotkit.ai/troubleshooting/common-issues`,
129
148
  });
130
149
  } else {
150
+ // Preserve already structured CopilotKit errors with semantic information
151
+ if (
152
+ lastError instanceof CopilotKitError ||
153
+ lastError instanceof CopilotKitLowLevelError ||
154
+ (lastError instanceof Error && lastError.name && lastError.name.includes("CopilotKit"))
155
+ ) {
156
+ throw lastError; // Re-throw to preserve semantic information and visibility settings
157
+ }
158
+
131
159
  throw new CopilotKitMisuseError({
132
160
  message: `
133
161
  The LangGraph client threw unhandled error ${lastError}.
@@ -188,7 +216,7 @@ async function streamEvents(controller: ReadableStreamDefaultController, args: E
188
216
  await client.threads.create({ threadId });
189
217
  }
190
218
 
191
- let agentState = { values: {} };
219
+ let agentState = { values: {} } as ThreadState;
192
220
  if (wasInitiatedWithExistingThread) {
193
221
  agentState = await client.threads.getState(threadId);
194
222
  }
@@ -218,16 +246,14 @@ async function streamEvents(controller: ReadableStreamDefaultController, args: E
218
246
  const lgInterruptMetaEvent = metaEvents?.find(
219
247
  (ev) => ev.name === MetaEventName.LangGraphInterruptEvent,
220
248
  );
221
- if (activeInterruptEvent && !lgInterruptMetaEvent) {
222
- // state.messages includes only messages that were not processed by the agent, which are the interrupt messages
223
- payload.command = { resume: state.messages };
224
- }
249
+
225
250
  if (lgInterruptMetaEvent?.response) {
226
251
  let response = lgInterruptMetaEvent.response;
227
252
  payload.command = { resume: parseJson(response, response) };
228
253
  }
229
254
 
230
- if (mode === "continue" && !activeInterruptEvent) {
255
+ const interrupts = (agentState.tasks?.[0]?.interrupts ?? []) as Interrupt[];
256
+ if (mode === "continue" && !interrupts.length) {
231
257
  await client.threads.updateState(threadId, { values: state, asNode: nodeName });
232
258
  }
233
259
 
@@ -311,17 +337,30 @@ async function streamEvents(controller: ReadableStreamDefaultController, args: E
311
337
  let shouldExit = false;
312
338
  let externalRunId = null;
313
339
 
314
- const streamResponse = client.runs.stream(threadId, assistantId, payload);
315
-
316
340
  const emit = (message: string) => controller.enqueue(new TextEncoder().encode(message));
317
341
 
342
+ // If there are still outstanding unresolved interrupts, we must force resolution of them before moving forward
343
+ if (interrupts?.length && !payload.command?.resume) {
344
+ // If the interrupt is "by message" we assume the upcoming user message is a resoluton for the interrupt
345
+ if (!lgInterruptMetaEvent) {
346
+ // state.messages includes only messages that were not processed by the agent, which are the interrupt messages
347
+ payload.command = { resume: state.messages };
348
+ } else {
349
+ interrupts.forEach((interrupt) => {
350
+ emitInterrupt(interrupt.value, emit);
351
+ });
352
+ return Promise.resolve();
353
+ }
354
+ }
355
+
356
+ const streamResponse = client.runs.stream(threadId, assistantId, payload);
357
+
318
358
  let latestStateValues = {};
319
359
  let updatedState = state;
320
360
  // If a manual emittance happens, it is the ultimate source of truth of state, unless a node has exited.
321
361
  // Therefore, this value should either hold null, or the only edition of state that should be used.
322
362
  let manuallyEmittedState = null;
323
363
 
324
- activeInterruptEvent = false;
325
364
  try {
326
365
  telemetry.capture("oss.runtime.agent_execution_stream_started", {
327
366
  hashedLgcKey: streamInfo.hashedLgcKey,
@@ -330,7 +369,43 @@ async function streamEvents(controller: ReadableStreamDefaultController, args: E
330
369
  if (!["events", "values", "error", "updates"].includes(streamResponseChunk.event)) continue;
331
370
 
332
371
  if (streamResponseChunk.event === "error") {
333
- throw new Error(`Error event thrown: ${streamResponseChunk.data.message}`);
372
+ const errorData = streamResponseChunk.data;
373
+
374
+ // Check if this is a structured error from our Python agent
375
+ if (errorData && typeof errorData === "object" && "error_details" in errorData) {
376
+ const errorDetails = (errorData as any).error_details;
377
+
378
+ // Create a structured error with preserved semantic information
379
+ const preservedError = new CopilotKitLowLevelError({
380
+ error: new Error(errorDetails.message),
381
+ url: "langgraph platform agent",
382
+ message: `${errorDetails.type}: ${errorDetails.message}`,
383
+ });
384
+
385
+ // Add additional error context
386
+ if (errorDetails.status_code) {
387
+ (preservedError as any).statusCode = errorDetails.status_code;
388
+ }
389
+ if (errorDetails.response_data) {
390
+ (preservedError as any).responseData = errorDetails.response_data;
391
+ }
392
+ (preservedError as any).agentName = errorDetails.agent_name;
393
+ (preservedError as any).originalErrorType = errorDetails.type;
394
+
395
+ throw preservedError;
396
+ }
397
+
398
+ // Fallback for generic error messages
399
+ const helpfulMessage = generateHelpfulErrorMessage(
400
+ new Error(errorData.message),
401
+ "LangGraph Platform agent",
402
+ );
403
+
404
+ throw new CopilotKitLowLevelError({
405
+ error: new Error(errorData.message),
406
+ url: "langgraph platform agent",
407
+ message: helpfulMessage,
408
+ });
334
409
  }
335
410
 
336
411
  // Force event type, as data is not properly defined on the LG side.
@@ -345,33 +420,8 @@ async function streamEvents(controller: ReadableStreamDefaultController, args: E
345
420
 
346
421
  const interruptEvents = chunk.data.__interrupt__;
347
422
  if (interruptEvents?.length) {
348
- activeInterruptEvent = true;
349
423
  const interruptValue = interruptEvents?.[0].value;
350
- if (
351
- typeof interruptValue != "string" &&
352
- "__copilotkit_interrupt_value__" in interruptValue
353
- ) {
354
- const evValue = interruptValue.__copilotkit_interrupt_value__;
355
- emit(
356
- JSON.stringify({
357
- event: LangGraphEventTypes.OnCopilotKitInterrupt,
358
- data: {
359
- value: typeof evValue === "string" ? evValue : JSON.stringify(evValue),
360
- messages: langchainMessagesToCopilotKit(interruptValue.__copilotkit_messages__),
361
- },
362
- }) + "\n",
363
- );
364
- } else {
365
- emit(
366
- JSON.stringify({
367
- event: LangGraphEventTypes.OnInterrupt,
368
- value:
369
- typeof interruptValue === "string"
370
- ? interruptValue
371
- : JSON.stringify(interruptValue),
372
- }) + "\n",
373
- );
374
- }
424
+ emitInterrupt(interruptValue, emit);
375
425
  continue;
376
426
  }
377
427
  if (streamResponseChunk.event === "updates") continue;
@@ -521,11 +571,29 @@ async function streamEvents(controller: ReadableStreamDefaultController, args: E
521
571
 
522
572
  return Promise.resolve();
523
573
  } catch (e) {
524
- logger.error(e);
574
+ // Distinguish between user errors and system errors for logging
575
+ if (isUserConfigurationError(e)) {
576
+ // Log user errors at debug level to reduce noise
577
+ logger.debug({ error: e.message, code: e.code }, "User configuration error");
578
+ } else {
579
+ // Log actual system errors at error level
580
+ logger.error(e);
581
+ }
582
+
525
583
  telemetry.capture("oss.runtime.agent_execution_stream_errored", {
526
584
  ...streamInfo,
527
585
  error: e.message,
528
586
  });
587
+
588
+ // Re-throw CopilotKit errors so they can be handled properly at higher levels
589
+ if (
590
+ e instanceof CopilotKitError ||
591
+ e instanceof CopilotKitLowLevelError ||
592
+ (e instanceof Error && e.name && e.name.includes("CopilotKit"))
593
+ ) {
594
+ throw e;
595
+ }
596
+
529
597
  return Promise.resolve();
530
598
  }
531
599
  }
@@ -909,3 +977,25 @@ function getSchemaKeys(graphSchema: GraphSchema): SchemaKeys {
909
977
  function filterObjectBySchemaKeys(obj: Record<string, any>, schemaKeys: string[]) {
910
978
  return Object.fromEntries(Object.entries(obj).filter(([key]) => schemaKeys.includes(key)));
911
979
  }
980
+
981
+ function emitInterrupt(interruptValue: any, emit: (data: string) => void) {
982
+ if (typeof interruptValue != "string" && "__copilotkit_interrupt_value__" in interruptValue) {
983
+ const evValue = interruptValue.__copilotkit_interrupt_value__;
984
+ emit(
985
+ JSON.stringify({
986
+ event: LangGraphEventTypes.OnCopilotKitInterrupt,
987
+ data: {
988
+ value: typeof evValue === "string" ? evValue : JSON.stringify(evValue),
989
+ messages: langchainMessagesToCopilotKit(interruptValue.__copilotkit_messages__),
990
+ },
991
+ }) + "\n",
992
+ );
993
+ } else {
994
+ emit(
995
+ JSON.stringify({
996
+ event: LangGraphEventTypes.OnInterrupt,
997
+ value: typeof interruptValue === "string" ? interruptValue : JSON.stringify(interruptValue),
998
+ }) + "\n",
999
+ );
1000
+ }
1001
+ }
@@ -1,5 +1,11 @@
1
1
  import { ReplaySubject } from "rxjs";
2
- import { CopilotKitLowLevelError, CopilotKitError, CopilotKitErrorCode } from "@copilotkit/shared";
2
+ import {
3
+ CopilotKitLowLevelError,
4
+ CopilotKitError,
5
+ CopilotKitErrorCode,
6
+ ensureStructuredError,
7
+ } from "@copilotkit/shared";
8
+ import { errorConfig, getFallbackMessage } from "./error-messages";
3
9
 
4
10
  export async function writeJsonLineResponseToEventStream<T>(
5
11
  response: ReadableStream<Uint8Array>,
@@ -52,10 +58,8 @@ export async function writeJsonLineResponseToEventStream<T>(
52
58
  }
53
59
  }
54
60
  } catch (error) {
55
- console.error("Error in stream", error);
56
-
57
- // Convert network termination errors to structured errors
58
- const structuredError = convertStreamingErrorToStructured(error);
61
+ // Preserve already structured CopilotKit errors, only convert unstructured errors
62
+ const structuredError = ensureStructuredError(error, convertStreamingErrorToStructured);
59
63
  eventStream$.error(structuredError);
60
64
  return;
61
65
  }
@@ -63,50 +67,135 @@ export async function writeJsonLineResponseToEventStream<T>(
63
67
  }
64
68
 
65
69
  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
- }
70
+ // Determine a more helpful error message based on context
71
+ let helpfulMessage = generateHelpfulErrorMessage(error);
79
72
 
80
- // Handle other network-related errors
73
+ // For network-related errors, use CopilotKitLowLevelError to preserve the original error
81
74
  if (
82
75
  error?.message?.includes("fetch failed") ||
83
76
  error?.message?.includes("ECONNREFUSED") ||
84
77
  error?.message?.includes("ENOTFOUND") ||
85
- error?.message?.includes("ETIMEDOUT")
78
+ error?.message?.includes("ETIMEDOUT") ||
79
+ error?.message?.includes("terminated") ||
80
+ error?.cause?.code === "UND_ERR_SOCKET" ||
81
+ error?.message?.includes("other side closed") ||
82
+ error?.code === "UND_ERR_SOCKET"
86
83
  ) {
87
84
  return new CopilotKitLowLevelError({
88
85
  error: error instanceof Error ? error : new Error(String(error)),
89
86
  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,
87
+ message: helpfulMessage,
104
88
  });
105
89
  }
106
90
 
107
- // Default: convert unknown streaming errors
91
+ // For all other errors, preserve the raw error in a basic CopilotKitError
108
92
  return new CopilotKitError({
109
- message: `Streaming error: ${error?.message || String(error)}`,
93
+ message: helpfulMessage,
110
94
  code: CopilotKitErrorCode.UNKNOWN,
111
95
  });
112
96
  }
97
+
98
+ /**
99
+ * Generates a helpful error message based on error patterns and context
100
+ */
101
+ export function generateHelpfulErrorMessage(error: any, context: string = "connection"): string {
102
+ const baseMessage = error?.message || String(error);
103
+
104
+ // Check for preserved error information from Python agent
105
+ const originalErrorType = error?.originalErrorType || error?.extensions?.originalErrorType;
106
+ const statusCode = error?.statusCode || error?.extensions?.statusCode;
107
+ const responseData = error?.responseData || error?.extensions?.responseData;
108
+
109
+ // First, try to match by original error type if available (more specific)
110
+ if (originalErrorType) {
111
+ const typeConfig = errorConfig.errorPatterns[originalErrorType];
112
+ if (typeConfig) {
113
+ return typeConfig.message.replace("{context}", context);
114
+ }
115
+ }
116
+
117
+ // Check for specific error patterns from configuration
118
+ for (const [pattern, config] of Object.entries(errorConfig.errorPatterns)) {
119
+ const shouldMatch =
120
+ baseMessage?.includes(pattern) ||
121
+ error?.cause?.code === pattern ||
122
+ error?.code === pattern ||
123
+ statusCode === parseInt(pattern) ||
124
+ (pattern === "other_side_closed" && baseMessage?.includes("other side closed")) ||
125
+ (pattern === "fetch_failed" && baseMessage?.includes("fetch failed")) ||
126
+ (responseData && JSON.stringify(responseData).includes(pattern));
127
+
128
+ if (shouldMatch) {
129
+ // Replace {context} placeholder with actual context
130
+ return config.message.replace("{context}", context);
131
+ }
132
+ }
133
+
134
+ // Try to match by category for fallback messages
135
+ if (isNetworkError(error)) {
136
+ return getFallbackMessage("network");
137
+ }
138
+
139
+ if (isConnectionError(error)) {
140
+ return getFallbackMessage("connection");
141
+ }
142
+
143
+ if (isAuthenticationError(error)) {
144
+ return getFallbackMessage("authentication");
145
+ }
146
+
147
+ // Default fallback
148
+ return getFallbackMessage("default");
149
+ }
150
+
151
+ /**
152
+ * Determines if an error is network-related
153
+ */
154
+ function isNetworkError(error: any): boolean {
155
+ const networkPatterns = ["ECONNREFUSED", "ENOTFOUND", "ETIMEDOUT", "fetch_failed"];
156
+ return networkPatterns.some(
157
+ (pattern) =>
158
+ error?.message?.includes(pattern) ||
159
+ error?.cause?.code === pattern ||
160
+ error?.code === pattern,
161
+ );
162
+ }
163
+
164
+ /**
165
+ * Determines if an error is connection-related
166
+ */
167
+ function isConnectionError(error: any): boolean {
168
+ const connectionPatterns = ["terminated", "UND_ERR_SOCKET", "other side closed"];
169
+ return connectionPatterns.some(
170
+ (pattern) =>
171
+ error?.message?.includes(pattern) ||
172
+ error?.cause?.code === pattern ||
173
+ error?.code === pattern,
174
+ );
175
+ }
176
+
177
+ /**
178
+ * Determines if an error is authentication-related
179
+ */
180
+ function isAuthenticationError(error: any): boolean {
181
+ const authPatterns = [
182
+ "401",
183
+ "api key",
184
+ "unauthorized",
185
+ "authentication",
186
+ "AuthenticationError",
187
+ "PermissionDeniedError",
188
+ ];
189
+ const baseMessage = error?.message || String(error);
190
+ const originalErrorType = error?.originalErrorType || error?.extensions?.originalErrorType;
191
+ const statusCode = error?.statusCode || error?.extensions?.statusCode;
192
+
193
+ return authPatterns.some(
194
+ (pattern) =>
195
+ baseMessage?.toLowerCase().includes(pattern.toLowerCase()) ||
196
+ originalErrorType === pattern ||
197
+ statusCode === 401 ||
198
+ error?.status === 401 ||
199
+ error?.statusCode === 401,
200
+ );
201
+ }
@@ -29,6 +29,7 @@ import {
29
29
  } from "./utils";
30
30
 
31
31
  import { randomId, randomUUID } from "@copilotkit/shared";
32
+ import { convertServiceAdapterError } from "../shared";
32
33
 
33
34
  const DEFAULT_MODEL = "claude-3-5-sonnet-latest";
34
35
 
@@ -60,6 +61,36 @@ export class AnthropicAdapter implements CopilotServiceAdapter {
60
61
  }
61
62
  }
62
63
 
64
+ private shouldGenerateFallbackResponse(messages: Anthropic.Messages.MessageParam[]): boolean {
65
+ if (messages.length === 0) return false;
66
+
67
+ const lastMessage = messages[messages.length - 1];
68
+
69
+ // Check if the last message is a tool result
70
+ const endsWithToolResult =
71
+ lastMessage.role === "user" &&
72
+ Array.isArray(lastMessage.content) &&
73
+ lastMessage.content.some((content: any) => content.type === "tool_result");
74
+
75
+ // Also check if we have a recent pattern of user message -> assistant tool use -> user tool result
76
+ // This indicates a completed action that might not need a response
77
+ if (messages.length >= 3 && endsWithToolResult) {
78
+ const lastThree = messages.slice(-3);
79
+ const hasRecentToolPattern =
80
+ lastThree[0]?.role === "user" && // Initial user message
81
+ lastThree[1]?.role === "assistant" && // Assistant tool use
82
+ Array.isArray(lastThree[1].content) &&
83
+ lastThree[1].content.some((content: any) => content.type === "tool_use") &&
84
+ lastThree[2]?.role === "user" && // Tool result
85
+ Array.isArray(lastThree[2].content) &&
86
+ lastThree[2].content.some((content: any) => content.type === "tool_result");
87
+
88
+ return hasRecentToolPattern;
89
+ }
90
+
91
+ return endsWithToolResult;
92
+ }
93
+
63
94
  async process(
64
95
  request: CopilotRuntimeChatCompletionRequest,
65
96
  ): Promise<CopilotRuntimeChatCompletionResponse> {
@@ -94,24 +125,30 @@ export class AnthropicAdapter implements CopilotServiceAdapter {
94
125
  }
95
126
 
96
127
  // Step 2: Map each message to an Anthropic message, eliminating invalid tool_results
128
+ const processedToolResultIds = new Set<string>();
97
129
  const anthropicMessages = messages
98
130
  .map((message) => {
99
- // For tool results, only include if they match a valid tool_use ID
131
+ // For tool results, only include if they match a valid tool_use ID AND haven't been processed
100
132
  if (message.isResultMessage()) {
101
133
  // Skip if there's no corresponding tool_use
102
134
  if (!validToolUseIds.has(message.actionExecutionId)) {
103
135
  return null; // Will be filtered out later
104
136
  }
105
137
 
106
- // Remove this ID from valid IDs so we don't process duplicates
107
- validToolUseIds.delete(message.actionExecutionId);
138
+ // Skip if we've already processed a result for this tool_use ID
139
+ if (processedToolResultIds.has(message.actionExecutionId)) {
140
+ return null; // Will be filtered out later
141
+ }
142
+
143
+ // Mark this tool result as processed
144
+ processedToolResultIds.add(message.actionExecutionId);
108
145
 
109
146
  return {
110
147
  role: "user",
111
148
  content: [
112
149
  {
113
150
  type: "tool_result",
114
- content: message.result,
151
+ content: message.result || "Action completed successfully",
115
152
  tool_use_id: message.actionExecutionId,
116
153
  },
117
154
  ],
@@ -139,6 +176,7 @@ export class AnthropicAdapter implements CopilotServiceAdapter {
139
176
  // Apply token limits
140
177
  const limitedMessages = limitMessagesToTokenCount(anthropicMessages, tools, model);
141
178
 
179
+ // We'll check if we need a fallback response after seeing what Anthropic returns
142
180
  // We skip grouping by role since we've already ensured uniqueness of tool_results
143
181
 
144
182
  let toolChoice: any = forwardedParameters?.toolChoice;
@@ -171,12 +209,14 @@ export class AnthropicAdapter implements CopilotServiceAdapter {
171
209
  let currentMessageId = randomId();
172
210
  let currentToolCallId = randomId();
173
211
  let filterThinkingTextBuffer = new FilterThinkingTextBuffer();
212
+ let hasReceivedContent = false;
174
213
 
175
214
  try {
176
215
  for await (const chunk of stream as AsyncIterable<any>) {
177
216
  if (chunk.type === "message_start") {
178
217
  currentMessageId = chunk.message.id;
179
218
  } else if (chunk.type === "content_block_start") {
219
+ hasReceivedContent = true;
180
220
  if (chunk.content_block.type === "text") {
181
221
  didOutputText = false;
182
222
  filterThinkingTextBuffer.reset();
@@ -220,15 +260,34 @@ export class AnthropicAdapter implements CopilotServiceAdapter {
220
260
  }
221
261
  }
222
262
  } catch (error) {
223
- console.error("[Anthropic] Error processing stream:", error);
224
- throw error;
263
+ throw convertServiceAdapterError(error, "Anthropic");
264
+ }
265
+
266
+ // Generate fallback response only if Anthropic produced no content
267
+ if (!hasReceivedContent && this.shouldGenerateFallbackResponse(limitedMessages)) {
268
+ // Extract the tool result content for a more contextual response
269
+ let fallbackContent = "Task completed successfully.";
270
+ const lastMessage = limitedMessages[limitedMessages.length - 1];
271
+ if (lastMessage?.role === "user" && Array.isArray(lastMessage.content)) {
272
+ const toolResult = lastMessage.content.find((c: any) => c.type === "tool_result");
273
+ if (toolResult?.content && toolResult.content !== "Action completed successfully") {
274
+ fallbackContent = toolResult.content;
275
+ }
276
+ }
277
+
278
+ currentMessageId = randomId();
279
+ eventStream$.sendTextMessageStart({ messageId: currentMessageId });
280
+ eventStream$.sendTextMessageContent({
281
+ messageId: currentMessageId,
282
+ content: fallbackContent,
283
+ });
284
+ eventStream$.sendTextMessageEnd({ messageId: currentMessageId });
225
285
  }
226
286
 
227
287
  eventStream$.complete();
228
288
  });
229
289
  } catch (error) {
230
- console.error("[Anthropic] Error during API call:", error);
231
- throw error;
290
+ throw convertServiceAdapterError(error, "Anthropic");
232
291
  }
233
292
 
234
293
  return {
@@ -1,11 +1,6 @@
1
- import {
2
- ActionExecutionMessage,
3
- Message,
4
- ResultMessage,
5
- TextMessage,
6
- } from "../../graphql/types/converted";
7
- import { ActionInput } from "../../graphql/inputs/action.input";
8
1
  import { Anthropic } from "@anthropic-ai/sdk";
2
+ import { ActionInput } from "../../graphql/inputs/action.input";
3
+ import { Message } from "../../graphql/types/converted";
9
4
 
10
5
  export function limitMessagesToTokenCount(
11
6
  messages: any[],
@@ -148,7 +143,7 @@ export function convertMessageToAnthropicMessage(
148
143
  content: [
149
144
  {
150
145
  type: "tool_result",
151
- content: message.result,
146
+ content: message.result || "Action completed successfully",
152
147
  tool_use_id: message.actionExecutionId,
153
148
  },
154
149
  ],