Skip to content

Commit 073030b

Browse files
authored
improvement(serializer): filter out advanced mode fields when executing in basic mode, persist the values but don't include them in serialized block for execution (#1018)
* improvement(serializer): filter out advanced mode fields when executing in basic mode, persist the values but don't include them in serialized block for execution * fix serializer exclusion logic
1 parent 871f4e8 commit 073030b

File tree

4 files changed

+279
-35
lines changed

4 files changed

+279
-35
lines changed

apps/sim/serializer/index.test.ts

Lines changed: 246 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -161,6 +161,56 @@ vi.mock('@/blocks', () => ({
161161
subreddit: { type: 'string' },
162162
},
163163
},
164+
// Mock block with both basic and advanced mode fields for testing
165+
slack: {
166+
name: 'Slack',
167+
description: 'Send messages to Slack',
168+
category: 'tools',
169+
bgColor: '#611f69',
170+
tools: {
171+
access: ['slack_send_message'],
172+
config: {
173+
tool: () => 'slack_send_message',
174+
},
175+
},
176+
subBlocks: [
177+
{ id: 'channel', type: 'dropdown', title: 'Channel', mode: 'basic' },
178+
{ id: 'manualChannel', type: 'short-input', title: 'Channel ID', mode: 'advanced' },
179+
{ id: 'text', type: 'long-input', title: 'Message' }, // mode: 'both' (default)
180+
{ id: 'username', type: 'short-input', title: 'Username', mode: 'both' },
181+
],
182+
inputs: {
183+
channel: { type: 'string' },
184+
manualChannel: { type: 'string' },
185+
text: { type: 'string' },
186+
username: { type: 'string' },
187+
},
188+
},
189+
// Mock agent block with memories for testing
190+
agentWithMemories: {
191+
name: 'Agent with Memories',
192+
description: 'AI Agent with memory support',
193+
category: 'ai',
194+
bgColor: '#2196F3',
195+
tools: {
196+
access: ['anthropic_chat'],
197+
config: {
198+
tool: () => 'anthropic_chat',
199+
},
200+
},
201+
subBlocks: [
202+
{ id: 'systemPrompt', type: 'long-input', title: 'System Prompt' }, // mode: 'both' (default)
203+
{ id: 'userPrompt', type: 'long-input', title: 'User Prompt' }, // mode: 'both' (default)
204+
{ id: 'memories', type: 'short-input', title: 'Memories', mode: 'advanced' },
205+
{ id: 'model', type: 'dropdown', title: 'Model' }, // mode: 'both' (default)
206+
],
207+
inputs: {
208+
systemPrompt: { type: 'string' },
209+
userPrompt: { type: 'string' },
210+
memories: { type: 'array' },
211+
model: { type: 'string' },
212+
},
213+
},
164214
}
165215

