Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 5 additions & 2 deletions frontend/plugins/zodJitless.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -14,10 +14,13 @@ import fs from 'node:fs/promises'

export const ZOD_CORE_FILE = /[\\/]zod[\\/]v4[\\/]core[\\/]core\.js$/

const DEFAULT_CONFIG = 'export const globalConfig = {};'
const DEFAULT_CONFIG = 'export const globalConfig = globalThis.__zod_globalConfig;'

export function withJitlessZod(source, filePath) {
const contents = source.replace(DEFAULT_CONFIG, 'export const globalConfig = { jitless: true };')
const contents = source.replace(
DEFAULT_CONFIG,
'globalThis.__zod_globalConfig.jitless = true;\n' + DEFAULT_CONFIG
)
if (contents === source) {
// zod changed its config declaration upstream. Fail the build rather than ship the JIT compiler.
throw new Error(`zod-jitless: no '${DEFAULT_CONFIG}' found in ${filePath}`)
Expand Down
38 changes: 28 additions & 10 deletions packages/agent/packages/harness/src/extensions/mcp/schema.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -108,16 +108,34 @@ describe("convertJsonSchemaToTypebox", () => {
expect(result.anyOf?.[1]).toMatchObject({ type: "null" });
});

it("validates nullable arrays", () => {
const schema = convertJsonSchemaToTypebox({
type: ["array", "null"],
items: { type: "string" },
});
const check = Compile(schema);

expect(check.Check(null)).toBe(true);
expect(check.Check(["value"])).toBe(true);
expect(check.Check([1])).toBe(false);
it.each([
[
"nullable arrays",
{ type: ["array", "null"], items: { type: "string" } },
[null, ["value"]],
[[1]],
],
[
"every listed type",
{ type: ["number", "string"] },
[42, "AaVQ8Ijw"],
[null, true],
],
[
"nullable multi-type",
{ type: ["string", "number", "null"] },
["a", 1, null],
[false],
],
])("validates type arrays: %s", (_label, input, accepted, rejected) => {
const check = Compile(convertJsonSchemaToTypebox(input));

for (const value of accepted) {
expect(check.Check(value)).toBe(true);
}
for (const value of rejected) {
expect(check.Check(value)).toBe(false);
}
});

it.each([["oneOf"], ["anyOf"]])("converts %s to a union", (key) => {
Expand Down
27 changes: 19 additions & 8 deletions packages/agent/packages/harness/src/extensions/mcp/schema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -81,15 +81,26 @@ export function convertJsonSchemaToTypebox(

// Nullable types: { "type": ["string", "null"] }
const rawType = s.type;
const type = Array.isArray(rawType)
? (rawType.find((t) => t !== "null") as string | undefined)
: typeof rawType === "string"
? rawType
: undefined;
const isNullable = Array.isArray(rawType) && rawType.includes("null");
if (Array.isArray(rawType) && rawType.length > 0) {
const members = rawType.map((t) =>
convertByType(
t as string,
s,
t === "null" ? {} : opts,
depth,
resolvedDefs,
),
);
return members.length === 1 ? (members[0] as TSchema) : Type.Union(members);
}

const base = convertByType(type, s, opts, depth, resolvedDefs);
return isNullable ? Type.Union([base, Type.Null()]) : base;
return convertByType(
typeof rawType === "string" ? rawType : undefined,
s,
opts,
depth,
resolvedDefs,
);
}

function convertRef(
Expand Down
Loading
Loading