Skip to content

Commit e141f3f

Browse files
committed
feat(sdk): improve error handling and surface SDK errors
- Add ErrorOr utilities and SdkErrorObject type to public exports - Create database-safe wrapper with ErrorOr return types - Update run() to use safe database calls and handle errors gracefully - Improve validateAgents error handling for network failures - Wrap validation API errors in NetworkError with proper metadata
1 parent 690768d commit e141f3f

File tree

5 files changed

+130
-10
lines changed

5 files changed

+130
-10
lines changed

sdk/src/error-or.ts

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
import { failure, getErrorObject, type ErrorObject, type Failure } from '../../common/src/util/error'
2+
3+
// SDK-level error object that preserves optional code/status when present on Error instances
4+
export type SdkErrorObject = ErrorObject & {
5+
code?: string
6+
status?: number
7+
originalError?: unknown
8+
}
9+
10+
/**
11+
* Wrap an unknown error into a Failure<SdkErrorObject>, preserving `code`, `status`, and `originalError`
12+
* when present on known SDK error types like NetworkError or AuthenticationError.
13+
*/
14+
export function failureWithCode(error: unknown): Failure<SdkErrorObject> {
15+
if (error instanceof Error) {
16+
const base = getErrorObject(error)
17+
const anyErr = error as any
18+
19+
const enriched: SdkErrorObject = {
20+
...base,
21+
}
22+
23+
if (typeof anyErr.code === 'string') {
24+
enriched.code = anyErr.code
25+
}
26+
27+
if (typeof anyErr.status === 'number') {
28+
enriched.status = anyErr.status
29+
}
30+
31+
if ('originalError' in anyErr) {
32+
enriched.originalError = anyErr.originalError
33+
}
34+
35+
return {
36+
success: false,
37+
error: enriched,
38+
}
39+
}
40+
41+
// Fallback to base failure, which will still give us an ErrorObject
42+
return failure(error)
43+
}

sdk/src/impl/database-safe.ts

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
import { getUserInfoFromApiKey } from './database'
2+
import { failureWithCode, type SdkErrorObject } from '../error-or'
3+
import type {
4+
GetUserInfoFromApiKeyInput,
5+
UserColumn,
6+
} from '@codebuff/common/types/contracts/database'
7+
import type { ErrorOr } from '@codebuff/common/util/error'
8+
9+
type User = {
10+
id: string
11+
email: string
12+
discord_id: string | null
13+
}
14+
15+
export type GetUserInfoFromApiKeySafeError = SdkErrorObject
16+
17+
export async function getUserInfoFromApiKeySafe<T extends UserColumn>(
18+
params: GetUserInfoFromApiKeyInput<T>,
19+
): Promise<ErrorOr<{ [K in T]: User[K] } | null, GetUserInfoFromApiKeySafeError>> {
20+
try {
21+
const result = await getUserInfoFromApiKey<T>(params)
22+
return {
23+
success: true,
24+
value: result,
25+
}
26+
} catch (error) {
27+
return failureWithCode(error)
28+
}
29+
}

sdk/src/index.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ export { formatState } from '../../common/src/websockets/websocket-client'
2626
export type { ReadyState } from '../../common/src/websockets/websocket-client'
2727

2828
export { getUserInfoFromApiKey } from './impl/database'
29+
export { getUserInfoFromApiKeySafe } from './impl/database-safe'
2930

3031
export { validateAgents } from './validate-agents'
3132

@@ -42,6 +43,10 @@ export {
4243
} from './errors'
4344
export type { ValidationResult, ValidateAgentsOptions } from './validate-agents'
4445

46+
// ErrorOr utilities
47+
export { failureWithCode, type SdkErrorObject } from './error-or'
48+
export type { ErrorOr, Success, Failure } from '@codebuff/common/util/error'
49+
4550
export type { CodebuffFileSystem } from '@codebuff/common/types/filesystem'
4651

4752
export { runTerminalCommand } from './tools/run-terminal-command'

sdk/src/run.ts

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ import { AgentOutputSchema } from '@codebuff/common/types/session-state'
1111
import { cloneDeep } from 'lodash'
1212

