# Build (https://condensate.dev/build/)

> The HTTP API, resumable streams, AG-UI and the SDK.

Condensate's own apps are built on the runtime: Dex, the browser app for your threads; the review hub, which runs review rooms; and Dojo, a tutor app. Your own product can be too, through the HTTP API the runtime serves on `127.0.0.1:4741`.

## What you get

### Streams that resume exactly

Every thread is an append-only log in SQLite, and each event is stored before any stream reads it. The durable streams, `tail`, `follow` and `agui`, read stored events from a position you pass. A client that disconnects reconnects with the last position it stored and gets exactly what it missed.

### Live text as a preview

The `stream` route carries text and thinking deltas as they are generated. It has no positions and replays nothing, and a slow reader loses its oldest frames. When a streamed block settles, its stored event replaces it.

### AG-UI from stored history

The `agui` route serves a thread as AG-UI events, the open Agent-User Interaction protocol that chat front ends read. It folds stored events and live deltas together, so a client that reconnects gets the thread's history back.

### One feed for a whole workspace

A change stream tells you which threads in a workspace moved, and to which position. It sends positions, not events, and a thread you subscribe starts with its current position.

### A typed SDK

`@condensate/agents-sdk` is a TypeScript client for the HTTP API, built on the Effect library. It decodes every response with a schema and fails with one of three typed errors. It ships with early access and is not published on npm.

### Code mode

Code mode is an opt-in MCP tool. An agent runs one short JavaScript program against typed namespaces in a QuickJS sandbox, instead of making one model round trip per tool call. Every run has caps, and each cap returns a typed error that names it.

### Runners

A runner is where an agent process runs. Today that is a process on your Mac. Every run passes one admission check before it starts, so a run is never placed on a runner that would refuse it.

### Context windows that keep the prompt cache

The runtime keeps the full history and reduces only what the model sees next, cheapest step first. The provider's view is rendered from the log, so two renders of one log are byte-identical and the prompt cache holds.

### Worktrees you can trust

Every task gets its own worktree. The worktree keeper frees a worktree only after its pull request has merged or closed and it holds no unpushed work, and it never wipes a new run's files.

### Failures that never replay a side effect

A run that fails after a tool has started does not retry. A host restart marks a run as lost and offers Resume instead of re-running it. The failure table at the end of this page lists every case.

## How a stream reads the log

A stream keeps no state of its own. There is no Redis and no stream buffer to keep in step.

1. The runtime stores an event.
2. It signals every follower waiting on that thread.
3. Each follower reads the log from its own position and sends what it finds.
4. A client that reconnects sends the last position it stored, as `after` or as the `Last-Event-ID` header, and gets everything after it.

| Route | What it carries |
| --- | --- |
| `GET /threads/:id/tail` | One page of stored events. `after` reads forward, `before` reads older events newest first. A page holds up to 1000 events. |
| `GET /threads/:id/follow` | Stored events as server-sent events, from `after` or `Last-Event-ID`. |
| `GET /threads/:id/stream` | Live deltas as server-sent events. A slow reader loses its oldest frames. |
| `GET /threads/:id/agui` | The thread as AG-UI events, from `after`. |
| `GET /threads/:id/agui-page` | One reverse page as AG-UI frames, for chat clients that open at the end and page back. |
| `GET /workspaces/:name/changes` | Which threads in a workspace moved, and to which position. |

- **Deltas can be lost.** Order comes only from stored events.
- **AG-UI sends usage in two parts.** When a client connects, it gets the newest 200 usage records one by one and any older ones as a single total.
- **A change stream holds up to 256 threads from one workspace.** Between two sends it keeps only the newest position for each thread. Each thread you subscribe starts with its current position, so a reconnecting client needs no extra tail read.

