diff --git a/frontend/src/app/shell/shell.component.html b/frontend/src/app/shell/shell.component.html
index a57e0114..c8ca5250 100644
--- a/frontend/src/app/shell/shell.component.html
+++ b/frontend/src/app/shell/shell.component.html
@@ -54,7 +54,7 @@
- Code Operations
+ Code Repositories
diff --git a/src/agent/LlmFunctions.ts b/src/agent/LlmFunctions.ts
index 7a3c94a6..3f2f5201 100644
--- a/src/agent/LlmFunctions.ts
+++ b/src/agent/LlmFunctions.ts
@@ -3,10 +3,11 @@ import { FUNC_SEP, FunctionSchema, getFunctionSchemas } from '#functionSchema/fu
import { FunctionCall } from '#llm/llm';
import { logger } from '#o11y/logger';
-import { FileSystem } from '#functions/storage/filesystem';
+import { FileSystemService } from '#functions/storage/fileSystemService';
import { GetToolType, ToolType, toolType } from '#functions/toolType';
import { functionFactory } from '#functionSchema/functionDecorators';
+import { FileSystemRead } from '#functions/storage/FileSystemRead';
/**
* Holds the instances of the classes with function callable methods.
@@ -31,6 +32,8 @@ export class LlmFunctions {
for (const functionClassName of functionClassNames) {
const ctor = functionFactory()[functionClassName];
if (ctor) this.functionInstances[functionClassName] = new ctor();
+ else if (functionClassName === 'FileSystem')
+ this.functionInstances[FileSystemRead.name] = new FileSystemRead(); // backwards compatability from creating FileSystemRead/Write wrappers
else logger.warn(`${functionClassName} not found`);
}
return this;
diff --git a/src/agent/agentContext.test.ts b/src/agent/agentContext.test.ts
index 3f6794a3..85327847 100644
--- a/src/agent/agentContext.test.ts
+++ b/src/agent/agentContext.test.ts
@@ -1,10 +1,12 @@
import { expect } from 'chai';
import sinon from 'sinon';
import { LlmFunctions } from '#agent/LlmFunctions';
-import { createContext, deserializeAgentContext, serializeContext } from '#agent/agentContextLocalStorage';
+import { createContext } from '#agent/agentContextLocalStorage';
import { AgentContext } from '#agent/agentContextTypes';
import { RunAgentConfig } from '#agent/agentRunner';
-import { FileSystem } from '#functions/storage/filesystem';
+import { deserializeAgentContext, serializeContext } from '#agent/agentSerialization';
+import { FileSystemRead } from '#functions/storage/FileSystemRead';
+import { FileSystemService } from '#functions/storage/fileSystemService';
import { LlmTools } from '#functions/util';
import { GPT4o } from '#llm/models/openai';
import { appContext } from '../app';
@@ -25,7 +27,7 @@ describe('agentContext', () => {
xhard: GPT4o(),
};
// We want to check that the FileSystem gets re-added by the resetFileSystemFunction function
- const functions = new LlmFunctions(LlmTools, FileSystem);
+ const functions = new LlmFunctions(LlmTools, FileSystemRead);
const config: RunAgentConfig = {
agentName: 'SWE',
@@ -36,7 +38,7 @@ describe('agentContext', () => {
metadata: { 'metadata-key': 'metadata-value' },
};
const agentContext: AgentContext = createContext(config);
- agentContext.fileSystem.setWorkingDirectory('./workingDir');
+ agentContext.fileSystem.setWorkingDirectory('./src');
agentContext.memory.memory_key = 'memory_value';
agentContext.functionCallHistory.push({
function_name: 'func',
@@ -63,9 +65,6 @@ describe('agentContext', () => {
const reserialised = serializeContext(deserialised);
expect(serialized).to.be.deep.equal(reserialised);
-
- // test agentContext.resetFileSystemFunction()
- expect(deserialised.fileSystem === deserialised.functions.getFunctionInstanceMap()[FileSystem.name]).to.be.true;
});
});
});
diff --git a/src/agent/agentContextLocalStorage.ts b/src/agent/agentContextLocalStorage.ts
index 48fef937..756f1247 100644
--- a/src/agent/agentContextLocalStorage.ts
+++ b/src/agent/agentContextLocalStorage.ts
@@ -4,12 +4,9 @@ import { LlmFunctions } from '#agent/LlmFunctions';
import { ConsoleCompletedHandler } from '#agent/agentCompletion';
import { AgentContext, AgentLLMs } from '#agent/agentContextTypes';
import { RunAgentConfig } from '#agent/agentRunner';
-import { getCompletedHandler } from '#agent/completionHandlerRegistry';
-import { FileSystem } from '#functions/storage/filesystem';
-import { deserializeLLMs } from '#llm/llmFactory';
+import { FileSystemService } from '#functions/storage/fileSystemService';
import { logger } from '#o11y/logger';
import { currentUser } from '#user/userService/userContext';
-import { appContext } from '../app';
export const agentContextStorage = new AsyncLocalStorage();
@@ -44,30 +41,15 @@ export function addNote(note: string): void {
/**
* @return the filesystem on the current agent context
*/
-export function getFileSystem(): FileSystem {
- if (!agentContextStorage.getStore()) return new FileSystem();
+export function getFileSystem(): FileSystemService {
+ if (!agentContextStorage.getStore()) return new FileSystemService();
const filesystem = agentContextStorage.getStore()?.fileSystem;
if (!filesystem) throw new Error('No file system available on the agent context');
return filesystem;
}
-/**
- * After we have added functions or deserialized an agent, we need to make sure that if the
- * agent has the FileSystem function available that it's the same object as the FileSystem on the agent context
- * @param agent
- */
-function resetFileSystemFunction(agent: AgentContext) {
- // Make sure we have the same FileSystem object on the context and in the functions
- const functions: LlmFunctions = Array.isArray(agent.functions) ? new LlmFunctions(...agent.functions) : agent.functions;
- if (functions.getFunctionClassNames().includes(FileSystem.name)) {
- functions.removeFunctionClass(FileSystem.name);
- functions.addFunctionInstance(agent.fileSystem, FileSystem.name);
- }
- agent.functions = functions;
-}
-
export function createContext(config: RunAgentConfig): AgentContext {
- const fileSystem = new FileSystem(config.fileSystemPath);
+ const fileSystem = new FileSystemService(config.fileSystemPath);
const hilBudget = config.humanInLoop?.budget ?? (process.env.HIL_BUDGET ? parseFloat(process.env.HIL_BUDGET) : 2);
const context: AgentContext = {
agentId: config.resumeAgentId || randomUUID(),
@@ -98,85 +80,5 @@ export function createContext(config: RunAgentConfig): AgentContext {
invoking: [],
lastUpdate: Date.now(),
};
- resetFileSystemFunction(context);
return context;
}
-
-export function serializeContext(context: AgentContext): Record {
- const serialized = {};
-
- for (const key of Object.keys(context) as Array) {
- if (context[key] === undefined) {
- // do nothing
- } else if (context[key] === null) {
- serialized[key] = null;
- }
- // Copy primitive properties across
- else if (typeof context[key] === 'string' || typeof context[key] === 'number' || typeof context[key] === 'boolean') {
- serialized[key] = context[key];
- }
- // Assume arrays (functionCallHistory) can be directly de(serialised) to JSON
- else if (Array.isArray(context[key])) {
- serialized[key] = context[key];
- }
- // Object type check for a toJSON function
- else if (typeof context[key] === 'object' && context[key].toJSON) {
- serialized[key] = context[key].toJSON();
- }
- // Handle Maps (must only contain primitive/simple object values)
- else if (key === 'memory' || key === 'metadata') {
- serialized[key] = context[key];
- } else if (key === 'llms') {
- serialized[key] = {
- easy: context.llms.easy?.getId(),
- medium: context.llms.medium?.getId(),
- hard: context.llms.hard?.getId(),
- xhard: context.llms.xhard?.getId(),
- };
- } else if (key === 'user') {
- serialized[key] = context.user.id;
- } else if (key === 'completedHandler') {
- context.completedHandler.agentCompletedHandlerId();
- }
- // otherwise throw error
- else {
- throw new Error(`Cant serialize context property ${key}`);
- }
- }
- return serialized;
-}
-
-export async function deserializeAgentContext(serialized: Record): Promise {
- const context: Partial = {};
-
- for (const key of Object.keys(serialized)) {
- // copy Array and primitive properties across
- if (Array.isArray(serialized[key]) || typeof serialized[key] === 'string' || typeof serialized[key] === 'number' || typeof serialized[key] === 'boolean') {
- context[key] = serialized[key];
- }
- }
-
- context.fileSystem = new FileSystem().fromJSON(serialized.fileSystem);
- context.functions = new LlmFunctions().fromJSON(serialized.functions ?? (serialized as any).toolbox); // toolbox for backward compat
-
- resetFileSystemFunction(context as AgentContext); // TODO add a test for this
-
- context.memory = serialized.memory;
- context.metadata = serialized.metadata;
- context.llms = deserializeLLMs(serialized.llms);
-
- const user = currentUser();
- if (serialized.user === user.id) context.user = user;
- else context.user = await appContext().userService.getUser(serialized.user);
-
- context.completedHandler = getCompletedHandler(serialized.completedHandler);
-
- // backwards compatability
- if (!context.type) context.type = 'xml';
- if (!context.iterations) context.iterations = 0;
-
- // Need to default empty parameters. Seems to get lost in Firestore
- for (const call of context.functionCallHistory) call.parameters ??= {};
-
- return context as AgentContext;
-}
diff --git a/src/agent/agentContextTypes.ts b/src/agent/agentContextTypes.ts
index 950fd244..e5fdd4cf 100644
--- a/src/agent/agentContextTypes.ts
+++ b/src/agent/agentContextTypes.ts
@@ -1,5 +1,5 @@
import { LlmFunctions } from '#agent/LlmFunctions';
-import { FileSystem } from '#functions/storage/filesystem';
+import { FileSystemService } from '#functions/storage/fileSystemService';
import { FunctionCall, FunctionCallResult, LLM, LlmMessage } from '#llm/llm';
import { User } from '#user/user';
@@ -95,7 +95,7 @@ export interface AgentContext {
/** Pre-configured LLMs by task difficulty level for the agent. Specific LLMs can always be instantiated if required. */
llms: AgentLLMs;
/** Working filesystem */
- fileSystem?: FileSystem | null;
+ fileSystem?: FileSystemService | null;
/** Memory persisted over the agent's executions */
memory: Record;
/** Time of the last database write of the state */
diff --git a/src/agent/agentPromptUtils.ts b/src/agent/agentPromptUtils.ts
index 2bb841ea..8816aec3 100644
--- a/src/agent/agentPromptUtils.ts
+++ b/src/agent/agentPromptUtils.ts
@@ -1,6 +1,6 @@
import { agentContext, getFileSystem } from '#agent/agentContextLocalStorage';
+import { FileSystemService } from '#functions/storage/fileSystemService';
import { FileMetadata, FileStore } from '#functions/storage/filestore';
-import { FileSystem } from '#functions/storage/filesystem';
import { FunctionCallResult } from '#llm/llm';
import { logger } from '#o11y/logger';
@@ -29,7 +29,7 @@ export async function buildToolStatePrompt(): Promise {
*/
function buildFileSystemPrompt(): string {
const functions = agentContext().functions;
- if (!functions.getFunctionClassNames().includes(FileSystem.name)) return '';
+ if (!functions.getFunctionClassNames().includes(FileSystemService.name)) return '';
const fileSystem = getFileSystem();
return `\n
${fileSystem.basePath}
diff --git a/src/agent/agentSerialization.ts b/src/agent/agentSerialization.ts
new file mode 100644
index 00000000..7a993b27
--- /dev/null
+++ b/src/agent/agentSerialization.ts
@@ -0,0 +1,84 @@
+import { LlmFunctions } from '#agent/LlmFunctions';
+import { AgentContext } from '#agent/agentContextTypes';
+import { getCompletedHandler } from '#agent/completionHandlerRegistry';
+import { FileSystemService } from '#functions/storage/fileSystemService';
+import { deserializeLLMs } from '#llm/llmFactory';
+import { currentUser } from '#user/userService/userContext';
+import { appContext } from '../app';
+
+export function serializeContext(context: AgentContext): Record {
+ const serialized = {};
+
+ for (const key of Object.keys(context) as Array) {
+ if (context[key] === undefined) {
+ // do nothing
+ } else if (context[key] === null) {
+ serialized[key] = null;
+ }
+ // Copy primitive properties across
+ else if (typeof context[key] === 'string' || typeof context[key] === 'number' || typeof context[key] === 'boolean') {
+ serialized[key] = context[key];
+ }
+ // Assume arrays (functionCallHistory) can be directly de(serialised) to JSON
+ else if (Array.isArray(context[key])) {
+ serialized[key] = context[key];
+ }
+ // Object type check for a toJSON function
+ else if (typeof context[key] === 'object' && context[key].toJSON) {
+ serialized[key] = context[key].toJSON();
+ }
+ // Handle Maps (must only contain primitive/simple object values)
+ else if (key === 'memory' || key === 'metadata') {
+ serialized[key] = context[key];
+ } else if (key === 'llms') {
+ serialized[key] = {
+ easy: context.llms.easy?.getId(),
+ medium: context.llms.medium?.getId(),
+ hard: context.llms.hard?.getId(),
+ xhard: context.llms.xhard?.getId(),
+ };
+ } else if (key === 'user') {
+ serialized[key] = context.user.id;
+ } else if (key === 'completedHandler') {
+ context.completedHandler.agentCompletedHandlerId();
+ }
+ // otherwise throw error
+ else {
+ throw new Error(`Cant serialize context property ${key}`);
+ }
+ }
+ return serialized;
+}
+
+export async function deserializeAgentContext(serialized: Record): Promise {
+ const context: Partial = {};
+
+ for (const key of Object.keys(serialized)) {
+ // copy Array and primitive properties across
+ if (Array.isArray(serialized[key]) || typeof serialized[key] === 'string' || typeof serialized[key] === 'number' || typeof serialized[key] === 'boolean') {
+ context[key] = serialized[key];
+ }
+ }
+
+ context.fileSystem = new FileSystemService().fromJSON(serialized.fileSystem);
+ context.functions = new LlmFunctions().fromJSON(serialized.functions ?? (serialized as any).toolbox); // toolbox for backward compat
+
+ context.memory = serialized.memory;
+ context.metadata = serialized.metadata;
+ context.llms = deserializeLLMs(serialized.llms);
+
+ const user = currentUser();
+ if (serialized.user === user.id) context.user = user;
+ else context.user = await appContext().userService.getUser(serialized.user);
+
+ context.completedHandler = getCompletedHandler(serialized.completedHandler);
+
+ // backwards compatability
+ if (!context.type) context.type = 'xml';
+ if (!context.iterations) context.iterations = 0;
+
+ // Need to default empty parameters. Seems to get lost in Firestore
+ for (const call of context.functionCallHistory) call.parameters ??= {};
+
+ return context as AgentContext;
+}
diff --git a/src/agent/agentStateService/fileAgentStateService.ts b/src/agent/agentStateService/fileAgentStateService.ts
index 250365f5..af8bfc81 100644
--- a/src/agent/agentStateService/fileAgentStateService.ts
+++ b/src/agent/agentStateService/fileAgentStateService.ts
@@ -1,8 +1,8 @@
import { mkdirSync, readFileSync, readdirSync, writeFileSync } from 'fs';
import { unlinkSync } from 'node:fs';
import { LlmFunctions } from '#agent/LlmFunctions';
-import { deserializeAgentContext, serializeContext } from '#agent/agentContextLocalStorage';
import { AgentContext, AgentRunningState } from '#agent/agentContextTypes';
+import { deserializeAgentContext, serializeContext } from '#agent/agentSerialization';
import { AgentStateService } from '#agent/agentStateService/agentStateService';
import { functionFactory } from '#functionSchema/functionDecorators';
import { logger } from '#o11y/logger';
diff --git a/src/agent/agentStateService/inMemoryAgentStateService.ts b/src/agent/agentStateService/inMemoryAgentStateService.ts
index aca16d27..8f77f5fd 100644
--- a/src/agent/agentStateService/inMemoryAgentStateService.ts
+++ b/src/agent/agentStateService/inMemoryAgentStateService.ts
@@ -1,6 +1,6 @@
import { LlmFunctions } from '#agent/LlmFunctions';
-import { deserializeAgentContext, serializeContext } from '#agent/agentContextLocalStorage';
import { AgentContext, AgentRunningState } from '#agent/agentContextTypes';
+import { deserializeAgentContext, serializeContext } from '#agent/agentSerialization';
import { AgentStateService } from '#agent/agentStateService/agentStateService';
import { functionFactory } from '#functionSchema/functionDecorators';
import { logger } from '#o11y/logger';
diff --git a/src/agent/cachingPythonAgentRunner.ts b/src/agent/cachingPythonAgentRunner.ts
index 611518e7..1b651676 100644
--- a/src/agent/cachingPythonAgentRunner.ts
+++ b/src/agent/cachingPythonAgentRunner.ts
@@ -6,7 +6,7 @@ import { AgentContext } from '#agent/agentContextTypes';
import { AGENT_COMPLETED_NAME, AGENT_REQUEST_FEEDBACK, AGENT_SAVE_MEMORY_CONTENT_PARAM_NAME } from '#agent/agentFunctions';
import { buildFunctionCallHistoryPrompt, buildMemoryPrompt, buildToolStatePrompt, updateFunctionSchemas } from '#agent/agentPromptUtils';
import { AgentExecution, formatFunctionError, formatFunctionResult } from '#agent/agentRunner';
-import { agentHumanInTheLoop, notifySupervisor } from '#agent/humanInTheLoop';
+import { humanInTheLoop, notifySupervisor } from '#agent/humanInTheLoop';
import { convertJsonToPythonDeclaration, extractPythonCode } from '#agent/pythonAgentUtils';
import { getServiceName } from '#fastify/trace-init/trace-init';
import { FUNC_SEP, FunctionSchema, getAllFunctionSchemas } from '#functionSchema/functions';
@@ -105,7 +105,7 @@ export async function runCachingPythonAgent(agent: AgentContext): Promise hilBudget) {
- await agentHumanInTheLoop(`Agent cost has increased by USD\$${costSinceHil.toFixed(2)}`);
+ await humanInTheLoop(`Agent cost has increased by USD\$${costSinceHil.toFixed(2)}`);
costSinceHil = 0;
}
diff --git a/src/agent/xmlAgentRunner.ts b/src/agent/xmlAgentRunner.ts
index 07e1b729..6a94d384 100644
--- a/src/agent/xmlAgentRunner.ts
+++ b/src/agent/xmlAgentRunner.ts
@@ -5,7 +5,7 @@ import { AgentContext } from '#agent/agentContextTypes';
import { AGENT_COMPLETED_NAME, AGENT_REQUEST_FEEDBACK } from '#agent/agentFunctions';
import { buildFunctionCallHistoryPrompt, buildMemoryPrompt, buildToolStatePrompt, updateFunctionSchemas } from '#agent/agentPromptUtils';
import { AgentExecution, formatFunctionError, formatFunctionResult, summariseLongFunctionOutput } from '#agent/agentRunner';
-import { agentHumanInTheLoop, notifySupervisor } from '#agent/humanInTheLoop';
+import { humanInTheLoop, notifySupervisor } from '#agent/humanInTheLoop';
import { getServiceName } from '#fastify/trace-init/trace-init';
import { FunctionSchema, getAllFunctionSchemas } from '#functionSchema/functions';
import { FunctionResponse } from '#llm/llm';
@@ -81,7 +81,7 @@ export async function runXmlAgent(agent: AgentContext): Promise
if (hilCount && countSinceHil === hilCount) {
agent.state = 'hil';
await agentStateService.save(agent);
- await agentHumanInTheLoop(`Agent control loop has performed ${hilCount} iterations`);
+ await humanInTheLoop(`Agent control loop has performed ${hilCount} iterations`);
agent.state = 'agent';
await agentStateService.save(agent);
countSinceHil = 0;
@@ -94,7 +94,7 @@ export async function runXmlAgent(agent: AgentContext): Promise
costSinceHil += newCosts;
logger.debug(`Spent $${costSinceHil.toFixed(2)} since last input. Total cost $${agentContextStorage.getStore().cost.toFixed(2)}`);
if (hilBudget && costSinceHil > hilBudget) {
- await agentHumanInTheLoop(`Agent cost has increased by USD\$${costSinceHil.toFixed(2)}`);
+ await humanInTheLoop(`Agent cost has increased by USD\$${costSinceHil.toFixed(2)}`);
costSinceHil = 0;
}
diff --git a/src/cli/agent.ts b/src/cli/agent.ts
index 8f753a30..ca6ee33c 100644
--- a/src/cli/agent.ts
+++ b/src/cli/agent.ts
@@ -1,7 +1,7 @@
import '#fastify/trace-init/trace-init'; // leave an empty line next so this doesn't get sorted from the first line
import { provideFeedback, resumeCompleted, resumeError, resumeHil, startAgentAndWait } from '#agent/agentRunner';
-import { FileSystem } from '#functions/storage/filesystem';
+import { FileSystemRead } from '#functions/storage/FileSystemRead';
import { Perplexity } from '#functions/web/perplexity';
import { PublicWeb } from '#functions/web/web';
import { ClaudeLLMs } from '#llm/models/anthropic';
@@ -20,9 +20,9 @@ export async function main() {
}
let functions: Array;
- functions = [FileSystem, SoftwareDeveloperAgent, Perplexity, PublicWeb];
+ functions = [FileSystemRead, SoftwareDeveloperAgent, Perplexity, PublicWeb];
functions = [CodeEditingAgent, Perplexity];
- functions = [FileSystem];
+ functions = [FileSystemRead];
const { initialPrompt, resumeAgentId } = parseProcessArgs();
diff --git a/src/cli/gaia.ts b/src/cli/gaia.ts
index 1e8c82b6..72144c66 100644
--- a/src/cli/gaia.ts
+++ b/src/cli/gaia.ts
@@ -4,7 +4,7 @@ import { promises as fs, readFileSync } from 'fs';
import { AgentLLMs } from '#agent/agentContextTypes';
import { AGENT_COMPLETED_PARAM_NAME } from '#agent/agentFunctions';
import { startAgent, startAgentAndWait } from '#agent/agentRunner';
-import { FileSystem } from '#functions/storage/filesystem';
+import { FileSystemRead } from '#functions/storage/FileSystemRead';
import { LlmTools } from '#functions/util';
import { Perplexity } from '#functions/web/perplexity';
import { PublicWeb } from '#functions/web/web';
@@ -101,7 +101,7 @@ async function answerGaiaQuestion(task: GaiaQuestion): Promise {
budget,
count: 100,
},
- functions: [PublicWeb, Perplexity, FileSystem, LlmTools],
+ functions: [PublicWeb, Perplexity, FileSystemRead, LlmTools],
});
const agent = await appContext().agentStateService.load(agentId);
diff --git a/src/cli/swe.ts b/src/cli/swe.ts
index f3e13dbb..fb3d620d 100644
--- a/src/cli/swe.ts
+++ b/src/cli/swe.ts
@@ -6,7 +6,7 @@ import { AgentContext, AgentLLMs } from '#agent/agentContextTypes';
import { RunAgentConfig } from '#agent/agentRunner';
import { runAgentWorkflow } from '#agent/agentWorkflowRunner';
import { GitLab } from '#functions/scm/gitlab';
-import { FileSystem } from '#functions/storage/filesystem';
+import { FileSystemRead } from '#functions/storage/FileSystemRead';
import { Perplexity } from '#functions/web/perplexity';
import { ClaudeLLMs } from '#llm/models/anthropic';
import { ClaudeVertexLLMs } from '#llm/models/anthropic-vertex';
@@ -32,7 +32,7 @@ async function main() {
const config: RunAgentConfig = {
agentName: 'cli-SWE',
llms,
- functions: [FileSystem, CodeEditingAgent, Perplexity],
+ functions: [FileSystemRead, CodeEditingAgent, Perplexity],
initialPrompt: initialPrompt.trim(),
resumeAgentId,
};
diff --git a/src/cli/swebench.ts b/src/cli/swebench.ts
index 96208131..89aa0bdc 100644
--- a/src/cli/swebench.ts
+++ b/src/cli/swebench.ts
@@ -7,7 +7,7 @@ import { RunAgentConfig, startAgent, startAgentAndWait } from '#agent/agentRunne
import { runAgentWorkflow } from '#agent/agentWorkflowRunner';
import { shutdownTrace } from '#fastify/trace-init/trace-init';
import { GitLab } from '#functions/scm/gitlab';
-import { FileSystem } from '#functions/storage/filesystem';
+import { FileSystemService } from '#functions/storage/fileSystemService';
import { LlmTools } from '#functions/util';
import { Perplexity } from '#functions/web/perplexity';
import { PublicWeb } from '#functions/web/web';
diff --git a/src/cli/util.ts b/src/cli/util.ts
index 87caa3dd..bc357cf2 100644
--- a/src/cli/util.ts
+++ b/src/cli/util.ts
@@ -5,7 +5,7 @@ import { agentContextStorage, createContext, getFileSystem, llms } from '#agent/
import { Jira } from '#functions/jira';
import { GitLab } from '#functions/scm/gitlab';
-import { FileSystem } from '#functions/storage/filesystem';
+import { FileSystemService } from '#functions/storage/fileSystemService';
import { Claude3_Opus, ClaudeLLMs } from '#llm/models/anthropic';
import { Claude3_5_Sonnet_Vertex, Claude3_Haiku_Vertex, Claude3_Sonnet_Vertex, ClaudeVertexLLMs } from '#llm/models/anthropic-vertex';
import { GPT4o } from '#llm/models/openai';
@@ -37,7 +37,7 @@ const utilLLMs: AgentLLMs = {
async function main() {
await appContext().userService.ensureSingleUser();
const functions = new LlmFunctions();
- functions.addFunctionClass(FileSystem);
+ functions.addFunctionClass(FileSystemService);
const config: RunAgentConfig = {
agentName: 'util',
diff --git a/src/functionRegistry.ts b/src/functionRegistry.ts
index 38d00b84..e4b31024 100644
--- a/src/functionRegistry.ts
+++ b/src/functionRegistry.ts
@@ -3,7 +3,7 @@ import { ImageGen } from '#functions/image';
import { Jira } from '#functions/jira';
import { GitHub } from '#functions/scm/github';
import { GitLab } from '#functions/scm/gitlab';
-import { FileSystem } from '#functions/storage/filesystem';
+import { FileSystemRead } from '#functions/storage/FileSystemRead';
import { LocalFileStore } from '#functions/storage/localFileStore';
import { LlmTools } from '#functions/util';
import { Perplexity } from '#functions/web/perplexity';
@@ -19,7 +19,8 @@ import { SoftwareDeveloperAgent } from '#swe/softwareDeveloperAgent';
export function functionRegistry(): Array any> {
return [
CodeEditingAgent,
- FileSystem,
+ FileSystemRead,
+ // FileSystemWrite,
LocalFileStore,
GitLab,
// GitHub, // Error: More than one function classes found implementing SourceControlManagement
diff --git a/src/functions/cloud/google-cloud.ts b/src/functions/cloud/google-cloud.ts
index d42b8670..3a9c2565 100644
--- a/src/functions/cloud/google-cloud.ts
+++ b/src/functions/cloud/google-cloud.ts
@@ -1,5 +1,5 @@
import { agentContext } from '#agent/agentContextLocalStorage';
-import { waitForConsoleInput } from '#agent/humanInTheLoop';
+import { humanInTheLoop, waitForConsoleInput } from '#agent/humanInTheLoop';
import { func, funcClass } from '#functionSchema/functionDecorators';
import { execCommand, failOnError } from '#utils/exec';
@@ -23,14 +23,29 @@ export class GoogleCloud {
/**
* Query resource information by executing the gcloud command line tool. This must ONLY be used for querying information, and MUST NOT update or modify resources.
* Must have the --project= argument.
- * @param gcloudQueryCommand The gcloud query command to execute (
+ * @param gcloudQueryCommand The gcloud query command to execute
* @returns the console output if the exit code is 0, else throws the console output
*/
@func()
async executeGcloudCommandQuery(gcloudQueryCommand: string): Promise {
- await waitForConsoleInput(`Agent "${agentContext().name}" is requesting to run the command ${gcloudQueryCommand}`);
if (!gcloudQueryCommand.includes('--project='))
throw new Error('When calling executeGcloudCommandQuery the gcloudQueryCommand parameter must include the --project= argument');
+
+ // Whitelist list, describe and get-iam-policy commands, otherwise require human-in-the-loop approval
+ const args = gcloudQueryCommand.split(' ');
+ if (args[1] === 'alpha' || args[1] === 'beta') {
+ args.splice(1, 1);
+ }
+
+ let isQuery = false;
+ for (const i of [2, 3, 4]) {
+ if (args[i].startsWith('list') || args[i] === 'describe' || args[i] === 'get-iam-policy') isQuery = true;
+ }
+
+ if (!isQuery) {
+ await humanInTheLoop(`Agent "${agentContext().name}" is requesting to run the command ${gcloudQueryCommand}`);
+ }
+
const result = await execCommand(gcloudQueryCommand);
if (result.exitCode > 0) throw new Error(`Error running ${gcloudQueryCommand}. ${result.stdout}${result.stderr}`);
return result.stdout;
diff --git a/src/functions/scm/git.ts b/src/functions/scm/git.ts
index 91fe2444..ac97fdbe 100644
--- a/src/functions/scm/git.ts
+++ b/src/functions/scm/git.ts
@@ -1,6 +1,6 @@
import util from 'util';
import { funcClass } from '#functionSchema/functionDecorators';
-import { FileSystem } from '#functions/storage/filesystem';
+import { FileSystemService } from '#functions/storage/fileSystemService';
import { logger } from '#o11y/logger';
import { span } from '#o11y/trace';
import { execCmd, execCommand, failOnError } from '#utils/exec';
@@ -12,7 +12,7 @@ export class Git implements VersionControlSystem {
/** The branch name before calling switchToBranch. This enables getting the diff between the current and previous branch */
previousBranch: string | undefined;
- constructor(private fileSystem: FileSystem) {}
+ constructor(private fileSystem: FileSystemService) {}
async clone(repoURL: string, commitOrBranch = ''): Promise {
const { exitCode, stdout, stderr } = await execCommand(`git clone ${repoURL} ${commitOrBranch}`);
diff --git a/src/functions/storage/FileSystemRead.ts b/src/functions/storage/FileSystemRead.ts
new file mode 100644
index 00000000..dc716bfe
--- /dev/null
+++ b/src/functions/storage/FileSystemRead.ts
@@ -0,0 +1,179 @@
+import { getFileSystem } from '#agent/agentContextLocalStorage';
+import { func, funcClass } from '#functionSchema/functionDecorators';
+
+/**
+ * Provides functions for LLMs to access the file system. Tools should generally use the functions as
+ * - They are automatically included in OpenTelemetry tracing
+ * - They use the working directory, so Sophia can perform its actions outside the process running directory.
+ *
+ * The FileSystem is constructed with the basePath property which is like a virtual root.
+ * Then the workingDirectory property is relative to the basePath.
+ *
+ * The functions which list/search filenames should return the paths relative to the workingDirectory.
+ *
+ * By default, the basePath is the current working directory of the process.
+ */
+@funcClass(__filename)
+export class FileSystemRead {
+ /**
+ * @returns the full path of the working directory on the filesystem
+ */
+ @func()
+ getWorkingDirectory(): string {
+ return getFileSystem().getWorkingDirectory();
+ }
+
+ /**
+ * Set the working directory. The dir argument may be an absolute filesystem path, otherwise relative to the current working directory.
+ * If the dir starts with / it will first be checked as an absolute directory, then as relative path to the working directory.
+ * @param dir the new working directory
+ */
+ @func()
+ setWorkingDirectory(dir: string): void {
+ getFileSystem().setWorkingDirectory(dir);
+ }
+
+ /**
+ * Returns the file contents of all the files under the provided directory path
+ * @param dirPath the directory to return all the files contents under
+ * @returns the contents of the file(s) as a Map keyed by the file path
+ */
+ @func()
+ async getFileContentsRecursively(dirPath: string, useGitIgnore = true): Promise