Skip to content

Commit 47844c5

Browse files
fix: defer truncation when last message has pending tool_use
1 parent bd0309e commit 47844c5

2 files changed

Lines changed: 97 additions & 0 deletions

File tree

src/askui/models/shared/truncation_strategies.py

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,27 @@ def _has_orphaned_tool_results(msg: MessageParam) -> bool:
5454
return any(isinstance(b, ToolResultBlockParam) for b in msg.content)
5555

5656

57+
def _has_pending_tool_use(messages: list[MessageParam]) -> bool:
58+
"""Check if the last message is an assistant message with tool_use blocks.
59+
60+
Truncation can fire when ``Conversation._get_next_message``
61+
adds the assistant response to history *before* the matching
62+
``tool_result`` user message is appended by
63+
``_execute_tools_if_present``. In that window the history
64+
ends on an assistant ``tool_use``, and
65+
``_summarize_message_history`` would append a plain-text user
66+
message (the summarization prompt) which violates the API
67+
constraint that every ``tool_use`` must be followed by its
68+
``tool_result``.
69+
"""
70+
if not messages:
71+
return False
72+
last = messages[-1]
73+
if last.role != "assistant" or isinstance(last.content, str):
74+
return False
75+
return any(isinstance(b, ToolUseBlockParam) for b in last.content)
76+
77+
5778
def _summarize_message_history(
5879
vlm_provider: VlmProvider,
5980
messages: list[MessageParam],
@@ -379,6 +400,11 @@ def truncate(self) -> None:
379400
msg = "Cannot truncate: no vlm_provider available"
380401
logger.warning(msg)
381402
return
403+
if _has_pending_tool_use(self._truncated_message_history):
404+
logger.debug(
405+
"Deferring truncation: last message has pending tool_use"
406+
)
407+
return
382408

383409
logger.info("Summarizing message history")
384410
system, tools, provider_options = self._summarization_request_context()
@@ -702,6 +728,11 @@ def truncate(self) -> None:
702728
msg = "Cannot truncate: no vlm_provider available"
703729
logger.warning(msg)
704730
return
731+
if _has_pending_tool_use(self._truncated_message_history):
732+
logger.debug(
733+
"Deferring truncation: last message has pending tool_use"
734+
)
735+
return
705736

706737
logger.info("Summarizing message history")
707738
system, tools, provider_options = self._summarization_request_context()

tests/unit/models/test_truncation_strategies.py

Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -471,6 +471,41 @@ def test_auto_truncation_on_token_limit(self) -> None:
471471
# Should have been auto-truncated
472472
vlm.create_message.assert_called_once()
473473

474+
def test_truncate_deferred_when_last_message_has_tool_use(self) -> None:
475+
"""Truncation must not fire when the last message is an assistant
476+
tool_use whose tool_result hasn't been appended yet."""
477+
vlm = _make_vlm_provider()
478+
strategy = _make_strategy(vlm_provider=vlm, n_messages_to_keep=2)
479+
for i in range(4):
480+
role = "user" if i % 2 == 0 else "assistant"
481+
strategy.append_message(MessageParam(role=role, content=f"msg {i}"))
482+
# Append an assistant message with tool_use (simulates the window
483+
# between _get_next_message and _execute_tools_if_present)
484+
strategy.append_message(
485+
MessageParam(
486+
role="assistant",
487+
content=[
488+
ToolUseBlockParam(
489+
id="tu_1", input={}, name="tool_a", type="tool_use"
490+
),
491+
],
492+
)
493+
)
494+
# Truncation should be deferred — VLM must NOT be called
495+
strategy.truncate()
496+
vlm.create_message.assert_not_called()
497+
# After appending the matching tool_result, truncation should proceed
498+
strategy.append_message(
499+
MessageParam(
500+
role="user",
501+
content=[
502+
ToolResultBlockParam(tool_use_id="tu_1", content="result"),
503+
],
504+
)
505+
)
506+
strategy.truncate()
507+
vlm.create_message.assert_called_once()
508+
474509

475510
# ---------------------------------------------------------------------------
476511
# Edge cases
@@ -743,6 +778,37 @@ def test_auto_truncation_on_token_limit(self) -> None:
743778
strategy.append_message(MessageParam(role="user", content="z" * 300))
744779
vlm.create_message.assert_called_once()
745780

781+
def test_truncate_deferred_when_last_message_has_tool_use(self) -> None:
782+
"""Truncation must not fire when the last message is an assistant
783+
tool_use whose tool_result hasn't been appended yet."""
784+
vlm = _make_vlm_provider()
785+
strategy = _make_summarizing_strategy(vlm_provider=vlm, n_messages_to_keep=2)
786+
for i in range(4):
787+
role = "user" if i % 2 == 0 else "assistant"
788+
strategy.append_message(MessageParam(role=role, content=f"msg {i}"))
789+
strategy.append_message(
790+
MessageParam(
791+
role="assistant",
792+
content=[
793+
ToolUseBlockParam(
794+
id="tu_1", input={}, name="tool_a", type="tool_use"
795+
),
796+
],
797+
)
798+
)
799+
strategy.truncate()
800+
vlm.create_message.assert_not_called()
801+
strategy.append_message(
802+
MessageParam(
803+
role="user",
804+
content=[
805+
ToolResultBlockParam(tool_use_id="tu_1", content="result"),
806+
],
807+
)
808+
)
809+
strategy.truncate()
810+
vlm.create_message.assert_called_once()
811+
746812

747813
class TestReporterIntegration:
748814
def test_summarizing_strategy_reports_summary_response(self) -> None:

0 commit comments

Comments
 (0)