feat: add workspace search and runnable details tools to AI chat modes (#7874)

* feat: add workspace search and runnable details tools to navigator mode

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* fix: correct uFuzzy search result indexing in workspace search

uFuzzy.search() returns [idxs, info, order] where order contains indices
into idxs, not into the original haystack. The code was using order values
directly as array indices, returning wrong results.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* refactor: mutualize search_workspace and get_runnable_details tools

- Move search_workspace tool def + implementation into shared.ts as
  createSearchWorkspaceTool() factory, used by navigator and flow modes
- Move get_runnable_details tool into shared.ts as
  createGetRunnableDetailsTool() factory, used by navigator, flow, and
  script modes
- Replace flow mode's scripts-only search_scripts with search_workspace
  that searches both scripts and flows
- Add search_workspace and get_runnable_details to script mode
- Remove duplicated WorkspaceScriptsSearch class from flow/core.ts

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* fix: add get_runnable_details to flow mode system prompt

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* fix: add hard limit on runnable content passed to AI context

Truncate script content and flow value at 20k chars in
get_runnable_details to avoid flooding the context window.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* fix: make search_workspace type param required for strict schema

OpenAI strict mode requires all properties in required array. Make type
a required enum ('all', 'scripts', 'flows') instead of optional.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* cleaning

* nit

* cleaning

* refactor: use shared createSearchWorkspaceTool in app mode

Replace app mode's local list_workspace_runnables tool with the shared
createSearchWorkspaceTool() factory, consistent with navigator, flow,
and script modes.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* search by keyword

* cleaning

* fix: document search_workspace and get_runnable_details in script mode system prompt

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* fix: add get_runnable_details tool to app mode

Without it, the AI can find scripts/flows but can't inspect their
schema/content when configuring backend runnables with correct inputs.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* fix: race condition in WorkspaceRunnablesSearch workspace caching

Track scriptsWorkspace and flowsWorkspace separately instead of a single
shared workspace field. Previously, initScripts could update the shared
workspace field, causing initFlows to skip re-fetching when the workspace
changed (it saw the workspace already matched), returning stale data.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
centdix
2026-02-10 17:44:07 +01:00
committed by GitHub
parent c2f8d5d686
commit fa2cf87e3a
7 changed files with 282 additions and 213 deletions
@@ -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
@@ -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
@@ -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, string[]>): string {
@@ -742,23 +649,8 @@ export const getAppTools = memo((): Tool<AppAIChatHelpers>[] => [
}
},
// 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.
`
@@ -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<ScriptLintResult>
}
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<FlowAIChatHelpers>[] = [
createSearchHubScriptsTool(false),
createDbSchemaTool<FlowAIChatHelpers>(),
{
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<FlowAIChatHelpers>[] = [
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
@@ -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(
@@ -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
}
@@ -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<string>
@@ -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<string>()
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}`
}
}
})