diff --git a/src/agent/completionHandlerRegistry.ts b/src/agent/completionHandlerRegistry.ts index 87aa35d5..f20753c2 100644 --- a/src/agent/completionHandlerRegistry.ts +++ b/src/agent/completionHandlerRegistry.ts @@ -1,5 +1,4 @@ import { ConsoleCompletedHandler } from '#agent/autonomous/agentCompletion'; -import { SlackChatBotService } from '#modules/slack/slackChatBotService'; import { logger } from '#o11y/logger'; import { GitLabNoteCompletedHandler } from '#routes/webhooks/gitlab/gitlabNoteHandler'; import type { AgentCompleted } from '#shared/agent/agent.model'; @@ -7,10 +6,11 @@ import type { AgentCompleted } from '#shared/agent/agent.model'; // Use a Map for easier addition/removal during tests let handlersMap = new Map AgentCompleted>(); -// Initialize with default handlers -handlersMap.set(new ConsoleCompletedHandler().agentCompletedHandlerId(), ConsoleCompletedHandler); -handlersMap.set(new SlackChatBotService().agentCompletedHandlerId(), SlackChatBotService); -handlersMap.set(new GitLabNoteCompletedHandler().agentCompletedHandlerId(), GitLabNoteCompletedHandler); +function initHandlers() { + // Initialize with default handlers + handlersMap.set(new ConsoleCompletedHandler().agentCompletedHandlerId(), ConsoleCompletedHandler); + handlersMap.set(new GitLabNoteCompletedHandler().agentCompletedHandlerId(), GitLabNoteCompletedHandler); +} /** * Return the AgentCompleted callback object from its id. @@ -20,6 +20,8 @@ handlersMap.set(new GitLabNoteCompletedHandler().agentCompletedHandlerId(), GitL export function getCompletedHandler(handlerId: string): AgentCompleted | null { if (!handlerId) return null; + if (handlersMap.size === 0) initHandlers(); + const HandlerCtor = handlersMap.get(handlerId); if (HandlerCtor) return new HandlerCtor(); @@ -47,5 +49,4 @@ export function clearCompletedHandlers(): void { handlersMap = new Map AgentCompleted>(); // Re-initialize with default handlers handlersMap.set(new ConsoleCompletedHandler().agentCompletedHandlerId(), ConsoleCompletedHandler); - handlersMap.set(new SlackChatBotService().agentCompletedHandlerId(), SlackChatBotService); } diff --git a/src/cli/agent.ts b/src/cli/agent.ts index cb5d6fcb..11518417 100644 --- a/src/cli/agent.ts +++ b/src/cli/agent.ts @@ -14,11 +14,9 @@ import { logger } from '#o11y/logger'; import type { AgentContext } from '#shared/agent/agent.model'; import { registerErrorHandlers } from '../errorHandlers'; import { parseProcessArgs, saveAgentId } from './cli'; -import { loadCliEnvironment } from './envLoader'; import { resolveFunctionClasses } from './functionAliases'; export async function main(): Promise { - loadCliEnvironment(); registerErrorHandlers(); await initApplicationContext(); const llms = defaultLLMs(); diff --git a/src/cli/chat.ts b/src/cli/chat.ts index 30a406b5..f287f1ad 100644 --- a/src/cli/chat.ts +++ b/src/cli/chat.ts @@ -9,11 +9,9 @@ import { getMarkdownFormatPrompt } from '#routes/chat/chatPromptUtils'; import { LLM, LlmMessage, UserContentExt, contentText, messageText, user } from '#shared/llm/llm.model'; import { currentUser } from '#user/userContext'; import { parseProcessArgs, saveAgentId } from './cli'; -import { loadCliEnvironment } from './envLoader'; import { LLM_CLI_ALIAS } from './llmAliases'; async function main() { - loadCliEnvironment(); await initApplicationContext(); const { initialPrompt: rawPrompt, resumeAgentId, flags } = parseProcessArgs(); diff --git a/src/cli/code.ts b/src/cli/code.ts index 8adf1058..7bdc1ede 100644 --- a/src/cli/code.ts +++ b/src/cli/code.ts @@ -11,11 +11,9 @@ import { contentText, messageText } from '#shared/llm/llm.model'; import { CodeEditingAgent } from '#swe/codeEditingAgent'; import { beep } from '#utils/beep'; import { parseProcessArgs, saveAgentId } from './cli'; -import { loadCliEnvironment } from './envLoader'; import { parsePromptWithImages } from './promptParser'; async function main() { - loadCliEnvironment(); await initApplicationContext(); const agentLlms: AgentLLMs = defaultLLMs(); diff --git a/src/cli/codeAgent.ts b/src/cli/codeAgent.ts index 40ba68e0..f99ab72c 100644 --- a/src/cli/codeAgent.ts +++ b/src/cli/codeAgent.ts @@ -21,7 +21,6 @@ import { CodeFunctions } from '#swe/codeFunctions'; import { MorphEditor } from '#swe/morph/morphEditor'; import { registerErrorHandlers } from '../errorHandlers'; import { parseProcessArgs, saveAgentId } from './cli'; -import { loadCliEnvironment } from './envLoader'; import { resolveFunctionClasses } from './functionAliases'; async function resumeAgent(resumeAgentId: string, initialPrompt: string) { @@ -44,7 +43,6 @@ async function resumeAgent(resumeAgentId: string, initialPrompt: string) { } export async function main(): Promise { - loadCliEnvironment(); registerErrorHandlers(); await initApplicationContext(); const llms = defaultLLMs(); diff --git a/src/cli/commit.ts b/src/cli/commit.ts index 5399ce34..dfe3a5d6 100644 --- a/src/cli/commit.ts +++ b/src/cli/commit.ts @@ -5,10 +5,8 @@ import { shutdownTrace } from '#fastify/trace-init/trace-init'; import { Git } from '#functions/scm/git'; import { FileSystemRead } from '#functions/storage/fileSystemRead'; import { defaultLLMs } from '#llm/services/defaultLlms'; -import { loadCliEnvironment } from './envLoader'; async function main() { - loadCliEnvironment(); await initApplicationContext(); console.log('Commit command starting...'); diff --git a/src/cli/debate.ts b/src/cli/debate.ts index ae1c6045..c025b2cc 100644 --- a/src/cli/debate.ts +++ b/src/cli/debate.ts @@ -12,11 +12,9 @@ import { logger } from '#o11y/logger'; import type { AgentLLMs } from '#shared/agent/agent.model'; import { messageText } from '#shared/llm/llm.model'; import { parseProcessArgs } from './cli'; -import { loadCliEnvironment } from './envLoader'; import { parsePromptWithImages } from './promptParser'; async function main() { - loadCliEnvironment(); await initApplicationContext(); const agentLLMs: AgentLLMs = defaultLLMs(); const { initialPrompt: rawPrompt, resumeAgentId, flags } = parseProcessArgs(); diff --git a/src/cli/easy.ts b/src/cli/easy.ts index 4426032b..dfef03d8 100644 --- a/src/cli/easy.ts +++ b/src/cli/easy.ts @@ -9,14 +9,12 @@ import { mockLLMs } from '#llm/services/mock-llm'; import { vertexGemini_2_5_Flash } from '#llm/services/vertexai'; import type { AgentContext } from '#shared/agent/agent.model'; import { parseProcessArgs } from './cli'; -import { loadCliEnvironment } from './envLoader'; // See https://arxiv.org/html/2405.19616v1 https://github.com/autogenai/easy-problems-that-llms-get-wrong // Usage: // npm run easy async function main() { - loadCliEnvironment(); await initApplicationContext(); const context: AgentContext = createContext({ diff --git a/src/cli/envLoader.ts b/src/cli/envLoader.ts index d401de62..1f4d14bc 100644 --- a/src/cli/envLoader.ts +++ b/src/cli/envLoader.ts @@ -4,10 +4,8 @@ * When using git worktrees enables using the local.env from the main repository * Extracted from startLocal.ts to be shared across all CLI tools. */ - import { existsSync, readFileSync } from 'node:fs'; import { isAbsolute, resolve } from 'node:path'; -import { logger } from '#o11y/logger'; interface ResolveEnvFileOptions { envFile?: string | null; @@ -21,6 +19,8 @@ interface ApplyEnvOptions { type ParsedEnv = Record; +export let loadedEnvFilePath: string | undefined; + /** * Builds an absolute path from a potential relative path. * @param value The path value (can be null or undefined). @@ -125,9 +125,12 @@ export function applyEnvFile(filePath: string, options: ApplyEnvOptions = {}): v export function loadCliEnvironment(options: ApplyEnvOptions = {}): void { try { const envFilePath = resolveEnvFilePath(); + loadedEnvFilePath = envFilePath; applyEnvFile(envFilePath, options); - logger.debug(`Loaded environment from ${envFilePath}`); + console.log(`Loaded environment from ${envFilePath}`); } catch (err) { - logger.debug(err, 'No environment file found; continuing with existing process.env'); + console.log(err, 'No environment file found; continuing with existing process.env'); } } + +loadCliEnvironment(); diff --git a/src/cli/export.ts b/src/cli/export.ts index 2d1c7567..5806993c 100644 --- a/src/cli/export.ts +++ b/src/cli/export.ts @@ -7,7 +7,6 @@ import micromatch from 'micromatch'; import { FileSystemService } from '#functions/storage/fileSystemService'; import { countTokens } from '#llm/tokens'; import { logger } from '#o11y/logger'; -import { loadCliEnvironment } from './envLoader'; /** * If there are no arguments then only write the exported contents to the console @@ -15,7 +14,6 @@ import { loadCliEnvironment } from './envLoader'; * If there is the -f arg write it to a file. Default to export.xml. If a value is provided, e.g. -f=export2.xml then write to export2.xml */ async function main() { - loadCliEnvironment(); const fileSystemService = new FileSystemService(); const basePath = fileSystemService.getBasePath(); diff --git a/src/cli/files.ts b/src/cli/files.ts index 53503558..64d97208 100644 --- a/src/cli/files.ts +++ b/src/cli/files.ts @@ -11,10 +11,8 @@ import type { AgentLLMs } from '#shared/agent/agent.model'; import { fastSelectFilesAgent } from '#swe/discovery/fastSelectFilesAgent'; import { selectFilesAgent } from '#swe/discovery/selectFilesAgentWithSearch'; import { parseProcessArgs } from './cli'; -import { loadCliEnvironment } from './envLoader'; async function main() { - loadCliEnvironment(); await initApplicationContext(); const agentLLMs: AgentLLMs = defaultLLMs(); diff --git a/src/cli/gaia.ts b/src/cli/gaia.ts index 2a1f99d2..be954bc6 100644 --- a/src/cli/gaia.ts +++ b/src/cli/gaia.ts @@ -14,7 +14,6 @@ import type { AgentLLMs } from '#shared/agent/agent.model'; import { lastText } from '#shared/llm/llm.model'; import type { LlmCall } from '#shared/llmCall/llmCall.model'; import { sleep } from '#utils/async-utils'; -import { loadCliEnvironment } from './envLoader'; const SYSTEM_PROMPT = `Finish your answer with the following template: FINAL ANSWER: [YOUR FINAL ANSWER]. YOUR FINAL ANSWER should be a number OR as few words as possible OR a comma separated list of numbers and/or strings. If you are asked for a number, don't use comma to write your number neither use units such as $ or percent sign unless specified otherwise. If you are asked for a string, don't use articles, neither abbreviations (e.g. for cities), and write the digits in plain text unless specified otherwise. If you are asked for a comma separated list, apply the above rules depending of whether the element to be put in the list is a number or a string.`; @@ -125,7 +124,6 @@ async function answerGaiaQuestion(task: GaiaQuestion): Promise { } async function main() { - loadCliEnvironment(); await initApplicationContext(); const llms = defaultLLMs(); diff --git a/src/cli/gen.ts b/src/cli/gen.ts index f14e42db..75a56da9 100644 --- a/src/cli/gen.ts +++ b/src/cli/gen.ts @@ -8,7 +8,6 @@ import { countTokens } from '#llm/tokens'; import { LLM, LlmMessage, ThinkingLevel, messageSources, messageText, system, user } from '#shared/llm/llm.model'; import { beep } from '#utils/beep'; import { parseProcessArgs } from './cli'; -import { loadCliEnvironment } from './envLoader'; import { LLM_CLI_ALIAS } from './llmAliases'; import { parsePromptWithImages } from './promptParser'; import { terminalLog } from './terminal'; @@ -17,7 +16,6 @@ import { terminalLog } from './terminal'; // ai gen -s="system prompt" 'input prompt' async function main() { - loadCliEnvironment(); const { initialPrompt: rawPrompt, llmId, flags } = parseProcessArgs(); const { textPrompt, userContent } = await parsePromptWithImages(rawPrompt); diff --git a/src/cli/index.ts b/src/cli/index.ts index 89dab1cd..fd355724 100644 --- a/src/cli/index.ts +++ b/src/cli/index.ts @@ -11,10 +11,8 @@ import { buildIndexDocs } from '#swe/index/repoIndexDocBuilder'; import { generateRepositoryMaps } from '#swe/index/repositoryMap'; import { getProjectInfos } from '#swe/projectDetection'; import { parseProcessArgs, saveAgentId } from './cli'; -import { loadCliEnvironment } from './envLoader'; async function main() { - loadCliEnvironment(); await initApplicationContext(); const agentLlms: AgentLLMs = defaultLLMs(); diff --git a/src/cli/morph.ts b/src/cli/morph.ts index bac813ab..f05580f0 100644 --- a/src/cli/morph.ts +++ b/src/cli/morph.ts @@ -20,7 +20,6 @@ import { CodeFunctions } from '#swe/codeFunctions'; import { MorphCodeAgent } from '#swe/morph/morphCoder'; import { registerErrorHandlers } from '../errorHandlers'; import { parseProcessArgs, saveAgentId } from './cli'; -import { loadCliEnvironment } from './envLoader'; import { resolveFunctionClasses } from './functionAliases'; async function resumeAgent(resumeAgentId: string, initialPrompt: string) { @@ -43,7 +42,6 @@ async function resumeAgent(resumeAgentId: string, initialPrompt: string) { } export async function main(): Promise { - loadCliEnvironment(); registerErrorHandlers(); await initApplicationContext(); const llms = defaultLLMs(); diff --git a/src/cli/query.ts b/src/cli/query.ts index 1dd76e02..5d43691b 100644 --- a/src/cli/query.ts +++ b/src/cli/query.ts @@ -11,11 +11,9 @@ import { logger } from '#o11y/logger'; import type { AgentLLMs } from '#shared/agent/agent.model'; import { queryWithFileSelection2 } from '#swe/discovery/selectFilesAgentWithSearch'; import { parseProcessArgs, saveAgentId } from './cli'; -import { loadCliEnvironment } from './envLoader'; import { parsePromptWithImages } from './promptParser'; async function main() { - loadCliEnvironment(); await initApplicationContext(); const agentLLMs: AgentLLMs = defaultLLMs(); const { initialPrompt: rawPrompt, resumeAgentId, flags } = parseProcessArgs(); diff --git a/src/cli/research.ts b/src/cli/research.ts index 39a061c5..90bd5ee6 100644 --- a/src/cli/research.ts +++ b/src/cli/research.ts @@ -8,7 +8,6 @@ import { PublicWeb } from '#functions/web/web'; import { defaultLLMs } from '#llm/services/defaultLlms'; import type { AgentLLMs } from '#shared/agent/agent.model'; import { parseProcessArgs, saveAgentId } from './cli'; -import { loadCliEnvironment } from './envLoader'; // Usage: // npm run research @@ -16,7 +15,6 @@ import { loadCliEnvironment } from './envLoader'; const llms: AgentLLMs = defaultLLMs(); export async function main(): Promise { - loadCliEnvironment(); const systemPrompt = readFileSync('src/cli/research-system', 'utf-8'); const { initialPrompt, resumeAgentId } = parseProcessArgs(); diff --git a/src/cli/review.ts b/src/cli/review.ts index b7effa0f..7397fe9a 100644 --- a/src/cli/review.ts +++ b/src/cli/review.ts @@ -10,10 +10,8 @@ import type { AgentLLMs } from '#shared/agent/agent.model'; import { performLocalBranchCodeReview } from '#swe/codeReview/local/localCodeReview'; import { beep } from '#utils/beep'; import { parseProcessArgs } from './cli'; -import { loadCliEnvironment } from './envLoader'; async function main() { - loadCliEnvironment(); await initApplicationContext(); const agentLlms: AgentLLMs = defaultLLMs(); diff --git a/src/cli/slack.ts b/src/cli/slack.ts index 5dae3a3f..e0deff55 100644 --- a/src/cli/slack.ts +++ b/src/cli/slack.ts @@ -1,9 +1,7 @@ import { initApplicationContext } from '#app/applicationContext'; import { sleep } from '#utils/async-utils'; -import { loadCliEnvironment } from './envLoader'; async function main() { - loadCliEnvironment(); await initApplicationContext(); const { SlackChatBotService } = await import('../modules/slack/slackModule.cjs'); const chatbot = new SlackChatBotService(); diff --git a/src/cli/startLocal.ts b/src/cli/startLocal.ts index f0ab79bb..c1a982ee 100644 --- a/src/cli/startLocal.ts +++ b/src/cli/startLocal.ts @@ -1,3 +1,7 @@ +import { existsSync, mkdirSync, writeFileSync } from 'node:fs'; +import { createRequire } from 'node:module'; +import { type Server as NetServer, createServer } from 'node:net'; +import path from 'node:path'; /** * @fileoverview * This script is the entry point for starting the backend server in a local development @@ -12,15 +16,7 @@ * frontend dev server) can read to discover the backend's port. * - Initializes and starts the Fastify server. */ -import '#fastify/trace-init/trace-init'; - -import { existsSync, mkdirSync, writeFileSync } from 'node:fs'; -import { open } from 'node:inspector'; -import { createRequire } from 'node:module'; -import { type Server as NetServer, createServer } from 'node:net'; -import path from 'node:path'; -import { logger } from '#o11y/logger'; -import { applyEnvFile, resolveEnvFilePath } from './envLoader'; +import { loadedEnvFilePath } from './envLoader'; type ServerFactory = () => NetServer; @@ -41,19 +37,16 @@ function setServerFactory(factory: ServerFactory | null): void { * This function orchestrates the entire startup sequence for local development. */ async function main(): Promise { - let envFilePath: string | undefined; - try { - // 1. Resolve and apply environment variables from a `.env` file. - envFilePath = resolveEnvFilePath(); - applyEnvFile(envFilePath); - } catch (err) { - logger.warn(err, '[start-local] no environment file found; continuing with existing process.env'); - } + const envFilePath = loadedEnvFilePath; process.env.NODE_ENV ??= 'development'; + const { logger } = await import('../o11y/ollyModule.cjs'); + const { initTrace } = await import('../fastify/trace-init/traceModule.cjs'); + initTrace(); + // Determine if this is the "default" repository setup (e.g., the main repo at $TYPEDAI_HOME) - // or a worktree or seperate clone. This affects port handling. + // or a worktree or separate clone. This affects port handling. // In the default setup, we use fixed ports (3000/9229) and fail if they're taken. // In a worktree/forked setup, we find the next available port to avoid conflicts. const repoRoot = path.resolve(process.cwd()); @@ -126,7 +119,9 @@ async function main(): Promise { require('../index'); } -main().catch((error) => { +main().catch(async (error) => { + // We need to import logger here again because the main() function might fail before the initial import is complete. + const { logger } = await import('../o11y/ollyModule.cjs'); logger.fatal(error, '[start-local] failed to start backend'); process.exitCode = 1; }); diff --git a/src/cli/summarize.ts b/src/cli/summarize.ts index 8bae23f8..3e5f01f1 100644 --- a/src/cli/summarize.ts +++ b/src/cli/summarize.ts @@ -9,10 +9,8 @@ import { SummarizerAgent } from '#functions/text/summarizer'; import { defaultLLMs } from '#llm/services/defaultLlms'; import type { AgentLLMs } from '#shared/agent/agent.model'; import { parseProcessArgs, saveAgentId } from './cli'; -import { loadCliEnvironment } from './envLoader'; async function main() { - loadCliEnvironment(); const agentLlms: AgentLLMs = defaultLLMs(); await initApplicationContext(); diff --git a/src/cli/swe.ts b/src/cli/swe.ts index 2675693a..215ed856 100644 --- a/src/cli/swe.ts +++ b/src/cli/swe.ts @@ -10,7 +10,6 @@ import type { AgentContext, AgentLLMs } from '#shared/agent/agent.model'; import { CodeEditingAgent } from '#swe/codeEditingAgent'; import { SoftwareDeveloperAgent } from '#swe/softwareDeveloperAgent'; import { parseProcessArgs, saveAgentId } from './cli'; -import { loadCliEnvironment } from './envLoader'; // Used to test the SoftwareDeveloperAgent @@ -18,7 +17,6 @@ import { loadCliEnvironment } from './envLoader'; // npm run swe async function main() { - loadCliEnvironment(); await initApplicationContext(); const llms: AgentLLMs = defaultLLMs(); diff --git a/src/cli/swebench.ts b/src/cli/swebench.ts index 097ab277..5bc2aae1 100644 --- a/src/cli/swebench.ts +++ b/src/cli/swebench.ts @@ -15,7 +15,6 @@ import { CodeFunctions } from '#swe/codeFunctions'; import { type SWEInstance, startContainer, stopContainer } from '../benchmarks/swebench/swe-bench-runner'; import { registerErrorHandlers } from '../errorHandlers'; import { parseProcessArgs } from './cli'; -import { loadCliEnvironment } from './envLoader'; async function loadDataset(datasetName: string, split: string): Promise { // const url = `https://huggingface.co/datasets/${datasetName}/resolve/main/swe-bench.json`; @@ -30,7 +29,6 @@ async function loadDataset(datasetName: string, split: string): Promise console.log(block)); + + // const results = await new Confluence().search( + // 'trafficguard-242710', + // `Just reviewing our GCP permissions and had a question: + + // Are owner permissions for non-infra team members really needed on trafficguard-242710? + + // boris.mesin@trafficguard.ai + // ivan.majnaric@trafficguard.ai + // tihomir.bregovic@trafficguard.ai + + // Are all owners. Does anyone remember why?`, + // ); + // for (const result of results) { + // // console.log(result.title.toUpperCase()); + // // console.log(result.bodyTokens); + // // console.log(result.filteredContentsTokens); + // // console.log(result.filteredContents); + // // console.log('\n=======================================\n') + // } + + // const results = await new GitLab().findDiscussionIdByNoteId('devops/experimental/ci-experiments', 6, 137698); + // // const results = await new Confluence().search('gitlab ops'); + // console.log(results); + // console.log(results?.notes?.map((d) => d.author)); + return; + + // console.log(await new GoogleCloud().getTraceSpans('tg-portal-prod', '7fa6095ecb12d8063deb2b8e87da068d')); + + // const threadMessages = await new SlackAPI().getConversationReplies('D08HGB1HF61', '1753971207.503459'); + // threadMessages.forEach((m) => console.log(m)); + // console.log(threadMessages.map((m) => `--------------------\n${m.text}`).join('\n')); + + // const store = new GoogleVectorStore({ + // project: 'tg-infra-dev', + // discoveryEngineLocation: 'global', + // collection: 'default_collection', + // dataStoreId: 'test-datastore', + // region: 'us-central1', + // embeddingModel: 'gemini-embedding-001', + // }); + // // await store.createDataStore(); + // // await store.indexRepository('./', './src/swe/vector/google'); + + // const result: ChunkSearchResult[] = await store.search('what languages are supported?'); + // for (const item of result) { + // // console.log(item.score); + // // console.log(item.document); + // } + + // console.log(src) + // console.log(await new GitLab().getBranches('devops/terraform/waf_infra')); + + // if (console.log) return; - const projects = await gitlab.getProjects(); - console.log(projects); - const cloned = await gitlab.cloneProject('devops/terraform/waf_infra', 'main'); - console.log(cloned); + const files = [ + 'shared/files/fileSystemService.ts', + 'shared/scm/versionControlSystem.ts', + // 'src/functions/scm/git.ts', + // 'src/functions/storage/fileSystemService.ts', + 'src/llm/services/mock-llm.ts', + 'src/swe/coder/validators/moduleAliasRule.ts', + 'src/swe/coder/validators/compositeValidator.ts', + 'src/swe/coder/fixSearchReplaceBlock.ts', + 'src/swe/coder/editSession.ts', + 'src/swe/coder/editApplier.ts', + 'src/swe/coder/reflectionUtils.ts', + 'src/swe/coder/searchReplaceCoder.test.ts', + 'src/swe/coder/searchReplaceCoder.ts', + ]; + + // const fileContent = await getFileSystem().readFilesAsXml(files); + // writeFileSync('coder.xml', fileContent); + + // const mad = MAD_Balanced(); + // llms().hard = mad; + // const plan = await mad.generateText( + // `${fileContent} + + // Analyse the SearchReplace coder and come up with a plan to improve/refactor it to make the code more clear, robust, etc + // `, + // { id: 'util' }, + // ); + + // console.log(plan); + // const result = await new CodeEditingAgent().implementDetailedDesignPlan(plan, files); + + // const result = await new LlmTools().analyseFile('test/llm/document.pdf', 'What is the content of this document?'); + // console.log(result); + // if (console) return; + + // const edited = await new SearchReplaceCoder().editFilesToMeetRequirements( + // 'Add another button, after the toggle thinking button, with the markdown material icon which calls a function called reformat() method on the component', + // ['frontend/src/app/modules/chat/conversation/conversation.component.html', 'frontend/src/app/modules/chat/conversation/conversation.component.ts'], + // [], + // ); + // console.log(await projectDetectionAgent()); + + // new TypescriptRefactor().moveFile('src/routes/agent/iteration-detail/[agentId]/[iterationNumber]/GET.ts', 'src/routes/agent/iteration-detail.ts'); + + // const gitlab = new GitLab(); + // const pipeline = await gitlab.getLatestMergeRequestPipeline(89, 34); + // const failedJobs = pipeline.jobs.filter(job => job.status === 'failed'); + // const failedJobs = await gitlab.getFailedJobLogs(89, 34); + // console.log(failedJobs); + // console.log(pipeline) + // console.log(await new GitLab().getProjects()); + // const fss = getFileSystem(); + // fss.setWorkingDirectory('frontend'); + // console.log(fss.getVcsRoot()); + // const fileSystemList = new FileSystemList(); + // console.log(await fileSystemList.listFiles('.', { recursive: true })); + // console.log('FileSystemList functionality temporarily disabled'); + // const gitlab = new GitLab(); + // + // const projects = await gitlab.getProjects(); + // console.log(projects); + // const cloned = await gitlab.cloneProject('devops/terraform/waf_infra', 'main'); + // console.log(cloned); // console.log(await new Jira().getJiraDetails('CLD-1685')); @@ -58,6 +211,77 @@ async function main() { // console.log(edited); } +async function morph() { + const fss = getFileSystem(); + const src = await new MorphAPI().edit( + await fss.readFile('src/agent/autonomous/codegen/codegenAutonomousAgent.ts'), + `... + jsFunctionProxies[\`_\${schema.name}\`] = async (...args: any[]) => { + // logger.info(\`args \${JSON.stringify(args)}\`); // Can be very verbose + // The system prompt instructs the generated code to use positional arguments. + const expectedParamNames: string[] = schema.parameters.map((p) => p.name); + + const { finalArgs, parameters } = processFunctionArguments(args, expectedParamNames); + + // Convert any Pyodide proxies in the parameters to plain JS objects before storing. + // This is necessary because Firestore cannot handle JsProxy objects. + // toJs() is recursive by default and will handle nested objects and arrays. + const convertedParameters: Record = {}; + for (const key of Object.keys(parameters)) { + const value = parameters[key]; + if (value && typeof value.toJs === 'function') { + convertedParameters[key] = value.toJs({ dict_converter: Object.fromEntries }); + } else { + convertedParameters[key] = value; + } + } + + try { + const functionResponse = await functionInstances[className][method](...finalArgs); + // Don't need to duplicate the content in the function call history + // TODO Would be nice to save over-written memory keys for history/debugging + let stdout = removeConsoleEscapeChars(functionResponse); + stdout = JSON.stringify(cloneAndTruncateBuffers(stdout)); + if (className === 'Agent' && method === 'saveMemory') convertedParameters[AGENT_SAVE_MEMORY_CONTENT_PARAM_NAME] = '(See entry)'; + if (className === 'Agent' && method === 'getMemory') stdout = '(See entry)'; + + let stdoutSummary: string | undefined; + if (stdout && stdout.length > FUNCTION_OUTPUT_THRESHOLD) { + stdoutSummary = await summarizeFunctionOutput(agent, agentPlanResponse, schema, convertedParameters, stdout); + } + + const functionCallResult: FunctionCallResult = { + iteration: agent.iterations, + function_name: schema.name, + parameters: convertedParameters, + stdout, + stdoutSummary, + }; + agent.functionCallHistory.push(functionCallResult); + currentIterationFunctionCalls.push(functionCallResult); + return functionResponse; + } catch (e) { + logger.warn(e, 'Error calling function'); + const stderr = removeConsoleEscapeChars(errorToString(e, false)); + if (stderr.length > FUNCTION_OUTPUT_THRESHOLD) { + // For function call errors, we might not need to summarize as aggressively as script errors. + // Keeping existing logic, or simplify if full error is always preferred for function calls. + // stderr = await summarizeFunctionOutput(agent, agentPlanResponse, schema, convertedParameters, stderr); + } + const functionCallResult: FunctionCallResult = { + iteration: agent.iterations, + function_name: schema.name, + parameters: convertedParameters, + stderr, + // stderrSummary: outputSummary, TODO + }; + agent.functionCallHistory.push(functionCallResult); + currentIterationFunctionCalls.push(functionCallResult); +...`, + ); + await fss.writeFile('src/agent/autonomous/codegen/codegenAutonomousAgent.ts', src); +} + main() .then(() => { console.log('done'); diff --git a/src/cli/watch.ts b/src/cli/watch.ts index ab05ce9b..c3220940 100644 --- a/src/cli/watch.ts +++ b/src/cli/watch.ts @@ -13,7 +13,6 @@ import { MorphEditor } from '#swe/morph/morphEditor'; import { beep } from '#utils/beep'; import { execCommand } from '#utils/exec'; import { parseProcessArgs } from './cli'; -import { loadCliEnvironment } from './envLoader'; /** * Walks up the directory tree from the file location until a `.git` folder is found. @@ -29,7 +28,6 @@ function findRepoRoot(startFilePath: string): string { } async function main() { - loadCliEnvironment(); // timeout avoids ReferenceError: Cannot access 'RateLimiter' before initialization setTimeout(() => { initInMemoryApplicationContext(); diff --git a/src/fastify/trace-init/trace-init.ts b/src/fastify/trace-init/trace-init.ts index ad4b7b29..759c6067 100644 --- a/src/fastify/trace-init/trace-init.ts +++ b/src/fastify/trace-init/trace-init.ts @@ -28,7 +28,7 @@ export function getServiceName(): string | undefined { * https://opentelemetry.io/docs/instrumentation/js/getting-started/nodejs/ * https://cloud.google.com/trace/docs/setup/nodejs-ot */ -function initTrace(): void { +export function initTrace(): void { if (initialized) return; initialized = true; @@ -116,5 +116,3 @@ export async function shutdownTrace(): Promise { console.error('Error shutting down trace:', error.message); } } - -initTrace(); diff --git a/src/fastify/trace-init/traceModule.cjs b/src/fastify/trace-init/traceModule.cjs new file mode 100644 index 00000000..395ca08b --- /dev/null +++ b/src/fastify/trace-init/traceModule.cjs @@ -0,0 +1,4 @@ +module.exports = { + initTrace: require('./trace-init.ts').initTrace, + shutdownTrace: require('./trace-init.ts').shutdownTrace, +}; diff --git a/src/llm/services/ai-llm.ts b/src/llm/services/ai-llm.ts index 9c3ed0e1..49fd0575 100644 --- a/src/llm/services/ai-llm.ts +++ b/src/llm/services/ai-llm.ts @@ -14,7 +14,7 @@ import { } from 'ai'; import { addCost, agentContext } from '#agent/agentContextLocalStorage'; import { cloneAndTruncateBuffers } from '#agent/trimObject'; -import { appContext } from '#app/applicationContext'; +import { ApplicationContext } from '#app/applicationTypes'; import { BaseLLM, type BaseLlmConfig } from '#llm/base-llm'; import { type CreateLlmRequest, callStack } from '#llm/llmCallService/llmCall'; import { logger } from '#o11y/logger'; @@ -44,6 +44,14 @@ import { quotaRetry } from '#utils/quotaRetry'; type GenerateTextArgs = Parameters[0]; type StreamTextArgs = Parameters[0]; +// Lazy load to fix dependency cycle +let _appContextModule: typeof import('#app/applicationContext') | undefined; +function appContext(): ApplicationContext { + _appContextModule ??= require('#app/applicationContext'); + if (!_appContextModule) throw new Error('appContext not initialized'); + return _appContextModule.appContext(); +} + // Helper to convert DataContent | URL to string for our UI-facing models function convertDataContentToString(content: string | URL | Uint8Array | ArrayBuffer | Buffer | undefined): string { if (content === undefined) return ''; diff --git a/src/log-loader.js b/src/log-loader.js index 3a7c3553..c93dbead 100644 --- a/src/log-loader.js +++ b/src/log-loader.js @@ -4,29 +4,77 @@ const path = require('node:path'); const originalRequire = Module.prototype.require; const loadOrder = []; +const loadedFrom = new Map(); +const requireStack = []; // Track the current require chain Module.prototype.require = function (id) { + const parentPath = this.filename ? path.relative(process.cwd(), this.filename) : ''; + + try { + const resolvedPath = Module._resolveFilename(id, this); + const relativePath = path.relative(process.cwd(), resolvedPath); + + // Push to stack before requiring + if (!resolvedPath.includes('node_modules')) { + requireStack.push(relativePath); + } + } catch (e) { + // Couldn't resolve, skip + } + // biome-ignore lint/style/noArguments: ok const result = originalRequire.apply(this, arguments); - // Get the resolved path try { const resolvedPath = Module._resolveFilename(id, this); - // Filter out node_modules + // Filter out node_modules completely if (!resolvedPath.includes('node_modules')) { const relativePath = path.relative(process.cwd(), resolvedPath); - if (!loadOrder.includes(relativePath)) { - loadOrder.push(relativePath); - console.log(`[${loadOrder.length}] Loaded: ${relativePath}`); + + // Only log if parent is also not from node_modules + if (!parentPath.includes('node_modules')) { + if (!loadedFrom.has(relativePath)) { + loadOrder.push(relativePath); + loadedFrom.set(relativePath, parentPath); + console.log(`[${loadOrder.length}] Loaded: ${relativePath}`); + console.log(` From: ${parentPath}`); + } else { + // Only log cache hits for actual source files being re-required + if (relativePath.startsWith('src/') || relativePath.startsWith('shared/')) { + console.log(`[CACHE] ${relativePath}`); + console.log(` From: ${parentPath} (originally from: ${loadedFrom.get(relativePath)})`); + } + } } } } catch (e) { // Built-in modules or modules that can't be resolved + } finally { + // Pop from stack after requiring + requireStack.pop(); } return result; }; -// Export to use elsewhere if needed +// Catch unhandled errors and show the require stack +process.on('uncaughtException', (err) => { + console.error('\n🔥 ERROR OCCURRED DURING MODULE LOADING'); + console.error('Current require stack:'); + requireStack.forEach((file, i) => { + console.error(` ${i + 1}. ${file}`); + }); + console.error('\nError:', err.message); + console.error(err.stack); + process.exit(1); +}); + +// Log already loaded files at startup +const alreadyLoaded = Object.keys(require.cache) + .filter((p) => !p.includes('node_modules')) + .map((p) => path.relative(process.cwd(), p)); + +console.log('Already loaded before hook:', alreadyLoaded); + module.exports = { loadOrder }; diff --git a/src/modules/slack/slackChatBotService.ts b/src/modules/slack/slackChatBotService.ts index b8885e25..96468732 100644 --- a/src/modules/slack/slackChatBotService.ts +++ b/src/modules/slack/slackChatBotService.ts @@ -4,6 +4,7 @@ import { llms } from '#agent/agentContextLocalStorage'; import { AgentExecution, isAgentExecuting } from '#agent/agentExecutions'; import { getLastFunctionCallArg } from '#agent/autonomous/agentCompletion'; import { resumeCompletedWithUpdatedUserRequest, startAgent } from '#agent/autonomous/autonomousAgentRunner'; +import { registerCompletedHandler } from '#agent/completionHandlerRegistry'; import { appContext } from '#app/applicationContext'; import { GoogleCloud } from '#functions/cloud/google/google-cloud'; import { Confluence } from '#functions/confluence'; @@ -127,7 +128,7 @@ export class SlackChatBotService implements ChatBotService, AgentCompleted { this.botMentionCache.clear(); } - async initSlack(): Promise { + async initSlack(startSocketListener = false): Promise { if (slackApp) { logger.warn('Slack app already initialized'); return; @@ -156,7 +157,7 @@ export class SlackChatBotService implements ChatBotService, AgentCompleted { logger.error(error, 'Failed to get bot user ID'); } - if (config.socketMode && config.autoStart) { + if (config.socketMode && (config.autoStart || startSocketListener === true)) { // Listen for messages in channels slackApp.event('message', async ({ event, say }) => { this.handleMessage(event, say); @@ -357,3 +358,5 @@ export class SlackChatBotService implements ChatBotService, AgentCompleted { return messages; } } + +registerCompletedHandler(new SlackChatBotService()); diff --git a/src/o11y/logger.ts b/src/o11y/logger.ts index 7d4a37d5..18dd6c0d 100644 --- a/src/o11y/logger.ts +++ b/src/o11y/logger.ts @@ -16,17 +16,6 @@ const PinoLevelToSeverityLookup: any = { const reportErrors = process.env.REPORT_ERROR_LOGS?.toLowerCase() === 'true'; -// When running locally log in a human-readable format and not JSON -const transport = - process.env.LOG_PRETTY === 'true' - ? { - target: 'pino-pretty', - options: { - colorize: true, - }, - } - : undefined; - const transportTargets: any[] = []; // // // When running locally log in a human-readable format and not JSO @@ -53,10 +42,6 @@ if (process.env.LOG_PRETTY === 'true') { // }) // } // -// const transport = Pino.transport({ -// targets: transportTargets, -// }); -// const multi = pino.multistream(targets) let logEnricherFn: ((logObj: any) => void) | undefined = undefined; @@ -70,81 +55,83 @@ const standardFields = new Set(['level', 'time', 'pid', 'hostname', 'msg', 'mess /** * Pino logger configured for a Google Cloud environment. */ +const pinoFormatters = + transportTargets.length > 0 + ? undefined + : { + log(obj) { + // Add stack_trace if an error is present + if (obj?.err) { + const error = obj.err; + if (error instanceof Error) { + obj.stack_trace = error.stack; + } else if (typeof error === 'object' && 'stack' in error && typeof error.stack === 'string') { + obj.stack_trace = error.stack; + } + // Optionally remove the original err object if you don’t want it duplicated + // delete obj.err; + } + + if (logEnricherFn) { + logEnricherFn(obj); + } + return obj; + }, + level(label: string, number: number) { + const severity = PinoLevelToSeverityLookup[label] ?? 'INFO'; + if (reportErrors && isGoogleCloud && (label === 'error' || label === 'fatal')) { + return { + severity, + '@type': 'type.googleapis.com/google.devtools.clouderrorreporting.v1beta1.ReportedErrorEvent', + }; + } + return { severity, level: number }; + }, + }; + +const pinoHooks = + transportTargets.length > 0 + ? {} // When transports are active, provide an empty hooks object + : { + logMethod(args, method) { + let objIndex = -1; + let msgIndex = -1; + + // Add custom keys to message so logs messages are like "the message [key1, key2]" + + // Identify the object and message arguments + for (let i = 0; i < args.length; i++) { + if (objIndex === -1 && args[i] && typeof args[i] === 'object') objIndex = i; + if (msgIndex === -1 && typeof args[i] === 'string') msgIndex = i; + } + + if (objIndex !== -1) { + const obj = args[objIndex] as Record; + const customKeys = Object.keys(obj).filter((k) => !standardFields.has(k)); + + if (customKeys.length > 0) { + const suffix = ` [${customKeys.join(', ')}]`; + + if (msgIndex !== -1) { + // Append to existing message + args[msgIndex] = `${args[msgIndex]}${suffix}`; + } else { + // No message was provided; create one + args.push(suffix); + } + } + } + + // Call the original logging method with modified arguments + return method.apply(this, args); + }, + }; + export const logger: Pino.Logger = Pino({ level: logLevel, messageKey: isGoogleCloud ? 'message' : 'msg', timestamp: !isGoogleCloud, // Provided by GCP log agents - formatters: { - level(label: string, number: number) { - // const severity = PinoLevelToSeverityLookup[label] || PinoLevelToSeverityLookup.info; - // const level = number; - // return { - // severity: PinoLevelToSeverityLookup[label] || PinoLevelToSeverityLookup.info, - // level: number, - // }; - - // const pinoLevel = label as Level; - const severity = PinoLevelToSeverityLookup[label] ?? 'INFO'; - if (reportErrors && isGoogleCloud && (label === 'error' || label === 'fatal')) { - return { - severity, - '@type': 'type.googleapis.com/google.devtools.clouderrorreporting.v1beta1.ReportedErrorEvent', - }; - } - return { severity, level: number }; - }, - log(obj) { - // Add stack_trace if an error is present - if (obj?.err) { - const error = obj.err; - if (error instanceof Error) { - obj.stack_trace = error.stack; - } else if (typeof error === 'object' && 'stack' in error && typeof error.stack === 'string') { - obj.stack_trace = error.stack; - } - // Optionally remove the original err object if you don’t want it duplicated - // delete obj.err; - } - - if (logEnricherFn) { - logEnricherFn(obj); - } - return obj; - }, - }, - hooks: { - logMethod(args, method) { - let objIndex = -1; - let msgIndex = -1; - - // Add custom keys to message so logs messages are like "the message [key1, key2]" - - // Identify the object and message arguments - for (let i = 0; i < args.length; i++) { - if (objIndex === -1 && args[i] && typeof args[i] === 'object') objIndex = i; - if (msgIndex === -1 && typeof args[i] === 'string') msgIndex = i; - } - - if (objIndex !== -1) { - const obj = args[objIndex] as Record; - const customKeys = Object.keys(obj).filter((k) => !standardFields.has(k)); - - if (customKeys.length > 0) { - const suffix = ` [${customKeys.join(', ')}]`; - - if (msgIndex !== -1) { - // Append to existing message - args[msgIndex] = `${args[msgIndex]}${suffix}`; - } else { - // No message was provided; create one - args.push(suffix); - } - } - } - - // Call the original logging method with modified arguments - return method.apply(this, args); - }, - }, - transport, + formatters: pinoFormatters, + hooks: pinoHooks, + transport: transportTargets.length > 0 ? { targets: transportTargets } : undefined, }); diff --git a/src/o11y/ollyModule.cjs b/src/o11y/ollyModule.cjs new file mode 100644 index 00000000..4d61c06f --- /dev/null +++ b/src/o11y/ollyModule.cjs @@ -0,0 +1,3 @@ +module.exports = { + logger: require('./logger.ts').logger, +}; diff --git a/src/routes/slack/slackRoutes.ts b/src/routes/slack/slackRoutes.ts index 04602112..4ab845a0 100644 --- a/src/routes/slack/slackRoutes.ts +++ b/src/routes/slack/slackRoutes.ts @@ -16,7 +16,7 @@ export async function slackRoutes(fastify: AppFastifyInstance): Promise { registerApiRoute(fastify, SLACK_API.start, async (_req, reply) => { if (!slackConfig().socketMode) return sendBadRequest(reply, 'Slack chatbot is not configured to use socket mode'); try { - await slackChatBotService.initSlack(); + await slackChatBotService.initSlack(true); return reply.sendJSON({ success: true }); } catch (error) { logger.error(error, 'Failed to start Slack chatbot [error]'); diff --git a/src/swe/discovery/selectFilesAgentWithSearch.ts b/src/swe/discovery/selectFilesAgentWithSearch.ts index a54f663d..060d8c28 100644 --- a/src/swe/discovery/selectFilesAgentWithSearch.ts +++ b/src/swe/discovery/selectFilesAgentWithSearch.ts @@ -1,6 +1,5 @@ import path from 'node:path'; import { agentContext, getFileSystem } from '#agent/agentContextLocalStorage'; -import { ReasonerDebateLLM } from '#llm/multi-agent/reasoning-debate'; import { extractTag } from '#llm/responseParsers'; import { defaultLLMs } from '#llm/services/defaultLlms'; import { logger } from '#o11y/logger'; @@ -131,9 +130,9 @@ At the very end of the block, add a line in the format "Confidence: LEV // Perform the additional LLM call to get the answer const xhard = resolvedLLMs.xhard; const llm: LLM = opts.useXtraHardLLM && xhard ? xhard : resolvedLLMs.hard; - const thinking: ThinkingLevel = llm instanceof ReasonerDebateLLM ? 'none' : 'high'; + // const thinking: ThinkingLevel = llm instanceof ReasonerDebateLLM ? 'none' : 'high'; - let answer = await llm.generateText(messages, { id: 'Select Files query Answer', thinking }); + let answer = await llm.generateText(messages, { id: 'Select Files query Answer', thinking: 'high' }); try { answer = extractTag(answer, 'result'); } catch {}