Norns Navigation Domain Supervisor
Status: Production
Location: /Users/ravenhelm/ravenhelm/docs/AI-ML-Platform/norns-agent/agent/agents/domain_supervisors/navigation_supervisor.py
Domain: Navigation
Level: Domain Supervisor (Level 2)
Overview
The Navigation Domain Supervisor coordinates navigation operations including directions, travel times, and place searches. It implements a sophisticated two-phase execution model and graceful error handling to ensure users never see raw API errors.
Architecture
Component Hierarchy
NornsSupervisor (Level 1)
└── NavigationDomainSupervisor (Level 2)
├── DirectionsWorker (Level 3)
└── PlacesWorker (Level 3)
Workers
| Worker | Purpose | Operations |
|---|---|---|
| DirectionsWorker | Get step-by-step directions and travel times | directions, travel_time |
| PlacesWorker | Search for nearby places (restaurants, stores, services) | search |
Core Features
1. Two-Phase Execution
The supervisor supports intelligent two-phase execution for "find X and get directions" queries:
Phase 1: Places Search
- User: "Find tacos near me and get directions"
- System searches for taco restaurants
- Returns list of places with ratings and addresses
Phase 2: Automatic Directions
- Extracts the top result from places search
- Automatically gets directions to that location
- Uses the actual place name and address (not a generic query)
Example Flow:
User: "Find me the best breakfast spot and send me directions"
Phase 1 (PlacesWorker):
→ Search for "best breakfast" near user's location
→ Return: "**The Buttermilk Cafe** 4.8/5\n 123 Main St, New Braunfels, TX"
Phase 2 (DirectionsWorker - automatic):
→ Get directions from current location to "The Buttermilk Cafe, 123 Main St, New Braunfels, TX"
→ Return: Step-by-step driving directions
2. Location Resolution System
The supervisor uses a three-tier fallback system for resolving "current location":
Priority Order:
1. Database (users.location field) - Most authoritative
2. Database (users.home_address field) - Fallback
3. Memory context - Legacy fallback
4. Default location (New Braunfels, TX) - Last resort
Database-First Approach:
async def _resolve_user_location(self, state: HierarchicalState) -> str:
# 1. Try current location field (e.g., "New Braunfels, TX")
if row["location"]:
return row["location"]
# 2. Try home address
if row["home_address"]:
return row["home_address"]
# 3. Extract city from location field
# 4. Use default
Why Database First?
- More reliable than context extraction
- User explicitly sets their location
- Supports "near me" queries without parsing conversation
- Works even with no prior conversation history
3. Graceful Error Handling
Never Expose Raw Errors - All API errors are converted to user-friendly messages:
ERROR_MESSAGES = {
"origin": "I need to know where you're starting from. Could you tell me your starting location?",
"destination": "I need to know where you want to go. Could you tell me your destination?",
"not defined": "I'm missing some location information. Could you provide both your starting point and destination?",
"api": "I'm having trouble connecting to the maps service right now. Please try again in a moment.",
"timeout": "The maps service is taking too long to respond. Please try again.",
"default": "I couldn't complete that navigation request. Could you try rephrasing it with specific locations?",
}
Example Error Flow:
API Error: "Error: origin parameter not defined"
User Sees: "I need to know where you're starting from. Could you tell me your starting location?"
4. Slack Delivery Integration
The Navigation supervisor handles its own Slack delivery - critical for preventing duplicate messages.
Key Rule: Only sends directions to Slack, not places lists.
# Detects Slack intent
if self._should_send_to_slack(action):
# Send only directions (more useful than places list)
directions_result = next(
(r for r in results if r.worker_name == "directions_worker" and r.success),
None
)
slack_message = str(directions_result.result) if directions_result else response
await self._send_to_slack(state, slack_message)
Slack Intent Detection:
def _should_send_to_slack(self, action: str) -> bool:
"""Detects phrases like send me, DM me, slack me, etc."""
return any(phrase in action_lower for phrase in [
"send me", "dm me", "slack me", "message me",
"send on slack", "send in slack", "send it to me",
"send to slack", "to my slack", "via slack",
"in slack", "slack"
])
Slack Error Messages (user-friendly):
SLACK_ERRORS = {
"missing_scope": "I need additional permissions to send you Slack DMs. The Slack app needs to be updated.",
"user_not_found": "I couldn't find your Slack account. Make sure your Slack is connected.",
"no_slack_id": "I don't have your Slack user ID on file yet.",
"connection": "I'm having trouble connecting to Slack right now.",
}
Routing Logic
The supervisor uses LLM-based routing with memory context awareness:
Routing Prompt
Available workers:
- directions: Get step-by-step directions or travel times between TWO KNOWN locations
- places: Search for nearby places (restaurants, stores, services)
IMPORTANT RULES:
1. If user wants to FIND a place (search), use "places" worker ONLY
2. If user wants directions to a SPECIFIC KNOWN place, use "directions" worker
3. If user wants to find a place AND get directions, use "places" worker ONLY
- directions will be handled automatically after we find the place
4. NEVER use placeholder values like "best taco place" as a destination
Routing Examples
| User Query | Worker(s) | Phase 2? | Reasoning |
|---|---|---|---|
| "Find tacos near me" | places | No | Search only |
| "Directions to Costco" | directions | No | Known destination |
| "Find best breakfast and get directions" | places | Yes | Two-phase: search → directions |
| "How long to drive to Austin" | directions | No | Travel time query |
Critical Fix: Preventing Social Domain Parallel Execution
The Problem
Navigation supervisor handles its own Slack delivery, but the main supervisor was also routing to Social domain in parallel, causing:
- Duplicate Slack messages
- Confusing user experience
- Wasted API calls
The Solution
Updated the main supervisor classification prompt to exclude Social domain when Navigation handles Slack:
File: /Users/ravenhelm/ravenhelm/docs/AI-ML-Platform/norns-agent/agent/agents/supervisor.py
Domain guide:
- navigation: directions, travel time, find restaurants, nearby places
(HANDLES ITS OWN SLACK DELIVERY - do NOT add social as parallel domain)
- social: slack messages, DM me, remind me, call me, notifications
Before Fix:
{
"domain": "navigation",
"parallel_domains": ["social"], // ❌ Causes duplicate Slack messages
"action": "Find tacos and send to Slack"
}
After Fix:
{
"domain": "navigation",
"parallel_domains": [], // ✅ Navigation handles Slack itself
"action": "Find tacos and send to Slack"
}
Implementation Details
-
Main Supervisor (
supervisor.py):- Classification prompt explicitly warns against Social + Navigation parallel routing
- Navigation is flagged as self-contained for Slack delivery
-
Navigation Supervisor (
navigation_supervisor.py):- Detects Slack intent in user query
- Sends only directions to Slack (not places list)
- Provides user-friendly confirmation or error messages
Database Integration
User Location Lookup
# Query user's current location
SELECT location, home_address FROM users WHERE user_id = $1
# Fields:
# - location: Current/preferred location (e.g., "New Braunfels, TX")
# - home_address: Full home address (fallback)
Slack User ID Lookup
# Query user's Slack ID for DM delivery
SELECT slack_user_id FROM users WHERE user_id = $1
API Integration
Bifrost API (Slack Delivery)
Endpoint: POST /api/v1/mcp/slack/dm
async with httpx.AsyncClient(timeout=10.0) as client:
resp = await client.post(
f"{BIFROST_URL}/api/v1/mcp/slack/dm",
params={
"user_id": slack_user_id,
"message": message
},
)
Response Handling:
{
"success": true/false,
"error": "missing_scope|user_not_found|connection|..."
}
Streaming Architecture
The Navigation supervisor implements full streaming support:
Event Flow
1. tool_start (supervisor)
2. routing (worker selection)
3. tool_start (worker)
4. thinking (worker processing)
5. tool_end (worker)
6. worker_result
7. [Optional: Phase 2 - repeat 3-6 for directions]
8. thinking (Slack delivery if requested)
9. tool_end (supervisor)
10. domain_result (final response)
Stream Event Types
StreamEvent(type="tool_start", name="navigation_supervisor", ...)
StreamEvent(type="routing", content="Routing to workers: places", ...)
StreamEvent(type="thinking", phase="delivery", content="Sending directions to your Slack...", ...)
StreamEvent(type="domain_result", content="[Final response]", ...)
Usage Examples
Example 1: Simple Places Search
User: "Find coffee shops near me"
Flow:
- Route to PlacesWorker
- Resolve location from database: "New Braunfels, TX"
- Search for coffee shops
- Return list with ratings and addresses
Response:
Places matching 'coffee shops':
• **Starbucks** 4.2/5
123 Main St, New Braunfels, TX 78130
Open now
• **Local Coffee Co** 4.7/5
456 Oak Ave, New Braunfels, TX 78130
Open now
Example 2: Two-Phase (Search + Directions)
User: "Find the best taco place and send me directions on Slack"
Flow:
- Phase 1: PlacesWorker searches for "best taco place"
- Returns top results
- Phase 2: DirectionsWorker gets directions to #1 result
- Uses actual place name and address
- Slack Delivery: Send only directions to Slack
Response:
Places matching 'best taco place':
• **Tacos El Rey** 4.9/5
789 River Rd, New Braunfels, TX 78130
[Directions from New Braunfels, TX to Tacos El Rey, 789 River Rd, New Braunfels, TX 78130]
Estimated time: 8 minutes (3.2 miles)
1. Head south on Main St
2. Turn right on River Rd
3. Destination on right
✓ Directions sent to your Slack\!
Example 3: Directions to Known Place
User: "How do I get to Costco from home?"
Flow:
- Route to DirectionsWorker
- Resolve origin: "home" → home_address from database
- Get directions to "Costco"
Response:
Directions from 123 Home St, New Braunfels, TX to Costco:
Estimated time: 15 minutes (6.5 miles)
1. Head north on Home St
2. Turn right on I-35
3. Take exit 189
...
Error Handling Examples
Missing Destination
API Error: "Error: destination parameter not defined"
User Sees: "I need to know where you want to go. Could you tell me your destination?"
Slack User Not Found
API Error: {"success": false, "error": "user_not_found"}
User Sees: "I couldn't find your Slack account. Make sure your Slack is connected."
Testing
Manual Test Cases
# Test 1: Simple search
curl -X POST https://norns.ravenhelm.dev/api/v1/chat/send \
-H "Authorization: Bearer $TOKEN" \
-d '{"message": "Find pizza near me"}'
# Test 2: Two-phase with Slack
curl -X POST https://norns.ravenhelm.dev/api/v1/chat/send \
-H "Authorization: Bearer $TOKEN" \
-d '{"message": "Find the best sushi place and slack me directions"}'
# Test 3: Direct directions
curl -X POST https://norns.ravenhelm.dev/api/v1/chat/send \
-H "Authorization: Bearer $TOKEN" \
-d '{"message": "Directions to Whole Foods"}'
Verification Checklist
- Location resolved from database (not memory context)
- Two-phase execution for "find + directions" queries
- Only directions sent to Slack (not places list)
- No duplicate Slack messages (Social domain not called)
- User-friendly error messages (no raw API errors)
- Slack user ID lookup works
- Streaming events emitted properly
Related Documentation
- Norns Agent Architecture - Main agent overview
- Norns Streaming Architecture - Event streaming system
- Bifrost MCP Gateway - Slack API integration
- User Profile System - Database schema for users table
Troubleshooting
Issue: Duplicate Slack Messages
Symptom: User receives two identical Slack DMs
Cause: Social domain called in parallel with Navigation
Fix: Verify main supervisor prompt excludes Social when Navigation handles Slack
Check:
# In supervisor.py classification_prompt
"navigation: ... (HANDLES ITS OWN SLACK DELIVERY - do NOT add social as parallel domain)"
Issue: Wrong Location Used
Symptom: Directions start from wrong location
Cause: Location not in database, falling back to default
Fix: Update user's location in database
UPDATE users SET location = 'City, ST' WHERE user_id = 'uuid';
Issue: Places List Sent to Slack Instead of Directions
Symptom: User gets full search results on Slack instead of just directions
Cause: Slack delivery sending wrong result
Fix: Verify this code in _send_to_slack:
# Send only directions (not places list)
directions_result = next(
(r for r in results if r.worker_name == "directions_worker" and r.success),
None
)
slack_message = str(directions_result.result) if directions_result else response
Future Enhancements
- Support multi-stop routes
- Add transit/walking/biking options
- Cache frequent destinations per user
- Add voice output for directions
- Support "remind me when I'm close" notifications
- Integrate with calendar for travel time to events