Skip to content

fix(agent): tool-pair sanitizers — whitespace ids pair, stale empty tool_calls drop, orphan logic unified (salvage #55845 + #78063 + #59434) - #97167

Merged
teknium1 merged 5 commits into
mainfrom
salv-imgwave3-k
Aug 28, 2026
Merged

fix(agent): tool-pair sanitizers — whitespace ids pair, stale empty tool_calls drop, orphan logic unified (salvage #55845 + #78063 + #59434)#97167
teknium1 merged 5 commits into
mainfrom
salv-imgwave3-k

Conversation

@teknium1

@teknium1 teknium1 commented Aug 28, 2026

Copy link
Copy Markdown
Collaborator

Summary

Combined salvage of three community tool-pair sanitizer PRs, applied fixes-first so the final refactor deduplicates the already-fixed logic:

  1. fix(compression): strip whitespace from tool_call_id in _sanitize_tool_pairs #55845 by @srojk34 — strip whitespace from tool_call_id in ContextCompressor._sanitize_tool_pairs, closing the sibling gap of fa3ab2ffd: IDs differing only by stray whitespace were misclassified as orphaned and replaced with [Result unavailable] stubs on every compression cycle.
  2. fix(agent): drop stale empty tool_calls on repair_message_sequence merge (#77921) #78063 by @JoaoMarcos44 — fix three stale-field bugs in agent/agent_runtime_helpers.py: drop stale empty tool_calls surviving the consecutive-assistant merge (fixes Still reproducing in v0.19.1: empty tool_calls after repair_message_sequence (follow-up to #58755) #77921), drop the stale api_content sidecar when the merge rewrites content (with the wz-heng review's negative controls), and drop tool results with a missing/empty tool_call_id at the pre-API chokepoint (sanitize_api_messages does not catch tool messages with missing/empty tool_call_id #78071).
  3. refactor(sanitizer): extract shared _classify_tool_call_orphans to eliminate drift #59434 by @isheng-eqi — extract a shared _classify_tool_call_orphans(messages) used by both sanitize_api_messages and _sanitize_tool_pairs, so orphan-detection/id-resolution rules can never drift between the two copies again (refactor(compression): consolidate _sanitize_tool_pairs with the canonical agent_runtime_helpers sanitizers (dedup/whitespace/orphan drift) #58357).

All three authors' commits are cherry-picked so authorship is preserved; conflict-resolution glue is in a separate commit.

Fixes #77921. Closes #58357. Closes #78071 (chokepoint half).

Changes

#55845 (whitespace ids)agent/context_compressor.py, tests/agent/test_context_compressor.py (+3 tests)

#78063 (stale fields on merge)agent/agent_runtime_helpers.py, tests/run_agent/test_message_sequence_repair.py (+7 tests)

  • Fix 1 (empty tool_calls dropped in repair_message_sequence merge) landed on main independently as f316f7d086 — redundant, not re-applied.
  • Fix 2 salvaged: content_rewritten tracking in the consecutive-assistant merge; drop the api_content sidecar only when the merged content value actually changed, so prompt-cache replay bytes stay stable when nothing moved (empty-string / None / multimodal negative controls included).
  • Fix 3 salvaged: explicit pre-filter in sanitize_api_messages dropping role=tool messages with a missing/empty tool_call_id, which previously slipped through the orphan sweep (truthy-only set membership) and reached strict providers as a schema violation.

#59434 (dedup refactor)agent/agent_runtime_helpers.py, agent/context_compressor.py, tests/run_agent/test_message_sequence_repair.py (+5 tests)

Validation

Live A/B on the real modules (.venv python + worktree sys.path), BEFORE = current main, AFTER = this branch:

Probe BEFORE (main) AFTER
#55845: tool_calls id 'call_x' + result 'call_x ' (trailing space) through _sanitize_tool_pairs paired ✅ (superseded by variant-strip on main; tests salvaged to pin it) paired & preserved ✅, deterministic ✅
#78063a: #77921 shape — merge leaves stale tool_calls: [] already dropped ✅ (f316f7d086) dropped ✅
#78063b: stale api_content sidecar after merge rewrites content survives ❌ (stale bytes would replay) dropped ✅; preserved when content unchanged ✅
#78063c: tool results with tool_call_id: "" / key absent through sanitize_api_messages reach the wire ❌ (['call_Z', '', None]) dropped ✅ (['call_Z'])
#59434: shared _classify_tool_call_orphans imported by both call sites absent ❌ (two drifting copies) present ✅ (grep: both agent_runtime_helpers.py + context_compressor.py delegate); both suites pass pre/post refactor proving identical behavior

Invariants held in every probe: role alternation preserved, no orphaned tool results, deterministic output.

Live repro: BEFORE on main — api_content sidecar survives the assistant merge (stale bytes win at API-build time) and id-less tool results pass sanitize_api_messages untouched (tool ids on wire: ['call_Z', '', None]); AFTER — sidecar dropped exactly when content changed, wire ids ['call_Z'], and both sanitizers classify orphans through the one shared function.

Tests: tests/run_agent/test_message_sequence_repair.py + tests/agent/test_context_compressor.py194 passed (memory-capped run), including the 15 new tests from the three PRs. ruff check clean on all touched files.

Note on #59434's scripts/release.py hunk: it appended to the frozen AUTHOR_MAP — dropped per current attribution policy (contributor mapping lives in contributors/emails/; [email protected], [email protected], and srojk34 noreply are all already mapped — scripts/audit_pr_attribution.py --fix reports clean).

Infographic

infographic

srojk34 and others added 5 commits August 28, 2026 06:05
…l_pairs

_sanitize_tool_pairs() in ContextCompressor compared raw tool_call_id
strings without stripping whitespace, the same bug fa3ab2f just fixed
in agent_runtime_helpers.py / run_agent.py (_get_tool_call_id_static +
sanitize_api_messages). ContextCompressor has its own near-identical
reimplementation of the pair-repair logic that was left unpatched.

When assistant-side and result-side IDs diverge only in surrounding
whitespace, the compressor misclassifies valid results as orphaned and
replaces them with [Result unavailable] stubs — silent data loss on
every compression cycle that touches such pairs.

Apply the same .strip() fix to all three sites:
- _get_tool_call_id (extracts IDs from assistant tool_calls)
- result_call_ids accumulation loop
- orphaned_results filter predicate

Closes the sibling gap of fa3ab2f / #42405.
Rebased onto current main to drop the empty-tool_calls fix (already on
main via #86654, cherry-picked from #77944 with @webtecnica's
authorship). This PR now carries only the two fixes unique to it:

1. A pre-existing api_content sidecar left stale on the consecutive-
   assistant merge. The sidecar takes priority over content at
   API-build time, so a merge could silently discard its own freshly
   concatenated content on the next call. Only dropped when the merge
   actually changes the resulting value (wz-heng, #78063 review) --
   content_rewritten compares before/after value, not just whether an
   assignment branch fired, so a falsy new_content (e.g. "") that
   strips to nothing no longer trips a spurious sidecar drop.

2. sanitize_api_messages never flagged a tool result with a missing/
   empty tool_call_id -- its orphan-detection set only ever collected
   truthy ids, so an unpaired result with no id passed the final
   chokepoint untouched.

Addresses teknium1's rebase request and wz-heng's review findings on
…iminate drift

sanitize_api_messages (agent_runtime_helpers) and
_sanitize_tool_pairs (context_compressor) both collected
tool-call IDs and classified orphans with near-identical logic
that had already drifted: the canonical sanitizer added dedup
(#58350), but the compressor's copy did not.

Extract the shared orphan-detection logic into
_classify_tool_call_orphans(messages) in agent_runtime_helpers.
Both call sites now delegate to it, preserving their divergent
remediation strategies (insert-stubs vs strip-orphans) while
ensuring id-resolution rules and dedup stay in sync.

Closes #58357
@github-actions

github-actions Bot commented Aug 28, 2026

Copy link
Copy Markdown
Contributor

૮ >ﻌ< ა ci review

ran on 3925230 — fix(sanitizer): drop duplicated legacy _classify_tool_call_o

⚠️ Warnings

OSV vulnerability scan · View job

6 known vulnerabilities found in pinned dependencies.

How to fix:

Review the findings in the Security tab. Update the affected dependencies if a patched version is available.


debug info

CI timings

CI timings · View report · View job

Wall time 4m12s vs 5m2s (-16.6%). 5 job(s) slower, 6 faster, 1 unchanged.

  • OS-specific tests / Windows-only tests: -10.0s
  • OSV scan / Emit review status: -4.0s
  • OS-specific tests / macOS-only tests: +4.0s
  • Check contributors / check-attribution: -4.0s
  • Python tests / e2e: +2.0s

@alt-glitch alt-glitch added type/bug Something isn't working P2 Medium — degraded but workaround exists comp/agent Core agent runtime: loop, agent_init, prompt builder, context-compression, responses endpoint sweeper:risk-session-state Sweeper risk: may lose/corrupt/mis-associate session or context state labels Aug 28, 2026
@teknium1
teknium1 merged commit 225fa13 into main Aug 28, 2026
37 checks passed
@teknium1
teknium1 deleted the salv-imgwave3-k branch August 28, 2026 13:32
teknium1 added a commit that referenced this pull request Aug 28, 2026
…phans (#97167) — classifier docstring reflects its remaining consumer; empty-id filter note updated
melon-xf added a commit to melon-xf/hermes-agent that referenced this pull request Sep 3, 2026
…phans (NousResearch#97167) — classifier docstring reflects its remaining consumer; empty-id filter note updated
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

comp/agent Core agent runtime: loop, agent_init, prompt builder, context-compression, responses endpoint P2 Medium — degraded but workaround exists sweeper:risk-session-state Sweeper risk: may lose/corrupt/mis-associate session or context state type/bug Something isn't working

Projects

None yet

5 participants