mirror of
https://github.com/badlogic/pi-mono.git
synced 2026-11-05 23:37:25 +00:00
fix(coding-agent): follow local schema refs in tool search and codemode summaries (#11850)
Fixes #11803. **Problem** `tool_search` builds a tool's search text from its parameter schema but never followed `$ref`. MCP servers (Pydantic, FastMCP) often put nested models in `$defs`: ```json { "properties": { "options": { "$ref": "#/$defs/options" } }, "$defs": { "options": { "properties": { "mode": { "description": "reconciliation" } } } } } ``` Search text was `Inspect records options`, so `tool_search("reconciliation")` found nothing. Durable's copy of `tool_search` had the same gap. The codemode `resolves to` summary had the same blind spot for output schemas: a field behind a `$ref` showed as `structuredContent?` with no field names. **Fix** - pi-codemode exports its existing local JSON Pointer resolver as `resolveSchemaRef(ref, root)`. A malformed percent escape now resolves to nothing instead of throwing `URIError` (this also affected `schemaToType()`). - coding-agent and Durable `schemaText()` follow local `$ref`s. Each referenced subschema is read once, so recursive and shared definitions neither loop nor inflate BM25 term counts. Unreferenced `$defs` and remote refs stay out. - coding-agent `describeFields()`/`describeUnionFields()` resolve `$ref` chains against the output schema root. **Tests** - `packages/codemode/test/declarations.test.ts`: escaped, remote, missing, malformed refs. - `packages/coding-agent/test/tool-search.test.ts`: refs, escaped segments, recursion and shared definitions, unused definitions. - `packages/coding-agent/test/suite/agent-session-codemode.test.ts`: summary through root, field, const, and union refs. - `packages/durable/test/tool-search.test.ts`: end-to-end load via a ref'd description; unused definition does not match. `npm run check` passes.
This commit is contained in:
parent
850ea2eae2
commit
6d3e342b25
12 changed files with 262 additions and 29 deletions
|
|
@ -230,6 +230,50 @@ describe("AgentSession codemode tool", () => {
|
|||
expect(description("count")).toBe("count tool\n\nCodemode: `tools.count(args)` resolves to `number`.");
|
||||
});
|
||||
|
||||
it("summarizes output fields behind local schema references", async () => {
|
||||
const outputTool = (name: string, outputSchema: Record<string, unknown>): AgentTool => ({
|
||||
name,
|
||||
label: name,
|
||||
description: `${name} tool`,
|
||||
parameters: Type.Object({}),
|
||||
outputSchema: Type.Unsafe(outputSchema),
|
||||
execute: async () => ({ content: [], details: {} }),
|
||||
});
|
||||
const report = outputTool("report", {
|
||||
$ref: "#/$defs/Report",
|
||||
$defs: {
|
||||
Report: {
|
||||
type: "object",
|
||||
properties: { kind: { $ref: "#/$defs/Kind" }, summary: { $ref: "#/$defs/Summary" } },
|
||||
required: ["kind", "summary"],
|
||||
},
|
||||
Kind: { const: "ledger" },
|
||||
Summary: { type: "object", properties: { total: { type: "number" } }, required: ["total"] },
|
||||
},
|
||||
});
|
||||
const result = outputTool("result", {
|
||||
anyOf: [{ $ref: "#/$defs/Ok" }, { $ref: "#/$defs/Failed" }],
|
||||
$defs: {
|
||||
Ok: { type: "object", properties: { value: { type: "string" } }, required: ["value"] },
|
||||
Failed: { type: "object", properties: { error: { type: "string" } }, required: ["error"] },
|
||||
},
|
||||
});
|
||||
const harness = await setup([
|
||||
(pi: ExtensionAPI) => {
|
||||
for (const tool of [report, result]) pi.registerTool(createToolDefinitionFromAgentTool(tool));
|
||||
},
|
||||
]);
|
||||
harness.session.setActiveToolsByName(["report", "result", "codemode"]);
|
||||
const description = (name: string) =>
|
||||
harness.session.agent.state.tools.find((tool) => tool.name === name)?.description ?? "";
|
||||
expect(description("report")).toBe(
|
||||
'report tool\n\nCodemode: `tools.report(args)` resolves to `{ kind: "ledger", summary: { total } }`.',
|
||||
);
|
||||
expect(description("result")).toBe(
|
||||
"result tool\n\nCodemode: `tools.result(args)` resolves to `{ value } | { error }`.",
|
||||
);
|
||||
});
|
||||
|
||||
// #10343
|
||||
it("shows the guidelines of tools that do not fit the inline budget through describeTool()", async () => {
|
||||
const harness = await setup();
|
||||
|
|
|
|||
|
|
@ -195,6 +195,61 @@ describe("Bm25Ranker", () => {
|
|||
});
|
||||
});
|
||||
|
||||
// #11803
|
||||
describe("schema references in the search text", () => {
|
||||
function searchText(parameters: Record<string, unknown>): string {
|
||||
const entry = tool("mcp__ledger__inspect", "Inspect records");
|
||||
return createToolSearchDocument({ ...entry, parameters: Type.Unsafe(parameters) }).text;
|
||||
}
|
||||
|
||||
it("reads names and descriptions behind local references", () => {
|
||||
const text = searchText({
|
||||
type: "object",
|
||||
properties: { options: { $ref: "#/$defs/options" } },
|
||||
$defs: {
|
||||
options: { type: "object", properties: { mode: { type: "string", description: "reconciliation" } } },
|
||||
},
|
||||
});
|
||||
expect(text).toBe("Inspect records options mode reconciliation");
|
||||
});
|
||||
|
||||
it("follows escaped pointer segments", () => {
|
||||
const text = searchText({
|
||||
type: "object",
|
||||
properties: { range: { $ref: "#/$defs/date~1range" } },
|
||||
$defs: { "date/range": { description: "fiscal period" } },
|
||||
});
|
||||
expect(text).toBe("Inspect records range fiscal period");
|
||||
});
|
||||
|
||||
it("reads recursive and shared definitions once", () => {
|
||||
const text = searchText({
|
||||
type: "object",
|
||||
properties: { root: { $ref: "#/$defs/node" }, other: { $ref: "#/$defs/node" }, self: { $ref: "#" } },
|
||||
$defs: {
|
||||
node: {
|
||||
description: "tree node",
|
||||
properties: { children: { type: "array", items: { $ref: "#/$defs/node" } } },
|
||||
},
|
||||
},
|
||||
});
|
||||
expect(text).toBe("Inspect records root tree node children other self");
|
||||
});
|
||||
|
||||
it("leaves out unreferenced definitions and remote or missing references", () => {
|
||||
const text = searchText({
|
||||
type: "object",
|
||||
properties: {
|
||||
remote: { $ref: "https://example.com/schema.json#/$defs/x" },
|
||||
missing: { $ref: "#/$defs/missing" },
|
||||
malformed: { $ref: "#/$defs/%E0%A4%A" },
|
||||
},
|
||||
$defs: { unused: { description: "reconciliation" } },
|
||||
});
|
||||
expect(text).toBe("Inspect records remote missing malformed");
|
||||
});
|
||||
});
|
||||
|
||||
describe("codemode description catalog", () => {
|
||||
const plain = tool("read_notes", "Read notes.");
|
||||
const github = ["a", "b", "c"].map((suffix) => tool(`mcp__github__${suffix}`, `GitHub ${suffix}.`));
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue