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

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. 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
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

# 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

# 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 Core dual-agent orchestration with ADRBaseline, TriageLLM, and escalation logic
Detection/config_detector.yaml Runtime configuration for enable_triage, model selections, and cost parameters
Detection/openai_config.py Shared LLM client and cost calculation utilities
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 (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 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 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).

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.

Have a question about this repo?

These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →