# Project export: Aegis

This document was generated by HackStack to give an AI agent context about a hackathon project. Sections are labeled with their provenance; content marked as truncated was cut to keep this document small.

## Project metadata

- Hackathon: UC Berkeley AI Hackathon 2026
- Tagline: A real-time 911 dispatch co-pilot that transcribes, translates, and protocol-grounds every call, then spins up secure AI agents to handle mass emergencies.
- Devpost: https://devpost.com/software/aegis-jtde8o
- GitHub: https://github.com/ARP-source/aegis-dispatch
- Result: winner (Best Use of ArmorIQ Platform to Build Secure Agents)
- Team: 1 GitHub contributor(s) — Anuraag (2 commits)

## Devpost submission (written by the team)

### Inspiration

911 call centers buckle under mass-casualty events, earthquakes, mass shootings, multi-vehicle pileups — when hundreds of calls arrive in languages dispatchers don't speak, all at once. I wanted to build the co-pilot that lets a single human dispatcher do the work of ten, without ever letting an AI give freelance medical advice.

### What it does

Aegis is a real-time dispatch console. A caller speaks in any language; Aegis live-transcribes, translates, classifies the incident, pulls up the correct vetted protocol card (CPR, choking, childbirth, structure fire, etc.), finds the nearest available unit by real road routing, and speaks instructions back in the caller's own language, all in under a few seconds, with a live latency breakdown per pipeline stage. When the system detects a mass emergency (e.g. our earthquake demo), it goes autonomous: every caller gets their own AI dispatch agent, grouped under area "manager" agents that allocate ambulances/police/fire across a zone with no double-booking, all reporting up to a main coordinator agent that keeps global situational awareness, while a human stays one tap away. ArmorIQ secures every agent at every level: each agent's plan is captured and cryptographically authorized via ArmorIQ's Intent Intelligence SDK before it can act, and any deviation from protocol is blocked, not just logged. We also built a live phone-to-laptop demo: open the app on your phone over a Cloudflare tunnel, hit "Talk," and watch the call appear live on the dispatcher dashboard, exactly like a real incoming 911 call.

### How we built it

Frontend: Next.js 16 (App Router) + React 19 + TypeScript + Tailwind CSS v4, a dark "operations console" design system, Leaflet + CARTO dark-matter tiles for the live incident map. Gateway: a standalone Node.js + ws WebSocket server brokering mic audio and orchestrating the whole pipeline, separate from the Next.js process for clean real-time streaming. Speech-to-text: Deepgram nova-3 with language=multi, streamed over a raw WebSocket, with PII redaction and interim results for sub-second feedback. LLM: Anthropic Claude,claude-haiku-4-5 on the hot path using structured outputs to guarantee valid JSON for translation, incident classification, and structured-field extraction; claude-sonnet-4-6 to compile the post-call incident report. Prompt compression: The Token Company compresses every prompt before it hits Claude, visible live on the latency meter. Text-to-speech: ElevenLabs multilingual voices speak instructions back to the caller in their own language. Data layer: Redis Stack — RediSearch vector search to confirm the right protocol card, GEOSEARCH for nearest unit/hospital, Streams for the per-call audit trail, Pub/Sub for the live supervisor board — with a zero-dependency in-memory fallback so the whole stack runs without Docker. Routing: OSRM for real road-network routing and ETA to the dispatched unit. Security: ArmorIQ's Intent Intelligence SDK (capturePlan + getIntentToken) signs and authorizes every agent's plan at the caller, manager, and main-coordinator level, with a local protocol-grounded guardrail as a safe fallback. Observability: Sentry traces every call as a transaction with a child span per pipeline stage, retries once on failure, and fires an SLA alert if the loop exceeds 4 seconds. Phone-as-caller: a Cloudflare Tunnel plus a custom Node reverse proxy puts the page and WebSocket gateway behind one HTTPS origin, so a real phone (which requires a secure context for mic access) can be the live caller while the laptop runs the dispatcher view.

### Challenges we ran into

Keeping caller-facing instructions safe by construction: Claude only selects and translates a pre-vetted protocol card; it never invents medical advice. That constraint shaped almost every other design decision. Streaming raw mic audio reliably from a phone, over a tunnel, into Deepgram, while keeping the dispatcher's dashboard perfectly in sync, including a tricky React bug where an unstable callbacks object caused an infinite re-render loop on the caller page, traced back to a missing useMemo. Allocating units across simultaneous incidents in an area without double-booking the same ambulance was solved with a greedy nearest-unit assignment inside each area-manager agent. Securing a 3-tier agent hierarchy with ArmorIQ rather than a single agent, so verification happens independently at the caller, manager, and main-coordinator levels.

### Accomplishments we're proud of

A full, real (non-mocked) pipeline: live Deepgram transcription, live Claude extraction, live ElevenLabs speech, live Redis geo/vector search, live OSRM routing, all wired together end to end. A working phone-as-911-caller demo that's indistinguishable from a real emergency call coming into the dashboard. A genuine multi-agent hierarchy, not a single LLM call dressed up as "agents", with cryptographically signed, ArmorIQ-verified intent at every level.

### What's next

Wire ArmorIQ's blocking path into a live "agent paused, human takeover required" UI state. Auto-resolve background/demo calls so the supervisor board self-cleans over time. Expand the protocol library and add real multi-jurisdiction unit data.

## README (from the GitHub repository)

# Aegis — Emergency Dispatch Co-Pilot

Real-time 911 dispatch co-pilot. A caller phones in — panicked, noisy, speaking **any
language**. Aegis live-transcribes and translates for an English-speaking dispatcher,
surfaces the correct **vetted protocol** steps, finds the nearest available unit and an
appropriate hospital, speaks instructions back to the caller in **their** language, and
keeps a structured, auditable incident record.

It is a **human-in-the-loop co-pilot** by default, with a documented autonomous "surge
mode" for when dispatchers are saturated.

> **The whole pipeline runs end-to-end with zero API keys and no Docker.** Every external
> service has a mock fallback, and Redis has an in-memory shim — so you can `npm run dev`
> and drive a full deterministic demo offline. Drop in real keys to go live, one service
> at a time.

---

## What's built (Tier 1)

```
 mic / demo ─▶ WS gateway ─▶ Deepgram (STT, nova-3/multi)
                   │
                   ├─ assemble prompt  ─▶ Token Company (compress)
                   │                          │
                   │                          ▼
                   │                     Claude Haiku  (translate + classify + extract, strict JSON)
                   │                          │
                   ├─ Redis vector  ◀─────────┤  pick / confirm the protocol card
                   ├─ Redis GEO     ◀─────────┘  nearest unit + hospital
                   │
                   └─▶ dispatcher UI: bilingual transcript · incident card · map ·
                       protocol steps · LATENCY METER · speak-to-caller (→ ElevenLabs)
```

Tier 2 (Sentry tracing, error fallbacks, SLA alerting) and Tier 3 (full surge mode,
supervisor board, audit-stream report → PDF) are fully implemented.

---

## Tech stack

| Layer        | Choice |
|--------------|--------|
| Frontend     | Next.js (App Router) + React + TypeScript + Tailwind v4 |
| Gateway      | Node + TypeScript WebSocket server (`ws`), separate process for clean real-time streaming |
| Map          | Leaflet + CARTO dark-matter tiles (no API key) |
| State/search | Redis Stack (RediSearch vector + GEO) **with a zero-dependency in-memory shim** |
| Embeddings   | `@xenova/transformers` (all-MiniLM-L6-v2), local; deterministic mock fallback |
| LLM          | `@anthropic-ai/sdk` — `claude-haiku-4-5` (hot path, structured outputs) · `claude-sonnet-4-6` (reports, Tier 3) |
| STT          | Deepgram `nova-3`, `language=multi` (code-switching) |
| TTS          | ElevenLabs `eleven_flash_v2_5` (low-latency multilingual) |

---

## Quick start

```bash
# 1. install
npm install

# 2. (optional) configure keys — everything works without this
cp .env.example .env        # then edit .env

# 3. run gateway + frontend together
npm run dev
```

Open **http://localhost:3000**, pick a scenario in the top bar, and click **Start demo**.

That's it — with no `.env` you'll be running fully mocked (in-memory Redis, scripted
multilingual transcript, mock translation/extraction, beep TTS) and the entire loop works.

### Going live

Add keys to `.env` and restart `npm run dev`. Each service independently flips from mock to
live based on whether its key is present:

- **`ANTHROPIC_API_KEY`** → real translation + extraction (`claude-haiku-4-5`, structured outputs).
- **`DEEPGRAM_API_KEY`** → real `nova-3`/`multi` streaming STT from the **Mic** button.
- **`ELEVENLABS_API_KEY`** → real spoken playback (otherwise a beep cue). *(mock by default)*
- **`TOKEN_COMPANY_API_KEY` + `TOKEN_COMPANY_API_URL`** → real prompt compression. *(passthrough by default)*

The top bar shows a `mock`/`live` badge per service so you always know what's active.

### Real Redis Stack (optional)

The default in-memory shim needs nothing. To use real Redis Stack instead:

```bash
docker compose up -d     # redis/redis-stack on :6379 (+ RedisInsight on :8001)
npm run seed             # seed GEO sets + build the protocol vector index
npm run dev
```

With `REDIS_URL` set and reachable, the gateway uses real RediSearch vector + GEO; if it
can't connect, it transparently falls back to the in-memory shim.

---

## Phone caller — real mic over a Cloudflare tunnel

Use your **phone as the 911 caller** (real audio → Deepgram), with the laptop as the
dispatch console showing the call live. Mobile browsers only grant mic access over
**HTTPS**, so a free Cloudflare quick-tunnel provides a trusted HTTPS origin, and a small
reverse proxy ([scripts/tunnel-proxy.mjs](scripts/tunnel-proxy.mjs)) serves the page **and**
the gateway WebSocket under that one origin (no env editing, no separate cert dance).

`cloudflared` is bundled at `./cloudflared.exe` (Windows, downloaded — not committed). Then,
with nothing else running:

```bash
npm run phone     # runs gateway + frontend + proxy + cloudflared together
```

Watch the output for the tunnel URL (`https://<random>.trycloudflare.com`), then:

- **Phone:** open `https://<random>.trycloudflare.com/caller` → tap the mic button to start
  talking, tap again to end.
- **Laptop:** open the same tunnel URL `/` (or just `http://localhost:3000`) → the dispatcher
  dashboard shows the phone's call live: transcript, incident card, map, protocol, latency.

Already running `npm run dev`? Leave it up and start the two extra pieces in separate
terminals instead: `npm run proxy` and `npm run tunnel`.

**How it works:** the phone connects as a `caller` (streams 16 kHz PCM audio); the laptop
connects as a `dispatcher` (viewer + controls). The gateway forwards a caller's full
pipeline to every dispatcher console, so one shared call drives both screens. The browser
auto-derives `wss://<host>/gateway` from the page origin.

---

## Environment variables

| Variable | Required? | Effect when missing |
|----------|-----------|---------------------|
| `DEEPGRAM_API_KEY` | optional | Mic STT runs in mock mode (use Demo) |
| `ANTHROPIC_API_KEY` | optional | Translation/extraction use keyword mock |
| `TOKEN_COMPANY_API_KEY` / `TOKEN_COMPANY_API_URL` | optional | Compression uses heuristic passthrough |
| `ELEVENLABS_API_KEY` | optional | TTS returns a beep cue |
| `REDIS_URL` | optional | In-memory Redis shim (no Docker needed) |
| `SENTRY_DSN` / `NEXT_PUBLIC_SENTRY_DSN` | optional | Sentry disabled (Tier 2) |
| `NEXT_PUBLIC_GATEWAY_WS_URL` | optional | Defaults to `ws://localhost:8080` |
| `GATEWAY_PORT` | optional | Defaults to `8080` |

Toggles: `EMBEDDINGS_MOCK=1` forces the mock embedder; `REDIS_MOCK=1` forces the in-memory
shim; `DEEPGRAM_KEYTERMS=1` enables street/landmark keyterm prompting on live STT.

---

## The deterministic demo

Three scripted, fully-offline scenarios (in `data/demo/scenarios.json`) let you show the
whole loop without any audio hardware or keys:

| Scenario | Language | Outcome |
|----------|----------|---------|
| **ES · Cardiac arrest** | Spanish | CPR protocol, Priority 1, nearest ambulance + hospital |
| **ZH · Choking (adult)** | Mandarin | Choking protocol, Priority 1 |
| **EN · Structure fire** | English | Structure-fire protocol, nearest fire unit |

Pick one in the top bar → **Start demo**. Watch, in order:

1. **Transcript** streams in the caller's language (interim text greyed), then the English
   translation fills in beneath each line.
2. **Incident card** auto-fills (location, nature, consciousness, breathing, hazards) with a
   priority accent.
3. **Map** drops the incident pin and the nearest available unit + hospital, with a route line.
4. **Protocol card** highlights the matched, vetted steps. Use **Prev/Next** to walk them.
5. **Latency meter** (top) shows per-stage timing and total loop time.
6. **Speak to caller**: type English (or hit *Speak current step*) → it's translated to the
   caller's language and played back.

### The latency meter (signature)

The top strip is a live, per-stage breakdown (Deepgram / Compression / Claude / Redis·vec /
Redis·geo / TTS) with the total loop time in large mono. The **Compression ON/OFF** tog

[README truncated for size]

## Detected evidence (automated analysis)

Indexed codebase: 49 recognized source files, 175 KB.
- Anthropic (technology) — detected in the code
- CSS (language) — detected in the code
- Next.js (technology) — detected in the code
- React (technology) — detected in the code
- Redis (technology) — detected in the code
- Tailwind CSS (technology) — detected in the code
- TypeScript (language) — detected in the code
- Node.js (technology) — claimed on Devpost, not found in the code

## Codebase structure (from repository index)

### Files (67 of 67)

```
.env.example
.gemini_diff.txt
.gitignore
apps/frontend/app/caller/page.tsx
apps/frontend/app/globals.css
apps/frontend/app/layout.tsx
apps/frontend/app/page.tsx
apps/frontend/components/CommandView.tsx
apps/frontend/components/IncidentCard.tsx
apps/frontend/components/LatencyMeter.tsx
apps/frontend/components/MapPanel.tsx
apps/frontend/components/ProtocolPanel.tsx
apps/frontend/components/ReportView.tsx
apps/frontend/components/SpeakToCaller.tsx
apps/frontend/components/SupervisorBoard.tsx
apps/frontend/components/TopBar.tsx
apps/frontend/components/TranscriptPanel.tsx
apps/frontend/components/ui.tsx
apps/frontend/instrumentation.ts
apps/frontend/lib/audio.ts
apps/frontend/lib/useGateway.ts
apps/frontend/next-env.d.ts
apps/frontend/next.config.ts
apps/frontend/package.json
apps/frontend/postcss.config.mjs
apps/frontend/sentry.client.config.ts
apps/frontend/sentry.edge.config.ts
apps/frontend/sentry.server.config.ts
apps/frontend/tsconfig.json
apps/gateway/package.json
apps/gateway/scripts/seed.ts
apps/gateway/src/config.ts
apps/gateway/src/coordination.ts
apps/gateway/src/index.ts
apps/gateway/src/pipeline.ts
apps/gateway/src/protocols/retrieval.ts
apps/gateway/src/redis/inMemory.ts
apps/gateway/src/redis/realRedis.ts
apps/gateway/src/redis/types.ts
apps/gateway/src/services/anthropic.ts
apps/gateway/src/services/armoriq.ts
apps/gateway/src/services/deepgram.ts
apps/gateway/src/services/elevenlabs.ts
apps/gateway/src/services/embeddings.ts
apps/gateway/src/services/index.ts
apps/gateway/src/services/redis.ts
apps/gateway/src/services/report.ts
apps/gateway/src/services/routing.ts
apps/gateway/src/services/tokenCompany.ts
apps/gateway/src/util/log.ts
apps/gateway/src/util/math.ts
apps/gateway/src/util/sentry.ts
apps/gateway/src/util/tokens.ts
apps/gateway/tsconfig.json
data/demo/scenarios.json
data/hospitals.json
data/keyterms.json
data/protocols.json
data/units.json
docker-compose.yml
package.json
packages/shared/package.json
packages/shared/src/index.ts
packages/shared/tsconfig.json
README.md
scripts/tunnel-proxy.mjs
tsconfig.base.json
```

