Skip to content

fix(loop): make text-mode tool_call ids unique across the history - #40

Merged
zhanghanduo merged 5 commits into
mainfrom
fix/turn-scoped-text-tool-call-ids
Sep 25, 2026
Merged

zhanghanduo merged 5 commits into
mainfrom
fix/turn-scoped-text-tool-call-ids

Conversation

@zhanghanduo

@zhanghanduo zhanghanduo commented Sep 24, 2026 •

Copy link
Copy Markdown
Collaborator

Problem

Text-mode tool-call parsers number calls per response (qwen_tc_0, seed_tc_0, fc_tc_0, mcp_tc_0, dangling_tc_0), so the same tool_call_id repeated every turn. The engine's own fallback call_{turn}_{n} also collided after a resume through initial_messages, because turn restarts at 1. Anything keyed by tool_call_id could then resolve to the wrong turn's result, for example compaction's preserved tool results and spill recovery handles.

Ordering within a parallel batch was already correct. The fix only affects id uniqueness across turns.

Fix

_assign_unique_tool_call_ids runs right after parsing. It rewrites every non-native call id to call_{turn}_{idx}_{8 hex}. Native provider ids (the ones present in the assistant message's tool_calls) are kept verbatim, because the tool reply must echo them. Since this runs before landing/cap filtering, observers, blocked or dropped replies, and executed results all see the same final id.

Parsers and execute_tools are unchanged.

Tests

  • test_text_parsed_call_ids_stay_unique_across_turns_and_resume: 2 turns × 2 calls, run once and again as a resume, gives 8 unique ids with bodies in call order. It fails on main with call_1_0 duplicated.
  • test_native_tool_call_ids_are_kept_verbatim
  • Full suite: 1579 passed, 2 skipped. ruff clean.
  • ApodexHarness: 512 tests in tests/core, the parser tests, the trajectory observer tests and the facade tests pass against this branch via PYTHONPATH.

Release note: changes/40.fix.md.

🤖 Generated with Claude Code

Release workflow

Uses the shared centralized-release workflow in #44. This PR keeps the current package version and contributes its own independent changes/ fragment. Version, lockfile and changelog aggregation happen in a later release PR. #40, #42 and #43 were tested in all six merge orders without conflicts.

zhanghanduo and others added 5 commits September 24, 2026 11:01
Text-mode parsers number calls per response (qwen_tc_0, fc_tc_0, ...), so
every turn reused the same tool_call_id, and the engine's call_{turn}_{n}
fallback collided again after resuming via initial_messages. Assign a
turn-scoped id with a random suffix to every non-native call right after
parsing, before filtering, so observers, blocked/dropped replies and executed
results share the final id. Native provider ids are kept verbatim.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
@zhanghanduo
zhanghanduo merged commit ce8a879 into main Sep 25, 2026
5 checks passed
@zhanghanduo
zhanghanduo deleted the fix/turn-scoped-text-tool-call-ids branch September 25, 2026 06:24
@zhanghanduo zhanghanduo mentioned this pull request Sep 25, 2026
4 tasks
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant