diff --git a/s08_context_compact/README.ja.md b/s08_context_compact/README.ja.md index f775876c..ef67a97c 100644 --- a/s08_context_compact/README.ja.md +++ b/s08_context_compact/README.ja.md @@ -117,7 +117,7 @@ messages = [*messages[:head_end], marker, *messages[tail_start:]] ## ステップ 3:micro_compact -`micro_compact` は、現在の履歴にあるすべての `tool_result` を収集します。最新 3 件は完全に保持し、それより古く 120 文字を超える結果を短くします。保存済みの結果にはファイルパスを残し、それ以外はプレースホルダーに置き換えます。 +`micro_compact` は最新の `tool_result` バッチを完全に保持し、さらに以前のバッチから最新 3 件を残します。それより古く 120 文字を超える結果を短くします。保存済みの結果にはファイルパスを残し、それ以外はプレースホルダーに置き換えます。 ![古い結果を置き換える](images/micro-compact.ja.svg) @@ -305,7 +305,7 @@ s01_agent_loop から s05_todo_write までの README.md を読み、 各ファイルの最上位見出しを比較して、命名の規則をまとめてください。 ``` -このタスクでは少なくとも 5 件のファイル結果が生成されます。最新 3 件は完全に残り、それより前の長い結果は `[Earlier tool result omitted.]` に変わります。保存済みの結果には保存先のパスが残ります。 +このタスクでは少なくとも 5 件のファイル結果が生成されます。最新のバッチと、それ以前の最新 3 件は完全に残り、それより前の長い結果は `[Earlier tool result omitted.]` に変わります。保存済みの結果には保存先のパスが残ります。 ### 実験 2:大きな結果を保存する diff --git a/s08_context_compact/README.md b/s08_context_compact/README.md index f5eadcb2..d564f12a 100644 --- a/s08_context_compact/README.md +++ b/s08_context_compact/README.md @@ -117,7 +117,7 @@ This step controls the number of messages. Tool results inside the retained mess ## Step 3: micro_compact -`micro_compact` collects all current `tool_result` blocks. It preserves the latest 3 results and shortens earlier results longer than 120 characters. Persisted results keep their file path; the rest become placeholders: +`micro_compact` preserves the newest `tool_result` batch in full, then keeps the latest 3 results from earlier batches and shortens older results longer than 120 characters. Persisted results keep their file path; the rest become placeholders: ![Replacing old results](images/micro-compact.en.svg) @@ -305,7 +305,7 @@ Read the README.md files from s01_agent_loop through s05_todo_write. Compare their top-level headings and summarize the naming pattern. ``` -This task produces at least 5 file results. The latest 3 remain complete, while earlier long results become `[Earlier tool result omitted.]`. A persisted result retains its saved path. +This task produces at least 5 file results. The newest batch and the latest 3 earlier results remain complete, while older long results become `[Earlier tool result omitted.]`. A persisted result retains its saved path. ### Experiment 2: Persist a Large Result diff --git a/s08_context_compact/README.zh.md b/s08_context_compact/README.zh.md index 3440be1e..75c60d55 100644 --- a/s08_context_compact/README.zh.md +++ b/s08_context_compact/README.zh.md @@ -117,7 +117,7 @@ messages = [*messages[:head_end], marker, *messages[tail_start:]] ## 第三步:micro_compact -`micro_compact` 收集当前历史里的全部 `tool_result`。最近 3 条保持完整,更早且超过 120 个字符的结果会缩短。已经转存的结果保留文件路径,其他结果只留下占位符: +`micro_compact` 会完整保留最新一批 `tool_result`,再保留更早批次中最近 3 条结果;其余超过 120 个字符的旧结果会缩短。已经转存的结果保留文件路径,其他结果只留下占位符: ![旧结果替换为占位符](images/micro-compact.svg) @@ -305,7 +305,7 @@ python s08_context_compact/code.py 比较它们的一级标题,并总结这些标题的命名规律。 ``` -任务会产生至少 5 条文件读取结果。最近 3 条保持完整,更早且较长的结果会变成 `[Earlier tool result omitted.]`。已经转存的结果会保留保存路径。 +任务会产生至少 5 条文件读取结果。最新一批以及更早批次中最近 3 条结果保持完整,更早且较长的结果会变成 `[Earlier tool result omitted.]`。已经转存的结果会保留保存路径。 ### 实验二:大结果转存 diff --git a/s08_context_compact/code.py b/s08_context_compact/code.py index d758d050..a00f2a04 100644 --- a/s08_context_compact/code.py +++ b/s08_context_compact/code.py @@ -331,7 +331,20 @@ class ContextCompactor: for block in message["content"] if isinstance(block, dict) and block.get("type") == "tool_result" ] - for block in results[:-self.KEEP_RECENT_RESULTS]: + latest_batch = [] + for message in reversed(messages): + content = message.get("content") + if message.get("role") != "user" or not isinstance(content, list): + continue + latest_batch = [ + block for block in content + if isinstance(block, dict) and block.get("type") == "tool_result" + ] + if latest_batch: + break + latest_batch_ids = {id(block) for block in latest_batch} + older_results = [block for block in results if id(block) not in latest_batch_ids] + for block in older_results[:-self.KEEP_RECENT_RESULTS]: content = str(block.get("content", "")) if len(content) <= 120: continue diff --git a/s15_integrated_harness/code.py b/s15_integrated_harness/code.py index bfefff58..d0b14ac5 100644 --- a/s15_integrated_harness/code.py +++ b/s15_integrated_harness/code.py @@ -1959,9 +1959,20 @@ def snip_compact(messages: list, max_messages: int = 50) -> list: def micro_compact(messages: list) -> list: tool_results = collect_tool_results(messages) - if len(tool_results) <= KEEP_RECENT_TOOL_RESULTS: - return messages - for _, _, block in tool_results[:-KEEP_RECENT_TOOL_RESULTS]: + latest_batch = [] + for message in reversed(messages): + content = message.get("content") + if message.get("role") != "user" or not isinstance(content, list): + continue + latest_batch = [ + block for block in content + if isinstance(block, dict) and block.get("type") == "tool_result" + ] + if latest_batch: + break + latest_batch_ids = {id(block) for block in latest_batch} + older_results = [entry for entry in tool_results if id(entry[2]) not in latest_batch_ids] + for _, _, block in older_results[:-KEEP_RECENT_TOOL_RESULTS]: if len(str(block.get("content", ""))) > 120: block["content"] = "[Earlier tool result compacted. Re-run if needed.]" return messages diff --git a/tests/test_compaction_tool_pairs.py b/tests/test_compaction_tool_pairs.py index be5ca323..d0eac2c7 100644 --- a/tests/test_compaction_tool_pairs.py +++ b/tests/test_compaction_tool_pairs.py @@ -86,6 +86,17 @@ def tool_result_message(tool_id="tool-1"): } +def long_tool_result_batch(*tool_ids): + return { + "role": "user", + "content": [ + {"type": "tool_result", "tool_use_id": tool_id, + "content": f"{tool_id}: " + "x" * 160} + for tool_id in tool_ids + ], + } + + def message_has_tool_use(message): content = message.get("content") return ( @@ -112,6 +123,33 @@ def compaction_api(module): class CompactionToolPairTests(unittest.TestCase): + def test_micro_compact_keeps_latest_tool_result_batch(self): + for name, path in MODULES.items(): + with self.subTest(name=name), tempfile.TemporaryDirectory() as tmp: + messages = [ + long_tool_result_batch("old-1"), + long_tool_result_batch("old-2"), + long_tool_result_batch("old-3"), + long_tool_result_batch("old-4"), + user_text(), + long_tool_result_batch( + "latest-1", "latest-2", "latest-3", "latest-4" + ), + ] + module = load_module(f"{name}_micro_batch_under_test", path, Path(tmp)) + compacted = compaction_api(module).micro_compact(messages) + results = { + block["tool_use_id"]: block["content"] + for message in compacted + if isinstance(message["content"], list) + for block in message["content"] + if isinstance(block, dict) and block.get("type") == "tool_result" + } + self.assertNotIn("old-1: ", results["old-1"]) + for tool_id in ("old-2", "old-3", "old-4", + "latest-1", "latest-2", "latest-3", "latest-4"): + self.assertIn(f"{tool_id}: ", results[tool_id]) + def test_snip_compact_keeps_head_tool_pair(self): messages = [ user_text(),