2025-09-29 16:33:15 +08:00
/**
* Internal tools for PageAgent.
* @note Adapted from browser-use
*/
2026-03-05 19:13:18 +08:00
import * as z from 'zod/v4'
2025-09-29 16:33:15 +08:00
2026-01-19 16:06:07 +08:00
import type { PageAgentCore } from '../PageAgentCore'
2025-12-05 16:18:01 +08:00
import { waitFor } from '../utils'
2025-09-29 16:33:15 +08:00
2026-06-05 21:23:18 +08:00
/**
* Per-invocation context passed to every tool execution.
* Tools MUST honor `signal` to support cooperative cancellation.
*/
export interface ToolContext {
signal : AbortSignal
}
2025-10-17 18:43:41 +08:00
/**
* Internal tool definition that has access to PageAgent `this` context
*/
export interface PageAgentTool < TParams = any > {
// name: string
description : string
2026-02-25 16:18:56 +08:00
inputSchema : z.ZodType < TParams >
2026-06-05 21:23:18 +08:00
execute : ( this : PageAgentCore , args : TParams , ctx : ToolContext ) => Promise < string >
2025-10-17 18:43:41 +08:00
}
export function tool < TParams >( options : PageAgentTool < TParams >) : PageAgentTool < TParams > {
return options
}
2025-09-29 16:33:15 +08:00
/**
* Internal tools for PageAgent.
2025-10-17 18:43:41 +08:00
* Note: Using any to allow different parameter types for each tool
2025-09-29 16:33:15 +08:00
*/
2025-10-17 18:43:41 +08:00
export const tools = new Map < string , PageAgentTool >()
2025-09-29 16:33:15 +08:00
tools . set (
'done' ,
tool ({
description :
2026-02-09 21:04:39 +08:00
'Complete task. Text is your final response to the user — keep it concise unless the user explicitly asks for detail.' ,
2026-02-25 16:18:56 +08:00
inputSchema : z.object ({
text : z.string (),
success : z.boolean (). default ( true ),
2025-09-29 16:33:15 +08:00
}),
2026-01-19 16:06:07 +08:00
execute : async function ( this : PageAgentCore , input ) {
2025-09-29 16:33:15 +08:00
// @note main loop will handle this one
2025-10-17 18:43:41 +08:00
return Promise . resolve ( 'Task completed' )
2025-09-29 16:33:15 +08:00
},
})
)
tools . set (
'wait' ,
tool ({
2026-02-09 21:04:39 +08:00
description : 'Wait for x seconds. Can be used to wait until the page or data is fully loaded.' ,
2026-02-25 16:18:56 +08:00
inputSchema : z.object ({
seconds : z.number (). min ( 1 ). max ( 10 ). default ( 1 ),
2025-09-29 16:33:15 +08:00
}),
2026-06-05 21:23:18 +08:00
execute : async function ( this : PageAgentCore , input , { signal }) {
2026-02-10 16:28:25 +08:00
// try to subtract LLM calling time from the actual wait time
2025-12-05 16:18:01 +08:00
const lastTimeUpdate = await this . pageController . getLastUpdateTime ()
2026-05-18 14:53:24 +02:00
const secondsSinceLastUpdate = ( Date . now () - lastTimeUpdate ) / 1000
const actualWaitTime = Math . max ( 0 , input . seconds - secondsSinceLastUpdate )
2025-09-29 16:33:15 +08:00
console . log ( `actualWaitTime: ${ actualWaitTime } seconds` )
2026-06-05 21:23:18 +08:00
await waitFor ( actualWaitTime , signal )
2026-01-15 20:17:50 +08:00
2026-06-12 17:44:21 +08:00
const waitedSeconds = ( secondsSinceLastUpdate + actualWaitTime ). toFixed ( 2 )
return `✅ Waited for ${ waitedSeconds } seconds.`
2025-09-29 16:33:15 +08:00
},
})
)
tools . set (
'ask_user' ,
tool ({
description :
'Ask the user a question and wait for their answer. Use this if you need more information or clarification.' ,
2026-02-25 16:18:56 +08:00
inputSchema : z.object ({
question : z.string (),
2025-09-29 16:33:15 +08:00
}),
2026-06-05 21:23:18 +08:00
execute : async function ( this : PageAgentCore , input , { signal }) {
2026-01-17 23:14:26 +08:00
if ( ! this . onAskUser ) {
throw new Error ( 'ask_user tool requires onAskUser callback to be set' )
2026-01-17 17:24:17 +08:00
}
2026-06-05 21:23:18 +08:00
const answer = await this . onAskUser ( input . question , { signal })
2026-01-17 23:14:26 +08:00
return `User answered: ${ answer } `
2025-09-29 16:33:15 +08:00
},
})
)
tools . set (
'click_element_by_index' ,
tool ({
description : 'Click element by index' ,
2026-02-25 16:18:56 +08:00
inputSchema : z.object ({
index : z.int (). min ( 0 ),
2025-09-29 16:33:15 +08:00
}),
2026-01-19 16:06:07 +08:00
execute : async function ( this : PageAgentCore , input ) {
2025-12-05 16:18:01 +08:00
const result = await this . pageController . clickElement ( input . index )
return result . message
2025-09-29 16:33:15 +08:00
},
})
)
tools . set (
'input_text' ,
tool ({
2026-02-09 21:04:39 +08:00
description : 'Click and type text into an interactive input element' ,
2026-02-25 16:18:56 +08:00
inputSchema : z.object ({
index : z.int (). min ( 0 ),
text : z.string (),
2025-09-29 16:33:15 +08:00
}),
2026-01-19 16:06:07 +08:00
execute : async function ( this : PageAgentCore , input ) {
2025-12-05 16:18:01 +08:00
const result = await this . pageController . inputText ( input . index , input . text )
return result . message
2025-09-29 16:33:15 +08:00
},
})
)
tools . set (
'select_dropdown_option' ,
tool ({
description :
'Select dropdown option for interactive element index by the text of the option you want to select' ,
2026-02-25 16:18:56 +08:00
inputSchema : z.object ({
index : z.int (). min ( 0 ),
text : z.string (),
2025-09-29 16:33:15 +08:00
}),
2026-01-19 16:06:07 +08:00
execute : async function ( this : PageAgentCore , input ) {
2025-12-05 16:18:01 +08:00
const result = await this . pageController . selectOption ( input . index , input . text )
return result . message
2025-09-29 16:33:15 +08:00
},
})
)
/**
* @note Reference from browser-use
*/
tools . set (
'scroll' ,
tool ({
2026-04-02 22:05:47 +08:00
description :
'Scroll vertically. Without index: scrolls the document. With index: scrolls the container at that index (or its nearest scrollable ancestor). Use index of a data-scrollable element to scroll a specific area.' ,
2026-02-25 16:18:56 +08:00
inputSchema : z.object ({
down : z.boolean (). default ( true ),
num_pages : z.number (). min ( 0 ). max ( 10 ). optional (). default ( 0.1 ),
pixels : z.number (). int (). min ( 0 ). optional (),
index : z.number (). int (). min ( 0 ). optional (),
2025-09-29 16:33:15 +08:00
}),
2026-01-19 16:06:07 +08:00
execute : async function ( this : PageAgentCore , input ) {
2025-12-05 16:18:01 +08:00
const result = await this . pageController . scroll ({
... input ,
numPages : input.num_pages ,
})
return result . message
2025-09-29 16:33:15 +08:00
},
})
)
2026-02-09 21:04:39 +08:00
/**
* @todo Tables need a dedicated parser to extract structured data. This tool is useless.
*/
2025-09-29 16:33:15 +08:00
tools . set (
'scroll_horizontally' ,
tool ({
description :
2026-04-02 22:05:47 +08:00
'Scroll horizontally. Without index: scrolls the document. With index: scrolls the container at that index (or its nearest scrollable ancestor). Use index of a data-scrollable element to scroll a specific area.' ,
2026-02-25 16:18:56 +08:00
inputSchema : z.object ({
right : z.boolean (). default ( true ),
pixels : z.number (). int (). min ( 0 ),
index : z.number (). int (). min ( 0 ). optional (),
2025-09-29 16:33:15 +08:00
}),
2026-01-19 16:06:07 +08:00
execute : async function ( this : PageAgentCore , input ) {
2025-12-05 16:18:01 +08:00
const result = await this . pageController . scrollHorizontally ( input )
return result . message
2025-09-29 16:33:15 +08:00
},
})
)
2025-10-23 19:59:17 +08:00
tools . set (
'execute_javascript' ,
tool ({
description :
2026-06-09 21:30:27 +08:00
'Execute JavaScript code on the current page. Supports async/await syntax. Use with caution! ' +
'An `AbortSignal` named `signal` is available in scope: long-running async code MUST honor it ' +
'(e.g. `await fetch(url, { signal })`, or `signal.throwIfAborted()` in loops)' ,
2026-02-25 16:18:56 +08:00
inputSchema : z.object ({
script : z.string (),
2025-10-23 19:59:17 +08:00
}),
2026-06-09 21:30:27 +08:00
execute : async function ( this : PageAgentCore , input , { signal }) {
const result = await this . pageController . executeJavascript ( input . script , signal )
signal . throwIfAborted ()
2025-12-05 16:18:01 +08:00
return result . message
2025-10-23 19:59:17 +08:00
},
})
)
2025-09-29 16:33:15 +08:00
// @todo send_keys
// @todo upload_file
// @todo extract_structured_data