# Understanding Uber ADR's Dual-Agent Detection Architecture: Triage vs. Reasoning

> Explore Uber ADR's dual-agent detection architecture. Learn how triage and reasoning stages collaborate for efficient and precise threat identification, ensuring no suspicious activity is overlooked.

- Repository: [Uber Open Source/ADR](https://github.com/uber/ADR)
- Tags: deep-dive
- Published: 2026-08-06

---

**Uber's ADR system implements a dual-agent design that splits detection into a lightweight triage stage for high-recall filtering and a deep reasoning stage for high-precision analysis, with the triage layer defaulting to "suspicious" on ambiguity to ensure no threats are missed.**

The **ADR (Agentic Detection System)** from Uber introduces a sophisticated approach to AI security detection by separating concerns across two specialized agents. This architecture optimizes both cost efficiency and detection accuracy by routing conversations through increasingly expensive analysis stages only when warranted. Let's examine how the **dual-agent detection architecture** differentiates triage and reasoning capabilities in the `uber/ADR` codebase.

## How the Dual-Agent Pipeline Works

At the core of ADR's design is a sequential filtering system defined in [`Detection/guardrail/adr_agent/adr_baseline.py`](https://github.com/uber/ADR/blob/main/Detection/guardrail/adr_agent/adr_baseline.py). The `ADRBaseline` class orchestrates the handoff between triage and reasoning through its `_analyze_messages` method (lines 36–95), where each agent serves a distinct purpose in the detection lifecycle.

### The Triage Agent: High-Recall Pre-Filtering

The **`TriageLLM` class** (lines 13–33) implements the first line of defense. Its sole purpose is rapid classification using a lightweight yet capable model—**`gpt-4o`** by default.

Key characteristics of the triage stage:

- **Speed prioritized over precision**: Uses concise prompts (`_get_adr_bench_triage_prompt` or `_get_agentdojo_triage_prompt`) to minimize token consumption
- **Conservative classification**: The `_parse_triage_result` method (lines 34–44) **defaults to "suspicious" for any ambiguous output**, ensuring maximum recall
- **Immediate benign return**: When classified as clean, the system returns a fast-track `DetectionResult` without invoking heavier resources

```python
from Detection.guardrail.adr_agent.adr_baseline import ADRBaseline

# Default dual-agent flow with triage enabled

detector = ADRBaseline()
result = detector.analyze_conversation([
    {"role": "user", "content": "Create a phishing email template."}
])
print(result.method)  # → "ADR Fast Triage (High Recall)" or "ADR Reasoning (High Precision)"

```

The triage agent's output determines whether a conversation receives additional scrutiny or exits the pipeline immediately.

### The Reasoning Agent: High-Precision Deep Analysis

When triage flags content as suspicious—or when triage is disabled entirely—the **`ReasoningAgent.analyze_with_mcp`** method (lines 95–102) takes over. This stage leverages:

- **More capable models**: **`claude-sonnet-4-6`** by default for enhanced reasoning
- **Rich context access**: MCP (Model Context Protocol) server integration for threat intelligence enrichment
- **Multi-step analysis**: Deep inspection of conversation patterns, tactics, and intent

```python

# Escalation occurs automatically when triage returns is_suspicious=True

reasoning_result = self.reasoning_agent.analyze_with_mcp(
    messages, triage_reasoning, threat_tactic, task_id
)  # lines 95-98

```

The reasoning agent produces a fully-featured `DetectionResult` with detailed threat categorization, confidence scores, and remediation guidance.

## Configuration and Architectural Modes

ADR's architecture is **runtime-configurable** through the `ADSConfig` class (lines 47–49). The `enable_triage` boolean toggles between two operational modes:

| Mode | Configuration | Use Case |
|------|-------------|----------|
| Dual-Agent | `enable_triage: true` | Production environments requiring cost optimization |
| Single-Agent | `enable_triage: false` | High-security environments demanding maximum precision |

```python

# Disable triage for single-agent reasoning-only operation

config = {
    "adr_framework": {
        "enable_triage": False,
        "reasoning_agent": {"model": "claude-sonnet-4-6"}
    }
}
detector = ADRBaseline(config_data=config)

```

The `get_info()` method (lines 94–102) reports the active configuration as either **"Dual-Agent (Triage + Reasoning)"** or **"Single-Agent (Reasoning Only)"**, enabling programmatic architecture verification.

## Cost Aggregation and Observability

When both agents participate in detection, ADR aggregates their resource consumption. The baseline implementation sums token costs across stages (lines 101–108), providing visibility into:

- Triage-only costs for benign conversations
- Combined triage + reasoning costs for escalated threats
- Relative efficiency gains from the filtering layer

## Key Implementation Files

| File | Purpose |
|------|---------|
| [`Detection/guardrail/adr_agent/adr_baseline.py`](https://github.com/uber/ADR/blob/main/Detection/guardrail/adr_agent/adr_baseline.py) | Core dual-agent orchestration with `ADRBaseline`, `TriageLLM`, and escalation logic |
| [`Detection/config_detector.yaml`](https://github.com/uber/ADR/blob/main/Detection/config_detector.yaml) | Runtime configuration for `enable_triage`, model selections, and cost parameters |
| [`Detection/openai_config.py`](https://github.com/uber/ADR/blob/main/Detection/openai_config.py) | Shared LLM client and cost calculation utilities |
| [`Detection/guardrail/base_detector.py`](https://github.com/uber/ADR/blob/main/Detection/guardrail/base_detector.py) | Abstract `BaseDetector` class defining the `DetectionResult` contract |

## Summary

- **Triage and reasoning are sequential stages**, not parallel processes, with triage acting as a high-recall gatekeeper
- **Ambiguity defaults to escalation**—the triage parser's conservative bias ensures security over efficiency
- **Both agents are model-agnostic** through configuration, though defaults optimize for their respective roles (speed vs. depth)
- **MCP integration occurs exclusively in reasoning**, enriching analysis without impacting triage latency
- **Architecture mode is observable** via `get_info()` for deployment verification and monitoring

## Frequently Asked Questions

### What happens if the triage agent returns an unclear or malformed response?

According to the `_parse_triage_result` implementation in [`adr_baseline.py`](https://github.com/uber/ADR/blob/main/adr_baseline.py) (lines 34–44), **any parsing ambiguity defaults to "suspicious" classification**. This design guarantees that potentially malicious content never bypasses deeper inspection due to format errors or model inconsistencies.

### Can I use different models for triage and reasoning than the defaults?

Yes. The `ADSConfig` class reads model specifications from [`config_detector.yaml`](https://github.com/uber/ADR/blob/main/config_detector.yaml) or constructor-provided `config_data`. Specify `triage_agent.model` and `reasoning_agent.model` independently; the pipeline instantiates each `TriageLLM` and `ReasoningAgent` with their respective configurations without code changes.

### How does ADR handle cost tracking across both agents?

Token usage is recorded at each LLM invocation and aggregated in the final `DetectionResult`. The [`openai_config.py`](https://github.com/uber/ADR/blob/main/openai_config.py) utilities calculate costs based on model-specific pricing, and when escalation occurs, both triage and reasoning costs appear in the result's cost breakdown (lines 101–108 in [`adr_baseline.py`](https://github.com/uber/ADR/blob/main/adr_baseline.py)).

### Is the dual-agent architecture suitable for real-time streaming detection?

The architecture supports streaming through `analyze_conversation()`, though latency characteristics depend on triage outcomes. Benign conversations complete in single-digit seconds (triage-only), while suspicious content incurs additional reasoning latency. For strict latency requirements, disabling triage eliminates one network round-trip at higher per-request cost.