// post

LangGraph vs vincent: generic graph primitives against an opinionated coding-agent runtime

// Where the two genuinely overlap, why a generic graph runtime has no word for worktree or merge conflict, what vincent's opinionatedness costs, and how the two layer instead of compete.

Vincent executes a graph of steps, persists the state, retries what fails, runs branches concurrently, and stops for a human. So does LangGraph. I build vincent, so “have I rebuilt a worse version of something that already exists” is a question I would rather answer properly than wave off.

The answer is no, and the reason is more interesting than “different scope”. LangGraph is totally generic. It hands you nodes, edges, state, and persistence, and takes no position whatsoever on what the graph means. That is the design, and it is a real strength. Vincent is the opposite kind of artifact: it is an attempt to answer how agentic development should be regulated and helped. What a coding agent is allowed to touch. How its work is isolated. What counts as it having actually worked. Where a person has to sign off. How several agents’ work comes back together without them overwriting each other.

That position is the whole product. Everything below is an attempt to say what it buys and what it costs.

Where they genuinely overlap

The overlap is not superficial, and pretending otherwise would be dishonest. Both treat agentic work as something structured rather than one process invocation, and both arrive at broadly the same primitives:

ConceptLangGraphvincent
Unit of executionnodestep
Deterministic worka functiontype: command
Agentic workan LLM or agent nodetype: agent
Human interactioninterrupttype: manual, a blocked task
Conditional executionconditional edgeif:, type: condition
Repetitiongraph cycletype: loop, type: break
Compositionsubgraphtype: include
Concurrencyconcurrent nodestype: parallel
Independent parallel jobsgraph branchestype: fan_out child tasks
Dependenciesedgeslane needs:
Durable statecheckpointerthe daemon-owned task snapshot

The right-hand column is a real graph, not a linear list dressed up. type: parallel runs sub-steps concurrently inside the task’s single worktree. type: fan_out is the other thing entirely: each lane becomes a real child task with its own worktree and branch, and those branches get merged back. Once lanes name each other through needs:, that lane set is a DAG and the step runs it in topological rounds, merging what is finished before spawning what those merges made eligible. Side by side, the shapes are similar and the substance is not:

LangGraphone typed state, every node reads and writes itplanretrievegeneratecriticstateTypedDictvincentone branch per lane, merged back into the parentimplementagentbuildfan_outapiown branchdbown branchdocsown branchgit mergeinto the parent
Both are graphs that fan out and join. On the left the join is a state update; on the right it is a merge commit that can conflict.

Both systems also converged on the same durability argument, which is that a workflow is persisted execution rather than the lifetime of one process. LangGraph does it with checkpointers and resumable runs. Vincent does it with a daemon that owns SQLite, decides when steps run, applies retry_backoff, enforces timeouts, records attempts, and parks a task in blocked when a person is needed. Different implementations, same observation underneath.

What each one is actually moving

LangGraph moves structured application state between functions. The node is application code, and the thing that travels is a typed structure the nodes read and return updates to:

from typing import TypedDict


class State(TypedDict):
    requirement: str
    plan: list[str]
    code: str
    attempts: int


def retrieve(state: State) -> State: ...
def generate(state: State) -> State: ...
def evaluate(state: State) -> State: ...

Vincent moves work performed against a repository. Its unit is a YAML step, and the shape most work takes is an agent that writes code, a check the agent cannot talk its way past, a gate, and a publish step that only runs once a person approved. This is examples/feature-pr.yaml from vincent’s own repo, with the prompt bodies and the inline comments cut:

  - id: implement
    type: agent
    prompt: |
      Implement the following task in this repository.
    check: go build ./... && go test ./...
    check_timeout: 15m

  - id: commit
    type: command
    run: 'git add -A && git commit -m "{{.Task.Title}}"'

  - id: review
    type: manual
    instructions: |
      Review the diff for task #{{.Task.ID}} on branch {{.Task.BranchName}}.

In the Python, the node’s return value is the state transition. In the YAML, the step’s return value is almost incidental. What it left on disk is the point.

The repository is the workflow state

This is the load-bearing difference, so let me be exact about it. A vincent agent step does not hand back something like:

{
  "modified_files": ["src/api.ts", "test/api.test.ts"]
}

The files have been modified. The repository itself is part of the workflow state, so the worktree is the return value, and everything downstream reads it directly rather than reading a report about it.

That one decision is what makes the rest of vincent’s vocabulary possible. check: is not a validator over a dict; it runs after the step body in that worktree, with a command step’s environment, and non-zero fails the attempt. The diff a human reviews at a manual gate is git diff against the base branch, not a serialized summary. A fan-out join is an actual git merge, so a conflict is conflict markers in files on disk, which is why merge.on_conflict has to choose between block (stop, leave the worktree conflicted, let a person resolve it in place) and agent (hand the conflicted paths to an agent step that has its own check). None of that has a representation in a state dict, because a state dict has no filesystem under it.

That is what a fan_out step looks like drawn at the level git sees:

parent task · worktree on the base branchmerged into the parentapi · child task, own worktreedb · child task, own worktreedocs · child task, own worktreeon conflictblockleft for a personagenta resolver step
Lane branches are cut from the parent at the point the step spawns them, so a lane sees what is committed there and nothing else. merge.on_conflict decides who resolves the join.

The agent step is a black box

Vincent’s type: agent is deliberately opaque. It renders a prompt, hands it to an adapter for Claude Code, Codex, or Cursor, and waits. What the agent does inside, its planning, its searching, its tool calls, its edits, is not vincent’s business and vincent has no opinion about it.

That is the cleanest architectural boundary between the two systems, because LangGraph is a good tool for building what happens inside that box:

seedcommandimplementagentreviewmanualbuildfan_outwhat LangGraph could build inside that boxplanretrievegeneratecriticstructured state
The step types are vincent's; the ids are illustrative. Everything in the dashed panel is invisible to the runtime on the left.

So the two systems are not competing for the same job. One owns the control flow inside an AI application. The other owns the operational execution of that application’s output across a real codebase.

The vocabulary is the regulation

Here is what a running vincent task looks like from the outside, which is the best single piece of evidence I have for the argument:

vincent Workflow tab for task #194 showing a succeeded command step, an approved manual step, an eager fan_out, and two derived lanes each running command, agent and command, with a status bar of step settings
Task #194 on a v0.7.0-211-g26aa193 development build. The status bar carries the selected step's own shell, max_retries, retry_backoff and timeout.

Every box on that screen is a domain noun a generic graph runtime does not have. LangGraph’s nouns are state, messages, nodes, edges, and checkpoints, all content-free on purpose. Vincent’s are repository, task, worktree, branch, diff, command, coding agent, check, child task, merge, conflict, and human gate. Each of those is a position taken, and taken together they are the regulation:

  • check: an agent reporting success is not evidence. The check runs after the step body, in the worktree, and its failure is appended to the next attempt’s prompt automatically, so a failed check becomes the agent’s next instruction rather than a message to you.
  • manual: a person is a scheduled participant, not an interruption. The task enters awaiting_gate and releases its concurrency slot while it waits, and rejecting moves it to blocked.
  • max_retries, retry_backoff, timeout: all three are per-step fields with workflow-level defaults, which is the max_retries: 1 retry_backoff: 30s timeout: 3m0s reading off the status bar at the bottom of that screenshot.
  • blocked: a resting state rather than a dead end. Work stops where it broke, with its worktree intact, and waits for a retry, an edited prompt, a skip, or an ad-hoc repair agent in the same worktree.
  • fan_out: parallel agents get isolated branches, not a shared mutable state object. max_depth is 3 and max_tasks is 64, both checked when the task is created, so an oversized or cyclic plan is a 400 naming what is wrong instead of two hundred worktrees you find later.

You can build every one of those on generic graph primitives. Nothing in the list is beyond a Python function and a conditional edge, and I want to be clear that this is not a capability claim. The difference isn’t what is possible. It’s what is default. In a generic runtime a guardrail is something you remember to build; here it is a field you would have to deliberately remove, and a step type you would have to deliberately not use. That is a different claim from “vincent can do more”, and it is the only one I am making.

LangGraph orchestrates what an AI agent does. Vincent orchestrates what AI coding agents do to a software project.

The operational problem, not the model problem

Generating a plausible diff is close to free now, and it keeps getting cheaper. Knowing whether that diff is correct, knowing exactly what it touched, and having a person who chose to let it through are the expensive parts, and none of them get cheaper when the next model lands. That is the half I care about, because it is the half a better model does not solve.

One agent you can watch. Four, you cannot, and at that point the question stops being whether it is a good agent and becomes an operational one: what is each of them allowed to touch, how do you tell afterwards what each of them actually did, and what happens when two of them edit the same file. Vincent’s answers are worktree isolation, a branch per lane, and a real merge at the join. The useful property of those three is that they hold whether or not the agent behaved.

Regulation, in the sense I mean here, is constraint that survives the agent being wrong. A check: is not a suggestion an agent can argue with; it is a command, and its exit code decides the attempt. A manual gate is not advice; the task does not advance until a person says so. max_depth: 3 and max_tasks: 64 are a ceiling on how far a fan-out can spread, checked when the task is created rather than discovered when the worktrees appear. Drawn as a path, one agent step and everything that can stop it looks like this:

agentwrites the worktreecheckits exit code decidesworktree changedfailthe failed check'soutput goes intothe next promptretries exhaustedpassmanual gatethe task stops hereapprovenext steprejectblockedworktree intacta person picks oneretryeditskiprepair
Two roads reach blocked: a check that ran out of retries, and a gate a person rejected. Neither one advances on its own.

The second output is a trail. GET /v1/tasks/{id}/steps returns every step run and every attempt carrying the rendered prompt, the rendered check, the exit code, the duration, and which level supplied the agent and the model, next to the per-attempt transcript and the diff sitting on a branch. A step a person skipped is even distinguishable from one an if: guard skipped, because the guard-skipped row carries skip_reason: "condition" and the hand-skipped one carries nothing. That record exists because the work happened in git and in a database rather than in a variable.

The gate is the piece I would defend hardest, and its credibility rests on something small: a task at awaiting_gate releases its concurrency slot. A waiting human is not a lock the rest of the board queues behind, so other tasks keep running while one sits waiting for a decision. That is what turns “a person signs off” into a schedulable event instead of a good intention.

None of this makes an agent’s output correct, and I would rather be blunt about that, because a claim that sounds like safety is cheap to make. A check asserts what somebody wrote it to assert and nothing more. A gate is worth exactly as much as the attention of the person reading the diff. Isolation between agents is not isolation from the machine they run on. What the vocabulary buys is narrower: the work becomes reviewable, and the damage a wrong answer can do stays bounded by a branch nobody has merged yet.

My read is that every team running more than one agent against a repository ends up answering these questions, deliberately or by accident. Vincent is one deliberate answer, and the reason I would rather ship it than argue it is that you can run it against your own repository and find out whether the answer holds.

What being opinionated costs

The strongest version of the objection is straightforward, and it is correct: a determined team can implement all of vincent’s semantics on top of a generic graph runtime, and what they get in exchange is a graph they can reshape however they like. Vincent’s opinions are a floor and a ceiling at the same time.

The ceiling is real and I would rather name it than let someone discover it. Vincent has nine step types. Its control flow is structured, not free: steps run in order, top to bottom, guarded by if:, ended early by type: condition, repeated by type: loop, left by type: break, and spliced from another file by type: include. There is no arbitrary edge from any step to any other step. If your problem needs a shape that structured control flow cannot draw, there is no syntax for it and no escape hatch that gives you one.

The scope is a harder limit than the syntax. Vincent only fits work that is SDLC-shaped. Its entire vocabulary assumes a git repository underneath, so if the thing being orchestrated is not a change to a codebase, every one of its concepts is dead weight and you should use something generic. And everything inside the agent is delegated by design. Message state, model and tool routing, retrieval, reasoning loops, long-term memory: vincent has none of it, and that is absence by design rather than weakness.

One more cost, which I have named before: vincent is pre-1.0, so the workflow and API surfaces can still move.

They compose

Because the boundary is clean, the two stack rather than compete. A vincent type: command step can invoke a LangGraph-based analysis tool and let a check: decide whether the run counted. A coding agent behind a vincent adapter can be a LangGraph application internally, and vincent will neither know nor care, because the contract at that boundary is a prompt in and a changed worktree out.

Not every coding agent needs LangGraph, and not every LangGraph application needs vincent. But an organization that builds a sophisticated custom agent and then wants to run it repeatedly, concurrently, and under human control against real repositories has two separate problems, and they happen to have two separate answers.

Where I have landed on positioning

Vincent should not be positioned as an alternative to LangGraph. That puts it in the wrong category and invites a comparison on features neither one is trying to win. If the hard part of your problem is inside the AI application, in the state, the routing, the retrieval, or the reasoning loop, reach for a generic graph runtime and enjoy the fact that it takes no position on your domain. If the hard part is what a fleet of coding agents is allowed to do to a repository, and how you would know afterwards that it worked, then a graph runtime gives you the primitives and leaves the entire question open.

That is the trade vincent makes: a vocabulary narrow enough to be useless outside software development, in exchange for guardrails you get by default instead of by discipline. The documentation and the repository have the detail, and the rest of what I work on is at lezli01.is-a.dev.

← all posts