feat(hooks): add message:pre_route event + multi-role-router reference hook - #78269
feat(hooks): add message:pre_route event + multi-role-router reference hook#78269raulvidis wants to merge 3 commits into
Conversation
…e hook Add a message:pre_route gateway hook event that fires after session resolution but before the turn-lease is claimed, letting a hook redirect a turn to a different session based on the inbound message. Ship a reference hook (optional-skills/multi-role-router/) that uses a cheap auxiliary LLM classifier to route each message to the best-suited worker role (code/knowledge/ml/ops), keeping continuations in the current session via a regex fast-path and a short history window. Includes: - gateway/hooks.py: document the message:pre_route event + context contract - gateway/run.py: emit message:pre_route, handle switch_session decision, evict the cached agent after a successful switch (mirrors /resume) - optional-skills/multi-role-router/: reference hook (handler, HOOK.yaml, SKILL.md, README) - tests: 58 tests for the hook + 13 for the gateway pre_route block Fixes over the upstream NousResearch#74272: - config-read guard: use canonical load_config_readonly() instead of raw yaml.safe_load of config.yaml (passes tests/hermes_cli/test_config_read_guard.py) - first-route isolation: a brand-new role now gets its own fresh session on the very first message (no shared-session gap)
Three fixes from qwen3.8-max code review of the branch: 1. Async clean: use async_call_llm() instead of the sync call_llm() in the async _call_auxiliary_llm(). The sync call blocked the gateway event loop for up to 15s per message and defeated the 5s asyncio.wait_for guard (the timeout callback couldn't fire while the loop was blocked). 2. Config-slot match: use task="triage_specifier" so the classifier uses the model a user configures under auxiliary.triage_specifier (the slot _get_auxiliary_config reads), not the compression slot. Read config and used config now agree. 3. Remove dead code: _update_meta_session() was never called (handle() only handles message:pre_route and has no assistant response at pre-route time), so the history list it wrote was never populated. Remove the dead function and its 3 tests to avoid shipping unwired state-management in a reference hook.
Duplicate of #74272: both add the message:pre_route session-switch hook and multi-role-router reference implementation. |
- README: remove the /role slash-command table — those commands are not implemented in this PR or on main; document the config.yaml controls (multi_role_router.auto) that actually work. Fix the troubleshooting entry that referenced /role auto off. - SKILL.md: shorten description to the <=60 char one-sentence standard. - HOOK.yaml: credit the original implementation (NousResearch#74272, Clark Everson) instead of 'community'; bump version to 1.0.1. - gateway/hooks.py: fix the message:pre_route chat_type contract to the real MessageSource values (dm|group|channel|thread|webhook), not the Telegram-specific ones. - handler.py: update the stale /role comment.
|
The triage bot is right — this builds directly on #74272 (same The only delta here is the reviewer-feedback fixes (first-route session isolation, agent-cache eviction after |
|
Follow-up: this PR couldn't be reopened after the branch rebuild (GitHub pins closed PRs to the old head lineage), so the rebuilt branch is up as #78326 instead — the #74272 base commits are now cherry-picked/rebased with original authorship preserved, with the review-fix delta stacked on top. This PR stays closed as the duplicate it was. |
Summary
Adds a
message:pre_routegateway hook event that fires after session resolution but before the turn-lease is claimed, letting a hook redirect a turn to a different session based on the inbound message. Ships a reference hook (optional-skills/multi-role-router/) that uses a cheap auxiliary LLM classifier to route each message to the best-suited worker role (code / knowledge / ml / ops), keeping short continuations in the current session via a regex fast-path.Closes #5143 (Multi-Role Auto-Routing via Gateway Hooks).
What's included
gateway/hooks.py— document themessage:pre_routeevent + context contract (platform, user_id, chat_id, thread_id, chat_type, session_id, session_key, message) and the return contract ({"decision":"switch_session","session_id":"<id>"}).gateway/run.py— emitmessage:pre_routeafter session resolution, honor aswitch_sessiondecision, and evict the cached agent after a switch so the next turn rebinds to the new session (mirrors/resume).optional-skills/multi-role-router/— reference hook:handler.py,HOOK.yaml,SKILL.md,README.md.Design decisions
switch_sessioncreates theSessionEntryfor a new target id./footext falls through (acceptable — it's just text).httpx.AsyncClient) so the hook never blocks the gateway event loop; thetriage_specifierauxiliary slot is used for the classifier (matching the config read).Notes for maintainers
This addresses the reviewer feedback on the earlier proposal: the config-read guard is satisfied (canonical
load_config_readonly()), the first-route isolation gap is closed, and dead state-management code was removed rather than shipped.There is a known routing-contract overlap with #69693 (
pre_agent_dispatchplugin hook) and #72942 (pre_gateway_dispatchaction) — three proposals at different layers. This PR implements themessage:pre_routelayer (after session resolution, before turn lease). Happy to align with whichever contract the maintainers decide on.