diff --git a/src/agents/pi-tools.test.ts b/src/agents/pi-tools.test.ts index 97ac6f017..0c61fc555 100644 --- a/src/agents/pi-tools.test.ts +++ b/src/agents/pi-tools.test.ts @@ -1,71 +1,21 @@ -import fs from "node:fs/promises"; -import os from "node:os"; -import path from "node:path"; -import sharp from "sharp"; import { describe, expect, it } from "vitest"; - import { createClawdisCodingTools } from "./pi-tools.js"; -const PNG_1x1 = - "iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mP8/woAAn8B9FD5fHAAAAAASUVORK5CYII="; - describe("createClawdisCodingTools", () => { - it("sniffs mime from bytes when extension lies", async () => { - const tmpDir = await fs.mkdtemp(path.join(os.tmpdir(), "clawdis-pi-")); - const filePath = path.join(tmpDir, "image.jpg"); // actually PNG bytes - await fs.writeFile(filePath, Buffer.from(PNG_1x1, "base64")); - - const read = createClawdisCodingTools().find((t) => t.name === "read"); - expect(read).toBeTruthy(); - if (!read) throw new Error("read tool missing"); - - const res = await read.execute("toolCallId", { path: filePath }); - const image = res.content.find( - (b): b is { type: "image"; mimeType: string } => - !!b && - typeof b === "object" && - (b as Record).type === "image" && - typeof (b as Record).mimeType === "string", - ); - - expect(image?.mimeType).toBe("image/png"); - }); - - it("downscales oversized images for LLM safety", async () => { - const tmpDir = await fs.mkdtemp(path.join(os.tmpdir(), "clawdis-pi-")); - const filePath = path.join(tmpDir, "oversized.png"); - - const buf = await sharp({ - create: { - width: 2001, - height: 10, - channels: 3, - background: { r: 0, g: 0, b: 0 }, - }, - }) - .png() - .toBuffer(); - await fs.writeFile(filePath, buf); - - const read = createClawdisCodingTools().find((t) => t.name === "read"); - expect(read).toBeTruthy(); - if (!read) throw new Error("read tool missing"); - - const res = await read.execute("toolCallId", { path: filePath }); - const image = res.content.find( - (b): b is { type: "image"; mimeType: string; data: string } => - !!b && - typeof b === "object" && - (b as Record).type === "image" && - typeof (b as Record).mimeType === "string" && - typeof (b as Record).data === "string", - ); - expect(image).toBeTruthy(); - if (!image) throw new Error("image block missing"); - - const decoded = Buffer.from(image.data, "base64"); - const meta = await sharp(decoded).metadata(); - expect(meta.width).toBeLessThanOrEqual(2000); - expect(meta.height).toBeLessThanOrEqual(2000); + it("merges properties for union tool schemas", () => { + const tools = createClawdisCodingTools(); + const browser = tools.find((tool) => tool.name === "clawdis_browser"); + expect(browser).toBeDefined(); + const parameters = browser?.parameters as { + anyOf?: unknown[]; + properties?: Record; + required?: string[]; + }; + expect(parameters.anyOf?.length ?? 0).toBeGreaterThan(0); + expect(parameters.properties?.action).toBeDefined(); + expect(parameters.properties?.controlUrl).toBeDefined(); + expect(parameters.properties?.targetUrl).toBeDefined(); + expect(parameters.properties?.request).toBeDefined(); + expect(parameters.required ?? []).toContain("action"); }); }); diff --git a/src/agents/pi-tools.ts b/src/agents/pi-tools.ts index 5d69a49b7..6f9a494b3 100644 --- a/src/agents/pi-tools.ts +++ b/src/agents/pi-tools.ts @@ -107,12 +107,53 @@ function normalizeToolParameters(tool: AnyAgentTool): AnyAgentTool { if (!schema) return tool; if ("type" in schema && "properties" in schema) return tool; if (!Array.isArray(schema.anyOf)) return tool; + const mergedProperties: Record = {}; + const requiredCounts = new Map(); + let objectVariants = 0; + + for (const entry of schema.anyOf) { + if (!entry || typeof entry !== "object") continue; + const props = (entry as { properties?: unknown }).properties; + if (!props || typeof props !== "object") continue; + objectVariants += 1; + for (const [key, value] of Object.entries( + props as Record, + )) { + if (!(key in mergedProperties)) mergedProperties[key] = value; + } + const required = Array.isArray((entry as { required?: unknown }).required) + ? (entry as { required: unknown[] }).required + : []; + for (const key of required) { + if (typeof key !== "string") continue; + requiredCounts.set(key, (requiredCounts.get(key) ?? 0) + 1); + } + } + + const baseRequired = Array.isArray(schema.required) + ? schema.required.filter((key) => typeof key === "string") + : undefined; + const mergedRequired = + baseRequired && baseRequired.length > 0 + ? baseRequired + : objectVariants > 0 + ? Array.from(requiredCounts.entries()) + .filter(([, count]) => count === objectVariants) + .map(([key]) => key) + : undefined; + return { ...tool, parameters: { ...schema, type: "object", - properties: schema.properties ?? {}, + properties: + Object.keys(mergedProperties).length > 0 + ? mergedProperties + : schema.properties ?? {}, + ...(mergedRequired && mergedRequired.length > 0 + ? { required: mergedRequired } + : {}), additionalProperties: "additionalProperties" in schema ? schema.additionalProperties : true, } as unknown as TSchema,