diff --git a/package-lock.json b/package-lock.json index 2f90228f..639d1715 100644 --- a/package-lock.json +++ b/package-lock.json @@ -44,7 +44,7 @@ "@grpc/grpc-js": "^1.12.6", "@microsoft/tiktokenizer": "^1.0.8", "@mistralai/mistralai": "^1.7.1", - "@modelcontextprotocol/sdk": "^1.13.2", + "@modelcontextprotocol/sdk": "^1.17.1", "@mozilla/readability": "^0.6.0", "@octokit/request": "^5.1.0", "@openrouter/ai-sdk-provider": "^0.4.5", @@ -5109,9 +5109,9 @@ "integrity": "sha512-Y28PR25bHXUg88kCV7nivXrP2Nj2RueZ3/l/jdx6J9f8J4nsEGcgX0Qe6lt7Pa+J79+kPiJU3LguR6O/6zrLOw==" }, "node_modules/@modelcontextprotocol/sdk": { - "version": "1.13.2", - "resolved": "https://registry.npmjs.org/@modelcontextprotocol/sdk/-/sdk-1.13.2.tgz", - "integrity": "sha512-Vx7qOcmoKkR3qhaQ9qf3GxiVKCEu+zfJddHv6x3dY/9P6+uIwJnmuAur5aB+4FDXf41rRrDnOEGkviX5oYZ67w==", + "version": "1.17.1", + "resolved": "https://registry.npmjs.org/@modelcontextprotocol/sdk/-/sdk-1.17.1.tgz", + "integrity": "sha512-CPle1OQehbWqd25La9Ack5B07StKIxh4+Bf19qnpZKJC1oI22Y0czZHbifjw1UoczIfKBwBDAp/dFxvHG13B5A==", "license": "MIT", "dependencies": { "ajv": "^6.12.6", @@ -5119,6 +5119,7 @@ "cors": "^2.8.5", "cross-spawn": "^7.0.5", "eventsource": "^3.0.2", + "eventsource-parser": "^3.0.0", "express": "^5.0.1", "express-rate-limit": "^7.5.0", "pkce-challenge": "^5.0.0", diff --git a/package.json b/package.json index 4d0214a1..483b2619 100644 --- a/package.json +++ b/package.json @@ -49,6 +49,7 @@ "test:postgres": " node --env-file=variables/test.env ./node_modules/mocha/bin/mocha -r esbuild-register -r \"./src/test/testSetup.ts\" \"src/modules/postgres/*.test.ts\" --timeout 10000", "test:mongo": " node --env-file=variables/test.env ./node_modules/mocha/bin/mocha -r esbuild-register -r \"./src/test/testSetup.ts\" \"src/modules/mongo/*.test.ts\" --timeout 10000", "test:db": " node --env-file=variables/test.env ./node_modules/mocha/bin/mocha -r esbuild-register -r \"./src/test/testSetup.ts\" \"src/modules/{firestore,mongo,postgres}/*.test.ts\" --timeout 10000", + "test:single": " node --env-file=variables/test.env ./node_modules/mocha/bin/mocha -r esbuild-register -r \"./src/test/testSetup.ts\" --timeout 10000 --exit", "test:ci:firestore": "firebase emulators:exec --only firestore \"npm run test:firestore\"", "test:ci:postgres": " npm run test:postgres", "test:ci:mongo": " npm run test:mongo", @@ -99,7 +100,7 @@ "@grpc/grpc-js": "^1.12.6", "@microsoft/tiktokenizer": "^1.0.8", "@mistralai/mistralai": "^1.7.1", - "@modelcontextprotocol/sdk": "^1.13.2", + "@modelcontextprotocol/sdk": "^1.17.1", "@mozilla/readability": "^0.6.0", "@octokit/request": "^5.1.0", "@openrouter/ai-sdk-provider": "^0.4.5", diff --git a/src/agent/agentContext.test.ts b/src/agent/agentContext.test.ts index 70bd33c2..3a89f6f7 100644 --- a/src/agent/agentContext.test.ts +++ b/src/agent/agentContext.test.ts @@ -6,7 +6,7 @@ import { deserializeContext, serializeContext } from '#agent/agentSerialization' import type { RunAgentConfig } from '#agent/autonomous/autonomousAgentRunner'; import { appContext } from '#app/applicationContext'; import { LlmTools } from '#functions/llmTools'; -import { openaiGPT41 } from '#llm/services/openai'; +import { openaiGPT5 } from '#llm/services/openai'; import type { AgentContext } from '#shared/agent/agent.model'; import { functionRegistry } from '../functionRegistry'; @@ -19,10 +19,10 @@ describe('agentContext', () => { describe('serialisation', () => { it('should be be identical after serialisation and deserialization', async () => { const llms = { - easy: openaiGPT41(), - medium: openaiGPT41(), - hard: openaiGPT41(), - xhard: openaiGPT41(), + easy: openaiGPT5(), + medium: openaiGPT5(), + hard: openaiGPT5(), + xhard: openaiGPT5(), }; // We want to check that the FileSystem gets re-added by the resetFileSystemFunction function const functions = new LlmFunctionsImpl(LlmTools); // FileSystemRead diff --git a/src/agent/autonomous/agentCompletion.ts b/src/agent/autonomous/agentCompletion.ts index dac145fb..2c26d5ed 100644 --- a/src/agent/autonomous/agentCompletion.ts +++ b/src/agent/autonomous/agentCompletion.ts @@ -4,15 +4,14 @@ import type { FunctionCallResult } from '#shared/llm/llm.model'; import { envVar } from '#utils/env-var'; /** - * Runs the completionHandler on an agent - * @param agent + * Executes the completion handler for a given agent. If the handler throws an error, it logs an error. + * @param agent - The agent context containing the completion handler to be invoked. */ export async function runAgentCompleteHandler(agent: AgentContext): Promise { try { await agent.completedHandler?.notifyCompleted(agent); } catch (e) { - logger.warn(e, `Completion handler error for agent ${agent.agentId}`); - throw e; + logger.error(e, `Completion handler error for agent ${agent.agentId}`); } } diff --git a/src/agent/autonomous/autonomousAgentRunner.ts b/src/agent/autonomous/autonomousAgentRunner.ts index 66ecfef1..ab845cf9 100644 --- a/src/agent/autonomous/autonomousAgentRunner.ts +++ b/src/agent/autonomous/autonomousAgentRunner.ts @@ -383,10 +383,6 @@ async function checkRepoHomeAndWorkingDirectory(agent: AgentContext) { agent.typedAiRepoDir = currentRepoDir; } const workingDir = fss.getWorkingDirectory(); - logger.info({ workingDir }, 'Verifying working directory exists'); const workDirExists = await fss.directoryExists(workingDir); - if (!workDirExists) { - throw new Error(`Working directory ${workingDir} does not exist or is not a directory.`); - } - logger.info({ workingDir }, 'Working directory verified.'); + if (!workDirExists) throw new Error(`Working directory ${workingDir} does not exist or is not a directory.`); } diff --git a/src/cli/cli.ts b/src/cli/cli.ts index c626bcff..a6e3a475 100644 --- a/src/cli/cli.ts +++ b/src/cli/cli.ts @@ -3,10 +3,10 @@ import path, { join } from 'node:path'; 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 { Claude4_1_Opus_Vertex } from '#llm/services/anthropic-vertex'; import { cerebrasQwen3_235b_Thinking, cerebrasQwen3_Coder } from '#llm/services/cerebras'; import { defaultLLMs } from '#llm/services/defaultLlms'; -import { openAIo3 } from '#llm/services/openai'; +import { openaiGPT5, openaiGPT5mini, openaiGPT5nano } from '#llm/services/openai'; import { perplexityDeepResearchLLM, perplexityLLM, perplexityReasoningProLLM } from '#llm/services/perplexity-llm'; import { xai_Grok4 } from '#llm/services/xai'; import { logger } from '#o11y/logger'; @@ -21,11 +21,13 @@ export const LLM_CLI_ALIAS: Record LLM> = { f: cerebrasQwen3_235b_Thinking, cc: cerebrasQwen3_Coder, x: xai_Grok4, - o3: openAIo3, + g5: openaiGPT5, + g5m: openaiGPT5mini, + g5n: openaiGPT5nano, madb: MAD_Balanced, mads: MAD_SOTA, madf: MAD_Fast, - opus: Claude4_Opus_Vertex, + opus: Claude4_1_Opus_Vertex, pp1: perplexityLLM, pp2: perplexityReasoningProLLM, pp3: perplexityDeepResearchLLM, diff --git a/src/cli/gen.ts b/src/cli/gen.ts index 0d2eb163..f85ae788 100644 --- a/src/cli/gen.ts +++ b/src/cli/gen.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 { writeFileSync } from 'node:fs'; -import { initInMemoryApplicationContext } from '#app/applicationContext'; +import { initApplicationContext, initInMemoryApplicationContext } from '#app/applicationContext'; import { ReasonerDebateLLM } from '#llm/multi-agent/reasoning-debate'; import { defaultLLMs } from '#llm/services/defaultLlms'; import { countTokens } from '#llm/tokens'; @@ -13,11 +13,13 @@ import { parsePromptWithImages } from './promptParser'; // npm run gen async function main() { - await initInMemoryApplicationContext(); - const { initialPrompt: rawPrompt, llmId, flags } = parseProcessArgs(); const { textPrompt, userContent } = parsePromptWithImages(rawPrompt); + // -s save to database + if (flags.s) await initApplicationContext(); + else await initInMemoryApplicationContext(); + let llm: LLM = defaultLLMs().medium; if (llmId) { if (!LLM_CLI_ALIAS[llmId]) { diff --git a/src/functions/scm/git.ts b/src/functions/scm/git.ts index 9bf1f588..2cc0f420 100644 --- a/src/functions/scm/git.ts +++ b/src/functions/scm/git.ts @@ -74,7 +74,7 @@ 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 + // Pre-commit hooks may make call lint/commit commands with characters for colours etc commitResult.stdout = formatAnsiWithMarkdownLinks(commitResult.stdout); failOnError(`Failed to commit changes for files: ${files.join(', ')}`, commitResult); } diff --git a/src/llm/multi-agent/blackberry.ts b/src/llm/multi-agent/blackberry.ts index 327216e0..4d2d6f09 100644 --- a/src/llm/multi-agent/blackberry.ts +++ b/src/llm/multi-agent/blackberry.ts @@ -1,7 +1,7 @@ import { BaseLLM } from '#llm/base-llm'; import { Claude4_Sonnet_Vertex } from '#llm/services/anthropic-vertex'; import { fireworksLlama3_405B } from '#llm/services/fireworks'; -import { openaiGPT41 } from '#llm/services/openai'; +import { openaiGPT5 } from '#llm/services/openai'; import { logger } from '#o11y/logger'; import type { GenerateTextOptions, LLM } from '#shared/llm/llm.model'; @@ -65,7 +65,7 @@ const MIND_OVER_DATA_SYS_PROMPT = `When addressing a problem, employ "Comparativ `; export class Blackberry extends BaseLLM { - llms: LLM[] = [Claude4_Sonnet_Vertex(), openaiGPT41(), Claude4_Sonnet_Vertex()]; + llms: LLM[] = [Claude4_Sonnet_Vertex(), openaiGPT5(), Claude4_Sonnet_Vertex()]; mediator: LLM = Claude4_Sonnet_Vertex(); constructor() { diff --git a/src/llm/multi-agent/reasoning-debate.ts b/src/llm/multi-agent/reasoning-debate.ts index d1d0eaa9..ea2ca99f 100644 --- a/src/llm/multi-agent/reasoning-debate.ts +++ b/src/llm/multi-agent/reasoning-debate.ts @@ -2,9 +2,9 @@ import { BaseLLM } from '#llm/base-llm'; import { getLLM } from '#llm/llmFactory'; import { FastMediumLLM } from '#llm/multi-agent/fastMedium'; import { anthropicClaude4_Sonnet } from '#llm/services/anthropic'; -import { Claude4_Opus_Vertex, Claude4_Sonnet_Vertex } from '#llm/services/anthropic-vertex'; +import { Claude4_1_Opus_Vertex, Claude4_Sonnet_Vertex } from '#llm/services/anthropic-vertex'; import { deepinfraDeepSeekR1 } from '#llm/services/deepinfra'; -import { openAIo3 } from '#llm/services/openai'; +import { openaiGPT5 } from '#llm/services/openai'; import { vertexGemini_2_5_Pro } from '#llm/services/vertexai'; import { xai_Grok4 } from '#llm/services/xai'; import { logger } from '#o11y/logger'; @@ -83,7 +83,7 @@ export function MAD_Balanced(): LLM { return new ReasonerDebateLLM( 'Balanced', vertexGemini_2_5_Pro, - [vertexGemini_2_5_Pro, xai_Grok4, openAIo3], + [vertexGemini_2_5_Pro, xai_Grok4, openaiGPT5], 'MAD:Balanced multi-agent debate (Gemini 2.5 Pro, Grok 4, o3)', ); } @@ -92,7 +92,7 @@ export function MAD_Balanced4(): LLM { return new ReasonerDebateLLM( 'Balanced4', vertexGemini_2_5_Pro, - [vertexGemini_2_5_Pro, xai_Grok4, openAIo3, Claude4_Sonnet_Vertex], + [vertexGemini_2_5_Pro, xai_Grok4, openaiGPT5, Claude4_Sonnet_Vertex], 'MAD:Balanced multi-agent debate (Gemini 2.5 Pro, Grok 4, o3, Sonnet 4)', ); } @@ -116,7 +116,7 @@ export function MAD_Anthropic(): LLM { } export function MAD_OpenAI(): LLM { - return new ReasonerDebateLLM('OpenAI', openAIo3, [openAIo3, openAIo3, openAIo3], 'MAD:OpenAI multi-agent debate (o3 x3)'); + return new ReasonerDebateLLM('OpenAI', openaiGPT5, [openaiGPT5, openaiGPT5, openaiGPT5], 'MAD:OpenAI multi-agent debate (GPT5 x3)'); } export function MAD_Grok(): LLM { @@ -126,9 +126,9 @@ export function MAD_Grok(): LLM { export function MAD_SOTA(): LLM { return new ReasonerDebateLLM( 'SOTA', - xai_Grok4, - [openAIo3, Claude4_Opus_Vertex, vertexGemini_2_5_Pro, xai_Grok4], - 'MAD:SOTA multi-agent debate (Opus 4, o3, Gemini 2.5 Pro, Grok 4)', + openaiGPT5, + [openaiGPT5, Claude4_1_Opus_Vertex, vertexGemini_2_5_Pro, xai_Grok4], + 'MAD:SOTA multi-agent debate (Opus 4, GPT5, Gemini 2.5 Pro, Grok 4)', ); } diff --git a/src/llm/services/anthropic-vertex.ts b/src/llm/services/anthropic-vertex.ts index eba70a02..0101e13c 100644 --- a/src/llm/services/anthropic-vertex.ts +++ b/src/llm/services/anthropic-vertex.ts @@ -14,15 +14,18 @@ export function anthropicVertexLLMRegistry(): Record LLM> { return { [`${ANTHROPIC_VERTEX_SERVICE}:claude-3-5-haiku`]: Claude3_5_Haiku_Vertex, [`${ANTHROPIC_VERTEX_SERVICE}:claude-sonnet-4`]: Claude4_Sonnet_Vertex, - [`${ANTHROPIC_VERTEX_SERVICE}:claude-opus-4`]: Claude4_Opus_Vertex, + [`${ANTHROPIC_VERTEX_SERVICE}:claude-opus-4-1@20250805`]: Claude4_1_Opus_Vertex, }; } // Supported image types image/jpeg', 'image/png', 'image/gif' or 'image/webp' -export function Claude4_Opus_Vertex(): LLM { - return new AnthropicVertexLLM('Claude 4 Opus (Vertex)', 'claude-opus-4', 200_000, anthropicCostFunction(15, 75)); + +// https://cloud.google.com/vertex-ai/generative-ai/docs/partner-models/claude/opus-4-1 +export function Claude4_1_Opus_Vertex(): LLM { + return new AnthropicVertexLLM('Claude 4.1 Opus (Vertex)', 'claude-opus-4-1@20250805', 200_000, anthropicCostFunction(15, 75), ['claude-opus-4']); } +// https://cloud.google.com/vertex-ai/generative-ai/docs/partner-models/claude/sonnet-4 export function Claude4_Sonnet_Vertex(): LLM { return new AnthropicVertexLLM('Claude 4 Sonnet (Vertex)', 'claude-sonnet-4', 200_000, anthropicCostFunction(3, 15)); } @@ -55,7 +58,7 @@ export function ClaudeVertexLLMs(): AgentLLMs { easy: Claude3_5_Haiku_Vertex(), medium: Claude4_Sonnet_Vertex(), hard: Claude4_Sonnet_Vertex(), - xhard: Claude4_Opus_Vertex(), + xhard: Claude4_1_Opus_Vertex(), }; } @@ -73,8 +76,8 @@ let gcloudProjectIndex = 0; * Vertex AI models - Gemini */ class AnthropicVertexLLM extends AiLLM { - constructor(displayName: string, model: string, maxInputToken: number, calculateCosts: LlmCostFunction) { - super(displayName, ANTHROPIC_VERTEX_SERVICE, model, maxInputToken, calculateCosts); + constructor(displayName: string, model: string, maxInputToken: number, calculateCosts: LlmCostFunction, oldIds?: string[]) { + super(displayName, ANTHROPIC_VERTEX_SERVICE, model, maxInputToken, calculateCosts, oldIds); } protected apiKey(): string { diff --git a/src/llm/services/anthropic.ts b/src/llm/services/anthropic.ts index 51e2d6cc..ef296f0a 100644 --- a/src/llm/services/anthropic.ts +++ b/src/llm/services/anthropic.ts @@ -12,26 +12,18 @@ export function anthropicLLMRegistry(): Record LLM> { return { [`${ANTHROPIC_SERVICE}:claude-3-5-haiku`]: Claude3_5_Haiku, [`${ANTHROPIC_SERVICE}:claude-sonnet-4-0`]: anthropicClaude4_Sonnet, - [`${ANTHROPIC_SERVICE}:claude-opus-4-0`]: anthropicClaude4_Opus, + [`${ANTHROPIC_SERVICE}:claude-opus-4-1-20250805`]: anthropicClaude4_1_Opus, }; } -export function anthropicClaude4_Opus(): LLM { - return new Anthropic('Claude 4 Opus (Anthropic)', 'claude-opus-4-0', anthropicCostFunction(15, 75)); +export function anthropicClaude4_1_Opus(): LLM { + return new Anthropic('Claude 4.1 Opus (Anthropic)', 'claude-opus-4-1-20250805', anthropicCostFunction(15, 75), ['claude-opus-4-0']); } export function anthropicClaude4_Sonnet(): LLM { return new Anthropic('Claude 4 Sonnet (Anthropic)', 'claude-sonnet-4-0', anthropicCostFunction(3, 15)); } -// export function Claude3_5_Sonnet() { -// return new Anthropic('Claude 3.5 Sonnet', 'claude-3-5-sonnet-20241022', 3, 15); -// } - -// export function Claude3_7_Sonnet() { -// return new Anthropic('Claude 3.7 Sonnet', 'claude-3-7-sonnet-latest', 3, 15); -// } - export function Claude3_5_Haiku(): LLM { return new Anthropic('Claude 3.5 Haiku', 'claude-3-5-haiku-20241022', anthropicCostFunction(1, 5)); } @@ -54,7 +46,7 @@ function anthropicCostFunction(inputMil: number, outputMil: number): LlmCostFunc export function ClaudeLLMs(): AgentLLMs { const sonnet4 = anthropicClaude4_Sonnet(); - const opus = anthropicClaude4_Opus(); + const opus = anthropicClaude4_1_Opus(); return { easy: Claude3_5_Haiku(), medium: sonnet4, @@ -64,8 +56,8 @@ export function ClaudeLLMs(): AgentLLMs { } export class Anthropic extends AiLLM { - constructor(displayName: string, model: string, calculateCosts: LlmCostFunction) { - super(displayName, ANTHROPIC_SERVICE, model, 200_000, calculateCosts); + constructor(displayName: string, model: string, calculateCosts: LlmCostFunction, oldIds?: string[]) { + super(displayName, ANTHROPIC_SERVICE, model, 200_000, calculateCosts, oldIds); } protected apiKey(): string { diff --git a/src/llm/services/cerebras.ts b/src/llm/services/cerebras.ts index 5abe1033..3dade305 100644 --- a/src/llm/services/cerebras.ts +++ b/src/llm/services/cerebras.ts @@ -65,7 +65,7 @@ export class CerebrasLLM extends AiLLM { if (this.getModel().includes('qwen-3')) { return wrapLanguageModel({ model: aiModel, - middleware: extractReasoningMiddleware({ tagName: 'think' }), + middleware: extractReasoningMiddleware({ tagName: 'think', startWithReasoning: true }), }); } return aiModel; diff --git a/src/llm/services/defaultLlms.ts b/src/llm/services/defaultLlms.ts index a11541de..261e87a5 100644 --- a/src/llm/services/defaultLlms.ts +++ b/src/llm/services/defaultLlms.ts @@ -11,7 +11,7 @@ import { cerebrasQwen3_235b_Thinking } from './cerebras'; import { Gemini_2_5_Flash, Gemini_2_5_Pro } from './gemini'; import { groqLlama4_Scout } from './groq'; import { Ollama_LLMs } from './ollama'; -import { openAIo3, openaiGPT41, openaiGPT41mini } from './openai'; +import { openaiGPT5, openaiGPT5mini } from './openai'; import { xai_Grok4 } from './xai'; let _summaryLLM: LLM; @@ -31,15 +31,15 @@ export function defaultLLMs(): AgentLLMs { // return _defaultLLMs; // } - const easyLLMs = [new FastEasyLLM(), vertexGemini_2_5_Flash(), Gemini_2_5_Flash(), groqLlama4_Scout(), openaiGPT41mini(), Claude3_5_Haiku()]; + const easyLLMs = [new FastEasyLLM(), vertexGemini_2_5_Flash(), Gemini_2_5_Flash(), groqLlama4_Scout(), openaiGPT5mini(), Claude3_5_Haiku()]; const easy: LLM | undefined = easyLLMs.find((llm) => llm.isConfigured()); if (!easy) throw new Error('No default easy LLM configured'); - const mediumLLMs = [new FastMediumLLM(), vertexGemini_2_5_Flash(), Gemini_2_5_Flash(), cerebrasQwen3_235b_Thinking(), openaiGPT41(), Claude3_5_Haiku()]; + const mediumLLMs = [new FastMediumLLM(), vertexGemini_2_5_Flash(), Gemini_2_5_Flash(), cerebrasQwen3_235b_Thinking(), openaiGPT5(), Claude3_5_Haiku()]; const medium: LLM | undefined = mediumLLMs.find((llm) => llm.isConfigured()); if (!medium) throw new Error('No default medium LLM configured'); - const hardLLMs = [vertexGemini_2_5_Pro(), Gemini_2_5_Pro(), xai_Grok4(), openAIo3(), anthropicClaude4_Sonnet()]; + const hardLLMs = [vertexGemini_2_5_Pro(), Gemini_2_5_Pro(), xai_Grok4(), openaiGPT5(), anthropicClaude4_Sonnet()]; const hard: LLM | undefined = hardLLMs.find((llm) => llm.isConfigured()); if (!hard) throw new Error('No default hard LLM configured'); diff --git a/src/llm/services/llm.int.ts b/src/llm/services/llm.int.ts index 58417b34..2f82fba5 100644 --- a/src/llm/services/llm.int.ts +++ b/src/llm/services/llm.int.ts @@ -6,7 +6,7 @@ import { deepSeekV3 } from '#llm/services/deepseek'; import { fireworksLlama3_70B } from '#llm/services/fireworks'; import { nebiusDeepSeekR1 } from '#llm/services/nebius'; import { Ollama_Phi3 } from '#llm/services/ollama'; -import { openaiGPT41mini } from '#llm/services/openai'; +import { openaiGPT5mini } from '#llm/services/openai'; import { perplexityLLM } from '#llm/services/perplexity-llm'; import { sambanovaDeepseekR1, sambanovaLlama3_3_70b, sambanovaLlama3_3_70b_R1_Distill } from '#llm/services/sambanova'; import { togetherDeepSeekR1_0528_tput } from '#llm/services/together'; @@ -197,7 +197,7 @@ describe('LLMs', () => { }); describe('OpenAI', () => { - const llm = openaiGPT41mini(); + const llm = openaiGPT5mini(); it('should generateText', async () => { const response = await llm.generateText(SKY_PROMPT, { temperature: 0, id: 'test' }); diff --git a/src/llm/services/openai.ts b/src/llm/services/openai.ts index f2b2cc92..72cb40c1 100644 --- a/src/llm/services/openai.ts +++ b/src/llm/services/openai.ts @@ -7,11 +7,10 @@ export const OPENAI_SERVICE = 'openai'; export function openAiLLMRegistry(): Record LLM> { return { - 'openai:gpt-4.1': () => openaiGPT41(), - 'openai:gpt-4.1-mini': () => openaiGPT41mini(), - 'openai:gpt-4.1-nano': () => openaiGPT41nano(), - 'openai:o3': () => openAIo3(), - 'openai:o4-mini': () => openAIo4mini(), + 'openai:gpt-5': () => openaiGPT5(), + 'openai:gpt-5-mini': () => openaiGPT5mini(), + 'openai:gpt-5-nano': () => openaiGPT5nano(), + 'openai:gpt-5-chat': () => openaiGPT5chat(), }; } @@ -35,29 +34,44 @@ function openAICostFunction(inputMil: number, outputMil: number): LlmCostFunctio }; } -export function openAIo3(): LLM { - return new OpenAI('OpenAI o3', 'o3', openAICostFunction(2, 8), 200_000); +function gpt5CostFunction(inputMil: number, outputMil: number): LlmCostFunction { + return (inputTokens: number, outputTokens: number, usage: any) => { + const metadata = usage as { openai?: { cachedPromptTokens?: number } }; + const cachedPromptTokens = metadata?.openai?.cachedPromptTokens ?? 0; + let inputCost: number; + if (cachedPromptTokens > 0) { + inputCost = ((inputTokens - cachedPromptTokens) * inputMil) / 1_000_000 + (cachedPromptTokens * inputMil) / 10 / 1_000_000; + } else { + inputCost = (inputTokens * inputMil) / 1_000_000; + } + const outputCost = (outputTokens * outputMil) / 1_000_000; + return { + inputCost, + outputCost, + totalCost: inputCost + outputCost, + }; + }; } -export function openAIo4mini(): LLM { - return new OpenAI('OpenAI o4-mini', 'o4-mini', openAICostFunction(1.1, 4.4), 200_000); +export function openaiGPT5(): LLM { + return new OpenAI('GPT5', 'gpt-5', gpt5CostFunction(1.25, 10), 200_000, ['o3', 'gpt-4.1']); } -export function openaiGPT41(): LLM { - return new OpenAI('GPT4.1', 'gpt-4.1', openAICostFunction(2, 8), 1_047_576); +export function openaiGPT5mini(): LLM { + return new OpenAI('GPT5 mini', 'gpt-5-mini', gpt5CostFunction(0.25, 2), 200_000, ['gpt-4.1-mini', 'o3-mini', 'o4-mini']); } -export function openaiGPT41mini(): LLM { - return new OpenAI('GPT4.1 mini', 'gpt-4.1-mini', openAICostFunction(0.4, 1.6), 1_047_576); +export function openaiGPT5nano(): LLM { + return new OpenAI('GPT5 nano', 'gpt-5-nano', gpt5CostFunction(0.05, 0.4), 200_000, ['gpt-4.1-nano', 'o3-nano', 'o4-mini']); } -export function openaiGPT41nano(): LLM { - return new OpenAI('GPT4.1 nano', 'gpt-4.1-nano', openAICostFunction(0.1, 0.4), 1_047_576); +export function openaiGPT5chat(): LLM { + return new OpenAI('GPT5 chat', 'gpt-5-chat', gpt5CostFunction(1.25, 10), 200_000, ['gpt-4']); } export class OpenAI extends AiLLM { - constructor(displayName: string, model: string, calculateCosts: LlmCostFunction, maxContext: number) { - super(displayName, OPENAI_SERVICE, model, maxContext, calculateCosts); + constructor(displayName: string, model: string, calculateCosts: LlmCostFunction, maxContext: number, oldIds?: string[]) { + super(displayName, OPENAI_SERVICE, model, maxContext, calculateCosts, oldIds); } protected apiKey(): string { diff --git a/src/modules/slack/slackApi.ts b/src/modules/slack/slackApi.ts index 94f45751..fb77d1a4 100644 --- a/src/modules/slack/slackApi.ts +++ b/src/modules/slack/slackApi.ts @@ -34,6 +34,7 @@ export class SlackAPI { } while (cursor); return allMessages; } + /** * Fetch all messages in a user's App (Direct Message) channel. * @param {string} channelId - The channel ID (like 'DXXX' for a Direct Message). @@ -67,23 +68,37 @@ export class SlackAPI { * Adds a reaction to a Slack message (e.g., πŸ€–πŸ’₯ for "bot broken") * @param channel Slack channel ID (e.g., "C1234567890") * @param messageTimestamp Message timestamp (e.g., "1629378123.000200" from event.message.ts) - * @param reaction Emoji combo name (e.g., "robot_face::boom") + * @param reaction Emoji name e.g., "robot_face::boom" (seperate mutliple with ::). Default is πŸ€– */ - async addReaction( - channel: string, - messageTimestamp: string, - reaction = 'robot_face', // Default to πŸ€– - ): Promise { + async addReaction(channel: string, messageTimestamp: string, reaction = 'robot_face'): Promise { try { await this.client.reactions.add({ channel, timestamp: messageTimestamp, - name: reaction, // Use "::" for combo emojis (NOT spaces) + name: reaction, }); - logger.debug(`Reaction added: ${reaction} to ${channel} @ ${messageTimestamp}`); + logger.info(`Reaction added: ${reaction} to ${channel} @ ${messageTimestamp}`); } catch (error) { logger.error(error, `Error adding Slack reaction to ${channel} @ ${messageTimestamp}`); - // Don't throw error, just log it + } + } + + /** + * Removes a reaction from a Slack message (e.g., πŸ€–πŸ’₯ for "bot broken") + * @param channel Slack channel ID (e.g., "C1234567890") + * @param messageTimestamp Message timestamp (e.g., "1629378123.000200" from event.message.ts) + * @param reaction Emoji combo name (e.g., "robot_face::boom") + */ + async removeReaction(channel: string, messageTimestamp: string, reaction = 'robot_face'): Promise { + try { + await this.client.reactions.remove({ + channel, + timestamp: messageTimestamp, + name: reaction, + }); + logger.info(`Reaction removed: ${reaction} from ${channel} @ ${messageTimestamp}`); + } catch (error) { + logger.error(error, `Error removing Slack reaction from ${channel} @ ${messageTimestamp}`); } } diff --git a/src/modules/slack/slackChatBotService.ts b/src/modules/slack/slackChatBotService.ts index 502f0e76..d4181a1a 100644 --- a/src/modules/slack/slackChatBotService.ts +++ b/src/modules/slack/slackChatBotService.ts @@ -1,7 +1,7 @@ import { App, type KnownEventFromType, type SayFn, StringIndexed } from '@slack/bolt'; import { MessageElement } from '@slack/web-api/dist/types/response/ConversationsHistoryResponse'; import { getLastFunctionCallArg } from '#agent/autonomous/agentCompletion'; -import { resumeCompleted, resumeCompletedWithUpdatedUserRequest, startAgent } from '#agent/autonomous/autonomousAgentRunner'; +import { AgentExecution, resumeCompleted, resumeCompletedWithUpdatedUserRequest, startAgent } from '#agent/autonomous/autonomousAgentRunner'; import { appContext } from '#app/applicationContext'; import { GoogleCloud } from '#functions/cloud/google/google-cloud'; import { Jira } from '#functions/jira'; @@ -14,6 +14,7 @@ import { type AgentCompleted, type AgentContext, isExecuting } from '#shared/age import { sleep } from '#utils/async-utils'; import type { ChatBotService } from '../../chatBot/chatBotService'; import { SlackAPI } from './slackApi'; +import { textToBlocks } from './slackMessageFormatter'; let slackApp: App | undefined; @@ -36,7 +37,14 @@ One quirk of threaded messages is that a parent message object will retain a thr export class SlackChatBotService implements ChatBotService, AgentCompleted { channels: Set = new Set(); appChannel = ''; - slackApi: SlackAPI; + slackApi: SlackAPI | undefined; + + private debounceTimers: Map = new Map(); + + api(): SlackAPI { + this.slackApi ??= new SlackAPI(); + return this.slackApi; + } threadId(agent: AgentContext): string { return agent.agentId.replace('Slack-', ''); @@ -75,12 +83,21 @@ export class SlackChatBotService implements ChatBotService, AgentCompleted { async sendMessage(agent: AgentContext, message: string): Promise { if (!slackApp) throw new Error('Slack app is not initialized. Call initSlack() first.'); + if (!agent.metadata?.slack?.channel || !agent.metadata?.slack?.thread_ts) { + logger.error({ metadata: agent.metadata }, `Agent ${agent.agentId} does not have a Slack channel and thread_ts metadata [metadata]`); + return; + } + + if (agent.metadata.slack.reply_ts) this.api().removeReaction(agent.metadata.slack.channel, agent.metadata.slack.reply_ts, 'robot_face'); + const params: any = { - channel: agent.metadata.channel, - text: message, - thread_ts: agent.metadata.thread_ts, + channel: agent.metadata.slack.channel, + thread_ts: agent.metadata.slack.thread_ts, + blocks: textToBlocks(message), }; + // TODO remove reaction from message it replied to + /* Only add thread_ts if we’re in a real thread. - In a channel: event.thread_ts is set for replies - In the App DM: event.thread_ts is undefined */ @@ -98,6 +115,11 @@ export class SlackChatBotService implements ChatBotService, AgentCompleted { } } + async shutdown() { + await slackApp?.stop(); + slackApp = undefined; + } + async initSlack(): Promise { if (slackApp) return; @@ -121,7 +143,7 @@ export class SlackChatBotService implements ChatBotService, AgentCompleted { appToken: appToken, }); - this.channels = new Set([this.appChannel, ...channels.split(',').map((s) => s.trim())]); + this.channels = new Set([...channels.split(',').map((s) => s.trim())]); // Listen for messages in channels slackApp.event('message', async ({ event, say }) => { @@ -144,15 +166,11 @@ export class SlackChatBotService implements ChatBotService, AgentCompleted { async handleMessage(event: KnownEventFromType<'message'>, say: SayFn) { // biomejs formatter changes event['property'] to event.property which doesn't compile const _event: any = event; - console.log('Event received for message'); - console.log('== BEGIN EVENT =='); - console.log(JSON.stringify(event)); - console.log('== END EVENT =='); - logger.info(`channel_type: ${event.channel_type}`); - // logger.info(await (say['message'])) - const _say: SayFn = say; - - // if (event.channel_type === 'im') + const threadId = _event.thread_ts ?? _event.ts; + const agentId = `Slack-${threadId}`; + const agentService = appContext().agentStateService; + logger.debug(event, 'Slack message received [event]'); + if (event.subtype === 'message_deleted') return; if (event.subtype === 'message_changed') return; if (event.subtype === 'channel_join') return; @@ -164,154 +182,88 @@ export class SlackChatBotService implements ChatBotService, AgentCompleted { return; } - console.log(`Message received in channel: ${_event.text}`); - - const agentService = appContext().agentStateService; - - // Messages with the app under the Apps section has different properties than messages from a regular channel? - if (event.channel === this.appChannel) { - const threadTs = (event as any).thread_ts; - const newThread = event.ts === threadTs; - let conversationHistory = ''; - - if (!newThread) { - const threadMessages = await new SlackAPI().getConversationReplies(event.channel, threadTs); - conversationHistory = `You are the bot and will be responding to the user.\n${threadMessages.map((message) => { - const tagName = message.bot_profile ? 'bot' : 'user'; - return `<${tagName}>\n${message.text}\n\n`; - })}\n\n`; - } - - try { - const agentExec = await startAgent({ - type: 'autonomous', - subtype: 'codegen', - resumeAgentId: 'Slack-app', - initialPrompt: conversationHistory + _event.text, - llms: defaultLLMs(), - functions: CHATBOT_FUNCTIONS, - agentName: 'Slack-app', - systemPrompt: - 'You are an AI support agent. You are responding to support requests on the company Slack account. Respond in a helpful, concise manner. If you encounter an error responding to the request do not provide details of the error to the user, only respond with "Sorry, I\'m having difficulties providing a response to your request"', - metadata: { channel: event.channel, thread_ts: event.ts }, - completedHandler: this, - humanInLoop: { - budget: 0.5, - count: 5, - }, - }); - await agentExec.execution; - const agent: AgentContext = await appContext().agentStateService.load(agentExec.agentId); - if (agent.state !== 'completed' && agent.state !== 'hitl_feedback') { - logger.error(`Agent did not complete. State was ${agent.state}`); - - await this.slackApi.addReaction(event.channel, event.ts, 'robot_face::boom'); - - return; - } - } catch (e) { - logger.error(e, 'Error handling new Slack app thread'); - } - return; - } + const text = _event.text; // In regular channels if the message is not a reply in a thread, then we will start a new agent to handle the first message in the thread if (!_event.thread_ts) { + // New top-level message (new thread) in any channel type const threadId = event.ts; logger.info(`New thread ${event.ts}`); - const text = _event.text; + await this.slackApi.addReaction(event.channel, threadId, 'robot_face'); try { - const ackResult = await say({ - text: "One moment, I'm analysing your request", - thread_ts: threadId, - channel: event.channel, - }); - if (!ackResult.ok) { - logger.error(ackResult.error, 'Error sending Slack acknowledgement'); - } - } catch (e) { - logger.error(e, 'Error sending Slack acknowledgement'); - } - - try { - const agentExec = await startAgent({ - type: 'autonomous', - subtype: 'codegen', - resumeAgentId: `Slack-${threadId}`, - initialPrompt: text, - llms: defaultLLMs(), - functions: CHATBOT_FUNCTIONS, - agentName: `Slack-${threadId}`, - systemPrompt: - 'You are an AI support agent. You are responding to support requests on the company Slack account. Respond in a helpful, concise manner. If you encounter an error responding to the request do not provide details of the error to the user, only respond with "Sorry, I\'m having difficulties providing a response to your request"', - metadata: { channel: event.channel }, - completedHandler: this, - humanInLoop: { - budget: 0.5, - count: 5, - }, - }); + const agentExec = await this.startAgentForThread(threadId, event.channel, text); await agentExec.execution; - const agent: AgentContext = await appContext().agentStateService.load(agentExec.agentId); + const agent: AgentContext = await agentService.load(agentExec.agentId); if (agent.state !== 'completed' && agent.state !== 'hitl_feedback') { logger.error(`Agent did not complete. State was ${agent.state}`); + await this.slackApi.addReaction(event.channel, event.ts, 'robot_face::boom'); return; } return; - // Agent completionHandler sends the message - // const response = agent.functionCallHistory.at(-1).parameters[agent.state === 'completed' ? AGENT_COMPLETED_PARAM_NAME : REQUEST_FEEDBACK_PARAM_NAME]; - // const sayResult = await say({ - // text: response, - // thread_ts: threadId, - // channel: event.channel, - // }); - // if (!sayResult.ok) { - // logger.error(sayResult.error, 'Error replying'); - // } } catch (e) { logger.error(e, 'Error handling new Slack thread'); } } else { // Otherwise this is a reply to a thread - const agentId = `Slack-${_event.thread_ts}`; + const threadId = _event.thread_ts; + const agentId = `Slack-${threadId}`; const agent: AgentContext | null = await agentService.load(agentId); - // Getting a null agent when a conversation is started in the App channel - handle in the app specific code - if (agent && isExecuting(agent)) { + const messages = await this.fetchThreadMessages(event.channel, threadId); + + await this.slackApi.addReaction(event.channel, _event.ts, 'robot_face'); + + const prompt = `${JSON.stringify(messages)}\n\nReply to this conversation thread`; + + if (!agent) { + this.startAgentForThread(threadId, event.channel, prompt, _event.ts); + } else if (isExecuting(agent)) { // TODO make this transactional, and implement agent.pendingMessages.push(_event.text); await agentService.save(agent); return; + } else { + await resumeCompletedWithUpdatedUserRequest(agentId, agent.executionId, prompt); } - const messages = await this.fetchThreadMessages(event.channel, _event.thread_ts); - await resumeCompletedWithUpdatedUserRequest( - agentId, - agent.executionId, - `${JSON.stringify(messages)}\n\nYour task is to reply to this conversation thread`, - ); } } + async startAgentForThread(threadId: string, channel: string, prompt: string, replyTs?: string): Promise { + return await startAgent({ + type: 'autonomous', + subtype: 'codegen', + resumeAgentId: `Slack-${threadId}`, + initialPrompt: prompt, + llms: defaultLLMs(), + functions: CHATBOT_FUNCTIONS, + agentName: `Slack-${threadId}`, + systemPrompt: + 'You are an AI support agent. You are responding to support requests on the company Slack account. Respond in a helpful, concise manner. If you encounter an error responding to the request do not provide details of the error to the user, only respond with "Sorry, I\'m having difficulties providing a response to your request"', + metadata: { slack: { channel, thread_ts: threadId, reply_ts: replyTs } }, // Use event.ts as thread_ts for new threads + completedHandler: this, + humanInLoop: { + budget: 2, + count: 10, + }, + }); + } + async fetchThreadMessages(channel: string, parentMessageTs: string): Promise { const result = await slackApp.client.conversations.replies({ ts: parentMessageTs, channel, - limit: 1000, // Maximum number of messages to return + limit: 1000, }); - // Process the messages const messages: MessageElement[] = result.messages; - // If there are more messages, use pagination if (result.has_more) { - // Fetch the next page of messages const nextResult = await slackApp.client.conversations.replies({ ts: parentMessageTs, cursor: result.response_metadata.next_cursor, channel, }); - // Process the next page of messages messages.push(...nextResult.messages); } return messages; diff --git a/src/modules/slack/slackMessageFormatter.test.ts b/src/modules/slack/slackMessageFormatter.test.ts new file mode 100644 index 00000000..56aab63a --- /dev/null +++ b/src/modules/slack/slackMessageFormatter.test.ts @@ -0,0 +1,81 @@ +import { expect } from 'chai'; + +import { textToBlocks } from './slackMessageFormatter'; + +describe.skip('textToBlocks()', () => { + /********************************************************************** + * SMALL / HAPPY-PATH CASES + *********************************************************************/ + it('returns an array with a single block for short plain text', () => { + const text = 'Hello, world!'; + const blocks = textToBlocks(text); + + expect(blocks).to.have.lengthOf(1); + expect(blocks[0].type).to.equal('section'); + expect(blocks[0].text.type).to.equal('mrkdwn'); + expect(blocks[0].text.text).to.equal(text); + }); + + it('converts the supported markdown to Slack mrkdwn', () => { + const markdown = + '# Heading 1\n' + '## Heading 2\n' + '### Heading 3\n' + 'Regular **bold** and *italic* and ~~strike~~.\n' + '- First item\n' + '- Second item'; + + const expectedMrkdwn = + '*Heading 1*\n' + '*Heading 2*\n' + '*Heading 3*\n' + 'Regular *bold* and _italic_ and ~strike~.\n' + 'β€’ First item\n' + 'β€’ Second item'; + + const blocks = textToBlocks(markdown); + + expect(blocks).to.have.lengthOf(1); + expect(blocks[0].text.text).to.equal(expectedMrkdwn); + }); + + /********************************************************************** + * LARGE MESSAGE – SPLITTING INTO MULTIPLE BLOCKS + *********************************************************************/ + it('splits long messages into multiple blocks, each ≀ 3 000 characters', () => { + // Build a predictable multi-line string just over 6 000 characters + const singleLine = '0123456789'.repeat(25); // 250 characters / line + const longText = Array.from({ length: 25 }, () => singleLine).join('\n'); // ~6 250 chars + + // Sanity check for setup + expect(longText.length).to.be.greaterThan(6000); + + const blocks = textToBlocks(longText); + + expect(blocks.length).to.be.greaterThan(1); + blocks.forEach((b, i) => { + expect(b.type).to.equal('section', `block #${i} wrong type`); + expect(b.text.type).to.equal('mrkdwn'); + expect(b.text.text.length).to.be.at.most(3000, `block #${i} is too long`); + }); + + // Re-assemble to ensure no data was lost while splitting + const reassembled = blocks.map((b) => b.text.text).join(''); + // The splitting algorithm drops the *first* \n when adding each line + // (it adds "\n" **before** the next line). Therefore re-assemble after + // trimming the leading \n that will be present in every block except the first. + expect(reassembled.replace(/\n/g, '')).to.equal(longText.replace(/\n/g, '')); + }); + + /********************************************************************** + * EDGE-CASES + *********************************************************************/ + it('handles a message exactly 3 000 characters long', () => { + const textExact = 'a'.repeat(3000); + + const blocks = textToBlocks(textExact); + + expect(blocks).to.have.lengthOf(1); + expect(blocks[0].text.text.length).to.equal(3000); + }); + + it('handles inline and fenced code blocks unchanged', () => { + const md = 'Here is some `inline()` code.\n' + '```ts\n' + 'const x: number = 42;\n' + '```\n'; + + const blocks = textToBlocks(md); + + expect(blocks).to.have.lengthOf(1); + expect(blocks[0].text.text).to.contain('`inline()`'); + expect(blocks[0].text.text).to.contain('```ts'); + }); +}); diff --git a/src/modules/slack/slackMessageFormatter.ts b/src/modules/slack/slackMessageFormatter.ts new file mode 100644 index 00000000..d1e18903 --- /dev/null +++ b/src/modules/slack/slackMessageFormatter.ts @@ -0,0 +1,72 @@ +export function convertMarkdownToMrkdwn(markdownText: string): string { + return ( + markdownText + // ── italic ── single * that is NOT preceded/followed by another * + .replace(/(^|[^*])\*(?!\*)([^*]+?)\*(?!\*)/g, '$1_$2_') + // ── bold ── + .replace(/\*\*(.+?)\*\*/g, '*$1*') + // ── strike-through ── + .replace(/~~(.+?)~~/g, '~$1~') + // ── code (unchanged) ── + .replace(/```([^`]*?)```/gs, '```$1```') + .replace(/`([^`]*?)`/g, '`$1`') + // ── headings ── + .replace(/^### (.*)$/gm, '*$1*') + .replace(/^## {2}(.*)$/gm, '*$1*') + .replace(/^# {3}(.*)$/gm, '*$1*') + // ── unordered list ── + .replace(/^- (.*)$/gm, 'β€’ $1') + ); +} +// export function convertMarkdownToMrkdwn(markdownText: string): string { +// return markdownText +// .replace(/\*\*(.*?)\*\*/g, '*$1*') // Bold: **text** -> *text* +// .replace(/\*(.*?)\*/g, '_$1_') // Italic: *text* -> _text_ +// .replace(/~~(.*?)~~/g, '~$1~') // Strikethrough +// .replace(/```([^`]*?)```/gs, '```$1```') // Code blocks (remain the same) +// .replace(/`(.*?)`/g, '`$1`') // Inline code (remains the same) +// .replace(/^### (.*$)/gm, '*$1*') // H3 headings +// .replace(/^## (.*$)/gm, '*$1*') // H2 headings +// .replace(/^# (.*$)/gm, '*$1*') // H1 headings +// .replace(/^- (.*$)/gm, 'β€’ $1'); // Unordered lists +// } + +function mrkdwnBlock(text: string): MarkdownBlock { + return { + type: 'section', + text: { + type: 'mrkdwn', + text: text, + }, + }; +} + +interface MarkdownBlock { + type: 'section'; + text: { + type: 'mrkdwn'; + text: string; + }; +} + +export function textToBlocks(text: string): MarkdownBlock[] { + const mrkdwn = convertMarkdownToMrkdwn(text); + const blocks: MarkdownBlock[] = []; + + if (mrkdwn.length > 3000) { + // split the message into multiple blocks. Find the first new lines under 3000 characters + let block = ''; + for (const line of mrkdwn.split('\n')) { + if (block.length + line.length > 3000) { + blocks.push(mrkdwnBlock(block)); + block = line; + } else { + block += `\n${line}`; + } + } + blocks.push(mrkdwnBlock(block)); + } else { + blocks.push(mrkdwnBlock(mrkdwn)); + } + return blocks; +} diff --git a/src/swe/aiderCodeEditor.ts b/src/swe/aiderCodeEditor.ts index e67928e6..0ee83aac 100644 --- a/src/swe/aiderCodeEditor.ts +++ b/src/swe/aiderCodeEditor.ts @@ -12,7 +12,7 @@ import { func, funcClass } from '#functionSchema/functionDecorators'; import { callStack } from '#llm/llmCallService/llmCall'; import { anthropicClaude4_Sonnet } from '#llm/services/anthropic'; import { deepSeekV3 } from '#llm/services/deepseek'; -import { openaiGPT41 } from '#llm/services/openai'; +import { openaiGPT5 } from '#llm/services/openai'; import { openRouterGemini2_5_Pro } from '#llm/services/openrouter'; import { vertexGemini_2_5_Pro } from '#llm/services/vertexai'; import { logger } from '#o11y/logger'; @@ -104,7 +104,7 @@ export class AiderCodeEditor { modelArg = ''; env = { OPENAI_API_KEY: openaiKey }; span.setAttribute('model', 'openai'); - llm = openaiGPT41(); + llm = openaiGPT5(); } else { throw new Error( 'Aider code editing requires either GCLOUD_PROJECT and GCLOUD_CLAUDE_REGION env vars set or else a key for Anthropic, Deepseek or OpenAI', diff --git a/src/swe/codeEditingAgent.ts b/src/swe/codeEditingAgent.ts index b9f08de6..39cc2125 100644 --- a/src/swe/codeEditingAgent.ts +++ b/src/swe/codeEditingAgent.ts @@ -97,7 +97,7 @@ export class CodeEditingAgent { throw new Error(`If fileSelection is provided it must be an array. Was type ${typeof fileSelection}`); } let projectInfo: ProjectInfo = altOptions?.projectInfo; - projectInfo ??= await getProjectInfo(); + projectInfo ??= await getProjectInfo(true); const fss: IFileSystemService = getFileSystem(); if (altOptions?.workingDirectory) fss.setWorkingDirectory(altOptions.workingDirectory);