From 7e2f759e3417121b5dfc941a69f7af7b3349c626 Mon Sep 17 00:00:00 2001 From: Marco Beretta Date: Fri, 18 Sep 2026 08:15:29 +0000 Subject: [PATCH] perf: exclude agent versions history from default getAgent reads getAgent now defaults its projection to { versions: 0 }; loadAgent/loadAddedAgent and canAccessAgentFromBody use getAgentWithVersionCount so the version count stays exact without transferring the unbounded versions array on every chat request. Callers that need history (v1 update/revert handlers) request it explicitly. Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> --- api/server/controllers/agents/client.js | 2 +- api/server/controllers/agents/v1.js | 6 +-- .../accessResources/canAccessAgentFromBody.js | 4 +- .../services/Endpoints/agents/addedConvo.js | 2 +- api/server/services/Endpoints/agents/build.js | 2 +- .../api/src/agents/__tests__/load.spec.ts | 7 +-- packages/api/src/agents/added.ts | 10 +++-- packages/api/src/agents/load.ts | 9 ++-- .../data-schemas/src/methods/agent.spec.ts | 44 +++++++++---------- packages/data-schemas/src/methods/agent.ts | 4 +- 10 files changed, 48 insertions(+), 42 deletions(-) diff --git a/api/server/controllers/agents/client.js b/api/server/controllers/agents/client.js index d11da051856..bdde6731c7c 100644 --- a/api/server/controllers/agents/client.js +++ b/api/server/controllers/agents/client.js @@ -226,7 +226,7 @@ const db = require('~/models'); const loadAgent = (params) => loadAgentFn(params, { - getAgent: db.getAgent, + getAgent: db.getAgentWithVersionCount, getMCPServerTools, getAccessibleMCPServers, }); diff --git a/api/server/controllers/agents/v1.js b/api/server/controllers/agents/v1.js index e34647cf2b7..e975d867461 100644 --- a/api/server/controllers/agents/v1.js +++ b/api/server/controllers/agents/v1.js @@ -1108,7 +1108,7 @@ const updateAgentHandler = async (req, res) => { includesToolsConfiguration || includesToolOptionsConfiguration ) { - existingAgent = await db.getAgent({ id }); + existingAgent = await db.getAgent({ id }, {}); if (!existingAgent) { return res.status(404).json({ error: 'Agent not found' }); } @@ -1237,7 +1237,7 @@ const updateAgentHandler = async (req, res) => { // Convert OCR to context in incoming updateData convertOcrToContextInPlace(updateData); - existingAgent ??= await db.getAgent({ id }); + existingAgent ??= await db.getAgent({ id }, {}); if (!existingAgent) { return res.status(404).json({ error: 'Agent not found' }); @@ -2066,7 +2066,7 @@ const revertAgentVersionHandler = async (req, res) => { return res.status(400).json({ error: 'version_index is required' }); } - const existingAgent = await db.getAgent({ id }); + const existingAgent = await db.getAgent({ id }, {}); if (!existingAgent) { return res.status(404).json({ error: 'Agent not found' }); diff --git a/api/server/middleware/accessResources/canAccessAgentFromBody.js b/api/server/middleware/accessResources/canAccessAgentFromBody.js index 913f6c35218..6a2b9ef914c 100644 --- a/api/server/middleware/accessResources/canAccessAgentFromBody.js +++ b/api/server/middleware/accessResources/canAccessAgentFromBody.js @@ -13,7 +13,7 @@ const { checkPermission } = require('~/server/services/PermissionService'); const { canAccessResource } = require('./canAccessResource'); const db = require('~/models'); -const { getRoleByName, getAgent } = db; +const { getRoleByName, getAgentWithVersionCount } = db; const resolveEnforcedAgentId = (req, endpoint) => { const modelSpecs = req.config?.modelSpecs; @@ -39,7 +39,7 @@ const resolveAgentIdFromBody = async (agentCustomId) => { if (isEphemeralAgentId(agentCustomId)) { return null; } - return getAgent({ id: agentCustomId }); + return getAgentWithVersionCount({ id: agentCustomId }); }; /** diff --git a/api/server/services/Endpoints/agents/addedConvo.js b/api/server/services/Endpoints/agents/addedConvo.js index 031e7d5823d..4fed5795688 100644 --- a/api/server/services/Endpoints/agents/addedConvo.js +++ b/api/server/services/Endpoints/agents/addedConvo.js @@ -17,7 +17,7 @@ const db = require('~/models'); const loadAddedAgent = (params) => loadAddedAgentFn(params, { - getAgent: db.getAgent, + getAgent: db.getAgentWithVersionCount, getMCPServerTools, getAccessibleMCPServers, }); diff --git a/api/server/services/Endpoints/agents/build.js b/api/server/services/Endpoints/agents/build.js index 4c9a40a2ab4..7c5ce99434c 100644 --- a/api/server/services/Endpoints/agents/build.js +++ b/api/server/services/Endpoints/agents/build.js @@ -7,7 +7,7 @@ const db = require('~/models'); const loadAgent = (params) => loadAgentFn(params, { - getAgent: db.getAgent, + getAgent: db.getAgentWithVersionCount, getMCPServerTools, getAccessibleMCPServers, }); diff --git a/packages/api/src/agents/__tests__/load.spec.ts b/packages/api/src/agents/__tests__/load.spec.ts index bb5559b87ae..18d199cedad 100644 --- a/packages/api/src/agents/__tests__/load.spec.ts +++ b/packages/api/src/agents/__tests__/load.spec.ts @@ -16,13 +16,14 @@ import { loadAgent } from '../load'; let Agent: mongoose.Model; let createAgent: ReturnType['createAgent']; -let getAgent: ReturnType['getAgent']; +let getAgentWithVersionCount: ReturnType['getAgentWithVersionCount']; const mockGetMCPServerTools = jest.fn(); const mockGetAccessibleMCPServers = jest.fn(); const deps: LoadAgentDeps = { - getAgent: (searchParameter) => getAgent(searchParameter) as Promise, + getAgent: (searchParameter) => + getAgentWithVersionCount(searchParameter) as Promise, getMCPServerTools: mockGetMCPServerTools, getAccessibleMCPServers: mockGetAccessibleMCPServers, }; @@ -37,7 +38,7 @@ describe('loadAgent', () => { await mongoose.connect(mongoUri); const methods = createMethods(mongoose); createAgent = methods.createAgent; - getAgent = methods.getAgent; + getAgentWithVersionCount = methods.getAgentWithVersionCount; }, 20000); afterAll(async () => { diff --git a/packages/api/src/agents/added.ts b/packages/api/src/agents/added.ts index b08fe163d31..f851d580b89 100644 --- a/packages/api/src/agents/added.ts +++ b/packages/api/src/agents/added.ts @@ -55,7 +55,10 @@ function applyModelSpecSubagents( } export interface LoadAddedAgentDeps { - getAgent: (searchParameter: { id: string }) => Promise; + /** Resolves the agent without its `versions` history; `version` carries the count. */ + getAgent: (searchParameter: { + id: string; + }) => Promise<(Agent & { version?: number; versions?: unknown[] }) | null>; getMCPServerTools: ( userId: string, serverName: string, @@ -99,9 +102,8 @@ export async function loadAddedAgent( return null; } - const agentRecord = agent as Record; - const versions = agentRecord.versions as unknown[] | undefined; - agentRecord.version = versions ? versions.length : 0; + const agentRecord = agent as Agent & { version?: number; versions?: unknown[] }; + agentRecord.version ??= agentRecord.versions?.length ?? 0; agent.id = appendAgentIdSuffix(agent.id, 1); return agent; } diff --git a/packages/api/src/agents/load.ts b/packages/api/src/agents/load.ts index 569a9caba8b..7c53dc6c1b3 100644 --- a/packages/api/src/agents/load.ts +++ b/packages/api/src/agents/load.ts @@ -31,7 +31,10 @@ const { mcp_all, mcp_delimiter } = Constants; type ModelParametersWithPromptPrefix = AgentModelParameters & { promptPrefix?: string | null }; export interface LoadAgentDeps { - getAgent: (searchParameter: { id: string }) => Promise; + /** Resolves the agent without its `versions` history; `version` carries the count. */ + getAgent: (searchParameter: { + id: string; + }) => Promise<(Agent & { version?: number; versions?: unknown[] }) | null>; getMCPServerTools: ( userId: string, serverName: string, @@ -242,8 +245,6 @@ export async function loadAgent( return null; } - // Set version count from versions array length - const agentWithVersion = agent as Agent & { versions?: unknown[]; version?: number }; - agentWithVersion.version = agentWithVersion.versions ? agentWithVersion.versions.length : 0; + agent.version ??= agent.versions?.length ?? 0; return agent; } diff --git a/packages/data-schemas/src/methods/agent.spec.ts b/packages/data-schemas/src/methods/agent.spec.ts index fc24723e3c3..aeb557e11da 100644 --- a/packages/data-schemas/src/methods/agent.spec.ts +++ b/packages/data-schemas/src/methods/agent.spec.ts @@ -2390,7 +2390,7 @@ describe('Agent Methods', () => { }, ); - const firstUpdate = await getAgent({ id: agentId }); + const firstUpdate = await getAgent({ id: agentId }, {}); expect(firstUpdate!.description).toBe('Updated description'); expect(firstUpdate!.tools).toContain('tool1'); expect(firstUpdate!.tools).toContain('tool2'); @@ -2403,7 +2403,7 @@ describe('Agent Methods', () => { }, ); - const secondUpdate = await getAgent({ id: agentId }); + const secondUpdate = await getAgent({ id: agentId }, {}); expect(secondUpdate!.tools).toHaveLength(2); expect(secondUpdate!.tools).toContain('tool2'); expect(secondUpdate!.tools).toContain('tool3'); @@ -2417,7 +2417,7 @@ describe('Agent Methods', () => { }, ); - const thirdUpdate = await getAgent({ id: agentId }); + const thirdUpdate = await getAgent({ id: agentId }, {}); const toolCount = thirdUpdate!.tools!.filter((t) => t === 'tool3').length; expect(toolCount).toBe(2); expect(thirdUpdate!.versions).toHaveLength(4); @@ -2487,14 +2487,14 @@ describe('Agent Methods', () => { }, ); - const complexAgent = await getAgent({ id: agentId }); + const complexAgent = await getAgent({ id: agentId }, {}); expect(complexAgent!.versions).toHaveLength(3); expect(complexAgent!.model_parameters?.temperature).toBe(0.8); expect(complexAgent!.model_parameters?.max_tokens).toBe(1000); await updateAgent({ id: agentId }, { model_parameters: {} }); - const emptyParamsAgent = await getAgent({ id: agentId }); + const emptyParamsAgent = await getAgent({ id: agentId }, {}); expect(emptyParamsAgent!.versions).toHaveLength(4); expect(emptyParamsAgent!.model_parameters).toEqual({}); }); @@ -2522,7 +2522,7 @@ describe('Agent Methods', () => { expect(duplicateUpdate!.versions).toHaveLength(2); // No new version created - const agent = await getAgent({ id: testAgentId }); + const agent = await getAgent({ id: testAgentId }, {}); expect(agent!.versions).toHaveLength(2); } }); @@ -2544,7 +2544,7 @@ describe('Agent Methods', () => { * same shape `addAgentResourceFile` produces when it attaches a file. */ await updateAgent({ id: agentId }, { $addToSet: { tools: 'file_search' } }); - const afterAdd = await getAgent({ id: agentId }); + const afterAdd = await getAgent({ id: agentId }, {}); expect(afterAdd!.tools).toEqual(['file_search']); expect(afterAdd!.versions).toHaveLength(2); expect((afterAdd!.versions![1] as VersionEntry).tools).toEqual([]); @@ -2556,7 +2556,7 @@ describe('Agent Methods', () => { expect(removed!.tools).toEqual([]); expect(removed!.versions).toHaveLength(2); - const reloaded = await getAgent({ id: agentId }); + const reloaded = await getAgent({ id: agentId }, {}); expect(reloaded!.tools).toEqual([]); expect(reloaded!.versions).toHaveLength(2); }); @@ -2592,7 +2592,7 @@ describe('Agent Methods', () => { /** The re-attach snapshots the current state, so the document now equals the newest * version and the next attach is judged a duplicate. */ - const settled = await getAgent({ id: agentId }); + const settled = await getAgent({ id: agentId }, {}); const newestVersion = settled!.versions![settled!.versions!.length - 1] as VersionEntry; expect(fileIdsOf(settled)).toEqual(['f1']); expect(newestVersion.tool_resources).toEqual(settled!.tool_resources); @@ -2632,7 +2632,7 @@ describe('Agent Methods', () => { /** The document now equals its newest version, so the snapshot is a duplicate and * only the operator can justify recording an entry. */ - const settled = await getAgent({ id: agentId }); + const settled = await getAgent({ id: agentId }, {}); const versionCount = settled!.versions!.length; /** Re-attaching an id the agent already holds makes `$addToSet` a Mongo no-op. An @@ -2643,7 +2643,7 @@ describe('Agent Methods', () => { file_id: 'f1', }); - const after = await getAgent({ id: agentId }); + const after = await getAgent({ id: agentId }, {}); expect(after!.versions).toHaveLength(versionCount); expect( (after?.tool_resources as Record | undefined)?.file_search @@ -2675,7 +2675,7 @@ describe('Agent Methods', () => { file_id: 'f1', }); - const settled = await getAgent({ id: agentId }); + const settled = await getAgent({ id: agentId }, {}); const versionCount = settled!.versions!.length; /** The no-op reading comes from a document fetched before the write, so a `$pull` @@ -2697,7 +2697,7 @@ describe('Agent Methods', () => { expect(suppressedUpdate.$addToSet).toBeUndefined(); expect(suppressedUpdate.$push).toBeUndefined(); expect(suppressedUpdate.$pull).toBeUndefined(); - expect((await getAgent({ id: agentId }))!.versions).toHaveLength(versionCount); + expect((await getAgent({ id: agentId }, {}))!.versions).toHaveLength(versionCount); }); test('should record a version when a duplicate direct update carries an atomic operator', async () => { @@ -2724,7 +2724,7 @@ describe('Agent Methods', () => { expect(updated!.tools).toEqual(['appended_tool']); expect(updated!.versions).toHaveLength(3); - const reloaded = await getAgent({ id: agentId }); + const reloaded = await getAgent({ id: agentId }, {}); expect(reloaded!.tools).toEqual(['appended_tool']); expect(reloaded!.versions).toHaveLength(3); }); @@ -2772,7 +2772,7 @@ describe('Agent Methods', () => { { avatar: { filepath: '/images/a.png', source: 'local' } }, { skipVersioning: true }, ); - const withAvatar = await getAgent({ id: agentId }); + const withAvatar = await getAgent({ id: agentId }, {}); expect(withAvatar!.avatar).toBeTruthy(); expect((withAvatar!.versions![1] as VersionEntry).avatar).toBeUndefined(); @@ -2798,13 +2798,13 @@ describe('Agent Methods', () => { }); await updateAgent({ id: agentId }, { name: 'Idempotent' }); - const before = await getAgent({ id: agentId }); + const before = await getAgent({ id: agentId }, {}); const duplicate = await updateAgent({ id: agentId }, { name: 'Idempotent' }); /** The suppressed path reports the unchanged version count as `version`. */ expect((duplicate as IAgent & { version?: number }).version).toBe(before!.versions!.length); - const after = await getAgent({ id: agentId }); + const after = await getAgent({ id: agentId }, {}); expect(after!.name).toBe(before!.name); expect(after!.tools).toEqual(before!.tools); expect(after!.versions).toHaveLength(before!.versions!.length); @@ -3454,7 +3454,7 @@ describe('Agent Methods', () => { // Since we're updating back to the same model_parameters but with a different description, // it should create a new version - const agent = await getAgent({ id: agentId }); + const agent = await getAgent({ id: agentId }, {}); expect(agent!.versions).toHaveLength(3); }); @@ -3622,7 +3622,7 @@ describe('Agent Methods', () => { expect(repopulated?.versions).toHaveLength(3); // Verify all versions have correct support_contact - const finalAgent = await getAgent({ id: agentId }); + const finalAgent = await getAgent({ id: agentId }, {}); expect(finalAgent!.versions![0]?.support_contact).toEqual({ name: 'Support', email: 'support@test.com', @@ -3672,7 +3672,7 @@ describe('Agent Methods', () => { }, ); - const finalAgent = await getAgent({ id: agentId }); + const finalAgent = await getAgent({ id: agentId }, {}); // Verify version history expect(finalAgent!.versions).toHaveLength(3); @@ -3831,7 +3831,7 @@ describe('Agent Methods', () => { await updateAgent({ id: agentId }, { description: `Version ${i}` }); } - const agent = await getAgent({ id: agentId }); + const agent = await getAgent({ id: agentId }, {}); expect(agent!.versions).toHaveLength(21); expect(agent!.description).toBe('Version 19'); }); @@ -4717,7 +4717,7 @@ describe('Agent Methods', () => { await updateAgent({ id: agentId }, { agent_ids: ['agent3'] }); - const finalAgent = await getAgent({ id: agentId }); + const finalAgent = await getAgent({ id: agentId }, {}); expect(finalAgent!.versions).toHaveLength(3); expect(finalAgent!.versions![0]?.agent_ids).toEqual(['agent1']); diff --git a/packages/data-schemas/src/methods/agent.ts b/packages/data-schemas/src/methods/agent.ts index ceeaa354b0b..bb73ba78cdf 100644 --- a/packages/data-schemas/src/methods/agent.ts +++ b/packages/data-schemas/src/methods/agent.ts @@ -732,10 +732,12 @@ export function createAgentMethods( /** * Get an agent document based on the provided search parameter. + * Without an explicit projection, the unbounded `versions` history is excluded; + * pass `{}` (or a projection including `versions`) to read the full history. */ async function getAgent( searchParameter: FilterQuery, - projection?: ProjectionType, + projection: ProjectionType = { versions: 0 }, ): Promise { const Agent = mongoose.models.Agent as Model; return await Agent.findOne(searchParameter, projection).lean();