fix(logs): surface handled errors as info in logs (#3190)

* fix(logs): surface handled errors as info in logs

* pr comments
This commit is contained in:
Waleed
2026-02-10 21:21:45 -08:00
committed by GitHub
parent af01dce2c3
commit 13a91113fd
11 changed files with 586 additions and 21 deletions

View File

@@ -67,7 +67,7 @@ function parseTime(value?: string | number | null): number {
}
/**
* Checks if a span or any of its descendants has an error
* Checks if a span or any of its descendants has an error (any error).
*/
function hasErrorInTree(span: TraceSpan): boolean {
if (span.status === 'error') return true
@@ -80,6 +80,23 @@ function hasErrorInTree(span: TraceSpan): boolean {
return false
}
/**
* Checks if a span or any of its descendants has an unhandled error.
* Spans with errorHandled: true (including containers that propagate it)
* are skipped. Used only for the root workflow span to match the actual
* workflow status.
*/
function hasUnhandledErrorInTree(span: TraceSpan): boolean {
if (span.status === 'error' && !span.errorHandled) return true
if (span.children && span.children.length > 0) {
return span.children.some((child) => hasUnhandledErrorInTree(child))
}
if (span.toolCalls && span.toolCalls.length > 0 && !span.errorHandled) {
return span.toolCalls.some((tc) => tc.error)
}
return false
}
/**
* Normalizes and sorts trace spans recursively.
* Deduplicates children and sorts by start time.
@@ -478,14 +495,13 @@ const TraceSpanNode = memo(function TraceSpanNode({
const duration = span.duration || spanEndTime - spanStartTime
const isDirectError = span.status === 'error'
const hasNestedError = hasErrorInTree(span)
const isRootWorkflow = depth === 0
const isRootWorkflowSpan = isRootWorkflow && span.type?.toLowerCase() === 'workflow'
const hasNestedError = isRootWorkflowSpan ? hasUnhandledErrorInTree(span) : hasErrorInTree(span)
const showErrorStyle = isDirectError || hasNestedError
const { icon: BlockIcon, bgColor } = getBlockIconAndColor(span.type, span.name)
// Root workflow execution is always expanded and has no toggle
const isRootWorkflow = depth === 0
// Build all children including tool calls
const allChildren = useMemo(() => {
const children: TraceSpan[] = []

View File

@@ -95,7 +95,6 @@ const WorkflowEdgeComponent = ({
color = 'var(--brand-tertiary-2)'
} else if (edgeRunStatus === 'success') {
// Use green for preview mode, default for canvas execution
// This also applies to error edges that were taken (error path executed)
color = previewExecutionStatus ? 'var(--brand-tertiary-2)' : 'var(--border-success)'
} else if (edgeRunStatus === 'error') {
color = 'var(--text-error)'

View File

@@ -55,7 +55,7 @@ interface DebugValidationResult {
interface BlockEventHandlerConfig {
workflowId?: string
executionId?: string
workflowEdges: Array<{ id: string; target: string }>
workflowEdges: Array<{ id: string; target: string; sourceHandle?: string | null }>
activeBlocksSet: Set<string>
accumulatedBlockLogs: BlockLog[]
accumulatedBlockStates: Map<string, BlockState>
@@ -322,7 +322,8 @@ export function useWorkflowExecution() {
if (!workflowId) return
const incomingEdges = workflowEdges.filter((edge) => edge.target === blockId)
incomingEdges.forEach((edge) => {
setEdgeRunStatus(workflowId, edge.id, 'success')
const status = edge.sourceHandle === 'error' ? 'error' : 'success'
setEdgeRunStatus(workflowId, edge.id, status)
})
}

View File

@@ -292,6 +292,9 @@ export class BlockExecutor {
const hasErrorPort = this.hasErrorPortEdge(node)
if (hasErrorPort) {
if (blockLog) {
blockLog.errorHandled = true
}
logger.info('Block has error port - returning error output instead of throwing', {
blockId: node.id,
error: errorMessage,

View File

@@ -112,6 +112,8 @@ export interface BlockLog {
output?: any
input?: any
error?: string
/** Whether this error was handled by an error handler path (error port) */
errorHandled?: boolean
loopId?: string
parallelId?: string
iterationIndex?: number

View File

@@ -219,11 +219,12 @@ export class ExecutionLogger implements IExecutionLoggerService {
| { traceSpans?: TraceSpan[] }
| undefined
// Determine if workflow failed by checking trace spans for errors
// Determine if workflow failed by checking trace spans for unhandled errors
// Errors handled by error handler paths (errorHandled: true) don't count as workflow failures
// Use the override if provided (for cost-only fallback scenarios)
const hasErrors = traceSpans?.some((span: any) => {
const checkSpanForErrors = (s: any): boolean => {
if (s.status === 'error') return true
if (s.status === 'error' && !s.errorHandled) return true
if (s.children && Array.isArray(s.children)) {
return s.children.some(checkSpanForErrors)
}

View File

@@ -301,7 +301,7 @@ export class LoggingSession {
const hasErrors = traceSpans.some((span: any) => {
const checkForErrors = (s: any): boolean => {
if (s.status === 'error') return true
if (s.status === 'error' && !s.errorHandled) return true
if (s.children && Array.isArray(s.children)) {
return s.children.some(checkForErrors)
}

View File

@@ -990,6 +990,535 @@ describe('buildTraceSpans', () => {
})
})
describe('errorHandled - handled errors should not bubble up', () => {
it.concurrent('block span stays error but is marked errorHandled', () => {
const result: ExecutionResult = {
success: true,
output: { content: 'done' },
logs: [
{
blockId: 'api-1',
blockName: 'API Call',
blockType: 'api',
startedAt: '2024-01-01T10:00:00.000Z',
endedAt: '2024-01-01T10:00:01.000Z',
durationMs: 1000,
success: false,
error: 'Request failed with status 500',
errorHandled: true,
executionOrder: 1,
},
{
blockId: 'fallback-1',
blockName: 'Fallback',
blockType: 'function',
startedAt: '2024-01-01T10:00:01.000Z',
endedAt: '2024-01-01T10:00:02.000Z',
durationMs: 1000,
success: true,
executionOrder: 2,
},
],
}
const { traceSpans } = buildTraceSpans(result)
const apiSpan = traceSpans.find((s) => s.blockId === 'api-1')!
expect(apiSpan.status).toBe('error')
expect(apiSpan.errorHandled).toBe(true)
expect((apiSpan.output as { error?: string }).error).toBe('Request failed with status 500')
const fallbackSpan = traceSpans.find((s) => s.blockId === 'fallback-1')!
expect(fallbackSpan.status).toBe('success')
expect(fallbackSpan.errorHandled).toBeUndefined()
})
it.concurrent('unhandled errors still produce error status', () => {
const result: ExecutionResult = {
success: false,
output: {},
metadata: { duration: 1000, startTime: '2024-01-01T10:00:00.000Z' },
logs: [
{
blockId: 'api-1',
blockName: 'API Call',
blockType: 'api',
startedAt: '2024-01-01T10:00:00.000Z',
endedAt: '2024-01-01T10:00:01.000Z',
durationMs: 1000,
success: false,
error: 'Request failed with status 500',
executionOrder: 1,
},
],
}
const { traceSpans } = buildTraceSpans(result)
const workflowSpan = traceSpans[0]
expect(workflowSpan.name).toBe('Workflow Execution')
expect(workflowSpan.status).toBe('error')
const apiSpan = workflowSpan.children![0]
expect(apiSpan.status).toBe('error')
expect(apiSpan.errorHandled).toBeUndefined()
})
it.concurrent('workflow-level span is success when all errors are handled', () => {
const result: ExecutionResult = {
success: true,
output: { content: 'recovered' },
metadata: { duration: 2000, startTime: '2024-01-01T10:00:00.000Z' },
logs: [
{
blockId: 'api-1',
blockName: 'API Call',
blockType: 'api',
startedAt: '2024-01-01T10:00:00.000Z',
endedAt: '2024-01-01T10:00:01.000Z',
durationMs: 1000,
success: false,
error: 'Connection timeout',
errorHandled: true,
executionOrder: 1,
},
{
blockId: 'handler-1',
blockName: 'Error Handler',
blockType: 'function',
startedAt: '2024-01-01T10:00:01.000Z',
endedAt: '2024-01-01T10:00:02.000Z',
durationMs: 1000,
success: true,
executionOrder: 2,
},
],
}
const { traceSpans } = buildTraceSpans(result)
const workflowSpan = traceSpans[0]
expect(workflowSpan.name).toBe('Workflow Execution')
expect(workflowSpan.status).toBe('success')
})
it.concurrent(
'workflow-level span is error when there is a mix of handled and unhandled errors',
() => {
const result: ExecutionResult = {
success: false,
output: {},
metadata: { duration: 3000, startTime: '2024-01-01T10:00:00.000Z' },
logs: [
{
blockId: 'api-1',
blockName: 'API Call (handled)',
blockType: 'api',
startedAt: '2024-01-01T10:00:00.000Z',
endedAt: '2024-01-01T10:00:01.000Z',
durationMs: 1000,
success: false,
error: 'Handled error',
errorHandled: true,
executionOrder: 1,
},
{
blockId: 'handler-1',
blockName: 'Error Handler',
blockType: 'function',
startedAt: '2024-01-01T10:00:01.000Z',
endedAt: '2024-01-01T10:00:02.000Z',
durationMs: 1000,
success: true,
executionOrder: 2,
},
{
blockId: 'api-2',
blockName: 'API Call (unhandled)',
blockType: 'api',
startedAt: '2024-01-01T10:00:02.000Z',
endedAt: '2024-01-01T10:00:03.000Z',
durationMs: 1000,
success: false,
error: 'Unhandled crash',
executionOrder: 3,
},
],
}
const { traceSpans } = buildTraceSpans(result)
const workflowSpan = traceSpans[0]
expect(workflowSpan.name).toBe('Workflow Execution')
expect(workflowSpan.status).toBe('error')
const handledSpan = workflowSpan.children!.find((s) => s.blockId === 'api-1')!
expect(handledSpan.status).toBe('error')
expect(handledSpan.errorHandled).toBe(true)
const unhandledSpan = workflowSpan.children!.find((s) => s.blockId === 'api-2')!
expect(unhandledSpan.status).toBe('error')
expect(unhandledSpan.errorHandled).toBeUndefined()
}
)
it.concurrent(
'handled errors inside loop iterations still show error on loop but not on workflow',
() => {
const result: ExecutionResult = {
success: true,
output: { content: 'all iterations recovered' },
metadata: { duration: 5000, startTime: '2024-01-01T10:00:00.000Z' },
logs: [
{
blockId: 'api-1',
blockName: 'API Call (iteration 0)',
blockType: 'api',
startedAt: '2024-01-01T10:00:00.000Z',
endedAt: '2024-01-01T10:00:01.000Z',
durationMs: 1000,
success: false,
error: 'Rate limited',
errorHandled: true,
loopId: 'loop-1',
iterationIndex: 0,
executionOrder: 1,
},
{
blockId: 'handler-1',
blockName: 'Rate Limit Handler (iteration 0)',
blockType: 'function',
startedAt: '2024-01-01T10:00:01.000Z',
endedAt: '2024-01-01T10:00:02.000Z',
durationMs: 1000,
success: true,
loopId: 'loop-1',
iterationIndex: 0,
executionOrder: 2,
},
{
blockId: 'api-1',
blockName: 'API Call (iteration 1)',
blockType: 'api',
startedAt: '2024-01-01T10:00:02.000Z',
endedAt: '2024-01-01T10:00:03.000Z',
durationMs: 1000,
success: true,
loopId: 'loop-1',
iterationIndex: 1,
executionOrder: 3,
},
{
blockId: 'api-1',
blockName: 'API Call (iteration 2)',
blockType: 'api',
startedAt: '2024-01-01T10:00:03.000Z',
endedAt: '2024-01-01T10:00:04.000Z',
durationMs: 1000,
success: false,
error: 'Rate limited again',
errorHandled: true,
loopId: 'loop-1',
iterationIndex: 2,
executionOrder: 4,
},
{
blockId: 'handler-1',
blockName: 'Rate Limit Handler (iteration 2)',
blockType: 'function',
startedAt: '2024-01-01T10:00:04.000Z',
endedAt: '2024-01-01T10:00:05.000Z',
durationMs: 1000,
success: true,
loopId: 'loop-1',
iterationIndex: 2,
executionOrder: 5,
},
],
}
const { traceSpans } = buildTraceSpans(result)
const workflowSpan = traceSpans[0]
expect(workflowSpan.name).toBe('Workflow Execution')
expect(workflowSpan.status).toBe('success')
const loopSpan = workflowSpan.children!.find((s) => s.type === 'loop')!
expect(loopSpan).toBeDefined()
expect(loopSpan.status).toBe('error')
const iterations = loopSpan.children!
expect(iterations).toHaveLength(3)
expect(iterations[0].status).toBe('error')
expect(iterations[1].status).toBe('success')
expect(iterations[2].status).toBe('error')
}
)
it.concurrent(
'handled errors inside parallel iterations still show error on parallel but not on workflow',
() => {
const result: ExecutionResult = {
success: true,
output: { content: 'parallel done' },
metadata: { duration: 2000, startTime: '2024-01-01T10:00:00.000Z' },
logs: [
{
blockId: 'api-1',
blockName: 'API Call (iteration 0)',
blockType: 'api',
startedAt: '2024-01-01T10:00:00.000Z',
endedAt: '2024-01-01T10:00:01.000Z',
durationMs: 1000,
success: false,
error: 'Timeout on iteration 0',
errorHandled: true,
parallelId: 'parallel-1',
iterationIndex: 0,
executionOrder: 1,
},
{
blockId: 'api-1',
blockName: 'API Call (iteration 1)',
blockType: 'api',
startedAt: '2024-01-01T10:00:01.000Z',
endedAt: '2024-01-01T10:00:02.000Z',
durationMs: 1000,
success: true,
parallelId: 'parallel-1',
iterationIndex: 1,
executionOrder: 2,
},
],
}
const { traceSpans } = buildTraceSpans(result)
const workflowSpan = traceSpans[0]
expect(workflowSpan.name).toBe('Workflow Execution')
expect(workflowSpan.status).toBe('success')
const parallelSpan = workflowSpan.children!.find((s) => s.type === 'parallel')!
expect(parallelSpan).toBeDefined()
expect(parallelSpan.status).toBe('error')
const iterations = parallelSpan.children!
expect(iterations).toHaveLength(2)
expect(iterations[0].status).toBe('error')
expect(iterations[1].status).toBe('success')
}
)
it.concurrent(
'unhandled error in one loop iteration still makes the loop and workflow error',
() => {
const result: ExecutionResult = {
success: false,
output: {},
metadata: { duration: 2000, startTime: '2024-01-01T10:00:00.000Z' },
logs: [
{
blockId: 'api-1',
blockName: 'API Call (iteration 0)',
blockType: 'api',
startedAt: '2024-01-01T10:00:00.000Z',
endedAt: '2024-01-01T10:00:01.000Z',
durationMs: 1000,
success: true,
loopId: 'loop-1',
iterationIndex: 0,
executionOrder: 1,
},
{
blockId: 'api-1',
blockName: 'API Call (iteration 1)',
blockType: 'api',
startedAt: '2024-01-01T10:00:01.000Z',
endedAt: '2024-01-01T10:00:02.000Z',
durationMs: 1000,
success: false,
error: 'Unhandled crash in iteration 1',
loopId: 'loop-1',
iterationIndex: 1,
executionOrder: 2,
},
],
}
const { traceSpans } = buildTraceSpans(result)
const workflowSpan = traceSpans[0]
expect(workflowSpan.name).toBe('Workflow Execution')
expect(workflowSpan.status).toBe('error')
const loopSpan = workflowSpan.children!.find((s) => s.type === 'loop')!
expect(loopSpan.status).toBe('error')
const iterations = loopSpan.children!
expect(iterations[0].status).toBe('success')
expect(iterations[1].status).toBe('error')
}
)
it.concurrent('error output is preserved on the span even when error is handled', () => {
const result: ExecutionResult = {
success: true,
output: { content: 'recovered' },
logs: [
{
blockId: 'api-1',
blockName: 'Flaky API',
blockType: 'api',
startedAt: '2024-01-01T10:00:00.000Z',
endedAt: '2024-01-01T10:00:01.000Z',
durationMs: 1000,
success: false,
error: 'ECONNRESET',
errorHandled: true,
output: { error: 'ECONNRESET' },
executionOrder: 1,
},
],
}
const { traceSpans } = buildTraceSpans(result)
const apiSpan = traceSpans[0]
expect(apiSpan.status).toBe('error')
expect(apiSpan.errorHandled).toBe(true)
expect((apiSpan.output as { error?: string }).error).toBe('ECONNRESET')
})
it.concurrent('block with error and errorHandled=false is treated as unhandled', () => {
const result: ExecutionResult = {
success: false,
output: {},
metadata: { duration: 1000, startTime: '2024-01-01T10:00:00.000Z' },
logs: [
{
blockId: 'api-1',
blockName: 'API Call',
blockType: 'api',
startedAt: '2024-01-01T10:00:00.000Z',
endedAt: '2024-01-01T10:00:01.000Z',
durationMs: 1000,
success: false,
error: 'Server error',
errorHandled: false,
executionOrder: 1,
},
],
}
const { traceSpans } = buildTraceSpans(result)
const workflowSpan = traceSpans[0]
expect(workflowSpan.name).toBe('Workflow Execution')
expect(workflowSpan.status).toBe('error')
const apiSpan = workflowSpan.children![0]
expect(apiSpan.status).toBe('error')
})
it.concurrent('many loop iterations with handled errors produce a successful workflow', () => {
const logs = []
for (let i = 0; i < 10; i++) {
const startMs = 1704103200000 + i * 2000
if (i % 3 === 0) {
logs.push({
blockId: 'api-1',
blockName: `API Call (iteration ${i})`,
blockType: 'api',
startedAt: new Date(startMs).toISOString(),
endedAt: new Date(startMs + 1000).toISOString(),
durationMs: 1000,
success: false,
error: `Error in iteration ${i}`,
errorHandled: true,
loopId: 'loop-1',
iterationIndex: i,
executionOrder: i * 2 + 1,
})
logs.push({
blockId: 'handler-1',
blockName: `Error Handler (iteration ${i})`,
blockType: 'function',
startedAt: new Date(startMs + 1000).toISOString(),
endedAt: new Date(startMs + 2000).toISOString(),
durationMs: 1000,
success: true,
loopId: 'loop-1',
iterationIndex: i,
executionOrder: i * 2 + 2,
})
} else {
logs.push({
blockId: 'api-1',
blockName: `API Call (iteration ${i})`,
blockType: 'api',
startedAt: new Date(startMs).toISOString(),
endedAt: new Date(startMs + 1000).toISOString(),
durationMs: 1000,
success: true,
loopId: 'loop-1',
iterationIndex: i,
executionOrder: i * 2 + 1,
})
}
}
const result: ExecutionResult = {
success: true,
output: { content: 'all done' },
metadata: { duration: 20000, startTime: '2024-01-01T10:00:00.000Z' },
logs: logs as any,
}
const { traceSpans } = buildTraceSpans(result)
const workflowSpan = traceSpans[0]
expect(workflowSpan.name).toBe('Workflow Execution')
expect(workflowSpan.status).toBe('success')
const loopSpan = workflowSpan.children!.find((s) => s.type === 'loop')!
expect(loopSpan).toBeDefined()
expect(loopSpan.status).toBe('error')
loopSpan.children!.forEach((iteration, i) => {
if (i % 3 === 0) {
expect(iteration.status).toBe('error')
} else {
expect(iteration.status).toBe('success')
}
})
})
it.concurrent('successful blocks without errors have no errorHandled flag', () => {
const result: ExecutionResult = {
success: true,
output: { content: 'fine' },
logs: [
{
blockId: 'text-1',
blockName: 'Text Block',
blockType: 'text',
startedAt: '2024-01-01T10:00:00.000Z',
endedAt: '2024-01-01T10:00:01.000Z',
durationMs: 1000,
success: true,
executionOrder: 1,
},
],
}
const { traceSpans } = buildTraceSpans(result)
const span = traceSpans[0]
expect(span.status).toBe('success')
expect(span.errorHandled).toBeUndefined()
})
})
describe('stripCustomToolPrefix', () => {
it.concurrent('strips custom_ prefix from tool names', () => {
expect(stripCustomToolPrefix('custom_test_tool')).toBe('test_tool')

View File

@@ -156,6 +156,7 @@ export function buildTraceSpans(result: ExecutionResult): {
output: output,
...(childWorkflowSnapshotId ? { childWorkflowSnapshotId } : {}),
...(childWorkflowId ? { childWorkflowId } : {}),
...(log.errorHandled && { errorHandled: true }),
...(log.loopId && { loopId: log.loopId }),
...(log.parallelId && { parallelId: log.parallelId }),
...(log.iterationIndex !== undefined && { iterationIndex: log.iterationIndex }),
@@ -501,15 +502,11 @@ export function buildTraceSpans(result: ExecutionResult): {
}
addRelativeTimestamps(groupedRootSpans, earliestStart)
const hasErrors = groupedRootSpans.some((span) => {
if (span.status === 'error') return true
const checkChildren = (children: TraceSpan[] = []): boolean => {
return children.some(
(child) => child.status === 'error' || (child.children && checkChildren(child.children))
)
}
return span.children && checkChildren(span.children)
})
const checkForUnhandledErrors = (s: TraceSpan): boolean => {
if (s.status === 'error' && !s.errorHandled) return true
return s.children ? s.children.some(checkForUnhandledErrors) : false
}
const hasUnhandledErrors = groupedRootSpans.some(checkForUnhandledErrors)
const workflowSpan: TraceSpan = {
id: 'workflow-execution',
@@ -518,7 +515,7 @@ export function buildTraceSpans(result: ExecutionResult): {
duration: actualWorkflowDuration, // Always use actual duration for the span
startTime: new Date(earliestStart).toISOString(),
endTime: new Date(latestEnd).toISOString(),
status: hasErrors ? 'error' : 'success',
status: hasUnhandledErrors ? 'error' : 'success',
children: groupedRootSpans,
}
@@ -710,6 +707,8 @@ function groupIterationBlocks(spans: TraceSpan[]): TraceSpan[] {
const iterDuration = iterLatestEnd - iterEarliestStart
const hasErrors = spans.some((span) => span.status === 'error')
const allErrorsHandled =
hasErrors && spans.every((span) => span.status !== 'error' || span.errorHandled)
const iterationSpan: TraceSpan = {
id: `${containerId}-iteration-${iterationIndex}`,
@@ -719,6 +718,7 @@ function groupIterationBlocks(spans: TraceSpan[]): TraceSpan[] {
startTime: new Date(iterEarliestStart).toISOString(),
endTime: new Date(iterLatestEnd).toISOString(),
status: hasErrors ? 'error' : 'success',
...(allErrorsHandled && { errorHandled: true }),
children: spans.map((span) => ({
...span,
name: span.name.replace(/ \(iteration \d+\)$/, ''),
@@ -729,6 +729,9 @@ function groupIterationBlocks(spans: TraceSpan[]): TraceSpan[] {
})
const hasErrors = allIterationSpans.some((span) => span.status === 'error')
const allErrorsHandled =
hasErrors &&
iterationChildren.every((span) => span.status !== 'error' || span.errorHandled)
const parallelContainer: TraceSpan = {
id: `parallel-execution-${containerId}`,
name: containerName,
@@ -737,6 +740,7 @@ function groupIterationBlocks(spans: TraceSpan[]): TraceSpan[] {
startTime: new Date(earliestStart).toISOString(),
endTime: new Date(latestEnd).toISOString(),
status: hasErrors ? 'error' : 'success',
...(allErrorsHandled && { errorHandled: true }),
children: iterationChildren,
}
@@ -762,6 +766,8 @@ function groupIterationBlocks(spans: TraceSpan[]): TraceSpan[] {
const iterDuration = iterLatestEnd - iterEarliestStart
const hasErrors = spans.some((span) => span.status === 'error')
const allErrorsHandled =
hasErrors && spans.every((span) => span.status !== 'error' || span.errorHandled)
const iterationSpan: TraceSpan = {
id: `${containerId}-iteration-${iterationIndex}`,
@@ -771,6 +777,7 @@ function groupIterationBlocks(spans: TraceSpan[]): TraceSpan[] {
startTime: new Date(iterEarliestStart).toISOString(),
endTime: new Date(iterLatestEnd).toISOString(),
status: hasErrors ? 'error' : 'success',
...(allErrorsHandled && { errorHandled: true }),
children: spans.map((span) => ({
...span,
name: span.name.replace(/ \(iteration \d+\)$/, ''),
@@ -781,6 +788,9 @@ function groupIterationBlocks(spans: TraceSpan[]): TraceSpan[] {
})
const hasErrors = allIterationSpans.some((span) => span.status === 'error')
const allErrorsHandled =
hasErrors &&
iterationChildren.every((span) => span.status !== 'error' || span.errorHandled)
const loopContainer: TraceSpan = {
id: `loop-execution-${containerId}`,
name: containerName,
@@ -789,6 +799,7 @@ function groupIterationBlocks(spans: TraceSpan[]): TraceSpan[] {
startTime: new Date(earliestStart).toISOString(),
endTime: new Date(latestEnd).toISOString(),
status: hasErrors ? 'error' : 'success',
...(allErrorsHandled && { errorHandled: true }),
children: iterationChildren,
}

View File

@@ -175,6 +175,8 @@ export interface TraceSpan {
children?: TraceSpan[]
toolCalls?: ToolCall[]
status?: 'success' | 'error'
/** Whether this block's error was handled by an error handler path */
errorHandled?: boolean
tokens?: number | TokenInfo
relativeStartMs?: number
blockId?: string

View File

@@ -86,6 +86,7 @@ export interface TraceSpan {
children?: TraceSpan[]
toolCalls?: ToolCall[]
status?: 'success' | 'error'
errorHandled?: boolean
tokens?: number | TokenInfo
relativeStartMs?: number // Time in ms from the start of the parent span
blockId?: string // Added to track the original block ID for relationship mapping