mirror of
https://github.com/NousResearch/hermes-agent.git
synced 2026-07-20 15:33:54 +00:00
fix(honcho): stop clipping honcho_reasoning tool results to the injection budget
dialecticMaxChars (default 600) is documented as the budget for the dialectic supplement auto-injected into the system prompt every turn — a small recurring cost that is correct to bound tightly. But dialectic_query() applied that cap unconditionally, so explicit honcho_reasoning tool calls — where the model deliberately spends a turn asking for a synthesized answer — were silently truncated mid-word to 600 chars with a trailing " …", no error surfaced. The full answer is returned by Honcho server-side; the clip happens client-side. The auto-injection path already has its own token-based budget (contextTokens, enforced in prefetch() via _truncate_to_budget), so the char cap's real job is a cheap always-on guardrail for that recurring injection. Explicit tool results are already bounded server-side by Honcho's dialectic MAX_OUTPUT_TOKENS and don't need the injection cap — sibling tools (honcho_search, honcho_context) don't post-clip their results either. Add apply_injection_cap (default True, preserving current behavior) to dialectic_query(); the honcho_reasoning tool handler passes False so it returns Honcho's full synthesized answer. Auto-injection is unchanged. Tests cover both the capped injection path and the uncapped tool path. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
parent
29e0471708
commit
56816f4232
4 changed files with 75 additions and 3 deletions
|
|
@ -1552,6 +1552,11 @@ class HonchoMemoryProvider(MemoryProvider):
|
|||
self._session_key, query,
|
||||
reasoning_level=reasoning_level,
|
||||
peer=peer,
|
||||
# Explicit tool call: return Honcho's full synthesized answer.
|
||||
# The system-prompt injection cap (dialecticMaxChars) must not
|
||||
# clip a result the model deliberately asked for; it's already
|
||||
# bounded server-side by Honcho's MAX_OUTPUT_TOKENS.
|
||||
apply_injection_cap=False,
|
||||
)
|
||||
# Update cadence tracker so auto-injection respects the gap after an explicit call
|
||||
self._last_dialectic_turn = self._turn_count
|
||||
|
|
|
|||
|
|
@ -331,7 +331,10 @@ class HonchoClientConfig:
|
|||
# honcho_reasoning tool param (agentic). When false, always uses
|
||||
# dialecticReasoningLevel and ignores model-provided overrides.
|
||||
dialectic_dynamic: bool = True
|
||||
# Max chars of dialectic result to inject into Hermes system prompt
|
||||
# Max chars of the dialectic supplement auto-injected into the Hermes system
|
||||
# prompt each turn. Applies ONLY to auto-injection — explicit honcho_reasoning
|
||||
# tool results return in full (bounded server-side by Honcho's MAX_OUTPUT_TOKENS),
|
||||
# via dialectic_query(apply_injection_cap=False).
|
||||
dialectic_max_chars: int = 600
|
||||
# Dialectic depth: how many .chat() calls per dialectic cycle (1-3).
|
||||
# Depth 1: single call. Depth 2: self-audit + targeted synthesis.
|
||||
|
|
|
|||
|
|
@ -602,6 +602,7 @@ class HonchoSessionManager:
|
|||
self, session_key: str, query: str,
|
||||
reasoning_level: str | None = None,
|
||||
peer: str = "user",
|
||||
apply_injection_cap: bool = True,
|
||||
) -> str:
|
||||
"""
|
||||
Query Honcho's dialectic endpoint about a peer.
|
||||
|
|
@ -617,6 +618,15 @@ class HonchoSessionManager:
|
|||
Only honored when dialecticDynamic is true.
|
||||
If None or dialecticDynamic is false, uses the configured default.
|
||||
peer: Which peer to query — "user" (default) or "ai".
|
||||
apply_injection_cap: When True (default), clip the result to
|
||||
``dialecticMaxChars`` — the budget for the dialectic
|
||||
supplement auto-injected into the system prompt every
|
||||
turn. Set False for explicit ``honcho_reasoning`` tool
|
||||
calls, where the model deliberately asked for a full
|
||||
synthesized answer; that result is already bounded
|
||||
server-side by Honcho's dialectic MAX_OUTPUT_TOKENS,
|
||||
so clipping it to the injection budget silently
|
||||
discards content the caller asked for.
|
||||
|
||||
Returns:
|
||||
Honcho's synthesized answer, or empty string on failure.
|
||||
|
|
@ -655,8 +665,17 @@ class HonchoSessionManager:
|
|||
target_peer = self._get_or_create_peer(target_peer_id)
|
||||
result = target_peer.chat(query, reasoning_level=level) or ""
|
||||
|
||||
# Apply Hermes-side char cap before caching
|
||||
if result and self._dialectic_max_chars and len(result) > self._dialectic_max_chars:
|
||||
# Apply the Hermes-side injection char cap before caching. This budget
|
||||
# (dialecticMaxChars) bounds the dialectic supplement auto-injected into
|
||||
# the system prompt every turn; it must NOT clip explicit
|
||||
# honcho_reasoning tool results, which the model asked for in full and
|
||||
# which Honcho already bounds server-side via MAX_OUTPUT_TOKENS.
|
||||
if (
|
||||
apply_injection_cap
|
||||
and result
|
||||
and self._dialectic_max_chars
|
||||
and len(result) > self._dialectic_max_chars
|
||||
):
|
||||
result = result[:self._dialectic_max_chars].rsplit(" ", 1)[0] + " …"
|
||||
return result
|
||||
except Exception as e:
|
||||
|
|
|
|||
|
|
@ -564,6 +564,7 @@ class TestConcludeToolDispatch:
|
|||
"who are you",
|
||||
reasoning_level=None,
|
||||
peer="hermes",
|
||||
apply_injection_cap=False,
|
||||
)
|
||||
|
||||
def test_honcho_conclude_missing_both_params_returns_error(self):
|
||||
|
|
@ -1010,6 +1011,50 @@ class TestDialecticInputGuard:
|
|||
assert len(actual_query) <= 100
|
||||
|
||||
|
||||
class TestDialecticInjectionCap:
|
||||
"""dialecticMaxChars must bound the auto-injected supplement but must NOT
|
||||
clip explicit honcho_reasoning tool results, which the model asked for in
|
||||
full (they are already bounded server-side by Honcho's MAX_OUTPUT_TOKENS)."""
|
||||
|
||||
def _manager_with_long_answer(self, answer):
|
||||
from plugins.memory.honcho.client import HonchoClientConfig
|
||||
|
||||
cfg = HonchoClientConfig(dialectic_max_chars=50)
|
||||
mgr = HonchoSessionManager(config=cfg)
|
||||
mgr._dialectic_max_chars = 50
|
||||
|
||||
session = HonchoSession(
|
||||
key="test", user_peer_id="u", assistant_peer_id="a",
|
||||
honcho_session_id="s",
|
||||
)
|
||||
mgr._cache["test"] = session
|
||||
|
||||
mock_peer = MagicMock()
|
||||
mock_peer.chat.return_value = answer
|
||||
mgr._get_or_create_peer = MagicMock(return_value=mock_peer)
|
||||
return mgr
|
||||
|
||||
def test_injection_path_truncates(self):
|
||||
"""Default (auto-injection) path clips to dialecticMaxChars with an ellipsis."""
|
||||
answer = "fact " * 100 # 500 chars, well over the 50-char cap
|
||||
mgr = self._manager_with_long_answer(answer)
|
||||
|
||||
result = mgr.dialectic_query("test", "summarize")
|
||||
|
||||
assert len(result) <= 60 # cap + word-boundary slack + ellipsis
|
||||
assert result.endswith(" …")
|
||||
|
||||
def test_tool_path_returns_full_answer(self):
|
||||
"""Explicit tool call (apply_injection_cap=False) returns the full answer."""
|
||||
answer = "fact " * 100 # 500 chars, well over the 50-char cap
|
||||
mgr = self._manager_with_long_answer(answer)
|
||||
|
||||
result = mgr.dialectic_query("test", "summarize", apply_injection_cap=False)
|
||||
|
||||
assert result == answer
|
||||
assert not result.endswith(" …")
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue