Skip to content
Open
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
32 changes: 32 additions & 0 deletions packages/core/src/core/client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,8 @@ import {
import { initializeContextManager } from '../context/initializer.js';

const MAX_TURNS = 100;
// Real-world time budget (ms) to protect against zero-latency event flooding
const DEFAULT_TIME_BUDGET_MS = 5000;
Comment on lines +80 to +81

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

high

The default time budget of 5000ms (5 seconds) is extremely short for real-world agent executions. A single LLM generation or tool execution (e.g., running a build or test) can easily exceed 5 seconds, causing the agent to prematurely abort with a LoopDetected event. Consider increasing this default significantly (e.g., to 5 minutes / 300,000ms) to ensure robust execution of non-trivial tasks.

Suggested change
// Real-world time budget (ms) to protect against zero-latency event flooding
const DEFAULT_TIME_BUDGET_MS = 5000;
// Real-world time budget (ms) to protect against zero-latency event flooding
const DEFAULT_TIME_BUDGET_MS = 300000;


type BeforeAgentHookReturn =
| {
Expand Down Expand Up @@ -617,6 +619,7 @@ export class GeminiClient {
prompt_id: string,
boundedTurns: number,
displayContent?: PartListUnion,
deadlineMs?: number,
): AsyncGenerator<ServerGeminiStreamEvent, Turn> {
// Re-initialize turn (it was empty before if in loop, or new instance)
let turn = new Turn(this.getChat(), prompt_id);
Expand Down Expand Up @@ -744,6 +747,12 @@ export class GeminiClient {
// Re-initialize turn with fresh history
turn = new Turn(this.getChat(), prompt_id);

const now = Date.now();
if (deadlineMs !== undefined && now > deadlineMs) {
yield { type: GeminiEventType.LoopDetected };
return turn;
}

const loopResult = await this.loopDetector.turnStarted(signal);
if (loopResult.count > 1) {
yield { type: GeminiEventType.LoopDetected };
Expand All @@ -759,6 +768,7 @@ export class GeminiClient {
prompt_id,
boundedTurns,
displayContent,
deadlineMs,
);
}

Expand Down Expand Up @@ -810,6 +820,13 @@ export class GeminiClient {
let loopDetectedAbort = false;
let loopRecoverResult: { detail?: string } | undefined;
for await (const event of resultStream) {
// Enforce time budget while iterating noisy/fast event streams
if (deadlineMs !== undefined && Date.now() > deadlineMs) {
yield { type: GeminiEventType.LoopDetected };
loopDetectedAbort = true;
break;
}

const loopResult = this.loopDetector.addAndCheck(event);
if (loopResult.count > 1) {
yield { type: GeminiEventType.LoopDetected };
Expand Down Expand Up @@ -852,6 +869,7 @@ export class GeminiClient {
prompt_id,
boundedTurns,
displayContent,
deadlineMs,
);
}
if (isError) {
Expand Down Expand Up @@ -899,6 +917,8 @@ export class GeminiClient {
prompt_id,
boundedTurns - 1,
displayContent,
false,
deadlineMs,
);
return turn;
}
Expand All @@ -914,6 +934,7 @@ export class GeminiClient {
turns: number = MAX_TURNS,
displayContent?: PartListUnion,
stopHookActive: boolean = false,
outerDeadlineMs?: number,
): AsyncGenerator<ServerGeminiStreamEvent, Turn> {
this.config.resetTurn();

Expand Down Expand Up @@ -958,6 +979,12 @@ export class GeminiClient {
}

const boundedTurns = Math.min(turns, MAX_TURNS);
// Establish deadline for this top-level call (or inherit from parent)
const deadline =
outerDeadlineMs !== undefined
? outerDeadlineMs
: Date.now() + DEFAULT_TIME_BUDGET_MS;

let turn = new Turn(this.getChat(), prompt_id);
let continuationHandled = false;

Expand All @@ -968,6 +995,7 @@ export class GeminiClient {
prompt_id,
boundedTurns,
displayContent,
deadline,
);

// Fire AfterAgent hook if we have a turn and no pending tools
Expand Down Expand Up @@ -1030,6 +1058,7 @@ export class GeminiClient {
boundedTurns - 1,
displayContent,
true, // stopHookActive: signal retry to AfterAgent hooks
deadline,
);
}
}
Expand Down Expand Up @@ -1273,6 +1302,7 @@ export class GeminiClient {
prompt_id: string,
boundedTurns: number,
displayContent?: PartListUnion,
deadlineMs?: number,
): AsyncGenerator<ServerGeminiStreamEvent, Turn> {
// Clear the detection flag so the recursive turn can proceed, but the count remains 1.
this.loopDetector.clearDetection();
Expand All @@ -1294,6 +1324,8 @@ export class GeminiClient {
prompt_id,
boundedTurns - 1,
displayContent,
false,
deadlineMs,
);
}
}