From 56816f4232ea6a78f10a57803c6b02cf481866f7 Mon Sep 17 00:00:00 2001 From: vizi0uz Date: Mon, 6 Jul 2026 04:50:22 -0300 Subject: [PATCH] fix(honcho): stop clipping honcho_reasoning tool results to the injection budget MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 --- plugins/memory/honcho/__init__.py | 5 ++++ plugins/memory/honcho/client.py | 5 +++- plugins/memory/honcho/session.py | 23 +++++++++++++-- tests/honcho_plugin/test_session.py | 45 +++++++++++++++++++++++++++++ 4 files changed, 75 insertions(+), 3 deletions(-) diff --git a/plugins/memory/honcho/__init__.py b/plugins/memory/honcho/__init__.py index 10d8a9c1f55a..17da911adfbc 100644 --- a/plugins/memory/honcho/__init__.py +++ b/plugins/memory/honcho/__init__.py @@ -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 diff --git a/plugins/memory/honcho/client.py b/plugins/memory/honcho/client.py index 93ba02543def..9f69f278ef4b 100644 --- a/plugins/memory/honcho/client.py +++ b/plugins/memory/honcho/client.py @@ -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. diff --git a/plugins/memory/honcho/session.py b/plugins/memory/honcho/session.py index 9fe53eaef5c9..a494945d1d78 100644 --- a/plugins/memory/honcho/session.py +++ b/plugins/memory/honcho/session.py @@ -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: diff --git a/tests/honcho_plugin/test_session.py b/tests/honcho_plugin/test_session.py index 8633ffc01758..a9b89842f41b 100644 --- a/tests/honcho_plugin/test_session.py +++ b/tests/honcho_plugin/test_session.py @@ -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(" …") + + # ---------------------------------------------------------------------------