diff --git a/src/agents/pi-embedded-runner/google.ts b/src/agents/pi-embedded-runner/google.ts index 2217224e3cd..0666e3b02fb 100644 --- a/src/agents/pi-embedded-runner/google.ts +++ b/src/agents/pi-embedded-runner/google.ts @@ -1,7 +1,8 @@ -import { EventEmitter } from "node:events"; import type { AgentMessage, AgentTool } from "@mariozechner/pi-agent-core"; import type { SessionManager } from "@mariozechner/pi-coding-agent"; import type { TSchema } from "@sinclair/typebox"; +import { EventEmitter } from "node:events"; +import type { TranscriptPolicy } from "../transcript-policy.js"; import { registerUnhandledRejectionHandler } from "../../infra/unhandled-rejections.js"; import { hasInterSessionUserProvenance, @@ -20,7 +21,6 @@ import { stripToolResultDetails, sanitizeToolUseResultPairing, } from "../session-transcript-repair.js"; -import type { TranscriptPolicy } from "../transcript-policy.js"; import { resolveTranscriptPolicy } from "../transcript-policy.js"; import { log } from "./logger.js"; import { describeUnknownError } from "./utils.js"; @@ -245,7 +245,11 @@ export function sanitizeToolsForGoogle< tools: AgentTool[]; provider: string; }): AgentTool[] { - if (params.provider !== "google-antigravity" && params.provider !== "google-gemini-cli") { + // google-antigravity serves Anthropic models (e.g. claude-opus-4-6-thinking), + // NOT Gemini. Applying Gemini schema cleaning strips JSON Schema keywords + // (minimum, maximum, format, etc.) that Anthropic's API requires for + // draft 2020-12 compliance. Only clean for actual Gemini providers. + if (params.provider !== "google-gemini-cli") { return params.tools; } return params.tools.map((tool) => { diff --git a/src/agents/pi-tools.schema.ts b/src/agents/pi-tools.schema.ts index ca8e64e08c1..41fdefb766e 100644 --- a/src/agents/pi-tools.schema.ts +++ b/src/agents/pi-tools.schema.ts @@ -62,7 +62,10 @@ function mergePropertySchemas(existing: unknown, incoming: unknown): unknown { return existing; } -export function normalizeToolParameters(tool: AnyAgentTool): AnyAgentTool { +export function normalizeToolParameters( + tool: AnyAgentTool, + options?: { modelProvider?: string }, +): AnyAgentTool { const schema = tool.parameters && typeof tool.parameters === "object" ? (tool.parameters as Record) @@ -75,15 +78,23 @@ export function normalizeToolParameters(tool: AnyAgentTool): AnyAgentTool { // - Gemini rejects several JSON Schema keywords, so we scrub those. // - OpenAI rejects function tool schemas unless the *top-level* is `type: "object"`. // (TypeBox root unions compile to `{ anyOf: [...] }` without `type`). + // - Anthropic (google-antigravity) expects full JSON Schema draft 2020-12 compliance. // // Normalize once here so callers can always pass `tools` through unchanged. + const isGeminiProvider = + options?.modelProvider?.toLowerCase().includes("google") || + options?.modelProvider?.toLowerCase().includes("gemini"); + const isAnthropicProvider = + options?.modelProvider?.toLowerCase().includes("anthropic") || + options?.modelProvider?.toLowerCase().includes("google-antigravity"); + // If schema already has type + properties (no top-level anyOf to merge), - // still clean it for Gemini compatibility + // clean it for Gemini compatibility (but only if using Gemini, not Anthropic) if ("type" in schema && "properties" in schema && !Array.isArray(schema.anyOf)) { return { ...tool, - parameters: cleanSchemaForGemini(schema), + parameters: isGeminiProvider && !isAnthropicProvider ? cleanSchemaForGemini(schema) : schema, }; } @@ -95,9 +106,13 @@ export function normalizeToolParameters(tool: AnyAgentTool): AnyAgentTool { !Array.isArray(schema.anyOf) && !Array.isArray(schema.oneOf) ) { + const schemaWithType = { ...schema, type: "object" }; return { ...tool, - parameters: cleanSchemaForGemini({ ...schema, type: "object" }), + parameters: + isGeminiProvider && !isAnthropicProvider + ? cleanSchemaForGemini(schemaWithType) + : schemaWithType, }; } @@ -154,26 +169,34 @@ export function normalizeToolParameters(tool: AnyAgentTool): AnyAgentTool { : undefined; const nextSchema: Record = { ...schema }; + const flattenedSchema = { + type: "object", + ...(typeof nextSchema.title === "string" ? { title: nextSchema.title } : {}), + ...(typeof nextSchema.description === "string" ? { description: nextSchema.description } : {}), + properties: + Object.keys(mergedProperties).length > 0 ? mergedProperties : (schema.properties ?? {}), + ...(mergedRequired && mergedRequired.length > 0 ? { required: mergedRequired } : {}), + additionalProperties: "additionalProperties" in schema ? schema.additionalProperties : true, + }; + return { ...tool, // Flatten union schemas into a single object schema: // - Gemini doesn't allow top-level `type` together with `anyOf`. // - OpenAI rejects schemas without top-level `type: "object"`. + // - Anthropic accepts proper JSON Schema with constraints. // Merging properties preserves useful enums like `action` while keeping schemas portable. - parameters: cleanSchemaForGemini({ - type: "object", - ...(typeof nextSchema.title === "string" ? { title: nextSchema.title } : {}), - ...(typeof nextSchema.description === "string" - ? { description: nextSchema.description } - : {}), - properties: - Object.keys(mergedProperties).length > 0 ? mergedProperties : (schema.properties ?? {}), - ...(mergedRequired && mergedRequired.length > 0 ? { required: mergedRequired } : {}), - additionalProperties: "additionalProperties" in schema ? schema.additionalProperties : true, - }), + parameters: + isGeminiProvider && !isAnthropicProvider + ? cleanSchemaForGemini(flattenedSchema) + : flattenedSchema, }; } +/** + * @deprecated Use normalizeToolParameters with modelProvider instead. + * This function should only be used for Gemini providers. + */ export function cleanToolSchemaForGemini(schema: Record): unknown { return cleanSchemaForGemini(schema); } diff --git a/src/agents/pi-tools.ts b/src/agents/pi-tools.ts index ce8e3c670c3..d7103d6381b 100644 --- a/src/agents/pi-tools.ts +++ b/src/agents/pi-tools.ts @@ -7,6 +7,9 @@ import { } from "@mariozechner/pi-coding-agent"; import type { OpenClawConfig } from "../config/config.js"; import type { ToolLoopDetectionConfig } from "../config/types.tools.js"; +import type { ModelAuthMode } from "./model-auth.js"; +import type { AnyAgentTool } from "./pi-tools.types.js"; +import type { SandboxContext } from "./sandbox.js"; import { logWarn } from "../logger.js"; import { getPluginToolMeta } from "../plugins/tools.js"; import { isSubagentSessionKey } from "../routing/session-key.js"; @@ -20,7 +23,6 @@ import { type ProcessToolDefaults, } from "./bash-tools.js"; import { listChannelAgentTools } from "./channel-tools.js"; -import type { ModelAuthMode } from "./model-auth.js"; import { createOpenClawTools } from "./openclaw-tools.js"; import { wrapToolWithAbortSignal } from "./pi-tools.abort.js"; import { wrapToolWithBeforeToolCallHook } from "./pi-tools.before-tool-call.js"; @@ -43,8 +45,6 @@ import { wrapToolParamNormalization, } from "./pi-tools.read.js"; import { cleanToolSchemaForGemini, normalizeToolParameters } from "./pi-tools.schema.js"; -import type { AnyAgentTool } from "./pi-tools.types.js"; -import type { SandboxContext } from "./sandbox.js"; import { getSubagentDepthFromSessionStore } from "./subagent-depth.js"; import { applyToolPolicyPipeline, @@ -474,7 +474,10 @@ export function createOpenClawCodingTools(options?: { }); // Always normalize tool JSON Schemas before handing them to pi-agent/pi-ai. // Without this, some providers (notably OpenAI) will reject root-level union schemas. - const normalized = subagentFiltered.map(normalizeToolParameters); + // Provider-specific cleaning: Gemini needs constraint keywords stripped, but Anthropic expects them. + const normalized = subagentFiltered.map((tool) => + normalizeToolParameters(tool, { modelProvider: options?.modelProvider }), + ); const withHooks = normalized.map((tool) => wrapToolWithBeforeToolCallHook(tool, { agentId, diff --git a/src/agents/schema/clean-for-gemini.ts b/src/agents/schema/clean-for-gemini.ts index 173480c4546..e18d2e8c18d 100644 --- a/src/agents/schema/clean-for-gemini.ts +++ b/src/agents/schema/clean-for-gemini.ts @@ -207,36 +207,6 @@ function simplifyUnionVariants(params: { obj: Record; variants: return { variants: stripped ? nonNullVariants : variants }; } -function flattenUnionFallback( - obj: Record, - value: unknown, -): Record | undefined { - if (!Array.isArray(value)) { - return undefined; - } - const variants = (value as Record[]).filter((v) => v && typeof v === "object"); - const types = new Set(variants.map((v) => v.type).filter(Boolean)); - if (variants.length === 1) { - const merged: Record = { ...variants[0] }; - copySchemaMeta(obj, merged); - return merged; - } - if (types.size === 1) { - const merged: Record = { type: Array.from(types)[0] }; - copySchemaMeta(obj, merged); - return merged; - } - const first = variants[0]; - if (first?.type) { - const merged: Record = { type: first.type }; - copySchemaMeta(obj, merged); - return merged; - } - const merged: Record = {}; - copySchemaMeta(obj, merged); - return merged; -} - function cleanSchemaForGeminiWithDefs( schema: unknown, defs: SchemaDefs | undefined, @@ -369,20 +339,6 @@ function cleanSchemaForGeminiWithDefs( } } - // Cloud Code Assist API also rejects anyOf/oneOf in nested schemas. - // If simplifyUnionVariants couldn't reduce the union above, flatten it - // here as a fallback: pick the first variant's type or use a permissive - // schema so the tool declaration is accepted. - const flattenedAnyOf = flattenUnionFallback(cleaned, cleaned.anyOf); - if (flattenedAnyOf) { - return flattenedAnyOf; - } - - const flattenedOneOf = flattenUnionFallback(cleaned, cleaned.oneOf); - if (flattenedOneOf) { - return flattenedOneOf; - } - return cleaned; }