Skip to content

fix(formatters): keep tool_calls when replaying instruct/react turns - #1787

Draft
simonyang08 wants to merge 1 commit into
generative-computing:mainfrom
simonyang08:fix/chat-formatter-preserve-tool-calls
Draft

simonyang08 wants to merge 1 commit into
generative-computing:mainfrom
simonyang08:fix/chat-formatter-preserve-tool-calls

Conversation

@simonyang08

Copy link
Copy Markdown
Contributor

Opening as a follow-up to my claim comment on #1784 — happy to yield or close if a team member prefers to take this; the issue looked unclaimed so I prepared the fix while awaiting assignment.

Problem

With a ChatContext, tool loops built on instruct() (and react()) replay the tool-calling assistant turn without its tool_calls: Instruction._parse and ReactThought._parse reduce the turn to text, and ChatFormatter.to_chat_messages rebuilt it as a plain assistant message. The following tool message then answers a call the provider never saw, and strict providers reject the second turn of the loop (OpenAI 400 on an orphaned tool message, LiteLLM APIConnectionError, Anthropic orphaned tool_result). session.chat() loops were unaffected because Message._parse already preserves tool calls.

Fix

ChatFormatter.to_chat_messages gains a small repair pass: when a computed ModelOutputThunk carries tool_calls and the next message has role tool, the turn is rebuilt through the provider-shaped parsing (now exposed as the static Message._parse_output, with Message._parse delegating to it). Turns without an answering tool message stay text-only, so calls the caller never ran don't go out as dangling tool_calls — strict providers reject those too.

Verification

  • New formatter-level regression tests cover both directions: the tool-issuing turn keeps its tool_calls when a tool message follows, and gains none when it doesn't (2 passed).
  • Focused suite: pytest test/formatters/ test/stdlib/components/test_chat.py test/backends/test_tool_serialization_unit.py test/backends/test_utils.py — 234 passed / 5 skipped.
  • ruff format/ruff check clean on touched files; docs quality gate (audit_coverage --quality) reports 0 issues.

Closes #1784

Components whose _parse reduces a model turn to text (Instruction,
ReactThought) lost the turn's tool_calls in ChatFormatter.to_chat_messages,
so a following tool message referenced a call the provider never saw and
strict providers rejected the second turn of the loop.

When a tool message immediately follows the turn, rebuild it through
Message._parse_output, which preserves provider-shaped tool_calls and
captured reasoning; turns without an answering tool message stay
text-only so unanswered calls do not go out as dangling tool_calls.

Assisted-by: ZCode (GLM)
@github-actions github-actions Bot added the bug Something isn't working label Nov 3, 2026

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

bug Something isn't working

Projects

None yet

Development

Successfully merging this pull request may close these issues.

bug: instruct() and react() replay tool-call turns without tool_calls

2 participants