Skip to content
This repository was archived by the owner on May 26, 2026. It is now read-only.

feat(kora): KR-HERMES-LOCAL-EXTENSIONS — in-fork hooks + listener registration#172

Merged
rafe-walker merged 1 commit into
feature/phase2-upgradesfrom
feat/kora-KR-HERMES-LOCAL-EXTENSIONS
May 24, 2026
Merged

feat(kora): KR-HERMES-LOCAL-EXTENSIONS — in-fork hooks + listener registration#172
rafe-walker merged 1 commit into
feature/phase2-upgradesfrom
feat/kora-KR-HERMES-LOCAL-EXTENSIONS

Conversation

@rafe-walker

Copy link
Copy Markdown
Owner

Summary

Path B kickoff bucket per Council R3 close-out + operator pre-decision B + feedback-local-first-upstream-after. Adds the Hermes-core extensions that KR-REASONING-ROUTE-THROUGH-GATEWAY will require. All changes in our local fork on feature/phase2-upgrades; zero upstream-PR work in this bucket — upstream packaging waits until the extensions are battle-tested.

Discovery findings — what was already supported vs gap

5 areas audited. 4 gaps confirmed + extended; 1 deferred.

# Surface Current Hermes state Extension?
1 pre_llm_call mutability Observer-only — plugins return {"context": str} or str for context-string injection. Cannot mutate model/kwargs. Added — new hook pre_api_request_mutable
2 post_llm_call re-issue Observer-only — return value discarded. DEFERRED to follow-on KR-HERMES-LOCAL-EXT-REISSUE (substantial control-flow change in conversation_loop; §4 STOP-ASK guidance)
3 context.route field Absent — hooks carry platform (slack/discord/cli) not the per-route telemetry taxonomy Addedroute kwarg threaded into 7 hook sites
4 Third-party listener registration register_platform exists but shaped for chat-platform adapters; no background-daemon shape AddedBackgroundDaemonRegistry + PluginContext.register_background_daemon
5 Tool-list manipulation hook Absent — agent.tools used verbatim in build_api_kwargs:235 Added — new hook pre_tool_list_finalized

Per-extension contract

pre_api_request_mutable hook (NEW)

  • Site: agent/conversation_loop.py:~990 (after _build_api_kwargs returns + before observer pre_api_request)
  • Return: {"override": {<kwarg>: <value>}} → keys replace api_kwargs[key]; multiple plugins merge left-to-right (last write wins)
  • Backward compat: observer pre_api_request still fires AFTER override applies — sees the final api_kwargs
  • Fail-safe: any hook exception → caught + logged at WARNING; loop continues unmodified

pre_tool_list_finalized hook (NEW)

  • Site: agent/chat_completion_helpers.build_api_kwargs:~237
  • Return: {"override": [<tool>, ...]} → replaces tools for that SINGLE API call
  • First non-None override wins; agent.tools NEVER mutated
  • Fail-safe: any hook exception → unfiltered agent.tools used

route field (NEW kwarg, threaded into 7 hook sites)

  • Sourced from getattr(agent, "route", "") or "" — caller (Kora's route-through code) sets agent.route = "slack_dm" before invoking
  • Default "" → maps to telemetry ROUTE_UNKNOWN
  • Sites: on_session_start, pre_llm_call, pre_api_request, pre_api_request_mutable, post_api_request, transform_llm_output, post_llm_call, on_session_end, pre_tool_list_finalized
  • Backward compat: existing **kwargs plugin callbacks ignore the new kwarg transparently

BackgroundDaemonRegistry + PluginContext.register_background_daemon (NEW)

  • New module agent/background_daemon_registry.py (188 LOC)
  • PluginContext.register_background_daemon(name, startup, shutdown, *, periodic_task=None, shutdown_timeout=5.0)
  • Registration-only in this bucket; lifecycle execution is consumer's responsibility (Kora's DaemonCoordinator already implements; gateway-side consumer wiring lands in KR-REASONING-ROUTE-THROUGH-GATEWAY)
  • Duplicate registration raises ValueError (matches platform_registry.register semantic)
  • Thread-safe (RLock-wrapped)

Upstream-PR readiness notes

