Merge remote-tracking branch 'shareai/main' into fix/pr-536-readline-prompt-wrap

# Conflicts:
#	web/src/data/generated/versions.json
This commit is contained in:
Haoran
2026-08-25 21:38:38 +08:00
95 changed files with 1673 additions and 922 deletions

View File

@@ -88,11 +88,11 @@ for block in ranked:
## ステップ 2snip_compact
履歴が 50 メッセージを超えると、`snip_compact` は完全な履歴を `.transcripts/` に保存してから、先頭 3 件と最新 47 件を保持します。中間のマーカーには、削除した件数と transcript の保存先を記録します。
履歴が 50 メッセージを超えると、`snip_compact` は完全な履歴を `.transcripts/` に保存してから、先頭 3 件と最新 46 件を保持します。残り 1 件は archive marker に使い、削除した件数と完全な transcript の保存先を記録します。
```python
head_end = 3
tail_start = len(messages) - (max_messages - head_end)
tail_start = len(messages) - (max_messages - head_end - 1)
if self.has_tool_use(messages[head_end - 1]):
while (head_end < tail_start
@@ -117,37 +117,34 @@ messages = [*messages[:head_end], marker, *messages[tail_start:]]
## ステップ 3micro_compact
`micro_compact` は直近の assistant 応答より後に追加されたすべての `tool_result` を完全に保持し、モデルが各結果を少なくとも 1 回は完全な形で読めるようにします。モデルがすでに読んだ結果については最新 3 件を残し、それより古く 120 文字を超える結果を短くします。保存済みの結果にはファイルパスを残し、それ以外はプレースホルダーに置き換えます。
最初の 2 ステップの後、`prepare` は残りのコンテキストサイズを推定し、`CONTEXT_CHAR_LIMIT` を超えている場合にだけ `micro_compact` を実行します。モデルがすでに読んだ結果については最新 3 件を残し、それより古く 120 文字を超える結果を、コンテキストが上限の 80% に近づくまで順に短くします。古い結果は置換前に完全な内容をディスクへ保存するため、各プレースホルダーには復元用のパスが残ります。
![古い結果を置き換える](images/micro-compact.ja.svg)
![古い結果を復元可能なパスへ置き換える](images/micro-compact.ja.svg)
```python
unseen = self.unseen_tool_result_positions(messages)
consumed = [entry for entry in results if entry[:2] not in unseen]
for _, _, block in consumed[:-self.KEEP_RECENT_RESULTS]:
if self.estimate_chars(messages) <= target_chars:
break
content = str(block.get("content", ""))
if len(content) <= 120:
continue
saved_path = next(
(line.removeprefix("Full output: ") for line in content.splitlines()
if line.startswith("Full output: ")),
None,
)
block["content"] = (
f"[Earlier tool result saved at {saved_path}]"
if saved_path else "[Earlier tool result omitted.]"
)
saved_path = self.persisted_output_path(content)
if not saved_path:
saved_path = self.save_output(block["tool_use_id"], content)
block["content"] = f"[Earlier tool result saved at {saved_path}]"
```
保存していない古い結果にはプレースホルダーだけが残ります。ステップ 1 で保存した結果には、完全な出力を読み直すためのパスが残ります。
新しい結果は通常、モデルが一度読むまで完全な形で保持されます。未読の最新バッチだけでコンテキストを超える場合、`fit_tool_results` は大きな結果を保存し、1,000 文字の preview と完全な出力へのパスを残します。これにより、モデルが新しい結果を見る前に履歴全体を要約する事態を避けます。
最初の 3 ステップは、決定的なテキスト処理と構造操作です。追加の API 呼び出しは発生しません。
最初の 2 ステップは毎ラウンド実行され、ステップ 3 はコンテキストが上限を超えた場合にだけ実行されます。3 ステップとも決定的で復元可能なテキスト処理と構造操作であり、追加の API 呼び出しは発生しません。
## ステップ 4compact_history
最初の 3 ステップの後、コードは `estimate_chars(messages)`現在のメッセージに含まれる文字数を数えます。
`micro_compact``fit_tool_results` の後、コードは `estimate_chars(messages)`コンテキストを再び推定します。
```python
CONTEXT_CHAR_LIMIT = 50000
@@ -156,7 +153,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. モデルに事実だけの状態要約を依頼します。
@@ -181,19 +178,24 @@ 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:
target = int(self.CONTEXT_CHAR_LIMIT * 0.8)
messages = self.micro_compact(messages, target)
if self.estimate_chars(messages) > self.CONTEXT_CHAR_LIMIT:
messages = self.fit_tool_results(messages, target)
if self.estimate_chars(messages) > self.CONTEXT_CHAR_LIMIT:
messages = self.compact_history(messages, active_request)
```
この順序には 2 つの条件があります。
1. 最初の 3 ステップはモデルを呼び出しません。ステップ 4 だけが API リクエストを追加します。
2. `tool_result_budget``micro_compact` より先に動く必要があります。古い結果をプレースホルダーにする前に、大きな結果をディスクへ保存します。
1. ステップ 1 と 2 は毎ラウンド実行され、ステップ 3 は上限を超えた場合だけ実行されます。API リクエストを追加するのはステップ 4 だけです。
2. 短縮した各ツール結果には `.task_outputs/tool-results/` 内の信頼できるパスを残します。それでも上限を超える場合にだけ、モデルによる履歴要約へ進みます。
各ラウンドは、コストが低く情報を再取得しやすい処理から始まります。
@@ -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 ツール
@@ -308,7 +310,7 @@ s01_agent_loop から s05_todo_write までの README.md を読み、
各ファイルの最上位見出しを比較して、命名の規則をまとめてください。
```
このタスクでは少なくとも 5 件のファイル結果が生成されます。各新規結果はモデルが初めて読むまで完全に保持されます。以降のターンでは、すでに読まれた最新 3 件を残し、それより前の長い結果は `[Earlier tool result omitted.]` に変わります。保存済みの結果には保存先のパスが残ります。
このタスクでは少なくとも 5 件のファイル結果が生成されます。新しい結果は通常、モデルが初めて読むまで完全に保持されます。未読結果自体が大きすぎる場合は、preview と復元パスを残します。以降のターンでは、すでに読まれた最新 3 件を残し、それより前の長い結果は `[Earlier tool result saved at ...]` 参照に変わります。
### 実験 2大きな結果を保存する

View File

