feat(agents): enhance error messages for agent and session operations; update accessible paths handling

This commit is contained in:
Vaayne 2025-09-19 20:20:14 +08:00
parent d6468f33c5
commit f0ac74dccf
6 changed files with 53 additions and 21 deletions

View File

@ -71,7 +71,7 @@ export const createAgent = async (req: Request, res: Response): Promise<Response
logger.error('Error creating agent:', error) logger.error('Error creating agent:', error)
return res.status(500).json({ return res.status(500).json({
error: { error: {
message: 'Failed to create agent', message: `Failed to create agent: ${error.message}`,
type: 'internal_error', type: 'internal_error',
code: 'agent_creation_failed' code: 'agent_creation_failed'
} }
@ -315,7 +315,7 @@ export const updateAgent = async (req: Request, res: Response): Promise<Response
logger.error('Error updating agent:', error) logger.error('Error updating agent:', error)
return res.status(500).json({ return res.status(500).json({
error: { error: {
message: 'Failed to update agent', message: 'Failed to update agent: ' + error.message,
type: 'internal_error', type: 'internal_error',
code: 'agent_update_failed' code: 'agent_update_failed'
} }
@ -461,7 +461,7 @@ export const patchAgent = async (req: Request, res: Response): Promise<Response>
logger.error('Error partially updating agent:', error) logger.error('Error partially updating agent:', error)
return res.status(500).json({ return res.status(500).json({
error: { error: {
message: 'Failed to partially update agent', message: `Failed to partially update agent: ${error.message}`,
type: 'internal_error', type: 'internal_error',
code: 'agent_patch_failed' code: 'agent_patch_failed'
} }

View File

@ -51,7 +51,7 @@ export const createSession = async (req: Request, res: Response): Promise<Respon
logger.error('Error creating session:', error) logger.error('Error creating session:', error)
return res.status(500).json({ return res.status(500).json({
error: { error: {
message: 'Failed to create session', message: `Failed to create session: ${error.message}`,
type: 'internal_error', type: 'internal_error',
code: 'session_creation_failed' code: 'session_creation_failed'
} }
@ -195,7 +195,7 @@ export const updateSession = async (req: Request, res: Response): Promise<Respon
logger.error('Error updating session:', error) logger.error('Error updating session:', error)
return res.status(500).json({ return res.status(500).json({
error: { error: {
message: 'Failed to update session', message: `Failed to update session: ${error.message}`,
type: 'internal_error', type: 'internal_error',
code: 'session_update_failed' code: 'session_update_failed'
} }
@ -254,7 +254,7 @@ export const patchSession = async (req: Request, res: Response): Promise<Respons
logger.error('Error patching session:', error) logger.error('Error patching session:', error)
return res.status(500).json({ return res.status(500).json({
error: { error: {
message: 'Failed to patch session', message: `Failed to patch session, ${error.message}`,
type: 'internal_error', type: 'internal_error',
code: 'session_patch_failed' code: 'session_patch_failed'
} }

View File

@ -163,20 +163,36 @@ export abstract class BaseService {
return deserialized return deserialized
} }
protected ensurePathsExist(paths?: string[]): void { /**
* Validate, normalize, and ensure filesystem access for a set of absolute paths.
*
* - Requires every entry to be an absolute path and throws if not.
* - Normalizes each path and deduplicates while preserving order.
* - Creates missing directories (or parent directories for file-like paths).
*/
protected ensurePathsExist(paths?: string[]): string[] {
if (!paths?.length) { if (!paths?.length) {
return return []
} }
const sanitizedPaths: string[] = []
const seenPaths = new Set<string>()
for (const rawPath of paths) { for (const rawPath of paths) {
if (!rawPath) { if (!rawPath) {
continue continue
} }
const resolvedPath = path.resolve(rawPath) if (!path.isAbsolute(rawPath)) {
throw new Error(`Accessible path must be absolute: ${rawPath}`)
}
// Normalize to provide consistent values to downstream consumers.
const resolvedPath = path.normalize(rawPath)
let stats: fs.Stats | null = null let stats: fs.Stats | null = null
try { try {
// Attempt to stat the path to understand whether it already exists and if it is a file.
if (fs.existsSync(resolvedPath)) { if (fs.existsSync(resolvedPath)) {
stats = fs.statSync(resolvedPath) stats = fs.statSync(resolvedPath)
} }
@ -190,6 +206,7 @@ export abstract class BaseService {
const looksLikeFile = const looksLikeFile =
(stats && stats.isFile()) || (!stats && path.extname(resolvedPath) !== '' && !resolvedPath.endsWith(path.sep)) (stats && stats.isFile()) || (!stats && path.extname(resolvedPath) !== '' && !resolvedPath.endsWith(path.sep))
// For file-like targets create the parent directory; otherwise ensure the directory itself.
const directoryToEnsure = looksLikeFile ? path.dirname(resolvedPath) : resolvedPath const directoryToEnsure = looksLikeFile ? path.dirname(resolvedPath) : resolvedPath
if (!fs.existsSync(directoryToEnsure)) { if (!fs.existsSync(directoryToEnsure)) {
@ -203,7 +220,15 @@ export abstract class BaseService {
throw error throw error
} }
} }
// Preserve the first occurrence only to avoid duplicates while keeping caller order stable.
if (!seenPaths.has(resolvedPath)) {
seenPaths.add(resolvedPath)
sanitizedPaths.push(resolvedPath)
}
} }
return sanitizedPaths
} }
/** /**

View File

@ -45,14 +45,16 @@ export class AgentService extends BaseService {
req.accessible_paths = [defaultPath] req.accessible_paths = [defaultPath]
} }
if (req.accessible_paths !== undefined) {
req.accessible_paths = this.ensurePathsExist(req.accessible_paths)
}
await this.validateAgentModels(req.type, { await this.validateAgentModels(req.type, {
model: req.model, model: req.model,
plan_model: req.plan_model, plan_model: req.plan_model,
small_model: req.small_model small_model: req.small_model
}) })
this.ensurePathsExist(req.accessible_paths)
const serializedReq = this.serializeJsonFields(req) const serializedReq = this.serializeJsonFields(req)
const insertData: InsertAgentRow = { const insertData: InsertAgentRow = {
@ -137,8 +139,8 @@ export class AgentService extends BaseService {
const now = new Date().toISOString() const now = new Date().toISOString()
if (updates.accessible_paths) { if (updates.accessible_paths !== undefined) {
this.ensurePathsExist(updates.accessible_paths) updates.accessible_paths = this.ensurePathsExist(updates.accessible_paths)
} }
const modelUpdates: Partial<Record<AgentModelField, string | undefined>> = {} const modelUpdates: Partial<Record<AgentModelField, string | undefined>> = {}

View File

@ -58,7 +58,9 @@ export class SessionService extends BaseService {
small_model: sessionData.small_model small_model: sessionData.small_model
}) })
this.ensurePathsExist(sessionData.accessible_paths) if (sessionData.accessible_paths !== undefined) {
sessionData.accessible_paths = this.ensurePathsExist(sessionData.accessible_paths)
}
const serializedData = this.serializeJsonFields(sessionData) const serializedData = this.serializeJsonFields(sessionData)
@ -179,8 +181,8 @@ export class SessionService extends BaseService {
const now = new Date().toISOString() const now = new Date().toISOString()
if (updates.accessible_paths) { if (updates.accessible_paths !== undefined) {
this.ensurePathsExist(updates.accessible_paths) updates.accessible_paths = this.ensurePathsExist(updates.accessible_paths)
} }
const modelUpdates: Partial<Record<AgentModelField, string | undefined>> = {} const modelUpdates: Partial<Record<AgentModelField, string | undefined>> = {}

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_1758252305914_9kef8yven @session_id=session_1758278828236_mqj91e7c0
### List Sessions ### List Sessions
GET {{host}}/v1/agents/{{agent_id}}/sessions GET {{host}}/v1/agents/{{agent_id}}/sessions
@ -16,11 +16,11 @@ Authorization: Bearer {{token}}
Content-Type: application/json Content-Type: application/json
{ {
"name": "Story Writing Session 1", "name": "Story Writing Session 2",
"instructions": "You are a creative writing assistant. Help me brainstorm and write engaging stories.", "instructions": "You are a creative writing assistant. Help me brainstorm and write engaging stories.",
"model": "anthropic:claude-sonnet-4", "model": "anthropic:claude-sonnet-4",
"accessible_paths": [ "accessible_paths": [
"~/Documents/stories" "/tmp/Documents/stories"
] ]
} }
@ -44,7 +44,7 @@ Content-Type: application/json
"instructions": "You are a creative writing assistant. Help me brainstorm and write engaging stories.", "instructions": "You are a creative writing assistant. Help me brainstorm and write engaging stories.",
"model": "anthropic:claude-sonnet-4", "model": "anthropic:claude-sonnet-4",
"accessible_paths": [ "accessible_paths": [
"~/Documents/stories" "/tmp/Documents/stories"
] ]
} }
@ -55,7 +55,10 @@ Authorization: Bearer {{token}}
Content-Type: application/json 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.", "model": "anthropic:claude-sonnet-4-20250514",
"accessible_paths": [
"/tmp/Documents/stories2"
]
} }