The meta-claim first
Architecture is the residue of good questions asked in the right order.
I didn't set out to redesign anything. I set out to learn LangGraph, and the fastest way I know to learn a system is to interrogate it from a background it wasn't built for. I came at this Python LLM-workflow framework as a Swift engineer — biased toward typed contracts, determinism, and minimal interfaces — and by the fourth or fifth uncomfortable question I wasn't holding LangGraph anymore. I was holding a different design.
This post is that design. It's honest about being unfinished — I rate it about 6/10 on "could another engineer build this from the spec." It's a thinking artifact, not a library. That's the point, and I'll defend it at the end.
A note on fairness up front: LangGraph is a good framework, and several of the moves below are things it already reaches toward. I'm not claiming it lacks types. I'm pushing off the default mental model most people carry — and then taking it somewhere the framework doesn't quite go.
The baseline I pushed off from
LangGraph is nodes and edges. Nodes do work; edges decide what runs next; state is shared between them. That's the surface, and it's a fine surface to build on.
My first real question wasn't "how do I use this." It was: what is the type of the thing flowing through here? The mental model most tutorials hand you is a flat dictionary — state as a bag every node reaches into. That's where a Swift brain starts itching, and the itch turned out to be load-bearing.
Reframing 1 — graph execution is reducers all the way down
Strip the framework language and every node is a reducer: it takes the current state and an input and returns new state. The whole graph is a fold over a stream of node activations. Seeing execution as reduce surfaces two things the bag-of-state model hides.
Each node owns its own merge semantics. A node isn't just "a function that writes to state." It's a mutation point that decides how its output combines with what's already there — append, replace, deep-merge, last-write-wins. Make that decision explicit and local to the node and a whole class of "wait, why did that key get clobbered" bugs becomes a declared policy instead of an accident.
This is the one place LangGraph is already most of the way here: it supports per-key reducers via Annotated state (add_messages is exactly "this key merges by appending"). So reframing 1 isn't a new feature — it's the lens that makes the rest click, and the push is to treat the reducer as part of the node's contract, not a field annotation off to the side.
// Every node is a reducer — and it owns how its output merges.
protocol Node {
associatedtype Out
var reads: [StateKey] { get } // declared inputs
var writes: StateKey { get } // declared output slice
func run(_ ctx: Context) async -> Out
func merge(_ current: Out, _ produced: Out) -> Out // local, explicit
}
The payoff shows up under parallelism. If two nodes write the same key concurrently, "the dict updates" is not an answer. The reducer framing forces you to name the conflict-resolution policy instead of discovering it in production. (It names the problem. It doesn't solve it — see hole #3.)
Reframing 2 — state is a typed knowledge store, not a bag
A flat dict says every node can read and write everything, and nothing tells you what's supposed to be there. Replace it with a typed knowledge store: state is a set of typed overlays, each with an owner, a schema, and a reducer. A node declares which slices it reads and which one it writes. Now the graph has a contract that's checkable before anything runs.
This is the Swift bias showing, and I'll own it — but the payoff isn't aesthetic. LangGraph already lets you type the state (TypedDict / Pydantic). The push here is to stop treating the schema as a shape and start treating it as a knowledge store with owners — because that ownership is exactly what makes the next reframing possible.
Reframing 3 — edges are typed projection layers, not routers
This is the move I actually care about.
In the default model an edge is a router: it picks which node runs next. That's underusing it. An edge sits between everything in state and what the next node actually needs — which means an edge is a projection. It can narrow, reshape, and select the slice of state the downstream node receives.
Push that one step and it gets interesting: make the selection a retrieval problem. Each node declares its responsibility — what it's for. Use that declaration as the query for RAG-based context selection on the edge: the edge retrieves, from the full knowledge store, the slice most relevant to the node about to run, ranked by that node's own declared purpose.
// An edge is a typed projection: full state -> the slice the next node needs.
// Selection is retrieval, keyed by the node's declared responsibility.
struct Edge<To: Node> {
let to: To
func project(_ store: KnowledgeStore) async -> Context {
let slice = await store.retrieve(query: to.responsibility, scope: to.reads)
return Context(slice) // minimal context, by construction
}
}
Context engineering stops being prompt-stuffing buried inside the node and becomes a typed, inspectable transform on the edge. You can test it. You can log what each node was actually handed. You can diff it when behavior drifts.
The governing principle is just an old one in new clothes: each node should receive the minimum context necessary to do its job — identical to minimal API surface. A node is a function. Its prompt is the function's input contract. The edge is what enforces that contract. That reframing — prompt as input contract, edge as the enforcer — is the single most useful thing that fell out of the whole interrogation.
The honest part — seven holes I didn't fill
This is a strong whitepaper and not yet a spec, because of these. Listing them is the credibility, not the embarrassment.
| # | Hole | Why it can sink the design |
|---|---|---|
| 1 | State storage model | In-memory? persisted? where do overlays actually live across a run? |
| 2 | RAG latency | Retrieval on every edge traversal is a real per-hop cost; unmitigated |
| 3 | Parallel merge conflicts | The reducer framing names concurrent writes; it doesn't resolve them |
| 4 | Partial failure | A node fails mid-graph — what's the state contract then? |
| 5 | Compilation model | When is the topology fixed vs. dynamic? |
| 6 | NodeResponsibility schema | RAG-on-the-edge hinges on this being well-typed — and it isn't yet |
| 7 | Context pruning | Overlays grow; nothing evicts |
Any one of these can break the design under load. #2 and #6 are the scary ones — the centerpiece idea (retrieval on the edge) is also the one with the worst latency story and the least-specified type. That's exactly why it's a 6 and not a 9.
Why publish a 6/10 design
Because the post isn't "here's a library, use it." It's "here's what happens when you refuse a framework's defaults and interrogate it from a foreign discipline." The redesign might be wrong in three of seven places. The method — architecture as the residue of good questions, asked from outside the framework's home turf — is the transferable thing, and it survives even if this specific graph design doesn't.
I learned more about LangGraph by trying to rebuild it than I would have by reading the quickstart twice. Interrogation beats absorption.