Level 3 of nine in Project Arc Rector — an agentic RAG stack built entirely from free, self-hostable parts, one swappable level at a time. Level 2 was the model. This one is the layer that decides what happens next.
The page, with the loop and the state machine both running in your browser: https://dev48.infy.uk/arcrector/level3-frameworks.html
Repo: https://github.com/dev48v/arc-rector
The question this layer actually answers
Not "which framework". The framework is downstream of a decision you have already made whether or not you noticed it:
Who decides what happens next — you, or the model?
There are only a few answers, and most systems that call themselves agents are using the first one.
1. A fixed pipeline. Retrieve, rerank, generate. The control flow is in your code; the model fills in the blanks. No framework required, and the honest name for it is "a function".
2. A loop with tools. The model picks a tool, you run it, you feed the result back, repeat until it stops. ReAct. This is where "agent" starts meaning something, and it is about forty lines:
while steps < max_steps:
reply = llm(messages)
call = parse_tool_call(reply)
if call is None:
return reply # the model decided it was done
result = TOOLS[call.name](**call.args)
messages.append(tool_message(result))
steps += 1
3. A graph. Nodes are steps, edges are transitions, some edges are conditional on state. LangGraph is this. The model still decides some transitions; you decide which ones it is allowed to decide.
4. A crew of roles. Several agents with personas, a supervisor routing between them. The most demoed and the least load-bearing — the personas are decoration over a graph you have not drawn.
The line that decides it
Every step you hand to the model buys flexibility and costs a call, a failure mode, and a piece of your ability to reason about what the system will do.
The useful question is per-decision, not per-system:
Is this decision hard to enumerate? Give it to the model. Is it enumerable? Put it in an edge.
"Which of these four tools fits this question" is enumerable — that is a router, and a router is a match statement or a small classifier, not an agent. "What sub-question do I need to answer first" is not enumerable. That one is worth a model call.
Arc Rector's default is LangGraph, Apache-2.0, and the reason is not features — it is that the graph is inspectable. You can print it. A framework whose control flow you cannot draw is a framework whose failure modes you will discover in production.
The cost that only shows up in the trace
A ReAct loop looks cheap in the diagram and is not, because the context grows every iteration:
step 1: system + question 1,100 tokens
step 2: + tool call + result 2,400
step 3: + tool call + result 4,050
step 4: + tool call + result 6,200
Four steps, ~13,750 tokens read — not 4 × 1,100. The loop re-reads its whole history every time, so cost is quadratic in steps, and this is precisely where a CPU-only deployment falls over. On the box every number in this repo came from, one generation was measured at 45–60 seconds; a five-step loop is five minutes.
Two mitigations, both boring and both effective: cap the steps (and treat hitting the cap as a reportable outcome, not a silent truncation), and summarise old tool results rather than carrying them verbatim.
Failure modes worth building for
- The loop that never terminates. The model keeps calling tools. A step cap is not optional, and neither is saying so in the output when it fires.
- The tool that fails. Feed the error back as a tool message. Most loops crash instead, which throws away the one thing the model could act on.
- The plan that ignores the result. The model calls a tool, gets a result, and produces an answer unrelated to it. This is the agent version of an unfaithful chain of thought, and it is the reason Level 1's tracing exists.
- Silent partial success. Three tools succeed, one fails, and the answer is written as though all four worked.
What the state actually needs to be
The thing most home-grown loops get wrong is holding state as a growing list of messages. Messages are a transport. State is:
@dataclass
class AgentState:
question: str
plan: list[str]
evidence: list[Chunk] # what has actually been retrieved
attempts: int
errors: list[str] # kept, not swallowed
Separating them is what lets you resume, cap, inspect and test the loop without a model in the room — and it is the single change that makes an agent debuggable.
Level 3 in one sentence
Pick the smallest control-flow primitive that expresses your problem, hand the model only the decisions you cannot enumerate, and make the loop's stopping conditions as explicit as its steps.
Level 4 is vector databases, where the question stops being "who decides" and starts being "what does similar mean".
The whole stack, nine levels, all free to self-host: https://dev48.infy.uk/arcrector.php
Top comments (0)