diff --git a/packages/core/src/PageAgentCore.ts b/packages/core/src/PageAgentCore.ts index c7ab84b0a..5ad2f6524 100644 --- a/packages/core/src/PageAgentCore.ts +++ b/packages/core/src/PageAgentCore.ts @@ -20,7 +20,15 @@ import type { MacroToolInput, MacroToolResult, } from './types' -import { assert, fetchLlmsTxt, normalizeResponse, uid, waitFor } from './utils' +import { + assert, + fetchLlmsTxt, + getEventDetail, + isAbortError, + normalizeResponse, + uid, + waitFor, +} from './utils' export { tool, type PageAgentTool } from './tools' export type * from './types' @@ -104,27 +112,35 @@ export class PageAgentCore extends EventTarget { // Listen to LLM retry events this.#llm.addEventListener('retry', (e) => { - const { attempt, maxAttempts } = (e as CustomEvent).detail - this.#emitActivity({ type: 'retrying', attempt, maxAttempts }) + const detail = getEventDetail<{ attempt: number; maxAttempts: number }>(e) + if (!detail) return + this.#emitActivity({ + type: 'retrying', + attempt: detail.attempt, + maxAttempts: detail.maxAttempts, + }) // Also push to history for panel rendering this.history.push({ type: 'retry', - message: `LLM retry attempt ${attempt} of ${maxAttempts}`, - attempt, - maxAttempts, + message: `LLM retry attempt ${detail.attempt} of ${detail.maxAttempts}`, + attempt: detail.attempt, + maxAttempts: detail.maxAttempts, }) this.#emitHistoryChange() }) this.#llm.addEventListener('error', (e) => { - const error = (e as CustomEvent).detail.error as Error | InvokeError - if ((error as any)?.rawError?.name === 'AbortError') return - const message = String(error) + const detail = getEventDetail<{ error: unknown }>(e) + if (!detail) return + const error = detail.error + if (isAbortError(error)) return + const message = error instanceof Error ? error.message : String(error) this.#emitActivity({ type: 'error', message }) // Also push to history for panel rendering this.history.push({ type: 'error', message, - rawResponse: (error as InvokeError).rawResponse, + rawResponse: + error instanceof Error ? (error as InvokeError).rawResponse : undefined, }) this.#emitHistoryChange() }) @@ -311,10 +327,10 @@ export class PageAgentCore extends EventTarget { } } catch (error: unknown) { console.groupEnd() // to prevent nested groups - const isAbortError = (error as any)?.rawError?.name === 'AbortError' + const isAborted = isAbortError(error) console.error('Task failed', error) - const errorMessage = isAbortError ? 'Task stopped' : String(error) + const errorMessage = isAborted ? 'Task stopped' : String(error) this.#emitActivity({ type: 'error', message: errorMessage }) this.history.push({ type: 'error', message: errorMessage, rawResponse: error }) this.#emitHistoryChange() @@ -511,7 +527,8 @@ export class PageAgentCore extends EventTarget { // Accumulated wait time warning if (this.#states.totalWaitTime >= 3) { this.pushObservation( - `You have waited ${this.#states.totalWaitTime} seconds accumulatively. DO NOT wait any longer unless you have a good reason.` + `You have waited ${this.#states.totalWaitTime} seconds accumulatively. ` + + `DO NOT wait any longer unless you have a good reason.` ) } @@ -527,11 +544,13 @@ export class PageAgentCore extends EventTarget { const remaining = this.config.maxSteps - step if (remaining === 5) { this.pushObservation( - `⚠️ Only ${remaining} steps remaining. Consider wrapping up or calling done with partial results.` + `⚠️ Only ${remaining} steps remaining. ` + + `Consider wrapping up or calling done with partial results.` ) } else if (remaining === 2) { this.pushObservation( - `⚠️ Critical: Only ${remaining} steps left! You must finish the task or call done immediately.` + `⚠️ Critical: Only ${remaining} steps left! ` + + `You must finish the task or call done immediately.` ) } diff --git a/packages/core/src/utils/index.ts b/packages/core/src/utils/index.ts index b740be1bc..84a8b558c 100644 --- a/packages/core/src/utils/index.ts +++ b/packages/core/src/utils/index.ts @@ -101,3 +101,30 @@ export function assert(condition: unknown, message?: string, silent?: boolean): throw new Error(errorMessage) } } + +/** + * Check if an error is an AbortError (from AbortController) + * Handles various forms: Error with name 'AbortError', or rawError property + */ +export function isAbortError(error: unknown): boolean { + if (error instanceof Error && error.name === 'AbortError') return true + if ( + typeof error === 'object' && + error !== null && + 'rawError' in error && + (error as { rawError?: Error }).rawError?.name === 'AbortError' + ) + return true + return false +} + +/** + * Safely extract detail from CustomEvent + * @returns The detail object or null if not a CustomEvent + */ +export function getEventDetail(event: Event): T | null { + if (event instanceof CustomEvent) { + return event.detail as T + } + return null +} diff --git a/packages/llms/src/OpenAIClient.ts b/packages/llms/src/OpenAIClient.ts index b78ba445a..c41ae3ab5 100644 --- a/packages/llms/src/OpenAIClient.ts +++ b/packages/llms/src/OpenAIClient.ts @@ -56,9 +56,9 @@ export class OpenAIClient implements LLMClient { signal: abortSignal, }) } catch (error: unknown) { - const isAbortError = (error as any)?.name === 'AbortError' - const errorMessage = isAbortError ? 'Network request aborted' : 'Network request failed' - if (!isAbortError) console.error(error) + const isAborted = error instanceof Error && error.name === 'AbortError' + const errorMessage = isAborted ? 'Network request aborted' : 'Network request failed' + if (!isAborted) console.error(error) throw new InvokeError(InvokeErrorType.NETWORK_ERROR, errorMessage, error) } @@ -135,7 +135,15 @@ export class OpenAIClient implements LLMClient { // Apply normalizeResponse if provided (for fixing format issues automatically) const normalizedData = options?.normalizeResponse ? options.normalizeResponse(data) : data - const normalizedChoice = (normalizedData as any).choices?.[0] + const normalizedChoice = ( + normalizedData as { + choices?: { + message?: { + tool_calls?: { function?: { name?: string; arguments?: string } }[] + } + }[] + } + )?.choices?.[0] // Get tool name from response const toolCallName = normalizedChoice?.message?.tool_calls?.[0]?.function?.name @@ -201,7 +209,7 @@ export class OpenAIClient implements LLMClient { } catch (e) { throw new InvokeError( InvokeErrorType.TOOL_EXECUTION_ERROR, - `Tool execution failed: ${(e as Error).message}`, + `Tool execution failed: ${e instanceof Error ? e.message : String(e)}`, e, data ) diff --git a/packages/llms/src/errors.ts b/packages/llms/src/errors.ts index 378af750b..da6cbbe06 100644 --- a/packages/llms/src/errors.ts +++ b/packages/llms/src/errors.ts @@ -40,8 +40,8 @@ export class InvokeError extends Error { } private isRetryable(type: InvokeErrorType, rawError?: unknown): boolean { - const isAbortError = (rawError as any)?.name === 'AbortError' - if (isAbortError) return false + const isAborted = rawError instanceof Error && rawError.name === 'AbortError' + if (isAborted) return false const retryableTypes: InvokeErrorType[] = [ InvokeErrorType.NETWORK_ERROR, diff --git a/packages/llms/src/index.ts b/packages/llms/src/index.ts index 0a1cad3a5..6e6c31d44 100644 --- a/packages/llms/src/index.ts +++ b/packages/llms/src/index.ts @@ -93,15 +93,21 @@ async function withRetry( return await fn() } catch (error: unknown) { // do not retry if aborted by user - if ((error as any)?.rawError?.name === 'AbortError') throw error + if ( + error instanceof InvokeError && + error.rawError instanceof Error && + error.rawError.name === 'AbortError' + ) { + throw error + } console.error(error) - settings.onError(error as Error) + settings.onError(error instanceof Error ? error : new Error(String(error))) // do not retry if error is not retryable (InvokeError) if (error instanceof InvokeError && !error.retryable) throw error - lastError = error as Error + lastError = error instanceof Error ? error : new Error(String(error)) attempt++ await new Promise((resolve) => setTimeout(resolve, 100))