mirror of
https://github.com/CherryHQ/cherry-studio.git
synced 2025-12-19 14:41:24 +08:00
♻️ refactor: split AgentService into focused service modules
- **BaseService**: Shared database connection and JSON serialization utilities - **AgentService**: Agent management operations (CRUD for agents) - **SessionService**: Session management operations (CRUD for sessions) - **SessionLogService**: Session log management operations (CRUD for session logs) Updated API routes to use appropriate services: - sessions.ts now uses SessionService for session operations - session-logs.ts now uses SessionLogService and SessionService as needed - Maintains backward compatibility with existing API endpoints Benefits: - Single Responsibility Principle - each service has a clear focus - Better code organization and maintainability - Easier testing and debugging - Improved separation of concerns - Shared database infrastructure via BaseService All TypeScript compilation and build checks pass.
This commit is contained in:
parent
9c956a30ea
commit
64f3d08d4e
13
CLAUDE.md
13
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
|
||||
|
||||
|
||||
@ -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}`)
|
||||
|
||||
@ -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}`)
|
||||
|
||||
@ -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<string, any>
|
||||
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<string, any>
|
||||
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<string, any>
|
||||
metadata?: Record<string, any>
|
||||
}
|
||||
|
||||
export interface UpdateSessionLogRequest {
|
||||
content?: Record<string, any>
|
||||
metadata?: Record<string, any>
|
||||
}
|
||||
|
||||
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<void> {
|
||||
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<AgentEntity | null> {
|
||||
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<boolean> {
|
||||
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<boolean> {
|
||||
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<AgentSessionEntity> {
|
||||
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<AgentSessionEntity | null> {
|
||||
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<any | null> {
|
||||
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<AgentSessionEntity | null> {
|
||||
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<AgentSessionEntity | null> {
|
||||
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<boolean> {
|
||||
this.ensureInitialized()
|
||||
|
||||
const result = await this.db!.execute({
|
||||
sql: AgentQueries.sessions.delete,
|
||||
args: [id]
|
||||
})
|
||||
|
||||
return result.rowsAffected > 0
|
||||
}
|
||||
|
||||
async sessionExists(id: string): Promise<boolean> {
|
||||
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<SessionLogEntity> {
|
||||
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<SessionLogEntity | null> {
|
||||
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<SessionLogEntity | null> {
|
||||
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<boolean> {
|
||||
this.ensureInitialized()
|
||||
|
||||
const result = await this.db!.execute({
|
||||
sql: AgentQueries.sessionLogs.deleteById,
|
||||
args: [id]
|
||||
})
|
||||
|
||||
return result.rowsAffected > 0
|
||||
}
|
||||
|
||||
async sessionLogExists(id: number): Promise<boolean> {
|
||||
this.ensureInitialized()
|
||||
|
||||
const result = await this.db!.execute({
|
||||
sql: AgentQueries.sessionLogs.getById,
|
||||
args: [id]
|
||||
})
|
||||
|
||||
return result.rows.length > 0
|
||||
}
|
||||
|
||||
async bulkCreateSessionLogs(logs: CreateSessionLogRequest[]): Promise<SessionLogEntity[]> {
|
||||
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()
|
||||
|
||||
97
src/main/services/agents/BaseService.ts
Normal file
97
src/main/services/agents/BaseService.ts
Normal file
@ -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<void> {
|
||||
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
|
||||
}
|
||||
}
|
||||
241
src/main/services/agents/SessionLogService.ts
Normal file
241
src/main/services/agents/SessionLogService.ts
Normal file
@ -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<string, any>
|
||||
metadata?: Record<string, any>
|
||||
}
|
||||
|
||||
export interface UpdateSessionLogRequest {
|
||||
content?: Record<string, any>
|
||||
metadata?: Record<string, any>
|
||||
}
|
||||
|
||||
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<void> {
|
||||
await BaseService.initialize()
|
||||
}
|
||||
|
||||
async createSessionLog(logData: CreateSessionLogRequest): Promise<SessionLogEntity> {
|
||||
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<SessionLogEntity | null> {
|
||||
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<SessionLogEntity | null> {
|
||||
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<boolean> {
|
||||
this.ensureInitialized()
|
||||
|
||||
const result = await this.database.execute({
|
||||
sql: AgentQueries.sessionLogs.deleteById,
|
||||
args: [id]
|
||||
})
|
||||
|
||||
return result.rowsAffected > 0
|
||||
}
|
||||
|
||||
async sessionLogExists(id: number): Promise<boolean> {
|
||||
this.ensureInitialized()
|
||||
|
||||
const result = await this.database.execute({
|
||||
sql: AgentQueries.sessionLogs.getById,
|
||||
args: [id]
|
||||
})
|
||||
|
||||
return result.rows.length > 0
|
||||
}
|
||||
|
||||
async bulkCreateSessionLogs(logs: CreateSessionLogRequest[]): Promise<SessionLogEntity[]> {
|
||||
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()
|
||||
323
src/main/services/agents/SessionService.ts
Normal file
323
src/main/services/agents/SessionService.ts
Normal file
@ -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<string, any>
|
||||
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<string, any>
|
||||
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<void> {
|
||||
await BaseService.initialize()
|
||||
}
|
||||
|
||||
async createSession(sessionData: CreateSessionRequest): Promise<AgentSessionEntity> {
|
||||
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<AgentSessionEntity | null> {
|
||||
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<any | null> {
|
||||
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<AgentSessionEntity | null> {
|
||||
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<AgentSessionEntity | null> {
|
||||
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<boolean> {
|
||||
this.ensureInitialized()
|
||||
|
||||
const result = await this.database.execute({
|
||||
sql: AgentQueries.sessions.delete,
|
||||
args: [id]
|
||||
})
|
||||
|
||||
return result.rowsAffected > 0
|
||||
}
|
||||
|
||||
async sessionExists(id: string): Promise<boolean> {
|
||||
this.ensureInitialized()
|
||||
|
||||
const result = await this.database.execute({
|
||||
sql: AgentQueries.sessions.checkExists,
|
||||
args: [id]
|
||||
})
|
||||
|
||||
return result.rows.length > 0
|
||||
}
|
||||
}
|
||||
|
||||
export const sessionService = SessionService.getInstance()
|
||||
@ -1,2 +1,5 @@
|
||||
export * from './AgentService'
|
||||
export * from './BaseService'
|
||||
export * from './db'
|
||||
export * from './SessionLogService'
|
||||
export * from './SessionService'
|
||||
|
||||
Loading…
Reference in New Issue
Block a user