# Project export: Relay AI: Context Routing for Multi-Agent AI

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: Relay intelligently routes context between AI agents, reducing token usage, latency, and cost without sacrificing output quality.
- Devpost: https://devpost.com/software/relay-ai-relay-context-routing-for-multi-agent-ai
- GitHub: https://github.com/Thegreatvegan/final_relay
- Demo: https://final-relay-two.vercel.app/
- Video: https://www.youtube.com/embed/sIrY2pg_xt4?enablejsapi=1&hl=en_US&rel=0&start=&version=3&wmode=transparent
- Team: 1 GitHub contributor(s) — Vedant (2 commits)

## Devpost submission (written by the team)

### Inspiration

Multi-agent AI systems waste tokens because every agent often receives the entire context, even when most of it is irrelevant. We wanted to solve that problem by routing information instead of compressing prompts.

### What it does

Relay is a context-routing engine for AI agents. Instead of giving every agent the full context, Relay: Breaks context into pieces Determines relevance Routes information to the agents that need it This reduces token usage, cost, and latency.

### How we built it

Built with: Anthropic Claude ASI:One (Fetch.ai) Next.js TypeScript React Flow Pipeline: Challenges The biggest challenge was proving the token savings were real. To solve this, we directly compare: Baseline: every agent gets full context Relay: agents get routed context using real token counts returned by the API.

### What we learned

Context management is becoming an infrastructure problem. As AI systems scale, intelligently distributing information may be just as important as improving the models themselves.

### What's next

Long-term memory Dynamic context caching Enterprise multi-agent systems Relay: We don't compress prompts. We route information.

## README (from the GitHub repository)

# Relay

Context-routing engine for multi-agent AI systems. Instead of sending every agent the full prompt, Relay routes only the context each agent actually needs.

## Goal

Demonstrate **70–95% token reduction** while maintaining output quality.

## Stack

- Next.js 15 + TypeScript + Tailwind
- Anthropic API + ASI:One API (OpenAI-compatible fallback)
- React Flow (agent graph) + Recharts (metrics)

## Setup

```bash
npm install
cp .env.example .env.local
# Add your API keys to .env.local
npm run dev
```

Open [http://localhost:3000](http://localhost:3000).

### Environment Variables

| Variable | Description |
|----------|-------------|
| `ANTHROPIC_API_KEY` | Anthropic API key (primary provider) |
| `ASI_ONE_API_KEY` | ASI:One API key (fallback provider) |
| `LLM_PROVIDER` | `anthropic` or `asi_one` (default: anthropic) |

## Pipeline

```
User Task → Task Analyzer → Context Graph → Context Router
  → Architect / Performance / Verification / Critic Agents
  → Merger Agent → Final Output
```

## Modes

- **Baseline**: Every agent receives full context
- **Relay**: Each agent receives targeted context slices

## Demo

Default prompt: *"Design an AI accelerator for robotics under a 50W power budget."*

Click **Run Baseline + Relay** to compare token usage, cost, and output quality side-by-side.

## Pitch

Traditional multi-agent systems send every agent all context. Relay routes only relevant information — lower cost, lower latency, fewer tokens, same output quality.


## Detected evidence (automated analysis)

Indexed codebase: 35 recognized source files, 95 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
- Tailwind CSS (technology) — detected in the code
- TypeScript (language) — detected in the code
- Vercel (technology) — claimed on Devpost, not found in the code

## Codebase structure (from repository index)

### Files (41 of 41)

```
.env.example
.gitignore
eslint.config.mjs
next.config.ts
package.json
postcss.config.mjs
project_spec.md
README.md
src/app/api/health/route.ts
src/app/api/run/route.ts
src/app/api/run/stream/route.ts
src/app/globals.css
src/app/layout.tsx
src/app/page.tsx
src/components/AgentGraph.tsx
src/components/ContextFlow.tsx
src/components/demo/activity-types.ts
src/components/demo/ActivityFeed.tsx
src/components/demo/AgentOutputsPanel.tsx
src/components/demo/ContextAtoms.tsx
src/components/demo/FinalOutputPanel.tsx
src/components/demo/LiveAgentGraph.tsx
src/components/demo/LiveMetricsBar.tsx
src/components/demo/TokenCascade.tsx
src/components/MetricsChart.tsx
src/components/PipelineProgress.tsx
src/components/ResultsPanel.tsx
src/hooks/useAnimatedNumber.ts
src/lib/agents/index.ts
src/lib/context/analyzer.ts
src/lib/context/graph.ts
src/lib/context/router.ts
src/lib/demo-events.ts
src/lib/fallback-demo.ts
src/lib/llm.ts
src/lib/pipeline-stream.ts
src/lib/pipeline.ts
src/lib/tokens.ts
src/lib/types.ts
tailwind.config.ts
tsconfig.json
```

### Dependencies

- package.json: @anthropic-ai/sdk@^0.39.0, @eslint/eslintrc@^3.2.0, @types/node@^22.10.0, @types/react@^19.0.0, @types/react-dom@^19.0.0, @xyflow/react@^12.6.0, autoprefixer@^10.5.0, eslint@^9.16.0, eslint-config-next@^15.1.0, next@^15.1.0, postcss@^8.4.49, react@^19.0.0, react-dom@^19.0.0, recharts@^2.15.0, tailwindcss@^3.4.16, typescript@^5.7.2

### Recent commits (newest first)

- Final Relay build
- Initial Relay build

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

### package.json

```
{
  "name": "relay",
  "version": "0.1.0",
  "private": true,
  "scripts": {
    "dev": "next dev",
    "build": "next build",
    "start": "next start",
    "lint": "next lint"
  },
  "dependencies": {
    "@anthropic-ai/sdk": "^0.39.0",
    "@xyflow/react": "^12.6.0",
    "next": "^15.1.0",
    "react": "^19.0.0",
    "react-dom": "^19.0.0",
    "recharts": "^2.15.0"
  },
  "devDependencies": {
    "@eslint/eslintrc": "^3.2.0",
    "@types/node": "^22.10.0",
    "@types/react": "^19.0.0",
    "@types/react-dom": "^19.0.0",
    "autoprefixer": "^10.5.0",
    "eslint": "^9.16.0",
    "eslint-config-next": "^15.1.0",
    "postcss": "^8.4.49",
    "tailwindcss": "^3.4.16",
    "typescript": "^5.7.2"
  }
}

```

### src/app/layout.tsx

```typescript
import type { Metadata } from "next";
import { Inter, JetBrains_Mono } from "next/font/google";
import "@xyflow/react/dist/style.css";
import "./globals.css";

const inter = Inter({
  subsets: ["latin"],
  variable: "--font-inter",
});

const jetbrainsMono = JetBrains_Mono({
  subsets: ["latin"],
  variable: "--font-mono",
});

export const metadata: Metadata = {
  title: "Relay — Context Routing Engine",
  description:
    "Route only relevant context to each agent. Lower cost, fewer tokens, same quality.",
};

export default function RootLayout({
  children,
}: Readonly<{
  children: React.ReactNode;
}>) {
  return (
    <html lang="en">
      <body
        className={`${inter.variable} ${jetbrainsMono.variable} min-h-screen antialiased font-sans bg-relay-bg text-relay-accent`}
      >
        {children}
      </body>
    </html>
  );
}

```

### src/app/page.tsx

```typescript
"use client";

import { useCallback, useRef, useState } from "react";
import type { ComparisonResult, ContextKey } from "@/lib/types";
import { DEMO_PROMPT } from "@/lib/types";
import type { StreamEvent, DemoPhase } from "@/lib/demo-events";
import { CORE_NODES, buildTokenCascade } from "@/lib/demo-events";
import LiveAgentGraph from "@/components/demo/LiveAgentGraph";
import ActivityFeed from "@/components/demo/ActivityFeed";
import type { ActivityEntry } from "@/components/demo/activity-types";
import LiveMetricsBar from "@/components/demo/LiveMetricsBar";
import TokenCascade from "@/components/demo/TokenCascade";
import ContextAtoms from "@/components/demo/ContextAtoms";
import MetricsChart from "@/components/MetricsChart";
import ContextFlow from "@/components/ContextFlow";
import FinalOutputPanel from "@/components/demo/FinalOutputPanel";
import AgentOutputsPanel from "@/components/demo/AgentOutputsPanel";
import type { ApiCallRecord } from "@/lib/types";

const INITIAL_VISIBLE = new Set(CORE_NODES);

export default function Home() {
  const [task, setTask] = useState(DEMO_PROMPT);
  const [loading, setLoading] = useState(false);
  const [error, setError] = useState<string | null>(null);
  const [result, setResult] = useState<ComparisonResult | null>(null);

  const [phase, setPhase] = useState<DemoPhase>("idle");
  const [activeNodeId, setActiveNodeId] = useState<string | null>(null);
  const [completedNodes, setCompletedNodes] = useState<Set<string>>(new Set());
  const [activeEdgeId, setActiveEdgeId] = useState<string | null>(null);
  const [visibleNodes, setVisibleNodes] = useState<Set<string>>(INITIAL_VISIBLE);
  const [expanded, setExpanded] = useState(false);
  const [atomKeys, setAtomKeys] = useState<ContextKey[]>([]);
  const [feed, setFeed] = useState<ActivityEntry[]>([]);
  const [baselineTokens, setBaselineTokens] = useState(0);
  const [relayTokens, setRelayTokens] = useState(0);
  const [reduction, setReduction] = useState(0);
  const [costSaved, setCostSaved] = useState(0);
  const [tokenSteps, setTokenSteps] = useState<number[]>([]);
  const [cascadeSteps, setCascadeSteps] = useState<number[]>([]);
  const [apiCalls, setApiCalls] = useState<ApiCallRecord[]>([]);
  const feedId = useRef(0);

  const pushFeed = useCallback((agent: string, message: string) => {
    feedId.current += 1;
    setFeed((prev) => [
      ...prev.map((e) => ({ ...e, isNew: false })),
      { id: String(feedId.current), agent, message, isNew: true },
    ]);
  }, []);

  const resetDemo = useCallback(() => {
    setPhase("idle");
    setActiveNodeId(null);
    setCompletedNodes(new Set());
    setActiveEdgeId(null);
    setVisibleNodes(new Set(INITIAL_VISIBLE));
    setExpanded(false);
    setAtomKeys([]);
    setFeed([]);
    setBaselineTokens(0);
    setRelayTokens(0);
    setReduction(0);
    setCostSaved(0);
    setTokenSteps([]);
    setCascadeSteps([]);
    setApiCalls([]);
  }, []);

  const handleEvent = useCallback(
    (event: StreamEvent) => {
      switch (event.type) {
        case "phase":
          setPhase(event.phase);
          break;
        case "node_active":
          setActiveNodeId(event.nodeId);
          break;
        case "node_complete":
          setCompletedNodes((prev) => new Set([...prev, event.nodeId]));
          setActiveNodeId(null);
          break;
        case "edge_active":
          setActiveEdgeId(event.edgeId);
          break;
        case "edge_complete":
          setActiveEdgeId(null);
          break;
        case "graph_expand":
          setExpanded(true);
          setVisibleNodes(new Set(event.visible));
          break;
        case "atoms":
          setAtomKeys(event.keys);
          break;
        case "routing":
          pushFeed("Context Router", event.message);
          break;
        case "packet":
          setActiveEdgeId(event.edgeId);
          break;
        case "agent_log":
          pushFeed(event.agent, `"${event.message}"`);
          break;
        case "metrics":
          if (event.baselineTokens > 0) setBaselineTokens(event.baselineTokens);
          if (event.relayTokens > 0) setRelayTokens(event.relayTokens);
          if (event.reduction > 0) setReduction(event.reduction);
          if (event.costSaved > 0) setCostSaved(event.costSaved);
          break;
        case "token_step":
          setTokenSteps((prev) => {
            const next = [...prev, event.value];
            return next.slice(-6);
          });
          setRelayTokens(event.value);
          break;
        case "api_call":
          setApiCalls((prev) => [...prev, event.record]);
          break;
        case "complete":
          setResult(event.result);
          setApiCalls(event.result.apiCalls);
          setBaselineTokens(event.result.baseline.metrics.inputTokens);
          setRelayTokens(event.result.relay.metrics.inputTokens);
          setReduction(event.result.tokenReductionPercent);
          setCostSaved(event.result.costSavings);
          setCascadeSteps(
            buildTokenCascade(
              event.result.baseline.metrics.inputTokens,
              event.result.relay.metrics.inputTokens
            )
          );
          setPhase("complete");
          setActiveNodeId(null);
          setActiveEdgeId(null);
          break;
        case "error":
          setError(event.message);
          break;
      }
    },
    [pushFeed]
  );

  const runPipeline = useCallback(async () => {
    setLoading(true);
    setError(null);
    setResult(null);
    resetDemo();

    try {
      const res = await fetch("/api/run/stream", {
        method: "POST",
        headers: { "Content-Type": "application/json" },
        body: JSON.stringify({ task }),
      });

      if (!res.ok || !res.body) {
        const data = await res.json().catch(() => ({}));
        throw new Error(data.error ?? "Stream failed");
      }

      const reader = res.body.getReader();
      const decoder = new TextDecoder();
      let buffer = "";

      while (tru
[truncated — 5838 more characters]
```

### src/lib/agents/index.ts

```typescript
import { complete, toApiCallRecord } from "../llm";
import { routeContext } from "../context/router";
import type { AgentStep, AnalyzedContext, RunMode } from "../types";

const AGENT_DEFS = [
  {
    id: "architect",
    name: "Architect Agent",
    role: "Design the autonomous vehicle stack architecture: hardware, perception, planning, SLAM, sensor fusion, networking, and safety systems.",
    systemExtra: "Focus on architecture proposal, subsystem breakdown, and key design decisions for a Level 4 robotaxi.",
  },
  {
    id: "performance",
    name: "Performance Agent",
    role: "Estimate performance metrics, latency budgets, power consumption, and benchmarks vs Waymo and Tesla.",
    systemExtra: "Provide quantitative performance estimates, BOM cost ranges, and tradeoff analysis.",
  },
  {
    id: "verification",
    name: "Verification Agent",
    role: "Verify the architecture against safety assumptions, regulatory compliance, failure modes, and redundancy requirements.",
    systemExtra: "Provide verification summary, safety case outline, and risk assessment for SF robotaxi deployment.",
  },
  {
    id: "critic",
    name: "Critic Agent",
    role: "Critically review all prior outputs for gaps, inconsistencies, and improvements.",
    systemExtra: "Provide a thorough critic review highlighting strengths, weaknesses, and missing elements.",
  },
] as const;

export async function runAgent(
  agentId: string,
  ctx: AnalyzedContext,
  mode: RunMode
): Promise<AgentStep> {
  const def = AGENT_DEFS.find((a) => a.id === agentId);
  if (!def) throw new Error(`Unknown agent: ${agentId}`);

  const { contextKeys, contextText } = routeContext(agentId, ctx, mode);
  const system = `You are the ${def.name} in a multi-agent autonomous vehicle design pipeline.
${def.role}
${def.systemExtra}
Be concise but technical. Use bullet points where helpful.`;

  const user = `Context:\n${contextText}\n\nProvide your analysis:`;
  const response = await complete(system, user);
  const apiCall = toApiCallRecord(def.name, response, mode);

  return {
    id: def.id,
    name: def.name,
    role: def.role,
    contextKeys,
    contextSent: contextText,
    output: response.content,
    usage: response.usage,
    apiCall,
  };
}

export async function runMerger(
  ctx: AnalyzedContext,
  outputs: Record<string, string>,
  mode: RunMode = "relay"
): Promise<{ deliverable: ReturnType<typeof parseDeliverable>; step: AgentStep }> {
  const system = `You are the Merger Agent. Synthesize all agent outputs into a cohesive final report for a Level 4 robotaxi AV stack.
Structure your response with these exact section headers:
## Architecture Proposal
## Tradeoff Analysis
## Verification Summary
## Performance Estimate
## Critic Review`;

  const user = `Task: ${ctx.task}

Architect Output:
${outputs.architect ?? ""}

Performance Output:
${outputs.performance ?? ""}

Verification Output:
${outputs.verification ?? ""}

Critic Output:
${outputs.critic ?? ""}

Merge into a unified final deliverable:`;

  const response = await complete(system, user);
  const deliverable = parseDeliverable(response.content, outputs);
  const apiCall = toApiCallRecord("Merger Agent", response, mode);

  return {
    deliverable,
    step: {
      id: "merger",
      name: "Merger Agent",
      role: "Combine all agent outputs into final deliverable",
      contextKeys: ["task", "summaries"],
      contextSent: user,
      output: response.content,
      usage: response.usage,
      apiCall,
    },
  };
}

