feat(core): add a observe phase in a step; improve abortSignal
This commit is contained in:
@@ -3,7 +3,7 @@
|
|||||||
* All rights reserved.
|
* All rights reserved.
|
||||||
*/
|
*/
|
||||||
import { InvokeError, LLM, type Tool } from '@page-agent/llms'
|
import { InvokeError, LLM, type Tool } from '@page-agent/llms'
|
||||||
import type { PageController } from '@page-agent/page-controller'
|
import type { BrowserState, PageController } from '@page-agent/page-controller'
|
||||||
import chalk from 'chalk'
|
import chalk from 'chalk'
|
||||||
import * as zod from 'zod'
|
import * as zod from 'zod'
|
||||||
|
|
||||||
@@ -31,6 +31,15 @@ export type * from './types'
|
|||||||
* AI agent for browser automation.
|
* AI agent for browser automation.
|
||||||
*
|
*
|
||||||
* @remarks
|
* @remarks
|
||||||
|
* ## Re-act Agent Loop
|
||||||
|
* - step
|
||||||
|
* - observe (gather information about current environment and context)
|
||||||
|
* - think (LLM calling)
|
||||||
|
* - reflection (evaluate history, generate memory, short-term planning)
|
||||||
|
* - action (give the action to approach the next goal)
|
||||||
|
* - act (execute the action)
|
||||||
|
* - loop
|
||||||
|
*
|
||||||
* ## Event System
|
* ## Event System
|
||||||
* - `statuschange` - Agent status transitions (idle → running → completed/error)
|
* - `statuschange` - Agent status transitions (idle → running → completed/error)
|
||||||
* - `historychange` - History events updated (persistent, part of agent memory)
|
* - `historychange` - History events updated (persistent, part of agent memory)
|
||||||
@@ -72,12 +81,14 @@ export class PageAgentCore extends EventTarget {
|
|||||||
#abortController = new AbortController()
|
#abortController = new AbortController()
|
||||||
#observations: string[] = []
|
#observations: string[] = []
|
||||||
|
|
||||||
/** internal states of a single task execution */
|
/** internal states during a single task execution */
|
||||||
#states = {
|
#states = {
|
||||||
/** Accumulated wait time in seconds */
|
/** Accumulated wait time in seconds */
|
||||||
totalWaitTime: 0,
|
totalWaitTime: 0,
|
||||||
/** Last known URL for detecting navigation */
|
/** For detecting navigation */
|
||||||
lastURL: '',
|
lastURL: '',
|
||||||
|
/** Browser state */
|
||||||
|
browserState: null as BrowserState | null,
|
||||||
}
|
}
|
||||||
|
|
||||||
constructor(config: PageAgentConfig & { pageController: PageController }) {
|
constructor(config: PageAgentConfig & { pageController: PageController }) {
|
||||||
@@ -202,7 +213,7 @@ export class PageAgentCore extends EventTarget {
|
|||||||
this.#emitHistoryChange()
|
this.#emitHistoryChange()
|
||||||
|
|
||||||
// Reset internal states
|
// Reset internal states
|
||||||
this.#states = { totalWaitTime: 0, lastURL: '' }
|
this.#states = { totalWaitTime: 0, lastURL: '', browserState: null }
|
||||||
|
|
||||||
let step = 0
|
let step = 0
|
||||||
|
|
||||||
@@ -212,16 +223,14 @@ export class PageAgentCore extends EventTarget {
|
|||||||
|
|
||||||
await onBeforeStep?.(this, step)
|
await onBeforeStep?.(this, step)
|
||||||
|
|
||||||
|
// observe (update browser state and other observations)
|
||||||
|
|
||||||
|
console.log(chalk.blue.bold('👀 Observing...'))
|
||||||
|
|
||||||
|
this.#states.browserState = await this.pageController.getBrowserState()
|
||||||
await this.#handleObservations(step)
|
await this.#handleObservations(step)
|
||||||
|
|
||||||
// abort
|
// assemble prompts
|
||||||
if (this.#abortController.signal.aborted) throw new Error('AbortError')
|
|
||||||
|
|
||||||
// status
|
|
||||||
console.log(chalk.blue('Thinking...'))
|
|
||||||
this.#emitActivity({ type: 'thinking' })
|
|
||||||
|
|
||||||
// invoke LLM
|
|
||||||
|
|
||||||
const messages = [
|
const messages = [
|
||||||
{ role: 'system' as const, content: this.#getSystemPrompt() },
|
{ role: 'system' as const, content: this.#getSystemPrompt() },
|
||||||
@@ -230,6 +239,11 @@ export class PageAgentCore extends EventTarget {
|
|||||||
|
|
||||||
const tools = { AgentOutput: this.#packMacroTool() }
|
const tools = { AgentOutput: this.#packMacroTool() }
|
||||||
|
|
||||||
|
// invoke LLM
|
||||||
|
|
||||||
|
console.log(chalk.blue.bold('🧠 Thinking...'))
|
||||||
|
this.#emitActivity({ type: 'thinking' })
|
||||||
|
|
||||||
const result = await this.#llm.invoke(messages, tools, this.#abortController.signal, {
|
const result = await this.#llm.invoke(messages, tools, this.#abortController.signal, {
|
||||||
toolChoiceName: 'AgentOutput',
|
toolChoiceName: 'AgentOutput',
|
||||||
normalizeResponse,
|
normalizeResponse,
|
||||||
@@ -267,7 +281,6 @@ export class PageAgentCore extends EventTarget {
|
|||||||
|
|
||||||
await onAfterStep?.(this, this.history)
|
await onAfterStep?.(this, this.history)
|
||||||
|
|
||||||
console.log(chalk.green('Step finished:'), actionName)
|
|
||||||
console.groupEnd()
|
console.groupEnd()
|
||||||
|
|
||||||
// finish task if done
|
// finish task if done
|
||||||
@@ -435,10 +448,10 @@ export class PageAgentCore extends EventTarget {
|
|||||||
if (!instructions) return ''
|
if (!instructions) return ''
|
||||||
|
|
||||||
const systemInstructions = instructions.system?.trim()
|
const systemInstructions = instructions.system?.trim()
|
||||||
const url = await this.pageController.getCurrentUrl()
|
|
||||||
let pageInstructions: string | undefined
|
let pageInstructions: string | undefined
|
||||||
|
|
||||||
if (instructions.getPageInstructions) {
|
const url = this.#states.browserState?.url || ''
|
||||||
|
if (instructions.getPageInstructions && url) {
|
||||||
try {
|
try {
|
||||||
pageInstructions = instructions.getPageInstructions(url)?.trim()
|
pageInstructions = instructions.getPageInstructions(url)?.trim()
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
@@ -479,7 +492,7 @@ export class PageAgentCore extends EventTarget {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Detect URL change
|
// Detect URL change
|
||||||
const currentURL = await this.pageController.getCurrentUrl()
|
const currentURL = this.#states.browserState?.url || ''
|
||||||
if (currentURL !== this.#states.lastURL) {
|
if (currentURL !== this.#states.lastURL) {
|
||||||
this.pushObservation(`Page navigated to → ${currentURL}`)
|
this.pushObservation(`Page navigated to → ${currentURL}`)
|
||||||
this.#states.lastURL = currentURL
|
this.#states.lastURL = currentURL
|
||||||
@@ -502,6 +515,7 @@ export class PageAgentCore extends EventTarget {
|
|||||||
if (this.#observations.length > 0) {
|
if (this.#observations.length > 0) {
|
||||||
for (const content of this.#observations) {
|
for (const content of this.#observations) {
|
||||||
this.history.push({ type: 'observation', content })
|
this.history.push({ type: 'observation', content })
|
||||||
|
console.log(chalk.cyan('Observation:'), content)
|
||||||
}
|
}
|
||||||
this.#observations = []
|
this.#observations = []
|
||||||
this.#emitHistoryChange()
|
this.#emitHistoryChange()
|
||||||
@@ -509,6 +523,8 @@ export class PageAgentCore extends EventTarget {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async #assembleUserPrompt(): Promise<string> {
|
async #assembleUserPrompt(): Promise<string> {
|
||||||
|
const browserState = this.#states.browserState!
|
||||||
|
|
||||||
let prompt = ''
|
let prompt = ''
|
||||||
|
|
||||||
// <instructions> (optional)
|
// <instructions> (optional)
|
||||||
@@ -562,8 +578,6 @@ export class PageAgentCore extends EventTarget {
|
|||||||
|
|
||||||
// <browser_state>
|
// <browser_state>
|
||||||
|
|
||||||
const browserState = await this.pageController.getBrowserState()
|
|
||||||
|
|
||||||
let pageContent = browserState.content
|
let pageContent = browserState.content
|
||||||
if (this.config.transformPageContent) {
|
if (this.config.transformPageContent) {
|
||||||
pageContent = await this.config.transformPageContent(pageContent)
|
pageContent = await this.config.transformPageContent(pageContent)
|
||||||
|
|||||||
@@ -49,6 +49,9 @@ export class LLM extends EventTarget {
|
|||||||
): Promise<InvokeResult> {
|
): Promise<InvokeResult> {
|
||||||
return await withRetry(
|
return await withRetry(
|
||||||
async () => {
|
async () => {
|
||||||
|
// in case user aborted before invoking
|
||||||
|
if (abortSignal.aborted) throw new Error('AbortError')
|
||||||
|
|
||||||
const result = await this.client.invoke(messages, tools, abortSignal, options)
|
const result = await this.client.invoke(messages, tools, abortSignal, options)
|
||||||
|
|
||||||
return result
|
return result
|
||||||
|
|||||||
Reference in New Issue
Block a user