fix(logs) Run workflows client side in mothership to transmit logs (#3529)

* Run workflows client side in mothership to transmit logs

* Initialize set as constant, prevent duplicate execution

* Fix lint

---------

Co-authored-by: Theodore Li <theo@sim.ai>
This commit is contained in:
Theodore Li
2026-03-11 18:28:32 -07:00
committed by GitHub
parent 8927807398
commit c2bf65fcf1
3 changed files with 39 additions and 4 deletions

View File

@@ -2,7 +2,9 @@ import { useCallback, useEffect, useRef, useState } from 'react'
import { createLogger } from '@sim/logger'
import { useQueryClient } from '@tanstack/react-query'
import { usePathname } from 'next/navigation'
import { executeRunToolOnClient } from '@/lib/copilot/client-sse/run-tool-execution'
import { MOTHERSHIP_CHAT_API_PATH } from '@/lib/copilot/constants'
import { isWorkflowToolName } from '@/lib/copilot/workflow-tools'
import { knowledgeKeys } from '@/hooks/queries/kb/knowledge'
import { tableKeys } from '@/hooks/queries/tables'
import {
@@ -249,6 +251,7 @@ export function useChat(workspaceId: string, initialChatId?: string): UseChatRet
let buffer = ''
const blocks: ContentBlock[] = []
const toolMap = new Map<string, number>()
const clientExecutionStarted = new Set<string>()
let activeSubagent: string | undefined
let lastTableId: string | null = null
let lastWorkflowId: string | null = null
@@ -336,6 +339,7 @@ export function useChat(workspaceId: string, initialChatId?: string): UseChatRet
const id = parsed.toolCallId
const data = getPayloadData(parsed)
const name = parsed.toolName || data?.name || 'unknown'
const isPartial = data?.partial === true
if (!id) break
if (RESOURCE_TOOL_NAMES.has(name)) {
@@ -373,6 +377,18 @@ export function useChat(workspaceId: string, initialChatId?: string): UseChatRet
}
}
flush()
if (
parsed.type === 'tool_call' &&
ui?.clientExecutable &&
isWorkflowToolName(name) &&
!isPartial &&
!clientExecutionStarted.has(id)
) {
clientExecutionStarted.add(id)
const args = data?.arguments ?? data?.input ?? {}
executeRunToolOnClient(id, name, args as Record<string, unknown>)
}
break
}
case 'tool_result': {

View File

@@ -202,6 +202,7 @@ export interface SSEPayloadUI {
phaseLabel?: string
icon?: string
internal?: boolean
clientExecutable?: boolean
}
export interface SSEPayloadData {
@@ -209,6 +210,7 @@ export interface SSEPayloadData {
ui?: SSEPayloadUI
id?: string
agent?: string
partial?: boolean
arguments?: Record<string, unknown>
input?: Record<string, unknown>
result?: unknown

View File

@@ -18,6 +18,7 @@ import type {
StreamingContext,
ToolCallState,
} from '@/lib/copilot/orchestrator/types'
import { isWorkflowToolName } from '@/lib/copilot/workflow-tools'
import { executeToolAndReport, waitForToolCompletion, waitForToolDecision } from './tool-execution'
const logger = createLogger('CopilotSseHandlers')
@@ -265,9 +266,17 @@ export const sseHandlers: Record<string, SSEHandler> = {
})
}
// Non-interactive mode (Mothership/MCP): skip confirmation & client gates,
// execute server-side directly.
if (options.interactive === false) {
if (clientExecutable && isWorkflowToolName(toolName)) {
toolCall.status = 'executing'
const completion = await waitForToolCompletion(
toolCallId,
options.timeout || STREAM_TIMEOUT_MS,
options.abortSignal
)
handleClientCompletion(toolCall, toolCallId, completion)
return
}
if (options.autoExecuteTools !== false) {
fireToolExecution()
}
@@ -514,9 +523,17 @@ export const subAgentHandlers: Record<string, SSEHandler> = {
})
}
// Non-interactive mode (Mothership/MCP): skip confirmation & client gates,
// execute server-side directly.
if (options.interactive === false) {
if (clientExecutable && isWorkflowToolName(toolName)) {
toolCall.status = 'executing'
const completion = await waitForToolCompletion(
toolCallId,
options.timeout || STREAM_TIMEOUT_MS,
options.abortSignal
)
handleClientCompletion(toolCall, toolCallId, completion)
return
}
if (options.autoExecuteTools !== false) {
fireToolExecution()
}