Skip to content
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
44 changes: 44 additions & 0 deletions docs/specialist-model-router.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
# Quota-aware specialist model router

The `specialist-router` plugin keeps ordinary Telegram conversation on the configured Hermes coordinator and delegates coding work to two independently metered Codex models.

## Policy

- Coordinator: `openai-api/gpt-5.6` for conversation, planning, summaries, status, and final reports.
- Spark: `gpt-5.3-codex-spark` for repository inspection, reproduction, focused tests, review, regression tests, and one bounded low-risk implementation attempt.
- Sol: `gpt-5.6-sol` immediately for high-risk or multi-file work, or after one failed/uncertain/incomplete Spark attempt.
- A successful sol implementation is independently reviewed by Spark.
- The router derives each pool's five-hour and weekly availability from Codex rollout telemetry and caches it for 120 seconds. At 20% weekly sol remaining, noncritical sol work stays on Spark; critical work may use the reserve.

The gateway hook leaves non-coding messages byte-for-byte unchanged. Coding messages receive an ephemeral route directive instructing the coordinator to invoke `route_specialist_task` once. The specialist runner feeds the complete prompt through Codex stdin (`-` + piped input) so multiline text survives intact, and it falls back to a coordinator/manual continuation when both specialist pools refuse to start. `/model-route-status` shows coordinator, routine/complex routes, active specialist sessions, task/repository, reason, five-hour and weekly quota state, the 20% Sol reserve, banked-reset availability/expiry, auto-review state, and the recommended burst state. Unsupported external values are reported as `unknown`; no banked reset is ever redeemed automatically.

Codex auto-review, if enabled by the CLI, is only a process signal. It is not code-quality approval. Every merge still requires a fresh exact-head GitHub Codex review (or an explicit human review decision), with findings resolved against the exact current SHA.

## Configuration

Enable the bundled plugin and configure behavioral settings in `~/.hermes/config.yaml` (never `.env`):

```yaml
plugins:
enabled: [specialist-router]
entries:
specialist-router:
coordinator_model: openai-api/gpt-5.6
spark_model: gpt-5.3-codex-spark
sol_model: gpt-5.6-sol
reserve_percent: 20
max_concurrent_editing: 2
banked_reset_available: unknown
banked_reset_expires_at: null
auto_review_enabled: unknown
quota_cache_seconds: 120
codex_binary: /home/ubuntu/.npm-global/bin/codex
```

Authentication remains in the existing global VM/Codex credential stores. The plugin never copies credentials into a project.

The installed CLI was verified with `codex exec --model` and exposes `codex exec resume <session-id> --model ...` for session reuse. Each specialist result records its returned thread ID; pass it back as `resume_session_id` so a follow-up resumes the compact specialist context instead of rereading a repository.

## Rollback

Remove `specialist-router` from `plugins.enabled`, restore the prior `model` and `delegation` blocks in `~/.hermes/config.yaml`, and restart `hermes-gateway.service`. The only runtime state created by the plugin is `~/.hermes/specialist-router-state.json`, which may be left in place or removed after rollback.
76 changes: 76 additions & 0 deletions plugins/specialist_router/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
"""Quota-aware specialist model router plugin."""

from __future__ import annotations

import json
import os
from pathlib import Path

from .router import Router, RouterConfig

_router: Router | None = None


def _config() -> RouterConfig:
try:
from hermes_cli.config import load_config
root = load_config() or {}
except Exception:
root = {}
entry = (((root.get("plugins") or {}).get("entries") or {}).get("specialist-router") or {})
return RouterConfig.from_mapping(entry)


def register(ctx) -> None:
global _router
_router = Router(_config())

def pre_gateway_dispatch(*, event, **_kwargs):
text = getattr(event, "text", "") or ""
decision = _router.classify(text)
Comment on lines +29 to +30

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Skip routing for slash commands

This hook classifies the raw gateway text before command dispatch and does not exempt event.is_command(). Built-in, quick, plugin, or skill commands whose name or arguments contain words like fix, test, lint, or repo will be rewritten into a specialist-route prompt before their handler sees them, so commands such as /queue fix this bug or a coding skill command no longer preserve their intended slash-command semantics.

Useful? React with 👍 / 👎.

if decision.route == "coordinator":
return {"action": "allow"}
directive = _router.route_directive(text, decision)
return {"action": "rewrite", "text": directive}
Comment on lines +33 to +34

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Leave text unchanged when the router tool is disabled

