From 8041cb8c18aac664ff5494a3b08fe04542e3f1fc Mon Sep 17 00:00:00 2001 From: centdix Date: Thu, 16 Apr 2026 17:09:05 +0200 Subject: [PATCH] fix: seed empty inline flow scripts Co-Authored-By: Claude Opus 4.5 --- .../copilot/chat/flow/FlowAIChat.svelte | 3 +- .../lib/components/copilot/chat/flow/core.ts | 45 ++++++-- .../copilot/chat/flow/helperUtils.test.ts | 70 ++++++++++++ .../copilot/chat/flow/helperUtils.ts | 104 +++++++++++++++++- 4 files changed, 207 insertions(+), 15 deletions(-) create mode 100644 frontend/src/lib/components/copilot/chat/flow/helperUtils.test.ts diff --git a/frontend/src/lib/components/copilot/chat/flow/FlowAIChat.svelte b/frontend/src/lib/components/copilot/chat/flow/FlowAIChat.svelte index 146ec508aa..0028138c52 100644 --- a/frontend/src/lib/components/copilot/chat/flow/FlowAIChat.svelte +++ b/frontend/src/lib/components/copilot/chat/flow/FlowAIChat.svelte @@ -207,7 +207,7 @@ } } - applyFlowJsonUpdate(flowStore.val, inlineScriptSession, { + const result = applyFlowJsonUpdate(flowStore.val, inlineScriptSession, { modules, schema, preprocessorModule, @@ -216,6 +216,7 @@ // Refresh the state store to update UI refreshStateStore(flowStore) + return result } catch (error) { throw new Error( `Failed to parse or apply JSON: ${error instanceof Error ? error.message : String(error)}` diff --git a/frontend/src/lib/components/copilot/chat/flow/core.ts b/frontend/src/lib/components/copilot/chat/flow/core.ts index 9a6164c9a9..b7bfbd8b74 100644 --- a/frontend/src/lib/components/copilot/chat/flow/core.ts +++ b/frontend/src/lib/components/copilot/chat/flow/core.ts @@ -35,6 +35,7 @@ import { import type { ContextElement } from '../context' import type { ExtendedOpenFlow } from '$lib/components/flows/types' import { createInlineScriptSession, type InlineScriptSession } from './inlineScriptsUtils' +import type { FlowJsonUpdateResult } from './helperUtils' import { flowModuleSchema, flowModulesSchema } from './openFlowZod' import { collectAllModuleIdsFromArray } from './utils' import { FLOW_CHAT_SPECIAL_MODULES, getFlowPrompt } from '$system_prompts' @@ -276,6 +277,17 @@ type EditableFlowJson = { failure_module: FlowModule | null } +function formatEmptyInlineScriptWarning({ + emptyInlineScriptModuleIds +}: FlowJsonUpdateResult): string { + if (emptyInlineScriptModuleIds.length === 0) { + return '' + } + + const moduleList = emptyInlineScriptModuleIds.map((id) => `'${id}'`).join(', ') + return ` Warning: inline scripts ${moduleList} are empty for now. Use set_module_code to fill them in.` +} + function validateFlowModules(rawModules: unknown): FlowModule[] { if (!Array.isArray(rawModules)) { throw new Error('Flow modules must be an array') @@ -492,7 +504,7 @@ export interface FlowAIChatHelpers { // ai chat tools setCode: (id: string, code: string) => Promise - setFlowJson: (update: FlowJsonUpdate) => Promise + setFlowJson: (update: FlowJsonUpdate) => Promise getFlowInputsSchema: () => Promise> /** Update exprsToSet store for InputTransformForm components (only if module is selected) */ updateExprsToSet: (id: string, inputTransforms: Record) => void @@ -1018,12 +1030,13 @@ export const flowTools: Tool[] = [ helpers.inlineScriptSession.set(moduleId, content) } - await helpers.setFlowJson({ + const updateResult = await helpers.setFlowJson({ modules: parsedFlow.modules, schema: parsedFlow.schema, preprocessorModule: parsedFlow.preprocessor_module, failureModule: parsedFlow.failure_module }) + const warning = formatEmptyInlineScriptWarning(updateResult) const selectedModule = findModuleInEditableFlow(parsedFlow, selectedId) if ( @@ -1039,7 +1052,7 @@ export const flowTools: Tool[] = [ result: 'Success' }) - return `Flow JSON updated` + return `Flow JSON updated.${warning}` } }, { @@ -1058,7 +1071,8 @@ export const flowTools: Tool[] = [ content: parsedModule === null ? 'Removing preprocessor module...' : 'Setting preprocessor module...' }) - await helpers.setFlowJson({ preprocessorModule: parsedModule }) + const updateResult = await helpers.setFlowJson({ preprocessorModule: parsedModule }) + const warning = formatEmptyInlineScriptWarning(updateResult) if ( parsedModule && @@ -1071,12 +1085,14 @@ export const flowTools: Tool[] = [ toolCallbacks.setToolStatus(toolId, { content: - parsedModule === null ? 'Preprocessor module removed' : 'Preprocessor module updated', + parsedModule === null + ? 'Preprocessor module removed' + : 'Preprocessor module updated', result: 'Success' }) return parsedModule === null ? 'Preprocessor module removed' - : 'Preprocessor module updated successfully.' + : `Preprocessor module updated successfully.${warning}` } }, { @@ -1094,7 +1110,8 @@ export const flowTools: Tool[] = [ toolCallbacks.setToolStatus(toolId, { content: parsedModule === null ? 'Removing failure module...' : 'Setting failure module...' }) - await helpers.setFlowJson({ failureModule: parsedModule }) + const updateResult = await helpers.setFlowJson({ failureModule: parsedModule }) + const warning = formatEmptyInlineScriptWarning(updateResult) if ( parsedModule && @@ -1106,12 +1123,13 @@ export const flowTools: Tool[] = [ } toolCallbacks.setToolStatus(toolId, { - content: parsedModule === null ? 'Failure module removed' : 'Failure module updated', + content: + parsedModule === null ? 'Failure module removed' : 'Failure module updated', result: 'Success' }) return parsedModule === null ? 'Failure module removed' - : 'Failure module updated successfully.' + : `Failure module updated successfully.${warning}` } }, { @@ -1181,7 +1199,7 @@ export const flowTools: Tool[] = [ toolCallbacks.setToolStatus(toolId, { content: `Setting flow...` }) - await helpers.setFlowJson({ + const updateResult = await helpers.setFlowJson({ ...(parsedModules !== undefined ? { modules: parsedModules } : {}), ...(parsedSchema !== undefined ? { schema: parsedSchema } : {}), ...(parsedPreprocessorModule !== undefined @@ -1189,6 +1207,7 @@ export const flowTools: Tool[] = [ : {}), ...(parsedFailureModule !== undefined ? { failureModule: parsedFailureModule } : {}) }) + const warning = formatEmptyInlineScriptWarning(updateResult) // Update exprsToSet if the selected module has input_transforms if ( @@ -1218,7 +1237,7 @@ export const flowTools: Tool[] = [ content: `Flow updated`, result: 'Success' }) - return `Flow updated` + return `Flow updated.${warning}` } }, { @@ -1490,6 +1509,10 @@ To reduce token usage, rawscript content in the flow you receive is replaced wit **To inspect existing code:** - Use \`inspect_inline_script\` tool to view the current code: \`inspect_inline_script({ moduleId: "step_a" })\` +**If a flow update tool warns that inline scripts are empty:** +- The module structure was created successfully, but the code is still empty +- Immediately call \`set_module_code\` for each warned module ID + ### Writing Code for Modules **IMPORTANT: Before writing any code for a rawscript module, you MUST call the \`get_instructions_for_code_generation\` tool with the target language.** This tool provides essential language-specific instructions. diff --git a/frontend/src/lib/components/copilot/chat/flow/helperUtils.test.ts b/frontend/src/lib/components/copilot/chat/flow/helperUtils.test.ts new file mode 100644 index 0000000000..3d69821edf --- /dev/null +++ b/frontend/src/lib/components/copilot/chat/flow/helperUtils.test.ts @@ -0,0 +1,70 @@ +import { describe, expect, it, vi } from 'vitest' +import type { FlowModule } from '$lib/gen' +import { applyFlowJsonUpdate } from './helperUtils' +import { createInlineScriptSession } from './inlineScriptsUtils' + +vi.mock('../shared', () => ({ + SPECIAL_MODULE_IDS: { + PREPROCESSOR: 'preprocessor', + FAILURE: 'failure' + } +})) + +vi.mock('$lib/components/flows/previousResults', () => ({ + dfs: () => [] +})) + +function makeRawScriptModule(id: string, content: string): FlowModule { + return { + id, + summary: id, + value: { + type: 'rawscript', + language: 'bun', + content, + input_transforms: {} + } + } as FlowModule +} + +describe('applyFlowJsonUpdate', () => { + it('accepts new self-referenced inline scripts and initializes them as empty', () => { + const flow = { + value: { + modules: [makeRawScriptModule('process_data', 'existing code')] + } + } + const inlineScriptSession = createInlineScriptSession() + inlineScriptSession.set('process_data', 'existing code') + + const result = applyFlowJsonUpdate(flow as any, inlineScriptSession, { + modules: [ + makeRawScriptModule('process_data', 'inline_script.process_data'), + makeRawScriptModule('validate_data', 'inline_script.validate_data') + ] + }) + const [processDataModule, validateDataModule] = flow.value.modules as Array + + expect(result.emptyInlineScriptModuleIds).toEqual(['validate_data']) + expect(inlineScriptSession.get('validate_data')).toBe('') + expect(processDataModule?.value.type).toBe('rawscript') + expect(processDataModule?.value.content).toBe('existing code') + expect(validateDataModule?.value.type).toBe('rawscript') + expect(validateDataModule?.value.content).toBe('') + }) + + it('still rejects unresolved inline script references that do not match the module id', () => { + const flow = { + value: { + modules: [] + } + } + const inlineScriptSession = createInlineScriptSession() + + expect(() => + applyFlowJsonUpdate(flow as any, inlineScriptSession, { + modules: [makeRawScriptModule('validate_data', 'inline_script.other_module')] + }) + ).toThrow('Unresolved inline script references: other_module') + }) +}) diff --git a/frontend/src/lib/components/copilot/chat/flow/helperUtils.ts b/frontend/src/lib/components/copilot/chat/flow/helperUtils.ts index 503fad6bc2..1c3b272130 100644 --- a/frontend/src/lib/components/copilot/chat/flow/helperUtils.ts +++ b/frontend/src/lib/components/copilot/chat/flow/helperUtils.ts @@ -14,6 +14,10 @@ export interface FlowJsonUpdate { failureModule?: FlowModule | null } +export interface FlowJsonUpdateResult { + emptyInlineScriptModuleIds: string[] +} + export function getFlowModuleById(flow: FlowLike | undefined, id: string): FlowModule | undefined { if (!flow) { return undefined @@ -60,8 +64,13 @@ export function applyFlowJsonUpdate( flow: FlowLike, inlineScriptSession: InlineScriptSession, { modules, schema, preprocessorModule, failureModule }: FlowJsonUpdate -): void { +): FlowJsonUpdateResult { + const emptyInlineScriptModuleIds = new Set() + if (modules !== undefined) { + seedMissingInlineScripts(modules, inlineScriptSession).forEach((id) => + emptyInlineScriptModuleIds.add(id) + ) flow.value.modules = restoreFlowModules(modules, inlineScriptSession) } @@ -73,12 +82,26 @@ export function applyFlowJsonUpdate( flow.value.preprocessor_module = preprocessorModule === null ? undefined - : restoreFlowModule(preprocessorModule, inlineScriptSession) + : restoreFlowModuleWithSeededInlineScripts( + preprocessorModule, + inlineScriptSession, + emptyInlineScriptModuleIds + ) } if (failureModule !== undefined) { flow.value.failure_module = - failureModule === null ? undefined : restoreFlowModule(failureModule, inlineScriptSession) + failureModule === null + ? undefined + : restoreFlowModuleWithSeededInlineScripts( + failureModule, + inlineScriptSession, + emptyInlineScriptModuleIds + ) + } + + return { + emptyInlineScriptModuleIds: Array.from(emptyInlineScriptModuleIds) } } @@ -100,6 +123,17 @@ function restoreFlowModule( return restoredModule } +function restoreFlowModuleWithSeededInlineScripts( + module: FlowModule, + inlineScriptSession: InlineScriptSession, + emptyInlineScriptModuleIds: Set +): FlowModule { + seedMissingInlineScripts([module], inlineScriptSession).forEach((id) => + emptyInlineScriptModuleIds.add(id) + ) + return restoreFlowModule(module, inlineScriptSession) +} + function assertResolvedInlineScripts( modules: FlowModule[], inlineScriptSession: InlineScriptSession @@ -109,3 +143,67 @@ function assertResolvedInlineScripts( throw new Error(`Unresolved inline script references: ${unresolvedRefs.join(', ')}`) } } + +function seedMissingInlineScripts( + modules: FlowModule[], + inlineScriptSession: InlineScriptSession +): string[] { + const emptyInlineScriptModuleIds: string[] = [] + + function ensureEmptyInlineScript(moduleId: string, inlineScriptRefId: string) { + if (moduleId !== inlineScriptRefId || inlineScriptSession.has(moduleId)) { + return + } + + inlineScriptSession.set(moduleId, '') + emptyInlineScriptModuleIds.push(moduleId) + } + + function visitModule(module: FlowModule) { + if (module.value.type === 'rawscript' && module.value.content) { + const match = module.value.content.match(/^inline_script\.(.+)$/) + if (match) { + ensureEmptyInlineScript(module.id, match[1]) + } + return + } + + if (module.value.type === 'forloopflow' || module.value.type === 'whileloopflow') { + module.value.modules?.forEach(visitModule) + return + } + + if (module.value.type === 'branchone') { + module.value.branches?.forEach((branch) => branch.modules?.forEach(visitModule)) + module.value.default?.forEach(visitModule) + return + } + + if (module.value.type === 'branchall') { + module.value.branches?.forEach((branch) => branch.modules?.forEach(visitModule)) + return + } + + if (module.value.type === 'aiagent') { + for (const tool of module.value.tools ?? []) { + if ( + tool.value && + 'tool_type' in tool.value && + tool.value.tool_type === 'flowmodule' && + 'type' in tool.value && + tool.value.type === 'rawscript' && + 'content' in tool.value && + tool.value.content + ) { + const match = (tool.value.content as string).match(/^inline_script\.(.+)$/) + if (match) { + ensureEmptyInlineScript(tool.id, match[1]) + } + } + } + } + } + + modules.forEach(visitModule) + return emptyInlineScriptModuleIds +}