feat(honcho): context injection overhaul, 5-tool surface, cost safety, session isolation
Context Injection Overhaul: - Base layer: peer.context() (representation + card) cached with 5-minute TTL - Dialectic supplement: cadence-gated, cached until next refresh - Trivial prompt skip: short inputs/slash commands skip injection - New peer guard: dialectic skipped at session start when peer has no context - Targeted warm prompt for better dialectic quality Tool Surface (5 bidirectional tools): - honcho_profile: read or update peer card - honcho_search: semantic search over context - honcho_context: full session context (summary, representation, card, messages) - honcho_reasoning: synthesized answer, reasoning_level param - honcho_conclude: create or delete conclusions (PII removal) Cost Safety: - dialectic_cadence defaults to 3 (~66% fewer LLM calls) - context_tokens defaults to uncapped (cap opt-in via config/wizard) - on_turn_start hook wired up (fixes broken cadence/injection gating) Correctness: - Explicit target= on peer context/card fetches (fixes identity blur) - honcho_search perspective fix under directional observation - Timeout config plumbing - peerName precedence over gateway user_id - skip_memory on temp agents (orphan session prevention) - gateway_session_key for stable per-chat session continuity - initOnSessionStart for eager tools-mode init - get_session_context fallback respects peer param - mid -> medium in reasoning level validation ABC changes (minimal, honcho-only): - run_agent.py: gateway_session_key param + memory provider wiring (+5 lines) - gateway/run.py: skip_memory on 2 temp agents, gateway_session_key on main agent (+3 lines) - agent/memory_manager.py: sanitize regex for context tag variants (+9 lines)
This commit is contained in:
@@ -25,6 +25,7 @@ class TestHonchoClientConfigDefaults:
|
||||
assert config.workspace_id == "hermes"
|
||||
assert config.api_key is None
|
||||
assert config.environment == "production"
|
||||
assert config.timeout is None
|
||||
assert config.enabled is False
|
||||
assert config.save_messages is True
|
||||
assert config.session_strategy == "per-directory"
|
||||
@@ -76,6 +77,11 @@ class TestFromEnv:
|
||||
assert config.base_url == "http://localhost:8000"
|
||||
assert config.enabled is True
|
||||
|
||||
def test_reads_timeout_from_env(self):
|
||||
with patch.dict(os.environ, {"HONCHO_TIMEOUT": "90"}, clear=True):
|
||||
config = HonchoClientConfig.from_env()
|
||||
assert config.timeout == 90.0
|
||||
|
||||
|
||||
class TestFromGlobalConfig:
|
||||
def test_missing_config_falls_back_to_env(self, tmp_path):
|
||||
@@ -87,10 +93,10 @@ class TestFromGlobalConfig:
|
||||
assert config.enabled is False
|
||||
assert config.api_key is None
|
||||
|
||||
def test_reads_full_config(self, tmp_path):
|
||||
def test_reads_full_config(self, tmp_path, monkeypatch):
|
||||
config_file = tmp_path / "config.json"
|
||||
config_file.write_text(json.dumps({
|
||||
"apiKey": "my-honcho-key",
|
||||
"apiKey": "***",
|
||||
"workspace": "my-workspace",
|
||||
"environment": "staging",
|
||||
"peerName": "alice",
|
||||
@@ -108,9 +114,11 @@ class TestFromGlobalConfig:
|
||||
}
|
||||
}
|
||||
}))
|
||||
# Isolate from real ~/.hermes/honcho.json
|
||||
monkeypatch.setenv("HERMES_HOME", str(tmp_path / "isolated"))
|
||||
|
||||
config = HonchoClientConfig.from_global_config(config_path=config_file)
|
||||
assert config.api_key == "my-honcho-key"
|
||||
assert config.api_key == "***"
|
||||
# Host block workspace overrides root workspace
|
||||
assert config.workspace_id == "override-ws"
|
||||
assert config.ai_peer == "override-ai"
|
||||
@@ -154,10 +162,31 @@ class TestFromGlobalConfig:
|
||||
def test_session_strategy_default_from_global_config(self, tmp_path):
|
||||
"""from_global_config with no sessionStrategy should match dataclass default."""
|
||||
config_file = tmp_path / "config.json"
|
||||
config_file.write_text(json.dumps({"apiKey": "key"}))
|
||||
config_file.write_text(json.dumps({"apiKey": "***"}))
|
||||
config = HonchoClientConfig.from_global_config(config_path=config_file)
|
||||
assert config.session_strategy == "per-directory"
|
||||
|
||||
def test_context_tokens_default_is_none(self, tmp_path):
|
||||
"""Default context_tokens should be None (uncapped) unless explicitly set."""
|
||||
config_file = tmp_path / "config.json"
|
||||
config_file.write_text(json.dumps({"apiKey": "***"}))
|
||||
config = HonchoClientConfig.from_global_config(config_path=config_file)
|
||||
assert config.context_tokens is None
|
||||
|
||||
def test_context_tokens_explicit_sets_cap(self, tmp_path):
|
||||
"""Explicit contextTokens in config sets the cap."""
|
||||
config_file = tmp_path / "config.json"
|
||||
config_file.write_text(json.dumps({"apiKey": "***", "contextTokens": 1200}))
|
||||
config = HonchoClientConfig.from_global_config(config_path=config_file)
|
||||
assert config.context_tokens == 1200
|
||||
|
||||
def test_context_tokens_explicit_overrides_default(self, tmp_path):
|
||||
"""Explicit contextTokens in config should override the default."""
|
||||
config_file = tmp_path / "config.json"
|
||||
config_file.write_text(json.dumps({"apiKey": "***", "contextTokens": 2000}))
|
||||
config = HonchoClientConfig.from_global_config(config_path=config_file)
|
||||
assert config.context_tokens == 2000
|
||||
|
||||
def test_context_tokens_host_block_wins(self, tmp_path):
|
||||
"""Host block contextTokens should override root."""
|
||||
config_file = tmp_path / "config.json"
|
||||
@@ -232,6 +261,20 @@ class TestFromGlobalConfig:
|
||||
config = HonchoClientConfig.from_global_config(config_path=config_file)
|
||||
assert config.base_url == "http://root:9000"
|
||||
|
||||
def test_timeout_from_config_root(self, tmp_path):
|
||||
config_file = tmp_path / "config.json"
|
||||
config_file.write_text(json.dumps({"timeout": 75}))
|
||||
|
||||
config = HonchoClientConfig.from_global_config(config_path=config_file)
|
||||
assert config.timeout == 75.0
|
||||
|
||||
def test_request_timeout_alias_from_config_root(self, tmp_path):
|
||||
config_file = tmp_path / "config.json"
|
||||
config_file.write_text(json.dumps({"requestTimeout": "82.5"}))
|
||||
|
||||
config = HonchoClientConfig.from_global_config(config_path=config_file)
|
||||
assert config.timeout == 82.5
|
||||
|
||||
|
||||
class TestResolveSessionName:
|
||||
def test_manual_override(self):
|
||||
@@ -333,13 +376,14 @@ class TestResolveConfigPath:
|
||||
hermes_home.mkdir()
|
||||
local_cfg = hermes_home / "honcho.json"
|
||||
local_cfg.write_text(json.dumps({
|
||||
"apiKey": "local-key",
|
||||
"apiKey": "***",
|
||||
"workspace": "local-ws",
|
||||
}))
|
||||
|
||||
with patch.dict(os.environ, {"HERMES_HOME": str(hermes_home)}):
|
||||
with patch.dict(os.environ, {"HERMES_HOME": str(hermes_home)}), \
|
||||
patch.object(Path, "home", return_value=tmp_path):
|
||||
config = HonchoClientConfig.from_global_config()
|
||||
assert config.api_key == "local-key"
|
||||
assert config.api_key == "***"
|
||||
assert config.workspace_id == "local-ws"
|
||||
|
||||
|
||||
@@ -500,46 +544,103 @@ class TestObservationModeMigration:
|
||||
assert cfg.ai_observe_others is True
|
||||
|
||||
|
||||
class TestInitOnSessionStart:
|
||||
"""Tests for the initOnSessionStart config field."""
|
||||
class TestGetHonchoClient:
|
||||
def teardown_method(self):
|
||||
reset_honcho_client()
|
||||
|
||||
def test_default_is_false(self):
|
||||
def test_passes_timeout_from_config(self):
|
||||
fake_honcho = MagicMock(name="Honcho")
|
||||
cfg = HonchoClientConfig(
|
||||
api_key="test-key",
|
||||
timeout=91.0,
|
||||
workspace_id="hermes",
|
||||
environment="production",
|
||||
)
|
||||
|
||||
with patch("honcho.Honcho", return_value=fake_honcho) as mock_honcho:
|
||||
client = get_honcho_client(cfg)
|
||||
|
||||
assert client is fake_honcho
|
||||
mock_honcho.assert_called_once()
|
||||
assert mock_honcho.call_args.kwargs["timeout"] == 91.0
|
||||
|
||||
def test_hermes_config_timeout_override_used_when_config_timeout_missing(self):
|
||||
fake_honcho = MagicMock(name="Honcho")
|
||||
cfg = HonchoClientConfig(
|
||||
api_key="test-key",
|
||||
workspace_id="hermes",
|
||||
environment="production",
|
||||
)
|
||||
|
||||
with patch("honcho.Honcho", return_value=fake_honcho) as mock_honcho, \
|
||||
patch("hermes_cli.config.load_config", return_value={"honcho": {"timeout": 88}}):
|
||||
client = get_honcho_client(cfg)
|
||||
|
||||
assert client is fake_honcho
|
||||
mock_honcho.assert_called_once()
|
||||
assert mock_honcho.call_args.kwargs["timeout"] == 88.0
|
||||
|
||||
def test_hermes_request_timeout_alias_used(self):
|
||||
fake_honcho = MagicMock(name="Honcho")
|
||||
cfg = HonchoClientConfig(
|
||||
api_key="test-key",
|
||||
workspace_id="hermes",
|
||||
environment="production",
|
||||
)
|
||||
|
||||
with patch("honcho.Honcho", return_value=fake_honcho) as mock_honcho, \
|
||||
patch("hermes_cli.config.load_config", return_value={"honcho": {"request_timeout": "77.5"}}):
|
||||
client = get_honcho_client(cfg)
|
||||
|
||||
assert client is fake_honcho
|
||||
mock_honcho.assert_called_once()
|
||||
assert mock_honcho.call_args.kwargs["timeout"] == 77.5
|
||||
|
||||
|
||||
class TestResolveSessionNameGatewayKey:
|
||||
"""Regression tests for gateway_session_key priority in resolve_session_name.
|
||||
|
||||
Ensures gateway platforms get stable per-chat Honcho sessions even when
|
||||
sessionStrategy=per-session would otherwise create ephemeral sessions.
|
||||
Regression: plugin refactor 924bc67e dropped gateway key plumbing.
|
||||
"""
|
||||
|
||||
def test_gateway_key_overrides_per_session_strategy(self):
|
||||
"""gateway_session_key must win over per-session session_id."""
|
||||
config = HonchoClientConfig(session_strategy="per-session")
|
||||
result = config.resolve_session_name(
|
||||
session_id="20260412_171002_69bb38",
|
||||
gateway_session_key="agent:main:telegram:dm:8439114563",
|
||||
)
|
||||
assert result == "agent-main-telegram-dm-8439114563"
|
||||
|
||||
def test_session_title_still_wins_over_gateway_key(self):
|
||||
"""Explicit /title remap takes priority over gateway_session_key."""
|
||||
config = HonchoClientConfig(session_strategy="per-session")
|
||||
result = config.resolve_session_name(
|
||||
session_title="my-custom-title",
|
||||
session_id="20260412_171002_69bb38",
|
||||
gateway_session_key="agent:main:telegram:dm:8439114563",
|
||||
)
|
||||
assert result == "my-custom-title"
|
||||
|
||||
def test_per_session_fallback_without_gateway_key(self):
|
||||
"""Without gateway_session_key, per-session returns session_id (CLI path)."""
|
||||
config = HonchoClientConfig(session_strategy="per-session")
|
||||
result = config.resolve_session_name(
|
||||
session_id="20260412_171002_69bb38",
|
||||
gateway_session_key=None,
|
||||
)
|
||||
assert result == "20260412_171002_69bb38"
|
||||
|
||||
def test_gateway_key_sanitizes_special_chars(self):
|
||||
"""Colons and other non-alphanumeric chars are replaced with hyphens."""
|
||||
config = HonchoClientConfig()
|
||||
assert config.init_on_session_start is False
|
||||
|
||||
def test_root_level_true(self, tmp_path):
|
||||
cfg_file = tmp_path / "config.json"
|
||||
cfg_file.write_text(json.dumps({
|
||||
"apiKey": "k",
|
||||
"initOnSessionStart": True,
|
||||
}))
|
||||
cfg = HonchoClientConfig.from_global_config(config_path=cfg_file)
|
||||
assert cfg.init_on_session_start is True
|
||||
|
||||
def test_host_block_overrides_root(self, tmp_path):
|
||||
cfg_file = tmp_path / "config.json"
|
||||
cfg_file.write_text(json.dumps({
|
||||
"apiKey": "k",
|
||||
"initOnSessionStart": True,
|
||||
"hosts": {"hermes": {"initOnSessionStart": False}},
|
||||
}))
|
||||
cfg = HonchoClientConfig.from_global_config(config_path=cfg_file)
|
||||
assert cfg.init_on_session_start is False
|
||||
|
||||
def test_host_block_true_overrides_root_absent(self, tmp_path):
|
||||
cfg_file = tmp_path / "config.json"
|
||||
cfg_file.write_text(json.dumps({
|
||||
"apiKey": "k",
|
||||
"hosts": {"hermes": {"initOnSessionStart": True}},
|
||||
}))
|
||||
cfg = HonchoClientConfig.from_global_config(config_path=cfg_file)
|
||||
assert cfg.init_on_session_start is True
|
||||
|
||||
def test_absent_everywhere_defaults_false(self, tmp_path):
|
||||
cfg_file = tmp_path / "config.json"
|
||||
cfg_file.write_text(json.dumps({"apiKey": "k"}))
|
||||
cfg = HonchoClientConfig.from_global_config(config_path=cfg_file)
|
||||
assert cfg.init_on_session_start is False
|
||||
result = config.resolve_session_name(
|
||||
gateway_session_key="agent:main:telegram:dm:8439114563",
|
||||
)
|
||||
assert result == "agent-main-telegram-dm-8439114563"
|
||||
assert ":" not in result
|
||||
|
||||
|
||||
class TestResetHonchoClient:
|
||||
|
||||
Reference in New Issue
Block a user