🦥 perf: Lazy-Load Agent Version History in Editor (#13977)

Opening the agent editor fetched the full `versions` array (each a complete
config snapshot) alongside the agent, so agents with large histories were slow
to open. Version history is now loaded only when the user opens it.

- Add `getAgentWithVersionCount` (aggregation: version count, no versions array)
  and `getAgentVersions` data-schemas methods.
- `getAgentHandler` returns the version count without the heavy array; add
  `GET /agents/:id/versions` (EDIT-gated) for lazy retrieval.
- Add `useGetAgentVersionsQuery`; VersionPanel reads current config from the
  cached expanded query and fetches versions on open. Revert keeps the expanded
  cache and versions query in sync.
This commit is contained in:
Danny Avila 2026-06-26 12:19:54 -04:00 • committed by GitHub
parent b15d40e3e4
commit 12fea693bb
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
10 changed files with 301 additions and 45 deletions

View file

@ -487,16 +487,15 @@ const getAgentHandler = async (req, res, expandProperties = false) => {
const id = req.params.id;
const author = req.user.id;
// Permissions are validated by middleware before calling this function
// Simply load the agent by ID
const agent = await db.getAgent({ id });
// Permissions are validated by middleware before calling this function.
// Load the agent with a `version` count but without the heavy `versions`
// array; version history is fetched lazily via GET /agents/:id/versions.
const agent = await db.getAgentWithVersionCount({ id });
if (!agent) {
return res.status(404).json({ error: 'Agent not found' });
}
agent.version = agent.versions ? agent.versions.length : 0;
if (agent.avatar && agent.avatar?.source === FileSources.s3) {
try {
agent.avatar = {
@ -561,6 +560,32 @@ const getAgentHandler = async (req, res, expandProperties = false) => {
}
};
/**
* Retrieves an agent's version history.
* Loaded lazily so the editor doesn't transfer large histories up front.
* @route GET /agents/:id/versions
* @param {object} req - Express Request
* @param {object} req.params - Request params
* @param {string} req.params.id - Agent identifier.
* @returns {Promise<Agent[]>} 200 - The agent's version history - application/json
* @returns {Error} 404 - Agent not found
*/
const getAgentVersionsHandler = async (req, res) => {
try {
const id = req.params.id;
const versions = await db.getAgentVersions({ id });
if (versions == null) {
return res.status(404).json({ error: 'Agent not found' });
}
return res.status(200).json(versions);
} catch (error) {
logger.error('[/Agents/:id/versions] Error retrieving agent versions', error);
res.status(500).json({ error: error.message });
}
};
/**
* Updates an Agent.
* @route PATCH /Agents/:id
@ -1330,6 +1355,7 @@ const getAgentCategories = async (_req, res) => {
module.exports = {
createAgent: createAgentHandler,
getAgent: getAgentHandler,
getAgentVersions: getAgentVersionsHandler,
updateAgent: updateAgentHandler,
duplicateAgent: duplicateAgentHandler,
deleteAgent: deleteAgentHandler,

View file

@ -79,6 +79,7 @@ jest.mock('~/cache', () => ({
const {
createAgent: createAgentHandler,
getAgent: getAgentHandler,
getAgentVersions: getAgentVersionsHandler,
duplicateAgent: duplicateAgentHandler,
revertAgentVersion: revertAgentVersionHandler,
updateAgent: updateAgentHandler,
@ -603,6 +604,45 @@ describe('Agent Controllers - Mass Assignment Protection', () => {
});
});
describe('getAgentVersionsHandler', () => {
test('returns the version history and excludes it from the basic VIEW response', async () => {
const agent = await Agent.create({
id: `agent_${uuidv4()}`,
name: 'Versioned Agent',
provider: 'openai',
model: 'gpt-4',
author: mockReq.user.id,
versions: [
{ name: 'V1', provider: 'openai', model: 'gpt-4', updatedAt: new Date() },
{ name: 'V2', provider: 'openai', model: 'gpt-4', updatedAt: new Date() },
],
});
mockReq.params = { id: agent.id };
await getAgentHandler(mockReq, mockRes);
const basicResponse = mockRes.json.mock.calls[0][0];
expect(basicResponse.versions).toBeUndefined();
expect(basicResponse.version).toBe(2);
mockRes.json.mockClear();
await getAgentVersionsHandler(mockReq, mockRes);
expect(mockRes.status).toHaveBeenCalledWith(200);
const versions = mockRes.json.mock.calls[0][0];
expect(Array.isArray(versions)).toBe(true);
expect(versions).toHaveLength(2);
expect(versions.map((v) => v.name)).toEqual(['V1', 'V2']);
});
test('returns 404 when the agent does not exist', async () => {
mockReq.params = { id: `agent_${uuidv4()}` };
await getAgentVersionsHandler(mockReq, mockRes);
expect(mockRes.status).toHaveBeenCalledWith(404);
});
});
describe('updateAgentHandler', () => {
let existingAgentId;
let existingAgentAuthorId;

View file

@ -81,6 +81,23 @@ router.get(
}),
(req, res) => v1.getAgent(req, res, true), // Expanded version
);
/**
* Retrieves an agent's version history (EDIT permission required).
* Loaded lazily so the editor doesn't transfer large histories up front.
* @route GET /agents/:id/versions
* @param {string} req.params.id - Agent identifier.
* @returns {Agent[]} 200 - Agent version history - application/json
*/
router.get(
'/:id/versions',
checkAgentAccess,
canAccessAgentResource({
requiredPermission: PermissionBits.EDIT,
resourceIdParam: 'id',
}),
v1.getAgentVersions,
);
/**
* Updates an agent.
* @route PATCH /agents/:id