Part of the StudioMeyer MCP Stack — Built in Mallorca 🌴 · ⭐ if you use it
LangGraph.js adapter for
darwin-agents. Wrap self-evolving Darwin agents asStateGraphnodes — with zero hard deps.
darwin-langgraph is a thin, peer-dependency-only bridge between two
independent libraries:
darwin-agents— Darwin is an agent framework where prompts evolve themselves via A/B testing, multi-model critics, safety gates, and (sincev0.5.0-alpha.1) full execution-trace capture for GEPA-style reflective optimizers.@langchain/langgraph— LangGraph is the state-graph orchestration runtime used by Uber, LinkedIn, Klarna, Replit, and others to build multi-step, multi-agent workflows with durable execution and human-in-the-loop.
If you use only Darwin, you install nothing extra — Darwin itself has zero LangChain dependency. If you use only LangGraph, the same. If you want to combine the two — let LangGraph orchestrate, let Darwin evolve — install this adapter.
npm install darwin-langgraph@alpha @langchain/langgraph darwin-agents@alphaBoth @langchain/langgraph and darwin-agents are declared as
peer-dependencies — you control the installed versions, the adapter
never pins them.
import { StateGraph } from "@langchain/langgraph";
import { defineAgent } from "darwin-agents";
import {
createDarwinNode,
darwinAnnotation,
withDarwinEvolution,
} from "darwin-langgraph";
const researcher = defineAgent({
name: "researcher",
role: "Topic Researcher",
description: "Five bullets on a topic.",
systemPrompt: "Return exactly 5 bullet points.",
});
const graph = withDarwinEvolution(
new StateGraph(darwinAnnotation())
.addNode("research", createDarwinNode(researcher))
.addEdge("__start__", "research")
.compile(),
{
nodeMap: { research: "researcher" },
onTrajectory: (event) => {
console.log(
`${event.nodeName} ran in ${event.trajectory.turnCount} turns ` +
`with ${event.trajectory.toolCalls.length} tool calls.`,
);
},
},
);
const result = await graph.invoke({ task: "What is GEPA?" });
console.log(result.output);
console.log("trajectory:", result.darwinTrajectory);V0.4 adds six new surfaces on top of the V0.3 baseline. Every addition is additive — existing V0.3 code keeps working. The full list:
| # | Name | When to use |
|---|---|---|
| 1 | createDarwinNode(agent, opts?) |
Wrap one Darwin agent as a StateGraph node. |
| 2 | darwinAnnotation(extra?) |
Annotation with task + output + singleton darwinTrajectory channel. |
| 3 | withDarwinEvolution(graph, opts) (deprecated since V0.2) |
Legacy monkey-patch wrapper — migrate to DarwinCallbackHandler. |
| 4 | DarwinCallbackHandler (V0.2) |
LangChain-native callback handler with onTrajectory + V0.4 onToolEvent + V0.4 maxTrajectoryBytes. |
| 5 | toOtelAttributes(trajectory, opts?) (V0.2) |
Map an ExecutionTrace to OpenTelemetry GenAI Semantic Conventions attributes. V0.4 adds langGraphNode / langGraphStep / userId / conversationId / requestId. |
| 6 | darwinMessagesAnnotation(extra?) (V0.2) |
Annotation with a messages channel for createReactAgent interop. |
| 7 | darwinAccumulatingAnnotation(extra?) (V0.4) |
Annotation with an accumulating darwinTrajectories: ExecutionTrace[] channel — every node write appends instead of overwriting. |
| 8 | createTokenBudgetCallbacks(opts) (V0.4) |
Production guard — throws DarwinTokenBudgetExceededError when a per-invocation token budget is breached. |
| 9 | toW3CTraceContext(event, opts?) (V0.4) |
Pure mapper to a W3C traceparent header value for cross-process span correlation. |
| 10 | MAX_KNOWN_TRACE_VERSION (V0.4 const) |
Highest ExecutionTrace.version this adapter natively understands; isExecutionTrace warns once on higher versions but emits them anyway. |
| 11 | Error classes | DarwinNodeError + DarwinEvolutionHookError + V0.4 DarwinTokenBudgetExceededError. |
Wraps a Darwin AgentDefinition as a LangGraph node action. The returned
function:
- Reads the task from
state[opts.taskKey ?? "task"]. - Calls
runAgent(agent, task, opts.runOptions)fromdarwin-agents. - Writes the agent's output to
state[opts.outputKey ?? "output"]. - Optionally writes the captured
ExecutionTracetostate[opts.trajectoryKey ?? "darwinTrajectory"].
| Option | Type | Default | Description |
|---|---|---|---|
taskKey |
string |
"task" |
State property to read the task from. |
outputKey |
string |
"output" |
State property the output is written to. |
trajectoryKey |
string |
"darwinTrajectory" |
State property the trace is written to. |
runOptions |
object | {} |
Passthrough for model, maxTurns, timeout, cwd, autonomous, config. |
captureTrace |
boolean |
true |
Set false to skip trajectory propagation. |
onResult |
function | — | Fire-and-forget side-effect after each run. Errors are swallowed. |
Returns an Annotation.Root with three pre-defined channels
(task / output / darwinTrajectory) plus any extra channels you pass
in. Composable:
const State = darwinAnnotation({
reviewNotes: Annotation<string[]>({
reducer: (a, b) => a.concat(b),
default: () => [],
}),
});Power-users can spread getDarwinChannelSpec() directly into their own
Annotation.Root({...}) when the helper doesn't fit.
Wraps a compiled StateGraph with a post-run hook that fires onTrajectory
for each node listed in nodeMap after every invoke / stream run.
Errors inside the hook are swallowed — the graph result is always
returned unchanged.
withDarwinEvolution(graph, {
nodeMap: {
research: "researcher",
critique: { agentName: "critic", trajectoryKey: "critiqueTrace" },
},
onTrajectory: (event) => {
// event.nodeName, event.agentName, event.trajectory, event.finalState
},
});import { DarwinNodeError, DarwinEvolutionHookError } from "darwin-langgraph";
try { await graph.invoke({ task: "..." }); }
catch (err) {
if (err instanceof DarwinNodeError) {
console.error(`Node "${err.agentName}" failed:`, err.cause);
}
}GraphInterrupt is re-thrown un-wrapped. When a downstream node calls LangGraph's
interrupt()for human-in-the-loop, the resultingGraphInterruptis surfaced throughcreateDarwinNodeun-touched so the resume protocol (Command({ resume: ... })) works. Only non-HITL errors get wrapped inDarwinNodeError.
Both hooks fire after a Darwin agent finishes, but at different layers:
onResult(per-node, oncreateDarwinNode) receives the full rawRunResult— includingexperiment,evolution,reportPath, and the raw output. Use it for per-agent side-effects that don't care about graph structure (e.g. writing to a database, logging the full result).onTrajectory(graph-wide, onwithDarwinEvolution) receives a normalisedDarwinTrajectoryEventwithnodeName,agentName, trajectory, and a frozen final-state snapshot. Use it for cross-node observability that needs to know which LangGraph node produced which trace (cost tracking, A/B telemetry, evolution loops).
If you only need per-agent side-effects, use onResult. If you need
node-aware telemetry, use onTrajectory. They are not redundant —
combining both is fine, just be aware they fire at slightly different
points (node-completion vs graph-completion).
withDarwinEvolution wraps both invoke() and stream(). The hook
reliably fires when:
- You call
graph.invoke({...})— always works. InternallystreamMode: "values". - You call
graph.stream({...})orgraph.stream({...}, { streamMode: "values" })— works. - You include
"values"in a multi-mode array (streamMode: ["values", "updates"]) — works.
It does not fire on streamMode: "updates" / "messages" /
"debug" (without "values") — the last chunk in those modes is a
node-update dict, not the final state, so the trajectory key lookup
silently misses. The adapter logs a console.warn (once per process)
the first time it detects a non-values stream so you don't silently
lose telemetry. V0.2 will migrate to the LangChain CallbackHandler
mechanism which fires regardless of stream mode.
The examples/ directory ships three runnable scripts:
01-basic-node.ts— single Darwin node in a 1-edge graph.02-multi-agent-research.ts— researcher → critic → writer pipeline with per-node trajectories.03-hitl-with-darwin.ts— LangGraphinterrupt()before a Darwin agent runs, withCommand({ resume: ... })to continue.
darwin-langgraph |
darwin-agents |
@langchain/langgraph |
Status |
|---|---|---|---|
0.3.0-alpha.x |
^0.5.0-alpha.1 |
^1.3.0 |
alpha (this release) |
0.2.0-alpha.x |
^0.5.0-alpha.1 |
^1.3.0 |
superseded |
0.1.0-alpha.x |
^0.5.0-alpha.1 |
^1.3.0 |
superseded |
The peer-dep range darwin-agents: "^0.5.0-alpha.1" follows npm's
prerelease semver rules — 0.5.0-alpha.N and 0.5.0 final satisfy it,
but 0.5.1-alpha.0 does NOT. A patch release of this adapter will be
required when darwin-agents bumps past 0.5.x.
V0.3 closes the three deferred items from V0.2's R2 review. Backwards compatible — no consumer code changes required.
DarwinCallbackHandler now populates two new fields on every emitted
event:
runId: string— the LangChain runId of the node-chain that produced the trajectory. Stable identifier suitable for correlation with OTEL spans, Langfuse traces, LangSmith runs.parentRunId?: string— the runId of the chain that invoked this node-chain. Omitted for top-level invokes. Use it to build the full span hierarchy in OTEL exporters.
const handler = new DarwinCallbackHandler({
nodeMap: { research: "researcher" },
onTrajectory: (event) => {
const attrs = toOtelAttributes(event.trajectory);
const span = tracer.startSpan(event.nodeName, {
attributes: { ...attrs, "darwin.run.id": event.runId },
});
// event.parentRunId can be used as the parent context for span linking
span.end();
},
});withDarwinEvolution legacy events do NOT carry the runId fields (no
runId exists in the monkey-patch path). Migrate to
DarwinCallbackHandler if you need them.
A Symbol.for("darwin-langgraph.evolution.wrapped") sentinel is now
stamped on each wrapped graph. A second withDarwinEvolution(graph, …)
call on the same graph instance emits a one-shot console.warn:
[darwin-langgraph] withDarwinEvolution(): graph appears to be wrapped twice
— both hooks will fire per run, producing duplicate trajectories.
Some users legitimately layer multiple nodeMap slices on one graph,
so the wrap still succeeds — but the silent-double-fire footgun is now
visible in logs. To layer cleanly, prefer multiple
DarwinCallbackHandler instances via callbacks: [h1, h2].
New option maxInFlightRuns?: number on
DarwinCallbackHandlerOptions. Caps the internal runId → InFlightRun
map at the configured size. When the cap is exceeded, the oldest
entry is evicted (Map insertion order) and a one-shot warning fires.
Defaults to 1024 — large enough for typical fan-out, small enough
to surface a real leak within minutes of an incident.
const handler = new DarwinCallbackHandler({
nodeMap: { research: "researcher" },
onTrajectory: (event) => { /* ... */ },
maxInFlightRuns: 500, // tighter for memory-constrained workers
});Set maxInFlightRuns: Infinity to opt out (discouraged in production).
This defends against the failure mode where handleChainEnd /
handleChainError never fires (LangGraph internal bug, OS-level kill
of the worker mid-invoke, parent invoke aborted mid-flight). Without
the cap, the map grows without bound and leaks memory in long-running
processes (server singletons, schedulers, Temporal Workers).
V0.2 ships the three items from the V0.1 V0.2-roadmap, plus runtime deprecation warnings on the legacy wrapper:
DarwinCallbackHandler— LangChain-native replacement forwithDarwinEvolution. Pass it viagraph.invoke(input, { callbacks: [new DarwinCallbackHandler({ nodeMap, onTrajectory }) ] }). No more monkey-patchinginvoke/stream, no moreSet<symbol>race-fix, no morestreamModegymnastics. Works identically withinvoke,stream(anystreamMode), andstreamEventsbecause LangChain's callback mechanism fires regardless of how the consumer iterates.toOtelAttributes(trajectory, opts?)+toolCallToOtelAttributes(call, opts?)— pure mappers from Darwin'sExecutionTraceto flatRecord<string, string|number|boolean>keyed by OpenTelemetry GenAI Semantic Conventions (gen_ai.operation.name,gen_ai.usage.input_tokens, etc.) plus Darwin-namespaced custom attrs. Drop straight into Langfuse, Braintrust, Honeycomb, Datadog. Sensitivearguments/resultfields are opt-in per OTEL spec.darwinMessagesAnnotation(extra?)— variant ofdarwinAnnotationthat also includes LangGraph's canonicalmessageschannel (messagesStateReducer). Use it when your graph mixes Darwin agents withcreateReactAgent/MessagesAnnotation-based prebuilt agents.- Runtime deprecation warning on
withDarwinEvolution— fires once per process on first call. The function still works identically for back-compat; it will be removed in v1.0. Migrate toDarwinCallbackHandler.
Two lines:
- import { withDarwinEvolution } from "darwin-langgraph";
+ import { DarwinCallbackHandler } from "darwin-langgraph";
- const graph = withDarwinEvolution(compiledGraph, { nodeMap, onTrajectory });
- const result = await graph.invoke(input);
+ const handler = new DarwinCallbackHandler({ nodeMap, onTrajectory });
+ const result = await compiledGraph.invoke(input, { callbacks: [handler] });DarwinEvolutionOptions, DarwinNodeMapEntry, and the
DarwinTrajectoryEvent shape passed to onTrajectory are 100% identical
between both APIs — no other code changes required.
No code changes required. V0.3 is purely additive:
event.runIdandevent.parentRunIdare new optional fields — existing callbacks that don't read them keep working untouched.- The double-wrap warning only fires if you actually double-wrap (most users do not).
- The default
maxInFlightRuns: 1024is invisible in normal use. Override only if you have memory pressure or want to opt out viaInfinity.
The adapter releases follow darwin-agents major bumps. When
@langchain/langgraph 2.x lands, the adapter ships a new major within
the same minor of the underlying Darwin release.
The adapter NEVER touches ANTHROPIC_API_KEY. If you run Darwin on a
Claude Max subscription via the Claude Code CLI, your bootstrap must
delete process.env.ANTHROPIC_API_KEY BEFORE invoking the graph —
otherwise Darwin's ClaudeCliProvider will hit the metered API and you
pay per request instead of the flat subscription.
// bootstrap.ts
import "dotenv/config";
delete process.env.ANTHROPIC_API_KEY; // enforce Max-Plan subscription- Zero runtime deps. Both
darwin-agentsand@langchain/langgraphare peer-dependencies. The adapter itself is a couple of hundred lines of TypeScript that re-exports a handful of types. invoke()callsstream()internally. LangGraph v1.x implementsinvokeon top ofstream. The adapter tracks in-flight invokes with a per-callSymbolmarker stored in aSetso the trajectory hook fires exactly once per logical run, even under concurrentPromise.all([graph.invoke(...), graph.invoke(...)]).GraphInterruptre-throw. The adapter importsisGraphInterruptdirectly from@langchain/langgraphso LangGraph's HITL exceptions (bothGraphInterruptandNodeInterrupt) propagate untouched —Command({ resume })works as expected.- Hook errors never propagate. Both
opts.onResult(oncreateDarwinNode) andopts.onTrajectory(onwithDarwinEvolution) use a fire-and-forget pattern with one-shotconsole.warnfor the first swallowed throw per process. - No memory provider construction. Memory backends (Postgres,
SQLite) are resolved by
darwin-agentsitself — setDARWIN_POSTGRES_URL/DARWIN_SQLITE_PATHenv vars or passrunOptions.config.dataDir.
Released under the alpha npm dist-tag in parallel with
darwin-agents@0.5.0-alpha.1. Because 0.1.0-alpha.1 is the FIRST
publish, npm assigns BOTH alpha and latest to it (npm rule: a
package must always have a latest tag) — so npm install darwin-langgraph and npm install darwin-langgraph@alpha resolve to
the same version right now. Once 0.1.0 final ships, latest will
point to the stable release and alpha will continue to track
pre-releases.
For maximum clarity in alpha-stage projects, prefer the explicit opt-in form:
npm install darwin-langgraph@alpha @langchain/langgraph darwin-agents@alphaMIT © 2026 StudioMeyer