diff --git a/src/agent/runner.ts b/src/agent/runner.ts index 78ae777..79096bd 100644 --- a/src/agent/runner.ts +++ b/src/agent/runner.ts @@ -310,14 +310,13 @@ export class AgentRunner { yield { type: 'text', data: turnText } satisfies StreamEvent } - // Announce each tool-use block the model requested. + // Extract tool-use blocks for detection and execution. const toolUseBlocks = extractToolUseBlocks(response.content) - for (const block of toolUseBlocks) { - yield { type: 'tool_use', data: block } satisfies StreamEvent - } // ------------------------------------------------------------------ - // Step 2.5: Loop detection — check before executing tools. + // Step 2.5: Loop detection — check before yielding tool_use events + // so that terminate mode doesn't emit orphaned tool_use without + // matching tool_result. // ------------------------------------------------------------------ let injectWarning = false let injectWarningKind: 'tool_repetition' | 'text_repetition' = 'tool_repetition' @@ -331,7 +330,7 @@ export class AgentRunner { options.onWarning?.(info.detail) const action = typeof loopAction === 'function' - ? loopAction(info) + ? await loopAction(info) : loopAction if (action === 'terminate') { @@ -363,6 +362,12 @@ export class AgentRunner { break } + // Announce each tool-use block the model requested (after loop + // detection, so terminate mode never emits unpaired events). + for (const block of toolUseBlocks) { + yield { type: 'tool_use', data: block } satisfies StreamEvent + } + // ------------------------------------------------------------------ // Step 4: Execute all tool calls in PARALLEL. // diff --git a/src/types.ts b/src/types.ts index f093288..25893fb 100644 --- a/src/types.ts +++ b/src/types.ts @@ -254,10 +254,10 @@ export interface LoopDetectionConfig { * - `'warn'` — inject a "you appear stuck" message, give the LLM one * more chance; terminate if the loop persists (default) * - `'terminate'` — stop the run immediately - * - `function` — custom callback; return `'continue'`, `'inject'`, or - * `'terminate'` to control the outcome + * - `function` — custom callback (sync or async); return `'continue'`, + * `'inject'`, or `'terminate'` to control the outcome */ - readonly onLoopDetected?: 'warn' | 'terminate' | ((info: LoopDetectionInfo) => 'continue' | 'inject' | 'terminate') + readonly onLoopDetected?: 'warn' | 'terminate' | ((info: LoopDetectionInfo) => 'continue' | 'inject' | 'terminate' | Promise<'continue' | 'inject' | 'terminate'>) } /** Diagnostic payload emitted when a loop is detected. */