The thread streams are one-way server-sent events. The [HTTP reference](https://condensate.dev/reference/http/) lists every route.

## The SDK

The three typed errors are `AgentsTransportError`, `AgentsRejected` and `AgentsDecodeError`. The SDK covers listing, spawning, sending, following, forking, approvals, memory, review rooms and workflows. [Use the TypeScript SDK](https://condensate.dev/guides/sdk/) has runnable examples.

- **A follower keeps one cursor across reconnects.** `follow` uses the push route when it exists. When it does not, it polls `tail` with the same cursor, so no event is delivered twice across the change.
- **A 404 is read by its body.** A missing thread is reported as gone, and a missing route as unsupported.
- **A workflow stops at a human gate.** Each step's gate is `auto` or `human`. A loop waits at a human gate and returns the settled outcome instead of failing.

```typescript
// The SDK ships with early access.
import { Agents } from "@condensate/agents-sdk"
import { Effect } from "effect"

// Store the last event position and pass it back as `after` on reconnect.
const resume = (threadId: string, lastPosition: number) =>
    Effect.map(Agents, (agents) => agents.follow(threadId, { after: lastPosition }))
```

## Code mode limits

Code mode is off by default. `CONDENSATE_CODE_MODE` switches it on, and only the principals listed in `CONDENSATE_CODE_MODE_PRINCIPALS` can see it.

A program can reach only the threads it opened, the thread it runs in, and threads below that one in the thread tree. To check this, the runtime walks up at most 32 parent links.

| Cap, per run | Default |
| --- | --- |
| Pure JavaScript time | 10 s. Time spent waiting on a host call does not count, up to a hard ceiling. The ceiling is 300 s for agent calls that wait on a model. |
| Tool calls | 20 |
| Namespace calls | 100 |
| Returned result | 64 KiB, counted in UTF-16 units |

Nothing is cut short silently.

## Runner states

The refusal from the admission check gives the same reason wherever the check fails.

| Runner | State | What it does |
| --- | --- | --- |
| Process | Available | Runs the agent as a process on your Mac. |
| Apple container VM | In development | Runs a Claude agent in a VM. It refuses local plugins, session forks, middleware keys and MCP servers. |
| Container and Kubernetes | In development | The Kubernetes runner takes Claude agents with no local plugins, no MCP servers and no fork, in the pod's empty `/work`. |

In the Apple container VM, the guest sees `/work/.git` masked, and `CAP_SYS_ADMIN` is dropped so it cannot remove the mask. The runtime checks at boot that the VM network is off the internet. Egress is limited to the credential proxy by an operator `pf` rule that lives outside the code.

## Context window steps

Each run has a finite input budget. The runtime moves to the next step when one frees less than 10%.

1. **Microcompact** clears older large results that can be read again, and keeps the last 5 tool results.
2. **Collapse** also clears older action results, and leaves a receipt of up to 200 characters for each.
3. **Summary** keeps the newest 20,000 estimated tokens as written, and summarizes what comes before them.

- **Estimates are never scaled down.** Token estimates are calibrated from measured usage on each run. An overcount only picks a deeper step, while an undercount would overflow.
- **Some results are never cleared by a cheap step.** These are memory and control results, and any result under 1,500 characters. A cut cannot cross a child thread whose spawn has not yet joined.
- **Every timestamp comes from an event.** The provider's view is rendered in event order, and no timestamp comes from the clock.

On Claude Code, the runtime switches off Claude Code's own auto-compaction and builds the context window itself. On Codex, it raises the native auto-compact limit so its own steps run first, and Codex can still compact if it overflows. Condensate's own agent runs the steps inside its loop. [Context windows and memory](https://condensate.dev/concepts/windows/) has the detail.

## Worktree rules

The worktree keeper is the part of the runtime that frees worktrees and watches free disk. It ships in observe mode. It reports what it finds, and acts only on the rules you set to act.

A worktree is held while its task runs, while its pull request is open, or while it holds unpushed work. Age does not matter. When the keeper's disk rule is set to act, new spawns pause while free disk is below 10 GB and resume once it is back above 30 GB.

1. The keeper checks the pull request's state and any unpushed work.
2. It checks again just before the worktree is returned.
3. The return is conditional on the lease, so a new run's files are never wiped.
4. A clone is archived and fingerprinted before it is removed.

## What the runtime does when something fails

| Fault | What the runtime does |
| --- | --- |
| A socket drops mid-stream | The client reconnects from its stored position. The chat client gives up on a read that stays silent for 60 seconds, and retries from its last complete event. |
| The push route is missing | The SDK polls with the same cursor. |
| The database is reset under a feed | The Needs you feed reports that the store changed instead of replaying old events. |
| The host restarts during a run | At boot the runtime marks the run as lost, once, and offers Resume. Nothing re-runs by itself. |
| A run fails before any tool has started | The run may retry once. After a tool has started it does not retry, so a side effect is never replayed. |
| An account refuses for quota | The task moves to the next account in its workspace without re-running tools that already ran. A stream that has already sent text is not moved. |
| Every account in the workspace is out | The refusal names each account, why it is out, and the earliest reset. No account is tried twice. |
| A login is rejected | The account is marked as needing sign-in, and the task moves to the next account. In a model failover chain, every remaining model on that engine is skipped, because they sit behind the same login. |
| A crash interrupts a token refresh | The rotated refresh token is saved to a recovery slot before the publish is retried, so the crash does not force a new login. |
| Two processes refresh at once | The commit is a compare-and-swap. A rotation made elsewhere is adopted, not overwritten. |
| A new run claims a worktree during cleanup | The return is conditional on the lease, so it fails instead of wiping the new run's files. |
| The event loop stalls | The process exits after 120 seconds stalled while idle, or after 600 seconds otherwise. |
| A restart is requested while runs are active | The drain answers new requests with `HostDraining` and lists the active threads. The restart script polls the drain before it restarts. |
| A GitHub write returns no readable result | The write is not sent again. The runtime reads the result back from GitHub to settle the outcome. |

## Where to read next

- [Use the TypeScript SDK](https://condensate.dev/guides/sdk/)
- [The HTTP API](https://condensate.dev/reference/http/)
- [Threads and the runtime](https://condensate.dev/concepts/runtime/)
- [Context windows and memory](https://condensate.dev/concepts/windows/)
- [Connect tools and hooks](https://condensate.dev/guides/middleware/)
- [Use](https://condensate.dev/use/), [Connect](https://condensate.dev/connect/), [Security](https://condensate.dev/security/) and [Status](https://condensate.dev/status/)
