Merge pull request #131 from Tencent/feat/dsh-browser-tools-parity

feat(dsh-plugin): add Browser tool parity and dedicated Harness skill
This commit is contained in:
Zhang GH
2026-08-27 16:29:30 +08:00
committed by GitHub
20 changed files with 2410 additions and 138 deletions
@@ -1,6 +1,6 @@
/**
* Lazy tool-schema injection ("progressive disclosure", final stage): with
* `lazyTools` on (the default), the 11 browser_* tool schemas stay OUT of the
* `lazyTools` on (the default), the 28 browser_* tool schemas stay OUT of the
* system prompt until the `browser-skill` skill has actually been invoked —
* the skill catalog entry is the only advertisement. One successful
* invocation (model tool call, or a user's `/browser-skill` gesture) reveals
@@ -96,7 +96,7 @@ export function hasSuccessfulSkillInvocation(events: readonly SessionEventLike[]
/**
* Arm the lazy reveal. Returns a disposer tearing down listeners and — when
* the reveal already happened — the tool suite itself.
* @param registerSuite - registers the 11 browser_* tools, returns their disposer.
* @param registerSuite - registers the 28 browser_* tools, returns their disposer.
*/
export function armLazyTools(ctx: Context, registerSuite: () => () => void): () => void {
let suiteDisposer: (() => void) | undefined;
@@ -439,14 +439,48 @@ export function actionForLabel(label: string): string {
return "observing";
case "click":
return "clicking";
case "hover":
return "hovering";
case "fill":
return "filling";
case "select":
return "selecting";
case "press":
return "pressing";
case "screenshot":
return "capturing";
case "emulate":
return "emulating";
case "tab list":
return "listing tabs";
case "tab create":
return "creating tab";
case "tab close":
return "closing tab";
case "tab select":
return "selecting tab";
case "tab borrow":
return "borrowing tab";
case "tab return":
return "returning tab";
case "navigate-back":
return "navigating back";
case "navigate-forward":
return "navigating forward";
case "reload":
return "reloading";
case "wait-for-navigation":
return "waiting for navigation";
case "request-help":
return "waiting for user";
case "get-html":
return "reading HTML";
case "console":
return "reading console";
case "network":
return "reading network";
case "window resize":
return "resizing window";
default:
return label;
}
@@ -0,0 +1,58 @@
import type { ToolDefinition, ToolResult, ToolRunContext } from "@deepseek-ai/dsh-tools";
import type { ToolDeps } from "./tools";
export type ToolRegistrar = (definition: ToolDefinition) => () => void;
type TerminalPresentation = { card: "terminal"; output: string; exitCode: number } | undefined;
/** Existing tool runtime seams reused by the phase-one capability modules. */
export interface PhaseOneRuntime {
run(
exec: ToolRunContext,
args: string[],
label: string,
observeSession?: string,
runnerTimeoutMs?: number,
): Promise<unknown>;
commandLine(args: string[]): string;
presentTerminalResult: (_args: never, result: ToolResult) => TerminalPresentation;
}
export function requireNonEmpty(value: string, name: string): void {
if (value.trim().length === 0) throw new Error(`${name} must be a non-empty string`);
}
export function requirePositive(value: number | undefined, name: string): void {
if (value !== undefined && value <= 0) throw new Error(`${name} must be greater than zero`);
}
/** CLI positional target detection is shared by hover/select/request-help. */
export function isSnapshotRef(target: string): boolean {
return /^@?e\d+$/.test(target);
}
/**
* Give the child enough time to honour a command-level timeout plus IPC
* settlement slack, without shortening the plugin's configured default.
*/
export function runnerTimeout(deps: ToolDeps, commandTimeoutMs: number | undefined): number {
if (commandTimeoutMs === undefined) return deps.config.defaultTimeoutMs;
return Math.max(deps.config.defaultTimeoutMs, commandTimeoutMs + 15_000);
}
export function appendTarget(args: string[], target: string): void {
args.push(target);
}
export function appendTabId(args: string[], tabId: number | undefined): void {
if (tabId !== undefined) args.push("--tab-id", String(tabId));
}
export function appendWaitOptions(
args: string[],
waitUntil: string | undefined,
timeoutMs: number | undefined,
): void {
if (waitUntil !== undefined) args.push("--wait-until", waitUntil);
if (timeoutMs !== undefined) args.push("--timeout", `${timeoutMs}ms`);
}
@@ -0,0 +1,197 @@
import { defineTool } from "@deepseek-ai/dsh-tools";
import {
appendTabId,
appendTarget,
type PhaseOneRuntime,
requireNonEmpty,
requirePositive,
runnerTimeout,
type ToolRegistrar,
} from "./phase-one-runtime";
import { SESSION_PARAM, TAB_ID_PARAM, TIMEOUT_MS_PARAM } from "./tool-params";
import type { ToolDeps } from "./tools";
const MODIFIERS = ["alt", "ctrl", "meta", "shift"] as const;
/** Add hover and select without bypassing session ownership or observation. */
export function registerPhaseOneInteractionTools(
deps: ToolDeps,
register: ToolRegistrar,
runtime: PhaseOneRuntime,
): void {
const { registry } = deps;
register(
defineTool({
name: "browser_hover",
description:
"Move the mouse over a snapshot ref or CSS selector to reveal hover-triggered UI. " +
"Run browser_observe or browser_snapshot again afterwards to discover newly visible refs.",
parameters: {
target: {
type: "string",
required: true,
description: "Snapshot ref (@e3 / e3) or CSS selector of the element to hover.",
},
session: SESSION_PARAM,
tabId: TAB_ID_PARAM,
modifiers: {
type: "array",
items: { type: "string", enum: MODIFIERS },
description: "Keyboard modifiers held during the mouse move.",
},
settleMs: {
type: "integer",
description: "Milliseconds to wait for hover-triggered UI to settle (default: 200).",
},
timeoutMs: TIMEOUT_MS_PARAM,
},
output: {
schema: {
type: "object",
additionalProperties: false,
properties: {
session: { type: "string", required: true },
tabId: { type: "integer", required: true },
x: { type: "number", required: true },
y: { type: "number", required: true },
},
},
render: (_args, value) => [
{
type: "text",
text: `[session ${value.session}] hovered at (${value.x}, ${value.y}) on tab ${value.tabId}`,
},
],
},
async execute(args, exec) {
requireNonEmpty(args.target, "target");
requirePositive(args.timeoutMs, "timeoutMs");
requirePositive(args.settleMs, "settleMs");
const sessionId = registry.resolve(args.session, "browser_hover");
const cmdArgs = ["hover", "--session", sessionId];
appendTabId(cmdArgs, args.tabId);
if (args.modifiers !== undefined && args.modifiers.length > 0) {
cmdArgs.push("--modifiers", args.modifiers.join(","));
}
if (args.settleMs !== undefined) cmdArgs.push("--settle", `${args.settleMs}ms`);
if (args.timeoutMs !== undefined) cmdArgs.push("--timeout", `${args.timeoutMs}ms`);
appendTarget(cmdArgs, args.target);
const reply = (await runtime.run(
exec,
cmdArgs,
"hover",
sessionId,
runnerTimeout(deps, args.timeoutMs),
)) as { tab_id: number; x: number; y: number };
return { session: sessionId, tabId: reply.tab_id, x: reply.x, y: reply.y };
},
presentCall: (args) => ({
card: "terminal",
title: runtime.commandLine([
"hover",
args.target,
"--session",
args.session ?? "(current)",
]),
description: "Hover an element",
}),
presentResult: runtime.presentTerminalResult,
}),
);
register(
defineTool({
name: "browser_select",
description:
"Set a select element's option values. Pass one value for a normal select or multiple " +
"values for a multi-select; values replace the current selection.",
parameters: {
target: {
type: "string",
required: true,
description: "Snapshot ref (@e3 / e3) or CSS selector of the select element.",
},
values: {
type: "array",
required: true,
items: { type: "string" },
description: "Option value attributes to select; at least one value is required.",
},
session: SESSION_PARAM,
tabId: TAB_ID_PARAM,
timeoutMs: TIMEOUT_MS_PARAM,
},
output: {
schema: {
type: "object",
additionalProperties: false,
properties: {
session: { type: "string", required: true },
tabId: { type: "integer", required: true },
multiple: { type: "boolean", required: true },
selectedValues: {
type: "array",
required: true,
items: { type: "string" },
},
selectedLabels: {
type: "array",
required: true,
items: { type: "string" },
},
},
},
render: (_args, value) => [
{
type: "text",
text:
`[session ${value.session}] selected ` +
`${value.selectedValues.join(", ") || "(none)"} on tab ${value.tabId}`,
},
],
},
async execute(args, exec) {
requireNonEmpty(args.target, "target");
requirePositive(args.timeoutMs, "timeoutMs");
if (args.values.length === 0) throw new Error("values must contain at least one option");
const sessionId = registry.resolve(args.session, "browser_select");
const cmdArgs = ["select", "--session", sessionId];
appendTabId(cmdArgs, args.tabId);
for (const value of args.values) cmdArgs.push("--value", value);
if (args.timeoutMs !== undefined) cmdArgs.push("--timeout", `${args.timeoutMs}ms`);
appendTarget(cmdArgs, args.target);
const reply = (await runtime.run(
exec,
cmdArgs,
"select",
sessionId,
runnerTimeout(deps, args.timeoutMs),
)) as {
tab_id: number;
multiple: boolean;
selected_values: string[];
selected_labels: string[];
};
return {
session: sessionId,
tabId: reply.tab_id,
multiple: reply.multiple,
selectedValues: reply.selected_values,
selectedLabels: reply.selected_labels,
};
},
presentCall: (args) => ({
card: "terminal",
title: runtime.commandLine([
"select",
args.target,
"--session",
args.session ?? "(current)",
]),
description: `Select ${args.values.length} option${args.values.length === 1 ? "" : "s"}`,
}),
presentResult: runtime.presentTerminalResult,
}),
);
}
@@ -0,0 +1,230 @@
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 = `browser_navigate_${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: "browser_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_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: "browser_wait_for_navigation",
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_wait_for_navigation");
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,
}),
);
}
@@ -0,0 +1,672 @@
import { defineTool } from "@deepseek-ai/dsh-tools";
import {
appendTabId,
isSnapshotRef,
type PhaseOneRuntime,
requireNonEmpty,
requirePositive,
runnerTimeout,
type ToolRegistrar,
} from "./phase-one-runtime";
import { SESSION_PARAM, TAB_ID_PARAM, TIMEOUT_MS_PARAM } from "./tool-params";
import type { ToolDeps } from "./tools";
interface HelpConditionInput {
urlContains?: string;
urlMatches?: string;
selectorExists?: string;
selectorMissing?: string;
textExists?: string;
textMissing?: string;
}
interface CompletionCriteriaInput {
any?: HelpConditionInput[];
all?: HelpConditionInput[];
stableForMs?: number;
}
interface RawConsoleFrame {
function_name?: string;
url?: string;
line?: number;
column?: number;
}
interface RawConsoleEntry {
sequence: number;
kind: string;
level: string;
text: string;
url?: string;
line?: number;
column?: number;
timestamp?: number;
stack_trace?: RawConsoleFrame[];
truncated?: boolean;
}
interface RawNetworkEntry {
sequence: number;
kind: "response" | "failure";
method?: string;
url?: string;
status?: number;
status_text?: string;
mime_type?: string;
resource_type?: string;
error_text?: string;
timestamp?: number;
truncated?: boolean;
}
const HELP_CONDITION_SCHEMA = {
type: "object",
additionalProperties: false,
properties: {
urlContains: { type: "string" },
urlMatches: { type: "string" },
selectorExists: { type: "string" },
selectorMissing: { type: "string" },
textExists: { type: "string" },
textMissing: { type: "string" },
},
} as const;
const DEBUG_PARAMETERS = {
session: SESSION_PARAM,
tabId: TAB_ID_PARAM,
since: {
type: "integer",
description: "Return entries with a sequence strictly greater than this cursor.",
},
limit: {
type: "integer",
description: "Maximum entries to return (default: 50, daemon cap: 200).",
},
maxTextChars: {
type: "integer",
description: "Maximum characters per entry text/URL (default: 1000, daemon cap: 4096).",
},
} as const;
function mapCondition(condition: HelpConditionInput) {
return {
...(condition.urlContains !== undefined ? { url_contains: condition.urlContains } : {}),
...(condition.urlMatches !== undefined ? { url_matches: condition.urlMatches } : {}),
...(condition.selectorExists !== undefined
? { selector_exists: condition.selectorExists }
: {}),
...(condition.selectorMissing !== undefined
? { selector_missing: condition.selectorMissing }
: {}),
...(condition.textExists !== undefined ? { text_exists: condition.textExists } : {}),
...(condition.textMissing !== undefined ? { text_missing: condition.textMissing } : {}),
};
}
function mapCompletionCriteria(criteria: CompletionCriteriaInput) {
return {
...(criteria.any !== undefined ? { any: criteria.any.map(mapCondition) } : {}),
...(criteria.all !== undefined ? { all: criteria.all.map(mapCondition) } : {}),
...(criteria.stableForMs !== undefined ? { stable_for_ms: criteria.stableForMs } : {}),
};
}
function validateDebugArgs(args: { since?: number; limit?: number; maxTextChars?: number }): void {
if (args.since !== undefined && args.since < 0) {
throw new Error("since must be zero or greater");
}
requirePositive(args.limit, "limit");
requirePositive(args.maxTextChars, "maxTextChars");
}
function appendDebugOptions(
args: string[],
options: { tabId?: number; since?: number; limit?: number; maxTextChars?: number },
): void {
appendTabId(args, options.tabId);
if (options.since !== undefined) args.push("--since", String(options.since));
if (options.limit !== undefined) args.push("--limit", String(options.limit));
if (options.maxTextChars !== undefined) {
args.push("--max-text-chars", String(options.maxTextChars));
}
}
/** Human help, raw HTML, diagnostics, and Agent Window sizing. */
export function registerPhaseOneSupportTools(
deps: ToolDeps,
register: ToolRegistrar,
runtime: PhaseOneRuntime,
): void {
const { registry } = deps;
register(
defineTool({
name: "browser_request_help",
description:
"Pause browser automation and ask the user to complete an in-page step such as login, " +
"captcha, OTP, or confirmation. After continued/completed, observe again before using refs.",
parameters: {
prompt: {
type: "string",
required: true,
description: "Clear instructions shown to the user in the browser help overlay.",
},
session: SESSION_PARAM,
tabId: TAB_ID_PARAM,
title: { type: "string", description: "Optional title for the help overlay." },
targets: {
type: "array",
items: { type: "string" },
description: "Snapshot refs or CSS selectors to scroll to and highlight for the user.",
},
timeoutMs: {
...TIMEOUT_MS_PARAM,
description: "Maximum wait for the user in milliseconds (default: 300000).",
},
completionCriteria: {
type: "object",
additionalProperties: false,
description: "Optional explicit success detector for automatic completion.",
properties: {
any: { type: "array", items: HELP_CONDITION_SCHEMA },
all: { type: "array", items: HELP_CONDITION_SCHEMA },
stableForMs: {
type: "integer",
description: "How long criteria must remain true before completing.",
},
},
},
},
output: {
schema: {
type: "object",
additionalProperties: false,
properties: {
session: { type: "string", required: true },
tabId: { type: "integer", required: true },
outcome: {
type: "string",
required: true,
enum: ["continued", "cancelled", "timed_out", "completed", "navigated", "disabled"],
},
completedBy: { type: "string" },
note: { type: "string" },
resolvedTargets: {
type: "array",
items: {
type: "object",
additionalProperties: false,
properties: {
matched: { type: "boolean", required: true },
ref: { type: "string" },
selector: { type: "string" },
},
},
},
},
},
render: (_args, value) => [
{
type: "text",
text:
`[session ${value.session}] user help outcome on tab ${value.tabId}: ${value.outcome}` +
(value.note !== undefined ? ` — ${value.note}` : "") +
(value.resolvedTargets !== undefined
? `\ntargets: ${value.resolvedTargets
.map(
(target) =>
`${target.ref ?? target.selector ?? "(unknown)"}=${target.matched ? "matched" : "not found"}`,
)
.join(", ")}`
: ""),
},
],
},
async execute(args, exec) {
requireNonEmpty(args.prompt, "prompt");
if (args.title !== undefined) requireNonEmpty(args.title, "title");
requirePositive(args.timeoutMs, "timeoutMs");
for (const target of args.targets ?? []) requireNonEmpty(target, "target");
const criteria = args.completionCriteria as CompletionCriteriaInput | undefined;
if (criteria?.stableForMs !== undefined && criteria.stableForMs < 0) {
throw new Error("completionCriteria.stableForMs must be zero or greater");
}
const sessionId = registry.resolve(args.session, "browser_request_help");
const timeoutMs = args.timeoutMs ?? 300_000;
const cmdArgs = [
"request-help",
"--session",
sessionId,
"--prompt",
args.prompt,
"--timeout",
`${timeoutMs}ms`,
];
appendTabId(cmdArgs, args.tabId);
if (args.title !== undefined) cmdArgs.push("--title", args.title);
for (const target of args.targets ?? []) cmdArgs.push("--target", target);
if (criteria !== undefined) {
cmdArgs.push("--completion-criteria", JSON.stringify(mapCompletionCriteria(criteria)));
}
const reply = (await runtime.run(
exec,
cmdArgs,
"request-help",
sessionId,
runnerTimeout(deps, timeoutMs),
)) as {
outcome: "continued" | "cancelled" | "timed_out" | "completed" | "navigated" | "disabled";
completed_by?: string;
note?: string;
tab_id: number;
resolved_targets?: { matched: boolean; ref?: string; selector?: string }[];
};
return {
session: sessionId,
tabId: reply.tab_id,
outcome: reply.outcome,
...(reply.completed_by !== undefined ? { completedBy: reply.completed_by } : {}),
...(reply.note !== undefined ? { note: reply.note } : {}),
...(reply.resolved_targets !== undefined
? { resolvedTargets: reply.resolved_targets }
: {}),
};
},
presentCall: (args) => ({
card: "terminal",
title: runtime.commandLine(["request-help", "--session", args.session ?? "(current)"]),
description: "Ask the user for browser help",
}),
presentResult: runtime.presentTerminalResult,
}),
);
register(
defineTool({
name: "browser_get_html",
description:
"Read raw DOM HTML from the active Agent Window tab, optionally scoped to a fresh " +
"snapshot ref. Use only when browser_observe/browser_snapshot cannot answer the question.",
parameters: {
session: SESSION_PARAM,
tabId: TAB_ID_PARAM,
ref: { type: "string", description: "Fresh snapshot ref that scopes the HTML subtree." },
maxBytes: {
type: "integer",
description: "Maximum returned HTML bytes before truncation (default: 524288).",
},
},
output: {
schema: {
type: "object",
additionalProperties: false,
properties: {
session: { type: "string", required: true },
tabId: { type: "integer", required: true },
html: { type: "string", required: true },
truncated: { type: "boolean", required: true },
byteSize: { type: "integer", required: true },
},
},
render: (_args, value) => [
{
type: "text",
text:
value.html +
(value.truncated ? `\n(HTML truncated; original size ${value.byteSize} bytes)` : ""),
},
],
},
isConcurrencySafe: () => true,
async execute(args, exec) {
if (args.ref !== undefined) {
requireNonEmpty(args.ref, "ref");
if (!isSnapshotRef(args.ref)) throw new Error("ref must be a snapshot ref such as @e3");
}
requirePositive(args.maxBytes, "maxBytes");
const sessionId = registry.resolve(args.session, "browser_get_html");
const cmdArgs = ["get-html", "--session", sessionId];
appendTabId(cmdArgs, args.tabId);
if (args.ref !== undefined) cmdArgs.push("--ref", args.ref);
if (args.maxBytes !== undefined) cmdArgs.push("--max-bytes", String(args.maxBytes));
const reply = (await runtime.run(exec, cmdArgs, "get-html", sessionId)) as {
tab_id: number;
html: string;
truncated?: boolean;
byte_size: number;
};
return {
session: sessionId,
tabId: reply.tab_id,
html: reply.html,
truncated: reply.truncated ?? false,
byteSize: reply.byte_size,
};
},
presentCall: (args) => ({
card: "terminal",
title: runtime.commandLine([
"get-html",
"--session",
args.session ?? "(current)",
...(args.ref !== undefined ? ["--ref", args.ref] : []),
]),
description: "Read raw page HTML",
}),
presentResult: runtime.presentTerminalResult,
}),
);
register(
defineTool({
name: "browser_console",
description:
"Read buffered console messages, browser log entries, and JavaScript exceptions from a tab. " +
"This is read-only and does not evaluate JavaScript.",
parameters: {
...DEBUG_PARAMETERS,
includeStack: {
type: "boolean",
description: "Include structured stack frames in returned console entries.",
},
},
output: {
schema: {
type: "object",
additionalProperties: false,
properties: {
session: { type: "string", required: true },
tabId: { type: "integer", required: true },
entries: {
type: "array",
required: true,
items: {
type: "object",
additionalProperties: false,
properties: {
sequence: { type: "integer", required: true },
kind: { type: "string", required: true },
level: { type: "string", required: true },
text: { type: "string", required: true },
url: { type: "string" },
line: { type: "integer" },
column: { type: "integer" },
timestamp: { type: "number" },
stackTrace: {
type: "array",
required: true,
items: {
type: "object",
additionalProperties: false,
properties: {
functionName: { type: "string" },
url: { type: "string" },
line: { type: "integer" },
column: { type: "integer" },
},
},
},
truncated: { type: "boolean", required: true },
},
},
},
nextSince: { type: "integer", required: true },
truncated: { type: "boolean", required: true },
},
},
render: (_args, value) => [
{
type: "text",
text:
value.entries.length === 0
? "(no console messages captured)"
: value.entries
.map((entry) => {
const location =
entry.url === undefined
? ""
: ` ${entry.url}${entry.line !== undefined ? `:${entry.line}` : ""}${entry.column !== undefined ? `:${entry.column}` : ""}`;
const stack = entry.stackTrace
.map(
(frame) =>
`\n at ${frame.functionName ?? "<anonymous>"} ${frame.url ?? ""}` +
`${frame.line !== undefined ? `:${frame.line}` : ""}` +
`${frame.column !== undefined ? `:${frame.column}` : ""}`,
)
.join("");
return `#${entry.sequence} ${entry.level} ${entry.kind}${location} ${entry.text}${stack}`;
})
.join("\n") +
(value.truncated
? `\n(output truncated; continue with since=${value.nextSince})`
: ""),
},
],
},
isConcurrencySafe: () => true,
async execute(args, exec) {
validateDebugArgs(args);
const sessionId = registry.resolve(args.session, "browser_console");
const cmdArgs = ["console", "--session", sessionId];
appendDebugOptions(cmdArgs, args);
if (args.includeStack === true) cmdArgs.push("--include-stack");
const reply = (await runtime.run(exec, cmdArgs, "console", sessionId)) as {
tab_id: number;
entries?: RawConsoleEntry[];
next_since: number;
truncated?: boolean;
};
return {
session: sessionId,
tabId: reply.tab_id,
entries: (reply.entries ?? []).map((entry) => ({
sequence: entry.sequence,
kind: entry.kind,
level: entry.level,
text: entry.text,
...(entry.url !== undefined ? { url: entry.url } : {}),
...(entry.line !== undefined ? { line: entry.line } : {}),
...(entry.column !== undefined ? { column: entry.column } : {}),
...(entry.timestamp !== undefined ? { timestamp: entry.timestamp } : {}),
stackTrace: (entry.stack_trace ?? []).map((frame) => ({
...(frame.function_name !== undefined ? { functionName: frame.function_name } : {}),
...(frame.url !== undefined ? { url: frame.url } : {}),
...(frame.line !== undefined ? { line: frame.line } : {}),
...(frame.column !== undefined ? { column: frame.column } : {}),
})),
truncated: entry.truncated ?? false,
})),
nextSince: reply.next_since,
truncated: reply.truncated ?? false,
};
},
presentCall: (args) => ({
card: "terminal",
title: runtime.commandLine(["console", "--session", args.session ?? "(current)"]),
description: "Read browser console messages",
}),
presentResult: runtime.presentTerminalResult,
}),
);
register(
defineTool({
name: "browser_network",
description:
"Read buffered network responses and failures from a tab. Returns metadata only; request " +
"and response headers and bodies are not captured.",
parameters: DEBUG_PARAMETERS,
output: {
schema: {
type: "object",
additionalProperties: false,
properties: {
session: { type: "string", required: true },
tabId: { type: "integer", required: true },
entries: {
type: "array",
required: true,
items: {
type: "object",
additionalProperties: false,
properties: {
sequence: { type: "integer", required: true },
kind: { type: "string", required: true, enum: ["response", "failure"] },
method: { type: "string" },
url: { type: "string" },
status: { type: "integer" },
statusText: { type: "string" },
mimeType: { type: "string" },
resourceType: { type: "string" },
errorText: { type: "string" },
timestamp: { type: "number" },
truncated: { type: "boolean", required: true },
},
},
},
nextSince: { type: "integer", required: true },
truncated: { type: "boolean", required: true },
},
},
render: (_args, value) => [
{
type: "text",
text:
value.entries.length === 0
? "(no network activity captured)"
: value.entries
.map((entry) =>
entry.kind === "failure"
? `#${entry.sequence} FAILED ${entry.method ?? "?"} ${entry.url ?? "(unknown)"} — ${entry.errorText ?? "failed"}`
: `#${entry.sequence} ${entry.status ?? "?"} ${entry.method ?? "?"} ${entry.url ?? "(unknown)"}`,
)
.join("\n") +
(value.truncated
? `\n(output truncated; continue with since=${value.nextSince})`
: ""),
},
],
},
isConcurrencySafe: () => true,
async execute(args, exec) {
validateDebugArgs(args);
const sessionId = registry.resolve(args.session, "browser_network");
const cmdArgs = ["network", "--session", sessionId];
appendDebugOptions(cmdArgs, args);
const reply = (await runtime.run(exec, cmdArgs, "network", sessionId)) as {
tab_id: number;
entries?: RawNetworkEntry[];
next_since: number;
truncated?: boolean;
};
return {
session: sessionId,
tabId: reply.tab_id,
entries: (reply.entries ?? []).map((entry) => ({
sequence: entry.sequence,
kind: entry.kind,
...(entry.method !== undefined ? { method: entry.method } : {}),
...(entry.url !== undefined ? { url: entry.url } : {}),
...(entry.status !== undefined ? { status: entry.status } : {}),
...(entry.status_text !== undefined ? { statusText: entry.status_text } : {}),
...(entry.mime_type !== undefined ? { mimeType: entry.mime_type } : {}),
...(entry.resource_type !== undefined ? { resourceType: entry.resource_type } : {}),
...(entry.error_text !== undefined ? { errorText: entry.error_text } : {}),
...(entry.timestamp !== undefined ? { timestamp: entry.timestamp } : {}),
truncated: entry.truncated ?? false,
})),
nextSince: reply.next_since,
truncated: reply.truncated ?? false,
};
},
presentCall: (args) => ({
card: "terminal",
title: runtime.commandLine(["network", "--session", args.session ?? "(current)"]),
description: "Read browser network activity",
}),
presentResult: runtime.presentTerminalResult,
}),
);
register(
defineTool({
name: "browser_window_resize",
description: "Resize the owned session's Agent Window using outer CSS-pixel dimensions.",
parameters: {
width: {
type: "integer",
required: true,
description: "Agent Window outer width in CSS pixels (100..=7680).",
},
height: {
type: "integer",
required: true,
description: "Agent Window outer height in CSS pixels (100..=7680).",
},
session: SESSION_PARAM,
},
output: {
schema: {
type: "object",
additionalProperties: false,
properties: {
session: { type: "string", required: true },
windowId: { type: "integer", required: true },
width: { type: "integer", required: true },
height: { type: "integer", required: true },
},
},
render: (_args, value) => [
{
type: "text",
text: `[session ${value.session}] resized window ${value.windowId} to ${value.width}x${value.height}`,
},
],
},
async execute(args, exec) {
if (args.width < 100 || args.width > 7680 || args.height < 100 || args.height > 7680) {
throw new Error("width and height must each be in the range 100..=7680");
}
const sessionId = registry.resolve(args.session, "browser_window_resize");
const reply = (await runtime.run(
exec,
[
"window",
"resize",
"--session",
sessionId,
"--width",
String(args.width),
"--height",
String(args.height),
],
"window resize",
sessionId,
)) as { window_id: number; width: number; height: number };
return {
session: sessionId,
windowId: reply.window_id,
width: reply.width,
height: reply.height,
};
},
presentCall: (args) => ({
card: "terminal",
title: runtime.commandLine([
"window",
"resize",
"--session",
args.session ?? "(current)",
"--width",
String(args.width),
"--height",
String(args.height),
]),
description: "Resize the Agent Window",
}),
presentResult: runtime.presentTerminalResult,
}),
);
}
@@ -0,0 +1,402 @@
import { defineTool } from "@deepseek-ai/dsh-tools";
import type { PhaseOneRuntime, ToolRegistrar } from "./phase-one-runtime";
import { requireNonEmpty } from "./phase-one-runtime";
import { SESSION_PARAM } from "./tool-params";
import type { ToolDeps } from "./tools";
const TAB_ID_REQUIRED = {
type: "integer",
required: true,
description: "Chrome tab id returned by browser_tab_list or browser_tab_create.",
} as const;
interface RawTabInfo {
tab_id: number;
title?: string;
url?: string;
window_id?: number;
active?: boolean;
scope?: "user" | "agent";
}
/** Add Agent Window tab management while preserving the owned-session boundary. */
export function registerPhaseOneTabTools(
deps: ToolDeps,
register: ToolRegistrar,
runtime: PhaseOneRuntime,
): void {
const { registry } = deps;
register(
defineTool({
name: "browser_tab_list",
description:
"List tabs visible to an owned browser session. Other sessions' Agent Windows remain " +
"hidden; use scope=user to find a user tab before borrowing it.",
parameters: {
session: SESSION_PARAM,
scope: {
type: "string",
enum: ["user", "agent", "all"],
description: "Which tabs to list (default: all).",
},
},
output: {
schema: {
type: "object",
additionalProperties: false,
properties: {
session: { type: "string", required: true },
tabs: {
type: "array",
required: true,
items: {
type: "object",
additionalProperties: false,
properties: {
tabId: { type: "integer", required: true },
title: { type: "string" },
url: { type: "string" },
windowId: { type: "integer" },
active: { type: "boolean" },
scope: { type: "string", enum: ["user", "agent"] },
},
},
},
},
},
render: (_args, value) => [
{
type: "text",
text:
value.tabs.length === 0
? `[session ${value.session}] no matching tabs`
: value.tabs
.map(
(tab) =>
`${tab.tabId} [${tab.scope ?? "unknown"}]${tab.active === true ? " [active]" : ""} ` +
`${tab.title ?? "(untitled)"} — ${tab.url ?? "(unknown URL)"}`,
)
.join("\n"),
},
],
},
isConcurrencySafe: () => true,
async execute(args, exec) {
const sessionId = registry.resolve(args.session, "browser_tab_list");
const cmdArgs = ["tab", "list", "--session", sessionId];
if (args.scope !== undefined) cmdArgs.push("--scope", args.scope);
const reply = (await runtime.run(exec, cmdArgs, "tab list", sessionId)) as {
tabs: RawTabInfo[];
};
return {
session: sessionId,
tabs: reply.tabs.map((tab) => ({
tabId: tab.tab_id,
...(tab.title !== undefined ? { title: tab.title } : {}),
...(tab.url !== undefined ? { url: tab.url } : {}),
...(tab.window_id !== undefined ? { windowId: tab.window_id } : {}),
...(tab.active !== undefined ? { active: tab.active } : {}),
...(tab.scope !== undefined ? { scope: tab.scope } : {}),
})),
};
},
presentCall: (args) => ({
card: "terminal",
title: runtime.commandLine([
"tab",
"list",
"--session",
args.session ?? "(current)",
...(args.scope !== undefined ? ["--scope", args.scope] : []),
]),
description: "List browser tabs",
}),
presentResult: runtime.presentTerminalResult,
}),
);
register(
defineTool({
name: "browser_tab_create",
description:
"Create a tab inside the owned session's Agent Window. The tab is focused by default; " +
"set active=false to open it in the background.",
parameters: {
session: SESSION_PARAM,
url: { type: "string", description: "Initial URL (default: chrome://newtab/)." },
active: { type: "boolean", description: "Whether to focus the new tab (default: true)." },
index: { type: "integer", description: "Insertion index in the Agent Window tab strip." },
},
output: {
schema: {
type: "object",
additionalProperties: false,
properties: {
session: { type: "string", required: true },
tabId: { type: "integer", required: true },
windowId: { type: "integer", required: true },
url: { type: "string", required: true },
},
},
render: (_args, value) => [
{
type: "text",
text: `[session ${value.session}] created tab ${value.tabId} in window ${value.windowId} (${value.url || "pending URL"})`,
},
],
},
async execute(args, exec) {
if (args.url !== undefined) requireNonEmpty(args.url, "url");
const sessionId = registry.resolve(args.session, "browser_tab_create");
const cmdArgs = ["tab", "create", "--session", sessionId];
if (args.url !== undefined) cmdArgs.push("--url", args.url);
if (args.active === false) cmdArgs.push("--no-active");
if (args.index !== undefined) cmdArgs.push("--index", String(args.index));
const reply = (await runtime.run(exec, cmdArgs, "tab create", sessionId)) as {
tab_id: number;
window_id: number;
url: string;
};
if (args.active !== false && reply.url.length > 0) {
deps.observation.setUrl(sessionId, reply.url);
}
return {
session: sessionId,
tabId: reply.tab_id,
windowId: reply.window_id,
url: reply.url,
};
},
presentCall: (args) => ({
card: "terminal",
title: runtime.commandLine([
"tab",
"create",
"--session",
args.session ?? "(current)",
...(args.url !== undefined ? ["--url", args.url] : []),
]),
description: "Create an Agent Window tab",
}),
presentResult: runtime.presentTerminalResult,
}),
);
register(
defineTool({
name: "browser_tab_close",
description: "Close a tab in the owned session's Agent Window.",
parameters: { tabId: TAB_ID_REQUIRED, session: SESSION_PARAM },
output: {
schema: {
type: "object",
additionalProperties: false,
properties: {
session: { type: "string", required: true },
tabId: { type: "integer", required: true },
},
},
render: (_args, value) => [
{ type: "text", text: `[session ${value.session}] closed tab ${value.tabId}` },
],
},
async execute(args, exec) {
const sessionId = registry.resolve(args.session, "browser_tab_close");
const reply = (await runtime.run(
exec,
["tab", "close", String(args.tabId), "--session", sessionId],
"tab close",
sessionId,
)) as { tab_id: number };
return { session: sessionId, tabId: reply.tab_id };
},
presentCall: (args) => ({
card: "terminal",
title: runtime.commandLine([
"tab",
"close",
String(args.tabId),
"--session",
args.session ?? "(current)",
]),
description: "Close an Agent Window tab",
}),
presentResult: runtime.presentTerminalResult,
}),
);
register(
defineTool({
name: "browser_tab_select",
description: "Focus a tab in the owned session's Agent Window.",
parameters: { tabId: TAB_ID_REQUIRED, session: SESSION_PARAM },
output: {
schema: {
type: "object",
additionalProperties: false,
properties: {
session: { type: "string", required: true },
tabId: { type: "integer", required: true },
windowId: { type: "integer", required: true },
},
},
render: (_args, value) => [
{
type: "text",
text: `[session ${value.session}] selected tab ${value.tabId} in window ${value.windowId}`,
},
],
},
async execute(args, exec) {
const sessionId = registry.resolve(args.session, "browser_tab_select");
const reply = (await runtime.run(
exec,
["tab", "select", String(args.tabId), "--session", sessionId],
"tab select",
sessionId,
)) as { tab_id: number; window_id: number };
return { session: sessionId, tabId: reply.tab_id, windowId: reply.window_id };
},
presentCall: (args) => ({
card: "terminal",
title: runtime.commandLine([
"tab",
"select",
String(args.tabId),
"--session",
args.session ?? "(current)",
]),
description: "Select an Agent Window tab",
}),
presentResult: runtime.presentTerminalResult,
}),
);
register(
defineTool({
name: "browser_tab_borrow",
description:
"Move a user-window tab into the owned session's Agent Window for controlled interaction. " +
"Return it with browser_tab_return when finished; stopping the session also auto-returns it.",
parameters: { tabId: TAB_ID_REQUIRED, session: SESSION_PARAM },
output: {
schema: {
type: "object",
additionalProperties: false,
properties: {
session: { type: "string", required: true },
tabId: { type: "integer", required: true },
originalWindowId: { type: "integer", required: true },
originalIndex: { type: "integer", required: true },
agentWindowId: { type: "integer", required: true },
},
},
render: (_args, value) => [
{
type: "text",
text:
`[session ${value.session}] borrowed tab ${value.tabId} from window ` +
`${value.originalWindowId} into Agent Window ${value.agentWindowId}`,
},
],
},
async execute(args, exec) {
const sessionId = registry.resolve(args.session, "browser_tab_borrow");
const reply = (await runtime.run(
exec,
["tab", "borrow", String(args.tabId), "--session", sessionId],
"tab borrow",
sessionId,
)) as {
tab_id: number;
original_window_id: number;
original_index: number;
agent_window_id: number;
};
return {
session: sessionId,
tabId: reply.tab_id,
originalWindowId: reply.original_window_id,
originalIndex: reply.original_index,
agentWindowId: reply.agent_window_id,
};
},
presentCall: (args) => ({
card: "terminal",
title: runtime.commandLine([
"tab",
"borrow",
String(args.tabId),
"--session",
args.session ?? "(current)",
]),
description: "Borrow a user tab",
}),
presentResult: runtime.presentTerminalResult,
}),
);
register(
defineTool({
name: "browser_tab_return",
description: "Return a borrowed tab to its original user window and tab-strip position.",
parameters: { tabId: TAB_ID_REQUIRED, session: SESSION_PARAM },
output: {
schema: {
type: "object",
additionalProperties: false,
properties: {
session: { type: "string", required: true },
tabId: { type: "integer", required: true },
returnedToWindowId: { type: "integer", required: true },
returnedToIndex: { type: "integer", required: true },
fallback: { type: "boolean", required: true },
},
},
render: (_args, value) => [
{
type: "text",
text:
`[session ${value.session}] returned tab ${value.tabId} to window ` +
`${value.returnedToWindowId} at index ${value.returnedToIndex}` +
(value.fallback ? " (original window unavailable; used fallback)" : ""),
},
],
},
async execute(args, exec) {
const sessionId = registry.resolve(args.session, "browser_tab_return");
const reply = (await runtime.run(
exec,
["tab", "return", String(args.tabId), "--session", sessionId],
"tab return",
sessionId,
)) as {
tab_id: number;
returned_to_window_id: number;
returned_to_index: number;
fallback?: boolean;
};
return {
session: sessionId,
tabId: reply.tab_id,
returnedToWindowId: reply.returned_to_window_id,
returnedToIndex: reply.returned_to_index,
fallback: reply.fallback ?? false,
};
},
presentCall: (args) => ({
card: "terminal",
title: runtime.commandLine([
"tab",
"return",
String(args.tabId),
"--session",
args.session ?? "(current)",
]),
description: "Return a borrowed tab",
}),
presentResult: runtime.presentTerminalResult,
}),
);
}
@@ -0,0 +1,18 @@
import type { PhaseOneRuntime, ToolRegistrar } from "./phase-one-runtime";
import { registerPhaseOneInteractionTools } from "./phase-one-tools-interaction";
import { registerPhaseOneNavigationTools } from "./phase-one-tools-navigation";
import { registerPhaseOneSupportTools } from "./phase-one-tools-support";
import { registerPhaseOneTabTools } from "./phase-one-tools-tabs";
import type { ToolDeps } from "./tools";
/** Register the first DSH capability-parity tranche on the existing runtime. */
export function registerPhaseOneTools(
deps: ToolDeps,
register: ToolRegistrar,
runtime: PhaseOneRuntime,
): void {
registerPhaseOneInteractionTools(deps, register, runtime);
registerPhaseOneTabTools(deps, register, runtime);
registerPhaseOneNavigationTools(deps, register, runtime);
registerPhaseOneSupportTools(deps, register, runtime);
}
@@ -2,10 +2,10 @@
* Progressive disclosure of the BrowserSkill agent skill through the harness's
* official skill seam (`ctx.skills`). The catalog entry (name + description)
* is resident; the body is loaded only when the model invokes the `skill`
* tool. The markdown is build-time assembled into a static module (dsh
* prelude + the canonical CLI skill body verbatim), so registration and every
* pre-step catalog snapshot are pure in-memory reads — no disk, no process,
* no daemon.
* tool. The dedicated DeepSeek Harness markdown is embedded into a static
* module at build time, so registration and every pre-step catalog snapshot
* are pure in-memory reads — no disk, no process, no daemon. The CLI-oriented
* repository skill is intentionally a separate interface.
*/
import type { Context } from "@deepseek-ai/cordis";
@@ -26,7 +26,7 @@ interface SkillsLike {
}
/**
* Publish the BSK skill as an embedded runtime skill. Returns the unregistration
* Publish the browser skill as an embedded runtime skill. Returns the unregistration
* disposer. A composition without the `skills` service (or with dsh-tool-skill
* retired) leaves the rest of the plugin unaffected — the no-op is silent.
*/
@@ -0,0 +1,24 @@
/** Shared model-facing parameter schemas for browser tools. */
export const SESSION_PARAM = {
type: "string",
description:
"bsk session id to act on; must be one created by this plugin's browser_session_start. " +
"Omit to use the current session (the one most recently started or used).",
} as const;
export const TAB_ID_PARAM = {
type: "integer",
description: "Target tab id. Omit to use the Agent Window's active tab.",
} as const;
export const WAIT_UNTIL_PARAM = {
type: "string",
enum: ["load", "domcontentloaded", "networkidle", "commit"],
description: "Page lifecycle phase to wait for (default: load).",
} as const;
export const TIMEOUT_MS_PARAM = {
type: "integer",
description: "Command timeout in milliseconds; must be greater than zero.",
} as const;
+10 -8
View File
@@ -20,9 +20,11 @@ import {
import { ownerSessionIds } from "./archive-cleanup";
import { trySaveScreenshot } from "./image";
import { actionForLabel, type ObservationService } from "./observation";
import { registerPhaseOneTools } from "./phase-one-tools";
import type { KeyedExecutor } from "./queue";
import { type BskRunner, bskInstallMessage, isCommandNotFound, parseBskJson } from "./runner";
import type { SessionRegistry } from "./sessions";
import { SESSION_PARAM } from "./tool-params";
/** Plugin configuration resolved from the Schemastery schema in index.ts. */
export interface PluginConfig {
@@ -62,13 +64,6 @@ const DEVICE_PRESETS = [
"galaxy-tab-s8",
] as const;
const SESSION_PARAM = {
type: "string",
description:
"bsk session id to act on; must be one created by this plugin's browser_session_start. " +
"Omit to use the current session (the one most recently started or used).",
} as const;
/**
* Run one bsk command and return its parsed JSON payload, or throw.
* `observeSession` marks the run as model-facing work on that session: it is
@@ -82,6 +77,7 @@ async function runBsk(
args: string[],
label: string,
observeSession?: string,
runnerTimeoutMs?: number,
): Promise<unknown> {
// beginAction must fire only when the task actually starts inside the
// per-session queue — marking it while still queued would overwrite the
@@ -98,7 +94,7 @@ async function runBsk(
}
return deps.runner.run(args, {
signal: exec.signal,
timeoutMs: deps.config.defaultTimeoutMs,
timeoutMs: runnerTimeoutMs ?? deps.config.defaultTimeoutMs,
...(observeSession !== undefined ? { tag: observeSession } : {}),
});
};
@@ -959,6 +955,12 @@ export function registerTools(deps: ToolDeps): () => void {
presentResult: presentTerminalResult,
}),
);
registerPhaseOneTools(deps, register, {
run: (exec, args, label, observeSession, runnerTimeoutMs) =>
runBsk(deps, exec, args, label, observeSession, runnerTimeoutMs),
commandLine: (args) => cmdline(deps, args),
presentTerminalResult,
});
return () => {
for (const dispose of disposers.splice(0)) dispose();
};