### Dependencies

- apps/frontend/package.json: @aegis/shared@*, @sentry/nextjs@^10.59.0, @tailwindcss/postcss@^4.3.1, @types/leaflet@^1.9.21, @types/node@^22.10.0, @types/react@^19.2.17, @types/react-dom@^19.2.3, leaflet@^1.9.4, lucide-react@^1.21.0, next@^16.2.9, postcss@^8.5.15, react@^19.2.7, react-dom@^19.2.7, tailwindcss@^4.3.1, typescript@^5.7.3
- apps/gateway/package.json: @aegis/shared@*, @anthropic-ai/sdk@^0.105.0, @armoriq/sdk@^0.3.8, @sentry/node@^10.59.0, @types/node@^22.10.0, @types/ws@^8.18.1, @xenova/transformers@^2.17.2, dotenv@^17.4.2, redis@^6.0.0, tsx@^4.22.4, typescript@^5.7.3, ws@^8.21.0
- package.json: concurrently@^9.1.2, tsx@^4.19.2, typescript@^5.7.3
- packages/shared/package.json: typescript@^5.7.3

### Recent commits (newest first)

- feat: implement autonomous command tier with ArmorIQ
- Initial commit of Aegis Emergency Dispatch Co-Pilot

## Key source files (fetched from GitHub, selected and truncated for size)

### docker-compose.yml

```yaml
# Redis Stack = Redis + RediSearch (vector) + GEO commands + RedisInsight UI.
# Optional: the gateway falls back to an in-memory shim if this isn't running.
#   Start:  docker compose up -d
#   Seed:   npm run seed
#   UI:     http://localhost:8001  (RedisInsight)
services:
  redis:
    image: redis/redis-stack:latest
    container_name: aegis-redis
    ports:
      - "6379:6379"   # redis protocol
      - "8001:8001"   # RedisInsight web UI
    volumes:
      - aegis-redis-data:/data
    healthcheck:
      test: ["CMD", "redis-cli", "ping"]
      interval: 5s
      timeout: 3s
      retries: 5

volumes:
  aegis-redis-data:

```

### package.json

```
{
  "name": "aegis",
  "version": "0.1.0",
  "private": true,
  "type": "module",
  "description": "Aegis — real-time 911 dispatch co-pilot (live transcribe/translate, protocol-grounded guidance, nearest-unit dispatch).",
  "workspaces": [
    "packages/*",
    "apps/*"
  ],
  "scripts": {
    "dev": "concurrently -n gateway,frontend -c blue,green \"npm:dev:gateway\" \"npm:dev:frontend\"",
    "dev:gateway": "npm run dev -w @aegis/gateway",
    "dev:frontend": "npm run dev -w @aegis/frontend",
    "proxy": "node scripts/tunnel-proxy.mjs",
    "tunnel": "cloudflared.exe tunnel --url http://localhost:8088 --protocol http2",
    "phone": "concurrently -n gw,fe,proxy,tunnel -c blue,green,magenta,yellow \"npm:dev:gateway\" \"npm:dev:frontend\" \"npm:proxy\" \"npm:tunnel\"",
    "seed": "npm run seed -w @aegis/gateway",
    "build": "npm run build -w @aegis/frontend",
    "typecheck": "npm run typecheck -w @aegis/shared && npm run typecheck -w @aegis/gateway && npm run typecheck -w @aegis/frontend"
  },
  "devDependencies": {
    "concurrently": "^9.1.2",
    "tsx": "^4.19.2",
    "typescript": "^5.7.3"
  },
  "engines": {
    "node": ">=20"
  }
}

```

### packages/shared/package.json

```
{
  "name": "@aegis/shared",
  "version": "0.1.0",
  "private": true,
  "type": "module",
  "main": "src/index.ts",
  "types": "src/index.ts",
  "exports": {
    ".": "./src/index.ts"
  },
  "scripts": {
    "typecheck": "tsc --noEmit"
  },
  "devDependencies": {
    "typescript": "^5.7.3"
  }
}

```

### apps/gateway/package.json

