@@ -8319,51 +8319,57 @@ impl RuntimeThreadManager {
83198319 }
83208320 TurnItemKind :: ToolCall => {
83218321 let meta = item. metadata . as_ref ( ) ;
8322- let is_tool_result = meta. and_then ( |m| m. get ( "tool_result_for" ) ) . is_some ( ) ;
8323- if is_tool_result {
8322+ let meta_str = |key : & str | {
8323+ meta. and_then ( |m| m. get ( key) )
8324+ . and_then ( Value :: as_str)
8325+ . unwrap_or_default ( )
8326+ . to_string ( )
8327+ } ;
8328+ let tool_use_id = meta_str ( "tool_use_id" ) ;
8329+ let tool_name = meta_str ( "tool_name" ) ;
8330+ let tool_result_for = meta_str ( "tool_result_for" ) ;
8331+ // Completed live turns persist the call and its result
8332+ // on one item; seeded history persists them as two.
8333+ // Both shapes must rebuild the paired tool_call /
8334+ // tool_result. Snapshots persisted before tool identity
8335+ // was durable carry neither side: skip them rather than
8336+ // replay an empty tool_call shell that strict
8337+ // OpenAI-compatible endpoints reject (#5823).
8338+ if !tool_use_id. is_empty ( ) && !tool_name. is_empty ( ) {
8339+ flush_user ( & mut user_blocks, & mut messages) ;
8340+ let input_str = meta
8341+ . and_then ( |m| m. get ( "tool_input" ) )
8342+ . and_then ( Value :: as_str)
8343+ . map ( str:: to_string)
8344+ . or_else ( || item. detail . clone ( ) )
8345+ . unwrap_or_default ( ) ;
8346+ let input: serde_json:: Value =
8347+ serde_json:: from_str ( & input_str) . unwrap_or ( serde_json:: Value :: Null ) ;
8348+ assistant_blocks. push ( ContentBlock :: ToolUse {
8349+ id : tool_use_id,
8350+ name : tool_name,
8351+ input,
8352+ caller : None ,
8353+ thought_signature : None ,
8354+ } ) ;
8355+ }
8356+ if !tool_result_for. is_empty ( ) {
83248357 flush_assistant ( & mut assistant_blocks, & mut messages) ;
8325- let tool_use_id = meta
8326- . and_then ( |m| m. get ( "tool_result_for" ) )
8327- . and_then ( |v| v. as_str ( ) )
8328- . unwrap_or ( "" )
8329- . to_string ( ) ;
83308358 let content = item. detail . unwrap_or_default ( ) ;
83318359 let is_error = meta
83328360 . and_then ( |m| m. get ( "is_error" ) )
8333- . and_then ( |v| v . as_bool ( ) )
8361+ . and_then ( Value :: as_bool)
83348362 . unwrap_or ( false ) ;
83358363 let content_blocks = meta
83368364 . and_then ( |m| m. get ( "content_blocks" ) )
8337- . and_then ( |v| v . as_array ( ) )
8365+ . and_then ( Value :: as_array)
83388366 . cloned ( ) ;
83398367 user_blocks. push ( ContentBlock :: ToolResult {
8340- tool_use_id,
8368+ tool_use_id : tool_result_for ,
83418369 content,
83428370 is_error : if is_error { Some ( true ) } else { None } ,
83438371 content_blocks,
83448372 } ) ;
8345- } else {
8346- flush_user ( & mut user_blocks, & mut messages) ;
8347- let tool_use_id = meta
8348- . and_then ( |m| m. get ( "tool_use_id" ) )
8349- . and_then ( |v| v. as_str ( ) )
8350- . unwrap_or ( "" )
8351- . to_string ( ) ;
8352- let tool_name = meta
8353- . and_then ( |m| m. get ( "tool_name" ) )
8354- . and_then ( |v| v. as_str ( ) )
8355- . unwrap_or ( "" )
8356- . to_string ( ) ;
8357- let input_str = item. detail . unwrap_or_default ( ) ;
8358- let input: serde_json:: Value =
8359- serde_json:: from_str ( & input_str) . unwrap_or ( serde_json:: Value :: Null ) ;
8360- assistant_blocks. push ( ContentBlock :: ToolUse {
8361- id : tool_use_id,
8362- name : tool_name,
8363- input,
8364- caller : None ,
8365- thought_signature : None ,
8366- } ) ;
83678373 }
83688374 }
83698375 _ => { }
@@ -8702,15 +8708,25 @@ impl RuntimeThreadManager {
87028708 tool_items. insert ( id. clone ( ) , item_id. clone ( ) ) ;
87038709 let kind = tool_kind_for_name ( & name) ;
87048710 let summary = summarize_text ( & format ! ( "{name} started" ) , SUMMARY_LIMIT ) ;
8711+ let input_str = serde_json:: to_string ( & input) . unwrap_or_default ( ) ;
87058712 let item = TurnItemRecord {
87068713 schema_version : CURRENT_RUNTIME_SCHEMA_VERSION ,
87078714 id : item_id. clone ( ) ,
87088715 turn_id : turn_id. clone ( ) ,
87098716 kind,
87108717 status : TurnItemLifecycleStatus :: InProgress ,
87118718 summary,
8712- detail : Some ( serde_json:: to_string ( & input) . unwrap_or_default ( ) ) ,
8713- metadata : None ,
8719+ detail : Some ( input_str. clone ( ) ) ,
8720+ // The tool identity must live in the durable item
8721+ // snapshot: restart history rebuild reads it back to
8722+ // re-emit provider tool_calls. Without it a restart
8723+ // replays empty id/name/arguments shells that strict
8724+ // OpenAI-compatible endpoints reject (#5823).
8725+ metadata : Some ( json ! ( {
8726+ "tool_use_id" : id. clone( ) ,
8727+ "tool_name" : name. clone( ) ,
8728+ "tool_input" : input_str,
8729+ } ) ) ,
87148730 artifact_refs : Vec :: new ( ) ,
87158731 started_at : Some ( Utc :: now ( ) ) ,
87168732 ended_at : None ,
@@ -8771,7 +8787,30 @@ impl RuntimeThreadManager {
87718787 SUMMARY_LIMIT ,
87728788 ) ;
87738789 item. detail = Some ( output. content . clone ( ) ) ;
8774- item. metadata = output. metadata . clone ( ) ;
8790+ // `detail` is now the tool output, so the
8791+ // call identity persisted at start must be
8792+ // carried through metadata. Mark the
8793+ // terminal result too so restart history
8794+ // rebuild can re-emit the paired
8795+ // tool_call/tool_result (#5823).
8796+ let mut meta = match output. metadata {
8797+ Some ( Value :: Object ( map) ) => Value :: Object ( map) ,
8798+ _ => json ! ( { } ) ,
8799+ } ;
8800+ if let Some ( obj) = meta. as_object_mut ( ) {
8801+ if let Some ( started) =
8802+ item. metadata . as_ref ( ) . and_then ( Value :: as_object)
8803+ {
8804+ for key in [ "tool_use_id" , "tool_name" , "tool_input" ] {
8805+ if let Some ( value) = started. get ( key) {
8806+ obj. insert ( key. to_string ( ) , value. clone ( ) ) ;
8807+ }
8808+ }
8809+ }
8810+ obj. insert ( "tool_result_for" . to_string ( ) , json ! ( id) ) ;
8811+ obj. insert ( "is_error" . to_string ( ) , json ! ( !output. success) ) ;
8812+ }
8813+ item. metadata = Some ( meta) ;
87758814 }
87768815 }
87778816 Err ( err) => {
0 commit comments