Introduction to agents: what makes a system agentic

A colleague asked me a few months ago, “Listen, we have so many AI tools running, from cybersecurity to customer support. Are any of those tools agentic?”. I have been working with AI tools since 2018, and mind you, I had to stop right there, google a bit, and think as hard as I could just to answer that “it depends”. I simply didn’t know, and possibly don’t know, where the line is.

This chapter is less about my attempt to educate others, but rather to start a discussion about what makes a system agentic and whether we can define it cleanly, as we can define at least that a system is “distributed”.

Introduction

The agent was called “cross-service-reviewer,” and for several months nobody had much to say about it. Its job was the one some busy developers hate: every night the scanners ran across about 400 repositories and orchestrated an intricate set of static and AI tools to ensure our codebase is safe and/or has a good trajectory to become safe in the near future.

The toolbox is banal: Software Composition Analysis (SCA), Static Application Security Testing, a couple of Kubernetes-focused tools to reveal the configuration, resources, and connections between services, and a couple of sub-agents that check code bugs. As you may expect, a system like that may produce a ridiculous amount of noise, so the “cross-service-reviewer” made sure the bugs are real. SCA findings were reported as is, but taken into account by other agents. In the end, the system notifies owners of the repository and urges them to take action on the findings as soon as possible.

The “cross-service-reviewer” was a monstrosity: to do what it was supposed to do, it had read access to repositories, read access to the Kubernetes API (almost unrestricted), an API to create, update, and suppress previous findings, communicate via Slack, and the ability to call effectively any tool it deems it needs to prove or disprove findings. In its early version, it had been looking at the code from different angles, effectively simulating a source-sanitizer-sink model. The permissions of the agent were also largely unrestricted, so it could edit its own configuration when needed to give it more control over the scanning and verification process.

Of course, the system worked well until it stopped working. Two things, two problems of different kinds happened, and one of them is one of the key premises of the book: that one of the traits of agentic systems is that they are, in practice, distributed, or, to be precise, have the same pitfalls, dangers, and disadvantages as distributed systems once deployed for real use.

The scan scheduled at 02:00 against commits on main. My reasoning was as follows: nobody works that late. The system can take its time to tally up the day’s improvements without overloading the system or misreporting. Some teams, however automated security updates that come from Dependabot, and if all checks are green, a GitHub action merges them. Sometimes it takes time to run tests and builds, so the timing is inherently undeterministic. A repository scan also takes time, roughly 5 minutes per repository, depending on the codebase and diff size.

Of course, this night the stars aligned and the pull request got merged right when the “code-quality” agent was reviewing the codebase. Several bugs were introduced by third-party packages, and the “code-quality” agent justly left them be in its output, keeping the references to the CVEs they come from. The SCA was subsequently run by the “cross-service-reviewer” and found no such references in the code, as it pulled another commit, where Dependabot addressed the issues. Referencing nothing, the “cross-service-reviewer” simply decided to report nothing at all. That means that the security dashboard was left unchanged for many days until the line chart on the dashboard became suspiciously flat.

The agent didn’t have “to report nothing at all”: it had enough capabilities to compare commits or, for example, re-run the scan again, but it “decided” that doing nothing in this failure mode is the best way forward. Perhaps, it could even figure out how to communicate with engineers via Slack, but it didn’t. It recorded its last commit as a completed milestone and has been broken since then.

In a completely different context, for example, if we build a distributed system to handle transactions, this reads like struggles of a university first grader: everybody knows how to handle guarantees, idempotency, event sourcing, or locking. But here, I made a completely avoidable mistake simply by not thinking of the whole thing as distributed and not taking into account the fact that the system’s output, at several steps, was probabilistic.

To untangle the issue, an engineer had to look at what was going on. The logs the system produced were seemingly legit: with the combination of factors it was given, the old bugs never disappeared, and the new ones never added. Some of the bugs actually fixed were still reported as present, with a hallucinated but remotely believable justification why so. All steps were green, nothing failed, no error messages; the agent, deep inside its internal machinery, produced the outcome. However, a qualified engineer could change the “last scanned” commit record several weeks back, replay the processing, and everything worked. There’s a good chance the system would work without editing the commit property: the agent could decide differently this time, report fixed and newly introduced issues, or run the tools in a different order.

Is this system agentic?

Besides the obvious bugs we discussed, the system didn’t have enough guardrails or a failure mode that could force reconciliation. In essence, it was a long-running (many months) ChatGPT query that, at the end of the day, needed a human response (the commit editing) in order to make it work again. We are generally ok with the fact that a bash script cannot improvise and halts or misbehaves at any data input that wasn’t foreseen by its creators.

Unlike with a bash script, a developer may rely on the ability of the LLM to take on the unknown, unexpected, or malformed input, map or restructure it, and execute the pipeline regardless. Less so when the system needs to make a judgment between two equally plausible inputs or states of its subsystems. In this specific example, the system didn’t improvise a new remediation strategy, despite visually, to a naked human eye, the output, especially plotted on the chart, was immediately recognized as faulty.