```
{
  "name": "@aegis/gateway",
  "version": "0.1.0",
  "private": true,
  "type": "module",
  "scripts": {
    "dev": "tsx watch src/index.ts",
    "start": "tsx src/index.ts",
    "seed": "tsx scripts/seed.ts",
    "typecheck": "tsc --noEmit"
  },
  "dependencies": {
    "@aegis/shared": "*",
    "@anthropic-ai/sdk": "^0.105.0",
    "@sentry/node": "^10.59.0",
    "dotenv": "^17.4.2",
    "redis": "^6.0.0",
    "ws": "^8.21.0"
  },
  "optionalDependencies": {
    "@armoriq/sdk": "^0.3.8",
    "@xenova/transformers": "^2.17.2"
  },
  "devDependencies": {
    "@types/node": "^22.10.0",
    "@types/ws": "^8.18.1",
    "tsx": "^4.22.4",
    "typescript": "^5.7.3"
  }
}

```

### apps/frontend/package.json

```
{
  "name": "@aegis/frontend",
  "version": "0.1.0",
  "private": true,
  "type": "module",
  "scripts": {
    "dev": "next dev -p 3000",
    "build": "next build",
    "start": "next start -p 3000",
    "typecheck": "tsc --noEmit"
  },
  "dependencies": {
    "@aegis/shared": "*",
    "@sentry/nextjs": "^10.59.0",
    "leaflet": "^1.9.4",
    "lucide-react": "^1.21.0",
    "next": "^16.2.9",
    "react": "^19.2.7",
    "react-dom": "^19.2.7"
  },
  "devDependencies": {
    "@tailwindcss/postcss": "^4.3.1",
    "@types/leaflet": "^1.9.21",
    "@types/node": "^22.10.0",
    "@types/react": "^19.2.17",
    "@types/react-dom": "^19.2.3",
    "postcss": "^8.5.15",
    "tailwindcss": "^4.3.1",
    "typescript": "^5.7.3"
  }
}

```

### apps/frontend/app/layout.tsx

```typescript
import type { Metadata } from "next";
import { Public_Sans, IBM_Plex_Mono } from "next/font/google";
import "./globals.css";

// UI face: Public Sans (USWDS — correct provenance for a public-safety tool).
const publicSans = Public_Sans({
  subsets: ["latin"],
  weight: ["400", "500", "700"],
  variable: "--font-public-sans",
  display: "swap",
});

// Data face: IBM Plex Mono — latency meter, timestamps, IDs, coordinates.
const plexMono = IBM_Plex_Mono({
  subsets: ["latin"],
  weight: ["400", "500", "600"],
  variable: "--font-ibm-plex-mono",
  display: "swap",
});

export const metadata: Metadata = {
  title: "Aegis — Emergency Dispatch Co-Pilot",
  description:
    "Real-time 911 dispatch co-pilot: live transcription + translation, protocol-grounded guidance, nearest-unit dispatch.",
};

export default function RootLayout({ children }: { children: React.ReactNode }) {
  return (
    <html lang="en" className={`${publicSans.variable} ${plexMono.variable}`}>
      <body suppressHydrationWarning>{children}</body>
    </html>
  );
}

```

### apps/frontend/app/page.tsx

