Skip to content

feat(gateway): route inbound messages via routes table + GatewayRunner profile binding (3/6) - #37497

Open
davidgut1982 wants to merge 7 commits into
NousResearch:mainfrom
davidgut1982:feat/mga-3-gateway-routing
Open

feat(gateway): route inbound messages via routes table + GatewayRunner profile binding (3/6)#37497
davidgut1982 wants to merge 7 commits into
NousResearch:mainfrom
davidgut1982:feat/mga-3-gateway-routing

Conversation

@davidgut1982

Copy link
Copy Markdown
Contributor

What

Route inbound messages via a routes table plus a select_agent hook, and have GatewayRunner load the agent registry, bind the active AgentProfile, and propagate agent_id to hooks. Adds per-agent profile runtime overrides (model/provider/base_url/api_key_env).

Why

This is the core of single-gateway/multi-agent: an inbound message must be mapped to an agent, that agent's profile bound for the turn, and its model/provider overrides applied over gateway defaults.

How to test

python -m pytest tests/gateway/test_agent_routing.py tests/gateway/test_profile_overrides.py -q

Platforms tested

Linux (CT 133 / Proxmox LXC)

Part 3 of 6 in the multi-agent gateway decomposition (replaces #34741). Depends on: #37496

Note on conflict resolution: two cherry-picks. gateway/config.py was an additive conflict (kept main's filter_silence_narration loader alongside the new agents/routes/default_agent loaders). gateway/run.py conflicted with main's #35314 empty-model "last-known-good" safety net; resolved by applying _apply_profile_runtime_overrides() first, then the safety net as the final guard, so an empty model after profile resolution is still recovered.

@alt-glitch alt-glitch added type/feature New feature or request P3 Low — cosmetic, nice to have comp/gateway Gateway runner, session dispatch, delivery labels Jun 2, 2026
@davidgut1982
davidgut1982 force-pushed the feat/mga-3-gateway-routing branch from c589883 to 99f6de7 Compare June 3, 2026 03:01
02356abc and others added 5 commits June 3, 2026 23:29
Introduce AgentProfile dataclass and a ContextVar (_current_agent_profile)
that lets path getters (get_hermes_home, get_skills_dir, get_memory_dir)
resolve to the active agent's home directory under asyncio.

- agent/profile.py: AgentProfile, use_profile() context manager,
  load_agent_registry() from GatewayConfig
- hermes_constants.py: get_hermes_home() reads ContextVar before env fallback
- tests/agent/test_profile_contextvar.py: ContextVar isolation under
  asyncio.gather, nested contexts, registry loading

Single-agent installs see zero change — no profile bound means fallback
to HERMES_HOME env var as before.
Add agent_id field to SessionSource and SessionEntry, prefix session keys
with agent:<id>: in build_session_key. Default "main" preserves every
historical key string for single-agent installs.

- gateway/session.py: SessionSource.agent_id, SessionEntry.agent_id,
  build_session_key prefixing
- hermes_state.py: sessions table migration (agent_id TEXT DEFAULT 'main'),
  new idx_sessions_agent index
- tests/gateway/test_session.py: build_session_key prefixing for all
  chat_type × agent_id combinations
- tests/*/test_session_boundary_hooks.py: hook payload agent_id kwarg
… hook

Add declarative routing (routes: match → agent) and a select_agent plugin
hook. _attach_agent_id injects the resolved agent_id into event.source
before build_session_key. Seven platform adapters get pre-injection for
batching paths; the rest inherit it from base.py.

- gateway/agent_routing.py: resolve_agent_id(), _route_matches()
- gateway/config.py: agents, routes, default_agent schema
- gateway/platforms/base.py: _attach_agent_id(), set_routing_context()
- gateway/platforms/{telegram,discord,slack,matrix,feishu,wecom,yuanbao}.py:
  pre-batch injection
- hermes_cli/plugins.py: select_agent hook registration
- tests/gateway/test_agent_routing.py: declared-order matching, hook chain,
  default fallback, profile isolation
…s agent_id to hooks

GatewayRunner loads the agent registry at init and wraps every inbound
message in use_profile(). AIAgent accepts an optional profile= kwarg.
All invoke_hook call sites gain agent_id= kwarg. _handle_message is
split into _handle_message (ContextVar plumbing) + _handle_message_inner
(legacy logic) so tests that grep the source body continue to work.

- gateway/run.py: registry loading, use_profile() wrapping, hook kwargs
- run_agent.py: AIAgent(profile=), profile-aware model/toolset resolution
- model_tools.py, tools/{approval,terminal,delegate}.py: hook agent_id
- cli.py, tui_gateway/server.py: session boundary hook agent_id
- tests/gateway/test_profile_overrides.py: per-agent model/toolset overrides
- tests/test_model_tools.py: hook payload verification
- tests/gateway/test_{update,title,reasoning}_command.py: adapt to
  _handle_message split
The MGA series added _attach_agent_id() into the handle_message hot path
(base.py:handle_message). It reads three instance attributes set only in
BasePlatformAdapter.__init__: _default_agent_id, _gateway_routes, and
_gateway_ref. Any adapter constructed without running __init__ (or a
future partial-construction path) crashes with AttributeError on
_default_agent_id, which is read outside the existing try/except.

Because this runs on every inbound message, a missing attribute would
crash real message processing, not just tests. Harden the method to read
all three via getattr() with safe defaults so a partially-constructed
adapter degrades to the "main" agent instead of raising.

Also fix the _make_adapter() test helper, which deliberately bypasses
__init__ via object.__new__() and hand-sets attributes: it predated the
MGA routing attributes and never set them. Add the three so the mock
faithfully mirrors a real __init__-constructed adapter.

Fixes 10 regressions in tests/gateway/test_active_session_text_merge.py.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
davidgut1982 and others added 2 commits June 4, 2026 19:49
DeliveryRouter.__init__ was missing the `registry` keyword argument
that gateway/run.py:1868 passes as `DeliveryRouter(self.config,
registry=self._agent_registry)`.  This caused a TypeError on startup
failing pytest shards 2/3/4/6 on PR NousResearch#37497 (mga-3).

Also restores:
- `agent_id` field on DeliveryTarget dataclass
- `agent_id=origin.agent_id` propagation in DeliveryTarget.parse()
- `_profile`/`use_profile` profile-binding block in deliver()

Authoritative reference: feat/mga-5-cron-api-propagation:gateway/delivery.py

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Empty commit to fire push event so the fix(gateway) delivery.py
commit (6fbb441) is picked up by NousResearch CI.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

@teknium1 teknium1 left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Thanks for the multi-agent routing work. There is one correctness issue that needs resolution before this design can be salvaged.

Problems

  • gateway/agent_routing.py:92 returns any configured agent ID. gateway/platforms/base.py:2268 stamps that value into the source, while gateway/run.py:7425 silently resolves an unknown ID to the main profile. Because gateway/session.py:645-646 keys the session from the stamped ID, an invalid route produces an agent:<unknown> session that runs with main-agent state.
  • Current main now uses gateway.multiplex_profiles (gateway/config.py:704-709), profile-stamped secondary adapters (gateway/run.py:8475-8487), and profile-scoped turns (gateway/run.py:16947-16975) instead of this PR's route-table mechanism. The implementation needs integration with that current lifecycle rather than a direct transplant.

Suggested changes

  • Validate route/default/hook-selected IDs against the registry before session-key construction; reject or explicitly fall back before assigning source.agent_id.
  • Rework the proposal around the current multiplexing path, including its credential and session isolation boundaries.

Automated hermes-sweeper review.

Comment thread gateway/agent_routing.py
if not isinstance(match, dict):
continue
if _route_matches(match, source):
return agent.strip()

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

This accepts any non-empty configured target. The adapter stamps it into source.agent_id, which namespaces the session, but GatewayRunner later falls back to the main profile when the registry lacks that ID. Validate route/default/hook-selected IDs against the registry before assigning the source so a typo cannot create an agent:<unknown> session that runs with main state.

@teknium1 teknium1 added sweeper:risk-session-state Sweeper risk: may lose/corrupt/mis-associate session or context state sweeper:risk-message-delivery Sweeper risk: may drop, duplicate, misroute, or suppress messages sweeper:risk-security-boundary Sweeper risk: may affect sandboxing, auth, credentials, or sensitive data sweeper:risk-compatibility Sweeper risk: may break existing users, config, migrations, defaults, or upgrades sweeper:blast-moderate Sweeper blast radius: moderate — a subsystem or single platform area/profiles Multi-profile isolation, HERMES_HOME scoping labels Jul 13, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

area/profiles Multi-profile isolation, HERMES_HOME scoping comp/gateway Gateway runner, session dispatch, delivery P3 Low — cosmetic, nice to have sweeper:blast-moderate Sweeper blast radius: moderate — a subsystem or single platform sweeper:risk-compatibility Sweeper risk: may break existing users, config, migrations, defaults, or upgrades sweeper:risk-message-delivery Sweeper risk: may drop, duplicate, misroute, or suppress messages sweeper:risk-security-boundary Sweeper risk: may affect sandboxing, auth, credentials, or sensitive data sweeper:risk-session-state Sweeper risk: may lose/corrupt/mis-associate session or context state type/feature New feature or request

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants