Skip to content

fix: keep tool_calls/tool_result pairs when trimming conversation - #55

Open
micpapal wants to merge 1 commit into
outshift-open:mainfrom
micpapal:fix/tools
Open

micpapal wants to merge 1 commit into
outshift-open:mainfrom
micpapal:fix/tools

Conversation

@micpapal

@micpapal micpapal commented Sep 7, 2026

Copy link
Copy Markdown
Contributor

Problem

Long-running agent conversations that exceed the configured history limit
(max_messages, max_turns, or the token budget) get trimmed by one of the
context_manager strategies (StackConversation, SlidingWindowConversation,
SummarizingConversation). All three cut the message list at a positional
boundary with no awareness of tool-call structure, so a truncation could land
between an assistant tool_calls message and its matching tool result,
sending one without the other.

Bedrock function-calling APIs reject that outright:

BedrockException - Expected toolResult blocks at messages.0.content for the following Ids: call_2

This surfaced in long-running multi-turn agents (e.g. a CONCORD consensus negotiation),
eventually exhausting the LLM call's fallback chain and failing the whole
benchmark run.

token_budget.py trim_messages_to_budget already handled this correctly
via a private _skip_tool_group helper — it just wasn't shared with the
other three strategies.

Fix

  • Add library-standard/.../plugins/context/tool_pairing.py with two shared
    helpers:
    • skip_tool_group(messages, start) — returns how many messages must be
      evicted together as one atomic unit (an assistant tool_calls message
      plus every tool response that references one of those call ids).
    • group_exchanges(past) — builds turn-level exchange groups for
      windowing, folding trailing tool call/result messages into the exchange
      they belong to instead of splitting them into their own single-message
      exchanges.
  • token_budget.py now imports skip_tool_group from the shared module
    instead of keeping a private duplicate (no behavior change).
  • StackConversation.manage_history now evicts from the front in
    tool-call-atomic units instead of a raw past[-max_messages:] slice.
  • SlidingWindowConversation and SummarizingConversation now build their
    exchange groups via group_exchanges, so slicing by turn count can never
    land inside a tool_calls/tool_result pair.

Testing

  • Existing test suite passes unchanged (runtime/tests, library-standard/tests).
  • Added a manual repro scenario (tool_calls immediately followed by its
    result, landing exactly on a trim boundary) confirming no orphaned tool
    message reaches position 0 after trimming, for both StackConversation
    and SlidingWindowConversation.
  • Re-ran the failing benchmark scenario end-to-end; the Bedrock
    Expected toolResult blocks error no longer occurs across 5 consecutive
    runs.

…ion history

Signed-off-by: Michele Papalini <micpapal@cisco.com>
@micpapal
micpapal marked this pull request as ready for review September 8, 2026 07:32
@jordanauge

Copy link
Copy Markdown
Collaborator

Superseded by #62

Thanks for the fix and the repro context. #62 addresses the same Bedrock failure (Expected toolResult blocks … for Ids: call_N) with a different layering:

Why we prefer #62:

  1. Trimming strategies stay simple and do not need to know about tool-call message shape.
  2. Provider invariants are enforced in one place (the assembly/API boundary).
  3. Regression tests cover the same failure modes (stack boundary orphan tools, sliding window splits, token budget on tool-heavy history).

Request: Could you confirm this approach fixes the original CONCORD/Bedrock scenario from your side? If so, we will close #55 when #62 merges.

Happy to compare diffs or run your benchmark scenario against our branch if useful.

jordanauge added a commit that referenced this pull request Sep 11, 2026
- Repair committed history at assembly via sanitize_provider_messages
- Pin in-turn working memory with token-budget pin_tail
- Add per-session kernel outbound-waits ledger (MODEL/TOOL/HITL)
- Fix assembler to retain tool messages and skip live-turn sanitize
- Fix pre-existing driver.py lint issues; document zero-tolerance for failures in AGENTS.md
jordanauge added a commit that referenced this pull request Sep 11, 2026
- Repair committed history at assembly via sanitize_provider_messages
- Pin in-turn working memory with token-budget pin_tail
- Add per-session kernel outbound-waits ledger (MODEL/TOOL/HITL)
- Fix assembler to retain tool messages and skip live-turn sanitize
- Fix pre-existing driver.py lint issues; document zero-tolerance for failures in AGENTS.md

Signed-off-by: Jordan Augé <augjorda@cisco.com>
jordanauge added a commit that referenced this pull request Sep 11, 2026
…) (#62)

- Repair committed history at assembly via sanitize_provider_messages
- Pin in-turn working memory with token-budget pin_tail
- Add per-session kernel outbound-waits ledger (MODEL/TOOL/HITL)
- Fix assembler to retain tool messages and skip live-turn sanitize
- Fix pre-existing driver.py lint issues; document zero-tolerance for failures in AGENTS.md

Signed-off-by: Jordan Augé <augjorda@cisco.com>
@jordanauge

Copy link
Copy Markdown
Collaborator

Superseded and merged via #62 (247131e). Closing in favour of assembly-boundary repair.

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.

2 participants