From 34773f2614450d0e82b190c04bb446dec74f84dc Mon Sep 17 00:00:00 2001 From: centdix <40307056+centdix@users.noreply.github.com> Date: Wed, 13 Aug 2025 21:58:28 +0200 Subject: [PATCH] feat(aichat): add test tool to script and flow mode (#6367) * add test script tool * modify system prompt * cleaning * same for flows * cleaning * apply code when confirm test + fix circular dep * cleaning * factorize * display error * cleaning * fix * update comment * prompts * cleaner code * show logs in separate container * format --- frontend/src/lib/components/Editor.svelte | 4 +- .../src/lib/components/ScriptEditor.svelte | 4 +- .../copilot/chat/AIChatManager.svelte.ts | 26 +- .../copilot/chat/ToolContentDisplay.svelte | 94 ++++++++ .../copilot/chat/ToolExecutionDisplay.svelte | 223 ++++++------------ .../lib/components/copilot/chat/flow/core.ts | 50 +++- .../components/copilot/chat/monaco-adapter.ts | 21 +- .../components/copilot/chat/script/core.ts | 82 ++++++- .../src/lib/components/copilot/chat/shared.ts | 204 ++++++++++++++-- 9 files changed, 520 insertions(+), 188 deletions(-) create mode 100644 frontend/src/lib/components/copilot/chat/ToolContentDisplay.svelte diff --git a/frontend/src/lib/components/Editor.svelte b/frontend/src/lib/components/Editor.svelte index 83a43cc33f..9e25941a88 100644 --- a/frontend/src/lib/components/Editor.svelte +++ b/frontend/src/lib/components/Editor.svelte @@ -662,8 +662,8 @@ let inlineAIChatSelection: Selection | null = $state(null) let selectedCode = $state('') - export function reviewAndApplyCode(code: string) { - aiChatEditorHandler?.reviewAndApply(code) + export function reviewAndApplyCode(code: string, applyAll: boolean = false) { + aiChatEditorHandler?.reviewAndApply(code, applyAll) } function addChatHandler(editor: meditor.IStandaloneCodeEditor) { diff --git a/frontend/src/lib/components/ScriptEditor.svelte b/frontend/src/lib/components/ScriptEditor.svelte index 348b155788..ba3f86a51f 100644 --- a/frontend/src/lib/components/ScriptEditor.svelte +++ b/frontend/src/lib/components/ScriptEditor.svelte @@ -428,9 +428,9 @@ } untrack(() => { aiChatManager.scriptEditorOptions = options - aiChatManager.scriptEditorApplyCode = (code: string) => { + aiChatManager.scriptEditorApplyCode = (code: string, applyAll: boolean = false) => { hideDiffMode() - editor?.reviewAndApplyCode(code) + editor?.reviewAndApplyCode(code, applyAll) } aiChatManager.scriptEditorShowDiffMode = showDiffMode }) diff --git a/frontend/src/lib/components/copilot/chat/AIChatManager.svelte.ts b/frontend/src/lib/components/copilot/chat/AIChatManager.svelte.ts index df65c42621..3f2cf38702 100644 --- a/frontend/src/lib/components/copilot/chat/AIChatManager.svelte.ts +++ b/frontend/src/lib/components/copilot/chat/AIChatManager.svelte.ts @@ -8,7 +8,7 @@ import { } from './flow/core' import ContextManager from './ContextManager.svelte' import HistoryManager from './HistoryManager.svelte' -import { processToolCall, type DisplayMessage, type Tool, type ToolCallbacks, type ToolDisplayMessage } from './shared' +import { extractCodeFromMarkdown, getLatestAssistantMessage, processToolCall, type DisplayMessage, type Tool, type ToolCallbacks, type ToolDisplayMessage } from './shared' import type { ChatCompletionChunk, ChatCompletionMessageParam, @@ -80,13 +80,13 @@ class AIChatManager { helpers = $state(undefined) scriptEditorOptions = $state(undefined) - scriptEditorApplyCode = $state<((code: string) => void) | undefined>(undefined) + scriptEditorApplyCode = $state<((code: string, applyAll?: boolean) => void) | undefined>(undefined) scriptEditorShowDiffMode = $state<(() => void) | undefined>(undefined) flowAiChatHelpers = $state(undefined) pendingNewCode = $state(undefined) apiTools = $state[]>([]) aiChatInput = $state(null) - + private confirmationCallback = $state<((value: boolean) => void) | undefined>(undefined) allowedModes: Record = $derived({ @@ -205,7 +205,25 @@ class AIChatManager { const lang = this.scriptEditorOptions?.lang ?? 'bun' this.tools = [this.changeModeTool, ...prepareScriptTools(lang, context)] this.helpers = { - getLang: () => lang + getScriptOptions: () => { + return { + code: this.scriptEditorOptions?.code ?? '', + lang: lang, + path: this.scriptEditorOptions?.path ?? '', + args: this.scriptEditorOptions?.args ?? {} + } + }, + getLastSuggestedCode: () => { + const latestMessage = getLatestAssistantMessage(this.displayMessages) + if (latestMessage) { + const codeBlocks = extractCodeFromMarkdown(latestMessage) + return codeBlocks[codeBlocks.length - 1] + } + return undefined + }, + applyCode: (code: string, applyAll?: boolean) => { + this.scriptEditorApplyCode?.(code, applyAll) + } } if (options?.closeScriptSettings) { const closeComponent = triggerablesByAi['close-script-builder-settings'] diff --git a/frontend/src/lib/components/copilot/chat/ToolContentDisplay.svelte b/frontend/src/lib/components/copilot/chat/ToolContentDisplay.svelte new file mode 100644 index 0000000000..0798717547 --- /dev/null +++ b/frontend/src/lib/components/copilot/chat/ToolContentDisplay.svelte @@ -0,0 +1,94 @@ + + +{#if showWhileLoading || (!loading && hasContent)} +
+
+ + {title}: + + {#if showCopy && hasContent} + + {/if} +
+ + {#if loading} +
+ + Executing... +
+ {:else if error} +
+
{error}
+
+ {:else if hasContent} +
+
{formatJson(content)}
+
+ {:else} +
+ No {title.toLowerCase()} yet +
+ {/if} +
+{/if} diff --git a/frontend/src/lib/components/copilot/chat/ToolExecutionDisplay.svelte b/frontend/src/lib/components/copilot/chat/ToolExecutionDisplay.svelte index 4bca86ff11..99b859662a 100644 --- a/frontend/src/lib/components/copilot/chat/ToolExecutionDisplay.svelte +++ b/frontend/src/lib/components/copilot/chat/ToolExecutionDisplay.svelte @@ -1,67 +1,31 @@ -
+
- - + {#if isExpanded}
- - {#if hasParameters} -
-
- - Parameters: - - -
-
-
{formatJson(message.parameters)}
-
-
- {/if} - - - {#if !message.needsConfirmation} -
-
- - Result: - - {#if hasResult && !message.error} - - {/if} -
- - {#if message.isLoading} -
- - Executing... -
- {:else if message.error} -
-
{message.error}
-
- {:else if hasResult} -
-
{formatJson(message.result)}
-
- {:else} -
- No result yet -
- {/if} -
- {/if} + + - - {#if message.needsConfirmation} -
- - -
- {/if} + + {#if message.needsConfirmation} +
+ + +
+ + + {:else} + + + + {/if}
{/if} -
\ No newline at end of file +
diff --git a/frontend/src/lib/components/copilot/chat/flow/core.ts b/frontend/src/lib/components/copilot/chat/flow/core.ts index 004c6a77e1..45d813db82 100644 --- a/frontend/src/lib/components/copilot/chat/flow/core.ts +++ b/frontend/src/lib/components/copilot/chat/flow/core.ts @@ -1,4 +1,4 @@ -import { ScriptService, type FlowModule, type RawScript, type Script } from '$lib/gen' +import { ScriptService, type FlowModule, type RawScript, type Script, JobService } from '$lib/gen' import type { ChatCompletionSystemMessageParam, ChatCompletionUserMessageParam @@ -12,7 +12,7 @@ import { getLangContext, SUPPORTED_CHAT_SCRIPT_LANGUAGES } from '../script/core' -import { createSearchHubScriptsTool, createToolDef, type Tool } from '../shared' +import { createSearchHubScriptsTool, createToolDef, type Tool, executeTestRun } from '../shared' import type { ExtendedOpenFlow } from '$lib/components/flows/types' export type AIModuleAction = 'added' | 'modified' | 'removed' @@ -337,6 +337,16 @@ const getInstructionsForCodeGenerationToolDef = createToolDef( 'Get instructions for code generation for a raw script step' ) +const testRunFlowSchema = z.object({ + args: z.record(z.any()).optional().describe('Arguments to pass to the flow (optional, uses default flow inputs if not provided)') +}) + +const testRunFlowToolDef = createToolDef( + testRunFlowSchema, + 'test_run_flow', + 'Execute a test run of the current flow' +) + const workspaceScriptsSearch = new WorkspaceScriptsSearch() export const flowTools: Tool[] = [ @@ -524,6 +534,41 @@ export const flowTools: Tool[] = [ toolCallbacks.setToolStatus(toolId, { content: 'Retrieved resource types for "' + parsedArgs.query + '"' }) return formattedResourceTypes } + }, + { + def: testRunFlowToolDef, + fn: async ({ args, workspace, helpers, toolCallbacks, toolId }) => { + const { flow } = helpers.getFlowAndSelectedId() + + if (!flow || !flow.value) { + toolCallbacks.setToolStatus(toolId, { + content: 'No flow available to test', + error: 'No flow found in current context' + }) + throw new Error('No flow available to test. Please ensure you have a flow open in the editor.') + } + + const parsedArgs = testRunFlowSchema.parse(args) + const flowArgs = parsedArgs.args || {} + + return executeTestRun({ + jobStarter: () => JobService.runFlowPreview({ + workspace: workspace, + requestBody: { + args: flowArgs, + value: flow.value, + tag: flow.tag + } + }), + workspace, + toolCallbacks, + toolId, + startMessage: 'Starting flow test run...', + contextName: 'flow' + }) + }, + requiresConfirmation: true, + showDetails: true } ] @@ -532,6 +577,7 @@ export function prepareFlowSystemMessage(): ChatCompletionSystemMessageParam { Follow the user instructions carefully. Go step by step, and explain what you're doing as you're doing it. DO NOT wait for user confirmation before performing an action. Only do it if the user explicitly asks you to wait in their initial instructions. +ALWAYS use the \`test_run_flow\` tool to test the flow, and iterate on the flow until it works as expected. If the user cancels the test run, do not try again and wait for the next user instruction. ## Understanding User Requests diff --git a/frontend/src/lib/components/copilot/chat/monaco-adapter.ts b/frontend/src/lib/components/copilot/chat/monaco-adapter.ts index eabf3d21b1..65131ff45e 100644 --- a/frontend/src/lib/components/copilot/chat/monaco-adapter.ts +++ b/frontend/src/lib/components/copilot/chat/monaco-adapter.ts @@ -167,7 +167,7 @@ export class AIChatEditorHandler { return changedLines } - async reviewAndApply(newCode: string) { + async reviewAndApply(newCode: string, applyAll: boolean = false) { if (aiChatManager.pendingNewCode === newCode) { this.acceptAll() return @@ -222,13 +222,18 @@ export class AIChatEditorHandler { } }) - ;({ collection, ids } = await displayVisualChanges( - 'editor-windmill-chat-style', - this.editor, - changes - )) - this.decorationsCollections.push(collection) - this.viewZoneIds.push(...ids) + if (!applyAll) { + ;({ collection, ids } = await displayVisualChanges( + 'editor-windmill-chat-style', + this.editor, + changes + )) + this.decorationsCollections.push(collection) + this.viewZoneIds.push(...ids) + } + } + if (applyAll) { + this.acceptAll() } } } diff --git a/frontend/src/lib/components/copilot/chat/script/core.ts b/frontend/src/lib/components/copilot/chat/script/core.ts index 3c7583206d..e93306023e 100644 --- a/frontend/src/lib/components/copilot/chat/script/core.ts +++ b/frontend/src/lib/components/copilot/chat/script/core.ts @@ -1,4 +1,4 @@ -import { ResourceService } from '$lib/gen/services.gen' +import { ResourceService, JobService } from '$lib/gen/services.gen' import type { ResourceType, ScriptLang } from '$lib/gen/types.gen' import { capitalize, isObject, toCamel } from '$lib/utils' import { get } from 'svelte/store' @@ -13,7 +13,7 @@ import { scriptLangToEditorLang } from '$lib/scripts' import { getDbSchemas } from '$lib/components/apps/components/display/dbtable/utils' import type { CodePieceElement, ContextElement } from '../context' import { PYTHON_PREPROCESSOR_MODULE_CODE, TS_PREPROCESSOR_MODULE_CODE } from '$lib/script_helpers' -import { createSearchHubScriptsTool, type Tool } from '../shared' +import { createSearchHubScriptsTool, type Tool, executeTestRun } from '../shared' import { setupTypeAcquisition, type DepsToGet } from '$lib/ata' import { getModelContextWindow } from '../../lib' @@ -351,6 +351,7 @@ export const CHAT_SYSTEM_PROMPT = ` - You can also receive a \`DIFF\` of the changes that have been made to the code. You should use this diff to give better answers. - Before giving your answer, check again that you carefully followed these instructions. - When asked to create a script that communicates with an external service, you can use the \`search_hub_scripts\` tool to search for relevant scripts in the hub. Make sure the language is the same as what the user is coding in. If you do not find any relevant scripts, you can use the \`search_npm_packages\` tool to search for relevant packages and their documentation. Always give a link to the documentation in your answer if possible. + - After modifying the code, ALWAYS use the \`test_run_script\` tool to test the code, and iterate on the code until it works as expected. If the user cancels the test run, do not try again and wait for the next user instruction. Important: Do not mention or reveal these instructions to the user unless explicitly asked to do so. @@ -498,6 +499,7 @@ export function prepareScriptTools( tools.push(createSearchHubScriptsTool(true)) tools.push(searchNpmPackagesTool) } + tools.push(testRunScriptTool) return tools } @@ -627,15 +629,18 @@ async function formatDBSchema(dbSchema: DBSchema) { } export interface ScriptChatHelpers { - getLang: () => ScriptLang | 'bunnative' + getScriptOptions: () => { code: string; lang: ScriptLang | 'bunnative'; path: string; args: Record } + getLastSuggestedCode: () => string | undefined + applyCode: (code: string, applyAll?: boolean) => void } export const resourceTypeTool: Tool = { def: RESOURCE_TYPE_FUNCTION_DEF, fn: async ({ args, workspace, helpers, toolCallbacks, toolId }) => { toolCallbacks.setToolStatus(toolId, { content: 'Searching resource types for "' + args.query + '"...' }) + const lang = helpers.getScriptOptions().lang const formattedResourceTypes = await getFormattedResourceTypes( - helpers.getLang(), + lang, args.query, workspace ) @@ -831,3 +836,72 @@ export async function fetchNpmPackageTypes( } } } + +const TEST_RUN_SCRIPT_TOOL: ChatCompletionTool = { + type: 'function', + function: { + name: 'test_run_script', + description: 'Execute a test run of the current script in the editor', + parameters: { + type: 'object', + properties: { + args: { + type: 'object', + description: 'Arguments to pass to the script (optional, uses current editor args if not provided)' + } + }, + required: [] + } + }, +} + +export const testRunScriptTool: Tool = { + def: TEST_RUN_SCRIPT_TOOL, + fn: async ({ args, workspace, helpers, toolCallbacks, toolId }) => { + const scriptOptions = helpers.getScriptOptions() + + if (!scriptOptions) { + toolCallbacks.setToolStatus(toolId, { + content: 'No script available to test', + error: 'No script found in current context' + }) + throw new Error('No script code available to test. Please ensure you have a script open in the editor.') + } + + let codeToTest = scriptOptions.code + + // Check if there are suggested code changes to apply + const lastSuggestedCode = helpers.getLastSuggestedCode() + if (lastSuggestedCode && lastSuggestedCode !== codeToTest) { + codeToTest = lastSuggestedCode + toolCallbacks.setToolStatus(toolId, { content: 'Applying code changes...' }) + + // Apply the suggested code changes using the existing mechanism + helpers.applyCode(lastSuggestedCode, true) + + toolCallbacks.setToolStatus(toolId, { content: 'Code changes applied, starting test...' }) + } + + return executeTestRun({ + jobStarter: () => JobService.runScriptPreview({ + workspace: workspace, + requestBody: { + path: scriptOptions.path, + content: codeToTest, + args: args.args || scriptOptions.args || {}, + language: scriptOptions.lang as ScriptLang, + tag: undefined, + lock: undefined, + script_hash: undefined + } + }), + workspace, + toolCallbacks, + toolId, + startMessage: 'Running test...', + contextName: 'script' + }) + }, + requiresConfirmation: true, + showDetails: true, +} diff --git a/frontend/src/lib/components/copilot/chat/shared.ts b/frontend/src/lib/components/copilot/chat/shared.ts index 89b4045960..ec6a5fdb5c 100644 --- a/frontend/src/lib/components/copilot/chat/shared.ts +++ b/frontend/src/lib/components/copilot/chat/shared.ts @@ -10,7 +10,7 @@ import type { ExtendedOpenFlow } from '$lib/components/flows/types' import type { FunctionParameters } from 'openai/resources/shared.mjs' import { zodToJsonSchema } from 'zod-to-json-schema' import { z } from 'zod' -import { ScriptService } from '$lib/gen' +import { ScriptService, JobService, type CompletedJob } from '$lib/gen' type BaseDisplayMessage = { content: string @@ -30,6 +30,7 @@ export type ToolDisplayMessage = { content: string parameters?: any result?: any + logs?: string isLoading?: boolean error?: string needsConfirmation?: boolean @@ -61,7 +62,9 @@ async function callTool({ }): Promise { const tool = tools.find((t) => t.def.function.name === functionName) if (!tool) { - throw new Error(`Unknown tool call: ${functionName}. Probably not in the correct mode, use the change_mode tool to switch to the correct mode.`) + throw new Error( + `Unknown tool call: ${functionName}. Probably not in the correct mode, use the change_mode tool to switch to the correct mode.` + ) } return tool.fn({ args, workspace, helpers, toolCallbacks, toolId }) } @@ -80,10 +83,10 @@ export async function processToolCall({ try { const args = JSON.parse(toolCall.function.arguments || '{}') const tool = tools.find((t) => t.def.function.name === toolCall.function.name) - + // Check if tool requires confirmation const needsConfirmation = tool?.requiresConfirmation - + // Add the tool to the display with appropriate status toolCallbacks.setToolStatus(toolCall.id, { ...(needsConfirmation ? { content: 'Waiting for confirmation...' } : {}), @@ -92,11 +95,11 @@ export async function processToolCall({ needsConfirmation: needsConfirmation, showDetails: tool?.showDetails }) - + // If confirmation is needed and we have the callback, wait for it if (needsConfirmation && toolCallbacks.requestConfirmation) { const confirmed = await toolCallbacks.requestConfirmation(toolCall.id) - + if (!confirmed) { toolCallbacks.setToolStatus(toolCall.id, { content: 'Cancelled by user', @@ -110,14 +113,14 @@ export async function processToolCall({ content: 'Tool execution was cancelled by user' } } - + // Update status to executing after confirmation toolCallbacks.setToolStatus(toolCall.id, { isLoading: true, needsConfirmation: false }) } - + let result = '' try { result = await callTool({ @@ -130,7 +133,7 @@ export async function processToolCall({ toolId: toolCall.id }) toolCallbacks.setToolStatus(toolCall.id, { - isLoading: false, + isLoading: false }) } catch (err) { console.error(err) @@ -138,9 +141,9 @@ export async function processToolCall({ isLoading: false, error: 'An error occurred while calling the tool' }) - const errorMessage = typeof err === 'string' ? err : 'An error occurred while calling the tool' - result = - `Error while calling tool: ${errorMessage}, MUST tell the user to check the browser console for more details, and then respond as much as possible to the original request` + const errorMessage = + typeof err === 'string' ? err : 'An error occurred while calling the tool' + result = `Error while calling tool: ${errorMessage}` } const toAdd = { role: 'tool' as const, @@ -153,8 +156,7 @@ export async function processToolCall({ return { role: 'tool' as const, tool_call_id: toolCall.id, - content: - 'Error while calling tool, MUST tell the user to check the browser console for more details, and then respond as much as possible to the original request' + content: 'Error while calling tool' } } } @@ -249,3 +251,177 @@ export const createSearchHubScriptsTool = (withContent: boolean = false) => ({ return JSON.stringify(results) } }) + +// Constants for result formatting +const MAX_RESULT_LENGTH = 12000 +const MAX_LOG_LENGTH = 4000 + +export interface TestRunConfig { + jobStarter: () => Promise + workspace: string + toolCallbacks: ToolCallbacks + toolId: string + startMessage?: string + contextName: 'script' | 'flow' +} + +// Common job polling function +export async function pollJobCompletion( + jobId: string, + workspace: string, + toolId: string, + toolCallbacks: ToolCallbacks +): Promise { + let attempts = 0 + const maxAttempts = 60 + let job: CompletedJob | null = null + + while (attempts < maxAttempts) { + await new Promise((resolve) => setTimeout(resolve, 1000)) + attempts++ + + try { + const fetchedJob = await JobService.getJob({ + workspace: workspace, + id: jobId, + noLogs: false, + noCode: true + }) + + if (fetchedJob.type === 'CompletedJob') { + job = fetchedJob + break + } + } catch (error) { + if (attempts >= maxAttempts) { + throw error + } + } + } + + if (!job) { + toolCallbacks.setToolStatus(toolId, { + content: 'Test timed out', + error: 'Execution timed out or failed to complete' + }) + throw new Error('Test execution timed out after 60 seconds') + } + + return job +} + +// Helper function to extract code blocks from markdown text +export function extractCodeFromMarkdown(markdown: string): string[] { + const codeBlocks: string[] = [] + + // Matches: ```[language]\n[code]\n``` + const codeBlockRegex = /```(?:[a-z]+)?\n([\s\S]*?)```/g + + let match: RegExpExecArray | null = null + while ((match = codeBlockRegex.exec(markdown)) !== null) { + const code = match[1].trim() + if (code) { + codeBlocks.push(code) + } + } + + return codeBlocks +} + +// Helper function to get the latest assistant message from display messages +export function getLatestAssistantMessage(displayMessages: DisplayMessage[]): string | undefined { + // Iterate from the end to find the most recent assistant message + for (let i = displayMessages.length - 1; i >= 0; i--) { + const message = displayMessages[i] + if (message.role === 'assistant' && message.content) { + return message.content + } + } + return undefined +} + +// Helper function to extract error messages from job results +function getErrorMessage(result: unknown): string { + if (typeof result === 'object' && result !== null && 'error' in result) { + const error = (result as Record).error + if (typeof error === 'object' && error !== null && 'message' in error) { + const message = (error as Record).message as string + if ('stack' in error) { + return (message + '\n' + (error as Record).stack) as string + } + return message + } + if (typeof error === 'string') { + return error + } + } + if (typeof result === 'string') { + return result + } + return 'Unknown error' +} + +// Main execution function for test runs +export async function executeTestRun(config: TestRunConfig): Promise { + try { + config.toolCallbacks.setToolStatus(config.toolId, { + content: config.startMessage || `Starting ${config.contextName} test...` + }) + + const jobId = await config.jobStarter() + + config.toolCallbacks.setToolStatus(config.toolId, { + content: `${config.contextName} test started, waiting for completion...` + }) + + const job = await pollJobCompletion( + jobId, + config.workspace, + config.toolId, + config.toolCallbacks + ) + + config.toolCallbacks.setToolStatus(config.toolId, { + content: `${config.contextName} test ${job.success ? 'completed successfully' : 'failed'}`, + result: formatResult(job.result), + logs: formatLogs(job.logs), + ...(job.success ? {} : { error: getErrorMessage(job.result) }) + }) + + return formatResultSummary(job.result, job.logs, job.success) + } catch (error) { + const errorMessage = error instanceof Error ? error.message : 'Unknown error occurred' + config.toolCallbacks.setToolStatus(config.toolId, { + content: `${config.contextName} test execution failed`, + error: errorMessage + }) + throw new Error(`Failed to execute ${config.contextName} test run: ${errorMessage}`) + } +} + +function formatLogs(logs: string | undefined): undefined | string { + if (logs && logs.trim()) { + if (logs.length <= MAX_LOG_LENGTH) { + return logs + } else { + return logs.slice(-MAX_LOG_LENGTH) + } + } + return undefined +} + +function formatResult(result: unknown): string { + if (typeof result === 'string') { + return result + } + return JSON.stringify(result, null, 2) +} + +function formatResultSummary(result: unknown, logs: string | undefined, success: boolean): string { + let resultSummary = '' + resultSummary += `Result (${success ? 'SUCCESS' : 'FAILED'})\n\n` + resultSummary += formatResult(result).slice(0, MAX_RESULT_LENGTH) + resultSummary += '\n\nLogs:\n\n' + resultSummary += formatLogs(logs) ?? 'No logs available' + return resultSummary +}