Skip to content

Commit d653cb9

Browse files
committed
feat: robust error handling and type safety
Add type-safe error helpers and remove 'as any' casts. Changes: - Add isAbortError() helper for checking abort signals - Add getEventDetail() helper for CustomEvent extraction - Fix variable naming in isRetryable() (isAborted vs isAbortError) - Use proper instanceof checks instead of casting - Add proper interface for normalized response data - Type-safe tool execution error messages
1 parent e2c00b1 commit d653cb9

5 files changed

Lines changed: 85 additions & 25 deletions

File tree

packages/core/src/PageAgentCore.ts

Lines changed: 34 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,15 @@ import type {
2020
MacroToolInput,
2121
MacroToolResult,
2222
} from './types'
23-
import { assert, fetchLlmsTxt, normalizeResponse, uid, waitFor } from './utils'
23+
import {
24+
assert,
25+
fetchLlmsTxt,
26+
getEventDetail,
27+
isAbortError,
28+
normalizeResponse,
29+
uid,
30+
waitFor,
31+
} from './utils'
2432

2533
export { tool, type PageAgentTool } from './tools'
2634
export type * from './types'
@@ -104,27 +112,35 @@ export class PageAgentCore extends EventTarget {
104112

105113
// Listen to LLM retry events
106114
this.#llm.addEventListener('retry', (e) => {
107-
const { attempt, maxAttempts } = (e as CustomEvent).detail
108-
this.#emitActivity({ type: 'retrying', attempt, maxAttempts })
115+
const detail = getEventDetail<{ attempt: number; maxAttempts: number }>(e)
116+
if (!detail) return
117+
this.#emitActivity({
118+
type: 'retrying',
119+
attempt: detail.attempt,
120+
maxAttempts: detail.maxAttempts,
121+
})
109122
// Also push to history for panel rendering
110123
this.history.push({
111124
type: 'retry',
112-
message: `LLM retry attempt ${attempt} of ${maxAttempts}`,
113-
attempt,
114-
maxAttempts,
125+
message: `LLM retry attempt ${detail.attempt} of ${detail.maxAttempts}`,
126+
attempt: detail.attempt,
127+
maxAttempts: detail.maxAttempts,
115128
})
116129
this.#emitHistoryChange()
117130
})
118131
this.#llm.addEventListener('error', (e) => {
119-
const error = (e as CustomEvent).detail.error as Error | InvokeError
120-
if ((error as any)?.rawError?.name === 'AbortError') return
121-
const message = String(error)
132+
const detail = getEventDetail<{ error: unknown }>(e)
133+
if (!detail) return
134+
const error = detail.error
135+
if (isAbortError(error)) return
136+
const message = error instanceof Error ? error.message : String(error)
122137
this.#emitActivity({ type: 'error', message })
123138
// Also push to history for panel rendering
124139
this.history.push({
125140
type: 'error',
126141
message,
127-
rawResponse: (error as InvokeError).rawResponse,
142+
rawResponse:
143+
error instanceof Error ? (error as InvokeError).rawResponse : undefined,
128144
})
129145
this.#emitHistoryChange()
130146
})
@@ -311,10 +327,10 @@ export class PageAgentCore extends EventTarget {
311327
}
312328
} catch (error: unknown) {
313329
console.groupEnd() // to prevent nested groups
314-
const isAbortError = (error as any)?.rawError?.name === 'AbortError'
330+
const isAborted = isAbortError(error)
315331

316332
console.error('Task failed', error)
317-
const errorMessage = isAbortError ? 'Task stopped' : String(error)
333+
const errorMessage = isAborted ? 'Task stopped' : String(error)
318334
this.#emitActivity({ type: 'error', message: errorMessage })
319335
this.history.push({ type: 'error', message: errorMessage, rawResponse: error })
320336
this.#emitHistoryChange()
@@ -511,7 +527,8 @@ export class PageAgentCore extends EventTarget {
511527
// Accumulated wait time warning
512528
if (this.#states.totalWaitTime >= 3) {
513529
this.pushObservation(
514-
`You have waited ${this.#states.totalWaitTime} seconds accumulatively. DO NOT wait any longer unless you have a good reason.`
530+
`You have waited ${this.#states.totalWaitTime} seconds accumulatively. ` +
531+
`DO NOT wait any longer unless you have a good reason.`
515532
)
516533
}
517534

@@ -527,11 +544,13 @@ export class PageAgentCore extends EventTarget {
527544
const remaining = this.config.maxSteps - step
528545
if (remaining === 5) {
529546
this.pushObservation(
530-
`⚠️ Only ${remaining} steps remaining. Consider wrapping up or calling done with partial results.`
547+
`⚠️ Only ${remaining} steps remaining. ` +
548+
`Consider wrapping up or calling done with partial results.`
531549
)
532550
} else if (remaining === 2) {
533551
this.pushObservation(
534-
`⚠️ Critical: Only ${remaining} steps left! You must finish the task or call done immediately.`
552+
`⚠️ Critical: Only ${remaining} steps left! ` +
553+
`You must finish the task or call done immediately.`
535554
)
536555
}
537556

packages/core/src/utils/index.ts

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -101,3 +101,30 @@ export function assert(condition: unknown, message?: string, silent?: boolean):
101101
throw new Error(errorMessage)
102102
}
103103
}
104+
105+
/**
106+
* Check if an error is an AbortError (from AbortController)
107+
* Handles various forms: Error with name 'AbortError', or rawError property
108+
*/
109+
export function isAbortError(error: unknown): boolean {
110+
if (error instanceof Error && error.name === 'AbortError') return true
111+
if (
112+
typeof error === 'object' &&
113+
error !== null &&
114+
'rawError' in error &&
115+
(error as { rawError?: Error }).rawError?.name === 'AbortError'
116+
)
117+
return true
118+
return false
119+
}
120+
121+
/**
122+
* Safely extract detail from CustomEvent
123+
* @returns The detail object or null if not a CustomEvent
124+
*/
125+
export function getEventDetail<T>(event: Event): T | null {
126+
if (event instanceof CustomEvent) {
127+
return event.detail as T
128+
}
129+
return null
130+
}

packages/llms/src/OpenAIClient.ts

Lines changed: 13 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -56,9 +56,9 @@ export class OpenAIClient implements LLMClient {
5656
signal: abortSignal,
5757
})
5858
} catch (error: unknown) {
59-
const isAbortError = (error as any)?.name === 'AbortError'
60-
const errorMessage = isAbortError ? 'Network request aborted' : 'Network request failed'
61-
if (!isAbortError) console.error(error)
59+
const isAborted = error instanceof Error && error.name === 'AbortError'
60+
const errorMessage = isAborted ? 'Network request aborted' : 'Network request failed'
61+
if (!isAborted) console.error(error)
6262
throw new InvokeError(InvokeErrorType.NETWORK_ERROR, errorMessage, error)
6363
}
6464

@@ -135,7 +135,15 @@ export class OpenAIClient implements LLMClient {
135135

136136
// Apply normalizeResponse if provided (for fixing format issues automatically)
137137
const normalizedData = options?.normalizeResponse ? options.normalizeResponse(data) : data
138-
const normalizedChoice = (normalizedData as any).choices?.[0]
138+
const normalizedChoice = (
139+
normalizedData as {
140+
choices?: {
141+
message?: {
142+
tool_calls?: { function?: { name?: string; arguments?: string } }[]
143+
}
144+
}[]
145+
}
146+
)?.choices?.[0]
139147

140148
// Get tool name from response
141149
const toolCallName = normalizedChoice?.message?.tool_calls?.[0]?.function?.name
@@ -201,7 +209,7 @@ export class OpenAIClient implements LLMClient {
201209
} catch (e) {
202210
throw new InvokeError(
203211
InvokeErrorType.TOOL_EXECUTION_ERROR,
204-
`Tool execution failed: ${(e as Error).message}`,
212+
`Tool execution failed: ${e instanceof Error ? e.message : String(e)}`,
205213
e,
206214
data
207215
)

packages/llms/src/errors.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -40,8 +40,8 @@ export class InvokeError extends Error {
4040
}
4141

4242
private isRetryable(type: InvokeErrorType, rawError?: unknown): boolean {
43-
const isAbortError = (rawError as any)?.name === 'AbortError'
44-
if (isAbortError) return false
43+
const isAborted = rawError instanceof Error && rawError.name === 'AbortError'
44+
if (isAborted) return false
4545

4646
const retryableTypes: InvokeErrorType[] = [
4747
InvokeErrorType.NETWORK_ERROR,

packages/llms/src/index.ts

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -93,15 +93,21 @@ async function withRetry<T>(
9393
return await fn()
9494
} catch (error: unknown) {
9595
// do not retry if aborted by user
96-
if ((error as any)?.rawError?.name === 'AbortError') throw error
96+
if (
97+
error instanceof InvokeError &&
98+
error.rawError instanceof Error &&
99+
error.rawError.name === 'AbortError'
100+
) {
101+
throw error
102+
}
97103

98104
console.error(error)
99-
settings.onError(error as Error)
105+
settings.onError(error instanceof Error ? error : new Error(String(error)))
100106

101107
// do not retry if error is not retryable (InvokeError)
102108
if (error instanceof InvokeError && !error.retryable) throw error
103109

104-
lastError = error as Error
110+
lastError = error instanceof Error ? error : new Error(String(error))
105111
attempt++
106112

107113
await new Promise((resolve) => setTimeout(resolve, 100))

0 commit comments

Comments
 (0)