diff --git a/AgentCompany-New/src/main/services/provider-tool-turns/responses-format.ts b/AgentCompany-New/src/main/services/provider-tool-turns/responses-format.ts index 99943382..2d84e61f 100644 --- a/AgentCompany-New/src/main/services/provider-tool-turns/responses-format.ts +++ b/AgentCompany-New/src/main/services/provider-tool-turns/responses-format.ts @@ -7,7 +7,16 @@ import { isRecord, type JsonRecord, type ProviderEventStreamMessage } from "../p import { parseArguments, type AgentTurnMessage, type AgentTurnResult, type ToolCall, type ToolDefinition } from "../provider-tool-turn"; export function responsesToolDefs(tools: ToolDefinition[], strict: boolean | null = false): unknown[] { - return tools.map((tool) => ({ type: "function", name: tool.name, description: tool.description, parameters: tool.parameters, strict })); + // `strict` must be a boolean or omitted. Passing `strict: null` (the Codex + // path) is malformed and makes the endpoint drop the whole tool list, so the + // model has nothing to call. Omit the field instead of sending null. + return tools.map((tool) => ({ + type: "function", + name: tool.name, + description: tool.description, + parameters: tool.parameters, + ...(strict === null ? {} : { strict }), + })); } export function toResponsesInput(messages: AgentTurnMessage[]): unknown[] { diff --git a/AgentCompany-New/tests/unit/provider-tool-turns.test.ts b/AgentCompany-New/tests/unit/provider-tool-turns.test.ts index ce482fbd..723adc5f 100644 --- a/AgentCompany-New/tests/unit/provider-tool-turns.test.ts +++ b/AgentCompany-New/tests/unit/provider-tool-turns.test.ts @@ -4,7 +4,7 @@ import { join } from "node:path"; import { describe, expect, test } from "vitest"; import type { JsonRecord, ProviderEventStreamMessage } from "../../src/main/services/provider-http"; import type { AgentTurnMessage } from "../../src/main/services/provider-tool-turn"; -import { accumulateResponsesStream, parseResponsesOutput, toResponsesInput } from "../../src/main/services/provider-tool-turns/responses-format"; +import { accumulateResponsesStream, parseResponsesOutput, responsesToolDefs, toResponsesInput } from "../../src/main/services/provider-tool-turns/responses-format"; import { parseChatChoice, toChatMessages } from "../../src/main/services/provider-tool-turns/chat-format"; import { parseAnthropicContent, toAnthropicMessages } from "../../src/main/services/provider-tool-turns/anthropic-format"; import { accumulateGeminiStream, antigravityRequestBody } from "../../src/main/services/provider-tool-turns/gemini-format"; @@ -209,3 +209,23 @@ describe("credential resolver", () => { })(); }); }); + +describe("tool definitions and forced tool choice", () => { + const tools = [{ name: "hire_agent", description: "Add a report.", parameters: { type: "object", properties: {} } }]; + + test("Responses tool defs omit strict when null, keep it when boolean", () => { + const codex = responsesToolDefs(tools, null)[0] as Record; + expect("strict" in codex).toBe(false); // null would make the endpoint drop the tools + expect(responsesToolDefs(tools)[0]).toMatchObject({ type: "function", name: "hire_agent", strict: false }); + expect(responsesToolDefs(tools, true)[0]).toMatchObject({ strict: true }); + }); + + test("Bedrock and Gemini encode a forced tool choice", () => { + expect(bedrockToolConfig(tools, "required").toolChoice).toEqual({ any: {} }); + expect(bedrockToolConfig(tools).toolChoice).toEqual({ auto: {} }); + const vertex = vertexRequestBody({ model: "m", instructions: null, messages: [], tools, toolChoice: "required" }); + expect(vertex.toolConfig).toEqual({ functionCallingConfig: { mode: "ANY" } }); + const anti = antigravityRequestBody({ model: "m", projectId: "p", instructions: null, messages: [], tools, requestId: "r" }); + expect((anti.request as Record).toolConfig).toEqual({ functionCallingConfig: { mode: "AUTO" } }); + }); +});