Extension PR-ready? What's needed before upstreaming
pre_api_request_mutable After battle-test Route-through bucket needs to exercise this against real Slack DM traffic
pre_tool_list_finalized After battle-test Route-through needs to demonstrate per-route tool manifests
route field Needs core taxonomy decision first Hermes-core would need to RFC the route vocabulary (free-form str vs typed Literal). Kora's KNOWN_ROUTES is a candidate; recommend extracting Hermes-friendly taxonomy first.
register_background_daemon PR-ready in isolation; companion runner is a separate upstream The registry itself is clean; gateway-side BackgroundDaemonRunner (consumer of registry) is a natural companion PR
post-LLM re-issue (DEFERRED) Not PR-ready Needs design RFC before either local OR upstream PR; KR-HERMES-LOCAL-EXT-REISSUE bucket

Test plan

  • 19 new tests in tests/agent/test_hermes_local_extensions.py pass
  • 145/145 pre-existing hook tests still pass (test_shell_hooks, test_plugin_llm, test_transform_llm_output_hook, test_transform_tool_result_hook, test_model_tools)
  • Backward compat: all 17 pre-existing VALID_HOOKS entries preserved; no existing call to register_hook / register_platform altered
  • Full repo xdist: 9599 passed, 45 failed. 43 are pre-existing baseline flakes; the +2 (test_panel_inventory_count_matches_expected pin drift 34→36 + test_banner_hides_when_no_active_alerts AlertsBanner.tsx regex drift) are pre-existing FE-instrumentation snapshot tests unrelated to agent/ changes (recent FE buckets added pages / tweaked components without updating the pins).
  • Zero failures in tests/agent/* or tests/test_*hook* — backward compat for all existing Hermes plugin code preserved.

Research doc

kora_docs/14_research/hermes_local_extensions_2026-05-23.md (~280 lines) — discovery findings + per-extension contract + backward-compat assertions + upstream-PR readiness checklist + verification commands. Will reuse this when packaging the upstream PRs once route-through stabilizes.

🤖 Generated with Claude Code

…istration

Per Council R3 close-out + operator pre-decision B + feedback-
local-first-upstream-after. Adds the Hermes-core extensions
KR-REASONING-ROUTE-THROUGH-GATEWAY will require. All changes in
our local fork on feature/phase2-upgrades. Upstream-PR
packaging waits until the route-through bucket battle-tests
these surfaces.

# Discovery findings (audit before code)

5 areas audited; 4 gaps confirmed + extension added; 1 deferred.

  1. pre_llm_call mutability     — gap (observer-only today). Added new
                                    hook pre_api_request_mutable.
  2. post_llm_call re-issue      — DEFERRED to follow-on bucket
                                    KR-HERMES-LOCAL-EXT-REISSUE.
                                    Substantial control-flow change
                                    inside conversation_loop; per §4
                                    STOP-ASK guidance for invasive
                                    extensions.
  3. context.route field         — gap (Hermes hooks carry `platform`
                                    not `route`). Threaded as `route`
                                    kwarg into 7 hook sites.
  4. Listener registration       — gap (register_platform is chat-
                                    platform shape, not background-
                                    daemon shape). Added
                                    register_background_daemon +
                                    BackgroundDaemonRegistry.
  5. Tool-list manipulation hook — gap. Added pre_tool_list_finalized
                                    in build_api_kwargs.

# Extension contracts

(a) Hook ``pre_api_request_mutable`` (VALID_HOOKS:135)
    Fires at api_kwargs construction site in conversation_loop
    (after _build_api_kwargs returns + before observer
    pre_api_request). Plugins return {"override": {...}} to
    replace api_kwargs keys before SDK call. Backward compat:
    pre_api_request observer still fires + sees the final
    post-override api_kwargs.

(b) Hook ``pre_tool_list_finalized`` (VALID_HOOKS:144)
    Fires in chat_completion_helpers.build_api_kwargs at the
    tools_for_api = agent.tools site. Plugins return
    {"override": [tool, ...]} to filter the tool list per-call
    WITHOUT mutating agent.tools (which is process-wide).
    First non-None override wins. Fail-safe: any hook exception
    → caught + logged → unfiltered tools.

(c) ``route`` kwarg threaded into 7 hook sites in
    conversation_loop.py (pre_llm_call, pre_api_request,
    pre_api_request_mutable, post_api_request, post_llm_call,
    on_session_start, on_session_end, transform_llm_output) +
    pre_tool_list_finalized in chat_completion_helpers. Sourced
    from getattr(agent, "route", "") or "". Backward compat:
    legacy CLI invocations that never set agent.route see "".

(d) PluginContext.register_background_daemon(name, startup,
    shutdown, *, periodic_task=None, shutdown_timeout=5.0) +
    BackgroundDaemonRegistry singleton at agent/background_
    daemon_registry.py. Registration-only in this bucket;
    lifecycle execution is the consumer's responsibility (Kora's
    DaemonCoordinator already implements consumer shape;
    gateway-side consumer wiring is the route-through bucket).

# Backward compatibility

  - All 17 pre-existing VALID_HOOKS entries preserved
  - All 7 pre-existing hook invocations gain a `route=` kwarg
    (additive; existing **kwargs plugins ignore it transparently)
  - register_hook contract unchanged for existing hook names
  - agent.tools is never mutated by pre_tool_list_finalized
  - 145/145 pre-existing hook tests (test_shell_hooks +
    test_plugin_llm + test_transform_llm_output_hook +
    test_transform_tool_result_hook + test_model_tools) pass

# Tests

tests/agent/test_hermes_local_extensions.py — 19 new tests:
  - VALID_HOOKS contains new entries (2 tests)
  - BackgroundDaemonRegistry: register / list / order / dup /
    by_name / periodic_task / shutdown_timeout / singleton (8)
  - PluginContext.register_background_daemon forwarder (3)
  - register_hook accepts new hook names without warning (2)
  - pre_tool_list_finalized integration: filter + no-override
    fall-through + exception fail-safe (3)
  - PeriodicTaskSpec round-trip (1)

# Documentation

kora_docs/14_research/hermes_local_extensions_2026-05-23.md
covers:
  - Discovery findings (per-surface gap analysis)
  - Per-extension contract (file:line, return semantics, fail
    handling)
  - Backward-compat assertions
  - Upstream-PR readiness checklist per extension (gates for
    when each can package into a Hermes-upstream PR)
  - Verification commands

# Regression

164/164 in-scope tests pass (19 new + 145 existing hook tests).
Full repo xdist: 9599 passed, 45 failed. 43 of 45 are pre-
existing baseline flakes; the +2 (test_panel_inventory_count_
matches_expected pin drift 34→36 + test_banner_hides_when_no_
active_alerts AlertsBanner.tsx regex drift) are pre-existing
FE-instrumentation snapshot tests unrelated to agent/ changes.

Zero failures in tests/agent/* or tests/test_*hook* —
backward compat for all existing Hermes plugin code preserved.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@rafe-walker rafe-walker merged commit 9cd6311 into feature/phase2-upgrades May 24, 2026
@rafe-walker rafe-walker deleted the feat/kora-KR-HERMES-LOCAL-EXTENSIONS branch May 24, 2026 03:20
rafe-walker added a commit that referenced this pull request May 24, 2026
…ck R3-2 Phase C completion (#189)

Substantial paired bundle.

Deliverable 1 — New local Hermes hook post_llm_call_can_reissue at agent/conversation_loop.py. First-non-None override semantics matching #172/#181 family. Anti-loop safety: at most one reissue per iteration. Backward compat: post_api_request observer sees FINAL (post-reissue) response only.

Deliverable 2 — kora_hermes_plugin/haiku_router/ following #185 template. Plugin consumes should_escalate_post_call (from cost_ladder/selector.py since #185 but no caller until now). Implements parallel-Claude pattern from R3: low-confidence Haiku response → reissue to Opus with Haiku response in messages context. record_inference fires twice per iteration (Haiku + Opus reissue) with escalated_to_opus tagged correctly.

Sample trace: Haiku reply uncertain (i"m not sure...) → escalates → Opus response includes Haiku context → Opus confirms terser. Two consecutive record_inference calls, only second tagged escalated_to_opus=True.

What this completes: 6 of 7 plugin extractions (KR-PLUGIN-IDENTITY still deferred per Lock R3-2); all KR-HAIKU-ROUTER #165 escalation paths now functional end-to-end; Lock R3-2 Phase C closed.

Two non-blocking follow-ups flagged in PR body: escalation_reason as structured telemetry field (one-line CostStateHolder.record_inference bump); per-call api_call_count accounting (transparent-upgrade vs per-call — currently transparent).

125 directly-affected tests green; 56 broader-suite failures verified pre-existing on base (fastapi/blake3/HERMES_HOME environmental, not regressions).
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant