Skip to content
This repository was archived by the owner on May 26, 2026. It is now read-only.
Merged
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
29 changes: 29 additions & 0 deletions kora_cli/reasoning/kora_hermes_plugin/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
"""KoraHermesPlugin package — canonical Kora-side home for the
Hermes-plugin behaviors that drive Kora's reasoning route-through.

Per KR-PLUGIN-COST-LADDER: this package is the new canonical
location for the Kora plugin code. The Hermes plugin discovery
entry at ``plugins/kora_hermes/`` re-exports from here so
``PluginManager.discover_and_load`` picks the plugin up while
the actual implementation lives in this importable Kora package
(future-proof for ``pip install kora-cost-ladder-plugin``-shape
distribution).

Sub-plugins:
- ``cost_ladder/`` — first extraction (this bucket). Owns the
``pre_api_request_mutable`` hook (model selection + cache
markers).
- (future) ``audit/`` — KR-PLUGIN-AUDIT will move audit emit
here (post_tool_call + post_llm_call audit JSONL writes)
- (future) ``caching/`` — KR-PLUGIN-CACHING will split the
caching half from cost_ladder
- (future) ``short_circuit/`` — KR-PLUGIN-SHORT-CIRCUIT
- (future) ``state_holders/`` — KR-PLUGIN-STATE-HOLDERS
"""

from kora_cli.reasoning.kora_hermes_plugin.plugin import (
KoraHermesPlugin,
register,
)

__all__ = ["KoraHermesPlugin", "register"]
55 changes: 55 additions & 0 deletions kora_cli/reasoning/kora_hermes_plugin/cost_ladder/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
"""Cost-ladder sub-plugin — default-Haiku with earned Opus escalation.

See ``selector.py`` for the pure decision functions,
``constants.py`` for env-var names + defaults, ``plugin.py``
for the ``pre_api_request_mutable`` hook handler + sub-register.

Public surface (re-exported for both plugin discovery and the
backward-compat shim at ``kora_cli/router/cost_router.py``):
"""

from kora_cli.reasoning.kora_hermes_plugin.cost_ladder.constants import (
DEFAULT_DECISION_PATTERNS,
DEFAULT_HAIKU_LOW_CONFIDENCE_PATTERNS,
DEFAULT_HAIKU_MODEL,
DEFAULT_OPUS_MODEL,
ENV_FORCE_OPUS,
ENV_HAIKU_LOW_CONFIDENCE_PATTERNS,
ENV_OPUS_PREFIX,
ENV_OPUS_TRIGGER_PATTERNS,
RUNG_DOWNSHIFT_90,
RUNG_HARD_STOP_100,
RUNG_NORMAL,
RUNG_WARN_75,
)
from kora_cli.reasoning.kora_hermes_plugin.cost_ladder.plugin import (
cost_ladder_and_caching_hook,
register,
)
from kora_cli.reasoning.kora_hermes_plugin.cost_ladder.selector import (
RoutingDecision,
select_model_pre_call,
should_escalate_post_call,
strip_opus_prefix,
)

__all__ = [
"DEFAULT_DECISION_PATTERNS",
"DEFAULT_HAIKU_LOW_CONFIDENCE_PATTERNS",
"DEFAULT_HAIKU_MODEL",
"DEFAULT_OPUS_MODEL",
"ENV_FORCE_OPUS",
"ENV_HAIKU_LOW_CONFIDENCE_PATTERNS",
"ENV_OPUS_PREFIX",
"ENV_OPUS_TRIGGER_PATTERNS",
"RUNG_DOWNSHIFT_90",
"RUNG_HARD_STOP_100",
"RUNG_NORMAL",
"RUNG_WARN_75",
"RoutingDecision",
"cost_ladder_and_caching_hook",
"register",
"select_model_pre_call",
"should_escalate_post_call",
"strip_opus_prefix",
]
69 changes: 69 additions & 0 deletions kora_cli/reasoning/kora_hermes_plugin/cost_ladder/constants.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
"""Constants for the cost-ladder plugin.

Default model identifiers + env-var names + the bundled
decision-language / low-confidence pattern lists. Operators
override the patterns via env (see ``selector.py`` for the
``_compiled_*_patterns`` accessors).

Moved verbatim from ``kora_cli.router.cost_router`` per
KR-PLUGIN-COST-LADDER; the shim at the old path re-exports
these symbols so existing call sites keep working.
"""

from __future__ import annotations

from typing import List

# Match the long-form ID the engine has used since KR-FEAT-AGENTIC-
# REASONING ST1 (``MODEL_HAIKU = "claude-haiku-4-5-20251001"``).
# Using the long form keeps cache-key stability with already-warm
# caches in production.
DEFAULT_HAIKU_MODEL = "claude-haiku-4-5-20251001"
DEFAULT_OPUS_MODEL = "claude-opus-4-7"


# Env names — operator overrides.
ENV_FORCE_OPUS = "KORA_FORCE_OPUS"
ENV_OPUS_TRIGGER_PATTERNS = "KORA_OPUS_TRIGGER_PATTERNS"
ENV_OPUS_PREFIX = "KORA_OPUS_PREFIX"
ENV_HAIKU_LOW_CONFIDENCE_PATTERNS = "KORA_HAIKU_LOW_CONFIDENCE_PATTERNS"


# Default decision-language patterns (case-insensitive). Operator
# overrides via ``KORA_OPUS_TRIGGER_PATTERNS`` (comma-separated
# regex list). Tuned for the question shapes operators use when
# they actually want a careful answer rather than a status read.
DEFAULT_DECISION_PATTERNS: List[str] = [
r"\b(should|do|can|will|would)\s+(i|we|you)\b",
r"\b(decide|decision|approve|approval|reject|go/no-go|ship or not)\b",
r"\bis it (better|worth|safe|right)\b",
r"\bwhat should (i|we)\b",
r"\b(plan|strategy|approach)\b.*\b(for|to)\b",
]


# Low-confidence markers in a Haiku response. Operator overrides
# via ``KORA_HAIKU_LOW_CONFIDENCE_PATTERNS``.
DEFAULT_HAIKU_LOW_CONFIDENCE_PATTERNS: List[str] = [
r"i'?m not (sure|certain|confident)",
r"i don'?t (have enough|know|understand)",
r"i might be wrong",
r"this is (a guess|just a guess|speculative)",
r"i can'?t (tell|determine|verify|confirm)",
r"unclear (whether|if|how)",
]


# Heuristic: a Haiku response that's clearly too short for a
# substantive question. Tunable inline; operator can edit constant
# in this module if they need to shift the threshold (no env for
# this one — it's a structural signal, not a phrase list).
_LOW_CONFIDENCE_SHORT_RESPONSE_THRESHOLD = 50
_LOW_CONFIDENCE_LONG_INPUT_THRESHOLD = 200


# Cost-rung literals (matching ``agent.cost_state_holder.CostRung``).
RUNG_NORMAL = "normal"
RUNG_WARN_75 = "warn_75"
RUNG_DOWNSHIFT_90 = "downshift_90"
RUNG_HARD_STOP_100 = "hard_stop_100"
185 changes: 185 additions & 0 deletions kora_cli/reasoning/kora_hermes_plugin/cost_ladder/plugin.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,185 @@
"""Cost-ladder plugin module — hook handler + sub-register.

The ``pre_api_request_mutable`` hook handler that drives Kora's
default-Haiku-with-earned-Opus model selection + the prompt-
caching ``cache_control: ephemeral`` markers on system + last
tool. Per KR-PLUGIN-COST-LADDER, this is the first plugin
extraction sub-module; the same pattern applies to follow-on
KR-PLUGIN-* buckets.

# Bundled responsibility (transitional)

Today's handler bundles cost-ladder model selection AND prompt-
caching cache_control wrapping. KR-PLUGIN-CACHING will extract
the caching half into its own ``caching/`` sub-plugin file.
Until then, both behaviors live here. The bundling is
intentional for v1: both fire at the same hook site
(``pre_api_request_mutable``), both build the same
``{"override": {...}}`` return shape, and splitting now would
add a second hook fire without behavior benefit.

# Activation gate

The handler short-circuits to ``None`` (no override) when the
call isn't a Kora-tagged route — protects Hermes-fork users who
load the plugin from inadvertently running Kora logic on their
sessions. Uses the same ``_is_kora_call`` predicate as the
top-level plugin's other hook handlers.
"""

from __future__ import annotations

import logging
from typing import Any, Optional

logger = logging.getLogger(__name__)


# ---------------------------------------------------------------------------
# Helpers
# ---------------------------------------------------------------------------


def _current_cost_rung() -> str:
"""Read the active cost-ladder rung. Defaults to ``"normal"``
on any failure (holder unwired, exception, etc.) — matches
cost_router's expectation."""
try:
from agent.cost_state_holder import get_cost_holder

holder = get_cost_holder()
if holder is None:
return "normal"
rung = holder.active_rung()
return getattr(rung, "value", str(rung)) or "normal"
except Exception:
return "normal"


def _is_kora_call(route_value: Any) -> bool:
"""Mirror of the top-level plugin's KORA_ROUTES gate. Imported
locally to avoid a circular import from
``plugins.kora_hermes`` (the top-level Hermes-discovery
entry point); the route literal set is small + stable so the
duplication cost is low + the import isolation is worth it."""
# Lazy import to break a potential circle.
from plugins.kora_hermes import KORA_ROUTES

if not isinstance(route_value, str) or not route_value:
return False
return route_value in KORA_ROUTES


# ---------------------------------------------------------------------------
# Hook handler: pre_api_request_mutable
# ---------------------------------------------------------------------------


def cost_ladder_and_caching_hook(
*,
route: str = "",
api_kwargs: Optional[dict] = None,
api_call_count: int = 0,
user_message: str = "",
**kw,
) -> Optional[dict]:
"""``pre_api_request_mutable`` handler.

For Kora-tagged calls:
1. Call the cost-router (``select_model_pre_call``) to
pick Haiku-default-or-Opus-earned based on iteration +
decision-language + cost rung + force-Opus env signals.
2. Wrap ``system`` + ``tools`` with
``cache_control: ephemeral`` markers so Anthropic
caches them (KR-CHEAP-PROMPT-CACHING semantic via the
hook layer rather than the bypass loop's inline wrap).

Returns ``{"override": {...}}`` with the keys to replace in
api_kwargs. ``None`` (no-op) for non-Kora calls.
"""
if not _is_kora_call(route):
return None

if not isinstance(api_kwargs, dict):
return None

override: dict = {}

# --- Cost-ladder model selection ---
# Use the (now-extracted) selector. Hermes's
# ``api_call_count`` is 1-indexed per-call; matches Kora's
# iteration count from the bypass loop. cost_rung comes from
# the process-global CostStateHolder.
try:
from kora_cli.reasoning.kora_hermes_plugin.cost_ladder.selector import (
select_model_pre_call,
)

cost_rung = _current_cost_rung()
decision = select_model_pre_call(
message_text=user_message or "",
iteration=max(int(api_call_count or 1), 1),
cost_rung=cost_rung,
)
if decision.model is not None:
override["model"] = decision.model
except Exception as exc:
logger.warning(
"[kora_hermes.cost_ladder] select_model_pre_call raised "
"%r — leaving api_kwargs['model'] unchanged",
exc,
)

# --- Caching: wrap system + tools with cache_control markers ---
try:
from kora_cli.reasoning.anthropic_engine import (
_wrap_system_as_cacheable,
_wrap_tools_as_cacheable,
)

# System: may be str (Hermes default) OR already a list
# (e.g. test fixture passed a content-block list). Wrap
# only the str case so we don't double-wrap.
existing_system = api_kwargs.get("system")
if isinstance(existing_system, str) and existing_system:
override["system"] = _wrap_system_as_cacheable(existing_system)

# Tools: tools_for_api is a list (may be empty in
# toolless v1 route-through). The wrapper handles empty
# list by returning empty list — safe to always call.
existing_tools = api_kwargs.get("tools") or []
if isinstance(existing_tools, list) and existing_tools:
override["tools"] = _wrap_tools_as_cacheable(existing_tools)
except Exception as exc:
logger.warning(
"[kora_hermes.cost_ladder] caching wrap raised %r — "
"leaving api_kwargs unchanged",
exc,
)

if not override:
return None
return {"override": override}


# ---------------------------------------------------------------------------
# Sub-register
# ---------------------------------------------------------------------------


def register(ctx) -> None:
"""Sub-plugin register. Called by the top-level
``KoraHermesPlugin.register`` to wire the cost-ladder hook.

The top-level plugin owns plugin-context ownership; this
sub-register is just a thin function that calls
``ctx.register_hook`` for the one hook this sub-plugin
owns. Keeps the file structure clean for the eventual
upstream PR (each sub-plugin is independently packageable).
"""
ctx.register_hook(
"pre_api_request_mutable", cost_ladder_and_caching_hook
)
logger.debug(
"[kora_hermes.cost_ladder] sub-plugin registered"
)
Loading