From 133889a2f63543a69fb6e3a9764c34b7643e90b9 Mon Sep 17 00:00:00 2001 From: Peter Steinberger Date: Sat, 3 Jan 2026 13:52:10 +0100 Subject: [PATCH] fix: scope gemini tool schemas --- src/agents/model-catalog.ts | 4 ++ src/agents/pi-embedded-runner.ts | 2 + src/agents/pi-tools.test.ts | 44 +++++++++++++ src/agents/pi-tools.ts | 108 +++++++++++++++++-------------- 4 files changed, 110 insertions(+), 48 deletions(-) diff --git a/src/agents/model-catalog.ts b/src/agents/model-catalog.ts index e8c6e2e48..ba10cb383 100644 --- a/src/agents/model-catalog.ts +++ b/src/agents/model-catalog.ts @@ -63,6 +63,10 @@ export async function loadModelCatalog(params?: { // If we found nothing, don't cache this result so we can try again. modelCatalogPromise = null; } + if (models.length === 0) { + // If we found nothing, don't cache this result so we can try again. + modelCatalogPromise = null; + } } catch { // Leave models empty on discovery errors and don't cache. modelCatalogPromise = null; diff --git a/src/agents/pi-embedded-runner.ts b/src/agents/pi-embedded-runner.ts index 2af132927..b89ca1834 100644 --- a/src/agents/pi-embedded-runner.ts +++ b/src/agents/pi-embedded-runner.ts @@ -408,6 +408,8 @@ export async function runEmbeddedPiAgent(params: { const promptSkills = resolvePromptSkills(skillsSnapshot, skillEntries); const tools = createClawdisCodingTools({ bash: params.config?.agent?.bash, + provider, + surface: params.surface, }); const machineName = await getMachineDisplayName(); const runtimeInfo = { diff --git a/src/agents/pi-tools.test.ts b/src/agents/pi-tools.test.ts index bc95eb4b1..0dde36e2e 100644 --- a/src/agents/pi-tools.test.ts +++ b/src/agents/pi-tools.test.ts @@ -72,6 +72,50 @@ describe("createClawdisCodingTools", () => { } }); + it("keeps anyOf variants for non-Gemini providers", () => { + const tools = createClawdisCodingTools({ provider: "openai" }); + const browser = tools.find((tool) => tool.name === "browser"); + expect(browser).toBeDefined(); + const parameters = browser?.parameters as { + anyOf?: Array<{ properties?: Record }>; + }; + expect(Array.isArray(parameters.anyOf)).toBe(true); + expect(parameters.anyOf?.length ?? 0).toBeGreaterThan(0); + const hasConst = parameters.anyOf?.some((variant) => { + const action = variant.properties?.action as + | { const?: unknown } + | undefined; + return typeof action?.const === "string"; + }); + expect(hasConst).toBe(true); + }); + + it("strips anyOf for google-gemini-cli tools", () => { + const tools = createClawdisCodingTools({ provider: "google-gemini-cli" }); + const browser = tools.find((tool) => tool.name === "browser"); + expect(browser).toBeDefined(); + const parameters = browser?.parameters as { + anyOf?: unknown; + properties?: Record; + required?: string[]; + }; + expect(parameters.anyOf).toBeUndefined(); + expect(parameters.properties?.action).toBeDefined(); + expect(parameters.required ?? []).toContain("action"); + }); + + it("strips anyOf for google-antigravity tools", () => { + const tools = createClawdisCodingTools({ provider: "google-antigravity" }); + const browser = tools.find((tool) => tool.name === "browser"); + expect(browser).toBeDefined(); + const parameters = browser?.parameters as { + anyOf?: unknown; + properties?: Record; + }; + expect(parameters.anyOf).toBeUndefined(); + expect(parameters.properties?.action).toBeDefined(); + }); + it("includes bash and process tools", () => { const tools = createClawdisCodingTools(); expect(tools.some((tool) => tool.name === "bash")).toBe(true); diff --git a/src/agents/pi-tools.ts b/src/agents/pi-tools.ts index 5ad63a177..f80abc639 100644 --- a/src/agents/pi-tools.ts +++ b/src/agents/pi-tools.ts @@ -141,80 +141,76 @@ function mergePropertySchemas(existing: unknown, incoming: unknown): unknown { return existing; } +// Gemini tool schemas accept an OpenAPI-ish subset; strip unsupported bits. function cleanSchemaForGemini(schema: unknown): unknown { if (!schema || typeof schema !== "object") return schema; if (Array.isArray(schema)) return schema.map(cleanSchemaForGemini); - + const obj = schema as Record; const hasAnyOf = "anyOf" in obj && Array.isArray(obj.anyOf); - const hasConst = "const" in obj; const cleaned: Record = {}; - + for (const [key, value] of Object.entries(obj)) { - // Skip unsupported schema features for Gemini: - // - patternProperties: not in OpenAPI 3.0 subset - // - const: convert to enum with single value instead if (key === "patternProperties") { - // Gemini doesn't support patternProperties - skip it continue; } - - // Convert const to enum (Gemini doesn't support const) + if (key === "const") { cleaned.enum = [value]; continue; } - - // Skip 'type' if we have 'anyOf' — Gemini doesn't allow both + if (key === "type" && hasAnyOf) { continue; } - + if (key === "properties" && value && typeof value === "object") { - // Recursively clean nested properties const props = value as Record; cleaned[key] = Object.fromEntries( - Object.entries(props).map(([k, v]) => [k, cleanSchemaForGemini(v)]) + Object.entries(props).map(([k, v]) => [k, cleanSchemaForGemini(v)]), ); } else if (key === "items" && value && typeof value === "object") { - // Recursively clean array items schema cleaned[key] = cleanSchemaForGemini(value); } else if (key === "anyOf" && Array.isArray(value)) { - // Clean each anyOf variant - cleaned[key] = value.map(v => cleanSchemaForGemini(v)); + cleaned[key] = value.map((v) => cleanSchemaForGemini(v)); } else if (key === "oneOf" && Array.isArray(value)) { - // Clean each oneOf variant - cleaned[key] = value.map(v => cleanSchemaForGemini(v)); + cleaned[key] = value.map((v) => cleanSchemaForGemini(v)); } else if (key === "allOf" && Array.isArray(value)) { - // Clean each allOf variant - cleaned[key] = value.map(v => cleanSchemaForGemini(v)); + cleaned[key] = value.map((v) => cleanSchemaForGemini(v)); } else if (key === "additionalProperties" && value && typeof value === "object") { - // Recursively clean additionalProperties schema cleaned[key] = cleanSchemaForGemini(value); } else { cleaned[key] = value; } } - + return cleaned; } -function normalizeToolParameters(tool: AnyAgentTool): AnyAgentTool { +// Only Gemini providers need schema cleanup; other providers can keep richer JSON Schema. +function shouldCleanSchemaForGemini(provider?: string): boolean { + return provider === "google-gemini-cli" || provider === "google-antigravity"; +} + +function normalizeToolParameters( + tool: AnyAgentTool, + options?: { cleanForGemini?: boolean }, +): AnyAgentTool { const schema = tool.parameters && typeof tool.parameters === "object" ? (tool.parameters as Record) : undefined; if (!schema) return tool; - - // If schema already has type + properties (no top-level anyOf to merge), - // still clean it for Gemini compatibility - if ("type" in schema && "properties" in schema && !Array.isArray(schema.anyOf)) { - return { - ...tool, - parameters: cleanSchemaForGemini(schema), - }; + const cleanForGemini = options?.cleanForGemini === true; + if ( + "type" in schema && + "properties" in schema && + !Array.isArray(schema.anyOf) + ) { + return cleanForGemini + ? { ...tool, parameters: cleanSchemaForGemini(schema) } + : tool; } - if (!Array.isArray(schema.anyOf)) return tool; const mergedProperties: Record = {}; const requiredCounts = new Map(); @@ -258,22 +254,29 @@ function normalizeToolParameters(tool: AnyAgentTool): AnyAgentTool { .map(([key]) => key) : undefined; - const { anyOf: _unusedAnyOf, ...restSchema } = schema; + const mergedSchema = { + ...schema, + type: "object", + properties: + Object.keys(mergedProperties).length > 0 + ? mergedProperties + : (schema.properties ?? {}), + ...(mergedRequired && mergedRequired.length > 0 + ? { required: mergedRequired } + : {}), + additionalProperties: + "additionalProperties" in schema ? schema.additionalProperties : true, + }; + + // Preserve anyOf for non-Gemini providers; Gemini rejects top-level anyOf. return { ...tool, - parameters: cleanSchemaForGemini({ - ...restSchema, - type: "object", - properties: - Object.keys(mergedProperties).length > 0 - ? mergedProperties - : (schema.properties ?? {}), - ...(mergedRequired && mergedRequired.length > 0 - ? { required: mergedRequired } - : {}), - additionalProperties: - "additionalProperties" in schema ? schema.additionalProperties : true, - }), + parameters: cleanForGemini + ? (() => { + const { anyOf: _unusedAnyOf, ...rest } = mergedSchema; + return cleanSchemaForGemini(rest); + })() + : mergedSchema, }; } @@ -364,6 +367,8 @@ function createClawdisReadTool(base: AnyAgentTool): AnyAgentTool { export function createClawdisCodingTools(options?: { bash?: BashToolDefaults & ProcessToolDefaults; + surface?: string; + provider?: string; }): AnyAgentTool[] { const bashToolName = "bash"; const base = (codingTools as unknown as AnyAgentTool[]).flatMap((tool) => { @@ -382,5 +387,12 @@ export function createClawdisCodingTools(options?: { createWhatsAppLoginTool(), ...createClawdisTools(), ]; - return tools.map(normalizeToolParameters); + const allowDiscord = shouldIncludeDiscordTool(options?.surface); + const filtered = allowDiscord + ? tools + : tools.filter((tool) => tool.name !== "discord"); + const cleanForGemini = shouldCleanSchemaForGemini(options?.provider); + return filtered.map((tool) => + normalizeToolParameters(tool, { cleanForGemini }), + ); }