From f6b0d0debf459a5c33dbdfd766966087931f1647 Mon Sep 17 00:00:00 2001 From: can1357 Date: Mon, 3 Aug 2026 15:17:46 +0200 Subject: [PATCH] feat(ai/dialect): added jsdoc-style tool example rendering for inventory - Add `renderToolExamplesJsdoc` to generate `@example` comment lines for tool inventories. - Extract `bareStringArg` helper to simplify single-argument checks across renderers. - Update tool inventory rendering to use the new JSDoc example format. --- packages/ai/src/dialect/examples.ts | 42 ++++++++++++++++++++----- packages/ai/src/dialect/inventory.ts | 4 +-- packages/ai/test/tool-inventory.test.ts | 13 ++++---- 3 files changed, 42 insertions(+), 17 deletions(-) diff --git a/packages/ai/src/dialect/examples.ts b/packages/ai/src/dialect/examples.ts index 9f953ba9d..cb86462f4 100644 --- a/packages/ai/src/dialect/examples.ts +++ b/packages/ai/src/dialect/examples.ts @@ -15,17 +15,12 @@ export function renderToolExamples(tool: InbandTool, intentField?: string): stri const examples = tool.examples; if (!examples?.length) return ""; const renderCall = (args: Record): string => { - let soleKey: string | undefined; - let argCount = 0; - for (const key in args) { - argCount++; - soleKey = key; - } - if (argCount === 1 && soleKey !== undefined && typeof args[soleKey] === "string") { + const bare = bareStringArg(args); + if (bare !== undefined) { // Bare payload. The intent placeholder still rides on the envelope so // intent-traced schemas (where `i` is required) keep teaching it. const intentAttr = intentField ? ` ${intentField}="${INTENT_PLACEHOLDER}"` : ""; - return `\n${args[soleKey]}\n`; + return `\n${bare}\n`; } // When intent tracing injects `i` into the schema, examples must show a // placeholder so the model learns to emit it. Keep it first, matching the @@ -43,3 +38,34 @@ export function renderToolExamples(tool: InbandTool, intentField?: string): stri }); return `\n${parts.join("\n")}\n`; } + +/** + * Render a tool's examples as JSDoc-style `@example` lines for comment-gutter + * contexts (the Harmony `namespace functions` inventory): `@example "caption"` + * followed by the call in the same Python kwargs syntax as the wire block. The + * tag line delimits each example, so no XML envelope is needed — which is why + * the inventory uses this instead of `//`-prefixing the `` block. + */ +export function renderToolExamplesJsdoc(tool: InbandTool): string { + const examples = tool.examples; + if (!examples?.length) return ""; + const renderCall = (args: Record): string => bareStringArg(args) ?? pyCall(tool.name, args); + const parts = examples.map(ex => { + const head = ex.caption ? `@example ${JSON.stringify(ex.caption)}` : "@example"; + if ("call" in ex) return `${head}\n${renderCall(ex.call)}`; + if ("good" in ex) return `${head}\nWRONG:\n${renderCall(ex.bad)}\nRIGHT:\n${renderCall(ex.good)}`; + return ex.note ? `${head}\n${ex.note}` : head; + }); + return parts.join("\n"); +} + +/** Sole-argument string payload, if the call has exactly one string argument. */ +function bareStringArg(args: Record): string | undefined { + let sole: unknown; + let count = 0; + for (const key in args) { + count++; + sole = args[key]; + } + return count === 1 && typeof sole === "string" ? sole : undefined; +} diff --git a/packages/ai/src/dialect/inventory.ts b/packages/ai/src/dialect/inventory.ts index f82687147..0927b700a 100644 --- a/packages/ai/src/dialect/inventory.ts +++ b/packages/ai/src/dialect/inventory.ts @@ -1,5 +1,5 @@ import { jsonSchemaToTypeScript, toolWireSchema } from "../utils/schema"; -import { renderToolExamples } from "./examples"; +import { renderToolExamplesJsdoc } from "./examples"; import type { InbandTool } from "./types"; /** @@ -18,7 +18,7 @@ export function renderToolInventory(tools: readonly InbandTool[]): string { if (description) { for (const line of description.split("\n")) lines.push(`// ${line}`.trimEnd()); } - const examples = renderToolExamples(tool); + const examples = renderToolExamplesJsdoc(tool); if (examples) { if (description) lines.push("//"); for (const line of examples.split("\n")) lines.push(`// ${line}`.trimEnd()); diff --git a/packages/ai/test/tool-inventory.test.ts b/packages/ai/test/tool-inventory.test.ts index cd329017e..6b9c3fa56 100644 --- a/packages/ai/test/tool-inventory.test.ts +++ b/packages/ai/test/tool-inventory.test.ts @@ -26,15 +26,14 @@ describe("renderToolInventory", () => { expect(out).toContain("});"); }); - it("renders examples as comment lines above the declaration", () => { + it("renders examples as @example comment lines above the declaration", () => { const out = renderToolInventory([searchTool]); - expect(out).toContain("// "); - expect(out).toContain("// # Basic"); + expect(out).toContain('// @example "Basic"'); expect(out).toContain('// web_search(query="rust", recency="week")'); - expect(out).toContain("// "); + expect(out).not.toContain(""); // Examples sit between the description and the type declaration. - expect(out.indexOf("// Searches the web.")).toBeLessThan(out.indexOf("// ")); - expect(out.indexOf("// ")).toBeLessThan(out.indexOf("type web_search")); + expect(out.indexOf("// Searches the web.")).toBeLessThan(out.indexOf('// @example "Basic"')); + expect(out.indexOf('// @example "Basic"')).toBeLessThan(out.indexOf("type web_search")); }); it("omits the examples comment block when a tool has none", () => { @@ -45,7 +44,7 @@ describe("renderToolInventory", () => { }; const out = renderToolInventory([tool]); expect(out).toContain("type noop = (_: {"); - expect(out).not.toContain(""); + expect(out).not.toContain("@example"); }); it("renders a parameterless tool without an args object", () => {