From 2ec3b20b23e8154859fdaa8330d4243ef3256611 Mon Sep 17 00:00:00 2001 From: Vaayne Date: Thu, 11 Sep 2025 14:35:34 +0800 Subject: [PATCH] =?UTF-8?q?=F0=9F=9A=80=20feat:=20add=20comprehensive=20RE?= =?UTF-8?q?ST=20API=20for=20agent=20management?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Implement full REST API with Express routes for agents, sessions, and logs: - CRUD operations for agents with validation and OpenAPI documentation - Session management with nested resource endpoints - Hierarchical logging system with bulk operations support - Request validation using express-validator - Proper error handling and structured responses --- src/main/apiServer/app.ts | 11 +- src/main/apiServer/routes/agents.ts | 564 ++++++++++++ src/main/apiServer/routes/session-logs.ts | 986 ++++++++++++++++++++ src/main/apiServer/routes/sessions.ts | 1013 +++++++++++++++++++++ src/main/index.ts | 9 + 5 files changed, 2582 insertions(+), 1 deletion(-) create mode 100644 src/main/apiServer/routes/agents.ts create mode 100644 src/main/apiServer/routes/session-logs.ts create mode 100644 src/main/apiServer/routes/sessions.ts diff --git a/src/main/apiServer/app.ts b/src/main/apiServer/app.ts index 46da10f876..bd67152cf0 100644 --- a/src/main/apiServer/app.ts +++ b/src/main/apiServer/app.ts @@ -6,9 +6,12 @@ import { v4 as uuidv4 } from 'uuid' import { authMiddleware } from './middleware/auth' import { errorHandler } from './middleware/error' import { setupOpenAPIDocumentation } from './middleware/openapi' +import { agentsRoutes } from './routes/agents' import { chatRoutes } from './routes/chat' import { mcpRoutes } from './routes/mcp' import { modelsRoutes } from './routes/models' +import { sessionLogsRoutes } from './routes/session-logs' +import { sessionsRoutes } from './routes/sessions' const logger = loggerService.withContext('ApiServer') @@ -104,7 +107,10 @@ app.get('/', (_req, res) => { health: 'GET /health', models: 'GET /v1/models', chat: 'POST /v1/chat/completions', - mcp: 'GET /v1/mcps' + mcp: 'GET /v1/mcps', + agents: 'GET /v1/agents', + sessions: 'GET /v1/sessions', + logs: 'GET /v1/sessions/{sessionId}/logs' } }) }) @@ -117,6 +123,9 @@ apiRouter.use(express.json()) apiRouter.use('/chat', chatRoutes) apiRouter.use('/mcps', mcpRoutes) apiRouter.use('/models', modelsRoutes) +apiRouter.use('/agents', agentsRoutes) +apiRouter.use('/sessions', sessionsRoutes) +apiRouter.use('/', sessionLogsRoutes) // This handles /sessions/:sessionId/logs and /session-logs/:logId app.use('/v1', apiRouter) // Setup OpenAPI documentation diff --git a/src/main/apiServer/routes/agents.ts b/src/main/apiServer/routes/agents.ts new file mode 100644 index 0000000000..f4b2761390 --- /dev/null +++ b/src/main/apiServer/routes/agents.ts @@ -0,0 +1,564 @@ +import express, { Request, Response } from 'express' +import { body, param, query, validationResult } from 'express-validator' + +import { agentService } from '../../services/agents/AgentService' +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 + */ + +/** + * @swagger + * /v1/agents: + * post: + * summary: Create a new agent + * description: Creates a new autonomous agent with the specified configuration + * tags: [Agents] + * requestBody: + * required: true + * content: + * application/json: + * schema: + * $ref: '#/components/schemas/CreateAgentRequest' + * responses: + * 201: + * description: Agent created successfully + * content: + * application/json: + * schema: + * $ref: '#/components/schemas/AgentEntity' + * 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.post('/', validateAgent, handleValidationErrors, async (req: Request, res: Response) => { + try { + logger.info('Creating new agent') + logger.debug('Agent data:', req.body) + + const agent = await agentService.createAgent(req.body) + + logger.info(`Agent created successfully: ${agent.id}`) + return res.status(201).json(agent) + } catch (error: any) { + logger.error('Error creating agent:', error) + return res.status(500).json({ + error: { + message: 'Failed to create agent', + type: 'internal_error', + code: 'agent_creation_failed' + } + }) + } +}) + +/** + * @swagger + * /v1/agents: + * get: + * summary: List all agents + * description: Retrieves a paginated list of all agents + * tags: [Agents] + * parameters: + * - in: query + * name: limit + * schema: + * type: integer + * minimum: 1 + * maximum: 100 + * default: 20 + * description: Number of agents to return + * - in: query + * name: offset + * schema: + * type: integer + * minimum: 0 + * default: 0 + * description: Number of agents to skip + * responses: + * 200: + * description: List of agents + * content: + * application/json: + * schema: + * type: object + * properties: + * data: + * type: array + * items: + * $ref: '#/components/schemas/AgentEntity' + * total: + * type: integer + * description: Total number of agents + * limit: + * type: integer + * description: Number of agents returned + * offset: + * type: integer + * description: Number of agents 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 + + logger.info(`Listing agents with limit=${limit}, offset=${offset}`) + + const result = await agentService.listAgents({ limit, offset }) + + logger.info(`Retrieved ${result.agents.length} agents (total: ${result.total})`) + return res.json({ + data: result.agents, + total: result.total, + limit, + offset + }) + } catch (error: any) { + logger.error('Error listing agents:', error) + return res.status(500).json({ + error: { + message: 'Failed to list agents', + type: 'internal_error', + code: 'agent_list_failed' + } + }) + } +}) + +/** + * @swagger + * /v1/agents/{agentId}: + * get: + * summary: Get agent by ID + * description: Retrieves a specific agent by its ID + * tags: [Agents] + * parameters: + * - in: path + * name: agentId + * required: true + * schema: + * type: string + * description: Agent ID + * responses: + * 200: + * description: Agent details + * content: + * application/json: + * schema: + * $ref: '#/components/schemas/AgentEntity' + * 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' + */ +router.get('/:agentId', validateAgentId, handleValidationErrors, async (req: Request, res: Response) => { + try { + const { agentId } = req.params + logger.info(`Getting agent: ${agentId}`) + + const agent = await agentService.getAgent(agentId) + + if (!agent) { + logger.warn(`Agent not found: ${agentId}`) + return res.status(404).json({ + error: { + message: 'Agent not found', + type: 'not_found', + code: 'agent_not_found' + } + }) + } + + logger.info(`Agent retrieved successfully: ${agentId}`) + return res.json(agent) + } catch (error: any) { + logger.error('Error getting agent:', error) + return res.status(500).json({ + error: { + message: 'Failed to get agent', + type: 'internal_error', + code: 'agent_get_failed' + } + }) + } +}) + +/** + * @swagger + * /v1/agents/{agentId}: + * put: + * summary: Update agent + * description: Updates an existing agent with the provided data + * tags: [Agents] + * parameters: + * - in: path + * name: agentId + * required: true + * schema: + * type: string + * description: Agent ID + * requestBody: + * required: true + * content: + * application/json: + * schema: + * $ref: '#/components/schemas/CreateAgentRequest' + * responses: + * 200: + * description: Agent updated successfully + * content: + * application/json: + * schema: + * $ref: '#/components/schemas/AgentEntity' + * 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' + */ +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) + + 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({ + error: { + message: 'Failed to update agent', + type: 'internal_error', + code: 'agent_update_failed' + } + }) + } + } +) + +/** + * @swagger + * /v1/agents/{agentId}: + * delete: + * summary: Delete agent + * description: Deletes an agent and all associated sessions and logs + * tags: [Agents] + * parameters: + * - in: path + * name: agentId + * required: true + * schema: + * type: string + * description: Agent ID + * responses: + * 204: + * description: Agent deleted successfully + * 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' + */ +router.delete('/:agentId', validateAgentId, handleValidationErrors, async (req: Request, res: Response) => { + try { + const { agentId } = req.params + logger.info(`Deleting agent: ${agentId}`) + + const deleted = await agentService.deleteAgent(agentId) + + if (!deleted) { + logger.warn(`Agent not found for deletion: ${agentId}`) + return res.status(404).json({ + error: { + message: 'Agent not found', + type: 'not_found', + code: 'agent_not_found' + } + }) + } + + logger.info(`Agent deleted successfully: ${agentId}`) + return res.status(204).send() + } catch (error: any) { + logger.error('Error deleting agent:', error) + return res.status(500).json({ + error: { + message: 'Failed to delete agent', + type: 'internal_error', + code: 'agent_delete_failed' + } + }) + } +}) + +// Mount session routes as nested resources +import { createSessionLogsRouter } from './session-logs' +import { createSessionsRouter } from './sessions' + +const sessionsRouter = createSessionsRouter() +const sessionLogsRouter = createSessionLogsRouter() + +// Mount nested routes +router.use('/:agentId/sessions', sessionsRouter) +router.use('/:agentId/sessions/:sessionId/logs', sessionLogsRouter) + +export { router as agentsRoutes } diff --git a/src/main/apiServer/routes/session-logs.ts b/src/main/apiServer/routes/session-logs.ts new file mode 100644 index 0000000000..dec733250b --- /dev/null +++ b/src/main/apiServer/routes/session-logs.ts @@ -0,0 +1,986 @@ +import express, { Request, Response } from 'express' +import { body, param, query, validationResult } from 'express-validator' + +import { agentService } from '../../services/agents/AgentService' +import { loggerService } from '../../services/LoggerService' + +const logger = loggerService.withContext('ApiServerSessionLogsRoutes') + +const router = express.Router() + +// Validation middleware +const validateSessionLog = [ + 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 validateSessionLogUpdate = [ + body('content').optional().isObject().withMessage('Content must be a valid object'), + body('metadata').optional().isObject().withMessage('Metadata must be a valid object') +] + +const validateBulkSessionLogs = [ + 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 validateLogId = [param('logId').isInt({ min: 1 }).withMessage('Log 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 agentService.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: + * SessionLogEntity: + * type: object + * properties: + * id: + * type: integer + * description: Unique log entry identifier + * session_id: + * type: string + * description: Reference to session + * parent_id: + * type: integer + * description: Parent log entry ID for tree structure + * role: + * type: string + * enum: [user, agent, system, tool] + * description: Role that created the log entry + * type: + * type: string + * description: Type of log entry + * content: + * type: object + * description: JSON structured log 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 + * CreateSessionLogRequest: + * type: object + * properties: + * parent_id: + * type: integer + * description: Parent log entry ID for tree structure + * role: + * type: string + * enum: [user, agent, system, tool] + * description: Role that created the log entry + * type: + * type: string + * description: Type of log entry + * content: + * type: object + * description: JSON structured log data + * metadata: + * type: object + * description: Additional metadata + * required: + * - role + * - type + * - content + */ + +// Create nested session logs router +function createSessionLogsRouter(): express.Router { + const sessionLogsRouter = express.Router({ mergeParams: true }) + + /** + * @swagger + * /v1/agents/{agentId}/sessions/{sessionId}/logs: + * post: + * summary: Create a new log entry for a session + * description: Creates a new log entry for the specified session + * tags: [Session Logs] + * 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/CreateSessionLogRequest' + * responses: + * 201: + * description: Log entry created successfully + * content: + * application/json: + * schema: + * $ref: '#/components/schemas/SessionLogEntity' + * 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' + */ + sessionLogsRouter.post( + '/', + validateAgentId, + validateSessionId, + checkAgentAndSessionExist, + validateSessionLog, + handleValidationErrors, + async (req: Request, res: Response) => { + try { + const { sessionId } = req.params + const logData = { ...req.body, session_id: sessionId } + + logger.info(`Creating new log entry for session: ${sessionId}`) + logger.debug('Log data:', logData) + + const log = await agentService.createSessionLog(logData) + + logger.info(`Log entry created successfully: ${log.id}`) + return res.status(201).json(log) + } catch (error: any) { + logger.error('Error creating session log:', error) + return res.status(500).json({ + error: { + message: 'Failed to create log entry', + type: 'internal_error', + code: 'log_creation_failed' + } + }) + } + } + ) + + /** + * @swagger + * /v1/agents/{agentId}/sessions/{sessionId}/logs/bulk: + * post: + * summary: Create multiple log entries for a session + * description: Creates multiple log entries for the specified session in a single request + * tags: [Session Logs] + * 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/CreateSessionLogRequest' + * responses: + * 201: + * description: Log entries created successfully + * content: + * application/json: + * schema: + * type: object + * properties: + * data: + * type: array + * items: + * $ref: '#/components/schemas/SessionLogEntity' + * count: + * type: integer + * description: Number of log 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' + */ + sessionLogsRouter.post( + '/bulk', + validateAgentId, + validateSessionId, + checkAgentAndSessionExist, + validateBulkSessionLogs, + handleValidationErrors, + async (req: Request, res: Response) => { + try { + const { sessionId } = req.params + const logsData = req.body.map((logData: any) => ({ ...logData, session_id: sessionId })) + + logger.info(`Creating ${logsData.length} log entries for session: ${sessionId}`) + + const logs = await agentService.bulkCreateSessionLogs(logsData) + + logger.info(`${logs.length} log entries created successfully for session: ${sessionId}`) + return res.status(201).json({ + data: logs, + count: logs.length + }) + } catch (error: any) { + logger.error('Error creating bulk session logs:', error) + return res.status(500).json({ + error: { + message: 'Failed to create log entries', + type: 'internal_error', + code: 'bulk_log_creation_failed' + } + }) + } + } + ) + + /** + * @swagger + * /v1/agents/{agentId}/sessions/{sessionId}/logs: + * get: + * summary: List log entries for a session + * description: Retrieves a paginated list of log entries for the specified session + * tags: [Session Logs] + * 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 log entries to return + * - in: query + * name: offset + * schema: + * type: integer + * minimum: 0 + * default: 0 + * description: Number of log entries to skip + * responses: + * 200: + * description: List of log entries + * content: + * application/json: + * schema: + * type: object + * properties: + * data: + * type: array + * items: + * $ref: '#/components/schemas/SessionLogEntity' + * total: + * type: integer + * description: Total number of log entries + * limit: + * type: integer + * description: Number of log entries returned + * offset: + * type: integer + * description: Number of log 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' + */ + sessionLogsRouter.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 logs for session: ${sessionId} with limit=${limit}, offset=${offset}`) + + const result = await agentService.listSessionLogs(sessionId, { limit, offset }) + + logger.info(`Retrieved ${result.logs.length} logs (total: ${result.total}) for session: ${sessionId}`) + return res.json({ + data: result.logs, + total: result.total, + limit, + offset + }) + } catch (error: any) { + logger.error('Error listing session logs:', error) + return res.status(500).json({ + error: { + message: 'Failed to list log entries', + type: 'internal_error', + code: 'log_list_failed' + } + }) + } + } + ) + + /** + * @swagger + * /v1/agents/{agentId}/sessions/{sessionId}/logs/{logId}: + * get: + * summary: Get log entry by ID + * description: Retrieves a specific log entry for the specified session + * tags: [Session Logs] + * 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: logId + * required: true + * schema: + * type: integer + * description: Log entry ID + * responses: + * 200: + * description: Log entry details + * content: + * application/json: + * schema: + * $ref: '#/components/schemas/SessionLogEntity' + * 404: + * description: Agent, session, or 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' + */ + sessionLogsRouter.get( + '/:logId', + validateAgentId, + validateSessionId, + validateLogId, + checkAgentAndSessionExist, + handleValidationErrors, + async (req: Request, res: Response) => { + try { + const { sessionId, logId } = req.params + const logIdNum = parseInt(logId) + + logger.info(`Getting log entry: ${logId} for session: ${sessionId}`) + + const log = await agentService.getSessionLog(logIdNum) + + if (!log) { + logger.warn(`Log entry not found: ${logId}`) + return res.status(404).json({ + error: { + message: 'Log entry not found', + type: 'not_found', + code: 'log_not_found' + } + }) + } + + // Verify log belongs to the session + if (log.session_id !== sessionId) { + logger.warn(`Log entry ${logId} does not belong to session ${sessionId}`) + return res.status(404).json({ + error: { + message: 'Log entry not found for this session', + type: 'not_found', + code: 'log_not_found' + } + }) + } + + logger.info(`Log entry retrieved successfully: ${logId}`) + return res.json(log) + } catch (error: any) { + logger.error('Error getting session log:', error) + return res.status(500).json({ + error: { + message: 'Failed to get log entry', + type: 'internal_error', + code: 'log_get_failed' + } + }) + } + } + ) + + /** + * @swagger + * /v1/agents/{agentId}/sessions/{sessionId}/logs/{logId}: + * put: + * summary: Update log entry + * description: Updates an existing log entry for the specified session + * tags: [Session Logs] + * 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: logId + * required: true + * schema: + * type: integer + * description: Log entry ID + * requestBody: + * required: true + * content: + * application/json: + * schema: + * type: object + * properties: + * content: + * type: object + * description: Updated log content + * metadata: + * type: object + * description: Updated metadata + * responses: + * 200: + * description: Log entry updated successfully + * content: + * application/json: + * schema: + * $ref: '#/components/schemas/SessionLogEntity' + * 400: + * description: Validation error + * content: + * application/json: + * schema: + * $ref: '#/components/schemas/Error' + * 404: + * description: Agent, session, or 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' + */ + sessionLogsRouter.put( + '/:logId', + validateAgentId, + validateSessionId, + validateLogId, + checkAgentAndSessionExist, + validateSessionLogUpdate, + handleValidationErrors, + async (req: Request, res: Response) => { + try { + const { sessionId, logId } = req.params + const logIdNum = parseInt(logId) + + logger.info(`Updating log entry: ${logId} for session: ${sessionId}`) + logger.debug('Update data:', req.body) + + // First check if log exists and belongs to session + const existingLog = await agentService.getSessionLog(logIdNum) + if (!existingLog || existingLog.session_id !== sessionId) { + logger.warn(`Log entry ${logId} not found for session ${sessionId}`) + return res.status(404).json({ + error: { + message: 'Log entry not found for this session', + type: 'not_found', + code: 'log_not_found' + } + }) + } + + const log = await agentService.updateSessionLog(logIdNum, req.body) + + if (!log) { + logger.warn(`Log entry not found for update: ${logId}`) + return res.status(404).json({ + error: { + message: 'Log entry not found', + type: 'not_found', + code: 'log_not_found' + } + }) + } + + logger.info(`Log entry updated successfully: ${logId}`) + return res.json(log) + } catch (error: any) { + logger.error('Error updating session log:', error) + return res.status(500).json({ + error: { + message: 'Failed to update log entry', + type: 'internal_error', + code: 'log_update_failed' + } + }) + } + } + ) + + /** + * @swagger + * /v1/agents/{agentId}/sessions/{sessionId}/logs/{logId}: + * delete: + * summary: Delete log entry + * description: Deletes a specific log entry + * tags: [Session Logs] + * 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: logId + * required: true + * schema: + * type: integer + * description: Log entry ID + * responses: + * 204: + * description: Log entry deleted successfully + * 404: + * description: Agent, session, or 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' + */ + sessionLogsRouter.delete( + '/:logId', + validateAgentId, + validateSessionId, + validateLogId, + checkAgentAndSessionExist, + handleValidationErrors, + async (req: Request, res: Response) => { + try { + const { sessionId, logId } = req.params + const logIdNum = parseInt(logId) + + logger.info(`Deleting log entry: ${logId} for session: ${sessionId}`) + + // First check if log exists and belongs to session + const existingLog = await agentService.getSessionLog(logIdNum) + if (!existingLog || existingLog.session_id !== sessionId) { + logger.warn(`Log entry ${logId} not found for session ${sessionId}`) + return res.status(404).json({ + error: { + message: 'Log entry not found for this session', + type: 'not_found', + code: 'log_not_found' + } + }) + } + + const deleted = await agentService.deleteSessionLog(logIdNum) + + if (!deleted) { + logger.warn(`Log entry not found for deletion: ${logId}`) + return res.status(404).json({ + error: { + message: 'Log entry not found', + type: 'not_found', + code: 'log_not_found' + } + }) + } + + logger.info(`Log entry deleted successfully: ${logId}`) + return res.status(204).send() + } catch (error: any) { + logger.error('Error deleting session log:', error) + return res.status(500).json({ + error: { + message: 'Failed to delete log entry', + type: 'internal_error', + code: 'log_delete_failed' + } + }) + } + } + ) + + return sessionLogsRouter +} + +// Convenience routes (standalone session logs without agent context) +/** + * @swagger + * /v1/sessions/{sessionId}/logs: + * get: + * summary: List log entries for a session (convenience endpoint) + * description: Retrieves a paginated list of log entries for the specified session without requiring agent context + * tags: [Session Logs] + * 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 log entries to return + * - in: query + * name: offset + * schema: + * type: integer + * minimum: 0 + * default: 0 + * description: Number of log entries to skip + * responses: + * 200: + * description: List of log entries + * content: + * application/json: + * schema: + * type: object + * properties: + * data: + * type: array + * items: + * $ref: '#/components/schemas/SessionLogEntity' + * total: + * type: integer + * description: Total number of log entries + * limit: + * type: integer + * description: Number of log entries returned + * offset: + * type: integer + * description: Number of log 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/logs', + 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 agentService.sessionExists(sessionId) + if (!sessionExists) { + return res.status(404).json({ + error: { + message: 'Session not found', + type: 'not_found', + code: 'session_not_found' + } + }) + } + + logger.info(`Listing logs for session: ${sessionId} with limit=${limit}, offset=${offset}`) + + const result = await agentService.listSessionLogs(sessionId, { limit, offset }) + + logger.info(`Retrieved ${result.logs.length} logs (total: ${result.total}) for session: ${sessionId}`) + return res.json({ + data: result.logs, + total: result.total, + limit, + offset + }) + } catch (error: any) { + logger.error('Error listing session logs:', error) + return res.status(500).json({ + error: { + message: 'Failed to list log entries', + type: 'internal_error', + code: 'log_list_failed' + } + }) + } + } +) + +/** + * @swagger + * /v1/session-logs/{logId}: + * get: + * summary: Get log entry by ID (convenience endpoint) + * description: Retrieves a specific log entry without requiring agent or session context + * tags: [Session Logs] + * parameters: + * - in: path + * name: logId + * required: true + * schema: + * type: integer + * description: Log entry ID + * responses: + * 200: + * description: Log entry details + * content: + * application/json: + * schema: + * $ref: '#/components/schemas/SessionLogEntity' + * 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-logs/:logId', validateLogId, handleValidationErrors, async (req: Request, res: Response) => { + try { + const { logId } = req.params + const logIdNum = parseInt(logId) + + logger.info(`Getting log entry: ${logId}`) + + const log = await agentService.getSessionLog(logIdNum) + + if (!log) { + logger.warn(`Log entry not found: ${logId}`) + return res.status(404).json({ + error: { + message: 'Log entry not found', + type: 'not_found', + code: 'log_not_found' + } + }) + } + + logger.info(`Log entry retrieved successfully: ${logId}`) + return res.json(log) + } catch (error: any) { + logger.error('Error getting session log:', error) + return res.status(500).json({ + error: { + message: 'Failed to get log entry', + type: 'internal_error', + code: 'log_get_failed' + } + }) + } +}) + +export { createSessionLogsRouter, router as sessionLogsRoutes } diff --git a/src/main/apiServer/routes/sessions.ts b/src/main/apiServer/routes/sessions.ts new file mode 100644 index 0000000000..78062864b8 --- /dev/null +++ b/src/main/apiServer/routes/sessions.ts @@ -0,0 +1,1013 @@ +import express, { Request, Response } from 'express' +import { body, param, query, validationResult } from 'express-validator' + +import { agentService } from '../../services/agents/AgentService' +import { loggerService } from '../../services/LoggerService' + +const logger = loggerService.withContext('ApiServerSessionsRoutes') + +const router = express.Router() + +// Validation middleware +const validateSession = [ + body('name').optional().isString(), + body('main_agent_id').notEmpty().withMessage('Main agent ID is required'), + 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 agentService.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 agentService.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 agentService.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: 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.put( + '/: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 agentService.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 session = await agentService.updateSession(sessionId, req.body) + + 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}/status: + * patch: + * summary: Update session status + * description: Updates the status of a specific session + * 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: + * type: object + * properties: + * status: + * type: string + * enum: [idle, running, completed, failed, stopped] + * required: + * - status + * responses: + * 200: + * description: Session status 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/status', + validateAgentId, + validateSessionId, + checkAgentExists, + validateStatusUpdate, + handleValidationErrors, + async (req: Request, res: Response) => { + try { + const { agentId, sessionId } = req.params + const { status } = req.body + + logger.info(`Updating session status: ${sessionId} for agent: ${agentId} to ${status}`) + + // First check if session exists and belongs to agent + const existingSession = await agentService.getSession(sessionId) + 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 session = await agentService.updateSessionStatus(sessionId, status) + + if (!session) { + logger.warn(`Session not found for status update: ${sessionId}`) + return res.status(404).json({ + error: { + message: 'Session not found', + type: 'not_found', + code: 'session_not_found' + } + }) + } + + logger.info(`Session status updated successfully: ${sessionId} -> ${status}`) + return res.json(session) + } catch (error: any) { + logger.error('Error updating session status:', error) + return res.status(500).json({ + error: { + message: 'Failed to update session status', + type: 'internal_error', + code: 'session_status_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 agentService.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 agentService.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 agentService.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 agentService.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/index.ts b/src/main/index.ts index b95cd70bf0..80db3a53c5 100644 --- a/src/main/index.ts +++ b/src/main/index.ts @@ -28,6 +28,7 @@ import { TrayService } from './services/TrayService' import { windowService } from './services/WindowService' import process from 'node:process' import { apiServerService } from './services/ApiServerService' +import { agentService } from './services/agents/AgentService' const logger = loggerService.withContext('MainEntry') @@ -147,6 +148,14 @@ if (!app.requestSingleInstanceLock()) { //start selection assistant service initSelectionService() + // Initialize Agent Service + try { + await agentService.initialize() + logger.info('Agent service initialized successfully') + } catch (error: any) { + logger.error('Failed to initialize Agent service:', error) + } + // Start API server if enabled try { const config = await apiServerService.getCurrentConfig()