Skip to content

Commit e8bdb89

Browse files
committed
Plan respond plan
1 parent 26e2146 commit e8bdb89

File tree

3 files changed

+86
-39
lines changed

3 files changed

+86
-39
lines changed

apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/copilot/components/tool-call/tool-call.tsx

Lines changed: 81 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -26,9 +26,6 @@ import { CLASS_TOOL_METADATA } from '@/stores/panel/copilot/store'
2626
import type { SubAgentContentBlock } from '@/stores/panel/copilot/types'
2727
import { useWorkflowStore } from '@/stores/workflows/workflow/store'
2828

29-
/**
30-
* Parse special tags from content
31-
*/
3229
/**
3330
* Plan step can be either a string or an object with title and plan
3431
*/
@@ -47,6 +44,62 @@ interface ParsedTags {
4744
cleanContent: string
4845
}
4946

47+
/**
48+
* Extract plan steps from plan_respond tool calls in subagent blocks.
49+
* Returns { steps, isComplete } where steps is in the format expected by PlanSteps component.
50+
*/
51+
function extractPlanFromBlocks(blocks: SubAgentContentBlock[] | undefined): {
52+
steps: Record<string, PlanStep> | undefined
53+
isComplete: boolean
54+
} {
55+
if (!blocks) return { steps: undefined, isComplete: false }
56+
57+
// Find the plan_respond tool call
58+
const planRespondBlock = blocks.find(
59+
(b) => b.type === 'subagent_tool_call' && b.toolCall?.name === 'plan_respond'
60+
)
61+
62+
if (!planRespondBlock?.toolCall) {
63+
return { steps: undefined, isComplete: false }
64+
}
65+
66+
// Tool call arguments can be in different places depending on the source
67+
// Also handle nested data.arguments structure from the schema
68+
const tc = planRespondBlock.toolCall as any
69+
const args =
70+
tc.params ||
71+
tc.parameters ||
72+
tc.input ||
73+
tc.arguments ||
74+
tc.data?.arguments ||
75+
{}
76+
const stepsArray = args.steps
77+
78+
if (!Array.isArray(stepsArray) || stepsArray.length === 0) {
79+
return { steps: undefined, isComplete: false }
80+
}
81+
82+
// Convert array format to Record<string, PlanStep> format
83+
// From: [{ number: 1, title: "..." }, { number: 2, title: "..." }]
84+
// To: { "1": "...", "2": "..." }
85+
const steps: Record<string, PlanStep> = {}
86+
for (const step of stepsArray) {
87+
if (step.number !== undefined && step.title) {
88+
steps[String(step.number)] = step.title
89+
}
90+
}
91+
92+
// Check if the tool call is complete (not pending/executing)
93+
const isComplete =
94+
planRespondBlock.toolCall.state === ClientToolCallState.success ||
95+
planRespondBlock.toolCall.state === ClientToolCallState.error
96+
97+
return {
98+
steps: Object.keys(steps).length > 0 ? steps : undefined,
99+
isComplete,
100+
}
101+
}
102+
50103
/**
51104
* Try to parse partial JSON for streaming options.
52105
* Attempts to extract complete key-value pairs from incomplete JSON.
@@ -1094,17 +1147,12 @@ function SubAgentContent({
10941147
})}
10951148
</div>
10961149

1097-
{/* Render PlanSteps for plan subagent when content contains <plan> tag */}
1150+
{/* Render PlanSteps for plan subagent when plan_respond tool is present */}
10981151
{toolName === 'plan' &&
10991152
(() => {
1100-
// Combine all text content from blocks
1101-
const allText = blocks
1102-
.filter((b) => b.type === 'subagent_text' && b.content)
1103-
.map((b) => b.content)
1104-
.join('')
1105-
const parsed = parseSpecialTags(allText)
1106-
if (parsed.plan && Object.keys(parsed.plan).length > 0) {
1107-
return <PlanSteps steps={parsed.plan} streaming={!isThinkingDone} />
1153+
const { steps, isComplete } = extractPlanFromBlocks(blocks)
1154+
if (steps && Object.keys(steps).length > 0) {
1155+
return <PlanSteps steps={steps} streaming={!isComplete} />
11081156
}
11091157
return null
11101158
})()}
@@ -1124,23 +1172,19 @@ function SubAgentThinkingContent({
11241172
isStreaming?: boolean
11251173
}) {
11261174
// Combine all text content from blocks
1127-
let allRawText = ''
11281175
let cleanText = ''
11291176
for (const block of blocks) {
11301177
if (block.type === 'subagent_text' && block.content) {
1131-
allRawText += block.content
11321178
const parsed = parseSpecialTags(block.content)
11331179
cleanText += parsed.cleanContent
11341180
}
11351181
}
11361182

1137-
// Parse plan from all text
1138-
const allParsed = parseSpecialTags(allRawText)
1183+
// Extract plan from plan_respond tool call
1184+
const { steps: planSteps, isComplete: planComplete } = extractPlanFromBlocks(blocks)
1185+
const hasPlan = !!(planSteps && Object.keys(planSteps).length > 0)
11391186

1140-
if (!cleanText.trim() && !allParsed.plan) return null
1141-
1142-
// Check if special tags are present
1143-
const hasSpecialTags = !!(allParsed.plan && Object.keys(allParsed.plan).length > 0)
1187+
if (!cleanText.trim() && !hasPlan) return null
11441188

11451189
return (
11461190
<div className='space-y-1.5'>
@@ -1149,12 +1193,10 @@ function SubAgentThinkingContent({
11491193
content={cleanText}
11501194
isStreaming={isStreaming}
11511195
hasFollowingContent={false}
1152-
hasSpecialTags={hasSpecialTags}
1196+
hasSpecialTags={hasPlan}
11531197
/>
11541198
)}
1155-
{allParsed.plan && Object.keys(allParsed.plan).length > 0 && (
1156-
<PlanSteps steps={allParsed.plan} streaming={isStreaming} />
1157-
)}
1199+
{hasPlan && <PlanSteps steps={planSteps} streaming={!planComplete} />}
11581200
</div>
11591201
)
11601202
}
@@ -1235,11 +1277,17 @@ function SubagentContentRenderer({
12351277
segments.push({ type: 'text', content: currentText })
12361278
}
12371279

1238-
// Parse plan and options
1280+
// Parse options from text (plan is extracted from tool call)
12391281
const allParsed = parseSpecialTags(allRawText)
1282+
1283+
// Extract plan from plan_respond tool call
1284+
const { steps: planSteps, isComplete: planComplete } = extractPlanFromBlocks(
1285+
toolCall.subAgentBlocks
1286+
)
1287+
const hasPlan = !!(planSteps && Object.keys(planSteps).length > 0)
1288+
12401289
const hasSpecialTags = !!(
1241-
(allParsed.plan && Object.keys(allParsed.plan).length > 0) ||
1242-
(allParsed.options && Object.keys(allParsed.options).length > 0)
1290+
hasPlan || (allParsed.options && Object.keys(allParsed.options).length > 0)
12431291
)
12441292

12451293
const formatDuration = (ms: number) => {
@@ -1251,9 +1299,6 @@ function SubagentContentRenderer({
12511299
const outerLabel = getSubagentCompletionLabel(toolCall.name)
12521300
const durationText = `${outerLabel} for ${formatDuration(duration)}`
12531301

1254-
// Check if we have a plan to render outside the collapsible
1255-
const hasPlan = allParsed.plan && Object.keys(allParsed.plan).length > 0
1256-
12571302
// Render the collapsible content (thinking blocks + tool calls, NOT plan)
12581303
// Inner thinking text always uses "Thought" label
12591304
const renderCollapsibleContent = () => (
@@ -1299,7 +1344,7 @@ function SubagentContentRenderer({
12991344
return (
13001345
<div className='w-full space-y-1.5'>
13011346
{renderCollapsibleContent()}
1302-
{hasPlan && <PlanSteps steps={allParsed.plan!} streaming={isStreaming} />}
1347+
{hasPlan && <PlanSteps steps={planSteps!} streaming={!planComplete} />}
13031348
</div>
13041349
)
13051350
}
@@ -1333,7 +1378,7 @@ function SubagentContentRenderer({
13331378
</div>
13341379

13351380
{/* Plan stays outside the collapsible */}
1336-
{hasPlan && <PlanSteps steps={allParsed.plan!} />}
1381+
{hasPlan && <PlanSteps steps={planSteps!} />}
13371382
</div>
13381383
)
13391384
}
@@ -1958,7 +2003,10 @@ export function ToolCall({ toolCall: toolCallProp, toolCallId, onStateChange }:
19582003
toolCall.name === 'checkoff_todo' ||
19592004
toolCall.name === 'mark_todo_in_progress' ||
19602005
toolCall.name === 'tool_search_tool_regex' ||
1961-
toolCall.name === 'user_memory'
2006+
toolCall.name === 'user_memory' ||
2007+
toolCall.name === 'edit_responsd' ||
2008+
toolCall.name === 'debug_respond' ||
2009+
toolCall.name === 'plan_respond'
19622010
)
19632011
return null
19642012

apps/sim/lib/copilot/tools/server/workflow/edit-workflow.ts

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -966,7 +966,10 @@ function validateConditionHandle(
966966
if (elseIfIndex === 0) {
967967
handleToNormalized.set(`${legacySemanticPrefix}else-if`, normalizedHandle)
968968
} else {
969-
handleToNormalized.set(`${legacySemanticPrefix}else-if-${elseIfIndex + 1}`, normalizedHandle)
969+
handleToNormalized.set(
970+
`${legacySemanticPrefix}else-if-${elseIfIndex + 1}`,
971+
normalizedHandle
972+
)
970973
}
971974
elseIfIndex++
972975
} else if (title === 'else') {

apps/sim/lib/workflows/sanitization/json-sanitizer.ts

Lines changed: 1 addition & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -330,11 +330,7 @@ function convertConditionHandleToSimple(
330330
* Convert internal router handle (router-{uuid}) to simple format (route-0, route-1)
331331
* Uses 0-indexed numbering for routes
332332
*/
333-
function convertRouterHandleToSimple(
334-
handle: string,
335-
_blockId: string,
336-
block: BlockState
337-
): string {
333+
function convertRouterHandleToSimple(handle: string, _blockId: string, block: BlockState): string {
338334
if (!handle.startsWith('router-')) {
339335
return handle
340336
}

0 commit comments

Comments
 (0)