Pinned Post

Who picks the tool

Bild
The previous post ended with an agent that writes a missing tool into a running node. Generate, sanitize, persist, compile, load, execute, about 25 milliseconds after the model answers. I was happy with that for roughly a week. Then I looked at the demo page again and counted the text fields. Tool name. What it should do. Parameters as JSON. I had filled in all three. The gap-finding was real. The deciding was mine. MetaPlannerAgent.run("Publish the article", [ %{tool: "classic_plan", params: %{goal: "Write draft, review, publish"}}, %{tool: "slugify_text", description: "Turns a title into a URL slug", params: %{text: "Hello World"}} ]) Everything interesting in that call sits in the second argument, and I typed it. The agent resolved slugify_text , noticed it wasn't in the catalog and had it written. It never asked whether the task needed a slug at all. So this post is about the agent in front of that, the on...

Classic Agent, AI Agent: the same job, two very different brains

For about two years now, everything is an "agent." Any app that makes more than one API call in a row sells itself as agentic, and in the same breath it sounds as if humanity had only just invented the concept. Both are off. Agents are ancient, and "agent" doesn't automatically mean "AI." I didn't want to assert the difference, I wanted to see it. So I built a small Elixir app in which two agents solve exactly the same task: break a goal down into a to-do list. One follows fixed rules, the other asks a language model. Both are the same kind of agent, just with a different brain. That single spot is where the whole difference sits.

Agents have been around far longer than "AI" in today's sense

Before we get to the contrast, a quick look back, because it puts the whole debate in perspective. In the textbook that every AI course has used since the nineties, Russell and Norvig define an agent about as plainly as it gets: something that perceives its environment through sensors and acts on it through actuators. Perceive, decide, act, in a loop. There's nothing more to the term. Not a word about neural networks.

And the examples are correspondingly old. Shakey, the robot that SRI rolled through its hallways between 1966 and 1972, broke a command like "push the block off the platform" into steps on its own, plan a route, drive there, find a ramp, push, and planned it with a method called STRIPS, pure symbolic logic. MYCIN diagnosed blood infections in the seventies with around six hundred if-then rules and occasionally beat human doctors doing it. Rodney Brooks introduced his subsumption architecture in 1986, robots that react to stimuli without any inner model of the world. In the nineties came BDI agents, which act from belief, desire and intention, and Etzioni and Weld christened their internet programs "softbots" in 1994. All agents. None of them had a language model inside.

The point isn't nostalgia. The point is: the loop of perceive, decide and act is the old, stable part. What's new is only what you put in the "decide" box. For decades that was rules, search trees, logic. Recently an LLM can sit there. That's a genuine leap, but it's swapping out one part, not inventing the car.

That's exactly what I wanted to make visible, by filling the box twice and keeping everything around it the same.

The setup: one agent framework, two agents

Both agents run on Jido, an agent framework for Elixir. The framework provides the loop: an agent holds state, receives an instruction, runs an action, the state changes. That's ideal for the comparison, because both agents use the same shell. The call is identical on both sides:

agent = ClassicAgent.new()
{agent, _} = ClassicAgent.cmd(agent, {ClassicPlan, %{goal: goal}})

What differs is only the action behind it. For the classic agent that's plain Elixir rule logic. For the AI agent it's a call to a language model. Nothing else.

Classic Agent Perceive DECIDE Rules Act repeats AI Agent Perceive DECIDE LLM Act repeats =
Same loop, same shell. Only the decision core in the middle is swapped.

The classic agent: rules, and only rules

The rule-based planner does three things. It splits the goal on commas and line breaks, assigns each piece a priority and a category by keyword, and sorts. The priority is a look into two word lists:

defp priority(text) do
  cond do
    Enum.any?(@high, &String.contains?(text, &1)) -> {:high, "keyword for high priority"}
    Enum.any?(@medium, &String.contains?(text, &1)) -> {:medium, "keyword for medium priority"}
    true -> {:low, "no keyword → default low"}
  end
end

For cleanly phrased input this works surprisingly well. Give it

Urgent: pay the invoice, prepare the meeting, reply to the customer email

and it breaks that into three tasks, recognizes "urgent" and pulls the invoice up, sorts "finance," "appointments," "communication" apart. No model, no cost, no wait, and the result is a hundred percent traceable. For exactly this kind of input the classic agent isn't just good enough, it's the better choice.

The method hits its limit at the first sentence that doesn't fit the scheme:

I want to get fitter this summer without neglecting work.

No comma, so a single task as far as the agent is concerned. No known keyword, so priority low, category "other." The agent returns a to-do list with exactly one useless entry, and it does so entirely without error. It did precisely what it was told. It just doesn't understand the sentence, because understanding isn't part of its rule set. That's not a bug, that's the design.

The AI agent: the same shell, a model as the core

The AI agent gets the same sentence and hands it to a backend. In the demo that's switchable: a simulated backend without an API key for a pure demo, or a real call to Claude. The real call is unremarkable, an HTTP request that forces a structured JSON response:

body = %{
  model: "claude-opus-4-8",
  system: "Break a free-form goal into concrete subtasks. " <>
          "Understand the intent, even when the text is vague.",
  messages: [%{role: "user", content: goal}],
  output_config: %{format: %{type: "json_schema", schema: @schema}}
}

Req.post("https://api.anthropic.com/v1/messages", json: body, headers: headers)

