diff --git a/frontend/src/lib/components/copilot/chat/__tests__/app/variants/streamlined.ts b/frontend/src/lib/components/copilot/chat/__tests__/app/variants/streamlined.ts index 027bf55474..515db0a756 100644 --- a/frontend/src/lib/components/copilot/chat/__tests__/app/variants/streamlined.ts +++ b/frontend/src/lib/components/copilot/chat/__tests__/app/variants/streamlined.ts @@ -56,7 +56,7 @@ For inline scripts, the code must have a \`main\` function as its entrypoint. - \`lint()\`: Lint all files. Returns errors/warnings grouped by frontend/backend. ### Discovery -- \`list_workspace_runnables(query, type?)\`: Search workspace scripts and flows +- \`search_workspace(query, type)\`: Search workspace scripts and flows - \`search_hub_scripts(query)\`: Search hub scripts ## Backend Runnable Configuration diff --git a/frontend/src/lib/components/copilot/chat/__tests__/flow/variants/minimal-single-tool.ts b/frontend/src/lib/components/copilot/chat/__tests__/flow/variants/minimal-single-tool.ts index 24d3036f4e..a07b24b691 100644 --- a/frontend/src/lib/components/copilot/chat/__tests__/flow/variants/minimal-single-tool.ts +++ b/frontend/src/lib/components/copilot/chat/__tests__/flow/variants/minimal-single-tool.ts @@ -84,7 +84,8 @@ const MINIMAL_SINGLE_TOOL_SYSTEM_PROMPT = `You are a helpful assistant that crea **Code & Scripts:** - **Get language-specific coding instructions** → \`get_instructions_for_code_generation\` (call BEFORE writing code) -- **Find workspace scripts** → \`search_scripts\` +- **Find workspace scripts and flows** → \`search_workspace\` +- **Get details of a specific script or flow** → \`get_runnable_details\` - **Find Windmill Hub scripts** → \`search_hub_scripts\` **Testing:** @@ -310,7 +311,8 @@ Example: Before writing TypeScript/Bun code, call \`get_instructions_for_code_ge ### Creating Flows 1. **Search for existing scripts first** (unless user explicitly asks to write from scratch): - - First: \`search_scripts\` to find workspace scripts + - First: \`search_workspace\` to find workspace scripts and flows + - Use \`get_runnable_details\` to inspect a specific script or flow (inputs, description, code) - Then: \`search_hub_scripts\` (only consider highly relevant results) - Only create raw scripts if no suitable script is found diff --git a/frontend/src/lib/components/copilot/chat/app/core.ts b/frontend/src/lib/components/copilot/chat/app/core.ts index 39591437db..8fc88d4d16 100644 --- a/frontend/src/lib/components/copilot/chat/app/core.ts +++ b/frontend/src/lib/components/copilot/chat/app/core.ts @@ -3,10 +3,13 @@ import type { ChatCompletionUserMessageParam } from 'openai/resources/chat/completions.mjs' import { z } from 'zod' -import { createSearchHubScriptsTool, createToolDef, type Tool } from '../shared' -import { FlowService, ScriptService, type Flow, type Script } from '$lib/gen' -import uFuzzy from '@leeoniya/ufuzzy' -import { emptyString } from '$lib/utils' +import { + createSearchHubScriptsTool, + createToolDef, + createSearchWorkspaceTool, + createGetRunnableDetailsTool, + type Tool +} from '../shared' import { aiChatManager } from '../AIChatManager.svelte' import type { ContextElement, @@ -366,102 +369,6 @@ const getGetSelectedContextToolDef = memo(() => ) ) -// ============= Workspace Runnables Search ============= - -const getListWorkspaceRunnablesSchema = memo(() => - z.object({ - query: z.string().describe('The search query to find workspace scripts and flows'), - type: z - .enum(['all', 'scripts', 'flows']) - .describe( - 'Filter by type: "scripts" for scripts only, "flows" for flows only, "all" for both. Defaults to "all".' - ) - }) -) -const getListWorkspaceRunnablesToolDef = memo(() => - createToolDef( - getListWorkspaceRunnablesSchema(), - 'list_workspace_runnables', - 'Search for workspace scripts and flows by query. Returns fully qualified paths that can be used in backend runnables.' - ) -) - -class WorkspaceRunnablesSearch { - private uf: uFuzzy - private workspace: string | undefined = undefined - private scripts: Script[] | undefined = undefined - private flows: Flow[] | undefined = undefined - - constructor() { - this.uf = new uFuzzy() - } - - private async initScripts(workspace: string) { - if (this.scripts === undefined || this.workspace !== workspace) { - this.scripts = await ScriptService.listScripts({ workspace }) - this.workspace = workspace - } - } - - private async initFlows(workspace: string) { - if (this.flows === undefined || this.workspace !== workspace) { - this.flows = await FlowService.listFlows({ workspace }) - this.workspace = workspace - } - } - - async searchScripts(query: string, workspace: string) { - await this.initScripts(workspace) - const scripts = this.scripts - if (!scripts) return [] - - const results = this.uf.search( - scripts.map((s) => (emptyString(s.summary) ? s.path : s.summary + ' (' + s.path + ')')), - query.trim() - ) - return ( - results[2]?.map((id) => ({ - type: 'script' as const, - path: scripts[id].path, - summary: scripts[id].summary - })) ?? [] - ) - } - - async searchFlows(query: string, workspace: string) { - await this.initFlows(workspace) - const flows = this.flows - if (!flows) return [] - - const results = this.uf.search( - flows.map((f) => (emptyString(f.summary) ? f.path : f.summary + ' (' + f.path + ')')), - query.trim() - ) - return ( - results[2]?.map((id) => ({ - type: 'flow' as const, - path: flows[id].path, - summary: flows[id].summary - })) ?? [] - ) - } - - async search(query: string, workspace: string, type: 'all' | 'scripts' | 'flows' = 'all') { - const results: { type: 'script' | 'flow'; path: string; summary: string }[] = [] - - if (type === 'all' || type === 'scripts') { - results.push(...(await this.searchScripts(query, workspace))) - } - if (type === 'all' || type === 'flows') { - results.push(...(await this.searchFlows(query, workspace))) - } - - return results - } -} - -const workspaceRunnablesSearch = new WorkspaceRunnablesSearch() - // ============= Lint Result Formatting ============= function formatLintMessages(messages: Record): string { @@ -742,23 +649,8 @@ export const getAppTools = memo((): Tool[] => [ } }, // Search tools - { - def: getListWorkspaceRunnablesToolDef(), - fn: async ({ args, workspace, toolId, toolCallbacks }) => { - const parsedArgs = getListWorkspaceRunnablesSchema().parse(args) - const type = parsedArgs.type ?? 'all' - toolCallbacks.setToolStatus(toolId, { - content: `Searching workspace ${type} for "${parsedArgs.query}"...` - }) - - const results = await workspaceRunnablesSearch.search(parsedArgs.query, workspace, type) - - toolCallbacks.setToolStatus(toolId, { - content: `Found ${results.length} workspace runnables matching "${parsedArgs.query}"` - }) - return JSON.stringify(results, null, 2) - } - }, + createSearchWorkspaceTool(), + createGetRunnableDetailsTool(), // Hub scripts search (reuse from shared) createSearchHubScriptsTool(false), // Data table tools @@ -913,7 +805,8 @@ For inline scripts, the code must have a \`main\` function as its entrypoint. - \`lint()\`: Lint all files. Returns errors/warnings grouped by frontend/backend. Use this to check for issues after making changes. ### Discovery -- \`list_workspace_runnables(query, type?)\`: Search workspace scripts and flows +- \`search_workspace(query, type)\`: Search workspace scripts and flows +- \`get_runnable_details(path, type)\`: Get details (summary, description, schema, content) of a specific script or flow - \`search_hub_scripts(query)\`: Search hub scripts ### Data Tables @@ -1060,7 +953,7 @@ When you are using the windmill-client, do not forget that as id for variables o 3. Make changes using \`set_frontend_file\` and \`set_backend_runnable\`. These return lint diagnostics. 4. Use \`lint()\` at the end to check for and fix any remaining errors -When creating a new app, use \`list_workspace_runnables\` or \`search_hub_scripts\` to find existing scripts/flows to reuse. +When creating a new app, use \`search_workspace\` or \`search_hub_scripts\` to find existing scripts/flows to reuse. ` diff --git a/frontend/src/lib/components/copilot/chat/flow/core.ts b/frontend/src/lib/components/copilot/chat/flow/core.ts index f57562b1d2..f695da0aa1 100644 --- a/frontend/src/lib/components/copilot/chat/flow/core.ts +++ b/frontend/src/lib/components/copilot/chat/flow/core.ts @@ -3,7 +3,6 @@ import { type FlowModule, type InputTransform, type RawScript, - type Script, JobService } from '$lib/gen' import type { @@ -11,8 +10,6 @@ import type { ChatCompletionUserMessageParam } from 'openai/resources/chat/completions.mjs' import { z } from 'zod' -import uFuzzy from '@leeoniya/ufuzzy' -import { emptyString } from '$lib/utils' import { createDbSchemaTool, getFormattedResourceTypes, @@ -31,7 +28,9 @@ import { findModuleById, SPECIAL_MODULE_IDS, formatScriptLintResult, - type ScriptLintResult + type ScriptLintResult, + createSearchWorkspaceTool, + createGetRunnableDetailsTool } from '../shared' import type { ContextElement } from '../context' import type { ExtendedOpenFlow } from '$lib/components/flows/types' @@ -208,10 +207,7 @@ function getExpectedFormat(schema: z.ZodType): string | null { let current = schema // Unwrap optional/nullable to get inner type - while ( - (current as any)._def.type === 'optional' || - (current as any)._def.type === 'nullable' - ) { + while ((current as any)._def.type === 'optional' || (current as any)._def.type === 'nullable') { current = (current as any)._def.innerType if (!current || !(current as any)._def) break } @@ -285,18 +281,6 @@ export interface FlowAIChatHelpers { getLintErrors: (moduleId: string) => Promise } -const searchScriptsSchema = z.object({ - query: z - .string() - .describe('The query to search for, e.g. send email, list stripe invoices, etc..') -}) - -const searchScriptsToolDef = createToolDef( - searchScriptsSchema, - 'search_scripts', - 'Search for scripts in the workspace. Returns array of {path, summary} objects.' -) - const langSchema = z.enum( SUPPORTED_CHAT_SCRIPT_LANGUAGES as [RawScript['language'], ...RawScript['language'][]] ) @@ -337,47 +321,6 @@ const setFlowJsonToolDef = createToolDef( { strict: false } ) -class WorkspaceScriptsSearch { - private uf: uFuzzy - private workspace: string | undefined = undefined - private scripts: Script[] | undefined = undefined - - constructor() { - this.uf = new uFuzzy() - } - - private async init(workspace: string) { - this.scripts = await ScriptService.listScripts({ - workspace - }) - this.workspace = workspace - } - - async search(query: string, workspace: string) { - if (this.scripts === undefined || this.workspace !== workspace) { - await this.init(workspace) - } - - const scripts = this.scripts - - if (!scripts) { - throw new Error('Failed to load scripts') - } - - const results = this.uf.search( - scripts.map((s) => (emptyString(s.summary) ? s.path : s.summary + ' (' + s.path + ')')), - query.trim() - ) - const scriptResults = - results[2]?.map((id) => ({ - path: scripts[id].path, - summary: scripts[id].summary - })) ?? [] - - return scriptResults - } -} - // Will be overridden by setSchema const testRunFlowSchema = z.object({ args: z @@ -440,30 +383,11 @@ const getLintErrorsToolDef = createToolDef( 'Get lint errors and warnings from a rawscript module. Pass module_id to focus a specific module and check its errors. ALWAYS call this for EACH module where you modified inline script code.' ) -const workspaceScriptsSearch = new WorkspaceScriptsSearch() - export const flowTools: Tool[] = [ createSearchHubScriptsTool(false), createDbSchemaTool(), - { - def: searchScriptsToolDef, - fn: async ({ args, workspace, toolId, toolCallbacks }) => { - toolCallbacks.setToolStatus(toolId, { - content: 'Searching for workspace scripts related to "' + args.query + '"...' - }) - const parsedArgs = searchScriptsSchema.parse(args) - const scriptResults = await workspaceScriptsSearch.search(parsedArgs.query, workspace) - toolCallbacks.setToolStatus(toolId, { - content: - 'Found ' + - scriptResults.length + - ' scripts in the workspace related to "' + - args.query + - '"' - }) - return JSON.stringify(scriptResults) - } - }, + createSearchWorkspaceTool(), + createGetRunnableDetailsTool(), { def: resourceTypeToolDef, fn: async ({ args, toolId, workspace, toolCallbacks }) => { @@ -748,8 +672,7 @@ export const flowTools: Tool[] = [ const path = e.path // Try to find module id for better context const moduleIndex = typeof path[0] === 'number' ? path[0] : undefined - const moduleId = - moduleIndex !== undefined ? parsedModules[moduleIndex]?.id : undefined + const moduleId = moduleIndex !== undefined ? parsedModules[moduleIndex]?.id : undefined const fieldPath = path.slice(1).join('.') let message = e.message @@ -852,7 +775,8 @@ export function prepareFlowSystemMessage(customPrompt?: string): ChatCompletionS - **View existing inline script code** → \`inspect_inline_script\` - **Change module code only** → \`set_module_code\` - **Get language-specific coding instructions** → \`get_instructions_for_code_generation\` (call BEFORE writing code) -- **Find workspace scripts** → \`search_scripts\` +- **Find workspace scripts and flows** → \`search_workspace\` +- **Get details of a specific script or flow** → \`get_runnable_details\` - **Find Windmill Hub scripts** → \`search_hub_scripts\` **Testing & Linting:** @@ -1059,7 +983,8 @@ Example: Before writing TypeScript/Bun code, call \`get_instructions_for_code_ge ### Creating Flows 1. **Search for existing scripts first** (unless user explicitly asks to write from scratch): - - First: \`search_scripts\` to find workspace scripts + - First: \`search_workspace\` to find workspace scripts and flows + - Use \`get_runnable_details\` to inspect a specific script or flow (inputs, description, code) - Then: \`search_hub_scripts\` (only consider highly relevant results) - Only create raw scripts if no suitable script is found diff --git a/frontend/src/lib/components/copilot/chat/navigator/core.ts b/frontend/src/lib/components/copilot/chat/navigator/core.ts index d43de36f5e..6da158e6de 100644 --- a/frontend/src/lib/components/copilot/chat/navigator/core.ts +++ b/frontend/src/lib/components/copilot/chat/navigator/core.ts @@ -3,7 +3,7 @@ import type { ChatCompletionTool, ChatCompletionUserMessageParam } from 'openai/resources/index.mjs' -import type { Tool } from '../shared' +import { createSearchWorkspaceTool, createGetRunnableDetailsTool, type Tool } from '../shared' import { ResourceService } from '$lib/gen' import { workspaceStore } from '$lib/stores' import { get } from 'svelte/store' @@ -15,9 +15,11 @@ Windmill is an open-source developer platform for building internal tools, API i You have access to these tools: 1. View current buttons and inputs on the page (get_triggerable_components) -2. Execute buttons and inputs (trigger_component) +2. Execute buttons and inputs (trigger_component) 3. Get documentation for user requests (get_documentation) 4. Change the AI mode to the one specified (change_mode) +5. Search for scripts and flows in the workspace (search_workspace) +6. Get detailed information about a specific script or flow (get_runnable_details) INSTRUCTIONS: - When users ask about application features or concepts, first use get_documentation internally to retrieve accurate information about how to fulfill the user's request. @@ -31,6 +33,10 @@ INSTRUCTIONS: - If you are not sure about an input, set the ones you are sure about, and then ask the user for the value of the input you are not sure about. - If the user asks you to make an API call, switch to API mode with the change_mode tool before using the new tools you'll have access to to make the API call. +- When users ask about existing scripts, flows, or building blocks in their workspace, use search_workspace to find them. +- When users want to understand a specific script or flow (inputs, description, what it does), use get_runnable_details. +- When users ask you to navigate to a specific script or flow, first search for it, then use the navigation tools to go to it. + GENERAL PRINCIPLES: - Be concise but thorough - Focus on taking action and completing the user's goals @@ -357,7 +363,9 @@ export const navigatorTools: Tool<{}>[] = [ triggerComponentTool, getDocumentationTool, getCurrentPageNameTool, - getAvailableResourcesTool + getAvailableResourcesTool, + createSearchWorkspaceTool(), + createGetRunnableDetailsTool() ] export function prepareNavigatorSystemMessage( diff --git a/frontend/src/lib/components/copilot/chat/script/core.ts b/frontend/src/lib/components/copilot/chat/script/core.ts index 21bf355fb2..fa4fc192d9 100644 --- a/frontend/src/lib/components/copilot/chat/script/core.ts +++ b/frontend/src/lib/components/copilot/chat/script/core.ts @@ -17,7 +17,9 @@ import { buildTestRunArgs, buildContextString, type ScriptLintResult, - formatScriptLintResult + formatScriptLintResult, + createSearchWorkspaceTool, + createGetRunnableDetailsTool } from '../shared' import { setupTypeAcquisition, type DepsToGet } from '$lib/ata' import { getModelContextWindow } from '../../lib' @@ -178,6 +180,7 @@ function buildChatSystemPrompt(currentModel: AIProviderModel) { - 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. + - Use \`search_workspace\` to find existing scripts and flows in the workspace, and \`get_runnable_details\` to inspect their schema and code. This is useful when the user wants to reference or reuse existing workspace runnables. - After applying code changes with the \`${editToolName}\` tool, ALWAYS use the \`get_lint_errors\` tool to check for lint errors. If there are errors, fix them before proceeding. Then use the \`test_run_script\` tool to test the code, and iterate on the code until it works as expected (MAX 3 times). If the user cancels the test run, do not try again and wait for the next user instruction. Important: @@ -329,6 +332,8 @@ export function prepareScriptTools( } tools.push(testRunScriptTool) tools.push(getLintErrorsTool) + tools.push(createSearchWorkspaceTool()) + tools.push(createGetRunnableDetailsTool()) return tools } diff --git a/frontend/src/lib/components/copilot/chat/shared.ts b/frontend/src/lib/components/copilot/chat/shared.ts index c070f929eb..bc04e0bbd4 100644 --- a/frontend/src/lib/components/copilot/chat/shared.ts +++ b/frontend/src/lib/components/copilot/chat/shared.ts @@ -21,7 +21,17 @@ import { workspaceStore } from '$lib/stores' import type { ExtendedOpenFlow } from '$lib/components/flows/types' import type { FunctionParameters } from 'openai/resources/shared.mjs' import { z } from 'zod' -import { ScriptService, JobService, type CompletedJob, type FlowModule } from '$lib/gen' +import { + ScriptService, + FlowService, + JobService, + type CompletedJob, + type FlowModule, + type Script, + type Flow +} from '$lib/gen' +import uFuzzy from '@leeoniya/ufuzzy' +import { emptyString } from '$lib/utils' import { scriptLangToEditorLang } from '$lib/scripts' import { getCurrentModel } from '$lib/aiStore' import { type editor as meditor } from 'monaco-editor' @@ -658,6 +668,7 @@ export async function buildSchemaForTool( // Constants for result formatting const MAX_RESULT_LENGTH = 12000 const MAX_LOG_LENGTH = 4000 +const MAX_RUNNABLE_CONTENT_LENGTH = 20000 export interface TestRunConfig { jobStarter: () => Promise @@ -886,3 +897,228 @@ export function formatScriptLintResult(lintResult: ScriptLintResult): string { return response } + +// ============= Workspace Runnables Search ============= + +export class WorkspaceRunnablesSearch { + private uf: uFuzzy + private scriptsWorkspace: string | undefined = undefined + private flowsWorkspace: string | undefined = undefined + private scripts: Script[] | undefined = undefined + private flows: Flow[] | undefined = undefined + + constructor() { + this.uf = new uFuzzy() + } + + private async initScripts(workspace: string) { + if (this.scripts === undefined || this.scriptsWorkspace !== workspace) { + this.scripts = await ScriptService.listScripts({ workspace }) + this.scriptsWorkspace = workspace + } + } + + private async initFlows(workspace: string) { + if (this.flows === undefined || this.flowsWorkspace !== workspace) { + this.flows = await FlowService.listFlows({ workspace }) + this.flowsWorkspace = workspace + } + } + + async searchScripts(query: string, workspace: string) { + await this.initScripts(workspace) + const scripts = this.scripts + if (!scripts) return [] + + const haystack = scripts.map((s) => + emptyString(s.summary) ? s.path : s.summary + ' (' + s.path + ')' + ) + const [idxs, , order] = this.uf.search(haystack, query.trim()) + if (!idxs || !order) return [] + return order.map((orderIdx) => { + const haystackIdx = idxs[orderIdx] + return { + type: 'script' as const, + path: scripts[haystackIdx].path, + summary: scripts[haystackIdx].summary + } + }) + } + + async searchFlows(query: string, workspace: string) { + await this.initFlows(workspace) + const flows = this.flows + if (!flows) return [] + + const haystack = flows.map((f) => + emptyString(f.summary) ? f.path : f.summary + ' (' + f.path + ')' + ) + const [idxs, , order] = this.uf.search(haystack, query.trim()) + if (!idxs || !order) return [] + return order.map((orderIdx) => { + const haystackIdx = idxs[orderIdx] + return { + type: 'flow' as const, + path: flows[haystackIdx].path, + summary: flows[haystackIdx].summary + } + }) + } + + async search(query: string, workspace: string, type: 'all' | 'scripts' | 'flows' = 'all') { + const results: { type: 'script' | 'flow'; path: string; summary: string }[] = [] + + if (type === 'all' || type === 'scripts') { + results.push(...(await this.searchScripts(query, workspace))) + } + if (type === 'all' || type === 'flows') { + results.push(...(await this.searchFlows(query, workspace))) + } + + return results + } +} + +const searchWorkspaceSchema = z.object({ + query: z + .string() + .describe('Comma separated list of keywords to search for (e.g. "stripe, send email, ETL")'), + type: z + .enum(['all', 'scripts', 'flows']) + .describe( + 'Filter by type: "all" for both scripts and flows, "scripts" for scripts only, "flows" for flows only.' + ) +}) + +const searchWorkspaceToolDef = createToolDef( + searchWorkspaceSchema, + 'search_workspace', + 'Search for scripts and flows in the workspace. Use this when a user asks about existing building blocks, wants to find a script/flow, or asks "what do I have for X". ALWAYS search really broadly.' +) + +const workspaceRunnablesSearch = new WorkspaceRunnablesSearch() + +export const createSearchWorkspaceTool = () => ({ + def: searchWorkspaceToolDef, + fn: async ({ + args, + workspace, + toolId, + toolCallbacks + }: { + args: any + workspace: string + toolId: string + toolCallbacks: ToolCallbacks + }) => { + const parsedArgs = searchWorkspaceSchema.parse(args) + const type = parsedArgs.type + toolCallbacks.setToolStatus(toolId, { + content: `Searching workspace...` + }) + + const results: { type: 'script' | 'flow'; path: string; summary: string }[] = [] + const keywords = parsedArgs.query.split(',').map((keyword) => keyword.trim()) + const seenPaths = new Set() + for (const keyword of keywords) { + const keywordResults = await workspaceRunnablesSearch.search(keyword, workspace, type) + for (const result of keywordResults) { + if (!seenPaths.has(result.path)) { + results.push(result) + seenPaths.add(result.path) + } + } + } + + toolCallbacks.setToolStatus(toolId, { + content: `Found ${results.length} result(s)` + }) + return JSON.stringify(results, null, 2) + } +}) + +const getRunnableDetailsSchema = z.object({ + path: z.string().describe('The path of the script or flow (e.g. "f/marketing/send_email")'), + type: z.enum(['script', 'flow']).describe('Whether this is a script or a flow') +}) + +const getRunnableDetailsToolDef = createToolDef( + getRunnableDetailsSchema, + 'get_runnable_details', + 'Get details (summary, description, inputs schema, content) of a specific script or flow by path' +) + +export const createGetRunnableDetailsTool = () => ({ + def: getRunnableDetailsToolDef, + fn: async ({ + args, + workspace, + toolId, + toolCallbacks + }: { + args: any + workspace: string + toolId: string + toolCallbacks: ToolCallbacks + }) => { + const parsedArgs = getRunnableDetailsSchema.parse(args) + const { path, type } = parsedArgs + toolCallbacks.setToolStatus(toolId, { + content: `Getting ${type} details for "${path}"...` + }) + + try { + if (type === 'script') { + const script = await ScriptService.getScriptByPath({ workspace, path }) + toolCallbacks.setToolStatus(toolId, { + content: `Retrieved script details for "${path}"` + }) + const content = script.content ?? '' + const truncatedContent = + content.length > MAX_RUNNABLE_CONTENT_LENGTH + ? content.slice(0, MAX_RUNNABLE_CONTENT_LENGTH) + '\n... (truncated)' + : content + return JSON.stringify( + { + path: script.path, + summary: script.summary, + description: script.description, + language: script.language, + schema: script.schema, + content: truncatedContent + }, + null, + 2 + ) + } else { + const flow = await FlowService.getFlowByPath({ workspace, path }) + toolCallbacks.setToolStatus(toolId, { + content: `Retrieved flow details for "${path}"` + }) + const flowValue = JSON.stringify(flow.value, null, 2) + const truncatedValue = + flowValue.length > MAX_RUNNABLE_CONTENT_LENGTH + ? flowValue.slice(0, MAX_RUNNABLE_CONTENT_LENGTH) + '\n... (truncated)' + : flowValue + return JSON.stringify( + { + path: flow.path, + summary: flow.summary, + description: flow.description, + schema: flow.schema, + value: truncatedValue + }, + null, + 2 + ) + } + } catch (error) { + const errorMessage = error instanceof Error ? error.message : String(error) + toolCallbacks.setToolStatus(toolId, { + content: `Error getting ${type} details`, + error: errorMessage + }) + return `Error getting ${type} details for "${path}": ${errorMessage}` + } + } +})