1313
import { getAgentRuntimeImpl } from './impl/agent-runtime'
14-
import { getUserInfoFromApiKey } from './impl/database'
14+
import { getUserInfoFromApiKeySafe } from './impl/database-safe'
1515
import { initialSessionState, applyOverridesToSessionState } from './run-state'
1616
import { filterXml } from './tool-xml-filter'
1717
import { changeFile } from './tools/change-file'
@@ -488,11 +488,21 @@ export async function run({
488488
const promptId = Math.random().toString(36).substring(2, 15)
489489

490490
// Send input
491-
const userInfo = await getUserInfoFromApiKey({
491+
const userInfoResult = await getUserInfoFromApiKeySafe({
492492
...agentRuntimeImpl,
493493
apiKey,
494494
fields: ['id'],
495495
})
496+
497+
if (!userInfoResult.success) {
498+
const err = userInfoResult.error
499+
const errorMessage = err.message || 'Failed to resolve user information from API key'
500+
await onError({ message: errorMessage })
501+
clearStreamTimeout()
502+
return getCancelledRunState(errorMessage)
503+
}
504+
505+
const userInfo = userInfoResult.value
496506
if (!userInfo) {
497507
const errorMessage = 'Invalid API key or user not found'
498508
await onError({ message: errorMessage })

sdk/src/validate-agents.ts

Lines changed: 41 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,22 @@ export interface ValidateAgentsOptions {
3030
websiteUrl?: string
3131
}
3232

33+
function buildValidationApiNetworkError(params: {
34+
message: string
35+
status?: number
36+
original?: unknown
37+
}): NetworkError {
38+
// For 5xx errors, use "Server error" prefix to match test expectations
39+
// For network failures, use "Failed to connect" prefix
40+
const prefix = params.status && params.status >= 500 ? 'Server error' : 'Failed to connect to validation API'
41+
const baseMessage = `${prefix}: ${params.message}`
42+
const wrapped = new NetworkError(baseMessage, {
43+
status: params.status,
44+
originalError: params.original,
45+
})
46+
return wrapped
47+
}
48+
3349
/**
3450
* Validates an array of agent definitions.
3551
*
@@ -63,7 +79,7 @@ export async function validateAgents(
6379
for (const [index, definition] of definitions.entries()) {
6480
// Handle null/undefined gracefully
6581
if (!definition) {
66-
agentTemplates[`agent_${index}`] = definition
82+
agentTemplates[`agent_${index}`] = definition as AgentDefinition
6783
continue
6884
}
6985
// Use index to ensure duplicates aren't overwritten
@@ -96,14 +112,28 @@ export async function validateAgents(
96112
})
97113

98114
if (!response.ok) {
99-
const errorData = await response.json().catch(() => ({}))
115+
let errorData: any = {}
116+
try {
117+
errorData = await response.json()
118+
} catch {
119+
// ignore JSON parse errors, we'll fall back to status text
120+
}
100121
const errorMessage =
101122
(errorData as any).error ||
102123
`HTTP ${response.status}: ${response.statusText}`
103124

104-
// For 5xx errors, throw network error
125+
// For 5xx errors, throw a NetworkError with original error metadata
105126
if (response.status >= 500) {
106-
throw new NetworkError(`Server error: ${errorMessage}`, { status: response.status })
127+
const original = {
128+
status: response.status,
129+
statusText: response.statusText,
130+
body: errorData,
131+
}
132+
throw buildValidationApiNetworkError({
133+
message: errorMessage,
134+
status: response.status,
135+
original,
136+
})
107137
}
108138

109139
// For client errors (4xx), return as validation errors
@@ -122,11 +152,14 @@ export async function validateAgents(
122152
const data = await response.json()
123153
validationErrors = data.validationErrors || []
124154
} catch (error) {
125-
const errorMessage =
126-
error instanceof Error ? error.message : String(error)
155+
const message = error instanceof Error ? error.message : String(error)
127156

128-
// Throw network errors instead of returning them as validation errors
129-
throw new NetworkError(`Failed to connect to validation API: ${errorMessage}`, { originalError: error })
157+
// Wrap all network failures in a NetworkError that includes the original error
158+
const networkError = buildValidationApiNetworkError({
159+
message,
160+
original: error,
161+
})
162+
throw networkError
130163
}
131164
} else {
132165
// Local validation: use common package validation logic

0 commit comments

Comments
 (0)