Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
26 changes: 16 additions & 10 deletions s08_context_compact/README.en.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.

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
def snip_compact(messages, max_messages=50):
if len(messages) <= max_messages:
return messages
keep_head, keep_tail = 3, max_messages - 3
snipped = len(messages) - keep_head - keep_tail
placeholder = {"role": "user",
"content": f"[snipped {snipped} messages from conversation middle]"}
return messages[:keep_head] + [placeholder] + messages[-keep_tail:]
head_end, tail_start = 3, len(messages) - (max_messages - 3)
if has_tool_use(messages[head_end - 1]):
while head_end < len(messages) and is_tool_result_message(messages[head_end]):
head_end += 1
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

Expand Down Expand Up @@ -130,15 +134,17 @@ def compact_history(messages):

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
def reactive_compact(messages):
transcript = write_transcript(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",
"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.
Expand Down
26 changes: 16 additions & 10 deletions s08_context_compact/README.ja.md
Original file line number Diff line number Diff line change
Expand Up @@ -39,20 +39,24 @@ s07 のフック構造、スキルロード、サブ Agent の骨格を維持し

Agent が 80 ラウンドの会話を実行し、`messages` が 160 件まで溜まった。先頭の「hello.py を作って」は現在の作業とほぼ無関係だが、スペースを占有し続けている。

メッセージ数が 50 を超えた場合 → 先頭 3 件(初期コンテキスト)と末尾 47 件(現在の作業)を保持し、中間を切り捨て
メッセージ数が 50 を超えた場合 → 先頭 3 件(初期コンテキスト)と末尾 47 件(現在の作業)を保持して中間を切り詰める。ただし切れ目だけは調整し、`assistant(tool_use)` と後続の `user(tool_result)` を分断しない

```python
def snip_compact(messages, max_messages=50):
if len(messages) <= max_messages:
return messages
keep_head, keep_tail = 3, max_messages - 3
snipped = len(messages) - keep_head - keep_tail
placeholder = {"role": "user",
"content": f"[snipped {snipped} messages from conversation middle]"}
return messages[:keep_head] + [placeholder] + messages[-keep_tail:]
head_end, tail_start = 3, len(messages) - (max_messages - 3)
if has_tool_use(messages[head_end - 1]):
while head_end < len(messages) and is_tool_result_message(messages[head_end]):
head_end += 1
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 — 古いツール結果をプレースホルダに置換

Expand Down Expand Up @@ -130,15 +134,17 @@ def compact_history(messages):

API がまだ `prompt_too_long`(413)を返すことがある。コンテキストの増加速度が圧縮のトリガー速度を上回る場合。

この時 **reactive_compact** がトリガーされる:compact_history よりもさらに積極的で、末尾からバイト単位の精度で API が受け入れ可能なサイズまで切り詰め、最後の 5 件のメッセージ + 要約のみを保持
この時 **reactive_compact** がトリガーされる:compact_history よりもさらに積極的だが、末尾を残す際も孤立した `tool_result` を残さないようにする

```python
def reactive_compact(messages):
transcript = write_transcript(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",
"content": f"[Reactive compact]\n\n{summary}"}, *tail]
"content": f"[Reactive compact]\n\n{summary}"}, *messages[tail_start:]]
```

reactive compact にはリトライ上限がある(デフォルト 1 回)。さらに失敗した場合は例外をスローし、無限ループしない。完全なエラー回復ロジックは s11 に委ねる。
Expand Down
26 changes: 16 additions & 10 deletions s08_context_compact/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -39,20 +39,24 @@ Agent 跑着跑着,不动了。

Agent 跑了 80 轮对话,`messages` 攒了 160 条。最前面的"帮我创建 hello.py"和当前工作几乎无关了,但全占着位置。

消息数超过 50 条 → 保留头部 3 条(初始上下文)和尾部 47 条(当前工作),中间裁掉:
消息数超过 50 条 → 保留头部 3 条(初始上下文)和尾部 47 条(当前工作),中间裁掉;唯一额外边界条件是,不能把 `assistant(tool_use)` 和后面的 `user(tool_result)` 拆开

```python
def snip_compact(messages, max_messages=50):
if len(messages) <= max_messages:
return messages
keep_head, keep_tail = 3, max_messages - 3
snipped = len(messages) - keep_head - keep_tail
placeholder = {"role": "user",
"content": f"[snipped {snipped} messages from conversation middle]"}
return messages[:keep_head] + [placeholder] + messages[-keep_tail:]
head_end, tail_start = 3, len(messages) - (max_messages - 3)
if has_tool_use(messages[head_end - 1]):
while head_end < len(messages) and is_tool_result_message(messages[head_end]):
head_end += 1
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 — 旧工具结果占位

