# Project export: Plan B Agent

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: When life gives you lemons, we send an AI agent to verify if there's actually a lemonade stand. When your Plan A falls through, Plan B is here to save the day so you can move forward, with your plan.
- Devpost: https://devpost.com/software/plan-b-agent
- GitHub: https://github.com/lilyMaung/PlanB-Agent
- Video: https://www.youtube.com/embed/p0WSOWe6wiY?enablejsapi=1&hl=en_US&rel=0&start=&version=3&wmode=transparent
- Team: 1 GitHub contributor(s) — lilyMaung (6 commits)

## Devpost submission (written by the team)

### Inspiration

As an international student navigating life in America, there were several times when my Plan A's fail and I felt anxious, tending to overthink everything. There were countless moments where life didn't always go as planned, you might fail a startup, you might get rejected from an internship or a job or places where you really want to be. "When one door is shut, another is opened", a saying popped into my head. Then I thought to myself what if there is a tool that will help to open that new door, a real Plan B, to slow down emotional roller coaster. So for my first hackathon, I decided to build that door for myself and other people who are in the same situation as me.

### What it does

Plan B is a two-agent system. You describe what fell through, a missed cohort, a rejected internship, anything in plain language. 1.The Triage agent, powered by Claude, reads it and extracts the real shape of the problem: your goal, your timeline, what's actually blocking you. 2.It hands this off over a genuine Fetch.ai agent-to-agent message, not a function call, to the Pathfinder agent. Pathfinder searches the live web, then opens a real Browserbase cloud browser session to visit the page and check there's a genuine way to reach out. 4.If it can't verify a real contact method, it throws the option away completely. 5.For what survives, Claude drafts a specific, ready-to-edit outreach email using only the details it actually confirmed. How I built it Two independently running agents built on Fetch.ai's uAgents framework, communicating over real inter-agent messaging, both registered on Agentverse with mailbox authentication. One of Claude's best qualities is reasoning so I have decided to include Claude in this role. Claude's role is to perform three distinct reasoning roles across the system Constraint extraction, Search-strategy decisions, Grounded result evaluation, each scoped to its own agent. Search-strategy decisions, Grounded result evaluation, each scoped to its own agent. In order for the agents to give answers based on real world, I used Browserbase and Stagehand that provides real cloud browser verification, not local simulation. A Flask API bridges the agents to a React/Vite frontend. My purpose for this project is to show what AI agent can do that regular AI tools and wrappers cannot which is giving non hallucinated answer. So I decided to go with this feature to make sure the users actually get what they are expecting(a real plan B product) The core architectural decision was building a real anti-hallucination filter directly into the code with is_doable(), is_real_candidate(), sanitize_contact_method() so an unverifiable option is structurally incapable of reaching the user, not just discouraged by a prompt. Challenges I ran into As a solo, first time hacker, the biggest challenge was what every developer has to face: debugging unfamiliar failure modes under time pressure, the API overload, garbled extraction data from real websites, model hallucination toward placeholder values, under time pressure, and learning to verify every fix with real evidence to solve the real problem instead of assuming it worked. Claude's agentic tool-use can pause mid-turn on long, multi-step requests, a mechanism I hadn't worked with before. I built a continuation loop so the agent could pick back up and finish reasoning properly, which taught me a lot about how Claude handles extended tool use in practice. Fetch.ai's side taught me something different, how genuinely convenient it is to register an agent on Agentverse and make it discoverable through ASI:One. That part came together smoothly. The harder part was a Python 3.9 compatibility wall that blocked full Agent Chat Protocol support specifically, I diagnosed this precisely (confirmed via pip index versions uagents-core, no compatible version exists for my environment) rather than guessing around it. Since time was running low, I moved forward with the working version, which taught me a lot about navigating sponsor tools efficiently under real constraints. Accomplishments that I am proud of Registering in UC Berkeley AI hackathon hosted by Calhacks, one of the biggest AI hackathons in the world with more than 1300 people with different experiences, as my first hackathon and on top of that, I am a first time hacker. Despite all the anxiety as a first time hacker and lack of experience, I really glad I applied for this. I wasn't expecting my project to be finished in 24 hours but I can proudly say I did finish it. I went from "I only heard about the AI agents but I don't know what they do" to "I built an AI agent to solve a problem that I am having", thanks to sponsors (mainly Anthropic and Fetch.ai)and their workshops. It helped me get familiar with AI agents and to actually use them in my projects for better quality, performance and solution. What I learned What is an AI agent? What can it do? What makes them different from regular wrappers? There are more than one agent and that they can communicate with one another. How real multi-agent systems actually communicate (not just simulate), how to build verification into code instead of trusting a prompt, and how to debug unfamiliar systems methodically, isolating each layer, demanding raw evidence instead of summaries, testing out after a few lines of code, and refusing to declare something "fixed" until I'd seen it work myself cause I am not only building for the hackathon but also for the real users. Another challenge was at first, I was automating browser on my own laptop instead of on the cloud then I learned about browserbase and its cloud infrastructure, making me change my direction. Switching to browserbase taught me how different cloud-based browser automation actually is from local testing, real network latency, real session lifecycle management, and a session start/navigate/act/extract/end flow that has to be handled deliberately, not assumed. Once switched over, I could see real Browserbase session logs confirming every page visit was genuinely happening in their cloud, not simulated which became the core proof behind my whole "verified, not guessed" approach.I cannot let un polished, untrustworthy product for the users to use. It makes me focus on one feature at a time, implement, test, debug, build and repeat again. This project and hackathon makes me a better Software Engineer(SWE).

### What's next

1.Full ASI:One Chat Protocol once migrated off Python 3.9 2.Expanding Pathfinder's real-world actions beyond drafting, and generalizing Making the candidates from 1 to 2-3. 4.Deployment with Docker, vercel and render.

## README (from the GitHub repository)

<div align="center">

# Plan B Agent

### When life gives you lemons, we send an AI agent to go verify if there's actually a lemonade stand. 
### Plan A doesn't work out? Don't crash out! We are here to help you with your plan B.

<img width="1200" height="675" alt="Plan B Agent banner" src="https://github.com/user-attachments/assets/a3533026-29d9-4608-8c28-c5dee89c0661" />

![tag:innovationlab](https://img.shields.io/badge/innovationlab-3D8BD3)
![tag:hackathon](https://img.shields.io/badge/hackathon-5F43F1)
![Built with Claude](https://img.shields.io/badge/Built%20with-Claude-D97757)
![Fetch.ai uAgents](https://img.shields.io/badge/Fetch.ai-uAgents-6C5CE7)
![Browserbase](https://img.shields.io/badge/Verified%20by-Browserbase-1D9E75)

*Built solo for the UC Berkeley AI Hackathon 2026 (Cal Hacks) — my first hackathon.🥳 *

</div>

---

<img width="1166" height="998" alt="image" src="https://github.com/user-attachments/assets/63aeb055-e236-4c74-ba46-a85c77b99dce" />

## The problem

Life doesn't always go our way. What happens when your Plan A doesn't work out?

Everybody should have a Plan B but it's overwhelming to think about one when your Plan A just failed and you're getting anxious, scrambling, unsure what's even still possible. If your brain is stressed, it's hard to think of a solutio. What actually helps isn't more advice. It's knowing what's real, verified, and actionable, with a clear first step.

**That's what Plan B Agent does.**

## What it does

Plan B is a two-agent system. You describe what fell through, in plain language. One agent figures out the real shape of your problem. The other goes out, searches the live web, verifies a real way to act exists, and drafts a ready-to-edit outreach message — never showing you anything it couldn't confirm is real.

<img width="528" height="556" alt="Screenshot 2026-06-20 at 11 37 25 PM" src="https://github.com/user-attachments/assets/b9ec46c3-124a-4fb4-87ce-f377b421c447" />


## Meet the agents

| Agent | Role |
|---|---|
| **Triage** | Reads your description, extracts structured constraints — goal, timeline, blocker, urgency |
| **Pathfinder** | Searches the live web, verifies real options exist, drafts your outreach |

### How it works, step by step

1. **Triage** reads your description and extracts structured constraints: goal, timeline, blocker, urgency.
2. **Triage** hands this off to **Pathfinder** over a real Fetch.ai agent-to-agent message — not a function call, a genuine inter-agent protocol exchange.
3. **Pathfinder** uses Claude with web search to generate concrete candidate options, grounded in real search results, not memorized knowledge.
4. For each candidate, **Pathfinder** opens a real **Browserbase** cloud browser session, navigates to the option's actual page, and looks for a real way to reach out (email, contact form, apply link).
5. If no real, verifiable contact method is found, **the option is dropped** — never shown as a guess. We only give you a solution that's actually solvable. This is enforced in code, not just prompted for.
6. For surviving options, Claude drafts a short, specific outreach email using **only** the verified details extracted from the real page.
7. You see a verified result and a ready-to-edit draft. Nothing is sent automatically until you review and send it yourself.

## Why this isn't a wrapper

Unlike wrappers, these two agents are **independently running agents**, built on Fetch.ai's `uagents` framework, communicating over the real Agent Chat/message protocol, not two prompts to one model dressed up as agents.

Claude's biggest strength is reasoning, so Claude performs **three distinct reasoning jobs** across the system, each scoped to a different agent and a different responsibility:

1. Constraint extraction
2. Search-strategy decisions
3. Grounded evaluation of real results

Unlike regular AI tools that sometimes hallucinate and hand you a "Plan B" that isn't actually an option, Plan B Agent puts the user first so I made one feature mandatory, not optional. That feature is

> **The anti-hallucination filter.** Any candidate without a genuine, Browserbase-verified contact method is discarded before it ever reaches the user. This is a hard rule enforced in code — `is_doable()`, `is_real_candidate()`, `sanitize_contact_method()` — not a hope baked into a prompt.

And it's a real action, not just advice: the system doesn't just suggest, it navigates a real page and produces a usable, specific draft grounded in what it actually found there.

## Tech stack

### Backend (Python)

| Tool | Role |
|---|---|
| **`uagents` (Fetch.ai)** | Multi-agent framework — Triage and Pathfinder run as a `Bureau`, communicate over real agent messaging, registered on Agentverse (`mailbox` + `publish_agent_details`) |
| **Anthropic SDK (`anthropic`, `AsyncAnthropic`)** | Claude calls powering Triage's extraction and Pathfinder's search/evaluation/drafting logic |
| **Stagehand (`AsyncStagehand`) + Browserbase** | Live cloud browser automation — real agentic web actions, not local simulation |
| **Flask + flask-cors** | REST API server bridging the agents to the frontend (`backend/app.py`) |
| **Pydantic** | Message schemas between agents (`messages.py`) |
| **python-dotenv** | Environment config loading |
| `cosmpy`, `bech32`, `ecdsa`, `pycryptodome` | Crypto/wallet dependencies pulled in by `uagents` |

### Frontend (JavaScript)

| Tool | Role |
|---|---|
| **React** | UI |
| **Vite** | Dev server / bundler |
| **ESLint** | Linting |

### Architecture flow

```
React frontend
   |
   v
POST /api/plan  (Flask)
   |
   v
Triage agent (Fetch.ai uAgent, Bureau, Agentverse-registered)
   -> Claude: constraint extraction
   |
   v   (real Fetch.ai agent-to-agent message)
Pathfinder agent (Fetch.ai uAgent)
   -> Claude: search strategy + grounded evaluation
   -> Stagehand / Browserbase: real page verification
   -> Claude: outreach drafting
   |
   v
result.json -> Flask -> React frontend
```

## Agentverse registration

Both agents are registered on Agentverse via `mailbox=True` and `publish_agent_details=True`, confirmed with a live-acquired mailbox access token.

- **Triage agent:** `agent1q2x6qgah36a43gl6xu59xc93j862n02tw5waqfw5kx8m8584jamfgqw28ht`
- **Pathfinder agent:** `agent1qv4t44yqllc74w3ykc5kwhfepnd6ff5akkq90cdr906pluwxlh57kuyxjl2`

<details>
<summary><strong>A note on Chat Protocol (click to expand)</strong></summary>
<br>

Full Agent Chat Protocol (`uagents_core.contrib.protocols.chat`) was implemented following Fetch.ai's official quickstart pattern, but the module is unavailable in any `uagents-core` version compatible with our Python 3.9 environment — confirmed via `pip index versions uagents-core`, no newer version exists.

Given hackathon time constraints, and as a solo hacker, I prioritized a fully working, verified agent pipeline over a last-minute Python version migration. Core Agentverse registration and inter-agent communication are real and fully verified; ASI:One conversational discoverability was not completed in the time available.

</details>

## Running it locally

### Prerequisites

- Python 3.9+
- Node.js
- An Anthropic API key
- A Browserbase API key + Project ID

### Setup

```bash
# clone the repo
git clone https://github.com/YOUR_USERNAME/plan-b-agent.git
cd plan-b-agent
```

```bash
# python environment
python3 -m venv venv
source venv/bin/activate
pip install -r backend/requirements.txt
```

```bash
# environment variables
cp .env.example .env
# then fill in: ANTHROPIC_API_KEY, BROWSERBASE_API_KEY, BROWSERBASE_PROJECT_ID,
# TRIAGE_SEED, PATHFINDER_SEED
```

```bash
# frontend dependencies
cd frontend
npm install
cd ..
```

### Run it (3 terminals)

```bash
# Terminal 1 — agents
cd backend
source ../venv/bin/activate
python3 run_agents.py
```

```bash
# Terminal 2 — Flask API
cd backend
source ../venv/bin/activate
python3 app.py
```

```bash
# Terminal 3 — frontend
cd frontend
npm run dev
```

Open **`http://localhost:5173`** and describe a 

[README truncated for size]

## Detected evidence (automated analysis)

Indexed codebase: 20 recognized source files, 55 KB.
- Anthropic (technology) — detected in the code
- CSS (language) — detected in the code
- Flask (technology) — detected in the code
- HTML (language) — detected in the code
- JavaScript (language) — detected in the code
- Python (language) — detected in the code
- React (technology) — detected in the code
- AI coding agent: GitHub Copilot — evidence: config files committed to the repository

## Codebase structure (from repository index)

### Files (27 of 27)

```
.github/copilot-instructions.md
.gitignore
api/plan.py
api/plan/status.py
api/status.py
backend/app.py
backend/data/demo_result.json
backend/data/result.json
backend/data/vercel.json
backend/messages.py
backend/pathfinder_agent/__init__.py
backend/pathfinder_agent/agent.py
backend/requirements.txt
backend/run_agents.py
backend/triage_agent/__init__.py
backend/triage_agent/agent.py
frontend/.gitignore
frontend/eslint.config.js
frontend/index.html
frontend/package.json
frontend/README.md
frontend/src/App.css
frontend/src/App.jsx
frontend/src/index.css
frontend/src/main.jsx
frontend/vite.config.js
README.md
```

### Dependencies

- backend/requirements.txt: aiohappyeyeballs@==2.6.1, aiohttp@==3.13.5, aiosignal@==1.4.0, annotated-types@==0.7.0, anthropic@==0.111.0, anyio@==4.12.1, async-timeout@==5.0.1, attrs@==26.1.0, bech32@==1.2.0, blinker@==1.9.0, certifi@==2026.6.17, charset-normalizer@==3.4.7, click@==8.1.8, cosmpy@==0.9.3, distlib@==0.4.3, distro@==1.9.0, docstring_parser@==0.18.0, ecdsa@==0.19.2, exceptiongroup@==1.3.1, filelock@==3.19.1, Flask@==3.1.3, flask-cors@==6.0.5, frozenlist@==1.8.0, googleapis-common-protos@==1.75.0, grpcio@==1.80.0, h11@==0.16.0, httpcore@==1.0.9, httpx@==0.28.1, idna@==3.18, importlib_metadata@==8.7.1, itsdangerous@==2.2.0, Jinja2@==3.1.6, jiter@==0.15.0, jsonschema@==4.25.1, jsonschema-specifications@==2025.9.1, MarkupSafe@==3.0.3, multidict@==6.7.1, platformdirs@==4.4.0, propcache@==0.4.1, protobuf@==5.29.6, pycryptodome@==3.23.0, pydantic@==2.8.2, pydantic_core@==2.20.1, python-dateutil@==2.9.0.post0, python-discovery@==1.4.2, python-dotenv@==1.2.1, referencing@==0.36.2, requests@==2.32.5, rpds-py@==0.27.1, six@==1.17.0, sniffio@==1.3.1, stagehand@==3.21.0, typing_extensions@==4.15.0, uagents@==0.20.1, urllib3@==2.6.3, uvicorn@==0.30.6, virtualenv@==21.5.1, Werkzeug@==3.1.8, yarl@==1.22.0, zipp@==3.23.1
- frontend/package.json: @eslint/js@^10.0.1, @types/react@^19.2.14, @types/react-dom@^19.2.3, @vitejs/plugin-react@^6.0.1, eslint@^10.3.0, eslint-plugin-react-hooks@^7.1.1, eslint-plugin-react-refresh@^0.5.2, globals@^17.6.0, react@^19.2.6, react-dom@^19.2.6, vite@^8.0.12

### Recent commits (newest first)

- Moved the vercel.json to frontend
- Fix Vercel build - include devDependencies for Vite
- Fix Vercel build
- Fixed the vercel
- Addd vercel serverless functions for free demo
- ADDED the Demo mode
- ADDed demo mode with pre-verified real result for free deployment without live API Calls
- Enhance README with images and detailed descriptions
- Readme
- ReadMe
- Revise README for clarity and project details
- draft readme
- Initial commit
- Plan B - multi agent system with Fetch.ai and Anthrophic

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

### .github/copilot-instructions.md

```markdown
Purpose: Help AI assistants become productive quickly in this repository.

Overview
- This project is a small Plan-B helper composed of two conversational agents (triage and pathfinder) hosted in a Bureau and a minimal frontend + REST shim.
- Key runtimes: Python (backend agents + Stagehand + Anthropi c clients) and a React/Vite frontend.

Quick start (developer)
- Run the agents/bureau: set environment variables in a .env at repository root (TRIAGE_SEED, PATHFINDER_SEED, ANTHROPIC_API_KEY, BUREAU_PORT). Then:
  - python -m plan-b-agent.backend.run_agents
  - The triage REST endpoint is exposed at http://127.0.0.1:<BUREAU_PORT>/triage
- Run the lightweight backend REST shim used by the demo UI: start `PlanB.py` (a Flask app) which POSTs to the triage endpoint. Use FLASK_BACKEND_PORT to change port.
- Frontend dev: from `frontend/` run `npm install` then `npm run dev` (uses Vite).

What matters to an AI assistant
- Agents: see `backend/triage_agent/agent.py` and `backend/pathfinder_agent/agent.py`.
  - Agents use `uagents.Agent` with seeded deterministic addresses (TRIAGE_SEED/PATHFINDER_SEED). The Bureau (in `backend/run_agents.py`) registers both agents and owns the HTTP endpoint.
  - Triage extracts structured constraints from freeform text and sends a `ConstraintsMessage` (defined in `backend/messages.py`) to the pathfinder agent.
  - Pathfinder generates candidate options (uses Anthropi c tool-enabled messages and Stagehand for scraping) and validates them by navigating candidate URLs.

Important files and examples
- `backend/messages.py` — Pydantic/uagents model for cross-agent payloads (ConstraintsMessage). Use this shape when sending between agents.
- `backend/triage_agent/agent.py` — shows REST binding (@agent.on_rest_post) and how Claude responses are parsed into `Constraints` then forwarded to `pathfinder_agent.address`.
- `backend/pathfinder_agent/agent.py` — generation prompt templates (GENERATION_PROMPT, DRAFT_PROMPT), JSON schema (`CANDIDATE_OPTIONS_SCHEMA`) and Stagehand scraping flow (`extract_contact_info`). Use these as canonical examples for tool use, schema-based output, and validation.
- `PlanB.py` — demo Flask app that triggers the triage flow and writes `data/result.json` for status polling. The UI polls `PlanB.py` for plan status.
- `frontend/` — React + Vite demo frontend. Check `frontend/package.json` scripts (dev/build/preview).

Project-specific conventions
- Deterministic agent addresses: seeds are required via env vars (`TRIAGE_SEED`, `PATHFINDER_SEED`) — do not alter address generation without updating the Bureau.
- Output artifacts: agents write status/results to `backend/data/result.json` (via write_result helpers). The frontend and `PlanB.py` expect this file.
- Tooling pattern: model calls frequently use structured output (pydantic models or JSON-schema). Prefer using the same schemas when extending or calling these agents.
- Stagehand usage: `pathfinder_agent` launches a headless browser via Stagehand to extrac
[truncated — 1333 more characters]
```

### frontend/package.json

```
{
  "name": "frontend",
  "private": true,
  "version": "0.0.0",
  "type": "module",
  "scripts": {
    "dev": "vite",
    "build": "vite build",
    "lint": "eslint .",
    "preview": "vite preview"
  },
  "dependencies": {
    "react": "^19.2.6",
    "react-dom": "^19.2.6"
  },
  "devDependencies": {
    "@eslint/js": "^10.0.1",
    "@types/react": "^19.2.14",
    "@types/react-dom": "^19.2.3",
    "@vitejs/plugin-react": "^6.0.1",
    "eslint": "^10.3.0",
    "eslint-plugin-react-hooks": "^7.1.1",
    "eslint-plugin-react-refresh": "^0.5.2",
    "globals": "^17.6.0",
    "vite": "^8.0.12"
  }
}

```

### backend/requirements.txt

```
aiohappyeyeballs==2.6.1
aiohttp==3.13.5
aiosignal==1.4.0
annotated-types==0.7.0
anthropic==0.111.0
anyio==4.12.1
async-timeout==5.0.1
attrs==26.1.0
bech32==1.2.0
blinker==1.9.0
certifi==2026.6.17
charset-normalizer==3.4.7
click==8.1.8
cosmpy==0.9.3
distlib==0.4.3
distro==1.9.0
docstring_parser==0.18.0
ecdsa==0.19.2
exceptiongroup==1.3.1
filelock==3.19.1
Flask==3.1.3
flask-cors==6.0.5
frozenlist==1.8.0
googleapis-common-protos==1.75.0
grpcio==1.80.0
h11==0.16.0
httpcore==1.0.9
httpx==0.28.1
idna==3.18
importlib_metadata==8.7.1
itsdangerous==2.2.0
Jinja2==3.1.6
jiter==0.15.0
jsonschema==4.25.1
jsonschema-specifications==2025.9.1
MarkupSafe==3.0.3
multidict==6.7.1
platformdirs==4.4.0
propcache==0.4.1
protobuf==5.29.6
pycryptodome==3.23.0
pydantic==2.8.2
pydantic_core==2.20.1
python-dateutil==2.9.0.post0
python-discovery==1.4.2
python-dotenv==1.2.1
referencing==0.36.2
requests==2.32.5
rpds-py==0.27.1
six==1.17.0
sniffio==1.3.1
stagehand==3.21.0
typing_extensions==4.15.0
uagents==0.20.1
urllib3==2.6.3
uvicorn==0.30.6
virtualenv==21.5.1
Werkzeug==3.1.8
yarl==1.22.0
zipp==3.23.1

```

### backend/app.py

```python
import json
import os

import requests
from dotenv import load_dotenv
from flask import Flask, jsonify, request
from flask_cors import CORS

#finding the .env file in the parent directory of the current file
load_dotenv(os.path.join(os.path.dirname(os.path.abspath(__file__)), "..", ".env"))

#Three constants Flask uses throughout BUREAU_PORT reads your port from .env
BUREAU_PORT = int(os.environ.get("BUREAU_PORT", 8000))
#This is the address of your Triage agent, Flask sends user descriptions here
TRIAGE_URL = f"http://127.0.0.1:{BUREAU_PORT}/triage"
#added this for demo for the frontend to know if it should show the demo mode banner instead of calling the APIs
DEMO_MODE=os.environ.get("DEMO_MODE", "false").lower() == "true"
#Also added this for demo, a separate file path for demo results 
DEMO_RESULT_PATH = os.path.join(os.path.dirname(os.path.abspath(__file__)), "data", "demo_result.json")
#The file path where pathfinder writts its result, and flask reads it back to give to the frontnd
RESULT_PATH = os.path.join(os.path.dirname(os.path.abspath(__file__)), "data", "result.json")

app = Flask(__name__)
CORS(app)


#A helper function that saves data to result.json
#os.makedirs creates the directory if it doesn't exist, and json.dump writes the data to the file with indentation for readability
#json.dump... writes the dictionary as formatted JSON with 2 space indentation
def write_result(data: dict) -> None:
    os.makedirs(os.path.dirname(RESULT_PATH), exist_ok=True)
    with open(RESULT_PATH, "w") as f:
        json.dump(data, f, indent=2)

#Main Endpoint- the one React calls when the user click "Find my plan B"
#silent=True means if parsing fails, return None instead of crashing, if the user sent nothing
@app.route("/api/plan", methods=["POST"])
def submit_plan():
    data = request.get_json(silent=True) or {}
    description = data.get("description", "").strip()
    
    if not description:
        return jsonify({"status": "error", "error": "description is required"}), 400
    #Only runs this branch when deployed, skips it locally
    if DEMO_MODE:
        #checking if our demo path exits or not
        if os.path.exists(DEMO_RESULT_PATH):
            #reads presaved real result
            with open(DEMO_RESULT_PATH) as f:
                demo_data = json.load(f)
            #write it to result.json immediately so that it could get "status":"done"
            write_result(demo_data)
        else:
            write_result({
                "status":"error",
                "error":"Demo result file not found. "
            })
        return jsonify({"status":"processing"})

    write_result({"status": "processing"})

    #Flask forwards the user's description to Triage agent
    #timeout=30 means giving up after 30 seconds if the agent doesn't respond
    #raise_for_status() throws an exception if the agent returned a 4xx/5xx error code
    #except block catches any network problem and saves a clean error message to result.json so the frontend shows a friendly error instead of freezing

    try:
        triage_response = requests.post(TRIAGE_URL, json={"description": description}, timeout=30)
        triage_response.raise_for_status()
        body = triage_response.json()
    except requests.RequestException as exc:
        write_result({"status": "error", "error": f"triage_agent unreachable: {exc}"})
        return jsonify({"status": "error", "error": str(exc)}), 502

    #Even if the HTTP request succeeded, the Triage agent might return {"status": "error"}(like when Claude extraction fails)
    if body.get("status") == "error":
        write_result({"status": "error", "error": body.get("error", "triage failed")})
        return jsonify({"status": "error", "error": body.get("error")}), 502
    #this tells React "we've started, now keep polling"
    return jsonify({"status": "processing"})


#React polls this every few seconds asking "are we done yet?" 
#if result.json doesn't exist yet, return "processing" 
#if it does exist, read it and return its contents as JSON
@app.route("/api/plan/status", methods=["GET"])
def get_status():
    if not os.path.exists(RESULT_PATH):
        return jsonify({"status": "processing"})
    with open(RESULT_PATH) as f:
        return jsonify(json.load(f))

#Only runs the server when execute this file directly(app.py)
#Gets the port from .env or defaults to 5002
if __name__ == "__main__":
    port = int(os.environ.get("FLASK_BACKEND_PORT", 5002))
    app.run(port=port, debug=True)

```

### frontend/src/main.jsx

```javascript
import { StrictMode } from 'react'
import { createRoot } from 'react-dom/client'
import './index.css'
import App from './App.jsx'

createRoot(document.getElementById('root')).render(
  <StrictMode>
    <App />
  </StrictMode>,
)

```

### frontend/src/App.jsx

```javascript
import { useEffect, useRef, useState } from "react";
import "./App.css";

const API_BASE = import.meta.env.VITE_API_BASE || "";
const POLL_INTERVAL_MS = 1500;

const REASONING_LINES = [
  "Reading what happened...",
  "Searching the web for a real option...",
  "Opening the page to verify a real contact...",
  "Drafting your outreach message...",
  "Almost ready...",
];

function InputScreen({ onSubmit }) {
  const [description, setDescription] = useState("");

  const handleSubmit = (e) => {
    e.preventDefault();
    if (!description.trim()) return;
    onSubmit(description.trim());
  };

  return (
    <div className="screen input-screen">
      <h1>Plan B</h1>
      <p className="subtitle">Tell us what fell through. We'll find a way around it.</p>
      <form onSubmit={handleSubmit}>
        <textarea
          value={description}
          onChange={(e) => setDescription(e.target.value)}
          placeholder="e.g. My flight to Chicago got cancelled and I have a meeting there tomorrow morning..."
          rows={6}
          autoFocus
        />
        <button type="submit" disabled={!description.trim()}>
          Find my plan B
        </button>
      </form>
    </div>
  );
}

function LoadingScreen() {
  const [lineIndex, setLineIndex] = useState(0);

  useEffect(() => {
    const interval = setInterval(() => {
      setLineIndex((i) => Math.min(i + 1, REASONING_LINES.length - 1));
    }, 2200);
    return () => clearInterval(interval);
  }, []);

  return (
    <div className="screen loading-screen">
      <div className="spinner" />
      <p className="reasoning-line" key={lineIndex}>
        {REASONING_LINES[lineIndex]}
      </p>
    </div>
  );
}

function OutreachDraft({ outreach }) {
  const [copied, setCopied] = useState(false);
  if (!outreach) return null;

  const handleCopy = async () => {
    try {
      await navigator.clipboard.writeText(outreach.email_draft);
      setCopied(true);
      setTimeout(() => setCopied(false), 1500);
    } catch {
      // clipboard access denied - user can still select text manually
    }
  };

  return (
    <div className="outreach">
      <div className="outreach-header">
        <span className="outreach-label">Drafted message</span>
        <button type="button" className="copy-btn" onClick={handleCopy}>
          {copied ? "Copied ✓" : "Copy"}
        </button>
      </div>
      <p className="outreach-body">{outreach.email_draft}</p>
      <p className="outreach-note">Review before sending — nothing has been sent.</p>
    </div>
  );
}

function ResultCard({ option }) {
  return (
    <div className="card">
      <div className="card-top">
        <h2>{option.title}</h2>
        <span className="verified-badge">✓ Verified</span>
      </div>
      <p className="card-description">{option.description}</p>
      <p className="why">{option.why_it_works}</p>
      {option.url && (
        <a className="option-link" href={option.url} target="_blank" rel="noreferrer">
          View source ↗
        </a>
      )}
      <OutreachDraft outreach={option.outreach} />
    </div>
  );
}

function ResultsScreen({ options, onRestart }) {
  return (
    <div className="screen results-screen">
      <h1>Here's your plan B</h1>
      <p className="subtitle">Checked for a real way to reach out before being shown to you.</p>
      <div className="card-stack">
        {options.map((option, i) => (
          <ResultCard option={option} key={i} />
        ))}
      </div>
      <button className="restart" onClick={onRestart}>
        Start over
      </button>
    </div>
  );
}

function ErrorScreen({ message, onRestart }) {
  return (
    <div className="screen error-screen">
      <h1>Something went wrong</h1>
      <p>{message}</p>
      <button onClick={onRestart}>Try again</button>
    </div>
  );
}

export default function App() {
  const [screen, setScreen] = useState("input");
  const [options, setOptions] = useState([]);
  const [error, setError] = useState("");
  const pollRef = useRef(null);

  const stopPolling = () => {
    if (pollRef.current) {
      clearInterval(pollRef.current);
      pollRef.current = null;
    }
  };

  useEffect(() => stopPolling, []);

  const startPolling = () => {
    pollRef.current = setInterval(async () => {
      try {
        const res = await fetch(`${API_BASE}/api/plan/status`);
        const data = await res.json();
        if (data.status === "done") {
          stopPolling();
          setOptions(data.options || []);
          setScreen("results");
        } else if (data.status === "error") {
          stopPolling();
          setError(data.error || "The pathfinder agent hit an error.");
          setScreen("error");
        }
      } catch {
        // transient network hiccup while polling - keep trying
      }
    }, POLL_INTERVAL_MS);
  };

  const handleSubmit = async (description) => {
    setScreen("loading");
    try {
      const res = await fetch(`${API_BASE}/api/plan`, {
        method: "POST",
        headers: { "Content-Type": "application/json" },
        body: JSON.stringify({ description }),
      });
      const data = await res.json();
      if (data.status === "error") {
        setError(data.error || "Could not reach the triage agent.");
        setScreen("error");
        return;
      }
      startPolling();
    } catch (err) {
      setError(err.message);
      setScreen("error");
    }
  };

  const handleRestart = () => {
    stopPolling();
    setOptions([]);
    setError("");
    setScreen("input");
  };

  return (
    <div className="app">
      {screen === "input" && <InputScreen onSubmit={handleSubmit} />}
      {screen === "loading" && <LoadingScreen />}
      {screen === "results" && <ResultsScreen options={options} onRestart={handleRestart} />}
      {screen === "error" && <ErrorScreen message={error} onRestart={handleRestart} />}
    </div>
  );
}

```

### backend/messages.py

```python
from uagents import Model


class ConstraintsMessage(Model):
    goal: str
    timeline: str
    blocker: str
    urgency: str
    raw_description: str

```

### frontend/vite.config.js

```javascript
import { defineConfig } from 'vite'
import react from '@vitejs/plugin-react'

// https://vite.dev/config/
export default defineConfig({
  plugins: [react()],
})

```

### frontend/index.html

```html
<!doctype html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <link rel="icon" type="image/svg+xml" href="/favicon.svg" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>frontend</title>
  </head>
  <body>
    <div id="root"></div>
    <script type="module" src="/src/main.jsx"></script>
  </body>
</html>

```

### frontend/eslint.config.js

```javascript
import js from '@eslint/js'
import globals from 'globals'
import reactHooks from 'eslint-plugin-react-hooks'
import reactRefresh from 'eslint-plugin-react-refresh'
import { defineConfig, globalIgnores } from 'eslint/config'

export default defineConfig([
  globalIgnores(['dist']),
  {
    files: ['**/*.{js,jsx}'],
    extends: [
      js.configs.recommended,
      reactHooks.configs.flat.recommended,
      reactRefresh.configs.vite,
    ],
    languageOptions: {
      globals: globals.browser,
      parserOptions: { ecmaFeatures: { jsx: true } },
    },
  },
])

```

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