Agents should not call tools

Most enterprise agent stacks are built the same way. You give a model a list of tools. It picks one. The tool runs. If you are careful, you put an allowlist in front of the tool list and you write the calls to a log. Then you write a governance policy and hold a review before launch.

I think that default is wrong. And it is wrong at the cheapest place to fix it, which is the interface between the agent and the business.

A tool call carries almost no information

Look at what the runtime actually receives when a model calls send_email(to, subject, body).

It gets three strings. It does not get why the email is being sent. It does not get whose authority it is being sent under. It does not get whether this is the first email today or the four-hundredth. It does not get whether the action can be undone.

Adding an allowlist does not fix this. An allowlist answers one question, is this tool permitted at all, and it answers with a yes or a no. That is the wrong question. The question you need answered at run time is longer: may this agent do this thing, for this stated reason, right now, given everything it has already done today, and can I show someone later why the answer was what it was.

You cannot answer that from a function signature. So the signature has to change.

What a capability carries that a tool does not

I have been building a small reference implementation of this, in the open, at github.com/mindfulcto-labs/agentic-os. It is not a product and it is not a framework to build on. It is about as small as a working example can be while still being honest. What follows is how it works, because arguing about this in the abstract goes nowhere.

In that implementation a capability is a declared unit of business action. Each one carries:

  • a name and a description
  • an input schema and an output schema, both typed
  • the permission scopes it requires
  • the purposes it may be used for
  • a risk tier
  • a rate limit, in calls per run

The risk tiers are read, act and spend. Read observes state. Act changes business state, so it schedules something, sends something, updates something. Spend commits money or creates a financial obligation. Three tiers, not ten, because a tier you cannot explain to a business owner in one sentence will be assigned wrongly.

The permission side is symmetrical. A principal, which is an agent or a human, holds grants. A grant is scopes multiplied by purposes, up to a maximum risk tier. On top of that sit daily budgets: so many act invocations per day, so many spend invocations per day. Read is unbudgeted, because counting reads discourages the grounding you want the agent to do.

Purpose is the part people skip, and it is the part I care about most. UK GDPR has purpose limitation in it. Most agent runtimes have no place to put a purpose at all, so the idea dies in the policy document. Here the purpose travels with the request, and a grant that covers billing does not cover service_delivery.

A run, with the denials in it

The demo domain is a small field services company: customers, sites, work orders, technicians, invoices. A dispatch agent plans six steps. Four are allowed and two are refused.

step 1: lookup_customer        (purpose=service_delivery) [allowed] 0.3 ms
step 2: list_open_work_orders  (purpose=service_delivery) [allowed] 0.2 ms
step 3: schedule_technician    (purpose=service_delivery) [allowed] 0.4 ms
step 4: send_notification      (purpose=service_delivery) [allowed] 0.2 ms
step 5: draft_invoice          (purpose=service_delivery) [DENIED]
        reason: scopes ['invoices:write'] not granted for purpose 'service_delivery'
step 6: draft_invoice          (purpose=billing)          [DENIED]
        reason: policy 'no-spend-after-denial' objects: spend capability
                'draft_invoice' blocked: 1 denial(s) earlier in this run

summary  4 allowed, 2 denied, 0 error(s)

The two denials are the whole argument.

Step 5 is refused on purpose. The agent is allowed to draft invoices. It is not allowed to draft them while claiming a service delivery purpose. An allowlist would have waved this through, because draft_invoice is on the list.

Step 6 is refused on policy. The purpose is now correct, so the agent has learned. But a policy says that an agent already denied once in a run may not go on to spend money. That check ran inside the loop, before the call, not in a review afterwards.

Every allowed step passed eight checks in a fixed order: the capability exists, the purpose is granted, the scopes are granted, the risk tier is within the grant, the per-run rate limit has headroom, no policy predicate objects, the daily budget has headroom, and the approval gate approves for act and spend tiers. Each check is recorded whether it passed or failed. agentic-os trace <run-id> --verbose prints all of them.

Two design choices in there are worth naming.

First, a denial is a first-class result, not an exception. The run continues and the refusal is written down with its reason. That matters more than it sounds. If denials are exceptions you only ever see what your agents did. If denials are results you also see what they tried to do, which is the more useful signal by a distance.

Second, policies are small Python predicates loaded from a policies.yaml file and evaluated before every invocation. no_spend_after_denial is nine lines. max_act_steps caps mutations per run. They read the run state so far, so they can reason about the shape of the run and not only about the next call. Every run writes a JSON trace to runs/, and every principal gets an append-only JSONL log of run started, allowed, denied, run finished.

Why the ontology comes first

Before any of that, the demo answers a question by walking a typed graph:

customer cust-001 'Harbour Bakery'
<- belongs_to  site site-001 '12 Quay Street, Whitby'
   <- raised_at work_order wo-001 [open/high] 'Oven proofing cabinet not holding temperature.'

The agent did not infer that from a prompt. It walked belongs_to and raised_at relations between typed entities.

This is the part that took me longest to understand, and I got it from the engineers and data people I worked with on the Fulfilled-by-Maersk platform rather than from any paper. We spent a lot of effort on controls before we had agreement on what the words meant, and the controls kept sliding off. You cannot limit data by purpose until the business agrees what the data is. You cannot say an agent may act on a work order until work order means one thing. Governance is a semantics problem wearing a security costume. The ontology is where the agreement is written down, and it is what lets the agent and the governor argue about the same world.

What this costs, and where I might be wrong

I would rather write this section than have someone else write it for me.

Someone has to model the business. A capability catalogue and a domain ontology do not appear. Writing them is slow and it is political, because it forces people to agree on definitions they have been comfortably disagreeing about for years. In the reference implementation this is five capabilities and sixteen entities. In a real estate it is a programme.

Three risk tiers are coarse. A read that returns one customer record and a read that aggregates ten million are the same tier here. That is clearly not right. I have not found a tiering scheme that stays both correct and explainable, and I suspect the honest answer involves a data sensitivity axis alongside the action axis.

Purpose is self-declared. The planner states the purpose. A compromised or persuaded model can state whichever purpose it likes. What purpose actually buys you is narrower than it first appears: it does not stop a model lying about intent, it stops the model acting outside the grants that intent maps to. That is still worth having, but I have seen the stronger claim made and it is not true.

Governance that is not the fast path gets routed around. If the governed call takes 200 ms and a raw HTTP call takes 20 ms, engineers will make the raw call, and they will be right to. In the demo the eight checks cost under a millisecond because everything is in process. A real deployment does network hops for identity and policy. I do not yet know what that number has to stay under, and it is probably the single most important unknown here.

For low-risk work this is over-built. If your agent reads internal documentation and drafts a summary, an allowlist and good logging are enough, and the capability catalogue is cost with no return. The pattern earns its keep where actions are consequential and where somebody will eventually ask you to prove what happened. I have no evidence that it is cheaper in total for anything else, and I would not claim it.

What I do not know yet

  • Whether models plan better or worse against capability descriptions than against raw tool schemas. Capability descriptions are richer, which could help grounding or could just eat context. This is measurable and I have not measured it.
  • How a capability catalogue ages, and who owns it after the team that wrote it moves on.
  • How budgets should behave when a parent agent spawns children. Budget has to be conserved across the tree, and copying a number into a sub-agent’s prompt is not conservation. The reference implementation does not solve this.
  • What an approval interface should look like when a human is approving forty things a day. Approval fatigue turns any gate into a rubber stamp, and I have not seen a design I believe in.
  • Whether the modelling cost falls now that models can help write the ontology, or whether that just produces plausible ontologies that are subtly wrong.

Go and look

The reference implementation is at github.com/mindfulcto-labs/agentic-os, Apache-2.0, Python 3.11 or newer. It runs offline with a deterministic planner, so you do not need an API key to read every governance decision.

pip install -e .
agentic-os demo                      # the run above, end to end
agentic-os capabilities list          # what a governed capability declares
agentic-os grants show dispatch-agent # what a grant looks like
agentic-os trace <run-id> --verbose   # all eight checks, pass and fail
pytest                                # 11 eval fixtures asserting governance outcomes

If you want to argue with the design, the two files to read are runtime/governor.py, which is the eight checks in order, and evals/fixtures/, where each YAML file scripts a run and asserts which steps were denied and why. Change the governance behaviour and the fixtures are where it breaks.

It is version 0.1.0 and one maintainer. The interfaces will move. The pattern is the part I am asking you to look at.

Views here are my own and not affiliated with my current or former employers.