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(" …") + + # ---------------------------------------------------------------------------