diff --git a/packages/agent-core-v2/docs/di-scope-domains.puml b/packages/agent-core-v2/docs/di-scope-domains.puml
index c93b6d10a..08070aef2 100644
--- a/packages/agent-core-v2/docs/di-scope-domains.puml
+++ b/packages/agent-core-v2/docs/di-scope-domains.puml
@@ -300,6 +300,7 @@ microCompaction --> loop #34495E
externalHooks --> config #34495E
externalHooks --> bootstrap #34495E
externalHooks --> plugin #34495E
+externalHooks --> contextMemory #34495E
todo --> agent_lifecycle #34495E
usage --> wireRecord #34495E
usage --> record #34495E
diff --git a/packages/agent-core-v2/docs/di-scope-domains.svg b/packages/agent-core-v2/docs/di-scope-domains.svg
index 6e9632437..8fdae5a57 100644
--- a/packages/agent-core-v2/docs/di-scope-domains.svg
+++ b/packages/agent-core-v2/docs/di-scope-domains.svg
@@ -1 +1 @@
-
\ No newline at end of file
+
\ No newline at end of file
diff --git a/packages/agent-core-v2/src/agent/externalHooks/externalHooksService.ts b/packages/agent-core-v2/src/agent/externalHooks/externalHooksService.ts
index 94330758e..4578316f2 100644
--- a/packages/agent-core-v2/src/agent/externalHooks/externalHooksService.ts
+++ b/packages/agent-core-v2/src/agent/externalHooks/externalHooksService.ts
@@ -5,7 +5,8 @@
* Listens to hook slots owned by the agent behavior/lifecycle domains
* (`toolExecutor`, `permissionGate`, `turn`, `loop`, `fullCompaction`, and
* `task`) and translates those minimal contexts into the configured external
- * HookEngine events. The `SubagentStart` / `SubagentStop` pair is the one
+ * HookEngine events. Appends Stop hook continuation prompts through
+ * `contextMemory`. The `SubagentStart` / `SubagentStop` pair is the one
* exception: the `agentLifecycle` tool wrapper has no hook service of its own,
* so `mirrorAgentRun` invokes `runAgentTaskStart` / `notifyAgentTaskStop` on
* this service directly.
@@ -17,12 +18,13 @@ import { LifecycleScope, registerScopedService } from '#/_base/di/scope';
import { isUserCancellation } from '#/_base/utils/abort';
import { isPlainRecord } from '#/_base/utils/canonical-args';
import { IAgentTaskService, type AgentTaskNotificationContext } from '#/agent/task';
+import { IAgentContextMemoryService } from '#/agent/contextMemory';
import {
IAgentFullCompactionService,
type FullCompactionDidCompactContext,
type FullCompactionWillCompactContext,
} from '#/agent/fullCompaction';
-import { IAgentLoopService, type TurnWillStopContext } from '#/agent/loop';
+import { IAgentLoopService, type TurnAfterStepContext } from '#/agent/loop';
import {
IAgentPermissionGate,
type PermissionApprovalResultContext,
@@ -81,6 +83,7 @@ export class AgentExternalHooksService extends Disposable implements IAgentExter
constructor(
private readonly options: ExternalHooksServiceOptions = {},
+ @IAgentContextMemoryService private readonly context: IAgentContextMemoryService,
@IInstantiationService private readonly instantiation: IInstantiationService,
@IConfigService private readonly config: IConfigService,
@IBootstrapService private readonly bootstrap: IBootstrapService,
@@ -198,14 +201,27 @@ export class AgentExternalHooksService extends Disposable implements IAgentExter
private registerLoopHooks(loop: IAgentLoopService): void {
this._register(
- loop.hooks.onWillStop.register('externalHooks', async (ctx, next) => {
+ loop.hooks.afterStep.register('externalHooks', async (ctx, next) => {
+ await next();
+ if (
+ ctx.stopReason === 'tool_calls' ||
+ ctx.stopReason === 'filtered' ||
+ ctx.continue
+ ) {
+ return;
+ }
const reason = await this.runStop(ctx);
if (reason !== undefined) {
this.stopHookContinuationUsed = true;
- ctx.continuationPrompt = reason;
+ this.context.splice(this.context.get().length, 0, [{
+ role: 'user',
+ content: [{ type: 'text', text: reason }],
+ toolCalls: [],
+ origin: { kind: 'system_trigger', name: 'stop_hook' },
+ }]);
+ ctx.continue = true;
return;
}
- await next();
}),
);
}
@@ -326,7 +342,7 @@ export class AgentExternalHooksService extends Disposable implements IAgentExter
);
}
- private async runStop(ctx: TurnWillStopContext): Promise {
+ private async runStop(ctx: TurnAfterStepContext): Promise {
ctx.signal.throwIfAborted();
if (this.stopHookContinuationUsed) return undefined;
diff --git a/packages/agent-core-v2/src/agent/goal/goalService.ts b/packages/agent-core-v2/src/agent/goal/goalService.ts
index eb73b6216..e46fd9076 100644
--- a/packages/agent-core-v2/src/agent/goal/goalService.ts
+++ b/packages/agent-core-v2/src/agent/goal/goalService.ts
@@ -212,8 +212,8 @@ export class AgentGoalService extends Disposable implements IAgentGoalService {
);
this._register(
loopService.hooks.afterStep.register('goal-outcome-continuation', async (ctx, next) => {
- await next();
this.handleAfterStep(ctx);
+ await next();
}),
);
this._register(
@@ -467,7 +467,7 @@ export class AgentGoalService extends Disposable implements IAgentGoalService {
if (this.goalOutcomeContinuationTurns.has(ctx.turnId)) return;
if (!isGoalOutcomeReminder(this.context.get().at(-1))) return;
this.goalOutcomeContinuationTurns.add(ctx.turnId);
- ctx.continueTurn = true;
+ ctx.continue = true;
}
private async handleTurnEnded(ctx: TurnEndedContext): Promise {
diff --git a/packages/agent-core-v2/src/agent/loop/loop.ts b/packages/agent-core-v2/src/agent/loop/loop.ts
index 15c41abf7..570baf6e3 100644
--- a/packages/agent-core-v2/src/agent/loop/loop.ts
+++ b/packages/agent-core-v2/src/agent/loop/loop.ts
@@ -1,5 +1,5 @@
import { createDecorator } from '#/_base/di';
-import type { TokenUsage } from '#/app/llmProtocol';
+import type { FinishReason, TokenUsage } from '#/app/llmProtocol';
import type { Hooks } from '#/hooks';
import type { TurnResult } from './types';
@@ -12,7 +12,8 @@ export interface TurnBeforeStepContext {
export interface TurnAfterStepContext extends TurnBeforeStepContext {
readonly usage: TokenUsage;
- continueTurn: boolean;
+ readonly stopReason: FinishReason;
+ continue: boolean;
}
export interface TurnContextOverflowContext {
@@ -22,11 +23,6 @@ export interface TurnContextOverflowContext {
handled: boolean;
}
-export interface TurnWillStopContext {
- readonly signal: AbortSignal;
- continuationPrompt?: string;
-}
-
export interface RunTurnOptions {
readonly signal?: AbortSignal;
/** Fires on the first model response event for a step, or at step completion. */
@@ -39,7 +35,6 @@ export interface IAgentLoopService {
beforeStep: TurnBeforeStepContext;
afterStep: TurnAfterStepContext;
onContextOverflow: TurnContextOverflowContext;
- onWillStop: TurnWillStopContext;
}>;
runTurn(turnId: number, options?: RunTurnOptions): Promise;
}
diff --git a/packages/agent-core-v2/src/agent/loop/loopService.ts b/packages/agent-core-v2/src/agent/loop/loopService.ts
index fd0574f62..7b1920bea 100644
--- a/packages/agent-core-v2/src/agent/loop/loopService.ts
+++ b/packages/agent-core-v2/src/agent/loop/loopService.ts
@@ -28,7 +28,11 @@ import {
isAbortError,
isMaxStepsExceededError,
} from './errors';
-import { IAgentLoopService, type RunTurnOptions, type TurnWillStopContext } from './loop';
+import {
+ IAgentLoopService,
+ type RunTurnOptions,
+ type TurnAfterStepContext,
+} from './loop';
import type { LoopInterruptReason, TurnResult } from './types';
const TOOL_ERROR_STATUS = 'ERROR: Tool execution failed.';
@@ -44,7 +48,6 @@ export class AgentLoopService implements IAgentLoopService {
beforeStep: new OrderedHookSlot(),
afterStep: new OrderedHookSlot(),
onContextOverflow: new OrderedHookSlot(),
- onWillStop: new OrderedHookSlot(),
};
constructor(
@@ -64,62 +67,45 @@ export class AgentLoopService implements IAgentLoopService {
const signal = options.signal ?? new AbortController().signal;
this.profile.resolveModelContext();
+ let steps = 0;
+ let activeStep: number | undefined;
while (true) {
- let steps = 0;
- let activeStep: number | undefined;
-
try {
+ activeStep = undefined;
+ signal.throwIfAborted();
+
const maxSteps = this.config.get(LOOP_CONTROL_SECTION)?.maxStepsPerTurn;
- while (true) {
- signal.throwIfAborted();
- if (maxSteps !== undefined && maxSteps > 0 && steps >= maxSteps) {
- throw createMaxStepsExceededError(maxSteps);
- }
-
- steps += 1;
- activeStep = steps;
- const stepResult = await this.executeLoopStep(
- turnId,
- signal,
- steps,
- options.onStepStarted,
- );
- activeStep = undefined;
-
- if (stepResult.stopReason === 'filtered') {
- throw new KimiError(
- ErrorCodes.PROVIDER_FILTERED,
- 'Provider safety policy blocked the response.',
- {
- name: 'ProviderFilteredError',
- details: { finishReason: 'filtered' },
- },
- );
- }
-
- if (stepResult.stopReason === 'tool_calls') {
- continue;
- }
-
- if (stepResult.continueTurn) {
- 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;
+ if (maxSteps !== undefined && maxSteps > 0 && steps >= maxSteps) {
+ throw createMaxStepsExceededError(maxSteps);
}
+
+ steps += 1;
+ activeStep = steps;
+ const stepResult = await this.executeLoopStep(
+ turnId,
+ signal,
+ steps,
+ options.onStepStarted,
+ );
+ activeStep = undefined;
+
+ if (stepResult.stopReason === 'filtered') {
+ throw new KimiError(
+ ErrorCodes.PROVIDER_FILTERED,
+ 'Provider safety policy blocked the response.',
+ {
+ name: 'ProviderFilteredError',
+ details: { finishReason: 'filtered' },
+ },
+ );
+ }
+
+ if (stepResult.stopReason === 'tool_calls' || stepResult.continue) {
+ continue;
+ }
+
+ return { reason: 'completed', steps };
} catch (error) {
if (isAbortError(error) || signal.aborted) {
this.emitStepInterrupted(turnId, activeStep, 'aborted');
@@ -140,12 +126,13 @@ export class AgentLoopService implements IAgentLoopService {
} catch (hookError) {
return { reason: 'failed', error: hookError, steps };
}
- if (context.handled) continue;
+ if (context.handled) {
+ activeStep = undefined;
+ continue;
+ }
}
return { reason: 'failed', error, steps };
}
-
- return { reason: 'completed', steps };
}
}
@@ -156,7 +143,7 @@ export class AgentLoopService implements IAgentLoopService {
onStepStarted: ((step: number) => void) | undefined,
): Promise<{
readonly stopReason: FinishReason;
- readonly continueTurn: boolean;
+ readonly continue: boolean;
}> {
await this.hooks.beforeStep.run({ turnId, step: currentStep, signal });
signal.throwIfAborted();
@@ -237,16 +224,24 @@ export class AgentLoopService implements IAgentLoopService {
markStepStarted();
this.emitStepCompleted(turnId, currentStep, stepUuid, usage, finishReason, response);
- const afterStepContext = { turnId, step: currentStep, signal, usage, continueTurn: false };
+ const afterStepContext: TurnAfterStepContext = {
+ turnId,
+ step: currentStep,
+ signal,
+ usage,
+ stopReason: finishReason,
+ continue: false,
+ };
try {
await this.hooks.afterStep.run(afterStepContext);
- } catch {
+ } catch (error) {
+ if (isAbortError(error) || signal.aborted) throw error;
// afterStep hook failures must not affect the turn result.
}
return {
stopReason: finishReason,
- continueTurn: finishReason !== 'tool_calls' && afterStepContext.continueTurn,
+ continue: afterStepContext.continue,
};
}
diff --git a/packages/agent-core-v2/src/agent/prompt/promptService.ts b/packages/agent-core-v2/src/agent/prompt/promptService.ts
index ec9a76949..b4275221a 100644
--- a/packages/agent-core-v2/src/agent/prompt/promptService.ts
+++ b/packages/agent-core-v2/src/agent/prompt/promptService.ts
@@ -29,10 +29,10 @@ export class AgentPromptService implements IAgentPromptService {
await next();
});
loopService.hooks.afterStep.register('prompt-service-steer', async (ctx, next) => {
- await next();
if (this.flushSteerQueue()) {
- ctx.continueTurn = true;
+ ctx.continue = true;
}
+ await next();
});
}
diff --git a/packages/agent-core-v2/test/externalHooks/integration.test.ts b/packages/agent-core-v2/test/externalHooks/integration.test.ts
index e4c48d255..fdde0217b 100644
--- a/packages/agent-core-v2/test/externalHooks/integration.test.ts
+++ b/packages/agent-core-v2/test/externalHooks/integration.test.ts
@@ -7,6 +7,8 @@ import {
type TestInstantiationService,
} from '#/_base/di/test';
import { Event } from '#/_base/event';
+import { emptyUsage } from '#/app/llmProtocol/kosong';
+import { IAgentContextMemoryService, type ContextMessage } from '#/agent/contextMemory';
import { IAgentTaskService } from '#/agent/task';
import {
AgentExternalHooksService,
@@ -19,7 +21,7 @@ import {
hooksToToml,
} from '#/agent/externalHooks/configSection';
import { IAgentFullCompactionService } from '#/agent/fullCompaction';
-import { IAgentLoopService, type TurnWillStopContext } from '#/agent/loop';
+import { IAgentLoopService, type TurnAfterStepContext } from '#/agent/loop';
import { IAgentPermissionGate } from '#/agent/permissionGate';
import { IAgentToolExecutorService } from '#/agent/toolExecutor';
import { IAgentTurnService, type Turn } from '#/agent/turn';
@@ -55,6 +57,32 @@ function makeTurn(id: number): Turn {
};
}
+function makeAfterStep(signal: AbortSignal): TurnAfterStepContext {
+ return {
+ turnId: 0,
+ step: 1,
+ signal,
+ usage: emptyUsage(),
+ stopReason: 'completed',
+ continue: false,
+ };
+}
+
+function stubContextMemory(): IAgentContextMemoryService & {
+ readonly messages: readonly ContextMessage[];
+} {
+ const messages: ContextMessage[] = [];
+ return {
+ _serviceBrand: undefined,
+ get: () => [...messages],
+ splice: (start, deleteCount, inserted) => {
+ messages.splice(start, deleteCount, ...inserted);
+ },
+ hooks: createHooks(['onSpliced']) as IAgentContextMemoryService['hooks'],
+ messages,
+ };
+}
+
describe('HookEngine integration', () => {
it('blocks a dangerous Bash command and allows a safe one via a PreToolUse script hook', async () => {
const engine = new HookEngine([
@@ -110,6 +138,7 @@ describe('HookEngine integration', () => {
try {
const loop = stubLoopWithHooks();
const turnService = stubTurnWithHooks();
+ const context = stubContextMemory();
const stopInputs: unknown[] = [];
const hookEngine = {
trigger: async () => [],
@@ -126,6 +155,7 @@ describe('HookEngine integration', () => {
reg.defineInstance(IBootstrapService, stubBootstrap());
reg.definePartialInstance(IConfigService, {});
reg.definePartialInstance(IPluginService, {});
+ reg.defineInstance(IAgentContextMemoryService, context);
reg.defineInstance(IAgentLoopService, loop);
reg.defineInstance(IAgentTurnService, turnService);
reg.defineInstance(IAgentToolExecutorService, stubToolExecutor());
@@ -147,13 +177,29 @@ describe('HookEngine integration', () => {
ix.get(IAgentExternalHooksService);
const signal = new AbortController().signal;
- const first: TurnWillStopContext = { signal };
- await loop.hooks.onWillStop.run(first);
- expect(first.continuationPrompt).toBe('continue 1');
+ const filtered: TurnAfterStepContext = {
+ ...makeAfterStep(signal),
+ stopReason: 'filtered',
+ };
+ await loop.hooks.afterStep.run(filtered);
+ expect(filtered.continue).toBe(false);
+ expect(stopInputs).toEqual([]);
+ expect(context.messages).toEqual([]);
- const second: TurnWillStopContext = { signal };
- await loop.hooks.onWillStop.run(second);
- expect(second.continuationPrompt).toBeUndefined();
+ const first = makeAfterStep(signal);
+ await loop.hooks.afterStep.run(first);
+ expect(first.continue).toBe(true);
+ expect(context.messages.at(-1)).toEqual(
+ expect.objectContaining({
+ role: 'user',
+ content: [{ type: 'text', text: 'continue 1' }],
+ origin: { kind: 'system_trigger', name: 'stop_hook' },
+ }),
+ );
+
+ const second = makeAfterStep(signal);
+ await loop.hooks.afterStep.run(second);
+ expect(second.continue).toBe(false);
expect(stopInputs).toEqual([{ stopHookActive: false }]);
await turnService.hooks.onEnded.run({
@@ -161,9 +207,16 @@ describe('HookEngine integration', () => {
result: { reason: 'completed' },
});
- const nextTurn: TurnWillStopContext = { signal };
- await loop.hooks.onWillStop.run(nextTurn);
- expect(nextTurn.continuationPrompt).toBe('continue 2');
+ const nextTurn = makeAfterStep(signal);
+ await loop.hooks.afterStep.run(nextTurn);
+ expect(nextTurn.continue).toBe(true);
+ expect(context.messages.at(-1)).toEqual(
+ expect.objectContaining({
+ role: 'user',
+ content: [{ type: 'text', text: 'continue 2' }],
+ origin: { kind: 'system_trigger', name: 'stop_hook' },
+ }),
+ );
expect(stopInputs).toEqual([{ stopHookActive: false }, { stopHookActive: false }]);
} finally {
ix?.dispose();
diff --git a/packages/agent-core-v2/test/fullCompaction/full.test.ts b/packages/agent-core-v2/test/fullCompaction/full.test.ts
index c8b20e14b..e5fb49430 100644
--- a/packages/agent-core-v2/test/fullCompaction/full.test.ts
+++ b/packages/agent-core-v2/test/fullCompaction/full.test.ts
@@ -1703,6 +1703,54 @@ describe('FullCompaction', () => {
await ctx.expectResumeMatches();
});
+ it('does not reset the step budget after provider context overflow compaction', async () => {
+ let callCount = 0;
+ const generate: GenerateFn = async (_provider, _system, _tools, _history, callbacks) => {
+ callCount += 1;
+ if (callCount === 1) {
+ throw new APIContextOverflowError(400, 'Context length exceeded', 'req-budget-overflow');
+ }
+ if (callCount === 2) {
+ return textResult('Budget compacted summary.');
+ }
+ await callbacks?.onMessagePart?.({ type: 'text', text: 'Should not run.' });
+ return textResult('Should not run.');
+ };
+ const ctx = testAgent({
+ generate,
+ initialConfig: {
+ providers: {},
+ loopControl: { maxStepsPerTurn: 1 },
+ },
+ });
+ ctx.configure({
+ provider: CATALOGUED_PROVIDER,
+ modelCapabilities: CATALOGUED_MODEL_CAPABILITIES,
+ });
+ ctx.appendExchange(1, 'old user one', 'old assistant one', 20);
+ ctx.newEvents();
+
+ await ctx.rpc.prompt({ input: [{ type: 'text', text: 'Retry after provider overflow' }] });
+ const events = await ctx.untilTurnEnd();
+
+ expect(callCount).toBe(2);
+ expect(events).toContainEqual(
+ expect.objectContaining({
+ event: 'turn.ended',
+ args: expect.objectContaining({
+ reason: 'failed',
+ error: expect.objectContaining({
+ code: 'loop.max_steps_exceeded',
+ details: expect.objectContaining({
+ maxSteps: 1,
+ }),
+ }),
+ }),
+ }),
+ );
+ await ctx.expectResumeMatches();
+ });
+
it('preserves thinking effort when compacting after provider context overflow', async () => {
let callCount = 0;
const records: TelemetryRecord[] = [];
diff --git a/packages/agent-core-v2/test/goal/goal.test.ts b/packages/agent-core-v2/test/goal/goal.test.ts
index f0a94920f..64d9e9c47 100644
--- a/packages/agent-core-v2/test/goal/goal.test.ts
+++ b/packages/agent-core-v2/test/goal/goal.test.ts
@@ -3,7 +3,7 @@ import { afterEach, beforeEach, describe, expect, it } from 'vitest';
import { IAgentContextMemoryService } from '#/agent/contextMemory';
import { IAgentEventSinkService } from '#/agent/eventSink';
import { IAgentGoalService, type AgentGoalService } from '#/agent/goal';
-import { IAgentLoopService } from '#/agent/loop';
+import { IAgentLoopService, type TurnAfterStepContext } from '#/agent/loop';
import { IAgentRecordService } from '#/agent/record';
import { IAgentTurnService, type Turn, type TurnResult } from '#/agent/turn';
import type { PersistedWireRecord, WireRecord } from '#/agent/wireRecord';
@@ -63,16 +63,17 @@ async function runGoalStep(loopService: IAgentLoopService, turn: Turn): Promise<
step: 1,
signal: turn.abortController.signal,
};
- const afterStep = {
+ const afterStep: TurnAfterStepContext = {
turnId: turn.id,
step: 1,
signal: turn.abortController.signal,
usage: zeroUsage,
- continueTurn: false,
+ stopReason: 'completed' as const,
+ continue: false,
};
await loopService.hooks.beforeStep.run(step);
await loopService.hooks.afterStep.run(afterStep);
- return afterStep.continueTurn;
+ return afterStep.continue;
}
async function runStepUsageHooks(
@@ -81,12 +82,13 @@ async function runStepUsageHooks(
turn: Turn,
usage: TokenUsage,
): Promise {
- const afterStep = {
+ const afterStep: TurnAfterStepContext = {
turnId: turn.id,
step: 1,
signal: turn.abortController.signal,
usage,
- continueTurn: false,
+ stopReason: 'completed' as const,
+ continue: false,
};
await loopService.hooks.afterStep.run(afterStep);
return goals.getGoal().goal?.budget.overBudget === true;
@@ -664,12 +666,13 @@ describe('AgentGoalService core workflow hooks', () => {
step: 1,
signal: turn.abortController.signal,
};
- const afterStep = {
+ const afterStep: TurnAfterStepContext = {
turnId: turn.id,
step: 1,
signal: turn.abortController.signal,
usage: zeroUsage,
- continueTurn: false,
+ stopReason: 'completed' as const,
+ continue: false,
};
await loopService.hooks.beforeStep.run(step);
@@ -677,7 +680,7 @@ describe('AgentGoalService core workflow hooks', () => {
await loopService.hooks.afterStep.run(afterStep);
await endTurn(turnService, turn);
- expect(afterStep.continueTurn).toBe(true);
+ expect(afterStep.continue).toBe(true);
expect(goals.getGoal().goal).toBeNull();
expect(turnService.launches).toEqual([]);
expect(context.get().at(-1)?.origin).toEqual({
diff --git a/packages/agent-core-v2/test/loop/basic.test.ts b/packages/agent-core-v2/test/loop/basic.test.ts
index 3a3287aab..d5fcd5806 100644
--- a/packages/agent-core-v2/test/loop/basic.test.ts
+++ b/packages/agent-core-v2/test/loop/basic.test.ts
@@ -3,6 +3,7 @@ import { afterEach, beforeEach, describe, expect, it } from 'vitest';
import { IAgentProfileService } from '#/index';
import { IAgentLLMRequesterService, type LLMStreamTiming } from '#/agent/llmRequester';
+import { IAgentContextMemoryService } from '#/agent/contextMemory';
import { IAgentLoopService } from '#/agent/loop';
import type { ExecutableTool } from '#/agent/tool';
import { IAgentToolRegistryService } from '#/agent/toolRegistry';
@@ -183,10 +184,18 @@ describe('Agent loop', () => {
it('lets non-external stop hooks continue a turn more than once', async () => {
profile.update({ activeToolNames: [] });
let continuations = 0;
- loop.hooks.onWillStop.register('test-repeat-stop-continuation', async (hookCtx, next) => {
+ loop.hooks.afterStep.register('test-repeat-stop-continuation', async (hookCtx, next) => {
if (continuations < 2) {
continuations += 1;
- hookCtx.continuationPrompt = `continue ${continuations}`;
+ const prompt = `continue ${continuations}`;
+ const context = ctx.get(IAgentContextMemoryService);
+ context.splice(context.get().length, 0, [{
+ role: 'user',
+ content: [{ type: 'text', text: prompt }],
+ toolCalls: [],
+ origin: { kind: 'system_trigger', name: 'stop_hook' },
+ }]);
+ hookCtx.continue = true;
return;
}
await next();
diff --git a/packages/agent-core-v2/test/turn/stubs.ts b/packages/agent-core-v2/test/turn/stubs.ts
index ad8a80a99..5d953251d 100644
--- a/packages/agent-core-v2/test/turn/stubs.ts
+++ b/packages/agent-core-v2/test/turn/stubs.ts
@@ -53,7 +53,6 @@ function makeAgentLoopHookSlots(): IAgentLoopService['hooks'] {
'beforeStep',
'afterStep',
'onContextOverflow',
- 'onWillStop',
]) as IAgentLoopService['hooks'];
}