summaryrefslogtreecommitdiff
path: root/packages/core/src/utils
diff options
context:
space:
mode:
authorAbhi <[email protected]>2025-08-06 20:34:38 -0400
committerGitHub <[email protected]>2025-08-07 00:34:38 +0000
commit36750ca49b1b2fa43a3d7904416b876203a1850f (patch)
tree876276dc4bd2c76a385944f0fd39f252357590ed /packages/core/src/utils
parentd6a7334279366762787bed6a5bd08a125c7c3ba8 (diff)
feat(agent): Introduce Foundational Subagent Architecture (#1805)
Co-authored-by: Colt McAnlis <[email protected]>
Diffstat (limited to 'packages/core/src/utils')
-rw-r--r--packages/core/src/utils/environmentContext.test.ts205
-rw-r--r--packages/core/src/utils/environmentContext.ts109
2 files changed, 314 insertions, 0 deletions
diff --git a/packages/core/src/utils/environmentContext.test.ts b/packages/core/src/utils/environmentContext.test.ts
new file mode 100644
index 00000000..656fb63f
--- /dev/null
+++ b/packages/core/src/utils/environmentContext.test.ts
@@ -0,0 +1,205 @@
+/**
+ * @license
+ * Copyright 2025 Google LLC
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+import {
+ describe,
+ it,
+ expect,
+ vi,
+ beforeEach,
+ afterEach,
+ type Mock,
+} from 'vitest';
+import {
+ getEnvironmentContext,
+ getDirectoryContextString,
+} from './environmentContext.js';
+import { Config } from '../config/config.js';
+import { getFolderStructure } from './getFolderStructure.js';
+
+vi.mock('../config/config.js');
+vi.mock('./getFolderStructure.js', () => ({
+ getFolderStructure: vi.fn(),
+}));
+vi.mock('../tools/read-many-files.js');
+
+describe('getDirectoryContextString', () => {
+ let mockConfig: Partial<Config>;
+
+ beforeEach(() => {
+ mockConfig = {
+ getWorkspaceContext: vi.fn().mockReturnValue({
+ getDirectories: vi.fn().mockReturnValue(['/test/dir']),
+ }),
+ getFileService: vi.fn(),
+ };
+ vi.mocked(getFolderStructure).mockResolvedValue('Mock Folder Structure');
+ });
+
+ afterEach(() => {
+ vi.resetAllMocks();
+ });
+
+ it('should return context string for a single directory', async () => {
+ const contextString = await getDirectoryContextString(mockConfig as Config);
+ expect(contextString).toContain(
+ "I'm currently working in the directory: /test/dir",
+ );
+ expect(contextString).toContain(
+ 'Here is the folder structure of the current working directories:\n\nMock Folder Structure',
+ );
+ });
+
+ it('should return context string for multiple directories', async () => {
+ (
+ vi.mocked(mockConfig.getWorkspaceContext!)().getDirectories as Mock
+ ).mockReturnValue(['/test/dir1', '/test/dir2']);
+ vi.mocked(getFolderStructure)
+ .mockResolvedValueOnce('Structure 1')
+ .mockResolvedValueOnce('Structure 2');
+
+ const contextString = await getDirectoryContextString(mockConfig as Config);
+ expect(contextString).toContain(
+ "I'm currently working in the following directories:\n - /test/dir1\n - /test/dir2",
+ );
+ expect(contextString).toContain(
+ 'Here is the folder structure of the current working directories:\n\nStructure 1\nStructure 2',
+ );
+ });
+});
+
+describe('getEnvironmentContext', () => {
+ let mockConfig: Partial<Config>;
+ let mockToolRegistry: { getTool: Mock };
+
+ beforeEach(() => {
+ vi.useFakeTimers();
+ vi.setSystemTime(new Date('2025-08-05T12:00:00Z'));
+
+ mockToolRegistry = {
+ getTool: vi.fn(),
+ };
+
+ mockConfig = {
+ getWorkspaceContext: vi.fn().mockReturnValue({
+ getDirectories: vi.fn().mockReturnValue(['/test/dir']),
+ }),
+ getFileService: vi.fn(),
+ getFullContext: vi.fn().mockReturnValue(false),
+ getToolRegistry: vi.fn().mockResolvedValue(mockToolRegistry),
+ };
+
+ vi.mocked(getFolderStructure).mockResolvedValue('Mock Folder Structure');
+ });
+
+ afterEach(() => {
+ vi.useRealTimers();
+ vi.resetAllMocks();
+ });
+
+ it('should return basic environment context for a single directory', async () => {
+ const parts = await getEnvironmentContext(mockConfig as Config);
+
+ expect(parts.length).toBe(1);
+ const context = parts[0].text;
+
+ expect(context).toContain("Today's date is Tuesday, August 5, 2025");
+ expect(context).toContain(`My operating system is: ${process.platform}`);
+ expect(context).toContain(
+ "I'm currently working in the directory: /test/dir",
+ );
+ expect(context).toContain(
+ 'Here is the folder structure of the current working directories:\n\nMock Folder Structure',
+ );
+ expect(getFolderStructure).toHaveBeenCalledWith('/test/dir', {
+ fileService: undefined,
+ });
+ });
+
+ it('should return basic environment context for multiple directories', async () => {
+ (
+ vi.mocked(mockConfig.getWorkspaceContext!)().getDirectories as Mock
+ ).mockReturnValue(['/test/dir1', '/test/dir2']);
+ vi.mocked(getFolderStructure)
+ .mockResolvedValueOnce('Structure 1')
+ .mockResolvedValueOnce('Structure 2');
+
+ const parts = await getEnvironmentContext(mockConfig as Config);
+
+ expect(parts.length).toBe(1);
+ const context = parts[0].text;
+
+ expect(context).toContain(
+ "I'm currently working in the following directories:\n - /test/dir1\n - /test/dir2",
+ );
+ expect(context).toContain(
+ 'Here is the folder structure of the current working directories:\n\nStructure 1\nStructure 2',
+ );
+ expect(getFolderStructure).toHaveBeenCalledTimes(2);
+ });
+
+ it('should include full file context when getFullContext is true', async () => {
+ mockConfig.getFullContext = vi.fn().mockReturnValue(true);
+ const mockReadManyFilesTool = {
+ build: vi.fn().mockReturnValue({
+ execute: vi
+ .fn()
+ .mockResolvedValue({ llmContent: 'Full file content here' }),
+ }),
+ };
+ mockToolRegistry.getTool.mockReturnValue(mockReadManyFilesTool);
+
+ const parts = await getEnvironmentContext(mockConfig as Config);
+
+ expect(parts.length).toBe(2);
+ expect(parts[1].text).toBe(
+ '\n--- Full File Context ---\nFull file content here',
+ );
+ expect(mockToolRegistry.getTool).toHaveBeenCalledWith('read_many_files');
+ expect(mockReadManyFilesTool.build).toHaveBeenCalledWith({
+ paths: ['**/*'],
+ useDefaultExcludes: true,
+ });
+ });
+
+ it('should handle read_many_files returning no content', async () => {
+ mockConfig.getFullContext = vi.fn().mockReturnValue(true);
+ const mockReadManyFilesTool = {
+ build: vi.fn().mockReturnValue({
+ execute: vi.fn().mockResolvedValue({ llmContent: '' }),
+ }),
+ };
+ mockToolRegistry.getTool.mockReturnValue(mockReadManyFilesTool);
+
+ const parts = await getEnvironmentContext(mockConfig as Config);
+
+ expect(parts.length).toBe(1); // No extra part added
+ });
+
+ it('should handle read_many_files tool not being found', async () => {
+ mockConfig.getFullContext = vi.fn().mockReturnValue(true);
+ mockToolRegistry.getTool.mockReturnValue(null);
+
+ const parts = await getEnvironmentContext(mockConfig as Config);
+
+ expect(parts.length).toBe(1); // No extra part added
+ });
+
+ it('should handle errors when reading full file context', async () => {
+ mockConfig.getFullContext = vi.fn().mockReturnValue(true);
+ const mockReadManyFilesTool = {
+ build: vi.fn().mockReturnValue({
+ execute: vi.fn().mockRejectedValue(new Error('Read error')),
+ }),
+ };
+ mockToolRegistry.getTool.mockReturnValue(mockReadManyFilesTool);
+
+ const parts = await getEnvironmentContext(mockConfig as Config);
+
+ expect(parts.length).toBe(2);
+ expect(parts[1].text).toBe('\n--- Error reading full file context ---');
+ });
+});
diff --git a/packages/core/src/utils/environmentContext.ts b/packages/core/src/utils/environmentContext.ts
new file mode 100644
index 00000000..79fb6049
--- /dev/null
+++ b/packages/core/src/utils/environmentContext.ts
@@ -0,0 +1,109 @@
+/**
+ * @license
+ * Copyright 2025 Google LLC
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+import { Part } from '@google/genai';
+import { Config } from '../config/config.js';
+import { getFolderStructure } from './getFolderStructure.js';
+
+/**
+ * Generates a string describing the current workspace directories and their structures.
+ * @param {Config} config - The runtime configuration and services.
+ * @returns {Promise<string>} A promise that resolves to the directory context string.
+ */
+export async function getDirectoryContextString(
+ config: Config,
+): Promise<string> {
+ const workspaceContext = config.getWorkspaceContext();
+ const workspaceDirectories = workspaceContext.getDirectories();
+
+ const folderStructures = await Promise.all(
+ workspaceDirectories.map((dir) =>
+ getFolderStructure(dir, {
+ fileService: config.getFileService(),
+ }),
+ ),
+ );
+
+ const folderStructure = folderStructures.join('\n');
+
+ let workingDirPreamble: string;
+ if (workspaceDirectories.length === 1) {
+ workingDirPreamble = `I'm currently working in the directory: ${workspaceDirectories[0]}`;
+ } else {
+ const dirList = workspaceDirectories.map((dir) => ` - ${dir}`).join('\n');
+ workingDirPreamble = `I'm currently working in the following directories:\n${dirList}`;
+ }
+
+ return `${workingDirPreamble}
+Here is the folder structure of the current working directories:
+
+${folderStructure}`;
+}
+
+/**
+ * Retrieves environment-related information to be included in the chat context.
+ * This includes the current working directory, date, operating system, and folder structure.
+ * Optionally, it can also include the full file context if enabled.
+ * @param {Config} config - The runtime configuration and services.
+ * @returns A promise that resolves to an array of `Part` objects containing environment information.
+ */
+export async function getEnvironmentContext(config: Config): Promise<Part[]> {
+ const today = new Date().toLocaleDateString(undefined, {
+ weekday: 'long',
+ year: 'numeric',
+ month: 'long',
+ day: 'numeric',
+ });
+ const platform = process.platform;
+ const directoryContext = await getDirectoryContextString(config);
+
+ const context = `
+This is the Gemini CLI. We are setting up the context for our chat.
+Today's date is ${today}.
+My operating system is: ${platform}
+${directoryContext}
+ `.trim();
+
+ const initialParts: Part[] = [{ text: context }];
+ const toolRegistry = await config.getToolRegistry();
+
+ // Add full file context if the flag is set
+ if (config.getFullContext()) {
+ try {
+ const readManyFilesTool = toolRegistry.getTool('read_many_files');
+ if (readManyFilesTool) {
+ const invocation = readManyFilesTool.build({
+ paths: ['**/*'], // Read everything recursively
+ useDefaultExcludes: true, // Use default excludes
+ });
+
+ // Read all files in the target directory
+ const result = await invocation.execute(AbortSignal.timeout(30000));
+ if (result.llmContent) {
+ initialParts.push({
+ text: `\n--- Full File Context ---\n${result.llmContent}`,
+ });
+ } else {
+ console.warn(
+ 'Full context requested, but read_many_files returned no content.',
+ );
+ }
+ } else {
+ console.warn(
+ 'Full context requested, but read_many_files tool not found.',
+ );
+ }
+ } catch (error) {
+ // Not using reportError here as it's a startup/config phase, not a chat/generation phase error.
+ console.error('Error reading full file context:', error);
+ initialParts.push({
+ text: '\n--- Error reading full file context ---',
+ });
+ }
+ }
+
+ return initialParts;
+}