Two deployments
The web app (Next.js, App Router) serves the UI, the REST API, and cron-driven queue dispatch. It deploys automatically on every push tomain.
The agent runtime is a separate Vercel project running eve, a durable agent runtime. It hosts the Producer and Briefing agents as long-lived sessions that survive well past a normal serverless function’s time limit — and survive a redeploy of either service, because a durable session’s state isn’t tied to the process that’s currently running it. This is what makes minutes-long course production and research-heavy briefing generation possible on infrastructure that’s otherwise built around short-lived requests.
The two deployments trust each other through Vercel’s OIDC token mechanism: the web app’s outbound calls to the agent runtime carry a signed deployment-identity token, and the agent runtime checks it against an allow-list of trusted project subjects before accepting a request. If that check fails, the request is rejected — there’s no fallback path that runs the work in-process instead.
One database
Both deployments read and write the same Postgres instance (hosted on Neon), withpgvector installed for the embeddings used in semantic term search. The schema is defined once but exists as two byte-identical copies — one under the web app, one under the agent runtime — because each deployment needs its own local schema module and there’s no shared package boundary between them. A drift test in CI fails the build if the two copies diverge. Migrations are written by hand rather than generated, and applied to both the production database and a separate local test database.
The model stack
Two models, doing two genuinely different jobs:DeepSeek — text reasoning
All agent text work — course curation, glossary review, briefing research and writing, and the Cockpit chat itself — runs on DeepSeek’s
deepseek-v4-pro reasoning model, called with thinking enabled and no cap on reasoning length. This is the only LLM in the production line. All agent text work runs on a single reasoning-model call per task — one agent session with full context on the document, not a chain of disconnected per-paragraph calls.MiMo — multimodal audio
Student sight-interpretation recordings are scored by MiMo, a multimodal model that listens to the audio directly rather than working from a transcript (see Practice loop). MiMo is scoped to that one job; all course and briefing text work runs on the reasoning model, keeping each model where it is strongest.
Queue, heartbeat, and self-healing
Both course production and briefing generation are modeled as queues: a row moves throughqueued → running → done/failed. A cron-driven dispatcher claims queued rows and hands them to the agent runtime; the runtime itself writes the terminal state directly when the work finishes, rather than the web app polling for a result.
The hard part of any queue like this is knowing when a running row is actually dead versus just slow. InterpScout uses a heartbeat: every meaningful step an agent takes — finishing a paragraph window, settling on a batch of terms, fetching a source — updates a timestamp on the row. A row is only reclaimed as orphaned once it’s gone quiet past a threshold, not on a fixed wall-clock timeout from when it started. That distinction matters in practice: a slow-but-alive agent and a dead one look identical on a wall clock, and only the heartbeat tells them apart.
Retries are capped, and a task that keeps failing doesn’t retry the same way forever: the last couple of attempts before the cap switch to a simplified mode that trades completeness for a real chance of finishing, rather than exhausting the retry budget on a full attempt every time.