function extractSection(text: string, header: string): string {
  const regex = new RegExp(`## ${header}\\s*([\\s\\S]*?)(?=## |$)`, "i");
  const match = text.match(regex);
  return match?.[1]?.trim() ?? "";
}

function parseDeliverable(
  mergedText: string,
  outputs: Record<string, string>
) {
  return {
    architectureProposal:
      extractSection(mergedText, "Architecture Proposal") || outputs.architect || "",
    tradeoffAnalysis:
      extractSection(mergedText, "Tradeoff Analysis") || outputs.performance || "",
    verificationSummary:
      extractSection(mergedText, "Verification Summary") || outputs.verification || "",
    performanceEstimate:
      extractSection(mergedText, "Performance Estimate") || outputs.performance || "",
    criticReview:
      extractSection(mergedText, "Critic Review") || outputs.critic || "",
    mergedReport: mergedText,
  };
}

export { AGENT_DEFS };

```

### src/app/api/health/route.ts

```typescript
import { NextResponse } from "next/server";

export async function GET() {
  const anthropic = Boolean(process.env.ANTHROPIC_API_KEY?.trim());
  const asiOne = Boolean(process.env.ASI_ONE_API_KEY?.trim());
  const provider = process.env.LLM_PROVIDER ?? "anthropic";

  return NextResponse.json({
    ok: anthropic || asiOne,
    anthropic,
    asiOne,
    provider,
  });
}

