CyberJabka

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.

Agent workflow with typed effects
Agent workflow with typed effects

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.

Validated tool request before side effect
Validated tool request before side effect

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.

Back to blog