fix(agent): support async onLoopDetected callbacks and prevent orphaned tool_use events
- Await onLoopDetected callback result so async functions work correctly instead of silently falling through to 'continue' - Move loop detection before yielding tool_use events so terminate mode never emits tool_use without a matching tool_result
This commit is contained in:
parent
cc957b3148
commit
56b8cef158
|
|
@ -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.
|
||||
//
|
||||
|
|
|
|||
|
|
@ -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. */
|
||||
|
|
|
|||
Loading…
Reference in New Issue