From b4e17cbd35ef1bb226ae099e63a2d69f9edd3d69 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E9=9B=B7=E7=94=B5=E8=8A=BD=E8=A1=A3?= Date: Tue, 9 Jun 2026 21:22:44 -0400 Subject: [PATCH] Fail runs whose first turn carries no text and no tool calls A reasoning-heavy model can burn the whole max_tokens budget on thinking blocks; the parsed turn then has neither text nor tool calls and the loop treated it as a successful completion, marking a run done when nothing happened. Such a first turn now fails the run so the failure is visible and the heartbeat retries; later empty-text turns after real tool work still complete normally. --- .../src/main/services/agent-run-loop.ts | 6 +++++ .../tests/unit/agent-run-loop.test.ts | 22 +++++++++++++++++++ 2 files changed, 28 insertions(+) diff --git a/AgentCompany-New/src/main/services/agent-run-loop.ts b/AgentCompany-New/src/main/services/agent-run-loop.ts index de7338fb..acb95b1f 100644 --- a/AgentCompany-New/src/main/services/agent-run-loop.ts +++ b/AgentCompany-New/src/main/services/agent-run-loop.ts @@ -48,6 +48,12 @@ export async function runAgentLoop(deps: AgentRunLoopDeps, input: AgentRunLoopIn lastRequestId = turn.requestId; if (turn.toolCalls.length === 0) { + 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 }; } diff --git a/AgentCompany-New/tests/unit/agent-run-loop.test.ts b/AgentCompany-New/tests/unit/agent-run-loop.test.ts index 35597bbb..83318d30 100644 --- a/AgentCompany-New/tests/unit/agent-run-loop.test.ts +++ b/AgentCompany-New/tests/unit/agent-run-loop.test.ts @@ -143,4 +143,26 @@ describe("agent run loop (real tool dispatch)", () => { expect(result.toolCallCount).toBe(1); connection.close(); }); + + 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 = ""; + 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, + }; + await expect(runAgentLoop( + { 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(/empty turn/); + connection.close(); + }); });