Skip to content
Merged
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -505,6 +505,112 @@ describe('NumericalClassifierStrategy', () => {
expect(contents).toHaveLength(9);
});

it('should adjust slice boundary to avoid severing a functionResponse from its preceding functionCall', async () => {
const history: Content[] = [
{ role: 'user', parts: [{ text: 'initial request' }] },
{ role: 'model', parts: [{ functionCall: { name: 'test_tool' } }] },
{
role: 'user',
parts: [
{ functionResponse: { name: 'test_tool', response: { ok: true } } },
],
},
{ role: 'model', parts: [{ text: 'tool output analyzed' }] },
{ role: 'user', parts: [{ text: 'next step' }] },
{ role: 'model', parts: [{ text: 'working on it' }] },
{ role: 'user', parts: [{ text: 'almost done?' }] },
{ role: 'model', parts: [{ text: 'yes' }] },
{ role: 'user', parts: [{ text: 'final check' }] },
{ role: 'model', parts: [{ text: 'all good' }] },
];
mockContext.history = history;
const mockApiResponse = {
complexity_reasoning: 'Simple.',
complexity_score: 10,
};
vi.mocked(mockBaseLlmClient.generateJson).mockResolvedValue(
mockApiResponse,
);

await strategy.route(
mockContext,
mockConfig,
mockBaseLlmClient,
mockLocalLiteRtLmClient,
);

const generateJsonCall = vi.mocked(mockBaseLlmClient.generateJson).mock
.calls[0][0];
const contents = generateJsonCall.contents;

// Expect it to start at index 1 (functionCall) rather than index 2 (functionResponse)
const expectedContents = [
...history.slice(1),
{
role: 'user',
parts: [{ text: 'simple task' }],
},
];

expect(contents).toEqual(expectedContents);
});

it('should adjust slice boundary correctly even when the preceding functionCall turn contains mixed parts (text + functionCall)', async () => {
const history: Content[] = [
{ role: 'user', parts: [{ text: 'initial request' }] },
{
role: 'model',
parts: [
{ text: 'thinking about which tool to call...' },
{ functionCall: { name: 'test_tool' } },
],
},
{
role: 'user',
parts: [
{ functionResponse: { name: 'test_tool', response: { ok: true } } },
],
},
{ role: 'model', parts: [{ text: 'tool output analyzed' }] },
{ role: 'user', parts: [{ text: 'next step' }] },
{ role: 'model', parts: [{ text: 'working on it' }] },
{ role: 'user', parts: [{ text: 'almost done?' }] },
{ role: 'model', parts: [{ text: 'yes' }] },
{ role: 'user', parts: [{ text: 'final check' }] },
{ role: 'model', parts: [{ text: 'all good' }] },
];
mockContext.history = history;
const mockApiResponse = {
complexity_reasoning: 'Simple.',
complexity_score: 10,
};
vi.mocked(mockBaseLlmClient.generateJson).mockResolvedValue(
mockApiResponse,
);

await strategy.route(
mockContext,
mockConfig,
mockBaseLlmClient,
mockLocalLiteRtLmClient,
);

const generateJsonCall = vi.mocked(mockBaseLlmClient.generateJson).mock
.calls[0][0];
const contents = generateJsonCall.contents;

// Expect it to start at index 1 (mixed functionCall turn) rather than index 2 (functionResponse)
const expectedContents = [
...history.slice(1),
{
role: 'user',
parts: [{ text: 'simple task' }],
},
];

expect(contents).toEqual(expectedContents);
});

it('should use a fallback promptId if not found in context', async () => {
const consoleWarnSpy = vi
.spyOn(debugLogger, 'warn')
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -115,7 +115,22 @@ export class NumericalClassifierStrategy implements RoutingStrategy {

const promptId = getPromptIdWithFallback('classifier-router');

const finalHistory = context.history.slice(-HISTORY_TURNS_FOR_CONTEXT);
let startIndex = Math.max(
0,
context.history.length - HISTORY_TURNS_FOR_CONTEXT,
);
// Ensure we don't sever a functionResponse from its preceding functionCall
while (
startIndex > 0 &&
startIndex < context.history.length &&
context.history[startIndex].role === 'user' &&
context.history[startIndex].parts?.some((p) => !!p.functionResponse) &&
context.history[startIndex - 1].role === 'model' &&
context.history[startIndex - 1].parts?.some((p) => !!p.functionCall)
) {
startIndex--;
}
const finalHistory = context.history.slice(startIndex);

// Wrap the user's request in tags to prevent prompt injection
const requestParts = Array.isArray(context.request)
Expand Down