```typescript
"use client";
import dynamic from "next/dynamic";
import { useRef, useState } from "react";
import { useGateway } from "@/lib/useGateway";
import { startMic, type MicHandle } from "@/lib/audio";
import { TopBar, type DemoScenario } from "@/components/TopBar";
import { LatencyMeter } from "@/components/LatencyMeter";
import { TranscriptPanel } from "@/components/TranscriptPanel";
import { IncidentCard } from "@/components/IncidentCard";
import { ProtocolPanel } from "@/components/ProtocolPanel";
import { SpeakToCaller } from "@/components/SpeakToCaller";
import { SupervisorBoard } from "@/components/SupervisorBoard";
import { CommandView } from "@/components/CommandView";

// Map is client-only (Leaflet touches window).
const MapPanel = dynamic(() => import("@/components/MapPanel").then((m) => m.MapPanel), {
  ssr: false,
  loading: () => (
    <div className="flex min-h-0 flex-1 items-center justify-center text-sm text-text-muted">
      Loading map…
    </div>
  ),
});

const SCENARIOS: DemoScenario[] = [
  { id: "es-cardiac", title: "ES · Cardiac arrest (CPR)" },
  { id: "zh-choking", title: "ZH · Choking (adult)" },
  { id: "en-fire", title: "EN · Structure fire" },
  { id: "en-earthquake", title: "EN · Massive Earthquake (Surge)" },
];

export default function Page() {
  const { state, actions, sendAudio } = useGateway();
  const micRef = useRef<MicHandle | null>(null);
  const [showCommand, setShowCommand] = useState(false);

  const callerLang = state.callState?.lang ?? state.segments.at(-1)?.lang ?? "en";
  const currentStepText = state.protocol
    ? (state.protocol.card.steps[state.protocol.currentStep]?.text ?? null)
    : null;

  const handleStartMic = async () => {
    actions.startMic();
    try {
      micRef.current = await startMic(sendAudio);
    } catch {
      // Permission denied or unsupported — the gateway still emits a mock notice.
    }
  };
  const handleEnd = () => {
    micRef.current?.stop();
    micRef.current = null;
    actions.endCall();
  };

  return (
    <div className="flex h-screen flex-col bg-bg text-text">
      <TopBar
        connected={state.connected}
        mockModes={state.mockModes}
        running={state.running}
        callState={state.callState}
        surge={state.surge}
        scenarios={SCENARIOS}
        onStartDemo={actions.startDemo}
        onStartMic={handleStartMic}
        onEndCall={handleEnd}
        onToggleSurge={actions.toggleSurge}
        onToggleCommand={() => setShowCommand((v) => !v)}
      />

      <LatencyMeter
        latency={state.latency}
        compressionEnabled={state.compressionEnabled}
        onToggleCompression={actions.toggleCompression}
      />

      <main className="grid min-h-0 flex-1 grid-cols-[minmax(300px,1fr)_minmax(380px,1.5fr)_minmax(340px,1fr)]">
        {/* Left — transcript */}
        <div className="flex min-h-0 flex-col border-r border-border">
          <TranscriptPanel segments={state.segments} />
        </div>

        {/* Center — incident card + map */}
        <div className="flex min-h-0 flex-col border-r border-border">
          <IncidentCard llm={state.llm} callState={state.callState} dispatch={state.dispatch} />
          <MapPanel dispatch={state.dispatch} supervisor={state.supervisor} />
        </div>

        {/* Right — protocol + speak to caller */}
        <div className="flex min-h-0 flex-col">
          <ProtocolPanel protocol={state.protocol} onAdvance={actions.advanceProtocol} />
          <SpeakToCaller
            callerLang={callerLang}
            currentStepText={currentStepText}
            lastTts={state.lastTts}
            onSay={actions.say}
          />
        </div>
      </main>

      <SupervisorBoard calls={state.supervisor} />

      {showCommand && (
        <CommandView command={state.command} onClose={() => setShowCommand(false)} />
      )}
    </div>
  );
}

```

### apps/gateway/src/index.ts

```typescript
import { WebSocketServer, WebSocket, type RawData } from "ws";
import type { IncomingMessage } from "node:http";
import type { ClientMessage, ServerMessage } from "@aegis/shared";
import { config } from "./config";
import { log } from "./util/log";
import { initSentry } from "./util/sentry";
import { createServices } from "./services";
import { CallSession } from "./pipeline";
import { createCoordinator } from "./coordination";

function send(ws: WebSocket, msg: ServerMessage): void {
  if (ws.readyState === ws.OPEN) ws.send(JSON.stringify(msg));
}

type Role = "caller" | "dispatcher";

function parseRole(url: string | undefined): Role {
  try {
    const role = new URL(url ?? "/", "http://localhost").searchParams.get("role");
    return role === "caller" ? "caller" : "dispatcher";
  } catch {
    return "dispatcher";
  }
}

async function main(): Promise<void> {
  initSentry();
  const services = await createServices();

  const wss = new WebSocketServer({ port: config.gatewayPort });

  // Dispatcher consoles (laptops). Every caller call's pipeline events are
  // forwarded here so the dashboard renders the phone caller's live call.
  const dispatchers = new Set<WebSocket>();
  // The most-recent caller call — dispatcher controls (say / advance / toggles)
  // act on this.
  let activeCall: CallSession | null = null;

  const broadcast = (msg: ServerMessage): void => {
    for (const d of dispatchers) send(d, msg);
  };

  // Autonomous coordination tier: area managers + main agent + ArmorIQ.
  const coordinator = createCoordinator(services, (command) =>
    broadcast({ type: "command", command }),
  );

  // Supervisor board + agent hierarchy: refresh on any call-state change.
  services.redis.subscribeCallStates(() => {
    void (async () => {
      const calls = await services.redis.getActiveCalls();
      broadcast({ type: "supervisor", calls });
    })();
    coordinator.notifyChange();
  });

  // Background surge calls for the earthquake demo: they only publish call-state
  // (supervisor board + map), so they get a no-op send (no UI ownership).
  const spawnEarthquakeSurge = (): void => {
    log.info("earthquake demo: spawning background surge calls");
    setTimeout(() => {
      const bg = new CallSession(services, () => {});
      bg.setSurge(true);
      bg.start("demo", "bg-eq-fire");
    }, 3000);
    setTimeout(() => {
      const bg = new CallSession(services, () => {});
      bg.setSurge(true);
      bg.start("demo", "bg-eq-medical");
    }, 6000);
  };

  wss.on("connection", (ws: WebSocket, req: IncomingMessage) => {
    const role = parseRole(req.url);
    const isDispatcher = role === "dispatcher";

    if (isDispatcher) {
      dispatchers.add(ws);
      void (async () => {
        const calls = await services.redis.getActiveCalls();
        send(ws, { type: "supervisor", calls });
      })();
      const cmd = coordinator.snapshot();
      if (cmd) send(ws, { type: "command", command: cmd });
    }

    // A caller's events go to its own socket (the phone) AND every dispatcher.
    // A dispatcher-started demo broadcasts to all dispatchers (incl. itself).
    const sessionSend = isDispatcher
      ? (m: ServerMessage) => broadcast(m)
      : (m: ServerMessage) => {
          send(ws, m);
          broadcast(m);
        };

    let ownSession: CallSession | null = null;
    log.info(`client connected role=${role} (dispatchers=${dispatchers.size})`);

    ws.on("message", (data: RawData, isBinary: boolean) => {
      if (isBinary) {
        ownSession?.sendAudio(data as Buffer);
        return;
      }
      let msg: ClientMessage;
      try {
        msg = JSON.parse(data.toString()) as ClientMessage;
      } catch {
        log.warn("dropping non-JSON control message");
        return;
      }
      // Dispatcher controls act on the active caller call; a connection that
      // started its own call controls that one.
      const target = ownSession ?? activeCall;
      switch (msg.type) {
        case "start":
          ownSession = new CallSession(services, sessionSend);
          activeCall = ownSession;
          ownSession.start(msg.mode, msg.demoId);
          if (msg.demoId === "en-earthquake") spawnEarthquakeSurge();
          break;
        case "dispatcher_say":
          void target?.dispatcherSay(msg.text);
          break;
        case "toggle_compression":
          target?.setCompression(msg.enabled);
          break;
        case "toggle_surge":
          target?.setSurge(msg.enabled);
          break;
        case "advance_protocol":
          target?.advanceProtocol(msg.currentStep);
          break;
        case "end_call":
          void target?.end();
          break;
        default:
          log.warn("unknown client message", msg);
      }
    });

    ws.on("close", () => {
      dispatchers.delete(ws);
      if (ownSession) {
        void ownSession.end();
        if (activeCall === ownSession) activeCall = null;
      }
      log.info(`client disconnected role=${role} (dispatchers=${dispatchers.size})`);
    });
    ws.on("error", (e) => log.warn("ws error:", e.message));
  });

  log.info(`gateway listening on ws://localhost:${config.gatewayPort}`);
}

