mirror of
https://github.com/NCBM/plyngent.git
synced 2026-11-07 23:47:54 +08:00
core/agent: add model-friendly hint to truncated tool results
This commit is contained in:
@@ -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.
|
- **`/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`).
|
- 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`.
|
- 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/`)
|
### Tools (`tools/`)
|
||||||
|
|
||||||
|
|||||||
@@ -28,13 +28,30 @@ DEFAULT_RECENT_TOOL_RESULTS = 12
|
|||||||
DEFAULT_CONTEXT_MAX_CHARS = DEFAULT_CONTEXT_MAX_TOKENS * 4
|
DEFAULT_CONTEXT_MAX_CHARS = DEFAULT_CONTEXT_MAX_TOKENS * 4
|
||||||
|
|
||||||
|
|
||||||
def truncate_tool_result(text: str, max_chars: int = DEFAULT_TOOL_RESULT_MAX_CHARS) -> str:
|
def truncate_tool_result(
|
||||||
"""Cap tool output so huge dumps do not flood model context."""
|
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:
|
if max_chars < 1:
|
||||||
return text
|
return text
|
||||||
if len(text) <= max_chars:
|
if len(text) <= max_chars:
|
||||||
return text
|
return text
|
||||||
omitted = len(text) - max_chars
|
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]"
|
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 message
|
||||||
return msgspec.structs.replace(
|
return msgspec.structs.replace(
|
||||||
message,
|
message,
|
||||||
content=truncate_tool_result(message.content, max_chars),
|
content=truncate_tool_result(message.content, max_chars, hint=False),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -12,8 +12,18 @@ def test_truncate_tool_result_long() -> None:
|
|||||||
text = "a" * 50
|
text = "a" * 50
|
||||||
out = truncate_tool_result(text, 20)
|
out = truncate_tool_result(text, 20)
|
||||||
assert out.startswith("a" * 20)
|
assert out.startswith("a" * 20)
|
||||||
assert "truncated" in out
|
assert "Truncated" in out
|
||||||
|
assert "20" in out
|
||||||
assert "30" 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:
|
def test_estimate_message_chars() -> None:
|
||||||
|
|||||||
@@ -41,8 +41,18 @@ def test_truncate_tool_result_long() -> None:
|
|||||||
text = "a" * 50
|
text = "a" * 50
|
||||||
out = truncate_tool_result(text, 20)
|
out = truncate_tool_result(text, 20)
|
||||||
assert out.startswith("a" * 20)
|
assert out.startswith("a" * 20)
|
||||||
assert "truncated" in out
|
assert "Truncated" in out
|
||||||
|
assert "20" in out
|
||||||
assert "30" 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:
|
def test_compact_shrinks_old_tool_results() -> None:
|
||||||
|
|||||||
@@ -848,7 +848,7 @@ async def test_tool_result_char_budget() -> None:
|
|||||||
tool_msgs = [m for m in messages if isinstance(m, ToolChatMessage)]
|
tool_msgs = [m for m in messages if isinstance(m, ToolChatMessage)]
|
||||||
assert len(tool_msgs) == 1
|
assert len(tool_msgs) == 1
|
||||||
assert tool_msgs[0].content.startswith("x" * 20)
|
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:
|
async def test_empty_completion_raises() -> None:
|
||||||
|
|||||||
Reference in New Issue
Block a user