mirror of
https://github.com/simstudioai/sim.git
synced 2026-04-06 03:00:16 -04:00
feat(logs): added sub-workflow logs, updated trace spans UI, fix scroll behavior in workflow registry sidebar (#1037)
* added sub-workflow logs * indent input/output in trace spans display * better color scheme for workflow logs * scroll behavior in sidebar updated * cleanup * fixed failing tests
This commit is contained in:
committed by
Siddharth Ganesan
parent
9ad36c0e34
commit
79e932fed9
@@ -82,14 +82,21 @@ function transformBlockData(data: any, blockType: string, isInput: boolean) {
|
||||
interface CollapsibleInputOutputProps {
|
||||
span: TraceSpan
|
||||
spanId: string
|
||||
depth: number
|
||||
}
|
||||
|
||||
function CollapsibleInputOutput({ span, spanId }: CollapsibleInputOutputProps) {
|
||||
function CollapsibleInputOutput({ span, spanId, depth }: CollapsibleInputOutputProps) {
|
||||
const [inputExpanded, setInputExpanded] = useState(false)
|
||||
const [outputExpanded, setOutputExpanded] = useState(false)
|
||||
|
||||
// Calculate the left margin based on depth to match the parent span's indentation
|
||||
const leftMargin = depth * 16 + 8 + 24 // Base depth indentation + icon width + extra padding
|
||||
|
||||
return (
|
||||
<div className='mt-2 mr-4 mb-4 ml-8 space-y-3 overflow-hidden'>
|
||||
<div
|
||||
className='mt-2 mr-4 mb-4 space-y-3 overflow-hidden'
|
||||
style={{ marginLeft: `${leftMargin}px` }}
|
||||
>
|
||||
{/* Input Data - Collapsible */}
|
||||
{span.input && (
|
||||
<div>
|
||||
@@ -162,26 +169,30 @@ function BlockDataDisplay({
|
||||
if (value === undefined) return <span className='text-muted-foreground italic'>undefined</span>
|
||||
|
||||
if (typeof value === 'string') {
|
||||
return <span className='break-all text-green-700 dark:text-green-400'>"{value}"</span>
|
||||
return <span className='break-all text-emerald-700 dark:text-emerald-400'>"{value}"</span>
|
||||
}
|
||||
|
||||
if (typeof value === 'number') {
|
||||
return <span className='text-blue-700 dark:text-blue-400'>{value}</span>
|
||||
return <span className='font-mono text-blue-700 dark:text-blue-400'>{value}</span>
|
||||
}
|
||||
|
||||
if (typeof value === 'boolean') {
|
||||
return <span className='text-purple-700 dark:text-purple-400'>{value.toString()}</span>
|
||||
return (
|
||||
<span className='font-mono text-amber-700 dark:text-amber-400'>{value.toString()}</span>
|
||||
)
|
||||
}
|
||||
|
||||
if (Array.isArray(value)) {
|
||||
if (value.length === 0) return <span className='text-muted-foreground'>[]</span>
|
||||
return (
|
||||
<div className='space-y-1'>
|
||||
<div className='space-y-0.5'>
|
||||
<span className='text-muted-foreground'>[</span>
|
||||
<div className='ml-4 space-y-1'>
|
||||
<div className='ml-2 space-y-0.5'>
|
||||
{value.map((item, index) => (
|
||||
<div key={index} className='flex min-w-0 gap-2'>
|
||||
<span className='flex-shrink-0 text-muted-foreground text-xs'>{index}:</span>
|
||||
<div key={index} className='flex min-w-0 gap-1.5'>
|
||||
<span className='flex-shrink-0 font-mono text-slate-600 text-xs dark:text-slate-400'>
|
||||
{index}:
|
||||
</span>
|
||||
<div className='min-w-0 flex-1 overflow-hidden'>{renderValue(item)}</div>
|
||||
</div>
|
||||
))}
|
||||
@@ -196,10 +207,10 @@ function BlockDataDisplay({
|
||||
if (entries.length === 0) return <span className='text-muted-foreground'>{'{}'}</span>
|
||||
|
||||
return (
|
||||
<div className='space-y-1'>
|
||||
<div className='space-y-0.5'>
|
||||
{entries.map(([objKey, objValue]) => (
|
||||
<div key={objKey} className='flex min-w-0 gap-2'>
|
||||
<span className='flex-shrink-0 font-medium text-orange-700 dark:text-orange-400'>
|
||||
<div key={objKey} className='flex min-w-0 gap-1.5'>
|
||||
<span className='flex-shrink-0 font-medium text-indigo-700 dark:text-indigo-400'>
|
||||
{objKey}:
|
||||
</span>
|
||||
<div className='min-w-0 flex-1 overflow-hidden'>{renderValue(objValue, objKey)}</div>
|
||||
@@ -227,12 +238,12 @@ function BlockDataDisplay({
|
||||
{transformedData &&
|
||||
Object.keys(transformedData).filter((key) => key !== 'error' && key !== 'success')
|
||||
.length > 0 && (
|
||||
<div className='space-y-1'>
|
||||
<div className='space-y-0.5'>
|
||||
{Object.entries(transformedData)
|
||||
.filter(([key]) => key !== 'error' && key !== 'success')
|
||||
.map(([key, value]) => (
|
||||
<div key={key} className='flex gap-2'>
|
||||
<span className='font-medium text-orange-700 dark:text-orange-400'>{key}:</span>
|
||||
<div key={key} className='flex gap-1.5'>
|
||||
<span className='font-medium text-indigo-700 dark:text-indigo-400'>{key}:</span>
|
||||
{renderValue(value, key)}
|
||||
</div>
|
||||
))}
|
||||
@@ -592,7 +603,9 @@ function TraceSpanItem({
|
||||
{expanded && (
|
||||
<div>
|
||||
{/* Block Input/Output Data - Collapsible */}
|
||||
{(span.input || span.output) && <CollapsibleInputOutput span={span} spanId={spanId} />}
|
||||
{(span.input || span.output) && (
|
||||
<CollapsibleInputOutput span={span} spanId={spanId} depth={depth} />
|
||||
)}
|
||||
|
||||
{/* Children and tool calls */}
|
||||
{/* Render child spans */}
|
||||
|
||||
@@ -720,21 +720,47 @@ export function Sidebar() {
|
||||
`[data-workflow-id="${workflowId}"]`
|
||||
) as HTMLElement
|
||||
if (activeWorkflow) {
|
||||
activeWorkflow.scrollIntoView({
|
||||
block: 'start',
|
||||
})
|
||||
// Check if this is a newly created workflow (created within the last 5 seconds)
|
||||
const currentWorkflow = workflows[workflowId]
|
||||
const isNewlyCreated =
|
||||
currentWorkflow &&
|
||||
currentWorkflow.lastModified instanceof Date &&
|
||||
Date.now() - currentWorkflow.lastModified.getTime() < 5000 // 5 seconds
|
||||
|
||||
// Adjust scroll position to eliminate the small gap at the top
|
||||
const scrollViewport = scrollContainer.querySelector(
|
||||
'[data-radix-scroll-area-viewport]'
|
||||
) as HTMLElement
|
||||
if (scrollViewport && scrollViewport.scrollTop > 0) {
|
||||
scrollViewport.scrollTop = Math.max(0, scrollViewport.scrollTop - 8)
|
||||
if (isNewlyCreated) {
|
||||
// For newly created workflows, use the original behavior - scroll to top
|
||||
activeWorkflow.scrollIntoView({
|
||||
block: 'start',
|
||||
})
|
||||
|
||||
// Adjust scroll position to eliminate the small gap at the top
|
||||
const scrollViewport = scrollContainer.querySelector(
|
||||
'[data-radix-scroll-area-viewport]'
|
||||
) as HTMLElement
|
||||
if (scrollViewport && scrollViewport.scrollTop > 0) {
|
||||
scrollViewport.scrollTop = Math.max(0, scrollViewport.scrollTop - 8)
|
||||
}
|
||||
} else {
|
||||
// For existing workflows, check if already visible and scroll minimally
|
||||
const containerRect = scrollContainer.getBoundingClientRect()
|
||||
const workflowRect = activeWorkflow.getBoundingClientRect()
|
||||
|
||||
// Only scroll if the workflow is not fully visible
|
||||
const isFullyVisible =
|
||||
workflowRect.top >= containerRect.top && workflowRect.bottom <= containerRect.bottom
|
||||
|
||||
if (!isFullyVisible) {
|
||||
// Use 'nearest' to scroll minimally - only bring into view, don't force to top
|
||||
activeWorkflow.scrollIntoView({
|
||||
block: 'nearest',
|
||||
behavior: 'smooth',
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}, [workflowId, isLoading])
|
||||
}, [workflowId, isLoading, workflows])
|
||||
|
||||
const [showSettings, setShowSettings] = useState(false)
|
||||
const [showHelp, setShowHelp] = useState(false)
|
||||
|
||||
@@ -209,6 +209,7 @@ describe('WorkflowBlockHandler', () => {
|
||||
success: true,
|
||||
childWorkflowName: 'Child Workflow',
|
||||
result: { data: 'test result' },
|
||||
childTraceSpans: [],
|
||||
})
|
||||
})
|
||||
|
||||
@@ -248,6 +249,7 @@ describe('WorkflowBlockHandler', () => {
|
||||
success: true,
|
||||
childWorkflowName: 'Child Workflow',
|
||||
result: { nested: 'data' },
|
||||
childTraceSpans: [],
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { generateInternalToken } from '@/lib/auth/internal'
|
||||
import { createLogger } from '@/lib/logs/console/logger'
|
||||
import { buildTraceSpans } from '@/lib/logs/execution/trace-spans/trace-spans'
|
||||
import { getBaseUrl } from '@/lib/urls/utils'
|
||||
import type { BlockOutput } from '@/blocks/types'
|
||||
import { Executor } from '@/executor'
|
||||
@@ -104,18 +105,17 @@ export class WorkflowBlockHandler implements BlockHandler {
|
||||
// Remove current execution from stack after completion
|
||||
WorkflowBlockHandler.executionStack.delete(executionId)
|
||||
|
||||
// Log execution completion
|
||||
logger.info(`Child workflow ${childWorkflowName} completed in ${Math.round(duration)}ms`)
|
||||
|
||||
// Map child workflow output to parent block output
|
||||
const childTraceSpans = this.captureChildWorkflowLogs(result, childWorkflowName, context)
|
||||
const mappedResult = this.mapChildOutputToParent(
|
||||
result,
|
||||
workflowId,
|
||||
childWorkflowName,
|
||||
duration
|
||||
duration,
|
||||
childTraceSpans
|
||||
)
|
||||
|
||||
// If the child workflow failed, throw an error to trigger proper error handling in the parent
|
||||
if ((mappedResult as any).success === false) {
|
||||
const childError = (mappedResult as any).error || 'Unknown error'
|
||||
throw new Error(`Error in child workflow "${childWorkflowName}": ${childError}`)
|
||||
@@ -125,19 +125,13 @@ export class WorkflowBlockHandler implements BlockHandler {
|
||||
} catch (error: any) {
|
||||
logger.error(`Error executing child workflow ${workflowId}:`, error)
|
||||
|
||||
// Clean up execution stack in case of error
|
||||
const executionId = `${context.workflowId}_sub_${workflowId}_${block.id}`
|
||||
WorkflowBlockHandler.executionStack.delete(executionId)
|
||||
|
||||
// Get workflow name for error reporting
|
||||
const { workflows } = useWorkflowRegistry.getState()
|
||||
const workflowMetadata = workflows[workflowId]
|
||||
const childWorkflowName = workflowMetadata?.name || workflowId
|
||||
|
||||
// Enhance error message with child workflow context
|
||||
const originalError = error.message || 'Unknown error'
|
||||
|
||||
// Check if error message already has child workflow context to avoid duplication
|
||||
if (originalError.startsWith('Error in child workflow')) {
|
||||
throw error // Re-throw as-is to avoid duplication
|
||||
}
|
||||
@@ -151,12 +145,9 @@ export class WorkflowBlockHandler implements BlockHandler {
|
||||
*/
|
||||
private async loadChildWorkflow(workflowId: string) {
|
||||
try {
|
||||
// Fetch workflow from API with internal authentication header
|
||||
const headers: Record<string, string> = {
|
||||
'Content-Type': 'application/json',
|
||||
}
|
||||
|
||||
// Add internal auth header for server-side calls
|
||||
if (typeof window === 'undefined') {
|
||||
const token = await generateInternalToken()
|
||||
headers.Authorization = `Bearer ${token}`
|
||||
@@ -182,16 +173,12 @@ export class WorkflowBlockHandler implements BlockHandler {
|
||||
}
|
||||
|
||||
logger.info(`Loaded child workflow: ${workflowData.name} (${workflowId})`)
|
||||
|
||||
// Extract the workflow state (API returns normalized data in state field)
|
||||
const workflowState = workflowData.state
|
||||
|
||||
if (!workflowState || !workflowState.blocks) {
|
||||
logger.error(`Child workflow ${workflowId} has invalid state`)
|
||||
return null
|
||||
}
|
||||
|
||||
// Use blocks directly since API returns data from normalized tables
|
||||
const serializedWorkflow = this.serializer.serializeWorkflow(
|
||||
workflowState.blocks,
|
||||
workflowState.edges || [],
|
||||
@@ -222,17 +209,101 @@ export class WorkflowBlockHandler implements BlockHandler {
|
||||
}
|
||||
|
||||
/**
|
||||
* Maps child workflow output to parent block output format
|
||||
* Captures and transforms child workflow logs into trace spans
|
||||
*/
|
||||
private captureChildWorkflowLogs(
|
||||
childResult: any,
|
||||
childWorkflowName: string,
|
||||
parentContext: ExecutionContext
|
||||
): any[] {
|
||||
try {
|
||||
if (!childResult.logs || !Array.isArray(childResult.logs)) {
|
||||
return []
|
||||
}
|
||||
|
||||
const { traceSpans } = buildTraceSpans(childResult)
|
||||
|
||||
if (!traceSpans || traceSpans.length === 0) {
|
||||
return []
|
||||
}
|
||||
|
||||
const transformedSpans = traceSpans.map((span: any) => {
|
||||
return this.transformSpanForChildWorkflow(span, childWorkflowName)
|
||||
})
|
||||
|
||||
return transformedSpans
|
||||
} catch (error) {
|
||||
logger.error(`Error capturing child workflow logs for ${childWorkflowName}:`, error)
|
||||
return []
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Transforms trace span for child workflow context
|
||||
*/
|
||||
private transformSpanForChildWorkflow(span: any, childWorkflowName: string): any {
|
||||
const transformedSpan = {
|
||||
...span,
|
||||
name: this.cleanChildSpanName(span.name, childWorkflowName),
|
||||
metadata: {
|
||||
...span.metadata,
|
||||
isFromChildWorkflow: true,
|
||||
childWorkflowName,
|
||||
},
|
||||
}
|
||||
|
||||
if (span.children && Array.isArray(span.children)) {
|
||||
transformedSpan.children = span.children.map((childSpan: any) =>
|
||||
this.transformSpanForChildWorkflow(childSpan, childWorkflowName)
|
||||
)
|
||||
}
|
||||
|
||||
if (span.output?.childTraceSpans) {
|
||||
transformedSpan.output = {
|
||||
...transformedSpan.output,
|
||||
childTraceSpans: span.output.childTraceSpans,
|
||||
}
|
||||
}
|
||||
|
||||
return transformedSpan
|
||||
}
|
||||
|
||||
/**
|
||||
* Cleans up child span names for readability
|
||||
*/
|
||||
private cleanChildSpanName(spanName: string, childWorkflowName: string): string {
|
||||
if (spanName.includes(`${childWorkflowName}:`)) {
|
||||
const cleanName = spanName.replace(`${childWorkflowName}:`, '').trim()
|
||||
|
||||
if (cleanName === 'Workflow Execution') {
|
||||
return `${childWorkflowName} workflow`
|
||||
}
|
||||
|
||||
if (cleanName.startsWith('Agent ')) {
|
||||
return `${cleanName}`
|
||||
}
|
||||
|
||||
return `${cleanName}`
|
||||
}
|
||||
|
||||
if (spanName === 'Workflow Execution') {
|
||||
return `${childWorkflowName} workflow`
|
||||
}
|
||||
|
||||
return `${spanName}`
|
||||
}
|
||||
|
||||
/**
|
||||
* Maps child workflow output to parent block output
|
||||
*/
|
||||
private mapChildOutputToParent(
|
||||
childResult: any,
|
||||
childWorkflowId: string,
|
||||
childWorkflowName: string,
|
||||
duration: number
|
||||
duration: number,
|
||||
childTraceSpans?: any[]
|
||||
): BlockOutput {
|
||||
const success = childResult.success !== false
|
||||
|
||||
// If child workflow failed, return minimal output
|
||||
if (!success) {
|
||||
logger.warn(`Child workflow ${childWorkflowName} failed`)
|
||||
return {
|
||||
@@ -241,18 +312,15 @@ export class WorkflowBlockHandler implements BlockHandler {
|
||||
error: childResult.error || 'Child workflow execution failed',
|
||||
} as Record<string, any>
|
||||
}
|
||||
|
||||
// Extract the actual result content from the flattened structure
|
||||
let result = childResult
|
||||
if (childResult?.output) {
|
||||
result = childResult.output
|
||||
}
|
||||
|
||||
// Return a properly structured response with all required fields
|
||||
return {
|
||||
success: true,
|
||||
childWorkflowName,
|
||||
result,
|
||||
childTraceSpans: childTraceSpans || [],
|
||||
} as Record<string, any>
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { BlockPathCalculator } from '@/lib/block-path-calculator'
|
||||
import { createLogger } from '@/lib/logs/console/logger'
|
||||
import type { TraceSpan } from '@/lib/logs/types'
|
||||
import type { BlockOutput } from '@/blocks/types'
|
||||
import { BlockType } from '@/executor/consts'
|
||||
import {
|
||||
@@ -1510,6 +1511,9 @@ export class Executor {
|
||||
blockLog.durationMs = Math.round(executionTime)
|
||||
blockLog.endedAt = new Date().toISOString()
|
||||
|
||||
// Handle child workflow logs integration
|
||||
this.integrateChildWorkflowLogs(block, output)
|
||||
|
||||
context.blockLogs.push(blockLog)
|
||||
|
||||
// Skip console logging for infrastructure blocks like loops and parallels
|
||||
@@ -1617,6 +1621,9 @@ export class Executor {
|
||||
blockLog.durationMs = Math.round(executionTime)
|
||||
blockLog.endedAt = new Date().toISOString()
|
||||
|
||||
// Handle child workflow logs integration
|
||||
this.integrateChildWorkflowLogs(block, output)
|
||||
|
||||
context.blockLogs.push(blockLog)
|
||||
|
||||
// Skip console logging for infrastructure blocks like loops and parallels
|
||||
@@ -2003,4 +2010,22 @@ export class Executor {
|
||||
context.blockLogs.push(starterBlockLog)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Preserves child workflow trace spans for proper nesting
|
||||
*/
|
||||
private integrateChildWorkflowLogs(block: SerializedBlock, output: NormalizedBlockOutput): void {
|
||||
if (block.metadata?.id !== BlockType.WORKFLOW) {
|
||||
return
|
||||
}
|
||||
|
||||
if (!output || typeof output !== 'object' || !output.childTraceSpans) {
|
||||
return
|
||||
}
|
||||
|
||||
const childTraceSpans = output.childTraceSpans as TraceSpan[]
|
||||
if (!Array.isArray(childTraceSpans) || childTraceSpans.length === 0) {
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -90,29 +90,44 @@ export function buildTraceSpans(result: ExecutionResult): {
|
||||
// Always add cost, token, and model information if available (regardless of provider timing)
|
||||
if (log.output?.cost) {
|
||||
;(span as any).cost = log.output.cost
|
||||
logger.debug(`Added cost to span ${span.id}`, {
|
||||
blockId: log.blockId,
|
||||
blockType: log.blockType,
|
||||
cost: log.output.cost,
|
||||
})
|
||||
}
|
||||
|
||||
if (log.output?.tokens) {
|
||||
;(span as any).tokens = log.output.tokens
|
||||
logger.debug(`Added tokens to span ${span.id}`, {
|
||||
blockId: log.blockId,
|
||||
blockType: log.blockType,
|
||||
tokens: log.output.tokens,
|
||||
})
|
||||
}
|
||||
|
||||
if (log.output?.model) {
|
||||
;(span as any).model = log.output.model
|
||||
logger.debug(`Added model to span ${span.id}`, {
|
||||
blockId: log.blockId,
|
||||
blockType: log.blockType,
|
||||
model: log.output.model,
|
||||
}
|
||||
|
||||
// Handle child workflow spans for workflow blocks
|
||||
if (
|
||||
log.blockType === 'workflow' &&
|
||||
log.output?.childTraceSpans &&
|
||||
Array.isArray(log.output.childTraceSpans)
|
||||
) {
|
||||
// Convert child trace spans to be direct children of this workflow block span
|
||||
const childTraceSpans = log.output.childTraceSpans as TraceSpan[]
|
||||
|
||||
// Process child workflow spans and add them as children
|
||||
const flatChildSpans: TraceSpan[] = []
|
||||
childTraceSpans.forEach((childSpan) => {
|
||||
// Skip the synthetic workflow span wrapper - we only want the actual block executions
|
||||
if (childSpan.type === 'workflow' && childSpan.name === 'Workflow Execution') {
|
||||
// Add its children directly, skipping the synthetic wrapper
|
||||
if (childSpan.children && Array.isArray(childSpan.children)) {
|
||||
flatChildSpans.push(...childSpan.children)
|
||||
}
|
||||
} else {
|
||||
// This is a regular span, add it directly
|
||||
// But first, ensure nested workflow blocks in this span are also processed
|
||||
const processedSpan = ensureNestedWorkflowsProcessed(childSpan)
|
||||
flatChildSpans.push(processedSpan)
|
||||
}
|
||||
})
|
||||
|
||||
// Add the child spans as children of this workflow block
|
||||
span.children = flatChildSpans
|
||||
}
|
||||
|
||||
// Enhanced approach: Use timeSegments for sequential flow if available
|
||||
@@ -163,20 +178,6 @@ export function buildTraceSpans(result: ExecutionResult): {
|
||||
status: 'success',
|
||||
}
|
||||
})
|
||||
|
||||
logger.debug(
|
||||
`Created ${span.children?.length || 0} sequential segments for span ${span.id}`,
|
||||
{
|
||||
blockId: log.blockId,
|
||||
blockType: log.blockType,
|
||||
segments:
|
||||
span.children?.map((child) => ({
|
||||
name: child.name,
|
||||
type: child.type,
|
||||
duration: child.duration,
|
||||
})) || [],
|
||||
}
|
||||
)
|
||||
} else {
|
||||
// Fallback: Extract tool calls using the original approach for backwards compatibility
|
||||
// Tool calls handling for different formats:
|
||||
@@ -237,12 +238,6 @@ export function buildTraceSpans(result: ExecutionResult): {
|
||||
}
|
||||
})
|
||||
.filter(Boolean) // Remove any null entries from failed processing
|
||||
|
||||
logger.debug(`Added ${span.toolCalls?.length || 0} tool calls to span ${span.id}`, {
|
||||
blockId: log.blockId,
|
||||
blockType: log.blockType,
|
||||
toolCallNames: span.toolCalls?.map((tc) => tc.name) || [],
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -384,6 +379,45 @@ export function buildTraceSpans(result: ExecutionResult): {
|
||||
return { traceSpans: rootSpans, totalDuration }
|
||||
}
|
||||
|
||||
// Helper function to recursively process nested workflow blocks in trace spans
|
||||
function ensureNestedWorkflowsProcessed(span: TraceSpan): TraceSpan {
|
||||
// Create a copy to avoid mutating the original
|
||||
const processedSpan = { ...span }
|
||||
|
||||
// If this is a workflow block and it has childTraceSpans in its output, process them
|
||||
if (
|
||||
span.type === 'workflow' &&
|
||||
span.output?.childTraceSpans &&
|
||||
Array.isArray(span.output.childTraceSpans)
|
||||
) {
|
||||
const childTraceSpans = span.output.childTraceSpans as TraceSpan[]
|
||||
const nestedChildren: TraceSpan[] = []
|
||||
|
||||
childTraceSpans.forEach((childSpan) => {
|
||||
// Skip synthetic workflow wrappers and get the actual blocks
|
||||
if (childSpan.type === 'workflow' && childSpan.name === 'Workflow Execution') {
|
||||
if (childSpan.children && Array.isArray(childSpan.children)) {
|
||||
// Recursively process each child to handle deeper nesting
|
||||
childSpan.children.forEach((grandchildSpan) => {
|
||||
nestedChildren.push(ensureNestedWorkflowsProcessed(grandchildSpan))
|
||||
})
|
||||
}
|
||||
} else {
|
||||
// Regular span, recursively process it for potential deeper nesting
|
||||
nestedChildren.push(ensureNestedWorkflowsProcessed(childSpan))
|
||||
}
|
||||
})
|
||||
|
||||
// Set the processed children on this workflow block
|
||||
processedSpan.children = nestedChildren
|
||||
} else if (span.children && Array.isArray(span.children)) {
|
||||
// Recursively process regular children too
|
||||
processedSpan.children = span.children.map((child) => ensureNestedWorkflowsProcessed(child))
|
||||
}
|
||||
|
||||
return processedSpan
|
||||
}
|
||||
|
||||
export function stripCustomToolPrefix(name: string) {
|
||||
return name.startsWith('custom_') ? name.replace('custom_', '') : name
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user