diff --git a/src/main/apiServer/app.ts b/src/main/apiServer/app.ts index 2377dbbeef..c52f4f85f1 100644 --- a/src/main/apiServer/app.ts +++ b/src/main/apiServer/app.ts @@ -10,8 +10,6 @@ import { agentsRoutes } from './routes/agents' import { chatRoutes } from './routes/chat' import { mcpRoutes } from './routes/mcp' import { modelsRoutes } from './routes/models' -import { sessionMessagesRoutes } from './routes/session-messages' -import { sessionsRoutes } from './routes/sessions' const logger = loggerService.withContext('ApiServer') @@ -104,13 +102,7 @@ app.get('/', (_req, res) => { name: 'Cherry Studio API', version: '1.0.0', endpoints: { - health: 'GET /health', - models: 'GET /v1/models', - chat: 'POST /v1/chat/completions', - mcp: 'GET /v1/mcps', - agents: 'GET /v1/agents', - sessions: 'GET /v1/sessions', - logs: 'GET /v1/sessions/{sessionId}/logs' + health: 'GET /health' } }) }) @@ -124,8 +116,6 @@ apiRouter.use('/chat', chatRoutes) apiRouter.use('/mcps', mcpRoutes) apiRouter.use('/models', modelsRoutes) apiRouter.use('/agents', agentsRoutes) -apiRouter.use('/sessions', sessionsRoutes) -apiRouter.use('/', sessionMessagesRoutes) // This handles /sessions/:sessionId/messages and /session-messages/:messageId app.use('/v1', apiRouter) // Setup OpenAPI documentation diff --git a/src/main/apiServer/routes/agents.ts b/src/main/apiServer/routes/agents/handlers/agents.ts similarity index 56% rename from src/main/apiServer/routes/agents.ts rename to src/main/apiServer/routes/agents/handlers/agents.ts index def5d000a3..b43d7fb068 100644 --- a/src/main/apiServer/routes/agents.ts +++ b/src/main/apiServer/routes/agents/handlers/agents.ts @@ -1,202 +1,9 @@ -import express, { Request, Response } from 'express' -import { body, param, query, validationResult } from 'express-validator' +import { Request, Response } from 'express' -import { agentService } from '../../services/agents' -import { loggerService } from '../../services/LoggerService' +import { agentService } from '../../../../services/agents' +import { loggerService } from '../../../../services/LoggerService' -const logger = loggerService.withContext('ApiServerAgentsRoutes') - -const router = express.Router() - -// Validation middleware -const validateAgent = [ - body('name').notEmpty().withMessage('Name is required'), - body('model').notEmpty().withMessage('Model is required'), - body('description').optional().isString(), - body('avatar').optional().isString(), - body('instructions').optional().isString(), - body('plan_model').optional().isString(), - body('small_model').optional().isString(), - body('built_in_tools').optional().isArray(), - body('mcps').optional().isArray(), - body('knowledges').optional().isArray(), - body('configuration').optional().isObject(), - body('accessible_paths').optional().isArray(), - body('permission_mode').optional().isIn(['readOnly', 'acceptEdits', 'bypassPermissions']), - body('max_steps').optional().isInt({ min: 1 }) -] - -const validateAgentUpdate = [ - body('name').optional().notEmpty().withMessage('Name cannot be empty'), - body('model').optional().notEmpty().withMessage('Model cannot be empty'), - body('description').optional().isString(), - body('avatar').optional().isString(), - body('instructions').optional().isString(), - body('plan_model').optional().isString(), - body('small_model').optional().isString(), - body('built_in_tools').optional().isArray(), - body('mcps').optional().isArray(), - body('knowledges').optional().isArray(), - body('configuration').optional().isObject(), - body('accessible_paths').optional().isArray(), - body('permission_mode').optional().isIn(['readOnly', 'acceptEdits', 'bypassPermissions']), - body('max_steps').optional().isInt({ min: 1 }) -] - -const validateAgentId = [param('agentId').notEmpty().withMessage('Agent ID is required')] - -const validatePagination = [ - query('limit').optional().isInt({ min: 1, max: 100 }).withMessage('Limit must be between 1 and 100'), - query('offset').optional().isInt({ min: 0 }).withMessage('Offset must be non-negative') -] - -// Error handler for validation -const handleValidationErrors = (req: Request, res: Response, next: any): void => { - const errors = validationResult(req) - if (!errors.isEmpty()) { - res.status(400).json({ - error: { - message: 'Validation failed', - type: 'validation_error', - details: errors.array() - } - }) - return - } - next() -} - -/** - * @swagger - * components: - * schemas: - * AgentEntity: - * type: object - * properties: - * id: - * type: string - * description: Unique agent identifier - * name: - * type: string - * description: Agent name - * description: - * type: string - * description: Agent description - * avatar: - * type: string - * description: Agent avatar URL - * instructions: - * type: string - * description: System prompt/instructions - * model: - * type: string - * description: Main model ID - * plan_model: - * type: string - * description: Optional planning model ID - * small_model: - * type: string - * description: Optional small/fast model ID - * built_in_tools: - * type: array - * items: - * type: string - * description: Built-in tool IDs - * mcps: - * type: array - * items: - * type: string - * description: MCP tool IDs - * knowledges: - * type: array - * items: - * type: string - * description: Knowledge base IDs - * configuration: - * type: object - * description: Extensible settings - * accessible_paths: - * type: array - * items: - * type: string - * description: Accessible directory paths - * permission_mode: - * type: string - * enum: [readOnly, acceptEdits, bypassPermissions] - * description: Permission mode - * max_steps: - * type: integer - * description: Maximum steps the agent can take - * created_at: - * type: string - * format: date-time - * updated_at: - * type: string - * format: date-time - * required: - * - id - * - name - * - model - * - created_at - * - updated_at - * CreateAgentRequest: - * type: object - * properties: - * name: - * type: string - * description: Agent name - * description: - * type: string - * description: Agent description - * avatar: - * type: string - * description: Agent avatar URL - * instructions: - * type: string - * description: System prompt/instructions - * model: - * type: string - * description: Main model ID - * plan_model: - * type: string - * description: Optional planning model ID - * small_model: - * type: string - * description: Optional small/fast model ID - * built_in_tools: - * type: array - * items: - * type: string - * description: Built-in tool IDs - * mcps: - * type: array - * items: - * type: string - * description: MCP tool IDs - * knowledges: - * type: array - * items: - * type: string - * description: Knowledge base IDs - * configuration: - * type: object - * description: Extensible settings - * accessible_paths: - * type: array - * items: - * type: string - * description: Accessible directory paths - * permission_mode: - * type: string - * enum: [readOnly, acceptEdits, bypassPermissions] - * description: Permission mode - * max_steps: - * type: integer - * description: Maximum steps the agent can take - * required: - * - name - * - model - */ +const logger = loggerService.withContext('ApiServerAgentsHandlers') /** * @swagger @@ -231,7 +38,7 @@ const handleValidationErrors = (req: Request, res: Response, next: any): void => * schema: * $ref: '#/components/schemas/Error' */ -router.post('/', validateAgent, handleValidationErrors, async (req: Request, res: Response) => { +export const createAgent = async (req: Request, res: Response): Promise => { try { logger.info('Creating new agent') logger.debug('Agent data:', req.body) @@ -250,7 +57,7 @@ router.post('/', validateAgent, handleValidationErrors, async (req: Request, res } }) } -}) +} /** * @swagger @@ -309,7 +116,7 @@ router.post('/', validateAgent, handleValidationErrors, async (req: Request, res * schema: * $ref: '#/components/schemas/Error' */ -router.get('/', validatePagination, handleValidationErrors, async (req: Request, res: Response) => { +export const listAgents = async (req: Request, res: Response): Promise => { try { const limit = req.query.limit ? parseInt(req.query.limit as string) : 20 const offset = req.query.offset ? parseInt(req.query.offset as string) : 0 @@ -335,7 +142,7 @@ router.get('/', validatePagination, handleValidationErrors, async (req: Request, } }) } -}) +} /** * @swagger @@ -371,7 +178,7 @@ router.get('/', validatePagination, handleValidationErrors, async (req: Request, * schema: * $ref: '#/components/schemas/Error' */ -router.get('/:agentId', validateAgentId, handleValidationErrors, async (req: Request, res: Response) => { +export const getAgent = async (req: Request, res: Response): Promise => { try { const { agentId } = req.params logger.info(`Getting agent: ${agentId}`) @@ -401,7 +208,7 @@ router.get('/:agentId', validateAgentId, handleValidationErrors, async (req: Req } }) } -}) +} /** * @swagger @@ -449,44 +256,38 @@ router.get('/:agentId', validateAgentId, handleValidationErrors, async (req: Req * schema: * $ref: '#/components/schemas/Error' */ -router.put( - '/:agentId', - validateAgentId, - validateAgentUpdate, - handleValidationErrors, - async (req: Request, res: Response) => { - try { - const { agentId } = req.params - logger.info(`Updating agent: ${agentId}`) - logger.debug('Update data:', req.body) +export const updateAgent = async (req: Request, res: Response): Promise => { + try { + const { agentId } = req.params + logger.info(`Updating agent: ${agentId}`) + logger.debug('Update data:', req.body) - const agent = await agentService.updateAgent(agentId, req.body) + const agent = await agentService.updateAgent(agentId, req.body) - if (!agent) { - logger.warn(`Agent not found for update: ${agentId}`) - return res.status(404).json({ - error: { - message: 'Agent not found', - type: 'not_found', - code: 'agent_not_found' - } - }) - } - - logger.info(`Agent updated successfully: ${agentId}`) - return res.json(agent) - } catch (error: any) { - logger.error('Error updating agent:', error) - return res.status(500).json({ + if (!agent) { + logger.warn(`Agent not found for update: ${agentId}`) + return res.status(404).json({ error: { - message: 'Failed to update agent', - type: 'internal_error', - code: 'agent_update_failed' + message: 'Agent not found', + type: 'not_found', + code: 'agent_not_found' } }) } + + logger.info(`Agent updated successfully: ${agentId}`) + return res.json(agent) + } catch (error: any) { + logger.error('Error updating agent:', error) + return res.status(500).json({ + error: { + message: 'Failed to update agent', + type: 'internal_error', + code: 'agent_update_failed' + } + }) } -) +} /** * @swagger @@ -587,44 +388,38 @@ router.put( * schema: * $ref: '#/components/schemas/Error' */ -router.patch( - '/:agentId', - validateAgentId, - validateAgentUpdate, - handleValidationErrors, - async (req: Request, res: Response) => { - try { - const { agentId } = req.params - logger.info(`Partially updating agent: ${agentId}`) - logger.debug('Partial update data:', req.body) +export const patchAgent = async (req: Request, res: Response): Promise => { + try { + const { agentId } = req.params + logger.info(`Partially updating agent: ${agentId}`) + logger.debug('Partial update data:', req.body) - const agent = await agentService.updateAgent(agentId, req.body) + const agent = await agentService.updateAgent(agentId, req.body) - if (!agent) { - logger.warn(`Agent not found for partial update: ${agentId}`) - return res.status(404).json({ - error: { - message: 'Agent not found', - type: 'not_found', - code: 'agent_not_found' - } - }) - } - - logger.info(`Agent partially updated successfully: ${agentId}`) - return res.json(agent) - } catch (error: any) { - logger.error('Error partially updating agent:', error) - return res.status(500).json({ + if (!agent) { + logger.warn(`Agent not found for partial update: ${agentId}`) + return res.status(404).json({ error: { - message: 'Failed to partially update agent', - type: 'internal_error', - code: 'agent_patch_failed' + message: 'Agent not found', + type: 'not_found', + code: 'agent_not_found' } }) } + + logger.info(`Agent partially updated successfully: ${agentId}`) + return res.json(agent) + } catch (error: any) { + logger.error('Error partially updating agent:', error) + return res.status(500).json({ + error: { + message: 'Failed to partially update agent', + type: 'internal_error', + code: 'agent_patch_failed' + } + }) } -) +} /** * @swagger @@ -656,7 +451,7 @@ router.patch( * schema: * $ref: '#/components/schemas/Error' */ -router.delete('/:agentId', validateAgentId, handleValidationErrors, async (req: Request, res: Response) => { +export const deleteAgent = async (req: Request, res: Response): Promise => { try { const { agentId } = req.params logger.info(`Deleting agent: ${agentId}`) @@ -686,17 +481,4 @@ router.delete('/:agentId', validateAgentId, handleValidationErrors, async (req: } }) } -}) - -// Mount session routes as nested resources -import { createSessionMessagesRouter } from './session-messages' -import { createSessionsRouter } from './sessions' - -const sessionsRouter = createSessionsRouter() -const sessionMessagesRouter = createSessionMessagesRouter() - -// Mount nested routes -router.use('/:agentId/sessions', sessionsRouter) -router.use('/:agentId/sessions/:sessionId/messages', sessionMessagesRouter) - -export { router as agentsRoutes } +} diff --git a/src/main/apiServer/routes/agents/handlers/index.ts b/src/main/apiServer/routes/agents/handlers/index.ts new file mode 100644 index 0000000000..0bd3e1d73a --- /dev/null +++ b/src/main/apiServer/routes/agents/handlers/index.ts @@ -0,0 +1,3 @@ +export * as agentHandlers from './agents' +export * as messageHandlers from './messages' +export * as sessionHandlers from './sessions' diff --git a/src/main/apiServer/routes/agents/handlers/messages.ts b/src/main/apiServer/routes/agents/handlers/messages.ts new file mode 100644 index 0000000000..4f595b509c --- /dev/null +++ b/src/main/apiServer/routes/agents/handlers/messages.ts @@ -0,0 +1,315 @@ +import { Request, Response } from 'express' + +import { agentService, sessionMessageService, sessionService } from '../../../../services/agents' +import { loggerService } from '../../../../services/LoggerService' + +const logger = loggerService.withContext('ApiServerMessagesHandlers') + +// Helper function to verify agent and session exist and belong together +const verifyAgentAndSession = async (agentId: string, sessionId: string) => { + const agentExists = await agentService.agentExists(agentId) + if (!agentExists) { + throw { status: 404, code: 'agent_not_found', message: 'Agent not found' } + } + + const session = await sessionService.getSession(sessionId) + if (!session) { + throw { status: 404, code: 'session_not_found', message: 'Session not found' } + } + + if (session.main_agent_id !== agentId) { + throw { status: 404, code: 'session_not_found', message: 'Session not found for this agent' } + } + + return session +} + +export const createMessage = async (req: Request, res: Response): Promise => { + try { + const { agentId, sessionId } = req.params + + await verifyAgentAndSession(agentId, sessionId) + + const messageData = { ...req.body, session_id: sessionId } + + logger.info(`Creating new message for session: ${sessionId}`) + logger.debug('Message data:', messageData) + + const message = await sessionMessageService.createSessionMessage(messageData) + + logger.info(`Message created successfully: ${message.id}`) + return res.status(201).json(message) + } catch (error: any) { + if (error.status) { + return res.status(error.status).json({ + error: { + message: error.message, + type: 'not_found', + code: error.code + } + }) + } + + logger.error('Error creating message:', error) + return res.status(500).json({ + error: { + message: 'Failed to create message', + type: 'internal_error', + code: 'message_creation_failed' + } + }) + } +} + +export const createBulkMessages = async (req: Request, res: Response): Promise => { + try { + const { agentId, sessionId } = req.params + + await verifyAgentAndSession(agentId, sessionId) + + const messagesData = req.body.map((msg: any) => ({ ...msg, session_id: sessionId })) + + logger.info(`Creating ${messagesData.length} messages for session: ${sessionId}`) + logger.debug('Messages data:', messagesData) + + const messages = await sessionMessageService.bulkCreateSessionMessages(messagesData) + + logger.info(`${messages.length} messages created successfully for session: ${sessionId}`) + return res.status(201).json(messages) + } catch (error: any) { + if (error.status) { + return res.status(error.status).json({ + error: { + message: error.message, + type: 'not_found', + code: error.code + } + }) + } + + logger.error('Error creating bulk messages:', error) + return res.status(500).json({ + error: { + message: 'Failed to create messages', + type: 'internal_error', + code: 'bulk_message_creation_failed' + } + }) + } +} + +export const listMessages = async (req: Request, res: Response): Promise => { + try { + const { agentId, sessionId } = req.params + + await verifyAgentAndSession(agentId, sessionId) + + const limit = req.query.limit ? parseInt(req.query.limit as string) : 50 + const offset = req.query.offset ? parseInt(req.query.offset as string) : 0 + + logger.info(`Listing messages for session: ${sessionId} with limit=${limit}, offset=${offset}`) + + const result = await sessionMessageService.listSessionMessages(sessionId, { limit, offset }) + + logger.info(`Retrieved ${result.messages.length} messages (total: ${result.total}) for session: ${sessionId}`) + return res.json({ + data: result.messages, + total: result.total, + limit, + offset + }) + } catch (error: any) { + if (error.status) { + return res.status(error.status).json({ + error: { + message: error.message, + type: 'not_found', + code: error.code + } + }) + } + + logger.error('Error listing messages:', error) + return res.status(500).json({ + error: { + message: 'Failed to list messages', + type: 'internal_error', + code: 'message_list_failed' + } + }) + } +} + +export const getMessage = async (req: Request, res: Response): Promise => { + try { + const { agentId, sessionId, messageId } = req.params + + await verifyAgentAndSession(agentId, sessionId) + + logger.info(`Getting message: ${messageId} for session: ${sessionId}`) + + const message = await sessionMessageService.getSessionMessage(parseInt(messageId)) + + if (!message) { + logger.warn(`Message not found: ${messageId}`) + return res.status(404).json({ + error: { + message: 'Message not found', + type: 'not_found', + code: 'message_not_found' + } + }) + } + + // Verify message belongs to the session + if (message.session_id !== sessionId) { + logger.warn(`Message ${messageId} does not belong to session ${sessionId}`) + return res.status(404).json({ + error: { + message: 'Message not found for this session', + type: 'not_found', + code: 'message_not_found' + } + }) + } + + logger.info(`Message retrieved successfully: ${messageId}`) + return res.json(message) + } catch (error: any) { + if (error.status) { + return res.status(error.status).json({ + error: { + message: error.message, + type: 'not_found', + code: error.code + } + }) + } + + logger.error('Error getting message:', error) + return res.status(500).json({ + error: { + message: 'Failed to get message', + type: 'internal_error', + code: 'message_get_failed' + } + }) + } +} + +export const updateMessage = async (req: Request, res: Response): Promise => { + try { + const { agentId, sessionId, messageId } = req.params + + await verifyAgentAndSession(agentId, sessionId) + + logger.info(`Updating message: ${messageId} for session: ${sessionId}`) + logger.debug('Update data:', req.body) + + // First check if message exists and belongs to session + const existingMessage = await sessionMessageService.getSessionMessage(parseInt(messageId)) + if (!existingMessage || existingMessage.session_id !== sessionId) { + logger.warn(`Message ${messageId} not found for session ${sessionId}`) + return res.status(404).json({ + error: { + message: 'Message not found for this session', + type: 'not_found', + code: 'message_not_found' + } + }) + } + + const message = await sessionMessageService.updateSessionMessage(parseInt(messageId), req.body) + + if (!message) { + logger.warn(`Message not found for update: ${messageId}`) + return res.status(404).json({ + error: { + message: 'Message not found', + type: 'not_found', + code: 'message_not_found' + } + }) + } + + logger.info(`Message updated successfully: ${messageId}`) + return res.json(message) + } catch (error: any) { + if (error.status) { + return res.status(error.status).json({ + error: { + message: error.message, + type: 'not_found', + code: error.code + } + }) + } + + logger.error('Error updating message:', error) + return res.status(500).json({ + error: { + message: 'Failed to update message', + type: 'internal_error', + code: 'message_update_failed' + } + }) + } +} + +export const deleteMessage = async (req: Request, res: Response): Promise => { + try { + const { agentId, sessionId, messageId } = req.params + + await verifyAgentAndSession(agentId, sessionId) + + logger.info(`Deleting message: ${messageId} for session: ${sessionId}`) + + // First check if message exists and belongs to session + const existingMessage = await sessionMessageService.getSessionMessage(parseInt(messageId)) + if (!existingMessage || existingMessage.session_id !== sessionId) { + logger.warn(`Message ${messageId} not found for session ${sessionId}`) + return res.status(404).json({ + error: { + message: 'Message not found for this session', + type: 'not_found', + code: 'message_not_found' + } + }) + } + + const deleted = await sessionMessageService.deleteSessionMessage(parseInt(messageId)) + + if (!deleted) { + logger.warn(`Message not found for deletion: ${messageId}`) + return res.status(404).json({ + error: { + message: 'Message not found', + type: 'not_found', + code: 'message_not_found' + } + }) + } + + logger.info(`Message deleted successfully: ${messageId}`) + return res.status(204).send() + } catch (error: any) { + if (error.status) { + return res.status(error.status).json({ + error: { + message: error.message, + type: 'not_found', + code: error.code + } + }) + } + + logger.error('Error deleting message:', error) + return res.status(500).json({ + error: { + message: 'Failed to delete message', + type: 'internal_error', + code: 'message_delete_failed' + } + }) + } +} diff --git a/src/main/apiServer/routes/agents/handlers/sessions.ts b/src/main/apiServer/routes/agents/handlers/sessions.ts new file mode 100644 index 0000000000..14d7cbc175 --- /dev/null +++ b/src/main/apiServer/routes/agents/handlers/sessions.ts @@ -0,0 +1,306 @@ +import { Request, Response } from 'express' + +import { sessionService } from '../../../../services/agents' +import { loggerService } from '../../../../services/LoggerService' + +const logger = loggerService.withContext('ApiServerSessionsHandlers') + +export const createSession = async (req: Request, res: Response): Promise => { + try { + const { agentId } = req.params + const sessionData = { ...req.body, main_agent_id: agentId } + + logger.info(`Creating new session for agent: ${agentId}`) + logger.debug('Session data:', sessionData) + + const session = await sessionService.createSession(sessionData) + + logger.info(`Session created successfully: ${session.id}`) + return res.status(201).json(session) + } catch (error: any) { + logger.error('Error creating session:', error) + return res.status(500).json({ + error: { + message: 'Failed to create session', + type: 'internal_error', + code: 'session_creation_failed' + } + }) + } +} + +export const listSessions = async (req: Request, res: Response): Promise => { + try { + const { agentId } = req.params + const limit = req.query.limit ? parseInt(req.query.limit as string) : 20 + const offset = req.query.offset ? parseInt(req.query.offset as string) : 0 + const status = req.query.status as any + + logger.info(`Listing sessions for agent: ${agentId} with limit=${limit}, offset=${offset}, status=${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({ + data: result.sessions, + total: result.total, + limit, + offset + }) + } catch (error: any) { + logger.error('Error listing sessions:', error) + return res.status(500).json({ + error: { + message: 'Failed to list sessions', + type: 'internal_error', + code: 'session_list_failed' + } + }) + } +} + +export const getSession = async (req: Request, res: Response): Promise => { + try { + const { agentId, sessionId } = req.params + logger.info(`Getting session: ${sessionId} for agent: ${agentId}`) + + const session = await sessionService.getSession(sessionId) + + if (!session) { + logger.warn(`Session not found: ${sessionId}`) + return res.status(404).json({ + error: { + message: 'Session not found', + type: 'not_found', + code: 'session_not_found' + } + }) + } + + // Verify session belongs to the agent + if (session.main_agent_id !== agentId) { + logger.warn(`Session ${sessionId} does not belong to agent ${agentId}`) + return res.status(404).json({ + error: { + message: 'Session not found for this agent', + type: 'not_found', + code: 'session_not_found' + } + }) + } + + logger.info(`Session retrieved successfully: ${sessionId}`) + return res.json(session) + } catch (error: any) { + logger.error('Error getting session:', error) + return res.status(500).json({ + error: { + message: 'Failed to get session', + type: 'internal_error', + code: 'session_get_failed' + } + }) + } +} + +export const updateSession = async (req: Request, res: Response): Promise => { + try { + const { agentId, sessionId } = req.params + logger.info(`Updating session: ${sessionId} for agent: ${agentId}`) + logger.debug('Update data:', req.body) + + // First check if session exists and belongs to agent + 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({ + error: { + message: 'Session not found for this agent', + type: 'not_found', + code: 'session_not_found' + } + }) + } + + // For PUT, we replace the entire resource + const sessionData = { ...req.body, main_agent_id: agentId } + const session = await sessionService.updateSession(sessionId, sessionData) + + if (!session) { + logger.warn(`Session not found for update: ${sessionId}`) + return res.status(404).json({ + error: { + message: 'Session not found', + type: 'not_found', + code: 'session_not_found' + } + }) + } + + logger.info(`Session updated successfully: ${sessionId}`) + return res.json(session) + } catch (error: any) { + logger.error('Error updating session:', error) + return res.status(500).json({ + error: { + message: 'Failed to update session', + type: 'internal_error', + code: 'session_update_failed' + } + }) + } +} + +export const patchSession = async (req: Request, res: Response): Promise => { + try { + const { agentId, sessionId } = req.params + logger.info(`Patching session: ${sessionId} for agent: ${agentId}`) + logger.debug('Patch data:', req.body) + + // First check if session exists and belongs to agent + 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({ + error: { + message: 'Session not found for this agent', + type: 'not_found', + code: 'session_not_found' + } + }) + } + + const updateSession = { ...existingSession, ...req.body } + const session = await sessionService.updateSession(sessionId, updateSession) + + if (!session) { + logger.warn(`Session not found for patch: ${sessionId}`) + return res.status(404).json({ + error: { + message: 'Session not found', + type: 'not_found', + code: 'session_not_found' + } + }) + } + + logger.info(`Session patched successfully: ${sessionId}`) + return res.json(session) + } catch (error: any) { + logger.error('Error patching session:', error) + return res.status(500).json({ + error: { + message: 'Failed to patch session', + type: 'internal_error', + code: 'session_patch_failed' + } + }) + } +} + +export const deleteSession = async (req: Request, res: Response): Promise => { + try { + const { agentId, sessionId } = req.params + logger.info(`Deleting session: ${sessionId} for agent: ${agentId}`) + + // First check if session exists and belongs to agent + 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({ + error: { + message: 'Session not found for this agent', + type: 'not_found', + code: 'session_not_found' + } + }) + } + + const deleted = await sessionService.deleteSession(sessionId) + + if (!deleted) { + logger.warn(`Session not found for deletion: ${sessionId}`) + return res.status(404).json({ + error: { + message: 'Session not found', + type: 'not_found', + code: 'session_not_found' + } + }) + } + + logger.info(`Session deleted successfully: ${sessionId}`) + return res.status(204).send() + } catch (error: any) { + logger.error('Error deleting session:', error) + return res.status(500).json({ + error: { + message: 'Failed to delete session', + type: 'internal_error', + code: 'session_delete_failed' + } + }) + } +} + +// Convenience endpoints for sessions without agent context +export const listAllSessions = async (req: Request, res: Response): Promise => { + try { + const limit = req.query.limit ? parseInt(req.query.limit as string) : 20 + const offset = req.query.offset ? parseInt(req.query.offset as string) : 0 + const status = req.query.status as any + + logger.info(`Listing all sessions with limit=${limit}, offset=${offset}, status=${status}`) + + const result = await sessionService.listSessions(undefined, { limit, offset, status }) + + logger.info(`Retrieved ${result.sessions.length} sessions (total: ${result.total})`) + return res.json({ + data: result.sessions, + total: result.total, + limit, + offset + }) + } catch (error: any) { + logger.error('Error listing all sessions:', error) + return res.status(500).json({ + error: { + message: 'Failed to list sessions', + type: 'internal_error', + code: 'session_list_failed' + } + }) + } +} + +export const getSessionById = async (req: Request, res: Response): Promise => { + try { + const { sessionId } = req.params + logger.info(`Getting session: ${sessionId}`) + + const session = await sessionService.getSession(sessionId) + + if (!session) { + logger.warn(`Session not found: ${sessionId}`) + return res.status(404).json({ + error: { + message: 'Session not found', + type: 'not_found', + code: 'session_not_found' + } + }) + } + + logger.info(`Session retrieved successfully: ${sessionId}`) + return res.json(session) + } catch (error: any) { + logger.error('Error getting session:', error) + return res.status(500).json({ + error: { + message: 'Failed to get session', + type: 'internal_error', + code: 'session_get_failed' + } + }) + } +} diff --git a/src/main/apiServer/routes/agents/index.ts b/src/main/apiServer/routes/agents/index.ts new file mode 100644 index 0000000000..c3b02b0a0f --- /dev/null +++ b/src/main/apiServer/routes/agents/index.ts @@ -0,0 +1,227 @@ +import express from 'express' + +import { agentHandlers, messageHandlers, sessionHandlers } from './handlers' +import { checkAgentExists, handleValidationErrors } from './middleware' +import { + validateAgent, + validateAgentId, + validateAgentUpdate, + validateBulkSessionMessages, + validateMessageId, + validatePagination, + validateSession, + validateSessionId, + validateSessionMessage, + validateSessionMessageUpdate, + validateSessionUpdate +} from './validators' + +// Create main agents router +const agentsRouter = express.Router() + +/** + * @swagger + * components: + * schemas: + * AgentEntity: + * type: object + * properties: + * id: + * type: string + * description: Unique agent identifier + * name: + * type: string + * description: Agent name + * description: + * type: string + * description: Agent description + * avatar: + * type: string + * description: Agent avatar URL + * instructions: + * type: string + * description: System prompt/instructions + * model: + * type: string + * description: Main model ID + * plan_model: + * type: string + * description: Optional planning model ID + * small_model: + * type: string + * description: Optional small/fast model ID + * built_in_tools: + * type: array + * items: + * type: string + * description: Built-in tool IDs + * mcps: + * type: array + * items: + * type: string + * description: MCP tool IDs + * knowledges: + * type: array + * items: + * type: string + * description: Knowledge base IDs + * configuration: + * type: object + * description: Extensible settings + * accessible_paths: + * type: array + * items: + * type: string + * description: Accessible directory paths + * permission_mode: + * type: string + * enum: [readOnly, acceptEdits, bypassPermissions] + * description: Permission mode + * max_steps: + * type: integer + * description: Maximum steps the agent can take + * created_at: + * type: string + * format: date-time + * updated_at: + * type: string + * format: date-time + * required: + * - id + * - name + * - model + * - created_at + * - updated_at + * CreateAgentRequest: + * type: object + * properties: + * name: + * type: string + * description: Agent name + * description: + * type: string + * description: Agent description + * avatar: + * type: string + * description: Agent avatar URL + * instructions: + * type: string + * description: System prompt/instructions + * model: + * type: string + * description: Main model ID + * plan_model: + * type: string + * description: Optional planning model ID + * small_model: + * type: string + * description: Optional small/fast model ID + * built_in_tools: + * type: array + * items: + * type: string + * description: Built-in tool IDs + * mcps: + * type: array + * items: + * type: string + * description: MCP tool IDs + * knowledges: + * type: array + * items: + * type: string + * description: Knowledge base IDs + * configuration: + * type: object + * description: Extensible settings + * accessible_paths: + * type: array + * items: + * type: string + * description: Accessible directory paths + * permission_mode: + * type: string + * enum: [readOnly, acceptEdits, bypassPermissions] + * description: Permission mode + * max_steps: + * type: integer + * description: Maximum steps the agent can take + * required: + * - name + * - model + */ + +// Agent CRUD routes +agentsRouter.post('/', validateAgent, handleValidationErrors, agentHandlers.createAgent) +agentsRouter.get('/', validatePagination, handleValidationErrors, agentHandlers.listAgents) +agentsRouter.get('/:agentId', validateAgentId, handleValidationErrors, agentHandlers.getAgent) +agentsRouter.put('/:agentId', validateAgentId, validateAgentUpdate, handleValidationErrors, agentHandlers.updateAgent) +agentsRouter.patch('/:agentId', validateAgentId, validateAgentUpdate, handleValidationErrors, agentHandlers.patchAgent) +agentsRouter.delete('/:agentId', validateAgentId, handleValidationErrors, agentHandlers.deleteAgent) + +// Create sessions router with agent context +const createSessionsRouter = (): express.Router => { + const sessionsRouter = express.Router({ mergeParams: true }) + + // Session CRUD routes (nested under agent) + sessionsRouter.post('/', validateSession, handleValidationErrors, sessionHandlers.createSession) + sessionsRouter.get('/', validatePagination, handleValidationErrors, sessionHandlers.listSessions) + sessionsRouter.get('/:sessionId', validateSessionId, handleValidationErrors, sessionHandlers.getSession) + sessionsRouter.put( + '/:sessionId', + validateSessionId, + validateSessionUpdate, + handleValidationErrors, + sessionHandlers.updateSession + ) + sessionsRouter.patch( + '/:sessionId', + validateSessionId, + validateSessionUpdate, + handleValidationErrors, + sessionHandlers.patchSession + ) + sessionsRouter.delete('/:sessionId', validateSessionId, handleValidationErrors, sessionHandlers.deleteSession) + + return sessionsRouter +} + +// Create messages router with agent and session context +const createMessagesRouter = (): express.Router => { + const messagesRouter = express.Router({ mergeParams: true }) + + // Message CRUD routes (nested under agent/session) + messagesRouter.post('/', validateSessionMessage, handleValidationErrors, messageHandlers.createMessage) + messagesRouter.post('/bulk', validateBulkSessionMessages, handleValidationErrors, messageHandlers.createBulkMessages) + messagesRouter.get('/', validatePagination, handleValidationErrors, messageHandlers.listMessages) + messagesRouter.get('/:messageId', validateMessageId, handleValidationErrors, messageHandlers.getMessage) + messagesRouter.put( + '/:messageId', + validateMessageId, + validateSessionMessageUpdate, + handleValidationErrors, + messageHandlers.updateMessage + ) + messagesRouter.delete('/:messageId', validateMessageId, handleValidationErrors, messageHandlers.deleteMessage) + + return messagesRouter +} + +// Mount nested resources with clear hierarchy +const sessionsRouter = createSessionsRouter() +const messagesRouter = createMessagesRouter() + +// Mount sessions under specific agent +agentsRouter.use('/:agentId/sessions', validateAgentId, checkAgentExists, handleValidationErrors, sessionsRouter) + +// Mount messages under specific agent/session +agentsRouter.use( + '/:agentId/sessions/:sessionId/messages', + validateAgentId, + validateSessionId, + handleValidationErrors, + messagesRouter +) + +// Export main router and convenience router +export const agentsRoutes = agentsRouter diff --git a/src/main/apiServer/routes/agents/middleware/common.ts b/src/main/apiServer/routes/agents/middleware/common.ts new file mode 100644 index 0000000000..b68c5f3bab --- /dev/null +++ b/src/main/apiServer/routes/agents/middleware/common.ts @@ -0,0 +1,53 @@ +import { Request, Response } from 'express' +import { validationResult } from 'express-validator' + +import { agentService } from '../../../../services/agents' +import { loggerService } from '../../../../services/LoggerService' + +const logger = loggerService.withContext('ApiServerMiddleware') + +// Error handler for validation +export const handleValidationErrors = (req: Request, res: Response, next: any): void => { + const errors = validationResult(req) + if (!errors.isEmpty()) { + res.status(400).json({ + error: { + message: 'Validation failed', + type: 'validation_error', + details: errors.array() + } + }) + return + } + next() +} + +// Middleware to check if agent exists +export const checkAgentExists = async (req: Request, res: Response, next: any): Promise => { + try { + const { agentId } = req.params + const exists = await agentService.agentExists(agentId) + + if (!exists) { + res.status(404).json({ + error: { + message: 'Agent not found', + type: 'not_found', + code: 'agent_not_found' + } + }) + return + } + + next() + } catch (error) { + logger.error('Error checking agent existence:', error as Error) + res.status(500).json({ + error: { + message: 'Failed to validate agent', + type: 'internal_error', + code: 'agent_validation_failed' + } + }) + } +} diff --git a/src/main/apiServer/routes/agents/middleware/index.ts b/src/main/apiServer/routes/agents/middleware/index.ts new file mode 100644 index 0000000000..89a3196b12 --- /dev/null +++ b/src/main/apiServer/routes/agents/middleware/index.ts @@ -0,0 +1 @@ +export * from './common' diff --git a/src/main/apiServer/routes/agents/validators/agents.ts b/src/main/apiServer/routes/agents/validators/agents.ts new file mode 100644 index 0000000000..9cdbabfcc3 --- /dev/null +++ b/src/main/apiServer/routes/agents/validators/agents.ts @@ -0,0 +1,37 @@ +import { body, param } from 'express-validator' + +export const validateAgent = [ + body('name').notEmpty().withMessage('Name is required'), + body('model').notEmpty().withMessage('Model is required'), + body('description').optional().isString(), + body('avatar').optional().isString(), + body('instructions').optional().isString(), + body('plan_model').optional().isString(), + body('small_model').optional().isString(), + body('built_in_tools').optional().isArray(), + body('mcps').optional().isArray(), + body('knowledges').optional().isArray(), + body('configuration').optional().isObject(), + body('accessible_paths').optional().isArray(), + body('permission_mode').optional().isIn(['readOnly', 'acceptEdits', 'bypassPermissions']), + body('max_steps').optional().isInt({ min: 1 }) +] + +export const validateAgentUpdate = [ + body('name').optional().notEmpty().withMessage('Name cannot be empty'), + body('model').optional().notEmpty().withMessage('Model cannot be empty'), + body('description').optional().isString(), + body('avatar').optional().isString(), + body('instructions').optional().isString(), + body('plan_model').optional().isString(), + body('small_model').optional().isString(), + body('built_in_tools').optional().isArray(), + body('mcps').optional().isArray(), + body('knowledges').optional().isArray(), + body('configuration').optional().isObject(), + body('accessible_paths').optional().isArray(), + body('permission_mode').optional().isIn(['readOnly', 'acceptEdits', 'bypassPermissions']), + body('max_steps').optional().isInt({ min: 1 }) +] + +export const validateAgentId = [param('agentId').notEmpty().withMessage('Agent ID is required')] diff --git a/src/main/apiServer/routes/agents/validators/common.ts b/src/main/apiServer/routes/agents/validators/common.ts new file mode 100644 index 0000000000..1d9a811466 --- /dev/null +++ b/src/main/apiServer/routes/agents/validators/common.ts @@ -0,0 +1,10 @@ +import { query } from 'express-validator' + +export const validatePagination = [ + query('limit').optional().isInt({ min: 1, max: 100 }).withMessage('Limit must be between 1 and 100'), + query('offset').optional().isInt({ min: 0 }).withMessage('Offset must be non-negative'), + query('status') + .optional() + .isIn(['idle', 'running', 'completed', 'failed', 'stopped']) + .withMessage('Invalid status filter') +] diff --git a/src/main/apiServer/routes/agents/validators/index.ts b/src/main/apiServer/routes/agents/validators/index.ts new file mode 100644 index 0000000000..7bba43e3b7 --- /dev/null +++ b/src/main/apiServer/routes/agents/validators/index.ts @@ -0,0 +1,4 @@ +export * from './agents' +export * from './common' +export * from './messages' +export * from './sessions' diff --git a/src/main/apiServer/routes/agents/validators/messages.ts b/src/main/apiServer/routes/agents/validators/messages.ts new file mode 100644 index 0000000000..76f937ef36 --- /dev/null +++ b/src/main/apiServer/routes/agents/validators/messages.ts @@ -0,0 +1,27 @@ +import { body, param } from 'express-validator' + +export const validateSessionMessage = [ + body('parent_id').optional().isInt({ min: 1 }).withMessage('Parent ID must be a positive integer'), + body('role').notEmpty().isIn(['user', 'agent', 'system', 'tool']).withMessage('Valid role is required'), + body('type').notEmpty().isString().withMessage('Type is required'), + body('content').notEmpty().isObject().withMessage('Content must be a valid object'), + body('metadata').optional().isObject().withMessage('Metadata must be a valid object') +] + +export const validateSessionMessageUpdate = [ + body('content').optional().isObject().withMessage('Content must be a valid object'), + body('metadata').optional().isObject().withMessage('Metadata must be a valid object') +] + +export const validateBulkSessionMessages = [ + body().isArray().withMessage('Request body must be an array'), + body('*.parent_id').optional().isInt({ min: 1 }).withMessage('Parent ID must be a positive integer'), + body('*.role').notEmpty().isIn(['user', 'agent', 'system', 'tool']).withMessage('Valid role is required'), + body('*.type').notEmpty().isString().withMessage('Type is required'), + body('*.content').notEmpty().isObject().withMessage('Content must be a valid object'), + body('*.metadata').optional().isObject().withMessage('Metadata must be a valid object') +] + +export const validateMessageId = [ + param('messageId').isInt({ min: 1 }).withMessage('Message ID must be a positive integer') +] diff --git a/src/main/apiServer/routes/agents/validators/sessions.ts b/src/main/apiServer/routes/agents/validators/sessions.ts new file mode 100644 index 0000000000..6accf2f401 --- /dev/null +++ b/src/main/apiServer/routes/agents/validators/sessions.ts @@ -0,0 +1,47 @@ +import { body, param } from 'express-validator' + +export const validateSession = [ + body('name').optional().isString(), + body('sub_agent_ids').optional().isArray(), + body('user_goal').optional().isString(), + body('status').optional().isIn(['idle', 'running', 'completed', 'failed', 'stopped']), + body('external_session_id').optional().isString(), + body('model').optional().isString(), + body('plan_model').optional().isString(), + body('small_model').optional().isString(), + body('built_in_tools').optional().isArray(), + body('mcps').optional().isArray(), + body('knowledges').optional().isArray(), + body('configuration').optional().isObject(), + body('accessible_paths').optional().isArray(), + body('permission_mode').optional().isIn(['readOnly', 'acceptEdits', 'bypassPermissions']), + body('max_steps').optional().isInt({ min: 1 }) +] + +export const validateSessionUpdate = [ + body('name').optional().isString(), + body('main_agent_id').optional().notEmpty().withMessage('Main agent ID cannot be empty'), + body('sub_agent_ids').optional().isArray(), + body('user_goal').optional().isString(), + body('status').optional().isIn(['idle', 'running', 'completed', 'failed', 'stopped']), + body('external_session_id').optional().isString(), + body('model').optional().isString(), + body('plan_model').optional().isString(), + body('small_model').optional().isString(), + body('built_in_tools').optional().isArray(), + body('mcps').optional().isArray(), + body('knowledges').optional().isArray(), + body('configuration').optional().isObject(), + body('accessible_paths').optional().isArray(), + body('permission_mode').optional().isIn(['readOnly', 'acceptEdits', 'bypassPermissions']), + body('max_steps').optional().isInt({ min: 1 }) +] + +export const validateStatusUpdate = [ + body('status') + .notEmpty() + .isIn(['idle', 'running', 'completed', 'failed', 'stopped']) + .withMessage('Valid status is required') +] + +export const validateSessionId = [param('sessionId').notEmpty().withMessage('Session ID is required')] diff --git a/src/main/apiServer/routes/session-messages.ts b/src/main/apiServer/routes/session-messages.ts deleted file mode 100644 index 4936b003d8..0000000000 --- a/src/main/apiServer/routes/session-messages.ts +++ /dev/null @@ -1,991 +0,0 @@ -import express, { Request, Response } from 'express' -import { body, param, query, validationResult } from 'express-validator' - -import { agentService, sessionMessageService, sessionService } from '../../services/agents' -import { loggerService } from '../../services/LoggerService' - -const logger = loggerService.withContext('ApiServerSessionMessagesRoutes') - -const router = express.Router() - -// Validation middleware -const validateSessionMessage = [ - body('parent_id').optional().isInt({ min: 1 }).withMessage('Parent ID must be a positive integer'), - body('role').notEmpty().isIn(['user', 'agent', 'system', 'tool']).withMessage('Valid role is required'), - body('type').notEmpty().isString().withMessage('Type is required'), - body('content').notEmpty().isObject().withMessage('Content must be a valid object'), - body('metadata').optional().isObject().withMessage('Metadata must be a valid object') -] - -const validateSessionMessageUpdate = [ - body('content').optional().isObject().withMessage('Content must be a valid object'), - body('metadata').optional().isObject().withMessage('Metadata must be a valid object') -] - -const validateBulkSessionMessages = [ - body().isArray().withMessage('Request body must be an array'), - body('*.parent_id').optional().isInt({ min: 1 }).withMessage('Parent ID must be a positive integer'), - body('*.role').notEmpty().isIn(['user', 'agent', 'system', 'tool']).withMessage('Valid role is required'), - body('*.type').notEmpty().isString().withMessage('Type is required'), - body('*.content').notEmpty().isObject().withMessage('Content must be a valid object'), - body('*.metadata').optional().isObject().withMessage('Metadata must be a valid object') -] - -const validateAgentId = [param('agentId').notEmpty().withMessage('Agent ID is required')] - -const validateSessionId = [param('sessionId').notEmpty().withMessage('Session ID is required')] - -const validateMessageId = [param('messageId').isInt({ min: 1 }).withMessage('Message ID must be a positive integer')] - -const validatePagination = [ - query('limit').optional().isInt({ min: 1, max: 100 }).withMessage('Limit must be between 1 and 100'), - query('offset').optional().isInt({ min: 0 }).withMessage('Offset must be non-negative') -] - -// Error handler for validation -const handleValidationErrors = (req: Request, res: Response, next: any): void => { - const errors = validationResult(req) - if (!errors.isEmpty()) { - res.status(400).json({ - error: { - message: 'Validation failed', - type: 'validation_error', - details: errors.array() - } - }) - return - } - next() -} - -// Middleware to check if agent and session exist -const checkAgentAndSessionExist = async (req: Request, res: Response, next: any): Promise => { - try { - const { agentId, sessionId } = req.params - - const agentExists = await agentService.agentExists(agentId) - if (!agentExists) { - res.status(404).json({ - error: { - message: 'Agent not found', - type: 'not_found', - code: 'agent_not_found' - } - }) - return - } - - const session = await sessionService.getSession(sessionId) - if (!session) { - res.status(404).json({ - error: { - message: 'Session not found', - type: 'not_found', - code: 'session_not_found' - } - }) - return - } - - // Verify session belongs to the agent - if (session.main_agent_id !== agentId) { - res.status(404).json({ - error: { - message: 'Session not found for this agent', - type: 'not_found', - code: 'session_not_found' - } - }) - return - } - - next() - } catch (error) { - logger.error('Error checking agent and session existence:', error as Error) - res.status(500).json({ - error: { - message: 'Failed to validate agent and session', - type: 'internal_error', - code: 'validation_failed' - } - }) - } -} - -/** - * @swagger - * components: - * schemas: - * SessionMessageEntity: - * type: object - * properties: - * id: - * type: integer - * description: Unique message entry identifier - * session_id: - * type: string - * description: Reference to session - * parent_id: - * type: integer - * description: Parent message entry ID for tree structure - * role: - * type: string - * enum: [user, agent, system, tool] - * description: Role that created the message entry - * type: - * type: string - * description: Type of message entry - * content: - * type: object - * description: JSON structured message data - * metadata: - * type: object - * description: Additional metadata - * created_at: - * type: string - * format: date-time - * updated_at: - * type: string - * format: date-time - * required: - * - id - * - session_id - * - role - * - type - * - content - * - created_at - * - updated_at - * CreateSessionMessageRequest: - * type: object - * properties: - * parent_id: - * type: integer - * description: Parent message entry ID for tree structure - * role: - * type: string - * enum: [user, agent, system, tool] - * description: Role that created the message entry - * type: - * type: string - * description: Type of message entry - * content: - * type: object - * description: JSON structured message data - * metadata: - * type: object - * description: Additional metadata - * required: - * - role - * - type - * - content - */ - -// Create nested session messages router -function createSessionMessagesRouter(): express.Router { - const sessionMessagesRouter = express.Router({ mergeParams: true }) - - /** - * @swagger - * /v1/agents/{agentId}/sessions/{sessionId}/messages: - * post: - * summary: Create a new message entry for a session - * description: Creates a new message entry for the specified session - * tags: [Session Messages] - * parameters: - * - in: path - * name: agentId - * required: true - * schema: - * type: string - * description: Agent ID - * - in: path - * name: sessionId - * required: true - * schema: - * type: string - * description: Session ID - * requestBody: - * required: true - * content: - * application/json: - * schema: - * $ref: '#/components/schemas/CreateSessionMessageRequest' - * responses: - * 201: - * description: Log entry created successfully - * content: - * application/json: - * schema: - * $ref: '#/components/schemas/SessionMessageEntity' - * 400: - * description: Validation error - * content: - * application/json: - * schema: - * $ref: '#/components/schemas/Error' - * 404: - * description: Agent or session not found - * content: - * application/json: - * schema: - * $ref: '#/components/schemas/Error' - * 500: - * description: Internal server error - * content: - * application/json: - * schema: - * $ref: '#/components/schemas/Error' - */ - sessionMessagesRouter.post( - '/', - validateAgentId, - validateSessionId, - checkAgentAndSessionExist, - validateSessionMessage, - handleValidationErrors, - async (req: Request, res: Response) => { - try { - const { sessionId } = req.params - const messageData = { ...req.body, session_id: sessionId } - - logger.info(`Creating new message entry for session: ${sessionId}`) - logger.debug('Message data:', messageData) - - const message = await sessionMessageService.createSessionMessage(messageData) - - logger.info(`Message entry created successfully: ${message.id}`) - return res.status(201).json(message) - } catch (error: any) { - logger.error('Error creating session message:', error) - return res.status(500).json({ - error: { - message: 'Failed to create message entry', - type: 'internal_error', - code: 'message_creation_failed' - } - }) - } - } - ) - - /** - * @swagger - * /v1/agents/{agentId}/sessions/{sessionId}/messages/bulk: - * post: - * summary: Create multiple message entries for a session - * description: Creates multiple message entries for the specified session in a single request - * tags: [Session Messages] - * parameters: - * - in: path - * name: agentId - * required: true - * schema: - * type: string - * description: Agent ID - * - in: path - * name: sessionId - * required: true - * schema: - * type: string - * description: Session ID - * requestBody: - * required: true - * content: - * application/json: - * schema: - * type: array - * items: - * $ref: '#/components/schemas/CreateSessionMessageRequest' - * responses: - * 201: - * description: Log entries created successfully - * content: - * application/json: - * schema: - * type: object - * properties: - * data: - * type: array - * items: - * $ref: '#/components/schemas/SessionMessageEntity' - * count: - * type: integer - * description: Number of message entries created - * 400: - * description: Validation error - * content: - * application/json: - * schema: - * $ref: '#/components/schemas/Error' - * 404: - * description: Agent or session not found - * content: - * application/json: - * schema: - * $ref: '#/components/schemas/Error' - * 500: - * description: Internal server error - * content: - * application/json: - * schema: - * $ref: '#/components/schemas/Error' - */ - sessionMessagesRouter.post( - '/bulk', - validateAgentId, - validateSessionId, - checkAgentAndSessionExist, - validateBulkSessionMessages, - handleValidationErrors, - async (req: Request, res: Response) => { - try { - const { sessionId } = req.params - const messagesData = req.body.map((messageData: any) => ({ ...messageData, session_id: sessionId })) - - logger.info(`Creating ${messagesData.length} message entries for session: ${sessionId}`) - - const messages = await sessionMessageService.bulkCreateSessionMessages(messagesData) - - logger.info(`${messages.length} message entries created successfully for session: ${sessionId}`) - return res.status(201).json({ - data: messages, - count: messages.length - }) - } catch (error: any) { - logger.error('Error creating bulk session messages:', error) - return res.status(500).json({ - error: { - message: 'Failed to create message entries', - type: 'internal_error', - code: 'bulk_message_creation_failed' - } - }) - } - } - ) - - /** - * @swagger - * /v1/agents/{agentId}/sessions/{sessionId}/messages: - * get: - * summary: List message entries for a session - * description: Retrieves a paginated list of message entries for the specified session - * tags: [Session Messages] - * parameters: - * - in: path - * name: agentId - * required: true - * schema: - * type: string - * description: Agent ID - * - in: path - * name: sessionId - * required: true - * schema: - * type: string - * description: Session ID - * - in: query - * name: limit - * schema: - * type: integer - * minimum: 1 - * maximum: 100 - * default: 50 - * description: Number of message entries to return - * - in: query - * name: offset - * schema: - * type: integer - * minimum: 0 - * default: 0 - * description: Number of message entries to skip - * responses: - * 200: - * description: List of message entries - * content: - * application/json: - * schema: - * type: object - * properties: - * data: - * type: array - * items: - * $ref: '#/components/schemas/SessionMessageEntity' - * total: - * type: integer - * description: Total number of message entries - * limit: - * type: integer - * description: Number of message entries returned - * offset: - * type: integer - * description: Number of message entries skipped - * 400: - * description: Validation error - * content: - * application/json: - * schema: - * $ref: '#/components/schemas/Error' - * 404: - * description: Agent or session not found - * content: - * application/json: - * schema: - * $ref: '#/components/schemas/Error' - * 500: - * description: Internal server error - * content: - * application/json: - * schema: - * $ref: '#/components/schemas/Error' - */ - sessionMessagesRouter.get( - '/', - validateAgentId, - validateSessionId, - checkAgentAndSessionExist, - validatePagination, - handleValidationErrors, - async (req: Request, res: Response) => { - try { - const { sessionId } = req.params - const limit = req.query.limit ? parseInt(req.query.limit as string) : 50 - const offset = req.query.offset ? parseInt(req.query.offset as string) : 0 - - logger.info(`Listing messages for session: ${sessionId} with limit=${limit}, offset=${offset}`) - - const result = await sessionMessageService.listSessionMessages(sessionId, { limit, offset }) - - logger.info(`Retrieved ${result.messages.length} messages (total: ${result.total}) for session: ${sessionId}`) - return res.json({ - data: result.messages, - total: result.total, - limit, - offset - }) - } catch (error: any) { - logger.error('Error listing session messages:', error) - return res.status(500).json({ - error: { - message: 'Failed to list message entries', - type: 'internal_error', - code: 'message_list_failed' - } - }) - } - } - ) - - /** - * @swagger - * /v1/agents/{agentId}/sessions/{sessionId}/messages/{messageId}: - * get: - * summary: Get message entry by ID - * description: Retrieves a specific message entry for the specified session - * tags: [Session Messages] - * parameters: - * - in: path - * name: agentId - * required: true - * schema: - * type: string - * description: Agent ID - * - in: path - * name: sessionId - * required: true - * schema: - * type: string - * description: Session ID - * - in: path - * name: messageId - * required: true - * schema: - * type: integer - * description: Log entry ID - * responses: - * 200: - * description: Log entry details - * content: - * application/json: - * schema: - * $ref: '#/components/schemas/SessionMessageEntity' - * 404: - * description: Agent, session, or message entry not found - * content: - * application/json: - * schema: - * $ref: '#/components/schemas/Error' - * 500: - * description: Internal server error - * content: - * application/json: - * schema: - * $ref: '#/components/schemas/Error' - */ - sessionMessagesRouter.get( - '/:messageId', - validateAgentId, - validateSessionId, - validateMessageId, - checkAgentAndSessionExist, - handleValidationErrors, - async (req: Request, res: Response) => { - try { - const { sessionId, messageId } = req.params - const messageIdNum = parseInt(messageId) - - logger.info(`Getting message entry: ${messageId} for session: ${sessionId}`) - - const message = await sessionMessageService.getSessionMessage(messageIdNum) - - if (!message) { - logger.warn(`Message entry not found: ${messageId}`) - return res.status(404).json({ - error: { - message: 'Message entry not found', - type: 'not_found', - code: 'message_not_found' - } - }) - } - - // Verify message belongs to the session - if (message.session_id !== sessionId) { - logger.warn(`Message entry ${messageId} does not belong to session ${sessionId}`) - return res.status(404).json({ - error: { - message: 'Message entry not found for this session', - type: 'not_found', - code: 'message_not_found' - } - }) - } - - logger.info(`Message entry retrieved successfully: ${messageId}`) - return res.json(message) - } catch (error: any) { - logger.error('Error getting session message:', error) - return res.status(500).json({ - error: { - message: 'Failed to get message entry', - type: 'internal_error', - code: 'message_get_failed' - } - }) - } - } - ) - - /** - * @swagger - * /v1/agents/{agentId}/sessions/{sessionId}/messages/{messageId}: - * put: - * summary: Update message entry - * description: Updates an existing message entry for the specified session - * tags: [Session Messages] - * parameters: - * - in: path - * name: agentId - * required: true - * schema: - * type: string - * description: Agent ID - * - in: path - * name: sessionId - * required: true - * schema: - * type: string - * description: Session ID - * - in: path - * name: messageId - * required: true - * schema: - * type: integer - * description: Log entry ID - * requestBody: - * required: true - * content: - * application/json: - * schema: - * type: object - * properties: - * content: - * type: object - * description: Updated message content - * metadata: - * type: object - * description: Updated metadata - * responses: - * 200: - * description: Log entry updated successfully - * content: - * application/json: - * schema: - * $ref: '#/components/schemas/SessionMessageEntity' - * 400: - * description: Validation error - * content: - * application/json: - * schema: - * $ref: '#/components/schemas/Error' - * 404: - * description: Agent, session, or message entry not found - * content: - * application/json: - * schema: - * $ref: '#/components/schemas/Error' - * 500: - * description: Internal server error - * content: - * application/json: - * schema: - * $ref: '#/components/schemas/Error' - */ - sessionMessagesRouter.put( - '/:messageId', - validateAgentId, - validateSessionId, - validateMessageId, - checkAgentAndSessionExist, - validateSessionMessageUpdate, - handleValidationErrors, - async (req: Request, res: Response) => { - try { - const { sessionId, messageId } = req.params - const messageIdNum = parseInt(messageId) - - logger.info(`Updating message entry: ${messageId} for session: ${sessionId}`) - logger.debug('Update data:', req.body) - - // First check if log exists and belongs to session - const existingMessage = await sessionMessageService.getSessionMessage(messageIdNum) - if (!existingMessage || existingMessage.session_id !== sessionId) { - logger.warn(`Log entry ${messageId} not found for session ${sessionId}`) - return res.status(404).json({ - error: { - message: 'Message entry not found for this session', - type: 'not_found', - code: 'message_not_found' - } - }) - } - - const message = await sessionMessageService.updateSessionMessage(messageIdNum, req.body) - - if (!message) { - logger.warn(`Log entry not found for update: ${messageId}`) - return res.status(404).json({ - error: { - message: 'Message entry not found', - type: 'not_found', - code: 'message_not_found' - } - }) - } - - logger.info(`Log entry updated successfully: ${messageId}`) - return res.json(message) - } catch (error: any) { - logger.error('Error updating session message:', error) - return res.status(500).json({ - error: { - message: 'Failed to update message entry', - type: 'internal_error', - code: 'message_update_failed' - } - }) - } - } - ) - - /** - * @swagger - * /v1/agents/{agentId}/sessions/{sessionId}/messages/{messageId}: - * delete: - * summary: Delete message entry - * description: Deletes a specific message entry - * tags: [Session Messages] - * parameters: - * - in: path - * name: agentId - * required: true - * schema: - * type: string - * description: Agent ID - * - in: path - * name: sessionId - * required: true - * schema: - * type: string - * description: Session ID - * - in: path - * name: messageId - * required: true - * schema: - * type: integer - * description: Log entry ID - * responses: - * 204: - * description: Log entry deleted successfully - * 404: - * description: Agent, session, or message entry not found - * content: - * application/json: - * schema: - * $ref: '#/components/schemas/Error' - * 500: - * description: Internal server error - * content: - * application/json: - * schema: - * $ref: '#/components/schemas/Error' - */ - sessionMessagesRouter.delete( - '/:messageId', - validateAgentId, - validateSessionId, - validateMessageId, - checkAgentAndSessionExist, - handleValidationErrors, - async (req: Request, res: Response) => { - try { - const { sessionId, messageId } = req.params - const messageIdNum = parseInt(messageId) - - logger.info(`Deleting message entry: ${messageId} for session: ${sessionId}`) - - // First check if log exists and belongs to session - const existingMessage = await sessionMessageService.getSessionMessage(messageIdNum) - if (!existingMessage || existingMessage.session_id !== sessionId) { - logger.warn(`Log entry ${messageId} not found for session ${sessionId}`) - return res.status(404).json({ - error: { - message: 'Message entry not found for this session', - type: 'not_found', - code: 'message_not_found' - } - }) - } - - const deleted = await sessionMessageService.deleteSessionMessage(messageIdNum) - - if (!deleted) { - logger.warn(`Log entry not found for deletion: ${messageId}`) - return res.status(404).json({ - error: { - message: 'Message entry not found', - type: 'not_found', - code: 'message_not_found' - } - }) - } - - logger.info(`Log entry deleted successfully: ${messageId}`) - return res.status(204).send() - } catch (error: any) { - logger.error('Error deleting session message:', error) - return res.status(500).json({ - error: { - message: 'Failed to delete message entry', - type: 'internal_error', - code: 'message_delete_failed' - } - }) - } - } - ) - - return sessionMessagesRouter -} - -// Convenience routes (standalone session messages without agent context) -/** - * @swagger - * /v1/sessions/{sessionId}/messages: - * get: - * summary: List message entries for a session (convenience endpoint) - * description: Retrieves a paginated list of message entries for the specified session without requiring agent context - * tags: [Session Messages] - * parameters: - * - in: path - * name: sessionId - * required: true - * schema: - * type: string - * description: Session ID - * - in: query - * name: limit - * schema: - * type: integer - * minimum: 1 - * maximum: 100 - * default: 50 - * description: Number of message entries to return - * - in: query - * name: offset - * schema: - * type: integer - * minimum: 0 - * default: 0 - * description: Number of message entries to skip - * responses: - * 200: - * description: List of message entries - * content: - * application/json: - * schema: - * type: object - * properties: - * data: - * type: array - * items: - * $ref: '#/components/schemas/SessionMessageEntity' - * total: - * type: integer - * description: Total number of message entries - * limit: - * type: integer - * description: Number of message entries returned - * offset: - * type: integer - * description: Number of message entries skipped - * 400: - * description: Validation error - * content: - * application/json: - * schema: - * $ref: '#/components/schemas/Error' - * 404: - * description: Session not found - * content: - * application/json: - * schema: - * $ref: '#/components/schemas/Error' - * 500: - * description: Internal server error - * content: - * application/json: - * schema: - * $ref: '#/components/schemas/Error' - */ -router.get( - '/:sessionId/messages', - validateSessionId, - validatePagination, - handleValidationErrors, - async (req: Request, res: Response) => { - try { - const { sessionId } = req.params - const limit = req.query.limit ? parseInt(req.query.limit as string) : 50 - const offset = req.query.offset ? parseInt(req.query.offset as string) : 0 - - // Check if session exists - const sessionExists = await sessionService.sessionExists(sessionId) - if (!sessionExists) { - return res.status(404).json({ - error: { - message: 'Session not found', - type: 'not_found', - code: 'session_not_found' - } - }) - } - - logger.info(`Listing messages for session: ${sessionId} with limit=${limit}, offset=${offset}`) - - const result = await sessionMessageService.listSessionMessages(sessionId, { limit, offset }) - - logger.info(`Retrieved ${result.messages.length} messages (total: ${result.total}) for session: ${sessionId}`) - return res.json({ - data: result.messages, - total: result.total, - limit, - offset - }) - } catch (error: any) { - logger.error('Error listing session messages:', error) - return res.status(500).json({ - error: { - message: 'Failed to list message entries', - type: 'internal_error', - code: 'message_list_failed' - } - }) - } - } -) - -/** - * @swagger - * /v1/session-messages/{messageId}: - * get: - * summary: Get message entry by ID (convenience endpoint) - * description: Retrieves a specific message entry without requiring agent or session context - * tags: [Session Messages] - * parameters: - * - in: path - * name: messageId - * required: true - * schema: - * type: integer - * description: Log entry ID - * responses: - * 200: - * description: Log entry details - * content: - * application/json: - * schema: - * $ref: '#/components/schemas/SessionMessageEntity' - * 404: - * description: Log entry not found - * content: - * application/json: - * schema: - * $ref: '#/components/schemas/Error' - * 500: - * description: Internal server error - * content: - * application/json: - * schema: - * $ref: '#/components/schemas/Error' - */ -router.get( - '/session-messages/:messageId', - validateMessageId, - handleValidationErrors, - async (req: Request, res: Response) => { - try { - const { messageId } = req.params - const messageIdNum = parseInt(messageId) - - logger.info(`Getting message entry: ${messageId}`) - - const message = await sessionMessageService.getSessionMessage(messageIdNum) - - if (!message) { - logger.warn(`Log entry not found: ${messageId}`) - return res.status(404).json({ - error: { - message: 'Log entry not found', - type: 'not_found', - code: 'message_not_found' - } - }) - } - - logger.info(`Log entry retrieved successfully: ${messageId}`) - return res.json(message) - } catch (error: any) { - logger.error('Error getting session message:', error) - return res.status(500).json({ - error: { - message: 'Failed to get message entry', - type: 'internal_error', - code: 'message_get_failed' - } - }) - } - } -) - -export { createSessionMessagesRouter, router as sessionMessagesRoutes } diff --git a/src/main/apiServer/routes/sessions.ts b/src/main/apiServer/routes/sessions.ts deleted file mode 100644 index 7677704a33..0000000000 --- a/src/main/apiServer/routes/sessions.ts +++ /dev/null @@ -1,1007 +0,0 @@ -import express, { Request, Response } from 'express' -import { body, param, query, validationResult } from 'express-validator' - -import { agentService, sessionService } from '../../services/agents' -import { loggerService } from '../../services/LoggerService' - -const logger = loggerService.withContext('ApiServerSessionsRoutes') - -const router = express.Router() - -// Validation middleware -const validateSession = [ - body('name').optional().isString(), - body('sub_agent_ids').optional().isArray(), - body('user_goal').optional().isString(), - body('status').optional().isIn(['idle', 'running', 'completed', 'failed', 'stopped']), - body('external_session_id').optional().isString(), - body('model').optional().isString(), - body('plan_model').optional().isString(), - body('small_model').optional().isString(), - body('built_in_tools').optional().isArray(), - body('mcps').optional().isArray(), - body('knowledges').optional().isArray(), - body('configuration').optional().isObject(), - body('accessible_paths').optional().isArray(), - body('permission_mode').optional().isIn(['readOnly', 'acceptEdits', 'bypassPermissions']), - body('max_steps').optional().isInt({ min: 1 }) -] - -const validateSessionUpdate = [ - body('name').optional().isString(), - body('main_agent_id').optional().notEmpty().withMessage('Main agent ID cannot be empty'), - body('sub_agent_ids').optional().isArray(), - body('user_goal').optional().isString(), - body('status').optional().isIn(['idle', 'running', 'completed', 'failed', 'stopped']), - body('external_session_id').optional().isString(), - body('model').optional().isString(), - body('plan_model').optional().isString(), - body('small_model').optional().isString(), - body('built_in_tools').optional().isArray(), - body('mcps').optional().isArray(), - body('knowledges').optional().isArray(), - body('configuration').optional().isObject(), - body('accessible_paths').optional().isArray(), - body('permission_mode').optional().isIn(['readOnly', 'acceptEdits', 'bypassPermissions']), - body('max_steps').optional().isInt({ min: 1 }) -] - -const validateStatusUpdate = [ - body('status') - .notEmpty() - .isIn(['idle', 'running', 'completed', 'failed', 'stopped']) - .withMessage('Valid status is required') -] - -const validateAgentId = [param('agentId').notEmpty().withMessage('Agent ID is required')] - -const validateSessionId = [param('sessionId').notEmpty().withMessage('Session ID is required')] - -const validatePagination = [ - query('limit').optional().isInt({ min: 1, max: 100 }).withMessage('Limit must be between 1 and 100'), - query('offset').optional().isInt({ min: 0 }).withMessage('Offset must be non-negative'), - query('status') - .optional() - .isIn(['idle', 'running', 'completed', 'failed', 'stopped']) - .withMessage('Invalid status filter') -] - -// Error handler for validation -const handleValidationErrors = (req: Request, res: Response, next: any): void => { - const errors = validationResult(req) - if (!errors.isEmpty()) { - res.status(400).json({ - error: { - message: 'Validation failed', - type: 'validation_error', - details: errors.array() - } - }) - return - } - next() -} - -// Middleware to check if agent exists -const checkAgentExists = async (req: Request, res: Response, next: any): Promise => { - try { - const { agentId } = req.params - const exists = await agentService.agentExists(agentId) - - if (!exists) { - res.status(404).json({ - error: { - message: 'Agent not found', - type: 'not_found', - code: 'agent_not_found' - } - }) - return - } - - next() - } catch (error) { - logger.error('Error checking agent existence:', error as Error) - res.status(500).json({ - error: { - message: 'Failed to validate agent', - type: 'internal_error', - code: 'agent_validation_failed' - } - }) - } -} - -/** - * @swagger - * components: - * schemas: - * AgentSessionEntity: - * type: object - * properties: - * id: - * type: string - * description: Unique session identifier - * name: - * type: string - * description: Session name - * main_agent_id: - * type: string - * description: Primary agent ID - * sub_agent_ids: - * type: array - * items: - * type: string - * description: Sub-agent IDs - * user_goal: - * type: string - * description: Initial user goal - * status: - * type: string - * enum: [idle, running, completed, failed, stopped] - * description: Session status - * external_session_id: - * type: string - * description: External session tracking ID - * model: - * type: string - * description: Override model ID - * plan_model: - * type: string - * description: Override planning model ID - * small_model: - * type: string - * description: Override small/fast model ID - * built_in_tools: - * type: array - * items: - * type: string - * description: Override built-in tool IDs - * mcps: - * type: array - * items: - * type: string - * description: Override MCP tool IDs - * knowledges: - * type: array - * items: - * type: string - * description: Override knowledge base IDs - * configuration: - * type: object - * description: Override configuration settings - * accessible_paths: - * type: array - * items: - * type: string - * description: Override accessible directory paths - * permission_mode: - * type: string - * enum: [readOnly, acceptEdits, bypassPermissions] - * description: Override permission mode - * max_steps: - * type: integer - * description: Override maximum steps - * created_at: - * type: string - * format: date-time - * updated_at: - * type: string - * format: date-time - * required: - * - id - * - main_agent_id - * - status - * - created_at - * - updated_at - * CreateSessionRequest: - * type: object - * properties: - * name: - * type: string - * description: Session name - * sub_agent_ids: - * type: array - * items: - * type: string - * description: Sub-agent IDs - * user_goal: - * type: string - * description: Initial user goal - * status: - * type: string - * enum: [idle, running, completed, failed, stopped] - * description: Session status - * external_session_id: - * type: string - * description: External session tracking ID - * model: - * type: string - * description: Override model ID - * plan_model: - * type: string - * description: Override planning model ID - * small_model: - * type: string - * description: Override small/fast model ID - * built_in_tools: - * type: array - * items: - * type: string - * description: Override built-in tool IDs - * mcps: - * type: array - * items: - * type: string - * description: Override MCP tool IDs - * knowledges: - * type: array - * items: - * type: string - * description: Override knowledge base IDs - * configuration: - * type: object - * description: Override configuration settings - * accessible_paths: - * type: array - * items: - * type: string - * description: Override accessible directory paths - * permission_mode: - * type: string - * enum: [readOnly, acceptEdits, bypassPermissions] - * description: Override permission mode - * max_steps: - * type: integer - * description: Override maximum steps - */ - -// Create nested session router -function createSessionsRouter(): express.Router { - const sessionsRouter = express.Router({ mergeParams: true }) - - /** - * @swagger - * /v1/agents/{agentId}/sessions: - * post: - * summary: Create a new session for an agent - * description: Creates a new session for the specified agent - * tags: [Sessions] - * parameters: - * - in: path - * name: agentId - * required: true - * schema: - * type: string - * description: Agent ID - * requestBody: - * required: true - * content: - * application/json: - * schema: - * $ref: '#/components/schemas/CreateSessionRequest' - * responses: - * 201: - * description: Session created successfully - * content: - * application/json: - * schema: - * $ref: '#/components/schemas/AgentSessionEntity' - * 400: - * description: Validation error - * content: - * application/json: - * schema: - * $ref: '#/components/schemas/Error' - * 404: - * description: Agent not found - * content: - * application/json: - * schema: - * $ref: '#/components/schemas/Error' - * 500: - * description: Internal server error - * content: - * application/json: - * schema: - * $ref: '#/components/schemas/Error' - */ - sessionsRouter.post( - '/', - validateAgentId, - checkAgentExists, - validateSession, - handleValidationErrors, - async (req: Request, res: Response) => { - try { - const { agentId } = req.params - const sessionData = { ...req.body, main_agent_id: agentId } - - logger.info(`Creating new session for agent: ${agentId}`) - logger.debug('Session data:', sessionData) - - const session = await sessionService.createSession(sessionData) - - logger.info(`Session created successfully: ${session.id}`) - return res.status(201).json(session) - } catch (error: any) { - logger.error('Error creating session:', error) - return res.status(500).json({ - error: { - message: 'Failed to create session', - type: 'internal_error', - code: 'session_creation_failed' - } - }) - } - } - ) - - /** - * @swagger - * /v1/agents/{agentId}/sessions: - * get: - * summary: List sessions for an agent - * description: Retrieves a paginated list of sessions for the specified agent - * tags: [Sessions] - * parameters: - * - in: path - * name: agentId - * required: true - * schema: - * type: string - * description: Agent ID - * - in: query - * name: limit - * schema: - * type: integer - * minimum: 1 - * maximum: 100 - * default: 20 - * description: Number of sessions to return - * - in: query - * name: offset - * schema: - * type: integer - * minimum: 0 - * default: 0 - * description: Number of sessions to skip - * - in: query - * name: status - * schema: - * type: string - * enum: [idle, running, completed, failed, stopped] - * description: Filter by session status - * responses: - * 200: - * description: List of sessions - * content: - * application/json: - * schema: - * type: object - * properties: - * data: - * type: array - * items: - * $ref: '#/components/schemas/AgentSessionEntity' - * total: - * type: integer - * description: Total number of sessions - * limit: - * type: integer - * description: Number of sessions returned - * offset: - * type: integer - * description: Number of sessions skipped - * 400: - * description: Validation error - * content: - * application/json: - * schema: - * $ref: '#/components/schemas/Error' - * 404: - * description: Agent not found - * content: - * application/json: - * schema: - * $ref: '#/components/schemas/Error' - * 500: - * description: Internal server error - * content: - * application/json: - * schema: - * $ref: '#/components/schemas/Error' - */ - sessionsRouter.get( - '/', - validateAgentId, - checkAgentExists, - validatePagination, - handleValidationErrors, - async (req: Request, res: Response) => { - try { - const { agentId } = req.params - const limit = req.query.limit ? parseInt(req.query.limit as string) : 20 - const offset = req.query.offset ? parseInt(req.query.offset as string) : 0 - const status = req.query.status as any - - logger.info(`Listing sessions for agent: ${agentId} with limit=${limit}, offset=${offset}, status=${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({ - data: result.sessions, - total: result.total, - limit, - offset - }) - } catch (error: any) { - logger.error('Error listing sessions:', error) - return res.status(500).json({ - error: { - message: 'Failed to list sessions', - type: 'internal_error', - code: 'session_list_failed' - } - }) - } - } - ) - - /** - * @swagger - * /v1/agents/{agentId}/sessions/{sessionId}: - * get: - * summary: Get session by ID - * description: Retrieves a specific session for the specified agent - * tags: [Sessions] - * parameters: - * - in: path - * name: agentId - * required: true - * schema: - * type: string - * description: Agent ID - * - in: path - * name: sessionId - * required: true - * schema: - * type: string - * description: Session ID - * responses: - * 200: - * description: Session details - * content: - * application/json: - * schema: - * $ref: '#/components/schemas/AgentSessionEntity' - * 404: - * description: Agent or session not found - * content: - * application/json: - * schema: - * $ref: '#/components/schemas/Error' - * 500: - * description: Internal server error - * content: - * application/json: - * schema: - * $ref: '#/components/schemas/Error' - */ - sessionsRouter.get( - '/:sessionId', - validateAgentId, - validateSessionId, - checkAgentExists, - handleValidationErrors, - async (req: Request, res: Response) => { - try { - const { agentId, sessionId } = req.params - logger.info(`Getting session: ${sessionId} for agent: ${agentId}`) - - const session = await sessionService.getSession(sessionId) - - if (!session) { - logger.warn(`Session not found: ${sessionId}`) - return res.status(404).json({ - error: { - message: 'Session not found', - type: 'not_found', - code: 'session_not_found' - } - }) - } - - // Verify session belongs to the agent - if (session.main_agent_id !== agentId) { - logger.warn(`Session ${sessionId} does not belong to agent ${agentId}`) - return res.status(404).json({ - error: { - message: 'Session not found for this agent', - type: 'not_found', - code: 'session_not_found' - } - }) - } - - logger.info(`Session retrieved successfully: ${sessionId}`) - return res.json(session) - } catch (error: any) { - logger.error('Error getting session:', error) - return res.status(500).json({ - error: { - message: 'Failed to get session', - type: 'internal_error', - code: 'session_get_failed' - } - }) - } - } - ) - - /** - * @swagger - * /v1/agents/{agentId}/sessions/{sessionId}: - * put: - * summary: Replace session - * description: Completely replaces an existing session for the specified agent - * tags: [Sessions] - * parameters: - * - in: path - * name: agentId - * required: true - * schema: - * type: string - * description: Agent ID - * - in: path - * name: sessionId - * required: true - * schema: - * type: string - * description: Session ID - * requestBody: - * required: true - * content: - * application/json: - * schema: - * $ref: '#/components/schemas/CreateSessionRequest' - * responses: - * 200: - * description: Session replaced successfully - * content: - * application/json: - * schema: - * $ref: '#/components/schemas/AgentSessionEntity' - * 400: - * description: Validation error - * content: - * application/json: - * schema: - * $ref: '#/components/schemas/Error' - * 404: - * description: Agent or session not found - * content: - * application/json: - * schema: - * $ref: '#/components/schemas/Error' - * 500: - * description: Internal server error - * content: - * application/json: - * schema: - * $ref: '#/components/schemas/Error' - */ - sessionsRouter.put( - '/:sessionId', - validateAgentId, - validateSessionId, - checkAgentExists, - validateSessionUpdate, - handleValidationErrors, - async (req: Request, res: Response) => { - try { - const { agentId, sessionId } = req.params - logger.info(`Replacing session: ${sessionId} for agent: ${agentId}`) - logger.debug('Replace data:', req.body) - - // First check if session exists and belongs to agent - 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({ - error: { - message: 'Session not found for this agent', - type: 'not_found', - code: 'session_not_found' - } - }) - } - - // For PUT, we replace the entire resource - const sessionData = { ...req.body, main_agent_id: agentId } - const session = await sessionService.updateSession(sessionId, sessionData) - - if (!session) { - logger.warn(`Session not found for replace: ${sessionId}`) - return res.status(404).json({ - error: { - message: 'Session not found', - type: 'not_found', - code: 'session_not_found' - } - }) - } - - logger.info(`Session replaced successfully: ${sessionId}`) - return res.json(session) - } catch (error: any) { - logger.error('Error replacing session:', error) - return res.status(500).json({ - error: { - message: 'Failed to replace session', - type: 'internal_error', - code: 'session_replace_failed' - } - }) - } - } - ) - - /** - * @swagger - * /v1/agents/{agentId}/sessions/{sessionId}: - * patch: - * summary: Update session - * description: Updates an existing session for the specified agent - * tags: [Sessions] - * parameters: - * - in: path - * name: agentId - * required: true - * schema: - * type: string - * description: Agent ID - * - in: path - * name: sessionId - * required: true - * schema: - * type: string - * description: Session ID - * requestBody: - * required: true - * content: - * application/json: - * schema: - * $ref: '#/components/schemas/CreateSessionRequest' - * responses: - * 200: - * description: Session updated successfully - * content: - * application/json: - * schema: - * $ref: '#/components/schemas/AgentSessionEntity' - * 400: - * description: Validation error - * content: - * application/json: - * schema: - * $ref: '#/components/schemas/Error' - * 404: - * description: Agent or session not found - * content: - * application/json: - * schema: - * $ref: '#/components/schemas/Error' - * 500: - * description: Internal server error - * content: - * application/json: - * schema: - * $ref: '#/components/schemas/Error' - */ - sessionsRouter.patch( - '/:sessionId', - validateAgentId, - validateSessionId, - checkAgentExists, - validateSessionUpdate, - handleValidationErrors, - async (req: Request, res: Response) => { - try { - const { agentId, sessionId } = req.params - logger.info(`Updating session: ${sessionId} for agent: ${agentId}`) - logger.debug('Update data:', req.body) - - // First check if session exists and belongs to agent - 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({ - error: { - message: 'Session not found for this agent', - type: 'not_found', - code: 'session_not_found' - } - }) - } - const updateSession = { ...existingSession, ...req.body } - const session = await sessionService.updateSession(sessionId, updateSession) - - if (!session) { - logger.warn(`Session not found for update: ${sessionId}`) - return res.status(404).json({ - error: { - message: 'Session not found', - type: 'not_found', - code: 'session_not_found' - } - }) - } - - logger.info(`Session updated successfully: ${sessionId}`) - return res.json(session) - } catch (error: any) { - logger.error('Error updating session:', error) - return res.status(500).json({ - error: { - message: 'Failed to update session', - type: 'internal_error', - code: 'session_update_failed' - } - }) - } - } - ) - - /** - * @swagger - * /v1/agents/{agentId}/sessions/{sessionId}: - * delete: - * summary: Delete session - * description: Deletes a session and all associated logs - * tags: [Sessions] - * parameters: - * - in: path - * name: agentId - * required: true - * schema: - * type: string - * description: Agent ID - * - in: path - * name: sessionId - * required: true - * schema: - * type: string - * description: Session ID - * responses: - * 204: - * description: Session deleted successfully - * 404: - * description: Agent or session not found - * content: - * application/json: - * schema: - * $ref: '#/components/schemas/Error' - * 500: - * description: Internal server error - * content: - * application/json: - * schema: - * $ref: '#/components/schemas/Error' - */ - sessionsRouter.delete( - '/:sessionId', - validateAgentId, - validateSessionId, - checkAgentExists, - handleValidationErrors, - async (req: Request, res: Response) => { - try { - const { agentId, sessionId } = req.params - logger.info(`Deleting session: ${sessionId} for agent: ${agentId}`) - - // First check if session exists and belongs to agent - 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({ - error: { - message: 'Session not found for this agent', - type: 'not_found', - code: 'session_not_found' - } - }) - } - - const deleted = await sessionService.deleteSession(sessionId) - - if (!deleted) { - logger.warn(`Session not found for deletion: ${sessionId}`) - return res.status(404).json({ - error: { - message: 'Session not found', - type: 'not_found', - code: 'session_not_found' - } - }) - } - - logger.info(`Session deleted successfully: ${sessionId}`) - return res.status(204).send() - } catch (error: any) { - logger.error('Error deleting session:', error) - return res.status(500).json({ - error: { - message: 'Failed to delete session', - type: 'internal_error', - code: 'session_delete_failed' - } - }) - } - } - ) - - return sessionsRouter -} - -// Convenience routes (standalone sessions without agent context) -/** - * @swagger - * /v1/sessions: - * get: - * summary: List all sessions - * description: Retrieves a paginated list of all sessions across all agents - * tags: [Sessions] - * parameters: - * - in: query - * name: limit - * schema: - * type: integer - * minimum: 1 - * maximum: 100 - * default: 20 - * description: Number of sessions to return - * - in: query - * name: offset - * schema: - * type: integer - * minimum: 0 - * default: 0 - * description: Number of sessions to skip - * - in: query - * name: status - * schema: - * type: string - * enum: [idle, running, completed, failed, stopped] - * description: Filter by session status - * responses: - * 200: - * description: List of sessions - * content: - * application/json: - * schema: - * type: object - * properties: - * data: - * type: array - * items: - * $ref: '#/components/schemas/AgentSessionEntity' - * total: - * type: integer - * description: Total number of sessions - * limit: - * type: integer - * description: Number of sessions returned - * offset: - * type: integer - * description: Number of sessions skipped - * 400: - * description: Validation error - * content: - * application/json: - * schema: - * $ref: '#/components/schemas/Error' - * 500: - * description: Internal server error - * content: - * application/json: - * schema: - * $ref: '#/components/schemas/Error' - */ -router.get('/', validatePagination, handleValidationErrors, async (req: Request, res: Response) => { - try { - const limit = req.query.limit ? parseInt(req.query.limit as string) : 20 - const offset = req.query.offset ? parseInt(req.query.offset as string) : 0 - const status = req.query.status as any - - logger.info(`Listing all sessions with limit=${limit}, offset=${offset}, status=${status}`) - - const result = await sessionService.listSessions(undefined, { limit, offset, status }) - - logger.info(`Retrieved ${result.sessions.length} sessions (total: ${result.total})`) - return res.json({ - data: result.sessions, - total: result.total, - limit, - offset - }) - } catch (error: any) { - logger.error('Error listing all sessions:', error) - return res.status(500).json({ - error: { - message: 'Failed to list sessions', - type: 'internal_error', - code: 'session_list_failed' - } - }) - } -}) - -/** - * @swagger - * /v1/sessions/{sessionId}: - * get: - * summary: Get session by ID (convenience endpoint) - * description: Retrieves a specific session without requiring agent context - * tags: [Sessions] - * parameters: - * - in: path - * name: sessionId - * required: true - * schema: - * type: string - * description: Session ID - * responses: - * 200: - * description: Session details - * content: - * application/json: - * schema: - * $ref: '#/components/schemas/AgentSessionEntity' - * 404: - * description: Session not found - * content: - * application/json: - * schema: - * $ref: '#/components/schemas/Error' - * 500: - * description: Internal server error - * content: - * application/json: - * schema: - * $ref: '#/components/schemas/Error' - */ -router.get('/:sessionId', validateSessionId, handleValidationErrors, async (req: Request, res: Response) => { - try { - const { sessionId } = req.params - logger.info(`Getting session: ${sessionId}`) - - const session = await sessionService.getSession(sessionId) - - if (!session) { - logger.warn(`Session not found: ${sessionId}`) - return res.status(404).json({ - error: { - message: 'Session not found', - type: 'not_found', - code: 'session_not_found' - } - }) - } - - logger.info(`Session retrieved successfully: ${sessionId}`) - return res.json(session) - } catch (error: any) { - logger.error('Error getting session:', error) - return res.status(500).json({ - error: { - message: 'Failed to get session', - type: 'internal_error', - code: 'session_get_failed' - } - }) - } -}) - -export { createSessionsRouter, router as sessionsRoutes } diff --git a/src/main/services/agents/services/SessionMessageService.ts b/src/main/services/agents/services/SessionMessageService.ts index 9f1fc0ddbc..4a02c56b8c 100644 --- a/src/main/services/agents/services/SessionMessageService.ts +++ b/src/main/services/agents/services/SessionMessageService.ts @@ -80,7 +80,11 @@ export class SessionMessageService extends BaseService { async getSessionMessage(id: number): Promise { this.ensureInitialized() - const result = await this.database.select().from(sessionMessagesTable).where(eq(sessionMessagesTable.id, id)).limit(1) + const result = await this.database + .select() + .from(sessionMessagesTable) + .where(eq(sessionMessagesTable.id, id)) + .limit(1) if (!result[0]) { return null