feat(session): persist model tier overrides per session
Store per-session config in SQLite and route /model and /reset through command fast-paths so channel sessions keep independent model selection across reconnects and restarts.
This commit is contained in:
@@ -4,7 +4,10 @@ import { ModelRouter } from '../../models/router.js';
|
||||
import type { ChatResponse, ModelClient } from '../../models/types.js';
|
||||
import { ToolRegistry, ToolExecutor } from '../../tools/index.js';
|
||||
import { HookEngine } from '../../hooks/engine.js';
|
||||
import type { SubAgentRequest } from './orchestrator.js';
|
||||
import { MemoryStore } from '../../memory/store.js';
|
||||
import { mkdtempSync, rmSync } from 'fs';
|
||||
import { tmpdir } from 'os';
|
||||
import { join } from 'path';
|
||||
|
||||
describe('AgentOrchestrator', () => {
|
||||
let mockDefaultClient: ModelClient;
|
||||
@@ -33,6 +36,14 @@ describe('AgentOrchestrator', () => {
|
||||
});
|
||||
});
|
||||
|
||||
const requireClient = (tier: 'default' | 'fast' | 'complex'): ModelClient => {
|
||||
const client = mockRouter.getClient(tier);
|
||||
if (!client) {
|
||||
throw new Error(`Expected ${tier} model client to exist in test router`);
|
||||
}
|
||||
return client;
|
||||
};
|
||||
|
||||
describe('delegate()', () => {
|
||||
it('routes to the correct tier when specified', async () => {
|
||||
const orchestrator = new AgentOrchestrator({
|
||||
@@ -69,7 +80,7 @@ describe('AgentOrchestrator', () => {
|
||||
});
|
||||
const mockToolExecutor = new ToolExecutor(mockToolRegistry, hooks);
|
||||
|
||||
const mockFastChatClient = mockRouter.getClient('fast')!;
|
||||
const mockFastChatClient = requireClient('fast');
|
||||
const mockFastChatFn = vi.fn().mockResolvedValue({
|
||||
content: 'response with tools',
|
||||
stopReason: 'end_turn',
|
||||
@@ -298,7 +309,7 @@ describe('AgentOrchestrator', () => {
|
||||
|
||||
describe('process()', () => {
|
||||
it('proxies to NativeAgent for user messages', async () => {
|
||||
const mockDefaultChatClient = mockRouter.getClient('default')!;
|
||||
const mockDefaultChatClient = requireClient('default');
|
||||
const mockDefaultChatFn = vi.fn().mockResolvedValue({
|
||||
content: 'Agent response',
|
||||
stopReason: 'end_turn',
|
||||
@@ -355,6 +366,88 @@ describe('AgentOrchestrator', () => {
|
||||
expect(history[4]).toEqual({ role: 'user', content: 'Tell me about yourself' });
|
||||
expect(history[5]).toEqual({ role: 'assistant', content: 'default response' });
|
||||
});
|
||||
|
||||
it('uses adaptive memory injection strategy when configured', async () => {
|
||||
const tempDir = mkdtempSync(join(tmpdir(), 'flynn-orchestrator-memory-'));
|
||||
const memoryStore = new MemoryStore({ dir: tempDir, maxContextTokens: 2000 });
|
||||
memoryStore.writeCategory('user', 'preferences', 'User prefers concise output.', 'replace');
|
||||
|
||||
const mockDefaultChatClient = requireClient('default');
|
||||
const mockDefaultChatFn = vi.fn().mockResolvedValue({
|
||||
content: 'Agent response',
|
||||
stopReason: 'end_turn',
|
||||
usage: { inputTokens: 50, outputTokens: 25 },
|
||||
} as ChatResponse);
|
||||
Object.assign(mockDefaultChatClient, { chat: mockDefaultChatFn });
|
||||
|
||||
const orchestrator = new AgentOrchestrator({
|
||||
modelRouter: mockRouter,
|
||||
systemPrompt: 'You are a helpful agent.',
|
||||
primaryTier: 'default',
|
||||
delegation: {
|
||||
compaction: 'fast',
|
||||
memory_extraction: 'default',
|
||||
classification: 'complex',
|
||||
tool_summarisation: 'default',
|
||||
complex_reasoning: 'complex',
|
||||
},
|
||||
maxDelegationDepth: 10,
|
||||
memoryStore,
|
||||
memoryInjectionStrategy: 'adaptive',
|
||||
memoryMaxInjectionTokens: 100,
|
||||
});
|
||||
|
||||
await orchestrator.process('Keep this concise please');
|
||||
|
||||
expect(mockDefaultChatFn).toHaveBeenCalled();
|
||||
const callArgs = mockDefaultChatFn.mock.calls[0][0];
|
||||
expect(callArgs.system).toContain('# Memory Context');
|
||||
expect(callArgs.system).toContain('concise');
|
||||
|
||||
rmSync(tempDir, { recursive: true, force: true });
|
||||
});
|
||||
|
||||
it('falls back to default memory context when adaptive injection errors', async () => {
|
||||
const tempDir = mkdtempSync(join(tmpdir(), 'flynn-orchestrator-memory-fallback-'));
|
||||
const memoryStore = new MemoryStore({ dir: tempDir, maxContextTokens: 2000 });
|
||||
memoryStore.write('user', 'Fallback memory content', 'replace');
|
||||
const getPromptSectionsSpy = vi.spyOn(memoryStore, 'getPromptSections').mockImplementationOnce(() => {
|
||||
throw new Error('boom');
|
||||
});
|
||||
|
||||
const mockDefaultChatClient = requireClient('default');
|
||||
const mockDefaultChatFn = vi.fn().mockResolvedValue({
|
||||
content: 'Agent response',
|
||||
stopReason: 'end_turn',
|
||||
usage: { inputTokens: 50, outputTokens: 25 },
|
||||
} as ChatResponse);
|
||||
Object.assign(mockDefaultChatClient, { chat: mockDefaultChatFn });
|
||||
|
||||
const orchestrator = new AgentOrchestrator({
|
||||
modelRouter: mockRouter,
|
||||
systemPrompt: 'You are a helpful agent.',
|
||||
primaryTier: 'default',
|
||||
delegation: {
|
||||
compaction: 'fast',
|
||||
memory_extraction: 'default',
|
||||
classification: 'complex',
|
||||
tool_summarisation: 'default',
|
||||
complex_reasoning: 'complex',
|
||||
},
|
||||
maxDelegationDepth: 10,
|
||||
memoryStore,
|
||||
memoryInjectionStrategy: 'adaptive',
|
||||
memoryMaxInjectionTokens: 100,
|
||||
});
|
||||
|
||||
await orchestrator.process('test message');
|
||||
|
||||
const callArgs = mockDefaultChatFn.mock.calls[0][0];
|
||||
expect(callArgs.system).toContain('Fallback memory content');
|
||||
|
||||
getPromptSectionsSpy.mockRestore();
|
||||
rmSync(tempDir, { recursive: true, force: true });
|
||||
});
|
||||
});
|
||||
|
||||
describe('reset()', () => {
|
||||
|
||||
Reference in New Issue
Block a user