From aaba77c36007d34150bbccaa26eb3b9542b6ad2c Mon Sep 17 00:00:00 2001 From: Vaayne Date: Sun, 14 Sep 2025 10:15:28 +0800 Subject: [PATCH] =?UTF-8?q?=E2=9C=A8=20feat:=20add=20comprehensive=20PUT/P?= =?UTF-8?q?ATCH=20support=20for=20agents=20and=20sessions=20APIs?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Add PATCH method to agents API for partial updates alongside existing PUT - Add PUT method to sessions API for complete replacement alongside existing PATCH - Update API documentation with clear PUT vs PATCH usage examples - Refactor session status updates to use standard PATCH endpoint - Ensure both methods use same validation middleware for consistency - Add comprehensive Swagger documentation for new endpoints This provides REST-compliant update operations where: - PUT: Complete resource replacement (idempotent) - PATCH: Partial resource updates (only specified fields) Both agents and sessions now support flexible update patterns for different use cases. --- docs/agents-api-ui-integration.md | 105 ++++++++++-- src/main/apiServer/routes/agents.ts | 138 ++++++++++++++++ src/main/apiServer/routes/sessions.ts | 228 +++++++++++++------------- 3 files changed, 340 insertions(+), 131 deletions(-) diff --git a/docs/agents-api-ui-integration.md b/docs/agents-api-ui-integration.md index d2c4465b5..fc12c0173 100644 --- a/docs/agents-api-ui-integration.md +++ b/docs/agents-api-ui-integration.md @@ -32,7 +32,8 @@ This document provides comprehensive guidance for UI components to integrate wit | POST | `/agents` | Create new agent | `CreateAgentRequest` | `AgentEntity` | | GET | `/agents` | List agents (paginated) | Query params | `{ data: AgentEntity[], total: number }` | | GET | `/agents/{id}` | Get specific agent | - | `AgentEntity` | -| PUT | `/agents/{id}` | Update agent | `UpdateAgentRequest` | `AgentEntity` | +| PUT | `/agents/{id}` | Replace agent (complete update) | `UpdateAgentRequest` | `AgentEntity` | +| PATCH | `/agents/{id}` | Partially update agent | `Partial` | `AgentEntity` | | DELETE | `/agents/{id}` | Delete agent | - | `204 No Content` | ### Session Management (`/agents/{agentId}/sessions`) @@ -42,8 +43,8 @@ This document provides comprehensive guidance for UI components to integrate wit | POST | `/agents/{agentId}/sessions` | Create session | `CreateSessionRequest` | `AgentSessionEntity` | | GET | `/agents/{agentId}/sessions` | List agent sessions | Query params | `{ data: AgentSessionEntity[], total: number }` | | GET | `/agents/{agentId}/sessions/{id}` | Get specific session | - | `AgentSessionEntity` | -| PUT | `/agents/{agentId}/sessions/{id}` | Update session | `UpdateSessionRequest` | `AgentSessionEntity` | -| PATCH | `/agents/{agentId}/sessions/{id}/status` | Update session status | `{ status: SessionStatus }` | `AgentSessionEntity` | +| PUT | `/agents/{agentId}/sessions/{id}` | Replace session (complete update) | `UpdateSessionRequest` | `AgentSessionEntity` | +| PATCH | `/agents/{agentId}/sessions/{id}` | Partially update session | `Partial` | `AgentSessionEntity` | | DELETE | `/agents/{agentId}/sessions/{id}` | Delete session | - | `204 No Content` | ### Message Streaming (`/agents/{agentId}/sessions/{sessionId}/messages`) @@ -53,6 +54,55 @@ This document provides comprehensive guidance for UI components to integrate wit | POST | `/agents/{agentId}/sessions/{sessionId}/messages` | Send message to agent | `CreateMessageRequest` | **Stream Response** | | GET | `/agents/{agentId}/sessions/{sessionId}/messages` | List session messages | Query params | `{ data: SessionMessageEntity[], total: number }` | +## HTTP Methods: PUT vs PATCH + +Both agents and sessions support two types of update operations: + +### PUT - Complete Replacement +- **Purpose**: Replaces the entire resource with the provided data +- **Behavior**: All fields in the request body will be applied to the resource +- **Use Case**: When you want to completely update a resource with a new set of values +- **Example**: Updating an agent's configuration completely + +```typescript +// PUT - Replace entire agent +await fetch('/v1/agents/agent-123', { + method: 'PUT', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ + name: 'New Agent Name', + model: 'gpt-4', + instructions: 'New instructions', + built_in_tools: ['search', 'calculator'], + // All other fields will be reset to defaults if not provided + }) +}) +``` + +### PATCH - Partial Update +- **Purpose**: Updates only the specified fields, leaving others unchanged +- **Behavior**: Only the fields present in the request body will be modified +- **Use Case**: When you want to update specific fields without affecting others +- **Example**: Updating only an agent's name or instructions + +```typescript +// PATCH - Update only specific fields +await fetch('/v1/agents/agent-123', { + method: 'PATCH', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ + name: 'Updated Agent Name' + // All other fields remain unchanged + }) +}) +``` + +### Validation +Both methods use the same validation rules: +- All fields are optional for both PUT and PATCH +- When provided, fields must meet their validation criteria (e.g., `name` cannot be empty) +- The same middleware (`validateAgentUpdate` for agents, `validateSessionUpdate` for sessions) handles both operations + ## Data Types & Schemas ### AgentEntity @@ -198,7 +248,42 @@ async function createSession(agentId: string, sessionData: CreateSessionRequest) } ``` -### Session Status Management +### Session Updates +Sessions can be updated using either PUT (complete replacement) or PATCH (partial update): + +#### Complete Session Replacement (PUT) +```typescript +async function replaceSession(agentId: string, sessionId: string, sessionData: UpdateSessionRequest): Promise { + const response = await fetch(`/v1/agents/${agentId}/sessions/${sessionId}`, { + method: 'PUT', + headers: { + 'Authorization': `Bearer ${apiKey}`, + 'Content-Type': 'application/json' + }, + body: JSON.stringify(sessionData) // Complete session configuration + }) + + return await response.json() +} +``` + +#### Partial Session Update (PATCH) +```typescript +async function updateSession(agentId: string, sessionId: string, updates: Partial): Promise { + const response = await fetch(`/v1/agents/${agentId}/sessions/${sessionId}`, { + method: 'PATCH', + headers: { + 'Authorization': `Bearer ${apiKey}`, + 'Content-Type': 'application/json' + }, + body: JSON.stringify(updates) // Only the fields to update + }) + + return await response.json() +} +``` + +#### Session Status Management Sessions have five possible statuses: - `idle`: Ready to process messages - `running`: Currently processing @@ -207,17 +292,9 @@ Sessions have five possible statuses: - `stopped`: Manually stopped by user ```typescript +// Update only the session status using PATCH async function updateSessionStatus(agentId: string, sessionId: string, status: SessionStatus): Promise { - const response = await fetch(`/v1/agents/${agentId}/sessions/${sessionId}/status`, { - method: 'PATCH', - headers: { - 'Authorization': `Bearer ${apiKey}`, - 'Content-Type': 'application/json' - }, - body: JSON.stringify({ status }) - }) - - return await response.json() + return await updateSession(agentId, sessionId, { status }) } ``` diff --git a/src/main/apiServer/routes/agents.ts b/src/main/apiServer/routes/agents.ts index f3ebe375b..def5d000a 100644 --- a/src/main/apiServer/routes/agents.ts +++ b/src/main/apiServer/routes/agents.ts @@ -488,6 +488,144 @@ router.put( } ) +/** + * @swagger + * /v1/agents/{agentId}: + * patch: + * summary: Partially update agent + * description: Partially updates an existing agent with only the provided fields + * tags: [Agents] + * parameters: + * - in: path + * name: agentId + * required: true + * schema: + * type: string + * description: Agent ID + * requestBody: + * required: true + * content: + * application/json: + * schema: + * 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 + * description: Only include the fields you want to update + * 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.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) + + 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({ + error: { + message: 'Failed to partially update agent', + type: 'internal_error', + code: 'agent_patch_failed' + } + }) + } + } +) + /** * @swagger * /v1/agents/{agentId}: diff --git a/src/main/apiServer/routes/sessions.ts b/src/main/apiServer/routes/sessions.ts index 891ff72c9..7677704a3 100644 --- a/src/main/apiServer/routes/sessions.ts +++ b/src/main/apiServer/routes/sessions.ts @@ -11,7 +11,6 @@ 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']), @@ -545,6 +544,114 @@ function createSessionsRouter(): express.Router { * @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] @@ -593,7 +700,7 @@ function createSessionsRouter(): express.Router { * schema: * $ref: '#/components/schemas/Error' */ - sessionsRouter.put( + sessionsRouter.patch( '/:sessionId', validateAgentId, validateSessionId, @@ -618,8 +725,8 @@ function createSessionsRouter(): express.Router { } }) } - - const session = await sessionService.updateSession(sessionId, req.body) + const updateSession = { ...existingSession, ...req.body } + const session = await sessionService.updateSession(sessionId, updateSession) if (!session) { logger.warn(`Session not found for update: ${sessionId}`) @@ -647,119 +754,6 @@ function createSessionsRouter(): express.Router { } ) - /** - * @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 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 session = await sessionService.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}: