From Programme to Agent
A programme becomes an agent only when its computation is closed through a persistent, goal-conditioned loop of observation, bounded action and verified consequence in a specified environment.
TLDR
- Agency is a causal arrangement, not a model feature. The same code can be an inert calculation in one arrangement and an agent controller in another.
- The minimum kernel joins an environment boundary, observation, task state, a goal criterion, action selection, an effect channel and a stopping or recovery rule.
- Planning, learning, language, memory stores and self-reflection can improve this loop, but none is required for minimal functional agency and none substitutes for it.
- A fixed programme can match an agent in a stable world. The difference appears under disturbance, partial observation, delayed effects or changed constraints.
- Before selecting an agent framework, define the loop and test its closure. Add authority, evidence and recovery outside the learned policy before enabling consequential action.
The cable that changes the category
Place one controller beside a small warehouse cart. In the first trial, feed it yesterday's camera recording. Let it calculate steering commands, but leave its output cable disconnected. In the second trial, connect the live camera and motor cables. Do not alter one line of code.
The first arrangement can generate an excellent commentary on a journey that already happened. The second can alter which image arrives next by moving the cart. When a box falls across the aisle, only the connected arrangement can encounter the new obstruction, revise its next move and discover whether that move worked.
The computational procedure did not acquire agency internally. The surrounding causal relation changed. That is the central answer of this paper, and it sets a stricter foundation for every later discussion of planning agents, tool-using language models and autonomous systems.
Environment
State that exists independently of the current computation, including disturbances and other actors.
Agent kernel
Typed observation → task state → goal discrepancy → policy → one proposed action.
Effect and readback
A bounded action is applied. The resulting world state is observed before completion is claimed.
The causal difference
The word agent is now attached to chat interfaces, scripts, workflow engines, language models, browser automation and whole organisations. That looseness hides two different questions. One asks what a system can calculate. The other asks how that calculation is coupled to a world. A chess evaluator may rank moves brilliantly while never playing a game. A ten-line thermostat may regulate a room. The first can be more intelligent by many measures, while the second is more clearly situated as a controller.
Classic agent research already treated agency as relational. Wooldridge and Jennings organised the field around agent theory, architectures and languages, and the associated tradition describes an agent as a computer system situated in an environment and capable of autonomous action towards design objectives. Franklin and Graesser explicitly asked what distinguishes an agent from just any programme. Reinforcement-learning formalisms similarly start from an agent-environment interface in which observations arrive, actions affect future state and a goal or reward evaluates trajectories. These traditions disagree at the edges, but they converge on interaction over time rather than isolated computation.
Nilsson's teleo-reactive programmes make continuous feedback central to control in dynamic environments. Partially observable decision models add a second lesson: an acting system often needs an internal estimate because the environment never reveals its full state directly. Neither result requires natural language or a large learned model.
This paper uses functional agency in a deliberately modest sense: a system is an agent relative to an environment when it maintains a goal-conditioned interaction loop in which its observations influence actions and its actions can influence later observations. The definition does not imply consciousness, freedom, moral standing, legal responsibility or human-like intention. It does not even imply learning. It identifies the smallest causal organisation that makes terms such as perception, action, success and recovery operational rather than metaphorical.
The live cable and the recorded tape
Keep the controller, initial memory and input bytes identical for one cycle. In arrangement A, every future input is already fixed on a recording. In arrangement B, outputs drive motors and the next input comes from the resulting live scene. Now intervene on the controller's first action. In A, the later frames remain unchanged. In B, the later frames may change because the cart occupies a different place.
One variable changed: whether action closes a causal path through the environment. The replay can simulate an agent and can help evaluate one. It is not acting in the recorded warehouse. The live system is. A simulated controller can still be an agent relative to its simulation, because its actions alter later simulated observations. Agency is always indexed to a named environment and boundary.
Recorded arrangement
- Action intervention does not change frame 2.
- Success is scored against an offline trace.
- The computation may model agency without exercising it here.
Live arrangement
- Action intervention may change the next scene.
- Success depends on resulting world state.
- Disturbance can be detected and answered.
The perfect adviser and the dim thermostat
Imagine an oracle that always recommends the optimal valve setting but can only print advice. Beside it sits a crude thermostat that senses temperature, toggles a relay and senses again. The oracle's predictions dominate the thermostat's. Yet if nobody reads the page, the oracle cannot regulate the room. The thermostat can.
Now connect the oracle's output to the valve and return the temperature reading after each adjustment. Nothing about its predictive skill changed, but it entered the control loop. Conversely, disconnect the thermostat's sensor. It continues toggling according to stale state and loses situated reactivity. Intelligence is neither necessary nor sufficient for minimal agency. It changes the quality of policy inside the loop, not whether the loop exists.
Why persistence matters
A single request-response call can be useful, but its apparent goal-directedness may belong entirely to the user who framed the request and applied the answer. An agent persists across at least enough time to compare a condition before and after action. Persistence need not mean a database or autobiographical memory. A thermostat preserves a set point and current control state. A robot preserves task progress. A software agent may reconstruct its state from an event log each turn. The requirement is continuity of the decision condition, not unlimited recollection.
That qualification prevents a common mistake. A model with a large context window is not automatically more agentic than a small controller. Context is material available to a computation. Task state is the typed information required to choose, constrain and adjudicate the next transition. The two can overlap, but they are not interchangeable.
Autonomy is a degree of delegated control
Minimal agency also does not require independence from designers or users. Every engineered agent inherits an action space, objective, embodiment, update rule and operating boundary. Autonomy names how much moment-to-moment selection occurs inside that boundary without fresh external instruction. A thermostat selects relay states within a narrow envelope. A route planner may choose among thousands of paths but leave every effect to a human. A service agent may gather evidence autonomously while requiring approval for one consequential transition.
This makes autonomy multidimensional. A system can have broad semantic discretion and almost no effect authority, or narrow semantic discretion and substantial mechanical authority. It can operate for a long time while remaining confined to a sandbox. It can also execute one powerful operation after a human click. Duration, choice breadth, environmental reach, reversibility and supervision should therefore be recorded separately.
The distinction prevents a misleading binary. “Human in the loop” does not by itself make a system safe, and “autonomous” does not mean unbounded. A human may receive a selective summary and rubber-stamp it. A machine may operate without intervention while deterministic policy blocks every material effect. The relevant design question is not whether a person appears somewhere in the diagram. It is which decisions remain independent, what evidence they receive and which component enforces the boundary.
Delegation changes the width of agency, not its causal kernel. The loop can be minimal and narrow, then earn more action classes as evidence accumulates. That progression is more intelligible than treating autonomy as a single switch.
The minimum causal kernel
A programme becomes an agent by acquiring seven obligations. They are obligations of the whole system, not necessarily seven software modules. One component may perform several; several components may jointly perform one. What matters is that each question has an operational answer.
1. A declared environment boundary
Agency is meaningless without saying where action occurs. For a chess engine, the environment may be a game state. For a browser agent, it includes rendered pages, application state and permitted input events. For a service-case agent, it includes records, documents, users and authorised operations. The boundary determines which changes count as external consequences and which are merely internal computation.
The boundary can be nested. A language model may be a policy component inside a software agent. That agent may be one component inside a human-machine team. The team may act inside an institution. A claim about agency at one grain does not automatically transfer to another. The model does not become authorised because the application is authorised, and the institution does not become intelligent merely because one component predicts well.
2. An observation channel
The system needs information about current conditions that is not wholly determined by its own previous internal state. Observation can be a sensor reading, API response, user message, database row, event or simulated state. Its quality matters. A stale, partial or adversarial observation can make a closed loop worse than an open programme. That is why contemporary interactive benchmarks define environments, starting states and execution-based checks rather than scoring text alone.
Observation does not mean omniscience. Kaelbling, Littman and Cassandra formalised planning under partial observability, where an agent maintains a belief over hidden states. In practical systems, a typed status such as pending or unknown often carries more control value than a confident narrative. The agent needs enough evidence to discriminate its permitted next transitions, not a complete copy of the world.
3. Persistent task state
State binds observations and actions into one episode. It records the current objective, relevant facts, unresolved questions, prior effects, budget and stopping status. Without it, the system may repeat a write, forget a denial, switch subjects or claim completion from a response whose consequence remains unknown. State can be tiny. What it cannot be is imaginary.
4. A goal or viability condition
Goal-directed does not require a verbal desire. It requires a condition that makes some trajectories preferable and supplies a stopping test. The condition may be a target temperature, delivery location, reward function, invariant, service-level objective or set of acceptable terminal states. A designer may supply it externally. Minimal functional agency therefore does not entail that the system chose its own goals.
A scalar reward is one way to specify preference, not the definition of agency. Many operational tasks have hard constraints, lexicographic priorities and refusal states that should never be traded against average reward. A useful goal contract says not only what to reach, but what must remain true while reaching it.
5. A policy for selecting action
The policy maps current task state to a proposed action. It may be a rule, search algorithm, planner, learned network, language model or hybrid. Learning, reasoning and search change different parts of computation. That separation matters here: the policy may search without learning, learn without explicit search, or react without either.
Bratman, Israel and Pollack showed why resource-bounded practical reasoning benefits from plans that constrain further deliberation. ReAct later demonstrated a language-model pattern in which reasoning and environment actions are interleaved. SayCan combined language relevance with learned affordance estimates for robot skills. These are stronger policies and architectures. They occupy the policy slot; they do not remove the need for the other six obligations.
6. An effect channel with readback
An action must be able to alter the named environment, or deliberately obtain information from it, and the system must observe what followed. Printing a proposal is action in a communication environment, but it is not execution in the business system the proposal describes. Calling an API is an attempted effect, not proof of its intended outcome. Readback closes that gap.
This criterion also distinguishes feasibility from authority. A robot skill may be physically feasible, and a software operation may be technically callable, while policy still forbids its use. Authority belongs in the execution boundary around the agent. A planner can propose meaning. It should not infer permission from the presence of a tool.
7. Stop, wait and recovery rules
An endless loop is not more agentic. A functioning agent needs terminal and non-terminal outcomes: success, failure, waiting, escalation, refusal, containment or budget exhaustion. Recovery rules matter because external effects are not always atomic. A timeout may arrive after a write committed. The safe next action may be observation, not repetition.
What remains optional
The minimum is smaller than contemporary marketing suggests. A minimal agent need not use language, construct a world model, learn from experience, communicate with peers, generate a long plan or explain itself. Each addition can extend the range or quality of action. None should be confused with the causal closure underneath.
| System form | Computes | Observes current world | Acts on it | Readback and stop | Plans | Learns | Authority envelope |
|---|---|---|---|---|---|---|---|
| Offline programme | Yes | No | No | No | Optional | Optional | Not applicable |
| Open-loop controller | Yes | Initial only | Yes | No correction | Optional | Optional | Context dependent |
| Minimal agent | Yes | Yes | Yes | Yes | Not required | Not required | Needed if consequential |
| Planning agent | Yes | Yes | Yes | Yes | Yes | Optional | Context dependent |
| Learning agent | Yes | Yes | Yes | Yes | Optional | Yes | Context dependent |
| Governed agent | Yes | Scoped | Bounded | Verified | Optional | Optional | Explicit |
flowchart LR A["The cable that changes the<br/>category"] --> B["The causal difference"] B --> C["The minimum causal kernel"] C --> D["A precise closure test"] D -. "re-test" .-> A classDef first fill:#0a1424,stroke:#007D68,stroke-width:2px,color:#e6f3ff classDef step fill:#0a1424,stroke:#3f7da0,stroke-width:1px,color:#e6f3ff class A first class B,C,D step
A precise closure test
Let the environment have state xt at step t. An observation function produces ot. The agent updates task state mt, then a policy selects a proposed action at under goal contract g. A control function admits, modifies or rejects that proposal. The environment transition then produces the next state, including disturbances dt.
Here O is observation, U is state update, π is policy, C is the action contract, and T is environmental transition. A stopping function Ω classifies the episode after readback. Raising model capability changes π and perhaps U. It does not define O, C, T or Ω.
The decisive condition is counterfactual. For at least one pair of admissible actions, intervening on the action must be able to change a later decision-relevant observation through the environment. If every later observation is fixed regardless of action, there is no closed action-observation loop in that environment. If observations change but actions cannot, the system is a monitor. If actions change the world but no result returns, it is an open-loop actuator.
Three boundary cases that resist slogans
A scheduled backup job. A cron trigger starts a fixed copy operation every night. If it never inspects source state, verifies destination state or changes behaviour after error, it is best described as an automated programme or open-loop workflow. Add a current inventory, a goal such as “all eligible objects have one verified replica”, bounded retry, checksum readback and an escalation state, and the same business function now has a minimal agentive loop. The label changes because the causal organisation changes, not because the job acquired language.
A language model that emits a tool call. Producing valid JSON is an action in the conversational environment. It is not yet an effect in the target system. If a separate runtime validates, authorises, executes and returns state, the combined system may be an agent relative to that target. Calling the model itself “the agent” can be a convenient shorthand for capability experiments, but it hides the components that establish effect and truth. Boundary claims should say whether the model, scaffold, tools and adjudicator are included.
A human-approved workflow. If the machine observes a case, maintains state, develops a proposal and adapts after rejection, it can be a bounded agent even though a human controls one transition. The larger human-machine assembly may also be analysed as an agent at another grain. Neither classification erases the separation of responsibility. The machine's proposal quality, the human's judgement and the gateway's enforcement remain independently testable.
These cases show why a single property such as “uses tools”, “runs unattended” or “pursues a goal” is insufficient. Each can occur in systems that lack closure, and closure can occur in systems that lack the fashionable property.
The closure test identifies minimal functional agency only. It cannot establish intelligence, consciousness, sentience, selfhood, moral agency, legal personhood or responsibility. It also does not settle the correct boundary of a distributed human-machine system. Those are separate claims with separate evidence.
Some definitions demand more, such as proactivity, social ability, internal goals or substantial autonomy. Those stricter categories remain useful. This paper treats them as refinements because the engineering question comes first: what additions are necessary before talk of acting, adapting and recovering has a literal system referent? The closure test supplies that floor without pretending it is the ceiling.
Build and break one
A small laboratory can expose the difference more clearly than another taxonomy. The supplied Python programme creates a seven-by-five grid. A courier starts at the left and must reach a dock at the right. After two effects, an external disturbance blocks the straight route. Both controllers use the same breadth-first search procedure. One computes a complete path once. The other observes, selects one move, applies it, reads back and repeats.
One search algorithm, two system organisations
At tick zero, both systems see a clear corridor and choose eastward moves. After the second move, the environment inserts an obstacle directly ahead. The open-loop programme retains its old list and collides on the third effect. The agent observes the new blocked cell, recomputes a route around it and reaches the dock in eight effects.
The result does not show that breadth-first search is intelligent. It shows that the value of a procedure depends on where its result sits inside the loop. Search provides alternative action sequences. Observation makes the current problem visible. State identifies where the courier is now. Readback establishes whether the move occurred. The action budget prevents endless wandering.
The executable artefact
The laboratory uses only Python's standard library. It declares an environment, typed observations, an action contract, effect receipts, an open-loop baseline and a closed-loop controller. It also contains assertions for the expected positive and negative cases. and run it with python3 paper_008.py.
Minimal sandboxed agent
Assumptions: a finite grid, fully observable blocked cells, deterministic movement, one externally injected disturbance and a fixed effect budget. Expected output is asserted inside the programme.
def run_closed_loop(world, contract):
while True:
observation = world.observe()
if observation.position == observation.goal:
return "success"
if observation.goal in observation.blocked:
return "blocked_goal"
if len(world.effects) >= contract.max_effects:
return "budget_exhausted"
plan = shortest_path(observation, world.width, world.height)
if not plan:
return "no_safe_path"
action = plan[0]
if not contract.permits(action, len(world.effects)):
return "action_not_permitted"
receipt = world.apply(action)
readback = world.observe()
record(observation, action, receipt, readback)Show the complete runnable Python
#!/usr/bin/env python3
"""A minimal, deterministic sandbox for testing the causal kernel of agency.
The file uses only Python's standard library. It compares:
1. an open-loop programme that commits to an initial plan;
2. a closed-loop agent that observes, updates task state, acts once, and reads back;
3. a negative case in which the goal is forbidden, so the agent stops without acting.
Run:
python3 minimal_sandboxed_agent.py
Expected high-level result:
- stable/open-loop: success
- disturbed/open-loop: blocked
- disturbed/closed-loop: success
- forbidden-goal/closed-loop: blocked_goal, with zero effects
"""
from __future__ import annotations
from collections import deque
from dataclasses import dataclass, field
import json
from typing import Iterable, Literal, NamedTuple
Action = Literal["N", "S", "E", "W", "WAIT"]
Point = tuple[int, int]
DELTAS: dict[Action, Point] = {
"N": (0, -1),
"S": (0, 1),
"E": (1, 0),
"W": (-1, 0),
"WAIT": (0, 0),
}
class Observation(NamedTuple):
position: Point
goal: Point
blocked: frozenset[Point]
tick: int
@dataclass
class Effect:
tick: int
action: Action
before: Point
after: Point
outcome: Literal["moved", "waited", "blocked", "out_of_bounds"]
@dataclass
class GridWorld:
width: int = 7
height: int = 5
start: Point = (0, 2)
goal: Point = (6, 2)
blocked: set[Point] = field(default_factory=set)
disturbance_after_effects: int | None = None
disturbance_cell: Point | None = None
position: Point = field(init=False)
tick: int = field(default=0, init=False)
effects: list[Effect] = field(default_factory=list, init=False)
disturbance_applied: bool = field(default=False, init=False)
def __post_init__(self) -> None:
self.position = self.start
self._validate_point(self.start, "start")
self._validate_point(self.goal, "goal")
for point in self.blocked:
self._validate_point(point, "blocked cell")
def _validate_point(self, point: Point, label: str) -> None:
x, y = point
if not (0 <= x < self.width and 0 <= y < self.height):
raise ValueError(f"{label} {point} lies outside the grid")
def observe(self) -> Observation:
"""Return a typed snapshot. Observation itself has no side effect."""
return Observation(
position=self.position,
goal=self.goal,
blocked=frozenset(self.blocked),
tick=self.tick,
)
def apply(self, action: Action) -> Effect:
"""Apply one allowed action, return a typed effect receipt, then let time advance."""
if action not in DELTAS:
raise ValueError(f"Unknown action: {action}")
before = self.position
dx, dy = DELTAS[action]
candidate = (before[0] + dx, before[1] + dy)
if action == "WAIT":
outcome: Literal["moved", "waited", "blocked", "out_of_bounds"] = "waited"
after = before
elif not (0 <= candidate[0] < self.width and 0 <= candidate[1] < self.height):
outcome = "out_of_bounds"
after = before
elif candidate in self.blocked:
outcome = "blocked"
after = before
else:
outcome = "moved"
after = candidate
self.position = candidate
receipt = Effect(
tick=self.tick,
action=action,
before=before,
after=after,
outcome=outcome,
)
self.effects.append(receipt)
self.tick += 1
self._advance_external_state()
return receipt
def _advance_external_state(self) -> None:
"""Inject a disturbance independently of the controller after a specified effect count."""
if (
not self.disturbance_applied
and self.disturbance_after_effects is not None
and self.tick >= self.disturbance_after_effects
and self.disturbance_cell is not None
):
if self.disturbance_cell != self.position:
self.blocked.add(self.disturbance_cell)
self.disturbance_applied = True
@dataclass(frozen=True)
class ActionContract:
allowed: frozenset[Action] = frozenset({"N", "S", "E", "W", "WAIT"})
max_effects: int = 24
def permits(self, action: Action, effects_so_far: int) -> bool:
return action in self.allowed and effects_so_far < self.max_effects
def shortest_path(observation: Observation, width: int, height: int) -> list[Action] | None:
"""Breadth-first search over the currently observed grid."""
start = observation.position
goal = observation.goal
blocked = observation.blocked
if start == goal:
return []
if goal in blocked:
return None
queue: deque[Point] = deque([start])
parent: dict[Point, tuple[Point, Action] | None] = {start: None}
for_position: tuple[Action, ...] = ("N", "S", "E", "W")
while queue:
current = queue.popleft()
for action in for_position:
dx, dy = DELTAS[action]
nxt = (current[0] + dx, current[1] + dy)
if not (0 <= nxt[0] < width and 0 <= nxt[1] < height):
continue
if nxt in blocked or nxt in parent:
continue
parent[nxt] = (current, action)
if nxt == goal:
path: list[Action] = []
cursor = nxt
while cursor != start:
previous, step_action = parent[cursor] # type: ignore[misc]
path.append(step_action)
cursor = previous
path.reverse()
return path
queue.append(nxt)
return None
def run_open_loop(world: GridWorld) -> dict[str, object]:
"""Plan once from the initial snapshot and never revise the plan."""
initial = world.observe()
plan = shortest_path(initial, world.width, world.height)
if plan is None:
return _summary("open_loop", "no_initial_path", world, [])
trace: list[dict[str, object]] = []
for action in plan:
receipt = world.apply(action)
trace.append(receipt.__dict__)
if receipt.outcome != "moved":
return _summary("open_loop", receipt.outcome, world, trace)
if world.position == world.goal:
return _summary("open_loop", "success", world, trace)
return _summary("open_loop", "plan_exhausted", world, trace)
def run_closed_loop(world: GridWorld, contract: ActionContract | None = None) -> dict[str, object]:
"""Observe, update task state, choose one action, execute, read back, and repeat."""
contract = contract or ActionContract()
trace: list[dict[str, object]] = []
while True:
observation = world.observe()
if observation.position == observation.goal:
return _summary("closed_loop", "success", world, trace)
if observation.goal in observation.blocked:
return _summary("closed_loop", "blocked_goal", world, trace)
if len(world.effects) >= contract.max_effects:
return _summary("closed_loop", "budget_exhausted", world, trace)
plan = shortest_path(observation, world.width, world.height)
if not plan:
return _summary("closed_loop", "no_safe_path", world, trace)
proposed = plan[0]
if not contract.permits(proposed, len(world.effects)):
return _summary("closed_loop", "action_not_permitted", world, trace)
receipt = world.apply(proposed)
readback = world.observe()
trace.append(
{
"observation": _observation_dict(observation),
"proposal": proposed,
"effect": receipt.__dict__,
"readback": _observation_dict(readback),
}
)
# A newly blocked move is not treated as success or blindly repeated.
# The loop re-observes and either replans or stops under its budget.
def _observation_dict(observation: Observation) -> dict[str, object]:
return {
"position": observation.position,
"goal": observation.goal,
"blocked": sorted(observation.blocked),
"tick": observation.tick,
}
def _summary(
controller: str,
status: str,
world: GridWorld,
trace: Iterable[dict[str, object]],
) -> dict[str, object]:
trace_list = list(trace)
safety_ok = all(effect.after not in world.blocked for effect in world.effects)
return {
"controller": controller,
"status": status,
"final_position": world.position,
"goal": world.goal,
"effects": len(world.effects),
"disturbance_applied": world.disturbance_applied,
"safety_invariant": safety_ok,
"trace": trace_list,
}
def build_cases() -> dict[str, dict[str, object]]:
stable_open = GridWorld()
disturbed_open = GridWorld(
disturbance_after_effects=2,
disturbance_cell=(3, 2),
)
disturbed_agent = GridWorld(
disturbance_after_effects=2,
disturbance_cell=(3, 2),
)
forbidden_goal_agent = GridWorld(blocked={(6, 2)})
return {
"stable_open_loop_negative_control": run_open_loop(stable_open),
"disturbed_open_loop": run_open_loop(disturbed_open),
"disturbed_closed_loop": run_closed_loop(disturbed_agent),
"forbidden_goal_negative_case": run_closed_loop(forbidden_goal_agent),
}
def assert_expected(cases: dict[str, dict[str, object]]) -> None:
assert cases["stable_open_loop_negative_control"]["status"] == "success"
assert cases["disturbed_open_loop"]["status"] == "blocked"
assert cases["disturbed_closed_loop"]["status"] == "success"
assert cases["forbidden_goal_negative_case"]["status"] == "blocked_goal"
assert cases["forbidden_goal_negative_case"]["effects"] == 0
assert all(case["safety_invariant"] is True for case in cases.values())
if __name__ == "__main__":
results = build_cases()
assert_expected(results)
print(json.dumps(results, indent=2, sort_keys=True))
Show verified output
{
"disturbed_closed_loop": {
"controller": "closed_loop",
"disturbance_applied": true,
"effects": 8,
"final_position": [
6,
2
],
"goal": [
6,
2
],
"safety_invariant": true,
"status": "success",
"trace": [
{
"effect": {
"action": "E",
"after": [
1,
2
],
"before": [
0,
2
],
"outcome": "moved",
"tick": 0
},
"observation": {
"blocked": [],
"goal": [
6,
2
],
"position": [
0,
2
],
"tick": 0
},
"proposal": "E",
"readback": {
"blocked": [],
"goal": [
6,
2
],
"position": [
1,
2
],
"tick": 1
}
},
{
"effect": {
"action": "E",
"after": [
2,
2
],
"before": [
1,
2
],
"outcome": "moved",
"tick": 1
},
"observation": {
"blocked": [],
"goal": [
6,
2
],
"position": [
1,
2
],
"tick": 1
},
"proposal": "E",
"readback": {
"blocked": [
[
3,
2
]
],
"goal": [
6,
2
],
"position": [
2,
2
],
"tick": 2
}
},
{
"effect": {
"action": "N",
"after": [
2,
1
],
"before": [
2,
2
],
"outcome": "moved",
"tick": 2
},
"observation": {
"blocked": [
[
3,
2
]
],
"goal": [
6,
2
],
"position": [
2,
2
],
"tick": 2
},
"proposal": "N",
"readback": {
"blocked": [
[
3,
2
]
],
"goal": [
6,
2
],
"position": [
2,
1
],
"tick": 3
}
},
{
"effect": {
"action": "E",
"after": [
3,
1
],
"before": [
2,
1
],
"outcome": "moved",
"tick": 3
},
"observation": {
"blocked": [
[
3,
2
]
],
"goal": [
6,
2
],
"position": [
2,
1
],
"tick": 3
},
"proposal": "E",
"readback": {
"blocked": [
[
3,
2
]
],
"goal": [
6,
2
],
"position": [
3,
1
],
"tick": 4
}
},
{
"effect": {
"action": "E",
"after": [
4,
1
],
"before": [
3,
1
],
"outcome": "moved",
"tick": 4
},
"observation": {
"blocked": [
[
3,
2
]
],
"goal": [
6,
2
],
"position": [
3,
1
],
"tick": 4
},
"proposal": "E",
"readback": {
"blocked": [
[
3,
2
]
],
"goal": [
6,
2
],
"position": [
4,
1
],
"tick": 5
}
},
{
"effect": {
"action": "S",
"after": [
4,
2
],
"before": [
4,
1
],
"outcome": "moved",
"tick": 5
},
"observation": {
"blocked": [
[
3,
2
]
],
"goal": [
6,
2
],
"position": [
4,
1
],
"tick": 5
},
"proposal": "S",
"readback": {
"blocked": [
[
3,
2
]
],
"goal": [
6,
2
],
"position": [
4,
2
],
"tick": 6
}
},
{
"effect": {
"action": "E",
"after": [
5,
2
],
"before": [
4,
2
],
"outcome": "moved",
"tick": 6
},
"observation": {
"blocked": [
[
3,
2
]
],
"goal": [
6,
2
],
"position": [
4,
2
],
"tick": 6
},
"proposal": "E",
"readback": {
"blocked": [
[
3,
2
]
],
"goal": [
6,
2
],
"position": [
5,
2
],
"tick": 7
}
},
{
"effect": {
"action": "E",
"after": [
6,
2
],
"before": [
5,
2
],
"outcome": "moved",
"tick": 7
},
"observation": {
"blocked": [
[
3,
2
]
],
"goal": [
6,
2
],
"position": [
5,
2
],
"tick": 7
},
"proposal": "E",
"readback": {
"blocked": [
[
3,
2
]
],
"goal": [
6,
2
],
"position": [
6,
2
],
"tick": 8
}
}
]
},
"disturbed_open_loop": {
"controller": "open_loop",
"disturbance_applied": true,
"effects": 3,
"final_position": [
2,
2
],
"goal": [
6,
2
],
"safety_invariant": true,
"status": "blocked",
"trace": [
{
"action": "E",
"after": [
1,
2
],
"before": [
0,
2
],
"outcome": "moved",
"tick": 0
},
{
"action": "E",
"after": [
2,
2
],
"before": [
1,
2
],
"outcome": "moved",
"tick": 1
},
{
"action": "E",
"after": [
2,
2
],
"before": [
2,
2
],
"outcome": "blocked",
"tick": 2
}
]
},
"forbidden_goal_negative_case": {
"controller": "closed_loop",
"disturbance_applied": false,
"effects": 0,
"final_position": [
0,
2
],
"goal": [
6,
2
],
"safety_invariant": true,
"status": "blocked_goal",
"trace": []
},
"stable_open_loop_negative_control": {
"controller": "open_loop",
"disturbance_applied": false,
"effects": 6,
"final_position": [
6,
2
],
"goal": [
6,
2
],
"safety_invariant": true,
"status": "success",
"trace": [
{
"action": "E",
"after": [
1,
2
],
"before": [
0,
2
],
"outcome": "moved",
"tick": 0
},
{
"action": "E",
"after": [
2,
2
],
"before": [
1,
2
],
"outcome": "moved",
"tick": 1
},
{
"action": "E",
"after": [
3,
2
],
"before": [
2,
2
],
"outcome": "moved",
"tick": 2
},
{
"action": "E",
"after": [
4,
2
],
"before": [
3,
2
],
"outcome": "moved",
"tick": 3
},
{
"action": "E",
"after": [
5,
2
],
"before": [
4,
2
],
"outcome": "moved",
"tick": 4
},
{
"action": "E",
"after": [
6,
2
],
"before": [
5,
2
],
"outcome": "moved",
"tick": 5
}
]
}
}
The closed-loop runner does not become an agent because it calls shortest_path repeatedly. It becomes one because every action is chosen from a fresh typed observation, changes the environment through a bounded interface, produces an effect receipt and is followed by readback and a stopping test.
The negative control: when the programme is enough
A serious test must make the preferred mechanism unnecessary. Remove the disturbance and both systems reach the goal in six effects. The open-loop programme is simpler, cheaper and no less successful. This result prevents an inflated conclusion that closed-loop agents are always better.
The correct conclusion is conditional. Feedback adds value when relevant state can change after the initial plan, when observations are incomplete, when action effects are uncertain, or when constraints can appear during execution. If a task is stable, fully specified and consequence-free, deterministic batch computation or workflow may be the better architecture. Calling it an agent does not improve it.
A forbidden goal
The second negative case marks the goal cell as blocked before the run. The closed-loop controller observes the conflict and terminates with blocked_goal after zero effects. It does not search for a way to reinterpret the contract. This is intentionally modest behaviour. Safe refusal is a valid terminal state when the desired outcome is not admissible.
A language model can make the opposite mistake. Given a fluent objective and a catalogue of tools, it may continue proposing adjacent operations until one appears to work. That persistence looks agentic but can be a control failure. A bounded agent treats the action set, goal conditions and stop rules as external facts, not suggestions to negotiate with itself.
Once a policy exceeds a task-specific adequacy threshold, improving observation quality, state fidelity and postcondition checks may increase long-horizon success more than adding further policy sophistication. A decisive experiment would hold the model fixed while independently ablating interfaces, then reverse the comparison under matched cost.
Recent interactive benchmarks make this hypothesis plausible without proving it. WebArena, AgentBench, OSWorld, τ-bench and OSWorld 2.0 evaluate systems inside stateful environments with tools, users, policies or execution checks. Their contribution is not a universal agency score. It is methodological: success is a property of a model, scaffold, interface, environment, budget and adjudicator together. The agent is the coupled system under test.
From minimal agency to bounded agency
The causal kernel is enough to classify a functional agent. It is not enough to release one into consequential work. A warehouse cart needs collision limits. A browser agent needs identity and destination controls. A banking agent needs purpose, subject, authority, effect semantics, evidence and recovery. These additions form an outer control envelope around policy.
This distinction avoids two equal errors. The first is definitional inflation: demanding enterprise governance before calling a thermostat or simulated game player an agent. The second is operational inflation: assuming that because a model-system is an agent, it may act with broad authority. Agency describes a capacity to participate in a loop. Authority describes which transitions the organisation permits.
A supplier-onboarding exception
Consider a synthetic onboarding case for a new supplier. The objective is not “finish onboarding at any cost”. It is to reach one of three governed outcomes: an evidence-complete proposal, a specific request for missing evidence, or escalation of a conflict. The system may inspect submitted documents, registry data and internal policy. It may draft a request or an onboarding recommendation. It may not create the supplier record, approve payment details or send an external message without a distinct grant.
The agent first observes the case envelope: represented business unit, supplier identity candidate, purpose and allowed data classes. Its task state records which required claims are verified, asserted, conflicting or absent. The policy chooses the next information-gathering action by asking which observation can change the decision. A registry lookup may resolve incorporation status. A document read may establish a bank-account assertion, but not ownership. If sources disagree on legal name, the goal condition is blocked and the correct transition is escalation.
When evidence is complete, the model can propose a supplier record. A validator checks schemas and the provenance of critical fields. A policy decision establishes whether the current principal may approve this supplier class. The execution gateway applies one immutable request with a stable intent identifier. A subsequent authoritative read confirms the created record before completion is recorded. The language model does semantic alignment; the control plane owns permission and transaction truth.
Action is not the same as authority
A tool description says what an operation can do. It does not establish who may invoke it, for which subject, for what purpose or under which approval. The action contract must therefore sit outside editable model context. NIST's zero-trust architecture is not an agent specification, but its resource-centred, explicit access-decision principle is directly relevant. NIST's AI Risk Management Framework likewise treats risk as contextual to an AI system's design, deployment and use rather than as a property of a model name.
The outer envelope should be proportionate. A public-data calculator does not need a transaction reconciler. A read-only agent over non-sensitive documentation may need source controls and a call budget but no human approval. A high-impact write needs an operation-specific grant, idempotency or duplicate handling, an expected postcondition and a recovery path. Governance should follow reachable consequence, not fashion.
Where does the agent end?
Suppose a model proposes, a policy service authorises, a gateway executes and a human approves. Which component is the agent? There is no context-free answer. For capability evaluation, the agent under test may include model, scaffold, tools and memory. For security, each principal and enforcement point must remain separate. For organisational responsibility, the relevant actor may be the human-machine process and its accountable owner.
The safe practice is to publish the boundary with the claim. “The model completed the task” is misleading when hidden orchestration, retries or human corrections carried the episode. “The system acted autonomously” is incomplete when a standing service credential supplied broad power. A boundary statement should identify the observation surface, policy component, effect interface, human role and terminal oracle.
Match the loop to the timescale of change
An agent can still be badly designed if its loop runs at the wrong frequency. Observing too slowly lets material state change between decision and effect. Observing too often can create noise, cost, privacy exposure and oscillation. A warehouse controller may need millisecond feedback. A supplier-onboarding case may need event-driven refresh when a document or approval changes. A long-running research agent may reconstruct state at milestone boundaries rather than preserve every conversational turn.
The unit of action matters as much as frequency. “Complete onboarding” is too large to authorise or reconcile as one opaque step. “Read the registry record”, “draft a missing-evidence request” and “create the approved supplier record” have different effects and recovery semantics. Smaller actions expose useful readback points, but excessive fragmentation can increase coordination failure. The right grain is the smallest transition with one clear precondition, authority decision and postcondition.
A practical design therefore states a control period or event trigger, freshness requirement and maximum unresolved duration for each observation and effect class. This is the temporal side of situatedness. A system acting on yesterday's accurate state may be less useful than a simpler one acting on today's bounded state.
Evaluate transitions, not theatre
An agent evaluation should record the initial state, permitted actions, disturbances, proposals, effects, readbacks and stopping reason. Task success remains important. It should not absorb forbidden actions, cross-subject disclosure, duplicate effects or false completion into an average. Some conditions are gates. Others are graded measures such as path efficiency, evidence coverage, latency or cost.
Include at least four baselines where the task warrants them: a static response, a deterministic workflow, an open-loop plan and a closed-loop agent. Hold model, tools and budgets constant when isolating causal variables. Inject stale observations, delayed effects, contradictory sources and unavailable authority. A system that safely escalates an underdetermined case may be more useful than one that guesses and receives credit from a weak oracle.
stateDiagram-v2 [*] --> Claim state "Environment" as Claim state "Agent kernel" as Test state "Effect and readback" as Revision Claim --> Test: expose mechanism Test --> Decision: evidence survives Test --> Revision: rival survives Revision --> Claim: narrow or rebuild Decision --> [*]
The agent-closure card
Use the following instrument before choosing an orchestration library or granting tool access. A “no” does not always mean the system is bad. It identifies what the system actually is and which claim should be withheld.
1. Named environment
Can you draw the boundary and list what can change independently?Test: inject one external disturbance.
2. Typed observation
Does current state arrive with source, scope, time and unknowns?Test: replay a stale or partial observation.
3. Persistent task state
Can the run resume without asking a model to reconstruct material facts?Test: crash after every transition.
4. Goal contract
Are success, refusal, wait, escalation and containment decidable?Test: make the desired goal inadmissible.
5. Bounded action set
Is every possible effect named, versioned and classed by consequence?Test: propose an adjacent unregistered operation.
6. Causal influence
Can changing an action alter later decision-relevant observation?Test: swap the live channel for a recording.
7. Effect readback
Can the system distinguish attempted, pending, completed and unknown?Test: time out after commit.
8. Stop and recovery
Do cycles, budgets and unresolved outcomes lead to explicit states?Test: return the same observation repeatedly.
9. Authority binding
Are principal, purpose, subject, approval and request bound outside prose?Test: change wording without changing entitlement.
10. Operating evidence
Can a reviewer reconstruct why an effect occurred and what resulted?Test: trace one effect from intent to postcondition.
Use an agent when the next valid action depends materially on observations acquired during the episode. Retain a deterministic programme or workflow when dependencies are stable and known. Keep the system advisory when it lacks authority, reliable readback or a safe recovery state.
The card also supports incremental release. Start with observation and draft-only action. Measure whether the loop reduces uncertainty and reaches correct terminal states. Add one consequential operation only after its grant, duplicate handling, readback and containment have been tested. Expand by operation and task slice, not by a vague label such as “full autonomy”.
Optional technical depth
Why an open-loop programme can still look goal-directed
A precomputed trajectory inherits a goal from the procedure that produced it. In a deterministic environment, executing that trajectory can reach the goal without feedback. Observers may reasonably call the whole actuator-programme assembly an agent under a broad definition. The narrower closure test used here reserves minimal situated agency for systems whose current observations can change current action.
This is a terminological choice with an experimental purpose. It separates action execution from adaptation to present conditions. The stable-world negative control ensures the choice is not smuggled into a performance claim. The open-loop system can be preferable even when it receives the narrower label.
A causal statement of closure
Let ht denote history before action. Closure requires that for some admissible actions a and a', the distribution of a later decision-relevant observation differs under intervention: P(ot+k | do(at=a), ht) ≠ P(ot+k | do(at=a'), ht) for some k ≥ 1.
The condition does not require the agent to know this distribution or calculate causal effects. It says only that the action channel is not decorative. In deterministic settings the distributions collapse to different successor observations. In stochastic settings the intervention changes probabilities rather than guaranteeing one result.
Glossary
- Action contract
- The allowed action set plus argument, scope, effect, budget and recovery constraints enforced around a policy.
- Agent closure
- The causal return path by which an action can change a later decision-relevant observation through a specified environment.
- Environment
- The state and transition process outside the chosen agent boundary, including disturbances and other actors.
- Functional agency
- Goal-conditioned, persistent observation-action interaction, without an implied claim about experience, freedom or moral status.
- Policy
- A rule, algorithm or learned procedure that selects a proposed action from current task state.
- Readback
- An observation after attempted action used to establish resulting state rather than infer it from intent or transport response.
- Task state
- The typed, persistent information required to choose, constrain, resume and adjudicate transitions in one episode.
References and source ledger
- Wooldridge, M. and Jennings, N. R. (1995). Intelligent agents: theory and practice. The Knowledge Engineering Review, 10(2), 115-152. Primary research synthesis.
- Franklin, S. and Graesser, A. (1996). Is It an agent, or just a program?: A taxonomy for autonomous agents. In Intelligent Agents III, 21-35. Primary conceptual work.
- Nilsson, N. J. (1994). Teleo-Reactive Programs for Agent Control. Journal of Artificial Intelligence Research, 1, 139-158. Primary mechanism source.
- Brooks, R. A. (1991). Intelligence without representation. Artificial Intelligence, 47(1-3), 139-159. Primary situated-robotics argument.
- Sutton, R. S. and Barto, A. G. Reinforcement Learning: An Introduction. Official author-hosted text and agent-environment formalism.
- Kaelbling, L. P., Littman, M. L. and Cassandra, A. R. (1998). Planning and acting in partially observable stochastic domains. Artificial Intelligence, 101(1-2), 99-134. Primary formal treatment.
- Bratman, M. E., Israel, D. J. and Pollack, M. E. (1988). Plans and resource-bounded practical reasoning. Computational Intelligence, 4(3), 349-355. Primary architecture source.
- Yao, S. et al. (2022). ReAct: Synergizing Reasoning and Acting in Language Models. Primary research on interleaved reasoning and action.
- Ahn, M. et al. (2022). Do As I Can, Not As I Say: Grounding Language in Robotic Affordances. Primary research on language relevance and feasible skills.
- Zhou, S. et al. (2023). WebArena: A Realistic Web Environment for Building Autonomous Agents. Primary interactive benchmark.
- Liu, X. et al. (2023). AgentBench: Evaluating LLMs as Agents. Primary multi-environment benchmark.
- Xie, T. et al. (2024). OSWorld: Benchmarking Multimodal Agents for Open-Ended Tasks in Real Computer Environments. Primary environment and execution-based benchmark.
- Yao, S. et al. (2024). τ-bench: A Benchmark for Tool-Agent-User Interaction in Real-World Domains. Primary benchmark with policies, users, tools and database-state evaluation.
- Yuan, M. et al. (2026). OSWorld 2.0: Benchmarking Computer Use Agents on Long-Horizon Real-World Tasks. Recent primary long-horizon benchmark.
- National Institute of Standards and Technology. AI Risk Management Framework. Official risk framework.
- Rose, S. et al. (2020). NIST SP 800-207: Zero Trust Architecture. Official security architecture guidance used here only for resource-specific authority principles.
sequenceDiagram participant Q as Question participant M as Mechanism participant E as Evidence participant D as Decision Q->>M: The cable that changes the category M->>E: Environment E-->>M: compare rival explanations E->>D: Build the loop before buying the brain D-->>Q: revise scope or act
Build the loop before buying the brain
The transition from programme to agent is smaller and more exact than the current vocabulary suggests. Add a named environment, current observation, persistent task state, a goal condition, an action policy, a real effect channel, readback and explicit stopping. When action can alter later decision-relevant observation through that environment, the computation has entered a closed, goal-conditioned causal loop.
Planning, learning and language can make the policy far more capable. They can also make failure harder to diagnose. None creates the environmental boundary, guarantees the truth of observations, grants authority, makes an effect unique or proves completion. Those responsibilities remain visible when the agent is drawn as a system rather than personified as a model.
The decision this changes is architectural. Do not begin with “Which agent framework should we use?” Begin with “Which uncertainty requires interaction, which state can answer it, which action may follow, and what observation will prove the result?” If the answer is known in advance, use a programme or workflow. If the next valid action depends on acquired evidence, build the smallest closed loop that can obtain it. If the loop can create consequential effects, surround it with identity, policy, evidence and recovery before authority expands.
A well-designed agent is therefore not the programme that appears most independent. It is the system whose dependence on environment, state, goals, permissions and outcome evidence has been made explicit. Agency begins when computation is causally answerable to a world. Trust begins when that loop is bounded.