Skip to content

Commit 963f1d5

Browse files
authored
Merge branch 'staging' into feat/copilot-subagents
2 parents 5e9d8e1 + 92fabe7 commit 963f1d5

File tree

13 files changed

+60
-30
lines changed

13 files changed

+60
-30
lines changed

apps/docs/content/docs/en/blocks/router.mdx

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,6 @@
22
title: Router
33
---
44

5-
import { Callout } from 'fumadocs-ui/components/callout'
65
import { Tab, Tabs } from 'fumadocs-ui/components/tabs'
76
import { Image } from '@/components/ui/image'
87

@@ -102,11 +101,18 @@ Input (Lead) → Router
102101
└── [Self-serve] → Workflow (Automated Onboarding)
103102
```
104103

104+
## Error Handling
105+
106+
When the Router cannot determine an appropriate route for the given context, it will route to the **error path** instead of arbitrarily selecting a route. This happens when:
107+
108+
- The context doesn't clearly match any of the defined route descriptions
109+
- The AI determines that none of the available routes are appropriate
110+
105111
## Best Practices
106112

107113
- **Write clear route descriptions**: Each route description should clearly explain when that route should be selected. Be specific about the criteria.
108114
- **Make routes mutually exclusive**: When possible, ensure route descriptions don't overlap to prevent ambiguous routing decisions.
109-
- **Include an error/fallback route**: Add a catch-all route for unexpected inputs that don't match other routes.
115+
- **Connect an error path**: Handle cases where no route matches by connecting an error handler for graceful fallback behavior.
110116
- **Use descriptive route titles**: Route titles appear in the workflow canvas, so make them meaningful for readability.
111117
- **Test with diverse inputs**: Ensure the Router handles various input types, edge cases, and unexpected content.
112118
- **Monitor routing performance**: Review routing decisions regularly and refine route descriptions based on actual usage patterns.

apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/condition-input/condition-input.tsx

Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -654,17 +654,20 @@ export function ConditionInput({
654654
}
655655

656656
const removeBlock = (id: string) => {
657-
if (isPreview || disabled || conditionalBlocks.length <= 2) return
657+
if (isPreview || disabled) return
658+
// Condition mode requires at least 2 blocks (if/else), router mode requires at least 1
659+
const minBlocks = isRouterMode ? 1 : 2
660+
if (conditionalBlocks.length <= minBlocks) return
658661

659662
// Remove any associated edges before removing the block
663+
const handlePrefix = isRouterMode ? `router-${id}` : `condition-${id}`
660664
const edgeIdsToRemove = edges
661-
.filter((edge) => edge.sourceHandle?.startsWith(`condition-${id}`))
665+
.filter((edge) => edge.sourceHandle?.startsWith(handlePrefix))
662666
.map((edge) => edge.id)
663667
if (edgeIdsToRemove.length > 0) {
664668
batchRemoveEdges(edgeIdsToRemove)
665669
}
666670

667-
if (conditionalBlocks.length === 1) return
668671
shouldPersistRef.current = true
669672
setConditionalBlocks((blocks) => updateBlockTitles(blocks.filter((block) => block.id !== id)))
670673

@@ -816,7 +819,9 @@ export function ConditionInput({
816819
<Button
817820
variant='ghost'
818821
onClick={() => removeBlock(block.id)}
819-
disabled={isPreview || disabled || conditionalBlocks.length === 1}
822+
disabled={
823+
isPreview || disabled || conditionalBlocks.length <= (isRouterMode ? 1 : 2)
824+
}
820825
className='h-auto p-0 text-[var(--text-error)] hover:text-[var(--text-error)]'
821826
>
822827
<Trash className='h-[14px] w-[14px]' />

apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/workflow-block/workflow-block.tsx

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -867,7 +867,8 @@ export const WorkflowBlock = memo(function WorkflowBlock({
867867
return parsed.map((item: unknown, index: number) => {
868868
const routeItem = item as { id?: string; value?: string }
869869
return {
870-
id: routeItem?.id ?? `${id}-route-${index}`,
870+
// Use stable ID format that matches ConditionInput's generateStableId
871+
id: routeItem?.id ?? `${id}-route${index + 1}`,
871872
value: routeItem?.value ?? '',
872873
}
873874
})
@@ -877,7 +878,8 @@ export const WorkflowBlock = memo(function WorkflowBlock({
877878
logger.warn('Failed to parse router routes value', { error, blockId: id })
878879
}
879880

880-
return [{ id: `${id}-route-route1`, value: '' }]
881+
// Fallback must match ConditionInput's default: generateStableId(blockId, 'route1') = `${blockId}-route1`
882+
return [{ id: `${id}-route1`, value: '' }]
881883
}, [type, subBlockState, id])
882884

883885
/**

apps/sim/app/workspace/[workspaceId]/w/[workflowId]/workflow.tsx

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -987,6 +987,14 @@ const WorkflowContent = React.memo(() => {
987987
const handleId = conditionHandles[0].getAttribute('data-handleid')
988988
if (handleId) return handleId
989989
}
990+
} else if (block.type === 'router_v2') {
991+
const routerHandles = document.querySelectorAll(
992+
`[data-nodeid^="${block.id}"][data-handleid^="router-"]`
993+
)
994+
if (routerHandles.length > 0) {
995+
const handleId = routerHandles[0].getAttribute('data-handleid')
996+
if (handleId) return handleId
997+
}
990998
} else if (block.type === 'loop') {
991999
return 'loop-end-source'
9921000
} else if (block.type === 'parallel') {

apps/sim/blocks/blocks/router.ts

Lines changed: 12 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -115,25 +115,26 @@ Description: ${route.value || 'No description provided'}
115115
)
116116
.join('\n')
117117

118-
return `You are an intelligent routing agent. Your task is to analyze the provided context and select the most appropriate route from the available options.
118+
return `You are a DETERMINISTIC routing agent. You MUST select exactly ONE option.
119119
120120
Available Routes:
121121
${routesInfo}
122122
123-
Context to analyze:
123+
Context to route:
124124
${context}
125125
126-
Instructions:
127-
1. Carefully analyze the context against each route's description
128-
2. Select the route that best matches the context's intent and requirements
129-
3. Consider the semantic meaning, not just keyword matching
130-
4. If multiple routes could match, choose the most specific one
126+
ROUTING RULES:
127+
1. ALWAYS prefer selecting a route over NO_MATCH
128+
2. Pick the route whose description BEST matches the context, even if it's not a perfect match
129+
3. If the context is even partially related to a route's description, select that route
130+
4. ONLY output NO_MATCH if the context is completely unrelated to ALL route descriptions
131131
132-
Response Format:
133-
Return ONLY the route ID as a single string, no punctuation, no explanation.
134-
Example: "route-abc123"
132+
OUTPUT FORMAT:
133+
- Output EXACTLY one route ID (copied exactly as shown above) OR "NO_MATCH"
134+
- No explanation, no punctuation, no additional text
135+
- Just the route ID or NO_MATCH
135136
136-
Remember: Your response must be ONLY the route ID - no additional text, formatting, or explanation.`
137+
Your response:`
137138
}
138139

139140
/**

apps/sim/executor/handlers/router/router-handler.ts

Lines changed: 13 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -278,14 +278,24 @@ export class RouterBlockHandler implements BlockHandler {
278278
const result = await response.json()
279279

280280
const chosenRouteId = result.content.trim()
281+
282+
if (chosenRouteId === 'NO_MATCH' || chosenRouteId.toUpperCase() === 'NO_MATCH') {
283+
logger.info('Router determined no route matches the context, routing to error path')
284+
throw new Error('Router could not determine a matching route for the given context')
285+
}
286+
281287
const chosenRoute = routes.find((r) => r.id === chosenRouteId)
282288

289+
// Throw error if LLM returns invalid route ID - this routes through error path
283290
if (!chosenRoute) {
291+
const availableRoutes = routes.map((r) => ({ id: r.id, title: r.title }))
284292
logger.error(
285-
`Invalid routing decision. Response content: "${result.content}", available routes:`,
286-
routes.map((r) => ({ id: r.id, title: r.title }))
293+
`Invalid routing decision. Response content: "${result.content}". Available routes:`,
294+
availableRoutes
295+
)
296+
throw new Error(
297+
`Router could not determine a valid route. LLM response: "${result.content}". Available route IDs: ${routes.map((r) => r.id).join(', ')}`
287298
)
288-
throw new Error(`Invalid routing decision: ${chosenRouteId}`)
289299
}
290300

291301
// Find the target block connected to this route's handle

apps/sim/lib/copilot/process-contents.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -369,7 +369,7 @@ async function processBlockMetadata(
369369
if (userId) {
370370
const permissionConfig = await getUserPermissionConfig(userId)
371371
const allowedIntegrations = permissionConfig?.allowedIntegrations
372-
if (allowedIntegrations !== null && !allowedIntegrations?.includes(blockId)) {
372+
if (allowedIntegrations != null && !allowedIntegrations.includes(blockId)) {
373373
logger.debug('Block not allowed by permission group', { blockId, userId })
374374
return null
375375
}

apps/sim/lib/copilot/tools/server/blocks/get-block-config.ts

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -359,8 +359,7 @@ export const getBlockConfigServerTool: BaseServerTool<
359359
const permissionConfig = context?.userId ? await getUserPermissionConfig(context.userId) : null
360360
const allowedIntegrations = permissionConfig?.allowedIntegrations
361361

362-
// Only restrict if allowedIntegrations is explicitly set (not null/undefined)
363-
if (allowedIntegrations && !allowedIntegrations.includes(blockType)) {
362+
if (allowedIntegrations != null && !allowedIntegrations.includes(blockType)) {
364363
throw new Error(`Block "${blockType}" is not available`)
365364
}
366365

apps/sim/lib/copilot/tools/server/blocks/get-block-options.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@ export const getBlockOptionsServerTool: BaseServerTool<
2424
const permissionConfig = context?.userId ? await getUserPermissionConfig(context.userId) : null
2525
const allowedIntegrations = permissionConfig?.allowedIntegrations
2626

27-
if (allowedIntegrations !== null && !allowedIntegrations?.includes(blockId)) {
27+
if (allowedIntegrations != null && !allowedIntegrations.includes(blockId)) {
2828
throw new Error(`Block "${blockId}" is not available`)
2929
}
3030

apps/sim/lib/copilot/tools/server/blocks/get-blocks-and-tools.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,7 @@ export const getBlocksAndToolsServerTool: BaseServerTool<
3131
Object.entries(blockRegistry)
3232
.filter(([blockType, blockConfig]: [string, BlockConfig]) => {
3333
if (blockConfig.hideFromToolbar) return false
34-
if (allowedIntegrations !== null && !allowedIntegrations?.includes(blockType)) return false
34+
if (allowedIntegrations != null && !allowedIntegrations.includes(blockType)) return false
3535
return true
3636
})
3737
.forEach(([blockType, blockConfig]: [string, BlockConfig]) => {

0 commit comments

Comments
 (0)