Expand Down Expand Up @@ -130,15 +134,17 @@ def compact_history(messages):

有时候 API 还是返回 `prompt_too_long`(413),上下文增长速度快于压缩触发速度时。

这时触发 **reactive_compact**:比 compact_history 更激进,从尾部回退,以字节级精度裁剪到 API 可接受的大小,只保留最后 5 条消息 + 摘要
这时触发 **reactive_compact**:比 compact_history 更激进,从尾部回退,但仍要避免留下孤立 `tool_result`

```python
def reactive_compact(messages):
transcript = write_transcript(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",
"content": f"[Reactive compact]\n\n{summary}"}, *tail]
"content": f"[Reactive compact]\n\n{summary}"}, *messages[tail_start:]]
```

reactive compact 有重试上限(默认 1 次)。再失败就抛出异常,不无限循环。完整的错误恢复逻辑留给 s11。
Expand Down
36 changes: 33 additions & 3 deletions s08_context_compact/code.py
Original file line number Diff line number Diff line change
Expand Up @@ -250,13 +250,40 @@ def spawn_subagent(task: str) -> str:

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
def snip_compact(messages, max_messages=50):
if len(messages) <= max_messages: return messages
keep_head, keep_tail = 3, max_messages - 3
snipped = len(messages) - keep_head - keep_tail
return messages[:keep_head] + [{"role": "user", "content": f"[snipped {snipped} messages]"}] + messages[-keep_tail:]
head_end, tail_start = keep_head, len(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
Expand Down Expand Up @@ -333,7 +360,10 @@ def compact_history(messages):
def reactive_compact(messages):
transcript = write_transcript(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:]]


# ═══════════════════════════════════════════════════════════
Expand Down
34 changes: 32 additions & 2 deletions s09_memory/code.py
Original file line number Diff line number Diff line change
Expand Up @@ -451,9 +451,36 @@ def spawn_subagent(task: str) -> str:

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):
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):
blocks = []
Expand Down Expand Up @@ -514,7 +541,10 @@ def compact_history(msgs):
def reactive_compact(msgs):
write_transcript(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:]]


# ═══════════════════════════════════════════════════════════
Expand Down
40 changes: 35 additions & 5 deletions s20_comprehensive/code.py
Original file line number Diff line number Diff line change
Expand Up @@ -1042,6 +1042,26 @@ def spawn_subagent(description: str) -> str:
def estimate_size(messages: list) -> int:
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):
found = []
Expand Down Expand Up @@ -1093,11 +1113,18 @@ def tool_result_budget(messages: list, max_bytes: int = 200_000) -> list:
def snip_compact(messages: list, max_messages: int = 50) -> list:
if len(messages) <= max_messages:
return messages
keep_head, keep_tail = 3, max_messages - 3
snipped = len(messages) - keep_head - keep_tail
return (messages[:keep_head]
head_end, tail_start = 3, len(messages) - (max_messages - 3)
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[-keep_tail:])
+ messages[tail_start:])


def micro_compact(messages: list) -> list:
Expand Down Expand Up @@ -1145,8 +1172,11 @@ def reactive_compact(messages: list) -> list:
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 has_tool_use(messages[tail_start - 1]):
tail_start -= 1
return [{"role": "user", "content": f"[Reactive compact]\n\n{summary}"},
*messages[-5:]]
*messages[tail_start:]]


# ── Error Recovery ──
Expand Down
Loading