fix: remove duplicate SubagentStart hook execution in run() (#3747)

runStartHooks() and run() both called executeSubagentStartHook() for
subagent requests, causing SubagentStart hooks to fire twice per
subagent invocation. The caller (defaultIntentRequestHandler) always
calls runStartHooks() before run(), so the call in run() was redundant.

Remove the duplicate call from run() since runStartHooks() is the
correct place for start hook execution.
This commit is contained in:
Rob Lourens
2026-02-14 19:44:12 +00:00
committed by GitHub
parent 60d9375465
commit b0cdbf862f
2 changed files with 30 additions and 12 deletions
@@ -558,18 +558,6 @@ export abstract class ToolCallingLoop<TOptions extends IToolCallingLoopOptions =
let stopHookActive = false;
const sessionId = this.options.conversation.sessionId;
// Execute SubagentStart hook for subagent requests to get additional context
if (this.options.request.subAgentInvocationId) {
const startHookResult = await this.executeSubagentStartHook({
agent_id: this.options.request.subAgentInvocationId,
agent_type: this.options.request.subAgentName ?? 'default',
}, sessionId, outputStream, token);
if (startHookResult.additionalContext) {
this.additionalHookContext = startHookResult.additionalContext;
this._logService.info(`[ToolCallingLoop] SubagentStart hook provided context for subagent ${this.options.request.subAgentInvocationId}`);
}
}
while (true) {
if (lastResult && i++ >= this.options.toolCallLimit) {
lastResult = this.hitToolCallLimit(outputStream, lastResult);
@@ -614,6 +614,36 @@ describe('ToolCallingLoop SubagentStart hook', () => {
const input = subagentStartCalls[0].input as SubagentStartHookInput;
expect(input.agent_type).toBe('default');
});
it('should execute SubagentStart hook only once when runStartHooks and run are both called', async () => {
const conversation = createTestConversation(1);
const request = createMockChatRequest({
subAgentInvocationId: 'subagent-dedup',
subAgentName: 'DedupAgent',
} as Partial<ChatRequest>);
const loop = instantiationService.createInstance(
TestToolCallingLoop,
{
conversation,
toolCallLimit: 10,
request,
}
);
disposables.add(loop);
// First call: runStartHooks should execute SubagentStart once
await loop.testRunStartHooks(tokenSource.token);
// Second call: run() should NOT execute SubagentStart again
// run() will throw because fetch() is not implemented, but SubagentStart
// happens before fetch, so we need to verify it wasn't called again
await expect(loop.run(undefined, tokenSource.token)).rejects.toThrow();
// SubagentStart should have been called exactly once (from runStartHooks only)
const subagentStartCalls = mockChatHookService.getCallsForHook('SubagentStart');
expect(subagentStartCalls).toHaveLength(1);
});
});
describe('SubagentStart hook result collection', () => {