mirror of
https://github.com/NousResearch/hermes-agent.git
synced 2026-07-22 16:25:58 +00:00
feat(honcho): make latency-adding paths configurable
queryRewrite (default off) gates the latest-message rewrite so the extra auxiliary LLM call is opt-in. firstTurnBaseWait and firstTurnDialecticWait expose the turn-1 bounded waits in seconds (0 disables). All three resolve host-block-first like every other field. Also pins per-host timeout resolution with tests.
This commit is contained in:
parent
e7fb51d5ac
commit
e8957babf4
5 changed files with 113 additions and 2 deletions
|
|
@ -256,6 +256,7 @@ class HonchoMemoryProvider(MemoryProvider):
|
|||
|
||||
# B5: Cost-awareness turn counting and cadence
|
||||
self._turn_count = 0
|
||||
self._query_rewrite_enabled = False
|
||||
self._injection_frequency = "every-turn" # or "first-turn"
|
||||
self._context_cadence = 1 # minimum turns between context API calls
|
||||
self._dialectic_cadence = 1 # backwards-compat fallback; wizard writes 2 on new configs
|
||||
|
|
@ -361,6 +362,9 @@ class HonchoMemoryProvider(MemoryProvider):
|
|||
self._injection_frequency = cfg.injection_frequency
|
||||
self._context_cadence = cfg.context_cadence
|
||||
self._dialectic_cadence = cfg.dialectic_cadence
|
||||
self._query_rewrite_enabled = cfg.query_rewrite
|
||||
self._FIRST_TURN_BASE_TIMEOUT = cfg.first_turn_base_wait
|
||||
self._FIRST_TURN_DIALECTIC_CAP = cfg.first_turn_dialectic_wait
|
||||
self._dialectic_depth = max(1, min(cfg.dialectic_depth, 3))
|
||||
self._dialectic_depth_levels = cfg.dialectic_depth_levels
|
||||
self._reasoning_heuristic = cfg.reasoning_heuristic
|
||||
|
|
@ -510,7 +514,7 @@ class HonchoMemoryProvider(MemoryProvider):
|
|||
except Exception as e:
|
||||
logger.debug("Honcho context prewarm failed: %s", e)
|
||||
|
||||
if self._query_rewriter is None:
|
||||
if self._query_rewriter is None or not self._query_rewrite_enabled:
|
||||
_prewarm_query = (
|
||||
"Summarize what you know about this user. "
|
||||
"Focus on preferences, current projects, and working style."
|
||||
|
|
@ -1225,7 +1229,7 @@ class HonchoMemoryProvider(MemoryProvider):
|
|||
is_cold = not self._base_context_cache
|
||||
results: list[str] = []
|
||||
rewritten_query = ""
|
||||
if use_query_rewrite and self._query_rewriter:
|
||||
if use_query_rewrite and self._query_rewrite_enabled and self._query_rewriter:
|
||||
try:
|
||||
rewritten_query = self._query_rewriter(query).strip()
|
||||
except Exception as exc:
|
||||
|
|
|
|||
|
|
@ -145,6 +145,17 @@ def _parse_int_config(host_val, root_val, default: int) -> int:
|
|||
return default
|
||||
|
||||
|
||||
def _parse_float_config(host_val, root_val, default: float) -> float:
|
||||
"""Parse a float config: host wins, then root, then default. Clamped ≥ 0."""
|
||||
for val in (host_val, root_val):
|
||||
if val is not None:
|
||||
try:
|
||||
return max(0.0, float(val))
|
||||
except (ValueError, TypeError):
|
||||
pass
|
||||
return default
|
||||
|
||||
|
||||
def _parse_string_map(host_obj: dict, root_obj: dict, key: str) -> dict[str, str]:
|
||||
"""Parse a string-to-string map with host-level whole-map override."""
|
||||
source = host_obj[key] if key in host_obj else root_obj.get(key)
|
||||
|
|
@ -368,6 +379,14 @@ class HonchoClientConfig:
|
|||
context_cadence: int = 1
|
||||
# Minimum turns between dialectic prefetch fires (supplement layer cadence)
|
||||
dialectic_cadence: int = 1
|
||||
# Rewrite the latest user message into a retrieval query before dialectic.
|
||||
# Off by default: adds one auxiliary LLM call per dialectic fire
|
||||
# (model/timeout under auxiliary.memory_query_rewrite in config.yaml).
|
||||
query_rewrite: bool = False
|
||||
# Bounded synchronous waits on turn 1, in seconds. 0 disables the wait
|
||||
# entirely (fully async first turn; context surfaces on later turns).
|
||||
first_turn_base_wait: float = 3.0
|
||||
first_turn_dialectic_wait: float = 2.0
|
||||
# Observation mode: legacy string shorthand ("directional" or "unified").
|
||||
# Kept for backward compat; granular per-peer booleans below are preferred.
|
||||
observation_mode: str = "directional"
|
||||
|
|
@ -627,6 +646,21 @@ class HonchoClientConfig:
|
|||
raw.get("dialecticCadence"),
|
||||
default=1,
|
||||
),
|
||||
query_rewrite=_resolve_bool(
|
||||
host_block.get("queryRewrite"),
|
||||
raw.get("queryRewrite"),
|
||||
default=False,
|
||||
),
|
||||
first_turn_base_wait=_parse_float_config(
|
||||
host_block.get("firstTurnBaseWait"),
|
||||
raw.get("firstTurnBaseWait"),
|
||||
default=3.0,
|
||||
),
|
||||
first_turn_dialectic_wait=_parse_float_config(
|
||||
host_block.get("firstTurnDialecticWait"),
|
||||
raw.get("firstTurnDialecticWait"),
|
||||
default=2.0,
|
||||
),
|
||||
# Migration guard: existing configs without an explicit
|
||||
# observationMode keep the old "unified" default so users
|
||||
# aren't silently switched to full bidirectional observation.
|
||||
|
|
|
|||
|
|
@ -100,6 +100,7 @@ def test_long_input_keeps_both_ends_with_a_hard_bound():
|
|||
|
||||
def _provider(query_rewriter, *, depth=1):
|
||||
provider = HonchoMemoryProvider(query_rewriter=query_rewriter)
|
||||
provider._query_rewrite_enabled = True
|
||||
provider._manager = MagicMock()
|
||||
provider._manager.dialectic_query.return_value = "memory synthesis"
|
||||
provider._session_key = "test-session"
|
||||
|
|
@ -189,6 +190,7 @@ def test_first_user_message_is_not_shadowed_by_generic_dialectic_prewarm():
|
|||
enabled=True,
|
||||
recall_mode="hybrid",
|
||||
timeout=1,
|
||||
query_rewrite=True,
|
||||
)
|
||||
|
||||
with (
|
||||
|
|
@ -237,3 +239,24 @@ def test_query_rewrite_has_an_independent_auxiliary_model_config():
|
|||
assert task_config["provider"] == "auto"
|
||||
assert task_config["timeout"] == 8
|
||||
assert TASK_KEY in {key for key, _name, _description in _AUX_TASKS}
|
||||
|
||||
|
||||
def test_query_rewrite_disabled_by_default():
|
||||
"""queryRewrite defaults OFF — the rewriter must not add an LLM call."""
|
||||
rewriter = MagicMock(return_value="What does the user prefer?")
|
||||
provider = _provider(rewriter)
|
||||
provider._query_rewrite_enabled = False
|
||||
|
||||
provider._run_dialectic_depth("what did we decide?")
|
||||
|
||||
rewriter.assert_not_called()
|
||||
provider._manager.dialectic_query.assert_called_once()
|
||||
|
||||
|
||||
def test_config_defaults_keep_latency_additions_off():
|
||||
from plugins.memory.honcho.client import HonchoClientConfig
|
||||
|
||||
cfg = HonchoClientConfig(api_key="k", enabled=True)
|
||||
assert cfg.query_rewrite is False
|
||||
assert cfg.first_turn_base_wait == 3.0
|
||||
assert cfg.first_turn_dialectic_wait == 2.0
|
||||
|
|
|
|||
|
|
@ -125,3 +125,50 @@ def test_save_config_sets_owner_only_permissions(tmp_path, monkeypatch):
|
|||
assert config_file.exists()
|
||||
mode = stat.S_IMODE(config_file.stat().st_mode)
|
||||
assert mode == 0o600, f"Expected 0o600 (owner-only), got {oct(mode)}"
|
||||
|
||||
|
||||
class TestLatencyFlagResolution:
|
||||
def test_defaults(self, tmp_path, monkeypatch):
|
||||
monkeypatch.delenv('HONCHO_BASE_URL', raising=False)
|
||||
config_path = tmp_path / 'config.json'
|
||||
config_path.write_text(json.dumps({'apiKey': 'k'}))
|
||||
cfg = HonchoClientConfig.from_global_config(config_path=config_path)
|
||||
assert cfg.query_rewrite is False
|
||||
assert cfg.first_turn_base_wait == 3.0
|
||||
assert cfg.first_turn_dialectic_wait == 2.0
|
||||
|
||||
def test_host_block_wins(self, tmp_path, monkeypatch):
|
||||
monkeypatch.delenv('HONCHO_BASE_URL', raising=False)
|
||||
config_path = tmp_path / 'config.json'
|
||||
config_path.write_text(json.dumps({
|
||||
'apiKey': 'k',
|
||||
'queryRewrite': False,
|
||||
'firstTurnBaseWait': 3,
|
||||
'hosts': {'hermes': {
|
||||
'queryRewrite': True,
|
||||
'firstTurnBaseWait': 0,
|
||||
'firstTurnDialecticWait': 0.5,
|
||||
}},
|
||||
}))
|
||||
cfg = HonchoClientConfig.from_global_config(config_path=config_path)
|
||||
assert cfg.query_rewrite is True
|
||||
assert cfg.first_turn_base_wait == 0.0
|
||||
assert cfg.first_turn_dialectic_wait == 0.5
|
||||
|
||||
def test_per_host_timeout_wins_over_global(self, tmp_path, monkeypatch):
|
||||
monkeypatch.delenv('HONCHO_TIMEOUT', raising=False)
|
||||
config_path = tmp_path / 'config.json'
|
||||
config_path.write_text(json.dumps({
|
||||
'apiKey': 'k',
|
||||
'timeout': 30,
|
||||
'hosts': {'hermes': {'timeout': 5}},
|
||||
}))
|
||||
cfg = HonchoClientConfig.from_global_config(config_path=config_path)
|
||||
assert cfg.timeout == 5.0
|
||||
|
||||
def test_timeout_falls_back_to_global(self, tmp_path, monkeypatch):
|
||||
monkeypatch.delenv('HONCHO_TIMEOUT', raising=False)
|
||||
config_path = tmp_path / 'config.json'
|
||||
config_path.write_text(json.dumps({'apiKey': 'k', 'timeout': 30}))
|
||||
cfg = HonchoClientConfig.from_global_config(config_path=config_path)
|
||||
assert cfg.timeout == 30.0
|
||||
|
|
|
|||
|
|
@ -25,6 +25,9 @@ def _configured_hybrid_config() -> _FakeHonchoConfig:
|
|||
injection_frequency="every-turn",
|
||||
context_cadence=1,
|
||||
dialectic_cadence=1,
|
||||
query_rewrite=False,
|
||||
first_turn_base_wait=3.0,
|
||||
first_turn_dialectic_wait=2.0,
|
||||
dialectic_depth=1,
|
||||
dialectic_depth_levels=None,
|
||||
reasoning_heuristic=True,
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue