Merge branch 'feat/agents-new' of github.com:CherryHQ/cherry-studio into feat/agents-new

This commit is contained in:
icarus 2025-09-19 12:58:21 +08:00
commit ae9c78e643
3 changed files with 924 additions and 152 deletions

View File

@ -22,76 +22,114 @@ const agentsRouter = express.Router()
* @swagger * @swagger
* components: * components:
* schemas: * schemas:
* PermissionMode:
* type: string
* enum: [default, acceptEdits, bypassPermissions, plan]
* description: Permission mode for agent operations
*
* AgentType:
* type: string
* enum: [claude-code]
* description: Type of agent
*
* AgentConfiguration:
* type: object
* properties:
* permission_mode:
* $ref: '#/components/schemas/PermissionMode'
* default: default
* max_turns:
* type: integer
* default: 10
* description: Maximum number of interaction turns
* additionalProperties: true
*
* AgentBase:
* type: object
* properties:
* name:
* type: string
* description: Agent name
* description:
* type: string
* description: Agent description
* accessible_paths:
* type: array
* items:
* type: string
* description: Array of directory paths the agent can access
* 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
* mcps:
* type: array
* items:
* type: string
* description: Array of MCP tool IDs
* allowed_tools:
* type: array
* items:
* type: string
* description: Array of allowed tool IDs (whitelist)
* configuration:
* $ref: '#/components/schemas/AgentConfiguration'
* required:
* - model
* - accessible_paths
*
* AgentEntity: * AgentEntity:
* type: object * allOf:
* properties: * - $ref: '#/components/schemas/AgentBase'
* id: * - type: object
* type: string * properties:
* description: Unique agent identifier * id:
* name: * type: string
* type: string * description: Unique agent identifier
* description: Agent name * type:
* description: * $ref: '#/components/schemas/AgentType'
* type: string * created_at:
* description: Agent description * type: string
* avatar: * format: date-time
* type: string * description: ISO timestamp of creation
* description: Agent avatar URL * updated_at:
* instructions: * type: string
* type: string * format: date-time
* description: System prompt/instructions * description: ISO timestamp of last update
* model: * required:
* type: string * - id
* description: Main model ID * - type
* plan_model: * - created_at
* type: string * - updated_at
* 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: * CreateAgentRequest:
* allOf:
* - $ref: '#/components/schemas/AgentBase'
* - type: object
* properties:
* type:
* $ref: '#/components/schemas/AgentType'
* name:
* type: string
* minLength: 1
* description: Agent name (required)
* model:
* type: string
* minLength: 1
* description: Main model ID (required)
* required:
* - type
* - name
* - model
*
* UpdateAgentRequest:
* type: object * type: object
* properties: * properties:
* name: * name:
@ -100,9 +138,11 @@ const agentsRouter = express.Router()
* description: * description:
* type: string * type: string
* description: Agent description * description: Agent description
* avatar: * accessible_paths:
* type: string * type: array
* description: Agent avatar URL * items:
* type: string
* description: Array of directory paths the agent can access
* instructions: * instructions:
* type: string * type: string
* description: System prompt/instructions * description: System prompt/instructions
@ -115,53 +155,409 @@ const agentsRouter = express.Router()
* small_model: * small_model:
* type: string * type: string
* description: Optional small/fast model ID * description: Optional small/fast model ID
* built_in_tools:
* type: array
* items:
* type: string
* description: Built-in tool IDs
* mcps: * mcps:
* type: array * type: array
* items: * items:
* type: string * type: string
* description: MCP tool IDs * description: Array of MCP tool IDs
* knowledges: * allowed_tools:
* type: array * type: array
* items: * items:
* type: string * type: string
* description: Knowledge base IDs * description: Array of allowed tool IDs (whitelist)
* configuration: * configuration:
* type: object * $ref: '#/components/schemas/AgentConfiguration'
* description: Extensible settings * description: Partial update - all fields are optional
*
* ReplaceAgentRequest:
* $ref: '#/components/schemas/AgentBase'
*
* SessionEntity:
* allOf:
* - $ref: '#/components/schemas/AgentBase'
* - type: object
* properties:
* id:
* type: string
* description: Unique session identifier
* agent_id:
* type: string
* description: Primary agent ID for the session
* agent_type:
* $ref: '#/components/schemas/AgentType'
* created_at:
* type: string
* format: date-time
* description: ISO timestamp of creation
* updated_at:
* type: string
* format: date-time
* description: ISO timestamp of last update
* required:
* - id
* - agent_id
* - agent_type
* - created_at
* - updated_at
*
* CreateSessionRequest:
* allOf:
* - $ref: '#/components/schemas/AgentBase'
* - type: object
* properties:
* model:
* type: string
* minLength: 1
* description: Main model ID (required)
* required:
* - model
*
* UpdateSessionRequest:
* type: object
* properties:
* name:
* type: string
* description: Session name
* description:
* type: string
* description: Session description
* accessible_paths: * accessible_paths:
* type: array * type: array
* items: * items:
* type: string * type: string
* description: Accessible directory paths * description: Array of directory paths the agent can access
* permission_mode: * instructions:
* type: string * type: string
* enum: [readOnly, acceptEdits, bypassPermissions] * description: System prompt/instructions
* description: Permission mode * model:
* max_steps: * type: string
* type: integer * description: Main model ID
* description: Maximum steps the agent can take * plan_model:
* type: string
* description: Optional planning model ID
* small_model:
* type: string
* description: Optional small/fast model ID
* mcps:
* type: array
* items:
* type: string
* description: Array of MCP tool IDs
* allowed_tools:
* type: array
* items:
* type: string
* description: Array of allowed tool IDs (whitelist)
* configuration:
* $ref: '#/components/schemas/AgentConfiguration'
* description: Partial update - all fields are optional
*
* ReplaceSessionRequest:
* allOf:
* - $ref: '#/components/schemas/AgentBase'
* - type: object
* properties:
* model:
* type: string
* minLength: 1
* description: Main model ID (required)
* required:
* - model
*
* CreateSessionMessageRequest:
* type: object
* properties:
* content:
* type: string
* minLength: 1
* description: Message content
* required: * required:
* - name * - content
* - model *
* PaginationQuery:
* type: object
* properties:
* limit:
* type: integer
* minimum: 1
* maximum: 100
* default: 20
* description: Number of items to return
* offset:
* type: integer
* minimum: 0
* default: 0
* description: Number of items to skip
* status:
* type: string
* enum: [idle, running, completed, failed, stopped]
* description: Filter by session status
*
* ListAgentsResponse:
* type: object
* properties:
* agents:
* type: array
* items:
* $ref: '#/components/schemas/AgentEntity'
* total:
* type: integer
* description: Total number of agents
* limit:
* type: integer
* description: Number of items returned
* offset:
* type: integer
* description: Number of items skipped
* required:
* - agents
* - total
* - limit
* - offset
*
* ListSessionsResponse:
* type: object
* properties:
* sessions:
* type: array
* items:
* $ref: '#/components/schemas/SessionEntity'
* total:
* type: integer
* description: Total number of sessions
* limit:
* type: integer
* description: Number of items returned
* offset:
* type: integer
* description: Number of items skipped
* required:
* - sessions
* - total
* - limit
* - offset
*
* ErrorResponse:
* type: object
* properties:
* error:
* type: object
* properties:
* message:
* type: string
* description: Error message
* type:
* type: string
* description: Error type
* code:
* type: string
* description: Error code
* required:
* - message
* - type
* - code
* required:
* - error
*/ */
/**
* @swagger
* /api/agents:
* post:
* summary: Create a new agent
* 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: Invalid request body
* content:
* application/json:
* schema:
* $ref: '#/components/schemas/ErrorResponse'
*/
// Agent CRUD routes // Agent CRUD routes
agentsRouter.post('/', validateAgent, handleValidationErrors, agentHandlers.createAgent) agentsRouter.post('/', validateAgent, handleValidationErrors, agentHandlers.createAgent)
/**
* @swagger
* /api/agents:
* get:
* summary: List all agents with pagination
* 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
* - in: query
* name: status
* schema:
* type: string
* enum: [idle, running, completed, failed, stopped]
* description: Filter by agent status
* responses:
* 200:
* description: List of agents
* content:
* application/json:
* schema:
* $ref: '#/components/schemas/ListAgentsResponse'
*/
agentsRouter.get('/', validatePagination, handleValidationErrors, agentHandlers.listAgents) agentsRouter.get('/', validatePagination, handleValidationErrors, agentHandlers.listAgents)
/**
* @swagger
* /api/agents/{agentId}:
* get:
* summary: Get agent by 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/ErrorResponse'
*/
agentsRouter.get('/:agentId', validateAgentId, handleValidationErrors, agentHandlers.getAgent) agentsRouter.get('/:agentId', validateAgentId, handleValidationErrors, agentHandlers.getAgent)
agentsRouter.put( /**
'/:agentId', * @swagger
validateAgentId, * /api/agents/{agentId}:
validateAgentReplace, * put:
handleValidationErrors, * summary: Replace agent (full update)
agentHandlers.updateAgent * 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/ReplaceAgentRequest'
* responses:
* 200:
* description: Agent updated successfully
* content:
* application/json:
* schema:
* $ref: '#/components/schemas/AgentEntity'
* 400:
* description: Invalid request body
* content:
* application/json:
* schema:
* $ref: '#/components/schemas/ErrorResponse'
* 404:
* description: Agent not found
* content:
* application/json:
* schema:
* $ref: '#/components/schemas/ErrorResponse'
*/
agentsRouter.put('/:agentId', validateAgentId, validateAgentReplace, handleValidationErrors, agentHandlers.updateAgent)
/**
* @swagger
* /api/agents/{agentId}:
* patch:
* summary: Update agent (partial update)
* 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/UpdateAgentRequest'
* responses:
* 200:
* description: Agent updated successfully
* content:
* application/json:
* schema:
* $ref: '#/components/schemas/AgentEntity'
* 400:
* description: Invalid request body
* content:
* application/json:
* schema:
* $ref: '#/components/schemas/ErrorResponse'
* 404:
* description: Agent not found
* content:
* application/json:
* schema:
* $ref: '#/components/schemas/ErrorResponse'
*/
agentsRouter.patch('/:agentId', validateAgentId, validateAgentUpdate, handleValidationErrors, agentHandlers.patchAgent) agentsRouter.patch('/:agentId', validateAgentId, validateAgentUpdate, handleValidationErrors, agentHandlers.patchAgent)
/**
* @swagger
* /api/agents/{agentId}:
* delete:
* summary: Delete agent
* 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/ErrorResponse'
*/
agentsRouter.delete('/:agentId', validateAgentId, handleValidationErrors, agentHandlers.deleteAgent) agentsRouter.delete('/:agentId', validateAgentId, handleValidationErrors, agentHandlers.deleteAgent)
// Create sessions router with agent context // Create sessions router with agent context
@ -169,9 +565,175 @@ const createSessionsRouter = (): express.Router => {
const sessionsRouter = express.Router({ mergeParams: true }) const sessionsRouter = express.Router({ mergeParams: true })
// Session CRUD routes (nested under agent) // Session CRUD routes (nested under agent)
/**
* @swagger
* /api/agents/{agentId}/sessions:
* post:
* summary: Create a new session for an 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/SessionEntity'
* 400:
* description: Invalid request body
* content:
* application/json:
* schema:
* $ref: '#/components/schemas/ErrorResponse'
* 404:
* description: Agent not found
* content:
* application/json:
* schema:
* $ref: '#/components/schemas/ErrorResponse'
*/
sessionsRouter.post('/', validateSession, handleValidationErrors, sessionHandlers.createSession) sessionsRouter.post('/', validateSession, handleValidationErrors, sessionHandlers.createSession)
/**
* @swagger
* /api/agents/{agentId}/sessions:
* get:
* summary: List sessions for an 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:
* $ref: '#/components/schemas/ListSessionsResponse'
* 404:
* description: Agent not found
* content:
* application/json:
* schema:
* $ref: '#/components/schemas/ErrorResponse'
*/
sessionsRouter.get('/', validatePagination, handleValidationErrors, sessionHandlers.listSessions) sessionsRouter.get('/', validatePagination, handleValidationErrors, sessionHandlers.listSessions)
/**
* @swagger
* /api/agents/{agentId}/sessions/{sessionId}:
* get:
* summary: Get session by ID
* 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/SessionEntity'
* 404:
* description: Agent or session not found
* content:
* application/json:
* schema:
* $ref: '#/components/schemas/ErrorResponse'
*/
sessionsRouter.get('/:sessionId', validateSessionId, handleValidationErrors, sessionHandlers.getSession) sessionsRouter.get('/:sessionId', validateSessionId, handleValidationErrors, sessionHandlers.getSession)
/**
* @swagger
* /api/agents/{agentId}/sessions/{sessionId}:
* put:
* summary: Replace session (full update)
* 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/ReplaceSessionRequest'
* responses:
* 200:
* description: Session updated successfully
* content:
* application/json:
* schema:
* $ref: '#/components/schemas/SessionEntity'
* 400:
* description: Invalid request body
* content:
* application/json:
* schema:
* $ref: '#/components/schemas/ErrorResponse'
* 404:
* description: Agent or session not found
* content:
* application/json:
* schema:
* $ref: '#/components/schemas/ErrorResponse'
*/
sessionsRouter.put( sessionsRouter.put(
'/:sessionId', '/:sessionId',
validateSessionId, validateSessionId,
@ -179,6 +741,51 @@ const createSessionsRouter = (): express.Router => {
handleValidationErrors, handleValidationErrors,
sessionHandlers.updateSession sessionHandlers.updateSession
) )
/**
* @swagger
* /api/agents/{agentId}/sessions/{sessionId}:
* patch:
* summary: Update session (partial update)
* 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/UpdateSessionRequest'
* responses:
* 200:
* description: Session updated successfully
* content:
* application/json:
* schema:
* $ref: '#/components/schemas/SessionEntity'
* 400:
* description: Invalid request body
* content:
* application/json:
* schema:
* $ref: '#/components/schemas/ErrorResponse'
* 404:
* description: Agent or session not found
* content:
* application/json:
* schema:
* $ref: '#/components/schemas/ErrorResponse'
*/
sessionsRouter.patch( sessionsRouter.patch(
'/:sessionId', '/:sessionId',
validateSessionId, validateSessionId,
@ -186,6 +793,35 @@ const createSessionsRouter = (): express.Router => {
handleValidationErrors, handleValidationErrors,
sessionHandlers.patchSession sessionHandlers.patchSession
) )
/**
* @swagger
* /api/agents/{agentId}/sessions/{sessionId}:
* delete:
* summary: Delete 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
* responses:
* 204:
* description: Session deleted successfully
* 404:
* description: Agent or session not found
* content:
* application/json:
* schema:
* $ref: '#/components/schemas/ErrorResponse'
*/
sessionsRouter.delete('/:sessionId', validateSessionId, handleValidationErrors, sessionHandlers.deleteSession) sessionsRouter.delete('/:sessionId', validateSessionId, handleValidationErrors, sessionHandlers.deleteSession)
return sessionsRouter return sessionsRouter
@ -196,6 +832,77 @@ const createMessagesRouter = (): express.Router => {
const messagesRouter = express.Router({ mergeParams: true }) const messagesRouter = express.Router({ mergeParams: true })
// Message CRUD routes (nested under agent/session) // Message CRUD routes (nested under agent/session)
/**
* @swagger
* /api/agents/{agentId}/sessions/{sessionId}/messages:
* post:
* summary: Create a new message in a session
* tags: [Messages]
* parameters:
* - in: path
* name: agentId
* required: true
* schema:
* type: string
* description: Agent ID
* - in: path
* name: sessionId
* required: true
* schema:
* type: string
* description: Session ID
* requestBody:
* required: true
* content:
* application/json:
* schema:
* $ref: '#/components/schemas/CreateSessionMessageRequest'
* responses:
* 201:
* description: Message created successfully
* content:
* application/json:
* schema:
* type: object
* properties:
* id:
* type: number
* description: Message ID
* session_id:
* type: string
* description: Session ID
* role:
* type: string
* enum: [assistant, user, system, tool]
* description: Message role
* content:
* type: object
* description: Message content (AI SDK format)
* agent_session_id:
* type: string
* description: Agent session ID for resuming
* metadata:
* type: object
* description: Additional metadata
* created_at:
* type: string
* format: date-time
* updated_at:
* type: string
* format: date-time
* 400:
* description: Invalid request body
* content:
* application/json:
* schema:
* $ref: '#/components/schemas/ErrorResponse'
* 404:
* description: Agent or session not found
* content:
* application/json:
* schema:
* $ref: '#/components/schemas/ErrorResponse'
*/
messagesRouter.post('/', validateSessionMessage, handleValidationErrors, messageHandlers.createMessage) messagesRouter.post('/', validateSessionMessage, handleValidationErrors, messageHandlers.createMessage)
return messagesRouter return messagesRouter
} }

View File

@ -9,7 +9,7 @@ import type {
} from '@types' } from '@types'
import { ModelMessage, UIMessage, UIMessageChunk } from 'ai' import { ModelMessage, UIMessage, UIMessageChunk } from 'ai'
import { convertToModelMessages, readUIMessageStream } from 'ai' import { convertToModelMessages, readUIMessageStream } from 'ai'
import { eq } from 'drizzle-orm' import { desc, eq } from 'drizzle-orm'
import { BaseService } from '../BaseService' import { BaseService } from '../BaseService'
import { InsertSessionMessageRow, sessionMessagesTable } from '../database/schema' import { InsertSessionMessageRow, sessionMessagesTable } from '../database/schema'
@ -170,29 +170,6 @@ export class SessionMessageService extends BaseService {
return { messages } return { messages }
} }
async saveUserMessage(
tx: any,
sessionId: string,
prompt: string,
agentSessionId: string
): Promise<AgentSessionMessageEntity> {
this.ensureInitialized()
const now = new Date().toISOString()
const insertData: InsertSessionMessageRow = {
session_id: sessionId,
role: 'user',
content: prompt,
agent_session_id: agentSessionId,
created_at: now,
updated_at: now
}
const [saved] = await tx.insert(sessionMessagesTable).values(insertData).returning()
return this.deserializeSessionMessage(saved) as AgentSessionMessageEntity
}
createSessionMessage(session: GetAgentSessionResponse, messageData: CreateSessionMessageRequest): EventEmitter { createSessionMessage(session: GetAgentSessionResponse, messageData: CreateSessionMessageRequest): EventEmitter {
this.ensureInitialized() this.ensureInitialized()
@ -210,12 +187,8 @@ export class SessionMessageService extends BaseService {
req: CreateSessionMessageRequest, req: CreateSessionMessageRequest,
sessionStream: EventEmitter sessionStream: EventEmitter
): Promise<void> { ): Promise<void> {
const previousMessages = session.messages || [] const agentSessionId = await this.getLastAgentSessionId(session.id)
let agentSessionId: string = '' let newAgentSessionId = ''
if (previousMessages.length > 0) {
agentSessionId = previousMessages[previousMessages.length - 1].agent_session_id
}
logger.debug('Session Message stream message data:', { message: req, session_id: agentSessionId }) logger.debug('Session Message stream message data:', { message: req, session_id: agentSessionId })
if (session.agent_type !== 'claude-code') { if (session.agent_type !== 'claude-code') {
@ -223,7 +196,6 @@ export class SessionMessageService extends BaseService {
logger.error('Unsupported agent type for streaming:', { agent_type: session.agent_type }) logger.error('Unsupported agent type for streaming:', { agent_type: session.agent_type })
throw new Error('Unsupported agent type for streaming') throw new Error('Unsupported agent type for streaming')
} }
let newAgentSessionId = ''
// Create the streaming agent invocation (using invokeStream for streaming) // Create the streaming agent invocation (using invokeStream for streaming)
const claudeStream = this.cc.invoke(req.content, session.accessible_paths[0], agentSessionId, { const claudeStream = this.cc.invoke(req.content, session.accessible_paths[0], agentSessionId, {
@ -261,30 +233,55 @@ export class SessionMessageService extends BaseService {
sessionStream.emit('data', { sessionStream.emit('data', {
type: 'error', type: 'error',
error: serializeError(underlyingError) error: serializeError(underlyingError),
persistScheduled: false
}) })
// Always emit a finish chunk at the end // Always emit a finish chunk at the end
sessionStream.emit('data', { sessionStream.emit('data', {
type: 'finish' type: 'finish',
persistScheduled: false
}) })
break break
} }
case 'complete': { case 'complete': {
// Then handle async persistence const completionPayload = event.result ?? accumulator.toModelMessage('assistant')
this.database.transaction(async (tx) => {
await this.saveUserMessage(tx, session.id, req.content, newAgentSessionId)
await this.persistSessionMessageAsync({
tx,
session,
accumulator,
agentSessionId: newAgentSessionId
})
})
// Always emit a finish chunk at the end
sessionStream.emit('data', { sessionStream.emit('data', {
type: 'finish' type: 'complete',
result: completionPayload
}) })
try {
const persisted = await this.database.transaction(async (tx) => {
const userMessage = await this.persistUserMessage(tx, session.id, req.content, newAgentSessionId)
const assistantMessage = await this.persistAssistantMessage({
tx,
session,
accumulator,
agentSessionId: newAgentSessionId
})
return { userMessage, assistantMessage }
})
sessionStream.emit('data', {
type: 'persisted',
message: persisted.assistantMessage,
userMessage: persisted.userMessage
})
} catch (persistError) {
sessionStream.emit('data', {
type: 'persist-error',
error: serializeError(persistError)
})
} finally {
// Always emit a finish chunk at the end
sessionStream.emit('data', {
type: 'finish',
persistScheduled: true
})
}
break break
} }
@ -304,7 +301,51 @@ export class SessionMessageService extends BaseService {
}) })
} }
private async persistSessionMessageAsync({ private async getLastAgentSessionId(sessionId: string): Promise<string> {
this.ensureInitialized()
try {
const result = await this.database
.select({ agent_session_id: sessionMessagesTable.agent_session_id })
.from(sessionMessagesTable)
.where(eq(sessionMessagesTable.session_id, sessionId))
.orderBy(desc(sessionMessagesTable.created_at))
.limit(1)
return result[0]?.agent_session_id || ''
} catch (error) {
logger.error('Failed to get last agent session ID', {
sessionId,
error
})
return ''
}
}
async persistUserMessage(
tx: any,
sessionId: string,
prompt: string,
agentSessionId: string
): Promise<AgentSessionMessageEntity> {
this.ensureInitialized()
const now = new Date().toISOString()
const insertData: InsertSessionMessageRow = {
session_id: sessionId,
role: 'user',
content: prompt,
agent_session_id: agentSessionId,
created_at: now,
updated_at: now
}
const [saved] = await tx.insert(sessionMessagesTable).values(insertData).returning()
return this.deserializeSessionMessage(saved) as AgentSessionMessageEntity
}
private async persistAssistantMessage({
tx, tx,
session, session,
accumulator, accumulator,
@ -314,11 +355,11 @@ export class SessionMessageService extends BaseService {
session: GetAgentSessionResponse session: GetAgentSessionResponse
accumulator: ChunkAccumulator accumulator: ChunkAccumulator
agentSessionId: string agentSessionId: string
}) { }): Promise<AgentSessionMessageEntity> {
if (!session?.id) { if (!session?.id) {
const missingSessionError = new Error('Missing session_id for persisted message') const missingSessionError = new Error('Missing session_id for persisted message')
logger.error('error persisting session message', { error: missingSessionError }) logger.error('error persisting session message', { error: missingSessionError })
return throw missingSessionError
} }
const sessionId = session.id const sessionId = session.id
@ -340,10 +381,13 @@ export class SessionMessageService extends BaseService {
updated_at: now updated_at: now
} }
await tx.insert(sessionMessagesTable).values(insertData).returning() const [saved] = await tx.insert(sessionMessagesTable).values(insertData).returning()
logger.debug('Success Persisted session message') logger.debug('Success Persisted session message')
return this.deserializeSessionMessage(saved) as AgentSessionMessageEntity
} catch (error) { } catch (error) {
logger.error('Failed to persist session message', { error }) logger.error('Failed to persist session message', { error })
throw error
} }
} }

View File

@ -2,7 +2,7 @@
@host=http://localhost:23333 @host=http://localhost:23333
@token=cs-sk-af798ed4-7cf5-4fd7-ae4b-df203b164194 @token=cs-sk-af798ed4-7cf5-4fd7-ae4b-df203b164194
@agent_id=agent_1758092281575_tn9dxio9k @agent_id=agent_1758092281575_tn9dxio9k
@session_id=session_1758092305477_b0g0cmnkp @session_id=session_1758252305914_9kef8yven
### List Sessions ### List Sessions
GET {{host}}/v1/agents/{{agent_id}}/sessions GET {{host}}/v1/agents/{{agent_id}}/sessions
@ -15,7 +15,14 @@ POST {{host}}/v1/agents/{{agent_id}}/sessions
Authorization: Bearer {{token}} Authorization: Bearer {{token}}
Content-Type: application/json Content-Type: application/json
{} {
"name": "Story Writing Session 1",
"instructions": "You are a creative writing assistant. Help me brainstorm and write engaging stories.",
"model": "anthropic:claude-sonnet-4",
"accessible_paths": [
"~/Documents/stories"
]
}
### Get Session Details ### Get Session Details
GET {{host}}/v1/agents/{{agent_id}}/sessions/{{session_id}} GET {{host}}/v1/agents/{{agent_id}}/sessions/{{session_id}}
@ -27,14 +34,28 @@ DELETE {{host}}/v1/agents/{{agent_id}}/sessions/{{session_id}}
Authorization: Bearer {{token}} Authorization: Bearer {{token}}
Content-Type: application/json Content-Type: application/json
### Update Session ### Full Update Session
PUT {{host}}/v1/agents/{{agent_id}}/sessions/{{session_id}} PUT {{host}}/v1/agents/{{agent_id}}/sessions/{{session_id}}
Authorization: Bearer {{token}} Authorization: Bearer {{token}}
Content-Type: application/json Content-Type: application/json
{ {
"name": "Code Review Session 1", "name": "Story Writing Session 2",
"instructions": "Review the newly implemented feature for bugs and improvements" "instructions": "You are a creative writing assistant. Help me brainstorm and write engaging stories.",
"model": "anthropic:claude-sonnet-4",
"accessible_paths": [
"~/Documents/stories"
]
}
### Partial Update Session
PATCH {{host}}/v1/agents/{{agent_id}}/sessions/{{session_id}}
Authorization: Bearer {{token}}
Content-Type: application/json
{
"instructions": "You are a creative writing assistant. Help me brainstorm and write engaging stories. Focus on character development and plot structure.",
} }
@ -44,5 +65,5 @@ Authorization: Bearer {{token}}
Content-Type: application/json Content-Type: application/json
{ {
"content": "a joke about programmers" "content": "Write a short story about a robot learning to love."
} }