From 2022fa146a72ac07ba2ab3d2c5545fce10e66482 Mon Sep 17 00:00:00 2001 From: Erosika Date: Fri, 27 Mar 2026 13:47:34 -0400 Subject: [PATCH 1/3] feat(honcho): configurable prefetch cadence, injection toggles, and reasoning cap Cost-awareness defaults for Honcho integration: - Prefetch cadence: context and dialectic default to "first-turn" instead of unconditional per-turn fetches. Configurable via contextCadence / dialecticCadence in honcho.yaml ("first-turn", "every-turn", or int N). - Injection toggles: per-component control over what gets injected into the system prompt. injectRepresentation, injectCard, injectDialectic default true; injectAiRepresentation, injectAiCard default false (rarely needed, saves tokens). - Reasoning cap: dialecticReasoningCap (default: same as floor) prevents _dynamic_reasoning_level from auto-bumping past the configured ceiling. Stops cost escalation from long messages triggering high reasoning. Existing honcho.yaml files with no new fields get cost-efficient defaults automatically. Users who want legacy per-turn behavior can set cadence to "every-turn" and raise the reasoning cap. Closes #3422 --- honcho_integration/client.py | 49 +++++++++++++++++++++++++++++++++++ honcho_integration/session.py | 45 ++++++++++++++++++++++++++++++-- run_agent.py | 19 +++++++++----- 3 files changed, 105 insertions(+), 8 deletions(-) diff --git a/honcho_integration/client.py b/honcho_integration/client.py index 50f7af30a28d7..c15ca7f6fd684 100644 --- a/honcho_integration/client.py +++ b/honcho_integration/client.py @@ -48,6 +48,18 @@ def resolve_config_path() -> Path: _VALID_RECALL_MODES = {"hybrid", "context", "tools"} +def _bool_opt(host_block: dict, raw: dict, key: str, default: bool) -> bool: + """Resolve a boolean config option with host-level override.""" + val = host_block.get(key) + if val is None: + val = raw.get(key) + if val is None: + return default + if isinstance(val, bool): + return val + return str(val).strip().lower() in ("true", "1", "yes", "on") + + def _normalize_recall_mode(val: str) -> str: """Normalize legacy recall mode values (e.g. 'auto' → 'hybrid').""" val = _RECALL_MODE_ALIASES.get(val, val) @@ -116,8 +128,23 @@ def peer_memory_mode(self, peer_name: str) -> str: # reasoning_level: "minimal" | "low" | "medium" | "high" | "max" # Used as the default; prefetch_dialectic may bump it dynamically. dialectic_reasoning_level: str = "low" + # Ceiling for auto-bump. When equal to dialectic_reasoning_level, auto-bump + # is disabled. Set higher to allow dynamic escalation up to the cap. + dialectic_reasoning_cap: str = "low" # Max chars of dialectic result to inject into Hermes system prompt dialectic_max_chars: int = 600 + # Prefetch cadence: which components to inject and how often. + # "first-turn" = session start only (default, cost-efficient). + # "every-turn" = unconditional per-turn fetch (legacy behavior). + # Integer N = every N turns. + dialectic_cadence: str | int = "first-turn" + context_cadence: str | int = "first-turn" + # Per-component injection toggles + inject_representation: bool = True + inject_card: bool = True + inject_ai_representation: bool = False + inject_ai_card: bool = False + inject_dialectic: bool = True # Recall mode: how memory retrieval works when Honcho is active. # "hybrid" — auto-injected context + Honcho tools available (model decides) # "context" — auto-injected context only, Honcho tools removed @@ -263,11 +290,33 @@ def from_global_config( or raw.get("dialecticReasoningLevel") or "low" ), + dialectic_reasoning_cap=( + host_block.get("dialecticReasoningCap") + or raw.get("dialecticReasoningCap") + or host_block.get("dialecticReasoningLevel") + or raw.get("dialecticReasoningLevel") + or "low" + ), dialectic_max_chars=int( host_block.get("dialecticMaxChars") or raw.get("dialecticMaxChars") or 600 ), + dialectic_cadence=( + host_block.get("dialecticCadence") + or raw.get("dialecticCadence") + or "first-turn" + ), + context_cadence=( + host_block.get("contextCadence") + or raw.get("contextCadence") + or "first-turn" + ), + inject_representation=_bool_opt(host_block, raw, "injectRepresentation", True), + inject_card=_bool_opt(host_block, raw, "injectCard", True), + inject_ai_representation=_bool_opt(host_block, raw, "injectAiRepresentation", False), + inject_ai_card=_bool_opt(host_block, raw, "injectAiCard", False), + inject_dialectic=_bool_opt(host_block, raw, "injectDialectic", True), recall_mode=_normalize_recall_mode( host_block.get("recallMode") or raw.get("recallMode") diff --git a/honcho_integration/session.py b/honcho_integration/session.py index 23b96d1cb196c..31f85f62eea5b 100644 --- a/honcho_integration/session.py +++ b/honcho_integration/session.py @@ -107,9 +107,15 @@ def __init__( self._dialectic_reasoning_level: str = ( config.dialectic_reasoning_level if config else "low" ) + self._dialectic_reasoning_cap: str = ( + config.dialectic_reasoning_cap if config else "low" + ) self._dialectic_max_chars: int = ( config.dialectic_max_chars if config else 600 ) + self._dialectic_cadence = config.dialectic_cadence if config else "first-turn" + self._context_cadence = config.context_cadence if config else "first-turn" + self._turn_counts: dict[str, int] = {} # session_key → turn number # Async write queue — started lazily on first enqueue self._async_queue: queue.Queue | None = None @@ -457,8 +463,9 @@ def _dynamic_reasoning_level(self, query: str) -> str: bump = 1 else: bump = 2 - # Cap at "high" (index 3) for auto-selection - idx = min(default_idx + bump, 3) + # Cap at configured ceiling (default: same as floor = no bump) + cap_idx = levels.index(self._dialectic_reasoning_cap) if self._dialectic_reasoning_cap in levels else default_idx + idx = min(default_idx + bump, cap_idx) return levels[idx] def dialectic_query( @@ -501,6 +508,28 @@ def dialectic_query( logger.warning("Honcho dialectic query failed: %s", e) return "" + def _should_prefetch(self, session_key: str, cadence: str | int) -> bool: + """Check whether a prefetch should fire based on cadence config. + + Cadence values: + "first-turn" — only on turn 0 (session start) + "every-turn" — unconditional (legacy behavior) + int N — every N turns (0 = first-turn only) + """ + turn = self._turn_counts.get(session_key, 0) + if cadence == "every-turn": + return True + if cadence == "first-turn" or cadence == 0: + return turn == 0 + if isinstance(cadence, int) and cadence > 0: + return turn % cadence == 0 + # Unknown cadence string — treat as first-turn + return turn == 0 + + def increment_turn(self, session_key: str) -> None: + """Advance the turn counter for cadence tracking.""" + self._turn_counts[session_key] = self._turn_counts.get(session_key, 0) + 1 + def prefetch_dialectic(self, session_key: str, query: str) -> None: """ Fire a dialectic_query in a background thread, caching the result. @@ -509,10 +538,16 @@ def prefetch_dialectic(self, session_key: str, query: str) -> None: on the next call (typically the following turn). Reasoning level is selected dynamically based on query complexity. + Respects dialectic_cadence: skips the call when cadence says + this turn doesn't need it. + Args: session_key: The session key to query against. query: The user's current message, used as the query. """ + if not self._should_prefetch(session_key, self._dialectic_cadence): + return + def _run(): result = self.dialectic_query(session_key, query) if result: @@ -543,7 +578,13 @@ def prefetch_context(self, session_key: str, user_message: str | None = None) -> Non-blocking. Consumed next turn via pop_context_result(). This avoids a synchronous HTTP round-trip blocking every response. + + Respects context_cadence: skips the call when cadence says this turn + doesn't need it. """ + if not self._should_prefetch(session_key, self._context_cadence): + return + def _run(): result = self.get_prefetch_context(session_key, user_message) if result: diff --git a/run_agent.py b/run_agent.py index 30453c01ce852..d1cc30b8fffc7 100644 --- a/run_agent.py +++ b/run_agent.py @@ -2447,33 +2447,40 @@ def _queue_honcho_prefetch(self, user_message: str) -> None: try: self._honcho.prefetch_context(self._honcho_session_key, user_message) self._honcho.prefetch_dialectic(self._honcho_session_key, user_message or "What were we working on?") + self._honcho.increment_turn(self._honcho_session_key) except Exception as exc: logger.debug("Honcho background prefetch failed (non-fatal): %s", exc) def _honcho_prefetch(self, user_message: str) -> str: - """Assemble the first-turn Honcho context from the pre-warmed cache.""" + """Assemble Honcho context from the pre-warmed cache. + + Respects per-component injection toggles from honcho.yaml so users + can disable representation, card, AI peer data, or dialectic + independently. + """ if not self._honcho or not self._honcho_session_key: return "" try: + hcfg = getattr(self, '_honcho_config', None) parts = [] ctx = self._honcho.pop_context_result(self._honcho_session_key) if ctx: rep = ctx.get("representation", "") card = ctx.get("card", "") - if rep: + if rep and (not hcfg or hcfg.inject_representation): parts.append(f"## User representation\n{rep}") - if card: + if card and (not hcfg or hcfg.inject_card): parts.append(card) ai_rep = ctx.get("ai_representation", "") ai_card = ctx.get("ai_card", "") - if ai_rep: + if ai_rep and (not hcfg or hcfg.inject_ai_representation): parts.append(f"## AI peer representation\n{ai_rep}") - if ai_card: + if ai_card and (not hcfg or hcfg.inject_ai_card): parts.append(ai_card) dialectic = self._honcho.pop_dialectic_result(self._honcho_session_key) - if dialectic: + if dialectic and (not hcfg or hcfg.inject_dialectic): parts.append(f"## Continuity synthesis\n{dialectic}") if not parts: From 99b1f7a68c03765b4af49e58e273c43815402c95 Mon Sep 17 00:00:00 2001 From: Erosika Date: Fri, 27 Mar 2026 13:51:28 -0400 Subject: [PATCH 2/3] feat(honcho): surface cost-awareness settings in CLI status and tokens commands hermes honcho status now shows: prefetch cadence, injection toggles, reasoning level with cap info. hermes honcho tokens now shows: cadence per component, injection summary (enabled/suppressed), reasoning cap alongside level. Users can see exactly what Honcho is doing per-turn and what's costing them money. --- honcho_integration/cli.py | 55 ++++++++++++++++++++++++++++++++------- 1 file changed, 45 insertions(+), 10 deletions(-) diff --git a/honcho_integration/cli.py b/honcho_integration/cli.py index ae09c3713419c..e7de699af4f93 100644 --- a/honcho_integration/cli.py +++ b/honcho_integration/cli.py @@ -270,6 +270,23 @@ def cmd_status(args) -> None: print(f" {peer}: {mode}") print(f" Write freq: {hcfg.write_frequency}") + # Prefetch cadence + print(f"\n Context cadence: {hcfg.context_cadence}") + print(f" Dialectic cadence: {hcfg.dialectic_cadence}") + + # Injection toggles + _inj = [] + if hcfg.inject_representation: _inj.append("representation") + if hcfg.inject_card: _inj.append("card") + if hcfg.inject_ai_representation: _inj.append("ai-rep") + if hcfg.inject_ai_card: _inj.append("ai-card") + if hcfg.inject_dialectic: _inj.append("dialectic") + print(f" Injecting: {', '.join(_inj) or 'nothing'}") + + # Reasoning + cap_info = f" (cap: {hcfg.dialectic_reasoning_cap})" if hcfg.dialectic_reasoning_cap != hcfg.dialectic_reasoning_level else " (no auto-bump)" + print(f" Reasoning level: {hcfg.dialectic_reasoning_level}{cap_info}") + if hcfg.enabled and (hcfg.api_key or hcfg.base_url): print("\n Connection... ", end="", flush=True) try: @@ -423,18 +440,36 @@ def cmd_tokens(args) -> None: ctx_tokens = hermes.get("contextTokens") or cfg.get("contextTokens") or "(Honcho default)" d_chars = hermes.get("dialecticMaxChars") or cfg.get("dialecticMaxChars") or 600 d_level = hermes.get("dialecticReasoningLevel") or cfg.get("dialecticReasoningLevel") or "low" - print("\nHoncho budgets\n" + "─" * 40) + d_cap = hermes.get("dialecticReasoningCap") or cfg.get("dialecticReasoningCap") or d_level + ctx_cadence = hermes.get("contextCadence") or cfg.get("contextCadence") or "first-turn" + d_cadence = hermes.get("dialecticCadence") or cfg.get("dialecticCadence") or "first-turn" + print("\nHoncho budgets\n" + "=" * 40) + print() + print(f" Context {ctx_tokens} tokens cadence: {ctx_cadence}") + print(" Peer representation + card. Fetched from Honcho and injected") + print(" into the system prompt. 'first-turn' fetches once per session.") print() - print(f" Context {ctx_tokens} tokens") - print(" Raw memory retrieval. Honcho returns stored facts/history about") - print(" the user and session, injected directly into the system prompt.") + cap_note = f"cap: {d_cap}" if d_cap != d_level else "no auto-bump" + print(f" Dialectic {d_chars} chars reasoning: {d_level} ({cap_note}) cadence: {d_cadence}") + print(" AI-to-AI inference. Honcho runs its own model to synthesize") + print(" session continuity. Higher reasoning and cadence increase cost.") print() - print(f" Dialectic {d_chars} chars, reasoning: {d_level}") - print(" AI-to-AI inference. Hermes asks Honcho's AI peer a question") - print(" (e.g. \"what were we working on?\") and Honcho runs its own model") - print(" to synthesize an answer. Used for first-turn session continuity.") - print(" Level controls how much reasoning Honcho spends on the answer.") - print("\n Set with: hermes honcho tokens [--context N] [--dialectic N]\n") + # Injection summary + _on = lambda k, d: hermes.get(k, cfg.get(k, d)) + inj_items = [ + ("representation", _on("injectRepresentation", True)), + ("card", _on("injectCard", True)), + ("ai-rep", _on("injectAiRepresentation", False)), + ("ai-card", _on("injectAiCard", False)), + ("dialectic", _on("injectDialectic", True)), + ] + enabled = [name for name, val in inj_items if val not in (False, "false", "0", "no")] + disabled = [name for name, val in inj_items if val in (False, "false", "0", "no")] + print(f" Injecting {', '.join(enabled) or 'nothing'}") + if disabled: + print(f" Suppressed {', '.join(disabled)}") + print(f"\n Set with: hermes honcho tokens [--context N] [--dialectic N]") + print(f" Edit {_config_path()} for cadence, injection, and reasoning cap.\n") return changed = False From 0de4baaaa9e2e8523af121188336ddd4d8538fca Mon Sep 17 00:00:00 2001 From: Erosika Date: Fri, 27 Mar 2026 14:02:12 -0400 Subject: [PATCH 3/3] feat(honcho): add injectionFrequency to control how long cached context stays in prompt MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Cadence controls when Honcho API calls fire. injectionFrequency controls how many turns the cached result stays in the system prompt — this is where the main LLM input token cost comes from. Options: "every-turn" (default, legacy), "first-turn" (inject once then drop), or integer N (inject for first N turns then suppress). Surfaced in hermes honcho status and hermes honcho tokens. --- honcho_integration/cli.py | 4 +++- honcho_integration/client.py | 11 +++++++++++ run_agent.py | 18 +++++++++++++++--- 3 files changed, 29 insertions(+), 4 deletions(-) diff --git a/honcho_integration/cli.py b/honcho_integration/cli.py index e7de699af4f93..723be1cbf0622 100644 --- a/honcho_integration/cli.py +++ b/honcho_integration/cli.py @@ -282,6 +282,7 @@ def cmd_status(args) -> None: if hcfg.inject_ai_card: _inj.append("ai-card") if hcfg.inject_dialectic: _inj.append("dialectic") print(f" Injecting: {', '.join(_inj) or 'nothing'}") + print(f" Inject frequency: {hcfg.injection_frequency}") # Reasoning cap_info = f" (cap: {hcfg.dialectic_reasoning_cap})" if hcfg.dialectic_reasoning_cap != hcfg.dialectic_reasoning_level else " (no auto-bump)" @@ -465,7 +466,8 @@ def cmd_tokens(args) -> None: ] enabled = [name for name, val in inj_items if val not in (False, "false", "0", "no")] disabled = [name for name, val in inj_items if val in (False, "false", "0", "no")] - print(f" Injecting {', '.join(enabled) or 'nothing'}") + inj_freq = hermes.get("injectionFrequency") or cfg.get("injectionFrequency") or "every-turn" + print(f" Injecting {', '.join(enabled) or 'nothing'} frequency: {inj_freq}") if disabled: print(f" Suppressed {', '.join(disabled)}") print(f"\n Set with: hermes honcho tokens [--context N] [--dialectic N]") diff --git a/honcho_integration/client.py b/honcho_integration/client.py index c15ca7f6fd684..c5ecea2bf8503 100644 --- a/honcho_integration/client.py +++ b/honcho_integration/client.py @@ -139,6 +139,12 @@ def peer_memory_mode(self, peer_name: str) -> str: # Integer N = every N turns. dialectic_cadence: str | int = "first-turn" context_cadence: str | int = "first-turn" + # Injection frequency: how many turns the cached Honcho context stays in + # the system prompt. Controls LLM input token cost. + # "first-turn" = inject on turn 0 only (model absorbs it, then it's dropped) + # "every-turn" = always inject (legacy behavior, costs tokens every turn) + # Integer N = inject for the first N turns, then suppress + injection_frequency: str | int = "every-turn" # Per-component injection toggles inject_representation: bool = True inject_card: bool = True @@ -312,6 +318,11 @@ def from_global_config( or raw.get("contextCadence") or "first-turn" ), + injection_frequency=( + host_block.get("injectionFrequency") + or raw.get("injectionFrequency") + or "every-turn" + ), inject_representation=_bool_opt(host_block, raw, "injectRepresentation", True), inject_card=_bool_opt(host_block, raw, "injectCard", True), inject_ai_representation=_bool_opt(host_block, raw, "injectAiRepresentation", False), diff --git a/run_agent.py b/run_agent.py index d1cc30b8fffc7..ebeee3d748428 100644 --- a/run_agent.py +++ b/run_agent.py @@ -2454,14 +2454,26 @@ def _queue_honcho_prefetch(self, user_message: str) -> None: def _honcho_prefetch(self, user_message: str) -> str: """Assemble Honcho context from the pre-warmed cache. - Respects per-component injection toggles from honcho.yaml so users - can disable representation, card, AI peer data, or dialectic - independently. + Respects per-component injection toggles and injection_frequency + from honcho.yaml. injection_frequency controls how many turns the + cached context stays in the system prompt — after that, it's dropped + to save LLM input tokens. """ if not self._honcho or not self._honcho_session_key: return "" try: hcfg = getattr(self, '_honcho_config', None) + + # Check injection frequency — skip if past the configured window + if hcfg: + freq = hcfg.injection_frequency + turn = self._honcho._turn_counts.get(self._honcho_session_key, 0) + if freq == "first-turn" and turn > 0: + return "" + elif isinstance(freq, int) and freq > 0 and turn >= freq: + return "" + # "every-turn" or unrecognized: always inject + parts = [] ctx = self._honcho.pop_context_result(self._honcho_session_key)