Skip to content

Commit 7678b42

Browse files
committed
fix(mship): transcript stuff
1 parent ece8412 commit 7678b42

8 files changed

Lines changed: 356 additions & 24 deletions

File tree

apps/sim/app/workspace/[workspaceId]/home/hooks/stream/turn-model-serialize.test.ts

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -323,6 +323,47 @@ describe('modelToContentBlocks', () => {
323323
expect(blocksByType(blocks, 'subagent_end')).toHaveLength(0)
324324
expect(blocksByType(blocks, 'subagent')).toHaveLength(1)
325325
})
326+
327+
it('persists a completed compaction inside its subagent span', () => {
328+
const sub: Scope = {
329+
lane: 'subagent',
330+
spanId: 'S1',
331+
parentSpanId: 'main',
332+
parentToolCallId: 'tc-workflow',
333+
agentId: 'workflow',
334+
}
335+
const blocks = modelToContentBlocks(
336+
build([
337+
env(
338+
1,
339+
'span',
340+
{
341+
kind: 'subagent',
342+
event: 'start',
343+
agent: 'workflow',
344+
data: { tool_call_id: 'tc-workflow' },
345+
},
346+
sub
347+
),
348+
env(2, 'run', { kind: 'compaction_start' }, sub),
349+
env(3, 'run', { kind: 'compaction_done' }, sub),
350+
])
351+
)
352+
353+
const compaction = blocks.find(
354+
(block) => block.type === 'tool_call' && block.toolCall?.name === 'context_compaction'
355+
)
356+
expect(compaction).toEqual(
357+
expect.objectContaining({
358+
spanId: 'S1',
359+
parentSpanId: 'main',
360+
toolCall: expect.objectContaining({
361+
calledBy: 'workflow',
362+
status: 'success',
363+
}),
364+
})
365+
)
366+
})
326367
})
327368

328369
describe('contentBlocksToModel round-trip', () => {

apps/sim/app/workspace/[workspaceId]/home/hooks/stream/turn-model.test.ts

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -435,6 +435,44 @@ describe('reduceEvent — error tag + compaction coverage', () => {
435435
expect(compaction.status).toBe('success')
436436
expect(compaction.uiTitle).toBe('Compacted context')
437437
})
438+
439+
it('pairs concurrent compactions only within their scoped subagent spans', () => {
440+
const scopeA: Scope = {
441+
lane: 'subagent',
442+
spanId: 'S1',
443+
parentSpanId: MAIN_SPAN,
444+
parentToolCallId: 'tc-A',
445+
agentId: 'workflow',
446+
}
447+
const scopeB: Scope = {
448+
lane: 'subagent',
449+
spanId: 'S2',
450+
parentSpanId: MAIN_SPAN,
451+
parentToolCallId: 'tc-B',
452+
agentId: 'workflow',
453+
}
454+
const m = apply([
455+
envelope(1, 'run', { kind: 'compaction_start' }, scopeA),
456+
envelope(2, 'run', { kind: 'compaction_start' }, scopeB),
457+
envelope(3, 'run', { kind: 'compaction_done' }, scopeA),
458+
])
459+
460+
expect(agent(m, 'S1').agentId).toBe('workflow')
461+
expect(agent(m, 'S2').agentId).toBe('workflow')
462+
expect(tool(m, 'compaction:1')).toEqual(
463+
expect.objectContaining({ spanId: 'S1', status: 'success', uiTitle: 'Compacted context' })
464+
)
465+
expect(tool(m, 'compaction:2')).toEqual(
466+
expect.objectContaining({
467+
spanId: 'S2',
468+
status: 'running',
469+
uiTitle: 'Compacting context...',
470+
})
471+
)
472+
473+
reduceEvent(m, envelope(4, 'run', { kind: 'compaction_done' }, scopeB))
474+
expect(tool(m, 'compaction:2').status).toBe('success')
475+
})
438476
})
439477

