feat(dsh-plugin-browserskill): reveal tool schemas only after skill invocation

Progressive disclosure, final stage: with lazyTools on (default), the eleven
browser_* tool schemas stay out of the system prompt — the browser-skill
catalog entry is the sole advertisement. One successful skill invocation
(model tool call via tools/result, or a /browser-skill user gesture via the
session append feed) registers the suite for the process lifetime, idempotent
on repeats; session resume is covered by scanning durable events for a past
tool/call+tool/result pair (or skill-invocation message) on session entry and
at apply time. lazyTools: false keeps the legacy always-on registration.

Investigation (recorded in the ticket): tools/result carries normalized
name/arguments with an isError discriminant; dsh 0.1 has no official global
visibility switch (ctx.tools.restrict is agent-scoped and throws from a plain
context), making conditional registration the intended pattern; per-step
assembly picks the new suite up on the next step, and visibility changes are
first-class inputs of tool-skill's catalog digest. registerTools now returns
the combined disposer via a prototype-preserving tracking overlay; the skill
prelude tells the model the browser_* tools unlock on skill load.

Tests: +8 (hidden-before/reveal/idempotent/gesture/history hit+miss/dispose/
scanner pairing/two-state apply wiring). 124/124 green.
This commit is contained in:
BB-fat
2026-08-14 16:43:51 +00:00
parent 4d1dcd122f
commit ce11325de2
7 changed files with 423 additions and 6 deletions
+26 -3
View File
@@ -26,6 +26,13 @@ export interface PluginConfig {
observationEnabled: boolean;
thumbnailIntervalMs: number;
idleIntervalMs: number;
/**
* Lazy tool-schema injection: when true (default), the browser_* tools are
* registered only after the `browser-skill` skill has been successfully
* invoked (skill catalog entry advertises alone until then); when false,
* the suite is registered at apply time (legacy always-on behavior).
*/
lazyTools: boolean;
}
export interface ToolDeps {
@@ -136,9 +143,22 @@ function abortError(): Error {
return error;
}
/** Register the full browser tool suite. */
export function registerTools(deps: ToolDeps): void {
const { ctx, registry } = deps;
/** Register the full browser tool suite; returns the combined unregister disposer. */
export function registerTools(deps: ToolDeps): () => void {
// Track every registration through a prototype-preserving overlay so the
// lazy-injection path can dispose the whole suite in one call.
const disposers: (() => void)[] = [];
const ctx = Object.create(deps.ctx) as ToolDeps["ctx"];
const toolsOverlay = Object.create(deps.ctx.tools) as ToolDeps["ctx"]["tools"];
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.
if (typeof dispose === "function") disposers.push(dispose);
return dispose;
};
ctx.tools = toolsOverlay;
const { registry } = deps;
ctx.tools.register(
defineTool({
@@ -934,4 +954,7 @@ export function registerTools(deps: ToolDeps): void {
presentResult: presentTerminalResult,
}),
);
return () => {
for (const dispose of disposers.splice(0)) dispose();
};
}