Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
57 changes: 47 additions & 10 deletions honcho_integration/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -270,6 +270,24 @@ 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'}")
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)"
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:
Expand Down Expand Up @@ -423,18 +441,37 @@ 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")]
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]")
print(f" Edit {_config_path()} for cadence, injection, and reasoning cap.\n")
return

changed = False
Expand Down
60 changes: 60 additions & 0 deletions honcho_integration/client.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -116,8 +128,29 @@ 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"
# 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
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
Expand Down Expand Up @@ -263,11 +296,38 @@ 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"
),
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),
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")
Expand Down
45 changes: 43 additions & 2 deletions honcho_integration/session.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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(
Expand Down Expand Up @@ -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.
Expand All @@ -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:
Expand Down Expand Up @@ -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:
Expand Down
31 changes: 25 additions & 6 deletions run_agent.py
Original file line number Diff line number Diff line change
Expand Up @@ -2447,33 +2447,52 @@ 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 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)
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:
Expand Down
Loading