@@ -88,11 +88,11 @@ This step examines only the latest batch of tool results. The complete output re
## Step 2: snip_compact
Once the history exceeds 50 messages, `snip_compact` writes the complete history to `.transcripts/`, then keeps the first 3 and latest 47 messages. The marker records how many messages were removed and where to find the complete transcript.
Once the history exceeds 50 messages, `snip_compact` writes the complete history to `.transcripts/`, then keeps the first 3 and latest 46 messages. The archive marker occupies the remaining slot, records how many messages were removed, and points to the complete transcript.
```python
head_end = 3
tail_start = len(messages) - (max_messages - head_end)
tail_start = len(messages) - (max_messages - head_end - 1)
if self.has_tool_use(messages[head_end - 1]):
while (head_end < tail_start
@@ -117,37 +117,34 @@ 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`. Among results the model has already consumed, `micro_compact` keeps the latest 3 and shortens older results longer than 120 characters until the context approaches 80% of the limit. Before replacing an old result, it writes the complete content to disk, so every replacement retains a recovery path:
![Replacing old results](images/micro-compact.en.svg)
![Replacing old results with recovery paths](images/micro-compact.en.svg)
```python
unseen = self.unseen_tool_result_positions(messages)
consumed = [entry for entry in results if entry[:2] not in unseen]
for _, _, block in consumed[:-self.KEEP_RECENT_RESULTS]:
if self.estimate_chars(messages) <= target_chars:
break
content = str(block.get("content", ""))
if len(content) <= 120:
continue
saved_path = next(
(line.removeprefix("Full output: ") for line in content.splitlines()
if line.startswith("Full output: ")),
None,
)
block["content"] = (
f"[Earlier tool result saved at {saved_path}]"
if saved_path else "[Earlier tool result omitted.]"
)
saved_path = self.persisted_output_path(content)
if not saved_path:
saved_path = self.save_output(block["tool_use_id"], content)
block["content"] = f"[Earlier tool result saved at {saved_path}]"
```
An old result that was not persisted keeps only a placeholder. Results saved in Step 1 retain the path to their complete output.
New results normally stay complete until the model consumes them. If an unseen batch alone is too large for the context, `fit_tool_results` persists its largest results and keeps a 1,000-character preview plus the full-output path. This avoids summarizing the entire history before the model can inspect the new result.
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 and recoverable 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` and `fit_tool_results`, the code estimates the context again with `estimate_chars(messages)`:
```python
CONTEXT_CHAR_LIMIT = 50000
@@ -156,7 +153,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.
@@ -181,19 +178,24 @@ 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 summary step 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:
target = int(self.CONTEXT_CHAR_LIMIT * 0.8)
messages = self.micro_compact(messages, target)
if self.estimate_chars(messages) > self.CONTEXT_CHAR_LIMIT:
messages = self.fit_tool_results(messages, target)
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.
2. `tool_result_budget` must run before `micro_compact`. Large results need to reach disk before older results can become placeholders.
1. Steps 1 and 2 run every round. Step 3 runs only above the limit, and only Step 4 adds an API request.
2. Every shortened tool result keeps a trusted path inside `.task_outputs/tool-results/`; only a remaining overflow reaches model-generated history summarization.
Each round therefore starts with the lowest-cost operation whose information is easiest to recover.
@@ -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
@@ -308,7 +310,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. Every result remains complete until the model sees it once. On later turns, the latest 3 consumed results remain complete while older long results become `[Earlier tool result omitted.]`. A persisted result retains its saved path.
This task produces at least 5 file results. New results normally remain complete until the model sees them once; an oversized unseen result keeps a preview and recovery path instead. On later turns, the latest 3 consumed results remain complete while older long results become `[Earlier tool result saved at ...]` references.
### Experiment 2: Persist a Large Result

View File

@@ -88,11 +88,11 @@ for block in ranked:
## 第二步snip_compact
消息数量超过 50 条后,`snip_compact` 先把完整历史写入 `.transcripts/`,再保留最初 3 条和最近 47 条。中间的标记会写明删去了多少条消息,以及完整记录保存在哪里。
消息数量超过 50 条后,`snip_compact` 先把完整历史写入 `.transcripts/`,再保留最初 3 条和最近 46 条。剩余一个位置用于归档标记,其中写明删去了多少条消息,以及完整记录保存在哪里。
```python
head_end = 3
tail_start = len(messages) - (max_messages - head_end)
tail_start = len(messages) - (max_messages - head_end - 1)
if self.has_tool_use(messages[head_end - 1]):
while (head_end < tail_start
@@ -117,37 +117,34 @@ messages = [*messages[:head_end], marker, *messages[tail_start:]]
## 第三步micro_compact
`micro_compact` 会完整保留最近一次 assistant 响应之后新增的所有 `tool_result`,确保模型至少完整读取每条新结果一次。对于模型已经读取过的结果,它保留最近 3 条,并缩短其余超过 120 个字符的结果。已经转存的结果保留文件路径,其他结果只留下占位符
前两步完成后,`prepare` 会估算剩余上下文的大小,只有超过 `CONTEXT_CHAR_LIMIT` 时才执行 `micro_compact`。对于模型已经读取过的结果,它保留最近 3 条,并逐条缩短更早且超过 120 个字符的结果,直到上下文接近阈值的 80%。旧结果被替换前会先完整落盘,因此每个占位都带有可恢复路径
![旧结果替换为占位符](images/micro-compact.svg)
![旧结果替换为可恢复路径](images/micro-compact.svg)
```python
unseen = self.unseen_tool_result_positions(messages)
consumed = [entry for entry in results if entry[:2] not in unseen]
for _, _, block in consumed[:-self.KEEP_RECENT_RESULTS]:
if self.estimate_chars(messages) <= target_chars:
break
content = str(block.get("content", ""))
if len(content) <= 120:
continue
saved_path = next(
(line.removeprefix("Full output: ") for line in content.splitlines()
if line.startswith("Full output: ")),
None,
)
block["content"] = (
f"[Earlier tool result saved at {saved_path}]"
if saved_path else "[Earlier tool result omitted.]"
)
saved_path = self.persisted_output_path(content)
if not saved_path:
saved_path = self.save_output(block["tool_use_id"], content)
block["content"] = f"[Earlier tool result saved at {saved_path}]"
```
未转存的旧结果只保留占位符。第一步保存过的完整结果仍能通过路径读取,不会在第三步丢失位置
新结果通常会保持完整,直到模型读取一次。如果仅未读取的最新一批结果就足以撑爆上下文,`fit_tool_results` 会把其中最大的结果落盘,并保留 1,000 字符预览和完整路径,避免模型看到新结果前就先总结整段历史
前三步都是确定性的结构和文本操作,不产生额外 API 调用。
两步每轮都会执行,第三步只在上下文超限时执行。三步都是确定性、可恢复的结构和文本操作,不产生额外 API 调用。
## 第四步compact_history
前三步执行后,代码用 `estimate_chars(messages)` 计算当前消息的字符数
`micro_compact``fit_tool_results` 执行后,代码会再次`estimate_chars(messages)` 估算上下文
```python
CONTEXT_CHAR_LIMIT = 50000
@@ -156,7 +153,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. 请求模型生成只包含事实的状态摘要。
@@ -181,19 +178,24 @@ 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:
target = int(self.CONTEXT_CHAR_LIMIT * 0.8)
messages = self.micro_compact(messages, target)
if self.estimate_chars(messages) > self.CONTEXT_CHAR_LIMIT:
messages = self.fit_tool_results(messages, target)
if self.estimate_chars(messages) > self.CONTEXT_CHAR_LIMIT:
messages = self.compact_history(messages, active_request)
```
这个顺序同时满足两个条件:
1. 前三步不调用模型,第四步才产生额外 API 请求。
2. `tool_result_budget` 必须早于 `micro_compact`。大结果先落盘,之后才允许旧结果变成占位符
1. 第一步和第二步每轮执行,第三步只在超限时执行,只有第四步会增加 API 请求。
2. 每条被缩短的工具结果都保留 `.task_outputs/tool-results/` 内的可信路径;只有仍然超限时才进入模型生成的历史摘要
顺序固定后,每一轮都从成本更低、信息更容易恢复的操作开始。
@@ -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 工具
@@ -308,7 +310,7 @@ python s08_context_compact/code.py
比较它们的一级标题,并总结这些标题的命名规律。
```
任务会产生至少 5 条文件读取结果。每条新结果在模型首次读取前都会保持完整;后续轮次保留最近 3 条已读取结果,更早且较长的结果会变成 `[Earlier tool result omitted.]`。已经转存的结果会保留保存路径
任务会产生至少 5 条文件读取结果。新结果通常会完整保留到模型首次读取;如果未读取结果本身过大,则保留预览和恢复路径。后续轮次保留最近 3 条已读取结果,更早且较长的结果会变成 `[Earlier tool result saved at ...]` 引用
### 实验二:大结果转存

