♻️ refactor: standardize tool management and API responses

- Rename built_in_tools field to tools for consistency
- Add type field to Tool schema (builtin/mcp/custom)
- Consolidate tool handling in BaseService with listMcpTools method
- Remove unused CreateSessionResponse and related schemas
- Clean up unused imports and dead code in session handlers
- Unify agent and session tool resolution logic
This commit is contained in:
Vaayne
2025-09-21 17:23:04 +08:00
parent a09c52424f
commit 1a49972583
9 changed files with 100 additions and 137 deletions

View File

@@ -365,11 +365,11 @@ export const updateAgent = async (req: Request, res: Response): Promise<Response
* small_model:
* type: string
* description: Optional small/fast model ID
* built_in_tools:
* tools:
* type: array
* items:
* type: string
* description: Built-in tool IDs
* description: Tools
* mcps:
* type: array
* items:

View File

@@ -1,11 +1,6 @@
import { loggerService } from '@logger'
import { AgentModelValidationError, sessionMessageService, sessionService } from '@main/services/agents'
import {
CreateSessionResponse,
ListAgentSessionsResponse,
type ReplaceSessionRequest,
UpdateSessionResponse
} from '@types'
import { ListAgentSessionsResponse, type ReplaceSessionRequest, UpdateSessionResponse } from '@types'
import { Request, Response } from 'express'
import type { ValidationRequest } from '../validators/zodValidator'
@@ -28,9 +23,9 @@ export const createSession = async (req: Request, res: Response): Promise<Respon
logger.info(`Creating new session for agent: ${agentId}`)
logger.debug('Session data:', sessionData)
const session = (await sessionService.createSession(agentId, sessionData)) satisfies CreateSessionResponse
const session = await sessionService.createSession(agentId, sessionData)
logger.info(`Session created successfully: ${session.id}`)
logger.info(`Session created successfully: ${session?.id}`)
return res.status(201).json(session)
} catch (error: any) {
if (error instanceof AgentModelValidationError) {
@@ -332,35 +327,3 @@ export const listAllSessions = async (req: Request, res: Response): Promise<Resp
})
}
}
export const getSessionById = async (req: Request, res: Response): Promise<Response> => {
try {
const { sessionId } = req.params
logger.info(`Getting session: ${sessionId}`)
const session = await sessionService.getSessionById(sessionId)
if (!session) {
logger.warn(`Session not found: ${sessionId}`)
return res.status(404).json({
error: {
message: 'Session not found',
type: 'not_found',
code: 'session_not_found'
}
})
}
logger.info(`Session retrieved successfully: ${sessionId}`)
return res.json(session)
} catch (error: any) {
logger.error('Error getting session:', error)
return res.status(500).json({
error: {
message: 'Failed to get session',
type: 'internal_error',
code: 'session_get_failed'
}
})
}
}

View File

