feat(honcho): wizard cadence default 2, surface reasoning level, backwards-compat fallback

Setup wizard now always writes dialecticCadence=2 on new configs and
surfaces the reasoning level as an explicit step with all five options
(minimal / low / medium / high / max), always writing
dialecticReasoningLevel.

Code keeps a backwards-compat fallback of 1 when dialecticCadence is
unset so existing honcho.json configs that predate the setting keep
firing every turn on upgrade. New setups via the wizard get 2
explicitly; docs show 2 as the default.

Also scrubs editorial lines from code and docs ("max is reserved for
explicit tool-path selection", "Unset → every turn; wizard pre-fills 2",
and similar process-exposing phrasing) and adds an inline link to
app.honcho.dev where the server-side observation sync is mentioned in
honcho.md. Recommended cadence range updated to 1-5 across docs and
wizard copy.
This commit is contained in:
Erosika 2026-04-18 13:49:50 -04:00 committed by kshitij
parent 5b6792f04d
commit 21d5ef2f17
7 changed files with 40 additions and 18 deletions

View file

@ -145,7 +145,7 @@ Controls **how often** dialectic and context calls happen.
| Key | Default | Description | | Key | Default | Description |
|-----|---------|-------------| |-----|---------|-------------|
| `contextCadence` | `1` | Min turns between context API calls | | `contextCadence` | `1` | Min turns between context API calls |
| `dialecticCadence` | `1` (wizard: `2`) | Min turns between dialectic API calls. Unset → every turn; wizard pre-fills `2` | | `dialecticCadence` | `2` | Min turns between dialectic API calls. Recommended 15 |
| `injectionFrequency` | `every-turn` | `every-turn` or `first-turn` for base context injection | | `injectionFrequency` | `every-turn` | `every-turn` or `first-turn` for base context injection |
Higher cadence values fire the dialectic LLM less often. `dialecticCadence: 2` means the engine fires every other turn. Setting it to `1` fires every turn. Higher cadence values fire the dialectic LLM less often. `dialecticCadence: 2` means the engine fires every other turn. Setting it to `1` fires every turn.
@ -370,7 +370,7 @@ Config file: `$HERMES_HOME/honcho.json` (profile-local) or `~/.honcho/config.jso
| `contextTokens` | uncapped | Max tokens for the combined base context injection (summary + representation + card). Opt-in cap — omit to leave uncapped, set to an integer to bound injection size. | | `contextTokens` | uncapped | Max tokens for the combined base context injection (summary + representation + card). Opt-in cap — omit to leave uncapped, set to an integer to bound injection size. |
| `injectionFrequency` | `every-turn` | `every-turn` or `first-turn` | | `injectionFrequency` | `every-turn` | `every-turn` or `first-turn` |
| `contextCadence` | `1` | Min turns between context API calls | | `contextCadence` | `1` | Min turns between context API calls |
| `dialecticCadence` | `1` (wizard: `2`) | Min turns between dialectic LLM calls | | `dialecticCadence` | `2` | Min turns between dialectic LLM calls (recommended 15) |
The `contextTokens` budget is enforced at injection time. If the session summary + representation + card exceed the budget, Honcho trims the summary first, then the representation, preserving the card. This prevents context blowup in long sessions. The `contextTokens` budget is enforced at injection time. If the session summary + representation + card exceed the budget, Honcho trims the summary first, then the representation, preserving the card. This prevents context blowup in long sessions.

View file

@ -207,7 +207,7 @@ class HonchoMemoryProvider(MemoryProvider):
self._turn_count = 0 self._turn_count = 0
self._injection_frequency = "every-turn" # or "first-turn" self._injection_frequency = "every-turn" # or "first-turn"
self._context_cadence = 1 # minimum turns between context API calls self._context_cadence = 1 # minimum turns between context API calls
self._dialectic_cadence = 1 # minimum turns between dialectic API calls self._dialectic_cadence = 1 # backwards-compat fallback; wizard writes 2 on new configs
self._dialectic_depth = 1 # how many .chat() calls per dialectic cycle (1-3) self._dialectic_depth = 1 # how many .chat() calls per dialectic cycle (1-3)
self._dialectic_depth_levels: list[str] | None = None # per-pass reasoning levels self._dialectic_depth_levels: list[str] | None = None # per-pass reasoning levels
self._reasoning_heuristic: bool = True # scale base level by query length self._reasoning_heuristic: bool = True # scale base level by query length
@ -304,6 +304,10 @@ class HonchoMemoryProvider(MemoryProvider):
raw = cfg.raw or {} raw = cfg.raw or {}
self._injection_frequency = raw.get("injectionFrequency", "every-turn") self._injection_frequency = raw.get("injectionFrequency", "every-turn")
self._context_cadence = int(raw.get("contextCadence", 1)) self._context_cadence = int(raw.get("contextCadence", 1))
# Backwards-compat: unset dialecticCadence falls back to 1
# (every turn) so existing honcho.json configs without the key
# behave as they did before. New setups via `hermes honcho setup`
# get dialecticCadence=2 written explicitly by the wizard.
self._dialectic_cadence = int(raw.get("dialecticCadence", 1)) self._dialectic_cadence = int(raw.get("dialecticCadence", 1))
self._dialectic_depth = max(1, min(cfg.dialectic_depth, 3)) self._dialectic_depth = max(1, min(cfg.dialectic_depth, 3))
self._dialectic_depth_levels = cfg.dialectic_depth_levels self._dialectic_depth_levels = cfg.dialectic_depth_levels
@ -844,9 +848,7 @@ class HonchoMemoryProvider(MemoryProvider):
def _apply_reasoning_heuristic(self, base: str, query: str) -> str: def _apply_reasoning_heuristic(self, base: str, query: str) -> str:
"""Scale `base` up by query length, clamped at reasoning_level_cap. """Scale `base` up by query length, clamped at reasoning_level_cap.
Char-count heuristic: +1 at >=120 chars, +2 at >=400. Ceiling is Char-count heuristic: +1 at >=120 chars, +2 at >=400.
reasoning_level_cap (default 'high' 'max' is reserved for
explicit tool-path selection).
""" """
if not self._reasoning_heuristic or not query: if not self._reasoning_heuristic or not query:
return base return base

View file

@ -463,7 +463,8 @@ def cmd_setup(args) -> None:
current_dialectic = str(hermes_host.get("dialecticCadence") or cfg.get("dialecticCadence") or "2") current_dialectic = str(hermes_host.get("dialecticCadence") or cfg.get("dialecticCadence") or "2")
print("\n Dialectic cadence:") print("\n Dialectic cadence:")
print(" How often Honcho rebuilds its user model (LLM call on Honcho backend).") print(" How often Honcho rebuilds its user model (LLM call on Honcho backend).")
print(" 1 = every turn, 2 = every other turn (wizard default), 3+ = sparse.") print(" 1 = every turn, 2 = every other turn, 3+ = sparser.")
print(" Recommended: 1-5.")
new_dialectic = _prompt("Dialectic cadence", default=current_dialectic) new_dialectic = _prompt("Dialectic cadence", default=current_dialectic)
try: try:
val = int(new_dialectic) val = int(new_dialectic)
@ -472,6 +473,25 @@ def cmd_setup(args) -> None:
except (ValueError, TypeError): except (ValueError, TypeError):
hermes_host["dialecticCadence"] = 2 hermes_host["dialecticCadence"] = 2
# --- 7c. Dialectic reasoning level ---
current_reasoning = (
hermes_host.get("dialecticReasoningLevel")
or cfg.get("dialecticReasoningLevel")
or "low"
)
print("\n Dialectic reasoning level:")
print(" Depth Honcho uses when synthesizing user context on auto-injected calls.")
print(" minimal -- quick factual lookups")
print(" low -- straightforward questions (default)")
print(" medium -- multi-aspect synthesis")
print(" high -- complex behavioral patterns")
print(" max -- thorough audit-level analysis")
new_reasoning = _prompt("Reasoning level", default=current_reasoning)
if new_reasoning in ("minimal", "low", "medium", "high", "max"):
hermes_host["dialecticReasoningLevel"] = new_reasoning
else:
hermes_host["dialecticReasoningLevel"] = "low"
# --- 8. Session strategy --- # --- 8. Session strategy ---
current_strat = hermes_host.get("sessionStrategy") or cfg.get("sessionStrategy", "per-session") current_strat = hermes_host.get("sessionStrategy") or cfg.get("sessionStrategy", "per-session")
print("\n Session strategy:") print("\n Session strategy:")

View file

@ -254,8 +254,7 @@ class HonchoClientConfig:
# When true, the auto-injected dialectic scales reasoning level up on # When true, the auto-injected dialectic scales reasoning level up on
# longer queries. See HonchoMemoryProvider for thresholds. # longer queries. See HonchoMemoryProvider for thresholds.
reasoning_heuristic: bool = True reasoning_heuristic: bool = True
# Ceiling for the heuristic-selected reasoning level. "max" is reserved # Ceiling for the heuristic-selected reasoning level.
# for explicit tool-path selection.
reasoning_level_cap: str = "high" reasoning_level_cap: str = "high"
# Honcho API limits — configurable for self-hosted instances # Honcho API limits — configurable for self-hosted instances
# Max chars per message sent via add_messages() (Honcho cloud: 25000) # Max chars per message sent via add_messages() (Honcho cloud: 25000)

View file

@ -865,8 +865,10 @@ class TestDialecticCadenceDefaults:
_settle_prewarm(provider) _settle_prewarm(provider)
return provider return provider
def test_default_is_1(self): def test_unset_falls_back_to_1(self):
"""Default dialectic_cadence is 1 — fires every turn unless overridden.""" """Unset dialecticCadence falls back to 1 (every turn) for backwards
compatibility with existing configs that predate the setting. The
setup wizard writes 2 explicitly on new configs."""
provider = self._make_provider() provider = self._make_provider()
assert provider._dialectic_cadence == 1 assert provider._dialectic_cadence == 1
@ -1569,8 +1571,7 @@ class TestDialecticLifecycleSmoke:
class TestReasoningHeuristic: class TestReasoningHeuristic:
"""Char-count heuristic that scales the auto-injected reasoning level by """Char-count heuristic that scales the auto-injected reasoning level by
query length, clamped at reasoning_level_cap. 'max' is reserved for query length, clamped at reasoning_level_cap."""
explicit tool-path selection."""
@staticmethod @staticmethod
def _make_provider(cfg_extra=None): def _make_provider(cfg_extra=None):

View file

@ -77,7 +77,7 @@ Cost and depth are controlled by three independent knobs:
| Knob | Controls | Default | | Knob | Controls | Default |
|------|----------|---------| |------|----------|---------|
| `contextCadence` | Turns between `context()` API calls (base layer refresh) | `1` | | `contextCadence` | Turns between `context()` API calls (base layer refresh) | `1` |
| `dialecticCadence` | Turns between `peer.chat()` LLM calls (dialectic layer refresh) | `1` (code default) / `2` (setup wizard default) | | `dialecticCadence` | Turns between `peer.chat()` LLM calls (dialectic layer refresh) | `2` (recommended 15) |
| `dialecticDepth` | Number of `.chat()` passes per dialectic invocation (13) | `1` | | `dialecticDepth` | Number of `.chat()` passes per dialectic invocation (13) | `1` |
These are orthogonal — you can have frequent context refreshes with infrequent dialectic, or deep multi-pass dialectic at low frequency. Example: `contextCadence: 1, dialecticCadence: 5, dialecticDepth: 2` refreshes base context every turn, runs dialectic every 5 turns, and each dialectic run makes 2 passes. These are orthogonal — you can have frequent context refreshes with infrequent dialectic, or deep multi-pass dialectic at low frequency. Example: `contextCadence: 1, dialecticCadence: 5, dialecticDepth: 2` refreshes base context every turn, runs dialectic every 5 turns, and each dialectic run makes 2 passes.
@ -100,7 +100,7 @@ On session init, Honcho fires a dialectic call in the background at the full con
### Query-Adaptive Reasoning Level ### Query-Adaptive Reasoning Level
The auto-injected dialectic scales `dialecticReasoningLevel` by query length: +1 level at ≥120 chars, +2 at ≥400, clamped at `reasoningLevelCap` (default `"high"`). Disable with `reasoningHeuristic: false` to pin every auto call to `dialecticReasoningLevel`. `"max"` is reserved for explicit tool-path selection via `honcho_reasoning`. The auto-injected dialectic scales `dialecticReasoningLevel` by query length: +1 level at ≥120 chars, +2 at ≥400, clamped at `reasoningLevelCap` (default `"high"`). Disable with `reasoningHeuristic: false` to pin every auto call to `dialecticReasoningLevel`. Available levels: `minimal`, `low`, `medium`, `high`, `max`.
## Configuration Options ## Configuration Options
@ -112,7 +112,7 @@ Honcho is configured in `~/.honcho/config.json` (global) or `$HERMES_HOME/honcho
|-----|---------|-------------| |-----|---------|-------------|
| `contextTokens` | `null` (uncapped) | Token budget for auto-injected context per turn. Set to an integer (e.g. 1200) to cap. Truncates at word boundaries | | `contextTokens` | `null` (uncapped) | Token budget for auto-injected context per turn. Set to an integer (e.g. 1200) to cap. Truncates at word boundaries |
| `contextCadence` | `1` | Minimum turns between `context()` API calls (base layer refresh) | | `contextCadence` | `1` | Minimum turns between `context()` API calls (base layer refresh) |
| `dialecticCadence` | `1` (wizard sets `2`) | Minimum turns between `peer.chat()` LLM calls (dialectic layer). Code default fires every turn when the key is unset; the setup wizard pre-fills `2`. In `tools` mode, irrelevant — model calls explicitly | | `dialecticCadence` | `2` | Minimum turns between `peer.chat()` LLM calls (dialectic layer). Recommended 15. In `tools` mode, irrelevant — model calls explicitly |
| `dialecticDepth` | `1` | Number of `.chat()` passes per dialectic invocation. Clamped to 13 | | `dialecticDepth` | `1` | Number of `.chat()` passes per dialectic invocation. Clamped to 13 |
| `dialecticDepthLevels` | `null` | Optional array of reasoning levels per pass, e.g. `["minimal", "low", "medium"]`. Overrides proportional defaults | | `dialecticDepthLevels` | `null` | Optional array of reasoning levels per pass, e.g. `["minimal", "low", "medium"]`. Overrides proportional defaults |
| `dialecticReasoningLevel` | `'low'` | Base reasoning level: `minimal`, `low`, `medium`, `high`, `max` | | `dialecticReasoningLevel` | `'low'` | Base reasoning level: `minimal`, `low`, `medium`, `high`, `max` |
@ -183,7 +183,7 @@ Common patterns:
| AI shouldn't re-model the user from its own replies | `"ai": {"observeMe": true, "observeOthers": false}` | | AI shouldn't re-model the user from its own replies | `"ai": {"observeMe": true, "observeOthers": false}` |
| Strong persona the AI peer shouldn't update from self-observation | `"ai": {"observeMe": false, "observeOthers": true}` | | Strong persona the AI peer shouldn't update from self-observation | `"ai": {"observeMe": false, "observeOthers": true}` |
Server-side toggles set via the Honcho dashboard win over local defaults — Hermes syncs them back at session init. Server-side toggles set via the [Honcho dashboard](https://app.honcho.dev) win over local defaults — Hermes syncs them back at session init.
## Tools ## Tools

View file

@ -82,7 +82,7 @@ hermes memory setup # select "honcho"
| `workspace` | host key | Shared workspace ID | | `workspace` | host key | Shared workspace ID |
| `contextTokens` | `null` (uncapped) | Token budget for auto-injected context per turn. Truncates at word boundaries | | `contextTokens` | `null` (uncapped) | Token budget for auto-injected context per turn. Truncates at word boundaries |
| `contextCadence` | `1` | Minimum turns between `context()` API calls (base layer refresh) | | `contextCadence` | `1` | Minimum turns between `context()` API calls (base layer refresh) |
| `dialecticCadence` | `1` (wizard sets `2`) | Minimum turns between `peer.chat()` LLM calls. Unset → every turn; wizard pre-fills `2`. Only applies to `hybrid`/`context` modes | | `dialecticCadence` | `2` | Minimum turns between `peer.chat()` LLM calls. Recommended 15. Only applies to `hybrid`/`context` modes |
| `dialecticDepth` | `1` | Number of `.chat()` passes per dialectic invocation. Clamped 13. Pass 0: cold/warm prompt, pass 1: self-audit, pass 2: reconciliation | | `dialecticDepth` | `1` | Number of `.chat()` passes per dialectic invocation. Clamped 13. Pass 0: cold/warm prompt, pass 1: self-audit, pass 2: reconciliation |
| `dialecticDepthLevels` | `null` | Optional array of reasoning levels per pass, e.g. `["minimal", "low", "medium"]`. Overrides proportional defaults | | `dialecticDepthLevels` | `null` | Optional array of reasoning levels per pass, e.g. `["minimal", "low", "medium"]`. Overrides proportional defaults |
| `dialecticReasoningLevel` | `'low'` | Base reasoning level: `minimal`, `low`, `medium`, `high`, `max` | | `dialecticReasoningLevel` | `'low'` | Base reasoning level: `minimal`, `low`, `medium`, `high`, `max` |