One-liner: "CloseLoop AI is a multi-agent chip-design copilot that automatically finds timing violations, recommends engineering fixes, simulates timing/power tradeoffs, and (aspirationally) learns from previous successful fixes using Redis."
Tagline: Cursor for chip timing closure.
Built for WeaveHacks 4 (June 2026). Stack: Next.js 14 (App Router) + TypeScript + React Flow + Zustand + Tailwind. Runs 100% offline in mock mode with zero API keys.
This document is reverse-engineered from the actual source tree, file by file and function by function, so the team has a precise, shared mental model of what the system does today, what the agents are, and where the pitch is aspirational vs. implemented.
Chip physical-design timing closure is one of the slowest steps in semiconductor development — traditionally weeks to months. Today the loop is fully manual:
Engineer → reads huge STA report → finds violation → guesses a fix →
re-runs the tool → checks if it worked → repeats for days
Replace that manual loop with an autonomous, self-correcting team of agents:
STA Agent (finds violation)
→ Fix Agent (suggests ECO fix)
→ PPA Agent (checks power/area impact)
→ Orchestrator (applies fix, recomputes timing, decides stop/continue)
→ Redis (stores the learning for reuse)
The differentiators claimed in the pitch:
- Open-source
- ASIC-targeted (vs. Nexus which is FPGA-only)
- Self-correcting (orchestrator detects diminishing returns and hard-stops)
- Live visualization (the only system with a visual sandbox)
- Engineering memory — accumulate problem→fix→result experience in Redis and reuse it (described as the strongest sponsor story).
⚠️ Reality check (see §11): items 1–4 are implemented. Item 5 (engineering memory / learning / ranking / reuse) is scaffolded but not yet wired — the Redis keys exist but no read/rank/retrieve logic uses them. This is the single most important gap to close.
┌───────────────────────────────────────────────────────────┐
│ Next.js 14 App Router (TypeScript) │
│ ┌──────────────┐ ┌──────────────────┐ │
│ │ Dashboard UI │ ◄────► │ Zustand store │ │
│ │ (React Flow) │ │ (useDesignStore) │ │
│ └──────┬───────┘ └──────────────────┘ │
│ │ fetch (lib/clientApi.ts) │
│ ┌──────▼─────────────────────────────────────┐ │
│ │ app/api/* route handlers (9 endpoints) │ │
│ └──────┬─────────────────────────────────────┘ │
│ │ │
│ ┌──────▼─────────────────────────────────────┐ │
│ │ lib/apiRouter.ts — switches mock ↔ real │ │
│ └──┬──────────────┬─────────────────┬─────────┘ │
│ ▼ ▼ ▼ │
│ Agents Redis Weave │
│ (5 mock or (in-mem Map or (no-op logger or │
│ GPT-4o) real ioredis) real W&B) │
│ │ │
│ ┌──▼──────────────────────────────┐ │
│ │ lib/timing/* — deterministic │ ← the "physics" │
│ │ timing engine (formulas) │ ground truth │
│ └─────────────────────────────────┘ │
└───────────────────────────────────────────────────────────┘
Key design principle: All agent outputs are grounded by a deterministic timing engine
(lib/timing/timingEngine.ts). Agents narrate and recommend; the engine computes the actual
slack from physical formulas every time a fix is applied. This is why it is "not a fake
hardcoded demo" — applying a fix re-runs the formulas.
lib/apiRouter.ts is the central switchboard:
isMockMode()→ mock unlessNEXT_PUBLIC_MOCK_MODE=falseandOPENAI_API_KEYis set.getAgentClient()→ mock agents or (placeholder) real OpenAI agents. Today both branches return the mock agent functions; the real branch only flips theisMockflag. Real GPT-4o wiring is a TODO with a clean upgrade path.getRedisClient()→ realioredisifREDIS_URLset, else in-memoryMockRedis.getWeaveClient()→ currently always returnsMockWeaveClient(real W&B is a stub).
So today the app is effectively mock-only for agents & Weave, with optional real Redis.
There are 5 agent types (AgentType in lib/types.ts): STA, FIX, PPA,
ORCHESTRATOR, EXPLAINABILITY. Each has:
- A wrapper in
lib/agents/<name>Agent.tsthat adds Weave tracing + audit logging and shapes the result into a uniformAgentOutput<TInput, TOutput>. - A mock implementation in
lib/mock/mockAgents.tscontaining the actual logic. - A system prompt in
lib/agents/prompts.ts(used when/if real LLM mode is wired).
Every agent call produces an AgentOutput with: agentType, timestamp, input, output,
reasoningSteps[], latencyMs, isMock. Mock agents simulate realistic latency
(simulateLatency, 400–900ms) so the timeline feels alive.
- Wrapper:
lib/agents/staAgent.ts→runSTAAgent(design). - Logic:
mockSTAAgent(design)inlib/mock/mockAgents.ts. - Input: full
DesignState. - Output schema:
STAAgentOutput:worst_path: string[],slack,wns,tns,required_time,arrival_timecell_delays: {cellId: ns},wire_delays: {netId: ns}root_causes: RootCause[],root_cause_analysis: {cause: explanation}plain_english,severity(critical|warning|marginal),recommended_priority.
- What it actually does:
- Calls
findWorstPath(design)(timing engine) to get the worst path + per-cell/per-net delays. - Builds
cell_delays/wire_delaysmaps. - Generates human-readable
root_cause_analysisstrings for each detected cause (e.g. "NET_LONG_1 is 210um — contributes 0.42ns, 42% of the clock period"). - Classifies severity by slack (
< -0.05= critical,< 0= warning). - Writes a plain-English diagnosis.
- Calls
- Root causes detected (
RootCause):long_net_delay,weak_buffer,high_fanout,logic_depth,clock_skew,setup_violation(last two are typed but not actively detected by the mock).
- Wrapper:
lib/agents/fixAgent.ts→runFixAgent(staOutput, design). - Logic:
mockFixAgent(staOutput, design). - Output schema:
FixAgentOutput={ fixes: Fix[], recommendedOrder: string[], stopCondition: string }. - Fix types (
FixType):upsize_cell,insert_buffer,move_closer,restructure_logic,add_repeater. - What it actually does (rule-based heuristics today):
- Weak buffer → upsize: if a
BUFwithdriveStrength ≤ 1is on the worst path, emitFIX_UPSIZE_1(e.g.BUF_X1 → BUF_X4), est. +0.09ns, +3.2% power, low risk. - Long net → buffer insertion: the longest critical net with
length > 150umgetsFIX_BUF_1(insertBUF_X2at midpoint, splits the wire), est. +0.17ns, +1.8% power. - Fallback: if neither,
move_closerto tighten margin (+0.02ns). - Each fix's
ecoBudgetis recomputed viacheckECOLegality(routability scoring). - Orders fixes by
estimatedSlackImprovementdesc; writes astopConditionsummary.
- Weak buffer → upsize: if a
- Each
Fixcarries:type,target,from/to,targetNet,bufferType,reason,estimatedSlackImprovement,estimatedPowerDelta,estimatedAreaDelta,riskLevel,ecoBudget,plainEnglish.
- Wrapper:
lib/agents/ppaAgent.ts→runPPAAgent(fixes, design). - Logic:
mockPPAAgent(fixes, design). - Output schema:
PPAAgentOutput={ perFixImpact[], combinedImpact, overallRecommendation, plainEnglish }. - What it actually does:
- For each fix, decides a verdict (
accept | modify | reject): good tradeoff ifslackImprovement > 0.03andpowerDelta < slackImprovement × 50; flagsmodifyifpowerDelta > 10%. - Sums per-fix deltas into
combinedImpact(timing/power/area/congestion). - Projects
expectedAfterSlack = design.wns + combinedTimingDeltaand writes an overall recommendation ("Accept all fixes. Timing improves from X to +Y (closure achieved)…").
- For each fix, decides a verdict (
- Wrapper:
lib/agents/orchestratorAgent.ts→runOrchestrator(design, iterations). - Logic:
mockOrchestratorAgent(design, iterations). - Output schema:
OrchestratorOutput={ nextAction, reason, closureAchieved, iterationSummary, stopReason };nextAction ∈ {run_sta, run_fix, apply_fix, stop}. - Stop rules (hard constraints):
- Closure achieved when
WNS ≥ +0.02ns→ stop. - Max 5 iterations → stop (prevents infinite loops).
- Diminishing returns: if the last 2 iterations each improved
< 0.01ns→ stop.
- Closure achieved when
- Full loop:
mockRunClosureLoop(initialDesign, onIteration?)ties everything together: recompute → orchestrator → STA → Fix →applyFixesToDesign→ recordIteration→ repeat (≤5). This is what the "Run Full Closure" button triggers.
- Wrapper:
lib/agents/explainabilityAgent.ts→runExplainabilityAgent(before, after). - Logic:
mockExplainabilityAgent(before, after). - Output schema:
ExplainabilityOutput={ beforeStory, afterStory, analogy, businessImpact, oneLiner }. - What it does: turns before/after slack & power into relay-race analogies and a business impact statement (e.g. "runs at target 1GHz → faster inference, better battery life").
- Note: the wrapper/agent exists and is fully functional, but the dashboard does not currently invoke it via an API route — the Judge Mode overlay hardcodes equivalent narration. (Easy win: wire it through.)
[Run STA button] → POST /api/sta/analyze
→ runSTAAgent → mockSTAAgent → STAAgentOutput (timeline entry pushed)
→ (auto) POST /api/fix/recommend
→ runFixAgent (Fix[]) + runPPAAgent (verdicts) → fix cards + PPA panel
[Apply Fix button] → POST /api/fix/apply
→ applyFixesToDesign(design, fixes) [timing engine recomputes]
→ store new design + iteration in Redis; append audit events
→ red path turns green if WNS ≥ 0
[Run Full Closure] → POST /api/closure/run
→ getAgentClient().closureLoop(design) = mockRunClosureLoop
→ loops Orchestrator → STA → Fix → apply (≤5 iterations)
→ persists finalDesign + iteration history to Redis
→ UI synthesizes one timeline entry per iteration
app/page.tsx (the Dashboard) is the front-end conductor. It uses lib/clientApi.ts
(thin fetch wrappers) to hit the API routes and writes results into the Zustand store
(lib/store/useDesignStore.ts). On mount it auto-loads the demo design and benchmarks.
lib/timing/timingEngine.ts is the ground truth that prevents the demo from being fake.
- Cell delay:
cellDelay(cell, fanout) = baseDelay / sqrt(driveStrength) + max(0, fanout-2)*0.03. → Upsizing a cell (higher drive strength) reduces its delay. - Wire delay (cubic RC model):
wireDelay(net) = (length/100)³ × 0.05. → Long wires hurt disproportionately; tuned so 210um ≈ 0.46ns and 105um ≈ 0.06ns. This makes buffer insertion physically meaningful — splitting a 210um wire into two 105um halves + a ~0.09ns buffer is a net win of ~0.25ns. - Path arrival / slack:
pathArrival = ΣcellDelay + ΣwireDelay + setupTime;slack = requiredTime − arrivalTime(negative = violation). - Fix application functions:
applyUpsize(delay↓, power/area↑ log-scaled),applyBufferInsertion(splits a net + inserts buffer cell at midpoint),applyMoveCloser(reduces net length %). findWorstPath: orders cells along the critical path via net topology (orderCellsAlongPath/orderNetsAlongPath) and detects root causes (detectRootCauses: long net > 150um, weak buffer, fanout ≥ 4, logic depth > 6 cells).recomputeDesign: the master recompute — recalculates worst path, WNS, TNS (tns ≈ wns × 2.2when negative), PPA, per-cell timing status, MCMM corners. Called after every fix.applyFixesToDesign: applies a list ofFixobjects to the design, then recomputes.- MCMM:
computeMCMMapplies PVT derating per corner (process ss=×1.15 / ff=×0.9, voltage 0.9/V, temp 1+(T−25)×0.001) → identifies the worst-casess_0p9v_125ccorner. - Signoff:
applySignoffMargin(slack, 5%)— tighter signoff requirement; a path can pass STA but fail signoff margin (flagged in the MCMM panel). - PPA:
computePPAsums cell power/area and derives congestion from area + total wire length.
lib/timing/ecoBudget.ts → buildECOReport: per-fix routability scoring; flags warnings if
routabilityScore < 50 and illegal if < 30.
lib/timing/reportParser.ts → parseOpenSTAReport + reportToTimingPath: lets a user paste
a real OpenSTA timing report (regex-based parser for Startpoint/Endpoint/arrival/required/
slack + per-cell/per-net lines) and convert it into the internal TimingPath model. Exposed
via POST /api/report/parse.
Core entities:
Cell— id, type (FF|BUF|NAND|INV|AND|OR|MUX), driveStrength, baseDelay, power, area, x/y (layout), label,isOnCriticalPath,timingStatus,block(hierarchy: ALU/Control).Net— id, source, target, length(um), fanout, baseWireDelay,isOnCriticalPath.TimingPath— startpoint/endpoint, required/arrival/slack, ordered cells+delays, ordered nets+delays, setupTime, rootCauses.DesignState— name, technology, cells[], nets[], worstPath, wns, tns, power, area, congestion, corners[], iteration, setupTime, requiredTime, clockPeriod.Fix,ECOBudget,Iteration,Corner,BenchmarkResult,AuditEvent, plus the agent I/O schemas (STAAgentOutput,FixAgentOutput,PPAAgentOutput,OrchestratorOutput,ExplainabilityOutput).
An 8-cell ALU datapath in nangate45 ASIC tech, 1.0ns clock period:
- Critical path:
FF1 → NAND_X1 → BUF_X1 (weak) → INV_X1 → FF2. - The smoking gun:
NET_LONG_1= 210um wire fromBUF_X1 → INV_X1(cubic delay penalty), driven by a weakBUF_X1(driveStrength 1). - Side branch (Control block):
NAND_X1 → AND_X2 → BUF_X2_side → OR_X1 → FF2. - 3 MCMM corners; worst-case =
ss_0p9v_125c. - Initial WNS ≈ −0.21 to −0.23ns (violation); after fixes ≈ +0.05 to +0.15ns (closed).
| Route | Method | Purpose |
|---|---|---|
/api/design/demo |
GET | Load demo design, persist to Redis, reset iterations |
/api/design/current |
GET/POST | Read/write current design state |
/api/sta/analyze |
POST | Run STA agent on a design |
/api/fix/recommend |
POST | Run Fix agent + PPA agent together |
/api/fix/apply |
POST | Apply selected fixes, recompute, record an iteration |
/api/closure/run |
POST | Run the full autonomous closure loop |
/api/iterations |
GET | Fetch iteration history from Redis |
/api/benchmark |
GET | Benchmark comparison (updates "ours" row live) |
/api/mcmm |
GET | MCMM corner slacks with PVT derating |
/api/report/parse |
POST | Parse a pasted OpenSTA report → TimingPath |
All routes return a uniform envelope: { ok, isMock, data?, error?, latencyMs }.
app/page.tsx lays out the dashboard. Top bar actions: Judge Mode, Export,
Reset Demo, Run STA, Run Full Closure.
| Component | Role |
|---|---|
ChipGraph/* |
React Flow graph of the chip. CellNode, NetEdge, PathAnimator/ConfettiBurst. Critical edges colored red/amber/green by status; animated signal flow on the critical path. |
TimingPanel/* |
SlackGauge (−0.5↔+0.5ns gauge), PathTable (cell-by-cell delay breakdown), PPAChart (WNS/TNS/power trend over iterations), plus the STA agent's plain-English explanation + root causes. |
MCMMPanel |
Corner selector with PVT derating, signoff-margin correlation, "passes STA but fails signoff" warning. |
FixCards/* |
Selectable ECO fix cards from the Fix Agent + combined-impact summary + PPA verdict. Apply button. |
AgentTimeline/* |
Live feed of agent steps (AgentBadge, AgentStep) with latency + mock badges; shows the running stage. |
BenchmarkPanel/* |
ComparisonTable: CloseLoop vs Nexus vs ChatEDA vs Manual. |
JudgeMode/* |
5-step auto-advancing non-technical walkthrough (StoryStep). The judge-facing narrative. |
CopilotChat |
Chat sidebar ("CopilotKit") with canned NL responses + action buttons (run STA, apply fixes, judge mode, run closure). |
ui/* |
Badge, Button, Card, StatusBar primitives. |
- The Problem — red critical path, WNS ≈ −0.21ns, signal arrives late.
- The AI Agent Found It — STA agent diagnosis (~750ms), root causes.
- It Recommended Two Fixes — upsize BUF_X1 → BUF_X4, insert relay buffer mid-net.
- The Chip Is Now Timing-Clean — slack red→green, power penalty small.
- The Impact — speedup vs Manual / Nexus / ChatEDA; "only system with live sandbox + self-correction on ASIC."
lib/store/useDesignStore.ts (Zustand) holds: design, initialDesign, iterations,
timeline, staResult, fixResult, ppaResult, benchmarks, plus UI flags (loading,
runningStage, judgeMode, selectedCornerName, isMockMode, closureAchieved).
Helpers buildTimelineEntryFromAgent and buildApplyTimelineEntry convert agent outputs into
timeline entries.
| Sponsor | Status | Detail |
|---|---|---|
| OpenAI | Stubbed | getAgentClient() has a real-mode branch, but it currently returns the same mock agent functions (only flips isMock). Prompts in lib/agents/prompts.ts are ready for GPT-4o wiring. |
| Redis | Partially real | getRedisClient() uses real ioredis when REDIS_URL is set, else an in-memory MockRedis (full get/set/del/lpush/rpush/lrange/llen/hset/hget/hgetall/flushall). Actually used keys: design:current, design:iterations, audit:events. |
| W&B Weave | No-op | getWeaveClient() always returns MockWeaveClient; .trace() wraps every agent call and logs [MOCK WEAVE]. Real W&B is a TODO. |
| CopilotKit | Simulated | CopilotChat is a hand-rolled chat with canned responses + action buttons; not the real CopilotKit SDK. |
All mock subsystems log with clear prefixes ([MOCK REDIS], [MOCK WEAVE], [MOCK AGENT])
so judges can open the console and see exactly what would be sent to real systems.
| Pitch claim | Status | Notes |
|---|---|---|
| STA agent finds violation | ✅ Implemented | Rule + timing-engine based |
| Fix agent suggests ECO | ✅ Implemented | Heuristic (upsize / buffer insert / move) |
| PPA agent checks tradeoffs | ✅ Implemented | Accept/modify/reject verdicts |
| Orchestrator applies + recomputes + stops | ✅ Implemented | ≤5 iters, closure & diminishing-returns stops |
| Timing recomputed from real formulas | ✅ Implemented | timingEngine.ts cubic RC model |
| Red path → green animation, slack −0.21 → +0.05 | ✅ Implemented | ChipGraph + Judge Mode |
| MCMM / signoff / ECO legality (Cadence parity) | ✅ Implemented | MCMM panel + ecoBudget |
| Benchmark vs Nexus/ChatEDA/Manual | ✅ Implemented | nexusBenchmark.ts |
| OpenSTA report parsing | ✅ Implemented | reportParser.ts + endpoint |
| Redis "engineering memory" — store fix→result, rank, reuse later | ❌ Not implemented | Keys agent:sta:memory, agent:fix:memory, agent:ppa:memory, timing:reports, benchmark:results, timing:corners are defined but never read/written. No similarity search, ranking, or reuse logic exists. |
| Explainability agent surfaced in UI | Agent works; dashboard uses hardcoded narration instead of calling it | |
| Real OpenAI / Weave / CopilotKit | Clean upgrade paths exist |
The pitch's unique claim — engineering memory that accumulates experience — is the one thing not yet built. To deliver it:
- On each applied fix, write
{ rootCauseSignature, fix, slackBefore, slackAfter, design }toagent:fix:memory(Redis list or sorted set keyed by root-cause signature). - In the Fix Agent, before recommending, query Redis for similar past root-cause signatures
and rank candidate fixes by historical
slackImprovement. - Surface "Similar issue seen before — buffer insertion historically improved slack by +0.24ns" in the UI (a memory card / timeline note).
This converts the demo from "smart automation" into a genuinely agentic, learning system.
- ≤ 15 nodes in the demo graph (8 cells + buffers inserted by fixes).
- No auth / no login.
- ≤ 5 closure iterations (orchestrator hard-stops).
- No real EDA engine — synthetic but physics-consistent timing.
- 100% offline in mock mode for demo safety.
- Nexus (arXiv:2502.19091) — multi-agent timing closure on FPGA (VTR), 30% power saving.
- ChatEDA (IEEE TCAD 2024, arXiv:2308.10204) — LLM agent for RTL-to-GDSII flow.
- Manual baseline — Cadence Certus / TDF industry data (~180 min to close).
- CloseLoop positioning: open-source, ASIC-targeted, self-correcting, live-visualized; closes the demo design in ~0.24–0.48 min (≈14–29s) vs. Nexus 3.2 min and Manual 180 min.
- STA — Static Timing Analysis.
- ECO — Engineering Change Order (a targeted post-layout fix).
- WNS / TNS — Worst / Total Negative Slack.
- Slack —
requiredTime − arrivalTime; negative = signal too late = violation. - PPA — Power, Performance, Area.
- MCMM — Multi-Corner Multi-Mode analysis.
- PVT — Process / Voltage / Temperature derating corner.
- Drive strength — a cell's current-driving capability; higher = faster but more power/area.
- Buffer insertion — adding a repeater mid-wire to break a long net into shorter segments.
Generated by reading the repository file-by-file, function-by-function (June 2026).