feat(core): experimental in-progress steering hints (2 of 2) (#19307)

This commit is contained in:
joshualitt
2026-02-18 14:05:50 -08:00
committed by GitHub
parent 81c8893e05
commit 87f5dd15d6
37 changed files with 1280 additions and 48 deletions
@@ -2037,6 +2037,215 @@ describe('LocalAgentExecutor', () => {
expect(recoveryEvent.success).toBe(true);
expect(recoveryEvent.reason).toBe(AgentTerminateMode.MAX_TURNS);
});
describe('Model Steering', () => {
let configWithHints: Config;
beforeEach(() => {
configWithHints = makeFakeConfig({ modelSteering: true });
vi.spyOn(configWithHints, 'getAgentRegistry').mockReturnValue({
getAllAgentNames: () => [],
} as unknown as AgentRegistry);
vi.spyOn(configWithHints, 'getToolRegistry').mockReturnValue(
parentToolRegistry,
);
});
it('should inject user hints into the next turn after they are added', async () => {
const definition = createTestDefinition();
const executor = await LocalAgentExecutor.create(
definition,
configWithHints,
);
// Turn 1: Model calls LS
mockModelResponse(
[{ name: LS_TOOL_NAME, args: { path: '.' }, id: 'call1' }],
'T1: Listing',
);
// We use a manual promise to ensure the hint is added WHILE Turn 1 is "running"
let resolveToolCall: (value: unknown) => void;
const toolCallPromise = new Promise((resolve) => {
resolveToolCall = resolve;
});
mockScheduleAgentTools.mockReturnValueOnce(toolCallPromise);
// Turn 2: Model calls complete_task
mockModelResponse(
[
{
name: TASK_COMPLETE_TOOL_NAME,
args: { finalResult: 'Done' },
id: 'call2',
},
],
'T2: Done',
);
const runPromise = executor.run({ goal: 'Hint test' }, signal);
// Give the loop a chance to start and register the listener
await vi.advanceTimersByTimeAsync(1);
configWithHints.userHintService.addUserHint('Initial Hint');
// Resolve the tool call to complete Turn 1
resolveToolCall!([
{
status: 'success',
request: {
callId: 'call1',
name: LS_TOOL_NAME,
args: { path: '.' },
isClientInitiated: false,
prompt_id: 'p1',
},
tool: {} as AnyDeclarativeTool,
invocation: {} as AnyToolInvocation,
response: {
callId: 'call1',
resultDisplay: 'file1.txt',
responseParts: [
{
functionResponse: {
name: LS_TOOL_NAME,
response: { result: 'file1.txt' },
id: 'call1',
},
},
],
},
},
]);
await runPromise;
// The first call to sendMessageStream should NOT contain the hint (it was added after start)
// The SECOND call to sendMessageStream SHOULD contain the hint
expect(mockSendMessageStream).toHaveBeenCalledTimes(2);
const secondTurnMessageParts = mockSendMessageStream.mock.calls[1][1];
expect(secondTurnMessageParts).toContainEqual(
expect.objectContaining({
text: expect.stringContaining('Initial Hint'),
}),
);
});
it('should NOT inject legacy hints added before executor was created', async () => {
const definition = createTestDefinition();
configWithHints.userHintService.addUserHint('Legacy Hint');
const executor = await LocalAgentExecutor.create(
definition,
configWithHints,
);
mockModelResponse([
{
name: TASK_COMPLETE_TOOL_NAME,
args: { finalResult: 'Done' },
id: 'call1',
},
]);
await executor.run({ goal: 'Isolation test' }, signal);
// The first call to sendMessageStream should NOT contain the legacy hint
expect(mockSendMessageStream).toHaveBeenCalled();
const firstTurnMessageParts = mockSendMessageStream.mock.calls[0][1];
// We expect only the goal, no hints injected at turn start
for (const part of firstTurnMessageParts) {
if (part.text) {
expect(part.text).not.toContain('Legacy Hint');
}
}
});
it('should inject mid-execution hints into subsequent turns', async () => {
const definition = createTestDefinition();
const executor = await LocalAgentExecutor.create(
definition,
configWithHints,
);
// Turn 1: Model calls LS
mockModelResponse(
[{ name: LS_TOOL_NAME, args: { path: '.' }, id: 'call1' }],
'T1: Listing',
);
// We use a manual promise to ensure the hint is added WHILE Turn 1 is "running"
let resolveToolCall: (value: unknown) => void;
const toolCallPromise = new Promise((resolve) => {
resolveToolCall = resolve;
});
mockScheduleAgentTools.mockReturnValueOnce(toolCallPromise);
// Turn 2: Model calls complete_task
mockModelResponse(
[
{
name: TASK_COMPLETE_TOOL_NAME,
args: { finalResult: 'Done' },
id: 'call2',
},
],
'T2: Done',
);
// Start execution
const runPromise = executor.run({ goal: 'Mid-turn hint test' }, signal);
// Small delay to ensure the run loop has reached the await and registered listener
await vi.advanceTimersByTimeAsync(1);
// Add the hint while the tool call is pending
configWithHints.userHintService.addUserHint('Corrective Hint');
// Now resolve the tool call to complete Turn 1
resolveToolCall!([
{
status: 'success',
request: {
callId: 'call1',
name: LS_TOOL_NAME,
args: { path: '.' },
isClientInitiated: false,
prompt_id: 'p1',
},
tool: {} as AnyDeclarativeTool,
invocation: {} as AnyToolInvocation,
response: {
callId: 'call1',
resultDisplay: 'file1.txt',
responseParts: [
{
functionResponse: {
name: LS_TOOL_NAME,
response: { result: 'file1.txt' },
id: 'call1',
},
},
],
},
},
]);
await runPromise;
expect(mockSendMessageStream).toHaveBeenCalledTimes(2);
// The second turn (turn 1) should contain the corrective hint.
const secondTurnMessageParts = mockSendMessageStream.mock.calls[1][1];
expect(secondTurnMessageParts).toContainEqual(
expect.objectContaining({
text: expect.stringContaining('Corrective Hint'),
}),
);
});
});
});
describe('Chat Compression', () => {
const mockWorkResponse = (id: string) => {