feat: add comprehensive PUT/PATCH support for agents and sessions APIs

- 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.
This commit is contained in:
Vaayne 2025-09-14 10:15:28 +08:00
parent 532bad8eb7
commit aaba77c360
3 changed files with 340 additions and 131 deletions

View File

@ -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<UpdateAgentRequest>` | `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<UpdateSessionRequest>` | `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<AgentSessionEntity> {
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<UpdateSessionRequest>): Promise<AgentSessionEntity> {
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<AgentSessionEntity> {
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 })
}
```

View File

@ -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}:

View File

@ -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}: