Who Should Own the Agent Loop: Runtime Harnesses vs Composed Frameworks
A decision framework for building AI agents that turns on one question, who owns the run loop, comparing Node harnesses against LangGraph, CrewAI, and LangChain.
This article is published in English.
Sources: Who Owns the Agent Loop? Node.js Harnesses vs. Python Frameworks (LangGraph, CrewAI, LangChain)
From a distance, the tools for building AI agents look interchangeable. Each one gives you a model, a way to call tools, and some machinery that keeps the model working until a task is finished, and with any of them you can have a functioning agent running in an afternoon. The disagreement between them stays hidden until you are past the demo and into production, and then it turns out to be fundamental: who owns the loop that drives the agent? One family of tools hands you a runtime and steps aside; the other hands you abstractions and asks you to assemble them. This article draws the line between those two philosophies precisely, then gives you a way to decide which side a given project belongs on. The goal is not to crown a winner but to make the trade explicit.
On one side sit harnesses: runtimes that own the full send-prompt, run-tools, feed-results, repeat cycle. You do not write that cycle; you configure and extend it. On the other side sit frameworks such as LangGraph, CrewAI, and LangChain, which give you explicit building blocks, graphs, crews, and chains, and expect you to wire the control flow together yourself. Although this gets framed as Node versus Python, the language is a side effect. The real variable is how much of the machinery you want to own.
Two definitions that hold up under pressure
Pin down the two words before leaning on them. A harness is a runtime that takes ownership of the agent loop. It performs the cycle on your behalf: dispatch the prompt, gather the tool calls the model wants, run them, return the outputs, and repeat until the model stops asking for anything. It also tends to absorb the surrounding hard problems, including context and memory handling, compaction of history as the token budget fills, retries, and session persistence. You direct it through configuration, meaning the system prompt, the model choice, the permitted tools, and turn limits, and through hooks and extension points, never by hand-writing the loop. One harness in this space, Pi, a Node and TypeScript runtime, sums up the philosophy as primitives rather than features: you adapt the runtime to your workflow instead of contorting your workflow to fit it.
A framework, or SDK, is instead a set of explicit abstractions that you compose in code. LangGraph offers a graph of nodes joined by edges. CrewAI offers crews of role-playing agents. LangChain offers chains. In every one of them, you are the author of the control flow, and the library only supplies the parts and lets you connect them. The distinction that actually matters is this: in a harness the loop is hidden and belongs to the runtime, while in a framework the loop, or the graph shape, or the crew arrangement, is out in the open and belongs to you.
It is a spectrum rather than a switch. At one extreme you steer a runtime through configuration and hooks while it owns the cycle; at the other you author every node, edge, crew, or chain, and the loop is literally your own code.
HARNESS you → [config + hooks] → ╔═══ runtime owns the loop ═══╗ → result
║ prompt → tools → results → ↺║
FRAMEWORK you author every node / edge / crew / chain → the loop IS your code
Keep that axis in mind, because everything below follows from where a tool sits on it.
The harness approach: give the loop to the runtime
Pi is a minimal, Node-native harness organized as a layered stack, and you consume only the layer you need. At the bottom is a provider abstraction spanning many model providers; in the middle is the agent loop and tool dispatch; above that is a full runtime with session persistence; and at the surface is a terminal UI. For most engineers the middle layer, the loop, is the interesting one. Starting an agent means describing what it should be and then handing control over. A small research agent built on the session API looks like this.
import { getModel } from "@mariozechner/pi-ai";
import { createAgentSession, SessionManager } from "@mariozechner/pi-coding-agent";
const { session } = await createAgentSession({
model: getModel("anthropic", "claude-sonnet-4-6"),
cwd: process.cwd(),
sessionManager: SessionManager.open("./session.jsonl"),
tools: ["read", "grep", "bash", "search_docs"], // built-ins + your custom tools, by name
customTools: [searchDocsTool], // your typed tools
});
// You steer the runtime with config, not loop code:
session.agent.state.systemPrompt = "You are a research assistant. Always cite sources.";
// A single call runs the entire prompt → tools → results → repeat cycle:
await session.prompt("Summarize the latest on agent frameworks and cite sources.");
Notice what is absent. There is no while loop, no branch that says "if the model returned tool calls, run them and call the model again," and no token accounting to decide when history should be compacted. The runtime handles every bit of that. All you supplied was a model, the tools it may use, and a system prompt, followed by a single call to the prompt method. A custom tool is likewise a small, typed object: a name, a description aimed at the model, a schema for its parameters, and an execute function.
const searchDocsTool = {
name: "search_docs",
description: "Search the internal docs and return matching passages.",
parameters: { type: "object", properties: { query: { type: "string" } }, required: ["query"] },
async execute(_id: string, params: { query: string }, signal?: AbortSignal) {
const passages = await search(params.query, signal);
return { content: [{ type: "text", text: passages }], details: {} };
},
};
So far this reads like convenience and little more. The depth shows up the moment you need to change how the loop behaves, because you cannot rewrite a loop you do not own. Instead you reach into it through events and hooks. Subscribing to events lets you observe the run and intervene, for example by counting turns and pulling the plug once a ceiling is reached.
let turns = 0;
const MAX_TURNS = 20;
const unsubscribe = session.agent.subscribe((event) => {
if (event.type === "turn_end" && ++turns >= MAX_TURNS) {
session.agent.abort(); // pull the cord on a loop you didn't write
}
});
The same shape covers a cost ceiling: accumulate the per-message cost as it arrives and abort once it crosses a threshold. The runtime keeps driving the loop while you watch it pass by and decide when to halt it. Hooks go a step further, letting you intercept a decision before it takes effect, invisibly to the model. A before-tool-call hook can inspect a pending call and refuse it.
session.agent.beforeToolCall = async (ctx) => {
if (ctx.toolCall.name === "bash" && isDangerous(ctx.args.command)) {
return { block: true, reason: "blocked by policy" };
}
return undefined; // allow
};
The model "asked" to run a shell command; the hook quietly declined and returned a reason, and the loop carried on as though nothing had happened. You reshaped the agent's behavior around a loop you never authored. That is the bargain a harness offers. You surrender the ability to rewrite the control flow, and in return you get a working agent quickly, strong defaults for the genuinely hard parts such as context management, compaction, and persistence, and a tidy set of seams, configuration, events, and hooks, for bending behavior. It is an excellent deal precisely when the runtime's opinions line up with yours.
The framework approach: compose the control flow yourself
The Python ecosystem placed the opposite bet. Rather than a runtime that owns the loop, it supplies abstractions you assemble, and three of them dominate.
LangChain: composing a chain
LangChain is the oldest and broadest of the three. It started as a way to string LLM calls together and grew into a sprawling platform: more than a thousand integrations, an interface that does not care which model sits behind it, and a sibling product for observability. For agents it provides a ReAct-style tool-calling loop, and its composition style chains one component into the next. Its great strength is reach: if a model, a vector store, or an external tool exists, a LangChain integration for it probably does too. Its weakness is the price of that generality. Deep layers of abstraction can turn debugging into spelunking, and because it is stateless by default, durability is something you bolt on rather than something you inherit. LangChain suits standard, largely linear single-agent workflows where breadth of integration is the thing you value most.
LangGraph: modeling a state machine
LangGraph makes a pointed, opinionated wager: agent systems are state machines, and you should govern every transition. You model the agent as a graph whose nodes are steps and whose edges are transitions, all operating over a typed state object.
from langgraph.graph import StateGraph, MessagesState, START, END
def call_model(state: MessagesState):
return {"messages": [llm.invoke(state["messages"])]}
graph = StateGraph(MessagesState)
graph.add_node(call_model)
graph.add_edge(START, "call_model")
graph.add_edge("call_model", END)
app = graph.compile()
app.invoke({"messages": [{"role": "user", "content": "hi"}]})
Where a harness conceals the loop, LangGraph turns it into the artifact you author. Conditional branching is spelled out explicitly.
graph.add_conditional_edges("call_model", should_continue, {"tools": "tools", "end": END})
That continue-or-stop function is the very branch a harness runs for you behind a single prompt call. Here it is yours: visible, testable, and versionable. The reward for the extra verbosity is LangGraph's standout feature, native checkpointing at every transition. Because state is written to disk as the graph advances, you get execution that survives restarts, recovery after a crash, the ability to rewind and inspect earlier states, and pauses that wait on a human partway through a run. The costs are a steep learning curve and a design that is deliberately unfriendly to serverless deployment. LangGraph suits long-running, cyclic, crash-resistant, stateful workflows.
CrewAI: assembling a team
CrewAI wagers that the majority of multi-agent problems decompose neatly into teams of specialists. Each agent is defined by a role, a goal, and a backstory, and you then collect them into a crew and hand it tasks.
from crewai import Agent, Task, Crew
researcher = Agent(role="Researcher", goal="Find key insights on {topic}",
backstory="A meticulous analyst.")
writer = Agent(role="Writer", goal="Summarize the research clearly",
backstory="A concise technical writer.")
research = Task(description="Research {topic}.", expected_output="3 insights.", agent=researcher)
summary = Task(description="Write a summary.", expected_output="A short summary.", agent=writer)
Crew(agents=[researcher, writer], tasks=[research, summary]).kickoff(inputs={"topic": "agent frameworks"})
The mental model is intuitive and the road to a working multi-agent pipeline is short, which is why it enjoys heavy enterprise uptake. The flip side is that the role-and-goal abstraction is convenient right up until something goes wrong, at which point it becomes a black box; delegation between agents can loop; and you have to supply your own guardrails against runaway cost. CrewAI suits role-based multi-agent systems and rapid prototyping. The thread tying all three together is that you author the control flow. That is both the cost, more code, more decisions, more to maintain, and the benefit, since you can see, test, and reshape every transition. Python's gravity in data and machine learning is a genuine tailwind here, because the agent lives beside the libraries and notebooks you already work in.
The dimensions where the choice is actually made
Lined up next to each other, these approaches pull apart along a small number of axes, and a few of them carry most of the weight in practice.
Ownership of the run loop is the parent axis; everything else descends from it. A harness keeps the loop for itself and a framework surrenders it to you, with LangGraph anchoring the you-write-everything end, Pi anchoring the runtime-writes-everything end, and the others distributed between.
Durability is LangGraph's decisive advantage. Persisting a checkpoint at each transition hands you crash recovery and replay at essentially no extra effort. The session persistence a harness offers and the memory types CrewAI provides are worth having, but neither is equivalent to state you can durably replay. When a process is killed halfway through a run, a checkpointed graph picks up from the last transition, whereas a session log generally only lets you piece together, after the fact, what took place.
Orchestrating several agents is built in for CrewAI, via roles, and for LangGraph, via subgraphs. A harness gets there by other means, whether sub-agents, extensions, or coordination logic you supply.
The surrounding language ecosystem tends to decide quietly. Choosing Python pulls the agent into the orbit of data and ML work, sitting alongside your dataframes, training code, and notebooks. Choosing TypeScript pulls it toward the web and the edge, sitting alongside your API surface, your shared frontend types, and wherever you deploy serverless functions. That gravitational pull frequently outweighs any one feature on a comparison sheet, and it pays to be candid about which world your team already inhabits.
A final qualifier keeps the comparison honest: the Node-with-harness, Python-with-framework pattern is a tendency, not a law. There are harness-style runtimes written in Python and graph-style orchestration libraries written in TypeScript. Decide from the paradigm and your actual constraints, never from the language stamped on the package.
One job, seen from each side
Picture a single, ordinary task: an agent that fetches something through a tool and then produces a summary of it. What each paradigm makes you confront up front is the revealing part. Under a harness, the work is nearly all configuration. Having declared a search tool and a system prompt, you issue one call and the runtime takes over, invoking the model, observing that it wants the search, executing it, returning the results, allowing the model to compose the summary, and terminating on its own. The harness effectively asked you a single thing: what capabilities should this agent have?
Under a framework such as LangGraph, the work is nearly all structure. You specify the state object, a node that calls the model, a node that executes the tool, and a conditional edge that chooses between returning to the model and ending. Until that graph is fully drawn, nothing executes at all. LangGraph asked you a different thing: what precisely is the control flow?
The insight to sit with is that neither version is hard; the task is trivial in both. What separates them is not effort but what you must decide in advance and what remains inspectable or recoverable later on. A harness makes the first question disappear and gives you a tidy agent in short order. A framework refuses to let it disappear and compensates you with a flow you can trace one transition at a time when the system misbehaves at an inconvenient hour. That trade grows more consequential as the system grows. When you are building a coding assistant or an agent tucked inside an application, the capability question is the one that matters and the harness is a relief. When you are building a workflow that has to run for a long time, endure crashes, and stand up to audits, the control-flow question is the one that matters and every extra line of the explicit graph pays for itself.
Fitting the tool to the loop you must own
The heuristics follow directly from all of the above. Choose a harness when a working agent fast is the priority, when the runtime's built-in choices already suit you, when the target is a coding or developer-tooling agent, or when you are placing an agent inside a TypeScript application and want type safety and proximity to your frontend. Choose LangGraph when losing progress is unacceptable, which covers long-lived or cyclic workflows that have to outlast crashes, need exact command over state and each transition, or must pause for a person. Choose CrewAI when the problem really does decompose into a set of specialist roles and you want a multi-agent pipeline stood up quickly. Choose LangChain when the breadth of available integrations for a conventional single-agent, tool-calling flow outweighs every other concern, particularly within an established Python codebase. And, once more with feeling, the boundaries are soft: decide on paradigm and constraints, not on which language is printed on the tin.
The loop was the question all along
The debate was never really Node versus Python or one package versus its rival. It was whether a runtime should own the agent loop or you should. A harness trades away the freedom to rewrite the flow in exchange for reaching a working agent fast and reshaping behavior through configuration, events, and hooks, all applied to a loop you never authored. A framework trades speed for explicit command, durability, and orchestration, and charges you the ongoing cost of writing and maintaining the flow. For most teams a sensible starting posture is a harness for prototypes, coding agents, and anything embedded in a product, where velocity is the payoff, moving to LangGraph once durable state becomes a firm requirement, and turning to CrewAI when the work genuinely is a team of specialists. Because these tools keep absorbing each other's best ideas, the skill that endures is not fluency in one API but the judgment to look at the agent in front of you and know who ought to own its loop.