Let’s consider what defining properties we (possibly) have.

Property 1: It uses a language model

The “language model” part here describes our (engineers’) current take on “thinking”, a non-deterministic way to make a decision, as opposed to e.g. a predefined chain of if/else conditions. It doesn’t necessarily need to be a matrix with weights; this is, however, our convenient way to reduce our language and knowledge into something portable.

There are agentic systems that use LLMs; however, when the term was coined (in 1995), there were no large language models, and the agency was defined by a combination of perception and action. In the modern sense, this property, however, is akin to saying that “use of computers” is a defining property of agentic systems, which is undeniably true, but cuts the wrong slice, as we try to tell apart agentic distributed systems from non-agentic ones.

We have an endpoint that uses an LLM to suggest a category of a menu item based on the item’s name. Hardly agentic, isn’t it?

Property 2: It uses tools / a toolbox

Alternatively, it interacts with the environment and has a way of perceiving the environment (in the form of data).

Way better property in my opinion, but still in the wrong way where it matters. We have an endpoint that transcribes a call in a foreign language for the customer support team. This endpoint consumes a WAV file and pipes it to a transcriber and translator. Both steps are predefined. The LLM on top of them doesn’t have any other choice. It can interpret or alter the result, but has little room to call those two tools in any other possible order, as otherwise it doesn’t make any sense (LLM completion tools normally don’t handle such binary files).

Property 3: It runs in a loop

Or, more scientifically, it has a closed observe-reason-act loop, e.g., it can return to step 0 as many times as it wants, perhaps with a slightly different state. The completion of a full pipeline doesn’t stop the tool from running, as the 2nd property may lead to different conditions and, therefore, outcomes.

Another good call, and on the surface it looks very believable: an infinite loop appears to the untrained human eye as some kind of a continuous thinking process, like the model actively makes itself busy. The example with the security orchestration above, however, clearly shows that infinite attempts aren’t always good when the model doesn’t have a way to exit a faulty state.

One may argue that the production of a faulty state doesn’t strip the system of its agentic status. Moreover, a faulty system is still a system (like a broken clock).

Property 4: It is autonomous and dynamic

This property effectively leads to the ability of the system to pursue an open-ended goal that is defined in the form of a natural language statement (“check Google stock, buy when it is low”). The system will then have to execute an action decomposition, such as:

  1. Find the ID of the “Google stock” using an MCP integration
  2. Periodically fetch the price of the stock
  3. Define “low” based on the historical data
  4. Periodically execute the “BUY” command using another tool
  5. Autonomously control the balance of the system, so we don’t overspend

As demonstrated, the autonomy here is a glue between the LLM’s ability to reason and the richness of tools exposed to the system. The LLM itself, while probabilistic, without dynamic use of tools still generally ends up in the same spot most of the time. Without the environment interaction (see the 2nd property), the system stops being dynamic, and the loop becomes meaningless.

Property 5: It handles unexpected input

Alternatively, we can define the property as adaptability.

It could be that one of the possible missing ingredients of the security agent was this property. While the code layer was faulty, the agent could handle the bug at the agentic layer if it had a way to handle novelty. This property can be achieved at the LLM level (e.g., a prompt that defines a safe default if something goes wrong), or at the code level (e.g., the system loops in a human, calls a tool that normalizes the data, etc).

Control flow

We tried our best to describe, perhaps ad hoc, what the term “agentic system” generally means. Let’s put the properties of agentic systems aside for a moment. While working with agentic systems, I occasionally answer a question that can be formulated like that: “Who owns the control flow?”, which, in other words, boils down to the source of the next task or process.

Getting back to the security agent I presented, one of the things I didn’t like was that the reason the system ran was a cron scheduler. Occasionally, at predefined time intervals, the scheduler initializes the supervisor agent (commonly called “synthesizer”), which is responsible for the entire pipeline. Right after that, the system is quite agile in what it executes, but I still felt that something was wrong.

Besides feelings, this setup comes with apparent issues. A static, predefined schedule is wasteful and redundant; it doesn’t take into account vacation periods and the different development cadence of different projects (e.g., a legacy system and a greenfield service). Unless specifically defined (with a greater engineering effort), it treats repositories as relatively equal and scans them with a certain level of redundancy. An agentic system, independently of the development efforts, could determine that only specific types of pull requests justify a full re-scan or that SCA usage is justified only when specific project files change.

Of course, a developer with a lot of free time could make all those decisions conditional, check a lock file, and consume a vacation sheet. The beauty of a well-designed agentic system is that it can model this exact control flow autonomously, or, at least, allow a human to interrupt a normal execution with an additional input.

Effectively, we reach several properties of agentic systems we established at once. If we look at a system from the control flow perspective, we may conclude that such a system comes in several flavours.

