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.
This commit is contained in:
can1357
2026-08-03 15:17:46 +02:00
parent 0faa145985
commit f6b0d0debf
3 changed files with 42 additions and 17 deletions
+34 -8
View File
@@ -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, unknown>): 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 `<example${intentAttr}>\n${args[soleKey]}\n</example>`;
return `<example${intentAttr}>\n${bare}\n</example>`;
}
// 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 `<examples>\n${parts.join("\n")}\n</examples>`;
}
/**
* 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 `<examples>` block.
*/
export function renderToolExamplesJsdoc(tool: InbandTool): string {
const examples = tool.examples;
if (!examples?.length) return "";
const renderCall = (args: Record<string, unknown>): 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, unknown>): 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;
}
+2 -2
View File
@@ -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());
+6 -7
View File
@@ -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("// <examples>");
expect(out).toContain("// # Basic");
expect(out).toContain('// @example "Basic"');
expect(out).toContain('// web_search(query="rust", recency="week")');
expect(out).toContain("// </examples>");
expect(out).not.toContain("<examples>");
// Examples sit between the description and the type declaration.
expect(out.indexOf("// Searches the web.")).toBeLessThan(out.indexOf("// <examples>"));
expect(out.indexOf("// </examples>")).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("<examples>");
expect(out).not.toContain("@example");
});
it("renders a parameterless tool without an args object", () => {