diff --git a/src/agents/pi-tools.test.ts b/src/agents/pi-tools.test.ts index 0c61fc555..77463f33d 100644 --- a/src/agents/pi-tools.test.ts +++ b/src/agents/pi-tools.test.ts @@ -18,4 +18,51 @@ describe("createClawdisCodingTools", () => { expect(parameters.properties?.request).toBeDefined(); expect(parameters.required ?? []).toContain("action"); }); + + it("preserves union action values in merged schema", () => { + const tools = createClawdisCodingTools(); + const toolNames = tools + .filter((tool) => tool.name.startsWith("clawdis_")) + .map((tool) => tool.name); + + for (const name of toolNames) { + const tool = tools.find((candidate) => candidate.name === name); + expect(tool).toBeDefined(); + const parameters = tool?.parameters as { + anyOf?: Array<{ properties?: Record }>; + properties?: Record; + }; + const actionValues = new Set(); + for (const variant of parameters.anyOf ?? []) { + const action = variant?.properties?.action as + | { const?: unknown; enum?: unknown[] } + | undefined; + if (typeof action?.const === "string") actionValues.add(action.const); + if (Array.isArray(action?.enum)) { + for (const value of action.enum) { + if (typeof value === "string") actionValues.add(value); + } + } + } + + const mergedAction = parameters.properties?.action as + | { const?: unknown; enum?: unknown[] } + | undefined; + const mergedValues = new Set(); + if (typeof mergedAction?.const === "string") { + mergedValues.add(mergedAction.const); + } + if (Array.isArray(mergedAction?.enum)) { + for (const value of mergedAction.enum) { + if (typeof value === "string") mergedValues.add(value); + } + } + + expect(actionValues.size).toBeGreaterThan(1); + expect(mergedValues.size).toBe(actionValues.size); + for (const value of actionValues) { + expect(mergedValues.has(value)).toBe(true); + } + } + }); }); diff --git a/src/agents/pi-tools.ts b/src/agents/pi-tools.ts index a0aa68539..273641ffb 100644 --- a/src/agents/pi-tools.ts +++ b/src/agents/pi-tools.ts @@ -99,6 +99,41 @@ async function normalizeReadImageResult( type AnyAgentTool = AgentTool; +function extractEnumValues(schema: unknown): unknown[] | undefined { + if (!schema || typeof schema !== "object") return undefined; + const record = schema as Record; + if (Array.isArray(record.enum)) return record.enum; + if ("const" in record) return [record.const]; + return undefined; +} + +function mergePropertySchemas(existing: unknown, incoming: unknown): unknown { + if (!existing) return incoming; + if (!incoming) return existing; + + const existingEnum = extractEnumValues(existing); + const incomingEnum = extractEnumValues(incoming); + if (existingEnum || incomingEnum) { + const values = Array.from( + new Set([...(existingEnum ?? []), ...(incomingEnum ?? [])]), + ); + const merged: Record = {}; + for (const source of [existing, incoming]) { + if (!source || typeof source !== "object") continue; + const record = source as Record; + for (const key of ["title", "description", "default"]) { + if (!(key in merged) && key in record) merged[key] = record[key]; + } + } + const types = new Set(values.map((value) => typeof value)); + if (types.size === 1) merged.type = Array.from(types)[0]; + merged.enum = values; + return merged; + } + + return existing; +} + function normalizeToolParameters(tool: AnyAgentTool): AnyAgentTool { const schema = tool.parameters && typeof tool.parameters === "object" @@ -119,7 +154,14 @@ function normalizeToolParameters(tool: AnyAgentTool): AnyAgentTool { for (const [key, value] of Object.entries( props as Record, )) { - if (!(key in mergedProperties)) mergedProperties[key] = value; + if (!(key in mergedProperties)) { + mergedProperties[key] = value; + continue; + } + mergedProperties[key] = mergePropertySchemas( + mergedProperties[key], + value, + ); } const required = Array.isArray((entry as { required?: unknown }).required) ? (entry as { required: unknown[] }).required