From the fitness sentence the model produces a real breakdown: define a realistic training goal, block fixed times in the calendar, align work and training times, each with a priority and a short rationale. It matched no keyword anywhere. It read the intent behind the sentence and translated it into actions. Exactly what the rule agent fails at is the normal case here.

But, and this belongs to being honest, the same strength is the weakness. The rule agent is stubborn but predictable. The model is flexible but not deterministic. It can plan differently for the same input, it takes seconds instead of milliseconds, it costs per call, and when it's wrong it's wrong with full conviction and a plausible-sounding rationale. The rule agent fails visibly. The model fails convincingly. That's a difference you have to reckon with when you build.

When is it actually an AI agent?

The clean split above is an idealization. In real systems it's rarely one or the other in pure form, and then comes the question my demo raises right away: if my rule-based agent calls an LLM at one spot, does that already tip it into an AI agent?

The obvious but wrong answer would be "yes, it now contains a model." Containing says nothing. The question is who decides the order of the steps. If fixed logic calls the model at a hard-wired spot as a subroutine, say "which category does this item belong to?", then the model is just a smarter lookup, no different from an OCR tool. The control flow stays rule-based. Structurally that's a classic agent with a smart building block, nothing more.

It becomes an AI agent only when the model sits at the top and takes decision-making power over the sequence of steps: which action next, in what order, whether a loop is needed. That's the sharpest line you can draw, and it has a neat test: take the LLM away. If a decider remains, it was a tool. If no one is left to determine the sequence, then the LLM was the agent.

Two things belong to this line, otherwise it's only half right.

First, the control has to be exercised iteratively against feedback, not declared once. A model that spits out one complete plan, which a fixed loop then dutifully works through, formally determines the order too, but that's a planner with downstream execution, not an agent. The genuinely agentic part is the closed loop: the model decides the next step from the result of the previous one. Exactly the old perceive-decide-act loop from earlier. "Sitting at the top" therefore doesn't mean "set the order once," it means "get to decide anew at every step."

Second, "top of the pyramid" is a scale, not a throne. Almost no one gives the model unrestricted power. It picks from a fixed tool set, inside a state machine, with budget limits and stopping conditions. It decides the sequence, but on rails. That's still an AI agent, and in fact the normal case, not the watered-down exception. Between "model picks from five tools within a frame" and "model writes itself arbitrary code and its own goals" lie worlds of autonomy. Both sit at the top, but they're very different animals. The line cleanly separates the AI agent from the rule-driven pipeline. About the degree of autonomy it says nothing, and that's the more interesting quantity in practice.

classic / hybrid AI agent LLM as a tool picks from tool set steers the sequence sets its own goals called at a fixed spot on rails, within a frame step by step, against feedback writes code, full autonomy more autonomy →
Not a switch but a gradient: the AI agent begins where the model determines the step sequence – from tightly guided to fully autonomous.

The two side by side

When you lay the properties next to each other, it becomes clear that this isn't "old versus new" or "bad versus good," but two tools with opposite strengths:

PropertyClassic AgentAI Agent
Decision corefixed rules, search logiclanguage model
Expected inputstructured, known formatarbitrary free text
Same input, same output?yes, alwaysnot guaranteed
Traceabilityfull, rule by ruleonly a rationale after the fact
Unexpected inputfails or lands in the fallbackinterprets, guesses plausibly
Cost per callpractically zerotokens / API cost
Latencymillisecondsseconds
Typical failure modetoo rigid, misses meaninginvents something plausible
Maintenancehand-tune rulestune prompt and model
Maturityproven for decadesyoung, fast-moving

You read the table best column by column as a character trait, not row by row as a contest. The classic agent is the reliable clerk: expects the right form, processes it without error, never thinks itself smarter than it is. The AI agent is the resourceful intern: understands even crudely phrased requests, thinks along, and occasionally tells you something false in a firm voice.

What I take away from it

The agent isn't the new part. The perceive-decide-act loop is decades old and just as visible in Jido as in Shakey. Anyone who says "agent" today and means "AI" mistakes the housing for the one part that got swapped. That doesn't make it smaller, but it sets it straight.

The break runs at exactly one spot. In my demo the two agents differ in a single file: the action. Everything else around it, the agent, the framework, the interface, is identical. That's the practical lesson from the history: you don't have to rethink things agentically to use AI. You swap the decision core.

Both have their place. For structured input with clear rules the classic way is faster, cheaper and auditable, and replacing it with a model would be showing off. As soon as the input is unstructured and ambiguous it tips over, and the fixed rules become a dead end. The interesting systems are rarely pure either-or; they let the rule part do what it's good at and call the model only where understanding is genuinely needed.

The code is deliberately kept small, so you can run both agents side by side and watch the difference on the same goal directly. The whole project is open on GitLab: gitlab.com/public_elixir/agent_demo. If some spot interests you more, how Jido attaches the action to the agent, why the real model call uses a forced JSON schema, or where the line between the simulated and the real backend runs, write it in the comments. I'm happy to go deeper.

Kommentare

Beliebte Posts aus diesem Blog

Splitting an ML model and a web app across two BEAM nodes — the technical blueprint

Jido in Practice: Agents in Elixir as Composable Actions

A Foundation Model in the BEAM: On-Chain Anomalies with Google TimesFM in Elixir