diff --git a/src/node/services/agentDefinitions/agentDefinitionsService.test.ts b/src/node/services/agentDefinitions/agentDefinitionsService.test.ts index 9ce3837bdb..32cbc97889 100644 --- a/src/node/services/agentDefinitions/agentDefinitionsService.test.ts +++ b/src/node/services/agentDefinitions/agentDefinitionsService.test.ts @@ -9,6 +9,7 @@ import { LocalRuntime } from "@/node/runtime/LocalRuntime"; import { RemoteRuntime, type SpawnResult } from "@/node/runtime/RemoteRuntime"; import { DisposableTempDir } from "@/node/services/tempDir"; import { + createAgentDefinitionRequestCache, discoverAgentDefinitions, getSkipScopesAboveForKnownScope, readAgentDefinition, @@ -873,6 +874,27 @@ base: a expect(shared[0]?.pluginName).toBeUndefined(); }); + test("cached plugin packages preserve plugin attribution during discovery", async () => { + using project = new DisposableTempDir("agent-defs-plugin-project"); + using global = new DisposableTempDir("agent-defs-plugin-global"); + + const pluginContainer = path.join(project.path, ".mux", "plugins"); + await writePluginWithAgent(pluginContainer, "my-plugin", "helper", "Helper (plugin)"); + + const roots = { + projectRoots: [path.join(project.path, ".mux", "agents")], + globalRoot: global.path, + projectPluginRoots: [pluginContainer], + }; + const runtime = new LocalRuntime(project.path); + const cache = createAgentDefinitionRequestCache(); + + await readAgentDefinition(runtime, project.path, "helper", { roots, cache }); + const agents = await discoverAgentDefinitions(runtime, project.path, { roots, cache }); + + expect(agents.find((agent) => agent.id === "helper")?.pluginName).toBe("my-plugin"); + }); + test("dedupeById: false returns shadowed plugin agents in precedence order", async () => { using project = new DisposableTempDir("agent-defs-plugin-project"); using global = new DisposableTempDir("agent-defs-plugin-global"); diff --git a/src/node/services/agentDefinitions/agentDefinitionsService.ts b/src/node/services/agentDefinitions/agentDefinitionsService.ts index 039c4064ed..f89c68eb53 100644 --- a/src/node/services/agentDefinitions/agentDefinitionsService.ts +++ b/src/node/services/agentDefinitions/agentDefinitionsService.ts @@ -407,6 +407,26 @@ async function readAgentDescriptorFromFile( } } +function buildAgentDescriptorFromPackage( + pkg: AgentDefinitionPackage, + pluginName?: string +): AgentDefinitionDescriptor { + const { selectable } = resolveAgentVisibility(pkg.frontmatter.ui); + return AgentDefinitionDescriptorSchema.parse({ + id: pkg.id, + scope: pkg.scope, + name: pkg.frontmatter.name, + description: pkg.frontmatter.description, + uiSelectable: selectable, + uiColor: pkg.frontmatter.ui?.color, + subagentRunnable: pkg.frontmatter.subagent?.runnable ?? false, + base: pkg.frontmatter.base, + aiDefaults: pkg.frontmatter.ai, + tools: pkg.frontmatter.tools, + ...(pluginName !== undefined ? { pluginName } : {}), + }); +} + function buildBuiltInAgentDescriptor( pkg: ReturnType[number] ): AgentDefinitionDescriptor { @@ -433,6 +453,8 @@ export async function discoverAgentDefinitions( roots?: AgentDefinitionsRoots; /** agent-plugins experiment: also scan Agent Plugins agents (used only when `roots` is absent). */ includeAgentPlugins?: boolean; + /** Request-scoped parsed definitions already loaded during agent resolution. */ + cache?: AgentDefinitionRequestCache; /** * When false, return every discovered descriptor in precedence order * (shadowed ids included) instead of only the effective one per id. @@ -494,14 +516,18 @@ export async function discoverAgentDefinitions( if (!contained) continue; } - const descriptor = await readAgentDescriptorFromFile( - scan.runtime, - filePath, - agentId, - scan.scope, - scan.pluginName, - scan.pluginRoot - ); + const cachedSource = options?.cache?.getSource(scan.runtime, filePath); + const descriptor = + cachedSource == null + ? await readAgentDescriptorFromFile( + scan.runtime, + filePath, + agentId, + scan.scope, + scan.pluginName, + scan.pluginRoot + ) + : buildAgentDescriptorFromPackage(cachedSource.definition, cachedSource.pluginName); if (!descriptor) continue; if (dedupeById) { @@ -551,10 +577,110 @@ export async function discoverAgentDefinitions( }); } +interface CachedAgentDefinitionSource { + definition: AgentDefinitionPackage; + pluginName?: string; +} + +interface AgentDefinitionSource { + runtime: Runtime; + filePath: string; + pluginName?: string; +} + +export interface AgentDefinitionRequestCache { + getWinningDefinition( + runtime: Runtime, + workspacePath: string, + agentId: AgentId, + options?: ReadAgentDefinitionOptions + ): AgentDefinitionPackage | undefined; + getSource(runtime: Runtime, filePath: string): CachedAgentDefinitionSource | undefined; + rememberWinningDefinition( + runtime: Runtime, + workspacePath: string, + agentId: AgentId, + definition: AgentDefinitionPackage, + options?: ReadAgentDefinitionOptions, + source?: AgentDefinitionSource + ): void; +} + +export function createAgentDefinitionRequestCache(): AgentDefinitionRequestCache { + const winningByRuntime = new WeakMap>(); + const bySource = new Map(); + const remoteRuntimeIds = new WeakMap(); + const rootsIds = new WeakMap(); + let nextRemoteRuntimeId = 1; + let nextRootsId = 1; + + const keyFor = ( + workspacePath: string, + agentId: AgentId, + options?: ReadAgentDefinitionOptions + ): string => { + let rootsId = "default"; + if (options?.roots != null) { + let id = rootsIds.get(options.roots); + if (id == null) { + id = nextRootsId++; + rootsIds.set(options.roots, id); + } + rootsId = String(id); + } + return JSON.stringify([ + workspacePath, + agentId, + options?.skipScopesAbove ?? null, + options?.includeAgentPlugins === true, + rootsId, + ]); + }; + + const sourceKeyFor = (runtime: Runtime, filePath: string): string => { + if (!(runtime instanceof RemoteRuntime)) { + return JSON.stringify(["host", filePath]); + } + let runtimeId = remoteRuntimeIds.get(runtime); + if (runtimeId == null) { + runtimeId = nextRemoteRuntimeId++; + remoteRuntimeIds.set(runtime, runtimeId); + } + return JSON.stringify([runtimeId, filePath]); + }; + + return { + getWinningDefinition(runtime, workspacePath, agentId, options) { + return winningByRuntime.get(runtime)?.get(keyFor(workspacePath, agentId, options)) + ?.definition; + }, + getSource(runtime, filePath) { + return bySource.get(sourceKeyFor(runtime, filePath)); + }, + rememberWinningDefinition(runtime, workspacePath, agentId, definition, options, source) { + const cachedSource = { + definition, + ...(source?.pluginName !== undefined ? { pluginName: source.pluginName } : {}), + }; + let definitions = winningByRuntime.get(runtime); + if (definitions == null) { + definitions = new Map(); + winningByRuntime.set(runtime, definitions); + } + definitions.set(keyFor(workspacePath, agentId, options), cachedSource); + if (source != null) { + bySource.set(sourceKeyFor(source.runtime, source.filePath), cachedSource); + } + }, + }; +} + export interface ReadAgentDefinitionOptions { roots?: AgentDefinitionsRoots; /** agent-plugins experiment: also probe Agent Plugins agents (used only when `roots` is absent). */ includeAgentPlugins?: boolean; + /** Request-scoped parsed definition reuse; callers own the cache lifetime. */ + cache?: AgentDefinitionRequestCache; /** * Skip scopes at or above this level when resolving. * Used for base resolution: when a project-scope agent has `base: exec`, @@ -575,6 +701,11 @@ export async function readAgentDefinition( throw new Error("readAgentDefinition: workspacePath is required"); } + const cached = options?.cache?.getWinningDefinition(runtime, workspacePath, agentId, options); + if (cached != null) { + return cached; + } + const roots = options?.roots ?? getDefaultAgentDefinitionsRoots(runtime, workspacePath, { @@ -621,6 +752,23 @@ export async function readAgentDefinition( if (!contained) continue; } + const cachedSource = options?.cache?.getSource(candidate.runtime, filePath); + if (cachedSource != null) { + options.cache?.rememberWinningDefinition( + runtime, + workspacePath, + agentId, + cachedSource.definition, + options, + { + runtime: candidate.runtime, + filePath, + pluginName: candidate.pluginName, + } + ); + return cachedSource.definition; + } + try { let content: string; let byteSize: number; @@ -671,6 +819,18 @@ export async function readAgentDefinition( ); } + options?.cache?.rememberWinningDefinition( + runtime, + workspacePath, + agentId, + validated.data, + options, + { + runtime: candidate.runtime, + filePath, + pluginName: candidate.pluginName, + } + ); return validated.data; } catch { continue; @@ -686,6 +846,13 @@ export async function readAgentDefinition( `Invalid built-in agent definition '${agentId}': ${validated.error.message}` ); } + options?.cache?.rememberWinningDefinition( + runtime, + workspacePath, + agentId, + validated.data, + options + ); return validated.data; } } @@ -709,6 +876,7 @@ export async function resolveAgentBody( agentId: AgentId, options?: { roots?: AgentDefinitionsRoots; + cache?: AgentDefinitionRequestCache; includeAgentPlugins?: boolean; skipScopesAbove?: AgentDefinitionScope; } @@ -751,6 +919,7 @@ export async function resolveAgentBody( const pkg = await readAgentDefinition(runtime, workspacePath, id, { roots: options?.roots, includeAgentPlugins: options?.includeAgentPlugins, + cache: options?.cache, skipScopesAbove, }); @@ -853,6 +1022,7 @@ export async function resolveAgentFrontmatter( agentId: AgentId, options?: { roots?: AgentDefinitionsRoots; + cache?: AgentDefinitionRequestCache; includeAgentPlugins?: boolean; skipScopesAbove?: AgentDefinitionScope; } @@ -900,6 +1070,7 @@ export async function resolveAgentFrontmatter( const pkg = await readAgentDefinition(runtime, workspacePath, id, { roots: options?.roots, includeAgentPlugins: options?.includeAgentPlugins, + cache: options?.cache, skipScopesAbove, }); diff --git a/src/node/services/agentDefinitions/resolveAgentInheritanceChain.ts b/src/node/services/agentDefinitions/resolveAgentInheritanceChain.ts index d361ececdc..e517e9e22d 100644 --- a/src/node/services/agentDefinitions/resolveAgentInheritanceChain.ts +++ b/src/node/services/agentDefinitions/resolveAgentInheritanceChain.ts @@ -8,6 +8,7 @@ import { computeBaseSkipScope, MAX_INHERITANCE_DEPTH, readAgentDefinition, + type AgentDefinitionRequestCache, } from "./agentDefinitionsService"; import { getErrorMessage } from "@/common/utils/errors"; @@ -30,6 +31,8 @@ interface ResolveAgentInheritanceChainOptions { agentDefinition: AgentDefinitionPackage; workspaceId: string; maxDepth?: number; + /** Request-scoped parsed definition reuse. */ + cache?: AgentDefinitionRequestCache; /** agent-plugins experiment: also resolve base agents contributed by Agent Plugins. */ includeAgentPlugins?: boolean; } @@ -88,6 +91,7 @@ export async function resolveAgentInheritanceChain( try { currentDefinition = await readAgentDefinition(runtime, workspacePath, baseId, { + cache: options.cache, includeAgentPlugins: options.includeAgentPlugins, skipScopesAbove, }); diff --git a/src/node/services/agentResolution.test.ts b/src/node/services/agentResolution.test.ts index d64af1e3a5..00f55b5bc0 100644 --- a/src/node/services/agentResolution.test.ts +++ b/src/node/services/agentResolution.test.ts @@ -8,8 +8,20 @@ import type { WorkspaceMetadata } from "@/common/types/workspace"; import { DEFAULT_RUNTIME_CONFIG } from "@/common/constants/workspace"; import { LocalRuntime } from "@/node/runtime/LocalRuntime"; import { DisposableTempDir } from "@/node/services/tempDir"; +import { buildStreamSystemContext } from "./streamContextBuilder"; import { getLegacyModeForAgentMetadata, resolveAgentForStream } from "./agentResolution"; +class CountingRuntime extends LocalRuntime { + readonly agentReads = new Map(); + + override readFile(filePath: string, abortSignal?: AbortSignal): ReadableStream { + if (filePath.endsWith(".md") && filePath.includes(`${path.sep}agents${path.sep}`)) { + this.agentReads.set(filePath, (this.agentReads.get(filePath) ?? 0) + 1); + } + return super.readFile(filePath, abortSignal); + } +} + const PARENT_WORKSPACE_ID = "parent-workspace"; const CHILD_WORKSPACE_ID = "child-workspace"; @@ -84,6 +96,168 @@ async function resolvePolicyForAgent(params: { return result.data.effectiveToolPolicy ?? []; } +describe("resolveAgentForStream discovery reuse", () => { + test("reuses request-scoped definitions for available-subagent discovery", async () => { + using tempDir = new DisposableTempDir("agent-resolution-discovery-reuse"); + const projectPath = path.join(tempDir.path, "project"); + const xumHome = path.join(tempDir.path, "xum-home"); + const previousXumRoot = process.env.XUM_ROOT; + process.env.XUM_ROOT = xumHome; + using _xumRoot = { + [Symbol.dispose]() { + if (previousXumRoot === undefined) { + delete process.env.XUM_ROOT; + } else { + process.env.XUM_ROOT = previousXumRoot; + } + }, + }; + const canonicalProjectAgents = path.join(projectPath, ".xum", "agents"); + const projectAgents = path.join(projectPath, ".mux", "agents"); + const globalAgents = path.join(xumHome, "agents"); + await fs.mkdir(canonicalProjectAgents, { recursive: true }); + await fs.mkdir(projectAgents, { recursive: true }); + await fs.mkdir(globalAgents, { recursive: true }); + + // An invalid canonical definition must not become the cached winner over the + // valid legacy-root definition at the same project scope. + await fs.writeFile(path.join(canonicalProjectAgents, "selected.md"), "invalid", "utf-8"); + await fs.writeFile( + path.join(projectAgents, "selected.md"), + `---\nname: Selected Project Agent\nbase: shared\nsubagent:\n runnable: true\ntools:\n add:\n - bash\n---\nSelected body\n`, + "utf-8" + ); + await fs.writeFile( + path.join(projectAgents, "disabled.md"), + `---\nname: Disabled Agent\ndisabled: true\nsubagent:\n runnable: true\n---\nDisabled body\n`, + "utf-8" + ); + await fs.writeFile( + path.join(globalAgents, "selected.md"), + `---\nname: Shadowed Global Agent\nsubagent:\n runnable: true\n---\nShadowed body\n`, + "utf-8" + ); + await fs.writeFile( + path.join(globalAgents, "shared.md"), + `---\nname: Shared Base\nbase: exec\nsubagent:\n runnable: true\n---\nShared body\n`, + "utf-8" + ); + + const runtime = new CountingRuntime(projectPath); + const metadata: WorkspaceMetadata = { + id: "workspace", + name: "workspace", + projectName: "project", + projectPath, + runtimeConfig: DEFAULT_RUNTIME_CONFIG, + }; + const cfg: ProjectsConfig = { + projects: new Map([ + [projectPath, { trusted: true, workspaces: [{ id: "workspace", name: "workspace" }] }], + ]), + agentAiDefaults: { + disabled: { enabled: false }, + }, + }; + + const baselineRuntime = new CountingRuntime(projectPath); + const baselineResult = await resolveAgentForStream({ + workspaceId: metadata.id, + metadata, + runtime: baselineRuntime, + workspacePath: projectPath, + requestedAgentId: "selected", + disableWorkspaceAgents: false, + callerToolPolicy: undefined, + cfg, + emitError: () => undefined, + isAdvisorExperimentEnabled: false, + }); + if (!baselineResult.success) { + throw new Error("Expected baseline agent resolution to succeed"); + } + await buildStreamSystemContext({ + runtime: baselineRuntime, + metadata, + workspacePath: projectPath, + workspaceId: metadata.id, + agentDefinition: baselineResult.data.agentDefinition, + effectiveMode: "exec", + agentDiscoveryRuntime: baselineResult.data.agentDiscoveryRuntime, + agentDiscoveryPath: baselineResult.data.agentDiscoveryPath, + isSubagentWorkspace: false, + effectiveAdditionalInstructions: undefined, + modelString: "openai:gpt-5.2", + cfg, + providersConfig: null, + mcpServers: {}, + loadDesktopCapability: () => + Promise.resolve({ available: false as const, reason: "unsupported_runtime" as const }), + }); + expect(baselineRuntime.agentReads.get(path.join(projectAgents, "selected.md"))).toBe(4); + + const result = await resolveAgentForStream({ + workspaceId: metadata.id, + metadata, + runtime, + workspacePath: projectPath, + requestedAgentId: "selected", + disableWorkspaceAgents: false, + callerToolPolicy: undefined, + cfg, + emitError: () => undefined, + isAdvisorExperimentEnabled: false, + }); + if (!result.success) { + throw new Error("Expected agent resolution to succeed"); + } + + const context = await buildStreamSystemContext({ + runtime, + metadata, + workspacePath: projectPath, + workspaceId: metadata.id, + agentDefinitionCache: result.data.agentDefinitionCache, + agentDefinition: result.data.agentDefinition, + effectiveMode: "exec", + agentDiscoveryRuntime: result.data.agentDiscoveryRuntime, + agentDiscoveryPath: result.data.agentDiscoveryPath, + isSubagentWorkspace: false, + effectiveAdditionalInstructions: undefined, + modelString: "openai:gpt-5.2", + cfg, + providersConfig: null, + mcpServers: {}, + loadDesktopCapability: () => + Promise.resolve({ available: false as const, reason: "unsupported_runtime" as const }), + }); + const available = context.agentDefinitions ?? []; + + expect(context.agentSystemPromptSections[0]).toContain("Selected body"); + expect(context.agentSystemPromptSections[0]).toContain("Shared body"); + expect(result.data.effectiveAgentId).toBe("selected"); + expect(result.data.agentDefinition.scope).toBe("project"); + expect(result.data.agentInheritanceChain.map((agent) => agent.id)).toEqual([ + "selected", + "shared", + "exec", + ]); + expect(result.data.effectiveToolPolicy).toContainEqual({ + regex_match: "bash", + action: "enable", + }); + expect(available.find((agent) => agent.id === "selected")?.name).toBe("Selected Project Agent"); + expect(available.find((agent) => agent.id === "selected")?.scope).toBe("project"); + expect(available.find((agent) => agent.id === "selected")?.subagentRunnable).toBe(true); + expect(available.some((agent) => agent.id === "disabled")).toBe(false); + expect(available.some((agent) => agent.id === "desktop")).toBe(false); + + expect(runtime.agentReads.get(path.join(projectAgents, "selected.md"))).toBe(1); + expect(runtime.agentReads.get(path.join(globalAgents, "selected.md")) ?? 0).toBe(0); + expect(runtime.agentReads.get(path.join(globalAgents, "shared.md"))).toBe(1); + }); +}); + describe("getLegacyModeForAgentMetadata", () => { test("omits legacy mode metadata for custom or derived agents", () => { expect(getLegacyModeForAgentMetadata("explore", "exec")).toBeUndefined(); diff --git a/src/node/services/agentResolution.ts b/src/node/services/agentResolution.ts index 69ef9729d7..1d9eb4a307 100644 --- a/src/node/services/agentResolution.ts +++ b/src/node/services/agentResolution.ts @@ -28,6 +28,7 @@ import { type ToolPolicy } from "@/common/utils/tools/toolPolicy"; import { createRuntimeContextForWorkspace } from "@/node/runtime/runtimeHelpers"; import type { Runtime } from "@/node/runtime/Runtime"; import { + createAgentDefinitionRequestCache, getSkipScopesAboveForKnownScope, readAgentDefinition, resolveAgentFrontmatter, @@ -78,6 +79,8 @@ export interface ResolveAgentOptions { export interface AgentResolutionResult { effectiveAgentId: string; agentDefinition: Awaited>; + /** Request-scoped parsed definitions shared with later stream-context discovery. */ + agentDefinitionCache: ReturnType; /** Runtime used for agent discovery (child workspace or parent fallback for untracked agents). */ agentDiscoveryRuntime: Runtime; /** Path used for agent discovery (workspace path or project path if agents disabled). */ @@ -207,6 +210,7 @@ export async function resolveAgentForStream( includeAgentPlugins, } = opts; + const agentDefinitionCache = createAgentDefinitionRequestCache(); const workspaceLog = log.withFields({ workspaceId, workspaceName: metadata.name }); // --- Agent ID resolution --- @@ -258,7 +262,7 @@ export async function resolveAgentForStream( discovery.runtime, discovery.workspacePath, candidateAgentId, - { includeAgentPlugins } + { includeAgentPlugins, cache: agentDefinitionCache } ); if (definition.scope === "project") { agentDefinition = definition; @@ -303,6 +307,7 @@ export async function resolveAgentForStream( }); agentDefinition = await readAgentDefinition(agentDiscoveryRuntime, agentDiscoveryPath, "exec", { includeAgentPlugins, + cache: agentDefinitionCache, }); } @@ -347,6 +352,7 @@ export async function resolveAgentForStream( agentDiscoveryPath, agentDefinition.id, { + cache: agentDefinitionCache, includeAgentPlugins, skipScopesAbove: getSkipScopesAboveForKnownScope(agentDefinition.scope), } @@ -398,7 +404,7 @@ export async function resolveAgentForStream( agentDiscoveryRuntime, agentDiscoveryPath, "exec", - { includeAgentPlugins } + { includeAgentPlugins, cache: agentDefinitionCache } ); effectiveAgentId = agentDefinition.id; } @@ -431,6 +437,7 @@ export async function resolveAgentForStream( workspacePath: agentDiscoveryPath, agentId: agentDefinition.id, agentDefinition, + cache: agentDefinitionCache, workspaceId, includeAgentPlugins, }); @@ -514,6 +521,7 @@ export async function resolveAgentForStream( return Ok({ effectiveAgentId, + agentDefinitionCache, agentDefinition, agentDiscoveryRuntime, agentDiscoveryPath, diff --git a/src/node/services/aiService.ts b/src/node/services/aiService.ts index d0d192fd51..940cfd9098 100644 --- a/src/node/services/aiService.ts +++ b/src/node/services/aiService.ts @@ -1889,6 +1889,7 @@ export class AIService extends EventEmitter { return agentResult; } const { + agentDefinitionCache, effectiveAgentId, agentDefinition, agentDiscoveryRuntime, @@ -2082,6 +2083,7 @@ export class AIService extends EventEmitter { metadata, workspacePath, workspaceId, + agentDefinitionCache, agentDefinition, effectiveMode, agentDiscoveryRuntime, diff --git a/src/node/services/streamContextBuilder.ts b/src/node/services/streamContextBuilder.ts index 0fe0a61e4a..5dcef36c73 100644 --- a/src/node/services/streamContextBuilder.ts +++ b/src/node/services/streamContextBuilder.ts @@ -37,6 +37,7 @@ import { resolveAgentFrontmatter, discoverAgentDefinitions, getSkipScopesAboveForKnownScope, + type AgentDefinitionRequestCache, type AgentDefinitionsRoots, } from "@/node/services/agentDefinitions/agentDefinitionsService"; import { isAgentEffectivelyDisabled } from "@/node/services/agentDefinitions/agentEnablement"; @@ -240,6 +241,7 @@ export interface BuildStreamSystemContextOptions { */ effectiveMode: "plan" | "exec" | "compact"; /** Runtime that resolved the active agent definition. May be the parent workspace runtime for subagents. */ + agentDefinitionCache?: AgentDefinitionRequestCache; agentDiscoveryRuntime: Runtime; agentDiscoveryPath: string; isSubagentWorkspace: boolean; @@ -535,6 +537,7 @@ export async function buildStreamSystemContext( agentDiscoveryPath, agentDefinition.id, { + cache: opts.agentDefinitionCache, includeAgentPlugins: opts.agentPluginsEnabled, skipScopesAbove: getSkipScopesAboveForKnownScope(agentDefinition.scope), } @@ -548,6 +551,7 @@ export async function buildStreamSystemContext( agentDiscoveryPath, agentDefinition.id, { + cache: opts.agentDefinitionCache, includeAgentPlugins: opts.agentPluginsEnabled, skipScopesAbove: getSkipScopesAboveForKnownScope(agentDefinition.scope), } @@ -584,6 +588,7 @@ export async function buildStreamSystemContext( agentDefinitions = await discoverAvailableSubagentsForToolContext({ runtime: agentDiscoveryRuntime, workspacePath: agentDiscoveryPath, + cache: opts.agentDefinitionCache, cfg, loadDesktopCapability, includeAgentPlugins: opts.agentPluginsEnabled, @@ -690,6 +695,8 @@ export async function discoverAvailableSubagentsForToolContext(args: { runtime: Parameters[0]; workspacePath: string; cfg: ProjectsConfig; + definitions?: Awaited>; + cache?: AgentDefinitionRequestCache; roots?: AgentDefinitionsRoots; loadDesktopCapability?: () => Promise; /** agent-plugins experiment: also discover agents contributed by Agent Plugins. */ @@ -703,10 +710,13 @@ export async function discoverAvailableSubagentsForToolContext(args: { ); assert(args.cfg, "discoverAvailableSubagentsForToolContext: cfg is required"); - const discovered = await discoverAgentDefinitions(args.runtime, args.workspacePath, { - roots: args.roots, - includeAgentPlugins: args.includeAgentPlugins, - }); + const discovered = + args.definitions ?? + (await discoverAgentDefinitions(args.runtime, args.workspacePath, { + cache: args.cache, + roots: args.roots, + includeAgentPlugins: args.includeAgentPlugins, + })); let desktopAvailablePromise: Promise | undefined; const isDesktopAvailable = async (): Promise => { @@ -731,6 +741,7 @@ export async function discoverAvailableSubagentsForToolContext(args: { args.workspacePath, descriptor.id, { + cache: args.cache, roots: args.roots, includeAgentPlugins: args.includeAgentPlugins, skipScopesAbove: getSkipScopesAboveForKnownScope(descriptor.scope),