main().catch((e) => {
  log.error("gateway failed to start:", e);
  process.exit(1);
});

```

### packages/shared/src/index.ts

```typescript
/**
 * @aegis/shared — the single source of truth for types shared between the
 * gateway (Node) and the frontend (Next.js). The WebSocket protocol lives here
 * so both ends stay in lockstep.
 */

// ── Geo ──────────────────────────────────────────────────────────────────────
export interface LatLng {
  lat: number;
  lng: number;
}

// ── Vetted protocol cards (stored data — the ONLY source of caller-facing steps)
export interface ProtocolStep {
  /** Plain-language instruction, read verbatim (then translated) to the caller. */
  text: string;
  /** A literal warning/caution step (rendered with the caution color). */
  warning?: boolean;
}

export type ProtocolCategory = "medical" | "fire" | "police";

export interface ProtocolCard {
  id: string;
  title: string;
  category: ProtocolCategory;
  /** Keywords used for vector-search confirmation + keyword fallback matching. */
  triggers: string[];
  /** Ordered, vetted steps. Claude may select WHICH card applies, never invent steps. */
  steps: ProtocolStep[];
}

// ── Response units & hospitals (GEO sets) ────────────────────────────────────
export type UnitStatus = "available" | "busy";
export type UnitKind = "ambulance" | "fire" | "police";

export interface Unit {
  id: string;
  kind: UnitKind;
  status: UnitStatus;
  location: LatLng;
  label?: string;
}

export interface Hospital {
  id: string;
  name: string;
  location: LatLng;
  /** 1 = highest capability trauma center. */
  traumaLevel?: number;
}

// ── Incident extraction (LLM hot path) ───────────────────────────────────────
export type IncidentType = "medical" | "fire" | "police" | "unknown";

export interface StructuredCard {
  location: string | null;
  callbackNumber: string | null;
  nature: string | null;
  numPatients: number | null;
  conscious: boolean | null;
  breathing: boolean | null;
  hazards: string[];
}

/** STRICT JSON contract returned by the hot-path model (claude-haiku-4-5). */
export interface LlmResult {
  englishTranslation: string;
  incidentType: IncidentType;
  structuredCard: StructuredCard;
  recommendedProtocolId: string | null;
  nextInstruction: string | null;
}

/** A safe default used when the model returns malformed/empty output. */
export const EMPTY_LLM_RESULT: LlmResult = {
  englishTranslation: "",
  incidentType: "unknown",
  structuredCard: {
    location: null,
    callbackNumber: null,
    nature: null,
    numPatients: null,
    conscious: null,
    breathing: null,
    hazards: [],
  },
  recommendedProtocolId: null,
  nextInstruction: null,
};

// ── Latency meter (signature feature) ────────────────────────────────────────
export type PipelineStage =
  | "deepgram"
  | "compression"
  | "claude"
  | "redisVector"
  | "redisGeo"
  | "tts";

export const PIPELINE_STAGES: PipelineStage[] = [
  "deepgram",
  "compression",
  "claude",
  "redisVector",
  "redisGeo",
  "tts",
];

export interface StageTiming {
  stage: PipelineStage;
  ms: number;
  /** True when this stage ran in mock mode (no real key/connection). */
  mock?: boolean;
}

