@skein-js/server-kit
v0.16.0
Published
Shared, framework-agnostic building blocks for skein-js HTTP adapters — in-memory dev runtime, LangGraph dev-state import, and langgraph.json CORS mapping.
Maintainers
Readme
@skein-js/server-kit
Shared, framework-agnostic building blocks for skein-js HTTP adapters.
Part of skein-js — the open-source alternative to LangGraph Platform for TypeScript: a self-hosted Agent Protocol server for LangGraph.js, and a drop-in replacement for the LangGraph CLI.
This package is the common ground the framework adapters (@skein-js/express,
@skein-js/fastify, @skein-js/nestjs,
@skein-js/nextjs) stand on — so no adapter has to depend on another (or on
Express) just to reuse it. It holds the framework-agnostic pieces (runtime assembly, dev-state import,
CORS, and the Node-http transport the Node-based adapters share); each adapter still writes only the
thin request/response shim its framework needs over the @skein-js/agent-protocol
handler table.
What's here
- Runtime resolution —
resolveProtocolRuntime: turn a{ config } | { deps }bag into a live runtime (assistants seeded, worker started) — the step every adapter runs before mounting routes. - In-memory dev runtime —
loadInMemoryRuntime/loadReloadableInMemoryRuntime: assemble aProtocolDepsbacked by in-process drivers from alanggraph.json. This is what powersskein devand every adapter's{ config }convenience path (hot-reload + snapshot/restore included). - In-code embedding —
embedInMemoryGraphs/graphMapToResolver: build aProtocolDepsaround a compiled graph (or map of them) you already hold — nolanggraph.json, no CLI — then pass{ deps }to any adapter.overridesswaps in production drivers/auth. See docs/embedding.md. - LangGraph dev-state import —
readLanggraphDevState/loadSnapshotIntoStore/describeSnapshot, from the@skein-js/server-kit/devsubpath: read an existing.langgraph_api/directory and reconstruct skein's ownDevStateSnapshot, so adopting skein carries all local state over losslessly. - CORS —
corsFromHttpConfig/toCorsOptionsmap alanggraph.jsonhttp.corsblock tocors-styleCorsOptions;allowedOrigin/corsResponseHeaders/applyNodeCors/sendNodePreflightderive CORS headers for the adapters without a CORS middleware of their own (an unset origin resolves to*, never a reflected origin, so it can't pair with credentials). - Node transport —
sendNodeResponse/sendNodeError: serialize aProtocolResponse(JSON / 204 / SSE) onto a NodeServerResponse, shared by the NestJS + Next.js Pages Router adapters. - Mount prefix —
stripBasePath: strip the path an adapter is mounted under before matching the route table, for adapters that mount a catch-all and match by hand (NestJS, Next.js). - Logging —
createConsoleLogger: the opt-inLoggerfor adapters whose framework owns none (Express, Next.js).formatLogMeta/describeErrorare the shared rendering every line-oriented logger needs — a failed run's identity, stack, andcausechain — so the framework bridges and the CLI's dev logger can't drift on how a failure reads.
The route table itself (
skeinRoutes) is not here — it lives with the engine in@skein-js/agent-protocol, since it references the handler names. Adapters import it from there.
Install
pnpm add @skein-js/server-kit @langchain/langgraph@langchain/langgraph is a peer dependency. You install this package directly when you embed a
graph in code (embedInMemoryGraphs) or write your own adapter; the shipped adapters depend on
it for you.
Usage
The most common direct use is the in-code on-ramp — turn a compiled graph (or a map of them) into a
ProtocolDeps and hand { deps } to any adapter, with no langgraph.json and no CLI:
import { createExpressServer } from "@skein-js/express";
import { embedInMemoryGraphs } from "@skein-js/server-kit";
import { graph } from "./my-graph.js";
const server = await createExpressServer({ deps: embedInMemoryGraphs({ agent: graph }) });
await server.listen(2024);Pass overrides to swap in production drivers or an auth engine while keeping the rest in-memory. See
docs/embedding.md.
API
embedInMemoryGraphs(graphs, options?): ProtocolDeps— build aProtocolDeps(store, queue, bus, checkpointer) around a compiled graph orRecord<string, EmbeddableGraph>.options.overridesreplaces any dep (e.g. a Postgres store, anauthengine).createInMemoryDepsis a@deprecatedalias.graphMapToResolver/normalizeEmbeddableGraphsare the lower-level graph→GraphResolverhelpers.resolveProtocolRuntime(options, frameworkLogger?): Promise<ResolvedProtocolRuntime>— turn a{ config } | { deps }bag (SkeinRuntimeOptions) into a live runtime (assistants seeded, worker started) — the step every adapter runs before mounting routes.options.worker(RunWorkerOptions) tunes the background worker;worker.maxConcurrencyis how many queued runs run at once.frameworkLoggeris the adapter's own default, applied only when the caller supplied neitheroptions.loggernordeps.logger; the result's.loggeris the winner, which the adapter should also use for its transport-fault logging.createConsoleLogger(options?): Logger— a plainconsole.*logger (level,prefix), for adapters whose framework owns none.formatLogMeta(meta)/describeError(thrown)render a failed run's identity, stack, andcausechain as text.resolveRunConcurrency(explicit?, env?): number— the one precedence chain behindworker.maxConcurrency: explicit value →SKEIN_RUN_CONCURRENCY→N_JOBS_PER_WORKER→DEFAULT_RUN_CONCURRENCY(10, matching the LangGraph CLI). The environment is validated even when an explicit value is given, so the two sources can't silently disagree.skein dev/startuse it to resolve the number they print in the startup banner.startHeapPressureMonitor(options): HeapPressureMonitor— samplesv8.getHeapStatistics()on an unref'd interval and warns once per crossing (85% of the heap limit, re-arming below 70%), with in-flight runs and buffered frames alongside so the line distinguishes too-much-concurrency from a slow SSE consumer from a leak.resolveHeapPressureOptions()readsSKEIN_HEAP_WARN_PERCENT(0disables) andSKEIN_HEAP_SAMPLE_MS. Started byresolveProtocolRuntimeand stopped with the worker, so no adapter has to remember it. Free when no logger is configured — it schedules nothing.checkHeapHeadroom(): HeapHeadroom— the boot-time counterpart: compares V8's ceiling with the container's cgroup limit and warns when the automatic sizing does not fit (see deploy.md).describePoolPressure(runConcurrency, poolMax): string | undefined— warns when the worker can execute more runs at once than the Postgres pool can serve.resolveMaxPageSize(explicit?, env?): number— the same chain for the store page bound: explicitmaxPageSize→SKEIN_MAX_PAGE_SIZE→DEFAULT_MAX_PAGE_SIZE(1000). Every driver applies it to list/search, including when the caller asks for no limit.resolveStoreTtl(raw?)/resolveThreadTtl(raw?)— map thelanggraph.jsonstore.ttlandcheckpointer.ttlblocks (snake_case, minutes) onto the drivers' camelCaseStoreTtlConfig/ThreadTtlConfig;undefinedwhen unset, so nothing expires unless asked. Plus the raw input typesRawStoreTtl/RawThreadTtl. They live here rather than in@skein-js/runtimebecause both assembly paths need them and neither owns the other — resolving a block in only one is how a TTL came to work underskein startand silently do nothing underskein dev.loadInMemoryRuntime/loadReloadableInMemoryRuntime— assemble aProtocolDepsfrom alanggraph.jsonusing in-process drivers, reading both TTL blocks itself. The reloadable variant addsreloadGraphs/snapshotState/hydrateState(what powersskein dev's hot reload + cross-restart persistence).readLanggraphDevState/loadSnapshotIntoStore/describeSnapshot— read an existing.langgraph_api/directory and reconstruct aDevStateSnapshot, so adopting skein carries local state over losslessly.Imported from
@skein-js/server-kit/dev, not the package root:import { readLanggraphDevState } from "@skein-js/server-kit/dev";They carry
superjsonandnode:fs/promises, and onlyskein dev/skein importcall them, so keeping them on the root barrel put a filesystem snapshot deserializer into every adapter's module graph. There is deliberately no root re-export — a re-export is still a static import. TheDevStateCountstype remains exported from the root, sinceexport typeis erased. See docs/bundling.md.CORS —
corsFromHttpConfig/toCorsOptionsmap alanggraph.jsonhttp.corsblock tocors-style options;allowedOrigin/corsResponseHeaders/applyNodeCors/sendNodePreflightderive CORS headers for adapters without a CORS middleware of their own.Node transport —
sendNodeResponse/sendNodeErrorserialize aProtocolResponse(JSON / 204 / SSE) onto a NodeServerResponse, shared by the NestJS + Next.js Pages Router adapters.stripBasePath(pathname, basePath): string | null— the pathname relative to a mount prefix, ornullwhen the path is not under it (the caller passes those through untouched). The prefix may be written any way the host wrote it (api,/api,/api/); an empty one passes everything through. Needed only by adapters that mount a catch-all — Express/Fastify get this from their router.
