@@ -329,148 +329,246 @@ def _track_file_activity(tool_name: str, tool_input: dict, metadata: dict):
329329 metadata ["web_fetches" ].append (url_or_query )
330330
331331
332- def _parse_tool_result (tool_result , slug : str = None ) -> dict | None :
333- """Figure out what kind of tool result this is (bash, file edit, glob, etc.)
334- by looking at which keys are present, since the JSONL doesn't always tag them."""
335- if not isinstance (tool_result , dict ):
336- return None
332+ def _tool_result_pred_bash (tr : dict ) -> bool :
333+ return "stdout" in tr or "stderr" in tr
334+
335+
336+ def _tool_result_build_bash (tr : dict , base : dict ) -> dict :
337+ result = dict (base )
338+ result ["result_type" ] = "bash"
339+ result ["stdout" ] = tr .get ("stdout" , "" )
340+ result ["stderr" ] = tr .get ("stderr" , "" )
341+ result ["exit_code" ] = tr .get ("exitCode" )
342+ result ["interrupted" ] = tr .get ("interrupted" , False )
343+ result ["is_error" ] = tr .get ("is_error" , False )
344+ result ["return_code_interpretation" ] = tr .get ("returnCodeInterpretation" )
345+ return result
337346
338- result = {"slug" : slug }
339-
340- # Bash results: have stdout/stderr/interrupted
341- if "stdout" in tool_result or "stderr" in tool_result :
342- result ["result_type" ] = "bash"
343- result ["stdout" ] = tool_result .get ("stdout" , "" )
344- result ["stderr" ] = tool_result .get ("stderr" , "" )
345- result ["exit_code" ] = tool_result .get ("exitCode" )
346- result ["interrupted" ] = tool_result .get ("interrupted" , False )
347- result ["is_error" ] = tool_result .get ("is_error" , False )
348- result ["return_code_interpretation" ] = tool_result .get (
349- "returnCodeInterpretation"
350- )
351- return result
352347
353- # File edit results: have filePath + structuredPatch or oldString/newString
354- if "structuredPatch" in tool_result or (
355- "filePath" in tool_result and "newString" in tool_result
356- ):
357- result ["result_type" ] = "file_edit"
358- result ["file_path" ] = tool_result .get ("filePath" , "" )
359- result ["replace_all" ] = tool_result .get ("replaceAll" , False )
360- return result
348+ def _tool_result_pred_file_edit (tr : dict ) -> bool :
349+ return "structuredPatch" in tr or (
350+ "filePath" in tr and "newString" in tr
351+ )
361352
362- # File create/write results: have filePath + content but no patch
363- if "filePath" in tool_result and "content" in tool_result :
364- result ["result_type" ] = "file_write"
365- result ["file_path" ] = tool_result .get ("filePath" , "" )
366- return result
367353
368- # Glob results: have filenames array
369- if "filenames" in tool_result and isinstance (
370- tool_result .get ("filenames" ), list
371- ):
372- result ["result_type" ] = "glob"
373- filenames = tool_result ["filenames" ]
374- result ["num_files" ] = tool_result .get ("numFiles" , len (filenames ))
375- result ["truncated" ] = tool_result .get ("truncated" , False )
376- result ["duration_ms" ] = tool_result .get ("durationMs" )
377- result ["filenames" ] = filenames
378- return result
354+ def _tool_result_build_file_edit (tr : dict , base : dict ) -> dict :
355+ result = dict (base )
356+ result ["result_type" ] = "file_edit"
357+ result ["file_path" ] = tr .get ("filePath" , "" )
358+ result ["replace_all" ] = tr .get ("replaceAll" , False )
359+ return result
379360
380- # Grep results: have mode + numFiles/numLines
381- if "mode" in tool_result and "numFiles" in tool_result :
382- result ["result_type" ] = "grep"
383- result ["mode" ] = tool_result .get ("mode" )
384- result ["num_files" ] = tool_result .get ("numFiles" , 0 )
385- result ["num_lines" ] = tool_result .get ("numLines" , 0 )
386- result ["duration_ms" ] = tool_result .get ("durationMs" )
387- content = tool_result .get ("content" , "" )
388- if content and isinstance (content , str ):
389- result ["content" ] = content
390- return result
391361
392- # Read result: have file dict with content
393- if "file" in tool_result and isinstance (tool_result ["file" ], dict ):
394- file_obj = tool_result ["file" ]
395- result ["result_type" ] = "file_read"
396- result ["file_path" ] = file_obj .get ("filePath" , "" )
397- result ["num_lines" ] = file_obj .get ("numLines" )
398- content = file_obj .get ("content" , "" )
399- if content and isinstance (content , str ):
400- result ["content" ] = content
401- return result
362+ def _tool_result_pred_file_write (tr : dict ) -> bool :
363+ return "filePath" in tr and "content" in tr
402364
403- # WebSearch results
404- if "query" in tool_result and "results" in tool_result :
405- result ["result_type" ] = "web_search"
406- result ["query" ] = tool_result .get ("query" , "" )
407- result ["result_count" ] = len (tool_result .get ("results" , []))
408- result ["duration_seconds" ] = tool_result .get ("durationSeconds" )
409- return result
410365
411- # WebFetch results
412- if "url" in tool_result and "code" in tool_result :
413- result ["result_type" ] = "web_fetch"
414- result ["url" ] = tool_result .get ("url" , "" )
415- result ["status_code" ] = tool_result .get ("code" )
416- result ["duration_ms" ] = tool_result .get ("durationMs" )
417- return result
366+ def _tool_result_build_file_write (tr : dict , base : dict ) -> dict :
367+ result = dict (base )
368+ result ["result_type" ] = "file_write"
369+ result ["file_path" ] = tr .get ("filePath" , "" )
370+ return result
418371
419- # Task results -- multiple variants
420- if "task_id" in tool_result or "message" in tool_result :
421- result ["result_type" ] = "task"
422- result ["task_id" ] = tool_result .get ("task_id" )
423- result ["task_type" ] = tool_result .get ("task_type" )
424- return result
425372
426- # Task retrieval (has nested "task" dict + retrieval_status)
427- if "retrieval_status" in tool_result and "task" in tool_result :
428- result ["result_type" ] = "task"
429- task_obj = tool_result ["task" ] if isinstance (tool_result ["task" ], dict ) else {}
430- result ["retrieval_status" ] = tool_result .get ("retrieval_status" )
431- result ["task_id" ] = task_obj .get ("task_id" )
432- return result
373+ def _tool_result_pred_glob (tr : dict ) -> bool :
374+ return "filenames" in tr and isinstance (tr .get ("filenames" ), list )
433375
434- # Task completed subagent (has agentId + totalDurationMs + status)
435- if "agentId" in tool_result and "totalDurationMs" in tool_result :
436- result ["result_type" ] = "task"
437- result ["agent_id" ] = tool_result .get ("agentId" )
438- result ["status" ] = tool_result .get ("status" )
439- result ["total_duration_ms" ] = tool_result .get ("totalDurationMs" )
440- result ["total_tokens" ] = tool_result .get ("totalTokens" )
441- result ["total_tool_use_count" ] = tool_result .get ("totalToolUseCount" )
442- return result
443376
444- # Task async launched (has agentId + isAsync + status)
445- if "agentId" in tool_result and "isAsync" in tool_result :
446- result ["result_type" ] = "task"
447- result ["agent_id" ] = tool_result .get ("agentId" )
448- result ["status" ] = tool_result .get ("status" )
449- result ["description" ] = tool_result .get ("description" )
450- return result
377+ def _tool_result_build_glob (tr : dict , base : dict ) -> dict :
378+ result = dict (base )
379+ filenames = tr ["filenames" ]
380+ result ["result_type" ] = "glob"
381+ result ["num_files" ] = tr .get ("numFiles" , len (filenames ))
382+ result ["truncated" ] = tr .get ("truncated" , False )
383+ result ["duration_ms" ] = tr .get ("durationMs" )
384+ result ["filenames" ] = filenames
385+ return result
451386
452- # TodoWrite results (has newTodos/oldTodos)
453- if "newTodos" in tool_result or "oldTodos" in tool_result :
454- result ["result_type" ] = "todo_write"
455- new_todos = tool_result .get ("newTodos" , [])
456- result ["todo_count" ] = len (new_todos ) if isinstance (new_todos , list ) else 0
457- result ["todos" ] = new_todos if isinstance (new_todos , list ) else []
458- return result
459387
460- # AskUserQuestion results (has questions/answers)
461- if "questions" in tool_result and "answers" in tool_result :
462- result ["result_type" ] = "user_input"
463- result ["questions" ] = tool_result .get ("questions" , [])
464- result ["answers" ] = tool_result .get ("answers" , {})
465- return result
388+ def _tool_result_pred_grep (tr : dict ) -> bool :
389+ return "mode" in tr and "numFiles" in tr
466390
467- # Plan results (has plan + filePath)
468- if "plan" in tool_result and "filePath" in tool_result :
469- result ["result_type" ] = "plan"
470- result ["file_path" ] = tool_result .get ("filePath" , "" )
471- return result
472391
473- # Generic fallback
392+ def _tool_result_build_grep (tr : dict , base : dict ) -> dict :
393+ result = dict (base )
394+ result ["result_type" ] = "grep"
395+ result ["mode" ] = tr .get ("mode" )
396+ result ["num_files" ] = tr .get ("numFiles" , 0 )
397+ result ["num_lines" ] = tr .get ("numLines" , 0 )
398+ result ["duration_ms" ] = tr .get ("durationMs" )
399+ content = tr .get ("content" , "" )
400+ if content and isinstance (content , str ):
401+ result ["content" ] = content
402+ return result
403+
404+
405+ def _tool_result_pred_file_read (tr : dict ) -> bool :
406+ return "file" in tr and isinstance (tr ["file" ], dict )
407+
408+
409+ def _tool_result_build_file_read (tr : dict , base : dict ) -> dict :
410+ result = dict (base )
411+ file_obj = tr ["file" ]
412+ result ["result_type" ] = "file_read"
413+ result ["file_path" ] = file_obj .get ("filePath" , "" )
414+ result ["num_lines" ] = file_obj .get ("numLines" )
415+ content = file_obj .get ("content" , "" )
416+ if content and isinstance (content , str ):
417+ result ["content" ] = content
418+ return result
419+
420+
421+ def _tool_result_pred_web_search (tr : dict ) -> bool :
422+ return "query" in tr and "results" in tr
423+
424+
425+ def _tool_result_build_web_search (tr : dict , base : dict ) -> dict :
426+ result = dict (base )
427+ result ["result_type" ] = "web_search"
428+ result ["query" ] = tr .get ("query" , "" )
429+ result ["result_count" ] = len (tr .get ("results" , []))
430+ result ["duration_seconds" ] = tr .get ("durationSeconds" )
431+ return result
432+
433+
434+ def _tool_result_pred_web_fetch (tr : dict ) -> bool :
435+ return "url" in tr and "code" in tr
436+
437+
438+ def _tool_result_build_web_fetch (tr : dict , base : dict ) -> dict :
439+ result = dict (base )
440+ result ["result_type" ] = "web_fetch"
441+ result ["url" ] = tr .get ("url" , "" )
442+ result ["status_code" ] = tr .get ("code" )
443+ result ["duration_ms" ] = tr .get ("durationMs" )
444+ return result
445+
446+
447+ def _tool_result_pred_task_message (tr : dict ) -> bool :
448+ return "task_id" in tr or "message" in tr
449+
450+
451+ def _tool_result_build_task_message (tr : dict , base : dict ) -> dict :
452+ result = dict (base )
453+ result ["result_type" ] = "task"
454+ result ["task_id" ] = tr .get ("task_id" )
455+ result ["task_type" ] = tr .get ("task_type" )
456+ return result
457+
458+
459+ def _tool_result_pred_task_retrieval (tr : dict ) -> bool :
460+ return "retrieval_status" in tr and "task" in tr
461+
462+
463+ def _tool_result_build_task_retrieval (tr : dict , base : dict ) -> dict :
464+ result = dict (base )
465+ task_obj = tr ["task" ] if isinstance (tr ["task" ], dict ) else {}
466+ result ["result_type" ] = "task"
467+ result ["retrieval_status" ] = tr .get ("retrieval_status" )
468+ result ["task_id" ] = task_obj .get ("task_id" )
469+ return result
470+
471+
472+ def _tool_result_pred_task_completed (tr : dict ) -> bool :
473+ return "agentId" in tr and "totalDurationMs" in tr
474+
475+
476+ def _tool_result_build_task_completed (tr : dict , base : dict ) -> dict :
477+ result = dict (base )
478+ result ["result_type" ] = "task"
479+ result ["agent_id" ] = tr .get ("agentId" )
480+ result ["status" ] = tr .get ("status" )
481+ result ["total_duration_ms" ] = tr .get ("totalDurationMs" )
482+ result ["total_tokens" ] = tr .get ("totalTokens" )
483+ result ["total_tool_use_count" ] = tr .get ("totalToolUseCount" )
484+ return result
485+
486+
487+ def _tool_result_pred_task_async (tr : dict ) -> bool :
488+ return "agentId" in tr and "isAsync" in tr
489+
490+
491+ def _tool_result_build_task_async (tr : dict , base : dict ) -> dict :
492+ result = dict (base )
493+ result ["result_type" ] = "task"
494+ result ["agent_id" ] = tr .get ("agentId" )
495+ result ["status" ] = tr .get ("status" )
496+ result ["description" ] = tr .get ("description" )
497+ return result
498+
499+
500+ def _tool_result_pred_todo_write (tr : dict ) -> bool :
501+ return "newTodos" in tr or "oldTodos" in tr
502+
503+
504+ def _tool_result_build_todo_write (tr : dict , base : dict ) -> dict :
505+ result = dict (base )
506+ new_todos = tr .get ("newTodos" , [])
507+ result ["result_type" ] = "todo_write"
508+ result ["todo_count" ] = len (new_todos ) if isinstance (new_todos , list ) else 0
509+ result ["todos" ] = new_todos if isinstance (new_todos , list ) else []
510+ return result
511+
512+
513+ def _tool_result_pred_user_input (tr : dict ) -> bool :
514+ return "questions" in tr and "answers" in tr
515+
516+
517+ def _tool_result_build_user_input (tr : dict , base : dict ) -> dict :
518+ result = dict (base )
519+ result ["result_type" ] = "user_input"
520+ result ["questions" ] = tr .get ("questions" , [])
521+ result ["answers" ] = tr .get ("answers" , {})
522+ return result
523+
524+
525+ def _tool_result_pred_plan (tr : dict ) -> bool :
526+ return "plan" in tr and "filePath" in tr
527+
528+
529+ def _tool_result_build_plan (tr : dict , base : dict ) -> dict :
530+ result = dict (base )
531+ result ["result_type" ] = "plan"
532+ result ["file_path" ] = tr .get ("filePath" , "" )
533+ return result
534+
535+
536+ # Ordered dispatch: first matching predicate wins (legacy if/elif semantics).
537+ _TOOL_RESULT_DISPATCH = (
538+ (_tool_result_pred_bash , _tool_result_build_bash ),
539+ (_tool_result_pred_file_edit , _tool_result_build_file_edit ),
540+ (_tool_result_pred_file_write , _tool_result_build_file_write ),
541+ (_tool_result_pred_glob , _tool_result_build_glob ),
542+ (_tool_result_pred_grep , _tool_result_build_grep ),
543+ (_tool_result_pred_file_read , _tool_result_build_file_read ),
544+ (_tool_result_pred_web_search , _tool_result_build_web_search ),
545+ (_tool_result_pred_web_fetch , _tool_result_build_web_fetch ),
546+ (_tool_result_pred_task_message , _tool_result_build_task_message ),
547+ (_tool_result_pred_task_retrieval , _tool_result_build_task_retrieval ),
548+ (_tool_result_pred_task_completed , _tool_result_build_task_completed ),
549+ (_tool_result_pred_task_async , _tool_result_build_task_async ),
550+ (_tool_result_pred_todo_write , _tool_result_build_todo_write ),
551+ (_tool_result_pred_user_input , _tool_result_build_user_input ),
552+ (_tool_result_pred_plan , _tool_result_build_plan ),
553+ )
554+
555+
556+ def _parse_tool_result (tool_result , slug : str = None ) -> dict | None :
557+ """Figure out what kind of tool result this is (bash, file edit, glob, etc.)
558+ by looking at which keys are present, since the JSONL doesn't always tag them.
559+
560+ Classification uses ``_TOOL_RESULT_DISPATCH``: append ``(predicate, builder)``
561+ pairs to register a new shape; keep order consistent with Claude Code JSONL
562+ evolution (more specific branches before generic ones)."""
563+ if not isinstance (tool_result , dict ):
564+ return None
565+
566+ base = {"slug" : slug }
567+ for pred , build in _TOOL_RESULT_DISPATCH :
568+ if pred (tool_result ):
569+ return build (tool_result , base )
570+
571+ result = dict (base )
474572 result ["result_type" ] = "unknown"
475573 return result
476574
0 commit comments