Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 3 additions & 2 deletions shared/files/fileSystemService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -125,11 +125,12 @@ export interface IFileSystemService {
/**
* Gets the contents of a list of files, returning a formatted XML string of all file contents
* @param {Array<string>} filePaths The files paths to read the contents of
* @param {boolean} includeTokenCount Include the token count as an attribute
* @returns {Promise<string>} the contents of the file(s) in format <file_contents path="dir/file1">file1 contents</file_contents><file_contents path="dir/file2">file2 contents</file_contents>
*/
readFilesAsXml(filePaths: string | string[]): Promise<string>;
readFilesAsXml(filePaths: string | string[], includeTokenCount?: boolean): Promise<string>;

formatFileContentsAsXml(fileContents: Map<string, string>): string;
formatFileContentsAsXml(fileContents: Map<string, string>, includeTokenCount?: boolean): Promise<string>;

/**
* Check if a file exists. A filePath starts with / is it relative to FileSystem.basePath, otherwise its relative to FileSystem.workingDirectory
Expand Down
42 changes: 20 additions & 22 deletions src/agent/agentContextService/agentContextService.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -346,13 +346,11 @@ export function runAgentStateServiceTests(
expect(loadedContext.lastUpdate).to.be.greaterThan(savedTime1);
});

// Modified test to expect NotFound error
it('should throw NotFound when loading a non-existent agent', async () => {
it('should return null when loading a non-existent agent', async () => {
const id = agentId();
await expect(service.load(id)).to.be.rejectedWith(NotFound);
expect(await service.load(id)).to.be.null;
});

// Added test for NotAllowed error
it('should throw NotAllowed when trying to load an agent belonging to another user', async () => {
const idForOtherUser = agentId();
setCurrentUser(otherUser);
Expand Down Expand Up @@ -394,8 +392,8 @@ export function runAgentStateServiceTests(

// Expect the save operation to be rejected
await expect(service.save(childContext)).to.be.rejected;
// Verify the child was not saved due to the rejection (load should throw NotFound)
await expect(service.load(childId)).to.be.rejectedWith(NotFound);
// Verify the child was not saved due to the rejection
expect(await service.load(childId)).to.be.null;
});
});

Expand Down Expand Up @@ -607,17 +605,17 @@ export function runAgentStateServiceTests(
setCurrentUser(testUser); // Ensure correct user context
await service.delete([agentIdCompleted, agentIdError]);

// Verify the specified agents are deleted (load should now throw NotFound)
await expect(service.load(agentIdCompleted)).to.be.rejectedWith(NotFound);
await expect(service.load(agentIdError)).to.be.rejectedWith(NotFound);
// Verify the specified agents are deleted
expect(await service.load(agentIdCompleted)).to.be.null;
expect(await service.load(agentIdError)).to.be.null;
});

it('should NOT delete agents belonging to other users', async () => {
setCurrentUser(testUser); // Ensure correct user context
await service.delete([agentIdCompleted, otherUserAgentId]);

// Verify testUser's agent is deleted (load should now throw NotFound)
await expect(service.load(agentIdCompleted)).to.be.rejectedWith(NotFound);
// Verify testUser's agent is deleted
expect(await service.load(agentIdCompleted)).to.be.null;
// Verify otherUser's agent is NOT deleted (load should now throw NotAllowed)
await expect(service.load(otherUserAgentId)).to.be.rejectedWith(NotAllowed);
});
Expand All @@ -626,8 +624,8 @@ export function runAgentStateServiceTests(
setCurrentUser(testUser); // Ensure correct user context
await service.delete([agentIdCompleted, executingAgentId]);

// Verify the non-executing agent is deleted (load should now throw NotFound)
await expect(service.load(agentIdCompleted)).to.be.rejectedWith(NotFound);
// Verify the non-executing agent is deleted
expect(await service.load(agentIdCompleted)).to.be.null;
// Verify the executing agent is NOT deleted (load should NOT throw NotFound, but should return the agent)
// Note: The delete logic filters out executing agents *before* attempting deletion.
// So, loading the executing agent after the delete call should still succeed.
Expand All @@ -636,15 +634,15 @@ export function runAgentStateServiceTests(
expect(executingAgentAfterDelete.agentId).to.equal(executingAgentId);
});

it('should delete a parent agent AND its children when parent ID is provided (if parent is deletable)', async () => {
it('should delete a parent agent and its children when parent ID is provided (if parent is deletable)', async () => {
setCurrentUser(testUser); // Ensure correct user context
// Delete the parent (which is in 'completed' state)
await service.delete([parentIdCompleted]);

// Verify parent and all children are deleted (load should now throw NotFound)
await expect(service.load(parentIdCompleted)).to.be.rejectedWith(NotFound);
await expect(service.load(childId1)).to.be.rejectedWith(NotFound);
await expect(service.load(childId2)).to.be.rejectedWith(NotFound);
// Verify parent and all children are deleted
expect(await service.load(parentIdCompleted)).to.be.null;
expect(await service.load(childId1)).to.be.null;
expect(await service.load(childId2)).to.be.null;
});

it('should NOT delete child agents if only child ID is provided (due to implementation filter)', async () => {
Expand All @@ -670,15 +668,15 @@ export function runAgentStateServiceTests(

it('should handle non-existent IDs gracefully without error', async () => {
const nonExistentId = agentId();
setCurrentUser(testUser); // Ensure correct user context
setCurrentUser(testUser);

// Attempt to delete an existing deletable agent and a non-existent one
await expect(service.delete([agentIdCompleted, nonExistentId])).to.not.be.rejected;

// Verify the existing deletable agent was actually deleted (load should now throw NotFound)
await expect(service.load(agentIdCompleted)).to.be.rejectedWith(NotFound);
// Verify the existing deletable agent was actually deleted
expect(await service.load(agentIdCompleted)).to.be.null;
// Verify the non-existent ID still results in NotFound on load
await expect(service.load(nonExistentId)).to.be.rejectedWith(NotFound);
expect(await service.load(nonExistentId)).to.be.null;
});
});

Expand Down
10 changes: 5 additions & 5 deletions src/agent/agentPromptUtils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -53,11 +53,11 @@ export async function buildFileSystemTreePrompt(): Promise<string> {
// focusing on folder structure and collapsed state.
const treeString = await generateFileSystemTreeWithSummaries(summaries, false, collapsedFolders);

if (!treeString.trim()) {
return '\n<file_system_tree>\n<!-- File system is empty or all folders are collapsed at the root. -->\n</file_system_tree>\n';
}
if (!treeString.trim()) return '\n<file_system_tree>\n<!-- File system is empty or all folders are collapsed at the root. -->\n</file_system_tree>\n';

const tokens = await countTokens(treeString);

return `\n<file_system_tree>
return `\n<file_system_tree tokens="${tokens}">
${treeString}
</file_system_tree>\n<file_system_tree_collapsed_folders>\n${collapsedFolders.join('\n')}\n</file_system_tree_collapsed_folders>`;
} catch (error) {
Expand Down Expand Up @@ -119,7 +119,7 @@ async function buildLiveFilesPrompt(): Promise<string> {
}

return `\n${rulesFilesPrompt}<live_files>
${await getFileSystem().readFilesAsXml(liveFiles)}
${await getFileSystem().readFilesAsXml(liveFiles, true)}
</live_files>
`;
}
Expand Down
4 changes: 2 additions & 2 deletions src/agent/autonomous/codegen/codeGenAgentRunner.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ import {
} from '#agent/autonomous/autonomousAgentRunner';
import { convertTypeScriptToPython } from '#agent/autonomous/codegen/pythonCodeGenUtils';
import { AGENT_REQUEST_FEEDBACK, AgentFeedback } from '#agent/autonomous/functions/agentFeedback';
import { AGENT_COMPLETED_NAME, AGENT_SAVE_MEMORY } from '#agent/autonomous/functions/agentFunctions';
import { AGENT_COMPLETED_NAME, AGENT_MEMORY } from '#agent/autonomous/functions/agentFunctions';
import { appContext, initInMemoryApplicationContext } from '#app/applicationContext';
import { TEST_FUNC_NOOP, TEST_FUNC_SKY_COLOUR, TEST_FUNC_SUM, TEST_FUNC_THROW_ERROR, TestFunctions } from '#functions/testFunctions';
import { MockLLM, mockLLM, mockLLMs } from '#llm/services/mock-llm';
Expand All @@ -30,7 +30,7 @@ const PY_TEST_FUNC_NOOP = `await ${TEST_FUNC_NOOP}()`;
const PY_TEST_FUNC_SKY_COLOUR = `await ${TEST_FUNC_SKY_COLOUR}()`;
const PY_TEST_FUNC_SUM = (num1, num2) => `await ${TEST_FUNC_SUM}(${num1}, ${num2})`;
const PY_TEST_FUNC_THROW_ERROR = `await ${TEST_FUNC_THROW_ERROR}()`;
const PY_SET_MEMORY = (key, content) => `await ${AGENT_SAVE_MEMORY}("${key}", "${content}")`;
const PY_SET_MEMORY = (key, content) => `await ${AGENT_MEMORY}("SAVE", "${key}", "${content}")`;

const PYTHON_CODE_PLAN = (pythonCode: string) => `<response>\n<plan>Run some code</plan>\n<python-code>${pythonCode}</python-code>\n</response>`;
const REQUEST_FEEDBACK_FUNCTION_CALL_PLAN = (feedback) =>
Expand Down
28 changes: 24 additions & 4 deletions src/agent/autonomous/functions/agentFunctions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import { agentContext } from '#agent/agentContextLocalStorage';
import { func, funcClass } from '#functionSchema/functionDecorators';
import { logger } from '#o11y/logger';

export const AGENT_SAVE_MEMORY = 'Agent_saveMemory';
export const AGENT_MEMORY = 'Agent_memory';

export const AGENT_COMPLETED_NAME = 'Agent_completed';

Expand Down Expand Up @@ -30,7 +30,7 @@ export class Agent {
* @param {string} key A descriptive identifier (alphanumeric and underscores allowed, under 30 characters) for the new memory contents explaining the source of the content. This must not exist in the current memory.
* @param {string} content The plain text contents to store in the working memory
*/
@func()
// @func()
async saveMemory(key: string, content: string): Promise<void> {
if (!key || !key.trim().length) throw new Error('Memory key must be provided');
if (!content || !content.trim().length) throw new Error('Memory content must be provided');
Expand All @@ -44,7 +44,7 @@ export class Agent {
* Note this will over-write any existing memory content
* @param {string} key An existing key in the memory contents to update the contents of.
*/
@func()
// @func()
async deleteMemory(key: string): Promise<void> {
const memory = agentContext().memory;
if (!memory[key]) logger.info(`deleteMemory key doesn't exist: ${key}`);
Expand All @@ -56,11 +56,31 @@ export class Agent {
* @param {string} key An existing key in the memory to retrieve.
* @return {string} The memory contents
*/
@func()
// @func()
async getMemory(key: string): Promise<string> {
if (!key) throw new Error(`Parameter "key" must be provided. Was ${key}`);
const memory = agentContext().memory;
if (!memory[key]) throw new Error(`Memory key ${key} does not exist`);
return memory[key];
}

/**
* Interacts with the memory entries
* @param operation 'SAVE', 'DELETE', or 'GET'
* @param key The memory key to save, delete, or get
* @param content The content to save to the memory (when operation is 'SAVE')
* @returns void, or string when operation is 'GET'
*/
@func()
async memory(operation: 'SAVE' | 'DELETE' | 'GET', key: string, content?: string): Promise<undefined | string> {
if (operation === 'SAVE') {
return this.saveMemory(key, content) as undefined;
}
if (operation === 'DELETE') {
return this.deleteMemory(key) as undefined;
}
if (operation === 'GET') {
return this.getMemory(key);
}
}
}
5 changes: 3 additions & 2 deletions src/cli/cli.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import { systemDir } from '#app/appDirs';
import { FastMediumLLM } from '#llm/multi-agent/fastMedium';
import { MAD_Balanced, MAD_Fast, MAD_SOTA } from '#llm/multi-agent/reasoning-debate';
import { Claude4_Opus_Vertex } from '#llm/services/anthropic-vertex';
import { cerebrasQwen3_235b } from '#llm/services/cerebras';
import { cerebrasQwen3_235b_Thinking, cerebrasQwen3_Coder } from '#llm/services/cerebras';
import { defaultLLMs } from '#llm/services/defaultLlms';
import { openAIo3 } from '#llm/services/openai';
import { perplexityDeepResearchLLM, perplexityLLM, perplexityReasoningProLLM } from '#llm/services/perplexity-llm';
Expand All @@ -18,7 +18,8 @@ export const LLM_CLI_ALIAS: Record<string, () => LLM> = {
h: () => defaultLLMs().hard,
xh: () => defaultLLMs().xhard,
fm: () => new FastMediumLLM(),
f: cerebrasQwen3_235b,
f: cerebrasQwen3_235b_Thinking,
cc: cerebrasQwen3_Coder,
x: xai_Grok4,
o3: openAIo3,
madb: MAD_Balanced,
Expand Down
5 changes: 5 additions & 0 deletions src/functionRegistry.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import { GoogleCloud } from '#functions/cloud/google/google-cloud';
import { CommandLineInterface } from '#functions/commandLine';
import { CustomFunctions } from '#functions/customFunctions';
import { DeepThink } from '#functions/deepThink';
import { GoogleCalendar } from '#functions/googleCalendar';
import { ImageGen } from '#functions/image';
import { Jira } from '#functions/jira';
import { LlmTools } from '#functions/llmTools';
Expand All @@ -18,6 +19,7 @@ import { FileSystemWrite } from '#functions/storage/fileSystemWrite';
import { LocalFileStore } from '#functions/storage/localFileStore';
import { Perplexity } from '#functions/web/perplexity';
import { PublicWeb } from '#functions/web/web';
import { SlackAPI } from '#modules/slack/slackApi';
import { type ToolType, hasGetToolType } from '#shared/agent/functions';
import { Slack } from '#slack/slack';
import { CodeEditingAgent } from '#swe/codeEditingAgent';
Expand Down Expand Up @@ -56,6 +58,9 @@ const FUNCTIONS = [
TypescriptTools,
BigQuery,
CustomFunctions,
GoogleCalendar,
SlackAPI,

// Add your own classes below this line
];

Expand Down
4 changes: 3 additions & 1 deletion src/functions/scm/git.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import { getFileSystem } from '#agent/agentContextLocalStorage';
import { func, funcClass } from '#functionSchema/functionDecorators';
import { logger } from '#o11y/logger';
import { span } from '#o11y/trace';
import { arg, execCmd, execCommand, failOnError } from '#utils/exec';
import { arg, execCmd, execCommand, failOnError, formatAnsiWithMarkdownLinks } from '#utils/exec';

import type { IFileSystemService } from '#shared/files/fileSystemService';
import type { Commit, VersionControlSystem } from '#shared/scm/versionControlSystem';
Expand Down Expand Up @@ -74,6 +74,8 @@ export class Git implements VersionControlSystem {

// The fix is to execute a specific commit command that targets only the added files.
const commitResult = await execCommand(`git commit -m ${arg(commitMessage)} -- ${filesToAdd}`);
// Pre-commit hooks make call lint/commit commands with
commitResult.stdout = formatAnsiWithMarkdownLinks(commitResult.stdout);
failOnError(`Failed to commit changes for files: ${files.join(', ')}`, commitResult);
}

Expand Down
16 changes: 9 additions & 7 deletions src/functions/storage/fileSystemService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import { TYPEDAI_FS } from '#app/appDirs';
import { parseArrayParameterValue } from '#functionSchema/functionUtils';
import { LlmTools } from '#functions/llmTools';
import { Git } from '#functions/scm/git';
import { countTokens } from '#llm/tokens';
import { logger } from '#o11y/logger';
import { getActiveSpan } from '#o11y/trace';
import { FileNotFound } from '#shared/errors';
Expand Down Expand Up @@ -479,23 +480,24 @@ export class FileSystemService implements IFileSystemService {

/**
* Gets the contents of a list of files, returning a formatted XML string of all file contents
* @param {Array<string>} filePaths The files paths to read the contents of
* @param {Array<string | string[]>} filePaths The files paths to read the contents of
* @returns {Promise<string>} the contents of the file(s) in format <file_contents path="dir/file1">file1 contents</file_contents><file_contents path="dir/file2">file2 contents</file_contents>
*/
async readFilesAsXml(filePaths: string | string[]): Promise<string> {
async readFilesAsXml(filePaths: string | string[], includeTokenCount = false): Promise<string> {
if (!Array.isArray(filePaths)) {
filePaths = parseArrayParameterValue(filePaths);
}
const fileContents: Map<string, string> = await this.readFiles(filePaths);
return this.formatFileContentsAsXml(fileContents);
return this.formatFileContentsAsXml(fileContents, includeTokenCount);
}

formatFileContentsAsXml(fileContents: Map<string, string>): string {
async formatFileContentsAsXml(fileContents: Map<string, string>, includeTokenCount = false): Promise<string> {
let result = '';

fileContents.forEach((contents, path) => {
result += `<file_content file_path="${path}">${formatXmlContent(contents)}</file_content>\n`;
});
for (const [path, contents] of fileContents) {
const tokens = includeTokenCount ? ` tokens="${await countTokens(contents)}"` : '';
result += `<file_content file_path="${path}"${tokens}>${formatXmlContent(contents)}</file_content>\n`;
}
return result;
}

Expand Down
4 changes: 2 additions & 2 deletions src/llm/multi-agent/blueberry.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { BaseLLM } from '#llm/base-llm';
import { getLLM } from '#llm/llmFactory';
import { cerebrasQwen3_235b } from '#llm/services/cerebras';
import { cerebrasQwen3_235b_Thinking } from '#llm/services/cerebras';
import { vertexGemini_2_5_Flash } from '#llm/services/vertexai';
import { logger } from '#o11y/logger';
import type { GenerateTextOptions, LLM } from '#shared/llm/llm.model';
Expand Down Expand Up @@ -101,7 +101,7 @@ export class Blueberry extends BaseLLM {
}
}
// if (!this.llms) this.llms = [Claude3_5_Sonnet_Vertex(), GPT4o(), Gemini_1_5_Pro(), Claude3_5_Sonnet_Vertex(), fireworksLlama3_405B()];
let llm = cerebrasQwen3_235b();
let llm = cerebrasQwen3_235b_Thinking();
// llm = groqLlama3_1_70B();
llm = vertexGemini_2_5_Flash();
if (!this.llms) this.llms = [llm, llm, llm, llm, llm];
Expand Down
4 changes: 2 additions & 2 deletions src/llm/multi-agent/cepo.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { BaseLLM } from '#llm/base-llm';
import { cerebrasQwen3_32b, cerebrasQwen3_235b } from '#llm/services/cerebras';
import { cerebrasQwen3_32b, cerebrasQwen3_235b_Thinking } from '#llm/services/cerebras';
import { logger } from '#o11y/logger';
import { withActiveSpan } from '#o11y/trace';
import { type GenerateTextOptions, type LLM, type LlmMessage, assistant, lastText, user } from '#shared/llm/llm.model';
Expand Down Expand Up @@ -73,7 +73,7 @@ const sotaConfig: CePOConfig = {

// https://github.com/codelion/optillm/blob/main/optillm/cepo/README.md

export function CePO_FastMedium(llmProvider: () => LLM = () => cerebrasQwen3_235b(), name?: string): LLM {
export function CePO_FastMedium(llmProvider: () => LLM = () => cerebrasQwen3_235b_Thinking(), name?: string): LLM {
return new CePO_LLM(() => new FastMediumLLM(), 'CePO (FastMedium)', limitedConfig);
}

Expand Down
8 changes: 4 additions & 4 deletions src/llm/multi-agent/fastMedium.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { cerebrasQwen3_235b } from '#llm/services/cerebras';
import { cerebrasQwen3_235b_Thinking } from '#llm/services/cerebras';
import { vertexGemini_2_5_Flash } from '#llm/services/vertexai';
import { countTokens } from '#llm/tokens';
import { logger } from '#o11y/logger';
Expand All @@ -7,7 +7,7 @@ import { BaseLLM } from '../base-llm';

/**
* LLM implementation for medium level LLM using a fast provider if available and applicable, else falling back to the standard medium LLM
* https://artificialanalysis.ai/?models=gemini-2-5-flash%2Cgemini-2-5-flash-reasoning%2Cgroq_qwen3-32b-instruct-reasoning%2Cgroq_qwen3-32b-instruct%2Ccerebras_qwen3-32b-instruct-reasoning&endpoints=groq_qwen3-32b-instruct%2Cgroq_qwen3-32b-instruct-reasoning%2Ccerebras_qwen3-235b-a22b-instruct%2Ccerebras_qwen3-32b-instruct-reasoning%2Ccerebras_qwen3-235b-a22b-instruct-reasoning
* https://artificialanalysis.ai/?models=gemini-2-5-flash%2Cgemini-2-5-flash-reasoning%2Cgroq_qwen3-32b-instruct-reasoning%2Cgroq_qwen3-32b-instruct%2Ccerebras_qwen3-32b-instruct-reasoning&endpoints=groq_qwen3-32b-instruct-reasoning%2Ccerebras_qwen3-235b-a22b-instruct-2507%2Ccerebras_qwen3-235b-a22b-instruct-2507-reasoning%2Ccerebras_qwen3-32b-instruct-reasoning
*/
export class FastMediumLLM extends BaseLLM {
private readonly providers: LLM[];
Expand All @@ -20,7 +20,7 @@ export class FastMediumLLM extends BaseLLM {
outputCost: 0,
totalCost: 0,
}));
this.providers = [cerebrasQwen3_235b(), vertexGemini_2_5_Flash({ thinking: 'high' })];
this.providers = [cerebrasQwen3_235b_Thinking(), vertexGemini_2_5_Flash({ thinking: 'high' })];
this.cerebras = this.providers[0];
this.gemini = this.providers[1];

Expand Down Expand Up @@ -62,7 +62,7 @@ export class FastMediumLLM extends BaseLLM {
if (tokens && this.cerebras.isConfigured() && tokens < this.cerebras.getMaxInputTokens() * 0.4)
return await this.cerebras.generateMessage(messages, opts);
} catch (e) {
logger.warn(`Error calling fast medium LLM with ${tokens} tokens: ${e.message}`);
logger.warn(e, `Error calling fast medium LLM with ${tokens} tokens: ${e.message}`);
}
return await this.gemini.generateMessage(messages, opts);
}
Expand Down
Loading
Loading