feat(ai): add code interpreter for AI data analysis (#16559)

## Summary

- Add code interpreter tool that enables AI to execute Python code for
data analysis, CSV processing, and chart generation
- Support for both local (development) and E2B (sandboxed production)
execution drivers
- Real-time streaming of stdout/stderr and generated files
- Frontend components for displaying code execution results with
expandable sections

## Code Quality Improvements

- Extract `getMimeType` to shared utility to reduce code duplication
between drivers
- Fix security issue: escape single quotes/backslashes in E2B driver env
variable injection
- Add `buildExecutionState` helper to reduce duplicated state object
construction
- Add `DEFAULT_CODE_INTERPRETER_TIMEOUT_MS` constant for consistency
- Fix lingui linting warning and TypeScript theme errors in frontend

## Test Plan

- [ ] Test code interpreter with local driver in development
- [ ] Test code interpreter with E2B driver in production environment
- [ ] Verify streaming output displays correctly in chat UI
- [ ] Verify generated files (charts, CSVs) are uploaded and
downloadable
- [ ] Test file upload flow (CSV, Excel) triggers code interpreter

<!-- CURSOR_SUMMARY -->
---

> [!NOTE]
> Updates generated i18n catalogs for Polish and pseudo-English, adding
strings for code execution/output (code interpreter) and various UI
messages, with minor text adjustments.
> 
> - **Localization**:
> - **Generated catalogs**: Refresh `locales/generated/pl-PL.ts` and
`locales/generated/pseudo-en.ts`.
> - Add strings for code execution/output (e.g., code, copy code/output,
running/waiting states, download files, generated files, Python code
execution).
> - Include new UI texts (errors, prompts, menus) and minor text
corrections.
>   - No changes to `pt-BR`; other files unchanged functionally.
> 
> <sup>Written by [Cursor
Bugbot](https://cursor.com/dashboard?tab=bugbot) for commit
befc13d02c21e5a6647bc1aa6daa2a89f60b7ef8. This will update automatically
on new commits. Configure
[here](https://cursor.com/dashboard?tab=bugbot).</sup>
<!-- /CURSOR_SUMMARY -->
This commit is contained in:
Félix Malfait
2025-12-15 16:11:24 +01:00
committed by GitHub
parent 4281a71f40
commit 2e104c8e76
151 changed files with 10361 additions and 449 deletions
@@ -2,4 +2,5 @@ export enum ToolType {
HTTP_REQUEST = 'HTTP_REQUEST',
SEND_EMAIL = 'SEND_EMAIL',
SEARCH_HELP_CENTER = 'SEARCH_HELP_CENTER',
CODE_INTERPRETER = 'CODE_INTERPRETER',
}
@@ -1,61 +0,0 @@
import { Injectable } from '@nestjs/common';
import { PermissionFlagType } from 'twenty-shared/constants';
import { ToolType } from 'src/engine/core-modules/tool/enums/tool-type.enum';
import { HttpTool } from 'src/engine/core-modules/tool/tools/http-tool/http-tool';
import { type SendEmailTool } from 'src/engine/core-modules/tool/tools/send-email-tool/send-email-tool';
import { type SendEmailInput } from 'src/engine/core-modules/tool/tools/send-email-tool/types/send-email-input.type';
import { type Tool } from 'src/engine/core-modules/tool/types/tool.type';
import { type TwentyConfigService } from 'src/engine/core-modules/twenty-config/twenty-config.service';
@Injectable()
export class ToolRegistryService {
private readonly toolFactories: Map<ToolType, () => Tool>;
constructor(
private readonly sendEmailTool: SendEmailTool,
private readonly twentyConfigService: TwentyConfigService,
) {
this.toolFactories = new Map<ToolType, () => Tool>([
[
ToolType.HTTP_REQUEST,
() => {
const httpTool = new HttpTool(twentyConfigService);
return {
description: httpTool.description,
inputSchema: httpTool.inputSchema,
execute: (params, workspaceId) =>
httpTool.execute(params, workspaceId),
flag: PermissionFlagType.HTTP_REQUEST_TOOL,
};
},
],
[
ToolType.SEND_EMAIL,
() => ({
description: this.sendEmailTool.description,
inputSchema: this.sendEmailTool.inputSchema,
execute: (params, workspaceId) =>
this.sendEmailTool.execute(params as SendEmailInput, workspaceId),
flag: PermissionFlagType.SEND_EMAIL_TOOL,
}),
],
]);
}
getTool(toolType: ToolType): Tool {
const factory = this.toolFactories.get(toolType);
if (!factory) {
throw new Error(`Unknown tool type: ${toolType}`);
}
return factory();
}
getAllToolTypes(): ToolType[] {
return Array.from(this.toolFactories.keys());
}
}
@@ -1,8 +1,11 @@
import { HttpModule } from '@nestjs/axios';
import { Module } from '@nestjs/common';
import { TypeOrmModule } from '@nestjs/typeorm';
import { FileEntity } from 'src/engine/core-modules/file/entities/file.entity';
import { FileModule } from 'src/engine/core-modules/file/file.module';
import { JwtModule } from 'src/engine/core-modules/jwt/jwt.module';
import { CodeInterpreterTool } from 'src/engine/core-modules/tool/tools/code-interpreter-tool/code-interpreter-tool';
import { HttpTool } from 'src/engine/core-modules/tool/tools/http-tool/http-tool';
import { SearchHelpCenterTool } from 'src/engine/core-modules/tool/tools/search-help-center-tool/search-help-center-tool';
import { SendEmailTool } from 'src/engine/core-modules/tool/tools/send-email-tool/send-email-tool';
@@ -13,8 +16,15 @@ import { MessagingImportManagerModule } from 'src/modules/messaging/message-impo
MessagingImportManagerModule,
TypeOrmModule.forFeature([FileEntity]),
FileModule,
HttpModule,
JwtModule,
],
providers: [HttpTool, SendEmailTool, SearchHelpCenterTool],
exports: [HttpTool, SendEmailTool, SearchHelpCenterTool],
providers: [
HttpTool,
SendEmailTool,
SearchHelpCenterTool,
CodeInterpreterTool,
],
exports: [HttpTool, SendEmailTool, SearchHelpCenterTool, CodeInterpreterTool],
})
export class ToolModule {}
@@ -0,0 +1,25 @@
import { z } from 'zod';
export const CodeInterpreterInputZodSchema = z.object({
code: z.string().describe('Python code to execute'),
files: z
.array(
z.object({
filename: z.string().describe('Name of the file'),
url: z
.string()
.describe('URL of the file to include (from user attachments)'),
}),
)
.optional()
.describe('Files to make available in the execution environment'),
});
export const CodeInterpreterToolParametersZodSchema = z.object({
loadingMessage: z
.string()
.describe(
"A clear, human-readable status message describing the code being executed. This will be shown to the user while the tool is running, so phrase it as a present-tense status update (e.g., 'Creating a bar chart from sales data'). Explain what analysis or visualization you are performing in natural language.",
),
input: CodeInterpreterInputZodSchema,
});
@@ -0,0 +1,435 @@
import { HttpService } from '@nestjs/axios';
import { Injectable, Logger } from '@nestjs/common';
import path from 'path';
import {
type CodeExecutionData,
type CodeExecutionFile,
type CodeExecutionState,
} from 'twenty-shared/ai';
import { v4 } from 'uuid';
import {
type InputFile,
type OutputFile,
} from 'src/engine/core-modules/code-interpreter/drivers/interfaces/code-interpreter-driver.interface';
import { FileFolder } from 'src/engine/core-modules/file/interfaces/file-folder.interface';
import {
type AccessTokenJwtPayload,
JwtTokenTypeEnum,
} from 'src/engine/core-modules/auth/types/auth-context.type';
import { CodeInterpreterService } from 'src/engine/core-modules/code-interpreter/code-interpreter.service';
import { FileStorageService } from 'src/engine/core-modules/file-storage/file-storage.service';
import { FileService } from 'src/engine/core-modules/file/services/file.service';
import { JwtWrapperService } from 'src/engine/core-modules/jwt/services/jwt-wrapper.service';
import { CodeInterpreterToolParametersZodSchema } from 'src/engine/core-modules/tool/tools/code-interpreter-tool/code-interpreter-tool.schema';
import { TWENTY_MCP_HELPER } from 'src/engine/core-modules/tool/tools/code-interpreter-tool/twenty-mcp-helper.const';
import { type CodeInterpreterInput } from 'src/engine/core-modules/tool/tools/code-interpreter-tool/types/code-interpreter-input.type';
import { type ToolInput } from 'src/engine/core-modules/tool/types/tool-input.type';
import { type ToolOutput } from 'src/engine/core-modules/tool/types/tool-output.type';
import {
type Tool,
type ToolExecutionContext,
} from 'src/engine/core-modules/tool/types/tool.type';
import { getSecureAdapter } from 'src/engine/core-modules/tool/utils/get-secure-axios-adapter.util';
import { TwentyConfigService } from 'src/engine/core-modules/twenty-config/twenty-config.service';
import { AuthProviderEnum } from 'src/engine/core-modules/workspace/types/workspace.type';
@Injectable()
export class CodeInterpreterTool implements Tool {
private readonly logger = new Logger(CodeInterpreterTool.name);
description =
'Execute Python code in a sandboxed environment for data analysis, CSV processing, calculations, and chart generation. Returns stdout, stderr, and generated files. Input files are available at /home/user/{filename}. Save output files (charts, reports) to /home/user/output/ using plt.savefig() for matplotlib charts.';
inputSchema = CodeInterpreterToolParametersZodSchema;
constructor(
private readonly codeInterpreterService: CodeInterpreterService,
private readonly fileStorageService: FileStorageService,
private readonly fileService: FileService,
private readonly httpService: HttpService,
private readonly twentyConfigService: TwentyConfigService,
private readonly jwtWrapperService: JwtWrapperService,
) {}
private buildExecutionState(
executionId: string,
state: CodeExecutionState,
code: string,
stdout: string,
stderr: string,
files: CodeExecutionFile[],
extras?: { exitCode?: number; executionTimeMs?: number; error?: string },
): CodeExecutionData {
return {
executionId,
state,
code,
language: 'python',
stdout,
stderr,
files,
...extras,
};
}
async execute(
parameters: ToolInput,
context: ToolExecutionContext,
): Promise<ToolOutput> {
const { workspaceId, userId, userWorkspaceId, onCodeExecutionUpdate } =
context;
const { code, files } = parameters as CodeInterpreterInput;
const executionId = v4();
const startTime = Date.now();
let accumulatedStdout = '';
let accumulatedStderr = '';
const streamedFiles: CodeExecutionFile[] = [];
onCodeExecutionUpdate?.(
this.buildExecutionState(executionId, 'pending', code, '', '', []),
);
try {
const inputFiles = await this.downloadInputFiles(files);
this.logger.log(
`Executing code interpreter with ${inputFiles.length} input files`,
);
onCodeExecutionUpdate?.(
this.buildExecutionState(executionId, 'running', code, '', '', []),
);
const serverUrl = this.twentyConfigService.get('SERVER_URL');
const sessionToken = this.generateSessionToken(
workspaceId,
userId,
userWorkspaceId,
);
this.logger.debug(
`MCP session: workspaceId=${workspaceId}, userId=${userId}, userWorkspaceId=${userWorkspaceId}, serverUrl=${serverUrl}`,
);
const codeWithHelper = TWENTY_MCP_HELPER + '\n\n' + code;
const result = await this.codeInterpreterService.execute(
codeWithHelper,
inputFiles,
{
env: {
TWENTY_SERVER_URL: serverUrl,
TWENTY_API_TOKEN: sessionToken,
},
},
{
onStdout: (line) => {
accumulatedStdout += line + '\n';
onCodeExecutionUpdate?.(
this.buildExecutionState(
executionId,
'running',
code,
accumulatedStdout,
accumulatedStderr,
streamedFiles,
),
);
},
onStderr: (line) => {
accumulatedStderr += line + '\n';
onCodeExecutionUpdate?.(
this.buildExecutionState(
executionId,
'running',
code,
accumulatedStdout,
accumulatedStderr,
streamedFiles,
),
);
},
onResult: async (outputFile: OutputFile) => {
const uploadedFile = await this.uploadSingleFile(
outputFile,
workspaceId,
executionId,
);
if (uploadedFile) {
streamedFiles.push(uploadedFile);
onCodeExecutionUpdate?.(
this.buildExecutionState(
executionId,
'running',
code,
accumulatedStdout,
accumulatedStderr,
streamedFiles,
),
);
}
},
},
);
this.logger.debug(
`Execution result: exitCode=${result.exitCode}, stdout length=${result.stdout.length}, stderr length=${result.stderr.length}`,
);
const allOutputFileUrls = await this.uploadOutputFiles(
result.files,
workspaceId,
executionId,
streamedFiles,
);
const executionTimeMs = Date.now() - startTime;
const finalState = result.exitCode === 0 ? 'completed' : 'error';
onCodeExecutionUpdate?.(
this.buildExecutionState(
executionId,
finalState,
code,
result.stdout || accumulatedStdout,
result.stderr || accumulatedStderr,
allOutputFileUrls,
{
exitCode: result.exitCode,
executionTimeMs,
error: result.error,
},
),
);
return {
success: result.exitCode === 0,
message:
result.exitCode === 0
? 'Code executed successfully'
: 'Code execution failed',
result: {
stdout: result.stdout,
stderr: result.stderr,
exitCode: result.exitCode,
files: allOutputFileUrls,
},
error: result.error,
};
} catch (error) {
this.logger.error('Code interpreter execution failed', error);
const executionTimeMs = Date.now() - startTime;
const errorMessage =
error instanceof Error ? error.message : 'Unknown error';
onCodeExecutionUpdate?.(
this.buildExecutionState(
executionId,
'error',
code,
accumulatedStdout,
accumulatedStderr,
streamedFiles,
{ executionTimeMs, error: errorMessage },
),
);
return {
success: false,
message: 'Code interpreter execution failed',
error: errorMessage,
};
}
}
private async downloadInputFiles(
files?: { filename: string; url: string }[],
): Promise<InputFile[]> {
if (!files || files.length === 0) {
return [];
}
const inputFiles: InputFile[] = [];
const serverUrl = this.twentyConfigService.get('SERVER_URL');
for (const file of files) {
try {
if (file.url.startsWith('data:')) {
const parsed = this.parseDataUrl(file.url);
if (parsed) {
inputFiles.push({
filename: file.filename,
content: parsed.content,
mimeType: parsed.mimeType,
});
}
continue;
}
// Allow requests to the server's own URL (for internal file downloads)
// but block all other private/internal IPs to prevent SSRF attacks
const isInternalFileUrl = file.url.startsWith(serverUrl);
const adapter = isInternalFileUrl ? undefined : getSecureAdapter();
const response = await this.httpService.axiosRef.get(file.url, {
responseType: 'arraybuffer',
timeout: 30_000,
adapter,
});
inputFiles.push({
filename: file.filename,
content: Buffer.from(response.data),
mimeType:
response.headers['content-type'] ?? 'application/octet-stream',
});
} catch (error) {
this.logger.warn(`Failed to download file ${file.filename}`, error);
}
}
return inputFiles;
}
private parseDataUrl(
dataUrl: string,
): { content: Buffer; mimeType: string } | null {
// Format: data:{mimeType};base64,{base64data}
const match = dataUrl.match(/^data:([^;]+);base64,(.+)$/);
if (!match) {
return null;
}
const [, mimeType, base64Data] = match;
return {
content: Buffer.from(base64Data, 'base64'),
mimeType,
};
}
private generateSessionToken(
workspaceId: string,
userId?: string,
userWorkspaceId?: string,
): string {
const secret = this.jwtWrapperService.generateAppSecret(
JwtTokenTypeEnum.ACCESS,
workspaceId,
);
const payload: AccessTokenJwtPayload = {
sub: userId ?? workspaceId,
type: JwtTokenTypeEnum.ACCESS,
workspaceId,
userId: userId ?? workspaceId,
userWorkspaceId: userWorkspaceId ?? workspaceId,
authProvider: AuthProviderEnum.Password,
};
return this.jwtWrapperService.sign(payload, {
secret,
expiresIn: '5m', // Short-lived token for code execution session
});
}
private async uploadSingleFile(
file: OutputFile,
workspaceId: string,
executionId: string,
): Promise<CodeExecutionFile | null> {
const subFolder = `${FileFolder.AgentChat}/code-interpreter/${executionId}`;
const folder = `workspace-${workspaceId}/${subFolder}`;
const sanitizedFilename = path.basename(file.filename);
try {
await this.fileStorageService.write({
file: file.content,
name: sanitizedFilename,
mimeType: file.mimeType,
folder,
});
const filePath = `${subFolder}/${sanitizedFilename}`;
const signedPath = this.fileService.signFileUrl({
url: filePath,
workspaceId,
});
const serverUrl = this.twentyConfigService.get('SERVER_URL');
return {
filename: sanitizedFilename,
url: `${serverUrl}/files/${signedPath}`,
mimeType: file.mimeType,
};
} catch (error) {
this.logger.warn(`Failed to upload output file ${file.filename}`, error);
return null;
}
}
private async uploadOutputFiles(
files: OutputFile[],
workspaceId: string,
executionId: string,
alreadyUploadedFiles: CodeExecutionFile[],
): Promise<CodeExecutionFile[]> {
const subFolder = `${FileFolder.AgentChat}/code-interpreter/${executionId}`;
const folder = `workspace-${workspaceId}/${subFolder}`;
const outputFileUrls: CodeExecutionFile[] = [...alreadyUploadedFiles];
const uploadedFilenames = new Set(
alreadyUploadedFiles.map((f) => f.filename),
);
for (const file of files) {
const sanitizedFilename = path.basename(file.filename);
if (uploadedFilenames.has(sanitizedFilename)) {
continue;
}
try {
await this.fileStorageService.write({
file: file.content,
name: sanitizedFilename,
mimeType: file.mimeType,
folder,
});
const filePath = `${subFolder}/${sanitizedFilename}`;
const signedPath = this.fileService.signFileUrl({
url: filePath,
workspaceId,
});
const serverUrl = this.twentyConfigService.get('SERVER_URL');
outputFileUrls.push({
filename: sanitizedFilename,
url: `${serverUrl}/files/${signedPath}`,
mimeType: file.mimeType,
});
} catch (error) {
this.logger.warn(
`Failed to upload output file ${file.filename}`,
error,
);
}
}
return outputFileUrls;
}
}
@@ -0,0 +1,91 @@
// Python helper that gets prepended to user code for MCP access
export const TWENTY_MCP_HELPER = `# Auto-injected Twenty MCP helper - provides access to Twenty tools
import os
import json
try:
import requests
_REQUESTS_AVAILABLE = True
except ImportError:
_REQUESTS_AVAILABLE = False
class TwentyMCP:
"""Helper class to call Twenty tools via MCP protocol"""
def __init__(self):
self.url = os.environ.get('TWENTY_SERVER_URL', '')
self.token = os.environ.get('TWENTY_API_TOKEN', '')
self._available = _REQUESTS_AVAILABLE and bool(self.url) and bool(self.token)
@property
def available(self) -> bool:
"""Check if MCP bridge is available"""
return self._available
def call_tool(self, name: str, arguments: dict = None):
"""
Call a Twenty tool via MCP protocol.
Args:
name: Tool name (e.g., 'find_person_records', 'create_company_record')
arguments: Tool arguments as a dictionary
Returns:
Tool result as parsed JSON
Example:
people = twenty.call_tool('find_person_records', {'limit': 10})
"""
if not self._available:
raise RuntimeError('Twenty MCP bridge not available. Missing requests library or credentials.')
response = requests.post(
f"{self.url}/mcp",
headers={"Authorization": f"Bearer {self.token}"},
json={
"jsonrpc": "2.0",
"id": 1,
"method": "tools/call",
"params": {"name": name, "arguments": arguments or {}}
},
timeout=30
)
response.raise_for_status()
result = response.json()
if "error" in result:
raise Exception(f"MCP Error: {result['error'].get('message', 'Unknown error')}")
content = result.get("result", {}).get("content", [])
if content and content[0].get("type") == "text":
return json.loads(content[0]["text"])
return result.get("result")
def list_tools(self):
"""
List all available Twenty tools.
Returns:
List of tool definitions with name, description, and inputSchema
"""
if not self._available:
raise RuntimeError('Twenty MCP bridge not available.')
response = requests.post(
f"{self.url}/mcp",
headers={"Authorization": f"Bearer {self.token}"},
json={
"jsonrpc": "2.0",
"id": 1,
"method": "tools/list",
"params": {}
},
timeout=30
)
response.raise_for_status()
result = response.json()
return result.get("result", {}).get("tools", [])
# Pre-instantiated helper - use 'twenty' in your code
twenty = TwentyMCP()
`;
@@ -0,0 +1,9 @@
export type CodeInterpreterFileInput = {
filename: string;
url: string;
};
export type CodeInterpreterInput = {
code: string;
files?: CodeInterpreterFileInput[];
};
@@ -8,7 +8,10 @@ import { HttpToolParametersZodSchema } from 'src/engine/core-modules/tool/tools/
import { type HttpRequestInput } from 'src/engine/core-modules/tool/tools/http-tool/types/http-request-input.type';
import { type ToolInput } from 'src/engine/core-modules/tool/types/tool-input.type';
import { type ToolOutput } from 'src/engine/core-modules/tool/types/tool-output.type';
import { type Tool } from 'src/engine/core-modules/tool/types/tool.type';
import {
type Tool,
type ToolExecutionContext,
} from 'src/engine/core-modules/tool/types/tool.type';
import { getSecureAdapter } from 'src/engine/core-modules/tool/utils/get-secure-axios-adapter.util';
import { TwentyConfigService } from 'src/engine/core-modules/twenty-config/twenty-config.service';
@@ -22,7 +25,7 @@ export class HttpTool implements Tool {
async execute(
parameters: ToolInput,
_workspaceId: string,
_context: ToolExecutionContext,
): Promise<ToolOutput> {
const { url, method, headers, body } = parameters as HttpRequestInput;
const headersCopy = { ...headers };
@@ -5,7 +5,10 @@ import axios from 'axios';
import { SearchHelpCenterToolParametersZodSchema } from 'src/engine/core-modules/tool/tools/search-help-center-tool/search-help-center-tool.schema';
import { type ToolInput } from 'src/engine/core-modules/tool/types/tool-input.type';
import { type ToolOutput } from 'src/engine/core-modules/tool/types/tool-output.type';
import { type Tool } from 'src/engine/core-modules/tool/types/tool.type';
import {
type Tool,
type ToolExecutionContext,
} from 'src/engine/core-modules/tool/types/tool.type';
import { TwentyConfigService } from 'src/engine/core-modules/twenty-config/twenty-config.service';
@Injectable()
@@ -16,7 +19,10 @@ export class SearchHelpCenterTool implements Tool {
constructor(private readonly twentyConfigService: TwentyConfigService) {}
async execute(parameters: ToolInput): Promise<ToolOutput> {
async execute(
parameters: ToolInput,
_context: ToolExecutionContext,
): Promise<ToolOutput> {
const { query } = parameters;
try {
@@ -18,7 +18,10 @@ import {
import { SendEmailToolParametersZodSchema } from 'src/engine/core-modules/tool/tools/send-email-tool/send-email-tool.schema';
import { type SendEmailInput } from 'src/engine/core-modules/tool/tools/send-email-tool/types/send-email-input.type';
import { type ToolOutput } from 'src/engine/core-modules/tool/types/tool-output.type';
import { type Tool } from 'src/engine/core-modules/tool/types/tool.type';
import {
type Tool,
type ToolExecutionContext,
} from 'src/engine/core-modules/tool/types/tool.type';
import { GlobalWorkspaceOrmManager } from 'src/engine/twenty-orm/global-workspace-datasource/global-workspace-orm.manager';
import { buildSystemAuthContext } from 'src/engine/twenty-orm/utils/build-system-auth-context.util';
import { type ConnectedAccountWorkspaceEntity } from 'src/modules/connected-account/standard-objects/connected-account.workspace-entity';
@@ -175,8 +178,9 @@ export class SendEmailTool implements Tool {
async execute(
parameters: SendEmailInput,
workspaceId: string,
context: ToolExecutionContext,
): Promise<ToolOutput> {
const { workspaceId } = context;
const { email, subject, body, files } = parameters;
let { connectedAccountId } = parameters;
@@ -1,12 +1,21 @@
import { type FlexibleSchema } from '@ai-sdk/provider-utils';
import { type PermissionFlagType } from 'twenty-shared/constants';
import { type CodeExecutionStreamEmitter } from 'src/engine/core-modules/tool-provider/interfaces/tool-provider.interface';
import { type ToolInput } from 'src/engine/core-modules/tool/types/tool-input.type';
import { type ToolOutput } from 'src/engine/core-modules/tool/types/tool-output.type';
export type ToolExecutionContext = {
workspaceId: string;
userId?: string;
userWorkspaceId?: string;
onCodeExecutionUpdate?: CodeExecutionStreamEmitter;
};
export type Tool = {
description: string;
inputSchema: FlexibleSchema<unknown>;
execute(input: ToolInput, workspaceId: string): Promise<ToolOutput>;
execute(input: ToolInput, context: ToolExecutionContext): Promise<ToolOutput>;
flag?: PermissionFlagType;
};