From 97242ae6842bcccb0da1df2de9c4d038f413316a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E9=9B=B7=E7=94=B5=E8=8A=BD=E8=A1=A3?= Date: Wed, 10 Jun 2026 23:17:42 -0400 Subject: [PATCH] =?UTF-8?q?Revert=20tool-choice=20forcing=20=E2=80=94=20th?= =?UTF-8?q?e=20Codex=20regression=20was=20strict:null?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The GPT-account (Codex) path supports tool calls and worked before; my forced tool_choice broke it and was the wrong fix. The real regression was strict:null in the rebuild's tool definitions, which made the endpoint drop the tool list so the model had nothing to call — a "Done" run that hired no one. Forcing then turned that silent failure into a red one. Restore the original "auto" loop (no forced tool_choice, no finish tool) and keep only the one-line strict:null omission. Codex now receives valid tools on the working "auto" choice, as in the version that worked. Co-Authored-By: Claude Opus 4.8 (1M context) --- .../src/main/services/agent-run-loop.ts | 39 +++++------- .../src/main/services/agent-tools.ts | 8 --- .../main/services/provider-response-runner.ts | 5 +- .../src/main/services/provider-tool-turn.ts | 6 -- .../provider-tool-turns/bedrock-format.ts | 6 +- .../provider-tool-turns/gemini-format.ts | 8 +-- .../services/provider-tool-turns/turns.ts | 20 +++--- .../src/main/services/task-prompts.ts | 5 +- .../tests/integration/agent-reply.test.ts | 6 +- .../tests/integration/autonomous-run.test.ts | 20 +++--- .../tests/unit/agent-run-loop.test.ts | 61 ++----------------- .../tests/unit/provider-tool-turns.test.ts | 15 +---- .../tests/unit/task-execution-service.test.ts | 18 ++---- 13 files changed, 57 insertions(+), 160 deletions(-) diff --git a/AgentCompany-New/src/main/services/agent-run-loop.ts b/AgentCompany-New/src/main/services/agent-run-loop.ts index cd961362..953ec254 100644 --- a/AgentCompany-New/src/main/services/agent-run-loop.ts +++ b/AgentCompany-New/src/main/services/agent-run-loop.ts @@ -1,5 +1,5 @@ import type { ProviderService } from "./provider-service"; -import { AGENT_TOOLS, dispatchAgentTool, FINISH_TOOL, type AgentToolContext, type AgentToolDeps } from "./agent-tools"; +import { AGENT_TOOLS, dispatchAgentTool, type AgentToolContext, type AgentToolDeps } from "./agent-tools"; import type { AgentTurnMessage } from "./provider-tool-turn"; const MAX_ITERATIONS = 12; @@ -46,37 +46,26 @@ export async function runAgentLoop(deps: AgentRunLoopDeps, input: AgentRunLoopIn instructions: input.instructions, messages, tools: AGENT_TOOLS, - // Compel a tool call on every turn so a weak model cannot answer with - // prose and stop after a token gesture. The agent ends deliberately by - // calling `finish` — only after it has actually done the work. - toolChoice: "required", }); lastRequestId = turn.requestId; if (turn.toolCalls.length === 0) { - // The provider ignored the forced tool choice; treat it as no action. - throw new Error("Agent returned no tool call on a forced turn."); - } - - const wantsFinish = turn.toolCalls.some((call) => call.name === FINISH_TOOL); - const actionable = turn.toolCalls.filter((call) => call.name !== FINISH_TOOL); - if (actionable.length > 0) { - messages.push({ role: "assistant", text: turn.text, toolCalls: actionable }); - const results = actionable.map((call) => { - toolCallCount += 1; - return { toolCallId: call.id, toolName: call.name, output: dispatchAgentTool(deps.tools, input.toolContext, call) }; - }); - messages.push({ role: "tool", results }); - deps.onToolResults?.(); - } - - if (wantsFinish) { - if (toolCallCount === 0) { - // Finishing before doing anything is the silent no-op we must reject. - throw new Error("Agent finished without taking any action."); + if (toolCallCount === 0 && !turn.text.trim()) { + // A first turn with neither text nor tool calls means the provider + // produced nothing actionable (e.g. output truncated mid-reasoning); + // completing the run would silently mark a no-op as done. + throw new Error("Provider returned an empty turn: no text and no tool calls."); } return { finalText: turn.text, iterations: iteration, toolCallCount, lastRequestId }; } + + messages.push({ role: "assistant", text: turn.text, toolCalls: turn.toolCalls }); + const results = turn.toolCalls.map((call) => { + toolCallCount += 1; + return { toolCallId: call.id, toolName: call.name, output: dispatchAgentTool(deps.tools, input.toolContext, call) }; + }); + messages.push({ role: "tool", results }); + deps.onToolResults?.(); } throw new Error(`Agent run did not converge within ${MAX_ITERATIONS} iterations.`); diff --git a/AgentCompany-New/src/main/services/agent-tools.ts b/AgentCompany-New/src/main/services/agent-tools.ts index f364883e..1708dd77 100644 --- a/AgentCompany-New/src/main/services/agent-tools.ts +++ b/AgentCompany-New/src/main/services/agent-tools.ts @@ -119,16 +119,8 @@ export const AGENT_TOOLS: ToolDefinition[] = [ required: ["title", "doc_type", "content"], }, }, - { - name: "finish", - description: "End your turn. Call this ONLY after you have taken every action this task needs — for a kickoff that means hiring your team, delegating their first tasks, and posting a kickoff message to a channel. Do not call it just to stop early.", - parameters: { type: "object", additionalProperties: false, properties: {}, required: [] }, - }, ]; -/** Control tool that ends an agent's turn; the run loop consumes it, never dispatched. */ -export const FINISH_TOOL = "finish"; - export function dispatchAgentTool(deps: AgentToolDeps, ctx: AgentToolContext, call: ToolCall): string { try { switch (call.name) { diff --git a/AgentCompany-New/src/main/services/provider-response-runner.ts b/AgentCompany-New/src/main/services/provider-response-runner.ts index 57121c4c..22e81771 100644 --- a/AgentCompany-New/src/main/services/provider-response-runner.ts +++ b/AgentCompany-New/src/main/services/provider-response-runner.ts @@ -12,7 +12,7 @@ import type { ProviderRateLimitRegistry } from "./provider-rate-limit-registry"; import type { ProviderRequestStore } from "./provider-request-store"; import type { ProviderResponseObserver } from "./provider-response-observer"; import { hashPrompt, readProviderResponseId, requireStreamText } from "./provider-response-utils"; -import { type AgentTurnMessage, type AgentTurnResult, type ToolChoice, type ToolDefinition, type ToolTurnFn } from "./provider-tool-turn"; +import { type AgentTurnMessage, type AgentTurnResult, type ToolDefinition, type ToolTurnFn } from "./provider-tool-turn"; import { selectToolTurn, supportsToolTurn } from "./provider-tool-turns/router"; import { resolveTurnCredential } from "./provider-tool-turns/credential-resolver"; import { @@ -103,7 +103,6 @@ export class ProviderResponseRunner { instructions: string | null; messages: AgentTurnMessage[]; tools: ToolDefinition[]; - toolChoice?: ToolChoice; }): Promise { const provider = loadConfiguredProvider(this.deps.db, input.orgId, input.providerId); const workspaceId = this.normalizeWorkspaceId(input.orgId, input.workspaceId ?? null); @@ -129,7 +128,7 @@ export class ProviderResponseRunner { }); const startedAt = Date.now(); try { - const result = await turn({ credential, model, instructions: input.instructions, messages: input.messages, tools: input.tools, toolChoice: input.toolChoice }); + const result = await turn({ credential, model, instructions: input.instructions, messages: input.messages, tools: input.tools }); this.deps.requestStore.markSucceeded({ orgId: input.orgId, requestId, diff --git a/AgentCompany-New/src/main/services/provider-tool-turn.ts b/AgentCompany-New/src/main/services/provider-tool-turn.ts index 90f3541d..867db307 100644 --- a/AgentCompany-New/src/main/services/provider-tool-turn.ts +++ b/AgentCompany-New/src/main/services/provider-tool-turn.ts @@ -51,18 +51,12 @@ export interface TurnCredential { serviceAccountJson?: string; } -/** How the backend should treat tool calls this turn. "required" compels the - * model to emit at least one tool call (no prose-only escape) — used to force a - * concrete action so a weak model cannot ramble its way to a silent no-op. */ -export type ToolChoice = "auto" | "required"; - export interface ProviderToolTurnInput { credential: TurnCredential; model: string; instructions: string | null; messages: AgentTurnMessage[]; tools: ToolDefinition[]; - toolChoice?: ToolChoice; } export type ToolTurnFn = (input: ProviderToolTurnInput) => Promise; diff --git a/AgentCompany-New/src/main/services/provider-tool-turns/bedrock-format.ts b/AgentCompany-New/src/main/services/provider-tool-turns/bedrock-format.ts index b17d22c3..3a14319e 100644 --- a/AgentCompany-New/src/main/services/provider-tool-turns/bedrock-format.ts +++ b/AgentCompany-New/src/main/services/provider-tool-turns/bedrock-format.ts @@ -1,11 +1,11 @@ /** AWS Bedrock Converse API encoding/decoding (ported from Pi's bedrock handler). */ import { isRecord, type JsonRecord } from "../provider-http"; -import type { AgentTurnMessage, ToolCall, ToolChoice, ToolDefinition } from "../provider-tool-turn"; +import type { AgentTurnMessage, ToolCall, ToolDefinition } from "../provider-tool-turn"; -export function bedrockToolConfig(tools: ToolDefinition[], toolChoice?: ToolChoice): JsonRecord { +export function bedrockToolConfig(tools: ToolDefinition[]): JsonRecord { return { tools: tools.map((tool) => ({ toolSpec: { name: tool.name, description: tool.description, inputSchema: { json: tool.parameters } } })), - toolChoice: toolChoice === "required" ? { any: {} } : { auto: {} }, + toolChoice: { auto: {} }, }; } diff --git a/AgentCompany-New/src/main/services/provider-tool-turns/gemini-format.ts b/AgentCompany-New/src/main/services/provider-tool-turns/gemini-format.ts index 94603e7a..1bc7424e 100644 --- a/AgentCompany-New/src/main/services/provider-tool-turns/gemini-format.ts +++ b/AgentCompany-New/src/main/services/provider-tool-turns/gemini-format.ts @@ -4,7 +4,7 @@ * Antigravity request wrapper in google-gemini-cli. */ import { isRecord, type JsonRecord, type ProviderEventStreamMessage } from "../provider-http"; -import { type AgentTurnMessage, type AgentTurnResult, type ToolCall, type ToolChoice, type ToolDefinition } from "../provider-tool-turn"; +import { type AgentTurnMessage, type AgentTurnResult, type ToolCall, type ToolDefinition } from "../provider-tool-turn"; const ANTIGRAVITY_SYSTEM_INSTRUCTION = "You are Antigravity, a powerful agentic AI coding assistant designed by the Google Deepmind team working on Advanced Agentic Coding." + @@ -64,7 +64,6 @@ export function antigravityRequestBody(input: { instructions: string | null; messages: AgentTurnMessage[]; tools: ToolDefinition[]; - toolChoice?: ToolChoice; requestId: string; }): JsonRecord { const request: JsonRecord = { @@ -80,7 +79,7 @@ export function antigravityRequestBody(input: { }; if (input.tools.length > 0) { request.tools = geminiToolDefs(input.model, input.tools); - request.toolConfig = { functionCallingConfig: { mode: input.toolChoice === "required" ? "ANY" : "AUTO" } }; + request.toolConfig = { functionCallingConfig: { mode: "AUTO" } }; } return { project: input.projectId, model: input.model, request, requestType: "agent", userAgent: "antigravity", requestId: input.requestId }; } @@ -91,13 +90,12 @@ export function vertexRequestBody(input: { instructions: string | null; messages: AgentTurnMessage[]; tools: ToolDefinition[]; - toolChoice?: ToolChoice; }): JsonRecord { const body: JsonRecord = { contents: toGeminiContents(input.model, input.messages) }; if (input.instructions) body.systemInstruction = { parts: [{ text: input.instructions }] }; if (input.tools.length > 0) { body.tools = geminiToolDefs(input.model, input.tools); - body.toolConfig = { functionCallingConfig: { mode: input.toolChoice === "required" ? "ANY" : "AUTO" } }; + body.toolConfig = { functionCallingConfig: { mode: "AUTO" } }; } return body; } diff --git a/AgentCompany-New/src/main/services/provider-tool-turns/turns.ts b/AgentCompany-New/src/main/services/provider-tool-turns/turns.ts index d8ee93f7..f3b342a8 100644 --- a/AgentCompany-New/src/main/services/provider-tool-turns/turns.ts +++ b/AgentCompany-New/src/main/services/provider-tool-turns/turns.ts @@ -41,7 +41,7 @@ export async function runOpenAiResponsesTurn(input: ProviderToolTurnInput): Prom model: input.model, input: toResponsesInput(input.messages), tools: responsesToolDefs(input.tools), - tool_choice: input.toolChoice ?? "auto", + tool_choice: "auto", store: false, ...(input.instructions ? { instructions: input.instructions } : {}), }; @@ -58,7 +58,7 @@ export function makeChatCompletionsTurn(endpoint: string, label: string): (input model: input.model, messages: toChatMessages(input.instructions, input.messages), tools: chatToolDefs(input.tools), - tool_choice: input.toolChoice ?? "auto", + tool_choice: "auto", stream: false, }; const response = await postJson(endpoint, { authorization: `Bearer ${requireApiKey(input.credential, label)}` }, body); @@ -74,7 +74,6 @@ export async function runAnthropicTurn(input: ProviderToolTurnInput): Promise Promise => { if ((input.instructions ?? "").includes("Dana Lee")) { @@ -46,11 +46,11 @@ function scriptedTeam(): (input: ProviderToolTurnInput) => Promise Promise { @@ -55,7 +55,7 @@ function scriptedCeo(): (input: ProviderToolTurnInput) => Promise { test("a goal created after onboarding reaches the CEO and runs autonomously", async () => { const connection = new DatabaseConnection(join(tmpdir(), `ac-goal-${randomUUID()}.db`)); - // The forced opening turn means each run must take a concrete action; it - // posts to the company channel, then stops on the next (free) turn. - const providers = new ProviderService(connection.db, providerAdapters, testVault, undefined, async (input) => { - const acted = input.messages.some((message) => message.role === "tool"); - if (acted) return { text: "Done.", toolCalls: [call("finish", {})], rawResponseJson: JSON.stringify({ id: "resp_goal_done" }) }; - return { - text: "Reviewing the goal.", - toolCalls: [call("send_message", { channel: "general", body: "Reviewed the goal and posting the operating plan." })], - rawResponseJson: JSON.stringify({ id: "resp_goal_act" }), - }; - }); + const providers = new ProviderService(connection.db, providerAdapters, testVault, undefined, async () => ({ + text: "Reviewed the goal and posted the operating plan.", + toolCalls: [], + rawResponseJson: JSON.stringify({ id: "resp_goal" }), + })); const onboarding = new OnboardingService({ db: connection.db, saveProviderCredential: (input) => providers.saveCredential(input) }); const organization = new OrganizationService(connection.db); const taskExecution = new TaskExecutionService(connection.db, providers); diff --git a/AgentCompany-New/tests/unit/agent-run-loop.test.ts b/AgentCompany-New/tests/unit/agent-run-loop.test.ts index ef11f1f4..44bcb361 100644 --- a/AgentCompany-New/tests/unit/agent-run-loop.test.ts +++ b/AgentCompany-New/tests/unit/agent-run-loop.test.ts @@ -19,8 +19,7 @@ function fakeProvider(script: ScriptedTurn[]) { let index = 0; return { runToolTurn: async (): Promise => { - // A run never ends on prose now; an exhausted script ends by calling finish. - const turn = script[index] ?? { text: "done", toolCalls: [call("finish", {})] }; + const turn = script[index] ?? { text: "done", toolCalls: [] }; index += 1; return { text: turn.text, toolCalls: turn.toolCalls, rawResponseJson: "{}", requestId: `req_${index}` }; }, @@ -31,25 +30,6 @@ function call(name: string, args: Record): ToolCall { return { id: `call_${randomUUID()}`, name, arguments: args }; } -/** Mimics a weak model: it rambles in prose and skips tools unless the backend - * compels a tool call. With forcing it posts a kickoff message, then stops. */ -function weakModelProvider() { - let acted = false; - return { - runToolTurn: async (input: { toolChoice?: "auto" | "required" }): Promise => { - // Left to itself this model rambles (no tools); compelled, it posts, then ends. - if (input.toolChoice !== "required") { - return { text: "Let me think about this some more.", toolCalls: [], rawResponseJson: "{}", requestId: "req_prose" }; - } - if (!acted) { - acted = true; - return { text: "Acting.", toolCalls: [call("send_message", { channel: "general", body: "Kicking off the company." })], rawResponseJson: "{}", requestId: "req_act" }; - } - return { text: "Done.", toolCalls: [call("finish", {})], rawResponseJson: "{}", requestId: "req_finish" }; - }, - }; -} - describe("agent run loop (real tool dispatch)", () => { test("the CEO hires, creates a task, writes a deliverable, then converges", async () => { const connection = new DatabaseConnection(join(tmpdir(), `ac-loop-${randomUUID()}.db`)); @@ -97,7 +77,7 @@ describe("agent run loop (real tool dispatch)", () => { call("write_deliverable", { path: "plan/operating-plan.md", content: "# Operating Plan\n\n1. Validate demand.\n2. Launch booking site.\n" }), call("send_message", { channel: "general", body: "Team staffed and the operating plan is live. Booking site is next." }), ] }, - { text: "Team staffed, work delegated, plan written.", toolCalls: [call("finish", {})] }, + { text: "Team staffed, work delegated, plan written.", toolCalls: [] }, ]; let toolResultNotifications = 0; @@ -140,33 +120,6 @@ describe("agent run loop (real tool dispatch)", () => { connection.close(); }); - test("forcing the opening turn makes a weak model act instead of completing a no-op", async () => { - const connection = new DatabaseConnection(join(tmpdir(), `ac-weak-${randomUUID()}.db`)); - const organization = new OrganizationService(connection.db); - let orgId = ""; - let ceoId = ""; - let providerId = ""; - connection.transaction(() => { - orgId = organization.createOrganization({ organizationName: "OrbitOps", employeeName: "Yang", mission: "m", goal: "g" }); - providerId = (connection.db.prepare("select id from providers where org_id = ? and provider_key = 'openai'").get(orgId) as { id: string }).id; - ceoId = organization.createAgent({ orgId, name: "CEO", role: "CEO", department: "Executive", modelProviderId: providerId, modelId: "gpt-5.4", budgetCents: 25000, managerAgentId: null }); - }); - const ctx: AgentToolContext = { - orgId, agent: { id: ceoId, modelProviderId: providerId, modelId: "gpt-5.4" }, - taskProjectId: null, taskGoalId: null, workspace: null, - }; - const result = await runAgentLoop( - { providers: weakModelProvider(), tools: { db: connection.db, organization, chat: new ChatService(connection.db) } }, - { orgId, providerId, workspaceId: null, instructions: "You are the CEO.", initialUserTurn: "Start the company.", toolContext: ctx }, - ); - // The loop forced the opening turn, so the otherwise-rambling model acted. - expect(result.toolCallCount).toBe(1); - const messages = connection.db.prepare("select sender_id from chat_messages where org_id = ?").all(orgId) as Array<{ sender_id: string }>; - expect(messages).toHaveLength(1); - expect(messages[0]?.sender_id).toBe(ceoId); - connection.close(); - }); - test("rejects deliverable paths that escape the workspace", async () => { const connection = new DatabaseConnection(join(tmpdir(), `ac-loop2-${randomUUID()}.db`)); const organization = new OrganizationService(connection.db); @@ -186,7 +139,7 @@ describe("agent run loop (real tool dispatch)", () => { }; const script: ScriptedTurn[] = [ { text: "escape", toolCalls: [call("write_deliverable", { path: "../escape.txt", content: "x" })] }, - { text: "done", toolCalls: [call("finish", {})] }, + { text: "done", toolCalls: [] }, ]; const result = await runAgentLoop( { providers: fakeProvider(script), tools: { db: connection.db, organization, chat: new ChatService(connection.db) } }, @@ -198,7 +151,7 @@ describe("agent run loop (real tool dispatch)", () => { connection.close(); }); - test("a first turn that takes no action (prose only, no tool calls) fails the run", async () => { + test("a first turn with no text and no tool calls fails the run instead of completing a no-op", async () => { const connection = new DatabaseConnection(join(tmpdir(), `ac-loop3-${randomUUID()}.db`)); const organization = new OrganizationService(connection.db); let orgId = ""; @@ -213,12 +166,10 @@ describe("agent run loop (real tool dispatch)", () => { orgId, agent: { id: ceoId, modelProviderId: providerId, modelId: "gpt-5.4" }, taskProjectId: null, taskGoalId: null, workspace: null, }; - // A weak model that answers the kickoff with prose and zero tool calls must - // not be recorded as a success — the forced first turn produced no action. await expect(runAgentLoop( - { providers: fakeProvider([{ text: "Here is my plan: first I will think about it.", toolCalls: [] }]), tools: { db: connection.db, organization, chat: new ChatService(connection.db) } }, + { providers: fakeProvider([{ text: "", toolCalls: [] }]), tools: { db: connection.db, organization, chat: new ChatService(connection.db) } }, { orgId, providerId, workspaceId: null, instructions: "i", initialUserTurn: "u", toolContext: ctx }, - )).rejects.toThrow(/no tool call on a forced turn/); + )).rejects.toThrow(/empty turn/); connection.close(); }); }); diff --git a/AgentCompany-New/tests/unit/provider-tool-turns.test.ts b/AgentCompany-New/tests/unit/provider-tool-turns.test.ts index 723adc5f..605e72bf 100644 --- a/AgentCompany-New/tests/unit/provider-tool-turns.test.ts +++ b/AgentCompany-New/tests/unit/provider-tool-turns.test.ts @@ -210,22 +210,13 @@ describe("credential resolver", () => { }); }); -describe("tool definitions and forced tool choice", () => { +describe("responses tool definitions", () => { 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", () => { + test("omit strict when null (so the Codex endpoint keeps the tools), 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("strict" in codex).toBe(false); // strict:null is malformed and makes 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" } }); - }); }); diff --git a/AgentCompany-New/tests/unit/task-execution-service.test.ts b/AgentCompany-New/tests/unit/task-execution-service.test.ts index dad7bb7b..dd9c9982 100644 --- a/AgentCompany-New/tests/unit/task-execution-service.test.ts +++ b/AgentCompany-New/tests/unit/task-execution-service.test.ts @@ -37,11 +37,7 @@ describe("task execution service", () => { const fakeToolTurn = async (input: ProviderToolTurnInput): Promise => { const first = input.messages[0]; if (first?.role === "user") capturedPrompt = first.text; - const acted = input.messages.some((message) => message.role === "tool"); - const toolCall = acted - ? { id: `call_${randomUUID()}`, name: "finish", arguments: {} } - : { id: `call_${randomUUID()}`, name: "create_document", arguments: { title: "Run note", doc_type: "memo", content: "Executed." } }; - return { text: "executed:gpt-5.4", toolCalls: [toolCall], rawResponseJson: JSON.stringify({ id: "resp-task-1" }) }; + return { text: "executed:gpt-5.4", toolCalls: [], rawResponseJson: JSON.stringify({ id: "resp-task-1" }) }; }; const providers = new ProviderService(connection.db, providerAdapters, testVault, undefined, fakeToolTurn); const skills = new SkillService(connection.db, join(tmpdir(), `agentcompany-task-skills-${randomUUID()}`)); @@ -145,13 +141,11 @@ describe("task execution service", () => { const organization = new OrganizationService(connection.db); const approvals = new ApprovalService(connection.db); const snapshot = new SnapshotService(connection.db); - const fakeToolTurn = async (input: ProviderToolTurnInput): Promise => { - const acted = input.messages.some((message) => message.role === "tool"); - const toolCall = acted - ? { id: `call_${randomUUID()}`, name: "finish", arguments: {} } - : { id: `call_${randomUUID()}`, name: "create_document", arguments: { title: "Run note", doc_type: "memo", content: "Executed." } }; - return { text: "executed:gpt-5.4", toolCalls: [toolCall], rawResponseJson: JSON.stringify({ id: "resp-task-approved" }) }; - }; + const fakeToolTurn = async (): Promise => ({ + text: "executed:gpt-5.4", + toolCalls: [], + rawResponseJson: JSON.stringify({ id: "resp-task-approved" }), + }); const providers = new ProviderService(connection.db, providerAdapters, testVault, undefined, fakeToolTurn); const execution = new TaskExecutionService(connection.db, providers); let orgId = "";