fix(providers): populate Function field in ToolCall responses to survive session serialization

ToolCall.Name and ToolCall.Arguments are both json:"-" — they are not
written to or read from JSONL session storage. The Function field
(json:"function,omitempty") is the only carrier that survives a
serialize/deserialize round-trip; NormalizeToolCall already knows how
to restore Name and Arguments from it.

Three provider parseResponse paths were creating ToolCall without
setting Function, so after the first session save every tool call in
history was unrecoverable:

  - pkg/providers/anthropic/provider.go  (SDK-based Anthropic provider)
  - pkg/providers/common/common.go       (OpenAI-compat / Azure / etc.)
  - pkg/providers/codex_provider.go      (Codex / o-series)

Fix: set Function.Name and Function.Arguments (JSON-encoded) alongside
Name and Arguments in each of these parseResponse paths, matching the
pattern already used by anthropic_messages, antigravity, and
tool_call_extract.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
cmclark00 2026-03-16 20:30:55 -04:00
parent 0c940a4d47
commit c5b947fff3
3 changed files with 24 additions and 0 deletions

View file

@ -351,10 +351,18 @@ func parseResponse(resp *anthropic.Message) *LLMResponse {
log.Printf("anthropic: failed to decode tool call input for %q: %v", tu.Name, err) log.Printf("anthropic: failed to decode tool call input for %q: %v", tu.Name, err)
args = map[string]any{"raw": string(tu.Input)} args = map[string]any{"raw": string(tu.Input)}
} }
argsJSON, _ := json.Marshal(args)
toolCalls = append(toolCalls, ToolCall{ toolCalls = append(toolCalls, ToolCall{
ID: tu.ID, ID: tu.ID,
Name: tu.Name, Name: tu.Name,
Arguments: args, Arguments: args,
// Function must be populated so Name and Arguments survive
// JSON serialization to session storage (both fields are
// json:"-" on ToolCall; Function carries them through).
Function: &FunctionCall{
Name: tu.Name,
Arguments: string(argsJSON),
},
}) })
} }
} }

View file

@ -378,10 +378,18 @@ func parseCodexResponse(resp *responses.Response) *LLMResponse {
if err := json.Unmarshal([]byte(item.Arguments), &args); err != nil { if err := json.Unmarshal([]byte(item.Arguments), &args); err != nil {
args = map[string]any{"raw": item.Arguments} args = map[string]any{"raw": item.Arguments}
} }
argsJSON, _ := json.Marshal(args)
toolCalls = append(toolCalls, ToolCall{ toolCalls = append(toolCalls, ToolCall{
ID: item.CallID, ID: item.CallID,
Name: item.Name, Name: item.Name,
Arguments: args, Arguments: args,
// Function must be populated so Name and Arguments survive
// JSON serialization to session storage (both fields are
// json:"-" on ToolCall; Function carries them through).
Function: &FunctionCall{
Name: item.Name,
Arguments: string(argsJSON),
},
}) })
} }
} }

View file

@ -190,11 +190,19 @@ func ParseResponse(body io.Reader) (*LLMResponse, error) {
arguments = DecodeToolCallArguments(tc.Function.Arguments, name) arguments = DecodeToolCallArguments(tc.Function.Arguments, name)
} }
argsJSON, _ := json.Marshal(arguments)
toolCall := ToolCall{ toolCall := ToolCall{
ID: tc.ID, ID: tc.ID,
Name: name, Name: name,
Arguments: arguments, Arguments: arguments,
ThoughtSignature: thoughtSignature, ThoughtSignature: thoughtSignature,
// Function must be populated so Name and Arguments survive
// JSON serialization to session storage (both fields are
// json:"-" on ToolCall; Function carries them through).
Function: &FunctionCall{
Name: name,
Arguments: string(argsJSON),
},
} }
if thoughtSignature != "" { if thoughtSignature != "" {