From 5048d6987d9cd217ac64d9e0c15ba5fe668c2122 Mon Sep 17 00:00:00 2001 From: Vaayne Date: Fri, 19 Sep 2025 10:27:02 +0800 Subject: [PATCH 1/4] refactor(validators): migrate validation logic to Zod for agents and sessions --- .../routes/agents/middleware/common.ts | 16 +--- .../routes/agents/validators/agents.ts | 46 +++------- .../routes/agents/validators/common.ts | 15 ++-- .../routes/agents/validators/messages.ts | 10 ++- .../routes/agents/validators/sessions.ts | 54 +++--------- .../routes/agents/validators/zodValidator.ts | 68 +++++++++++++++ src/renderer/src/types/agent.ts | 87 +++++++++++++++---- 7 files changed, 173 insertions(+), 123 deletions(-) create mode 100644 src/main/apiServer/routes/agents/validators/zodValidator.ts diff --git a/src/main/apiServer/routes/agents/middleware/common.ts b/src/main/apiServer/routes/agents/middleware/common.ts index b68c5f3bab..582384056b 100644 --- a/src/main/apiServer/routes/agents/middleware/common.ts +++ b/src/main/apiServer/routes/agents/middleware/common.ts @@ -1,24 +1,12 @@ import { Request, Response } from 'express' -import { validationResult } from 'express-validator' import { agentService } from '../../../../services/agents' import { loggerService } from '../../../../services/LoggerService' const logger = loggerService.withContext('ApiServerMiddleware') -// Error handler for validation -export 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 - } +// Since Zod validators handle their own errors, this is now a pass-through +export const handleValidationErrors = (_req: Request, _res: Response, next: any): void => { next() } diff --git a/src/main/apiServer/routes/agents/validators/agents.ts b/src/main/apiServer/routes/agents/validators/agents.ts index 9cdbabfcc3..d9c23b13f1 100644 --- a/src/main/apiServer/routes/agents/validators/agents.ts +++ b/src/main/apiServer/routes/agents/validators/agents.ts @@ -1,37 +1,15 @@ -import { body, param } from 'express-validator' +import { AgentIdParamSchema, CreateAgentRequestSchema, UpdateAgentRequestSchema } from '@types' -export 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 }) -] +import { createZodValidator } from './zodValidator' -export 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 }) -] +export const validateAgent = createZodValidator({ + body: CreateAgentRequestSchema +}) -export const validateAgentId = [param('agentId').notEmpty().withMessage('Agent ID is required')] +export const validateAgentUpdate = createZodValidator({ + body: UpdateAgentRequestSchema +}) + +export const validateAgentId = createZodValidator({ + params: AgentIdParamSchema +}) diff --git a/src/main/apiServer/routes/agents/validators/common.ts b/src/main/apiServer/routes/agents/validators/common.ts index 1d9a811466..4e9a8ceaa2 100644 --- a/src/main/apiServer/routes/agents/validators/common.ts +++ b/src/main/apiServer/routes/agents/validators/common.ts @@ -1,10 +1,7 @@ -import { query } from 'express-validator' +import { PaginationQuerySchema } from '@types' -export 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') -] +import { createZodValidator } from './zodValidator' + +export const validatePagination = createZodValidator({ + query: PaginationQuerySchema +}) diff --git a/src/main/apiServer/routes/agents/validators/messages.ts b/src/main/apiServer/routes/agents/validators/messages.ts index 84de744620..9a4f7dbbf0 100644 --- a/src/main/apiServer/routes/agents/validators/messages.ts +++ b/src/main/apiServer/routes/agents/validators/messages.ts @@ -1,5 +1,7 @@ -import { body } from 'express-validator' +import { CreateSessionMessageRequestSchema } from '@types' -export const validateSessionMessage = [ - body('content').notEmpty().isString().withMessage('Content must be a valid string') -] +import { createZodValidator } from './zodValidator' + +export const validateSessionMessage = createZodValidator({ + body: CreateSessionMessageRequestSchema +}) diff --git a/src/main/apiServer/routes/agents/validators/sessions.ts b/src/main/apiServer/routes/agents/validators/sessions.ts index 6accf2f401..3c87349230 100644 --- a/src/main/apiServer/routes/agents/validators/sessions.ts +++ b/src/main/apiServer/routes/agents/validators/sessions.ts @@ -1,47 +1,15 @@ -import { body, param } from 'express-validator' +import { CreateSessionRequestSchema, SessionIdParamSchema, UpdateSessionRequestSchema } from '@types' -export const validateSession = [ - body('name').optional().isString(), - 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 }) -] +import { createZodValidator } from './zodValidator' -export 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 }) -] +export const validateSession = createZodValidator({ + body: CreateSessionRequestSchema +}) -export const validateStatusUpdate = [ - body('status') - .notEmpty() - .isIn(['idle', 'running', 'completed', 'failed', 'stopped']) - .withMessage('Valid status is required') -] +export const validateSessionUpdate = createZodValidator({ + body: UpdateSessionRequestSchema +}) -export const validateSessionId = [param('sessionId').notEmpty().withMessage('Session ID is required')] +export const validateSessionId = createZodValidator({ + params: SessionIdParamSchema +}) diff --git a/src/main/apiServer/routes/agents/validators/zodValidator.ts b/src/main/apiServer/routes/agents/validators/zodValidator.ts new file mode 100644 index 0000000000..ce8a1cb68f --- /dev/null +++ b/src/main/apiServer/routes/agents/validators/zodValidator.ts @@ -0,0 +1,68 @@ +import { NextFunction,Request, Response } from 'express' +import { ZodError, ZodType } from 'zod' + +export interface ValidationRequest extends Request { + validatedBody?: any + validatedParams?: any + validatedQuery?: any +} + +export interface ZodValidationConfig { + body?: ZodType + params?: ZodType + query?: ZodType +} + +export const createZodValidator = (config: ZodValidationConfig) => { + return (req: ValidationRequest, res: Response, next: NextFunction): void => { + try { + if (config.body && req.body) { + req.validatedBody = config.body.parse(req.body) + } + + if (config.params && req.params) { + req.validatedParams = config.params.parse(req.params) + } + + if (config.query && req.query) { + req.validatedQuery = config.query.parse(req.query) + } + + next() + } catch (error) { + if (error instanceof ZodError) { + const validationErrors = error.issues.map((err) => ({ + type: 'field', + value: err.input, + msg: err.message, + path: err.path.map(p => String(p)).join('.'), + location: getLocationFromPath(err.path, config) + })) + + res.status(400).json({ + error: { + message: 'Validation failed', + type: 'validation_error', + details: validationErrors + } + }) + return + } + + res.status(500).json({ + error: { + message: 'Internal validation error', + type: 'internal_error', + code: 'validation_processing_failed' + } + }) + } + } +} + +function getLocationFromPath(path: (string | number | symbol)[], config: ZodValidationConfig): string { + if (config.body && path.length > 0) return 'body' + if (config.params && path.length > 0) return 'params' + if (config.query && path.length > 0) return 'query' + return 'unknown' +} \ No newline at end of file diff --git a/src/renderer/src/types/agent.ts b/src/renderer/src/types/agent.ts index c8e9a3112b..cad935d0e8 100644 --- a/src/renderer/src/types/agent.ts +++ b/src/renderer/src/types/agent.ts @@ -5,11 +5,21 @@ import { ModelMessage, modelMessageSchema, TextStreamPart, UIMessageChunk } from 'ai' import { z } from 'zod' +// ------------------ Core enums and helper types ------------------ export const PermissionModeSchema = z.enum(['default', 'acceptEdits', 'bypassPermissions', 'plan']) export type PermissionMode = z.infer export type SessionMessageRole = ModelMessage['role'] +const sessionMessageRoles = ['assistant', 'user', 'system', 'tool'] as const satisfies readonly [ + SessionMessageRole, + ...SessionMessageRole[] +] + +export const SessionMessageRoleSchema = z.enum(sessionMessageRoles) + +export type SessionMessageType = TextStreamPart>['type'] + export const AgentTypeSchema = z.enum(['claude-code']) export type AgentType = z.infer @@ -17,8 +27,7 @@ export const isAgentType = (type: unknown): type is AgentType => { return AgentTypeSchema.safeParse(type).success } -export type SessionMessageType = TextStreamPart>['type'] - +// ------------------ Tool metadata ------------------ export const ToolSchema = z.object({ id: z.string(), name: z.string(), @@ -28,6 +37,7 @@ export const ToolSchema = z.object({ export type Tool = z.infer +// ------------------ Agent configuration & base schema ------------------ export const AgentConfigurationSchema = z .object({ permission_mode: PermissionModeSchema.default('default'), // Permission mode, default to 'default' @@ -62,6 +72,8 @@ export const AgentBaseSchema = z.object({ export type AgentBase = z.infer +// ------------------ Persistence entities ------------------ + // Agent entity representing an autonomous agent configuration export const AgentEntitySchema = AgentBaseSchema.extend({ id: z.string(), @@ -95,7 +107,7 @@ export const AgentSessionMessageEntitySchema = z.object({ id: z.number(), // Auto-increment primary key session_id: z.string(), // Reference to session // manual defined. may not synced with ai sdk definition - role: z.enum(['assistant', 'user', 'system', 'tool']), // 'assistant' | 'user' | 'system' | 'tool' + role: SessionMessageRoleSchema, // Enforce roles supported by modelMessageSchema content: modelMessageSchema, agent_session_id: z.string(), // agent session id, use to resume agent session metadata: z.record(z.string(), z.any()).optional(), // Additional metadata (optional) @@ -105,6 +117,8 @@ export const AgentSessionMessageEntitySchema = z.object({ export type AgentSessionMessageEntity = z.infer +// ------------------ Session message payload ------------------ + // Structured content for session messages that preserves both AI SDK and raw data export interface SessionMessageContent { chunk: UIMessageChunk[] // UI-friendly AI SDK chunks for rendering @@ -119,10 +133,11 @@ export interface SessionMessageContent { // - mcps: Optional array of MCP (Model Control Protocol) tool IDs // - allowed_tools: Optional array of permitted tool IDs // - configuration: Optional agent settings (temperature, top_p, etc.) -export interface BaseAgentForm { +// ------------------ Form models ------------------ +export type BaseAgentForm = { id?: string type: AgentType - // These fileds should be editable by user. + // These fields should be editable by user. name: string description?: string instructions?: string @@ -130,30 +145,22 @@ export interface BaseAgentForm { accessible_paths: string[] } -export interface AddAgentForm extends BaseAgentForm { - id?: never -} +export type AddAgentForm = Omit & { id?: never } -export interface UpdateAgentForm extends Partial { +export type UpdateAgentForm = Partial> & { id: string type?: never } export type AgentForm = AddAgentForm | UpdateAgentForm -export interface BaseSessionForm extends AgentBase {} +export type BaseSessionForm = AgentBase -export interface CreateSessionForm extends BaseSessionForm { - id?: never -} +export type CreateSessionForm = BaseSessionForm & { id?: never } -export interface UpdateSessionForm extends Partial { - id: string -} +export type UpdateSessionForm = Partial & { id: string } -// ------------------------ -// API Data Transfer Object -// ------------------------ +// ------------------ API data transfer objects ------------------ export interface CreateAgentRequest extends AgentBase { type: AgentType } @@ -220,3 +227,45 @@ export const AgentServerErrorSchema = z.object({ }) export type AgentServerError = z.infer + +// ------------------ API validation schemas ------------------ + +// Parameter validation schemas +export const AgentIdParamSchema = z.object({ + agentId: z.string().min(1, 'Agent ID is required') +}) + +export const SessionIdParamSchema = z.object({ + sessionId: z.string().min(1, 'Session ID is required') +}) + +// Query validation schemas +export const PaginationQuerySchema = z.object({ + limit: z.coerce.number().int().min(1).max(100).optional().default(20), + offset: z.coerce.number().int().min(0).optional().default(0), + status: z.enum(['idle', 'running', 'completed', 'failed', 'stopped']).optional() +}) + +// Request body validation schemas derived from shared bases +const agentCreatableSchema = AgentBaseSchema.extend({ + name: z.string().min(1, 'Name is required'), + model: z.string().min(1, 'Model is required') +}) + +export const CreateAgentRequestSchema = agentCreatableSchema.extend({ + type: AgentTypeSchema +}) + +export const UpdateAgentRequestSchema = AgentBaseSchema.partial() + +const sessionCreatableSchema = AgentBaseSchema.extend({ + model: z.string().min(1, 'Model is required') +}) + +export const CreateSessionRequestSchema = sessionCreatableSchema + +export const UpdateSessionRequestSchema = sessionCreatableSchema.partial() + +export const CreateSessionMessageRequestSchema = z.object({ + content: z.string().min(1, 'Content must be a valid string') +}) From 4839b91cef134ba79e3ca6829841ed3d27dc05f6 Mon Sep 17 00:00:00 2001 From: Vaayne Date: Fri, 19 Sep 2025 10:27:09 +0800 Subject: [PATCH 2/4] chore(docs): remove session tracking protocol from AI Assistant Guide --- CLAUDE.md | 83 +++---------------------------------------------------- 1 file changed, 4 insertions(+), 79 deletions(-) diff --git a/CLAUDE.md b/CLAUDE.md index 726c59f5e5..3a47b0de5f 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -14,85 +14,10 @@ This file provides guidance to AI coding assistants when working with code in th 1. **Code Search**: Use `ast-grep` for semantic code pattern searches when available. Fallback to `rg` (ripgrep) or `grep` for text-based searches. 2. **UI Framework**: Exclusively use **HeroUI** for all new UI components. The use of `antd` or `styled-components` is strictly **PROHIBITED**. 3. **Quality Assurance**: **Always** run `yarn build:check` before finalizing your work or making any commits. This ensures code quality (linting, testing, and type checking). -4. **Session Tracking Documentation**: MUST Consistently maintain the session SDLC log file following the template structure outlined in the Session Tracking section. -5. **Centralized Logging**: Use the `loggerService` exclusively for all application logging (info, warn, error levels) with proper context. Do not use `console.log`. -6. **External Research**: Leverage `subagent` for gathering external information, including latest documentation, API references, news, or web-based research. This keeps the main conversation focused on the task at hand. - -## Session Tracking Protocol - -Purpose: keep a living SDLC record so any coding agent can pause or resume work without losing momentum. - -### When to Log -- Start a new file when kicking off a feature or major task branch. -- Append to the existing file whenever you switch focus, finish a task, encounter a blocker, or hand over. -- If you resume someone else's session, add a new patch log entry summarizing what you picked up and what remains. - -### File Naming -- `.sessions/YYYYMMDD-.md` -- Example: `.sessions/20250916-agent-onboarding.md` - -### Template -```md -# — SDLC Session () - -## Session Metadata -- Participants: -- Repo state / branch: -- Related tickets / docs: -- Links to prior sessions: - -## Design Brief -- Problem & goals: -- Non-goals / scope: -- Constraints & risks: -- Acceptance criteria: - -## Solution Plan -- Architecture / flow: -- Key interfaces or modules: -- Data considerations: -- Test strategy: - -## Work Plan -| ID | Task | Owner | Depends | Est | Status | Notes | -| --- | ---- | ----- | ------- | --- | ------ | ----- | -| T1 | | | | | TODO | | - -_Status values: TODO, IN PROGRESS, BLOCKED, DONE. Update estimates as work evolves._ - -## Execution Log -### -- Activity summary (what changed, decisions made) -- Artifacts (PRs, commits, file paths, specs) -- Tests / Commands run: -- Issues / Risks: -- Next focus before handoff: - -_Append a new timestamped block for each meaningful work segment._ - -## Handoff Checklist -- [ ] Remaining work captured in Work Plan -- [ ] Blockers / questions called out -- [ ] Links to diffs / PRs / relevant artifacts -- [ ] Next session entry point documented - -## Validation -- [ ] Acceptance criteria met -- [ ] `yarn build:check` passes -- [ ] Tests required by strategy green -- [ ] Docs / tickets updated (if applicable) -``` - -### Usage Example -``` -### 2025-09-16 18:40 -- Activity: Finished wiring HeroUI settings panel skeleton; left TODO for data bindings. -- Artifacts: src/renderer/.../SettingsPanel.tsx, PR #1234 (draft). -- Tests / Commands: yarn lint -- Issues / Risks: Waiting on API schema (#456). -- Next focus: Bind `updateSettings` once API lands; run yarn build:check before flip to PR. -``` - +4. **Centralized Logging**: Use the `loggerService` exclusively for all application logging (info, warn, error levels) with proper context. Do not use `console.log`. +5. **External Research**: Leverage `subagent` for gathering external information, including latest documentation, API references, news, or web-based research. This keeps the main conversation focused on the task at hand. +6. **Code Reviews**: Always seek a code review from a human developer before merging significant changes. This ensures adherence to project standards and catches potential issues. +7. **Documentation**: Update or create documentation for any new features, modules, or significant changes to existing functionality. ## Development Commands From df1d4cd62b41184607b09ce8148e34b1d5ba644f Mon Sep 17 00:00:00 2001 From: Vaayne Date: Fri, 19 Sep 2025 10:41:26 +0800 Subject: [PATCH 3/4] refactor(sessions): update getSession and related methods to include agentId parameter --- .../routes/agents/handlers/messages.ts | 2 +- .../routes/agents/handlers/sessions.ts | 16 +++---- .../agents/services/SessionMessageService.ts | 14 ++---- .../agents/services/SessionService.ts | 44 ++++++++++++------- src/renderer/src/types/agent.ts | 10 +---- 5 files changed, 41 insertions(+), 45 deletions(-) diff --git a/src/main/apiServer/routes/agents/handlers/messages.ts b/src/main/apiServer/routes/agents/handlers/messages.ts index 0f731fe089..4364868dac 100644 --- a/src/main/apiServer/routes/agents/handlers/messages.ts +++ b/src/main/apiServer/routes/agents/handlers/messages.ts @@ -12,7 +12,7 @@ const verifyAgentAndSession = async (agentId: string, sessionId: string) => { throw { status: 404, code: 'agent_not_found', message: 'Agent not found' } } - const session = await sessionService.getSession(sessionId) + const session = await sessionService.getSession(agentId, sessionId) if (!session) { throw { status: 404, code: 'session_not_found', message: 'Session not found' } } diff --git a/src/main/apiServer/routes/agents/handlers/sessions.ts b/src/main/apiServer/routes/agents/handlers/sessions.ts index d9f45833c1..f164d6cd52 100644 --- a/src/main/apiServer/routes/agents/handlers/sessions.ts +++ b/src/main/apiServer/routes/agents/handlers/sessions.ts @@ -64,7 +64,7 @@ export const getSession = async (req: Request, res: Response): Promise const { agentId, sessionId } = req.params logger.info(`Getting session: ${sessionId} for agent: ${agentId}`) - const session = await sessionService.getSession(sessionId) + const session = await sessionService.getSession(agentId, sessionId) if (!session) { logger.warn(`Session not found: ${sessionId}`) @@ -119,7 +119,7 @@ export const updateSession = async (req: Request, res: Response): Promise { + ): Promise<{ messages: AgentSessionMessageEntity[] }> { this.ensureInitialized() - // Get total count - const totalResult = await this.database - .select({ count: count() }) - .from(sessionMessagesTable) - .where(eq(sessionMessagesTable.session_id, sessionId)) - - const total = totalResult[0].count - // Get messages with pagination const baseQuery = this.database .select() @@ -175,7 +167,7 @@ export class SessionMessageService extends BaseService { const messages = result.map((row) => this.deserializeSessionMessage(row)) as AgentSessionMessageEntity[] - return { messages, total } + return { messages } } async saveUserMessage( diff --git a/src/main/services/agents/services/SessionService.ts b/src/main/services/agents/services/SessionService.ts index 1141a26014..63a64c1efb 100644 --- a/src/main/services/agents/services/SessionService.ts +++ b/src/main/services/agents/services/SessionService.ts @@ -78,7 +78,25 @@ export class SessionService extends BaseService { return this.deserializeJsonFields(result[0]) as AgentSessionEntity } - async getSession(id: string): Promise { + async getSession(agentId: string, id: string): Promise { + this.ensureInitialized() + + const result = await this.database + .select() + .from(sessionsTable) + .where(and(eq(sessionsTable.id, id), eq(sessionsTable.agent_id, agentId))) + .limit(1) + + if (!result[0]) { + return null + } + + const session = this.deserializeJsonFields(result[0]) as GetAgentSessionResponse + + return session + } + + async getSessionById(id: string): Promise { this.ensureInitialized() const result = await this.database.select().from(sessionsTable).where(eq(sessionsTable.id, id)).limit(1) @@ -92,14 +110,6 @@ export class SessionService extends BaseService { return session } - async getSessionWithAgent(id: string): Promise { - this.ensureInitialized() - - // TODO: Implement join query with agents table when needed - // For now, just return the session - return await this.getSession(id) - } - async listSessions( agentId?: string, options: ListOptions = {} @@ -139,11 +149,11 @@ export class SessionService extends BaseService { return { sessions, total } } - async updateSession(id: string, updates: UpdateSessionRequest): Promise { + async updateSession(agentId: string, id: string, updates: UpdateSessionRequest): Promise { this.ensureInitialized() // Check if session exists - const existing = await this.getSession(id) + const existing = await this.getSession(agentId, id) if (!existing) { return null } @@ -173,24 +183,26 @@ export class SessionService extends BaseService { await this.database.update(sessionsTable).set(updateData).where(eq(sessionsTable.id, id)) - return await this.getSession(id) + return await this.getSession(agentId, id) } - async deleteSession(id: string): Promise { + async deleteSession(agentId: string, id: string): Promise { this.ensureInitialized() - const result = await this.database.delete(sessionsTable).where(eq(sessionsTable.id, id)) + const result = await this.database + .delete(sessionsTable) + .where(and(eq(sessionsTable.id, id), eq(sessionsTable.agent_id, agentId))) return result.rowsAffected > 0 } - async sessionExists(id: string): Promise { + async sessionExists(agentId: string, id: string): Promise { this.ensureInitialized() const result = await this.database .select({ id: sessionsTable.id }) .from(sessionsTable) - .where(eq(sessionsTable.id, id)) + .where(and(eq(sessionsTable.id, id), eq(sessionsTable.agent_id, agentId))) .limit(1) return result.length > 0 diff --git a/src/renderer/src/types/agent.ts b/src/renderer/src/types/agent.ts index cad935d0e8..ecce2941cf 100644 --- a/src/renderer/src/types/agent.ts +++ b/src/renderer/src/types/agent.ts @@ -2,7 +2,7 @@ * Database entity types for Agent, Session, and SessionMessage * Shared between main and renderer processes */ -import { ModelMessage, modelMessageSchema, TextStreamPart, UIMessageChunk } from 'ai' +import { ModelMessage, modelMessageSchema, TextStreamPart } from 'ai' import { z } from 'zod' // ------------------ Core enums and helper types ------------------ @@ -119,14 +119,6 @@ export type AgentSessionMessageEntity = z.infer Date: Fri, 19 Sep 2025 11:13:05 +0800 Subject: [PATCH 4/4] feat(agents, sessions): implement replace functionality for agent and session updates --- .../routes/agents/handlers/agents.ts | 13 ++++-- .../routes/agents/handlers/sessions.ts | 11 +++-- src/main/apiServer/routes/agents/index.ts | 12 +++++- .../routes/agents/validators/agents.ts | 11 ++++- .../routes/agents/validators/sessions.ts | 11 ++++- src/main/services/agents/BaseService.ts | 2 +- .../services/agents/services/AgentService.ts | 33 +++++++++------ .../agents/services/SessionService.ts | 42 +++++++++---------- src/renderer/src/types/agent.ts | 8 ++++ 9 files changed, 97 insertions(+), 46 deletions(-) diff --git a/src/main/apiServer/routes/agents/handlers/agents.ts b/src/main/apiServer/routes/agents/handlers/agents.ts index cd1cbf850b..aa021af063 100644 --- a/src/main/apiServer/routes/agents/handlers/agents.ts +++ b/src/main/apiServer/routes/agents/handlers/agents.ts @@ -1,8 +1,9 @@ import { loggerService } from '@logger' -import { ListAgentsResponse } from '@types' +import { ListAgentsResponse,type ReplaceAgentRequest, type UpdateAgentRequest } from '@types' import { Request, Response } from 'express' import { agentService } from '../../../../services/agents' +import type { ValidationRequest } from '../validators/zodValidator' const logger = loggerService.withContext('ApiServerAgentsHandlers') @@ -263,7 +264,10 @@ export const updateAgent = async (req: Request, res: Response): Promise logger.info(`Partially updating agent: ${agentId}`) logger.debug('Partial update data:', req.body) - const agent = await agentService.updateAgent(agentId, req.body) + const { validatedBody } = req as ValidationRequest + const updatePayload = (validatedBody ?? {}) as UpdateAgentRequest + + const agent = await agentService.updateAgent(agentId, updatePayload) if (!agent) { logger.warn(`Agent not found for partial update: ${agentId}`) diff --git a/src/main/apiServer/routes/agents/handlers/sessions.ts b/src/main/apiServer/routes/agents/handlers/sessions.ts index f164d6cd52..fb28515558 100644 --- a/src/main/apiServer/routes/agents/handlers/sessions.ts +++ b/src/main/apiServer/routes/agents/handlers/sessions.ts @@ -1,8 +1,10 @@ import { loggerService } from '@logger' import { sessionMessageService, sessionService } from '@main/services/agents' -import { CreateSessionResponse, ListAgentSessionsResponse } from '@types' +import { CreateSessionResponse, ListAgentSessionsResponse,type ReplaceSessionRequest } from '@types' import { Request, Response } from 'express' +import type { ValidationRequest } from '../validators/zodValidator' + const logger = loggerService.withContext('ApiServerSessionsHandlers') export const createSession = async (req: Request, res: Response): Promise => { @@ -131,9 +133,10 @@ export const updateSession = async (req: Request, res: Response): Promise { sessionsRouter.put( '/:sessionId', validateSessionId, - validateSessionUpdate, + validateSessionReplace, handleValidationErrors, sessionHandlers.updateSession ) diff --git a/src/main/apiServer/routes/agents/validators/agents.ts b/src/main/apiServer/routes/agents/validators/agents.ts index d9c23b13f1..4b29e66929 100644 --- a/src/main/apiServer/routes/agents/validators/agents.ts +++ b/src/main/apiServer/routes/agents/validators/agents.ts @@ -1,4 +1,9 @@ -import { AgentIdParamSchema, CreateAgentRequestSchema, UpdateAgentRequestSchema } from '@types' +import { + AgentIdParamSchema, + CreateAgentRequestSchema, + ReplaceAgentRequestSchema, + UpdateAgentRequestSchema +} from '@types' import { createZodValidator } from './zodValidator' @@ -6,6 +11,10 @@ export const validateAgent = createZodValidator({ body: CreateAgentRequestSchema }) +export const validateAgentReplace = createZodValidator({ + body: ReplaceAgentRequestSchema +}) + export const validateAgentUpdate = createZodValidator({ body: UpdateAgentRequestSchema }) diff --git a/src/main/apiServer/routes/agents/validators/sessions.ts b/src/main/apiServer/routes/agents/validators/sessions.ts index 3c87349230..5081849649 100644 --- a/src/main/apiServer/routes/agents/validators/sessions.ts +++ b/src/main/apiServer/routes/agents/validators/sessions.ts @@ -1,4 +1,9 @@ -import { CreateSessionRequestSchema, SessionIdParamSchema, UpdateSessionRequestSchema } from '@types' +import { + CreateSessionRequestSchema, + ReplaceSessionRequestSchema, + SessionIdParamSchema, + UpdateSessionRequestSchema +} from '@types' import { createZodValidator } from './zodValidator' @@ -6,6 +11,10 @@ export const validateSession = createZodValidator({ body: CreateSessionRequestSchema }) +export const validateSessionReplace = createZodValidator({ + body: ReplaceSessionRequestSchema +}) + export const validateSessionUpdate = createZodValidator({ body: UpdateSessionRequestSchema }) diff --git a/src/main/services/agents/BaseService.ts b/src/main/services/agents/BaseService.ts index 2da1a8aa96..40a3d059dc 100644 --- a/src/main/services/agents/BaseService.ts +++ b/src/main/services/agents/BaseService.ts @@ -27,7 +27,7 @@ export abstract class BaseService { protected static db: LibSQLDatabase | null = null protected static isInitialized = false protected static initializationPromise: Promise | null = null - protected jsonFields: string[] = ['built_in_tools', 'mcps', 'configuration', 'accessible_paths'] + protected jsonFields: string[] = ['built_in_tools', 'mcps', 'configuration', 'accessible_paths', 'allowed_tools'] /** * Initialize database with retry logic and proper error handling diff --git a/src/main/services/agents/services/AgentService.ts b/src/main/services/agents/services/AgentService.ts index ec72bacd54..03110cc55a 100644 --- a/src/main/services/agents/services/AgentService.ts +++ b/src/main/services/agents/services/AgentService.ts @@ -1,7 +1,8 @@ import path from 'node:path' import { getDataPath } from '@main/utils' -import type { +import { + AgentBaseSchema, AgentEntity, CreateAgentRequest, CreateAgentResponse, @@ -111,7 +112,11 @@ export class AgentService extends BaseService { return { agents, total: totalResult[0].count } } - async updateAgent(id: string, updates: UpdateAgentRequest): Promise { + async updateAgent( + id: string, + updates: UpdateAgentRequest, + options: { replace?: boolean } = {} + ): Promise { this.ensureInitialized() // Check if agent exists @@ -126,18 +131,20 @@ export class AgentService extends BaseService { const updateData: Partial = { updated_at: now } + const replaceableFields = Object.keys(AgentBaseSchema.shape) as (keyof AgentRow)[] + const shouldReplace = options.replace ?? false + + for (const field of replaceableFields) { + if (shouldReplace || Object.prototype.hasOwnProperty.call(serializedUpdates, field)) { + if (Object.prototype.hasOwnProperty.call(serializedUpdates, field)) { + const value = serializedUpdates[field as keyof typeof serializedUpdates] + ;(updateData as Record)[field] = value ?? null + } else if (shouldReplace) { + ;(updateData as Record)[field] = null + } + } + } - // Only update fields that are provided - if (serializedUpdates.name !== undefined) updateData.name = serializedUpdates.name - if (serializedUpdates.description !== undefined) updateData.description = serializedUpdates.description - if (serializedUpdates.instructions !== undefined) updateData.instructions = serializedUpdates.instructions - if (serializedUpdates.model !== undefined) updateData.model = serializedUpdates.model - if (serializedUpdates.plan_model !== undefined) updateData.plan_model = serializedUpdates.plan_model - if (serializedUpdates.small_model !== undefined) updateData.small_model = serializedUpdates.small_model - if (serializedUpdates.mcps !== undefined) updateData.mcps = serializedUpdates.mcps - if (serializedUpdates.configuration !== undefined) updateData.configuration = serializedUpdates.configuration - if (serializedUpdates.accessible_paths !== undefined) - updateData.accessible_paths = serializedUpdates.accessible_paths await this.database.update(agentsTable).set(updateData).where(eq(agentsTable.id, id)) return await this.getAgent(id) } diff --git a/src/main/services/agents/services/SessionService.ts b/src/main/services/agents/services/SessionService.ts index 63a64c1efb..07c4c40cce 100644 --- a/src/main/services/agents/services/SessionService.ts +++ b/src/main/services/agents/services/SessionService.ts @@ -1,11 +1,12 @@ -import type { - AgentEntity, - AgentSessionEntity, - CreateSessionRequest, - CreateSessionResponse, - GetAgentSessionResponse, - ListOptions, - UpdateSessionRequest +import { + AgentBaseSchema, + type AgentEntity, + type AgentSessionEntity, + type CreateSessionRequest, + type CreateSessionResponse, + type GetAgentSessionResponse, + type ListOptions, + type UpdateSessionRequest } from '@types' import { and, count, eq, type SQL } from 'drizzle-orm' @@ -149,7 +150,11 @@ export class SessionService extends BaseService { return { sessions, total } } - async updateSession(agentId: string, id: string, updates: UpdateSessionRequest): Promise { + async updateSession( + agentId: string, + id: string, + updates: UpdateSessionRequest + ): Promise { this.ensureInitialized() // Check if session exists @@ -167,19 +172,14 @@ export class SessionService extends BaseService { const updateData: Partial = { updated_at: now } + const replaceableFields = Object.keys(AgentBaseSchema.shape) as (keyof SessionRow)[] - // Only update fields that are provided - if (serializedUpdates.name !== undefined) updateData.name = serializedUpdates.name - - if (serializedUpdates.model !== undefined) updateData.model = serializedUpdates.model - if (serializedUpdates.plan_model !== undefined) updateData.plan_model = serializedUpdates.plan_model - if (serializedUpdates.small_model !== undefined) updateData.small_model = serializedUpdates.small_model - - if (serializedUpdates.mcps !== undefined) updateData.mcps = serializedUpdates.mcps - - if (serializedUpdates.configuration !== undefined) updateData.configuration = serializedUpdates.configuration - if (serializedUpdates.accessible_paths !== undefined) - updateData.accessible_paths = serializedUpdates.accessible_paths + for (const field of replaceableFields) { + if (Object.prototype.hasOwnProperty.call(serializedUpdates, field)) { + const value = serializedUpdates[field as keyof typeof serializedUpdates] + ;(updateData as Record)[field] = value ?? null + } + } await this.database.update(sessionsTable).set(updateData).where(eq(sessionsTable.id, id)) diff --git a/src/renderer/src/types/agent.ts b/src/renderer/src/types/agent.ts index ecce2941cf..9e2f967a0d 100644 --- a/src/renderer/src/types/agent.ts +++ b/src/renderer/src/types/agent.ts @@ -163,6 +163,8 @@ export type CreateAgentResponse = AgentEntity export interface UpdateAgentRequest extends Partial {} +export type ReplaceAgentRequest = AgentBase + export const GetAgentResponseSchema = AgentEntitySchema.extend({ built_in_tools: z.array(ToolSchema).optional() // Built-in tools available to the agent }) @@ -250,6 +252,8 @@ export const CreateAgentRequestSchema = agentCreatableSchema.extend({ export const UpdateAgentRequestSchema = AgentBaseSchema.partial() +export const ReplaceAgentRequestSchema = AgentBaseSchema + const sessionCreatableSchema = AgentBaseSchema.extend({ model: z.string().min(1, 'Model is required') }) @@ -258,6 +262,10 @@ export const CreateSessionRequestSchema = sessionCreatableSchema export const UpdateSessionRequestSchema = sessionCreatableSchema.partial() +export const ReplaceSessionRequestSchema = sessionCreatableSchema + +export type ReplaceSessionRequest = z.infer + export const CreateSessionMessageRequestSchema = z.object({ content: z.string().min(1, 'Content must be a valid string') })