Files
BrowserSkill/packages/dsh-plugin-browserskill/src/phase-one-tools-navigation.ts
T

231 lines
7.7 KiB
TypeScript

import { defineTool } from "@deepseek-ai/dsh-tools";
import {
appendTabId,
appendWaitOptions,
type PhaseOneRuntime,
requirePositive,
runnerTimeout,
type ToolRegistrar,
} from "./phase-one-runtime";
import { SESSION_PARAM, TAB_ID_PARAM, TIMEOUT_MS_PARAM, WAIT_UNTIL_PARAM } from "./tool-params";
import type { ToolDeps } from "./tools";
interface RawHistoryResult {
tab_id: number;
previous_url?: string;
final_url?: string;
reached: string;
error_text?: string;
}
const HISTORY_OUTPUT = {
type: "object",
additionalProperties: false,
properties: {
session: { type: "string", required: true },
tabId: { type: "integer", required: true },
previousUrl: { type: "string" },
finalUrl: { type: "string" },
reached: { type: "string", required: true },
errorText: { type: "string" },
},
} as const;
function mapHistory(sessionId: string, reply: RawHistoryResult) {
return {
session: sessionId,
tabId: reply.tab_id,
...(reply.previous_url !== undefined ? { previousUrl: reply.previous_url } : {}),
...(reply.final_url !== undefined ? { finalUrl: reply.final_url } : {}),
reached: reply.reached,
...(reply.error_text !== undefined ? { errorText: reply.error_text } : {}),
};
}
/** Add browser history, reload, and explicit lifecycle waiting. */
export function registerPhaseOneNavigationTools(
deps: ToolDeps,
register: ToolRegistrar,
runtime: PhaseOneRuntime,
): void {
const { registry } = deps;
const registerHistory = (direction: "back" | "forward") => {
const toolName = `page.${direction}` as const;
const command = `navigate-${direction}`;
register(
defineTool({
name: toolName,
description: `Navigate the active Agent Window tab ${direction} by one history entry.`,
parameters: {
session: SESSION_PARAM,
tabId: TAB_ID_PARAM,
waitUntil: WAIT_UNTIL_PARAM,
timeoutMs: TIMEOUT_MS_PARAM,
},
output: {
schema: HISTORY_OUTPUT,
render: (_args, value) => [
{
type: "text",
text:
`[session ${value.session}] navigated ${direction} on tab ${value.tabId}` +
(value.finalUrl !== undefined ? ` to ${value.finalUrl}` : "") +
` (reached: ${value.reached})` +
(value.errorText !== undefined ? ` — ${value.errorText}` : ""),
},
],
},
async execute(args, exec) {
requirePositive(args.timeoutMs, "timeoutMs");
const sessionId = registry.resolve(args.session, toolName);
const cmdArgs = [command, "--session", sessionId];
appendTabId(cmdArgs, args.tabId);
appendWaitOptions(cmdArgs, args.waitUntil, args.timeoutMs);
const reply = (await runtime.run(
exec,
cmdArgs,
command,
sessionId,
runnerTimeout(deps, args.timeoutMs),
)) as RawHistoryResult;
if (reply.final_url !== undefined) deps.observation.setUrl(sessionId, reply.final_url);
return mapHistory(sessionId, reply);
},
presentCall: (args) => ({
card: "terminal",
title: runtime.commandLine([command, "--session", args.session ?? "(current)"]),
description: `Navigate ${direction}`,
}),
presentResult: runtime.presentTerminalResult,
}),
);
};
registerHistory("back");
registerHistory("forward");
register(
defineTool({
name: "page.reload",
description:
"Reload the active Agent Window tab and wait for a lifecycle phase. Set hard=true to " +
"bypass the HTTP cache.",
parameters: {
session: SESSION_PARAM,
tabId: TAB_ID_PARAM,
waitUntil: WAIT_UNTIL_PARAM,
timeoutMs: TIMEOUT_MS_PARAM,
hard: { type: "boolean", description: "Bypass the HTTP cache while reloading." },
},
output: {
schema: HISTORY_OUTPUT,
render: (_args, value) => [
{
type: "text",
text:
`[session ${value.session}] reloaded tab ${value.tabId}` +
(value.finalUrl !== undefined ? ` at ${value.finalUrl}` : "") +
` (reached: ${value.reached})` +
(value.errorText !== undefined ? ` — ${value.errorText}` : ""),
},
],
},
async execute(args, exec) {
requirePositive(args.timeoutMs, "timeoutMs");
const sessionId = registry.resolve(args.session, "browser_page(action=reload)");
const cmdArgs = ["reload", "--session", sessionId];
appendTabId(cmdArgs, args.tabId);
appendWaitOptions(cmdArgs, args.waitUntil, args.timeoutMs);
if (args.hard === true) cmdArgs.push("--hard");
const reply = (await runtime.run(
exec,
cmdArgs,
"reload",
sessionId,
runnerTimeout(deps, args.timeoutMs),
)) as RawHistoryResult;
if (reply.final_url !== undefined) deps.observation.setUrl(sessionId, reply.final_url);
return mapHistory(sessionId, reply);
},
presentCall: (args) => ({
card: "terminal",
title: runtime.commandLine([
"reload",
"--session",
args.session ?? "(current)",
...(args.hard === true ? ["--hard"] : []),
]),
description: "Reload the active tab",
}),
presentResult: runtime.presentTerminalResult,
}),
);
register(
defineTool({
name: "page.wait",
description:
"Wait for a page lifecycle event on the active Agent Window tab. Use after an action " +
"that may navigate when the action result itself does not wait for navigation.",
parameters: {
session: SESSION_PARAM,
tabId: TAB_ID_PARAM,
waitUntil: WAIT_UNTIL_PARAM,
timeoutMs: TIMEOUT_MS_PARAM,
},
output: {
schema: {
type: "object",
additionalProperties: false,
properties: {
session: { type: "string", required: true },
tabId: { type: "integer", required: true },
reached: { type: "string", required: true },
errorText: { type: "string" },
},
},
render: (_args, value) => [
{
type: "text",
text:
`[session ${value.session}] navigation wait on tab ${value.tabId}: ${value.reached}` +
(value.errorText !== undefined ? ` — ${value.errorText}` : ""),
},
],
},
async execute(args, exec) {
requirePositive(args.timeoutMs, "timeoutMs");
const sessionId = registry.resolve(args.session, "browser_page(action=wait)");
const timeoutMs = args.timeoutMs ?? 30_000;
const cmdArgs = ["wait-for-navigation", "--session", sessionId];
appendTabId(cmdArgs, args.tabId);
if (args.waitUntil !== undefined) cmdArgs.push("--wait-until", args.waitUntil);
cmdArgs.push("--timeout", `${timeoutMs}ms`);
const reply = (await runtime.run(
exec,
cmdArgs,
"wait-for-navigation",
sessionId,
runnerTimeout(deps, timeoutMs),
)) as { tab_id: number; reached: string; error_text?: string };
return {
session: sessionId,
tabId: reply.tab_id,
reached: reply.reached,
...(reply.error_text !== undefined ? { errorText: reply.error_text } : {}),
};
},
presentCall: (args) => ({
card: "terminal",
title: runtime.commandLine([
"wait-for-navigation",
"--session",
args.session ?? "(current)",
]),
description: "Wait for page navigation",
}),
presentResult: runtime.presentTerminalResult,
}),
);
}