Skip to content

Commit ce95de9

Browse files
refactor(jsonl-parser): replace _parse_tool_result if/elif with dispatch table
Implement _TOOL_RESULT_DISPATCH as an ordered sequence of (predicate, builder) pairs with explicit _tool_result_pred_* / _tool_result_build_* helpers so new tool-result shapes register in one place. Preserve legacy branch order and unknown fallback; no intentional behavior change.
1 parent 09de881 commit ce95de9

1 file changed

Lines changed: 224 additions & 126 deletions

File tree

utils/jsonl_parser.py

Lines changed: 224 additions & 126 deletions
Original file line numberDiff line numberDiff line change
@@ -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

Comments
 (0)