Skip to content
Closed
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
60 changes: 55 additions & 5 deletions packages/core/src/agents/executor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -393,11 +393,11 @@ export class AgentExecutor<TOutput extends z.ZodTypeAny> {

while (true) {
// Check for termination conditions like max turns.
const reason = this.checkTermination(startTime, turnCounter);
if (reason) {
terminateReason = reason;
break;
}
// const reason = this.checkTermination(startTime, turnCounter);
// if (reason) {
// terminateReason = reason;
// break;
// }

// Check for timeout or external abort.
if (combinedSignal.aborted) {
Expand All @@ -408,6 +408,9 @@ export class AgentExecutor<TOutput extends z.ZodTypeAny> {
break;
}

// Check if this will be the last turn before executing it
const isLastTurn = this.checkIsLastTurn(turnCounter);

const turnResult = await this.executeTurn(
chat,
currentMessage,
Expand All @@ -425,6 +428,40 @@ export class AgentExecutor<TOutput extends z.ZodTypeAny> {
break; // Exit the loop for *any* stop reason.
}

// If this was the last allowed turn and task is not complete, append warning to the response
if (isLastTurn && turnResult.status === 'continue') {
// Append warning message to the tool results
const warningText = this.getFinalWarningMessage(
AgentTerminateMode.MAX_TURNS,
);
const nextMessageParts = turnResult.nextMessage.parts || [];
nextMessageParts.push({ text: warningText });
currentMessage = {
role: 'user',
parts: nextMessageParts,
};

// Execute one more turn with the warning included
const finalTurnResult = await this.executeTurn(
chat,
currentMessage,
turnCounter++,
combinedSignal,
timeoutController.signal,
);

if (
finalTurnResult.status === 'stop' &&
finalTurnResult.terminateReason === AgentTerminateMode.GOAL
) {
terminateReason = AgentTerminateMode.GOAL;
finalResult = finalTurnResult.finalResult;
} else {
terminateReason = AgentTerminateMode.MAX_TURNS;
}
break;
}
Comment on lines +432 to +463
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

critical

This new logic block is a great improvement for handling the max_turns limit, as it ensures the final turn's tool calls are executed. However, it introduces a side effect by conflicting with the existing generic recovery mechanism.

When this block sets terminateReason = AgentTerminateMode.MAX_TURNS and breaks the loop, the code proceeds to the unified recovery block at line 472. This causes executeFinalWarningTurn to run again, resulting in a redundant recovery attempt and an extra, unexpected turn for the agent.

To fix this, the unified recovery block should be modified to exclude the MAX_TURNS case, since it's now handled here. Please update the condition at line 472 to prevent this redundant execution:

// In file packages/core/src/agents/executor.ts at line 472

      if (
        terminateReason !== AgentTerminateMode.ERROR &&
        terminateReason !== AgentTerminateMode.ABORTED &&
        terminateReason !== AgentTerminateMode.GOAL &&
        terminateReason !== AgentTerminateMode.MAX_TURNS
      ) {


// If status is 'continue', update message for the next loop
currentMessage = turnResult.nextMessage;
}
Expand Down Expand Up @@ -1061,6 +1098,19 @@ Important Rules:
return null;
}

/**
* Checks if the current turn is the last allowed turn before hitting max_turns.
*
* @param turnCounter - The current turn counter (0-indexed).
* @returns True if this is the last turn before max_turns is reached, false otherwise.
*/
private checkIsLastTurn(turnCounter: number): boolean {
return (
!!this.definition.runConfig.max_turns &&
turnCounter >= this.definition.runConfig.max_turns - 1
);
}

/** Emits an activity event to the configured callback. */
private emitActivity(
type: SubagentActivityEvent['type'],
Expand Down