Merge pull request #537 from 0xJieREN/fix/s08-recursive-glob-conditional-micro-compact
fix(s08): support recursive glob and conditional micro compact
@@ -74,7 +74,12 @@ def run_edit(path, old_text, new_text):
|
|||||||
|
|
||||||
def run_glob(pattern):
|
def run_glob(pattern):
|
||||||
import glob as g
|
import glob as g
|
||||||
return "\n".join(g.glob(pattern, root_dir=WORKDIR))
|
matches = sorted(set(g.glob(
|
||||||
|
pattern, root_dir=WORKDIR, recursive=True)))
|
||||||
|
shown = matches[:200]
|
||||||
|
if len(matches) > 200:
|
||||||
|
shown.append("... (more matches omitted; narrow the pattern)")
|
||||||
|
return "\n".join(shown)
|
||||||
```
|
```
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|||||||
@@ -74,7 +74,12 @@ def run_edit(path, old_text, new_text):
|
|||||||
|
|
||||||
def run_glob(pattern):
|
def run_glob(pattern):
|
||||||
import glob as g
|
import glob as g
|
||||||
return "\n".join(g.glob(pattern, root_dir=WORKDIR))
|
matches = sorted(set(g.glob(
|
||||||
|
pattern, root_dir=WORKDIR, recursive=True)))
|
||||||
|
shown = matches[:200]
|
||||||
|
if len(matches) > 200:
|
||||||
|
shown.append("... (more matches omitted; narrow the pattern)")
|
||||||
|
return "\n".join(shown)
|
||||||
```
|
```
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|||||||
@@ -74,7 +74,12 @@ def run_edit(path, old_text, new_text):
|
|||||||
|
|
||||||
def run_glob(pattern):
|
def run_glob(pattern):
|
||||||
import glob as g
|
import glob as g
|
||||||
return "\n".join(g.glob(pattern, root_dir=WORKDIR))
|
matches = sorted(set(g.glob(
|
||||||
|
pattern, root_dir=WORKDIR, recursive=True)))
|
||||||
|
shown = matches[:200]
|
||||||
|
if len(matches) > 200:
|
||||||
|
shown.append("... (more matches omitted; narrow the pattern)")
|
||||||
|
return "\n".join(shown)
|
||||||
```
|
```
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|||||||
@@ -110,11 +110,15 @@ def run_edit(path: str, old_text: str, new_text: str) -> str:
|
|||||||
def run_glob(pattern: str) -> str:
|
def run_glob(pattern: str) -> str:
|
||||||
import glob as g
|
import glob as g
|
||||||
try:
|
try:
|
||||||
results = []
|
matches = sorted({
|
||||||
for match in g.glob(pattern, root_dir=WORKDIR):
|
match for match in g.glob(
|
||||||
if (WORKDIR / match).resolve().is_relative_to(WORKDIR):
|
pattern, root_dir=WORKDIR, recursive=True)
|
||||||
results.append(match)
|
if (WORKDIR / match).resolve().is_relative_to(WORKDIR)
|
||||||
return "\n".join(results) if results 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 e:
|
except Exception as e:
|
||||||
return f"Error: {e}"
|
return f"Error: {e}"
|
||||||
|
|
||||||
@@ -130,7 +134,7 @@ TOOLS = [
|
|||||||
"input_schema": {"type": "object", "properties": {"path": {"type": "string"}, "content": {"type": "string"}}, "required": ["path", "content"]}},
|
"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.",
|
{"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"]}},
|
"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"]}},
|
"input_schema": {"type": "object", "properties": {"pattern": {"type": "string"}}, "required": ["pattern"]}},
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|||||||
@@ -105,11 +105,15 @@ def run_edit(path: str, old_text: str, new_text: str) -> str:
|
|||||||
def run_glob(pattern: str) -> str:
|
def run_glob(pattern: str) -> str:
|
||||||
import glob as g
|
import glob as g
|
||||||
try:
|
try:
|
||||||
results = []
|
matches = sorted({
|
||||||
for match in g.glob(pattern, root_dir=WORKDIR):
|
match for match in g.glob(
|
||||||
if (WORKDIR / match).resolve().is_relative_to(WORKDIR):
|
pattern, root_dir=WORKDIR, recursive=True)
|
||||||
results.append(match)
|
if (WORKDIR / match).resolve().is_relative_to(WORKDIR)
|
||||||
return "\n".join(results) if results 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 e:
|
except Exception as e:
|
||||||
return f"Error: {e}"
|
return f"Error: {e}"
|
||||||
|
|
||||||
@@ -125,7 +129,7 @@ TOOLS = [
|
|||||||
"input_schema": {"type": "object", "properties": {"path": {"type": "string"}, "content": {"type": "string"}}, "required": ["path", "content"]}},
|
"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.",
|
{"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"]}},
|
"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"]}},
|
"input_schema": {"type": "object", "properties": {"pattern": {"type": "string"}}, "required": ["pattern"]}},
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|||||||
@@ -91,11 +91,15 @@ def run_edit(path: str, old_text: str, new_text: str) -> str:
|
|||||||
def run_glob(pattern: str) -> str:
|
def run_glob(pattern: str) -> str:
|
||||||
import glob as g
|
import glob as g
|
||||||
try:
|
try:
|
||||||
results = []
|
matches = sorted({
|
||||||
for match in g.glob(pattern, root_dir=WORKDIR):
|
match for match in g.glob(
|
||||||
if (WORKDIR / match).resolve().is_relative_to(WORKDIR):
|
pattern, root_dir=WORKDIR, recursive=True)
|
||||||
results.append(match)
|
if (WORKDIR / match).resolve().is_relative_to(WORKDIR)
|
||||||
return "\n".join(results) if results 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 e:
|
except Exception as e:
|
||||||
return f"Error: {e}"
|
return f"Error: {e}"
|
||||||
|
|
||||||
@@ -108,7 +112,7 @@ TOOLS = [
|
|||||||
"input_schema": {"type": "object", "properties": {"path": {"type": "string"}, "content": {"type": "string"}}, "required": ["path", "content"]}},
|
"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.",
|
{"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"]}},
|
"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"]}},
|
"input_schema": {"type": "object", "properties": {"pattern": {"type": "string"}}, "required": ["pattern"]}},
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|||||||
@@ -96,11 +96,15 @@ def run_edit(path: str, old_text: str, new_text: str) -> str:
|
|||||||
def run_glob(pattern: str) -> str:
|
def run_glob(pattern: str) -> str:
|
||||||
import glob as g
|
import glob as g
|
||||||
try:
|
try:
|
||||||
results = []
|
matches = sorted({
|
||||||
for match in g.glob(pattern, root_dir=WORKDIR):
|
match for match in g.glob(
|
||||||
if (WORKDIR / match).resolve().is_relative_to(WORKDIR):
|
pattern, root_dir=WORKDIR, recursive=True)
|
||||||
results.append(match)
|
if (WORKDIR / match).resolve().is_relative_to(WORKDIR)
|
||||||
return "\n".join(results) if results 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 e:
|
except Exception as e:
|
||||||
return f"Error: {e}"
|
return f"Error: {e}"
|
||||||
|
|
||||||
@@ -186,7 +190,7 @@ TOOLS = [
|
|||||||
"input_schema": {"type": "object", "properties": {"path": {"type": "string"}, "content": {"type": "string"}}, "required": ["path", "content"]}},
|
"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.",
|
{"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"]}},
|
"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"]}},
|
"input_schema": {"type": "object", "properties": {"pattern": {"type": "string"}}, "required": ["pattern"]}},
|
||||||
# s05: new tool
|
# s05: new tool
|
||||||
{"name": "todo_write", "description": "Create and manage a task list for your current coding session.",
|
{"name": "todo_write", "description": "Create and manage a task list for your current coding session.",
|
||||||
|
|||||||
@@ -101,11 +101,15 @@ def run_edit(path: str, old_text: str, new_text: str) -> str:
|
|||||||
def run_glob(pattern: str) -> str:
|
def run_glob(pattern: str) -> str:
|
||||||
import glob
|
import glob
|
||||||
try:
|
try:
|
||||||
matches = []
|
matches = sorted({
|
||||||
for match in glob.glob(pattern, root_dir=WORKDIR):
|
match for match in glob.glob(
|
||||||
if (WORKDIR / match).resolve().is_relative_to(WORKDIR):
|
pattern, root_dir=WORKDIR, recursive=True)
|
||||||
matches.append(match)
|
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 e:
|
except Exception as e:
|
||||||
return f"Error: {e}"
|
return f"Error: {e}"
|
||||||
|
|
||||||
@@ -119,7 +123,7 @@ BASE_TOOLS = [
|
|||||||
"input_schema": {"type": "object", "properties": {"path": {"type": "string"}, "content": {"type": "string"}}, "required": ["path", "content"]}},
|
"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.",
|
{"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"]}},
|
"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"]}},
|
"input_schema": {"type": "object", "properties": {"pattern": {"type": "string"}}, "required": ["pattern"]}},
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|||||||
@@ -185,11 +185,15 @@ def run_edit(path: str, old_text: str, new_text: str) -> str:
|
|||||||
def run_glob(pattern: str) -> str:
|
def run_glob(pattern: str) -> str:
|
||||||
import glob
|
import glob
|
||||||
try:
|
try:
|
||||||
matches = []
|
matches = sorted({
|
||||||
for match in glob.glob(pattern, root_dir=WORKDIR):
|
match for match in glob.glob(
|
||||||
if (WORKDIR / match).resolve().is_relative_to(WORKDIR):
|
pattern, root_dir=WORKDIR, recursive=True)
|
||||||
matches.append(match)
|
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 e:
|
except Exception as e:
|
||||||
return f"Error: {e}"
|
return f"Error: {e}"
|
||||||
|
|
||||||
@@ -203,7 +207,7 @@ TOOLS = [
|
|||||||
"input_schema": {"type": "object", "properties": {"path": {"type": "string"}, "content": {"type": "string"}}, "required": ["path", "content"]}},
|
"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.",
|
{"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"]}},
|
"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"]}},
|
"input_schema": {"type": "object", "properties": {"pattern": {"type": "string"}}, "required": ["pattern"]}},
|
||||||
{"name": "load_skill", "description": "Load the full SKILL.md content by skill name.",
|
{"name": "load_skill", "description": "Load the full SKILL.md content by skill name.",
|
||||||
"input_schema": {"type": "object", "properties": {"name": {"type": "string"}}, "required": ["name"]}},
|
"input_schema": {"type": "object", "properties": {"name": {"type": "string"}}, "required": ["name"]}},
|
||||||
|
|||||||
@@ -88,11 +88,11 @@ for block in ranked:
|
|||||||
|
|
||||||
## ステップ 2:snip_compact
|
## ステップ 2:snip_compact
|
||||||
|
|
||||||
履歴が 50 メッセージを超えると、`snip_compact` は完全な履歴を `.transcripts/` に保存してから、先頭 3 件と最新 47 件を保持します。中間のマーカーには、削除した件数と transcript の保存先を記録します。
|
履歴が 50 メッセージを超えると、`snip_compact` は完全な履歴を `.transcripts/` に保存してから、先頭 3 件と最新 46 件を保持します。残り 1 件は archive marker に使い、削除した件数と完全な transcript の保存先を記録します。
|
||||||
|
|
||||||
```python
|
```python
|
||||||
head_end = 3
|
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]):
|
if self.has_tool_use(messages[head_end - 1]):
|
||||||
while (head_end < tail_start
|
while (head_end < tail_start
|
||||||
@@ -117,37 +117,34 @@ messages = [*messages[:head_end], marker, *messages[tail_start:]]
|
|||||||
|
|
||||||
## ステップ 3:micro_compact
|
## ステップ 3:micro_compact
|
||||||
|
|
||||||
`micro_compact` は直近の assistant 応答より後に追加されたすべての `tool_result` を完全に保持し、モデルが各結果を少なくとも 1 回は完全な形で読めるようにします。モデルがすでに読んだ結果については最新 3 件を残し、それより古く 120 文字を超える結果を短くします。保存済みの結果にはファイルパスを残し、それ以外はプレースホルダーに置き換えます。
|
最初の 2 ステップの後、`prepare` は残りのコンテキストサイズを推定し、`CONTEXT_CHAR_LIMIT` を超えている場合にだけ `micro_compact` を実行します。モデルがすでに読んだ結果については最新 3 件を残し、それより古く 120 文字を超える結果を、コンテキストが上限の 80% に近づくまで順に短くします。古い結果は置換前に完全な内容をディスクへ保存するため、各プレースホルダーには復元用のパスが残ります。
|
||||||
|
|
||||||

|

|
||||||
|
|
||||||
```python
|
```python
|
||||||
unseen = self.unseen_tool_result_positions(messages)
|
unseen = self.unseen_tool_result_positions(messages)
|
||||||
consumed = [entry for entry in results if entry[:2] not in unseen]
|
consumed = [entry for entry in results if entry[:2] not in unseen]
|
||||||
|
|
||||||
for _, _, block in consumed[:-self.KEEP_RECENT_RESULTS]:
|
for _, _, block in consumed[:-self.KEEP_RECENT_RESULTS]:
|
||||||
|
if self.estimate_chars(messages) <= target_chars:
|
||||||
|
break
|
||||||
content = str(block.get("content", ""))
|
content = str(block.get("content", ""))
|
||||||
if len(content) <= 120:
|
if len(content) <= 120:
|
||||||
continue
|
continue
|
||||||
saved_path = next(
|
saved_path = self.persisted_output_path(content)
|
||||||
(line.removeprefix("Full output: ") for line in content.splitlines()
|
if not saved_path:
|
||||||
if line.startswith("Full output: ")),
|
saved_path = self.save_output(block["tool_use_id"], content)
|
||||||
None,
|
block["content"] = f"[Earlier tool result saved at {saved_path}]"
|
||||||
)
|
|
||||||
block["content"] = (
|
|
||||||
f"[Earlier tool result saved at {saved_path}]"
|
|
||||||
if saved_path else "[Earlier tool result omitted.]"
|
|
||||||
)
|
|
||||||
```
|
```
|
||||||
|
|
||||||
保存していない古い結果にはプレースホルダーだけが残ります。ステップ 1 で保存した結果には、完全な出力を読み直すためのパスが残ります。
|
新しい結果は通常、モデルが一度読むまで完全な形で保持されます。未読の最新バッチだけでコンテキストを超える場合、`fit_tool_results` は大きな結果を保存し、1,000 文字の preview と完全な出力へのパスを残します。これにより、モデルが新しい結果を見る前に履歴全体を要約する事態を避けます。
|
||||||
|
|
||||||
最初の 3 ステップは、決定的なテキスト処理と構造操作です。追加の API 呼び出しは発生しません。
|
最初の 2 ステップは毎ラウンド実行され、ステップ 3 はコンテキストが上限を超えた場合にだけ実行されます。3 ステップとも決定的で復元可能なテキスト処理と構造操作であり、追加の API 呼び出しは発生しません。
|
||||||
|
|
||||||
|
|
||||||
## ステップ 4:compact_history
|
## ステップ 4:compact_history
|
||||||
|
|
||||||
最初の 3 ステップの後、コードは `estimate_chars(messages)` で現在のメッセージに含まれる文字数を数えます。
|
`micro_compact` と `fit_tool_results` の後、コードは `estimate_chars(messages)` でコンテキストを再び推定します。
|
||||||
|
|
||||||
```python
|
```python
|
||||||
CONTEXT_CHAR_LIMIT = 50000
|
CONTEXT_CHAR_LIMIT = 50000
|
||||||
@@ -156,7 +153,7 @@ def estimate_chars(messages):
|
|||||||
return len(json.dumps(messages, default=str, ensure_ascii=False))
|
return len(json.dumps(messages, default=str, ensure_ascii=False))
|
||||||
```
|
```
|
||||||
|
|
||||||
文字数が `CONTEXT_CHAR_LIMIT` を超えると、`compact_history` は 4 つの処理を行います。
|
文字数がまだ `CONTEXT_CHAR_LIMIT` を超えている場合、`compact_history` は 4 つの処理を行います。
|
||||||
|
|
||||||
1. 完全なメッセージ履歴を `.transcripts/` に書き込みます。
|
1. 完全なメッセージ履歴を `.transcripts/` に書き込みます。
|
||||||
2. モデルに事実だけの状態要約を依頼します。
|
2. モデルに事実だけの状態要約を依頼します。
|
||||||
@@ -181,19 +178,24 @@ def compact_history(messages, active_request):
|
|||||||
|
|
||||||
## 順序を固定する理由
|
## 順序を固定する理由
|
||||||
|
|
||||||
パイプラインは常に次の順序で実行されます。
|
パイプラインは次の順序で処理し、必要な場合にだけ情報を失う要約へ進みます。
|
||||||
|
|
||||||
```text
|
```python
|
||||||
tool_result_budget
|
messages = self.tool_result_budget(messages)
|
||||||
→ snip_compact
|
messages = self.snip_compact(messages)
|
||||||
→ micro_compact
|
if self.estimate_chars(messages) > self.CONTEXT_CHAR_LIMIT:
|
||||||
→ compact_history(上限を超えた場合)
|
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 つの条件があります。
|
この順序には 2 つの条件があります。
|
||||||
|
|
||||||
1. 最初の 3 ステップはモデルを呼び出しません。ステップ 4 だけが API リクエストを追加します。
|
1. ステップ 1 と 2 は毎ラウンド実行され、ステップ 3 は上限を超えた場合だけ実行されます。API リクエストを追加するのはステップ 4 だけです。
|
||||||
2. `tool_result_budget` は `micro_compact` より先に動く必要があります。古い結果をプレースホルダーにする前に、大きな結果をディスクへ保存します。
|
2. 短縮した各ツール結果には `.task_outputs/tool-results/` 内の信頼できるパスを残します。それでも上限を超える場合にだけ、モデルによる履歴要約へ進みます。
|
||||||
|
|
||||||
各ラウンドは、コストが低く情報を再取得しやすい処理から始まります。
|
各ラウンドは、コストが低く情報を再取得しやすい処理から始まります。
|
||||||
|
|
||||||
@@ -243,7 +245,7 @@ def agent_loop(messages, active_request):
|
|||||||
raise
|
raise
|
||||||
```
|
```
|
||||||
|
|
||||||
すべてのモデル呼び出しが同じパイプラインを通ります。CLI は `query` を追加した後に `agent_loop(history, query)` を呼ぶため、圧縮を繰り返しても現在の要求は失われません。最初の 3 ステップ後も上限を超える場合、または API が拒否した場合にだけ、コードはモデルへ要約を依頼します。
|
すべてのモデル呼び出しが同じパイプラインを通ります。CLI は `query` を追加した後に `agent_loop(history, query)` を呼ぶため、圧縮を繰り返しても現在の要求は失われません。`micro_compact` の後も上限を超える場合、または API が拒否した場合にだけ、コードはモデルへ要約を依頼します。
|
||||||
|
|
||||||
|
|
||||||
## compact ツール
|
## 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:大きな結果を保存する
|
### 実験 2:大きな結果を保存する
|
||||||
|
|
||||||
|
|||||||
@@ -88,11 +88,11 @@ This step examines only the latest batch of tool results. The complete output re
|
|||||||
|
|
||||||
## Step 2: snip_compact
|
## 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
|
```python
|
||||||
head_end = 3
|
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]):
|
if self.has_tool_use(messages[head_end - 1]):
|
||||||
while (head_end < tail_start
|
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
|
## 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:
|
||||||
|
|
||||||

|

|
||||||
|
|
||||||
```python
|
```python
|
||||||
unseen = self.unseen_tool_result_positions(messages)
|
unseen = self.unseen_tool_result_positions(messages)
|
||||||
consumed = [entry for entry in results if entry[:2] not in unseen]
|
consumed = [entry for entry in results if entry[:2] not in unseen]
|
||||||
|
|
||||||
for _, _, block in consumed[:-self.KEEP_RECENT_RESULTS]:
|
for _, _, block in consumed[:-self.KEEP_RECENT_RESULTS]:
|
||||||
|
if self.estimate_chars(messages) <= target_chars:
|
||||||
|
break
|
||||||
content = str(block.get("content", ""))
|
content = str(block.get("content", ""))
|
||||||
if len(content) <= 120:
|
if len(content) <= 120:
|
||||||
continue
|
continue
|
||||||
saved_path = next(
|
saved_path = self.persisted_output_path(content)
|
||||||
(line.removeprefix("Full output: ") for line in content.splitlines()
|
if not saved_path:
|
||||||
if line.startswith("Full output: ")),
|
saved_path = self.save_output(block["tool_use_id"], content)
|
||||||
None,
|
block["content"] = f"[Earlier tool result saved at {saved_path}]"
|
||||||
)
|
|
||||||
block["content"] = (
|
|
||||||
f"[Earlier tool result saved at {saved_path}]"
|
|
||||||
if saved_path else "[Earlier tool result omitted.]"
|
|
||||||
)
|
|
||||||
```
|
```
|
||||||
|
|
||||||
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
|
## 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
|
```python
|
||||||
CONTEXT_CHAR_LIMIT = 50000
|
CONTEXT_CHAR_LIMIT = 50000
|
||||||
@@ -156,7 +153,7 @@ def estimate_chars(messages):
|
|||||||
return len(json.dumps(messages, default=str, ensure_ascii=False))
|
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/`.
|
1. Writes the complete message history to `.transcripts/`.
|
||||||
2. Asks the model for a factual state summary.
|
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
|
## 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
|
```python
|
||||||
tool_result_budget
|
messages = self.tool_result_budget(messages)
|
||||||
→ snip_compact
|
messages = self.snip_compact(messages)
|
||||||
→ micro_compact
|
if self.estimate_chars(messages) > self.CONTEXT_CHAR_LIMIT:
|
||||||
→ compact_history (only above the 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:
|
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.
|
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.
|
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
|
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
|
## 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.
|
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
|
### Experiment 2: Persist a Large Result
|
||||||
|
|
||||||
|
|||||||
@@ -88,11 +88,11 @@ for block in ranked:
|
|||||||
|
|
||||||
## 第二步:snip_compact
|
## 第二步:snip_compact
|
||||||
|
|
||||||
消息数量超过 50 条后,`snip_compact` 先把完整历史写入 `.transcripts/`,再保留最初 3 条和最近 47 条。中间的标记会写明删去了多少条消息,以及完整记录保存在哪里。
|
消息数量超过 50 条后,`snip_compact` 先把完整历史写入 `.transcripts/`,再保留最初 3 条和最近 46 条。剩余一个位置用于归档标记,其中写明删去了多少条消息,以及完整记录保存在哪里。
|
||||||
|
|
||||||
```python
|
```python
|
||||||
head_end = 3
|
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]):
|
if self.has_tool_use(messages[head_end - 1]):
|
||||||
while (head_end < tail_start
|
while (head_end < tail_start
|
||||||
@@ -117,37 +117,34 @@ messages = [*messages[:head_end], marker, *messages[tail_start:]]
|
|||||||
|
|
||||||
## 第三步:micro_compact
|
## 第三步:micro_compact
|
||||||
|
|
||||||
`micro_compact` 会完整保留最近一次 assistant 响应之后新增的所有 `tool_result`,确保模型至少完整读取每条新结果一次。对于模型已经读取过的结果,它保留最近 3 条,并缩短其余超过 120 个字符的旧结果。已经转存的结果保留文件路径,其他结果只留下占位符:
|
前两步完成后,`prepare` 会估算剩余上下文的大小,只有超过 `CONTEXT_CHAR_LIMIT` 时才执行 `micro_compact`。对于模型已经读取过的结果,它保留最近 3 条,并逐条缩短更早且超过 120 个字符的结果,直到上下文接近阈值的 80%。旧结果被替换前会先完整落盘,因此每个占位都带有可恢复路径:
|
||||||
|
|
||||||

|

|
||||||
|
|
||||||
```python
|
```python
|
||||||
unseen = self.unseen_tool_result_positions(messages)
|
unseen = self.unseen_tool_result_positions(messages)
|
||||||
consumed = [entry for entry in results if entry[:2] not in unseen]
|
consumed = [entry for entry in results if entry[:2] not in unseen]
|
||||||
|
|
||||||
for _, _, block in consumed[:-self.KEEP_RECENT_RESULTS]:
|
for _, _, block in consumed[:-self.KEEP_RECENT_RESULTS]:
|
||||||
|
if self.estimate_chars(messages) <= target_chars:
|
||||||
|
break
|
||||||
content = str(block.get("content", ""))
|
content = str(block.get("content", ""))
|
||||||
if len(content) <= 120:
|
if len(content) <= 120:
|
||||||
continue
|
continue
|
||||||
saved_path = next(
|
saved_path = self.persisted_output_path(content)
|
||||||
(line.removeprefix("Full output: ") for line in content.splitlines()
|
if not saved_path:
|
||||||
if line.startswith("Full output: ")),
|
saved_path = self.save_output(block["tool_use_id"], content)
|
||||||
None,
|
block["content"] = f"[Earlier tool result saved at {saved_path}]"
|
||||||
)
|
|
||||||
block["content"] = (
|
|
||||||
f"[Earlier tool result saved at {saved_path}]"
|
|
||||||
if saved_path else "[Earlier tool result omitted.]"
|
|
||||||
)
|
|
||||||
```
|
```
|
||||||
|
|
||||||
未转存的旧结果只保留占位符。第一步保存过的完整结果仍能通过路径读取,不会在第三步丢失位置。
|
新结果通常会保持完整,直到模型读取一次。如果仅未读取的最新一批结果就足以撑爆上下文,`fit_tool_results` 会把其中最大的结果落盘,并保留 1,000 字符预览和完整路径,避免模型看到新结果前就先总结整段历史。
|
||||||
|
|
||||||
前三步都是确定性的结构和文本操作,不产生额外 API 调用。
|
前两步每轮都会执行,第三步只在上下文超限时执行。三步都是确定性、可恢复的结构和文本操作,不产生额外 API 调用。
|
||||||
|
|
||||||
|
|
||||||
## 第四步:compact_history
|
## 第四步:compact_history
|
||||||
|
|
||||||
前三步执行后,代码用 `estimate_chars(messages)` 计算当前消息的字符数:
|
`micro_compact` 和 `fit_tool_results` 执行后,代码会再次用 `estimate_chars(messages)` 估算上下文:
|
||||||
|
|
||||||
```python
|
```python
|
||||||
CONTEXT_CHAR_LIMIT = 50000
|
CONTEXT_CHAR_LIMIT = 50000
|
||||||
@@ -156,7 +153,7 @@ def estimate_chars(messages):
|
|||||||
return len(json.dumps(messages, default=str, ensure_ascii=False))
|
return len(json.dumps(messages, default=str, ensure_ascii=False))
|
||||||
```
|
```
|
||||||
|
|
||||||
字符数超过 `CONTEXT_CHAR_LIMIT` 时,`compact_history` 完成四件事:
|
字符数仍然超过 `CONTEXT_CHAR_LIMIT` 时,`compact_history` 完成四件事:
|
||||||
|
|
||||||
1. 将完整消息历史写入 `.transcripts/`。
|
1. 将完整消息历史写入 `.transcripts/`。
|
||||||
2. 请求模型生成只包含事实的状态摘要。
|
2. 请求模型生成只包含事实的状态摘要。
|
||||||
@@ -181,19 +178,24 @@ def compact_history(messages, active_request):
|
|||||||
|
|
||||||
## 为什么顺序固定
|
## 为什么顺序固定
|
||||||
|
|
||||||
四步管线的执行顺序是:
|
管线按以下顺序执行,并且只在必要时进入有损的摘要步骤:
|
||||||
|
|
||||||
```text
|
```python
|
||||||
tool_result_budget
|
messages = self.tool_result_budget(messages)
|
||||||
→ snip_compact
|
messages = self.snip_compact(messages)
|
||||||
→ micro_compact
|
if self.estimate_chars(messages) > self.CONTEXT_CHAR_LIMIT:
|
||||||
→ compact_history(超过阈值时)
|
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 请求。
|
1. 第一步和第二步每轮执行,第三步只在超限时执行,只有第四步会增加 API 请求。
|
||||||
2. `tool_result_budget` 必须早于 `micro_compact`。大结果先落盘,之后才允许旧结果变成占位符。
|
2. 每条被缩短的工具结果都保留 `.task_outputs/tool-results/` 内的可信路径;只有仍然超限时才进入模型生成的历史摘要。
|
||||||
|
|
||||||
顺序固定后,每一轮都从成本更低、信息更容易恢复的操作开始。
|
顺序固定后,每一轮都从成本更低、信息更容易恢复的操作开始。
|
||||||
|
|
||||||
@@ -243,7 +245,7 @@ def agent_loop(messages, active_request):
|
|||||||
raise
|
raise
|
||||||
```
|
```
|
||||||
|
|
||||||
每次调用模型前都会经过同一条管线。CLI 在追加 `query` 后调用 `agent_loop(history, query)`,所以压缩多少次都不会丢失本轮请求。前三步处理后仍超过阈值,或者 API 明确拒绝上下文时,代码才会请求模型生成摘要。
|
每次调用模型前都会经过同一条管线。CLI 在追加 `query` 后调用 `agent_loop(history, query)`,所以压缩多少次都不会丢失本轮请求。只有 `micro_compact` 处理后仍超过阈值,或者 API 明确拒绝上下文时,代码才会请求模型生成摘要。
|
||||||
|
|
||||||
|
|
||||||
## compact 工具
|
## compact 工具
|
||||||
@@ -308,7 +310,7 @@ python s08_context_compact/code.py
|
|||||||
比较它们的一级标题,并总结这些标题的命名规律。
|
比较它们的一级标题,并总结这些标题的命名规律。
|
||||||
```
|
```
|
||||||
|
|
||||||
任务会产生至少 5 条文件读取结果。每条新结果在模型首次读取前都会保持完整;后续轮次只保留最近 3 条已读取结果,更早且较长的结果会变成 `[Earlier tool result omitted.]`。已经转存的结果会保留保存路径。
|
任务会产生至少 5 条文件读取结果。新结果通常会完整保留到模型首次读取;如果未读取结果本身过大,则保留预览和恢复路径。后续轮次保留最近 3 条已读取结果,更早且较长的结果会变成 `[Earlier tool result saved at ...]` 引用。
|
||||||
|
|
||||||
### 实验二:大结果转存
|
### 实验二:大结果转存
|
||||||
|
|
||||||
|
|||||||
@@ -11,18 +11,24 @@ s08_context_compact.py - Context Compact
|
|||||||
v
|
v
|
||||||
+--------------------+
|
+--------------------+
|
||||||
| snip_compact | archive the old middle -> .transcripts/
|
| snip_compact | archive the old middle -> .transcripts/
|
||||||
+--------------------+
|
|
||||||
|
|
|
||||||
v
|
|
||||||
+--------------------+
|
|
||||||
| micro_compact | shorten old tool results
|
|
||||||
+--------------------+
|
+--------------------+
|
||||||
|
|
|
|
||||||
v
|
v
|
||||||
context over limit?
|
context over limit?
|
||||||
| no | yes
|
| no | yes
|
||||||
v v
|
| v
|
||||||
model call compact_history -> model call
|
| +--------------------+
|
||||||
|
| | 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:
|
Other entry points:
|
||||||
|
|
||||||
@@ -115,11 +121,14 @@ def run_edit(path: str, old_text: str, new_text: str) -> str:
|
|||||||
|
|
||||||
def run_glob(pattern: str) -> str:
|
def run_glob(pattern: str) -> str:
|
||||||
try:
|
try:
|
||||||
matches = [
|
matches = sorted({
|
||||||
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)
|
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:
|
except Exception as error:
|
||||||
return f"Error: {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"]}},
|
"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.",
|
{"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"]}},
|
"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"]}},
|
"input_schema": {"type": "object", "properties": {"pattern": {"type": "string"}}, "required": ["pattern"]}},
|
||||||
]
|
]
|
||||||
COMPACT_TOOL = {
|
COMPACT_TOOL = {
|
||||||
@@ -292,15 +301,53 @@ class ContextCompactor:
|
|||||||
transcript.write(json.dumps(message, default=str, ensure_ascii=False) + "\n")
|
transcript.write(json.dumps(message, default=str, ensure_ascii=False) + "\n")
|
||||||
return path
|
return path
|
||||||
|
|
||||||
def persist_large_output(self, tool_use_id: str, output: str) -> str:
|
def persisted_output_path(self, output: str) -> str | None:
|
||||||
if len(output) <= self.LARGE_RESULT_CHAR_LIMIT:
|
candidate = None
|
||||||
return output
|
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)
|
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"
|
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"
|
path = self.tool_results_dir / f"{safe_id}.txt"
|
||||||
if not path.exists():
|
path.write_text(output, encoding="utf-8")
|
||||||
path.write_text(output)
|
return path
|
||||||
return f"<persisted-output>\nFull output: {path}\nPreview:\n{output[:2000]}\n</persisted-output>"
|
|
||||||
|
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:
|
def tool_result_budget(self, messages: list, max_chars: int | None = None) -> list:
|
||||||
if not messages:
|
if not messages:
|
||||||
@@ -322,11 +369,21 @@ class ContextCompactor:
|
|||||||
total = sum(len(str(item.get("content", ""))) for item in blocks)
|
total = sum(len(str(item.get("content", ""))) for item in blocks)
|
||||||
return messages
|
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:
|
def snip_compact(self, messages: list, max_messages: int = 50) -> list:
|
||||||
if len(messages) <= max_messages:
|
if len(messages) <= max_messages:
|
||||||
return messages
|
return messages
|
||||||
head_end = 3
|
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]):
|
if self.has_tool_use(messages[head_end - 1]):
|
||||||
while head_end < tail_start and self.is_tool_result(messages[head_end]):
|
while head_end < tail_start and self.is_tool_result(messages[head_end]):
|
||||||
head_end += 1
|
head_end += 1
|
||||||
@@ -335,12 +392,16 @@ class ContextCompactor:
|
|||||||
tail_start -= 1
|
tail_start -= 1
|
||||||
if head_end >= tail_start:
|
if head_end >= tail_start:
|
||||||
return messages
|
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)
|
transcript_path = self.write_transcript(messages)
|
||||||
marker = {"role": "user", "content":
|
marker = {"role": "user", "content":
|
||||||
f"[{tail_start - head_end} messages archived at {transcript_path}]"}
|
f"[{tail_start - head_end} messages archived at {transcript_path}]"}
|
||||||
return [*messages[:head_end], marker, *messages[tail_start:]]
|
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 = [
|
results = [
|
||||||
(message_index, block_index, block)
|
(message_index, block_index, block)
|
||||||
for message_index, message in enumerate(messages)
|
for message_index, message in enumerate(messages)
|
||||||
@@ -351,18 +412,38 @@ class ContextCompactor:
|
|||||||
unseen = self.unseen_tool_result_positions(messages)
|
unseen = self.unseen_tool_result_positions(messages)
|
||||||
consumed = [entry for entry in results if entry[:2] not in unseen]
|
consumed = [entry for entry in results if entry[:2] not in unseen]
|
||||||
for _, _, block in consumed[:-self.KEEP_RECENT_RESULTS]:
|
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", ""))
|
content = str(block.get("content", ""))
|
||||||
if len(content) <= 120:
|
if len(content) <= 120:
|
||||||
continue
|
continue
|
||||||
saved_path = next(
|
saved_path = self.persisted_output_path(content)
|
||||||
(line.removeprefix("Full output: ") for line in content.splitlines()
|
if not saved_path:
|
||||||
if line.startswith("Full output: ")),
|
saved_path = str(self.save_output(
|
||||||
None,
|
block.get("tool_use_id", "unknown"), content))
|
||||||
)
|
block["content"] = f"[Earlier tool result saved at {saved_path}]"
|
||||||
block["content"] = (
|
return messages
|
||||||
f"[Earlier tool result saved at {saved_path}]"
|
|
||||||
if saved_path else "[Earlier tool result omitted.]"
|
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
|
return messages
|
||||||
|
|
||||||
def summary_input(self, messages: list) -> str:
|
def summary_input(self, messages: list) -> str:
|
||||||
@@ -419,10 +500,14 @@ class ContextCompactor:
|
|||||||
def prepare(self, messages: list, active_request: str) -> list:
|
def prepare(self, messages: list, active_request: str) -> list:
|
||||||
messages = self.tool_result_budget(messages)
|
messages = self.tool_result_budget(messages)
|
||||||
messages = self.snip_compact(messages)
|
messages = self.snip_compact(messages)
|
||||||
messages = self.micro_compact(messages)
|
|
||||||
if self.estimate_chars(messages) > self.CONTEXT_CHAR_LIMIT:
|
if self.estimate_chars(messages) > self.CONTEXT_CHAR_LIMIT:
|
||||||
print("[auto compact]")
|
target = int(self.CONTEXT_CHAR_LIMIT * 0.8)
|
||||||
messages = self.compact_history(messages, active_request)
|
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
|
return messages
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -16,7 +16,7 @@
|
|||||||
<!-- Trigger Condition -->
|
<!-- Trigger Condition -->
|
||||||
<rect x="20" y="54" width="680" height="44" rx="6" fill="#fef2f2" stroke="#fca5a5" stroke-width="1"/>
|
<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="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 1–3, estimate_chars(messages) > CONTEXT_CHAR_LIMIT.</text>
|
<text x="140" y="70" fill="#991b1b" font-size="11">After micro_compact, estimate_chars(messages) > CONTEXT_CHAR_LIMIT.</text>
|
||||||
<text x="140" y="86" fill="#991b1b" font-size="10">The current CONTEXT_CHAR_LIMIT is 50,000 characters.</text>
|
<text x="140" y="86" fill="#991b1b" font-size="10">The current CONTEXT_CHAR_LIMIT is 50,000 characters.</text>
|
||||||
|
|
||||||
<!-- Steps -->
|
<!-- Steps -->
|
||||||
|
|||||||
|
Before Width: | Height: | Size: 5.7 KiB After Width: | Height: | Size: 5.7 KiB |
@@ -16,7 +16,7 @@
|
|||||||
<!-- トリガー条件 -->
|
<!-- トリガー条件 -->
|
||||||
<rect x="20" y="54" width="680" height="44" rx="6" fill="#fef2f2" stroke="#fca5a5" stroke-width="1"/>
|
<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="35" y="70" fill="#991b1b" font-size="11" font-weight="600">トリガー条件</text>
|
||||||
<text x="115" y="70" fill="#991b1b" font-size="11">Step 1~3 の後、estimate_chars(messages) > CONTEXT_CHAR_LIMIT。</text>
|
<text x="115" y="70" fill="#991b1b" font-size="11">micro_compact の後、estimate_chars(messages) > CONTEXT_CHAR_LIMIT。</text>
|
||||||
<text x="115" y="86" fill="#991b1b" font-size="10">現在の CONTEXT_CHAR_LIMIT は 50,000 文字。</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 |
@@ -16,7 +16,7 @@
|
|||||||
<!-- 触发条件 -->
|
<!-- 触发条件 -->
|
||||||
<rect x="20" y="54" width="680" height="44" rx="6" fill="#fef2f2" stroke="#fca5a5" stroke-width="1"/>
|
<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="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) > CONTEXT_CHAR_LIMIT。</text>
|
<text x="105" y="70" fill="#991b1b" font-size="11">micro_compact 后,estimate_chars(messages) > CONTEXT_CHAR_LIMIT。</text>
|
||||||
<text x="105" y="86" fill="#991b1b" font-size="10">当前实现的 CONTEXT_CHAR_LIMIT 为 50,000 个字符。</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 |
@@ -45,9 +45,9 @@
|
|||||||
<rect x="170" y="82" width="200" height="252" rx="10" fill="#fffbeb" stroke="#d97706" stroke-width="2"/>
|
<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>
|
<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"/>
|
<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 1–2 Every Turn · 0 API</text>
|
||||||
|
|
||||||
<rect x="186" y="130" width="168" height="24" rx="4" fill="#fef3c7" stroke="#d97706" stroke-width="1"/>
|
<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>
|
<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>
|
<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"/>
|
<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)"/>
|
<line x1="270" y1="210" x2="270" y2="222" stroke="#555" stroke-width="1.2" marker-end="url(#arrow)"/>
|
||||||
|
|
||||||
<!-- ◇ Decision Diamond -->
|
<!-- ◇ Decision Diamond -->
|
||||||
<polygon points="270,226 300,244 270,262 240,244" fill="#f0f4ff" stroke="#ea580c" stroke-width="1.5"/>
|
<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 -->
|
<!-- No: right annotation -->
|
||||||
<text x="306" y="240" fill="#16a34a" font-size="9" font-weight="700">No → Pass</text>
|
<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>
|
<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"/>
|
<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"/>
|
<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"/>
|
<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>
|
<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 |
@@ -45,9 +45,9 @@
|
|||||||
<rect x="170" y="82" width="200" height="252" rx="10" fill="#fffbeb" stroke="#d97706" stroke-width="2"/>
|
<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>
|
<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"/>
|
<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 1–2 は毎ターン · 0 API</text>
|
||||||
|
|
||||||
<rect x="186" y="130" width="168" height="24" rx="4" fill="#fef3c7" stroke="#d97706" stroke-width="1"/>
|
<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>
|
<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>
|
<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"/>
|
<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)"/>
|
<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"/>
|
<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>
|
<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>
|
<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"/>
|
<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"/>
|
<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"/>
|
<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>
|
<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 |
@@ -45,9 +45,9 @@
|
|||||||
<rect x="170" y="82" width="200" height="252" rx="10" fill="#fffbeb" stroke="#d97706" stroke-width="2"/>
|
<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>
|
<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"/>
|
<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 1–2 每轮 · 0 API</text>
|
||||||
|
|
||||||
<rect x="186" y="130" width="168" height="24" rx="4" fill="#fef3c7" stroke="#d97706" stroke-width="1"/>
|
<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>
|
<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>
|
<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"/>
|
<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)"/>
|
<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"/>
|
<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>
|
<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>
|
<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"/>
|
<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"/>
|
<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_history,1 API</text>
|
<text x="94" y="458" fill="#334155" font-size="10">② 条件触发:Step 3 后 size 仍超阈值 → 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"/>
|
<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>
|
<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 |
@@ -39,7 +39,7 @@
|
|||||||
|
|
||||||
<!-- ===== Pre-processing pipeline title ===== -->
|
<!-- ===== Pre-processing pipeline title ===== -->
|
||||||
<rect x="20" y="146" width="720" height="24" rx="4" fill="#f1f5f9"/>
|
<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 -->
|
<!-- 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"/>
|
<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"/>
|
<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="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="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="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 ===== -->
|
<!-- ===== Auto-compact title ===== -->
|
||||||
<rect x="20" y="358" width="720" height="24" rx="4" fill="#f1f5f9"/>
|
<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 -->
|
<!-- Step 4: compact_history -->
|
||||||
<rect x="80" y="390" width="600" height="58" rx="7" fill="url(#auto)" stroke="#dc2626" stroke-width="2"/>
|
<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 |
@@ -39,7 +39,7 @@
|
|||||||
|
|
||||||
<!-- ===== 前処理パイプラインタイトル ===== -->
|
<!-- ===== 前処理パイプラインタイトル ===== -->
|
||||||
<rect x="20" y="146" width="720" height="24" rx="4" fill="#f1f5f9"/>
|
<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 -->
|
<!-- 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"/>
|
<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"/>
|
<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="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="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="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"/>
|
<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 -->
|
<!-- Step 4: compact_history -->
|
||||||
<rect x="80" y="390" width="600" height="58" rx="7" fill="url(#auto)" stroke="#dc2626" stroke-width="2"/>
|
<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 |
@@ -39,7 +39,7 @@
|
|||||||
|
|
||||||
<!-- ===== 预处理管线标题 ===== -->
|
<!-- ===== 预处理管线标题 ===== -->
|
||||||
<rect x="20" y="146" width="720" height="24" rx="4" fill="#f1f5f9"/>
|
<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 3,0 API)</text>
|
||||||
|
|
||||||
<!-- Step 1: tool_result_budget -->
|
<!-- 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"/>
|
<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"/>
|
<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="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="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="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"/>
|
<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 -->
|
<!-- Step 4: compact_history -->
|
||||||
<rect x="80" y="390" width="600" height="58" rx="7" fill="url(#auto)" stroke="#dc2626" stroke-width="2"/>
|
<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 |
@@ -41,18 +41,18 @@
|
|||||||
<rect x="400" y="130" width="290" height="10" rx="2" fill="#fef3c7"/>
|
<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>
|
<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"/>
|
<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"/>
|
<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"/>
|
<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="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 -->
|
<!-- How -->
|
||||||
<rect x="20" y="228" width="680" height="62" rx="6" fill="#f8fafc" stroke="#cbd5e1" stroke-width="1"/>
|
<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="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="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">Placeholder</text>
|
<text x="35" y="264" fill="#1e3a5f" font-size="11" font-weight="600">Recovery</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="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>
|
<text x="105" y="280" fill="#94a3b8" font-size="9">The message structure remains valid for the next loop iteration.</text>
|
||||||
</svg>
|
</svg>
|
||||||
|
|||||||
|
Before Width: | Height: | Size: 4.3 KiB After Width: | Height: | Size: 4.3 KiB |
@@ -41,18 +41,18 @@
|
|||||||
<rect x="400" y="130" width="290" height="10" rx="2" fill="#fef3c7"/>
|
<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>
|
<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"/>
|
<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"/>
|
<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"/>
|
<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="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"/>
|
<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="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="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="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="264" fill="#475569" font-size="10">短縮した各結果に .task_outputs/ 内の信頼できるパスを残す。</text>
|
||||||
<text x="125" y="280" fill="#94a3b8" font-size="9">メッセージ構造を保ったまま次のループへ進める。</text>
|
<text x="125" y="280" fill="#94a3b8" font-size="9">メッセージ構造を保ったまま次のループへ進める。</text>
|
||||||
</svg>
|
</svg>
|
||||||
|
|||||||
|
Before Width: | Height: | Size: 4.5 KiB After Width: | Height: | Size: 4.5 KiB |
@@ -11,7 +11,7 @@
|
|||||||
<rect width="720" height="300" fill="#fafbfc" rx="8"/>
|
<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="0" width="720" height="38" fill="url(#header)" rx="8"/>
|
||||||
<rect x="0" y="30" width="720" height="8" fill="url(#header)"/>
|
<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"/>
|
<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"/>
|
<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>
|
<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"/>
|
<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"/>
|
<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"/>
|
<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="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"/>
|
<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="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="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="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="264" fill="#475569" font-size="10">每条缩短结果都保留 .task_outputs/ 下的可信路径。</text>
|
||||||
<text x="95" y="280" fill="#94a3b8" font-size="9">消息结构保持不变,后续循环仍可继续处理。</text>
|
<text x="95" y="280" fill="#94a3b8" font-size="9">消息结构保持不变,后续循环仍可继续处理。</text>
|
||||||
</svg>
|
</svg>
|
||||||
|
|||||||
|
Before Width: | Height: | Size: 4.3 KiB After Width: | Height: | Size: 4.3 KiB |
@@ -579,12 +579,15 @@ def run_edit(path: str, old_text: str, new_text: str) -> str:
|
|||||||
|
|
||||||
def run_glob(pattern: str) -> str:
|
def run_glob(pattern: str) -> str:
|
||||||
try:
|
try:
|
||||||
matches = [
|
matches = sorted({
|
||||||
match
|
match
|
||||||
for match in glob.glob(pattern, root_dir=WORKDIR)
|
for match in glob.glob(pattern, root_dir=WORKDIR, recursive=True)
|
||||||
if (WORKDIR / match).resolve().is_relative_to(WORKDIR)
|
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:
|
except Exception as error:
|
||||||
return f"Error: {error}"
|
return f"Error: {error}"
|
||||||
|
|
||||||
@@ -597,7 +600,7 @@ TOOLS = [
|
|||||||
"input_schema": {"type": "object", "properties": {"path": {"type": "string"}, "content": {"type": "string"}}, "required": ["path", "content"]}},
|
"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.",
|
{"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"]}},
|
"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"]}},
|
"input_schema": {"type": "object", "properties": {"pattern": {"type": "string"}}, "required": ["pattern"]}},
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|||||||
@@ -325,12 +325,15 @@ def run_edit(path: str, old_text: str, new_text: str) -> str:
|
|||||||
|
|
||||||
def run_glob(pattern: str) -> str:
|
def run_glob(pattern: str) -> str:
|
||||||
try:
|
try:
|
||||||
matches = [
|
matches = sorted({
|
||||||
match
|
match
|
||||||
for match in glob.glob(pattern, root_dir=WORKDIR)
|
for match in glob.glob(pattern, root_dir=WORKDIR, recursive=True)
|
||||||
if (WORKDIR / match).resolve().is_relative_to(WORKDIR)
|
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:
|
except Exception as error:
|
||||||
return f"Error: {error}"
|
return f"Error: {error}"
|
||||||
|
|
||||||
@@ -392,7 +395,7 @@ TOOLS = [
|
|||||||
"input_schema": {"type": "object", "properties": {"path": {"type": "string"}, "content": {"type": "string"}}, "required": ["path", "content"]}},
|
"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.",
|
{"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"]}},
|
"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"]}},
|
"input_schema": {"type": "object", "properties": {"pattern": {"type": "string"}}, "required": ["pattern"]}},
|
||||||
{"name": "create_task", "description": "Create a task and return its runtime-generated ID.",
|
{"name": "create_task", "description": "Create a task and return its runtime-generated ID.",
|
||||||
"input_schema": {"type": "object", "properties": {"subject": {"type": "string"}, "description": {"type": "string"}}, "required": ["subject"], "additionalProperties": False}},
|
"input_schema": {"type": "object", "properties": {"subject": {"type": "string"}, "description": {"type": "string"}}, "required": ["subject"], "additionalProperties": False}},
|
||||||
|
|||||||
@@ -156,12 +156,15 @@ def run_edit(path: str, old_text: str, new_text: str) -> str:
|
|||||||
|
|
||||||
def run_glob(pattern: str) -> str:
|
def run_glob(pattern: str) -> str:
|
||||||
try:
|
try:
|
||||||
matches = [
|
matches = sorted({
|
||||||
match
|
match
|
||||||
for match in glob.glob(pattern, root_dir=WORKDIR)
|
for match in glob.glob(pattern, root_dir=WORKDIR, recursive=True)
|
||||||
if (WORKDIR / match).resolve().is_relative_to(WORKDIR)
|
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:
|
except Exception as error:
|
||||||
return f"Error: {error}"
|
return f"Error: {error}"
|
||||||
|
|
||||||
@@ -189,7 +192,7 @@ TOOLS = [
|
|||||||
"old_text": {"type": "string"},
|
"old_text": {"type": "string"},
|
||||||
"new_text": {"type": "string"}},
|
"new_text": {"type": "string"}},
|
||||||
"required": ["path", "old_text", "new_text"]}},
|
"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",
|
"input_schema": {"type": "object",
|
||||||
"properties": {"pattern": {"type": "string"}},
|
"properties": {"pattern": {"type": "string"}},
|
||||||
"required": ["pattern"]}},
|
"required": ["pattern"]}},
|
||||||
|
|||||||
@@ -106,12 +106,15 @@ def run_edit(path: str, old_text: str, new_text: str) -> str:
|
|||||||
|
|
||||||
def run_glob(pattern: str) -> str:
|
def run_glob(pattern: str) -> str:
|
||||||
try:
|
try:
|
||||||
matches = [
|
matches = sorted({
|
||||||
match
|
match
|
||||||
for match in glob.glob(pattern, root_dir=WORKDIR)
|
for match in glob.glob(pattern, root_dir=WORKDIR, recursive=True)
|
||||||
if (WORKDIR / match).resolve().is_relative_to(WORKDIR)
|
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:
|
except Exception as error:
|
||||||
return f"Error: {error}"
|
return f"Error: {error}"
|
||||||
|
|
||||||
@@ -137,7 +140,7 @@ TOOLS = [
|
|||||||
"old_text": {"type": "string"},
|
"old_text": {"type": "string"},
|
||||||
"new_text": {"type": "string"}},
|
"new_text": {"type": "string"}},
|
||||||
"required": ["path", "old_text", "new_text"]}},
|
"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",
|
"input_schema": {"type": "object",
|
||||||
"properties": {"pattern": {"type": "string"}},
|
"properties": {"pattern": {"type": "string"}},
|
||||||
"required": ["pattern"]}},
|
"required": ["pattern"]}},
|
||||||
|
|||||||
@@ -727,7 +727,10 @@ def run_glob(pattern: str, cwd: Path | None = None) -> str:
|
|||||||
for path in sorted(base.glob(pattern))
|
for path in sorted(base.glob(pattern))
|
||||||
if path.resolve().is_relative_to(base)
|
if path.resolve().is_relative_to(base)
|
||||||
]
|
]
|
||||||
return "\n".join(matches[:200]) or "No files found"
|
shown = matches[:200]
|
||||||
|
if len(matches) > 200:
|
||||||
|
shown.append("... (more matches omitted; narrow the pattern)")
|
||||||
|
return "\n".join(shown) or "No files found"
|
||||||
except Exception as exc:
|
except Exception as exc:
|
||||||
return f"Error: {exc}"
|
return f"Error: {exc}"
|
||||||
|
|
||||||
@@ -1519,7 +1522,7 @@ BASE_TOOLS = [
|
|||||||
"old_text": {"type": "string"},
|
"old_text": {"type": "string"},
|
||||||
"new_text": {"type": "string"}},
|
"new_text": {"type": "string"}},
|
||||||
"required": ["path", "old_text", "new_text"]}},
|
"required": ["path", "old_text", "new_text"]}},
|
||||||
{"name": "glob", "description": "Find files by glob pattern.",
|
{"name": "glob", "description": "Find files by glob pattern; ** matches recursively.",
|
||||||
"input_schema": {"type": "object",
|
"input_schema": {"type": "object",
|
||||||
"properties": {"pattern": {"type": "string"}},
|
"properties": {"pattern": {"type": "string"}},
|
||||||
"required": ["pattern"]}},
|
"required": ["pattern"]}},
|
||||||
|
|||||||
@@ -109,12 +109,15 @@ def run_edit(path: str, old_text: str, new_text: str) -> str:
|
|||||||
|
|
||||||
def run_glob(pattern: str) -> str:
|
def run_glob(pattern: str) -> str:
|
||||||
try:
|
try:
|
||||||
matches = [
|
matches = sorted({
|
||||||
match
|
match
|
||||||
for match in glob.glob(pattern, root_dir=WORKDIR)
|
for match in glob.glob(pattern, root_dir=WORKDIR, recursive=True)
|
||||||
if (WORKDIR / match).resolve().is_relative_to(WORKDIR.resolve())
|
if (WORKDIR / match).resolve().is_relative_to(WORKDIR.resolve())
|
||||||
]
|
})
|
||||||
return "\n".join(matches[:200]) 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 exc:
|
except Exception as exc:
|
||||||
return f"Error: {exc}"
|
return f"Error: {exc}"
|
||||||
|
|
||||||
@@ -140,7 +143,7 @@ BASE_TOOLS = [
|
|||||||
"old_text": {"type": "string"},
|
"old_text": {"type": "string"},
|
||||||
"new_text": {"type": "string"}},
|
"new_text": {"type": "string"}},
|
||||||
"required": ["path", "old_text", "new_text"]}},
|
"required": ["path", "old_text", "new_text"]}},
|
||||||
{"name": "glob", "description": "Find files by glob pattern.",
|
{"name": "glob", "description": "Find files by glob pattern; ** matches recursively.",
|
||||||
"input_schema": {"type": "object",
|
"input_schema": {"type": "object",
|
||||||
"properties": {"pattern": {"type": "string"}},
|
"properties": {"pattern": {"type": "string"}},
|
||||||
"required": ["pattern"]}},
|
"required": ["pattern"]}},
|
||||||
|
|||||||
@@ -154,6 +154,8 @@ LLM call の前に compaction pipeline を走らせる:
|
|||||||
tool_result_budget → snip_compact → micro_compact → compact_history
|
tool_result_budget → snip_compact → micro_compact → compact_history
|
||||||
```
|
```
|
||||||
|
|
||||||
|
`snip_compact` は中間メッセージを切る前に完全な履歴を保存する。`micro_compact` はコンテキストが上限を超えた場合にだけ実行し、古い既読結果を保存して復元パスへ置き換え、最新 3 件を完全に保ち、上限の約 80% で停止する。未読の新しい結果自体が大きすぎる場合、S15 は履歴要約を検討する前に preview と完全な出力へのパスを残す。
|
||||||
|
|
||||||
model call は recovery で包む:
|
model call は recovery で包む:
|
||||||
|
|
||||||
- 429: exponential backoff retry
|
- 429: exponential backoff retry
|
||||||
|
|||||||
@@ -154,6 +154,8 @@ Before the LLM call, S15 runs the compaction pipeline:
|
|||||||
tool_result_budget → snip_compact → micro_compact → compact_history
|
tool_result_budget → snip_compact → micro_compact → compact_history
|
||||||
```
|
```
|
||||||
|
|
||||||
|
`snip_compact` archives the complete history before trimming its middle. `micro_compact` runs only above the context limit: it saves older consumed results before replacing them with recovery paths, keeps the latest 3 complete, and stops near 80% of the limit. If a new unseen result is itself too large, S15 keeps a preview and the full-output path before considering history summarization.
|
||||||
|
|
||||||
The model call is wrapped with recovery:
|
The model call is wrapped with recovery:
|
||||||
|
|
||||||
- 429: exponential backoff retry
|
- 429: exponential backoff retry
|
||||||
|
|||||||
@@ -154,6 +154,8 @@ LLM 前先跑压缩管线:
|
|||||||
tool_result_budget → snip_compact → micro_compact → compact_history
|
tool_result_budget → snip_compact → micro_compact → compact_history
|
||||||
```
|
```
|
||||||
|
|
||||||
|
`snip_compact` 会先归档完整历史,再裁掉中段消息。`micro_compact` 只在上下文超限时运行:它先保存较早且已读取的结果,再用恢复路径替换;最近 3 条保持完整,并在接近阈值 80% 时停止。如果未读取的新结果本身过大,S15 会先保留预览和完整输出路径,再考虑总结历史。
|
||||||
|
|
||||||
调用模型时再包一层恢复:
|
调用模型时再包一层恢复:
|
||||||
|
|
||||||
- 429:指数退避重试
|
- 429:指数退避重试
|
||||||
|
|||||||
@@ -972,11 +972,15 @@ def run_glob(pattern: str, cwd: Path | None = None) -> str:
|
|||||||
import glob as g
|
import glob as g
|
||||||
try:
|
try:
|
||||||
base = (cwd or WORKDIR).resolve()
|
base = (cwd or WORKDIR).resolve()
|
||||||
results = []
|
matches = sorted({
|
||||||
for match in g.glob(pattern, root_dir=base):
|
match for match in g.glob(
|
||||||
if (base / match).resolve().is_relative_to(base):
|
pattern, root_dir=base, recursive=True)
|
||||||
results.append(match)
|
if (base / match).resolve().is_relative_to(base)
|
||||||
return "\n".join(results) if results 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 e:
|
except Exception as e:
|
||||||
return f"Error: {e}"
|
return f"Error: {e}"
|
||||||
|
|
||||||
@@ -1487,7 +1491,7 @@ def spawn_teammate_thread(name: str, role: str, prompt: str,
|
|||||||
"old_text": {"type": "string"},
|
"old_text": {"type": "string"},
|
||||||
"new_text": {"type": "string"}},
|
"new_text": {"type": "string"}},
|
||||||
"required": ["path", "old_text", "new_text"]}},
|
"required": ["path", "old_text", "new_text"]}},
|
||||||
{"name": "glob", "description": "Find files by glob pattern.",
|
{"name": "glob", "description": "Find files by glob pattern; ** matches recursively.",
|
||||||
"input_schema": {"type": "object",
|
"input_schema": {"type": "object",
|
||||||
"properties": {
|
"properties": {
|
||||||
"pattern": {"type": "string"}},
|
"pattern": {"type": "string"}},
|
||||||
@@ -1849,7 +1853,7 @@ SUB_TOOLS = [
|
|||||||
"old_text": {"type": "string"},
|
"old_text": {"type": "string"},
|
||||||
"new_text": {"type": "string"}},
|
"new_text": {"type": "string"}},
|
||||||
"required": ["path", "old_text", "new_text"]}},
|
"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",
|
"input_schema": {"type": "object",
|
||||||
"properties": {"pattern": {"type": "string"}},
|
"properties": {"pattern": {"type": "string"}},
|
||||||
"required": ["pattern"]}},
|
"required": ["pattern"]}},
|
||||||
@@ -1971,15 +1975,55 @@ def unseen_tool_result_positions(messages: list) -> set[tuple[int, int]]:
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def persisted_output_path(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(TOOL_RESULTS_DIR.resolve())
|
||||||
|
or not path.is_file()):
|
||||||
|
return None
|
||||||
|
return str(path)
|
||||||
|
|
||||||
|
|
||||||
|
def save_output(tool_use_id: str, output: str) -> Path:
|
||||||
|
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 = TOOL_RESULTS_DIR / f"{safe_id}.txt"
|
||||||
|
path.write_text(output, encoding="utf-8")
|
||||||
|
return path
|
||||||
|
|
||||||
|
|
||||||
|
def persisted_preview(tool_use_id: str, output: str,
|
||||||
|
preview_chars: int = 2000) -> str:
|
||||||
|
saved_path = 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 = 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(tool_use_id: str, output: str) -> str:
|
def persist_large_output(tool_use_id: str, output: str) -> str:
|
||||||
if len(output) <= PERSIST_THRESHOLD:
|
if len(output) <= PERSIST_THRESHOLD:
|
||||||
return output
|
return output
|
||||||
TOOL_RESULTS_DIR.mkdir(parents=True, exist_ok=True)
|
return persisted_preview(tool_use_id, output)
|
||||||
path = TOOL_RESULTS_DIR / f"{tool_use_id}.txt"
|
|
||||||
if not path.exists():
|
|
||||||
path.write_text(output)
|
|
||||||
return (f"<persisted-output>\nFull output: {path}\n"
|
|
||||||
f"Preview:\n{output[:2000]}\n</persisted-output>")
|
|
||||||
|
|
||||||
|
|
||||||
def tool_result_budget(messages: list, max_bytes: int = 200_000) -> list:
|
def tool_result_budget(messages: list, max_bytes: int = 200_000) -> list:
|
||||||
@@ -2006,10 +2050,22 @@ def tool_result_budget(messages: list, max_bytes: int = 200_000) -> list:
|
|||||||
return messages
|
return messages
|
||||||
|
|
||||||
|
|
||||||
|
def is_archive_marker(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(TRANSCRIPT_DIR.resolve())
|
||||||
|
and path.is_file())
|
||||||
|
|
||||||
|
|
||||||
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
|
||||||
head_end, tail_start = 3, len(messages) - (max_messages - 3)
|
head_end = 3
|
||||||
|
tail_start = len(messages) - (max_messages - head_end - 1)
|
||||||
if head_end > 0 and message_has_tool_use(messages[head_end - 1]):
|
if head_end > 0 and message_has_tool_use(messages[head_end - 1]):
|
||||||
while head_end < len(messages) and is_tool_result_message(messages[head_end]):
|
while head_end < len(messages) and is_tool_result_message(messages[head_end]):
|
||||||
head_end += 1
|
head_end += 1
|
||||||
@@ -2019,26 +2075,55 @@ def snip_compact(messages: list, max_messages: int = 50) -> list:
|
|||||||
tail_start -= 1
|
tail_start -= 1
|
||||||
if head_end >= tail_start:
|
if head_end >= tail_start:
|
||||||
return messages
|
return messages
|
||||||
|
middle = messages[head_end:tail_start]
|
||||||
|
if len(middle) == 1 and is_archive_marker(middle[0]):
|
||||||
|
return messages
|
||||||
snipped = tail_start - head_end
|
snipped = tail_start - head_end
|
||||||
|
transcript = write_transcript(messages)
|
||||||
return (messages[:head_end]
|
return (messages[:head_end]
|
||||||
+ [{"role": "user", "content": f"[snipped {snipped} messages]"}]
|
+ [{"role": "user", "content":
|
||||||
|
f"[{snipped} messages archived at {transcript}]"}]
|
||||||
+ messages[tail_start:])
|
+ messages[tail_start:])
|
||||||
|
|
||||||
|
|
||||||
def micro_compact(messages: list) -> list:
|
def micro_compact(messages: list, target_chars: int | None = None) -> list:
|
||||||
tool_results = collect_tool_results(messages)
|
tool_results = collect_tool_results(messages)
|
||||||
unseen = unseen_tool_result_positions(messages)
|
unseen = unseen_tool_result_positions(messages)
|
||||||
consumed = [entry for entry in tool_results if entry[:2] not in unseen]
|
consumed = [entry for entry in tool_results if entry[:2] not in unseen]
|
||||||
for _, _, block in consumed[:-KEEP_RECENT_TOOL_RESULTS]:
|
for _, _, block in consumed[:-KEEP_RECENT_TOOL_RESULTS]:
|
||||||
if len(str(block.get("content", ""))) > 120:
|
if target_chars is not None and estimate_size(messages) <= target_chars:
|
||||||
block["content"] = "[Earlier tool result compacted. Re-run if needed.]"
|
break
|
||||||
|
content = str(block.get("content", ""))
|
||||||
|
if len(content) <= 120:
|
||||||
|
continue
|
||||||
|
saved_path = persisted_output_path(content)
|
||||||
|
if not saved_path:
|
||||||
|
saved_path = str(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(messages: list, target_chars: int) -> list:
|
||||||
|
results = [block for _, _, block in collect_tool_results(messages)]
|
||||||
|
for block in sorted(
|
||||||
|
results,
|
||||||
|
key=lambda item: len(str(item.get("content", ""))),
|
||||||
|
reverse=True):
|
||||||
|
if estimate_size(messages) <= target_chars:
|
||||||
|
break
|
||||||
|
output = str(block.get("content", ""))
|
||||||
|
replacement = persisted_preview(
|
||||||
|
block.get("tool_use_id", "unknown"), output, preview_chars=1000)
|
||||||
|
if len(replacement) < len(output):
|
||||||
|
block["content"] = replacement
|
||||||
return messages
|
return messages
|
||||||
|
|
||||||
|
|
||||||
def write_transcript(messages: list) -> Path:
|
def write_transcript(messages: list) -> Path:
|
||||||
TRANSCRIPT_DIR.mkdir(parents=True, exist_ok=True)
|
TRANSCRIPT_DIR.mkdir(parents=True, exist_ok=True)
|
||||||
path = TRANSCRIPT_DIR / f"transcript_{int(time.time())}.jsonl"
|
path = TRANSCRIPT_DIR / f"transcript_{time.time_ns()}.jsonl"
|
||||||
with path.open("w") as f:
|
with path.open("x") as f:
|
||||||
for msg in messages:
|
for msg in messages:
|
||||||
f.write(json.dumps(msg, default=str) + "\n")
|
f.write(json.dumps(msg, default=str) + "\n")
|
||||||
return path
|
return path
|
||||||
@@ -2776,7 +2861,7 @@ BUILTIN_TOOLS = [
|
|||||||
"old_text": {"type": "string"},
|
"old_text": {"type": "string"},
|
||||||
"new_text": {"type": "string"}},
|
"new_text": {"type": "string"}},
|
||||||
"required": ["path", "old_text", "new_text"]}},
|
"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",
|
"input_schema": {"type": "object",
|
||||||
"properties": {"pattern": {"type": "string"}},
|
"properties": {"pattern": {"type": "string"}},
|
||||||
"required": ["pattern"]}},
|
"required": ["pattern"]}},
|
||||||
@@ -2966,7 +3051,11 @@ def prepare_context(messages: list, active_request: str) -> list:
|
|||||||
# Every LLM turn enters through the same context budget pipeline.
|
# Every LLM turn enters through the same context budget pipeline.
|
||||||
messages[:] = tool_result_budget(messages)
|
messages[:] = tool_result_budget(messages)
|
||||||
messages[:] = snip_compact(messages)
|
messages[:] = snip_compact(messages)
|
||||||
messages[:] = micro_compact(messages)
|
if estimate_size(messages) > CONTEXT_LIMIT:
|
||||||
|
target = int(CONTEXT_LIMIT * 0.8)
|
||||||
|
messages[:] = micro_compact(messages, target)
|
||||||
|
if estimate_size(messages) > CONTEXT_LIMIT:
|
||||||
|
messages[:] = fit_tool_results(messages, target)
|
||||||
if estimate_size(messages) > CONTEXT_LIMIT:
|
if estimate_size(messages) > CONTEXT_LIMIT:
|
||||||
messages[:] = compact_history(messages, active_request)
|
messages[:] = compact_history(messages, active_request)
|
||||||
return messages
|
return messages
|
||||||
|
|||||||
@@ -515,7 +515,7 @@ TOOLS = [
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "glob",
|
"name": "glob",
|
||||||
"description": "Find files matching a glob pattern.",
|
"description": "Find files matching a glob pattern; ** matches recursively.",
|
||||||
"input_schema": {
|
"input_schema": {
|
||||||
"type": "object",
|
"type": "object",
|
||||||
"properties": {"pattern": {"type": "string"}},
|
"properties": {"pattern": {"type": "string"}},
|
||||||
@@ -797,12 +797,16 @@ class AgentSession:
|
|||||||
return f"Edited {path.relative_to(self.workdir)}"
|
return f"Edited {path.relative_to(self.workdir)}"
|
||||||
|
|
||||||
if name == "glob":
|
if name == "glob":
|
||||||
matches = [
|
matches = sorted({
|
||||||
match
|
match
|
||||||
for match in glob.glob(str(arguments["pattern"]), root_dir=self.workdir)
|
for match in glob.glob(
|
||||||
|
str(arguments["pattern"]), root_dir=self.workdir, recursive=True)
|
||||||
if (self.workdir / match).resolve().is_relative_to(self.workdir)
|
if (self.workdir / match).resolve().is_relative_to(self.workdir)
|
||||||
]
|
})
|
||||||
return "\n".join(matches[:200]) 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)"
|
||||||
|
|
||||||
raise GoalError(f"unknown tool '{name}'")
|
raise GoalError(f"unknown tool '{name}'")
|
||||||
|
|
||||||
|
|||||||
@@ -30,6 +30,8 @@ LESSONS = tuple(
|
|||||||
)
|
)
|
||||||
)
|
)
|
||||||
INTEGRATED_LESSON = ROOT / "s15_integrated_harness" / "code.py"
|
INTEGRATED_LESSON = ROOT / "s15_integrated_harness" / "code.py"
|
||||||
|
GOAL_LESSON = ROOT / "s17_goal_loop" / "code.py"
|
||||||
|
GLOB_LESSONS = (*LESSONS[1:], INTEGRATED_LESSON, GOAL_LESSON)
|
||||||
|
|
||||||
|
|
||||||
class FakeMessagesApi:
|
class FakeMessagesApi:
|
||||||
@@ -137,6 +139,42 @@ def bash_tool_call():
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def run_glob_tool(lesson, workdir: Path, pattern: str) -> str:
|
||||||
|
if hasattr(lesson, "run_glob"):
|
||||||
|
return lesson.run_glob(pattern)
|
||||||
|
session = object.__new__(lesson.AgentSession)
|
||||||
|
session.workdir = workdir.resolve()
|
||||||
|
return session._run_tool("glob", {"pattern": pattern})
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize("lesson_path", GLOB_LESSONS,
|
||||||
|
ids=lambda path: path.parent.name)
|
||||||
|
def test_glob_double_star_matches_files_at_any_depth(
|
||||||
|
tmp_path: Path, lesson_path: Path):
|
||||||
|
(tmp_path / "root.py").write_text("")
|
||||||
|
(tmp_path / "one" / "two").mkdir(parents=True)
|
||||||
|
(tmp_path / "one" / "one.py").write_text("")
|
||||||
|
(tmp_path / "one" / "two" / "deep.py").write_text("")
|
||||||
|
lesson = load_lesson(tmp_path, lesson_path)
|
||||||
|
|
||||||
|
matches = set(run_glob_tool(lesson, tmp_path, "**/*.py").splitlines())
|
||||||
|
|
||||||
|
assert matches == {"root.py", "one/one.py", "one/two/deep.py"}
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize("lesson_path", GLOB_LESSONS,
|
||||||
|
ids=lambda path: path.parent.name)
|
||||||
|
def test_glob_caps_large_result_sets(tmp_path: Path, lesson_path: Path):
|
||||||
|
for index in range(205):
|
||||||
|
(tmp_path / f"file-{index:03}.txt").write_text("")
|
||||||
|
lesson = load_lesson(tmp_path, lesson_path)
|
||||||
|
|
||||||
|
lines = run_glob_tool(lesson, tmp_path, "*.txt").splitlines()
|
||||||
|
|
||||||
|
assert len(lines) == 201
|
||||||
|
assert lines[-1] == "... (more matches omitted; narrow the pattern)"
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.parametrize("lesson_path", LESSONS, ids=lambda path: path.parent.name)
|
@pytest.mark.parametrize("lesson_path", LESSONS, ids=lambda path: path.parent.name)
|
||||||
@pytest.mark.parametrize("content", ([], None), ids=("empty-content", "empty-text"))
|
@pytest.mark.parametrize("content", ([], None), ids=("empty-content", "empty-text"))
|
||||||
def test_parent_loop_does_not_append_an_empty_tool_result_turn(
|
def test_parent_loop_does_not_append_an_empty_tool_result_turn(
|
||||||
|
|||||||
@@ -132,7 +132,104 @@ def compaction_api(module):
|
|||||||
return getattr(module, "COMPACTOR", module)
|
return getattr(module, "COMPACTOR", module)
|
||||||
|
|
||||||
|
|
||||||
|
def prepare_context(module, messages, active_request="continue"):
|
||||||
|
api = compaction_api(module)
|
||||||
|
if hasattr(api, "prepare"):
|
||||||
|
return api.prepare(messages, active_request)
|
||||||
|
return module.prepare_context(messages, active_request)
|
||||||
|
|
||||||
|
|
||||||
class CompactionToolPairTests(unittest.TestCase):
|
class CompactionToolPairTests(unittest.TestCase):
|
||||||
|
def test_prepare_preserves_consumed_results_below_pressure_limit(self):
|
||||||
|
for name, path in MODULES.items():
|
||||||
|
with self.subTest(name=name), tempfile.TemporaryDirectory() as tmp:
|
||||||
|
messages = []
|
||||||
|
expected = {}
|
||||||
|
for index in range(5):
|
||||||
|
tool_id = f"tool-{index}"
|
||||||
|
output = f"{tool_id}: " + "x" * 160
|
||||||
|
expected[tool_id] = output
|
||||||
|
messages.extend([
|
||||||
|
tool_use_message(tool_id),
|
||||||
|
{"role": "user", "content": [{
|
||||||
|
"type": "tool_result",
|
||||||
|
"tool_use_id": tool_id,
|
||||||
|
"content": output,
|
||||||
|
}]},
|
||||||
|
])
|
||||||
|
messages.append(assistant_text())
|
||||||
|
module = load_module(f"{name}_below_limit", path, Path(tmp))
|
||||||
|
|
||||||
|
prepared = prepare_context(module, messages)
|
||||||
|
actual = {
|
||||||
|
block["tool_use_id"]: block["content"]
|
||||||
|
for message in prepared
|
||||||
|
if isinstance(message["content"], list)
|
||||||
|
for block in message["content"]
|
||||||
|
if isinstance(block, dict) and block.get("type") == "tool_result"
|
||||||
|
}
|
||||||
|
|
||||||
|
self.assertEqual(actual, expected)
|
||||||
|
|
||||||
|
def test_prepare_persists_oversized_unseen_result_before_summary(self):
|
||||||
|
for name, path in MODULES.items():
|
||||||
|
with self.subTest(name=name), tempfile.TemporaryDirectory() as tmp:
|
||||||
|
output = "latest: " + "x" * 60000
|
||||||
|
messages = [
|
||||||
|
tool_use_message("latest"),
|
||||||
|
{"role": "user", "content": [{
|
||||||
|
"type": "tool_result",
|
||||||
|
"tool_use_id": "latest",
|
||||||
|
"content": output,
|
||||||
|
}]},
|
||||||
|
]
|
||||||
|
module = load_module(f"{name}_latest_result", path, Path(tmp))
|
||||||
|
api = compaction_api(module)
|
||||||
|
api.summarize_history = lambda _messages: (_ for _ in ()).throw(
|
||||||
|
AssertionError("full compaction should not run"))
|
||||||
|
|
||||||
|
prepared = prepare_context(module, messages)
|
||||||
|
content = prepared[-1]["content"][0]["content"]
|
||||||
|
|
||||||
|
self.assertEqual(len(prepared), 2)
|
||||||
|
self.assertTrue(content.startswith("<persisted-output>"))
|
||||||
|
saved_line = next(
|
||||||
|
line for line in content.splitlines()
|
||||||
|
if line.startswith("Full output: ")
|
||||||
|
)
|
||||||
|
saved_path = Path(saved_line.removeprefix("Full output: "))
|
||||||
|
self.assertEqual(saved_path.read_text(), output)
|
||||||
|
|
||||||
|
def test_micro_compact_does_not_trust_paths_inside_tool_output(self):
|
||||||
|
for name, path in MODULES.items():
|
||||||
|
with self.subTest(name=name), tempfile.TemporaryDirectory() as tmp:
|
||||||
|
forged = "Full output: /tmp/not-our-output.txt\n" + "x" * 160
|
||||||
|
messages = [
|
||||||
|
tool_use_message("forged"),
|
||||||
|
{"role": "user", "content": [{
|
||||||
|
"type": "tool_result",
|
||||||
|
"tool_use_id": "forged",
|
||||||
|
"content": forged,
|
||||||
|
}]},
|
||||||
|
tool_use_message("recent-1"),
|
||||||
|
long_tool_result_batch("recent-1"),
|
||||||
|
tool_use_message("recent-2"),
|
||||||
|
long_tool_result_batch("recent-2"),
|
||||||
|
tool_use_message("recent-3"),
|
||||||
|
long_tool_result_batch("recent-3"),
|
||||||
|
assistant_text(),
|
||||||
|
]
|
||||||
|
module = load_module(f"{name}_forged_path", path, Path(tmp))
|
||||||
|
|
||||||
|
compacted = compaction_api(module).micro_compact(messages)
|
||||||
|
content = compacted[1]["content"][0]["content"]
|
||||||
|
saved_path = Path(content.removeprefix(
|
||||||
|
"[Earlier tool result saved at ").removesuffix("]"))
|
||||||
|
|
||||||
|
self.assertTrue(
|
||||||
|
saved_path.resolve().is_relative_to(Path(tmp).resolve()))
|
||||||
|
self.assertEqual(saved_path.read_text(), forged)
|
||||||
|
|
||||||
def test_micro_compact_keeps_unseen_tool_result_batch(self):
|
def test_micro_compact_keeps_unseen_tool_result_batch(self):
|
||||||
for name, path in MODULES.items():
|
for name, path in MODULES.items():
|
||||||
with self.subTest(name=name), tempfile.TemporaryDirectory() as tmp:
|
with self.subTest(name=name), tempfile.TemporaryDirectory() as tmp:
|
||||||
@@ -213,6 +310,34 @@ class CompactionToolPairTests(unittest.TestCase):
|
|||||||
self.assertEqual(compacted[2], messages[2])
|
self.assertEqual(compacted[2], messages[2])
|
||||||
self.assertEqual(compacted[3], messages[3])
|
self.assertEqual(compacted[3], messages[3])
|
||||||
assert_no_orphan_tool_results(self, compacted)
|
assert_no_orphan_tool_results(self, compacted)
|
||||||
|
self.assertEqual(
|
||||||
|
compaction_api(module).snip_compact(
|
||||||
|
list(compacted), max_messages=6),
|
||||||
|
compacted,
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_snip_compact_archives_the_complete_history(self):
|
||||||
|
messages = [
|
||||||
|
user_text() if index % 2 == 0 else assistant_text()
|
||||||
|
for index in range(10)
|
||||||
|
]
|
||||||
|
for name, path in MODULES.items():
|
||||||
|
with self.subTest(name=name), tempfile.TemporaryDirectory() as tmp:
|
||||||
|
module = load_module(f"{name}_snip_archive", path, Path(tmp))
|
||||||
|
|
||||||
|
compacted = compaction_api(module).snip_compact(
|
||||||
|
list(messages), max_messages=6)
|
||||||
|
marker = compacted[3]["content"]
|
||||||
|
saved_path = Path(marker.rsplit(" at ", 1)[-1].removesuffix("]"))
|
||||||
|
|
||||||
|
self.assertEqual(len(compacted), 6)
|
||||||
|
self.assertTrue(saved_path.is_file())
|
||||||
|
self.assertEqual(len(saved_path.read_text().splitlines()), 10)
|
||||||
|
self.assertEqual(
|
||||||
|
compaction_api(module).snip_compact(
|
||||||
|
list(compacted), max_messages=6),
|
||||||
|
compacted,
|
||||||
|
)
|
||||||
|
|
||||||
def test_snip_compact_keeps_tail_tool_pair(self):
|
def test_snip_compact_keeps_tail_tool_pair(self):
|
||||||
messages = [
|
messages = [
|
||||||
|
|||||||
138
tests/test_s08_context_compact.py
Normal file
@@ -0,0 +1,138 @@
|
|||||||
|
import runpy
|
||||||
|
import sys
|
||||||
|
import types
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
|
||||||
|
ROOT = Path(__file__).resolve().parents[1]
|
||||||
|
LESSON = ROOT / "s08_context_compact" / "code.py"
|
||||||
|
|
||||||
|
|
||||||
|
def load_lesson(monkeypatch, workdir: Path):
|
||||||
|
fake_anthropic = types.ModuleType("anthropic")
|
||||||
|
fake_dotenv = types.ModuleType("dotenv")
|
||||||
|
|
||||||
|
class FakeAnthropic:
|
||||||
|
def __init__(self, *args, **kwargs):
|
||||||
|
self.messages = types.SimpleNamespace(create=None)
|
||||||
|
|
||||||
|
fake_anthropic.Anthropic = FakeAnthropic
|
||||||
|
fake_dotenv.load_dotenv = lambda override=True: None
|
||||||
|
monkeypatch.setitem(sys.modules, "anthropic", fake_anthropic)
|
||||||
|
monkeypatch.setitem(sys.modules, "dotenv", fake_dotenv)
|
||||||
|
monkeypatch.setenv("MODEL_ID", "test-model")
|
||||||
|
monkeypatch.setenv("ANTHROPIC_API_KEY", "test-key")
|
||||||
|
monkeypatch.chdir(workdir)
|
||||||
|
return runpy.run_path(str(LESSON))
|
||||||
|
|
||||||
|
|
||||||
|
def test_glob_double_star_matches_files_at_any_depth(tmp_path, monkeypatch):
|
||||||
|
(tmp_path / "root.py").write_text("")
|
||||||
|
(tmp_path / "one").mkdir()
|
||||||
|
(tmp_path / "one" / "one.py").write_text("")
|
||||||
|
(tmp_path / "one" / "two").mkdir()
|
||||||
|
(tmp_path / "one" / "two" / "deep.py").write_text("")
|
||||||
|
lesson = load_lesson(monkeypatch, tmp_path)
|
||||||
|
|
||||||
|
matches = set(lesson["run_glob"]("**/*.py").splitlines())
|
||||||
|
|
||||||
|
assert matches == {"root.py", "one/one.py", "one/two/deep.py"}
|
||||||
|
|
||||||
|
|
||||||
|
def test_prepare_preserves_tool_results_while_context_is_within_limit(
|
||||||
|
tmp_path, monkeypatch):
|
||||||
|
lesson = load_lesson(monkeypatch, tmp_path)
|
||||||
|
messages = []
|
||||||
|
expected_results = []
|
||||||
|
for index in range(5):
|
||||||
|
tool_id = f"tool-{index}"
|
||||||
|
result = f"result-{index}:" + "x" * 200
|
||||||
|
expected_results.append(result)
|
||||||
|
messages.extend([
|
||||||
|
{"role": "assistant", "content": [
|
||||||
|
{"type": "tool_use", "id": tool_id, "name": "bash", "input": {}}
|
||||||
|
]},
|
||||||
|
{"role": "user", "content": [
|
||||||
|
{"type": "tool_result", "tool_use_id": tool_id, "content": result}
|
||||||
|
]},
|
||||||
|
])
|
||||||
|
messages.append({"role": "assistant", "content": [
|
||||||
|
{"type": "text", "text": "continue"}
|
||||||
|
]})
|
||||||
|
|
||||||
|
prepared = lesson["COMPACTOR"].prepare(messages, "inspect the repository")
|
||||||
|
actual_results = [
|
||||||
|
block["content"]
|
||||||
|
for message in prepared
|
||||||
|
if message["role"] == "user"
|
||||||
|
for block in message["content"]
|
||||||
|
if block["type"] == "tool_result"
|
||||||
|
]
|
||||||
|
|
||||||
|
assert actual_results == expected_results
|
||||||
|
|
||||||
|
|
||||||
|
def test_prepare_micro_compacts_tool_results_after_context_exceeds_limit(
|
||||||
|
tmp_path, monkeypatch):
|
||||||
|
lesson = load_lesson(monkeypatch, tmp_path)
|
||||||
|
messages = []
|
||||||
|
for index in range(5):
|
||||||
|
tool_id = f"tool-{index}"
|
||||||
|
messages.extend([
|
||||||
|
{"role": "assistant", "content": [
|
||||||
|
{"type": "tool_use", "id": tool_id, "name": "bash", "input": {}}
|
||||||
|
]},
|
||||||
|
{"role": "user", "content": [
|
||||||
|
{"type": "tool_result", "tool_use_id": tool_id,
|
||||||
|
"content": f"result-{index}:" + "x" * 1000}
|
||||||
|
]},
|
||||||
|
])
|
||||||
|
messages.append({"role": "assistant", "content": [
|
||||||
|
{"type": "text", "text": "continue"}
|
||||||
|
]})
|
||||||
|
compactor = lesson["COMPACTOR"]
|
||||||
|
compactor.CONTEXT_CHAR_LIMIT = 4500
|
||||||
|
|
||||||
|
prepared = compactor.prepare(messages, "inspect the repository")
|
||||||
|
actual_results = [
|
||||||
|
block["content"]
|
||||||
|
for message in prepared
|
||||||
|
if message["role"] == "user"
|
||||||
|
for block in message["content"]
|
||||||
|
if block["type"] == "tool_result"
|
||||||
|
]
|
||||||
|
|
||||||
|
assert all(result.startswith("[Earlier tool result saved at ")
|
||||||
|
for result in actual_results[:2])
|
||||||
|
for index, result in enumerate(actual_results[:2]):
|
||||||
|
saved_path = Path(result.removeprefix(
|
||||||
|
"[Earlier tool result saved at ").removesuffix("]"))
|
||||||
|
assert saved_path.read_text() == f"result-{index}:" + "x" * 1000
|
||||||
|
assert all(result.startswith(f"result-{index}:")
|
||||||
|
for index, result in enumerate(actual_results[2:], start=2))
|
||||||
|
|
||||||
|
|
||||||
|
def test_prepare_persists_oversized_unseen_result_before_full_compact(
|
||||||
|
tmp_path, monkeypatch):
|
||||||
|
lesson = load_lesson(monkeypatch, tmp_path)
|
||||||
|
output = "latest-result:" + "x" * 60000
|
||||||
|
messages = [
|
||||||
|
{"role": "assistant", "content": [
|
||||||
|
{"type": "tool_use", "id": "latest", "name": "read_file", "input": {}}
|
||||||
|
]},
|
||||||
|
{"role": "user", "content": [
|
||||||
|
{"type": "tool_result", "tool_use_id": "latest", "content": output}
|
||||||
|
]},
|
||||||
|
]
|
||||||
|
compactor = lesson["COMPACTOR"]
|
||||||
|
compactor.summarize_history = lambda _messages: (_ for _ in ()).throw(
|
||||||
|
AssertionError("full compaction should not run"))
|
||||||
|
|
||||||
|
prepared = compactor.prepare(messages, "inspect the result")
|
||||||
|
content = prepared[-1]["content"][0]["content"]
|
||||||
|
|
||||||
|
assert len(prepared) == 2
|
||||||
|
assert content.startswith("<persisted-output>")
|
||||||
|
saved_line = next(line for line in content.splitlines()
|
||||||
|
if line.startswith("Full output: "))
|
||||||
|
assert Path(saved_line.removeprefix("Full output: ")).read_text() == output
|
||||||
@@ -16,7 +16,7 @@
|
|||||||
<!-- Trigger Condition -->
|
<!-- Trigger Condition -->
|
||||||
<rect x="20" y="54" width="680" height="44" rx="6" fill="#fef2f2" stroke="#fca5a5" stroke-width="1"/>
|
<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="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 1–3, estimate_chars(messages) > CONTEXT_CHAR_LIMIT.</text>
|
<text x="140" y="70" fill="#991b1b" font-size="11">After micro_compact, estimate_chars(messages) > CONTEXT_CHAR_LIMIT.</text>
|
||||||
<text x="140" y="86" fill="#991b1b" font-size="10">The current CONTEXT_CHAR_LIMIT is 50,000 characters.</text>
|
<text x="140" y="86" fill="#991b1b" font-size="10">The current CONTEXT_CHAR_LIMIT is 50,000 characters.</text>
|
||||||
|
|
||||||
<!-- Steps -->
|
<!-- Steps -->
|
||||||
|
|||||||
|
Before Width: | Height: | Size: 5.7 KiB After Width: | Height: | Size: 5.7 KiB |
@@ -16,7 +16,7 @@
|
|||||||
<!-- トリガー条件 -->
|
<!-- トリガー条件 -->
|
||||||
<rect x="20" y="54" width="680" height="44" rx="6" fill="#fef2f2" stroke="#fca5a5" stroke-width="1"/>
|
<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="35" y="70" fill="#991b1b" font-size="11" font-weight="600">トリガー条件</text>
|
||||||
<text x="115" y="70" fill="#991b1b" font-size="11">Step 1~3 の後、estimate_chars(messages) > CONTEXT_CHAR_LIMIT。</text>
|
<text x="115" y="70" fill="#991b1b" font-size="11">micro_compact の後、estimate_chars(messages) > CONTEXT_CHAR_LIMIT。</text>
|
||||||
<text x="115" y="86" fill="#991b1b" font-size="10">現在の CONTEXT_CHAR_LIMIT は 50,000 文字。</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 |
@@ -16,7 +16,7 @@
|
|||||||
<!-- 触发条件 -->
|
<!-- 触发条件 -->
|
||||||
<rect x="20" y="54" width="680" height="44" rx="6" fill="#fef2f2" stroke="#fca5a5" stroke-width="1"/>
|
<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="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) > CONTEXT_CHAR_LIMIT。</text>
|
<text x="105" y="70" fill="#991b1b" font-size="11">micro_compact 后,estimate_chars(messages) > CONTEXT_CHAR_LIMIT。</text>
|
||||||
<text x="105" y="86" fill="#991b1b" font-size="10">当前实现的 CONTEXT_CHAR_LIMIT 为 50,000 个字符。</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 |
@@ -45,9 +45,9 @@
|
|||||||
<rect x="170" y="82" width="200" height="252" rx="10" fill="#fffbeb" stroke="#d97706" stroke-width="2"/>
|
<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>
|
<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"/>
|
<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 1–2 Every Turn · 0 API</text>
|
||||||
|
|
||||||
<rect x="186" y="130" width="168" height="24" rx="4" fill="#fef3c7" stroke="#d97706" stroke-width="1"/>
|
<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>
|
<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>
|
<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"/>
|
<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)"/>
|
<line x1="270" y1="210" x2="270" y2="222" stroke="#555" stroke-width="1.2" marker-end="url(#arrow)"/>
|
||||||
|
|
||||||
<!-- ◇ Decision Diamond -->
|
<!-- ◇ Decision Diamond -->
|
||||||
<polygon points="270,226 300,244 270,262 240,244" fill="#f0f4ff" stroke="#ea580c" stroke-width="1.5"/>
|
<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 -->
|
<!-- No: right annotation -->
|
||||||
<text x="306" y="240" fill="#16a34a" font-size="9" font-weight="700">No → Pass</text>
|
<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>
|
<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"/>
|
<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"/>
|
<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"/>
|
<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>
|
<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 |
@@ -45,9 +45,9 @@
|
|||||||
<rect x="170" y="82" width="200" height="252" rx="10" fill="#fffbeb" stroke="#d97706" stroke-width="2"/>
|
<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>
|
<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"/>
|
<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 1–2 は毎ターン · 0 API</text>
|
||||||
|
|
||||||
<rect x="186" y="130" width="168" height="24" rx="4" fill="#fef3c7" stroke="#d97706" stroke-width="1"/>
|
<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>
|
<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>
|
<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"/>
|
<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)"/>
|
<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"/>
|
<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>
|
<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>
|
<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"/>
|
<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"/>
|
<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"/>
|
<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>
|
<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 |
@@ -45,9 +45,9 @@
|
|||||||
<rect x="170" y="82" width="200" height="252" rx="10" fill="#fffbeb" stroke="#d97706" stroke-width="2"/>
|
<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>
|
<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"/>
|
<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 1–2 每轮 · 0 API</text>
|
||||||
|
|
||||||
<rect x="186" y="130" width="168" height="24" rx="4" fill="#fef3c7" stroke="#d97706" stroke-width="1"/>
|
<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>
|
<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>
|
<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"/>
|
<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)"/>
|
<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"/>
|
<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>
|
<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>
|
<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"/>
|
<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"/>
|
<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_history,1 API</text>
|
<text x="94" y="458" fill="#334155" font-size="10">② 条件触发:Step 3 后 size 仍超阈值 → 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"/>
|
<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>
|
<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 |
@@ -39,7 +39,7 @@
|
|||||||
|
|
||||||
<!-- ===== Pre-processing pipeline title ===== -->
|
<!-- ===== Pre-processing pipeline title ===== -->
|
||||||
<rect x="20" y="146" width="720" height="24" rx="4" fill="#f1f5f9"/>
|
<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 -->
|
<!-- 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"/>
|
<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"/>
|
<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="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="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="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 ===== -->
|
<!-- ===== Auto-compact title ===== -->
|
||||||
<rect x="20" y="358" width="720" height="24" rx="4" fill="#f1f5f9"/>
|
<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 -->
|
<!-- Step 4: compact_history -->
|
||||||
<rect x="80" y="390" width="600" height="58" rx="7" fill="url(#auto)" stroke="#dc2626" stroke-width="2"/>
|
<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 |
@@ -39,7 +39,7 @@
|
|||||||
|
|
||||||
<!-- ===== 前処理パイプラインタイトル ===== -->
|
<!-- ===== 前処理パイプラインタイトル ===== -->
|
||||||
<rect x="20" y="146" width="720" height="24" rx="4" fill="#f1f5f9"/>
|
<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 -->
|
<!-- 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"/>
|
<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"/>
|
<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="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="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="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"/>
|
<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 -->
|
<!-- Step 4: compact_history -->
|
||||||
<rect x="80" y="390" width="600" height="58" rx="7" fill="url(#auto)" stroke="#dc2626" stroke-width="2"/>
|
<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 |
@@ -39,7 +39,7 @@
|
|||||||
|
|
||||||
<!-- ===== 预处理管线标题 ===== -->
|
<!-- ===== 预处理管线标题 ===== -->
|
||||||
<rect x="20" y="146" width="720" height="24" rx="4" fill="#f1f5f9"/>
|
<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 3,0 API)</text>
|
||||||
|
|
||||||
<!-- Step 1: tool_result_budget -->
|
<!-- 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"/>
|
<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"/>
|
<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="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="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="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"/>
|
<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 -->
|
<!-- Step 4: compact_history -->
|
||||||
<rect x="80" y="390" width="600" height="58" rx="7" fill="url(#auto)" stroke="#dc2626" stroke-width="2"/>
|
<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 |
@@ -41,18 +41,18 @@
|
|||||||
<rect x="400" y="130" width="290" height="10" rx="2" fill="#fef3c7"/>
|
<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>
|
<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"/>
|
<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"/>
|
<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"/>
|
<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="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 -->
|
<!-- How -->
|
||||||
<rect x="20" y="228" width="680" height="62" rx="6" fill="#f8fafc" stroke="#cbd5e1" stroke-width="1"/>
|
<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="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="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">Placeholder</text>
|
<text x="35" y="264" fill="#1e3a5f" font-size="11" font-weight="600">Recovery</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="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>
|
<text x="105" y="280" fill="#94a3b8" font-size="9">The message structure remains valid for the next loop iteration.</text>
|
||||||
</svg>
|
</svg>
|
||||||
|
|||||||
|
Before Width: | Height: | Size: 4.3 KiB After Width: | Height: | Size: 4.3 KiB |
@@ -41,18 +41,18 @@
|
|||||||
<rect x="400" y="130" width="290" height="10" rx="2" fill="#fef3c7"/>
|
<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>
|
<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"/>
|
<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"/>
|
<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"/>
|
<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="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"/>
|
<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="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="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="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="264" fill="#475569" font-size="10">短縮した各結果に .task_outputs/ 内の信頼できるパスを残す。</text>
|
||||||
<text x="125" y="280" fill="#94a3b8" font-size="9">メッセージ構造を保ったまま次のループへ進める。</text>
|
<text x="125" y="280" fill="#94a3b8" font-size="9">メッセージ構造を保ったまま次のループへ進める。</text>
|
||||||
</svg>
|
</svg>
|
||||||
|
|||||||
|
Before Width: | Height: | Size: 4.5 KiB After Width: | Height: | Size: 4.5 KiB |
@@ -11,7 +11,7 @@
|
|||||||
<rect width="720" height="300" fill="#fafbfc" rx="8"/>
|
<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="0" width="720" height="38" fill="url(#header)" rx="8"/>
|
||||||
<rect x="0" y="30" width="720" height="8" fill="url(#header)"/>
|
<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"/>
|
<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"/>
|
<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>
|
<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"/>
|
<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"/>
|
<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"/>
|
<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="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"/>
|
<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="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="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="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="264" fill="#475569" font-size="10">每条缩短结果都保留 .task_outputs/ 下的可信路径。</text>
|
||||||
<text x="95" y="280" fill="#94a3b8" font-size="9">消息结构保持不变,后续循环仍可继续处理。</text>
|
<text x="95" y="280" fill="#94a3b8" font-size="9">消息结构保持不变,后续循环仍可继续处理。</text>
|
||||||
</svg>
|
</svg>
|
||||||
|
|||||||
|
Before Width: | Height: | Size: 4.3 KiB After Width: | Height: | Size: 4.3 KiB |