core/agent: add model-friendly hint to truncated tool results

This commit is contained in:
2026-08-01 01:42:17 +08:00
parent 49a3ef0004
commit 3fe1c4cf91
5 changed files with 44 additions and 7 deletions
+1 -1
View File
@@ -76,7 +76,7 @@ Async SQLAlchemy + aiosqlite. `MemoryStore`: schema init (+ lightweight SQLite `
- **`/compact`**: soft-compact tool dumps → model summary (no tools) via the same kind dispatch → **new** session seeded with summary message. Soft-compact keeps the last **12** tool results full-size by default (`DEFAULT_RECENT_TOOL_RESULTS`); older tool payloads shrink first.
- Events: text_delta, **reasoning_delta**, assistant_message, tool_call/result, max_rounds, **error** (`retryable`/`source`), **cancelled** (`reason`), **usage** (`TokenUsage`).
- Usage: API `usage` from completions (stream with `include_usage`); **char≈token fallback** (~4 chars/token) when omitted; **context size** = last request ``prompt_tokens`` (API preferred); `last_turn_usage` / `session_usage` are **billed sums** (tool rounds re-send history); CLI end-of-turn + `/status`.
- Config ``[agent]``: `system_prompt` (persona; default `DEFAULT_SYSTEM_PROMPT`; `""` omits persona), `tool_directives` (tool playbook; default `DEFAULT_TOOL_DIRECTIVES`; `""` omits playbook; both empty → no system), `max_tool_result_chars`, `parallel_tools`, `confirm_destructive`, `path_denylist`, `max_context_tokens` (default 200k est. tokens).
- Config ``[agent]``: `system_prompt` (persona; default `DEFAULT_SYSTEM_PROMPT`; `""` omits persona), `tool_directives` (tool playbook; default `DEFAULT_TOOL_DIRECTIVES`; `""` omits playbook; both empty → no system), `max_tool_result_chars`, `parallel_tools`, `confirm_destructive`, `path_denylist`, `max_context_tokens` (default 200k est. tokens). Truncated tool results carry a model hint (`[Truncated (N chars max; M omitted). You may ask for user to increase the limit via configuration file.]`) so the model can ask the user to raise `max_tool_result_chars`; request-time compact shrinks (`hint=False`) keep the terse `...[truncated N characters]` marker.
### Tools (`tools/`)
+20 -3
View File
@@ -28,13 +28,30 @@ DEFAULT_RECENT_TOOL_RESULTS = 12
DEFAULT_CONTEXT_MAX_CHARS = DEFAULT_CONTEXT_MAX_TOKENS * 4
def truncate_tool_result(text: str, max_chars: int = DEFAULT_TOOL_RESULT_MAX_CHARS) -> str:
"""Cap tool output so huge dumps do not flood model context."""
def truncate_tool_result(
text: str,
max_chars: int = DEFAULT_TOOL_RESULT_MAX_CHARS,
*,
hint: bool = True,
) -> str:
"""Cap tool output so huge dumps do not flood model context.
With ``hint=True`` the suffix tells the model the result was cut and that
it may ask the user to raise the config limit (``max_tool_result_chars``).
``hint=False`` keeps the terse marker for internal shrinks whose cap is not
a user-facing config knob (e.g. request-time compacting).
"""
if max_chars < 1:
return text
if len(text) <= max_chars:
return text
omitted = len(text) - max_chars
if hint:
return (
f"{text[:max_chars]}\n"
f"...[Truncated ({max_chars} chars max.; {omitted} chars omitted). "
f"You may ask for user to increase the limit via configuration file.]"
)
return f"{text[:max_chars]}\n...[truncated {omitted} characters]"
@@ -103,7 +120,7 @@ def _shrink_tool(message: ToolChatMessage, max_chars: int) -> ToolChatMessage:
return message
return msgspec.structs.replace(
message,
content=truncate_tool_result(message.content, max_chars),
content=truncate_tool_result(message.content, max_chars, hint=False),
)
+11 -1
View File
@@ -12,8 +12,18 @@ def test_truncate_tool_result_long() -> None:
text = "a" * 50
out = truncate_tool_result(text, 20)
assert out.startswith("a" * 20)
assert "truncated" in out
assert "Truncated" in out
assert "20" in out
assert "30" in out
assert "configuration file" in out
def test_truncate_tool_result_no_hint() -> None:
text = "a" * 50
out = truncate_tool_result(text, 20, hint=False)
assert out.startswith("a" * 20)
assert "truncated" in out
assert "configuration file" not in out
def test_estimate_message_chars() -> None:
+11 -1
View File
@@ -41,8 +41,18 @@ def test_truncate_tool_result_long() -> None:
text = "a" * 50
out = truncate_tool_result(text, 20)
assert out.startswith("a" * 20)
assert "truncated" in out
assert "Truncated" in out
assert "20" in out
assert "30" in out
assert "configuration file" in out
def test_truncate_tool_result_no_hint() -> None:
text = "a" * 50
out = truncate_tool_result(text, 20, hint=False)
assert out.startswith("a" * 20)
assert "truncated" in out
assert "configuration file" not in out
def test_compact_shrinks_old_tool_results() -> None:
+1 -1
View File
@@ -848,7 +848,7 @@ async def test_tool_result_char_budget() -> None:
tool_msgs = [m for m in messages if isinstance(m, ToolChatMessage)]
assert len(tool_msgs) == 1
assert tool_msgs[0].content.startswith("x" * 20)
assert "truncated" in tool_msgs[0].content
assert "Truncated" in tool_msgs[0].content
async def test_empty_completion_raises() -> None: