-
Notifications
You must be signed in to change notification settings - Fork 222
DurableAgent stops tool loop when step has tool calls but finishReason is "other"/"unknown" #1387
Description
Bug
DurableAgent stops instead of continuing the tool loop when a step contains tool calls but the provider/model reports finishReason: other (or raw unknown) instead of tool-calls.
This is reproducible with a mock model, so it does not appear to be provider-specific.
Versions
@workflow/ai:4.0.1-beta.54workflow:4.2.0-beta.67ai:6.0.116
I also checked the published @workflow/ai@4.0.1-beta.56 tarball and the same loop logic still appears to be present.
Expected
If a step contains tool calls, DurableAgent should yield those tool calls, accept tool results, and continue the loop, regardless of whether the provider reports finishReason: tool-calls vs other/unknown.
ai core streamText already behaves this way.
Actual
DurableAgent stops after the first step and never executes/feeds back the tool result.
Minimal repro
import { streamText, simulateReadableStream, stepCountIs, tool } from 'ai';
import { MockLanguageModelV3 } from 'ai/test';
import { DurableAgent } from '@workflow/ai/agent';
import z from 'zod/v3';
function makeMockModel() {
let call = 0;
return new MockLanguageModelV3({
doStream: async () => {
call += 1;
if (call === 1) {
return {
stream: simulateReadableStream({
chunks: [
{ type: 'stream-start', warnings: [] },
{
type: 'tool-call',
toolCallId: 'call_1',
toolName: 'readFile',
input: JSON.stringify({ path: '.claude/plan.md' }),
},
{
type: 'finish',
finishReason: { unified: 'other', raw: 'unknown' },
usage: {
inputTokens: {
total: 10,
noCache: 10,
cacheRead: undefined,
cacheWrite: undefined,
},
outputTokens: {
total: 5,
text: 0,
reasoning: 5,
},
},
},
],
}),
};
}
return {
stream: simulateReadableStream({
chunks: [
{ type: 'stream-start', warnings: [] },
{ type: 'text-start', id: 'text_1' },
{ type: 'text-delta', id: 'text_1', delta: 'continued after tool result' },
{ type: 'text-end', id: 'text_1' },
{
type: 'finish',
finishReason: { unified: 'stop', raw: 'stop' },
usage: {
inputTokens: {
total: 20,
noCache: 20,
cacheRead: undefined,
cacheWrite: undefined,
},
outputTokens: {
total: 6,
text: 6,
reasoning: 0,
},
},
},
],
}),
};
},
});
}
const tools = {
readFile: tool({
inputSchema: z.object({ path: z.string() }),
execute: async ({ path }) => ({ path, content: 'plan contents' }),
}),
};
// AI SDK core continues correctly
const aiResult = streamText({
model: makeMockModel(),
prompt: 'Read the plan',
tools,
stopWhen: stepCountIs(5),
});
console.log('streamText text:', await aiResult.text);
console.log(
'streamText steps:',
(await aiResult.steps).map(step => ({
finishReason: step.finishReason,
toolCalls: step.toolCalls.length,
toolResults: step.toolResults.length,
})),
);
// DurableAgent stops after step 1
const agent = new DurableAgent({
model: async () => makeMockModel(),
tools,
});
const writable = new WritableStream({ write() {} });
const agentResult = await agent.stream({
messages: [{ role: 'user', content: [{ type: 'text', text: 'Read the plan' }] }],
writable,
maxSteps: 5,
stopWhen: [({ steps }) => steps.length >= 5],
});
console.log(
'DurableAgent steps:',
agentResult.steps.map(step => ({
finishReason: step.finishReason,
toolCalls: step.toolCalls.length,
toolResults: step.toolResults.length,
text: step.text,
})),
);Try DurableAgent with the new Grok 4.20 and you will probably encounter it