mirror of
https://github.com/Tencent/BrowserSkill.git
synced 2026-09-10 13:01:23 +08:00
fix(dsh-plugin-browserskill): register the lazy suite from any fiber
Cordis contexts are fiber-owned: assigning onto an Object.create() overlay of the apply-time context fails with 'cannot set property tools in multiple fibers' when the reveal fires from the tools/result listener fiber (caught by the e2e: the skill invocation matched but the suite never registered). registerTools now tracks disposers through a plain closure over the captured context — no context mutation anywhere. Also documents lazyTools in the README and hardens the reveal against a failed registration (log and stay hidden instead of latching). Verified end to end against the real stack: first request carries 0 browser_* schemas (skill catalog entry only); after the scripted skill invocation the next request carries exactly the 11 browser_* tools and browser_session_start dispatches for real.
This commit is contained in:
@@ -82,8 +82,17 @@ All fields are optional and validated through the plugin's Schemastery `Config`:
|
|||||||
# observationEnabled: true # live PiP/overlay observation (below)
|
# observationEnabled: true # live PiP/overlay observation (below)
|
||||||
# thumbnailIntervalMs: 1500 # frame cadence while a session is active
|
# thumbnailIntervalMs: 1500 # frame cadence while a session is active
|
||||||
# idleIntervalMs: 8000 # idle cadence / recent-activity window
|
# idleIntervalMs: 8000 # idle cadence / recent-activity window
|
||||||
|
# lazyTools: true # reveal browser_* tools only after the skill is invoked
|
||||||
```
|
```
|
||||||
|
|
||||||
|
- **`lazyTools` (default `true`)** — the final progressive-disclosure stage: the eleven
|
||||||
|
`browser_*` tool schemas stay OUT of the system prompt (zero schema tokens) until the
|
||||||
|
`browser-skill` skill is actually invoked — the skill catalog entry is the only
|
||||||
|
advertisement. One successful invocation (model tool call, or a `/browser-skill` user
|
||||||
|
gesture) registers the whole suite for the rest of the process; repeated invocations are
|
||||||
|
no-ops, and sessions resumed with a past invocation in their durable log reveal the suite
|
||||||
|
on entry. Set `false` for the legacy always-on registration.
|
||||||
|
|
||||||
## Observation overlay (PiP mini-window)
|
## Observation overlay (PiP mini-window)
|
||||||
|
|
||||||
When the plugin runs inside the dsh Web UI, an **observation overlay** floats over the app
|
When the plugin runs inside the dsh Web UI, an **observation overlay** floats over the app
|
||||||
|
|||||||
@@ -103,7 +103,16 @@ export function armLazyTools(ctx: Context, registerSuite: () => () => void): ()
|
|||||||
const disposers: (() => void)[] = [];
|
const disposers: (() => void)[] = [];
|
||||||
const ensureSuite = (): void => {
|
const ensureSuite = (): void => {
|
||||||
if (suiteDisposer !== undefined) return;
|
if (suiteDisposer !== undefined) return;
|
||||||
suiteDisposer = registerSuite();
|
try {
|
||||||
|
suiteDisposer = registerSuite();
|
||||||
|
} catch (error) {
|
||||||
|
// A failed reveal must not strand the plugin: stay hidden, log, retry on
|
||||||
|
// the next trigger instead of latching a half-registered suite.
|
||||||
|
suiteDisposer = undefined;
|
||||||
|
console.warn(
|
||||||
|
`[dsh-plugin-browserskill] lazy tool registration failed: ${error instanceof Error ? error.message : String(error)}`,
|
||||||
|
);
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// Live trigger: a successful model invocation of skill/browser-skill.
|
// Live trigger: a successful model invocation of skill/browser-skill.
|
||||||
|
|||||||
@@ -11,7 +11,12 @@ import { tmpdir } from "node:os";
|
|||||||
import { join } from "node:path";
|
import { join } from "node:path";
|
||||||
import type { Context } from "@deepseek-ai/cordis";
|
import type { Context } from "@deepseek-ai/cordis";
|
||||||
import type { ContentBlock } from "@deepseek-ai/dsh-llm";
|
import type { ContentBlock } from "@deepseek-ai/dsh-llm";
|
||||||
import { defineTool, type ToolResult, type ToolRunContext } from "@deepseek-ai/dsh-tools";
|
import {
|
||||||
|
defineTool,
|
||||||
|
type ToolDefinition,
|
||||||
|
type ToolResult,
|
||||||
|
type ToolRunContext,
|
||||||
|
} from "@deepseek-ai/dsh-tools";
|
||||||
import { trySaveScreenshot } from "./image";
|
import { trySaveScreenshot } from "./image";
|
||||||
import { actionForLabel, type ObservationService } from "./observation";
|
import { actionForLabel, type ObservationService } from "./observation";
|
||||||
import type { KeyedExecutor } from "./queue";
|
import type { KeyedExecutor } from "./queue";
|
||||||
@@ -145,22 +150,18 @@ function abortError(): Error {
|
|||||||
|
|
||||||
/** Register the full browser tool suite; returns the combined unregister disposer. */
|
/** Register the full browser tool suite; returns the combined unregister disposer. */
|
||||||
export function registerTools(deps: ToolDeps): () => void {
|
export function registerTools(deps: ToolDeps): () => void {
|
||||||
// Track every registration through a prototype-preserving overlay so the
|
const { ctx, registry } = deps;
|
||||||
// lazy-injection path can dispose the whole suite in one call.
|
// Track every registration without touching the cordis context itself
|
||||||
|
// (contexts are fiber-owned; the lazy path registers from a listener fiber).
|
||||||
const disposers: (() => void)[] = [];
|
const disposers: (() => void)[] = [];
|
||||||
const ctx = Object.create(deps.ctx) as ToolDeps["ctx"];
|
const register = (definition: ToolDefinition): (() => void) => {
|
||||||
const toolsOverlay = Object.create(deps.ctx.tools) as ToolDeps["ctx"]["tools"];
|
const dispose = ctx.tools.register(definition);
|
||||||
const originalRegister = deps.ctx.tools.register;
|
|
||||||
toolsOverlay.register = (definition: Parameters<typeof originalRegister>[0]) => {
|
|
||||||
const dispose = originalRegister.call(deps.ctx.tools, definition);
|
|
||||||
// Test doubles occasionally return the registry map instead of a disposer.
|
// Test doubles occasionally return the registry map instead of a disposer.
|
||||||
if (typeof dispose === "function") disposers.push(dispose);
|
if (typeof dispose === "function") disposers.push(dispose);
|
||||||
return dispose;
|
return dispose;
|
||||||
};
|
};
|
||||||
ctx.tools = toolsOverlay;
|
|
||||||
const { registry } = deps;
|
|
||||||
|
|
||||||
ctx.tools.register(
|
register(
|
||||||
defineTool({
|
defineTool({
|
||||||
name: "browser_session_start",
|
name: "browser_session_start",
|
||||||
description:
|
description:
|
||||||
@@ -305,7 +306,7 @@ export function registerTools(deps: ToolDeps): () => void {
|
|||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
|
|
||||||
ctx.tools.register(
|
register(
|
||||||
defineTool({
|
defineTool({
|
||||||
name: "browser_session_stop",
|
name: "browser_session_stop",
|
||||||
description:
|
description:
|
||||||
@@ -339,7 +340,7 @@ export function registerTools(deps: ToolDeps): () => void {
|
|||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
|
|
||||||
ctx.tools.register(
|
register(
|
||||||
defineTool({
|
defineTool({
|
||||||
name: "browser_session_list",
|
name: "browser_session_list",
|
||||||
description:
|
description:
|
||||||
@@ -404,7 +405,7 @@ export function registerTools(deps: ToolDeps): () => void {
|
|||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
|
|
||||||
ctx.tools.register(
|
register(
|
||||||
defineTool({
|
defineTool({
|
||||||
name: "browser_navigate",
|
name: "browser_navigate",
|
||||||
description:
|
description:
|
||||||
@@ -485,7 +486,7 @@ export function registerTools(deps: ToolDeps): () => void {
|
|||||||
"carry @eN refs for browser_click / browser_fill. Prefer browser_observe for a richer semantic view."
|
"carry @eN refs for browser_click / browser_fill. Prefer browser_observe for a richer semantic view."
|
||||||
: "Produce a semantic VOM observation of the session's active tab (roles, states, perception " +
|
: "Produce a semantic VOM observation of the session's active tab (roles, states, perception " +
|
||||||
"probes) with @eN refs for browser_click / browser_fill. Read-only: never submits input.";
|
"probes) with @eN refs for browser_click / browser_fill. Read-only: never submits input.";
|
||||||
ctx.tools.register(
|
register(
|
||||||
defineTool({
|
defineTool({
|
||||||
name,
|
name,
|
||||||
description,
|
description,
|
||||||
@@ -551,7 +552,7 @@ export function registerTools(deps: ToolDeps): () => void {
|
|||||||
registerObservationTool("snapshot");
|
registerObservationTool("snapshot");
|
||||||
registerObservationTool("observe");
|
registerObservationTool("observe");
|
||||||
|
|
||||||
ctx.tools.register(
|
register(
|
||||||
defineTool({
|
defineTool({
|
||||||
name: "browser_click",
|
name: "browser_click",
|
||||||
description:
|
description:
|
||||||
@@ -615,7 +616,7 @@ export function registerTools(deps: ToolDeps): () => void {
|
|||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
|
|
||||||
ctx.tools.register(
|
register(
|
||||||
defineTool({
|
defineTool({
|
||||||
name: "browser_fill",
|
name: "browser_fill",
|
||||||
description:
|
description:
|
||||||
@@ -672,7 +673,7 @@ export function registerTools(deps: ToolDeps): () => void {
|
|||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
|
|
||||||
ctx.tools.register(
|
register(
|
||||||
defineTool({
|
defineTool({
|
||||||
name: "browser_press",
|
name: "browser_press",
|
||||||
description:
|
description:
|
||||||
@@ -742,7 +743,7 @@ export function registerTools(deps: ToolDeps): () => void {
|
|||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
|
|
||||||
ctx.tools.register(
|
register(
|
||||||
defineTool({
|
defineTool({
|
||||||
name: "browser_screenshot",
|
name: "browser_screenshot",
|
||||||
description:
|
description:
|
||||||
@@ -868,7 +869,7 @@ export function registerTools(deps: ToolDeps): () => void {
|
|||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
|
|
||||||
ctx.tools.register(
|
register(
|
||||||
defineTool({
|
defineTool({
|
||||||
name: "browser_emulate",
|
name: "browser_emulate",
|
||||||
description:
|
description:
|
||||||
|
|||||||
Reference in New Issue
Block a user