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:
Petr Houška 2026-11-06 00:19:58 +01:00 • committed by GitHub
parent 850ea2eae2
commit 6d3e342b25
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
12 changed files with 262 additions and 29 deletions

View file

@ -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();