Skip to content

Python: fix(python): preserve non-adjacent tool pairs during history truncation - #14658

Open
hylin (linhongyu510) wants to merge 1 commit into
microsoft:mainfrom
linhongyu510:fix/nonadjacent-tool-pair-truncation
Open

hylin (linhongyu510) wants to merge 1 commit into
microsoft:mainfrom
linhongyu510:fix/nonadjacent-tool-pair-truncation

Conversation

@linhongyu510

Copy link
Copy Markdown

Title

fix(python): preserve non-adjacent tool pairs during history truncation

Motivation and Context

  • Problem: ChatHistoryTruncationReducer could retain a FunctionResultContent while dropping its matching FunctionCallContent when the two were separated by non-function messages.
  • Existing contract: adjacent tool blocks were already protected, and matched ID pairs are used elsewhere by the reducer utilities. A retained tool result should not become an orphan after truncation.
  • Why this is minimal: the fix chooses the existing user-threshold candidate boundary first, then applies a final matched-ID pair closure.
  • Scope: offline chat-history reduction only; no provider, network, E2E, public API, dependency, lockfile, or documentation-only refactor.
  • Issue: self-discovered during audit; independent dedup found no equivalent open issue/PR.

Description

locate_safe_reduction_index now builds an O(n) suffix minimum-call map from get_call_result_pairs(history). After the existing adjacency and threshold-window candidate selection, it moves the cut back only when a retained result requires an earlier matched call. Chained non-adjacent pairs are closed transitively. If the required call is before offset_count, reduction returns None rather than crossing offset/summary territory.

Offset, threshold, system/developer preservation, unknown/unpaired content, and existing user-boundary search semantics are otherwise unchanged.

Local verification

  • Base: 483ffb487a33a1644886992d56599feae3b69b76.
  • Pre-fix formal regression: 4 failed, 4 passed.
  • Post-fix formal regression: 8 passed.
  • Production-only revert cycle: red exit 1, restore green exit 0.
  • Related chat-history suite: 105 passed.
  • Ruff: 0.9.10; check and format check pass.
  • Mypy: targeted changed production file passes.
  • Multi-Python matrix and wheel build were not run in this preparation step.
  • AI assistance was used; an independent local review was rerun before publication. No external CLI review is claimed.

Contribution Checklist

  • The code builds clean without any errors or warnings in the focused local checks.
  • The PR follows the Semantic Kernel Contribution Guide and local Ruff/Mypy checks.
  • All unit tests pass, and I added focused tests for the non-adjacent call/result regression.
  • I did not break anyone.
  • AI assistance is disclosed.

@semantic-kernel-automation semantic-kernel-automation Bot added the python Pull requests for the Python Semantic Kernel label Oct 24, 2026
@github-actions github-actions Bot changed the title fix(python): preserve non-adjacent tool pairs during history truncation Python: fix(python): preserve non-adjacent tool pairs during history truncation Oct 24, 2026
Preserve matched FunctionCallContent and FunctionResultContent pairs when ChatHistoryTruncationReducer cuts between a call and its result.

Co-authored-by: AI assistance was used and reviewed.

Signed-off-by: hylin <linhongyu510@gmail.com>
@linhongyu510
hylin (linhongyu510) force-pushed the fix/nonadjacent-tool-pair-truncation branch from 3cd8e4b to 4c489f2 Compare October 27, 2026 17:14
@linhongyu510

hylin (linhongyu510) commented Oct 27, 2026 •

Copy link
Copy Markdown
Author

Rebased onto upstream main snapshot b5b526d9b8bd39da41db4a5c0f1aff20d26f7ec8 (2026-10-27).

Local verification on this rebased HEAD (4c489f2d44a9d41164d40c2b27aaf59333421b70), frozen lock env (uv 0.13.1, uv sync --frozen --all-extras --dev):

  • Formal regression suite: 8 passed.
  • Production-only revert (base bytes restored via try/finally): exactly 4 failed / 4 passed, exit 1; restored HEAD green 8 passed.
  • Focused reducer suite: 43 passed.
  • Related chat-history suite: 212 passed.
  • Official type gate mypy -p semantic_kernel --config-file mypy.ini: no issues found in 556 source files (HEAD and base).
  • Ruff check: clean.
  • Official pre-commit (--all-files): 13 hooks passed (exit 0).
  • Warnings: pytest-timeout config warning resolved by installing the locked plugin; Pydantic deprecation warnings are pre-existing on base (40 in focused, 52 in related suite).
  • AI assistance was used and reviewed; this is an independent local verification, not a maintainer review. Multi-Python matrix / wheel build not run in this step.

@linhongyu510
hylin (linhongyu510) marked this pull request as ready for review October 29, 2026 17:03
@linhongyu510
hylin (linhongyu510) requested a review from a team as a code owner October 29, 2026 17:03
Copilot AI balanced review requested due to automatic review settings October 29, 2026 17:03

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Changes recommended

History truncation can still orphan tool results because only the first matched result per message is collected and backward cut adjustments can split adjacent tool blocks.

2 open issues
What changed in this PR

This PR updates Python chat-history truncation to retain matching function calls when their results appear later, separated by other messages.

Changes:

  • Adds matched-ID boundary adjustment after existing adjacency and user-threshold selection.
  • Handles chained pair dependencies and rejects cuts that require crossing the offset boundary.
  • Adds regression tests for pair preservation, thresholds, offsets, and system-message retention.
File Description
python/​tests/​unit/​contents/​test_chat_history_truncation_pair_invariant.py Adds focused truncation and pair-preservation regression tests.
python/​semantic_kernel/​contents/​history_reducer/​chat_history_reducer_utils.py Adjusts truncation boundaries using suffix minima of matched call indices.

Review effort: Balanced

Comment on lines +145 to +146
for call_index, result_index in get_call_result_pairs(history):
suffix_min_call_index[result_index] = min(suffix_min_call_index[result_index], call_index)
break
if required_call_index < offset_count:
return None
candidate_index = required_call_index

This branch was successfully deployed

1 active deployment
github-app-auth — 4c489f2d Deployed Oct 27, 2026 by linhongyu510 via add_label #29470
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

python Pull requests for the Python Semantic Kernel

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants