An autonomous multi-agent microgrid resilience system — it predicts grid stress, coordinates peer-to-peer energy transfer, and plans recovery before a failure spreads.
Seven specialised agents run as a LangGraph pipeline over a 50-home community. A 3D digital twin renders what they decide, house by house, in real time.
Prithvi S P · Darshan Pawar · Chandan Kumar K
Team Bit-N-Build — Decentralised Energy Systems & Micro-Grids
- The problem
- How FlowState works
- Screenshot tour
- Architecture
- The resilience agent in detail
- Repository layout
- Quickstart
- API reference
- Testing
- Known issues
- Roadmap
- Ownership
Challenge track — Decentralised Energy Systems & Micro-Grids Create agent networks that monitor hyper-local power generation, predict grid failures, and autonomously execute peer-to-peer energy trading during peak demand.
Most microgrid control software is reactive. It notices instability after the lights flicker, then sheds load. By then the community has already lost the cheapest option it had: the surplus sitting on its neighbours' roofs.
FlowState is built the other way round. Every cycle, seven agents evaluate generation, consumption, storage, EV demand and grid availability, score the community's failure risk, and — while that risk is still climbing — find local energy and move it.
Predict → Coordinate → Act → Stabilise.
| Layer | What it actually does |
|---|---|
| Solar agent | Reads generation and the 24 h forecast, computes surplus against current demand, flags low-generation windows |
| Battery agent | Tracks state-of-charge and health, computes available discharge, recommends charge/discharge |
| House agent | Processes all 50 households in one vectorised pass; separates critical from flexible load and forecasts next-hour demand |
| EV agent | Builds charging schedules, flexibility windows, V2G eligibility and charging priority |
| Grid agent | Tracks pricing, availability, carbon intensity and peak hours |
| Resilience agent | Scores failure risk, coordinates P2P transfers between surplus and deficit homes, and writes an autonomous recovery plan |
| Optimizer | Resolves the remaining supply-demand mismatch: battery dispatch, EV pause, load deferral, savings and carbon |
| Digital twin | Renders all of it — houses, EVs, solar farm, battery, flows — and streams the agent log live |
| ARIA | A Groq-backed advisor that answers questions grounded in the selected house and the current community state |
Every image below was captured from the running application with the Python agent backend live — not from mockups.
The left pane is the community: 50 homes on a street grid, a substation, the community battery, the solar farm and the EV charging row. It is not decoration — every element is bound to agent state.
| Scene element | Bound to |
|---|---|
| House roof colour | that household's renewable share this cycle |
| Orange corridor | solar dispatch from the farm into the grid |
| Green corridor | community battery discharge |
| Red corridor | grid import |
| Violet corridor | a peer-to-peer transfer between two homes |
| EV row | live count of vehicles actually charging (7/10 ACTIVE) |
| Battery cylinder fill | state-of-charge |
| Scene lighting and fog | the active scenario — an outage dims the whole community |
| Agent Network overlay | the live negotiation log, newest first |
The Supply Controls panel drives the agents directly: solar, battery, grid and P2P availability are inputs to the next cycle, not display filters.
Click any house to open it. Eight metric cards, an energy supply breakdown showing
exactly how many kWh came from solar, battery and grid, and a live household auditor
tagging the home as RENEWABLE, MIXED or GRID.
Because the breakdown is per-source rather than a single "green score", a home reading 81% renewable during normal operation and 0% grid dependency during an island-mode outage tells two different, verifiable stories.
Six buttons, each mapping to a state mutation in backend/agents/scenarios.py:
normal, cloud_cover, heatwave, grid_failure, ev_surge and peak_demand.
Selecting one re-runs the whole agent pipeline against the mutated community state.
This is where a resilience system earns its name. Four of these five are bad days.
Peak demand is the flagship scenario: community demand roughly doubles. All four
corridors light at once — solar, battery, grid and a violet P2P transfer — and EV
charging saturates at 10/10. This is the case the whole system was built for: the
community is meeting a spike from local resources before the grid has to absorb it.
The three headline numbers are risk_score, kWh traded locally, and how many homes are
acting as sellers versus buyers — the shape of the local market this cycle.
TRIGGER NEGOTIATE opens a WebSocket to /ws/negotiate. The backend runs
langgraph_app.stream(state) and forwards each node's new log lines as that node
executes, with a 0.38 s pace between lines — so the overlay reads as the agents
thinking rather than as a result appearing all at once.
The screenshot above is a grid_failure run: the resilience agent scores the risk at
MEDIUM (54%), the P2P agent matches 50 local trades worth 45.4 kWh, and the optimizer
closes at 100% renewable utilisation with 0.0 kW of grid import — the community is
carrying itself. Here is a peak_demand run captured from the same socket:
Solar Scenario [peak_demand]: community demand increased from 163.1 kWh to 273.8 kWh.
Solar Generation 309.9 kWh, surplus of 36.1 kWh available for battery/grid export.
Battery SoC 39.8%, health 86.4%, available discharge 25.9 kWh.
Battery Solar surplus 36.1 kWh exceeds 20% of capacity — recommending charge of 30.0 kWh.
House Processed 50 households — total demand 273.8 kWh (avg 5.48 kWh/house).
House 14 flexible, 9 critical households; 51.9 kWh of deferrable load available.
EV 10 EVs tracked — 8 charging now, total charge needed 97.4 kWh.
EV 9 EV(s) V2G-eligible (flex window >= 4h).
None of this is scripted in the frontend. If the socket cannot be reached the store
falls back to a canned replay so a demo never dead-ends on a dropped backend — you can
tell the two apart by the timestamp format: the backend sends 13:38:00, the fallback
uses the browser's locale string (1:38:00 pm).
ARIA's system prompt is built from the live community state — solar generation, battery
level, grid import, EVs charging, renewable share, active scenario — plus the selected
house, so answers are grounded in what is actually on screen. Groq
llama-3.1-8b-instant does the reasoning and Sarvam AI handles speech-to-text and
text-to-speech behind the microphone button, with English, Hindi, Telugu and Urdu
supported.
The reply in this screenshot is the keyless mock response, which is why it offers
help rather than answering the question — this capture ran without a GROQ_API_KEY.
That is the designed degradation: both LLM clients fall back to mock mode and the
console, the agents and the twin stay fully usable. Set the key to get a real answer.
Below ARIA, the household auditor's rule-based recommendation (ISLAND MODE ACTIVE. Battery storage is backing up critical appliances…) needs no LLM at all, and the 24 h
solar-vs-load forecast chart is drawn straight from community state.
FastAPI serves interactive docs at http://localhost:8000/docs.
flowchart TB
subgraph ui["Digital twin — Next.js 16 · React 19 · R3F"]
TW["CommunityScene<br/>houses · EVs · solar · battery · flows"]
PANEL["Telemetry · Resilience<br/>Scenario simulator · ARIA"]
STORE["useEnergyStore<br/>zustand"]
end
subgraph api["FastAPI"]
SIM["POST /simulate"]
WS["WS /ws/negotiate"]
CHAT["POST /chat"]
VOICE["POST /voice/stt · /voice/tts"]
end
subgraph graph["LangGraph pipeline — sequential, deterministic"]
S["Solar agent"] --> B["Battery agent"] --> H["House agent"]
H --> E["EV agent"] --> G["Grid agent"] --> R["Resilience agent"] --> O["Optimizer"]
end
subgraph llm["LLM layer"]
GROQ["Groq llama-3.1-8b-instant<br/>reasoning · ARIA"]
SARVAM["Sarvam AI<br/>STT · TTS · translation"]
end
SCEN["scenarios.py<br/>cloud_cover · heatwave · grid_failure<br/>ev_surge · peak_demand"]
TW <--> STORE
PANEL <--> STORE
STORE --> SIM & WS & CHAT & VOICE
SCEN --> S
SIM --> S
WS -->|"streams each node's log line"| STORE
O -->|"decisions + community_metrics"| SIM
CHAT --> GROQ
VOICE --> SARVAM
Two properties make this tractable:
- The graph is sequential and deterministic. No branching, no subgraphs, no dynamic
routing —
Solar → Battery → House → EV → Grid → Resilience → Optimizer, every cycle. The sameCommunityStateflows through and each agent both reads and appends to it, so any output can be traced back to the node that produced it. - The state schema is a contract.
agents/state.pycarries an explicit "DO NOT MODIFY FIELD NAMES — frontend and backend depend on these shapes", and the optimizer documents its output contract the same way. The twin can bind directly to agent output because that output is pinned.
The resilience layer is deterministic, not learned. Risk is a weighted sum of four normalised pressures:
risk = demand_pressure × 0.35 # community demand against available supply
+ battery_stress × 0.20 # how little headroom storage has left
+ ev_pressure × 0.15 # share of the fleet drawing charge right now
+ outage_factor × 0.30 # 1.0 when the grid is unavailable, else 0
# clamped to [0, 1]
risk ≥ 0.70 HIGH autonomous recovery planned
risk ≥ 0.40 MEDIUM preventive actions armed
otherwise LOW monitoring only
Peer-to-peer coordination. The agent pairs homes with surplus against homes in deficit and records simulated transfers — reported to the UI as kWh traded and a sellers/buyers count.
Autonomous recovery. At HIGH risk the agent emits an ordered plan drawn from what
is actually available this cycle:
- Execute the local P2P transfer it just matched
- Reserve the community battery for critical demand
- Defer flexible EV charging — naming the vehicles with a flex window over an hour
- Shift flexible household loads — naming the homes with flexibility ≥ 0.5
- Maintain island mode when grid import is unavailable
The plan is recommended, not executed against hardware. FlowState is a simulation and decision-support system; nothing in this repository actuates a physical switch.
Bit_N_Build/
├── backend/
│ ├── main.py FastAPI app, CORS, router mounting, /health
│ ├── requirements.txt
│ ├── agents/
│ │ ├── graph.py LangGraph topology (sequential, no branching)
│ │ ├── state.py CommunityState schema — a pinned contract
│ │ ├── solar_agent.py generation, 24 h forecast, surplus, low-gen windows
│ │ ├── battery_agent.py SoC, health, available discharge, charge/discharge
│ │ ├── house_agent.py all 50 households in one vectorised pass
│ │ ├── ev_agent.py schedules, flex windows, V2G eligibility, priority
│ │ ├── grid_agent.py pricing, availability, carbon intensity, peak hours
│ │ ├── resilience_agent.py risk score, P2P coordination, recovery plan
│ │ ├── optimizer.py dispatch, EV pause, load deferral, savings, carbon
│ │ ├── scenarios.py the five state mutations behind the simulator
│ │ ├── run.py entrypoint + mock community generators
│ │ ├── logger.py agent negotiation log helper
│ │ └── test_*.py 37 unit tests across agents and the optimizer
│ ├── routers/
│ │ ├── simulate.py POST /simulate and WS /ws/negotiate
│ │ ├── chat.py POST /chat — ARIA's grounded system prompt
│ │ └── voice.py POST /voice/stt and /voice/tts
│ └── llms/
│ ├── groq_client.py reasoning brain (llama-3.1-8b-instant)
│ └── sarwam_client.py STT / TTS / translation
│
├── frontend/
│ └── src/
│ ├── app/page.tsx the console shell and every dashboard panel
│ ├── store/useEnergyStore.ts zustand state, polling, WS negotiation
│ ├── data/mockData.ts offline community + per-scenario agent scripts
│ ├── hooks/ useBackendHealth · useAgentWebSocket · useVoiceRecorder
│ ├── components/digitalTwin/ CommunityScene · CommunityGrid · House · SolarFarm ·
│ │ Battery · EVZone · EnergyFlow · ScenarioEffects ·
│ │ AgentOverlay
│ └── components/dashboard/ EnergySliders · ScenarioControls · ResiliencePanel
│
└── docs/screenshots/ the images in this README
| Component | Requirement |
|---|---|
| Digital twin | Node 20 or newer (built and tested on Node 24) |
| Agent backend | Python 3.11+ and backend/requirements.txt |
| LLM features | Optional Groq and Sarvam API keys — everything degrades to mock mode without them |
cd backend
python -m venv venv
venv\Scripts\activate # Windows; source venv/bin/activate on macOS/Linux
pip install -r requirements.txt
# Optional — create backend/.env and keep it out of git:
# GROQ_API_KEY=your_key
# SARWAM_API_KEY=your_key
uvicorn main:app --reloadThe API comes up on http://localhost:8000, with interactive docs at /docs. Without
keys the log will say SarvamClient running in MOCK mode — that is expected, and the
agent pipeline itself needs no keys at all.
cd frontend
npm install
npm run devOpen http://localhost:3000. Use localhost, not 127.0.0.1 — the store derives
the backend origin from window.location.hostname (see Known issues).
Drag to rotate the community, scroll to zoom, click any house to open its telemetry, then use the scenario simulator to put the agents under load.
A frontend build is live at bit-n-build-wine.vercel.app.
It runs in mock mode against data/mockData.ts, so the steps above are only needed to
exercise the real Python agents.
Base URL http://localhost:8000. Interactive docs at /docs.
| Method | Path | Purpose |
|---|---|---|
GET |
/health |
liveness — the twin polls this every 15 s to set BACKEND LIVE / MOCK MODE |
POST |
/simulate |
run the full pipeline once; returns decisions, resilience, community_metrics, logs |
WS |
/ws/negotiate |
stream each agent's log line as its LangGraph node executes |
POST |
/chat |
ARIA — grounded in the selected house and community context |
POST |
/voice/stt |
speech to text (Sarvam) |
POST |
/voice/tts |
text to speech (Sarvam) |
/simulate and /ws/negotiate both accept scenario, solar_pct, battery_pct and
grid_pct, so the Supply Controls sliders and the scenario simulator are genuine inputs
to the agent run.
cd backend
pip install pytest
python -m pytest agents -qVerified on this checkout (Windows 11, Python 3.14):
| Suite | Tests | Result |
|---|---|---|
test_solar.py, test_battery.py, test_house.py, test_ev.py |
— | ✅ pass |
test_scenarios.py |
— | ✅ pass |
test_optimizer.py |
— | |
| Total | 37 | 36 passed, 1 failed |
The one failure is a stale assertion rather than a broken agent.
test_run_cycle_returns_decisions_only pins the optimizer's output keys to an exact set:
AssertionError: Extra items in the left set: 'resilience'
The resilience layer added a resilience key to the decisions payload and
EXPECTED_KEYS in agents/test_optimizer.py was never updated. The fix is to add
"resilience" to that set.
The suite covers agent outputs, scenario mutations, and the optimizer's dispatch contract. There are no frontend tests.
Recorded honestly, because both are easy to trip over when running this locally.
1 · peakDemand is missing from the frontend's offline scenario data.
ScenarioControls offers six scenarios, but scenarioData in data/mockData.ts
defines only five — normal, cloudCover, heatwave, gridFailure, evSurge. When
the WebSocket negotiation falls back to the scripted replay while PEAK DEMAND is
active, _runMockNegotiation() reads scenarioData['peakDemand'].agentDecisions and
throws:
TypeError: Cannot read properties of undefined (reading 'agentDecisions')
The Agent Network overlay is then left empty on AWAITING NETWORK NEGOTIATION…. The fix
is a peakDemand entry in scenarioData; a guard in _runMockNegotiation() would make
it safe regardless.
2 · The backend origin is hardcoded to port 8000 in the browser.
getHttpUrl() and getWsUrl() in store/useEnergyStore.ts return
`http://${window.location.hostname}:8000` whenever window exists, so
NEXT_PUBLIC_BACKEND_URL and NEXT_PUBLIC_BACKEND_WS_URL are ignored client-side —
they only apply during SSR. Two consequences: the backend must be on port 8000, and the
frontend must be opened on the same hostname the backend is reachable at (use
localhost:3000, not 127.0.0.1:3000). useBackendHealth does read the env var, so
a mismatch shows the confusing combination of a green BACKEND LIVE badge next to a
negotiation that silently falls back to mock data.
3 · Every resilience log line is attributed to the Optimizer.
AGENT_DISPLAY_NAMES in routers/simulate.py maps six graph nodes but not
resilience_agent, so its lines go out labelled "System". The frontend's agentMap
has no System either, and its ?? 'Optimizer' default catches them. The result is
visible in the negotiation screenshot: the risk assessment, the P2P match and the
recovery-coordinator line all read OPTIMIZER AGENT. The messages are correct — only
the attribution is wrong. Fixing it takes one entry in each map.
- Add the missing
peakDemandscenario data and guard the mock fallback - Honour
NEXT_PUBLIC_BACKEND_URL/NEXT_PUBLIC_BACKEND_WS_URLin the browser - Update
EXPECTED_KEYSso the optimizer contract test passes again - Map
resilience_agentin both display-name tables so its log lines are attributed correctly - Persist negotiation history so a cycle can be replayed and audited after the fact
- Real forecast data in place of the synthetic 24 h curve
- Settlement and pricing for P2P transfers, not just matched volume
- Frontend test coverage — there is currently none
- Hardware-in-the-loop against a real inverter or battery management system
FlowState was designed and built entirely by the Bit-N-Build team for the Bit-N-Build hackathon: the multi-agent energy architecture, the simulation environment, the digital-twin interface, the energy optimisation components, the resilience layer, P2P coordination, autonomous recovery planning and the FlowState visualisation.
Prithvi S P · Darshan Pawar · Chandan Kumar K
Track: Decentralised Energy Systems & Micro-Grids













