refactor: summarize only trimmed history in reactive_compact

reactive_compact summarized the full message history before slicing off
the recent tail, so the kept tail was summarized and then re-attached
verbatim, and the emergency prompt-too-long path re-summarized the whole
oversized context. Move the summarize_history call below the tail
computation (including the tool_use/tool_result pair adjustment) and
summarize only messages[:tail_start], so the summary covers older history
while the recent tail stays verbatim.

Applied to the duplicated function in s08, s09, and s20, with the s08
README snippets (en/zh/ja) updated to match. Adds tests covering the
normal path and the case where a tool pair straddles the tail boundary.

Fixes #350
This commit is contained in:
John Costa
2026-06-15 21:55:21 -07:00
parent 20e7cbb72c
commit 49b3822247
7 changed files with 75 additions and 9 deletions

View File

@@ -139,10 +139,10 @@ This triggers **reactive_compact**: more aggressive than compact_history, it ret
```python
def reactive_compact(messages):
transcript = write_transcript(messages)
summary = summarize_history(messages)
tail_start = max(0, len(messages) - 5)
if _is_tool_result_message(messages[tail_start]) and _message_has_tool_use(messages[tail_start - 1]):
tail_start -= 1
summary = summarize_history(messages[:tail_start])
return [{"role": "user",
"content": f"[Reactive compact]\n\n{summary}"}, *messages[tail_start:]]
```

View File

@@ -139,10 +139,10 @@ API がまだ `prompt_too_long`413を返すことがある。コンテキ
```python
def reactive_compact(messages):
transcript = write_transcript(messages)
summary = summarize_history(messages)
tail_start = max(0, len(messages) - 5)
if _is_tool_result_message(messages[tail_start]) and _message_has_tool_use(messages[tail_start - 1]):
tail_start -= 1
summary = summarize_history(messages[:tail_start])
return [{"role": "user",
"content": f"[Reactive compact]\n\n{summary}"}, *messages[tail_start:]]
```

View File

@@ -139,10 +139,10 @@ def compact_history(messages):
```python
def reactive_compact(messages):
transcript = write_transcript(messages)
summary = summarize_history(messages)
tail_start = max(0, len(messages) - 5)
if _is_tool_result_message(messages[tail_start]) and _message_has_tool_use(messages[tail_start - 1]):
tail_start -= 1
summary = summarize_history(messages[:tail_start])
return [{"role": "user",
"content": f"[Reactive compact]\n\n{summary}"}, *messages[tail_start:]]
```

View File

@@ -382,12 +382,12 @@ def compact_history(messages):
# Emergency: reactiveCompact — on API error
def reactive_compact(messages):
transcript = write_transcript(messages)
summary = summarize_history(messages)
tail_start = max(0, len(messages) - 5)
if (tail_start > 0 and tail_start < len(messages)
and _is_tool_result_message(messages[tail_start])
and _message_has_tool_use(messages[tail_start - 1])):
tail_start -= 1
summary = summarize_history(messages[:tail_start])
return [{"role": "user", "content": f"[Reactive compact]\n\n{summary}"}, *messages[tail_start:]]

View File

@@ -540,12 +540,12 @@ def compact_history(msgs):
def reactive_compact(msgs):
write_transcript(msgs)
summary = summarize_history(msgs)
tail_start = max(0, len(msgs) - 5)
if (tail_start > 0 and tail_start < len(msgs)
and _is_tool_result_message(msgs[tail_start])
and _message_has_tool_use(msgs[tail_start - 1])):
tail_start -= 1
summary = summarize_history(msgs[:tail_start])
return [{"role": "user", "content": f"[Reactive compact]\n\n{summary}"}, *msgs[tail_start:]]

View File

@@ -1190,15 +1190,15 @@ def compact_history(messages: list) -> list:
def reactive_compact(messages: list) -> list:
transcript = write_transcript(messages)
print(f" \033[31m[reactive compact] transcript saved: {transcript}\033[0m")
try:
summary = summarize_history(messages)
except Exception:
summary = "Earlier conversation was trimmed after a prompt-too-long error."
tail_start = max(0, len(messages) - 5)
if (tail_start > 0 and tail_start < len(messages)
and is_tool_result_message(messages[tail_start])
and message_has_tool_use(messages[tail_start - 1])):
tail_start -= 1
try:
summary = summarize_history(messages[:tail_start])
except Exception:
summary = "Earlier conversation was trimmed after a prompt-too-long error."
return [{"role": "user", "content": f"[Reactive compact]\n\n{summary}"},
*messages[tail_start:]]

View File

@@ -178,6 +178,72 @@ class CompactionToolPairTests(unittest.TestCase):
self.assertEqual(compacted[1], messages[3])
assert_no_orphan_tool_results(self, compacted)
def test_reactive_compact_summarizes_only_old_history(self):
messages = [
user_text(),
assistant_text(),
user_text(),
assistant_text(),
user_text(),
assistant_text(),
user_text(),
assistant_text(),
user_text(),
]
for name, path in MODULES.items():
with self.subTest(name=name), tempfile.TemporaryDirectory() as tmp:
module = load_module(f"{name}_reactive_oldhist_under_test", path, Path(tmp))
module.write_transcript = lambda _messages: Path("transcript.jsonl")
captured = {}
def fake_summarize(passed, _store=captured):
_store["messages"] = list(passed)
return "summary"
module.summarize_history = fake_summarize
compacted = module.reactive_compact(list(messages))
# The summary must cover only the old history, not the kept tail.
self.assertEqual(captured["messages"], messages[:4])
# The recent tail is appended verbatim after the summary message.
self.assertEqual(compacted[1:], messages[4:])
assert_no_orphan_tool_results(self, compacted)
def test_reactive_compact_summary_excludes_tail_pair_pulled_in(self):
# A tool_use/tool_result pair straddles the tail boundary, so the
# adjustment pulls the tool_use into the kept tail. The summary must
# cover only what stays trimmed (messages[:adjusted_tail_start]), i.e.
# it must not re-summarize the tool_use that is kept verbatim.
messages = [
user_text(),
assistant_text(),
user_text(),
tool_use_message("reactive-tool"),
tool_result_message("reactive-tool"),
assistant_text(),
user_text(),
assistant_text(),
user_text(),
]
for name, path in MODULES.items():
with self.subTest(name=name), tempfile.TemporaryDirectory() as tmp:
module = load_module(f"{name}_reactive_pairscope_under_test", path, Path(tmp))
module.write_transcript = lambda _messages: Path("transcript.jsonl")
captured = {}
def fake_summarize(passed, _store=captured):
_store["messages"] = list(passed)
return "summary"
module.summarize_history = fake_summarize
compacted = module.reactive_compact(list(messages))
# tail_start starts at 4, decrements to 3 to keep the pair intact.
self.assertEqual(captured["messages"], messages[:3])
self.assertEqual(compacted[1], messages[3])
self.assertEqual(compacted[1:], messages[3:])
assert_no_orphan_tool_results(self, compacted)
def test_s20_has_tool_use_still_accepts_content_blocks(self):
with tempfile.TemporaryDirectory() as tmp:
module = load_module("s20_has_tool_use_under_test", MODULES["s20"], Path(tmp))