core/tools: token-ify per-stream caps and resume memory remainders

This commit is contained in:
2026-08-11 18:53:06 +08:00
parent 6fdfb8398e
commit 26472776fb
5 changed files with 93 additions and 22 deletions
+4 -4
View File
@@ -246,9 +246,9 @@ async def test_fetch_char_truncation_embeds_token(workspace: Path, http_server:
out = await call_async(fetch, f"{http_server}/big", max_chars=1000)
assert "truncated=true" in out
body = out.split("--- body ---", 1)[-1]
assert "TRUNCATE_TOKEN:" in body
token_line = next(ln for ln in body.splitlines() if "TRUNCATE_TOKEN:" in ln)
token_str = token_line.split("[TRUNCATE_TOKEN: ", 1)[1].rstrip("]")
assert "truncate_token=" in body
token_line = next(ln for ln in body.splitlines() if "truncate_token=" in ln)
token_str = token_line.split("truncate_token=", 1)[1].rstrip("]")
token = decode_truncate_token(token_str)
assert token is not None
assert token.kind == "http"
@@ -269,7 +269,7 @@ async def test_fetch_offset_skips_body(workspace: Path, http_server: str) -> Non
out = await call_async(fetch, f"{http_server}/big", max_chars=200, offset=4900)
body = out.split("--- body ---", 1)[-1]
assert body.strip() == "x" * 100
assert "TRUNCATE_TOKEN:" not in out
assert "truncate_token=" not in out
async def test_fetch_private_denied_without_grant(workspace: Path, http_server: str) -> None:
+5 -5
View File
@@ -11,8 +11,8 @@ get_truncated_module = importlib.import_module("plyngent.tools.file.get_truncate
def _extract_token(out: str) -> str | None:
for line in out.splitlines():
if "TRUNCATE_TOKEN:" in line:
return line.split("[TRUNCATE_TOKEN: ", 1)[1].rstrip("]")
if "truncate_token=" in line:
return line.split("truncate_token=", 1)[1].rstrip("]")
return None
@@ -33,10 +33,10 @@ def test_read_file_max_chars_embeds_token(workspace: Path) -> None:
out = call_sync(read_file, "big.txt", max_chars=200)
header, _, rest = out.partition("\n")
assert header.startswith("L1-") # 1-based range of the raw slice read
content, _, marker = rest.partition("\n...[")
content, _, marker = rest.partition("\n[Truncated")
assert content == "a" * len(content)
assert len(content) < 200
assert "TRUNCATE_TOKEN:" in marker
assert "truncate_token=" in marker
token = _extract_token(out)
assert token is not None
parsed = get_truncated_module.decode_truncate_token(token)
@@ -49,7 +49,7 @@ def test_read_file_max_chars_small_file_untouched(workspace: Path) -> None:
(workspace / "small.txt").write_text("tiny", encoding="utf-8")
out = call_sync(read_file, "small.txt", max_chars=200)
assert out == "L1-1\ntiny"
assert "TRUNCATE_TOKEN:" not in out
assert "truncate_token=" not in out
def test_edit_replace_first_only(workspace: object) -> None:
+42 -3
View File
@@ -12,8 +12,8 @@ get_truncated = get_truncated_module.get_truncated
def _extract_token(out: str) -> str | None:
for line in out.splitlines():
if "TRUNCATE_TOKEN:" in line:
return line.split("[TRUNCATE_TOKEN: ", 1)[1].rstrip("]")
if "truncate_token=" in line:
return line.split("truncate_token=", 1)[1].rstrip("]")
return None
@@ -29,7 +29,7 @@ async def test_get_truncated_file_chains_without_gap(workspace: Path) -> None:
total_read = 0
for _ in range(20):
out = await call_async(get_truncated, token, max_chars=200)
body, _, _ = out.partition("\n...[")
body, _, _ = out.partition("\n[Truncated")
header, _, content = body.partition("\n")
assert header.startswith("L") and "-" in header
assert content == "a" * len(content)
@@ -62,3 +62,42 @@ async def test_get_truncated_http_passthrough(monkeypatch) -> None:
out = await call_async(get_truncated, token, max_chars=200)
assert out == "body-from-500"
assert calls == [("https://example.com/x", 500, 400)]
async def test_get_truncated_memory_chains() -> None:
from plyngent.tools.truncate_token import truncate_generic
bounded = truncate_generic("m" * 1000, 200)
assert "truncate_token=" in bounded
token = bounded.split("truncate_token=", 1)[1].rstrip("]")
parsed = decode_truncate_token(token)
assert parsed is not None
assert parsed.kind == "memory"
cursor = parsed.offset # chars already delivered by truncate_generic
delivered = cursor
for _ in range(20):
out = await call_async(get_truncated, token, max_chars=200)
content, _, _ = out.partition("\n[Truncated")
assert content == "m" * len(content)
delivered += len(content)
next_token = _extract_token(out)
if next_token is None:
break # source exhausted → no token, no marker
parsed = decode_truncate_token(next_token)
assert parsed is not None
assert parsed.kind == "memory"
# No gap: the next cursor is exactly where this chunk ended.
assert parsed.offset == delivered
token = next_token
assert delivered == 1000
async def test_get_truncated_memory_expired() -> None:
from plyngent.tools.truncate_token import _MEMORY_REMAINDERS, store_remainder
key = store_remainder("x" * 500)
del _MEMORY_REMAINDERS[key]
token = encode_truncate_token(TruncateToken(kind="memory", location=key, offset=0, limit=200))
out = await call_async(get_truncated, token, max_chars=200)
assert out.startswith("error:")
assert "expired" in out