> ## Documentation Index
> Fetch the complete documentation index at: https://docs.interpscout.org/llms.txt
> Use this file to discover all available pages before exploring further.

# Architecture

> Two Vercel deployments, a shared Postgres, and a two-model stack

InterpScout is two independently deployed services talking to one database, not a monolith.

## 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 to `main`.

**The agent runtime** is a separate Vercel project running [eve](https://github.com/anthropics/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), with `pgvector` 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:

<CardGroup cols={2}>
  <Card title="DeepSeek — text reasoning" icon="brain">
    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.
  </Card>

  <Card title="MiMo — multimodal audio" icon="waveform-lines">
    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](/concepts/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.
  </Card>
</CardGroup>

## Queue, heartbeat, and self-healing

Both course production and briefing generation are modeled as queues: a row moves through `queued → 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.
