condensate

Build and connect

Use the TypeScript SDK

Build a TypeScript client that lists, follows, sends and spawns threads.

On this page

This guide builds a TypeScript client with @condensate/agents-sdk: list threads, follow their events, send work, and start and control threads. The SDK's methods return Effect values, which Effect.runPromise executes. It decodes runtime responses and provides streams for following events.

The SDK ships with early access. It is a private workspace package inside the Condensate repository and is not published to npm. The examples below import it the way code inside the repository does.

These examples are runnable files in apps/docs/src/examples. The docs TypeScript check compiles them, and the website displays their source directly.

1. Configure the client

The client layer provides the runtime's base URL. Save it as client.ts if you are recreating the example in another workspace:

import { Agents, agentsConfigLayer } from "@condensate/agents-sdk"
import { Layer } from "effect"

export const client = Agents.Default.pipe(
    Layer.provide(agentsConfigLayer({ baseUrl: "http://127.0.0.1:4741" }))
)

The default local runtime listens on port 4741. Start it with the local setup guide.

To read the connection settings from the environment instead, provide AgentsConfigLive. It reads these variables:

VariableDefaultMeaning
CONDENSATE_AGENTS_URLhttp://127.0.0.1:4741The runtime origin, with an optional path prefix
CONDENSATE_AGENTS_POLL_MS2000The polling interval for followers, and the first reconnect delay
CONDENSATE_AGENTS_TAIL_LIMIT200The default events per tail page. The runtime caps a page at 1000

2. List the first page of tasks

This read-only example obtains the client, requests up to 20 thread summaries, and prints the pagination metadata:

import { Agents } from "@condensate/agents-sdk"
import { Effect } from "effect"
import { client } from "./client.js"

const page = await Effect.runPromise(
    Effect.gen(function* () {
        const agents = yield* Agents
        return yield* agents.threadPage({ limit: 20 })
    }).pipe(Effect.provide(client))
)

console.log(page.threads, page.nextAfter, page.hasMore)

Run the included example from the repository root:

bun apps/docs/src/examples/list.ts

Pass nextAfter back as the next request's after cursor when hasMore is true. Treat the cursor as opaque. Load a selected thread's detail separately with thread(threadId) so a sidebar does not download every conversation.

The runtime returns at most 200 summaries per page. It also limits how often one client can read the first page. It identifies a client by the peer address plus the x-condensate-client header, or the user agent if that header is missing. A client that repeats that request quickly receives 429 with the code thread-list-rate-limited and a retry-after header. Read the first page once, then follow after cursors, or use followWorkspace (section 7) to learn what changed instead of polling the list. allThreads(query) walks every page for you.

3. Follow recorded events

A follower reads after a durable sequence cursor, then waits for more events. This example prints ten events and exits:

import { Agents } from "@condensate/agents-sdk"
import { Effect, Stream } from "effect"
import { client } from "./client.js"

const threadId = process.argv[2]
if (threadId === undefined) throw new Error("Pass an existing thread ID")

await Effect.runPromise(
    Effect.gen(function* () {
        const agents = yield* Agents
        yield* agents.follow(threadId, { after: 0 }).pipe(
            Stream.take(10),
            Stream.runForEach((event) => Effect.sync(() => console.log(event)))
        )
    }).pipe(Effect.provide(client))
)
bun apps/docs/src/examples/follow.ts THREAD_ID

Replace THREAD_ID with a real ID. The command can remain open if fewer than ten events arrive; press Ctrl+C to stop it. after: 0 starts from the default beginning, which can use a snapshot shortcut for long histories. Use agents.history(threadId) when you intentionally need the complete history behind snapshots.

For an application, store the last event sequence and use it as after when resuming. The follower uses the runtime's push route when it exists and falls back to polling tail at the poll interval when it does not. It reconnects from the stored cursor, so a dropped connection does not repeat or skip events.

Use tailReverse(threadId, { before, limit }) to open a long thread at its end. It returns the newest events first, with a prevBefore cursor for older pages.

stream(threadId, onFrame, options) is different. It delivers live delta frames for one thread without storing them in the log. Its status callback reports live, lost, unsupported or gone. Use follow when you need a record, and stream when you only need what is happening now.

4. Send a message

This example sends a real message to an existing thread. It can start model and tool work under that thread's configuration.

import { Agents } from "@condensate/agents-sdk"
import { Effect } from "effect"
import { client } from "./client.js"

const threadId = process.argv[2]
if (threadId === undefined) throw new Error("Pass an existing thread ID")

const accepted = await Effect.runPromise(
    Effect.gen(function* () {
        const agents = yield* Agents
        return yield* agents.enqueue(threadId, "Summarize the current task and its next step.")
    }).pipe(Effect.provide(client))
)

console.log("Message accepted at sequence", accepted.seq)
bun apps/docs/src/examples/send.ts THREAD_ID

enqueue returns when the message is accepted, with the sequence number and timestamp of the recorded event. Follow the log to observe execution. If your caller needs the eventual text response in one request, agents.send(threadId, text) waits for the reply to settle and returns it.

Both methods take an options object with to. Use it to address one member of a group thread. Without it, the message goes to every member.

Method summary

MethodUse it for
threadPage({ limit, after })A paginated task list
allThreads(query)Every matching thread summary, read page by page
thread(threadId)One task's detail
tail(threadId, { after, limit })A bounded event page
tailReverse(threadId, { before, limit })The newest events first, for opening a long thread at its end
history(threadId, after)Every event, including those behind snapshots
follow(threadId, { after })An ongoing durable event stream
stream(threadId, onFrame, options)Live delta frames that are not stored in the log
followWorkspace(name, options)Every event in one workspace, in host-wide cursor order
enqueue(threadId, text)Asynchronous message delivery
send(threadId, text)A request that waits for the reply

