Live demo: https://maestro-gemini-multi-agent-angular.vercel.app/
Five Gemini agents. One natural-language brief. A live, generative Angular dashboard, entirely in your browser.
Maestro turns a single sentence into a structured event plan rendered as live Angular widgets. It's a working reference for multi-agent orchestration, generative UI, and bring-your-own-key privacy on a static-SPA budget — no backend, no telemetry. Just Angular (zoneless, signals), the @google/genai SDK, and a five-agent pipeline.
"Plan a 3-day, 800-attendee Agentic AI conference in Bengaluru this coming October, INR ₹2.5 crore budget, with hands-on workshops on multi-agent orchestration and a closing fireside."
↓ ~12 seconds later
A live Budget widget · a multi-day Schedule with grounded speaker suggestions · a Venue card with real Google-Search citations · an Auditor ribbon that catches cross-widget inconsistencies and fixes them with one click.
You type a brief, hit Architect Dashboard, and five agents take over. Each has a single, narrow job and a typed JSON schema:
| Agent | Role | Returns | Tools |
|---|---|---|---|
| Planner | Reads your brief, decides which specialists to run, writes a tailored sub-brief for each. | Routing + per-specialist briefs | – |
| Budget | Builds a categorized budget with line items and a totals row. | BudgetWidgetConfig |
– |
| Schedule | Builds a multi-day, multi-track schedule with sessions, speakers, and rooms. | ScheduleWidgetConfig |
Google Search |
| Venue | Picks a venue with capacity, AV, catering, and accessibility notes plus citations. | VenueWidgetConfig |
Google Search |
| Auditor | Cross-checks the widgets for overruns, gaps, and capacity mismatches, and emits one-tap fix-its. | AuditIssue[] |
– |
The three specialists run in parallel under Promise.allSettled, so one failure renders an error shell for that widget instead of taking down the dashboard. After settlement, the Auditor runs. Every step streams its usageMetadata into the Control Tower for live status, duration, token, and USD-cost readouts.
- Multi-agent orchestration — Planner + three parallel specialists + Auditor, coordinated by
AgentOrchestrator, each with its own schema, system prompt, and per-agent retry. - Generative UI —
WIDGET_REGISTRYmaps each specialist to a lazyimport();WidgetSlotinstantiates the component throughViewContainerRefand updates it in place viasetInput(). Outputs are real, editable Angular components, not a transcript. - Cross-widget ripple — changing Schedule or Venue marks Budget stale with a one-click Update; Auditor fix-its cascade through dependent agents, then re-audit.
- Per-widget refine bars — ask for surgical edits ("cut A/V cost by 25%", "swap to an outdoor venue") and only the owning specialist re-runs.
- Confidence scoring & opt-out auto-repair — the Auditor scores each widget's quality; widgets below the threshold can be auto-repaired after a run (one extra generation + re-audit per widget, on your own key). It's on by default, toggleable from the Control Tower (persisted), and every repair announces its before → after confidence so the extra spend is visible.
- Multimodal brief intake — draft a brief by dictating (Web Speech API) or attaching an image/PDF that Gemini reads into an editable prompt before you run.
- Keyless guided demo — no key? Open
/architect?demo=1(or Watch a sample run from the home page) to replay a complete run — planner → three specialists → an Auditor that flags the budget and then self-heals it — driven entirely by canned data. It scripts the real signal store on a cancelable timeline that respectsprefers-reduced-motionand pauses on tab-hide, so there's no network call and no cost; every run-mutating control is disabled so a demo can never touch a real key. - Google-Search grounding — Schedule and Venue surface real
groundingMetadatacitations as source chips withrel="noopener noreferrer". - Friendly, centralized errors — failures map to a sanitized
AppError(auth, quota, network, invalid-model, …) shown as a toast or an inline retry shell; raw API text never reaches the UI. - BYOK by design — your Gemini key is validated against
models.list, stored only inlocalStorage, masked in the UI (••••abcd), and never sent to any server we operate.
Prerequisites: Node (see .nvmrc / engines) · npm · a Gemini API key from Google AI Studio (free tier works). Run nvm use to match the pinned Node version.
npm install
npm start # http://localhost:4200No .env needed — the app prompts for your Gemini key on first run and keeps it in localStorage only.
| Path | Page | Purpose |
|---|---|---|
/ |
Home | Feature tour with one-click Try this brief demos |
/architect |
Workspace | Prompt, Control Tower, audit ribbon, and live widgets |
/architect?try=… |
Workspace (pre-filled) | Deep-link a prompt into the textarea |
/architect?demo=1 |
Workspace (demo) | Keyless, scripted sample run — no API key, no network |
npm start # dev server
npm run build # production build → dist/maestro/browser/
npm test -- --watch=false # one-shot unit run
npm run lint # ESLint (angular-eslint) over TS + templates
npm run lint:styles # Stylelint: design-token guardrail over SCSS
npm run watch # dev-config build with watch (no serve)Pure SPA: drop dist/maestro/browser/ on any static host (Vercel, Netlify, Cloudflare Pages, S3+CloudFront, GitHub Pages…). Configure two things:
- SPA fallback — serve
index.htmlfor unknown paths so/architect?try=…deep-links don't 404. - CSP — the strict policy in
index.htmlusesscript-src 'self'(no'unsafe-inline'; the anti-FOUC theme script is self-hosted inpublic/theme-init.js) and whitelistshttps://generativelanguage.googleapis.comforconnect-srcplus the Google Fonts origins for fonts/styles.style-srckeeps'unsafe-inline'for Angular Material. Extend the relevant directive if your host adds origins.
┌──────────────────── User brief ─────────────────────┐
│ "Plan a 3-day Agentic AI conference in Bengaluru…" │
└──────────────────────────┬──────────────────────────┘
▼
┌────────────────────┐
│ PlannerAgent │ responseSchema → routing JSON
│ (decomposition) │ + per-specialist briefs
└───┬─────┬─────┬────┘
▼ ▼ ▼ Promise.allSettled
┌─────────────┐ ┌──────────────┐ ┌──────────────┐
│ BudgetAgent │ │ScheduleAgent │ │ VenueAgent │
│ (JSON) │ │ + Search🔍 │ │ + Search🔍 │
└──────┬──────┘ └──────┬───────┘ └──────┬───────┘
│ │ │
└───────┬───────┴────────────────┘
▼
┌──────────────┐
│ AuditorAgent │ cross-widget consistency JSON
└──────┬───────┘
▼
╔════════════════════════════╗
║ AgentStore (signals) ║ id-keyed widgets,
║ ║ per-agent state,
║ ║ global status, telemetry
╚══════════════╤═════════════╝
│
┌───────────────────┼──────────────────────────┐
▼ ▼ ▼
AuditRibbon ControlTower GenerativeRenderer
(fix-it chips) (timeline, telemetry, (ghost → real → error
│ per-agent retry) shells via ViewContainerRef)
│ apply fix │
└──────────────────► refine specialist ───────┤
▼
Budget / Schedule / Venue widgets
(+ Refine bars, Citation chips,
Stale-state banners)
CommandCenter.submit()callsAgentOrchestrator.run(), which spawns a freshAbortControllerfor the whole run.- Planner produces typed JSON: a routing list plus a tailored sub-brief per specialist.
- Specialists dispatch in parallel under
Promise.allSettled, streamingusageMetadataand final payloads intoAgentStore. AgentStoreis signal-based; updates fan out toGenerativeRenderer, which usesViewContainerRef.createComponentagainst the dynamic import inWIDGET_REGISTRYand feeds each widget viasetInput().- Auditor runs after settlement, reviews all widgets together, and emits
auditIssues; each can be applied viaapplyFixIt(id), which refines the owning specialist and re-audits.
- Every specialist runs under
Promise.allSettled, so one failure renders an error-mode widget shell instead of collapsing the dashboard. - If the Planner itself errors, the orchestrator falls back to running all three specialists on the raw brief.
- Grounded outputs that wrap JSON in prose are recovered by a tolerant JSON parser.
- Thrown values are converted to a sanitized
AppErrorbycore/errorsand surfaced through a globalErrorHandler+ snackbar, with inline retry on agent shells. - Every orchestrator entry point gets a fresh
AbortController, so switching keys, starting a new run, or unmounting cancels in-flight Gemini streams synchronously.
?demo=1 boots a DemoModeService that replays a canned run without touching the orchestrator or the network. It lazy-loads a hand-authored dataset + choreography and drives AgentStore mutations on a DemoTimeline: the timeline scales down under prefers-reduced-motion, pauses while the tab is hidden (so a backgrounded demo doesn't fire a burst of beats on return), and shares one AbortController so Replay / Exit / navigation cancel cleanly. Because it feeds the real widgets and telemetry, the demo is indistinguishable from a live run — including a scripted Auditor flag and one-tap self-heal — while the production Gemini path stays completely untouched.
src/
├── _mixins.scss Shared SCSS design-system mixins
├── styles.scss Global design tokens, theme blocks, Material overrides
├── index.html CSP, OG tags, subsetted Material Symbols font
└── app/
├── app.* Shell (topbar + footer + <router-outlet>), routes, zoneless config
│
├── core/ Pure logic, no DOM
│ ├── ai/ Agents, orchestrator, schemas, prompts, pricing, grounding, intake
│ ├── auth/ BYOK: validate() against models.list + connect-key dialog
│ ├── errors/ toAppError() + global ErrorHandler + snackbar service
│ ├── demo/ Keyless sample run: mode service · script · canned data · timeline
│ ├── state/ Signal store + query-param → textarea hand-off
│ ├── theme/ Light/dark, persisted, prefers-color-scheme aware
│ └── … format · settings · types
│
├── features/ UI building blocks
│ ├── audit-ribbon/ Critic banner + fix-it chips + Re-audit
│ ├── command-center/ Prompt card + sample chips + no-key empty state
│ ├── control-tower/ Live agent timeline + per-agent Retry + telemetry
│ ├── demo/ Demo banner: phase status + Replay / Exit / Connect-key
│ ├── renderer/ WIDGET_REGISTRY (lazy) + WidgetSlot + GenerativeRenderer
│ └── widgets/ widget-shell · refine-bar · citation-chips + the three widgets
│
└── pages/ Lazy route components: home (`/`) and workspace (`/architect`)
| Layer | Choice | Why |
|---|---|---|
| Framework | Angular zoneless | Smallest reactive surface; signals everywhere; OnPush default. |
| State | Signals + per-feature stores | Computed views; native cancellation via AbortController. |
| LLM SDK | @google/genai (lazy) |
Structured outputs + streaming + Google Search grounding; lazy-loaded off the initial bundle. |
| UI kit | Angular Material | Form fields, button toggles, progress bars, snackbars, violet theme. |
| Styles | SCSS + design tokens + mixins | Tokens are the single source of truth; glass-surface/pill mixins keep it DRY. |
| Routing | Standalone routes + lazy loading | Pages, the API-key dialog, the SDK, the demo, and every widget are lazy chunks. |
| Tests | Vitest (jsdom) |
Unit specs for schemas, agents, store, errors, pricing, telemetry, settings, and the demo. |
| Lint | angular-eslint + Stylelint | TS + template a11y rules (npm run lint); design-token guardrail (npm run lint:styles). |
| Build | Angular esbuild (@angular/build) |
Fast production builds; enforced initial-bundle + per-component-style budgets. |
- Lean initial transfer — the initial bundle stays small because everything non-critical is a lazy chunk:
@google/genai(on the first API call), the workspace page, the API-key dialog, each widget, and the guided-demo script + canned data (only on?demo=1). A production build budget guards against regressions. - Performance — lazy SDK/dialog/widgets/pages keep first paint lean; the Material Symbols font is subsetted to the glyphs actually used; the Control Tower ticker runs at 500 ms and pauses when the tab is hidden; every Gemini stream is tied to an
AbortController. - Security — strict CSP in
index.html(script-src 'self'with no'unsafe-inline';connect-srclimited togenerativelanguage.googleapis.com, plusobject-src 'none',frame-ancestors 'none',base-uri 'self');rel="noopener noreferrer"on external links; no analytics or third-party scripts; the API key is never logged, is masked in the UI, and is wiped on clear. (The key lives inlocalStorageas a deliberate BYOK/no-backend trade-off.) - Multimodal intake privacy — attached images/PDFs are read in-browser and sent (base64-inline) only to the Gemini API on your own key, exactly like a typed brief. Voice input uses the browser's built-in Web Speech API; note that in Chromium browsers this streams audio to the browser vendor's speech service (a browser-level behavior outside the app's CSP), so it is opt-in.
npm test -- --watch=falseSpecs cover the structured-output schemas, agent streaming + tolerant JSON parsing, store mutations and audit lifecycle (incl. issue-id de-dup and confidence clearing), error mapping (toAppError) and classification, BYOK validation, ripple builders, pricing math, telemetry formatting, settings persistence, currency normalization, the self-heal/intake flows, and the guided demo (timeline cancellation + tab-hide pausing, scripted run, canned-data integrity, and lifecycle guards).
MIT © 2026 Ankit.