diff --git a/plan.md b/plan.md new file mode 100644 index 0000000000..49c37ea3a9 --- /dev/null +++ b/plan.md @@ -0,0 +1,224 @@ +Overview + +Implement comprehensive CRUD APIs for agent, agentSession, and agentSessionLogs management +in Cherry Studio's API server using RESTful URL conventions. + +Architecture Overview + +1. Service Layer + +- Create AgentService class in src/main/services/agents/AgentService.ts + - Handles database operations using SQL queries from db.ts + - Manages SQLite database initialization and connections + - Provides business logic for agent operations + +2. API Routes + +- Create route files in src/main/apiServer/routes/: + - agents.ts - Agent CRUD endpoints + - sessions.ts - Session CRUD endpoints + - session-logs.ts - Session logs CRUD endpoints + +3. Database Integration + +- Use SQLite with @libsql/client (following MemoryService pattern) +- Database location: userData/agents.db +- Leverage existing SQL queries in src/main/services/agents/db.ts + +Implementation Steps + +Phase 1: Database Service Setup + +1. Create AgentService class with database initialization +2. Implement database connection management +3. Add database initialization to main process startup +4. Create helper methods for JSON field serialization/deserialization + +Phase 2: Agent CRUD Operations + +1. Implement service methods: + - createAgent(agent: Omit) + - getAgent(id: string) + - listAgents(options?: { limit?: number, offset?: number }) + - updateAgent(id: string, updates: Partial) + - deleteAgent(id: string) +2. Create API routes: + - POST /v1/agents - Create agent + - GET /v1/agents - List all agents + - GET /v1/agents/:agentId - Get agent by ID + - PUT /v1/agents/:agentId - Update agent + - DELETE /v1/agents/:agentId - Delete agent + +Phase 3: Session CRUD Operations + +1. Implement service methods: + - createSession(session: Omit) + - getSession(id: string) + - listSessions(agentId?: string, options?: { status?: SessionStatus, limit?: number, +offset?: number }) + - updateSession(id: string, updates: Partial) + - updateSessionStatus(id: string, status: SessionStatus) + - deleteSession(id: string) + - getSessionWithAgent(id: string) - Get session with merged agent configuration +2. Create API routes (RESTful nested resources): + - POST /v1/agents/:agentId/sessions - Create session for specific agent + - GET /v1/agents/:agentId/sessions - List sessions for specific agent + - GET /v1/agents/:agentId/sessions/:sessionId - Get specific session + - PUT /v1/agents/:agentId/sessions/:sessionId - Update session + - PATCH /v1/agents/:agentId/sessions/:sessionId/status - Update session status + - DELETE /v1/agents/:agentId/sessions/:sessionId - Delete session + +Additional convenience endpoints: + - GET /v1/sessions - List all sessions (across all agents) + - GET /v1/sessions/:sessionId - Get session by ID (without agent context) + +Phase 4: Session Logs CRUD Operations + +1. Implement service methods: + - createSessionLog(log: Omit) + - getSessionLog(id: number) + - listSessionLogs(sessionId: string, options?: { limit?: number, offset?: number }) + - updateSessionLog(id: number, updates: { content?: any, metadata?: any }) + - deleteSessionLog(id: number) + - getSessionLogTree(sessionId: string) - Get logs with parent-child relationships + - bulkCreateSessionLogs(logs: Array<...>) - Batch insert logs +2. Create API routes (RESTful nested resources): + - POST /v1/agents/:agentId/sessions/:sessionId/logs - Create log entry + - GET /v1/agents/:agentId/sessions/:sessionId/logs - List logs for session + - GET /v1/agents/:agentId/sessions/:sessionId/logs/:logId - Get specific log + - PUT /v1/agents/:agentId/sessions/:sessionId/logs/:logId - Update log + - DELETE /v1/agents/:agentId/sessions/:sessionId/logs/:logId - Delete log + - POST /v1/agents/:agentId/sessions/:sessionId/logs/bulk - Bulk create logs + +Additional convenience endpoints: + - GET /v1/sessions/:sessionId/logs - Get logs without agent context + - GET /v1/session-logs/:logId - Get specific log by ID + +Phase 5: Route Organization + +1. Mount routes with proper nesting: +// In app.ts +apiRouter.use('/agents', agentsRoutes) +// agentsRoutes will handle: +// - /agents/* +// - /agents/:agentId/sessions/* +// - /agents/:agentId/sessions/:sessionId/logs/* + +// Convenience routes +apiRouter.use('/sessions', sessionsRoutes) +apiRouter.use('/session-logs', sessionLogsRoutes) + +2. Use Express Router mergeParams for nested routes: +// In agents.ts +const sessionsRouter = express.Router({ mergeParams: true }) +router.use('/:agentId/sessions', sessionsRouter) + +Phase 6: OpenAPI Documentation + +1. Add Swagger schemas for new entities: + - AgentEntity schema + - AgentSessionEntity schema + - SessionLogEntity schema + - Request/Response schemas +2. Document all new endpoints with: + - Clear path parameters (agentId, sessionId, logId) + - Request body schemas + - Response examples + - Error responses + - Proper grouping by resource + +Phase 7: Validation & Error Handling + +1. Add path parameter validation: + - Validate agentId exists before processing session requests + - Validate sessionId belongs to agentId + - Validate logId belongs to sessionId +2. Implement middleware for: + - Request validation using express-validator + - Resource existence checks + - Permission validation (future consideration) + - Transaction support for complex operations + +Phase 8: Testing + +1. Unit tests for service methods +2. Integration tests for API endpoints +3. Test nested resource validation +4. Test cascading deletes +5. Test transaction rollbacks + +File Structure + +src/ +├── main/ +│ └── services/ +│ └── agents/ +│ ├── index.ts (existing) +│ ├── db.ts (existing) +│ └── AgentService.ts (new) +├── main/ +│ └── apiServer/ +│ └── routes/ +│ ├── agents.ts (new - includes nested routes) +│ ├── sessions.ts (new - convenience endpoints) +│ └── session-logs.ts (new - convenience endpoints) +└── renderer/ + └── src/ + └── types/ + └── agent.ts (existing) + +API Endpoint Summary + +Agent Endpoints + +- POST /v1/agents +- GET /v1/agents +- GET /v1/agents/:agentId +- PUT /v1/agents/:agentId +- DELETE /v1/agents/:agentId + +Session Endpoints (RESTful) + +- POST /v1/agents/:agentId/sessions +- GET /v1/agents/:agentId/sessions +- GET /v1/agents/:agentId/sessions/:sessionId +- PUT /v1/agents/:agentId/sessions/:sessionId +- PATCH /v1/agents/:agentId/sessions/:sessionId/status +- DELETE /v1/agents/:agentId/sessions/:sessionId + +Session Convenience Endpoints + +- GET /v1/sessions +- GET /v1/sessions/:sessionId + +Session Log Endpoints (RESTful) + +- POST /v1/agents/:agentId/sessions/:sessionId/logs +- GET /v1/agents/:agentId/sessions/:sessionId/logs +- GET /v1/agents/:agentId/sessions/:sessionId/logs/:logId +- PUT /v1/agents/:agentId/sessions/:sessionId/logs/:logId +- DELETE /v1/agents/:agentId/sessions/:sessionId/logs/:logId +- POST /v1/agents/:agentId/sessions/:sessionId/logs/bulk + +Session Log Convenience Endpoints + +- GET /v1/sessions/:sessionId/logs +- GET /v1/session-logs/:logId + +Key Considerations + +- Follow RESTful URL conventions with proper resource nesting +- Validate parent-child relationships in nested routes +- Use Express Router with mergeParams for nested routing +- Implement proper cascading deletes +- Add transaction support for data consistency +- Follow existing patterns from MemoryService +- Ensure backward compatibility +- Add rate limiting for write operations + +Dependencies + +- @libsql/client - SQLite database client +- express-validator - Request validation +- swagger-jsdoc - API documentation +- Existing types from @types/agent.ts diff --git a/src/main/services/agents/AgentService.ts b/src/main/services/agents/AgentService.ts index fd348e4dc0..4fb265be4b 100644 --- a/src/main/services/agents/AgentService.ts +++ b/src/main/services/agents/AgentService.ts @@ -1,6 +1,13 @@ import { Client, createClient } from '@libsql/client' import { loggerService } from '@logger' -import type { AgentEntity, AgentSessionEntity, PermissionMode, SessionLogEntity, SessionStatus } from '@types' +import type { + AgentEntity, + AgentSessionEntity, + AgentType, + PermissionMode, + SessionLogEntity, + SessionStatus +} from '@types' import { app } from 'electron' import path from 'path' @@ -9,6 +16,7 @@ import { AgentQueries } from './db' const logger = loggerService.withContext('AgentService') export interface CreateAgentRequest { + type: AgentType name: string description?: string avatar?: string @@ -201,6 +209,7 @@ export class AgentService { const values = [ id, + serializedData.type, serializedData.name, serializedData.description || null, serializedData.avatar || null, diff --git a/src/main/services/agents/db.ts b/src/main/services/agents/db.ts index e5c7e53a33..7c4b1176e4 100644 --- a/src/main/services/agents/db.ts +++ b/src/main/services/agents/db.ts @@ -8,6 +8,7 @@ export const AgentQueries = { agents: ` CREATE TABLE IF NOT EXISTS agents ( id TEXT PRIMARY KEY, + type TEXT NOT NULL DEFAULT 'custom', -- 'claudeCode', 'codex', 'custom' name TEXT NOT NULL, description TEXT, avatar TEXT, @@ -72,6 +73,7 @@ export const AgentQueries = { // Index creation queries createIndexes: { agentsName: 'CREATE INDEX IF NOT EXISTS idx_agents_name ON agents(name)', + agentsType: 'CREATE INDEX IF NOT EXISTS idx_agents_type ON agents(type)', agentsModel: 'CREATE INDEX IF NOT EXISTS idx_agents_model ON agents(model)', agentsPlanModel: 'CREATE INDEX IF NOT EXISTS idx_agents_plan_model ON agents(plan_model)', agentsSmallModel: 'CREATE INDEX IF NOT EXISTS idx_agents_small_model ON agents(small_model)', @@ -99,8 +101,8 @@ export const AgentQueries = { // Agent operations agents: { insert: ` - INSERT INTO agents (id, name, description, avatar, instructions, model, plan_model, small_model, built_in_tools, mcps, knowledges, configuration, accessible_paths, permission_mode, max_steps, created_at, updated_at) - VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) + INSERT INTO agents (id, type, name, description, avatar, instructions, model, plan_model, small_model, built_in_tools, mcps, knowledges, configuration, accessible_paths, permission_mode, max_steps, created_at, updated_at) + VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) `, update: ` diff --git a/src/renderer/src/types/agent.ts b/src/renderer/src/types/agent.ts index 36ccf5de70..819f0ae4bd 100644 --- a/src/renderer/src/types/agent.ts +++ b/src/renderer/src/types/agent.ts @@ -6,6 +6,7 @@ export type SessionStatus = 'idle' | 'running' | 'completed' | 'failed' | 'stopped' export type PermissionMode = 'readOnly' | 'acceptEdits' | 'bypassPermissions' export type SessionLogRole = 'user' | 'agent' | 'system' | 'tool' +export type AgentType = 'claude-code' | 'codex' | 'qwen-cli' | 'gemini-cli' | 'custom' export type SessionLogType = | 'message' // User or agent message @@ -38,6 +39,7 @@ export interface AgentConfiguration { // Agent entity representing an autonomous agent configuration export interface AgentEntity extends AgentConfiguration { id: string + type: AgentType name: string description?: string avatar?: string