```

### src/app/api/run/route.ts

```typescript
import { NextRequest, NextResponse } from "next/server";
import { runComparison, runPipeline } from "@/lib/pipeline";
import type { RunMode } from "@/lib/types";

export const maxDuration = 300;

export async function POST(request: NextRequest) {
  try {
    const body = await request.json();
    const task = (body.task as string)?.trim();
    const mode = (body.mode as RunMode | "compare") ?? "compare";

    if (!task) {
      return NextResponse.json({ error: "Task is required" }, { status: 400 });
    }

    if (mode === "compare") {
      const result = await runComparison(task);
      return NextResponse.json(result);
    }

    const result = await runPipeline(task, mode as RunMode);
    return NextResponse.json(result);
  } catch (error) {
    console.error("Pipeline error:", error);
    return NextResponse.json(
      { error: error instanceof Error ? error.message : "Pipeline failed" },
      { status: 500 }
    );
  }
}

```

### src/app/api/run/stream/route.ts

```typescript
import { NextRequest } from "next/server";
import { streamComparison } from "@/lib/pipeline-stream";
import { createFallbackComparison } from "@/lib/fallback-demo";
import type { StreamEvent } from "@/lib/demo-events";

export const maxDuration = 300;

export async function POST(request: NextRequest) {
  const body = await request.json();
  const task = (body.task as string)?.trim();

  if (!task) {
    return new Response(JSON.stringify({ error: "Task is required" }), {
      status: 400,
      headers: { "Content-Type": "application/json" },
    });
  }

  const encoder = new TextEncoder();

  const stream = new ReadableStream({
    async start(controller) {
      const send = (event: StreamEvent) => {
        controller.enqueue(encoder.encode(`data: ${JSON.stringify(event)}\n\n`));
      };

      try {
        const result = await streamComparison(task, send);
        send({ type: "complete", result });
      } catch (error) {
        const message = error instanceof Error ? error.message : "Pipeline failed";
        send({ type: "error", message });
        const fallback = createFallbackComparison(task, message);
        send({ type: "complete", result: fallback });
      } finally {
        controller.close();
      }
    },
  });

  return new Response(stream, {
    headers: {
      "Content-Type": "text/event-stream",
      "Cache-Control": "no-cache, no-transform",
      Connection: "keep-alive",
    },
  });
}

