11import { createLogger } from '@sim/logger'
22import { isRecordLike } from '@sim/utils/object'
3+ import { truncate } from '@sim/utils/string'
34import type { ProviderTiming , TraceSpan } from '@/lib/logs/types'
45import {
56 isConditionBlockType ,
@@ -14,6 +15,7 @@ import type {
1415} from '@/executor/types'
1516
1617const logger = createLogger ( 'SpanFactory' )
18+ const TOOL_CALL_ERROR_MAX_LENGTH = 4096
1719
1820/** A BlockLog that has already passed the id/type validity check. */
1921type ValidBlockLog = BlockLog & { blockType : string }
@@ -24,6 +26,47 @@ function normalizeTraceOutput(value: unknown): Record<string, unknown> | undefin
2426 return isRecordLike ( value ) ? value : { value }
2527}
2628
29+ function nonEmptyString ( value : unknown ) : string | undefined {
30+ return typeof value === 'string' && value . trim ( ) . length > 0 ? value : undefined
31+ }
32+
33+ /**
34+ * Returns the canonical error message for a failed agent tool call.
35+ *
36+ * Providers expose failures through several normalized shapes. The nested
37+ * fallback intentionally requires Sim's complete error envelope so ordinary
38+ * successful tool data with an `error` field is not misclassified.
39+ */
40+ function getToolCallErrorMessage (
41+ toolCall : BlockToolCall | undefined ,
42+ segmentErrorMessage ?: string
43+ ) : string | undefined {
44+ const rawResult = toolCall ?. result ?? toolCall ?. output
45+ const result = isRecordLike ( rawResult ) ? rawResult : undefined
46+ const topLevelError = nonEmptyString ( toolCall ?. error )
47+ const segmentError = nonEmptyString ( segmentErrorMessage )
48+ const hasExplicitFailure =
49+ toolCall ?. success === false ||
50+ toolCall ?. status === 'error' ||
51+ topLevelError !== undefined ||
52+ segmentError !== undefined
53+ const hasStandardSimError =
54+ result ?. error === true &&
55+ nonEmptyString ( result . message ) !== undefined &&
56+ nonEmptyString ( result . tool ) !== undefined
57+
58+ if ( ! hasExplicitFailure && ! hasStandardSimError ) return undefined
59+
60+ const nestedMessage = nonEmptyString ( result ?. message ) ?? nonEmptyString ( result ?. error )
61+ const message =
62+ topLevelError ??
63+ segmentError ??
64+ nestedMessage ??
65+ `Tool ${ toolCall ?. name || 'call' } execution failed`
66+
67+ return truncate ( message , TOOL_CALL_ERROR_MAX_LENGTH )
68+ }
69+
2770/**
2871 * Creates a TraceSpan from a BlockLog. Returns null for invalid logs.
2972 *
@@ -202,6 +245,10 @@ function buildChildrenFromTimeSegments(
202245 const match = callsForName [ currentIndex ]
203246 toolCallIndices . set ( normalizedName , currentIndex + 1 )
204247 const output = normalizeTraceOutput ( match ?. result ?? match ?. output )
248+ const errorMessage = getToolCallErrorMessage ( match , segment . errorMessage )
249+ const errorHandled = Boolean (
250+ errorMessage && span . type === 'agent' && span . status === 'success'
251+ )
205252
206253 const toolChild : TraceSpan = {
207254 id : `${ span . id } -segment-${ index } ` ,
@@ -210,13 +257,14 @@ function buildChildrenFromTimeSegments(
210257 duration : segment . duration ,
211258 startTime : segmentStartTime ,
212259 endTime : segmentEndTime ,
213- status : match ?. error || segment . errorMessage ? 'error' : 'success' ,
260+ status : errorMessage ? 'error' : 'success' ,
214261 input : match ?. arguments ?? match ?. input ,
215262 output : match ?. error ? { error : match . error , ...output } : output ,
263+ ...( errorHandled && { errorHandled : true } ) ,
216264 }
217265 if ( segment . toolCallId ) toolChild . toolCallId = segment . toolCallId
218266 if ( segment . errorType ) toolChild . errorType = segment . errorType
219- if ( segment . errorMessage ) toolChild . errorMessage = segment . errorMessage
267+ if ( errorMessage ) toolChild . errorMessage = errorMessage
220268 return toolChild
221269 }
222270
@@ -280,16 +328,20 @@ function buildChildrenFromToolCalls(span: TraceSpan, log: ValidBlockLog): TraceS
280328 const startTime = tc . startTime ?? log . startedAt
281329 const endTime = tc . endTime ?? log . endedAt
282330 const output = normalizeTraceOutput ( tc . result ?? tc . output )
331+ const errorMessage = getToolCallErrorMessage ( tc )
332+ const errorHandled = Boolean ( errorMessage && span . type === 'agent' && span . status === 'success' )
283333 return {
284334 id : `${ span . id } -tool-${ index } ` ,
285335 name : stripCustomToolPrefix ( tc . name ?? 'unnamed-tool' ) ,
286336 type : 'tool' ,
287337 duration : tc . duration ?? 0 ,
288338 startTime,
289339 endTime,
290- status : tc . error ? 'error' : 'success' ,
340+ status : errorMessage ? 'error' : 'success' ,
291341 input : tc . arguments ?? tc . input ,
292342 output : tc . error ? { error : tc . error , ...output } : output ,
343+ ...( errorMessage && { errorMessage } ) ,
344+ ...( errorHandled && { errorHandled : true } ) ,
293345 }
294346 } )
295347}
0 commit comments