DEV Community

Devanshu Biswas
Devanshu Biswas

Posted on

Your Agent Framework Is a Control-Flow Choice, and Three of the Four Options Are Not Frameworks

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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)