Skip to content

Commit ed8fd75

Browse files
authored
Revert "Revert "Fix agent loop not continuing when onIterationComplete returns continue: true"" (#14170)
Reverts #14165 I think this was just due to a bad error message, can't repro now anymore even after re-introducing this commit and trying a bunch of times
1 parent 6577267 commit ed8fd75

3 files changed

Lines changed: 113 additions & 9 deletions

File tree

.changeset/crisp-eyes-invite.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'@mastra/core': patch
3+
---
4+
5+
Fix agent loop not continuing when `onIterationComplete` returns `continue: true`

packages/core/src/agent/__tests__/supervisor-integration.test.ts

Lines changed: 98 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -234,7 +234,6 @@ describe('Supervisor Pattern Integration Tests', () => {
234234
maxSteps: 3,
235235
onIterationComplete: (ctx: IterationCompleteContext) => {
236236
iterations.push(ctx.iteration);
237-
return { continue: true };
238237
},
239238
});
240239

@@ -561,7 +560,7 @@ describe('Supervisor Pattern Integration Tests', () => {
561560

562561
it('should accept iteration complete hook configuration', async () => {
563562
const iterationHook = vi.fn(() => {
564-
return { continue: true };
563+
return undefined;
565564
});
566565

567566
const agent = new Agent({
@@ -1610,7 +1609,6 @@ describe('Supervisor Pattern - onIterationComplete Hook Integration', () => {
16101609
maxSteps: 5,
16111610
onIterationComplete: (ctx: IterationCompleteContext) => {
16121611
iterations.push(ctx.iteration);
1613-
return { continue: true };
16141612
},
16151613
});
16161614

@@ -2038,12 +2036,103 @@ describe('Supervisor Pattern - onIterationComplete Hook Integration', () => {
20382036
},
20392037
});
20402038

2041-
// When the model returns stop (isFinal), the loop ends after that iteration
2042-
// even if the hook returns continue: true with feedback. Feedback only adds
2043-
// a user message for the *next* iteration when the loop would naturally continue
2044-
// (e.g. during a tool-call sequence). Here the model says stop on iteration 1
2045-
// so the loop ends and the hook is called exactly once.
2046-
expect(iterationCount).toBe(1);
2039+
expect(iterationCount).toBe(2);
2040+
});
2041+
2042+
it('should allow onIterationComplete continue:true to override final stop in stream (issue #14134)', async () => {
2043+
const iterations: number[] = [];
2044+
let callCount = 0;
2045+
2046+
const agent = new Agent({
2047+
id: 'continue-override-stream-agent',
2048+
name: 'Continue Override Stream Agent',
2049+
instructions: 'You may take multiple turns.',
2050+
model: new MockLanguageModelV2({
2051+
doGenerate: async () => {
2052+
callCount++;
2053+
if (callCount === 1) {
2054+
return {
2055+
rawCall: { rawPrompt: null, rawSettings: {} },
2056+
finishReason: 'stop',
2057+
usage: { inputTokens: 10, outputTokens: 20, totalTokens: 30 },
2058+
text: 'First response ',
2059+
content: [{ type: 'text', text: 'First response ' }],
2060+
warnings: [],
2061+
};
2062+
}
2063+
2064+
return {
2065+
rawCall: { rawPrompt: null, rawSettings: {} },
2066+
finishReason: 'stop',
2067+
usage: { inputTokens: 10, outputTokens: 20, totalTokens: 30 },
2068+
text: 'Second response',
2069+
content: [{ type: 'text', text: 'Second response' }],
2070+
warnings: [],
2071+
};
2072+
},
2073+
doStream: async () => {
2074+
callCount++;
2075+
const currentCall = callCount;
2076+
const responseText = currentCall === 1 ? 'First response ' : 'Second response';
2077+
2078+
return {
2079+
rawCall: { rawPrompt: null, rawSettings: {} },
2080+
warnings: [],
2081+
stream: convertArrayToReadableStream([
2082+
{ type: 'stream-start', warnings: [] },
2083+
{
2084+
type: 'response-metadata',
2085+
id: `id-${currentCall}`,
2086+
modelId: 'mock-model-id',
2087+
timestamp: new Date(0),
2088+
},
2089+
{ type: 'text-start', id: `text-${currentCall}` },
2090+
{ type: 'text-delta', id: `text-${currentCall}`, delta: responseText },
2091+
{ type: 'text-end', id: `text-${currentCall}` },
2092+
{
2093+
type: 'finish',
2094+
finishReason: 'stop',
2095+
usage: { inputTokens: 10, outputTokens: 20, totalTokens: 30 },
2096+
},
2097+
]),
2098+
};
2099+
},
2100+
}),
2101+
memory: new MockMemory(),
2102+
});
2103+
2104+
const mastra = new Mastra({
2105+
agents: {
2106+
'continue-override-stream-agent': agent,
2107+
},
2108+
storage: new InMemoryStore(),
2109+
});
2110+
2111+
const testAgent = mastra.getAgent('continue-override-stream-agent');
2112+
2113+
const result = await testAgent.stream('Take multiple turns', {
2114+
maxSteps: 5,
2115+
onIterationComplete: ctx => {
2116+
iterations.push(ctx.iteration);
2117+
if (ctx.iteration === 1) {
2118+
return { continue: true };
2119+
}
2120+
},
2121+
});
2122+
2123+
const reader = result.fullStream.getReader();
2124+
while (true) {
2125+
const { done } = await reader.read();
2126+
if (done) break;
2127+
}
2128+
2129+
const text = await result.text;
2130+
2131+
// When the model returns stop (isFinal), the hook's continue:true should be
2132+
// able to request another iteration in the streaming supervisor loop.
2133+
expect(iterations).toEqual([1, 2]);
2134+
expect(callCount).toBe(2);
2135+
expect(text).toBe('First response Second response');
20472136
});
20482137

20492138
it('should accept onIterationComplete configuration without errors', async () => {

packages/core/src/loop/workflows/agentic-loop/index.ts

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -202,6 +202,16 @@ export function createAgenticLoopWorkflow<Tools extends ToolSet = ToolSet, OUTPU
202202
}
203203
} else if (iterationResult.continue === false && !hasFinishedSteps) {
204204
hasFinishedSteps = true;
205+
} else if (
206+
iterationResult.continue === true &&
207+
(hasFinishedSteps || !typedInputData.stepResult?.isContinued)
208+
) {
209+
if ((rest.maxSteps && accumulatedSteps.length < rest.maxSteps) || !rest.maxSteps) {
210+
hasFinishedSteps = false;
211+
if (typedInputData.stepResult) {
212+
typedInputData.stepResult.isContinued = true;
213+
}
214+
}
205215
}
206216
}
207217
} catch (error) {

0 commit comments

Comments
 (0)