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
34 changes: 34 additions & 0 deletions packages/core/src/PageAgentCore.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -363,3 +363,37 @@ describe.concurrent('PageAgentCore lifecycle', () => {
})
})
})

describe.concurrent('experimental tool gating', () => {
it('omits hover_element_by_index by default', () => {
const agent = createAgent(createFetchMock())
expect(agent.tools.has('hover_element_by_index')).toBe(false)
})

it('keeps hover_element_by_index available when experimentalPointerActions is true', () => {
const agent = createAgent(createFetchMock(), { experimentalPointerActions: true })
expect(agent.tools.has('hover_element_by_index')).toBe(true)
})

it('preserves a caller-provided hover tool when the built-in gate is disabled', () => {
const customHover = tool({
description: 'caller-provided hover tool',
inputSchema: z.object({}),
execute: async () => 'custom hover',
})
const agent = createAgent(createFetchMock(), {
customTools: { hover_element_by_index: customHover },
})
expect(agent.tools.get('hover_element_by_index')).toBe(customHover)
})

it('removes execute_javascript when experimentalScriptExecutionTool is false', () => {
const agent = createAgent(createFetchMock())
expect(agent.tools.has('execute_javascript')).toBe(false)
})

it('keeps execute_javascript when experimentalScriptExecutionTool is true', () => {
const agent = createAgent(createFetchMock(), { experimentalScriptExecutionTool: true })
expect(agent.tools.has('execute_javascript')).toBe(true)
})
})
11 changes: 8 additions & 3 deletions packages/core/src/PageAgentCore.ts
Original file line number Diff line number Diff line change
Expand Up @@ -114,6 +114,14 @@ export class PageAgentCore extends EventTarget {
this.tools = new Map(tools)
this.pageController = config.pageController

if (!this.config.experimentalScriptExecutionTool) {
this.tools.delete('execute_javascript')
}

if (!this.config.experimentalPointerActions) {
this.tools.delete('hover_element_by_index')
}

this.#llm.addEventListener('retry', (e) => {
const { attempt, maxAttempts, lastError } = (e as CustomEvent).detail
this.#emitActivity({ type: 'retrying', attempt, maxAttempts })
Expand Down Expand Up @@ -141,9 +149,6 @@ export class PageAgentCore extends EventTarget {
}
}

if (!this.config.experimentalScriptExecutionTool) {
this.tools.delete('execute_javascript')
}
}

/** Get current agent status */
Expand Down
16 changes: 16 additions & 0 deletions packages/core/src/tools/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -135,6 +135,22 @@ tools.set(
})
)

tools.set(
'hover_element_by_index',
tool({
// @experimental Tool surface gated by `experimentalPointerActions` in PageAgentCore.
description:
'Dispatch synthetic pointer/mouse hover events to an element by index for JavaScript hover handlers. This does not activate CSS :hover. Requires the `experimentalPointerActions` flag to be enabled.',
inputSchema: z.object({
index: z.int().min(0),
}),
execute: async function (this: PageAgentCore, input) {
const result = await this.pageController.hoverElement(input.index)
Comment thread
huyua9 marked this conversation as resolved.
return result.message
},
})
)

/**
* @note Reference from browser-use
*/
Expand Down
12 changes: 12 additions & 0 deletions packages/core/src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -131,6 +131,18 @@ export interface AgentConfig extends LLMConfig {
*/
experimentalLlmsTxt?: boolean

/**
* @experimental
* Enable experimental pointer-based tools (currently only `hover_element_by_index`).
* Disabled by default — keep tool surface minimal until real usage shows the value.
* Also pass `experimentalPointerActions: true` to the underlying `PageControllerConfig`
* to actually allow the action to run; mismatched flags will cause the tool to
* fail with an explanatory message.
* @see https://github.com/alibaba/page-agent/issues/222
* @default false
*/
experimentalPointerActions?: boolean

/**
* Transform page content before sending to LLM.
* Called after DOM extraction and simplification, before LLM invocation.
Expand Down
4 changes: 3 additions & 1 deletion packages/extension/src/agent/MultiPageAgent.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,9 @@ export class MultiPageAgent extends PageAgentCore {
constructor(config: MultiPageAgentConfig) {
// multi page controller
const tabsController = new TabsController()
const pageController = new RemotePageController(tabsController)
const pageController = new RemotePageController(tabsController, {
experimentalPointerActions: config.experimentalPointerActions,
})
const customTools = createTabTools(tabsController)

// system prompt - auto-detect language if not specified
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,13 @@
*/

export function handlePageControlMessage(
message: { type: 'PAGE_CONTROL'; action: string; payload: any; targetTabId: number },
message: {
type: 'PAGE_CONTROL'
action: string
payload: any
targetTabId: number
experimentalPointerActions?: boolean
},
sender: chrome.runtime.MessageSender,
sendResponse: (response: unknown) => void
): true | undefined {
Expand All @@ -26,6 +32,7 @@ export function handlePageControlMessage(
type: 'PAGE_CONTROL',
action,
payload,
experimentalPointerActions: message.experimentalPointerActions,
})
.then((result) => {
sendResponse(result)
Expand Down
24 changes: 16 additions & 8 deletions packages/extension/src/agent/RemotePageController.content.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,16 @@
/**
* content script for RemotePageController
*/
import { PageController } from '@page-agent/page-controller'
import { PageController, type PageControllerConfig } from '@page-agent/page-controller'

export function initPageController() {
let pageController: PageController | null = null
let intervalID: number | null = null
const pageControllerConfig: PageControllerConfig = {
enableMask: false,
viewportExpansion: 400,
experimentalPointerActions: false,
}

const myTabIdPromise = chrome.runtime
.sendMessage({ type: 'PAGE_CONTROL', action: 'get_my_tab_id' })
Expand All @@ -17,12 +22,12 @@ export function initPageController() {
return null
})

function getPC(): PageController {
function getPC(experimentalPointerActions = false): PageController {
// PageController retains this config object, so a later PAGE_CONTROL message
// can enable the experimental action without losing the indexed tree.
pageControllerConfig.experimentalPointerActions = experimentalPointerActions
if (!pageController) {
pageController = new PageController({
enableMask: false,
viewportExpansion: 400,
})
pageController = new PageController(pageControllerConfig)
}
return pageController
}
Expand Down Expand Up @@ -66,10 +71,10 @@ export function initPageController() {
return
}

const { action, payload } = message
const { action, payload, experimentalPointerActions } = message
const methodName = getMethodName(action)

const pc = getPC() as any
const pc = getPC(experimentalPointerActions === true) as any

switch (action) {
case 'get_last_update_time':
Expand All @@ -81,6 +86,7 @@ export function initPageController() {
case 'select_option':
case 'scroll':
case 'scroll_horizontally':
case 'hover_element_by_index':
case 'execute_javascript':
pc[methodName](...(payload || []))
.then((result: any) => sendResponse(result))
Expand Down Expand Up @@ -126,6 +132,8 @@ function getMethodName(action: string): string {
return 'scroll' as const
case 'scroll_horizontally':
return 'scrollHorizontally' as const
case 'hover_element_by_index':
return 'hoverElement' as const
case 'execute_javascript':
return 'executeJavascript' as const

Expand Down
17 changes: 16 additions & 1 deletion packages/extension/src/agent/RemotePageController.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ function sendMessage(message: {
action: string
targetTabId: number
payload?: any
experimentalPointerActions?: boolean
}): Promise<any> {
return chrome.runtime.sendMessage(message).catch((error) => {
console.error(PREFIX, message.action, error)
Expand All @@ -25,9 +26,14 @@ function sendMessage(message: {
*/
export class RemotePageController {
tabsController: TabsController
private experimentalPointerActions: boolean

constructor(tabsController: TabsController) {
constructor(
tabsController: TabsController,
config: { experimentalPointerActions?: boolean } = {}
) {
this.tabsController = tabsController
this.experimentalPointerActions = config.experimentalPointerActions === true
}

get currentTabId(): number | null {
Expand All @@ -52,6 +58,7 @@ export class RemotePageController {
type: 'PAGE_CONTROL',
action: 'get_last_update_time',
targetTabId: this.currentTabId,
experimentalPointerActions: this.experimentalPointerActions,
})
}

Expand All @@ -75,6 +82,7 @@ export class RemotePageController {
type: 'PAGE_CONTROL',
action: 'get_browser_state',
targetTabId: this.currentTabId,
experimentalPointerActions: this.experimentalPointerActions,
})
}

Expand All @@ -95,6 +103,7 @@ export class RemotePageController {
type: 'PAGE_CONTROL',
action: 'update_tree',
targetTabId: this.currentTabId,
experimentalPointerActions: this.experimentalPointerActions,
})
}

Expand All @@ -107,6 +116,7 @@ export class RemotePageController {
type: 'PAGE_CONTROL',
action: 'clean_up_highlights',
targetTabId: this.currentTabId,
experimentalPointerActions: this.experimentalPointerActions,
})
}

Expand All @@ -133,6 +143,10 @@ export class RemotePageController {
return this.remoteCallDomAction('scroll_horizontally', args)
}

async hoverElement(...args: any[]): Promise<DomActionReturn> {
return this.remoteCallDomAction('hover_element_by_index', args)
}

// `execute_javascript` is intentionally not implemented: AbortSignal cannot cross context

/** @note Managed by content script via storage polling. */
Expand Down Expand Up @@ -160,6 +174,7 @@ export class RemotePageController {
action: action,
targetTabId: this.currentTabId!,
payload,
experimentalPointerActions: this.experimentalPointerActions,
})
}
}
Expand Down
Loading