mirror of
https://github.com/morluto/rea
synced 2026-10-26 06:16:17 -04:00
perf(mcp): share repeated output schema definitions
This commit is contained in:
@@ -31,6 +31,10 @@ check the complete catalog after SDK conversion and its generated counterpart.
|
||||
This is REA's local compatibility profile; individual model APIs can impose
|
||||
additional limits.
|
||||
|
||||
Output schemas share repeated definitions through local references while
|
||||
preserving their complete fields and validation rules. Canonical Zod validation
|
||||
and the input compatibility profile continue to apply.
|
||||
|
||||
For passive `compare_web_captures` inputs, pass each complete
|
||||
`inspect_web_page` result in `before.inspection` or `after.inspection`, with an
|
||||
optional complete `discover_webmcp_tools` result in the matching `webmcp` field.
|
||||
|
||||
@@ -0,0 +1,177 @@
|
||||
import { Ajv } from "ajv";
|
||||
import { Ajv2020 } from "ajv/dist/2020.js";
|
||||
import { expect, it } from "vitest";
|
||||
import { z } from "zod";
|
||||
|
||||
import { inlineLocalJsonSchemaReferences } from "../../tests/fixtures/localJsonSchemaReferences.js";
|
||||
import { toolContract } from "./toolContracts.js";
|
||||
import { toolOutputSchemaWithMetadata } from "./toolSchemaMetadata.js";
|
||||
|
||||
it.each(["draft-2020-12", "draft-07"] as const)(
|
||||
"preserves refined numeric bounds and strict repeated objects through %s references",
|
||||
(target) => {
|
||||
const integer = z.number().int().nonnegative();
|
||||
const range = z
|
||||
.object({
|
||||
offset: integer.max(65535),
|
||||
length: integer.min(1),
|
||||
exclusive: integer.positive(),
|
||||
})
|
||||
.strict();
|
||||
const canonical = z.object({ before: range, after: range }).strict();
|
||||
const advertised = toolOutputSchemaWithMetadata({
|
||||
...toolContract("open_binary"),
|
||||
outputSchema: canonical,
|
||||
})["~standard"].jsonSchema.output({ target });
|
||||
const ajv =
|
||||
target === "draft-2020-12"
|
||||
? new Ajv2020({ strict: false })
|
||||
: new Ajv({ strict: false });
|
||||
const validate = ajv.compile(advertised);
|
||||
const valid = {
|
||||
before: { offset: 0, length: 1, exclusive: 1 },
|
||||
after: { offset: 65535, length: 9007199254740991, exclusive: 1 },
|
||||
};
|
||||
const samples: readonly unknown[] = [
|
||||
valid,
|
||||
...[
|
||||
{ offset: -1 },
|
||||
{ offset: 65536 },
|
||||
{ offset: 0.5 },
|
||||
{ length: 0 },
|
||||
{ length: 9007199254740992 },
|
||||
{ exclusive: 0 },
|
||||
{ exclusive: -1 },
|
||||
{ length: null },
|
||||
{ unexpected: true },
|
||||
].map((change) => ({ ...valid, before: { ...valid.before, ...change } })),
|
||||
{ ...valid, before: { offset: 0, exclusive: 1 } },
|
||||
{ ...valid, after: null },
|
||||
{ ...valid, unexpected: true },
|
||||
];
|
||||
expect(canonical.safeParse(valid).success).toBe(true);
|
||||
for (const sample of samples)
|
||||
expect(validate(sample)).toBe(canonical.safeParse(sample).success);
|
||||
expect(inlineLocalJsonSchemaReferences(advertised)).toEqual(
|
||||
inlineLocalJsonSchemaReferences(
|
||||
canonical["~standard"].jsonSchema.output({ target }),
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
it("keeps schema data containing reference-shaped objects untouched", () => {
|
||||
const literal = { $ref: "https://example.invalid/value" };
|
||||
const expanded = inlineLocalJsonSchemaReferences({
|
||||
$defs: {
|
||||
"a/b": {
|
||||
type: "object",
|
||||
properties: { $defs: { const: literal } },
|
||||
examples: [literal],
|
||||
},
|
||||
},
|
||||
$ref: "#/$defs/a~1b",
|
||||
description: "Keep the caller-facing annotation",
|
||||
});
|
||||
expect(expanded).toEqual({
|
||||
type: "object",
|
||||
properties: { $defs: { const: literal } },
|
||||
examples: [literal],
|
||||
description: "Keep the caller-facing annotation",
|
||||
});
|
||||
});
|
||||
|
||||
it.each(["draft-2020-12", "draft-07"] as const)(
|
||||
"preserves layered native-selector string patterns through %s references",
|
||||
(target) => {
|
||||
const selector = z
|
||||
.string()
|
||||
.min(1)
|
||||
.regex(/^[^\0]*$/u)
|
||||
.regex(/^[^\s[\]]+$/u);
|
||||
const canonical = z.object({ before: selector, after: selector }).strict();
|
||||
const advertised = toolOutputSchemaWithMetadata({
|
||||
...toolContract("open_binary"),
|
||||
outputSchema: canonical,
|
||||
})["~standard"].jsonSchema.output({ target });
|
||||
const ajv =
|
||||
target === "draft-2020-12"
|
||||
? new Ajv2020({ strict: false })
|
||||
: new Ajv({ strict: false });
|
||||
const validate = ajv.compile(advertised);
|
||||
for (const value of [
|
||||
"setFoo:",
|
||||
"dataTaskWithRequest:completionHandler:",
|
||||
"",
|
||||
"set\0Foo:",
|
||||
"bad selector:",
|
||||
"bad[selector]:",
|
||||
]) {
|
||||
const sample = { before: value, after: "setFoo:" };
|
||||
expect(validate(sample)).toBe(canonical.safeParse(sample).success);
|
||||
}
|
||||
expect(inlineLocalJsonSchemaReferences(advertised)).toEqual(
|
||||
inlineLocalJsonSchemaReferences(
|
||||
canonical["~standard"].jsonSchema.output({ target }),
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
it("rejects unresolved and cyclic references rather than concealing schema drift", () => {
|
||||
expect(() =>
|
||||
inlineLocalJsonSchemaReferences({ $ref: "#/$defs/missing" }),
|
||||
).toThrow("Missing schema reference");
|
||||
expect(() =>
|
||||
inlineLocalJsonSchemaReferences({
|
||||
$defs: { loop: { $ref: "#/$defs/loop" } },
|
||||
$ref: "#/$defs/loop",
|
||||
}),
|
||||
).toThrow("Recursive schema reference");
|
||||
});
|
||||
|
||||
it("intersects referenced numeric bounds and rejects conflicting validation keywords", () => {
|
||||
expect(
|
||||
inlineLocalJsonSchemaReferences({
|
||||
$defs: { bounded: { type: "number", minimum: 1, maximum: 10 } },
|
||||
$ref: "#/$defs/bounded",
|
||||
minimum: 0,
|
||||
maximum: 9,
|
||||
}),
|
||||
).toEqual({ type: "number", minimum: 1, maximum: 9 });
|
||||
expect(() =>
|
||||
inlineLocalJsonSchemaReferences({
|
||||
$defs: { strict: { type: "object" } },
|
||||
$ref: "#/$defs/strict",
|
||||
type: "string",
|
||||
}),
|
||||
).toThrow("Conflicting schema reference sibling: type");
|
||||
});
|
||||
|
||||
it("preserves strict-object evaluation scope across references and singleton intersections", () => {
|
||||
const strict = {
|
||||
type: "object",
|
||||
properties: { value: { type: "string" } },
|
||||
additionalProperties: false,
|
||||
};
|
||||
const schemas = [
|
||||
{
|
||||
$defs: { strict },
|
||||
$ref: "#/$defs/strict",
|
||||
additionalProperties: false,
|
||||
},
|
||||
{ allOf: [strict], additionalProperties: false },
|
||||
];
|
||||
for (const schema of schemas) {
|
||||
const expanded = z
|
||||
.record(z.string(), z.unknown())
|
||||
.parse(inlineLocalJsonSchemaReferences(schema));
|
||||
const ajv = new Ajv2020({ strict: false });
|
||||
const original = ajv.compile(schema);
|
||||
const normalized = ajv.compile(expanded);
|
||||
expect(original({})).toBe(true);
|
||||
expect(original({ value: "kept" })).toBe(false);
|
||||
for (const sample of [{}, { value: "kept" }, { unexpected: true }, null])
|
||||
expect(normalized(sample)).toBe(original(sample));
|
||||
}
|
||||
});
|
||||
@@ -1,5 +1,6 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
|
||||
import { inlineLocalJsonSchemaReferences } from "../../tests/fixtures/localJsonSchemaReferences.js";
|
||||
import { TOOL_CONTRACTS, toolContract } from "./toolContracts.js";
|
||||
import {
|
||||
toolInputSchemaWithMetadata,
|
||||
@@ -19,8 +20,13 @@ describe("advertised tool JSON Schema", () => {
|
||||
expect(advertisedInput.examples, contract.name).toEqual(
|
||||
contract.examples.map(({ input: example }) => example),
|
||||
);
|
||||
expect(advertisedOutput, contract.name).toEqual(
|
||||
contract.outputSchema["~standard"].jsonSchema.output({ target }),
|
||||
expect(
|
||||
inlineLocalJsonSchemaReferences(advertisedOutput),
|
||||
contract.name,
|
||||
).toEqual(
|
||||
inlineLocalJsonSchemaReferences(
|
||||
contract.outputSchema["~standard"].jsonSchema.output({ target }),
|
||||
),
|
||||
);
|
||||
}
|
||||
});
|
||||
@@ -34,4 +40,46 @@ describe("advertised tool JSON Schema", () => {
|
||||
expect(latest.$schema).toBe("https://json-schema.org/draft/2020-12/schema");
|
||||
expect(draft07.$schema).toBe("http://json-schema.org/draft-07/schema#");
|
||||
});
|
||||
|
||||
it("preserves complete output meaning across JSON Schema dialects", () => {
|
||||
for (const dialect of ["draft-2020-12", "draft-07"] as const)
|
||||
for (const contract of TOOL_CONTRACTS) {
|
||||
const advertised = toolOutputSchemaWithMetadata(contract)[
|
||||
"~standard"
|
||||
].jsonSchema.output({ target: dialect });
|
||||
const canonical = contract.outputSchema["~standard"].jsonSchema.output({
|
||||
target: dialect,
|
||||
});
|
||||
expect(
|
||||
inlineLocalJsonSchemaReferences(advertised),
|
||||
contract.name,
|
||||
).toEqual(inlineLocalJsonSchemaReferences(canonical));
|
||||
}
|
||||
});
|
||||
|
||||
it("retains memoization and explicit caller projection options", () => {
|
||||
const contract = toolContract("analyze_javascript_application");
|
||||
const output =
|
||||
toolOutputSchemaWithMetadata(contract)["~standard"].jsonSchema;
|
||||
const shared = output.output({ target });
|
||||
expect(shared.$defs).toBeDefined();
|
||||
expect(output.output({ target })).toBe(shared);
|
||||
const options = { target, libraryOptions: { reused: "inline" } };
|
||||
const inline = output.output(options);
|
||||
expect(inline).toEqual(
|
||||
contract.outputSchema["~standard"].jsonSchema.output(options),
|
||||
);
|
||||
expect(inline.$defs).toBeUndefined();
|
||||
expect(output.output({ target })).toBe(shared);
|
||||
const draft07 = output.output({ target: "draft-07" });
|
||||
expect(draft07.$schema).toBe("http://json-schema.org/draft-07/schema#");
|
||||
expect(inlineLocalJsonSchemaReferences(draft07)).toEqual(
|
||||
inlineLocalJsonSchemaReferences(
|
||||
contract.outputSchema["~standard"].jsonSchema.output({
|
||||
target: "draft-07",
|
||||
}),
|
||||
),
|
||||
);
|
||||
expect(output.output({ target: "draft-07" })).toBe(draft07);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -63,11 +63,19 @@ export const toolInputSchemaWithMetadata = <Contract extends ToolContract>(
|
||||
}),
|
||||
);
|
||||
|
||||
/** Advertise a canonical output schema without reconverting it per listing. */
|
||||
/** Share repeated output definitions without changing canonical validation. */
|
||||
export const toolOutputSchemaWithMetadata = <Contract extends ToolContract>(
|
||||
contract: Contract,
|
||||
): Contract["outputSchema"] =>
|
||||
withAdvertisedJsonSchema(contract.outputSchema, "output");
|
||||
withAdvertisedJsonSchema(
|
||||
contract.outputSchema,
|
||||
"output",
|
||||
(project) => (options) =>
|
||||
project({
|
||||
...options,
|
||||
libraryOptions: { reused: "ref", ...options.libraryOptions },
|
||||
}),
|
||||
);
|
||||
|
||||
const withAdvertisedJsonSchema = <Schema extends z.ZodType>(
|
||||
canonical: Schema,
|
||||
|
||||
@@ -17,6 +17,7 @@ import {
|
||||
import { nativeUiResultSchema } from "../../../src/domain/native/nativeUiObservation.js";
|
||||
import { processCaptureSchema } from "../../../src/domain/process/processCapture.js";
|
||||
import { toolRegistrationOptions } from "../../../src/server/toolRegistrationOptions.js";
|
||||
import { inlineLocalJsonSchemaReferences } from "../../fixtures/localJsonSchemaReferences.js";
|
||||
|
||||
const record = (value: unknown): Record<string, unknown> => {
|
||||
if (typeof value !== "object" || value === null || Array.isArray(value))
|
||||
@@ -66,7 +67,7 @@ it("advertises semantic output invariants and keeps producers aligned with Zod",
|
||||
: { $defs: outputSchema.$defs }),
|
||||
});
|
||||
return {
|
||||
result,
|
||||
result: record(inlineLocalJsonSchemaReferences(standaloneResultSchema)),
|
||||
validate: new Ajv2020({
|
||||
strict: false,
|
||||
validateFormats: false,
|
||||
|
||||
+170
@@ -0,0 +1,170 @@
|
||||
const schemaMaps = new Set([
|
||||
"properties",
|
||||
"patternProperties",
|
||||
"dependentSchemas",
|
||||
]);
|
||||
const schemaArrays = new Set(["allOf", "anyOf", "oneOf", "prefixItems"]);
|
||||
const schemaChildren = new Set([
|
||||
"items",
|
||||
"additionalItems",
|
||||
"contains",
|
||||
"additionalProperties",
|
||||
"unevaluatedProperties",
|
||||
"unevaluatedItems",
|
||||
"propertyNames",
|
||||
"not",
|
||||
"if",
|
||||
"then",
|
||||
"else",
|
||||
]);
|
||||
const lowerBounds = new Set(["minimum", "exclusiveMinimum"]);
|
||||
const upperBounds = new Set(["maximum", "exclusiveMaximum"]);
|
||||
const scopedKeywords = new Set([
|
||||
"properties",
|
||||
"patternProperties",
|
||||
"additionalProperties",
|
||||
"items",
|
||||
"additionalItems",
|
||||
"prefixItems",
|
||||
"contains",
|
||||
"unevaluatedProperties",
|
||||
"unevaluatedItems",
|
||||
]);
|
||||
|
||||
const record = (value: unknown): value is Record<string, unknown> =>
|
||||
typeof value === "object" && value !== null && !Array.isArray(value);
|
||||
|
||||
/** Inline finite local references for representation-independent schema assertions. */
|
||||
export const inlineLocalJsonSchemaReferences = (
|
||||
root: Readonly<Record<string, unknown>>,
|
||||
): unknown => {
|
||||
const resolve = (reference: string): Record<string, unknown> => {
|
||||
if (!reference.startsWith("#/"))
|
||||
throw new Error(`Expected a local schema reference: ${reference}`);
|
||||
let current: unknown = root;
|
||||
for (const encoded of reference.slice(2).split("/")) {
|
||||
const key = encoded.replaceAll("~1", "/").replaceAll("~0", "~");
|
||||
if (!record(current) || !Object.hasOwn(current, key))
|
||||
throw new Error(`Missing schema reference: ${reference}`);
|
||||
current = current[key];
|
||||
}
|
||||
if (!record(current))
|
||||
throw new Error(`Expected a referenced schema object: ${reference}`);
|
||||
return current;
|
||||
};
|
||||
|
||||
const visit = (value: unknown, active: ReadonlySet<string>): unknown => {
|
||||
if (!record(value)) return value;
|
||||
if (typeof value.$ref === "string") {
|
||||
const { $ref: reference, ...siblings } = value;
|
||||
if (active.has(reference))
|
||||
throw new Error(`Recursive schema reference: ${reference}`);
|
||||
const referenced = resolve(reference);
|
||||
// Keep declarations in their original evaluation scope.
|
||||
if (Object.keys(siblings).some((key) => scopedKeywords.has(key)))
|
||||
return {
|
||||
allOf: [
|
||||
visit(referenced, new Set([...active, reference])),
|
||||
visit(siblings, active),
|
||||
],
|
||||
};
|
||||
const merged = { ...referenced, ...siblings };
|
||||
for (const [key, entry] of Object.entries(siblings)) {
|
||||
const previous = referenced[key];
|
||||
if (
|
||||
previous === undefined ||
|
||||
JSON.stringify(previous) === JSON.stringify(entry)
|
||||
)
|
||||
continue;
|
||||
if (typeof previous === "number" && typeof entry === "number") {
|
||||
if (lowerBounds.has(key)) {
|
||||
merged[key] = Math.max(previous, entry);
|
||||
continue;
|
||||
}
|
||||
if (upperBounds.has(key)) {
|
||||
merged[key] = Math.min(previous, entry);
|
||||
continue;
|
||||
}
|
||||
}
|
||||
throw new Error(`Conflicting schema reference sibling: ${key}`);
|
||||
}
|
||||
return visit(merged, new Set([...active, reference]));
|
||||
}
|
||||
if (Array.isArray(value.allOf) && value.allOf.length === 1) {
|
||||
const child = visit(value.allOf[0], active);
|
||||
const { allOf: _allOf, ...siblings } = value;
|
||||
if (
|
||||
record(child) &&
|
||||
!Object.keys(siblings).some((key) => scopedKeywords.has(key)) &&
|
||||
Object.entries(siblings).every(
|
||||
([key, entry]) =>
|
||||
child[key] === undefined ||
|
||||
JSON.stringify(child[key]) === JSON.stringify(entry) ||
|
||||
(typeof child[key] === "number" &&
|
||||
typeof entry === "number" &&
|
||||
(lowerBounds.has(key) || upperBounds.has(key))),
|
||||
)
|
||||
) {
|
||||
const merged = { ...child, ...siblings };
|
||||
for (const [key, entry] of Object.entries(siblings)) {
|
||||
const previous = child[key];
|
||||
if (typeof previous !== "number" || typeof entry !== "number")
|
||||
continue;
|
||||
if (lowerBounds.has(key)) merged[key] = Math.max(previous, entry);
|
||||
else if (upperBounds.has(key))
|
||||
merged[key] = Math.min(previous, entry);
|
||||
}
|
||||
return visit(merged, active);
|
||||
}
|
||||
}
|
||||
const normalized = { ...value };
|
||||
if (
|
||||
typeof normalized.pattern === "string" &&
|
||||
Array.isArray(normalized.allOf) &&
|
||||
normalized.allOf.some(
|
||||
(child: unknown) =>
|
||||
record(child) && child.pattern === normalized.pattern,
|
||||
)
|
||||
)
|
||||
delete normalized.pattern;
|
||||
if (
|
||||
typeof normalized.minimum === "number" &&
|
||||
typeof normalized.exclusiveMinimum === "number" &&
|
||||
normalized.exclusiveMinimum >= normalized.minimum
|
||||
)
|
||||
delete normalized.minimum;
|
||||
if (
|
||||
typeof normalized.maximum === "number" &&
|
||||
typeof normalized.exclusiveMaximum === "number" &&
|
||||
normalized.exclusiveMaximum <= normalized.maximum
|
||||
)
|
||||
delete normalized.maximum;
|
||||
return Object.fromEntries(
|
||||
Object.entries(normalized)
|
||||
.filter(([key]) => key !== "$defs" && key !== "definitions")
|
||||
.map(([key, child]) => {
|
||||
if (schemaMaps.has(key) && record(child))
|
||||
return [
|
||||
key,
|
||||
Object.fromEntries(
|
||||
Object.entries(child).map(([name, schema]) => [
|
||||
name,
|
||||
visit(schema, active),
|
||||
]),
|
||||
),
|
||||
];
|
||||
if (schemaArrays.has(key) && Array.isArray(child))
|
||||
return [key, child.map((schema) => visit(schema, active))];
|
||||
if (schemaChildren.has(key))
|
||||
return [
|
||||
key,
|
||||
Array.isArray(child)
|
||||
? child.map((schema) => visit(schema, active))
|
||||
: visit(child, active),
|
||||
];
|
||||
return [key, child];
|
||||
}),
|
||||
);
|
||||
};
|
||||
return visit(root, new Set());
|
||||
};
|
||||
Reference in New Issue
Block a user