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
|
|
@ -2,6 +2,14 @@
|
|||
|
||||
## [Unreleased]
|
||||
|
||||
### Added
|
||||
|
||||
- Added `resolveSchemaRef(ref, root)`, which returns the subschema a local `$ref` (`#`, `#/$defs/Item`) names in a schema, with URI-decoded and JSON Pointer unescaped segments ([#11803](https://github.com/earendil-works/pi/issues/11803))
|
||||
|
||||
### Fixed
|
||||
|
||||
- Fixed `schemaToType()`, `renderDeclarations()`, `renderToolSignature()`, and `renderToolSample()` throwing `URIError` for a local `$ref` with a malformed percent escape (`#/$defs/%E0`), so one such tool schema failed every caller that renders all tools. The reference now renders as `unknown`, like a reference that names nothing ([#11803](https://github.com/earendil-works/pi/issues/11803))
|
||||
|
||||
## [1.5.0] - 2026-11-02
|
||||
|
||||
### Changed
|
||||
|
|
|
|||
|
|
@ -153,11 +153,22 @@ interface SchemaContext {
|
|||
tags: boolean;
|
||||
}
|
||||
|
||||
function resolveRef(ref: string, root: CodemodeJsonSchema): CodemodeJsonSchema | undefined {
|
||||
/**
|
||||
* The subschema a local `$ref` (`#`, `#/$defs/Item`) names in `root`, or undefined for remote references and
|
||||
* references that name nothing. Pointer segments are URI-decoded and JSON Pointer unescaped (`~1` is `/`, `~0` is `~`).
|
||||
*/
|
||||
export function resolveSchemaRef(ref: string, root: unknown): CodemodeJsonSchema | undefined {
|
||||
if (ref !== "#" && !ref.startsWith("#/")) return undefined;
|
||||
let current: unknown = root;
|
||||
for (const segment of ref.slice(2).split("/").filter(Boolean)) {
|
||||
const key = decodeURIComponent(segment).replaceAll("~1", "/").replaceAll("~0", "~");
|
||||
let decoded: string;
|
||||
try {
|
||||
decoded = decodeURIComponent(segment);
|
||||
} catch {
|
||||
// Schemas come from tools and MCP servers: a malformed escape names nothing instead of failing the caller.
|
||||
return undefined;
|
||||
}
|
||||
const key = decoded.replaceAll("~1", "/").replaceAll("~0", "~");
|
||||
if (typeof current !== "object" || current === null || Array.isArray(current) || !(key in current)) {
|
||||
return undefined;
|
||||
}
|
||||
|
|
@ -176,7 +187,7 @@ function toType(schema: CodemodeJsonSchema, context: SchemaContext): string {
|
|||
if (typeof schema.$ref === "string") {
|
||||
const ref = schema.$ref;
|
||||
if (context.resolving.has(ref) || context.expansions >= MAX_REF_EXPANSIONS) return "unknown";
|
||||
const target = resolveRef(ref, context.root);
|
||||
const target = resolveSchemaRef(ref, context.root);
|
||||
if (target === undefined) return "unknown";
|
||||
context.expansions++;
|
||||
context.resolving.add(ref);
|
||||
|
|
|
|||
|
|
@ -5,6 +5,7 @@ export {
|
|||
renderToolOutputType,
|
||||
renderToolSample,
|
||||
renderToolSignature,
|
||||
resolveSchemaRef,
|
||||
schemaToType,
|
||||
} from "./declarations.ts";
|
||||
export { toCodemodeIdentifier } from "./identifier.ts";
|
||||
|
|
|
|||
|
|
@ -1,5 +1,11 @@
|
|||
import { describe, expect, it } from "vitest";
|
||||
import { renderDeclarations, renderToolSample, renderToolSignature, schemaToType } from "../src/index.ts";
|
||||
import {
|
||||
renderDeclarations,
|
||||
renderToolSample,
|
||||
renderToolSignature,
|
||||
resolveSchemaRef,
|
||||
schemaToType,
|
||||
} from "../src/index.ts";
|
||||
|
||||
const execute = () => undefined;
|
||||
|
||||
|
|
@ -222,6 +228,25 @@ describe("schemaToType", () => {
|
|||
});
|
||||
});
|
||||
|
||||
describe("resolveSchemaRef", () => {
|
||||
const item = { type: "string" };
|
||||
const root = { $defs: { item, "a/b": item, "c~d": item, "e f": item } };
|
||||
|
||||
it("resolves local references with escaped segments", () => {
|
||||
expect(resolveSchemaRef("#", root)).toBe(root);
|
||||
expect(resolveSchemaRef("#/$defs/item", root)).toBe(item);
|
||||
expect(resolveSchemaRef("#/$defs/a~1b", root)).toBe(item);
|
||||
expect(resolveSchemaRef("#/$defs/c~0d", root)).toBe(item);
|
||||
expect(resolveSchemaRef("#/$defs/e%20f", root)).toBe(item);
|
||||
});
|
||||
|
||||
it("returns undefined for remote, missing, and malformed references", () => {
|
||||
expect(resolveSchemaRef("other.json#/$defs/item", root)).toBeUndefined();
|
||||
expect(resolveSchemaRef("#/$defs/missing", root)).toBeUndefined();
|
||||
expect(resolveSchemaRef("#/$defs/%E0%A4%A", root)).toBeUndefined();
|
||||
});
|
||||
});
|
||||
|
||||
describe("tool declarations", () => {
|
||||
it("renders signatures with normalized identifiers", () => {
|
||||
expect(
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue