A shared AI workspace designed to bring DataHub company memory into live meetings.
Company definitions · schemas · lineage · ownership · quality signals · past decisions
One changing shared context for every agent in the room
Most meeting agents work from the transcript alone. They do not know which metric definition is official, which table owns a field, what depends on a proposed change, or why the company made a previous decision. Handy is designed to use DataHub Agent Context as the connected company memory behind the room.
As the topic changes, Handy focuses that memory into a small shared meeting context. Every agent reads the same definitions, data relationships, rules, and history alongside the live conversation, accepted files, and screen context. The room can then produce grounded summaries, checked claims, runnable prototypes, and a durable recap without each agent inventing company meaning independently.
Status: hackathon MVP. The shared room, reviewed file context, routed agents, live prototypes, fact-checks, recaps, and export package run end to end. DataHub retrieval, lineage-aware impact analysis, and structured knowledge write-back are the planned hackathon integration; the repository does not connect to DataHub yet. That connection must ship before this project meets the hackathon's DataHub-use requirement.
The challenge asks agents to read DataHub, understand what is connected, take useful action, and write results back so the next person or agent inherits the knowledge. Handy applies that loop to live meetings:
- Read: use the DataHub Agent Context Kit to retrieve the schemas, lineage, ownership, governance signals, documents, and decisions relevant to the current topic.
- Act: give every Handy agent the same focused context so the room can check a claim, assess downstream impact, create a prototype, or record a decision without inventing company meaning.
- Write back: return structured decisions, changed definitions, findings, prototype context, and unresolved questions to DataHub—not the raw meeting transcript.
The secondary Open / Wildcard angle is live organizational knowledge capture: DataHub is not only queried by an agent; its context graph becomes the shared memory for a multiplayer agent workspace and improves with the useful outcomes of each meeting.
DataHub already connects business definitions, schemas, lineage, ownership, quality signals, documents, and systems. Handy turns the subset relevant to the current discussion into live working context, then returns useful decisions and findings to that memory after the meeting.
Dashed edges and dashed-outline nodes mark the planned DataHub-backed context and write-back path. The implemented meeting signals, router, agents, and live artifacts are shown as solid nodes.
- Build live dashboards: describe what you need and Handy creates an interactive dashboard using the right company data.
- Review data changes: see what a metric or schema means, who owns it, and what depends on it before making a change.
- Solve data issues: bring the issue, affected data, and responsible people into the same meeting.
- Remember decisions: save decisions, action items, and open questions so the team does not lose context after the meeting.
- Listen. Conversation, accepted files, screen context, and new decisions reveal what the room needs.
- Retrieve (planned). DataHub supplies the relevant company knowledge and relationships.
- Focus (planned). Handy keeps one changing DataHub-backed context containing only the definitions, sources, rules, owners, and history that matter now.
- Create. Every agent uses that same context to produce checks, summaries, prototypes, decisions, and the recap.
- Remember (planned). Structured outcomes flow back into DataHub instead of disappearing into a transcript archive.
If the room asks for a customer-retention dashboard, DataHub could provide the official active-customer definition, approved source tables, related dashboards and lineage, data owners, quality signals, earlier retention decisions, and sensitive-field constraints. Handy can use that context to build with company terminology, follow the correct data relationships, shape realistic sample values, and exclude private customer fields.
The planned Fact and Safety Checker uses the same relationship map before the room acts. For example, changing “Active Customer” from 90 days to 60 days could surface affected dashboards, reports, models, owners, and a conflicting prior decision while the proposal is still being discussed.
The intended judge demo uses DataHub's official
showcase-ecommerce datapack, whose cross-platform
graph spans datasets, dashboards, dbt, governance, glossary terms, and lineage. It gives
the meeting a realistic company context without requiring private data.
Handy is multiplayer. There is one in-memory room per server; every connection is a thin session that joins it.
- Host + guests. The host captures screen + speech and hosts the shared canvas; guests open an invite link and watch the same event stream. Live presence (cursors, pings, participant bar).
- Invite gating. Set
HOST_PASSCODEand the host authenticates with it; guests each get a unique, host-minted invite code carried as?key=on their link (revocable, kickable). Empty passcode = open (fine for local dev). - Bring-your-own transcription. Per participant, pick a backend:
ElevenLabs Scribe v2 Realtime (the browser streams mic audio straight to ElevenLabs
using a 15-min single-use token minted server-side — the key never reaches the client), or
fully on-device via WebGPU Whisper (
@huggingface/transformers, base/small/large-v3-turbo tiers) or Gemma 4 E4B on Ollama, or the free Web Speech API fallback. Per-participant language picker, noise-floor calibration, and a live VAD latency tuner. Push-to-talk or continuous capture. - Screen-aware (multimodal). The host browser samples screen frames, downscales them, and sends them only to the local server for screen-aware prototype prompts — raw frames are never rebroadcast to viewers. "Build it like this diagram" works because Gemma 4 sees it.
- File context. Drop files into the meeting; the host accepts a bundle into the workspace and its summary is fed to the router / summarizer / prototype agents.
- Meeting recap. When the host ends the meeting, a closing agent drafts a themed, self-contained HTML recap (executive summary, decisions, action items, open questions, prototypes built) and streams it to everyone on the same link. Every generated artifact is also saved under the host export folder and exposed as download links for participants on the final recap screen.
- Draggable, dockable, resizable panels over an infinite canvas, with a Paper / Ink (light / dark) editorial theme.
Bun-workspace monorepo, three packages. packages/shared is the contract the other two
compile against.
.
├─ packages/shared/ @handy/shared — WS event protocol (events.ts), Zod schemas +
│ inferred types (schemas.ts), agent prompts (prompts.ts), design
│ languages + mock builders (themes.ts) + DESIGN.md serializer (designmd.ts)
├─ apps/server/ Bun.serve WebSocket (/ws) + static host + a shared Room that owns
│ the Orchestrator and the four agents (router/summarizer/prototype/
│ factcheck) + the closing finaldoc agent
├─ apps/web/ Vite + React. One useReducer (ws.ts) holds all client state, driven
│ purely by the inbound WS stream; per-participant capture + ASR
├─ fixtures/ committed 16 kHz audio sets for the ASR benches (audio/, meetings/)
├─ scripts/ asr/meeting fixture generation + WER benches + live-sim
├─ test-transcripts.json stable meeting fixtures (gold-label `expect` blocks)
├─ handy-build-spec.md design source of truth (prompts + schemas + protocol)
├─ docs/index.html self-contained landing page — the GitHub Pages site (served from docs/)
└─ docs/positioning.md Handy vs the 2026 AI-meeting landscape + the honest A/B
Handy runs in Docker with a Tailscale sidecar, so the app comes up as its own
isolated node on your tailnet (e.g. https://handy.tail1234.ts.net) — no host-level
funnel, and the whole thing tears down with one command. The Bun server serves the web app,
/ws, and the API on a single port, so one tailscale serve carries everything.
cp .env.example .env
# In .env:
# - AGENTS=mock works with no keys; set AGENTS=live + CEREBRAS_API_KEY for real inference
# - add a Tailscale auth key for the sidecar (ephemeral + reusable):
# https://login.tailscale.com/admin/settings/keys
# TS_AUTHKEY=tskey-auth-...
# TS_HOSTNAME=handy # -> https://handy.<your-tailnet>.ts.net (tailnet only)
docker compose up # build + run both containers (logs in foreground)Open https://<TS_HOSTNAME>.<your-tailnet>.ts.net from any device on your tailnet (host
login uses HOST_PASSCODE / MEETING_PASSWORD).
| Command | Does |
|---|---|
docker compose up / up -d |
build + run, foreground / detached |
docker compose logs -f app |
follow the server log (live agent calls show here) |
docker compose down |
stop; the tailnet node goes offline |
docker compose exec app bun run build |
rebuild the web bundle after web-only edits |
Hot-reload: your working tree is bind-mounted and the server runs under bun --watch,
so server / orchestrator edits reload live. Web (apps/web) edits need a bun run build
(command above) then a refresh. Change a package.json / lockfile → docker compose build
and docker compose down -v (the anonymous node_modules volumes re-seed from the image).
Files:
Dockerfile,docker-compose.yml,docker/tailscale/serve.json. The sidecar serves tailnet-only (Tailscaleserve); to expose publicly, switch the handler tofunnel. If you use local-Gemma ASR or the GPU A/B baseline, the compose file already pointsOLLAMA_URL/BASELINE_BASE_URLathost.docker.internal.
Prefer Docker (above). To run directly on the host — handy for quick, key-less local dev — with Bun ≥ 1.1:
bun install
cp .env.example .env # works as-is in mock mode
bun run dev # server on :3001, web on :5173Open http://localhost:5173, pick a scenario at the bottom (Q3 Sprint Planning / Growth
Review / Launch Page Jam). It streams the fixture transcript over the WebSocket, the router
fires, the summary updates, and the prototype agent fans out four designs onto the canvas —
pick one and watch the Design DNA lock in (then view / download its Google DESIGN.md
from the DNA panel).
Handy talks to DataHub through a small MCP server. Think of the three running pieces like this:
DataHub Core (:8080 metadata, :9002 UI)
↓
official DataHub MCP server (:8000 agent tools)
↓
Handy (:3001 server, :5173 dev UI)
-
Start DataHub Core. This starts the local catalog and UI; it does not start Handy or the MCP bridge.
datahub docker quickstart --version v1.6.0
-
Start the official MCP server in another terminal. It translates Handy's simple tool calls into authenticated DataHub API calls. Use a DataHub access token; do not put that token in Handy or the browser.
export DATAHUB_GMS_URL=http://127.0.0.1:8080 export DATAHUB_GMS_TOKEN='<your DataHub access token>' export TOOLS_IS_MUTATION_ENABLED=true export SAVE_DOCUMENT_PARENT_TITLE='Handy Memory' export FASTMCP_HOST=127.0.0.1 export FASTMCP_PORT=8000 uvx mcp-server-datahub@0.6.0 --transport http
Mutation mode is required because Handy saves accepted files and meeting summaries. Handy does not use the other metadata-editing tools.
-
Enable the bridge in Handy's
.env:DATAHUB_ENABLED=true DATAHUB_MCP_URL=http://127.0.0.1:8000/mcp DATAHUB_FRONTEND_URL=http://localhost:9002
-
Test the contract before starting a demo:
bun run datahub:smoke DATAHUB_SMOKE_WRITE=1 bun run datahub:smoke
The first command proves catalog search, schema reading, and lineage reading. The second also saves a disposable document and waits up to ten seconds for search indexing. In our local test the document became searchable after about 2.5 seconds. Handy adds new memory to the current meeting immediately, so that indexing delay matters only to later search.
Then run Handy normally with bun run dev. Use AGENTS=live for the real demo: mock
prototype HTML is intentionally fixed and cannot visibly prove that DataHub context changed
the generated prototype.
Legacy host path — Docker (above) is the default. Use this only to run on the host directly without containers.
One participant hosts Handy on their own machine, captures their screen, and serves the shared UI. Everyone else opens the same URL.
bun install
cp .env.example .env
bun run host # builds web and serves app + /ws on :3001
bun run funnel # (other terminal) HTTPS Funnel :8443 -> local :3001Open the Funnel HTTPS URL with ?host=1 on the host machine, then press Live → Screen →
Mic. Share the same URL (or a per-guest invite link) with viewers. bun run funnel:off
stops the public listener. Set HOST_PASSCODE before exposing the server publicly.
| Env | Values | Meaning |
|---|---|---|
AGENTS |
mock | live |
mock replays fixture gold labels (no key); live calls Cerebras |
SOURCE |
fixtures | asr |
fixture transcript stream, or live browser/on-device ASR |
FIXTURE_SCENARIO |
id | default scenario (sprint-planning, growth-review, launch-page) |
FACTCHECK_SEARCH |
tavily | none |
ground fact-checks on Tavily, or let the model self-report |
HOST_PASSCODE |
string | gates the WS/ASR/upload endpoints; empty = open. MEETING_PASSWORD is an alias |
EXPORTS_DIR |
path | host-visible folder for saved meeting artifacts (./exports by default; Docker mounts it to /app/exports) |
Two committed audio-fixture sets (16 kHz mono WAV, generated once and committed — the bench
never calls the network). fixtures/audio/ is clean TTS of test-transcripts.json;
fixtures/meetings/ is naturalistic, multilingual (EN/PT-BR), overlapping meetings with true
cross-talk. See fixtures/meetings/README.md.
bun run asr:bench # WER of local Gemma ASR vs the clean fixtures (needs Ollama)
bun run meetings:bench # WER on the realism set; --backend both -> Gemma vs ElevenLabs Scribe
bun run asr:livesim # simulate the live on-device path: energy-VAD over meeting.wav -> GemmaDiscriminated union on type (see packages/shared/src/events.ts). Add an event by
extending ServerEvent/ClientEvent and handling it in both the room (emit) and the ws.ts
reducer (consume) — the compiler flags missing arms.
Server → client: meeting.start, transcript.partial|final, capture.status,
router.decision, summary.update, fanout.start, prototype.start|token|complete,
fanout.resolved, dna.update, factcheck.result, telemetry, mode.changed,
agents.changed, meeting.end, meeting.over, meeting.clear, finaldoc.start|token|complete,
presence (presence.snapshot|join|update|leave|cursor|ping, kicked), context
(context.snapshot|item|updated), exports (export.snapshot), and invite.list.
Client → server: start, live.start|stop, transcript.partial|final, screen.frame,
capture.status, pick (learn this design language), resetTaste, setAbMode, setAgent,
presence.hello|cursor|ping, host.kick, context.accept|reject|clear, meeting.clear|end,
invite.create|revoke.
- Mock / live duality (central pattern). Every agent has two paths selected by
config.agents.mockreplays the gold-labelexpectblocks intest-transcripts.jsonand streams a pre-built themed HTML doc chunked over time to fake token streaming;livedoes real Cerebras inference. When adding agent behavior, implement both and keep the fixtureexpectshape in sync with the Zod schemas. - Session → Room → Orchestrator.
Bun.servegives each socket a thinSession(session.ts) that forwards to one shared in-memoryRoom(room.ts, implementsMeetingRuntime). The room owns presence, invites, file context, the learned Design DNA, and oneOrchestrator(orchestrator.ts) that drives the meeting and the agents. No DB. - The fan-out → pick → learn loop. The first prototype build fans out 4 themes in
parallel, awaits the user's
pick(or 4.2s timeout → recommended), thenlearn(chosen)locks the Design DNA. Later builds are single-shot in the learned theme (and race the GPU baseline when A/B is on). The learned DNA is serialized to a GoogleDESIGN.md(packages/shared/src/designmd.ts) for prompt injection, the DNA-panel download, and the recap appendix —ThemeTokensstays the internal source of truth. - Cancellation. The orchestrator guards every
awaitwithif (my !== this.runId) return;runIdbumps on a newstart()/startLive()/clear()/stop(), so in-flight async work from a stale run self-cancels. Preserve this when adding awaits. bun run typecheckis the only automated check — there is no test runner and no linter. Run it before claiming a change is done.CEREBRAS_BASE_URL/BASELINE_BASE_URLmust not include/v1(the client appends it). All env config is centralized inapps/server/src/config.ts.
| Command | Does |
|---|---|
bun run dev |
server + web together (concurrently) |
bun run dev:server / dev:web |
just the Bun WS server / just the Vite app |
bun run host |
build the web app and serve app + /ws from Bun on :3001 |
bun run funnel / funnel:off |
publish / stop a Tailscale Funnel :8443 → :3001 |
bun run typecheck |
tsc --noEmit across shared + server + web — the check gate |
bun run build |
production web build |
bun run asr:gen / meetings:gen |
(re)generate the audio fixture sets (needs an ElevenLabs TTS key) |
bun run asr:bench / meetings:bench |
WER benches against the committed fixtures |
bun run asr:livesim |
simulate the live on-device VAD → Gemma path |
- Hackathon-critical: implement relevant-context retrieval, structured meeting write-back, and lineage-aware impact analysis through DataHub Agent Context.
- Deepgram as a managed
SOURCE=asrbackend (the scaffold exists; on-device + ElevenLabs are the shipped paths today). - Screen-capture polish: frame dedup (perceptual hash), preview, user-visible privacy state.
- Prompt caching + an explicit token-bucket rate budget in the orchestrator.
- Prototype remix — click a past artifact, speak a change, regenerate.
- CRM / Slack export of the recap and action items.
Licensed under the Apache License 2.0.