166216
return mockConfigs[type] || null
@@ -716,4 +766,200 @@ describe('Serializer', () => {
716766
}).toThrow('Test Reddit Block is missing required fields: Reddit Account')
717767
})
718768
})
769+
770+
/**
771+
* Advanced mode field filtering tests
772+
*/
773+
describe('advanced mode field filtering', () => {
774+
it.concurrent('should include all fields when block is in advanced mode', () => {
775+
const serializer = new Serializer()
776+
777+
const advancedModeBlock: any = {
778+
id: 'slack-1',
779+
type: 'slack',
780+
name: 'Test Slack Block',
781+
position: { x: 0, y: 0 },
782+
advancedMode: true, // Advanced mode enabled
783+
subBlocks: {
784+
channel: { value: 'general' }, // basic mode field
785+
manualChannel: { value: 'C1234567890' }, // advanced mode field
786+
text: { value: 'Hello world' }, // both mode field
787+
username: { value: 'bot' }, // both mode field
788+
},
789+
outputs: {},
790+
enabled: true,
791+
}
792+
793+
const serialized = serializer.serializeWorkflow({ 'slack-1': advancedModeBlock }, [], {})
794+
795+
const slackBlock = serialized.blocks.find((b) => b.id === 'slack-1')
796+
expect(slackBlock).toBeDefined()
797+
798+
// In advanced mode, should include ALL fields (basic, advanced, and both)
799+
expect(slackBlock?.config.params.channel).toBe('general') // basic mode field included
800+
expect(slackBlock?.config.params.manualChannel).toBe('C1234567890') // advanced mode field included
801+
expect(slackBlock?.config.params.text).toBe('Hello world') // both mode field included
802+
expect(slackBlock?.config.params.username).toBe('bot') // both mode field included
803+
})
804+
805+
it.concurrent('should exclude advanced-only fields when block is in basic mode', () => {
806+
const serializer = new Serializer()
807+
808+
const basicModeBlock: any = {
809+
id: 'slack-1',
810+
type: 'slack',
811+
name: 'Test Slack Block',
812+
position: { x: 0, y: 0 },
813+
advancedMode: false, // Basic mode enabled
814+
subBlocks: {
815+
channel: { value: 'general' }, // basic mode field
816+
manualChannel: { value: 'C1234567890' }, // advanced mode field
817+
text: { value: 'Hello world' }, // both mode field
818+
username: { value: 'bot' }, // both mode field
819+
},
820+
outputs: {},
821+
enabled: true,
822+
}
823+
824+
const serialized = serializer.serializeWorkflow({ 'slack-1': basicModeBlock }, [], {})
825+
826+
const slackBlock = serialized.blocks.find((b) => b.id === 'slack-1')
827+
expect(slackBlock).toBeDefined()
828+
829+
// In basic mode, should include basic-only fields and exclude advanced-only fields
830+
expect(slackBlock?.config.params.channel).toBe('general') // basic mode field included
831+
expect(slackBlock?.config.params.manualChannel).toBeUndefined() // advanced mode field excluded
832+
expect(slackBlock?.config.params.text).toBe('Hello world') // both mode field included
833+
expect(slackBlock?.config.params.username).toBe('bot') // both mode field included
834+
})
835+
836+
it.concurrent(
837+
'should exclude advanced-only fields when advancedMode is undefined (defaults to basic mode)',
838+
() => {
839+
const serializer = new Serializer()
840+
841+
const defaultModeBlock: any = {
842+
id: 'slack-1',
843+
type: 'slack',
844+
name: 'Test Slack Block',
845+
position: { x: 0, y: 0 },
846+
// advancedMode: undefined (defaults to false)
847+
subBlocks: {
848+
channel: { value: 'general' }, // basic mode field
849+
manualChannel: { value: 'C1234567890' }, // advanced mode field
850+
text: { value: 'Hello world' }, // both mode field
851+
username: { value: 'bot' }, // both mode field
852+
},
853+
outputs: {},
854+
enabled: true,
855+
}
856+
857+
const serialized = serializer.serializeWorkflow({ 'slack-1': defaultModeBlock }, [], {})
858+
859+
const slackBlock = serialized.blocks.find((b) => b.id === 'slack-1')
860+
expect(slackBlock).toBeDefined()
861+
862+
// Should default to basic mode behavior (include basic + both, exclude advanced)
863+
expect(slackBlock?.config.params.channel).toBe('general') // basic mode field included
864+
expect(slackBlock?.config.params.manualChannel).toBeUndefined() // advanced mode field excluded
865+
expect(slackBlock?.config.params.text).toBe('Hello world') // both mode field included
866+
expect(slackBlock?.config.params.username).toBe('bot') // both mode field included
867+
}
868+
)
869+
870+
it.concurrent('should filter memories field correctly in agent blocks', () => {
871+
const serializer = new Serializer()
872+
873+
const agentInBasicMode: any = {
874+
id: 'agent-1',
875+
type: 'agentWithMemories',
876+
name: 'Test Agent',
877+
position: { x: 0, y: 0 },
878+
advancedMode: false, // Basic mode
879+
subBlocks: {
880+
systemPrompt: { value: 'You are helpful' }, // both mode field
881+
userPrompt: { value: 'Hello' }, // both mode field
882+
memories: { value: [{ role: 'user', content: 'My name is John' }] }, // advanced mode field
883+
model: { value: 'claude-3-sonnet' }, // both mode field
884+
},
885+
outputs: {},
886+
enabled: true,
887+
}
888+
889+
const serialized = serializer.serializeWorkflow({ 'agent-1': agentInBasicMode }, [], {})
890+
891+
const agentBlock = serialized.blocks.find((b) => b.id === 'agent-1')
892+
expect(agentBlock).toBeDefined()
893+
894+
// In basic mode, memories should be excluded
895+
expect(agentBlock?.config.params.systemPrompt).toBe('You are helpful')
896+
expect(agentBlock?.config.params.userPrompt).toBe('Hello')
897+
expect(agentBlock?.config.params.memories).toBeUndefined() // Excluded in basic mode
898+
expect(agentBlock?.config.params.model).toBe('claude-3-sonnet')
899+
})
900+
901+
it.concurrent('should include memories field when agent is in advanced mode', () => {
902+
const serializer = new Serializer()
903+
904+
const agentInAdvancedMode: any = {
905+
id: 'agent-1',
906+
type: 'agentWithMemories',
907+
name: 'Test Agent',
908+
position: { x: 0, y: 0 },
909+
advancedMode: true, // Advanced mode
910+
subBlocks: {
911+
systemPrompt: { value: 'You are helpful' }, // both mode field
912+
userPrompt: { value: 'Hello' }, // both mode field
913+
memories: { value: [{ role: 'user', content: 'My name is John' }] }, // advanced mode field
914+
model: { value: 'claude-3-sonnet' }, // both mode field
915+
},
916+
outputs: {},
917+
enabled: true,
918+
}
919+
920+
const serialized = serializer.serializeWorkflow({ 'agent-1': agentInAdvancedMode }, [], {})
921+
922+
const agentBlock = serialized.blocks.find((b) => b.id === 'agent-1')
923+
expect(agentBlock).toBeDefined()
924+
925+
// In advanced mode, memories should be included
926+
expect(agentBlock?.config.params.systemPrompt).toBe('You are helpful')
927+
expect(agentBlock?.config.params.userPrompt).toBe('Hello')
928+
expect(agentBlock?.config.params.memories).toEqual([
929+
{ role: 'user', content: 'My name is John' },
930+
]) // Included in advanced mode
931+
expect(agentBlock?.config.params.model).toBe('claude-3-sonnet')
932+
})
933+
934+
it.concurrent('should handle blocks with no matching subblock config gracefully', () => {
935+
const serializer = new Serializer()
936+
937+
const blockWithUnknownField: any = {
938+
id: 'slack-1',
939+
type: 'slack',
940+
name: 'Test Slack Block',
941+
position: { x: 0, y: 0 },
942+
advancedMode: false, // Basic mode
943+
subBlocks: {
944+
channel: { value: 'general' }, // known field
945+
unknownField: { value: 'someValue' }, // field not in config
946+
text: { value: 'Hello world' }, // known field
947+
},
948+
outputs: {},
949+
enabled: true,
950+
}
951+
952+
const serialized = serializer.serializeWorkflow({ 'slack-1': blockWithUnknownField }, [], {})
953+
954+
const slackBlock = serialized.blocks.find((b) => b.id === 'slack-1')
955+
expect(slackBlock).toBeDefined()
956+
957+
// Known fields should be processed according to mode rules
958+
expect(slackBlock?.config.params.channel).toBe('general')
959+
expect(slackBlock?.config.params.text).toBe('Hello world')
960+
961+
// Unknown fields are filtered out (no subblock config found, so shouldIncludeField is not called)
962+
expect(slackBlock?.config.params.unknownField).toBeUndefined()
963+
})
964+
})
719965
})

