Effect Types for AI Agents
2026-05-22
Agent tools should be typed by their side effects: pure reads, idempotent writes, irreversible actions, human-gated transitions, and compensatable operations.
Tool lists are too flat. search_docs, send_email, refund_payment, and delete_user_data are not the same kind of operation. They differ by reversibility, idempotency, required approval, audit level, and blast radius.
An AI agent runtime should understand those differences before the model asks for anything. This is basically an effect system for tool use.
Start by naming the effects. The categories can be domain-specific, but the split between pure reads, idempotent writes, irreversible actions, and compensatable actions is already useful.
from enum import StrEnum
from pydantic import BaseModel
class Effect(StrEnum):
pure_read = "pure_read"
idempotent_write = "idempotent_write"
compensatable_write = "compensatable_write"
irreversible_write = "irreversible_write"
class ToolPolicy(BaseModel):
name: str
effect: Effect
requires_approval: bool
max_uses_per_run: int
audit_payload: bool
Once tools have effect types, the planner can be constrained mechanically. A low-trust autonomous run may use pure reads and idempotent writes. A human-approved run may use compensatable writes. Irreversible writes may require a separate approval transition and a human-readable diff.
def allowed(policy: ToolPolicy, autonomy_level: int) -> bool:
if policy.effect == Effect.pure_read:
return True
if policy.effect == Effect.idempotent_write:
return autonomy_level >= 2
if policy.effect == Effect.compensatable_write:
return autonomy_level >= 3 and policy.requires_approval
return False
This is stronger than telling the model “be careful.” The model can still propose a dangerous action, but the runtime has an enforceable type-level reason to reject it.
Idempotency should be part of the tool contract, not an implementation detail hidden in one adapter. If a tool performs a write, the runtime should know how duplicate calls are detected.
class ToolInvocation(BaseModel):
run_id: str
tool: str
arguments_digest: str
idempotency_key: str
def idempotency_key(run_id: str, tool: str, arguments_digest: str) -> str:
return f"{run_id}:{tool}:{arguments_digest}"
Compensatable actions need a reverse edge. The reverse action may not perfectly undo the world, but the system should know what remediation exists.
CREATE TABLE tool_effects (
tool TEXT PRIMARY KEY,
effect TEXT NOT NULL,
compensating_tool TEXT,
CHECK (
effect != 'compensatable_write'
OR compensating_tool IS NOT NULL
)
);
The workflow engine can now ask more intelligent questions than “what tool did the model choose?” It can ask: what effect class is this, is it allowed in the current state, is it idempotent, does it need approval, and do we know how to compensate?
This makes agent behavior inspectable. You can graph a run by effect classes: reads, writes, approvals, compensations. You can detect prompt drift when a model suddenly requests more irreversible actions. You can block entire classes of action during incidents.
The model is not the authority over side effects. The workflow runtime is. An agent with effect types is less magical, but much more interesting: it can act in the real world while remaining constrained by software invariants that humans can understand.