core/agent+config: soft context budget in estimated tokens

Replace max_context_chars with max_context_tokens (default 200k est.
tokens via char/4); compact and /status use the token budget.
This commit is contained in:
2026-07-15 10:58:01 +08:00
parent d2cab4198b
commit 58041c6c87
13 changed files with 59 additions and 45 deletions
+5 -5
View File
@@ -6,7 +6,7 @@ from msgspec import UNSET
from plyngent.agent.budget import (
compact_messages_for_request,
estimate_messages_chars,
estimate_messages_tokens,
truncate_tool_result,
)
from plyngent.agent.loop import run_chat_loop
@@ -75,7 +75,7 @@ def test_compact_shrinks_old_tool_results() -> None:
compacted = compact_messages_for_request(
messages,
max_chars=estimate_messages_chars(messages) - 1,
max_tokens=max(1, estimate_messages_tokens(messages) - 1),
old_tool_result_chars=40,
keep_recent_tool_results=1,
)
@@ -90,11 +90,11 @@ def test_compact_shrinks_old_tool_results() -> None:
assert compacted[5].content == "NEW" * 50
def test_compact_disabled_when_max_chars_zero() -> None:
def test_compact_disabled_when_max_tokens_zero() -> None:
messages: list[AnyChatMessage] = [
ToolChatMessage(content="x" * 500, tool_call_id="1"),
]
out = compact_messages_for_request(messages, max_chars=0)
out = compact_messages_for_request(messages, max_tokens=0)
assert out[0] is messages[0] or (
isinstance(out[0], ToolChatMessage) and out[0].content == "x" * 500
)
@@ -211,7 +211,7 @@ async def test_loop_sends_compacted_request_not_history() -> None:
history,
model="m",
stream=False,
max_context_chars=200,
max_context_tokens=50,
max_tool_result_chars=50,
)
]
+1 -1
View File
@@ -43,7 +43,7 @@ def test_soft_compact_transcript_shrinks_tools() -> None:
ToolChatMessage(content=big, tool_call_id="1"),
ToolChatMessage(content="recent", tool_call_id="2"),
]
out = soft_compact_transcript(messages, max_chars=500)
out = soft_compact_transcript(messages, max_tokens=100)
assert "truncated" in out or len(out) < len(big) + 50
assert "recent" in out
+2 -1
View File
@@ -142,7 +142,7 @@ async def test_rounds(state: ReplState) -> None:
assert state.agent.max_rounds == 40
async def test_status_shows_context_chars(
async def test_status_shows_context_tokens(
state: ReplState, capsys: pytest.CaptureFixture[str]
) -> None:
from plyngent.lmproto.openai_compatible.model import UserChatMessage
@@ -150,6 +150,7 @@ async def test_status_shows_context_chars(
state.agent.messages = [UserChatMessage(content="hello")]
assert await handle_slash(state, "/status") is True
out = capsys.readouterr().out
assert "context_tokens~=" in out
assert "context_chars=" in out
assert "tool_result_max=" in out
assert str(state.workspace) in out
+4 -3
View File
@@ -17,7 +17,7 @@ def test_agent_section_defaults(tmp_path: Path) -> None:
assert store.agent_config.parallel_tools is True
assert store.agent_config.confirm_destructive is True
assert store.agent_config.path_denylist == []
assert store.agent_config.max_context_chars == 200_000
assert store.agent_config.max_context_tokens == 200_000
def test_agent_section_parse(tmp_path: Path) -> None:
@@ -30,7 +30,7 @@ max_tool_result_chars = 100
parallel_tools = false
confirm_destructive = false
path_denylist = ["/secrets/", ".ssh/"]
max_context_chars = 5000
max_context_tokens = 5000
""",
encoding="utf-8",
)
@@ -40,4 +40,5 @@ max_context_chars = 5000
assert store.agent_config.parallel_tools is False
assert store.agent_config.confirm_destructive is False
assert store.agent_config.path_denylist == ["/secrets/", ".ssh/"]
assert store.agent_config.max_context_chars == 5000
assert store.agent_config.max_context_tokens == 5000