export interface CompressionStats {
  enabled: boolean;
  originalTokens: number;
  compressedTokens: number;
  /** Estimated ms saved on the Claude stage by sending fewer tokens. */
  savedMs: number;
}

export interface LatencyBreakdown {
  stages: StageTiming[];
  totalMs: number;
  compression: CompressionStats;
  /** SLA flag — set by the gateway when totalMs exceeds the threshold (Tier 2). */
  slaBreached?: boolean;
}

// ── Transcript ───────────────────────────────────────────────────────────────
export type Speaker = "caller" | "dispatcher";

export interface TranscriptSegment {
  id: string;
  speaker: Speaker;
  /** Detected language code (e.g. "es", "zh", "en"); "und" if unknown. */
  lang: string;
  /** Text in the speaker's own language. */
  original: string;
  /** English translation (filled in once the LLM stage runs). */
  english?: string;
  interim: boolean;
  ts: number;
}

// ── Dispatch (GEO results) ───────────────────────────────────────────────────
export interface NearbyUnit extends Unit {
  distanceKm: number;
}
export interface NearbyHospital extends Hospital {
  distanceKm: number;
}

export interface DispatchResult {
  incident: LatLng | null;
  nearestUnit: NearbyUnit | null;
  hospital: NearbyHospital | null;
  /** True road polyline returned by OSRM API */
  routeLine: LatLng[];
  /** Estimated time of arrival in seconds */
  etaSeconds?: number | null;
}

// ── Call state (supervisor board / pub-sub) ──────────────────────────────────
export type CallPriority = 1 | 2 | 3;
export type CallStatus =
  | "active"
  | "dispatched"
  | "resolved"
  | "needs-takeover";

export interface CallState {
  callId: string;
  priority: CallPriority;
  status: CallStatus;
  incidentType: IncidentType;
  lang: string;
  location: string | null;
  /** Autonomous surge mode engaged for this call. */
  surge: boolean;
  /** Set when a critical pipeline stage fails after retry — supervisor must intervene. */
  humanTakeoverRequired: boolean;
  /** Broadcast when surge mode executes an autonomous protocol step. */
  surgeAlert?: boolean;
  startedAt: number;
  updatedAt: number;
  dispatch?: DispatchResult | null;
}

// ── Which services are running in mock mode (surfaced in the UI) ──────────────
export interface MockModes {
  deepgram: boolean;
  anthropic: boolean;
  tokenCompany: boolean;
  elevenlabs: boolean;
  redis: boolean;
  embeddings: boolean;
}

// ── Agent hierarchy (autonomous surge coordination) ──────────────────────────
export type AgentLevel = "caller" | "manager" | "main";

/** ArmorIQ verdict attached to every agent decision (Intent Intelligence). */
export interface ArmorVerdict {
  /** True when verified by the live ArmorIQ SDK (vs. the local protocol guardrail). */
  live: boolean;
  status: "verified" | "blocked" | "local";
  /** ArmorIQ intent-token id + plan hash when verified live. */
  tokenId?: string;
  planHash?: string;
  reason?: string;
}

export 
[truncated — 2529 more characters]
```

### apps/gateway/src/services/index.ts

```typescript
import type { MockModes } from "@aegis/shared";
import { log } from "../util/log";
import type { RedisBackend } from "../redis/types";
import { createEmbedder, type Embedder } from "./embeddings";
import { createDeepgram, type DeepgramService } from "./deepgram";
import { createAnthropic, type AnthropicService } from "./anthropic";
import { createCompressor, type Compressor } from "./tokenCompany";
import { createTts, type TtsService } from "./elevenlabs";
import { createReportService, type ReportService } from "./report";
import { createRedis } from "./redis";
import { createRoutingService, type RoutingService } from "./routing";
import { createArmor, type ArmorService } from "./armoriq";

export interface Services {
  deepgram: DeepgramService;
  anthropic: AnthropicService;
  compressor: Compressor;
  tts: TtsService;
  redis: RedisBackend;
  embedder: Embedder;
  report: ReportService;
  routing: RoutingService;
  armor: ArmorService;
  mockModes: MockModes;
}

/** Wire every service once at startup, choosing real vs mock per available key. */
export async function createServices(): Promise<Services> {
  const embedder = createEmbedder();
  embedder.warm(); // start the model download/load in the background

  const deepgram = createDeepgram();
  const anthropic = createAnthropic();
  const compressor = createCompressor();
  const tts = createTts();
  const redis = await createRedis(embedder);
  const report = createReportService(compressor);
  const routing = createRoutingService();
  const armor = createArmor();

  const mockModes: MockModes = {
    deepgram: deepgram.mock,
    anthropic: anthropic.mock,
    tokenCompany: compressor.mock,
    elevenlabs: tts.mock,
    redis: redis.mock,
    embeddings: embedder.mock,
  };

  log.info(
    "services ready —",
    Object.entries(mockModes)
      .map(([k, v]) => `${k}:${v ? "mock" : "live"}`)
      .join("  "),
  );

  return {
    deepgram,
    anthropic,
    compressor,
    tts,
    redis,
    embedder,
    report,
    routing,
    armor,
    mockModes,
  };
}

```

[42 more indexed source files omitted to keep this export small. The full file list is in the Codebase structure section above.]