Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
5028875
feat(transport): replace shared chat transport with mothership-stream…
Sg312 Mar 25, 2026
448ea02
improvement(contracts): regenerate contracts from go
Sg312 Mar 25, 2026
7c5547f
feat(tools): add tool catalog codegen from go tool contracts
Sg312 Mar 25, 2026
d5131ae
feat(tools): add tool-executor dispatch framework for sim side tool r…
Sg312 Mar 26, 2026
1157dfc
feat(orchestrator): rewrite tool dispatch with catalog-driven executo…
Sg312 Mar 26, 2026
997896d
feat(orchestrator): checkpoint resume flow
Sg312 Mar 26, 2026
f9c185b
refactor(copilot): consolidate orchestrator into request/ layer
Sg312 Mar 27, 2026
97d41e9
refactor(mothership): reorganize lib/copilot into structured subdirec…
Sg312 Mar 27, 2026
c4876ba
refactor(mothership): canonical transcript layer, dead code cleanup, …
Sg312 Mar 27, 2026
410dd9a
refactor(mothership): rebase onto latest staging
Sg312 Mar 27, 2026
741d856
refactor(mothership): rename request continue to lifecycle
Sg312 Mar 27, 2026
407d254
feat(trace): add initial version of request traces
Sg312 Mar 28, 2026
4b3b6ae
improvement(stream): batch stream from redis
Sg312 Mar 28, 2026
946751e
fix(resume): fix the resume checkpoint
Sg312 Apr 1, 2026
ba3bdd0
fix(resume): fix resume client tool
Sg312 Apr 1, 2026
e3f8663
fix(subagents): subagent resume should join on existing subagent text…
Sg312 Apr 1, 2026
e22fccd
improvement(reconnect): harden reconnect logic
Sg312 Apr 1, 2026
86207ee
fix(superagent): fix superagent integration tools
Sg312 Apr 2, 2026
83cf090
improvement(stream): improve stream perf
Sg312 Apr 3, 2026
54266b9
Rebase with origin dev
Sg312 Apr 3, 2026
d7bfe16
fix(tests): fix failing test
Sg312 Apr 3, 2026
8f61262
fix(build): fix type errors
Sg312 Apr 3, 2026
63e9dff
fix(build): fix build errors
Sg312 Apr 3, 2026
2548912
fix(build): fix type errors
Sg312 Apr 3, 2026
7cd4545
feat(mothership): add cli execution
Sg312 Apr 4, 2026
fb12805
fix(mothership): fix function execute tests
Sg312 Apr 4, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Prev Previous commit
Next Next commit
fix(subagents): subagent resume should join on existing subagent text…
… block
  • Loading branch information
Sg312 committed Apr 3, 2026
commit e3f8663feb1c4937fe0475f6138ae999d93198e3
22 changes: 21 additions & 1 deletion apps/sim/app/workspace/[workspaceId]/home/hooks/use-chat.ts
Original file line number Diff line number Diff line change
Expand Up @@ -531,6 +531,7 @@ export function useChat(
const toolArgsMap = new Map<string, Record<string, unknown>>()
const clientExecutionStarted = new Set<string>()
let activeSubagent: string | undefined
let activeSubagentParentToolCallId: string | undefined
let activeCompactionId: string | undefined
let runningText = ''
let lastContentSource: 'main' | 'subagent' | null = null
Expand Down Expand Up @@ -1095,23 +1096,42 @@ export function useChat(
break
}
const spanEvent = typeof payload.event === 'string' ? payload.event : ''
const spanData = asPayloadRecord(payload.data)
const parentToolCallId =
typeof parsed.scope?.parentToolCallId === 'string'
? parsed.scope.parentToolCallId
: typeof spanData?.tool_call_id === 'string'
? spanData.tool_call_id
: undefined
const isPendingPause = spanData?.pending === true
const name =
typeof payload.agent === 'string'
? payload.agent
: typeof parsed.scope?.agentId === 'string'
? parsed.scope.agentId
: undefined
if (spanEvent === MothershipStreamV1SpanLifecycleEvent.start && name) {
const isSameActiveSubagent =
activeSubagent === name &&
activeSubagentParentToolCallId &&
parentToolCallId === activeSubagentParentToolCallId
activeSubagent = name
blocks.push({ type: 'subagent', content: name })
activeSubagentParentToolCallId = parentToolCallId
if (!isSameActiveSubagent) {
blocks.push({ type: 'subagent', content: name })
}
if (name === FileWrite.id) {
const emptyFile = { fileName: '', content: '' }
streamingFileRef.current = emptyFile
setStreamingFile(emptyFile)
}
flush()
} else if (spanEvent === MothershipStreamV1SpanLifecycleEvent.end) {
if (isPendingPause) {
break
}
activeSubagent = undefined
activeSubagentParentToolCallId = undefined
blocks.push({ type: 'subagent_end' })
flush()
}
Expand Down
23 changes: 19 additions & 4 deletions apps/sim/lib/copilot/request/go/stream.ts
Original file line number Diff line number Diff line change
Expand Up @@ -160,14 +160,26 @@ export async function runStreamLoop(
(spanData?.tool_call_id as string | undefined)
const subagentName = streamEvent.payload.agent as string | undefined
const spanEvent = streamEvent.payload.event as string | undefined
const isPendingPause = spanData?.pending === true
if (spanEvent === MothershipStreamV1SpanLifecycleEvent.start) {
const lastParent = context.subAgentParentStack[context.subAgentParentStack.length - 1]
const lastBlock = context.contentBlocks[context.contentBlocks.length - 1]
if (toolCallId) {
context.subAgentParentStack.push(toolCallId)
if (lastParent !== toolCallId) {
context.subAgentParentStack.push(toolCallId)
}
context.subAgentParentToolCallId = toolCallId
context.subAgentContent[toolCallId] = ''
context.subAgentToolCalls[toolCallId] = []
context.subAgentContent[toolCallId] ??= ''
context.subAgentToolCalls[toolCallId] ??= []
}
if (subagentName) {
if (
subagentName &&
!(
lastParent === toolCallId &&
lastBlock?.type === 'subagent' &&
lastBlock.content === subagentName
)
) {
context.contentBlocks.push({
type: 'subagent',
content: subagentName,
Expand All @@ -177,6 +189,9 @@ export async function runStreamLoop(
continue
}
if (spanEvent === MothershipStreamV1SpanLifecycleEvent.end) {
if (isPendingPause) {
continue
}
if (context.subAgentParentStack.length > 0) {
context.subAgentParentStack.pop()
} else {
Expand Down
61 changes: 61 additions & 0 deletions apps/sim/lib/copilot/request/handlers/handlers.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@ vi.mock('@/lib/copilot/async-runs/repository', async () => {

import {
MothershipStreamV1EventType,
MothershipStreamV1TextChannel,
MothershipStreamV1ToolExecutor,
MothershipStreamV1ToolMode,
MothershipStreamV1ToolOutcome,
Expand Down Expand Up @@ -186,6 +187,66 @@ describe('sse-handlers tool lifecycle', () => {
})
})

it('routes subagent text using the event scope parent tool call id', async () => {
context.subAgentParentToolCallId = 'wrong-parent'
context.subAgentContent['parent-1'] = ''

await subAgentHandlers.text(
{
type: MothershipStreamV1EventType.text,
scope: { lane: 'subagent', parentToolCallId: 'parent-1', agentId: 'deploy' },
payload: {
channel: MothershipStreamV1TextChannel.assistant,
text: 'hello from deploy',
},
} satisfies StreamEvent,
context,
execContext,
{ interactive: false, timeout: 1000 }
)

expect(context.subAgentContent['parent-1']).toBe('hello from deploy')
expect(context.contentBlocks.at(-1)).toEqual(
expect.objectContaining({
type: 'subagent_text',
content: 'hello from deploy',
})
)
})

it('routes subagent tool calls using the event scope parent tool call id', async () => {
executeTool.mockResolvedValueOnce({ success: true, output: { ok: true } })
context.subAgentParentToolCallId = 'wrong-parent'
context.toolCalls.set('parent-1', {
id: 'parent-1',
name: 'deploy',
status: 'pending',
startTime: Date.now(),
})

await subAgentHandlers.tool(
{
type: MothershipStreamV1EventType.tool,
scope: { lane: 'subagent', parentToolCallId: 'parent-1', agentId: 'deploy' },
payload: {
toolCallId: 'sub-tool-scope-1',
toolName: 'read',
arguments: { path: 'workflow.json' },
executor: MothershipStreamV1ToolExecutor.sim,
mode: MothershipStreamV1ToolMode.async,
phase: MothershipStreamV1ToolPhase.call,
},
} satisfies StreamEvent,
context,
execContext,
{ interactive: false, timeout: 1000 }
)

await new Promise((resolve) => setTimeout(resolve, 0))

expect(context.subAgentToolCalls['parent-1']?.[0]?.id).toBe('sub-tool-scope-1')
})

it('skips duplicate tool_call after result', async () => {
executeTool.mockResolvedValueOnce({ success: true, output: { ok: true } })

Expand Down
5 changes: 3 additions & 2 deletions apps/sim/lib/copilot/request/handlers/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,8 +33,9 @@ export const subAgentHandlers: Record<string, StreamHandler> = {
export function handleSubagentRouting(event: StreamEvent, context: StreamingContext): boolean {
if (event.scope?.lane !== 'subagent') return false

// Prefer the wire-level parentToolCallId when present; fall back to the stack.
if (event.scope?.parentToolCallId && !context.subAgentParentToolCallId) {
// Keep the latest scoped parent on hand for legacy callers, but subagent
// handlers should prefer the event-local scope for correctness.
if (event.scope?.parentToolCallId) {
context.subAgentParentToolCallId = event.scope.parentToolCallId
}

Expand Down
4 changes: 2 additions & 2 deletions apps/sim/lib/copilot/request/handlers/text.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,14 +4,14 @@ import {
} from '@/lib/copilot/generated/mothership-stream-v1'
import { getEventData } from '@/lib/copilot/request/sse-utils'
import type { StreamHandler, ToolScope } from './types'
import { addContentBlock } from './types'
import { addContentBlock, getScopedParentToolCallId } from './types'

export function handleTextEvent(scope: ToolScope): StreamHandler {
return (event, context) => {
const d = getEventData(event)

if (scope === 'subagent') {
const parentToolCallId = context.subAgentParentToolCallId
const parentToolCallId = getScopedParentToolCallId(event, context)
if (!parentToolCallId || d?.channel !== MothershipStreamV1TextChannel.assistant) return
const chunk = d?.text as string | undefined
if (!chunk) return
Expand Down
3 changes: 2 additions & 1 deletion apps/sim/lib/copilot/request/handlers/tool.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ import {
emitSyntheticToolResult,
ensureTerminalToolCallState,
getEventUI,
getScopedParentToolCallId,
handleClientCompletion,
inferToolSuccess,
registerPendingToolPromise,
Expand All @@ -54,7 +55,7 @@ export async function handleToolEvent(
scope: ToolScope
): Promise<void> {
const isSubagent = scope === 'subagent'
const parentToolCallId = isSubagent ? context.subAgentParentToolCallId : undefined
const parentToolCallId = isSubagent ? getScopedParentToolCallId(event, context) : undefined

if (isSubagent && !parentToolCallId) return

Expand Down
7 changes: 7 additions & 0 deletions apps/sim/lib/copilot/request/handlers/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,13 @@ export function addContentBlock(
})
}

export function getScopedParentToolCallId(
event: StreamEvent,
context: StreamingContext
): string | undefined {
return event.scope?.parentToolCallId || context.subAgentParentToolCallId
}

export function registerPendingToolPromise(
context: StreamingContext,
toolCallId: string,
Expand Down