View File

@@ -11,18 +11,24 @@ s08_context_compact.py - Context Compact
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 | save + shorten old results
| +--------------------+
| |
| v
| fit_tool_results persist oversized new results
| |
| v
| still over limit?
| | no | yes
v v v
model call compact_history -> model call
Other entry points:
@@ -83,7 +89,7 @@ def run_bash(command: str) -> str:
def run_read(path: str, limit: int | None = None) -> str:
try:
lines = (WORKDIR / path).resolve().read_text().splitlines()
lines = (WORKDIR / path).resolve().read_text(encoding="utf-8").splitlines()
if limit and limit < len(lines):
lines = lines[:limit] + [f"... ({len(lines) - limit} more lines)"]
return "\n".join(lines)
@@ -95,7 +101,7 @@ def run_write(path: str, content: str) -> str:
try:
file_path = (WORKDIR / path).resolve()
file_path.parent.mkdir(parents=True, exist_ok=True)
file_path.write_text(content)
file_path.write_text(content, encoding="utf-8")
return f"Wrote {len(content)} bytes to {path}"
except Exception as error:
return f"Error: {error}"
@@ -104,10 +110,10 @@ def run_write(path: str, content: str) -> str:
def run_edit(path: str, old_text: str, new_text: str) -> str:
try:
file_path = (WORKDIR / path).resolve()
text = file_path.read_text()
text = file_path.read_text(encoding="utf-8")
if old_text not in text:
return f"Error: text not found in {path}"
file_path.write_text(text.replace(old_text, new_text, 1))
file_path.write_text(text.replace(old_text, new_text, 1), encoding="utf-8")
return f"Edited {path}"
except Exception as error:
return f"Error: {error}"
@@ -115,11 +121,14 @@ 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)
matches = sorted({
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)"
})
shown = matches[:200]
if len(matches) > 200:
shown.append("... (more matches omitted; narrow the pattern)")
return "\n".join(shown) if shown else "(no matches)"
except Exception as error:
return f"Error: {error}"
@@ -133,7 +142,7 @@ BASE_TOOLS = [
"input_schema": {"type": "object", "properties": {"path": {"type": "string"}, "content": {"type": "string"}}, "required": ["path", "content"]}},
{"name": "edit_file", "description": "Replace exact text in a file once.",
"input_schema": {"type": "object", "properties": {"path": {"type": "string"}, "old_text": {"type": "string"}, "new_text": {"type": "string"}}, "required": ["path", "old_text", "new_text"]}},
{"name": "glob", "description": "Find files matching a glob pattern.",
{"name": "glob", "description": "Find files matching a glob pattern; ** matches recursively.",
"input_schema": {"type": "object", "properties": {"pattern": {"type": "string"}}, "required": ["pattern"]}},
]
COMPACT_TOOL = {
@@ -287,20 +296,58 @@ class ContextCompactor:
def write_transcript(self, messages: list) -> Path:
self.transcript_dir.mkdir(parents=True, exist_ok=True)
path = self.transcript_dir / f"transcript_{uuid.uuid4().hex}.jsonl"
with path.open("x") as transcript:
with path.open("x", encoding="utf-8") as transcript:
for message in messages:
transcript.write(json.dumps(message, default=str, ensure_ascii=False) + "\n")
return path
def persist_large_output(self, tool_use_id: str, output: str) -> str:
if len(output) <= self.LARGE_RESULT_CHAR_LIMIT:
return output
def persisted_output_path(self, output: str) -> str | None:
candidate = None
if output.startswith("<persisted-output>\n"):
candidate = next(
(line.removeprefix("Full output: ")
for line in output.splitlines()
if line.startswith("Full output: ")),
None,
)
prefix = "[Earlier tool result saved at "
if output.startswith(prefix) and output.endswith("]"):
candidate = output.removeprefix(prefix).removesuffix("]")
if not candidate:
return None
path = Path(candidate)
if (not path.resolve().is_relative_to(self.tool_results_dir.resolve())
or not path.is_file()):
return None
return str(path)
def save_output(self, tool_use_id: str, output: str) -> Path:
self.tool_results_dir.mkdir(parents=True, exist_ok=True)
safe_id = re.sub(r"[^A-Za-z0-9._-]", "_", str(tool_use_id))[:120] or "unknown"
path = self.tool_results_dir / f"{safe_id}.txt"
if not path.exists():
path.write_text(output)
return f"<persisted-output>\nFull output: {path}\nPreview:\n{output[:2000]}\n</persisted-output>"
path.write_text(output, encoding="utf-8")
return path
def persisted_preview(self, tool_use_id: str, output: str,
preview_chars: int = 2000) -> str:
saved_path = self.persisted_output_path(output)
if saved_path:
path = Path(saved_path)
try:
with path.open(encoding="utf-8") as saved:
preview = saved.read(preview_chars)
except OSError:
preview = output[:preview_chars]
else:
path = self.save_output(tool_use_id, output)
preview = output[:preview_chars]
return (f"<persisted-output>\nFull output: {path}\n"
f"Preview:\n{preview}\n</persisted-output>")
def persist_large_output(self, tool_use_id: str, output: str) -> str:
if len(output) <= self.LARGE_RESULT_CHAR_LIMIT:
return output
return self.persisted_preview(tool_use_id, output)
def tool_result_budget(self, messages: list, max_chars: int | None = None) -> list:
if not messages:
@@ -322,11 +369,21 @@ class ContextCompactor:
total = sum(len(str(item.get("content", ""))) for item in blocks)
return messages
def is_archive_marker(self, message: dict) -> bool:
content = message.get("content")
match = (re.fullmatch(r"\[\d+ messages archived at (.+)\]", content)
if isinstance(content, str) else None)
if not match:
return False
path = Path(match.group(1))
return (path.resolve().is_relative_to(self.transcript_dir.resolve())
and path.is_file())
def snip_compact(self, messages: list, max_messages: int = 50) -> list:
if len(messages) <= max_messages:
return messages
head_end = 3
tail_start = len(messages) - (max_messages - head_end)
tail_start = len(messages) - (max_messages - head_end - 1)
if self.has_tool_use(messages[head_end - 1]):
while head_end < tail_start and self.is_tool_result(messages[head_end]):
head_end += 1
@@ -335,12 +392,16 @@ class ContextCompactor:
tail_start -= 1
if head_end >= tail_start:
return messages
middle = messages[head_end:tail_start]
if len(middle) == 1 and self.is_archive_marker(middle[0]):
return messages
transcript_path = self.write_transcript(messages)
marker = {"role": "user", "content":
f"[{tail_start - head_end} messages archived at {transcript_path}]"}
return [*messages[:head_end], marker, *messages[tail_start:]]
def micro_compact(self, messages: list) -> list:
def micro_compact(self, messages: list,
target_chars: int | None = None) -> list:
results = [
(message_index, block_index, block)
for message_index, message in enumerate(messages)
@@ -351,18 +412,38 @@ class ContextCompactor:
unseen = self.unseen_tool_result_positions(messages)
consumed = [entry for entry in results if entry[:2] not in unseen]
for _, _, block in consumed[:-self.KEEP_RECENT_RESULTS]:
if (target_chars is not None
and self.estimate_chars(messages) <= target_chars):
break
content = str(block.get("content", ""))
if len(content) <= 120:
continue
saved_path = next(
(line.removeprefix("Full output: ") for line in content.splitlines()
if line.startswith("Full output: ")),
None,
)
block["content"] = (
f"[Earlier tool result saved at {saved_path}]"
if saved_path else "[Earlier tool result omitted.]"
)
saved_path = self.persisted_output_path(content)
if not saved_path:
saved_path = str(self.save_output(
block.get("tool_use_id", "unknown"), content))
block["content"] = f"[Earlier tool result saved at {saved_path}]"
return messages
def fit_tool_results(self, messages: list, target_chars: int) -> list:
results = [
block
for message in messages
if message.get("role") == "user" and isinstance(message.get("content"), list)
for block in message["content"]
if isinstance(block, dict) and block.get("type") == "tool_result"
]
for block in sorted(
results,
key=lambda item: len(str(item.get("content", ""))),
reverse=True):
if self.estimate_chars(messages) <= target_chars:
break
output = str(block.get("content", ""))
replacement = self.persisted_preview(
block.get("tool_use_id", "unknown"), output, preview_chars=1000)
if len(replacement) < len(output):
block["content"] = replacement
return messages
def summary_input(self, messages: list) -> str:
@@ -419,10 +500,14 @@ class ContextCompactor:
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)
target = int(self.CONTEXT_CHAR_LIMIT * 0.8)
messages = self.micro_compact(messages, target)
if self.estimate_chars(messages) > self.CONTEXT_CHAR_LIMIT:
messages = self.fit_tool_results(messages, target)
if self.estimate_chars(messages) > self.CONTEXT_CHAR_LIMIT:
print("[auto compact]")
messages = self.compact_history(messages, active_request)
return messages

View File

@@ -16,7 +16,7 @@
<!-- Trigger Condition -->
<rect x="20" y="54" width="680" height="44" rx="6" fill="#fef2f2" stroke="#fca5a5" stroke-width="1"/>
<text x="35" y="70" fill="#991b1b" font-size="11" font-weight="600">Trigger Condition</text>
<text x="140" y="70" fill="#991b1b" font-size="11">After Steps 13, estimate_chars(messages) &gt; CONTEXT_CHAR_LIMIT.</text>
<text x="140" y="70" fill="#991b1b" font-size="11">After micro_compact, estimate_chars(messages) &gt; CONTEXT_CHAR_LIMIT.</text>
<text x="140" y="86" fill="#991b1b" font-size="10">The current CONTEXT_CHAR_LIMIT is 50,000 characters.</text>
<!-- Steps -->

Before

Width:  |  Height:  |  Size: 5.7 KiB

After

Width:  |  Height:  |  Size: 5.7 KiB

View File

@@ -16,7 +16,7 @@
<!-- トリガー条件 -->
<rect x="20" y="54" width="680" height="44" rx="6" fill="#fef2f2" stroke="#fca5a5" stroke-width="1"/>
<text x="35" y="70" fill="#991b1b" font-size="11" font-weight="600">トリガー条件</text>
<text x="115" y="70" fill="#991b1b" font-size="11">Step 13 の後、estimate_chars(messages) &gt; CONTEXT_CHAR_LIMIT。</text>
<text x="115" y="70" fill="#991b1b" font-size="11">micro_compact の後、estimate_chars(messages) &gt; CONTEXT_CHAR_LIMIT。</text>
<text x="115" y="86" fill="#991b1b" font-size="10">現在の CONTEXT_CHAR_LIMIT は 50,000 文字。</text>
<!-- ステップ -->

Before

Width:  |  Height:  |  Size: 5.7 KiB

After

Width:  |  Height:  |  Size: 5.8 KiB

View File

@@ -16,7 +16,7 @@
<!-- 触发条件 -->
<rect x="20" y="54" width="680" height="44" rx="6" fill="#fef2f2" stroke="#fca5a5" stroke-width="1"/>
<text x="35" y="70" fill="#991b1b" font-size="11" font-weight="600">触发条件</text>
<text x="105" y="70" fill="#991b1b" font-size="11">前三步执行estimate_chars(messages) &gt; CONTEXT_CHAR_LIMIT。</text>
<text x="105" y="70" fill="#991b1b" font-size="11">micro_compact estimate_chars(messages) &gt; CONTEXT_CHAR_LIMIT。</text>
<text x="105" y="86" fill="#991b1b" font-size="10">当前实现的 CONTEXT_CHAR_LIMIT 为 50,000 个字符。</text>
<!-- 步骤 -->

Before

Width:  |  Height:  |  Size: 5.7 KiB

After

Width:  |  Height:  |  Size: 5.6 KiB

View File

@@ -45,9 +45,9 @@
<rect x="170" y="82" width="200" height="252" rx="10" fill="#fffbeb" stroke="#d97706" stroke-width="2"/>
<text x="270" y="102" fill="#92400e" font-size="11" font-weight="700" text-anchor="middle">Compression Pipeline</text>
<!-- ── ① Every Turn Auto ── -->
<!-- ── ① Pre-processing ── -->
<rect x="186" y="110" width="168" height="16" rx="3" fill="#fde68a" stroke="#d97706" stroke-width="0.8"/>
<text x="270" y="122" fill="#92400e" font-size="8" font-weight="700" text-anchor="middle">① Every Turn · Unconditional · 0 API</text>
<text x="270" y="122" fill="#92400e" font-size="8" font-weight="700" text-anchor="middle">Steps 12 Every Turn · 0 API</text>
<rect x="186" y="130" width="168" height="24" rx="4" fill="#fef3c7" stroke="#d97706" stroke-width="1"/>
<text x="270" y="146" fill="#92400e" font-size="10" font-weight="600" text-anchor="middle">Step 1 tool_result_budget</text>
@@ -56,14 +56,14 @@
<text x="270" y="174" fill="#92400e" font-size="10" font-weight="600" text-anchor="middle">Step 2 snip_compact</text>
<rect x="186" y="186" width="168" height="24" rx="4" fill="#fef3c7" stroke="#d97706" stroke-width="1"/>
<text x="270" y="202" fill="#92400e" font-size="10" font-weight="600" text-anchor="middle">Step 3 micro_compact</text>
<text x="270" y="202" fill="#92400e" font-size="9" font-weight="600" text-anchor="middle">Step 3 micro_compact (over limit)</text>
<!-- ↓ → ◇ -->
<line x1="270" y1="210" x2="270" y2="222" stroke="#555" stroke-width="1.2" marker-end="url(#arrow)"/>
<!-- ◇ Decision Diamond -->
<polygon points="270,226 300,244 270,262 240,244" fill="#f0f4ff" stroke="#ea580c" stroke-width="1.5"/>
<text x="270" y="247" fill="#9a3412" font-size="7" font-weight="600" text-anchor="middle">Over threshold?</text>
<text x="270" y="247" fill="#9a3412" font-size="7" font-weight="600" text-anchor="middle">Still over?</text>
<!-- No: right annotation -->
<text x="306" y="240" fill="#16a34a" font-size="9" font-weight="700">No → Pass</text>
@@ -126,10 +126,10 @@
<text x="94" y="414" fill="#334155" font-size="10">Shared: loop, hooks, permissions, five base tools</text>
<rect x="70" y="426" width="16" height="12" rx="3" fill="#fde68a" stroke="#d97706" stroke-width="1"/>
<text x="94" y="436" fill="#334155" font-size="10">Every Turn: Steps 1→2→3 run before each LLM call, 0 API</text>
<text x="94" y="436" fill="#334155" font-size="10">Pre-process: Steps 1→2 every turn; Step 3 only over the limit, 0 API</text>
<rect x="70" y="448" width="16" height="12" rx="3" fill="#fed7aa" stroke="#ea580c" stroke-width="1"/>
<text x="94" y="458" fill="#334155" font-size="10">② Conditional: size remains over the limit after Step 3 → compact_history, 1 API</text>
<text x="94" y="458" fill="#334155" font-size="10">② Conditional: still over the limit after Step 3 → compact_history, 1 API</text>
<rect x="70" y="470" width="16" height="12" rx="3" fill="#fef2f2" stroke="#dc2626" stroke-width="1" stroke-dasharray="3,2"/>
<text x="94" y="480" fill="#334155" font-size="10">③ Recovery: API returns prompt_too_long → reactive_compact → retry once</text>

Before

Width:  |  Height:  |  Size: 9.0 KiB

After

Width:  |  Height:  |  Size: 9.0 KiB

View File

@@ -45,9 +45,9 @@
<rect x="170" y="82" width="200" height="252" rx="10" fill="#fffbeb" stroke="#d97706" stroke-width="2"/>
<text x="270" y="102" fill="#92400e" font-size="11" font-weight="700" text-anchor="middle">圧縮パイプライン</text>
<!-- ── ① 毎ターン自動 ── -->
<!-- ── ① 前処理 ── -->
<rect x="186" y="110" width="168" height="16" rx="3" fill="#fde68a" stroke="#d97706" stroke-width="0.8"/>
<text x="270" y="122" fill="#92400e" font-size="8" font-weight="700" text-anchor="middle">① 毎ターン自動 · 無条件 · 0 API</text>
<text x="270" y="122" fill="#92400e" font-size="8" font-weight="700" text-anchor="middle">Step 12 は毎ターン · 0 API</text>
<rect x="186" y="130" width="168" height="24" rx="4" fill="#fef3c7" stroke="#d97706" stroke-width="1"/>
<text x="270" y="146" fill="#92400e" font-size="10" font-weight="600" text-anchor="middle">Step 1 tool_result_budget</text>
@@ -56,14 +56,14 @@
<text x="270" y="174" fill="#92400e" font-size="10" font-weight="600" text-anchor="middle">Step 2 snip_compact</text>
<rect x="186" y="186" width="168" height="24" rx="4" fill="#fef3c7" stroke="#d97706" stroke-width="1"/>
<text x="270" y="202" fill="#92400e" font-size="10" font-weight="600" text-anchor="middle">Step 3 micro_compact</text>
<text x="270" y="202" fill="#92400e" font-size="9" font-weight="600" text-anchor="middle">Step 3 micro_compact(上限超過時)</text>
<!-- ↓ → ◇ -->
<line x1="270" y1="210" x2="270" y2="222" stroke="#555" stroke-width="1.2" marker-end="url(#arrow)"/>
<!-- ◇ 判定ダイヤモンド -->
<polygon points="270,226 300,244 270,262 240,244" fill="#f0f4ff" stroke="#ea580c" stroke-width="1.5"/>
<text x="270" y="247" fill="#9a3412" font-size="7" font-weight="600" text-anchor="middle">推定値超過?</text>
<text x="270" y="247" fill="#9a3412" font-size="7" font-weight="600" text-anchor="middle">まだ超過?</text>
<!-- いいえ:右側注釈 -->
<text x="306" y="240" fill="#16a34a" font-size="9" font-weight="700">No → 通過</text>
@@ -126,10 +126,10 @@
<text x="94" y="414" fill="#334155" font-size="10">共通ループ、フック、権限確認、5 個の基本ツール</text>
<rect x="70" y="426" width="16" height="12" rx="3" fill="#fde68a" stroke="#d97706" stroke-width="1"/>
<text x="94" y="436" fill="#334155" font-size="10">毎ターンStep 1→2→3 を各 LLM 呼び出し前に実行、0 API</text>
<text x="94" y="436" fill="#334155" font-size="10">前処理Step 1→2 は毎ターン、Step 3 は上限超過時のみ、0 API</text>
<rect x="70" y="448" width="16" height="12" rx="3" fill="#fed7aa" stroke="#ea580c" stroke-width="1"/>
<text x="94" y="458" fill="#334155" font-size="10">② 条件Step 3 後もサイズ上限超過 → compact_history、1 API</text>
<text x="94" y="458" fill="#334155" font-size="10">② 条件Step 3 後も上限超過 → compact_history、1 API</text>
<rect x="70" y="470" width="16" height="12" rx="3" fill="#fef2f2" stroke="#dc2626" stroke-width="1" stroke-dasharray="3,2"/>
<text x="94" y="480" fill="#334155" font-size="10">③ 回復API が prompt_too_long を返す → reactive_compact → 1 回リトライ</text>

Before

Width:  |  Height:  |  Size: 9.1 KiB

After

Width:  |  Height:  |  Size: 9.1 KiB

View File

@@ -45,9 +45,9 @@
<rect x="170" y="82" width="200" height="252" rx="10" fill="#fffbeb" stroke="#d97706" stroke-width="2"/>
<text x="270" y="102" fill="#92400e" font-size="11" font-weight="700" text-anchor="middle">压缩管线</text>
<!-- ── ① 每轮自动 ── -->
<!-- ── ① 预处理 ── -->
<rect x="186" y="110" width="168" height="16" rx="3" fill="#fde68a" stroke="#d97706" stroke-width="0.8"/>
<text x="270" y="122" fill="#92400e" font-size="8" font-weight="700" text-anchor="middle">① 每轮自动 · 无条件 · 0 API</text>
<text x="270" y="122" fill="#92400e" font-size="8" font-weight="700" text-anchor="middle">Step 12 每轮 · 0 API</text>
<rect x="186" y="130" width="168" height="24" rx="4" fill="#fef3c7" stroke="#d97706" stroke-width="1"/>
<text x="270" y="146" fill="#92400e" font-size="10" font-weight="600" text-anchor="middle">Step 1 tool_result_budget</text>
@@ -56,14 +56,14 @@
<text x="270" y="174" fill="#92400e" font-size="10" font-weight="600" text-anchor="middle">Step 2 snip_compact</text>
<rect x="186" y="186" width="168" height="24" rx="4" fill="#fef3c7" stroke="#d97706" stroke-width="1"/>
<text x="270" y="202" fill="#92400e" font-size="10" font-weight="600" text-anchor="middle">Step 3 micro_compact</text>
<text x="270" y="202" fill="#92400e" font-size="10" font-weight="600" text-anchor="middle">Step 3 micro_compact(超限时)</text>
<!-- ↓ → ◇ -->
<line x1="270" y1="210" x2="270" y2="222" stroke="#555" stroke-width="1.2" marker-end="url(#arrow)"/>
<!-- ◇ 判断菱形(紧凑) -->
<polygon points="270,226 300,244 270,262 240,244" fill="#f0f4ff" stroke="#ea580c" stroke-width="1.5"/>
<text x="270" y="247" fill="#9a3412" font-size="7" font-weight="600" text-anchor="middle">估算超限?</text>
<text x="270" y="247" fill="#9a3412" font-size="7" font-weight="600" text-anchor="middle">超限?</text>
<!-- 否:右侧文字标注 -->
<text x="306" y="240" fill="#16a34a" font-size="9" font-weight="700">否 → 通过</text>
@@ -126,10 +126,10 @@
<text x="94" y="414" fill="#334155" font-size="10">共同骨架循环、hook、权限检查、5 个基础工具</text>
<rect x="70" y="426" width="16" height="12" rx="3" fill="#fde68a" stroke="#d97706" stroke-width="1"/>
<text x="94" y="436" fill="#334155" font-size="10">每轮自动Step 1→2→3 在每次 LLM 调用前执行0 API</text>
<text x="94" y="436" fill="#334155" font-size="10">预处理Step 1→2 每轮执行;超限时再执行 Step 30 API</text>
<rect x="70" y="448" width="16" height="12" rx="3" fill="#fed7aa" stroke="#ea580c" stroke-width="1"/>
<text x="94" y="458" fill="#334155" font-size="10">② 条件触发:前三步后 size 仍超阈值 → compact_history1 API</text>
<text x="94" y="458" fill="#334155" font-size="10">② 条件触发:Step 3 后 size 仍超阈值 → compact_history1 API</text>
<rect x="70" y="470" width="16" height="12" rx="3" fill="#fef2f2" stroke="#dc2626" stroke-width="1" stroke-dasharray="3,2"/>
<text x="94" y="480" fill="#334155" font-size="10">③ 异常触发API 返回 prompt_too_long → reactive_compact → 重试一次</text>

Before

Width:  |  Height:  |  Size: 9.1 KiB

After

Width:  |  Height:  |  Size: 9.1 KiB

View File

@@ -39,7 +39,7 @@
<!-- ===== Pre-processing pipeline title ===== -->
<rect x="20" y="146" width="720" height="24" rx="4" fill="#f1f5f9"/>
<text x="55" y="163" fill="#64748b" font-size="11" font-weight="600">Pre-processing (Step 1 → Step 2 → Step 3 before every LLM call, 0 API)</text>
<text x="55" y="163" fill="#64748b" font-size="11" font-weight="600">Pre-processing (Steps 1 → 2 every turn; Step 3 only over the limit, 0 API)</text>
<!-- Step 1: tool_result_budget -->
<rect x="80" y="180" width="600" height="46" rx="7" fill="url(#pre)" stroke="#2563eb" stroke-width="1.5"/>
@@ -67,13 +67,13 @@
<rect x="80" y="300" width="600" height="46" rx="7" fill="url(#pre)" stroke="#2563eb" stroke-width="1.5"/>
<text x="100" y="320" fill="#1e40af" font-size="12" font-weight="600">Step 3</text>
<text x="155" y="320" fill="#1e40af" font-size="13" font-weight="700">micro_compact</text>
<text x="260" y="320" fill="#1e40af" font-size="11">old tool_result → placeholder (keep latest 3)</text>
<text x="260" y="320" fill="#1e40af" font-size="11">old tool_result → recovery path (keep latest 3)</text>
<text x="650" y="320" fill="#1e40af" font-size="10" text-anchor="end">compact old</text>
<text x="155" y="338" fill="#2563eb" font-size="9">Runs every turn and keeps the latest 3 results complete</text>
<text x="155" y="338" fill="#2563eb" font-size="9">Runs over limit; saves old results and targets 80% of the limit</text>
<!-- ===== Auto-compact title ===== -->
<rect x="20" y="358" width="720" height="24" rx="4" fill="#f1f5f9"/>
<text x="70" y="375" fill="#64748b" font-size="11" font-weight="600">Auto-compact Decision (triggered when pre-processing is insufficient, 1 API call)</text>
<text x="70" y="375" fill="#64748b" font-size="11" font-weight="600">Auto-compact Decision (triggered when still over after Step 3, 1 API call)</text>
<!-- Step 4: compact_history -->
<rect x="80" y="390" width="600" height="58" rx="7" fill="url(#auto)" stroke="#dc2626" stroke-width="2"/>

Before

Width:  |  Height:  |  Size: 6.6 KiB

After

Width:  |  Height:  |  Size: 6.6 KiB

View File

@@ -39,7 +39,7 @@
<!-- ===== 前処理パイプラインタイトル ===== -->
<rect x="20" y="146" width="720" height="24" rx="4" fill="#f1f5f9"/>
<text x="55" y="163" fill="#64748b" font-size="11" font-weight="600">前処理Step 1 → Step 2 → Step 3、各 LLM 呼び出し前、0 API</text>
<text x="55" y="163" fill="#64748b" font-size="11" font-weight="600">前処理Step 1 → 2 は毎ターン、Step 3 は上限超過時のみ、0 API</text>
<!-- Step 1: tool_result_budget -->
<rect x="80" y="180" width="600" height="46" rx="7" fill="url(#pre)" stroke="#2563eb" stroke-width="1.5"/>
@@ -67,13 +67,13 @@
<rect x="80" y="300" width="600" height="46" rx="7" fill="url(#pre)" stroke="#2563eb" stroke-width="1.5"/>
<text x="100" y="320" fill="#1e40af" font-size="12" font-weight="600">Step 3</text>
<text x="155" y="320" fill="#1e40af" font-size="13" font-weight="700">micro_compact</text>
<text x="260" y="320" fill="#1e40af" font-size="11">古い tool_result → プレースホルダー(最新 3 件保持)</text>
<text x="260" y="320" fill="#1e40af" font-size="11">古い tool_result → 復元パス(最新 3 件保持)</text>
<text x="650" y="320" fill="#1e40af" font-size="10" text-anchor="end">旧結果を圧縮</text>
<text x="155" y="338" fill="#2563eb" font-size="9">毎ターン実行し、最新 3 件は完全に保持</text>
<text x="155" y="338" fill="#2563eb" font-size="9">上限超過時に古い結果を保存し、上限の約 80% を目標に短縮</text>
<!-- ===== 自動圧縮タイトル ===== -->
<rect x="20" y="358" width="720" height="24" rx="4" fill="#f1f5f9"/>
<text x="70" y="375" fill="#64748b" font-size="11" font-weight="600">自動圧縮判定(前処理で不足時にトリガー、1 API 呼び出し)</text>
<text x="70" y="375" fill="#64748b" font-size="11" font-weight="600">自動圧縮判定(Step 3 後も上限超過時にトリガー、1 API 呼び出し)</text>
<!-- Step 4: compact_history -->
<rect x="80" y="390" width="600" height="58" rx="7" fill="url(#auto)" stroke="#dc2626" stroke-width="2"/>

Before

Width:  |  Height:  |  Size: 6.8 KiB

After

Width:  |  Height:  |  Size: 6.9 KiB

View File

@@ -39,7 +39,7 @@
<!-- ===== 预处理管线标题 ===== -->
<rect x="20" y="146" width="720" height="24" rx="4" fill="#f1f5f9"/>
<text x="55" y="163" fill="#64748b" font-size="11" font-weight="600">预处理管线(执行顺序:Step 1 → Step 2 → Step 3每轮调用前执行0 API</text>
<text x="55" y="163" fill="#64748b" font-size="11" font-weight="600">预处理管线Step 1 → Step 2 每轮执行;超限时执行 Step 30 API</text>
<!-- Step 1: tool_result_budget -->
<rect x="80" y="180" width="600" height="46" rx="7" fill="url(#pre)" stroke="#2563eb" stroke-width="1.5"/>
@@ -67,13 +67,13 @@
<rect x="80" y="300" width="600" height="46" rx="7" fill="url(#pre)" stroke="#2563eb" stroke-width="1.5"/>
<text x="100" y="320" fill="#1e40af" font-size="12" font-weight="600">Step 3</text>
<text x="155" y="320" fill="#1e40af" font-size="13" font-weight="700">micro_compact</text>
<text x="260" y="320" fill="#1e40af" font-size="11">旧 tool_result → 占位符(保留最近 3 条)</text>
<text x="260" y="320" fill="#1e40af" font-size="11">旧 tool_result → 恢复路径(保留最近 3 条)</text>
<text x="650" y="320" fill="#1e40af" font-size="10" text-anchor="end">压旧结果</text>
<text x="155" y="338" fill="#2563eb" font-size="9">每轮执行,最近 3 条结果保持完整</text>
<text x="155" y="338" fill="#2563eb" font-size="9">超限时保存旧结果,并将上下文压到阈值约 80%</text>
<!-- ===== 自动压缩标题 ===== -->
<rect x="20" y="358" width="720" height="24" rx="4" fill="#f1f5f9"/>
<text x="70" y="375" fill="#64748b" font-size="11" font-weight="600">自动压缩决策(预处理不够时触发1 API 调用)</text>
<text x="70" y="375" fill="#64748b" font-size="11" font-weight="600">自动压缩决策(Step 3 后仍超限时触发1 API 调用)</text>
<!-- Step 4: compact_history -->
<rect x="80" y="390" width="600" height="58" rx="7" fill="url(#auto)" stroke="#dc2626" stroke-width="2"/>

Before

Width:  |  Height:  |  Size: 6.6 KiB

After

Width:  |  Height:  |  Size: 6.6 KiB

View File

@@ -41,18 +41,18 @@
<rect x="400" y="130" width="290" height="10" rx="2" fill="#fef3c7"/>
<text x="408" y="138" fill="#92400e" font-size="8" font-family="monospace">[Earlier tool result saved at .task_outputs/...]</text>
<rect x="400" y="145" width="290" height="10" rx="2" fill="#fef3c7"/>
<text x="408" y="153" fill="#92400e" font-size="8" font-family="monospace">[Earlier tool result omitted.]</text>
<text x="408" y="153" fill="#92400e" font-size="8" font-family="monospace">[Earlier tool result saved at .task_outputs/...]</text>
<rect x="400" y="160" width="290" height="10" rx="2" fill="#fef3c7"/>
<text x="408" y="168" fill="#92400e" font-size="8" font-family="monospace">[Earlier tool result omitted.]</text>
<text x="408" y="168" fill="#92400e" font-size="8" font-family="monospace">[Earlier tool result saved at .task_outputs/...]</text>
<rect x="400" y="175" width="290" height="10" rx="2" fill="#fef3c7"/>
<text x="408" y="183" fill="#92400e" font-size="8" font-family="monospace">Read file J: (full content, 2800 chars)</text>
<text x="545" y="212" fill="#ca8a04" font-size="9" font-weight="600" text-anchor="middle">Keep latest 3; first 7 become placeholders</text>
<text x="545" y="212" fill="#ca8a04" font-size="9" font-weight="600" text-anchor="middle">Keep latest 3; first 7 become recovery paths</text>
<!-- How -->
<rect x="20" y="228" width="680" height="62" rx="6" fill="#f8fafc" stroke="#cbd5e1" stroke-width="1"/>
<text x="35" y="248" fill="#1e3a5f" font-size="11" font-weight="600">Rule</text>
<text x="75" y="248" fill="#475569" font-size="10">Keep the latest 3; replace older results above 120 characters with placeholders.</text>
<text x="35" y="264" fill="#1e3a5f" font-size="11" font-weight="600">Placeholder</text>
<text x="105" y="264" fill="#475569" font-size="10">Keep the saved path when one exists; otherwise mark the result omitted.</text>
<text x="75" y="248" fill="#475569" font-size="10">Keep the latest 3; save and shorten older results until context reaches 80%.</text>
<text x="35" y="264" fill="#1e3a5f" font-size="11" font-weight="600">Recovery</text>
<text x="95" y="264" fill="#475569" font-size="10">Every shortened result retains its trusted path under .task_outputs/.</text>
<text x="105" y="280" fill="#94a3b8" font-size="9">The message structure remains valid for the next loop iteration.</text>
</svg>

Before

Width:  |  Height:  |  Size: 4.3 KiB

After

Width:  |  Height:  |  Size: 4.3 KiB

View File

@@ -41,18 +41,18 @@
<rect x="400" y="130" width="290" height="10" rx="2" fill="#fef3c7"/>
<text x="408" y="138" fill="#92400e" font-size="8" font-family="monospace">[Earlier tool result saved at .task_outputs/...]</text>
<rect x="400" y="145" width="290" height="10" rx="2" fill="#fef3c7"/>
<text x="408" y="153" fill="#92400e" font-size="8" font-family="monospace">[Earlier tool result omitted.]</text>
<text x="408" y="153" fill="#92400e" font-size="8" font-family="monospace">[Earlier tool result saved at .task_outputs/...]</text>
<rect x="400" y="160" width="290" height="10" rx="2" fill="#fef3c7"/>
<text x="408" y="168" fill="#92400e" font-size="8" font-family="monospace">[Earlier tool result omitted.]</text>
<text x="408" y="168" fill="#92400e" font-size="8" font-family="monospace">[Earlier tool result saved at .task_outputs/...]</text>
<rect x="400" y="175" width="290" height="10" rx="2" fill="#fef3c7"/>
<text x="408" y="183" fill="#92400e" font-size="8" font-family="monospace">Read file J: (完全な内容, 2800 文字)</text>
<text x="545" y="212" fill="#ca8a04" font-size="9" font-weight="600" text-anchor="middle">最新 3 件を保持、前 7 件は置換</text>
<text x="545" y="212" fill="#ca8a04" font-size="9" font-weight="600" text-anchor="middle">最新 3 件を保持、前 7 件は復元パスへ置換</text>
<!-- 原理 -->
<rect x="20" y="228" width="680" height="62" rx="6" fill="#f8fafc" stroke="#cbd5e1" stroke-width="1"/>
<text x="35" y="248" fill="#1e3a5f" font-size="11" font-weight="600">処理規則</text>
<text x="95" y="248" fill="#475569" font-size="10">最新 3 件を保持し、120 文字超の古い結果をプレースホルダーに置換</text>
<text x="35" y="264" fill="#1e3a5f" font-size="11" font-weight="600">プレースホルダー</text>
<text x="125" y="264" fill="#475569" font-size="10">保存先があればパスを残し、なければ省略済みと示す。</text>
<text x="95" y="248" fill="#475569" font-size="10">最新 3 件を保持し、古い結果を保存して上限の 80% まで短縮</text>
<text x="35" y="264" fill="#1e3a5f" font-size="11" font-weight="600">復元方法</text>
<text x="95" y="264" fill="#475569" font-size="10">短縮した各結果に .task_outputs/ 内の信頼できるパスを残す。</text>
<text x="125" y="280" fill="#94a3b8" font-size="9">メッセージ構造を保ったまま次のループへ進める。</text>
</svg>

Before

Width:  |  Height:  |  Size: 4.5 KiB

After

Width:  |  Height:  |  Size: 4.5 KiB

View File

@@ -11,7 +11,7 @@
<rect width="720" height="300" fill="#fafbfc" rx="8"/>
<rect x="0" y="0" width="720" height="38" fill="url(#header)" rx="8"/>
<rect x="0" y="30" width="720" height="8" fill="url(#header)"/>
<text x="360" y="25" fill="#fff" font-size="14" font-weight="700" text-anchor="middle">Step 3: micro_compact旧结果占位替换</text>
<text x="360" y="25" fill="#fff" font-size="14" font-weight="700" text-anchor="middle">Step 3: micro_compact旧结果可恢复替换</text>
<!-- 痛点 -->
<rect x="20" y="54" width="680" height="36" rx="6" fill="#fef2f2" stroke="#fca5a5" stroke-width="1"/>
@@ -40,18 +40,18 @@
<rect x="400" y="130" width="290" height="10" rx="2" fill="#fef3c7"/>
<text x="408" y="138" fill="#92400e" font-size="8" font-family="monospace">[Earlier tool result saved at .task_outputs/...]</text>
<rect x="400" y="145" width="290" height="10" rx="2" fill="#fef3c7"/>
<text x="408" y="153" fill="#92400e" font-size="8" font-family="monospace">[Earlier tool result omitted.]</text>
<text x="408" y="153" fill="#92400e" font-size="8" font-family="monospace">[Earlier tool result saved at .task_outputs/...]</text>
<rect x="400" y="160" width="290" height="10" rx="2" fill="#fef3c7"/>
<text x="408" y="168" fill="#92400e" font-size="8" font-family="monospace">[Earlier tool result omitted.]</text>
<text x="408" y="168" fill="#92400e" font-size="8" font-family="monospace">[Earlier tool result saved at .task_outputs/...]</text>
<rect x="400" y="175" width="290" height="10" rx="2" fill="#fef3c7"/>
<text x="408" y="183" fill="#92400e" font-size="8" font-family="monospace">Read file J: (完整内容, 2800 字符)</text>
<text x="545" y="212" fill="#ca8a04" font-size="9" font-weight="600">保留最近 3 条,前 7 条变占位</text>
<text x="545" y="212" fill="#ca8a04" font-size="9" font-weight="600">保留最近 3 条,前 7 条变恢复路径</text>
<!-- 原理 -->
<rect x="20" y="228" width="680" height="62" rx="6" fill="#f8fafc" stroke="#cbd5e1" stroke-width="1"/>
<text x="35" y="248" fill="#1e3a5f" font-size="11" font-weight="600">处理规则</text>
<text x="95" y="248" fill="#475569" font-size="10">最近 3 条保持完整,更早且超过 120 字符的结果替换为占位符</text>
<text x="35" y="264" fill="#1e3a5f" font-size="11" font-weight="600">占位内容</text>
<text x="95" y="264" fill="#475569" font-size="10">有落盘路径时保留路径,否则标记该结果已省略</text>
<text x="95" y="248" fill="#475569" font-size="10">最近 3 条保持完整,更早的结果先保存,再逐条缩短到阈值 80%</text>
<text x="35" y="264" fill="#1e3a5f" font-size="11" font-weight="600">恢复方式</text>
<text x="95" y="264" fill="#475569" font-size="10">每条缩短结果都保留 .task_outputs/ 下的可信路径</text>
<text x="95" y="280" fill="#94a3b8" font-size="9">消息结构保持不变,后续循环仍可继续处理。</text>
</svg>

Before

Width:  |  Height:  |  Size: 4.3 KiB

After

Width:  |  Height:  |  Size: 4.3 KiB