Skip to content

Commit ece8412

Browse files
committed
fix(subagents): lanes
1 parent 421d227 commit ece8412

3 files changed

Lines changed: 67 additions & 3 deletions

File tree

apps/sim/app/workspace/[workspaceId]/home/components/message-content/message-content.test.ts

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -266,3 +266,47 @@ describe('shouldSmoothTextSegment', () => {
266266
)
267267
})
268268
})
269+
270+
describe('parseBlocks legacy — thinking between top-level tools', () => {
271+
it('keeps consecutive mothership tools in one group across intervening thinking', () => {
272+
const blocks: ContentBlock[] = [
273+
{ type: 'thinking', content: 'planning the search', timestamp: 1 },
274+
mainToolCall('t1', 'grep'),
275+
{ type: 'thinking', content: 'now read the workflow', timestamp: 1 },
276+
mainToolCall('t2', 'read'),
277+
mainToolCall('t3', 'read'),
278+
]
279+
const segments = parseBlocks(blocks)
280+
const groups = segments.filter((s) => s.type === 'agent_group')
281+
expect(groups).toHaveLength(1)
282+
if (groups[0].type !== 'agent_group') throw new Error('expected group')
283+
expect(groups[0].agentName).toBe('mothership')
284+
expect(groups[0].items).toHaveLength(3)
285+
})
286+
287+
it('still splits the mothership run on real main text', () => {
288+
const blocks: ContentBlock[] = [
289+
mainToolCall('t1', 'grep'),
290+
mainText('Here is what I found so far.'),
291+
mainToolCall('t2', 'read'),
292+
]
293+
const segments = parseBlocks(blocks)
294+
const groups = segments.filter((s) => s.type === 'agent_group')
295+
expect(groups).toHaveLength(2)
296+
})
297+
298+
it('still breaks subagent lanes on main thinking', () => {
299+
const blocks: ContentBlock[] = [
300+
{ type: 'subagent', content: 'workflow', parentToolCallId: 'd1', timestamp: 1 },
301+
{ type: 'subagent_text', content: 'working', parentToolCallId: 'd1', timestamp: 1 },
302+
{ type: 'thinking', content: 'main reasoning', timestamp: 1 },
303+
{ type: 'subagent_text', content: 'later chunk with no lane tag', timestamp: 1 },
304+
]
305+
const segments = parseBlocks(blocks)
306+
const groups = segments.filter((s) => s.type === 'agent_group')
307+
expect(groups).toHaveLength(1)
308+
if (groups[0].type !== 'agent_group') throw new Error('expected group')
309+
// The untagged chunk after thinking must NOT merge into the flushed lane.
310+
expect(groups[0].items).toHaveLength(1)
311+
})
312+
})

apps/sim/app/workspace/[workspaceId]/home/components/message-content/message-content.tsx

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -502,10 +502,20 @@ function parseBlocksLegacy(blocks: ContentBlock[]): MessageSegment[] {
502502
}
503503

504504
if (block.type === 'thinking') {
505-
// Main-agent thinking is not rendered, but it still breaks open subagent
506-
// lanes so later chunks don't merge across it (display-only omission).
505+
// Main-agent thinking is not rendered. It still breaks open SUBAGENT
506+
// lanes so later chunks don't merge across them, but it must not
507+
// fracture the top-level (mothership) tool run: models stream reasoning
508+
// between consecutive tool rounds (Anthropic thinking, OpenAI per-round
509+
// summaries), and splitting on it renders back-to-back top-level tools
510+
// as separate groups. Thinking is also stripped at persistence, so a
511+
// live split would disagree with the reloaded transcript. Every other
512+
// segment kind still breaks the run via its own branch.
507513
if (!block.content?.trim()) continue
514+
const mothershipKey = groupKey('mothership', undefined)
515+
const mothership = groupsByKey.get(mothershipKey)
516+
if (mothership) groupsByKey.delete(mothershipKey)
508517
flushLanes()
518+
if (mothership) groupsByKey.set(mothershipKey, mothership)
509519
continue
510520
}
511521

apps/sim/lib/copilot/chat/persisted-message.ts

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -269,7 +269,17 @@ export function buildPersistedAssistantMessage(
269269
}
270270

271271
if (result.contentBlocks.length > 0) {
272-
message.contentBlocks = mergeAndRedactPersistedBlocks(result.contentBlocks.map(mapContentBlock))
272+
// Reasoning is display-transient and never rendered, so it is never
273+
// persisted either: storing it bloats whale chats and lets the persisted
274+
// turn diverge from the streamed one (the refresh-vs-switch mismatch).
275+
// This is the single write-side choke point for assistant blocks, so the
276+
// guarantee holds for every terminal path (complete, cancelled, error).
277+
const withoutThinking = result.contentBlocks.filter(
278+
(block) => block.type !== 'thinking' && block.type !== 'subagent_thinking'
279+
)
280+
if (withoutThinking.length > 0) {
281+
message.contentBlocks = mergeAndRedactPersistedBlocks(withoutThinking.map(mapContentBlock))
282+
}
273283
}
274284

275285
return message

0 commit comments

Comments
 (0)