diff --git a/ROADMAP.md b/ROADMAP.md index 18e75e8..cafb785 100644 --- a/ROADMAP.md +++ b/ROADMAP.md @@ -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** diff --git a/src/PageAgent.ts b/src/PageAgent.ts index 20c412d..fe5457a 100644 --- a/src/PageAgent.ts +++ b/src/PageAgent.ts @@ -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 { 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 { 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, execute: async (input: MacroToolInput): Promise => { // 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) } } diff --git a/src/config/index.ts b/src/config/index.ts index 5d806ae..04d54a4 100644 --- a/src/config/index.ts +++ b/src/config/index.ts @@ -57,15 +57,34 @@ export interface UIConfig { customTools?: Record // lifecycle hooks + // @todo: use event instead of hooks onBeforeStep?: (this: PageAgent, stepCnt: number) => Promise | void onAfterStep?: (this: PageAgent, stepCnt: number, history: AgentHistory[]) => Promise | void - onBeforeTask?: (this: PageAgent, task: string) => Promise | void - onAfterTask?: (this: PageAgent, task: string, result: ExecutionResult) => Promise | void + onBeforeTask?: (this: PageAgent) => Promise | void + onAfterTask?: (this: PageAgent, result: ExecutionResult) => Promise | 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 + /** + * 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 + + /** + * 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