mirror of
https://github.com/danny-avila/LibreChat.git
synced 2026-10-22 01:53:33 +00:00
🏛️ feat: Establish Agent Management API Contract (#15545)
Some checks are pending
Backend Unit Tests / Build packages (push) Waiting to run
Backend Unit Tests / Codegraph select (push) Waiting to run
Backend Unit Tests / TypeScript type checks (push) Blocked by required conditions
Backend Unit Tests / Circular dependency checks (push) Waiting to run
Backend Unit Tests / Tests: api (shard 1/3) (push) Blocked by required conditions
Backend Unit Tests / Tests: api (shard 2/3) (push) Blocked by required conditions
Backend Unit Tests / Tests: api (shard 3/3) (push) Blocked by required conditions
Backend Unit Tests / Tests: data-provider (push) Blocked by required conditions
Backend Unit Tests / Tests: data-schemas (push) Blocked by required conditions
Backend Unit Tests / Tests: @librechat/api (shard 1/4) (push) Blocked by required conditions
Backend Unit Tests / Tests: @librechat/api (shard 2/4) (push) Blocked by required conditions
Backend Unit Tests / Tests: @librechat/api (shard 3/4) (push) Blocked by required conditions
Backend Unit Tests / Tests: @librechat/api (shard 4/4) (push) Blocked by required conditions
Codegraph E2E Votes / vote (full suite) (push) Waiting to run
Docker Dev Branch Images Build / publish (push) Waiting to run
Frontend Unit Tests / Codegraph select (push) Waiting to run
Frontend Unit Tests / Build packages (push) Waiting to run
Frontend Unit Tests / TypeScript type checks (client) (push) Blocked by required conditions
Frontend Unit Tests / Tests: @librechat/client (push) Blocked by required conditions
Frontend Unit Tests / Tests: Ubuntu (shard 1/2) (push) Blocked by required conditions
Frontend Unit Tests / Tests: Ubuntu (shard 2/2) (push) Blocked by required conditions
Frontend Unit Tests / Vite build verification (push) Blocked by required conditions
Some checks are pending
Backend Unit Tests / Build packages (push) Waiting to run
Backend Unit Tests / Codegraph select (push) Waiting to run
Backend Unit Tests / TypeScript type checks (push) Blocked by required conditions
Backend Unit Tests / Circular dependency checks (push) Waiting to run
Backend Unit Tests / Tests: api (shard 1/3) (push) Blocked by required conditions
Backend Unit Tests / Tests: api (shard 2/3) (push) Blocked by required conditions
Backend Unit Tests / Tests: api (shard 3/3) (push) Blocked by required conditions
Backend Unit Tests / Tests: data-provider (push) Blocked by required conditions
Backend Unit Tests / Tests: data-schemas (push) Blocked by required conditions
Backend Unit Tests / Tests: @librechat/api (shard 1/4) (push) Blocked by required conditions
Backend Unit Tests / Tests: @librechat/api (shard 2/4) (push) Blocked by required conditions
Backend Unit Tests / Tests: @librechat/api (shard 3/4) (push) Blocked by required conditions
Backend Unit Tests / Tests: @librechat/api (shard 4/4) (push) Blocked by required conditions
Codegraph E2E Votes / vote (full suite) (push) Waiting to run
Docker Dev Branch Images Build / publish (push) Waiting to run
Frontend Unit Tests / Codegraph select (push) Waiting to run
Frontend Unit Tests / Build packages (push) Waiting to run
Frontend Unit Tests / TypeScript type checks (client) (push) Blocked by required conditions
Frontend Unit Tests / Tests: @librechat/client (push) Blocked by required conditions
Frontend Unit Tests / Tests: Ubuntu (shard 1/2) (push) Blocked by required conditions
Frontend Unit Tests / Tests: Ubuntu (shard 2/2) (push) Blocked by required conditions
Frontend Unit Tests / Vite build verification (push) Blocked by required conditions
* feat: add agent management API contract * fix: annotate agent management exports * fix: strengthen agent projection typing * fix: decouple agent responses from admission limits
This commit is contained in:
parent
f0dfeed2b0
commit
04e4fd6b68
3 changed files with 519 additions and 0 deletions
|
|
@ -27,6 +27,7 @@ export * from './legacy';
|
|||
export * from './lazySubagents';
|
||||
export * from './lazyHistory';
|
||||
export * from './memory';
|
||||
export * from './management';
|
||||
export * from './mcpIdentity';
|
||||
export * from './orphans';
|
||||
export * from './migration';
|
||||
|
|
|
|||
218
packages/api/src/agents/management.spec.ts
Normal file
218
packages/api/src/agents/management.spec.ts
Normal file
|
|
@ -0,0 +1,218 @@
|
|||
import { z } from 'zod';
|
||||
import { MAX_SUBAGENTS, setMaxSubagents } from 'librechat-data-provider';
|
||||
import {
|
||||
agentManagementCreateSchema,
|
||||
agentManagementListResponseSchema,
|
||||
agentManagementListSchema,
|
||||
agentManagementResponseSchema,
|
||||
agentManagementUpdateSchema,
|
||||
mapAgentManagementError,
|
||||
projectAgentManagementListResponse,
|
||||
projectAgentManagementResponse,
|
||||
} from './management';
|
||||
|
||||
const timestamps = {
|
||||
createdAt: new Date('2026-09-01T10:00:00.000Z'),
|
||||
updatedAt: new Date('2026-09-02T10:00:00.000Z'),
|
||||
};
|
||||
|
||||
const persistedAgent = {
|
||||
_id: '64da00000000000000000001',
|
||||
id: 'agent_public_id',
|
||||
tenantId: 'tenant-secret',
|
||||
author: 'user-secret',
|
||||
credentials: { apiKey: 'secret' },
|
||||
versions: [{}, {}],
|
||||
mcpServerNames: ['private-routing-name'],
|
||||
is_promoted: true,
|
||||
name: 'Researcher',
|
||||
description: 'Finds primary sources',
|
||||
instructions: 'Be precise',
|
||||
provider: 'openAI',
|
||||
model: 'gpt-5',
|
||||
model_parameters: { temperature: 0.2 },
|
||||
tools: ['web_search'],
|
||||
skills: ['64da00000000000000000002'],
|
||||
conversation_starters: ['Research this'],
|
||||
...timestamps,
|
||||
};
|
||||
|
||||
describe('Agent Management contract', () => {
|
||||
describe('inputs', () => {
|
||||
it('keeps create and update fields aligned with the browser Agent validators', () => {
|
||||
expect(
|
||||
agentManagementCreateSchema.parse({
|
||||
provider: 'openAI',
|
||||
model: 'gpt-5',
|
||||
name: 'Researcher',
|
||||
stateful_code_environment: 'agent-user',
|
||||
subagents: { enabled: true, allowSelf: true, agent_ids: [] },
|
||||
}),
|
||||
).toMatchObject({ provider: 'openAI', model: 'gpt-5', tools: [] });
|
||||
|
||||
expect(
|
||||
agentManagementUpdateSchema.parse({
|
||||
instructions: 'Updated',
|
||||
model_parameters: { temperature: 0.1 },
|
||||
code_environment_id: null,
|
||||
}),
|
||||
).toEqual({
|
||||
instructions: 'Updated',
|
||||
model_parameters: { temperature: 0.1 },
|
||||
code_environment_id: null,
|
||||
});
|
||||
});
|
||||
|
||||
it.each([agentManagementCreateSchema, agentManagementUpdateSchema])(
|
||||
'rejects unknown and read-only fields',
|
||||
(schema) => {
|
||||
const base =
|
||||
schema === agentManagementCreateSchema ? { provider: 'openAI', model: 'gpt-5' } : {};
|
||||
expect(schema.safeParse({ ...base, _id: '64da00000000000000000001' }).success).toBe(false);
|
||||
expect(schema.safeParse({ ...base, id: 'agent_forged' }).success).toBe(false);
|
||||
expect(schema.safeParse({ ...base, tenantId: 'tenant_forged' }).success).toBe(false);
|
||||
expect(schema.safeParse({ ...base, credentials: { apiKey: 'secret' } }).success).toBe(
|
||||
false,
|
||||
);
|
||||
expect(schema.safeParse({ ...base, versions: [] }).success).toBe(false);
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
describe('pagination', () => {
|
||||
const cursor = Buffer.from(
|
||||
JSON.stringify({
|
||||
updatedAt: '2026-09-02T10:00:00.000Z',
|
||||
_id: '64da00000000000000000001',
|
||||
}),
|
||||
).toString('base64');
|
||||
|
||||
it('normalizes a bounded limit and accepts the opaque database cursor', () => {
|
||||
expect(agentManagementListSchema.parse({ limit: '25', cursor })).toEqual({
|
||||
limit: 25,
|
||||
cursor,
|
||||
});
|
||||
expect(agentManagementListSchema.parse({})).toEqual({ limit: 20 });
|
||||
});
|
||||
|
||||
it.each([
|
||||
{ limit: 0 },
|
||||
{ limit: 101 },
|
||||
{ limit: 1.5 },
|
||||
{ cursor: 'not-a-cursor' },
|
||||
{
|
||||
cursor: Buffer.from(JSON.stringify({ updatedAt: 'nope', _id: 'nope' })).toString('base64'),
|
||||
},
|
||||
{ search: 'unsupported' },
|
||||
])('rejects invalid list input %#', (input) => {
|
||||
expect(agentManagementListSchema.safeParse(input).success).toBe(false);
|
||||
});
|
||||
|
||||
it('projects every list item and emits only a usable next cursor', () => {
|
||||
const response = projectAgentManagementListResponse({
|
||||
data: [persistedAgent],
|
||||
has_more: true,
|
||||
after: cursor,
|
||||
});
|
||||
|
||||
expect(agentManagementListResponseSchema.parse(response)).toEqual(response);
|
||||
expect(response).toMatchObject({
|
||||
object: 'list',
|
||||
first_id: 'agent_public_id',
|
||||
last_id: 'agent_public_id',
|
||||
has_more: true,
|
||||
after: cursor,
|
||||
});
|
||||
expect(
|
||||
projectAgentManagementListResponse({ data: [], has_more: false, after: cursor }),
|
||||
).toEqual({
|
||||
object: 'list',
|
||||
data: [],
|
||||
first_id: null,
|
||||
last_id: null,
|
||||
has_more: false,
|
||||
after: null,
|
||||
});
|
||||
|
||||
expect(
|
||||
agentManagementListResponseSchema.safeParse({
|
||||
...response,
|
||||
has_more: true,
|
||||
after: null,
|
||||
}).success,
|
||||
).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('responses', () => {
|
||||
it('allowlists supported configuration and stable metadata', () => {
|
||||
const response = projectAgentManagementResponse(persistedAgent);
|
||||
|
||||
expect(agentManagementResponseSchema.parse(response)).toEqual(response);
|
||||
expect(response).toMatchObject({
|
||||
id: 'agent_public_id',
|
||||
version: 2,
|
||||
createdAt: '2026-09-01T10:00:00.000Z',
|
||||
updatedAt: '2026-09-02T10:00:00.000Z',
|
||||
name: 'Researcher',
|
||||
provider: 'openAI',
|
||||
model: 'gpt-5',
|
||||
});
|
||||
expect(response).not.toHaveProperty('_id');
|
||||
expect(response).not.toHaveProperty('tenantId');
|
||||
expect(response).not.toHaveProperty('author');
|
||||
expect(response).not.toHaveProperty('credentials');
|
||||
expect(response).not.toHaveProperty('versions');
|
||||
expect(response).not.toHaveProperty('mcpServerNames');
|
||||
expect(response).not.toHaveProperty('is_promoted');
|
||||
});
|
||||
|
||||
it('does not apply the current request admission limit to persisted subagents', () => {
|
||||
const subagents = {
|
||||
enabled: true,
|
||||
agent_ids: Array.from({ length: MAX_SUBAGENTS }, (_, index) => `agent_${index}`),
|
||||
};
|
||||
|
||||
setMaxSubagents(1);
|
||||
try {
|
||||
expect(agentManagementUpdateSchema.safeParse({ subagents }).success).toBe(false);
|
||||
expect(projectAgentManagementResponse({ ...persistedAgent, subagents }).subagents).toEqual(
|
||||
subagents,
|
||||
);
|
||||
} finally {
|
||||
setMaxSubagents(undefined);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
describe('errors', () => {
|
||||
it.each([
|
||||
['not_found', 404, 'Agent not found'],
|
||||
['permission_denied', 403, 'Permission denied'],
|
||||
['internal_error', 500, 'Internal server error'],
|
||||
] as const)('maps %s without leaking an internal error', (code, status, message) => {
|
||||
expect(mapAgentManagementError(code, new Error('database password leaked'))).toEqual({
|
||||
status,
|
||||
body: { error: { code, message } },
|
||||
});
|
||||
});
|
||||
|
||||
it('maps validation issues to safe paths and messages', () => {
|
||||
const validation = z.object({ limit: z.number().max(100) }).safeParse({ limit: 101 });
|
||||
if (validation.success) {
|
||||
throw new Error('Expected validation to fail');
|
||||
}
|
||||
|
||||
expect(mapAgentManagementError('invalid_request', validation.error)).toMatchObject({
|
||||
status: 400,
|
||||
body: {
|
||||
error: {
|
||||
code: 'invalid_request',
|
||||
message: 'Invalid request',
|
||||
details: [{ path: ['limit'] }],
|
||||
},
|
||||
},
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
300
packages/api/src/agents/management.ts
Normal file
300
packages/api/src/agents/management.ts
Normal file
|
|
@ -0,0 +1,300 @@
|
|||
import { z } from 'zod';
|
||||
import type {
|
||||
AgentSubagentGraph,
|
||||
AgentSubagentGraphEdge,
|
||||
AgentSubagentsConfig,
|
||||
} from 'librechat-data-provider';
|
||||
import { agentCreateSchema, agentUpdateSchema } from './validation';
|
||||
|
||||
const MAX_LIST_LIMIT = 100;
|
||||
const DEFAULT_LIST_LIMIT = 20;
|
||||
const MAX_CURSOR_LENGTH = 512;
|
||||
|
||||
export type AgentManagementCreate = z.output<typeof agentCreateSchema>;
|
||||
export type AgentManagementUpdate = z.output<typeof agentUpdateSchema>;
|
||||
export type AgentManagementList = {
|
||||
limit: number;
|
||||
cursor?: string;
|
||||
};
|
||||
export type AgentManagementResponse = Omit<
|
||||
z.output<typeof agentUpdateSchema>,
|
||||
'provider' | 'model'
|
||||
> & {
|
||||
id: string;
|
||||
provider: string;
|
||||
model: string | null;
|
||||
version: number;
|
||||
createdAt: string;
|
||||
updatedAt: string;
|
||||
};
|
||||
export type AgentManagementProjectionSource = Partial<
|
||||
Omit<AgentManagementResponse, 'id' | 'provider' | 'model' | 'version' | 'createdAt' | 'updatedAt'>
|
||||
> & {
|
||||
id?: string;
|
||||
provider?: string;
|
||||
model?: string | null;
|
||||
version?: number;
|
||||
versions?: readonly object[];
|
||||
createdAt?: string | Date;
|
||||
updatedAt?: string | Date;
|
||||
};
|
||||
export type AgentManagementListProjectionSource = {
|
||||
data?: AgentManagementProjectionSource[];
|
||||
has_more?: boolean;
|
||||
after?: string | null;
|
||||
};
|
||||
export type AgentManagementListResponse = {
|
||||
object: 'list';
|
||||
data: AgentManagementResponse[];
|
||||
first_id: string | null;
|
||||
last_id: string | null;
|
||||
has_more: boolean;
|
||||
after: string | null;
|
||||
};
|
||||
export type AgentManagementErrorCode =
|
||||
| 'invalid_request'
|
||||
| 'not_found'
|
||||
| 'permission_denied'
|
||||
| 'internal_error';
|
||||
export type AgentManagementError = {
|
||||
error: {
|
||||
code: AgentManagementErrorCode;
|
||||
message: string;
|
||||
details?: Array<{ path: Array<string | number>; message: string }>;
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* Agent Management accepts the browser-supported configuration fields, but unlike the
|
||||
* browser endpoints it rejects unknown top-level fields instead of silently stripping them.
|
||||
*/
|
||||
export const agentManagementCreateSchema: z.ZodType<
|
||||
AgentManagementCreate,
|
||||
z.ZodTypeDef,
|
||||
z.input<typeof agentCreateSchema>
|
||||
> = agentCreateSchema.strict();
|
||||
export const agentManagementUpdateSchema: z.ZodType<AgentManagementUpdate> =
|
||||
agentUpdateSchema.strict();
|
||||
|
||||
const agentManagementCursorSchema = z
|
||||
.string()
|
||||
.min(1)
|
||||
.max(MAX_CURSOR_LENGTH)
|
||||
.superRefine((cursor, context) => {
|
||||
try {
|
||||
const decoded = JSON.parse(Buffer.from(cursor, 'base64').toString('utf8')) as unknown;
|
||||
const result = z
|
||||
.object({
|
||||
updatedAt: z.string().datetime(),
|
||||
_id: z.string().regex(/^[a-f\d]{24}$/i),
|
||||
})
|
||||
.strict()
|
||||
.safeParse(decoded);
|
||||
|
||||
if (!result.success) {
|
||||
context.addIssue({ code: z.ZodIssueCode.custom, message: 'Invalid cursor' });
|
||||
}
|
||||
} catch {
|
||||
context.addIssue({ code: z.ZodIssueCode.custom, message: 'Invalid cursor' });
|
||||
}
|
||||
});
|
||||
|
||||
export const agentManagementListSchema: z.ZodType<AgentManagementList, z.ZodTypeDef, unknown> = z
|
||||
.object({
|
||||
limit: z.coerce.number().int().min(1).max(MAX_LIST_LIMIT).default(DEFAULT_LIST_LIMIT),
|
||||
cursor: agentManagementCursorSchema.optional(),
|
||||
})
|
||||
.strict();
|
||||
|
||||
const timestampSchema = z.string().datetime();
|
||||
const agentManagementGraphEdgeResponseSchema: z.ZodType<AgentSubagentGraphEdge> = z
|
||||
.object({
|
||||
from: z.union([z.string(), z.array(z.string())]),
|
||||
to: z.union([z.string(), z.array(z.string())]),
|
||||
description: z.string().optional(),
|
||||
edgeType: z.literal('direct'),
|
||||
prompt: z.string().optional(),
|
||||
excludeResults: z.boolean().optional(),
|
||||
})
|
||||
.strict();
|
||||
const agentManagementGraphResponseSchema: z.ZodType<AgentSubagentGraph> = z
|
||||
.object({
|
||||
type: z.string(),
|
||||
name: z.string(),
|
||||
description: z.string(),
|
||||
agent_ids: z.array(z.string()),
|
||||
edges: z.array(agentManagementGraphEdgeResponseSchema),
|
||||
entry_agent_id: z.string(),
|
||||
result_agent_id: z.string(),
|
||||
})
|
||||
.strict();
|
||||
const agentManagementSubagentsResponseSchema: z.ZodType<AgentSubagentsConfig | undefined> = z
|
||||
.object({
|
||||
enabled: z.boolean().optional(),
|
||||
allowSelf: z.boolean().optional(),
|
||||
agent_ids: z.array(z.string()).optional(),
|
||||
graphs: z.array(agentManagementGraphResponseSchema).optional(),
|
||||
})
|
||||
.strict()
|
||||
.optional();
|
||||
|
||||
/** The externally supported Agent shape. Persistence and ownership fields are intentionally absent. */
|
||||
export const agentManagementResponseSchema: z.ZodType<AgentManagementResponse> = agentUpdateSchema
|
||||
.extend({
|
||||
id: z.string().min(1),
|
||||
provider: z.string(),
|
||||
model: z.string().nullable(),
|
||||
version: z.number().int().nonnegative(),
|
||||
createdAt: timestampSchema,
|
||||
updatedAt: timestampSchema,
|
||||
subagents: agentManagementSubagentsResponseSchema,
|
||||
})
|
||||
.strict();
|
||||
|
||||
export const agentManagementListResponseSchema: z.ZodType<AgentManagementListResponse> = z
|
||||
.object({
|
||||
object: z.literal('list'),
|
||||
data: z.array(agentManagementResponseSchema),
|
||||
first_id: z.string().nullable(),
|
||||
last_id: z.string().nullable(),
|
||||
has_more: z.boolean(),
|
||||
after: agentManagementCursorSchema.nullable(),
|
||||
})
|
||||
.strict()
|
||||
.superRefine(({ has_more, after }, context) => {
|
||||
if (has_more !== (after != null)) {
|
||||
context.addIssue({
|
||||
code: z.ZodIssueCode.custom,
|
||||
path: ['after'],
|
||||
message: 'A next cursor is required exactly when more results are available',
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
export const agentManagementErrorCodeSchema: z.ZodType<AgentManagementErrorCode> = z.enum([
|
||||
'invalid_request',
|
||||
'not_found',
|
||||
'permission_denied',
|
||||
'internal_error',
|
||||
]);
|
||||
|
||||
const agentManagementValidationIssueSchema = z
|
||||
.object({
|
||||
path: z.array(z.union([z.string(), z.number()])),
|
||||
message: z.string(),
|
||||
})
|
||||
.strict();
|
||||
|
||||
export const agentManagementErrorSchema: z.ZodType<AgentManagementError> = z
|
||||
.object({
|
||||
error: z
|
||||
.object({
|
||||
code: agentManagementErrorCodeSchema,
|
||||
message: z.string(),
|
||||
details: z.array(agentManagementValidationIssueSchema).optional(),
|
||||
})
|
||||
.strict(),
|
||||
})
|
||||
.strict();
|
||||
|
||||
function toTimestamp(value: string | Date | undefined): string | undefined {
|
||||
if (value instanceof Date) {
|
||||
return value.toISOString();
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
function getVersion(source: AgentManagementProjectionSource): number | undefined {
|
||||
if (source.version != null) {
|
||||
return source.version;
|
||||
}
|
||||
return source.versions?.length;
|
||||
}
|
||||
|
||||
/** Build an external response from a persistence result using an explicit allowlist. */
|
||||
export function projectAgentManagementResponse(
|
||||
source: AgentManagementProjectionSource,
|
||||
): AgentManagementResponse {
|
||||
return agentManagementResponseSchema.parse({
|
||||
id: source.id,
|
||||
provider: source.provider,
|
||||
model: source.model,
|
||||
version: getVersion(source),
|
||||
createdAt: toTimestamp(source.createdAt),
|
||||
updatedAt: toTimestamp(source.updatedAt),
|
||||
name: source.name,
|
||||
description: source.description,
|
||||
instructions: source.instructions,
|
||||
avatar: source.avatar,
|
||||
model_parameters: source.model_parameters,
|
||||
tools: source.tools,
|
||||
skills: source.skills,
|
||||
skills_enabled: source.skills_enabled,
|
||||
skill_authoring_enabled: source.skill_authoring_enabled,
|
||||
skills_scope: source.skills_scope,
|
||||
memory_scope: source.memory_scope,
|
||||
agent_ids: source.agent_ids,
|
||||
edges: source.edges,
|
||||
end_after_tools: source.end_after_tools,
|
||||
hide_sequential_outputs: source.hide_sequential_outputs,
|
||||
stateful_code_sessions: source.stateful_code_sessions,
|
||||
stateful_code_environment: source.stateful_code_environment,
|
||||
code_environment_id: source.code_environment_id,
|
||||
artifacts: source.artifacts,
|
||||
recursion_limit: source.recursion_limit,
|
||||
conversation_starters: source.conversation_starters,
|
||||
tool_resources: source.tool_resources,
|
||||
tool_options: source.tool_options,
|
||||
subagents: source.subagents,
|
||||
support_contact: source.support_contact,
|
||||
category: source.category,
|
||||
});
|
||||
}
|
||||
|
||||
export function projectAgentManagementListResponse(
|
||||
source: AgentManagementListProjectionSource,
|
||||
): AgentManagementListResponse {
|
||||
const data = (source.data ?? []).map(projectAgentManagementResponse);
|
||||
return agentManagementListResponseSchema.parse({
|
||||
object: 'list',
|
||||
data,
|
||||
first_id: data[0]?.id ?? null,
|
||||
last_id: data[data.length - 1]?.id ?? null,
|
||||
has_more: source.has_more,
|
||||
after: source.has_more === true && typeof source.after === 'string' ? source.after : null,
|
||||
});
|
||||
}
|
||||
|
||||
const ERROR_STATUS: Record<AgentManagementErrorCode, number> = {
|
||||
invalid_request: 400,
|
||||
not_found: 404,
|
||||
permission_denied: 403,
|
||||
internal_error: 500,
|
||||
};
|
||||
|
||||
const ERROR_MESSAGE: Record<AgentManagementErrorCode, string> = {
|
||||
invalid_request: 'Invalid request',
|
||||
not_found: 'Agent not found',
|
||||
permission_denied: 'Permission denied',
|
||||
internal_error: 'Internal server error',
|
||||
};
|
||||
|
||||
/** Map known failure classes to a stable envelope without exposing internal error messages. */
|
||||
export function mapAgentManagementError(
|
||||
code: AgentManagementErrorCode,
|
||||
error?: unknown,
|
||||
): { status: number; body: AgentManagementError } {
|
||||
const details =
|
||||
code === 'invalid_request' && error instanceof z.ZodError
|
||||
? error.issues.map(({ path, message }) => ({ path, message }))
|
||||
: undefined;
|
||||
const body = agentManagementErrorSchema.parse({
|
||||
error: {
|
||||
code,
|
||||
message: ERROR_MESSAGE[code],
|
||||
...(details != null ? { details } : {}),
|
||||
},
|
||||
});
|
||||
|
||||
return { status: ERROR_STATUS[code], body };
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue