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: 14 additions & 12 deletions s08_context_compact/README.ja.md
Original file line number Diff line number Diff line change
Expand Up @@ -117,7 +117,7 @@ messages = [*messages[:head_end], marker, *messages[tail_start:]]

## ステップ 3:micro_compact

`micro_compact` は直近の assistant 応答より後に追加されたすべての `tool_result` を完全に保持し、モデルが各結果を少なくとも 1 回は完全な形で読めるようにします。モデルがすでに読んだ結果については最新 3 件を残し、それより古く 120 文字を超える結果を短くします。保存済みの結果にはファイルパスを残し、それ以外はプレースホルダーに置き換えます。
最初の 2 ステップの後、`prepare` は残りのコンテキストサイズを推定し、`CONTEXT_CHAR_LIMIT` を超えている場合にだけ `micro_compact` を実行します。`micro_compact` は直近の assistant 応答より後に追加されたすべての `tool_result` を完全に保持し、モデルが各結果を少なくとも 1 回は完全な形で読めるようにします。モデルがすでに読んだ結果については最新 3 件を残し、それより古く 120 文字を超える結果を短くします。保存済みの結果にはファイルパスを残し、それ以外はプレースホルダーに置き換えます。

![古い結果を置き換える](images/micro-compact.ja.svg)

Expand All @@ -142,12 +142,12 @@ for _, _, block in consumed[:-self.KEEP_RECENT_RESULTS]:

保存していない古い結果にはプレースホルダーだけが残ります。ステップ 1 で保存した結果には、完全な出力を読み直すためのパスが残ります。

最初の 3 ステップは、決定的なテキスト処理と構造操作です。追加の API 呼び出しは発生しません。
最初の 2 ステップは毎ラウンド実行され、ステップ 3 はコンテキストが上限を超えた場合にだけ実行されます。3 ステップとも決定的なテキスト処理と構造操作であり、追加の API 呼び出しは発生しません。


## ステップ 4:compact_history

最初の 3 ステップの後、コードは `estimate_chars(messages)` で現在のメッセージに含まれる文字数を数えます
`micro_compact` の後、コードは `estimate_chars(messages)` でコンテキストを再び推定します

```python
CONTEXT_CHAR_LIMIT = 50000
Expand All @@ -156,7 +156,7 @@ def estimate_chars(messages):
return len(json.dumps(messages, default=str, ensure_ascii=False))
```

文字数が `CONTEXT_CHAR_LIMIT` を超えると、`compact_history` は 4 つの処理を行います。
文字数がまだ `CONTEXT_CHAR_LIMIT` を超えている場合、`compact_history` は 4 つの処理を行います。

1. 完全なメッセージ履歴を `.transcripts/` に書き込みます。
2. モデルに事実だけの状態要約を依頼します。
Expand All @@ -181,18 +181,20 @@ def compact_history(messages, active_request):

## 順序を固定する理由

パイプラインは常に次の順序で実行されます
パイプラインは次の順序で処理し、必要な場合にだけ情報を失う圧縮へ進みます

```text
tool_result_budget
→ snip_compact
→ micro_compact
→ compact_history(上限を超えた場合)
```python
messages = self.tool_result_budget(messages)
messages = self.snip_compact(messages)
if self.estimate_chars(messages) > self.CONTEXT_CHAR_LIMIT:
messages = self.micro_compact(messages)
if self.estimate_chars(messages) > self.CONTEXT_CHAR_LIMIT:
messages = self.compact_history(messages, active_request)
```

この順序には 2 つの条件があります。

1. 最初の 3 ステップはモデルを呼び出しません。ステップ 4 だけが API リクエストを追加します
1. ステップ 1 と 2 は毎ラウンド実行され、ステップ 3 は上限を超えた場合だけ実行されます。API リクエストを追加するのはステップ 4 だけです
2. `tool_result_budget` は `micro_compact` より先に動く必要があります。古い結果をプレースホルダーにする前に、大きな結果をディスクへ保存します。

各ラウンドは、コストが低く情報を再取得しやすい処理から始まります。
Expand Down Expand Up @@ -243,7 +245,7 @@ def agent_loop(messages, active_request):
raise
```

すべてのモデル呼び出しが同じパイプラインを通ります。CLI は `query` を追加した後に `agent_loop(history, query)` を呼ぶため、圧縮を繰り返しても現在の要求は失われません。最初の 3 ステップ後も上限を超える場合、または API が拒否した場合にだけ、コードはモデルへ要約を依頼します。
すべてのモデル呼び出しが同じパイプラインを通ります。CLI は `query` を追加した後に `agent_loop(history, query)` を呼ぶため、圧縮を繰り返しても現在の要求は失われません。`micro_compact` の後も上限を超える場合、または API が拒否した場合にだけ、コードはモデルへ要約を依頼します。


