Message Routing
Routing lets you shape every incoming message before the LLM sees it. Detect tags, inject context, restrict tools — all configurable in a dedicated TOML file.
Overview
The routing framework runs a three-stage pipeline at BeforeAgentTurn (before safety hooks):
inbound message → detector → (optional) rewriter → action- Detector — classifies the message shape (tag prefix/suffix, complexity, scope)
- Rewriter — transforms the message text (prepend instructions, inject enrichment context)
- Action — decides what happens (restrict tools, enrich with context, reject, dispatch)
Rules can run in first_match_wins mode (stop at first match) or all_match mode (evaluate every matching rule in order).
Config File
Routing lives in a dedicated routing.toml, discovered in this order (first found wins):
<workspace>/routing.toml<workspace>/.ugent/routing.toml~/.ugent/routing.toml
A missing file means routing is disabled. Editing routing.toml triggers a full agent rebuild on the next reload — the new pipeline takes effect without restarting.
TIP
Any inline [routing] section in ugent.toml is ignored. Routing must live in its own file.
Top-Level Fields
enabled = true # master switch
mode = "all_match" # "first_match_wins" (default) | "all_match"Use all_match when multiple rules should fire on the same message (e.g., one rule enriches context, another prepends it).
Quick Start
# ~/.ugent/routing.toml
enabled = true
mode = "first_match_wins"
# Tag-suffixed messages get a restricted, read-only toolset.
[[rules]]
name = "research-mode"
enabled = true
detector = "suffix"
detector_params = { tag = "#research", case_sensitive = false, strip_suffix = true }
rewriter = "prepend-instruction"
rewriter_params = { template = "You are in research mode. Do not modify files." }
action = "restrict-toolset"
action_params = { allowed = ["web_search", "web_fetch", "workspace_search"], denied = [] }With this rule, a message like "summarize the auth module #research" is rewritten to "You are in research mode. Do not modify files." and the LLM only sees search and web tools.
Detectors
| Name | Description | Key Params |
|---|---|---|
prefix | Matches a literal tag at the start of the message | tag, case_sensitive, strip_prefix |
suffix | Matches a literal tag at the end | tag, case_sensitive, strip_suffix |
complexity | Scores message complexity by keyword density and length | threshold (0.0–1.0), keywords |
out_of_scope | Detects whether a question is outside the configured scope | research_keywords, scope_keywords |
intent-gate | Classifies intent via an external LLM endpoint | server_url, timeout_secs |
noop | Always matches (useful with all_match for unconditional rules) | — |
Rewriters
| Name | Description | Key Params |
|---|---|---|
prepend / prepend-instruction | Prepends a template string to the message using minijinja | template (supports , , ) |
enrichment-prepend | Injects previously fetched enrichment context into the message | enrichment_keys (list of metadata keys from tool-enrich actions) |
noop | No text transformation | — |
Actions
| Name | Description | Key Params |
|---|---|---|
restrict-toolset | Limits which tools the LLM can call this turn | allowed (list), denied (list; ["*"] denies all) |
tool-enrich | Calls a provider to fetch context and stores it in routing metadata | provider, metadata_key, plus passthrough params like max_results, snippet_lines |
intent-reject | Blocks the message from reaching the LLM | decision |
delegate-research | Dispatches the message as a deep-research sub-task | workflow, report_style, output_format |
noop | No action (useful when only the rewriter matters) | — |
Enrichment Providers
Providers wrap one or more tools so a tool-enrich action can inject context before the LLM responds. A legacy codebase-search provider is always available when an agent is attached; explicit [[providers]] entries are preferred for clarity.
Single-Tool Provider
[[providers]]
name = "workspace-search"
type = "tool"
tool_name = "workspace_search"
input_mode = "query_passthrough" # default
summary_format = "search" # default
default_params = { limit = 5 }Multi-Tool Provider
[[providers]]
name = "context-bundle"
type = "tool"
default_params = { limit = 5 }
[[providers.tools]]
tool_name = "workspace_search"
input_mode = "query_passthrough"
summary_format = "search"
default_params = { limit = 8 }
[[providers.tools]]
tool_name = "graph_search"
input_mode = "query_passthrough"
summary_format = "graph"
[[providers.tools]]
tool_name = "workspace_overview"
input_mode = "static"
summary_format = "stats"Flexible Source Provider
Combines exact MCP/ToolRegistry tool names and optional skill guidance:
[[providers]]
name = "flexible-context"
type = "tool"
[[providers.sources]]
name = "code"
kind = "tool"
tool_name = "mcp_ugent-context_workspace_search"
input_mode = "query_passthrough"
summary_format = "search"
default_params = { max_results = 8, snippet_lines = 20 }
# Optional skill guidance
# [[providers.sources]]
# name = "guide"
# kind = "skill"
# skill_name = "routing-review"
# max_chars = 20000Recipes
Quick Response Mode
Tag a message with [quick_response] to get a fast, tool-free answer grounded in retrieved context:
[[rules]]
name = "quick-response-enrich"
enabled = true
detector = "prefix"
detector_params = { tag = "[quick_response]", case_sensitive = false, strip_prefix = true }
action = "tool-enrich"
action_params = { provider = "flexible-context", metadata_key = "enrichment.qr", max_results = 15, snippet_lines = 20 }
[[rules]]
name = "quick-response-answer"
enabled = true
detector = "prefix"
detector_params = { tag = "[quick_response]", case_sensitive = false, strip_prefix = true }
rewriter = "enrichment-prepend"
rewriter_params = { enrichment_keys = ["enrichment.qr"] }
action = "restrict-toolset"
action_params = { denied = ["*"] }Enrich Complex Questions
Automatically inject codebase context when a question mentions refactoring or architecture:
[[rules]]
name = "enrich-complex-fetch"
enabled = true
detector = "complexity"
detector_params = { threshold = 0.6, keywords = ["refactor", "architecture", "design"] }
action = "tool-enrich"
action_params = { provider = "flexible-context", metadata_key = "context" }
[[rules]]
name = "enrich-complex-prepend"
enabled = true
detector = "complexity"
detector_params = { threshold = 0.6, keywords = ["refactor", "architecture", "design"] }
rewriter = "enrichment-prepend"
rewriter_params = { enrichment_keys = ["context"] }
action = "noop"Rewriter Runs Before Action
Within a single rule, the rewriter runs before the action. So a rule that both enriches (tool-enrich) and prepends (enrichment-prepend) will not work — the rewriter runs before the enrichment metadata exists. Split into two rules sharing the same detector under all_match.
Input and Summary Modes
Provider tools accept these input_mode values:
| Mode | Description |
|---|---|
query_passthrough (default) | The user's message text becomes the tool query |
static | Uses only default_params — no query input |
template | Renders input_template with minijinja |
metadata_template | Renders input_template with detection metadata |
Summary summary_format values: raw (default), search, graph, stats, template.