core/tools: add get_truncated tool for chained resume

This commit is contained in:
2026-08-11 14:59:39 +08:00
parent c96f669bcb
commit 00db7cdc7f
7 changed files with 118 additions and 5 deletions
+5 -1
View File
@@ -252,8 +252,12 @@ async def test_fetch_char_truncation_embeds_token(workspace: Path, http_server:
token = decode_truncate_token(token_str)
assert token is not None
assert token.kind == "http"
assert token.offset == 1000 # next chunk starts at max_chars
assert token.limit == 1000
assert 0 < token.offset < 1000
# Continuation resumes exactly where the chunk stopped (no gap).
out2 = await call_async(fetch, f"{http_server}/big", max_chars=1000, offset=token.offset)
body2 = out2.split("--- body ---", 1)[-1].strip()
assert body2.startswith("x")
async def test_fetch_offset_skips_body(workspace: Path, http_server: str) -> None:
+62
View File
@@ -0,0 +1,62 @@
from __future__ import annotations
import importlib
from pathlib import Path
from plyngent.tools.truncate_token import TruncateToken, decode_truncate_token, encode_truncate_token
from tests.test_tools.helpers import call_async
get_truncated_module = importlib.import_module("plyngent.tools.file.get_truncated")
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("]")
return None
async def test_get_truncated_invalid_token() -> None:
out = await call_async(get_truncated, "garbage-token")
assert out.startswith("error:")
assert "invalid" in out
async def test_get_truncated_file_chains_without_gap(workspace: Path) -> None:
(workspace / "big.txt").write_text("a" * 1000, encoding="utf-8")
token = encode_truncate_token(TruncateToken(kind="file", location="big.txt", offset=0, limit=200))
total_read = 0
for _ in range(20):
out = await call_async(get_truncated, token, max_chars=200)
content, _, _ = out.partition("\n...[")
assert content == "a" * len(content)
total_read += len(content)
next_token = _extract_token(out)
if next_token is None:
break
parsed = decode_truncate_token(next_token)
assert parsed is not None
assert parsed.offset == total_read # no gap, no overlap
token = next_token
assert total_read == 1000
async def test_get_truncated_file_missing(workspace: Path) -> None:
token = encode_truncate_token(TruncateToken(kind="file", location="nope.txt", offset=0, limit=200))
out = await call_async(get_truncated, token, max_chars=200)
assert out.startswith("error:")
async def test_get_truncated_http_passthrough(monkeypatch) -> None:
calls: list[tuple[str, int, int]] = []
async def fake_fetch(url: str, *, offset: int, max_chars: int) -> str:
calls.append((url, offset, max_chars))
return f"body-from-{offset}"
monkeypatch.setattr(get_truncated_module.fetch, "handler", fake_fetch)
token = encode_truncate_token(TruncateToken(kind="http", location="https://example.com/x", offset=500, limit=400))
out = await call_async(get_truncated, token, max_chars=200)
assert out == "body-from-500"
assert calls == [("https://example.com/x", 500, 400)]
+6 -4
View File
@@ -53,10 +53,11 @@ def test_truncate_with_token_cuts_and_advances() -> None:
total_len=500,
)
assert token is not None
content, _, _ = out.partition("\n...[")
assert content.startswith("x")
assert "truncated 300 chars" in out
assert f"[TRUNCATE_TOKEN: {encode_truncate_token(token)}]" in out
assert token.offset == 200
assert token.limit == 200
assert token.offset == len(content) # cursor lands exactly where content stops
assert len(out) <= 200 # marker fits inside the char budget
@@ -73,7 +74,7 @@ def test_truncate_with_token_more_after_nonoverflow() -> None:
total_len=200,
)
assert token is not None
assert token.offset == 40
assert token.offset == 30 # advanced by returned length only
assert "more content available" in out
assert out.startswith("y" * 30)
@@ -106,6 +107,7 @@ def test_truncate_with_token_nonzero_start() -> None:
)
assert token is not None
assert token.kind == "http"
assert token.offset == 350 # 150 + 200
content, _, _ = out.partition("\n...[")
assert token.offset == 150 + len(content)
assert "truncated 300 chars" in out
assert len(out) <= 200