Workflow: code owns the control flow

This is exactly the classic automated system approach we define, but don’t discuss in this chapter. A workflow can be a part of an agentic system on a worker agent level or as a part of its toolbox, but it clearly fails to fulfill several properties at once. The absolute majority of engineers have no issues telling apart a workflow and an agentic system.

Some examples:

  • Scripts, cron jobs, state machines, traditional RPAs (robotic process automations)
  • Hardcoded (including ones that have complex sets of gates) workflows and rule engines
  • Services, APIs, other deterministic pipelines
  • Older chatbots that use word closeness, full-text search, classifiers, predictors, or Eliza-like tricks

Assistant: user owns the control flow

Here it gets more difficult, as some assistants are genuinely sophisticated and can check lots of the “agentic system” checkboxes. We, however, may reason that the only starting point for their execution is the will of their user, as assistants are fundamentally reactive and conversational.

The absolute majority of the tools I work with (and, honestly, observe in the wild) are assistants. They may have additional instructions (falsely presented as goals) and a rich set of tools, but they do not run independently from the user. The system doesn’t know in advance what goal the user will set and doesn’t pre-plan, as we cannot foresee all possible interaction scenarios.

Given the difficulty of telling them apart, let’s outline key points, two strong and two weak:

  • The “user” is not necessarily a human being. It is a rather abstract concept of an unknown actor that initializes the execution of the pipelines and text generation by the model. An API call, a fired cron job, or a chat message all act in the “user” capacity.
  • Regardless of the complexity of the pipeline, if the pipeline cannot start execution other than from outside stimuli, it tends to be an assistant rather than an agent. All that complexity idles, unless asked.
  • An assistant normally is conversation-scoped, unless it produces a side effect. We must be careful in our assessment of the side effects, though. One user may ask the assistant to generate a revenue report, and another user, who asks the same, may receive the same file without any tool execution or processing. Ultimately, the original execution has still been triggered by an outside force.
  • Assistants tend to demonstrate a low level of autonomy. A chatbot usually has a heavy set of guardrails and may ask the user about the next step on every turn.

The last two points don’t define assistants in theory; they are rather observations, common occurrences, that can mislead engineers.

Agent: model owns the control flow

Or, in simpler terms, the agent itself defines when it starts. Due to the computing limitations, of course, it cannot come from the model itself, and it’s an important caveat. The system may use APIs, cron jobs, or other, more sophisticated software (e.g., durable execution systems) in order to initialize the execution. However, the record that the said execution must be done is created because the model output said so.

Is Claude Code an agent?

This is where it gets controversial. Anthropic claims that Claude Code is an agent, and a lot of people in the industry agree with Anthropic. In a more classical sense (e.g., if we follow the definition given by Russel and Norvig in their book “Artificial Intelligence: A Modern Approach”), it is indeed an agent, as it can perceive the environment through sensors and then act on whatever it found there. This definition doesn’t require that agents have long-term goals, show proactivity, or own their control flow, a simple combination of sensors and actions is enough.

Moreover, a reasonable assumption here (and a useful real-world practice) is to allow that such a system may decide not to schedule its next execution at all and simply exit the loop. This, on the surface, contradicts the 3rd property of an agentic system, but we will discuss the validity of this approach and its merits further.

Another observation we may notice in real-life agentic systems is that an agent very often outputs a state that influences the agent’s environment on the next turn. Consequently, an agent repeats, refines, adapts, and improves its state on each invocation, and the state changes may lead to a different sequence of execution and scheduling, tool usage, and reasoning behind looping in a user (interruption). As we previously identified, some assistants may also produce a side effect that affects the next, seemingly independent conversation, but this is a significantly rarer occasion.

This test raises the bar for what data qualify as “the output that changes the model’s world”. Unlike an assistant implementation that commonly is satisfied with a chat history, the agent inter-execution state must demonstrate a targeted, goal-driven evolution of the data. Such data may follow a strict schema, or evolve into a rich graph of connected entities. A common approach is to use event sourcing with occasional compaction.

In my personal experience, the only “no-no” on the production scale is to pass the state in the form of a constantly evolving “summary” text. Over time, the summary becomes either noisy, full of edge cases and anecdotes, or loses critical data. Besides an obvious “summary”, this principle is fair for the event sourcing compaction: the compaction should deduplicate the faulty pipelines, generalize the inputs that led to outcomes, but be mindful about keeping everything or dropping chunks of generally expected happy paths.

An agentic system must make a distinction between the “state that changes the world” and the artifacts it produces. In some agentic systems, artifacts are accessible (like, in our security agent, all previous scans can be fetched if needed). Unlike the state, accessing the artifacts is the decision of the model (with several reasonable exceptions, e.g., an agent needs to know upfront the results of the previous execution). State is a constant of the system that comes together with its system prompt.