Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
49 changes: 34 additions & 15 deletions packages/core/src/PageAgentCore.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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'
Expand Down Expand Up @@ -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()
})
Expand Down Expand Up @@ -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()
Expand Down Expand Up @@ -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.`
)
}

Expand All @@ -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.`
)
}

Expand Down
27 changes: 27 additions & 0 deletions packages/core/src/utils/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<T>(event: Event): T | null {
if (event instanceof CustomEvent) {
return event.detail as T
}
return null
}
18 changes: 13 additions & 5 deletions packages/llms/src/OpenAIClient.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}

Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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
)
Expand Down
4 changes: 2 additions & 2 deletions packages/llms/src/errors.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
12 changes: 9 additions & 3 deletions packages/llms/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -93,15 +93,21 @@ async function withRetry<T>(
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))
Expand Down
Loading