## compact ツール
Expand Down
26 changes: 14 additions & 12 deletions s08_context_compact/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -117,7 +117,7 @@ This step controls the number of messages. Tool results inside the retained mess

## Step 3: micro_compact

`micro_compact` preserves every `tool_result` added after the most recent assistant response, so the model sees each new result in full once. Among results the model has already consumed, it keeps the latest 3 and shortens older results longer than 120 characters. Persisted results keep their file path; the rest become placeholders:
After the first two steps, `prepare` estimates the remaining context size and runs `micro_compact` only when it is above `CONTEXT_CHAR_LIMIT`. `micro_compact` preserves every `tool_result` added after the most recent assistant response, so the model sees each new result in full once. Among results the model has already consumed, it keeps the latest 3 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)

Expand All @@ -142,12 +142,12 @@ for _, _, block in consumed[:-self.KEEP_RECENT_RESULTS]:

An old result that was not persisted keeps only a placeholder. Results saved in Step 1 retain the path to their complete output.

The first three steps are deterministic text and structure operations. They do not add API calls.
The first two steps run every round. Step 3 runs only when the context is above the limit. All three are deterministic text and structure operations; they do not add API calls.


## Step 4: compact_history

After the first three steps, the code counts the characters in the current messages with `estimate_chars(messages)`:
After `micro_compact`, the code estimates the context again with `estimate_chars(messages)`:

```python
CONTEXT_CHAR_LIMIT = 50000
Expand All @@ -156,7 +156,7 @@ def estimate_chars(messages):
return len(json.dumps(messages, default=str, ensure_ascii=False))
```

When the count exceeds `CONTEXT_CHAR_LIMIT`, `compact_history` does four things:
When the count still exceeds `CONTEXT_CHAR_LIMIT`, `compact_history` does four things:

1. Writes the complete message history to `.transcripts/`.
2. Asks the model for a factual state summary.
Expand All @@ -181,18 +181,20 @@ This lesson uses character count as its trigger, and all related thresholds use

## Why the Order Is Fixed

The pipeline always runs in this order:
The pipeline uses this order and only enters the lossy steps when necessary:

```text
tool_result_budget
→ snip_compact
→ micro_compact
→ compact_history (only above the limit)
```python
messages = self.tool_result_budget(messages)
messages = self.snip_compact(messages)
if self.estimate_chars(messages) > self.CONTEXT_CHAR_LIMIT:
messages = self.micro_compact(messages)
if self.estimate_chars(messages) > self.CONTEXT_CHAR_LIMIT:
messages = self.compact_history(messages, active_request)
```

This order satisfies two constraints:

1. The first three steps do not call the model. Only Step 4 adds an API request.
1. Steps 1 and 2 run every round. Step 3 runs only above the limit, and only Step 4 adds an API request.
2. `tool_result_budget` must run before `micro_compact`. Large results need to reach disk before older results can become placeholders.

Each round therefore starts with the lowest-cost operation whose information is easiest to recover.
Expand Down Expand Up @@ -243,7 +245,7 @@ def agent_loop(messages, active_request):
raise
```

Every model call enters through the same pipeline. After appending `query`, the CLI calls `agent_loop(history, query)`, so repeated compaction cannot lose the current request. The code asks for a summary only when the first three steps leave the context above the limit or when the API rejects it.
Every model call enters through the same pipeline. After appending `query`, the CLI calls `agent_loop(history, query)`, so repeated compaction cannot lose the current request. The code asks for a summary only when `micro_compact` still leaves the context above the limit or when the API rejects it.


## The compact Tool
Expand Down
26 changes: 14 additions & 12 deletions s08_context_compact/README.zh.md
Original file line number Diff line number Diff line change
Expand Up @@ -117,7 +117,7 @@ messages = [*messages[:head_end], marker, *messages[tail_start:]]

## 第三步:micro_compact

`micro_compact` 会完整保留最近一次 assistant 响应之后新增的所有 `tool_result`,确保模型至少完整读取每条新结果一次。对于模型已经读取过的结果,它保留最近 3 条,并缩短其余超过 120 个字符的旧结果。已经转存的结果保留文件路径,其他结果只留下占位符:
前两步完成后,`prepare` 会估算剩余上下文的大小,只有超过 `CONTEXT_CHAR_LIMIT` 时才执行 `micro_compact`。`micro_compact` 会完整保留最近一次 assistant 响应之后新增的所有 `tool_result`,确保模型至少完整读取每条新结果一次。对于模型已经读取过的结果,它保留最近 3 条,并缩短其余超过 120 个字符的旧结果。已经转存的结果保留文件路径,其他结果只留下占位符:

![旧结果替换为占位符](images/micro-compact.svg)

Expand All @@ -142,12 +142,12 @@ for _, _, block in consumed[:-self.KEEP_RECENT_RESULTS]:

