Voice-first research demo. Click the mic, say a topic, get a cited briefing back.
Every step is a Render Workflow task.
| Platform | Job | |
|---|---|---|
| Render Workflows | Orchestrator. voiceSession task owns the session; plan_queries → search_branch × N → synthesize are independently-retried subtasks. |
|
| AssemblyAI Voice Agent | STT + VAD + LLM + TTS in one WebSocket. Lives inside the voiceSession task. | |
| Mastra | Agent primitive. Plans queries and writes the briefing using Anthropic Sonnet 4 (see ANTHROPIC_MODEL in src/config.ts). |
|
| You.com Research | One call per planned angle, fanned out in parallel. |
Browser ←audio WS→ Web service (broker) ←reverse WS→ voiceSession task ←→ AssemblyAI
│ │
Postgres NOTIFY ←── phase events ── │
│ │
▼ SSE ▼ on tool.call "research":
Browser activity feed research subtask
├─ plan_queries
├─ search_branch × N
└─ synthesize
- Click mic →
POST /api/start→ Render dispatchesvoiceSession. - Task opens AssemblyAI and a reverse WS back to the broker; audio tunnels through.
- You speak a topic. AssemblyAI fires
research(topic). The tool dispatches the research subtask. - Each subtask emits a phase event via Postgres NOTIFY → SSE → activity feed.
briefing.readyfires; tool.result returns the full briefing; AssemblyAI reads it aloud.
cp .env.example .env
createdb ravendr
npm install
npm run migrate
npm run dev # web service on :3000
npm run dev:tasks # workflow runner in a second terminalRequired env on both services:
DATABASE_URL,ANTHROPIC_API_KEY,YOU_API_KEY,ASSEMBLYAI_API_KEY
Web only: RENDER_API_KEY, WORKFLOW_SLUG (default ravendr-workflow).
- Fork. Hit Deploy to Render — Blueprint creates
ravendr-web+ravendr-db. - In the dashboard, create a Workflow service
ravendr-workflow, same repo, start commandnode dist/render/tasks/index.js. - Put secrets in an env group
ravendr-sharedso both services share them. - Migrations run on every web deploy (
preDeployCommand: npm run migrate).
One folder per vendor — each owns its protocol or SDK; the Render task files are thin orchestration glue that compose them.
src/
server.ts routes.ts config.ts web service composition root
assemblyai/
voice-agent.ts AssemblyAI WebSocket protocol client
mastra/
agents.ts Planner, synthesizer, verifier factories
youcom/
research.ts You.com Research API adapter
render/
db.ts typed Postgres queries
event-bus.ts LISTEN/NOTIFY event bus
session-broker.ts pairs /ws/client with /ws/task
workflow-dispatcher.ts @renderinc/sdk wrapper
tasks/
index.ts auto-registers every task with Render
research.ts ROOT orchestrator (pure Render Workflows)
assemblyai/
voice-session.ts holds AssemblyAI WS + reverse WS
mastra/
classify-ask.ts ask-shape classifier
plan-queries.ts shape-aware planner
synthesize.ts shape-aware synthesizer
verify.ts shape-aware verifier + 1-retry
youcom/
search-branch.ts one You.com call (× N parallel)
shared/ ports + events + envelope + errors + logger
static/ vanilla ES modules (index.html + main.js + mic.js)
AssemblyAI's Voice Agent doesn't let the server force the agent to speak a specific string. After tool.result the agent's LLM usually reads the briefing aloud, but not always. When it goes silent, the briefing still renders on screen — voice is the soft path, UI is the hard one. For guaranteed single-voice narration of every phase, swap AssemblyAI for OpenAI Realtime (conversation.item.create).
MIT