mirror of
https://github.com/shareAI-lab/analysis_claude_code.git
synced 2026-06-21 04:33:36 +08:00
fix: keep tool-use/result pairs intact during compaction
Preserve assistant tool_use / user tool_result adjacency when compaction trims message history. Fixes #325. Squashed original PR commits: - Fix compaction breaking tool-use/result pairs - Simplify compaction boundary fix
This commit is contained in:
@@ -39,20 +39,24 @@ Core design: cheap first, expensive last.
|
|||||||
|
|
||||||
The agent ran 80 turns of conversation, accumulating 160 `messages`. The very first "help me create hello.py" is barely relevant to current work, yet it still occupies space.
|
The agent ran 80 turns of conversation, accumulating 160 `messages`. The very first "help me create hello.py" is barely relevant to current work, yet it still occupies space.
|
||||||
|
|
||||||
Message count exceeds 50 → keep the first 3 (initial context) and the last 47 (current work), trim the middle:
|
Message count exceeds 50 → keep the first 3 (initial context) and the last 47 (current work), trim the middle; the only extra boundary rule is that `assistant(tool_use)` must not be separated from the following `user(tool_result)`:
|
||||||
|
|
||||||
```python
|
```python
|
||||||
def snip_compact(messages, max_messages=50):
|
def snip_compact(messages, max_messages=50):
|
||||||
if len(messages) <= max_messages:
|
if len(messages) <= max_messages:
|
||||||
return messages
|
return messages
|
||||||
keep_head, keep_tail = 3, max_messages - 3
|
head_end, tail_start = 3, len(messages) - (max_messages - 3)
|
||||||
snipped = len(messages) - keep_head - keep_tail
|
if has_tool_use(messages[head_end - 1]):
|
||||||
placeholder = {"role": "user",
|
while head_end < len(messages) and is_tool_result_message(messages[head_end]):
|
||||||
"content": f"[snipped {snipped} messages from conversation middle]"}
|
head_end += 1
|
||||||
return messages[:keep_head] + [placeholder] + messages[-keep_tail:]
|
if is_tool_result_message(messages[tail_start]) and has_tool_use(messages[tail_start - 1]):
|
||||||
|
tail_start -= 1
|
||||||
|
snipped = tail_start - head_end
|
||||||
|
placeholder = {"role": "user", "content": f"[snipped {snipped} messages from conversation middle]"}
|
||||||
|
return messages[:head_end] + [placeholder] + messages[tail_start:]
|
||||||
```
|
```
|
||||||
|
|
||||||
Entire messages are trimmed, but `tool_result` content within remaining messages keeps accumulating — message #34 may still hold 30KB of old file contents. → L2.
|
Messages are still trimmed directly; this just adds one boundary guard. `tool_result` content within remaining messages still keeps accumulating — message #34 may still hold 30KB of old file contents. → L2.
|
||||||
|
|
||||||
### L2: micro_compact — Placeholder for Old Tool Results
|
### L2: micro_compact — Placeholder for Old Tool Results
|
||||||
|
|
||||||
@@ -130,15 +134,17 @@ def compact_history(messages):
|
|||||||
|
|
||||||
Sometimes the API still returns `prompt_too_long` (413) — when context grows faster than compression triggers.
|
Sometimes the API still returns `prompt_too_long` (413) — when context grows faster than compression triggers.
|
||||||
|
|
||||||
This triggers **reactive_compact**: more aggressive than compact_history, it retreats from the tail, trimming to an API-acceptable size with byte-level precision, keeping only the last 5 messages + summary.
|
This triggers **reactive_compact**: more aggressive than compact_history, it retreats from the tail, but still avoids leaving an orphaned `tool_result`.
|
||||||
|
|
||||||
```python
|
```python
|
||||||
def reactive_compact(messages):
|
def reactive_compact(messages):
|
||||||
transcript = write_transcript(messages)
|
transcript = write_transcript(messages)
|
||||||
summary = summarize_history(messages)
|
summary = summarize_history(messages)
|
||||||
tail = messages[-5:]
|
tail_start = max(0, len(messages) - 5)
|
||||||
|
if is_tool_result_message(messages[tail_start]) and has_tool_use(messages[tail_start - 1]):
|
||||||
|
tail_start -= 1
|
||||||
return [{"role": "user",
|
return [{"role": "user",
|
||||||
"content": f"[Reactive compact]\n\n{summary}"}, *tail]
|
"content": f"[Reactive compact]\n\n{summary}"}, *messages[tail_start:]]
|
||||||
```
|
```
|
||||||
|
|
||||||
Reactive compact has a retry limit (default 1). If it still fails, an exception is raised instead of looping forever. Full error recovery is deferred to s11.
|
Reactive compact has a retry limit (default 1). If it still fails, an exception is raised instead of looping forever. Full error recovery is deferred to s11.
|
||||||
|
|||||||
@@ -39,20 +39,24 @@ s07 のフック構造、スキルロード、サブ Agent の骨格を維持し
|
|||||||
|
|
||||||
Agent が 80 ラウンドの会話を実行し、`messages` が 160 件まで溜まった。先頭の「hello.py を作って」は現在の作業とほぼ無関係だが、スペースを占有し続けている。
|
Agent が 80 ラウンドの会話を実行し、`messages` が 160 件まで溜まった。先頭の「hello.py を作って」は現在の作業とほぼ無関係だが、スペースを占有し続けている。
|
||||||
|
|
||||||
メッセージ数が 50 を超えた場合 → 先頭 3 件(初期コンテキスト)と末尾 47 件(現在の作業)を保持し、中間を切り捨て:
|
メッセージ数が 50 を超えた場合 → 先頭 3 件(初期コンテキスト)と末尾 47 件(現在の作業)を保持して中間を切り詰める。ただし切れ目だけは調整し、`assistant(tool_use)` と後続の `user(tool_result)` を分断しない:
|
||||||
|
|
||||||
```python
|
```python
|
||||||
def snip_compact(messages, max_messages=50):
|
def snip_compact(messages, max_messages=50):
|
||||||
if len(messages) <= max_messages:
|
if len(messages) <= max_messages:
|
||||||
return messages
|
return messages
|
||||||
keep_head, keep_tail = 3, max_messages - 3
|
head_end, tail_start = 3, len(messages) - (max_messages - 3)
|
||||||
snipped = len(messages) - keep_head - keep_tail
|
if has_tool_use(messages[head_end - 1]):
|
||||||
placeholder = {"role": "user",
|
while head_end < len(messages) and is_tool_result_message(messages[head_end]):
|
||||||
"content": f"[snipped {snipped} messages from conversation middle]"}
|
head_end += 1
|
||||||
return messages[:keep_head] + [placeholder] + messages[-keep_tail:]
|
if is_tool_result_message(messages[tail_start]) and has_tool_use(messages[tail_start - 1]):
|
||||||
|
tail_start -= 1
|
||||||
|
snipped = tail_start - head_end
|
||||||
|
placeholder = {"role": "user", "content": f"[snipped {snipped} messages from conversation middle]"}
|
||||||
|
return messages[:head_end] + [placeholder] + messages[tail_start:]
|
||||||
```
|
```
|
||||||
|
|
||||||
メッセージ全体は切り捨てたが、残ったメッセージ内の `tool_result` 内容はまだ蓄積され続けている。34 番目のメッセージに 30KB の古いファイル内容が残っているかもしれない。→ L2。
|
切り捨て自体は単純なままで、境界だけを保護する。残ったメッセージ内の `tool_result` 内容はまだ蓄積され続けている。34 番目のメッセージに 30KB の古いファイル内容が残っているかもしれない。→ L2。
|
||||||
|
|
||||||
### L2: micro_compact — 古いツール結果をプレースホルダに置換
|
### L2: micro_compact — 古いツール結果をプレースホルダに置換
|
||||||
|
|
||||||
@@ -130,15 +134,17 @@ def compact_history(messages):
|
|||||||
|
|
||||||
API がまだ `prompt_too_long`(413)を返すことがある。コンテキストの増加速度が圧縮のトリガー速度を上回る場合。
|
API がまだ `prompt_too_long`(413)を返すことがある。コンテキストの増加速度が圧縮のトリガー速度を上回る場合。
|
||||||
|
|
||||||
この時 **reactive_compact** がトリガーされる:compact_history よりもさらに積極的で、末尾からバイト単位の精度で API が受け入れ可能なサイズまで切り詰め、最後の 5 件のメッセージ + 要約のみを保持。
|
この時 **reactive_compact** がトリガーされる:compact_history よりもさらに積極的だが、末尾を残す際も孤立した `tool_result` を残さないようにする。
|
||||||
|
|
||||||
```python
|
```python
|
||||||
def reactive_compact(messages):
|
def reactive_compact(messages):
|
||||||
transcript = write_transcript(messages)
|
transcript = write_transcript(messages)
|
||||||
summary = summarize_history(messages)
|
summary = summarize_history(messages)
|
||||||
tail = messages[-5:]
|
tail_start = max(0, len(messages) - 5)
|
||||||
|
if is_tool_result_message(messages[tail_start]) and has_tool_use(messages[tail_start - 1]):
|
||||||
|
tail_start -= 1
|
||||||
return [{"role": "user",
|
return [{"role": "user",
|
||||||
"content": f"[Reactive compact]\n\n{summary}"}, *tail]
|
"content": f"[Reactive compact]\n\n{summary}"}, *messages[tail_start:]]
|
||||||
```
|
```
|
||||||
|
|
||||||
reactive compact にはリトライ上限がある(デフォルト 1 回)。さらに失敗した場合は例外をスローし、無限ループしない。完全なエラー回復ロジックは s11 に委ねる。
|
reactive compact にはリトライ上限がある(デフォルト 1 回)。さらに失敗した場合は例外をスローし、無限ループしない。完全なエラー回復ロジックは s11 に委ねる。
|
||||||
|
|||||||
@@ -39,20 +39,24 @@ Agent 跑着跑着,不动了。
|
|||||||
|
|
||||||
Agent 跑了 80 轮对话,`messages` 攒了 160 条。最前面的"帮我创建 hello.py"和当前工作几乎无关了,但全占着位置。
|
Agent 跑了 80 轮对话,`messages` 攒了 160 条。最前面的"帮我创建 hello.py"和当前工作几乎无关了,但全占着位置。
|
||||||
|
|
||||||
消息数超过 50 条 → 保留头部 3 条(初始上下文)和尾部 47 条(当前工作),中间裁掉:
|
消息数超过 50 条 → 保留头部 3 条(初始上下文)和尾部 47 条(当前工作),中间裁掉;唯一额外边界条件是,不能把 `assistant(tool_use)` 和后面的 `user(tool_result)` 拆开:
|
||||||
|
|
||||||
```python
|
```python
|
||||||
def snip_compact(messages, max_messages=50):
|
def snip_compact(messages, max_messages=50):
|
||||||
if len(messages) <= max_messages:
|
if len(messages) <= max_messages:
|
||||||
return messages
|
return messages
|
||||||
keep_head, keep_tail = 3, max_messages - 3
|
head_end, tail_start = 3, len(messages) - (max_messages - 3)
|
||||||
snipped = len(messages) - keep_head - keep_tail
|
if has_tool_use(messages[head_end - 1]):
|
||||||
placeholder = {"role": "user",
|
while head_end < len(messages) and is_tool_result_message(messages[head_end]):
|
||||||
"content": f"[snipped {snipped} messages from conversation middle]"}
|
head_end += 1
|
||||||
return messages[:keep_head] + [placeholder] + messages[-keep_tail:]
|
if is_tool_result_message(messages[tail_start]) and has_tool_use(messages[tail_start - 1]):
|
||||||
|
tail_start -= 1
|
||||||
|
snipped = tail_start - head_end
|
||||||
|
placeholder = {"role": "user", "content": f"[snipped {snipped} messages from conversation middle]"}
|
||||||
|
return messages[:head_end] + [placeholder] + messages[tail_start:]
|
||||||
```
|
```
|
||||||
|
|
||||||
裁掉了整条消息,但剩下的消息里 `tool_result` 内容仍在累积——第 34 条消息里可能躺着 30KB 的旧文件内容。→ L2。
|
裁掉的是消息本身,只是在切口处多做一步保护;剩下的消息里 `tool_result` 内容仍在累积——第 34 条消息里可能躺着 30KB 的旧文件内容。→ L2。
|
||||||
|
|
||||||
### L2: micro_compact — 旧工具结果占位
|
### L2: micro_compact — 旧工具结果占位
|
||||||
|
|
||||||
@@ -130,15 +134,17 @@ def compact_history(messages):
|
|||||||
|
|
||||||
有时候 API 还是返回 `prompt_too_long`(413),上下文增长速度快于压缩触发速度时。
|
有时候 API 还是返回 `prompt_too_long`(413),上下文增长速度快于压缩触发速度时。
|
||||||
|
|
||||||
这时触发 **reactive_compact**:比 compact_history 更激进,从尾部回退,以字节级精度裁剪到 API 可接受的大小,只保留最后 5 条消息 + 摘要。
|
这时触发 **reactive_compact**:比 compact_history 更激进,从尾部回退,但仍要避免留下孤立 `tool_result`。
|
||||||
|
|
||||||
```python
|
```python
|
||||||
def reactive_compact(messages):
|
def reactive_compact(messages):
|
||||||
transcript = write_transcript(messages)
|
transcript = write_transcript(messages)
|
||||||
summary = summarize_history(messages)
|
summary = summarize_history(messages)
|
||||||
tail = messages[-5:]
|
tail_start = max(0, len(messages) - 5)
|
||||||
|
if is_tool_result_message(messages[tail_start]) and has_tool_use(messages[tail_start - 1]):
|
||||||
|
tail_start -= 1
|
||||||
return [{"role": "user",
|
return [{"role": "user",
|
||||||
"content": f"[Reactive compact]\n\n{summary}"}, *tail]
|
"content": f"[Reactive compact]\n\n{summary}"}, *messages[tail_start:]]
|
||||||
```
|
```
|
||||||
|
|
||||||
reactive compact 有重试上限(默认 1 次)。再失败就抛出异常,不无限循环。完整的错误恢复逻辑留给 s11。
|
reactive compact 有重试上限(默认 1 次)。再失败就抛出异常,不无限循环。完整的错误恢复逻辑留给 s11。
|
||||||
|
|||||||
@@ -268,13 +268,40 @@ PERSIST_THRESHOLD = 30000
|
|||||||
|
|
||||||
def estimate_size(msgs): return len(str(msgs))
|
def estimate_size(msgs): return len(str(msgs))
|
||||||
|
|
||||||
|
def _block_type(block):
|
||||||
|
return getattr(block, "type", None) if not isinstance(block, dict) else block.get("type")
|
||||||
|
|
||||||
|
def _has_tool_use(msg):
|
||||||
|
if msg.get("role") != "assistant":
|
||||||
|
return False
|
||||||
|
content = msg.get("content")
|
||||||
|
if not isinstance(content, list):
|
||||||
|
return False
|
||||||
|
return any(_block_type(block) == "tool_use" for block in content)
|
||||||
|
|
||||||
|
def _is_tool_result_message(msg):
|
||||||
|
if msg.get("role") != "user":
|
||||||
|
return False
|
||||||
|
content = msg.get("content")
|
||||||
|
if not isinstance(content, list):
|
||||||
|
return False
|
||||||
|
return any(isinstance(block, dict) and block.get("type") == "tool_result" for block in content)
|
||||||
|
|
||||||
|
|
||||||
# L1: snipCompact — trim middle messages
|
# L1: snipCompact — trim middle messages
|
||||||
def snip_compact(messages, max_messages=50):
|
def snip_compact(messages, max_messages=50):
|
||||||
if len(messages) <= max_messages: return messages
|
if len(messages) <= max_messages: return messages
|
||||||
keep_head, keep_tail = 3, max_messages - 3
|
keep_head, keep_tail = 3, max_messages - 3
|
||||||
snipped = len(messages) - keep_head - keep_tail
|
head_end, tail_start = keep_head, len(messages) - keep_tail
|
||||||
return messages[:keep_head] + [{"role": "user", "content": f"[snipped {snipped} messages]"}] + messages[-keep_tail:]
|
if head_end > 0 and _has_tool_use(messages[head_end - 1]):
|
||||||
|
while head_end < len(messages) and _is_tool_result_message(messages[head_end]):
|
||||||
|
head_end += 1
|
||||||
|
if tail_start > 0 and tail_start < len(messages) and _is_tool_result_message(messages[tail_start]) and _has_tool_use(messages[tail_start - 1]):
|
||||||
|
tail_start -= 1
|
||||||
|
if head_end >= tail_start:
|
||||||
|
return messages
|
||||||
|
snipped = tail_start - head_end
|
||||||
|
return messages[:head_end] + [{"role": "user", "content": f"[snipped {snipped} messages]"}] + messages[tail_start:]
|
||||||
|
|
||||||
|
|
||||||
# L2: microCompact — old result placeholders
|
# L2: microCompact — old result placeholders
|
||||||
@@ -351,7 +378,10 @@ def compact_history(messages):
|
|||||||
def reactive_compact(messages):
|
def reactive_compact(messages):
|
||||||
transcript = write_transcript(messages)
|
transcript = write_transcript(messages)
|
||||||
summary = summarize_history(messages)
|
summary = summarize_history(messages)
|
||||||
return [{"role": "user", "content": f"[Reactive compact]\n\n{summary}"}, *messages[-5:]]
|
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 _has_tool_use(messages[tail_start - 1]):
|
||||||
|
tail_start -= 1
|
||||||
|
return [{"role": "user", "content": f"[Reactive compact]\n\n{summary}"}, *messages[tail_start:]]
|
||||||
|
|
||||||
|
|
||||||
# ═══════════════════════════════════════════════════════════
|
# ═══════════════════════════════════════════════════════════
|
||||||
|
|||||||
@@ -449,9 +449,36 @@ CONTEXT_LIMIT = 50000; KEEP_RECENT = 3; PERSIST_THRESHOLD = 30000
|
|||||||
|
|
||||||
def estimate_size(msgs): return len(str(msgs))
|
def estimate_size(msgs): return len(str(msgs))
|
||||||
|
|
||||||
|
def _block_type(block):
|
||||||
|
return getattr(block, "type", None) if not isinstance(block, dict) else block.get("type")
|
||||||
|
|
||||||
|
def _has_tool_use(msg):
|
||||||
|
if msg.get("role") != "assistant":
|
||||||
|
return False
|
||||||
|
content = msg.get("content")
|
||||||
|
if not isinstance(content, list):
|
||||||
|
return False
|
||||||
|
return any(_block_type(block) == "tool_use" for block in content)
|
||||||
|
|
||||||
|
def _is_tool_result_message(msg):
|
||||||
|
if msg.get("role") != "user":
|
||||||
|
return False
|
||||||
|
content = msg.get("content")
|
||||||
|
if not isinstance(content, list):
|
||||||
|
return False
|
||||||
|
return any(isinstance(block, dict) and block.get("type") == "tool_result" for block in content)
|
||||||
|
|
||||||
def snip_compact(msgs, mx=50):
|
def snip_compact(msgs, mx=50):
|
||||||
if len(msgs) <= mx: return msgs
|
if len(msgs) <= mx: return msgs
|
||||||
return msgs[:3] + [{"role": "user", "content": f"[snipped {len(msgs)-mx} msgs]"}] + msgs[-(mx-3):]
|
head_end, tail_start = 3, len(msgs) - (mx - 3)
|
||||||
|
if head_end > 0 and _has_tool_use(msgs[head_end - 1]):
|
||||||
|
while head_end < len(msgs) and _is_tool_result_message(msgs[head_end]):
|
||||||
|
head_end += 1
|
||||||
|
if tail_start > 0 and tail_start < len(msgs) and _is_tool_result_message(msgs[tail_start]) and _has_tool_use(msgs[tail_start - 1]):
|
||||||
|
tail_start -= 1
|
||||||
|
if head_end >= tail_start:
|
||||||
|
return msgs
|
||||||
|
return msgs[:head_end] + [{"role": "user", "content": f"[snipped {tail_start - head_end} msgs]"}] + msgs[tail_start:]
|
||||||
|
|
||||||
def collect_tool_results(msgs):
|
def collect_tool_results(msgs):
|
||||||
blocks = []
|
blocks = []
|
||||||
@@ -512,7 +539,10 @@ def compact_history(msgs):
|
|||||||
def reactive_compact(msgs):
|
def reactive_compact(msgs):
|
||||||
write_transcript(msgs)
|
write_transcript(msgs)
|
||||||
summary = summarize_history(msgs)
|
summary = summarize_history(msgs)
|
||||||
return [{"role": "user", "content": f"[Reactive compact]\n\n{summary}"}, *msgs[-5:]]
|
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 _has_tool_use(msgs[tail_start - 1]):
|
||||||
|
tail_start -= 1
|
||||||
|
return [{"role": "user", "content": f"[Reactive compact]\n\n{summary}"}, *msgs[tail_start:]]
|
||||||
|
|
||||||
|
|
||||||
# ═══════════════════════════════════════════════════════════
|
# ═══════════════════════════════════════════════════════════
|
||||||
|
|||||||
@@ -1060,6 +1060,26 @@ def spawn_subagent(description: str) -> str:
|
|||||||
def estimate_size(messages: list) -> int:
|
def estimate_size(messages: list) -> int:
|
||||||
return len(json.dumps(messages, default=str))
|
return len(json.dumps(messages, default=str))
|
||||||
|
|
||||||
|
def block_type(block):
|
||||||
|
return getattr(block, "type", None) if not isinstance(block, dict) else block.get("type")
|
||||||
|
|
||||||
|
def has_tool_use(message: dict) -> bool:
|
||||||
|
if message.get("role") != "assistant":
|
||||||
|
return False
|
||||||
|
content = message.get("content")
|
||||||
|
if not isinstance(content, list):
|
||||||
|
return False
|
||||||
|
return any(block_type(block) == "tool_use" for block in content)
|
||||||
|
|
||||||
|
def is_tool_result_message(message: dict) -> bool:
|
||||||
|
if message.get("role") != "user":
|
||||||
|
return False
|
||||||
|
content = message.get("content")
|
||||||
|
if not isinstance(content, list):
|
||||||
|
return False
|
||||||
|
return any(isinstance(block, dict) and block.get("type") == "tool_result"
|
||||||
|
for block in content)
|
||||||
|
|
||||||
|
|
||||||
def collect_tool_results(messages: list):
|
def collect_tool_results(messages: list):
|
||||||
found = []
|
found = []
|
||||||
@@ -1111,11 +1131,18 @@ def tool_result_budget(messages: list, max_bytes: int = 200_000) -> list:
|
|||||||
def snip_compact(messages: list, max_messages: int = 50) -> list:
|
def snip_compact(messages: list, max_messages: int = 50) -> list:
|
||||||
if len(messages) <= max_messages:
|
if len(messages) <= max_messages:
|
||||||
return messages
|
return messages
|
||||||
keep_head, keep_tail = 3, max_messages - 3
|
head_end, tail_start = 3, len(messages) - (max_messages - 3)
|
||||||
snipped = len(messages) - keep_head - keep_tail
|
if head_end > 0 and has_tool_use(messages[head_end - 1]):
|
||||||
return (messages[:keep_head]
|
while head_end < len(messages) and is_tool_result_message(messages[head_end]):
|
||||||
|
head_end += 1
|
||||||
|
if tail_start > 0 and tail_start < len(messages) and is_tool_result_message(messages[tail_start]) and has_tool_use(messages[tail_start - 1]):
|
||||||
|
tail_start -= 1
|
||||||
|
if head_end >= tail_start:
|
||||||
|
return messages
|
||||||
|
snipped = tail_start - head_end
|
||||||
|
return (messages[:head_end]
|
||||||
+ [{"role": "user", "content": f"[snipped {snipped} messages]"}]
|
+ [{"role": "user", "content": f"[snipped {snipped} messages]"}]
|
||||||
+ messages[-keep_tail:])
|
+ messages[tail_start:])
|
||||||
|
|
||||||
|
|
||||||
def micro_compact(messages: list) -> list:
|
def micro_compact(messages: list) -> list:
|
||||||
@@ -1163,8 +1190,11 @@ def reactive_compact(messages: list) -> list:
|
|||||||
summary = summarize_history(messages)
|
summary = summarize_history(messages)
|
||||||
except Exception:
|
except Exception:
|
||||||
summary = "Earlier conversation was trimmed after a prompt-too-long error."
|
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 has_tool_use(messages[tail_start - 1]):
|
||||||
|
tail_start -= 1
|
||||||
return [{"role": "user", "content": f"[Reactive compact]\n\n{summary}"},
|
return [{"role": "user", "content": f"[Reactive compact]\n\n{summary}"},
|
||||||
*messages[-5:]]
|
*messages[tail_start:]]
|
||||||
|
|
||||||
|
|
||||||
# ── Error Recovery ──
|
# ── Error Recovery ──
|
||||||
|
|||||||
File diff suppressed because one or more lines are too long
Reference in New Issue
Block a user