440478
describe('turn-terminal propagation', () => {

apps/sim/app/workspace/[workspaceId]/home/hooks/stream/turn-model.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -564,6 +564,7 @@ export function reduceEvent(model: TurnModel, envelope: PersistedStreamEventEnve
564564
const payload = payloadRecord(envelope.payload)
565565
const kind = payload.kind
566566
if (kind === MothershipStreamV1RunKind.compaction_start) {
567+
ensureSubagentLane(model, spanId, scope, seq, tsMs)
567568
const node = upsertToolNode(
568569
model,
569570
`compaction:${seq}`,
@@ -574,11 +575,13 @@ export function reduceEvent(model: TurnModel, envelope: PersistedStreamEventEnve
574575
)
575576
node.uiTitle = 'Compacting context...'
576577
} else if (kind === MothershipStreamV1RunKind.compaction_done) {
578+
ensureSubagentLane(model, spanId, scope, seq, tsMs)
577579
let finalized = false
578580
for (let i = model.order.length - 1; i >= 0; i--) {
579581
const node = model.nodes.get(model.order[i])
580582
if (
581583
node?.kind === 'tool' &&
584+
node.spanId === spanId &&
582585
node.name === 'context_compaction' &&
583586
node.status === 'running'
584587
) {

apps/sim/lib/copilot/chat/effective-transcript.test.ts

Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,8 +12,12 @@ import { normalizeMessage } from '@/lib/copilot/chat/persisted-message'
1212
import {
1313
MothershipStreamV1CompletionStatus,
1414
MothershipStreamV1EventType,
15+
MothershipStreamV1RunKind,
1516
MothershipStreamV1SessionKind,
17+
MothershipStreamV1SpanLifecycleEvent,
18+
MothershipStreamV1SpanPayloadKind,
1619
MothershipStreamV1TextChannel,
20+
MothershipStreamV1ToolOutcome,
1721
} from '@/lib/copilot/generated/mothership-stream-v1'
1822
import type { StreamBatchEvent } from '@/lib/copilot/request/session/types'
1923

@@ -229,6 +233,80 @@ describe('buildEffectiveChatTranscript', () => {
229233
])
230234
})
231235

236+
it('pairs a scoped compaction inside the owning subagent during stream replay', () => {
237+
const scope = {
238+
lane: 'subagent' as const,
239+
parentToolCallId: 'tc-workflow',
240+
spanId: 'span-workflow',
241+
parentSpanId: 'span-superagent',
242+
agentId: 'superagent',
243+
}
244+
const stream = { streamId: 'stream-1' }
245+
const result = buildEffectiveChatTranscript({
246+
messages: [buildUserMessage('stream-1', 'Hello')],
247+
activeStreamId: 'stream-1',
248+
streamSnapshot: {
249+
events: [
250+
toBatchEvent(1, {
251+
v: 1,
252+
seq: 1,
253+
ts: '2026-04-15T12:00:01.000Z',
254+
type: MothershipStreamV1EventType.span,
255+
stream,
256+
scope,
257+
payload: {
258+
kind: MothershipStreamV1SpanPayloadKind.subagent,
259+
event: MothershipStreamV1SpanLifecycleEvent.start,
260+
agent: 'workflow',
261+
data: { tool_call_id: 'tc-workflow' },
262+
},
263+
}),
264+
toBatchEvent(2, {
265+
v: 1,
266+
seq: 2,
267+
ts: '2026-04-15T12:00:02.000Z',
268+
type: MothershipStreamV1EventType.run,
269+
stream,
270+
scope,
271+
payload: { kind: MothershipStreamV1RunKind.compaction_start },
272+
}),
273+
toBatchEvent(3, {
274+
v: 1,
275+
seq: 3,
276+
ts: '2026-04-15T12:00:03.000Z',
277+
type: MothershipStreamV1EventType.run,
278+
stream,
279+
scope,
280+
payload: {
281+
kind: MothershipStreamV1RunKind.compaction_done,
282+
data: { summary_chars: 42 },
283+
},
284+
}),
285+
],
286+
previewSessions: [],
287+
status: 'active',
288+
},
289+
})
290+
291+
const compactions = result[1]?.contentBlocks?.filter(
292+
(block) => block.type === MothershipStreamV1EventType.tool
293+
)
294+
expect(compactions).toHaveLength(1)
295+
expect(compactions?.[0]).toEqual(
296+
expect.objectContaining({
297+
parentToolCallId: 'tc-workflow',
298+
spanId: 'span-workflow',
299+
parentSpanId: 'span-superagent',
300+
toolCall: expect.objectContaining({
301+
id: 'compaction_2',
302+
name: 'context_compaction',
303+
calledBy: 'workflow',
304+
state: MothershipStreamV1ToolOutcome.success,
305+
}),
306+
})
307+
)
308+
})
309+
232310
it('materializes a cancelled assistant tail when the stream ends before persistence', () => {
233311
const result = buildEffectiveChatTranscript({
234312
messages: [buildUserMessage('stream-1', 'Hello')],

apps/sim/lib/copilot/chat/effective-transcript.ts

Lines changed: 27 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -115,7 +115,7 @@ function buildLiveAssistantMessage(params: {
115115
const subagentBySpanId = new Map<string, string>()
116116
let activeSubagent: string | undefined
117117
let activeSubagentParentToolCallId: string | undefined
118-
let activeCompactionId: string | undefined
118+
const activeCompactionIdByLane = new Map<string, string>()
119119
let runningText = ''
120120
let lastContentSource: 'main' | 'subagent' | null = null
121121
let requestId: string | undefined
@@ -129,7 +129,6 @@ function buildLiveAssistantMessage(params: {
129129
parentToolCallId: string | undefined,
130130
spanId?: string
131131
): string | undefined => {
132-
if (agentId) return agentId
133132
if (spanId) {
134133
const scoped = subagentBySpanId.get(spanId)
135134
if (scoped) return scoped
@@ -138,7 +137,7 @@ function buildLiveAssistantMessage(params: {
138137
const scoped = subagentByParentToolCallId.get(parentToolCallId)
139138
if (scoped) return scoped
140139
}
141-
return undefined
140+
return agentId
142141
}
143142

144143
const resolveParentForSubagentBlock = (
@@ -240,6 +239,11 @@ function buildLiveAssistantMessage(params: {
240239
...(scopedSpanId ? { spanId: scopedSpanId } : {}),
241240
...(scopedParentSpanId ? { parentSpanId: scopedParentSpanId } : {}),
242241
}
242+
const compactionLaneKey = scopedSpanId
243+
? `span:${scopedSpanId}`
244+
: scopedParentToolCallId
245+
? `parent:${scopedParentToolCallId}`
246+
: 'main'
243247

244248
switch (parsed.type) {
245249
case MothershipStreamV1EventType.session: {
@@ -385,22 +389,38 @@ function buildLiveAssistantMessage(params: {
385389
}
386390
case MothershipStreamV1EventType.run: {
387391
if (parsed.payload.kind === MothershipStreamV1RunKind.compaction_start) {
388-
activeCompactionId = `compaction_${entry.eventId}`
392+
const compactionId = `compaction_${entry.eventId}`
393+
activeCompactionIdByLane.set(compactionLaneKey, compactionId)
394+
const parentForBlock = resolveParentForSubagentBlock(
395+
scopedSubagent,
396+
scopedParentToolCallId
397+
)
389398
ensureToolBlock({
390-
toolCallId: activeCompactionId,
399+
toolCallId: compactionId,
391400
toolName: 'context_compaction',
401+
calledBy: scopedSubagent,
402+
...(parentForBlock ? { parentToolCallId: parentForBlock } : {}),
403+
...spanIdentity,
392404
displayTitle: 'Compacting context...',
393405
state: 'executing',
394406
})
395407
continue
396408
}
397409

398410
if (parsed.payload.kind === MothershipStreamV1RunKind.compaction_done) {
399-
const compactionId = activeCompactionId ?? `compaction_${entry.eventId}`
400-
activeCompactionId = undefined
411+
const compactionId =
412+
activeCompactionIdByLane.get(compactionLaneKey) ?? `compaction_${entry.eventId}`
413+
activeCompactionIdByLane.delete(compactionLaneKey)
414+
const parentForBlock = resolveParentForSubagentBlock(
415+
scopedSubagent,
416+
scopedParentToolCallId
417+
)
401418
ensureToolBlock({
402419
toolCallId: compactionId,
403420
toolName: 'context_compaction',
421+
calledBy: scopedSubagent,
422+
...(parentForBlock ? { parentToolCallId: parentForBlock } : {}),
423+
...spanIdentity,
404424
displayTitle: 'Compacted context',
405425
state: MothershipStreamV1ToolOutcome.success,
406426
})

apps/sim/lib/copilot/request/handlers/handlers.test.ts

Lines changed: 97 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -558,6 +558,103 @@ describe('sse-handlers tool lifecycle', () => {
558558
expect(context.subAgentToolCalls['parent-1']?.[0]?.id).toBe('sub-tool-scope-1')
559559
})
560560

561+
it('pairs compaction lifecycle events within each scoped subagent lane', async () => {
562+
context.toolCalls.set('parent-A', {
563+
id: 'parent-A',
564+
name: 'workflow',
565+
status: 'executing',
566+
})
567+
context.toolCalls.set('parent-B', {
568+
id: 'parent-B',
569+
name: 'workflow',
570+
status: 'executing',
571+
})
572+
const sendCompaction = async (
573+
kind: 'compaction_start' | 'compaction_done',
574+
parentToolCallId: string,
575+
spanId: string
576+
) => {
577+
await subAgentHandlers.run(
578+
{
579+
type: MothershipStreamV1EventType.run,
580+
scope: {
581+
lane: 'subagent',
582+
parentToolCallId,
583+
spanId,
584+
parentSpanId: 'main',
585+
agentId: 'superagent',
586+
},
587+
payload: { kind },
588+
} as StreamEvent,
589+
context,
590+
execContext,
591+
{ interactive: false, timeout: 1000 }
592+
)
593+
}
594+
595+
await sendCompaction(MothershipStreamV1RunKind.compaction_start, 'parent-A', 'span-A')
596+
await sendCompaction(MothershipStreamV1RunKind.compaction_start, 'parent-B', 'span-B')
597+
await sendCompaction(MothershipStreamV1RunKind.compaction_done, 'parent-A', 'span-A')
598+
599+
const compactions = context.contentBlocks.filter(
600+
(block) => block.type === 'tool_call' && block.toolCall?.name === 'context_compaction'
601+
)
602+
expect(compactions).toHaveLength(2)
603+
604+
const laneA = compactions.find((block) => block.spanId === 'span-A')
605+
const laneB = compactions.find((block) => block.spanId === 'span-B')
606+
expect(laneA).toEqual(
607+
expect.objectContaining({
608+
calledBy: 'workflow',
609+
parentToolCallId: 'parent-A',
610+
parentSpanId: 'main',
611+
endedAt: expect.any(Number),
612+
toolCall: expect.objectContaining({ status: MothershipStreamV1ToolOutcome.success }),
613+
})
614+
)
615+
expect(laneB?.toolCall?.status).toBe('executing')
616+
617+
await sendCompaction(MothershipStreamV1RunKind.compaction_done, 'parent-B', 'span-B')
618+
619+
expect(context.contentBlocks).toHaveLength(2)
620+
expect(laneB?.toolCall?.status).toBe(MothershipStreamV1ToolOutcome.success)
621+
})
622+
623+
it('pairs main-lane compaction start and done into one completed block', async () => {
624+
await sseHandlers.run(
625+
{
626+
type: MothershipStreamV1EventType.run,
627+
payload: { kind: MothershipStreamV1RunKind.compaction_start },
628+
} satisfies StreamEvent,
629+
context,
630+
execContext,
631+
{ interactive: false }
632+
)
633+
const compactionId = context.contentBlocks[0]?.toolCall?.id
634+
635+
await sseHandlers.run(
636+
{
637+
type: MothershipStreamV1EventType.run,
638+
payload: { kind: MothershipStreamV1RunKind.compaction_done },
639+
} satisfies StreamEvent,
640+
context,
641+
execContext,
642+
{ interactive: false }
643+
)
644+
645+
expect(context.contentBlocks).toHaveLength(1)
646+
expect(context.contentBlocks[0]).toEqual(
647+
expect.objectContaining({
648+
endedAt: expect.any(Number),
649+
toolCall: expect.objectContaining({
650+
id: compactionId,
651+
name: 'context_compaction',
652+
status: MothershipStreamV1ToolOutcome.success,
653+
}),
654+
})
655+
)
656+
})
657+
561658
it('keeps two concurrent subagent lanes separate for text and thinking', async () => {
562659
const send = (parent: string, channel: MothershipStreamV1TextChannel, text: string) =>
563660
subAgentHandlers.text(

0 commit comments

Comments
 (0)