Skip to content

fix(research): keep tool_call/tool_response pairs intact when compressing trajectories#40593

Merged
teknium1 merged 2 commits into
mainfrom
salvage/40495-trajectory-tool-pairs
Jun 7, 2026
Merged

fix(research): keep tool_call/tool_response pairs intact when compressing trajectories#40593
teknium1 merged 2 commits into
mainfrom
salvage/40495-trajectory-tool-pairs

Conversation

@teknium1

@teknium1 teknium1 commented Jun 6, 2026

Copy link
Copy Markdown
Contributor

Summary

Prevents the offline trajectory compressor from splitting a <tool_call>/<tool_response> pair at the compression boundary, which corrupts training trajectories.

In the from/value format a tool turn is always emitted right after the gpt turn whose <tool_call> it answers. When token-accumulation landed the compression boundary on a tool turn, it cut between the call and its response.

Changes

  • trajectory_compressor.py: _is_boundary_clean() (a boundary is clean only at end-of-trajectory or on a non-tool turn) + _snap_boundary() (move the boundary onto the nearest clean turn, preferring forward so an orphaned tool folds in with its gpt; clamp to range).
  • tests: a paired trajectory with an oversized middle turn that forces a mid-pair boundary; asserts tool-call/response markers stay balanced after compression.
  • scripts/release.py: AUTHOR_MAP entry for the contributor.

Scope

This is the offline compressor (scripts/sample_and_compress.py / training-data path), not the live conversation compressor — no prompt-cache implications.

Validation

TestCompressionToolPairIntegrity 4 passed. py_compile OK.

Cherry-picked from #40495 (@synapsesx), authorship preserved.

synapsesx and others added 2 commits June 6, 2026 08:28
…sing trajectories

## What does this PR do?

The trajectory compressor could corrupt training trajectories by cutting a
conversation in the middle of a tool-call/tool-response pair. In the from/value
trajectory format a `tool` turn (carrying `<tool_response>` markers) is always
emitted immediately after the `gpt` turn whose `<tool_call>` it answers, so the
two turns must stay together. The compressible region's end boundary, however,
was chosen purely by token accumulation: the loop stopped at the first turn where
the accumulated tokens met the savings target, with no regard for turn roles. For
any over-budget trajectory whose savings boundary happened to land between a `gpt`
turn and its `tool` turn, the `gpt` (with its `<tool_call>`) was summarised away
into the replacement `human` message while the now-orphaned `tool` turn (with its
`<tool_response>`) was kept verbatim in the tail — producing an unmatched marker
and silently corrupting the training signal. The head boundary had the mirror
problem when the first tool turn was not protected.

This change snaps both compression boundaries to a clean turn boundary before the
region is extracted and replaced, so the summary always covers whole gpt+tool
blocks and a `tool` turn is never separated from the `gpt` turn that precedes it.
The boundary is moved forward when possible (folding an orphaned tool turn into
the region that already holds its gpt) and falls back to moving backward when no
clean boundary exists ahead, such as when the protected tail itself begins on a
tool turn.

## Related Issue

N/A

## Type of Change

- [x] 🐛 Bug fix (non-breaking change that fixes an issue)

## Changes Made

- `trajectory_compressor.py`: added `_is_boundary_clean()` and `_snap_boundary()`
  helpers on `TrajectoryCompressor`, and applied them to both the head and tail
  compression boundaries in `compress_trajectory()` and
  `compress_trajectory_async()`. When snapping collapses the region to nothing
  safe to compress, the trajectory is returned unchanged and flagged as still
  over the limit rather than being corrupted.
- `tests/test_trajectory_compressor.py`: added `TestCompressionToolPairIntegrity`
  covering the sync and async paths plus direct unit tests for the boundary
  snapping (forward skip and backward fallback).

## How to Test

1. Run the focused tests: `pytest tests/test_trajectory_compressor.py -q`.
2. The new sync/async cases build a trajectory of gpt/tool pairs with an oversized
   middle gpt turn and choose a token target that forces the accumulation
   boundary to stop between a `<tool_call>` and its `<tool_response>`. They assert
   that `<tool_call>` and `<tool_response>` markers stay balanced after
   compression and that every kept `tool` turn is immediately preceded by a `gpt`
   turn (never the inserted summary or another tool turn).

## Checklist

### Code

- [x] I've read the [Contributing Guide](https://github.com/NousResearch/hermes-agent/blob/main/CONTRIBUTING.md)
- [x] My commit messages follow [Conventional Commits](https://www.conventionalcommits.org/) (`fix(scope):`, `feat(scope):`, etc.)
- [x] I searched for [existing PRs](https://github.com/NousResearch/hermes-agent/pulls) to make sure this isn't a duplicate
- [x] My PR contains **only** changes related to this fix/feature (no unrelated commits)
- [x] I've run `pytest tests/ -q` and all tests pass
- [x] I've added tests for my changes (required for bug fixes, strongly encouraged for features)
- [x] I've tested on my platform: macOS 15 (Darwin 25.5)

### Documentation & Housekeeping

- [x] I've updated relevant documentation (README, `docs/`, docstrings) — or N/A
- [x] I've updated `cli-config.yaml.example` if I added/changed config keys — or N/A
- [x] I've updated `CONTRIBUTING.md` or `AGENTS.md` if I changed architecture or workflows — or N/A
- [x] I've considered cross-platform impact (Windows, macOS) per the [compatibility guide](https://github.com/NousResearch/hermes-agent/blob/main/CONTRIBUTING.md#cross-platform-compatibility) — or N/A
- [x] I've updated tool descriptions/schemas if I changed tool behavior — or N/A
@github-actions

github-actions Bot commented Jun 6, 2026

Copy link
Copy Markdown
Contributor

🔎 Lint report: salvage/40495-trajectory-tool-pairs vs origin/main

ruff

Total: 0 on HEAD, 0 on base (➖ 0)

🆕 New issues: none

✅ Fixed issues: none

Unchanged: 0 pre-existing issues carried over.

ty (type checker)

Total: 9962 on HEAD, 9962 on base (➖ 0)

🆕 New issues: none

✅ Fixed issues: none

Unchanged: 5167 pre-existing issues carried over.

Diagnostics are surfaced as warnings — this check never fails the build.

@alt-glitch alt-glitch added type/bug Something isn't working P3 Low — cosmetic, nice to have labels Jun 6, 2026
@teknium1 teknium1 merged commit fa8fd51 into main Jun 7, 2026
23 checks passed
@teknium1 teknium1 deleted the salvage/40495-trajectory-tool-pairs branch June 7, 2026 12:01
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

P3 Low — cosmetic, nice to have type/bug Something isn't working

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants