AGENTS.md · git:20261025.7513728 · 2026-10-25 · sha256 d65ed76e76c6d644
AGENTS.md git:20261025.7513728A
Immutable. This exact content never changes and is served at /api/v1/blob/d65ed76e76c6d644.
# AGENTS.md
Canonical guide for humans **and** AI coding agents working in this repo. If you only read
one file before contributing, read this one. (Claude Code: `CLAUDE.md` points here.)
## What skein-js is
The **open-source, self-hosted runtime for LangGraph.js agents** in TypeScript. LangGraph.js
defines and executes each graph; skein-js hosts one or many graphs and supplies the API, persistence,
background work, and console around them. It is an alternative to LangGraph Platform (now LangSmith
Deployment), and includes an [Agent Protocol](https://github.com/langchain-ai/agent-protocol) server
plus a CLI that is a **drop-in replacement for the LangGraph CLI** (`skein dev` ⇄ `langgraph dev`,
unchanged `langgraph.json`).
Think "[aegra](https://github.com/aegra/aegra) for TypeScript."
Local deployment is an option alongside shared servers: `@skein-js/local` embeds the same Agent
Protocol engine with SQLite in a desktop app, CLI, daemon, or one Node owner. LangGraph owns
execution and checkpoints; applications own models, tools, credentials, process lifetime, and
recovery decisions. SQLite records abandoned attempts as failures without automatically replaying
input. See [docs/local-deployment.md](./docs/local-deployment.md).
## Read first
- [docs/index.md](./docs/index.md) — overview & architecture
- [docs/reuse.md](./docs/reuse.md) — **what we reuse from LangGraph OSS vs. rebuild**
- [docs/code-practices.md](./docs/code-practices.md) — codified conventions
- [docs/testing.md](./docs/testing.md) — testing strategy
- [docs/roadmap.md](./docs/roadmap.md) — milestones
## Golden rules
1. **Reuse first.** Before writing anything, check whether a `@langchain/*` package already
does it ([docs/reuse.md](./docs/reuse.md)), and whether a user could already solve it over the
API we ship — if they nearly can, build the missing primitive, not the feature. The best code is
the code we don't write, and published API is the code we can never unwrite: we rename by alias +
`@deprecated` and never by removal, and a break majors every package at once.
[`/audit-plan`](.agents/skills/audit-plan/SKILL.md) runs that check over a plan or proposal
before any of it is built.
2. **Pragmatic functional style.** Pure core, dependencies injected; thin classes only for
stateful resources (pools/clients). Immutable by default. Validate at boundaries with Zod;
throw typed errors at the edges.
3. **Simple & consistent.** Small functions, named exports, kebab-case files, one public
surface per package (`src/index.ts`). Match the surrounding style. Let the linter/formatter
settle style — don't hand-argue it.
4. **Green before commit — never commit red.** A pre-commit hook enforces this mechanically:
[`.githooks/pre-commit`](.githooks/pre-commit), wired by `pnpm install` (`prepare` sets
`core.hooksPath`), runs `nx format:check` + `nx affected -t lint typecheck test` (with `CI=true`
so Vitest runs once; `examples/*` excluded — they need live services / local `.env`) for whatever
your staged changes touch. Build and the Docker-backed integration tests run in
[CI](.github/workflows/ci.yml) — run them locally too for anything substantial
(`nx run-many -t build test-integration`). Keep docs current with any behavior/architecture change —
including the installable `skills/` guidance when onboarding or deployment changes —
and when you touch a user-facing doc, regenerate the LLM bundle with `pnpm docs:llms` (rebuilds
[`llms-full.txt`](llms-full.txt) from the curated list in `scripts/generate-llms-full.mjs`; the
hand-written index is [`llms.txt`](llms.txt)).
[`/commit`](.agents/skills/commit/SKILL.md) does the full local run + docs check for you. Only
bypass the hook (`git commit --no-verify`) in a genuine pinch, and go green before you push. Same
bar as [Definition of done](#definition-of-done).
## This is an Nx monorepo (pnpm) — use Nx
**Always drive tasks through Nx**, not ad-hoc scripts. Nx gives caching, the affected-graph,
and consistent targets across packages. Every root `pnpm` script is a thin wrapper over an
Nx target — there are no bare `eslint`/`prettier`/`vitest` invocations.
- `build` / `typecheck` / `test-integration` are defined per project in `project.json`.
- `lint` is inferred by the **`@nx/eslint`** plugin from the root `eslint.config.mjs`.
- `test` is inferred by the **`@nx/vite`** plugin from each project's `vitest.config.ts`.
- Formatting is **Nx's built-in Prettier** (`nx format:write` / `nx format:check`).
```bash
pnpm install # bootstrap the workspace
# whole workspace (each is `nx run-many -t <target>`)
pnpm build
pnpm typecheck
pnpm lint # nx run-many -t lint (@nx/eslint)
pnpm format # nx format:write (Prettier via Nx)
pnpm test # nx run-many -t test (@nx/vite, unit)
pnpm test:integration # nx run-many -t test-integration (Testcontainers; needs Docker)
# a single project
nx build core # == nx run core:build
nx test storage-postgres
nx test-integration storage-postgres
# only what your change affects (prefer this in CI + locally)
pnpm affected # nx affected -t lint test typecheck build
nx graph # visualize the project graph
```
### Tests
Vitest via a workspace config; Testcontainers for real Postgres/Redis. See
[docs/testing.md](./docs/testing.md).
SQLite's real-file, reopening, and subprocess-crash cases run in ordinary Nx `test` targets
(`storage-sqlite` and `local`) without Docker. Native desktop packaging has separate CI gates.
```bash
pnpm test # fast unit + conformance (memory), no Docker
pnpm test:integration # *.integration.test.ts — needs Docker
pnpm test:coverage
nx affected -t test # affected projects only
```
### Adding a package
Match the existing shape exactly (keep it boring and consistent):
`packages/<dir>/{package.json, project.json, tsconfig.json, vitest.config.ts, README.md, src/index.ts}`,
`"@skein-js/<name>"`, publishable metadata (`publishConfig.access: public`, `repository.directory`),
and a path alias in [`tsconfig.base.json`](./tsconfig.base.json). `project.json` defines only
`build` + `typecheck` (+ `test-integration` if it touches Postgres/Redis); `lint` and `test`
are **inferred** by the Nx plugins from `eslint.config.mjs` and `vitest.config.ts`.
**Scaffold with the Nx CLI, never by hand:** `npx nx g @nx/js:lib <name> --directory=packages/<dir>
--importPath=@skein-js/<name>`, then align the generated files to the `@skein-js/core` template. Do
not create a package tree file-by-file.
### Maintaining scaffolding and workspace guidance
`packages/scaffold` owns the shared builders, Node policy and compiled templates used by
`create-skein-js` and `@skein-js/nx`. Edit template sources, then run
`pnpm exec nx run scaffold:generate-templates`; never hand-edit `templates.generated.ts`.
Generated projects default to ESLint/Prettier and recommend Node 24, with engines
`^22.12.0 || ^24.0.0 || ^26.0.0`. Preserve explicit opt-outs, compatible workspace pins, root configuration,
registration exclusions and existing dependency versions.
- Scaffold into a new child directory in an existing workspace. Avoid `create-skein-js . --force`
at a workspace root: it is rejected before writes. Use the Nx generator for Nx applications.
- Check tool dependencies separately from inherited ESLint/Prettier configuration. Root configs
reuse declared root tools and add missing tools to the generated dependency owner. Both entrypoints
must format new files using inherited settings. Workspace installation stays manual at the root.
- Preserve generated `^default` dependency inputs and `^build` ordering for build/typecheck/test.
Preserve SQLite/WAL/SHM exclusions and live-target caching settings. Verify that a library change
invalidates the application's cached build and checks.
- Keep [workspace tooling and caching](docs/scaffolding.md#workspace-tooling-and-caching), package READMEs,
and installable `skills/` guidance aligned with behavior. When changing behavior, add its
regression coverage and update the documentation together. The [testing guide](docs/testing.md#scaffold-consumers)
describes the 96 generation cases and 33 installed consumers; green curated CI does not prove
cases outside those fixtures.
## Releasing
Releases are cut with **`nx release`** (configured in [`nx.json`](./nx.json) as a _fixed_ group — every
`packages/*` shares one version and one `v{version}` tag) and **published by CI, not locally**:
```bash
pnpm release:dry-run 0.3.0 # preview everything; changes nothing
pnpm release 0.3.0 # or: patch | minor | major
```
`pnpm release <specifier>` runs a build + typecheck gate, bumps every package to the new version, writes
`CHANGELOG.md`, commits, tags `v<version>`, pushes, and **creates a GitHub Release**. It does not publish
(the script bakes in `--skip-publish`). Publishing to npm is triggered by that release:
[`.github/workflows/release.yml`](.github/workflows/release.yml) runs on `release: published` and
publishes each changed, non-private package with provenance via npm OIDC trusted publishing.
After a stable release successfully publishes to npm, the release workflow calls
[`.github/workflows/docs.yml`](.github/workflows/docs.yml) to build and deploy the docs from the same
release commit. Pushes to `main` and pull requests still run docs checks, but do not deploy the site.
Pre-releases and manual Release dispatches do not deploy docs. Retry a failed docs deployment by
re-running its job in the original release run, preserving the released commit; there is no standalone
manual Docs trigger.
Prerequisites: the git remote must point at `skein-js/skein-js` (Nx derives the release repo from
`origin`), and `gh` must be authenticated (or `GITHUB_TOKEN` set) for the GitHub Release step.
The `github-pages` environment must allow deployments from tags matching `v*`, alongside its existing
deployment rules and protections.
## Package map
| Package | Role |
| ----------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `@skein-js/core` | The shared contract: Agent Protocol wire types + `SkeinStore`/queue/bus interfaces + edge error |
| `@skein-js/agent-protocol` | Framework- **and runtime**-agnostic Agent Protocol engine — run engine, handler table, SSE (the heart); installs with no graph runtime |
| `@skein-js/langgraph` | The LangGraph.js binding: presents a compiled graph as an `AgentGraph`, plus the store bridge and checkpoint clone the engine injects |
| `@skein-js/config` | `langgraph.json` parser + graph loader (wraps `@langchain/langgraph-api`) |
| `@skein-js/server-kit` | Shared, framework-agnostic adapter building blocks (in-memory runtime, dev-state import, CORS mapping) |
| `@skein-js/express` · `@skein-js/fastify` · `@skein-js/nestjs` · `@skein-js/nextjs` | Framework adapters — thin transport shims over the engine's handler table + `skeinRoutes` |
| `@skein-js/fetch` | Web-standard Fetch transport — `Bun.serve` / `Deno.serve`; the production path on those runtimes |
| `@skein-js/storage-memory` | In-memory `SkeinStore` + queue (dev/tests) |
| `@skein-js/storage-sqlite` | Durable local `SkeinStore` + run queue in one exclusively owned SQLite file |
| `@skein-js/local` | Lean SQLite/LangGraph embedding through `embedSqliteGraphs` |
| `@skein-js/storage-postgres` | Postgres `SkeinStore` + pgvector; reuses `PostgresSaver` |
| `@skein-js/redis` | Run **queue** + cross-instance pub/sub (not a checkpointer) |
| `@skein-js/bullmq` | Optional external command/outcome transport over the native client; separate from the runtime's execution and delivery queues |
| `@skein-js/console` | The skein console — a web UI (assistants/threads/runs/store/crons/time travel) compiled into the package and served by the server at `/console` |
| `@skein-js/langsmith` · `@skein-js/posthog` · `@skein-js/otel` | Telemetry sinks — LangSmith tracing, PostHog analytics, OpenTelemetry spans/metrics |
| `@skein-js/runtime` | Assembles `ProtocolDeps` from `langgraph.json` with memory/Postgres/Redis or the optional SQLite pair; `embedPostgresGraphs` embeds graphs in code |
| `skein-js` (CLI) | Drop-in `dev` / `up` / `build` / `dockerfile` |
| `@skein-js/nx` | Nx application generator (23.2.1 through 23.x) |
| `@skein-js/scaffold` | _(private)_ Shared templates, Node policy and workspace planning |
| `create-skein-js` | `npm create skein-js@latest` — scaffolds a project. Templates are shared under `packages/scaffold/templates/*.tmpl`, compiled into `src/templates.generated.ts` |
| `@skein-js/test-support` | _(private)_ Testcontainers helpers + `SkeinStore` conformance suite |
Local applications are `desktop-electron` (Daybook meeting workspace), `desktop-tauri` (Reading Room
document library), and `local-cli` (Dayplan daily planner). Their private shared graph/UI code lives
in `local-workflow`. Normal runs select Vertex AI, Gemini API, OpenAI, Anthropic or Ollama through UI/environment settings; deterministic fixtures
are limited to tests and packaging gates. See [local deployment](docs/local-deployment.md) for start
commands and host boundaries.
Examples live in `examples/`: `chat-app` (flagship — research assistant + Next.js/shadcn UI),
`triage-agent` (the console's workload — cron sweep, a durable run per item, idempotent re-sweeps,
conditional HITL approval, store memory; runs offline with no API key),
`deep-agent` (experimental Deep Agents support — a research briefing assistant with per-user memory,
streamed and background subagents, and approval before emailing; runs offline with a scripted model), `migrated-langgraph` (drop-in proof), `gemini-chat` (model-backed e2e), `express-basic`
(zero-setup), `react-usestream` (`useStream` harness). Each non-Express adapter ships a **standalone**
example (a dedicated graph server) and an **embedded** one (graphs mounted alongside the app's own
routes): `fastify-basic`/`fastify-app`, `nestjs-basic`/`nestjs-app`, and `nextjs-basic` (Pages Router,
headless) / `nextjs-app` (App Router, same-origin `useStream` UI). **`@skein-js/fetch` is the exception**
— it has no example yet. It is not mounted by hand in a Bun/Deno app so much as selected with
`skein build --runtime`, and its serving behaviour is covered by the runtime matrix in
[`ci.yml`](.github/workflows/ci.yml) rather than by an example anyone would copy.
## Conventions (enforced)
- **TypeScript strict**, ESM only, **named exports only** in `packages/*` (Next.js example excepted).
- **Filenames** `kebab-case.ts`; types `PascalCase`, values `camelCase`.
- **Names reveal purpose** — a reader knows what a symbol does from its name alone. No vague names
(`data`, `info`, `util`, `handle`, `manager`, `process`, `doIt`) and no unexplained abbreviations.
Functions read verb-first (`startRun`, `resolveGraph`, `mirrorThreadState`); values are nouns
(`activeRun`, `graphResolver`). Prefer a longer honest name over a short opaque one.
- **Layout by feature** (`runs/`, `threads/`, `store/`), not by kind.
- **Zod** at boundaries; typed error classes at edges.
- **Publishable packages stay consumable and bundleable.** Every publishable _library_ sets
`exports["."]` to `{ types, import, default }` — `default` last, so `require()` resolves instead of
throwing `ERR_PACKAGE_PATH_NOT_EXPORTED`. The `skein-js` CLI is the deliberate exception (no
exports, runs on import). Guarded by `packages/test-support/src/package-exports.test.ts`.
Library code must not read package-relative files at runtime: bundlers rewrite `import.meta.url` to
the output location. Compile the asset in instead — see `packages/storage-postgres/scripts/` and
[docs/bundling.md](./docs/bundling.md).
- **[Conventional Commits](https://www.conventionalcommits.org)** (`feat(core): …`), small focused PRs.
## Definition of done
- [ ] `pnpm lint`, `pnpm format:check`, `pnpm typecheck` clean.
- [ ] `pnpm test` green; container tests added for new DB/queue behavior (`*.integration.test.ts`).
- [ ] New storage drivers pass the shared `SkeinStore` conformance suite.
- [ ] Reused an existing `@langchain/*` capability instead of reinventing where one exists.
- [ ] Docs updated if behavior/architecture changed.