@@ -394,13 +394,130 @@ def load_run_candidates(thread_id: str, limit: int = 20) -> list[dict]:
394394 ]
395395
396396
397+ def _msg_text (content : object ) -> str :
398+ if isinstance (content , str ):
399+ return content
400+ if isinstance (content , list ):
401+ texts : list [str ] = []
402+ for block in content :
403+ if isinstance (block , dict ) and block .get ("type" ) == "text" :
404+ texts .append (str (block .get ("text" , "" )))
405+ return "" .join (texts )
406+ return str (content or "" )
407+
408+
409+ def _load_checkpoint_events (thread_id : str , limit : int ) -> tuple [list [dict ], dict [str , int ]]:
410+ with sqlite3 .connect (str (DB_PATH )) as conn :
411+ row = conn .execute (
412+ "SELECT checkpoint FROM checkpoints WHERE thread_id=? ORDER BY rowid DESC LIMIT 1" ,
413+ (thread_id ,),
414+ ).fetchone ()
415+ if not row :
416+ return [], {}
417+
418+ from langgraph .checkpoint .serde .jsonplus import JsonPlusSerializer
419+
420+ checkpoint_blob = row [0 ]
421+ serde = JsonPlusSerializer ()
422+ checkpoint = serde .loads_typed (("msgpack" , checkpoint_blob ))
423+ messages = checkpoint .get ("channel_values" , {}).get ("messages" , [])
424+
425+ call_name_by_id : dict [str , str ] = {}
426+ events : list [dict ] = []
427+ counts : dict [str , int ] = {}
428+ seq = 1
429+ for msg in messages :
430+ cls = msg .__class__ .__name__
431+ if cls == "AIMessage" :
432+ text = _msg_text (getattr (msg , "content" , "" ))
433+ if text .strip ():
434+ payload = {"content" : text , "_seq" : seq , "_run_id" : "checkpoint" }
435+ events .append (
436+ {
437+ "seq" : seq ,
438+ "event_type" : "text" ,
439+ "payload" : payload ,
440+ "message_id" : None ,
441+ "created_at" : None ,
442+ "created_ago" : None ,
443+ }
444+ )
445+ counts ["text" ] = counts .get ("text" , 0 ) + 1
446+ seq += 1
447+ for call in getattr (msg , "tool_calls" , None ) or []:
448+ call_id = str (call .get ("id" , "" ))
449+ name = str (call .get ("name" , "tool" ))
450+ if call_id :
451+ call_name_by_id [call_id ] = name
452+ payload = {"id" : call_id , "name" : name , "args" : call .get ("args" , {}), "_seq" : seq , "_run_id" : "checkpoint" }
453+ events .append (
454+ {
455+ "seq" : seq ,
456+ "event_type" : "tool_call" ,
457+ "payload" : payload ,
458+ "message_id" : None ,
459+ "created_at" : None ,
460+ "created_ago" : None ,
461+ }
462+ )
463+ counts ["tool_call" ] = counts .get ("tool_call" , 0 ) + 1
464+ seq += 1
465+ elif cls == "ToolMessage" :
466+ tool_call_id = str (getattr (msg , "tool_call_id" , "" ) or "" )
467+ name = call_name_by_id .get (tool_call_id , "tool" )
468+ payload = {
469+ "tool_call_id" : tool_call_id ,
470+ "name" : name ,
471+ "content" : _msg_text (getattr (msg , "content" , "" )),
472+ "_seq" : seq ,
473+ "_run_id" : "checkpoint" ,
474+ }
475+ events .append (
476+ {
477+ "seq" : seq ,
478+ "event_type" : "tool_result" ,
479+ "payload" : payload ,
480+ "message_id" : None ,
481+ "created_at" : None ,
482+ "created_ago" : None ,
483+ }
484+ )
485+ counts ["tool_result" ] = counts .get ("tool_result" , 0 ) + 1
486+ seq += 1
487+ # @@@checkpoint-trace-fallback - convert latest checkpoint messages into event-like rows so thread trace still renders when run_events are absent.
488+ if limit > 0 :
489+ events = events [- limit :]
490+ return events , counts
491+
492+
397493def load_thread_trace_payload (thread_id : str , run_id : str | None = None , limit : int = 2000 ) -> dict :
398494 """Load persisted trace bound to thread/run (not session)."""
399495 run_candidates = load_run_candidates (thread_id , limit = 50 )
400496 if not run_id :
401497 run_id = run_candidates [0 ]["run_id" ] if run_candidates else None
402498
499+ if run_id == "checkpoint" :
500+ checkpoint_events , checkpoint_counts = _load_checkpoint_events (thread_id , limit )
501+ return {
502+ "thread_id" : thread_id ,
503+ "run_id" : "checkpoint" ,
504+ "run_candidates" : [],
505+ "event_count" : len (checkpoint_events ),
506+ "events" : checkpoint_events ,
507+ "event_type_counts" : checkpoint_counts ,
508+ }
509+
403510 if not run_id :
511+ checkpoint_events , checkpoint_counts = _load_checkpoint_events (thread_id , limit )
512+ if checkpoint_events :
513+ return {
514+ "thread_id" : thread_id ,
515+ "run_id" : "checkpoint" ,
516+ "run_candidates" : [],
517+ "event_count" : len (checkpoint_events ),
518+ "events" : checkpoint_events ,
519+ "event_type_counts" : checkpoint_counts ,
520+ }
404521 return {
405522 "thread_id" : thread_id ,
406523 "run_id" : None ,
0 commit comments