未转存的旧结果只保留占位符。第一步保存过的完整结果仍能通过路径读取,不会在第三步丢失位置。

前三步都是确定性的结构和文本操作,不产生额外 API 调用。
前两步每轮都会执行,第三步只在上下文超限时执行。三步都是确定性的结构和文本操作,不产生额外 API 调用。


## 第四步:compact_history

前三步执行后,代码用 `estimate_chars(messages)` 计算当前消息的字符数
`micro_compact` 执行后,代码会再次用 `estimate_chars(messages)` 估算上下文

```python
CONTEXT_CHAR_LIMIT = 50000
Expand All @@ -156,7 +156,7 @@ def estimate_chars(messages):
return len(json.dumps(messages, default=str, ensure_ascii=False))
```

字符数超过 `CONTEXT_CHAR_LIMIT` 时,`compact_history` 完成四件事:
字符数仍然超过 `CONTEXT_CHAR_LIMIT` 时,`compact_history` 完成四件事:

1. 将完整消息历史写入 `.transcripts/`。
2. 请求模型生成只包含事实的状态摘要。
Expand All @@ -181,18 +181,20 @@ def compact_history(messages, active_request):

## 为什么顺序固定

四步管线的执行顺序是
管线按以下顺序执行,并且只在必要时进入有损压缩步骤

```text
tool_result_budget
→ snip_compact
→ micro_compact
→ compact_history(超过阈值时)
```python
messages = self.tool_result_budget(messages)
messages = self.snip_compact(messages)
if self.estimate_chars(messages) > self.CONTEXT_CHAR_LIMIT:
messages = self.micro_compact(messages)
if self.estimate_chars(messages) > self.CONTEXT_CHAR_LIMIT:
messages = self.compact_history(messages, active_request)
```

这个顺序同时满足两个条件:

1. 前三步不调用模型,第四步才产生额外 API 请求。
1. 第一步和第二步每轮执行,第三步只在超限时执行,只有第四步会增加 API 请求。
2. `tool_result_budget` 必须早于 `micro_compact`。大结果先落盘,之后才允许旧结果变成占位符。

顺序固定后,每一轮都从成本更低、信息更容易恢复的操作开始。
Expand Down Expand Up @@ -243,7 +245,7 @@ def agent_loop(messages, active_request):
raise
```

每次调用模型前都会经过同一条管线。CLI 在追加 `query` 后调用 `agent_loop(history, query)`,所以压缩多少次都不会丢失本轮请求。前三步处理后仍超过阈值,或者 API 明确拒绝上下文时,代码才会请求模型生成摘要。
每次调用模型前都会经过同一条管线。CLI 在追加 `query` 后调用 `agent_loop(history, query)`,所以压缩多少次都不会丢失本轮请求。只有 `micro_compact` 处理后仍超过阈值,或者 API 明确拒绝上下文时,代码才会请求模型生成摘要。


## compact 工具
Expand Down
26 changes: 15 additions & 11 deletions s08_context_compact/code.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,18 +11,21 @@
v
+--------------------+
| snip_compact | archive the old middle -> .transcripts/
+--------------------+
|
v
+--------------------+
| micro_compact | shorten old tool results
+--------------------+
|
v
context over limit?
| no | yes
v v
model call compact_history -> model call
| v
| +--------------------+
| | micro_compact | shorten old tool results
| +--------------------+
| |
| v
| still over limit?
| | no | yes
v v v
model call compact_history -> model call

Other entry points:

Expand Down Expand Up @@ -116,7 +119,7 @@ def run_edit(path: str, old_text: str, new_text: str) -> str:
def run_glob(pattern: str) -> str:
try:
matches = [
match for match in glob.glob(pattern, root_dir=WORKDIR)
match for match in glob.glob(pattern, root_dir=WORKDIR, recursive=True)
if (WORKDIR / match).resolve().is_relative_to(WORKDIR)
]
return "\n".join(matches) if matches else "(no matches)"
Expand Down Expand Up @@ -419,10 +422,11 @@ def reactive_compact(self, messages: list, active_request: str) -> list:
def prepare(self, messages: list, active_request: str) -> list:
messages = self.tool_result_budget(messages)
messages = self.snip_compact(messages)
messages = self.micro_compact(messages)
if self.estimate_chars(messages) > self.CONTEXT_CHAR_LIMIT:
print("[auto compact]")
messages = self.compact_history(messages, active_request)
messages = self.micro_compact(messages)
if self.estimate_chars(messages) > self.CONTEXT_CHAR_LIMIT:
print("[auto compact]")
messages = self.compact_history(messages, active_request)
return messages


Expand Down
2 changes: 1 addition & 1 deletion s08_context_compact/images/auto-compact.en.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
2 changes: 1 addition & 1 deletion s08_context_compact/images/auto-compact.ja.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
2 changes: 1 addition & 1 deletion s08_context_compact/images/auto-compact.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading