diff --git a/packages/coding-agent/src/config/settings-schema.ts b/packages/coding-agent/src/config/settings-schema.ts index 37c465faa..0b693f077 100644 --- a/packages/coding-agent/src/config/settings-schema.ts +++ b/packages/coding-agent/src/config/settings-schema.ts @@ -4036,6 +4036,28 @@ export const SETTINGS_SCHEMA = { }, }, + "tools.xdevDocs": { + type: "enum", + values: ["inline", "builtins", "catalog"] as const, + default: "builtins", + ui: { + tab: "tools", + group: "Discovery & MCP", + label: "xd:// Prompt Docs", + description: + "Choose which mounted-device docs and schemas are inlined in the system prompt. Built-ins keeps core tools inline while MCP and extension tools stay on-demand.", + options: [ + { value: "inline", label: "All Devices", description: "Inline docs and schemas for every mounted device." }, + { + value: "builtins", + label: "Built-ins Only", + description: "Inline built-in docs; fetch MCP and extension docs on demand.", + }, + { value: "catalog", label: "Catalog Only", description: "List every device; fetch all docs on demand." }, + ], + }, + }, + // MCP "mcp.enableProjectConfig": { type: "boolean", diff --git a/packages/coding-agent/src/sdk.ts b/packages/coding-agent/src/sdk.ts index 3ed00eaff..80e6cd58e 100644 --- a/packages/coding-agent/src/sdk.ts +++ b/packages/coding-agent/src/sdk.ts @@ -2596,7 +2596,7 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {} cwd, additionalWorkspaceRoots: sessionManager.getAdditionalDirectories(), xdevTools: toolSession.xdevRegistry?.entries() ?? [], - xdevDocs: toolSession.xdevRegistry?.docsAll() ?? "", + xdevDocs: toolSession.xdevRegistry?.docsAll(settings.get("tools.xdevDocs")) ?? "", autoQaEnabled: !restrictToolNames && isAutoQaEnabled(settings), resolvedCustomPrompt: options.customSystemPrompt, skills: session?.skills ?? skills, diff --git a/packages/coding-agent/src/tools/xdev.ts b/packages/coding-agent/src/tools/xdev.ts index 48ca0cd17..d85665d18 100644 --- a/packages/coding-agent/src/tools/xdev.ts +++ b/packages/coding-agent/src/tools/xdev.ts @@ -60,6 +60,9 @@ export const XDEV_KEEP_TOP_LEVEL: Record = { */ export const XDEV_TRANSPORT_TOOLS: Record = { read: true, write: true }; +/** Controls which mounted-device docs are inlined into the system prompt. */ +export type XdevDocsMode = "inline" | "builtins" | "catalog"; + /** * Whether an enabled tool is presented under `xd://` (rather than top-level) * while the `xd://` transport is active. Discoverable tools mount unless they @@ -273,11 +276,16 @@ export class XdevRegistry { * Dynamic mounts embed at most {@link EXTERNAL_DESCRIPTION_CAP} description * chars (schema always intact); `read xd://` returns the full text. */ - docsAll(): string { + docsAll(mode: XdevDocsMode = "inline"): string { const sections: string[] = []; const overflow: Tool[] = []; let used = 0; for (const tool of this.list()) { + const isBuiltin = this.#builtins.has(tool.name); + if (mode === "catalog" || (mode === "builtins" && !isBuiltin)) { + overflow.push(tool); + continue; + } const descriptionCap = this.#dynamic.has(tool.name) ? XdevRegistry.EXTERNAL_DESCRIPTION_CAP : undefined; const docs = renderDocs(tool, "##", descriptionCap); if (docs.length > XdevRegistry.DOCS_PER_DEVICE_CAP || used + docs.length > XdevRegistry.DOCS_TOTAL_BUDGET) { diff --git a/packages/coding-agent/test/write-xdev-dispatch.test.ts b/packages/coding-agent/test/write-xdev-dispatch.test.ts index 23f717cfd..cd87faf6d 100644 --- a/packages/coding-agent/test/write-xdev-dispatch.test.ts +++ b/packages/coding-agent/test/write-xdev-dispatch.test.ts @@ -186,6 +186,7 @@ describe("read and write route xd:// device URLs", () => { const tempDir = await fs.mkdtemp(path.join(os.tmpdir(), "write-xdev-docs-")); try { const session = xdevSession(tempDir); + expect(session.settings.get("tools.xdevDocs")).toBe("builtins"); await createTools(session); const mounted = session.xdevRegistry?.list() ?? []; expect(mounted.length).toBeGreaterThan(0); @@ -208,30 +209,37 @@ describe("read and write route xd:// device URLs", () => { } }); - it("docsAll truncates external (dynamic-mount) descriptions to the cap; built-ins and read xd:// stay full", async () => { + it("docsAll supports inline, builtins, and catalog prompt modes", async () => { const tempDir = await fs.mkdtemp(path.join(os.tmpdir(), "write-xdev-external-")); try { const session = xdevSession(tempDir); + expect(session.settings.get("tools.xdevDocs")).toBe("builtins"); await createTools(session); const registry = session.xdevRegistry; if (!registry) throw new Error("expected xdev registry"); const mounted = registry.list(); - const longDescription = `LEDE ${"y".repeat(XdevRegistry.EXTERNAL_DESCRIPTION_CAP * 3)} TAIL`; + const longDescription = "LEDE " + "y".repeat(XdevRegistry.EXTERNAL_DESCRIPTION_CAP * 3) + " TAIL"; const external = Object.create(mounted[0]!) as (typeof mounted)[number]; Object.defineProperty(external, "name", { value: "mcp_external_tool" }); Object.defineProperty(external, "description", { value: longDescription }); registry.reconcile([external]); - const docs = registry.docsAll(); - // External device: schema section present, description cut at the cap. - expect(docs).toContain("## mcp_external_tool"); - expect(docs).toContain("LEDE "); - expect(docs).not.toContain("TAIL"); - expect(docs).toContain("… (full docs: read xd://mcp_external_tool)"); - // Built-in devices keep their full curated description. - expect(docs).toContain(mounted[0]!.description ?? ""); - // On-demand docs return the untruncated text. + const inlineDocs = registry.docsAll("inline"); + expect(inlineDocs).toContain("## mcp_external_tool"); + expect(inlineDocs).toContain("LEDE "); + expect(inlineDocs).not.toContain("TAIL"); + expect(inlineDocs).toContain("… (full docs: read xd://mcp_external_tool)"); + + const builtinsDocs = registry.docsAll("builtins"); + expect(builtinsDocs).toContain("## "); + expect(builtinsDocs).not.toContain("## mcp_external_tool"); + expect(builtinsDocs).toContain("- xd://mcp_external_tool —"); + + const catalogDocs = registry.docsAll("catalog"); + expect(catalogDocs).not.toContain(`## ${mounted[0]!.name}`); + expect(catalogDocs).toContain("- xd://"); + expect(catalogDocs).toContain("- xd://mcp_external_tool —"); expect(registry.docs("mcp_external_tool")).toContain("TAIL"); } finally { await removeWithRetries(tempDir);