For React chat products, also inspect @condensate/chat-sdk and its shared state and rendering components. The HTTP reference explains the routes beneath these operations.

5. Start and control threads

Agents also starts and controls work. These methods are the common ones:

MethodWhat it does
spawn(request)Opens a thread from a spec (workspace, title, role, engine, model, permissions) and returns its ID. Fields you leave out come from the workspace defaults
fork(threadId, atSeq, spec?)Starts a child thread from an event in a parent's log. The host records the link in both logs
interrupt(threadId, by)Stops the thread's current reply. by is written to the host log
approve(threadId, callId, "allow" | "deny")Answers a tool call that is waiting for approval
gate(threadId, options)Runs the gate check and returns a verdict with any refusal reasons. A refusal is a normal result, not an error
loop(threadId, request)Runs up to maxRounds rounds of a prompt. It waits at human gates and returns the settled outcome. loopStatus, loopContinue and loopStop manage a running loop
merge(childId, result, options)Merges a child's result into its parent. A repeated merge reports alreadyMerged
subscribe(threadId, request)Sends wake messages to the subscribing thread when a watched thread records matching events

A spec's engine is one of three ids. claude runs Claude Code through the Claude Agent SDK. codex runs the Codex CLI's app-server. pi runs Condensate's own agent, which talks to provider APIs from inside the runtime. If you leave engine out, the runtime reads it from the model id: a Claude id (claude-*, opus, sonnet, haiku, fable) runs on Claude Code, and every other id, including OpenAI ids such as gpt-*, runs on Condensate's own agent. A model id alone never picks Codex, so name codex when you want it. The runtime refuses a spawn whose engine cannot serve its model, such as codex with a Claude id. It refuses a codex spawn with HTTP 503 and the code engine-unavailable when the machine lacks the pinned Codex CLI. Both refusals happen before any thread is created. The runtime accepts all three ids, but the SDK's SpawnSpec type spells only claude and pi today, so a TypeScript caller that wants codex has to widen that field.

The credential proxy is on by default; the accounts guide lists its settings and the CONDENSATE_PROXY=off opt-out. With it on, sessions hold placeholder credentials instead of real ones. Pass proxyEnv on a spawn only when one session's outbound traffic should go through a specific proxy. If you omit it, the session leaves through the runtime's own proxy (CONDENSATE_PROXY_URL and CONDENSATE_PROXY_CA). With CONDENSATE_PROXY=off, a session without proxyEnv keeps its network access unchanged.

6. Memory, completions and accounts

  • memory(workspace) returns four methods for the workspace's memory bank: recall finds memories by meaning, retain stores a note and waits for extraction, reflect asks for a synthesized answer without writing memories, and browse reads one page at a time.
  • complete(request) runs an OpenAI-compatible chat completion. Its usage is metered on the workspace's completions thread. The host also serves POST /v1/chat/completions and GET /v1/models, so OpenAI-compatible clients can call it directly.
  • Accounts lists the engine logins the host holds, with their health and identity, and issues short-lived access tokens. Refresh tokens stay on the host. It also adds accounts (create, createOpencode) and starts, completes and checks logins. A token request can be refused for an untrusted caller (403), a rate limit (429), an unknown account (404) or a credential that is not available (409 or 503). Apps that need a model login without a mounted account directory use the token route.

7. Other services

Every service is exported from @condensate/agents-sdk. Workspaces, Github, GithubThreads, Rooms, Questions and Attention also have their own subpath exports, such as @condensate/agents-sdk/rooms.

ServiceUse it for
WorkspacesRead the registered workspaces, and start a build or fix thread on a pull request with startOnPr
GithubRead pull requests, diffs, comments, checks and merge receipts through the runtime's cache and GraphQL budget
GithubThreadsReply to and resolve pull request review threads
RoomsArm a review on a pull request, run its rounds, read the verdict, and finalize it
QuestionsRead a thread's open questions, then answer, dismiss or continue
AttentionRead the items that wait on a person, and follow changes to them
OperationsOpen a progress record for a long action, add milestones, report the outcome, and watch it
CodeRead-only code search, outlines and file reads for served roots (TypeScript, TSX and JavaScript)
XRead public X posts without a host. Expanding a full thread needs an authenticated xurl app
stepsRun a workflow definition, one thread per step

Workflows are ordered steps. Each step spawns a thread, and a step can fork from a named earlier step. A parallel step runs its children together, up to a concurrency limit. If you leave the limit out, it is the hub's build limit when a hub is configured, and otherwise every child runs at once. After one child fails, no child that is still queued starts. Each step's gate is auto or human. A child step cannot carry its own gate. The parallel step carries the gate instead.

8. Errors

Agents, Accounts, Workspaces, Github, Code and Operations fail through the Effect error channel with one of three tagged errors:

ErrorWhen it happens
AgentsTransportErrorThe request failed before a usable response arrived, such as a network failure
AgentsRejectedThe runtime refused the request. It carries status, a code when the runtime sent one, detail, and details with any other fields from the refusal body. A missing thread returns status 404
AgentsDecodeErrorThe response did not match the expected schema

Some services add their own tagged errors for refusals you are expected to handle. Rooms has RoomConflict, RoomNotFound, RoomRequestInvalid and RoomUnavailable. GithubThreads has GithubWritesOff, GithubWriteKeyReused, GithubWriteNotFound, GithubThreadRefused and GithubWriteUnconfirmed. X has XPostNotFound, XUpstreamUnavailable and XAuthUnavailable. Questions and Attention also have their own error types.

follow and followWorkspace report the same errors through the stream. Handle them in the same place you handle the rest of the client's errors. stream has no typed errors. It reports connection problems through its status callback instead.