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.
This commit is contained in:
1 parent
d6d6656428
commit
b4e17cbd35
2 files changed
+28
No files matched your search
@@ -48,6 +48,12 @@ export async function runAgentLoop(deps: AgentRunLoopDeps, input: AgentRunLoopIn
|
|||||||
lastRequestId = turn.requestId;
|
lastRequestId = turn.requestId;
|
||||||
|
|
||||||
if (turn.toolCalls.length === 0) {
|
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 };
|
return { finalText: turn.text, iterations: iteration, toolCallCount, lastRequestId };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -143,4 +143,26 @@ describe("agent run loop (real tool dispatch)", () => {
|
|||||||
expect(result.toolCallCount).toBe(1);
|
expect(result.toolCallCount).toBe(1);
|
||||||
connection.close();
|
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();
|
||||||
|
});
|
||||||
});
|
});
|
||||||
Reference in new issue
Block a user