Skip to content

Commit 3d1771b

Browse files
committed
refactor: make error code field required and add standard error codes
- Make code field required in PrintModeError schema - Add ErrorCodes constants (AUTH_FAILED, NETWORK_ERROR, VALIDATION_ERROR, etc.) - Update all error creation sites to include appropriate error codes - Remove all `as any` type casting for error.code - Update tests to include error codes - All TypeScript checks pass
1 parent a6d1bc1 commit 3d1771b

File tree

8 files changed

+56
-49
lines changed

8 files changed

+56
-49
lines changed

cli/src/hooks/use-send-message.ts

Lines changed: 5 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -1204,7 +1204,7 @@ export const useSendMessage = ({
12041204
// Handle error events from SDK
12051205
if (event.type === 'error') {
12061206
logger.error(
1207-
{ errorMessage: event.message },
1207+
{ errorMessage: event.message, code: event.code },
12081208
'SDK error event received',
12091209
)
12101210
currentRunContextRef.current = null
@@ -1239,14 +1239,8 @@ export const useSendMessage = ({
12391239
)
12401240

12411241
// Track failed messages for batch retry on reconnection (avoids state update cascade)
1242-
const errorMsg = event.message || ''
12431242
const isConnectionError =
1244-
!isConnectedRef.current ||
1245-
(typeof errorMsg === 'string' &&
1246-
(errorMsg.includes('Failed to start agent run') ||
1247-
errorMsg.includes('Unable to connect') ||
1248-
errorMsg.includes('network') ||
1249-
errorMsg.includes('fetch failed')))
1243+
!isConnectedRef.current || event.code === 'NETWORK_ERROR'
12501244

12511245
if (isConnectionError && userMessageId && content && agentMode) {
12521246
failedDueToConnectionRef.current[userMessageId] = {
@@ -1917,25 +1911,9 @@ export const useSendMessage = ({
19171911

19181912
logger.warn({ errorMessage }, 'Agent run failed')
19191913

1920-
// Track failed messages for batch retry on reconnection (avoids state update cascade)
1921-
const isConnectionError =
1922-
!isConnectedRef.current ||
1923-
(typeof errorMessage === 'string' &&
1924-
(errorMessage.includes('Unable to connect') ||
1925-
errorMessage.includes('network') ||
1926-
errorMessage.includes('fetch failed') ||
1927-
errorMessage.includes('connection')))
1928-
1929-
if (isConnectionError && userMessageId && content && agentMode) {
1930-
logger.info(
1931-
{ userMessageId },
1932-
'[RUNSTATE-ERROR] Tracking message for retry on reconnection',
1933-
)
1934-
failedDueToConnectionRef.current[userMessageId] = {
1935-
content,
1936-
agentMode,
1937-
}
1938-
}
1914+
// Note: Connection errors should be caught as thrown NetworkError exceptions
1915+
// in the catch block below, not through runState.output.type === 'error'.
1916+
// If we get here with an error output, it's likely a non-retryable error.
19391917

19401918
return
19411919
}

common/src/types/print-mode.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,8 @@ export type PrintModeStart = z.infer<typeof printModeStartSchema>
1212
export const printModeErrorSchema = z.object({
1313
type: z.literal('error'),
1414
message: z.string(),
15+
// Machine-readable error code for consistent error handling
16+
code: z.string(),
1517
})
1618
export type PrintModeError = z.infer<typeof printModeErrorSchema>
1719

common/src/util/error.ts

Lines changed: 13 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -76,22 +76,27 @@ export type ExtendedErrorObject = ErrorObject & {
7676
export function failureWithCode(error: unknown): Failure<ExtendedErrorObject> {
7777
if (error instanceof Error) {
7878
const base = getErrorObject(error)
79-
const anyErr = error as any
80-
8179
const enriched: ExtendedErrorObject = {
8280
...base,
8381
}
8482

85-
if (typeof anyErr.code === 'string') {
86-
enriched.code = anyErr.code
83+
// Safely extract code, status, and originalError if present
84+
const errorWithMetadata = error as Error & {
85+
code?: string
86+
status?: number
87+
originalError?: unknown
88+
}
89+
90+
if (typeof errorWithMetadata.code === 'string') {
91+
enriched.code = errorWithMetadata.code
8792
}
8893

89-
if (typeof anyErr.status === 'number') {
90-
enriched.status = anyErr.status
94+
if (typeof errorWithMetadata.status === 'number') {
95+
enriched.status = errorWithMetadata.status
9196
}
9297

93-
if ('originalError' in anyErr) {
94-
enriched.originalError = anyErr.originalError
98+
if ('originalError' in errorWithMetadata) {
99+
enriched.originalError = errorWithMetadata.originalError
95100
}
96101

97102
return {

packages/agent-runtime/src/tool-stream-parser.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -110,6 +110,7 @@ export async function* processStreamWithTags(params: {
110110
onResponseChunk({
111111
type: 'error',
112112
message: errorMessage,
113+
code: 'VALIDATION_ERROR',
113114
})
114115
onError('parse_error', errorMessage)
115116
return

packages/agent-runtime/src/tools/stream-parser.ts

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -206,7 +206,13 @@ export async function processStreamWithTools(
206206
onResponseChunk(chunk.text)
207207
fullResponseChunks.push(chunk.text)
208208
} else if (chunk.type === 'error') {
209-
onResponseChunk(chunk)
209+
// Ensure error has a code field
210+
const code = 'code' in chunk && typeof chunk.code === 'string' ? chunk.code : 'INTERNAL_ERROR'
211+
onResponseChunk({
212+
type: 'error',
213+
message: chunk.message,
214+
code,
215+
})
210216
} else {
211217
chunk satisfies never
212218
}

sdk/src/__tests__/client.test.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -148,7 +148,7 @@ describe('CodebuffClient', () => {
148148
// Trigger the default error handler
149149
const defaultHandler = client.options.handleEvent
150150
if (defaultHandler) {
151-
defaultHandler({ type: 'error', message: 'Test error' })
151+
defaultHandler({ type: 'error', message: 'Test error', code: 'TEST_ERROR' })
152152
}
153153

154154
expect(consoleErrorSpy).toHaveBeenCalledTimes(2) // Error message + tip
@@ -199,7 +199,7 @@ describe('CodebuffClient', () => {
199199

200200
// Trigger the handler
201201
if (client.options.handleEvent) {
202-
client.options.handleEvent({ type: 'error', message: 'Test' })
202+
client.options.handleEvent({ type: 'error', message: 'Test', code: 'TEST_ERROR' })
203203
}
204204

205205
// Custom handler should be called, not console.error
@@ -216,7 +216,7 @@ describe('CodebuffClient', () => {
216216

217217
const defaultHandler = client.options.handleEvent
218218
if (defaultHandler) {
219-
defaultHandler({ type: 'error', message: 'Connection failed' })
219+
defaultHandler({ type: 'error', message: 'Connection failed', code: 'NETWORK_ERROR' })
220220
}
221221

222222
const calls = consoleErrorSpy.mock.calls

sdk/src/errors.ts

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,20 @@
22
* Custom error classes for the SDK
33
*/
44

5+
/**
6+
* Standard error codes used throughout the SDK
7+
*/
8+
export const ErrorCodes = {
9+
AUTH_FAILED: 'AUTH_FAILED',
10+
NETWORK_ERROR: 'NETWORK_ERROR',
11+
VALIDATION_ERROR: 'VALIDATION_ERROR',
12+
INTERNAL_ERROR: 'INTERNAL_ERROR',
13+
INVALID_RESPONSE: 'INVALID_RESPONSE',
14+
USER_NOT_FOUND: 'USER_NOT_FOUND',
15+
} as const
16+
17+
export type ErrorCode = typeof ErrorCodes[keyof typeof ErrorCodes]
18+
519
/**
620
* Sanitizes error messages by removing unhelpful system messages
721
*/

sdk/src/run.ts

Lines changed: 11 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ import { glob } from './tools/glob'
2020
import { listDirectory } from './tools/list-directory'
2121
import { getFiles } from './tools/read-files'
2222
import { runTerminalCommand } from './tools/run-terminal-command'
23-
import { NetworkError } from './errors'
23+
import { NetworkError, ErrorCodes } from './errors'
2424

2525
import type { CustomToolDefinition } from './custom-tool'
2626
import type { RunState } from './run-state'
@@ -245,9 +245,9 @@ export async function run({
245245
return getCancelledRunState()
246246
}
247247

248-
async function onError(error: { message: string }) {
248+
async function onError(error: { message: string; code: string }) {
249249
if (handleEvent) {
250-
await handleEvent({ type: 'error', message: error.message })
250+
await handleEvent({ type: 'error', message: error.message, code: error.code })
251251
}
252252
}
253253

@@ -435,7 +435,7 @@ export async function run({
435435
},
436436
sendAction: ({ action }) => {
437437
if (action.type === 'action-error') {
438-
onError({ message: action.message })
438+
onError({ message: action.message, code: ErrorCodes.INTERNAL_ERROR })
439439
return
440440
}
441441
if (action.type === 'response-chunk') {
@@ -497,15 +497,15 @@ export async function run({
497497
if (!userInfoResult.success) {
498498
const err = userInfoResult.error
499499
const errorMessage = err.message || 'Failed to resolve user information from API key'
500-
await onError({ message: errorMessage })
500+
await onError({ message: errorMessage, code: err.code ?? ErrorCodes.INTERNAL_ERROR })
501501
clearStreamTimeout()
502502
return getCancelledRunState(errorMessage)
503503
}
504504

505505
const userInfo = userInfoResult.value
506506
if (!userInfo) {
507507
const errorMessage = 'Invalid API key or user not found'
508-
await onError({ message: errorMessage })
508+
await onError({ message: errorMessage, code: ErrorCodes.USER_NOT_FOUND })
509509
clearStreamTimeout()
510510
return getCancelledRunState(errorMessage)
511511
}
@@ -538,7 +538,7 @@ export async function run({
538538
return
539539
}
540540
const errorMessage = error.message || String(error)
541-
await onError({ message: errorMessage })
541+
await onError({ message: errorMessage, code: ErrorCodes.NETWORK_ERROR })
542542
settleResolve(getCancelledRunState(errorMessage))
543543
})
544544
} catch (error) {
@@ -698,11 +698,11 @@ async function handlePromptResponse({
698698
}: {
699699
action: ServerAction<'prompt-response'> | ServerAction<'prompt-error'>
700700
resolve: (value: RunReturnType) => any
701-
onError: (error: { message: string }) => void
701+
onError: (error: { message: string; code: string }) => void
702702
initialSessionState: SessionState
703703
}) {
704704
if (action.type === 'prompt-error') {
705-
onError({ message: action.message })
705+
onError({ message: action.message, code: ErrorCodes.INTERNAL_ERROR })
706706
resolve({
707707
sessionState: initialSessionState,
708708
output: {
@@ -720,7 +720,7 @@ async function handlePromptResponse({
720720
JSON.stringify(parsedOutput.error.issues),
721721
'If this issues persists, please contact support@codebuff.com',
722722
].join('\n')
723-
onError({ message })
723+
onError({ message, code: ErrorCodes.INVALID_RESPONSE })
724724
resolve({
725725
sessionState: initialSessionState,
726726
output: {
@@ -744,6 +744,7 @@ async function handlePromptResponse({
744744
action satisfies never
745745
onError({
746746
message: 'Internal error: prompt response type not handled',
747+
code: ErrorCodes.INTERNAL_ERROR,
747748
})
748749
resolve({
749750
sessionState: initialSessionState,

0 commit comments

Comments
 (0)