refactor: integrate SessionManager into daemon and agent

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
William Valentin
2026-02-05 00:43:09 -08:00
parent f671ea5ab5
commit fb7575f850
3 changed files with 78 additions and 86 deletions
+45 -55
View File
@@ -1,79 +1,69 @@
import { describe, it, expect, vi } from 'vitest'; import { describe, it, expect, vi, beforeEach } from 'vitest';
import { NativeAgent } from './agent.js'; import { NativeAgent } from './agent.js';
import type { ModelClient, ChatResponse } from '../../models/types.js'; import type { ModelClient, ChatResponse } from '../../models/types.js';
describe('NativeAgent', () => { describe('NativeAgent', () => {
it('processes message and returns response', async () => { const createMockClient = (): ModelClient => ({
const mockClient: ModelClient = { chat: vi.fn().mockResolvedValue({
chat: vi.fn().mockResolvedValue({ content: 'Hello!',
content: 'Hello! How can I help you?', stopReason: 'end_turn',
stopReason: 'end_turn', usage: { inputTokens: 10, outputTokens: 5 },
usage: { inputTokens: 10, outputTokens: 8 }, } satisfies ChatResponse),
} satisfies ChatResponse),
};
const agent = new NativeAgent({
modelClient: mockClient,
systemPrompt: 'You are Flynn, a helpful assistant.',
});
const response = await agent.process('Hello');
expect(response).toBe('Hello! How can I help you?');
expect(mockClient.chat).toHaveBeenCalledWith({
messages: [{ role: 'user', content: 'Hello' }],
system: 'You are Flynn, a helpful assistant.',
});
}); });
it('maintains conversation history', async () => { it('processes messages and maintains history', async () => {
const mockClient: ModelClient = { const mockClient = createMockClient();
chat: vi.fn().mockResolvedValue({
content: 'Response',
stopReason: 'end_turn',
usage: { inputTokens: 10, outputTokens: 5 },
} satisfies ChatResponse),
};
const agent = new NativeAgent({ const agent = new NativeAgent({
modelClient: mockClient, modelClient: mockClient,
systemPrompt: 'System', systemPrompt: 'You are helpful.',
}); });
await agent.process('First message'); const response = await agent.process('Hi');
await agent.process('Second message');
expect(mockClient.chat).toHaveBeenLastCalledWith({ expect(response).toBe('Hello!');
messages: [ expect(mockClient.chat).toHaveBeenCalledWith({
{ role: 'user', content: 'First message' }, messages: [{ role: 'user', content: 'Hi' }],
{ role: 'assistant', content: 'Response' }, system: 'You are helpful.',
{ role: 'user', content: 'Second message' },
],
system: 'System',
}); });
const history = agent.getHistory();
expect(history).toHaveLength(2);
expect(history[0]).toEqual({ role: 'user', content: 'Hi' });
expect(history[1]).toEqual({ role: 'assistant', content: 'Hello!' });
}); });
it('resets conversation history', async () => { it('resets conversation history', async () => {
const mockClient: ModelClient = { const mockClient = createMockClient();
chat: vi.fn().mockResolvedValue({ const agent = new NativeAgent({
content: 'Response', modelClient: mockClient,
stopReason: 'end_turn', systemPrompt: 'You are helpful.',
usage: { inputTokens: 10, outputTokens: 5 }, });
} satisfies ChatResponse),
await agent.process('Hi');
agent.reset();
expect(agent.getHistory()).toHaveLength(0);
});
it('uses session when provided', async () => {
const mockClient = createMockClient();
const mockSession = {
id: 'test-session',
getHistory: vi.fn().mockReturnValue([]),
addMessage: vi.fn(),
clear: vi.fn(),
}; };
const agent = new NativeAgent({ const agent = new NativeAgent({
modelClient: mockClient, modelClient: mockClient,
systemPrompt: 'System', systemPrompt: 'You are helpful.',
session: mockSession,
}); });
await agent.process('Message 1'); await agent.process('Hi');
agent.reset();
await agent.process('Message 2');
expect(mockClient.chat).toHaveBeenLastCalledWith({ expect(mockSession.addMessage).toHaveBeenCalledTimes(2);
messages: [{ role: 'user', content: 'Message 2' }], expect(mockSession.addMessage).toHaveBeenNthCalledWith(1, { role: 'user', content: 'Hi' });
system: 'System', expect(mockSession.addMessage).toHaveBeenNthCalledWith(2, { role: 'assistant', content: 'Hello!' });
});
}); });
}); });
+21 -22
View File
@@ -1,59 +1,58 @@
import type { ModelClient, Message } from '../../models/types.js'; import type { ModelClient, Message } from '../../models/types.js';
import type { SessionStore } from '../../session/index.js'; import type { Session } from '../../session/index.js';
export interface NativeAgentConfig { export interface NativeAgentConfig {
modelClient: ModelClient; modelClient: ModelClient;
systemPrompt: string; systemPrompt: string;
sessionStore?: SessionStore; session?: Session;
sessionId?: string;
} }
export class NativeAgent { export class NativeAgent {
private modelClient: ModelClient; private modelClient: ModelClient;
private systemPrompt: string; private systemPrompt: string;
private sessionStore?: SessionStore; private session?: Session;
private sessionId: string; private inMemoryHistory: Message[] = [];
private history: Message[] = [];
constructor(config: NativeAgentConfig) { constructor(config: NativeAgentConfig) {
this.modelClient = config.modelClient; this.modelClient = config.modelClient;
this.systemPrompt = config.systemPrompt; this.systemPrompt = config.systemPrompt;
this.sessionStore = config.sessionStore; this.session = config.session;
this.sessionId = config.sessionId ?? 'default'; }
// Load existing history from store private get history(): Message[] {
if (this.sessionStore) { return this.session?.getHistory() ?? [...this.inMemoryHistory];
this.history = this.sessionStore.getMessages(this.sessionId);
}
} }
async process(userMessage: string): Promise<string> { async process(userMessage: string): Promise<string> {
const userMsg: Message = { role: 'user', content: userMessage }; const userMsg: Message = { role: 'user', content: userMessage };
this.history.push(userMsg);
if (this.sessionStore) { if (this.session) {
this.sessionStore.addMessage(this.sessionId, userMsg); this.session.addMessage(userMsg);
} else {
this.inMemoryHistory.push(userMsg);
} }
const response = await this.modelClient.chat({ const response = await this.modelClient.chat({
messages: [...this.history], messages: this.history,
system: this.systemPrompt, system: this.systemPrompt,
}); });
const assistantMsg: Message = { role: 'assistant', content: response.content }; const assistantMsg: Message = { role: 'assistant', content: response.content };
this.history.push(assistantMsg);
if (this.sessionStore) { if (this.session) {
this.sessionStore.addMessage(this.sessionId, assistantMsg); this.session.addMessage(assistantMsg);
} else {
this.inMemoryHistory.push(assistantMsg);
} }
return response.content; return response.content;
} }
reset(): void { reset(): void {
this.history = []; if (this.session) {
if (this.sessionStore) { this.session.clear();
this.sessionStore.clearSession(this.sessionId); } else {
this.inMemoryHistory = [];
} }
} }
+12 -9
View File
@@ -4,7 +4,7 @@ import type { Config } from '../config/index.js';
import { AnthropicClient, OpenAIClient, OllamaClient, ModelRouter } from '../models/index.js'; import { AnthropicClient, OpenAIClient, OllamaClient, ModelRouter } from '../models/index.js';
import { NativeAgent } from '../backends/index.js'; import { NativeAgent } from '../backends/index.js';
import { createTelegramBot } from '../frontends/telegram/index.js'; import { createTelegramBot } from '../frontends/telegram/index.js';
import { SessionStore } from '../session/index.js'; import { SessionStore, SessionManager } from '../session/index.js';
import { HookEngine } from '../hooks/index.js'; import { HookEngine } from '../hooks/index.js';
import { resolve } from 'path'; import { resolve } from 'path';
import { homedir } from 'os'; import { homedir } from 'os';
@@ -16,6 +16,7 @@ export interface DaemonContext {
bot: Bot; bot: Bot;
agent: NativeAgent; agent: NativeAgent;
sessionStore: SessionStore; sessionStore: SessionStore;
sessionManager: SessionManager;
hookEngine: HookEngine; hookEngine: HookEngine;
modelRouter: ModelRouter; modelRouter: ModelRouter;
} }
@@ -27,12 +28,10 @@ Keep responses focused and avoid unnecessary verbosity. Use markdown formatting
function createModelRouter(config: Config): ModelRouter { function createModelRouter(config: Config): ModelRouter {
const models = config.models; const models = config.models;
// Create default client (required)
const defaultClient = new AnthropicClient({ const defaultClient = new AnthropicClient({
model: models.default.model, model: models.default.model,
}); });
// Create optional tier clients
let fastClient; let fastClient;
let complexClient; let complexClient;
let localClient; let localClient;
@@ -54,7 +53,6 @@ function createModelRouter(config: Config): ModelRouter {
} }
} }
// Build fallback chain
const fallbackChain = []; const fallbackChain = [];
for (const providerName of models.fallback_chain) { for (const providerName of models.fallback_chain) {
if (providerName === 'openai') { if (providerName === 'openai') {
@@ -80,8 +78,10 @@ export async function startDaemon(config: Config): Promise<DaemonContext> {
const dataDir = resolve(homedir(), '.local/share/flynn'); const dataDir = resolve(homedir(), '.local/share/flynn');
mkdirSync(dataDir, { recursive: true }); mkdirSync(dataDir, { recursive: true });
// Initialize session store // Initialize session store and manager
const sessionStore = new SessionStore(resolve(dataDir, 'sessions.db')); const sessionStore = new SessionStore(resolve(dataDir, 'sessions.db'));
const sessionManager = new SessionManager(sessionStore);
lifecycle.onShutdown(async () => { lifecycle.onShutdown(async () => {
sessionStore.close(); sessionStore.close();
console.log('Session store closed'); console.log('Session store closed');
@@ -93,12 +93,15 @@ export async function startDaemon(config: Config): Promise<DaemonContext> {
// Initialize model router // Initialize model router
const modelRouter = createModelRouter(config); const modelRouter = createModelRouter(config);
// Initialize native agent with session persistence // Get Telegram session
const telegramUserId = String(config.telegram.allowed_chat_ids[0]);
const session = sessionManager.getSession('telegram', telegramUserId);
// Initialize native agent with session
const agent = new NativeAgent({ const agent = new NativeAgent({
modelClient: modelRouter, modelClient: modelRouter,
systemPrompt: SYSTEM_PROMPT, systemPrompt: SYSTEM_PROMPT,
sessionStore, session,
sessionId: `telegram-${config.telegram.allowed_chat_ids[0]}`,
}); });
// Initialize Telegram bot with hook engine // Initialize Telegram bot with hook engine
@@ -136,7 +139,7 @@ export async function startDaemon(config: Config): Promise<DaemonContext> {
console.log('Flynn daemon started'); console.log('Flynn daemon started');
return { config, lifecycle, bot, agent, sessionStore, hookEngine, modelRouter }; return { config, lifecycle, bot, agent, sessionStore, sessionManager, hookEngine, modelRouter };
} }
export { Lifecycle } from './lifecycle.js'; export { Lifecycle } from './lifecycle.js';