English | 中文
Warning
This project is under active development and is not production-ready. Pull requests for bug fixes and enhancements are warmly welcomed!
Self-hosted LangGraph Agent Protocol server on Bun + TypeScript + PostgreSQL,
with a standalone agentseek-api CLI. This is the TypeScript/Bun counterpart of
the Python edition of AgentSeek API, focused on two hard guarantees:
- Background runs — runs execute server-side; an SSE disconnect never kills a task.
- Resumable streams — reconnect with
Last-Event-IDand missed events are replayed from the in-memory replay buffer. After a restart, crashed runs are requeued and resume from their last checkpoint, and results/step history stay queryable from Postgres — only the pre-restart stream replay is lost.
Note
Composition, not fork. The official
@langchain/langgraph-api
package is used unmodified for HTTP routes, SSE, protocol semantics, the run
queue, and graph loading. This repo only adds a PostgreSQL storage backend
injected through the official startServer(options, { ops }) extension
point, plus a Bun runtime plugin that swaps the package's in-memory
checkpointer for PostgresSaver at module-load time (no files are ever
patched). Everything the official server supports
(Agent Protocol surface, stream modes, LangGraph Studio) works as upstream.
Current release boundary:
- Implemented: assistants, threads, runs, SSE streaming (all stream modes of
the official server), background + resumable runs, crash recovery,
human-in-the-loop (
interrupt/ resume withCommand), Postgres-persisted checkpoints, in-memory event replay (Aegra-style) - Explicitly not implemented: multi-instance / distributed runtime (single instance by design), Postgres-backed Store API (the upstream in-memory store is used as-is), crons
- Bun 1.3+
- PostgreSQL (the scaffolded
docker-compose.ymlprovides Postgres 16 on:5442— Docker is enough)
mkdir my-agent && cd my-agent
bun add agentseek-api-ts
bunx agentseek-api init
bun installinit scaffolds langgraph.json, an example graph (graphs/agent.ts),
docker-compose.yml, and .env. No patching is involved: the Postgres
checkpointer is swapped in at runtime by a Bun plugin inside the server
bootstrap, before any @langchain/langgraph-api module loads.
docker compose up -d postgres
bun run dev # agentseek-api dev — hot reload, default port 2024curl http://127.0.0.1:2024/infoimport { Client } from '@langchain/langgraph-sdk'
const client = new Client({ apiUrl: 'http://localhost:2024' })
// An assistant is auto-registered for every graph in langgraph.json.
const assistants = await client.assistants.search()
const thread = await client.threads.create()
const stream = client.runs.stream(thread.thread_id, 'agent', {
input: { steps: [] },
streamMode: 'values',
})
for await (const chunk of stream) {
console.log(chunk.event, chunk.data)
}Create a background, resumable run; watch it; disconnect; reconnect with
Last-Event-ID:
THREAD=$(curl -s -X POST localhost:2024/threads -H 'Content-Type: application/json' -d '{}' | jq -r .thread_id)
RUN=$(curl -s -X POST localhost:2024/threads/$THREAD/runs \
-H 'Content-Type: application/json' \
-d '{"assistant_id":"agent","input":{"steps":[]},"stream_mode":["values"],"stream_resumable":true}' \
| jq -r .run_id)
# Attach, then Ctrl-C mid-run — the run keeps executing server-side
curl -N localhost:2024/threads/$THREAD/runs/$RUN/stream
# Reconnect later: events after seq 3 are replayed from the buffer, then live-tailed
curl -N -H 'Last-Event-ID: 3' localhost:2024/threads/$THREAD/runs/$RUN/streamOr run the scripted version against your dev server:
bun run verify # disconnect + resumable reconnect
bun run scripts/verify-advanced.ts # token streaming / interrupt / double-textThe package installs agentseek-api as the executable.
| Command | What it does |
|---|---|
init |
Scaffold a project: langgraph.json, example graph, docker-compose.yml, .env. |
dev |
Start the server with hot reload (re-runs on graph/config edits). |
serve |
Start the server without reload, for containers or production. |
Options: -p, --port <port> (default 2024, or PORT env), -h, --help.
| Variable | Default | Meaning |
|---|---|---|
DATABASE_URL |
postgresql://postgres:postgres@localhost:5442/langgraph |
Postgres connection string (metadata + run queue + checkpoints). |
PORT |
2024 |
HTTP port. |
HOST |
0.0.0.0 |
Bind address. |
N_WORKERS |
2 |
Concurrent run workers. |
RUN_EVENTS_TTL_SECONDS |
3600 |
How long a finished resumable run stays replayable (non-resumable buffers are dropped ~30s after finish). |
agentseek-api reads langgraph.json from the working directory — the same
schema as the official LangGraph CLI, so one config file works with this
server, langgraph dev, and LangGraph Studio.
{
"graphs": {
"agent": "./graphs/agent.ts:graph"
},
"env": ".env"
}graphs: graph id →./path/to/file.ts:exportName(object form withpathis also accepted)env: dotenv file path, or an object of environment values (existing process env always wins)auth,http(route toggles, CORS),ui,ui_config: passed through to the official server
bin/agentseek-api.ts CLI: init | dev | serve
src/server.ts bootstrap: config + env → checkpointer plugin → import the app
src/checkpointer.ts Bun plugin: swaps the upstream in-memory checkpointer for PostgresSaver
src/app.ts runServer(): PostgresOps + startServer(..., { ops })
src/storage/ PostgresOps — the Ops implementation injected upstream
- Storage:
assistants,assistant_versions,threads, andrunstables (idempotent DDL at boot). Checkpoints live inPostgresSaver's own tables. - Run queue: pending runs are claimed atomically with
FOR UPDATE SKIP LOCKED; runs on the same thread are serialized. - Resumable streams: every published event goes into an in-memory replay
buffer with a per-run monotonic sequence (capped at 10k events per run). A
reconnect replays
seq > Last-Event-IDfrom the buffer, then tails live. Replayability derives from the run's ownstream_resumableflag — there is no first-joiner race. - Crash recovery: on boot, runs stuck in
runningare requeued (up to 3 attempts) and resume from their last checkpoint. - Replay window: events of a finished resumable run are kept for
RUN_EVENTS_TTL_SECONDS(default 1 hour), then swept; non-resumable buffers are dropped ~30s after the run finishes.
Postgres is the source of truth for run state, thread state, and checkpoints —
those survive any restart. The stream replay buffer is deliberately in-memory
(the same trade-off Aegra made): a restart loses the event-by-event replay
window, while results and step history remain queryable via /state,
/history, and /join.
The repo registers three LLM-free graphs for exercising the runtime:
agent— deterministic 5-step slow graph (~10s), for disconnect/reconnectchat— fake streaming chat model, for token-levelmessagesstream modesapproval—interrupt()human-in-the-loop, resumed with aCommand
import { runServer, loadConfig, applyEnv } from 'agentseek-api-ts'
const config = loadConfig(process.cwd())
applyEnv(config, process.cwd())
await runServer(config)runServer registers the checkpointer plugin first and only then loads the
protocol server, so the module-load ordering is handled for you.
- Single instance by design. Queue wakeups, cancellation, stream
coordination, and the event replay buffer are all in-process. Running two
copies against one database is not supported — a second process (including a
forgotten
bun run dev) steals queued runs and its stream events are invisible to the first. Multi-instance needs a shared broker (e.g. Redis pub/sub + Redis Lists), not just a second process. - Stream replay does not survive a restart. Events are buffered in memory (TTL 1 hour after a resumable run finishes, ~30s for non-resumable ones, 10k events per run). Run results and step history always survive via Postgres checkpoints; what's lost is only the ability to replay the token-by-token stream from before the restart.
- Store API is not Postgres-backed. The upstream store is used as-is: it
operates in memory and snapshots to a
.langgraphjs_api.store.jsonfile in the working directory. Data survives a same-directory restart, but is lost when the directory changes or a container has no volume, and it is not queryable via SQL. @langchain/langgraph-apiis pinned exactly (currently1.4.2) because the checkpointer plugin substitutes a specific internal module of it; upgrading means re-verifying that module path and export still match.
git clone <repo> && cd agentseek-api-ts
bun install
docker compose -f templates/docker-compose.yml up -d postgres
bun run dev # server on :2024
bun test # unit + e2e suites (e2e spawns its own server on :2098; needs Postgres up)
bun run check # prettier + oxlint + tsc --noEmit — must pass (pre-commit runs it)- Bun — runtime, test runner, and the
Bun.pluginmodule-interception API - LangGraph — graph runtime
@langchain/langgraph-api— HTTP routes, SSE, protocol semantics, run queue (used unmodified; its in-memory checkpointer is swapped at runtime via a Bun plugin)@langchain/langgraph-checkpoint-postgres— checkpoint persistence (PostgresSaver)- PostgreSQL + node-postgres — metadata, run queue, and checkpoints
- Agent Protocol — external compatibility reference for the assistants/threads/runs surface