After months of building LLM agent pipelines for real projects, I kept hitting the same three walls: my graph state degraded into any soup, wiring MCP servers into agents meant writing the same glue code over and over, and answering “why did this run cost $4?” required either a SaaS observability subscription or grepping JSON logs at 3 a.m.
So I built weave — a small, opinionated framework for multi-agent workflows in TypeScript. It’s MIT licensed, published on npm, and the core is about 600 lines of code. This post is a tour of what it does and why I made the choices I made.
What weave gives you Link to heading
- Type-safe graphs — full TypeScript inference across nodes, no
any-laundering - MCP-native — plug any MCP server in as tools, zero glue code
- Observability built-in — SQLite-backed tracer plus a local trace UI, no SaaS signup
- Durable — SQLite checkpoints, resume a crashed run from the last completed node
- Cost guardrails — per-run USD budget with a hard kill switch
- Multi-provider — Anthropic, OpenAI, Google, Ollama via the Vercel AI SDK v6
Docs live at polymatx.dev/weave, code at github.com/polymatx/weave.
A two-agent pipeline in 60 seconds Link to heading
Install the packages (the AI SDK and provider are peer dependencies, so you control their versions):
npm install @polymatx/weave ai @ai-sdk/anthropic dotenv
Then a complete pipeline — two agents, persistence, cost tracking, audit trail — looks like this:
import 'dotenv/config';
import { agent, graph, END, SqliteTracer } from '@polymatx/weave';
import { anthropic } from '@ai-sdk/anthropic';
const summarizer = agent({
name: 'summarizer',
model: anthropic('claude-haiku-4-5'),
system: 'You write tight 2-sentence summaries. No fluff.',
});
const reviewer = agent({
name: 'reviewer',
model: anthropic('claude-haiku-4-5'),
system: 'You critique writing in 1 sentence — call out the single weakest part.',
});
const tracer = new SqliteTracer('./weave.sqlite');
const flow = graph()
.node('summarize', summarizer.asNode(
(s) => `Summarize: ${s.input}`,
(r) => ({ summary: r.text }),
))
.node('review', reviewer.asNode(
(s) => `Critique this summary:\n${s.summary}`,
(r) => ({ critique: r.text }),
))
.edge('summarize', 'review')
.edge('review', END)
.compile();
const result = await flow.run({
initialState: { input: 'TypeScript is a superset of JavaScript with optional static typing.' },
budgetUsd: 0.05,
onEvent: (e) => tracer.record(e),
});
console.log('SUMMARY:', result.state.summary);
console.log('CRITIQUE:', result.state.critique);
Then inspect what actually happened:
npx @polymatx/weave-ui --db ./weave.sqlite --port 4321
# open http://localhost:4321
You get a runs list, a per-run timeline, every model call with token counts and cost in USD, every tool call with arguments and results. All of it lives in a SQLite file next to your code.
Typed state, for real this time Link to heading
The thing that pushed me to write my own orchestrator instead of settling for what exists: I wanted the graph’s state to be a real TypeScript type, checked end to end.
interface State {
topic: string;
research: string;
draft: string;
}
const flow = graph<State>()
.node('research', researcher.asNode<State>(
(s) => `Research: ${s.topic}`,
(r) => ({ research: r.text }),
))
.node('write', writer.asNode<State>(
(s) => `Topic: ${s.topic}\n\nNotes:\n${s.research}`,
(r) => ({ draft: r.text }),
))
.edge('research', 'write')
.edge('write', END)
.compile();
Every node receives the typed state and returns a typed partial patch. If you rename research to notes in the interface, the compiler walks you through every node that needs updating. That’s the whole point of writing this in TypeScript.
MCP servers as tools, no adapter layer Link to heading
The Model Context Protocol solved the “every framework reinvents tool integrations” problem — as long as your framework can actually speak it. In weave, connecting MCP servers is one call:
import { connectMcpServers } from '@polymatx/weave-mcp';
import { agent } from '@polymatx/weave';
import { anthropic } from '@ai-sdk/anthropic';
const mcp = await connectMcpServers({
fetch: { type: 'stdio', command: 'uvx', args: ['mcp-server-fetch'] },
github: { type: 'stdio', command: 'npx', args: ['-y', '@modelcontextprotocol/server-github'] },
});
const researcher = agent({
model: anthropic('claude-sonnet-4-6'),
tools: mcp.tools, // all MCP tools, namespaced as `<server>__<tool>`
});
// when you're done:
await mcp.closeAll();
Both stdio and SSE transports are supported. Tool schemas are converted from JSON Schema to Zod internally, so the AI SDK’s strict typing survives the trip, and tools are namespaced per server so two servers exposing a search tool don’t collide.
Routing, checkpoints, and the 3 a.m. kill switch Link to heading
Conditional edges route on state — the target can be a node name, END, or a function (sync or async):
graph<State>()
.node('classify', classifier)
.node('billing', billingAgent.asNode(/* ... */))
.node('support', supportAgent.asNode(/* ... */))
.edge('classify', (s) => (s.intent === 'billing' ? 'billing' : 'support'))
.edge('billing', END)
.edge('support', END)
.compile();
Checkpoints make long flows crash-safe. Each completed node appends a checkpoint row to SQLite; resuming restores state and continues from the recorded next node instead of replaying (and re-paying for) finished steps:
const checkpoints = new SqliteCheckpointStore('./weave.sqlite');
const flow = graph<State>()
/* ...nodes/edges... */
.checkpoint(checkpoints)
.compile();
await flow.run({ initialState, runId: 'order-123' });
// later, after a crash:
await flow.run({ initialState, runId: 'order-123', resumeFromCheckpoint: true });
Budgets are the guardrail I wish every framework shipped. Weave keeps a per-model pricing table and computes the cost of every agent call; if the cumulative cost of a run crosses your cap, it throws BudgetExceededError and stops:
await flow.run({
initialState,
budgetUsd: 0.50, // hard cap on cumulative agent call cost
onEvent: (e) => tracer.record(e),
});
An agent loop that gets stuck retrying with an expensive model will burn fifty cents, not fifty dollars.
Observability without a SaaS signup Link to heading
Every run emits structured events — run.start, node.start, node.end, agent.call (with token usage and cost), tool.call (with args and results), token.delta for streaming, run.end, run.error. The SqliteTracer writes them to two tables you can query yourself:
const runs = tracer.listRuns(50); // recent runs with cost/token totals
const events = tracer.getEvents(runId); // full replay of one run
The weave-ui dashboard is just a local Hono + React app reading that same file. Nothing leaves your machine, there’s no agent-analytics vendor in the loop, and the audit trail survives as long as you keep the .sqlite file.
Why not LangGraph.js? Link to heading
Fair question — LangGraph is good software. My reasons, which are also weave’s design goals:
- TS-first: full type inference across graph state was non-negotiable for me.
- MCP-native:
mcp.toolsgoes straight intoagent({ tools }). No adapter layer. - Observability bundled: tracer and dashboard are in the repo, not a paid add-on.
- Small: ~600 LOC core with no LangChain dependency. You can read the whole thing in an afternoon.
If you need a large ecosystem and battle-tested scale today, LangGraph remains a solid choice. If you want something small and sharp that treats TypeScript and MCP as first-class citizens, that’s the niche weave targets.
Try it Link to heading
The monorepo ships three packages and three runnable examples:
| Package | What it is |
|---|---|
@polymatx/weave | Core: agent(), graph(), checkpoints, tracer |
@polymatx/weave-mcp | MCP client integration (stdio + SSE) |
@polymatx/weave-ui | Local trace dashboard (CLI: weave-ui) |
The examples cover a research bot (MCP web fetch + checkpoints), a chat router (conditional edges), and a code reviewer (pipes git diff through a review pipeline):
git clone https://github.com/polymatx/weave && cd weave
pnpm install && pnpm build
echo "ANTHROPIC_API_KEY=sk-ant-..." > examples/chat-router/.env
pnpm --filter weave-example-chat-router start "my invoice looks wrong"
You’ll need Node.js 22+, ai@^6, and any @ai-sdk/* provider on the v3 spec.
Weave is young (v0.1.0) and the surface area is deliberately small. If you build something with it, hit a rough edge, or your favorite model is missing from the pricing table, issues and PRs are open — the pricing data is one hand-maintained file, and adding a model is a one-line change.
Happy weaving.