diff --git a/CLAUDE.md b/CLAUDE.md index 0b63607b4..b63a74665 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -21,19 +21,8 @@ This file provides guidance to Claude Code (claude.ai/code) when working with co ### Testing & Quality +- **Build Check**: `yarn build:check` - Checks build including type checking, it's REQUIRED before commits - **Run Tests**: `yarn test` - Runs all tests (Vitest) -- **Run E2E Tests**: `yarn test:e2e` - Playwright end-to-end tests -- **Type Check**: `yarn typecheck` - Checks TypeScript for both node and web -- **Lint**: `yarn lint` - ESLint with auto-fix -- **Format**: `yarn format` - Prettier formatting - -### Build & Release - -- **Build**: `yarn build` - Builds for production (includes typecheck) -- **Platform-specific builds**: - - Windows: `yarn build:win` - - macOS: `yarn build:mac` - - Linux: `yarn build:linux` ## Architecture Overview diff --git a/src/main/apiServer/routes/session-logs.ts b/src/main/apiServer/routes/session-logs.ts index dec733250..825910469 100644 --- a/src/main/apiServer/routes/session-logs.ts +++ b/src/main/apiServer/routes/session-logs.ts @@ -2,6 +2,8 @@ import express, { Request, Response } from 'express' import { body, param, query, validationResult } from 'express-validator' import { agentService } from '../../services/agents/AgentService' +import { sessionLogService } from '../../services/agents/SessionLogService' +import { sessionService } from '../../services/agents/SessionService' import { loggerService } from '../../services/LoggerService' const logger = loggerService.withContext('ApiServerSessionLogsRoutes') @@ -75,7 +77,7 @@ const checkAgentAndSessionExist = async (req: Request, res: Response, next: any) return } - const session = await agentService.getSession(sessionId) + const session = await sessionService.getSession(sessionId) if (!session) { res.status(404).json({ error: { @@ -251,7 +253,7 @@ function createSessionLogsRouter(): express.Router { logger.info(`Creating new log entry for session: ${sessionId}`) logger.debug('Log data:', logData) - const log = await agentService.createSessionLog(logData) + const log = await sessionLogService.createSessionLog(logData) logger.info(`Log entry created successfully: ${log.id}`) return res.status(201).json(log) @@ -344,7 +346,7 @@ function createSessionLogsRouter(): express.Router { logger.info(`Creating ${logsData.length} log entries for session: ${sessionId}`) - const logs = await agentService.bulkCreateSessionLogs(logsData) + const logs = await sessionLogService.bulkCreateSessionLogs(logsData) logger.info(`${logs.length} log entries created successfully for session: ${sessionId}`) return res.status(201).json({ @@ -454,7 +456,7 @@ function createSessionLogsRouter(): express.Router { logger.info(`Listing logs for session: ${sessionId} with limit=${limit}, offset=${offset}`) - const result = await agentService.listSessionLogs(sessionId, { limit, offset }) + const result = await sessionLogService.listSessionLogs(sessionId, { limit, offset }) logger.info(`Retrieved ${result.logs.length} logs (total: ${result.total}) for session: ${sessionId}`) return res.json({ @@ -536,7 +538,7 @@ function createSessionLogsRouter(): express.Router { logger.info(`Getting log entry: ${logId} for session: ${sessionId}`) - const log = await agentService.getSessionLog(logIdNum) + const log = await sessionLogService.getSessionLog(logIdNum) if (!log) { logger.warn(`Log entry not found: ${logId}`) @@ -658,7 +660,7 @@ function createSessionLogsRouter(): express.Router { logger.debug('Update data:', req.body) // First check if log exists and belongs to session - const existingLog = await agentService.getSessionLog(logIdNum) + const existingLog = await sessionLogService.getSessionLog(logIdNum) if (!existingLog || existingLog.session_id !== sessionId) { logger.warn(`Log entry ${logId} not found for session ${sessionId}`) return res.status(404).json({ @@ -670,7 +672,7 @@ function createSessionLogsRouter(): express.Router { }) } - const log = await agentService.updateSessionLog(logIdNum, req.body) + const log = await sessionLogService.updateSessionLog(logIdNum, req.body) if (!log) { logger.warn(`Log entry not found for update: ${logId}`) @@ -755,7 +757,7 @@ function createSessionLogsRouter(): express.Router { logger.info(`Deleting log entry: ${logId} for session: ${sessionId}`) // First check if log exists and belongs to session - const existingLog = await agentService.getSessionLog(logIdNum) + const existingLog = await sessionLogService.getSessionLog(logIdNum) if (!existingLog || existingLog.session_id !== sessionId) { logger.warn(`Log entry ${logId} not found for session ${sessionId}`) return res.status(404).json({ @@ -767,7 +769,7 @@ function createSessionLogsRouter(): express.Router { }) } - const deleted = await agentService.deleteSessionLog(logIdNum) + const deleted = await sessionLogService.deleteSessionLog(logIdNum) if (!deleted) { logger.warn(`Log entry not found for deletion: ${logId}`) @@ -880,7 +882,7 @@ router.get( const offset = req.query.offset ? parseInt(req.query.offset as string) : 0 // Check if session exists - const sessionExists = await agentService.sessionExists(sessionId) + const sessionExists = await sessionService.sessionExists(sessionId) if (!sessionExists) { return res.status(404).json({ error: { @@ -893,7 +895,7 @@ router.get( logger.info(`Listing logs for session: ${sessionId} with limit=${limit}, offset=${offset}`) - const result = await agentService.listSessionLogs(sessionId, { limit, offset }) + const result = await sessionLogService.listSessionLogs(sessionId, { limit, offset }) logger.info(`Retrieved ${result.logs.length} logs (total: ${result.total}) for session: ${sessionId}`) return res.json({ @@ -956,7 +958,7 @@ router.get('/session-logs/:logId', validateLogId, handleValidationErrors, async logger.info(`Getting log entry: ${logId}`) - const log = await agentService.getSessionLog(logIdNum) + const log = await sessionLogService.getSessionLog(logIdNum) if (!log) { logger.warn(`Log entry not found: ${logId}`) diff --git a/src/main/apiServer/routes/sessions.ts b/src/main/apiServer/routes/sessions.ts index 78062864b..e355c1506 100644 --- a/src/main/apiServer/routes/sessions.ts +++ b/src/main/apiServer/routes/sessions.ts @@ -2,6 +2,7 @@ import express, { Request, Response } from 'express' import { body, param, query, validationResult } from 'express-validator' import { agentService } from '../../services/agents/AgentService' +import { sessionService } from '../../services/agents/SessionService' import { loggerService } from '../../services/LoggerService' const logger = loggerService.withContext('ApiServerSessionsRoutes') @@ -321,7 +322,7 @@ function createSessionsRouter(): express.Router { logger.info(`Creating new session for agent: ${agentId}`) logger.debug('Session data:', sessionData) - const session = await agentService.createSession(sessionData) + const session = await sessionService.createSession(sessionData) logger.info(`Session created successfully: ${session.id}`) return res.status(201).json(session) @@ -428,7 +429,7 @@ function createSessionsRouter(): express.Router { logger.info(`Listing sessions for agent: ${agentId} with limit=${limit}, offset=${offset}, status=${status}`) - const result = await agentService.listSessions(agentId, { limit, offset, status }) + const result = await sessionService.listSessions(agentId, { limit, offset, status }) logger.info(`Retrieved ${result.sessions.length} sessions (total: ${result.total}) for agent: ${agentId}`) return res.json({ @@ -501,7 +502,7 @@ function createSessionsRouter(): express.Router { const { agentId, sessionId } = req.params logger.info(`Getting session: ${sessionId} for agent: ${agentId}`) - const session = await agentService.getSession(sessionId) + const session = await sessionService.getSession(sessionId) if (!session) { logger.warn(`Session not found: ${sessionId}`) @@ -607,7 +608,7 @@ function createSessionsRouter(): express.Router { logger.debug('Update data:', req.body) // First check if session exists and belongs to agent - const existingSession = await agentService.getSession(sessionId) + const existingSession = await sessionService.getSession(sessionId) if (!existingSession || existingSession.main_agent_id !== agentId) { logger.warn(`Session ${sessionId} not found for agent ${agentId}`) return res.status(404).json({ @@ -619,7 +620,7 @@ function createSessionsRouter(): express.Router { }) } - const session = await agentService.updateSession(sessionId, req.body) + const session = await sessionService.updateSession(sessionId, req.body) if (!session) { logger.warn(`Session not found for update: ${sessionId}`) @@ -720,7 +721,7 @@ function createSessionsRouter(): express.Router { logger.info(`Updating session status: ${sessionId} for agent: ${agentId} to ${status}`) // First check if session exists and belongs to agent - const existingSession = await agentService.getSession(sessionId) + const existingSession = await sessionService.getSession(sessionId) if (!existingSession || existingSession.main_agent_id !== agentId) { logger.warn(`Session ${sessionId} not found for agent ${agentId}`) return res.status(404).json({ @@ -732,7 +733,7 @@ function createSessionsRouter(): express.Router { }) } - const session = await agentService.updateSessionStatus(sessionId, status) + const session = await sessionService.updateSessionStatus(sessionId, status) if (!session) { logger.warn(`Session not found for status update: ${sessionId}`) @@ -808,7 +809,7 @@ function createSessionsRouter(): express.Router { logger.info(`Deleting session: ${sessionId} for agent: ${agentId}`) // First check if session exists and belongs to agent - const existingSession = await agentService.getSession(sessionId) + const existingSession = await sessionService.getSession(sessionId) if (!existingSession || existingSession.main_agent_id !== agentId) { logger.warn(`Session ${sessionId} not found for agent ${agentId}`) return res.status(404).json({ @@ -820,7 +821,7 @@ function createSessionsRouter(): express.Router { }) } - const deleted = await agentService.deleteSession(sessionId) + const deleted = await sessionService.deleteSession(sessionId) if (!deleted) { logger.warn(`Session not found for deletion: ${sessionId}`) @@ -923,7 +924,7 @@ router.get('/', validatePagination, handleValidationErrors, async (req: Request, logger.info(`Listing all sessions with limit=${limit}, offset=${offset}, status=${status}`) - const result = await agentService.listSessions(undefined, { limit, offset, status }) + const result = await sessionService.listSessions(undefined, { limit, offset, status }) logger.info(`Retrieved ${result.sessions.length} sessions (total: ${result.total})`) return res.json({ @@ -983,7 +984,7 @@ router.get('/:sessionId', validateSessionId, handleValidationErrors, async (req: const { sessionId } = req.params logger.info(`Getting session: ${sessionId}`) - const session = await agentService.getSession(sessionId) + const session = await sessionService.getSession(sessionId) if (!session) { logger.warn(`Session not found: ${sessionId}`) diff --git a/src/main/services/agents/AgentService.ts b/src/main/services/agents/AgentService.ts index 08c0b15a5..c34be93ad 100644 --- a/src/main/services/agents/AgentService.ts +++ b/src/main/services/agents/AgentService.ts @@ -1,20 +1,8 @@ -import { Client, createClient } from '@libsql/client' -import { loggerService } from '@logger' -import type { - AgentEntity, - AgentSessionEntity, - AgentType, - PermissionMode, - SessionLogEntity, - SessionStatus -} from '@types' -import { app } from 'electron' -import path from 'path' +import type { AgentEntity, AgentType, PermissionMode } from '@types' +import { BaseService } from './BaseService' import { AgentQueries } from './db' -const logger = loggerService.withContext('AgentService') - export interface CreateAgentRequest { type: AgentType name: string @@ -50,71 +38,13 @@ export interface UpdateAgentRequest { max_steps?: number } -export interface CreateSessionRequest { - name?: string - main_agent_id: string - sub_agent_ids?: string[] - user_goal?: string - status?: SessionStatus - external_session_id?: string - model?: string - plan_model?: string - small_model?: string - built_in_tools?: string[] - mcps?: string[] - knowledges?: string[] - configuration?: Record - accessible_paths?: string[] - permission_mode?: PermissionMode - max_steps?: number -} - -export interface UpdateSessionRequest { - name?: string - main_agent_id?: string - sub_agent_ids?: string[] - user_goal?: string - status?: SessionStatus - external_session_id?: string - model?: string - plan_model?: string - small_model?: string - built_in_tools?: string[] - mcps?: string[] - knowledges?: string[] - configuration?: Record - accessible_paths?: string[] - permission_mode?: PermissionMode - max_steps?: number -} - -export interface CreateSessionLogRequest { - session_id: string - parent_id?: number - role: 'user' | 'agent' | 'system' | 'tool' - type: string - content: Record - metadata?: Record -} - -export interface UpdateSessionLogRequest { - content?: Record - metadata?: Record -} - -export interface ListOptions { +export interface ListAgentsOptions { limit?: number offset?: number } -export interface ListSessionsOptions extends ListOptions { - status?: SessionStatus -} - -export class AgentService { +export class AgentService extends BaseService { private static instance: AgentService | null = null - private db: Client | null = null - private isInitialized = false static getInstance(): AgentService { if (!AgentService.instance) { @@ -124,78 +54,7 @@ export class AgentService { } async initialize(): Promise { - if (this.isInitialized) { - return - } - - try { - const userDataPath = app.getPath('userData') - const dbPath = path.join(userDataPath, 'agents.db') - - logger.info(`Initializing Agent database at: ${dbPath}`) - - this.db = createClient({ - url: `file:${dbPath}` - }) - - // Create tables - await this.db.execute(AgentQueries.createTables.agents) - await this.db.execute(AgentQueries.createTables.sessions) - await this.db.execute(AgentQueries.createTables.sessionLogs) - - // Create indexes - const indexQueries = Object.values(AgentQueries.createIndexes) - for (const query of indexQueries) { - await this.db.execute(query) - } - - this.isInitialized = true - logger.info('Agent database initialized successfully') - } catch (error) { - logger.error('Failed to initialize Agent database:', error as Error) - throw error - } - } - - private ensureInitialized(): void { - if (!this.isInitialized || !this.db) { - throw new Error('AgentService not initialized. Call initialize() first.') - } - } - - private serializeJsonFields(data: any): any { - const serialized = { ...data } - const jsonFields = ['built_in_tools', 'mcps', 'knowledges', 'configuration', 'accessible_paths', 'sub_agent_ids'] - - for (const field of jsonFields) { - if (serialized[field] !== undefined) { - serialized[field] = - Array.isArray(serialized[field]) || typeof serialized[field] === 'object' - ? JSON.stringify(serialized[field]) - : serialized[field] - } - } - - return serialized - } - - private deserializeJsonFields(data: any): any { - if (!data) return data - - const deserialized = { ...data } - const jsonFields = ['built_in_tools', 'mcps', 'knowledges', 'configuration', 'accessible_paths', 'sub_agent_ids'] - - for (const field of jsonFields) { - if (deserialized[field] && typeof deserialized[field] === 'string') { - try { - deserialized[field] = JSON.parse(deserialized[field]) - } catch (error) { - logger.warn(`Failed to parse JSON field ${field}:`, error as Error) - } - } - } - - return deserialized + await BaseService.initialize() } // Agent Methods @@ -228,12 +87,12 @@ export class AgentService { now ] - await this.db!.execute({ + await this.database.execute({ sql: AgentQueries.agents.insert, args: values }) - const result = await this.db!.execute({ + const result = await this.database.execute({ sql: AgentQueries.agents.getById, args: [id] }) @@ -248,7 +107,7 @@ export class AgentService { async getAgent(id: string): Promise { this.ensureInitialized() - const result = await this.db!.execute({ + const result = await this.database.execute({ sql: AgentQueries.agents.getById, args: [id] }) @@ -260,11 +119,11 @@ export class AgentService { return this.deserializeJsonFields(result.rows[0]) as AgentEntity } - async listAgents(options: ListOptions = {}): Promise<{ agents: AgentEntity[]; total: number }> { + async listAgents(options: ListAgentsOptions = {}): Promise<{ agents: AgentEntity[]; total: number }> { this.ensureInitialized() // Get total count - const countResult = await this.db!.execute(AgentQueries.agents.count) + const countResult = await this.database.execute(AgentQueries.agents.count) const total = (countResult.rows[0] as any).total // Get agents with pagination @@ -281,7 +140,7 @@ export class AgentService { } } - const result = await this.db!.execute({ + const result = await this.database.execute({ sql: query, args: args }) @@ -342,7 +201,7 @@ export class AgentService { id ] - await this.db!.execute({ + await this.database.execute({ sql: AgentQueries.agents.update, args: values }) @@ -353,7 +212,7 @@ export class AgentService { async deleteAgent(id: string): Promise { this.ensureInitialized() - const result = await this.db!.execute({ + const result = await this.database.execute({ sql: AgentQueries.agents.delete, args: [id] }) @@ -364,480 +223,13 @@ export class AgentService { async agentExists(id: string): Promise { this.ensureInitialized() - const result = await this.db!.execute({ + const result = await this.database.execute({ sql: AgentQueries.agents.checkExists, args: [id] }) return result.rows.length > 0 } - - // Session Methods - async createSession(sessionData: CreateSessionRequest): Promise { - this.ensureInitialized() - - // Validate agent exists - const agentExists = await this.agentExists(sessionData.main_agent_id) - if (!agentExists) { - throw new Error(`Agent with id ${sessionData.main_agent_id} does not exist`) - } - - const id = `session_${Date.now()}_${Math.random().toString(36).substring(2, 11)}` - const now = new Date().toISOString() - - const serializedData = this.serializeJsonFields(sessionData) - - const values = [ - id, - serializedData.name || null, - serializedData.main_agent_id, - serializedData.sub_agent_ids || null, - serializedData.user_goal || null, - serializedData.status || 'idle', - serializedData.external_session_id || null, - serializedData.model || null, - serializedData.plan_model || null, - serializedData.small_model || null, - serializedData.built_in_tools || null, - serializedData.mcps || null, - serializedData.knowledges || null, - serializedData.configuration || null, - serializedData.accessible_paths || null, - serializedData.permission_mode || 'readOnly', - serializedData.max_steps || 10, - now, - now - ] - - await this.db!.execute({ - sql: AgentQueries.sessions.insert, - args: values - }) - - const result = await this.db!.execute({ - sql: AgentQueries.sessions.getById, - args: [id] - }) - - if (!result.rows[0]) { - throw new Error('Failed to create session') - } - - return this.deserializeJsonFields(result.rows[0]) as AgentSessionEntity - } - - async getSession(id: string): Promise { - this.ensureInitialized() - - const result = await this.db!.execute({ - sql: AgentQueries.sessions.getById, - args: [id] - }) - - if (!result.rows[0]) { - return null - } - - return this.deserializeJsonFields(result.rows[0]) as AgentSessionEntity - } - - async getSessionWithAgent(id: string): Promise { - this.ensureInitialized() - - const result = await this.db!.execute({ - sql: AgentQueries.sessions.getSessionWithAgent, - args: [id] - }) - - if (!result.rows[0]) { - return null - } - - return this.deserializeJsonFields(result.rows[0]) - } - - async listSessions( - agentId?: string, - options: ListSessionsOptions = {} - ): Promise<{ sessions: AgentSessionEntity[]; total: number }> { - this.ensureInitialized() - - let countQuery: string - let listQuery: string - const countArgs: any[] = [] - const listArgs: any[] = [] - - // Build base queries - if (agentId) { - countQuery = 'SELECT COUNT(*) as total FROM sessions WHERE main_agent_id = ?' - listQuery = 'SELECT * FROM sessions WHERE main_agent_id = ?' - countArgs.push(agentId) - listArgs.push(agentId) - } else { - countQuery = AgentQueries.sessions.count - listQuery = AgentQueries.sessions.list - } - - // Filter by status if specified - if (options.status) { - if (agentId) { - countQuery += ' AND status = ?' - listQuery += ' AND status = ?' - } else { - countQuery = 'SELECT COUNT(*) as total FROM sessions WHERE status = ?' - listQuery = 'SELECT * FROM sessions WHERE status = ?' - } - countArgs.push(options.status) - listArgs.push(options.status) - } - - // Add ordering if not already present - if (!listQuery.includes('ORDER BY')) { - listQuery += ' ORDER BY created_at DESC' - } - - // Get total count - const countResult = await this.db!.execute({ - sql: countQuery, - args: countArgs - }) - const total = (countResult.rows[0] as any).total - - // Add pagination - if (options.limit !== undefined) { - listQuery += ' LIMIT ?' - listArgs.push(options.limit) - - if (options.offset !== undefined) { - listQuery += ' OFFSET ?' - listArgs.push(options.offset) - } - } - - const result = await this.db!.execute({ - sql: listQuery, - args: listArgs - }) - - const sessions = result.rows.map((row) => this.deserializeJsonFields(row)) as AgentSessionEntity[] - - return { sessions, total } - } - - async updateSession(id: string, updates: UpdateSessionRequest): Promise { - this.ensureInitialized() - - // Check if session exists - const existing = await this.getSession(id) - if (!existing) { - return null - } - - // Validate agent exists if changing main_agent_id - if (updates.main_agent_id && updates.main_agent_id !== existing.main_agent_id) { - const agentExists = await this.agentExists(updates.main_agent_id) - if (!agentExists) { - throw new Error(`Agent with id ${updates.main_agent_id} does not exist`) - } - } - - const now = new Date().toISOString() - const serializedUpdates = this.serializeJsonFields(updates) - - const values = [ - serializedUpdates.name !== undefined ? serializedUpdates.name : existing.name, - serializedUpdates.main_agent_id !== undefined ? serializedUpdates.main_agent_id : existing.main_agent_id, - serializedUpdates.sub_agent_ids !== undefined - ? serializedUpdates.sub_agent_ids - : existing.sub_agent_ids - ? JSON.stringify(existing.sub_agent_ids) - : null, - serializedUpdates.user_goal !== undefined ? serializedUpdates.user_goal : existing.user_goal, - serializedUpdates.status !== undefined ? serializedUpdates.status : existing.status, - serializedUpdates.external_session_id !== undefined - ? serializedUpdates.external_session_id - : existing.external_session_id, - serializedUpdates.model !== undefined ? serializedUpdates.model : existing.model, - serializedUpdates.plan_model !== undefined ? serializedUpdates.plan_model : existing.plan_model, - serializedUpdates.small_model !== undefined ? serializedUpdates.small_model : existing.small_model, - serializedUpdates.built_in_tools !== undefined - ? serializedUpdates.built_in_tools - : existing.built_in_tools - ? JSON.stringify(existing.built_in_tools) - : null, - serializedUpdates.mcps !== undefined - ? serializedUpdates.mcps - : existing.mcps - ? JSON.stringify(existing.mcps) - : null, - serializedUpdates.knowledges !== undefined - ? serializedUpdates.knowledges - : existing.knowledges - ? JSON.stringify(existing.knowledges) - : null, - serializedUpdates.configuration !== undefined - ? serializedUpdates.configuration - : existing.configuration - ? JSON.stringify(existing.configuration) - : null, - serializedUpdates.accessible_paths !== undefined - ? serializedUpdates.accessible_paths - : existing.accessible_paths - ? JSON.stringify(existing.accessible_paths) - : null, - serializedUpdates.permission_mode !== undefined ? serializedUpdates.permission_mode : existing.permission_mode, - serializedUpdates.max_steps !== undefined ? serializedUpdates.max_steps : existing.max_steps, - now, - id - ] - - await this.db!.execute({ - sql: AgentQueries.sessions.update, - args: values - }) - - return await this.getSession(id) - } - - async updateSessionStatus(id: string, status: SessionStatus): Promise { - this.ensureInitialized() - - const now = new Date().toISOString() - - const result = await this.db!.execute({ - sql: AgentQueries.sessions.updateStatus, - args: [status, now, id] - }) - - if (result.rowsAffected === 0) { - return null - } - - return await this.getSession(id) - } - - async deleteSession(id: string): Promise { - this.ensureInitialized() - - const result = await this.db!.execute({ - sql: AgentQueries.sessions.delete, - args: [id] - }) - - return result.rowsAffected > 0 - } - - async sessionExists(id: string): Promise { - this.ensureInitialized() - - const result = await this.db!.execute({ - sql: AgentQueries.sessions.checkExists, - args: [id] - }) - - return result.rows.length > 0 - } - - // Session Log Methods - async createSessionLog(logData: CreateSessionLogRequest): Promise { - this.ensureInitialized() - - // Validate session exists - const sessionExists = await this.sessionExists(logData.session_id) - if (!sessionExists) { - throw new Error(`Session with id ${logData.session_id} does not exist`) - } - - // Validate parent exists if specified - if (logData.parent_id) { - const parentExists = await this.sessionLogExists(logData.parent_id) - if (!parentExists) { - throw new Error(`Parent log with id ${logData.parent_id} does not exist`) - } - } - - const now = new Date().toISOString() - - const values = [ - logData.session_id, - logData.parent_id || null, - logData.role, - logData.type, - JSON.stringify(logData.content), - logData.metadata ? JSON.stringify(logData.metadata) : null, - now, - now - ] - - const result = await this.db!.execute({ - sql: AgentQueries.sessionLogs.insert, - args: values - }) - - if (!result.lastInsertRowid) { - throw new Error('Failed to create session log') - } - - const logResult = await this.db!.execute({ - sql: AgentQueries.sessionLogs.getById, - args: [result.lastInsertRowid] - }) - - if (!logResult.rows[0]) { - throw new Error('Failed to retrieve created session log') - } - - return this.deserializeSessionLog(logResult.rows[0]) as SessionLogEntity - } - - async getSessionLog(id: number): Promise { - this.ensureInitialized() - - const result = await this.db!.execute({ - sql: AgentQueries.sessionLogs.getById, - args: [id] - }) - - if (!result.rows[0]) { - return null - } - - return this.deserializeSessionLog(result.rows[0]) as SessionLogEntity - } - - async listSessionLogs( - sessionId: string, - options: ListOptions = {} - ): Promise<{ logs: SessionLogEntity[]; total: number }> { - this.ensureInitialized() - - // Get total count - const countResult = await this.db!.execute({ - sql: AgentQueries.sessionLogs.countBySessionId, - args: [sessionId] - }) - const total = (countResult.rows[0] as any).total - - // Get logs with pagination - let query: string - const args: any[] = [sessionId] - - if (options.limit !== undefined) { - query = AgentQueries.sessionLogs.getBySessionIdWithPagination - args.push(options.limit) - - if (options.offset !== undefined) { - args.push(options.offset) - } else { - args.push(0) - } - } else { - query = AgentQueries.sessionLogs.getBySessionId - } - - const result = await this.db!.execute({ - sql: query, - args: args - }) - - const logs = result.rows.map((row) => this.deserializeSessionLog(row)) as SessionLogEntity[] - - return { logs, total } - } - - async updateSessionLog(id: number, updates: UpdateSessionLogRequest): Promise { - this.ensureInitialized() - - // Check if log exists - const existing = await this.getSessionLog(id) - if (!existing) { - return null - } - - const now = new Date().toISOString() - - const values = [ - updates.content !== undefined ? JSON.stringify(updates.content) : JSON.stringify(existing.content), - updates.metadata !== undefined - ? updates.metadata - ? JSON.stringify(updates.metadata) - : null - : existing.metadata - ? JSON.stringify(existing.metadata) - : null, - now, - id - ] - - await this.db!.execute({ - sql: AgentQueries.sessionLogs.update, - args: values - }) - - return await this.getSessionLog(id) - } - - async deleteSessionLog(id: number): Promise { - this.ensureInitialized() - - const result = await this.db!.execute({ - sql: AgentQueries.sessionLogs.deleteById, - args: [id] - }) - - return result.rowsAffected > 0 - } - - async sessionLogExists(id: number): Promise { - this.ensureInitialized() - - const result = await this.db!.execute({ - sql: AgentQueries.sessionLogs.getById, - args: [id] - }) - - return result.rows.length > 0 - } - - async bulkCreateSessionLogs(logs: CreateSessionLogRequest[]): Promise { - this.ensureInitialized() - - const results: SessionLogEntity[] = [] - - // Use a transaction for bulk insert - for (const logData of logs) { - const result = await this.createSessionLog(logData) - results.push(result) - } - - return results - } - - private deserializeSessionLog(data: any): SessionLogEntity { - if (!data) return data - - const deserialized = { ...data } - - // Parse content JSON - if (deserialized.content && typeof deserialized.content === 'string') { - try { - deserialized.content = JSON.parse(deserialized.content) - } catch (error) { - logger.warn(`Failed to parse content JSON:`, error as Error) - } - } - - // Parse metadata JSON - if (deserialized.metadata && typeof deserialized.metadata === 'string') { - try { - deserialized.metadata = JSON.parse(deserialized.metadata) - } catch (error) { - logger.warn(`Failed to parse metadata JSON:`, error as Error) - } - } - - return deserialized - } } export const agentService = AgentService.getInstance() diff --git a/src/main/services/agents/BaseService.ts b/src/main/services/agents/BaseService.ts new file mode 100644 index 000000000..d2e441a18 --- /dev/null +++ b/src/main/services/agents/BaseService.ts @@ -0,0 +1,97 @@ +import { Client, createClient } from '@libsql/client' +import { loggerService } from '@logger' +import { app } from 'electron' +import path from 'path' + +import { AgentQueries } from './db' + +const logger = loggerService.withContext('BaseService') + +/** + * Base service class providing shared database connection and utilities + * for all agent-related services + */ +export abstract class BaseService { + protected static db: Client | null = null + protected static isInitialized = false + + protected static async initialize(): Promise { + if (BaseService.isInitialized) { + return + } + + try { + const userDataPath = app.getPath('userData') + const dbPath = path.join(userDataPath, 'agents.db') + + logger.info(`Initializing Agent database at: ${dbPath}`) + + BaseService.db = createClient({ + url: `file:${dbPath}` + }) + + // Create tables + await BaseService.db.execute(AgentQueries.createTables.agents) + await BaseService.db.execute(AgentQueries.createTables.sessions) + await BaseService.db.execute(AgentQueries.createTables.sessionLogs) + + // Create indexes + const indexQueries = Object.values(AgentQueries.createIndexes) + for (const query of indexQueries) { + await BaseService.db.execute(query) + } + + BaseService.isInitialized = true + logger.info('Agent database initialized successfully') + } catch (error) { + logger.error('Failed to initialize Agent database:', error as Error) + throw error + } + } + + protected ensureInitialized(): void { + if (!BaseService.isInitialized || !BaseService.db) { + throw new Error('Database not initialized. Call initialize() first.') + } + } + + protected get database(): Client { + this.ensureInitialized() + return BaseService.db! + } + + protected serializeJsonFields(data: any): any { + const serialized = { ...data } + const jsonFields = ['built_in_tools', 'mcps', 'knowledges', 'configuration', 'accessible_paths', 'sub_agent_ids'] + + for (const field of jsonFields) { + if (serialized[field] !== undefined) { + serialized[field] = + Array.isArray(serialized[field]) || typeof serialized[field] === 'object' + ? JSON.stringify(serialized[field]) + : serialized[field] + } + } + + return serialized + } + + protected deserializeJsonFields(data: any): any { + if (!data) return data + + const deserialized = { ...data } + const jsonFields = ['built_in_tools', 'mcps', 'knowledges', 'configuration', 'accessible_paths', 'sub_agent_ids'] + + for (const field of jsonFields) { + if (deserialized[field] && typeof deserialized[field] === 'string') { + try { + deserialized[field] = JSON.parse(deserialized[field]) + } catch (error) { + logger.warn(`Failed to parse JSON field ${field}:`, error as Error) + } + } + } + + return deserialized + } +} diff --git a/src/main/services/agents/SessionLogService.ts b/src/main/services/agents/SessionLogService.ts new file mode 100644 index 000000000..17e67ec09 --- /dev/null +++ b/src/main/services/agents/SessionLogService.ts @@ -0,0 +1,241 @@ +import { loggerService } from '@logger' +import type { SessionLogEntity } from '@types' + +import { BaseService } from './BaseService' +import { AgentQueries } from './db' + +const logger = loggerService.withContext('SessionLogService') + +export interface CreateSessionLogRequest { + session_id: string + parent_id?: number + role: 'user' | 'agent' | 'system' | 'tool' + type: string + content: Record + metadata?: Record +} + +export interface UpdateSessionLogRequest { + content?: Record + metadata?: Record +} + +export interface ListSessionLogsOptions { + limit?: number + offset?: number +} + +export class SessionLogService extends BaseService { + private static instance: SessionLogService | null = null + + static getInstance(): SessionLogService { + if (!SessionLogService.instance) { + SessionLogService.instance = new SessionLogService() + } + return SessionLogService.instance + } + + async initialize(): Promise { + await BaseService.initialize() + } + + async createSessionLog(logData: CreateSessionLogRequest): Promise { + this.ensureInitialized() + + // Validate session exists - we'll need to import SessionService for this check + // For now, we'll skip this validation to avoid circular dependencies + // The database foreign key constraint will handle this + + // Validate parent exists if specified + if (logData.parent_id) { + const parentExists = await this.sessionLogExists(logData.parent_id) + if (!parentExists) { + throw new Error(`Parent log with id ${logData.parent_id} does not exist`) + } + } + + const now = new Date().toISOString() + + const values = [ + logData.session_id, + logData.parent_id || null, + logData.role, + logData.type, + JSON.stringify(logData.content), + logData.metadata ? JSON.stringify(logData.metadata) : null, + now, + now + ] + + const result = await this.database.execute({ + sql: AgentQueries.sessionLogs.insert, + args: values + }) + + if (!result.lastInsertRowid) { + throw new Error('Failed to create session log') + } + + const logResult = await this.database.execute({ + sql: AgentQueries.sessionLogs.getById, + args: [result.lastInsertRowid] + }) + + if (!logResult.rows[0]) { + throw new Error('Failed to retrieve created session log') + } + + return this.deserializeSessionLog(logResult.rows[0]) as SessionLogEntity + } + + async getSessionLog(id: number): Promise { + this.ensureInitialized() + + const result = await this.database.execute({ + sql: AgentQueries.sessionLogs.getById, + args: [id] + }) + + if (!result.rows[0]) { + return null + } + + return this.deserializeSessionLog(result.rows[0]) as SessionLogEntity + } + + async listSessionLogs( + sessionId: string, + options: ListSessionLogsOptions = {} + ): Promise<{ logs: SessionLogEntity[]; total: number }> { + this.ensureInitialized() + + // Get total count + const countResult = await this.database.execute({ + sql: AgentQueries.sessionLogs.countBySessionId, + args: [sessionId] + }) + const total = (countResult.rows[0] as any).total + + // Get logs with pagination + let query: string + const args: any[] = [sessionId] + + if (options.limit !== undefined) { + query = AgentQueries.sessionLogs.getBySessionIdWithPagination + args.push(options.limit) + + if (options.offset !== undefined) { + args.push(options.offset) + } else { + args.push(0) + } + } else { + query = AgentQueries.sessionLogs.getBySessionId + } + + const result = await this.database.execute({ + sql: query, + args: args + }) + + const logs = result.rows.map((row) => this.deserializeSessionLog(row)) as SessionLogEntity[] + + return { logs, total } + } + + async updateSessionLog(id: number, updates: UpdateSessionLogRequest): Promise { + this.ensureInitialized() + + // Check if log exists + const existing = await this.getSessionLog(id) + if (!existing) { + return null + } + + const now = new Date().toISOString() + + const values = [ + updates.content !== undefined ? JSON.stringify(updates.content) : JSON.stringify(existing.content), + updates.metadata !== undefined + ? updates.metadata + ? JSON.stringify(updates.metadata) + : null + : existing.metadata + ? JSON.stringify(existing.metadata) + : null, + now, + id + ] + + await this.database.execute({ + sql: AgentQueries.sessionLogs.update, + args: values + }) + + return await this.getSessionLog(id) + } + + async deleteSessionLog(id: number): Promise { + this.ensureInitialized() + + const result = await this.database.execute({ + sql: AgentQueries.sessionLogs.deleteById, + args: [id] + }) + + return result.rowsAffected > 0 + } + + async sessionLogExists(id: number): Promise { + this.ensureInitialized() + + const result = await this.database.execute({ + sql: AgentQueries.sessionLogs.getById, + args: [id] + }) + + return result.rows.length > 0 + } + + async bulkCreateSessionLogs(logs: CreateSessionLogRequest[]): Promise { + this.ensureInitialized() + + const results: SessionLogEntity[] = [] + + // Use a transaction for bulk insert + for (const logData of logs) { + const result = await this.createSessionLog(logData) + results.push(result) + } + + return results + } + + private deserializeSessionLog(data: any): SessionLogEntity { + if (!data) return data + + const deserialized = { ...data } + + // Parse content JSON + if (deserialized.content && typeof deserialized.content === 'string') { + try { + deserialized.content = JSON.parse(deserialized.content) + } catch (error) { + logger.warn(`Failed to parse content JSON:`, error as Error) + } + } + + // Parse metadata JSON + if (deserialized.metadata && typeof deserialized.metadata === 'string') { + try { + deserialized.metadata = JSON.parse(deserialized.metadata) + } catch (error) { + logger.warn(`Failed to parse metadata JSON:`, error as Error) + } + } + + return deserialized + } +} + +export const sessionLogService = SessionLogService.getInstance() diff --git a/src/main/services/agents/SessionService.ts b/src/main/services/agents/SessionService.ts new file mode 100644 index 000000000..764739262 --- /dev/null +++ b/src/main/services/agents/SessionService.ts @@ -0,0 +1,323 @@ +import type { AgentSessionEntity, SessionStatus } from '@types' + +import { BaseService } from './BaseService' +import { AgentQueries } from './db' + +export interface CreateSessionRequest { + name?: string + main_agent_id: string + sub_agent_ids?: string[] + user_goal?: string + status?: SessionStatus + external_session_id?: string + model?: string + plan_model?: string + small_model?: string + built_in_tools?: string[] + mcps?: string[] + knowledges?: string[] + configuration?: Record + accessible_paths?: string[] + permission_mode?: 'readOnly' | 'acceptEdits' | 'bypassPermissions' + max_steps?: number +} + +export interface UpdateSessionRequest { + name?: string + main_agent_id?: string + sub_agent_ids?: string[] + user_goal?: string + status?: SessionStatus + external_session_id?: string + model?: string + plan_model?: string + small_model?: string + built_in_tools?: string[] + mcps?: string[] + knowledges?: string[] + configuration?: Record + accessible_paths?: string[] + permission_mode?: 'readOnly' | 'acceptEdits' | 'bypassPermissions' + max_steps?: number +} + +export interface ListSessionsOptions { + limit?: number + offset?: number + status?: SessionStatus +} + +export class SessionService extends BaseService { + private static instance: SessionService | null = null + + static getInstance(): SessionService { + if (!SessionService.instance) { + SessionService.instance = new SessionService() + } + return SessionService.instance + } + + async initialize(): Promise { + await BaseService.initialize() + } + + async createSession(sessionData: CreateSessionRequest): Promise { + this.ensureInitialized() + + // Validate agent exists - we'll need to import AgentService for this check + // For now, we'll skip this validation to avoid circular dependencies + // The database foreign key constraint will handle this + + const id = `session_${Date.now()}_${Math.random().toString(36).substring(2, 11)}` + const now = new Date().toISOString() + + const serializedData = this.serializeJsonFields(sessionData) + + const values = [ + id, + serializedData.name || null, + serializedData.main_agent_id, + serializedData.sub_agent_ids || null, + serializedData.user_goal || null, + serializedData.status || 'idle', + serializedData.external_session_id || null, + serializedData.model || null, + serializedData.plan_model || null, + serializedData.small_model || null, + serializedData.built_in_tools || null, + serializedData.mcps || null, + serializedData.knowledges || null, + serializedData.configuration || null, + serializedData.accessible_paths || null, + serializedData.permission_mode || 'readOnly', + serializedData.max_steps || 10, + now, + now + ] + + await this.database.execute({ + sql: AgentQueries.sessions.insert, + args: values + }) + + const result = await this.database.execute({ + sql: AgentQueries.sessions.getById, + args: [id] + }) + + if (!result.rows[0]) { + throw new Error('Failed to create session') + } + + return this.deserializeJsonFields(result.rows[0]) as AgentSessionEntity + } + + async getSession(id: string): Promise { + this.ensureInitialized() + + const result = await this.database.execute({ + sql: AgentQueries.sessions.getById, + args: [id] + }) + + if (!result.rows[0]) { + return null + } + + return this.deserializeJsonFields(result.rows[0]) as AgentSessionEntity + } + + async getSessionWithAgent(id: string): Promise { + this.ensureInitialized() + + const result = await this.database.execute({ + sql: AgentQueries.sessions.getSessionWithAgent, + args: [id] + }) + + if (!result.rows[0]) { + return null + } + + return this.deserializeJsonFields(result.rows[0]) + } + + async listSessions( + agentId?: string, + options: ListSessionsOptions = {} + ): Promise<{ sessions: AgentSessionEntity[]; total: number }> { + this.ensureInitialized() + + let countQuery: string + let listQuery: string + const countArgs: any[] = [] + const listArgs: any[] = [] + + // Build base queries + if (agentId) { + countQuery = 'SELECT COUNT(*) as total FROM sessions WHERE main_agent_id = ?' + listQuery = 'SELECT * FROM sessions WHERE main_agent_id = ?' + countArgs.push(agentId) + listArgs.push(agentId) + } else { + countQuery = AgentQueries.sessions.count + listQuery = AgentQueries.sessions.list + } + + // Filter by status if specified + if (options.status) { + if (agentId) { + countQuery += ' AND status = ?' + listQuery += ' AND status = ?' + } else { + countQuery = 'SELECT COUNT(*) as total FROM sessions WHERE status = ?' + listQuery = 'SELECT * FROM sessions WHERE status = ?' + } + countArgs.push(options.status) + listArgs.push(options.status) + } + + // Add ordering if not already present + if (!listQuery.includes('ORDER BY')) { + listQuery += ' ORDER BY created_at DESC' + } + + // Get total count + const countResult = await this.database.execute({ + sql: countQuery, + args: countArgs + }) + const total = (countResult.rows[0] as any).total + + // Add pagination + if (options.limit !== undefined) { + listQuery += ' LIMIT ?' + listArgs.push(options.limit) + + if (options.offset !== undefined) { + listQuery += ' OFFSET ?' + listArgs.push(options.offset) + } + } + + const result = await this.database.execute({ + sql: listQuery, + args: listArgs + }) + + const sessions = result.rows.map((row) => this.deserializeJsonFields(row)) as AgentSessionEntity[] + + return { sessions, total } + } + + async updateSession(id: string, updates: UpdateSessionRequest): Promise { + this.ensureInitialized() + + // Check if session exists + const existing = await this.getSession(id) + if (!existing) { + return null + } + + // Validate agent exists if changing main_agent_id + // We'll skip this validation for now to avoid circular dependencies + + const now = new Date().toISOString() + const serializedUpdates = this.serializeJsonFields(updates) + + const values = [ + serializedUpdates.name !== undefined ? serializedUpdates.name : existing.name, + serializedUpdates.main_agent_id !== undefined ? serializedUpdates.main_agent_id : existing.main_agent_id, + serializedUpdates.sub_agent_ids !== undefined + ? serializedUpdates.sub_agent_ids + : existing.sub_agent_ids + ? JSON.stringify(existing.sub_agent_ids) + : null, + serializedUpdates.user_goal !== undefined ? serializedUpdates.user_goal : existing.user_goal, + serializedUpdates.status !== undefined ? serializedUpdates.status : existing.status, + serializedUpdates.external_session_id !== undefined + ? serializedUpdates.external_session_id + : existing.external_session_id, + serializedUpdates.model !== undefined ? serializedUpdates.model : existing.model, + serializedUpdates.plan_model !== undefined ? serializedUpdates.plan_model : existing.plan_model, + serializedUpdates.small_model !== undefined ? serializedUpdates.small_model : existing.small_model, + serializedUpdates.built_in_tools !== undefined + ? serializedUpdates.built_in_tools + : existing.built_in_tools + ? JSON.stringify(existing.built_in_tools) + : null, + serializedUpdates.mcps !== undefined + ? serializedUpdates.mcps + : existing.mcps + ? JSON.stringify(existing.mcps) + : null, + serializedUpdates.knowledges !== undefined + ? serializedUpdates.knowledges + : existing.knowledges + ? JSON.stringify(existing.knowledges) + : null, + serializedUpdates.configuration !== undefined + ? serializedUpdates.configuration + : existing.configuration + ? JSON.stringify(existing.configuration) + : null, + serializedUpdates.accessible_paths !== undefined + ? serializedUpdates.accessible_paths + : existing.accessible_paths + ? JSON.stringify(existing.accessible_paths) + : null, + serializedUpdates.permission_mode !== undefined ? serializedUpdates.permission_mode : existing.permission_mode, + serializedUpdates.max_steps !== undefined ? serializedUpdates.max_steps : existing.max_steps, + now, + id + ] + + await this.database.execute({ + sql: AgentQueries.sessions.update, + args: values + }) + + return await this.getSession(id) + } + + async updateSessionStatus(id: string, status: SessionStatus): Promise { + this.ensureInitialized() + + const now = new Date().toISOString() + + const result = await this.database.execute({ + sql: AgentQueries.sessions.updateStatus, + args: [status, now, id] + }) + + if (result.rowsAffected === 0) { + return null + } + + return await this.getSession(id) + } + + async deleteSession(id: string): Promise { + this.ensureInitialized() + + const result = await this.database.execute({ + sql: AgentQueries.sessions.delete, + args: [id] + }) + + return result.rowsAffected > 0 + } + + async sessionExists(id: string): Promise { + this.ensureInitialized() + + const result = await this.database.execute({ + sql: AgentQueries.sessions.checkExists, + args: [id] + }) + + return result.rows.length > 0 + } +} + +export const sessionService = SessionService.getInstance() diff --git a/src/main/services/agents/index.ts b/src/main/services/agents/index.ts index 778cb4477..db2685145 100644 --- a/src/main/services/agents/index.ts +++ b/src/main/services/agents/index.ts @@ -1,2 +1,5 @@ export * from './AgentService' +export * from './BaseService' export * from './db' +export * from './SessionLogService' +export * from './SessionService'