mirror of
https://github.com/shareAI-lab/analysis_claude_code.git
synced 2026-08-06 12:03:37 +08:00
Merge pull request #364 from costajohnt/refactor/reactive-compact-summarize-old-history
refactor: summarize only trimmed history in reactive_compact
This commit is contained in:
@@ -46,10 +46,12 @@ def snip_compact(messages, max_messages=50):
|
|||||||
if len(messages) <= max_messages:
|
if len(messages) <= max_messages:
|
||||||
return messages
|
return messages
|
||||||
head_end, tail_start = 3, len(messages) - (max_messages - 3)
|
head_end, tail_start = 3, len(messages) - (max_messages - 3)
|
||||||
if _message_has_tool_use(messages[head_end - 1]):
|
if head_end > 0 and _message_has_tool_use(messages[head_end - 1]):
|
||||||
while head_end < len(messages) and _is_tool_result_message(messages[head_end]):
|
while head_end < len(messages) and _is_tool_result_message(messages[head_end]):
|
||||||
head_end += 1
|
head_end += 1
|
||||||
if _is_tool_result_message(messages[tail_start]) and _message_has_tool_use(messages[tail_start - 1]):
|
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
|
tail_start -= 1
|
||||||
snipped = tail_start - head_end
|
snipped = tail_start - head_end
|
||||||
placeholder = {"role": "user", "content": f"[snipped {snipped} messages from conversation middle]"}
|
placeholder = {"role": "user", "content": f"[snipped {snipped} messages from conversation middle]"}
|
||||||
@@ -139,10 +141,12 @@ This triggers **reactive_compact**: more aggressive than compact_history, it ret
|
|||||||
```python
|
```python
|
||||||
def reactive_compact(messages):
|
def reactive_compact(messages):
|
||||||
transcript = write_transcript(messages)
|
transcript = write_transcript(messages)
|
||||||
summary = summarize_history(messages)
|
|
||||||
tail_start = max(0, len(messages) - 5)
|
tail_start = max(0, len(messages) - 5)
|
||||||
if _is_tool_result_message(messages[tail_start]) and _message_has_tool_use(messages[tail_start - 1]):
|
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
|
tail_start -= 1
|
||||||
|
summary = summarize_history(messages[:tail_start])
|
||||||
return [{"role": "user",
|
return [{"role": "user",
|
||||||
"content": f"[Reactive compact]\n\n{summary}"}, *messages[tail_start:]]
|
"content": f"[Reactive compact]\n\n{summary}"}, *messages[tail_start:]]
|
||||||
```
|
```
|
||||||
|
|||||||
@@ -46,10 +46,12 @@ def snip_compact(messages, max_messages=50):
|
|||||||
if len(messages) <= max_messages:
|
if len(messages) <= max_messages:
|
||||||
return messages
|
return messages
|
||||||
head_end, tail_start = 3, len(messages) - (max_messages - 3)
|
head_end, tail_start = 3, len(messages) - (max_messages - 3)
|
||||||
if _message_has_tool_use(messages[head_end - 1]):
|
if head_end > 0 and _message_has_tool_use(messages[head_end - 1]):
|
||||||
while head_end < len(messages) and _is_tool_result_message(messages[head_end]):
|
while head_end < len(messages) and _is_tool_result_message(messages[head_end]):
|
||||||
head_end += 1
|
head_end += 1
|
||||||
if _is_tool_result_message(messages[tail_start]) and _message_has_tool_use(messages[tail_start - 1]):
|
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
|
tail_start -= 1
|
||||||
snipped = tail_start - head_end
|
snipped = tail_start - head_end
|
||||||
placeholder = {"role": "user", "content": f"[snipped {snipped} messages from conversation middle]"}
|
placeholder = {"role": "user", "content": f"[snipped {snipped} messages from conversation middle]"}
|
||||||
@@ -139,10 +141,12 @@ API がまだ `prompt_too_long`(413)を返すことがある。コンテキ
|
|||||||
```python
|
```python
|
||||||
def reactive_compact(messages):
|
def reactive_compact(messages):
|
||||||
transcript = write_transcript(messages)
|
transcript = write_transcript(messages)
|
||||||
summary = summarize_history(messages)
|
|
||||||
tail_start = max(0, len(messages) - 5)
|
tail_start = max(0, len(messages) - 5)
|
||||||
if _is_tool_result_message(messages[tail_start]) and _message_has_tool_use(messages[tail_start - 1]):
|
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
|
tail_start -= 1
|
||||||
|
summary = summarize_history(messages[:tail_start])
|
||||||
return [{"role": "user",
|
return [{"role": "user",
|
||||||
"content": f"[Reactive compact]\n\n{summary}"}, *messages[tail_start:]]
|
"content": f"[Reactive compact]\n\n{summary}"}, *messages[tail_start:]]
|
||||||
```
|
```
|
||||||
|
|||||||
@@ -46,10 +46,12 @@ def snip_compact(messages, max_messages=50):
|
|||||||
if len(messages) <= max_messages:
|
if len(messages) <= max_messages:
|
||||||
return messages
|
return messages
|
||||||
head_end, tail_start = 3, len(messages) - (max_messages - 3)
|
head_end, tail_start = 3, len(messages) - (max_messages - 3)
|
||||||
if _message_has_tool_use(messages[head_end - 1]):
|
if head_end > 0 and _message_has_tool_use(messages[head_end - 1]):
|
||||||
while head_end < len(messages) and _is_tool_result_message(messages[head_end]):
|
while head_end < len(messages) and _is_tool_result_message(messages[head_end]):
|
||||||
head_end += 1
|
head_end += 1
|
||||||
if _is_tool_result_message(messages[tail_start]) and _message_has_tool_use(messages[tail_start - 1]):
|
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
|
tail_start -= 1
|
||||||
snipped = tail_start - head_end
|
snipped = tail_start - head_end
|
||||||
placeholder = {"role": "user", "content": f"[snipped {snipped} messages from conversation middle]"}
|
placeholder = {"role": "user", "content": f"[snipped {snipped} messages from conversation middle]"}
|
||||||
@@ -139,10 +141,12 @@ def compact_history(messages):
|
|||||||
```python
|
```python
|
||||||
def reactive_compact(messages):
|
def reactive_compact(messages):
|
||||||
transcript = write_transcript(messages)
|
transcript = write_transcript(messages)
|
||||||
summary = summarize_history(messages)
|
|
||||||
tail_start = max(0, len(messages) - 5)
|
tail_start = max(0, len(messages) - 5)
|
||||||
if _is_tool_result_message(messages[tail_start]) and _message_has_tool_use(messages[tail_start - 1]):
|
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
|
tail_start -= 1
|
||||||
|
summary = summarize_history(messages[:tail_start])
|
||||||
return [{"role": "user",
|
return [{"role": "user",
|
||||||
"content": f"[Reactive compact]\n\n{summary}"}, *messages[tail_start:]]
|
"content": f"[Reactive compact]\n\n{summary}"}, *messages[tail_start:]]
|
||||||
```
|
```
|
||||||
|
|||||||
@@ -382,12 +382,12 @@ def compact_history(messages):
|
|||||||
# Emergency: reactiveCompact — on API error
|
# Emergency: reactiveCompact — on API error
|
||||||
def reactive_compact(messages):
|
def reactive_compact(messages):
|
||||||
transcript = write_transcript(messages)
|
transcript = write_transcript(messages)
|
||||||
summary = summarize_history(messages)
|
|
||||||
tail_start = max(0, len(messages) - 5)
|
tail_start = max(0, len(messages) - 5)
|
||||||
if (tail_start > 0 and tail_start < len(messages)
|
if (tail_start > 0 and tail_start < len(messages)
|
||||||
and _is_tool_result_message(messages[tail_start])
|
and _is_tool_result_message(messages[tail_start])
|
||||||
and _message_has_tool_use(messages[tail_start - 1])):
|
and _message_has_tool_use(messages[tail_start - 1])):
|
||||||
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:]]
|
return [{"role": "user", "content": f"[Reactive compact]\n\n{summary}"}, *messages[tail_start:]]
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -540,12 +540,12 @@ def compact_history(msgs):
|
|||||||
|
|
||||||
def reactive_compact(msgs):
|
def reactive_compact(msgs):
|
||||||
write_transcript(msgs)
|
write_transcript(msgs)
|
||||||
summary = summarize_history(msgs)
|
|
||||||
tail_start = max(0, len(msgs) - 5)
|
tail_start = max(0, len(msgs) - 5)
|
||||||
if (tail_start > 0 and tail_start < len(msgs)
|
if (tail_start > 0 and tail_start < len(msgs)
|
||||||
and _is_tool_result_message(msgs[tail_start])
|
and _is_tool_result_message(msgs[tail_start])
|
||||||
and _message_has_tool_use(msgs[tail_start - 1])):
|
and _message_has_tool_use(msgs[tail_start - 1])):
|
||||||
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:]]
|
return [{"role": "user", "content": f"[Reactive compact]\n\n{summary}"}, *msgs[tail_start:]]
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -1190,15 +1190,15 @@ def compact_history(messages: list) -> list:
|
|||||||
def reactive_compact(messages: list) -> list:
|
def reactive_compact(messages: list) -> list:
|
||||||
transcript = write_transcript(messages)
|
transcript = write_transcript(messages)
|
||||||
print(f" \033[31m[reactive compact] transcript saved: {transcript}\033[0m")
|
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)
|
tail_start = max(0, len(messages) - 5)
|
||||||
if (tail_start > 0 and tail_start < len(messages)
|
if (tail_start > 0 and tail_start < len(messages)
|
||||||
and is_tool_result_message(messages[tail_start])
|
and is_tool_result_message(messages[tail_start])
|
||||||
and message_has_tool_use(messages[tail_start - 1])):
|
and message_has_tool_use(messages[tail_start - 1])):
|
||||||
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}"},
|
return [{"role": "user", "content": f"[Reactive compact]\n\n{summary}"},
|
||||||
*messages[tail_start:]]
|
*messages[tail_start:]]
|
||||||
|
|
||||||
|
|||||||
@@ -178,6 +178,72 @@ class CompactionToolPairTests(unittest.TestCase):
|
|||||||
self.assertEqual(compacted[1], messages[3])
|
self.assertEqual(compacted[1], messages[3])
|
||||||
assert_no_orphan_tool_results(self, compacted)
|
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):
|
def test_s20_has_tool_use_still_accepts_content_blocks(self):
|
||||||
with tempfile.TemporaryDirectory() as tmp:
|
with tempfile.TemporaryDirectory() as tmp:
|
||||||
module = load_module("s20_has_tool_use_under_test", MODULES["s20"], Path(tmp))
|
module = load_module("s20_has_tool_use_under_test", MODULES["s20"], Path(tmp))
|
||||||
|
|||||||
Reference in New Issue
Block a user