@@ -115,41 +115,6 @@ class MCPApiService extends EventEmitter {
logger.info(`Server with id ${id} info:`, { tools: JSON.stringify(tools) })
// const [version, tools, prompts, resources] = await Promise.all([
// () => {
// try {
// return client.getServerVersion()
// } catch (error) {
// logger.error(`Failed to get server version for id ${id}:`, { error: error })
// return '1.0.0'
// }
// },
// (() => {
// try {
// return client.listTools()
// } catch (error) {
// logger.error(`Failed to list tools for id ${id}:`, { error: error })
// return []
// }
// })(),
// (() => {
// try {
// return client.listPrompts()
// } catch (error) {
// logger.error(`Failed to list prompts for id ${id}:`, { error: error })
// return []
// }
// })(),
// (() => {
// try {
// return client.listResources()
// } catch (error) {
// logger.error(`Failed to list resources for id ${id}:`, { error: error })
// return []
// }
// })()
// ])
return {
id: server.id,
name: server.name,

View File

@@ -1,7 +1,8 @@
import { type Client, createClient } from '@libsql/client'
import { loggerService } from '@logger'
import { mcpApiService } from '@main/apiServer/services/mcp'
import { ModelValidationError, validateModelId } from '@main/apiServer/utils'
import { AgentType, objectKeys, Provider } from '@types'
import { AgentType, MCPTool, objectKeys, Provider, Tool } from '@types'
import { drizzle, type LibSQLDatabase } from 'drizzle-orm/libsql'
import fs from 'fs'
import path from 'path'
@@ -10,6 +11,7 @@ import { MigrationService } from './database/MigrationService'
import * as schema from './database/schema'
import { dbPath } from './drizzle.config'
import { AgentModelField, AgentModelValidationError } from './errors'
import { builtinTools } from './services/claudecode/tools'
const logger = loggerService.withContext('BaseService')
@@ -30,7 +32,7 @@ export abstract class BaseService {
protected static db: LibSQLDatabase<typeof schema> | null = null
protected static isInitialized = false
protected static initializationPromise: Promise<void> | null = null
protected jsonFields: string[] = ['built_in_tools', 'mcps', 'configuration', 'accessible_paths', 'allowed_tools']
protected jsonFields: string[] = ['tools', 'mcps', 'configuration', 'accessible_paths', 'allowed_tools']
/**
* Initialize database with retry logic and proper error handling
@@ -49,6 +51,31 @@ export abstract class BaseService {
return BaseService.initializationPromise
}
public async listMcpTools(agentType: AgentType, ids?: string[]): Promise<Tool[]> {
const tools: Tool[] = []
if (agentType === 'claude-code') {
tools.push(...builtinTools)
}
if (ids && ids.length > 0) {
for (const id of ids) {
const server = await mcpApiService.getServerInfo(id)
if (server) {
server.tools.forEach((tool: MCPTool) => {
tools.push({
id: `mcp_${id}_${tool.name}`,
name: tool.name,
type: 'mcp',
description: tool.description || '',
requirePermissions: true
})
})
}
}
}
return tools
}
private static async performInitialization(): Promise<void> {
const maxRetries = 3
let lastError: Error

View File

@@ -16,7 +16,6 @@ import { count, eq } from 'drizzle-orm'
import { BaseService } from '../BaseService'
import { type AgentRow, agentsTable, type InsertAgentRow } from '../database/schema'
import { AgentModelField } from '../errors'
import { builtinTools } from './claudecode/tools'
export class AgentService extends BaseService {
private static instance: AgentService | null = null
@@ -92,10 +91,7 @@ export class AgentService extends BaseService {
}
const agent = this.deserializeJsonFields(result[0]) as GetAgentResponse
if (agent.type === 'claude-code') {
agent.built_in_tools = builtinTools
}
agent.tools = await this.listMcpTools(agent.type, agent.mcps)
return agent
}
@@ -115,11 +111,9 @@ export class AgentService extends BaseService {
const agents = result.map((row) => this.deserializeJsonFields(row)) as GetAgentResponse[]
agents.forEach((agent) => {
if (agent.type === 'claude-code') {
agent.built_in_tools = builtinTools
}
})
for (const agent of agents) {
agent.tools = await this.listMcpTools(agent.type, agent.mcps)
}
return { agents, total: totalResult[0].count }
}

View File

@@ -3,7 +3,6 @@ import {
type AgentEntity,
type AgentSessionEntity,
type CreateSessionRequest,
type CreateSessionResponse,
type GetAgentSessionResponse,
type ListOptions,
type UpdateSessionRequest,
@@ -30,7 +29,7 @@ export class SessionService extends BaseService {
await BaseService.initialize()
}
async createSession(agentId: string, req: CreateSessionRequest): Promise<CreateSessionResponse> {
async createSession(agentId: string, req: CreateSessionRequest): Promise<GetAgentSessionResponse | null> {
this.ensureInitialized()
// Validate agent exists - we'll need to import AgentService for this check
@@ -89,7 +88,8 @@ export class SessionService extends BaseService {
throw new Error('Failed to create session')
}
return this.deserializeJsonFields(result[0]) as AgentSessionEntity
const session = this.deserializeJsonFields(result[0])
return await this.getSession(agentId, session.id)
}
async getSession(agentId: string, id: string): Promise<GetAgentSessionResponse | null> {
@@ -106,21 +106,7 @@ export class SessionService extends BaseService {
}
const session = this.deserializeJsonFields(result[0]) as GetAgentSessionResponse
return session
}
async getSessionById(id: string): Promise<GetAgentSessionResponse | null> {
this.ensureInitialized()
const result = await this.database.select().from(sessionsTable).where(eq(sessionsTable.id, id)).limit(1)
if (!result[0]) {
return null
}
const session = this.deserializeJsonFields(result[0]) as GetAgentSessionResponse
session.tools = await this.listMcpTools(session.agent_type, session.mcps)
return session
}

View File

@@ -2,47 +2,83 @@ import { Tool } from '@types'
// https://docs.anthropic.com/en/docs/claude-code/settings#tools-available-to-claude
export const builtinTools: Tool[] = [
{ id: 'Bash', name: 'Bash', description: 'Executes shell commands in your environment', requirePermissions: true },
{ id: 'Edit', name: 'Edit', description: 'Makes targeted edits to specific files', requirePermissions: true },
{ id: 'Glob', name: 'Glob', description: 'Finds files based on pattern matching', requirePermissions: false },
{ id: 'Grep', name: 'Grep', description: 'Searches for patterns in file contents', requirePermissions: false },
{
id: 'Bash',
name: 'Bash',
description: 'Executes shell commands in your environment',
requirePermissions: true,
type: 'builtin'
},
{
id: 'Edit',
name: 'Edit',
description: 'Makes targeted edits to specific files',
requirePermissions: true,
type: 'builtin'
},
{
id: 'Glob',
name: 'Glob',
description: 'Finds files based on pattern matching',
requirePermissions: false,
type: 'builtin'
},
{
id: 'Grep',
name: 'Grep',
description: 'Searches for patterns in file contents',
requirePermissions: false,
type: 'builtin'
},
{
id: 'MultiEdit',
name: 'MultiEdit',
description: 'Performs multiple edits on a single file atomically',
requirePermissions: true
requirePermissions: true,
type: 'builtin'
},
{
id: 'NotebookEdit',
name: 'NotebookEdit',
description: 'Modifies Jupyter notebook cells',
requirePermissions: true
requirePermissions: true,
type: 'builtin'
},
{
id: 'NotebookRead',
name: 'NotebookRead',
description: 'Reads and displays Jupyter notebook contents',
requirePermissions: false
requirePermissions: false,
type: 'builtin'
},
{ id: 'Read', name: 'Read', description: 'Reads the contents of files', requirePermissions: false },
{ id: 'Read', name: 'Read', description: 'Reads the contents of files', requirePermissions: false, type: 'builtin' },
{
id: 'Task',
name: 'Task',
description: 'Runs a sub-agent to handle complex, multi-step tasks',
requirePermissions: false
requirePermissions: false,
type: 'builtin'
},
{
id: 'TodoWrite',
name: 'TodoWrite',
description: 'Creates and manages structured task lists',
requirePermissions: false
requirePermissions: false,
type: 'builtin'
},
{
id: 'WebFetch',
name: 'WebFetch',
description: 'Fetches content from a specified URL',
requirePermissions: true,
type: 'builtin'
},
{ id: 'WebFetch', name: 'WebFetch', description: 'Fetches content from a specified URL', requirePermissions: true },
{
id: 'WebSearch',
name: 'WebSearch',
description: 'Performs web searches with domain filtering',
requirePermissions: true
requirePermissions: true,
type: 'builtin'
},
{ id: 'Write', name: 'Write', description: 'Creates or overwrites files', requirePermissions: true }
{ id: 'Write', name: 'Write', description: 'Creates or overwrites files', requirePermissions: true, type: 'builtin' }
]

View File

@@ -11,11 +11,10 @@ import {
CreateAgentResponseSchema,
CreateSessionForm,
CreateSessionRequest,
CreateSessionResponse,
CreateSessionResponseSchema,
GetAgentResponse,
GetAgentResponseSchema,
GetAgentSessionResponse,
GetAgentSessionResponseSchema,
ListAgentSessionsResponse,
ListAgentSessionsResponseSchema,
type ListAgentsResponse,
@@ -27,9 +26,7 @@ import {
UpdateAgentResponse,
UpdateAgentResponseSchema,
UpdateSessionForm,
UpdateSessionRequest,
UpdateSessionResponse,
UpdateSessionResponseSchema
UpdateSessionRequest
} from '@types'
import axios, { Axios, AxiosRequestConfig, isAxiosError } from 'axios'
import { ZodError } from 'zod'
@@ -172,12 +169,12 @@ export class AgentApiClient {
}
}
public async createSession(agentId: string, session: CreateSessionForm): Promise<CreateSessionResponse> {
public async createSession(agentId: string, session: CreateSessionForm): Promise<GetAgentSessionResponse> {
const url = this.getSessionPaths(agentId).base
try {
const payload = session satisfies CreateSessionRequest
const response = await this.axios.post(url, payload)
const data = CreateSessionResponseSchema.parse(response.data)
const data = GetAgentSessionResponseSchema.parse(response.data)
return data
} catch (error) {
throw processError(error, 'Failed to add session.')
@@ -209,12 +206,12 @@ export class AgentApiClient {
}
}
public async updateSession(agentId: string, session: UpdateSessionForm): Promise<UpdateSessionResponse> {
public async updateSession(agentId: string, session: UpdateSessionForm): Promise<GetAgentSessionResponse> {
const url = this.getSessionPaths(agentId).withId(session.id)
try {
const payload = session satisfies UpdateSessionRequest
const response = await this.axios.patch(url, payload)
const data = UpdateSessionResponseSchema.parse(response.data)
const data = GetAgentSessionResponseSchema.parse(response.data)
if (session.id !== data.id) {
throw new Error('Session ID mismatch in response')
}

View File

@@ -35,6 +35,7 @@ export const isAgentType = (type: unknown): type is AgentType => {
export const ToolSchema = z.object({
id: z.string(),
name: z.string(),
type: z.enum(['builtin', 'mcp', 'custom']),
description: z.string().optional(),
requirePermissions: z.boolean().optional()
})
@@ -201,7 +202,7 @@ export interface UpdateAgentRequest extends Partial<AgentBase> {}
export type ReplaceAgentRequest = AgentBase
export const GetAgentResponseSchema = AgentEntitySchema.extend({
built_in_tools: z.array(ToolSchema).optional() // Built-in tools available to the agent
tools: z.array(ToolSchema).optional() // All tools available to the agent (including built-in and custom)
})
export type GetAgentResponse = z.infer<typeof GetAgentResponseSchema>
@@ -224,7 +225,7 @@ export type CreateSessionRequest = z.infer<typeof CreateSessionRequestSchema>
export interface UpdateSessionRequest extends Partial<AgentBase> {}
export const GetAgentSessionResponseSchema = AgentSessionEntitySchema.extend({
built_in_tools: z.array(ToolSchema).optional(), // Built-in tools available to the agent
tools: z.array(ToolSchema).optional(), // All tools available to the session (including built-in and custom)
messages: z.array(AgentSessionMessageEntitySchema).optional() // Messages in the session
})
@@ -241,12 +242,6 @@ export type ListAgentSessionsResponse = z.infer<typeof ListAgentSessionsResponse
export type CreateSessionMessageRequest = z.infer<typeof CreateSessionMessageRequestSchema>
export const CreateSessionResponseSchema = AgentSessionEntitySchema
export type CreateSessionResponse = AgentSessionEntity
export const UpdateSessionResponseSchema = GetAgentSessionResponseSchema
export type UpdateSessionResponse = GetAgentSessionResponse
export const AgentServerErrorSchema = z.object({