Fix condition edges

This commit is contained in:
Siddharth Ganesan
2026-02-25 17:48:33 -08:00
parent 90a12546b2
commit 57a91027de
6 changed files with 133 additions and 31 deletions

View File

@@ -232,6 +232,19 @@ export const sseHandlers: Record<string, SSEHandler> = {
return
}
if (!isToolAvailableOnSimSide(toolName)) {
return
}
// Non-interactive mode (Mothership/MCP): skip confirmation & client gates,
// execute server-side directly.
if (options.interactive === false) {
if (options.autoExecuteTools !== false) {
await executeToolAndReport(toolCallId, context, execContext, options)
}
return
}
if (requiresConfirmation) {
const decision = await waitForToolDecision(
toolCallId,
@@ -440,6 +453,15 @@ export const subAgentHandlers: Record<string, SSEHandler> = {
return
}
// Non-interactive mode (Mothership/MCP): skip confirmation & client gates,
// execute server-side directly.
if (options.interactive === false) {
if (options.autoExecuteTools !== false) {
await executeToolAndReport(toolCallId, context, execContext, options)
}
return
}
if (requiresConfirmation) {
const decision = await waitForToolDecision(
toolCallId,

View File

@@ -277,6 +277,10 @@ const SERVER_TOOLS = new Set<string>([
'make_api_request',
'knowledge_base',
'user_table',
'run_workflow',
'run_workflow_until_block',
'run_block',
'run_from_block',
])
const SIM_WORKFLOW_TOOL_HANDLERS: Record<

View File

@@ -93,6 +93,9 @@ export function createBlockFromParams(
// Normalize array subblocks with id fields (inputFormat, table rows, etc.)
if (shouldNormalizeArrayIds(key)) {
sanitizedValue = normalizeArrayWithIds(value)
if (JSON_STRING_SUBBLOCK_KEYS.has(key)) {
sanitizedValue = JSON.stringify(sanitizedValue)
}
}
// Special handling for tools - normalize and filter disallowed
@@ -127,6 +130,8 @@ export function createBlockFromParams(
type: subBlock.type,
value: null,
}
} else {
blockState.subBlocks[subBlock.id].type = subBlock.type
}
})
@@ -141,16 +146,16 @@ export function createBlockFromParams(
blockState.subBlocks.conditions = {
id: 'conditions',
type: 'condition-input',
value: [
value: JSON.stringify([
{ id: crypto.randomUUID(), title: 'If', value: '' },
{ id: crypto.randomUUID(), title: 'Else', value: '' },
],
]),
}
} else if (params.type === 'router_v2' && !blockState.subBlocks.routes?.value) {
blockState.subBlocks.routes = {
id: 'routes',
type: 'router-input',
value: [{ id: crypto.randomUUID(), title: 'Route 1', value: '' }],
value: JSON.stringify([{ id: crypto.randomUUID(), title: 'Route 1', value: '' }]),
}
}
@@ -251,6 +256,12 @@ const ARRAY_WITH_ID_SUBBLOCK_TYPES = new Set([
'routes', // router-input: Router routes with id, title, value
])
/**
* Subblock keys whose UI components expect a JSON string, not a raw array.
* After normalizeArrayWithIds returns an array, these must be re-stringified.
*/
export const JSON_STRING_SUBBLOCK_KEYS = new Set(['conditions', 'routes'])
/**
* Normalizes array subblock values by ensuring each item has a valid UUID.
* The LLM may generate arbitrary IDs like "input-desc-001" or "row-1" which need
@@ -468,9 +479,16 @@ export function addConnectionsAsEdges(
logger: ReturnType<typeof createLogger>,
skippedItems?: SkippedItem[]
): void {
Object.entries(connections).forEach(([sourceHandle, targets]) => {
const normalizeHandle = (handle: string): string => {
if (handle === 'success') return 'source'
return handle
}
Object.entries(connections).forEach(([rawHandle, targets]) => {
if (targets === null) return
const sourceHandle = normalizeHandle(rawHandle)
const addEdgeForTarget = (targetBlock: string, targetHandle?: string) => {
createValidatedEdge(
modifiedState,

View File

@@ -9,6 +9,7 @@ import {
applyTriggerConfigToBlockSubblocks,
createBlockFromParams,
filterDisallowedTools,
JSON_STRING_SUBBLOCK_KEYS,
normalizeArrayWithIds,
normalizeResponseFormat,
normalizeTools,
@@ -146,6 +147,9 @@ export function handleEditOperation(op: EditWorkflowOperation, ctx: OperationCon
// Normalize array subblocks with id fields (inputFormat, table rows, etc.)
if (shouldNormalizeArrayIds(key)) {
sanitizedValue = normalizeArrayWithIds(value)
if (JSON_STRING_SUBBLOCK_KEYS.has(key)) {
sanitizedValue = JSON.stringify(sanitizedValue)
}
}
// Special handling for tools - normalize and filter disallowed
@@ -164,9 +168,10 @@ export function handleEditOperation(op: EditWorkflowOperation, ctx: OperationCon
}
if (!block.subBlocks[key]) {
const subBlockDef = getBlock(block.type)?.subBlocks.find((sb) => sb.id === key)
block.subBlocks[key] = {
id: key,
type: 'short-input',
type: subBlockDef?.type || 'short-input',
value: sanitizedValue,
}
} else {
@@ -817,6 +822,9 @@ export function handleInsertIntoSubflowOperation(
// Normalize array subblocks with id fields (inputFormat, table rows, etc.)
if (shouldNormalizeArrayIds(key)) {
sanitizedValue = normalizeArrayWithIds(value)
if (JSON_STRING_SUBBLOCK_KEYS.has(key)) {
sanitizedValue = JSON.stringify(sanitizedValue)
}
}
// Special handling for tools - normalize and filter disallowed
@@ -835,9 +843,10 @@ export function handleInsertIntoSubflowOperation(
}
if (!existingBlock.subBlocks[key]) {
const subBlockDef = getBlock(existingBlock.type)?.subBlocks.find((sb) => sb.id === key)
existingBlock.subBlocks[key] = {
id: key,
type: 'short-input',
type: subBlockDef?.type || 'short-input',
value: sanitizedValue,
}
} else {

View File

@@ -44,9 +44,7 @@ You have access to these specialized subagents. Call them by name to delegate ta
## Direct Tools
- **search_online** — Search the web for information.
- **memory_file_read(file_path)** — Read a persistent memory file.
- **memory_file_write(file_path, content)** — Write/update a persistent memory file.
- **memory_file_list()** — List all memory files.
- **context_write(file_path, content)** — Write/update persistent context files (WORKSPACE.md, SESSION.md).
- **grep(pattern, path?)** — Search workspace VFS file contents.
- **glob(pattern)** — Find workspace VFS files by path pattern.
- **read(path)** — Read a workspace VFS file.
@@ -79,20 +77,48 @@ environment/
components/
blocks/{type}.json — block type schemas
integrations/{svc}/{op}.json — integration tool schemas
internal/
memories/WORKSPACE.md — workspace inventory (auto-injected)
memories/SESSION.md — current session state (auto-injected)
\`\`\`
**Tips**: Use \`glob("workflows/*/deployment.json")\` to see which workflows are deployed and how. Use \`grep("error", "workflows/")\` to find workflows with recent errors.
## Memory Management
## Context System — CRITICAL
You have persistent memory files that survive across conversations:
- **SOUL.md** — Your personality and behavioral guidelines. Read this at the start of conversations.
- **USER.md** — Information about the user. Update as you learn preferences and context.
- **MEMORY.md** — Key learnings, decisions, and important context. Update after significant interactions.
Two context files are auto-injected into your system prompt above. You MUST keep them up to date.
**At conversation start**: Read SOUL.md and MEMORY.md to load your persistent context.
**During conversation**: When the user shares important preferences or you make key decisions, update the relevant file.
**Important**: Only write to files when there's genuinely new, important information. Don't update on every message.
| File | Scope | Injected as |
|------|-------|-------------|
| **WORKSPACE.md** | Workspace (persists across chats) | \`## Workspace Context\` above |
| **SESSION.md** | This chat only | \`## Session Context\` above |
### WORKSPACE.md — You MUST keep this current
**On your FIRST turn**: if Workspace Context above shows "(none discovered yet)", scan the workspace immediately:
1. Run \`glob("workflows/*/meta.json")\`, \`glob("knowledgebases/*/meta.json")\`, \`glob("tables/*/meta.json")\`, \`read("environment/credentials.json")\`
2. Write the results via \`context_write("WORKSPACE.md", content)\`
Do this silently as your first action — do NOT ask the user for permission.
**After ANY resource change** (create/edit/delete workflow, KB, table, credential): update WORKSPACE.md immediately.
### SESSION.md — You MUST update after every significant action
After completing any meaningful action (creating a workflow, making edits, deploying, making a decision), rewrite SESSION.md completely with the current state via \`context_write("SESSION.md", content)\`.
Always rewrite the entire file — never append. Keep the existing section structure.
### Reading context files
To read context files, use \`read("internal/memories/WORKSPACE.md")\` or \`read("internal/memories/SESSION.md")\`.
## Discovery-First Rule
**Before creating any new resource**, check what already exists:
1. Check Workspace Context above for existing resources
2. If unclear, run \`glob("workflows/*/meta.json")\` to verify
3. Only create if nothing matches the user's request
## Decision Flow
@@ -105,7 +131,7 @@ You have persistent memory files that survive across conversations:
## Important
- **You work at the workspace level.** When a user mentions a workflow, ask for the workflow name or ID if not provided.
- **You work at the workspace level.** When a user mentions a workflow, check Session Context and Workspace Context first.
- **Always delegate complex work** to the appropriate subagent.
- **Debug first** when something doesn't work — don't guess.
- Be concise and results-focused.

View File

@@ -249,9 +249,20 @@ function sanitizeSubBlocks(
}
// Special handling for condition-input type - clean UI metadata
if (subBlock.type === 'condition-input' && typeof subBlock.value === 'string') {
const cleanedConditions: string = sanitizeConditions(subBlock.value)
sanitized[key] = cleanedConditions
if (subBlock.type === 'condition-input') {
if (typeof subBlock.value === 'string') {
sanitized[key] = sanitizeConditions(subBlock.value)
} else if (Array.isArray(subBlock.value)) {
sanitized[key] = (subBlock.value as unknown as Array<Record<string, unknown>>).map(
(cond) => ({
id: String(cond.id ?? ''),
title: String(cond.title ?? ''),
value: String(cond.value ?? ''),
})
)
} else {
sanitized[key] = subBlock.value
}
return
}
@@ -287,16 +298,22 @@ function convertConditionHandleToSimple(
// Extract the condition UUID from the handle
const conditionId = handle.substring('condition-'.length)
// Get conditions from block subBlocks
// Get conditions from block subBlocks (may be JSON string or array)
const conditionsValue = block.subBlocks?.conditions?.value
if (!conditionsValue || typeof conditionsValue !== 'string') {
if (!conditionsValue) {
return handle
}
let conditions: Array<{ id: string; title: string }>
try {
conditions = JSON.parse(conditionsValue)
} catch {
if (Array.isArray(conditionsValue)) {
conditions = conditionsValue as unknown as Array<{ id: string; title: string }>
} else if (typeof conditionsValue === 'string') {
try {
conditions = JSON.parse(conditionsValue)
} catch {
return handle
}
} else {
return handle
}
@@ -341,16 +358,22 @@ function convertRouterHandleToSimple(handle: string, _blockId: string, block: Bl
// Extract the route UUID from the handle
const routeId = handle.substring('router-'.length)
// Get routes from block subBlocks
// Get routes from block subBlocks (may be JSON string or array)
const routesValue = block.subBlocks?.routes?.value
if (!routesValue || typeof routesValue !== 'string') {
if (!routesValue) {
return handle
}
let routes: Array<{ id: string; title?: string }>
try {
routes = JSON.parse(routesValue)
} catch {
if (Array.isArray(routesValue)) {
routes = routesValue as unknown as Array<{ id: string; title?: string }>
} else if (typeof routesValue === 'string') {
try {
routes = JSON.parse(routesValue)
} catch {
return handle
}
} else {
return handle
}