OpenBuilds · Build · In progress

Drizzle

A career mentor you talk to. Built during a job search, mostly out of irritation.

Six years in, you know two things about your next job that your résumé cannot say: why you left the last one, and which parts of it you would not do again. Every tool asks for the document anyway, hands it back reformatted, and shows you roles that share its keywords.

A job description is not a direction. Drizzle reads a person once — résumé, a voice call, the paths they try on — and matches on trajectory: role family, domain, altitude. Understand once, exploit many. Everything downstream runs off that one read.

TypeScriptNext.js 15 · Cloudflare WorkersPostgres · Supabase · pgvectorGemma4 on a 4090bge-m3 embeddingsDeepgram Voice Agent
Aligned to your trajectoryKeyword-only matchDirection > keywords
One “you”, three futures. Roles along a direction warm up — including one that shares no keywords with your past. The keyword matches flare once, then cool.

What it does

  1. 01

    Drop a résumé

    One dropzone. No account, no form.

    Parsed to text, then stored verbatim as an immutable source before anything touches it — so every fact downstream can be traced back and re-derived later.

  2. 02

    Extract into a relational shape

    A clean, editable résumé, in seconds.

    The extractor is held to a schema, so it returns typed rows instead of prose I have to parse back out. Editing a field writes a new source — your correction is evidence too.

    The résumé editor — design and template controls on the left, the live résumé in the middle, and an AI panel showing résumé strength and the skills the market keeps asking for
  3. 03

    Talk to it

    A call, not a chat box.

    The prompt is assembled once from your map, your live roles and the people it could introduce you to. Mid-conversation the agent calls back into the app, and roles appear on screen while you're still talking.

    Mid-call: the mentor is speaking, and three real roles have surfaced on screen — a strong fit, a different path, and a pivot — each one tappable to go deeper without leaving the conversation
  4. 04

    Let it change its mind

    Nothing visible. That's the point.

    After the call a second agent reads the transcript and writes insights, each with a confidence. Nothing is deleted — a changed view supersedes the old one and keeps the chain.

  5. 05

    Read the market, separately

    Nothing you see. It runs offline, on a schedule.

    Postings are pulled from company boards continuously, which costs almost nothing. Turning each one into facts and scores is the expensive pass, so it runs once per role well before anyone asks for a match — never inside a request.

  6. 06

    Rank against where you're going

    Roles with a fit number, and the reason for it.

    Three things decide it: skills you can evidence, how the work suits you, and how close the role sits to your direction. The percentage opens into the sentence behind it — including what the role wants that your résumé doesn't show.

    Two ranked roles, each with a fit percentage and the reasons behind it — required skills matched, impact and building aligned — plus comp, location and what the role is looking for

How it works

Each call, and what it cost.

Two ingest paths, one spine. The fast path streams; the slow path thinks.
The spine
01

Separate the immutable from the derivable

What is kept, and what is recomputed

Sources are append-only and never edited; every derived row carries its source id and a confidence. That costs a join on nearly every read, and it means improving a prompt re-derives history instead of orphaning it.

02

Supersede, never delete

How the picture of a person changes

A revised conclusion supersedes its predecessor and keeps the chain intact. Contradiction stays queryable, so the system can say that March disagrees with today. Overwriting would have let March quietly become today.

03

Four layers, one direction of flow

The shape of the data

Sources hold raw evidence, facts the parsed résumé, insights what the calls concluded, variants what gets rendered. Flow is one-way — nothing downstream writes back — so any layer can be dropped and rebuilt from the one above it.

Reading the market
04

Keep the expensive read off the request path

Ingestion

Collecting postings is cheap and runs on a schedule; interpreting them is not, so it happens once per role offline — roughly ten seconds — and never while someone waits. Each row stores the prompt version that produced it, so a prompt change forces a re-read instead of mixing vintages.

05

Place each model where its latency budget is

Model placement

Postings are batch work with nobody waiting, so they run on a local 4090; 2,000 roles through a hosted API is cost without benefit. Résumé parsing sits on the request path, so it runs hosted. Both sides of retrieval share one embedding model, or the distances aren't comparable.

06

Make the model commit

Posting → twelve scored axes

Each posting yields hard facts plus twelve scored axes: what the role demands, what it offers. My first model came back at roughly 0.5 on everything, which is a model declining to discriminate. It took six prompt revisions to fix, and the variance check that caught it now gates any replacement.

Search and ranking
07

The profile is the query

What retrieval means here

Nobody types anything. Hard filters cut the pool first, and what survives goes to the scorer. Applications and dismissals feed back as a weight adjustment afterwards, so ranking personalises without training a second model.

08

Requirements are two-sided

Hard gates

Credentials and years are binary, not soft signals — no score should recover a role that needs a licence someone lacks. I only checked the lower bound at first, so an eight-year analyst got shown internships. Being over the bar disqualifies too.

09

Calibrate the similarity, don't use it raw

Cosine → a score

Cosine isn't a score. I measured the pool's distribution and mapped it linearly: the median, 0.41, becomes the floor; 0.68, the genuine best-match ceiling, becomes 1.0. Anchoring the top at the p99 instead compresses every good match into a tie.

10

Three signals, renormalised when one is absent

The blend

Evidence, fit and direction at 0.35 / 0.35 / 0.30. Fit was 0.45 until I demoted it — it's the softest of the three, estimated by a model on both sides. A missing component drops out and the rest renormalise, so absent data never scores as good or bad.

11

Rank beside the data, not at the edge

Where the arithmetic runs

One call gathers the candidate pool with each role's distance to the direction already computed, and scoring runs in a function next to the database. The edge Worker only fetches the result. I tried doing the arithmetic there first and blew the CPU budget.

Knowing it works
12

Freeze the fixtures, then let them fail

Regression testing a ranker

Twenty-five assertions over fixed profiles pin orderings rather than scores — scores drift, orderings shouldn't. Eight archetypes then run against the live pool with an expected top-K and a must-not-appear list. The must-not half is where an internship ranking for a senior shows up.

Voice
13

Ask for what a résumé structurally cannot hold

Why there is a call at all

A résumé records what happened. It cannot say why you left in 2019, which of those jobs you would refuse to repeat, or where you are trying to get to — and trajectory needs all three. Spoken answers are hedged in ways a date range is not, so every insight the call produces carries a confidence and can be superseded.

14

Trade control of the turn loop for latency

Who owns the conversation

A spoken turn has a few hundred milliseconds, so the live path does the minimum and everything expensive moves after the call. My first design owned every turn and could re-steer mid-sentence; the vendor's agent owns it now — real control traded for a native speech path.

What I tried

Including the ones that lost.

Run the whole voice loop locally, then measure it against hosted

hosted won the live path · local kept for everything off it

Whisper, a local model and Kokoro on 24GB, to see whether hosted inference was needed at all. Steady-state latency was competitive; cold start was not. The first turn spent about ten seconds loading three models, which no conversation survives.

Cost a user before scaling anything

hosted spend stayed inside the noise floor

A first run is two model calls — parse, then score — at fractions of a cent. Per-user cost was never the exposure; interpreting the whole market was, and that sits on hardware already bought. A per-account ceiling caps the blast radius of a runaway loop.

Measure whether a model discriminates, not whether it reads well

model rejected · the variance check became the acceptance bar

A faster model produced plausible output, so I measured score variance instead of reading samples. Five of twelve axes were effectively constant — 0.60 for an intern, 0.60 for a CTO. Everything downstream ranks on those numbers, so the throughput bought nothing.

A silent regression after swapping the embedding model

recalibrated against the measured distribution

New model, old calibration. Its similarity distribution sits lower and tighter, so every trajectory score collapsed to 0.5 — no exception, no alert, the feature simply inert in production. A frozen fixture set caught it: two matches tied at 1.0 that could not both be perfect.

Verify two providers share one vector space

identical — the split is safe

The pool is embedded locally, live queries in the cloud. Same model name, different vendors — comparable only if the spaces are identical, which a shared name does not guarantee. Measured: cosine 1.0000. Otherwise ranking silently compares two coordinate systems.

Give the conversation a way to write to the screen

the thing worth keeping

Letting the agent call back into the app changed what a call is worth. Mid-sentence it surfaces real roles, opens one, names a person to talk to. A conversation that leaves state behind beats one the user has to remember.

Where it is

  • The spine — four layers, sources as evidence
  • Résumé ingest and an editable view
  • Voice mentor with live tool calls
  • Insight extraction from transcripts
  • pgvector trajectory matching over the spine
  • Ranking in an edge function, next to the data
  • Learned drift from what you apply to and dismiss
  • Retire filled and expired roles from the poolnext
  • A distilled extractor — smaller, quantized, cheapernext