From 9662521b7520ab9c61fe975bb0465ee66bd13a52 Mon Sep 17 00:00:00 2001 From: Kartik Sharma Date: Mon, 2 Feb 2026 23:18:30 +0530 Subject: [PATCH 1/8] feat: pass project context explicitly via experimental_context --- .../src/agents/tools/definitions/display-chart.ts | 2 +- .../src/agents/tools/definitions/execute-sql.ts | 2 +- apps/backend/src/agents/tools/definitions/grep.ts | 2 +- apps/backend/src/agents/tools/definitions/list.ts | 2 +- apps/backend/src/agents/tools/definitions/read.ts | 2 +- .../src/agents/tools/definitions/search.ts | 2 +- .../src/agents/tools/functions/display-chart.ts | 6 +++++- .../src/agents/tools/functions/execute-sql.ts | 5 +++-- apps/backend/src/agents/tools/functions/grep.ts | 15 ++++++--------- apps/backend/src/agents/tools/functions/list.ts | 5 +++-- apps/backend/src/agents/tools/functions/read.ts | 5 +++-- apps/backend/src/agents/tools/functions/search.ts | 5 +++-- apps/backend/src/services/agent.service.ts | 14 ++++++++++++++ apps/backend/src/types/tools.ts | 13 ++++++++++--- 14 files changed, 53 insertions(+), 27 deletions(-) diff --git a/apps/backend/src/agents/tools/definitions/display-chart.ts b/apps/backend/src/agents/tools/definitions/display-chart.ts index 986a516b..a57efc1e 100644 --- a/apps/backend/src/agents/tools/definitions/display-chart.ts +++ b/apps/backend/src/agents/tools/definitions/display-chart.ts @@ -7,5 +7,5 @@ export default { description, inputSchema, outputSchema, - execute, + execute: (args, _context) => execute(args, _context), } satisfies ToolDefinition; diff --git a/apps/backend/src/agents/tools/definitions/execute-sql.ts b/apps/backend/src/agents/tools/definitions/execute-sql.ts index e65cee79..fdb82a47 100644 --- a/apps/backend/src/agents/tools/definitions/execute-sql.ts +++ b/apps/backend/src/agents/tools/definitions/execute-sql.ts @@ -7,5 +7,5 @@ export default { description, inputSchema, outputSchema, - execute, + execute: (args, context) => execute(args, context), } satisfies ToolDefinition; diff --git a/apps/backend/src/agents/tools/definitions/grep.ts b/apps/backend/src/agents/tools/definitions/grep.ts index 5a9ef833..af26e465 100644 --- a/apps/backend/src/agents/tools/definitions/grep.ts +++ b/apps/backend/src/agents/tools/definitions/grep.ts @@ -7,5 +7,5 @@ export default { description, inputSchema, outputSchema, - execute, + execute: (args, context) => execute(args, context), } satisfies ToolDefinition; diff --git a/apps/backend/src/agents/tools/definitions/list.ts b/apps/backend/src/agents/tools/definitions/list.ts index 70ef68f3..d6e9ba0c 100644 --- a/apps/backend/src/agents/tools/definitions/list.ts +++ b/apps/backend/src/agents/tools/definitions/list.ts @@ -7,5 +7,5 @@ export default { description, inputSchema, outputSchema, - execute, + execute: (args, context) => execute(args, context), } satisfies ToolDefinition; diff --git a/apps/backend/src/agents/tools/definitions/read.ts b/apps/backend/src/agents/tools/definitions/read.ts index df446c25..dab95ef8 100644 --- a/apps/backend/src/agents/tools/definitions/read.ts +++ b/apps/backend/src/agents/tools/definitions/read.ts @@ -7,5 +7,5 @@ export default { description, inputSchema, outputSchema, - execute, + execute: (args, context) => execute(args, context), } satisfies ToolDefinition; diff --git a/apps/backend/src/agents/tools/definitions/search.ts b/apps/backend/src/agents/tools/definitions/search.ts index d6176085..7f9d03ff 100644 --- a/apps/backend/src/agents/tools/definitions/search.ts +++ b/apps/backend/src/agents/tools/definitions/search.ts @@ -7,5 +7,5 @@ export default { description, inputSchema, outputSchema, - execute, + execute: (args, context) => execute(args, context), } satisfies ToolDefinition; diff --git a/apps/backend/src/agents/tools/functions/display-chart.ts b/apps/backend/src/agents/tools/functions/display-chart.ts index 13729897..ee6b3ebc 100644 --- a/apps/backend/src/agents/tools/functions/display-chart.ts +++ b/apps/backend/src/agents/tools/functions/display-chart.ts @@ -1,6 +1,10 @@ +import type { ToolContext } from '../../../types/tools'; import type { Input, Output } from '../schema/display-chart'; -export const execute = async ({ chart_type: chartType, x_axis_key: xAxisKey, series }: Input): Promise => { +export const execute = async ( + { chart_type: chartType, x_axis_key: xAxisKey, series }: Input, + _context?: ToolContext, +): Promise => { // Validate xAxisKey is provided for bar/area charts if ((chartType === 'bar' || chartType === 'line') && !xAxisKey) { return { error: `xAxisKey is required for ${chartType} charts.` }; diff --git a/apps/backend/src/agents/tools/functions/execute-sql.ts b/apps/backend/src/agents/tools/functions/execute-sql.ts index 10e26b35..f022b1ca 100644 --- a/apps/backend/src/agents/tools/functions/execute-sql.ts +++ b/apps/backend/src/agents/tools/functions/execute-sql.ts @@ -1,8 +1,9 @@ +import type { ToolContext } from '../../../types/tools'; import { getProjectFolder } from '../../../utils/tools'; import type { Input, Output } from '../schema/execute-sql'; -export const execute = async ({ sql_query, database_id }: Input): Promise => { - const naoProjectFolder = getProjectFolder(); +export const execute = async ({ sql_query, database_id }: Input, context?: ToolContext): Promise => { + const naoProjectFolder = context?.projectPath ?? getProjectFolder(); const response = await fetch(`${process.env.FASTAPI_URL}/execute_sql`, { method: 'POST', diff --git a/apps/backend/src/agents/tools/functions/grep.ts b/apps/backend/src/agents/tools/functions/grep.ts index 2fdad771..ae8a6a5a 100644 --- a/apps/backend/src/agents/tools/functions/grep.ts +++ b/apps/backend/src/agents/tools/functions/grep.ts @@ -2,6 +2,7 @@ import { spawn } from 'child_process'; import fs from 'fs'; import path from 'path'; +import type { ToolContext } from '../../../types/tools'; import { getProjectFolder, isWithinProjectFolder, @@ -51,15 +52,11 @@ interface RipgrepMatch { context_after?: string[]; } -export const execute = async ({ - pattern, - path: searchPath, - glob, - case_insensitive, - context_lines, - max_results = 100, -}: Input): Promise => { - const projectFolder = getProjectFolder(); +export const execute = async ( + { pattern, path: searchPath, glob, case_insensitive, context_lines, max_results = 100 }: Input, + context?: ToolContext, +): Promise => { + const projectFolder = context?.projectPath ?? getProjectFolder(); const rgPath = getRipgrepPath(); // Determine the search path diff --git a/apps/backend/src/agents/tools/functions/list.ts b/apps/backend/src/agents/tools/functions/list.ts index 27f242ba..518825bb 100644 --- a/apps/backend/src/agents/tools/functions/list.ts +++ b/apps/backend/src/agents/tools/functions/list.ts @@ -1,11 +1,12 @@ import fs from 'fs/promises'; import path from 'path'; +import type { ToolContext } from '../../../types/tools'; import { getProjectFolder, shouldExcludeEntry, toRealPath, toVirtualPath } from '../../../utils/tools'; import type { Input, Output } from '../schema/list'; -export const execute = async ({ path: filePath }: Input): Promise => { - const projectFolder = getProjectFolder(); +export const execute = async ({ path: filePath }: Input, context?: ToolContext): Promise => { + const projectFolder = context?.projectPath ?? getProjectFolder(); const realPath = toRealPath(filePath, projectFolder); // Get the relative path of the parent directory for naoignore matching diff --git a/apps/backend/src/agents/tools/functions/read.ts b/apps/backend/src/agents/tools/functions/read.ts index 3f57b737..f782bfc3 100644 --- a/apps/backend/src/agents/tools/functions/read.ts +++ b/apps/backend/src/agents/tools/functions/read.ts @@ -1,10 +1,11 @@ import fs from 'fs/promises'; +import type { ToolContext } from '../../../types/tools'; import { getProjectFolder, toRealPath } from '../../../utils/tools'; import type { Input, Output } from '../schema/read'; -export const execute = async ({ file_path }: Input): Promise => { - const projectFolder = getProjectFolder(); +export const execute = async ({ file_path }: Input, context?: ToolContext): Promise => { + const projectFolder = context?.projectPath ?? getProjectFolder(); const realPath = toRealPath(file_path, projectFolder); const content = await fs.readFile(realPath, 'utf-8'); diff --git a/apps/backend/src/agents/tools/functions/search.ts b/apps/backend/src/agents/tools/functions/search.ts index 2715bd0a..1b9f278f 100644 --- a/apps/backend/src/agents/tools/functions/search.ts +++ b/apps/backend/src/agents/tools/functions/search.ts @@ -2,11 +2,12 @@ import fs from 'fs/promises'; import { glob } from 'glob'; import path from 'path'; +import type { ToolContext } from '../../../types/tools'; import { getProjectFolder, isWithinProjectFolder, loadNaoignorePatterns, toVirtualPath } from '../../../utils/tools'; import type { Input, Output } from '../schema/search'; -export const execute = async ({ pattern }: Input): Promise => { - const projectFolder = getProjectFolder(); +export const execute = async ({ pattern }: Input, context?: ToolContext): Promise => { + const projectFolder = context?.projectPath ?? getProjectFolder(); // Sanitize pattern to prevent escaping project folder if (path.isAbsolute(pattern)) { diff --git a/apps/backend/src/services/agent.service.ts b/apps/backend/src/services/agent.service.ts index ce6c9d3b..ba093d8b 100644 --- a/apps/backend/src/services/agent.service.ts +++ b/apps/backend/src/services/agent.service.ts @@ -10,6 +10,7 @@ import { getInstructions } from '../agents/prompt'; import { createProviderModel } from '../agents/providers'; import { tools } from '../agents/tools'; import * as chatQueries from '../queries/chat.queries'; +import * as projectQueries from '../queries/project.queries'; import * as llmConfigQueries from '../queries/project-llm-config.queries'; import { UIChat, UIMessage } from '../types/chat'; import { convertToTokenUsage } from '../utils/chat'; @@ -149,9 +150,22 @@ class AgentManager { }); } + // Fetch project path and run agent within project context + const project = await projectQueries.getProjectById(this.chat.projectId); + if (!project) { + throw new Error(`Project not found: ${this.chat.projectId}`); + } + if (!project.path) { + throw new Error(`Project path not configured: ${this.chat.projectId}`); + } + result = await this._agent.stream({ messages: await convertToModelMessages(messages), abortSignal: this._abortController.signal, + // @ts-expect-error - experimental_context is not yet in the types + experimental_context: { + projectPath: project.path, + }, }); writer.merge(result.toUIMessageStream({})); diff --git a/apps/backend/src/types/tools.ts b/apps/backend/src/types/tools.ts index 33f7cc0c..f0144add 100644 --- a/apps/backend/src/types/tools.ts +++ b/apps/backend/src/types/tools.ts @@ -3,19 +3,23 @@ import type { z } from 'zod/v3'; type ZodSchema = z.ZodTypeAny; +export interface ToolContext { + projectPath?: string; +} + export interface ToolDefinition { name: string; description: string; inputSchema: TInput; outputSchema: TOutput; - execute: (input: z.infer) => Promise>; + execute: (input: z.infer, context: ToolContext) => Promise>; } export type AnyToolDefinition = ToolDefinition; /** * Creates a tool with consistent structure. - * Schemas are exported separately from each tool's schema.ts file. + * Adapts AI SDK's experimental_context to the tool's expected context. */ export function createTool( definition: ToolDefinition, @@ -25,7 +29,10 @@ export function createTool( description: definition.description, inputSchema: definition.inputSchema, outputSchema: definition.outputSchema, - execute: definition.execute, + execute: async (input, { experimental_context }) => { + const context = (experimental_context as ToolContext) ?? {}; + return definition.execute(input, context); + }, }), }; } From 6e6d74b7c04a5df0c50a24c366b3be220b2994e5 Mon Sep 17 00:00:00 2001 From: Kartik Sharma Date: Tue, 3 Feb 2026 22:51:18 +0530 Subject: [PATCH 2/8] fix: correct indentation for prettier --- apps/backend/src/services/agent.service.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/backend/src/services/agent.service.ts b/apps/backend/src/services/agent.service.ts index 47b84911..c87a4125 100644 --- a/apps/backend/src/services/agent.service.ts +++ b/apps/backend/src/services/agent.service.ts @@ -163,7 +163,7 @@ class AgentManager { } if (!project.path) { throw new Error(`Project path not configured: ${this.chat.projectId}`); - } + } const messages = await this._buildInitialMessages(uiMessages, this._modelSelection.provider); result = await this._agent.stream({ From d92ddcecaae2108fb6a01fc66f0f5a90aa375631 Mon Sep 17 00:00:00 2001 From: MatLBS <135004599+MatLBS@users.noreply.github.com> Date: Tue, 10 Feb 2026 17:08:06 +0100 Subject: [PATCH 3/8] branch rebased + changes applied in agent.service --- apps/backend/src/services/agent.service.ts | 12 +++--------- apps/backend/src/utils/chat.ts | 13 +++++++++++++ 2 files changed, 16 insertions(+), 9 deletions(-) diff --git a/apps/backend/src/services/agent.service.ts b/apps/backend/src/services/agent.service.ts index 0cdbd684..aa20a1d0 100644 --- a/apps/backend/src/services/agent.service.ts +++ b/apps/backend/src/services/agent.service.ts @@ -13,10 +13,9 @@ import { getInstructions } from '../agents/prompt'; import { CACHE_1H, CACHE_5M, createProviderModel } from '../agents/providers'; import { tools } from '../agents/tools'; import * as chatQueries from '../queries/chat.queries'; -import * as projectQueries from '../queries/project.queries'; import * as llmConfigQueries from '../queries/project-llm-config.queries'; import { TokenCost, TokenUsage, UIChat, UIMessage } from '../types/chat'; -import { convertToCost, convertToTokenUsage } from '../utils/chat'; +import { convertToCost, convertToTokenUsage, retrieveProjectById } from '../utils/chat'; import { getDefaultModelId, getEnvApiKey, getEnvModelSelections, ModelSelection } from '../utils/llm'; export type { ModelSelection }; @@ -177,13 +176,8 @@ class AgentManager { } // Fetch project path and run agent within project context - const project = await projectQueries.getProjectById(this.chat.projectId); - if (!project) { - throw new Error(`Project not found: ${this.chat.projectId}`); - } - if (!project.path) { - throw new Error(`Project path not configured: ${this.chat.projectId}`); - } + const project = await retrieveProjectById(this.chat.projectId); + const messages = await this._buildModelMessages(uiMessages); result = await this._agent.stream({ diff --git a/apps/backend/src/utils/chat.ts b/apps/backend/src/utils/chat.ts index d00cddf1..01973fd8 100644 --- a/apps/backend/src/utils/chat.ts +++ b/apps/backend/src/utils/chat.ts @@ -1,6 +1,8 @@ import { LanguageModelUsage } from 'ai'; import { LLM_PROVIDERS } from '../agents/providers'; +import * as projectQueries from '../queries/project.queries'; +import { DBProject } from '../queries/project-slack-config.queries'; import { TokenCost, TokenUsage } from '../types/chat'; import { LlmProvider } from '../types/llm'; @@ -51,3 +53,14 @@ export const extractLastTextFromMessage = (message: { parts: { type: string; tex } return ''; }; + +export const retrieveProjectById = async (projectId: string): Promise => { + const project = await projectQueries.getProjectById(projectId); + if (!project) { + throw new Error(`Project not found: ${projectId}`); + } + if (!project.path) { + throw new Error(`Project path not configured: ${projectId}`); + } + return project; +}; From ed8958f3019b10370dbb601ac328dfdbc3f14a73 Mon Sep 17 00:00:00 2001 From: MatLBS <135004599+MatLBS@users.noreply.github.com> Date: Thu, 12 Feb 2026 11:54:42 +0100 Subject: [PATCH 4/8] reimplemented context with projectPath in tool execution --- apps/backend/src/agents/tools/execute-sql.ts | 12 ++++---- apps/backend/src/agents/tools/grep.ts | 19 ++++++------- apps/backend/src/agents/tools/list.ts | 10 +++---- apps/backend/src/agents/tools/read.ts | 10 +++---- apps/backend/src/agents/tools/search.ts | 10 +++---- apps/backend/src/types/tools.ts | 29 ++++++++++++++++++++ apps/backend/src/utils/tools.ts | 12 -------- 7 files changed, 59 insertions(+), 43 deletions(-) create mode 100644 apps/backend/src/types/tools.ts diff --git a/apps/backend/src/agents/tools/execute-sql.ts b/apps/backend/src/agents/tools/execute-sql.ts index 06ad74e5..371959e7 100644 --- a/apps/backend/src/agents/tools/execute-sql.ts +++ b/apps/backend/src/agents/tools/execute-sql.ts @@ -1,12 +1,14 @@ import type { executeSql } from '@nao/shared/tools'; import { executeSql as schemas } from '@nao/shared/tools'; -import { tool } from 'ai'; import { env } from '../../env'; -import { getProjectFolder } from '../../utils/tools'; +import { createTool, type ToolContext } from '../../types/tools'; -export async function executeQuery({ sql_query, database_id }: executeSql.Input): Promise { - const naoProjectFolder = getProjectFolder(); +export async function executeQuery( + { sql_query, database_id }: executeSql.Input, + context?: ToolContext, +): Promise { + const naoProjectFolder = context?.projectPath; const response = await fetch(`${env.FASTAPI_URL}/execute_sql`, { method: 'POST', @@ -32,7 +34,7 @@ export async function executeQuery({ sql_query, database_id }: executeSql.Input) }; } -export default tool({ +export default createTool({ description: 'Execute a SQL query against the connected database and return the results. If multiple databases are configured, specify the database_id.', inputSchema: schemas.InputSchema, diff --git a/apps/backend/src/agents/tools/grep.ts b/apps/backend/src/agents/tools/grep.ts index 538f5cd9..97c53043 100644 --- a/apps/backend/src/agents/tools/grep.ts +++ b/apps/backend/src/agents/tools/grep.ts @@ -1,16 +1,10 @@ import { grep } from '@nao/shared/tools'; -import { tool } from 'ai'; import { spawn } from 'child_process'; import fs from 'fs'; import path from 'path'; -import { - getProjectFolder, - isWithinProjectFolder, - loadNaoignorePatterns, - toRealPath, - toVirtualPath, -} from '../../utils/tools'; +import { createTool } from '../../types/tools'; +import { isWithinProjectFolder, loadNaoignorePatterns, toRealPath, toVirtualPath } from '../../utils/tools'; /** * Gets the path to the ripgrep binary. @@ -52,12 +46,15 @@ interface RipgrepMatch { context_after?: string[]; } -export default tool({ +export default createTool({ description: 'Search for text patterns in files using ripgrep. Supports regex patterns and respects .gitignore.', inputSchema: grep.InputSchema, outputSchema: grep.OutputSchema, - execute: async ({ pattern, path: searchPath, glob, case_insensitive, context_lines, max_results = 100 }) => { - const projectFolder = getProjectFolder(); + execute: async ( + { pattern, path: searchPath, glob, case_insensitive, context_lines, max_results = 100 }, + context, + ) => { + const projectFolder = context.projectPath || ''; const rgPath = getRipgrepPath(); // Determine the search path diff --git a/apps/backend/src/agents/tools/list.ts b/apps/backend/src/agents/tools/list.ts index 5e65c057..5efc3d4b 100644 --- a/apps/backend/src/agents/tools/list.ts +++ b/apps/backend/src/agents/tools/list.ts @@ -1,16 +1,16 @@ import { list } from '@nao/shared/tools'; -import { tool } from 'ai'; import fs from 'fs/promises'; import path from 'path'; -import { getProjectFolder, shouldExcludeEntry, toRealPath, toVirtualPath } from '../../utils/tools'; +import { createTool } from '../../types/tools'; +import { shouldExcludeEntry, toRealPath, toVirtualPath } from '../../utils/tools'; -export default tool({ +export default createTool({ description: 'List files and directories at the specified path.', inputSchema: list.InputSchema, outputSchema: list.OutputSchema, - execute: async ({ path: filePath }) => { - const projectFolder = getProjectFolder(); + execute: async ({ path: filePath }, context) => { + const projectFolder = context.projectPath || ''; const realPath = toRealPath(filePath, projectFolder); // Get the relative path of the parent directory for naoignore matching diff --git a/apps/backend/src/agents/tools/read.ts b/apps/backend/src/agents/tools/read.ts index c4ea1ebe..9b192e99 100644 --- a/apps/backend/src/agents/tools/read.ts +++ b/apps/backend/src/agents/tools/read.ts @@ -1,15 +1,15 @@ import { readFile } from '@nao/shared/tools'; -import { tool } from 'ai'; import fs from 'fs/promises'; -import { getProjectFolder, toRealPath } from '../../utils/tools'; +import { createTool } from '../../types/tools'; +import { toRealPath } from '../../utils/tools'; -export default tool({ +export default createTool({ description: 'Read the contents of a file at the specified path.', inputSchema: readFile.InputSchema, outputSchema: readFile.OutputSchema, - execute: async ({ file_path }) => { - const projectFolder = getProjectFolder(); + execute: async ({ file_path }, context) => { + const projectFolder = context.projectPath || ''; const realPath = toRealPath(file_path, projectFolder); const content = await fs.readFile(realPath, 'utf-8'); diff --git a/apps/backend/src/agents/tools/search.ts b/apps/backend/src/agents/tools/search.ts index e2375c3f..30a87d45 100644 --- a/apps/backend/src/agents/tools/search.ts +++ b/apps/backend/src/agents/tools/search.ts @@ -1,17 +1,17 @@ import { searchFiles } from '@nao/shared/tools'; -import { tool } from 'ai'; import fs from 'fs/promises'; import { glob } from 'glob'; import path from 'path'; -import { getProjectFolder, isWithinProjectFolder, loadNaoignorePatterns, toVirtualPath } from '../../utils/tools'; +import { createTool } from '../../types/tools'; +import { isWithinProjectFolder, loadNaoignorePatterns, toVirtualPath } from '../../utils/tools'; -export default tool({ +export default createTool({ description: 'Search for files matching a glob pattern within the project.', inputSchema: searchFiles.InputSchema, outputSchema: searchFiles.OutputSchema, - execute: async ({ pattern }) => { - const projectFolder = getProjectFolder(); + execute: async ({ pattern }, context) => { + const projectFolder = context.projectPath || ''; // Sanitize pattern to prevent escaping project folder if (path.isAbsolute(pattern)) { diff --git a/apps/backend/src/types/tools.ts b/apps/backend/src/types/tools.ts new file mode 100644 index 00000000..22c881e9 --- /dev/null +++ b/apps/backend/src/types/tools.ts @@ -0,0 +1,29 @@ +import { tool } from 'ai'; +import type { z } from 'zod/v3'; + +type ZodSchema = z.ZodTypeAny; + +export interface ToolContext { + projectPath?: string; +} + +export interface ToolDefinition { + description: string; + inputSchema: TInput; + outputSchema: TOutput; + execute: (input: z.infer, context: ToolContext) => Promise>; +} + +export function createTool( + definition: ToolDefinition, +) { + return tool({ + description: definition.description, + inputSchema: definition.inputSchema, + outputSchema: definition.outputSchema, + execute: async (input, { experimental_context }) => { + const context = (experimental_context as ToolContext) ?? {}; + return definition.execute(input, context); + }, + }); +} diff --git a/apps/backend/src/utils/tools.ts b/apps/backend/src/utils/tools.ts index e8ee0c88..ddcfb489 100644 --- a/apps/backend/src/utils/tools.ts +++ b/apps/backend/src/utils/tools.ts @@ -146,18 +146,6 @@ export const shouldExcludeEntry = (entryName: string, parentPath: string, projec return isIgnoredByNaoignore(relativePath, projectFolder); }; -/** - * Gets the resolved project folder path from the NAO_DEFAULT_PROJECT_PATH environment variable. - * @throws Error if NAO_DEFAULT_PROJECT_PATH is not set - */ -export const getProjectFolder = (): string => { - const projectFolder = env.NAO_DEFAULT_PROJECT_PATH; - if (!projectFolder) { - throw new Error('NAO_DEFAULT_PROJECT_PATH environment variable is not set'); - } - return path.resolve(projectFolder); -}; - /** * Checks if a given path is within the project folder, not in an excluded directory, * and not ignored by .naoignore. From f86ddd61afbb568aa112787ce9bd000574ed7eeb Mon Sep 17 00:00:00 2001 From: MatLBS <135004599+MatLBS@users.noreply.github.com> Date: Thu, 12 Feb 2026 12:22:04 +0100 Subject: [PATCH 5/8] projectPath cannot be null --- apps/backend/src/agents/tools/execute-sql.ts | 4 ++-- apps/backend/src/agents/tools/grep.ts | 2 +- apps/backend/src/agents/tools/list.ts | 2 +- apps/backend/src/agents/tools/read.ts | 2 +- apps/backend/src/agents/tools/search.ts | 2 +- apps/backend/src/routes/test.ts | 7 ++++++- apps/backend/src/types/tools.ts | 4 ++-- apps/backend/src/utils/tools.ts | 2 -- 8 files changed, 14 insertions(+), 11 deletions(-) diff --git a/apps/backend/src/agents/tools/execute-sql.ts b/apps/backend/src/agents/tools/execute-sql.ts index 371959e7..d63e351f 100644 --- a/apps/backend/src/agents/tools/execute-sql.ts +++ b/apps/backend/src/agents/tools/execute-sql.ts @@ -6,9 +6,9 @@ import { createTool, type ToolContext } from '../../types/tools'; export async function executeQuery( { sql_query, database_id }: executeSql.Input, - context?: ToolContext, + context: ToolContext, ): Promise { - const naoProjectFolder = context?.projectPath; + const naoProjectFolder = context.projectPath; const response = await fetch(`${env.FASTAPI_URL}/execute_sql`, { method: 'POST', diff --git a/apps/backend/src/agents/tools/grep.ts b/apps/backend/src/agents/tools/grep.ts index 97c53043..0fc16d0c 100644 --- a/apps/backend/src/agents/tools/grep.ts +++ b/apps/backend/src/agents/tools/grep.ts @@ -54,7 +54,7 @@ export default createTool({ { pattern, path: searchPath, glob, case_insensitive, context_lines, max_results = 100 }, context, ) => { - const projectFolder = context.projectPath || ''; + const projectFolder = context.projectPath; const rgPath = getRipgrepPath(); // Determine the search path diff --git a/apps/backend/src/agents/tools/list.ts b/apps/backend/src/agents/tools/list.ts index 5efc3d4b..f927ae81 100644 --- a/apps/backend/src/agents/tools/list.ts +++ b/apps/backend/src/agents/tools/list.ts @@ -10,7 +10,7 @@ export default createTool({ inputSchema: list.InputSchema, outputSchema: list.OutputSchema, execute: async ({ path: filePath }, context) => { - const projectFolder = context.projectPath || ''; + const projectFolder = context.projectPath; const realPath = toRealPath(filePath, projectFolder); // Get the relative path of the parent directory for naoignore matching diff --git a/apps/backend/src/agents/tools/read.ts b/apps/backend/src/agents/tools/read.ts index 9b192e99..37d5d136 100644 --- a/apps/backend/src/agents/tools/read.ts +++ b/apps/backend/src/agents/tools/read.ts @@ -9,7 +9,7 @@ export default createTool({ inputSchema: readFile.InputSchema, outputSchema: readFile.OutputSchema, execute: async ({ file_path }, context) => { - const projectFolder = context.projectPath || ''; + const projectFolder = context.projectPath; const realPath = toRealPath(file_path, projectFolder); const content = await fs.readFile(realPath, 'utf-8'); diff --git a/apps/backend/src/agents/tools/search.ts b/apps/backend/src/agents/tools/search.ts index 30a87d45..c9198259 100644 --- a/apps/backend/src/agents/tools/search.ts +++ b/apps/backend/src/agents/tools/search.ts @@ -11,7 +11,7 @@ export default createTool({ inputSchema: searchFiles.InputSchema, outputSchema: searchFiles.OutputSchema, execute: async ({ pattern }, context) => { - const projectFolder = context.projectPath || ''; + const projectFolder = context.projectPath; // Sanitize pattern to prevent escaping project folder if (path.isAbsolute(pattern)) { diff --git a/apps/backend/src/routes/test.ts b/apps/backend/src/routes/test.ts index 4f08f291..008a61c1 100644 --- a/apps/backend/src/routes/test.ts +++ b/apps/backend/src/routes/test.ts @@ -6,6 +6,7 @@ import { authMiddleware } from '../middleware/auth'; import { ModelSelection } from '../services/agent.service'; import { TestAgentService, testAgentService } from '../services/test-agent.service'; import { llmProviderSchema } from '../types/llm'; +import { retrieveProjectById } from '../utils/chat'; const modelSelectionSchema = z.object({ provider: llmProviderSchema, @@ -43,10 +44,14 @@ export const testRoutes = async (app: App) => { try { const modelSelection = model as ModelSelection | undefined; const result = await testAgentService.runTest(projectId, prompt, modelSelection); + const project = await retrieveProjectById(projectId); let verification; if (sql) { - const { data: expectedData, columns: expectedColumns } = await executeQuery({ sql_query: sql }); + const { data: expectedData, columns: expectedColumns } = await executeQuery( + { sql_query: sql }, + { projectPath: project.path! }, + ); const { data } = await testAgentService.runVerification( projectId, result, diff --git a/apps/backend/src/types/tools.ts b/apps/backend/src/types/tools.ts index 22c881e9..10c09857 100644 --- a/apps/backend/src/types/tools.ts +++ b/apps/backend/src/types/tools.ts @@ -4,7 +4,7 @@ import type { z } from 'zod/v3'; type ZodSchema = z.ZodTypeAny; export interface ToolContext { - projectPath?: string; + projectPath: string; } export interface ToolDefinition { @@ -22,7 +22,7 @@ export function createTool( inputSchema: definition.inputSchema, outputSchema: definition.outputSchema, execute: async (input, { experimental_context }) => { - const context = (experimental_context as ToolContext) ?? {}; + const context = experimental_context as ToolContext; return definition.execute(input, context); }, }); diff --git a/apps/backend/src/utils/tools.ts b/apps/backend/src/utils/tools.ts index ddcfb489..980e1635 100644 --- a/apps/backend/src/utils/tools.ts +++ b/apps/backend/src/utils/tools.ts @@ -2,8 +2,6 @@ import fs from 'fs'; import { minimatch } from 'minimatch'; import path from 'path'; -import { env } from '../env'; - const MCP_TOOL_SEPARATOR = '__'; /** From 9f88143b3b93013ed1218c207e71ede2278f35c4 Mon Sep 17 00:00:00 2001 From: MatLBS <135004599+MatLBS@users.noreply.github.com> Date: Thu, 12 Feb 2026 14:00:42 +0100 Subject: [PATCH 6/8] reformat execute-python to match the new 'createTool' function --- .../src/agents/tools/execute-python.ts | 23 ++++++++----------- apps/backend/src/agents/tools/grep.ts | 1 + apps/backend/src/agents/tools/list.ts | 2 +- apps/backend/src/agents/tools/read.ts | 2 +- apps/backend/src/agents/tools/search.ts | 2 +- apps/backend/src/types/tools.ts | 3 +++ 6 files changed, 17 insertions(+), 16 deletions(-) diff --git a/apps/backend/src/agents/tools/execute-python.ts b/apps/backend/src/agents/tools/execute-python.ts index 76727e08..cf3dc804 100644 --- a/apps/backend/src/agents/tools/execute-python.ts +++ b/apps/backend/src/agents/tools/execute-python.ts @@ -7,11 +7,11 @@ import { MontySyntaxError, MontyTypingError, } from '@pydantic/monty'; -import { tool } from 'ai'; import fs from 'fs'; import path from 'path'; -import { getProjectFolder, isWithinProjectFolder, toVirtualPath } from '../../utils/tools'; +import { createTool } from '../../types/tools'; +import { isWithinProjectFolder, toVirtualPath } from '../../utils/tools'; const RESOURCE_LIMITS = { maxDurationSecs: 30, @@ -20,9 +20,9 @@ const RESOURCE_LIMITS = { maxRecursionDepth: 500, }; -export async function executePython({ code, inputs }: schemas.Input): Promise { +async function executePython({ code, inputs }: schemas.Input, projectFolder: string): Promise { const inputNames = inputs ? Object.keys(inputs) : []; - const virtualFS = createVirtualFS(); + const virtualFS = createVirtualFS(projectFolder); let monty: Monty; try { @@ -117,15 +117,10 @@ function findAllFiles(dir: string, projectFolder: string): schemas.VirtualFile[] return files; } -function loadProjectFiles(): schemas.VirtualFile[] { - const projectFolder = getProjectFolder(); - return findAllFiles(projectFolder, projectFolder); -} - -function createVirtualFS(): Map { +function createVirtualFS(projectFolder: string): Map { const vfs = new Map(); - const projectFiles = loadProjectFiles(); + const projectFiles = findAllFiles(projectFolder, projectFolder); for (const file of projectFiles) { vfs.set(file.path, file.content); } @@ -136,9 +131,11 @@ function createVirtualFS(): Map { const EXTERNAL_FUNCTION_MAP = new Map(schemas.EXTERNAL_FUNCTIONS.map((f) => [f.name, f])); const EXTERNAL_FUNCTION_NAMES = schemas.EXTERNAL_FUNCTIONS.map((f) => f.name); -export default tool({ +export default createTool({ description: schemas.description, inputSchema: schemas.inputSchema, outputSchema: schemas.outputSchema, - execute: executePython, + execute: async (input, context) => { + return executePython(input, context.projectPath); + }, }); diff --git a/apps/backend/src/agents/tools/grep.ts b/apps/backend/src/agents/tools/grep.ts index b4ce7f1f..a7cfe84a 100644 --- a/apps/backend/src/agents/tools/grep.ts +++ b/apps/backend/src/agents/tools/grep.ts @@ -3,6 +3,7 @@ import { spawn } from 'child_process'; import fs from 'fs'; import path from 'path'; +import { GrepOutput, renderToModelOutput } from '../../components/tool-outputs'; import { createTool } from '../../types/tools'; import { isWithinProjectFolder, loadNaoignorePatterns, toRealPath, toVirtualPath } from '../../utils/tools'; diff --git a/apps/backend/src/agents/tools/list.ts b/apps/backend/src/agents/tools/list.ts index ea105520..450323dc 100644 --- a/apps/backend/src/agents/tools/list.ts +++ b/apps/backend/src/agents/tools/list.ts @@ -57,7 +57,7 @@ export default createTool({ }), ); - return { _version: '1', entries }; + return { _version: '1' as const, entries }; }, toModelOutput: ({ output }) => renderToModelOutput(ListOutput({ output }), output), diff --git a/apps/backend/src/agents/tools/read.ts b/apps/backend/src/agents/tools/read.ts index 3e596d10..ad2cc15b 100644 --- a/apps/backend/src/agents/tools/read.ts +++ b/apps/backend/src/agents/tools/read.ts @@ -17,7 +17,7 @@ export default createTool({ const numberOfTotalLines = content.split('\n').length; return { - _version: '1', + _version: '1' as const, content, numberOfTotalLines, }; diff --git a/apps/backend/src/agents/tools/search.ts b/apps/backend/src/agents/tools/search.ts index 408c8550..56a69fdc 100644 --- a/apps/backend/src/agents/tools/search.ts +++ b/apps/backend/src/agents/tools/search.ts @@ -54,7 +54,7 @@ export default createTool({ }), ); - return { _version: '1', files }; + return { _version: '1' as const, files }; }, toModelOutput: ({ output }) => renderToModelOutput(SearchOutput({ output }), output), diff --git a/apps/backend/src/types/tools.ts b/apps/backend/src/types/tools.ts index 10c09857..30a58481 100644 --- a/apps/backend/src/types/tools.ts +++ b/apps/backend/src/types/tools.ts @@ -1,3 +1,4 @@ +import type { ToolResultOutput } from '@ai-sdk/provider-utils'; import { tool } from 'ai'; import type { z } from 'zod/v3'; @@ -12,6 +13,7 @@ export interface ToolDefinition, context: ToolContext) => Promise>; + toModelOutput?: (params: { output: z.infer }) => ToolResultOutput; } export function createTool( @@ -25,5 +27,6 @@ export function createTool( const context = experimental_context as ToolContext; return definition.execute(input, context); }, + ...(definition.toModelOutput && { toModelOutput: definition.toModelOutput }), }); } From e136f340af75aa50a447e3880974d427dcaf998f Mon Sep 17 00:00:00 2001 From: MatLBS <135004599+MatLBS@users.noreply.github.com> Date: Thu, 12 Feb 2026 16:06:35 +0100 Subject: [PATCH 7/8] rename projectPath to projectFolder --- apps/backend/src/agents/tools/execute-python.ts | 2 +- apps/backend/src/agents/tools/execute-sql.ts | 2 +- apps/backend/src/agents/tools/grep.ts | 2 +- apps/backend/src/agents/tools/list.ts | 2 +- apps/backend/src/agents/tools/read.ts | 2 +- apps/backend/src/agents/tools/search.ts | 2 +- apps/backend/src/routes/test.ts | 2 +- apps/backend/src/services/agent.service.ts | 2 +- apps/backend/src/types/tools.ts | 2 +- 9 files changed, 9 insertions(+), 9 deletions(-) diff --git a/apps/backend/src/agents/tools/execute-python.ts b/apps/backend/src/agents/tools/execute-python.ts index cf3dc804..9045967b 100644 --- a/apps/backend/src/agents/tools/execute-python.ts +++ b/apps/backend/src/agents/tools/execute-python.ts @@ -136,6 +136,6 @@ export default createTool({ inputSchema: schemas.inputSchema, outputSchema: schemas.outputSchema, execute: async (input, context) => { - return executePython(input, context.projectPath); + return executePython(input, context.projectFolder); }, }); diff --git a/apps/backend/src/agents/tools/execute-sql.ts b/apps/backend/src/agents/tools/execute-sql.ts index 0cee73c6..ed6d120b 100644 --- a/apps/backend/src/agents/tools/execute-sql.ts +++ b/apps/backend/src/agents/tools/execute-sql.ts @@ -9,7 +9,7 @@ export async function executeQuery( { sql_query, database_id }: executeSql.Input, context: ToolContext, ): Promise { - const naoProjectFolder = context.projectPath; + const naoProjectFolder = context.projectFolder; const response = await fetch(`http://localhost:${env.FASTAPI_PORT}/execute_sql`, { method: 'POST', diff --git a/apps/backend/src/agents/tools/grep.ts b/apps/backend/src/agents/tools/grep.ts index a7cfe84a..937f1ad1 100644 --- a/apps/backend/src/agents/tools/grep.ts +++ b/apps/backend/src/agents/tools/grep.ts @@ -55,7 +55,7 @@ export default createTool({ { pattern, path: searchPath, glob, case_insensitive, context_lines, max_results = 100 }, context, ) => { - const projectFolder = context.projectPath; + const projectFolder = context.projectFolder; const rgPath = getRipgrepPath(); // Determine the search path diff --git a/apps/backend/src/agents/tools/list.ts b/apps/backend/src/agents/tools/list.ts index 450323dc..216b383a 100644 --- a/apps/backend/src/agents/tools/list.ts +++ b/apps/backend/src/agents/tools/list.ts @@ -11,7 +11,7 @@ export default createTool({ inputSchema: list.InputSchema, outputSchema: list.OutputSchema, execute: async ({ path: filePath }, context) => { - const projectFolder = context.projectPath; + const projectFolder = context.projectFolder; const realPath = toRealPath(filePath, projectFolder); // Get the relative path of the parent directory for naoignore matching diff --git a/apps/backend/src/agents/tools/read.ts b/apps/backend/src/agents/tools/read.ts index ad2cc15b..d78b7e3d 100644 --- a/apps/backend/src/agents/tools/read.ts +++ b/apps/backend/src/agents/tools/read.ts @@ -10,7 +10,7 @@ export default createTool({ inputSchema: readFile.InputSchema, outputSchema: readFile.OutputSchema, execute: async ({ file_path }, context) => { - const projectFolder = context.projectPath; + const projectFolder = context.projectFolder; const realPath = toRealPath(file_path, projectFolder); const content = await fs.readFile(realPath, 'utf-8'); diff --git a/apps/backend/src/agents/tools/search.ts b/apps/backend/src/agents/tools/search.ts index 56a69fdc..5dc3242d 100644 --- a/apps/backend/src/agents/tools/search.ts +++ b/apps/backend/src/agents/tools/search.ts @@ -12,7 +12,7 @@ export default createTool({ inputSchema: searchFiles.InputSchema, outputSchema: searchFiles.OutputSchema, execute: async ({ pattern }, context) => { - const projectFolder = context.projectPath; + const projectFolder = context.projectFolder; // Sanitize pattern to prevent escaping project folder if (path.isAbsolute(pattern)) { diff --git a/apps/backend/src/routes/test.ts b/apps/backend/src/routes/test.ts index 008a61c1..f7a4f1f4 100644 --- a/apps/backend/src/routes/test.ts +++ b/apps/backend/src/routes/test.ts @@ -50,7 +50,7 @@ export const testRoutes = async (app: App) => { if (sql) { const { data: expectedData, columns: expectedColumns } = await executeQuery( { sql_query: sql }, - { projectPath: project.path! }, + { projectFolder: project.path! }, ); const { data } = await testAgentService.runVerification( projectId, diff --git a/apps/backend/src/services/agent.service.ts b/apps/backend/src/services/agent.service.ts index cbf71837..a724eba8 100644 --- a/apps/backend/src/services/agent.service.ts +++ b/apps/backend/src/services/agent.service.ts @@ -190,7 +190,7 @@ class AgentManager { abortSignal: this._abortController.signal, // @ts-expect-error - experimental_context is not yet in the types experimental_context: { - projectPath: project.path, + projectFolder: project.path, }, }); diff --git a/apps/backend/src/types/tools.ts b/apps/backend/src/types/tools.ts index 30a58481..bdec600e 100644 --- a/apps/backend/src/types/tools.ts +++ b/apps/backend/src/types/tools.ts @@ -5,7 +5,7 @@ import type { z } from 'zod/v3'; type ZodSchema = z.ZodTypeAny; export interface ToolContext { - projectPath: string; + projectFolder: string; } export interface ToolDefinition { From d5a5c836ca80df03d5540144a1c0b5e01e34bb37 Mon Sep 17 00:00:00 2001 From: MatLBS <135004599+MatLBS@users.noreply.github.com> Date: Thu, 12 Feb 2026 16:18:45 +0100 Subject: [PATCH 8/8] executePython takes explicitely a 'projectFolder' param --- apps/backend/src/agents/tools/execute-python.ts | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/apps/backend/src/agents/tools/execute-python.ts b/apps/backend/src/agents/tools/execute-python.ts index 255370eb..3cefff63 100644 --- a/apps/backend/src/agents/tools/execute-python.ts +++ b/apps/backend/src/agents/tools/execute-python.ts @@ -21,7 +21,10 @@ const RESOURCE_LIMITS = { maxRecursionDepth: 500, }; -async function executePython({ code, inputs }: schemas.Input, projectFolder: string): Promise { +async function executePython( + { code, inputs }: schemas.Input, + { projectFolder }: { projectFolder: string }, +): Promise { if (!montyModule) { throw new Error('Python execution is not available on this platform'); } @@ -145,7 +148,7 @@ export default montyModule inputSchema: schemas.inputSchema, outputSchema: schemas.outputSchema, execute: async (input, context) => { - return executePython(input, context.projectFolder); + return executePython(input, { projectFolder: context.projectFolder }); }, }) : null;