forked from CopilotKit/CopilotKit
-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathuse-copilot-runtime-client.ts
More file actions
178 lines (163 loc) · 5.83 KB
/
Copy pathuse-copilot-runtime-client.ts
File metadata and controls
178 lines (163 loc) · 5.83 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
import {
CopilotRuntimeClient,
CopilotRuntimeClientOptions,
GraphQLError,
} from "@copilotkit/runtime-client-gql";
import { useToast } from "../components/toast/toast-provider";
import { useMemo, useRef } from "react";
import {
ErrorVisibility,
CopilotKitApiDiscoveryError,
CopilotKitRemoteEndpointDiscoveryError,
CopilotKitAgentDiscoveryError,
CopilotKitError,
CopilotKitErrorCode,
CopilotErrorHandler,
CopilotErrorEvent,
} from "@copilotkit/shared";
import { shouldShowDevConsole } from "../utils/dev-console";
export interface CopilotRuntimeClientHookOptions extends CopilotRuntimeClientOptions {
showDevConsole?: boolean;
onError: CopilotErrorHandler;
}
export const useCopilotRuntimeClient = (
options: CopilotRuntimeClientHookOptions,
) => {
const { setBannerError } = useToast();
const { showDevConsole, onError, ...runtimeOptions } = options;
// Deduplication state for structured errors
const lastStructuredErrorRef = useRef<{
message: string;
timestamp: number;
} | null>(null);
// Helper function to trace UI errors
const traceUIError = async (error: CopilotKitError, originalError?: any) => {
try {
const errorEvent: CopilotErrorEvent = {
type: "error",
timestamp: Date.now(),
context: {
source: "ui",
request: {
operation: "runtimeClient",
url: runtimeOptions.url,
startTime: Date.now(),
},
technical: {
environment: "browser",
userAgent:
typeof navigator !== "undefined"
? navigator.userAgent
: undefined,
stackTrace:
originalError instanceof Error ? originalError.stack : undefined,
},
},
error,
};
await onError(errorEvent);
} catch (error) {
console.error("Error in onError handler:", error);
}
};
const runtimeClient = useMemo(() => {
return new CopilotRuntimeClient({
...runtimeOptions,
handleGQLErrors: (error) => {
if ((error as any).graphQLErrors?.length) {
const graphQLErrors = (error as any).graphQLErrors as GraphQLError[];
// Route all errors to banners for consistent UI
const routeError = (gqlError: GraphQLError) => {
const extensions = gqlError.extensions;
const visibility = extensions?.visibility as ErrorVisibility;
// Silent errors - just log
if (visibility === ErrorVisibility.SILENT) {
console.error("CopilotKit Silent Error:", gqlError.message);
return;
}
// All errors (including DEV_ONLY) show as banners for consistency
// Deduplicate to prevent spam
const now = Date.now();
const errorMessage = gqlError.message;
if (
lastStructuredErrorRef.current &&
lastStructuredErrorRef.current.message === errorMessage &&
now - lastStructuredErrorRef.current.timestamp < 150
) {
return; // Skip duplicate
}
lastStructuredErrorRef.current = {
message: errorMessage,
timestamp: now,
};
const ckError = createStructuredError(gqlError);
if (ckError) {
setBannerError(ckError);
// Trace the error
traceUIError(ckError, gqlError);
// TODO: if onError & renderError should work without key, insert here
} else {
// Fallback for unstructured errors
const fallbackError = new CopilotKitError({
message: gqlError.message,
code: CopilotKitErrorCode.UNKNOWN,
});
setBannerError(fallbackError);
// Trace the fallback error
traceUIError(fallbackError, gqlError);
// TODO: if onError & renderError should work without key, insert here
}
};
// Process all errors as banners
graphQLErrors.forEach(routeError);
} else {
// Route non-GraphQL errors to banner as well
const fallbackError = new CopilotKitError({
message: error?.message || String(error),
code: CopilotKitErrorCode.UNKNOWN,
});
setBannerError(fallbackError);
// Trace the non-GraphQL error
traceUIError(fallbackError, error);
// TODO: if onError & renderError should work without key, insert here
}
},
handleGQLWarning: (message: string) => {
console.warn(message);
// Show warnings as banners too for consistency
const warningError = new CopilotKitError({
message,
code: CopilotKitErrorCode.UNKNOWN,
});
setBannerError(warningError);
},
});
}, [runtimeOptions, setBannerError, onError]);
return runtimeClient;
};
// Create appropriate structured error from GraphQL error
function createStructuredError(gqlError: GraphQLError): CopilotKitError | null {
const extensions = gqlError.extensions;
const originalError = extensions?.originalError as any;
const message = originalError?.message || gqlError.message;
const code = extensions?.code as CopilotKitErrorCode;
if (code) {
return new CopilotKitError({ message, code });
}
// Legacy error detection by stack trace
if (originalError?.stack?.includes("CopilotApiDiscoveryError")) {
return new CopilotKitApiDiscoveryError({ message });
}
if (
originalError?.stack?.includes("CopilotKitRemoteEndpointDiscoveryError")
) {
return new CopilotKitRemoteEndpointDiscoveryError({ message });
}
if (originalError?.stack?.includes("CopilotKitAgentDiscoveryError")) {
return new CopilotKitAgentDiscoveryError({
agentName: "",
availableAgents: [],
});
}
return null;
}