apps/sim/serializer/index.ts

Lines changed: 27 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,26 @@
11
import type { Edge } from 'reactflow'
22
import { createLogger } from '@/lib/logs/console/logger'
33
import { getBlock } from '@/blocks'
4+
import type { SubBlockConfig } from '@/blocks/types'
45
import type { SerializedBlock, SerializedWorkflow } from '@/serializer/types'
56
import type { BlockState, Loop, Parallel } from '@/stores/workflows/workflow/types'
67
import { getTool } from '@/tools/utils'
78

89
const logger = createLogger('Serializer')
910

11+
/**
12+
* Helper function to check if a subblock should be included in serialization based on current mode
13+
*/
14+
function shouldIncludeField(subBlockConfig: SubBlockConfig, isAdvancedMode: boolean): boolean {
15+
const fieldMode = subBlockConfig.mode
16+
17+
if (fieldMode === 'advanced' && !isAdvancedMode) {
18+
return false // Skip advanced-only fields when in basic mode
19+
}
20+
21+
return true
22+
}
23+
1024
export class Serializer {
1125
serializeWorkflow(
1226
blocks: Record<string, BlockState>,
@@ -198,16 +212,26 @@ export class Serializer {
198212
}
199213

200214
const params: Record<string, any> = {}
215+
const isAdvancedMode = block.advancedMode ?? false
201216

202-
// First collect all current values from subBlocks
217+
// First collect all current values from subBlocks, filtering by mode
203218
Object.entries(block.subBlocks).forEach(([id, subBlock]) => {
204-
params[id] = subBlock.value
219+
// Find the corresponding subblock config to check its mode
220+
const subBlockConfig = blockConfig.subBlocks.find((config) => config.id === id)
221+
222+
if (subBlockConfig && shouldIncludeField(subBlockConfig, isAdvancedMode)) {
223+
params[id] = subBlock.value
224+
}
205225
})
206226

207227
// Then check for any subBlocks with default values
208228
blockConfig.subBlocks.forEach((subBlockConfig) => {
209229
const id = subBlockConfig.id
210-
if (params[id] === null && subBlockConfig.value) {
230+
if (
231+
params[id] === null &&
232+
subBlockConfig.value &&
233+
shouldIncludeField(subBlockConfig, isAdvancedMode)
234+
) {
211235
// If the value is null and there's a default value function, use it
212236
params[id] = subBlockConfig.value(params)
213237
}

apps/sim/stores/workflows/workflow/store.test.ts

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -355,7 +355,7 @@ describe('workflow store', () => {
355355
)
356356
})
357357

358-
it('should clear memories when switching from advanced to basic mode', () => {
358+
it('should preserve memories when switching from advanced to basic mode', () => {
359359
const { addBlock, toggleBlockAdvancedMode } = useWorkflowStore.getState()
360360
const { setState: setSubBlockState } = useSubBlockStore
361361

@@ -387,15 +387,18 @@ describe('workflow store', () => {
387387
// Toggle back to basic mode
388388
toggleBlockAdvancedMode('agent1')
389389

390-
// Check that prompts are preserved but memories are cleared
390+
// Check that prompts and memories are all preserved
391391
const subBlockState = useSubBlockStore.getState()
392392
expect(subBlockState.workflowValues['test-workflow'].agent1.systemPrompt).toBe(
393393
'You are a helpful assistant'
394394
)
395395
expect(subBlockState.workflowValues['test-workflow'].agent1.userPrompt).toBe(
396396
'What did we discuss?'
397397
)
398-
expect(subBlockState.workflowValues['test-workflow'].agent1.memories).toBeNull()
398+
expect(subBlockState.workflowValues['test-workflow'].agent1.memories).toEqual([
399+
{ role: 'user', content: 'My name is John' },
400+
{ role: 'assistant', content: 'Nice to meet you, John!' },
401+
])
399402
})
400403

401404
it('should handle mode switching when no subblock values exist', () => {

apps/sim/stores/workflows/workflow/store.ts

Lines changed: 0 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -974,35 +974,6 @@ export const useWorkflowStore = create<WorkflowStoreWithHistory>()(
974974

975975
set(newState)
976976

977-
// Clear the appropriate subblock values based on the new mode
978-
const activeWorkflowId = useWorkflowRegistry.getState().activeWorkflowId
979-
if (activeWorkflowId) {
980-
const subBlockStore = useSubBlockStore.getState()
981-
const blockValues = subBlockStore.workflowValues[activeWorkflowId]?.[id] || {}
982-
const updatedValues = { ...blockValues }
983-
984-
if (!block.advancedMode) {
985-
// Switching TO advanced mode
986-
// Preserve systemPrompt and userPrompt, memories starts empty
987-
// No need to clear anything since advanced mode has all fields
988-
} else {
989-
// Switching TO basic mode
990-
// Preserve systemPrompt and userPrompt, but clear memories
991-
updatedValues.memories = null
992-
}
993-
994-
// Update subblock store with the cleared values
995-
useSubBlockStore.setState({
996-
workflowValues: {
997-
...subBlockStore.workflowValues,
998-
[activeWorkflowId]: {
999-
...subBlockStore.workflowValues[activeWorkflowId],
1000-
[id]: updatedValues,
1001-
},
1002-
},
1003-
})
1004-
}
1005-
1006977
get().triggerUpdate()
1007978
// Note: Socket.IO handles real-time sync automatically
1008979
},

0 commit comments

Comments
 (0)