fix: seed empty inline flow scripts

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
centdix
2026-04-16 17:09:10 +02:00
co-authored by Claude Opus 4.5
parent 49844eb240
commit 8041cb8c18
4 changed files with 207 additions and 15 deletions
@@ -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)}`
@@ -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<void>
setFlowJson: (update: FlowJsonUpdate) => Promise<void>
setFlowJson: (update: FlowJsonUpdate) => Promise<FlowJsonUpdateResult>
getFlowInputsSchema: () => Promise<Record<string, any>>
/** Update exprsToSet store for InputTransformForm components (only if module is selected) */
updateExprsToSet: (id: string, inputTransforms: Record<string, InputTransform>) => void
@@ -1018,12 +1030,13 @@ export const flowTools: Tool<FlowAIChatHelpers>[] = [
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<FlowAIChatHelpers>[] = [
result: 'Success'
})
return `Flow JSON updated`
return `Flow JSON updated.${warning}`
}
},
{
@@ -1058,7 +1071,8 @@ export const flowTools: Tool<FlowAIChatHelpers>[] = [
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<FlowAIChatHelpers>[] = [
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<FlowAIChatHelpers>[] = [
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<FlowAIChatHelpers>[] = [
}
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<FlowAIChatHelpers>[] = [
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<FlowAIChatHelpers>[] = [
: {}),
...(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<FlowAIChatHelpers>[] = [
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.
@@ -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<FlowModule & { value: any }>
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')
})
})
@@ -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<string>()
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<string>
): 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
}