From 3381c44b16aaa57f631e70477f45eb637f7fb8bb Mon Sep 17 00:00:00 2001 From: _Kerman Date: Fri, 3 Jul 2026 12:01:48 +0800 Subject: [PATCH] fix: stopHookContinuationUsed --- .../externalHooks/externalHooksService.ts | 7 +++- packages/agent-core-v2/src/agent/loop/loop.ts | 2 +- .../src/agent/loop/loopService.ts | 32 +++++++------------ 3 files changed, 19 insertions(+), 22 deletions(-) diff --git a/packages/agent-core-v2/src/agent/externalHooks/externalHooksService.ts b/packages/agent-core-v2/src/agent/externalHooks/externalHooksService.ts index ba9044602..1f4b296f9 100644 --- a/packages/agent-core-v2/src/agent/externalHooks/externalHooksService.ts +++ b/packages/agent-core-v2/src/agent/externalHooks/externalHooksService.ts @@ -77,6 +77,7 @@ export class AgentExternalHooksService extends Disposable implements IAgentExter declare readonly _serviceBrand: undefined; private dynamicEngine: HookEngine | undefined; + private stopHookContinuationUsed = false; constructor( private readonly options: ExternalHooksServiceOptions = {}, @@ -203,6 +204,7 @@ export class AgentExternalHooksService extends Disposable implements IAgentExter loop.hooks.onWillStop.register('externalHooks', async (ctx, next) => { const reason = await this.runStop(ctx); if (reason !== undefined) { + this.stopHookContinuationUsed = true; ctx.continuationPrompt = reason; return; } @@ -314,6 +316,7 @@ export class AgentExternalHooksService extends Disposable implements IAgentExter } private notifyTurnEnded(ctx: TurnEndedContext): void { + this.stopHookContinuationUsed = false; if (ctx.result.reason === 'failed' && ctx.result.error !== undefined) { this.notifyStopFailure(ctx.result.error, ctx.turn.abortController.signal); } @@ -343,9 +346,11 @@ export class AgentExternalHooksService extends Disposable implements IAgentExter private async runStop(ctx: TurnWillStopContext): Promise { ctx.signal.throwIfAborted(); + if (this.stopHookContinuationUsed) return undefined; + const block = await this.engine()?.triggerBlock('Stop', { signal: ctx.signal, - inputData: { stopHookActive: ctx.stopHookActive }, + inputData: { stopHookActive: false }, }); ctx.signal.throwIfAborted(); return block?.reason; diff --git a/packages/agent-core-v2/src/agent/loop/loop.ts b/packages/agent-core-v2/src/agent/loop/loop.ts index a668b87c4..3c35cee68 100644 --- a/packages/agent-core-v2/src/agent/loop/loop.ts +++ b/packages/agent-core-v2/src/agent/loop/loop.ts @@ -6,6 +6,7 @@ import type { TurnResult } from './types'; export interface TurnBeforeStepContext { readonly turnId: number; + readonly step: number; readonly signal: AbortSignal; } @@ -23,7 +24,6 @@ export interface TurnContextOverflowContext { export interface TurnWillStopContext { readonly signal: AbortSignal; - readonly stopHookActive: boolean; continuationPrompt?: string; } diff --git a/packages/agent-core-v2/src/agent/loop/loopService.ts b/packages/agent-core-v2/src/agent/loop/loopService.ts index b9f04a0b9..550785134 100644 --- a/packages/agent-core-v2/src/agent/loop/loopService.ts +++ b/packages/agent-core-v2/src/agent/loop/loopService.ts @@ -71,7 +71,6 @@ export class AgentLoopService implements IAgentLoopService { let stopReason: LoopTurnStopReason = 'completed'; let activeStep: number | undefined; const maxSteps = this.config.get(LOOP_CONTROL_SECTION)?.maxStepsPerTurn; - let stopHookContinuationUsed = false; try { while (true) { @@ -96,19 +95,16 @@ export class AgentLoopService implements IAgentLoopService { continue; } - if (!stopHookContinuationUsed) { - const context: TurnWillStopContext = { signal, stopHookActive: false }; - await this.hooks.onWillStop.run(context); - if (context.continuationPrompt !== undefined && hasStepBudgetRemaining(maxSteps, steps)) { - stopHookContinuationUsed = true; - this.append({ - role: 'user', - content: [{ type: 'text', text: context.continuationPrompt }], - toolCalls: [], - origin: { kind: 'system_trigger', name: 'stop_hook' }, - }); - continue; - } + const context: TurnWillStopContext = { signal }; + await this.hooks.onWillStop.run(context); + if (context.continuationPrompt !== undefined) { + this.append({ + role: 'user', + content: [{ type: 'text', text: context.continuationPrompt }], + toolCalls: [], + origin: { kind: 'system_trigger', name: 'stop_hook' }, + }); + continue; } break; @@ -150,7 +146,7 @@ export class AgentLoopService implements IAgentLoopService { readonly stopReason: FinishReason; readonly continueTurn: boolean; }> { - await this.hooks.beforeStep.run({ turnId, signal }); + await this.hooks.beforeStep.run({ turnId, step: currentStep, signal }); signal.throwIfAborted(); const stepUuid = randomUUID(); @@ -228,7 +224,7 @@ export class AgentLoopService implements IAgentLoopService { this.emitStepCompleted(turnId, currentStep, stepUuid, usage, finishReason, response); - const afterStepContext = { turnId, signal, usage, continueTurn: false }; + const afterStepContext = { turnId, step: currentStep, signal, usage, continueTurn: false }; try { await this.hooks.afterStep.run(afterStepContext); } catch { @@ -383,10 +379,6 @@ function toolResultOutputForModel(result: ToolResult): string | ContentPart[] { return output; } -function hasStepBudgetRemaining(maxSteps: number | undefined, currentStep: number): boolean { - return maxSteps === undefined || maxSteps <= 0 || currentStep < maxSteps; -} - registerScopedService( LifecycleScope.Agent, IAgentLoopService,