```

### next.config.ts

```typescript
import type { NextConfig } from "next";

const nextConfig: NextConfig = {};

export default nextConfig;

```

### tailwind.config.ts

```typescript
import type { Config } from "tailwindcss";

const config: Config = {
  content: [
    "./src/pages/**/*.{js,ts,jsx,tsx,mdx}",
    "./src/components/**/*.{js,ts,jsx,tsx,mdx}",
    "./src/app/**/*.{js,ts,jsx,tsx,mdx}",
  ],
  theme: {
    extend: {
      colors: {
        relay: {
          bg: "#09090b",
          surface: "#0f0f11",
          elevated: "#141416",
          border: "rgba(255,255,255,0.06)",
          "border-strong": "rgba(255,255,255,0.1)",
          accent: "#fafafa",
          muted: "#71717a",
          subtle: "#a1a1aa",
        },
        demo: {
          accent: "#34d399",
          glow: "rgba(52,211,153,0.35)",
        },
      },
      fontFamily: {
        sans: ["var(--font-inter)", "system-ui", "sans-serif"],
        mono: ["var(--font-mono)", "ui-monospace", "monospace"],
      },
      letterSpacing: {
        label: "0.08em",
      },
    },
  },
  plugins: [],
};

export default config;

```

### src/components/PipelineProgress.tsx

```typescript
"use client";

const STEPS = [
  "Analyzing task",
  "Building context graph",
  "Running baseline agents",
  "Routing context",
  "Running relay agents",
  "Merging outputs",
];

export default function PipelineProgress() {
  return (
    <div className="panel px-6 py-5">
      <div className="flex items-center gap-3 mb-5">
        <div className="w-4 h-4 border border-relay-border-strong border-t-relay-accent rounded-full animate-spin" />
        <p className="text-sm text-relay-subtle">
          Running pipeline — typically 1–3 minutes
        </p>
      </div>
      <div className="space-y-2.5">
        {STEPS.map((step) => (
          <div
            key={step}
            className="flex items-center gap-3 text-sm text-relay-muted"
          >
            <span className="w-1 h-1 rounded-full bg-relay-muted" />
            {step}
          </div>
        ))}
      </div>
    </div>
  );
}

```

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