2025-09-29 16:33:15 +08:00
|
|
|
/**
|
|
|
|
|
* Copyright (C) 2025 Alibaba Group Holding Limited
|
|
|
|
|
* All rights reserved.
|
|
|
|
|
*/
|
2026-01-13 13:49:19 +08:00
|
|
|
import { LLM, type Tool } from '@page-agent/llms'
|
2025-12-05 16:18:01 +08:00
|
|
|
import { PageController } from '@page-agent/page-controller'
|
2026-01-13 17:33:38 +08:00
|
|
|
import { Panel } from '@page-agent/ui'
|
2025-09-29 16:33:15 +08:00
|
|
|
import chalk from 'chalk'
|
|
|
|
|
import zod from 'zod'
|
|
|
|
|
|
|
|
|
|
import type { PageAgentConfig } from './config'
|
2025-12-05 16:18:01 +08:00
|
|
|
import { MAX_STEPS } from './config/constants'
|
2025-09-29 16:33:15 +08:00
|
|
|
import SYSTEM_PROMPT from './prompts/system_prompt.md?raw'
|
2025-10-21 18:47:16 +08:00
|
|
|
import { tools } from './tools'
|
2026-01-13 13:49:19 +08:00
|
|
|
import { normalizeResponse, trimLines, uid, waitUntil } from './utils'
|
2025-09-29 16:33:15 +08:00
|
|
|
import { assert } from './utils/assert'
|
|
|
|
|
|
2026-01-13 13:49:19 +08:00
|
|
|
/**
|
|
|
|
|
* Agent brain state - the reflection-before-action model
|
|
|
|
|
*
|
|
|
|
|
* Every tool call must first reflect on:
|
|
|
|
|
* - evaluation_previous_goal: How well did the previous action achieve its goal?
|
|
|
|
|
* - memory: Key information to remember for future steps
|
|
|
|
|
* - next_goal: What should be accomplished in the next action?
|
|
|
|
|
*/
|
|
|
|
|
export interface AgentReflection {
|
|
|
|
|
evaluation_previous_goal: string
|
|
|
|
|
memory: string
|
|
|
|
|
next_goal: string
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* MacroTool input structure
|
|
|
|
|
*
|
|
|
|
|
* This is the core abstraction that enforces the "reflection-before-action" mental model.
|
|
|
|
|
* Before executing any action, the LLM must output its reasoning state.
|
|
|
|
|
*/
|
|
|
|
|
export interface MacroToolInput extends Partial<AgentReflection> {
|
|
|
|
|
action: Record<string, any>
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* MacroTool output structure
|
|
|
|
|
*/
|
|
|
|
|
export interface MacroToolResult {
|
|
|
|
|
input: MacroToolInput
|
|
|
|
|
output: string
|
|
|
|
|
}
|
|
|
|
|
|
2025-09-29 16:33:15 +08:00
|
|
|
export type { PageAgentConfig }
|
2025-10-21 19:29:13 +08:00
|
|
|
export { tool, type PageAgentTool } from './tools'
|
2025-10-17 18:43:41 +08:00
|
|
|
|
2026-01-15 19:18:25 +08:00
|
|
|
/**
|
|
|
|
|
* A single agent step with reflection and action
|
|
|
|
|
*/
|
|
|
|
|
export interface AgentStep {
|
|
|
|
|
type: 'step'
|
2026-01-13 14:21:57 +08:00
|
|
|
brain: Partial<AgentReflection>
|
2025-09-29 16:33:15 +08:00
|
|
|
action: {
|
|
|
|
|
name: string
|
|
|
|
|
input: any
|
2025-10-17 18:43:41 +08:00
|
|
|
output: string
|
|
|
|
|
}
|
|
|
|
|
usage: {
|
|
|
|
|
promptTokens: number
|
|
|
|
|
completionTokens: number
|
|
|
|
|
totalTokens: number
|
|
|
|
|
cachedTokens?: number
|
|
|
|
|
reasoningTokens?: number
|
2025-09-29 16:33:15 +08:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-01-15 19:18:25 +08:00
|
|
|
/**
|
|
|
|
|
* Persistent observation event (stays in memory)
|
|
|
|
|
*/
|
|
|
|
|
export interface ObservationEvent {
|
|
|
|
|
type: 'observation'
|
|
|
|
|
content: string
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* User takeover event
|
|
|
|
|
*/
|
|
|
|
|
export interface UserTakeoverEvent {
|
|
|
|
|
type: 'user_takeover'
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Union type for all history events
|
|
|
|
|
*/
|
|
|
|
|
export type HistoryEvent = AgentStep | ObservationEvent | UserTakeoverEvent
|
|
|
|
|
|
|
|
|
|
/** @deprecated Use AgentStep instead */
|
|
|
|
|
export type AgentHistory = AgentStep
|
|
|
|
|
|
2025-09-29 16:33:15 +08:00
|
|
|
export interface ExecutionResult {
|
|
|
|
|
success: boolean
|
|
|
|
|
data: string
|
2026-01-15 19:18:25 +08:00
|
|
|
history: HistoryEvent[]
|
2025-09-29 16:33:15 +08:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export class PageAgent extends EventTarget {
|
|
|
|
|
config: PageAgentConfig
|
|
|
|
|
id = uid()
|
2025-10-16 16:49:13 +08:00
|
|
|
panel: Panel
|
2025-10-21 19:29:13 +08:00
|
|
|
tools: typeof tools
|
2025-09-29 16:33:15 +08:00
|
|
|
paused = false
|
|
|
|
|
disposed = false
|
|
|
|
|
task = ''
|
2025-10-21 21:31:35 +08:00
|
|
|
taskId = ''
|
2025-09-29 16:33:15 +08:00
|
|
|
|
|
|
|
|
#llm: LLM
|
|
|
|
|
#abortController = new AbortController()
|
2025-12-15 09:50:28 +00:00
|
|
|
#llmRetryListener: ((e: Event) => void) | null = null
|
|
|
|
|
#llmErrorListener: ((e: Event) => void) | null = null
|
2025-12-15 09:52:23 +00:00
|
|
|
#beforeUnloadListener: ((e: Event) => void) | null = null
|
2025-09-29 16:33:15 +08:00
|
|
|
|
2025-12-05 16:18:01 +08:00
|
|
|
/** PageController for DOM operations */
|
|
|
|
|
pageController: PageController
|
2025-09-29 16:33:15 +08:00
|
|
|
|
2026-01-15 20:28:39 +08:00
|
|
|
/** Runtime states for tracking across steps */
|
|
|
|
|
states = {
|
|
|
|
|
/** Accumulated wait time in seconds, used by wait tool */
|
|
|
|
|
totalWaitTime: 0,
|
|
|
|
|
/** Last known URL for detecting navigation */
|
|
|
|
|
lastURL: '',
|
|
|
|
|
}
|
2026-01-15 20:17:50 +08:00
|
|
|
|
2026-01-15 19:18:25 +08:00
|
|
|
/** History event stream */
|
|
|
|
|
history: HistoryEvent[] = []
|
2025-09-29 16:33:15 +08:00
|
|
|
|
2026-01-14 18:49:23 +08:00
|
|
|
constructor(config: PageAgentConfig) {
|
2025-09-29 16:33:15 +08:00
|
|
|
super()
|
|
|
|
|
|
|
|
|
|
this.config = config
|
2025-12-15 17:02:45 +08:00
|
|
|
this.#llm = new LLM(this.config)
|
|
|
|
|
this.panel = new Panel({
|
|
|
|
|
language: this.config.language,
|
|
|
|
|
onExecuteTask: (task) => this.execute(task),
|
|
|
|
|
onStop: () => this.dispose(),
|
|
|
|
|
onPauseToggle: () => {
|
|
|
|
|
this.paused = !this.paused
|
|
|
|
|
return this.paused
|
|
|
|
|
},
|
|
|
|
|
getPaused: () => this.paused,
|
|
|
|
|
})
|
2025-10-21 19:29:13 +08:00
|
|
|
this.tools = new Map(tools)
|
|
|
|
|
|
2026-01-13 17:33:38 +08:00
|
|
|
// Initialize PageController with config (mask enabled by default)
|
|
|
|
|
this.pageController = new PageController({
|
|
|
|
|
...this.config,
|
|
|
|
|
enableMask: this.config.enableMask ?? true,
|
|
|
|
|
})
|
2025-12-05 16:18:01 +08:00
|
|
|
|
2025-12-15 17:02:45 +08:00
|
|
|
// Listen to LLM events
|
2025-12-15 09:50:28 +00:00
|
|
|
this.#llmRetryListener = (e) => {
|
2025-12-15 17:02:45 +08:00
|
|
|
const { current, max } = (e as CustomEvent).detail
|
|
|
|
|
this.panel.update({ type: 'retry', current, max })
|
2025-12-15 09:50:28 +00:00
|
|
|
}
|
|
|
|
|
this.#llmErrorListener = (e) => {
|
2025-12-15 17:02:45 +08:00
|
|
|
const { error } = (e as CustomEvent).detail
|
|
|
|
|
this.panel.update({ type: 'error', message: `step failed: ${error.message}` })
|
2025-12-15 09:50:28 +00:00
|
|
|
}
|
|
|
|
|
this.#llm.addEventListener('retry', this.#llmRetryListener)
|
|
|
|
|
this.#llm.addEventListener('error', this.#llmErrorListener)
|
2025-12-15 17:02:45 +08:00
|
|
|
|
2025-10-21 19:29:13 +08:00
|
|
|
if (this.config.customTools) {
|
|
|
|
|
for (const [name, tool] of Object.entries(this.config.customTools)) {
|
|
|
|
|
if (tool === null) {
|
|
|
|
|
this.tools.delete(name)
|
|
|
|
|
continue
|
|
|
|
|
}
|
|
|
|
|
this.tools.set(name, tool)
|
|
|
|
|
}
|
|
|
|
|
}
|
2025-09-29 16:33:15 +08:00
|
|
|
|
2025-10-23 19:59:17 +08:00
|
|
|
if (!this.config.experimentalScriptExecutionTool) {
|
|
|
|
|
this.tools.delete('execute_javascript')
|
|
|
|
|
}
|
|
|
|
|
|
2025-12-15 09:52:23 +00:00
|
|
|
this.#beforeUnloadListener = (e) => {
|
2025-10-21 21:31:35 +08:00
|
|
|
if (!this.disposed) this.dispose('PAGE_UNLOADING')
|
2025-12-15 09:52:23 +00:00
|
|
|
}
|
|
|
|
|
window.addEventListener('beforeunload', this.#beforeUnloadListener)
|
2025-09-29 16:33:15 +08:00
|
|
|
}
|
|
|
|
|
|
2026-01-15 19:18:25 +08:00
|
|
|
/**
|
|
|
|
|
* Push a persistent observation to the history event stream.
|
|
|
|
|
* This will be visible in <agent_history> and remain in memory across steps.
|
|
|
|
|
*/
|
|
|
|
|
pushObservation(content: string): void {
|
|
|
|
|
this.history.push({ type: 'observation', content })
|
|
|
|
|
}
|
|
|
|
|
|
2025-09-29 16:33:15 +08:00
|
|
|
async execute(task: string): Promise<ExecutionResult> {
|
|
|
|
|
if (!task) throw new Error('Task is required')
|
|
|
|
|
this.task = task
|
2025-10-21 21:31:35 +08:00
|
|
|
this.taskId = uid()
|
2025-09-29 16:33:15 +08:00
|
|
|
|
2025-10-21 20:05:39 +08:00
|
|
|
const onBeforeStep = this.config.onBeforeStep || (() => void 0)
|
|
|
|
|
const onAfterStep = this.config.onAfterStep || (() => void 0)
|
|
|
|
|
const onBeforeTask = this.config.onBeforeTask || (() => void 0)
|
|
|
|
|
const onAfterTask = this.config.onAfterTask || (() => void 0)
|
|
|
|
|
|
2025-10-21 21:31:35 +08:00
|
|
|
await onBeforeTask.call(this)
|
2025-10-21 20:05:39 +08:00
|
|
|
|
2025-09-29 16:33:15 +08:00
|
|
|
// Show mask and panel
|
2026-01-13 17:33:38 +08:00
|
|
|
this.pageController.showMask()
|
2025-09-29 16:33:15 +08:00
|
|
|
|
2025-12-15 17:02:45 +08:00
|
|
|
this.panel.show()
|
|
|
|
|
this.panel.reset()
|
2025-09-29 16:33:15 +08:00
|
|
|
|
2025-12-15 17:02:45 +08:00
|
|
|
this.panel.update({ type: 'input', task: this.task })
|
2025-09-29 16:33:15 +08:00
|
|
|
|
|
|
|
|
if (this.#abortController) {
|
|
|
|
|
this.#abortController.abort()
|
|
|
|
|
this.#abortController = new AbortController()
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
this.history = []
|
|
|
|
|
|
2026-01-15 20:28:39 +08:00
|
|
|
// Reset states
|
|
|
|
|
this.states = {
|
|
|
|
|
totalWaitTime: 0,
|
|
|
|
|
lastURL: '',
|
|
|
|
|
}
|
|
|
|
|
|
2025-09-29 16:33:15 +08:00
|
|
|
try {
|
|
|
|
|
let step = 0
|
|
|
|
|
|
|
|
|
|
while (true) {
|
2026-01-15 20:28:39 +08:00
|
|
|
await this.#generateObservations(step)
|
|
|
|
|
|
2025-10-21 20:05:39 +08:00
|
|
|
await onBeforeStep.call(this, step)
|
2025-10-21 19:39:06 +08:00
|
|
|
|
2025-12-05 16:18:01 +08:00
|
|
|
console.group(`step: ${step}`)
|
2025-09-29 16:33:15 +08:00
|
|
|
|
|
|
|
|
// abort
|
|
|
|
|
if (this.#abortController.signal.aborted) throw new Error('AbortError')
|
|
|
|
|
// pause
|
|
|
|
|
await waitUntil(() => !this.paused)
|
|
|
|
|
|
|
|
|
|
// Update status to thinking
|
|
|
|
|
console.log(chalk.blue('Thinking...'))
|
2025-12-15 17:02:45 +08:00
|
|
|
this.panel.update({ type: 'thinking' })
|
2025-09-29 16:33:15 +08:00
|
|
|
|
|
|
|
|
const result = await this.#llm.invoke(
|
|
|
|
|
[
|
|
|
|
|
{
|
|
|
|
|
role: 'system',
|
|
|
|
|
content: this.#getSystemPrompt(),
|
|
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
role: 'user',
|
2025-12-05 16:18:01 +08:00
|
|
|
content: await this.#assembleUserPrompt(),
|
2025-09-29 16:33:15 +08:00
|
|
|
},
|
|
|
|
|
],
|
2025-10-17 18:43:41 +08:00
|
|
|
{ AgentOutput: this.#packMacroTool() },
|
2026-01-13 13:49:19 +08:00
|
|
|
this.#abortController.signal,
|
|
|
|
|
{
|
|
|
|
|
toolChoiceName: 'AgentOutput',
|
|
|
|
|
normalizeResponse,
|
|
|
|
|
}
|
2025-09-29 16:33:15 +08:00
|
|
|
)
|
|
|
|
|
|
2025-10-17 18:43:41 +08:00
|
|
|
const macroResult = result.toolResult as MacroToolResult
|
|
|
|
|
const input = macroResult.input
|
|
|
|
|
const output = macroResult.output
|
2025-09-29 16:33:15 +08:00
|
|
|
const brain = {
|
2025-10-17 18:43:41 +08:00
|
|
|
evaluation_previous_goal: input.evaluation_previous_goal || '',
|
|
|
|
|
memory: input.memory || '',
|
|
|
|
|
next_goal: input.next_goal || '',
|
2025-09-29 16:33:15 +08:00
|
|
|
}
|
|
|
|
|
const actionName = Object.keys(input.action)[0]
|
|
|
|
|
const action = {
|
|
|
|
|
name: actionName,
|
|
|
|
|
input: input.action[actionName],
|
|
|
|
|
output: output,
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
this.history.push({
|
2026-01-15 19:18:25 +08:00
|
|
|
type: 'step',
|
2025-09-29 16:33:15 +08:00
|
|
|
brain,
|
|
|
|
|
action,
|
|
|
|
|
usage: result.usage,
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
console.log(chalk.green('Step finished:'), actionName)
|
|
|
|
|
console.groupEnd()
|
|
|
|
|
|
2025-10-21 20:05:39 +08:00
|
|
|
await onAfterStep.call(this, step, this.history)
|
2025-10-21 19:39:06 +08:00
|
|
|
|
2025-09-29 16:33:15 +08:00
|
|
|
step++
|
|
|
|
|
if (step > MAX_STEPS) {
|
|
|
|
|
this.#onDone('Step count exceeded maximum limit', false)
|
2025-10-21 20:05:39 +08:00
|
|
|
const result: ExecutionResult = {
|
2025-09-29 16:33:15 +08:00
|
|
|
success: false,
|
|
|
|
|
data: 'Step count exceeded maximum limit',
|
|
|
|
|
history: this.history,
|
|
|
|
|
}
|
2025-10-21 21:31:35 +08:00
|
|
|
await onAfterTask.call(this, result)
|
2025-10-21 20:05:39 +08:00
|
|
|
return result
|
2025-09-29 16:33:15 +08:00
|
|
|
}
|
|
|
|
|
if (actionName === 'done') {
|
2025-10-17 18:43:41 +08:00
|
|
|
const success = action.input?.success ?? false
|
|
|
|
|
const text = action.input?.text || 'no text provided'
|
2025-09-29 16:33:15 +08:00
|
|
|
console.log(chalk.green.bold('Task completed'), success, text)
|
|
|
|
|
this.#onDone(text, success)
|
2025-10-21 20:05:39 +08:00
|
|
|
const result: ExecutionResult = {
|
2025-09-29 16:33:15 +08:00
|
|
|
success,
|
|
|
|
|
data: text,
|
|
|
|
|
history: this.history,
|
|
|
|
|
}
|
2025-10-21 21:31:35 +08:00
|
|
|
await onAfterTask.call(this, result)
|
2025-10-21 20:05:39 +08:00
|
|
|
return result
|
2025-09-29 16:33:15 +08:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
} catch (error: unknown) {
|
|
|
|
|
console.error('Task failed', error)
|
|
|
|
|
this.#onDone(String(error), false)
|
2025-10-21 20:05:39 +08:00
|
|
|
const result: ExecutionResult = {
|
2025-09-29 16:33:15 +08:00
|
|
|
success: false,
|
|
|
|
|
data: String(error),
|
|
|
|
|
history: this.history,
|
|
|
|
|
}
|
2025-10-21 21:31:35 +08:00
|
|
|
await onAfterTask.call(this, result)
|
2025-10-21 20:05:39 +08:00
|
|
|
return result
|
2025-09-29 16:33:15 +08:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Merge all tools into a single MacroTool with the following input:
|
|
|
|
|
* - thinking: string
|
|
|
|
|
* - evaluation_previous_goal: string
|
|
|
|
|
* - memory: string
|
|
|
|
|
* - next_goal: string
|
|
|
|
|
* - action: { toolName: toolInput }
|
|
|
|
|
* where action must be selected from tools defined in this.tools
|
|
|
|
|
*/
|
2025-10-17 18:43:41 +08:00
|
|
|
#packMacroTool(): Tool<MacroToolInput, MacroToolResult> {
|
2025-09-29 16:33:15 +08:00
|
|
|
const tools = this.tools
|
2025-10-21 21:31:35 +08:00
|
|
|
|
2025-09-29 16:33:15 +08:00
|
|
|
const actionSchemas = Array.from(tools.entries()).map(([toolName, tool]) => {
|
2025-12-24 19:28:59 +08:00
|
|
|
return zod.object({ [toolName]: tool.inputSchema }).describe(tool.description)
|
2025-09-29 16:33:15 +08:00
|
|
|
})
|
|
|
|
|
|
2025-10-17 18:43:41 +08:00
|
|
|
const actionSchema = zod.union(
|
|
|
|
|
actionSchemas as unknown as [zod.ZodType, zod.ZodType, ...zod.ZodType[]]
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
const macroToolSchema = zod.object({
|
|
|
|
|
// thinking: zod.string().optional(),
|
|
|
|
|
evaluation_previous_goal: zod.string().optional(),
|
|
|
|
|
memory: zod.string().optional(),
|
|
|
|
|
next_goal: zod.string().optional(),
|
|
|
|
|
action: actionSchema,
|
|
|
|
|
})
|
2025-09-29 16:33:15 +08:00
|
|
|
|
|
|
|
|
return {
|
2026-01-13 15:04:03 +08:00
|
|
|
description: 'You MUST call this tool every step. Outputs your reflections and next action.',
|
2025-10-17 18:43:41 +08:00
|
|
|
inputSchema: macroToolSchema as zod.ZodType<MacroToolInput>,
|
|
|
|
|
execute: async (input: MacroToolInput): Promise<MacroToolResult> => {
|
|
|
|
|
// abort
|
|
|
|
|
if (this.#abortController.signal.aborted) throw new Error('AbortError')
|
|
|
|
|
// pause
|
|
|
|
|
await waitUntil(() => !this.paused)
|
|
|
|
|
|
|
|
|
|
console.log(chalk.blue.bold('MacroTool execute'), input)
|
|
|
|
|
const action = input.action
|
|
|
|
|
|
|
|
|
|
const toolName = Object.keys(action)[0]
|
|
|
|
|
const toolInput = action[toolName]
|
|
|
|
|
const brain = trimLines(`✅: ${input.evaluation_previous_goal}
|
2025-09-29 16:33:15 +08:00
|
|
|
💾: ${input.memory}
|
|
|
|
|
🎯: ${input.next_goal}
|
|
|
|
|
`)
|
|
|
|
|
|
2025-10-17 18:43:41 +08:00
|
|
|
console.log(brain)
|
2025-12-15 17:02:45 +08:00
|
|
|
this.panel.update({ type: 'thinking', text: brain })
|
2025-10-17 18:43:41 +08:00
|
|
|
|
|
|
|
|
// Find the corresponding tool
|
|
|
|
|
const tool = tools.get(toolName)
|
|
|
|
|
assert(tool, `Tool ${toolName} not found. (@note should have been caught before this!!!)`)
|
|
|
|
|
|
|
|
|
|
console.log(chalk.blue.bold(`Executing tool: ${toolName}`), toolInput)
|
2025-12-15 17:02:45 +08:00
|
|
|
this.panel.update({ type: 'toolExecuting', toolName, args: toolInput })
|
2025-09-29 16:33:15 +08:00
|
|
|
|
2025-10-17 18:43:41 +08:00
|
|
|
const startTime = Date.now()
|
2025-09-29 16:33:15 +08:00
|
|
|
|
2025-10-17 18:43:41 +08:00
|
|
|
// Execute tool, bind `this` to PageAgent
|
2026-01-15 20:17:50 +08:00
|
|
|
const result = await tool.execute.bind(this)(toolInput)
|
2025-10-17 18:43:41 +08:00
|
|
|
|
|
|
|
|
const duration = Date.now() - startTime
|
|
|
|
|
console.log(chalk.green.bold(`Tool (${toolName}) executed for ${duration}ms`), result)
|
|
|
|
|
|
2026-01-15 20:17:50 +08:00
|
|
|
// Reset wait time for non-wait tools
|
|
|
|
|
if (toolName !== 'wait') {
|
2026-01-15 20:28:39 +08:00
|
|
|
this.states.totalWaitTime = 0
|
2025-10-17 18:43:41 +08:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Briefly display execution result
|
2025-12-15 17:02:45 +08:00
|
|
|
this.panel.update({
|
|
|
|
|
type: 'toolCompleted',
|
|
|
|
|
toolName,
|
|
|
|
|
args: toolInput,
|
|
|
|
|
result,
|
|
|
|
|
duration,
|
|
|
|
|
})
|
2025-09-29 16:33:15 +08:00
|
|
|
|
2025-10-17 18:43:41 +08:00
|
|
|
// Wait a moment to let user see the result
|
|
|
|
|
await new Promise((resolve) => setTimeout(resolve, 100))
|
2025-09-29 16:33:15 +08:00
|
|
|
|
2025-10-17 18:43:41 +08:00
|
|
|
// Return structured result
|
|
|
|
|
return {
|
|
|
|
|
input,
|
|
|
|
|
output: result,
|
|
|
|
|
}
|
|
|
|
|
},
|
2025-09-29 16:33:15 +08:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Get system prompt, dynamically replace language settings based on configured language
|
|
|
|
|
*/
|
|
|
|
|
#getSystemPrompt(): string {
|
|
|
|
|
let systemPrompt = SYSTEM_PROMPT
|
|
|
|
|
|
|
|
|
|
const targetLanguage = this.config.language === 'zh-CN' ? '中文' : 'English'
|
|
|
|
|
systemPrompt = systemPrompt.replace(
|
|
|
|
|
/Default working language: \*\*.*?\*\*/,
|
|
|
|
|
`Default working language: **${targetLanguage}**`
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
return systemPrompt
|
|
|
|
|
}
|
|
|
|
|
|
2026-01-10 18:44:09 +08:00
|
|
|
/**
|
|
|
|
|
* Get instructions from config and format as XML block
|
|
|
|
|
*/
|
|
|
|
|
async #getInstructions(): Promise<string> {
|
|
|
|
|
const { instructions } = this.config
|
|
|
|
|
if (!instructions) return ''
|
|
|
|
|
|
|
|
|
|
const systemInstructions = instructions.system?.trim()
|
|
|
|
|
const url = await this.pageController.getCurrentUrl()
|
2026-01-10 19:19:20 +08:00
|
|
|
let pageInstructions: string | undefined
|
|
|
|
|
|
|
|
|
|
if (instructions.getPageInstructions) {
|
|
|
|
|
try {
|
|
|
|
|
pageInstructions = instructions.getPageInstructions(url)?.trim()
|
|
|
|
|
} catch (error) {
|
|
|
|
|
console.error(
|
|
|
|
|
chalk.red('[PageAgent] Failed to execute getPageInstructions callback:'),
|
|
|
|
|
error
|
|
|
|
|
)
|
|
|
|
|
}
|
|
|
|
|
}
|
2026-01-10 18:44:09 +08:00
|
|
|
if (!systemInstructions && !pageInstructions) return ''
|
|
|
|
|
|
|
|
|
|
let result = '<instructions>\n'
|
|
|
|
|
|
|
|
|
|
if (systemInstructions) {
|
|
|
|
|
result += `<system_instructions>\n${systemInstructions}\n</system_instructions>\n`
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (pageInstructions) {
|
|
|
|
|
result += `<page_instructions>\n${pageInstructions}\n</page_instructions>\n`
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
result += '</instructions>\n\n'
|
|
|
|
|
|
|
|
|
|
return result
|
|
|
|
|
}
|
|
|
|
|
|
2026-01-15 20:28:39 +08:00
|
|
|
/**
|
|
|
|
|
* Generate observations before each step
|
|
|
|
|
* - URL change detection
|
|
|
|
|
* - Too many steps warning
|
|
|
|
|
* @todo loop detection
|
|
|
|
|
* @todo console error
|
|
|
|
|
*/
|
|
|
|
|
async #generateObservations(stepCount: number): Promise<void> {
|
|
|
|
|
// Detect URL change
|
|
|
|
|
const currentURL = await this.pageController.getCurrentUrl()
|
|
|
|
|
if (this.states.lastURL && currentURL !== this.states.lastURL) {
|
|
|
|
|
this.pushObservation(`Page navigated to → ${currentURL}`)
|
|
|
|
|
}
|
|
|
|
|
this.states.lastURL = currentURL
|
|
|
|
|
|
|
|
|
|
// Warn about remaining steps
|
|
|
|
|
const remaining = MAX_STEPS - stepCount
|
|
|
|
|
if (remaining === 5) {
|
|
|
|
|
this.pushObservation(
|
|
|
|
|
`⚠️ 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.`
|
|
|
|
|
)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2025-12-05 16:18:01 +08:00
|
|
|
async #assembleUserPrompt(): Promise<string> {
|
2025-09-29 16:33:15 +08:00
|
|
|
let prompt = ''
|
|
|
|
|
|
2026-01-10 18:44:09 +08:00
|
|
|
// <instructions> (optional)
|
|
|
|
|
prompt += await this.#getInstructions()
|
|
|
|
|
|
2025-09-29 16:33:15 +08:00
|
|
|
// <agent_state>
|
|
|
|
|
// - <user_request>
|
|
|
|
|
// - <step_info>
|
|
|
|
|
// <agent_state>
|
|
|
|
|
|
2026-01-15 19:18:25 +08:00
|
|
|
const stepCount = this.history.filter((e) => e.type === 'step').length
|
|
|
|
|
|
2025-09-29 16:33:15 +08:00
|
|
|
prompt += `<agent_state>
|
|
|
|
|
<user_request>
|
|
|
|
|
${this.task}
|
|
|
|
|
</user_request>
|
|
|
|
|
<step_info>
|
2026-01-15 19:18:25 +08:00
|
|
|
Step ${stepCount + 1} of ${MAX_STEPS} max possible steps
|
2025-09-29 16:33:15 +08:00
|
|
|
Current date and time: ${new Date().toISOString()}
|
|
|
|
|
</step_info>
|
|
|
|
|
</agent_state>
|
|
|
|
|
`
|
|
|
|
|
|
2026-01-10 17:44:08 +08:00
|
|
|
// <agent_history>
|
2026-01-15 19:18:25 +08:00
|
|
|
// - <step_N> for steps
|
|
|
|
|
// - <sys> for observations and system messages
|
2026-01-10 17:44:08 +08:00
|
|
|
|
|
|
|
|
prompt += '\n<agent_history>\n'
|
|
|
|
|
|
2026-01-15 19:18:25 +08:00
|
|
|
let stepIndex = 0
|
|
|
|
|
for (const event of this.history) {
|
|
|
|
|
if (event.type === 'step') {
|
|
|
|
|
stepIndex++
|
|
|
|
|
prompt += `<step_${stepIndex}>
|
|
|
|
|
Evaluation of Previous Step: ${event.brain.evaluation_previous_goal}
|
|
|
|
|
Memory: ${event.brain.memory}
|
|
|
|
|
Next Goal: ${event.brain.next_goal}
|
|
|
|
|
Action Results: ${event.action.output}
|
|
|
|
|
</step_${stepIndex}>
|
2026-01-10 17:44:08 +08:00
|
|
|
`
|
2026-01-15 19:18:25 +08:00
|
|
|
} else if (event.type === 'observation') {
|
|
|
|
|
prompt += `<sys>${event.content}</sys>\n`
|
|
|
|
|
} else if (event.type === 'user_takeover') {
|
|
|
|
|
prompt += `<sys>User took over control and made changes to the page.</sys>\n`
|
|
|
|
|
}
|
|
|
|
|
}
|
2026-01-10 17:44:08 +08:00
|
|
|
|
|
|
|
|
prompt += '</agent_history>\n\n'
|
|
|
|
|
|
2025-09-29 16:33:15 +08:00
|
|
|
// <browser_state>
|
|
|
|
|
|
2025-12-05 16:18:01 +08:00
|
|
|
prompt += await this.#getBrowserState()
|
2025-09-29 16:33:15 +08:00
|
|
|
|
|
|
|
|
return trimLines(prompt)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#onDone(text: string, success = true) {
|
2025-12-05 16:18:01 +08:00
|
|
|
this.pageController.cleanUpHighlights()
|
2025-09-29 16:33:15 +08:00
|
|
|
|
|
|
|
|
// Update panel status
|
2025-12-15 17:02:45 +08:00
|
|
|
if (success) {
|
|
|
|
|
this.panel.update({ type: 'output', text })
|
|
|
|
|
} else {
|
|
|
|
|
this.panel.update({ type: 'error', message: text })
|
|
|
|
|
}
|
2025-09-29 16:33:15 +08:00
|
|
|
|
|
|
|
|
// Task completed
|
2025-12-15 17:02:45 +08:00
|
|
|
this.panel.update({ type: 'completed' })
|
2025-09-29 16:33:15 +08:00
|
|
|
|
2026-01-13 17:33:38 +08:00
|
|
|
this.pageController.hideMask()
|
2025-09-29 16:33:15 +08:00
|
|
|
|
|
|
|
|
this.#abortController.abort()
|
|
|
|
|
}
|
|
|
|
|
|
2025-12-05 16:18:01 +08:00
|
|
|
async #getBrowserState(): Promise<string> {
|
2026-01-15 20:17:50 +08:00
|
|
|
const state = await this.pageController.getBrowserState()
|
2026-01-11 00:54:59 +08:00
|
|
|
|
2026-01-15 20:17:50 +08:00
|
|
|
let content = state.content
|
2026-01-11 00:54:59 +08:00
|
|
|
if (this.config.transformPageContent) {
|
2026-01-15 20:17:50 +08:00
|
|
|
content = await this.config.transformPageContent(content)
|
2026-01-11 00:54:59 +08:00
|
|
|
}
|
2025-09-29 16:33:15 +08:00
|
|
|
|
2026-01-15 20:17:50 +08:00
|
|
|
return trimLines(`<browser_state>
|
|
|
|
|
Current Page: [${state.title}](${state.url})
|
2026-01-15 20:28:39 +08:00
|
|
|
|
2026-01-15 20:17:50 +08:00
|
|
|
${state.header}
|
|
|
|
|
${content}
|
|
|
|
|
${state.footer}
|
2025-09-29 16:33:15 +08:00
|
|
|
|
2026-01-15 20:17:50 +08:00
|
|
|
</browser_state>
|
2025-09-29 16:33:15 +08:00
|
|
|
`)
|
|
|
|
|
}
|
|
|
|
|
|
2025-10-21 21:31:35 +08:00
|
|
|
dispose(reason?: string) {
|
2025-09-29 16:33:15 +08:00
|
|
|
console.log('Disposing PageAgent...')
|
|
|
|
|
this.disposed = true
|
2025-12-05 16:18:01 +08:00
|
|
|
this.pageController.dispose()
|
2025-09-29 16:33:15 +08:00
|
|
|
this.panel.dispose()
|
|
|
|
|
this.history = []
|
2025-10-21 21:31:35 +08:00
|
|
|
this.#abortController.abort(reason ?? 'PageAgent disposed')
|
|
|
|
|
|
2025-12-15 09:50:28 +00:00
|
|
|
// Clean up LLM event listeners
|
|
|
|
|
if (this.#llmRetryListener) {
|
|
|
|
|
this.#llm.removeEventListener('retry', this.#llmRetryListener)
|
|
|
|
|
this.#llmRetryListener = null
|
|
|
|
|
}
|
|
|
|
|
if (this.#llmErrorListener) {
|
|
|
|
|
this.#llm.removeEventListener('error', this.#llmErrorListener)
|
|
|
|
|
this.#llmErrorListener = null
|
|
|
|
|
}
|
|
|
|
|
|
2025-12-15 09:52:23 +00:00
|
|
|
// Clean up window event listeners
|
|
|
|
|
if (this.#beforeUnloadListener) {
|
|
|
|
|
window.removeEventListener('beforeunload', this.#beforeUnloadListener)
|
|
|
|
|
this.#beforeUnloadListener = null
|
|
|
|
|
}
|
|
|
|
|
|
2025-10-21 21:31:35 +08:00
|
|
|
this.config.onDispose?.call(this, reason)
|
2025-09-29 16:33:15 +08:00
|
|
|
}
|
|
|
|
|
}
|