Import AITURK IDE 1.0.0-beta.1 from Hermes 63279301; preserve MIT license
This commit is contained in:
@@ -0,0 +1,117 @@
|
||||
# Profile-Based Routing for Inbound Messages
|
||||
|
||||
> **Audience:** Gateway operators and contributors
|
||||
> **Source files:** `gateway/profile_routing.py`, `gateway/run.py` (`_profile_name_for_source`), `gateway/platforms/base.py` (`build_source`), `gateway/config.py`
|
||||
> **Related:** [Session Lifecycle](session-lifecycle.md), `docs/design/profile-builder.md`
|
||||
|
||||
## Overview
|
||||
|
||||
By default a single gateway run uses one profile (memory, persona, tools). **Profile-based
|
||||
routing** lets one gateway instance serve **multiple isolated profiles**, selecting which
|
||||
profile handles an inbound message based on *where the message came from* — the platform,
|
||||
server (`guild_id`), channel (`chat_id`), and/or thread (`thread_id`).
|
||||
|
||||
This is the inbound counterpart to multiplexing: instead of running N gateways, run one
|
||||
gateway and route per-community / per-channel / per-thread to a dedicated profile. Each
|
||||
profile keeps fully isolated state (`MEMORY.md`, `USER.md`, `SOUL.md`, sessions, tools).
|
||||
|
||||
Routing is **platform-generic**: it works for Discord, Telegram, Feishu, Slack, and every
|
||||
adapter — not just Discord.
|
||||
|
||||
## Configuring routes
|
||||
|
||||
Routes live under `profile_routes` in `config.yaml`. Both the top-level and the nested
|
||||
`gateway.profile_routes` forms are accepted (the nested form is what
|
||||
`hermes config set gateway.profile_routes ...` writes).
|
||||
|
||||
```yaml
|
||||
profile_routes:
|
||||
# Route an entire Discord server (guild) to one profile.
|
||||
- name: server-default
|
||||
platform: discord
|
||||
guild_id: "1234567890"
|
||||
profile: server-profile
|
||||
|
||||
# Override a specific channel within that server with a different profile.
|
||||
- name: support-channel
|
||||
platform: discord
|
||||
guild_id: "1234567890"
|
||||
chat_id: "9876543210"
|
||||
profile: support-profile
|
||||
|
||||
# Pin a Telegram group to a profile (Telegram has no guild_id — chat_id only).
|
||||
- name: tg-group
|
||||
platform: telegram
|
||||
chat_id: "-1001234567890"
|
||||
profile: tg-profile
|
||||
|
||||
# Route a single Discord thread.
|
||||
- name: standup-thread
|
||||
platform: discord
|
||||
guild_id: "1234567890"
|
||||
chat_id: "9876543210"
|
||||
thread_id: "1111111111"
|
||||
profile: standup
|
||||
```
|
||||
|
||||
### Fields
|
||||
|
||||
| Field | Required | Description |
|
||||
|---|---|---|
|
||||
| `name` | yes | Human-readable route identifier (used in logs). |
|
||||
| `platform` | yes | Adapter platform: `discord`, `telegram`, `feishu`, `slack`, … |
|
||||
| `profile` | yes | Target profile name (must exist under `~/.hermes/profiles/<name>`). |
|
||||
| `guild_id` | no | Server/guild (Discord). |
|
||||
| `chat_id` | no | Channel/group/DM id. |
|
||||
| `thread_id` | no | Thread id within a channel. |
|
||||
| `enabled` | no | Default `true`; set `false` to disable a route without removing it. |
|
||||
|
||||
## Matching rules
|
||||
|
||||
A route matches an inbound source when **every discriminator the route declares is satisfied**
|
||||
(conjunctive / AND). A field the route leaves unset is ignored.
|
||||
|
||||
- **`platform`** must equal the source platform exactly.
|
||||
- **`thread_id`** (if set) must equal the source thread id.
|
||||
- **`chat_id`** (if set) must match the source channel **or** its parent — a thread in a
|
||||
channel matches the channel's route (hierarchical match for Discord forums/threads).
|
||||
- **`guild_id`** (if set) must equal the source guild.
|
||||
|
||||
> A route declaring **both** `guild_id` and `chat_id` requires both to hold. A channel match
|
||||
> alone does not satisfy a guild constraint — this is intentional and tested.
|
||||
|
||||
When multiple routes match, the **most specific** one wins. Specificity is additive:
|
||||
|
||||
| Discriminator | Weight |
|
||||
|---|---|
|
||||
| `thread_id` | 8 |
|
||||
| `chat_id` | 4 |
|
||||
| `guild_id` | 2 |
|
||||
| (platform only) | 0 |
|
||||
|
||||
So a thread route (8) beats a channel route (4) beats a guild route (2) within the same server.
|
||||
If no route matches, the message uses the default/active profile.
|
||||
|
||||
## How it works at runtime
|
||||
|
||||
1. An inbound message arrives at a platform adapter.
|
||||
2. `BasePlatformAdapter.build_source` builds the `SessionSource` for the message. Every
|
||||
adapter carries a back-reference to the running `GatewayRunner`
|
||||
(`gateway_runner`, injected in `gateway/run.py`), so it asks the runner to resolve the
|
||||
target profile via `_profile_name_for_source`.
|
||||
3. `_profile_name_for_source` runs the configured routes through `match_profile_route` and
|
||||
stamps `source.profile` with the winning route's profile (or leaves it unset).
|
||||
4. Downstream, `_resolve_profile_home_for_source` chooses the profile home directory
|
||||
(`source.profile` → active profile → `default`) and the session is scoped per-profile, so
|
||||
each routed community gets isolated memory and conversation state.
|
||||
|
||||
Because `gateway_runner` is injected for **all** adapters (declared on `BasePlatformAdapter`),
|
||||
every platform goes through this path — not just Discord.
|
||||
|
||||
## Relationship to multiplexing
|
||||
|
||||
`profile_routes` requires `gateway.multiplex_profiles: true`. Multiplexing is what
|
||||
activates the per-profile runtime scope (per-profile `HERMES_HOME`, secret scope, and
|
||||
profile-namespaced session keys); routing is the decision layer that picks *which*
|
||||
profile a given guild/channel/thread lands in. With multiplexing off, `profile_routes`
|
||||
is ignored entirely — behavior is byte-identical to a single-profile gateway.
|
||||
Reference in New Issue
Block a user