fix: normalize message roles for local model backends (llama.cpp, Ollama)
Local backends using strict chat templates (e.g. Mistral 3) rejected Flynn's Anthropic-style tool_use/tool_result content blocks, causing 'roles must alternate' errors. Added getMessageTextWithTools() and normalizeMessagesForLocal() to serialize structured blocks to plain text, drop empty messages, and merge consecutive same-role messages. Also fixed compaction to ensure kept messages start with user role.
This commit is contained in:
@@ -101,4 +101,23 @@ describe('compactHistory', () => {
|
||||
expect(DEFAULT_COMPACTION_CONFIG.keepTurns).toBe(4);
|
||||
expect(DEFAULT_COMPACTION_CONFIG.summaryMaxTokens).toBe(1024);
|
||||
});
|
||||
|
||||
it('shifts leading assistant messages from toKeep into toCompact to ensure user-first', async () => {
|
||||
// 9 messages: user(0), assistant(1), ..., user(8)
|
||||
// With keepTurns=2 (keepCount=4), toKeep starts as [assistant(5), user(6), assistant(7), user(8)]
|
||||
// The fix shifts assistant(5) into toCompact, so toKeep = [user(6), assistant(7), user(8)]
|
||||
const messages = makeMessages(9);
|
||||
const orchestrator = makeMockOrchestrator();
|
||||
|
||||
const result = await compactHistory({ messages, orchestrator, config });
|
||||
|
||||
// summary(assistant) + user(6) + assistant(7) + user(8) = 4 messages
|
||||
expect(result.messages).toHaveLength(4);
|
||||
expect(result.compactedCount).toBe(6);
|
||||
// First is summary (assistant), second must be user
|
||||
expect(result.messages[0].role).toBe('assistant');
|
||||
expect(result.messages[0].content).toContain('[Summary of earlier conversation]');
|
||||
expect(result.messages[1].role).toBe('user');
|
||||
expect(result.messages[1].content).toBe('Message 6');
|
||||
});
|
||||
});
|
||||
|
||||
@@ -53,6 +53,12 @@ export async function compactHistory(opts: {
|
||||
const toCompact = messages.slice(0, -keepCount);
|
||||
const toKeep = messages.slice(-keepCount);
|
||||
|
||||
// Ensure toKeep starts with a user message to avoid assistant→assistant
|
||||
// after the compaction summary (which has role 'assistant').
|
||||
while (toKeep.length > 0 && toKeep[0].role === 'assistant') {
|
||||
toCompact.push(toKeep.shift()!);
|
||||
}
|
||||
|
||||
const formattedConversation = toCompact.map((msg) => `${msg.role}: ${getMessageText(msg)}`).join('\n\n');
|
||||
|
||||
const tier = orchestrator.getDelegationTier('compaction');
|
||||
|
||||
Reference in New Issue
Block a user