feat: add lifecycle hooks

This commit is contained in:
Simon
2025-10-21 21:31:35 +08:00
parent 71d1cd4df3
commit 116d76b230
3 changed files with 41 additions and 15 deletions

View File

@@ -17,9 +17,10 @@ The development progress and future plans for PageAgent.
- [x] **Free evaluation plan?**
- [x] **Custom actions and HITL**
- [ ] **Hooks and Events**
- [x] **lifecycle**
- [ ] **Pause and intervene**
- [ ] **Hijacking `page_open/page_change/page_unload` event**
- [x] **lifecycle hooks**
- [ ] **lifecycle events**
- [ ] **❗Pause and intervene**
- [ ] **❗Hijack `page_open/page_change/page_unload` behavior**
- [ ] **Custom knowledge base and instructions**
- [ ] **Black/white-list safeguard**
- [ ] **Data-masking**

View File

@@ -81,6 +81,7 @@ export class PageAgent extends EventTarget {
paused = false
disposed = false
task = ''
taskId = ''
#llm: LLM
#totalWaitTime = 0
@@ -125,6 +126,10 @@ export class PageAgent extends EventTarget {
}
patchReact(this)
window.addEventListener('beforeunload', (e) => {
if (!this.disposed) this.dispose('PAGE_UNLOADING')
})
}
/**
@@ -133,13 +138,14 @@ export class PageAgent extends EventTarget {
async execute(task: string): Promise<ExecutionResult> {
if (!task) throw new Error('Task is required')
this.task = task
this.taskId = uid()
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)
await onBeforeTask.call(this, task)
await onBeforeTask.call(this)
// Show mask and panel
this.mask.show()
@@ -228,7 +234,7 @@ export class PageAgent extends EventTarget {
data: 'Step count exceeded maximum limit',
history: this.history,
}
await onAfterTask.call(this, task, result)
await onAfterTask.call(this, result)
return result
}
if (actionName === 'done') {
@@ -241,7 +247,7 @@ export class PageAgent extends EventTarget {
data: text,
history: this.history,
}
await onAfterTask.call(this, task, result)
await onAfterTask.call(this, result)
return result
}
}
@@ -253,7 +259,7 @@ export class PageAgent extends EventTarget {
data: String(error),
history: this.history,
}
await onAfterTask.call(this, task, result)
await onAfterTask.call(this, result)
return result
}
}
@@ -269,7 +275,7 @@ export class PageAgent extends EventTarget {
*/
#packMacroTool(): Tool<MacroToolInput, MacroToolResult> {
const tools = this.tools
// union version
const actionSchemas = Array.from(tools.entries()).map(([toolName, tool]) => {
return zod.object({
[toolName]: tool.inputSchema,
@@ -289,8 +295,6 @@ export class PageAgent extends EventTarget {
})
return {
// name: MACRO_TOOL_NAME,
// description: 'Execute agent action', // @todo remote
inputSchema: macroToolSchema as zod.ZodType<MacroToolInput>,
execute: async (input: MacroToolInput): Promise<MacroToolResult> => {
// abort
@@ -512,7 +516,7 @@ export class PageAgent extends EventTarget {
this.dispatchEvent(new Event('afterUpdate'))
}
dispose() {
dispose(reason?: string) {
console.log('Disposing PageAgent...')
this.disposed = true
dom.cleanUpHighlights()
@@ -522,6 +526,8 @@ export class PageAgent extends EventTarget {
this.panel.dispose()
this.mask.dispose()
this.history = []
this.#abortController.abort('PageAgent disposed')
this.#abortController.abort(reason ?? 'PageAgent disposed')
this.config.onDispose?.call(this, reason)
}
}

View File

@@ -57,15 +57,34 @@ export interface UIConfig {
customTools?: Record<string, PageAgentTool | null>
// lifecycle hooks
// @todo: use event instead of hooks
onBeforeStep?: (this: PageAgent, stepCnt: number) => Promise<void> | void
onAfterStep?: (this: PageAgent, stepCnt: number, history: AgentHistory[]) => Promise<void> | void
onBeforeTask?: (this: PageAgent, task: string) => Promise<void> | void
onAfterTask?: (this: PageAgent, task: string, result: ExecutionResult) => Promise<void> | void
onBeforeTask?: (this: PageAgent) => Promise<void> | void
onAfterTask?: (this: PageAgent, result: ExecutionResult) => Promise<void> | void
/**
* @note this hook can block the disposal process
* @note when dispose caused by page unload, `reason` will be 'PAGE_UNLOADING'. this method CANNOT block the unload process. async operations may be cut.
*/
onDispose?: (this: PageAgent, reason?: string) => void
// page behavior hooks
onPageUnload?: (this: PageAgent) => Promise<void> | void
/**
* TODO: @unimplemented
* hook when action causes a new page to be opened
* @note PageAgent will try to detect new pages and decide if it's caused by an action. But not very reliable.
*/
onNewPageOpen?: (this: PageAgent, url: string) => Promise<void> | void
/**
* TODO: @unimplemented
* try to navigate to a new page instead of opening a new tab/window.
* @note will unload the current page when a action tries to open a new page. so that things keep in the same tab/window.
*/
experimentalPreventNewPage?: boolean
}
export type PageAgentConfig = LLMConfig & DomConfig & UIConfig