@layerless/core
v1.3.1
Published
The serverless-native agent framework. Zero schema migration. Runtime contracts. Built-in analytics. RAG support.
Maintainers
Readme
Layerless
The serverless-native agent framework.
LangGraph runs on servers. Mastra runs in containers. Layerless runs in functions — by design, not by patching.
npm install @layerless/corev1.2 — Runtime contracts, built-in analytics, RAG support, Schema Auto-Adapter CLI.
The problem
Every major agent framework was built for persistent servers. Deploy them on Vercel, AWS Lambda, or Cloudflare Workers and you hit the same wall:
- Cold start kills state — function wakes up with no memory of previous steps
- Timeout kills runs — complex agents need 60–120s, serverless limits are 10–60s
- No intent reset — user sends a new message mid-run, agent ignores it or crashes
- Invisible cost — no per-run token tracking, surprise bills
Layerless solves all of these at the framework level.
Quick start
import {
defineState, defineGraph, defineNode,
createRunner, MemoryStore, createLlmClient,
messagesReducer, START, END,
} from "@layerless/core";
// 1. Define state
const AgentState = defineState({
messages: { type: "value", default: [], reducer: messagesReducer },
result: { type: "value", default: "" },
});
// 2. Define nodes
const classify = defineNode("classify", {
type: "deterministic",
execute: async (state) => ({
intent: state.messages.at(-1)?.content.includes("research") ? "research" : "chat",
}),
});
const chat = defineNode("chat", {
type: "ai",
model: "gemini-2.5-flash-preview-04-17",
system: "You are a helpful assistant.",
onComplete: async (result) => ({ result: result.text ?? "" }),
});
// 3. Wire the graph
const graph = defineGraph({ id: "my-agent", state: AgentState, llm: createLlmClient() })
.addNode(classify).addNode(chat)
.addEdge(START, "classify")
.addConditionalEdge("classify", (s) => s.intent === "research" ? "research" : "chat")
.addEdge("chat", END)
.compile({
store: new MemoryStore(), // VercelKVStore in production
tickBudgetMs: 45_000, // yield 5s before Vercel's 50s limit
maxTotalSteps: 20,
});
// 4. Run
const agent = createRunner(graph);
const done = await agent.invoke({
input: { messages: [{ role: "user", content: "What is serverless?" }] },
});
console.log(done.state.result);Core concepts
State survives cold starts
Every state mutation auto-persists. No saveSession(). No manual checkpoints.
// This is ALL you need — state is saved after every node, automatically
const store = new VercelKVStore({ kv });Tick budgeting
Set tickBudgetMs to yield gracefully before your platform's timeout kills the function. The agent resumes exactly where it left off on the next invocation.
.compile({
tickBudgetMs: 45_000, // yield 5s before Vercel's 50s limit
maxTotalSteps: 40,
})Three node types
// Deterministic — no LLM, runs first, cheapest
const classify = defineNode("classify", {
type: "deterministic",
execute: async (state) => ({ intent: detectIntent(state) }),
});
// AI — owns the LLM call + full tool loop
const researcher = defineNode("researcher", {
type: "ai",
model: "claude-sonnet-4-20250514",
system: "You are a thorough researcher.",
tools: [searchWeb, readPage],
});
// Interrupt — pauses run, waits for human approval
const review = defineNode("review", {
type: "interrupt",
onInterrupt: async (state, ctx) => notify.send({ runId: ctx.runId }),
});Parallel agents
Run multiple agents simultaneously, merge their results.
graph
.addEdge(START, "classify")
.addParallelEdge("classify", ["researcher", "fact_checker"]) // run in parallel
.addMergeEdge(["researcher", "fact_checker"], "writer") // merge results
.addEdge("writer", END)Agent memory
Agents remember across runs. Per-user, per-agent, or per-session.
const node = defineNode("chat", {
type: "ai", model: "gemini-2.5-flash-preview-04-17",
system: "You are a helpful assistant.",
execute: async (state, ctx) => {
// Load past conversation
const history = await ctx.memory.loadHistory({
scope: "user", scopeId: ctx.metadata.userId as string,
});
return { messages: history };
},
onComplete: async (result, state, ctx) => {
// Save this exchange
await ctx.memory.saveMessage(
{ role: "assistant", content: result.text ?? "" },
{ scope: "user", scopeId: ctx.metadata.userId as string },
);
return { result: result.text ?? "" };
},
});
// Add memory store to graph
defineGraph({ id: "agent", state, llm, memoryStore: new KVMemoryStore({ kv }) })Dynamic agent spawning
One agent spawns specialists at runtime based on the task — this is CrewAI's "hierarchical process", built for serverless.
import { SpawnRegistry } from "@layerless/core";
// 1. Define specialist sub-agents
const researchGraph = defineGraph({ id: "researcher", ... }).compile({ ... });
const factCheckGraph = defineGraph({ id: "fact_checker", ... }).compile({ ... });
const writerGraph = defineGraph({ id: "writer", ... }).compile({ ... });
// 2. Register them
const registry = new SpawnRegistry();
registry.register("researcher", researchGraph);
registry.register("fact_checker", factCheckGraph);
registry.register("writer", writerGraph);
// 3. Orchestrator spawns at runtime
const orchestrator = defineNode("orchestrator", {
type: "ai",
model: "gemini-2.5-flash-preview-04-17",
system: "You are an orchestrator. Delegate to specialists.",
execute: async (state, ctx) => {
// Each spawn runs inline and returns its final state
const research = await ctx.spawn("researcher", { input: { topic: state.query }, maxSteps: 10 });
const verified = await ctx.spawn("fact_checker", { input: { content: research.state.result }, maxSteps: 5 });
const article = await ctx.spawn("writer", { input: { facts: verified.state.result }, maxSteps: 5 });
return { result: article.state.result };
},
});
// 4. Add registry to graph config
defineGraph({ id: "orchestrator", state, llm, spawnRegistry: registry })Sub-agents:
- Share the parent's store and memory
- Have their own token budget (tracked separately)
- Can spawn their own sub-agents (up to depth 5)
- Return
{ state, runId, totalSteps, durationMs, estimatedCostUsd, status }
Token budgeting
Know exactly what every run costs. Yield or abort when limits are hit.
defineGraph({
id: "agent", state, llm,
budget: {
maxTokensPerRun: 50_000,
maxCostPerRun: 0.10, // $0.10 hard cap
onBudgetExceeded: "yield",
},
})LLM providers
Layerless works with all major providers. Switch by changing one line.
import { createLlmClient } from "@layerless/core";
// Auto-detects from model name
const llm = createLlmClient({ model: "gemini-2.5-flash-preview-04-17" });
const llm = createLlmClient({ model: "claude-sonnet-4-20250514" });
const llm = createLlmClient({ model: "gpt-4o" });
// Or explicit
import { AnthropicClient, GeminiClient, OpenAIClient } from "@layerless/core";
const llm = new GeminiClient({ apiKey: process.env.GEMINI_API_KEY });Set the matching environment variable:
GEMINI_API_KEY=AIza...
ANTHROPIC_API_KEY=sk-ant-...
OPENAI_API_KEY=sk-...Next.js integration
npm install @layerless/next// app/api/agent/route.ts
import { createAgentRoute } from "@layerless/next";
import { agent } from "@/lib/agent";
export const maxDuration = 60;
export const { POST } = createAgentRoute(agent, {
onYield: async ({ runId }) => {
// Re-queue for continuation when tick budget is hit
await fetch("/api/agent/continue", {
method: "POST",
body: JSON.stringify({ runId }),
});
},
});// app/api/agent/continue/route.ts
import { createContinuationRoute } from "@layerless/next";
import { agent } from "@/lib/agent";
export const maxDuration = 60;
export const { POST } = createContinuationRoute(agent);Store adapters
import { MemoryStore } from "@layerless/core"; // dev/testing
import { VercelKVStore } from "@layerless/core"; // Vercel KV (Upstash)// Vercel KV
import { kv } from "@vercel/kv";
const store = new VercelKVStore({ kv, namespace: "my-agent", ttlSeconds: 604800 });Dev server
npx layerless devOpens http://localhost:4000 with:
- Live run list with status dots
- Event stream per run (checkpoints, node timing, errors)
- State snapshot tab — see exactly what's in state after each node
- Budget tab — total tokens, cost breakdown per node
// layerless.config.ts (project root)
import { defineGraph, ... } from "./packages/core/src/index.js";
export default graph.compile({ ... });What makes it different
| Feature | LangGraph | Mastra | Layerless | |---|---|---|---| | Cold start survival | ❌ manual | Via Inngest | ✅ built-in | | Tick budgeting | ❌ | ❌ | ✅ | | Optimistic locking | ❌ | ❌ | ✅ versioned | | Parallel agents | ❌ | ❌ | ✅ fan-out/fan-in | | Agent memory | ❌ | ❌ | ✅ scoped | | Context compression | ❌ | ❌ | ✅ sliding-window | | Token/cost budgeting | ❌ | ❌ | ✅ per run | | Intent reset | ❌ | ❌ | ✅ configurable | | Dynamic spawning | ✅ | ❌ | ✅ | | Built-in analytics | ❌ | ❌ | ✅ your DB | | RAG node type | ❌ | ❌ | ✅ any store | | Schema Auto-Adapter | ❌ | ❌ | ✅ CLI | | Next.js native | ❌ | Partial | ✅ first-class | | Local dev server | ❌ | ❌ | ✅ with replay |
Built-in analytics
Zero-config observability. Data lives in YOUR database. No LangSmith subscription needed.
import { MemoryAnalyticsStore, SupabaseAnalyticsStore } from "@layerless/core";
// Dev: in-memory
const analytics = new MemoryAnalyticsStore();
// Production: your own Supabase
const analytics = new SupabaseAnalyticsStore({ supabase, runsTable: "layerless_runs" });
defineGraph({ id: "pipepal", state, llm, analytics })Query your agent's performance from anywhere:
const mgr = new AnalyticsManager(analytics, "pipepal");
// High-level health summary
const stats = await mgr.summary({ from: "2026-04-01" });
// { totalRuns: 1240, successRate: 0.97, avgCostUsd: 0.0003, p95LatencyMs: 2100 }
// Which nodes are slow or expensive?
const nodes = await mgr.byNode();
// { chat: { runs: 892, avgDurationMs: 1200, avgCostUsd: 0.00028, contractViolations: 3 } }
// What broke in the last hour?
const errors = await mgr.errors({ from: Date.now() - 3600_000 });Required SQL (run once in Supabase):
create table layerless_runs ( run_id text primary key, graph_id text, status text, ... );
create table layerless_nodes ( id bigserial primary key, run_id text, node_id text, ... );RAG support
RAG as a first-class node type. Works with any vector store. Retrieved context is saved in state and survives cold starts.
import { PineconeAdapter, SupabaseVectorAdapter } from "@layerless/core";
// Pinecone
const vectorStore = new PineconeAdapter({
index: pinecone.index("docs"),
embeddingFn: (text) => openai.embeddings.create({ input: text, model: "text-embedding-3-small" })
.then((r) => r.data[0].embedding),
});
// Supabase pgvector (free tier works)
const vectorStore = new SupabaseVectorAdapter({
supabase,
table: "documents",
embeddingFn: (text) => openai.embeddings.create(...).then((r) => r.data[0].embedding),
});
// RAG node — retrieves context, stores it in state, tracks cost
const retrieve = defineNode("retrieve_context", {
type: "rag",
vectorStore,
topK: 5,
queryFrom: (state) => state.messages.at(-1)?.content ?? "",
storeAs: "context",
minScore: 0.7,
contracts: {
post: [{ check: (s) => s.context.length > 0, message: "Must retrieve at least one result" }],
},
});Schema Auto-Adapter CLI
The only framework that adopts YOUR database schema — not forces you to adopt ours.
npx @layerless/cli init --db postgres://user:pass@host/db --table sessionsReads your real table, samples 3 rows, infers your JSON blob structure, generates a working TypeScript store adapter in 30 seconds. Zero schema changes.
# Also available:
npx @layerless/cli dev # Start local dev server
npx @layerless/cli replay --runId run_17759... # Replay a production run locallyAPI reference
defineState(schema)
Define the typed state for your agent graph.
defineNode(id, def)
Define a graph node. Types: deterministic | ai | interrupt.
defineGraph(config)
Create a graph builder. Chain .addNode(), .addEdge(), .addParallelEdge(), .addMergeEdge(), .addConditionalEdge(), then .compile().
compile({ store, tickBudgetMs, maxTotalSteps, ... })
Validate and lock the graph. Required fields — store, tickBudgetMs, maxTotalSteps. The framework won't let you skip these.
createRunner(graph)
Create a runnable agent from a compiled graph.
agent.stream(options)
Run the agent, yielding StreamEvent objects. Handles start, resume, yield, interrupt.
agent.invoke(options)
Like stream() but waits for completion and returns the final checkpoint.
agent.getSnapshot(runId)
Retrieve the current checkpoint for any run.
ctx.memory
Available inside every node.
ctx.spawn(agentName, options)
Dynamically spawn a named sub-agent at runtime. Returns SpawnResult with state, runId, totalSteps, durationMs, estimatedCostUsd, status.
SpawnRegistry
Register named sub-agent graphs. Pass to defineGraph({ spawnRegistry: registry }). remember(), recall(), append(), search(), loadHistory(), saveMessage().
License
MIT — see LICENSE