This hook rewrites every coding-looking gateway message whenever the plugin is loaded, but the plugin toolset can still be disabled per platform or omitted from a restricted session's enabled_toolsets. In that configuration route_specialist_task is absent from the model schema while the prompt is explicitly told to call it, so normal coding messages turn into impossible tool-call instructions instead of falling back to the coordinator.

Useful? React with 👍 / 👎.

Comment on lines +33 to +34

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Skip rewrite when the router toolset is disabled

If an operator disables the specialist-router plugin toolset for a platform via hermes tools, the agent will no longer receive route_specialist_task, but this hook still rewrites coding messages to instruct the coordinator to call that exact tool. In that configuration every coding message is steered toward an unavailable tool instead of falling back to normal coordinator handling, so the hook should check platform toolset availability before rewriting.

Useful? React with 👍 / 👎.

Comment on lines +33 to +34

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Do not rewrite when the route tool is disabled

If an operator keeps the plugin enabled but disables the specialist-router toolset for a gateway platform via the normal tools configuration, this hook still rewrites coding messages to instruct the coordinator to call route_specialist_task. In that session the tool schema is absent, so ordinary coding messages become polluted with an impossible tool directive instead of falling back to normal coordinator handling; the hook should check that the route tool is available for the session before rewriting.

Useful? React with 👍 / 👎.


def status(_raw_args: str = "") -> str:
return _router.format_status()

def route_tool(args: dict, **_kwargs) -> str:
result = _router.execute(
goal=str(args.get("goal") or ""),
repository=str(args.get("repository") or os.getcwd()),
risk=str(args.get("risk") or "auto"),
simulate_spark_failure=bool(args.get("simulate_spark_failure", False)),

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Do not let public tool args simulate Spark failure

Because this handler accepts simulate_spark_failure directly from the model tool arguments (and the schema advertises it below), a user prompt can set it to true and force the router to record a Spark failure without actually spending the required Spark attempt, immediately escalating a low-risk task to Sol. Keep this test hook out of the runtime tool surface or gate it behind a non-user-controlled test path.

Useful? React with 👍 / 👎.

resume_session_id=str(args.get("resume_session_id") or "") or None,
)
return json.dumps(result, ensure_ascii=False)

ctx.register_hook("pre_gateway_dispatch", pre_gateway_dispatch)
ctx.register_command(
"model-route-status",
handler=status,
description="Show specialist routing, quota pools, and sol reserve state.",
)
ctx.register_tool(
name="route_specialist_task",
toolset="specialist-router",
description="Route one coding goal to Spark or GPT-5.6-sol and return its verified result.",
emoji="⇄",
schema={
"name": "route_specialist_task",
"description": "Execute a coding task through the quota-aware Codex specialist router.",
"parameters": {
"type": "object",
"properties": {
"goal": {"type": "string"},
"repository": {"type": "string"},
"risk": {"type": "string", "enum": ["auto", "low", "high", "critical"]},
"simulate_spark_failure": {"type": "boolean"},

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Remove the public Spark-failure simulator

Because this property is in the model-visible tool schema and route_tool forwards it directly, any prompt/model call that sets simulate_spark_failure=true skips the real Spark attempt and drives the escalation path instead. This looks like a test hook, but exposing it in production lets user content alter routing and consume the more limited Sol quota; keep the simulation path out of the registered schema/runtime handler.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Hide the Spark-failure test switch from the tool schema

Exposing simulate_spark_failure in the production model tool schema lets any prompted tool call skip the real Spark attempt and mark it failed, which then drives escalation/fallback behavior and can force Sol usage for otherwise routine work. If this is only for tests, keep it out of the public schema and gate it in test-only code instead of giving users or prompt injection a routing-control knob.

Useful? React with 👍 / 👎.

"resume_session_id": {"type": "string", "description": "Optional Codex thread ID to resume instead of rereading repository context."},
},
"required": ["goal", "repository"],
},
},
handler=route_tool,
)
6 changes: 6 additions & 0 deletions plugins/specialist_router/plugin.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
name: specialist-router
version: 1.0.0
description: Quota-aware Codex specialist routing for coding tasks received by Hermes gateways.
author: DJ Papzin
hooks:
- pre_gateway_dispatch
Loading
Loading